Pré-processamento acelerado por GPU com NVIDIA DALI#
Ao implantar modelos Ultralytics YOLO em produção, o pré-processamento costuma se tornar o gargalo. Embora o TensorRT possa 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 (Biblioteca de carregamento de dados) resolve esse problema transferindo todo o pipeline de pré-processamento para a GPU.
Este guia mostra como criar pipelines DALI que reproduzem exatamente o pré-processamento do Ultralytics YOLO, integrá-los com model.predict(), processar fluxos de vídeo e fazer a implantação de ponta a ponta com o Triton Inference Server.
Este guia é para engenheiros que implantam modelos YOLO em ambientes de produção nos quais o pré-processamento na CPU é um gargalo comprovado — normalmente implantações com TensorRT em GPUs NVIDIA, pipelines de vídeo de alto desempenho ou configurações do Triton Inference Server. Se você executa inferência padrão com model.predict() e não tem um gargalo de pré-processamento, o pipeline padrão na CPU funciona bem.
- Criando um pipeline DALI? Use
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizepara reproduzir o pré-processamento letterbox do YOLO na GPU. - Integrando com o Ultralytics? Passe a saída do DALI como
torch.Tensorparamodel.predict()— o Ultralytics ignora o pré-processamento da imagem automaticamente. - Fazendo a implantação com o Triton? Use o backend DALI com um ensemble TensorRT para eliminar o pré-processamento na CPU.
Por que usar o DALI para o pré-processamento do YOLO#
Em um pipeline típico de inferência YOLO, as etapas de pré-processamento são executadas na CPU:
- Decodificar a imagem (JPEG/PNG)
- Redimensionar preservando a proporção
- Preencher até o tamanho de destino (letterbox)
- Normalizar os valores dos pixels de
[0, 255]a[0, 1] - 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 vantajoso quando:
| Cenário | Por que o DALI ajuda |
|---|---|
| Inferência rápida na GPU | Os mecanismos TensorRT com inferência abaixo de um milissegundo tornam o pré-processamento na CPU o custo dominante |
| Entradas de alta resolução | Fluxos de vídeo em 1080p e 4K exigem operações de redimensionamento dispendiosas |
| Tamanhos de lote grandes | Inferência no servidor que processa muitas imagens em paralelo |
| Núcleos de CPU limitados | Dispositivos de borda como o NVIDIA Jetson ou servidores GPU densos com poucos núcleos de CPU por GPU |
Pré-requisitos#
O NVIDIA DALI é compatível somente com Linux. Ele não está disponível para Windows ou macOS.
Instale os pacotes necessários:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130Requisitos:
- 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 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, # Centraliza a imagem (preenche igualmente os dois lados)
stride=32, # Alinhamento ao stride
padding_value=114, # Preenchimento cinza (114, 114, 114)
)O pipeline completo de pré-processamento em ultralytics/engine/predictor.py executa estas etapas:
| Etapa | Operação | Função da CPU | Equivalente no 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 + normalização /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 de destino - Centralizar a imagem para distribuir o preenchimento igualmente nos dois lados
Pipeline DALI para YOLO#
O pipeline DALI recomendado reproduz o comportamento padrão LetterBox(center=True) do Ultralytics, usado na inferência YOLO padrão.
Pipeline centralizado (recomendado; corresponde ao LetterBox do Ultralytics)#
Esta versão reproduz exatamente o pré-processamento padrão do Ultralytics com preenchimento centralizado, correspondendo a LetterBox(center=True):
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)
# Redimensionamento que preserva 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 > tamanho da imagem, fn.crop centraliza a imagem e adiciona preenchimento simetricamente
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 outputSe você não precisa de uma correspondência exata com LetterBox(center=True), 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.
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 de crop_pos_x=0.5 e crop_pos_y=0.5, a imagem é centralizada automaticamente com preenchimento simétrico.
O fn.resize do DALI habilita 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. Omitir essa configuração causa pequenas diferenças numéricas que podem afetar a 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 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 executa apenas a transferência de dispositivo e a conversão de tipo de dados antes de enviá-lo ao modelo.
Como o Ultralytics não tem acesso às dimensões da imagem original neste 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 formato (N, 4) no formato xyxy, em coordenadas letterbox de 640x640
# Redimensiona as caixas das coordenadas letterbox (640, 640) de volta para as coordenadas originais (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))Isso se aplica a todos os fluxos de pré-processamento externo — entrada direta de tensor, fluxos de vídeo e implantação com 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(), a etapa de pré-processamento da imagem leva ~0.004ms (praticamente zero), em comparação com ~1-10ms no pré-processamento na CPU. O tensor deve estar no formato BCHW, ser float32 (ou float16) e estar normalizado para [0, 1]. O Ultralytics ainda gerencia automaticamente a transferência de dispositivo e a conversão de tipo de dados.
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:
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."""
# Fonte 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 outputTriton Inference Server com DALI#
Para implantações em produção, combine o pré-processamento do DALI com a inferência do TensorRT no Triton Inference Server, usando um modelo de conjunto. Isso elimina totalmente o pré-processamento na CPU: entram bytes JPEG brutos e saem detecções, 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.pbtxtEtapa 1: criar o pipeline DALI#
Serialize o pipeline DALI para o backend DALI do 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 de imagem codificada 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 de modelos
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")Etapa 2: exportar YOLO para 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 adiciona 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"
}
}
]
}O conjunto conecta modelos por meio de nomes de tensores virtuais. O valor output_map "preprocessed_image" na etapa DALI corresponde ao valor input_map "preprocessed_image" na etapa TensorRT. Esses nomes arbitrários conectam a saída de uma etapa à entrada da etapa seguinte — eles não precisam corresponder aos nomes internos de tensores de nenhum modelo.
Etapa 4: enviar solicitações de inferência#
O Ultralytics tem suporte integrado ao Triton, que processa automaticamente o pré e o pós-processamento. No entanto, esse suporte não funciona com o conjunto DALI porque YOLO() envia um tensor float32 pré-processado, enquanto o conjunto espera bytes JPEG brutos. Use tritonclient diretamente para conjuntos DALI e a integração integrada para implantações padrão sem 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 for the nms=False export)
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 até que tenham o mesmo comprimento (a contagem máxima de bytes no lote). O Triton exige formas homogêneas para o tensor de entrada do lote.
Tarefas compatíveis#
O pré-processamento DALI funciona com todas as tarefas YOLO que usam o pipeline padrão LetterBox:
| Tarefa | Compatível | Observações |
|---|---|---|
| Deteção | ✅ | Pré-processamento letterbox padrão |
| Segmentação de instâncias | ✅ | O mesmo pré-processamento da detecção |
| Segmentação semântica | ✅ | O mesmo pré-processamento de imagem da detecção |
| Estimativa de profundidade | ✅ | O mesmo pré-processamento de imagem da detecção |
| Classificação | ❌ | Usa transformações do torchvision (recorte central), não letterbox |
| Estimativa de pose | ✅ | O mesmo pré-processamento da detecção |
| Detecção Orientada (OBB) | ✅ | O mesmo pré-processamento da detecção |
Limitações#
- Somente Linux: o DALI não é compatível com Windows nem macOS
- É necessária uma GPU NVIDIA: não há alternativa que use somente a CPU
- Pipeline estático: a estrutura do pipeline é definida durante a compilação e não pode ser alterada dinamicamente
fn.padaplica preenchimento somente à direita e embaixo: usefn.cropcomout_of_bounds_policy="pad"para centralizar o preenchimento- Sem modo rect: os pipelines DALI geram saídas de tamanho fixo (por exemplo, 640×640). O modo rect
auto=True, que gera saídas de tamanho variável (por exemplo, 384×640), não é compatível. Observe que, embora o TensorRT seja compatível com formas de entrada dinâmicas, um pipeline DALI de tamanho fixo combina naturalmente com um mecanismo de tamanho fixo para maximizar a taxa de processamento - Memória com várias instâncias: usar
instance_groupcomcount> 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 custo. O DALI elimina esse gargalo ao executar o pré-processamento na GPU. Os maiores ganhos ocorrem com entradas de alta resolução (1080p, 4K), tamanhos de lote grandes e sistemas com poucos núcleos de CPU por GPU.
Sim. Use
DALIGenericIteratorpara obter saídastorch.Tensorpré-processadas e, em seguida, passe-as paramodel.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.padadiciona preenchimento somente às bordas direita e inferior.fn.cropcomout_of_bounds_policy="pad"centraliza a imagem e adiciona preenchimento simetricamente em todos os lados, correspondendo ao comportamentoLetterBox(center=True)do Ultralytics.Quase idênticos. Defina
antialias=Falseemfn.resizepara corresponder acv2.INTER_LINEARdo OpenCV. Podem ocorrer pequenas diferenças de ponto flutuante (< 0.001) devido aos cálculos 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.
cvcuda.copymakeborder()do CV-CUDA permite definir explicitamente o preenchimento de cada lado, facilitando o letterbox centralizado. Escolha DALI para fluxos de trabalho baseados em pipeline (especialmente com Triton) e CV-CUDA para ter controle refinado no nível do operador em código de inferência personalizado.