마침 동적 양자화 전략 두 가지를 정의해서 각각의 저정밀 정적 모델 케이스와 비교해봤어. 이 동적 양자화 전략들도 둘 다 24GB GPU에 맞춘 컨텍스트 제한을 걸었어.
IQ3_S 비교
이 전략에서는 최악의 경우 IQ3_S 양자화를 사용하는 멀티 양자화 전략으로 작업을 돌렸어.
- 작업 시작: Q6_K, f16 kvcache, 최대 컨텍스트 54,272
- 중간 교체: Q4_K_XL, f16 kvcache, 최대 컨텍스트 109,312
- 최종 교체: IQ3_S, f16 kvcache, 최대 컨텍스트 192,096
데이터에서 이 전략은 Q6→Q4→Q3, f16 KV라고 표시되어 있을 거야.
Q4_K_XL, q8_0 kv 비교
이 전략에서는 최악의 경우 Q4_K_XL, q8_0 kv 양자화를 사용하는 멀티 양자화 전략으로 작업을 돌렸어.
- 작업 시작: Q6_K, f16 kvcache, 최대 컨텍스트 54,272
- 중간 양자화: 모델 kvcache를 q8_0으로 변환. 최대 컨텍스트: 91,136
- 중간 교체: Q4_K_XL, f16 kvcache, 최대 컨텍스트 109,312
- 최종 양자화: 모델 kvcache를 q8_0으로 변환. 최대 컨텍스트: 183,296
(중간 실행 중 양자화는 지난 글에서 설명한 핫 리로드 방식을 사용했어. f16<->q8_0 변환은 동일한 llama.cpp 포크에서 처리해.)
데이터에서 이 전략은 Q6/f16→Q6/q8→Q4/f16→Q4/q8이라고 표시되어 있을 거야.
작업
모델이 방대한 입력 텍스트를 분석하고, 여기저기 흩어진 지시사항을 따라 비밀 값을 찾아내도록 하는 NIAH("건초 더미 속 바늘 찾기") 태스크를 수십 개 만들었어. (소스 자료 일부는 gkamradt/needle-in-a-haystack에서 참고함)
멀티 퀀트 전략의 일관성을 평가하는 데 NIAH가 합리적이라고 생각해서 이걸 선택했지. 각 태스크는 모델이 방해용 텍스트 속에 숨겨진 일련의 '단계'를 추론해야 하거든. 그러니까 KV 캐시를 이식받은 모델은 소스 모델이 멈췄던 지점부터 사고의 흐름을 정확하게 이어받을 수 있어야 해.
게다가 NIAH는 복잡한 테스트 환경이 필요 없고, 정답이 확실하게 갈리니까 딱 좋지.
각 태스크와 테스트 케이스마다 다음 항목들을 측정했어.
결과 (정답/오답)
생성된 총 토큰 수
소요된 총 시간
퀀트 방식마다 프리필/디코드 속도는 비슷하지만, 멀티 퀀트 방식이 단일 퀀트 전략보다 눈에 띄게 느려지지 않는다는 걸 보여주려고 소요 시간을 포함했어. KV 캐시를 한 퀀트에서 다른 퀀트로 옮기면 프리필 과정을 반복할 필요가 없거든!
결과
내가 설계한 벤치마크 태스크는 총 180번의 개별 실행으로 이루어졌고, 내 GPU로 완료하는 데 14시간 26분이 걸렸어.
여기서 가장 골칫덩이는 IQ3_S/f16 전략이었어. 특히 무거운 태스크에서 5만 개가 넘는 추론 토큰을 계속 뱉어내더니, 결국 응답도 못 내놓고 컨텍스트 윈도우(~196k)를 꽉 채워버리는 경우가 너무 많았거든.
그래도 짧은 태스크에서는 나름대로 잘 버텼어. Q6/f16과의 일치도 면에서는 Q4_K_XL이나 q8_0보다 높은 점수를 받기도 했으니까. 뭐, 고도의 추론 능력 덕분이라고 해두자고!
Q6/f16과의 일치도 얘기가 나와서 말인데, 이건 내가 꼭 추적해야 할 중요한 지표였어. 동적 접근 방식이 고정밀 레퍼런스 모델을 얼마나 비슷하게 흉내 낼 수 있는지 비교하고 싶었거든.
Q6/f16과의 일치도
SEE IMAGE 1
이 그래프는 최종 답변이 Q6/f16 결과와 완전히 일치하는 태스크의 수를 보여줘. 답이 틀렸더라도 말이야. 여기서 의도한 건 동적 양자화 전략이 Q6/f16을 근사할 수 있는지 확인하는 거였는데, 이 그래프가 그걸 아주 잘 보여준다고 봐!
두 경우 모두 동적 퀀트가 정적 퀀트보다 Q6/f16 모델의 결과와 훨씬 더 비슷하게 맞아떨어져. 전반적으로 IQ3_S를 피하는 경로가 높은 컨텍스트에서 Q6에 훨씬 더 가까워지는데, 이건 뭐 그리 놀라운 결과도 아니지!
태스크 정답 여부에 너무 큰 의미를 두고 싶지는 않아서 "Q6/f16과의 일치도"에 집중했어. 내 NIAH 태스크가 전반적인 성능을 완벽하게 대변한다고 확신할 수는 없으니까. (그래도 궁금할까 봐 태스크 정답 결과도 아래에 같이 넣어뒀어.)
평균 추론 시간 및 평균 출력 토큰
SEE IMAGES 2 and 3
이 그래프들은 오답이나 컨텍스트가 고갈된 실행을 포함해서, 완료된 태스크 시드 전반의 추론 시간(초)과 총 출력 토큰의 산술 평균을 보여줘.
특히 추론 시간을 강조하고 싶었는데, 동적 전략의 경우 모델 가중치를 교체하고 KV 캐시를 양자화하는 시간까지 포함되어 있기 때문이야!
이게 여기서 얻는 이점을 잘 보여준다고 생각해. 복잡한 동적 양자화 전략을 쓴다고 해서 태스크 전반의 추론 시간이 느려지지 않거든. 이유는 다음과 같아:
모델 가중치를 교체할 때(예: Q6->Q4), KV 캐시를 한 퀀트에서 다른 퀀트로 바로 옮기는 직접적인 KV 캐시 이식을 수행하기 때문이지.
- 기존 모델의 kvcache를 양자화할 때(예: Q6/f16->Q6/q8), 나는 라이브 모델의 컨텍스트/런타임을 즉시 새로고침하고 kvcache 정밀도 간 자동 변환을 수행하는 내 llama.cpp 포크 버전을 사용 중임.
요약하자면, 두 경우 모두 prefill을 다시 할 필요가 없음! 전환 후에도 모델은 딱 멈췄던 지점부터 바로 prefill/decode를 이어감.
작업 정확도
모든 전략/실행에 대한 작업 정확도 표임. 정적-동적 비교를 쉽게 하려고 "사용된 최저 모델 정밀도"별로 결과를 나눴음.
최악의 경우 IQ3_S:
Strategy
10k
25k
50k
75k
Q3/f16
6/10
9/10
2/10
4/10
Q6->Q4>Q3
7/10
7/10
7/10
4/10
결과: 동적 양자화가 정적보다 2번 이기고, 1번 비기고, 1번 짐.
최악의 경우 Q4_K_XL, q8 kv:
Strategy
10k
25k
50k
75k
Q4/q8
7/10
8/10
5/10
4/10
Q6->Q6/q8->Q4->Q4/q8
7/10
7/10
7/10
7/10
결과: 50k 컨텍스트 이상에서는 동적 양자화가 정적보다 낫고, 그 이전에는 거의 비슷함.
전체:
여기서는 10k와 25k 작업을 제외하고 동적 전략을 레퍼런스와 직접 비교했음. 그 이하 수준에서는 동적 전략이 그냥 Q6/f16을 돌리는 거랑 똑같거든. 매번 결과가 같음.
Strategy
50k
75k
Q6->Q4->Q3
7/10
4/10
Q6->Q6/q8->Q4->Q4/q8
7/10
7/10
Q6/f16
6/10
8/10
결과: IQ3_S 양자화가 높은 컨텍스트에서 완전히 맛이 간다는 점 말고는 딱히 뽑아낼 결론이 없는 듯. 이 표를 보면 내가 왜 작업 정확도를 그렇게 심각하게 안 봤는지 알 수 있을 거임. 문자 그대로 받아들이면 50-75k 컨텍스트 사이에서는 Q6->Q4랑 Q6->Q6/q8이 Q6/f16보다 더 낫다는 소리가 되니까!
결론
전반적으로 이번 결과 아주 만족스러움!
이 벤치마크가 완벽하진 않지만, 내 목적상 동적 모델 양자화가 하드웨어 제약으로 인한 일반적인 정밀도 손실을 상쇄하는 좋은 방법이라는 점은 충분히 확인했음.
테스트는 주로 24 GiB GPU의 한계를 밀어붙이는 쪽으로 맞췄는데, 32 GiB GPU에서만 돌아가는 레퍼런스랑 비교하기가 더 쉬워서 그랬음. 다음 단계로는 이걸 내 추론 설정에 통합해서, 온갖 기능(speculative decoding이랑 mmproj인데, 이번 벤치마크에선 둘 다 안 켰음)을 다 켰을 때 얼마나 잘 버티는지 볼 생각임.
내 직감으로는 실전 추론에서 이런 전략을 쓸 때 핵심은 모델 양자화 단계를 너무 자주 바꾸지 않는 거임. 일관성은 괜찮을 것 같긴 한데, 가중치를 교체하는 데 드는 시간이 어느 순간부터는 체감될 거거든. 그래서 모델을 ~40-50k 토큰 동안은 안 바꾸고 돌리는 식으로 큰 "구간(tranche)"을 만들어서 설정해 볼 생각임.
내 생각에 Q6/f16->Q6/q8->Q4/f16->Q4/q8 전략은 이미 이걸 아주 잘 보여주는 예시임. 적어도 내가 수정한 llama.cpp에서는 모델 다시 불러오는 것보다 kvcache 다시 불러오는 게 시간이 훨씬 덜 걸리거든. 나중에 이 부분도 좀 더 손봐야겠음!
주요 댓글
r/localllama
사용자들은 llama.cpp의 기존 기능을 활용한 기술적 구현 가능성을 언급하면서도, 작성자의 평가 방법론이 가진 정답 확인의 한계와 데이터 해석의 신뢰성에 대해 비판적인 토론을 이어가고 있습니다.
25
세상에, OpenAI나 Anthropic이 모델을 양자화하는 방식을 우연히 발견한 것 같아. 이거야. 다들 이걸 보고 있는 게 분명해. '우린 항상 fp16을 쓴다'는 그들의 주장이 거짓말이라는 건 뻔했지만, Opus나 GPT 벤치마크를 다시 돌리는 추적 사이트들에서는 실제 변화가 관찰되지 않았거든.
다들 이걸 보고 있는 거야. 그들이 처음부터 양자화된 모델을 서비스하는 게 아니라, 컨텍스트가 커짐에 따라 요청을 더 많이 양자화된 버전으로 넘기는 거지. TTFT가 무작위로 증가하거나 연결이 끊기는 현상도 이걸로 설명이 돼.
3
단일 머신에서 성능을 극대화할 때는 말이 되지만, OAI나 Anthropic이 최적화하려는 배치 처리나 병렬화와 잘 맞을지는 모르겠네.
난 전혀 모르겠어! 😄
4
이거 진짜 손수 다 썼고 시간도 엄청 걸렸어. 포맷에 대해 불평하는 거라면 미안해. 마크다운 초안을 레딧에 표시되는 형태로 바꾸는 게 너무 힘들더라고.
근데, 맞아. 이 포스트 쓰는 데 LLM은 안 썼어.
3
흥미로운 접근 방식이네. '평범한' 소비자용 머신을 쓰는 사람들에게 가장 큰 단점은 처음부터 큰 모델 양자화가 필요하고, 첫 n개의 토큰에 대해 큰 KV 캐시를 결합해야 한다는 점이야. 즉, VRAM이 제한적이라면 시도조차 하기 어렵다는 거지.
3
반대로, 어떤 크기의 머신을 쓰든 이 결과는 광범위하게 적용 가능하다고 생각해. 그 주된 이유는 내 경험상 대부분의 사람들이 이미 (하드웨어 대비) '중간 크기'의 양자화를 선택하고 있기 때문이야. 그게 허용 가능한 수준의 컨텍스트를 담을 수 있게 해주니까.
내 작업의 요점은 현재 실행 중인 것보다 '약간 더 큰 양자화'를 선택해서 컨텍스트가 확장됨에 따라 동적으로 교체할 수 있다는 걸 보여주는 거야. 기본적으로 현재의 '편안한 크기' 양자화가 최악의 시나리오가 되는 셈이지. 하지만 추론은 훨씬 더 높은 정밀도로 시작할 수 있어!
2
다음엔 도구 사용(tool-use) 벤치마크도 해주면 좋겠어. '바늘 찾기(needle in a haystack)'는 한 가지 측면일 뿐이고, 그 뒤에 이어지는 작업은 또 다른 문제니까.
2
나도 더 범용적이고 광범위하게 적용 가능한 벤치마크를 원하지만, 이 포스트에 나온 것만으로도 내 머신에서 14시간 넘게 걸렸어. 다시 하고 싶지는 않아 😄
2
허, 이거 진짜 깔끔하네. 중간에 양자화 방식을 바꿀 생각은 전혀 못 해봤어.
1
기본 아이디어는 똑똑해. 세션 시작 시 컨텍스트를 얼마나 적게 사용하든, VRAM 풀이 제공할 수 있는 최고의 정밀도를 항상 얻어야지.
컨텍스트가 커짐에 따라 비트를 낮출 방법이 필요해.
1
dwarfstar 같은 거랑 같이 하면 진짜 흥미롭겠다. 캐시랑 세션 저장 기능이 워낙 좋으니까. 그 엔진에서 같은 캐시를 사용하면서 모델을 전환하는 게 가능할지 궁금하네.
1
llama.cpp에서는 슬롯 저장/복구(/slots/{id}?action=save) 기능을 사용하면 이 작업의 대부분을 수행할 수 있어. 상태 파일은 어떤 모델이 작성했는지 기록하지 않거든. 다만 데이터 타입(dtype)과 레이아웃은 일치해야 해. fp16 K/V로 저장하고 q8_0 캐시를 실행하는 빌드에 로드하면 에러 대신 조용히 쓰레기 값이 나올 거야.
1
셀당 n=10, 베이스라인과의 정확한 문자열 일치 여부, 그리고 정답 확인은 없네. 두 모델이 똑같이 틀려도 일치할 수 있잖아. 교차 양자화(cross-quant) 이식보다 동일 양자화 교차 작업 셀의 점수가 낮게 나오는 게 네 생각보다 더 신경 쓰여야 할 부분 같은데.
1
맞아, 모델 내부 양자화 복구는 순정 llama.cpp에서도 작동해. kvcache 변환의 경우에는 내가 수정한 llama.cpp 포크를 사용했어.
1
정답 확인도 했어. 게시물 하단 표를 봐. 네가 언급한 그래프 바로 아래에 두 모델이 모두 틀린 경우의 일치 데이터도 포함되어 있다고 명시해 뒀거든.
내 방법론이 완벽하지 않다는 건 나도 솔직히 인정해! 이런 식의 교차 모델/양자화 평가 같은 건 해본 적이 없거든. 만약 내 전략들을 비교하고 싶다면 넌 어떻게 할 거야?
(물론 '샘플 사이즈 늘리기'는 제외하고 말이야. 당연히 그게 더 좋겠지만, 이 게시물 하나 쓰려고 GPU를 15시간이나 돌렸고, 나도 내 GPU로 다른 걸 좀 하고 싶거든!)
내 포스트는 32GB와 24GB 하드웨어 클래스에 관한 것이었지만, 16GB GPU에서도 같은 방법론을 사용해 품질을 높일 수 있을 거야.
1
아키텍처에 따라 다르다는 점에는 어느 정도 동의해. 많은 아키텍처에서는 아주 적은 컨텍스트만으로도 엄청난 KV가 생성되니까(K2 Horizon이나 Gemma 4를 봐). Qwen은 지금까지 이 규칙의 예외였고, 매우 가벼운 KV 아키텍처를 가진 Ling도 마찬가지야. 그래서 말이 되긴 하지만, 이미 상당히 효율적인 아키텍처에만 해당돼. FP16이나 Q8_0 KV 캐시로 4K 컨텍스트만 얻어도 VRAM이 꽉 차버린다면 이 방법은 안 통할 거야.
그래서 아이디어는 이해했어. Beellama(KV tail)와 비슷한 면이 있지만 더 포괄적인 방식인 것 같아. 그래도 제대로 작동하게 하려면 약간의 여유 공간은 필요할 거야.