Skip to content

Commit 894c76a

Browse files
fluffy314cursoragent
authored andcommitted
Address PR109 Mac validation review
Use fair e2e prefill+decode timing for cross/oracle comparisons, chunk long-context MLX prefill paths, and record ctx280 n=5/gen32 evidence showing Step 2 recall parity and speedup under the corrected gate. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c0c5d3c commit 894c76a

6 files changed

Lines changed: 836 additions & 43 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# PR109 Mac ctx280 Validation
2+
3+
This note records the review-driven rerun for PR #109 after fixing the
4+
measurement issues called out in review.
5+
6+
## Review Corrections
7+
8+
- Fair timing: cross and oracle now report the same `e2e_prefill_plus_decode`
9+
scope, plus per-sample `prefill_s`, `decode_s`, and `e2e_s`.
10+
- Chunked prefill: MLX prompt prefill now uses `--prefill-chunk-size` to avoid
11+
the long-context one-shot forward path that can OOM.
12+
- Adaptive native path: Step 2 adaptive S5 native skips `build_restoration`,
13+
f_theta restoration, and aux capture.
14+
- Gemma4 stop tokens: `<turn|>` is treated as a generation stop token alongside
15+
`<eos>`.
16+
- Gate scale: validation was rerun with `n=5`, `max_new_tokens=32`, and
17+
haystack lines `238..322`, producing prompt lengths `4406..5810`.
18+
19+
## Command
20+
21+
```bash
22+
PYTHONPATH=.:sdks/python python scripts/research/k3_integrated_niah_eval_mac.py \
23+
--verifier-path /Users/fluffy314/Documents/Kakeya-LLM-Inference-engine-pr94-resolve/models/gemma-4-26B-A4B-it-mlx-4bit \
24+
--drafter-id z-lab/gemma-4-26B-A4B-it-DFlash \
25+
--f-theta-dir results/research/f_theta_v5_s5_sliding \
26+
--s5-exact-full-attn --fused-specdecode --block-size 4 \
27+
--n-samples 5 --haystack-min-lines 238 --haystack-max-lines 322 \
28+
--max-new-tokens 32 --prefill-chunk-size 512 --decode-warmup-tokens 1 \
29+
--output results/research/k3_mlx_fused_fair_ctx280_n5_gen32_20260612_105807.json
30+
```
31+
32+
## Result
33+
34+
- Recall: cross `5/5 = 1.0`, oracle `5/5 = 1.0`, delta `0pp`.
35+
- Prompt lengths: `4406..5810` tokens.
36+
- Timing scope: `e2e_prefill_plus_decode` for both cross and oracle.
37+
- Cross Step 2 throughput: `0.2217 tok/s` (`39 tok / 175.893s`).
38+
- Oracle AR throughput: `0.0858 tok/s` (`39 tok / 454.484s`).
39+
- Speedup vs oracle AR: `2.584x`.
40+
- KV memory: S5 `132.92 MB`, naive full KV `1308.88 MB`, savings `89.8%`.
41+
42+
## Interpretation
43+
44+
This validation supports Step 2 adaptive S5 native under the corrected e2e
45+
measurement scope at ctx280 scale on the tested Mac setup. It does not claim
46+
that Step 1 incremental is fixed; earlier evidence still shows Step 1 remains
47+
slow and should be treated as a separate optimization target.

inference_engine/backends/mlx/cross_model_dlm_verifier.py

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ def restored_prefill_cache(
375375
restored_k_per_layer: Dict[int, Any],
376376
restored_v_per_layer: Dict[int, Any],
377377
evicted_positions: Sequence[int],
378+
prefill_chunk_size: int = 0,
378379
):
379380
"""Prefill ONCE with restoration, capturing the restored K/V into a
380381
persistent mlx_lm prompt cache; return ``(cache, last_logits)``.
@@ -396,11 +397,32 @@ def restored_prefill_cache(
396397
text_model = resolve_mlx_text_model(mlx_model)
397398
T = len(list(input_ids))
398399
evicted = set(int(p) for p in evicted_positions if 0 <= int(p) < T)
399-
evicted_mask = mx.array([p in evicted for p in range(T)])
400-
401400
cache = make_prompt_cache(mlx_model)
402-
touched = []
403-
try:
401+
402+
def _slice_restored(a, start: int, end: int):
403+
if a is None:
404+
return None
405+
try:
406+
return a[:, start:end, :, :]
407+
except Exception:
408+
# Linux fake tests use sentinel objects rather than tensors.
409+
return a
410+
411+
def _clear(touched):
412+
for obj in touched:
413+
for name in (
414+
"_kakeya_inject",
415+
"kakeya_evicted_mask",
416+
"kakeya_restored_pre_keys",
417+
"kakeya_restored_pre_values",
418+
):
419+
if hasattr(obj, name):
420+
delattr(obj, name)
421+
422+
def _attach_chunk(start: int, end: int):
423+
evicted_mask = mx.array([p in evicted for p in range(start, end)])
424+
touched = []
425+
needs_attention_patch = False
404426
for idx, layer in enumerate(text_model.layers):
405427
attn = layer.self_attn
406428
if idx >= len(cache) or not bool(getattr(attn, "has_kv", True)):
@@ -410,22 +432,47 @@ def restored_prefill_cache(
410432
if rk is None:
411433
continue
412434
c = cache[idx]
413-
c.kakeya_evicted_mask = evicted_mask
414-
c.kakeya_restored_pre_keys = rk
415-
c.kakeya_restored_pre_values = rv
416-
touched.append(c)
417-
ids = mx.array([list(input_ids)])
435+
try:
436+
c.kakeya_evicted_mask = evicted_mask
437+
c.kakeya_restored_pre_keys = _slice_restored(rk, start, end)
438+
c.kakeya_restored_pre_values = _slice_restored(rv, start, end)
439+
touched.append(c)
440+
except Exception:
441+
attn._kakeya_inject = {
442+
"mode": "inject",
443+
"evicted_mask": evicted_mask,
444+
"restored_k": _slice_restored(rk, start, end),
445+
"restored_v": _slice_restored(rv, start, end),
446+
}
447+
touched.append(attn)
448+
needs_attention_patch = True
449+
return touched, needs_attention_patch
450+
451+
ids_list = list(input_ids)
452+
chunk = int(prefill_chunk_size or 0)
453+
if chunk <= 0 or T <= chunk:
454+
chunks = [(0, T)]
455+
else:
456+
chunks = [(s, min(s + chunk, T)) for s in range(0, T, chunk)]
457+
458+
logits = None
459+
for start, end in chunks:
460+
touched, needs_attention_patch = _attach_chunk(start, end)
461+
try:
462+
ids = mx.array([ids_list[start:end]])
463+
if needs_attention_patch:
464+
with _patched_attention_class(text_model):
465+
logits = mlx_model(ids, cache=cache)
466+
mx.eval(logits)
467+
else:
468+
logits = mlx_model(ids, cache=cache)
469+
mx.eval(logits)
470+
finally:
471+
_clear(touched)
472+
if logits is None:
473+
ids = mx.array([ids_list])
418474
logits = mlx_model(ids, cache=cache)
419475
mx.eval(logits)
420-
finally:
421-
for c in touched:
422-
for name in (
423-
"kakeya_evicted_mask",
424-
"kakeya_restored_pre_keys",
425-
"kakeya_restored_pre_values",
426-
):
427-
if hasattr(c, name):
428-
delattr(c, name)
429476
# Subsequent decode steps run native incremental attention over this cache.
430477
return cache, logits[0, -1]
431478

inference_engine/backends/mlx/fused_specdecode.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def prefill(
169169
restored_k_per_layer: Dict[int, Any],
170170
restored_v_per_layer: Dict[int, Any],
171171
evicted_positions: Sequence[int],
172+
prefill_chunk_size: int = 0,
172173
) -> None:
173174
if not prompt_ids:
174175
raise ValueError("prompt_ids must be non-empty")
@@ -178,6 +179,7 @@ def prefill(
178179
restored_k_per_layer=restored_k_per_layer,
179180
restored_v_per_layer=restored_v_per_layer,
180181
evicted_positions=evicted_positions,
182+
prefill_chunk_size=prefill_chunk_size,
181183
)
182184
self._past_len = len(prompt_ids)
183185

0 commit comments

Comments
 (0)