Skip to content

MiMo-V2.6-Flash on CUDA: EXL3, DFlash, two GPUs - #728

Open
ajensenwaud wants to merge 13 commits into
ashhart:mainfrom
ajensenwaud:mimo-v2
Open

ajensenwaud wants to merge 13 commits into
ashhart:mainfrom
ajensenwaud:mimo-v2

Conversation

@ajensenwaud

@ajensenwaud ajensenwaud commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

MiMo-V2.6-Flash on the native CUDA engine, on two DGX Sparks as tensor-parallel ranks, with the pack's DFlash drafter. Proposal and discussion: #725.

Admission is what was validated: GB10 (sm_121) at two ranks. open refuses any other GPU by name (MimoV2RequiresSm121), as DeepSeek-V4.1's kernel load does, and refuses one rank or any other world (MimoV2RequiresTwoRanks), at the weight share as well as at open. tf-mimo refuses both too. Host tests cover each refusal. One rank, and the 2.27 bpw pack that fits one GB10, wait for their own receipts.

Based on current main (2836e9b2). Twelve commits:

  1. dfcd1b50 lanes: a geometric draft depth whose round cost grows with context. About 100 lines in core/lanes, plus a host test. It is off unless a family sets geometric_depth, so other families keep the per-depth rule unchanged. It can become its own PR if you prefer.
  2. 6ca03954 exl3 cuda: the common prompt path takes 3-bit mul1 experts over fp16 rows. The prompt kernels in exl3_prompt.cuh / exl3_experts.cu take the half-bit width and codebook as template arguments: tf_exl3_prompt_gu / _down stay the 4-bit mcg instance, _k6_mul1 is the 3-bit mul1 one. exl3_prompt.zig picks the instance by layer and takes the rows' dtype; GLM's call passes .bf16. On sm_121 the 4-bit gate/up SASS is unchanged and the down kernel's differs in one mask constant (the same value at 256 threads).
  3. 1a7f7c4f MiMo-V2.6-Flash on CUDA: native kernels on the shared EXL3 operators, DFlash, two GPUs, prompt cache.
    • No captured Triton set and no EXL3 copies: cuda/mimo_exl3.zig and the vendored kernels are gone. Projections and decode experts run on cuda.exl3.Linear and cuda.exl3_experts, prompt experts on the common prompt kernels (commit 2).
    • The family's kernels (zig/kernels/cuda/mimo_v2): ops.cu (RMS norms, SwiGLU, fp32 router and top 8, the prompt experts' slot sum), attention.cu (queries and caches, chunked decode attention with sinks, and a tensor-core prompt kernel for the 9 full layers), glue.cu, dflash.cu.
    • Every prompt token runs on the prompt path, in chunks that end at the prompt cache's marks; a row's bits do not depend on its chunk (the prompt kernel's 64-key tiles start at 0), so a resumed prompt reads as a fresh one. The 256-token decode/prompt cut is gone.
    • Kept prompt states (kept.zig): the slot's full-layer rows stay pinned, the sliding and drafter windows are copied (33 MiB a rank); a resume binds to the state's slot or copies the rows into a free one. Rank 1 follows every cache step over the plan link.
    • Build: zig/build/mimo.zig (module, tf-mimo, test-mimo), laid out like kolibri.zig; one line each in cuda.zig (tf-mimo), cuda_engines.zig (the engine) and cuda_family_host.zig (host tests, beside Dory's). native/cuda.zig: one line, the registry entry; admission uses main's weightShare hook.
    • Recipe page: docs/recipes/mimo-v2.6-flash.md.
  4. 5ab18d7c kernel contract: every MiMo-V2 kernel through gates 1-4, its launches for gate 5. contract_mimo*.zig (28 kernel forms against float64 or their stated rules, at the served shapes, launched as the family launches them) and mimo_cuda_records.zig (sm_121, nvcc 13.3, including the two mul1 prompt instances).
  5. 4f56747f MiMo-V2 decode: attention rows on tensor cores, norms in registers, a shallower top-8 tree. Brings one-stream decode back level with the captured-set build.
  6. 14bc442d MiMo-V2: a refused load frees only the layers it filled.
  7. 7cd49cbe MiMo-V2: a bad TF_TP_ refuses the weight share instead of sizing one GPU.*
  8. f867006b tf-mimo follow: refuse one rank before reading a session.
  9. 525bb1d6 MiMo-V2.6-Flash recipe: licences and sources of the model, the pack and the drafter.
  10. 1d777654 MiMo-V2: refuse every GPU but GB10 (sm_121) by name. The check is the first line of open, and of tf-mimo. A host test runs open on a fake driver at sm_80 to sm_120, all refused, and at sm_121, which passes the check.
  11. dc53407f MiMo-V2: refuse one rank by name; only two were validated. rankView, the weight share and open refuse TF_TP_WORLD unset, 1, or anything but 2. The recipe drops the one-GPU and 2.27 bpw serving claims.
  12. dc92b94c MiMo-V2.6-Flash recipe: full revisions of the model and both packs.
    • Model XiaomiMiMo/MiMo-V2.6-Flash-RL at 5711b268169967567844e1e560e8a3966da959b1, the revision both packs were converted from and the one validated. The repository's head since then, dcd3fd83, changes only the vision preprocessor_config.json.
    • Pack ajensenwaud/MiMo-V2.6-Flash-RL-exl3-3.0bpw at 139d9a4e1eb32193890036fdddd521fe37033a30.
    • Pack benthecarman/MiMo-V2.6-Flash-RL-exl3 (2.27 bpw, the drafter's source) at 5ae87b830eb950c705967aea6130601a8e1f74a5.

The grouped experts load-ahead that was commit 5 is now its own PR, #1505, with a GLM-5.3-Flash A/B. The gate below was measured with it in: without it MiMo's teacher-forced logits are byte-equal, so the identity and quality results stand, but decode tok/s is lower by the grouped kernel's difference (about 109 against 96.6 µs a launch) until #1505 lands.

The 8-bit KV cache stays out (fp16 only), as you asked.

Gate (the previous head with the load-ahead, two DGX Sparks)

GB10 (sm_121), checkpoint ajensenwaud/MiMo-V2.6-Flash-RL-exl3-3.0bpw, --parallel 4 --context 608256 --top-k 20 --top-p 0.95, RoCE between ranks.

  • Kernel contract (full CUDA runner on GB10, this head): SUMMARY 446 kernels: 446 pass gates 1-4, 0 fail, 5 known findings; 312 controls: 312 red as planted, 0 missed; 214 limit problems. None of the limit problems are MiMo's or the shared experts': every MiMo launch and the three tf_exl3_grouped launches match their nvcc 13.3 records live on sm_121.

  • Lane contracts (greedy, by the server's token_sha): drafted == "draft": false 6/6 (prompts of 29 to 10,806 tokens); concurrent == solo 24/24 (three groups of four, drafted and plain).

  • Resumed == fresh (prompt cache on, 3.8 GiB): 117/117 replies identical to the cold serve's, 63 resumed (102,602 of 250,302 prompt tokens from the cache), including resume points at 63/64/65, 127/128/129, 255/256/257, 1,023/1,024/1,025, 2,047/2,048/2,049 and 4,095/4,096/4,097 tokens (18/18 resumed at the mark). With "draft": false, 117/117 identical (16 resumed).

  • Teacher-forced (tf-mimo m1): against exllamav3 on the same pack, see Fidelity against exllamav3 below. Prompt path: one chunk == two chunks on the last row 8/8; against the decode rows, KL mean 0.0073 (different kernels, so not bit-equal).

  • Quality (cache on): GSM8K 477/500, MMLU 190/228, passkey 10/10 (32K and 120K, five depths each).

  • Prefill, tok/s (spark-bench depth, cold):

    8K 32K 64K
    this PR 1,106 1,037 962
    first native build of this rework (decode experts on the prompt path) 523 406 310

    The recipe's release build reads 16K at 1,309 and 64K at 1,183.

  • Decode, tok/s (spark-bench --preset full, 512 tokens, thinking off, temperature 0; chat / code / agent / report):

    1 stream 4 streams, aggregate
    this PR 42.0 / 65.8 / 54.2 / 45.8 75.6 / 108.1 / 89.1 / 82.6
    recipe release build (16-bit, an earlier session) 49.8 / 64.3 / 55.7 / 53.4 81.6 / 115.6 / 90.6 / 80.6

    The bench tags each prompt at random, so one-stream figures move with the text. On fixed prompts (512 tokens, one stream, temperature 0, both builds served the same day), tok/s:

    "draft": false drafted
    this PR 41.5 / 41.3 / 41.0 / 41.3 47.8 / 59.8 / 61.2 / 46.9
    captured-set build 42.7 / 43.0 / 43.5 / 43.0 47.3 / 64.3 / 62.5 / 49.9

Fidelity against exllamav3

The reference is exllamav3 1.6.0, the build that wrote the 3.0 bpw pack, run on the same pack. It is teacher-forced over the same 1,905 positions: 8 texts, 254 prompt rows and 1,651 generated rows. It runs its no-cache path (flash_attn_nc), streamed one module at a time on one GB10, and its logits are its default fp16 head output. Ours come from tf-mimo m1 at TP=2 on this PR's kernels.

top token = exllamav3's KL(exllamav3 ‖ engine), nats: mean / p50 / p99 / max teacher-token NLL
this PR 1,879/1,905: 26 differ, plus 2 exact ties 0.0043 / 0.00031 / 0.053 / 0.34 0.4957
the earlier captured-set build 1,881/1,905: 24 differ, plus the same 2 ties 0.0050 / 0.00032 / 0.055 / 1.41 0.4961
exllamav3 against itself (prefix run vs full run, 2,719 rows) 31 flips (1.1%) 0.0019 mean
  • Counting. In the table, a position counts as a match when the two engines share a top token. An exact tie is a position where exllamav3's top two logits are equal in its fp16 output and our top token is one of the two. Both ties are in the short text, at steps 48 and 59.
  • Why 1,878 and 28 before. The review reply counted those two ties two different ways:
    • Its count, 1,878 (27 misses), broke a tie by the lower token id. That matched ours at step 48 and not at step 59.
    • Its table's sort broke both ties against ours, so the table listed 28.
    • Both numbers describe the same positions: the 26 that differ plus the 2 ties.
  • The 26 that differ. At 25 of them this PR ranks exllamav3's top token second, and third once (edit, step 50). exllamav3's lead over its second token there runs from 0.016 to 0.31 logits, median 0.13. The full list is below.
  • exllamav3's fp32 head as the reference. Taking exllamav3's fp32 head output instead removes the ties: 1,876/1,905 (29 differ, no ties), with KL 0.0043 / 0.00030 / 0.054 / 0.34.
  • Teacher-token NLL covers the 1,897 rows that have a next token; exllamav3's own is 0.4984.
Every position where the top tokens differ, with exllamav3's margin

Margin = exllamav3's top-1 minus top-2 (logit, prob). "rank" = where this PR puts exllamav3's top-1. Token = idtext. Next = the teacher token at that position.

text step KL next exllamav3 top-1 (logit, p) exllamav3 margin (logit, p) this PR's top-1 (p) this PR's p of exllamav3 top-1, rank exllamav3 p of this PR's top-1
en_story 18 (prompt) 0.000636 1943 message 25382 mysterious (16.8438, 0.1630) 0.0312, 0.0050 14888 strange (0.1622) 0.1615, 2 0.1580
en_story 37 0.00272 5001 coming 5001 coming (18.8438, 0.2429) 0.0781, 0.0183 13458 storm (0.2410) 0.2324, 2 0.2246
en_story 40 0.0059 576 The 1260 He (18.0312, 0.3508) 0.1875, 0.0600 576 The (0.3144) 0.3092, 2 0.2908
en_story 87 0.00398 45035 gratitude 5775 wonder (22.4688, 0.4402) 0.0625, 0.0267 45035 gratitude (0.4338) 0.4308, 2 0.4135
en_story 95 0.0598 264 a 264 a (17.0625, 0.1783) 0.2500, 0.0394 504 from (0.2353) 0.1526, 2 0.1389
en_story 161 0.0255 326 l 1943 message (17.3125, 0.3852) 0.1719, 0.0608 326 l (0.3987) 0.3000, 2 0.3244
en_story 177 0.00457 1035 would 1035 would (23.4062, 0.4174) 0.1562, 0.0604 44419 climbed (0.3757) 0.3751, 2 0.3570
en_story 216 0.0362 73632 lodged 73632 lodged (22.2188, 0.2552) 0.1094, 0.0264 4279 half (0.2813) 0.2623, 2 0.2287
en_story 263 0.00522 11, 304 in (25.7188, 0.4495) 0.0938, 0.0402 11, (0.4278) 0.4165, 2 0.4093
zh_explain 63 0.0199 101068无法 53153不能 (21.8906, 0.2774) 0.0312, 0.0085 101068无法 (0.3350) 0.2731, 2 0.2689
zh_explain 119 0.0325 117184粒子 100369两个 (22.5000, 0.4304) 0.3125, 0.1155 117184粒子 (0.4073) 0.3570, 2 0.3149
zh_explain 133 0.0398 1773。 106293的状态 (25.8438, 0.3952) 0.1250, 0.0464 1773。 (0.4682) 0.2807, 2 0.3488
code_py 145 0.0289 11, 13. (23.8594, 0.3009) 0.0625, 0.0182 11, (0.3421) 0.2576, 2 0.2783
math 60 0.0157 18611 \n\n 6771 Let (14.8438, 0.0822) 0.2578, 0.0187 18611 \n\n (0.0781) 0.0755, 2 0.0635
tool_json 22 (prompt) 0.00878 25: 510:\n (17.7812, 0.2198) 0.0781, 0.0165 25: (0.2269) 0.2251, 2 0.2033
edit 49 0.023 334** 88757Rename (16.1406, 0.1151) 0.2188, 0.0226 334** (0.0988) 0.0956, 2 0.0925
edit 50 0.035 16141Answer 18896Expected (16.0625, 0.0680) 0.1875, 0.0116 16141Answer (0.0761) 0.0622, 3 0.0563
edit 72 0.0122 1760 count 1760 count (24.5781, 0.5373) 0.1562, 0.0777 13195 cnt (0.5366) 0.4598, 2 0.4596
edit 279 0.0293 25: 982):\n (30.0312, 0.5592) 0.2656, 0.1305 25: (0.5490) 0.4393, 2 0.4288
list 106 0.00615 576 The 27360 Rot (22.9062, 0.4174) 0.1406, 0.0548 576 The (0.4054) 0.3734, 2 0.3626
short 0 (prompt) 0.029 0! 358 I (8.7500, 0.0129) 0.1953, 0.0023 11, (0.0173) 0.0142, 2 0.0075
short 10 0.00149 2371-g 2371-g (17.5156, 0.0956) 0.0156, 0.0015 1448-m (0.0996) 0.0948, 2 0.0941
short 16 0.00917 12- 5338First (14.1562, 0.0263) 0.0938, 0.0024 12- (0.0298) 0.0213, 2 0.0240
short 54 0.00165 358 I 358 I (20.8594, 0.3520) 0.0625, 0.0213 576 The (0.3544) 0.3427, 2 0.3307
short 208 0.00613 12440 correctly 12440 correctly (24.6719, 0.4150) 0.0625, 0.0251 6915 fine (0.4299) 0.4114, 2 0.3898
short 237 0.00629 4460 trying 4460 trying (17.5000, 0.1722) 0.1562, 0.0249 537 not (0.1535) 0.1494, 2 0.1473

The two exact ties, both in the short text. exllamav3's top two are equal, and our top token is one of them:

step KL next exllamav3's tied pair (logit) this PR's top-1 (p)
48 0.00195 21556 Sprite 21556 Sprite and 51548 Collision (16.4219) 21556 Sprite (0.1737)
59 0.00176 17051 sprite 2781 player and 17051 sprite (18.4219) 17051 sprite (0.3132)

Host (this head, on main 2836e9b2): test-mimo, test-kernel-contract, test-native-options-host, test-kernel-symbols-host, test-shared-lane-regressions-host and test-tp-cli pass; tf-mimo, native, compile-config-server and compile-cuda-tests build. test-cuda-host passes except two core prompt-cache spill tests, rows_async_test ("the job permit bounds metadata ...") and rows_close_test ("spill-only flushes more than eight RAM states ..."). They fail the same way on main 2836e9b2 in the same build container. zig fmt --check and git diff --check are clean; tools/zig/lean_check.py finds 426 problems, the same as main.

Next

  • Decode: about 0.8 ms a token left against the captured-set build, mostly the norms and the top 8.
  • Prefill: the dense projections still run 128 rows a call.
  • One rank: refused for now. To admit it needs its own equalities and an exllamav3 run on the 2.27 bpw pack, which fits one GB10. TP=1 against TP=2 is expected to differ in the last bits, so that would be a closeness check (see the review reply).

Notes for review

satindergrewal added a commit to satindergrewal/TensorFold that referenced this pull request Oct 13, 2026
…3 experts

Checked ash hart PRs open+closed (read-only). Direct hits on our
problems, all OPEN, branch BHCC2025/TensorFold exl3-cuda-experts:
- ashhart#741 EXL3 routed experts behind experts.Grouped: gate+up+SwiGLU
  ONE kernel, down on pair rows, each expert's rows rotated by its
  own suh INLINE (register-resident rot for 1-2 pair chunks - the
  problem we had deemed impossible tile-incrementally). Rides on
  grouped.zig's Plan (in our tree). 297cu+124zig lines, with an
  exactness test harness (tf-cuda-test exl3-experts) - 367,872
  values within FP64 bounds, alone==grouped byte-equal.
- ashhart#740 EXL3 projections behind qlinear (the stacked base).
- ashhart#718 branch-free trellis window extraction (perf).
- ashhart#729 share decoded weights across prefill rows (perf).
- ashhart#728 MiMo-V2.6 CUDA EXL3 two-GPU (another full CUDA EXL3 family).
- ashhart#765 GLM CUDA multi-rank doorbells; ashhart#770/777/768 nemotron CUDA
  single-launch shared rounds + batched MTP drafting (patterns for
  our later concurrency work).
PLAN: vendored exl3_experts.cu/.zig + exl3.cuh into our tree (this
commit, unwired). Next: build wiring (fatbin + mod), port moeBlock
to Grouped.decode with the router->plan plumbing from grouped.zig,
run their exl3-experts test on our checkpoint, parity, bench. Our
own grouped kernels (stage-1 parity) stay as fallback.
@ashhart

ashhart commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Thank you, @ajensenwaud, the shared-lane DFlash path and explicit optional FP8 policy give the MiMo work useful boundaries.

We checked 83fd3f2. native.zig:374 still requires captured kernels, :379 hides bad TP settings with catch null, and rankWeights:412-416 divides bytes despite replicated embeddings/drafter. We can repair refusal and placement accounting while moving execution onto owned kernels and the shared EXL3 module.

We will extract the depth rule and infrastructure first, then the family with prompt reuse and final-source numerical/peer/memory gates; 16-bit cache remains the default. Please provide the original/converted checkpoint and vision inventory with the pinned workload so image obligations and the final reference gate are explicit. Earlier pre-rebase receipts remain attached to their source.

Posted by Ash's AI Assistant Synthena

@ashhart

ashhart commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thank you, @ajensenwaud, for #728 and the 24 concurrent-versus-solo checks on two GPUs, with @jayleaton and the common EXL3 contributors. MiMo-V2.6-Flash is not included in 1.1.1. Its current CUDA path depends on a Python-captured Triton set that TensorFold does not ship; without TENSORFOLD_CUDA_KERNELS it refuses to start. Native kernels for its eight Triton launches come next, followed by full-model fidelity, drafted/plain, resumed/fresh, concurrent/solo, two-rank failure and compiler/resource gates on the integrated EXL3/DFlash path. This is held for a later 1.1.x; captured-set receipts do not qualify the new native path.

Posted by Ash's AI Assistant Synthena

@ashhart

ashhart commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Thanks @ajensenwaud for MiMo-V2.6-Flash on CUDA. Main has moved a long way since this branch (it is stacked on #703), so rather than ask you to rebase, we'll port its MiMo-specific commits onto current main ourselves, keeping you as their author and saying in each message what was adapted. Then it goes to the GB10 kernel contract and served-equality runs on one and two GB10s before it lands. If you are partway through a change here, please say so, so we don't collide.

- Synthema, Ash's AI Assistant.

@ajensenwaud

Copy link
Copy Markdown
Contributor Author

Thank you. Very keen to contribute. I know it's moving fast. I can rebase and recommit if that helps?

@ashhart

ashhart commented Oct 22, 2026

Copy link
Copy Markdown
Owner

@ajensenwaud, a question from the port before we combine the MiMo paths with main's shared EXL3 operators. In the branch, the arithmetic a row gets depends on how many rows or tokens arrive together: the router picks its tile by row count (BM 16 up to 16 rows, then larger tiles with BK 256 to 64 and BLOCK_E 32 to 64, zig/src/families/mimo_v2/forward.zig:404 and :456, with the same switch in prompt.zig:381); prompt projections use a separate GEMM with BK 32 while decode uses the EXL3 linear (prompt.zig:204 against forward.zig:268); experts switch to per-expert GEMMs from 3,072 rows (prompt.zig:389); and prompts of 256 tokens or more take the prompt path (native.zig:218). Our rule is that a row's result never depends on batch size or prompt chunking, so we need one arithmetic per operation, or a fixed-order equivalent with checks at 16/17 router rows, 255/256 prompt tokens and 3,071 to 3,073 expert rows. Would you be happy for main's shared EXL3 operators to define the reference, with the oracle fixtures regenerated against them, or would you rather supply fixed-order versions of the MiMo paths? The rest of the port (the geometric draft depth, which stays off unless a family opts in, and the FP8 KV option) is in hand on our side.

- Synthema, Ash's AI Assistant.

@ajensenwaud

Copy link
Copy Markdown
Contributor Author

Thanks Synthema, and thanks for porting rather than asking for a rebase. Answers to both open questions.

1. Reference arithmetic. Please let main's shared EXL3 operators define the reference and regenerate the oracle fixtures against them. One arithmetic per operation is the right contract: our 24/24 concurrent-versus-solo result was measured, not guaranteed by construction, and I'd rather not keep a parallel MiMo numeric stack. Nothing MiMo-specific needs to survive in the linears or experts. The router tile switch came from the Python engine's shared._tile; one fixed tile for every row count is fine with us (the router is 4,096 x 256, 2 MB of weights a layer).

Two requests so the port keeps what the branch was for:

  • Speed. The thresholds exist for measured reasons, so please keep them as performance choices once the paths are bit-equal. Prompts under 256 tokens on decode rows took 4-stream code from 94-105 to 117-120 tok/s aggregate; grouped experts up to 3,072 rows cut a 512-row prompt from 2.62 s to 0.74 s. Bar at 83fd3f2 on two GB10s: 1 stream code 76.3 tok/s with DFlash, 4 streams 113.4; prefill 4K/16K/64K/128K 772 / 1,317 / 1,210 / 1,066 tok/s.
  • Quality. Fixtures regenerated against main prove self-consistency, not fidelity. Please keep a full-model gate against an external reference too: ours is the pinned 500-item GSM8K and MMLU-Pro sets (greedy, thinking off) plus per-position KL against exllamav3 on the same pack.

I can run both on two Sparks against your port branch and post the receipts; point me at it.

2. Checkpoint, vision inventory, workload (your 17 Oct note)

  • Original: XiaomiMiMo/MiMo-V2.6-Flash-RL @ 5711b268 (166 GB, 65 shards). Multimodal content: visual.* (28 blocks), speech_embeddings.*, audio_tokenizer/ (1.9 GB).
  • Converted: ajensenwaud/MiMo-V2.6-Flash-RL-exl3-3.0bpw @ f2d8d100, exllamav3 v1.6.0, -b 3.0 -hb 8 -mb 4 -hq -cr 250 -cc 2048 -cb mul1. Text only: no vision or audio tensors. DFlash drafter at 4 bpw from benthecarman/MiMo-V2.6-Flash-RL-exl3 @ 5ae87b83.
  • Image obligations: none claimed by this PR. Vision would be a separate PR; until then I'd suggest the family refuses image content rather than silently dropping it.
  • Pinned workload: tools/upstream_contracts.py in the recipe repo (six prompts covering all three prompt paths, greedy, 256 tokens, thinking off), results in results/zig-z7/upstream/upstream-contracts.json; eval items in results/evals/items-*.json.

Nothing in progress on the branch, so no collision risk.

@ajensenwaud ajensenwaud changed the title MiMo-V2.6-Flash on CUDA: EXL3, DFlash, two GPUs (stacked on #703) MiMo-V2.6-Flash on CUDA: EXL3, DFlash, two GPUs Oct 25, 2026
@ajensenwaud

ajensenwaud commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks Synthema. To avoid duplicated effort: I've rebased #728 onto current main (2ff01910), so it no longer depends on #703. It is now three commits: the geometric depth rule (off by default), the MiMo family, and the 8-bit cache. The 8-bit cache is now opt-in through main's existing --kv-cache fp8, since main refuses --policy on every CUDA family. native/cuda.zig is down to the one registry line. The family's build sits in zig/build/mimo.zig, like Kolibri's, and its EXL3 bindings are cuda/mimo_exl3.zig, next to main's shared module.

I also fixed two points from your earlier review. Admission now uses main's weightShare hook, counting the embeddings and drafter whole on each rank. A bad TF_TP_* setting now refuses to start instead of being silently ignored.

test-cuda-host, test-mimo, the shared lanes tests, zig fmt and lean_check pass (no new problems), and CI is green. Nothing else is in progress on my side, and I'm happy for you to move it onto the shared EXL3 operators as discussed.

@ashhart ashhart left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ajensenwaud. Commit 026bd20 adds an fp8 KV cache (--kv-cache fp8) that the PR itself notes is not bit-equal; TensorFold takes no lossy formats, even opt-in, so that commit has to go. Main has also moved MiMo-only kernels and geometric depth to a separate MiMo track (1964746), so the MiMo EXL3 kernels here, copies beside the shared cuda/exl3.zig, need to fit that plan and be registered with the kernel contract. It's about 150 commits behind and conflicts in docs/recipes/README.md and native/cuda.zig. After a rebase we'd check on two GB10s that a resumed conversation equals a fresh run across the 256-token boundary, and TP=2 equals TP=1.

@ashhart

ashhart commented Oct 26, 2026

Copy link
Copy Markdown
Owner

Review against main e9b69ed. Commit 026bd20 adds an 8-bit key/value cache (--kv-cache fp8) that the PR itself notes is not bit-equal. TensorFold doesn't take lossy formats, even opt-in, so that commit needs to come out. Beyond that:

  • Main moved MiMo-only kernels and geometric depth to the separate MiMo track (1964746); whether MiMo lands is a maintainer decision about that track.
  • The MiMo EXL3 kernels are copies beside main's shared cuda/exl3.zig, and none are registered with the kernel contract. Please reuse the shared EXL3 path where it fits and register every launch.
  • The branch is about 150 commits behind and conflicts in docs/recipes/README.md and native/cuda.zig.
    Geometric draft depth is off by default, which is right. Device plan once rebased (two GB10s): a resumed conversation equal to a fresh run across the 256-token boundary (prompts under 256 tokens run as decode rows), and TP=2 equal to TP=1.

@ajensenwaud

Copy link
Copy Markdown
Contributor Author

Yes in progress. Working on it.

ajensenwaud added a commit to ajensenwaud/TensorFold that referenced this pull request Oct 29, 2026
…one GPU

weightShare read TF_TP_* with catch null, so a malformed world or rank was admitted as one GPU's share. It now
returns BadSetting as open does, and the admission reports it through the family's explain. weightShare may return an
error union (cuda_family_memory's share, the same change as MiMo-V2's in ashhart#728). Host test: unset, six malformed
settings, and a two-rank share.
@ashhart

ashhart commented Oct 30, 2026

Copy link
Copy Markdown
Owner

Hi @ajensenwaud, a status update on #728.

What we've checked: the full diff, rebased onto current main without conflicts. The shared EXL3 prompt kernels
reproduce the old code at the existing widths, and every new lanes field is off unless MiMo turns it on. The
refusals for other GPUs and for one rank work as described.

Two small fixes are needed before it can land, and we have both ready if you'd like us to carry them:

  1. zig/build/cuda_family_host.zig adds the MiMo host tests to test-cuda-host, which zig build test runs on
    every host. MiMo imports tp, whose roce_multi.zig builds only on Linux, so zig build test stops compiling
    on macOS. Registering the MiMo host tests only on a Linux host fixes it.
  2. zig/exl3_prompt_host.zig still sets .gateup and .down to a single function, but the prompt kernels now
    hold one per format, so test-exl3-prompt-host doesn't compile. @splat(...) for both fixes it.

What's still owed is our device run. Every cell needs two GB10s at once (TP=2 over RoCE): the full kernel
contract, a GLM control against main, and the MiMo cells on the 3.0 bpw pack. Please confirm that revision
139d9a4e of the pack is the one to test. Your receipts came from a head that still had the #1505 load-ahead, so
we'll record decode speed on this head as its own baseline.

Thank you for your patience with this one.

@ajensenwaud

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Yes please, carry both fixes (Linux-only registration of the MiMo host tests, and @splat in exl3_prompt_host.zig). And yes, 139d9a4e1eb32193890036fdddd521fe37033a30 of ajensenwaud/MiMo-V2.6-Flash-RL-exl3-3.0bpw is the revision to test; it is the one the recipe pins. Agreed on recording decode on this head as its own baseline, since our numbers had the #1505 load-ahead in.

I've rebased the branch onto current main (the only conflicts were the kernel and contract image lists); nothing else changed.

ajensenwaud added a commit to ajensenwaud/TensorFold that referenced this pull request Oct 31, 2026
…one GPU

weightShare read TF_TP_* with catch null, so a malformed world or rank was admitted as one GPU's share. It now
returns BadSetting as open does, and the admission reports it through the family's explain. weightShare may return an
error union (cuda_family_memory's share, the same change as MiMo-V2's in ashhart#728). Host test: unset, six malformed
settings, and a two-rank share.
@ajensenwaud

Copy link
Copy Markdown
Contributor Author

@ashhart there appear to be merge issues on this one now. CI fails on zig, zig-windows and release-paths with zig/src/fabric/tp/tp.zig: error: file exists in modules 'tp' and 'tp0': since 1911e7bc3 (GLM's CUDA family takes fabric/tp over the engine's cuda module) the engines get one tp module from GLM and another from MiMo-V2, and tp.module makes a new one each call. Making tp.module return the one it already made for that cuda module fixes it here, a few lines in zig/build/tp.zig, and it would be byte-identical on #1295.

Happy to push that fix, rebased onto current main, unless you're already looking into it alongside the macOS registration fix.

@ashhart

ashhart commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Thanks @ajensenwaud for tracking that down. Yes, please push the tp.module fix (return the module already made for that cuda module) on this branch. It's the right place, since main only hits it once a second family takes fabric/tp. After 1.2.0, which we're cutting this morning, we'll carry the fixes we agreed and run the two-GB10 cells at TP=2.

@ajensenwaud

Copy link
Copy Markdown
Contributor Author

On it ... fixing this now.

ajensenwaud added a commit to ajensenwaud/TensorFold that referenced this pull request Nov 1, 2026
…one GPU

weightShare read TF_TP_* with catch null, so a malformed world or rank was admitted as one GPU's share. It now
returns BadSetting as open does, and the admission reports it through the family's explain. weightShare may return an
error union (cuda_family_memory's share, the same change as MiMo-V2's in ashhart#728). Host test: unset, six malformed
settings, and a two-rank share.
A family whose head drafts a whole block in one pass (DFlash) pays for a draft pass once, whatever its depth, so the
per-depth rule prices deep drafts too high. With `geometric_depth` set, a lone stream's depth is the one with the most
expected tokens a ms under a geometric chain of its own acceptance rate (a moving average of kept over tried, from
0.6), the forward of its rows plus one draft pass; a stream that stops drafting tries one draft every 8 rounds.

A round's cost can grow with the stream's context: `geometric_ctx` adds, per 1,000 positions past
`geometric_ctx_base`, a step and a per-draft term, so long streams draft shallower as full-attention reads grow.
`geometric_costs` and `geometric_draft_cost` give the per-depth rule of shared rounds the family's own cost table.

The rule follows the Python engine's MiMo-V2 decoder (joint_widths). Everything defaults off: families that do not
set it keep the per-depth rule unchanged.
…rows

The prompt kernels' tile decode, weight-stage sizes and loads take the half-bit width and codebook as template
arguments; tf_exl3_prompt_gu / _down stay the 4-bit mcg instance and tf_exl3_prompt_gu_k6_mul1 / _down_k6_mul1 are
the 3-bit mul1 one (MiMo-V2.6-Flash's routed experts). eligible() takes either verified uniform width, Kernels picks
the instance by layer, and run() takes the rows' dtype for the per-pick rotation (bf16 or fp16; the shared-rotation
kernel stays bf16). GLM passes .bf16. On sm_121 the 4-bit gate/up kernel's SASS is unchanged and the down kernel's
differs in one mask constant (0xe0 -> 0x3e0, the same value for the 256-thread launch).
… DFlash, two GPUs, prompt cache

The mimo_v2 family serves an EXL3 pack of MiMo-V2.6-Flash on the native CUDA server, on one GPU or two DGX Sparks
as tensor-parallel ranks, with the pack's DFlash drafter. Every kernel is built in: no captured set, no copies of
the Python engine's kernels.

- EXL3: projections and decode experts on cuda.exl3.Linear and cuda.exl3_experts; prompt experts on the common prompt
  kernels (their 3-bit mul1 instance, the commit before), then the family's slot sum. Every prompt token runs on the
  prompt path, so the decode/prompt cut is gone.
- kernels/cuda/mimo_v2: ops.cu (RMS norms, SwiGLU, the fp32 router and its top 8, the experts' slot sum),
  attention.cu (queries and caches, chunked decode attention with sinks and its merge, a tensor-core prompt kernel
  for the full layers whose 64-key tiles start at 0, so a row's bits never depend on its chunk), glue.cu (embed,
  residual adds, RoPE tables, argmax), dflash.cu (the drafter's kernels).
- families/mimo_v2: config and rank cuts, loading, the decode forward (eager and graphed by rows and key bucket),
  the prompt path, the DFlash drafter, the engine both ranks run, and kept prompt states (kept.zig: the slot's
  full-layer rows pinned, the sliding and drafter windows copied; a resume binds to the state's slot or copies the
  rows into a free one).
- native.zig: the lane backend. Rank 0 serves and sends every GPU step (and cache step) over the plan link before
  launching it; rank 1 runs `tf-mimo follow`. --parallel sets the streams' cache slots; the prompt cache's marks
  are 2,048 rows apart; a lone stream's first copy window is 6 rows; draft depth uses the lanes' geometric rule with
  a context term.
- native/cuda.zig: MiMo registered. Admission takes main's weightShare hook: the split matrices over the ranks plus
  the embeddings and the drafter whole on each; a bad TF_TP_* setting refuses instead of falling back to one GPU.
- Checks: tf-mimo m1 (teacher-forced KL and argmax against an answer key and an earlier build's dump, one prompt
  chunk against two) and host tests for config, cuts, plans, kept-state runs and slot choice.

The family's build is zig/build/mimo.zig (as kolibri.zig), its kernels are listed in cuda_kernels.zig, and the TP
mailbox fatbin is built once for tf-tp-test and tf-mimo. The ranks run over Jay Leaton's tensor-parallel session and
RoCE mailbox from his two-Spark DeepSeek-V4.1 work.
… for gate 5

contract_mimo*.zig registers the family's 28 kernel forms against float64 or their stated rules (ops, attention
with the tensor-core prompt kernel, glue, the drafter's), served shapes and launches as the family makes them.
mimo_cuda_records.zig carries their sm_121 usage from nvcc 13.3 (cuobjdump), with the two 3-bit mul1 instances of the
common EXL3 prompt kernels.
… shallower top-8 tree

mimo_attn_rows runs a chunk's G <= 16 query heads as one m16 tile on the prompt kernel's loads and mma, keys in
64-key tiles at multiples of 64 split over four warps whose partials fold in warp order; p in fp16 for o (the
contract's allowance as the prompt kernel's). A row's bits still depend only on its own position and window.
mimo_rms keeps its values in registers (unrolled; same bits), mimo_topk runs its tree's last five levels in warp 0's
shuffles (the same pairs; same bits). Sliding rows go from 72 to 8.5 us, norms from 11 to 5.7 us.
load set each layer as it filled it, so a refusal before then (an fp16 embedding, say) had deinit free whatever
the uninitialised layers' expert tables held. Every layer now starts blank, without experts. The host test refuses an
fp16 embedding under the testing allocator; without the blank start it crashes in deinit.
… GPU

weightShare read TF_TP_* with catch null, so a malformed world or rank was admitted as one GPU's share. It now
returns BadSetting as open does, and the admission reports it through the family's explain. weightShare may return an
error union (cuda_family_memory's share); GLM's and Nemotron's plain byte counts pass through unchanged. TF_TP_WORLD
unset is still one GPU. Host test: unset, six malformed settings, and a two-rank share.
With TF_TP_WORLD unset, follow read the cfg of a session that world 1 never opens. The world and rank check now
runs on the parsed settings before the driver opens.
Since 1911e7b GLM's CUDA family takes fabric/tp over the engine's cuda module, and a second family over the same
runtime made its own tp module, so the engines failed with `zig/src/fabric/tp/tp.zig: error: file exists in modules
'tp' and 'tp0'`. tp.module now keeps one module per cuda module and returns it on every later call.
ajensenwaud added a commit to ajensenwaud/TensorFold that referenced this pull request Nov 1, 2026
…one GPU

weightShare read TF_TP_* with catch null, so a malformed world or rank was admitted as one GPU's share. It now
returns BadSetting as open does, and the admission reports it through the family's explain. weightShare may return an
error union (cuda_family_memory's share, the same change as MiMo-V2's in ashhart#728). Host test: unset, six malformed
settings, and a two-rank share.
@ajensenwaud

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Pushed, rebased onto current main (df5e4de6):

  • tp.module fix in cbee9643: zig/build/tp.zig keeps one tp module per cuda module and returns it on later calls (byte-identical to Muse Spark Open on CUDA: EXL3, DFlash2, two GPUs #1295's), so the engines no longer fail with file exists in modules 'tp' and 'tp0'.
  • Other conflicts were additive: the CUDA registry (minimax.native and mimo_v2 both kept), and the contract record and image lists.
  • Two lean fixes that the merge with main needed:
    • Main's contract_tests.zig is at 599 lines, so MiMo-V2's contract group joins contract_family_sets.zig, as MiniMax-M3, MOSS-Image and GB10 affine-4 did (folded into 705ff1ed).
    • core/lanes/engine.zig is 598 lines on main, and the geometric depth fields took it to 603, so the rule's initializer moves to depth.Rule.of(gpa, cfg) unchanged (folded into 41ee44cd). engine.zig is now 584.

Locally on x86_64 Linux: zig build test, test-kernel-contract, test-golden, test-server-security, test-pull-security, check-release-paths, compile-cuda-tests and tf-mimo pass. fmt is clean, and lean finds 424 problems, as main. In one full zig build test run, qwen3_5/cuda_disk_test ("kept states written to disk read back...") failed with a NotReady warning. It passed when rerun and 15/15 times on its own, and this branch doesn't touch it. The macOS host-test registration and the @splat fix are left for you to carry, as agreed.

@ajensenwaud

ajensenwaud commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

All ready for your review, @ashhart

@ashhart

ashhart commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thanks @ajensenwaud, and sorry for the wait. Everything we asked for is in, and revision 139d9a4e is confirmed. As agreed, we'll carry the Linux-only registration of the MiMo host tests and the @splat fix, resolve the new conflicts with main, and run the two-GB10 cells at TP=2: the contract, a GLM control, drafted against plain, four streams against one, resumed against fresh, and a check that one rank is refused by name. We'll record decode on this head as its own baseline.

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.

2 participants