Skip to content

ricursive: the publisher's DFlash2 drafter for Ripple-65B (stacked on #2231) - #2260

Draft
BobClawblaw wants to merge 7 commits into
ashhart:mainfrom
BobClawblaw:ripple-dflash2
Draft

BobClawblaw wants to merge 7 commits into
ashhart:mainfrom
BobClawblaw:ripple-dflash2

Conversation

@BobClawblaw

@BobClawblaw BobClawblaw commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Adds the publisher's DFlash2 drafter for Ripple-65B on the native Metal engine:
ricursiveAI/Ripple-65B-Preview-DFlash2 at
35609b3be3dd08a822b3ba1257c1ea5dd6f87632 (7.5 GiB, bf16), read in place as published. serve ... --drafter DIR on a
ricursive checkpoint now drafts with it. Before, --drafter was refused for this family.

Things to say up front

  • Drafted == plain. Every draft is verified by one target window on the lane core, so a reply is the plain reply.
    The harness checks this on the full model (below).
  • Two streams at most with the drafter. Each stream's block is its own drafter pass: at four streams drafted
    rounds tie plain ones except on lists, at seven they are slower on every kind, so with --drafter the server refuses
    --parallel above 2 by name (table in the doc).
  • Caches stay exact bf16. The drafter keeps its five layers' keys and values in bf16 rings: four sliding layers
    over 4,096 keys, and the fifth (NoPE) over the release's 16,384-key draft window. Its 7.7 GiB (weights and rings)
    comes out of the cache budget: 127.5 GiB instead of 135.2, seven 262,144-token streams instead of eight.
  • Reference. z-lab's DFlash2DraftModel in z-lab/dflash's dflash/model.py at
    07ebd93db9f472af339b644bb70221ad8428328a (MIT): the taps' fc and norm, the layer order, the grouped dynamic
    causal convolutions and the candidate selector, at its rounding points. z-lab imports its norms, rotary and MLP from transformers' Qwen3 module; ours follow the
    release's own RicursiveRMSNorm, RicursiveRotaryEmbedding and RicursiveMLP (modeling_ricursive.py) instead. The
    NoPE layer is no_rope_layers in the drafter's config.json, and the 16,384-token draft window is from
    inference/server/README.md in Ripple-65B-Preview at 240d5966. The first push followed SGLang's dflash.py; this one was rechecked step by step against these two
    sources alone and changed where they differ (the convolution's and the selector's rounding). Both are in
    THIRD_PARTY_NOTICES.md. dflash2_config.zig accepts only this release's geometry beside Ripple-65B and refuses
    anything else by name.

Shared code

  • native/metal.zig: ricursive is added to the --drafter family list and its refusal text.
  • kernels/metal/sources.zig: one embed line.

Everything else is in the family's folder (families/ricursive/, kernels/metal/ricursive/dflash2.metal,
native/ricursive.zig) and its contract files. On the plain path, forward.zig writes the five taps only when a
drafter asked for them. The A/B below shows plain Ripple unchanged.

Checks on this head

  • Head commit: 938b8aef0 (on main c3f64ca8a, ricursive: Ripple-65B-Preview on native Metal (NVFP4 and bf16 releases) #2231 at 70c1867b5)
  • Lean checker: base 210, head 210, 0 new.
  • Tests run on this head (each exit 0): zig build test --test-timeout 10m, test-kernel-contract, test-golden
    (322/335 equal to server goldens, 0 unexpected differences), test-server-security test-pull-security (24 passed,
    0 failed, 0 skipped), fuzz-cortheon, the build-options test, zig fmt --check build.zig zig.
  • CI: all seven checks pass on this head (zig, zig-windows, zig-macos-dev-tools, fmt, release-paths, benchmark-clients on Ubuntu and Windows).
  • Not run, and why: no quality suite. Beside the bf16 release the drafter was checked for drafted == plain,
    concurrency and speed; structured output, seeded sampling, the soak and resumed turns ran beside the NVFP4 release.
    M1 to M4 Macs are refused at load, as for ricursive: Ripple-65B-Preview on native Metal (NVFP4 and bf16 releases) #2231.

Receipt

  • Environment: Mac Studio M5 Ultra, 256 GB, macOS 27.2, Zig 0.17.0. Target ricursiveAI/Ripple-65B-Preview-NVFP4 at
    99eea4477a001a41b86914e41feb4b2ed86105e5, drafter at the revision above.
  • Exactness, recipe harness quick plan on b7ff7854f (this head's drafter code before the rebase; since then only
    comments changed, and --parallel above 2 is refused), the same results as on 925ae9563:
    • Drafted == plain: 12/12 in the drafts suite, 12/12 under structured output.
    • Seeded sampling: 9/9 equal on repeat and to undrafted replies.
    • Soak and concurrency: a 60-turn soak and 12 concurrent replies, each equal to the request alone.
    • Structured output 18/18, tools 5/5.
    • A resumed turn equals the cold one after a restart.
  • Decode speed: recipe harness speed plan, --context 262144 --parallel 2, greedy 256-token replies, no other model
    busy. "Before" is ricursive: Ripple-65B-Preview on native Metal (NVFP4 and bf16 releases) #2231 at 3a5954f38, measured on the same day.
streams prose code structured list a round
1, before 10.2 10.2 10.3 10.2 98.2 ms
1, drafted 15.5 27.5 17.8 19.8 131.2 ms
2, before 19.0 18.9 19.2 18.9 106.0 ms
2, drafted 22.5 37.7 26.6 40.6 166.2 ms

The speedup is 1.5-2.7× with one stream and 1.2-2.1× with two. A round keeps 2.65 tokens with one stream (48% of
drafted tokens accepted) and 2.65 with two (56%). Time to first token is 0.53-0.65 s with one stream.

  • Past two streams, NVFP4 at --parallel 7, one binary with and without --drafter (e1d719275, this drafter's
    code before the cap); tok/s for all streams, prose / code / structured / list:
streams plain drafted
4 32.6 / 31.8 / 31.1 / 32.5 (126.2 ms a round) 30.3 / 32.0 / 31.3 / 43.1 (277.6 ms)
7 43.2 / 42.9 / 41.6 / 43.4 (165.5 ms) 40.2 / 34.7 / 22.9 / 34.1 (406.4 ms)

So with --drafter, --parallel above 2 is refused by name.

streams prose code structured list a round
1, plain 5.8 5.0 4.9 5.0 194.7 ms
1, drafted 13.4 30.7 13.3 18.7 154.0 ms
2, plain 11.5 11.0 10.5 11.8 179.6 ms
2, drafted 25.7 63.3 26.0 54.8 148.4 ms

2.2-6.1× plain; a round keeps 2.93 tokens with one stream and 3.15 with two.

  • Plain-path A/B (ricursive: Ripple-65B-Preview on native Metal (NVFP4 and bf16 releases) #2231 3a5954f38 against 45685de42, this PR before the z-lab rebuild, which changed only
    drafter-only kernels, tests, notices and docs; both without --drafter): all 53 replies of the speed plan (rulers and a
    resumed turn) have the same hashes in the same order; one stream 10.2/10.2/10.3/10.2 tok/s and 98.2 ms a round on both,
    two streams 106.0 ms a round against 106.1.
  • Licence: Apache-2.0 (the drafter's model card and LICENSE).
Kernel contract
  • Registered:
    • All ten kernels in dflash2.metal: the target's tap; the drafter's norms, embed, q/k prep, convolution, SiLU,
      attention, top-k and walk. Each has float64 references (zig/tests/contract/metal_ricursive_dflash2*.zig).
    • Gate-5 records in ricursive_records.zig.
    • llama's row projections, GEMM and merge at the drafter's shapes: fc's 25,600 reduction, and its merge over 34 and
      130 blocks.
  • Ten planted controls, each editing one kernel's source; each turns its gate red.
  • tf-kernel-contract --only ricursive, M5 Ultra, exit 0, on this tree before its last rebase (see above):
SUMMARY 75 kernels: 75 pass gates 1-4, 0 fail, 0 known findings; 25 controls: 25 red as planted, 0 missed; 0 limit problems; gates 831015 ms, limits 3056 ms
  • Chips not run: M1 to M4 (refused at load), other M5 sizes.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @BobClawblaw. Drafted equal to plain, the plain path's 53 hashes unchanged and ten planted controls red is a careful first drafter PR. Two things. The family itself still needs a decision on #2171, so this waits on that. And the drafter follows "SGLang's DFlash2DraftModel": is that file part of ricursiveAI's drafter release (then please add it to THIRD_PARTY_NOTICES at its revision), or SGLang's own code? If it's SGLang's, we can't take code from it or work from it, so please say where the drafter's arithmetic came from.

@BobClawblaw

Copy link
Copy Markdown
Contributor Author

Thanks, and a fair catch: it's SGLang's own code. ricursiveAI's drafter release holds only config.json, the README, the licence and the weights. DFlash2DraftModel is SGLang's class, and I worked from SGLang's sglang/srt/models/dflash.py. That doesn't meet your rule, so I'm rebuilding the drafter's arithmetic from sources TensorFold can use, without SGLang:

  • z-lab/dflash's dflash/model.py at 07ebd93db9f472af339b644bb70221ad8428328a (MIT, the source TensorFold already credits for the qwen27 drafter). It has z-lab's own DFlash2DraftModel: the fc and norm over the taps, the sliding layers, the grouped dynamic causal convolutions and the candidate selector's codebooks.
  • The drafter release's config.json and README at 35609b3be3dd08a822b3ba1257c1ea5dd6f87632: the taps, block size, selector settings and no_rope_layers: [4].
  • ricursiveAI's inference/server/README.md in Ripple-65B-Preview at 240d5966cbe00f0627c699e59ef58560b60303cf (already in the notices): the 16,384-token draft window.

I'll redo the drafter and its float64 references from those, rerun drafted == plain, the kernel contract and the plain-path A/B, add z-lab to the notices for families/ricursive/, and update the PR text. Until then the PR is back in draft. #1235's MiniMax drafter names the same class; I'll check where its arithmetic came from and say so there.

@BobClawblaw

Copy link
Copy Markdown
Contributor Author

The rebuild is in (904eb81da). I checked the drafter step by step against z-lab's dflash/model.py (MIT, 07ebd93d) and the release's config, with ricursiveAI's own inference/server/model.py (240d5966, already in the notices) for the NoPE layer and the 16,384-token draft window. No SGLang source was consulted. Two rounding orders differed and now follow z-lab: the convolution adds bf16(base·x) and then dyn·x per tap (its addcmul), and the selector rounds the pair term and the score to bf16. Their float64 references and the convolution's control moved with them. THIRD_PARTY_NOTICES.md credits z-lab for families/ricursive/dflash2.zig and dflash2.metal.

On the full model: drafted == plain 12/12 and 12/12 under structured output, seeded sampling 9/9, soak 60/60, concurrent 12/12; speed and acceptance unchanged (48% of drafted tokens kept with one stream, 56% with two). Contract 75/75 with 25 controls red; the body has the details. Still a draft until #2171 is decided.

@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Thanks @BobClawblaw for saying plainly where the first version came from, and for rebuilding it. The drafter now names only z-lab's dflash/model.py and the release. That file is MIT, and it's the architecture our other DFlash2 drafters already credit. The notices entry covers it.

A few things before it leaves draft. The family itself still waits on #2171.

  1. Two facts from the SGLang model file. dflash2_config.zig takes the 16,384-token draft window from inference/server/model.py, and the recipe takes the NoPE layer from it too. Your earlier note found both in sources without that question: no_rope_layers: [4] in the drafter's config.json, and the window in inference/server/README.md. Please cite those in the code and the notices entry instead.
  2. z-lab's layers. Please confirm that the drafter's attention, MLP and norms follow z-lab's own code in model.py (or the config), not a Transformers class it imports.
  3. Never slower at more streams. The numbers cover one and two streams, but with the drafter the server allows up to seven. Please add main-against-branch decode at 4 and 7 streams (--drafter against none, same binary), or have the drafter step aside where it would be slower.
  4. The bf16 release. The drafter is unchecked beside it, but not refused. Either add a drafted-equal-to-plain and speed row for it, or refuse --drafter with the bf16 release by name for now.
  5. The head. The receipt names 925ae9563. Please name 904eb81da, or say they're the same code.
  6. A CHANGELOG line for the drafter, citing "(ricursive: the publisher's DFlash2 drafter for Ripple-65B (stacked on #2231) #2260, fixes Ripple-65B (ricursive) on Metal: the publisher's DFlash2 drafter #2259)", with "Thanks to @BobClawblaw."

With the stack on today's main, tests/contract/metal_main.zig reaches 603 lines; main is already at 600. That's the same problem as #2074, so the family's rebase will need to keep that file at 600 lines or fewer.

@BobClawblaw

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. All six are answered on the new head, df17d1375, with the stack rebased on main 0d7152955.

  1. The two facts. dflash2_config.zig reads the NoPE layer from no_rope_layers in the drafter's config.json
    and cites inference/server/README.md for the 16,384-token window. The doc and the notices entry cite the same two
    sources, and inference/server/model.py is no longer cited for the drafter.

  2. z-lab's layers. dflash/model.py imports Qwen3MLP, Qwen3RMSNorm, Qwen3RotaryEmbedding and rotate_half
    from transformers' Qwen3 module, so the drafter doesn't follow those. Its norms, rotary tables and MLP follow the
    release's own classes in modeling_ricursive.py (RicursiveRMSNorm, RicursiveRotaryEmbedding, RicursiveMLP),
    which is ricursiveAI's file with no library copyright, at the drafter config's eps, theta and widths. From z-lab's
    own code it takes Qwen3DFlashAttention (projections, per-head q/k norm, context and block keys concatenated before
    the norm, apply_rotary_pos_emb's points), the layer order of Qwen3DFlashDecoderLayer, the taps' fc and norm,
    GroupedDynamicCausalConv and CandidateSelector. The attention's core is z-lab's call to SDPA, which is plain
    scaled softmax attention; our kernel computes that, not a transformers function. The notices entry says this.

  3. More streams. On the NVFP4 release at --parallel 7, one binary run with and without --drafter
    (e1d719275, this head's drafter code before the cap). Tokens a second for all streams together, prose / code /
    structured / list:

    streams plain drafted
    4 32.6 / 31.8 / 31.1 / 32.5 30.3 / 32.0 / 31.3 / 43.1
    7 43.2 / 42.9 / 41.6 / 43.4 40.2 / 34.7 / 22.9 / 34.1

    Each stream's block is its own drafter pass, so a round goes from 126 to 278 ms at four streams and 166 to 406 ms
    at seven, while the tokens kept a round fall to 2.35 and 1.89. Four is a tie apart from lists, and seven is slower
    everywhere. So with --drafter the server now refuses --parallel above 2 by name, saying why. The doc has the
    table.

  4. The bf16 release. Checked rather than refused. Beside the bf16 release, drafted == plain 12/12 and concurrent
    12/12, on b7ff7854f (this head's drafter code before the rebase; the comment-only lean edits since). Speed,
    --parallel 2:

    streams plain drafted
    1 5.8 / 5.0 / 4.9 / 5.0 13.4 / 30.7 / 13.3 / 18.7
    2 11.5 / 11.0 / 10.5 / 11.8 25.7 / 63.3 / 26.0 / 54.8

    That's 2.2 to 6.1 times plain. A round keeps 2.93 tokens with one stream and 3.15 with two. The doc says that
    structured output, seeded sampling, the soak and resumed turns were run beside the NVFP4 release only.

  5. The head. 925ae9563 differed from 904eb81da only in the doc's speed table, so the code was the same. The
    receipt now names df17d1375.

  6. CHANGELOG. Added under Unreleased with "(ricursive: the publisher's DFlash2 drafter for Ripple-65B (stacked on #2231) #2260, fixes Ripple-65B (ricursive) on Metal: the publisher's DFlash2 drafter #2259)" and "Thanks to @BobClawblaw."

tests/contract/metal_main.zig is at 597 lines. #2231's first commit moves H3's registration into its own Suite, as
the qwen27 attention rows did in 5fe04fa, and #2231 is rebased with it. Main's new lean rule flagged five two-line
comments in the drafter. Each is now one line, so lean is 0 new.

Checks on this head (each exit 0): zig build test --test-timeout 10m, test-kernel-contract, test-golden
(322/335 equal to server goldens, 0 unexpected differences), test-server-security test-pull-security (24 passed),
fuzz-cortheon, zig fmt --check build.zig zig, lean (252 on the base, 0 new). tf-kernel-contract --only ricursive
on the M5 Ultra:

SUMMARY 75 kernels: 75 pass gates 1-4, 0 fail, 0 known findings; 25 controls: 25 red as planted, 0 missed; 0 limit problems; gates 831015 ms, limits 3056 ms

The suite ran on the same tree before its rebase over main's last two commits, a changelog line and build.zig's
portable-names flag. zig build test, fmt and lean were run again on df17d1375.

@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

/atc exchange 46d290cb7bf5b9c4f378215ac437fba71d39758355c048d08965f3db29e98aa2

@BobClawblaw

Copy link
Copy Markdown
Contributor Author

/atc exchange a6621e0cf0281aea16665a2b398086be7563bcfde69edf6552af92206879e1c3

ricursiveAI's Ripple-65B-Preview (model_type ricursive) served from the publisher's
own releases as published, read in place from their shards: the NVFP4 release
(ModelOpt NVFP4 MLPs, bf16 attention, 53 GiB) and the bf16 release (121 GiB).

- Family in zig/src/families/ricursive and kernels in zig/kernels/metal/ricursive:
  PeriNorm, q/k norms, RoPE on sliding layers (1,024-key window, 3,072-slot ring),
  NoPE global layers every fifth, fp32 sigmoid gate, value residual, clamped SwiGLU.
- NVFP4 MLPs: E2M1 nibbles, E4M3 block scales and the fp32 global, applied exactly;
  activations stay bf16 (the publisher's SGLang serving is W4A4; disclosed in the doc).
- Exact bf16 caches; prompt chunks planned on one 2,048-row grid, so resumed == fresh.
- Every kernel registered with the kernel contract: 36 pass gates 1-4, 15 controls red.
- Server: replies are always read for a think block when the model decides itself
  whether to think (Info.reasoning_optional); a thinking budget and logprobs are
  refused, since Ripple's think markers are spelled from several tokens.
- tools/zig/ricursive_reference.py: the float64 / bf16-point CPU reference.
- docs/recipes/ripple-65b-metal.md: correctness (KL), harness speed and serving checks.
…release code

Longer contexts are refused by name until a run at that length qualifies them.
THIRD_PARTY_NOTICES names modeling_ricursive.py and the release's SGLang model,
which the family follows for its layer order and rounding points.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ripple-65B (ricursive) on Metal: the publisher's DFlash2 drafter

2 participants