Documents
Home>Documents>AI>Inference

LLM 서빙에서 Decode 배치 효율의 상한선: GPU 메모리 대역폭이 결정하는 최적 동시 요청 수

10 min readSep 1, 2026Sep 1, 2026

max_num_seqs를 512로 올려도 aggregate throughput은 거의 늘지 않는데 TPOT p99는 계속 치솟는 구간이 있다. 컨텍스트 길이 2048 토큰, Llama-3-8B 기준으로 그 구간은 배치 크기 60부터 시작하며, 이 숫자는 GPU 메모리 대역폭·모델 파라미터 바이트·컨텍스트 길이 세 값으로 정확히 예측된다.

Decode의 Arithmetic Intensity가 배치 크기에 갇히는 이유

Prefill과 Decode의 결정적 차이는 Arithmetic Intensity(AI: FLOP/byte)에 있다.

Prefill은 시퀀스 길이 S가 길수록 AI가 S에 비례해서 오른다. attention FLOP이 O(S²·d)인데 메모리 읽기는 O(S·d)에 머물기 때문이다. 충분히 긴 시퀀스에서 Prefill이 compute-bound로 전환되는 이유가 이것이다.

Decode에서는 배치 내 B개 요청이 각자의 KV 캐시를 독립적으로 읽는다. 레이어 하나의 한 decode step을 분해하면:

  • 가중치 행렬 연산: FLOP ≈ 24Bd², 메모리 ≈ 24d² bytes (가중치는 배치가 공유)
  • KV 캐시 attention: FLOP ≈ 4BSD, 메모리 ≈ 4BSD bytes (요청별 독립 읽기)

(d: hidden dim, S: 평균 컨텍스트 길이, B: 배치 크기, BF16 가정)

전체 AI = (24Bd² + 4BSD) / (24d² + 4BSD)

B가 늘면 분자와 분모의 KV 캐시 항이 같은 비율로 커진다. AI는 두 극단 사이에 고정된다:

  • 컨텍스트가 짧고 B가 작을 때: AI ≈ B (가중치 읽기 지배)
  • 컨텍스트가 길 때: AI ≈ 1 (KV 캐시 읽기 지배, B와 무관)

A100의 roofline ridge point는 약 156 FLOP/byte다(312 TFLOPs ÷ 2 TB/s). 컨텍스트 512 토큰, 배치 32에서 Llama-3-405B의 실측 AI가 41.56이고 DeepSeekV3가 89.83이라는 값이 있는데, 두 경우 모두 ridge point 훨씬 아래다. Decode는 어떤 배치 크기에서도 완전한 compute-bound에 도달하지 않는다.

FlashAttention-2는 attention 커널의 메모리 접근 패턴을 분석하면서 Decode 단계의 scattered KV cache read가 캐시 locality를 파괴한다고 지적한다. Prefill의 consolidated access와 근본적으로 다른 이유다.

GPU 메모리 대역폭으로 상한선 계산하기

한 decode step에서 GPU가 읽어야 하는 바이트 수는:

총 읽기 bytes = model_bytes + B × kv_bytes_per_seq

model_bytes      = num_params × 2  (BF16)
kv_bytes_per_seq = num_layers × 2 × num_kv_heads × head_dim × 2 × ctx_len
                   (K와 V 각각, BF16)

이론적 aggregate throughput은 B × GPU_BW / 총_읽기_bytes다. B가 작을 때는 분모의 model_bytes가 지배해 throughput이 B에 거의 선형으로 오른다. B가 커질수록 B × kv_bytes_per_seq 항이 커져 증가율이 꺾인다. 두 항이 같아지는 지점이 B_knee다:

B_knee = model_bytes / kv_bytes_per_seq

Llama-3-8B(32 layers, num_kv_heads=8 GQA, head_dim=128, BF16 16GB)와 Llama-3-70B(80 layers, num_kv_heads=8 GQA, head_dim=128, BF16 140GB) 기준으로 계산하면:

컨텍스트 길이8B kv/요청8B B_knee70B kv/요청70B B_knee
512 tok67 MB238168 MB833
2048 tok268 MB60671 MB208
8192 tok1.07 GB152.68 GB52

70B의 B_knee가 8B보다 훨씬 높다. GQA로 KV head 수가 동일(8개)한 상황에서 모델 파라미터는 8.75배 크지만 KV 캐시는 2.5배만 크기 때문이다. H100 SXM(3.35 TB/s)과 A100(2 TB/s)에서 B_knee는 같다. BW가 바뀌어도 두 항의 교차점 자체는 달라지지 않는다. H100은 TPOT을 A100 대비 40% 낮추지만 B_knee는 동일한 위치에 있다.

배치 크기별 TPOT과 throughput: 이론값

Llama-3-8B를 A100(2 TB/s) 한 장에서 서빙할 때, roofline 모델 기반 이론값(실측과 10~20% 오차 내)은 다음과 같다.

배치 크기S=512 TPOTS=512 tok/sS=2048 TPOTS=2048 tok/sS=8192 TPOTS=8192 tok/s
B=18.0 ms1258.1 ms1238.5 ms117
B=88.3 ms9689.1 ms88012.3 ms651
B=329.1 ms3,52712.3 ms2,60225.2 ms1,270
B=6410.1 ms6,30416.6 ms3,85742.4 ms1,509
B=12812.3 ms10,41525.2 ms5,07976.7 ms1,668
B=25616.6 ms15,42242.4 ms6,040145.5 ms1,759

S=512에서는 B=256까지도 throughput이 꾸준히 오르고 TPOT 악화가 완만하다. B_knee가 238이라 아직 한계에 닿지 않았기 때문이다.

S=2048에서는 B=32→64 구간에서 TPOT이 12.3 → 16.6 ms(35% 증가), tok/s는 2,602 → 3,857(48% 증가)이다. 여기까진 배치를 늘릴 이유가 있다. B=64→128 구간은 TPOT이 51% 더 오르고 tok/s는 32% 오르는 데 그친다. 한계 효율이 역전되는 지점이 이 구간이다.

S=8192가 가장 극적이다. B=8에서 이미 TPOT이 B=1 대비 1.45배고, B=32에서는 2.95배다. B_knee가 15이므로 배치 32는 이미 knee를 두 배 지난 위치다. B=32에서 B=256으로 배치를 8배 키워도 tok/s는 1,270 → 1,759로 38% 오르는 데 그친다.

이론값과 실측의 오차 주요 원인은 메모리 단편화(PagedAttention의 블록 단위 할당), 스케줄링 오버헤드, 그리고 attention 커널의 실제 HBM 접근 효율이다.

서빙 엔진의 대응 방식

vLLM--max-num-seqs가 decode 배치의 직접적인 상한이다. continuous batching은 새 prefill을 기존 decode iteration에 끼워 넣으면서 decode 배치가 이 값을 넘지 않도록 유지한다. 문제는 --max-num-seqs 기본값이 256인데, 컨텍스트 2048 토큰 환경에서 8B 모델의 B_knee는 60이라는 점이다. 기본값으로 운영하면 이미 4배를 넘어선 채로 서빙하는 셈이다.

TensorRT-LLMKvCacheConfig(max_tokens=N)으로 전체 KV 캐시 토큰 수에 하드 캡을 건다. 배치 크기 자체가 아닌 KV 캐시 총량을 제한하므로, 컨텍스트 길이가 달라도 대역폭 포화 수준이 일정하게 유지된다. kv_cache_free_gpu_mem_fraction(기본 0.9)과 함께 쓸 때 두 조건 중 작은 쪽이 실제 할당량이 된다.

SGLang의 RadixAttention은 prefix가 동일한 요청들의 KV 캐시를 공유한다. 시스템 프롬프트처럼 모든 요청이 같은 prefix를 쓰는 환경에서는 effective kv_bytes_per_seq가 줄어 B_knee가 올라간다. prefix 공유 비율이 60% 이상인 배포에서는 실질적으로 배치 여유가 2배 가까이 늘기도 한다.

Sarathi-Serve(OSDI 2024)는 chunked prefill로 prefill 청크와 decode를 같은 iteration에 섞어 처리한다. stall-free 스케줄링 덕에 새 요청을 받을 때 decode 배치를 멈추지 않아도 돼서, 배치 크기가 갑자기 치솟을 때 TPOT이 폭등하는 현상이 완화된다. A100 단일 GPU에서 Mistral-7B 기준 vLLM 대비 2.6배 throughput 개선을 보고했다.

세 엔진이 택하는 방식은 다르지만 풀려는 문제는 하나다: decode step에서 GPU가 읽어야 하는 총 바이트를 줄이거나, 그 총량을 제어하는 파라미터를 노출하는 것.

운영에서 쓰는 판단 기준

세 값만 있으면 B_knee를 추정할 수 있다.

# B_knee 추정
model_bytes      = num_params * 2                           # BF16
kv_bytes_per_seq = num_layers * 2 * num_kv_heads * head_dim * 2 * avg_ctx_len
B_knee           = model_bytes / kv_bytes_per_seq

# max_num_seqs 권장 설정 범위: B_knee의 1.0~1.5배

Llama-3-8B, 평균 컨텍스트 2048 토큰: B_knee ≈ 60 → --max-num-seqs 64.
Llama-3-8B, 평균 컨텍스트 8192 토큰: B_knee ≈ 15 → --max-num-seqs 16.

진단 신호는 TPOT p99다. TTFT(Time To First Token) p99가 SLO를 먼저 위반하면 prefill 병목이고, TPOT p99가 먼저 튀기 시작하면 decode 배치가 B_knee를 넘었다는 신호다. nvidia-smi dmon으로 memory bandwidth utilization이 85% 이상에 고착돼 있을 때 배치 크기를 줄이면 TPOT이 즉시 회복된다.

자주 하는 착각 하나: 8B 모델이 70B보다 작으니 더 많은 배치를 감당할 것이라는 직관이다. GQA로 KV head 수가 같은 한, 작은 모델일수록 model_bytes 대비 kv_bytes_per_seq 비율이 높아져 B_knee가 낮다. Llama-3-8B의 B_knee(S=2048)는 60, 70B는 208이다.

Tags
LLMInferenceGPU서빙KV 캐시메모리vLLM