Ultralytics YOLO27: ウェイティングリストに登録してください。
Get Started

NVIDIA DALIによるGPUアクセラレーション対応の前処理#

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

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

このガイドの対象読者

このガイドは、CPUによる前処理が計測可能なボトルネックとなっている本番環境でYOLOモデルをデプロイするエンジニアを対象としています。具体的には、NVIDIA GPUでのTensorRTデプロイ、高スループットのビデオパイプライン、Triton Inference Serverのセットアップなどが該当します。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でデプロイする場合: DALIバックエンドとTensorRTアンサンブルを使用すると、CPUを使わずに前処理を実行できます。

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

一般的なYOLOの推論パイプラインでは、前処理の手順をCPU上で実行します。

  1. デコードして画像を読み込みます(JPEG/PNG)。
  2. アスペクト比を維持しながらリサイズします。
  3. パディングしてターゲットサイズに合わせます(レターボックス)
  4. ピクセル値を[0, 255]から[0, 1]の範囲に正規化します
  5. レイアウトをHWCからCHWに変換します

DALIでは、これらの処理はすべてGPU上で実行されるため、CPUがボトルネックになりません。これは特に、次のような場合に有効です。

シナリオDALIが役立つ理由
高速なGPU推論TensorRTエンジンの推論時間が1ミリ秒未満の場合、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),  # ターゲットサイズ
    center=True,  # 画像を中央に配置します(両側に均等にパディングします)
    stride=32,  # ストライドに合わせます
    padding_value=114,  # グレーでパディングします(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 + /255で正規化np.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パイプラインは、UltralyticsのデフォルトのLetterBox(center=True)の動作を再現します。これは標準的なYOLO推論で使用される動作です。

このバージョンは中央揃えのパディングを使用し、デフォルトの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.
    """
    # 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)

    # アスペクト比を維持してリサイズします
    resized = fn.resize(
        images,
        resize_x=target_size,
        resize_y=target_size,
        mode="not_larger",
        interp_type=types.INTERP_LINEAR,
        antialias=False,  # cv2.INTER_LINEARに合わせます(アンチエイリアスなし)
    )

    # out_of_bounds_policyを指定したfn.cropで中央揃えのパディングを行います
    # クロップサイズが画像サイズより大きい場合、fn.cropは画像を中央に配置し、左右対称にパディングします
    padded = fn.crop(
        resized,
        crop=(target_size, target_size),
        out_of_bounds_policy="pad",
        fill_values=114,  # YOLOのパディング値
    )

    # 正規化してレイアウトを変換します
    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)に準拠)にするには、out_of_bounds_policy="pad"とともにfn.cropを使用します。デフォルトのcrop_pos_x=0.5とcrop_pos_y=0.5を使うと、画像は自動的に中央に配置され、左右対称にパディングされます。

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

DALIのfn.resizeはデフォルトでアンチエイリアス(antialias=True)を有効にしますが、OpenCVのINTER_LINEARを指定した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による正規化)をスキップし、モデルに渡す前にデバイス転送とデータ型変換のみを行います。

この場合、Ultralyticsは元画像の寸法にアクセスできないため、検出ボックスの座標は640×640のレターボックス適用後の空間で返されます。元画像の座標に戻すには、LetterBoxで使われている正確な丸め処理を行うscale_boxesを使用します。

from ultralytics.utils.ops import scale_boxes

# boxes: 640x640のレターボックス適用後の座標をxyxy形式で格納した形状(N, 4)のテンソル
# ボックスをレターボックス適用後のサイズ(640, 640)から元のサイズ(orig_h, orig_w)に戻します
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))

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

DALI + Ultralytics Predict
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.Tensorをmodel.predict()に渡すと、画像の前処理時間は約0.004ms(実質ゼロ)になります。CPUで前処理する場合は約1~10msかかります。テンソルはBCHW形式で、float32(またはfloat16)とし、[0, 1]に正規化する必要があります。デバイス転送とデータ型変換は、引き続きUltralyticsが自動で処理します。

ビデオストリームでの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."""
    # OpenCVやGStreamerなどからフレームを入力するための外部ソース
    frames = fn.external_source(device="cpu", name="input")
    frames = fn.reshape(frames, layout="HWC")

    # GPUに転送して前処理します
    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

DALIを使用したTriton Inference Server#

本番環境へのデプロイでは、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."""
    # 入力: 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

# パイプラインをモデルリポジトリにシリアライズします
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")

ステップ2: YOLOをTensorRTにエクスポートする#

YOLOモデルを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
)  # NMSなし(N, 300, 6); TensorRT >= 8.5

# Ultralyticsは.engineファイルの先頭にメタデータヘッダーを付加します。Tritonが生のTensorRTプランを読み込めるよう、ヘッダーを削除してください
with open(engine_path, "rb") as f:
    meta_len = int.from_bytes(f.read(4), byteorder="little")  # 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)

ステップ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: 推論リクエストを送信する#

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

Ultralyticsには、前処理と後処理を自動で処理する組み込みのTritonサポートがあります。ただし、YOLO()が前処理済みのfloat32テンソルを送信する一方、DALIアンサンブルは未加工の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 for the nms=False export)
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タスクで動作します。

タスクサポート対象備考
物体検出✅標準的なレターボックス前処理
インスタンスセグメンテーション✅検出と同じ前処理
セマンティックセグメンテーション✅検出と同じ画像前処理
深度推定✅検出と同じ画像前処理
分類❌レターボックスではなく、torchvisionの変換(中央クロップ)を使用します
姿勢推定✅検出と同じ前処理
方向付き検出(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でcountを1より大きくしてinstance_groupを使用すると、メモリ使用量が大幅に増える場合があります。DALIモデルにはデフォルトのインスタンスグループを使用してください

よくある質問#

  • 効果はパイプラインによって異なります。TensorRTでGPU推論がすでに高速な場合、2~10 msかかるCPU前処理が支配的なコストになることがあります。DALIはGPU上で前処理を実行することで、このボトルネックを解消します。特に大きな効果が見られるのは、高解像度入力(1080p、4K)、大きなバッチサイズ、およびGPUあたりのCPUコア数が限られたシステムです。

  • はい。DALIGenericIteratorを使用して前処理済みのtorch.Tensor出力を取得し、それをmodel.predict()に渡してください。ただし、TensorRTモデルでは推論がすでに非常に高速で、CPU前処理がボトルネックになるため、性能上の効果が最も大きくなります。

  • 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アクセラレーションによるビジョン処理のためのNVIDIAライブラリです。DALIのパイプライン方式ではなく、(GPU上で動作するOpenCVのように)オペレーター単位で制御できます。CV-CUDAのcvcuda.copymakeborder()は、辺ごとの明示的なパディングをサポートしているため、中央揃えのレターボックスを簡単に実現できます。パイプラインベースのワークフロー(特にTritonとの連携)にはDALIを、カスタム推論コードでオペレーター単位のきめ細かな制御を行うにはCV-CUDAを選択してください。

コメント