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.
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.
- Construindo um pipeline DALI? Use
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizepara replicar o pré-processamento letterbox do YOLO na GPU. - Integrando com Ultralytics? Passe a saída do DALI como um
torch.Tensorparamodel.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:
- Decodificar a imagem (JPEG/PNG)
- Redimensionar preservando a proporção
- Preencher até o tamanho alvo (letterbox)
- Normalizar valores de pixel de
[0, 255]para[0, 1] - 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ário | Por que o DALI ajuda |
|---|---|
| Inferência rápida em GPU | TensorRT motores com inferência de sub-milissegundos tornam o pré-processamento por CPU o custo dominante |
| Entradas de alta resolução | fluxos 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 limitados | Dispositivos 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
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-cuda120Requisitos:
- 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:
| Passo | Operação | Função CPU | Equivalente DALI |
|---|---|---|---|
| 1 | Redimensionamento letterbox | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Preenchimento centralizado | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + normalizar /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
A operação letterbox preserva a proporção ao:
- Calcular a escala:
r = min(target_h / h, target_w / w) - Redimensionar para
(round(w * r), round(h * r)) - Preencher o espaço restante com cinza (
114) para atingir o tamanho alvo - 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):
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 outputSe 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.
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.
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
# 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.
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")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:
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 outputServidor 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.pbtxtPasso 1: Crie o Pipeline DALI
Serialize o pipeline DALI para o backend DALI do 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
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.planPasso 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"
}
}
]
}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.
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")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:
| Tarefa | Suportado | Notas |
|---|---|---|
| Detection | ✅ | Pré-processamento letterbox padrão |
| Segmentation | ✅ | Mesmo pré-processamento que a detecção |
| Estimativa de Pose | ✅ | Mesmo pré-processamento que a detecção |
| Detecção Orientada (OBB) | ✅ | Mesmo pré-processamento que a detecção |
| Classification | ❌ | Usa 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: Usefn.cropcomout_of_bounds_policy="pad"para preenchimento centralizado- Sem modo rect: Os pipelines DALI produzem saídas de tamanho fixo (ex: 640×640). O
auto=Truemodo 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_groupcomcount> 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.