제5부 추론 최적화와 서빙
5.14 Triton IV — 앙상블·BLS·운영 지표
5.14.1 이 장에서 배우는 것 / 선수 지식
5.11~5.13에서 Triton의 리포지토리·백엔드·인스턴스·배칭을 익혔다. 그런데 실제 추론은 모델 하나가 아니라 파이프라인이다 — 전처리(디코딩·리사이즈·토크나이즈) → 추론 → 후처리(NMS·디토크나이즈)가 이어진다. 이것을 클라이언트가 단계마다 서버를 왕복하며 호출하면 데이터가 GPU↔클라이언트를 오가며 느려진다. 이 장은 그 파이프라인을 서버 안에서 한 번에 묶는 두 방법 — ensemble(정적 DAG) 과 BLS(동적 제어 흐름) — 와, 운영의 눈인 Prometheus 메트릭으로 마무리한다(Triton 묶음 5.11~5.14 완결).
배우는 것:
- ensemble — 전처리·추론·후처리 모델을 DAG로 묶어 클라이언트가 한 번만 호출, 데이터는 GPU에 유지.
- BLS(Business Logic Scripting) — Python으로 모델을 프로그래밍 방식 호출(조건 분기·반복·캐스케이드), 정적 DAG로 안 되는 동적 파이프라인.
- ensemble vs BLS — 정적 그래프 vs 제어 흐름, 언제 무엇을.
- 운영 지표 — Prometheus 메트릭(처리량·큐 시간 vs 컴퓨트 시간·지연·GPU 활용률·성공/실패)으로 병목을 진단·관측.
- 헬스·워밍업·k8s 연동 — readiness, model warmup, DCGM(7.3).
- ensemble·BLS를 구성하고 메트릭을 긁어 해석하는 실습.
선수 지식: 5.11~5.13(리포지토리·백엔드·Python 백엔드·배칭·인스턴스), 3.8(DALI 전처리), 5.1(지표·goodput), 5.7~5.10(LLM은 ensemble로 전/후처리 연결), 7.3(DCGM·k8s 모니터링 — 본격), 7.7(MLOps 관측). 환경은 5.11.
정직: ensemble·BLS의 config 스키마는 비교적 안정적이나, 일부 메트릭 이름·BLS API·기본값은 버전 의존이다 — 정확한 목록은 그 버전 문서/
:8002/metrics실제 출력으로 확인. 모든 수치는 환경 의존 개략값, 실측.
5.14.2 왜 파이프라인을 서버에서 — 클라이언트 왕복의 비용
추론 한 건은 보통 여러 단계다:
위(나쁨)는 매 화살표마다 네트워크 왕복 + GPU↔호스트 복사(2.5)가 일어나 중간 텐서가 GPU를 떠났다 돌아온다 → 지연·대역 낭비, 코드 복잡. 아래(좋음)는 한 번 호출로 중간 텐서가 GPU에 머물러(단계 간 직접 전달) → 왕복·복사 제거, 클라이언트 단순, 단계 재사용.
이점:
- 데이터 지역성: 중간 텐서가 GPU(또는 서버)에 머물러 호스트 왕복 제거(2.5·3.8).
- 단일 API 호출: 클라이언트는 파이프라인을 한 모델처럼 호출.
- 재사용·관측: 각 단계가 독립 모델이라 재사용·버전·메트릭이 단계별로.
- 배칭·인스턴스 적용: 각 단계가 자기 백엔드의 배칭·인스턴스를 가짐(5.12~5.13).
5.14.3 ensemble — 정적 DAG 파이프라인
ensemble은 모델들을 방향성 그래프(DAG) 로 연결한다. ensemble 플랫폼의 config.pbtxt에 단계(step)와 텐서 매핑을 선언한다(실제 가중치 없음 — 라우팅만).
# model_repository/detect_pipeline/config.pbtxt
name: "detect_pipeline"
platform: "ensemble"
max_batch_size: 32
input [ { name: "RAW_IMAGE", data_type: TYPE_UINT8, dims: [ -1 ] } ] # 인코딩된 이미지 바이트
output [ { name: "DETECTIONS", data_type: TYPE_FP32, dims: [ -1, 6 ] } ]
ensemble_scheduling {
step [
{
model_name: "dali_preprocess" # ① 전처리(DALI, 3.8)
model_version: -1
input_map { key: "DALI_INPUT" value: "RAW_IMAGE" } # ensemble 입력 → 단계 입력
output_map { key: "DALI_OUTPUT" value: "preprocessed" } # 단계 출력 → 내부 텐서명
},
{
model_name: "detector_trt" # ② 추론(TensorRT, 5.2)
model_version: -1
input_map { key: "input" value: "preprocessed" } # 앞 단계 출력을 입력으로
output_map { key: "raw_boxes" value: "raw_boxes" }
},
{
model_name: "nms_python" # ③ 후처리(Python: NMS)
model_version: -1
input_map { key: "boxes" value: "raw_boxes" }
output_map { key: "result" value: "DETECTIONS" } # 최종 → ensemble 출력
}
]
}
작동:
- 각 단계는 독립 모델(자기 백엔드·인스턴스·배칭).
- Triton이 텐서를 단계 간 직접 전달(GPU 유지). 클라이언트는 detect_pipeline만 호출.
- 단계는 분기·병합 가능(한 출력을 여러 단계 입력으로 등) — 단, '정적' 그래프.
한계: ensemble은 정적 DAG다 — 조건 분기(if), 반복(loop), 데이터 의존적 라우팅이 안 된다. "입력에 따라 모델 A 또는 B"나 "결과가 임계 미만이면 재시도" 같은 동적 제어가 필요하면 BLS로.
5.14.4 BLS — 동적 제어 흐름
BLS(Business Logic Scripting) 는 Python 백엔드 안에서 다른 모델을 프로그래밍 방식으로 호출한다. 일반 코드(if/else·loop·예외)로 파이프라인을 짠다.
# model_repository/router/1/model.py — BLS 골격(API는 버전 확인)
import triton_python_backend_utils as pb_utils
import numpy as np
class TritonPythonModel:
def execute(self, requests):
responses = []
for request in requests:
x = pb_utils.get_input_tensor_by_name(request, "input")
# ① 다른 모델을 '호출'(InferenceRequest) — 조건·반복 가능
cls_req = pb_utils.InferenceRequest(
model_name="classifier_trt", requested_output_names=["probs"],
inputs=[x])
cls_res = cls_req.exec() # 동기 호출(async_exec도)
probs = pb_utils.get_output_tensor_by_name(cls_res, "probs").as_numpy()
# ② 데이터 의존 분기: 확신 낮으면 더 큰 모델로 캐스케이드
if probs.max() < 0.7:
big_req = pb_utils.InferenceRequest(
model_name="big_model_trt", requested_output_names=["probs"],
inputs=[x])
probs = pb_utils.get_output_tensor_by_name(
big_req.exec(), "probs").as_numpy()
out = pb_utils.Tensor("probs", probs.astype(np.float32))
responses.append(pb_utils.InferenceResponse(output_tensors=[out]))
return responses
BLS가 가능케 하는 것:
- 조건 분기: 입력·중간 결과에 따라 다른 모델 선택(라우팅·캐스케이드 — 저비용 모델로 거르고 어려운 것만 큰 모델로).
- 반복: 결과가 기준 미달이면 재시도, 또는 반복적 정제.
- 다중 모델 조합: 여러 모델 호출·결과 결합(앙상블 투표 등)을 코드로.
- 동기/비동기 호출:
exec/async_exec로 병렬 호출.
| ensemble(정적 DAG) | BLS(프로그래밍) | |
|---|---|---|
| 형태 | A → B → C (고정 경로) | if cond: call B else call C; loop ... |
| 장점 | 단순·선언적·오버헤드↓ | 조건·반복·동적 라우팅 |
| 단점 | 분기·반복 불가 | Python 오버헤드, 복잡도↑ |
견해: 정적 선형/분기 파이프라인은 ensemble(선언적·경량이다), 데이터 의존 제어(분기·반복·캐스케이드)는 BLS. 둘을 섞기도 한다(ensemble 안의 한 단계가 BLS). 캐스케이드(저비용 모델 먼저 → 어려운 것만 큰 모델)는 BLS의 대표 활용 — 평균 비용을 낮춘다.
5.14.5 운영 지표 — Prometheus 메트릭
Triton은 :8002/metrics 로 Prometheus 형식 메트릭을 낸다(5.11). 운영의 눈이다.
curl -s localhost:8002/metrics | grep -E "nv_inference|nv_gpu" | head -30
핵심 메트릭(이름은 버전 의존, 개략):
| 메트릭 | 의미 | 진단 |
|---|---|---|
| nv_inference_request_success/failure | 성공·실패 요청 수 | 실패율·SLA |
| nv_inference_count / exec_count | 추론·실행 수 | 처리량(미분), 배칭 효율(요청수/실행수) |
| nv_inference_queue_duration | 큐 대기 시간 누적 | 큐 김 → 배칭·인스턴스 부족(5.12~5.13) |
| nv_inference_compute_(input/infer/output)_duration | 컴퓨트(복사·실행·출력) 시간 | 실제 GPU 일 |
| nv_gpu_utilization | GPU 활용률 | 낮으면 저활용(배칭·인스턴스) |
| nv_gpu_memory_used_bytes | GPU 메모리 | OOM·인스턴스 예산 |
가장 중요한 진단 — 큐 시간 vs 컴퓨트 시간:
이 분해가 "어디를 고칠지"를 알려준다 — 맹목적으로 GPU를 늘리기 전에 큐인지 컴퓨트인지 복사인지를 메트릭으로 본다.
# 배칭 효율: exec_count 대비 inference_count
# inference_count / exec_count ≈ 평균 배치 크기 → 1에 가까우면 배칭 거의 안 됨(5.13)
5.14.6 워크스루 — 메트릭으로 병목 진단
# 1) 부하를 주며(perf_analyzer, 5.11) 메트릭을 주기적으로 수집
perf_analyzer -m detect_pipeline --concurrency-range 1:16 -u localhost:8000 &
watch -n 1 'curl -s localhost:8002/metrics | grep -E "queue_duration|compute_infer|gpu_utilization"'
# 2) 해석
# GPU util 낮고 queue_duration 큼 → 자원 못 받고 큐에 쌓임 → 인스턴스·동적배칭↑(5.12~5.13)
# GPU util 높고 compute_infer 큼 → GPU가 일하느라 바쁨 → 모델 최적화(정밀도·엔진, 5.2~5.5)
# compute_input(복사) 큼 → 전처리/전송 병목 → ensemble로 GPU 유지·DALI(3.8)
# 3) 단계별(ensemble): 각 단계 모델의 메트릭을 따로 봐 어느 단계가 병목인지
curl -s localhost:8002/metrics | grep 'model="detector_trt"' # vs dali_preprocess, nms_python
model warmup·readiness(운영 안정):
- warmup: 모델 로드 후 첫 요청이 느리지 않게(엔진 초기화·캐시)
model_warmup으로 사전 추론(config). 첫 요청 지연 스파이크 방지. - readiness:
:8000/v2/health/ready를 k8s readiness probe로 — 준비 안 된 파드에 트래픽 안 보냄(7.3).
5.14.7 중간 점검 (체크포인트)
- [ ] 파이프라인을 서버에서 묶는 이유(클라이언트 왕복·GPU 복사 제거)를 설명한다.
- [ ] ensemble로 전처리→추론→후처리를 DAG로 연결하고, input/output 매핑·정적 한계를 안다.
- [ ] BLS가 조건 분기·반복·캐스케이드를 가능케 함을, ensemble과 대비해 설명한다.
- [ ] Prometheus 메트릭에서 큐 시간 vs 컴퓨트 시간 vs 복사로 병목을 진단한다.
- [ ] inference_count/exec_count로 배칭 효율을, gpu_utilization으로 활용률을 읽는다.
- [ ] warmup·readiness로 운영 안정성을 확보하고 k8s·DCGM(7.3)과 연동한다.
증상→의심: 큐 시간 ≫ 컴퓨트 → 자원 부족 → 인스턴스·동적 배칭↑·GPU 추가(5.12~5.13). 컴퓨트 ≫ 큐 → 모델 느림 → 정밀도·엔진(5.2~5.5). input 복사 큼 → 전처리/전송 → ensemble(GPU 유지)·DALI·pinned(3.8·2.5). 배치 크기≈1 → 동적 배칭 미작동(5.13). 첫 요청만 느림 → warmup 미설정. 조건 분기 필요한데 ensemble로 안 됨 → BLS. k8s가 준비 안 된 파드로 트래픽 → readiness probe 미연결.
5.14.8 트레이드오프·대안
파이프라인을 무엇으로?
| 방식 | 누구에게 | 비고 |
|---|---|---|
| ensemble | 정적 선형·분기 파이프라인 | 선언적·경량, 조건/반복 불가 |
| BLS | 동적 라우팅·캐스케이드·반복 | 유연, Python 오버헤드·복잡 |
| 클라이언트 오케스트레이션 | 파이프라인이 서버 밖 로직과 얽힘 | 단순하나 왕복·복사 비용 |
| 외부 워크플로(예: KServe 그래프) | 다중 서비스·복잡 워크플로 | 서비스 경계 넘는 오케스트레이션 |
견해: GPU 데이터 지역성이 중요한 추론 파이프라인은 서버 안(ensemble/BLS) 에 두는 것이 거의 항상 낫다 — 왕복·복사를 없앤다. 정적이면 ensemble, 동적 제어가 필요하면 BLS, 둘을 섞는다. 서비스 경계를 넘는 복잡 워크플로(여러 마이크로서비스·외부 API)는 상위 오케스트레이션(6.11 에이전트·11.1 워크플로)으로. 캐스케이드(저비용→고비용)는 BLS로 평균 비용을 낮추는 강력한 패턴.
비엔비디아 정직: 파이프라인·관측은 모든 서빙의 공통 과제다(KServe inference graph·Seldon·Ray Serve DAG, Prometheus는 표준). Triton의 강점은 ensemble/BLS의 GPU 데이터 지역성 + 단계별 배칭·인스턴스 + DCGM·메트릭 통합이다. 복잡 워크플로는 상위 오케스트레이터와 조합. 모든 운영 결정은 메트릭 실측으로.
5.14.9 흔한 함정
- 클라이언트에서 단계 왕복. 전처리·후처리를 클라이언트가 따로 호출하면 GPU 복사·네트워크 왕복 낭비. ensemble/BLS로 서버에.
- ensemble로 조건 분기 시도. ensemble은 정적 DAG — if/loop 불가. 동적이면 BLS.
- BLS 남용. 정적 선형 파이프라인까지 BLS로 구성하면 Python 오버헤드·복잡도만↑. 정적은 ensemble.
- 메트릭 없이 운영.
:8002/metrics·Grafana 없이 추측으로 튜닝 → 병목을 잘못 짚음. 큐 vs 컴퓨트 분해를 본다. - 큐 시간을 모델 느림으로 오해. 큐 ≫ 컴퓨트면 모델이 아니라 자원·배칭 문제 → 인스턴스·배칭, GPU 최적화가 아님.
- warmup 미설정. 첫 요청이 엔진 초기화로 느려 p99 스파이크.
model_warmup. - readiness 미연결. k8s가 준비 안 된 파드로 트래픽을 보냄 → 초기 실패. readiness probe 연결(7.3).
- 단계별 메트릭 무시. ensemble 전체만 보면 어느 단계가 병목인지 모름. 단계 모델별 메트릭으로 좁힌다.
5.14.10 연습문제
연습 1 (왜 서버 파이프라인). 이미지 검출 서비스가 전처리(리사이즈·정규화)·추론·후처리(NMS)를 클라이언트에서 각각 호출한다. 지연이 높다. ensemble로 바꾸면 무엇이·왜 개선되나?
정답
개선: ① GPU↔클라이언트 왕복 제거 — 클라이언트 호출이 3번에서 1번으로, 중간 텐서(전처리 결과·raw boxes)가 네트워크를 안 탐. ② GPU 데이터 지역성 — 중간 텐서가 GPU에 머물러 H2D/D2H 복사(2.5)가 단계마다 안 일어남(특히 큰 이미지·특징맵). ③ 전처리 GPU 가속 — DALI 단계로 전처리도 GPU에서(3.8), 클라이언트 CPU 전처리 제거. ④ 단일 API·단계별 배칭/인스턴스 — 각 단계가 자기 배칭·인스턴스를 가짐. 왜: 검출 파이프라인의 지연은 모델 실행만이 아니라 단계 간 데이터 이동·복사가 큰 비중인데, ensemble이 그것을 GPU 안에 가둬 없앤다.
연습 2 (ensemble vs BLS). 다음에 ensemble과 BLS 중 무엇이 맞나: (a) 전처리→추론→후처리 고정 파이프라인, (b) 입력 난이도에 따라 작은 모델로 거르고 어려운 것만 큰 모델로(캐스케이드), (c) 결과 신뢰도가 낮으면 다른 입력으로 재추론 반복.
정답
(a) ensemble — 고정 선형 DAG라 선언적으로 묶으면 됨(분기·반복 없음), 경량이고 오버헤드 적음. (b) BLS — "신뢰도에 따라 큰 모델 호출"은 데이터 의존 조건 분기라 정적 DAG로 불가. Python if로 캐스케이드(저비용 모델 먼저 → max prob<임계면 큰 모델). 평균 비용↓. (c) BLS — 반복(루프)이 필요해 ensemble 불가. BLS에서 while로 신뢰도 기준까지 재추론. 정리: 정적=ensemble, 조건/반복/캐스케이드=BLS. (a)에 BLS를 쓰면 불필요한 복잡·오버헤드.
연습 3 (큐 vs 컴퓨트). 두 모델의 메트릭: 모델X는 queue=2ms, compute_infer=1ms. 모델Y는 queue=0.2ms, compute_infer=8ms. 각각의 병목과 처방을 구분하라.
정답
모델X(queue 2ms ≫ compute 1ms): 요청이 처리 자원을 기다림 — 모델 실행은 빠른데 인스턴스·배칭 부족으로 큐에 쌓임. 처방: 인스턴스↑·동적 배칭↑(5.12~5.13) 또는 GPU 추가. 모델을 더 최적화해도 큐는 안 줄어듦. 모델Y(queue 0.2ms ≪ compute 8ms): 자원은 충분, 모델 실행 자체가 느림. 처방: 모델 최적화 — 정밀도(FP16/INT8/FP8, 5.4~5.5)·엔진 튜닝(5.2~5.3)·융합. 인스턴스를 늘려도 compute가 안 줄어 처리량만 약간. 핵심: 큐 문제는 자원/배칭, 컴퓨트 문제는 모델 — 메트릭 분해로 구분해야 잘못된 처방을 피한다.
연습 4 (복사 병목). ensemble 메트릭에서 compute_input(입력 복사) 시간이 compute_infer보다 크다. 원인 후보와 해결책을 제시하라.
정답
원인 후보: ① 큰 입력의 H2D 전송(고해상도 이미지·큰 텐서)이 추론보다 오래 걸림 — pinned 메모리 미사용·전처리가 CPU(2.5). ② 전처리가 GPU 밖이라 데이터가 호스트를 오감. 해결: ① DALI 전처리 단계를 ensemble에 넣어 디코딩·리사이즈를 GPU에서(3.8) → 작은 원시 바이트만 전송하고 GPU에서 확장. ② pinned 메모리·비동기 전송으로 복사·연산 오버랩(2.5·2.10). ③ 입력을 인코딩된 바이트로 받아(예: JPEG) GPU에서 디코딩 → 전송량↓. 핵심: 큰 전처리 입력을 GPU 안에서 처리해 호스트 왕복·복사를 줄인다.
연습 5 (운영 설계). production Triton 배포의 관측·안정성을 설계하라(메트릭·헬스·warmup·k8s).
정답
① 메트릭: :8002/metrics를 Prometheus로 스크레이프, Grafana 대시보드(처리량·p99 지연·실패율·큐 vs 컴퓨트 시간 분해·GPU util·메모리). DCGM(7.3)으로 GPU 온도·전력·ECC 결합. ② 헬스/readiness: :8000/v2/health/ready를 k8s readiness probe, :8000/v2/health/live를 liveness probe — 준비 안 된 파드에 트래픽 차단·죽은 파드 재시작. ③ warmup: model_warmup으로 로드 후 사전 추론 → 첫 요청 지연 스파이크 방지. ④ 오토스케일: GPU util·큐 시간 기반 HPA(또는 KEDA)로 replica 조정. ⑤ 알림: 실패율·p99·GPU 온도 임계 알림. ⑥ 단계별 메트릭: ensemble 각 단계 모델별로 병목 추적. 핵심: 추측이 아니라 메트릭으로 병목·용량·SLA를 상시 가시화.
5.14.11 요약 · 다음 장
- 추론 파이프라인(전처리→추론→후처리)을 서버 안에서 묶으면 클라이언트 왕복·GPU 복사를 없앤다 — 데이터 지역성이 핵심.
- ensemble은 모델을 정적 DAG로 연결(선언적·경량), 클라이언트는 한 번 호출. BLS는 Python으로 조건 분기·반복·캐스케이드 등 동적 제어. 정적=ensemble, 동적=BLS, 섞어 쓴다.
- 운영 지표(Prometheus,
:8002)의 핵심은 큐 시간 vs 컴퓨트 시간 vs 복사 분해 — 큐 문제는 자원/배칭(5.12~5.13), 컴퓨트 문제는 모델(5.2~5.5), 복사 문제는 전처리/전송(3.8·2.5). 배칭 효율은 inference/exec_count로. - warmup·readiness로 안정성을, Grafana·DCGM·k8s(7.3)로 관측·오토스케일을 — 추측이 아니라 메트릭으로.
- 정직: 메트릭 이름·BLS API 일부 버전 의존, 모든 수치 실측, 복잡 워크플로는 상위 오케스트레이션과 조합.
다음 장: 5.15 오픈소스 서빙과 비교. 제5부의 마지막. 지금까지 본 엔비디아 스택(TensorRT·TRT-LLM·Triton)을 vLLM·SGLang 등 오픈소스 LLM 서빙과 정직하게 비교한다 — 같은 사면체(5.1)를 다른 설계로 푸는 두 진영의 강약, 언제 무엇을 고르나, 그리고 워크로드로 실측하는 법을 본다.