[WS1][Ascend] [Qwen3-8b] LM head ops - #371
Open
zhangj1an wants to merge 1 commit into
Open
Conversation
Mirror the SM90 CUDA lm_head kernel (csrc/cuda/embedding_lm_head_sm90.cu)
with an Ascend C forward in csrc/ascend/lm_head_ascend.asc: one output
element per block, full hidden-dimension fp32 reduction over a fixed tile
order, bias in fp32, round-to-nearest output cast. No Split-K, so a row's
logits are bitwise identical across batch sizes, positions, and block
assignments on the NPU. The hardware reduction tree differs from CUDA's
warp-shuffle tree, so the comparison against the torch.mv reference is
tolerance-based per the reduction contract (not bitwise).
Wrapper mirrors SM90LMHeadOp (forward/forward_fp32, bias, dtype gate) with
an fp32-formula VJP backward. Registered as the ascend candidate in the
lm_head gtest spec and in the registry's NPU priority map. Ports the
shared-module Ascend build (npu_module.cpp + setup.py bisheng) and the
check_operator --device npu support.
Verified on Ascend 910 / CANN 8.5.1:
- gtest lm_head ascend candidate fp32/bf16/fp16, output + both gradients:
all pass at the reduction contract tolerances.
- Batch invariance bitwise: batch 1 vs {2,4,16,300}, positions 1..7,
multi-tile H=10000, repeated runs.
- pytest tests/test_lm_head_ascend.py: 23 passed.
- Regression: test_batch_invariant_logp.py 44 passed, test_dispatch.py
13 passed.
zhangj1an
requested review from
Flink-ddd,
KJLdefeated,
bitborne and
inaniloquentee
as code owners
September 1, 2026 05:42
|
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: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
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 [1 Sep 2026]
Ready for review.
Summary
Port of the CUDA deterministic LM-head (
csrc/cuda/embedding_lm_head_sm90.cu+SM90LMHeadOp) to Ascend NPU:_C_npu.lm_head_ascend): mirrors the SM90 CUDA kernel's structure —out[n, v] = dot(hidden[n, :], weight[v, :]) (+ bias[v])with one output element per block iteration, the full hidden-dimension reduction inside that block over a fixed tile order (products → per-tile sum → sequential scalar accumulation, all fp32), bias added in fp32, final cast to the output dtype with round-to-nearest. No Split-K, no algorithm selection.MAX_BLOCKS=128); the instruction sequence for an element depends only on H, never on N or block assignment, so a row's logits are bitwise identical across batch sizes, row positions, and block assignments on the NPU (verified withtorch.equal).torch.mv) has its own unspecified internal order, so bitwise parity with either is not claimed for this reduction op — the gtest comparison is tolerance-based per thereductioncontract (fp32 drift measured ~2.4e-4 vs atol/rtol 1e-4/1e-4 at the 2x16x4096x257 shape... see Test results; bf16/fp16 drift is ULP-scale after the output cast).AscendLMHeadOpmirrorsSM90LMHeadOp(forward/forward_fp32, bias support, dtype gate fp32/bf16/fp16, hidden[-1] == weight[1] check). The backward is the standard linear VJP computed in fp32 on the NPU (grad_hidden = grad @ W,grad_weight = grad^T @ H, bias = fixed-order row sum) then cast to the input dtypes — the CUDA op routes through_C.det_gemm, which has no NPU equivalent; gradients compare against the native backward at the gradient contract tolerance."ascend"inoperator_specs.py;ASCEND_LM_HEADin the registry with the NPU priority[ASCEND_LM_HEAD, PYTORCH_NATIVE_LM_HEAD];scripts/check_operator.pygains--device npusupport.csrc/ascend/npu_module.cppconsolidates the singlePYBIND11_MODULE(batch_invariant_logp + lm_head);batch_invariant_logp_ascend.asconly drops itsPYBIND11_MODULEblock.setup.pygains the Ascend extension build (bisheng,**/*.ascglob) with the CANN env export in_find_ascend_home.Build notes (same pattern as PR #320 / #355)
Each
.ascsource file can define only onePYBIND11_MODULE(linking multiple sources with Bisheng causes a duplicatePyInit__C_npuerror), so pybind registrations are consolidated incsrc/ascend/npu_module.cpp;batch_invariant_logp_ascend.asconly drops itsPYBIND11_MODULEblock.Files
csrc/ascend/lm_head_ascend.asccsrc/ascend/npu_module.cpp_C_npupybind registration (batch_invariant_logp + lm_head). New.csrc/ascend/batch_invariant_logp_ascend.ascPYBIND11_MODULE(moved to the aggregated file). Kernel logic unchanged.rl_engine/kernels/ops/ascend/linear/lm_head.pyAscendLMHeadOp(Ascend C forward + fp32-formula VJP backward). New.rl_engine/kernels/ops/ascend/linear/__init__.pyrl_engine/kernels/ops/ascend/__init__.pylinearsubpackage.rl_engine/_C_npu.pyilm_head_ascendtype stub.rl_engine/kernels/gtest/operator_specs.py"ascend"candidate for thelm_headop.rl_engine/kernels/registry.pyASCEND_LM_HEADbackend + NPU priority map entry.rl_engine/tests/test_dispatch.pyscripts/check_operator.py--device npu/ auto-detect support.tests/test_lm_head_ascend.pydocs/operators/lm_head.mdsetup.py_find_ascend_home.Test
Test results
Environment: Ascend 910, CANN 8.5.1 (Bisheng), torch 2.10.0 + torch_npu 2.10.0.post2.
lm_headascend candidate, fp32 × 2x16x4096x257, output + both gradientslm_headascend candidate, bf16 × 2x16x4096x257, output + both gradientslm_headascend candidate, fp16 × 2x16x4096x257, output + both gradientstorch.equal)tests/test_lm_head_ascend.pytests/test_batch_invariant_logp.pyrl_engine/tests/test_dispatch.pygtest raw output (fp32, representative)
pytest: tests/test_lm_head_ascend.py (summary)
Notes
torch.mvinternal order are unspecified platform internals, so no independent kernel can match them bit for bit. The Ascend kernel pins its own fixed order, which makes the op batch-invariant bitwise on the NPU — the property the WS1 contract'sforward_invariancerow requires (bitwise) — whileforward_accuracyis tolerance-based. Note the bf16 gtest numbers look large in absolute terms but are ULP-scale on logits of magnitude ~40 (max_rel 3.9e-3).torch.matmulon NPU), cast back to the input dtypes; gradients compare against the native backward at the gradient contract tolerance.NativeLMHeadOp.forward's bf16 dtype path is itself unavailable on this CANN (torch.mvrejects bf16), which is why the reference comparisons go throughforward_fp32.black/isort/flake8(line-length 100) pass for all modified Python files.