Inference 비용 낮추기: KV cache, batching, 양자화
같은 모델이 같은 질문에 8.8초와 78.9초로 byte 단위 동일 출력. INT4도 세 방식으로 측정합니다.
이 페이지에서
같은 모델, 같은 머신, 같은 질문, 같은 48개 token으로 답합니다. 두 출력은 token 하나하나까지 동일합니다 — 가정한 것이 아니라 확인했습니다.
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)바뀐 인자는 하나뿐입니다: use_cache=False. 모델, prompt, sampling, 산술은 아무것도 달라지지 않았고, 두 번째 실행이 그 고생만큼 더 정확한 것도 아닙니다. 아무 이유 없이 아홉 배 느릴 뿐입니다.
이 장의 모양이 바로 이렇습니다. 여기의 모든 것 — cache, batch, 양자화된 가중치 — 은 답을 바꾸지 않는 작업에 비용을 내지 않거나, 더 싼 답이 무엇을 대가로 치르는지 알아내려는 시도입니다. 10장은 학습의 가격표를 세웠습니다. 여기는 영원히 비용을 내는 쪽의 가격표입니다. 배포된 모델은 남은 수명 동안 모든 요청에서 내보내는 token마다 대략 FLOPs를 씁니다.
두 번째 실행의 시간이 어디로 갔나
섹션 링크: 두 번째 실행의 시간이 어디로 갔나token을 생성하려면 decoder-only transformer는 지금까지의 전체 sequence를 받아 모든 layer를 통과시키고, 마지막 위치에서 확률 분포를 읽습니다. 그런 다음 선택된 token을 붙이고 다시 반복합니다. 이 설명은 맞고, 느린 실행이 하는 일이 바로 이것입니다.
하지만 엄청나게 낭비적이기도 합니다. 이유는 9장의 causal mask입니다. 위치 7의 key와 value 벡터는 위치 7의 입력과 그 앞 위치들로부터 계산됩니다. 위치 8이 들어와도 위치 7은 그것을 볼 수 없습니다 — causal하다는 뜻이 바로 그것입니다 — 그래서 위치 7의 key와 value는 이전과 정확히 같은 숫자입니다. 느린 실행은 그래도 매 step마다 그것들을 다시 계산합니다.
그러니 저장하면 됩니다. 그 저장소가 key-value cache, 언어 모델 serving에서 가장 결정적인 최적화입니다:
out = model(prompt_ids, use_cache=True) # prefill: the whole prompt
past = out.past_key_values
nxt = out.logits[:, -1].argmax(-1, keepdim=True)
for _ in range(n - 1):
out = model(nxt, past_key_values=past, use_cache=True)
past = out.past_key_values
nxt = out.logits[:, -1].argmax(-1, keepdim=True)loop 안에서 모델에 무엇이 들어가는지 보세요: nxt, token 하나입니다. sequence가 아닙니다. 새 token의 query는 모든 cached key에 attention하고, cached key들은 애초에 바뀔 일이 없었습니다. 이것은 근사가 아닙니다 — 위의 동일 출력 확인이 핵심입니다. cache는 품질을 속도와 맞바꾸지 않습니다. 중복 산술을 지워 버립니다.
스케일링을 깔끔하게 보려면 transformer를 걷어 내고 인 단일 attention head 하나를 두 방식으로 generation 한 step만 시간 측정합니다:
| context 안의 token | 전부 재계산 | cache 사용 | 비율 | score matrix |
|---|---|---|---|---|
| 128 | 0.59 ms | 0.062 ms | 10x | 65,536 B vs 512 B |
| 256 | 1.20 ms | 0.163 ms | 7x | 262,144 B vs 1,024 B |
| 512 | 7.03 ms | 0.078 ms | 90x | 1,048,576 B vs 2,048 B |
| 1024 | 17.31 ms | 0.114 ms | 152x | 4,194,304 B vs 4,096 B |
| 2048 | 59.83 ms | 0.214 ms | 279x | 16,777,216 B vs 8,192 B |
| 4096 | 236.18 ms | 0.284 ms | 832x | 67,108,864 B vs 16,384 B |
오른쪽 열이 원인입니다. 재계산은 매 step마다 전체 attention matrix를 만듭니다 — 9장의 점근 표기 상자에 있던 를 token마다 한 번씩 치르는 셈입니다. cache를 쓰면 대신 행 하나를 만듭니다. 4,096 token에서는 score가 67 MB 대 16 KB입니다.
밀리초 대신 multiply-accumulate를 세면 머신을 논의에서 제거할 수 있습니다. cold start에서 token을 생성하려면:
| 생성된 token | cache 사용 | 재계산 | 비율 |
|---|---|---|---|
| 128 | 2.6 M | 192.0 M | 73x |
| 512 | 23.1 M | 7.36 G | 318x |
| 2048 | 293.7 M | 392.6 G | 1,336x |
step마다 cached 버전은 context에 대해 선형이고 uncached 버전은 제곱입니다. generation 전체로 합치면 대 이고, 비율은 끝없이 커집니다. 첫머리의 아홉 배 차이는 48 token에서 측정한 것입니다 — 저 표의 첫 행보다도 짧습니다.
cache는 메모리에 무엇이 있어야 하는지도 바꿉니다. 8 GB 노트북 GPU에서 fp16으로 256 token을 생성하며 allocator의 peak에서 resident weights를 빼면:
| peak working memory | |
|---|---|
| cache 사용 | 21.8 MB |
| 재계산 | 181.7 MB |
8.3배 더 많은 메모리를 써서 같은 token을 더 느리게 만들어 냅니다. 이것은 5장에서 한 약속이 예상치 못한 방향에서 도착한 것입니다. 그곳에서는 reverse-mode autodiff가 backward pass를 위해 모든 intermediate를 살려 둬야 했고, activation이 학습 메모리를 지배했습니다. inference에는 backward pass가 없고 그것을 위해 보존할 것도 없습니다 — 그래서 대신 메모리를 지배하는 것은 cache이며, 이는 피할 수 없는 비용이 아니라 의도적인 선택입니다.
Prefill과 decode는 서로 다른 두 기계다
섹션 링크: Prefill과 decode는 서로 다른 두 기계다빠른 실행을 다시 보세요. 첫 token은 나머지 47개와 다르게 행동했습니다.
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepprompt 비용은 token당 25.6 ms였고, 생성된 각 token은 166 ms였습니다. 같은 모델, 같은 하드웨어, 같은 가중치인데 token당 여섯 배 차이입니다 — 그리고 대부분의 사람이 예상하지 않는 방향입니다. prompt가 싼 부분입니다. Generation은 실제 물리가 다른 두 phase로 갈라집니다:
Prefill
섹션 링크: Prefill전체 prompt에 대한 forward pass 한 번입니다. 모든 token이 병렬로 처리되므로 각 weight matrix는 메모리에서 한 번 로드되고 수백 개 token 벡터의 matrix와 곱해집니다 — matrix-matrix product이며, 이동한 byte당 산술이 많고, GPU가 바로 이런 일을 위해 만들어졌습니다. Prefill은 compute-bound이고, 비용은 prompt 길이에 대략 선형입니다.
Decode
섹션 링크: Decodetoken마다 forward pass 한 번, batch는 1이고 sequence도 1입니다. 모든 weight matrix는 여전히 메모리에서 통째로 로드되고, 단일 벡터와 곱해집니다 — matrix-vector product이며, 이동한 byte당 산술이 거의 없습니다. Decode는 memory-bandwidth-bound이고, token당 비용은 context 길이에 거의 의존하지 않습니다.
두 절반은 모두 측정할 수 있습니다. Prefill, token에 대한 pass 한 번:
| prompt token | 초 | token당 ms |
|---|---|---|
| 16 | 0.3515 | 21.97 |
| 32 | 0.5254 | 16.42 |
| 64 | 1.0491 | 16.39 |
| 128 | 1.6552 | 12.93 |
| 256 | 3.0965 | 12.10 |
Decode, cache에 대해 token 하나:
| cached token | token 하나당 ms |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.86 |
두 번째 표를 두 번 읽어 보세요. context가 16 token에서 1,024 token으로 — attention해야 할 history가 64배로 — 늘어났는데도 step 비용은 측정 가능한 만큼 변하지 않았습니다. cache에 대한 attention은 실제 작업이지만, 벡터 하나를 만들기 위해 5억 개 가중치를 memory bus로 끌고 오는 고정 비용에 압도됩니다. 그 고정 비용이 다음 섹션의 모든 이유입니다.
이 두 phase가 모든 serving 시스템이 보고하는 두 숫자의 기원입니다. Time to first token은 본질적으로 prefill이며 prompt와 함께 늘어납니다. 그래서 긴 대화는 시작이 느리게 느껴집니다. Tokens per second는 이고 대략 일정합니다. 그래서 답변은 그 뒤로 고르게 흘러갑니다. 느리게 시작해 부드럽게 streaming되는 chat은 렌더링 trick이 아닙니다. 바로 이 두 표입니다.
cache는 청구서이기도 하다
섹션 링크: cache는 청구서이기도 하다cache는 산술을 메모리와 맞바꾸며, 그것이 원하는 메모리는 작지 않습니다. context의 token마다, 모든 layer는 key-value head마다 key vector 하나와 value vector 하나를 보유합니다:
2는 key와 value 때문이고, 나머지는 architecture입니다. 이 장 전체에서 측정한 모델 — 24 layer, 14 query head, 2 key-value head, head dimension 64 — 에서는 fp16 기준 token당 byte입니다.
이 분야의 공식은 두 배씩 틀리는 버릇이 있으니, 믿기 전에 allocator와 대조해 봅니다:
KV cache tensors per layer: (1, 2, 295, 64) float16
measured: 3,624,960 bytes for 295 tokens = 12,288 bytes/token
formula : 2 * 24 * 2 * 64 * 2 = 12,288 bytes/token정확합니다. 시도한 모든 shape에서 계속 정확합니다:
| batch | context | 측정된 cache | 예측값 | peak working memory |
|---|---|---|---|---|
| 1 | 512 | 6.0 MB | 6.0 MB | 15.4 MB |
| 1 | 16,384 | 192.0 MB | 192.0 MB | 207.3 MB |
| 1 | 65,536 | 768.0 MB | 768.0 MB | 793.7 MB |
| 8 | 4,096 | 384.0 MB | 384.0 MB | 401.5 MB |
| 32 | 2,048 | 768.0 MB | 768.0 MB | 794.2 MB |
| 64 | 1,024 | 768.0 MB | 768.0 MB | 797.0 MB |
| 128 | 512 | 768.0 MB | 768.0 MB | 816.4 MB |
마지막 세 행은 다시 볼 만합니다. 2,048 token씩 가진 사용자 32명, 1,024 token씩 가진 64명, 512 token씩 가진 128명 — 세 경우 모두 cache는 768 MB입니다. 셋 모두 65,536 token을 보유하기 때문입니다. cache는 resident인 총 token 수에만 의존하고, 그것들이 사용자 사이에 어떻게 분포하는지에는 의존하지 않습니다. 이 사실이 batching 섹션의 토대입니다.
MQA와 GQA는 어디서 오는가
섹션 링크: MQA와 GQA는 어디서 오는가9장은 multi-query attention과 grouped-query attention을 소개하고 이유를 이 장으로 미뤘습니다. 이유는 바로 저 공식, 특히 그 안의 입니다.
표준 multi-head attention은 모든 query head에 자기 key와 value head를 줍니다. 여기의 모델에는 14개 query head가 있습니다. full multi-head attention이었다면 cache는 token당 byte였을 것입니다 — 12 KB가 아니라 84 KB, 정확히 일곱 배이며, query head와 key-value head의 비율입니다.
Multi-query attention1은 이것을 극한까지 밀어붙입니다. 모든 query head가 단일 key-value head를 공유합니다. Grouped-query attention2은 승리한 절충안입니다 — 몇 개의 key-value head가 있고, 각 head를 query head 그룹이 공유합니다 — MQA의 품질 손실은 실제였고 GQA의 손실은 그렇지 않았기 때문입니다. 둘 다 산술을 줄여 주지 않습니다. 이들은 저 공식을 정수로 나누기 위해 존재하며, 긴 context가 cache를 binding constraint로 만든 순간 업계 전반으로 퍼졌습니다.
그리고 그것은 빠르게 그렇게 됩니다. 32 layer와 dimension 128의 key-value head 8개를 가진 7B급 모델에서 cache는 fp16 기준 token당 128 KB입니다:
| context token | 사용자 1명 | 사용자 8명 | 사용자 64명 |
|---|---|---|---|
| 4,000 | 0.49 GB | 3.91 GB | 31.2 GB |
| 32,000 | 3.91 GB | 31.25 GB | 250.0 GB |
| 128,000 | 15.62 GB | 125.00 GB | 1,000.0 GB |
| 1,000,000 | 122.07 GB | 976.56 GB | 7,812.5 GB |
그 모델 자체의 가중치는 fp16에서 13.0 GB이며, 이 장 끝 표의 수치입니다. 그래서 128,000-token context에서는 사용자 한 명의 cache가 모델보다 큽니다. 이 산술을 16장이 돈으로 바꿉니다. 긴 대화가 단지 느린 대화가 아닌 이유도 이것입니다 — 요청이 살아 있는 동안 머신의 고정된 조각을 점유합니다.
Batching: 올라가는 숫자와 내려가는 숫자
섹션 링크: Batching: 올라가는 숫자와 내려가는 숫자Decode는 memory-bound입니다. 가중치를 bus로 끌고 와 token 하나를 만들고, 산술 유닛은 놀고 있습니다. 그러니 같은 step에 더 많은 일을 넣습니다. 여러 요청을 한 번에 실행하면 한 번 읽은 가중치가 모두에게 쓰입니다. 같은 모델에서 각 요청이 64-token cache를 들고 token 하나를 decode하도록 측정했습니다:
| batch | step당 latency | throughput | B=1 대비 latency |
|---|---|---|---|
| 1 | 0.1286 s | 7.78 tok/s | 1.00x |
| 2 | 0.1839 s | 10.88 tok/s | 1.43x |
| 4 | 0.1909 s | 20.95 tok/s | 1.49x |
| 8 | 0.2781 s | 28.76 tok/s | 2.16x |
| 16 | 0.3430 s | 46.64 tok/s | 2.67x |
| 32 | 0.6302 s | 50.78 tok/s | 4.90x |
오른쪽 두 열을 서로 맞대어 읽어 보세요. 그게 전부입니다. 요청 하나에서 열여섯 개로 가면 throughput은 6.0배가 되고, 개별 요청이 기다리는 시간은 2.67배가 됩니다. batch는 서버를 더 좋게 만들고 모든 사용자를 더 나쁘게 만들었습니다.
이것은 조정해서 없앨 bug가 아닙니다. 바로 그 trade-off 자체이며, 양쪽에 이름이 있습니다. Latency는 답을 기다리는 사람이 경험하는 것입니다. Throughput은 청구서를 나누는 값입니다. 둘 다 개선하는 설정은 없습니다.
멈추는 지점도 보세요. 16에서 32로 가면 throughput은 9% 늘지만 latency는 거의 두 배가 됩니다. step이 memory-bound를 벗어나 compute-bound가 되었고, 그 knee를 지나면 batch가 아무것도 사 주지 않습니다. 모든 deployment에는 그런 knee가 있습니다. 위치는 여러분의 시스템에서 측정해야 하지만, 그 존재는 그렇지 않습니다.
Static batching은 얻은 것의 대부분을 낭비한다
섹션 링크: Static batching은 얻은 것의 대부분을 낭비한다naive한 batching 방식은 개 요청을 모아 함께 실행하고, 모두 끝나면 반환하는 것입니다. 하지만 요청들은 함께 끝나지 않습니다. 어떤 답은 20 token이고 어떤 답은 500 token입니다. 고정 batch는 가장 긴 구성원이 끝날 때까지 실행되고, 이미 끝난 요청은 그때까지 slot을 계속 차지하며 padding에 기여합니다.
현실적인 output 길이의 편향을 가진 64개 요청을 봅시다 — median 18 token, longest 231, 총 1,874 — 그리고 8개 slot의 측정된 per-step 비용으로 두 policy를 simulation합니다:
| policy | wall clock | throughput | 요청당 평균 latency | 낭비된 slot-step |
|---|---|---|---|---|
| static batches of 8 | 176.9 s | 10.6 tok/s | 83.2 s | 3,214 |
| continuous, 8 slots | 109.0 s | 17.2 tok/s | 8.1 s | 0 |
Throughput은 1.6배 개선됩니다. 평균 latency는 열 배 넘게 좋아집니다. static batching에서는 네 step 만에 끝난 요청도 231-token 이웃이 끝날 때까지 누구에게도 들리지 않기 때문입니다.
Continuous batching3이 해결책이며, 말 그대로 단순합니다. batch는 그룹이 아니라 slot의 집합이고, slot이 비면 바로 다음 step에서 대기 중인 다음 요청을 받습니다. scheduler는 요청 하나가 아니라 token 하나의 granularity로 동작합니다. production의 모든 serving stack은 이제 이렇게 합니다.
두 번째 절반은 cache입니다. 들어오고 나가는 slot은 cache 메모리를 fragment시키고, 각 slot에 가능한 최대 context를 예약하면 예약분 대부분을 낭비합니다. PagedAttention4은 운영체제에서 답을 빌려옵니다. cache를 고정 크기 block에 저장하고 sequence마다 block table을 둬서, sequence의 cache가 물리적으로 흩어져 있어도 논리적으로는 연속되게 합니다 — 또한 shared prefix를 가진 두 sequence가 그것을 담은 block을 공유할 수 있게 합니다. vLLM이 그 위에 지어졌고, serving engine이 transformer가 붙은 memory allocator인 이유입니다.
양자화, 그리고 처음으로 잘못되는 것
섹션 링크: 양자화, 그리고 처음으로 잘못되는 것청구서의 다른 절반은 가중치 자체입니다. 5억 개 parameter가 각각 4 byte이면 1.98 GB이고, 2 byte이면 0.99 GB, 1 byte이면 0.49 GB입니다. weight당 bit가 적을수록 disk의 모델도 작아지고, 메모리의 모델도 작아지며, decode가 bandwidth-bound이기 때문에 각 step도 빨라집니다. 옮길 byte가 적기 때문입니다.
가장 단순한 방식은 symmetric absolute-maximum quantization이고, 세 줄에 들어갑니다:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantized가장 큰 weight가 가장 큰 integer에 대응되도록 scale을 고르고, 나누고, 반올림하고, integer와 scale을 저장합니다. 복원은 다시 곱하면 됩니다. 영리한 것은 하나도 없지만 작동합니다 — 작동하지 않을 때까지는요.
모델의 실제 가중치에서 측정했습니다. projection matrix 168개 전체, parameter 3억 5,780만 개, relative error :
| scheme | 평균 relative error | 최악의 matrix |
|---|---|---|
| INT8, 전체 matrix에 scale 하나 | 0.0400 | 0.1487 |
| INT8, output row마다 scale 하나 | 0.0100 | 0.0149 |
| INT4, 전체 matrix에 scale 하나 | 0.6026 | 0.9931 |
| INT4, output row마다 scale 하나 | 0.1790 | 0.2589 |
| INT4, 128개 그룹마다 scale 하나 | 0.1323 | 0.1992 |
| NF4, 64개 block마다 scale 하나 | 0.0952 | 0.1205 |
| INT3, 128개 그룹마다 scale 하나 | 0.3044 | 0.4123 |
| INT2, 128개 그룹마다 scale 하나 | 0.7790 | 0.8076 |
네 번째 행이 붕괴입니다. 최악의 matrix에서 relative error 0.99라는 것은 복원이 원본의 거의 아무것도 보존하지 못했다는 뜻입니다 — matrix가 대략 맞는 크기의 noise로 바뀐 것입니다. 원인은 단일 matrix에 대한 같은 실험에서 보입니다:
model.layers.12.mlp.down_proj.weight (896 x 4864)
mean |w| 0.01386 std 0.01822 max |w| 0.43945 max/std 24.1
weights beyond 6 sigma: 692 of 4,358,144 (0.016 %)6,000개 중 weight 하나가 6 standard deviation 너머에 있고, 가장 큰 것은 24 밖에 있습니다. 전체 matrix에 scale 하나를 쓰면 그 weight 하나가 430만 개 전체의 step size를 정합니다. 8 bit에서는 256 step이 있어서 typical weight도 의미 있는 step에 떨어집니다. 4 bit에서는 16개뿐이고, 가장 바깥쪽은 거의 아무것도 갖지 않는 값에 예약되어 있으며, ordinary weight — 사실상 전부 — 는 두세 개 level로 반올림됩니다.
그 행 이후의 모든 것은 다른 granularity에서 같은 수리입니다. scale의 영토를 줄입니다. output row별로 하면 error가 3.4로 나뉘고, 연속된 128개 weight 그룹별로 하면 다시 나뉩니다. 비용은 bookkeeping입니다 — 128개 그룹마다 16-bit scale 하나면 weight당 4 bit가 아니라 bit입니다 — 그리고 잃어버린 간격 대부분을 되사 옵니다.
NF4는 반대쪽에서 접근합니다.5 level이 등간격일 필요는 없습니다. block 안의 weight는 대략 normal distribution이므로, 16개 level을 normal distribution의 quantile로 고릅니다. weight가 실제로 있는 0 근처에는 촘촘하게, 없는 tail에는 드문드문 둡니다. 같은 4 bit, 같은 block scaling, 더 작은 block — group-128의 4.125에 비해 weight당 4.25 bit — 이고 측정 error는 0.1323에서 0.0952로, 28% 낮아집니다. 일부는 더 세밀한 block 때문이고 나머지는 level을 질량이 있는 곳에 둔 덕분이며, 둘을 분리하려면 세 번째 행이 필요합니다.
outlier feature
섹션 링크: outlier feature2장의 floating-point 상자는 약속으로 끝났습니다. 이 장이 가중치를 8 bit와 4 bit로 quantize하고, 눌러 담기 거부하는 몇몇 outlier feature를 찾을 것이라는 약속입니다. 여기 있습니다. 그리고 이것들이 왜 “숫자를 그냥 반올림”하는 방식이 activation에서는 통할 수 없었는지 설명합니다.
위의 가중치도 얌전하지 않았습니다. activation은 다른 리그에 있습니다. 평범한 84-token prompt를 사용해 각 layer의 residual stream을 캡처하고, 896개 dimension 각각이 도달한 최대 magnitude를 측정합니다:
| layer | 최대 |h| | median dimension의 최대 |h| | 비율 | median의 6배 초과 dimension |
|---|---|---|---|---|
| 1 | 6.19 | 0.339 | 18x | 2 |
| 4 | 1543.48 | 1.550 | 996x | 34 |
| 8 | 1571.63 | 1.498 | 1049x | 36 |
| 12 | 1575.03 | 1.546 | 1019x | 34 |
| 16 | 1579.60 | 1.617 | 977x | 32 |
| 20 | 1577.98 | 2.361 | 668x | 24 |
| 24 | 204.44 | 10.760 | 19x | 12 |
Dimension 62는 1,579.6에 도달하지만 median dimension은 1.6을 넘지 않습니다. token 하나나 layer 하나의 우연이 아닙니다. 같은 dimension이 layer 4에도 있고 layer 20에도 거의 같은 값으로 남아 있습니다. 이것이 outlier feature6이며, 체계적입니다 — 입력의 속성이 아니라 학습된 모델의 속성입니다.
layer 16에서 896개 per-dimension maximum의 histogram을 보면 모양이 분명합니다:
0 - 1 | ######################################## 254
1 - 2 | ######################################## 283
2 - 4 | ######################################## 226
4 - 8 | ######################################## 93
8 - 16 | ################## 18
16 - 32 | ######### 9
32 - 64 | ####### 7
64 - 128 | ##### 5
128 - 256 | 0
256 - 512 | 0
512 - 1024 | 0
1024 - 4096 | # 1900개 dimension이 8 아래에 가지런히 쌓여 있고, 세 octave 동안 아무것도 없다가, 맨 끝에 dimension 하나가 홀로 있습니다. 이제 그 tensor를 INT8로 quantize하고 무슨 일이 일어나는지 세어 봅니다:
| scheme | relative error | 전체 tensor에서 사용된 distinct integer level |
|---|---|---|
| 전체 tensor에 scale 하나 | 0.1083 | 256개 중 14개 |
| token마다 scale 하나(row별) | 0.0433 | 158 |
| 전체 tensor, outlier dimension 1개를 fp32로 유지 | 0.0442 | 48 |
| 전체 tensor, outlier dimension 4개를 fp32로 유지 | 0.0279 | 57 |
| 전체 tensor, outlier dimension 16개를 fp32로 유지 | 0.0085 | 102 |
256개 level 중 14개입니다. scale은 1,579.6으로 정해졌으므로 모든 step은 12.44 폭입니다. typical activation — median magnitude 0.26, ninety-ninth percentile 2.51 — 은 내려앉을 곳이 없습니다. dimension별로 보면 더 극명합니다:
single tensor-wide scale = 12.4378
dim 826 (max |h| = 4.77): 1 distinct level out of 256
dim 336 (max |h| = 1.62): 1 distinct level out of 256
dim 96 (max |h| = 0.69): 1 distinct level out of 256
after excluding the top 4 dimensions, scale = 0.5749 (22x smaller)
dim 826: 8 levels dim 336: 4 levels dim 96: 3 levelslevel 하나. 전체 dimension, 모든 token이 같은 숫자로 quantize되었습니다. 8 bit를 할당했는데 사실상 0 bit가 쓰였고, 그 activation을 읽는 모델은 상수를 건네받습니다.
이 측정이 사람들이 실제로 쓰는 모든 기법의 정당화입니다:
outlier는 밖에 둔다. LLM.int8()6은 matrix multiply를 분해합니다. 극단적 magnitude를 가진 dimension은 16 bit로 계산하고, 나머지는 INT8로 계산한 뒤 둘을 더합니다. 위 표가 영수증입니다 — dimension 4개를 제거하면 error가 거의 네 배 줄어듭니다. SmoothQuant7는 대신 어려움을 이동시킵니다. activation을 per-channel factor로 나누고 대응하는 weight column에 곱합니다. product는 변하지 않고, outlier는 그것을 흡수할 수 없는 tensor에서 흡수할 수 있는 tensor로 옮겨집니다.
그냥 반올림하지 말고, 반올림을 선택한다. 위의 어떤 것도 matrix가 무엇을 위한 것인지 묻지 않습니다. GPTQ8는 column별로 quantize하고, 매번 이미 저지른 error를 보상하도록 남은 full-precision column을 조정합니다 — weight 자체의 error가 아니라 실제 입력에서 layer output의 error를 최소화합니다. AWQ9는 소수의 weight channel이 나머지보다 훨씬 중요하다는 점에 주목하고, activation 통계에서 그것들을 찾아 quantizing 전에 키워 더 촘촘한 level에 떨어지게 합니다. 둘 다 calibration set이 필요하지만 gradient는 필요 없습니다.
세부 정보 보기
GGUF, 그리고 file format이 이 모든 것과 무슨 상관인가.
GGUF는 quantization method가 아닙니다. llama.cpp가 쓰는 container이며, gguf vs gptq 비교의 혼란은 둘을 같은 종류로 취급하는 데서 옵니다. GGUF는 tensor, tokenizer, architecture metadata, chat template을 memory-mappable file 하나에 담고, 그 안에 block scheme의 family를 싣습니다 — Q4_K_M 같은 이름은 weight당 bit, block size, 일부 tensor가 더 높은 precision으로 유지되는지 여부를 encode합니다.
중요한 engineering 차이는 이것입니다. GPTQ와 AWQ는 GPU kernel에 최적화된 가중치를 만들고, GGUF의 scheme은 파일을 로드하지 않고 map한 채 CPU에서 싸게 decode되도록 되어 있습니다. 그래서 같은 명목상의 “4-bit 7B model”이 두 세계에 서로 다른 크기와 품질로 존재합니다. 정직한 비교는 format이 아니라 아래의 측정이며, 여러분 자신의 task에서 실행해야 합니다.
quantization이 실제로 치르는 비용, 측정하기
섹션 링크: quantization이 실제로 치르는 비용, 측정하기quantization에 관한 거의 모든 글은 이전 섹션에서 멈춥니다. method를 설명하고, compression ratio를 인용하고, 품질이 “대체로 보존된다”고 주장합니다. 4장은 스스로를 속이지 않는 법에 관한 것이었으니, 직접 알아봅시다.
같은 모델의 가중치를 각 scheme으로 제자리에서 quantize한 뒤 세 가지를 측정했습니다. held-out English prose 2,048 token에서의 perplexity — 여기서는 이 강좌의 초안이며, 그래서 repository는 고정된 public-domain 책으로 대체하고 같은 모양의 표를 다른 숫자로 출력합니다 — greedy decoding 아래 known answer가 있는 짧은 factual question 16개 battery, 그리고 동일 context에서 quantized model이 full-precision model과 같은 token을 고르는 비율입니다.
| scheme | 평균 weight error | perplexity | question battery | fp32와 일치 |
|---|---|---|---|---|
| fp32 (reference) | 0.0000 | 23.08 | 13/16 | 100.0 % |
| INT8 per tensor | 0.0400 | 23.58 | 13/16 | — |
| INT8 per row | 0.0100 | 22.96 | 13/16 | 98.6 % |
| INT4 per tensor | 0.6026 | 365,416,000 | 0/16 | — |
| INT4 per row | 0.1790 | 46.18 | 6/16 | 58.3 % |
| INT4 group 128 | 0.1323 | 31.08 | 10/16 | 71.5 % |
| NF4 block 64 | 0.0952 | 24.55 | 11/16 | 84.7 % |
| INT3 group 128 | 0.3044 | 213.09 | 0/16 | 5.6 % |
| INT2 group 128 | 0.7790 | 26,325,436 | 0/16 | 0.0 % |
이 표에서 네 가지는 분명히 말할 가치가 있습니다.
제대로 한 INT8은 공짜다. per-row INT8은 reference의 23.08에 대해 22.96을 기록합니다 — 200분의 1 차이이며 noise로 보고 “동일”하다고 읽어야 합니다. noise가 어느 방향을 가리키는지는 안정적이지 않습니다. repository의 public-domain corpus에서는 같은 두 scheme이 22.18에 대해 22.24로 나옵니다. 절반 거리이고 방향도 반대입니다. 생성된 144개 token 중 142개에서 full-precision model과 일치합니다. fp32 reference 대비 메모리의 4분의 1, 실제로 deploy할 fp16 대비 절반, 감지 가능한 비용은 없습니다. INT8을 부주의하게 해도 거의 공짜입니다. matrix마다 scale 하나를 쓰면 perplexity 0.5 point와 battery 답변 0개를 잃을 뿐입니다. 8 bit는 granularity가 거의 중요하지 않을 만큼 관대합니다. 사람들이 INT8에서 INT4로 일반화했다가 다치는 이유가 정확히 이것입니다.
tensor당 scale 하나의 INT4는 모델을 파괴한다. Perplexity 3억 6,500만: 저하가 아니라 전멸입니다. 그다음부터는 granularity가 전부입니다 — per-tensor 365,416,000, per-row 46.18, per-group-of-128 31.08, NF4 24.55. weight당 같은 4 bit인데 최악과 최선 사이가 1,500만 배입니다.
Perplexity는 거친 도구이고 battery는 더 거칠다. NF4와 group-128 INT4 사이의 perplexity gap은 6.5 point이고 battery는 질문 하나 차이입니다 — 4장의 confidence interval에 따르면 16개 중 질문 하나는 아무것도 구분하지 못합니다. interval보다 더 날카로운 시연도 있습니다. 모델의 기본 repetition penalty를 끄고 같은 battery를 실행해 보세요. 그것이 greedy decoding이 실제로 뜻하는 바인데, 그러면 두 행의 순위가 바뀝니다. 16개 중 질문 하나는 작은 효과가 아니라 효과가 없음입니다. 8장의 경고도 적용됩니다. perplexity는 tokenizer를 공유하는 모델 사이에서만 비교 가능하므로, 다른 사람 글의 숫자를 여러분의 것과 비교할 수 없습니다.
agreement 열은 셋 중 가장 날카롭고, 거의 공짜입니다. full-precision model을 greedily 실행한 뒤, quantized model에게 매 위치에서 같은 prefix가 주어졌다면 무엇을 골랐을지 묻습니다. 16개가 아니라 144개의 독립 관측을 얻고, ground truth가 필요 없으며, battery가 계단식으로 나빠지는 곳에서도 부드럽게 degrade됩니다. 또한 다음 섹션에 필요한 양이 정확히 이것입니다.
이것은 1장이 이 장에 대해 한 약속이 제때 도착한 것입니다. 수학은 4-bit model이 가능하다고 말하고, engineering은 그것이 usable한지 결정합니다.
Speculative decoding
섹션 링크: Speculative decoding12장이 이것을 예고했고 청구서는 여기로 남겼습니다.
아이디어는 prefill/decode 분리에서 곧장 나옵니다. token의 제안된 sequence를 verify하는 비용은 위치에 대한 forward pass 한 번입니다 — matrix-matrix product이며, 하나에 대한 pass보다 겨우 더 비쌉니다. 그러므로:
Draft
섹션 링크: Draft작고 싼 모델이 개 candidate token을 autoregressively 생성합니다.
Verify
섹션 링크: Verify큰 모델이 개 candidate 전체에 대해 forward pass 한 번을 실행해, 각 위치에서 자신이 무엇을 말했을지 산출합니다.
Accept
섹션 링크: Accept둘이 일치하는 가장 긴 prefix를 유지하고, 첫 불일치 지점에서 큰 모델이 공짜로 제공하는 token을 하나 더합니다. 나머지는 버리고 다시 시작합니다.
output distribution은 변하지 않습니다. greedy decoding에서는 명백합니다 — target이 만들었을 token일 때만 token을 accept하기 때문입니다. sampling에서는 수정된 acceptance rule이 필요하고, Leviathan et al.은 그 결과 distribution이 정확히 target의 것임을 증명합니다.10 이것이 이 장의 두 번째 exact optimisation입니다.
따라서 모든 것은 acceptance rate 에 달려 있고, 이는 측정 가능합니다 — 위의 agreement 열이며, 그래서 거기서 계산했습니다. 각 quantized model을 full-precision target의 draft로 사용해 144개 생성 위치에서 측정하면:
| draft model | acceptance | longest accepted run | target pass당 기대 token, |
|---|---|---|---|
| fp32 (target itself) | 100.0 % | 48 | 5.00 |
| INT8 per row | 98.6 % | 48 | 4.86 |
| NF4 block 64 | 84.7 % | 20 | 3.69 |
| INT4 group 128 | 71.5 % | 13 | 2.85 |
| INT4 per row | 58.3 % | 7 | 2.24 |
| INT3 group 128 | 5.6 % | 2 | 1.06 |
| INT2 group 128 | 0.0 % | 0 | 1.00 |
verification pass당 accept되는 기대 token 수는 draft length 에서
이고, 순 speedup은 target 대비 token당 fraction 인 draft 자체의 비용으로 그것을 나눕니다:
| acceptance | , | , | , | , |
|---|---|---|---|---|
| 30 % | 1.19x | 1.02x | 0.79x | 0.79x |
| 50 % | 1.61x | 1.38x | 1.08x | 1.11x |
| 70 % | 2.31x | 1.98x | 1.54x | 1.78x |
| 90 % | 3.41x | 2.93x | 2.28x | 3.40x |
굵은 항목을 기억해야 합니다. speculative decoding은 generation을 더 느리게 만들 수 있습니다. acceptance가 30%이고 draft 비용이 target의 5분의 1이면, forward pass 다섯 번 값을 치르고 1.4 token을 유지합니다. 마지막 열은 또 다른 함정입니다 — 긴 draft는 acceptance가 높을 때만 도움이 됩니다. -token 추측의 tail에는 거의 도달하지 않기 때문입니다. acceptance 90%에서는 가 3.40x 가치가 있고, 30%에서는 0.79x 가치입니다. 같은 configuration이 traffic에서 측정한 숫자에 따라 이익이거나 손실입니다.
Distillation, 그리고 soft label이 담는 것
섹션 링크: Distillation, 그리고 soft label이 담는 것Quantization은 같은 function을 더 적은 bit에 저장해 모델을 줄입니다. Distillation은 작은 모델이 큰 모델을 모방하도록 학습해 줄입니다11 — deep learning보다 거의 10년 앞선 아이디어입니다.12
미묘한 부분은 student가 무엇으로부터 배우는가입니다. 정답이 아닙니다. 정답만으로도 직접 학습할 수 있었을 것입니다. teacher가 더하는 것은 전체 distribution입니다. phrase 뒤에 무엇이 오는지 모델에게 묻고 argmax 너머를 봅니다:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360hard label은 jug라고 말하고 그뿐입니다. soft label은 jug라고 말하고, 또한 cup도 거의 그만큼 좋았고, bowl도 그럴듯했으며, large — 형용사, 완전히 다른 문법적 continuation — 도 아직 살아 있다고 말합니다. 원래의 논거가 이것입니다. 이것은 7이지만 1과 꽤 많이 닮았다, 그리고 그 닮음은 hard label이 버리는 정보입니다.
그래서 distillation은 temperature를 씁니다. softmax 전에 logits를 로 나누면 distribution이 평평해지고 runner-up의 relative weight가 올라갑니다. 이 phrase에서는 top token과 세 번째 token의 비율이 에서 2.24였다가 에서 1.50으로 떨어집니다 — 첫 값의 제곱근이며, logits를 2로 나누면 비율에 일어나는 일이 바로 그것입니다. 같은 ordering, near miss에 더 많은 loss의 attention. student의 gradient는 teacher의 verdict만이 아니라 uncertainty도 담습니다.
8, 16, 24 GB에 무엇이 들어가는가
섹션 링크: 8, 16, 24 GB에 무엇이 들어가는가이 장의 모든 것은 이제 하나의 합입니다:
여기서 는 모든 concurrent request에 걸쳐 resident인 총 token입니다. 적용해 봅시다. 7B와 70B 행은 dimension 128의 key-value head 8개를 가정하고, 13B 행은 40 head의 full multi-head attention을 가정합니다. 그 세대의 모델이 그렇게 만들어졌고 — 차이가 드러납니다.
8 GB
| model | precision | weights | overhead 이후 여유 | 들어가는 context token |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | 들어가지 않음 | — |
| 7B | int8 | 6.5 GB | 들어가지 않음 | — |
| 7B | int4 (g128) | 3.4 GB | 3.1 GB | 25,710 |
| 13B | int4 (g128) | 6.2 GB | 0.3 GB | 337 |
| 70B | int4 (g128) | 33.6 GB | 들어가지 않음 | — |
16 GB
| model | precision | weights | overhead 이후 여유 | 들어가는 context token |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | 1.5 GB | 11,972 |
| 7B | int8 | 6.5 GB | 8.0 GB | 65,378 |
| 7B | int4 (g128) | 3.4 GB | 11.1 GB | 91,246 |
| 13B | int8 | 12.1 GB | 2.4 GB | 3,136 |
| 13B | int4 (g128) | 6.2 GB | 8.3 GB | 10,822 |
24 GB
| model | precision | weights | overhead 이후 여유 | 들어가는 context token |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | 9.5 GB | 77,508 |
| 7B | int8 | 6.5 GB | 16.0 GB | 130,914 |
| 7B | int4 (g128) | 3.4 GB | 19.1 GB | 156,782 |
| 13B | int8 | 12.1 GB | 10.4 GB | 13,622 |
| 13B | int4 (g128) | 6.2 GB | 16.3 GB | 21,308 |
| 70B | int4 (g128) | 33.6 GB | 들어가지 않음 | — |
8 GB 표의 13B 행을 보세요. 가중치는 들어갑니다 — 8 GB 중 6.2 GB — 그래서 흔히 말하는 방식으로는 13B 모델이 “8 GB card에서 돈다”고 합니다. 하지만 context는 337 token입니다. 대화가 아니라 겨우 prompt입니다. “들어가느냐”는 잘못된 질문입니다. 맞는 질문은 “얼마나 많은 context로, 동시에 몇 명에게”입니다.
16 GB int8 두 행도 보세요. 7B는 65,378 token을 얻고 13B는 3,136 token을 얻습니다 — 추가 weight 5.6 GB에서 생긴 20배 차이입니다. 여기의 13B는 multi-head attention이고 cache 비용이 token당 800 KB로, 7B의 128 KB와 다르기 때문입니다. 비슷한 크기의 두 모델 중 하나는 긴 context에 쓸 수 없고, 그 이유는 어떤 model card의 headline에도 나타나지 않습니다.
다음은 어디로 가는가
섹션 링크: 다음은 어디로 가는가열세 장 전에는 이것이 weight 두 개와 bias 하나를 가진 perceptron이었습니다. 이제는 설계되고, 학습되고, aligned되고, 어려운 질문에 compute를 쓰도록 배웠으며, token당 측정된 비용으로 served되는 transformer입니다 — 안에 닫힌 상자가 하나도 남아 있지 않습니다.
여기서 끝나며, 의도적으로 끝납니다.
14장은 모델이 다른 곳에 있는 상태에서 시작합니다. 여러분의 process 안도, memory 안도, print할 수 있는 variable 안도 아닙니다. 여러분이 관리하지 않는 머신, API key와 port와 bill 뒤에 있습니다. 여기서 측정한 모든 것은 여전히 일어나고 있습니다 — 첫 token 전에 prefill은 여전히 실행되고, cache는 대화와 함께 여전히 자라며, 여러분이 속한 batch는 여전히 누군가 다른 사람의 것이고 여전히 여러분의 latency를 결정합니다 — 하지만 이제부터는 Server-Sent Events stream, finish_reason, 그리고 Retry-After header가 붙은 HTTP 429를 통해 그것을 관찰합니다. vantage point가 바뀌면 질문도 바뀝니다. 이 gradient는 어떻게 계산되는가가 아니라 왜 invoice가 세 배가 되었는가입니다. 언어도 바뀌며, 14장은 그 규칙을 선언하지 않고 설명합니다 — 여기까지 code는 weights, gradients, logits, tokenizer byte를 들고 있었고, 그다음부터는 connection, retry, cancellation, accumulated state를 듭니다. 건너간다고 해서 뒤의 열세 장이 버려지는 것은 아닙니다. 그것들은 port 반대편에서 실행 중인 것의 설명입니다.
출처와 방법
섹션 링크: 출처와 방법두 가지 생략은 의도적입니다. FlashAttention (Dao et al., arXiv:2205.14135)은 다른 attention이 아닙니다 — score matrix가 메모리에 쓰이지 않도록 연산을 tiling해서 같은 function을 계산합니다. 그래서 이 장의 두 번째 표에 있는 67 MB는 실제로는 산술이 시사하는 것보다 작습니다. 그리고 kernel 자체는 위임했습니다. Stanford CS336의 lecture 10은 inference system을 여기서 시도하지 않는 깊이로 다루며, llama.cpp repository와 GGUF specification은 CPU 쪽의 primary source입니다.
-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). 이 논문은 대체로 memory-bandwidth 논증이며, 실제로 그렇게 읽힙니다. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). 기존 multi-head checkpoint를 변환하는 uptraining recipe가 포함되어 있고, 그래서 GQA가 그렇게 빠르게 퍼졌습니다. ↩
-
Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. and Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. iteration-level scheduling — continuous batching — 과 selective batching을 소개합니다. ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. vLLM의 기반 논문입니다. §3에는 운영체제 비유가 온전히 담겨 있습니다. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4는 §3에 정의되어 있습니다. 위 측정에서 쓴 16개 level 값은 이 논문이 유도한 것입니다. ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). §4의 outlier-feature 분석이 위에서 측정한 현상의 출처이며, outlier가 scale에서 체계적으로 나타난다는 발견도 포함합니다. ↩ ↩2
-
Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022). ↩
-
Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022). ↩
-
Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023). ↩
-
Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Theorem 1은 output distribution이 변하지 않는다는 증명입니다. Chen et al. (arXiv:2302.01318)도 같은 아이디어를 독립적으로 발표했습니다. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). temperature와 “dark knowledge” 논거입니다. ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. transformer가 아니라 ensemble을 대상으로 한, 9년 앞선 distillation입니다. ↩