1-bit LLM「Bonsai-8B」をMacでローカル実行する — セットアップからチャットUI構築まで
はじめに
「8Bパラメータのモデルが1.28GBに収まる」— 最初に聞いた時は信じられなかった。
PrismML が2026年3月末にリリースした Bonsai-8B は、重みを1ビットに圧縮した LLM。フルプレシジョン(FP16)なら16.38GBになるところを、92%圧縮して1.28GB。それでいてベンチマーク平均70.5と、フルプレシジョンの8Bモデルに匹敵する性能を出す。
これを MacBook Air M3(8GB)でローカル実行し、Svelte + FastAPI でチャットUIを構築するまでの全過程を記録する。
Bonsai-8B とは
| 項目 | 仕様 |
|---|---|
| パラメータ | 8.19B(~6.95B non-embedding) |
| アーキテクチャ | Qwen3-8B dense(GQA, SwiGLU MLP, RoPE) |
| 量子化 | 1-bit(0→−scale, 1→+scale)、128重みごとにFP16スケール係数 |
| 実効ビット幅 | 1.125 bits/weight |
| ディスクサイズ | 1.28 GB |
| コンテキスト長 | 65,536 tokens |
| ライセンス | Apache 2.0 |
各重みが「0か1か」の1ビット。128個の重みごとにFP16のスケール係数を1つ共有する。この構造のおかげでメモリ14x削減、推論6x高速、エネルギー5x削減を実現している。
性能レベルとしては GPT-3.5-turbo 相当。GPT-4oやClaude Sonnetには遠く及ばないが、分類・要約・簡単な質疑応答なら十分実用的。価値は賢さではなく「1.28GBでローカルで57tok/sで動く」こと。
セットアップ
前提環境
- MacBook Air 2024(M3, 8GB RAM)
- macOS(Darwin 23.6.0)
- Python 3.13
- Xcode.app が必要(Command Line Tools だけでは不可)
なぜ Xcode.app が必要か
Bonsai-8B の MLX 版は、PrismML が fork した カスタム MLX を使う。1-bit カーネルが含まれているため、標準の pip install mlx では動かない。
この fork をソースビルドするには Metal コンパイラが必要で、Xcode.app に含まれる metal ユーティリティがないとビルドが失敗する。Command Line Tools だけでは xcrun: error: unable to find utility "metal" になる。
# Xcode.app インストール後
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
# 確認
xcrun --find metal
# → /Applications/Xcode.app/Contents/Developer/Toolchains/.../metal
PrismML fork MLX のインストール
pip install "mlx @ git+https://github.com/PrismML-Eng/mlx.git@prism"
初回ビルドは数分かかる。成功すると mlx-0.31.2.dev... がインストールされる。
モデルのダウンロードと動作確認
from mlx_lm import load, generate
model, tokenizer = load("prism-ml/Bonsai-8B-mlx-1bit")
response = generate(
model,
tokenizer,
prompt="Explain quantum computing in simple terms.",
max_tokens=256,
)
print(response)
初回はモデルダウンロード(~1.3GB)が走る。2回目以降は ~/.cache/huggingface/ からキャッシュ読み込み。
ベンチマーク
MacBook Air M3(8GB)での実測値:
| テスト | max_tokens | 生成tokens | 時間 | tok/s |
|---|---|---|---|---|
| 短文EN | 32 | 31 | 0.79s | 39.4 |
| 中文EN | 128 | 128 | 2.28s | 56.2 |
| 長文EN | 512 | 512 | 8.79s | 58.2 |
| 短文JP | 32 | 32 | 0.70s | 45.4 |
| 中文JP | 128 | 128 | 2.29s | 55.9 |
| 長文JP | 512 | 512 | 8.92s | 57.4 |
- 安定速度: 55-58 tok/s(長文で安定、短文は初期化コストで低め)
- メモリ: Active 1.19GB / Peak 1.23GB
- 参考: 公式の M4 Pro では 131 tok/s
日本語でも英語とほぼ同等の速度が出る。
品質テスト
| テスト | 結果 |
|---|---|
| 推論(三段論法) | 思考は正しいが結論を明示せず続く |
| コード生成 | 関数定義前に説明が始まり不完全 |
| 事実(JFK) | ハルシネーション(Polk と誤答) |
| 数学(17×23) | 計算過程を説明するが答えに到達しない |
| 日本語推論 | 正答するが同じ文を7回繰り返す |
1-bit の限界が見える。事実の記憶が弱く、繰り返しが起きやすい。用途を選べば十分実用的だが、知識応答や複雑な推論には向かない。
チャットUI の構築
アーキテクチャ
Browser (Svelte) ←WebSocket→ FastAPI (Python) ← MLX → Bonsai-8B
- バックエンド: FastAPI + WebSocket ストリーミング
- フロントエンド: SvelteKit(static build → FastAPI が配信)
- 推論: mlx-lm の
stream_generate()をrun_in_executorで非同期実行
バックエンド(server.py)
from fastapi import FastAPI, WebSocket
from mlx_lm import load
from mlx_lm.generate import stream_generate
from mlx_lm.sample_utils import make_sampler, make_logits_processors
model, tokenizer = load("prism-ml/Bonsai-8B-mlx-1bit")
@app.websocket("/ws/chat")
async def websocket_chat(websocket: WebSocket):
await websocket.accept()
data = await websocket.receive_text()
msg = json.loads(data)
sampler = make_sampler(temp=0.5, top_k=20, top_p=0.9)
logits_processors = make_logits_processors(
repetition_penalty=1.2,
repetition_context_size=64,
)
# stream_generate は同期なので executor で実行
responses = await loop.run_in_executor(None, lambda: list(
stream_generate(model, tokenizer, prompt=prompt,
sampler=sampler, logits_processors=logits_processors)
))
for resp in responses:
await ws.send_text(json.dumps({"type": "token", "text": resp.text}))
ポイント:
StaticFiles(directory="frontend/build", html=True)で SvelteKit のビルドをルート配信- API/WebSocket ルートを先に定義し、static mount は最後(catch-all)
繰り返し問題の対策
1-bit モデルで最も目立つ問題が繰り返し(repetition)。mlx-lm の make_logits_processors で対策できる:
logits_processors = make_logits_processors(
repetition_penalty=1.2, # 繰り返しトークンのスコアを下げる
repetition_context_size=64, # 直近64トークンを監視
)
repetition_penalty の目安:
- 1.0: ペナルティなし(繰り返しが起きやすい)
- 1.2: 軽いペナルティ(推奨デフォルト)
- 1.5: 強めのペナルティ(多様性は上がるが coherence が落ちる場合も)
フロントエンド(Svelte)
SvelteKit で簡素なチャットUIを構築。デザインは「盆栽(Bonsai)」の名前から Wabi-Sabi × Terminal の美学 — 墨色の背景に侘び寂びの余白、モノスペースフォントが端末のように流れる。
主な機能:
- WebSocket ストリーミング(トークンが1つずつ流れる)
- サンプリングパラメータのリアルタイム調整(temp, top_k, top_p, rep_penalty)
- tok/s リアルタイム表示
- 自動再接続(3秒間隔、ループ防止付き)
Svelte 5 での WebSocket 接続の注意点
Svelte 5 の $effect() 内で WebSocket 接続すると、リアクティブな依存追跡で再接続が無限ループする。onMount() を使うこと:
<!-- ❌ $effect は依存追跡で無限ループ -->
$effect(() => { connect(); });
<!-- ✅ onMount は1回だけ -->
onMount(() => {
connect();
return () => { ws?.close(); };
});
チューニングの可能性 — Bankai(XOR パッチ)
1-bit モデルでは従来の LoRA / Fine-tuning は使えない。重みが0/1しかないので勾配ベースの学習が効かない。
代わりに Bankai という新しい手法がある:
従来: LoRA → FP16 adapter (~100MB) → swap が遅い
Bankai: XOR patch → JSON (~KB) → ゼロオーバーヘッドで適用/除去
1-bit の重みを行単位でビット反転(XOR)する。パッチは KB 単位の JSON ファイルで、同じ XOR 操作で適用も除去もできる(完全可逆)。Scale 値に基づく Greedy Hill-Climbing で効率的に探索し、ランダムの 3.88x の行動変化を実現する。
これは面白い構造で、対象(モデル)を変えずに意味(行動)だけを変える、可逆で合成可能な操作 — まさにレンズのような性質を持っている。KB単位のパッチでペルソナを切り替えたり、タスク特化の行動変容を実現できる可能性がある。
まとめ
| 項目 | 結果 |
|---|---|
| モデルサイズ | 1.28 GB(FP16比 92%圧縮) |
| 実行速度 | 55-58 tok/s(M3 8GB) |
| メモリ使用 | 1.19 GB |
| 性能レベル | GPT-3.5-turbo 相当 |
| 向いてる用途 | 分類・要約・抽出・短文応答 |
| 向いてない用途 | 事実応答・複雑推論・長文コード |
| チューニング | Bankai(XOR patch)で行動変容可能 |
1.28GB で 8B パラメータ。MacBook Air の 8GB メモリでも余裕を持って動く。性能は GPT-3.5 級で万能ではないが、「ローカルで常時起動できる軽量LLM」としての価値は大きい。
Bankai の XOR パッチによるチューニングは、従来の LoRA とは全く異なるアプローチで、1-bit モデルならではの可能性を開いている。今後、ペルソナ切替やタスク特化での活用を実験していきたい。
MacBook Air M3 8GB で検証。2026-04-05。