tonbistudio/turboquant-pytorch · Python
"From-scratch PyTorch implementation of Google's TurboQuant (ICLR 2026) for LLM KV cache compression"
지난달 초 Google Research 블로그에 TurboQuant 발표가 올라왔다. 요지는 이렇다. LLM 추론에서 제일 큰 메모리 병목은 긴 컨텍스트의 KV 캐시인데, 여기에 극단적 양자화(3-bit)를 먹이면 약 6배까지 메모리를 줄일 수 있고 품질도 거의 손실 없다. ICLR 2026 accept, Google + KAIST + NYU 조인트. 제목에 "Near-optimal Distortion Rate"가 붙어 있어 이론 쪽 주장도 강했다.
최근 AI 인프라 쪽에서 KV 캐시 메모리 절감이 꽤 핫한 주제다. 특히 TurboQuant의 "3-bit까지 가면서 품질은 거의 유지"라는 라인은 눈에 띄었다. 모델 가중치 자체를 건드리지 않고 inference 메모리를 수 배 줄일 수 있다면 단일 GPU에서 돌릴 수 있는 컨텍스트 길이가 그만큼 달라진다는 뜻이라, 내 작업 환경(3090 Ti 24GB 한 장)에도 직접 영향이 있는 얘기였다. 한 번 해봐야겠다고 생각했다.
"TurboQuant"라고 검색하면 별 표시가 많은 레포가 여러 개 떠서 처음엔 어떤 걸 돌려야 할지 혼란스럽다. GitHub에서 찾아본 star 순위를 정리하면:
| 순위 | 레포 | Stars | 성격 |
|---|---|---|---|
| 1 | TheTom/turboquant_plus | 6,408 | llama.cpp 확장, GGUF, Metal/CUDA/HIP 네이티브 커널 |
| 2 | 0xSero/turboquant | 1,136 | Triton 커널 + vLLM 통합 |
| 3 | tonbistudio/turboquant-pytorch | 951 | 순수 PyTorch 참조 구현 |
| 4~ | OnlyTerp / back2matching / RecursiveIntell(Rust) / scos-lab 등 | 4~56 | 각자 다른 포크 |
상위 3개가 다 같은 "TurboQuant"이지만 포지션이 완전히 다르다.
--cache-type-k turbo3 같은 cache type을 추가한 확장판. C/Metal/CUDA native kernel을 쓰고 GGUF 모델(Q8_0 / Q4_K_M)을 먹는다. Boundary V (첫·끝 2개 레이어 q8_0 보호)와 sparse V dequantization (softmax weight<1e-6 지점 dequant 스킵, +22.8% decode) 같은 실전 최적화가 붙어 있다. Qwen2.5 1.5~7B, Llama 3.1-70B, Mistral, phi-4, Command-R+, Qwen3.5 MoE 35B까지 검증. 배포 지향.proof.py)가 4× RTX 3090을 요구해서 단일 GPU 환경과 안 맞는다.내 환경은 단일 RTX 3090 Ti 24GB다. 조건에 맞추면:
이 선택의 성격을 미리 밝혀두면: 이 글은 "TurboQuant PyTorch 참조 구현(tonbistudio) 재현 기록"이지 TurboQuant 전체 기술이나 실전 deploy까지의 평가는 아니다. 배포 지향 구현(TheTom, 0xSero)에서는 이 글의 숫자가 다르게 나올 가능성이 높고, 그건 별도 실험 주제다. 다음 GitHub 탐방 글에서 TheTom으로 같은 실험을 재현해볼 계획.
tonbistudio의 표준 실행은 한 줄이다.
python -m turboquant.generation_test # 고정: Qwen2.5-3B-Instruct, 내장 6 config × 3 ctx
여기서 두 가지를 바꿨다.
--model-path, --model-label, --prompt-format)로 바꾸고, 내 장비에 이미 있던 Qwen2.5-7B / Qwen2.5-14B / EXAONE-3.5-7.8B 로 확장했다. MODEL_NAME = "Qwen/Qwen2.5-3B-Instruct" 한 줄을 바꾸기만 해선 프롬프트 포맷·trust_remote_code 등이 엉켜서, 얇은 argparse 래퍼를 덧붙이는 게 깔끔했다.elapsed_sec / peak_mb (torch.cuda.max_memory_allocated())를 붙여 JSON으로 떨어뜨리게 했다. "몇 배 빨라졌다", "몇 배 메모리 줄였다" 류의 주장을 숫자로 관찰하려면 이게 있어야 했다.즉 이 글은 레포의 공식 테스트 재현이라기보다 레포를 baseline으로 "다른 모델 크기에서도 같은 패턴이 나오는지"를 보는 확장 실험에 가깝다. 원 README가 Qwen2.5-3B에 대해서만 보고한 pass/fail 표가 있으니, 이 글에서 나온 결과와 겹치는 부분은 교차 검증 정도로 읽으면 좋다.
참고로 tonbistudio의 V3는 원 논문의 QJL 단계를 제거한 수정판이다. README에 "We implemented the paper's algorithm, found that its key innovation (QJL) actually hurts in practice, and built an improved version (V3)" 라고 명시되어 있다. 즉 이 레포는 이미 논문에서 한 발짝 옮겨와 있고, 이 글 역시 그 수정판 기준의 재현이지 논문의 알고리즘 그대로의 재현이 아니다.
LLM_Model_File/ 에 이미 있던 모델 4종을 시도 대상으로 잡았다.The secret project code name is AURORA-7749.를 꽂고, 생성 결과에 AURORA-7749가 나오면 FOUND.torch.cuda.max_memory_allocated() 피크 VRAM.generation_test.py에 CLI 파라미터와 시간/메모리 계측을 덧붙인 generation_test_instrumented.py (약 250줄). 시간·피크 메모리·pass/fail을 JSON으로 떨어뜨림.
| Config | Qwen2.5-7B (2K/4K/8K) | Qwen2.5-14B (2K/4K/8K) |
|---|---|---|
| FP16 baseline | FOUND / FOUND / FOUND | FOUND / FOUND / FOUND |
| V3 K4/V4 rw=0 | MISS / MISS / MISS | FOUND / FOUND / FOUND |
| V3 K4/V2 rw=0 | MISS / MISS / MISS | FOUND / FOUND / FOUND |
| V3 K3/V3 rw=0 | MISS / MISS / MISS | FOUND / FOUND / FOUND |
| V3 K4/V4 rw=128 | MISS / MISS / MISS | FOUND / FOUND / FOUND |
| V3 K4/V2 rw=128 | MISS / MISS / MISS | FOUND / FOUND / FOUND |
Qwen2.5-7B 기준 18 테스트 중 FP16 3개만 통과, V3 압축 15개 전부 깨졌다. MISS 사례는 전형적으로 "1006)450505 0000 the.00..." 식으로 생성 자체가 망가진 출력이었다. 같은 스크립트·같은 설정을 Qwen2.5-14B에 돌리면 18개 전부 FOUND. 바이트 수준에선 가장 공격적인 K3/V3 rw=0 조합조차 14B에선 멀쩡했다.
레포 README는 Qwen2.5-3B에서 K4/V4 rw=128이 EXACT라고 보고했고, rw=0은 깨진다고 명시해 두었다. 내가 본 결과는 그것보다 한 단계 더 갈라진다. 같은 alg가 7B에선 rw=128에서도 깨지고 14B에선 rw=0에서도 안전하다. 모델 크기(또는 layer·head 구성)가 압축 오차 흡수 능력에 결정적으로 작용한다는 뜻으로 읽었다.
가장 직관적인 가설은 capacity redundancy다. 파라미터 수·레이어 수가 2배인 14B는 각 레이어가 가진 중복 표현이 더 넉넉해서 KV 양자화로 인한 오차를 흡수할 여유가 있는 쪽으로 해석할 수 있다. 작은 모델일수록 각 토큰·각 헤드의 정보 밀도가 상대적으로 높아, 약간의 quantization noise가 downstream attention 패턴을 쉽게 무너뜨리는 것 같다.
다만 이 설명만으로는 바이트 레벨에서 가장 공격적인 K3/V3 rw=0조차 14B에선 멀쩡한 이유까지 깔끔히 정리되진 않는다. capacity 의존성이 선형보다 더 가파르게 작용하는 걸 수도 있고, layer 수 자체(28 → 48)가 더 결정적인 변수일 수도 있다. 내 실험 두 모델만으론 단정이 어렵고, 3B·32B까지 포함된 스윕이 있어야 제대로 답이 나올 문제다. 그래서 이 섹션은 "한 번 해봤더니 두 모델 사이에 이렇게 갈렸다"는 단일 관찰 정도로 읽어주면 좋겠다.

| FP16 baseline | V3 min | V3 max | |
|---|---|---|---|
| Qwen2.5-7B 8K | 7,166 MB | 7,258 MB | 7,289 MB |
| Qwen2.5-14B 8K | 12,624 MB | 13,084 MB | 13,597 MB |
이 표가 이번 실험에서 가장 들여다본 결과였다. V3 압축 config의 피크 VRAM이 FP16과 같거나 살짝 높게 나왔다. 14B의 8K에선 최대 약 +970MB (+7.7%)까지 올라갔다.
처음엔 측정 실수인가 했는데 코드를 보면 구조적인 실행 경로 차이다. V3Cache.update()가 매 스텝·매 레이어에서 다음을 한다.
decompress_kv() 호출 → fp16 풀 텐서로 materializetorch.cat으로 합친 fp16 텐서를 attention 레이어에 반환즉 이 구현은 "bit-packed 저장 + 매 스텝 decompress-then-attend" 구조다. 저장 공간은 분명 줄지만(이론적으로 K4/V2에서 5.12×) — 위의 compression_ratios.py에서 확인한 숫자 — attention 연산 시점엔 fp16 풀 텐서가 재구성되고 그 위에 압축기 중간 버퍼도 얹히니까 "저장소 절감"과 "inference 피크 VRAM 절감"이 같은 수치로 맞물리지 않았다.
원 논문이나 Google 포스트의 6× 주장은 compressed domain에서 attention이 직접 도는 경로를 상정한 쪽에 가깝다 (fused kernel이 압축된 K와 쿼리를 그대로 곱하는 식). 이 PyTorch 참조 구현은 알고리즘 자체의 검증과 양자화 오차 측정에 초점을 맞추고 그 경로는 잡지 않았을 뿐이다. 커뮤니티 포크 중 하나인 0xSero/turboquant는 Triton 커널로 그 쪽을 노리고 있어서, 그 구현 위에서 돌리면 피크 VRAM 그림이 달라질 가능성이 높다. 이 글은 그 영역까지 가진 않았다.
| FP16 2K | V3 min 2K | V3 max 2K | slowdown | |
|---|---|---|---|---|
| Qwen2.5-7B | 1.19s | 7.61s | 13.26s | 6.4× ~ 11.1× |
| Qwen2.5-14B | 1.73s | 3.48s | 5.90s | 2.0× ~ 3.4× |
32 토큰 생성 기준. 양쪽 다 V3이 느리고, 7B가 비율로 더 느리다. 레이어당 압축/해제 오버헤드가 어텐션 자체 연산과 같은 스케일로 들어가다 보니, 레이어가 적고 어텐션이 상대적으로 가벼운 7B 쪽에서 상대적 slowdown이 더 크게 나타났다.
이 숫자는 파이썬 전용 참조 구현의 비용이라는 맥락에서 봐야 한다. 레포 자체가 "algorithm validation"이 주 목적이고 배포용 최적화는 스코프 밖이라 fused kernel이 없다. Triton/CUDA 커널이 붙은 포크에선 이 숫자가 많이 줄어들 것이다. 그래서 이 표는 "TurboQuant이 느리다"가 아니라 "이 참조 구현 기준으로 inference 중 오버헤드가 어느 정도 규모인지"를 보여주는 자료로 읽으면 좋다.
원래 네 모델을 다 돌려보고 싶었지만 두 개는 실험 전 단계에서 멈췄다. 그 과정에서 "이 V3Cache가 어떤 조건에선 아예 슬롯인 안 된다"는 그림이 자연스럽게 그려져서, 같이 적는다.
로컬에 gemma-4-E2B가 있어서 같이 돌릴 생각이었는데 config.json을 열어보면 model_type=gemma4에 architectures=["Gemma4ForConditionalGeneration"]. 즉 ...ForCausalLM이 아니라 텍스트 디코더 + 오디오 인코더 + 비전 인코더의 conditional generation 모델이다. 텍스트만 돌리려 해도 내부 cache 구조가 일반 디코더 LLM과 다르다.
여기서 문제가 두 겹이다.
V3Cache는 transformers.DynamicCache를 단순 상속한다. 멀티모달 파이프라인이 기대하는 건 종종 HybridCache (sliding window 레이어와 일반 레이어를 섞어 쓰는 경우)나 모델 전용 cache 구조다. 이 둘 사이에 슬롯인을 하려면 V3Cache가 cache type을 감지해서 레이어별로 다르게 동작하도록 바꿔야 한다.이 글 범위에서 패치할 일이 아니라 스킵했지만, "멀티모달 / hybrid cache 모델에 TurboQuant를 적용하려면 이 참조 구현 위에 레이어 타입 분기가 추가로 필요하다"는 건 꽤 현실적인 제약이다. 음성·비전이 점점 LLM에 합쳐지는 추세라 이 부분이 필요해질 일이 많을 것 같다.
EXAONE-3.5-7.8B-Instruct도 같이 실험하려 했지만 로드는 되는데 첫 model.generate()에서 다음으로 죽는다.
# modeling_exaone.py:1124
elif input_ids.shape[1] != cache_position.shape[0]:
AttributeError: 'NoneType' object has no attribute 'shape'
EXAONE이 배포한 custom modeling_exaone.py는 transformers 4.x 시대 prepare_inputs_for_generation에 맞춰 작성되어 있어서 cache_position이 항상 넘어온다고 가정한다. transformers 5.x에서 그 경로에 None이 들어오는 케이스가 열리면서 가드가 빠져버렸다. FP16 baseline generate() 단계에서 죽는 거라 V3Cache 호환성은 아예 시도조차 못 했다. TurboQuant이슈라기보다 커스텀 모델 코드와 transformers 메이저 버전 사이의 지연 이슈. 적어 둔다.
compressors_v3.py의 bit-packing 로직은 indices_per_byte = 8 // bits. 4-bit면 8/4=2 인덱스/바이트, 3-bit면 8/3=2 (내림). 즉 3-bit 설정도 실제론 인덱스 하나가 바이트의 절반(4 bits effective)을 차지한다. 같은 아키텍처에서 K3/V2 ≡ K4/V2 ≡ 5.12× 압축, K3/V3 ≡ K4/V3 ≡ 3.88× 압축. 헤드라인에 "3-bit"라 써 있지만 현재 구현의 실제 저장은 4-bit와 구분되지 않는다는 뜻. 알고리즘이 처음부터 3비트의 정보량을 쓰도록 설계된 것과, 구현이 3비트를 바이트에 패킹하는 것은 다른 얘기다.
3-bit라는 숫자가 실제 저장량에선 4-bit와 구분되지 않는 건 현재 구현의 제약이지만, 알고리즘의 정보량 설계는 여전히 3-bit 경계를 쓰니까 양자화 오차 역시 3-bit 기준으로 쌓인다. 즉 "3-bit의 손해는 다 보면서 4-bit의 이득만 얻는" 포지션이 되기 쉬워 보여서, 의도적으로 3-bit 설정을 고를 땐 조심할 부분이다. 개인적으로도 이런 "헤드라인 숫자와 실제 저장 사이의 갭"은 양자화 쪽에선 흔한 함정이라 듣기만 했었는데 손에 쥐어본 건 이번이 처음이었다. 앞으로 양자화 기법 볼 때 indices_per_byte 같은 패킹 코드부터 먼저 읽는 습관을 들이려 한다.
실험이 3090 Ti 한 장이라 "RTX 50 (Blackwell) 이나 H100에서 돌리면 다르지 않을까"라는 질문이 자연스럽게 따라온다. 세대 간 정밀도 지원 차이를 정리해 두면:
| 세대 | Tensor Core 정밀도 | 비고 |
|---|---|---|
| RTX 30 (Ampere) | fp16, bf16, tf32, int8 | fp8/fp4 없음 |
| RTX 40 (Ada) | + fp8 (E4M3 / E5M2) | 첫 소비자용 fp8 |
| RTX 50 (Blackwell) | + fp4 (NVFP4 / MXFP4), GDDR7, 32GB | 첫 소비자용 fp4 |
| H100 / H200 (Hopper) | fp8 + Transformer Engine | 데이터센터 계열 |
| B200 (Blackwell DC) | fp4 + 대용량 HBM | 데이터센터 Blackwell |
결과 3축별로 영향이 다르다.
(1) 품질 (7B 전멸·14B 전승) — GPU 무관일 가능성 높음. TurboQuant V3의 핵심은 Lloyd-Max 스칼라 양자화. 2^bits 개의 센트로이드에 각 좌표를 매핑하는 순수 수학 연산이라 hardware fp8/fp4 Tensor Core를 쓰지 않는다. 센트로이드 인덱스가 결정되는 순간 양자화 오차는 확정이고, 그걸 softmax에 통과시켰을 때 attention 패턴이 버티느냐 깨지느냐는 모델의 representation 밀도에 달린 문제다. RTX 50에서 같은 코드를 돌려도 7B는 깨지고 14B는 통과할 것으로 본다.
(2) 피크 VRAM — 참조 구현에선 GPU 무관, 커널 구현에선 유관. 이번 실험에서 드러난 "저장 공간 절감 ≠ 피크 VRAM 절감" 갭은 매 스텝마다 compressed chunk를 fp16으로 풀어 materialize하는 Python 로직에서 나온다. 이건 Ampere건 Blackwell이건 똑같이 일어나는 일이다. 갭을 닫으려면 compressed domain에서 attention을 직접 도는 fused kernel이 필요한데, 이 경로는 fp8이나 int8 Tensor Core를 써야 효율적이다. RTX 30은 fp8이 없어서 이 커널 경로를 int8 fallback으로 내려야 하고, RTX 40/50이나 H100은 fp8 Tensor Core를 그대로 활용할 수 있다. 0xSero/turboquant의 Triton 커널이 이 쪽을 겨냥하고 있으니, RTX 50 + 0xSero 조합이면 Google 포스트의 "6× 메모리 절감" 숫자가 처음으로 제대로 드러날 가능성이 있다.
(3) 속도 — 참조 구현에선 GPU 무관. 같은 이유. 현재 참조 구현의 병목은 Python 레이어별 압축/해제 오버헤드이지 Tensor Core 처리량이 아니다. RTX 50으로 옮겨도 slowdown 비율은 크게 다르지 않을 것으로 예상. 다만 kernel-fused 구현에선 fp8 Tensor Core 처리량 이득으로 큰 차이가 날 수 있다.
정리하면: "RTX 50에서 돌리면 TurboQuant이 잘 먹히는가"라는 질문은, 질문을 조금 더 쪼개면 이렇게 갈린다.
이번 실험은 전자만 측정했다. 후자는 4× RTX 3090 또는 RTX 50급 장비·fused kernel 환경이 필요한 일이라 별도 실험 주제다. "메모리 절감이 실제로 확 드러나는 실험"은 그쪽에서 해야 한다는 숙제를 인지하고 넘긴다.
이번 확장 실험에서 관찰한 것들.
memory_bytes()로 계산한 이론 압축비(5.12× 등)는 그대로 유지되지만 max_memory_allocated() 피크는 FP16과 같거나 살짝 더 높게 나왔다. 원 논문의 "6× 메모리 절감"은 compressed-domain attention 커널을 전제한 수치인데, 참조 구현이 그 경로를 잡지 않은 것이 원인."TurboQuant이 나쁘다"보다는 "어느 구현·어느 전제 위에서 그 숫자가 성립하는가"를 들여다본 결과에 가깝다. 같은 이름의 레포가 GitHub에 여럿 있고(turboquant_plus, 0xSero/turboquant, tonbistudio/turboquant-pytorch), 각자 다른 디자인 선택·다른 최적화를 적용한다. 내가 본 건 그중 파이썬 참조 구현(tonbistudio) 하나의 그림이지, TurboQuant 계열 전체의 평가가 아니다.
이번 실험 덕에 앞으로 KV 양자화 기법을 볼 때 먼저 체크할 것들이 손에 잡혔다. 세 가지 정도. (1) attention이 compressed domain에서 직접 도는 구조인가, 아니면 decompress-then-attend인가. (2) 논문·레포의 압축비 숫자가 저장소의 숫자인가 피크 VRAM의 숫자인가. (3) 모델 크기 스케일링을 어떤 범위에서 검증했는가 — "6× 메모리 절감"이라고 쓰여 있다면 어느 크기 모델에서 관측된 숫자인지.
이 블로그의 GitHub 탐방 편을 쓰는 이유 자체가, 논문·레포 헤드라인 숫자가 내 환경·내 유즈케이스에서도 같은 숫자로 나오는지 손으로 한 번 돌려보는 데 있다. 이번처럼 "어느 전제 위에서 그 숫자가 성립하는가"를 하나씩 구분해서 읽을 수 있게 되는 것, 그 자체가 가장 큰 수확이었다.
TurboQuant trending 1위인 TheTom/turboquant_plus(6.4k stars)는 llama.cpp 확장으로 구성되어 있고 Boundary V·sparse V dequantization 같은 실전 최적화가 붙어 있다. 이 글의 후속편에서 같은 Qwen2.5-7B/14B 모델(GGUF 변환)에 대해 같은 needle 태스크를 재현해볼 계획이다. 연구용 구현(tonbistudio)에서 깨진 구간이 실전용 구현(TheTom)에서 어떻게 달라지는지 — 혹은 달라지지 않는지 — 를 보는 게 목적.
이 글에서 돌린 레포
같은 주제의 다른 trending 레포들 (star 순)
원 논문·블로그