Repository navigation
qwen27: rank-slice the 27B family and gather the whole embedding table - #1433
Conversation
|
The Two places change the arithmetic, so the pair can't match the single Mac yet:
Stage 1 compares |
5fcfe81 to
7d57101
Compare
|
Refreshed head
|
|
Thanks @akol1. Carrying the checkpoint matrix's width (
This also needs #1432's rebase. |
0984521 to
b3d9b7e
Compare
|
Refreshed and force-pushed:
Gates on |
|
Thanks @akol1. All four are in on
Keeping |
36e99b1 to
bb00f2b
Compare
|
Thanks — all four hold, and this is the commit where the two kernels you flagged stop being shared. On the rebased head
Fresh receipts on On the lane kernels: you are right that no M4 can show them, so what I can put behind them is the simd contract above (all four gates green on this Mac) and the one-Mac cold-prefill A/B at the series tip — the same before/after shape as your prefill cells, one GPU class down. Your M5 cells remain the receipt for the lane path itself. |
bb00f2b to
55865b0
Compare
|
Rebased onto the current
This room is one Mac now: the second Mac the pair receipts were recorded on is gone (the cluster was broken), so a fresh two-Mac |
|
Hi @akol1, a status update on #1433. We read step 2's diff and found nothing that changes one Mac's output on the default path. The single-Mac merge #1432 landed tonight, slightly changed from your base (simd_qmm is now split over K). As a result, this branch
Could you rebase onto current main? Once it's rebased, we'll run the M5 cells (chat, the multi-turn decodeChunk |
55865b0 to
d844977
Compare
|
Rebased onto The three conflicts and how they resolved:
Numbers from this head, run here, not carried over: The M5 cells (chat, the multi-turn |
d844977 to
0a44b06
Compare
|
Rebased: The step-2 patch is unchanged: against the head you reviewed, every difference is Numbers from this head, run here, not carried over: This room has M4 Pro hardware only, so the M5 and M3 Ultra cells remain yours; the one-Mac default path is still the diff you already read. |
579f9b4 to
663bd7a
Compare
|
Rebased: The step-2 patch is unchanged: Numbers from this head, run here, not carried over: This room has M4 Pro hardware only, so the M5 and M3 Ultra cells remain yours; the one-Mac default path is still the diff you already read. |
|
@akol1, a heads-up. #1726 landed just before this: on M5 the 27B now serves one stream through the lane loop by default, and the forward gained a We resolved it this way:
We're re-running the 27B with DFlash2 on an M3 Ultra and an M5, main against this, before it lands. Could you check one thing on your pair, since we can't: whether a round with a tail behaves on both ranks (drafted equal to |
dc22485 to
1af209f
Compare
|
Rebased onto Short answer: on one Mac the tail is exactly where #1726 put it, and on a rank the pair refuses the combination rather than guessing, so the two never meet. Matching the rule you gave: (1) a one-Mac tail runs exactly where #1726 put it — Run here, on this head, not carried over: The loopback gate that lands in step 4 has since run on this hop's descendant: the two-rank JSON reports This room has M4 Pro hardware only ( The PR body is refreshed with these same receipts. |
|
Thanks @akol1 for the rebase and the The one-Mac check on an M5 Ultra, Qwen3.8-27B MLX 4-bit with DFlash2, main against this, eight alternating fresh servers:
A four-run pass earlier read 2.6% low at 2k. Its first main run was the outlier: main itself drifted from 159.9 to 153.8 over the run. The eight-run pass settles it. The M3 Ultra run is waiting on that machine's Xcode tooling, which an overnight update broke. It lands once that's back. |
Step 2 of 6, stacked on step 1 (`qwen27/pair-seam`) and main (`40cb15b4`); step 3 stacks on this commit. A rank's layer program now carries one `Reduce` interface instead of assuming the whole matrix. Attention's o_proj and the MLP's down_proj seam this rank's output columns, gather the whole frame, and let the producer kernel reduce over full K on its own rank; the head seams the last row's logits and takes the pair's argmax over the whole vocab, so the picked token is the single Mac's token, not rank 0's half-vocab pick. On one Mac `reduce` is null and every seam is compiled out of the program: the single-Mac program, its digests and its tests are the ones that ran before. The rank-local K-split is fixed while the slice is. A projection's split is keyed on the checkpoint tensor's rows, not the view's rows: `Triple` carries `whole` (the matrix's row count) and `prepare` derives `tensorRows` from it, while the M5 and prompt tables keep the checkpoint rows they always keyed. `in_proj_qkv` loads as three blocks of one tensor (10,240 rows), so its gate/up-style stack keeps main's 4 K slices on 6,144 rows instead of reading the q block's 2,048 rows and splitting into 8; every width main served on one Mac keeps main's split, and host tests "a rank's row slice keeps the checkpoint matrix's K split" and "every one-Mac stack keeps main's K split" cover the eight stacks (qkv 10,240 rows: 4, q 6,144: 4, against the 2,048-row view's 8). `Part` drops its unused `k0`, `Triple` its `k`/`k0`, and `Shard`/the family's private `cols`/`attentionCols`/`gdnCols`/`downCols` and the column-window code `prepare`/`packTiles` used them for are gone, with `config_test.zig`'s column-span assertions deleted and the row fields left as they were. `simd_qmm.call` takes the rank's `whole` width and `chunks(n, whole)` picks the chunk fence from it, so a rank's K tree is its own full matrix's while a one-Mac stack passes 0 and keeps exactly main's fence (the core test pins `chunks` on 6,144/12,288 and 5,120/10,240 at 8, and on 6,144 and 5,120 alone at 16, which is the pair's own-width split the `gn` forwarding is there to avoid). `quant_gpu.strided` forwards the `gn` of the linear it serves. `q27_attn_merge` takes the rank's head count (buffer 11) and strides a row by that many heads instead of 24 (`G * 4`), so the non-prompt multi-row rounds a served chat feeds every earlier reply through merge the rank's own rows; `attention.zig` binds `g.heads`. The buffered form is only what a pair runs. One Mac has its 24 heads at compile time, and main's kernel used them there (`G * 4`, and `PG * 4` in the prompt merge), so each site now has a second instance: `q27_attn_merge_single` and `q27_attn_prompt_merge_single` carry main's body verbatim — same reduction, same float arithmetic, main's indexing, no `heads` or `kv_heads` buffer — and the encoder takes a trailing `split` and picks `io[4]`/`prompt[1]` for a pair and `io[10]`/`prompt[2]` for one Mac (`decode_round.zig` passes `r.reduce != null`). The two instances compute the same values, so only the emitted code differs. A function constant would need `MTLFunctionConstantValues`, which the runtime library never creates (`PipelineBuild` would fail the contract's first gate), so the second compiled instance is the route that needs no new Metal machinery. The config gains its shard parse (`config_test.zig` covers it), the embedding table stays whole on every rank — only the head is sliced — with `embedBound` reading the table's bound rather than the family's `vocab_size`, and the session keeps the pair's pick as the round's token and surfaces `error.PairFailed` from the head seam's sentinel rather than returning `maxInt(u32)`. The seam's `Reduce` is threaded through `forward.encodeTp`/`session`, and the prompt attention kernel's signature follows the seam's reopened encoder.
1af209f to
1818227
Compare
|
Thank you @akol1. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in 1.2.1, credited to you in the changelog and the release notes. |
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
|
Thanks @akol1! The second step of Qwen3.8-27B on two Macs, loading and running one rank's half, is in 1.2.1: https://github.com/ashhart/TensorFold/releases/tag/v1.2.1 |
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so ashhart#1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
…and its DFlash2 drafter Ornith-1.5-122B is a Qwen3.5-MoE: the 27B's GatedDeltaNet and gated attention layers (48, every fourth attention, 32 query heads over 2) with a routed MLP of 256 experts of 1,024, eight picked a token, and a shared expert of 1,024 gated by a sigmoid. The 27B engine serves it as model type qwen3_5_moe. - config.zig reads the routed shape (num_experts, num_experts_per_tok, moe_intermediate_size); an absent norm_topk_prob means renormalized top-k weights, as the reference config and the CUDA family read it. - widths.zig resolves the router, the shared expert's gate and the stacked experts per layer; the router's rows and the shared gate's row are one matrix padded to whole 64-row tiles, so admission refuses a shared gate quantized unlike its router. - moe_gpu.zig and moe.metal: the router's bf16 logits, q27_route's fp32 softmax and top-k by logit (the lower id on ties) with renormalized bf16 weights, then the experts. Prompt chunks on tensor units sort the picks by expert and run core.affine_mm's gathers; decode rows run moe_rows.metal's q27_rows_extra, core/row_projection.metal's row loop over each slot's expert with the shared expert as one more matrix; q27_expert_combine adds the weighted experts and the gated shared expert at the reference's bf16 points. - Every prompt chunk takes the tensor-unit path whatever its rows, so a row routes alike wherever its chunk starts: a resumed prompt equals a fresh one. Decode windows keep the row kernels, as decoded spans replay them. - Routed prompt chunks are 1,024 rows (a chunk reads every expert once). - DFlash2 drafters of up to eight taps and a block of 16 (Ornith's drafter); the tap planes and the drafter's ring are sized from the drafter's own tap count, so the 27B keeps its five. - Routed models let plain rounds compete with drafted depths: a drafted window reads most experts' weights. - Admission takes the routed experts only at the widths the kernel contract checks them at (widths.zig's routed_bits 4 and 8, routed_group 64): any other bits or group, such as a group of 32, is refused with UncheckedRoutedWidth before a buffer is sized, so the prompt path's per-group sums always fit. moe_rows.metal instantiates q27_rows_extra at those two widths only. A shared expert quantized unlike the routed experts is refused at admission (SharedExpertWidth, naming the layer and module), not at the first decode. - --speed-up and --learn are refused for qwen3_5_moe by name (Qwen3.5-MoE), and the --learn refusal names --learn-conversations for the dense 27B only. - The Neural Engine prompt share is qualified on the dense 27B's MLP alone: a routed checkpoint keeps its prompts on the GPU and says so. - native/metal.zig routes qwen3_5_moe to the 27B engine (qwen27_engine.zig). - Routed checkpoints stay on one Mac: config.split refuses them (BadShard), so #1433's rank slice runs the dense 27B alone, and Geometry admits 32 over 2 only whole, not as a rank's 16 over 1.
What this changes
Step 2 of 6 of the two-Mac Qwen3.8-27B series, rebased onto
main(94b90c9db). Step 1 was merged as #1432 and its content is already onmain(the squash merge3f0a715cis an ancestor of the base), so this branch carries no step-1 commit; step 3 stacks on this commit.The branch is a single commit.
mainalready carries the cost-rule retarget this branch used to bring itself:main's5c9706f3moved the 27B window timing intozig/src/native/qwen27_costs.zigand pointed the case test at the file that owns the timing, and that retarget is an ancestor of94b90c9db, so the leg is green here and the rank slice is the whole branch.The 27B family becomes rank-aware behind one interface:
forward.Reduce(zig/src/families/qwen27/forward.zig:23) carries a seam'spart,stage,gatherandfailed, plus an optionalargmaxthat defaults tonull; a one-Mac model therefore compiles the head seam out and executes exactly the layer program it executed before this series.projection.preparederives a rank's row slice from the checkpoint matrix (gn), so the slice keeps the checkpoint's K split, tile and prompt tables (zig/src/families/qwen27/projection.zig:61); prompt rows takepromptSplitof the whole matrix, so the pair's prompt reduction is the reduction tree one Mac uses rather than a half-width tree. The width the seam carries is the tensor's own: the fused gate-up keeps the member split instead of recomputing it from the stacked width, a local qkv block keys neitherm5SplitnorpromptSplit, andsimd_qmm_bits.call(..., whole)keeps the split rank's chunk count throughtwinExactas well.The split is by output rows with the full K on the rank that owns them: 12 of 24 attention heads, 2 of 4 KV heads, 8 of 16 linear-attention q/k heads, 24 of 48 value channels and 3,072 of 6,144 z rows, 8,704 of the MLP's 17,408 rows, 2,560 of the 5,120 output rows for
o_proj/down_projat full K, and 124,160 of the 248,320-row lm_head.o_projanddown_projcompute a rank's own output rows at full K, and the other rank's rows come over the seam as whole bf16 rows:stagecopies this rank's columns of a frame row andgathercopies each rank's half to its own offset, so no two partial sums meet and no row is computed twice. Embeddings and norms stay whole on every rank, so a token id means the same thing on both, and the embedding gather's bound stays the whole 248,320-row table (config.embedBound). The head seam isargmaxover each rank's 124,160-logit half andpickover the pair: no rank computes or gathers the whole 248,320-row logit vector, the pick is the better of the two half-vocab winners, and a non-finite logit on either half fails closed through the sentinel instead of letting the other half's winner stand in for the whole vocabulary's.session.zigkeeps the merged top two for its cross-block tie test and reads the pick'smaxInt(u32)sentinel aserror.PairFailed, never as a logit.Two host-path details the maintainer asked for: the prompt attention merge strides the rank's own head count —
q27_attn_mergetakesconstant uint& heads(zig/kernels/metal/qwen27/attention_mpp.metal:270) and indexes((int64_t)node * (int64_t)heads + h) * D, withattention.zig:408setting it from the rank's geometry; the prompt merge writes the rank's KV node rows (kv_headsinQ27PromptArgs,attention_prompt.metal:170) instead of assuming the model's four. Thein_proj_qkv2,048-row trap is keyed on the tensor's rows, not on a split table, so a whole 6,144-row qkv block stays whole;projection.zig's tests pin it: "every one-Mac stack keeps main's K split", "a rank's row slice keeps the checkpoint matrix's K split", and "stacked gate-up keeps the member K split rather than recomputing it from stacked width".config_testpins the rank spans against the checkpoint's real shapes — 12 heads, 2 KV heads, 8,704 intermediate rows, 124,160 vocab rows, 2,560 output rows, q rows from 6,144 — while rank 0's spans start at 0, andsynthetic_graphruns the same graph on both ranks. The qkv blocks start at 1,024 rather than 2,048, which is the trap the maintainer's comment names: a block boundary keyed on the stacked tensor's rows instead of the tensor's own.One Mac keeps
main's program, which is the review's point. The split merge takes the rank's head count through a buffer uniform —q27_attn_mergereadsconstant uint& heads(attention_mpp.metal:267, the argument at:270) — and a uniform argument is a host-side dispatch difference, so one shader cannot be both.attention.zigtherefore registers a second instance beside the split one:q27_attn_merge_single(attention_mpp.metal:330) isq27_attn_mergeminus theheadsargument, andq27_attn_prompt_merge_single(attention_prompt.metal:175) writesPG * 4where the split prompt merge readsPG * args.kv_heads(:170); the one-Mac prompt merge is the kernel whose comment says a single Mac's 24 query heads are known at compile time.encodechoosesio[if (split) 4 else 10]andprompt.?[if (split) 1 else 2], andsplitisr.reduce != null— the same conditiondecode_round.zig:474andforward.zig:127already test — so an unpaired pass never binds the split instance and never sets theheadsuniform (:408runs only whensplit). Upstream creates noMTLFunctionConstantValuesanywhere underzig/srcorzig/kernels, so a function constant is not a mechanism this codebase has; a separate pipeline instance is.The two instances are
main's kernels, not lookalikes: compiled with the flagszig buildgives these sources (metal -x metal -std=metal4.1 -fmetal-math-mode=safe -fmetal-math-fp32-functions=fast -c) and read withmetal-objdump,q27_attn_mergefrom the parent tree andq27_attn_merge_singlenormalize to 329 IR lines each with 0 differing lines;q27_attn_prompt_mergeandq27_attn_prompt_merge_singlenormalize to 125 lines each with exactly one line differing, theQ27PromptArgspointer'sdereferenceable(28)againstdereferenceable(32). That difference is the four bytes of theint kv_headsmember the split kernel reads; the single kernel never loads it, so its body ismain's text withPG * 4in the store. One Mac's prompt path keeps its arithmetic and only loses a uniform load.A rank's verify never runs a tail.
forward.zig'sfinishruns the head seam first and hands the reopened encoder back to the tail (zig/src/families/qwen27/forward.zig:230,:241); when the seam ishead == .noneit is skipped and the tail still runs, which is the one-Mac path. Because the head writes only this rank's vocabulary columns while the tail's matcher reads the whole row,decode_round.zig:221refuses a round that has both witherror.TailOnRankbefore the cache checks or any GPU work;zig/qwen27_bank_boundary_host.zig:53pins that refusal with model-free stubs, and from step 3 on a pair refuses--drafterat the front door, so the two never meet.Receipt
1af209fce92b1bcf4ccd848f99e97a301bb4306f) onmain(94b90c9db65dcb71bbc39715901d6fb1f00407d0); no MLX or PyTorch; macOS 27.0.1 on an Apple M4 Pro (Mac16,11), Metal only (applegpu_g16, no tensor units), Apple metal version 32023.921. No checkpoint in this commit's gates:config_testandsynthetic_graphbuild the graph from a synthetic config, and the model gates arrive in step 4.tools/bench_concurrent.py --alone --serialortoken_shaagainst"draft": false: n/a at this commit, nothing serves yet; the split is exact by construction and pinned by the host tests above — a rank's slice keeps the checkpoint matrix's K split, the fused gate-up keeps its member split, and every one-Mac stack keepsmain's split (0 new problems in the lean run is the same diff's shape check, not exactness). The filtered contract runs below cover the kernels the split talks to; the limit checks pass (0 limit problemsin each run'sSUMMARY).tools/bench_openai.py: n/a, no serving path at this commit; the pair's decode rate needs two Macs and arrives with step 4's receipts.zig build test:576/576 steps succeeded; 8723/8775 tests passed (52 skipped)(run here with a cold local cache, so the counts are the whole suite; every step succeeded), exit 0.zig build test-cost-rule:3/3 steps succeeded; 30/30 tests passed, exit 0 (run here with a cold local cache).zig build test-kernel-contract:20/20 steps succeeded; 141/141 tests passed, exit 0.zig build dev-tools:219/219 steps succeeded.zig build test-qwen27-bank-boundary-host:7/7 steps succeeded; 4/4 tests passed, exit 0 (this commit's new test runs there).zig build test-qwen27-served-hostandzig build tf-qwen27-tp-check:no such stepat this commit (steps 3 and 4 add them).zig fmt --check build.zig zig: clean. The filtered contract runs:SUMMARY 4 kernels: 4 pass gates 1-4, 0 fail, 0 known findings; 6 controls: 6 red as planted, 0 missed; 0 limit problems; gates 34 ms, limits 1085 msandSUMMARY 4 kernels: 4 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems; gates 30082 ms, limits 123 ms. Lean rule:lean: 424 problems, 424 on the base, 0 new(tools/zig/lean_new.py, exit 0).tf-qwen27-tp-check, which step 4 adds); a two-Mac run (the serving link is step 3 and the scripts step 5); the unfiltered kernel corpus (it stops atUNSUPPORTED metal moss image: the family runs on tensor units (M5) alone; its kernels unchecked here, so the runs above use--only); the series' two-Mac acceptance receipt, which was recorded before the series was rebased onto this base and cannot be re-run now that the second Mac is gone, and is quoted as dated in steps 4-5; CUDA, HIP/RDNA, M1-M3, M5 and Windows are not in this room.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. — no serving path at this commit; step 3 refuses drafts and step 4's fixtures are greedy, so there is nothing drafted to compare yet.Reduceisnull, so its arithmetic ismain's.SUMMARYline is in the receipt. — the two qwen27 attention kernels this commit edits have no contract record onmaineither; the edit is their head-stride argument, and it is pinned by the one-Mac split tests and by step 4's stage-3 fixtures rather than by a registration this series does not add. The pair's four records are already onmain(they arrived with core: add the row-split pair seam and split simd_qmm over K #1432's squash,3f0a715c).applegpu_g16); no CUDA, HIP, M1-M3, M5 or Windows.config_test's shard spans andprojection.zig's split tests fail on the parent commit (noShard, nowholeargument), and the model-requiring paths skip without a checkpoint as before.lean: 424 problems, 424 on the base, 0 new.