A100 GPU를 놓고 LLM decode를 돌리면 SM(Streaming Multiprocessor) 활성률이 10~20% 근처를 맴돈다. 312 TFLOPS짜리 하드웨어가 연산 능력의 5%도 활용하지 않는 것처럼 보인다. 많은 엔지니어가 이 수치를 보고 "뭔가 최적화가 덜 됐다"고 판단하는데, 그 직관은 틀렸다. decode 단계에서 실제 한계는 연산 능력이 아니라 메모리 대역폭이기 때문이다.
이 구조적 이유를 이해하지 않고 최적화 기법을 고르면 효과 없는 기법에 시간을 쏟게 된다. Arithmetic Intensity와 Roofline Model이 여기서 필요한 이유다.
Arithmetic Intensity: 병목을 결정하는 하나의 비율
Arithmetic Intensity (AI)는 연산이 메모리에서 데이터를 1바이트 읽을 때마다 몇 번의 부동소수점 연산을 수행하는지를 나타내는 비율이다.
AI = FLOPs / bytes_moved
값이 크면 데이터를 적게 읽으면서 연산을 많이 하는 kernel이고 (compute-bound), 작으면 연산보다 메모리 읽기에 시간이 더 걸리는 kernel이다 (memory-bound).
Williams et al. (2009)이 제안한 Roofline Model은 이 두 한계를 하나의 그래프로 표현한다. X축이 AI (FLOPs/Byte), Y축이 달성 가능한 성능 (TFLOPS)이고, 두 한계선이 만나는 점이 Ridge Point다.
달성 가능한 성능 = min(AI × BW_peak, FLOPs_peak)
NVIDIA A100 80GB 기준으로:
- Peak BF16 Tensor Core: 312 TFLOPS
- HBM2e Memory Bandwidth: 2,039 GB/s (≈ 2 TB/s)
- Ridge Point: 312 TFLOPS ÷ 2 TB/s = 156 FLOPs/Byte
AI가 156 이상인 연산만 Tensor Core를 포화시킬 수 있다. 그 아래는 메모리 대역폭이 먼저 막힌다.
Prefill과 Decode를 같은 수식으로 계산하면
이 수식을 LLM의 두 단계에 직접 적용해 보자. Llama-3 8B, hidden size 4096의 단일 Linear layer (4096→4096, BF16)을 기준으로 한다.
Decode — batch size 1, 토큰 1개 생성
행렬×벡터 연산이다.
FLOPs = 2 × 1 × 4096 × 4096 ≈ 33.5 MFLOP
Bytes read = 4096 × 4096 × 2 ≈ 33.5 MB (weight matrix, BF16)
AI = 33.5M / 33.5M = 1 FLOPs/Byte
Ridge point 156에 비해 156분의 1이다.
Prefill — sequence length 512
입력 행렬이 (512, 4096)으로 커지면서 행렬×행렬 연산이 된다.
FLOPs = 2 × 512 × 4096 × 4096 ≈ 17.2 GFLOP
Bytes read = 4096 × 4096 × 2 ≈ 33.5 MB (weight matrix 동일)
AI = 17.2G / 33.5M ≈ 512 FLOPs/Byte
같은 weight matrix를 읽는데 AI가 512다. Ridge point 156을 훌쩍 넘어 compute-bound 영역이다.
prefill의 AI가 높은 이유는 단순하다. 512개 토큰이 weight matrix를 공유해서 읽기 때문이다. weight 33.5MB는 한 번만 읽히지만 그 위에서 512배의 연산이 이뤄진다. decode에서는 그 weight를 토큰 1개를 위해서만 읽으므로 재사용이 없다. 행렬 크기가 같고 읽는 데이터양이 같아도, 위에 얹히는 연산량이 배치에 비례하기 때문에 AI가 배치 크기만큼 선형으로 오른다.
Roofline 그래프에서 두 단계의 위치
이 두 점을 Roofline 그래프에 찍으면 차이가 명확하다.
- Decode (B=1): AI ≈ 1, ridge point 훨씬 왼쪽. 달성 가능한 성능 = 1 FLOPs/Byte × 2 TB/s = 2 TFLOPS. peak의 0.6%
- Prefill (S=512): AI ≈ 512, ridge point 오른쪽. 달성 가능한 성능 ≈ peak의 40~50%
MFU (Model FLOPs Utilization)로 보면 같은 모델, 같은 A100에서 prefill (S=512)의 MFU는 4050%인 반면 decode (B=1)의 MFU는 13%다.
여기서 MFU가 낮다고 비효율로 읽으면 안 된다. decode는 구조적으로 bandwidth-bound이기 때문에, 진짜 utilization 지표는 MBU (Memory Bandwidth Utilization)여야 한다. 실제로 decode 중 메모리 throughput을 재면 1.61.8 TB/s가 나온다. A100 이론 최대치(2 TB/s)의 8090%다. SM이 15% 활성인데 메모리는 85%로 포화되어 있다. 이 상태가 decode의 정상이고, "SM이 논다"는 관찰은 병목을 잘못 읽은 것이다.
배치 크기가 Arithmetic Intensity를 바꾸는 이유
batch size B로 decode를 돌리면 행렬×벡터가 행렬×행렬에 가까워지면서 AI가 B에 비례해 오른다.
| Batch size | AI (FLOPs/Byte) | 상태 |
|---|---|---|
| 1 | ~1 | memory-bound |
| 16 | ~16 | memory-bound |
| 64 | ~64 | memory-bound |
| 256 | ~156 | ridge point 근접 |
| 512 | ~312 | compute-bound 진입 |
B=256이 되어야 겨우 ridge point에 도달한다. 그전까지는 배치를 두 배로 늘리면 throughput이 거의 두 배가 된다. bandwidth가 병목이라 weight를 읽는 속도 자체가 처리 속도를 결정하기 때문이다.
Sarathi-Serve를 포함한 LLM 서빙 스케줄러들이 요청을 decode 배치에 끊임없이 끼워넣으려 하는 이유가 여기 있다. 실효 배치 크기를 높여서 AI를 끌어올리는 것이다. "더 많이 처리한다"가 아니라 hardware utilization의 물리적 한계를 바꾼다.
각 최적화 기법이 Roofline의 어느 축을 건드리는가
Roofline에서 병목을 개선하는 방법은 두 가지다. Ridge point 위치를 왼쪽으로 당기거나 (필요한 bytes를 줄이거나), 연산의 AI 자체를 오른쪽으로 밀거나 (배치 확대, 재사용 증가). 주요 최적화 기법이 어느 축을 건드리는지 정리하면 다음과 같다.
| 기법 | 건드리는 축 | 메커니즘 | 효과가 두드러지는 조건 |
|---|---|---|---|
| W4 양자화 | bandwidth 압박 감소 | weight bytes 절반 → AI 2배 | 소규모 배치 (B=1~16) |
| Continuous batching | AI 직접 상승 | B 증가 → AI 비례 상승 | 처리량 목표가 있을 때 |
| Speculative decoding | effective AI 상승 | N개 토큰 검증 → 1 forward에서 AI×N | draft 수락률 80% 이상일 때 |
| Tensor Parallel (N GPU) | bandwidth 분산 | 각 GPU 부담 1/N | latency SLO가 빡빡할 때 |
이 기법들은 서로 다른 축을 건드린다. decode B=1 단일 요청에서 W4 양자화는 AI를 1에서 2로 올린다. ridge point까지는 아직 78배가 남았다. 그 상황에서 Tensor Parallel을 4 GPU로 늘려봤자 각 GPU의 bandwidth 부담은 줄지만 AI 자체는 올라가지 않는다. latency는 줄어도 throughput은 크게 달라지지 않는다.
반대로 이미 B=256 이상에서 배치를 더 키우면 compute-bound로 진입해서 배치 확대의 이득이 포화된다. 그 시점에서는 양자화나 Tensor Parallel의 의미가 달라진다.
진단 없이 기법을 찍어 적용하면 이런 식으로 효과를 못 보거나, speculative decoding의 경우 draft overhead가 수락률 대비 커지면 latency가 오히려 늘어나기도 한다.
실측으로 확인하기: nvidia-smi dmon
실제로 decode 중에 어떤 수치가 나오는지 확인하는 가장 빠른 방법은 nvidia-smi dmon이다.
# 500ms 간격으로 SM utilization, memory controller busy ratio 모니터링
nvidia-smi dmon -s um -d 500
출력 예시 (decode 구간):
# gpu sm mem enc dec jpg ofa
0 15 88 0 0 0 0
0 14 91 0 0 0 0
0 16 87 0 0 0 0
sm이 SM utilization (%), mem이 memory controller busy ratio (%)다. mem이 88이면 memory bandwidth의 88%가 사용 중이라는 뜻이고, sm이 15이면 SM의 15%만 활성이다.
두 숫자를 동시에 보면 병목 위치가 바로 나온다. mem이 높고 sm이 낮으면 memory-bound, sm이 높고 mem이 낮으면 compute-bound다. decode 구간에서 전자가 나오는 게 정상이다.
Nsight Systems를 쓰면 kernel별로 memory throughput (GB/s)과 SM occupancy를 타임라인에서 분리해 볼 수 있어서, prefill kernel과 decode kernel의 특성 차이를 직접 비교하기 좋다. prefill kernel이 SM을 포화시키는 동안 decode kernel이 memory bandwidth를 포화시키는 모습이 타임라인에 나란히 찍힌다.
워크로드를 먼저 분류하라
최적화 기법을 고르기 전에 자신의 워크로드가 prefill-heavy인지 decode-heavy인지부터 파악해야 한다.
Prefill-heavy 워크로드는 입력이 수천 토큰이고 출력이 짧다 (문서 요약, 분류). compute-bound 구간이 길어서 Flash Attention, kernel fusion 같은 연산 최적화가 효과적이다.
Decode-heavy 워크로드는 입력이 짧고 출력이 길다 (코드 생성, 채팅). memory-bound 구간이 압도적으로 길다. 배치를 키우거나 (continuous batching), weight를 가볍게 하거나 (양자화), 한 forward에서 여러 토큰을 소화하는 방향 (speculative decoding)이 효과적이다.
배치를 늘릴 수 없는 상황 — 실시간 응답이 필요하고 요청이 sparse하게 들어오는 경우 — 이라면 W4 양자화가 가장 직접적이다. B=1 기준 AI를 1에서 2로 올리는 것만으로도 throughput이 거의 두 배가 된다. bandwidth를 절반만 써서 같은 연산을 마치니까.
B=64 이상을 안정적으로 유지할 수 있다면 speculative decoding을 먼저 검토하는 게 낫다. draft 모델의 수락률에 따라 effective throughput이 크게 달라지지만, 수락률이 80% 이상이면 latency와 throughput을 동시에 잡는 드문 기회다.