Link to this sectionNVIDIA DALIによるGPUアクセラレーション付きプリプロセッシング#
Link to this sectionはじめに#
Ultralytics YOLOモデルを本番環境でデプロイする場合、プリプロセッシングがボトルネックになることがよくあります。TensorRTを使えばモデルの推論を数ミリ秒で実行できますが、CPUベースのプリプロセッシング(リサイズ、パディング、正規化)には、特に高解像度の場合、1画像あたり2~10msかかることがあります。NVIDIA DALI(Data Loading Library)は、プリプロセッシングパイプライン全体を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を使用して、GPU上でYOLOのレターボックス・プリプロセッシングを再現します。 - Ultralyticsとの統合方法: DALIの出力を
torch.Tensorとしてmodel.predict()に渡します。これにより、Ultralyticsは自動的に画像プリプロセッシングをスキップします。 - Tritonでのデプロイ方法: DALIバックエンドとTensorRTアンサンブルを使用して、CPUプリプロセッシングをゼロにします。
Link to this sectionなぜ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サーバーの場合 |
Link to this section前提条件#
NVIDIA DALIはLinuxでのみサポートされています。WindowsやmacOSでは利用できません。
必要なパッケージをインストールします。
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130要件:
- NVIDIA GPU(Compute Capability 5.0以上 / Maxwell以降)
- CUDA 11.0+, 12.0+, または 13.0+
- Python 3.10-3.14
- Linuxオペレーティングシステム
Link to this sectionYOLOプリプロセッシングの理解#
Before building a DALI pipeline, it helps to understand exactly what Ultralytics does during preprocessing. The key class is LetterBox in 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)
)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)で埋める - パディングが両側に均等に分配されるように画像を中央に配置する
Link to this sectionYOLO用DALIパイプライン#
以下のセンター配置パイプラインをデフォルトの参照として使用してください。これは標準的なYOLO推論で使用されるUltralyticsの LetterBox(center=True) の挙動と一致します。
Link to this sectionセンター配置パイプライン(推奨、UltralyticsのLetterBoxと一致)#
このバージョンは、Ultralyticsのデフォルトのプリプロセッシングを中央パディングで正確に再現しており、LetterBox(center=True) と一致します。
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 outputIf you do not need exact LetterBox(center=True) parity, you can simplify the padding step by using fn.pad(...) instead of fn.crop(..., out_of_bounds_policy="pad"). That variant pads only the right and bottom edges, which can be acceptable for custom deployment pipelines, but it will not match Ultralytics' default centered letterbox behavior exactly.
DALI's fn.pad operator only adds padding to the right and bottom edges. To get centered padding (matching Ultralytics LetterBox(center=True)), use fn.crop with out_of_bounds_policy="pad". With the default crop_pos_x=0.5 and crop_pos_y=0.5, the image is automatically centered with symmetric padding.
DALIの fn.resize はデフォルトでアンチエイリアスを有効にしますが(antialias=True)、OpenCVの cv2.resize (INTER_LINEAR使用時)はアンチエイリアスを適用しません。CPUパイプラインと一致させるには、DALIで常に antialias=False を設定してください。これを省略すると、モデル精度に影響を及ぼすわずかな数値上の差異が生じる可能性があります。
Link to this sectionパイプラインの実行#
# 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}]")Link to this sectionUltralyticsのPredictとDALIの使用#
プリプロセッシング済みの PyTorch テンソルを model.predict() に直接渡すことができます。torch.Tensor が渡されると、Ultralyticsは画像プリプロセッシング(レターボックス、BGR→RGB、HWC→CHW、および /255 正規化)をスキップし、デバイス転送とデータ型変換のみを行ってからモデルに送信します。
Since Ultralytics doesn't have access to the original image dimensions in this case, detection box coordinates are returned in the 640×640 letterboxed space. To map them back to original image coordinates, use scale_boxes which handles the exact rounding logic used by 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))これは、直接テンソル入力、ビデオストリーム、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() に渡す場合、画像プリプロセッシングのステップには ~0.004ms(実質ゼロ)しかかかりません。CPUプリプロセッシングの ~1-10ms とは対照的です。テンソルはBCHW形式、float32(またはfloat16)であり、[0, 1] に正規化されている必要があります。Ultralyticsは、デバイス転送とデータ型変換を引き続き自動的に処理します。
Link to this sectionビデオストリームとDALI#
リアルタイムビデオ処理では、fn.external_source を使用して、OpenCV、GStreamer、またはカスタムキャプチャライブラリなどのソースからフレームを供給します。
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 outputLink to this sectionDALIを用いたTriton Inference Server#
For production deployment, combine DALI preprocessing with TensorRT inference in Triton Inference Server using an ensemble model. This eliminates CPU preprocessing entirely — raw JPEG bytes go in, detections come out, with everything processed on the GPU.
Link to this sectionモデルリポジトリ構造#
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.pbtxtLink to this sectionステップ 1: DALIパイプラインの作成#
Triton DALIバックエンド向けにDALIパイプラインをシリアライズします。
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")Link to this sectionステップ 2: YOLOを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.planLink to this sectionステップ 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"と一致します。これらはあるステップの出力を次のステップの入力にリンクするための任意の名前であり、モデル内部のテンソル名と一致させる必要はありません。
Link to this sectionステップ4: 推論リクエストの送信#
!!! info "Why tritonclient instead of YOLO(\"http://...\")?"
Ultralytics has [built-in Triton support](triton-inference-server.md#running-inference) that handles pre/postprocessing automatically. However, it won't work with the DALI ensemble because `YOLO()` sends a preprocessed float32 tensor while the ensemble expects raw JPEG bytes. Use `tritonclient` directly for DALI ensembles, and the [built-in integration](triton-inference-server.md) for standard deployments without 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 — 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画像のバッチをTritonに送信する際は、すべてのエンコード済みバイト配列を同じ長さ(バッチ内の最大バイト数)にパディングしてください。Tritonは入力テンソルに対して均一なバッチ形状を要求します。
Link to this sectionサポートされているタスク#
DALIの前処理は、標準のLetterBoxパイプラインを使用するすべてのYOLOタスクで動作します。
| タスク | サポート状況 | 備考 |
|---|---|---|
| Detection | ✅ | 標準のレターボックス前処理 |
| Instance Segmentation | ✅ | 物体検出と同じ前処理 |
| セマンティックセグメンテーション | ✅ | 物体検出と同じ画像前処理 |
| 姿勢推定 | ✅ | 物体検出と同じ前処理 |
| 指向性物体検出 (OBB) | ✅ | 物体検出と同じ前処理 |
| 分類 | ❌ | レターボックスではなくtorchvisionの変換(センタークロップ)を使用 |
Link to this section制限事項#
- Linuxのみ: DALIはWindowsまたはmacOSをサポートしていません
- NVIDIA GPUが必要: CPUのみのフォールバックはありません
- 静的パイプライン: パイプライン構造はビルド時に定義され、動的に変更することはできません
fn.padis right/bottom only: Usefn.cropwithout_of_bounds_policy="pad"for centered padding- 矩形モードなし: DALIパイプラインは固定サイズの出力(例: 640×640)を生成します。可変サイズの出力(例: 384×640)を生成する
auto=Trueの矩形モードはサポートされていません。TensorRTは動的入力形状をサポートしていますが、固定サイズのDALIパイプラインは、最大スループットのために固定サイズのエンジンと組み合わせるのが自然です - 複数インスタンスでのメモリ: Tritonで
instance_groupのcountを1より大きく設定すると、メモリ使用量が増加する可能性があります。DALIモデルにはデフォルトのインスタンスグループを使用してください
Link to this sectionFAQ#
Link to this sectionDALIの前処理はCPUでの前処理と比べてどの程度の速度ですか?#
利点はパイプラインに依存します。TensorRTでGPU推論が既に高速な場合、2〜10msかかるCPU前処理が支配的なコストになることがあります。DALIは前処理をGPU上で実行することでこのボトルネックを排除します。最大の利益は、高解像度の入力(1080p、4K)、大きなバッチサイズ、およびGPUあたりのCPUコア数が制限されているシステムで見られます。
Link to this sectionDALIを(TensorRTだけでなく)PyTorchモデルで使用できますか?#
はい、可能です。DALIGenericIteratorを使用して前処理済みのtorch.Tensor出力を取得し、それをmodel.predict()に渡します。ただし、パフォーマンス上のメリットが最大化されるのは、既に推論が非常に高速で、CPUの前処理がボトルネックとなっているTensorRTモデルの場合です。
Link to this sectionパディングにおけるfn.padとfn.cropの違いは何ですか?#
fn.pad adds padding only to the right and bottom edges. fn.crop with out_of_bounds_policy="pad" centers the image and adds padding symmetrically on all sides, matching Ultralytics LetterBox(center=True) behavior.
Link to this sectionDALIはCPUの前処理とピクセル単位で同一の結果を生成しますか?#
Nearly identical. Set antialias=False in fn.resize to match OpenCV's 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 accuracy.
Link to this sectionDALIの代替としてのCV-CUDAはどうでしょうか?#
CV-CUDAは、GPUアクセラレーションによる画像処理のためのもう一つのNVIDIAライブラリです。DALIのパイプラインアプローチとは異なり、オペレーター単位の制御(OpenCVのようなものですがGPU上で動作)を提供します。CV-CUDAのcvcuda.copymakeborder()は辺ごとの明示的なパディングをサポートしており、中央揃えのレターボックスを簡単に実装できます。パイプラインベースのワークフロー(特にTritonを使用する場合)にはDALIを選択し、カスタム推論コードでの細かいオペレーターレベルの制御にはCV-CUDAを選択してください。