앙상블 모델
앙상블 모델 (Ensemble Models)
앙상블 모델(ensemble model)은 하나 이상의 모델의 파이프라인과, 그 모델들 사이의 입력·출력 텐서 연결을 나타내요. 앙상블 모델은 "데이터 전처리(preprocessing) → 추론(inference) → 데이터 후처리(postprocessing)"처럼 여러 모델이 관여하는 절차를 캡슐화하는 데 쓰여요. 이렇게 쓰면 중간 텐서를 전달하는 오버헤드를 피할 수 있고, Triton에 보내야 하는 요청 수를 최소화할 수 있어요.
앙상블 내부의 모델들이 쓰는 스케줄러와 무관하게, 앙상블 모델에는 반드시 앙상블 스케줄러를 사용해야 해요. 앙상블 스케줄러 관점에서 앙상블 모델은 실제 모델이 아니에요. 대신 모델 구성의 ModelEnsembling::Step 항목으로 앙상블 내부 모델들 사이의 데이터 흐름을 지정해요. 스케줄러는 각 스텝의 출력 텐서를 모아서, 명세에 따라 다른 스텝의 입력 텐서로 제공해요. 그럼에도 앙상블 모델은 외부 관점에서는 여전히 단일 모델로 보여요.
앙상블 모델은 관여하는 모델들의 특성을 그대로 물려받으므로, 요청 헤더의 메타데이터가 앙상블 내부 모델들과 일치해야 해요. 예를 들어 내부 모델 중 하나가 stateful 모델이라면, 앙상블 모델의 추론 요청은 Stateful 모델에서 언급된 정보를 담아야 하고, 스케줄러가 그 정보를 stateful 모델에 제공해요.
예시로, 이미지 분류·분할을 위한 앙상블 모델이 다음과 같은 모델 구성을 갖는다고 해볼게요.
name: "ensemble_model"
platform: "ensemble"
max_batch_size: 1
input [
{
name: "IMAGE"
data_type: TYPE_STRING
dims: [ 1 ]
}
]
output [
{
name: "CLASSIFICATION"
data_type: TYPE_FP32
dims: [ 1000 ]
},
{
name: "SEGMENTATION"
data_type: TYPE_FP32
dims: [ 3, 224, 224 ]
}
]
ensemble_scheduling {
step [
{
model_name: "image_preprocess_model"
model_version: -1
input_map {
key: "RAW_IMAGE"
value: "IMAGE"
}
output_map {
key: "PREPROCESSED_OUTPUT"
value: "preprocessed_image"
}
},
{
model_name: "classification_model"
model_version: -1
input_map {
key: "FORMATTED_IMAGE"
value: "preprocessed_image"
}
output_map {
key: "CLASSIFICATION_OUTPUT"
value: "CLASSIFICATION"
}
},
{
model_name: "segmentation_model"
model_version: -1
input_map {
key: "FORMATTED_IMAGE"
value: "preprocessed_image"
}
output_map {
key: "SEGMENTATION_OUTPUT"
value: "SEGMENTATION"
}
}
]
}
ensemble_scheduling 섹션은 앙상블 스케줄러가 사용될 것이고, 앙상블 모델이 세 개의 서로 다른 모델로 구성되어 있음을 나타내요. step 섹션의 각 요소는 사용할 모델과, 그 모델의 입력·출력이 스케줄러가 인식하는 텐서 이름에 어떻게 매핑되는지를 지정해요. 예를 들어 step의 첫 번째 요소는 image_preprocess_model의 최신 버전을 쓰고, 그 입력 "RAW_IMAGE"의 내용은 "IMAGE" 텐서가 제공하며, 그 출력 "PREPROCESSED_OUTPUT"의 내용은 나중에 쓰기 위해 "preprocessed_image" 텐서로 매핑된다는 뜻이에요. 스케줄러가 인식하는 텐서 이름은 앙상블 입력, 앙상블 출력, 그리고 input_map과 output_map의 모든 값이에요.
앙상블을 구성하는 모델들은 동적 배칭(dynamic batching)도 켤 수 있어요. 앙상블 모델은 구성 모델들 사이에서 데이터를 라우팅만 하므로, 구성 모델들의 동적 배칭을 활용하기 위해 앙상블의 구성을 수정하지 않고도 Triton이 요청을 앙상블 모델로 받아들일 수 있어요.
앙상블 모델, 전처리 모델, 분류 모델, 분할 모델만 서빙 중이라고 가정하면, 클라이언트 애플리케이션은 이 넷을 각각 독립적으로 요청을 처리할 수 있는 네 개의 서로 다른 모델로 보게 돼요. 하지만 앙상블 스케줄러는 앙상블 모델을 아래처럼 보게 돼요.

앙상블 모델에 대한 추론 요청을 받으면, 앙상블 스케줄러는:
- 요청의 "IMAGE" 텐서가 전처리 모델의 입력 "RAW_IMAGE"에 매핑되어 있음을 인식해요.
- 앙상블 내부 모델들을 확인하고, 필요한 모든 입력 텐서가 준비됐으므로 전처리 모델에 내부 요청을 보내요.
- 내부 요청 완료를 인식하고, 출력 텐서를 모아서 그 내용을 앙상블 내에서 고유한 이름으로 알려진 "preprocessed_image"에 매핑해요.
- 새로 모은 텐서를 앙상블 내부 모델들의 입력에 매핑해요. 이 경우 "classification_model"과 "segmentation_model"의 입력이 매핑되고 준비됨으로 표시돼요.
- 새로 모은 텐서를 필요로 하는 모델을 확인하고, 입력이 준비된 모델(이 경우 분류 모델과 분할 모델)에 내부 요청을 보내요. 응답 순서는 각 모델의 부하와 계산 시간에 따라 임의의 순서가 될 수 있다는 점에 유의하세요.
- 보낼 내부 요청이 더 없을 때까지 3~5단계를 반복하고, 앙상블 출력 이름에 매핑된 텐서로 추론 요청에 응답해요.
다른 모델과 달리 앙상블 모델은 모델 구성에서 "instance_group" 필드를 지원하지 않아요. 그 이유는 앙상블 스케줄러 자체가 오버헤드가 아주 작은 이벤트 기반 스케줄러라 거의 파이프라인의 병목이 되지 않기 때문이에요. 앙상블 내부의 구성 모델들은 각자의 instance_group 설정으로 개별적으로 확장·축소할 수 있어요. 모델 파이프라인 성능을 최적화하려면 Model Analyzer로 최적의 모델 구성을 찾을 수 있어요.
앙상블 스텝을 만들 때 input_map/output_map의 key와 value의 차이를 알아두면 유용해요.
- key: 구성 모델의
input/output텐서 이름이에요. - value: 앙상블 모델의 텐서 이름으로, 앙상블
input/output을 구성 모델의 그것들에 그리고 구성 모델 서로에게 연결하는 식별자 역할을 해요.
앙상블 모델의 메모리 증가 제한 (Limit Memory Growth in Ensemble Models, BETA)
inflight request(처리 중 요청)란 앙상블 파이프라인에서 상류(upstream) 모델이 생성해 큐에 넣고, 하류(downstream) 모델이 처리할 때까지 메모리에 보관되는 중간 요청을 말해요. 상류 모델이 하류 모델보다 훨씬 빠르게 요청을 처리하면 이런 inflight 요청이 쌓여 잠재적으로 무한정 메모리가 늘어날 수 있어요. 이 문제는 파이프라인의 서로 다른 스텝 사이에 속도 불일치가 있을 때 발생하며, 특히 요청당 여러 응답을 하류 모델이 소비할 수 있는 속도보다 빠르게 생성하는 decoupled 모델에서 흔해요.
상류 모델이 10배 빠른 두 스텝짜리 앙상블 모델 예시를 들어볼게요.
- 전처리 모델: 초당 100개의 전처리 요청 생성
- 추론 모델: 초당 10개의 요청 소비
백프레셔(backpressure)가 없으면 요청이 처리 속도보다 빨리 파이프라인에 쌓여 결국 메모리 부족(out-of-memory) 오류로 이어져요.
앙상블 구성의 max_inflight_requests 필드는 각 앙상블 스텝에서 허용되는 동시 inflight 요청 수의 상한을 정의해요. 이 한도는 그 앙상블 모델의 모든 활성 요청에 걸쳐 공유돼요. 한도에 도달하면 하류 모델이 여유를 확보할 때까지 해당 스텝의 새 요청 스케줄링이 일시 중지돼요. 이렇게 하면 요청이 무한정 쌓이지 않고 메모리 사용이 통제되게 유지돼요.
ensemble_scheduling {
max_inflight_requests: 16
step [
{
model_name: "dali_preprocess"
model_version: -1
input_map { key: "RAW_IMAGE", value: "IMAGE" }
output_map { key: "PREPROCESSED_IMAGE", value: "preprocessed" }
},
{
model_name: "onnx_inference"
model_version: -1
input_map { key: "INPUT", value: "preprocessed" }
output_map { key: "OUTPUT", value: "RESULT" }
}
]
}
구성:
max_inflight_requests: 16: 주어진 앙상블 스텝에서 동시 inflight 요청 수를 16으로 제한해요(예:onnx_inference가 완료되기를 기다리는dali_preprocess의 요청). 이 한도는 그 앙상블 모델의 모든 활성 요청에 걸쳐 공유돼요. 한도에 도달하면 하류 용량이 확보될 때까지 해당 스텝의 새 작업 스케줄링이 일시 중지돼요.- 기본값 (
0): 제한 없음 — inflight 요청 수를 무제한 허용(원래 동작)해요.
이 기능을 언제 쓸까 (When to Use This Feature)
max_inflight_requests는 앙상블 파이프라인에 다음이 포함될 때 쓰세요.
- 스트리밍·decoupled 모델: 모델이 요청당 여러 응답을 하류 스텝이 처리할 수 있는 속도보다 빠르게 생성할 때.
- 메모리 제약: 요청이 쌓이면서 무한정 메모리가 늘어날 위험이 있을 때.
올바른 값 고르기 (Choosing the Right Value)
최적의 값은 배치 크기, 요청 속도, 사용 가능한 메모리, 처리량 등 구체적인 배포 환경에 따라 달라요.
- 너무 낮으면: 생산자(producer) 스텝이 너무 자주 막혀서 더 빠른 모델을 충분히 활용하지 못해요.
- 너무 높으면: 메모리 사용이 늘어나 백프레셔의 효과가 줄어들어요.
- 권장: 작은 값에서 시작해서 메모리 사용과 처리량 모니터링에 따라 조정하세요.
성능 고려사항 (Performance Considerations)
-
비활성 시 오버헤드 0:
max_inflight_requests: 0(기본값)이면 동기화 오버헤드가 전혀 발생하지 않아요. -
활성 시 최소 오버헤드: 앙상블 스텝마다 blocking/wakeup 메커니즘을 써서, inflight 요청 한도에 도달하면 상류 모델이 일시 중지("blocked")되고 하류 모델이 요청 처리를 완료하면 재개("woken up")돼요. 이 동기화로 메모리 사용이 범위 내에 유지되지만, 지연시간(latency)은 늘어날 수 있어요.
참고: 이 blocking은 중간 요청을 취소하거나 내부적으로 타임아웃시키지 않지만, 클라이언트는 종단 간 지연시간이 늘어난 것을 경험할 수 있어요.
추가 자료 (Additional Resources)
앙상플에 대한 더 자세한 종단 간(end-to-end) 예시는 아래 링크에서 볼 수 있어요.
- 이 가이드가 실행 예시와 함께 앙상블 개념을 탐구해요.
- Python 백엔드에서 앙상블을 이용한 전처리
- NVIDIA Triton Inference Server와 NVIDIA DALI로 추론 가속화
- NVIDIA Triton Inference Server와 RAPIDS AI 사용