NVIDIA 활용한 GPU 전처리
소개
배포 시 Ultralytics YOLO 모델을 프로덕션 환경에 배포할 때, 전처리 과정이 종종 병목 현상이 됩니다. 반면 TensorRT 는 모델 추론을 단 몇 밀리초 만에 수행할 수 있지만, CPU 전처리(크기 조정, 패딩, 정규화)는 특히 고해상도 이미지에서 이미지당 2~10ms가 소요될 수 있습니다. NVIDIA (Data Loading Library)는 전체 전처리 파이프라인을 GPU 이동시켜 이 문제를 해결합니다.
이 가이드에서는 Ultralytics YOLO 정확히 재현하는 DALI 파이프라인을 구축하고, 이를 model.predict(), 비디오 스트림 처리, 그리고 Triton 추론 서버.
이 가이드는 누구를 위한 것인가요?
이 가이드는 CPU 명백한 병목 현상으로 작용하는 운영 환경에 YOLO 배포하는 엔지니어를 위한 것입니다. 일반적으로 TensorRT NVIDIA 기반 배포 환경, 고처리량 비디오 파이프라인, 또는 Triton 추론 서버 설정. 표준 추론을 실행하는 경우 model.predict() 그리고 전처리 단계에서 병목 현상이 발생하지 않는다면, 기본 CPU 잘 작동합니다.
간략한 요약
- DALI 파이프라인을 구축하고 계신가요? 다음을 사용합니다.
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeGPU YOLO 레터박스 전처리 과정을 재현하기 위해. - Ultralytics와 연동하시겠습니까? DALI 출력을
torch.Tensor에서model.predict()— Ultralytics 이미지 전처리를 자동으로 Ultralytics . - Triton 배포하시나요?CPU 전혀CPU 위해 TensorRT 함께 DALI 백엔드를 사용해 보세요.
YOLO 단계에서 DALI를 사용하는 이유
일반적인 YOLO 파이프라인에서는 전처리 단계가 CPU에서 실행됩니다:
- 이미지(JPEG/PNG) 해독하기
- 가로세로 비율을 유지하며 크기 조정
- 목표 크기로 자르기 (레터박스)
- 정규화 다음의 픽셀 값
[0, 255]에서[0, 1] - 레이아웃을 HWC에서 CHW로 변환
DALI를 사용하면 이러한 모든 작업이 GPU 실행되므로 CPU 현상이 해소됩니다. 이는 특히 다음과 같은 경우에 유용합니다:
| 시나리오 | DALI가 도움이 되는 이유 |
|---|---|
| 고속 GPU | TensorRT 1밀리초 미만의 추론 시간을 자랑하는 TensorRT 엔진 덕분에 CPU 가장 큰 비용 요인이 됩니다 |
| 고해상도 입력 | 1080p 및 4K 동영상 스트림은 비용이 많이 드는 크기 조정 작업이 필요합니다 |
| 대량 생산 | 서버 측에서 다수의 이미지를 병렬로 추론 처리 |
| 제한된 CPU 수 | NVIDIA 과 같은 엣지 디바이스, 또는 GPU CPU 수가 적은 고밀도 GPU |
필수 조건
리눅스 전용
NVIDIA Linux에서만 지원됩니다. Windows나 macOS에서는 사용할 수 없습니다.
필요한 패키지를 설치합니다:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda110
요구 사항:
- NVIDIA GPU 컴퓨트 성능 5.0 이상 / Maxwell 또는 그 이후 버전)
- CUDA .0 이상 또는 12.0 이상
- Python .10–3.14
- 리눅스 운영 체제
YOLO 이해하기
DALI 파이프라인을 구축하기 전에, Ultralytics 전처리 단계에서 정확히 어떤 작업을 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) 목표 크기에 도달하기 위해 - 이미지를 중앙에 배치하여 양쪽에 패딩이 균등하게 분배되도록 하기
Y YOLO용 DALI 파이프라인
아래의 중앙 정렬된 파이프라인을 기본 참조로 사용하십시오. 이는 Ultralytics 일치합니다. LetterBox(center=True) 표준 YOLO 사용하는 행동입니다.
중앙 정렬 파이프라인 (권장, Ultralytics 와 호환)
이 버전은 중심 정렬 패딩을 적용한 기본 Ultralytics 방식을 정확히 재현하며, LetterBox(center=True):
중앙 정렬 패딩이 적용된 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 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
언제 fn.pad 충분해?
정확한 수치가 필요하지 않다면 LetterBox(center=True) 패리티를 활용하면 다음을 사용하여 패딩 단계를 간소화할 수 있습니다. fn.pad(...) 대신 fn.crop(..., out_of_bounds_policy="pad"). 해당 변형은 오직 오른쪽과 아래쪽 가장자리가 표시되는데, 이는 사용자 지정 배포 파이프라인에서는 허용될 수 있지만, Ultralytics 기본 설정인 중앙 정렬 레터박스 동작과는 정확히 일치하지 않습니다.
왜 fn.crop 중앙 정렬 패딩을 위해?
DALI의 fn.pad 이 연산자는 오직 오른쪽과 아래쪽 가장자리. 중앙 정렬 패딩을 적용하려면 ( Ultralytics 일치하도록) LetterBox(center=True)), 사용 fn.crop 와 함께 out_of_bounds_policy="pad". 기본값으로 crop_pos_x=0.5 및 crop_pos_y=0.5, 이미지는 대칭 여백을 적용하여 자동으로 중앙 정렬됩니다.
앤티앨리어싱 불일치
DALI의 fn.resize 기본적으로 앤티앨리어싱을 활성화합니다 (antialias=True), 반면 OpenCV cv2.resize 와 함께 INTER_LINEAR ~합니다 아니 앤티앨리어싱을 적용합니다. 항상 설정하십시오 antialias=False DALI에서 CPU 맞춰 조정됩니다. 이를 생략하면 미묘한 수치적 차이가 발생하여 모델 정확도.
파이프라인 실행
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 에서 DALI 사용
사전 처리된 PyTorch tensor model.predict(). 어떤 torch.Tensor 통과되면, Ultralytics 이미지 전처리 과정을 건너뜁니다 (레터박스, BGR→RGB, HWC→CHW 변환 및 /255 정규화)를 수행하며, 모델로 전송하기 전에 장치 변환과 데이터형 변환만 수행합니다.
이 경우 Ultralytics 원본 이미지의 크기를 확인할 Ultralytics , 탐지 상자의 좌표는 640×640 크기의 레터박스 형식으로 반환됩니다. 이를 원본 이미지의 좌표로 변환하려면 다음을 사용하십시오. scale_boxes 다음에서 사용하는 정확한 반올림 논리를 처리하는 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))
이는 직접 tensor , 비디오 스트림, Triton 등 모든 외부 전처리 경로에 적용됩니다.
DALI와 Ultralytics
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 이용한 CPU 소요되는 약 1~10ms에 비해 약 0.004ms(사실상 0에 가깝다)가 소요됩니다. tensor BCHW 형식, float32(또는 float16) tensor , [0, 1]. Ultralytics 여전히 기기 간 데이터 전송과 데이터형 변환을 자동으로 처리 Ultralytics .
비디오 스트리밍이 지원되는 DALI
실시간 영상 처리를 위해서는 다음을 사용하십시오 fn.external_source 어떤 소스에서든 프레임을 입력하기 위해 — OpenCV, GStreamer 또는 사용자 정의 캡처 라이브러리:
동영상 스트림 전처리를 위한 DALI 파이프라인
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 output
import cv2
import numpy as np
import torch
from ultralytics import YOLO
model = YOLO("yolo26n.engine") # TensorRT model
pipe = yolo_video_pipeline(target_size=640)
pipe.build()
cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# Feed BGR frame (convert to RGB for DALI)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pipe.feed_input("input", [np.array(frame_rgb)])
(output,) = pipe.run()
# Convert DALI output to torch tensor for inference.
# This is a simple fallback path: using feed_input() with pipe.run() keeps a GPU->CPU->GPU copy.
# For high-throughput deployments, prefer a reader-based pipeline plus DALIGenericIterator to keep data on GPU.
tensor = torch.tensor(output.as_cpu().as_array()).to("cuda")
results = model.predict(tensor, verbose=False)
DALI를 지원하는 Triton 서버
프로덕션 배포 시, DALI 전처리 과정을 TensorRT 추론을 Triton 앙상블 모델을 사용하십시오. 이렇게 하면 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 파이프라인을 직렬화합니다:
Triton 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 YOLO 내보내기
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.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.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"
}
}
]
}
앙상블 매핑의 작동 원리
이 앙상블은 다음을 통해 모델들을 연결합니다 가상 tensor. 다음 output_map 값 "preprocessed_image" DALI 단계에서 일치하는 input_map 값 "preprocessed_image" TensorRT . 이는 한 단계의 출력을 다음 단계의 입력에 연결하는 임의의 이름이며, 모델의 내부 tensor 일치할 필요는 없습니다.
4단계: 추론 요청 전송
왜 tritonclient 대신 YOLO(\"http://...\")?
Ultralytics 내장된 Triton 사전/사후 처리를 자동으로 처리해 주는 기능입니다. 하지만 DALI 앙상블에서는 작동하지 않습니다. 왜냐하면 YOLO() 전처리된 float32 tensor 전송하는 tensor , 앙상블은 원시 JPEG 바이트를 기대합니다. 다음을 사용하십시오 tritonclient DALI 앙상블을 위해 직접, 그리고 내장형 통합 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 — 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 입력 tensor 배치 형식이 균일해야 Triton .
지원되는 작업
DALI 전처리 YOLO 표준을 사용하는 모든 YOLO 작동합니다. LetterBox 파이프라인:
| 작업 | 지원됨 | 참고 사항 |
|---|---|---|
| 객체 탐지 | ✅ | 표준 레터박스 전처리 |
| Segmentation | ✅ | 탐지와 동일한 전처리 |
| 포즈 추정 | ✅ | 탐지와 동일한 전처리 |
| 방향 기반 탐지(OBB) | ✅ | 탐지와 동일한 전처리 |
| 분류 | ❌ | 레터박스 방식이 아닌 토치비전 변환(중앙 자르기)을 사용합니다 |
제한 사항
- Linux 전용: DALI는 Windows나 macOS를 지원하지 않습니다
- NVIDIA GPU : CPU 대체 모드 없음
- 정적 파이프라인: 파이프라인 구조는 빌드 시점에 정의되며 동적으로 변경할 수 없습니다
fn.pad오른쪽/아래쪽만: 사용fn.crop와 함께out_of_bounds_policy="pad"중앙 정렬 패딩을 위해- 직선 모드 없음: DALI 파이프라인은 고정 크기의 출력(예: 640×640)을 생성합니다.
auto=True가변 크기 출력(예: 384×640)을 생성하는 rect 모드는 지원되지 않습니다. 참고로, TensorRT 동적 입력 형식을 지원하지만, 고정 크기의 DALI 파이프라인은 최대 처리량을 확보하기 위해 고정 크기의 엔진과 자연스럽게 결합됩니다. - 여러 인스턴스가 있는 메모리: 사용법
instance_group와 함께count> Triton 1번 Triton 메모리 사용량이 과도하게 증가할 Triton . DALI 모델에는 기본 인스턴스 그룹을 사용하십시오.
FAQ
DALI 전처리 속도는 CPU 속도와 비교했을 때 어떤가요?
GPU 점은 사용자의 파이프라인에 따라 달라집니다. TensorRT에서 이미 빠른 경우, 2~10ms 소요되는 CPU 주된 비용이 될 수 있습니다. DALI는 전처리 작업을 GPU 수행함으로써 이러한 병목 현상을 해소합니다. 고해상도 입력(1080p, 4K), 대용량 배치, 그리고 GPU CPU 제한된 시스템에서 가장 큰 성능 향상을 확인할 수 있습니다.
PyTorch ( TensorRT뿐만 아니라)에서 DALI를 사용할 수 있나요?
네. 사용하세요 DALIGenericIterator 사전 처리하기 위해 torch.Tensor 출력한 다음, 이를 model.predict(). 그러나 성능 향상 효과는 다음의 경우 가장 큽니다. TensorRT 추론 속도가 이미 매우 빨라 CPU 단계가 병목 현상이 되는 모델들.
~와 ~의 차이점은 무엇인가요? fn.pad 및 fn.crop 채우기용인가요?
fn.pad 다음에만 여백을 추가합니다 오른쪽과 아래쪽 가장자리. fn.crop 와 함께 out_of_bounds_policy="pad" 이미지를 중앙에 배치하고 Ultralytics 일치하도록 모든 면에 대칭적으로 여백을 추가합니다 LetterBox(center=True) 행동.
DALI는 CPU 처리와 픽셀 단위로 동일한 결과를 산출하나요?
거의 똑같다. 세트 antialias=False 에서 fn.resize OpenCV 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 정확도.
DALI의 대안CUDA 어떨까요?
CUDA NVIDIA GPU 가속 비전 처리를 위한 또 다른 NVIDIA . 이 라이브러리는 연산자별 제어 기능을 제공하며(예: OpenCV DALI의 파이프라인 방식보다는 ( GPU) 처리하는 방식입니다.CUDA cvcuda.copymakeborder() 각 면별 패딩을 명시적으로 지원하므로, 레터박스 중앙 정렬을 간편하게 처리할 수 있습니다. 파이프라인 기반 워크플로우(특히 Triton), 그리고 사용자 정의 추론 코드에서 세밀한 연산자 수준 제어를CUDA .