YOLO Vision 2026:

Pré-processamento acelerado por GPU com NVIDIA DALI#

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

Este guia orienta na construçã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 de ponta a ponta com o Triton Inference Server.

Para quem é este guia?

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

Resumo Rápido
  • A construir um pipeline DALI? Usa 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.
  • A integrar com o Ultralytics? Passa 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 um pré-processamento com zero uso de CPU.

Por que usar DALI para o pré-processamento do 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 mantendo a proporção
  3. Preencher até o tamanho alvo (letterbox)
  4. Normalizar os valores dos pixéis 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 GPUMotores TensorRT com inferência em menos de um milissegundo tornam o pré-processamento em CPU o custo dominante
Entradas de alta resoluçãoFluxos de vídeo 1080p e 4K exigem operações de redimensionamento custosas
Tamanhos de lote grandesProcessamento de inferência no lado do servidor com muitas imagens em paralelo
Núcleos de CPU limitadosDispositivos de ponta como NVIDIA Jetson, ou servidores de GPU densos com poucos núcleos de CPU por GPU

Pré-requisitos#

Somente Linux

A 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-cuda130

Requisitos:

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

Entendendo o pré-processamento do YOLO#

Antes de construir um pipeline DALI, vale a pena 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 de pré-processamento completo em ultralytics/engine/predictor.py executa estes passos:

EtapaOperaçãoFunção de 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. A calcular a escala: r = min(target_h / h, target_w / w)
  2. A redimensionar para (round(w * r), round(h * r))
  3. A preencher o espaço restante com cinzento (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#

O pipeline DALI recomendado replica o comportamento padrão LetterBox(center=True) do Ultralytics, que é o que a inferência YOLO padrão utiliza.

Pipeline centralizado (recomendado, corresponde ao LetterBox do Ultralytics)#

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

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

Se não precisares de paridade exata com LetterBox(center=True), podes simplificar o passo de preenchimento utilizando fn.pad(...) em vez de fn.crop(..., out_of_bounds_policy="pad"). Essa variante preenche apenas as margens 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 usar `fn.crop` para preenchimento centralizado?

O operador fn.pad do DALI adiciona preenchimento apenas às margens direita e inferior. Para obter um preenchimento centralizado (compatível com LetterBox(center=True) do Ultralytics), usa fn.crop com out_of_bounds_policy="pad". Com crop_pos_x=0.5 e crop_pos_y=0.5 predefinidos, a imagem é automaticamente centrada com preenchimento simétrico.

Incompatibilidade de Antialias

O fn.resize do DALI ativa o antialiasing por predefinição (antialias=True), enquanto o cv2.resize do OpenCV com INTER_LINEAR não aplica antialiasing. Define sempre antialias=False no DALI para corresponder ao pipeline da CPU. Omitir isto causa subtis diferenças numéricas que podem afetar a precisão do modelo.

Executando o Pipeline#

Construir e executar 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 Predict do Ultralytics#

Podes passar um tensor do PyTorch pré-processado diretamente para model.predict(). Quando um torch.Tensor é passado, o Ultralytics ignora o pré-processamento da imagem (letterbox, BGR→RGB, HWC→CHW e normalização /255) e executa apenas a transferência para o dispositivo e a conversão de tipo de dados (dtype) antes de o enviar para o modelo.

Como o Ultralytics não tem acesso às dimensões originais da imagem neste caso, as coordenadas da caixa de deteção são devolvidas no espaço letterboxed de 640×640. Para mapeá-las de volta às coordenadas originais da imagem, usa scale_boxes, que lida com a lógica de arredondamento exata usada por 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 + predict do 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")
Zero Overhead de pré-processamento

Quando passas um torch.Tensor para model.predict(), o passo de pré-processamento da imagem demora ~0,004 ms (essencialmente zero) em comparação com ~1-10 ms com o pré-processamento em CPU. O tensor deve estar no formato BCHW, float32 (ou float16) e normalizado para [0, 1]. O Ultralytics continuará a tratar da transferência para o dispositivo e da conversão do dtype automaticamente.

DALI com fluxos de vídeo#

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

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

Para implantação em produção, combina o pré-processamento DALI com a inferência do TensorRT no Triton Inference Server utilizando um modelo de ensemble. Isto elimina totalmente o pré-processamento em CPU — entram bytes JPEG brutos, saem deteções, com tudo a ser 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

Etapa 1: Criar o pipeline DALI#

Serialize o pipeline DALI para o backend DALI do Triton:

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

Etapa 2: Exportar YOLO para TensorRT#

Exportar modelo YOLO para motor 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

Etapa 3: Configurar 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 liga modelos através de nomes de tensores virtuais. O valor "preprocessed_image" de output_map no passo DALI corresponde ao valor "preprocessed_image" de input_map no passo TensorRT. Estes são nomes arbitrários que ligam a saída de um passo à entrada do seguinte — não precisam de coincidir com os nomes internos de tensores de nenhum modelo.

Etapa 4: Enviar solicitações de inferência#

Por que usar `tritonclient` em vez de `YOLO('http://...')`?

O Ultralytics tem suporte integrado para o Triton que gere o pré/pós-processamento automaticamente. No entanto, não funcionará com o ensemble DALI porque YOLO() envia um tensor float32 pré-processado, enquanto o ensemble espera bytes JPEG brutos. Usa tritonclient diretamente para ensembles DALI e a integração integrada para implantações padrão sem DALI.

Enviar imagens para o conjunto 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 exige formas de lote homogêneas para o tensor de entrada.

Tarefas Suportadas#

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

TarefaSuportadoNotas
DetecçãoPré-processamento letterbox padrão
Segmentação de InstânciaO mesmo pré-processamento da detecção
Segmentação SemânticaO mesmo pré-processamento de imagem da detecção
ClassificaçãoUsa transformações torchvision (corte central), não letterbox
Estimativa de PoseO mesmo pré-processamento da detecção
Deteção Orientada (OBB)O mesmo pré-processamento da detecção

Limitações#

  • Somente Linux: DALI não oferece suporte a Windows ou macOS
  • GPU NVIDIA necessária: Sem fallback apenas para CPU
  • Pipeline estático: A estrutura do pipeline é definida em tempo de compilação e não pode ser alterada dinamicamente
  • fn.pad é apenas direita/inferior: Usa fn.crop com out_of_bounds_policy="pad" para preenchimento centralizado
  • Sem modo rect: Os pipelines DALI produzem saídas de tamanho fixo (por exemplo, 640×640). O modo rect auto=True, que produz saídas de tamanho variável (por exemplo, 384×640), não é suportado. Nota que embora o TensorRT suporte formas de entrada dinâmicas, um pipeline DALI de tamanho fixo emparelha-se naturalmente com um motor de tamanho fixo para obter o máximo rendimento
  • Memória com múltiplas instâncias: Utilizar instance_group com count > 1 no Triton pode causar um elevado uso de memória. Utiliza o grupo de instâncias predefinido para o modelo DALI

FAQ#

  • O benefício depende do teu pipeline. Quando a inferência na GPU já é rápida com o TensorRT, o pré-processamento em CPU a 2-10 ms pode tornar-se o custo dominante. O DALI elimina este gargalo ao executar o pré-processamento na GPU. Os maiores ganhos são observados com entradas de alta resolução (1080p, 4K), tamanhos de lote grandes e sistemas com núcleos de CPU limitados por GPU.

  • Sim. Usa DALIGenericIterator para obter saídas torch.Tensor pré-processadas e, em seguida, passa-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 em CPU se torna o gargalo.

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

  • Quase idêntico. Define antialias=False em fn.resize para corresponder ao cv2.INTER_LINEAR do OpenCV. Podem ocorrer pequenas diferenças de vírgula flutuante (< 0,001) devido à aritmética de GPU vs CPU, mas estas não têm impacto mensurável na precisão da deteção.

  • CV-CUDA é outra biblioteca da NVIDIA para processamento de visão acelerado por GPU. Oferece controlo por operador (como o OpenCV mas na GPU) em vez da abordagem de pipeline do DALI. O cvcuda.copymakeborder() do CV-CUDA suporta preenchimento explícito por margem, tornando o letterbox centralizado simples. Escolhe o DALI para fluxos de trabalho baseados em pipeline (especialmente com Triton) e o CV-CUDA para controlo granular ao nível do operador em código de inferência personalizado.

Comentários