YOLO Vision 2026:

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

При развертывании моделей 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.

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

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

  1. Декодирование изображения (JPEG/PNG)
  2. Изменение размера с сохранением соотношения сторон
  3. Дополнение (padding) до целевого размера (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
1Letterbox resize (изменение размера с сохранением пропорций)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#

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

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

Эта версия точно воспроизводит предварительную обработку 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` для центрированного дополнения?

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

Различия в сглаживании

Оператор fn.resize в DALI по умолчанию включает сглаживание (antialias=True), в то время как cv2.resize с INTER_LINEAR в OpenCV не применяет сглаживание. Всегда устанавливай 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)
# 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: Отправь запросы на инференс#

Почему `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 — 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:

ЗадачаПоддерживаетсяПримечания
ДетектированиеСтандартный препроцессинг 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

FAQ#

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

Комментарии