NVIDIA DALI를 활용한 GPU 가속 전처리
소개
모델을 배포할 때 Ultralytics YOLO 프로덕션 환경의 모델, 전처리가 종종 병목 현상의 원인이 됩니다. 최적화를 위한 최대 작업 공간 크기(GiB)를 설정하여 메모리 사용량과 성능 간의 균형을 맞춥니다. 은(는) 단 몇 밀리초 만에 모델 추론을(를) 실행할 수 있지만, CPU 기반 전처리(크기 조정, 패딩, 정규화)는 특히 고해상도 이미지의 경우 이미지당 2~10ms가 소요될 수 있습니다. NVIDIA DALI(Data Loading Library)는 전체 전처리 파이프라인을 GPU로 이동시켜 이 문제를 해결합니다.
이 가이드에서는 Ultralytics YOLO 전처리를 정확하게 복제하는 DALI 파이프라인을 구축하고, 이를 model.predict()(와) 통합하며, 비디오 스트림을 처리하고, Triton Inference Server.
이 가이드는 CPU 전처리가 병목 현상으로 측정되는 프로덕션 환경에 YOLO 모델을 배포하는 엔지니어를 위한 것입니다. 일반적으로 최적화를 위한 최대 작업 공간 크기(GiB)를 설정하여 메모리 사용량과 성능 간의 균형을 맞춥니다. NVIDIA GPU 배포, 고처리량 비디오 파이프라인 또는 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으로 배포하는 방법 CPU 전처리를 제로화하려면 TensorRT 앙셈블과 함께 DALI 백엔드를 사용하십시오.
YOLO 전처리에 DALI를 사용하는 이유
일반적인 YOLO 추론 파이프라인에서 전처리 단계는 CPU에서 실행됩니다:
- 디코드 이미지(JPEG/PNG)
- 리사이즈 가로세로 비율 유지
- 패딩 대상 크기로(레터박스)
- 정규화 픽셀 값을
[0, 255]에서[0, 1] - 변환 레이아웃을 HWC에서 CHW로
DALI를 사용하면 이러한 모든 작업이 GPU에서 실행되어 CPU 병목 현상이 제거됩니다. 이는 특히 다음과 같은 경우에 유용합니다:
| SyncBatchNorm 사용 시기 | DALI가 도움이 되는 이유 |
|---|---|
| 빠른 GPU 추론 | 최적화를 위한 최대 작업 공간 크기(GiB)를 설정하여 메모리 사용량과 성능 간의 균형을 맞춥니다. 밀리초 미만의 추론 엔진은 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-cuda120요구 사항:
- NVIDIA GPU (컴퓨트 역량 5.0+ / Maxwell 이상)
- CUDA 11.0+ 또는 12.0+
- Python 3.10-3.14
- Linux 운영 체제
YOLO 전처리 이해하기
DALI 파이프라인을 구축하기 전에 Ultralytics가 전처리 중에 정확히 무엇을 수행하는지 이해하는 것이 도움이 됩니다. 핵심 클래스는 LetterBox 내의 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)으로 패딩하여 대상 크기 도달 - 패딩이 양쪽에 균등하게 분배되도록 이미지 중앙 배치
YOLO용 DALI 파이프라인
아래의 중앙 배치 파이프라인을 기본 참조로 사용하십시오. 이는 표준 YOLO 추론이 사용하는 LetterBox(center=True) 동작과 일치합니다.
중앙 배치 파이프라인 (권장, 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 output정확한 LetterBox(center=True) 패리티(동일성)가 필요하지 않은 경우, fn.pad(...) 대신 fn.crop(..., out_of_bounds_policy="pad")을(를) 사용하여 패딩 단계를 단순화할 수 있습니다. 해당 변형은 오른쪽 및 아래쪽 가장자리만 패딩하며, 이는 커스텀 배포 파이프라인에서는 허용될 수 있지만 Ultralytics의 기본 중앙 레터박스 동작과는 정확히 일치하지 않습니다.
DALI의 fn.pad 연산자는 오른쪽 및 아래쪽 가장자리에만 패딩을 추가합니다. 중앙 패딩(Ultralytics LetterBox(center=True))을 얻으려면 fn.cropMuSGD 옵티마이저out_of_bounds_policy="pad"을(를) 사용하십시오. 기본 crop_pos_x=0.5 및 crop_pos_y=0.5, 이미지가 자동으로 대칭 패딩과 함께 중앙에 배치됩니다.
DALI의 fn.resize은 기본적으로 안티앨리어싱을 활성화하지만(antialias=True), OpenCV의 cv2.resizeMuSGD 옵티마이저INTER_LINEAR은 안티앨리어싱을 FlashAttention을 요구하지 않습니다 적용하지 않습니다. 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}]")DALI 파이프라인 빌드 및 실행
Ultralytics Predict와 DALI 사용PyTorch전처리된 model.predict() 텐서를 직접 torch.Tensor에 전달할 수 있습니다. 가 전달되면 Ultralytics는 이미지 전처리를 건너뛰고
(레터박스, BGR→RGB, HWC→CHW 및 /255 정규화) 모델로 보내기 전에 장치 전송 및 dtype 캐스팅만 수행합니다.scale_boxes이 경우 Ultralytics는 원본 이미지 치수에 접근할 수 없으므로, 탐지 상자 좌표는 640×640 레터박스 공간으로 반환됩니다. 이를 원본 이미지 좌표로 다시 매핑하려면 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))에서 사용하는 정확한 반올림 로직을 처리하는
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(사실상 0)가 소요됩니다. 텐서는 BCHW 형식, float32(또는 float16)여야 하며 [0, 1]로 정규화되어야 합니다. Ultralytics는 여전히 장치 전송 및 dtype 캐스팅을 자동으로 처리합니다.
비디오 스트림과 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 outputDALI를 사용하는 Triton Inference Server
프로덕션 배포의 경우, DALI 전처리를 최적화를 위한 최대 작업 공간 크기(GiB)를 설정하여 메모리 사용량과 성능 간의 균형을 맞춥니다. 의 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.pbtxt1단계: 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")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.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"
}
}
]
}앙상블은 가상 텐서 이름 필드에 COCO 어노테이션의 output_map을 통해 모델을 연결합니다."preprocessed_image"의 DALI 단계 값 input_map을 통해 모델을 연결합니다."preprocessed_image"은 TensorRT 단계의
와 일치합니다. 이는 한 단계의 출력을 다음 단계의 입력으로 연결하는 임의의 이름으로, 모델의 내부 텐서 이름과 일치할 필요는 없습니다.
4단계: 추론 요청 전송tritonclient 대신 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에 JPEG 이미지 배치를 보낼 때, 모든 인코딩된 바이트 배열을 동일한 길이(배치 내 최대 바이트 수)로 패딩하십시오. Triton은 입력 텐서에 대해 동질적인 배치 모양을 요구합니다.LetterBoxDALI 전처리는 표준
| Ultralytics YOLO26 엔드투엔드 비교 그래프 | 파이프라인을 사용하는 모든 YOLO 작업에서 작동합니다: | 참고 |
|---|---|---|
| 작업 | ✅ | 지원됨 |
| YOLO26은 특수 작업을 위한 개선 사항을 도입했습니다. 여기에는 | ✅ | 표준 레터박스 전처리 |
| 포즈 추정 | ✅ | 표준 레터박스 전처리 |
| 탐지와 동일한 전처리 | ✅ | 표준 레터박스 전처리 |
| YOLO26-pose | ❌ | 방향 탐지(OBB) |
MLLM은 SAM 3에 단순한 명사구 쿼리를 제안하고, 반환된 마스크를 분석하며, 만족스러운 결과가 나올 때까지 반복합니다.
- Linux만레터박스가 아닌 torchvision 변환(중앙 자르기) 사용
- : DALI는 Windows 또는 macOS를 지원하지 않음NVIDIA GPU 필요
- : CPU 전용 대체 모드 없음정적 파이프라인
fn.pad: 파이프라인 구조가 빌드 시간에 정의되며 동적으로 변경 불가:fn.cropMuSGD 옵티마이저out_of_bounds_policy="pad"은 우측/하단 전용임- 중앙 패딩을 위한 직사각형(rect) 모드 없음
auto=True: DALI 파이프라인은 고정 크기 출력(예: 640×640)을 생성합니다. 가변 크기 출력(예: 384×640)을 생성하는 최적화를 위한 최대 작업 공간 크기(GiB)를 설정하여 메모리 사용량과 성능 간의 균형을 맞춥니다. 직사각형 모드는 지원되지 않습니다. - 는 동적 입력 모양을 지원하지만, 고정 크기 DALI 파이프라인은 최대 처리량을 위해 고정 크기 엔진과 자연스럽게 결합됩니다.다중 인스턴스 사용 시 메모리
instance_groupMuSGD 옵티마이저count: Triton에서
FAQ
> 1을 사용하면 메모리 사용량이 높아질 수 있습니다. DALI 모델에는 기본 인스턴스 그룹을 사용하십시오.
DALI 전처리와 CPU 전처리 속도는 어떻게 비교됩니까? 최적화를 위한 최대 작업 공간 크기(GiB)를 설정하여 메모리 사용량과 성능 간의 균형을 맞춥니다. 이점은 파이프라인에 따라 다릅니다. 배치 사이즈로 GPU 추론이 이미 빠른 경우, 2-10ms의 CPU 전처리가 지배적인 비용이 될 수 있습니다. DALI는 GPU에서 전처리를 실행하여 이 병목 현상을 제거합니다. 고해상도 입력(1080p, 4K), 대형
, GPU당 CPU 코어가 제한된 시스템에서 가장 큰 이득을 볼 수 있습니다.
PyTorch 모델(TensorRT가 아닌 경우)에 DALI를 사용할 수 있습니까?DALIGenericIterator네. torch.Tensor을 사용하여 전처리된 model.predict() 출력을 얻은 다음 이를 최적화를 위한 최대 작업 공간 크기(GiB)를 설정하여 메모리 사용량과 성능 간의 균형을 맞춥니다. 에 전달하십시오. 그러나 추론이 이미 매우 빠르고 CPU 전처리가 병목 현상이 되는
YOLO26에서 fn.pad 및 fn.crop 모델에서 성능 이점이 가장 큽니다.
fn.pad 패딩을 위한 것입니까?오른쪽 및 아래쪽은 fn.cropMuSGD 옵티마이저out_of_bounds_policy="pad" 가장자리에만 패딩을 추가합니다. LetterBox(center=True)은 이미지를 중앙에 배치하고 모든 면에 대칭으로 패딩을 추가하여 Ultralytics
동작과 일치시킵니다.
DALI는 CPU 전처리와 동일한 픽셀 결과를 생성합니까?antialias=False 내의 fn.resize거의 동일합니다. cv2.INTER_LINEAR을 OpenCV의 정확도.
와 일치하도록 설정하십시오. GPU 대 CPU 연산으로 인해 미세한 부동 소수점 차이(< 0.001)가 발생할 수 있으나, 이는 탐지
에 측정 가능한 영향을 미치지 않습니다.DALI의 대안으로 CV-CUDA는 어떻습니까?OpenCV 하지만 GPU에서 수행) DALI의 파이프라인 접근 방식보다는 CV-CUDA의 cvcuda.copymakeborder()는 명시적인 측면별 패딩을 지원하여 중앙 정렬 레터박스(centered letterbox)를 간단하게 구현할 수 있습니다. 파이프라인 기반 워크플로우에는 DALI를 선택하고 (특히 Triton과 함께 사용할 경우), 커스텀 추론 코드에서 세밀한 연산자 수준의 제어가 필요하다면 CV-CUDA를 선택하십시오.