인코더는 왜 다른가¶
TL;DR
인코더에는 KV cache와 decode step가 없습니다. 따라서 vLLM의 KV cache 최적화, 연속 배칭, TTFT처럼 디코더 LLM을 전제로 한 서빙 방식과 지표를 그대로 적용할 수 없습니다.
추론 패턴의 차이¶
| 디코더 LLM (GPT, Llama) | 인코더 (BERT, DeBERTa) | |
|---|---|---|
| 요청 1건의 계산 | 프롬프트 처리 + N번의 decode step | forward pass 1회 |
| KV cache | 디코딩 성능의 핵심 | 없음 |
| 출력 | 가변 길이 토큰 시퀀스 | 고정 크기 logit(3-way 등) |
| 시퀀스 길이 | 가변, 수천~수만 | 고정 상한(512 등) |
| 스트리밍 | 토큰을 순차 반환 | 응답을 한 번에 반환 |
이 차이가 세 가지 결과를 만듭니다.
인코더와 디코더의 핵심 차이
디코더는 토큰을 하나씩 생성합니다. 앞서 생성한 토큰을 다음 입력으로 사용하므로 KV cache가 필요하고, 요청 하나가 여러 decode step에 걸쳐 처리됩니다.
인코더는 입력 전체를 한 번에 처리하고 분류, 임베딩, 랭킹과 같은 고정 크기 결과를 반환합니다. forward pass가 한 번뿐이므로 KV cache와 토큰 스트리밍이 필요하지 않습니다.
1. vLLM 최적화의 적용 범위¶
vLLM의 대표적인 최적화는 다음 두 가지입니다.
- Paged KV cache: 디코딩 중 증가하는 KV cache를 페이지 단위로 관리
- Continuous batching: 완료된 요청의 자리에 새로운 요청을 추가
인코더에는 KV cache가 없고 요청이 forward pass 한 번으로 끝나므로 두 최적화의 이점이 제한적입니다. 이 저장소의 측정에서도 batch가 증가하면 vLLM의 throughput이 HF DLC보다 낮아졌습니다.
| batch | HF DLC(samples/s) | vLLM(samples/s) | HF DLC 대비 |
|---|---|---|---|
| 1 | 140.9 | 213.5 | 1.52x |
| 8 | 926.5 | 906.4 | 0.98x |
| 32 | 2374.6 | 1835.0 | 0.77x |
batch 1에서 vLLM이 앞선 원인은 더 단순한 HTTP 처리 경로로 추정됩니다. 이 결과만으로 KV cache 최적화의 효과라고 볼 수는 없습니다.
자세한 내용: 배포 방식 고르기
지원하지 않는 모델 아키텍처¶
이 저장소의 DebertaV2ForSequenceClassification 모델(model_type: deberta-v2)은 다음 서빙 스택에서 로드되지 않았습니다.
| 스택 | 결과 | 확인 방법 |
|---|---|---|
| vLLM | 아키텍처 미지원 | 실측: Model architectures ['DebertaV2ForSequenceClassification'] are not supported |
| Hugging Face TEI | 아키텍처 미지원 | Sequence Classification 지원 목록에 DeBERTa가 없음 |
| Ollama, llama.cpp | 변환 불가 + 분류 API 없음 | 문서와 소스 확인 |
vLLM은 직접 배포해 확인했으며, TEI와 Ollama/llama.cpp는 공식 문서와 소스를 기준으로 확인했습니다.
llama.cpp의 GGUF 변환 경로에는 2026년 8월 8일 확인 당시 DeBERTa 계열이 포함되어 있지 않았습니다. 또한 Ollama API는 생성, 채팅, 임베딩 API를 제공하지만 Sequence Classification용 API는 제공하지 않습니다. 임베딩 모델 지원과 분류 모델 서빙 지원은 구분해야 합니다.
DeBERTaV2는 상대 위치 인코딩과 disentangled attention을 사용하므로 표준 BERT 어텐션을 전제로 한 구현에 별도 지원 코드가 필요합니다.
이 모델은 ONNX 또는 TensorRT로 내보낸 그래프를 실행하는 경로를 사용할 수 있습니다. 이 저장소의 02 ONNX Runtime과 07 Triton + TensorRT가 해당 방식입니다. 모델을 변경할 수 있다면 XLM-RoBERTa 계열에서 더 많은 서빙 엔진을 선택할 수 있지만, 모델별 정확도는 별도로 검증해야 합니다. 측정 결과는 모델 선택의 절충에 정리되어 있습니다.
2. 디코더 지표를 쓸 수 없습니다¶
| 디코더 지표 | 인코더 모델에서의 의미 |
|---|---|
| TTFT (time to first token) | 최초 출력 토큰이 없으므로 정의할 수 없음 |
| TPOT (time per output token) | 출력 토큰을 순차 생성하지 않으므로 정의할 수 없음 |
| ITL (inter-token latency) | 토큰 사이의 간격이 없으므로 정의할 수 없음 |
| Output token throughput | 출력이 고정 크기 logit이므로 정의할 수 없음 |
| E2E latency | 요청부터 응답까지의 종단 간 latency |
| Request throughput | 초당 처리한 요청 수 |
| Batch efficiency | batch 증가에 따른 throughput 확장 효율 |
이 저장소는 인코더용 전송 계층과 지표를 별도로 구현했습니다. 감마 분포 기반 요청 간격을 포함한 부하 생성 로직은 sm-endpoint-bmt를 참고했습니다.
자세한 내용: 벤치마크 방법
3. 비스트리밍 추론 API¶
디코더 벤치마크는 invoke_endpoint_with_response_stream으로 응답 스트림을 처리해 TTFT를 측정합니다. 인코더는 출력을 한 번에 반환하므로 invoke_endpoint를 사용합니다.
# 디코더: 토큰이 프레임으로 나옴
resp = rt.invoke_endpoint_with_response_stream(...)
for event in resp["Body"]:
... # TTFT, ITL 측정 지점
# 인코더: 한 번에 끝
resp = rt.invoke_endpoint(...)
body = json.loads(resp["Body"].read())
인코더 성능을 결정하는 요소¶
인코더 서빙에서 성능을 결정하는 것은 다른 요소들입니다.
배칭¶
forward pass가 한 번이므로 batch를 높이면 GPU 활용률을 높일 수 있습니다. 다만 HF DLC에서는 요청 payload의 parameters.batch_size를 명시해야 실제 배칭이 적용됩니다.
| batch 32, mDeBERTa, L40S | latency |
|---|---|
parameters 없음 |
510 ms |
parameters.batch_size=32 |
25 ms |
Transformers Pipeline은 batch_size를 지정하지 않으면 입력을 한 건씩 처리합니다.
런타임¶
같은 인스턴스에서 런타임만 바꿔도 31배 차이가 납니다.
| 런타임 | ml.c6i.2xlarge p50 |
|---|---|
| PyTorch | 1810 ms |
| ONNX Runtime | 58 ms |
자세한 내용: ONNX Runtime으로 CPU 배포
워커 수¶
SAGEMAKER_MODEL_SERVER_WORKERS로 컨테이너 내부의 워커 프로세스 수를 조절할 수 있습니다.
| concurrency | 워커 1개 | 워커 4개 |
|---|---|---|
| 4 | 60 req/s | 168 req/s |
| 8 | 60 req/s | 183 req/s |
시퀀스 길이¶
운영 워크로드의 입력 길이가 512로 고정되어 있다면 벤치마크도 같은 조건에서 실행해야 합니다. 실제 문장 길이만 사용하면 필요한 연산량을 과소평가할 수 있습니다.
다음은 같은 03 Inference Component endpoint를 두 조건에서 측정한 결과입니다.
| 조건 | b=1 p50 | b=8 p50 | b=8 samples/s |
|---|---|---|---|
| 실제 입력 길이(토큰 59개) | 23.2 ms | 23.9 ms | 330/s |
max_len=512 고정 |
25.8 ms | 102.8 ms | 78/s |
batch 1에서는 latency 차이가 1.1배지만, batch 8에서는 throughput 차이가 4.3배로 커집니다. 패딩 토큰도 모두 연산하기 때문입니다.