Ultralytics YOLO27: Inscreve-te na lista de espera.
Get Started

Pré-processamento acelerado por GPU com NVIDIA DALI#

Ao implementar modelos Ultralytics YOLO em produção, o pré-processamento torna-se frequentemente 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 demorar entre 2 e 10 ms por imagem, sobretudo em resoluções elevadas. A NVIDIA DALI (Data Loading Library) resolve este problema ao transferir todo o pipeline de pré-processamento para a GPU.

Este guia mostra como criar pipelines DALI que reproduzem exatamente o pré-processamento Ultralytics YOLO, integrá-los com model.predict(), processar fluxos de vídeo e implementar a solução completa com o Triton Inference Server.

A quem se destina este guia?

Este guia destina-se a engenheiros que implementam modelos YOLO em ambientes de produção nos quais o pré-processamento na CPU é um gargalo comprovado — normalmente implementações de TensorRT em GPUs NVIDIA, pipelines de vídeo de alto throughput ou configurações do Triton Inference Server. Se estiveres a executar inferências padrão com model.predict() e não tiveres um gargalo no pré-processamento, o pipeline predefinido na CPU funciona bem.

Resumo rápido
  • A criar um pipeline DALI? Usa fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize para reproduzir na GPU o pré-processamento letterbox do YOLO.
  • A integrar com o Ultralytics? Passa a saída do DALI como torch.Tensor para model.predict() — o Ultralytics ignora automaticamente o pré-processamento de imagens.
  • A implementar com Triton? Usa o backend DALI com um ensemble TensorRT para fazer o pré-processamento sem usar a CPU.

Por que usar DALI no pré-processamento do YOLO#

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

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

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

CenárioPor que o DALI ajuda
Inferência rápida na GPUOs mecanismos TensorRT, com inferência em menos de um milissegundo, fazem do pré-processamento na CPU o principal custo
Entradas de alta resoluçãoFluxos de vídeo em 1080p e 4K exigem operações de redimensionamento dispendiosas
Tamanhos de lote grandesInferência no servidor processando muitas imagens em paralelo
Número limitado de núcleos de CPUDispositivos de borda, como o NVIDIA Jetson, ou servidores com GPUs de alta densidade e poucos núcleos de CPU por GPU

Pré-requisitos#

Somente Linux

O NVIDIA DALI é compatível somente com Linux. Ele não está disponível para Windows nem 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 criar um pipeline do 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),  # Tamanho de destino
    center=True,  # Centralizar a imagem (preencher igualmente nos dois lados)
    stride=32,  # Alinhamento de stride
    padding_value=114,  # Preenchimento cinza (114, 114, 114)
)

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

EtapaOperaçãoFunção da CPUEquivalente no 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 + normalização /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

A operação letterbox preserva a proporção da imagem 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 alcançar o tamanho de destino
  4. Centralizar a imagem para distribuir o preenchimento igualmente nos dois lados

Pipeline do DALI para YOLO#

O pipeline recomendado do DALI reproduz o comportamento padrão LetterBox(center=True) do Ultralytics, usado na inferência padrão do YOLO.

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

Pipeline do 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.
    """
    # Ler e decodificar imagens na 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)

    # Redimensionar preservando a proporção
    resized = fn.resize(
        images,
        resize_x=target_size,
        resize_y=target_size,
        mode="not_larger",
        interp_type=types.INTERP_LINEAR,
        antialias=False,  # Corresponder a cv2.INTER_LINEAR (sem antisserrilhamento)
    )

    # Preenchimento centralizado usando fn.crop com out_of_bounds_policy
    # Quando o tamanho do recorte é maior que o da imagem, fn.crop centraliza a imagem e adiciona preenchimento simétrico
    padded = fn.crop(
        resized,
        crop=(target_size, target_size),
        out_of_bounds_policy="pad",
        fill_values=114,  # Valor de preenchimento do YOLO
    )

    # Normalizar e converter o 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 precisa de correspondência exata com LetterBox(center=True), você pode simplificar a etapa de preenchimento usando fn.pad(...) em vez de fn.crop(..., out_of_bounds_policy="pad"). Essa variante preenche apenas as bordas direita e inferior, o que pode ser aceitável em pipelines de implantação personalizados, mas não corresponde exatamente ao comportamento letterbox centralizado padrão do Ultralytics.

Por que usar `fn.crop` para o preenchimento centralizado?

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

Incompatibilidade de antisserrilhamento

O fn.resize do DALI ativa o antisserrilhamento por padrão (antialias=True), enquanto o cv2.resize do OpenCV com INTER_LINEAR não aplica antisserrilhamento. Sempre defina antialias=False no DALI para corresponder ao pipeline da CPU. Se você não fizer isso, ocorrerão diferenças numéricas sutis que podem afetar a precisão do modelo.

Executando o pipeline#

Criar e executar um pipeline do 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 o DALI com o Predict do Ultralytics#

Você pode passar um tensor 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 realiza apenas a transferência para o dispositivo e a conversão do tipo de dados antes de enviar a imagem ao modelo.

Como o Ultralytics não tem acesso às dimensões originais da imagem nesse caso, as coordenadas das caixas de detecção são retornadas no espaço letterbox de 640×640. Para mapeá-las de volta às coordenadas da imagem original, use scale_boxes, que aplica a lógica exata de arredondamento usada por LetterBox:

from ultralytics.utils.ops import scale_boxes

# caixas: tensor de forma (N, 4) no formato xyxy, em coordenadas letterbox de 640x640
# Redimensionar as caixas de letterbox (640, 640) para o tamanho original (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))

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

Previsão com 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")
Nenhuma sobrecarga de pré-processamento

Quando você passa um torch.Tensor para model.predict(), a etapa de pré-processamento da imagem leva ~0.004ms (praticamente zero), em comparação com ~1-10ms no pré-processamento pela CPU. O tensor precisa estar no formato BCHW, ser float32 (ou float16) e estar normalizado para [0, 1]. O Ultralytics ainda fará automaticamente a transferência para o dispositivo e a conversão do tipo de dados.

DALI com fluxos de vídeo#

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

Pipeline do 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."""
    # Origem externa para fornecer quadros do OpenCV, GStreamer etc.
    frames = fn.external_source(device="cpu", name="input")
    frames = fn.reshape(frames, layout="HWC")

    # Mover para a GPU e pré-processar
    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ções em produção, combine o pré-processamento do DALI com a inferência TensorRT no Triton Inference Server, usando um modelo de ensemble. Isso elimina completamente o pré-processamento na CPU: entram bytes JPEG brutos e saem detecções, com todo o processamento feito na GPU.

Estrutura do repositório do modelo#

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 do DALI#

Serialize o pipeline do DALI para o backend DALI do Triton:

Serializar o pipeline do DALI para o 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."""
    # Entrada: bytes brutos da imagem codificada recebidos do 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

# Serializar o pipeline no repositório do modelo
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")

Etapa 2: Exportar o YOLO para TensorRT#

Exportar o modelo YOLO para um mecanismo TensorRT
from pathlib import Path

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
engine_path = model.export(
    format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
)  # Sem NMS (N, 300, 6); TensorRT >= 8.5

# O Ultralytics acrescenta um cabeçalho de metadados aos arquivos .engine; remova-o para que o Triton possa carregar o plano TensorRT bruto
with open(engine_path, "rb") as f:
    meta_len = int.from_bytes(f.read(4), byteorder="little")  # comprimento do cabeçalho de metadados JSON
    f.seek(4 + meta_len)
    plan = f.read()
Path("model_repository/yolo_trt/1").mkdir(parents=True, exist_ok=True)
Path("model_repository/yolo_trt/1/model.plan").write_bytes(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 do ensemble

O ensemble conecta modelos por meio de nomes virtuais de tensores. O valor output_map "preprocessed_image" na etapa DALI corresponde ao valor input_map "preprocessed_image" na etapa TensorRT. Esses são nomes arbitrários que conectam a saída de uma etapa à entrada da etapa seguinte — eles não precisam corresponder aos nomes internos dos tensores de nenhum modelo.

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

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

A Ultralytics oferece suporte integrado ao Triton que lida automaticamente com o pré e o pós-processamento. No entanto, esse suporte não funciona com o ensemble DALI, porque YOLO() envia um tensor float32 pré-processado, enquanto o ensemble espera bytes JPEG brutos. Use tritonclient diretamente para ensembles DALI e a integração integrada para implantações padrão sem DALI.

Enviar imagens para o ensemble 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 for the nms=False export)
detections = detections[0]  # First image
detections = detections[detections[:, 4] > 0.25]  # Confidence threshold
print(f"Detected {len(detections)} objects")
Agrupamento de imagens JPEG

Ao enviar um lote de imagens JPEG para o Triton, preencha todos os arrays de bytes codificados até que tenham o mesmo comprimento (a contagem máxima de bytes no lote). O Triton exige formatos de lote homogêneos para o tensor de entrada.

Tarefas suportadas#

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

TarefaCompatívelObservações
Detecção✅Pré-processamento letterbox padrão
Segmentação de instâncias✅Mesmo pré-processamento da detecção
Segmentação semântica✅Mesmo pré-processamento de imagem da detecção
Estimativa de profundidade✅Mesmo pré-processamento de imagem da detecção
Classificação❌Usa transformações do torchvision (recorte central), não letterbox
Estimativa de pose✅Mesmo pré-processamento da detecção
Detecção orientada (OBB)✅Mesmo pré-processamento da detecção

Limitações#

  • Somente Linux: DALI não é compatível com Windows nem macOS
  • GPU NVIDIA necessária: não há alternativa que funcione somente com CPU
  • Pipeline estático: a estrutura do pipeline é definida durante a compilação e não pode mudar dinamicamente
  • fn.pad só adiciona padding à direita e embaixo: use fn.crop com out_of_bounds_policy="pad" para centralizar o padding
  • 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 é compatível. Observe que, embora o TensorRT aceite formas de entrada dinâmicas, um pipeline DALI de tamanho fixo combina naturalmente com um mecanismo de tamanho fixo para maximizar a taxa de transferência
  • Memória com várias instâncias: usar instance_group com count > 1 no Triton pode causar alto uso de memória. Use o grupo de instâncias padrão para o modelo DALI

Perguntas frequentes#

  • O benefício depende do seu pipeline. Quando a inferência na GPU já é rápida com TensorRT, o pré-processamento na CPU, que leva de 2 a 10 ms, pode se tornar o principal gargalo. O DALI elimina esse gargalo executando o pré-processamento na GPU. Os maiores ganhos aparecem com entradas de alta resolução (1080p, 4K), tamanhos de lote grandes e sistemas com poucos núcleos de CPU por GPU.

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

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

  • Quase idênticos. Defina antialias=False em fn.resize para corresponder a cv2.INTER_LINEAR do OpenCV. Podem ocorrer pequenas diferenças de ponto flutuante (< 0.001) devido às operações aritméticas na GPU e na CPU, mas elas não têm impacto mensurável na precisão da detecção.

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

Comentários