Перейти к содержанию

Предварительная обработка GPU с помощью NVIDIA

Введение

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

В этом руководстве подробно описано, как создавать конвейеры DALI, точно повторяющиеYOLO Ultralytics YOLO , и интегрировать их с model.predict(), обработка видеопотоков и развертывание сквозных решений с помощью Triton Inference Server.

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

Данное руководство предназначено для инженеров, развертывающих YOLO в производственных средах, где CPU является выявленным узким местом — как правило, TensorRT развертывание на 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 автоматически Ultralytics этап предварительной обработки изображений.
  • Развертываете с помощью Triton? Используйте бэкэнд DALI с TensorRT дляCPU .

Почему для YOLO используется DALI

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

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

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

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

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

Только для Linux

NVIDIA поддерживается только в Linux. В Windows и macOS эта функция недоступна.

Установите необходимые пакеты:

pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda110

Требования:

  • GPU NVIDIA GPU вычислительная мощность 5.0+ / архитектура Maxwell или более поздней версии)
  • CUDA .0+ или 12.0+
  • Python .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Изменение размера почтового ящика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])

Операция «почтовый ящик» сохраняет соотношение сторон следующим образом:

  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(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"). В этом варианте заполняются только справа и внизу края, что может быть приемлемо для пользовательских конвейеров развертывания, но не будет точно соответствовать стандартному поведению Ultralytics с центрированным «letterbox»-форматом.

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

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

DALI fn.resize по умолчанию включает сглаживание (antialias=True), в то время как OpenCV cv2.resize с 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

Вы можете передать предварительно обработанный PyTorch tensor в model.predict(). Когда torch.Tensor принимается, Ultralytics пропускает предварительную обработку изображений (подача в формате letterbox, преобразование BGR в RGB, HWC в CHW и нормализация до значения /255) и перед отправкой в модель выполняет только преобразование для устройства и приведение типов данных.

Поскольку в данном случае Ultralytics доступа к исходным размерам изображения, координаты рамок обнаружения возвращаются в пространстве размером 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))

Это относится ко всем внешним путям предварительной обработки — прямому tensor , видеопотокам и 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 . tensor быть представлен в формате BCHW, иметь тип float32 (или float16) и быть нормированным до [0, 1]. Ultralytics по-прежнему 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
import cv2
import numpy as np
import torch

from ultralytics import YOLO

model = YOLO("yolo26n.engine")  # TensorRT model

pipe = yolo_video_pipeline(target_size=640)
pipe.build()

cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # Feed BGR frame (convert to RGB for DALI)
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pipe.feed_input("input", [np.array(frame_rgb)])
    (output,) = pipe.run()

    # Convert DALI output to torch tensor for inference.
    # This is a simple fallback path: using feed_input() with pipe.run() keeps a GPU->CPU->GPU copy.
    # For high-throughput deployments, prefer a reader-based pipeline plus DALIGenericIterator to keep data on GPU.
    tensor = torch.tensor(output.as_cpu().as_array()).to("cuda")
    results = model.predict(tensor, verbose=False)

Сервер Triton с DALI

Для развёртки в производственной среде объедините предварительную обработку DALI с TensorRT в Triton 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 для 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.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"
      }
    }
  ]
}

Как работает ансамблевое картографирование

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

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

Почему tritonclient вместо YOLO(\"http://...\")?

Ultralytics встроенная Triton который автоматически выполняет предварительную и последующую обработку. Однако он не будет работать с ансамблем DALI, поскольку YOLO() передает предварительно обработанный tensor типа float32, tensor ансамбль ожидает необработанные байты 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 , чтобы входной tensor состоял из однородных блоков.

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

Предварительная обработка DALI поддерживается для всех YOLO , в которых используется стандартный LetterBox конвейер:

ЗадачаПоддерживаетсяПримечания
ОбнаружениеСтандартная предварительная обработка почтовых ящиков
СегментацияТакая же предварительная обработка, как и при обнаружении
Оценка позыТакая же предварительная обработка, как и при обнаружении
Ориентированное обнаружение (OBB)Такая же предварительная обработка, как и при обнаружении
КлассификацияИспользует трансформации Torchvision (кадрирование по центру), а не формат «letterbox»

Ограничения

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

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

Как скорость предварительной обработки с помощью DALI соотносится со скоростью CPU ?

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

Можно ли использовать DALI с PyTorch (а не только с TensorRT)?

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

В чём разница между fn.pad и fn.crop для заполнения?

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

Дает ли DALI результаты, идентичные по пикселям результатам CPU на CPU ?

Практически одинаковые. Набор antialias=False в fn.resize в соответствии с OpenCV cv2.INTER_LINEAR. Minor floating-point differences (< 0.001) may occur due to GPU vs CPU arithmetic, but these have no measurable impact on detection точность.

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

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



📅 Создано 0 дней назад ✏️ Обновлено 0 дней назад
raimbekovm

Комментарии