LLM 서빙 설계 플레이그라운드
모델을 학습시키는 것은 한 번이지만 서빙은 매일, 매초 일어난다. "Llama 3 70B를 하루 수백만 명에게 보여 주려면 GPU가 몇 장 필요하고, 응답은 얼마나 빠르며, 토큰 백만 개에 얼마가 드는가?" 이 질문에 답하는 데 필요한 것은 의외로 몇 개의 숫자뿐이다 — 메모리 용량, 메모리 대역폭, 연산 성능, 그리고 모델의 크기와 KV 캐시. 이 장에서는 그 숫자들을 손으로 계산하고, 시뮬레이터에서 직접 조립해 보면서 서빙 시스템을 설계하는 감각을 기른다.
- TTFT·TPOT·처리량의 의미와 서로 부딪히는 이유를 설명할 수 있다.
- 가중치·KV 캐시·작업 공간으로 GPU 메모리 예산을 세우고, 동시에 처리할 수 있는 요청 수를 계산한다.
- 루프라인 모델로 디코딩이 왜 메모리 대역폭 바운드이고 프리필은 왜 계산 바운드인지 산술 강도로 설명한다.
- 배치 크기에 따른 처리량–지연 트레이드오프와 연속 배칭(continuous batching)의 이점을 시뮬레이션으로 확인한다.
- 텐서·파이프라인·데이터·전문가 병렬의 역할과 NVLink, 파이프라인 버블을 이해한다.
- MoE 모델에서 메모리는 총 파라미터, 토큰당 계산·대역폭은 활성 파라미터로 따지는 이유와 전문가 병렬·all-to-all의 역할을 설명한다.
- 프리필/디코드 분리 서빙이 TTFT·TPOT 분포와 SLA 굿풋을 어떻게 바꾸는지 시뮬레이션으로 확인한다.
- $/1M 토큰 비용과 학습 연산량 \(C \approx 6ND\)를 직접 추정할 수 있다.
서빙의 요구: 지연과 처리량
8장에서 본 것처럼 LLM 응답은 두 단계로 만들어진다. 프롬프트 전체를 한 번에 통과시켜 KV 캐시를 채우는 프리필(prefill)과, 토큰을 하나씩 뽑아내는 디코드(decode)다. 사용자가 느끼는 속도는 이 두 단계에 각각 대응하는 두 숫자로 요약된다.
- TTFT(Time To First Token): 요청을 보낸 뒤 첫 토큰이 보일 때까지의 시간. 대기열에서 기다린 시간 + 프리필 시간이다. 채팅에서는 보통 수백 ms~1~2초 안을 목표로 한다.
- TPOT(Time Per Output Token, ITL이라고도 한다): 이어지는 토큰 사이의 간격. 사람이 읽는 속도(초당 5~10 단어)보다 빠르면 충분하다는 관점에서 보통 20~50 ms(사용자당 20~50 tok/s)를 목표로 잡는다.
- 처리량(throughput): GPU 한 대(또는 클러스터)가 초당 만들어 내는 전체 토큰 수. 사업자 입장의 숫자이며, 곧바로 비용이 된다.
지연과 처리량은 서로 부딪힌다. 요청 여러 개를 묶어(배치) 한 번에 처리하면 GPU가 바쁘게 일해 처리량이 오르지만, 한 스텝이 길어져 각 사용자의 TPOT은 늘어난다. 그래서 서빙 설계는 보통 SLA(Service Level Agreement) 또는 SLO 형태로 "TTFT p95 ≤ 1 s, TPOT p95 ≤ 50 ms"처럼 지연 상한을 먼저 정하고, 그 안에서 처리량(= 비용)을 최대화하는 문제로 정리된다. 이 장의 시뮬레이터는 모두 이 한 문장을 다른 각도에서 들여다본다.
하드웨어 숫자: GPU 표
서빙 설계에 필요한 GPU 숫자는 세 개다. 메모리 용량(가중치와 KV 캐시가 들어가는가), 메모리 대역폭(디코드 한 스텝에 가중치를 얼마나 빨리 읽는가), 연산 성능(프리필과 큰 배치를 얼마나 빨리 계산하는가). 여기에 여러 GPU를 묶을 때의 GPU 간 대역폭(NVLink)이 더해진다. 아래 표는 이 장의 시뮬레이터가 쓰는 값이다.
| GPU | 메모리 | 대역폭 | BF16 dense | FP8 dense | 리지 포인트 (BF16) | GPU 간 연결 |
|---|---|---|---|---|---|---|
| A100 SXM 80GB | 80 GB HBM2e | 약 2.0 TB/s | 312 TFLOPS | — (미지원) | ≈ 156 FLOP/B | NVLink 600 GB/s |
| H100 SXM | 80 GB HBM3 | 약 3.35 TB/s | 약 989 TFLOPS | 약 1,979 | ≈ 295 FLOP/B | NVLink 900 GB/s |
| H200 | 141 GB HBM3e | 약 4.8 TB/s | 약 989 | 약 1,979 | ≈ 206 FLOP/B | NVLink 900 GB/s |
| B200 (2024~25 발표치) | 약 180~192 GB HBM3e | 약 8 TB/s | 약 2,250 | 약 4,500 | ≈ 281 FLOP/B | NVLink 약 1.8 TB/s |
| B300 / GB300 (Blackwell Ultra, 2025 하반기) | 288 GB HBM3e | 약 8 TB/s | 약 2,500 | 약 5,000 | ≈ 313 FLOP/B | NVLink 약 1.8 TB/s |
| Rubin (Vera Rubin, 2026 하반기 공급) | 288 GB HBM4 | 약 19~22 TB/s | 약 4,000 | 약 17,500 | ≈ 208 FLOP/B | NVLink 6 약 3 TB/s |
| RTX 4090 | 24 GB GDDR6X | 약 1.0 TB/s | 약 165 (FP16 텐서) | 약 330 | ≈ 165 FLOP/B | PCIe 4.0 약 32 GB/s |
| Apple M2 Ultra | 최대 192 GB 통합 메모리 | 약 0.8 TB/s | 약 27 (GPU, 추정) | — | ≈ 34 FLOP/B | — (단일 칩) |
표의 FLOPS는 희소성(sparsity)을 쓰지 않은 dense 텐서 코어 최대치이고, 대역폭도 사양서 최대치다. 실제 커널은 대역폭의 70~90%, 연산의 40~70% 정도를 낸다. B200 수치는 2024~25년 발표 기준이며 제품 구성(공랭/수랭, HGX/GB200)에 따라 다르고, RTX 4090의 텐서 FLOPS는 FP16 누산 기준 약 165 TFLOPS(FP32 누산이면 그 절반)이다. Apple 통합 메모리는 macOS가 기본적으로 GPU에 전체의 약 75%만 할당한다. B300 수치는 NVIDIA의 GB300 NVL72 랙 사양(GPU 72개, 희소성 포함 표기)을 GPU 하나·dense로 환산한 값이다. Rubin은 NVIDIA 제품 페이지(2026) 기준 BF16 dense 약 4 PFLOPS, FP8 dense 약 17.5 PFLOPS이고, 대역폭은 초기 발표(2026 초)의 약 22 TB/s와 현재 제품 페이지의 19.2 TB/s가 함께 보여 "약 19~22"로 적었다(시뮬레이터는 19.2 TB/s 사용). 4비트 연산은 별도 칸으로 넣지 않았다: B300은 dense FP4 약 15 PFLOPS, Rubin은 NVFP4 추론 약 50 PFLOPS(NVIDIA 발표치, 희소성 기준 표기)다. FP4로 가중치까지 계산하는 경로(9장의 NVFP4/MXFP4)는 이 장의 단순 모델에 넣지 않았으므로, 4비트 가중치를 골라도 연산은 BF16(또는 FP8) 피크로 계산한다. 이런 불확실성 때문에 이 장의 모든 계산은 "자릿수가 맞는 추정"으로 읽어야 한다.
메모리 예산: 가중치 + KV 캐시 + 작업 공간
서빙 GPU 메모리는 세 덩어리로 나뉜다. 첫째는 가중치로, 파라미터 수 × 파라미터당 바이트다. Llama 3 70B(약 70.6B 파라미터)를 BF16(2바이트)으로 올리면 141 GB, FP8이면 71 GB, 4비트(그룹 스케일 포함 약 4.125비트)면 약 36 GB다.
둘째는 KV 캐시다. 8장에서 본 대로 각 층은 지나간 모든 토큰의 키와 값을 저장한다. GQA(Grouped-Query Attention)로 KV 헤드가 \(n_{kv}\)개라면 토큰 하나당 크기는 다음과 같다.
| 모델 | 파라미터 | 층 \(L\) | \(d_\text{model}\) | KV 헤드 × \(d_\text{head}\) | KV/토큰 (BF16) | 8K 컨텍스트 1요청 |
|---|---|---|---|---|---|---|
| Llama 3 8B | 8.03B | 32 | 4,096 | 8 × 128 | 128 KiB | 1.07 GB |
| Llama 3 70B | 70.6B | 80 | 8,192 | 8 × 128 | 320 KiB | 2.68 GB |
| Llama 3.1 405B | 405.9B | 126 | 16,384 | 8 × 128 | 504 KiB | 4.23 GB |
| Mixtral 8x7B (MoE) | 46.7B (활성 약 12.9B) | 32 | 4,096 | 8 × 128 | 128 KiB | 1.07 GB |
| gpt-oss-120b (MoE, 2025) | 116.8B (활성 5.1B) | 36 (풀 18 + 창 128 18) | 2,880 | 8 × 64 | 36 KiB (+ 요청당 고정 약 4.7 MB) | 0.31 GB |
| Qwen3.5-397B-A17B (MoE, 2026) | 397B (활성 17B) | 60 (풀 15 + 선형 45) | 4,096 | 2 × 256 | 30 KiB (+ 요청당 상태 약 192 MB) | 0.44 GB |
| DeepSeek-V4-Flash (MoE, 2026) | 284B (활성 13B) | 43 (CSA 21 + HCA 20 + 창 2) | 4,096 | 압축 KV | 약 3.4 KiB* | 약 0.03 GB |
| DeepSeek-V4-Pro (MoE, 2026) | 1.6T (활성 49B) | 61 (CSA 30 + HCA 31) | 7,168 | 압축 KV | 약 4.9 KiB* | 약 0.04 GB |
아래 네 줄은 구조가 Llama와 다르다(층 구성은 각 모델의 Hugging Face config.json 기준). gpt-oss-120b는 풀 어텐션과 윈도우 128 슬라이딩 어텐션을 한 층씩 번갈아 쓰므로, 길이에 비례해 자라는 KV는 풀 어텐션 18층분뿐이고 슬라이딩 층은 요청당 128토큰어치(약 4.7 MB)로 고정된다. Qwen3.5는 60층 중 45층이 Gated DeltaNet 선형 어텐션이라 KV 대신 고정 크기 상태(층당 64헤드 × 128 × 128)를 갖는다. 상태를 FP32로 저장한다고 가정하면(config의 mamba_ssm_dtype) 요청당 약 192 MB로, 짧은 요청에서는 오히려 KV보다 크지만 컨텍스트가 길어져도 늘지 않는다. 풀 어텐션 15층은 KV 헤드 2개 × 256차원이다. DeepSeek-V4는 KV를 시퀀스 방향으로 4배(CSA) 또는 128배(HCA) 압축하고 대부분을 FP8로 저장하는데, 층마다 구조가 달라 위 식 하나로 쓰기 어렵다. 그래서 *표의 값은 공개된 상대 수치 "8헤드 GQA(BF16) 대비 약 2%"(Hugging Face 블로그, 2026)를 같은 층 수의 8 × 128 GQA에 곱해서 얻었다. 슬라이딩 창 분기와 인덱서 키도 이 근사 안에 들어 있다고 본다. 시뮬레이터도 이 값을 쓰며, V4에서는 KV 정밀도 선택을 무시한다.
셋째는 작업 공간이다. CUDA 컨텍스트, cuBLAS·어텐션 커널의 임시 버퍼, 프리필 중간 활성값, 메모리 단편화를 합쳐 GPU당 수 GB가 든다. vLLM 같은 서빙 엔진은 기본으로 전체 메모리의 90%만 쓰고(gpu_memory_utilization=0.9), 그 안에서 가중치와 작업 공간을 뺀 나머지를 전부 KV 캐시 블록으로 미리 잡아 둔다(PagedAttention). 결국 동시에 처리할 수 있는 요청 수는 다음처럼 정해진다.
H100 한 장(80 GB)에 Llama 3 8B BF16을 올리면 가중치 16 GB, 작업 공간 2 GB를 빼고 약 54 GB가 KV 캐시 몫이다. 컨텍스트 8K 요청은 요청당 1.07 GB이므로 약 50개를 동시에 처리할 수 있다. 같은 GPU에 70B를 BF16으로는 아예 올릴 수 없다. 아래 시뮬레이터에서 막대 위쪽을 끌어 올리면 동시 요청 수가 늘어나며 KV 캐시가 차오른다.
GPU 메모리 적재
루프라인: 메모리 한계인가, 연산 한계인가
메모리에 올렸다면 다음 질문은 "얼마나 빠른가"다. 커널 하나의 실행 시간은 두 가지 중 느린 쪽으로 정해진다. 필요한 바이트를 메모리에서 읽어 오는 시간 \(Q/\beta\)와, 필요한 연산을 수행하는 시간 \(W/\pi\)다(\(\beta\): 대역폭, \(\pi\): 최대 FLOPS). 둘을 가르는 기준은 산술 강도(arithmetic intensity) — 읽어 온 바이트 하나당 몇 번 연산하는가 — 이다.
디코드와 프리필의 산술 강도
디코드 한 스텝을 보자. 파라미터 \(P\)개짜리 모델에서 토큰 하나는 대략 \(2P\) FLOPs(곱셈 + 덧셈)를 쓴다(6장). 배치에 토큰이 \(b\)개 있으면 연산은 \(2Pb\)지만, 가중치는 한 번만 읽어서 \(b\)개 토큰이 같이 쓴다. BF16이면 읽는 바이트는 \(2P\)다.
배치 1이면 \(I \approx 1\) FLOP/B로, H100의 리지 포인트 295와 비교하면 300배 가까이 부족하다. 즉 배치 1 디코딩은 거의 순수하게 "가중치를 읽는 속도"로 결정된다. 토큰당 걸리는 시간의 하한은 \(2P/\beta\)이고, Llama 3 8B BF16을 H100에서 돌리면 \(16\,\text{GB} / 3.35\,\text{TB/s} \approx 4.8\) ms, 즉 사용자 1명 기준 약 210 tok/s가 이론 상한이다. 반면 프리필은 프롬프트 \(p\)개 토큰을 한 번에 처리하므로 \(I \approx p\)가 되어, 프롬프트가 수백 토큰만 넘어도 계산 바운드 영역에 들어간다.
하나 더 중요한 점: KV 캐시 읽기는 배치로 나눠 쓸 수 없다. 어텐션 부분만 떼어 보면 토큰당 연산 \(4 L d\, c\)에 비해 읽는 KV는 \(c \cdot 2 L n_{kv} d_\text{head} \cdot 2\) 바이트라서, 산술 강도가 \(d/(n_{kv} d_\text{head})\) = Llama 3 8B 기준 약 4 FLOP/B로 배치와 무관하게 고정된다. 컨텍스트가 길수록 디코드는 배치를 키워도 계산 바운드로 넘어가지 못한다.
루프라인 플레이그라운드
배치: 처리량과 지연의 트레이드오프
루프라인을 시간으로 바꿔 쓰면 디코드 한 스텝(= TPOT)의 길이를 바로 계산할 수 있다. 배치 \(b\), 평균 컨텍스트 \(c\)일 때
배치가 작을 때는 가중치 읽기가 지배하므로 \(t_\text{step}\)이 거의 일정하다 — 배치를 2배로 늘려도 TPOT은 그대로이고 총 처리량만 2배가 된다. 사실상 공짜 점심이다. 배치가 커지면 두 가지 일이 생긴다. KV 캐시 읽기 \(b\,c\,M_\text{KV}\)가 가중치 읽기를 넘어서며 \(t_\text{step}\)이 \(b\)에 비례해 늘기 시작하고, 결국 연산 시간 항이 지붕에 닿는다. 그 지점부터는 배치를 키워도 총 처리량이 더 이상 늘지 않고 사용자당 속도만 떨어진다. 그리고 그 전에 메모리 용량(최대 동시 요청)이 한계를 긋는 경우가 많다.
배치 크기 스윕
연속 배칭: 빈자리를 즉시 채우기
배치가 처리량의 열쇠라면, 실제 서비스에서 문제는 요청이 제각각의 시각에 도착하고 출력 길이도 제각각이라는 점이다. 전통적인 정적 배칭(static batching)은 요청 몇 개를 묶어 시작한 뒤 가장 긴 요청이 끝날 때까지 배치를 유지한다. 10 토큰만에 끝난 요청의 자리는 500 토큰짜리 요청이 끝날 때까지 비어 있고, 그 사이 도착한 요청은 대기열에서 기다린다.
연속 배칭(continuous batching, iteration-level scheduling)은 스케줄링 단위를 "요청"에서 "디코드 한 스텝"으로 바꾼다(Orca, 2022). 매 스텝마다 끝난 요청을 빼고 대기 중인 요청을 빈 슬롯에 넣는다. 디코드 스텝 비용은 배치 크기에 크게 의존하지 않으므로(앞 절), 슬롯을 늘 채워 두는 것이 곧 처리량이다. vLLM, TensorRT-LLM, SGLang 등 현대 서빙 엔진은 모두 이 방식이며, 새 요청의 프리필을 디코드 스텝에 잘게 끼워 넣는 chunked prefill도 함께 쓴다.
아래 시뮬레이터는 실제로 요청을 생성해(포아송 도착, 시드 고정) 두 스케줄러를 같은 입력으로 돌린다. 시간 단위는 디코드 한 스텝이고, 한 스텝의 비용은 배치 크기와 무관하다고 가정했다(메모리 바운드 영역). 프리필 시간은 생략했다.
스케줄러 타임라인: 정적 vs 연속 배칭
병렬화: 한 GPU를 넘어서
모델이 GPU 하나에 안 들어가거나, 하나로는 지연 목표를 맞출 수 없으면 여러 GPU로 나눈다. 나누는 방법은 크게 네 가지다.
텐서 병렬(Tensor Parallelism)은 각 층의 가중치 행렬을 열/행 방향으로 쪼개 GPU마다 일부를 계산하게 한다(Megatron-LM 방식). 어텐션은 헤드 단위로, FFN은 중간 차원 단위로 나누면 층 하나에 all-reduce 두 번이면 된다. 장점은 가중치와 KV가 \(N\)등분되고 대역폭도 \(N\)배가 되어 디코드 TPOT이 실제로 짧아진다는 점이다. 대신 토큰 하나마다 층당 두 번씩, 80층이면 160번의 all-reduce가 필요하다. 각 all-reduce는 \(b \cdot d\) 원소로 작지만 횟수가 많아 지연이 쌓이므로, GPU 사이가 수백 GB/s~TB/s급 NVLink로 묶여 있지 않으면(PCIe는 약 32 GB/s) 이득이 금방 사라진다. KV 헤드가 8개인 모델은 TP 8까지가 자연스럽다.
파이프라인 병렬(Pipeline Parallelism)은 층을 몇 단계로 잘라 GPU마다 연속된 층 묶음을 맡긴다. 경계에서 활성값만 넘기면 되므로 통신은 적지만, 한 요청의 한 토큰은 단계를 순서대로 지나가므로 단일 요청의 지연은 줄지 않는다. 여러 마이크로배치를 흘려 넣어야 모든 단계가 동시에 바쁘고, 시작과 끝에 일부 단계가 노는 파이프라인 버블이 생긴다. GPipe 방식에서 버블 비율은 다음과 같다.
파이프라인 버블
데이터 병렬은 모델 전체를 복제해 요청을 나눠 받는다. 서빙에서는 가장 단순하고 확장성이 좋은 방법이다 — 모델이 한 묶음(예: H100 2장)에 들어가면, 그 묶음을 여러 벌 띄우고 로드 밸런서로 나누면 된다. 전문가 병렬은 MoE(6장)의 전문가들을 GPU마다 나눠 두고 라우터가 고른 전문가 쪽으로 토큰을 all-to-all로 보낸다. 전문가 수가 많은 대형 MoE(예: 수백 개 전문가)에서 필수적이다.
비용: $/1M 토큰
서빙 비용은 결국 "GPU 시간을 몇 개의 토큰으로 나눠 갖느냐"다. GPU \(N\)장을 시간당 \(c_\text{GPU}\) 달러에 빌렸고 총 처리량이 \(T\) tok/s라면
예를 들어 시간당 $2.5짜리 H100 한 장이 Llama 3 8B를 배치 1로 약 200 tok/s 낸다면 약 $3.5/1M 토큰이지만, 배치를 키워 2,500 tok/s를 내면 약 $0.28/1M 토큰으로 열 배 이상 싸진다. 같은 하드웨어, 같은 모델인데 단가를 정하는 것은 배치(= 처리량)다. 여기서 GPU 시간당 가격은 클라우드·약정·시기에 따라 크게 다르므로 아래 슬라이더의 값은 모두 사용자가 넣는 예시값이다. 또한 이 식은 출력 토큰만 셌다. 실제 API는 입력(프리필) 토큰을 출력보다 훨씬 싸게 받는데, 프리필은 계산 바운드라 토큰당 GPU 시간이 디코드보다 훨씬 적기 때문이다.
비용 계산기
종합 서빙 플레이그라운드
이제 지금까지의 식을 모두 묶어 하나의 서빙 구성을 평가해 보자. 아래 시뮬레이터가 쓰는 모델은 다음과 같다(모두 GPU 하나당 값으로 계산한 뒤 텐서 병렬 \(N\)을 반영한다). 교육용으로 단순화한 모델이며, 실제 엔진은 커널 효율, 스케줄링, 프리필 끼워 넣기, 투기적 디코딩 등으로 수십 % 이상 다를 수 있다.
- 적재: 가중치 \(M_w/N\) + KV + 작업 공간 2 GB가 GPU 메모리의 90% 안에 들어가야 한다. 실제 배치 \(b = \min(U, B_{\max})\). \(U > B_{\max}\)면 나머지는 대기열로 간다.
- 디코드 스텝: \(t_\text{mem} = (M_w^{\text{read}}(b) + b\,\bar c\,M_\text{KV}) / (N \beta \eta_m)\), \(t_\text{comp} = b(2P_\text{act} + 4Ld\bar c)/(N\pi\eta_c)\), \(t_\text{comm} = 2L\left(\frac{2(N-1)}{N}\frac{2bd}{\beta_\text{link}} + \tau\right)\), \(\text{TPOT} = \max(t_\text{mem}, t_\text{comp}) + t_\text{comm}\). 여기서 \(\bar c = p + o/2\)는 평균 컨텍스트, \(\eta_m = 0.8\), \(\eta_c = 0.6\)은 실효 효율, \(\tau\)는 all-reduce 한 번의 고정 지연(NVLink 5 µs, PCIe 20 µs로 가정)이다. MoE는 배치의 토큰들이 건드리는 전문가 비율 \(1-(1-k/E)^b\)만큼 전문가 가중치를 읽고, 연산은 활성 파라미터 \(P_\text{act}\)로 센다. 어텐션 연산 \(4Ld\bar c\)는 모델별로 바꿔 쓴다. 풀 어텐션 층만 컨텍스트에 비례하고, 슬라이딩 창 층은 128토큰, V4의 CSA·HCA 층은 압축된 길이(\(c/4\)의 top-k, \(c/128\))만 본다.
- 프리필: \(t_\text{pf} = \max\!\big(p(2P_\text{act} + 2Ldp)/(N\pi\eta_c),\ M_w/(N\beta\eta_m)\big) + t_\text{comm}(p)\). TTFT = 대기 + \(t_\text{pf}\) + 진행 중인 스텝 하나. 대기열이 있으면 슬롯이 비는 데 걸리는 시간 \(\lceil U/b - 1\rceil (t_\text{pf} + o \cdot \text{TPOT})\)을 더한다.
- 처리량·비용: 총 \(b/\text{TPOT}\) tok/s(출력 기준), 사용자당 \(1/\text{TPOT}\), 단가는 앞 절의 식.
종합 서빙 플레이그라운드
분리 서빙: 프리필과 디코드를 다른 GPU로
지금까지는 한 GPU(또는 TP 묶음)가 프리필과 디코드를 모두 했다. 이렇게 섞어 쓰는 방식을 콜로케이션(colocation)이라 한다. 문제는 두 단계의 성질이 정반대라는 점이다(루프라인 절). 프리필은 계산 바운드이고 한 번에 수천 토큰을 처리하며 수백 ms가 걸린다. 디코드는 대역폭 바운드이고 수십 ms마다 한 토큰씩 꾸준히 나와야 한다. 같은 GPU에서 4K 토큰짜리 프롬프트가 프리필되는 동안, 그 GPU에서 디코딩 중이던 수십 명의 다음 토큰은 함께 멈춘다. 그래서 TPOT의 꼬리(p99)가 튄다. 이것을 프리필–디코드 간섭이라 한다.
해결책은 두 가지다. 하나는 연속 배칭 절에서 말한 chunked prefill이다. 프롬프트를 수백 토큰 조각으로 잘라 디코드 스텝마다 조금씩 끼워 넣으면 멈춤은 사라진다. 대신 모든 디코드 스텝이 조금씩 길어지고, 긴 프롬프트의 TTFT도 늘어난다. 다른 하나는 분리 서빙(prefill/decode disaggregation)이다. GPU를 프리필 풀과 디코드 풀로 아예 나누고, 프리필이 끝난 요청의 KV 캐시를 네트워크로 디코드 GPU에 보낸다(DistServe·Splitwise, 2024). 2026년에는 NVIDIA Dynamo(+NIXL 전송 라이브러리), llm-d, vLLM, SGLang, TensorRT-LLM이 모두 이 구성을 지원한다. DeepSeek도 V3 기술 보고서(2024)부터 프리필과 디코드를 서로 다른 배치·병렬 구성으로 나눠 서빙한다고 밝혔다.
분리 서빙의 새 비용은 KV 전송이다. 프롬프트 \(p\) 토큰의 KV를 링크 대역폭 \(B_\text{link}\)로 보내는 시간은 다음과 같다.
평가 지표로는 굿풋(goodput)을 쓴다. 굿풋은 SLA(TTFT 상한과 TPOT 상한)를 둘 다 만족한 요청만 센 처리량이다. 아무리 토큰을 많이 뽑아도 TPOT 목표를 넘긴 요청은 사용자에게는 실패다. 아래 시뮬레이터는 같은 요청열(시드 고정)을 세 방식으로 실제로 스케줄링한다.
- 요청: 포아송 도착(도착률 λ). 프롬프트 길이는 중앙값 \(\tilde p\)의 로그정규 분포(σ = 0.8, 16~16K)이고, 출력 길이는 중앙값 \(\tilde o\)의 로그정규 분포(σ = 0.8, 2~4K)다. 요청은 400개다.
- GPU: 한 장이 모델 전체를 올린 인스턴스 하나다(TP 없음). 반복 하나의 시간은 종합 플레이그라운드와 같은 루프라인 식으로 계산한다. \(\max(\text{읽을 바이트}/0.8\beta,\ \text{FLOPs}/0.6\pi) + 0.3\,\text{ms}\)이고, KV 예산이 동시 요청 수를 제한한다(요청당 \(p+o\) 토큰을 미리 예약).
- 콜로케이션: 대기 중인 프롬프트가 있으면 프리필을 먼저 한다(한 반복에 최대 16K 토큰). 그동안 디코드는 멈춘다. Chunked: 매 반복 디코드 토큰 + 프리필 조각을 합쳐 512토큰 예산으로 처리한다.
- 분리: 앞쪽 \(P\)장은 프리필만 하고, 끝난 요청의 KV를 GPU마다 하나의 링크로 순서대로 보낸다(전송 대기 포함). 나머지 \(N-P\)장은 받은 요청을 연속 배칭으로 디코딩한다. 첫 토큰은 디코드 GPU가 요청을 넘겨받는 시점에 사용자에게 보인다고 본다. 그래서 TTFT에 전송 시간이 들어간다.
- 지표: TTFT = 첫 토큰 시각 − 도착, TPOT = (완료 − 첫 토큰)/(o − 1)이다(요청별 평균). SLA 달성률과 굿풋(달성 요청 수 / 전체 소요 시간)을 계산한다. 네트워크 경합, 프리픽스 캐시, 투기적 디코딩, 선점(preemption)은 넣지 않았다.
분리 서빙 vs 콜로케이션: TTFT·TPOT 분포와 굿풋
MoE 서빙과 KV 캐시 계층화
2026년 대형 오픈 모델은 대부분 MoE다. gpt-oss-120b(전문가 128개 중 4개), Qwen3.5-397B-A17B(512개 중 10개 + 공유 1개), DeepSeek-V4(Flash 256개·Pro 384개 중 6개 + 공유 1개)처럼 전문가 수는 수백 개이고 활성 비율은 1~5%다. 앞 절들에서 본 대로 메모리는 총 파라미터만큼, 연산은 활성 파라미터만큼 든다. 이 절에서는 MoE를 여러 GPU에서 서빙할 때 생기는 문제와, 에이전트처럼 긴 대화를 반복하는 워크로드에서 KV 캐시를 어디에 두는지를 다룬다.
전문가 병렬과 all-to-all
전문가 병렬(Expert Parallelism, EP)은 전문가들을 GPU에 나눠 둔다. EP = 64면 V4-Pro의 384개 전문가가 GPU마다 6개씩 들어간다. 어텐션은 보통 데이터 병렬(DP)로 각 GPU가 자기 요청을 처리한다. 그 뒤 MoE 층마다 두 번의 all-to-all 통신이 일어난다. 먼저 각 토큰의 은닉 벡터를 라우터가 고른 \(k\)개 전문가가 있는 GPU로 보내고(dispatch), 전문가 출력을 원래 GPU로 모은다(combine). 토큰 하나가 층 하나에서 보내는 양은 대략 다음과 같다.
큰 배치가 MoE에 특히 중요한 이유는 산술 강도에 있다. 디코드 스텝에서 배치 \(b\)의 토큰이 전문가 \(E\)개에 고르게 흩어지면, 전문가 하나가 받는 토큰은 평균 \(bk/E\)개뿐이다. 전문가 가중치는 한 번 읽어서 그 몇 개 토큰에만 쓰인다. 그래서 dense 모델에서 배치 \(b\)가 하던 역할(산술 강도 \(\approx b\))을 MoE의 전문가 GEMM에서는 \(bk/E\)가 한다(FP8 가중치면 \(I \approx 2bk/E\)). FP8 가중치로 H100의 리지 포인트(FP8 약 590 FLOP/B)에 닿으려면 전문가당 수백 토큰이 필요하다. V4-Pro라면 한 스텝에 \(b \approx 300 \times 384/6 \approx 2\)만 토큰이다. 그래서 대형 MoE의 디코드는 많은 GPU를 하나의 EP 그룹으로 묶어 전역 배치를 키운다. DeepSeek-V3 보고서(2024)는 디코드 최소 배치 단위를 40노드·320 GPU로 적었다. 대신 인기 있는 전문가에 토큰이 몰리면 그 GPU만 바빠지는 부하 불균형이 생긴다. 그래서 인기 전문가를 여러 GPU에 복제하는 중복 전문가(redundant experts)를 둔다.
MoE 라우팅: 배치, 전문가 적중, EP 부하
에이전트를 위한 KV 캐시: 프리픽스 캐싱과 계층화
11장의 에이전트는 같은 대화를 수십 번 이어 간다. 매 턴의 입력은 [시스템 프롬프트 + 도구 정의 + 지금까지의 대화와 도구 결과 + 새 메시지]다. 앞부분 수만 토큰은 지난 턴과 똑같다. 캐시가 없으면 매 턴 이 전체를 다시 프리필해야 해서 턴 수의 제곱으로 비용이 쌓인다. 프리픽스 캐싱(prefix caching)은 같은 접두사의 KV 블록을 해시로 찾아 재사용한다(vLLM의 automatic prefix caching, SGLang의 RadixAttention). 그러면 새로 붙은 토큰만 프리필하면 된다. 상용 API의 "프롬프트 캐싱" 할인도 같은 원리다.
문제는 HBM이 작다는 것이다. 수천 개의 대화 세션 KV를 모두 HBM에 둘 수는 없다. 그래서 KV를 계층화(KV tiering/offloading)한다. 지금 디코딩 중인 요청의 KV는 HBM에 두고, 잠시 쉬는 세션(도구 실행을 기다리는 에이전트 등)의 KV는 CPU DRAM으로, 더 오래된 것은 로컬 NVMe SSD나 원격 저장소로 내린다(LMCache, Dynamo의 KV 블록 매니저 등). 다시 쓸 때 불러오는 시간 \(c \cdot M_\text{KV}/B_\text{tier}\)이 재계산(프리필) 시간보다 짧으면 이득이다.
에이전트 턴별 TTFT: 재계산 vs 캐시 불러오기
학습 쪽 숫자: \(C \approx 6ND\)
서빙과 같은 방식으로 학습 규모도 추정할 수 있다. 파라미터 \(N\)개 모델을 토큰 \(D\)개로 학습하는 데 드는 연산량은 대략
MFU(Model FLOPs Utilization)는 이론 최대 FLOPS 중 모델 연산에 실제로 쓰인 비율이다. 통신, 파이프라인 버블, 메모리 바운드 연산, 장애 복구 때문에 대규모 학습의 MFU는 보통 30~50% 수준이다. 공개 보고 기준으로 Llama 3.1 405B는 약 15.6조 토큰으로 학습했고, \(6 \times 405\times10^9 \times 15.6\times10^{12} \approx 3.8\times10^{25}\) FLOPs, 최대 약 16K장의 H100에서 BF16 MFU 약 38~43%로 학습했다(Meta, 2024). 메모리 쪽도 계산해 보자. Adam 혼합 정밀도 학습은 파라미터당 약 16바이트(BF16 가중치·기울기 4 B + FP32 마스터 가중치·모멘트 2개 12 B)가 필요하므로 405B는 상태만 약 6.5 TB — 그래서 학습은 FSDP/ZeRO로 상태까지 GPU 수천 장에 쪼갠다.
MoE, 저정밀도, 새 옵티마이저
MoE 학습에서는 식의 \(N\)에 활성 파라미터를 넣는다: \(C \approx 6 N_\text{act} D\). 토큰마다 활성 전문가만 순전파·역전파하기 때문이다. DeepSeek-V3(671B 총/37B 활성, 14.8조 토큰)는 \(6 \times 37\times10^9 \times 14.8\times10^{12} \approx 3.3\times10^{24}\) FLOPs로 405B dense의 약 1/11이다. 보고된 학습량은 H800 약 279만 GPU·시간(2,048장, FP8 혼합 정밀도)이었다(DeepSeek, 2024). Kimi K2(1T 총/32B 활성, 15.5조 토큰)도 같은 자릿수다. 반면 메모리 쪽 상태(가중치·기울기·옵티마이저)는 총 파라미터에 비례한다. 671B × 16바이트 ≈ 10.7 TB이므로 MoE도 상태를 수천 장에 나눠야 한다. 서빙과 똑같은 "메모리는 총, 연산은 활성" 구조다.
2025~26년에 \(C/(\text{GPU 수}\cdot\pi\cdot\text{MFU})\)의 각 항을 줄이는 기법도 실전에 들어왔다. ① 더 낮은 정밀도: DeepSeek-V3의 FP8 학습에 이어, NVIDIA는 12B 모델을 10조 토큰 동안 NVFP4로 사전학습해 FP8에 가까운 손실을 보고했다(arXiv 2509.25149, 2025). 4비트 텐서 코어의 \(\pi\)를 학습에도 쓰려는 방향이다. ② Muon 옵티마이저: 2D 가중치의 모멘텀 업데이트를 Newton–Schulz 반복으로 직교화해 적용한다. Kimi는 같은 손실에 AdamW 대비 약 2배의 계산 효율을 보고했다('Muon is Scalable', 2025). K2는 이를 개량한 MuonClip으로 손실 스파이크 없이 학습했다. 같은 \(C\)로 더 낮은 손실을 얻는 셈이고, 모멘트를 하나만 들고 있어 옵티마이저 상태 메모리도 Adam보다 작다. ③ RL 사후학습: 추론 모델(11장)의 GRPO류 강화학습은 프롬프트마다 응답 여러 개를 생성(롤아웃)한 뒤 학습한다. 그래서 비용의 상당 부분이 이 장의 디코드 처리량 문제가 되고, 학습 클러스터 안에서도 서빙 엔진(vLLM, SGLang 등)이 롤아웃을 맡는다.
학습 연산량·기간 추정
핵심 정리
- 서빙 품질은 TTFT(대기 + 프리필), TPOT(디코드 한 스텝), 처리량(비용)으로 요약되며, 지연 SLA 아래에서 처리량을 최대화하는 문제로 정리된다.
- GPU 메모리 = 가중치(파라미터 × 바이트) + KV 캐시(\(2Ln_{kv}d_\text{head}b_{kv}\) × 토큰 × 요청) + 작업 공간. KV 예산이 최대 동시 요청 수를 정한다.
- 디코드의 산술 강도는 BF16 기준 약 \(b\) FLOP/B로 리지 포인트(H100 약 295)보다 훨씬 작아 메모리 대역폭 바운드다. 배치 1의 상한은 \(\beta / M_w\) tok/s. 프리필은 \(I \approx p\)로 계산 바운드다.
- 배치를 키우면 TPOT은 거의 그대로인 채 처리량이 늘다가, KV 읽기와 연산이 지배하면서 꺾인다. KV 읽기는 배치로 공유되지 않아 긴 컨텍스트일수록 일찍 꺾인다.
- 연속 배칭은 매 디코드 스텝마다 빈 슬롯을 채워, 출력 길이가 제각각인 실제 트래픽에서 슬롯 활용률과 대기 시간을 크게 개선한다.
- TP는 가중치·KV·대역폭을 N등분/N배 하지만 층마다 all-reduce가 필요해 NVLink가 필수이고, PP는 통신이 적은 대신 버블 \((p-1)/(m+p-1)\)이 생기며, DP는 복제로 처리량을 늘린다.
- $/1M 토큰 = GPU 비용 / (처리량 × 3600) × 10⁶. 같은 하드웨어에서 단가는 배치와 가동률이 정한다. 학습 연산량은 \(C \approx 6ND\)(MoE는 \(N\) = 활성 파라미터), 기간은 \(C/(\text{GPU 수} \cdot \pi \cdot \text{MFU})\).
- MoE는 메모리를 총 파라미터로, 토큰당 연산을 활성 파라미터로 센다. 한 스텝에 읽는 전문가 비율 \(1-(1-k/E)^b\)는 배치와 함께 1로 다가간다. 전문가당 토큰 \(bk/E\)가 산술 강도를 정하므로 대형 MoE는 큰 EP 그룹과 큰 전역 배치로 서빙하고, MoE 층마다 dispatch/combine all-to-all이 필요하다.
- 분리 서빙은 프리필 풀과 디코드 풀을 나눠 프리필–디코드 간섭을 없앤다. 대가는 KV 전송 \(p\,M_\text{KV}/B_\text{link}\)와 P:D 비율 조정이며, 평가는 SLA를 모두 지킨 요청만 세는 굿풋으로 한다. 에이전트처럼 접두사를 반복하는 워크로드에서는 프리픽스 캐싱과 KV 계층화(HBM → DRAM → SSD)가 프리필을 새 토큰분으로 줄인다.
확인 퀴즈
1. H100 SXM(약 3.35 TB/s) 한 장에서 Llama 3 8B BF16을 배치 1로 디코딩할 때, 이론적 최대 속도에 가장 가까운 것은?
2. KV 캐시를 무시하면 BF16 디코딩의 산술 강도는 배치 \(b\)에 대해 약 \(b\) FLOP/B다. H100에서 디코딩이 계산 바운드가 되기 시작하는 배치는 대략?
3. Llama 3 70B(80층, KV 헤드 8, 헤드 차원 128)의 KV 캐시를 BF16으로 저장할 때, 8,192 토큰 요청 하나의 KV 크기는?
4. 연속 배칭이 정적 배칭보다 처리량이 좋은 가장 근본적인 이유는?
5. 시간당 $2.5인 GPU 한 장이 총 2,500 tok/s를 생성한다. 100% 가동 시 1M 출력 토큰당 비용은?
6. 70B 파라미터 모델을 15조(1.5×10¹³) 토큰으로 학습하는 연산량은 \(C \approx 6ND\)로 약?
7. gpt-oss-120b(총 116.8B, 활성 5.1B, MoE 가중치 MXFP4·나머지 BF16)를 배치 1로 디코딩한다. GPU에 올려야 하는 가중치와 토큰 하나에 읽는 가중치에 가장 가까운 것은?
8. 분리 서빙에서 Llama 3 70B의 8K 토큰 프롬프트 KV(BF16, 약 2.7 GB)를 400 Gb/s RDMA(약 50 GB/s)로 디코드 GPU에 보내는 시간과, 그 영향으로 옳은 것은?