Conversation
A fused QKV weight holds Q, K and V in one matrix. deepspeedai#8384 left it on the full-matrix path because the three sections do not share a head count under GQA, and they do not have to: every head is `head_dim` contiguous rows of dim 0 in both layouts that ship - sectioned (`cat([q, k, v])`, what most training stacks store) and interleaved per KV group (Falcon's `view(..., num_kv_heads, num_heads // num_kv_heads + 2, head_dim)`, GPT-NeoX's per-head q, k, v). The two layouts present the same row partition in a different order, and per-head Newton-Schulz is block-wise with the same `max(1, head_dim / in_features)**0.5` scale on every block, so the update does not depend on which layout the weight uses and the Q/K/V labelling never enters it. GQA changes how many heads a section holds, not where the blocks are. So a fused weight needs no section bookkeeping: it is tagged with `rows // head_dim` head blocks, which for 8 query heads over 2 KV heads is 12 rather than 8 or `3 * 8`. The candidate requires the exact fused total `(num_attention_heads + 2 * num_kv_heads) * head_dim`, which is also what declines a transposed `Conv1D` weight (GPT-2's `c_attn`, `[hidden, 3*hidden]`) whose dim 0 is the input axis. The tag rides on `muon_num_heads`, so the kernel, the ZeRO call sites and the config switch need no new API. Falcon's `query_key_value` stays on the full-matrix path and the tests pin why: its config reports `num_attention_heads` KV heads while `multi_query=True` makes the module build one, so the config's fused total (192 rows) does not describe the weight (80). That is a pre-existing `AutoTPMeta` reading rather than something the per-head split introduces; the exact-total check turns it into the full-matrix path instead of a split of the wrong blocks. Tests: 64 CPU cases - fused tagging under GQA, the exact-total guard, the Conv1D decline, sectioned/interleaved equivalence on a real gradient, the real architectures, and the existing suite - plus 26 accelerated cases on 2 x L20 across ZeRO 1/2/3, including a fused training model. End to end on 2 x L20, ZeRO-2, bf16: a 17.2M-parameter fused-QKV model trains for 120 steps on real text with the flag on and off, the fused projection carries 12 head blocks with the flag on and none with it off, the two settings move all 34 trainable weights differently, and on a real gradient the per-head update norms land within 1.30x of each other where the full-matrix ones span 2.78x. Signed-off-by: 0z5a <192209249+0z5a@users.noreply.github.com>
alanhuangyoo
left a comment
There was a problem hiding this comment.
Owner geometry is lost for a fused leaf once this rebases on #8436 (blocking)
_owner_candidate only answers for QUERY and KV, so a module in _LINEAR_ATTENTION_OWNERS with a fused projection gets [] and falls back to the config candidates — and in_proj_qkv is in _FUSED_QKV_LEAVES. Those leaves were declined outright before this PR, so this is the coincidence the whitelist was added to prevent. Either extend the owner path to the fused kind, or decline fused leaves under a listed owner. The rebase itself is clean — I ran it locally, 106 tests pass on 2 GPUs.
Fused and separate layouts now agree (non-blocking, worth putting in the description)
The full-matrix path scales a fused qkv by max(1, rows/cols)**0.5, up to sqrt(3) for [3h, h], while separately stored q/k/v each get 1.0. Today the step size depends on how the checkpoint happens to store the projection; per-head makes both 1.0. That's a correctness argument, stronger than the coverage one.
Also checked CodeGen — its 4-block reshape still lands on head boundaries when 4 divides num_heads, so it's covered rather than mis-split.
Implements the fused-QKV part of #8367, on top of #8384.
What this PR does
A fused QKV weight holds Q, K and V in one matrix. #8384 left it on the full-matrix path because the three sections do not share a head count under GQA. They do not have to.
Every head is
head_dimcontiguous rows of dim 0 in both layouts that ship:cat([q_proj, k_proj, v_proj]), what most training stacks store):n_qquery heads, thenn_kvkey heads, thenn_kvvalue heads, eachhead_dimrows;view(..., num_kv_heads, num_heads // num_kv_heads + 2, head_dim), GPT-NeoX's per-headq, k, v): each group isn_q/n_kvquery heads, one key head, one value head, eachhead_dimrows.Both layouts present the same row partition in a different order, and per-head Newton-Schulz is block-wise with the same
max(1, head_dim / in_features)**0.5scale on every block - so the update does not depend on which layout the weight uses, and the Q/K/V labelling never enters it. GQA changes how many heads a section holds, not where the blocks are.So a fused weight needs no section bookkeeping: it is tagged with
rows // head_dimhead blocks, which for 8 query heads over 2 KV heads is 12 rather than 8 or3 * 8. The candidate requires the exact fused total(num_attention_heads + 2 * num_kv_heads) * head_dim, which is also what declines a transposedConv1Dweight (GPT-2'sc_attn,[hidden, 3*hidden], whose dim 0 is the input axis).The tag rides on
muon_num_heads, somuon_update, the six ZeRO/DDP call sites and the config surface need no new API.Not in this PR
query_key_valuestays on the full-matrix path, and a test pins why: its config reportsnum_attention_headskey/value heads whilemulti_query=Truemakes the module build a single one, so the config's fused total (192 rows) does not describe the weight (80). That is a pre-existingAutoTPMetareading rather than something the per-head split introduces; the exact-total check sends it to the full-matrix path instead of splitting the wrong blocks.Tests
CPU coverage added: fused tagging under GQA (
8query heads over2KV heads is12blocks, not8), the exact-total guard, theConv1Ddecline, the real architectures, and sectioned/interleaved equivalence on a real gradient - the test builds one fused weight in each layout and requires the updates to be the same permutation of each other, which is the property the change rests on. Accelerated coverage adds a fused training model across ZeRO 1/2/3 at world_size 2.End-to-end training
2 x NVIDIA L20 46 GB, ZeRO-2, bf16, torch 2.10.0+cu128, dp=2. A 17,177,088-parameter LLaMA-shaped model whose attention holds Q|K|V in one
qkv_proj(6 layers, hidden 512, 8 query heads over 2 KV heads,head_dim64), 120 optimizer steps on 983,040 tokens of real text, global batch 16, both arms from an identical initialisation.per_head_muon: falseper_head_muon: trueblocks.*.qkv_proj.weightcarriesmuon_num_heads = 12and is the only tagged parameter; with the flag off every tag isNone.The two loss curves are indistinguishable, and that is expected rather than a result: both arms start from an untrained initialisation whose heads are interchangeable, so the split has nothing to separate - the same outcome #8384 reported for its models. The mechanism measurement is what shows the feature does something; a convergence claim needs trained checkpoints and is not made here.