Meet YOLO26: next-gen vision AI.

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:

  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

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

Только 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

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

Link to this sectionКонвейер DALI для YOLO#

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

Link to this sectionЦентрированный конвейер (рекомендуется, соответствует 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` для центрированного заполнения?

Оператор fn.pad в DALI добавляет заполнение только к правому и нижнему краям. Чтобы получить центрированное заполнение (соответствующее LetterBox(center=True) от Ultralytics), используй fn.crop с out_of_bounds_policy="pad". При значениях по умолчанию crop_pos_x=0.5 и crop_pos_y=0.5 изображение автоматически центрируется с симметричным заполнением.

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

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

Link to this sectionЗапуск конвейера#

Создай и запусти конвейер 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}]")

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.

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 по-прежнему будет автоматически обрабатывать перенос на устройство и приведение типа данных.

Link to this sectionDALI с видеопотоками#

Для обработки видео в реальном времени используй 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

Link 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.pbtxt

Link to this sectionШаг 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")

Link to this sectionШаг 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

Link 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.
Отправка изображений в ансамбль 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 требует однородных форм пакета (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 — для мелкозернистого контроля на уровне операторов в кастомном коде логического вывода.

Комментарии