Skip to content
Published on

로컬에서 LLM 돌리려면 VRAM이 얼마나 필요한가 — 표 말고 공식으로 계산하기

공유하기
Authors

들어가며 — 표를 찾지 말고 공식을 외우세요

"8B 모델 돌리려면 VRAM 몇 GB 필요해요?" 이 질문에 대한 인터넷의 표준 답변은 어딘가에서 복사된 표입니다. 그 표는 대개 출처가 없고, 컨텍스트 길이를 명시하지 않고, 어느 양자화인지 흐릿하고, 무엇보다 당신의 경우에는 맞지 않습니다. 모델도 컨텍스트도 양자화도 당신이 고르는데, 표는 누군가 한 번 고른 조합의 스냅샷일 뿐이니까요.

그래서 답부터 드립니다. VRAM은 크게 두 덩어리이고, 각각 공식이 있습니다.

(1) 가중치 = 파라미터 수 × bpw / 8         [바이트]
       bpw = bits per weight (양자화 레벨의 "실제" 비트 수)

(2) KV 캐시 = 2 × 레이어 수 × KV헤드 수 × head_dim × 바이트/원소 × 토큰 수
       2 = K와 V, 두 벌
       바이트/원소 = f16이면 2, q8_0이면 1

(1)은 상수입니다. 모델과 양자화를 고르는 순간 고정됩니다. (2)는 컨텍스트에 정비례해서 자랍니다. 그리고 대부분의 사람이 로컬 LLM에서 벽에 부딪히는 이유는 (1)이 아니라 (2)입니다. 미리 결론을 말하면 — Llama-3.1-8B를 Q4_K_M으로 양자화하면 가중치는 4.58 GiB인데, 이 모델이 지원하는 128K 컨텍스트를 f16 KV 캐시로 다 쓰면 캐시만 정확히 16 GiB입니다. 가중치의 3.5배입니다.

이 글은 이 두 공식을 세우고, llama.cpp의 소스 코드와 공식 발표 표에 직접 대조해서 검증합니다. 그다음 정직한 부분 — 비트를 줄이면 품질을 얼마나 잃는지(측정된 값으로만), 그리고 더 줄이면 정말 더 빨라지는지(아닙니다) — 를 다룹니다.

관련해서 브라우저 안에서 돌리는 경우의 제약은 llama.cpp WebGPU 백엔드 편에서 따로 다뤘고(거기서는 병목이 VRAM이 아니라 탭 메모리입니다), 양자화 기법 자체의 지형도 — GPTQ·AWQ·FP8·MXFP4가 각각 무엇인지 — 는 LLM 양자화 총정리 편에 있습니다. 이 글은 오직 메모리 산수 한 가지에 집중합니다.

VRAM은 무엇으로 채워지나 — 세 덩어리

먼저 지형입니다. GPU 메모리에 들어가는 것은 셋입니다.

  1. 가중치. 모델 파일 그 자체. 컨텍스트와 무관하게 고정.
  2. KV 캐시. 이미 처리한 토큰마다 어텐션의 K와 V를 저장해 둔 것. 토큰 수에 정비례.
  3. 나머지. 연산용 임시 버퍼(compute buffer), CUDA 컨텍스트, 그리고 데스크톱을 같은 카드에 물렸다면 화면 출력분까지.

3번은 공식으로 못 씁니다. 백엔드·배치 크기·플래시 어텐션 여부에 따라 달라지니까요. 여기서 숫자를 지어내지 않겠습니다 — 대신 llama.cpp가 모델을 로드할 때 직접 인쇄해 줍니다. 로그에서 KV self sizecompute buffer size 줄을 읽으시면 됩니다. 이 글은 (1)과 (2), 즉 하한선을 정확히 계산하는 데 집중합니다. 하한선을 알아야 3번이 얼마나 붙었는지도 판단할 수 있습니다.

가중치 계산 — "4비트"는 4비트가 아니다

공식 (1)은 쉬워 보이지만, 함정이 bpw에 숨어 있습니다. "4비트 양자화"의 bpw는 4가 아닙니다.

이유는 양자화의 작동 방식에 있습니다. 가중치를 4비트 정수로 바꾸려면 "이 정수들을 실제 값으로 되돌리는 배율(scale)"을 같이 저장해야 합니다. 그 배율은 4비트가 아닙니다. 그래서 실제 비트 수는 항상 명목값보다 큽니다.

얼마나 큰지는 추정할 필요가 없습니다. ggml의 블록 구조체에 그대로 적혀 있습니다. ggml/src/ggml-common.h에서 가장 단순한 Q8_0을 보면 이렇습니다.

#define QK8_0 32
typedef struct {
    ggml_half d;       // delta
    int8_t  qs[QK8_0]; // quants
} block_q8_0;

읽는 법은 이렇습니다. 가중치 32개(QK8_0)가 한 블록이고, 블록마다 int8 32개(32바이트)와 ggml_half 배율 1개(2바이트)가 들어갑니다. 그러면,

Q8_0 = (32 × 8 + 16) 비트 / 32 가중치
     = 272 / 32
     = 8.5 bits per weight

명목은 8비트인데 실제는 8.5비트입니다. 32개마다 fp16 배율 하나가 붙어서 정확히 0.5비트가 추가된 겁니다.

이제 k-quant를 봅시다. Q4_K는 256개(QK_K)를 하나의 슈퍼블록으로 묶습니다.

#define QK_K 256
#define K_SCALE_SIZE 12
typedef struct {
    ggml_half d;                  // 슈퍼블록 배율                      2바이트
    ggml_half dmin;               // 최솟값용 슈퍼블록 배율             2바이트
    uint8_t scales[K_SCALE_SIZE]; // 6비트로 양자화된 배율과 최솟값     12바이트
    uint8_t qs[QK_K/2];           // 4비트 양자값                     128바이트
} block_q4_K;

합이 144바이트, 가중치 256개니까 144 × 8 / 256 = 4.5 bpw입니다. 그리고 이건 제 계산이 아니라 소스의 주석이 직접 말해 줍니다 — block_q4_K 바로 위에 // Effectively 4.5 bits per weight가 적혀 있습니다. 같은 파일에 다섯 개가 있습니다.

ggml-common.h 주석이 직접 밝힌 "실효" bpw
  Q2_K   2.625      Q3_K   3.4375     Q4_K   4.5
  Q5_K   5.5        Q6_K   6.5625
  (Q8_0은 주석은 없지만 구조체에서 8.5가 그대로 나옵니다)

공식이 맞는지 검증하기

여기까지는 유도입니다. 맞는지 확인해야죠. llama.cpp의 공식 문서(tools/quantize/README.md)가 Llama-3.1-8B에 대해 양자화별 bpw와 파일 크기를 발표해 두었습니다. 공식을 뒤집어서 파라미터 수를 역산해 보면 — 파라미터 수 = 크기 × 8 / bpw — 모든 행에서 같은 값이 나와야 합니다.

quant      발표 bpw   발표 크기(GiB)   역산한 파라미터 수
F16        16.0005      14.96            8.031 B
Q8_0        8.5008       7.95            8.033 B
Q6_K        6.5633       6.14            8.036 B
Q5_K_M      5.7036       5.33            8.027 B
Q4_K_M      4.8944       4.58            8.038 B
Q3_K_M      3.9960       3.74            8.040 B
Q2_K        3.1593       2.95            8.021 B
IQ1_S       2.0042       1.87            8.015 B

전부 8.02~8.04B에 모입니다. 그리고 Llama-3.1-8B의 실제 파라미터 수는 Hugging Face의 safetensors 메타데이터에 정수로 박혀 있습니다 — 8,030,261,248개. 공식이 맞습니다. 반올림 오차 범위 안에서 모든 행이 실제 값을 맞힙니다.

더 좋은 확인도 있습니다. 블록 구조체에서 유도한 값과 llama.cpp가 실제 측정해 발표한 값을 나란히 놓아 봅시다.

                 구조체에서 유도    발표된 실측     차이
Q8_0                 8.5000          8.5008      +0.0008
Q6_K                 6.5625          6.5633      +0.0008
Q5_K_S               5.5000          5.5704      +0.0704
Q5_K_M               5.5000          5.7036      +0.2036
Q4_K_S               4.5000          4.6672      +0.1672
Q4_K_M               4.5000          4.8944      +0.3944
Q3_K_S               3.4375          3.6429      +0.2054
Q3_K_M               3.4375          3.9960      +0.5585
Q2_K                 2.6250          3.1593      +0.5343

Q8_0과 Q6_K는 소수점 셋째 자리까지 맞습니다(8.5000 대 8.5008). 순수한 블록 산수만으로 공식 수치를 예측한 겁니다. 그런데 나머지는 최대 0.56비트까지 초과합니다. 이 초과분이 다음 절의 주제입니다.

왜 Q4_K_M은 4.5가 아니라 4.89인가

_S(small)와 _M(medium)은 양자화 타입이 아니라 혼합 레시피입니다. 모든 텐서를 Q4_K로 찍어내는 게 아니라, 어떤 텐서는 더 높은 정밀도로 남깁니다.

레시피는 src/llama-quant.cpp에 명시돼 있습니다. Q4_K_M의 경우 이런 규칙들이 걸립니다.

  • 어텐션의 attn_v 텐서 — 일부 레이어에서 Q6_K로 승격 (use_more_bits가 참인 레이어)
  • FFN의 ffn_down 텐서 — 마찬가지로 일부 레이어에서 Q6_K로 승격
  • 어텐션 QKV 텐서 — Q5_K로 승격

즉 Q4_K_M은 "대부분 4.5비트인데, 품질에 민감한 텐서 일부는 6.5비트"입니다. 그래서 평균이 4.5에서 4.8944로 올라갑니다. 소스의 주석이 이 설계 철학을 잘 설명합니다 — 70B 모델의 attn_v에 대한 코멘트를 옮기면, 8개 헤드가 attn_v 가중치를 공유하기 때문에 그 텐서는 attn_q보다 8배 작고, 따라서 "모델 크기는 거의 안 늘리면서 양자화 정확도는 꽤 올릴 수 있다"는 겁니다.

여기서 실무적으로 중요한 결론 하나. _M_S보다 항상 낫다는 뜻이 아닙니다. _M은 비트를 더 쓰는 대신 품질을 사는 거래이고, 그 거래가 유리한지는 뒤의 품질 수치를 봐야 압니다. 그리고 Q4_K_M이 로컬 진영의 기본값처럼 굳어진 이유도 뒤에 나옵니다.

KV 캐시 — 컨텍스트에 비례해 자라는 쪽

이제 진짜 문제입니다.

공식 (2)를 다시 씁니다.

KV 캐시 = 2 × L × n_kv_heads × head_dim × bytes × n_tokens

이 공식이 맞는지도 llama.cpp 소스로 확인할 수 있습니다. src/llama-kv-cache.cpp는 레이어마다 K와 V 텐서를 이렇게 만듭니다.

const uint32_t n_embd_k_gqa = hparams.n_embd_k_gqa(il);
ggml_tensor * k = ggml_new_tensor_3d(ctx, type_k, n_embd_k_gqa, kv_size, n_stream);
ggml_tensor * v = ggml_new_tensor_3d(ctx, type_v, n_embd_v_gqa, kv_size, n_stream);

그리고 n_embd_k_gqa의 정의는 src/llama-hparams.cpp에 있습니다.

uint32_t llama_hparams::n_embd_k_gqa(uint32_t il) const {
    const uint32_t n_head_kv = this->n_head_kv(il);
    return n_embd_head_k(il) * n_head_kv;
}

head_dim × n_kv_heads짜리 벡터가 토큰(kv_size)마다, K와 V 두 벌, 레이어마다 잡힙니다. 공식 (2) 그대로입니다.

여기서 핵심은 n_kv_heads입니다. 어텐션 헤드 수가 아니라 KV 헤드 수입니다. 요즘 모델은 거의 다 GQA(Grouped-Query Attention)를 써서 KV 헤드를 크게 줄여 놨습니다. Llama-3.1-8B는 어텐션 헤드가 32개인데 KV 헤드는 8개뿐입니다. 여기서 32를 넣으면 답이 4배로 틀립니다. 인터넷 표들이 자주 틀리는 지점이 정확히 여기입니다.

Llama-3.1-8B로 계산해 보기

설정값은 config.json에 있습니다 — 레이어 32개, 어텐션 헤드 32개, KV 헤드 8개, hidden 4096, 최대 컨텍스트 131,072. head_dim은 따로 없으니 hidden / n_heads = 4096 / 32 = 128입니다. (참고로 Meta의 공식 저장소는 접근 승인이 필요해서 익명 조회가 막혀 있습니다. 위 값과 뒤에 나오는 파라미터 수는 미러 저장소에서 읽은 것이고, 출처를 참고 자료에 밝혀 두었습니다.)

토큰 1개당 = 2 × 32레이어 × 8KV헤드 × 128 × 2바이트(f16)
           = 131,072 바이트
           = 128 KiB / 토큰

128K 컨텍스트(131,072 토큰) 전부:
  131,072 바이트 × 131,072 토큰 = 17,179,869,184 바이트 = 정확히 16 GiB

토큰 하나에 128 KiB. 외우기 좋습니다. 그리고 128K를 다 채우면 16 GiB, 딱 떨어집니다.

이제 아까 그 숫자와 나란히 놓아 보세요.

Llama-3.1-8B, Q4_K_M
  가중치         4.58 GiB   (고정)
  KV @  4K        0.5 GiB
  KV @ 32K        4.0 GiB   ← 여기서 이미 가중치와 맞먹음
  KV @ 128K      16.0 GiB   ← 가중치의 3.5배

당신을 막는 건 모델이 아니라 컨텍스트입니다. 8B 모델을 4비트로 줄이는 데 성공했다고 해서 128K 컨텍스트를 쓸 수 있다는 뜻이 전혀 아닙니다.

다른 모델은 다릅니다 — 그래서 공식이 필요합니다

같은 "8B"라도 값이 다릅니다. Qwen3-8B의 config.json은 레이어 36개, KV 헤드 8개, head_dim 128, 최대 컨텍스트 40,960입니다.

Qwen3-8B: 2 × 36 × 8 × 128 × 2 = 147,456 바이트 = 144 KiB / 토큰
          최대 컨텍스트 40,960 토큰을 다 쓰면 → 5.625 GiB

토큰당으로는 Llama보다 12.5% 더 먹지만(레이어가 36개라서), 최대 컨텍스트가 40,960이라 총량은 훨씬 작습니다. "8B"라는 라벨은 KV 캐시에 대해 아무것도 말해 주지 않습니다. 레이어 수, KV 헤드 수, head_dim, 그리고 당신이 실제로 열 컨텍스트 — 이 넷이 정합니다.

KV 캐시도 양자화할 수 있습니다

llama.cpp는 KV 캐시 자체를 낮은 정밀도로 저장하는 옵션을 줍니다. tools/server/README.md에서 그대로 옮기면,

  • -ctk, --cache-type-k TYPE — 허용값: f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1 (기본값 f16)
  • -ctv, --cache-type-v TYPE — 같은 허용값, 같은 기본값

q8_0으로 바꾸면 원소당 2바이트가 1바이트로 줄어 KV 캐시가 정확히 반이 됩니다. Llama-3.1-8B의 128K 캐시가 16 GiB에서 8 GiB로 내려갑니다. 허용값 목록에서 눈여겨볼 점 하나 — k-quant(q4_K 같은 것)는 목록에 없습니다. KV 캐시에는 레거시 양자화만 쓸 수 있습니다.

다만 여기에 품질 대가가 있는데, 그 대가는 가중치 양자화만큼 잘 측정돼 공개돼 있지 않습니다. 이 방향의 최신 연구는 INT4 KV 캐시 양자화 편에서 따로 다뤘습니다. 여기서는 메모리가 정확히 절반이 된다는 산수만 확정하고 넘어갑니다.

그래서 당신 카드에 뭐가 들어가나

이제 조립합니다. Llama-3.1-8B를 Q4_K_M(4.58 GiB)으로 올린다고 할 때, 남는 공간 전부를 KV에 쓴다면 컨텍스트를 얼마나 열 수 있나.

주의 — 아래는 연산 버퍼와 화면 출력을 뺀 이론적 상한입니다. 실제로는 여기서 더 줄어듭니다. 그 몫은 앞에서 말했듯 지어내지 않고, 당신의 로그에서 읽으셔야 합니다.

VRAM     가중치 뺀 나머지    f16 KV로 열 수 있는 컨텍스트(상한)   q8_0 KV면
16 GiB      11.4 GiB              약  93,000 토큰                약 187,000
24 GiB      19.4 GiB              약 159,000 토큰                약 318,000
32 GiB      27.4 GiB              약 224,000 토큰                약 449,000

읽는 법이 중요합니다.

  • 16GB 카드: 8B 모델은 여유롭게 들어갑니다(4.58 GiB). 하지만 128K 컨텍스트는 f16으로는 상한선이 93,000 토큰이라 못 엽니다. 여기에 연산 버퍼까지 빼면 더 못 엽니다. -ctk q8_0 -ctv q8_0을 켜면 그제야 128K가 사정권에 들어옵니다.
  • 24GB 카드(RTX 3090/4090): f16으로 128K가 상한 안에는 들어오지만(159,000) 여유가 크지 않습니다.
  • 32GB 카드(RTX 5090): f16 128K가 편하게 들어갑니다.

한 줄 요약 — "모델이 카드에 들어가나"는 잘못된 질문입니다. 옳은 질문은 "모델 + 내가 실제로 쓸 컨텍스트가 들어가나"입니다.

양자화는 품질을 얼마나 깎나 — 측정된 값만

여기서부터는 주장하지 않고 인용만 하겠습니다.

llama.cpp는 tools/perplexity/README.md에 양자화별 품질 손실을 측정해 공개합니다. 이 표가 좋은 이유는 조건을 밝히기 때문입니다.

출처: llama.cpp tools/perplexity/README.md — "LLaMA 3 8b Scoreboard"
  Revision : f364eb6f
  Backend  : CUDA
  CPU      : AMD Epyc 7742
  GPU      : 1x NVIDIA RTX 4090
  데이터셋 : Wikitext-2 테스트셋

먼저 큰 경고 하나. 저 revision f364eb6f를 GitHub API로 조회해 보면 커밋 날짜가 2024년 4월 30일입니다. 즉 이 표는 현재 master의 README에 실려 있지만, 데이터 자체는 2년 넘은 빌드에서 나온 것입니다. 그 사이 llama.cpp의 양자화 구현은 계속 바뀌었습니다. 그러니 이 숫자들은 양자화 레벨 간의 상대적 서열을 읽는 데 쓰고, 절대값을 오늘의 빌드에 그대로 옮기지는 마십시오. 다만 이게 프로젝트가 직접 측정해 공개한, 조건이 명시된 유일한 공개 데이터입니다.

지표는 두 가지를 봅니다. PPL(perplexity)은 낮을수록 좋고, KLD(KL 발산)는 fp16과 확률 분포가 얼마나 달라졌는지로 0이면 완전히 같습니다. README 자신이 KLD를 더 신뢰하는 쪽으로 표를 정렬해 놨습니다.

quant      크기(GiB)    PPL        ΔPPL       KLD        평균 Δp
f16          14.97    6.233160        —      0.000551     0.001 %
q8_0          7.96    6.234284   0.002650   0.001355    -0.019 %
q6_K          6.14    6.253382   0.021748   0.005452    -0.007 %
q5_K_M        5.33    6.288607   0.056974   0.010762    -0.114 %
q4_K_M        4.58    6.407115   0.175482   0.031273    -0.596 %
q3_K_M        3.74    6.888498   0.656864   0.101913    -1.990 %
q2_K          2.96    9.751568   3.519934   0.445132    -9.123 %
iq1_S         1.88   58.097760  51.866126   2.211278   -32.471 %
   (imatrix 없음 기준. iq1_S만 "WT 1m" imatrix 사용)

이 표를 읽는 법.

  • q8_0은 사실상 공짜입니다. PPL이 6.233에서 6.234로 갑니다. 크기는 절반인데 KLD가 0.0014입니다. fp16을 고집할 이유가 거의 없습니다.
  • q6_K, q5_K_M도 매우 싸다. 평균 Δp(맞는 토큰의 확률 변화)가 각각 -0.007%, -0.114%입니다.
  • q4_K_M이 무릎입니다. 크기는 fp16의 31% 수준인데 평균 Δp가 -0.596%. 이게 Q4_K_M이 로컬 진영의 기본값이 된 이유입니다.
  • q3_K_M부터 눈에 띄게 아픕니다. PPL이 6.41에서 6.89로 뜁니다.
  • q2_K는 다른 모델이 됩니다. PPL 9.75, 평균 Δp가 -9.1%.
  • iq1_S는 무너집니다. PPL 58.1. 원본이 6.23이니 9.3배입니다.

README 자신이 붙인 경고를 그대로 옮깁니다

이 숫자들을 쓰기 전에 문서가 직접 단 단서들이 있습니다. 원문의 뜻 그대로 옮기면,

  • perplexity는 모델 간에 직접 비교할 수 없습니다. 특히 토크나이저가 다르면 그렇습니다.
  • 파인튜닝은 사람이 평가한 출력 품질이 올라가도 perplexity는 대개 올라갑니다.
  • llama.cpp의 수치는 구현 세부에 크게 의존하므로 다른 프로젝트의 수치와 직접 비교할 수 없습니다.

세 번째가 특히 중요합니다. 이 표의 PPL을 GPTQ나 AWQ 논문의 PPL과 나란히 놓고 비교하면 안 됩니다. 서로 다른 구현의 자체 측정값이고, 공통 벤치마크가 아닙니다.

그리고 두 번째 경고가 함의하는 바가 큽니다 — perplexity는 품질의 대리 지표일 뿐, 품질 자체가 아닙니다. "q4_K_M의 평균 Δp가 -0.596%"라는 문장은 "당신의 코드 생성 작업이 0.596% 나빠진다"는 뜻이 아닙니다. 그건 아무도 측정해 주지 않았습니다. 당신의 작업으로 재야 합니다.

같은 양자화가 모델마다 다르게 아프다

이게 이 표에서 가장 덜 알려진, 그리고 가장 중요한 사실입니다. 같은 문서의 "LLaMA 2 vs. LLaMA 3 Quantization comparison" 표를 보면 (같은 revision, 같은 4090),

Mean PPL ratio (1.0 = 손실 없음)
                    L2 7b        L3 8b
  q8_0            1.000689     1.000425
  q6_K            1.002406     1.003490
  q4_K_M          1.014242     1.028160      ← 1.4% vs 2.8%, 두 배
  q2_K            1.107955     1.564849      ← 10.8% vs 56.5%, 다섯 배

Mean KLD
  q2_K            0.108903     0.445132      ← 4.1배

같은 q2_K가 Llama-2 7B에서는 PPL을 10.8% 올리는데, Llama-3 8B에서는 56.5% 올립니다. q4_K_M도 1.4% 대 2.8%로 정확히 두 배입니다.

시사점이 큽니다. "Q4_K_M은 품질 손실이 미미하다"는 문장은 모델에 대한 한정 없이는 참이 아닙니다. 더 많은 토큰으로 더 빡세게 학습된 모델일수록 가중치에 여유가 적고, 그래서 같은 비트 예산에서 더 많이 잃습니다. 2026년의 모델들은 Llama-3보다도 더 많이 학습됐습니다. 그러니 위 표의 수치는 오늘의 최신 모델에 대해 낙관적인 쪽으로 틀렸을 가능성이 큽니다. 당신의 모델에 대해 다시 재는 것 말고 방법이 없습니다.

참고로 imatrix(importance matrix)에 대해서도 README가 정직한 관찰을 남겼습니다 — Wikitext 토큰을 더 많이 써서 imatrix를 만든다고 일관된 개선이 나타나지는 않았다는 것입니다. imatrix 자체는 도움이 됩니다(q4_K_M의 KLD가 0.0313에서 0.0282로), 하지만 "더 많은 토큰 = 더 좋은 imatrix"는 이 데이터에서 지지되지 않습니다.

tok/s 숫자가 하드웨어 없이 오면 버리세요

이제 속도입니다. 그리고 여기서 제일 하고 싶은 말은 방법론입니다.

tok/s 하나가 의미를 가지려면 최소 네 가지가 같이 와야 합니다 — 정확한 하드웨어(GPU 모델명), 모델, 양자화, 그리고 컨텍스트 길이(또는 배치 길이). 넷 중 하나라도 없으면 그 숫자는 다른 사람에게 아무 정보도 주지 못합니다.

이게 잔소리가 아니라는 걸 보여드리겠습니다. llama.cpp 공식 README(tools/quantize/README.md)에 이런 표가 실려 있습니다.

Measure                       Q4_K_M          Q8_0            F16
bits/weight                   4.8944          8.5008        16.0005
size (GiB)                      4.58            7.95          14.96
prompt processing t/s @ 512   821.81 ±21.44   865.09 ±8.30   923.49 ±0.53
text generation t/s @ 128      71.93 ±1.52     50.93 ±0.08    29.17 ±0.04

모델은 밝혔습니다(Llama-3.1-8B). 양자화도 밝혔습니다. 컨텍스트 길이도 밝혔습니다(@ 512, @ 128). 오차 범위까지 붙였습니다. 그런데 하드웨어가 어디에도 없습니다. 문서 전체를 훑어도 GPU도, CPU도, 백엔드도, 빌드 리비전도 없습니다. 이 표를 마지막으로 고친 PR(#24133, 2026년 6월 5일 머지)의 본문에도 없습니다.

그래서 "Q4_K_M은 71.93 tok/s"라는 문장은 혼자서는 쓸모가 없습니다. 3090에서인가요, 5090에서인가요, M4 Max에서인가요? 3090과 5090은 메모리 대역폭이 1.9배 차이 납니다. 같은 문서를 쓴 같은 프로젝트가 perplexity README에서는 CPU·GPU·백엔드·리비전을 전부 밝혔다는 걸 생각하면, 이건 문서의 실수에 가깝습니다.

하지만 — 그리고 이게 재미있는 부분인데 — 이 표에서 여전히 읽어낼 수 있는 게 있습니다. 절대값은 못 읽지만, 모든 행이 같은(미지의) 하드웨어에서 나왔으니 행 사이의 비율은 유효합니다. 다음 절이 그 이야기입니다.

비트를 더 줄이면 더 빨라지나 — 어느 선까지만

먼저 왜 양자화가 속도를 올리는지부터. 배치 1로 토큰을 생성할 때, 모델은 토큰 하나마다 가중치 전체를 메모리에서 한 번씩 읽습니다. 연산량은 얼마 안 되는데 읽을 게 많습니다. 그래서 디코딩은 메모리 대역폭에 묶입니다(memory-bandwidth-bound). 여기서 천장 공식이 나옵니다.

tok/s 상한 = 메모리 대역폭 / 모델 크기        (dense 모델, 배치 1)

NVIDIA의 RTX Blackwell 아키텍처 백서에 실린 대역폭 스펙으로 계산해 봅시다. 백서 본문 그대로 — RTX 5090은 28 Gbps GDDR7을 달고 1.792 TB/sec의 피크 메모리 대역폭을 냅니다. 같은 백서의 비교표가 3090은 936 GB/sec, 4090은 1008 GB/sec로 적고 있습니다.

Llama-3.1-8B Q4_K_M (4.58 GiB) 의 이론적 천장
  RTX 3090 (936 GB/s)  →  190 tok/s
  RTX 4090 (1008 GB/s) →  205 tok/s
  RTX 5090 (1792 GB/s) →  364 tok/s

이건 천장입니다. 실측은 항상 이보다 낮습니다. 하지만 이 공식은 당신 카드에 대해 "이보다 빠를 수는 없다"를 알려주고, 어떤 벤치마크 수치가 물리적으로 말이 되는지 판정하게 해 줍니다. 어떤 글이 4090에서 8B Q4_K_M을 300 tok/s로 돌렸다고 하면, 배치 1 디코딩이 아니거나 뭔가 다른 걸 잰 겁니다.

그런데 공식이 아래쪽에서 깨집니다

이제 아까 말한 "비율은 유효하다"를 씁니다. 만약 디코딩이 순수하게 대역폭에 묶여 있다면, 크기 × tok/s는 모든 양자화에서 일정한 값(= 실효 대역폭)이 나와야 합니다. llama.cpp 공식 표의 숫자로 직접 곱해 보겠습니다. (아래 곱셈은 제가 그들의 발표 수치로 계산한 값입니다.)

quant       크기(GiB)   생성 tok/s    크기 × tok/s
F16           14.96       29.17          436
Q8_0           7.95       50.93          405
Q6_K           6.14       58.67          360
Q5_K_M         5.33       67.23          358
Q4_K_M         4.58       71.93          329
Q3_K_M         3.74       71.68          268
Q2_K           2.95       79.85          236
IQ2_XXS        2.23       79.86          178
IQ1_S          1.87       79.73          149

일정하지 않습니다. 436에서 149로 계속 떨어집니다. 비트를 줄일수록 대역폭을 점점 못 쓰고 있다는 뜻입니다 — 읽을 데이터는 줄었는데, 그걸 다시 풀어내는(dequantize) 연산이 늘어나서 병목이 대역폭에서 연산으로 옮겨 간 겁니다.

결과를 비율로 보면 아프도록 명확합니다.

F16    → Q4_K_M :  크기 3.27배 작아지고, 속도 2.47배 빨라짐   (거래 성립)
Q4_K_M → Q2_K   :  크기 1.55배 작아지고, 속도 1.11배 빨라짐   (거래 나쁨)
Q4_K_M → IQ1_S  :  크기 2.45배 작아지고, 속도 1.11배 빨라짐   (거래 최악)

Q4_K_M에서 IQ1_S로 내려가면 모델이 2.45배 작아지는데 속도는 11%밖에 안 빨라집니다. 그동안 품질은 — 앞 절의 표에서 — PPL이 6.41에서 58.1로(9.1배), KLD가 0.031에서 2.21로(71배) 갑니다. 9배 나빠진 모델을 얻고 속도 11%를 받는 거래입니다.

단서를 정직하게 답니다. 이 속도 표(Llama-3.1-8B, 하드웨어 미상)와 앞의 품질 표(Llama-3 8B, RTX 4090, 2024년 4월 리비전)는 서로 다른 실행입니다. 모델도 미묘하게 다르고 빌드도 다릅니다. 그러니 두 표를 가로질러 읽는 건 엄밀한 비교가 아니라 방향을 보는 것입니다. 그럼에도 두 표 각각의 내부 서열은 견고하고, 그 서열이 같은 곳을 가리킵니다.

그래서 아주 낮은 비트로 내려가는 유일하게 정당한 이유는 속도가 아니라 "그것 말고는 안 들어가서"입니다. 속도 때문에 IQ1_S를 고르는 건 산수가 안 맞습니다.

계산이 달라지는 경우 — MoE와 하이브리드 어텐션

두 공식에는 가정이 있습니다. 요즘 모델들이 그 가정을 깹니다.

MoE(Mixture of Experts). OpenAI의 gpt-oss-20b 모델 카드를 그대로 인용하면, 이 모델은 "21B 파라미터에 활성 파라미터 3.6B"입니다. 여기서 두 공식이 갈라집니다.

  • 메모리는 전체가 필요합니다. 어느 전문가가 뽑힐지 미리 모르니 21B를 다 올려야 합니다.
  • 대역폭은 활성분만 읽습니다. 토큰마다 3.6B어치만 읽습니다.

즉 앞의 천장 공식 대역폭 / 모델 크기에서 분모가 전체 크기가 아니라 활성 크기로 바뀝니다. MoE가 "무겁지만 빠른" 이유가 이것입니다. 메모리는 21B의 값을 치르고 속도는 3.6B의 값을 받습니다. 같은 모델 카드가 gpt-oss-20b는 MXFP4로 MoE 가중치를 양자화해 16GB 메모리 안에서 돌아간다고 밝히고 있습니다.

하이브리드 어텐션. gpt-oss-20b의 config.json을 보면 layer_typessliding_attentionfull_attention을 번갈아 배치하고, sliding_window가 128입니다. 슬라이딩 윈도우 레이어는 최근 128토큰의 KV만 들고 있으면 되므로, 그 레이어의 KV는 컨텍스트에 비례해 자라지 않습니다. 공식 (2)의 n_tokens가 레이어마다 달라지는 겁니다.

이게 예외가 아니라는 게 요점입니다. llama.cpp의 소스가 이미 이걸 반영하고 있습니다 — 앞에서 본 n_embd_k_gqa(il)이 레이어 인덱스 il을 인자로 받는 이유가 바로 레이어마다 KV 헤드 수가 다를 수 있어서입니다. 모델의 config.json을 열어 보는 것을 대체할 수 있는 표는 없습니다.

그래서, 무엇을 포기하는가

정리합니다. 로컬 LLM에서 당신이 포기하는 것은 셋입니다.

첫째, 품질. 측정된 값으로 말하면 — q8_0은 사실상 공짜(KLD 0.0014), q5_K_M까지도 싸고(KLD 0.011), q4_K_M이 무릎(KLD 0.031), q3_K_M부터 아프고(KLD 0.102), q2_K 아래는 다른 모델(KLD 0.445+)입니다. 단, 이 서열은 2024년 4월 빌드의 Llama-3 8B 기준이고, 더 많이 학습된 모델일수록 같은 비트에서 더 많이 잃습니다.

둘째, 컨텍스트. 이게 가장 과소평가된 항목입니다. 8B 모델을 16GB 카드에 올리는 건 쉽습니다. 그 모델에 128K 컨텍스트를 f16으로 주는 건 불가능합니다. 대부분의 사람이 "모델은 들어갔는데 왜 긴 문서를 못 넣지"에서 막힙니다.

셋째, 속도의 예측 가능성. 남의 tok/s는 당신의 tok/s가 아닙니다. 하드웨어가 명시되지 않은 수치는 버리시고, 명시된 수치도 당신 카드의 대역폭으로 천장을 다시 계산해 보십시오.

결정 규칙

"경우에 따라 다르다"로 끝내지 않겠습니다. 이렇게 하십시오.

  1. 양자화는 Q4_K_M에서 시작하십시오. 품질/크기 무릎이고, 아래로 더 내려가도 속도는 거의 안 붙습니다. 카드에 여유가 있으면 Q5_K_M이나 Q6_K로 올리는 게, Q4 아래로 내려가 남은 공간을 쓰지 않고 두는 것보다 항상 낫습니다.
  2. 가중치를 정한 다음, 남은 VRAM을 KV 예산으로 놓고 컨텍스트를 역산하십시오. 반대로 하지 마십시오. 컨텍스트가 먼저 정해져 있다면 그게 양자화를 결정합니다.
  3. KV가 안 들어가면, 가중치를 더 깎기 전에 -ctk q8_0 -ctv q8_0을 먼저 켜십시오. 캐시가 정확히 반이 됩니다. 가중치를 Q4에서 Q3로 내리는 것보다 이쪽이 대개 이득입니다.
  4. 모델을 바꿀 때마다 config.json을 여십시오. 레이어 수, KV 헤드 수, head_dim. 30초면 됩니다.
  5. 당신의 카드, 당신의 모델, 당신의 컨텍스트로 한 번 재십시오. 그리고 재셨으면 하드웨어를 밝혀서 공유하십시오.

언제 로컬을 쓰면 안 되나

정직하게 — 로컬이 답이 아닌 경우가 많습니다.

  • 품질이 최우선일 때. 4비트 8B는 프런티어 모델이 아닙니다. 그 격차는 양자화로 메울 수 없습니다.
  • 긴 컨텍스트가 본질일 때. KV 산수가 당신을 이깁니다. 128K를 늘 쓴다면 로컬 단일 카드는 잘못된 도구입니다.
  • 동시 사용자가 있을 때. 위 공식은 전부 배치 1 기준입니다. 여러 요청을 동시에 받으면 KV 캐시가 사용자 수만큼 곱해집니다.
  • 가끔 쓸 때. 카드값은 API 토큰 아주 많은 양과 같습니다.

반대로 로컬이 이기는 경우는 명확합니다 — 데이터가 기기 밖으로 나가면 안 될 때, 오프라인이어야 할 때, 호출량이 크고 예측 가능해서 고정비가 상환될 때, 그리고 지연이 네트워크 왕복보다 중요할 때. 그리고 RTX 5090으로 작은 모델들을 직접 굴려 본 기록에서 보듯, 작은 모델로 충분한 작업은 생각보다 많습니다.

마치며

로컬 LLM의 VRAM 질문에 대한 정답은 숫자가 아니라 두 개의 공식입니다.

가중치  = 파라미터 수 × bpw / 8
KV캐시  = 2 × 레이어 × KV헤드 × head_dim × 바이트 × 토큰 수

첫 번째는 고정이고, 두 번째가 자랍니다. 그리고 이 글에서 확인한 대로, 두 번째가 첫 번째를 가볍게 넘어섭니다 — Llama-3.1-8B Q4_K_M의 가중치는 4.58 GiB인데 128K 컨텍스트의 f16 KV는 16 GiB입니다.

이 공식들을 믿어도 되는 이유는 제가 그렇다고 해서가 아닙니다. ggml 블록 구조체에서 유도한 Q8_0의 8.5 bpw가 llama.cpp가 발표한 8.5008과 맞고, 그 공식을 뒤집어 역산한 파라미터 수 8.02~8.04B가 실제 값 8,030,261,248과 맞기 때문입니다. 공식이 벤더의 자체 발표 수치를 재현하면, 그 공식은 당신의 경우에도 작동합니다. 표는 그렇지 않습니다.

마지막으로 방법론 하나만 더. 이 글에서 가장 오래 붙들고 있던 문제는 llama.cpp 공식 README의 속도 표에 하드웨어가 없다는 것이었습니다. 그 표는 모델도, 양자화도, 컨텍스트 길이도, 심지어 오차 범위까지 밝혔는데 GPU 이름이 없습니다. 그래서 "71.93 tok/s"는 인용할 수 없는 숫자가 됩니다. 당신이 벤치마크를 공유할 때는 — GPU, 모델, 양자화, 컨텍스트 길이. 넷을 다 쓰십시오. 넷이 없는 숫자는 아무도 쓸 수 없습니다.

참고 자료