Link to this sectionУскоренная обработка данных с помощью NVIDIA DALI#
Link to this sectionВведение#
При развертывании моделей 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.
Link to this sectionПочему стоит использовать 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 |
Link to this sectionПредварительные требования#
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
Link to this sectionПонимание препроцессинга 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 | 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) до достижения целевого размера - Центрирования изображения так, чтобы заполнение было распределено поровну с обеих сторон
Link to this sectionКонвейер DALI для YOLO#
Используй приведенный ниже центрированный конвейер в качестве эталонного. Он соответствует поведению Ultralytics LetterBox(center=True), которое используется в стандартном инференсе YOLO.
Link to this sectionЦентрированный конвейер (рекомендуется, соответствует Ultralytics LetterBox)#
Эта версия в точности повторяет стандартный препроцессинг Ultralytics с центрированным заполнением, соответствуя LetterBox(center=True):
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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 добавляет заполнение только к правому и нижнему краям. Чтобы получить центрированное заполнение (соответствующее 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 не применяет сглаживание. Всегда устанавливай antialias=False в DALI, чтобы соответствовать конвейеру CPU. Отказ от этого приводит к едва заметным числовым различиям, которые могут повлиять на точность модели.
Link to this sectionЗапуск конвейера#
# 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}]")Link to this sectionИспользование DALI с предиктом Ultralytics#
Ты можешь передать предобработанный тензор 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 по-прежнему будет автоматически обрабатывать перенос на устройство и приведение типа данных.
Link to this sectionDALI с видеопотоками#
Для обработки видео в реальном времени используй fn.external_source для подачи кадров из любого источника — OpenCV, GStreamer или кастомных библиотек захвата:
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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 outputLink to this sectionTriton Inference Server с DALI#
Для промышленного развертывания объедини препроцессинг DALI с инференсом TensorRT в Triton Inference Server с использованием модели-ансамбля. Это полностью устраняет препроцессинг на CPU — на вход поступают «сырые» байты JPEG, а на выходе — детекции, при этом все обрабатывается на GPU.
Link to this sectionСтруктура репозитория моделей#
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.pbtxtLink to this sectionШаг 1: Создай конвейер DALI#
Сериализуй конвейер DALI для бэкенда Triton DALI:
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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")Link to this sectionШаг 2: Экспорт YOLO в TensorRT#
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.export(format="engine", imgsz=640, half=True, batch=8)
# Copy the .engine file to model_repository/yolo_trt/1/model.planLink to this sectionШаг 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. Это произвольные имена, которые связывают выход одного этапа с входом следующего — им не обязательно совпадать с внутренними именами тензоров какой-либо модели.
Link to this sectionШаг 4: Отправка запросов на логический вывод#
!!! info "Почему tritonclient, а не YOLO(\"http://...\")?"
Ultralytics has [built-in Triton support](triton-inference-server.md#running-inference) that handles pre/postprocessing automatically. However, it won't work with the DALI ensemble because `YOLO()` sends a preprocessed float32 tensor while the ensemble expects raw JPEG bytes. Use `tritonclient` directly for DALI ensembles, and the [built-in integration](triton-inference-server.md) for standard deployments without 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 требует однородных форм пакета (batch shapes) для входного тензора.
Link to this sectionПоддерживаемые задачи#
Препроцессинг DALI работает со всеми задачами YOLO, использующими стандартный конвейер LetterBox:
| Задача | Поддерживается | Примечания |
|---|---|---|
| Обнаружение | ✅ | Стандартный препроцессинг letterbox |
| Сегментация экземпляров | ✅ | Такой же препроцессинг, как при детекции |
| Семантическая сегментация | ✅ | Такой же препроцессинг изображений, как при детекции |
| Оценка позы | ✅ | Такой же препроцессинг, как при детекции |
| Ориентированная детекция (OBB) | ✅ | Такой же препроцессинг, как при детекции |
| Классификация | ❌ | Использует трансформации torchvision (center crop), а не letterbox |
Link to this sectionОграничения#
- Только Linux: DALI не поддерживает Windows или macOS
- Требуется NVIDIA GPU: Нет резервного варианта только для CPU
- Статический конвейер: Структура конвейера определяется во время сборки и не может меняться динамически
fn.padработает только справа/снизу: Используйfn.cropсout_of_bounds_policy="pad"для центрированного дополнения (padding)- Без режима rect: Конвейеры DALI создают выходные данные фиксированного размера (например, 640×640). Режим rect
auto=True, создающий выходные данные переменного размера (например, 384×640), не поддерживается. Обрати внимание, что хотя TensorRT поддерживает динамические формы входа, конвейер DALI с фиксированным размером лучше сочетается с механизмом (engine) фиксированного размера для достижения максимальной пропускной способности - Память при нескольких экземплярах: Использование
instance_groupсcount> 1 в Triton может привести к высокому потреблению памяти. Используй группу экземпляров по умолчанию для модели DALI
Link to this sectionFAQ#
Link to this sectionКак препроцессинг DALI соотносится со скоростью препроцессинга на CPU?#
Преимущество зависит от твоего конвейера. Когда логический вывод на GPU уже работает быстро с помощью TensorRT, препроцессинг на CPU за 2-10 мс может стать основным узким местом. DALI устраняет это, выполняя препроцессинг на GPU. Наибольший прирост заметен при входах с высоким разрешением (1080p, 4K), больших размерах пакетов и системах с ограниченным количеством ядер CPU на один GPU.
Link to this sectionМогу ли я использовать DALI с моделями PyTorch (не только TensorRT)?#
Да. Используй DALIGenericIterator для получения предобработанных выходов torch.Tensor, затем передай их в model.predict(). Однако выигрыш в производительности максимален при использовании моделей TensorRT, где логический вывод уже очень быстр, а препроцессинг на CPU становится узким местом.
Link to this sectionВ чем разница между fn.pad и fn.crop для дополнения?#
fn.pad добавляет дополнение только к правому и нижнему краям. fn.crop с out_of_bounds_policy="pad" центрирует изображение и добавляет дополнение симметрично со всех сторон, повторяя поведение LetterBox(center=True) в Ultralytics.
Link to this sectionДает ли DALI результаты, идентичные попиксельно препроцессингу на CPU?#
Почти идентичные. Установи antialias=False в fn.resize, чтобы соответствовать cv2.INTER_LINEAR в OpenCV. Небольшие различия в числах с плавающей запятой (< 0.001) могут возникать из-за разницы в арифметике GPU и CPU, но они не оказывают измеримого влияния на точность детекции.
Link to this sectionКак насчет CV-CUDA в качестве альтернативы DALI?#
CV-CUDA — это еще одна библиотека NVIDIA для ускоренного на GPU визуального процессинга. Она предоставляет контроль над отдельными операторами (как OpenCV, но на GPU), в отличие от конвейерного подхода DALI. cvcuda.copymakeborder() в CV-CUDA поддерживает явное дополнение для каждой стороны, что упрощает центрированный letterbox. Выбирай DALI для конвейерных рабочих процессов (особенно с Triton), а CV-CUDA — для мелкозернистого контроля на уровне операторов в кастомном коде логического вывода.