Skip to content

[WS1][CUDA][Qwen3-Next] TP4 GDN, convolution and D=256 attention mixers on real weights (RFC #428 stack 6/11) - #507

Open
fusheng-ji wants to merge 34 commits into
RL-Align:test-qwennextfrom
fusheng-ji:feat/ws1-qwen3-next-tp4-mixers
Open

fusheng-ji wants to merge 34 commits into
RL-Align:test-qwennextfrom
fusheng-ji:feat/ws1-qwen3-next-tp4-mixers

Conversation

@fusheng-ji

@fusheng-ji fusheng-ji commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #466 (merged) → #467 → #468 → #469 → #503 → this PR → #543 → #544 → #549 → #557 → benchmarks (#551), base test-qwennext; merges after #503. This PR's own diff. Prep for RFC #428 full_model_chain; serves the gdn_state_bridge row (CUDA).

What

Prior art & reuse decision

GDN: reused as surveyed in #469 (the decode step runs vLLM's own FLA kernels; RL-Kernel supplies the state boundary and the golden).

Attention (one TP4 rank: 4 query heads, 1 KV head, D = 256, causal), measured:

Library Implementation Batch BI Chunk (last 64) = prefill Decode (last 1) = prefill Backward, packed = alone Max abs vs FP64 Verdict
CUTLASS via FlashAttention-4 (CuTe DSL) and TE's cuDNN fused kernel, rows below covered
FlashAttention FA-4 (CuTe DSL, vendored by vLLM 0.30.0) yes yes yes yes ¹ 6.7e-3 BI; strongest follow-up candidate
FlashAttention FA-2 varlen (vLLM), num_splits=1 yes yes yes no backward 6.7e-3 BI, inference only
FlashAttention FA-2 varlen (vLLM), num_splits auto yes yes no no backward 6.7e-3 not BI
vLLM 0.30.0 Triton unified attention (2D) yes yes no no backward 6.7e-3 not BI
FlashInfer 0.6.18.post1 BatchPrefillWithRaggedKVCache no no no no backward 6.7e-3 not BI
SGLang 0.5.21 attention backends are FlashInfer, FlashAttention and Triton (rows above) covered
Miles / verl / Vime rollout through vLLM or SGLang, training through Megatron-core / TE or HF + SDPA / flash-attn (rows here); no attention kernel of their own covered
Transformer Engine 2.16.1 inference: cuDNN 9.20 fused yes yes yes no D = 256 backward on SM100 6.7e-3 BI, inference only
Transformer Engine 2.16.1 training: unfused fallback no error ² error ² no 1.3e-2 not BI
Megatron-core 0.16 local DotProductAttention yes ³ yes yes yes ³ 1.3e-2 BI only by one launch per sequence
PyTorch 2.13 SDPA (default backend) yes no no dq no 6.7e-3 not BI
RL-Kernel deterministic attention yes yes yes yes 6.7e-3

¹ Default backward; its deterministic=True backward asserts for head dim 256 on SM100. ² TE's unfused THD path raises a shape error when the query is shorter than the KV. ³ No packed-sequence path: one launch per sequence.

Decision: keep RL-Kernel's deterministic attention on both sides in this PR. It covers forward and backward with one documented reduction order and backs the WS1 attention contract; train and rollout must run the same kernel. FlashAttention-4 also passes every check here and is 30-500× faster at 2048-8192 tokens, so it is the follow-up for the slow forward. It is not adopted here because its backward has no deterministic mode at head dim 256 on SM100 (deterministic=True asserts), so the batch-invariant backward measured here is not guaranteed by its contract, and because adopting it means pinning vLLM's vendored CuTe build on both engines. TE fused is invariant for inference only; Megatron-core's local path only by launching per sequence.

Results (4 × B200, official checkpoint)

TP4 attention prior art

Latency (ms) RL-Kernel FA-4 FA-2 split=1 FA-2 auto Triton 2D TE fused TE unfused Megatron SDPA FlashInfer
prefill 512 / 2048 / 8192 0.73 / 10.1 / 160 0.21 / 0.21 / 0.31 0.12 / 0.16 / 0.66 0.12 / 0.15 / 0.66 0.26 / 0.32 / 1.13 0.34 / 0.32 / 0.61 2.0 / 5.5 / 22.7 0.32 / 0.32 / 3.25 0.07 / 0.12 / 0.61 0.24 / 0.29 / 0.76
decode, 8192 KV 0.76 0.26 0.34 0.16 0.56 0.44 4.5 0.30 0.31 0.24
forward + backward, 2048 25.0 0.91 – – – – 10.3 1.34 0.74 –
Gate Layer Cases (all bitwise, all 4 ranks)
GDN 0 HF load/export; full == chunked prefill (output, conv and recurrent state) at 8/64/256/1024 tokens; recompute == no-grad forward; gradient reaches the prompt through the state; replicated norm gradient — 7 / 7
attention 3 HF load/export with KV pair replicas; full == chunked prefill and KV continuation at 8-1024; variable-length batch of 4 == each alone, any order; 16-token decode == prefill rows; recompute; KV-pair and QK-norm gradient replicas — 10 / 10
MoE 0 as in #503 — 12 / 12

Summary, tested commits and commands: reports/experiments/qwen3_next/qwen3-next-tp4-mixers-b200.md; raw reports ZIP (SHA-256 0976d8e9…e8d4b). Gates measured at 9e6c4f2, attention prior art at 703c6dc.

Status

  • Head d849e180, on top of [WS1][CUDA][Qwen3-Next] MoE route/combine contract with a batch-invariant grouped expert GEMM (RFC #428 stack 5/11) #503 (9800e1a4). CI is green on this head: lint, unit-tests, docs, DCO and the bot review pass; provider and ws1-gtest skip because no self-hosted runner is attached.
  • Ready for review. No known acceptance failure.
  • Known: the attention forward is slow at long prefill. RL-Kernel's deterministic attention materialises the FP32 probability matrix: 10 ms at 2048 tokens and 160 ms at 8192, against 0.21 / 0.31 ms for FlashAttention-4. Moving to FlashAttention-4 is follow-up work (see the decision above).
  • Rerun after the refactor (f10a271): all three gates passed with every case value and digest identical; only MoE wall-clock timings differ.
  • Rerun after aligning the stack with the reviewed layout (clean clone of the stack top 90c09164, 4 × B200): the standalone GDN, attention and MoE gates passed on all four ranks (7, 10 and 12 cases), and the full-model L2 artifacts are bitwise identical to the pre-move chain.

History

Tests

python setup.py build_ext --inplace
python -m pytest -q tests/models/qwen3_next/test_qwen3_next_tp_gdn.py tests/models/qwen3_next/test_qwen3_next_tp_blocks.py tests/models/qwen3_next/test_qwen3_next_forward_contract.py
TRITON_F32_DEFAULT=ieee python -m pytest -q tests/models/qwen3_next/check_qwen3_next_{attention,gdn_bridge,gdn_sequence,conv_bridge,core_matrix,shared_core,forward}.py
torchrun --nproc-per-node 4 tools/validation/models/qwen3_next/qwen3_next_tp_gdn_check.py --checkpoint <ckpt> --output <dir>
torchrun --nproc-per-node 4 tools/validation/models/qwen3_next/qwen3_next_tp_attention_check.py --checkpoint <ckpt> --output <dir>
Check Result
Qwen3-Next CPU list in ci.yml at this branch 105 passed (99 CUDA-only skips)
layout check, pre-commit on all files clean
mixer GPU checks + CPU tests, one process (at 9e6c4f2) 123 passed
existing attention suites (D = 128) 766 passed; 2 failures in test_attention_dispatch, also on main
real-weight gates 29 / 29 cases, 4 ranks

The check_ files import vLLM and run in the Qwen3-Next-provider-GPU workflow.

Scope

CUDA, TP4. ROCm keeps D = 128. No engine adapter or model-level claim; those are full_model_chain (#543).

Results (H100 / Hopper, 4 GPUs, official checkpoint)

Run at 0fdbf80b, this PR's head on 2026-10-19 (it is an ancestor of the current head d849e180). The raw files record ed885ed0, the id of this commit before the branch was rewritten; both ids have the tree a1f26d9003bfd8bd71f5245b49b5b81bb9d1556e (git rev-parse 0fdbf80b^{tree}). Not rerun at d849e180: since 0fdbf80b the branch changed the RMSNorm sources (csrc/cuda/norm/rmsnorm.cu, csrc/bindings/ops.cpp, rl_engine/backends/cuda/norm/rmsnorm.py), rl_engine/config/workload.py, the norm and conv references, the workload validation and tests, but none of the three TP4 modules these gates run. 4 × NVIDIA H100 80GB, torch 2.13.0+cu130, vLLM 0.30.0, NCCL 2.29.7, checkpoint revision 9c7f2fbe. Raw data: hopper-qwen3-next/pr507.

Check Result
tests/models/qwen3_next/test_*.py + check_*.py (4 GPUs) 325 passed
TP4 attention gate, TP4 GDN gate, TP4 MoE gate (4 ranks, real weights) all passed

D=256 attention prior art on H100

Attention prior art (head dim 256, one launch per sequence; runner of #551 at 58aa50c7, which imports RL-Kernel's attention from that tree; the raw files record 77d59a59, the id of this commit before the branch was rewritten, and both ids have the tree 67a6c5c8a30c1351b05beb9b9f509976f8dff45c):

Implementation Batch invariant Backward dq / dk / dv batch invariant Prefill 512 / 2048 / 8192 (µs)
RL-Kernel deterministic attention yes (prefill/decode too) yes / yes / yes 825 / 11275 / 176067
torch SDPA no (prefill vs decode) no / yes / yes 109 / 168 / 865
vLLM FlashAttention-2, num_splits=0 no – 215 / 286 / 926
vLLM FlashAttention-2, num_splits=1 yes – 215 / 229 / 798
vLLM Triton unified (2D) yes – 412 / 527 / 1949
FlashInfer ragged prefill yes – 364 / 420 / 793
FlashAttention-4 (CuTe) no no / no / no 328 / 350 / 547
Transformer Engine fused (inference) yes – 702 / 784 / 1206
Transformer Engine, training fallback no no / yes / yes 665 / 676 / 978
Megatron-core local yes yes / yes / yes 582 / 633 / 5207

All implementations match the FP64 reference to the same max abs error (6.7e-3; Megatron local 1.3e-2). RL-Kernel's attention is the slowest of the ten at all three lengths on H100: it buys invariance with one launch per sequence.

…rouped expert GEMM

RFC RL-Align#428 moe_route_combine_contract (CUDA).

- Routing: FP32 router through the pinned vLLM batch-invariant GEMM, a
  fixed-order softmax, stable descending argsort (ties go to the lower
  expert id) and FP32 renormalisation of the ten selected weights.
- Experts: per-route gate_up and down projections through vLLM 0.30.0's
  fused_moe Triton kernel with the batch-invariant tile passed explicitly
  (BLOCK 64/64/32, SPLIT_K=1); no routed-weight multiply and no sum inside
  the kernel, so every (token, route) row has one writer. Bitwise equal to
  the previous per-expert GEMM loop at TP4 and TP1 widths; fails closed if
  VLLM_TRITON_USE_TD is enabled.
- Combine: ten ordered FP32 multiply-adds and one BF16 cast; no atomics.
- Backward: per-expert pinned GEMMs into one dense gradient buffer; dx is
  summed in ascending expert order.
- TP4 block: HF load/export for all 512 experts, replicated router and
  shared-expert gate, Megatron-style copy/reduce autograd boundaries moved
  into qwen3_next_tp.py.
- Prior-art runner and plot comparing HF transformers, vLLM fused_moe
  (BI=0/1) and FlashInfer cutlass_fused_moe; TP4 real-weight gate script.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Prep for RFC RL-Align#428 full_model_chain; serves the gdn_state_bridge row.

- GDN: packed recurrent prefill/decode through the pinned vLLM 0.30.0
  provider with an explicit state boundary (qwen3_next_gdn.py), the
  depthwise causal convolution with cache handoff (qwen3_next_conv.py),
  their combined shared core (qwen3_next_provider.py) and the TP4 block
  with HF GQA-interleaved weight mapping (qwen3_next_tp_gdn.py), now on the
  shared TP boundaries in qwen3_next_tp.py.
- Attention: the deterministic CUDA attention kernel accepts head dim 256
  (ROCm stays 128) and validates backward inputs; shared_attention and
  TP4FullAttention with the KV head replicated on each rank pair, partial
  RoPE at absolute positions and explicit per-slot KV state.
- Real-weight TP4 gates for GDN (layer 0) and attention (layer 3), sharing
  scripts/qwen3_next_gate_common.py with the MoE gate.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
…nce (B200, 2e950f3)

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Compares RL-Kernel's deterministic attention with torch SDPA, vLLM FlashAttention-2 (num_splits auto and 1), vLLM's Triton unified attention and FlashInfer's ragged prefill: batch, chunked-prefill and decode invariance, backward repeatability, accuracy against FP64 and latency.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
…nce (B200, 9e6c4f2/6517077)

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
…r-art runner

- Megatron-core 0.16 MoELayer with Transformer Engine grouped GEMM and fused
  permute, configured as VIME runs Qwen3-Next (FP32 softmax router, top-10,
  all-to-all dispatcher), including its backward.
- SGLang 0.5.21 Triton fused_moe in default and deterministic-inference mode.
  sgl_kernel cannot load next to torch 2.13, so its two kernels on this path
  are replaced by SGLang's own Triton moe_sum_reduce and JIT
  moe_align_block_size; any other sgl_kernel call fails loudly.
- --only and --merge combine reports from different Python environments.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
…ured (B200, c58816b)

Every library in the reuse table is now measured: Megatron-core 0.16 + TE 2.16 as VIME configures Qwen3-Next, and SGLang 0.5.21 in default and deterministic-inference mode. The TP4 gate evidence is unchanged (2e950f3, identical MoE code).

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
…gatron-core attention

Add three engines to the attention prior-art runner:
- FlashAttention-4 (CuTe DSL) varlen as vendored by vLLM, forward and backward.
- Transformer Engine DotProductAttention on THD-packed sequences, in
  inference mode (cuDNN fused) and in training mode (on SM100 TE has no
  fused head-dim-256 backward and falls back to its unfused path).
- Megatron-core's local DotProductAttention, one launch per sequence.

The backward check now compares the target's dq/dk/dv alone against the
same sequence packed first and last, so engines with a packed backward are
checked for batch invariance rather than repeatability. A prefill/decode
crash is recorded per candidate without skipping its other checks.
--only and --merge combine reports from different Python environments.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
The decode and forward+backward latency sections are keyed by candidate
directly, the prefill section by size first; merge them at any depth.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3715b7d7-b130-4267-89ef-75748f361f3d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
…p as E402-exempt

The gate scripts insert the repo root into sys.path before importing
rl_engine and tools, which flake8 reports as E402. Mark those imports
with noqa, as the other tools/ scripts do.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
…p as E402-exempt

The gate scripts insert the repo root into sys.path before importing
rl_engine and tools, which flake8 reports as E402. Mark those imports
with noqa, as the other tools/ scripts do.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Annotate the latency samples and accept the default-argument lambdas used as
timed calls. No behaviour change.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
… runner

Annotate the latency samples and accept the default-argument lambdas used as
timed calls. No behaviour change.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Brings in RL-Align#468's caedea5 and the provider path-filter changes of RL-Align#469 and RL-Align#503.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
From this PR the provider modules reach compiled code: the shared GDN core
uses the gated RMSNorm kernel (csrc/cuda/norm) and the D=256 attention
kernels are bound in csrc/bindings/ops.cpp; deterministic_attn imports
rl_engine/contracts. rl_engine/backends/** only covers the Python wrappers,
so these paths are added.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
fusheng-ji added a commit to fusheng-ji/RL-Kernel that referenced this pull request Oct 26, 2026
Brings in RL-Align#468's caedea5 and the provider path-filter changes of RL-Align#469, RL-Align#503 and RL-Align#507.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
echo "source_sha=${{ github.event.pull_request.head.sha }}"

provider:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · critical
pull_request trigger + this if means any same-repo branch (e.g. an unreviewed collaborator branch) executes PR-controlled code (setup.py build_ext, tools/checks/ci_smoke.py, the check_*.py modules) directly on the trusted self-hosted runner rl-kernel-qwen3-next via ref: ... head.sha. The header comment states "Fork PRs never reach the self-hosted runner; a maintainer dispatches the reviewed commit", but a same-repo PR bypasses maintainer dispatch entirely and still gets persistent runner filesystem state (which the cleanup step below exists to work around). This is the exact privileged-execution pattern the workflow claims to avoid. Either drop the pull_request trigger and keep only workflow_dispatch, or gate the job on github.event.pull_request.head.repo.full_name == github.repository && contains(fromJSON('["maintainer","member"]'), github.event.pull_request.author_association).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as r4372884187, r4372907792 and 549-I4381852969; not changed. Pushing a branch to this repository already requires write access, the job runs with contents: read and no pull_request_target, and fork PRs never reach the runner. This is the trust model ws1-chain-npu.yml uses; an approval environment or a dispatch-only trigger is a maintainer policy choice for both workflows.

Comment on lines +132 to +138
set -o pipefail
log="$RUNNER_TEMP/provider-pytest.log"
python3 -m pytest -q -rs tests/models/qwen3_next/check_qwen3_next_norm_providers.py tests/models/qwen3_next/check_gdn_recurrent_golden.py \
tests/models/qwen3_next/check_qwen3_next_forward.py tests/models/qwen3_next/check_qwen3_next_attention.py \
tests/models/qwen3_next/check_qwen3_next_gdn_bridge.py tests/models/qwen3_next/check_qwen3_next_gdn_sequence.py \
tests/models/qwen3_next/check_qwen3_next_conv_bridge.py tests/models/qwen3_next/check_qwen3_next_core_matrix.py \
tests/models/qwen3_next/check_qwen3_next_shared_core.py | tee "$log"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
set -o pipefail is set inside this script but the step itself is an implicit bash -e, not bash -eo pipefail: pipefail here only affects pipelines within the step, and the python3 -m pytest ... | tee "$log" pipeline's exit status is discarded because the step does not exit on it. The next commands are if grep ... (whose status is consumed by the if) and a final grep -Eq '[0-9]+ passed', so a non-zero pytest exit (e.g. an ImportError or a partially-failing module) is masked as long as some module printed "N passed". With -q, a partial run of 8 modules that fails the 9th still prints "X passed, 1 failed", which the final grep matches. Capture the pytest status explicitly and propagate it, e.g. pytest ... | tee "$log"; status=${PIPESTATUS[0]}; [ "$status" -eq 0 ] || exit "$status", and make the final grep reject failed/error lines too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: set -o pipefail runs in the same shell before the pytest | tee pipeline, so a failing pytest gives the pipeline a non-zero status and bash -e stops the step there, before either grep (bash -e -c 'set -o pipefail; false | cat; echo reached' exits 1 without printing). Same point as 557-I4381857006.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · .github/workflows/qwen3-next-provider-gpu.yml (L27-L27): The path filter omits csrc/cuda/norm/**, but this workflow gates the C3/C4 norm providers (qwen3_next_rms_norm / rms_norm_gated) whose CUDA kernels live in csrc/cuda/norm/rmsnorm.cu.

Fixed in b9390f3: the path filter now includes csrc/cuda/norm/**, csrc/bindings/** and rl_engine/contracts/**. From this PR the shared GDN core uses the gated RMSNorm kernel and the D=256 attention kernels, so a change to their CUDA sources or bindings must run the provider job; rl_engine/backends/** covered only the Python wrappers. #557 adds csrc/cuda/logprob/** and the vLLM rollout bridge for the same reason (6dc9d41).

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · csrc/cuda/norm/rmsnorm.cu (L510-L518): rmsnorm_gated_bwd_dx_kernel reduces dot = sum_col dy*(w*act(gate))*x and forms coeff = dot * r^3 / H, i.e.

Agreed, and nothing to change: both loops call the same gate_activation<ACT>, so the forward and the dx kernel take the same expf path. The comment above gate_activation records why expf is used instead of __expf.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · rl_engine/backends/cuda/norm/rmsnorm.py (L403-L407): parameter_vjp_contributions_fp32 returns per-row contributions g * x * rstd where g = dy * act(gate), giving dw = sum_t dy_t * x_t * rstd_t * act(gate_t).

Agreed, no defect: the contributions use the same dy * act(gate) * x * rstd product as the autograd backward, with gate and x widened to fp32 inside the helpers. test_cuda_parameter_contributions_reproduce_backward checks the two agree.

Brings in RL-Align#468's a203719: the norm manifest's top-level keys, chain semantics and duplicate-op checks, the gate dtype message and the new norm tests.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
Brings in RL-Align#468's a203719: the norm manifest's top-level keys, chain semantics and duplicate-op checks, the gate dtype message and the new norm tests.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
fusheng-ji added a commit to fusheng-ji/RL-Kernel that referenced this pull request Oct 26, 2026
Brings in RL-Align#468's a203719: the norm manifest's top-level keys, chain semantics and duplicate-op checks, the gate dtype message and the new norm tests.

Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com>
@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · rl_engine/backends/cuda/norm/rmsnorm.py (L404-L409): The gated parameter_vjp_contributions_fp32 returns raw per-row contributions without the fp32 left fold, whereas RMSNormGatedCuda.backward returns _fold_dweight_rows(rows, weight.dtype).

Not changed. The hook returns per-row contributions on purpose: the C4 harness keys them per logical token, checks the row count, and folds them in fp32 in sorted key order itself, which is the property C4 tests; a pre-folded [H] would fail that shape check. The plain op does the same. The rows are elementwise fp32 products, so the layout of grad_output does not change them. test_cuda_gated_parameter_vjp_contributions_match_the_fold checks that folding the rows gives autograd's dweight bit for bit, now also for N-D x (in a203719 on #468, merged here in d849e18).

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · rl_engine/backends/cuda/norm/rmsnorm.py (L11-L16): The docstring claims "both the plain and the gated backward route through here", but the gated path has two dweight producers: RMSNormGatedCuda.backward (routes here) and Qwen3NextRMSNormGatedCudaOp.parameter_vjp_contributions_fp32 (returns raw rows, bypassing the fold).

Not changed: _fold_dweight_rows is the single fold for both backwards, as the docstring says. The parameter-VJP hooks return unfolded rows, which the C4 harness folds with the same fixed-order fp32 rule; test_cuda_gated_parameter_vjp_contributions_match_the_fold checks the two agree bit for bit.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · csrc/cuda/norm/rmsnorm.cu (L473-L473): The gated wrappers dispatch on weight.scalar_type() for w_t, but the binding only checks weight.dtype == x.dtype || weight.dtype == kFloat32.

Same point as 468-S2, 549-S2; not changed. weight must have x's dtype or float32 (rmsnorm_check_weight), so no other weight dtype reaches the dispatch, and rmsnorm_gated_check checks weight for CUDA and contiguity on the raw backward binding too.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · tests/validation/operators/test_ws1_gtest_gpu.py (L38-L40): rms_norm_gated and qwen3_next_rms_norm are only asserted to be present in operator_names(), but test_check_operator_runs_ported_ops's parametrization below is not extended with them and no other C3/C4 test in this file runs them.

Not changed. This file is not the norms' gate: ci/scripts/run_ws1_gtest.sh (run under set -euo pipefail by the GPU CI) runs C3 and C4 for both ops on real candidates, and the same script runs tests/models/qwen3_next/test_qwen3_next_norm.py, which has the CUDA tests. They are not added to test_check_operator_runs_ported_ops because the norm gates need the norm manifest and the checkpoint widths.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · rl_engine/config/workload.py (L267-L268): raw.get("scope", ...) does not treat an explicit "scope": null as the default scope, whereas workload_report uses the same expression (so it will additionally report "scope": None and then load_manifest will raise unknown manifest scope None).

Not changed: an explicit "scope": null matches neither branch and raises WorkloadError("unknown manifest scope None"), so it fails closed, and workload_report only sees manifests that load_manifest accepted (r4380173239). The case is now part of test_unknown_manifest_scope_is_rejected (in a203719 on #468, merged here in d849e18).

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · rl_engine/runtime/registry.py (L673-L673): The qwen3_next_rms_norm operator gives the NPU device only the PyTorch-native reference implementation, while other devices (CUDA and the generic/CPU maps) have a dedicated CUDA/optimised kernel.

Not changed; the premise is off. Line 673 is in the ROCm map, and the only dedicated kernel for this op is CUDA: ROCm, MUSA, CPU and NPU all list just the PyTorch reference, on purpose, so the lookup cannot fall through to the generic PYTORCH_NATIVE entry (which is the logprob op). tests/runtime/test_dispatch.py pins this for every device.

Comment on lines +72 to +74
provider:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: [self-hosted, linux, x64, rl-kernel-qwen3-next]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
The pull_request trigger can reach the self-hosted runner for any same-repo branch, including branches pushed by less-trusted collaborators, and then builds and executes that branch's setup.py/tests (python3 setup.py build_ext --inplace, arbitrary test imports) on a long-lived privileged runner. The header comment claims "a maintainer dispatches the reviewed commit from a trusted branch", but that only holds for workflow_dispatch; the pull_request path bypasses the reviewed-SHA gate entirely (ws1-chain-npu.yml has the same shape, but this new runner is explicitly described as audited and provides no additional protection). Either drop the pull_request trigger, or restrict it to the trusted base (e.g. require github.event.pull_request.head.ref == 'main' / an environment approval / pull_request_target-free label gating) so unreviewed PR code cannot execute here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as 549-I4381852969, 507-I4383081716 and r4372884187; not changed. Pushing a branch here already requires write access, the job runs with contents: read and no pull_request_target, and fork PRs never reach the runner; this is the trust model of ws1-chain-npu.yml.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · csrc/cuda/attention/deterministic_attention.cu (L578-L579): check_deterministic_attention_inputs now restricts D to {128, 256} on CUDA and 128 on ROCm.

Not changed. Forward and backward call the same check_deterministic_attention_inputs, so anything the forward accepted passes the backward check; ROCm is limited to D=128 in both, as is the Python wrapper's _HEAD_DIMS.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · .github/workflows/qwen3-next-provider-gpu.yml (L141-L145): The skip detection is brittle: grep -Eq '^SKIPPED' does not match pytest's -rs skip summary lines (they are indented, e.g. SKIPPED [1] file:line: reason), while the [0-9]+ skipped alternative in the final summary does work.

Same point as 544-S2, 503-S2; not changed. With -rs, pytest prints a SKIPPED [n] ... line at column 0 in the short summary and M skipped in the final line at any verbosity, so any skip fails the step before the passed check; collection and import errors exit non-zero and fail it through pipefail.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

OpenCodeReview · ci/scripts/run_ws1_gtest.sh (L86-L87): For qwen3_next_rms_norm the runner width is hidden (_row_parameters/_row_inputs build the weight and x as (hidden,)), so --hidden 2048 is correct here.

Same point as 468-I4383081337, 543-S6, 503-S3; not changed. Both gate tools take the pair --hidden 2048 --head-dim 128: the zero-centred norm is built at --hidden and the gated norm at --head-dim, and the expected pair comes from NORM_OPS, so a drifted width raises WorkloadError.

@hongleng

hongleng commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Hi, I'm working on Task 20 of RFC #642.

Following the maintainer's guidance, I'll first extract the reusable GDN/causal-convolution references from #469 into a small PR targeting main, then build the shared numerical contract and CPU sequence/chunk references on top.

I noticed #507 already includes packed sequence execution and state handling. I'll keep the shared CPU reference independent of the CUDA/vLLM provider and avoid duplicating that integration work.

Please let me know if there are any interfaces or semantics I should preserve to keep the two efforts compatible.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

Hi @hongleng, thanks for checking before building. Keeping the CPU reference independent of the CUDA/vLLM provider is the right split. Here are the contracts in #507 (head d849e180) that the downstream PRs (#543, #544, #549, #557) rely on. If your reference matches them, it can be used as a golden for #507 without changes on either side.

Packed sequences (qwen3_next_gdn.py, qwen3_next_conv.py)

  • cu_seqlens is a CPU int32/int64 vector. It starts at 0, is non-decreasing, and ends at the total packed token count. Empty sequences are allowed.
  • indices[B] maps each sequence to a distinct cache slot. Slot 0 is reserved by the vLLM decode provider, so indices must be positive.
  • Inputs are packed as [T, D], with no padding between sequences.

Recurrent state (packed_recurrent_sequence, packed_decode_training_step)

  • State is an FP32 bank [blocks, HV, 128, 128]. Heads are 128 wide, and HV % num_k_heads == 0.
  • The packed qkv width is 2 * num_k_heads * 128 + HV * 128, in BF16.
  • a and b are [T, HV] in BF16. A_log and dt_bias are [HV] in FP32.
  • The default scale is 128 ** -0.5.
  • The whole state bank is returned. Chunk continuation and sequence reordering go through it explicitly, and the state is passed on without detaching so the gradient reaches the prompt.

Causal conv (causal_conv_sequence)

  • Width 4, no bias, SiLU. Weight is BF16 [dim, 4].
  • The cache is BF16 [blocks, dim, 3]. It is never mutated. The call returns the output and an independent new cache.

Properties the gates check, bitwise on all 4 TP ranks

  • Full prefill equals chunked prefill, for the output, the conv state and the recurrent state (8 to 1024 tokens).
  • A variable-length batch gives the same result as each sequence alone, in any order.
  • Recompute equals the no-grad forward.

Notes on scope

  • packed_recurrent_sequence is an intentionally slow token-by-token reference over the decode provider. It is not equivalence evidence for the upstream chunk-prefill kernel, so a chunked CPU reference that checks it is useful.

Questions for you: which dtype and state layout will your shared numerical contract use? If it uses the same [HV, 128, 128] FP32 layout, the two can be compared directly.

@hongleng

hongleng commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Thanks for the detailed contracts! This is very helpful.

I plan to define the shared recurrent-state layout as [blocks, HV, V, K], with configurable dimensions and explicit dtype/cast semantics. The #507-compatible profile will use FP32 state with V = K = 128, so we can compare directly.

I'll keep the CPU sequence/chunk reference independent of the CUDA/vLLM provider and use your packed-sequence and state-continuation conventions for the #507 compatibility tests.

I'll also make sure provider-specific arithmetic is explicit rather than assuming that vLLM agreement implies compatibility with miles.

Thanks for coordinating!

weight.contiguous(),
activation="silu",
conv_state_indices=indices[active].to(torch.int32).contiguous(),
out=torch.empty_like(packed),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
The pinned causal_conv1d_update is called with an out= keyword and a stacked (num_seqs, dim, seq_len) multi-token tensor. Neither the out= keyword nor a grouped multi-length prefill layout is part of the documented single-token/decode signature of the update kernel. If the pinned signature lacks out, this raises TypeError on the very first call (including any prefill chunk of length > 1); if it accepts out but only fills seq_len == 1, longer chunks silently drop positions and leave new_state partially updated. Verify the provider signature and restrict the call to the supported shape/kwargs (e.g. loop over positions for multi-token groups, or use the documented out contract).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: the pinned provider documents both. In vLLM 0.30.0 causal_conv1d_update is (x, conv_state, weight, bias=None, activation=None, conv_state_indices=None, ..., out=None) and its docstring says x may be [batch, dim, seqlen] ("single or multiple tokens prediction") and out is an "optional output tensor with the same shape as x" (vllm/model_executor/layers/mamba/ops/causal_conv1d.py:1096-1118, 1148-1149). The kernel loops over seqlen and writes every token (for idx_token in tl.range(seqlen), line 999), and shifts the cache once per call, so a chunk longer than 1 does not drop positions.

The bridge calls it with equal-length groups only (no padding, no speculative cache), and uses the returned tensor, which is out itself. The bridge test covers the case you describe: check_qwen3_next_conv_bridge.py::test_varlen_matches_repeated_decode_and_preserves_inputs runs lengths (64, 8, 0, 16) and (1024, 256, 64, 8) through the grouped call and compares output and final cache bitwise against single-token provider calls made one at a time (lines 31-53); test_chunked_and_reordered_sequences_match checks chunk continuation. At 1fef29b0: 6 passed, 0 skipped on RTX PRO 6000 Blackwell (sm_120), vLLM 0.30.0.

Comment on lines +74 to +75
new_state,
out,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
_PackedDecode.forward allocates out as zeros, passes it into the provider, discards the provider's return value, and returns the preallocated out. This silently assumes the pinned fused_recurrent_gated_delta_rule_packed_decode writes into out in place (and that out is the correct positional argument). If the provider instead allocates/returns a fresh tensor (or returns a (out, state) tuple), the returned out stays all zeros, so the whole conv→recurrence→norm chain downstream produces zero output and zero/incorrect gradients with no error. The CPU-only bridge tests cannot catch this because they do not execute the GPU provider. Capture and use the provider's return value (falling back to out only when it writes in place), and assert/verify non-placeholder output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: the pinned provider writes out in place and its return value is the same objects. In vLLM 0.30.0 fused_recurrent_gated_delta_rule_packed_decode(mixed_qkv, a, b, A_log, dt_bias, scale, initial_state, out, ssm_state_indices, use_qk_l2norm_in_kernel=False) takes initial_state and out as its 7th and 8th arguments, which is the bridge's order. It passes o=out, h0=initial_state, ht=initial_state to the Triton kernel and ends with return out, initial_state (vllm/third_party/flash_linear_attention/ops/fused_recurrent.py:343-353, 462-464, 485). The kernel stores zeros for state index <= 0 and the result otherwise, so every element of out is written. vLLM's own decode path calls it the same way, with a preallocated out= and the return value ignored. The bridge only runs when importlib.metadata.version("vllm") == "0.30.0".

Measured on 1fef29b0 (RTX PRO 6000 Blackwell, sm_120, vLLM 0.30.0, Triton 3.7.1): the returned out and state are the same tensors that were passed in (is and data_ptr); an out pre-filled with NaN is fully overwritten; the bridge output agrees with an independent PyTorch reference to 3e-5 (16 key heads, 32 value heads, slots [1, 0, 3, -1, 2]), and packed sequences of length 0-7 agree with a token-by-token loop to 1e-6. The GPU bridge checks are 21 passed.

You are right that the CPU-only tests never reach the provider; the checks that do are the GPU files run by qwen3-next-provider-gpu.yml, which is skipped for fork PRs and needs a maintainer dispatch.

@fusheng-ji

Copy link
Copy Markdown
Contributor Author

Thanks, [blocks, HV, V, K] with FP32 and V = K = 128 is exactly the #507 layout. I checked the index order, not just the shape: the pinned packed-decode kernel addresses state as state[block, hv, v, k] (fused_recurrent.py:306), and the merged #469 golden documents the same [num_blocks, HV, V, K] ABI. One thing to watch if you also compare with Hugging Face: transformers' torch fallback keeps the state as [batch, num_v_heads, k_head_dim, v_head_dim], K-major, so it is the transpose of the vLLM layout, and the two need an explicit transpose before a bitwise comparison.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-model platform: cuda Specific optimizations or bugs in NVIDIA graphics cards (such as FlashInfer, TMA optimizations) qwen WS1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants