Ir para o conteúdo

Pré-processamento GPU com NVIDIA

Introdução

Ao implementar Ultralytics YOLO em produção, o pré-processamento torna-se frequentemente o gargalo. Embora TensorRT consegue executar a inferência do modelo em apenas alguns milissegundos, o pré-processamento CPU(redimensionamento, preenchimento, normalização) pode demorar entre 2 e 10 ms por imagem, especialmente em altas resoluções. NVIDIA (Data Loading Library) resolve este problema transferindo todo o pipeline de pré-processamento para a GPU.

Este guia orienta-o na criação de pipelines DALI que reproduzem exatamenteYOLO Ultralytics YOLO , integrando-os com model.predict(), processamento de fluxos de vídeo e implementação de ponta a ponta com Triton Inference Server.

A quem se destina este guia?

Este guia destina-se a engenheiros que implementam YOLO em ambientes de produção onde CPU constitui um gargalo comprovado — normalmente TensorRT implementações em NVIDIA , pipelines de vídeo de alto rendimento ou Triton Inference Server configurações. Se estiver a executar uma inferência padrão com model.predict() e, desde que não haja um estrangulamento no pré-processamento, o CPU padrão CPU funciona bem.

Resumo rápido

  • Está a criar um pipeline DALI? Use fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize para reproduzir o pré-processamento «letterbox» YOLO na GPU.
  • Integração com Ultralytics? Passe a saída DALI como um torch.Tensor para model.predict() — Ultralytics automaticamente o pré-processamento de imagens.
  • Está a fazer a implementação com Triton? Utilize o backend DALI com um TensorRT paraCPU .

Por que utilizar o DALI no YOLO

Num pipeline 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. Ajustar ao tamanho pretendido (formato letterbox)
  4. Normalizar valores de píxeis de [0, 255] para [0, 1]
  5. Converter o layout de HWC para CHW

Com o DALI, todas estas operações são executadas na GPU, eliminando o CPU . Isto é especialmente útil quando:

CenárioPor que o DALI ajuda
GPU rápida GPUTensorRT Os motores com inferência inferior a um milésimo de segundo fazem com que CPU seja o custo dominante
Entradas de alta resoluçãoAs transmissões de vídeo em 1080p e 4K exigem operações de redimensionamento dispendiosas
Lotes de grande dimensãoProcessamento de inferência do lado do servidor de várias imagens em paralelo
Número limitado de CPUDispositivos de borda, como NVIDIA , ou GPU com grande densidade GPU e poucos CPU por GPU

Pré-requisitos

Apenas para Linux

NVIDIA é compatível apenas com Linux. Não está disponível no Windows nem no macOS.

Instale os pacotes necessários:

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

Requisitos:

  • GPU NVIDIA GPU capacidade de computação 5.0+ / Maxwell ou mais recente)
  • CUDA .CUDA ou superior ou 12.0 ou superior
  • Python .10-3.14
  • Sistema operativo Linux

Compreender YOLO

Antes de criar um pipeline DALI, é útil compreender exatamente o que Ultralytics 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 fluxo completo de pré-processamento em ultralytics/engine/predictor.py executa estes passos:

PassoFuncionamentoCPUEquivalente a DALI
1Redimensionar a caixa de correiocv2.resizefn.resize(mode="not_larger")
2Alinhamento 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 técnica de letterboxing preserva a proporção da imagem ao:

  1. Escala de cálculo: r = min(target_h / h, target_w / w)
  2. Redimensionar para (round(w * r), round(h * r))
  3. Preencher o espaço restante com cinzento (114) para atingir o tamanho pretendido
  4. Centralizar a imagem de forma a que o preenchimento seja distribuído igualmente em ambos os lados

Pipeline DALI para YOLO

Utilize o fluxo centralizado abaixo como referência padrão. Corresponde ao Ultralytics LetterBox(center=True) comportamento, que é o que YOLO padrão utiliza.

Esta versão reproduz exatamente o Ultralytics padrão Ultralytics com preenchimento centralizado, correspondendo 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 é que fn.pad é suficiente?

Se não precisar de uma precisão exata LetterBox(center=True) paridade, pode simplificar a etapa de preenchimento utilizando fn.pad(...) em vez de fn.crop(..., out_of_bounds_policy="pad"). Essa variante preenche apenas o à direita e em baixo bordas, o que pode ser aceitável para pipelines de implementação personalizados, mas não corresponderá exatamente ao comportamento padrão de letterbox centrado Ultralytics.

Porquê fn.crop para o preenchimento centralizado?

da DALI fn.pad O operador apenas adiciona preenchimento ao à direita e em baixo bordas. Para obter um preenchimento centralizado (em consonância com Ultralytics LetterBox(center=True)), utilize fn.crop com out_of_bounds_policy="pad". Com a configuração padrão crop_pos_x=0.5 e crop_pos_y=0.5, a imagem é automaticamente centrada com um preenchimento simétrico.

Incompatibilidade de antialias

da DALI fn.resize ativa o antisserrilhamento por predefinição (antialias=True), enquanto OpenCV cv2.resize com INTER_LINEAR faz não aplicar suavização. Definir sempre antialias=False em DALI para corresponder ao CPU . A omissão desta configuração provoca diferenças numéricas subtis que podem afetar precisão do modelo.

Executar o pipeline

Criar 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}]")

Utilização do DALI com Ultralytics

Pode passar um PyTorch tensor para model.predict(). Quando um torch.Tensor é aprovado, Ultralytics ignora o pré-processamento de imagens (letterbox, BGR→RGB, HWC→CHW e normalização a /255) e apenas realiza a conversão de dispositivos e a conversão de tipos de dados antes de enviar os dados para o modelo.

Uma vez que Ultralytics tem acesso às dimensões originais da imagem neste caso, as coordenadas da caixa de deteção são apresentadas no espaço com formato letterbox de 640×640. Para as mapear de volta para as coordenadas da imagem original, utilize scale_boxes que gere a lógica de arredondamento exata utilizada 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))

Isto aplica-se a todos os percursos de pré-processamento externos — tensor direta tensor , fluxos de vídeo e Triton .

A 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")

Sem sobrecarga de pré-processamento

Quando passa por um torch.Tensor para model.predict(), a etapa de pré-processamento da imagem demora cerca de 0,004 ms (praticamente zero), em comparação com os cerca de 1 a 10 ms CPU . O tensor estar no formato BCHW, ser do tipo float32 (ou float16) e estar normalizado para [0, 1]. Ultralytics tratar automaticamente da conversão de dispositivos e de tipos de dados.

DALI com transmissões de vídeo

Para o processamento de vídeo em tempo real, utilize fn.external_source para receber imagens de qualquer fonte — OpenCV, GStreamer ou bibliotecas de captura personalizadas:

Pipeline DALI para pré-processamento de fluxos 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
import cv2
import numpy as np
import torch

from ultralytics import YOLO

model = YOLO("yolo26n.engine")  # TensorRT model

pipe = yolo_video_pipeline(target_size=640)
pipe.build()

cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # Feed BGR frame (convert to RGB for DALI)
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pipe.feed_input("input", [np.array(frame_rgb)])
    (output,) = pipe.run()

    # Convert DALI output to torch tensor for inference.
    # This is a simple fallback path: using feed_input() with pipe.run() keeps a GPU->CPU->GPU copy.
    # For high-throughput deployments, prefer a reader-based pipeline plus DALIGenericIterator to keep data on GPU.
    tensor = torch.tensor(output.as_cpu().as_array()).to("cuda")
    results = model.predict(tensor, verbose=False)

Servidor Triton com DALI

Para a implementação em produção, combine o pré-processamento DALI com TensorRT no Triton Server utilizando um modelo de conjunto. Isto elimina totalmente CPU — os bytes JPEG brutos entram, as deteçõ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: Criar o pipeline DALI

Serializar o pipeline DALI para o backend Triton :

Serializar o pipeline DALI para 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: Exportar YOLO TensorRT

Exportar 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.plan

Passo 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.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 conjuntos

O conjunto liga os modelos através de tensor virtuais. O output_map valor "preprocessed_image" na etapa DALI corresponde ao input_map valor "preprocessed_image" na TensorRT do TensorRT . Trata-se de nomes arbitrários que associam a saída de uma etapa à entrada da etapa seguinte — não precisam de corresponder a quaisquer tensor internos do modelo.

Passo 4: Enviar pedidos de inferência

Porquê tritonclient em vez de YOLO(\"http://...\")?

A Ultralytics Triton integrado para Triton que trata do pré e pós-processamento automaticamente. No entanto, não funciona com o conjunto DALI porque YOLO() envia um tensor float32 pré-processado, tensor o conjunto espera bytes JPEG não processados. Use tritonclient diretamente para conjuntos DALI, e o integração integrada para instalações padrão sem DALI.

Enviar imagens para 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 Triton, preencha todas as matrizes de bytes codificados até atingirem o mesmo comprimento (o número máximo de bytes do lote). Triton os lotes tenham formas homogéneas para o tensor de entrada.

Tarefas Suportadas

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

TarefaCompatívelNotas
DetecçãoPré-processamento padrão de caixas de correio
SegmentaçãoO mesmo pré-processamento que na deteção
Estimativa de PoseO mesmo pré-processamento que na deteção
Detecção Orientada (OBB)O mesmo pré-processamento que na deteção
ClassificaçãoUtiliza transformações do Torchvision (recorte central), em vez do formato letterbox

Limitações

  • Apenas para Linux: o DALI não é compatível com Windows nem com macOS
  • GPU NVIDIA : não há alternativa CPU
  • Pipeline estático: a estrutura do pipeline é definida no momento da compilação e não pode ser alterada dinamicamente
  • fn.pad apenas à direita/em baixo: Use fn.crop com out_of_bounds_policy="pad" para preenchimento centralizado
  • Sem modo retangular: Os pipelines DALI produzem saídas de tamanho fixo (por exemplo, 640×640). O auto=True O modo de saída que produz imagens de tamanho variável (por exemplo, 384×640) não é suportado. Note que, embora TensorRT Embora suporte formatos de entrada dinâmicos, um pipeline DALI de tamanho fixo combina naturalmente com um motor de tamanho fixo para obter o máximo rendimento
  • Memória com múltiplas instâncias: Utilizando instance_group com count > A versão 1 do Triton causar um elevado consumo de memória. Utilize o grupo de instâncias predefinido para o modelo DALI

FAQ

Como se compara a velocidade do pré-processamento DALI com a CPU ?

A vantagem depende do seu pipeline. Quando GPU já é rápida com TensorRT, CPU , com duração de 2 a 10 ms, pode tornar-se o custo dominante. O DALI elimina este gargalo ao executar o pré-processamento na GPU. Os maiores ganhos observam-se com entradas de alta resolução (1080p, 4K), lotes de grande dimensão e sistemas com um número limitado CPU por GPU.

Posso utilizar o DALI com PyTorch (e não apenas com TensorRT)?

Sim. Utilize DALIGenericIterator para serem pré-processados torch.Tensor resultados e, em seguida, transmita-os para model.predict(). No entanto, o ganho de desempenho é maior com TensorRT modelos em que a inferência já é muito rápida e CPU passa a ser o gargalo.

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

fn.pad adiciona espaçamento apenas ao à direita e em baixo bordas. fn.crop com out_of_bounds_policy="pad" alinhar a imagem ao centro e adicionar um preenchimento simétrico em todos os lados, em conformidade com Ultralytics LetterBox(center=True) comportamento.

O DALI produz resultados com pixels idênticos aos CPU ?

Quase idênticos. Conjunto antialias=False em fn.resize para corresponder OpenCV cv2.INTER_LINEAR. Minor floating-point differences (< 0.001) may occur due to GPU vs CPU arithmetic, but these have no measurable impact on detection precisão.

E quanto aoCUDA alternativa ao DALI?

CUDA é outra NVIDIA para o processamento de imagens GPU. Oferece controlo por operador (como OpenCV mas na GPU), em vez da abordagem de pipeline do DALI.CUDA cvcuda.copymakeborder() suporta preenchimento explícito por lado, facilitando a criação de uma moldura centralizada. Escolha o DALI para fluxos de trabalho baseados em pipeline (especialmente com Triton), eCUDA um controlo minucioso ao nível do operador em código de inferência personalizado.



📅 Criado há 0 dias ✏️ Atualizado há 0 dias
raimbekovm

Comentários