Ускоренная обработка данных с помощью GPU и NVIDIA DALI#
При развертывании моделей Ultralytics YOLO в продакшене предварительная обработка часто становится узким местом. В то время как TensorRT может выполнять инференс модели всего за несколько миллисекунд, предварительная обработка на базе CPU (изменение размера, добавление отступов, нормализация) может занимать 2–10 мс на изображение, особенно при высоких разрешениях. NVIDIA DALI (Data Loading Library) решает эту проблему, перенося весь конвейер предварительной обработки на GPU.
В этом руководстве показано, как создавать конвейеры DALI, которые точно воспроизводят предварительную обработку Ultralytics YOLO, интегрировать их с model.predict(), обрабатывать видеопотоки и выполнять сквозное развертывание с помощью Triton Inference Server.
Это руководство предназначено для инженеров, развертывающих модели YOLO в производственных средах, где предварительная обработка на CPU является узким местом (как правило, при развертывании TensorRT на GPU NVIDIA, в высокопроизводительных видеоконвейерах или настройках Triton Inference Server). Если ты выполняешь стандартный инференс с помощью model.predict() и у тебя нет проблем с узким местом предварительной обработки, конвейер CPU по умолчанию работает отлично.
- Создаешь конвейер DALI? Используй
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeдля воспроизведения предварительной обработки letterbox в YOLO на GPU. - Интегрируешь с Ultralytics? Передай вывод DALI в качестве
torch.Tensorвmodel.predict()— Ultralytics пропускает предварительную обработку изображений автоматически. - Развертываешь с Triton? Используй бэкенд DALI вместе с ансамблем TensorRT для исключения предварительной обработки на CPU.
Почему стоит использовать DALI для предварительной обработки YOLO#
В обычном конвейере инференса YOLO шаги предварительной обработки выполняются на CPU:
- Декодирование изображения (JPEG/PNG)
- Изменение размера с сохранением соотношения сторон
- Дополнение (padding) до целевого размера (letterbox)
- Нормализуй значения пикселей от
[0, 255]до[0, 1] - Преобразование формата данных из HWC в CHW
С DALI все эти операции выполняются на GPU, что устраняет «узкое место» CPU. Это особенно полезно, когда:
| Сценарий | Почему DALI помогает |
|---|---|
| Быстрый инференс на GPU | Движки TensorRT с инференсом менее чем за миллисекунду делают предварительную обработку на CPU основной статьей затрат |
| Входные данные высокого разрешения | Видеопотоки 1080p и 4K требуют дорогостоящих операций изменения размера |
| Большие размеры батчей | Серверный инференс, обрабатывающий множество изображений параллельно |
| Ограниченное количество ядер CPU | Периферийные устройства, такие как NVIDIA Jetson, или плотные серверы GPU с небольшим количеством ядер CPU на один GPU |
Предварительные требования#
NVIDIA DALI поддерживает только Linux. Он недоступен на Windows или macOS.
Установи необходимые пакеты:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130Требования:
- GPU NVIDIA (вычислительная способность 5.0+ / Maxwell или новее)
- CUDA 11.0+, 12.0+ или 13.0+
- Python 3.10–3.14
- Операционная система Linux
Понимание предварительной обработки YOLO#
Прежде чем создавать конвейер DALI, стоит точно понять, что делает Ultralytics во время предварительной обработки. Ключевым классом является LetterBox в ultralytics/data/augment.py:
from ultralytics.data.augment import LetterBox
letterbox = LetterBox(
new_shape=(640, 640), # Target size
center=True, # Center the image (pad equally on both sides)
stride=32, # Stride alignment
padding_value=114, # Gray padding (114, 114, 114)
)Полный конвейер предварительной обработки в ultralytics/engine/predictor.py выполняет следующие шаги:
| Шаг | Операция | Функция CPU | Эквивалент DALI |
|---|---|---|---|
| 1 | Letterbox resize (изменение размера с сохранением пропорций) | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Центрированное дополнение (padding) | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + нормализация /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
Операция letterbox сохраняет соотношение сторон путем:
- Вычисление масштаба:
r = min(target_h / h, target_w / w) - Изменение размера до
(round(w * r), round(h * r)) - Заполнение оставшегося пространства серым цветом (
114) для достижения целевого размера - Центрирования изображения так, чтобы отступы распределялись поровну с обеих сторон
Конвейер DALI для YOLO#
Рекомендуемый конвейер DALI повторяет поведение LetterBox(center=True) по умолчанию в Ultralytics, которое используется в стандартном инференсе YOLO.
Центрированный конвейер (рекомендуется, соответствует LetterBox в Ultralytics)#
Эта версия точно воспроизводит предварительную обработку Ultralytics по умолчанию с центрированными отступами, что соответствует LetterBox(center=True):
from nvidia import dali
from nvidia.dali import fn, types
@dali.pipeline_def(batch_size=8, num_threads=4, device_id=0)
def yolo_dali_pipeline_centered(image_dir, target_size=640):
"""DALI pipeline replicating YOLO preprocessing with centered padding.
Matches Ultralytics LetterBox(center=True) behavior exactly.
"""
# Read and decode images on GPU
jpegs, _ = fn.readers.file(file_root=image_dir, random_shuffle=False, name="Reader")
images = fn.decoders.image(jpegs, device="mixed", output_type=types.RGB)
# Aspect-ratio-preserving resize
resized = fn.resize(
images,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False, # Match cv2.INTER_LINEAR (no antialiasing)
)
# Centered padding using fn.crop with out_of_bounds_policy
# When crop size > image size, fn.crop centers the image and pads symmetrically
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114, # YOLO padding value
)
# Normalize and convert layout
output = fn.crop_mirror_normalize(
padded,
dtype=types.FLOAT,
output_layout="CHW",
mean=[0.0, 0.0, 0.0],
std=[255.0, 255.0, 255.0],
)
return outputЕсли тебе не требуется точное соответствие LetterBox(center=True), ты можешь упростить шаг добавления отступов, используя fn.pad(...) вместо fn.crop(..., out_of_bounds_policy="pad"). Этот вариант добавляет отступы только к правому и нижнему краям, что может быть приемлемо для пользовательских конвейеров развертывания, но оно не будет точно соответствовать поведению letterbox по центру в Ultralytics по умолчанию.
Оператор fn.pad в DALI добавляет отступы только к правому и нижнему краям. Чтобы получить центрированные отступы (соответствующие Ultralytics LetterBox(center=True)), используй fn.crop с out_of_bounds_policy="pad". С параметрами по умолчанию crop_pos_x=0.5 и crop_pos_y=0.5 изображение автоматически центрируется с симметричными отступами.
Оператор fn.resize в DALI по умолчанию включает сглаживание (antialias=True), в то время как cv2.resize с INTER_LINEAR в OpenCV не применяет сглаживание. Всегда устанавливай antialias=False в DALI, чтобы соответствовать конвейеру CPU. Опущение этого параметра приводит к незначительным числовым различиям, которые могут повлиять на точность модели.
Запуск конвейера#
# Build and run the pipeline
pipe = yolo_dali_pipeline_centered(image_dir="/path/to/images", target_size=640)
pipe.build()
# Get a batch of preprocessed images
(output,) = pipe.run()
# Convert to numpy or PyTorch tensors
batch_np = output.as_cpu().as_array() # Shape: (batch_size, 3, 640, 640)
print(f"Output shape: {batch_np.shape}, dtype: {batch_np.dtype}")
print(f"Value range: [{batch_np.min():.4f}, {batch_np.max():.4f}]")Использование DALI с Ultralytics Predict#
Ты можешь передать предварительно обработанный тензор PyTorch напрямую в model.predict(). Когда передается torch.Tensor, Ultralytics пропускает предварительную обработку изображения (letterbox, BGR→RGB, HWC→CHW и нормализацию /255) и выполняет только перенос на устройство и приведение типов перед отправкой в модель.
Поскольку в этом случае у Ultralytics нет доступа к исходным размерам изображения, координаты рамки обнаружения возвращаются в пространстве letterbox 640×640. Чтобы сопоставить их с исходными координатами изображения, используй scale_boxes, который обрабатывает точную логику округления, используемую LetterBox:
from ultralytics.utils.ops import scale_boxes
# boxes: tensor of shape (N, 4) in xyxy format, in 640x640 letterboxed coords
# Scale boxes from letterboxed (640, 640) back to original (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))Это применимо ко всем внешним путям предварительной обработки — прямому вводу тензоров, видеопотокам и развертыванию через Triton.
from nvidia.dali.plugin.pytorch import DALIGenericIterator
from ultralytics import YOLO
# Load model
model = YOLO("yolo26n.pt")
# Create DALI iterator
pipe = yolo_dali_pipeline_centered(image_dir="/path/to/images", target_size=640)
pipe.build()
dali_iter = DALIGenericIterator(pipe, ["images"], reader_name="Reader")
# Run inference with DALI-preprocessed tensors
for batch in dali_iter:
images = batch[0]["images"] # Already on GPU, shape (B, 3, 640, 640)
results = model.predict(images, verbose=False)
for result in results:
print(f"Detected {len(result.boxes)} objects")Когда ты передаешь torch.Tensor в model.predict(), шаг предварительной обработки изображения занимает ~0.004 мс (практически ноль) по сравнению с ~1-10 мс при предварительной обработке на CPU. Тензор должен быть в формате BCHW, float32 (или float16) и нормализован до [0, 1]. Ultralytics по-прежнему будет автоматически обрабатывать перенос на устройство и приведение типов.
DALI с видеопотоками#
Для обработки видео в реальном времени используй fn.external_source для подачи кадров из любого источника — OpenCV, GStreamer или пользовательских библиотек захвата:
from nvidia import dali
from nvidia.dali import fn, types
@dali.pipeline_def(batch_size=1, num_threads=4, device_id=0)
def yolo_video_pipeline(target_size=640):
"""DALI pipeline for processing video frames from external source."""
# External source for feeding frames from OpenCV, GStreamer, etc.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# Move to GPU and preprocess
frames_gpu = frames.gpu()
resized = fn.resize(
frames_gpu,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False,
)
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114,
)
output = fn.crop_mirror_normalize(
padded,
dtype=types.FLOAT,
output_layout="CHW",
mean=[0.0, 0.0, 0.0],
std=[255.0, 255.0, 255.0],
)
return outputTriton Inference Server с DALI#
Для развертывания в продакшене объедини предварительную обработку DALI с инференсом TensorRT в Triton Inference Server, используя ансамблевую модель. Это полностью исключает предварительную обработку на CPU: на вход поступают необработанные байты JPEG, на выходе получаются результаты обнаружения, причем все обрабатывается на GPU.
Структура репозитория моделей#
model_repository/
├── dali_preprocessing/
│ ├── 1/
│ │ └── model.dali
│ └── config.pbtxt
├── yolo_trt/
│ ├── 1/
│ │ └── model.plan
│ └── config.pbtxt
└── ensemble_dali_yolo/
├── 1/ # Empty directory (required by Triton)
└── config.pbtxtШаг 1: Создай конвейер DALI#
Сериализуй конвейер DALI для бэкенда Triton DALI:
from nvidia import dali
from nvidia.dali import fn, types
@dali.pipeline_def(batch_size=8, num_threads=4, device_id=0)
def triton_dali_pipeline():
"""DALI preprocessing pipeline for Triton deployment."""
# Input: raw encoded image bytes from Triton
images = fn.external_source(device="cpu", name="DALI_INPUT_0")
images = fn.decoders.image(images, device="mixed", output_type=types.RGB)
resized = fn.resize(
images,
resize_x=640,
resize_y=640,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False,
)
padded = fn.crop(
resized,
crop=(640, 640),
out_of_bounds_policy="pad",
fill_values=114,
)
output = fn.crop_mirror_normalize(
padded,
dtype=types.FLOAT,
output_layout="CHW",
mean=[0.0, 0.0, 0.0],
std=[255.0, 255.0, 255.0],
)
return output
# Serialize pipeline to model repository
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")Шаг 2: Экспортируй YOLO в TensorRT#
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.export(format="engine", imgsz=640, quantize=16, batch=8)
# Copy the .engine file to model_repository/yolo_trt/1/model.planШаг 3: Настрой Triton#
dali_preprocessing/config.pbtxt:
name: "dali_preprocessing"
backend: "dali"
max_batch_size: 8
input [
{
name: "DALI_INPUT_0"
data_type: TYPE_UINT8
dims: [ -1 ]
}
]
output [
{
name: "DALI_OUTPUT_0"
data_type: TYPE_FP32
dims: [ 3, 640, 640 ]
}
]yolo_trt/config.pbtxt:
name: "yolo_trt"
platform: "tensorrt_plan"
max_batch_size: 8
input [
{
name: "images"
data_type: TYPE_FP32
dims: [ 3, 640, 640 ]
}
]
output [
{
name: "output0"
data_type: TYPE_FP32
dims: [ 300, 6 ]
}
]ensemble_dali_yolo/config.pbtxt:
name: "ensemble_dali_yolo"
platform: "ensemble"
max_batch_size: 8
input [
{
name: "INPUT"
data_type: TYPE_UINT8
dims: [ -1 ]
}
]
output [
{
name: "OUTPUT"
data_type: TYPE_FP32
dims: [ 300, 6 ]
}
]
ensemble_scheduling {
step [
{
model_name: "dali_preprocessing"
model_version: -1
input_map {
key: "DALI_INPUT_0"
value: "INPUT"
}
output_map {
key: "DALI_OUTPUT_0"
value: "preprocessed_image"
}
},
{
model_name: "yolo_trt"
model_version: -1
input_map {
key: "images"
value: "preprocessed_image"
}
output_map {
key: "output0"
value: "OUTPUT"
}
}
]
}Ансамбль соединяет модели через виртуальные имена тензоров. Значение output_map "preprocessed_image" на шаге DALI соответствует значению input_map "preprocessed_image" на шаге TensorRT. Это произвольные имена, которые связывают вывод одного шага с вводом следующего — они не обязательно должны совпадать с внутренними именами тензоров какой-либо модели.
Шаг 4: Отправь запросы на инференс#
В Ultralytics есть встроенная поддержка Triton, которая автоматически обрабатывает предварительную и последующую обработку. Однако она не будет работать с ансамблем DALI, потому что YOLO() отправляет предварительно обработанный тензор float32, в то время как ансамбль ожидает необработанные байты JPEG. Используй tritonclient напрямую для ансамблей DALI и встроенную интеграцию для стандартных развертываний без DALI.
import numpy as np
import tritonclient.http as httpclient
client = httpclient.InferenceServerClient(url="localhost:8000")
# Load image as raw bytes (JPEG/PNG encoded)
image_data = np.fromfile("image.jpg", dtype="uint8")
image_data = np.expand_dims(image_data, axis=0) # Add batch dimension
# Create input
input_tensor = httpclient.InferInput("INPUT", image_data.shape, "UINT8")
input_tensor.set_data_from_numpy(image_data)
# Run inference through the ensemble
result = client.infer(model_name="ensemble_dali_yolo", inputs=[input_tensor])
detections = result.as_numpy("OUTPUT") # Shape: (1, 300, 6) -> [x1, y1, x2, y2, conf, class_id]
# Filter by confidence (no NMS needed — YOLO26 is end-to-end)
detections = detections[0] # First image
detections = detections[detections[:, 4] > 0.25] # Confidence threshold
print(f"Detected {len(detections)} objects")При отправке пакета изображений JPEG в Triton, дополни все кодированные массивы байтов до одинаковой длины (максимальное количество байтов в пакете). Для входного тензора Triton требуются однородные размеры пакета.
Поддерживаемые задачи#
Предварительная обработка DALI работает со всеми задачами YOLO, в которых используется стандартный конвейер LetterBox:
| Задача | Поддерживается | Примечания |
|---|---|---|
| Детектирование | ✅ | Стандартный препроцессинг letterbox |
| Сегментация экземпляров | ✅ | Тот же препроцессинг, что и в детекции |
| Семантическая сегментация | ✅ | Тот же препроцессинг изображений, что и в детекции |
| Классификация | ❌ | Использует трансформации torchvision (кроп по центру), а не letterbox |
| Оценка позы | ✅ | Тот же препроцессинг, что и в детекции |
| Ориентированное обнаружение (OBB) | ✅ | Тот же препроцессинг, что и в детекции |
Ограничения#
- Только для Linux: DALI не поддерживает Windows или macOS
- Требуется NVIDIA GPU: Нет резервного варианта только на CPU
- Статический конвейер: Структура конвейера определяется во время сборки и не может меняться динамически
fn.padтолько для право/нижнего отступа: Используйfn.cropсout_of_bounds_policy="pad"для центрированного отступа- Без режима rect: Конвейеры DALI создают выходные данные фиксированного размера (например, 640×640). Режим rect в
auto=True, который создает выходные данные переменного размера (например, 384×640), не поддерживается. Обрати внимание, что хотя TensorRT поддерживает динамические формы ввода, конвейер DALI фиксированного размера естественным образом сочетается с движком фиксированного размера для максимальной пропускной способности - Память при использовании нескольких экземпляров: Использование
instance_groupсcount> 1 в Triton может привести к высокому потреблению памяти. Используй группу экземпляров по умолчанию для модели DALI
FAQ#
Преимущество зависит от твоего конвейера. Когда инференс на GPU уже работает быстро с TensorRT, предварительная обработка на CPU за 2–10 мс может стать основной статьей затрат. DALI устраняет это узкое место, выполняя предварительную обработку на GPU. Наибольший прирост наблюдается при высоких разрешениях ввода (1080p, 4K), больших размерах батчей и на системах с ограниченным количеством ядер CPU на один GPU.
Да. Используй
DALIGenericIteratorдля получения предварительно обработанных результатовtorch.Tensor, а затем передай их вmodel.predict(). Тем не менее, наибольший прирост производительности достигается с моделями TensorRT, где инференс уже очень быстрый, а предварительная обработка на CPU становится узким местом.fn.padдобавляет отступы только к правому и нижнему краям.fn.cropсout_of_bounds_policy="pad"центрирует изображение и добавляет отступы симметрично со всех сторон, что соответствует поведениюLetterBox(center=True)в Ultralytics.Практически идентично. Установи
antialias=Falseвfn.resizeв соответствии сcv2.INTER_LINEARв OpenCV. Могут возникать небольшие различия с плавающей точкой (< 0.001) из-за арифметики GPU по сравнению с CPU, но они не оказывают измеримого влияния на точность обнаружения.CV-CUDA — это еще одна библиотека NVIDIA для ускоренного на GPU компьютерного зрения. Она предоставляет контроль на уровне отдельных операторов (как OpenCV, но на GPU), а не конвейерный подход DALI. Оператор
cvcuda.copymakeborder()в CV-CUDA поддерживает явные отступы с каждой стороны, что упрощает создание центрированного letterbox. Выбирай DALI для конвейерных рабочих процессов (особенно с Triton), и CV-CUDA для детального контроля на уровне операторов в пользовательском коде инференса.