Repository navigation
Conversation
Builds on the hy_v3 model support in ml-explore#1211 (kernelpool). Instead of stripping the Multi-Token-Prediction layer at load, this keeps it and uses it for self-speculative decoding: - models/hy_v3.py: load the MTP block (HYV3 MTPBlock) instead of dropping it in sanitize(); add Model.predict_next_tokens(...) and a return_hidden_states path. sanitize() also accepts already-stacked (MLX-converted) expert weights, not only the per-expert Tencent layout. - generate.py: mtp_generate_step(...) + auto-routing in stream_generate when num_nextn_predict_layers > 0 (no external draft model; greedy verification, up to two tokens per target pass).
|
Nice work — we independently built the same MTP path for Hy3 on the oMLX serving stack (same block wiring: 1. Your 35–40% acceptance is likely the checkpoint, not the algorithm. We measured drafter-precision sensitivity directly on the full 295B release (target: 4-bit imatrix quant,
Even 8→4-bit on the drafter costs ~6 points at temp 0.9. With 2. Consider distribution-preserving verification instead of greedy-only. The current One honest caveat from profiling the same configuration: because Hy3's active parameters are dominated by routed experts, the 2-token verify pass activates nearly disjoint expert sets (~1.6× the weight traffic of a 1-token forward), so the k=1 speedup ceiling is lower than on dense models — acceptance quality is what decides whether it nets positive. |
|
Thanks @gilby — great cross-check, and the wiring matches exactly ( On (1) — you diagnosed it correctly. I pulled the actual quant config for the test checkpoint (
So the drafter's routed experts sit at the same 2–3-bit as the target and the fusion projection isn't held unquantized — 35–40% @ greedy lands right on the curve you measured rather than a method ceiling. I can't re-measure the higher-precision case myself, though: I'm on a 128 GB box, and a ≥4-bit 295B checkpoint doesn't fit — squeezing it under that ceiling is the whole reason for the On (2) — agreed, and it's the more important fix. And thanks for the honest expert-traffic caveat — it matches what I profile: the 2-token verify pass activates nearly disjoint expert sets (~1.6× the weight traffic of a 1-token forward), so on this MoE the k=1 speedup ceiling is lower than on a dense target and acceptance quality is what decides whether it nets positive. I'll push the sampling fix to this branch. |
…ts_processors) mtp_generate_step verified with argmax on both draft and target and ignored sampler/logits_processors, so any temperature>0 request silently produced temperature-0 output. Hy3's recommended sampling is temperature=0.9, top_p=1.0 (greedy degrades its CoT), so most real usage could not use the MTP path. Draw the drafted token and the target's own token from their processed distributions (via sampler + logits_processors) and accept the draft only when it matches the target's independent sample -- the same acceptance rule as speculative_generate_step. Every emitted token is thus a sample from the target's processed distribution, so temperature/top-p/top-k are honored exactly; a temp-0 (argmax) sampler reduces to the previous greedy behavior. Also thread a token history so stateful logits processors see the full sequence.
The match acceptance rule (draft == target's own sample) preserves the target distribution but accepts with probability sum_x p(x)q(x). The residual rule accepts x~q with prob min(1, p(x)/q(x)) and resamples from relu(p-q) on reject, accepting with sum_x min(p(x), q(x)) >= sum_x p(x)q(x) -- strictly more often -- while emitting the exact same target distribution. make_sampler now tags its returned callable with its parameters (temp, top_p, top_k, min_p, xtc_probability). mtp_generate_step reconstructs the processed draft/target distributions from those params and uses the residual rule when the sampler exposes them, temp>0, XTC is off, and no logits processors are active; otherwise it falls back to the sampler-agnostic match rule. On a 2-3-bit-expert MTP drafter (M3 Max, temp=0.9) residual measured ~25% vs ~18% accept for match. Both paths are distribution-preserving (verified by Monte-Carlo: reconstructed distribution and residual-emitted distribution match the sampler's to within sampling noise, and measured acceptance matches the sum_x min(p,q) theory).
|
Update on (2), @gilby — implemented.
Measured on the aggressive q2-lite build (MTP routed experts 2-bit,
So +7.5 pts (~+42% relative) from the rule alone — directionally exactly your point. Small sample (~65 steps/rule), so I'd treat the absolute gap as directional rather than tight, but it's consistent with the provable Absolute acceptance is still checkpoint-bound — 2-bit experts in the drafter is about as hard as it gets — and I can't go past 3-bit on 128 GB, so the ≥4-bit sweep with |
|
Real-world data point on the exact test checkpoint this PR references ( Per our
Outputs matched byte-for-byte both paths (correctness was never in question, only throughput). Given @gilby's note above about drafter precision — this checkpoint's MTP block runs at the same 2–3-bit as the target experts — this lines up with the "checkpoint-bound, not algorithm-bound" read. Curious whether the residual verification commit changes this number on the same checkpoint; happy to re-benchmark against current |
|
Haven't re-benchmarked against the fixed HEAD ( In the meantime, linking a related data point in case it's useful context: a separate speculative-decoding implementation for the same Hy3 checkpoint (drafter-contract based, not this PR's native self-spec path) hit a different but adjacent failure mode — youssofal/MTPLX#142 (comment) — MTP-drafted depths pass quality only at the slowest setting (27% slower than AR), fail quality (repetition loop) at the depth that's competitive on speed, and the contract calibration reports |
|
Third independent implementation datapoint, on a different Hy3 variant — supports the "checkpoint-bound, not method-bound" read, with one addition: a cheap diagnostic that separates head quality from loop plumbing. Setup: Hy3 152B REAP-pruned to 50% experts, 4-bit/g64 (router 8-bit). That quant also strips layer 80, so we streamed the nextn head back from The diagnostic: before touching the loop, teacher-force the drafter at full-context real positions and measure the acceptance ceiling:
Then the loop, greedy, exact-match verified:
So on our checkpoint the head was fine all along — a starved drafter context masqueraded as a 37% "method ceiling," and the persistent cache (which this PR and @gilby's implementation already have) recovers the teacher-forced ceiling almost exactly. The suggestion for the Two smaller notes:
|
|
Follow-up from our streamed-MoE work: the expert-traffic caveat is showing up as a first-order runtime cost, not just a theoretical ceiling. On a separate 744B GLM-5.2 runtime, we instrumented consumer-blocked expert-load stall separately from asynchronous read service. In the baseline fixed-trace decode, exposed stall was 62.3s of 130.6s (47.7%). Holding expert bytes and read count constant, deterministic post-read completion delay increased read-completion p50 and decode time together: Added completion delay Read-completion p50 Decode time The claim is intentionally narrow: token latency is causally sensitive to added expert-read completion delay under this treatment. This is not a physical SSD-bandwidth estimate, and we are not deriving compression-speedup projections from it. Our first full-context canary also caught a setup confound: applying the treatment from process start throttled the 8K prefill. The experiment now runs prefill with delay off, activates treatment only at the decode boundary, warms for 32 tokens, resets measurement counters at loader-idle, and measures exactly the following 256 tokens. That v3 path is currently 620/620 green, with numerical parity and single-variable trace proofs passing and a new binary epoch frozen. The 16 ms canary is running now. Practical implication for this PR: on streamed MoEs, MTP acceptance should be reported together with extra expert bytes/reads and consumer-exposed stall. A k=1 verification pass can still lose to added expert traffic even when acceptance improves. I’ll post the randomized-block result when it completes. |
|
Follow-up to the datapoint above — we've now reviewed the branch code (HEAD 1. 2. Headless (MTP-stripped) repacks no longer load. 3. The MTP KV cache is never synced to the committed sequence — this is the measured low-acceptance regime. 4. Question — which hidden does the head expect? 5. Smaller items:
If a REAP-pruned trunk carrying the base model's un-pruned MTP head is a case you want to support (it's how we run — head grafted back from |
|
Fourth datapoint on the expert-traffic caveat, from a different model family — and measured from the router rather than inferred from timing. Setup: GLM-4.7-Flash ( Expert divergence, measured directly. I instrumented the router across 96 greedy decode steps, all MoE layers, real hidden states — 4,370 layer-position pairs: consecutive decode positions share 25.7% of their top-4 experts (chance would be 4/64 = 6.25%). So a k=1 verify step reads ~1.74x the expert weights of a 1-token step — between @gilby's ~1.6x and @pierre427's ~2x, same mechanism you both describe. End-to-end it lands where the arithmetic predicts: acceptance 0.770, baseline 16.8 ms/step vs 26.7 ms/step with the drafter, i.e. 1.77 tokens per cycle at ~1.59x step cost = 1.11x. Same shape as @pierre427's 1.84 tok at 1.53x = 1.20x. A teacher-forced ceiling from a loop that isn't starving the head. @pierre427's diagnostic is the right first move, and it seems worth having a reference point for what a healthy loop looks like. Same head, teacher-forced at full-context real positions, 191 slots per prompt:
Live acceptance in the serving loop is 77.0% — within ~5 points of the ceiling, and the prompt mixes differ, so some of that is prompt variance (live per-prompt acceptance ranges 0.71-0.83 on this head). So here the persistent cache is already capturing essentially all of the head's quality; the gap that turned into 37% -> 84% on hy_v3 is absent. That makes the diagnostic doubly useful — it tells you which of the two problems you have, and ~5 points is what "the loop is fine" looks like. The prose/code split is consistent with your 44% prose vs 83% code ceiling: this head is much stronger on structured text than prose, on a similar spread. On the quantization thread: this head is 4-bit against a 4-bit target, so by @gilby's curve there should be a few points available from an 8-bit drafter. I have a bf16 split of the same head published and will measure that pair next. (#1543 is mine — glad it is useful. It exists because of exactly the stripped-quant problem @pierre427 hit.) |
|
Useful fourth point, and the router-measured framing is the right upgrade over One thing worth flagging before anyone stacks these across models: the On your last line — "this head is 4-bit against a 4-bit target, so there We started a matrix on Hy3's MTP head that overlaps with your setup (teacher- |
The three upstream MTP commits from ml-explore#1485 were not run through this repo's pre-commit hooks. Reformat with the pinned black 25.1.0 / isort 6.0.0 so the pre-commit hook passes. Formatting only -- no semantic change.
|
Thanks for your contribution! We recently introduced a new policy regarding the use of AI in pull request descriptions and discussions #1770. |
Summary
Adds Multi-Token-Prediction (MTP) self-speculative decoding for Tencent Hunyuan 3 (
hy_v3).This is stacked on #1211 (kernelpool), which adds the base
hy_v3model + tool parsers + tokenizer fixes. That PR intentionally strips the MTP layer at load. This branch keeps kernelpool's commits as-is and adds three commits on top that keep and use the MTP head:models/hy_v3.py: load the MTP block instead of dropping it insanitize(); addModel.predict_next_tokens(...)and areturn_hidden_statespath.sanitize()also accepts already-stacked (MLX-converted) expert weights, not just the per-expert Tencent layout.generate.py:mtp_generate_step(...)+ auto-routing instream_generatewhennum_nextn_predict_layers > 0— no external draft model; up to two tokens per target pass. Verification is distribution-preserving and honorssampler/logits_processors/ temperature / top-p / top-k exactly.x~qwith probmin(1, p(x)/q(x)), resample fromrelu(p-q)on reject) for strictly higher acceptance, falling back to sampler-agnostic match verification (emit the target's own sample) for temp-0, custom samplers, XTC, or active logits processors.make_samplernow tags its callable with its params so the MTP path can reconstruct the processed distributions.Only the last three commits are mine to review (the rest is #1211). Happy to rebase to an MTP-only diff once #1211 merges.
Test checkpoint (2/3-bit experts + MTP, MLX):
ox-ox/Hy3-295B-Instruct-w2q3exp-AProjQ8-SExpQ8-OutQ8-MTP-mlx.Testing
sanitizeremapping validated in isolation for both the MLX-converted (pre-stacked experts +mtp.*) and the original Tencent (per-expert +model.layers.N) layouts.Notes
sampler/logits_processors/ temperature / top-p / top-k: residual (Leviathan) — acceptx~qwith probmin(1, p(x)/q(x)), resample from the normalizedrelu(p-q)on reject — used when the sampler exposes its params (anymake_sampleroutput), temp>0, XTC off, no logits processors; and match (emit the target's own independent sample, accept when it equals the draft), the sampler-agnostic fallback used otherwise and for temp-0 (where it reduces to greedy). Residual acceptsΣ min(p,q) ≥ Σ p·q(match), i.e. strictly more often, for the same output distribution. Validated by Monte-Carlo: the reconstructed distribution and the residual-emitted distribution match the sampler's to within sampling noise, and measured acceptance matches theΣ min(p,q)theory.switch_layers.pyor the default (non-MTP) generation path.