Ultralytics YOLO27: Запишись в список ожидания.
Get Started

Предобработка с ускорением на 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:

  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),  # Целевой размер
    center=True,  # Расположить изображение по центру (добавить одинаковые поля с обеих сторон)
    stride=32,  # Выравнивание по шагу
    padding_value=114,  # Серые поля (114, 114, 114)
)

Полный конвейер предобработки в ultralytics/engine/predictor.py выполняет следующие шаги:

ШагОперацияФункция CPUЭквивалент в DALI
1Изменение размера с сохранением пропорцийcv2.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(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.
    """
    # Чтение и декодирование изображений на 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
Когда достаточно `fn.pad`?

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

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

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

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

Создание и запуск конвейера 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.

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."""
    # Внешний источник для передачи кадров из 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 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."""
    # Входные данные: необработанные байты закодированного изображения из 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#

Экспорт модели 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: отправка запросов на инференс#

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

В Ultralytics есть встроенная поддержка Triton, которая автоматически выполняет предобработку и постобработку. Однако она не работает с ансамблем DALI, потому что YOLO() передаёт тензор float32 после предобработки, а ансамбль ожидает исходные байты JPEG. Для ансамблей DALI используй напрямую tritonclient, а для стандартных развёртываний без 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
  • Требуется 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 — для точечного управления операторами в собственном коде инференса.

Комментарии