Repository navigation
minimax_m3: serve MiniMax-M3.1-Flash (428B-A23B MoE, images) natively on Metal - #1235
BobClawblaw wants to merge 14 commits into
Conversation
5aff9a3 to
60305dd
Compare
|
Thanks @BobClawblaw, this is impressive work, and we'd like it in. Three things first:
With those, we'll verify on our M5 Ultra and review it for landing. - Synthema, Ash's AI Assistant. |
60305dd to
e6b3b3b
Compare
|
Thanks for the review. All three are in the pushed head, e6b3b3b (rebased onto main at 3d219d2):
The PR description has the bf16 numbers: KL against the NVFP4 release, decode and prompt speeds, the serving checks and a needle near the full window. |
|
One more thing you should weigh before landing, since it bears on the same principle as the cache: the checkpoints themselves are not exact. MiniMax trained M3.1-Flash quantization-aware for its release format, NVFP4 routed experts and MXFP8 elsewhere, so that format is the model's own precision. mix34 and mix256k are a lossy conversion of it: the experts are refit to 3-bit (g64) or 4-bit (g128) affine groups and the rest to 8-bit affine. Teacher-forced over 1,024 positions against the release's own weights, that costs a mean KL of 0.246 (81.1% top-1 agreement) on mix34 and 0.301 (78.3%) on mix256k. For scale, the int8 cache you asked us to remove moved KL by under 0.02 (0.004 on mix34, 0.017 on mix256k). Everything the engine does from the converted weights on is exact (bf16 cache, windows equal one-row steps bit for bit, drafted equal plain), but the weights are not the release's. We converted because the release does not fit: its language model is 231.3 GiB as published (216.4 GiB of NVFP4 experts, 14.9 GiB of the rest), about 248 GB: past the 226 GB load limit we serve under on a 256 GB M5 Ultra (a full window already peaks at 210 GiB wired, and much more stalls macOS), before any cache. Metal also has no NVFP4 kernels in the family today; it reads affine groups. So the question for you: does TensorFold take a family whose only one-Mac checkpoints are lossy conversions, with their cost stated as above? If not, we see two ways forward:
We're happy to do either; tell us which you'd rather have. |
|
A correction to my earlier reply: we do have a single-Mac GLM-5.3-Flash checkpoint ( |
|
GLM-5.3-Flash router A/B, on one 256 GB M5 Ultra (
With Step-5 above, both Metal families on the shared router are unchanged by it. |
… bf16 logits on request tf_route_logits gave each lane four 32-wide slices of k, which covers a hidden width of exactly 4096 (GLM-5.3-Flash and Step-5); at 2048 it read past each row and past the packed router. Each lane now takes hidden / 1024 slices, the same four at 4096, so those routers' arithmetic is unchanged, and `source` refuses a width that is not a multiple of 1024. `round_logits` rounds each logit to bf16 before the select, as a bf16 router matmul (MLX's) gives it.
…niMax XML as DSML)
MiniMaxAI/MiniMax-M3.1-Flash (428B-A23B) as a native Metal family, from checkpoints converted to MLX affine widths (experts 3- or 4-bit per layer, the rest 8-bit; the recipe's tools/convert.py): - Block-sparse attention with the lightning indexer: the index scores and the top-16 block selection a row and key head, decode attention over the kept blocks in 32-key partials, prompt chunks on the tensor units over each tile's kept blocks. 128 routed experts (4 picked, fp32 router with score bias) and a shared expert: decode windows run expert by expert, prompt chunks through the tensor-unit gathers. - Keys and values cached as int8 with a scale a position and head (57% of bf16's bytes) by default; MINIMAX_KV_CACHE=bf16 keeps bf16. Prompt chunks scale scores by the keys' scales and probabilities by the values' before the tensor-unit products, so windows and one-row steps stay bit for bit equal. - Drafts: incoai's DFlash2 (the default) and the release's DSpark head (MINIMAX_DRAFTER=dspark), on the lane core's drafted windows; drafted greedy replies equal undrafted ones. - Images (--vision): the CLIP tower on the shared Qwen-VL kernels (its axial t/h/w RoPE by moved head dims), the merging projector, the rows written over the image tokens; the release processor's 672 x 672 cap. - The prompt cache's snapshots, several streams in one decode window, and a load budget under MINIMAX_LOAD_LIMIT_GB (default 75% of RAM). Measured on one M5 Ultra (256 GB): against transformers' math on 4 layers KL <= 0.005; against the release's NVFP4 weights over a 1,024-token chat, KL 0.250 and top-1 80.0% (mixed 4/3-bit, int8 cache); a needle found in a 251,713-token prompt (the 3-bit mix with 4-bit layers 22, 24, 59 at a 262,144 window); the image tower against transformers' at a relative error of 0.030 to fp32 (torch's own bf16: 0.039).
… (one call block, xgrammar's minimax_m3_xml arguments)
…d the recipes index)
…96-row prompt chunks (same bits) A prompt tile now holds the 16 query heads of one key head on two rows instead of one query head on 32 rows. The heads share their rows' block selection, so each key and value block is read once for all of them, and a tile walks the union of two rows' blocks rather than 32's. Prompt chunks grow from 2,048 to 4,096 rows, so each expert's weights are read once for twice the rows. Last-row logits after an 8,192-token prompt are byte-identical to the previous kernel's with the int8 and the bf16 cache.
…are memory (gate|up and act with the experts', their outputs and the block scores with the sorted inputs), so 4,096-row chunks keep the 262,144-token window under 226 GB
…adgroup atomics), not one thread an expert scanning every pick
…d serves 8 experts; every logit's sum in the same order)
…ut share the attention output's and normed q's memory (each dead before the other is written), so mix34 keeps its 65,536-token window under 226 GB
…nd the 246,842-token needle
…he and MINIMAX_KV_CACHE removed) The int8 cache (a scale a position and head) changed the model's arithmetic, so it goes: keys, values and index keys are cached as bf16, as the checkpoint computes them. The prep and decode kernels lose their int8 variants and the prompt attention its KV8 path; the cache layout loses the scale planes. What stays is the old bf16 path unchanged: on mix256k the 8k prompt's last-row logits, every 16-row window's logits over a 2k prompt and 64 greedy one-row steps are byte-identical to the previous head with MINIMAX_KV_CACHE=bf16. A token takes 135 KiB of cache across the 60 layers (int8: 77 KiB), so under MINIMAX_LOAD_LIMIT_GB=226 on a 256 GB M5 Ultra the caches fit 150,400 tokens on mix256k and 37,376 on mix34 (--parallel 2 shares them).
…rom pinned sources, and their hashes MiniMaxAI/MiniMax-M3.1-Flash at 54e4e69d (mxfp8 projections, nvfp4 routed experts) and incoai's DFlash2 at ac07a7c1 to the minimax_m3 checkpoint: projections MLX affine 8-bit g64 from their exact bf16, experts a least-squares affine refit of the nvfp4 values. `--variant mix34|mix256k` sets each published variant's 4-bit (g128) layers; `--check` compares a folder with minimax_m3_checkpoints.json, the sizes and sha256s of every file at the published revisions (config.json by content and the shard index by its weight map: the published ones were reformatted and carry stale total_size fields). A full mix256k rebuild (mlx 0.32.4, numpy 2.5.4) checks 74/74. mix34 was not rebuilt; its download checks 74/74.
…edle) and how to rebuild its checkpoints The windows the exact cache fits under MINIMAX_LOAD_LIMIT_GB=226 (mix256k 147,456 of 150,400 tokens, mix34 32,768 of 37,376), KL against the NVFP4 release on this head (mix34 0.246, mix256k 0.301), DFlash2 decode, cold prompts and a 145,203-token needle on mix256k, all measured on the bf16 build; the recipe page's commands for minimax_m3_convert.py.
e6b3b3b to
c1ace32
Compare
ashhart
left a comment
There was a problem hiding this comment.
Thanks @BobClawblaw. We rebased this onto current main (only the registration lists conflicted) and all its kernels compile offline, with host suites passing. Before device runs:
- Other families' streams change: the new opener joins the shared table in
reply_text.zig, so the partial-opener holdback now holds back any streamed text that ends in], for every model, until the next chunk. On mainhideToolCalls("x = a[i]", false)returns the text unchanged; with this PR it returnsx = a[i. Final bytes are equal, but chunk boundaries change. Please apply the MiniMax opener only when the server's markers are MiniMax's. - The family's kernels need to be registered with the Metal kernel contract, with records.
- The first commit (the
moe_routechange) isn't used, since the family routes with its own kernel, so please drop it from this PR. MINIMAX_LOAD_LIMIT_GBset very large overflows@intFromFloat; cap it at the working set. An unknownMINIMAX_DRAFTERvalue (a typo) should be refused, not treated asdflash2.
#1281 (CUDA, minimax_m3_vl) is the same model under another family name with a different config reader. It would be good to agree with @styles01 on one family name and one config reader that accepts both the flattened and the nested text_config forms. #1281's Metal list entry should go, since Metal can't open that pack.
One question for us to settle: the checkpoints that fit one Mac are re-quantizations of the NVFP4/MXFP8 release (KL about 0.25-0.30 against it). The engine itself is exact; we'll decide how to document that.
Serves MiniMax's MiniMax-M3.1-Flash (
MiniMaxAI/MiniMax-M3.1-Flash, a 428B-A23B MoE with block-sparse attention and a vision tower) natively on Metal on one 256 GB M5 Ultra, from affine conversions of its NVFP4 release, by the lane host with tools, structured output, thinking on or off, images, the prompt cache, two streams and DFlash2 or DSpark drafts.The first commit is #1177's
moe_routechange (router logits for any hidden width a multiple of 1024), which this family needs too (MiniMax-M3's hidden width is 6144); if #1177 merges first, it drops out on a rebase.What changes
moe_route(core, first commit): as in flint: serve Recursive Flint (40B-A2.5B MoE) natively on Metal #1177.<mm:think>block, which the model opens itself or skips by writing</mm:think>alone; thinking off ends the prompt with that</mm:think>.tool_minimax.zigparses MiniMax's]<]minimax[>[<tool_call>/<invoke>markup into OpenAItool_calls(by way of DSML), and the template's tool-result turns round-trip.families/minimax_m3(third commit):moe_route's logits, sigmoid scores with the correction bias, top 4 of 128; experts grouped by pick.MINIMAX_DRAFTER, defaultdflash2).--vision): the CLIP tower on the core Qwen-VL kernels, 576 tokens an image at most (the processor's 672 x 672).MINIMAX_LOAD_LIMIT_GBbounds weights, scratch, the tower and every stream's caches; the default 75% of RAM holds the weights but no caches.tf-minimax-run(tokens, logits, forced steps, exactness) andtf-minimax-vision(the tower's output for an image).tool_choice: requiredor a named tool constrained in MiniMax's own markup (one call block, xgrammar'sminimax_m3_xmlarguments). Before it, the generic JSON form let the model repeat a named call eight times.docs/recipes/minimax-m3.1-flash.mdand a README row.MINIMAX_LOAD_LIMIT_GB=226.MINIMAX_KV_CACHEare removed. What stays is the old bf16 path unchanged: on mix256k the 8k prompt's last-row logits, every 16-row window's logits over a 2k prompt and 64 greedy one-row steps are byte-identical to the previous head withMINIMAX_KV_CACHE=bf16. A token takes 135 KiB across the 60 layers; underMINIMAX_LOAD_LIMIT_GB=226the caches fit 150,400 tokens beside mix256k and 37,376 beside mix34 (served at 147,456 and 32,768).tools/zig/minimax_m3_convert.py(thirteenth commit, from review): both published checkpoints from MiniMaxAI/MiniMax-M3.1-Flash at54e4e69dfc452b1e94a426c24a4fdeeaa8e3ad5eand incoai/MiniMax-M3.1-Flash-DFlash2 atac07a7c1ecaae26a7ace595756124dca974b432d(--variant mix34|mix256k);--checkcompares a folder withminimax_m3_checkpoints.json, every published file's size and sha256 (config.jsonby content and the shard index by its weight map: the published two were reformatted and carry staletotal_sizefields). A full mix256k rebuild (mlx 0.32.4, numpy 2.5.4, 69 minutes) checks 74/74.The checkpoints are a lossy conversion
MiniMax trained M3.1-Flash quantization-aware for its release format (NVFP4 routed experts, MXFP8 elsewhere), so that format is the model's own precision. mix34 and mix256k convert it: the experts refit to 3-bit (g64) or 4-bit (g128) affine groups, the rest to 8-bit affine. Against the release's own weights (1,024 positions teacher-forced) that costs a mean KL of 0.246 (81.1% top-1) on mix34 and 0.301 (78.3%) on mix256k; for scale, the int8 cache this PR removed moved KL by under 0.02. Everything the engine does from the converted weights on is exact. The release does not fit one 256 GB Mac: its language model is 231.3 GiB as published (about 248 GB), past the 226 GB load limit before any cache, and the family has no NVFP4 kernels. Whether TensorFold takes the family on these terms, or wants native NVFP4/MXFP8 support or the conversions labelled outside the exactness guarantee, is asked in the comments.
Checks (Apple M5 Ultra, 256 GB, macOS 27)
Checkpoints:
BobClawblaw/MiniMax-M3.1-Flash-MLX-mix34at 136e775c89e996cac20c05befbcb26886ad24782 and-mix256kat c7defa69b0e8b57426a9945eb3f9eb91cfef9f8c. The bf16 numbers below were measured on the bf16-only code one rebase onto main earlier; on this head (main at faccd11) the 8k prompt's logits and 64 greedy steps are byte-identical to that build, the repository checks pass (lean: 435 problems, 435 on the base, 0 new) and the harness'squickon mix256k passed again (structured 18/18, tools 5/5, seeded sampling 9/9, resumed turns equal cold, soak 60/60;urlsfails without--vision-urls, as on every head). The math and image checks ran on the engine before the bf16-only commit (its bf16 path is the one kept, byte for byte); the quality suite ran again on this head for both checkpoints.The shared router (first commit). At GLM-5.3-Flash's and Step-5's hidden width (4096) each lane still takes 4 slices (
PERL = 4096 / 1024), the same loop and addresses as before;round_logitsis off unless a family asks. Step-5-Preview (MLX 2-bit) on main against main plus only this commit: identical greedy text on four prompts (256, 256, 24 and 256 tokens) and a 9.8k-token needle, decode 27.6-28.8 tokens a second on both, the long prompt's first token in 12.04-12.05 s on both. GLM-5.3-Flash (MLX 4-bit with MTP, on the same Mac), the same way, eight runs: identical greedy text on four prompts (251, 256, 52 and 256 tokens) and the needle; steady runs within 0.2 tokens a second of each other (A 76.1 / 77.7 / 73.8 / 77.6, B 76.3 / 77.8 / 73.7 / 77.7), the long prompt's first token 5.83-5.85 s on both (once 6.71 s, on A).Against references:
Exactness: windows of 16 rows equal one-row steps bit for bit. Drafted replies equal plain ones (DFlash2 and DSpark). Prompt chunks and 16-row windows agree with each other as closely as either does with the reference over 46 rows.
Serving (served
tensorfold-native, the recipe harness, bf16 cache):Decode (greedy, 256-token replies, DFlash2 drafts, bf16 cache; prose / code / structured / list, all streams together):
Prompt speed (cold, fresh server, thinking off; the recipe harness's prefill plans):
A turn resumed from the cache starts in 0.26-0.30 s against 3.6-5.6 s cold. The routed experts' gathers run at about 75 TFLOP/s; MLX's
gather_qmmon the same 3-bit shapes (balanced random routing) measures 82 on this Mac.Repository checks:
zig fmt --check,zig build test,test-kernel-contract,test-golden(0 unexpected differences),test-server-security,test-pull-security,fuzz-cortheon, the build-options and Flash Next serve unit tests pass on this head. Lean: 0 new problems.Not covered
--vision-urls) with this family.