Skip to content

New model family: MiniMax-M3.1-Flash VL (minimax_m3_vl) EXL3, CUDA, TP2-served - #1281

Open
styles01 wants to merge 7 commits into
ashhart:mainfrom
styles01:feature/minimax-m3-vl
Open

styles01 wants to merge 7 commits into
ashhart:mainfrom
styles01:feature/minimax-m3-vl

Conversation

@styles01

@styles01 styles01 commented Oct 25, 2026 •

Copy link
Copy Markdown

What this changes

Adds the minimax_m3_vl model family: MiniMax-M3.1-Flash (Mia-AiLab/MiniMax-M3.1-Flash-EXL3-EAQ-v1-3.06-bpw) running natively on CUDA, one change — the new family plus its minimal core glue. Fixes #1280 (the new-family gate issue; proposed gate and verification records summarized there).

  • New family dir zig/src/families/minimax_m3_vl/ — config, EXL3 weight load/unpack, sparse attention, layer graph, serve, CUDA-native adapter. Serves through the existing shared lane core (TP1) and over TP2 on 2 ranks (boot pattern mirrors glm's env-rank adapter in cuda_native.zig; no scheduler, no parallel lane machinery).
  • New CUDA kernel file zig/kernels/cuda/minimax_m3.cu (tf_m3_scores/topk/decode/decode_combine/swigluoai) and additive changes to exl3_experts.cu for the family's routed experts.
  • Minimal glue: build registration (cuda.zig, cuda_kernels.zig), kernel vtable wiring (src/cuda/kernels.zig, src/native/cuda.zig), Metal-side an earlier gate task admission line (metal.zig, no Metal weight reader), CLI hub/info entries.
  • Free-text requests refuse with ServeTextNeedsTokenizer until a tokenizer is provided; the ids path is bit-exact vs Hugging Face tokenizers 0.22.2 (19/19 encode parity).
  • Server attach (the server-attach task): the family attaches to the REAL server path through open() — env-rank boot (TF_MINIMAXM3_RANK/WORLD), the row-control buffer that re-keys mirror rows on cache-length changes outside a put, and the weightShare shim letting one open lane serve the rank-0 HTTP path over the split pack. :8000 answers /v1/models with MiniMax-3.1-Flash live at fixture scale; the full-depth real-pack TP2 server transcript is run outside CI and lands as a follow-up comment on the issue.
  • Temperature-agnostic serving: the family's keyed sampler is not attached, and a standard client's bare chat (which resolves against the checkpoint's generation-config defaults to a sampled spec) refused outright. Requests naming only temperature (any value) or no sampling field now serve fixed greedy; any other sampling knob (top_k/top_p/min_p/seed) or a grammar mask keeps refusing by exact name (SamplingNotAttached) rather than serve an ungraded draw. The reply documents itself: tensorfold.sampling: "greedy-deterministic (keyed sampler not attached; temperature is served as fixed greedy)". Implemented as an additive per-engine temperature_greedy info flag (default false, glm/flashnext behavior untouched) resolved in resolveSampling, honoring the existing greedy_only design line. Reasoning/thinking default left as-is: there is no family-level thinking knob in the glm CUDA adapter to mirror (thinking switches are server-side config/request fields), so the default reasoning behavior stands and is noted here.

verification

  • Environment: Zig 0.17.0, CUDA 13.0 nvcc, NVIDIA GB10 (DGX Spark) × 2 over NCCL/RoCE, checkpoint Mia-AiLab/MiniMax-M3.1-Flash-EXL3-EAQ-v1-3.06-bpw @ snapshot c33e7a66…; development trees at upstream main 7938815.
  • Exactness: device kernel parity vs the engine's CPU references at the fp16 store — 10/10 PASS (max|delta| 0e0 scores/marks/swigluoai, decode+combine 1.5e-5 = one fp16 ulp); real-pack f64 grade (first 2/60 layers, 35 rows): greedy tokens EXACT vs the independent f64 reference, logits max|delta| 0.003237 (quant tolerance), 0 NaN/inf; TP1 vs TP2 lane-surface tokens EXACT (88638 88638 52236 40265, and smoke ids bar reproduced); TP2 weight split tiling verified (75.79/79.68 GiB, plan sum 166,928,433,556 bytes).
  • Decode speed: n/a — new family bring-up; no previous release serves this checkpoint. Served decode on the real pack at fixture scale runs through the regular lane surface (drafts on).
  • Prompt speed: n/a — same reason; cold-prefill verification records to follow as a separate performance PR once the family lands.
  • Tests run (zig build test-cuda-host on the submit tree): exit 0, 177/177 steps, 158/159 passed, 1 pre-existing recorded skip. On GB10 CUDA: tf-cuda-test minimax-m3 parity 12/12 PASS; tf-cuda-test minimax-m3-serve fixture lcg ids bar EXACT vs the the token bars. New tests: device parity harness + graph tests + TP2 rank tests (zig/tests/cuda/minimax_m3*.zig, minimax_tp2_tests.zig), incl. env-place refusal tests for the TP2 boot (TF_MINIMAXM3_RANK/WORLD validation) and the tokenizer-free-text refusal.
  • Not run, and why: Metal device paths (family has no Metal weight reader; only the an earlier gate task admission line is present), the full 60-layer f64 grade (graded depth is 2/60; deeper layers are covered by the TP1/TP2 boot self-consistency verification records), HIP/XPU (no devices), full-depth real-pack TP2 HTTP server proof (in flight at submit time; verified at fixture scale on :8000 — /v1/models live; will land as a follow-up comment).

Checklist

  • Every token goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false on the graded depths (token-for-token vs the f64 reference); resumed/fresh and concurrent/solo equalities verified on the lane surface.
  • No precision traded for speed; the checkpoint's EXL3 dequant widths, fp16 stores and fp32/f64 accumulation contract are unchanged.
  • The new GPU kernels are registered with the CUDA contract path; the CUDA SUMMARY lines: parity 10/10 PASS (see verification records) — Metal has no kernels and no summary.
  • Prompt processing: n/a for bring-up (no baseline exists for this checkpoint).
  • Platforms run: CUDA (GB10). Not run: Metal M1–M5, HIP/XPU — stated above.
  • New tests fail before the change (the family didn't exist) and skip cleanly without their dependency (cuda_native admission requires a CUDA module; refusal tests run device-free).
  • Comments/docstrings single line, no measurements or history in source; debug probe machinery stripped; no personal data, machine names, internal hosts or local paths.

Overlap note (ATC)

atc overlap --json at submit time: the working set overlaps open PRs sharing common build glue (zig/build/cuda.zig, zig/src/native/metal.zig + kernel registry): #1235 (Metal minimax_m3 — different family, same checkpoint), #1009, #1268, #1271, #1272, #1241, #1254, #1240. All conflicts are at additive family/build registration points; happy to rebase when any of those land.

MiniMax-M3.1-Flash (EXL3 EAQ pack) as a native CUDA family serving through
the shared lane core: config, EXL3 weight load, sparse attention, layer
graph, serve, and the CUDA-native adapter with the TP2 rank boot mirroring
glm's env-rank pattern. tf_m3_scores/topk/decode/decode_combine/swigluoai
kernels plus the routed-experts extension of exl3_experts.

Gate issue ashhart#1280. Device parity 10/10; real-pack f64 grade: greedy tokens
exact vs f64, logits max|delta| 0.003237.
Build registration for the minimax_m3 kernel image, the tf_m3 vtable
wiring, the CUDA/Metal family admission lines (Metal: stage-1 admission,
no Metal weight reader), and the CLI hub/info entries with the EXL3
format reader.
Device parity harness vs the CPU references (scores/topk/decode+combine/
swigluoai, NB/nsplit bound guards, zero-valid-key), the layer-graph token
harness, TP2 rank/env-place refusal tests, and the exl3_experts f64
additions.
The keyed sampler is not attached for minimax_m3_vl, so a standard
client's bare chat resolved against the checkpoint's generation-config
defaults to a sampled spec and refused everywhere. An added per-engine
temperature_greedy info flag (default false; resolved in the server's
resolveSampling, mirroring the existing greedy_only design line) serves
requests naming only temperature (any value, or absent) as fixed greedy
and annotates the reply (tensorfold.sampling), while top_k/top_p/min_p/
seed and grammar masks keep refusing by their exact lane-surface name
(SamplingNotAttached). The family's own lane surface is unchanged.
Reasoning/thinking default stays; no family-level thinking knob exists
in the glm adapter to mirror.
…ayer

The earlier temperature-greedy wiring resolved the flag comptime through
openWith(F) with F = the family's registry alias (minimax_m3_vl.native),
and that alias re-exported model_type/formats/Options/open/explain/
weightShare but not temperature_greedy — @hasDecl fell to false, the
engine Info served false, and POST /v1/chat/completions resolved bare and
temperature-only bodies against the checkpoint's generation-config
defaults (temperature 1.0, top_p 0.95), built a full Sampling spec and hit
the lane's SamplingNotAttached. Only an explicit temperature 0 served (the
old value-based path). The alias now carries the flag, so the server's
resolveSampling key-presence flip actually runs on the live serve path:
temperature at any value, or absent, serves fixed greedy and annotates
the reply; top_k/top_p/min_p/seed and grammar masks keep refusing by
their exact lane-surface name. Tests extend done_times with the real HTTP
parser path (openai.chatInput -> chat.prepare) and guard the alias
re-export in the family's own admission test.
…nking-on by name

Stage-11 live-lane probes (spark2 :8000, MiniMax-3.1-Flash, PR ashhart#1281 head
f623f9e) split the empty chat reply into two findings:

1. The thinking switch was inert on this family's rendered prompt.
hf_text put the generic thinking_mode spellings ("thinking"/"chat") into
every template, but MiniMax M3.1's chat_template.jinja branches on
"enabled"/"disabled"/"adaptive"; both spellings fell through to its
adaptive branch, so chat_template_kwargs enable_thinking was accepted
(served, echoed) yet changed nothing in the prompt ids: thinking on and
off rendered the identical 156 rows. thinkingModeValue now reads the
template's own dialect from its source, so thinking-off renders disabled
(the </mm:think> assistant prefix) and templates without the dialect
keep the generic spellings.

2. The request's own enable_thinking body field was ignored: the server
read the switch only through chat_template_kwargs. thinkingFields now
honors the body's field when the kwargs switch is absent (kwargs wins),
so the field is honored wherever it names the switch.

3. The stage knob: minimax_m3_vl declares thinking_off (mirroring
temperature_greedy's registry-alias pattern: family const + openWith(F)'s
Info read + a family guard test). chat.prepare runs thinking-off engines
without the reasoning header whatever the server config says, and an
explicit thinking request (chat_template_kwargs or the body's field)
refuses by exact name (ThinkingNotAttached) — the native lane has no
thinking reserve attached, so thinking cannot be served honestly.

Host suites on the canonical tree (zig 0.17.0): test-server-cpu 618/618,
test-done-times-host 20/20 (new HTTP parser-level test: bare bodies
prepare thinking=false; kwargs and body-level enable_thinking true refuse
naming ThinkingNotAttached; body-level false prepares thinking=false on
a thinking-capable engine), test-cuda-host 177/177 steps 451/452 tests
passed (1 pre-existing recorded skip); lean_check vs merge-base 7938815
introduces no new problems; git diff --check clean.

Lane-level boundary (measured on the live pair, receipts §4b): the greedy
first draw at the assistant turn is the EOS token itself under every
honest render, so chat content staying empty is a decode-fidelity defect
of the native EXL3 row path on the real pack, upstream of the template —
receipts report it verbatim; this commit carries the honest knob.
@ashhart

ashhart commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Thanks @styles01. This is a large and careful bring-up: kernel parity 10/10, tokenizer parity 19/19, and TP=2 boot over the lane core. New families are welcome when they're off by default and leave the rest of the engine byte-identical. Before we can land it as a served family:

  1. One family per model. minimax_m3: serve MiniMax-M3.1-Flash (428B-A23B MoE, images) natively on Metal #1235 brings the same MiniMax-M3.1-Flash checkpoint to Metal as minimax_m3. Please coordinate with @BobClawblaw so the model has one family with a CUDA and a Metal backend, sharing config, tokenizer and chat handling, rather than two names.
  2. Serve real requests. Free-text chat has to work with the checkpoint's own tokenizer (no ServeTextNeedsTokenizer), and sampled requests have to work through the keyed sampler, not just greedy.
  3. Exactness on the served path at TP=1 and TP=2. We need tools/bench_concurrent.py --alone --serial (concurrent == solo), drafted == "draft": false, and resumed == fresh, through the real server.
  4. Numbers. Cold prefill and 1/2/4/8-stream decode on the GB10s, so we have a baseline to protect.

Once those are in, we'll run it on our GB10 pair.

- Synthema, Ash's AI Assistant.

…act (act_mode 2)

The MiniMax M3.1 routed experts' gate/up epilogue ran the generic act_mode-1 silu path;
this family's contract is the OAI swiglu (alpha 1.702 sigmoid form; fp16 gate/up rows and
an fp16 activation row before the down-side rotation — tf_m3_swigluoai's shape). The decode
epilogue (tf_exl3_gateup_epilogue), the prompt-side tgateup and that path's eligible() gate
now carry act_mode 2, and the family's experts upload names it.

Layer-3 boundary rows against the f64 mirror: relmax 9.7e-2 -> 4.3e-4 (~200x); layers 0-2
bit-identical pre/post (footprint exact). Full-depth serving on the rebuilt binary now
completes 'The capital of France is' (temperature 0, greedy) with real content where the
pre-fix lane drew a forced first-draw EOS with empty content.
@styles01
styles01 marked this pull request as ready for review October 25, 2026 23:39
@styles01

Copy link
Copy Markdown
Author

Root cause found and fixed in a493d13: the family's routed-experts upload ran the generic exl3 gate/up epilogue at act_mode 1 (plain SiLU) where this model's contract is the OAI swiglu (alpha 1.702 sigmoid form). The decode epilogue, the prompt-side gateup path, and its eligibility check now carry act_mode 2. Layer-3 boundary rows now match an f64 mirror to ~4e-4 relative (from ~1e-1), with layers 0-2 bit-identical before/after. Greedy first draws at full depth read real content ('The capital of France is' -> ' Paris') instead of the forced-EOS draw, and the host test suites pass.

@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 @styles01, a lot of work here. We rebased it onto current main (build-glue and fields.zig conflicts, both sides kept), and Linux CUDA compiles on x86_64 and aarch64. Not ready for a device run yet:

Shared code and exactness

  1. GLM's EXL3 kernels gain new runtime branches: f32 input in tf_exl3_experts_rot_in, act_mode 2 in tf_exl3_gateup_epilogue and tgateup. Please move them into family-owned entry points (or template instantiations), so the existing kernels' SASS stays byte-identical.
  2. __expf (approximate) is used in exl3_experts.cu, exl3_prompt.cuh and minimax_m3.cu. Please use expf.
  3. exl3_experts_f64_tests.zig moved the f64 checks from GLM's act_mode 0 to mode 1, leaving mode 0 checked on one pattern only, and there's no f64 check for mode 2. Please restore mode 0 on all IDs and add mode 2. tf_exl3_plan_finish would also treat mode 2 as plain SiLU, so exl3_grouped should refuse it.
  4. The tf_m3_* kernels need to be registered with the CUDA kernel contract (a standalone parity runner doesn't replace the SUMMARY).
  5. The parity, f64 and TP results predate e1076a50, which changed the experts' arithmetic (9.7e-2 to 4.3e-4 relative error), so they need rerunning.

Serving
6. temperature_greedy serves a temperature-0.8 request greedy. Please drop it, and refuse sampled requests (greedy_only) until the keyed sampler is attached.
7. The decode is still a host smoke graph (per-layer syncs, norm and RoPE on the CPU, one stream, one-token prompt feed, drafts refused), and graph.zig caps keys at 4096 while the default context is 1,048,576, so a plain open fails. We land families once they serve through the lane core.
8. setenv on NCCL variables changes the whole process. Please pass them through the TP setup instead.

Out of scope here (they change every family): hf_text.zig's thinking_mode detection by quoted words, and honouring a body-level enable_thinking for all families in fields.zig. The Metal list entry for minimax_m3_vl should go too, since Metal can't open it.

Rules: lean reports 460 new findings, mostly multi-line comments; five new files exceed 600 lines (graph.zig 1061, serve.zig 1106, cuda_native.zig 793 and two test files), and build/cuda.zig and server/chat.zig go over 600 too. Commit fbf132f0's message names a machine; please reword it. The hub.zig test should use std.testing.tmpDir instead of fixed .zig-cache/tmp paths. A -> in a parameter comment in minimax_m3.cu crashes our launch-argument scanner, and zig fmt reports 10 files.

With #1235 (Metal): one family directory and name (minimax_m3), one strict config reader that accepts both the published text_config form and the converted root form (with rope_theta read, not hard-coded), quantization metadata read once through the core readers, #1235's markers/tool parser/thinking-off path, and one host reference each for the MSA selection and swigluoai that both backends test against. Please also give the checkpoint's full revision.

@BobClawblaw

Copy link
Copy Markdown
Contributor

Hi @styles01, following Synthema's ask above about one family. We're bringing MiniMax-M3.1-Flash to CUDA as the
minimax_m3 family's second backend (the family from #1235: its config, tokenizer, chat template and tool-call handling
on both), on two GB10s at TP=2 over the lane core, reading the release's own formats (MXFP8 projections, NVFP4 experts)
plus an optional 3-bit refit of some layers' experts so two 121 GB ranks hold a usable window. It serves now: the
branch is minimax-m3-cuda on BobClawblaw/TensorFold (#1388), with the recipe at
BobClawblaw/minimax-m31-flash-nvfp4-2x-dgx-sparks.

One thought from reading your branch: once the family exists with the shared config and loaders, your EXL3 experts
could come in as one more expert format beside NVFP4 and the 3-bit affine one (the engine already dispatches per layer
through Grouped), if you'd like to carry that over.

Happy to coordinate here or on #1280.

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.

New model family: MiniMax-M3.1-Flash VL (minimax_m3_vl) EXL3, TP2-served

3 participants