qwen4_exp: build o_proj row-parallel, as every other family does - #429
qwen4_exp: build o_proj row-parallel, as every other family does#429gberasmus87 wants to merge 1 commit into
Conversation
qwen4_exp is the only family that builds its attention o_proj as LinearReplicated; llama, gpt_oss and minimax_m2 all use LinearOProj. That is correct at TP=1 and wrong under TP>1 three ways at once: qkv_proj is column-parallel, so a rank's attention output is its local head slice rather than the full qo_attn_dim, o_proj therefore has to take the sharded input dim, and the partial sums need an all-reduce. LinearReplicated keeps the full [hidden, qo_attn_dim] weight, expects the unsharded input and reduces nothing. It also fails quietly: a missing all-reduce leaves each rank holding a partial sum that still decodes to fluent-looking text. LinearOProj degenerates to exactly LinearReplicated at TP=1 -- div_even(x, 1) == x, and the all-reduce is skipped when tp_size == 1 -- so this is a no-op for main as it stands and only changes what FlashML-org#385 finds when the two meet. It adds no constraint from calling get_tp_info() in __init__ either, since the same constructor already reaches it two lines up through LinearColParallelMerged. The comment above the branch now describes what is built. Raised by @gdevenyi against the earlier form of this work in FlashML-org#392.
|
Confirming this from the TP>1 side, which is the half the PR cannot test on one card.
self.o_proj = LinearOProj(
self.qo_attn_dim, config.hidden_size, has_bias=False,
quant_config=config.quant, prefix=f"{prefix}.o_proj",
)On 2 x RTX 6000 Ada (sm_89, PCIe, no NVLink), So: TP=1 bit-identity from you, TP=2 accuracy from us. Between them the change is covered in both directions. Two notes for whoever merges. It is not a no-op the moment #385 lands — it is a prerequisite. #385 shards
self.o_proj = make_replicated_quant("none", mode, self.qo_attn_dim, config.hidden_size)That is consistent today only because #392 declares itself TP=1-only. If #428 supersedes #392 and drops that restriction, this branch needs the same treatment, and it will fail the same quiet way. Worth a comment there rather than a silent gap. 🤖 Generated with Claude Code |
|
Thanks — GSM8K 97.00% at TP=2 is exactly the half I could not produce, and you are right that it is the load-bearing number. A token-level smoke test cannot distinguish a correct Your That also tightens your merge-order point. With the block-fp8 branch gone, this is a one-line change to the single site #385 needs corrected, so landing it first genuinely does make #385 smaller rather than just earlier. To be precise about what is verified when: the |
|
Follow-up: I swept the rest of the repo for this pairing and it is not unique to qwen4_exp.
All three are latent exactly as this one is: their loaders raise Left alone deliberately: glm5_next, glm_moe_dsa and minimax_m3 replicate their q/kv projections too, so replicated |
qwen4_expis the only family that builds its attentiono_projasLinearReplicated. llama, gpt_oss and minimax_m2 all useLinearOProj.That is correct at TP=1 and wrong under TP>1 in three ways at once.
qkv_projis column-parallel, so a rank's attention output is its local head slice rather than the fullqo_attn_dim;o_projtherefore has to take the sharded input dim; and the partial sums need an all-reduce.LinearReplicatedkeeps the full[hidden, qo_attn_dim]weight, expects the unsharded input, and reduces nothing. The loader already assumes the row-parallel layout —_shardputso_projon dim 1.It also fails quietly. A missing all-reduce leaves each rank holding a partial sum that still decodes to fluent-looking text, so there is no crash to catch it.
LinearOProjdegenerates to exactlyLinearReplicatedat TP=1:div_even(x, 1) == x, and the all-reduce is skipped whentp_size == 1. So this is a no-op formainas it stands, and only changes what #385 finds when the two meet. It adds no constraint from callingget_tp_info()in__init__either — the same constructor already reaches it two lines up throughLinearColParallelMerged, so that path was engine-only before and still is.The comment above the branch also promised a row-parallel
o_projthat the code did not build; it now describes what is there.Verified at TP=1 on an RTX PRO 4000 Blackwell: the built
o_projis aLinearOProjwith weight[hidden, qo_attn_dim]andlocal_input_size == qo_attn_dim, and its forward is bit-identical (max |diff| = 0.0) to aLinearReplicatedcarrying the same weight, on CPU and on CUDA.tests/models/qwen4_exp/shows the same pass/fail set with and without the change.Raised by @gdevenyi on #392 as a merge hazard against #385. Split out here so it can land on its own — #392's reader half is superseded by #428.