제5부 추론 최적화와 서빙
5.2 TensorRT I — ONNX→엔진 빌드 흐름
5.2.1 이 장에서 배우는 것 / 선수 지식
5.1에서 추론 최적화의 사면체(지연·처리량·정밀도·메모리)를 세웠다. 이제 그 사면체의 [지연·정밀도·메모리] 축을 직접 미는 첫 도구 — TensorRT — 를 직접 다룬다. TensorRT는 학습된 모델을 그 GPU에 특화된 최적화 실행 형태("엔진") 로 컴파일하는 추론 옵티마이저 + 런타임이다. 이 장은 그 빌드 파이프라인(프레임워크 → ONNX → 엔진 → 실행 → 검증)을 복붙 가능한 명령·코드로 끝까지 따라간다.
비유로 시작하면(2.12의 nvcc 흐름과 닮았다): 소스를 그 CPU의 기계어로 컴파일하듯, TensorRT는 신경망 그래프를 그 GPU·그 정밀도·그 입력 형상에 맞춘 커널 시퀀스("plan") 로 컴파일한다. 차이는 컴파일이 빌드 시점에 실제 GPU에서 여러 커널 구현(tactic)을 재 보고 가장 빠른 것을 고른다는 점(오토튜닝)이다.
배우는 것:
- TensorRT가 무엇을·왜 하는가 — 레이어/텐서 융합, 커널 오토튜닝, 정밀도 선택, 메모리 재사용이 빌드 시점에 일어나는 원리.
- 빌드 시점(offline) vs 런타임(load+execute) 의 분리, 그리고 엔진이 GPU 아키텍처·TRT 버전에 종속된다는 사실(이식 불가, 재빌드).
- 모델을 TensorRT로 가져오는 세 경로(ONNX·in-framework·API 직접)와 ONNX 경로가 기본인 이유.
trtexec로 엔진을 빌드·벤치마크하고, Python API로 빌드·추론하며, polygraphy로 정확도를 검증한다(실습).- 빌드 손잡이 — workspace(메모리 풀)·optimization level·timing cache·정밀도 플래그 — 의 의미.
선수 지식: 5.1(추론 최적화 개관·사면체), 2.12(컴파일 흐름 — 빌드/JIT 비유), 3.3(cuDNN 그래프 융합 — TRT도 융합이 핵심), 1.13~1.14(정밀도 — FP16/INT8/FP8 플래그), 0.3/A.1(컨테이너·환경). 환경은 NGC TensorRT 컨테이너 사용을 권장(드라이버·CUDA·TRT·ONNX 버전 정합이 까다롭다).
정직: TensorRT의 Python/C++ API와 CLI 플래그는 버전마다 바뀐다(메서드 이름·기본값·deprecation). 이 장의 코드는 최근 major(8.x~10.x 계열)의 흐름을 따르되, 정확한 시그니처·플래그는 당신이 쓰는 버전의 공식 문서/
trtexec --help로 확인한다("시점 의존, 확인 필요"). 벤치마크 수치는 환경 의존 개략값 — 직접 실측.
5.2.2 TensorRT는 무엇을·왜 하는가
TensorRT는 두 부분이다:
- 빌더(builder) — 모델 그래프를 받아 최적화된 엔진(plan) 을 만든다. 부하가 크고 느리지만 한 번만(offline).
- 런타임(runtime) — 만들어진 엔진을 로드해 빠르게 실행한다. 경량이고, 프로덕션에서 매 요청마다 실행된다.
빌더가 하는 최적화(사면체의 어느 축을 미는지 함께):
| 최적화 | 무엇을 | 효과 |
|---|---|---|
| 레이어/텐서 융합 | conv+bias+activation을 한 커널로(수직), 같은 입력의 여러 분기를 합침(수평) | 중간 텐서 HBM 왕복 제거 → 지연↓·메모리 트래픽↓ (1.10·3.3) |
| 커널 오토튜닝(tactic) | 각 레이어에 대해 여러 커널 구현을 실제 GPU에서 재 보고 가장 빠른 것 선택 | 그 GPU에 최적 → 지연↓ |
| 정밀도 선택 | 레이어별로 FP32/FP16/INT8/FP8 중 허용된 가장 빠른 것 | 처리량↑·메모리↓ (정밀도 대가, 5.4~5.5) |
| 상수 폴딩·dead-layer 제거 | 추론에 불필요한 학습 전용 연산(dropout 등)·상수 미리 계산 | 그래프 축소 |
| 메모리 재사용·텐서 배치 | 수명이 겹치지 않는 활성화 텐서가 메모리 공유 | 메모리↓ |
| 커널 선택·다중 스트림 | 형상·정밀도에 맞는 커널, 동시 실행 구성 | 활용률↑ |
핵심: 이 모든 결정이 "이 GPU·이 정밀도·이 입력 형상"에 묶여 빌드 시점에 고정된다. 그래서 결과 엔진은 빠르지만 이식 불가 — 다른 GPU 아키텍처나 다른 TRT 버전에서는 재빌드해야 한다(5.2.7의 함정).
5.2.3 모델을 가져오는 세 경로
TensorRT로 모델을 넣는 길은 셋이다. 상황에 맞게 고른다.
각 경로의 장단:
- ① ONNX 경로 — 장: 프레임워크 독립, 표준 포맷,
trtexec/polygraphy 도구 풍부. 단: export 시 opset·미지원 연산 문제 가능. - ② in-framework 경로 — 장: 코드 거의 그대로, 미지원 연산은 프레임워크로 폴백. 단: 그래프 일부만 가속, 제어·이식성 낮음.
- ③ API 직접 — 장: 완전한 제어, 커스텀 구조. 단: 노동집약. 보통 플러그인·특수 구조에만(5.6).
견해: 대부분은 ① ONNX 경로로 시작한다 — 도구(
trtexec·polygraphy)·이식성·디버깅이 가장 좋다. 프레임워크에 깊이 묶인 동적 모델이나 빠른 프로토타입은 ②가 편하다. ③은 미지원 연산·커스텀 커널(5.6)이 필요할 때 부분적으로. 이 장은 ①을 깊게, ②·③은 맥락만.
5.2.4 워크스루 (1) — ONNX export → trtexec 빌드·벤치마크
가장 빠른 경로부터. PyTorch 모델 → ONNX → trtexec로 엔진 빌드·벤치마크.
5.2.4.1 환경 (컨테이너 권장)
버전 정합(드라이버·CUDA·TRT·ONNX) 때문에 NGC TensorRT 컨테이너를 쓰는 것이 가장 덜 아프다(태그는 시점 의존, 확인 필요):
# 예시 — 정확한 태그는 NGC 카탈로그에서 확인(시점 의존)
docker run --gpus all -it --rm \
-v "$PWD":/work -w /work \
nvcr.io/nvidia/tensorrt:<YY.MM>-py3 # <YY.MM> = 릴리스 태그(확인 필요)
# 컨테이너 안에서 버전 확인 — 코드/플래그는 이 버전 기준으로
trtexec --help | head # CLI 옵션(버전마다 다름)
python -c "import tensorrt as trt; print(trt.__version__)"
5.2.4.2 모델을 ONNX로 export
예시로 torchvision의 ResNet-50(분류)을 ONNX로 내보낸다. (실제 작업에선 당신의 모델로.)
# export_onnx.py — PyTorch 모델을 ONNX로
import torch, torchvision
model = torchvision.models.resnet50(weights="DEFAULT").eval().cuda()
dummy = torch.randn(1, 3, 224, 224, device="cuda") # (N,C,H,W)
torch.onnx.export(
model, dummy, "resnet50.onnx",
input_names=["input"], output_names=["logits"],
opset_version=17, # opset은 TRT 지원 범위 확인(버전 의존)
dynamic_axes={ # 배치 차원을 동적으로(가변 배치 추론 대비)
"input": {0: "batch"},
"logits": {0: "batch"},
},
)
print("wrote resnet50.onnx")
python export_onnx.py
# (선택) ONNX 자체 점검 — 그래프가 유효한지, 미지원 연산 없는지
polygraphy inspect model resnet50.onnx --show layers | head -40
함정 미리: export 단계에서 opset 불일치·미지원 연산·동적 축 누락이 가장 흔한 실패다.
dynamic_axes를 안 주면 엔진이 배치 1 고정이 된다(5.3에서 동적 shape 전면 다룸).
5.2.4.3 trtexec로 엔진 빌드 + 벤치마크
trtexec는 빌드·프로파일·벤치마크를 한 번에 하는 CLI다. 코드를 작성하지 않고 빠르게 검증할 때 최고의 출발점.
# FP16 엔진 빌드 + 저장 + 즉시 벤치마크
trtexec \
--onnx=resnet50.onnx \
--saveEngine=resnet50_fp16.plan \
--fp16 \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:32x3x224x224 \
--memPoolSize=workspace:4096 # 빌더 작업 메모리(MiB). 부족하면 일부 tactic 못 씀
출력에서 볼 것(수치는 환경 의존 개략):
[I] Engine built in N.N sec. ← 빌드 시간(오토튜닝 비용)
[I] === Performance summary ===
[I] Throughput: ~XXXX qps ← 처리량(opt 형상 기준)
[I] Latency: min/mean/median/max/p99 ← 지연 분포(p99까지!) — 5.1의 지표
[I] GPU Compute Time: mean ... ← 순수 GPU 실행 시간
비교를 위해 FP32도 빌드해 본다:
trtexec --onnx=resnet50.onnx --saveEngine=resnet50_fp32.plan \
--optShapes=input:8x3x224x224
# fp16 vs fp32의 Throughput·Latency·엔진 크기를 비교 → 사면체가 숫자로 보임(5.1)
체크: FP16이 FP32보다 처리량↑·지연↓·엔진 작아짐이 보이면 정상(텐서코어·메모리 트래픽, 1.11·1.12). 안 보이면 GPU가 FP16 텐서코어를 안 쓰거나(세대·형상), 모델이 메모리 바운드가 아닐 수 있다. 정확도는 아직 검증 안 했다 — 5.2.6에서.
5.2.5 워크스루 (2) — Python API로 빌드·추론
trtexec는 편하지만, 프로덕션에선 Python/C++ API로 빌드·실행을 제어한다. 빌드와 추론을 분리해 본다.
주의: 아래 API는 버전 의존이다. 메서드명(
build_serialized_network등)·플래그가 major 버전 간 다를 수 있으니 당신 버전 문서로 확인. 흐름(파서→config→빌드→직렬화→런타임→context→execute)은 버전이 달라도 같다.
5.2.5.1 빌드 (ONNX → 직렬화된 엔진)
# build_engine.py — ONNX를 파싱해 엔진을 빌드하고 파일로 저장
import tensorrt as trt
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
def build_engine(onnx_path, plan_path, fp16=True, ws_mib=4096):
builder = trt.Builder(TRT_LOGGER)
# EXPLICIT_BATCH: ONNX는 명시적 배치 차원을 쓴다(필수)
flag = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
network = builder.create_network(flag)
parser = trt.OnnxParser(network, TRT_LOGGER)
with open(onnx_path, "rb") as f:
if not parser.parse(f.read()):
for i in range(parser.num_errors):
print("ONNX parse error:", parser.get_error(i))
raise RuntimeError("failed to parse ONNX")
config = builder.create_builder_config()
# 빌더 작업 메모리 풀(부족하면 일부 tactic 제외 → 느린 엔진)
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, ws_mib << 20)
if fp16 and builder.platform_has_fast_fp16:
config.set_flag(trt.BuilderFlag.FP16)
# 동적 배치: optimization profile (min/opt/max). 5.3에서 깊게.
profile = builder.create_optimization_profile()
profile.set_shape("input", (1,3,224,224), (8,3,224,224), (32,3,224,224))
config.add_optimization_profile(profile)
# timing cache: 오토튜닝 결과 캐시 → 재빌드 가속(같은 GPU/버전)
# (선택) cache = config.create_timing_cache(b""); config.set_timing_cache(cache, False)
serialized = builder.build_serialized_network(network, config)
if serialized is None:
raise RuntimeError("engine build failed")
with open(plan_path, "wb") as f:
f.write(serialized)
print(f"wrote {plan_path}")
if __name__ == "__main__":
build_engine("resnet50.onnx", "resnet50_fp16.plan", fp16=True)
python build_engine.py # resnet50_fp16.plan 생성(=trtexec가 만든 것과 동등한 산출)
5.2.5.2 추론 (엔진 로드 → 실행)
# infer.py — 엔진을 로드해 실제 입력으로 추론
import numpy as np, tensorrt as trt
from cuda import cudart # cuda-python (또는 pycuda). 패키지·API는 버전 확인
TRT_LOGGER = trt.Logger(trt.Logger.WARNING)
def load_engine(plan_path):
runtime = trt.Runtime(TRT_LOGGER)
with open(plan_path, "rb") as f:
return runtime.deserialize_cuda_engine(f.read())
def infer(engine, x): # x: np.float32 (N,3,224,224)
ctx = engine.create_execution_context()
ctx.set_input_shape("input", x.shape) # 동적 배치: 이번 실행의 실제 형상 지정
# 출력 형상·버퍼 준비
out_shape = tuple(ctx.get_tensor_shape("logits"))
out = np.empty(out_shape, dtype=np.float32)
# GPU 메모리 할당 + H2D 복사
_, d_in = cudart.cudaMalloc(x.nbytes)
_, d_out = cudart.cudaMalloc(out.nbytes)
cudart.cudaMemcpy(d_in, x.ctypes.data, x.nbytes,
cudart.cudaMemcpyKind.cudaMemcpyHostToDevice)
ctx.set_tensor_address("input", int(d_in))
ctx.set_tensor_address("logits", int(d_out))
_, stream = cudart.cudaStreamCreate()
ctx.execute_async_v3(stream) # 비동기 실행(스트림, 2.10)
cudart.cudaStreamSynchronize(stream)
cudart.cudaMemcpy(out.ctypes.data, d_out, out.nbytes,
cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost)
cudart.cudaFree(d_in); cudart.cudaFree(d_out); cudart.cudaStreamDestroy(stream)
return out
if __name__ == "__main__":
engine = load_engine("resnet50_fp16.plan")
x = np.random.randn(8,3,224,224).astype(np.float32)
logits = infer(engine, x)
print("output:", logits.shape, "argmax:", logits.argmax(1))
python infer.py # (8, 1000) 형상, 배치별 top-1 클래스 인덱스 출력
메서드명(
execute_async_v3·set_tensor_address·set_input_shape)은 버전 의존이다. 옛 버전은execute_v2·바인딩 인덱스 방식을 썼다 — 당신 버전 문서로 확인. 흐름(deserialize → context → 입출력 주소 바인딩 → 비동기 execute → 동기화)은 동일하다.
5.2.6 워크스루 (3) — 정확도 검증 (반드시)
빌드가 됐다 ≠ 결과가 맞다. FP16/INT8 변환·융합·export 과정에서 출력이 틀어질 수 있다. polygraphy로 TRT 엔진과 ONNX 런타임(또는 프레임워크)의 출력을 수치 비교한다 — 이 단계를 건너뛰면 안 된다.
# 같은 입력으로 TRT(FP16)와 ONNX-Runtime을 실행해 출력을 비교
polygraphy run resnet50.onnx \
--trt --fp16 \
--onnxrt \
--input-shapes input:[8,3,224,224] \
--atol 1e-2 --rtol 1e-2 # 허용 오차: FP16은 느슨하게(과제·모델 의존)
# PASS/FAIL과 함께, 어느 출력 텐서가 얼마나 벗어났는지 표로 보여줌
해석:
- PASS — 허용 오차 안. 분류처럼 argmax만 중요한 태스크는 여기에 더해 대표 평가셋의 top-1/top-5 정확도를 FP32 대비로 확인(검출·생성은 태스크 지표로).
- FAIL — 어느 레이어부터 어긋나는지 좁힌다:
polygraphy run ... --trt --onnxrt --save-results,polygraphy debug류로 레이어별 비교. 흔한 원인: 과한 정밀도 강등, 융합 부작용, opset/연산 차이, 동적 shape 범위 밖 입력.
정직: 허용 오차는 모델·태스크가 정한다. FP16은 보통
1e-2~1e-3, INT8(5.4)은 수치 비교가 아니라 태스크 정확도로 판정한다(원소별 오차는 크지만 분류 정확도는 유지될 수 있음). "수치가 약간 다르다"를 무조건 실패로 보지 말고, 최종 태스크 지표로 합격선을 정한다.
5.2.7 빌드 손잡이 — workspace·opt level·timing cache·정밀도
빌드 품질·시간을 정하는 주요 손잡이:
| 손잡이 | 의미 | 트레이드오프 |
|---|---|---|
| workspace(메모리 풀) | 빌더가 tactic 탐색에 쓸 수 있는 작업 메모리 상한 | 작으면 빠른 tactic을 못 써 느린 엔진. 충분히 크게(--memPoolSize=workspace:) |
| optimization level | 오토튜닝 탐색 강도(레벨↑) | 레벨↑ → 빌드 느림·엔진 빠름. 레벨↓ → 빌드 빠름·엔진 느림(개발 중엔 낮게) |
| timing cache | 오토튜닝 측정 결과 캐시 | 같은 GPU·같은 TRT 버전 재빌드 가속. GPU/버전 다르면 무효 |
| 정밀도 플래그 | --fp16/--int8/--fp8(세대·버전 의존), strict types | 켠다고 전부 그 정밀도는 아님 — 빌더가 레이어별로 빠른 쪽 선택(섞임) |
| builder optimization for shapes | min/opt/max 프로파일 | opt 형상에 최적화. 실제 분포에 맞춰 정함(5.3) |
핵심 주의 — 엔진의 종속성:
따라서 — 엔진은 '배포 대상 GPU에서, 배포할 TRT 버전으로' 빌드해야 한다. 빌드 서버와 추론 서버의 GPU·버전을 일치시키거나, 대상별로 빌드한다.
이것이 컨테이너로 버전을 못 박는 이유이고, CI에서 대상 GPU별로 엔진을 빌드해 아티팩트로 배포하는 패턴이 흔한 이유다.
5.2.8 중간 점검 (체크포인트)
- [ ] TensorRT의 빌드(offline) vs 런타임 분리와, 빌더가 하는 최적화 4가지(융합·오토튜닝·정밀도·메모리)를 설명한다.
- [ ] ONNX·in-framework·API 직접 세 경로의 장단을 대고, ONNX가 기본인 이유를 말한다.
- [ ]
trtexec로 FP16 엔진을 빌드·벤치마크하고, 출력에서 Throughput·p99 지연·빌드 시간을 읽는다. - [ ] Python API의 흐름(parser→config→build→serialize / deserialize→context→execute)을 따라 빌드·추론한다.
- [ ] polygraphy로 정확도를 검증하고, 허용 오차를 태스크 지표로 정한다.
- [ ] 엔진이 GPU 아키텍처·TRT 버전에 종속됨을 이해하고 배포 전략(대상별 빌드)을 제시한다.
증상→의심: trtexec는 빠른데 내 코드가 느림 → H2D/D2H 복사·런치 오버헤드 미최적(2.5·2.10), context 재사용 안 함. 엔진이 다른 머신에서 로드 실패 → GPU 아키텍처/TRT 버전 불일치 → 대상에서 재빌드. FP16 빌드가 FP32와 성능 차이 없음 → 텐서코어 미사용(형상·세대)·메모리 바운드 아님. 빌드가 비정상적으로 느리거나 일부 tactic 경고 → workspace 부족 → 메모리 풀 키우기. 출력이 틀림 → polygraphy로 레이어별 좁히기·export opset·동적 shape 범위 확인.
5.2.9 트레이드오프·대안
언제 TensorRT를 쓰나(vs 프레임워크 직접·in-framework).
| 방식 | 누구에게 | 비고 |
|---|---|---|
| TRT ONNX 엔진(①) | production·고정 모델·SLA·비용 압박 | 최고 성능·이식성·도구. 빌드/버전 관리 비용 |
| Torch-TensorRT/TF-TRT(②) | 프레임워크 의존 큰 모델·빠른 적용 | 코드 거의 그대로, 부분 가속·제어↓ |
| 프레임워크 직접/torch.compile | 프로토타입·잦은 변경·낮은 트래픽 | 변환 없음. 효율 낮음(5.1.10 ①) |
| TRT-LLM(5.7~) | LLM(자기회귀·KV·인플라이트 배칭) | 일반 TRT가 아니라 LLM 특화 런타임 — 별도 |
견해: 모델이 안정적이고 production이면 ① ONNX 엔진이 보통 최적이다 — 도구·성능·이식성이 가장 좋고 Triton 서빙(5.11)과도 자연스럽다. 모델이 자주 바뀌거나 export가 까다로우면 ②로 시작해도 된다. LLM은 일반 TRT가 아니라 TRT-LLM 으로 — KV 캐시·인플라이트 배칭(5.7~5.8)이 일반 TRT 빌드 모델로는 안 풀린다.
비엔비디아 정직: ONNX는 표준 포맷이라 ONNX Runtime·OpenVINO·다른 가속기 백엔드로도 같은 모델을 실행할 수 있다. TensorRT의 강점은 엔비디아 GPU에서의 오토튜닝·융합·최신 정밀도(FP8/FP4) 흡수다. 이식성을 최우선하면 ONNX Runtime, 그 GPU에서의 최대 성능이면 TensorRT — 워크로드로 실측해 정한다.
5.2.10 흔한 함정
- 엔진을 다른 GPU/버전에서 재사용 시도. plan은 아키텍처·TRT 버전 종속 → 로드 실패나 미정의 동작. 대상 GPU·대상 버전에서 빌드.
- 정확도 검증 생략. "빌드됨 = 맞음"이 아니다. FP16/INT8·융합으로 출력이 틀어질 수 있다 — polygraphy + 태스크 지표로 반드시 확인(5.2.6).
- 동적 축 누락.
dynamic_axes없이 export하면 배치 1 고정 엔진. 가변 배치 서빙이 안 됨 → export·프로파일에서 동적 shape 설정(5.3). - workspace 너무 작게. 빌더가 빠른 tactic을 못 써 느린 엔진이 됨(경고를 흘리기 쉬움). 충분히 크게.
--fp16켰으니 전부 FP16이라 가정. 빌더는 레이어별로 빠른 정밀도를 섞어 고른다. 특정 레이어를 강제하려면 별도 설정(strict types 등, 버전 의존).- 빌드 시간을 런타임 비용으로 오해. 오토튜닝은 빌드 시점 일회성이다(timing cache로 재빌드 가속). 프로덕션 지연은 런타임만 본다.
- opset/미지원 연산 무시. export opset이 TRT 지원 범위를 벗어나거나 미지원 연산이 있으면 파싱 실패 → opset 조정·연산 대체·플러그인(5.6).
- 복사·오버헤드 미최적. 엔진은 빠른데 H2D/D2H 복사·작은 런치로 종단 지연이 나쁠 수 있다 — pinned 메모리·스트림·그래프(2.5·2.10·2.11), 또는 Triton(5.11)에 맡김.
5.2.11 연습문제
연습 1 (빌드/런타임 분리). TensorRT의 오토튜닝이 "빌드 시점에 실제 GPU에서 여러 tactic을 재 본다"는 설계가, 왜 엔진을 GPU 아키텍처·TRT 버전에 종속시키는지 설명하라. 이로부터 CI/배포에서 따라야 할 규칙은?
정답
오토튜닝은 그 GPU의 실제 커널 성능을 측정해 레이어별 최적 구현(tactic)을 고르고, 그 선택을 엔진에 굳힌다. 최적 tactic은 GPU 아키텍처(SM·텐서코어·캐시)마다 다르고, TRT 버전마다 커널 라이브러리·융합 규칙이 달라진다 → 한 GPU/버전에서 최적이던 엔진이 다른 환경에선 무효하거나 비호환. 규칙: 배포 대상 GPU·배포할 TRT 버전과 동일한 환경에서 엔진을 빌드(CI에서 대상별 빌드 → 아티팩트), 버전 업그레이드 시 재빌드, 빌드/추론 컨테이너 버전 고정.
연습 2 (trtexec 읽기). trtexec --onnx=m.onnx --fp16 --optShapes=input:8x3x224x224 출력에서 Throughput·median latency·p99 latency·GPU Compute Time·빌드 시간을 봤다. (a) 프로덕션 SLA 판정에 직접 쓸 지표 둘, (b) "엔진은 빠른데 서비스가 느리다"를 진단할 때 비교할 지표를 고르고 이유.
정답
(a) median/p99 latency(SLA는 분위수)와 Throughput(처리량·비용). 특히 p99가 SLA 기준(5.1). (b) GPU Compute Time vs end-to-end latency 비교 — GPU Compute는 빠른데 종단이 느리면 GPU 밖(H2D/D2H 복사·전처리·런치·큐잉)이 병목. trtexec의 순수 GPU 시간과 내 파이프라인 종단 시간의 격차가 곧 "GPU 외" 오버헤드. 빌드 시간은 일회성이라 SLA와 무관.
연습 3 (정확도 검증). 동료가 INT8 엔진을 polygraphy 수치 비교(--atol 1e-3)로 실행했더니 FAIL이라며 INT8을 포기하려 한다. 무엇이 잘못된 판정이고, 올바른 검증 절차는?
정답
잘못: INT8을 원소별 수치 오차(atol 1e-3)로 판정한 것. 양자화는 본래 원소값이 크게 달라지므로 좁은 절대 오차로는 거의 항상 FAIL이다 — 이는 품질 저하의 증거가 아니다. 올바른 절차: 대표 평가셋으로 최종 태스크 지표(분류면 top-1/top-5, 검출이면 mAP, 생성이면 해당 지표)를 FP32(또는 FP16) 기준 대비로 측정해, 허용 가능한 하락(예: top-1 −0.5%p 이내) 안인지로 판정. 수치 비교는 FP16처럼 오차가 작아야 하는 경우의 보조 도구다(5.4에서 INT8 캘리브레이션·검증을 깊게).
연습 4 (경로 선택). 다음에 ① ONNX 엔진 / ② Torch-TensorRT / ③ API 직접(플러그인 포함) 중 무엇으로 시작할지와 이유: (a) 표준 CNN 검출 모델을 고QPS production 서빙, (b) 제어 흐름이 복잡하고 자주 바뀌는 연구용 PyTorch 모델, (c) ONNX로 표현 안 되는 커스텀 연산이 한 레이어 있는 모델.
정답
(a) ① ONNX 엔진 — 표준 구조·고정·production이라 도구·성능·Triton 서빙 정합이 최적. (b) ② Torch-TensorRT — 코드 거의 그대로 두고 가속 서브그래프만 컴파일, 미지원/동적 부분은 프레임워크 폴백. 잦은 변경에 export 부담 적음. (c) ① + ③ 플러그인 — 대부분 ONNX 경로로 가되, 커스텀 연산은 TRT 플러그인(5.6)으로 구현해 그 레이어만 API로 채움. 모델 전체를 API로 작성할 필요는 없음.
연습 5 (빌드 손잡이). 개발 중 빌드가 너무 오래 걸려 반복이 느리다. 빌드를 빠르게 하되 최종 배포 엔진의 성능은 지키는 전략을 두 가지 이상 제시하라.
정답
① 개발 중 optimization level↓(또는 빠른 빌드 옵션)로 반복하고, 배포용만 레벨↑로 최종 빌드. ② timing cache 활용 — 같은 GPU·같은 TRT 버전 재빌드 시 오토튜닝 측정 재사용으로 큰 단축. ③ 모델·형상이 안정되면 엔진을 아티팩트로 캐싱해 매번 빌드하지 않음(CI에서 1회 빌드 → 배포). ④ workspace를 충분히 줘 tactic 탐색이 공회전하지 않게(너무 작으면 느리고 결과도 나쁨). 핵심: 개발 반복용 빌드와 배포용 빌드를 분리하고, 캐시로 재측정을 피한다.
5.2.12 요약 · 다음 장
- TensorRT는 추론 옵티마이저(빌드, offline) + 런타임이다. 빌더가 레이어 융합·커널 오토튜닝·정밀도 선택·메모리 재사용을 빌드 시점에 적용해 그 GPU에 특화된 엔진(plan) 을 만든다.
- 모델 유입은 ONNX(기본)·in-framework·API 직접 세 경로. 표준 흐름은 프레임워크 → ONNX → (
trtexec/Python API) 엔진 빌드 → 추론 → polygraphy 정확도 검증. - 엔진은 GPU 아키텍처·TRT 버전에 종속된다(이식 불가) → 대상 환경에서 빌드, 컨테이너로 버전 고정, CI에서 대상별 빌드.
- 빌드 손잡이: workspace(충분히)·optimization level·timing cache·정밀도 플래그.
--fp16은 레이어별로 섞여 선택되며, 정확도는 태스크 지표로 검증(특히 INT8은 수치 비교가 아니라 태스크 정확도). - 정직: API·플래그는 버전 의존(확인 필요), 모든 성능 수치는 실측, 정밀도 변환은 검증과 함께.
다음 장: 5.3 TensorRT II — 빌더 최적화·동적 shape. 이 장에서 맛본 optimization profile(min/opt/max) 을 전면으로 끌어와, 가변 배치·가변 해상도·가변 시퀀스를 하나의 엔진으로 서빙하는 법, 프로파일을 실제 입력 분포에 맞춰 튜닝하는 법, 그리고 빌드 최적화 손잡이를 실습으로 깊게 판다.