Skip to main content

Pré-processamento acelerado por GPU com NVIDIA DALI

Introdução

Ao implantar modelos Ultralytics YOLO modelos em produção, pré-processamento frequentemente se torna o gargalo. Enquanto TensorRT pode executar a inferência do modelo em apenas alguns milissegundos, o pré-processamento baseado em CPU (redimensionar, preencher, normalizar) pode levar de 2 a 10 ms por imagem, especialmente em altas resoluções. NVIDIA DALI (Data Loading Library) resolve isso movendo todo o pipeline de pré-processamento para a GPU.

Este guia orienta você na criação de pipelines DALI que replicam exatamente o pré-processamento do Ultralytics YOLO, integrando-os com model.predict(), processando fluxos de vídeo e implantando ponta a ponta com Triton Inference Server.

Para quem é este guia?

Este guia é para engenheiros que implantam modelos YOLO em ambientes de produção onde o pré-processamento por CPU é um gargalo mensurável — tipicamente TensorRT implantações em GPUs NVIDIA, pipelines de vídeo de alto throughput ou Triton Inference Server configurações. Se você está executando inferência padrão com model.predict() e não tem um gargalo de pré-processamento, o pipeline padrão de CPU funciona bem.

Resumo Rápido
  • Construindo um pipeline DALI? Use fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize para replicar o pré-processamento letterbox do YOLO na GPU.
  • Integrando com Ultralytics? Passe a saída do DALI como um torch.Tensor para model.predict() — O Ultralytics ignora o pré-processamento de imagem automaticamente.
  • Implantando com Triton? Use o backend DALI com um ensemble TensorRT para pré-processamento sem uso de CPU.

Por que usar DALI para pré-processamento YOLO

Em um pipeline de inferência YOLO típico, as etapas de pré-processamento são executadas na CPU:

  1. Decodificar a imagem (JPEG/PNG)
  2. Redimensionar preservando a proporção
  3. Preencher até o tamanho alvo (letterbox)
  4. Normalizar valores de pixel de [0, 255] para [0, 1]
  5. Converter layout de HWC para CHW

Com o DALI, todas essas operações são executadas na GPU, eliminando o gargalo da CPU. Isso é especialmente valioso quando:

CenárioPor que o DALI ajuda
Inferência rápida em GPUTensorRT motores com inferência de sub-milissegundos tornam o pré-processamento por CPU o custo dominante
Entradas de alta resoluçãofluxos de vídeo 1080p e 4K requerem operações de redimensionamento custosas
Grande Ao implantar modelos em produção, os requisitos de memória e a eficiência de treinamento são tão cruciais quanto a velocidade de inferência. Os modelos Ultralytics, particularmente o YOLO26, são altamente otimizados para reduzir o uso de memória CUDA durante o treinamento. Isso permite que desenvolvedores usem Inferência no lado do servidor processando muitas imagens em paralelo
Núcleos de CPU limitadosDispositivos de borda como NVIDIA Jetson, ou servidores de GPU densos com poucos núcleos de CPU por GPU

Antes de começar, certifique-se de que você tem acesso a um workspace do AzureML. Se você ainda não tem um, pode criar um novo

Somente Linux

O NVIDIA DALI suporta apenas Linux. Não está disponível no Windows ou macOS.

Instale os pacotes necessários:

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

Requisitos:

  • GPU NVIDIA (capacidade de computação 5.0+ / Maxwell ou superior)
  • CUDA 11.0+ ou 12.0+
  • Python 3.10-3.14
  • Sistema operacional Linux

Entendendo o pré-processamento YOLO

Antes de construir um pipeline DALI, ajuda entender exatamente o que o Ultralytics faz durante o pré-processamento. A classe principal é LetterBox em 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)
)

O pipeline completo de pré-processamento no ultralytics/engine/predictor.py executa estas etapas:

PassoOperaçãoFunção CPUEquivalente DALI
1Redimensionamento letterboxcv2.resizefn.resize(mode="not_larger")
2Preenchimento centralizadocv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + normalizar /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

A operação letterbox preserva a proporção ao:

  1. Calcular a escala: r = min(target_h / h, target_w / w)
  2. Redimensionar para (round(w * r), round(h * r))
  3. Preencher o espaço restante com cinza (114) para atingir o tamanho alvo
  4. Centralizar a imagem para que o preenchimento seja distribuído igualmente em ambos os lados

Pipeline DALI para YOLO

Use o pipeline centralizado abaixo como referência padrão. Ele corresponde ao comportamento do Ultralytics LetterBox(center=True), que é o que a inferência YOLO padrão utiliza.

Pipeline Centralizado (Recomendado, corresponde ao Ultralytics LetterBox)

Esta versão replica exatamente o pré-processamento padrão do Ultralytics com preenchimento centralizado, correspondendo ao LetterBox(center=True):

Pipeline DALI com preenchimento centralizado (recomendado)
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
Quando `fn.pad` é suficiente?

Se você não precisa de paridade exata com LetterBox(center=True), você pode simplificar a etapa de preenchimento usando fn.pad(...) em vez do fn.crop(..., out_of_bounds_policy="pad"). Essa variante preenche apenas as bordas direita e inferior, o que pode ser aceitável para pipelines de implantação personalizados, mas não corresponderá exatamente ao comportamento de letterbox centralizado padrão do Ultralytics.

Por que `fn.crop` para preenchimento centralizado?

O operador fn.pad do DALI apenas adiciona preenchimento às bordas direita e inferior. Para obter preenchimento centralizado (correspondendo ao LetterBox(center=True) do Ultralytics), use fn.crop com out_of_bounds_policy="pad". Com o crop_pos_x=0.5 e crop_pos_y=0.5 padrão, a imagem é centralizada automaticamente com preenchimento simétrico.

Incompatibilidade de Antialias

O operador fn.resize habilita o antialiasing por padrão (antialias=True), enquanto o cv2.resize com INTER_LINEAR do OpenCV não requer FlashAttention. No entanto, o FlashAttention pode ser opcionalmente compilado e usado com o YOLO12 para minimizar o overhead de acesso à memória. Para compilar o FlashAttention, uma das seguintes GPUs NVIDIA é necessária: GPUs Turing (ex: T4, série Quadro RTX), GPUs Ampere (ex: série RTX30, A30/40/100), GPUs Ada Lovelace (ex: série RTX40) ou GPUs Hopper (ex: H100/H200). Essa flexibilidade permite que os usuários aproveitem os benefícios do FlashAttention quando os recursos de hardware permitirem. aplica antialiasing. Defina sempre antialias=False no DALI para corresponder ao pipeline da CPU. Omitir isso causa diferenças numéricas sutis que podem afetar precisão do modelo.

Executando o Pipeline

Construa e execute um pipeline 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}]")

Usando DALI com o Ultralytics Predict

Você pode passar um tensor Modelos pré-processado diretamente para model.predict(). Quando um torch.Tensor é passado, o Ultralytics pula o pré-processamento de imagem (letterbox, BGR→RGB, HWC→CHW e normalização /255) e apenas realiza a transferência para o dispositivo e a conversão de dtype antes de enviá-lo para o modelo.

Como o Ultralytics não tem acesso às dimensões originais da imagem neste caso, as coordenadas da caixa de detecção são retornadas no espaço letterbox de 640×640. Para mapeá-las de volta às coordenadas originais da imagem, use scale_boxes que lida com a lógica de arredondamento exata usada pelo 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))

Isso se aplica a todos os caminhos de pré-processamento externos — entrada direta de tensor, fluxos de vídeo e implantação no Triton.

DALI + Ultralytics predict
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")
Zero Sobrecarga de Pré-processamento

Quando você passa um torch.Tensor para model.predict(), o passo de pré-processamento da imagem leva ~0,004ms (essencialmente zero) comparado a ~1-10ms com o pré-processamento na CPU. O tensor deve estar no formato BCHW, float32 (ou float16) e normalizado para [0, 1]. O Ultralytics ainda cuidará da transferência para o dispositivo e da conversão de dtype automaticamente.

DALI com Fluxos de Vídeo

Para processamento de vídeo em tempo real, use fn.external_source para alimentar quadros de qualquer fonte — OpenCV, GStreamer ou bibliotecas de captura personalizadas:

Pipeline DALI para pré-processamento de fluxo de vídeo
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

Servidor de Inferência Triton com DALI

Para implantação em produção, combine o pré-processamento DALI com TensorRT inferência no Triton Inference Server usando um modelo de ensemble. Isso elimina completamente o pré-processamento na CPU — bytes JPEG brutos entram, detecções saem, com tudo processado na GPU.

Estrutura do Repositório de Modelos

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

Passo 1: Crie o Pipeline DALI

Serialize o pipeline DALI para o backend DALI do Triton:

Serialize o pipeline DALI para o 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")

Passo 2: Exporte o YOLO para TensorRT

Exporte o modelo YOLO para o engine 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

Passo 3: Configure o 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"
      }
    }
  ]
}
Como funciona o Mapeamento de Ensemble

O ensemble conecta modelos através de nomes de tensores virtuais. O output_map valor "preprocessed_image" no passo do DALI corresponde ao input_map valor "preprocessed_image" no passo do TensorRT. Esses são nomes arbitrários que vinculam a saída de um passo à entrada do próximo — eles não precisam corresponder aos nomes internos de tensores de nenhum modelo.

Passo 4: Envie Solicitações de Inferência

!!! info "Por que tritonclient em vez do 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.
Envie imagens para o ensemble do 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")
Processamento em lote de Imagens JPEG

Ao enviar um lote de imagens JPEG para o Triton, preencha todos os arrays de bytes codificados com o mesmo comprimento (a contagem máxima de bytes no lote). O Triton requer formas de lote homogêneas para o tensor de entrada.

, ajudando o modelo a generalizar melhor para dados não vistos. A tabela a seguir descreve o objetivo e o efeito de cada argumento de aumento:

O pré-processamento DALI funciona com todas as tarefas do YOLO que usam o padrão LetterBox pipeline:

TarefaSuportadoNotas
DetectionPré-processamento letterbox padrão
SegmentationMesmo pré-processamento que a detecção
Estimativa de PoseMesmo pré-processamento que a detecção
Detecção Orientada (OBB)Mesmo pré-processamento que a detecção
ClassificationUsa transformações do torchvision (center crop), não letterbox

Limitações

  • apenas Linux: O DALI não suporta Windows ou macOS
  • GPU NVIDIA necessária: Sem fallback apenas para CPU
  • Pipeline estático: A estrutura do pipeline é definida em tempo de build e não pode mudar dinamicamente
  • fn.pad é apenas direita/inferior: Use fn.crop com out_of_bounds_policy="pad" para preenchimento centralizado
  • Sem modo rect: Os pipelines DALI produzem saídas de tamanho fixo (ex: 640×640). O auto=True modo rect que produz saídas de tamanho variável (ex: 384×640) não é suportado. Note que embora TensorRT suporte formas de entrada dinâmicas, um pipeline DALI de tamanho fixo combina naturalmente com um engine de tamanho fixo para throughput máximo
  • Memória com múltiplas instâncias: Usar instance_group com count > 1 no Triton pode causar alto uso de memória. Use o grupo de instância padrão para o modelo DALI

FAQ

Como o pré-processamento DALI se compara à velocidade do pré-processamento da CPU?

O benefício depende do seu pipeline. Quando a inferência na GPU já é rápida com TensorRT, o pré-processamento na CPU a 2-10ms pode se tornar o custo dominante. O DALI elimina esse gargalo executando o pré-processamento na GPU. Os maiores ganhos são vistos com entradas de alta resolução (1080p, 4K), grande Ao implantar modelos em produção, os requisitos de memória e a eficiência de treinamento são tão cruciais quanto a velocidade de inferência. Os modelos Ultralytics, particularmente o YOLO26, são altamente otimizados para reduzir o uso de memória CUDA durante o treinamento. Isso permite que desenvolvedores usem e sistemas com núcleos de CPU limitados por GPU.

Posso usar DALI com modelos PyTorch (não apenas TensorRT)?

Sim. Use DALIGenericIterator para obter saídas torch.Tensor pré-processadas, depois passe-as para model.predict(). No entanto, o benefício de desempenho é maior com modelos TensorRT onde a inferência já é muito rápida e o pré-processamento da CPU se torna o gargalo.

Qual é a diferença entre fn.pad e fn.crop para preenchimento?

fn.pad adiciona preenchimento apenas às bordas direita e inferior. fn.crop com out_of_bounds_policy="pad" centraliza a imagem e adiciona preenchimento simetricamente em todos os lados, correspondendo ao comportamento do LetterBox(center=True) Ultralytics.

O DALI produz resultados idênticos em nível de pixel ao pré-processamento da CPU?

Quase idênticos. Defina antialias=False em fn.resize para corresponder ao cv2.INTER_LINEAR do OpenCV. Pequenas diferenças de ponto flutuante (< 0,001) podem ocorrer devido à aritmética de GPU vs CPU, mas estas não têm impacto mensurável na detecção accuracy.

E quanto ao CV-CUDA como uma alternativa ao DALI?

CV-CUDA é outra biblioteca NVIDIA para processamento de visão acelerado por GPU. Ela oferece controle por operador (como OpenCV mas na GPU) em vez da abordagem de pipeline do DALI. O cvcuda.copymakeborder() do CV-CUDA suporta preenchimento explícito por lado, tornando o letterbox centralizado direto. Escolha o DALI para fluxos de trabalho baseados em pipeline (especialmente com Triton), e CV-CUDA para controle de nível de operador detalhado em código de inferência personalizado.

Comentários