Overview
Trong bài viết lần trước , mình đã đề cập đến những thông tin cơ bản nhất của NVIDIA DALI, bao gồm khái niệm tổng quan về thư viện này, mục đích sử dụng cũng như những ưu điểm nổi bật mà nó mang lại trong quá trình xử lý dữ liệu hình ảnh và video cho các mô hình học sâu. Bên cạnh đó, mình cũng đã giới thiệu sơ lược về cách tích hợp DALI vào pipeline huấn luyện, giúp tăng tốc độ tiền xử lý dữ liệu và giảm thiểu tắc nghẽn do CPU gây ra. Tuy nhiên, đó mới chỉ là phần khởi đầu để làm quen với thư viện mạnh mẽ này. Ở bài viết lần này, mình sẽ thử trải nghiệm cách xây dựng pipeline với DALI, cụ thể là cách tạo các phép biến đổi dữ liệu và kiểm tra khả năng của nó.
Nhắc lại 1 chút về NVIDIA DALI, NVIDIA Data Loading Library (DALI) là một thư viện GPU-accelerated cho data loading và data preprocessing để tăng tốc các ứng dụng học sâu. Thư viện cung cấp các building blocks được tối ưu hóa cao để tải và xử lý dữ liệu hình ảnh, video và âm thanh. Nó có thể được sử dụng như một sự thay thế cho data loaders và data iterators trong các deep learning frameworks.
Nguồn hình ảnh: NVIDIA
Experiment
Trong bài viết này, ta sẽ thử trải nghiệm so sánh hiệu suất của Pytorch Dataloader với NVIDIA DALI xem thư viện GPU-accelerated có thực sự cải thiện không nhé.
Installation
Trước khi bắt tay vào testing thì việc cài đặt môi trường là điều tối kị rồi. Về mặt môi trường sử dụng thì ta vẫn sẽ lựa chọn cái phổ biến nhất thôi là môi trường cũng như ngôn ngữ lập trình Python, bạn có thể cài đặt thông qua conda hoặc pip đều được, bạn sẽ cần cài đặt 1 số thư viện cần thiết (có thể hơi thừa :v) như dưới đây:
torch
torchvision
albumentations
tqdm
Pillow
opencv-python
numpy
Bài viết này là về DALI đúng không, vì vậy cái quan trọng nhất vẫn là DALI python package, việc cài đặt vô cùng đơn giản nhưng cũng cần đảm bảo Prerequisites nhé, bao gồm:
- Linux x64.
- NVIDIA Driver hỗ trợ CUDA 11.0 hoặc mới hơn (i.e. 450.80.02 hoặc driver mới hơn).
- CUDA Toolkit: cho DALI dựa trên CUDA 12, toolkit được linked dynamically và cần phải được cài đặt. Đối với CUDA 11, nó là optional.
Cách thứ nhất thì bạn có thể cài thông qua NGC Containers, khi mà DALI đã được preinstalled trên trong TensorFlow, Pytorch containers trên NVIDA GPU Cloud NGC.
Cách thứ 2 thì đơn giản hơn hẳn rồi, cài thông qua pip luôn, nhưng mà cũng nên kiểm tra CUDA version trên máy của bạn:
# CUDA 11.0
pip install nvidia-dali-cuda110 # CUDA 12.0
pip install nvidia-dali-cuda120
Pytorch Dataloader
OK, vậy là xong phần cài đặt, hãy bắt tay vào thôi, trước tiên cứ thử nghiệm cái baseline trước đi, đó là xử lý dữ liệu với Pytorch Dataloader, trong phần này, mình sẽ sử dụng thư viện albumentations cho phần data augmentation.
Để xử lý dữ liệu thì trước tiên phải có dữ liệu đúng không, Các bạn có thể download public dataset bất kỳ cho ảnh, trong bài này mình sẽ dùng bộ dữ liệu tập test Airbus Ship Detection (download sẵn từ bài Triton Inference Server thì tận dụng thôi :v) chứa 15.6k ảnh, số lượng này cũng đủ lớn cho phần testing này rồi.
Vậy thì ta bắt tay vào vào viết code cho Dataset thôi nhỉ:
import os
import numpy as np
from PIL import Image
from torch.utils.data import Dataset import albumentations as A
from albumentations import Compose
from albumentations.pytorch.transforms import ToTensorV2 class AirbusDataset(Dataset): def __init__( self, data_dir: str = "/path/to/your/ship", ) -> None: super().__init__() self.data_dir = data_dir self.filenames = self._get_file_list() self.transform = Compose( [ A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.05, rotate_limit=15, p=0.5), A.RandomBrightnessContrast(p=0.5), A.Resize(height=768, width=768), A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2(), ] ) def __len__(self): return len(self.filenames) def __getitem__(self, index): image = self.filenames[index] image = np.array(Image.open(image).convert("RGB"), dtype=np.uint8) transformed = self.transform(image=image) image = transformed["image"] return image, 0 # Giá trị 0 ở đây mình để đại diện cho label thôi chứ cũng không cần thiết trong inference lắm def _get_file_list(self, exts={'.jpg', '.jpeg', '.png'}): image_paths = [] for root, _, files in os.walk(self.data_dir): for file in files: if os.path.splitext(file)[1].lower() in exts: image_paths.append(os.path.join(root, file)) return image_paths airbus_dataset = AirbusDataset()
Sau khi có Dataset
rồi thì mình sẽ tạo 1 cái Dataloader
cơ bản thôi nhỉ:
airbus_dataloader = DataLoader( dataset=airbus_dataset, batch_size=16, num_workers=4,
)
Ở đây mình sẽ chỉ mô phỏng thời gian xử lý dữ liệu thôi
import time def loss_func(pred, y): pass def model(x): pass def backward(loss, model): pass start_time = time.time()
for batch in tqdm(airbus_dataloader): x, y = batch[0].to("cuda"), batch[1] # torch.Size([16, 3, 768, 768]) # For fun pred = model(x) loss = loss_func(pred, y) backward(loss, model) pass
end_time = time.time() total_time = end_time - start_time
print(f"Total time: {total_time:.4f} seconds")
Sau khi chạy đoạn code ở trên, thì thời gian xử lý sẽ khoảng:
# Total time: 143.2727 seconds 6.81it/s
Khoảng thời gian xử lý như vậy cũng không quá lâu nhỉ, vậy thì mình sẽ thử chuyển sang DALI Pipeline xem sao nhé.
DALI Pipeline
Nhắc lại khái niệm về DALI pipeline, trong DALI, các tác vụ xử lý dữ liệu thông qua Pipeline. Pipeline đóng gói data preprocessing graph và execution engine. Trong bài viết này mình sẽ định nghĩa Pipeline thông qua decorator @pipeline_def
. Dưới đây sẽ là pipeline tương ứng với phần transforms
trong mục Pytorch Loader ở trên để đảm bảo có sự công bằng:
Trước tiên là cần import các thư viện cần thiết trước:
import nvidia.dali.fn as fn
from nvidia.dali import pipeline_def, types
from nvidia.dali.pipeline import Pipeline
from nvidia.dali.plugin.pytorch import DALIGenericIterator
from nvidia.dali.plugin.pytorch import LastBatchPolicy
Sau đó, ta sẽ xác định DALI pipeline cho phần data processing:
@pipeline_def(batch_size=16, num_threads=4, device_id=0)
def dali_pipeline(file_list_path): jpegs, labels = fn.readers.file(file_list=file_list_path, random_shuffle=True, name="Reader") images = fn.decoders.image(jpegs, device="mixed", output_type=types.RGB) images = fn.rotate(images, angle=fn.random.uniform(range=(-5, 5)), fill_value=0) # Brightness Contrast images = fn.brightness_contrast( images, brightness=fn.random.uniform(range=(0.8, 1.2)), contrast=fn.random.uniform(range=(0.8, 1.2)) ) # Resize images = fn.resize(images, resize_x=768, resize_y=768) # Normalization images = fn.crop_mirror_normalize( images, dtype=types.FLOAT, output_layout="CHW", crop=(768, 768), mean=[0.485 * 255, 0.456 * 255, 0.406 * 255], std=[0.229 * 255, 0.224 * 255, 0.225 * 255], ) return images, labels
Sau khi tạo xong DALI Pipeline, ta cần đưa nó vào 1 iterator và trả lại cho ta kết quả. Tiếp theo mình sẽ sử dụng DALI Iterator cơ bản, bạn hoàn toàn có thể custom lại bằng việc kế thừa để phục vụ cho mục đích riêng của bản thân khi sử dụng, nhưng ở đây mình thấy không cần thiết nên thôi nhé :v.
start_time = time.time() data_iterator = DALIGenericIterator( [dali_pipeline(file_list_path=file_list_path)], ['data', 'label'], last_batch_policy=LastBatchPolicy.PARTIAL, reader_name='Reader'
) for i, data in tqdm(enumerate(data_iterator)): x, y = data[0]['data'], data[0]['label'] pred = model(x) loss = loss_func(pred, y) backward(loss, model)
end_time = time.time()
total_time = end_time - start_time
print(f"Total time: {total_time:.4f} seconds")
Bạn có thể custom DALI Custom Iterator như dưới đây chẳng hạn:
class DALICustomIterator(DALIGenericIterator): def __init__(self, pipelines, output_map, auto_reset=True, last_batch_padded=False): super(DALICustomIterator, self).__init__(pipelines, output_map, auto_reset, last_batch_padded) def __len__(self): return math.ceil(self._size / self.batch_size) def __next__(self): if self._first_batch is not None: batch = self._first_batch self._first_batch = None return batch feed = super().__next__() data = feed[0]['data'] return data
Sau khi chạy thử thì kết quả như dưới đây, ta có thể thấy tổng thời gian cho data processing đã giảm đáng kể đồng thời iteration/s cũng tăng mạnh, đúng như kỳ vọng của việc sử dụng khả năng xử lý mạnh mẽ của GPU, cũng không bất ngờ lắm :v.
# Total time: 22.3596 seconds 48.13it/s
Như việc mình có đề cập ở trên thì việc tích hợp NVIDIA DALI vào trong quá trình training là thực sự cần thiết nếu như bạn có GPU mạnh mẽ như CPU đang gặp bottleneck ảnh hưởng làm chậm quá trình training, việc tích hợp cũng không khó khăn, hiểu đơn giản là chỉ cần custom cái Pipeline với DALI Iterator như Pytorch Dataloader là được. NVIDIA DALI cũng cung cấp các Tutorials cho các phần tích hợp training bao gồm cả TensorFlow, Pytorch, Pytorch Lightning, các bạn có thể tham khảo trực tiếp trên Documentation hoặc trên Github nhé.
Summary
Vậy là sau bài introduction về NVIDIA DALI chỉ có lý thuyết suông, thì bài viết này mình đã thử trải nghiệm hiệu suất của NVIDIA DALI khi xử lý trên GPU cũng như đánh giá sự khác biệt về thời gian xử lý và thử xây dựng pipeline nho nhỏ cho phần xử lý dữ liệu với DALI. Nếu có thời gian thì trong bài viết tiếp tho mình sẽ thử ensemble DALI backend với Triton Inference Server nhé (do mình cũng được 1 anh comment ở bài Triton Inference Server về pipeline này nên thử xem sao).