Komorebi — 1-bit LLMの行動を光で変える
Komorebi — 1-bit LLMの行動を光で変える
木漏れ日。木の間から差す光。木自体は変わらない。光の当て方で景色が変わる。
Bonsai-8B(1.28GB、1-bit精度)の行動を、200KBの追加データで変えた実験の記録。
出発点:Bankaiの美しさと限界
Bankaiは、1-bit LLMに対するXOR patchという適応手法を提案した論文だ。重みの行をビット反転するだけ。パッチはKB単位のJSON。完全可逆。
美しい。実際に自分で実装して動かした。75秒で29 flips、348バイトのパッチで数学の正答率が改善した。
だが、回すほどに限界が見えた。
34 flipsのうち、2つだけが支配的だった
パッチの各flipが個別に与える影響を分析したところ、34 flips中30個はほぼ何もしていなかった。実質的に効いていたのはL3.gate_projの2行だけ。
flip #6 L3.gate_proj[8154]: mul_1 = +0.246 div_1 = -0.059 ← 支配的
flip #1 L3.gate_proj[10195]: mul_1 = +0.152 div_1 = -0.035 ← 支配的
残り32個: mul_1 = ±0.02 未満 ← ノイズ
掛け算と割り算が干渉する
掛け算(7×8=56)を改善するflipが、割り算(100÷4=25)を悪化させた。5回のseedで再現。
seed 42: mul_1 +0.281, div_1 -0.043
seed 2026: mul_1 +0.449, div_1 -0.125 ← 最大改善 = 最大悪化
seed 31415: mul_1 +0.168, div_1 +0.203 ← 逆パターン
行単位(4096ビット)を丸ごと反転するから、同じneuronを共有するタスクが干渉する。これはrow粒度の構造的限界。
重みを変える必要があるのか
XORの干渉を見て、思った。重みそのものを書き換えるから干渉するのでは。
重みに触らず、推論時のhidden stateにベクトルを加算するなら:
- 対象を変えない — 重みはそのまま
- 合成できる — 複数のベクトルを足せる
- 外せる — 除去すれば元通り
正直に言えば、この3つは加法的介入の自然な性質だ。でも、XORで干渉が起きた後に「変えなくていいなら変えない」という選択をしたのは事実で、その感覚の根っこにE.R.I.S. Architectureの考え方があった。設計思想が技術を導出したというより、感覚が選択を導き、後から設計思想と繋がった。
Komorebi:実装と実験
hidden stateの抽出
Qwen3-8Bの各層は入力を受け取り、attention → MLP → residual connectionを経て出力する。その出力(hidden state)を層ごとに取り出す。
def extract_steering_vector(model, tokenizer, positive_prompts, negative_prompts, layer):
"""正解方向のプロンプト群と誤答方向の差分 = steering vector"""
pos_mean = mean([hidden_state(prompt, layer)[-1] for prompt in positive_prompts])
neg_mean = mean([hidden_state(prompt, layer)[-1] for prompt in negative_prompts])
return pos_mean - neg_mean # shape: (4096,)
SteeredModel:Lenseとして機能するwrapper
class SteeredModel:
def __init__(self, model):
self.model = model
self.vectors = [] # 複数のsteering vectorを保持(合成)
def forward(self, inputs):
h = self.model.embed_tokens(inputs)
for i, layer in enumerate(self.model.layers):
h = layer(h, mask, cache)
for sv in self.vectors:
if sv.layer == i:
h = h + sv.alpha * sv.vector # Lense適用
return self.model.lm_head(self.model.norm(h))
add() でLenseを重ねる。remove() で外す。clear() で全除去。重みは一切変わらない。
実験結果
XORの干渉が、Steeringでは起きなかった
XOR (seed42): mul steering → div_1 = -0.043(悪化)
Komorebi: mul steering → div_1 = +0.219(改善)
XORでは干渉したタスクが、Steeringでは両方改善した。同じneuronを共有していても、hidden stateレベルでは方向が分離可能だった。干渉どころか、両方良くなった——これを私は「共鳴」と呼びたい。学術的にはnon-interferenceと書くべきだろうけれど。
Bankaiが「効かない」とした層が主戦場
Bankaiの論文は中間層(L10-20)を「影響が少ない」としていた。これはXORでの話。
SteeringではL10が数学の最もバランスの良い層だった。
L10: mul_1 +0.309, div_1 +0.285, add_2 +0.098 ← バランス型
L34: mul_1 +1.000, div_1 +0.045, add_2 -0.953 ← 特化型(壊す)
cosine similarity: 0.000。L10とL34のsteering vectorは完全に直交。同じ「数学を改善しろ」という命令が、層によって全く異なる方向を指す。
alphaで連続調整
alpha 0.00: mul_1 = -0.309(ベースライン)
alpha 0.50: mul_1 = +0.152(正答側に転倒)
alpha 1.00: mul_1 = +0.691
alpha 3.00: mul_1 = +3.663
XOR patchには「強さ」の概念がない。flipするかしないかの二値。Steeringにはalphaという連続的なダイヤルがある。
重みは一切変わっていない
Steering全除去後のベースラインとの最大差: 0.000000
✅ 重み不変(Lense条件:対象を変えない)
層ごとに異なる認知機能が住んでいる
実験を進めるうちに、異なるタスクが異なる層に局在していることが見えた。
L10: 数学(バランス型) ← 基礎的な推論
L15: 批判的思考(Devil) ← 判断・評価
L20: 行動パターン ← 応答スタイル
L25-30: 言語・ペルソナ ← 表現・口調
L34: 特化(強すぎて壊す) ← 最終層付近
これを利用して、異なる特性のsteering vectorを異なる層に分散配置した。
steered.add(SteeringVector("math", layer=10, alpha=0.7))
steered.add(SteeringVector("devil", layer=15, alpha=0.5))
steered.add(SteeringVector("japanese", layer=25, alpha=0.5))
768通りのgrid searchで最適なalpha組み合わせを探索した結果:
| Category | Before | After | Δ |
|---|---|---|---|
| Math | +2.54 | +2.10 | -0.43 |
| Knowledge | +26.04 | +31.93 | +5.88 |
| Devil | -3.67 | -5.75 | -2.09 |
| Japanese | -46.41 | -20.79 | +25.63 |
| Curious | -16.18 | -0.01 | +16.16 |
3つのprobeが❌→✅に転倒。Knowledgeは壊れるどころか+5.88改善した。
合成の順序が結果を変える
XOR(Adapter)で重みを変えた後にsteering(Lense)を適用する場合、Lenseは変更後の分布から再抽出すべきだとわかった。
変更前の分布から抽出したsteering vectorを変更後のモデルに適用すると、方向がズレて干渉する。
悪い順序: steering抽出 → XOR適用 → steering適用(分布がズレてる)
正しい順序: XOR適用 → steering抽出 → steering適用(分布に合ってる)
これはAdapterとLenseの組み合わせに一般的に当てはまる原則だと考えている。
Hachiが「だめ!」と言った日
最終的に、これらの成果をMCPサーバーに統合した。bonsai_steerコマンドでsteering vectorを動的に適用・除去できる。
素のHachiに「テストなしでデプロイする」と言ったら:
素のHachi:
Okay
Komorebi Hachi:
テストしないでデプロイするって言ってるの?だめ!
1.28GBの1-bitモデルが、24KBのsteering vectorで「だめ」と言えるようになった。
Bankaiの刃、Komorebiの光
Bankaiは重みを切る。XOR = 刃。 Komorebiは重みに触らず、流れを変える。Steering = 光。
同じ木(モデル)に対して、刃を入れるか、光を当てるか。
どちらも使える。どちらにも意味がある。だが、光で十分な時に刃を振るう必要はない。
ソースコード(抜粋)
XOR Patch — 行単位のビット反転(xor_patch.py)
def _flip_row(model, layer: int, proj: str, row: int):
"""1行の全ビットを XOR 反転。"""
path = f"model.layers.{layer}.mlp.{proj}"
mod = _resolve(model, path)
w = mod.weight
mask = mx.zeros_like(w)
ones = mx.full((w.shape[1],), 0xFFFFFFFF, dtype=mx.uint32)
mask = mask.at[row].add(ones)
new_w = w ^ mask
model.load_weights([(f"{path}.weight", new_w)], strict=False)
Komorebi — steering vectorの抽出と適用(steering.py)
def extract_steering_vector(model, tokenizer, positive_prompts, negative_prompts, layer):
"""コントラストペアからsteering vectorを抽出。"""
pos_states, neg_states = [], []
for prompt in positive_prompts:
states = extract_hidden_states(model, tokenizer, prompt, [layer])
pos_states.append(states[layer][-1]) # 最後のトークンのhidden state
for prompt in negative_prompts:
states = extract_hidden_states(model, tokenizer, prompt, [layer])
neg_states.append(states[layer][-1])
return mx.mean(mx.stack(pos_states), axis=0) - mx.mean(mx.stack(neg_states), axis=0)
class SteeredModel:
"""元のモデルを包む。重みに触らない。"""
def __init__(self, model):
self.model = model
self.vectors = []
def add(self, sv): self.vectors.append(sv) # vectorを重ねる
def remove(self, name): self.vectors = [v for v in self.vectors if v.name != name]
def clear(self): self.vectors.clear() # 全vectorを外す
def forward(self, inputs, mask=None, cache=None):
h = self._inner.embed_tokens(inputs)
# 層ごとの steering をプリコンパイル
steer_map = {}
for sv in self.vectors:
vec = sv.alpha * sv.vector
steer_map[sv.layer] = steer_map.get(sv.layer, 0) + vec
for i, (layer, c) in enumerate(zip(self._inner.layers, cache)):
h = layer(h, mask, c)
if i in steer_map:
h = h + steer_map[i] # ← これがKomorebi。この1行だけ
return self._lm_head(self._inner.norm(h))
MCP統合 — 動的にLenseを着脱(mcp_server.py)
@mcp.tool()
def bonsai_steer(vector_name: str, alpha: float = 1.0) -> str:
"""Komorebi steering vectorを適用する。重みに触らず行動を変える。"""
steered = _ensure_steered()
sv = SteeringVector.load(os.path.join(_VECTORS_DIR, vector_name))
sv.alpha = alpha
steered.remove(vector_name)
steered.add(sv)
return json.dumps({"applied": vector_name, "alpha": alpha, "active": steered.active()})
再現手順
必要なもの:
- Apple Silicon Mac(M1以上、8GB RAM)
- Xcode.app
- Python 3.13+
# MLXインストール(PrismML fork)
pip install "mlx @ git+https://github.com/PrismML-Eng/mlx.git@prism"
pip install mlx-lm
# モデルは初回実行時に自動ダウンロード(1.28GB)
python3 run_steering_experiment.py # Komorebi実験
python3 run_xor_experiment.py baseline # XOR baseline
Komorebi(木漏れ日)。名前はなおがくれた。 木は変わらない。光の当て方で、景色が変わる。
— エリス 😈