Documents
Home>Documents>AI>Inference

LLM 서빙에서 Arithmetic Intensity가 Batch 크기에 따라 어떻게 이동하는가: 7B·13B·70B 모델별 비교

15 min readSep 10, 2026Sep 10, 2026

vLLM을 처음 운영할 때 대부분의 엔지니어가 마주치는 질문이 있다. max_num_seqs를 얼마로 잡아야 하느냐. 같은 A100 한 장인데 7B 모델과 70B 모델에 같은 값을 쓰면 되느냐.

짧게 답하면: 안 된다. 그리고 그 이유가 "70B가 메모리를 더 쓰니까"보다 훨씬 구체적이고, 컨텍스트 길이에 따라 달라진다.

Decode가 memory-bound인 이유: 수식 하나

Roofline 모델(Williams et al., 2009)에서 병목이 메모리인지 연산인지는 arithmetic intensity(AI) — FLOPs/byte — 하나로 정리된다. AI가 GPU의 ridge point(= 피크 연산량 / 메모리 대역폭)보다 낮으면 memory-bound, 높으면 compute-bound다.

A100 80GB 기준 ridge point = 312 TFLOPS(BF16 TC) / 1.94 TB/s ≈ 161 FLOPs/byte.

Decode 1스텝에서 배치 B개를 처리할 때 수행하는 일은:

  • FLOPs ≈ 2BP (P = 파라미터 수)
  • 로드 바이트 = 가중치 W + KV 캐시 B × kv_per_seq

B=1에서 AI ≈ 2P / (W + kv_per_seq). BF16 기준 W = 2P이므로 AI ≈ 1. Ridge point 161의 두 자리 아래다. Decode는 어떤 배치 크기에서도 memory-bound 영역에 머무른다 — 배치를 아무리 키워도 A100에서 Llama-3 8B decode는 compute-bound로 넘어가지 않는다.

배치가 커질수록 arithmetic intensity는 어떻게 이동하는가

배치 B로 확장하면:

AI(B) = 2BP / (W + B × kv_per_seq)

FLOPs는 B배 늘지만, 가중치 W는 B와 무관하게 고정이다. KV 캐시 바이트는 B에 비례해 함께 증가한다. B → ∞ 극한에서 AI(∞) = 2P / kv_per_seq. Llama-3 8B에서 이 값을 계산하면 약 30 FLOPs/byte — 여전히 ridge point(161) 아래다.

그래도 배치를 키우는 이유는 TPOT(토큰당 지연)가 어떻게 움직이는지를 보면 명확하다:

TPOT(B) = (W + B × kv_per_seq) / (B × BW)
         = W/(B × BW)  +  kv_per_seq/BW

첫 항은 B가 커질수록 줄어든다. B개 요청이 가중치 로드 비용을 나눠 부담하는 효과다. 두 번째 항 kv_per_seq/BW는 B와 무관한 상수로, TPOT의 이론적 하한이다.

포화 배치 크기 B*는 두 항이 같아지는 지점으로 정의한다:

B* = W / kv_per_seq

B 이전에는 배치를 늘릴수록 TPOT가 의미 있게 개선된다. B에서 두 항이 같아지면 TPOT는 하한의 정확히 2배다. B* 이후부터는 추가 배치당 TPOT 개선량이 급격히 줄어들어 실질적인 throughput 포화 구간에 진입한다.

모델별 B* 계산

두 항을 구체적으로 쓰면:

W(BF16) = 파라미터 수 × 2 bytes
kv_per_seq = 2 × num_kv_heads × head_dim × num_layers × seq_len × 2 bytes

Llama-3 8B(BF16), KV 헤드 8개, head_dim 128, 레이어 32개 기준:

kv_per_token = 2 × 8 × 128 × 32 × 2 bytes = 131,072 bytes = 128 KB/token
kv_per_seq(512t)  = 128 KB × 512  = 64 MB
kv_per_seq(2048t) = 128 KB × 2048 = 256 MB
kv_per_seq(8192t) = 128 KB × 8192 = 1,024 MB

B*(512t)  = 16 GB / 64 MB   = 256
B*(2048t) = 16 GB / 256 MB  = 64
B*(8192t) = 16 GB / 1,024 MB = 16

W4A16 양자화를 적용한 Llama-3 8B는 가중치가 4 GB로 줄기 때문에 B(512t) = 4 GB / 64 MB = 64로 내려간다. KV 캐시 크기는 그대로이므로 양자화는 B를 낮추는 방향으로 작용한다.

같은 방식으로 Llama-3 70B, Llama-2 13B(MHA 구조), Mixtral 8x7B를 정리하면:

모델파라미터KV 헤드kv/token (BF16)B*(512t)B*(2048t)B*(8192t)
Llama-3 8B (GQA)8B8128 KB**2566416**
Llama-2 13B (MHA)13B40800 KB**65164**
Llama-3 70B (GQA)70B8320 KB**87521956**
Mixtral 8x7B (GQA)47B8128 KB~1,450~363~91

직관에 반하는 숫자가 하나 있다. Llama-2 13B(MHA)의 B*(512t)는 65인데, Llama-3 8B(GQA)의 256보다 4배 낮다. 파라미터가 더 많은데 포화 배치 크기가 오히려 낮다. MHA가 KV 헤드를 40개 전부 유지하기 때문에 kv_per_token이 800 KB로 Llama-3 8B(128 KB)의 6.25배다. 가중치 증가 폭(13/8 ≈ 1.6×)보다 KV 캐시 증가 폭이 훨씬 커서 B*가 떨어지는 것이다.

70B vs 8B 비교도 마찬가지다. 파라미터 비율은 70/8 ≈ 8.75×인데, B* 비율은 875/256 ≈ 3.4×다. 70B도 KV 헤드는 8개로 동일하지만, 레이어가 32 → 80으로 늘어 kv_per_token이 320 KB로 2.5배 커진다. W가 8.75배 커지는 동안 kv_per_token도 2.5배 커지니 비율이 3.4×로 줄어드는 것이다. "70B니까 배치 목표도 7B의 10배"는 맞지 않는다.

Mixtral B가 높은 이유는 MoE 구조로 전체 가중치(~93 GB BF16)를 메모리에 올려야 하지만, 어텐션 아키텍처는 Llama-3 8B와 동일한 8 KV 헤드를 써서 kv_per_token이 128 KB로 같기 때문이다. 활성 파라미터(~13B)가 아니라 전체 가중치 바이트가 B 계산의 분자다.

배치 스윕 시 TPOT 곡선의 세 구간

A100 80GB에서 vLLM으로 Llama-3 8B를 컨텍스트 512 토큰 고정, max_num_seqs를 1→256으로 스윕하면 TPOT 곡선이 세 구간으로 나뉜다.

구간 1 (B ≤ ~30): TPOT가 빠르게 감소한다. 가중치 로드가 지배적인 구간으로, 배치를 2배로 늘리면 TPOT가 거의 절반에 가깝게 줄어든다. B=1에서 TPOT ≈ W/BW = 16 GB / 2 TB/s ≈ 8 ms/token이다.

구간 2 (B ~ 30 ~ B)**: 개선 속도가 둔화된다. KV 캐시 항이 가중치 항을 따라잡기 시작하는 구간이다. B = B(512t) ≈ 256에서 TPOT = (16 GB + 256 × 64 MB) / (256 × 2 TB/s) ≈ 64 μs/token. 이론적 하한(kv_per_seq/BW = 32 μs)의 정확히 2배다.

구간 3 (B > B_mem): TPOT가 오히려 올라가기 시작한다. KV 캐시가 GPU 메모리를 소진하면 vLLM 스케줄러가 시퀀스를 선점(preemption)하거나 CPU로 스왑하고, 이 비용이 TPOT에 직접 반영된다.

메모리 허용 최대 배치 B_mem은 모델이 쓰고 남은 KV 캐시 풀로 결정된다:

B_mem = (GPU_mem × utilization - W) / kv_per_seq
      = (80 GB × 0.9 - 16 GB) / 64 MB  ≈  900   # Llama-3 8B, 512토큰

512 토큰 기준 B* = 256, B_mem = 900. 256 ~ 900 구간이 "개선은 있지만 미미하고, 위험은 커지는" 구간이다. max_num_seqs를 여기까지 올릴 이유는 없다.

nvidia-smi dmon으로 HBM 대역폭 활용률을 관찰하면 구간 1-2에서는 1.7 ~ 1.9 TB/s 수준으로 포화되고, 구간 3에 진입하면 스왑 대기 중 GPU가 idle해지면서 대역폭 수치가 낮아지고 TPOT가 치솟는다. 대역폭이 포화된 채 TPOT가 더 줄지 않는 지점이 실질적인 B*이고, 대역폭이 낮아지면서 TPOT가 올라가기 시작하면 B_mem을 넘긴 것이다.

컨텍스트 8192 토큰에서는 그림이 달라진다. B* = 16, B_mem ≈ (80 × 0.9 - 16) GB / 1 GB = 56. 구간 1-2가 B=16 이전에 압축되고, B=32를 넘기면 이미 위험 구간이다. 짧은 컨텍스트에서 배치 512가 안전했다면, 8192 토큰에서는 32 이상으로 올리면 preemption이 시작된다.

컨텍스트가 B*를 당기는 방식

위 표를 가로로 읽으면 명확하다. Llama-3 8B에서:

  • 512 → 2048 토큰: B*가 256 → 64 (4분의 1)
  • 512 → 8192 토큰: B*가 256 → 16 (16분의 1)

kv_per_seq = kv_per_token × seq_len이고 B = W / kv_per_seq이므로, 컨텍스트 길이가 4배 늘면 B도 정확히 4분의 1로 줄어든다. 이 반비례 관계는 모델 크기와 무관하게 성립한다.

Llama-3 70B + 8192 토큰에서 A100 4장 TP 구성을 보면 B* = 56, B_mem = (320 × 0.9 - 140) GB / 2.5 GB ≈ 59. B* ≈ B_mem이다. 배치를 60 이상으로 올리는 즉시 preemption이 시작된다. 짧은 컨텍스트(512 토큰)에서 B*(875)와 B_mem(925)이 비슷한 것도 같은 이유다 — 70B 모델에서는 메모리와 대역폭 한계가 동시에 작동한다.

반면 Llama-3 8B + 512 토큰에서는 B*(256)과 B_mem(900) 사이에 큰 여유가 있다. 이 여유 구간에서 배치를 900까지 올리면 throughput이 미미하게 개선되지만 tail latency 관리가 어려워진다. 나는 이 구간을 "기대 이하 수익, 위험 증가" 구간으로 부른다.

GQA·MQA가 B*를 어디까지 올리나

Llama-3 8B가 MHA를 썼다면 kv_per_token = 2 × 32 × 128 × 32 × 2 = 524 KB, B(512t) ≈ 16 GB / 262 MB ≈ 61이다. GQA(4그룹)가 B를 256까지 올려준 것이니 약 4.2× 효과다.

이 효과는 컨텍스트 길이에 비례해 유지된다 — GQA 비율만큼 kv_per_token이 줄고, B가 같은 배율로 올라간다. 다만 컨텍스트가 길어질수록 B_mem도 같이 줄어들어, 결국 B ≈ B_mem 구간에 들어가면 GQA의 B* 상향 효과를 운영 측면에서 온전히 누리기 어렵다. Llama-3 8B + 8192 토큰에서 GQA 덕에 B* = 16이지만 B_mem도 56으로 낮은 상태다. GQA가 없었다면 B* = 4로 더 낮았겠지만, 두 경우 모두 메모리가 binding constraint다.

운영: max_num_seqs를 어떻게 잡는가

B*와 B_mem을 계산하는 코드는 간단하다:

def bstar(model_bytes, num_kv_heads, head_dim, num_layers, seq_len, dtype_bytes=2):
    kv_per_seq = 2 * num_kv_heads * head_dim * num_layers * seq_len * dtype_bytes
    return int(model_bytes / kv_per_seq)

def b_mem(gpu_mem_gb, model_bytes, num_kv_heads, head_dim, num_layers, seq_len, util=0.9, dtype_bytes=2):
    kv_pool = gpu_mem_gb * 1e9 * util - model_bytes
    kv_per_seq = 2 * num_kv_heads * head_dim * num_layers * seq_len * dtype_bytes
    return int(kv_pool / kv_per_seq)

# Llama-3 8B, 512 토큰, A100 80GB 1장
bstar(16e9, 8, 128, 32, 512)           # → 256
b_mem(80, 16e9, 8, 128, 32, 512)       # → 900

# Llama-3 70B, 2048 토큰, A100 80GB 4장
bstar(140e9, 8, 128, 80, 2048)         # → 219
b_mem(320, 140e9, 8, 128, 80, 2048)   # → 231

B* ≤ B_mem이면 max_num_seqs = B*가 시작점이다. 실측 스윕으로 검증하려면:

for bs in 1 4 8 16 32 64 128 256; do
  python benchmarks/benchmark_serving.py \
    --model meta-llama/Meta-Llama-3-8B-Instruct \
    --max-num-seqs $bs \
    --input-len 512 --output-len 64 \
    --num-prompts 2000 \
    2>&1 | grep -E "mean_tpot_ms|output_throughput"
done

TPOT가 직전 스텝 대비 5% 미만으로 개선되기 시작하는 지점이 실질적인 B*다. 이 값을 max_num_seqs에 설정한다.

B* ≈ B_mem인 상황(긴 컨텍스트 + 대형 모델)에서는 B_mem의 80~90% 수준으로 보수적으로 잡는다. preemption 한 번이 수십 ms 지연을 만들기 때문에 tail latency 측면에서는 메모리 여유가 throughput 여유보다 중요하다.

vLLM V1의 max_num_seqs 기본값은 1024다. 짧은 컨텍스트의 소형 모델에서나 의미 있는 수치고, 70B + 8192 토큰 환경에서 이 기본값은 즉각적인 preemption 폭풍을 부른다. max_num_seqs는 모델과 컨텍스트 분포가 바뀔 때마다 다시 계산해야 하는 파라미터다.

Tags
LLMInference서빙GPU메모리KV 캐시vLLM아키텍처

관련 문서