Предварительная обработка с ускорением на 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 | Изменение размера с letterbox | 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 повторяет стандартное поведение 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.
"""
# Считать и декодировать изображения на 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"). Этот вариант добавляет поля только справа и снизу. Это может подойти для пользовательских конвейеров развертывания, но он не будет в точности соответствовать стандартному поведению letterbox с центрированием в 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), тогда как cv2.resize в OpenCV с INTER_LINEAR не применяет сглаживание. Чтобы конвейер соответствовал варианту на CPU, всегда устанавливай в DALI antialias=False. Если этого не сделать, появятся небольшие численные расхождения, которые могут повлиять на точность модели.
Запуск конвейера#
# 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: тензор формы (N, 4) в формате xyxy, координаты letterbox 640x640
# Масштабировать рамки из пространства 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#
Для развёртывания в production объедини предобработку 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")При отправке пакета изображений JPEG в Triton дополни все массивы закодированных байтов до одинаковой длины (максимального числа байтов в пакете). 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 — для детального управления отдельными операциями в пользовательском коде инференса.