hipfire
/learn · Flash-Next · native MTP

Speculative decoding on RDNA

Decoding one token at a time leaves most of a GPU idle. Speculative decoding guesses several tokens cheaply, then checks all of them with one pass of the real model. In 0.4.1, hipfire does this on Qwen3.8 Flash-Next with the model's own multi-token-prediction (MTP) head as the guesser, for greedy and sampled requests, with the request's penalties applied on every row it checks.

This page explains the idea, the parts that are specific to RDNA (what a verify row actually costs on Strix Halo and on the Radeon AI PRO R9700), and what it measured in the 0.4.1 release.

01 Why decode leaves room

the idea

Generating one token means reading every active weight once and doing very little math with each byte. That is a bandwidth-bound job: the GPU's matrix units wait on memory. If you hand the same pass several positions at once, the weights are read once and used several times, so a few extra rows cost much less than a few extra tokens would.

Speculative decoding spends that slack. A cheap draft proposes the next k tokens. The full target model scores the current position plus all k guesses in one verify pass of k + 1 rows. Every guess the target agrees with is a token you got without its own decode step.

fig · one speculative window, k = 3 drafts, 4 verify rows
  emitted so far:  … the  quick
  draft head:              brown   fox   jumps        (k = 3 guesses)
  verify pass:     row 0   row 1   row 2   row 3      (one target pass, 4 rows)
  target says:     brown   fox     leaps   ·
                   ✓       ✓       ✗ → emit "leaps"
  window result:   brown fox leaps     3 tokens for one verify pass

The window always emits at least one token (the target's own choice at the first disagreement, or a bonus token after the last row when every draft is accepted), so a window is never worse than one decode step in tokens. Whether it is better in time depends on what the extra rows cost. That is the RDNA-specific part (§ 05).

02 The draft is part of the model

how hipfire does it

Flash-Next carries a native MTP layer: an extra prediction head that reads the trunk's hidden state and proposes the tokens after the next one. hipfire attaches it as the drafter, so there is no second model to download, load or keep in sync. On the two release cards, Strix Halo (gfx1151) and the R9700 (gfx1201), native MTP is on by default; the draft depth defaults to 3 (speculation.mtp_k).

Greedy requests accept the longest prefix of drafts that equal the target's argmax. Without n-gram takeovers (§ 06), 0.4.1's greedy MTP and plain AR agree token for token across the repaired paths: 2–8-row verify projections now sum in exactly the single-row decode order (the gemv_bf16_xf32_k4_rows_r2..r8 kernels), and the Q8 recurrent state is quantized at every verify row the way AR quantizes it after every token.

03 Sampled requests: rejection sampling

the idea

With temperature above zero there is no single “right” token to compare against, so agreement has to be defined on distributions. Speculative rejection sampling (Leviathan et al.; Chen et al.) does this without changing what you sample from:

  1. Draw the draft token x from the draft distribution q.
  2. Accept it with probability min(1, p(x) / q(x)), where p is the target's distribution at that row.
  3. On rejection, emit a draw from the residual (p − q)+, renormalized, and end the window.
  4. If every draft is accepted, draw the bonus token from p at the last row.

The emitted tokens follow p exactly, the same distribution plain sampling would use. Drafts only change how many tokens one verify pass produces.

how hipfire does it

p is exactly the Flash-Next AR sampler's distribution: temperature, the request's top-k (absent means 20 candidates, 0 means the 64-wide pool), min-p and top-p, with the same pool, tie order and summation. q applies the same policy to the draft head's 8 re-scored candidates and their exact logits. One request-seeded random stream supplies every draw, so the same seed replays the same tokens.

lossless, precisely

Sampled MTP matches AR in distribution, not byte for byte: for a given seed it emits a different (equally valid) token stream than AR. The CPU test draws 200,000 trials per case and checks the emitted tokens against p; drawing the rejection replacement from p instead of the residual fails the same test in every control case.

04 Penalties on every verify row

how hipfire does it

Repeat, presence and frequency penalties depend on history: which tokens were already produced. In a speculative window that history grows row by row, so each verify row must be penalized against the history as if the earlier drafts had been accepted:

fig · the penalty history of verify row i
  H_i = last W tokens of ( prompt ‖ emitted so far ‖ drafts[0 .. i) )

  row 0 penalized by  … prompt, emitted
  row 1 penalized by  … prompt, emitted, d0
  row 2 penalized by  … prompt, emitted, d0, d1
  W = the request's penalty window; a rejected draft never enters the next window's history

Both p (the target row) and q (the draft step proposed for that row) are penalized by the same H_i, so rejection sampling still targets the distribution a penalized AR sampler would use. Target rows are penalized on the GPU by a prepass that is bit-identical to the host penalty stage (same emitted IDs on 24 of 24 prompts on each architecture), so only the already-penalized row is downloaded.

In 0.4.0, native MTP verification silently dropped these penalties. Agent workloads often send them (the release's agent evaluations sample with a presence penalty of 1.5), so penalized requests now speculate with the distribution they asked for.

05 What a verify row costs on RDNA

the RDNA part

“Extra rows are cheap” is only true up to a point, and the point is different on every card. hipfire 0.4.1 carries measured window-cost fits for Flash-Next on both release cards and uses them to price decisions:

fig · Flash-Next verify window cost by rows, measured fits used by 0.4.1
0 35 70 105 140 ms per window 1 2 3 4 5 6 7 8 verify rows per window (drafts + 1) Strix Halo 30.5 + 6.9 ms/row R9700 30.3 + 14.8 ms/row

One row (a plain decode step) costs about the same on both cards. Each extra row adds 6.9 ms on Strix Halo but 14.8 ms on the R9700. Divide a window's cost by one decode step and you get the number of tokens the window must emit just to break even with AR:

draftsverify rowsHalo: tokens to break evenR9700: tokens to break even
1 2 1.23 1.49
2 3 1.45 1.98
3 4 1.68 2.47

Ratios computed from the fits above (window cost ÷ one-row cost). Up to 8 rows the fits are explicit per-row buckets; 9 or more rows run a different expert-sharing route, which 0.4.1 does not extrapolate to and does not assume is cheap.

This matches the gap between the two cards in § 08: the same head gains far more where extra rows are cheap. It is also why hipfire chooses the draft depth per window: it picks the depth k that maximizes expected emitted tokens (1 plus the running product of per-depth acceptance) per unit of window cost, and falls back to one-token-per-step when no batched window beats it.

06 N-gram takeovers, priced on the same clock

how hipfire does it

Agent and coding traffic repeats itself: file paths, identifiers, JSON keys, quoted tool output. A request-local n-gram pool, seeded from the full prompt, can propose those continuations for free. In 0.4.1 such a match may take over a window from the MTP head (default triple 5/3/3: match 5 tokens, propose 3).

A takeover only happens when its expected emitted tokens per millisecond, priced with the same row-cost model as § 05, are at least what the native window offers at its current acceptance. Verification is unchanged: greedy candidates must match the target, and sampled ones go through the same rejection test with a point-mass draft q = δ(candidate) (accept with probability p(candidate), otherwise draw from p with the candidate removed). Takeovers are lossless in distribution, but greedy text can differ from MTP-only output.

07 The AR floor

how hipfire does it

Some requests never benefit: low-acceptance prose on a card where rows are expensive. The same-request AR floor measures instead of guessing:

fig · the floor's first windows of a request
1 · time ARone ordinary AR token, timed (sampled draw included), plus one head append
2 · probeup to three speculative windows, each one timed
3 · comparethis request's measured costs per emitted token vs the AR time
4 · decidenothing strictly cheaper → retire to head-free AR until the next request

A tie or an invalid timing also retires. What the floor bounds is the speculative probing (one calibration append and at most three probe windows before the first measured decision), not the full request time. Retired requests are reported as mtp_retired / ar_windows.

08 What it buys you

Flash-Next decode, 0.4.1 release defaults · medians of three fresh processes
33.78tok/s
Strix Halo, greedy AR
range 33.77–33.88
58.67tok/s
Strix Halo, greedy native MTP
1.74× AR · range 58.54–58.76
56.98tok/s
Strix Halo, sampled MTP with presence penalty 1.5
T 0.7 / top-p 0.8 / top-k 20 · one disturbed repetition at 45.55
34.92tok/s
R9700, greedy AR
range 34.88–35.01
40.71tok/s
R9700, greedy native MTP
1.17× AR · range 40.12–40.86
41.09tok/s
R9700, sampled native MTP
range 40.99–41.62
Penalties are about free on AR (Strix Halo 33.87 tok/s neutral vs 33.82 penalized) and cost at most about 1.5 % on sampled MTP (+0.74 ms per 52 ms window). R9700 figures were measured on an earlier build of the 0.4.1 release cycle and were not re-run on the final build. Model: qwen3.8:flash-next.

The gap between the two cards is the row-cost table from § 05 in action: the same head, the same acceptance logic, and roughly twice the price per extra row on the R9700.

09 Limits and switches

what 0.4.1 does not claim
  • Sampled MTP preserves AR's sampling distribution, not its seeded token stream.
  • With n-gram takeovers on, greedy output can differ from MTP-only output; it is not a universal greedy-token identity claim.
  • The AR floor bounds speculative probing, not total request wall time.
  • Cost-aware takeover pricing covers gfx1151 and gfx1201; other architectures keep the previous chooser.
behaviourdefaultopt-out
sampled native MTPonHIPFIRE_MTP_SAMPLED=0 (sampled requests run AR)
n-gram takeovers inside MTPauto: on for Flash-Next on gfx1151 / gfx1201HIPFIRE_MTP_NGRAM=0
same-request AR flooronHIPFIRE_MTP_AR_FLOOR=0 (pre-floor chooser)
GPU penalty prepassonHIPFIRE_QWEN4_PENALTY_PREPASS=0 (host penalties, same IDs)

10 Sources