Skip to content

Commit 22847e0

Browse files
feat(mac): full fused-engine chat (verifier+proposer+f_θ, f_θ default-ON) + reasoning-channel strip (#144)
* feat(mac): gemma-4 interactive CLI chat on the Kakeya-for-Mac (MLX) engine scripts/chat_mlx_kakeya.py — single-stream (B=1) gemma-4 chat over the Kakeya S5 bounded cache (SinkWindowKVCache): sliding layers bounded to sink+window, full- attn (exact) layers keep full context (S5 free lunch, no f_theta/proposer needed on gemma-4). B=1 sidesteps the MLX B>1,L=1 batched-decode bug. Interactive REPL + --scripted mode (for non-interactive bridge verification) + transcript JSON. Adds mac-bridge preset mlx-kakeya-chat-smoke (scripted 3-turn run) + manifest test (100% coverage, 29 presets). Operator runs the same script without --scripted for a real interactive chat on the Mac. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): use template enable_thinking instead of raw channel marker Smoke run showed the injected <|channel>content marker leaked 'thought' text and caused greedy looping (turns 1/3 repeated). Replace with the chat template's enable_thinking flag (default off → clean direct answers), with a TypeError fallback for templates that don't accept the kwarg. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): add repetition_penalty (default 1.3) to stop greedy JSON loops enable_thinking=False gave correct answers but greedy decoding looped on a repeated JSON wrapper. Add mlx_lm repetition penalty via logits_processors (with a TypeError fallback for older mlx_lm signatures). Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): generic loop guard — stop + trim when output degenerates into a repeat The 4-bit gemma-4-it checkpoint appends a repeating json answer-wrapper under greedy; repetition_penalty alone didn't stop it. Add a generic guard: break when a 24-char tail recurs 3+ times, and _trim_loop() keeps only the first occurrence for the stored/displayed text. The (correct) answer is always first. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): strict consecutive-loop guard (no longer trims real answers) _trim_loop over-cut (dropped 'Paris' because the answer text recurs inside the model's json echo). Replace with _is_degenerate_loop: stop ONLY on a true back-to-back repeat (same 16-char block 3x consecutively); never trim stored text. Answer + a single echo is preserved. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): raise default --max-new-tokens 256->1024 + show stop reason (eos/max/loop) User hit truncation ('断掉') on a long PoW explanation: exactly 256 tokens = the old default cap. Raise default to 1024 and surface the stop reason each turn (WARN when it's the max-new-tokens cap), so truncation is obvious and tunable. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): stop at gemma's natural turn end (real EOS detection) — output length must not be a user knob User correctly noted: needing --max-new-tokens to avoid truncation = unusable. Root cause: <end_of_turn> was dropped from the stop set (old code used encode()+ single-token check on a special token), so the model ran PAST its turn (verbose JSON echoes / self-correction) until the max-tokens cap — that IS the '断掉' and the verbosity, same bug. Now resolve stops via tokenizer.eos_token_ids + convert_tokens_to_ids('<end_of_turn>'/'<eos>'); the cap is only a backstop. Report eos_token_ids + per-turn stop_reason; smoke now includes a long-answer prompt to confirm it stops on eos, not max. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): default --max-new-tokens=2048 as a generous backstop (natural EOS stops first) Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * chore(mac-bridge): raise MAX_NEW_TOKENS bound 512->2048 (chat answers need it; natural EOS stops first) Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * feat(mac): FULL fused-engine chat (verifier+DFlash proposer+f_θ+S5) — not verifier-only Adds --chat/--chat-scripted to k3_integrated_niah_eval_mac.py: an interactive REPL that drives the EXACT validated fused spec-decode per-turn sequence (build_restoration -> S5 prefill -> aux capture -> fused_specdecode_generate_mlx_trim), reusing the harness's engine construction. The NIAH eval loop is untouched (zero risk to the evidence path). Each turn reports blocks / mean_accept_len to prove the proposer is live + bounded resident KV. Natural EOS stop (<end_of_turn>). Adds bridge preset mlx-kakeya-fused-chat-smoke (full fused flags + --chat-scripted) + manifest test. This is the verifier/PROPOSER bounded-memory engine the project is about — not the verifier-only AR path. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * polish(mac fused chat): strip stop-token markers from displayed text (skip_special_tokens) Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * docs(mac fused chat): accurate engine label — proposer live; f_θ bypassed on gemma-4 (S5 free lunch) Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * feat(mac fused chat): --force-f-theta — f_θ actually RUNS each turn (even if recall-irrelevant on gemma-4) User: f_θ must execute even if its output is discarded by the verifier attention. - build_restoration: --force-f-theta bypasses the S5 native-prefill short-circuit (the line that stopped f_θ running under --s5-exact-full-attn), so f_θ projects proposer hidden -> verifier K/V and injects it into the sliding layers. - _gen_turn now branches over all-mlx (proposer; f_θ bypassed) AND torch drafter+f_θ (proposer + f_θ runs) paths; reports f_theta_ran + f_theta_layers. - new preset mlx-kakeya-fused-chat-ftheta (torch path + --force-f-theta). On gemma-4 the restored sliding K/V are recall-irrelevant (exact layers carry recall) — f_θ still EXECUTES, exercising the full verifier/proposer/f_θ pipeline. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * feat(mac chat): f_θ default-ON in interactive chat + ADR 0015 update - --chat now auto-enables --force-f-theta (full verifier/proposer/f_θ pipeline) unless --all-mlx-drafter (fast, f_θ-bypassed) or native baseline is chosen. - mlx-kakeya-fused-chat-ftheta preset drops the explicit --force-f-theta to verify the DEFAULT-on behavior on the Mac. - ADR 0015: new section 'Mac (MLX) interactive engine — full pipeline, f_θ default-ON' documenting the engine, the gemma-4 recall-irrelevant-but-runs caveat, the forensic timeline (f_θ S5-bypassed 2026-06-12 b3a04d0; proposer blocks=0 caught by 0a6fb19), and the measured result (f_theta_ran=TRUE 25 sliding layers + proposer blocks>0). Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> * fix(mac chat): strip gemma-4 reasoning-channel bleed (\nthought) from displayed answer Cut the decoded text at the first channel marker (<|channel / <channel / \nthought / \nthink) so the chat shows only the natural-language answer. Applied to both the full fused-engine chat (harness _gen_turn) and the lightweight AR chat. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent b761454 commit 22847e0

5 files changed

Lines changed: 356 additions & 2 deletions

File tree

docs/adr/0015-kakeya-attention-and-engine-substrate.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,41 @@ full-attention fraction:
9393
*only* way to bound memory at full recall — and vLLM, having no restoration,
9494
**must keep full KV and cannot match it**. This is the engine's target regime.
9595

96+
## Mac (MLX) interactive engine — full verifier/proposer/f_θ pipeline, f_θ default-ON
97+
98+
The Apple-Silicon interactive CLI (`scripts/research/k3_integrated_niah_eval_mac.py
99+
--chat`) runs the **full Kakeya engine** — gemma-4 verifier (MLX) + **DFlash
100+
proposer** (fused spec-decode) + **f_θ K/V restoration** + **S5 bounded KV** — not
101+
verifier-only. It reuses the validated `fused_specdecode_generate_mlx_trim`
102+
per-turn sequence; the NIAH evidence loop is untouched.
103+
104+
- **f_θ runs by default in chat.** `--force-f-theta` is auto-enabled in `--chat`
105+
unless the fast all-MLX path (`--all-mlx-drafter`, f_θ bypassed) is explicitly
106+
chosen. It bypasses the S5 native-prefill short-circuit so f_θ **executes** each
107+
turn: it projects the proposer's hidden states → verifier K/V for the **25
108+
sliding layers** and injects them.
109+
- **gemma-4 caveat (honest).** On gemma-4 those restored sliding-layer K/V are
110+
**recall-irrelevant** — the 5 exact full-attention layers carry recall (the "S5
111+
free lunch"), so f_θ's output is effectively *discarded by the recall path*. We
112+
still run f_θ by default so the **full verifier/proposer/f_θ pipeline is
113+
exercised end-to-end**; on **full-attention models** the same f_θ path is
114+
load-bearing (it is the only way to bound memory at full recall).
115+
- **Forensic — when f_θ stopped running.** f_θ was silently bypassed under
116+
`--s5-exact-full-attn` on **2026-06-12** by the *"Optimize MLX adaptive S5
117+
native smoke path"* commits (`b3a04d0` / `1f6e58c`), which made
118+
`build_restoration` short-circuit to `{}` under S5; the same *"adaptive S5
119+
native"* path also let the proposer go to `blocks=0` while keeping the fused
120+
label — caught by the evidence gate (`0a6fb19`, *"enforce PR #109 review
121+
constraints"*) which added `--force-fused-specdecode`. Both squashed into main
122+
via #117. f_θ remained S5-bypassed until `--force-f-theta` (this ADR's change)
123+
made it default-on in the interactive chat.
124+
- **Measured (Mac M4, via the git-bus bridge).** Both chat turns:
125+
**`f_theta_ran=TRUE`** restoring the **25 sliding layers** + **proposer
126+
`blocks=2/4`, `mean_accept_len=4.0/3.5`** + correct answers ("Paris"; "red,
127+
yellow, and blue") + natural `<end_of_turn>` stop + bounded resident KV
128+
(12–18 MB). Torch-bridge path is slow (~0.5–6 tok/s); the all-MLX path
129+
(proposer-only) is the fast option.
130+
96131
## Feasibility probes so far (informed the design — NOT the product)
97132

98133
These ran on the eager-transformers research bench; they validate correctness and

inference_engine/bridge/manifest.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,77 @@ def _harness_preset(
678678
params={"max_new_tokens": ("int:max_new_tokens", "64")},
679679
validate_reports=False,
680680
),
681+
Preset(
682+
name="mlx-kakeya-fused-chat-smoke",
683+
description="Run gemma-4 on the FULL Kakeya fused engine (verifier + "
684+
"DFlash proposer + f_θ + S5 bounded KV) via the harness "
685+
"--chat --chat-scripted mode — NOT verifier-only. Verifies "
686+
"the proposer is live (blocks>0, mean_accept_len>0) AND the "
687+
"answer is correct AND KV is bounded, per turn. Writes a "
688+
"transcript JSON.",
689+
command_templates=(
690+
(
691+
"python3", "scripts/research/k3_integrated_niah_eval_mac.py",
692+
"--verifier-path", "${ENV:KAKEYA_MAC_VERIFIER_PATH}",
693+
"--drafter-id", "${ENV:KAKEYA_MAC_DRAFTER_ID}",
694+
"--f-theta-dir", "${ENV:KAKEYA_MAC_FTHETA_DIR}",
695+
"--s5-exact-full-attn", "--fused-specdecode",
696+
"--all-mlx-drafter", "--cuda-trim",
697+
"--sink-size", "4", "--window-size", "64",
698+
"--block-size", "{block_size}",
699+
"--max-new-tokens", "{max_new_tokens}",
700+
"--prefill-chunk-size", "512",
701+
"--chat",
702+
"--chat-scripted",
703+
"What is the capital of France? Answer in one short sentence."
704+
"||Name three primary colors.",
705+
"--output",
706+
"results/research/k3_mac_bridge_mlx_kakeya_fused_chat.json",
707+
),
708+
),
709+
timeout_minutes=60,
710+
params={
711+
"max_new_tokens": ("int:max_new_tokens", "64"),
712+
"block_size": ("int:block_size", "4"),
713+
},
714+
validate_reports=False,
715+
),
716+
Preset(
717+
name="mlx-kakeya-fused-chat-ftheta",
718+
description="Like mlx-kakeya-fused-chat-smoke but on the TORCH drafter "
719+
"+ f_θ path with --force-f-theta: f_θ restoration ACTUALLY "
720+
"RUNS each turn (projects proposer hidden → verifier K/V, "
721+
"injected into the sliding layers) even though on gemma-4 "
722+
"those K/V are recall-irrelevant (the exact layers carry "
723+
"recall). Verifies the FULL verifier/proposer/f_θ pipeline: "
724+
"report shows f_theta_ran=true + blocks>0. (No "
725+
"--all-mlx-drafter; torch bridge path is slower.)",
726+
command_templates=(
727+
(
728+
"python3", "scripts/research/k3_integrated_niah_eval_mac.py",
729+
"--verifier-path", "${ENV:KAKEYA_MAC_VERIFIER_PATH}",
730+
"--drafter-id", "${ENV:KAKEYA_MAC_DRAFTER_ID}",
731+
"--f-theta-dir", "${ENV:KAKEYA_MAC_FTHETA_DIR}",
732+
"--s5-exact-full-attn", "--fused-specdecode",
733+
"--sink-size", "4", "--window-size", "64",
734+
"--block-size", "{block_size}",
735+
"--max-new-tokens", "{max_new_tokens}",
736+
"--prefill-chunk-size", "512",
737+
"--chat",
738+
"--chat-scripted",
739+
"What is the capital of France? Answer in one short sentence."
740+
"||Name three primary colors.",
741+
"--output",
742+
"results/research/k3_mac_bridge_mlx_kakeya_fused_chat_ftheta.json",
743+
),
744+
),
745+
timeout_minutes=90,
746+
params={
747+
"max_new_tokens": ("int:max_new_tokens", "32"),
748+
"block_size": ("int:block_size", "4"),
749+
},
750+
validate_reports=False,
751+
),
681752
)
682753
}
683754

scripts/chat_mlx_kakeya.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,14 @@ def _iter():
192192
stop_reason = "loop"
193193
break
194194
dt = max(time.time() - t0, 1e-9)
195+
_txt = tok.decode(toks, skip_special_tokens=True)
196+
# gemma-4 sometimes bleeds its reasoning channel after the answer; cut it.
197+
for _cut in ("<|channel", "<channel", "\nthought", "\nthink"):
198+
_i = _txt.find(_cut)
199+
if _i > 0:
200+
_txt = _txt[:_i]
195201
return {
196-
"text": tok.decode(toks, skip_special_tokens=True),
202+
"text": _txt.strip(),
197203
"n_tokens": len(toks),
198204
"stop_reason": stop_reason,
199205
"decode_tps": round(len(toks) / dt, 2),

0 commit comments

Comments
 (0)