YOLO Vision 2026:

Предобработка с ускорением на GPU с использованием NVIDIA DALI#

При развёртывании моделей Ultralytics YOLO в рабочей среде предобработка часто становится узким местом. Хотя TensorRT может выполнять вывод модели всего за несколько миллисекунд, предобработка на CPU (изменение размера, добавление отступов, нормализация) может занимать 2–10 мс на изображение, особенно при высоком разрешении. NVIDIA DALI (библиотека загрузки данных) решает эту проблему, перенося весь конвейер предобработки на 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, чтобы воспроизвести предобработку YOLO с letterbox на GPU.
  • Интегрируешь с Ultralytics? Передай результат DALI как torch.Tensor в model.predict() — Ultralytics автоматически пропустит предобработку изображения.
  • Развёртываешь с Triton? Используй бэкенд DALI вместе с ансамблем TensorRT, чтобы полностью исключить предобработку на CPU.

Зачем использовать DALI для предобработки YOLO#

В типичном конвейере вывода YOLO этапы предобработки выполняются на CPU:

  1. Декодирование изображения (JPEG/PNG)
  2. Изменение размера с сохранением соотношения сторон
  3. Добавление отступов до целевого размера (letterbox)
  4. Нормализация значений пикселей с [0, 255] до [0, 1]
  5. Преобразование формата из HWC в CHW

С DALI все эти операции выполняются на GPU, что устраняет узкое место на CPU. Это особенно полезно, когда:

СценарийПочему DALI помогает
Быстрый вывод на GPUДвижки TensorRT с выводом менее чем за миллисекунду делают предобработку на CPU основной статьёй затрат
Входные данные высокого разрешенияВидеопотоки 1080p и 4K требуют ресурсоёмных операций изменения размера
Большие размеры батчаСерверный вывод с параллельной обработкой множества изображений
Ограниченное количество ядер CPUПериферийные устройства, такие как NVIDIA Jetson, или плотные серверы с GPU и небольшим количеством ядер CPU на каждый GPU

Предварительные требования#

Только Linux

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Изменение размера с letterboxcv2.resizefn.resize(mode="not_larger")
2Центрированное добавление отступовcv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + нормализация /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

Операция letterbox сохраняет соотношение сторон следующим образом:

  1. Вычисление масштаба: r = min(target_h / h, target_w / w)
  2. Изменение размера до (round(w * r), round(h * r))
  3. Заполнение оставшегося пространства серым цветом (114) до достижения целевого размера
  4. Центрирование изображения, чтобы отступы равномерно распределялись с обеих сторон

Конвейер DALI для YOLO#

Рекомендуемый конвейер DALI воспроизводит стандартное поведение Ultralytics LetterBox(center=True), используемое при стандартном выводе YOLO.

Центрированный конвейер (рекомендуется, соответствует Ultralytics LetterBox)#

Эта версия в точности воспроизводит стандартную предобработку Ultralytics с центрированными отступами, соответствующими LetterBox(center=True):

Конвейер DALI с центрированными отступами (рекомендуется)
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
Когда достаточно `fn.pad`?

Если тебе не требуется точное соответствие LetterBox(center=True), этап добавления отступов можно упростить, используя fn.pad(...) вместо fn.crop(..., out_of_bounds_policy="pad"). Этот вариант добавляет отступы только справа и снизу, что может быть приемлемо для пользовательских конвейеров развёртывания, но он не будет в точности соответствовать стандартному центрированному поведению letterbox в Ultralytics.

Зачем использовать `fn.crop` для центрированных отступов?

Оператор DALI fn.pad добавляет отступы только справа и снизу. Чтобы получить центрированные отступы, соответствующие 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. Если этого не сделать, возникнут небольшие численные различия, которые могут повлиять на точность модели.

Запуск конвейера#

Создание и запуск конвейера 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 не имеет доступа к исходным размерам изображения, координаты рамок обнаруженных объектов возвращаются в пространстве 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.

Предсказание DALI + Ultralytics
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 или пользовательских библиотек захвата:

Конвейер DALI для предобработки видеопотока
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 output

Triton 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:

Сериализация конвейера DALI для Triton
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#

Экспорт модели YOLO в движок TensorRT
from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.export(
    format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
)  # NMS-free (N, 300, 6); TensorRT >= 8.5
# 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"
      }
    }
  ]
}
Как работает сопоставление в ансамбле

Ансамбль соединяет модели через виртуальные имена тензоров. Значение "preprocessed_image" для output_map на шаге DALI соответствует значению "preprocessed_image" для input_map на шаге TensorRT. Это произвольные имена, связывающие выход одного шага со входом следующего; им не нужно совпадать с внутренними именами тензоров какой-либо модели.

Шаг 4: Отправка запросов на вывод#

Почему `tritonclient`, а не `YOLO('http://...')`?

В Ultralytics есть встроенная поддержка Triton, которая автоматически обрабатывает пред- и постобработку. Однако она не будет работать с ансамблем DALI, поскольку YOLO() отправляет предварительно обработанный тензор float32, тогда как ансамбль ожидает необработанные байты JPEG. Используй tritonclient напрямую для ансамблей DALI, а встроенную интеграцию — для стандартных развёртываний без DALI.

Отправка изображений в ансамбль Triton
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 батча изображений JPEG дополни все закодированные массивы байтов до одинаковой длины (максимального количества байтов в батче). 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

Часто задаваемые вопросы#

  • Преимущество зависит от твоего конвейера. Когда вывод на 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 — для детального управления на уровне операторов в пользовательском коде инференса.

Комментарии