Skip to main content

Ускоренная на GPU предобработка с помощью NVIDIA DALI

Введение

При развертывании моделей Ultralytics YOLO моделей в продакшене, предобработка часто становится «бутылочным горлышком». В то время как TensorRT может выполнять инференс модели производительность вывода всего за несколько миллисекунд, предобработка на CPU (изменение размера, добавление полей, нормализация) может занимать 2-10 мс на изображение, особенно при высоком разрешении. NVIDIA DALI (библиотека загрузки данных) решает эту проблему, перенося весь конвейер предобработки на GPU.

Это руководство поможет тебе создать DALI-конвейеры, которые в точности повторяют предобработку Ultralytics YOLO, интегрировать их с model.predict(), обрабатывать видеопотоки и выполнять развертывание «от начала до конца» с помощью Triton Inference Server.

Для кого это руководство?

Это руководство предназначено для инженеров, развертывающих YOLO модели в продакшене, где предобработка на CPU является измеримым узким местом — как правило, это TensorRT развертывания на NVIDIA GPU, высокопроизводительные видеоконвейеры или 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 из процесса предобработки.

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

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

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

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

СценарийПочему DALI помогает
Быстрый GPU-инференсTensorRT движки с субмиллисекундным инференсом делают предобработку на CPU основными затратами
Входные данные высокого разрешения1080p и 4K видеопотоки требуют затратных операций по изменению размера
Большие batch sizesСерверный инференс, обрабатывающий множество изображений параллельно
Ограниченные CPU-ресурсыEdge-устройства, такие как 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-cuda120

Требования:

  • NVIDIA GPU (вычислительная способность 5.0+ / Maxwell или новее)
  • CUDA 11.0+ или 12.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-эквивалент
1Letterbox-ресайзcv2.resizefn.resize(mode="not_larger")
2Центрированное дополнение (padding)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

Используй приведенный ниже центрированный конвейер в качестве базового эталона. Он соответствует поведению Ultralytics LetterBox(center=True), которое используется при стандартном инференсе YOLO.

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

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

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 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 добавляет отступы только по правому и нижнему краям. Чтобы получить центрированные отступы (соответствующие Ultralytics LetterBox(center=True)), используй fn.crop с out_of_bounds_policy="pad". С параметром по умолчанию crop_pos_x=0.5 и crop_pos_y=0.5 изображение автоматически центрируется с симметричными отступами.

Несоответствие сглаживания (Antialias)

Оператор DALI fn.resize включает сглаживание по умолчанию (antialias=True), в то время как cv2.resize с INTER_LINEAR из не этого не делает. Всегда устанавливай antialias=False в DALI, чтобы соответствовать CPU-конвейеру. Пропуск этого шага вызывает незначительные численные различия, которые могут повлиять на model accuracy.

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

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

# 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 конвейер для предобработки видеопотока
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 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
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")

Шаг 2: Экспортируй YOLO в TensorRT

Экспорт 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.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: Отправь запросы на инференс

!!! 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.
Отправка изображений в ансамбль 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 — 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 изображений

При отправке пакета JPEG изображений в Triton выровняй все закодированные массивы байтов по одной длине (максимальное количество байтов в пакете). Triton требует однородных форм пакетов для входного тензора.

Поддерживаемые задачи

Предобработка DALI работает со всеми задачами YOLO, использующими стандартный LetterBox конвейер:

ЗадачаПоддерживаетсяПримечания
DetectionСтандартная предобработка letterbox
SegmentationТакая же предобработка, как для детекции
Оценка позыТакая же предобработка, как для детекции
Ориентированная детекция (OBB)Такая же предобработка, как для детекции
ClassificationИспользует torchvision преобразования (center crop), а не letterbox

Ограничения

  • только Linux: DALI не поддерживает Windows или macOS
  • Требуется NVIDIA GPU: Нет резервного варианта только для CPU
  • Статический конвейер: Структура конвейера определяется во время сборки и не может меняться динамически
  • fn.pad только справа/снизу: Используй fn.crop с out_of_bounds_policy="pad" для центрированного заполнения
  • Нет режима rect: DALI конвейеры создают выходы фиксированного размера (например, 640×640). Режим auto=True rect, который создает выходы переменного размера (например, 384×640), не поддерживается. Заметь, что хотя TensorRT поддерживает динамические формы входа, конвейер DALI фиксированного размера естественным образом сочетается с движком фиксированного размера для максимальной пропускной способности
  • Память при нескольких экземплярах: Использование instance_group с count > 1 в Triton может вызвать высокое потребление памяти. Используй группу экземпляров по умолчанию для модели DALI

FAQ

Как предобработка DALI соотносится со скоростью CPU-предобработки?

Преимущество зависит от твоего конвейера. Когда GPU-инференс уже быстрый с TensorRT, CPU-предобработка со скоростью 2-10 мс может стать основным расходом времени. DALI устраняет это узкое место, выполняя предобработку на GPU. Наибольший прирост наблюдается при использовании входов высокого разрешения (1080p, 4K), больших batch sizes и систем с ограниченным числом ядер CPU на каждый GPU.

Могу ли я использовать DALI с моделями PyTorch (а не только TensorRT)?

Да. Используй DALIGenericIterator, чтобы получить предварительно обработанные torch.Tensor выходы, затем передай их в model.predict(). Однако преимущество в производительности максимально с TensorRT моделями, где инференс уже очень быстр, а CPU-предобработка становится узким местом.

на датасете COCO8-seg. Аргументы не требуются, так как модель fn.pad и fn.crop для заполнения?

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

Производит ли DALI попиксельно идентичные результаты по сравнению с CPU-предобработкой?

Почти идентичные. Установи antialias=False в fn.resize, чтобы соответствовать cv2.INTER_LINEAR в OpenCV. Незначительные различия в числах с плавающей запятой (< 0.001) могут возникать из-за разницы арифметики GPU и CPU, но они не оказывают заметного влияния на детекцию точности.

Как насчет CV-CUDA в качестве альтернативы DALI?

CV-CUDA — это еще одна библиотека NVIDIA для ускоренной на GPU обработки видео. Она предоставляет управление на уровне операторов (как OpenCV, но на GPU), а не конвейерный подход, как в DALI. cvcuda.copymakeborder() в CV-CUDA поддерживает явное заполнение для каждой стороны, что делает центрированный letterbox простым. Выбирай DALI для конвейерных рабочих процессов (особенно с Triton) и CV-CUDA для детального управления на уровне операторов в твоем собственном коде инференса.

Комментарии