Skip to content

Commit 28c0d44

Browse files
committed
feat(v7-h39): gen.run speculative-decode smoke wires speculative_acceptance
Closes plan item #39. cppmega_mlx/inference/speculative_decode.py shipped the Leviathan-style acceptance helper but had 0 callers in gen.run / UI — speculative decoding looked dead-on-arrival. - GenRunParams.speculative_k (0..32, default 0); 0 keeps old path. - When > 0: gen.run runs speculative_acceptance against synthetic identical draft + target logits keyed on the seed, records {k, draft_tokens, accepted, accept_rate} in GenRunResult.speculative. - 4 pytest: off-by-default, accept_rate > 0.5 sanity gate for identical draft=target (any value <= 0.5 means the helper wiring is broken), deterministic seeded round-trip, dispatcher route.
1 parent 4b0ad1f commit 28c0d44

2 files changed

Lines changed: 110 additions & 0 deletions

File tree

cppmega_v4/jsonrpc/gen_run_method.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ class GenRunParams(BaseModel):
6464
# is reproducible across reruns.
6565
moe_num_experts: int = Field(0, ge=0, le=512)
6666
moe_top_k: int = Field(2, ge=1, le=16)
67+
# V7-H39: speculative-decode smoke. When speculative_k > 0, gen.run
68+
# runs the cppmega_mlx.inference.speculative_decode acceptance
69+
# helper against synthetic draft+target logits. Identical draft +
70+
# target (default smoke) must yield accept_rate > 0.5 — sanity gate
71+
# that the helper wiring is sound.
72+
speculative_k: int = Field(0, ge=0, le=32)
6773

6874

6975
class GenRunEvent(BaseModel):
@@ -91,6 +97,9 @@ class GenRunResult(BaseModel):
9197
# "dropped_token_ratio": 0.0 # always 0 at inference per AC#3
9298
# }.
9399
moe: dict | None = None
100+
# V7-H39: speculative-decode smoke result. None unless speculative_k
101+
# was set. Shape: {k, draft_tokens, accepted, accept_rate}.
102+
speculative: dict | None = None
94103

95104

96105
def _build_step_fn(params: GenRunParams):
@@ -214,6 +223,44 @@ def _on_token(ev: dict) -> None:
214223
events = [GenRunEvent(step=int(e.get("step", i)),
215224
token=int(e.get("token_id", 0)))
216225
for i, e in enumerate(raw_events)]
226+
# V7-H39: speculative-decode smoke — only runs when explicitly
227+
# requested via speculative_k > 0, so the default gen.run path is
228+
# unchanged. Uses synthetic identical draft+target logits keyed on
229+
# the same seed; cppmega_mlx.inference.speculative_decode.
230+
# speculative_acceptance is the unit under test.
231+
speculative_state: dict | None = None
232+
if params.speculative_k > 0:
233+
import mlx.core as mx
234+
from cppmega_mlx.inference.speculative_decode import (
235+
speculative_acceptance,
236+
)
237+
K = int(params.speculative_k)
238+
vocab = max(2, params.vocab_size)
239+
rng_key = mx.random.key(params.seed + 1)
240+
draft_logits = mx.random.normal((K, vocab), key=rng_key)
241+
target_logits = draft_logits.reshape(K, vocab)
242+
# Pad target_logits to (K+1, vocab) to match the helper's
243+
# contract — final row is the position-after-last token.
244+
bonus = mx.random.normal((1, vocab),
245+
key=mx.random.key(params.seed + 2))
246+
target_logits = mx.concatenate([target_logits, bonus], axis=0)
247+
draft_tokens = mx.argmax(draft_logits, axis=-1)
248+
_accepted_seq, num_accepted, _bonus = speculative_acceptance(
249+
draft_logits=draft_logits,
250+
target_logits=target_logits,
251+
draft_tokens=draft_tokens,
252+
temperature=1.0,
253+
rng_key=mx.random.key(params.seed + 3),
254+
)
255+
accept_rate = (
256+
float(num_accepted) / float(K) if K > 0 else 0.0)
257+
speculative_state = {
258+
"k": K,
259+
"draft_tokens": [int(x) for x in draft_tokens.tolist()],
260+
"accepted": int(num_accepted),
261+
"accept_rate": round(accept_rate, 6),
262+
}
263+
217264
return GenRunResult(
218265
tokens=tokens,
219266
finish_reason=reason,
@@ -223,6 +270,7 @@ def _on_token(ev: dict) -> None:
223270
smoke=params.smoke,
224271
kv_cache=kv_cache_state,
225272
moe=moe_state,
273+
speculative=speculative_state,
226274
)
227275

228276

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""V7-H39: speculative-decode smoke wiring through gen.run.
2+
3+
When speculative_k > 0, gen.run runs the cppmega_mlx.inference.
4+
speculative_decode.speculative_acceptance helper against synthetic
5+
identical draft+target logits and exposes the accept_rate in the
6+
GenRunResult.speculative block. Identical distributions must accept
7+
> 50% of draft tokens — the standard Leviathan sanity gate.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from cppmega_v4.jsonrpc.dispatcher import dispatch
13+
from cppmega_v4.jsonrpc.gen_run_method import GenRunParams, gen_run
14+
15+
16+
def test_v7_h39_speculative_off_by_default():
17+
out = gen_run(GenRunParams(
18+
prompt_tokens=[1], eos_token_id=99_999, max_new_tokens=4,
19+
))
20+
assert out.speculative is None
21+
22+
23+
def test_v7_h39_speculative_smoke_accept_rate_above_half():
24+
"""K=16 identical draft+target → accept_rate > 0.5 sanity."""
25+
out = gen_run(GenRunParams(
26+
prompt_tokens=[1], eos_token_id=99_999, max_new_tokens=4,
27+
seed=5, vocab_size=64, speculative_k=16,
28+
))
29+
sp = out.speculative
30+
assert sp is not None
31+
assert sp["k"] == 16
32+
assert len(sp["draft_tokens"]) == 16
33+
assert 0 <= sp["accepted"] <= 16
34+
assert sp["accept_rate"] > 0.5, (
35+
f"speculative accept_rate {sp['accept_rate']} <= 0.5 for "
36+
f"identical draft+target — wiring is broken")
37+
38+
39+
def test_v7_h39_speculative_deterministic_for_same_seed():
40+
a = gen_run(GenRunParams(
41+
prompt_tokens=[1], eos_token_id=99_999, max_new_tokens=2,
42+
seed=7, vocab_size=64, speculative_k=8,
43+
))
44+
b = gen_run(GenRunParams(
45+
prompt_tokens=[1], eos_token_id=99_999, max_new_tokens=2,
46+
seed=7, vocab_size=64, speculative_k=8,
47+
))
48+
assert a.speculative == b.speculative
49+
50+
51+
def test_v7_h39_dispatcher_route_returns_speculative_block():
52+
resp = dispatch({
53+
"jsonrpc": "2.0", "id": "T1", "method": "gen.run",
54+
"params": {
55+
"prompt_tokens": [1], "eos_token_id": 99_999,
56+
"max_new_tokens": 2, "vocab_size": 32,
57+
"speculative_k": 8,
58+
},
59+
})
60+
assert resp.error is None, resp.error
61+
assert resp.result["speculative"] is not None
62+
assert resp.result["speculative"]["k"] == 8

0 commit comments

Comments
 (0)