Ultralytics YOLO27: 대기자 명단에 등록해 주십시오.
Get Started

NVIDIA DALI를 사용한 GPU 가속 전처리#

프로덕션 환경에 Ultralytics YOLO 모델을 배포할 때 전처리가 병목이 되는 경우가 많습니다. TensorRT는 모델 추론을 단 몇 밀리초 만에 실행할 수 있지만, CPU 기반 전처리(크기 조정, 패딩, 정규화)는 특히 해상도가 높을 때 이미지당 2~10ms가 걸릴 수 있습니다. NVIDIA DALI(데이터 로딩 라이브러리)는 전체 전처리 파이프라인을 GPU로 옮겨 이 문제를 해결합니다.

이 가이드에서는 Ultralytics YOLO 전처리를 정확하게 재현하는 DALI 파이프라인 구축, model.predict()과의 통합, 비디오 스트림 처리, Triton Inference Server를 사용한 엔드투엔드 배포 방법을 안내합니다.

이 가이드는 누구를 위한 것인가요?

이 가이드는 CPU 전처리가 측정 가능한 병목인 프로덕션 환경에서 YOLO 모델을 배포하는 엔지니어를 위한 것입니다. 일반적으로 TensorRT를 사용하는 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에서 실행됩니다:

  1. 이미지 디코딩(JPEG/PNG)
  2. 종횡비를 유지하면서 크기 조정
  3. 대상 크기에 맞게 패딩합니다(레터박스).
  4. [0, 255]에서 [0, 1]까지 픽셀 값을 정규화합니다.
  5. 레이아웃을 HWC에서 CHW로 변환합니다.

DALI를 사용하면 이러한 모든 작업이 GPU에서 실행되어 CPU 병목 현상이 제거됩니다. 다음과 같은 경우 특히 유용합니다:

시나리오DALI가 유용한 이유
빠른 GPU 추론밀리초 미만의 추론을 제공하는 TensorRT 엔진에서는 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의 cv2.resize와 INTER_LINEAR은 안티앨리어싱을 적용하지 않습니다. 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 정규화)를 건너뛰고, 모델에 입력하기 전에 디바이스 전송과 dtype 변환만 수행합니다.

이 경우 Ultralytics는 원본 이미지 크기에 접근할 수 없으므로 감지 박스 좌표가 640×640 레터박스 공간 기준으로 반환됩니다. 좌표를 원본 이미지 좌표로 변환하려면 LetterBox에서 사용되는 정확한 반올림 로직을 처리하는 scale_boxes을 사용하세요:

from ultralytics.utils.ops import scale_boxes

# xyxy 형식이며 640x640 레터박스 좌표 기준인 (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(사실상 0)가 소요되며, CPU 전처리에서는 약 ~1-10ms가 소요됩니다. 텐서는 BCHW 형식의 float32(또는 float16)여야 하며 [0, 1]로 정규화해야 합니다. Ultralytics는 디바이스 전송과 dtype 변환을 계속 자동으로 처리합니다.

비디오 스트림에서 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 전처리와 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 파이프라인을 직렬화합니다:

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 plan을 로드할 수 있도록 헤더를 제거합니다.
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단계: 추론 요청 전송#

`YOLO('http://...')` 대신 `tritonclient`을 사용하는 이유는 무엇인가요?

Ultralytics에는 전처리와 후처리를 자동으로 처리하는 내장 Triton 지원이 있습니다. 하지만 YOLO()은 전처리된 float32 텐서를 전송하는 반면 앙상블은 원시 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~10ms가 걸리는 CPU 전처리가 주요 병목이 될 수 있습니다. DALI는 GPU에서 전처리를 수행해 이 병목을 제거합니다. 입력 해상도가 높을수록(1080p, 4K), 배치 크기가 클수록, GPU당 CPU 코어 수가 제한된 시스템일수록 가장 큰 성능 향상을 얻을 수 있습니다.

  • 예. DALIGenericIterator을 사용해 전처리된 torch.Tensor 출력을 가져온 다음 model.predict()에 전달하세요. 하지만 추론이 이미 매우 빨라 CPU 전처리가 병목이 되는 TensorRT 모델에서 성능 향상이 가장 큽니다.

  • fn.pad은 오른쪽과 아래쪽 가장자리에만 패딩을 추가합니다. fn.crop은 out_of_bounds_policy="pad"와 함께 사용하면 이미지를 중앙에 배치하고 모든 가장자리에 대칭으로 패딩을 추가하여 Ultralytics LetterBox(center=True) 동작과 일치합니다.

  • 거의 동일합니다. OpenCV의 cv2.INTER_LINEAR와 일치시키려면 fn.resize에서 antialias=False을 설정하세요. GPU와 CPU의 연산 방식 차이로 미세한 부동소수점 차이(< 0.001)가 발생할 수 있지만, 탐지 정확도에는 측정 가능한 영향이 없습니다.

  • CV-CUDA는 GPU 가속 비전 처리를 위한 또 다른 NVIDIA 라이브러리입니다. DALI의 파이프라인 방식과 달리 연산자별 제어를 제공합니다(OpenCV와 비슷하지만 GPU에서 실행됩니다). CV-CUDA의 cvcuda.copymakeborder()는 측면별 패딩을 명시적으로 지정할 수 있어 중앙 레터박스를 간편하게 적용할 수 있습니다. 파이프라인 기반 워크플로(특히 Triton 사용)에는 DALI를, 맞춤형 추론 코드에서 세밀한 연산자 수준 제어가 필요하면 CV-CUDA를 선택하세요.

댓글