Предобработка с ускорением на GPU с помощью NVIDIA DALI#
При развертывании моделей Ultralytics YOLO в production предобработка часто становится узким местом. TensorRT может выполнять инференс модели всего за несколько миллисекунд, тогда как предобработка на CPU (изменение размера, добавление полей, нормализация) может занимать 2–10 мс на изображение, особенно при высоком разрешении. NVIDIA DALI (библиотека загрузки данных) решает эту проблему, перенося весь конвейер предобработки на GPU.
В этом руководстве ты узнаешь, как создавать конвейеры DALI, точно повторяющие предобработку Ultralytics YOLO, интегрировать их с model.predict(), обрабатывать видеопотоки и выполнять сквозное развертывание с помощью Triton Inference Server.
Это руководство предназначено для инженеров, развертывающих модели YOLO в production-средах, где измерения показывают, что предобработка на 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, чтобы повторить предобработку YOLO с letterbox на GPU. - Интегрируешь с Ultralytics? Передай результат DALI как
torch.Tensorвmodel.predict()— Ultralytics автоматически пропустит предобработку изображения. - Развертываешь с Triton? Используй бэкенд DALI вместе с ансамблем TensorRT, чтобы выполнять предобработку без участия CPU.
Зачем использовать DALI для предобработки YOLO#
В типичном конвейере инференса YOLO этапы предобработки выполняются на CPU:
- Декодирование изображения (JPEG/PNG)
- Изменение размера с сохранением соотношения сторон
- Дополнить до целевого размера (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), # Целевой размер
center=True, # Расположить изображение по центру (добавить одинаковые поля с обеих сторон)
stride=32, # Выравнивание по шагу
padding_value=114, # Серые поля (114, 114, 114)
)Полный конвейер предобработки в ultralytics/engine/predictor.py выполняет следующие шаги:
| Шаг | Операция | Функция CPU | Эквивалент в DALI |
|---|---|---|---|
| 1 | Изменение размера с сохранением пропорций | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Поля по центру | 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 повторяет поведение Ultralytics по умолчанию LetterBox(center=True), используемое при стандартном инференсе 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.
"""
# Чтение и декодирование изображений на 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)
# Изменение размера с сохранением соотношения сторон
resized = fn.resize(
images,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False, # Соответствие cv2.INTER_LINEAR (без сглаживания)
)
# Центрирование с помощью fn.crop и out_of_bounds_policy
# Если размер обрезки больше размера изображения, fn.crop размещает изображение по центру и симметрично добавляет поля
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114, # Значение заполнения полей YOLO
)
# Нормализация и изменение порядка измерений
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"). Этот вариант добавляет поля только справа и снизу. Для пользовательских конвейеров развёртывания этого может быть достаточно, но он не будет точно соответствовать стандартному поведению Ultralytics с центрированием изображения.
Оператор fn.pad в DALI добавляет поля только справа и снизу. Чтобы центрировать поля (как в LetterBox(center=True) Ultralytics), используй fn.crop с out_of_bounds_policy="pad". При стандартных crop_pos_x=0.5 и crop_pos_y=0.5 изображение автоматически размещается по центру с симметричными полями.
Оператор fn.resize в DALI по умолчанию включает сглаживание (antialias=True), тогда как OpenCV cv2.resize с INTER_LINEAR сглаживание не применяет. Чтобы соответствовать конвейеру CPU, всегда задавай antialias=False в DALI. Если этого не сделать, возникнут небольшие численные расхождения, которые могут повлиять на точность модели.
Запуск конвейера#
# 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 не знает исходных размеров изображения, поэтому координаты рамок детекций возвращаются в пространстве изображения 640×640 с полями letterbox. Чтобы преобразовать их обратно в координаты исходного изображения, используй scale_boxes: эта функция учитывает ту же логику округления, что и LetterBox:
from ultralytics.utils.ops import scale_boxes
# рамки: тензор формы (N, 4) в формате xyxy, координаты в пространстве 640x640 с полями letterbox
# Масштабировать рамки из изображения с полями letterbox (640, 640) обратно до исходного размера (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."""
# Внешний источник для передачи кадров из OpenCV, GStreamer и т. д.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# Перенос на GPU и предобработка
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."""
# Входные данные: необработанные байты закодированного изображения из 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
# Сериализовать конвейер в репозиторий моделей
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")Шаг 2: Экспортируй YOLO в TensorRT#
from pathlib import Path
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
engine_path = model.export(
format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
) # Без NMS (N, 300, 6); TensorRT >= 8.5
# Ultralytics добавляет заголовок с метаданными в файлы .engine; удали его, чтобы Triton мог загрузить исходный план TensorRT
with open(engine_path, "rb") as f:
meta_len = int.from_bytes(f.read(4), byteorder="little") # длина заголовка JSON с метаданными
f.seek(4 + meta_len)
plan = f.read()
Path("model_repository/yolo_trt/1").mkdir(parents=True, exist_ok=True)
Path("model_repository/yolo_trt/1/model.plan").write_bytes(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. Для ансамблей DALI используй напрямую tritonclient, а для стандартных развёртываний без 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 for the nms=False export)
detections = detections[0] # First image
detections = detections[detections[:, 4] > 0.25] # Confidence threshold
print(f"Detected {len(detections)} objects")При отправке в Triton пакета изображений JPEG дополни все массивы закодированных байтов до одинаковой длины (максимального числа байтов в пакете). Для входного тензора Triton требует, чтобы формы элементов пакета были одинаковыми.
Поддерживаемые задачи#
Предобработка DALI работает со всеми задачами YOLO, использующими стандартный конвейер LetterBox:
| Задача | Поддерживается | Примечания |
|---|---|---|
| Обнаружение | ✅ | Стандартная предобработка с letterbox |
| Сегментация экземпляров | ✅ | Та же предобработка, что и для детекции |
| Семантическая сегментация | ✅ | Та же предобработка изображения, что и для детекции |
| Оценка глубины | ✅ | Та же предобработка изображения, что и для детекции |
| Классификация | ❌ | Используются преобразования torchvision (центральное кадрирование), а не letterbox |
| Оценка позы | ✅ | Та же предобработка, что и для детекции |
| Детекция объектов с ориентированными рамками (OBB) | ✅ | Та же предобработка, что и для детекции |
Ограничения#
- Только Linux: DALI не поддерживает Windows и macOS
- Требуется GPU NVIDIA: режима работы только на 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 используй группу экземпляров по умолчанию.
Часто задаваемые вопросы#
Преимущество зависит от твоего конвейера. Если инференс на 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. Из-за вычислений на GPU и CPU могут возникать незначительные различия с плавающей точкой (< 0.001), но они не оказывают измеримого влияния на точность детекции.CV-CUDA — ещё одна библиотека NVIDIA для ускоренной на GPU обработки изображений. Она позволяет управлять каждым оператором отдельно (как в OpenCV, но на GPU), а не использовать конвейерный подход DALI.
cvcuda.copymakeborder()в CV-CUDA поддерживает явное задание отступов для каждой стороны, что упрощает центрирование letterbox. Выбирай DALI для конвейерных сценариев (особенно с Triton), а CV-CUDA — для точечного управления операторами в собственном коде инференса.