디코드 한 스텝을 메모리 계층으로 분해하기
오늘의 질문
가속기의 최대 연산 성능은 세대마다 크게 오른다. 그런데 LLM이 답변을 한 token씩 뽑아내는 속도는 그만큼 오르지 않는다. 여기서 나오는 질문은 이것이다.
디코드 1 스텝에서 데이터가 실제로 어느 계층에서 어느 계층으로 얼마나 움직이는가? 그리고 그 움직임이 막히는 경계는 정확히 어디인가?
“메모리가 병목”이라는 말은 이제 흔하지만, 그 말만으로는 무엇을 개선해야 하는지 알 수 없다. 용량이 문제인지 대역폭이 문제인지, 어느 계층 경계에서 막히는지, 배치를 키우면 해소되는지 아닌지가 전부 다른 문제다. 이 글은 그걸 숫자로 분해한다.
먼저 답하기
세 문장으로 줄이면 이렇다.
배치가 작은 디코드에서 병목은 온칩 SRAM과 HBM 사이의 경계에 있다. 가중치를 읽어와 한 번 곱하고 버리기 때문에 연산 강도가 약 1 FLOP/B에 구조적으로 고정되고, 가속기가 요구하는 균형점(수백 FLOP/B)과 두 자릿수 이상 벌어진다. 그리고 이 격차를 배칭으로 메우려 하면, 이번에는 KV 캐시가 대역폭과 용량을 동시에 잡아먹으면서 새로운 벽이 나타난다.
마지막 문장이 이 글에서 가장 하고 싶은 이야기다. 배칭은 문제를 없애는 게 아니라 문제의 성격을 가중치 대역폭에서 KV 캐시 용량·대역폭으로 옮긴다. 아래에서 이 교환을 숫자로 확인한다.
주제에 맞는 핵심 설명
데이터 경로와 병목 경계
계층 구조가 존재하는 이유는 익숙한 트레이드오프 때문이다. 온칩 SRAM은 대역폭이 압도적으로 높지만 용량이 수십 MB 수준이라 수십~수백 GB짜리 가중치를 상주시킬 수 없다. HBM은 가중치를 담을 용량이 되지만 대역폭이 SRAM보다 한참 낮다. 그래서 가중치는 HBM에 눕고, 매 스텝 SRAM 쪽으로 건너온다.
여기서 캐시 계층이 제 역할을 못 한다는 점이 중요하다. 캐시는 시간적·공간적 지역성을 전제로 동작하는데, 배치가 1인 디코드에서 가중치 원소는 스텝당 정확히 한 번만 참조된다. 재사용이 없으면 캐시 히트도 없다. L2를 키워도 이 상황은 개선되지 않는다. 캐시가 무력한 워크로드라는 것이 이 문제의 본질에 가깝다.
왜 1 FLOP/B에 고정되는가
연산 강도(arithmetic intensity)를 계산해보면 이 고정이 어디서 오는지 바로 보인다.
가정 — 아래는 특정 제품 구성이 아니라 자릿수를 맞추기 위한 toy 설정이다.
| 항목 | 값 |
|---|---|
| 파라미터 | 700억 (70B) |
| 가중치 dtype | bf16 (2 B/param) |
| 레이어 수 | 80 |
| KV 헤드 구성 | GQA, kv_heads 8 × head_dim 128 → kv_dim 1024 |
| HBM 구성 | 스택 8개, 스택당 2 TB/s, 스택당 64 GB |
| 집계 대역폭 / 용량 | 16 TB/s / 512 GB |
| 가속기 dense bf16 피크 | 2000 TFLOPS (가정값) |
계산 — 배치 1, 짧은 컨텍스트 기준
| 항목 | 식 | 결과 |
|---|---|---|
| 가중치 총량 | 70e9 × 2 |
140 GB |
| 스텝당 가중치 트래픽 | 전량 1회 읽기 | 140 GB |
| 스텝당 FLOP | 2 × 70e9 |
140 GFLOP |
| 가중치 읽기 시간 | 140e9 ÷ 16e12 |
8.75 ms |
| 연산 시간 | 140e9 ÷ 2000e12 |
0.070 ms |
| 비율 | 8.75 ÷ 0.070 |
125배 |
| 연산 강도 | 140e9 FLOP ÷ 140e9 B |
1.00 FLOP/B |
| 머신 밸런스 | 2000e12 ÷ 16e12 |
125 FLOP/B |
| 대역폭 한계 처리율 | 1 ÷ 8.75 ms |
약 114 token/s |
1.00 FLOP/B라는 값은 우연이 아니다. bf16 원소 하나(2 B)를 읽어와 GEMV에서 곱셈 1회와 덧셈 1회, 즉 2 FLOP을 수행하고 버린다. 2 FLOP ÷ 2 B = 1 FLOP/B. dtype과 재사용 횟수가 정해지면 연산 강도가 결정되며, 커널을 아무리 잘 짜도 이 값은 움직이지 않는다. 루프라인 상에서 이 워크로드는 대역폭 경사면의 한참 왼쪽에 고정되어 있고, 피크 FLOPS를 두 배로 올리면 머신 밸런스가 250 FLOP/B가 되어 격차가 오히려 벌어진다.
적용 한계: 2000 TFLOPS는 가정값이며 특정 제품 사양이 아니다. 가중치를 정확히 1회만 읽는다고 가정했으므로 8.75 ms는 이상적 하한이다. 실제로는 커널 퓨전 정도, 텐서 병렬 시 통신, 소프트맥스·정규화 같은 비-GEMM 연산이 더해진다.
배칭의 이득과 그 이득을 갉아먹는 것
배칭은 가중치를 한 번 읽어 B개 요청이 공유하게 만든다. 가중치만 놓고 보면 연산 강도가 거의 B배로 오른다. 그런데 배치를 키우면 KV 캐시 트래픽이 B에 비례해 함께 늘어난다. 두 힘이 반대로 작용한다.
위 toy 설정에서 KV 캐시는 토큰당 2 × 80 layers × 1024 × 2 B = 320 KiB다. 이걸 넣고 다시 계산하면 이렇게 된다.
| 배치 | 컨텍스트 | KV 캐시 총량 | 스텝당 총 트래픽 | 연산 강도 | 512 GB에 수용? |
|---|---|---|---|---|---|
| 1 | 4K | 1.3 GB | 141.3 GB | 1.0 FLOP/B | 가능 |
| 32 | 4K | 42.9 GB | 182.9 GB | 24.5 FLOP/B | 가능 |
| 32 | 32K | 343.6 GB | 483.6 GB | 9.3 FLOP/B | 가능 (여유 없음) |
| 64 | 32K | 687.2 GB | 827.2 GB | 10.8 FLOP/B | 불가 |
| 32 | 128K | 1374.4 GB | 1514.4 GB | 3.0 FLOP/B | 불가 |
세 번째 줄이 핵심이다. 배치를 32로 키웠는데 연산 강도가 24.5에서 9.3으로 오히려 떨어졌다. 컨텍스트가 4K에서 32K로 늘면서 KV 트래픽이 343 GB가 되어 가중치 140 GB를 압도했기 때문이다. 배칭으로 얻은 가중치 재사용 이득을, KV 캐시가 재사용 없는 트래픽으로 되돌려놓는다.
그리고 네 번째 줄부터는 성능 이전에 용량에서 먼저 막힌다. 여기서 문제의 성격이 바뀐다는 것이 드러난다.
- 가중치는 고정비다. 모델을 정하면 크기가 정해지고 배치·컨텍스트와 무관하다. 순수한 대역폭 문제다.
- KV 캐시는 변동비다.
배치 × 컨텍스트 × 레이어 × kv_dim에 비례해 자라며, 대역폭과 용량을 동시에 요구한다.
그래서 서빙 파라미터를 조금만 바꿔도 병목의 종류가 달라진다. 짧은 컨텍스트·작은 배치에서는 가중치 대역폭 문제이고, 긴 컨텍스트·큰 배치에서는 KV 캐시 용량 문제로 옮겨간다. “메모리가 병목”이라는 한마디로 뭉뚱그리면 이 전환이 안 보인다.
HBM4가 바꾼 축과 바꾸지 못한 축
JEDEC이 2025년 4월 공개한 HBM4 표준(JESD270-4)의 주요 항목을 위 분석에 대응시켜 보면 무엇이 개선되고 무엇이 남는지가 정리된다.
JEDEC 발표에 따르면 HBM4는 2048비트 인터페이스에서 핀당 최대 8 Gb/s로 스택당 최대 2 TB/s 대역폭을 제공하고, 독립 채널 수를 HBM3의 16개에서 32개로 두 배로 늘리며 채널당 2개의 pseudo-channel을 둔다. 용량 측면에서는 32 Gb 다이를 16단으로 쌓아 스택당 최대 64 GB까지 지원한다. 또한 커맨드 버스와 데이터 버스를 분리해 동시성을 높이고 지연을 줄이도록 설계되었으며, row-hammer 대응을 위한 **DRFM(Directed Refresh Management)**과 RAS 기능이 강화되었다.
이 항목들을 앞의 표에 비춰보면 이렇게 읽힌다.
대역폭 축은 실제로 개선된다. 스택당 2 TB/s는 위 계산의 8.75 ms를 직접 줄이는 값이다. 다만 앞서 봤듯 연산 강도 1 FLOP/B는 그대로이므로, 워크로드는 여전히 대역폭 경사면 위에 있다. 즉 HBM4는 경사면 자체를 위로 올려주지만 워크로드를 경사면 밖으로 옮겨주지는 않는다.
채널 수 증가는 디코드처럼 접근 단위가 작고 산발적인 워크로드에 특히 의미가 있다. 32채널·64 pseudo-channel은 독립적으로 접근 가능한 경로가 늘어난다는 뜻이고, 이는 배치된 여러 시퀀스의 KV 캐시처럼 서로 무관한 영역을 동시에 읽어야 하는 패턴에서 뱅크 충돌과 버스 유휴를 줄이는 방향으로 작용한다. 커맨드/데이터 버스 분리도 같은 맥락에서 읽을 수 있다. 피크 대역폭 숫자보다 실효 대역폭에 더 직접적으로 기여하는 항목이라고 본다. 다만 이건 표준 문서의 설계 의도에서 끌어낸 해석이고, 실제 이득 폭은 컨트롤러 구현과 접근 패턴에 달려 있어 측정이 필요하다.
용량 축은 위 표의 4·5번째 줄이 그대로 대응된다. 스택당 64 GB × 8스택 = 512 GB라는 값이 배치 64·컨텍스트 32K에서 이미 부족했다. 파라미터 수가 2년에 410배로 늘어난 반면 단일 GPU 메모리는 2년에 2배 늘어나는 데 그쳤다는 AI and Memory Wall 논문(Gholami 외, 2024)의 지적이 여기에 겹친다. 같은 논문은 지난 20년간 서버 하드웨어 피크 FLOPS가 2년마다 3.0배 증가한 반면 DRAM 대역폭은 1.6배, 인터커넥트 대역폭은 1.4배에 그쳤고, 그 결과 연산이 아니라 메모리가 주된 병목이 되었으며 특히 디코더 모델과 서빙 상황에서 그렇다고 분석한다.
정리하면 HBM4는 대역폭과 용량 양쪽을 다 올렸지만, 워크로드 쪽 요구가 더 빠르게 늘고 있어서 격차의 방향은 바뀌지 않았다. 같은 논문이 모델 구조와 학습·배포 전략 전반의 재설계를 주장하는 이유도 여기에 있다.
메모리 쪽에서 손댈 수 있는 자리
앞의 분해를 그대로 뒤집으면 개선 축이 나온다. 스텝 시간은 대략 (가중치 바이트 + KV 바이트) ÷ 실효 대역폭이므로 분자를 줄이거나 분모를 키우는 수밖에 없다.
분자 쪽에서는 dtype이 가장 직접적이다. bf16을 FP8로 내리면 가중치 트래픽이 절반이 되고 연산 강도는 2 FLOP/B가 된다. KV 캐시는 가중치와 별개로 양자화 여부를 정할 수 있는데, 위 표에서 봤듯 긴 컨텍스트·큰 배치에서는 KV 쪽 절감이 가중치 절감보다 총량에 더 크게 기여한다. 어느 쪽을 먼저 줄일지는 서빙 파라미터에 따라 답이 달라진다.
분모 쪽에서는 세 갈래가 있다. 스택당 대역폭(세대 교체), 스택 수(패키징·인터포저 제약), 그리고 실효 대역폭(채널 구성, 접근 패턴, 컨트롤러 스케줄링)이다. 앞의 둘은 물리적 제약이 크고, 세 번째는 표준의 채널 확장이 여지를 넓혀준 영역이다.
여기에 계층을 하나 더 두는 방향도 있다. 위 다이어그램에서 CXL 계층을 “거의 안 읽히는 데이터”의 오프로딩 대상으로 표시한 이유가 이것이다. 데이터마다 바이트당 읽히는 빈도가 크게 다르다는 점이 출발점이다. 예를 들어 임베딩 테이블은 용량은 크지만 스텝당 실제로 읽는 양은 극히 일부인 반면, 가중치 행렬은 스텝마다 전량이 읽힌다. 접근 빈도가 낮고 용량만 큰 데이터를 대역폭이 낮은 계층으로 내리면 HBM의 귀한 용량을 KV 캐시에 더 쓸 수 있다. 다만 이 배치가 실제로 이득인지는 해당 데이터의 접근 지연이 임계 경로에 들어가는지에 달려 있고, 이건 워크로드별 측정 문제다.
연산을 메모리 쪽으로 내리는 접근(PIM/PNM)도 같은 식으로 읽을 수 있다. 연산 강도가 낮은 구간을 대역폭이 가장 넓은 지점에서 처리하면 위 경계를 건너는 바이트 자체가 줄어든다. 어떤 연산이 그 대상으로 적합한지는 위 표에서 연산 강도가 낮게 나오는 항목을 보면 후보가 좁혀진다.
자주 묻는 질문
대역폭과 용량 중 어느 쪽이 더 급한 문제인가요?
서빙 파라미터에 따라 답이 다르며, 위 표가 그 전환점을 보여준다. 짧은 컨텍스트·작은 배치에서는 가중치 트래픽이 지배적이라 대역폭 문제다. 컨텍스트가 길어지고 배치가 커지면 KV 캐시가 트래픽과 용량을 동시에 잡아먹으면서 용량이 먼저 한계에 닿는다. 실무에서 판단하려면 목표 SLA에 해당하는 배치·컨텍스트 조합을 먼저 고정하고, 그 지점에서 가중치 바이트 : KV 바이트 비율을 계산해보는 것이 가장 빠르다. 이 비율이 어느 쪽으로 기우는지가 곧 우선순위다.
피크 대역폭 숫자만 보고 성능을 예측할 수 있나요? 어렵다. 위 계산의 8.75 ms는 피크 대역폭을 100% 활용한다는 가정에서 나온 하한이며, 실측은 접근 패턴에 따라 그보다 낮게 나온다. 특히 디코드는 GEMV 중심이라 접근이 산발적이고, 배치된 여러 시퀀스의 KV 캐시는 서로 떨어진 영역에 흩어져 있다. HBM4가 채널 수를 늘리고 커맨드/데이터 버스를 분리한 것도 이 실효율 문제를 겨냥한 설계로 읽힌다. 그래서 세대 비교나 구성 비교를 할 때는 피크값이 아니라 해당 접근 패턴에서의 실효 대역폭을 측정하는 편이 실질적이다.
연산 강도를 근본적으로 올릴 방법은 없나요? 가중치 GEMV 자체의 연산 강도는 dtype과 재사용 횟수로 결정되므로 커널 최적화로는 움직이지 않는다. 올리는 방법은 재사용 횟수를 만드는 것뿐인데, 배칭이 대표적이고 speculative decoding처럼 한 번의 가중치 읽기로 여러 token 후보를 검증하는 방식도 같은 원리다. 다만 위 표에서 봤듯 배칭의 이득은 KV 캐시 증가에 상쇄되므로, 배치를 키우는 것과 KV 트래픽을 줄이는 것은 함께 가야 한다. 둘 중 하나만 하면 다른 쪽이 새로운 병목이 된다.
이 계산을 우리 환경 숫자로 다시 하려면 무엇을 알아야 하나요?
최소한 다섯 개다. 파라미터 수와 가중치 dtype, 레이어 수와 kv_dim(GQA 구성 포함), KV dtype, 집계 대역폭, 집계 용량이다. 이 값들만 있으면 위 두 표를 그대로 재현할 수 있다. 가중치 트래픽은 파라미터 수 × dtype 바이트, KV는 2 × 레이어 수 × kv_dim × dtype 바이트 × 컨텍스트 × 배치로 계산된다. 여기에 목표 처리율을 넣으면 필요한 대역폭이, 목표 배치·컨텍스트를 넣으면 필요한 용량이 나온다. 이 두 요구를 동시에 만족하는 구성이 있는지 확인하는 것이 서빙 구성 설계의 출발점이다.