# PyTorch 效能懶人包 [TOC] ## 1. 減少 I/O 時間 ### 盡量不要從硬碟讀,能放 RAM 就放 RAM Slow :-1: ```python class Dataset(torch.utils.data.Dataset): def __init__(self, data_root): self.image_paths = glob.glob(os.path.join(data_root, '*.png')) def __len__(self): return len(self.image_paths) def __getitem__(self, index): return Image.open(self.image_path[index]).copy() ``` Fast :+1: ```python class Dataset(torch.utils.data.Dataset): def __init__(self, data_root): image_paths = glob.glob(os.path.join(data_root, '*.png')) self.images = [Image.open(path).copy() for path in image_paths] def __len__(self): return len(self.images) def __getitem__(self, index): return self.images[index] ``` - 只用 `Image.open(...)` 不會把圖片檔案關掉,開太多系統會爆炸,所以要記得 `.copy()` - 因為加上 `copy()` 會 dereference `ImageFile` 物件,去 call destructor 把檔案關掉 - 記得估計 dataset 放進 RAM 會不會塞爆 - 如果 dataset 很大會塞爆,要確認 swap 夠大,放進 RAM 的大小如果超過 RAM+swap 整台 server 會卡到需要重開,如果是樓上的 server 如 Salmonella 和 COVID-19 還要申請才能上去重開。 - 看 swap 大小及用量可用 `htop`,詳細紀錄在 slack 的 server-maintain channal - 部分從硬碟讀也是個策略 ### LRU Cache 假如:資料超大,不想把所有圖片都讀進 RAM,但是有兩個 patch 是用同一張圖挖出來的,又不想讀兩次圖(你的 dataset class 可能會即時讀圖,因為全部讀進來太大了) ```python from functools import lru_cache @lru_cache(1000) def read_image(path): return Image.open(path).copy() ``` ### 如果用到 DataLoader 記得設 `pin_memory=True` 好慢 :-1: ```python loader = DataLoader(dataset) for xs, ys in iter(loader): xs, ys = xs.cuda(), ys.cuda() ... ``` 好快 :+1: ```python loader = DataLoader(dataset, pin_memory=True) for xs, ys in iter(loader): xs, ys = xs.cuda(), ys.cuda() ... ``` - 有用 CUDA 就設成 True。這個選項代表 使用 page-locked memory,讓 GPU 可以直接拿,不然 GPU 取得 tensor 資料時還要再自己 allocate 暫時性的 page-locked memory,會很慢。 https://devblogs.nvidia.com/how-optimize-data-transfers-cuda-cc/ > Host (CPU) data allocations are pageable by default. The GPU cannot access data directly from pageable host memory, so when a data transfer from pageable host memory to device memory is invoked, the CUDA driver must first allocate a temporary page-locked, or “pinned”, host array, copy the host data to the pinned array, and then transfer the data from the pinned array to device memory - `tensor.cuda(non_blocking=True)` 可以異步 allocate 空間,有機會加速 https://stackoverflow.com/questions/55563376/pytorch-how-does-pin-memory-works-in-dataloader ### `/dev/shm` 這個目錄底下是掛在記憶體內的檔案系統,所以讀寫比硬碟快很多,如果記憶體夠大,你可以把 dataset 目錄整個放進去 `/dev/shm`。 ```shell mkdir /dev/shm/dataset cp dataset/*.png /dev/shm/dataset ln -s /dev/shm/dataset dataset_in_memory ``` ## 2. 減少 CPU 運算時間 ### DataLoader workers - 如果 `__getitem__` 有處理資料,DataLoader 可開多點 worker - 如果沒有就不要,因為建立 worker 也會花時間 - 看 CPU worker 數量可以用 `nproc` ```python class Dataset(torch.utils.data.Dataset): ... def __getitem__(self, index): image = self.images[index] image = crop_image(image) image = rotate_image(image) # very CPU-bound if angle % 90 != 0 image = resize_image(image) image = normalize_image(image) image = binarize_image(image) return image dataset = Dataset('./data') ``` 超慢 :-1: ```python loader = torch.utils.data.DataLoader(dataset) ``` 好快 :+1: ```python loader = torch.utils.data.DataLoader(dataset, num_workers=48) ``` ### 多用 torch.tensor 或者 np.array 來操作資料 - Python 原生物件慢到不行,把資料轉成 `np.ndarray` 再做運算,跟用 C 寫的速度差不多! - PyTorch 的 CPU tensor 跟 Numpy ndarray 速度差不多 - 如果一次對多個值操作,`np.ndarray` 的 function 都很快,能用就用吧 ```python py_list = list(range(1000)) np_array = np.arange(1000) max(py_list) # 14.9 µs np_array.max() # 2.88 µs # ~5x speedup sum(a * b for a, b in zip(py_list, py_list)) # 88.3 µs np.dot(np_array, np_array) # 1.89 µs # ~47x speedup ``` ### 東西先處理好存起來,訓練的時候不要浪費 CPU 資源 可以用 pickle 存任意 Python object ```python import pickle data = preprocess_dataset() with open('data.pkl', 'wb') as f: pickle.dump(data, f) with open('data.pkl', 'rb') as f: data = pickle.load(f) ``` 可以用 numpy.save 存 np.array ```python import numpy as np data = np.random.randn(100, 100) np.save('data.npy', data) data = np.load('data.npy') ``` 還有其他類似的東西可以快速存 list, dict, str 之類的物件 - torch.save / torch.load - bson - cbor - msgpack - 效能比較: https://medium.com/@shmulikamar/python-serialization-benchmarks-8e5bb700530b ## 3. 增加 GPU 運算效率 ### Batch 塞大坨一點 - GPU 就是拿來平行運算的 - 有時候 batch=32 跟 batch=64 跑的時間根本差不多,可以自己試試看 - 只要 memory 大小允許,batch 就設大一點,gradient 也會比較穩定 ### 一張卡跑多個 model - 在記憶體夠的情況下,如果 GPU util 很低 - 多跑幾個 model 比較不浪費 GPU 的資源,提高 GPU util ### CUDNN ```python from torch.backends import cudnn cudnn.benchmark = True # fast training ``` ### 用 apex.amp 自動將 model 轉換成混合浮點精度 NVIDIA 的套件,幫你把模型轉成混合精度給 CUDA 算: https://github.com/NVIDIA/apex AMP = Automatic Mixed Precision #### 安裝 1. 安裝時的環境必須要有 nvcc 和 g++,他要現場編譯。 2. 順便安裝 `libgomp` 3. Apex 會用到 PyTorch 的 package,因此 PyTorch 的 CUDA 版本要和系統的一樣,你可以用 `nvidia-smi` 來看現在的 driver 版本,如果 PyTorch 會動就是那個版本。 4. 如果編譯時出現版本不符,要下 `export CUDA_HOME=/usr/local/cuda-10.2` 和 `export PATH="/usr/local/cuda-10.2/bin:$PATH"`。`10.2` 可要改成你的 cuda 版本,如果沒有這個資料夾應該是因為你沒有 `sudo apt install cuda-toolkit-102`,`102` 就是 `10.2` 的意思。 ```bash mkdir -p /tmp/$USER cd /tmp/$USER git clone https://github.com/NVIDIA/apex cd apex pip install -v --disable-pip-version-check --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" ./ ``` - 備註:PyTorch 1.5.0 有自己做的 amp:`torch.cuda.amp` https://pytorch.org/docs/stable/amp.html https://pytorch.org/docs/stable/notes/amp_examples.html#gradient-scaling-examples #### 片語 ```python # import from apex import amp # initialize model and optimizer model, optimizer = amp.initialize(model, optimizer, opt_level='O1') model, optimizer = amp.initialize(model, optimizer, opt_level='O2') [model1, model2], [optimizer1, optimizer2] = amp.initialize([model1, model2], [optimizer1, optimizer2]) # rescale loss when backward with amp.scale_loss(loss, optimizer) as scaled_loss: scaled_loss.backward() ``` - Loss scaling 可讓過小的值不至於在低精度時消失。 https://docs.nvidia.com/deeplearning/performance/mixed-precision-training/index.html - Advanced Usage: https://nvidia.github.io/apex/advanced.html #### 重點 - 一定要記得 `scale_loss` 不然會 train 爛 - `opt_level` 有四種 `O0`、`O1`、`O2`、`O3` - `O0` 沒任何效果,`O1`、`O2` 差不多,`O3` 很常爛掉(例如出現 NaN) - PyTorch 偵測 NaN: https://pytorch.org/docs/stable/autograd.html#torch.autograd.detect_anomaly 或者 ```python assert not torch.isnan(grad).any(), 'gradient contains NaN' ``` - 通常只會用 `O1` 和 `O2`,兩者只是實作上的差異而已。一般來說建議用 `O1`。 - Model 越大越複雜,或者 batch 或 input 越大,加速效果越明顯。(反過來說,小的 model 用 apex 有可能沒用或者變慢) #### 範例 `# IMPORTANT PART` 是用 apex 要加的地方。 ```python import torch from torch.optim import Adam from torch.utils.data import DataLoader from torchvision.models.densenet import densenet121 from torchvision.datasets import CIFAR10 import torchvision.transforms as transforms # IMPORTANT PART from apex import amp # IMPORTANT PART torch.cuda.set_device(0) # see: https://github.com/NVIDIA/apex/issues/319 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) dataset = CIFAR10(root='/tmp/cifar10', train=True, download=True, transform=transform) loader = DataLoader(dataset, batch_size=128, shuffle=True, num_workers=16, pin_memory=True) net = densenet121(num_classes=len(dataset.classes)).train().cuda() opt = Adam(net.parameters()) # IMPORTANT PART net, opt = amp.initialize(net, opt) # default is O1 loss_fn = torch.nn.CrossEntropyLoss() for xs, ys in iter(loader): xs = xs.cuda() ys = ys.cuda() logits = net(xs) loss = loss_fn(logits, ys) print('loss:', loss.item()) opt.zero_grad() # IMPORTANT PART with amp.scale_loss(loss, opt) as scaled_loss: scaled_loss.backward() opt.step() ``` #### 儲存與載入 - 轉成 apex 模型之後的 `state_dict` 跟原本不相容 - Checkpoint 也要順便存 amp state! - Save: ```python net = Net().cuda() opt = Opt(net.parameters()) net, opt = amp.initialize(net, opt, opt_level='O1') torch.save({ 'net': net.state_dict(), 'opt': opt.state_dict(), 'amp': amp.state_dict(), # IMPORTANT PART }, 'checkpoint.pt') ``` - Load: ```python net = Net().cuda() opt = Opt(net.parameters()) # using the same opt_level is recommended! net, opt = amp.initialize(net, opt, opt_level='O1') state = torch.load('checkpoint.pt') net.load_state_dict(state['net']) opt.load_state_dict(state['opt']) amp.load_state_dict(state['amp']) # IMPORTANT PART ``` ## 4. 其他 ### `nn.DataParallel` - 可以用多張 GPU 同時計算,一次跑大一點的 batch size - https://pytorch.org/tutorials/beginner/blitz/data_parallel_tutorial.html ### Multiprocessing - 訓練 Hogwild, A3C... 時很重要 - https://pytorch.org/docs/stable/notes/multiprocessing.html ### Distributed - 一次用多台 server 跑 - https://pytorch.org/tutorials/intermediate/dist_tuto.html ### `torch.utils.bottleneck` - https://pytorch.org/docs/stable/bottleneck.html ### PyTorch JIT (TorchScript) - 把一系列運算編譯成靜態圖,可以存起來丟給其其他 frontend 跑。 - 存成計算圖的好處是我們可以一次丟比較多運算給 CUDA 跑,這會讓運算能比較好的被最佳化。 - 這篇提到可以用 TorchScript 自己寫 RNN Cell: https://pytorch.org/blog/optimizing-cuda-rnn-with-torchscript/ ### TensorFlow VS PyTorch VS MXNet https://medium.com/syncedreview/tensorflow-pytorch-or-mxnet-a-comprehensive-evaluation-on-nlp-cv-tasks-with-titan-rtx-cdf816fc3935