NVIDIA DALI를 사용한 GPU 가속 전처리#
프로덕션 환경에 Ultralytics YOLO 모델을 배포할 때, 전처리(preprocessing)는 종종 병목 현상의 원인이 됩니다. TensorRT는 몇 밀리초 만에 모델 추론(inference)을 실행할 수 있지만, 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 파이프라인을 구축하시나요? GPU에서 YOLO의 레터박스(letterbox) 전처리를 복제하려면
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalize를 사용하세요. - Ultralytics와의 통합: DALI 출력을
torch.Tensor으로 전달하여model.predict()에 입력하세요. Ultralytics는 이미지 전처리를 자동으로 건너뜁니다. - Triton으로 배포하시나요? TensorRT 앙상블과 함께 DALI 백엔드를 사용하여 CPU 전처리를 제로(zero)로 만드십시오.
YOLO 전처리에 DALI를 사용해야 하는 이유#
일반적인 YOLO 추론 파이프라인에서 전처리 단계는 CPU에서 실행됩니다:
- 이미지 디코딩(JPEG/PNG)
- 종횡비를 유지하며 크기 조정(Resize)
- 대상 크기에 맞춰 패딩(Pad) (레터박스)
[0, 255]에서[0, 1](으)로 픽셀 값을 정규화합니다.- 레이아웃을 HWC에서 CHW로 변환(Convert)
DALI를 사용하면 이러한 모든 작업이 GPU에서 실행되어 CPU 병목 현상이 제거됩니다. 이는 다음과 같은 경우에 특히 유용합니다:
| 시나리오 | DALI가 도움이 되는 이유 |
|---|---|
| 빠른 GPU 추론 | 밀리초 미만의 추론 속도를 제공하는 TensorRT 엔진은 CPU 전처리를 주요 비용 원인으로 만듭니다. |
| 고해상도 입력 | 1080p 및 4K 비디오 스트림은 값비싼 크기 조정 연산을 요구합니다. |
| 큰 배치 크기(batch sizes) | 서버 측 추론에서 많은 이미지를 병렬로 처리하는 경우 |
| 제한된 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), # 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))(으)로 크기 조정(Resizing)- 목표 크기에 도달하기 위해 남은 공간을 회색(
114)으로 패딩 - 패딩이 양쪽에 동일하게 분배되도록 이미지를 중앙에 배치
YOLO를 위한 DALI 파이프라인#
권장되는 DALI 파이프라인은 표준 YOLO 추론에서 사용하는 Ultralytics의 기본 LetterBox(center=True) 동작을 복제합니다.
중앙 파이프라인 (권장, 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.
"""
# 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정확한 LetterBox(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), INTER_LINEAR을 사용하는 OpenCV의 cv2.resize는 안티앨리어싱을 적용하지 않습니다. 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 정규화), 모델로 보내기 전에 디바이스 전송 및 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 배포를 포함한 모든 외부 전처리 경로에 적용됩니다.
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 전처리의 110ms에 비해 ~0.004ms(사실상 0) 소요됩니다. 텐서는 BCHW 형식, float32(또는 float16)여야 하며 [0, 1](으)로 정규화되어야 합니다. Ultralytics는 여전히 디바이스 전송 및 dtype 캐스팅을 자동으로 처리합니다.
비디오 스트림과 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."""
# 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 outputDALI를 사용하는 Triton Inference Server#
프로덕션 배포의 경우, 앙상블 모델을 사용하여 Triton Inference Server에서 DALI 전처리와 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.pbtxt1단계: 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."""
# 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로 내보내기#
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.plan3단계: 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 — YOLO26 is end-to-end)
detections = detections[0] # First image
detections = detections[detections[:, 4] > 0.25] # Confidence threshold
print(f"Detected {len(detections)} objects")Triton으로 JPEG 이미지 배치를 보낼 때는 모든 인코딩된 바이트 배열을 동일한 길이(배치 내 최대 바이트 수)로 패딩하십시오. 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=Truerect 모드는 지원되지 않습니다. TensorRT는 동적 입력 모양을 지원하지만, 고정 크기 DALI 파이프라인은 최대 처리량을 위해 고정 크기 엔진과 자연스럽게 조화를 이룹니다. - 다중 인스턴스 사용 시 메모리: Triton에서
count> 1인 상태로instance_group을 사용하면 높은 메모리 사용량이 발생할 수 있습니다. DALI 모델에는 기본 인스턴스 그룹을 사용하세요.
FAQ#
이점은 파이프라인에 따라 다릅니다. TensorRT를 통한 GPU 추론이 이미 빠른 경우, 2~10ms의 CPU 전처리가 주요 비용 원인이 될 수 있습니다. DALI는 GPU에서 전처리를 실행하여 이 병목 현상을 제거합니다. 고해상도 입력(1080p, 4K), 큰 배치 크기(batch sizes), 그리고 GPU당 CPU 코어가 제한된 시스템에서 가장 큰 성능 향상을 볼 수 있습니다.
네.
DALIGenericIterator을 사용하여 전처리된torch.Tensor출력을 얻은 다음model.predict()에 전달하세요. 그러나 추론이 이미 매우 빠르고 CPU 전처리가 병목 현상이 되는 TensorRT 모델에서 성능 향상이 가장 큽니다.fn.pad은 오른쪽 및 아래쪽 가장자리에만 패딩을 추가합니다.out_of_bounds_policy="pad"와 함께fn.crop을 사용하면 이미지를 중앙에 배치하고 모든 측면에 대칭으로 패딩을 추가하여 UltralyticsLetterBox(center=True)동작과 일치시킵니다.거의 동일합니다. OpenCV의
cv2.INTER_LINEAR와 일치시키려면fn.resize에서antialias=False을 설정하세요. GPU와 CPU 산술 연산의 차이로 인해 미세한 부동 소수점 차이(< 0.001)가 발생할 수 있지만, 이는 감지 정확도에 측정 가능한 영향을 미치지 않습니다.