YOLO Vision 2026:

NVIDIA DALIによるGPU加速前処理#

Ultralytics YOLO モデルをプロダクション環境にデプロイする場合、前処理がボトルネックになることがよくあります。TensorRT はモデルの推論をわずか数ミリ秒で実行できますが、CPUベースの前処理(リサイズ、パディング、正規化)は、特に高解像度の場合、画像あたり2〜10msかかります。NVIDIA DALI(Data Loading Library)は、前処理パイプライン全体をGPUに移行することでこの問題を解決します。

このガイドでは、Ultralytics YOLOの前処理を正確に再現するDALIパイプラインの構築方法、model.predict() との統合、ビデオストリームの処理、および Triton Inference Server を使用したエンドツーエンドのデプロイについて説明します。

本ガイドの対象読者

このガイドは、CPUの前処理がボトルネックとして測定されるプロダクション環境(通常はNVIDIA GPU上の TensorRT デプロイ、高スループットのビデオパイプライン、または Triton Inference Server セットアップ)にYOLOモデルをデプロイするエンジニアを対象としています。model.predict() を使用して標準的な推論を実行しており、前処理のボトルネックがない場合、デフォルトのCPUパイプラインで十分に機能します。

クイックサマリー
  • DALIパイプラインを構築中ですか? fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize を使用して、YOLOのレターボックス前処理をGPU上で再現します。
  • Ultralyticsとの統合 DALI出力を torch.Tensor として model.predict() に渡します。Ultralyticsは画像の前処理を自動的にスキップします。
  • Tritonでのデプロイ: TensorRTアンサンブルと組み合わせたDALIバックエンドを使用することで、CPU前処理をゼロにします。

YOLOの前処理にDALIを使用する理由#

一般的なYOLOの推論パイプラインでは、前処理ステップはCPUで実行されます:

  1. デコード: 画像の読み込み (JPEG/PNG)
  2. リサイズ: アスペクト比を維持したリサイズ
  3. パディング: ターゲットサイズへの埋め込み (レターボックス)
  4. [0, 255] から [0, 1] へのピクセル値の正規化
  5. 変換: レイアウトをHWCからCHWへ変換

DALIを使用すると、これらの操作すべてがGPU上で実行されるため、CPUのボトルネックが解消されます。これは特に以下のような場合に有効です。

シナリオDALIが役立つ理由
高速なGPU推論サブミリ秒推論を備えた TensorRT エンジンにより、CPU前処理が主要なコストになる
高解像度の入力1080pや4Kのビデオストリームでは、負荷の高いリサイズ操作が要求されます
大規模なバッチサイズサーバーサイドの推論で多数の画像を並列処理する場合
限られたCPUコアNVIDIA Jetson などのエッジデバイス、またはGPUあたりのCPUコア数が少ない高密度GPUサーバー

前提条件#

Linuxのみ

NVIDIA DALIはLinuxでのみサポートされています。WindowsやmacOSでは利用できません。

必要なパッケージをインストールします:

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

要件:

  • NVIDIA GPU (計算能力 5.0以上 / Maxwell以降)
  • CUDA 11.0+, 12.0+ または 13.0+
  • Python 3.10-3.14
  • Linuxオペレーティングシステム

YOLOの前処理を理解する#

DALIパイプラインを構築する前に、前処理中にUltralyticsが正確に何を行っているのかを理解しておく価値があります。主要なクラスは ultralytics/data/augment.py 内の LetterBox です。

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

ultralytics/engine/predictor.py 内の完全な前処理パイプラインは、以下のステップを実行します。

ステップ操作CPU関数DALIの代替
1レターボックスリサイズcv2.resizefn.resize(mode="not_larger")
2中央寄せパディングcv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + 正規化 /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

レターボックス操作は以下のようにしてアスペクト比を維持します:

  1. スケールの計算: r = min(target_h / h, target_w / w)
  2. (round(w * r), round(h * r)) へのリサイズ
  3. ターゲットサイズに達するまで、残りのスペースをグレー(114)でパディングする
  4. 両側のパディングが均等になるように画像を中央に配置する

YOLO用DALIパイプライン#

推奨されるDALIパイプラインは、標準的なYOLO推論で使用されるUltralyticsのデフォルトの LetterBox(center=True) の動作を再現します。

センターパイプライン (推奨、Ultralytics LetterBoxと一致)#

このバージョンは、中央揃えのパディングでデフォルトのUltralytics前処理を正確に再現し、LetterBox(center=True) と一致させます。

中央寄せパディング付きDALIパイプライン (推奨)
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
`fn.pad`で十分なケースは?

正確な LetterBox(center=True) の一致を必要としない場合は、fn.crop(..., out_of_bounds_policy="pad") の代わりに fn.pad(...) を使用してパディングステップを簡素化できます。そのバリアントは右側と下側のエッジのみにパディングを追加します。これはカスタムデプロイメントパイプラインでは許容される場合がありますが、Ultralyticsのデフォルトの中央揃えレターボックス動作とは正確に一致しません。

なぜ中央寄せパディングに`fn.crop`を使うのか?

DALIの fn.pad オペレーターは、右側と下側のエッジにのみパディングを追加します。中央揃えのパディングを取得するには(Ultralyticsの LetterBox(center=True) と一致させる)、fn.cropout_of_bounds_policy="pad" を使用します。デフォルトの crop_pos_x=0.5 および crop_pos_y=0.5 を使用すると、画像は対称的なパディングで自動的に中央揃えされます。

アンチエイリアスの不一致

DALIの fn.resize はデフォルトでアンチエイリアスを有効にしますが(antialias=True)、INTER_LINEAR を使用するOpenCVの cv2.resize はアンチエイリアスを適用しません。CPUパイプラインと一致させるために、DALIで必ず antialias=False を設定してください。これを省略すると、モデル精度に影響を与える微妙な数値の差が生じます。

パイプラインの実行#

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

Ultralytics PredictとDALIの併用#

前処理された PyTorch テンソルを model.predict() に直接渡すことができます。torch.Tensor が渡されると、Ultralyticsは画像の前処理(レターボックス、BGR→RGB、HWC→CHW、および /255 の正規化)をスキップし、デバイス転送と dtype キャストのみを実行してモデルに送信します。

この場合、Ultralyticsは元の画像の寸法にアクセスできないため、検出ボックスの座標は640×640のレターボックススペースで返されます。それらを元の画像の座標にマッピングし直すには、LetterBox によって使用される正確な丸めロジックを処理する scale_boxes を使用してください。

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

これは、直接のテンソル入力、ビデオストリーム、Tritonデプロイメントなど、すべての外部前処理パスに適用されます。

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")
前処理オーバーヘッドゼロ

torch.Tensormodel.predict() に渡す場合、CPU前処理での約1〜10msと比較して、画像前処理ステップは ~0.004ms(実質的にゼロ)かかります。テンソルは BCHW 形式、float32(または float16)であり、[0, 1] に正規化されている必要があります。Ultralyticsは引き続きデバイス転送と dtype キャストを自動的に処理します。

ビデオストリームでのDALI利用#

リアルタイムビデオ処理の場合は、fn.external_source を使用して、OpenCV、GStreamer、またはカスタムキャプチャライブラリなどのあらゆるソースからフレームを供給します。

ビデオストリーム前処理用DALIパイプライン
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とDALI#

本番環境へのデプロイでは、アンサンブルモデルを使用して、DALI前処理と Triton Inference Server での TensorRT 推論を組み合わせます。これによりCPU前処理が完全に排除され、生のJPEGバイトが入力され、検出結果が出力され、すべてがGPU上で処理されます。

モデルリポジトリの構造#

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

ステップ1: DALIパイプラインの作成#

Triton DALIバックエンド用にDALIパイプラインをシリアライズします:

Triton用DALIパイプラインのシリアライズ
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")

ステップ2: YOLOのTensorRTエクスポート#

YOLOモデルを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

ステップ3: 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"
      }
    }
  ]
}
アンサンブルマッピングの仕組み

アンサンブルは仮想テンソル名を介してモデルを接続します。DALIステップの output_map"preprocessed_image" は、TensorRTステップの input_map"preprocessed_image" と一致します。これらは、あるステップの出力を次のステップの入力にリンクする任意の名前であり、モデルの内部テンソル名と一致させる必要はありません。

ステップ4: 推論リクエストの送信#

`YOLO('http://...')` ではなくなぜ `tritonclient` を使用するのですか?

Ultralyticsには、前処理および後処理を自動的に処理する組み込みのTritonサポートがあります。ただし、YOLO() は前処理された float32 テンソルを送信する一方で、アンサンブルは生のJPEGバイトを期待するため、DALIアンサンブルでは機能しません。DALIアンサンブルには tritonclient を直接使用し、DALIを使用しない標準的なデプロイには組み込みの統合を使用してください。

画像を 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")
JPEG 画像のバッチ処理

JPEG 画像のバッチを Triton に送信する場合、すべてのエンコード済みバイト配列を同じ長さ(バッチ内の最大バイト数)にパディングしてください。Triton では、入力テンソルに対してバッチ形状が均一である必要があります。

サポートされているタスク#

DALIの前処理は、標準の LetterBox パイプラインを使用するすべてのYOLOタスクで機能します。

タスクサポート状況注意点
Detection標準のレターボックス前処理
Instance Segmentation検出と同じ前処理
Semantic Segmentation検出と同じ画像前処理
Classificationレターボックスではなく、torchvision 変換 (中心クロップ) を使用します
Pose Estimation検出と同じ前処理
方向付き検出(OBB)検出と同じ前処理

制限事項#

  • Linux 専用: DALI は Windows または macOS をサポートしていません
  • NVIDIA GPU 必須: CPU のみのフォールバックはありません
  • 静的パイプライン: パイプライン構造はビルド時に定義され、動的に変更することはできません
  • fn.pad は右/下のみ: 中央揃えのパディングには out_of_bounds_policy="pad" と共に fn.crop を使用します
  • rect モードなし: DALIパイプラインは固定サイズの出力(例: 640×640)を生成します。可変サイズの出力(例: 384×640)を生成する auto=True rect モードはサポートされていません。TensorRT は動的な入力形状をサポートしていますが、固定サイズのDALIパイプラインは最大スループットのために固定サイズのエンジンと自然にペアになることに注意してください
  • 複数のインスタンスでのメモリ: Tritonで instance_groupcount > 1 を使用すると、メモリ使用量が高くなる可能性があります。DALIモデルにはデフォルトのインスタンスグループを使用してください

よくある質問 (FAQ)#

  • 利点はパイプラインによって異なります。TensorRT によるGPU推論がすでに高速である場合、2〜10msのCPU前処理が主要なコストになる可能性があります。DALIは、GPU上で前処理を実行することにより、このボトルネックを解消します。最大の利益は、高解像度の入力(1080p、4K)、大規模なバッチサイズ、およびGPUあたりのCPUコア数が限られているシステムで見られます。

  • はい。DALIGenericIterator を使用して前処理された torch.Tensor の出力を取得し、それを model.predict() に渡します。ただし、パフォーマンスの利得が最も大きいのは、推論がすでに非常に高速であり、CPU前処理がボトルネックになる TensorRT モデルを使用する場合です。

  • fn.pad右側と下側のエッジにのみパディングを追加します。out_of_bounds_policy="pad" を伴う fn.crop は画像を中央揃えにし、すべての側に対称的にパディングを追加して、Ultralyticsの LetterBox(center=True) の動作と一致させます。

  • ほぼ同一です。OpenCVの cv2.INTER_LINEAR に一致させるには、fn.resize 内で antialias=False を設定します。GPUとCPUの演算の違いにより、わずかな浮動小数点数の差(< 0.001)が発生する場合がありますが、これらは検出精度に測定可能な影響を与えません。

  • CV-CUDA は、GPUアクセラレーションによるビジョン処理のためのもう1つのNVIDIAライブラリです。DALIのパイプラインアプローチではなく、オペレーターごとの制御(OpenCV のようであるがGPU上で行う)を提供します。CV-CUDA の cvcuda.copymakeborder() は、明示的な側面ごとのパディングをサポートしており、中央揃えのレターボックスをシンプルにします。パイプラインベースのワークフロー(特に Triton を使用する場合)にはDALIを選択し、カスタム推論コードでのきめ細かなオペレーターレベルの制御にはCV-CUDAを選択してください。

コメント