[WS1][Ascend] Qwen3-8B Dense train-inference closeout for BF16 execution vs FP32 reference - #406
Merged
Merged
Conversation
zhangj1an
requested review from
Flink-ddd,
KJLdefeated,
bitborne and
inaniloquentee
as code owners
September 11, 2026 20:18
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
zhangj1an
force-pushed
the
feat/ws1-ascend-closeout
branch
from
September 12, 2026 04:52
0e34b95 to
9546a74
Compare
…quired profile Issue RL-Align#266 is the WS1 acceptance entry (C1-C11 / RL-Align#267-RL-Align#277) for single-GPU model-level train-inference consistency on full Qwen3-8B Dense, with cuda_bf16 and triton_cuda_bf16 as its required profiles. This adds the Ascend version: ascend_bf16 (backend family "ascend") carried through every one of C1-C11 on the same shared contract and the same harnesses. ascend_bf16 is required, not optional: a missing or unexecuted Ascend cell is red, never N/A and never a fallback to another vendor's kernels. Kernel gaps closed first: - silu is a required C2 chain node with no Ascend kernel. Added to csrc/ascend/activation.asc next to SwiGLU, sharing its tile geometry and FP32 sigmoid sequence, so silu(x) is bitwise equal to swiglu(x, ones). Dispatching SwiGLU-with-a-unit-operand instead would report SwiGLU provenance, which C1 treats as an undeclared backend. - The canonical row-fold VJP needs a deterministic FP32-in GEMM; the Ascend det_gemm kernel is BF16-in only. det_gemm_rowwise_ascend_fwd_fp32 exposes the existing lm_head_ascend kernel (FP32 input, one fixed per-row reduction order) as a general GEMM via B^T - the same construction CUDA uses to build det_gemm_rowwise_fwd_fp32 from its SM90 lm_head kernel. Casting the VJP to BF16 would have broken the contract's FP32-accumulation rule. C1-C11: - C1 tolerance_contract.json declares ascend_bf16 -> family "ascend"; tolerance.py requires it. No Ascend-private tolerance relaxation. TF32 holds by construction (Ascend has no TF32 mode). - C2 ws1_manifest.json gains the profile with all 11 nodes declared and 23 representative cases mirroring the CUDA set, each pinning a real .asc entry point. version -> ws1-c2-v8, identity regenerated; workload_id is unchanged so existing CUDA/Triton evidence stays bound to the workload. - C3/C4 check_forward_invariance.py / check_gradient_invariance.py take --backend-profile ascend_bf16 and run on the profile's own device. - C5 elementwise_inventory gains an ascend_verdict column. - C6/C7 kv_consistency and its CLIs resolve the device from the profile. - C8 four_judgment_matrix covers the profile and can be scoped per host. - C9 qwen3_dense is device-agnostic; canonical backward paths gained Ascend branches recording family="ascend". - C10 chain_gate and ws1_chain_gate.py run the full RL-Align#150 matrix on the NPU. - C11 ci/run_ws1_ascend_ci.sh plus .github/workflows/ws1-chain-npu.yml; ci/run_ws1_chain_gate.sh is parameterised through WS1_PROFILES. A host has a GPU or an NPU, not both, so the C8 sweep, candidate-evidence script and chain-gate CI script take an explicit profile list and each vendor's job proves its own profiles. C11 closes only when every required profile has gone green on its own hardware. rl_engine/kernels/gtest/accelerator.py holds the vendor-dependent facts the gates need and fails closed: no NPU means AcceleratorUnavailable, and pointing an Ascend profile at cuda:0 is rejected before any device probe. On-device evidence is not collected yet - it needs an Ascend host. Until then the Ascend C5 rows are tracked_red and the C8 Ascend cells are red, which is the correct pre-execution state. Includes PR RL-Align#405 (Ascend deterministic GEMM), which this builds on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PzQmyerKmyyiMPSyPGspCH Signed-off-by: Zhang Jian <jianmusings@gmail.com>
On-device bring-up of the RL-Align#266 closeout (feat/ws1-ascend-closeout) exposed two structural comparison mismatches and two missing canonical backward hooks; the C8 four-judgment sweep went from 10 red cells to 0 and the C2 candidate evidence from 21/23 to 23/23: - det_gemm accuracy gold: the deterministic GEMM rounds every 32-element leaf and every tree merge node to BF16, so comparing a candidate against the single-rounding torch.matmul gold fails structurally at near- cancellation outputs on random inputs (max_abs 2.0-4.0, reproducible in pure numpy on the manifest fixture data). The gold is now DetGemmTreeReferenceOp, the exact leaf-space mid-split tree. - det_gemm gradients: the autograd backward now uses the canonical FP32-accumulation rowwise VJP (det_gemm_rowwise_ascend_fwd_fp32) instead of the BF16 tree da/db kernels, so gradient_accuracy matches the unrounded FP32 reference grads to ULP. Determinism is preserved: the rowwise kernel reduces each output row in one fixed per-row order. - C4 gradient invariance: DetGemmAscendOp and RMSNormAscendOp now expose parameter_vjp_contributions_fp32 (the CUDA twin): per-row FP32 contributions that the harness accumulates in FP32 across call spans, so chunked / padded / permuted / singleton-aggregated weight gradients are bitwise identical (previously 1.5e-4 - 3.1e-4 drifts). - canonical embedding accepts the ascend family (the Ascend embedding's deterministic grad-weight reuses the CUDA construction bit-for-bit). - FP32-output attention: the model's FP32 composite attention edge had no Ascend path. The Ascend C kernel now accepts an outFp32 flag and emits the exact FP32 accumulator; DeterministicAttentionAscendOp.forward_fp32 exposes it (the twin of the CUDA op's forward_fp32). - tests/test_det_gemm_ascend.py backward reference updated to the FP32 matmul VJP (the gradient-accuracy gold semantics). Verified on device (Ascend 910B, CANN 9.0.0): C2 23/23, C8 88 green / 0 red / 8 N/A, C3/C4 invariance bitwise, det_gemm/attention operator suites green (34 + 26), and the C10 full-model gate now runs all eight cells with real backward; parity aggregates pass (max_abs_dlogp 1.7e-6). The C10 selected_logp config invariance still drifts on three pairs (BN/padded_left 0.12, B1-singleton/chunked 0.07, B1-singleton/full 1 ULP) - candidate-side model-wiring gaps tracked for follow-up. The contract's FP32-reference cell cannot run on 64 GB HBM (needs ~4.7 GiB more; the CUDA reference ran on an 80 GB H20). Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Zhang Jian <jianmusings@gmail.com>
The model feeds the selected-logp a non-contiguous slice (score_logits[:, :-1]), and FusedLogpAscendOp.apply silently fell back to the native torch log-softmax for non-contiguous inputs. The native path's per-row numerics depend on the batch layout, so the B1-singleton-aggregate cell's logp differed from the BN cell's by 1-2 fp32 ULP (19/27 tokens) and broke the C10 forward_invariance judgment (bitwise required). The wrapper now materializes the logits so the batch-invariant Ascend kernel runs for every NPU input; the B1-vs-BN selected_logp comparison is bitwise (0/27 diffs on the full-model gate cells). Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Zhang Jian <jianmusings@gmail.com>
… tiling) Left padding shifted the physical 64-key tile boundaries, and the softmax denominator (sumExp) is reduced per physical tile before the per-tile results are summed -- so the FP32 addition grouping of the valid keys depended on where the padding sat, and the FP32 composite edges amplified the 1-ULP-level difference across the 36 layers into a visible logp drift (BN/padded_left selected_logp max_abs 0.1197). The tiles are now anchored to the first valid key (keyBegin), so the valid keys always start at the first position of the first tile; the masked lanes are additionally zeroed after the Exp so they contribute exactly zero regardless of the vector Exp's behavior on the -FLT_MAX sentinel. Fully-masked batches fall through to the existing out=0 / lse=-inf path. Verified on device: the attention op's FP32 output and LSE are bitwise identical across left-pad lengths 0/1/63/64/65, and the BN/padded_left selected_logp is bitwise identical to BN/full (0/27 tokens, was 26/27). The B1-singleton/chunked cell still drifts: traced to a 1-bf16-ULP K/V divergence at layer 3 key 2 (the q_proj matches bitwise while k_proj / v_proj differ by one ULP) -- the chunked path's projection chain, not the attention kernel itself; tracked for follow-up. Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Zhang Jian <jianmusings@gmail.com>
torch mean/sum select shape-dependent reduction kernels on NPU and flip single-ULP results between batch layouts (verified: 52/140 rows flip between [1,7,H] and [1,20,H] on the same data). The canonical and native RMSNorm forwards computed the rstd with the torch mean, so the chunked path's [1,chunk,H] slices and the full path's [1,20,H] batch produced ULP-different rstd values; the difference entered at layer 3, amplified through the FP32 composite edges, and reached 0.07 at the selected logp (the B1-singleton/chunked C10 invariance failure). The native reference, the Ascend op, and the canonical path now share one shape_invariant_rstd helper: the sum of squares is reduced in FIXED 32-wide chunks first, so the intermediate shapes (and hence the reduction kernels) never depend on the batch layout, and the rstd is bitwise identical for every layout on every device. The chunked cell's internal stateful-prefill consistency check passes, and the BN/padded_left, B1-singleton/full, and B1-singleton/chunked selected_logp maps are all bitwise identical to BN/full (0/27 each; all three were non-zero before). tests/test_rms_norm.py's manual reference uses the shared helper (the implementation's formula changed; the test's independent formula mirrors it). The gtest acceptance checks are untouched. Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Zhang Jian <jianmusings@gmail.com>
The forward rstd was made shape-invariant earlier, but the backward's dx dot product s = sum(dy * w * x, dim=-1) still used the plain torch sum, whose reduction kernel is selected by shape on NPU and flips single-ULP results between batch layouts. The resulting dx differences propagated to every upstream parameter gradient: the B1-singleton/chunked cell differed from B1/full on 694/2394 weight gradients (max_abs 0.031). The ordinary and canonical backward now share _rms_norm_backward_rows, whose hidden-dim reduction is an explicit adjacent-pair FP32 tree (_fixed_row_sum) — elementwise adds whose pairing depends only on the hidden dimension, never on the row count. The ordinary backward reduces the dweight rows with the shared reduce_rows_fp32; the canonical backward keeps its session fold over the logical rows unchanged. Adds tests/test_ascend_rmsnorm_backward_partition.py: CPU/NPU partition regressions covering the fixed-pair sum (incl. odd widths and bf16 inputs), a float64-autograd backward oracle, and chunk-boundary independence for the canonical embedding + norm gradients. Verified on device: 20 new tests pass and the chunked cell's weight gradients are now bitwise identical to B1/full (0/399, was 694/2394 differing). The gtest checks and references are untouched. Signed-off-by: Zhang Jian <jianmusings@gmail.com> Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Zhang Jian <jianmusings@gmail.com>
zhangj1an
force-pushed
the
feat/ws1-ascend-closeout
branch
from
September 12, 2026 07:27
8296207 to
d452e95
Compare
…action The attention's VJP (the fp32 reference backward) ran the torch softmax/matmul on the padded layout, where left/right padding shifts the valid values inside the reduction trees and flips ULPs in dq/dk/dv (up to 7.0 between pad sides for identical logical tokens); the differences then propagated to every upstream parameter gradient (BN/padded_left: 390/2394 weight gradients differing, max_abs 0.031). The backward now compacts the valid tokens into the logical order before the VJP and scatters the gradients back, so the reductions are padding-invariant and the padded positions receive zero grads. Verified on device: the op-level dq/dk/dv are bitwise identical between the pad sides (0.0), the BN/padded_left weight gradients are bitwise identical to BN/full (0/399, was 390 differing), and the attention / rmsnorm / partition suites pass (137 passed, 32 skipped). The gtest checks and references are untouched. Signed-off-by: Zhang Jian <jianmusings@gmail.com> Co-Authored-By: Claude Code <noreply@anthropic.com> Signed-off-by: Zhang Jian <jianmusings@gmail.com>
zhangj1an
force-pushed
the
feat/ws1-ascend-closeout
branch
from
September 12, 2026 09:14
16bb4ae to
2993cff
Compare
…nvariance Replace the torch-compaction VJP with a dedicated Ascend C backward (deterministic_attention_backward_ascend.asc): three stream-ordered launches (rows/dV/dK) that recompute P/dS and the dq/dk/dv VJP with fixed keyBegin-anchored logical reduction orders. Gradients are now bitwise invariant to the batch layout and to where padding sits across physical lengths, closing the remaining PR RL-Align#406 follow-up (valid Sv=63/64 with pad=5 previously showed 5e-4/2.4e-4 residuals; now 0.0 bitwise for Sv=63/64/65/96/128 on both pad sides). FP16 (an optional contract row) falls back to the torch VJP path, since the kernel is bf16-only. test_backward_grads now resolves the contract's gradient_accuracy/attention row and compares candidate grads against the FP32-kept reference VJP per the WS1 precision standard. Verified on device: 26/26 attention tests; boundary padding invariance bitwise; the full C10 gate reports gradient_invariance 2394/2394 with max_abs 0.0 and forward invariance 7/7 bitwise. Signed-off-by: Zhang Jian <jianmusings@gmail.com> Co-Authored-By: Claude Code <noreply@anthropic.com>
…B HBM The FP32-reference full-model backward OOMs the 64 GB HBM (~4.7 GiB short), so the C10 reference cell runs in BF16 on this host (documented in the PR description): the gold topology resolves family='pytorch' with a plain matmul and the reference model builds in bfloat16. The accuracy judgment must be re-assessed with the official FP32 reference on a larger-memory device. Signed-off-by: Zhang Jian <jianmusings@gmail.com> Co-Authored-By: Claude Code <noreply@anthropic.com>
The FP32-reference full-model backward OOMs the 64 GB HBM with resident weights (~4.7 GiB short). The reference cell now keeps the FP32 weights CPU-resident (Qwen3DenseWeightsOffloaded) and pages each weight onto the NPU per access: autograd holds each copy only until its VJP consumes it, and the FP32 gradients accumulate on the CPU leaves, so the peak HBM is ~36 GB instead of ~70 GB. The paging copies are exact, so the reference numerics are bitwise identical to the resident-FP32 model (verified on device: layer-0/1 forward and all gradients bitwise, offloaded vs resident). The gate now measures the accuracy judgments against the official FP32 reference on this host: selected_logp max_abs 0.0857 (atol 0.06) and 44/798 gradient rows fail at near-zero reference-gradient elements (max_abs 0.11-1.88), a candidate-side accuracy gap against the contract tree reference (CUDA H20: 798/798 at 0.1034). Invariance and parity are unaffected. Signed-off-by: Zhang Jian <jianmusings@gmail.com> Co-Authored-By: Claude Code <noreply@anthropic.com>
maxiaosong1124
approved these changes
Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Latest Status [12 Sep 2026]
Ready to be included in v0.1.0. No further PR will be created for Ascend WS1. The discrepancy listed below stems from BF16-execution-vs-FP32-reference quantization drift (a control chain built entirely from gold ops reproduces the same failure class), not from a defect in any individual Ascend kernel.
Forward accuracy: Against the same FP32 reference, the full-model selected_logp error is max_abs 0.0388 on CUDA (H20) versus 0.0857 on Ascend, about 2.2× larger on Ascend.
Gradient accuracy: On the same judgment, the CUDA candidate passes all 798 weight-gradient rows with a worst-case max_abs 0.1034, while the Ascend candidate fails 44 rows with a worst-case max_abs 1.88 — about 18× larger at the near-zero gradient elements.
Details
This PR closes #266 for the Ascend profile by mirroring the protocol and requirement of #315 (the WS1 full Qwen3-8B Dense train-inference closeout), adding
ascend_bf16(BF16 execution vs FP32 reference) as the third required profile.Passing:
0.0 / 0.0 / 0.0(exact).deterministic_attention_backward_ascend.asc) with fixed keyBegin-anchored reduction orders, closing the cross-physical-length padding-invariance follow-up (Sv=63/64/65/96/128 with pad=5: 0.0 bitwise, both pad sides). FP16 backward falls back to the torch VJP.Qwen3DenseWeightsOffloaded), verified bitwise against a resident-FP32 model.Not passing (accuracy judgments):
selected_logp max_abs 0.0857vsatol 0.06.## Validation Identity
1fe03e7(head)ws1-qwen3-8b-dense-primary-v620260812ws1-c2-v8Qwen/Qwen3-8B@b968826d9c46dd6066d109eabc6255188de91218, content hashfc664a19c52c82b6f5ddb33d4fe2723181daeb93a344b16fee6369963e5a13a5## C1 contract (tolerance contract, dtype policy, aggregates)
tolerance_contract.jsondeclaresascend_bf16 -> backend_family "ascend"as a third required profile; thresholds, roles and aggregates unchanged.## C2 candidate evidence (manifest-pinned, random inputs, --check-grad)
python scripts/ws1_candidate_evidence.py --profile ascend_bf16 --all --check-gradThe deterministic GEMM's accuracy gold is the leaf-space tree reference (
DetGemmTreeReferenceOp) and its backward is the canonical FP32-accumulation rowwise VJP; both match the unrounded FP32 reference to ULP.## C3 forward invariance / C4 gradient invariance
silu): all pairs bitwise,max_abs=0.0(chunk, batch, permutation, left/right padding).det_gemm/rms_norm/qk_norm: green via the canonicalparameter_vjp_contributions_fp32hooks — chunk/permute/padding/singleton pairs bitwise.## C5 elementwise inventory
tests/test_elementwise_inventory.pypasses (CPU classification); on-NPU verdicts flow through C3/C4.## C6 direct decode-prefill consistency
python scripts/check_decode_prefill.py --backend-profile ascend_bf166/6 passed, max_abs 0.0.
## C7 stateful KV + generate-rescore
python scripts/check_stateful_kv.py --backend-profile ascend_bf16passed.
## C8 four-judgment matrix sweep
python scripts/sweep_ws1_four_judgments.py --execute --profile ascend_bf16 --jsonpacklayout-helper boundary)## C9 full-model assembly
## C10 full Qwen3-8B Dense chain gate
python scripts/ws1_chain_gate.py --backend-profile ascend_bf16 --model qwen3-8b-dense --dtype bfloat16 --weights required --weights-path ...The FP32 reference runs on this host with CPU-resident weights paged onto the NPU per access (bitwise-identical to a resident-FP32 model); the gate runs all eight cells with real backward:
max_abs_dlogp = 0.0,approx_kl0 = 0.0,clipfrac0 = 0.0(exact)## C11 CI
Not runnable on this host (requires the self-hosted Ascend runner and the GitHub workflow). The CI script
ci/run_ws1_ascend_ci.shwas exercised step-by-step locally; its gates are the sections above.## Regression tests
## Notes
shape_invariant_rstd. All verified bitwise at the cell level.🤖 Generated with Claude Code