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上で実行します。
- 画像をデコードする(JPEG/PNG)
- アスペクト比を維持しながらリサイズする
- 目標サイズに合わせてパディングする(レターボックス)
- ピクセル値を
[0, 255]から[0, 1]の範囲に正規化する - レイアウトをHWCからCHWに変換する
DALIを使うと、これらの処理をすべてGPU上で実行でき、CPUのボトルネックを解消できます。特に次のような場合に効果的です。
| シナリオ | DALIが役立つケース |
|---|---|
| GPU推論が高速な場合 | サブミリ秒の推論を実現するTensorRTエンジンでは、CPUでの前処理が主なコストになります |
| 高解像度の入力 | 1080pおよび4Kのビデオストリームでは、負荷の高いリサイズ処理が必要です |
| バッチサイズが大きい場合 | サーバー側で多数の画像を並列処理する推論 |
| CPUコアが限られている場合 | NVIDIA Jetsonなどのエッジデバイスや、GPUあたりのCPUコア数が少ない高密度GPUサーバー |
前提条件#
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.resize | fn.resize(mode="not_larger") |
| 2 | 中央揃えのパディング | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + 正規化 /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
レターボックス処理では、次の方法でアスペクト比を維持します。
- スケールを計算する:
r = min(target_h / h, target_w / w) (round(w * r), round(h * r))にリサイズする- 目標サイズに合わせるため、残りの領域をグレー(
114)でパディングする - 画像を中央に配置し、両側に均等にパディングする
YOLO向けDALIパイプライン#
推奨するDALIパイプラインは、UltralyticsのデフォルトのLetterBox(center=True)の動作を再現します。これは標準的なYOLO推論で使用される動作です。
中央揃えパイプライン(推奨、UltralyticsのLetterBoxに準拠)#
このバージョンは、中央揃えのパディングを使用したUltralyticsのデフォルトの前処理を正確に再現し、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.
"""
# 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 outputLetterBox(center=True)と完全に一致させる必要がない場合は、fn.crop(..., out_of_bounds_policy="pad")の代わりにfn.pad(...)を使うと、パディング処理を簡略化できます。この方法では右端と下端のみがパディングされるため、カスタムのデプロイパイプラインでは許容できる場合があります。ただし、Ultralyticsのデフォルトの中央揃えレターボックス処理とは完全には一致しません。
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のcv2.resizeとINTER_LINEARでは、アンチエイリアスは適用されません。CPUパイプラインに合わせるには、DALIで必ずantialias=Falseを設定してください。これを省略すると、微妙な数値の差が生じ、モデルの精度に影響する可能性があります。
パイプラインの実行#
# 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
# ボックス: 640x640のレターボックス座標をxyxy形式で格納した、形状(N, 4)のテンソル
# レターボックス後の(640, 640)から元のサイズ(orig_h, orig_w)へボックスをスケーリングする
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))これは、テンソルの直接入力、ビデオストリーム、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")torch.Tensorをmodel.predict()に渡すと、画像の前処理時間はCPUで前処理する場合の~1~10msに対して、~0.004ms(実質ゼロ)になります。テンソルはBCHW形式、float32(またはfloat16)で、[0, 1]に正規化されている必要があります。Ultralyticsは引き続き、デバイス転送とデータ型変換を自動的に処理します。
ビデオストリームでDALIを使う#
リアルタイムのビデオ処理では、fn.external_sourceを使って、OpenCV、GStreamer、カスタムキャプチャライブラリなど、あらゆるソースからフレームを入力します。
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 outputDALIを使ったTriton Inference Server#
本番環境へのデプロイでは、アンサンブルモデルを使用して、DALIによる前処理とTensorRTによる推論をTriton Inference Serverで組み合わせます。これにより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パイプラインをシリアライズします。
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にエクスポートする#
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は.metadataヘッダーを.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:推論リクエストを送信する#
Ultralyticsには、前処理と後処理を自動で行う組み込みのTritonサポートがあります。ただし、YOLO()は前処理済みのfloat32テンソルを送信する一方、アンサンブルがJPEGの生バイトデータを想定しているため、DALIアンサンブルでは機能しません。DALIアンサンブルではtritonclientを直接使用し、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")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モデルにはデフォルトのインスタンスグループを使用してください。
よくある質問#
はい。
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)が生じることがありますが、検出精度への測定可能な影響はありません。