
Metal 환경에서 삼진 모델(Ternary Model)인 Bonsai 8b를 파인튜닝(Fine-tuning)해 보았습니다
요약
PrismML의 Bonsai 8B와 같은 진정한 sub-2-bit 삼진(Ternary) 모델을 Metal 환경에서 파인튜닝하는 방법을 다룹니다. 기존 양자화 방식과 달리 네이티브 삼진 모델의 특성을 이해하고, Straight-through estimator를 활용한 양자화 인식 학습(QAT) 과정을 설명합니다.
핵심 포인트
- Bonsai 모델은 가중치가 -1, 0, +1인 진정한 sub-2-bit 모델임
- 기존의 imatrix, AWQ, GPTQ 방식은 네이티브 삼진 모델에 적용되지 않음
- Straight-through estimator를 사용한 QAT 방식으로 파인튜닝 가능
- 학습 시 fp16 잠재 가중치를 유지하며 실시간 삼진화 연산 수행
우리는 바이너리(Binary) 및 삼진(Ternary) 모델에 대한 화제성을 들어왔으며, 최근 이들의 성능이 다양한 기기에서 일상적인 사용을 위해 경쟁력을 갖추며 따라잡기 시작한 것으로 보입니다. 물론 아직 개선할 점은 남아있지만 말이죠...
이 모델들이란 무엇인가: 진정한 sub-2-bit LLM의 새로운 제품군이 출시되었습니다: PrismML의 Bonsai 라인업입니다. 모든 가중치(Weight)는 삼진(-1/0/+1) 또는 바이너리(-1/+1)이며, 128개의 가중치 그룹당 하나의 공유된 fp16 스케일(Scale)을 가집니다. 삼진 모델은 진정한 약 ~1.7 bits/weight에 도달하며, 바이너리 모델은 약 ~1.1에 도달합니다. 이는 고정밀 텐서(Tensor)를 포함하여 평균을 내면 실제로는 약 ~2.8 bits가 되는 기존의 "2-bit" GGUF 양자화(Quantization) 모델들과 달리, 진정한 sub-2-bit 모델입니다.
알아둘 만한 두 가지 사항: 이들은 BitNet 방식으로 만들어지지 않았습니다. BitNet은 1.58-bit 모델을 처음부터 사전 학습(Pretrain)합니다. 반면 Bonsai는 독자적인 방법을 사용하여 기존의 사전 학습된 모델(Qwen3)을 삼진 모델로 변환하므로, 이미 원하던 모델을 그대로 유지할 수 있습니다. 그들의 새로운 플래그십 모델은 27B이며, 제가 사용한 릴리스는 초기 버전인 8B(Ternary-Bonsai-8B, 삼진 Qwen3-8B)입니다. PrismML 측에서도 초기 1.7B-8B 릴리스는 추론(Reasoning)이나 신뢰할 수 있는 도구 사용(Tool use)을 목표로 하지 않았다고 명시했으며, 이는 아래의 제 결과에 중요한 영향을 미칩니다. 8B 모델은 약 2 GB로 구동되지만, 포맷이 새롭기 때문에 이를 실행하려면 PrismML의 llama.cpp 포크(Fork) 버전이 필요합니다(업스트림 PR 대기 중).
제가 궁금했던 질문: 자신의 데이터로 모델을 특화(Specialize)할 수 있는가? 사후 양자화 보정(Post-hoc quant calibration)은 여기에서 아무런 역할을 하지 못합니다. imatrix, AWQ, GPTQ는 전체 정밀도(Full-precision) 원본과 저비트 그리드(Low-bit grid) 사이의 간극인 "양자화 반올림 오차(Quantization rounding error)"를 복구하기 위해 존재합니다. 네이티브 삼진(Native-ternary) 모델은 그러한 간극이 없습니다. 체크포인트 자체가 이미 정확히 scale * {-1,0,+1} 이기 때문에, 2-bit GGUF는 손실 없는 재인코딩(Lossless re-encode)이 되며, 이는 이러한 기술들이 이 부류의 모델에는 작동하지 않음을 의미합니다.
그것을 파인튜닝(Fine-tuning)하는 방법: Bonsai의 생성 방식은 독점적(Proprietary)이지만, 출시된 삼진 모델(Ternary model)을 계속 학습시키는 것은 Straight-through estimator를 사용하는 표준적인 양자화 인식 학습(Quantization Aware Training, QAT)이며, 이 부분은 (AI의 도움을 받는다면) "쉽게" 재현 가능합니다: 압축 해제된 체크포인트(fp16 가중치 = 큰 컨테이너에 담긴 삼진 값들)를 로드합니다. 순전파(Forward pass): 각 가중치 그룹(128개 가중치 단위)별로 실시간으로 각 가중치를 삼진화(Ternarize)합니다. 모델은 삼진 가중치(Ternary weights)로 연산을 수행합니다. 역전파(Backward pass): Straight-through estimator를 사용하여 그래디언트(Gradients)가 fp16 잠재 가중치(Latent weights)로 흐르게 합니다. 학습합니다. 잠재 값(Latents)이 표류(Drift)하면 매 단계마다 다시 삼진화하며, 잠재 값이 임계값(Threshold)을 넘으면 가중치의 부호가 바뀝니다. 다시 삼진화한 후, 원본과 동일한 용량을 갖도록 2비트 GGUF로 다시 패킹(Re-pack)합니다. 배포된 가중치에서 초기화하면 0단계에서 모델을 정확히 재현하므로, 실제 모델을 파인튜닝하게 됩니다. 가장 좋은 점은 CUDA 없이 완전히 Metal 환경에서 수행할 수 있다는 것이며, 비록 속도는 약간 느리지만 말입니다. 네이티브 삼진 모델(Native-ternary model)에서 모든 가중치는 -1, 0, +1 중 하나의 값으로 저장됩니다. 이를 가중치의 코드(Code)라고 부릅니다. 또한 128개 가중치 그룹마다 하나의 fp16 스케일(Scale)을 공유하므로, 가중치가 나타내는 실제 숫자는 코드 * 스케일입니다. 코드 플립(Code flip)은 학습 과정에서 가중치의 코드가 예를 들어 0에서 +1로, 또는 +1에서 -1로 바뀌는 것을 의미합니다. 이것이 모델의 실제 로직이 변하는 유일한 방법입니다. 학습은 공유된 스케일을 미세하게 조정(Nudge)할 수도 있는데, 이는 볼륨 노브를 올리고 내리는 것과 같아서 아무것도 재배선(Rewire)하지 않습니다. 앞으로 보게 되겠지만, "코드가 플립되었는가, 아니면 스케일만 움직였는가"가 모델이 무언가를 실제로 배우는 것과 단순히 배우는 것처럼 보이는 것 사이의 정확한 경계선입니다. 부분적 결과(예비): 작업: SWE-rebench-v1(Python 전용으로 추출된 10개의 실제 이슈)을 기준으로 측정하여, 더 나은 에이전트형 코더(Agentic coder)로 만들기.
| run | patch rate | pass rate | training loss | notes |
|---|---|---|---|---|
| base 8B (no fine-tune) | 50% | 0% | - | - |
| QAT, last 18 layers | 40% | 0% | ~1.0 | looped badly (one command repeated up to 553x) |
| QAT, gradient-influential layers | 40% | 0% | ~1.0 | looping fixed, clean runs |
| QAT, ALL 36 layers | 30% | 0% | 0.91 | best-behaved, worst patch rate |
사후에 발견한 두 가지 주의 사항이 있으며, 두 가지 모두 같은 방향을 가리키고 있습니다. 첫째, 이 실행들은 stop-token masking 버그가 있는 코퍼스(corpus)로 학습되었기 때문에, 일부 예산(budget)이 잘못된 목표에 소모되었습니다. 둘째, 더 중요한 점은 모든 레이어(all-layers)를 학습시킨 실행이 다른 어떤 실행보다 학습 손실(training loss)을 더 낮게(LOWER) 유도했음에도 불구하고, 가장 최악의 패치 성공률(patch rate)을 기록했다는 것입니다. 가장 낮은 손실이 가장 능력이 떨어지는 에이전트를 만들어냈습니다. 이 모델은 가장 깔끔했지만(가장 적은 단계, 루프 없음, 가장 깨끗한 도구 호출), 가장 적은 수의 이슈를 해결했습니다. 이는 어느 정도의 과적합(over-fitting) 가능성을 시사하며, 로그가 적절한 문제 해결 과정을 충분히 가르치지 못하고 있음을 나타낼 수도 있습니다. 왜냐하면 저 역시 한 번에 하나의 작업에 구체적으로 집중하는 짧고 간결한 대화보다는, 길게 늘어지는 매우 긴 대화 로그를 가지고 있는 것에 유죄이기 때문입니다. 이는
통과하지 못하는 패치(patch)는 동일한 모방 함정(mimicry trap)에 빠지게 되므로 필터링됩니다. 승리한 궤적(winning trajectories)들은 고정된 마스킹(masking)과 함께 삼진 모델(ternary model) 자체의 토크나이저(tokenizer)를 통해 다시 렌더링되므로, 모델은 로그의 스타일이 아니라 작동하는 솔루션의 형태(SHAPE)를 학습하게 됩니다. 발생한 문제들은 제가 채점하는 문제들과 서로 분리되어 있으므로, 성능 향상은 일반화(generalization)를 의미합니다. 이 첫 번째 실행에서는 단 12개의 검증된 궤적(verified trajectories)만을 사용했습니다 (신호가 존재하는지 확인하기 위해 의도적으로 작게 설정했습니다). 그 결과는 학습률(learning rate, lr)에 대한 교훈을 주었으며, 이는 이 글 전체에서 가장 중요한 부분입니다. lr 3e-4: 모델이 전혀 학습하지 못했습니다. 코드 반전(code flips)이 전혀 없었습니다 (기껏해야 0.003%). 손실(loss)은 0.6까지 부드럽게 떨어졌지만, 이는 순수하게 삼진 그룹(ternary groups)의 리스케일링(rescaling)에 의한 것이었습니다. 실제 -1/0/+1 할당은 전혀 움직이지 않았습니다. 행동은 변하지만 능력은 변하지 않습니다. 이는 지난 섹션에서 언급한 함정이며, 상당히 일반적인 학습률에서도 여전히 발생하고 있었습니다. lr 1e-3: 실제 코드 반전이 일어났지만, 모델을 망가뜨렸습니다. 이제 코드들이 움직였습니다 (첫 번째 레이어에서 3.8%, 수백만 개의 가중치). 하지만 단 12개의 궤적만으로는 너무 공격적이었습니다. 모델이 기존에 가진 도구 사용(tool-use) 능력을 덮어쓰고 고작 12개의 예시를 암기해 버렸습니다. 패치 성공률(patch rate)은 20%로 떨어졌고, 도구 오류(tool errors)는 73%로 치솟았으며, 몇 단계 만에 학습을 포기했습니다. lr 5e-4로 약 2 에포크(epochs): 최적의 지점(sweet spot)이었습니다. 적절한 수준의 반전(전체 코드의 0.7%)이 일어났고, 손실은 0.01까지 암기하듯 떨어지는 대신 0.5 근처에서 안정되었습니다. 도구 사용 능력도 온전했습니다. 이후 저는 동일한 레시피를 12개에서 30개의 검증된 궤적으로 확장하여 두 가지 분할(split)에 대해 다시 측정했습니다: 서로 분리된 홀드아웃 문제(held-out issues, 일반화)와 학습에 사용된 정확한 문제(in-distribution)입니다. 모든 평가는 lr 5e-4, 약 2 에포크, 36개 레이어 전체를 대상으로 수행되었으므로 직접 재현이 가능합니다.
run eval split patch rate pass rate base 8B (no fine-tune) held-out 50% 0% 5e-4, 12 trajectories held-out (generalization) 40% 0% 5e-4, 12 trajectories trained-on (in-distribution) 25% 8% 5e-4, 30 trajectories held-on (generalization) 50% 0% 5e-4, 30 trajectories trained-on (in-distribution) 43% 0% 마지막 두 행을 주의 깊게 읽어주세요. 이것이 정직하고 재현 가능한 상태이기 때문입니다. 30개의 검증된 궤적(trajectories)을 사용했을 때, 모델은 보지 못한 이슈(unseen issues)에 대해 베이스 모델(base model)의 50% 패치율(patch rate)과 동일한 수준에 머물렀으며, 통과율(pass rate)은 0%였습니다. 모델은 베이스 모델과 동일한 속도로 패치를 작성하지만, 그 중 어떤 것도 숨겨진 테스트를 통과하지 못합니다. 동작 자체는 12개일 때보다 실제로 더 건강해졌지만(도구 오류율(tool-error rate)이 감소했고, 중단하는 대신 계속 참여함), 홀드아웃(held-out) 저장소에서는 아무것도 해결하지 못합니다. 12개 궤적에서 나타났던 유일한 희망이었던 8%의 인-디스트리뷰션(in-distribution) 해결 사례는 30개에서는 재현되지 않았습니다. 따라서 저는 이를 추세가 아닌 노이즈(noise)로 해석합니다. 즉, 운 좋게 해결된 단 하나의 이슈였으며, 훈련 분포(training distribution)가 변화하면서 상쇄된 것입니다. 남은 것은 데이터를 추가함에 따라 베이스라인으로 다시 올라가는 패치율과, 보지 못한 이슈에 대해 완강하게 0%에 머물러 있는 통과율뿐입니다. 따라서 재현 가능한 결론은 의도적으로 화려하지 않습니다. 이 2비트(2-bit) 모델을 파인튜닝(fine-tune)하여 베이스 모델의 일반화 패치율(generalization patch rate) 수준까지 맞출 수는 있지만, (아직은) 실제로 더 많은 테스트를 통과하게 만들 수는 없다는 것입니다. 보지 못한 이슈에 대해 실제로 반복 가능한 통과(pass)를 얻어내는 것은 해결된 문제가 아니라 여전히 남아 있는 과제(open problem)입니다.
다음 단계는 무엇인가
자동화된 방식으로 더 많은 데이터 확보하기. 저는 새로운 검증된 궤적을 계속 생성하고, 고정된 5e-4 / 2-에포크(epoch) 레시피로 재학습하며, 일반화 통과율(generalization pass rate)이 마침내 0을 돌파할 때까지(또는 정체기에 도달할 때까지) 두 분할(splits)을 모두 다시 측정하는 루프를 가지고 있습니다. 12개에서 30개로 늘렸을 때 패치율은 베이스라인으로 회복되었지만 통과에는 아무런 도움이 되지 않았으므로, 핵심 질문은 50개, 100개, 1000개의 궤적이 결국 통과 곡선을 위로 끌어올릴 것인지, 아니면 0에 고정된 채로 머물 것인지입니다.
단순히 동작뿐만 아니라 로짓(logits)을 증류(distill)하기. 만약 데이터 추가가 정체기에 도달한다면, 유력한 해결책은 소프트 라벨 증류(soft-label distillation)가 될 것입니다.
제가 수집한 솔버(solver)는 다른 토크나이저(tokenizer)를 사용하므로, 그 동작을 모방할 수밖에 없습니다. 하지만 이 삼진 모델(ternary model)은 변환된 Qwen3-8B이며, 동일한 어휘 사전(vocabulary)을 사용하는 해당 베이스 모델의 강력한 SWE 파인튜닝(fine-tune) 모델들이 존재합니다. 저비트 QAT(low-bit-QAT) 문헌에 따르면, 모든 토큰에서 교사 모델(teacher)의 전체 출력 분포로 학습하는 것이 2비트에서의 원-핫 타겟(one-hot targets)보다 효과적이며, 토큰 모방만으로 충분하지 않을 때 시도해 볼 수 있는 자연스러운 다음 단계입니다.
Metal / MPS 함정(유용한 부분) 중 하나는 foreach=False가 필수라는 점입니다. MPS 멀티 텐서(multi-tensor, foreach) 옵티마이저(optimizer)/클립(clip) 커널은 전체 모델 규모에서 데드락(deadlock)이 발생합니다. 증상은 "5단계에서 학습이 멈춤"입니다. 이를 AdamW와 clip_grad_norm_으로 넘기세요. 시퀀스 길이(Sequence length)는 4096 이하로 설정해야 합니다. 8192로 설정하면 "MPSGraph tensor dims larger than INT_MAX" 오류가 발생합니다(MPS에는 플래시 어텐션(flash attention)이 없습니다). 잠재 변수(latents)는 bf16이 아닌 fp32를 사용하세요. bf16을 사용하면 값이 폭발하거나, 아주 작은 업데이트가 삼진 임계값(ternary threshold) 미만으로 언더플로우(underflow)되어 가중치가 전혀 바뀌지 않습니다.
옵티마이저 설정에 따라 몇 개의 레이어(layer)를 수용할 수 있는지가 결정됩니다. AdamW는 두 개의 fp32 상태(state)를 유지하며(~56GB 추가 필요), 128GB 메모리에서도 스왑(swap)이 발생합니다. Adafactor는 인수 분해된 상태(factored state)를 사용하여 36개 레이어 전체를 약 70GB 내에 수용할 수 있습니다. 레이어는 위치가 아닌 그래디언트(gradient) 중요도에 따라 선택하세요. 이 모델의 경우 초기 레이어가 작업 그래디언트의 거의 전부를 담당하며, 중간 레이어들은 거의 죽어 있는 상태입니다.
손실(loss)을 도구 호출(tool-call) 토큰에 마스킹(masking)하고, 도구 스키마(tool schemas)를 채팅 템플릿(chat template)에 렌더링하여, 상용구(boilerplate)가 아닌 스키마 조건부 도구 사용을 학습시키세요. 정지 토큰(stop token)에 라벨을 붙이지 않으면 모델은 멈추는 법을 배우지 못합니다. 이 부분이 저에게 가장 큰 비용을 치르게 했습니다. 제가 적용한 마스킹은 어시스턴트(assistant) 콘텐츠에 라벨을 붙였지만, <|im_end|> 종료 토큰 한 개 전에서 범위를 끝내버렸습니다. 표준적인 인과적 언어 모델(causal-LM) 라벨 시프트(label shift) 하에서 이는 전체 코퍼스(corpus) 중 어떤 위치에서도 정지 토큰이 타겟이 된 적이 없음을 의미하며, 결과적으로 모델이 자신의 턴을 종료하는 방향으로 그래디언트를 전혀 받지 못하게 만들었습니다. 병적인 루핑(pathological looping, 명령어가 수백 번 반복되는 현상)의 진짜 원인은 학습의 "지속성 교육" 문제가 아니라 바로 이것이었습니다. 만약 파인튜닝한 모델이 루핑을 발생시킨다면, 다른 무엇보다도 손실(loss)이 실제로 턴 종료(end-of-turn) 토큰을 포함하고 있는지 확인하십시오.
손실(loss)뿐만 아니라 코드 플립(code flips)을 관찰하십시오. 삼진 네트워크(ternary net)의 경우, 잠재값(latent)이 부호 임계값(sign threshold)을 넘을 때만 가중치(weight)가 변경됩니다. 학습률(LR)이 낮으면 옵티마이저(optimizer)는 단순히 그룹별 스케일(per-group scales)만 미세하게 조정할 뿐 단 하나의 코드도 플립시키지 않으므로, 실제 삼진 가중치(ternary weights)는 거의 움직이지 않으면서 손실(loss)만 떨어지게 됩니다. 체크포인트(checkpoint)당 얼마나 많은 코드가 플립되는지 기록하십시오. 만약 이 수치가 0에 가깝다면, 손실 곡선(loss curve)은 당신을 속이고 있는 것이며 학습률(LR)이 너무 낮은 것입니다. [Links] QAT 코드, 제가 이 모든 작업을 수행하는 데 사용한 리포지토리(repo), 그리고 docs/ternary_qat.md에 있는 재사용 가능한 파이프라인 가이드. LogMiner, 저의 Claude Code / OpenCode / Qwen Code 세션을 첫 번째 (로그) 학습 코퍼스(training corpus)로 스크래핑한 도구. 2비트 모델: prism-ml/Ternary-Bonsai-8B (삼진 Qwen3-8B). prism-ml 조직(org) 전체에는 27B 플래그십을 포함한 Bonsai 라인이 있습니다. 제가 증류(distilled)에 사용한 솔버(solver): Ornith-1.0-9B, 저의 자체 에이전틱 코더(agentic coder). 이슈(issues): nebius/SWE-rebench, 숨겨진 pass/fail 테스트가 포함된 실제 GitHub 버그들. 로짓 증류(logit-distillation) 아이디어를 위한 동일 어휘(same-vocab) 교사 모델(iter-6): SWE-Lego/SWE-Lego-Qwen3-8B 및 Open-Bee/Bee-8B-RL, 둘 다 Qwen3-8B 파인튜닝(fine-tunes) 모델이므로 Bonsai 토크나이저(tokenizer)를 공유합니다. 시도해 보십시오. 충분한 통합 메모리(unified memory)를 갖춘 Apple Silicon만 있으면 됩니다 (이 작업에는 128GB의 M4 Max를 사용했습니다). 2비트 모델은 Mac에서 학습할 수 있으며, 일반화 패치 비율(generalization patch rate) 측면에서 베이스 모델과 일치할 수 있습니다. 모델은 분명히 학습합니다 (코드가 플립되고, 분포 내(in-distribution) 동작이 변화함). 이를 홀드아웃 리포지토리(held-out repos)에 대한 반복 가능한 패스(pass)로 만드는 것은 여전히 미해결 과제이며 진행 중인 도전입니다. 만약 그 단계에 도달한다면, 어떻게 했는지 꼭 알려주세요. 어쩌면 MLX가 더 나은 솔루션을 가지고 있을지도 모릅니다? 여기까지 읽어주셔서 감사합니다. 다음 업데이트를 기대해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 r/LocalLLaMA의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기