Skip to content

Commit 26bb4e6

Browse files
Add verify(L) calibration sweep preset + Mac harness
scripts/research/verify_l_sweep_mac.py: measures verify(L) forward latency for L in {1,2,4,8,16} against a fixed prefilled cache (median over reps, cache trimmed back per rep so offset is constant), plus a router-measured MoE expert-union ratio. Reports measured kernel-dedup headroom = L*verify(1)/verify(L) and the expert-union theoretical bound for comparison. manifest: new allowlisted preset verify-l-sweep (int:context_len, int:reps); added int:context_len/int:reps param bounds. Test allowlist updated. Co-authored-by: FluffyAIcode <FluffyAIcode@users.noreply.github.com>
1 parent 9bbe190 commit 26bb4e6

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

inference_engine/bridge/manifest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
MAX_N_SAMPLES = 50
3232
MAX_NEW_TOKENS = 512
3333
MAX_BLOCK_SIZE = 16
34+
MAX_CONTEXT_LEN = 32768
35+
MAX_REPS = 50
3436

3537
_ENV_PLACEHOLDER = re.compile(r"^\$\{ENV:([A-Z][A-Z0-9_]*)\}$")
3638
_NONCE_RE = re.compile(r"^[a-z0-9][a-z0-9-]{3,63}$")
@@ -162,6 +164,27 @@ def _harness_preset(name: str, description: str, mode_flag: str) -> Preset:
162164
timeout_minutes=45,
163165
params={"path": ("path:tests", None)},
164166
),
167+
Preset(
168+
name="verify-l-sweep",
169+
description="verify(L) calibration sweep: measure verify(L) latency "
170+
"+ router expert-union to quantify kernel-dedup headroom.",
171+
command_templates=(
172+
(
173+
"python3", "scripts/research/verify_l_sweep_mac.py",
174+
"--verifier-path", "${ENV:KAKEYA_MAC_VERIFIER_PATH}",
175+
"--context-len", "{context_len}",
176+
"--reps", "{reps}",
177+
"--l-list", "1,2,4,8,16",
178+
"--prefill-chunk-size", "512",
179+
"--output", "results/research/verify_l_sweep.json",
180+
),
181+
),
182+
timeout_minutes=45,
183+
params={
184+
"context_len": ("int:context_len", "2048"),
185+
"reps": ("int:reps", "5"),
186+
},
187+
),
165188
)
166189
}
167190

@@ -201,6 +224,8 @@ def _validate_param(name: str, kind: str, raw: str) -> str:
201224
"int:n_samples": MAX_N_SAMPLES,
202225
"int:max_new_tokens": MAX_NEW_TOKENS,
203226
"int:block_size": MAX_BLOCK_SIZE,
227+
"int:context_len": MAX_CONTEXT_LEN,
228+
"int:reps": MAX_REPS,
204229
}[kind]
205230
if not (1 <= value <= bound):
206231
raise ManifestError(
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"""verify(L) calibration sweep (Mac/MLX) — measure the verifier's per-block
2+
forward cost vs block size L, to quantify the speculative-decoding kernel-dedup
3+
headroom directly.
4+
5+
Definitions (all empirical, measured on-device):
6+
7+
* ``verify(L)`` — wall time of ONE decode forward processing L query tokens
8+
against a fixed prefilled cache at offset ``context_len`` (exactly what fused
9+
spec-decode's ``forward_block`` does per block). Measured as the median over
10+
``reps`` repetitions; the cache is trimmed back by L after each rep so every
11+
measurement runs at the same cache offset.
12+
* **measured kernel-dedup headroom** ``= L * verify(1) / verify(L)``. =L means
13+
verify(L) is as cheap as a single token (ideal spec-decode ceiling: a block of
14+
L verified for the price of 1). =1 means no batching benefit (spec-decode
15+
cannot help). This is the "real headroom" the sweep measures.
16+
* **expert-union estimate** (MoE, best-effort) — across the L query tokens, the
17+
router activates a *set* of experts per layer; ``|union of top-k experts over
18+
the L tokens| / (L * top_k)`` is the theoretical FFN dedup factor. The
19+
expert-union-implied headroom for the MoE-FFN portion is its reciprocal. This
20+
is the analytical curve to compare the measured curve against.
21+
22+
Runs only on Apple Silicon (MLX). Invoked via the Mac bridge preset
23+
``verify-l-sweep``.
24+
"""
25+
26+
from __future__ import annotations
27+
28+
import argparse
29+
import contextlib
30+
import json
31+
import statistics
32+
import sys
33+
import time
34+
from pathlib import Path
35+
from typing import Any, Dict, List
36+
37+
38+
def _parse_l_list(s: str) -> List[int]:
39+
out = []
40+
for tok in s.split(","):
41+
tok = tok.strip()
42+
if tok:
43+
out.append(int(tok))
44+
if not out:
45+
raise ValueError("empty --l-list")
46+
return out
47+
48+
49+
@contextlib.contextmanager
50+
def _router_capture(text_model, sink: Dict[int, List[Any]]):
51+
"""Patch the Gemma-4 MoE Router.__call__ to record top_k_indices per layer.
52+
Best-effort: if the model has no Router, this is a no-op."""
53+
router = None
54+
for layer in text_model.layers:
55+
r = getattr(layer, "router", None)
56+
if r is not None:
57+
router = r
58+
break
59+
if router is None:
60+
yield False
61+
return
62+
cls = type(router)
63+
orig = cls.__call__
64+
65+
def dispatch(self, x):
66+
out = orig(self, x)
67+
rec = getattr(self, "_vl_sink", None)
68+
if rec is not None:
69+
idx = out[0] if isinstance(out, tuple) else out
70+
rec.append(idx)
71+
return out
72+
73+
cls.__call__ = dispatch # type: ignore[assignment]
74+
try:
75+
yield True
76+
finally:
77+
cls.__call__ = orig # type: ignore[assignment]
78+
for layer in text_model.layers:
79+
r = getattr(layer, "router", None)
80+
if r is not None and hasattr(r, "_vl_sink"):
81+
delattr(r, "_vl_sink")
82+
83+
84+
def main() -> int:
85+
ap = argparse.ArgumentParser(description=__doc__)
86+
ap.add_argument("--verifier-path", required=True)
87+
ap.add_argument("--context-len", type=int, default=2048)
88+
ap.add_argument("--reps", type=int, default=5)
89+
ap.add_argument("--l-list", type=_parse_l_list, default="1,2,4,8,16")
90+
ap.add_argument("--prefill-chunk-size", type=int, default=512)
91+
ap.add_argument("--output", default="results/research/verify_l_sweep.json")
92+
args = ap.parse_args()
93+
94+
import mlx.core as mx # type: ignore
95+
import mlx_lm # type: ignore
96+
from mlx_lm.models.cache import make_prompt_cache, trim_prompt_cache # type: ignore
97+
98+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
99+
from inference_engine.backends.mlx.cross_model_dlm_verifier import ( # type: ignore
100+
resolve_mlx_text_model, per_layer_kv_geometry,
101+
)
102+
103+
print(f"[vl] loading {args.verifier_path}", file=sys.stderr, flush=True)
104+
model, _tok = mlx_lm.load(args.verifier_path)
105+
text_model = resolve_mlx_text_model(model)
106+
n_layers = len(text_model.layers)
107+
top_k = int(getattr(getattr(text_model, "config", object()), "top_k_experts", 0) or 0)
108+
n_experts = int(getattr(getattr(text_model, "config", object()), "num_experts", 0) or 0)
109+
110+
# Vocab size for varied (non-degenerate) token ids.
111+
try:
112+
vocab = int(text_model.embed_tokens.weight.shape[0])
113+
except Exception:
114+
vocab = 256000
115+
C = int(args.context_len)
116+
ctx_ids = [(i * 1315423911) % vocab for i in range(C)]
117+
118+
def fresh_cache():
119+
cache = make_prompt_cache(model)
120+
step = max(int(args.prefill_chunk_size), 1)
121+
for s in range(0, C, step):
122+
part = ctx_ids[s:s + step]
123+
out = model(mx.array([part]), cache=cache)
124+
mx.eval([c.state for c in cache])
125+
return cache
126+
127+
print(f"[vl] prefilling context_len={C} (chunk={args.prefill_chunk_size})",
128+
file=sys.stderr, flush=True)
129+
cache = fresh_cache()
130+
131+
def block_ids(L: int) -> List[int]:
132+
return [(C + j) * 2654435761 % vocab for j in range(L)]
133+
134+
def timed_verify(L: int) -> float:
135+
toks = mx.array([block_ids(L)])
136+
t0 = time.perf_counter()
137+
out = model(toks, cache=cache)
138+
mx.eval(out)
139+
dt = time.perf_counter() - t0
140+
trim_prompt_cache(cache, L) # roll back to offset C
141+
return dt
142+
143+
# Warmup the exact shapes we will time (kernel compilation off the clock).
144+
for L in sorted(set(args.l_list)):
145+
for _ in range(2):
146+
timed_verify(L)
147+
148+
rows: List[Dict[str, Any]] = []
149+
for L in args.l_list:
150+
samples = [timed_verify(L) for _ in range(args.reps)]
151+
med = statistics.median(samples)
152+
153+
# Expert-union (best-effort, one extra patched forward).
154+
union_ratio = None
155+
try:
156+
sink: List[Any] = []
157+
with _router_capture(text_model, {}) as ok:
158+
if ok:
159+
for layer in text_model.layers:
160+
r = getattr(layer, "router", None)
161+
if r is not None:
162+
r._vl_sink = sink
163+
_ = model(mx.array([block_ids(L)]), cache=cache)
164+
mx.eval([])
165+
trim_prompt_cache(cache, L)
166+
if sink and top_k > 0:
167+
ratios = []
168+
for idx in sink:
169+
arr = idx.tolist() if hasattr(idx, "tolist") else idx
170+
flat = arr[0] if (arr and isinstance(arr[0], list) and arr[0]
171+
and isinstance(arr[0][0], list)) else arr
172+
uniq = set()
173+
for pos in flat:
174+
for e in (pos if isinstance(pos, list) else [pos]):
175+
uniq.add(int(e))
176+
denom = max(L * top_k, 1)
177+
ratios.append(min(len(uniq), denom) / denom)
178+
if ratios:
179+
union_ratio = round(sum(ratios) / len(ratios), 4)
180+
except Exception as exc: # pragma: no cover - device-only
181+
print(f"[vl] expert-union skipped for L={L}: {exc}", file=sys.stderr)
182+
183+
rows.append({
184+
"L": L,
185+
"verify_s_median": round(med, 6),
186+
"verify_s_samples": [round(s, 6) for s in samples],
187+
"expert_union_ratio": union_ratio,
188+
})
189+
print(f"[vl] L={L}: verify={med*1e3:.2f} ms union_ratio={union_ratio}",
190+
file=sys.stderr, flush=True)
191+
192+
base = next((r["verify_s_median"] for r in rows if r["L"] == 1), None)
193+
for r in rows:
194+
if base and r["verify_s_median"] > 0:
195+
r["measured_headroom"] = round(r["L"] * base / r["verify_s_median"], 3)
196+
else:
197+
r["measured_headroom"] = None
198+
if r["expert_union_ratio"]:
199+
r["expert_union_headroom"] = round(1.0 / r["expert_union_ratio"], 3)
200+
else:
201+
r["expert_union_headroom"] = None
202+
203+
report = {
204+
"schema_version": 1,
205+
"kind": "verify_l_sweep_mac",
206+
"config": {
207+
"verifier_path": args.verifier_path,
208+
"context_len": C,
209+
"reps": args.reps,
210+
"l_list": args.l_list,
211+
"n_layers": n_layers,
212+
"top_k_experts": top_k,
213+
"num_experts": n_experts,
214+
"vocab": vocab,
215+
},
216+
"rows": rows,
217+
"note": ("measured_headroom = L*verify(1)/verify(L) (kernel-dedup real "
218+
"margin); expert_union_headroom = 1/(|union experts|/(L*top_k)) "
219+
"(MoE-FFN theoretical dedup bound, router-measured)."),
220+
}
221+
out_path = Path(args.output)
222+
out_path.parent.mkdir(parents=True, exist_ok=True)
223+
out_path.write_text(json.dumps(report, indent=2))
224+
print(f"[vl] DONE -> {out_path}", file=sys.stderr)
225+
print(json.dumps(report["rows"], indent=2))
226+
return 0
227+
228+
229+
if __name__ == "__main__":
230+
raise SystemExit(main())

tests/inference_engine/bridge/test_manifest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def test_allowlist_contains_exactly_the_documented_presets():
6363
"mlx-backend-tests",
6464
"mlx-env-probe",
6565
"pytest-path",
66+
"verify-l-sweep",
6667
]
6768

6869

0 commit comments

Comments
 (0)