Repository navigation
[Feature] NPU (Ascend) training performance optimizations for Qwen3.5 dense text model #2118
Description
Motivation
Training Qwen3.5-class models (GatedDeltaNet layers, dense MLPs, a 154,880-vocab lm head) on Ascend NPUs currently hits four gaps in the v1 ops/engine stack:
- GDN stack dead ends.
get_zero_centered_rms_norm_fn()inxtuner/v1/ops/rms_normraisesNotImplementedErroron NPU, so the in-repo zero-centered RMSNorm route is unusable on Ascend; and the GatedDeltaNet op stack (causal conv1d, chunked gated delta rule, gated RMSNorm) falls back to slow fp32 torch decompositions — causal conv1d has no NPU kernel at all in the package. The native fp32 decomposition OOMs at step 1 on all 16 ranks at the default GA=4/mbs=2 config. - Eager activation chain. The dense-MLP activation runs as a two-pass
silu(gate)+mulforward and a two-pass backward — four full-tensor memory passes where fused one-pass kernels need one each. - No NPU liger loss.
loss_cfg.mode == "liger"imports liger_kernel's stock CUDA class unconditionally, so the fused-linear-cross-entropy path (which avoids materializing full logits over the 154,880-token vocab) has no working NPU implementation. - FSDP2 all-gather memory strand. torch_npu's caching allocator segregates cached blocks per stream, so FSDP2's full-param all-gather on the separate
all_gather_streamstrands reserved memory thatempty_cachecannot reclaim — pure per-stream fragmentation, at no benefit. - No host-side CPU affinity. Every rank drives its NPU from the host — aclnn op dispatch, tensor metadata, and the device's companion host threads are all host CPU work; unpinned ranks can land on NUMA nodes remote from their NPU, and a single straggler rank paces the synchronized step.
Proposal
Six independent PRs move the production NPU implementations into the package — one per gap, each opt-in/env-gated, each leaving non-NPU behavior unchanged — plus two stacked follow-up PRs (#2240, #2241):
- [Feature] NPU fused zero-centered RMSNorm (XTUNER_NPU_FUSED_ZC_NORM) #2119 — NPU fused zero-centered RMSNorm: a single
npu_rms_normpass with the(1 + w)scale folded into the fp32 gamma (bf16 output rounds once); fp32-decomposition fallback; gated byXTUNER_NPU_FUSED_ZC_NORM(default on). - [Feature] NPU AscendC op stack for GatedDeltaNet (causal conv1d / chunk delta rule / fused gated norm) #2120 — NPU AscendC op stack for GatedDeltaNet: causal conv1d with three-level in-module dispatch, the chunked gated delta rule kernel wired to fla_npu AscendC ops plus mindspeed triton helpers with a device-side chunk-indices cache, and a fused gated RMSNorm. Dispatch is gated on
get_device() == 'npu'plusXTUNER_NPU_*env knobs (all default on). - [Feature] Fused SwiGLU Triton kernels for MLP activation (XTUNER_NPU_FUSED_SWIGLU) #2128 — Fused SwiGLU Triton kernels: one-pass
silu(gate)·upforward and backward for the dense-MLP activation. Device-agnostic plain Triton (no torch_npu dependency), opt-in viaXTUNER_NPU_FUSED_SWIGLU(default off), read live so runtime injection works. - [Feature] NPU liger fused-linear-cross-entropy loss (XTUNER_LOSS_MODE=liger) #2129 — NPU liger fused-linear-cross-entropy loss: a
get_flce_loss_cls()device dispatch — NPU returns an in-packageNPUFusedLinearCrossEntropyLossdriving liger-ascend's triton kernels (the stock ascend plain-CE kernel miscomputesloss_1din multi-rank training, so the forward computes CE via torch on the matmul logits), CUDA keeps liger_kernel's stock class. Chunked forward behind a full-logits byte gate; no device env knob by design — the fallback arm is numerically wrong. - [Feature] Route FSDP2 unshard to the compute stream (XTUNER_FSDP_UNSHARD_ASYNC_OP) #2130 — Route FSDP2 unshard to the compute stream:
XTUNER_FSDP_UNSHARD_ASYNC_OP=1(default off) issues FSDP2's parameter all-gather on the compute stream so the per-stream strand is never created;wait_for_unshard+ explicit forward prefetch keep the overlap. - [Feature] Bind NPU training ranks to NUMA-local CPU slices (XTUNER_NPU_CPU_BIND) #2131 — Bind NPU training ranks to NUMA-local CPU slices: a stdlib-only
npu_cpu_binderpins each rank process and its device's host threads (acl/release/hccp) to an 8-core NUMA-local slice, wired into_try_bind_numaso all training modes bind;XTUNER_NPU_CPU_BIND(default on), bind failures degrade to a log line. - [Fix] fla_npu >= 26.10 compatibility for the GDN AscendC stack (chunk loader + causal conv backends) #2240 — fla_npu ≥ 26.10 compatibility for the GDN AscendC stack (stacked on [Feature] NPU AscendC op stack for GatedDeltaNet (causal conv1d / chunk delta rule / fused gated norm) #2120): fla_npu's 26.10 line splits its API surface — stable raw ops move to
fla_npu.ops.ascendc.*and the legacytorch.ops.npu.*registrations need an explicitload_legacy_torch_ops(). The causal-conv backend becomes a dual-backend resolver decided once per process (stable API on 26.10, legacytorch.opson 26.6), the chunk loader degrades cleanly when the embedded legacy extension is absent, and the startup self-probe upgrades from isfinite-only to a numeric check against anF.conv1dautograd reference, so "finite but wrong" kernels are rejected into the unmasked-correction fallback instead of surfacing as mid-training NaN.XTUNER_NPU_GDN_APIno longer selects the conv backend. Behavior on fla_npu 26.6 is unchanged. - [Feature] Optimize the NPU liger FLCE backward and add a fused LM z-loss #2241 — NPU liger FLCE backward optimization + fused LM z-loss (stacked on [Feature] NPU liger fused-linear-cross-entropy loss (XTUNER_LOSS_MODE=liger) #2129): backward scratch-buffer reuse and vocabulary-slab grad_weight accumulation remove the two large logits-path transients (~1.9 GiB fresh alloc per chunk + the ~2 GiB
[V, H]temporary; −4.6 GB max_memory e2e), the backward recompute chunk ties toLOSS_CHUNK_SIZE, a latentreduction='none'multi-chunkgrad_outputindexing bug is fixed (chunks > 0 read wrong rows;ce_loss.pyusessum, so latent only), the LM z-loss (CELossConfig.z_loss_coefficient, default off) fuses CE+LSE² into one forward kernel, and the no-weight backward call gains 0.8.1/0.8.5 dual support viaHAS_TAILsignature detection.
Dependency (NPU only)
- flash-linear-attention-npu (fla_npu) v26.6.0, compiled and installed from source ([Feature] NPU fused zero-centered RMSNorm (XTUNER_NPU_FUSED_ZC_NORM) #2119 / [Feature] NPU AscendC op stack for GatedDeltaNet (causal conv1d / chunk delta rule / fused gated norm) #2120); the 26.10 line (decoupled runtime, stable
fla_npu.ops.ascendc.*raw ops + explicit legacy loader) is supported by [Fix] fla_npu >= 26.10 compatibility for the GDN AscendC stack (chunk loader + causal conv backends) #2240. - liger-kernel
0.8.1([Feature] NPU liger fused-linear-cross-entropy loss (XTUNER_LOSS_MODE=liger) #2129) and0.8.5([Feature] Optimize the NPU liger FLCE backward and add a fused LM z-loss #2241 —HAS_TAILsignature detection; the fused z-loss kernels need >= 0.8.4) — the versions whoseliger_kernel.ops.backends._ascendships the ascend triton kernels used by the NPU FLCE; CUDA installs keep the version they already use.
Both are imported lazily inside the NPU dispatch branches only; non-NPU environments need no new dependency.
Results (Qwen3.5 dense text model, 16 Ascend NPUs, frozen launcher recipe)
-
Step-1/step-2 losses bitwise-identical across A/B runs (anchor gate), 0 NaN; 1000-step run exits 0 with 0 NaN.
-
Global throughput 179.2K tok/s on the default GA=4/mbs=2 config with the GDN stack enabled; the fused RMSNorm is +10.6% over the native fp32 decomposition at GA=2/mbs=1, and is what makes the default config fit in memory at all (the native decomposition OOMs at step 1 on all 16 ranks).
-
CPU binding (
XTUNER_NPU_CPU_BIND) is scheduling-only — step-1/step-2 losses bitwise-identical, peak memory unchanged — and adds +1.7% global tok/s at 256 ranks (16 nodes × 16 Ascend NPUs, 100-step A/B). -
fla_npu 26.10 compat ([Fix] fla_npu >= 26.10 compatibility for the GDN AscendC stack (chunk loader + causal conv backends) #2240): on a 26.10 dev wheel (256 Ascend NPUs = 16 nodes × 16, Qwen3.5 dense text, 10-step benchmark, mbz=1/gas=2/gbs=512) the GDN chunk route resolves via the stable API, 10/10 steps, 0 NaN, benchmark success; the broken stable conv adapter is rejected by the numeric startup probe and the unmasked-correction fallback engages (error band fwd 0.094 / dx 0.125 / dw 1.0). On fla_npu 26.6 the conv route is bit-exact at dispatcher level (dx/dw within the same few-ulp band as [Feature] NPU AscendC op stack for GatedDeltaNet (causal conv1d / chunk delta rule / fused gated norm) #2120).
-
Liger FLCE backward optimization ([Feature] Optimize the NPU liger FLCE backward and add a fused LM z-loss #2241; same implementation A/B'd on an internal fork, GLM-5.3-Flash 30B VL, 16 NPUs, 10 steps): step-1 loss bitwise identical to baseline,
max_memory−4.6 GB sustained (27.6 vs 32.2 GB steady), steady tok/s +4.2% (trend only).
Env knobs (per PR)
| PR | Env knob | Default | Function |
|---|---|---|---|
| #2119 | XTUNER_NPU_FUSED_ZC_NORM |
1 (on) |
NPU fused zero-centered RMSNorm — one npu_rms_norm pass with the (1 + w) scale folded into the fp32 gamma; 0 (or a failed built-in npu_rms_norm self-test) falls back to the fp32 decomposition |
| #2120 | XTUNER_NPU_GDN_ASCENDC |
1 (on) |
Master gate for the GDN AscendC chunk-kernel route; 0 falls back to the in-repo chunk_gated_delta_rule |
| #2120 | XTUNER_NPU_CONV |
1 (on) |
Master gate for the NPU causal-conv1d stack; 0 falls back to the in-repo causal_conv1d_fn |
| #2120 | XTUNER_NPU_GDN_DEVICE_INDICES |
1 (on) |
Device-side chunk-indices cache (replaces the mindspeed host helper); 0 restores the host-side helper |
| #2120 | XTUNER_NPU_GDN_SPLIT_MAX_TOKENS |
131072 (unset) |
Fixed-chunk cap (tokens) for long sequences; 0 disables the cap; non-integer values raise |
| #2120 | XTUNER_NPU_FUSED_GATED_NORM |
1 (on) |
Fused gated RMSNorm for GDN (also subject to the built-in npu_rms_norm probe); 0 falls back to the fp32 decomposition |
| #2128 | XTUNER_NPU_FUSED_SWIGLU |
off (0) |
Fused single-pass SwiGLU forward/backward (plain Triton, also runs on CUDA); opt-in, read live so runtime injection works; off = eager silu·mul chain |
| #2129 | XTUNER_LIGER_CHUNK_FWD |
1 (on) |
Chunked lm-head+CE forward in liger mode when the full logits exceed the byte gate; 0 forces the full-logits path |
| #2129 | XTUNER_LIGER_CHUNK_FWD_LIMIT |
2048 (MiB) |
Full-logits byte gate for the chunked liger forward |
| #2129 | LOSS_CHUNK_SIZE |
1024 |
Rows per chunk in the chunked liger forward |
| #2241 | XTUNER_LIGER_GRAD_WEIGHT_CHUNK |
4096 |
Rows per vocabulary slab in the liger backward grad_weight accumulation; bounds the [V, H] grad temporary to a 4096-row slab |
| #2130 | XTUNER_FSDP_UNSHARD_ASYNC_OP |
0 (off) |
Route FSDP2's parameter all-gather (unshard) onto the compute stream, removing the per-stream reserved-memory strand |
| #2131 | XTUNER_NPU_CPU_BIND |
1 (on) |
Per-rank pinning to the NPU-local CPU slice (main process + acl/release/hccp host threads); 0 disables; bind failures log and continue |
Notes: #2129 selects its NPU path via the loss config (loss_cfg.mode == "liger", in our runs injected as XTUNER_LOSS_MODE=liger) — the device dispatch itself has no env knob by design; its three knobs above only tune the chunked forward. #2120 additionally reuses the existing XTUNER_HF_IMPL knob for bitwise-parity comparison against the fla/HF reference implementations. #2241 additionally adds the config field CELossConfig.z_loss_coefficient (default 0.0) for the fused LM z-loss — a config, not an env knob; in liger mode the z-loss enters the gradient and logging (lm_z_loss) but not the loss value. #2240 narrows XTUNER_NPU_GDN_API to the GDN chunk op resolution only (it no longer selects the causal-conv backend, which is auto-resolved per fla_npu version instead).
PRs
-
[Feature] NPU fused zero-centered RMSNorm (XTUNER_NPU_FUSED_ZC_NORM) #2119 — NPU fused zero-centered RMSNorm (
XTUNER_NPU_FUSED_ZC_NORM) -
[Feature] NPU AscendC op stack for GatedDeltaNet (causal conv1d / chunk delta rule / fused gated norm) #2120 — NPU AscendC op stack for GatedDeltaNet (causal conv1d / chunk delta rule / fused gated norm)
-
[Feature] Fused SwiGLU Triton kernels for MLP activation (XTUNER_NPU_FUSED_SWIGLU) #2128 — Fused SwiGLU Triton kernels for MLP activation (
XTUNER_NPU_FUSED_SWIGLU) -
[Feature] NPU liger fused-linear-cross-entropy loss (XTUNER_LOSS_MODE=liger) #2129 — NPU liger fused-linear-cross-entropy loss (
XTUNER_LOSS_MODE=liger) -
[Feature] Route FSDP2 unshard to the compute stream (XTUNER_FSDP_UNSHARD_ASYNC_OP) #2130 — Route FSDP2 unshard to the compute stream (
XTUNER_FSDP_UNSHARD_ASYNC_OP) -
[Feature] Bind NPU training ranks to NUMA-local CPU slices (XTUNER_NPU_CPU_BIND) #2131 — Bind NPU training ranks to NUMA-local CPU slices (
XTUNER_NPU_CPU_BIND) -
[Fix] fla_npu >= 26.10 compatibility for the GDN AscendC stack (chunk loader + causal conv backends) #2240 — fla_npu >= 26.10 compatibility for the GDN AscendC stack (chunk loader + causal conv backends; stacked on [Feature] NPU AscendC op stack for GatedDeltaNet (causal conv1d / chunk delta rule / fused gated norm) #2120)
-
[Feature] Optimize the NPU liger FLCE backward and add a fused LM z-loss #2241 — NPU liger FLCE backward optimization + fused LM z-loss (
XTUNER_LIGER_GRAD_WEIGHT_CHUNK,CELossConfig.z_loss_coefficient; stacked on [Feature] NPU liger fused-linear-cross-entropy loss (XTUNER_LOSS_MODE=liger) #2129)
Activity
[-][Feature] NPU (Ascend) acceleration for GatedDeltaNet and RMSNorm ops[/-][+][Feature] NPU (Ascend) training performance optimizations for Qwen3.5 dense text model[/+]longboat2010 commentedon Sep 27, 2026
New companion PR: #2129 — NPU liger fused-linear-cross-entropy loss (
XTUNER_LOSS_MODE=liger), independent of #2119 / #2120 / #2128.mode="liger"now works on NPU via an in-package FLCE that drives liger-ascend's triton kernels directly (the stock ascend plain-CE kernel miscomputesloss_1din multi-rank training); CUDA keeps liger_kernel's stock class. Chunk-vs-liger A/B (Qwen3.5 dense text model, 16 Ascend NPUs, 30 steps, GA=4/mbs=2): step-1/step-2 losses bitwise-identical, +12.3% global tok/s (157.9K → 177.3K), +1.89 GB peak memory, 0 NaN.longboat2010 commentedon Sep 27, 2026
New companion PR: #2130 — Route FSDP2 unshard to the compute stream (
XTUNER_FSDP_UNSHARD_ASYNC_OP), independent of #2119 / #2120 / #2128 / #2129.Behind the knob (default off), FSDP2's parameter all-gather is issued on the compute stream instead of the separate
all_gather_stream. On Ascend the caching allocator segregates cached blocks per stream, so the separate-stream unshard buffer strands reserved memory thatempty_cachecannot reclaim; routing the unshard to the compute stream removes the strand, whilewait_for_unshard+ explicit prefetch keep the overlap. A/B (Qwen3.5 dense text model, 16 Ascend NPUs, 30 steps, GA=4/mbs=2): reserved −1.04 GB, live peak unchanged, TGS parity within noise, step-1/step-2 losses bitwise-identical, 0 NaN.longboat2010 commentedon Sep 27, 2026
New companion PR: #2131 — Bind NPU training ranks to NUMA-local CPU slices (
XTUNER_NPU_CPU_BIND), independent of #2119 / #2120 / #2128 / #2129 / #2130.A stdlib-only
npu_cpu_binderpins each rank process and its NPU's host threads to an 8-core NUMA-local slice, wired into_try_bind_numa(all training modes bind). A/B (Qwen3.5-2B dense text, 16 nodes × 16 Ascend NPUs = 256 ranks, 100 steps): binding verified 256/256, +1.7% global tok/s (2527.4K vs 2485.7K; paired clean steps +4.0% median, 9/12), step-1/step-2 losses bitwise-identical, peak memory unchanged.PussyCat0700 commentedon Oct 27, 2026
Follow-up precision fix: #2222 — Preserve FP32 target subtraction in NPU Liger CE backward (draft, depends on #2129).
We found an extra low-precision rounding in the no-weight Ascend CE backward used by the proposed NPU FLCE path: the target currently follows
BF16(BF16(p_y * u) - u)rather than subtracting/scaling in FP32 before a single BF16 store. A dependency-free high-confidence-target example gives approximately-2.074e-10instead of-5.120e-12; the fix yields-5.116e-12. This is a deterministic local derivative bias, not evidence that global gradients are amplified by the same ratio or that all run-to-run variation/NaN events have this cause.The PR adds a local backward kernel, CPU regressions and device-gated NPU kernel tests, without monkey-patching the installed Liger package or changing CUDA dispatch. CPU checks pass; the exact new NPU test module is still pending Ascend execution, hence draft status. Because #2129 is not merged, its implementation is included as an explicitly identified dependency with original authorship, followed by the isolated correction commit.
longboat2010 commentedon Oct 29, 2026
New companion PR: #2240 — fla_npu >= 26.10 compatibility for the GDN AscendC stack (chunk loader + causal conv backends), stacked on #2120 and touching only its two GDN modules.
fla_npu's 26.10 line splits the API surface: stable raw ops move to
fla_npu.ops.ascendc.*, the legacytorch.ops.npu.*registrations need an explicitload_legacy_torch_ops()call, and some wheel variants ship no embedded legacy extension. The chunk loader now tolerates that (best-effort shim; the stable API path the chunk route uses is unaffected), and the causal-conv backend resolution becomes dual-generation (stable raw vs legacytorch.ops.npu.*) with per-backend calling conventions. The startup conv probe is upgraded from isfinite-only to a numericF.conv1dautograd check, so the "finite but wrong" stable conv adapter observed on a 26.10 dev wheel (all-ones input → all-zeros output) is rejected at startup and the unmasked-correction fallback engages, instead of surfacing as NaN loss mid-training.Validation: fla_npu 26.6.0 dispatcher-level conv fwd bit-exact (0.00000), dx ≤ 0.125, dw ≤ 1.0 (behavior on 26.6 unchanged); on 26.10.0+dev the GDN chunk route resolves via the stable API with a clean legacy skip and clean conv fallback. End-to-end (Qwen3.5 dense text, 256 Ascend NPUs = 16 nodes × 16, 8K seq, 10-step benchmark, mbz=1/gas=2/gbs=512):
[gdn_ascendc] GDN kernel: fla_npu AscendC+[l2norm] fla_npu triton, 10/10 steps, 0 NaN, benchmark success. Independent of #2119 / #2128 / #2129 / #2130 / #2131; depends on #2120 (files it introduces).longboat2010 commentedon Oct 29, 2026
#2129 follow-up: #2241 applies the v3 liger CE optimization set on top of it (stacked branch
feat/npu-flce-liger-ce-opt; merge #2129 first, then #2241 becomes a clean one-commit diff):torch.mm(out=)) — removes the largest per-chunk transient (~1.9 GiB at vocab 248k).XTUNER_LIGER_GRAD_WEIGHT_CHUNK, default 4096) — replaces the ~2 GiB[V, H]temporary; K-order per element unchanged.LOSS_CHUNK_SIZE(was hardcoded 4096).reduction='none'multi-chunk backward indexed the full[BT]grad_outputchunk-locally, so chunks > 0 read wrong rows (probe: old rel 21–5727 vs reference, fixed 0.00e+00).ce_loss.py's liger path usessum, so latent only.CELossConfig.z_loss_coefficient(default 0.0), fused CE+LSE² forward kernel; in liger mode the z-loss enters gradient + logging (lm_z_loss) but not the loss value.HAS_TAILsignature detection so the no-weight backward call works on both liger 0.8.1 and 0.8.5+ (0.8.5 made it a required constexpr).Validated 45/45 vs fp32 autograd references on NPU (chunked + saved-logits phases, confident-row cases, z-loss vs reference, wiring) plus a 16-NPU e2e A/B with the same implementation on an internal fork: step-1 loss bitwise identical,
max_memory−4.6 GB sustained, steady tgs +4.2% (trend).XTUNER_LIGER_GRAD_WEIGHT_CHUNKadded to the knob table above.