From 22f2ac378013592d667f173d3121cb3a5386e12c Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:25:21 +0000 Subject: [PATCH 01/44] fix(registry): dispatch rms_norm to the CUDA backend on CUDA `test_registry_dispatches_rms_norm` asserts that the registry resolves `rms_norm` to `RMSNormCudaOp` whenever CUDA and the compiled kernels are both present, but `OpBackend` had no CUDA member for this operator and the CUDA priority list contained only `PYTORCH_NATIVE_RMS_NORM`, so the assert could never hold. The test therefore fails on any CUDA machine that builds the native extension, and only passes when `_HAS_CUDA_RMSNORM` is false -- which is why an unbuilt CI has not caught it. `RMSNormCudaOp` is already a first-class backend elsewhere: it is the `"cuda"` candidate in `gtest/operator_specs.py` and is used directly by `attention_preprocess.py`. Only the registry was missing it. Add `OpBackend.CUDA_RMS_NORM` and put it ahead of the PyTorch reference in the CUDA priority list. Because `_load_backend` only catches import errors and this module imports cleanly without `_C`, a CUDA-first list would otherwise hand out an op that raises at call time on an unbuilt install; so `RMSNormCudaOp.__init__` now validates the extension and its three symbols, matching `_require_cuda_activation` in the activation ops. The registry already treats a backend whose construction raises as unavailable, so the list degrades to `NativeRMSNormOp` as before. Only the `cuda` priority map changes; rocm/musa/cpu/npu are untouched. Verified on 2x B200 (sm_100, driver 580.126.20, torch 2.13.0+cu130) with the extension rebuilt from source: pytest tests/test_rms_norm.py -q pytest tests/ rl_engine/tests/ -q -p no:randomly \ --ignore=tests/test_rocm_aiter_api_contract.py tests/test_rms_norm.py passes, including test_registry_dispatches_rms_norm, which fails on the merge-base. The full suite gains no failure. The ignored file fails to import on the merge-base as well. Absolute suite counts are reported in the PR description against a named base commit, not here: they shift whenever a sibling test is added, so a count frozen in a commit message goes stale the moment the branch is rebased. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 27 +++++++++++++++++ rl_engine/kernels/registry.py | 6 +++- tests/test_rms_norm.py | 35 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index 773325c5d..f7af2a8bc 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -5,6 +5,25 @@ from rl_engine.kernels.ops.vjp_fp32 import reduce_rows_fp32, rmsnorm_dweight_rows_fp32 +def _require_cuda_symbols(what: str, *names: str) -> None: + """Raise when the compiled kernels backing ``what`` are missing. + + The registry treats a backend whose construction raises as unavailable and + falls through to the next candidate, so calling this from ``__init__`` is + what lets a CUDA-first priority list degrade to the PyTorch reference on a + build without the extension. Mirrors ``_require_cuda_activation`` in the + activation ops. + """ + if not _EXT_AVAILABLE or _C is None: + raise RuntimeError(f"{what} requires the compiled rl_engine._C extension.") + missing = [name for name in names if not hasattr(_C, name)] + if missing: + raise RuntimeError( + f"{what} symbols ({', '.join(missing)}) are not compiled into _C. " + "Rebuild the extension with csrc/cuda/rmsnorm.cu." + ) + + class RMSNormCuda(torch.autograd.Function): """ PyTorch autograd wrapper for CUDA RMSNorm. @@ -99,6 +118,14 @@ class RMSNormCudaOp: backward_impl = "cuda_rmsnorm_dx_declared_fp32_rowfold_dw" + def __init__(self) -> None: + _require_cuda_symbols( + "CUDA RMSNorm", + "rmsnorm_forward", + "rmsnorm_backward_dx", + "rmsnorm_backward_dw", + ) + def __call__(self, x, weight, *, eps=1e-6): return self.forward(x, weight, eps=eps) diff --git a/rl_engine/kernels/registry.py b/rl_engine/kernels/registry.py index 9eeb1c42b..3b935499a 100644 --- a/rl_engine/kernels/registry.py +++ b/rl_engine/kernels/registry.py @@ -154,6 +154,7 @@ class OpBackend(Enum, metaclass=_KernelEnumMeta): ) # RMSNorm(pre-norm / QK-Norm) - pure Pytorch reference(ws1 ground-truth) + CUDA_RMS_NORM = "rl_engine.kernels.ops.cuda.norm.rmsnorm.RMSNormCudaOp" TRITON_RMS_NORM = "rl_engine.kernels.ops.triton.rmsnorm_triton.RMSNormTritonOp" PYTORCH_NATIVE_RMS_NORM = "rl_engine.kernels.ops.pytorch.norm.rms_norm.NativeRMSNormOp" @@ -589,7 +590,10 @@ def __init__(self): OpBackend.TRITON_BATCH_INVARIANT_LOGP, OpBackend.PYTORCH_BATCH_INVARIANT_LOGP, ], - "rms_norm": [OpBackend.PYTORCH_NATIVE_RMS_NORM], + "rms_norm": [ + OpBackend.CUDA_RMS_NORM, + OpBackend.PYTORCH_NATIVE_RMS_NORM, + ], "lm_head": [OpBackend.PYTORCH_NATIVE_LM_HEAD], "embedding": [OpBackend.PYTORCH_NATIVE_EMBEDDING], "silu": [ diff --git a/tests/test_rms_norm.py b/tests/test_rms_norm.py index e8dcaa0a2..cfc12ab68 100644 --- a/tests/test_rms_norm.py +++ b/tests/test_rms_norm.py @@ -247,6 +247,41 @@ def test_backward_batch_invariance_slice(): assert torch.equal(x_slice.grad, grad_x_full_sliced) +# 9b. The CUDA backend must report itself unavailable by failing construction, +# which is the seam the registry uses to fall back (see _get_or_create_backend). +def test_cuda_op_construction_fails_without_extension(monkeypatch): + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", False) + monkeypatch.setattr(cuda_rmsnorm, "_C", None) + with pytest.raises(RuntimeError, match="requires the compiled rl_engine._C extension"): + cuda_rmsnorm.RMSNormCudaOp() + + +def test_cuda_op_construction_fails_when_symbols_missing(monkeypatch): + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + + class _WithoutRMSNorm: # a built extension that lacks the rmsnorm symbols + pass + + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", True) + monkeypatch.setattr(cuda_rmsnorm, "_C", _WithoutRMSNorm()) + with pytest.raises(RuntimeError, match="are not compiled into _C"): + cuda_rmsnorm.RMSNormCudaOp() + + +@requires_cuda +def test_registry_falls_back_to_native_without_extension(monkeypatch): + """A CUDA-first priority list must still resolve on a build without _C.""" + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + from rl_engine.kernels.registry import KernelRegistry + + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", False) + monkeypatch.setattr(cuda_rmsnorm, "_C", None) + # A fresh registry, so the cached instance from other tests is not reused. + assert isinstance(KernelRegistry().get_op("rms_norm"), NativeRMSNormOp) + + # 10. Registry dispatch resolves to the hardware op when available def test_registry_dispatches_rms_norm(): from rl_engine.kernels.registry import kernel_registry From ba9b89e34408671b66a4e10a9e9ced570373358e Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:06:44 +0000 Subject: [PATCH 02/44] fix(norm): validate only used CUDA symbols and exercise explicit fallback Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 1 - tests/test_rms_norm.py | 34 ++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index f7af2a8bc..21be8dfb8 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -123,7 +123,6 @@ def __init__(self) -> None: "CUDA RMSNorm", "rmsnorm_forward", "rmsnorm_backward_dx", - "rmsnorm_backward_dw", ) def __call__(self, x, weight, *, eps=1e-6): diff --git a/tests/test_rms_norm.py b/tests/test_rms_norm.py index cfc12ab68..0b2a6178f 100644 --- a/tests/test_rms_norm.py +++ b/tests/test_rms_norm.py @@ -270,7 +270,6 @@ class _WithoutRMSNorm: # a built extension that lacks the rmsnorm symbols cuda_rmsnorm.RMSNormCudaOp() -@requires_cuda def test_registry_falls_back_to_native_without_extension(monkeypatch): """A CUDA-first priority list must still resolve on a build without _C.""" from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm @@ -279,7 +278,38 @@ def test_registry_falls_back_to_native_without_extension(monkeypatch): monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", False) monkeypatch.setattr(cuda_rmsnorm, "_C", None) # A fresh registry, so the cached instance from other tests is not reused. - assert isinstance(KernelRegistry().get_op("rms_norm"), NativeRMSNormOp) + assert isinstance(KernelRegistry().get_op("rms_norm", device="cuda"), NativeRMSNormOp) + + +@pytest.mark.parametrize("missing", ["rmsnorm_forward", "rmsnorm_backward_dx"]) +def test_registry_falls_back_when_required_symbol_is_missing(monkeypatch, missing): + from types import SimpleNamespace + + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + from rl_engine.kernels.registry import KernelRegistry + + symbols = {name: object() for name in ("rmsnorm_forward", "rmsnorm_backward_dx")} + del symbols[missing] + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", True) + monkeypatch.setattr(cuda_rmsnorm, "_C", SimpleNamespace(**symbols)) + assert isinstance(KernelRegistry().get_op("rms_norm", device="cuda"), NativeRMSNormOp) + + +def test_registry_cuda_requires_only_used_symbols_and_cpu_stays_native(monkeypatch): + from types import SimpleNamespace + + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + from rl_engine.kernels.registry import KernelRegistry + + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", True) + monkeypatch.setattr( + cuda_rmsnorm, + "_C", + SimpleNamespace(rmsnorm_forward=object(), rmsnorm_backward_dx=object()), + ) + registry = KernelRegistry() + assert isinstance(registry.get_op("rms_norm", device="cuda"), RMSNormCudaOp) + assert isinstance(registry.get_op("rms_norm", device="cpu"), NativeRMSNormOp) # 10. Registry dispatch resolves to the hardware op when available From 1dd6dbb698c35b7e666cda77a8df5b32fb46ca80 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:48:41 +0000 Subject: [PATCH 03/44] test(rmsnorm): gate the dispatch test on the symbols the CUDA op requires Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- tests/test_rms_norm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_rms_norm.py b/tests/test_rms_norm.py index 0b2a6178f..2785ec678 100644 --- a/tests/test_rms_norm.py +++ b/tests/test_rms_norm.py @@ -17,9 +17,10 @@ try: from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE + # The same two symbols RMSNormCudaOp.__init__ requires; a build that has them + # dispatches to the CUDA op, so the dispatch test must agree with that guard. _HAS_CUDA_RMSNORM = _EXT_AVAILABLE and all( - hasattr(_C, name) - for name in ("rmsnorm_forward", "rmsnorm_backward_dx", "rmsnorm_backward_dw") + hasattr(_C, name) for name in ("rmsnorm_forward", "rmsnorm_backward_dx") ) except ImportError: # pragma: no cover - import can fail when the extension is not built. _HAS_CUDA_RMSNORM = False From 6a4f07818c75b14af84c7a8b7ed45aaa99a69659 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:51:23 +0000 Subject: [PATCH 04/44] fix(rmsnorm): guard the CUDA launchers on the input's device The forward, backward-dx and both backward-dw launchers took the current CUDA stream without switching to the input's device, so a tensor on cuda:1 while cuda:0 is current launched on the wrong GPU. Add a device guard on the input's device in each launcher. Use at::cuda::OptionalCUDAGuard with the headers included unconditionally, as activation.cu does: those launchers compile in the ROCm build too, where the file's existing c10::cuda::CUDAGuard stays inside the !USE_ROCM block. A two-device test checks that the op runs on the input's device and matches the single-device result. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- csrc/cuda/rmsnorm.cu | 8 ++++++-- tests/test_rms_norm.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index 1adaca5bd..b32bc5af4 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -1,9 +1,7 @@ #include #include -#if !defined(USE_ROCM) #include #include -#endif #include #include #include @@ -251,6 +249,9 @@ void rmsnorm_forward_cuda( torch::Tensor rstd, double eps ) { + // Launch on x's device: the current CUDA stream belongs to the current + // device, which need not be x's. + const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); int H = x.size(1); int threads = choose_threads(H); @@ -283,6 +284,7 @@ void rmsnorm_backward_dx_cuda( torch::Tensor rstd, torch::Tensor dx ) { + const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); int H = x.size(1); int threads = choose_threads(H); @@ -315,6 +317,7 @@ void rmsnorm_backward_partial_dw_cuda( torch::Tensor mask, torch::Tensor partial_dw ) { + const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); int H = x.size(1); @@ -342,6 +345,7 @@ void rmsnorm_backward_reduce_dw_cuda( torch::Tensor partial_dw, torch::Tensor dw ) { + const at::cuda::OptionalCUDAGuard device_guard(device_of(partial_dw)); int chunks = partial_dw.size(0); int H = partial_dw.size(1); diff --git a/tests/test_rms_norm.py b/tests/test_rms_norm.py index 2785ec678..b17a3f302 100644 --- a/tests/test_rms_norm.py +++ b/tests/test_rms_norm.py @@ -379,6 +379,33 @@ def test_cuda_triton_rms_norm_matches_native_forward_and_backward(impl, dtype, r ) +@requires_cuda_rmsnorm +@pytest.mark.skipif(torch.cuda.device_count() < 2, reason="requires at least two CUDA devices") +def test_cuda_rms_norm_runs_on_the_input_device_not_the_current_one(): + # The launchers take the current CUDA stream, which belongs to the current + # device; they must switch to x's device first or a cuda:1 input while + # cuda:0 is current launches on the wrong GPU. + torch.manual_seed(0) + x_cpu = torch.randn(8, 768, dtype=torch.float32) + w_cpu = torch.randn(768, dtype=torch.float32) + dy_cpu = torch.randn(8, 768, dtype=torch.float32) + + def run(device): + x = x_cpu.to(device=device, dtype=torch.bfloat16).requires_grad_(True) + w = w_cpu.to(device=device, dtype=torch.bfloat16).requires_grad_(True) + y = rmsnorm_cuda(x, w, eps=_EPS) + y.backward(dy_cpu.to(device=device, dtype=torch.bfloat16)) + torch.cuda.synchronize(device) + return y.detach(), x.grad.detach(), w.grad.detach() + + with torch.cuda.device(0): + expected = run("cuda:0") + actual = run("cuda:1") + for got, want in zip(actual, expected, strict=True): + assert got.device == torch.device("cuda:1") + assert torch.equal(got.cpu(), want.cpu()) + + @requires_cuda @pytest.mark.parametrize("impl", ["triton", "cuda"]) def test_cuda_triton_rms_norm_deterministic_repeat(impl): From 6741d16505b6c40c8abab3c91d887af8d3f76baf Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:03:12 +0000 Subject: [PATCH 05/44] feat(ws1): Qwen3-Next RMSNorm references and zero-centred CUDA offset Advances RFC #428 C1 on the CUDA track. Claim level: L0 repeatable and L1 batch-invariant. L2 is NOT claimed -- see "On exactness against vLLM" below. Qwen3-Next's decoder and final norms store a zero-centred weight and compute `x * rstd * (1 + w)`, with the `1 +` applied in fp32 after the upcast. Folding it into a bf16 weight beforehand rounds the offset away, so it has to reach the kernel as a parameter rather than being pre-applied by the caller. NativeRMSNormOp gains `weight_offset` (default 0.0) Qwen3NextRMSNormOp subclasses it, overriding only weight_offset = 1.0 Qwen3NextRMSNormGatedOp GDN gated norm: plain w, weight multiply in fp32 Qwen3NextRMSNormGatedHFOp the transformers convention, kept as a witness The offset is applied under `if cls.weight_offset:` rather than unconditionally, because `0.0 + w` rewrites -0.0 to +0.0. torch.equal does not notice that, but a bitwise comparison does, and the plain path must stay bit-for-bit what it was. A test pins it at the bit level. The gated pair exists because transformers and vLLM disagree on where the gated norm's weight multiply happens, and the gap is not a ULP: on bf16 / head_v_dim=128 they differ in 35% of elements with max|diff| = 6.25e-2. Isolating the cast order alone reproduces the gap (5.3e-2), so the cast order dominates rather than the reduction order. The two conventions share their validation and normalization and differ only in a `_scale_by_weight` hook. CUDA: `weight_offset` added to the forward and dx kernels, defaulting to 0.0 so every existing caller and binding is unaffected. The dw kernel is untouched: d/dw (offset + w) == d/dw w. weight_offset=1.0 vs an explicit fp32 (1 + w) weight bitwise equal weight_offset=1.0 vs a bf16-folded (1 + w) weight differs, as required default offset vs the previous kernel bitwise equal The first line is the correctness argument: the in-kernel offset is the same arithmetic as the fp32 reference, not an approximation. The second is a regression guard -- if it ever passes, the offset has stopped being fp32. On exactness against vLLM ------------------------- Measured over 40 seeds (bf16, head_v_dim=128, 512 rows): ours vs forward_native 6/40 seeds differ, worst 1.56e-2 ours vs forward_cuda 18/40 seeds differ, worst 3.91e-3 forward_native vs forward_cuda 21/40 seeds differ, worst 1.56e-2 vLLM's own two paths are not bitwise equal to each other, so "bitwise equal to vLLM" is undefined until a single provider is named. In fp32 the two paths differ on ~36% of elements, every one by an fp32 ULP -- the tree shapes differ, the semantics do not. What this reproduces is the convention (fp32 weight multiply, single trailing cast); the residual is the reduction tree. The reduction stays the repo's fixed 32-wide chunked sum, which is what buys L1. It was introduced for NPU but is needed on CUDA too: over 20 seeds at H=2048 in bf16 -- Qwen3-Next's own hidden_size and dtype -- a plain mean(-1) broke slice invariance on 1 of 20 while the chunked reduction broke on 0 of 20. Matching stock vLLM bitwise would mean adopting a reduction that is not itself batch-invariant, i.e. trading L1 for L2. tests/check_qwen3_next_norm_providers.py pins the dispatch facts and bounds the gap, asserting magnitudes rather than equality so a vLLM bump that changes the provider fails loudly. It is named `check_` rather than `test_`, following tests/distributed/check_*.py: it imports real vLLM, and tests/test_framework_operator_integrations.py asserts vllm is absent from sys.modules, an invariant any collected test importing vLLM would break for the whole session. Also: `rl_engine/_C.pyi` updated for the new `weight_offset` argument (CI runs mypy against it), `tests/test_qwen3_next_norm.py` added to the CI test list in .github/workflows/ci.yml, and an operator page added per docs/operators/README.md ("the documentation page is part of the operator contract"). Verified on 2x B200 (sm_100, driver 580.126.20, torch 2.13.0+cu130, triton 3.7.1, vllm 0.30.0, transformers 5.17.0): pytest tests/test_qwen3_next_norm.py -q pytest tests/check_qwen3_next_norm_providers.py -q pytest tests/ rl_engine/tests/ -q -p no:randomly \ --ignore=tests/test_rocm_aiter_api_contract.py Both new files pass and the full suite gains no failure. The ignored file fails to import on the merge-base as well. Absolute suite counts are reported in the PR description against a named base commit, not here: they shift whenever a sibling test is added, so a count frozen in a commit message goes stale the moment the branch is rebased. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- csrc/cuda/rmsnorm.cu | 29 +- csrc/ops.cpp | 24 +- docs/.nav.yml | 1 + docs/operators/README.md | 1 + docs/operators/qwen3-next-rms-norm.md | 103 ++++ rl_engine/_C.pyi | 2 + rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 37 +- .../ops/pytorch/norm/qwen3_next_rms_norm.py | 198 +++++++ .../kernels/ops/pytorch/norm/rms_norm.py | 30 +- tests/check_qwen3_next_norm_providers.py | 232 ++++++++ tests/test_qwen3_next_norm.py | 502 ++++++++++++++++++ 12 files changed, 1127 insertions(+), 34 deletions(-) create mode 100644 docs/operators/qwen3-next-rms-norm.md create mode 100644 rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py create mode 100644 tests/check_qwen3_next_norm_providers.py create mode 100644 tests/test_qwen3_next_norm.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75627621e..5f8c900b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: run: | python -m pytest rl_engine/tests/test_dispatch.py -v PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest tests/test_attention_correctness.py -q -rs - python -m pytest tests/test_forward_invariance.py tests/test_tolerance_contract.py tests/test_ws1_workload.py tests/test_gradient_invariance.py tests/test_elementwise_inventory.py tests/test_four_judgment_matrix.py tests/test_op_checks.py tests/test_operator_inputs.py tests/test_profiler.py tests/test_kv_consistency.py tests/test_ws1_qwen3_dense.py tests/test_ws1_chain_integration.py -q + python -m pytest tests/test_forward_invariance.py tests/test_tolerance_contract.py tests/test_ws1_workload.py tests/test_gradient_invariance.py tests/test_elementwise_inventory.py tests/test_four_judgment_matrix.py tests/test_op_checks.py tests/test_operator_inputs.py tests/test_profiler.py tests/test_kv_consistency.py tests/test_ws1_qwen3_dense.py tests/test_ws1_chain_integration.py tests/test_qwen3_next_norm.py -q - name: Run Cross-Configuration Contract Tests (CPU-safe) run: | diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index b32bc5af4..fc6c425d6 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -108,7 +108,8 @@ __global__ void rmsnorm_fwd_kernel( float* __restrict__ rstd, int T, int H, - float eps + float eps, + float weight_offset ) { int row = blockIdx.x; int tid = threadIdx.x; @@ -135,10 +136,13 @@ __global__ void rmsnorm_fwd_kernel( __syncthreads(); - // Write y = x * rstd * weight. + // Write y = x * rstd * (weight_offset + weight). The offset is added in + // fp32 after the upcast: a zero-centred weight (Qwen3-Next, Gemma) must not + // have its "+1" folded into the low-precision weight beforehand, which would + // round the offset and break the bitwise contract. for (int col = tid; col < H; col += blockDim.x) { float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col); + float wv = load_as_float(weight + col) + weight_offset; float out = xv * row_rstd * wv; store_from_float(y_row + col, out); } @@ -153,7 +157,8 @@ __global__ void rmsnorm_bwd_dx_kernel( const float* __restrict__ rstd, scalar_t* __restrict__ dx, int T, - int H + int H, + float weight_offset ) { int row = blockIdx.x; int tid = threadIdx.x; @@ -167,7 +172,7 @@ __global__ void rmsnorm_bwd_dx_kernel( for (int col = tid; col < H; col += blockDim.x) { float dyv = load_as_float(dy_row + col); float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col); + float wv = load_as_float(weight + col) + weight_offset; local_dot += dyv * wv * xv; } @@ -179,7 +184,7 @@ __global__ void rmsnorm_bwd_dx_kernel( for (int col = tid; col < H; col += blockDim.x) { float dyv = load_as_float(dy_row + col); float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col); + float wv = load_as_float(weight + col) + weight_offset; float out = r * dyv * wv - xv * coeff; store_from_float(dx_row + col, out); @@ -247,7 +252,8 @@ void rmsnorm_forward_cuda( torch::Tensor weight, torch::Tensor y, torch::Tensor rstd, - double eps + double eps, + double weight_offset ) { // Launch on x's device: the current CUDA stream belongs to the current // device, which need not be x's. @@ -270,7 +276,8 @@ void rmsnorm_forward_cuda( rstd.data_ptr(), T, H, - static_cast(eps) + static_cast(eps), + static_cast(weight_offset) ); }); }); @@ -282,7 +289,8 @@ void rmsnorm_backward_dx_cuda( torch::Tensor x, torch::Tensor weight, torch::Tensor rstd, - torch::Tensor dx + torch::Tensor dx, + double weight_offset ) { const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); @@ -303,7 +311,8 @@ void rmsnorm_backward_dx_cuda( rstd.data_ptr(), dx.data_ptr(), T, - H + H, + static_cast(weight_offset) ); }); }); diff --git a/csrc/ops.cpp b/csrc/ops.cpp index 11fda1159..4e786e5b3 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -260,14 +260,16 @@ void rmsnorm_forward_cuda( torch::Tensor weight, torch::Tensor y, torch::Tensor rstd, - double eps); + double eps, + double weight_offset); void rmsnorm_backward_dx_cuda( torch::Tensor dy, torch::Tensor x, torch::Tensor weight, torch::Tensor rstd, - torch::Tensor dx); + torch::Tensor dx, + double weight_offset); void rmsnorm_backward_partial_dw_cuda( torch::Tensor dy, @@ -296,7 +298,8 @@ static void rmsnorm_check_input(const torch::Tensor& x, const char* name) { std::vector rmsnorm_forward( torch::Tensor x, torch::Tensor weight, - double eps) + double eps, + double weight_offset) { rmsnorm_check_input(x, "x"); rmsnorm_check_input(weight, "weight"); @@ -309,7 +312,7 @@ std::vector rmsnorm_forward( auto y = torch::empty_like(x); auto rstd = torch::empty({T}, x.options().dtype(torch::kFloat32)); - rmsnorm_forward_cuda(x, weight, y, rstd, eps); + rmsnorm_forward_cuda(x, weight, y, rstd, eps, weight_offset); return {y, rstd}; } @@ -318,7 +321,8 @@ torch::Tensor rmsnorm_backward_dx( torch::Tensor dy, torch::Tensor x, torch::Tensor weight, - torch::Tensor rstd) + torch::Tensor rstd, + double weight_offset) { rmsnorm_check_input(dy, "dy"); rmsnorm_check_input(x, "x"); @@ -333,7 +337,7 @@ torch::Tensor rmsnorm_backward_dx( auto dx = torch::empty_like(x); - rmsnorm_backward_dx_cuda(dy, x, weight, rstd, dx); + rmsnorm_backward_dx_cuda(dy, x, weight, rstd, dx, weight_offset); return dx; } @@ -712,8 +716,12 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { &det_gemm_db_transposed, "Batch-invariant deterministic GEMM backward in canonical [N,K] layout"); // registry RMSNorm - m.def("rmsnorm_forward", &rmsnorm_forward, "Batch-invariant RMSNorm forward CUDA"); - m.def("rmsnorm_backward_dx", &rmsnorm_backward_dx, "Batch-invariant RMSNorm backward dx CUDA"); + m.def("rmsnorm_forward", &rmsnorm_forward, "Batch-invariant RMSNorm forward CUDA", + py::arg("x"), py::arg("weight"), py::arg("eps"), + py::arg("weight_offset") = 0.0); + m.def("rmsnorm_backward_dx", &rmsnorm_backward_dx, "Batch-invariant RMSNorm backward dx CUDA", + py::arg("dy"), py::arg("x"), py::arg("weight"), py::arg("rstd"), + py::arg("weight_offset") = 0.0); m.def("rmsnorm_backward_dw", &rmsnorm_backward_dw, "Deterministic RMSNorm backward dweight CUDA"); #if !defined(USE_ROCM) m.def( diff --git a/docs/.nav.yml b/docs/.nav.yml index 7d71230a3..9133710de 100644 --- a/docs/.nav.yml +++ b/docs/.nav.yml @@ -29,6 +29,7 @@ nav: - operators/sampling.md - operators/det-gemm.md - operators/embedding.md + - operators/qwen3-next-rms-norm.md - Developer Guide: - contributing/README.md - General: diff --git a/docs/operators/README.md b/docs/operators/README.md index 00f4cbb45..0d955ed18 100644 --- a/docs/operators/README.md +++ b/docs/operators/README.md @@ -32,4 +32,5 @@ Every operator page should include: - [Matmul](matmul.md) - [Sampling](sampling.md) - [Token Embedding](embedding.md) +- [Qwen3-Next RMSNorm (zero-centred)](qwen3-next-rms-norm.md) - [Operator Doc Template](../contributing/operator-doc-template.md) diff --git a/docs/operators/qwen3-next-rms-norm.md b/docs/operators/qwen3-next-rms-norm.md new file mode 100644 index 000000000..292139bea --- /dev/null +++ b/docs/operators/qwen3-next-rms-norm.md @@ -0,0 +1,103 @@ +# Qwen3-Next RMSNorm (zero-centred) + +## Summary + +Qwen3-Next's decoder and final norms store a **zero-centred** weight and compute +`x * rstd * (1 + w)` rather than `x * rstd * w`. The `1 +` is applied in fp32, +after the upcast — folding it into a low-precision weight beforehand rounds the +offset away and silently breaks any bitwise claim. + +This operator exists for RFC #428 C1 (Embedding / RMSNorm / residual / final norm +exactness) on the Qwen3-Next rollout-vs-replay path. The Gated DeltaNet block uses +a *different* weight convention and a different cast order; it is a separate +operator. + +Upstream references: `transformers` `Qwen3NextRMSNorm`, and vLLM's `GemmaRMSNorm`, +which `vllm/model_executor/models/qwen3_next.py` aliases as `Qwen3NextRMSNorm`. + +## Entry Point + +```python +from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormOp +from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormCudaOp, rmsnorm_cuda + +y = Qwen3NextRMSNormOp().forward(x, weight, eps=1e-6) # reference +y = Qwen3NextRMSNormCudaOp().forward(x, weight, eps=1e-6) # CUDA + +# The offset is a kernel parameter, not a pre-pass: +y = rmsnorm_cuda(x, weight, eps=1e-6, weight_offset=1.0) +``` + +## Backends + +| Backend | Wrapper | Native symbol | Status | +| --- | --- | --- | --- | +| CUDA | `Qwen3NextRMSNormCudaOp` | `rl_engine._C.rmsnorm_forward` (`weight_offset=1.0`) | Supported | +| ROCm | — | — | Not implemented | +| PyTorch fallback | `Qwen3NextRMSNormOp` | — | Supported (WS1 gold) | + +`Qwen3NextRMSNormOp` subclasses `NativeRMSNormOp` and overrides only +`weight_offset`; the base applies it in fp32. + +## Tensor Contract + +| Argument | Shape | Dtype | Requirements | +| --- | --- | --- | --- | +| `x` | `[..., H]` | fp32 / bf16 / fp16 | CUDA path requires contiguous | +| `weight` | `[H]` | matches `x` | zero-centred (upstream inits to zeros) | +| `eps` | scalar | float | inside the sqrt; `1e-6` for Qwen3-Next | + +## Dispatch Behavior + +Registered as the `qwen3_next_rms_norm` gtest operator. On CUDA the registry +prefers `Qwen3NextRMSNormCudaOp`; every other platform resolves to the PyTorch +reference. The CUDA op validates the compiled symbols in `__init__`, so on a build +without the extension construction raises and the registry falls through to the +reference rather than handing out an op that fails at call time. + +## Accuracy + +Claim level: **L0 repeatable, L1 batch-invariant**. L2 is not claimed. + +The reduction is the repo's fixed 32-wide chunked sum +(`shape_invariant_rstd`), which is what makes a row's result independent of the +batch layout. It deliberately differs from upstream's `mean(-1)`: measured on B200, +over 20 seeds at `H=2048` in bf16, a plain `mean(-1)` broke slice invariance on 1 +of 20 while the chunked reduction broke on 0 of 20. + +The cost is that the decoder norm is **not** bitwise equal to stock vLLM — 7 +elements of 1048576 differ, `max|diff| = 1.56e-2` in bf16 at `H=2048`. That gap is +inherent: matching stock vLLM bitwise would mean adopting a reduction that is not +itself batch-invariant, i.e. trading L1 for L2. + +The in-kernel offset is exact, not an approximation: `weight_offset=1.0` is bitwise +equal to passing an explicit fp32 `1 + w` weight, and differs from a bf16-folded +`1 + w`, both asserted. + +Tolerances come from `tolerance_contract.json` (`reduction` × dtype); no private +thresholds. + +## Performance Notes + +The CUDA path reuses the existing `rmsnorm_fwd_kernel` reduction +(`block_reduce_sum` over `choose_threads(H)`), so the offset costs one fp32 add per +element and no extra memory traffic. + +```bash +python scripts/check_operator.py --op qwen3_next_rms_norm --candidate cuda \ + --device cuda --dtype bf16 --check-grad +``` + +## Tests + +```bash +python -m pytest tests/test_qwen3_next_norm.py -v +``` + +## Known Limitations + +- CUDA only; no ROCm, Ascend or Triton backend. +- Not bitwise against stock vLLM (see Accuracy); an L2 claim needs the strict + provider on both sides, per RFC #428 §1 item 1. +- Measured on sm_100 (B200). Per RFC #428 §2.2 no claim carries across + H100/H200/B100/B200. diff --git a/rl_engine/_C.pyi b/rl_engine/_C.pyi index fb3c4379c..b911feaf7 100644 --- a/rl_engine/_C.pyi +++ b/rl_engine/_C.pyi @@ -260,12 +260,14 @@ def rmsnorm_forward( x: torch.Tensor, weight: torch.Tensor, eps: float, + weight_offset: float = ..., ) -> list[torch.Tensor]: ... def rmsnorm_backward_dx( dy: torch.Tensor, x: torch.Tensor, weight: torch.Tensor, rstd: torch.Tensor, + weight_offset: float = ..., ) -> torch.Tensor: ... def rmsnorm_backward_dw( dy: torch.Tensor, diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index 21be8dfb8..3b72e497d 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -30,16 +30,18 @@ class RMSNormCuda(torch.autograd.Function): """ @staticmethod - def forward(ctx, x, weight, mask=None, eps=1e-6): + def forward(ctx, x, weight, mask=None, eps=1e-6, weight_offset=0.0): """ Forward: - y = x * rsqrt(mean(x^2) + eps) * weight + y = x * rsqrt(mean(x^2) + eps) * (weight_offset + weight) Input: x: [T, H], fp16/bf16/fp32 CUDA tensor weight: [H], fp16/bf16/fp32 CUDA tensor mask: [T], bool CUDA tensor eps: float + weight_offset: float, added to weight in fp32 inside the kernel. + 1.0 selects the zero-centred (1 + w) convention. Output: y: [T, H] @@ -64,10 +66,11 @@ def forward(ctx, x, weight, mask=None, eps=1e-6): assert mask.dim() == 1, "mask must be [T]" assert mask.shape[0] == x.shape[0], "mask length mismatch" - y, rstd = _C.rmsnorm_forward(x, weight, float(eps)) + y, rstd = _C.rmsnorm_forward(x, weight, float(eps), float(weight_offset)) ctx.save_for_backward(x, weight, rstd, mask) ctx.eps = eps + ctx.weight_offset = float(weight_offset) return y @@ -81,7 +84,7 @@ def backward(ctx, grad_out): x, weight, rstd, mask = ctx.saved_tensors dy = grad_out.contiguous() - dx = _C.rmsnorm_backward_dx(dy, x, weight, rstd) + dx = _C.rmsnorm_backward_dx(dy, x, weight, rstd, ctx.weight_offset) # The shape-independent FP32 left fold preserves the C2 Batch/Chunk # reduction order while the CUDA reducer executes it in one launch. @@ -101,16 +104,18 @@ def backward(ctx, grad_out): family="cuda", ) - return dx, dw, None, None + # dw is unchanged by the offset: d/dw (offset + w) == d/dw w. + return dx, dw, None, None, None -def rmsnorm_cuda(x, weight, eps=1e-6, mask=None): +def rmsnorm_cuda(x, weight, eps=1e-6, mask=None, weight_offset=0.0): """ use: y = rmsnorm_cuda(x, weight) y = rmsnorm_cuda(x, weight, mask=mask) + y = rmsnorm_cuda(x, weight, weight_offset=1.0) # zero-centred weight """ - return RMSNormCuda.apply(x, weight, mask, eps) + return RMSNormCuda.apply(x, weight, mask, eps, weight_offset) class RMSNormCudaOp: @@ -124,6 +129,9 @@ def __init__(self) -> None: "rmsnorm_forward", "rmsnorm_backward_dx", ) + #: Added to the weight in fp32 inside the kernel. Subclasses override it; + #: 0.0 is the plain convention. + weight_offset = 0.0 def __call__(self, x, weight, *, eps=1e-6): return self.forward(x, weight, eps=eps) @@ -131,7 +139,9 @@ def __call__(self, x, weight, *, eps=1e-6): def forward(self, x, weight, *, eps=1e-6): hidden = x.shape[-1] x_2d = x.contiguous().view(-1, hidden) - y_2d = rmsnorm_cuda(x_2d, weight.contiguous(), eps=eps) + y_2d = rmsnorm_cuda( + x_2d, weight.contiguous(), eps=eps, weight_offset=self.weight_offset + ) return y_2d.view_as(x) def parameter_vjp_contributions_fp32(self, *, x, weight, grad_output, eps=1e-6): @@ -140,3 +150,14 @@ def parameter_vjp_contributions_fp32(self, *, x, weight, grad_output, eps=1e-6): rstd = torch.rsqrt(x32.square().mean(dim=-1) + float(eps)) rows = grad_output.float() * x32 * rstd.unsqueeze(-1) return {"weight": rows} + + +class Qwen3NextRMSNormCudaOp(RMSNormCudaOp): + """Zero-centred CUDA RMSNorm: ``y = x * rstd * (1 + weight)``. + + The decoder and final norms of Qwen3-Next (and Gemma) store a zero-centred + weight. The ``+1`` is applied inside the kernel after the fp32 upcast, so it + is never rounded through the low-precision weight dtype. + """ + + weight_offset = 1.0 diff --git a/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py new file mode 100644 index 000000000..5261245d0 --- /dev/null +++ b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py @@ -0,0 +1,198 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Qwen3-Next RMSNorm references (WS1 ground truth for RFC #428 C1). + +Qwen3-Next ships two RMSNorm conventions that differ both in how the weight is +applied and in where the dtype casts sit. They are kept as separate operators +because the cast order is part of the contract, not a flag: + +``Qwen3NextRMSNorm`` (decoder / final norm) + normalize in fp32, scale by ``(1 + weight)`` in fp32, cast once at the end. + The stored weight is zero-centred, so the ``1 +`` offset MUST be applied + after the fp32 upcast -- folding it into a bf16 weight first rounds the + offset and silently breaks the bitwise claim. + +``Qwen3NextRMSNormGated`` (inside the Gated DeltaNet block) + normalize in fp32, scale by a plain (non zero-centred) weight, then gate by + ``silu(gate)``. Where the weight multiply happens is NOT agreed upstream -- + see below -- so it is an explicit part of the operator identity here. + +Divergence at the gated-norm boundary (measured) +------------------------------------------------- +``transformers`` and vLLM do not agree on the gated variant: + +* vLLM (``RMSNormGated``, both ``forward_native`` and the FLA Triton + ``forward_cuda``) keeps the normalized value in fp32 for the weight multiply. +* ``transformers`` (``Qwen3NextRMSNormGated``) casts the normalized value back + to the input dtype *before* the weight multiply. + +On B200 / bf16 / ``head_v_dim=128`` these differ in 35% of elements with +``max|diff| = 6.25e-2``; swapping only the cast order reproduces the gap +(``5.3e-2``), so the cast order -- not the reduction order -- dominates. Because +RFC #428 claims L2 exactness against **vLLM rollout**, the fp32 multiply is the +strict default; the transformers convention is kept as a named witness so the +divergence stays testable instead of being silently picked. + +What "agrees with vLLM" means here, precisely +---------------------------------------------- +Only the *convention* is reproduced, not the bits. vLLM's own two paths are not +bitwise equal to each other: over 40 seeds (bf16, ``head_v_dim=128``, 512 rows), +``forward_native`` and ``forward_cuda`` disagreed on 21, worst +``max|diff| = 1.56e-2``. Against this operator the figures were 6/40 and 18/40. +So "bitwise equal to vLLM" is undefined until a single provider is named, and +this operator does not claim it -- see the reduction-order note below. + +Both reuse :func:`shape_invariant_rstd` so the reduction order -- and hence the +result -- never depends on the batch layout (RFC #428 section 6, item 2). The +reduction order therefore deliberately differs from upstream's ``mean(-1)``; +what is reproduced exactly is the weight convention and the cast order. + +Why the chunked reduction is kept on CUDA too +--------------------------------------------- +:func:`shape_invariant_rstd` was introduced for NPU, where ``mean``/``sum`` pick +shape-dependent kernels. Measured on B200 (sm_100) it is needed on CUDA as well: +over 20 seeds at ``H=2048`` in bf16 -- Qwen3-Next's own ``hidden_size`` and dtype +-- a plain ``mean(-1)`` broke slice invariance (``rstd(x[3:5]) != rstd(x)[3:5]``) +on 1 of 20, while the chunked reduction broke on 0 of 20. Failures were also seen +at ``H=5120`` in fp32. + +The cost is that the decoder norm is NOT bitwise equal to stock vLLM: 7 elements +of 1048576 differ (``max|diff| = 1.56e-2``, bf16, H=2048). That gap is inherent -- +matching stock vLLM bitwise would mean adopting a reduction that is itself not +batch-invariant, i.e. trading L1 for L2. These operators therefore claim L0 and L1 +only; an L2 claim needs the strict provider on both sides, per RFC #428 section 1, +item 1. +""" + +from __future__ import annotations + +import torch +import torch.nn.functional as F + +from rl_engine.kernels.ops.pytorch.norm.rms_norm import ( + NativeRMSNormOp, + check_norm_weight, + shape_invariant_rstd, +) + +__all__ = [ + "Qwen3NextRMSNormOp", + "Qwen3NextRMSNormGatedOp", + "Qwen3NextRMSNormGatedHFOp", +] + + +class Qwen3NextRMSNormOp(NativeRMSNormOp): + """Zero-centred RMSNorm: ``out = x * rstd * (1 + weight)``. + + Only the weight convention differs from :class:`NativeRMSNormOp`, so that is + all this overrides. The base applies the offset in fp32, after the upcast, + which is what ``transformers`` and vLLM both do -- folding ``1 +`` into a + bf16 weight beforehand would round the offset away. + """ + + weight_offset = 1.0 + + +class Qwen3NextRMSNormGatedOp: + """Gated RMSNorm used by the Gated DeltaNet block (vLLM/strict convention). + + ``out = (x * rstd * weight) * silu(gate)``, with every multiply in fp32 and + a single cast on the way out. This is the convention vLLM's ``RMSNormGated`` + uses with ``norm_before_gate=True``, which is what the RFC #428 L2 claim is + measured against. + + Not a subclass of the plain op: it takes an extra tensor and its epilogue + differs, so it is not a drop-in substitute for one. + + The weight is plain, NOT zero-centred -- upstream initializes it to ones. + """ + + def __call__( + self, + x: torch.Tensor, + weight: torch.Tensor, + gate: torch.Tensor, + *, + eps: float = 1e-6, + ) -> torch.Tensor: + return self.forward(x, weight, gate, eps=eps) + + def forward( + self, + x: torch.Tensor, + weight: torch.Tensor, + gate: torch.Tensor, + *, + eps: float = 1e-6, + ) -> torch.Tensor: + return self._rms_norm_gated(x, weight, gate, eps=eps, output_dtype=x.dtype) + + def forward_fp32( + self, + x: torch.Tensor, + weight: torch.Tensor, + gate: torch.Tensor, + *, + eps: float = 1e-6, + ) -> torch.Tensor: + """Ground-truth: fp32 in, fp32 out.""" + return self._rms_norm_gated(x, weight, gate, eps=eps, output_dtype=torch.float32) + + # ------------------------------------------------------------------ # + # Shared by both conventions; only `_scale_by_weight` differs. + # ------------------------------------------------------------------ # + @staticmethod + def _normalized( + x: torch.Tensor, weight: torch.Tensor, gate: torch.Tensor, *, eps: float + ) -> torch.Tensor: + check_norm_weight(x, weight) + if gate.shape != x.shape: + raise ValueError( + f"gate must match x, got tuple(gate.shape)={tuple(gate.shape)} " + f"vs tuple(x.shape)={tuple(x.shape)}" + ) + x_f = x.float() + rstd = shape_invariant_rstd(x_f, float(eps)).unsqueeze(-1) + return x_f * rstd + + @staticmethod + def _scale_by_weight( + normed: torch.Tensor, weight: torch.Tensor, input_dtype: torch.dtype + ) -> torch.Tensor: + """vLLM: the weight multiply stays in fp32.""" + del input_dtype + return normed * weight.float() + + @classmethod + def _rms_norm_gated( + cls, + x: torch.Tensor, + weight: torch.Tensor, + gate: torch.Tensor, + *, + eps: float, + output_dtype: torch.dtype, + ) -> torch.Tensor: + normed = cls._normalized(x, weight, gate, eps=eps) + scaled = cls._scale_by_weight(normed, weight, x.dtype) + # The gate activation is evaluated in fp32 and promotes the product. + gated = scaled * F.silu(gate.float()) + return gated.to(output_dtype) + + +class Qwen3NextRMSNormGatedHFOp(Qwen3NextRMSNormGatedOp): + """``transformers`` gated convention: weight multiply in the input dtype. + + Kept so the HF-vs-vLLM divergence documented in the module docstring stays + covered by a test rather than discovered in a drift report. Do NOT use this + for an L2 claim against vLLM rollout. + """ + + @staticmethod + def _scale_by_weight( + normed: torch.Tensor, weight: torch.Tensor, input_dtype: torch.dtype + ) -> torch.Tensor: + """transformers: round-trip through the input dtype first.""" + return weight * normed.to(input_dtype) diff --git a/rl_engine/kernels/ops/pytorch/norm/rms_norm.py b/rl_engine/kernels/ops/pytorch/norm/rms_norm.py index 135556c3c..66d4723fa 100644 --- a/rl_engine/kernels/ops/pytorch/norm/rms_norm.py +++ b/rl_engine/kernels/ops/pytorch/norm/rms_norm.py @@ -86,6 +86,15 @@ def strict_add_rms_norm( return _strict_add_rms_norm(x, residual, weight, eps) +def check_norm_weight(x: torch.Tensor, weight: torch.Tensor) -> None: + """Shared shape guard for the RMSNorm family (plain, zero-centred, gated).""" + if weight.dim() != 1 or weight.shape[0] != x.shape[-1]: + raise ValueError( + f"weight must be 1-D of size x.shape[-1]={x.shape[-1]}, " + f"got tuple(weight.shape)={tuple(weight.shape)}" + ) + + def shape_invariant_rstd(x_f: torch.Tensor, eps: float) -> torch.Tensor: """Shape-invariant per-row rstd (the shared RMSNorm statistic). @@ -113,6 +122,10 @@ class NativeRMSNormOp: out = x * rsqrt(mean(x^2, dim=-1) + eps) * weight """ + #: Added to the weight in fp32 before it scales the normalized value. + #: Subclasses set 1.0 for the zero-centred ``(1 + w)`` convention. + weight_offset = 0.0 + def __init__(self) -> None: pass @@ -151,21 +164,24 @@ def forward_fp32( # ------------------------------------------------------------------ # # Helpers # ------------------------------------------------------------------ # - @staticmethod + @classmethod def _rms_norm( + cls, x: torch.Tensor, weight: torch.Tensor, *, eps: float, output_dtype: torch.dtype, ) -> torch.Tensor: - if weight.dim() != 1 or weight.shape[0] != x.shape[-1]: - raise ValueError( - f"weight must be 1-D of size x.shape[-1]={x.shape[-1]}, " - f"got tuple(weight.shape)={tuple(weight.shape)}" - ) + check_norm_weight(x, weight) x_f = x.float() rstd = shape_invariant_rstd(x_f, float(eps)).unsqueeze(-1) normed = x_f * rstd - out = normed * weight.float() + scale = weight.float() + # Guarded rather than unconditional: `0.0 + w` rewrites -0.0 to +0.0, + # which torch.equal does not notice but a bitwise comparison does. The + # plain path must stay bit-for-bit what it was. + if cls.weight_offset: + scale = cls.weight_offset + scale + out = normed * scale return out.to(output_dtype) diff --git a/tests/check_qwen3_next_norm_providers.py b/tests/check_qwen3_next_norm_providers.py new file mode 100644 index 000000000..062052cfa --- /dev/null +++ b/tests/check_qwen3_next_norm_providers.py @@ -0,0 +1,232 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Which vLLM gated-RMSNorm path is the strict provider, and how far apart are they. + +Named ``check_`` rather than ``test_`` on purpose, following +``tests/distributed/check_*.py``: this module imports real vLLM, and +``tests/test_framework_operator_integrations.py`` asserts that ``vllm`` is absent +from ``sys.modules`` -- an invariant any collected test importing vLLM would break +for the whole session. Run it explicitly: + + pytest tests/check_qwen3_next_norm_providers.py -v + +RFC #428 section 2.1 defines L2 as "bitwise identical to vLLM rollout". For +Qwen3-Next's GDN gated norm that phrase is not well defined until a single +provider is named, because vLLM ships several and they do not agree bitwise with +each other. + +This module pins two things so a vLLM upgrade cannot move them silently: + +1. **Dispatch facts** -- which provider actually runs, asserted on the env + defaults and the registered ops rather than inferred from reading one branch. +2. **The size of the gap** -- a seed sweep that asserts an upper bound on the + disagreement and on the mismatch rate. It deliberately does NOT assert + equality; the point is to keep the number honest, not to pretend it is zero. + +Measured on 2x B200 (sm_100, torch 2.13.0+cu130, vllm 0.30.0), bf16, +``head_v_dim=128``, 512 rows, 40 seeds: + +=============================== ================== ================ +comparison seeds not bitwise worst max|diff| +=============================== ================== ================ +ours vs ``forward_native`` 6 / 40 1.56e-2 +ours vs ``forward_cuda`` 18 / 40 3.91e-3 +``forward_native`` vs ``cuda`` 21 / 40 1.56e-2 +=============================== ================== ================ +""" + +from __future__ import annotations + +import pytest +import torch + +from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormGatedOp + +# vLLM is imported inside the checks, never at module scope, so that merely +# collecting this file does not pull it into sys.modules. + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required") + +# Qwen3-Next-80B-A3B-Instruct: linear_value_head_dim / rms_norm_eps. +_HEAD_V_DIM = 128 +_EPS = 1e-6 +_ROWS = 512 +_SEEDS = 40 + +# The gated norm is constructed by vLLM's GDN block with exactly these settings; +# see vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py. +_NORM_BEFORE_GATE = True +_GROUP_SIZE = None +_ACTIVATION = "silu" + + +@pytest.fixture(scope="module") +def vllm_config_ctx(): + """`RMSNormGated` is a CustomOp and refuses to build outside a config context.""" + pytest.importorskip("vllm", reason="vLLM is required to identify the provider") + from vllm.config import VllmConfig, set_current_vllm_config + + with set_current_vllm_config(VllmConfig()): + yield + + +def _make_norm(): + from vllm.model_executor.layers.layernorm import RMSNormGated + + return RMSNormGated( + _HEAD_V_DIM, + eps=_EPS, + group_size=_GROUP_SIZE, + norm_before_gate=_NORM_BEFORE_GATE, + activation=_ACTIVATION, + ) + + +def _inputs(seed: int, dtype: torch.dtype, rows: int = _ROWS): + g = torch.Generator(device="cuda").manual_seed(seed) + x = torch.randn(rows, _HEAD_V_DIM, device="cuda", dtype=dtype, generator=g) + gate = torch.randn(rows, _HEAD_V_DIM, device="cuda", dtype=dtype, generator=g) + weight = torch.randn(_HEAD_V_DIM, device="cuda", dtype=dtype, generator=g) + return x, gate, weight + + +def _disagreement(a: torch.Tensor, b: torch.Tensor) -> tuple[float, float]: + """(worst absolute difference, fraction of elements that differ bitwise).""" + bits_a = a.float().view(torch.int32) + bits_b = b.float().view(torch.int32) + mismatch = int((bits_a != bits_b).sum()) + worst = (a.float() - b.float()).abs().max().item() + return worst, mismatch / a.numel() + + +# --------------------------------------------------------------------------- # +# 1. Dispatch facts -- which provider actually runs +# --------------------------------------------------------------------------- # +def test_custom_op_has_distinct_native_and_cuda_paths(vllm_config_ctx): + """`forward_native` is a reference; `forward_cuda` is what CustomOp dispatches.""" + norm = _make_norm() + assert type(norm).forward_cuda is not type(norm).forward_native + + +def test_gdn_decode_provider_env_defaults_are_recorded(): + """Pin the env defaults that decide which GDN decode kernel runs. + + These are what make ``fused_recurrent_gated_delta_rule_packed_decode`` (not + ``fused_sigmoid_gating_delta_rule_update``) the rollout decode path. If a + vLLM bump flips either default, the provider identity behind any L2 claim + changes, so this must fail loudly rather than drift. + """ + pytest.importorskip("vllm", reason="vLLM is required to identify the provider") + import vllm.envs as envs + + observed = { + "VLLM_GDN_DECODE_KERNEL": envs.VLLM_GDN_DECODE_KERNEL.strip().lower(), + "VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE": bool( + envs.VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE + ), + } + assert observed == { + "VLLM_GDN_DECODE_KERNEL": "cuda", + "VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE": True, + }, ( + f"GDN decode provider defaults changed: {observed}. Re-derive which kernel " + "the rollout decode path takes before relying on any exactness claim." + ) + + +# --------------------------------------------------------------------------- # +# 2. The gap, bounded -- never asserted to be zero +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize( + "dtype, max_abs, max_mismatch_rate", + [ + # bf16 rounding absorbs most of the reduction-tree difference, so few + # elements move -- but each that does moves by a whole bf16 ULP. + (torch.bfloat16, 2e-2, 0.05), + # fp32 has nothing to absorb it: ~36% of elements differ, every one of + # them by an fp32 ULP. Bounding the rate here would be measuring the + # wrong thing; the magnitude is what says the two agree semantically. + (torch.float32, 1e-5, 1.0), + ], +) +def test_vllm_paths_disagree_only_within_bounds(vllm_config_ctx, dtype, max_abs, max_mismatch_rate): + """vLLM's own two paths differ; bound how much. + + A growing magnitude means the reduction trees have diverged semantically, + which would invalidate treating either as the reference. A high mismatch + *rate* at ULP magnitude does not -- it just means the tree shapes differ. + """ + norm = _make_norm().to("cuda", dtype) + worst_abs, worst_rate, differing = 0.0, 0.0, 0 + for seed in range(_SEEDS): + x, gate, weight = _inputs(seed, dtype) + norm.weight.data = weight.clone() + native = norm.forward_native(x, gate) + cuda = norm.forward_cuda(x, gate) + abs_d, rate = _disagreement(native, cuda) + worst_abs, worst_rate = max(worst_abs, abs_d), max(worst_rate, rate) + differing += int(rate > 0.0) + + assert worst_abs <= max_abs, ( + f"vLLM forward_native vs forward_cuda worst |diff| {worst_abs:.3e} exceeds " + f"{max_abs:.3e} over {_SEEDS} seeds ({differing} seeds differ)" + ) + assert worst_rate <= max_mismatch_rate + + +@pytest.mark.parametrize("path", ["forward_native", "forward_cuda"]) +def test_ours_tracks_each_vllm_path_within_bounds(vllm_config_ctx, path): + """Our strict op follows vLLM's convention; bound the residual. + + Not an equality assertion. We reproduce the fp32 weight multiply and the + single trailing cast, but our reduction is the repo's fixed 32-wide chunked + sum rather than whatever tree the provider uses, so a few elements straddle + a rounding boundary. + """ + dtype = torch.bfloat16 + ours = Qwen3NextRMSNormGatedOp() + norm = _make_norm().to("cuda", dtype) + + worst_abs, worst_rate = 0.0, 0.0 + for seed in range(_SEEDS): + x, gate, weight = _inputs(seed, dtype) + norm.weight.data = weight.clone() + reference = getattr(norm, path)(x, gate) + abs_d, rate = _disagreement(ours.forward(x, weight, gate), reference) + worst_abs, worst_rate = max(worst_abs, abs_d), max(worst_rate, rate) + + assert worst_abs <= 2e-2, f"worst |diff| vs {path} was {worst_abs:.3e}" + assert worst_rate <= 0.05, f"mismatch rate vs {path} was {worst_rate:.3%}" + + +# --------------------------------------------------------------------------- # +# 3. Batch invariance -- the property we DO claim +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("path", ["forward_native", "forward_cuda"]) +def test_vllm_gated_norm_is_batch_invariant(vllm_config_ctx, path): + """If this ever fails, vLLM stops being a coherent L2 target. + + Kept as a guard rather than a claim about our code: a provider whose output + depends on unrelated rows cannot anchor a bitwise contract. + """ + dtype = torch.bfloat16 + norm = _make_norm().to("cuda", dtype) + for seed in range(8): + x, gate, weight = _inputs(seed, dtype) + norm.weight.data = weight.clone() + full = getattr(norm, path)(x, gate) + for n in (1, 2, 8, 16, 32, 48, 64, 256): + sliced = getattr(norm, path)(x[:n], gate[:n]) + assert torch.equal(sliced, full[:n]), f"{path} seed={seed} n={n}" + + +def test_our_gated_op_is_batch_invariant(): + """The L1 claim for this operator, bitwise.""" + dtype = torch.bfloat16 + ours = Qwen3NextRMSNormGatedOp() + for seed in range(8): + x, gate, weight = _inputs(seed, dtype) + full = ours.forward(x, weight, gate) + for n in (1, 2, 8, 16, 32, 48, 64, 256): + assert torch.equal(ours.forward(x[:n], weight, gate[:n]), full[:n]) diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py new file mode 100644 index 000000000..c66bf2ba7 --- /dev/null +++ b/tests/test_qwen3_next_norm.py @@ -0,0 +1,502 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""WS1 C1 (RFC #428): Qwen3-Next RMSNorm conventions and batch invariance. + +Claim levels exercised here (RFC #428 section 2.1): + * L0 repeatable -- identical inputs reproduce bitwise identical outputs. + * L1 batch-invariant -- a row is unaffected by unrelated rows, padding or order. + +L2 (train-rollout exact against vLLM) is NOT claimed by this file; it needs the +rollout engine on the other side. + +The reduction order here is the repo's fixed 32-wide chunked reduction, which +differs from upstream's ``mean(-1)``. What is reproduced exactly is the weight +convention and the cast order, so comparisons against the upstream formula are +tolerance based while the invariance assertions are bitwise. +""" + +from __future__ import annotations + +import pytest +import torch +import torch.nn.functional as F + +from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import ( + Qwen3NextRMSNormGatedHFOp, + Qwen3NextRMSNormGatedOp, + Qwen3NextRMSNormOp, +) + +# Qwen3-Next-80B-A3B-Instruct config.json +_HIDDEN = 2048 # hidden_size +_HEAD_V_DIM = 128 # linear_value_head_dim -- the gated norm width +_EPS = 1e-6 # rms_norm_eps + + +def _rand(shape, seed): + g = torch.Generator().manual_seed(seed) + return torch.randn(*shape, generator=g, dtype=torch.float32) + + +# --------------------------------------------------------------------------- # +# Upstream formulas, transcribed from transformers/models/qwen3_next. +# These pin the exact semantics; they are deliberately verbatim. +# --------------------------------------------------------------------------- # +def _hf_rms_norm(x, weight, eps=_EPS): + output = x.float() * torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + eps) + output = output * (1.0 + weight.float()) + return output.type_as(x) + + +def _vllm_rms_norm_gated(x, weight, gate, eps=_EPS): + """vLLM ``RMSNormGated.forward_static`` with ``norm_before_gate=True``.""" + orig_dtype = x.dtype + x, weight, z = x.float(), weight.float(), gate.float() + variance = x.pow(2).mean(dim=-1, keepdim=True) + out = (x * torch.rsqrt(variance + eps)) * weight + out = out * F.silu(z) + return out.to(orig_dtype) + + +def _hf_rms_norm_gated(x, weight, gate, eps=_EPS): + input_dtype = x.dtype + h = x.to(torch.float32) + variance = h.pow(2).mean(-1, keepdim=True) + h = h * torch.rsqrt(variance + eps) + h = weight * h.to(input_dtype) + h = h * F.silu(gate.to(torch.float32)) + return h.to(input_dtype) + + +# --------------------------------------------------------------------------- # +# 1. The zero-centred (1 + w) convention +# --------------------------------------------------------------------------- # +def test_zero_weight_is_identity_scaling(): + """weight == 0 must leave the normalized value untouched, bitwise. + + This is what separates the zero-centred convention from the plain one: a + plain RMSNorm returns all zeros for a zero weight, this one returns the + bare normalized value. + """ + from rl_engine.kernels.ops.pytorch.norm.rms_norm import ( + NativeRMSNormOp, + shape_invariant_rstd, + ) + + x = _rand((4, _HIDDEN), seed=0) + zeros = torch.zeros(_HIDDEN) + + x_f = x.float() + bare = x_f * shape_invariant_rstd(x_f, _EPS).unsqueeze(-1) + assert torch.equal(Qwen3NextRMSNormOp().forward_fp32(x, zeros, eps=_EPS), bare) + + # ... and the plain convention really does differ here. + assert torch.equal( + NativeRMSNormOp().forward_fp32(x, zeros, eps=_EPS), torch.zeros_like(bare) + ) + + +def test_weight_offset_is_one(): + """w and w+1 scaling relationship: out(w) == norm * (1 + w).""" + op = Qwen3NextRMSNormOp() + x = _rand((2, _HEAD_V_DIM), seed=1) + unit = op.forward_fp32(x, torch.zeros(_HEAD_V_DIM)) # scale = 1 + doubled = op.forward_fp32(x, torch.ones(_HEAD_V_DIM)) # scale = 2 + torch.testing.assert_close(doubled, 2.0 * unit, atol=1e-6, rtol=1e-6) + + +def test_offset_applied_in_fp32_not_folded_into_bf16(): + """The 1 + w offset must not be pre-rounded through bf16. + + A weight one bf16 ULP below zero stays distinguishable from exactly zero + once the offset is added in fp32; folding (1 + w) into bf16 first would + collapse both to 1.0 and lose the difference. + """ + op = Qwen3NextRMSNormOp() + x = _rand((2, _HEAD_V_DIM), seed=2) + tiny = torch.full((_HEAD_V_DIM,), -(2**-9), dtype=torch.bfloat16) + folded = (1.0 + tiny.float()).bfloat16() # the wrong way + assert not torch.equal( + op.forward_fp32(x, tiny), + op.forward_fp32(x, folded - 1.0), + ) + + +# --------------------------------------------------------------------------- # +# 2. L1 -- batch invariance, bitwise +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("hidden", [_HIDDEN, _HEAD_V_DIM]) +def test_batch_invariance_slice(hidden): + op = Qwen3NextRMSNormOp() + w, x = _rand((hidden,), seed=3), _rand((8, 32, hidden), seed=4) + full = op.forward_fp32(x, w) + assert torch.equal(op.forward_fp32(x[:1], w), full[:1]) + assert torch.equal(op.forward_fp32(x[3:5], w), full[3:5]) + + +@pytest.mark.parametrize("batch", [1, 2, 8, 16, 32, 48, 64]) +def test_batch_invariance_across_concurrency(batch): + """RFC #428 section 10: batch size / concurrency axis 1..64.""" + op = Qwen3NextRMSNormOp() + w = _rand((_HIDDEN,), seed=5) + target = _rand((1, _HIDDEN), seed=6) + others = _rand((batch - 1, _HIDDEN), seed=7) if batch > 1 else None + alone = op.forward_fp32(target, w) + for position in ("first", "last"): + if others is None: + batched, index = target, 0 + elif position == "first": + batched, index = torch.cat([target, others]), 0 + else: + batched, index = torch.cat([others, target]), batch - 1 + assert torch.equal(op.forward_fp32(batched, w)[index : index + 1], alone) + + +def test_batch_invariance_with_padding(): + op = Qwen3NextRMSNormOp() + w = _rand((_HIDDEN,), seed=8) + x = _rand((4, _HIDDEN), seed=9) + padded = torch.cat([x, _rand((6, _HIDDEN), seed=10)], dim=0) + assert torch.equal(op.forward_fp32(padded, w)[:4], op.forward_fp32(x, w)) + + +def test_gated_batch_invariance_slice(): + op = Qwen3NextRMSNormGatedOp() + w = _rand((_HEAD_V_DIM,), seed=11) + x = _rand((8, 32, _HEAD_V_DIM), seed=12) + gate = _rand((8, 32, _HEAD_V_DIM), seed=13) + full = op.forward_fp32(x, w, gate) + assert torch.equal(op.forward_fp32(x[:1], w, gate[:1]), full[:1]) + assert torch.equal(op.forward_fp32(x[3:5], w, gate[3:5]), full[3:5]) + + +# --------------------------------------------------------------------------- # +# 2b. The fixed-order reduction is what buys invariance -- on CUDA too +# --------------------------------------------------------------------------- # +@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required") +@pytest.mark.parametrize("hidden", [_HIDDEN, _HEAD_V_DIM]) +def test_chunked_reduction_is_slice_invariant_on_device(hidden): + """The rstd statistic must be slice-invariant on the accelerator, every seed. + + Measured counterpoint on B200/bf16/H=2048: a plain ``mean(-1)`` broke this on + 1 of 20 seeds. We assert only the property we rely on -- asserting that torch's + ``mean`` is broken would be a brittle test of someone else's kernel. + """ + from rl_engine.kernels.ops.pytorch.norm.rms_norm import shape_invariant_rstd + + for seed in range(20): + g = torch.Generator(device="cuda").manual_seed(seed) + x = torch.randn(64, hidden, device="cuda", dtype=torch.bfloat16, generator=g) + full = shape_invariant_rstd(x.float(), _EPS) + assert torch.equal(shape_invariant_rstd(x[3:5].float(), _EPS), full[3:5]), seed + assert torch.equal(shape_invariant_rstd(x[:1].float(), _EPS), full[:1]), seed + + +@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required") +def test_op_batch_invariance_on_device(): + """L1 for the op itself, on the accelerator, in the model's dtype.""" + op = Qwen3NextRMSNormOp() + g = torch.Generator(device="cuda").manual_seed(0) + x = torch.randn(64, _HIDDEN, device="cuda", dtype=torch.bfloat16, generator=g) + w = torch.randn(_HIDDEN, device="cuda", dtype=torch.bfloat16, generator=g) + full = op.forward(x, w) + for n in (1, 2, 8, 16, 32, 48, 64): + assert torch.equal(op.forward(x[:n], w), full[:n]), n + + +# --------------------------------------------------------------------------- # +# 3. L0 -- repeatable +# --------------------------------------------------------------------------- # +def test_deterministic_repeat(): + op = Qwen3NextRMSNormOp() + x, w = _rand((64, _HIDDEN), seed=14), _rand((_HIDDEN,), seed=15) + first = op.forward_fp32(x, w) + for _ in range(10): + assert torch.equal(op.forward_fp32(x, w), first) + + +# --------------------------------------------------------------------------- # +# 4. Agreement with the upstream formula (tolerance, not bitwise: the reduction +# order differs by design) +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("hidden", [_HIDDEN, _HEAD_V_DIM]) +def test_matches_upstream_formula_fp32(hidden): + op = Qwen3NextRMSNormOp() + x, w = _rand((4, 16, hidden), seed=16), _rand((hidden,), seed=17) + torch.testing.assert_close(op.forward_fp32(x, w), _hf_rms_norm(x, w), atol=1e-6, rtol=1e-6) + + +def test_gated_strict_matches_vllm_formula_fp32(): + """The strict gated op follows vLLM, which is what the L2 claim targets.""" + op = Qwen3NextRMSNormGatedOp() + x = _rand((4, 16, _HEAD_V_DIM), seed=18) + w = _rand((_HEAD_V_DIM,), seed=19) + gate = _rand((4, 16, _HEAD_V_DIM), seed=20) + torch.testing.assert_close( + op.forward_fp32(x, w, gate), + _vllm_rms_norm_gated(x, w, gate), + atol=1e-6, + rtol=1e-6, + ) + + +def test_gated_hf_witness_matches_hf_formula_fp32(): + op = Qwen3NextRMSNormGatedHFOp() + x = _rand((4, 16, _HEAD_V_DIM), seed=18) + w = _rand((_HEAD_V_DIM,), seed=19) + gate = _rand((4, 16, _HEAD_V_DIM), seed=20) + torch.testing.assert_close( + op.forward_fp32(x, w, gate), + _hf_rms_norm_gated(x, w, gate), + atol=1e-6, + rtol=1e-6, + ) + + +def test_gated_conventions_diverge_in_low_precision(): + """Pin the HF-vs-vLLM gated divergence (RFC #428 first-divergence boundary). + + With fp32 inputs the two conventions coincide, because the HF round-trip is + an identity. In bf16 they do not, and the gap is far larger than a ULP: this + is why the convention is part of the operator identity and not a detail. + """ + strict, witness = Qwen3NextRMSNormGatedOp(), Qwen3NextRMSNormGatedHFOp() + x = _rand((64, _HEAD_V_DIM), seed=33).bfloat16() + w = _rand((_HEAD_V_DIM,), seed=34).bfloat16() + gate = _rand((64, _HEAD_V_DIM), seed=35).bfloat16() + + a, b = strict.forward(x, w, gate), witness.forward(x, w, gate) + assert not torch.equal(a, b), "the two gated conventions must be distinguishable" + assert (a.float() - b.float()).abs().max() > 1e-3 + + # ... and in fp32 the round-trip vanishes, so they agree bitwise. + xf, wf, gf = x.float(), w.float(), gate.float() + assert torch.equal(strict.forward(xf, wf, gf), witness.forward(xf, wf, gf)) + + +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) +def test_matches_upstream_formula_low_precision(dtype): + op = Qwen3NextRMSNormOp() + x = _rand((4, 16, _HIDDEN), seed=21).to(dtype) + w = _rand((_HIDDEN,), seed=22).to(dtype) + got, ref = op.forward(x, w), _hf_rms_norm(x, w) + assert got.dtype == ref.dtype == dtype + torch.testing.assert_close(got.float(), ref.float(), atol=2e-2, rtol=1.6e-2) + + +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) +def test_gated_hf_witness_low_precision(dtype): + """Covers the mid-computation cast back to the input dtype.""" + op = Qwen3NextRMSNormGatedHFOp() + x = _rand((4, 16, _HEAD_V_DIM), seed=23).to(dtype) + w = _rand((_HEAD_V_DIM,), seed=24).to(dtype) + gate = _rand((4, 16, _HEAD_V_DIM), seed=25).to(dtype) + got, ref = op.forward(x, w, gate), _hf_rms_norm_gated(x, w, gate) + assert got.dtype == ref.dtype == dtype + torch.testing.assert_close(got.float(), ref.float(), atol=2e-2, rtol=1.6e-2) + + +@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) +def test_gated_strict_low_precision(dtype): + op = Qwen3NextRMSNormGatedOp() + x = _rand((4, 16, _HEAD_V_DIM), seed=23).to(dtype) + w = _rand((_HEAD_V_DIM,), seed=24).to(dtype) + gate = _rand((4, 16, _HEAD_V_DIM), seed=25).to(dtype) + got, ref = op.forward(x, w, gate), _vllm_rms_norm_gated(x, w, gate) + assert got.dtype == ref.dtype == dtype + torch.testing.assert_close(got.float(), ref.float(), atol=2e-2, rtol=1.6e-2) + + +# --------------------------------------------------------------------------- # +# 5. dtype paths and guards +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16, torch.float16]) +def test_dtype_paths(dtype): + op = Qwen3NextRMSNormOp() + x = _rand((2, 16, _HIDDEN), seed=26).to(dtype) + w = _rand((_HIDDEN,), seed=27).to(dtype) + assert op.forward(x, w).dtype == dtype + assert op.forward_fp32(x, w).dtype == torch.float32 + + +def test_eps_inside_sqrt(): + op = Qwen3NextRMSNormOp() + out = op.forward_fp32(torch.zeros(1, _HIDDEN), torch.zeros(_HIDDEN)) + assert torch.isfinite(out).all() + assert torch.equal(out, torch.zeros(1, _HIDDEN)) + + +def test_bad_weight_shape_raises(): + op = Qwen3NextRMSNormOp() + with pytest.raises(ValueError, match="weight must be 1-D"): + op.forward_fp32(_rand((2, _HIDDEN), seed=28), _rand((_HIDDEN - 1,), seed=29)) + + +def test_gated_shape_mismatch_raises(): + op = Qwen3NextRMSNormGatedOp() + x, w = _rand((2, _HEAD_V_DIM), seed=30), _rand((_HEAD_V_DIM,), seed=31) + with pytest.raises(ValueError, match="gate must match x"): + op.forward_fp32(x, w, _rand((3, _HEAD_V_DIM), seed=32)) + + +# --------------------------------------------------------------------------- # +# 6. CUDA kernel: the offset is applied in fp32 inside the kernel +# --------------------------------------------------------------------------- # +_CUDA_RMSNORM = False +if torch.cuda.is_available(): # pragma: no branch - probe only + try: + from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE + + _CUDA_RMSNORM = _EXT_AVAILABLE and hasattr(_C, "rmsnorm_forward") + except ImportError: # pragma: no cover + _CUDA_RMSNORM = False + +requires_cuda_rmsnorm = pytest.mark.skipif( + not _CUDA_RMSNORM, reason="CUDA RMSNorm extension is not available" +) + + +@requires_cuda_rmsnorm +def test_cuda_offset_equals_explicit_fp32_weight(): + """``weight_offset=1.0`` must equal passing an fp32 ``1 + w``, bitwise. + + This is the correctness proof for doing the offset inside the kernel: it is + the same arithmetic as the fp32 reference, not an approximation of it. + """ + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_cuda + + torch.manual_seed(0) + x = torch.randn(512, _HIDDEN, device="cuda", dtype=torch.bfloat16) + w = torch.randn(_HIDDEN, device="cuda", dtype=torch.bfloat16) + + offset = rmsnorm_cuda(x, w, eps=_EPS, weight_offset=1.0) + explicit = rmsnorm_cuda(x, (1.0 + w.float()), eps=_EPS) + assert torch.equal(offset, explicit) + + +@requires_cuda_rmsnorm +def test_cuda_offset_is_not_folded_through_bfloat16(): + """Pre-rounding ``1 + w`` to bf16 must give a different answer. + + If this ever becomes equal, the offset has stopped being applied in fp32 and + the zero-centred contract is silently broken. + """ + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_cuda + + torch.manual_seed(0) + x = torch.randn(512, _HIDDEN, device="cuda", dtype=torch.bfloat16) + w = torch.randn(_HIDDEN, device="cuda", dtype=torch.bfloat16) + + offset = rmsnorm_cuda(x, w, eps=_EPS, weight_offset=1.0) + folded = rmsnorm_cuda(x, (1.0 + w.float()).bfloat16(), eps=_EPS) + assert not torch.equal(offset, folded) + + +@requires_cuda_rmsnorm +def test_cuda_default_offset_preserves_plain_convention(): + """An unset offset must leave the existing kernel behaviour untouched.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import RMSNormCudaOp, rmsnorm_cuda + + torch.manual_seed(0) + x = torch.randn(256, _HEAD_V_DIM, device="cuda", dtype=torch.bfloat16) + w = torch.randn(_HEAD_V_DIM, device="cuda", dtype=torch.bfloat16) + assert torch.equal(RMSNormCudaOp().forward(x, w, eps=_EPS), rmsnorm_cuda(x, w, eps=_EPS)) + + +@requires_cuda_rmsnorm +@pytest.mark.parametrize("batch", [1, 2, 8, 16, 32, 48, 64, 512]) +def test_cuda_zero_centred_batch_invariance(batch): + """L1 for the zero-centred CUDA op across the RFC #428 concurrency axis.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormCudaOp + + op = Qwen3NextRMSNormCudaOp() + torch.manual_seed(0) + x = torch.randn(512, _HIDDEN, device="cuda", dtype=torch.bfloat16) + w = torch.randn(_HIDDEN, device="cuda", dtype=torch.bfloat16) + assert torch.equal(op.forward(x[:batch], w, eps=_EPS), op.forward(x, w, eps=_EPS)[:batch]) + + +@requires_cuda_rmsnorm +def test_cuda_zero_centred_within_tolerance_of_golden(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormCudaOp + + torch.manual_seed(0) + x = torch.randn(256, _HIDDEN, device="cuda", dtype=torch.bfloat16) + w = torch.randn(_HIDDEN, device="cuda", dtype=torch.bfloat16) + got = Qwen3NextRMSNormCudaOp().forward(x, w, eps=_EPS) + ref = Qwen3NextRMSNormOp().forward_fp32(x, w, eps=_EPS) + torch.testing.assert_close(got.float(), ref, atol=2e-2, rtol=1.6e-2) + + +@requires_cuda_rmsnorm +def test_cuda_zero_centred_backward_is_offset_aware(): + """dx must see (1 + w); dw is offset-independent since d/dw (1+w) == d/dw w.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_cuda + + torch.manual_seed(0) + x = torch.randn(128, _HEAD_V_DIM, device="cuda", dtype=torch.float32) + w = torch.randn(_HEAD_V_DIM, device="cuda", dtype=torch.float32) + dy = torch.randn(128, _HEAD_V_DIM, device="cuda", dtype=torch.float32) + + grads = {} + for name, off in (("plain", 0.0), ("zero_centred", 1.0)): + xg = x.clone().requires_grad_(True) + wg = w.clone().requires_grad_(True) + rmsnorm_cuda(xg, wg, eps=_EPS, weight_offset=off).backward(dy.clone()) + grads[name] = (xg.grad.clone(), wg.grad.clone()) + + assert torch.isfinite(grads["zero_centred"][0]).all() + assert not torch.equal(grads["plain"][0], grads["zero_centred"][0]) # dx differs + assert torch.equal(grads["plain"][1], grads["zero_centred"][1]) # dw does not + + +# --------------------------------------------------------------------------- # +# 8. `__call__` is the documented entry point and must agree with `forward` +# --------------------------------------------------------------------------- # +def test_call_matches_forward(): + x, w = _rand((4, _HIDDEN), seed=40), _rand((_HIDDEN,), seed=41) + op = Qwen3NextRMSNormOp() + assert torch.equal(op(x, w, eps=_EPS), op.forward(x, w, eps=_EPS)) + + +@pytest.mark.parametrize("cls", [Qwen3NextRMSNormGatedOp, Qwen3NextRMSNormGatedHFOp]) +def test_gated_call_matches_forward(cls): + x = _rand((4, _HEAD_V_DIM), seed=42) + w = _rand((_HEAD_V_DIM,), seed=43) + gate = _rand((4, _HEAD_V_DIM), seed=44) + op = cls() + assert torch.equal(op(x, w, gate, eps=_EPS), op.forward(x, w, gate, eps=_EPS)) + + +def test_zero_centred_op_inherits_the_plain_reference(): + """The only difference from the plain op is the weight convention. + + Pins the inheritance: if the subclass ever grows its own `_rms_norm`, this + stops being true and the two references can drift apart silently. + """ + from rl_engine.kernels.ops.pytorch.norm.rms_norm import NativeRMSNormOp + + assert issubclass(Qwen3NextRMSNormOp, NativeRMSNormOp) + assert Qwen3NextRMSNormOp.weight_offset == 1.0 + assert NativeRMSNormOp.weight_offset == 0.0 + assert Qwen3NextRMSNormOp._rms_norm.__func__ is NativeRMSNormOp._rms_norm.__func__ + + +def test_plain_reference_is_unchanged_by_the_offset_plumbing(): + """Adding `weight_offset` to the base must not perturb the plain path. + + `0.0 + w` rewrites -0.0 to +0.0, which torch.equal does not notice, so this + compares the raw bits. + """ + from rl_engine.kernels.ops.pytorch.norm.rms_norm import NativeRMSNormOp + + x = _rand((2, _HEAD_V_DIM), seed=45) + w = torch.zeros(_HEAD_V_DIM) + w[0] = -0.0 + got = NativeRMSNormOp().forward_fp32(x, w, eps=_EPS) + x_f = x.float() + from rl_engine.kernels.ops.pytorch.norm.rms_norm import shape_invariant_rstd + + expected = (x_f * shape_invariant_rstd(x_f, _EPS).unsqueeze(-1)) * w.float() + assert torch.equal(got.view(torch.int32), expected.view(torch.int32)) From 5f5654a99268ba7e7bfc0aef646bf46e1c7d379d Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:15:09 +0000 Subject: [PATCH 06/44] fix(norm): preserve signed zero and use CUDA statistics for parameter VJPs Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- csrc/cuda/rmsnorm.cu | 9 ++-- rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 17 +++++--- tests/test_qwen3_next_norm.py | 51 +++++++++++++++++++--- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index fc6c425d6..5b570c557 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -142,7 +142,8 @@ __global__ void rmsnorm_fwd_kernel( // round the offset and break the bitwise contract. for (int col = tid; col < H; col += blockDim.x) { float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col) + weight_offset; + float wv = load_as_float(weight + col); + if (weight_offset != 0.0f) wv += weight_offset; float out = xv * row_rstd * wv; store_from_float(y_row + col, out); } @@ -172,7 +173,8 @@ __global__ void rmsnorm_bwd_dx_kernel( for (int col = tid; col < H; col += blockDim.x) { float dyv = load_as_float(dy_row + col); float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col) + weight_offset; + float wv = load_as_float(weight + col); + if (weight_offset != 0.0f) wv += weight_offset; local_dot += dyv * wv * xv; } @@ -184,7 +186,8 @@ __global__ void rmsnorm_bwd_dx_kernel( for (int col = tid; col < H; col += blockDim.x) { float dyv = load_as_float(dy_row + col); float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col) + weight_offset; + float wv = load_as_float(weight + col); + if (weight_offset != 0.0f) wv += weight_offset; float out = r * dyv * wv - xv * coeff; store_from_float(dx_row + col, out); diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index 3b72e497d..7d5a7a754 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -129,6 +129,7 @@ def __init__(self) -> None: "rmsnorm_forward", "rmsnorm_backward_dx", ) + #: Added to the weight in fp32 inside the kernel. Subclasses override it; #: 0.0 is the plain convention. weight_offset = 0.0 @@ -139,16 +140,18 @@ def __call__(self, x, weight, *, eps=1e-6): def forward(self, x, weight, *, eps=1e-6): hidden = x.shape[-1] x_2d = x.contiguous().view(-1, hidden) - y_2d = rmsnorm_cuda( - x_2d, weight.contiguous(), eps=eps, weight_offset=self.weight_offset - ) + y_2d = rmsnorm_cuda(x_2d, weight.contiguous(), eps=eps, weight_offset=self.weight_offset) return y_2d.view_as(x) def parameter_vjp_contributions_fp32(self, *, x, weight, grad_output, eps=1e-6): - del weight - x32 = x.float() - rstd = torch.rsqrt(x32.square().mean(dim=-1) + float(eps)) - rows = grad_output.float() * x32 * rstd.unsqueeze(-1) + hidden = x.shape[-1] + _, rstd = _C.rmsnorm_forward( + x.contiguous().reshape(-1, hidden), + weight.contiguous(), + float(eps), + float(self.weight_offset), + ) + rows = rmsnorm_dweight_rows_fp32(x, grad_output, rstd=rstd.reshape(x.shape[:-1])) return {"weight": rows} diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index c66bf2ba7..9fdf02e73 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -79,10 +79,7 @@ def test_zero_weight_is_identity_scaling(): plain RMSNorm returns all zeros for a zero weight, this one returns the bare normalized value. """ - from rl_engine.kernels.ops.pytorch.norm.rms_norm import ( - NativeRMSNormOp, - shape_invariant_rstd, - ) + from rl_engine.kernels.ops.pytorch.norm.rms_norm import NativeRMSNormOp, shape_invariant_rstd x = _rand((4, _HIDDEN), seed=0) zeros = torch.zeros(_HIDDEN) @@ -92,9 +89,7 @@ def test_zero_weight_is_identity_scaling(): assert torch.equal(Qwen3NextRMSNormOp().forward_fp32(x, zeros, eps=_EPS), bare) # ... and the plain convention really does differ here. - assert torch.equal( - NativeRMSNormOp().forward_fp32(x, zeros, eps=_EPS), torch.zeros_like(bare) - ) + assert torch.equal(NativeRMSNormOp().forward_fp32(x, zeros, eps=_EPS), torch.zeros_like(bare)) def test_weight_offset_is_one(): @@ -500,3 +495,45 @@ def test_plain_reference_is_unchanged_by_the_offset_plumbing(): expected = (x_f * shape_invariant_rstd(x_f, _EPS).unsqueeze(-1)) * w.float() assert torch.equal(got.view(torch.int32), expected.view(torch.int32)) + + +@requires_cuda_rmsnorm +@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) +def test_cuda_plain_signed_zero_is_preserved(dtype): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_cuda + + x = torch.ones(2, 128, device="cuda", dtype=dtype) + x[1].neg_() + weight = torch.full((128,), -0.0, device="cuda", dtype=dtype) + actual = rmsnorm_cuda(x, weight) + expected = x * weight + bits = torch.int32 if dtype == torch.float32 else torch.int16 + assert torch.equal(actual.view(bits), expected.view(bits)) + + +@requires_cuda_rmsnorm +@pytest.mark.parametrize("offset", [0.0, 1.0]) +@pytest.mark.parametrize("shape", [(512, 128), (2, 3, 2048)]) +@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16]) +def test_cuda_parameter_contributions_reproduce_backward(offset, shape, dtype): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormCudaOp, RMSNormCudaOp + from rl_engine.kernels.ops.vjp_fp32 import reduce_rows_fp32 + + torch.manual_seed(128) + x = torch.randn(shape, device="cuda", dtype=dtype) + weight = torch.randn(shape[-1], device="cuda", dtype=dtype, requires_grad=True) + upstream = torch.randn_like(x) + op = RMSNormCudaOp() if offset == 0.0 else Qwen3NextRMSNormCudaOp() + op(x, weight).backward(upstream) + rows = op.parameter_vjp_contributions_fp32(x=x, weight=weight, grad_output=upstream)["weight"] + folded = reduce_rows_fp32(rows.reshape(-1, shape[-1])).to(dtype) + assert torch.equal(weight.grad, folded) + + +def test_zero_centred_cuda_constructor_rejects_missing_extension(monkeypatch): + from rl_engine.kernels.ops.cuda.norm import rmsnorm + + monkeypatch.setattr(rmsnorm, "_EXT_AVAILABLE", False) + monkeypatch.setattr(rmsnorm, "_C", None) + with pytest.raises(RuntimeError, match="requires the compiled"): + rmsnorm.Qwen3NextRMSNormCudaOp() From b11b957ef6eba3b1ae7ceab68dd342ff04a8b243 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:34:21 +0000 Subject: [PATCH 07/44] feat(cuda): gated RMSNorm kernel for the Qwen3-Next GDN block Adds the CUDA kernel behind `Qwen3NextRMSNormGatedOp`, and registers both it and the zero-centred decoder norm from the previous commit. y = x * rstd * (weight_offset + weight) * act(gate) Every multiply is fp32 with one cast at the store, matching vLLM's RMSNormGated with norm_before_gate=True and group_size=None -- the only configuration the GDN block constructs. Other configurations are rejected rather than approximated. `expf` is used rather than the `__expf` intrinsic: the fast intrinsic trades accuracy for speed and would move the result away from the fp32 reference. Forward reuses the existing block_reduce_sum / choose_threads(H), so for a fixed H the reduction tree is independent of the row count -- that is the L1 guarantee -- and `rstd` comes out bitwise identical to the ungated kernel for the same x, which is asserted. Backward is assembled from deterministic pieces: dx new kernel, the ungated dx with (w + offset) -> (w + offset) * act(z) dweight reuses rmsnorm_dweight_rows_fp32 + the ascending-row fp32 left fold dgate row-local and reduction-free, fp32 in the wrapper Both backwards route through one `_fold_dweight_rows` helper so the file keeps a single left-fold entrypoint, which tests/test_vjp_fp32.py pins, and one `_require_cuda_symbols` helper so the module has a single availability contract. The gated op is deliberately NOT a subclass of RMSNormCudaOp: it takes an extra required tensor, so it cannot stand in for one. Same reasoning as on the PyTorch side, and stated in its docstring so the question is not reopened. Registration: `rms_norm_gated` and `qwen3_next_rms_norm` in OP_SPECS (both with a cuda-sm90 candidate, as every other reduction spec carries), operator_inputs builders, OpBackend members and priority maps on all five platforms, test_dispatch assertions, and the WS1 registered-ops set in tests/test_ws1_gtest_gpu.py. Operator pages added per docs/operators/README.md, which states the page is part of the operator contract. Claim level: L0 repeatable and L1 batch-invariant. NOT L2 -- see tests/check_qwen3_next_norm_providers.py, which pins the dispatch facts and bounds the gap against vLLM rather than asserting equality. Verified on 2x B200 (sm_100, driver 580.126.20, torch 2.13.0+cu130, vllm 0.30.0): pytest tests/test_qwen3_next_norm.py -q pytest tests/check_qwen3_next_norm_providers.py -q python scripts/check_operator.py --op rms_norm_gated --candidate cuda \ --device cuda --dtype bf16 --check-grad -> pass_rate=1.0000 python scripts/check_operator.py --op qwen3_next_rms_norm --candidate cuda \ --device cuda --dtype bf16 --check-grad -> pass_rate=1.0000 pytest tests/ rl_engine/tests/ -q -p no:randomly \ --ignore=tests/test_rocm_aiter_api_contract.py Both new files pass, both operators report pass_rate=1.0000 on the CUDA candidate, and the full suite gains no failure. The ignored file fails to import on the merge-base as well. Absolute suite counts are reported in the PR description against a named base commit, not here: they shift whenever a sibling test is added, so a count frozen in a commit message goes stale the moment the branch is rebased. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- csrc/cuda/rmsnorm.cu | 197 ++++++++++++++++++ csrc/ops.cpp | 96 +++++++++ docs/.nav.yml | 1 + docs/operators/README.md | 1 + docs/operators/qwen3-next-rms-norm-gated.md | 114 +++++++++++ docs/operators/qwen3-next-rms-norm.md | 4 +- rl_engine/kernels/gtest/operator_inputs.py | 23 +++ rl_engine/kernels/gtest/operator_specs.py | 30 +++ rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 207 +++++++++++++++++-- rl_engine/kernels/registry.py | 32 +++ rl_engine/tests/test_dispatch.py | 36 ++++ tests/test_qwen3_next_norm.py | 215 ++++++++++++++++++++ tests/test_ws1_gtest_gpu.py | 2 + 13 files changed, 940 insertions(+), 18 deletions(-) create mode 100644 docs/operators/qwen3-next-rms-norm-gated.md diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index 0c09b3a4a..c80945300 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -198,6 +198,130 @@ __global__ void rmsnorm_bwd_dx_kernel( } +// --------------------------------------------------------------------------- +// Gated RMSNorm (Qwen3-Next GDN block). +// +// y = x * rstd * (weight_offset + weight) * act(gate) +// +// The gate activation and the weight multiply are both evaluated in fp32 and +// there is exactly one cast, at the store. This mirrors vLLM's RMSNormGated +// with norm_before_gate=True, which is how the GDN block constructs it. +// +// ACT selects the gate activation: 0 = silu/swish, 1 = sigmoid. expf (not the +// __expf intrinsic) is used deliberately -- the fast intrinsic trades accuracy +// for speed and would put the result further from the fp32 reference. +// --------------------------------------------------------------------------- + +template +__device__ __forceinline__ float gate_activation(float z) { + const float sigma = 1.0f / (1.0f + expf(-z)); + return (ACT == 0) ? z * sigma : sigma; +} + +template +__device__ __forceinline__ float gate_activation_grad(float z) { + const float sigma = 1.0f / (1.0f + expf(-z)); + // d/dz [z * sigma] = sigma * (1 + z * (1 - sigma)); d/dz [sigma] = sigma * (1 - sigma) + return (ACT == 0) ? sigma * (1.0f + z * (1.0f - sigma)) : sigma * (1.0f - sigma); +} + + +template +__global__ void rmsnorm_gated_fwd_kernel( + const scalar_t* __restrict__ x, + const weight_t* __restrict__ weight, + const scalar_t* __restrict__ gate, + scalar_t* __restrict__ y, + float* __restrict__ rstd, + int T, + int H, + float eps, + float weight_offset +) { + int row = blockIdx.x; + int tid = threadIdx.x; + + const scalar_t* x_row = x + row * H; + const scalar_t* gate_row = gate + row * H; + scalar_t* y_row = y + row * H; + + float local_sum = 0.0f; + + // The statistic is over x only; the gate never enters the reduction, so + // rstd here is bit-identical to the ungated kernel's for the same x. + for (int col = tid; col < H; col += blockDim.x) { + float xv = load_as_float(x_row + col); + local_sum += xv * xv; + } + + float sum = block_reduce_sum(local_sum); + + float row_rstd = rsqrtf(sum / static_cast(H) + eps); + + if (tid == 0) { + rstd[row] = row_rstd; + } + + __syncthreads(); + + for (int col = tid; col < H; col += blockDim.x) { + float xv = load_as_float(x_row + col); + float wv = load_as_float(weight + col) + weight_offset; + float zv = load_as_float(gate_row + col); + float out = xv * row_rstd * wv * gate_activation(zv); + store_from_float(y_row + col, out); + } +} + + +template +__global__ void rmsnorm_gated_bwd_dx_kernel( + const scalar_t* __restrict__ dy, + const scalar_t* __restrict__ x, + const weight_t* __restrict__ weight, + const scalar_t* __restrict__ gate, + const float* __restrict__ rstd, + scalar_t* __restrict__ dx, + int T, + int H, + float weight_offset +) { + int row = blockIdx.x; + int tid = threadIdx.x; + + const scalar_t* dy_row = dy + row * H; + const scalar_t* x_row = x + row * H; + const scalar_t* gate_row = gate + row * H; + scalar_t* dx_row = dx + row * H; + + float local_dot = 0.0f; + + // Identical to the ungated dx, with the per-column scale (w + offset) + // replaced by (w + offset) * act(gate): the gate is a constant wrt x. + for (int col = tid; col < H; col += blockDim.x) { + float dyv = load_as_float(dy_row + col); + float xv = load_as_float(x_row + col); + float wv = load_as_float(weight + col) + weight_offset; + float zv = load_as_float(gate_row + col); + local_dot += dyv * (wv * gate_activation(zv)) * xv; + } + + float dot = block_reduce_sum(local_dot); + + float r = rstd[row]; + float coeff = dot * r * r * r / static_cast(H); + + for (int col = tid; col < H; col += blockDim.x) { + float dyv = load_as_float(dy_row + col); + float xv = load_as_float(x_row + col); + float wv = load_as_float(weight + col) + weight_offset; + float zv = load_as_float(gate_row + col); + + float out = r * dyv * (wv * gate_activation(zv)) - xv * coeff; + store_from_float(dx_row + col, out); + } +} + template __global__ void rmsnorm_partial_dw_kernel( const scalar_t* __restrict__ dy, @@ -325,6 +449,79 @@ void rmsnorm_backward_dx_cuda( } +void rmsnorm_gated_forward_cuda( + torch::Tensor x, + torch::Tensor weight, + torch::Tensor gate, + torch::Tensor y, + torch::Tensor rstd, + double eps, + double weight_offset, + int64_t activation +) { + int T = x.size(0); + int H = x.size(1); + int threads = choose_threads(H); + size_t smem = threads * sizeof(float); + + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + + AT_DISPATCH_FLOATING_TYPES_AND2(at::kHalf, at::kBFloat16, x.scalar_type(), "rmsnorm_gated_forward_cuda", [&] { + using x_t = scalar_t; + AT_DISPATCH_FLOATING_TYPES_AND2(at::kHalf, at::kBFloat16, weight.scalar_type(), "rmsnorm_gated_forward_weight_cuda", [&] { + using w_t = scalar_t; + if (activation == 0) { + rmsnorm_gated_fwd_kernel<<>>( + x.data_ptr(), weight.data_ptr(), gate.data_ptr(), + y.data_ptr(), rstd.data_ptr(), + T, H, static_cast(eps), static_cast(weight_offset)); + } else { + rmsnorm_gated_fwd_kernel<<>>( + x.data_ptr(), weight.data_ptr(), gate.data_ptr(), + y.data_ptr(), rstd.data_ptr(), + T, H, static_cast(eps), static_cast(weight_offset)); + } + }); + }); +} + + +void rmsnorm_gated_backward_dx_cuda( + torch::Tensor dy, + torch::Tensor x, + torch::Tensor weight, + torch::Tensor gate, + torch::Tensor rstd, + torch::Tensor dx, + double weight_offset, + int64_t activation +) { + int T = x.size(0); + int H = x.size(1); + int threads = choose_threads(H); + size_t smem = threads * sizeof(float); + + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + + AT_DISPATCH_FLOATING_TYPES_AND2(at::kHalf, at::kBFloat16, x.scalar_type(), "rmsnorm_gated_backward_dx_cuda", [&] { + using x_t = scalar_t; + AT_DISPATCH_FLOATING_TYPES_AND2(at::kHalf, at::kBFloat16, weight.scalar_type(), "rmsnorm_gated_backward_dx_weight_cuda", [&] { + using w_t = scalar_t; + if (activation == 0) { + rmsnorm_gated_bwd_dx_kernel<<>>( + dy.data_ptr(), x.data_ptr(), weight.data_ptr(), + gate.data_ptr(), rstd.data_ptr(), dx.data_ptr(), + T, H, static_cast(weight_offset)); + } else { + rmsnorm_gated_bwd_dx_kernel<<>>( + dy.data_ptr(), x.data_ptr(), weight.data_ptr(), + gate.data_ptr(), rstd.data_ptr(), dx.data_ptr(), + T, H, static_cast(weight_offset)); + } + }); + }); +} + void rmsnorm_backward_partial_dw_cuda( torch::Tensor dy, torch::Tensor x, diff --git a/csrc/ops.cpp b/csrc/ops.cpp index 4e786e5b3..b37ab102c 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -271,6 +271,26 @@ void rmsnorm_backward_dx_cuda( torch::Tensor dx, double weight_offset); +void rmsnorm_gated_forward_cuda( + torch::Tensor x, + torch::Tensor weight, + torch::Tensor gate, + torch::Tensor y, + torch::Tensor rstd, + double eps, + double weight_offset, + int64_t activation); + +void rmsnorm_gated_backward_dx_cuda( + torch::Tensor dy, + torch::Tensor x, + torch::Tensor weight, + torch::Tensor gate, + torch::Tensor rstd, + torch::Tensor dx, + double weight_offset, + int64_t activation); + void rmsnorm_backward_partial_dw_cuda( torch::Tensor dy, torch::Tensor x, @@ -342,6 +362,73 @@ torch::Tensor rmsnorm_backward_dx( return dx; } +static void rmsnorm_gated_check( + const torch::Tensor& x, + const torch::Tensor& weight, + const torch::Tensor& gate, + int64_t activation) +{ + rmsnorm_check_input(x, "x"); + rmsnorm_check_input(weight, "weight"); + rmsnorm_check_input(gate, "gate"); + + TORCH_CHECK(x.dim() == 2, "x must be 2D [T, H]"); + TORCH_CHECK(weight.dim() == 1, "weight must be 1D [H]"); + TORCH_CHECK(x.size(1) == weight.size(0), "x.size(1) must equal weight.size(0)"); + TORCH_CHECK(gate.sizes() == x.sizes(), "gate must have the same shape as x"); + TORCH_CHECK( + gate.scalar_type() == x.scalar_type(), + "gate must have the same dtype as x"); + // 0 = silu/swish, 1 = sigmoid. Anything else fails closed rather than + // silently computing a different activation (RFC #428 section 6, item 7). + TORCH_CHECK( + activation == 0 || activation == 1, + "activation must be 0 (silu) or 1 (sigmoid), got ", activation); +} + +std::vector rmsnorm_gated_forward( + torch::Tensor x, + torch::Tensor weight, + torch::Tensor gate, + double eps, + double weight_offset, + int64_t activation) +{ + rmsnorm_gated_check(x, weight, gate, activation); + + auto T = x.size(0); + auto y = torch::empty_like(x); + auto rstd = torch::empty({T}, x.options().dtype(torch::kFloat32)); + + rmsnorm_gated_forward_cuda(x, weight, gate, y, rstd, eps, weight_offset, activation); + + return {y, rstd}; +} + +torch::Tensor rmsnorm_gated_backward_dx( + torch::Tensor dy, + torch::Tensor x, + torch::Tensor weight, + torch::Tensor gate, + torch::Tensor rstd, + double weight_offset, + int64_t activation) +{ + rmsnorm_gated_check(x, weight, gate, activation); + rmsnorm_check_input(dy, "dy"); + rmsnorm_check_input(rstd, "rstd"); + + TORCH_CHECK(dy.sizes() == x.sizes(), "dy must have the same shape as x"); + TORCH_CHECK(rstd.dim() == 1 && rstd.size(0) == x.size(0), "rstd must be [T]"); + + auto dx = torch::empty_like(x); + + rmsnorm_gated_backward_dx_cuda( + dy, x, weight, gate, rstd, dx, weight_offset, activation); + + return dx; +} + torch::Tensor rmsnorm_backward_dw( torch::Tensor dy, torch::Tensor x, @@ -723,6 +810,15 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { py::arg("dy"), py::arg("x"), py::arg("weight"), py::arg("rstd"), py::arg("weight_offset") = 0.0); m.def("rmsnorm_backward_dw", &rmsnorm_backward_dw, "Deterministic RMSNorm backward dweight CUDA"); + m.def("rmsnorm_gated_forward", &rmsnorm_gated_forward, + "Batch-invariant gated RMSNorm forward CUDA", + py::arg("x"), py::arg("weight"), py::arg("gate"), py::arg("eps"), + py::arg("weight_offset") = 0.0, py::arg("activation") = 0); + m.def("rmsnorm_gated_backward_dx", &rmsnorm_gated_backward_dx, + "Batch-invariant gated RMSNorm backward dx CUDA", + py::arg("dy"), py::arg("x"), py::arg("weight"), py::arg("gate"), + py::arg("rstd"), py::arg("weight_offset") = 0.0, + py::arg("activation") = 0); #if !defined(USE_ROCM) m.def( "reduce_rows_fp32_left_fold", diff --git a/docs/.nav.yml b/docs/.nav.yml index 9133710de..88dea32e5 100644 --- a/docs/.nav.yml +++ b/docs/.nav.yml @@ -30,6 +30,7 @@ nav: - operators/det-gemm.md - operators/embedding.md - operators/qwen3-next-rms-norm.md + - operators/qwen3-next-rms-norm-gated.md - Developer Guide: - contributing/README.md - General: diff --git a/docs/operators/README.md b/docs/operators/README.md index 0d955ed18..0b93b3d90 100644 --- a/docs/operators/README.md +++ b/docs/operators/README.md @@ -33,4 +33,5 @@ Every operator page should include: - [Sampling](sampling.md) - [Token Embedding](embedding.md) - [Qwen3-Next RMSNorm (zero-centred)](qwen3-next-rms-norm.md) +- [Qwen3-Next Gated RMSNorm](qwen3-next-rms-norm-gated.md) - [Operator Doc Template](../contributing/operator-doc-template.md) diff --git a/docs/operators/qwen3-next-rms-norm-gated.md b/docs/operators/qwen3-next-rms-norm-gated.md new file mode 100644 index 000000000..1bc531ea6 --- /dev/null +++ b/docs/operators/qwen3-next-rms-norm-gated.md @@ -0,0 +1,114 @@ +# Qwen3-Next Gated RMSNorm + +## Summary + +The norm inside Qwen3-Next's Gated DeltaNet block: + +``` +out = x * rstd * weight * silu(gate) +``` + +It is a different operator from the decoder norm, not a variant of it: the weight is +plain rather than zero-centred (upstream initializes it to ones), it takes a second +input tensor, and — critically — `transformers` and vLLM disagree about where the +weight multiply happens. See [Qwen3-Next RMSNorm](qwen3-next-rms-norm.md) for the +decoder-norm convention. + +Added for RFC #428 C1 on the Qwen3-Next rollout-vs-replay path. + +## Entry Point + +```python +from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import ( + Qwen3NextRMSNormGatedOp, # strict / vLLM convention + Qwen3NextRMSNormGatedHFOp, # transformers convention, kept as a witness +) +from rl_engine.kernels.ops.cuda.norm.rmsnorm import ( + Qwen3NextRMSNormGatedCudaOp, + rmsnorm_gated_cuda, +) + +y = Qwen3NextRMSNormGatedCudaOp().forward(x, weight, gate, eps=1e-6) +y = rmsnorm_gated_cuda(x, weight, gate, eps=1e-6, activation="sigmoid") +``` + +## Backends + +| Backend | Wrapper | Native symbol | Status | +| --- | --- | --- | --- | +| CUDA | `Qwen3NextRMSNormGatedCudaOp` | `rl_engine._C.rmsnorm_gated_forward` | Supported | +| ROCm | — | — | Not implemented | +| PyTorch fallback | `Qwen3NextRMSNormGatedOp` | — | Supported (WS1 gold) | + +The CUDA op is deliberately **not** a subclass of `RMSNormCudaOp`: it takes an extra +required tensor, so it cannot stand in for one. + +## Tensor Contract + +| Argument | Shape | Dtype | Requirements | +| --- | --- | --- | --- | +| `x` | `[..., H]` | fp32 / bf16 / fp16 | contiguous on the CUDA path | +| `weight` | `[H]` | matches `x` | plain, NOT zero-centred | +| `gate` | same as `x` | same as `x` | shape and dtype are enforced | +| `eps` | scalar | float | `1e-6` for Qwen3-Next | +| `activation` | — | `"silu"`/`"swish"`/`"sigmoid"` | anything else is rejected | + +Only `norm_before_gate=True` and `group_size=None` are implemented — the single +configuration vLLM's GDN block constructs. Other configurations fail closed rather +than being approximated (RFC #428 §6 item 7). + +## Dispatch Behavior + +Registered as `rms_norm_gated`. CUDA prefers the kernel; every other platform +resolves to the PyTorch reference. `__init__` validates the compiled symbols, so on a +build without the extension the registry falls through instead of returning an op +that raises at call time. + +## Accuracy + +Claim level: **L0 repeatable, L1 batch-invariant**. L2 is not claimed. + +`transformers` casts the normalized value back to the input dtype *before* the weight +multiply; vLLM keeps it in fp32. On B200 / bf16 / `head_v_dim=128` the two differ in +35% of elements with `max|diff| = 6.25e-2`, and isolating the cast order alone +reproduces the gap — so the cast order dominates, not the reduction order. Because +RFC #428 measures L2 against vLLM rollout, the fp32 multiply is the strict default; +`Qwen3NextRMSNormGatedHFOp` keeps the other convention as a tested witness. + +"Bitwise equal to vLLM" is undefined until a provider is named: vLLM's own +`forward_native` and `forward_cuda` disagreed on 21 of 40 seeds (worst 1.56e-2 in +bf16; ~36% of elements at fp32 ULP in fp32). What this reproduces is the convention; +the residual is the reduction tree. + +`rstd` is bitwise identical to the ungated kernel's for the same `x` — asserted, so a +gate leaking into the statistic would fail. + +Backward is assembled from deterministic pieces: `dx` from a row-local kernel, +`dweight` from fp32 row contributions reduced by the ascending-row left fold, and +`dgate` elementwise in fp32 with no reduction. + +## Performance Notes + +Reuses the existing `block_reduce_sum` / `choose_threads(H)` reduction, so the gate +costs one extra load and one fp32 multiply per element. + +```bash +python scripts/check_operator.py --op rms_norm_gated --candidate cuda \ + --device cuda --dtype bf16 --check-grad +``` + +## Tests + +```bash +python -m pytest tests/test_qwen3_next_norm.py -v +# imports real vLLM, so it is not collected by default: +python -m pytest tests/check_qwen3_next_norm_providers.py -v +``` + +## Known Limitations + +- CUDA only; no ROCm, Ascend or Triton backend. +- `norm_before_gate=False` and grouped norms are not implemented. +- Not bitwise against either vLLM path (see Accuracy). +- Measured on sm_100 (B200); RFC #428 §2.2 forbids carrying the claim across + H100/H200/B100/B200. diff --git a/docs/operators/qwen3-next-rms-norm.md b/docs/operators/qwen3-next-rms-norm.md index 81fdde9b3..e5e9a47c0 100644 --- a/docs/operators/qwen3-next-rms-norm.md +++ b/docs/operators/qwen3-next-rms-norm.md @@ -9,8 +9,8 @@ offset away and silently breaks any bitwise claim. This operator exists for RFC #428 C1 (Embedding / RMSNorm / residual / final norm exactness) on the Qwen3-Next rollout-vs-replay path. The Gated DeltaNet block uses -a *different* weight convention and a different cast order; it is a separate -operator. +a *different* weight convention and a different cast order; see +[Gated RMSNorm](qwen3-next-rms-norm-gated.md). Upstream references: `transformers` `Qwen3NextRMSNorm`, and vLLM's `GemmaRMSNorm`, which `vllm/model_executor/models/qwen3_next.py` aliases as `Qwen3NextRMSNorm`. diff --git a/rl_engine/kernels/gtest/operator_inputs.py b/rl_engine/kernels/gtest/operator_inputs.py index ca3b7c120..1cd03d31f 100644 --- a/rl_engine/kernels/gtest/operator_inputs.py +++ b/rl_engine/kernels/gtest/operator_inputs.py @@ -26,6 +26,8 @@ def make_operator_inputs( ) -> dict[str, Any]: builders = { "rms_norm": _make_rms_norm_inputs, + "rms_norm_gated": _make_rms_norm_gated_inputs, + "qwen3_next_rms_norm": _make_rms_norm_inputs, "qk_norm": _make_qk_norm_inputs, "pack": _make_pack_inputs, "matmul": _make_matmul_inputs, @@ -54,6 +56,8 @@ def operator_shape_name(op_name: str, args: argparse.Namespace) -> str: vocab = _arg_int(args, "vocab", DEFAULT_VOCAB) names = { "rms_norm": f"{batch}x{seq}x{_normalized_dim(args)}", + "rms_norm_gated": f"{batch}x{seq}x{_arg_int(args, 'head_dim', DEFAULT_HEAD_DIM)}", + "qwen3_next_rms_norm": f"{batch}x{seq}x{_normalized_dim(args)}", "qk_norm": f"{batch}x{seq}x{_arg_int(args, 'n_heads', DEFAULT_N_HEADS)}x" f"{_arg_int(args, 'head_dim', DEFAULT_HEAD_DIM)}", "pack": f"{batch}x{seq}x{_normalized_dim(args)}", @@ -91,6 +95,25 @@ def _make_rms_norm_inputs( } +def _make_rms_norm_gated_inputs( + args: argparse.Namespace, dtype: torch.dtype, device: torch.device +) -> dict[str, Any]: + """Gated RMSNorm inputs at the GDN block's width. + + The gated norm normalizes over ``linear_value_head_dim`` (128 for + Qwen3-Next), not ``hidden_size``, so it follows ``head_dim`` rather than + ``normalized_dim``. + """ + batch, seq = _batch_seq(args) + head_dim = _arg_int(args, "head_dim", DEFAULT_HEAD_DIM) + return { + "x": _floating_tensor((batch, seq, head_dim), args, dtype, device, offset=0), + "weight": _floating_tensor((head_dim,), args, dtype, device, offset=1), + "gate": _floating_tensor((batch, seq, head_dim), args, dtype, device, offset=2), + "eps": _arg_float(args, "eps", DEFAULT_RMS_EPS), + } + + def _make_qk_norm_inputs( args: argparse.Namespace, dtype: torch.dtype, device: torch.device ) -> dict[str, Any]: diff --git a/rl_engine/kernels/gtest/operator_specs.py b/rl_engine/kernels/gtest/operator_specs.py index 4b86c2b7e..cccbc23b2 100644 --- a/rl_engine/kernels/gtest/operator_specs.py +++ b/rl_engine/kernels/gtest/operator_specs.py @@ -46,6 +46,36 @@ def _load_object(path: str) -> Any: }, grad_input_names=("x", "weight"), ), + "rms_norm_gated": OperatorSpec( + name="rms_norm_gated", + op_class="reduction", + gold_path=( + "rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm.Qwen3NextRMSNormGatedOp" + ), + gold_method="forward_fp32", + candidate_paths={ + "pytorch": ( + "rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm.Qwen3NextRMSNormGatedOp" + ), + "cuda": ("rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormGatedCudaOp"), + "cuda-sm90": ("rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormGatedCudaOp"), + }, + grad_input_names=("x", "weight", "gate"), + ), + "qwen3_next_rms_norm": OperatorSpec( + name="qwen3_next_rms_norm", + op_class="reduction", + gold_path=("rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm.Qwen3NextRMSNormOp"), + gold_method="forward_fp32", + candidate_paths={ + "pytorch": ( + "rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm.Qwen3NextRMSNormOp" + ), + "cuda": "rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormCudaOp", + "cuda-sm90": ("rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormCudaOp"), + }, + grad_input_names=("x", "weight"), + ), "qk_norm": OperatorSpec( name="qk_norm", op_class="reduction", diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index af9a31358..fc1aa9554 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -5,23 +5,14 @@ from rl_engine.kernels.ops.vjp_fp32 import reduce_rows_fp32, rmsnorm_dweight_rows_fp32 -def _require_cuda_symbols(what: str, *names: str) -> None: - """Raise when the compiled kernels backing ``what`` are missing. +def _fold_dweight_rows(rows: torch.Tensor, dtype: torch.dtype) -> torch.Tensor: + """The single left-fold entrypoint for this backend's dweight reductions. - The registry treats a backend whose construction raises as unavailable and - falls through to the next candidate, so calling this from ``__init__`` is - what lets a CUDA-first priority list degrade to the PyTorch reference on a - build without the extension. Mirrors ``_require_cuda_activation`` in the - activation ops. + Both the plain and the gated backward route through here so the file keeps + one auditable reduction path; the ascending-row fp32 fold is what makes + dweight independent of the batch layout. """ - if not _EXT_AVAILABLE or _C is None: - raise RuntimeError(f"{what} requires the compiled rl_engine._C extension.") - missing = [name for name in names if not hasattr(_C, name)] - if missing: - raise RuntimeError( - f"{what} symbols ({', '.join(missing)}) are not compiled into _C. " - "Rebuild the extension with csrc/cuda/rmsnorm.cu." - ) + return reduce_rows_fp32(rows).to(dtype) class RMSNormCuda(torch.autograd.Function): @@ -92,7 +83,7 @@ def backward(ctx, grad_out): # Multiplication is part of the pre-existing mask contract, including # IEEE propagation for non-finite inactive contributions. rows = rows * mask.to(dtype=rows.dtype).unsqueeze(-1) - dw = reduce_rows_fp32(rows).to(weight.dtype) + dw = _fold_dweight_rows(rows, weight.dtype) record_backward( "rms_norm", kernel_id=( @@ -166,3 +157,187 @@ class Qwen3NextRMSNormCudaOp(RMSNormCudaOp): """ weight_offset = 1.0 + + +# --------------------------------------------------------------------------- # +# Gated RMSNorm (Qwen3-Next GDN block) +# --------------------------------------------------------------------------- # + +def _require_cuda_symbols(what: str, *names: str) -> None: + """Raise when the compiled kernels backing ``what`` are missing. + + The registry treats a backend whose construction raises as unavailable and + falls through, so calling this from ``__init__`` is what lets a CUDA-first + priority list degrade to the PyTorch reference on a build without the + extension. Mirrors ``_require_cuda_activation`` in the activation ops. + """ + if not _EXT_AVAILABLE or _C is None: + raise RuntimeError(f"{what} requires the compiled rl_engine._C extension.") + missing = [name for name in names if not hasattr(_C, name)] + if missing: + raise RuntimeError( + f"{what} symbols ({', '.join(missing)}) are not compiled into _C. " + "Rebuild the extension with csrc/cuda/rmsnorm.cu." + ) + + +#: Gate activations understood by the CUDA kernel, in binding order. +_GATE_ACTIVATIONS = {"silu": 0, "swish": 0, "sigmoid": 1} + + +def _gate_activation_fp32(gate: torch.Tensor, activation: int) -> torch.Tensor: + """act(gate) in fp32, matching the kernel's ``gate_activation``.""" + gate32 = gate.float() + return torch.nn.functional.silu(gate32) if activation == 0 else torch.sigmoid(gate32) + + +def _gate_activation_grad_fp32(gate: torch.Tensor, activation: int) -> torch.Tensor: + """d act(gate) / d gate in fp32, matching ``gate_activation_grad``.""" + gate32 = gate.float() + sigma = torch.sigmoid(gate32) + if activation == 0: + return sigma * (1.0 + gate32 * (1.0 - sigma)) + return sigma * (1.0 - sigma) + + +class RMSNormGatedCuda(torch.autograd.Function): + """Autograd wrapper for the gated CUDA RMSNorm. + + Forward is the fused kernel. Backward is assembled from deterministic + pieces: ``dx`` from a row-local CUDA kernel, ``dweight`` from fp32 row + contributions reduced by the ascending-row left fold, and ``dgate`` purely + elementwise in fp32 (no reduction, so batch invariance is trivial). + """ + + @staticmethod + def forward(ctx, x, weight, gate, eps=1e-6, weight_offset=0.0, activation=0): + """ + Forward: + y = x * rsqrt(mean(x^2) + eps) * (weight_offset + weight) * act(gate) + + Input: + x, gate: [T, H], fp16/bf16/fp32 CUDA tensors of matching dtype + weight: [H] + activation: 0 = silu/swish, 1 = sigmoid + """ + assert x.is_cuda and weight.is_cuda and gate.is_cuda, "inputs must be CUDA tensors" + assert x.is_contiguous() and weight.is_contiguous() and gate.is_contiguous() + assert x.dim() == 2, "x must be [T, H]" + assert weight.dim() == 1, "weight must be [H]" + assert gate.shape == x.shape, "gate must match x" + assert _EXT_AVAILABLE and hasattr(_C, "rmsnorm_gated_forward"), ( + "Gated RMSNorm CUDA extension is unavailable. Rebuild with csrc/cuda/rmsnorm.cu." + ) + + y, rstd = _C.rmsnorm_gated_forward( + x, weight, gate, float(eps), float(weight_offset), int(activation) + ) + + ctx.save_for_backward(x, weight, gate, rstd) + ctx.eps = eps + ctx.weight_offset = float(weight_offset) + ctx.activation = int(activation) + + return y + + @staticmethod + def backward(ctx, grad_out): + x, weight, gate, rstd = ctx.saved_tensors + dy = grad_out.contiguous() + act = ctx.activation + + dx = _C.rmsnorm_gated_backward_dx( + dy, x, weight, gate, rstd, ctx.weight_offset, act + ) + + # dweight: the gate is a per-element constant here, so the ungated row + # contributions apply once dy carries act(gate). + gate_act = _gate_activation_fp32(gate, act) + rows = rmsnorm_dweight_rows_fp32(x, dy.float() * gate_act, rstd=rstd) + dw = _fold_dweight_rows(rows, weight.dtype) + + # dgate: row-local and reduction-free. + normed = x.float() * rstd.unsqueeze(-1) + scale = weight.float() + ctx.weight_offset + dgate = (dy.float() * normed * scale * _gate_activation_grad_fp32(gate, act)).to( + gate.dtype + ) + + record_backward( + "rms_norm_gated", + kernel_id=( + "rl_engine._C.rmsnorm_gated_backward_dx" + "+rl_engine.kernels.ops.vjp_fp32.rmsnorm_dweight_rows_fp32" + "+rl_engine.kernels.ops.vjp_fp32.reduce_rows_fp32" + ), + impl="cuda_rmsnorm_gated_dx_declared_fp32_rowfold_dw", + family="cuda", + ) + + return dx, dw, dgate, None, None, None + + +def rmsnorm_gated_cuda(x, weight, gate, eps=1e-6, weight_offset=0.0, activation="silu"): + """ + use: + y = rmsnorm_gated_cuda(x, weight, gate) + y = rmsnorm_gated_cuda(x, weight, gate, activation="sigmoid") + """ + if activation not in _GATE_ACTIVATIONS: + raise ValueError( + f"activation must be one of {sorted(_GATE_ACTIVATIONS)}, got {activation!r}" + ) + return RMSNormGatedCuda.apply( + x, weight, gate, eps, weight_offset, _GATE_ACTIVATIONS[activation] + ) + + +class Qwen3NextRMSNormGatedCudaOp: + """CUDA gated RMSNorm for the Qwen3-Next GDN block. + + Deliberately not a subclass of :class:`RMSNormCudaOp`: it takes an extra + required tensor, so it cannot stand in for one. + + ``out = x * rstd * weight * silu(gate)``, every multiply in fp32 with a + single cast at the store. The weight is plain, not zero-centred, matching + vLLM's ``RMSNormGated`` with ``norm_before_gate=True`` and ``group_size=None`` + -- which is exactly how the GDN block constructs it. Other configurations + are rejected rather than approximated. + """ + + backward_impl = "cuda_rmsnorm_gated_dx_declared_fp32_rowfold_dw" + + #: The gated weight is plain; kept as an attribute so the surface matches + #: the ungated op and a zero-centred variant stays one subclass away. + weight_offset = 0.0 + activation = "silu" + + def __init__(self) -> None: + _require_cuda_symbols( + "Gated CUDA RMSNorm", "rmsnorm_gated_forward", "rmsnorm_gated_backward_dx" + ) + + def __call__(self, x, weight, gate, *, eps=1e-6): + return self.forward(x, weight, gate, eps=eps) + + def forward(self, x, weight, gate, *, eps=1e-6): + hidden = x.shape[-1] + x_2d = x.contiguous().view(-1, hidden) + gate_2d = gate.contiguous().view(-1, hidden) + y_2d = rmsnorm_gated_cuda( + x_2d, + weight.contiguous(), + gate_2d, + eps=eps, + weight_offset=self.weight_offset, + activation=self.activation, + ) + return y_2d.view_as(x) + + def parameter_vjp_contributions_fp32(self, *, x, weight, gate, grad_output, eps=1e-6): + del weight + x32 = x.float() + rstd = torch.rsqrt(x32.square().mean(dim=-1) + float(eps)) + act = _GATE_ACTIVATIONS[self.activation] + rows = grad_output.float() * _gate_activation_fp32(gate, act) * x32 * rstd.unsqueeze(-1) + return {"weight": rows} diff --git a/rl_engine/kernels/registry.py b/rl_engine/kernels/registry.py index 3b935499a..f3aa51bb1 100644 --- a/rl_engine/kernels/registry.py +++ b/rl_engine/kernels/registry.py @@ -158,6 +158,18 @@ class OpBackend(Enum, metaclass=_KernelEnumMeta): TRITON_RMS_NORM = "rl_engine.kernels.ops.triton.rmsnorm_triton.RMSNormTritonOp" PYTORCH_NATIVE_RMS_NORM = "rl_engine.kernels.ops.pytorch.norm.rms_norm.NativeRMSNormOp" + # Zero-centred RMSNorm (Qwen3-Next decoder / final norm) + CUDA_QWEN3_NEXT_RMS_NORM = "rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormCudaOp" + PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM = ( + "rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm.Qwen3NextRMSNormOp" + ) + + # Gated RMSNorm (Qwen3-Next Gated DeltaNet block) + CUDA_RMS_NORM_GATED = "rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormGatedCudaOp" + PYTORCH_NATIVE_RMS_NORM_GATED = ( + "rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm.Qwen3NextRMSNormGatedOp" + ) + # Generic fallback TRITON_GENERIC = "rl_engine.kernels.ops.triton.generic.TritonOp" PYTORCH_ATTN = "rl_engine.kernels.ops.pytorch.attention.NativeAttentionOp" @@ -594,6 +606,14 @@ def __init__(self): OpBackend.CUDA_RMS_NORM, OpBackend.PYTORCH_NATIVE_RMS_NORM, ], + "rms_norm_gated": [ + OpBackend.CUDA_RMS_NORM_GATED, + OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED, + ], + "qwen3_next_rms_norm": [ + OpBackend.CUDA_QWEN3_NEXT_RMS_NORM, + OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM, + ], "lm_head": [OpBackend.PYTORCH_NATIVE_LM_HEAD], "embedding": [OpBackend.PYTORCH_NATIVE_EMBEDDING], "silu": [ @@ -649,6 +669,8 @@ def __init__(self): ], "matmul": [OpBackend.PYTORCH_NATIVE_MATMUL], "rms_norm": [OpBackend.PYTORCH_NATIVE_RMS_NORM], + "rms_norm_gated": [OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED], + "qwen3_next_rms_norm": [OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM], "lm_head": [OpBackend.PYTORCH_NATIVE_LM_HEAD], "embedding": [OpBackend.PYTORCH_NATIVE_EMBEDDING], "silu": [OpBackend.TRITON_SILU, OpBackend.PYTORCH_NATIVE_SILU], @@ -690,6 +712,8 @@ def __init__(self): OpBackend.TRITON_RMS_NORM, OpBackend.PYTORCH_NATIVE_RMS_NORM, ], + "rms_norm_gated": [OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED], + "qwen3_next_rms_norm": [OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM], "lm_head": [OpBackend.PYTORCH_NATIVE_LM_HEAD], "embedding": [ OpBackend.TRITON_EMBEDDING, @@ -718,6 +742,8 @@ def __init__(self): "batch_invariant_logp": [OpBackend.PYTORCH_BATCH_INVARIANT_LOGP], "matmul": [OpBackend.PYTORCH_NATIVE_MATMUL], "rms_norm": [OpBackend.PYTORCH_NATIVE_RMS_NORM], + "rms_norm_gated": [OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED], + "qwen3_next_rms_norm": [OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM], "lm_head": [OpBackend.PYTORCH_NATIVE_LM_HEAD], "embedding": [OpBackend.PYTORCH_NATIVE_EMBEDDING], "silu": [OpBackend.PYTORCH_NATIVE_SILU], @@ -758,6 +784,12 @@ def __init__(self): OpBackend.ASCEND_RMS_NORM, OpBackend.PYTORCH_NATIVE_RMS_NORM, ] + self._priority_map["npu"]["rms_norm_gated"] = [ + OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED, + ] + self._priority_map["npu"]["qwen3_next_rms_norm"] = [ + OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM, + ] self._priority_map["npu"]["embedding"] = [ OpBackend.ASCEND_EMBEDDING, OpBackend.PYTORCH_NATIVE_EMBEDDING, diff --git a/rl_engine/tests/test_dispatch.py b/rl_engine/tests/test_dispatch.py index d095619ee..ed873c2dd 100644 --- a/rl_engine/tests/test_dispatch.py +++ b/rl_engine/tests/test_dispatch.py @@ -4,6 +4,7 @@ import sys from types import ModuleType +import pytest import torch import rl_engine.platforms.device as device_module @@ -256,3 +257,38 @@ def test_executor_flow(): print("\n All infrastructure tests passed!") except Exception as e: print(f"\n Test failed with error: {e}") + + +def test_gated_rms_norm_priority_is_cuda_first_with_pytorch_fallback(): + """The gated norm has a CUDA kernel; every other platform falls back. + + No Triton, ROCm or Ascend gated kernel exists yet, so those platforms must + resolve to the PyTorch reference rather than to nothing -- an operator + missing from a priority map falls through to ``OpBackend.PYTORCH_NATIVE``, + which is the logprob op, not a norm. + """ + registry = KernelRegistry() + + assert registry._priority_map["cuda"]["rms_norm_gated"] == [ + OpBackend.CUDA_RMS_NORM_GATED, + OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED, + ] + for platform in ("rocm", "musa", "cpu", "npu"): + assert registry._priority_map[platform]["rms_norm_gated"] == [ + OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED + ], platform + + +def test_gated_rms_norm_cuda_backend_reports_absence_by_failing_construction(monkeypatch): + """Without the compiled symbols the CUDA backend must be skipped, not returned.""" + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormGatedOp + + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", False) + monkeypatch.setattr(cuda_rmsnorm, "_C", None) + with pytest.raises(RuntimeError, match="requires the compiled rl_engine._C extension"): + cuda_rmsnorm.Qwen3NextRMSNormGatedCudaOp() + + # ... and the registry must therefore hand out the reference, not raise. + resolved = KernelRegistry().get_op("rms_norm_gated") + assert isinstance(resolved, Qwen3NextRMSNormGatedOp) diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index a62d8410f..c1f7e9d73 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -458,6 +458,221 @@ def test_cuda_zero_centred_backward_is_offset_aware(): assert torch.equal(grads["plain"][1], grads["zero_centred"][1]) # dw does not +# --------------------------------------------------------------------------- # +# 7. Gated RMSNorm CUDA kernel (the GDN block's norm) +# --------------------------------------------------------------------------- # +_HAS_CUDA_GATED = False +if torch.cuda.is_available(): # pragma: no branch - probe only + try: + from rl_engine.kernels.ops.base import _C as _C_probe + from rl_engine.kernels.ops.base import _EXT_AVAILABLE as _EXT_probe + + _HAS_CUDA_GATED = _EXT_probe and hasattr(_C_probe, "rmsnorm_gated_forward") + except ImportError: # pragma: no cover + _HAS_CUDA_GATED = False + +requires_cuda_gated = pytest.mark.skipif( + not _HAS_CUDA_GATED, reason="gated RMSNorm CUDA extension is not available" +) + +# tolerance_contract.json, judgments/forward_accuracy/by_op_class/reduction/bfloat16 +_BF16_ATOL, _BF16_RTOL = 2e-2, 1.6e-2 + + +def _gated_cuda_inputs(seed=0, rows=512, hidden=_HEAD_V_DIM, dtype=torch.bfloat16): + g = torch.Generator(device="cuda").manual_seed(seed) + x = torch.randn(rows, hidden, device="cuda", dtype=dtype, generator=g) + gate = torch.randn(rows, hidden, device="cuda", dtype=dtype, generator=g) + weight = torch.randn(hidden, device="cuda", dtype=dtype, generator=g) + return x, weight, gate + + +@requires_cuda_gated +def test_cuda_gated_matches_golden_within_contract(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp + + x, w, gate = _gated_cuda_inputs() + got = Qwen3NextRMSNormGatedCudaOp().forward(x, w, gate, eps=_EPS) + ref = Qwen3NextRMSNormGatedOp().forward_fp32(x, w, gate, eps=_EPS) + torch.testing.assert_close(got.float(), ref, atol=_BF16_ATOL, rtol=_BF16_RTOL) + + +@requires_cuda_gated +def test_cuda_gated_rstd_is_bitwise_identical_to_plain_kernel(): + """The gate must not perturb the normalization statistic. + + Same x, same rstd, bitwise -- otherwise the gate has leaked into the + reduction and the two kernels no longer share a contract. + """ + from rl_engine.kernels.ops.base import _C + + x, w, gate = _gated_cuda_inputs() + _, rstd_gated = _C.rmsnorm_gated_forward(x, w, gate, _EPS, 0.0, 0) + _, rstd_plain = _C.rmsnorm_forward(x, w, _EPS, 0.0) + assert torch.equal(rstd_gated, rstd_plain) + + +@requires_cuda_gated +@pytest.mark.parametrize("batch", [1, 2, 8, 16, 32, 48, 64, 512]) +def test_cuda_gated_batch_invariance(batch): + """L1 across the RFC #428 concurrency axis, bitwise.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp + + op = Qwen3NextRMSNormGatedCudaOp() + x, w, gate = _gated_cuda_inputs() + full = op.forward(x, w, gate, eps=_EPS) + assert torch.equal(op.forward(x[:batch], w, gate[:batch], eps=_EPS), full[:batch]) + + +@requires_cuda_gated +def test_cuda_gated_zero_gate_zeroes_output(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp + + x, w, _ = _gated_cuda_inputs() + out = Qwen3NextRMSNormGatedCudaOp().forward(x, w, torch.zeros_like(x), eps=_EPS) + assert torch.equal(out, torch.zeros_like(out)) + + +@requires_cuda_gated +def test_cuda_gated_unit_weight_is_plain_norm_times_silu(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import ( + Qwen3NextRMSNormGatedCudaOp, + rmsnorm_cuda, + ) + + x, _, gate = _gated_cuda_inputs() + ones = torch.ones(_HEAD_V_DIM, device="cuda", dtype=x.dtype) + got = Qwen3NextRMSNormGatedCudaOp().forward(x, ones, gate, eps=_EPS).float() + ref = rmsnorm_cuda(x, ones, eps=_EPS).float() * F.silu(gate.float()) + torch.testing.assert_close(got, ref, atol=_BF16_ATOL, rtol=_BF16_RTOL) + + +@requires_cuda_gated +def test_cuda_gated_sigmoid_activation(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda, rmsnorm_cuda + + x, w, gate = _gated_cuda_inputs() + got = rmsnorm_gated_cuda(x, w, gate, eps=_EPS, activation="sigmoid").float() + ref = rmsnorm_cuda(x, w, eps=_EPS).float() * torch.sigmoid(gate.float()) + torch.testing.assert_close(got, ref, atol=_BF16_ATOL, rtol=_BF16_RTOL) + + +@requires_cuda_gated +def test_cuda_gated_backward_matches_autograd_golden(): + """dx, dweight and dgate against the fp32 reference's autograd.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + + x, w, gate = _gated_cuda_inputs() + x, w, gate = x.float(), w.float(), gate.float() + dy = torch.randn_like(x) + + got = [t.clone().requires_grad_(True) for t in (x, w, gate)] + rmsnorm_gated_cuda(*got, eps=_EPS).backward(dy) + ref = [t.clone().requires_grad_(True) for t in (x, w, gate)] + Qwen3NextRMSNormGatedOp().forward_fp32(*ref, eps=_EPS).backward(dy) + + for name, a, b in zip(("dx", "dweight", "dgate"), got, ref): + torch.testing.assert_close(a.grad, b.grad, atol=1e-4, rtol=1e-4, msg=name) + + +@requires_cuda_gated +@pytest.mark.parametrize("batch", [1, 8, 64, 512]) +def test_cuda_gated_dweight_is_batch_invariant(batch): + """A row's contribution to dweight must not depend on the batch it sits in. + + The ascending-row fp32 left fold makes dweight over the first n rows exactly + the fold of those n row-contributions -- so the n-row run must reproduce the + 512-row run's prefix, bitwise. + """ + from rl_engine.kernels.ops.base import _C + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + from rl_engine.kernels.ops.vjp_fp32 import reduce_rows_fp32 + + x, w, gate = _gated_cuda_inputs() + x, w, gate = x.float(), w.float(), gate.float() + dy = torch.randn_like(x) + + wg = w.clone().requires_grad_(True) + rmsnorm_gated_cuda(x[:batch], wg, gate[:batch], eps=_EPS).backward(dy[:batch]) + + # rstd from the full-batch forward: the row statistic is batch-invariant, so + # its prefix is what the n-row run must have seen. Recomputing it with + # mean(-1) instead would compare against a different reduction. + _, rstd_full = _C.rmsnorm_gated_forward(x, w, gate, _EPS, 0.0, 0) + rows = dy * F.silu(gate) * x * rstd_full.unsqueeze(-1) + assert torch.equal(wg.grad, reduce_rows_fp32(rows[:batch])) + + +@requires_cuda_gated +def test_cuda_gated_weight_offset_is_applied_in_fp32(): + """The gated kernel carries the same offset contract as the plain one.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + + x, w, gate = _gated_cuda_inputs() + offset = rmsnorm_gated_cuda(x, w, gate, eps=_EPS, weight_offset=1.0) + explicit = rmsnorm_gated_cuda(x, (1.0 + w.float()), gate, eps=_EPS) + assert torch.equal(offset, explicit) + folded = rmsnorm_gated_cuda(x, (1.0 + w.float()).bfloat16(), gate, eps=_EPS) + assert not torch.equal(offset, folded) + + +@requires_cuda_gated +def test_cuda_gated_parameter_vjp_contributions_match_the_fold(): + """The harness hook must return the same rows the backward folds.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp + + x, w, gate = _gated_cuda_inputs(rows=64) + x, w, gate = x.float(), w.float(), gate.float() + dy = torch.randn_like(x) + + rows = Qwen3NextRMSNormGatedCudaOp().parameter_vjp_contributions_fp32( + x=x, weight=w, gate=gate, grad_output=dy, eps=_EPS + )["weight"] + rstd = torch.rsqrt(x.square().mean(dim=-1) + _EPS) + expected = dy * F.silu(gate) * x * rstd.unsqueeze(-1) + torch.testing.assert_close(rows, expected, atol=1e-6, rtol=1e-6) + + +@requires_cuda_gated +def test_cuda_gated_sigmoid_backward_uses_the_sigmoid_derivative(): + """The activation gradient has two branches; only silu was covered.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + + x, w, gate = _gated_cuda_inputs(rows=64) + x, w, gate = x.float(), w.float(), gate.float() + dy = torch.randn_like(x) + + got = [t.clone().requires_grad_(True) for t in (x, w, gate)] + rmsnorm_gated_cuda(*got, eps=_EPS, activation="sigmoid").backward(dy) + + ref = [t.clone().requires_grad_(True) for t in (x, w, gate)] + rstd = torch.rsqrt(ref[0].square().mean(dim=-1) + _EPS) + out = (ref[0] * rstd.unsqueeze(-1) * ref[1]) * torch.sigmoid(ref[2]) + out.backward(dy) + for name, a, b in zip(("dx", "dweight", "dgate"), got, ref): + torch.testing.assert_close(a.grad, b.grad, atol=1e-4, rtol=1e-4, msg=name) + + +@requires_cuda_gated +def test_cuda_gated_rejects_bad_activation(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + + x, w, gate = _gated_cuda_inputs(rows=8) + with pytest.raises(ValueError, match="activation must be one of"): + rmsnorm_gated_cuda(x, w, gate, eps=_EPS, activation="gelu") + + +@requires_cuda_gated +def test_cuda_gated_rejects_mismatched_gate(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + + x, w, gate = _gated_cuda_inputs(rows=8) + with pytest.raises((AssertionError, RuntimeError)): + rmsnorm_gated_cuda(x, w, gate[:4], eps=_EPS) + with pytest.raises((AssertionError, RuntimeError)): + rmsnorm_gated_cuda(x, w, gate.float(), eps=_EPS) + + # --------------------------------------------------------------------------- # # 8. `__call__` is the documented entry point and must agree with `forward` # --------------------------------------------------------------------------- # diff --git a/tests/test_ws1_gtest_gpu.py b/tests/test_ws1_gtest_gpu.py index 2f4ec8648..84b6daccb 100644 --- a/tests/test_ws1_gtest_gpu.py +++ b/tests/test_ws1_gtest_gpu.py @@ -36,6 +36,8 @@ def test_all_ws1_single_ops_are_registered(): names = set(operator_names()) assert { "rms_norm", + "rms_norm_gated", + "qwen3_next_rms_norm", "qk_norm", "det_gemm", "attention", From d03c8523a6e39bb616deead401d13ff9b53b4740 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:36:52 +0000 Subject: [PATCH 08/44] fix(norm): contract tolerances and corrected claims for the Qwen3-Next norms Tests: the low-precision and CUDA-vs-golden comparisons hard-coded atol=2e-2/rtol=1.6e-2, which is the contract's elementwise bf16 row. They now resolve forward_accuracy for op_class="reduction" from tolerance_contract.json. That loosens bf16 (5e-2/2e-2) and tightens fp16 (1e-3/1e-3, previously 2e-2); the fp16 cases still pass. The vLLM provider-gap bounds in tests/check_qwen3_next_norm_providers.py are labelled as gap bounds, not contract thresholds. Docs and docstrings: correct statements this branch had committed. - The operator page no longer says the op is registered or prints a check_operator command; the gtest spec and registry entry arrive with the gated-norm PR. - Withdrawn: "7 of 1048576 differ" (single seed, no script), the 35% / 5.3e-2 cast-order isolation (an fp32 round-trip is a no-op), "needed on CUDA as well", and "matching vLLM means trading L1 for L2" (one unreproduced observation). Replaced with the scoped claim levels and the probe results. - The provider check no longer claims to establish which path vLLM dispatches, or a per-element fp32 ULP bound. - The module docstring is cut to the contract and links to the operator page. Comments: why the kernel's offset add is guarded (signed zero, with the pinning test), and why parameter_vjp_contributions_fp32 passes the offset. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- csrc/cuda/rmsnorm.cu | 3 + docs/operators/qwen3-next-rms-norm.md | 60 +++++++++------ rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 2 + .../ops/pytorch/norm/qwen3_next_rms_norm.py | 76 +++++-------------- tests/check_qwen3_next_norm_providers.py | 43 ++++++----- tests/test_qwen3_next_norm.py | 22 ++++-- 6 files changed, 102 insertions(+), 104 deletions(-) diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index 5b570c557..0c09b3a4a 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -143,6 +143,9 @@ __global__ void rmsnorm_fwd_kernel( for (int col = tid; col < H; col += blockDim.x) { float xv = load_as_float(x_row + col); float wv = load_as_float(weight + col); + // Guarded: `-0.0f + 0.0f` is +0.0f, so an unconditional add would flip the + // sign bit of -0.0 weights on the plain path. Pinned by + // tests/test_qwen3_next_norm.py::test_cuda_plain_signed_zero_is_preserved. if (weight_offset != 0.0f) wv += weight_offset; float out = xv * row_rstd * wv; store_from_float(y_row + col, out); diff --git a/docs/operators/qwen3-next-rms-norm.md b/docs/operators/qwen3-next-rms-norm.md index 292139bea..81fdde9b3 100644 --- a/docs/operators/qwen3-next-rms-norm.md +++ b/docs/operators/qwen3-next-rms-norm.md @@ -49,32 +49,47 @@ y = rmsnorm_cuda(x, weight, eps=1e-6, weight_offset=1.0) ## Dispatch Behavior -Registered as the `qwen3_next_rms_norm` gtest operator. On CUDA the registry -prefers `Qwen3NextRMSNormCudaOp`; every other platform resolves to the PyTorch -reference. The CUDA op validates the compiled symbols in `__init__`, so on a build -without the extension construction raises and the registry falls through to the -reference rather than handing out an op that fails at call time. +Not registered on this branch: there is no `qwen3_next_rms_norm` gtest spec and no +registry entry yet. Both arrive with the gated-norm PR. Until then, construct the +ops directly as in "Entry Point". The CUDA op validates the compiled symbols in +`__init__`, so on a build without the extension construction raises instead of +handing out an op that fails at call time. ## Accuracy -Claim level: **L0 repeatable, L1 batch-invariant**. L2 is not claimed. +Claim levels: -The reduction is the repo's fixed 32-wide chunked sum -(`shape_invariant_rstd`), which is what makes a row's result independent of the -batch layout. It deliberately differs from upstream's `mean(-1)`: measured on B200, -over 20 seeds at `H=2048` in bf16, a plain `mean(-1)` broke slice invariance on 1 -of 20 while the chunked reduction broke on 0 of 20. +- **L1** (prefix-slice and concurrency) for `Qwen3NextRMSNormCudaOp`; padding, + packing and order are not tested for the CUDA op. +- **L1** (slice, concurrency and padding) for `Qwen3NextRMSNormOp`. +- **L0** for `Qwen3NextRMSNormOp` only (CPU, fp32); no test repeats the CUDA op. +- L2 is not claimed. -The cost is that the decoder norm is **not** bitwise equal to stock vLLM — 7 -elements of 1048576 differ, `max|diff| = 1.56e-2` in bf16 at `H=2048`. That gap is -inherent: matching stock vLLM bitwise would mean adopting a reduction that is not -itself batch-invariant, i.e. trading L1 for L2. +The PyTorch reference uses the repo's fixed 32-wide chunked sum +(`shape_invariant_rstd`, introduced in `8ed1693` as a device-agnostic reference). +On B200, over 20 seeds at `H=2048` in bf16, a plain `mean(-1)` broke slice +invariance on 1 of 20 seeds (slices `x[3:5]` and `x[:1]` of 64 rows; which one +failed was not recorded), and the chunked reduction broke on none. That is a recorded +observation, not an assertion. + +The CUDA kernel has its own fixed-order reduction (per-thread strided partial sums, +then `block_reduce_sum`). It is not bitwise equal to the PyTorch reference. + +Neither is bitwise equal to vLLM. In one-off probes (not committed checks), the +reference differed from every vLLM path tried (eager, inductor-compiled, and eager +under `VLLM_BATCH_INVARIANT=1`) on 36–39 of 40 seeds, `max|diff| <= 1.56e-2` +(bf16, `H=2048`, 512 rows). The difference is attributed to reduction order, but that +has not been isolated. Only the no-residual call was compared; vLLM's +`fused_add_rms_norm` path (every decoder norm except layer 0's input norm) has no +reference here. The in-kernel offset is exact, not an approximation: `weight_offset=1.0` is bitwise equal to passing an explicit fp32 `1 + w` weight, and differs from a bf16-folded `1 + w`, both asserted. -Tolerances come from `tolerance_contract.json` (`reduction` × dtype); no private +Accuracy tests resolve their tolerances from `tolerance_contract.json` +(`forward_accuracy`, `reduction` × dtype). The bounds in +`tests/check_qwen3_next_norm_providers.py` are provider-gap bounds, not contract thresholds. ## Performance Notes @@ -83,11 +98,6 @@ The CUDA path reuses the existing `rmsnorm_fwd_kernel` reduction (`block_reduce_sum` over `choose_threads(H)`), so the offset costs one fp32 add per element and no extra memory traffic. -```bash -python scripts/check_operator.py --op qwen3_next_rms_norm --candidate cuda \ - --device cuda --dtype bf16 --check-grad -``` - ## Tests ```bash @@ -97,7 +107,11 @@ python -m pytest tests/test_qwen3_next_norm.py -v ## Known Limitations - CUDA only; no ROCm, Ascend or Triton backend. -- Not bitwise against stock vLLM (see Accuracy); an L2 claim needs the strict - provider on both sides, per RFC #428 §1 item 1. +- Not registered as a gtest operator or in the registry on this branch (see + "Dispatch Behavior"). +- Not bitwise against vLLM (see Accuracy). An L2 claim needs a single source of + truth for the forward on both sides, per RFC #428 §0 item 1. +- The gated pair (`Qwen3NextRMSNormGatedOp`, `Qwen3NextRMSNormGatedHFOp`) is + documented with its CUDA kernel in the gated-norm PR, not on this page. - Measured on sm_100 (B200). Per RFC #428 §2.2 no claim carries across H100/H200/B100/B200. diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index 7d5a7a754..af9a31358 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -145,6 +145,8 @@ def forward(self, x, weight, *, eps=1e-6): def parameter_vjp_contributions_fp32(self, *, x, weight, grad_output, eps=1e-6): hidden = x.shape[-1] + # Only `rstd` is used, and it does not depend on the offset; the offset is + # passed so this is the same call the forward makes, not because it matters. _, rstd = _C.rmsnorm_forward( x.contiguous().reshape(-1, hidden), weight.contiguous(), diff --git a/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py index 5261245d0..2b3c410a9 100644 --- a/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py +++ b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py @@ -3,66 +3,26 @@ """Qwen3-Next RMSNorm references (WS1 ground truth for RFC #428 C1). -Qwen3-Next ships two RMSNorm conventions that differ both in how the weight is -applied and in where the dtype casts sit. They are kept as separate operators -because the cast order is part of the contract, not a flag: +Qwen3-Next ships two RMSNorm conventions. They differ in how the weight is applied +and in where the dtype casts sit, so they are separate operators rather than a flag: ``Qwen3NextRMSNorm`` (decoder / final norm) - normalize in fp32, scale by ``(1 + weight)`` in fp32, cast once at the end. - The stored weight is zero-centred, so the ``1 +`` offset MUST be applied - after the fp32 upcast -- folding it into a bf16 weight first rounds the - offset and silently breaks the bitwise claim. + Normalize in fp32, scale by ``(1 + weight)`` in fp32, cast once at the end. The + stored weight is zero-centred, so the ``1 +`` must be applied after the fp32 + upcast; folding it into a bf16 weight first rounds the offset. ``Qwen3NextRMSNormGated`` (inside the Gated DeltaNet block) - normalize in fp32, scale by a plain (non zero-centred) weight, then gate by - ``silu(gate)``. Where the weight multiply happens is NOT agreed upstream -- - see below -- so it is an explicit part of the operator identity here. - -Divergence at the gated-norm boundary (measured) -------------------------------------------------- -``transformers`` and vLLM do not agree on the gated variant: - -* vLLM (``RMSNormGated``, both ``forward_native`` and the FLA Triton - ``forward_cuda``) keeps the normalized value in fp32 for the weight multiply. -* ``transformers`` (``Qwen3NextRMSNormGated``) casts the normalized value back - to the input dtype *before* the weight multiply. - -On B200 / bf16 / ``head_v_dim=128`` these differ in 35% of elements with -``max|diff| = 6.25e-2``; swapping only the cast order reproduces the gap -(``5.3e-2``), so the cast order -- not the reduction order -- dominates. Because -RFC #428 claims L2 exactness against **vLLM rollout**, the fp32 multiply is the -strict default; the transformers convention is kept as a named witness so the -divergence stays testable instead of being silently picked. - -What "agrees with vLLM" means here, precisely ----------------------------------------------- -Only the *convention* is reproduced, not the bits. vLLM's own two paths are not -bitwise equal to each other: over 40 seeds (bf16, ``head_v_dim=128``, 512 rows), -``forward_native`` and ``forward_cuda`` disagreed on 21, worst -``max|diff| = 1.56e-2``. Against this operator the figures were 6/40 and 18/40. -So "bitwise equal to vLLM" is undefined until a single provider is named, and -this operator does not claim it -- see the reduction-order note below. - -Both reuse :func:`shape_invariant_rstd` so the reduction order -- and hence the -result -- never depends on the batch layout (RFC #428 section 6, item 2). The -reduction order therefore deliberately differs from upstream's ``mean(-1)``; -what is reproduced exactly is the weight convention and the cast order. - -Why the chunked reduction is kept on CUDA too ---------------------------------------------- -:func:`shape_invariant_rstd` was introduced for NPU, where ``mean``/``sum`` pick -shape-dependent kernels. Measured on B200 (sm_100) it is needed on CUDA as well: -over 20 seeds at ``H=2048`` in bf16 -- Qwen3-Next's own ``hidden_size`` and dtype --- a plain ``mean(-1)`` broke slice invariance (``rstd(x[3:5]) != rstd(x)[3:5]``) -on 1 of 20, while the chunked reduction broke on 0 of 20. Failures were also seen -at ``H=5120`` in fp32. - -The cost is that the decoder norm is NOT bitwise equal to stock vLLM: 7 elements -of 1048576 differ (``max|diff| = 1.56e-2``, bf16, H=2048). That gap is inherent -- -matching stock vLLM bitwise would mean adopting a reduction that is itself not -batch-invariant, i.e. trading L1 for L2. These operators therefore claim L0 and L1 -only; an L2 claim needs the strict provider on both sides, per RFC #428 section 1, -item 1. + Normalize in fp32, scale by a plain weight, then gate by ``silu(gate)``. + vLLM's ``RMSNormGated`` multiplies the weight in fp32; transformers casts the + normalized value back to the input dtype first. ``Qwen3NextRMSNormGatedOp`` + follows vLLM; ``Qwen3NextRMSNormGatedHFOp`` keeps the transformers convention + as a witness, and ``test_gated_conventions_diverge_in_low_precision`` pins that + the two differ in bf16 and agree bitwise in fp32. + +All three reuse :func:`shape_invariant_rstd`, a fixed-order reduction. They +reproduce the weight convention and cast order, not vLLM's reduction tree, and are +not bitwise equal to any vLLM path probed so far. Claim levels, measurements and +limitations are in ``docs/operators/qwen3-next-rms-norm.md``. """ from __future__ import annotations @@ -100,8 +60,8 @@ class Qwen3NextRMSNormGatedOp: ``out = (x * rstd * weight) * silu(gate)``, with every multiply in fp32 and a single cast on the way out. This is the convention vLLM's ``RMSNormGated`` - uses with ``norm_before_gate=True``, which is what the RFC #428 L2 claim is - measured against. + uses with ``norm_before_gate=True``. Which gated convention is the strict + default is still open; see the operator page. Not a subclass of the plain op: it takes an extra tensor and its epilogue differs, so it is not a drop-in substitute for one. diff --git a/tests/check_qwen3_next_norm_providers.py b/tests/check_qwen3_next_norm_providers.py index 062052cfa..7086aa341 100644 --- a/tests/check_qwen3_next_norm_providers.py +++ b/tests/check_qwen3_next_norm_providers.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 RL-Kernel Contributors -"""Which vLLM gated-RMSNorm path is the strict provider, and how far apart are they. +"""How far apart vLLM's eager gated-RMSNorm paths are, and ours from each. Named ``check_`` rather than ``test_`` on purpose, following ``tests/distributed/check_*.py``: this module imports real vLLM, and @@ -16,13 +16,17 @@ provider is named, because vLLM ships several and they do not agree bitwise with each other. -This module pins two things so a vLLM upgrade cannot move them silently: +This module records two things so a vLLM upgrade cannot move them silently: -1. **Dispatch facts** -- which provider actually runs, asserted on the env - defaults and the registered ops rather than inferred from reading one branch. +1. **Provider facts** -- the GDN decode env defaults, and that ``RMSNormGated`` + has distinct ``forward_native`` and ``forward_cuda`` methods. It does NOT + determine which path vLLM dispatches at runtime: the checks below call each + method directly (eager), and vLLM's default compiled mode traces + ``forward_native`` into an inductor graph instead. 2. **The size of the gap** -- a seed sweep that asserts an upper bound on the - disagreement and on the mismatch rate. It deliberately does NOT assert - equality; the point is to keep the number honest, not to pretend it is zero. + disagreement and on the mismatch rate between the eager paths. The bounds are + provider-gap bounds, not ``tolerance_contract.json`` thresholds, and they + deliberately do NOT assert equality. Measured on 2x B200 (sm_100, torch 2.13.0+cu130, vllm 0.30.0), bf16, ``head_v_dim=128``, 512 rows, 40 seeds: @@ -101,21 +105,24 @@ def _disagreement(a: torch.Tensor, b: torch.Tensor) -> tuple[float, float]: # --------------------------------------------------------------------------- # -# 1. Dispatch facts -- which provider actually runs +# 1. Provider facts -- recorded, not a runtime dispatch check # --------------------------------------------------------------------------- # def test_custom_op_has_distinct_native_and_cuda_paths(vllm_config_ctx): - """`forward_native` is a reference; `forward_cuda` is what CustomOp dispatches.""" + """The two methods are distinct. Which one runs depends on the vLLM mode: eager + (``custom_ops="all"``) dispatches ``forward_cuda``; the default compiled mode + traces ``forward_native``. This test does not check that choice.""" norm = _make_norm() assert type(norm).forward_cuda is not type(norm).forward_native def test_gdn_decode_provider_env_defaults_are_recorded(): - """Pin the env defaults that decide which GDN decode kernel runs. + """Record the GDN decode env defaults. - These are what make ``fused_recurrent_gated_delta_rule_packed_decode`` (not - ``fused_sigmoid_gating_delta_rule_update``) the rollout decode path. If a - vLLM bump flips either default, the provider identity behind any L2 claim - changes, so this must fail loudly rather than drift. + This records the defaults only; it does not assert which kernel runs. For + Qwen3-Next the ``VLLM_GDN_DECODE_KERNEL="cuda"`` default does not take effect: + vLLM builds its GDN layers with ``gqa_interleaved_layout=True`` and falls back + to the Triton decode kernel. A change of either default still fails here, as a + prompt to re-derive the decode path. """ pytest.importorskip("vllm", reason="vLLM is required to identify the provider") import vllm.envs as envs @@ -141,12 +148,11 @@ def test_gdn_decode_provider_env_defaults_are_recorded(): @pytest.mark.parametrize( "dtype, max_abs, max_mismatch_rate", [ - # bf16 rounding absorbs most of the reduction-tree difference, so few - # elements move -- but each that does moves by a whole bf16 ULP. + # Provider-gap bounds, not contract thresholds. bf16 rounding absorbs most + # of the reduction-tree difference, so few elements move. (torch.bfloat16, 2e-2, 0.05), - # fp32 has nothing to absorb it: ~36% of elements differ, every one of - # them by an fp32 ULP. Bounding the rate here would be measuring the - # wrong thing; the magnitude is what says the two agree semantically. + # In fp32 about 36% of elements differ, so the rate is left unbounded and + # only the magnitude is bounded (no per-element ULP bound is asserted). (torch.float32, 1e-5, 1.0), ], ) @@ -196,6 +202,7 @@ def test_ours_tracks_each_vllm_path_within_bounds(vllm_config_ctx, path): abs_d, rate = _disagreement(ours.forward(x, weight, gate), reference) worst_abs, worst_rate = max(worst_abs, abs_d), max(worst_rate, rate) + # Provider-gap bounds, not contract thresholds. assert worst_abs <= 2e-2, f"worst |diff| vs {path} was {worst_abs:.3e}" assert worst_rate <= 0.05, f"mismatch rate vs {path} was {worst_rate:.3%}" diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index 9fdf02e73..a62d8410f 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -5,7 +5,8 @@ Claim levels exercised here (RFC #428 section 2.1): * L0 repeatable -- identical inputs reproduce bitwise identical outputs. - * L1 batch-invariant -- a row is unaffected by unrelated rows, padding or order. + * L1 batch-invariant -- a row is unaffected by slicing, concurrency and (for the + PyTorch reference) padding; packing and order are not exercised. L2 (train-rollout exact against vLLM) is NOT claimed by this file; it needs the rollout engine on the other side. @@ -22,6 +23,7 @@ import torch import torch.nn.functional as F +from rl_engine.kernels.gtest.tolerance import load_contract, resolve_tolerance from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import ( Qwen3NextRMSNormGatedHFOp, Qwen3NextRMSNormGatedOp, @@ -33,6 +35,16 @@ _HEAD_V_DIM = 128 # linear_value_head_dim -- the gated norm width _EPS = 1e-6 # rms_norm_eps +_CONTRACT = load_contract() + + +def _forward_tol(dtype: torch.dtype) -> dict[str, float]: + """C1 forward_accuracy row for the ``reduction`` op class -- no private thresholds.""" + spec = resolve_tolerance( + _CONTRACT, judgment="forward_accuracy", op_class="reduction", dtype=dtype + ) + return {"atol": spec.atol, "rtol": spec.rtol} + def _rand(shape, seed): g = torch.Generator().manual_seed(seed) @@ -277,7 +289,7 @@ def test_matches_upstream_formula_low_precision(dtype): w = _rand((_HIDDEN,), seed=22).to(dtype) got, ref = op.forward(x, w), _hf_rms_norm(x, w) assert got.dtype == ref.dtype == dtype - torch.testing.assert_close(got.float(), ref.float(), atol=2e-2, rtol=1.6e-2) + torch.testing.assert_close(got.float(), ref.float(), **_forward_tol(dtype)) @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) @@ -289,7 +301,7 @@ def test_gated_hf_witness_low_precision(dtype): gate = _rand((4, 16, _HEAD_V_DIM), seed=25).to(dtype) got, ref = op.forward(x, w, gate), _hf_rms_norm_gated(x, w, gate) assert got.dtype == ref.dtype == dtype - torch.testing.assert_close(got.float(), ref.float(), atol=2e-2, rtol=1.6e-2) + torch.testing.assert_close(got.float(), ref.float(), **_forward_tol(dtype)) @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16]) @@ -300,7 +312,7 @@ def test_gated_strict_low_precision(dtype): gate = _rand((4, 16, _HEAD_V_DIM), seed=25).to(dtype) got, ref = op.forward(x, w, gate), _vllm_rms_norm_gated(x, w, gate) assert got.dtype == ref.dtype == dtype - torch.testing.assert_close(got.float(), ref.float(), atol=2e-2, rtol=1.6e-2) + torch.testing.assert_close(got.float(), ref.float(), **_forward_tol(dtype)) # --------------------------------------------------------------------------- # @@ -421,7 +433,7 @@ def test_cuda_zero_centred_within_tolerance_of_golden(): w = torch.randn(_HIDDEN, device="cuda", dtype=torch.bfloat16) got = Qwen3NextRMSNormCudaOp().forward(x, w, eps=_EPS) ref = Qwen3NextRMSNormOp().forward_fp32(x, w, eps=_EPS) - torch.testing.assert_close(got.float(), ref, atol=2e-2, rtol=1.6e-2) + torch.testing.assert_close(got.float(), ref, **_forward_tol(torch.bfloat16)) @requires_cuda_rmsnorm From 7dd291a9d25757079406989004745ebd3b7a7284 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:22:52 +0000 Subject: [PATCH 09/44] fix(norm): enforce gated tensor boundaries and align parameter VJP statistics Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- csrc/cuda/rmsnorm.cu | 13 +++ csrc/ops.cpp | 27 ++++++ docs/operators/qwen3-next-rms-norm-gated.md | 10 ++- rl_engine/_C.pyi | 17 ++++ rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 37 +++++--- .../ops/pytorch/norm/qwen3_next_rms_norm.py | 2 + tests/test_qwen3_next_norm.py | 88 +++++++++++++++---- 7 files changed, 162 insertions(+), 32 deletions(-) diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index c80945300..2d5c98ec4 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -389,6 +389,7 @@ void rmsnorm_forward_cuda( // device, which need not be x's. const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); + if (T == 0) return; int H = x.size(1); int threads = choose_threads(H); size_t smem = threads * sizeof(float); @@ -411,6 +412,7 @@ void rmsnorm_forward_cuda( ); }); }); + C10_CUDA_KERNEL_LAUNCH_CHECK(); } @@ -424,6 +426,7 @@ void rmsnorm_backward_dx_cuda( ) { const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); + if (T == 0) return; int H = x.size(1); int threads = choose_threads(H); size_t smem = threads * sizeof(float); @@ -446,6 +449,7 @@ void rmsnorm_backward_dx_cuda( ); }); }); + C10_CUDA_KERNEL_LAUNCH_CHECK(); } @@ -459,7 +463,9 @@ void rmsnorm_gated_forward_cuda( double weight_offset, int64_t activation ) { + const c10::cuda::CUDAGuard device_guard(x.device()); int T = x.size(0); + if (T == 0) return; int H = x.size(1); int threads = choose_threads(H); size_t smem = threads * sizeof(float); @@ -483,6 +489,7 @@ void rmsnorm_gated_forward_cuda( } }); }); + C10_CUDA_KERNEL_LAUNCH_CHECK(); } @@ -496,7 +503,9 @@ void rmsnorm_gated_backward_dx_cuda( double weight_offset, int64_t activation ) { + const c10::cuda::CUDAGuard device_guard(x.device()); int T = x.size(0); + if (T == 0) return; int H = x.size(1); int threads = choose_threads(H); size_t smem = threads * sizeof(float); @@ -520,6 +529,7 @@ void rmsnorm_gated_backward_dx_cuda( } }); }); + C10_CUDA_KERNEL_LAUNCH_CHECK(); } void rmsnorm_backward_partial_dw_cuda( @@ -531,6 +541,7 @@ void rmsnorm_backward_partial_dw_cuda( ) { const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); + if (T == 0) return; int H = x.size(1); int chunks = (T + RMSNORM_DW_ROWS_PER_CHUNK - 1) / RMSNORM_DW_ROWS_PER_CHUNK; @@ -550,6 +561,7 @@ void rmsnorm_backward_partial_dw_cuda( H ); }); + C10_CUDA_KERNEL_LAUNCH_CHECK(); } @@ -572,6 +584,7 @@ void rmsnorm_backward_reduce_dw_cuda( chunks, H ); + C10_CUDA_KERNEL_LAUNCH_CHECK(); } #if !defined(USE_ROCM) diff --git a/csrc/ops.cpp b/csrc/ops.cpp index b37ab102c..1efb116fc 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -315,6 +315,24 @@ static void rmsnorm_check_input(const torch::Tensor& x, const char* name) { TORCH_CHECK(x.is_contiguous(), name, " must be contiguous"); } +static void rmsnorm_check_weight(const torch::Tensor& x, const torch::Tensor& weight) { + TORCH_CHECK(x.dim() == 2 && x.size(1) > 0, "x must be 2D with positive hidden size"); + TORCH_CHECK(x.scalar_type() == torch::kFloat32 || x.scalar_type() == torch::kFloat16 || + x.scalar_type() == torch::kBFloat16, "x must be float32, float16 or bfloat16"); + TORCH_CHECK(weight.dim() == 1 && weight.size(0) == x.size(1), "weight must be [H]"); + TORCH_CHECK(weight.device() == x.device(), "weight must be on the same device as x"); + TORCH_CHECK(weight.scalar_type() == x.scalar_type() || weight.scalar_type() == torch::kFloat32, + "weight must have x dtype or float32"); +} + +static void rmsnorm_check_backward(const torch::Tensor& dy, const torch::Tensor& x, + const torch::Tensor& rstd) { + TORCH_CHECK(dy.device() == x.device() && rstd.device() == x.device(), + "dy and rstd must be on the same device as x"); + TORCH_CHECK(dy.scalar_type() == x.scalar_type(), "dy must have the same dtype as x"); + TORCH_CHECK(rstd.scalar_type() == torch::kFloat32, "rstd must be float32"); +} + std::vector rmsnorm_forward( torch::Tensor x, torch::Tensor weight, @@ -323,6 +341,7 @@ std::vector rmsnorm_forward( { rmsnorm_check_input(x, "x"); rmsnorm_check_input(weight, "weight"); + rmsnorm_check_weight(x, weight); TORCH_CHECK(x.dim() == 2, "x must be 2D [T, H]"); TORCH_CHECK(weight.dim() == 1, "weight must be 1D [H]"); @@ -347,7 +366,9 @@ torch::Tensor rmsnorm_backward_dx( rmsnorm_check_input(dy, "dy"); rmsnorm_check_input(x, "x"); rmsnorm_check_input(weight, "weight"); + rmsnorm_check_weight(x, weight); rmsnorm_check_input(rstd, "rstd"); + rmsnorm_check_backward(dy, x, rstd); TORCH_CHECK(dy.sizes() == x.sizes(), "dy and x must have same shape"); TORCH_CHECK(x.dim() == 2, "x must be 2D [T, H]"); @@ -370,7 +391,9 @@ static void rmsnorm_gated_check( { rmsnorm_check_input(x, "x"); rmsnorm_check_input(weight, "weight"); + rmsnorm_check_weight(x, weight); rmsnorm_check_input(gate, "gate"); + TORCH_CHECK(gate.device() == x.device(), "gate must be on the same device as x"); TORCH_CHECK(x.dim() == 2, "x must be 2D [T, H]"); TORCH_CHECK(weight.dim() == 1, "weight must be 1D [H]"); @@ -417,6 +440,7 @@ torch::Tensor rmsnorm_gated_backward_dx( rmsnorm_gated_check(x, weight, gate, activation); rmsnorm_check_input(dy, "dy"); rmsnorm_check_input(rstd, "rstd"); + rmsnorm_check_backward(dy, x, rstd); TORCH_CHECK(dy.sizes() == x.sizes(), "dy must have the same shape as x"); TORCH_CHECK(rstd.dim() == 1 && rstd.size(0) == x.size(0), "rstd must be [T]"); @@ -438,6 +462,7 @@ torch::Tensor rmsnorm_backward_dw( rmsnorm_check_input(dy, "dy"); rmsnorm_check_input(x, "x"); rmsnorm_check_input(rstd, "rstd"); + rmsnorm_check_backward(dy, x, rstd); rmsnorm_check_input(mask, "mask"); TORCH_CHECK(dy.sizes() == x.sizes(), "dy and x must have same shape"); @@ -445,6 +470,8 @@ torch::Tensor rmsnorm_backward_dw( TORCH_CHECK(rstd.dim() == 1, "rstd must be 1D [T]"); TORCH_CHECK(mask.dim() == 1, "mask must be 1D [T]"); TORCH_CHECK(mask.scalar_type() == torch::kBool, "mask must be bool"); + TORCH_CHECK(mask.device() == x.device(), "mask must be on the same device as x"); + TORCH_CHECK(x.size(1) > 0, "hidden size must be positive"); TORCH_CHECK(rstd.size(0) == x.size(0), "rstd.size(0) must equal x.size(0)"); TORCH_CHECK(mask.size(0) == x.size(0), "mask.size(0) must equal x.size(0)"); diff --git a/docs/operators/qwen3-next-rms-norm-gated.md b/docs/operators/qwen3-next-rms-norm-gated.md index 1bc531ea6..55071d25b 100644 --- a/docs/operators/qwen3-next-rms-norm-gated.md +++ b/docs/operators/qwen3-next-rms-norm-gated.md @@ -47,12 +47,16 @@ required tensor, so it cannot stand in for one. | Argument | Shape | Dtype | Requirements | | --- | --- | --- | --- | -| `x` | `[..., H]` | fp32 / bf16 / fp16 | contiguous on the CUDA path | -| `weight` | `[H]` | matches `x` | plain, NOT zero-centred | -| `gate` | same as `x` | same as `x` | shape and dtype are enforced | +| `x` | `[..., H]` | fp32 / bf16 / fp16 | H > 0; wrapper makes contiguous copies | +| `weight` | `[H]` | matches `x` or fp32 | plain, NOT zero-centred | +| `gate` | same as `x` | same as `x` | shape, dtype and device must match before flattening | | `eps` | scalar | float | `1e-6` for Qwen3-Next | | `activation` | — | `"silu"`/`"swish"`/`"sigmoid"` | anything else is rejected | +All tensors must share a device. Low-level CUDA bindings require contiguous +2-D inputs, and validate backward gradient dtype and FP32 statistics. Empty +batches are supported by the bindings. + Only `norm_before_gate=True` and `group_size=None` are implemented — the single configuration vLLM's GDN block constructs. Other configurations fail closed rather than being approximated (RFC #428 §6 item 7). diff --git a/rl_engine/_C.pyi b/rl_engine/_C.pyi index b911feaf7..1e686fb9c 100644 --- a/rl_engine/_C.pyi +++ b/rl_engine/_C.pyi @@ -269,6 +269,23 @@ def rmsnorm_backward_dx( rstd: torch.Tensor, weight_offset: float = ..., ) -> torch.Tensor: ... +def rmsnorm_gated_forward( + x: torch.Tensor, + weight: torch.Tensor, + gate: torch.Tensor, + eps: float, + weight_offset: float = ..., + activation: int = ..., +) -> list[torch.Tensor]: ... +def rmsnorm_gated_backward_dx( + dy: torch.Tensor, + x: torch.Tensor, + weight: torch.Tensor, + gate: torch.Tensor, + rstd: torch.Tensor, + weight_offset: float = ..., + activation: int = ..., +) -> torch.Tensor: ... def rmsnorm_backward_dw( dy: torch.Tensor, x: torch.Tensor, diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index fc1aa9554..3c66e144c 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -163,6 +163,7 @@ class Qwen3NextRMSNormCudaOp(RMSNormCudaOp): # Gated RMSNorm (Qwen3-Next GDN block) # --------------------------------------------------------------------------- # + def _require_cuda_symbols(what: str, *names: str) -> None: """Raise when the compiled kernels backing ``what`` are missing. @@ -225,9 +226,9 @@ def forward(ctx, x, weight, gate, eps=1e-6, weight_offset=0.0, activation=0): assert x.dim() == 2, "x must be [T, H]" assert weight.dim() == 1, "weight must be [H]" assert gate.shape == x.shape, "gate must match x" - assert _EXT_AVAILABLE and hasattr(_C, "rmsnorm_gated_forward"), ( - "Gated RMSNorm CUDA extension is unavailable. Rebuild with csrc/cuda/rmsnorm.cu." - ) + assert _EXT_AVAILABLE and hasattr( + _C, "rmsnorm_gated_forward" + ), "Gated RMSNorm CUDA extension is unavailable. Rebuild with csrc/cuda/rmsnorm.cu." y, rstd = _C.rmsnorm_gated_forward( x, weight, gate, float(eps), float(weight_offset), int(activation) @@ -246,9 +247,7 @@ def backward(ctx, grad_out): dy = grad_out.contiguous() act = ctx.activation - dx = _C.rmsnorm_gated_backward_dx( - dy, x, weight, gate, rstd, ctx.weight_offset, act - ) + dx = _C.rmsnorm_gated_backward_dx(dy, x, weight, gate, rstd, ctx.weight_offset, act) # dweight: the gate is a per-element constant here, so the ungated row # contributions apply once dy carries act(gate). @@ -259,9 +258,7 @@ def backward(ctx, grad_out): # dgate: row-local and reduction-free. normed = x.float() * rstd.unsqueeze(-1) scale = weight.float() + ctx.weight_offset - dgate = (dy.float() * normed * scale * _gate_activation_grad_fp32(gate, act)).to( - gate.dtype - ) + dgate = (dy.float() * normed * scale * _gate_activation_grad_fp32(gate, act)).to(gate.dtype) record_backward( "rms_norm_gated", @@ -321,6 +318,8 @@ def __call__(self, x, weight, gate, *, eps=1e-6): return self.forward(x, weight, gate, eps=eps) def forward(self, x, weight, gate, *, eps=1e-6): + if gate.shape != x.shape: + raise ValueError(f"gate must match x, got {tuple(gate.shape)} vs {tuple(x.shape)}") hidden = x.shape[-1] x_2d = x.contiguous().view(-1, hidden) gate_2d = gate.contiguous().view(-1, hidden) @@ -335,9 +334,21 @@ def forward(self, x, weight, gate, *, eps=1e-6): return y_2d.view_as(x) def parameter_vjp_contributions_fp32(self, *, x, weight, gate, grad_output, eps=1e-6): - del weight - x32 = x.float() - rstd = torch.rsqrt(x32.square().mean(dim=-1) + float(eps)) + if gate.shape != x.shape: + raise ValueError(f"gate must match x, got {tuple(gate.shape)} vs {tuple(x.shape)}") + hidden = x.shape[-1] act = _GATE_ACTIVATIONS[self.activation] - rows = grad_output.float() * _gate_activation_fp32(gate, act) * x32 * rstd.unsqueeze(-1) + _, rstd = _C.rmsnorm_gated_forward( + x.contiguous().reshape(-1, hidden), + weight.contiguous(), + gate.contiguous().reshape(-1, hidden), + float(eps), + float(self.weight_offset), + act, + ) + rows = rmsnorm_dweight_rows_fp32( + x, + grad_output.float() * _gate_activation_fp32(gate, act), + rstd=rstd.reshape(x.shape[:-1]), + ) return {"weight": rows} diff --git a/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py index 2b3c410a9..1f84f86f3 100644 --- a/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py +++ b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py @@ -113,6 +113,8 @@ def _normalized( f"gate must match x, got tuple(gate.shape)={tuple(gate.shape)} " f"vs tuple(x.shape)={tuple(x.shape)}" ) + if gate.dtype != x.dtype or gate.device != x.device: + raise ValueError("gate must have the same dtype and device as x") x_f = x.float() rstd = shape_invariant_rstd(x_f, float(eps)).unsqueeze(-1) return x_f * rstd diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index c1f7e9d73..2f738a1c8 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -535,10 +535,7 @@ def test_cuda_gated_zero_gate_zeroes_output(): @requires_cuda_gated def test_cuda_gated_unit_weight_is_plain_norm_times_silu(): - from rl_engine.kernels.ops.cuda.norm.rmsnorm import ( - Qwen3NextRMSNormGatedCudaOp, - rmsnorm_cuda, - ) + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp, rmsnorm_cuda x, _, gate = _gated_cuda_inputs() ones = torch.ones(_HEAD_V_DIM, device="cuda", dtype=x.dtype) @@ -549,7 +546,7 @@ def test_cuda_gated_unit_weight_is_plain_norm_times_silu(): @requires_cuda_gated def test_cuda_gated_sigmoid_activation(): - from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda, rmsnorm_cuda + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_cuda, rmsnorm_gated_cuda x, w, gate = _gated_cuda_inputs() got = rmsnorm_gated_cuda(x, w, gate, eps=_EPS, activation="sigmoid").float() @@ -617,20 +614,35 @@ def test_cuda_gated_weight_offset_is_applied_in_fp32(): @requires_cuda_gated -def test_cuda_gated_parameter_vjp_contributions_match_the_fold(): - """The harness hook must return the same rows the backward folds.""" - from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp +@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16]) +@pytest.mark.parametrize("activation", ["silu", "sigmoid"]) +def test_cuda_gated_parameter_vjp_contributions_match_the_fold(dtype, activation): + """Compare the harness hook with actual autograd, including its CUDA rstd.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import ( + Qwen3NextRMSNormGatedCudaOp, + _fold_dweight_rows, + ) - x, w, gate = _gated_cuda_inputs(rows=64) - x, w, gate = x.float(), w.float(), gate.float() + x, w, gate = (t.to(dtype) for t in _gated_cuda_inputs(rows=512)) + w = w.float().requires_grad_() dy = torch.randn_like(x) + op = Qwen3NextRMSNormGatedCudaOp() + op.activation = activation + op.forward(x, w, gate, eps=_EPS).backward(dy) + rows = op.parameter_vjp_contributions_fp32(x=x, weight=w, gate=gate, grad_output=dy, eps=_EPS)[ + "weight" + ] + assert torch.equal(_fold_dweight_rows(rows, torch.float32), w.grad) - rows = Qwen3NextRMSNormGatedCudaOp().parameter_vjp_contributions_fp32( - x=x, weight=w, gate=gate, grad_output=dy, eps=_EPS - )["weight"] - rstd = torch.rsqrt(x.square().mean(dim=-1) + _EPS) - expected = dy * F.silu(gate) * x * rstd.unsqueeze(-1) - torch.testing.assert_close(rows, expected, atol=1e-6, rtol=1e-6) + +@requires_cuda_gated +def test_cuda_gated_wrapper_rejects_equal_numel_wrong_shape(): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp + + x, w, gate = _gated_cuda_inputs(rows=6) + x = x.reshape(2, 3, -1) + with pytest.raises(ValueError, match="gate must match x"): + Qwen3NextRMSNormGatedCudaOp().forward(x, w, gate) @requires_cuda_gated @@ -764,3 +776,47 @@ def test_zero_centred_cuda_constructor_rejects_missing_extension(monkeypatch): monkeypatch.setattr(rmsnorm, "_C", None) with pytest.raises(RuntimeError, match="requires the compiled"): rmsnorm.Qwen3NextRMSNormCudaOp() + + +@requires_cuda_gated +@pytest.mark.parametrize( + "fault", ["weight_shape", "weight_dtype", "gate_dtype", "dy_dtype", "rstd_dtype"] +) +def test_gated_extension_rejects_invalid_tensor_contract(fault): + from rl_engine import _C + + x, w, gate = _gated_cuda_inputs(rows=8) + dy = torch.ones_like(x) + rstd = torch.ones(8, device=x.device, dtype=torch.float32) + if fault == "weight_shape": + w = w[:-1] + elif fault == "weight_dtype": + w = w.half() + elif fault == "gate_dtype": + gate = gate.float() + elif fault == "dy_dtype": + dy = dy.float() + else: + rstd = rstd.bfloat16() + with pytest.raises(RuntimeError): + _C.rmsnorm_gated_backward_dx(dy, x, w, gate, rstd) + + +@requires_cuda_gated +def test_gated_extension_handles_empty_batch_and_nondefault_stream(): + from rl_engine import _C + + stream = torch.cuda.Stream() + with torch.cuda.stream(stream): + x, w, gate = _gated_cuda_inputs(rows=8) + actual, rstd = _C.rmsnorm_gated_forward(x, w, gate, _EPS) + dx = _C.rmsnorm_gated_backward_dx(torch.ones_like(x), x, w, gate, rstd) + empty, empty_rstd = _C.rmsnorm_gated_forward(x[:0], w, gate[:0], _EPS) + empty_dx = _C.rmsnorm_gated_backward_dx(x[:0], x[:0], w, gate[:0], empty_rstd) + stream.synchronize() + expected, expected_rstd = _C.rmsnorm_gated_forward(x, w, gate, _EPS) + expected_dx = _C.rmsnorm_gated_backward_dx(torch.ones_like(x), x, w, gate, expected_rstd) + assert torch.equal(actual, expected) + assert torch.equal(dx, expected_dx) + assert empty.shape == empty_dx.shape == (0, x.shape[-1]) + assert empty_rstd.numel() == 0 From 8df1ffbc4a3984dd3a25bacd1b46633d915dc88a Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:34:21 +0000 Subject: [PATCH 10/44] test(norm): add Qwen3-Next workload and C3 C4 adapters Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- ci/run_ws1_gtest.sh | 10 + rl_engine/kernels/gtest/gradient_adapters.py | 58 +- .../testing/qwen3_next_norm_manifest.json | 759 ++++++++++++++++++ rl_engine/testing/qwen3_next_workload.py | 90 +++ rl_engine/testing/ws1_workload.py | 18 +- scripts/check_forward_invariance.py | 16 +- scripts/check_gradient_invariance.py | 16 +- tests/test_qwen3_next_workload.py | 60 ++ tests/test_ws1_ascend_closeout.py | 22 + 9 files changed, 1035 insertions(+), 14 deletions(-) create mode 100644 rl_engine/testing/qwen3_next_norm_manifest.json create mode 100644 rl_engine/testing/qwen3_next_workload.py create mode 100644 tests/test_qwen3_next_workload.py diff --git a/ci/run_ws1_gtest.sh b/ci/run_ws1_gtest.sh index 0e9b172cc..03b77173d 100755 --- a/ci/run_ws1_gtest.sh +++ b/ci/run_ws1_gtest.sh @@ -75,3 +75,13 @@ for cell in required: ) print("[ws1-gtest] C8 gate passed") PY + +echo "[ws1-gtest] Qwen3-Next C3/C4 norms (operator scope)" +for op in qwen3_next_rms_norm rms_norm_gated; do + for gate in forward gradient; do + "$PY" "scripts/check_${gate}_invariance.py" \ + --manifest rl_engine/testing/qwen3_next_norm_manifest.json \ + --op "$op" --candidate cuda --backend-profile cuda_bf16 \ + --hidden 2048 --head-dim 128 + done +done diff --git a/rl_engine/kernels/gtest/gradient_adapters.py b/rl_engine/kernels/gtest/gradient_adapters.py index 74bb2c0a0..a1fb325c0 100644 --- a/rl_engine/kernels/gtest/gradient_adapters.py +++ b/rl_engine/kernels/gtest/gradient_adapters.py @@ -53,6 +53,7 @@ class GradientAdapterSpec: source_files: tuple[str, ...] shape_dependent_bwd_accum: str = "forbidden" atomic_add: str = "forbidden" + model_id: str | None = None @dataclass(frozen=True) @@ -115,6 +116,26 @@ def to_dict(self) -> dict[str, Any]: "csrc/cuda/rmsnorm.cu", ), ), + "qwen3_next_rms_norm": GradientAdapterSpec( + op_name="qwen3_next_rms_norm", + chain_node="qwen3_next_rms_norm", + op_class="reduction", + spec_name="qwen3_next_rms_norm", + tensors=(_DX, _DWEIGHT), + requirement="required", + source_files=("rl_engine/kernels/ops/cuda/norm/rmsnorm.py", "csrc/cuda/rmsnorm.cu"), + model_id="Qwen/Qwen3-Next-80B-A3B-Instruct", + ), + "rms_norm_gated": GradientAdapterSpec( + op_name="rms_norm_gated", + chain_node="rms_norm_gated", + op_class="reduction", + spec_name="rms_norm_gated", + tensors=(_DX, _DWEIGHT, _DGATE), + requirement="required", + source_files=("rl_engine/kernels/ops/cuda/norm/rmsnorm.py", "csrc/cuda/rmsnorm.cu"), + model_id="Qwen/Qwen3-Next-80B-A3B-Instruct", + ), "qk_norm": GradientAdapterSpec( op_name="qk_norm", chain_node="qk_norm", @@ -291,18 +312,27 @@ def get_adapter(op_name: str) -> GradientAdapterSpec: raise KeyError(f"unknown gradient adapter {op_name!r}") from exc -def required_gradient_adapters() -> tuple[GradientAdapterSpec, ...]: +def required_gradient_adapters( + manifest: WS1Manifest | None = None, +) -> tuple[GradientAdapterSpec, ...]: + selected = manifest or load_manifest() + model_id = selected.model_identity["model_id"] + subset = selected.raw.get("scope") == "qwen3_next_norm_operators" return tuple( spec for spec in GRADIENT_ADAPTERS.values() if spec.requirement in ("required", "layout_supported") + and spec.model_id in (None, model_id) + and (not subset or spec.model_id == model_id) ) -def required_forward_adapters() -> tuple[GradientAdapterSpec, ...]: +def required_forward_adapters( + manifest: WS1Manifest | None = None, +) -> tuple[GradientAdapterSpec, ...]: """Same enumerable WS1 ops as C4; C3 reuses the registry, not a second list.""" - return required_gradient_adapters() + return required_gradient_adapters(manifest) @dataclass(frozen=True) @@ -620,9 +650,9 @@ def _row_parameters( head_dim: int = 16, ) -> dict[str, torch.Tensor]: """Config-independent trainable parameters, built in the execution dtype.""" - if op_name == "rms_norm": + if op_name in {"rms_norm", "qwen3_next_rms_norm"}: return {"weight": _shared_parameter((hidden,), device=device, dtype=dtype, offset=1)} - if op_name == "qk_norm": + if op_name in {"qk_norm", "rms_norm_gated"}: return {"weight": _shared_parameter((head_dim,), device=device, dtype=dtype, offset=1)} if op_name == "det_gemm": return {"b": _shared_parameter((hidden, hidden), device=device, dtype=dtype, offset=2)} @@ -663,12 +693,19 @@ def _row_inputs( """ n = len(keys) leading = (n,) - if op_name == "rms_norm": + if op_name in {"rms_norm", "qwen3_next_rms_norm"}: return { "x": _stack_rows(keys, leading, (hidden,), device=device, dtype=dtype), "weight": params["weight"], "eps": 1.0e-6, } + if op_name == "rms_norm_gated": + return { + "x": _stack_rows(keys, leading, (head_dim,), device=device, dtype=dtype), + "gate": _stack_rows(keys, leading, (head_dim,), device=device, dtype=dtype, offset=7), + "weight": params["weight"], + "eps": 1.0e-6, + } if op_name == "qk_norm": return { "x": _stack_rows(keys, leading, (head_dim,), device=device, dtype=dtype), @@ -1228,6 +1265,15 @@ def resolve_profile_candidate( manifest: WS1Manifest | None = None, ) -> dict[str, Any]: m = manifest if manifest is not None else load_manifest() + subset = m.raw.get("scope") == "qwen3_next_norm_operators" + if adapter.model_id not in (None, m.model_identity["model_id"]) or ( + subset and adapter.model_id != m.model_identity["model_id"] + ): + return { + "status": "absent_not_required", + "expected_backend_id": None, + "candidate_path": None, + } if adapter.requirement == "absent_not_required": return { "status": "absent_not_required", diff --git a/rl_engine/testing/qwen3_next_norm_manifest.json b/rl_engine/testing/qwen3_next_norm_manifest.json new file mode 100644 index 000000000..9cf5977ef --- /dev/null +++ b/rl_engine/testing/qwen3_next_norm_manifest.json @@ -0,0 +1,759 @@ +{ + "version": "1.0", + "workload_id": "qwen3-next-80b-a3b-norm-c3-c4-v1", + "seed": 20260812, + "model_identity": { + "model_id": "Qwen/Qwen3-Next-80B-A3B-Instruct", + "revision": "9c7f2fbe84465e40164a94cc16cd30b6999b0cc7", + "config_fingerprint": { + "num_hidden_layers": 48, + "hidden_size": 2048, + "intermediate_size": 5120, + "num_attention_heads": 16, + "num_key_value_heads": 2, + "head_dim": 256, + "vocab_size": 151936, + "linear_key_head_dim": 128, + "linear_value_head_dim": 128, + "linear_num_key_heads": 16, + "linear_num_value_heads": 32, + "num_experts": 512, + "num_experts_per_tok": 10 + }, + "exit_forbids_architecture_shrink": true, + "weight_snapshot": { + "pin_method": "revision-and-sha256", + "total_size_bytes": 162649725440, + "index_file": "model.safetensors.index.json", + "index_sha256": "0e5274f538d750abdd7fd55a36bad64100f9e881d59c65f6a59f364d2f8b6dd7", + "content_hash_algorithm": "sha256-of-sorted-shard-records-v1", + "content_hash": "00a6a3b5573ccbac2e01fadb673c2cd7bca172d40ddd73f95ed7afa5cbdb9dcd", + "shards": [ + { + "filename": "model-00001-of-00041.safetensors", + "size_bytes": 3999619256, + "sha256": "d8908fd48650169854b5ed815cb01bfc1741152cc58795ec99188862c6a29e11" + }, + { + "filename": "model-00002-of-00041.safetensors", + "size_bytes": 3999841784, + "sha256": "c753c9bfaca220781d4030c3a99e69b4a256434c9d6ec223f5147edc265289df" + }, + { + "filename": "model-00003-of-00041.safetensors", + "size_bytes": 3999515584, + "sha256": "51aaa14dd50c5ab90c363227bfb1ac51182118f588e0141dcaadda012548407c" + }, + { + "filename": "model-00004-of-00041.safetensors", + "size_bytes": 3999842000, + "sha256": "82a33096134fb6e7751a423f142e79b1bbe89f45242b75397c2d7170fffa75bd" + }, + { + "filename": "model-00005-of-00041.safetensors", + "size_bytes": 3999842208, + "sha256": "41abda7bf93f27c36cb28382114f5b33defa6dbfd154abb6886a6fec20f9e479" + }, + { + "filename": "model-00006-of-00041.safetensors", + "size_bytes": 3999853216, + "sha256": "a7794475040ebd62c9a1f9c94c17e7c600873e14fcabc656b32faea09bc7fd2d" + }, + { + "filename": "model-00007-of-00041.safetensors", + "size_bytes": 3999841912, + "sha256": "64d7e90d00ce15cc8bbf7677ad07bf3cffc9e90a1b777e3334fa15ffea219d6b" + }, + { + "filename": "model-00008-of-00041.safetensors", + "size_bytes": 3999842000, + "sha256": "9ace9b99e71490d619656956d457744f727647b3f36fa3d801798a5156599d35" + }, + { + "filename": "model-00009-of-00041.safetensors", + "size_bytes": 3999843192, + "sha256": "5b1b374c9e6100077c446a293566c1f644475c94739ab08b7fa26ba847110216" + }, + { + "filename": "model-00010-of-00041.safetensors", + "size_bytes": 3999517808, + "sha256": "38e51dc850a39c325324e6ddd23347e6e3e76cba1341aeef2e1f260ca2cb9f49" + }, + { + "filename": "model-00011-of-00041.safetensors", + "size_bytes": 4000181296, + "sha256": "c06719dc79fbbc796b8751ccbb29c8ecbdb686de9f89e3c741b5bbfec203cf0c" + }, + { + "filename": "model-00012-of-00041.safetensors", + "size_bytes": 3999843880, + "sha256": "349036c3e3f6a8645af47906dae7272bd5352f822a3c354a16ef4084b3555679" + }, + { + "filename": "model-00013-of-00041.safetensors", + "size_bytes": 3999517472, + "sha256": "bfab17d1535ef23e99e54ff3156ce1456aa5f0a4a93b281a7adb9d8b921dc829" + }, + { + "filename": "model-00014-of-00041.safetensors", + "size_bytes": 3999843984, + "sha256": "318d10df78f1647189941884acc01616a6a78d5d45d74ea30955be7f509c580a" + }, + { + "filename": "model-00015-of-00041.safetensors", + "size_bytes": 4000181736, + "sha256": "aa3400abde789ecca625b8cea37aa31232bf6696923f10981d028518a2393173" + }, + { + "filename": "model-00016-of-00041.safetensors", + "size_bytes": 3999517256, + "sha256": "3776700f68222f0174bb14fea8480da41d928a1bfd6b5317c4fc2b14006d506e" + }, + { + "filename": "model-00017-of-00041.safetensors", + "size_bytes": 3999843880, + "sha256": "052726cd55224d3d217dac93eaed060215b8029afb44861c0d927d87c6a046ab" + }, + { + "filename": "model-00018-of-00041.safetensors", + "size_bytes": 3999843880, + "sha256": "dd1b8d843e0013ed3beb6ba7fc36dc4b1f2df5d71b40520f82f369cd8fce5cba" + }, + { + "filename": "model-00019-of-00041.safetensors", + "size_bytes": 3999844096, + "sha256": "bfe5489c61334f9ce9bc6b369c3edf44420d7f585c6438f3afb98a120b26e6f3" + }, + { + "filename": "model-00020-of-00041.safetensors", + "size_bytes": 3999855040, + "sha256": "eba3113cbd34304751e148502f314be5ceae6e7d830bd33f2c0cf3bb8dfb28c2" + }, + { + "filename": "model-00021-of-00041.safetensors", + "size_bytes": 3999843792, + "sha256": "9ddd72e8ada4480e86e4230639210df1039802db9f78537673c19117d2e69b95" + }, + { + "filename": "model-00022-of-00041.safetensors", + "size_bytes": 3999843880, + "sha256": "c6fc2bb1762d34b8db83fb11566e462c9166e132bb2bfcef45a33a4bd41c02db" + }, + { + "filename": "model-00023-of-00041.safetensors", + "size_bytes": 3999517464, + "sha256": "484bf8c327f7f7aaa20fbc4f6800d4aab7870749335b6f6bbfdb83570a1f66e2" + }, + { + "filename": "model-00024-of-00041.safetensors", + "size_bytes": 3999844264, + "sha256": "cfbb94709f5dac71ffe144ee3b9a976a49a2a9b9863caa29620fbc65820c2342" + }, + { + "filename": "model-00025-of-00041.safetensors", + "size_bytes": 4000181296, + "sha256": "b5799d19dcccfb17b108b5349900a4c42eb7bc241149cb7d1b38b48c707070cf" + }, + { + "filename": "model-00026-of-00041.safetensors", + "size_bytes": 3999517472, + "sha256": "98003819929196d2f6f05c42603e9124a7a7853f372089eed3ffbd0c244fbdac" + }, + { + "filename": "model-00027-of-00041.safetensors", + "size_bytes": 3999843880, + "sha256": "3d6b8b9d1c0c262cc8a63c41c3f75053ab8b31b6107130800718048da64b86db" + }, + { + "filename": "model-00028-of-00041.safetensors", + "size_bytes": 3999843984, + "sha256": "836d4a85cc72243c6caee2b6cf470cd598d6ab817e0685daf1128fd97bb133e3" + }, + { + "filename": "model-00029-of-00041.safetensors", + "size_bytes": 3999855320, + "sha256": "7b083d02c732458cfaaee8b0d1407626ad5ee10fc7eeed6b294b7d6853ac30e8" + }, + { + "filename": "model-00030-of-00041.safetensors", + "size_bytes": 3999843672, + "sha256": "e81006eeaa1152c67798640be46b710e87d44a307effca60cbdb7e9d1a3b268b" + }, + { + "filename": "model-00031-of-00041.safetensors", + "size_bytes": 3999843880, + "sha256": "4fd46dbc55ceefa6146a7687cd02d040087c58b0f7ea42c4cd7dc66b838f531c" + }, + { + "filename": "model-00032-of-00041.safetensors", + "size_bytes": 3999843880, + "sha256": "7edf6738b9bc204f0b39e9c7899515d1a1ed9d7d5f7c1cce32c981120c4f034d" + }, + { + "filename": "model-00033-of-00041.safetensors", + "size_bytes": 3999517688, + "sha256": "9f0f735e75e8d648b4b8d601a598d489fc4e4c7876da4f3406bf3bcc8990a181" + }, + { + "filename": "model-00034-of-00041.safetensors", + "size_bytes": 4000181496, + "sha256": "3499ba70dbb6a9acf87509c3b82c5790461a73f8d0d12f0e3699354f34dc02b7" + }, + { + "filename": "model-00035-of-00041.safetensors", + "size_bytes": 3999843792, + "sha256": "de04b5e39b9494b44becacf22fbe9c02890ef323a3dfe37640a2bfa80335504c" + }, + { + "filename": "model-00036-of-00041.safetensors", + "size_bytes": 3999517472, + "sha256": "3eddb88c62194513f94ef3b1c11577ad324fdcc21265ff7b11a4b50d50b03c5f" + }, + { + "filename": "model-00037-of-00041.safetensors", + "size_bytes": 3999843872, + "sha256": "01992493e00d45d5fec4b5989392f401c9ebe3e13973edaaf1973577e132ca11" + }, + { + "filename": "model-00038-of-00041.safetensors", + "size_bytes": 3999844264, + "sha256": "e328c6d2e5f2a42eb99a0335bde7aa4c42cbd72b88fb988d5a2c96ba6231e95e" + }, + { + "filename": "model-00039-of-00041.safetensors", + "size_bytes": 3999854888, + "sha256": "15063cd0a46af3fa37e74590534f62a81f1ede5340e7640713bf98f39f20961d" + }, + { + "filename": "model-00040-of-00041.safetensors", + "size_bytes": 3365572496, + "sha256": "e458c4f4345d7082e7063f381ee1962725f84c44d53a5b70afb77e601ff22017" + }, + { + "filename": "model-00041-of-00041.safetensors", + "size_bytes": 3301131296, + "sha256": "1f3ac4d828f7e08dd14eb4dc6f282139ae1fa0d894e43f247da2539d2ef43826" + } + ], + "weight_files_total_size_bytes": 162659161528 + } + }, + "chain_semantics": { + "execution_dtype": "bfloat16", + "reference_dtype": "float32", + "accumulation_dtype": "float32", + "temperature": 1.0, + "loss_reduction": "sum_over_active_tokens_then_optional_mean_by_active_count", + "logprob_selection": "selected_token_logprob_on_active_mask", + "active_token_policy": "active selected tokens only", + "aggregates": [ + "max_abs_dlogp", + "approx_kl0", + "clipfrac0" + ], + "clip_interval": [ + 0.8, + 1.2 + ], + "clip_interval_note": "Pinned for clipfrac0; must match C1 chain_logprob_aggregates.default_clip_interval unless an explicit contract revision changes both.", + "comparison_roles_source": "rl_engine/kernels/gtest/tolerance_contract.json", + "forbidden_comparison_roles": [ + "baseline", + "singleton_aggregate" + ], + "singleton_aggregate_note": "singleton_aggregate is a C2 execution/aggregation mode only. It must never populate comparison_lhs_role or comparison_rhs_role.", + "tf32_policy_ref": "rl_engine/kernels/gtest/tolerance_contract.json#/policy/tf32", + "tf32_note": "WS1 TF32 enable/disable is owned by the C1 contract; C2 gates must not introduce a private TF32 policy.", + "report_naming": { + "comparison_lhs_role": "from_c1_by_report_kind", + "comparison_rhs_role": "from_c1_by_report_kind", + "forbidden_in_reports": [ + "baseline", + "singleton_aggregate" + ], + "singleton_aggregate_is": "c2_execution_aggregation_mode_only", + "note": "C2 freezes naming rules; C3+ emit reports that must obey these roles." + }, + "backend_actual_semantics": { + "c2_representative_actual_source": "scripts/ws1_candidate_evidence.py runtime execution", + "full_model_runtime_observed_actual_owner": [ + "C3", + "C8", + "C10", + "C11" + ], + "note": "C2 executes every representative case and records runtime-observed actual backend/kernel provenance. Later children own full-model dispatch provenance." + } + }, + "stochastic_policy": { + "dropout": 0.0, + "attention_dropout": 0.0, + "sampling_in_logprob_parity": false, + "canonical_gate_uses_dropout_zero": true, + "rng_source": "manifest_seed_plus_logical_sample_token_identity", + "undeclared_randomness": "hard_fail", + "retained_stochastic_ops": [] + }, + "primary_matrix": { + "description": "Fixed #150 Batch \u00d7 Chunked-Prefill matrix prerequisite workload cells.", + "N": 4, + "batch_size_bn": 4, + "sample_ids": [ + "s0", + "s1", + "s2", + "s3" + ], + "sample_order_fixed": true, + "batch_permutation": { + "enabled": true, + "permutation": [ + 2, + 0, + 3, + 1 + ], + "target_sample_position_in_bn": 0, + "note": "Permutation exercises layout invariance; logical compare restores sample_id order." + }, + "chunk": { + "chunk_size_tokens": 7, + "require_ge_2_chunks": true, + "non_divisible_case": true, + "note": "Longest primary seq_len=19 with chunk_size=7 yields chunks [7,7,5]." + }, + "cells": [ + { + "cell_id": "B1-singleton_aggregate/full", + "batch_mode": "singleton_aggregate", + "batch_size_per_run": 1, + "num_runs": 4, + "prefill_mode": "full", + "aggregation": { + "order": "sample_ids", + "denominator": "active_token_count_across_all_samples" + } + }, + { + "cell_id": "BN/full", + "batch_mode": "batched", + "batch_size_per_run": 4, + "num_runs": 1, + "prefill_mode": "full", + "aggregation": { + "order": "sample_ids", + "denominator": "active_token_count_across_all_samples" + } + }, + { + "cell_id": "B1-singleton_aggregate/chunked", + "batch_mode": "singleton_aggregate", + "batch_size_per_run": 1, + "num_runs": 4, + "prefill_mode": "chunked", + "aggregation": { + "order": "sample_ids", + "denominator": "active_token_count_across_all_samples" + } + }, + { + "cell_id": "BN/chunked", + "batch_mode": "batched", + "batch_size_per_run": 4, + "num_runs": 1, + "prefill_mode": "chunked", + "aggregation": { + "order": "sample_ids", + "denominator": "active_token_count_across_all_samples" + } + } + ] + }, + "fixtures": { + "prompt_template": "ws1_fixed_token_fixture", + "dtype_for_token_tensors": "int64", + "position_ids": { + "basis": "logical_zero_based_per_sample", + "reset_after_pack_boundary": true + }, + "attention_mask": { + "active_value": 1, + "padding_value": 0, + "causal": true + }, + "primary_seq_len": 19, + "primary_prompt_len": 8, + "short_seq_len": 8, + "long_seq_len": 32, + "varlen_seq_lens": [ + 11, + 16, + 13, + 19 + ], + "padding": { + "modes": [ + "right", + "left" + ], + "pad_token_id": 151643, + "primary_padded_len": 20 + }, + "packing": { + "status": "supported", + "implementation": "rl_engine.kernels.ops.pytorch.packing.pack.NativePackOp", + "packed_fixture": { + "sample_order": [ + "s0", + "s1", + "s2", + "s3" + ], + "segment_lengths": [ + 11, + 16, + 13, + 19 + ], + "total_tokens": 59, + "restore_key": [ + "sample_id", + "token_position" + ] + } + }, + "loss_mask": { + "prompt_tokens_active": false, + "completion_tokens_active": true + }, + "samples": [ + { + "sample_id": "s0", + "seq_len": 11, + "prompt_len": 8, + "token_ids": [ + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 200, + 201, + 202 + ] + }, + { + "sample_id": "s1", + "seq_len": 16, + "prompt_len": 8, + "token_ids": [ + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217 + ] + }, + { + "sample_id": "s2", + "seq_len": 13, + "prompt_len": 8, + "token_ids": [ + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 220, + 221, + 222, + 223, + 224 + ] + }, + { + "sample_id": "s3", + "seq_len": 19, + "prompt_len": 8, + "token_ids": [ + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 230, + 231, + 232, + 233, + 234, + 235, + 236, + 237, + 238, + 239, + 240 + ] + } + ], + "short_full_model_fixture": { + "fixture_id": "short_full_model_seq8", + "seq_len": 8, + "prompt_len": 4, + "token_ids": [ + 310, + 311, + 312, + 313, + 410, + 411, + 412, + 413 + ], + "note": "Shorter sequence on full architecture+weights only; never shrinks layers/hidden/heads/vocab.", + "candidate_case_ids": [ + "short_full_model_seq8_qwen3_next_rms_norm", + "short_full_model_seq8_rms_norm_gated" + ] + }, + "long_full_model_fixture": { + "fixture_id": "long_full_model_seq32", + "seq_len": 32, + "prompt_len": 16, + "token_ids": [ + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 509, + 510, + 511, + 512, + 513, + 514, + 515, + 600, + 601, + 602, + 603, + 604, + 605, + 606, + 607, + 608, + 609, + 610, + 611, + 612, + 613, + 614, + 615 + ], + "note": "Long fixed sequence on the same full architecture and pinned weight snapshot.", + "candidate_case_ids": [ + "long_full_model_seq32_qwen3_next_rms_norm", + "long_full_model_seq32_rms_norm_gated" + ] + }, + "representative_full_model_fixture": { + "fixture_id": "rep_full_model_seq16", + "seq_len": 16, + "prompt_len": 8, + "sample_ids": [ + "s0", + "s1", + "s2", + "s3" + ], + "note": "Primary variable-length matrix fixture; full architecture+weights.", + "candidate_case_ids": [ + "rep_full_model_seq16_qwen3_next_rms_norm", + "rep_full_model_seq16_rms_norm_gated" + ] + }, + "prompt_lens": [ + 8, + 8, + 8, + 8 + ], + "completion_lens": [ + 3, + 8, + 5, + 11 + ], + "max_completion_len": 11 + }, + "logical_identity": { + "key": [ + "sample_id", + "token_position" + ], + "token_position_basis": "logical_unpadded_index_in_sample", + "restore_before_compare_after": [ + "pad", + "pack", + "chunk", + "batch_permute" + ], + "gradient_singleton_aggregate": { + "definition": "N independent B=1 runs of the same N logical samples, aggregated with fixed sample order and active-token denominator", + "compare_to": "single B=N run of the same logical sample/token multiset", + "forbid_different_sample_sets": true + } + }, + "capabilities": { + "required_chain_ops": [ + { + "op": "qwen3_next_rms_norm", + "status": "required" + }, + { + "op": "rms_norm_gated", + "status": "required" + } + ], + "operator_spec_map": { + "qwen3_next_rms_norm": "qwen3_next_rms_norm", + "rms_norm_gated": "rms_norm_gated" + } + }, + "backend_profiles": { + "cuda_bf16": { + "backend_family": "cuda", + "execution_dtype": "bfloat16", + "required_nodes": [ + { + "node": "qwen3_next_rms_norm", + "status": "declared", + "expected_backend_id": "cuda", + "expected_kernel_config_id": "rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormCudaOp", + "algorithm_property": "fixed row reduction and ordered FP32 parameter fold" + }, + { + "node": "rms_norm_gated", + "status": "declared", + "expected_backend_id": "cuda", + "expected_kernel_config_id": "rl_engine.kernels.ops.cuda.norm.rmsnorm.Qwen3NextRMSNormGatedCudaOp", + "algorithm_property": "fixed row reduction and ordered FP32 parameter fold" + } + ] + }, + "triton_cuda_bf16": { + "backend_family": "triton", + "execution_dtype": "bfloat16", + "required_nodes": [ + { + "node": "qwen3_next_rms_norm", + "status": "missing_required", + "expected_backend_id": null, + "expected_kernel_config_id": null, + "algorithm_property": "fixed row reduction and ordered FP32 parameter fold" + }, + { + "node": "rms_norm_gated", + "status": "missing_required", + "expected_backend_id": null, + "expected_kernel_config_id": null, + "algorithm_property": "fixed row reduction and ordered FP32 parameter fold" + } + ] + }, + "ascend_bf16": { + "backend_family": "ascend", + "execution_dtype": "bfloat16", + "required_nodes": [ + { + "node": "qwen3_next_rms_norm", + "status": "missing_required", + "expected_backend_id": null, + "expected_kernel_config_id": null, + "algorithm_property": "fixed row reduction and ordered FP32 parameter fold" + }, + { + "node": "rms_norm_gated", + "status": "missing_required", + "expected_backend_id": null, + "expected_kernel_config_id": null, + "algorithm_property": "fixed row reduction and ordered FP32 parameter fold" + } + ] + } + }, + "representative_cases": [ + { + "case_id": "short_full_model_seq8_qwen3_next_rms_norm", + "fixture_id": "short_full_model_seq8", + "operator_spec": "qwen3_next_rms_norm", + "hidden": 2048, + "architecture_identity": "qwen3_next_80b_a3b_norm_operators" + }, + { + "case_id": "short_full_model_seq8_rms_norm_gated", + "fixture_id": "short_full_model_seq8", + "operator_spec": "rms_norm_gated", + "hidden": 128, + "architecture_identity": "qwen3_next_80b_a3b_norm_operators" + }, + { + "case_id": "long_full_model_seq32_qwen3_next_rms_norm", + "fixture_id": "long_full_model_seq32", + "operator_spec": "qwen3_next_rms_norm", + "hidden": 2048, + "architecture_identity": "qwen3_next_80b_a3b_norm_operators" + }, + { + "case_id": "long_full_model_seq32_rms_norm_gated", + "fixture_id": "long_full_model_seq32", + "operator_spec": "rms_norm_gated", + "hidden": 128, + "architecture_identity": "qwen3_next_80b_a3b_norm_operators" + }, + { + "case_id": "rep_full_model_seq16_qwen3_next_rms_norm", + "fixture_id": "rep_full_model_seq16", + "operator_spec": "qwen3_next_rms_norm", + "hidden": 2048, + "architecture_identity": "qwen3_next_80b_a3b_norm_operators" + }, + { + "case_id": "rep_full_model_seq16_rms_norm_gated", + "fixture_id": "rep_full_model_seq16", + "operator_spec": "rms_norm_gated", + "hidden": 128, + "architecture_identity": "qwen3_next_80b_a3b_norm_operators" + } + ], + "fixture_identity_sha256": "3ac9490ffbb1486edac6ea509811fd4d5affbe880b208bd7d9b052b0ce418442", + "provenance_boundary": { + "scope": "Synthetic norm inputs at checkpoint dimensions; not checkpoint execution or model-level L2.", + "runtime_verified": false + }, + "scope": "qwen3_next_norm_operators", + "full_model_evidence": false +} diff --git a/rl_engine/testing/qwen3_next_workload.py b/rl_engine/testing/qwen3_next_workload.py new file mode 100644 index 000000000..ffe3367cd --- /dev/null +++ b/rl_engine/testing/qwen3_next_workload.py @@ -0,0 +1,90 @@ +"""Qwen3-Next norm-only C3/C4 workload, explicitly separate from the Dense chain.""" + +from collections.abc import Mapping +from typing import Any + +from rl_engine.testing.ws1_workload import ( + WorkloadError, + _validate_backend_profiles, + _validate_fixtures, + _validate_logical_identity, + _validate_model_identity, + _validate_primary_matrix, + _validate_stochastic_policy, + manifest_identity_hash, +) + +MODEL_ID = "Qwen/Qwen3-Next-80B-A3B-Instruct" +REVISION = "9c7f2fbe84465e40164a94cc16cd30b6999b0cc7" +FINGERPRINT = { + "num_hidden_layers": 48, + "hidden_size": 2048, + "intermediate_size": 5120, + "num_attention_heads": 16, + "num_key_value_heads": 2, + "head_dim": 256, + "vocab_size": 151936, + "linear_key_head_dim": 128, + "linear_value_head_dim": 128, + "linear_num_key_heads": 16, + "linear_num_value_heads": 32, + "num_experts": 512, + "num_experts_per_tok": 10, +} +NORM_OPS = {"qwen3_next_rms_norm": 2048, "rms_norm_gated": 128} + + +def validate_norm_manifest(raw: Mapping[str, Any]) -> None: + identity = raw["model_identity"] + if identity["model_id"] != MODEL_ID or identity["revision"] != REVISION: + raise WorkloadError("Qwen3-Next norm workload requires the pinned official checkpoint") + _validate_model_identity(identity, fingerprint=FINGERPRINT, model_label="Qwen3-Next") + if ( + raw.get("scope") != "qwen3_next_norm_operators" + or raw.get("full_model_evidence") is not False + ): + raise WorkloadError("norm workload cannot claim full-model evidence") + _validate_stochastic_policy(raw["stochastic_policy"]) + _validate_primary_matrix(raw["primary_matrix"], raw["fixtures"]) + _validate_fixtures(raw["fixtures"], raw["primary_matrix"]) + _validate_logical_identity(raw["logical_identity"]) + caps = raw["capabilities"] + if {e["op"] for e in caps["required_chain_ops"]} != set(NORM_OPS): + raise WorkloadError("norm workload must contain exactly the two Qwen3-Next norms") + if any(e["status"] != "required" for e in caps["required_chain_ops"]): + raise WorkloadError("both norm operators are required") + _validate_backend_profiles(raw["backend_profiles"], caps) + cases = {case["case_id"]: case for case in raw["representative_cases"]} + if len(cases) != len(raw["representative_cases"]): + raise WorkloadError("duplicate norm case ID") + referenced = set() + for key in ( + "short_full_model_fixture", + "long_full_model_fixture", + "representative_full_model_fixture", + ): + fixture = raw["fixtures"][key] + for case_id in fixture["candidate_case_ids"]: + if case_id not in cases or cases[case_id]["fixture_id"] != fixture["fixture_id"]: + raise WorkloadError("norm case fixture binding mismatch") + referenced.add(case_id) + if referenced != set(cases): + raise WorkloadError("unreferenced norm case") + if {case["operator_spec"] for case in cases.values()} != set(NORM_OPS): + raise WorkloadError("representative cases must cover both norms") + for case in cases.values(): + if case["hidden"] != NORM_OPS[case["operator_spec"]]: + raise WorkloadError("norm case hidden dimension does not match checkpoint") + if case["architecture_identity"] != "qwen3_next_80b_a3b_norm_operators": + raise WorkloadError("norm cases cannot claim Dense or full-model architecture evidence") + if raw["fixture_identity_sha256"] != manifest_identity_hash(raw): + raise WorkloadError("Qwen3-Next fixture identity hash mismatch") + + +def validate_norm_dimensions(raw: Mapping[str, Any], op: str, hidden: int, head_dim: int) -> None: + if raw.get("scope") != "qwen3_next_norm_operators": + return + if op not in NORM_OPS or hidden != 2048 or head_dim != 128: + raise WorkloadError( + "Qwen3-Next norm gate requires its two norm ops, --hidden 2048 --head-dim 128" + ) diff --git a/rl_engine/testing/ws1_workload.py b/rl_engine/testing/ws1_workload.py index 376a7a66b..997f441e7 100644 --- a/rl_engine/testing/ws1_workload.py +++ b/rl_engine/testing/ws1_workload.py @@ -264,7 +264,12 @@ def load_manifest(path: str | Path | None = None) -> WS1Manifest: raw = json.load(fh) if not isinstance(raw, dict): raise WorkloadError("manifest root must be a JSON object") - validate_manifest(raw) + if raw.get("scope") == "qwen3_next_norm_operators": + from rl_engine.testing.qwen3_next_workload import validate_norm_manifest + + validate_norm_manifest(raw) + else: + validate_manifest(raw) return WS1Manifest(raw=raw, path=manifest_path) @@ -292,20 +297,25 @@ def validate_manifest(raw: Mapping[str, Any]) -> None: ) -def _validate_model_identity(identity: Mapping[str, Any]) -> None: +def _validate_model_identity( + identity: Mapping[str, Any], + *, + fingerprint: Mapping[str, Any] = _OFFICIAL_FINGERPRINT, + model_label: str = "Qwen3-8B Dense", +) -> None: for key in ("model_id", "revision", "config_fingerprint", "weight_snapshot"): if key not in identity: raise WorkloadError(f"model_identity missing {key!r}") fp = identity["config_fingerprint"] if not isinstance(fp, Mapping): raise WorkloadError("config_fingerprint must be an object") - for key, expected in _OFFICIAL_FINGERPRINT.items(): + for key, expected in fingerprint.items(): if key not in fp: raise WorkloadError(f"config_fingerprint missing {key!r}") if fp[key] != expected: raise WorkloadError( f"config_fingerprint {key}={fp[key]!r} does not match official " - f"Qwen3-8B Dense pin {expected!r}; architecture shrink is forbidden" + f"{model_label} pin {expected!r}; architecture shrink is forbidden" ) if not identity.get("exit_forbids_architecture_shrink", False): raise WorkloadError("exit_forbids_architecture_shrink must be true") diff --git a/scripts/check_forward_invariance.py b/scripts/check_forward_invariance.py index 8736bc7bc..57a27088b 100644 --- a/scripts/check_forward_invariance.py +++ b/scripts/check_forward_invariance.py @@ -112,6 +112,7 @@ def parse_args() -> argparse.Namespace: if adapter.requirement != "absent_not_required" ] parser = argparse.ArgumentParser(description="WS1 C3 forward invariance GPU gate") + parser.add_argument("--manifest", type=pathlib.Path, default=None) parser.add_argument("--op", choices=sorted(runnable), default="rms_norm") parser.add_argument( "--candidate", required=True, help="Manifest-declared CUDA/Triton/Ascend candidate" @@ -145,7 +146,10 @@ def main() -> None: raise SystemExit("ERROR: --vocab must cover every fixed C2 workload token id") contract = load_contract() - manifest = load_manifest() + manifest = load_manifest(args.manifest) + from rl_engine.testing.qwen3_next_workload import validate_norm_dimensions + + validate_norm_dimensions(manifest.raw, args.op, args.hidden, args.head_dim) adapter = get_adapter(args.op) if adapter.requirement == "layout_supported": raise SystemExit( @@ -231,7 +235,15 @@ def main() -> None: ) if args.json: - print(json.dumps(report.to_dict(), indent=2, default=str)) + payload = report.to_dict() + payload["workload"] = { + "workload_id": manifest.workload_id, + "scope": manifest.raw.get("scope", "qwen3_8b_dense"), + "fixture_identity_sha256": manifest.raw["fixture_identity_sha256"], + "model_id": manifest.model_identity["model_id"], + "full_model_evidence": False, + } + print(json.dumps(payload, indent=2, default=str)) else: _summarize(report) if not report.passed: diff --git a/scripts/check_gradient_invariance.py b/scripts/check_gradient_invariance.py index 606be9a54..c2943a4c2 100644 --- a/scripts/check_gradient_invariance.py +++ b/scripts/check_gradient_invariance.py @@ -124,6 +124,7 @@ def parse_args() -> argparse.Namespace: if adapter.requirement != "absent_not_required" ] parser = argparse.ArgumentParser(description="WS1 C4 gradient invariance GPU gate") + parser.add_argument("--manifest", type=pathlib.Path, default=None) parser.add_argument("--op", choices=sorted(runnable), default="rms_norm") parser.add_argument( "--candidate", required=True, help="Manifest-declared CUDA/Triton/Ascend candidate" @@ -158,7 +159,10 @@ def main() -> None: raise SystemExit(f"ERROR: C4 required-profile evidence needs a real device: {exc}") from exc contract = load_contract() - manifest = load_manifest() + manifest = load_manifest(args.manifest) + from rl_engine.testing.qwen3_next_workload import validate_norm_dimensions + + validate_norm_dimensions(manifest.raw, args.op, args.hidden, args.head_dim) adapter = get_adapter(args.op) if adapter.requirement == "layout_supported": # Pack is the same PyTorch layout op under both profiles and is not a C2 @@ -253,7 +257,15 @@ def main() -> None: ) from exc if args.json: - print(json.dumps(report.to_dict(), indent=2, default=str)) + payload = report.to_dict() + payload["workload"] = { + "workload_id": manifest.workload_id, + "scope": manifest.raw.get("scope", "qwen3_8b_dense"), + "fixture_identity_sha256": manifest.raw["fixture_identity_sha256"], + "model_id": manifest.model_identity["model_id"], + "full_model_evidence": False, + } + print(json.dumps(payload, indent=2, default=str)) else: _summarize(report) if not report.passed: diff --git a/tests/test_qwen3_next_workload.py b/tests/test_qwen3_next_workload.py new file mode 100644 index 000000000..e433de102 --- /dev/null +++ b/tests/test_qwen3_next_workload.py @@ -0,0 +1,60 @@ +"""The norm workload cannot stand in for Dense or full-checkpoint evidence.""" + +import copy +from pathlib import Path + +import pytest + +from rl_engine.kernels.gtest.gradient_adapters import get_adapter, resolve_profile_candidate +from rl_engine.testing.qwen3_next_workload import validate_norm_manifest +from rl_engine.testing.ws1_workload import WorkloadError, load_manifest, manifest_identity_hash + +MANIFEST = Path(__file__).resolve().parents[1] / "rl_engine/testing/qwen3_next_norm_manifest.json" + + +def test_norm_manifest_pins_real_architecture_and_separate_scope(): + manifest = load_manifest(MANIFEST) + assert manifest.model_identity["config_fingerprint"]["hidden_size"] == 2048 + assert manifest.raw["full_model_evidence"] is False + for name, gradients in ( + ("qwen3_next_rms_norm", ("dx", "dweight")), + ("rms_norm_gated", ("dx", "dweight", "dgate")), + ): + adapter = get_adapter(name) + assert tuple(t.name for t in adapter.tensors) == gradients + assert resolve_profile_candidate(adapter, "cuda_bf16", manifest)["status"] == "declared" + assert ( + resolve_profile_candidate(adapter, "triton_cuda_bf16", manifest)["status"] + == "missing_required" + ) + assert ( + resolve_profile_candidate(adapter, "cuda_bf16", load_manifest())["status"] + == "absent_not_required" + ) + + +@pytest.mark.parametrize("fault", ["architecture", "full_model", "revision", "shape", "binding"]) +def test_norm_manifest_rejects_false_evidence_even_with_regenerated_hash(fault): + raw = copy.deepcopy(load_manifest(MANIFEST).raw) + if fault == "architecture": + raw["model_identity"]["config_fingerprint"]["hidden_size"] = 4096 + elif fault == "full_model": + raw["full_model_evidence"] = True + elif fault == "revision": + raw["model_identity"]["revision"] = "main" + elif fault == "shape": + raw["representative_cases"][0]["hidden"] = 64 + else: + raw["representative_cases"][0]["fixture_id"] = "wrong_fixture" + raw["fixture_identity_sha256"] = manifest_identity_hash(raw) + with pytest.raises(WorkloadError): + validate_norm_manifest(raw) + + +def test_norm_dimension_gate_rejects_shrunk_workload(): + from rl_engine.testing.qwen3_next_workload import validate_norm_dimensions + + raw = load_manifest(MANIFEST).raw + with pytest.raises(WorkloadError, match="hidden 2048"): + validate_norm_dimensions(raw, "qwen3_next_rms_norm", 64, 128) + validate_norm_dimensions(raw, "rms_norm_gated", 2048, 128) diff --git a/tests/test_ws1_ascend_closeout.py b/tests/test_ws1_ascend_closeout.py index 0aed72980..29369eb8a 100644 --- a/tests/test_ws1_ascend_closeout.py +++ b/tests/test_ws1_ascend_closeout.py @@ -188,9 +188,15 @@ def test_c2_ascend_cases_pin_real_ascend_kernels_and_sources(): def test_c3_c4_every_required_adapter_resolves_an_ascend_candidate(): manifest = load_manifest() + model_id = manifest.model_identity["model_id"] for name, adapter in GRADIENT_ADAPTERS.items(): if adapter.requirement not in ("required",): continue + # An adapter scoped to another model (the CUDA-only Qwen3-Next norms) + # resolves to `absent_not_required` for this manifest by design; this + # test covers the chain of the model whose manifest it loads. + if adapter.model_id not in (None, model_id): + continue resolved = resolve_profile_candidate(adapter, PROFILE, manifest) assert resolved["status"] == "declared", name assert resolved["expected_backend_id"] == "ascend", name @@ -198,6 +204,22 @@ def test_c3_c4_every_required_adapter_resolves_an_ascend_candidate(): assert candidate_family(str(resolved["expected_backend_id"])) == "ascend" +def test_model_scoped_adapters_are_absent_not_required_for_other_models(): + """The scoping the test above relies on must actually hold. + + Without this, skipping scoped adapters could hide one that silently resolves + to a real candidate for the wrong model. + """ + manifest = load_manifest() + model_id = manifest.model_identity["model_id"] + scoped = [a for a in GRADIENT_ADAPTERS.values() if a.model_id not in (None, model_id)] + assert scoped, "expected at least the Qwen3-Next norm adapters to be model-scoped" + for adapter in scoped: + resolved = resolve_profile_candidate(adapter, PROFILE, manifest) + assert resolved["status"] == "absent_not_required", adapter.op_name + assert resolved["candidate_path"] is None, adapter.op_name + + def test_c4_adapter_status_matrix_has_no_red_ascend_rows(): rows = [r for r in gradient_adapter_status_matrix() if r.backend_profile == PROFILE] assert rows From aaf09864cb4a3ce1574ea79eb7c5490697c05db8 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:40:19 +0000 Subject: [PATCH 11/44] fix(norm): preserve gated signed-zero weights when offset is disabled Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- csrc/cuda/rmsnorm.cu | 9 ++++++--- tests/test_qwen3_next_norm.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index 2d5c98ec4..4249cf525 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -266,7 +266,8 @@ __global__ void rmsnorm_gated_fwd_kernel( for (int col = tid; col < H; col += blockDim.x) { float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col) + weight_offset; + float wv = load_as_float(weight + col); + if (weight_offset != 0.0f) wv += weight_offset; float zv = load_as_float(gate_row + col); float out = xv * row_rstd * wv * gate_activation(zv); store_from_float(y_row + col, out); @@ -301,7 +302,8 @@ __global__ void rmsnorm_gated_bwd_dx_kernel( for (int col = tid; col < H; col += blockDim.x) { float dyv = load_as_float(dy_row + col); float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col) + weight_offset; + float wv = load_as_float(weight + col); + if (weight_offset != 0.0f) wv += weight_offset; float zv = load_as_float(gate_row + col); local_dot += dyv * (wv * gate_activation(zv)) * xv; } @@ -314,7 +316,8 @@ __global__ void rmsnorm_gated_bwd_dx_kernel( for (int col = tid; col < H; col += blockDim.x) { float dyv = load_as_float(dy_row + col); float xv = load_as_float(x_row + col); - float wv = load_as_float(weight + col) + weight_offset; + float wv = load_as_float(weight + col); + if (weight_offset != 0.0f) wv += weight_offset; float zv = load_as_float(gate_row + col); float out = r * dyv * (wv * gate_activation(zv)) - xv * coeff; diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index 2f738a1c8..ff82183fc 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -820,3 +820,17 @@ def test_gated_extension_handles_empty_batch_and_nondefault_stream(): assert torch.equal(dx, expected_dx) assert empty.shape == empty_dx.shape == (0, x.shape[-1]) assert empty_rstd.numel() == 0 + + +@requires_cuda_gated +@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) +def test_cuda_gated_signed_zero_weight_is_preserved(dtype): + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + + x = torch.ones(2, 128, device="cuda", dtype=dtype) + x[1].neg_() + weight = torch.full((128,), -0.0, device="cuda", dtype=dtype) + actual = rmsnorm_gated_cuda(x, weight, torch.ones_like(x)) + expected = x * weight + bits = torch.int32 if dtype == torch.float32 else torch.int16 + assert torch.equal(actual.view(bits), expected.view(bits)) From 4ea1611f48eeb45cb31c21804a96bb8f520450e0 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:28:40 +0000 Subject: [PATCH 12/44] test(dispatch): cover qwen3_next_rms_norm priority and missing-extension fallback The zero-centred norm was registered CUDA-first alongside the gated one, but only rms_norm_gated had dispatch tests. Mirror them: pin the priority on all five platforms, and assert that the inherited RMSNormCudaOp.__init__ symbol check makes the registry fall through to the PyTorch reference. Each assertion was checked to fail on CPU with the behaviour removed: PyTorch first in the CUDA list, the ROCm entry dropped, and the __init__ check removed. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- rl_engine/tests/test_dispatch.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rl_engine/tests/test_dispatch.py b/rl_engine/tests/test_dispatch.py index ed873c2dd..7f1066ab0 100644 --- a/rl_engine/tests/test_dispatch.py +++ b/rl_engine/tests/test_dispatch.py @@ -292,3 +292,31 @@ def test_gated_rms_norm_cuda_backend_reports_absence_by_failing_construction(mon # ... and the registry must therefore hand out the reference, not raise. resolved = KernelRegistry().get_op("rms_norm_gated") assert isinstance(resolved, Qwen3NextRMSNormGatedOp) + + +def test_qwen3_next_rms_norm_priority_is_cuda_first_with_pytorch_fallback(): + """The zero-centred norm follows the gated one: CUDA kernel first, reference elsewhere.""" + registry = KernelRegistry() + + assert registry._priority_map["cuda"]["qwen3_next_rms_norm"] == [ + OpBackend.CUDA_QWEN3_NEXT_RMS_NORM, + OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM, + ] + for platform in ("rocm", "musa", "cpu", "npu"): + assert registry._priority_map[platform]["qwen3_next_rms_norm"] == [ + OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM + ], platform + + +def test_qwen3_next_rms_norm_cuda_backend_reports_absence_by_failing_construction(monkeypatch): + """The inherited ``RMSNormCudaOp.__init__`` check must reach the subclass.""" + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormOp + + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", False) + monkeypatch.setattr(cuda_rmsnorm, "_C", None) + with pytest.raises(RuntimeError, match="requires the compiled rl_engine._C extension"): + cuda_rmsnorm.Qwen3NextRMSNormCudaOp() + + resolved = KernelRegistry().get_op("qwen3_next_rms_norm") + assert isinstance(resolved, Qwen3NextRMSNormOp) From 5eaeca8983f7e057b8e96dc49312020da916e594 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:28:40 +0000 Subject: [PATCH 13/44] test(norm): sweep the gated-vs-plain rstd identity across dtype, H, activation, offset The bitwise rstd identity was asserted for one configuration only (bf16, H=128, silu, offset 0). Parametrize it over fp32/fp16/bf16 x H in {128, 2048, 5120} x {silu, sigmoid} x offset in {0.0, 1.0}, 36 cases, with the plain kernel taking the same offset. All 36 are expected to hold by construction: both kernels run the same sum-of-squares loop, block_reduce_sum and choose_threads(H) launch shape, and neither the gate nor the offset enters the statistic. The H values cover both block sizes (128 and 512 threads) and two per-thread serial lengths. Not yet run on a GPU; the cases skip without the compiled extension. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- tests/test_qwen3_next_norm.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index ff82183fc..f9c10b46e 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -498,17 +498,27 @@ def test_cuda_gated_matches_golden_within_contract(): @requires_cuda_gated -def test_cuda_gated_rstd_is_bitwise_identical_to_plain_kernel(): +@pytest.mark.parametrize("offset", [0.0, 1.0]) +@pytest.mark.parametrize("activation", [0, 1], ids=["silu", "sigmoid"]) +@pytest.mark.parametrize("hidden", [_HEAD_V_DIM, 2048, 5120]) +@pytest.mark.parametrize( + "dtype", [torch.float32, torch.float16, torch.bfloat16], ids=["fp32", "fp16", "bf16"] +) +def test_cuda_gated_rstd_is_bitwise_identical_to_plain_kernel(dtype, hidden, activation, offset): """The gate must not perturb the normalization statistic. Same x, same rstd, bitwise -- otherwise the gate has leaked into the - reduction and the two kernels no longer share a contract. + reduction and the two kernels no longer share a contract. Both kernels + launch with ``choose_threads(H)``: H=128 runs 128 threads with one column + each, 2048 and 5120 run 512 threads with 4 and 10 serial columns each. + Each activation is its own template instantiation, and the offset is + applied only after the statistic, so neither it nor the gate may move it. """ from rl_engine.kernels.ops.base import _C - x, w, gate = _gated_cuda_inputs() - _, rstd_gated = _C.rmsnorm_gated_forward(x, w, gate, _EPS, 0.0, 0) - _, rstd_plain = _C.rmsnorm_forward(x, w, _EPS, 0.0) + x, w, gate = _gated_cuda_inputs(hidden=hidden, dtype=dtype) + _, rstd_gated = _C.rmsnorm_gated_forward(x, w, gate, _EPS, offset, activation) + _, rstd_plain = _C.rmsnorm_forward(x, w, _EPS, offset) assert torch.equal(rstd_gated, rstd_plain) From b0f4c5cae326ce47500efa0237a70bbd41605f8b Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:46:15 +0000 Subject: [PATCH 14/44] fix(norm): address the gated-norm review (R2-2, 7, 8, 9, 12, 13, 14, 16, 17) R2-1 (private bf16 tolerances cited as the reduction row) was resolved in the preceding merge, which switched the gated tests to c1's _forward_tol. - R2-2 docs/operators/qwen3-next-rms-norm-gated.md: Accuracy now points to tolerance_contract.json (forward_accuracy, reduction x dtype); the 35% / 6.25e-2 HF-vs-vLLM figure is labelled a one-off with its setup (one seed, B200, bf16, head_v_dim=128, 512 rows, eager); the 40-seed figures are attributed to tests/check_qwen3_next_norm_providers.py. Withdrawn, as on the c1 page: the cast-order isolation, the per-element fp32 ULP wording, and "fails closed (RFC section 6 item 7)" for configurations the op has no parameter for. Claim levels now read L1 (prefix slices; batch size, chunking, padding, permutation in the C3/C4 gates), L0 not separately tested; the rstd sweep is described. - R2-7 csrc/ops.cpp: drop the dim and size TORCH_CHECKs that rmsnorm_check_weight already performs, in rmsnorm_forward, rmsnorm_backward_dx and rmsnorm_gated_check. Error messages for those cases now come from rmsnorm_check_weight; no test matches the removed messages. - R2-8 workload_report() in rl_engine/testing/ws1_workload.py replaces the payload block duplicated in both gate scripts. full_model_evidence is read from the manifest: False for the Qwen3-Next norm manifest, None (not declared) for the Dense manifest, which previously reported a hard-coded False. - R2-9 the gate scripts import validate_norm_dimensions at module level, and --manifest has a help string. - R2-12 the four dispatch tests become two, parametrized over both norms. The fallback test now resolves for device="cuda", so the CUDA-first list is walked even on a CPU-only host, and asserts the CUDA backend was tried and failed. Before, on CPU, get_op went through the CPU list and the assertion held trivially. - R2-13 SPDX headers on rl_engine/testing/qwen3_next_workload.py and tests/test_qwen3_next_workload.py. - R2-14 ci/run_ws1_gtest.sh runs the four Qwen3-Next gates as explicit commands instead of a loop over interpolated script names. - R2-16 Qwen3NextRMSNormGatedCudaOp takes activation as a constructor argument, validated before the extension check; the test no longer mutates the instance. "swish" stays as vLLM's alias for "silu", now asserted bitwise on CUDA. - R2-17 the Python dgate path adds weight_offset only when it is nonzero, like the kernels, so a -0.0 weight keeps its sign in dgate. New tests: activation rejected at construction (CPU), swish == silu (CUDA), dgate signed zero (CUDA, 3 dtypes). Checked on CPU that the fallback and constructor tests fail with the behaviour removed. The CUDA tests and the csrc change are not yet run on a GPU. Deferred: R2-6 (shared sum-of-squares device function), R2-10 (private validator imports), R2-11. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- ci/run_ws1_gtest.sh | 17 ++-- csrc/ops.cpp | 10 --- docs/operators/qwen3-next-rms-norm-gated.md | 54 ++++++++---- rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 38 +++++--- rl_engine/testing/qwen3_next_workload.py | 3 + rl_engine/testing/ws1_workload.py | 15 ++++ rl_engine/tests/test_dispatch.py | 97 +++++++++++---------- scripts/check_forward_invariance.py | 20 ++--- scripts/check_gradient_invariance.py | 20 ++--- tests/test_qwen3_next_norm.py | 44 +++++++++- tests/test_qwen3_next_workload.py | 3 + 11 files changed, 200 insertions(+), 121 deletions(-) diff --git a/ci/run_ws1_gtest.sh b/ci/run_ws1_gtest.sh index 03b77173d..e6d8e0dc0 100755 --- a/ci/run_ws1_gtest.sh +++ b/ci/run_ws1_gtest.sh @@ -77,11 +77,12 @@ print("[ws1-gtest] C8 gate passed") PY echo "[ws1-gtest] Qwen3-Next C3/C4 norms (operator scope)" -for op in qwen3_next_rms_norm rms_norm_gated; do - for gate in forward gradient; do - "$PY" "scripts/check_${gate}_invariance.py" \ - --manifest rl_engine/testing/qwen3_next_norm_manifest.json \ - --op "$op" --candidate cuda --backend-profile cuda_bf16 \ - --hidden 2048 --head-dim 128 - done -done +QWEN3_NEXT_NORM_MANIFEST=rl_engine/testing/qwen3_next_norm_manifest.json +"$PY" scripts/check_forward_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op qwen3_next_rms_norm --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 +"$PY" scripts/check_gradient_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op qwen3_next_rms_norm --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 +"$PY" scripts/check_forward_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op rms_norm_gated --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 +"$PY" scripts/check_gradient_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op rms_norm_gated --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 diff --git a/csrc/ops.cpp b/csrc/ops.cpp index 1efb116fc..ee9862b28 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -343,10 +343,6 @@ std::vector rmsnorm_forward( rmsnorm_check_input(weight, "weight"); rmsnorm_check_weight(x, weight); - TORCH_CHECK(x.dim() == 2, "x must be 2D [T, H]"); - TORCH_CHECK(weight.dim() == 1, "weight must be 1D [H]"); - TORCH_CHECK(x.size(1) == weight.size(0), "x.size(1) must equal weight.size(0)"); - auto T = x.size(0); auto y = torch::empty_like(x); auto rstd = torch::empty({T}, x.options().dtype(torch::kFloat32)); @@ -371,8 +367,6 @@ torch::Tensor rmsnorm_backward_dx( rmsnorm_check_backward(dy, x, rstd); TORCH_CHECK(dy.sizes() == x.sizes(), "dy and x must have same shape"); - TORCH_CHECK(x.dim() == 2, "x must be 2D [T, H]"); - TORCH_CHECK(weight.dim() == 1, "weight must be 1D [H]"); TORCH_CHECK(rstd.dim() == 1, "rstd must be 1D [T]"); TORCH_CHECK(rstd.size(0) == x.size(0), "rstd.size(0) must equal x.size(0)"); @@ -394,10 +388,6 @@ static void rmsnorm_gated_check( rmsnorm_check_weight(x, weight); rmsnorm_check_input(gate, "gate"); TORCH_CHECK(gate.device() == x.device(), "gate must be on the same device as x"); - - TORCH_CHECK(x.dim() == 2, "x must be 2D [T, H]"); - TORCH_CHECK(weight.dim() == 1, "weight must be 1D [H]"); - TORCH_CHECK(x.size(1) == weight.size(0), "x.size(1) must equal weight.size(0)"); TORCH_CHECK(gate.sizes() == x.sizes(), "gate must have the same shape as x"); TORCH_CHECK( gate.scalar_type() == x.scalar_type(), diff --git a/docs/operators/qwen3-next-rms-norm-gated.md b/docs/operators/qwen3-next-rms-norm-gated.md index 55071d25b..49221c8a0 100644 --- a/docs/operators/qwen3-next-rms-norm-gated.md +++ b/docs/operators/qwen3-next-rms-norm-gated.md @@ -29,6 +29,7 @@ from rl_engine.kernels.ops.cuda.norm.rmsnorm import ( ) y = Qwen3NextRMSNormGatedCudaOp().forward(x, weight, gate, eps=1e-6) +y = Qwen3NextRMSNormGatedCudaOp(activation="sigmoid").forward(x, weight, gate, eps=1e-6) y = rmsnorm_gated_cuda(x, weight, gate, eps=1e-6, activation="sigmoid") ``` @@ -51,15 +52,15 @@ required tensor, so it cannot stand in for one. | `weight` | `[H]` | matches `x` or fp32 | plain, NOT zero-centred | | `gate` | same as `x` | same as `x` | shape, dtype and device must match before flattening | | `eps` | scalar | float | `1e-6` for Qwen3-Next | -| `activation` | — | `"silu"`/`"swish"`/`"sigmoid"` | anything else is rejected | +| `activation` | — | `"silu"`/`"swish"`/`"sigmoid"` | constructor argument; `"swish"` is an alias for `"silu"`, as in vLLM; anything else is rejected | All tensors must share a device. Low-level CUDA bindings require contiguous 2-D inputs, and validate backward gradient dtype and FP32 statistics. Empty batches are supported by the bindings. -Only `norm_before_gate=True` and `group_size=None` are implemented — the single -configuration vLLM's GDN block constructs. Other configurations fail closed rather -than being approximated (RFC #428 §6 item 7). +Only `norm_before_gate=True` and `group_size=None` are implemented — the +configuration vLLM's GDN block constructs. The op has no parameter for either, so +other configurations are not implemented rather than rejected at runtime. ## Dispatch Behavior @@ -70,22 +71,39 @@ that raises at call time. ## Accuracy -Claim level: **L0 repeatable, L1 batch-invariant**. L2 is not claimed. +Claim levels: + +- **L1** for `Qwen3NextRMSNormGatedCudaOp`: prefix slices in + `tests/test_qwen3_next_norm.py`, and batch size, chunking, padding and permutation + in the C3/C4 gates (`ci/run_ws1_gtest.sh`). +- **L0** is not separately tested for the CUDA op; no test repeats it. +- L2 is not claimed. + +Accuracy tests resolve their tolerances from `tolerance_contract.json` +(`forward_accuracy`, `reduction` × dtype). The bounds in +`tests/check_qwen3_next_norm_providers.py` are provider-gap bounds, not contract +thresholds. `transformers` casts the normalized value back to the input dtype *before* the weight -multiply; vLLM keeps it in fp32. On B200 / bf16 / `head_v_dim=128` the two differ in -35% of elements with `max|diff| = 6.25e-2`, and isolating the cast order alone -reproduces the gap — so the cast order dominates, not the reduction order. Because -RFC #428 measures L2 against vLLM rollout, the fp32 multiply is the strict default; -`Qwen3NextRMSNormGatedHFOp` keeps the other convention as a tested witness. - -"Bitwise equal to vLLM" is undefined until a provider is named: vLLM's own -`forward_native` and `forward_cuda` disagreed on 21 of 40 seeds (worst 1.56e-2 in -bf16; ~36% of elements at fp32 ULP in fp32). What this reproduces is the convention; -the residual is the reduction tree. - -`rstd` is bitwise identical to the ungated kernel's for the same `x` — asserted, so a -gate leaking into the statistic would fail. +multiply; vLLM keeps it in fp32. `Qwen3NextRMSNormGatedOp` follows vLLM and +`Qwen3NextRMSNormGatedHFOp` keeps the transformers convention as a witness. +`test_gated_conventions_diverge_in_low_precision` asserts that the two differ in bf16 +(by more than `1e-3`) and agree bitwise in fp32. One earlier measurement, with no +committed script, found them differing on 35% of elements with `max|diff| = 6.25e-2` +(one seed, B200, bf16, `head_v_dim=128`, 512 rows, direct eager calls). That is a +one-off observation, not an assertion. Which convention the strict profile should use +is an open question for RFC #428. + +"Bitwise equal to vLLM" is undefined until a provider is named. Over 40 seeds (bf16, +`head_v_dim=128`, 512 rows, B200), vLLM's eager `forward_native` and `forward_cuda` +disagreed on 21, worst `1.56e-2`; in fp32 they stay within the asserted `1e-5`. These +figures come from `tests/check_qwen3_next_norm_providers.py`, which imports real vLLM +and must be run explicitly. The PyTorch reference reproduces the convention, not +vLLM's reduction tree. + +`rstd` is bitwise identical to the ungated kernel's for the same `x`, asserted over +fp32/fp16/bf16 × H ∈ {128, 2048, 5120} × {silu, sigmoid} × offset ∈ {0, 1}, so a gate +leaking into the statistic would fail. Backward is assembled from deterministic pieces: `dx` from a row-local kernel, `dweight` from fp32 row contributions reduced by the ascending-row left fold, and diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index 3c66e144c..08934d9bb 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -182,10 +182,20 @@ def _require_cuda_symbols(what: str, *names: str) -> None: ) -#: Gate activations understood by the CUDA kernel, in binding order. +#: Gate activations understood by the CUDA kernel, in binding order. ``swish`` is +#: an alias for ``silu``, as in vLLM's GDN block, which maps ``output_gate_type`` +#: "swish" to "silu" before constructing ``RMSNormGated``. _GATE_ACTIVATIONS = {"silu": 0, "swish": 0, "sigmoid": 1} +def _check_gate_activation(activation: str) -> int: + if activation not in _GATE_ACTIVATIONS: + raise ValueError( + f"activation must be one of {sorted(_GATE_ACTIVATIONS)}, got {activation!r}" + ) + return _GATE_ACTIVATIONS[activation] + + def _gate_activation_fp32(gate: torch.Tensor, activation: int) -> torch.Tensor: """act(gate) in fp32, matching the kernel's ``gate_activation``.""" gate32 = gate.float() @@ -257,7 +267,10 @@ def backward(ctx, grad_out): # dgate: row-local and reduction-free. normed = x.float() * rstd.unsqueeze(-1) - scale = weight.float() + ctx.weight_offset + # Same guard as the kernels: an unconditional `+ 0.0` turns -0.0 weights into +0.0. + scale = weight.float() + if ctx.weight_offset != 0.0: + scale = scale + ctx.weight_offset dgate = (dy.float() * normed * scale * _gate_activation_grad_fp32(gate, act)).to(gate.dtype) record_backward( @@ -280,13 +293,8 @@ def rmsnorm_gated_cuda(x, weight, gate, eps=1e-6, weight_offset=0.0, activation= y = rmsnorm_gated_cuda(x, weight, gate) y = rmsnorm_gated_cuda(x, weight, gate, activation="sigmoid") """ - if activation not in _GATE_ACTIVATIONS: - raise ValueError( - f"activation must be one of {sorted(_GATE_ACTIVATIONS)}, got {activation!r}" - ) - return RMSNormGatedCuda.apply( - x, weight, gate, eps, weight_offset, _GATE_ACTIVATIONS[activation] - ) + act = _check_gate_activation(activation) + return RMSNormGatedCuda.apply(x, weight, gate, eps, weight_offset, act) class Qwen3NextRMSNormGatedCudaOp: @@ -298,8 +306,11 @@ class Qwen3NextRMSNormGatedCudaOp: ``out = x * rstd * weight * silu(gate)``, every multiply in fp32 with a single cast at the store. The weight is plain, not zero-centred, matching vLLM's ``RMSNormGated`` with ``norm_before_gate=True`` and ``group_size=None`` - -- which is exactly how the GDN block constructs it. Other configurations - are rejected rather than approximated. + -- which is how the GDN block constructs it. The op has no ``norm_before_gate`` + or ``group_size`` parameter, so other configurations are not implemented. + + ``activation`` is fixed at construction; the registry constructs the + released config's ``"silu"``. """ backward_impl = "cuda_rmsnorm_gated_dx_declared_fp32_rowfold_dw" @@ -307,9 +318,10 @@ class Qwen3NextRMSNormGatedCudaOp: #: The gated weight is plain; kept as an attribute so the surface matches #: the ungated op and a zero-centred variant stays one subclass away. weight_offset = 0.0 - activation = "silu" - def __init__(self) -> None: + def __init__(self, activation: str = "silu") -> None: + _check_gate_activation(activation) + self.activation = activation _require_cuda_symbols( "Gated CUDA RMSNorm", "rmsnorm_gated_forward", "rmsnorm_gated_backward_dx" ) diff --git a/rl_engine/testing/qwen3_next_workload.py b/rl_engine/testing/qwen3_next_workload.py index ffe3367cd..63c1f2399 100644 --- a/rl_engine/testing/qwen3_next_workload.py +++ b/rl_engine/testing/qwen3_next_workload.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + """Qwen3-Next norm-only C3/C4 workload, explicitly separate from the Dense chain.""" from collections.abc import Mapping diff --git a/rl_engine/testing/ws1_workload.py b/rl_engine/testing/ws1_workload.py index 997f441e7..9162aa798 100644 --- a/rl_engine/testing/ws1_workload.py +++ b/rl_engine/testing/ws1_workload.py @@ -273,6 +273,21 @@ def load_manifest(path: str | Path | None = None) -> WS1Manifest: return WS1Manifest(raw=raw, path=manifest_path) +def workload_report(manifest: WS1Manifest) -> dict[str, Any]: + """The ``workload`` block the C3/C4 gate scripts attach to their JSON report. + + ``full_model_evidence`` is whatever the manifest declares, and ``None`` when + it declares nothing. + """ + return { + "workload_id": manifest.workload_id, + "scope": manifest.raw.get("scope", "qwen3_8b_dense"), + "fixture_identity_sha256": manifest.raw["fixture_identity_sha256"], + "model_id": manifest.model_identity["model_id"], + "full_model_evidence": manifest.raw.get("full_model_evidence"), + } + + def validate_manifest(raw: Mapping[str, Any]) -> None: """Hard-fail if any required C2 pin is missing or inconsistent.""" missing = [k for k in _REQUIRED_TOP_LEVEL if k not in raw] diff --git a/rl_engine/tests/test_dispatch.py b/rl_engine/tests/test_dispatch.py index 7f1066ab0..716cb8c09 100644 --- a/rl_engine/tests/test_dispatch.py +++ b/rl_engine/tests/test_dispatch.py @@ -259,64 +259,65 @@ def test_executor_flow(): print(f"\n Test failed with error: {e}") -def test_gated_rms_norm_priority_is_cuda_first_with_pytorch_fallback(): - """The gated norm has a CUDA kernel; every other platform falls back. - - No Triton, ROCm or Ascend gated kernel exists yet, so those platforms must - resolve to the PyTorch reference rather than to nothing -- an operator - missing from a priority map falls through to ``OpBackend.PYTORCH_NATIVE``, - which is the logprob op, not a norm. - """ - registry = KernelRegistry() - - assert registry._priority_map["cuda"]["rms_norm_gated"] == [ +_QWEN3_NEXT_NORMS = [ + pytest.param( + "rms_norm_gated", OpBackend.CUDA_RMS_NORM_GATED, OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED, - ] - for platform in ("rocm", "musa", "cpu", "npu"): - assert registry._priority_map[platform]["rms_norm_gated"] == [ - OpBackend.PYTORCH_NATIVE_RMS_NORM_GATED - ], platform - - -def test_gated_rms_norm_cuda_backend_reports_absence_by_failing_construction(monkeypatch): - """Without the compiled symbols the CUDA backend must be skipped, not returned.""" - from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm - from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormGatedOp - - monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", False) - monkeypatch.setattr(cuda_rmsnorm, "_C", None) - with pytest.raises(RuntimeError, match="requires the compiled rl_engine._C extension"): - cuda_rmsnorm.Qwen3NextRMSNormGatedCudaOp() - - # ... and the registry must therefore hand out the reference, not raise. - resolved = KernelRegistry().get_op("rms_norm_gated") - assert isinstance(resolved, Qwen3NextRMSNormGatedOp) - - -def test_qwen3_next_rms_norm_priority_is_cuda_first_with_pytorch_fallback(): - """The zero-centred norm follows the gated one: CUDA kernel first, reference elsewhere.""" - registry = KernelRegistry() - - assert registry._priority_map["cuda"]["qwen3_next_rms_norm"] == [ + "Qwen3NextRMSNormGatedCudaOp", + "Qwen3NextRMSNormGatedOp", + id="rms_norm_gated", + ), + pytest.param( + "qwen3_next_rms_norm", OpBackend.CUDA_QWEN3_NEXT_RMS_NORM, OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM, - ] + "Qwen3NextRMSNormCudaOp", + "Qwen3NextRMSNormOp", + id="qwen3_next_rms_norm", + ), +] + + +@pytest.mark.parametrize("op_type, cuda_backend, ref_backend, cuda_cls, ref_cls", _QWEN3_NEXT_NORMS) +def test_qwen3_next_norm_priority_is_cuda_first_with_pytorch_fallback( + op_type, cuda_backend, ref_backend, cuda_cls, ref_cls +): + """Both Qwen3-Next norms have a CUDA kernel; every other platform falls back. + + No Triton, ROCm or Ascend kernel exists yet, so those platforms must resolve + to the PyTorch reference rather than to nothing -- an operator missing from a + priority map falls through to ``OpBackend.PYTORCH_NATIVE``, which is the + logprob op, not a norm. + """ + registry = KernelRegistry() + + assert registry._priority_map["cuda"][op_type] == [cuda_backend, ref_backend] for platform in ("rocm", "musa", "cpu", "npu"): - assert registry._priority_map[platform]["qwen3_next_rms_norm"] == [ - OpBackend.PYTORCH_NATIVE_QWEN3_NEXT_RMS_NORM - ], platform + assert registry._priority_map[platform][op_type] == [ref_backend], platform -def test_qwen3_next_rms_norm_cuda_backend_reports_absence_by_failing_construction(monkeypatch): - """The inherited ``RMSNormCudaOp.__init__`` check must reach the subclass.""" +@pytest.mark.skipif(torch.version.hip is not None, reason="a cuda device maps to rocm on HIP") +@pytest.mark.parametrize("op_type, cuda_backend, ref_backend, cuda_cls, ref_cls", _QWEN3_NEXT_NORMS) +def test_qwen3_next_norm_cuda_backend_absence_falls_through_to_reference( + monkeypatch, op_type, cuda_backend, ref_backend, cuda_cls, ref_cls +): + """Without the compiled symbols the CUDA backend must be skipped, not returned. + + The zero-centred op inherits its check from ``RMSNormCudaOp.__init__``. The + lookup names a CUDA device so the CUDA-first list is walked even on a + CPU-only host; resolving for the host's own platform would reach the + reference through the CPU list without ever trying the CUDA backend. + """ from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm - from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormOp + from rl_engine.kernels.ops.pytorch.norm import qwen3_next_rms_norm as reference monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", False) monkeypatch.setattr(cuda_rmsnorm, "_C", None) with pytest.raises(RuntimeError, match="requires the compiled rl_engine._C extension"): - cuda_rmsnorm.Qwen3NextRMSNormCudaOp() + getattr(cuda_rmsnorm, cuda_cls)() - resolved = KernelRegistry().get_op("qwen3_next_rms_norm") - assert isinstance(resolved, Qwen3NextRMSNormOp) + registry = KernelRegistry() + resolved = registry.get_op(op_type, device="cuda") + assert type(resolved) is getattr(reference, ref_cls) + assert cuda_backend.name in registry._failed_backends diff --git a/scripts/check_forward_invariance.py b/scripts/check_forward_invariance.py index 57a27088b..9757946c8 100644 --- a/scripts/check_forward_invariance.py +++ b/scripts/check_forward_invariance.py @@ -40,7 +40,8 @@ resolve_profile_candidate, ) from rl_engine.kernels.gtest.tolerance import resolve_dtype_policy # noqa: E402 -from rl_engine.testing.ws1_workload import load_manifest # noqa: E402 +from rl_engine.testing.qwen3_next_workload import validate_norm_dimensions # noqa: E402 +from rl_engine.testing.ws1_workload import load_manifest, workload_report # noqa: E402 def _object_path(value: Any) -> str: @@ -112,7 +113,12 @@ def parse_args() -> argparse.Namespace: if adapter.requirement != "absent_not_required" ] parser = argparse.ArgumentParser(description="WS1 C3 forward invariance GPU gate") - parser.add_argument("--manifest", type=pathlib.Path, default=None) + parser.add_argument( + "--manifest", + type=pathlib.Path, + default=None, + help="Workload manifest JSON (default: the Qwen3-8B Dense C2 manifest)", + ) parser.add_argument("--op", choices=sorted(runnable), default="rms_norm") parser.add_argument( "--candidate", required=True, help="Manifest-declared CUDA/Triton/Ascend candidate" @@ -147,8 +153,6 @@ def main() -> None: contract = load_contract() manifest = load_manifest(args.manifest) - from rl_engine.testing.qwen3_next_workload import validate_norm_dimensions - validate_norm_dimensions(manifest.raw, args.op, args.hidden, args.head_dim) adapter = get_adapter(args.op) if adapter.requirement == "layout_supported": @@ -236,13 +240,7 @@ def main() -> None: if args.json: payload = report.to_dict() - payload["workload"] = { - "workload_id": manifest.workload_id, - "scope": manifest.raw.get("scope", "qwen3_8b_dense"), - "fixture_identity_sha256": manifest.raw["fixture_identity_sha256"], - "model_id": manifest.model_identity["model_id"], - "full_model_evidence": False, - } + payload["workload"] = workload_report(manifest) print(json.dumps(payload, indent=2, default=str)) else: _summarize(report) diff --git a/scripts/check_gradient_invariance.py b/scripts/check_gradient_invariance.py index c2943a4c2..fb4e8ccdf 100644 --- a/scripts/check_gradient_invariance.py +++ b/scripts/check_gradient_invariance.py @@ -40,7 +40,8 @@ ) from rl_engine.kernels.gtest.gradient_invariance import MissingBackwardError # noqa: E402 from rl_engine.kernels.gtest.tolerance import resolve_dtype_policy # noqa: E402 -from rl_engine.testing.ws1_workload import load_manifest # noqa: E402 +from rl_engine.testing.qwen3_next_workload import validate_norm_dimensions # noqa: E402 +from rl_engine.testing.ws1_workload import load_manifest, workload_report # noqa: E402 def _object_path(value: Any) -> str: @@ -124,7 +125,12 @@ def parse_args() -> argparse.Namespace: if adapter.requirement != "absent_not_required" ] parser = argparse.ArgumentParser(description="WS1 C4 gradient invariance GPU gate") - parser.add_argument("--manifest", type=pathlib.Path, default=None) + parser.add_argument( + "--manifest", + type=pathlib.Path, + default=None, + help="Workload manifest JSON (default: the Qwen3-8B Dense C2 manifest)", + ) parser.add_argument("--op", choices=sorted(runnable), default="rms_norm") parser.add_argument( "--candidate", required=True, help="Manifest-declared CUDA/Triton/Ascend candidate" @@ -160,8 +166,6 @@ def main() -> None: contract = load_contract() manifest = load_manifest(args.manifest) - from rl_engine.testing.qwen3_next_workload import validate_norm_dimensions - validate_norm_dimensions(manifest.raw, args.op, args.hidden, args.head_dim) adapter = get_adapter(args.op) if adapter.requirement == "layout_supported": @@ -258,13 +262,7 @@ def main() -> None: if args.json: payload = report.to_dict() - payload["workload"] = { - "workload_id": manifest.workload_id, - "scope": manifest.raw.get("scope", "qwen3_8b_dense"), - "fixture_identity_sha256": manifest.raw["fixture_identity_sha256"], - "model_id": manifest.model_identity["model_id"], - "full_model_evidence": False, - } + payload["workload"] = workload_report(manifest) print(json.dumps(payload, indent=2, default=str)) else: _summarize(report) diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index f9c10b46e..be48ebd07 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -636,8 +636,7 @@ def test_cuda_gated_parameter_vjp_contributions_match_the_fold(dtype, activation x, w, gate = (t.to(dtype) for t in _gated_cuda_inputs(rows=512)) w = w.float().requires_grad_() dy = torch.randn_like(x) - op = Qwen3NextRMSNormGatedCudaOp() - op.activation = activation + op = Qwen3NextRMSNormGatedCudaOp(activation=activation) op.forward(x, w, gate, eps=_EPS).backward(dy) rows = op.parameter_vjp_contributions_fp32(x=x, weight=w, gate=gate, grad_output=dy, eps=_EPS)[ "weight" @@ -684,6 +683,27 @@ def test_cuda_gated_rejects_bad_activation(): rmsnorm_gated_cuda(x, w, gate, eps=_EPS, activation="gelu") +def test_gated_cuda_op_rejects_bad_activation_at_construction(): + """The activation is validated before the extension check, so this runs on CPU.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp + + with pytest.raises(ValueError, match="activation must be one of"): + Qwen3NextRMSNormGatedCudaOp(activation="gelu") + + +@requires_cuda_gated +def test_cuda_gated_swish_is_an_alias_for_silu(): + """vLLM maps output_gate_type "swish" to "silu"; the alias must not select sigmoid.""" + from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormGatedCudaOp + + x, w, gate = _gated_cuda_inputs(rows=64) + swish = Qwen3NextRMSNormGatedCudaOp(activation="swish").forward(x, w, gate, eps=_EPS) + silu = Qwen3NextRMSNormGatedCudaOp(activation="silu").forward(x, w, gate, eps=_EPS) + sigmoid = Qwen3NextRMSNormGatedCudaOp(activation="sigmoid").forward(x, w, gate, eps=_EPS) + assert torch.equal(swish, silu) + assert not torch.equal(swish, sigmoid) + + @requires_cuda_gated def test_cuda_gated_rejects_mismatched_gate(): from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda @@ -844,3 +864,23 @@ def test_cuda_gated_signed_zero_weight_is_preserved(dtype): expected = x * weight bits = torch.int32 if dtype == torch.float32 else torch.int16 assert torch.equal(actual.view(bits), expected.view(bits)) + + +@requires_cuda_gated +@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16]) +def test_cuda_gated_dgate_keeps_signed_zero_weight(dtype): + """dgate scales by the weight too, so it must not turn a -0.0 weight into +0.0. + + dgate = dy * x * rstd * w * act'(gate); with dy, rstd, act'(1) > 0 its sign is + the sign of ``x * w``, zeros included. + """ + from rl_engine.kernels.ops.cuda.norm.rmsnorm import rmsnorm_gated_cuda + + x = torch.ones(2, 128, device="cuda", dtype=dtype) + x[1].neg_() + weight = torch.full((128,), -0.0, device="cuda", dtype=dtype) + gate = torch.ones_like(x).requires_grad_() + rmsnorm_gated_cuda(x, weight, gate).backward(torch.ones_like(x)) + expected = x * weight + bits = torch.int32 if dtype == torch.float32 else torch.int16 + assert torch.equal(gate.grad.view(bits), expected.view(bits)) diff --git a/tests/test_qwen3_next_workload.py b/tests/test_qwen3_next_workload.py index e433de102..b0fafe26c 100644 --- a/tests/test_qwen3_next_workload.py +++ b/tests/test_qwen3_next_workload.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + """The norm workload cannot stand in for Dense or full-checkpoint evidence.""" import copy From 0cceb8445a81f4b346572bbb387f9ecc4fb10335 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:19:47 +0000 Subject: [PATCH 15/44] fix(rmsnorm): use OptionalCUDAGuard in the launchers outside the ROCm guard The six rmsnorm launchers compiled on both CUDA and ROCm (plain forward and dx, gated forward and dx, partial and reduce dweight) constructed a non-optional c10::cuda::CUDAGuard. No other file the ROCm build compiles uses that class outside a USE_ROCM guard, while activation.cu and deterministic_attention.cu use at::cuda::OptionalCUDAGuard. Match the guard used by activation.cu so the ROCm build relies only on patterns it already compiles. The guard follows the same tensor as before (x, or partial_dw for the reduce launcher). CUDA behaviour is unchanged: these tensors are always on a CUDA device, so the optional guard always sets the device. The left-fold launcher sits inside the USE_ROCM guard and keeps its c10::cuda::CUDAGuard. The ROCm build is still untested. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- csrc/cuda/rmsnorm.cu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csrc/cuda/rmsnorm.cu b/csrc/cuda/rmsnorm.cu index 4249cf525..5a334ea8a 100644 --- a/csrc/cuda/rmsnorm.cu +++ b/csrc/cuda/rmsnorm.cu @@ -466,7 +466,7 @@ void rmsnorm_gated_forward_cuda( double weight_offset, int64_t activation ) { - const c10::cuda::CUDAGuard device_guard(x.device()); + const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); if (T == 0) return; int H = x.size(1); @@ -506,7 +506,7 @@ void rmsnorm_gated_backward_dx_cuda( double weight_offset, int64_t activation ) { - const c10::cuda::CUDAGuard device_guard(x.device()); + const at::cuda::OptionalCUDAGuard device_guard(device_of(x)); int T = x.size(0); if (T == 0) return; int H = x.size(1); From cf68fd94cc10e61b8898d9ff3ac41e57639db09a Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:07:15 +0000 Subject: [PATCH 16/44] docs(norm): reconcile the C1 norm page, docstrings and gated tests with the stacked C1 branch Carried over from the former merge of the C1 branch into this one: the decoder-norm page again states the registration and the check_operator command, the gated tests use the contract's reduction tolerances through _forward_tol, and the reference docstrings name the gated page. Same final tree as before the re-stack. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- docs/operators/qwen3-next-rms-norm.md | 16 ++++++++-------- .../ops/pytorch/norm/qwen3_next_rms_norm.py | 5 +++-- tests/test_qwen3_next_norm.py | 9 +++------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/operators/qwen3-next-rms-norm.md b/docs/operators/qwen3-next-rms-norm.md index e5e9a47c0..420409857 100644 --- a/docs/operators/qwen3-next-rms-norm.md +++ b/docs/operators/qwen3-next-rms-norm.md @@ -49,11 +49,11 @@ y = rmsnorm_cuda(x, weight, eps=1e-6, weight_offset=1.0) ## Dispatch Behavior -Not registered on this branch: there is no `qwen3_next_rms_norm` gtest spec and no -registry entry yet. Both arrive with the gated-norm PR. Until then, construct the -ops directly as in "Entry Point". The CUDA op validates the compiled symbols in -`__init__`, so on a build without the extension construction raises instead of -handing out an op that fails at call time. +Registered as the `qwen3_next_rms_norm` gtest operator. On CUDA the registry +prefers `Qwen3NextRMSNormCudaOp`; every other platform resolves to the PyTorch +reference. The CUDA op validates the compiled symbols in `__init__`, so on a build +without the extension construction raises and the registry falls through to the +reference rather than handing out an op that fails at call time. ## Accuracy @@ -102,16 +102,16 @@ element and no extra memory traffic. ```bash python -m pytest tests/test_qwen3_next_norm.py -v +python scripts/check_operator.py --op qwen3_next_rms_norm --candidate cuda \ + --device cuda --dtype bf16 --check-grad ``` ## Known Limitations - CUDA only; no ROCm, Ascend or Triton backend. -- Not registered as a gtest operator or in the registry on this branch (see - "Dispatch Behavior"). - Not bitwise against vLLM (see Accuracy). An L2 claim needs a single source of truth for the forward on both sides, per RFC #428 §0 item 1. - The gated pair (`Qwen3NextRMSNormGatedOp`, `Qwen3NextRMSNormGatedHFOp`) is - documented with its CUDA kernel in the gated-norm PR, not on this page. + documented with its CUDA kernel on [Gated RMSNorm](qwen3-next-rms-norm-gated.md). - Measured on sm_100 (B200). Per RFC #428 §2.2 no claim carries across H100/H200/B100/B200. diff --git a/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py index 1f84f86f3..598eaf3ad 100644 --- a/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py +++ b/rl_engine/kernels/ops/pytorch/norm/qwen3_next_rms_norm.py @@ -22,7 +22,8 @@ All three reuse :func:`shape_invariant_rstd`, a fixed-order reduction. They reproduce the weight convention and cast order, not vLLM's reduction tree, and are not bitwise equal to any vLLM path probed so far. Claim levels, measurements and -limitations are in ``docs/operators/qwen3-next-rms-norm.md``. +limitations are in ``docs/operators/qwen3-next-rms-norm.md`` and, for the gated +pair, ``docs/operators/qwen3-next-rms-norm-gated.md``. """ from __future__ import annotations @@ -61,7 +62,7 @@ class Qwen3NextRMSNormGatedOp: ``out = (x * rstd * weight) * silu(gate)``, with every multiply in fp32 and a single cast on the way out. This is the convention vLLM's ``RMSNormGated`` uses with ``norm_before_gate=True``. Which gated convention is the strict - default is still open; see the operator page. + default is still open; see ``docs/operators/qwen3-next-rms-norm-gated.md``. Not a subclass of the plain op: it takes an extra tensor and its epilogue differs, so it is not a drop-in substitute for one. diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index be48ebd07..ffa20253f 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -475,9 +475,6 @@ def test_cuda_zero_centred_backward_is_offset_aware(): not _HAS_CUDA_GATED, reason="gated RMSNorm CUDA extension is not available" ) -# tolerance_contract.json, judgments/forward_accuracy/by_op_class/reduction/bfloat16 -_BF16_ATOL, _BF16_RTOL = 2e-2, 1.6e-2 - def _gated_cuda_inputs(seed=0, rows=512, hidden=_HEAD_V_DIM, dtype=torch.bfloat16): g = torch.Generator(device="cuda").manual_seed(seed) @@ -494,7 +491,7 @@ def test_cuda_gated_matches_golden_within_contract(): x, w, gate = _gated_cuda_inputs() got = Qwen3NextRMSNormGatedCudaOp().forward(x, w, gate, eps=_EPS) ref = Qwen3NextRMSNormGatedOp().forward_fp32(x, w, gate, eps=_EPS) - torch.testing.assert_close(got.float(), ref, atol=_BF16_ATOL, rtol=_BF16_RTOL) + torch.testing.assert_close(got.float(), ref, **_forward_tol(torch.bfloat16)) @requires_cuda_gated @@ -551,7 +548,7 @@ def test_cuda_gated_unit_weight_is_plain_norm_times_silu(): ones = torch.ones(_HEAD_V_DIM, device="cuda", dtype=x.dtype) got = Qwen3NextRMSNormGatedCudaOp().forward(x, ones, gate, eps=_EPS).float() ref = rmsnorm_cuda(x, ones, eps=_EPS).float() * F.silu(gate.float()) - torch.testing.assert_close(got, ref, atol=_BF16_ATOL, rtol=_BF16_RTOL) + torch.testing.assert_close(got, ref, **_forward_tol(torch.bfloat16)) @requires_cuda_gated @@ -561,7 +558,7 @@ def test_cuda_gated_sigmoid_activation(): x, w, gate = _gated_cuda_inputs() got = rmsnorm_gated_cuda(x, w, gate, eps=_EPS, activation="sigmoid").float() ref = rmsnorm_cuda(x, w, eps=_EPS).float() * torch.sigmoid(gate.float()) - torch.testing.assert_close(got, ref, atol=_BF16_ATOL, rtol=_BF16_RTOL) + torch.testing.assert_close(got, ref, **_forward_tol(torch.bfloat16)) @requires_cuda_gated From ef4c88de2ec684b329c258c532260a84389c7888 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:39:36 +0000 Subject: [PATCH 17/44] feat(ws1): Gated DeltaNet decode-step goldens for RFC #428 C6 The trainer-side reference for a Qwen3-Next rollout decode step: the recurrent Gated DeltaNet update and the causal-conv1d state update that precedes it. Claim level: L0 repeatable and L1 batch-invariant; L2 is not claimed. Design notes, measurements and the deferral list are in docs/design/ws1-c6-428-gdn-recurrent-replay.md. Provider -------- `fused_recurrent_gated_delta_rule_packed_decode`, not `fused_sigmoid_gating_delta_rule_update`. A decode-only, non-speculative batch returns early at qwen_gdn_linear_attn.py:1295-1307 into the packed path because VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE defaults to true; the sigmoid-gating kernel is the fallback for mixed prefill+decode and spec decode. The env defaults that decide this are asserted, so a vLLM bump fails loudly. Three places the kernel and the HF model disagree, transcribed from the kernel: * The gating is fused -- beta = sigmoid(b) and g = -exp(A_log) * softplus(a + dt_bias) are computed in fp32 inside the kernel with a threshold branch at 20, not as separate PyTorch ops. * No repeat_interleave: the kernel indexes i_h = i_hv // (HV // H). * The QK norm is an L2 norm over a plain sum, x / sqrt(sum(x*x) + 1e-6), dividing by sqrt rather than multiplying by rsqrt. scale hits q after the norm; k is never scaled. A fourth candidate divergence turned out not to be one: prefill passes use_qk_l2norm_in_kernel=False only because fused_post_conv_prep(apply_l2norm=True) already normalized q/k. Both paths normalize exactly once. Conv accumulation order is load-bearing: taps accumulate sequentially, acc = acc + win[t] * w[t] from zero. With an fp32 cache that reproduces the provider bitwise; a tree sum over the same four terms does not, and neither does an FMA. State ABI is mirrored: paged [num_blocks, HV, V, K] and [num_blocks, dim, width-1], NULL_BLOCK_ID (<= 0) skipping, an fp32 accumulator, and a store that rounds to the cache dtype -- fp32 or bf16, per FUSED_GDN_STATE_DTYPES. Contractions run in the repo's fixed 32-wide chunk order via one `_chunked_sum` primitive, which is what the L1 claim rests on. Agreement, B200, Qwen3-Next dims (H=16, HV=32, K=V=128) ------------------------------------------------------- recurrent, fp32 state, B=1..64 max|d out| 1.5e-08..6.1e-05, state <= 3.0e-07 recurrent, bf16 state, B=1..64 max|d out| 3.7e-09..3.1e-05, state <= 2.0e-03 conv: the rolled state is BITWISE exact in every configuration conv, fp32 cache: output bitwise but for 15 of 524288 elements at B=64 conv, bf16 cache: output agrees on ~63%, each disagreement one bf16 ULP L1: a sequence's output and state block are bitwise identical alone or at any position in a batch of 64, for both state dtypes Where the provider rounds in the bf16-conv-cache case is not reproduced; recorded as an open gap rather than guessed at. Decode versus chunked prefill ----------------------------- T=8 max|d| 3.66e-04 fp32 state / 5.49e-04 bf16 (7.0e-03 / 1.0e-02 rel) T=64 4.88e-04 / 5.49e-04 (6.6e-03 / 7.5e-03 rel) T=256 3.66e-04 / 3.66e-04 (5.0e-03 / 5.0e-03 rel) ~0.5-1% relative and flat in T. The recurrence is contracting -- per-step decay exp(g) averages ~0.47 -- so old rounding error is forgotten at roughly the rate old signal is, and the fp32-vs-bf16 state drift saturates at ~2% relative on the state (2.67e-03 at step 1, 1.52e-02 at 64, 2.47e-02 at 1024: a 16x longer run past step 64 grows it 1.6x). So the two paths are not bitwise, and ~1% relative on logits is still material for RL importance ratios, but this is a bounded error rather than a divergence. An fp32 recurrent state remains the right choice; the reason is the plateau, not a blow-up. Separately, the chunked prefill kernel refuses fp32 q/k/v outright (chunk.py:213), and causal_conv1d_update does not bounds-check conv_state_indices under its default validate_data=False. tests/check_gdn_recurrent_golden.py is named check_ rather than test_, following tests/distributed/check_*.py: it imports real vLLM, and tests/test_framework_operator_integrations.py asserts vllm is absent from sys.modules. pytest tests/check_gdn_recurrent_golden.py -q pytest tests/ rl_engine/tests/ -q -p no:randomly \ --ignore=tests/test_rocm_aiter_api_contract.py The new file passes and the full suite gains no failure. Absolute suite counts are reported in the PR description against a named base commit, not here: they shift whenever a sibling test is added, so a count frozen in a commit message goes stale the moment the branch is rebased. Not covered, deliberately: speculative decode and MTP, a backward for the recurrence, and the provider bridge. Reasons in the design doc. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .../design/ws1-c6-428-gdn-recurrent-replay.md | 145 +++++ .../ops/pytorch/linear_attn/__init__.py | 20 + .../ops/pytorch/linear_attn/causal_conv1d.py | 193 +++++++ .../pytorch/linear_attn/gated_delta_rule.py | 294 ++++++++++ tests/check_gdn_recurrent_golden.py | 501 ++++++++++++++++++ 5 files changed, 1153 insertions(+) create mode 100644 docs/design/ws1-c6-428-gdn-recurrent-replay.md create mode 100644 rl_engine/kernels/ops/pytorch/linear_attn/__init__.py create mode 100644 rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py create mode 100644 rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py create mode 100644 tests/check_gdn_recurrent_golden.py diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/ws1-c6-428-gdn-recurrent-replay.md new file mode 100644 index 000000000..d0d0f3ea3 --- /dev/null +++ b/docs/design/ws1-c6-428-gdn-recurrent-replay.md @@ -0,0 +1,145 @@ +# WS1 C6 — Qwen3-Next Gated DeltaNet recurrent replay + +Design notes for RFC #428 work item C6 (GDN recurrent response replay) on the CUDA +track. Measured on 2× B200 (sm_100), torch 2.13.0+cu130, vllm 0.30.0, +transformers 5.17.0. + +Claim level reached: **L0 repeatable, L1 batch-invariant**. L2 is not claimed. + +## 1. Which provider a rollout decode actually takes + +vLLM 0.30.0 has three GDN decode paths, selected by env defaults rather than by the +model: + +| condition | path | +|---|---| +| `VLLM_GDN_DECODE_KERNEL="cuda"` (default) + MTP | `torch.ops._C.fused_gdn_decode_post_conv_mtp` — conv, recurrence and the gated norm fused | +| `VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE=1` (default), decode-only, non-spec | `fused_recurrent_gated_delta_rule_packed_decode` | +| otherwise | `fused_sigmoid_gating_delta_rule_update` | + +A pure RL rollout decode — N sequences each emitting one token, no speculative +decoding — returns early at `qwen_gdn_linear_attn.py:1295-1307` into the **packed** +path. The golden targets that one. Aligning against the sigmoid-gating kernel would +validate a path production does not take. + +`tests/check_qwen3_next_norm_providers.py` asserts those two env defaults, so a vLLM +bump that flips either fails loudly rather than silently re-pointing the claim. + +## 2. Three places the kernel and the HF model disagree + +The golden is transcribed from the kernel, not from `modeling_qwen3_next.py`: + +1. **The gating is fused.** `beta = sigmoid(b)` and + `g = -exp(A_log) * softplus(a + dt_bias)` are computed inside the Triton kernel, + with a `softplus` threshold branch at 20. HF computes them as separate PyTorch + ops — a different rounding path. +2. **No `repeat_interleave`.** The kernel indexes `i_h = i_hv // (HV // H)`, so q/k + stay at 16 heads while v has 32. HF materializes the repeat. +3. **The QK norm is an L2 norm over a plain sum**, `x / sqrt(sum(x*x) + 1e-6)` — not + an RMSNorm, not `F.normalize`, and dividing by `sqrt` rather than multiplying by + `rsqrt`, which differs in the last bit. `scale` is applied to `q` *after* the + norm; `k` is never scaled. + +A fourth, checked and found **not** to be a divergence: prefill passes +`use_qk_l2norm_in_kernel=False` only because `fused_post_conv_prep(apply_l2norm=True)` +already normalized q/k. Both paths normalize exactly once. + +## 3. State ABI + +Mirrored rather than reinvented: + +- recurrent state `[num_blocks, HV, V, K]`, V-major, addressed by `ssm_state_indices` +- conv state `[num_blocks, dim, width-1]`, layout chosen by the global + `is_conv_state_dim_first()`; the golden takes it as an argument and both are tested +- `NULL_BLOCK_ID` (index `<= 0`) means skip: zeros out, block untouched +- the accumulator is fp32 for the whole step; the store rounds to the state tensor's + dtype, which `FUSED_GDN_STATE_DTYPES` allows to be fp32 **or** bf16 +- `causal_conv1d_update` casts `x` to the cache dtype before anything else + +Contractions run in the repo's fixed 32-wide chunk order rather than `torch.matmul`, +whose reduction order is unspecified. That is what the L1 claim rests on, and also +why the golden is not bitwise against the kernel's tree. + +## 4. Agreement with the provider + +Qwen3-Next dims (H=16, HV=32, K=V=128), `use_qk_l2norm_in_kernel=True`: + +| | max\|diff\| out | max\|diff\| state | +|---|---|---| +| fp32 state, B=1..64 | 1.5e-08 .. 6.1e-05 | ≤ 3.0e-07 | +| bf16 state, B=1..64 | 3.7e-09 .. 3.1e-05 | ≤ 2.0e-03 | + +Causal conv (`conv_dim=8192`, `W=4`, B ≤ 64): the **rolled state is bitwise exact in +every configuration**. With an fp32 cache the output is bitwise but for a handful of +elements (15 of 524288 at B=64); with a bf16 cache it agrees on ~63%, each +disagreement exactly one bf16 ULP. The conv taps accumulate **sequentially**, +`acc = acc + win[t] * w[t]` from zero — a tree sum over the same four terms does not +reproduce the provider, an FMA does not either. + +Open: where the provider rounds in the bf16-conv-cache case is not reproduced. +Recorded rather than guessed at. + +## 5. Decode versus chunked prefill + +The quantity RFC #428 §4.2 is about. Single sequence, zero initial state: + +| T | max\|diff\|, fp32 state | max\|diff\|, bf16 state | +|---|---|---| +| 8 | 3.66e-04 (7.0e-03 rel) | 5.49e-04 (1.0e-02 rel) | +| 64 | 4.88e-04 (6.6e-03 rel) | 5.49e-04 (7.5e-03 rel) | +| 256 | 3.66e-04 (5.0e-03 rel) | 3.66e-04 (5.0e-03 rel) | + +**The gap is ~0.5–1% relative and flat in T** — smaller at T=256 than at T=8 — and the +state dtype barely moves it. + +The reason is that the recurrence is **contracting**: per-step decay `exp(g)` averages +~0.47 (max 0.996), so old rounding error is forgotten at roughly the rate old signal +is. Comparing an fp32 state against a bf16 one over 1024 steps shows the same shape: + +| step | relative \|d\| state | relative \|d\| out | +|---|---|---| +| 1 | 2.67e-03 | 3.47e-03 | +| 64 | 1.52e-02 | 1.07e-02 | +| 256 | 2.02e-02 | 1.09e-02 | +| 1024 | 2.47e-02 | 1.57e-02 | + +A 16× longer run past step 64 grows the drift only 1.6×; it saturates at ~2% relative +on the state and ~1–1.8% on the output. + +So the two paths are not bitwise, and ~1% relative on logits is still material for RL +importance ratios, but this is a bounded, characterizable error rather than a +divergence. An fp32 recurrent state remains the right choice for exactness work — the +reason is the 2% plateau, not a blow-up. + +Two related constraints: the chunked prefill kernel refuses fp32 q/k/v outright +(`chunk.py:213`), so the prefill side is bf16-only regardless; and +`causal_conv1d_update` does not bounds-check `conv_state_indices` under its default +`validate_data=False` — an index past the cache is an out-of-bounds write, not an +error. + +## 6. Current boundary + +Not covered, with reasons: + +| deferred | why | +|---|---| +| Speculative decode / MTP | `fused_gdn_decode_post_conv_mtp` fuses conv, recurrence and the gated norm; validating it needs a draft model | +| Backward for the recurrent step | no upstream backward exists, and a naive BPTT through a sequential recurrence is not batch-invariant — it needs its own design | +| Provider bridge / registry entry | the golden should survive a drift sweep against a real checkpoint first | +| Paged block allocation policy | the ABI is mirrored; the allocator is not modelled | +| TP sharding of `A_log` / `dt_bias` | single card only | +| ROCm / Ascend | CUDA first, per the WS1 order | + +`runtime_verified=false` (no checkpoint), `supports_backward=false`, +`checkpoint=absent`. Op-level agreement says nothing about 48 composed layers. + +## 7. Questions for the maintainers + +1. Is a **bf16 recurrent state** a supported configuration? The answer decides whether + the 2% plateau is a finding or a non-issue. +2. Is MTP / speculative decode in scope for WS1? If so, + `fused_gdn_decode_post_conv_mtp` becomes the primary provider for both the norm and + the recurrence, and the largest deferral above reopens. +3. For the CUDA strict profile, is the single source of truth **vLLM's** arithmetic or + **PyTorch eager**? §1 item 1 asks for one provider on both sides but §2.1 does not + say which, and the two differ by ~6e-2 in bf16. diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/__init__.py b/rl_engine/kernels/ops/pytorch/linear_attn/__init__.py new file mode 100644 index 000000000..28602f18f --- /dev/null +++ b/rl_engine/kernels/ops/pytorch/linear_attn/__init__.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Linear-attention operators (Gated DeltaNet and friends).""" + +from rl_engine.kernels.ops.pytorch.linear_attn.causal_conv1d import ( + CausalConv1dUpdateOp, +) +from rl_engine.kernels.ops.pytorch.linear_attn.gated_delta_rule import ( + GatedDeltaRuleRecurrentStepOp, + NULL_BLOCK_ID, + SOFTPLUS_THRESHOLD, +) + +__all__ = [ + "CausalConv1dUpdateOp", + "GatedDeltaRuleRecurrentStepOp", + "NULL_BLOCK_ID", + "SOFTPLUS_THRESHOLD", +] diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py b/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py new file mode 100644 index 000000000..ffbac6e13 --- /dev/null +++ b/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py @@ -0,0 +1,193 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Causal depthwise conv1d single-token state update (WS1 ground truth, RFC #428 C6). + +The trainer-side reference for ``causal_conv1d_update``, which vLLM calls once +per decode token before the Gated DeltaNet recurrence. + +Semantics were established by differential testing against the provider rather +than transcribed, and the accumulation order matters: + +* ``x`` is cast to ``conv_state.dtype`` **before** anything else, so a bf16 + conv cache rounds the incoming token (``causal_conv1d.py:1160``). +* The window is ``[state[..., -(width-1):], x]``; the taps are accumulated + **sequentially in tap order**, ``acc = acc + win[t] * w[t]`` starting from + zero -- not a tree reduction and not an FMA. With an fp32 conv state this + reproduces the provider bitwise; a tree sum does not. +* Bias and the activation are applied in fp32, with one cast on the way out to + the input's original dtype. +* The new state is the window minus its oldest column. + +Measured against the provider on B200 (``conv_dim=8192``, ``W=4``, B up to 64): + +* **The rolled state is bitwise exact in every configuration**, fp32 and bf16 + cache alike -- ``max|diff| = 0``. That is the part the next token consumes, + so the recurrence carries no error from here. +* With an **fp32** conv state the output is bitwise on all but a handful of + elements (15 of 524288 at B=64), the residual being fp32 ULP. +* With a **bf16** conv state the output agrees on ~63% of elements, every + disagreement exactly one bf16 ULP. + +The bf16 output gap is a rounding-path difference, not a semantic one: none of +accumulate-in-bf16, bias-in-bf16 or activation-in-bf16 reproduces the provider, +so where it rounds is left unresolved rather than guessed at. An fp32 conv state +is what an exactness claim should use anyway -- the same conclusion the +recurrent state reaches in :mod:`.gated_delta_rule`. +""" + +from __future__ import annotations + +import torch +import torch.nn.functional as F + +from rl_engine.kernels.ops.pytorch.linear_attn.gated_delta_rule import NULL_BLOCK_ID + +__all__ = ["CausalConv1dUpdateOp"] + + +class CausalConv1dUpdateOp: + """One decode token through the paged causal-conv1d cache. + + ============== ========================== ========================== + tensor shape notes + ============== ========================== ========================== + ``x`` ``[B, dim]`` + ``conv_state`` ``[num_blocks, dim, W-1]`` paged; ``dim_first`` layout + ``weight`` ``[dim, W]`` + ``bias`` ``[dim]`` or ``None`` + ``indices`` ``[B]`` ``<= 0`` skips + ============== ========================== ========================== + + Returns ``(out, conv_state)``; the state is updated out of place. + """ + + def __call__( + self, + x: torch.Tensor, + conv_state: torch.Tensor, + weight: torch.Tensor, + conv_state_indices: torch.Tensor, + *, + bias: torch.Tensor | None = None, + activation: str | None = "silu", + dim_first: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor]: + return self.forward( + x, + conv_state, + weight, + conv_state_indices, + bias=bias, + activation=activation, + dim_first=dim_first, + ) + + def forward( + self, + x: torch.Tensor, + conv_state: torch.Tensor, + weight: torch.Tensor, + conv_state_indices: torch.Tensor, + *, + bias: torch.Tensor | None = None, + activation: str | None = "silu", + dim_first: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor]: + return self._update( + x, + conv_state, + weight, + conv_state_indices, + bias=bias, + activation=activation, + dim_first=dim_first, + output_dtype=x.dtype, + ) + + def forward_fp32( + self, + x: torch.Tensor, + conv_state: torch.Tensor, + weight: torch.Tensor, + conv_state_indices: torch.Tensor, + *, + bias: torch.Tensor | None = None, + activation: str | None = "silu", + dim_first: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Ground truth: fp32 output, so only the cache dtype rounds.""" + return self._update( + x, + conv_state, + weight, + conv_state_indices, + bias=bias, + activation=activation, + dim_first=dim_first, + output_dtype=torch.float32, + ) + + @staticmethod + def _update( + x, + conv_state, + weight, + conv_state_indices, + *, + bias, + activation, + dim_first, + output_dtype, + ): + if activation not in (None, "silu", "swish"): + raise ValueError(f"activation must be None, 'silu' or 'swish', got {activation!r}") + if x.dim() != 2: + raise ValueError(f"x must be 2-D [B, dim], got {tuple(x.shape)}") + if conv_state.dim() != 3: + raise ValueError( + f"conv_state must be 3-D [num_blocks, dim, W-1], got {tuple(conv_state.shape)}" + ) + if conv_state_indices.dim() != 1 or conv_state_indices.shape[0] != x.shape[0]: + raise ValueError("conv_state_indices must be 1-D with one entry per row of x") + + # The "SD" layout stores (state_len, dim); the kernels want (dim, state_len). + state = conv_state if dim_first else conv_state.transpose(-1, -2) + dim, tail = state.shape[-2], state.shape[-1] + width = weight.shape[-1] + if weight.shape[0] != dim: + raise ValueError(f"weight must be [dim, W] with dim={dim}, got {tuple(weight.shape)}") + if tail != width - 1: + raise ValueError(f"conv_state tail must be W-1={width - 1}, got {tail}") + + active = conv_state_indices > NULL_BLOCK_ID + out = torch.zeros(x.shape, dtype=torch.float32, device=x.device) + new_state = conv_state.clone() # the cache dtype is the contract here + if not bool(active.any()): + return out.to(output_dtype), new_state + + rows = torch.nonzero(active, as_tuple=False).flatten() + blocks = conv_state_indices[rows].long() + + # The incoming token is rounded to the cache dtype before it is used. + token = x[rows].to(conv_state.dtype) + window = torch.cat([state[blocks], token.unsqueeze(-1)], dim=-1) # [R, dim, W] + + # Sequential tap accumulation from zero, in fp32. A tree sum over the + # same four terms gives a different last bit and does NOT match. + acc = torch.zeros(len(rows), dim, dtype=torch.float32, device=x.device) + w32 = weight.float() + for tap in range(width): + acc = acc + window[..., tap].float() * w32[:, tap].unsqueeze(0) + if bias is not None: + acc = acc + bias.float() + if activation in ("silu", "swish"): + acc = F.silu(acc) + + out[rows] = acc + rolled = window[..., 1:] + if dim_first: + new_state[blocks] = rolled.to(conv_state.dtype) + else: + new_state[blocks] = rolled.transpose(-1, -2).to(conv_state.dtype) + return out.to(output_dtype), new_state diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py new file mode 100644 index 000000000..cdf6f17b7 --- /dev/null +++ b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py @@ -0,0 +1,294 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Gated DeltaNet single-token recurrent step (WS1 ground truth for RFC #428 C6). + +This is the trainer-side reference for what vLLM runs during rollout decode: +``fused_recurrent_gated_delta_rule_packed_decode``. That kernel -- not +``fused_sigmoid_gating_delta_rule_update`` -- is the path a pure RL rollout +takes, because ``VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE`` defaults to true and +a decode-only, non-speculative batch returns early into it. + +Three things are transcribed from the kernel rather than from the HuggingFace +model, because they differ: + +* **The gating is fused.** ``beta = sigmoid(b)`` and + ``g = -exp(A_log) * softplus(a + dt_bias)`` are computed here, in fp32, with + the kernel's ``softplus`` threshold branch. HF computes them separately in + PyTorch, which is a different rounding path. +* **No ``repeat_interleave``.** The kernel indexes ``i_h = i_hv // (HV // H)``, + so q/k stay at ``H`` heads while v has ``HV``. HF materializes the repeat. +* **The QK norm is an L2 norm over a plain sum**, ``x / sqrt(sum(x*x) + 1e-6)``, + not an RMSNorm and not ``F.normalize``. It divides by ``sqrt`` rather than + multiplying by ``rsqrt``; the two differ in the last bit, so the division is + kept. + +``scale`` is applied to ``q`` *after* the L2 norm; ``k`` is never scaled. + +State ABI (mirrored, not reinvented) +------------------------------------ +The recurrent state is paged: ``[num_blocks, HV, V, K]``, addressed per +sequence by ``ssm_state_indices``. Index ``<= 0`` is ``NULL_BLOCK_ID`` and means +"skip": the kernel writes zeros to the output and leaves the block untouched. +The accumulator is fp32 throughout the step, and the store rounds to the state +tensor's dtype -- which upstream allows to be fp32 *or* bf16. That rounding is +part of the recurrence and compounds across tokens, so it is modelled here +rather than skipped. +""" + +from __future__ import annotations + +import torch + +__all__ = ["GatedDeltaRuleRecurrentStepOp", "NULL_BLOCK_ID", "SOFTPLUS_THRESHOLD"] + +#: Paged-state sentinel: a sequence pointing here is skipped. +NULL_BLOCK_ID = 0 + +#: Above this, softplus is the identity (matches the kernel's constexpr). +SOFTPLUS_THRESHOLD = 20.0 + +#: Width of the fixed-order reduction chunks. The contraction order must not +#: depend on the batch layout, so it is pinned here exactly as +#: :func:`~rl_engine.kernels.ops.pytorch.norm.rms_norm.shape_invariant_rstd` +#: pins the RMSNorm statistic. +_REDUCTION_CHUNK = 32 + + +def _chunked_sum(x: torch.Tensor) -> torch.Tensor: + """Sum the last dim in a fixed 32-wide chunk order. + + The single reduction primitive for this module. Deliberately not + ``sum``/``matmul``/``einsum`` on the whole axis: their reduction order is + unspecified and may vary with shape, which is what a batch-invariant claim + cannot tolerate. Mirrors + :func:`~rl_engine.kernels.ops.pytorch.norm.rms_norm.shape_invariant_rstd`. + """ + tail = x.shape[-1] + if tail % _REDUCTION_CHUNK != 0: + return x.sum(dim=-1) + return x.reshape(*x.shape[:-1], -1, _REDUCTION_CHUNK).sum(dim=-1).sum(dim=-1) + + +def _fixed_order_contract(mat: torch.Tensor, vec: torch.Tensor) -> torch.Tensor: + """``sum(mat * vec, dim=-1)`` in a fixed chunk order.""" + return _chunked_sum(mat * vec) + + +def _l2_normalize(x: torch.Tensor, eps: float = 1e-6) -> torch.Tensor: + """``x / sqrt(sum(x*x) + eps)`` over the last dim, in fp32. + + A plain sum, not a mean: this is an L2 norm, not an RMSNorm. The division is + kept rather than folded into a reciprocal-sqrt multiply because the kernel + divides. + """ + return x / torch.sqrt(_chunked_sum(x * x) + eps).unsqueeze(-1) + + +def _softplus(x: torch.Tensor, threshold: float = SOFTPLUS_THRESHOLD) -> torch.Tensor: + """The kernel's branched softplus; the branch matters for large ``a + dt_bias``.""" + return torch.where(x <= threshold, torch.log1p(torch.exp(x)), x) + + +class GatedDeltaRuleRecurrentStepOp: + """One decode token of the Gated DeltaNet recurrence, over a paged state. + + Shapes follow the provider, not the model definition: + + ============= ====================================== =================== + tensor shape notes + ============= ====================================== =================== + ``mixed_qkv`` ``[B, H*K + H*K + HV*V]`` q | k | v, packed + ``a``, ``b`` ``[B, HV]`` + ``A_log`` ``[HV]`` fp32 + ``dt_bias`` ``[HV]`` fp32 + ``state`` ``[num_blocks, HV, V, K]`` paged, V-major + ``indices`` ``[B]`` ``<= 0`` skips + ============= ====================================== =================== + + Returns ``(out, state)`` with ``out`` of shape ``[B, 1, HV, V]``. The state + is updated out of place; pass the result back to continue the recurrence. + """ + + def __call__( + self, + mixed_qkv: torch.Tensor, + a: torch.Tensor, + b: torch.Tensor, + A_log: torch.Tensor, + dt_bias: torch.Tensor, + state: torch.Tensor, + ssm_state_indices: torch.Tensor, + *, + scale: float, + num_k_heads: int, + use_qk_l2norm: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor]: + return self.forward( + mixed_qkv, + a, + b, + A_log, + dt_bias, + state, + ssm_state_indices, + scale=scale, + num_k_heads=num_k_heads, + use_qk_l2norm=use_qk_l2norm, + ) + + def forward( + self, + mixed_qkv: torch.Tensor, + a: torch.Tensor, + b: torch.Tensor, + A_log: torch.Tensor, + dt_bias: torch.Tensor, + state: torch.Tensor, + ssm_state_indices: torch.Tensor, + *, + scale: float, + num_k_heads: int, + use_qk_l2norm: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Step once, rounding the stored state to ``state.dtype``.""" + return self._step( + mixed_qkv, + a, + b, + A_log, + dt_bias, + state, + ssm_state_indices, + scale=scale, + num_k_heads=num_k_heads, + use_qk_l2norm=use_qk_l2norm, + state_dtype=state.dtype, + output_dtype=mixed_qkv.dtype, + ) + + def forward_fp32( + self, + mixed_qkv: torch.Tensor, + a: torch.Tensor, + b: torch.Tensor, + A_log: torch.Tensor, + dt_bias: torch.Tensor, + state: torch.Tensor, + ssm_state_indices: torch.Tensor, + *, + scale: float, + num_k_heads: int, + use_qk_l2norm: bool = True, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Ground truth: keep the state and the output in fp32. + + The difference from :meth:`forward` is the per-token state rounding, so + running both and comparing isolates how much that rounding costs. + """ + return self._step( + mixed_qkv, + a, + b, + A_log, + dt_bias, + state, + ssm_state_indices, + scale=scale, + num_k_heads=num_k_heads, + use_qk_l2norm=use_qk_l2norm, + state_dtype=torch.float32, + output_dtype=torch.float32, + ) + + @staticmethod + def _step( + mixed_qkv, + a, + b, + A_log, + dt_bias, + state, + ssm_state_indices, + *, + scale, + num_k_heads, + use_qk_l2norm, + state_dtype, + output_dtype, + ): + if mixed_qkv.dim() != 2: + raise ValueError(f"mixed_qkv must be 2-D [B, D], got {tuple(mixed_qkv.shape)}") + if state.dim() != 4: + raise ValueError( + f"state must be 4-D [num_blocks, HV, V, K], got {tuple(state.shape)}" + ) + if ssm_state_indices.dim() != 1: + raise ValueError("ssm_state_indices must be 1-D [B] for packed decode") + + batch = mixed_qkv.shape[0] + hv, v_dim, k_dim = state.shape[-3:] + heads = int(num_k_heads) + if hv % heads != 0: + raise ValueError(f"HV={hv} must be a multiple of num_k_heads={heads}") + if a.shape != (batch, hv) or b.shape != (batch, hv): + raise ValueError( + f"a/b must be [B, HV] = {(batch, hv)}, got {tuple(a.shape)} / {tuple(b.shape)}" + ) + expected = heads * k_dim * 2 + hv * v_dim + if mixed_qkv.shape[1] != expected: + raise ValueError( + f"mixed_qkv last dim must be {expected} (q|k|v packed), " + f"got {mixed_qkv.shape[1]}" + ) + if ssm_state_indices.shape[0] != batch: + raise ValueError("ssm_state_indices must have one entry per sequence") + + group = hv // heads + qkv32 = mixed_qkv.float() + + # Unpack q | k | v. q and k carry H heads, v carries HV. + q = qkv32[:, : heads * k_dim].reshape(batch, heads, k_dim) + k = qkv32[:, heads * k_dim : 2 * heads * k_dim].reshape(batch, heads, k_dim) + v = qkv32[:, 2 * heads * k_dim :].reshape(batch, hv, v_dim) + + if use_qk_l2norm: + q = _l2_normalize(q) + k = _l2_normalize(k) + q = q * scale + + # i_h = i_hv // (HV // H): index, do not materialize a repeat. + head_of = torch.arange(hv, device=q.device) // group + q = q[:, head_of, :] # [B, HV, K] + k = k[:, head_of, :] + + # Fused gating, fp32, with the kernel's softplus branch. + decay = -torch.exp(A_log.float()) * _softplus(a.float() + dt_bias.float()) + beta = torch.sigmoid(b.float()) + + active = ssm_state_indices > NULL_BLOCK_ID + out = torch.zeros(batch, 1, hv, v_dim, dtype=torch.float32, device=q.device) + # The returned state carries `state_dtype`, not the caller's: forward_fp32 + # exists precisely to run the recurrence without the per-token rounding, + # so it must be able to widen a bf16 cache to fp32. + new_state = state.to(state_dtype).clone() + if not bool(active.any()): + return out.to(output_dtype), new_state + + rows = torch.nonzero(active, as_tuple=False).flatten() + blocks = ssm_state_indices[rows].long() + + h = state[blocks].float() # [R, HV, V, K] + h = h * torch.exp(decay[rows]).unsqueeze(-1).unsqueeze(-1) + + k_sel, q_sel, v_sel = k[rows], q[rows], v[rows] + # v -= h @ k, then v *= beta, then h += outer(v, k), then o = h @ q. + v_sel = v_sel - _fixed_order_contract(h, k_sel.unsqueeze(-2)) + v_sel = v_sel * beta[rows].unsqueeze(-1) + h = h + v_sel.unsqueeze(-1) * k_sel.unsqueeze(-2) + o = _fixed_order_contract(h, q_sel.unsqueeze(-2)) + + out[rows, 0] = o + # The store rounds; that rounding is part of the recurrence. + new_state[blocks] = h.to(state_dtype) + return out.to(output_dtype), new_state diff --git a/tests/check_gdn_recurrent_golden.py b/tests/check_gdn_recurrent_golden.py new file mode 100644 index 000000000..e9e18487c --- /dev/null +++ b/tests/check_gdn_recurrent_golden.py @@ -0,0 +1,501 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""The GDN recurrent-step golden against the kernel vLLM actually runs (RFC #428 C6). + +Named ``check_`` rather than ``test_``, following ``tests/distributed/check_*.py``: +this module imports real vLLM, and ``tests/test_framework_operator_integrations.py`` +asserts ``vllm`` is absent from ``sys.modules`` -- an invariant any collected test +importing vLLM would break for the whole session. Run it explicitly: + + pytest tests/check_gdn_recurrent_golden.py -v + +The provider here is ``fused_recurrent_gated_delta_rule_packed_decode``, not +``fused_sigmoid_gating_delta_rule_update``. A pure RL rollout decode -- N +sequences each emitting one token, no speculative decoding -- returns early into +the packed path because ``VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE`` defaults to +true. Aligning against the sigmoid-gating kernel would be validating a path +production does not take. + +Claim levels (RFC #428 section 2.1): L0 and L1 for the golden. L2 is not claimed; +the golden agrees with the provider at fp32-ULP scale but not bitwise, because +its contractions run in a fixed 32-wide chunk order rather than the kernel's +tree. + +Recurrent-state dtype, and how far a decode run actually drifts +---------------------------------------------------------------- +``FUSED_GDN_STATE_DTYPES`` allows the recurrent state to be fp32 or bf16, and a +bf16 store rounds the ``[HV, V, K]`` state once per token. The drift that causes +does **not** compound without bound, because the recurrence is contracting: the +per-step decay ``exp(g)`` averages ~0.47 here (max 0.996), so old error is +forgotten at roughly the rate old signal is. + +fp32 state vs bf16 state, identical inputs, B=8, B200: + +====== ================= =============== +step relative |d| state relative |d| out +====== ================= =============== +1 2.67e-03 3.47e-03 +64 1.52e-02 1.07e-02 +256 2.02e-02 1.09e-02 +1024 2.47e-02 1.57e-02 +====== ================= =============== + +The curve saturates: a 16x longer run past step 64 grows the state drift only +1.6x. The plateau is ~2% relative on the state and ~1-1.8% on the output. + +Chunked prefill vs step-by-step decode replay, single sequence, from a zero +state -- this is the quantity RFC #428 section 4.2 is about: + +====== ================== ================== +T max|diff|, fp32 max|diff|, bf16 +====== ================== ================== +8 3.66e-04 (7.0e-03) 5.49e-04 (1.0e-02) +64 4.88e-04 (6.6e-03) 5.49e-04 (7.5e-03) +256 3.66e-04 (5.0e-03) 3.66e-04 (5.0e-03) +====== ================== ================== + +(relative in parentheses). The gap is ~0.5-1% relative and **flat in T** -- at +T=256 it is smaller than at T=8 -- and the state dtype barely moves it. + +So the two paths are not bitwise, and ~1% relative on logits is still material +for RL importance ratios, but this is a bounded, characterizable error rather +than a divergence. An fp32 recurrent state is still the right choice for +exactness work; the reason is the 2% state plateau, not a blow-up. + +Note the chunked prefill kernel refuses fp32 q/k/v outright +(``chunk.py:213``), so the prefill side is bf16-only regardless. +""" + +from __future__ import annotations + +import pytest +import torch + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required") + +from rl_engine.kernels.ops.pytorch.linear_attn import ( # noqa: E402 + GatedDeltaRuleRecurrentStepOp, +) + +# Qwen3-Next-80B-A3B-Instruct: linear_num_key_heads / linear_num_value_heads / +# linear_key_head_dim / linear_value_head_dim. +_H, _HV, _K, _V = 16, 32, 128, 128 +_SCALE = _K**-0.5 +_PACKED_DIM = _H * _K * 2 + _HV * _V + + +def _vllm_step(): + pytest.importorskip("vllm", reason="vLLM is required to compare against the provider") + from vllm.third_party.flash_linear_attention.ops import ( + fused_recurrent_gated_delta_rule_packed_decode, + ) + + return fused_recurrent_gated_delta_rule_packed_decode + + +def _inputs(batch, num_blocks, state_dtype, io_dtype, seed, indices=None): + g = torch.Generator(device="cuda").manual_seed(seed) + rand = lambda *shape, dtype: torch.randn( # noqa: E731 + *shape, device="cuda", dtype=dtype, generator=g + ) + if indices is None: + indices = torch.arange(1, batch + 1, device="cuda", dtype=torch.int32) + return { + "mixed_qkv": rand(batch, _PACKED_DIM, dtype=io_dtype), + "a": rand(batch, _HV, dtype=io_dtype), + "b": rand(batch, _HV, dtype=io_dtype), + "A_log": rand(_HV, dtype=torch.float32), + "dt_bias": rand(_HV, dtype=torch.float32), + "state": rand(num_blocks, _HV, _V, _K, dtype=state_dtype) * 0.1, + "indices": indices, + } + + +def _run_provider(inp, io_dtype=torch.bfloat16): + """Returns (out, mutated_state). The kernel updates the state in place.""" + step = _vllm_step() + state = inp["state"].clone() + out = torch.empty( + inp["mixed_qkv"].shape[0], 1, _HV, _V, device="cuda", dtype=io_dtype + ) + step( + inp["mixed_qkv"], + inp["a"], + inp["b"], + inp["A_log"], + inp["dt_bias"], + _SCALE, + state, + out, + inp["indices"], + use_qk_l2norm_in_kernel=True, + ) + return out, state + + +def _run_golden(inp): + return GatedDeltaRuleRecurrentStepOp().forward( + inp["mixed_qkv"], + inp["a"], + inp["b"], + inp["A_log"], + inp["dt_bias"], + inp["state"].clone(), + inp["indices"], + scale=_SCALE, + num_k_heads=_H, + ) + + +# --------------------------------------------------------------------------- # +# 1. Against the provider +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("batch", [1, 4, 17, 64]) +@pytest.mark.parametrize( + "state_dtype, out_atol, state_atol", + [ + # fp32 state: agreement is at fp32-ULP scale. + (torch.float32, 1e-3, 1e-5), + # bf16 state: the store rounds every token, so the state carries a + # bf16 ULP. This is the configuration knob behind the drift probe. + (torch.bfloat16, 1e-3, 5e-3), + ], +) +def test_golden_matches_packed_decode_provider(batch, state_dtype, out_atol, state_atol): + inp = _inputs(batch, batch + 2, state_dtype, torch.bfloat16, seed=batch) + out_ref, state_ref = _run_provider(inp) + out_got, state_got = _run_golden(inp) + + assert (out_got.float() - out_ref.float()).abs().max().item() <= out_atol + assert (state_got.float() - state_ref.float()).abs().max().item() <= state_atol + + +def test_null_block_id_is_skipped_by_both(): + """Index 0 means "no state": zeros out, and the block is left alone.""" + indices = torch.tensor([1, 0, 2, 0], device="cuda", dtype=torch.int32) + inp = _inputs(4, 4, torch.float32, torch.bfloat16, seed=7, indices=indices) + + out_ref, state_ref = _run_provider(inp) + out_got, state_got = _run_golden(inp) + + for row in (1, 3): + assert bool((out_ref[row] == 0).all()), f"provider row {row}" + assert bool((out_got[row] == 0).all()), f"golden row {row}" + assert torch.equal(state_got[0], inp["state"][0]), "block 0 must be untouched" + + +# --------------------------------------------------------------------------- # +# 2. L1 -- a sequence is unaffected by the others sharing the batch +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("state_dtype", [torch.float32, torch.bfloat16]) +def test_golden_is_batch_invariant(state_dtype): + """Bitwise: the same sequence, alone or in a batch of 64, at any position.""" + batch = 64 + inp = _inputs(batch, batch + 2, state_dtype, torch.bfloat16, seed=3) + full_out, full_state = _run_golden(inp) + + op = GatedDeltaRuleRecurrentStepOp() + for row in (0, 1, 31, 63): + alone = { + key: (value[row : row + 1] if key in ("mixed_qkv", "a", "b", "indices") else value) + for key, value in inp.items() + } + out, state = op.forward( + alone["mixed_qkv"], + alone["a"], + alone["b"], + alone["A_log"], + alone["dt_bias"], + alone["state"].clone(), + alone["indices"], + scale=_SCALE, + num_k_heads=_H, + ) + assert torch.equal(out[0], full_out[row]), f"out row {row}" + block = int(inp["indices"][row]) + assert torch.equal(state[block], full_state[block]), f"state block {block}" + + +# --------------------------------------------------------------------------- # +# 3. The state-dtype rounding is modelled, not skipped +# --------------------------------------------------------------------------- # +def test_bf16_state_rounding_is_visible_but_saturates(): + """A bf16 state rounds from the first store, and the drift then flattens. + + The recurrence is contracting -- ``exp(g)`` is well below 1 for most heads -- + so old rounding error is forgotten at roughly the rate old signal is. The + drift therefore grows quickly at first and then plateaus, rather than + compounding without bound. Asserting a bound is the honest form: asserting + monotone growth would have been true over a truncated run and false over a + long one. + """ + batch, steps = 8, 128 + inp = _inputs(batch, batch + 2, torch.float32, torch.bfloat16, seed=11) + op = GatedDeltaRuleRecurrentStepOp() + + # Both runs start from the SAME bf16-representable state, so the initial + # cast is a no-op and drift[0] can only come from a per-token store. + seed_state = inp["state"].to(torch.bfloat16).float() + state_fp32 = seed_state.clone() + state_bf16 = seed_state.to(torch.bfloat16).clone() + assert torch.equal(state_fp32, state_bf16.float()), "the two runs must start equal" + drift = [] + for step in range(steps): + g = torch.Generator(device="cuda").manual_seed(100 + step) + token = torch.randn( + batch, _PACKED_DIM, device="cuda", dtype=torch.bfloat16, generator=g + ) + common = (inp["a"], inp["b"], inp["A_log"], inp["dt_bias"]) + _, state_fp32 = op.forward( + token, *common, state_fp32, inp["indices"], scale=_SCALE, num_k_heads=_H + ) + _, state_bf16 = op.forward( + token, *common, state_bf16, inp["indices"], scale=_SCALE, num_k_heads=_H + ) + scale = max(state_fp32.float().abs().max().item(), 1e-9) + drift.append((state_fp32.float() - state_bf16.float()).abs().max().item() / scale) + + assert drift[0] > 0.0, "a bf16 state must round on the very first store" + # Measured plateau is ~2% relative; the bound leaves room without hiding a + # genuine divergence. + assert max(drift) < 0.05, f"relative state drift reached {max(drift):.3e}" + # The back half must not be materially worse than the front half. + assert max(drift[steps // 2 :]) < 2.0 * max(drift[: steps // 2]) + 1e-3 + + +# --------------------------------------------------------------------------- # +# 4. The causal-conv1d state update, the other half of a decode step +# --------------------------------------------------------------------------- # +_CONV_DIM, _CONV_WIDTH = 8192, 4 # Qwen3-Next: key_dim*2 + value_dim, linear_conv_kernel_dim + + +def _conv_update(): + pytest.importorskip("vllm", reason="vLLM is required to compare against the provider") + from vllm.model_executor.layers.mamba.ops.causal_conv1d import causal_conv1d_update + + return causal_conv1d_update + + +def _conv_inputs(batch, state_dtype, seed, with_bias=True, indices=None): + g = torch.Generator(device="cuda").manual_seed(seed) + rand = lambda *shape, dtype: torch.randn( # noqa: E731 + *shape, device="cuda", dtype=dtype, generator=g + ) + if indices is None: + indices = torch.arange(1, batch + 1, device="cuda", dtype=torch.int32) + return { + "x": rand(batch, _CONV_DIM, dtype=torch.bfloat16), + # The cache must have room for every index used, plus the null block. + "state": rand(batch + 2, _CONV_DIM, _CONV_WIDTH - 1, dtype=state_dtype), + "weight": rand(_CONV_DIM, _CONV_WIDTH, dtype=torch.bfloat16), + "bias": rand(_CONV_DIM, dtype=torch.bfloat16) if with_bias else None, + "indices": indices, + } + + +def _run_conv_pair(inp, activation="silu"): + from rl_engine.kernels.ops.pytorch.linear_attn import CausalConv1dUpdateOp + + state_ref, out_ref = inp["state"].clone(), torch.empty_like(inp["x"]) + _conv_update()( + inp["x"], + state_ref, + inp["weight"], + inp["bias"], + activation, + conv_state_indices=inp["indices"], + out=out_ref, + ) + out_got, state_got = CausalConv1dUpdateOp().forward( + inp["x"], + inp["state"].clone(), + inp["weight"], + inp["indices"], + bias=inp["bias"], + activation=activation, + ) + return (out_ref, state_ref), (out_got, state_got) + + +@pytest.mark.parametrize("batch", [1, 4, 17, 64]) +def test_conv_state_update_is_bitwise_exact(batch): + """The rolled window is what the next token consumes, so it must be exact. + + Holds for an fp32 and a bf16 cache alike -- the rolling is a copy, not a + computation, which is also why bias and activation are not varied here: they + cannot reach the state. + """ + for state_dtype in (torch.float32, torch.bfloat16): + inp = _conv_inputs(batch, state_dtype, seed=batch) + (_, state_ref), (_, state_got) = _run_conv_pair(inp) + assert torch.equal(state_got, state_ref), f"{state_dtype} batch={batch}" + + +@pytest.mark.parametrize("batch", [1, 4, 17, 64]) +def test_conv_output_matches_provider_with_fp32_cache(batch): + """An fp32 cache reproduces the provider up to fp32 ULP on a few elements. + + Measured: 1 element of 8192 at B=1, 15 of 524288 at B=64. The bound is on + the magnitude and on a handful of elements rather than on a tight rate -- + at small batches a single straddling element is already 1.2e-4 of the + tensor, which says nothing about accuracy. + """ + inp = _conv_inputs(batch, torch.float32, seed=batch) + (out_ref, _), (out_got, _) = _run_conv_pair(inp) + mismatch = int( + (out_got.float().view(torch.int32) != out_ref.float().view(torch.int32)).sum() + ) + assert mismatch <= 32, f"{mismatch} of {out_got.numel()} elements differ" + assert (out_got.float() - out_ref.float()).abs().max().item() <= 1e-2 + + +@pytest.mark.parametrize("batch", [1, 17]) +def test_conv_output_bf16_cache_differs_only_by_one_ulp(batch): + """A bf16 cache disagrees often but never by more than a bf16 ULP. + + Bounded rather than asserted equal: where the provider rounds in this + configuration is not reproduced, and pretending otherwise would hide it. + """ + inp = _conv_inputs(batch, torch.bfloat16, seed=batch) + (out_ref, _), (out_got, _) = _run_conv_pair(inp) + assert (out_got.float() - out_ref.float()).abs().max().item() <= 7e-2 + + +def test_conv_null_block_id_is_skipped(): + from rl_engine.kernels.ops.pytorch.linear_attn import CausalConv1dUpdateOp + + indices = torch.tensor([1, 0, 2, 0], device="cuda", dtype=torch.int32) + inp = _conv_inputs(4, torch.float32, seed=5, with_bias=False, indices=indices) + out_got, state_got = CausalConv1dUpdateOp().forward( + inp["x"], inp["state"].clone(), inp["weight"], indices, bias=None + ) + for row in (1, 3): + assert bool((out_got[row] == 0).all()) + assert torch.equal(state_got[0], inp["state"][0]) + + +def test_conv_rejects_unknown_activation_and_bad_shapes(): + from rl_engine.kernels.ops.pytorch.linear_attn import CausalConv1dUpdateOp + + op = CausalConv1dUpdateOp() + inp = _conv_inputs(2, torch.float32, seed=1) + with pytest.raises(ValueError, match="activation must be"): + op.forward(inp["x"], inp["state"], inp["weight"], inp["indices"], activation="relu") + with pytest.raises(ValueError, match="conv_state tail must be"): + op.forward( + inp["x"], + inp["state"][..., :1], + inp["weight"], + inp["indices"], + activation=None, + ) + + +# --------------------------------------------------------------------------- # +# 5. forward_fp32, and the branches the provider comparison never reaches +# --------------------------------------------------------------------------- # +@pytest.mark.parametrize("state_dtype", [torch.float32, torch.bfloat16]) +def test_recurrent_forward_fp32_widens_the_state(state_dtype): + """forward_fp32 must run the recurrence without the per-token rounding. + + With a bf16 cache that means widening it -- the whole point of having the + method is to be able to compare against the rounded path. + """ + op = GatedDeltaRuleRecurrentStepOp() + inp = _inputs(4, 6, state_dtype, torch.bfloat16, seed=1) + out, state = op.forward_fp32( + inp["mixed_qkv"], + inp["a"], + inp["b"], + inp["A_log"], + inp["dt_bias"], + inp["state"].clone(), + inp["indices"], + scale=_SCALE, + num_k_heads=_H, + ) + assert out.dtype is torch.float32 and state.dtype is torch.float32 + + rounded, _ = _run_golden(inp) + if state_dtype is torch.bfloat16: + # The rounded path went through bf16; the fp32 path did not. + assert not torch.equal(rounded.float(), out) + + +def test_recurrent_call_matches_forward(): + op = GatedDeltaRuleRecurrentStepOp() + inp = _inputs(4, 6, torch.float32, torch.bfloat16, seed=2) + args = ( + inp["mixed_qkv"], + inp["a"], + inp["b"], + inp["A_log"], + inp["dt_bias"], + inp["state"].clone(), + inp["indices"], + ) + kwargs = dict(scale=_SCALE, num_k_heads=_H) + a_out, a_state = op(*args, **kwargs) + b_out, b_state = op.forward(*args, **kwargs) + assert torch.equal(a_out, b_out) and torch.equal(a_state, b_state) + + +def test_recurrent_without_in_kernel_l2norm_differs(): + """`use_qk_l2norm=False` is the prefill convention and must be reachable.""" + op = GatedDeltaRuleRecurrentStepOp() + inp = _inputs(4, 6, torch.float32, torch.bfloat16, seed=3) + common = ( + inp["mixed_qkv"], + inp["a"], + inp["b"], + inp["A_log"], + inp["dt_bias"], + ) + normed, _ = op.forward( + *common, inp["state"].clone(), inp["indices"], scale=_SCALE, num_k_heads=_H + ) + raw, _ = op.forward( + *common, + inp["state"].clone(), + inp["indices"], + scale=_SCALE, + num_k_heads=_H, + use_qk_l2norm=False, + ) + assert not torch.equal(normed, raw) + + +def test_conv_dim_first_false_matches_the_transposed_layout(): + """`dim_first=False` is vLLM's "SD" cache layout, not a dead branch.""" + from rl_engine.kernels.ops.pytorch.linear_attn import CausalConv1dUpdateOp + + op = CausalConv1dUpdateOp() + inp = _conv_inputs(4, torch.float32, seed=9) + ds_out, ds_state = op.forward( + inp["x"], inp["state"].clone(), inp["weight"], inp["indices"], bias=inp["bias"] + ) + sd_out, sd_state = op.forward( + inp["x"], + inp["state"].transpose(-1, -2).contiguous(), + inp["weight"], + inp["indices"], + bias=inp["bias"], + dim_first=False, + ) + assert torch.equal(ds_out, sd_out) + assert torch.equal(ds_state, sd_state.transpose(-1, -2)) + + +def test_conv_forward_fp32_and_call_entry_points(): + from rl_engine.kernels.ops.pytorch.linear_attn import CausalConv1dUpdateOp + + op = CausalConv1dUpdateOp() + inp = _conv_inputs(4, torch.float32, seed=10) + args = (inp["x"], inp["state"].clone(), inp["weight"], inp["indices"]) + out, _ = op.forward(*args, bias=inp["bias"]) + called, _ = op(*args, bias=inp["bias"]) + fp32, _ = op.forward_fp32(*args, bias=inp["bias"]) + assert torch.equal(out, called) + assert fp32.dtype is torch.float32 + torch.testing.assert_close(fp32, out.float(), atol=1e-2, rtol=1e-2) From dbb8d00ae8cc7c5d8dcd772be6715e8500e9d14b Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:28:34 +0000 Subject: [PATCH 18/44] fix(gdn): validate cache ownership and reproduce conv product rounding Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .github/workflows/ws1-gtest-gpu.yml | 8 ++ ci/run_ws1_gtest.sh | 21 ++-- .../design/ws1-c6-428-gdn-recurrent-replay.md | 78 +++++--------- .../ops/pytorch/linear_attn/__init__.py | 6 +- .../ops/pytorch/linear_attn/causal_conv1d.py | 66 ++++++------ .../pytorch/linear_attn/gated_delta_rule.py | 45 +++++++- tests/check_gdn_recurrent_golden.py | 101 +++++------------- tests/test_gdn_state_contract.py | 98 +++++++++++++++++ 8 files changed, 245 insertions(+), 178 deletions(-) create mode 100644 tests/test_gdn_state_contract.py diff --git a/.github/workflows/ws1-gtest-gpu.yml b/.github/workflows/ws1-gtest-gpu.yml index 646d80bdf..d8f32b714 100644 --- a/.github/workflows/ws1-gtest-gpu.yml +++ b/.github/workflows/ws1-gtest-gpu.yml @@ -20,6 +20,10 @@ on: - "scripts/check_gradient_invariance.py" - "scripts/ws1_candidate_evidence.py" - "tests/test_ws1_*.py" + - "tests/test_gdn_state_contract.py" + - "tests/test_qwen3_next_norm.py" + - "tests/check_qwen3_next_norm_providers.py" + - "tests/check_gdn_recurrent_golden.py" - "tests/test_forward_invariance.py" - "tests/test_gradient_invariance.py" - "tests/test_four_judgment_matrix.py" @@ -38,6 +42,10 @@ on: - "scripts/check_gradient_invariance.py" - "scripts/ws1_candidate_evidence.py" - "tests/test_ws1_*.py" + - "tests/test_gdn_state_contract.py" + - "tests/test_qwen3_next_norm.py" + - "tests/check_qwen3_next_norm_providers.py" + - "tests/check_gdn_recurrent_golden.py" - "tests/test_forward_invariance.py" - "tests/test_gradient_invariance.py" - "tests/test_four_judgment_matrix.py" diff --git a/ci/run_ws1_gtest.sh b/ci/run_ws1_gtest.sh index e6d8e0dc0..c9daa1118 100755 --- a/ci/run_ws1_gtest.sh +++ b/ci/run_ws1_gtest.sh @@ -77,12 +77,15 @@ print("[ws1-gtest] C8 gate passed") PY echo "[ws1-gtest] Qwen3-Next C3/C4 norms (operator scope)" -QWEN3_NEXT_NORM_MANIFEST=rl_engine/testing/qwen3_next_norm_manifest.json -"$PY" scripts/check_forward_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ - --op qwen3_next_rms_norm --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 -"$PY" scripts/check_gradient_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ - --op qwen3_next_rms_norm --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 -"$PY" scripts/check_forward_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ - --op rms_norm_gated --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 -"$PY" scripts/check_gradient_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ - --op rms_norm_gated --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 +for op in qwen3_next_rms_norm rms_norm_gated; do + for gate in forward gradient; do + "$PY" "scripts/check_${gate}_invariance.py" \ + --manifest rl_engine/testing/qwen3_next_norm_manifest.json \ + --op "$op" --candidate cuda --backend-profile cuda_bf16 \ + --hidden 2048 --head-dim 128 + done +done +# Provider imports run in a separate process from framework-isolation tests. +echo "[ws1-gtest] Qwen3-Next provider comparisons (required)" +"$PY" -c 'import torch, vllm; assert torch.cuda.is_available(), "CUDA is required"' +"$PY" -m pytest -q tests/check_qwen3_next_norm_providers.py tests/check_gdn_recurrent_golden.py diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/ws1-c6-428-gdn-recurrent-replay.md index d0d0f3ea3..2aa1ddf23 100644 --- a/docs/design/ws1-c6-428-gdn-recurrent-replay.md +++ b/docs/design/ws1-c6-428-gdn-recurrent-replay.md @@ -69,53 +69,28 @@ Qwen3-Next dims (H=16, HV=32, K=V=128), `use_qk_l2norm_in_kernel=True`: | fp32 state, B=1..64 | 1.5e-08 .. 6.1e-05 | ≤ 3.0e-07 | | bf16 state, B=1..64 | 3.7e-09 .. 3.1e-05 | ≤ 2.0e-03 | -Causal conv (`conv_dim=8192`, `W=4`, B ≤ 64): the **rolled state is bitwise exact in -every configuration**. With an fp32 cache the output is bitwise but for a handful of -elements (15 of 524288 at B=64); with a bf16 cache it agrees on ~63%, each -disagreement exactly one bf16 ULP. The conv taps accumulate **sequentially**, -`acc = acc + win[t] * w[t]` from zero — a tree sum over the same four terms does not -reproduce the provider, an FMA does not either. - -Open: where the provider rounds in the bf16-conv-cache case is not reproduced. -Recorded rather than guessed at. +Causal conv uses sequential FP32 accumulation **starting from bias**, with +products first rounded to the operand dtype. The previous BF16 path incorrectly +promoted both operands to FP32; its disagreements were not limited to one BF16 +ULP. Cancellation and bias-order CPU tests now cover these errors. Provider +comparisons preserve the existing absolute bounds and additionally limit BF16 +mismatches to 32 elements on the checked fixtures. This is not a bitwise claim. ## 5. Decode versus chunked prefill -The quantity RFC #428 §4.2 is about. Single sequence, zero initial state: - -| T | max\|diff\|, fp32 state | max\|diff\|, bf16 state | -|---|---|---| -| 8 | 3.66e-04 (7.0e-03 rel) | 5.49e-04 (1.0e-02 rel) | -| 64 | 4.88e-04 (6.6e-03 rel) | 5.49e-04 (7.5e-03 rel) | -| 256 | 3.66e-04 (5.0e-03 rel) | 3.66e-04 (5.0e-03 rel) | +The earlier 1024-step drift and prefill tables did not have a checked-in runner; +they are withdrawn as acceptance evidence. The existing 128-step synthetic test +only bounds its fixed seed and gate inputs. It does not establish a universal +plateau, prefill/decode equality, or any bound on model logits. -**The gap is ~0.5–1% relative and flat in T** — smaller at T=256 than at T=8 — and the -state dtype barely moves it. +The strict profile uses FP32 recurrent state. BF16 state remains a differential +experiment. Full checkpoint prefill, response replay, optimizer updates and +reload all remain required before L2 can pass. -The reason is that the recurrence is **contracting**: per-step decay `exp(g)` averages -~0.47 (max 0.996), so old rounding error is forgotten at roughly the rate old signal -is. Comparing an fp32 state against a bf16 one over 1024 steps shows the same shape: - -| step | relative \|d\| state | relative \|d\| out | -|---|---|---| -| 1 | 2.67e-03 | 3.47e-03 | -| 64 | 1.52e-02 | 1.07e-02 | -| 256 | 2.02e-02 | 1.09e-02 | -| 1024 | 2.47e-02 | 1.57e-02 | - -A 16× longer run past step 64 grows the drift only 1.6×; it saturates at ~2% relative -on the state and ~1–1.8% on the output. - -So the two paths are not bitwise, and ~1% relative on logits is still material for RL -importance ratios, but this is a bounded, characterizable error rather than a -divergence. An fp32 recurrent state remains the right choice for exactness work — the -reason is the 2% plateau, not a blow-up. - -Two related constraints: the chunked prefill kernel refuses fp32 q/k/v outright -(`chunk.py:213`), so the prefill side is bf16-only regardless; and -`causal_conv1d_update` does not bounds-check `conv_state_indices` under its default -`validate_data=False` — an index past the cache is an out-of-bounds write, not an -error. +Cache indices must be int32/int64, on the input device, and positive active +indices must be unique and in range. Nonpositive sentinels may repeat. The golden +validates before any cache update; raw provider calls remain the caller's +responsibility (the provider's default does not bounds-check). ## 6. Current boundary @@ -133,13 +108,14 @@ Not covered, with reasons: `runtime_verified=false` (no checkpoint), `supports_backward=false`, `checkpoint=absent`. Op-level agreement says nothing about 48 composed layers. -## 7. Questions for the maintainers +## 7. Accepted execution boundaries + +Use a shared, explicitly pinned vLLM-compatible forward provider on both sides, +with independent VIME recomputation. Disable MTP and prefix reuse. Keep FP32 +recurrent state for strict acceptance; BF16 is experimental. Require bitwise +logits/logprobs at a common topology, at least two real optimizer updates, weight +synchronization and checkpoint reload. No operator-level test closes these gates. -1. Is a **bf16 recurrent state** a supported configuration? The answer decides whether - the 2% plateau is a finding or a non-issue. -2. Is MTP / speculative decode in scope for WS1? If so, - `fused_gdn_decode_post_conv_mtp` becomes the primary provider for both the norm and - the recurrence, and the largest deferral above reopens. -3. For the CUDA strict profile, is the single source of truth **vLLM's** arithmetic or - **PyTorch eager**? §1 item 1 asks for one provider on both sides but §2.1 does not - say which, and the two differ by ~6e-2 in bf16. +The 2026-09-30 real-checkpoint startup attempt with vLLM 0.30.0 failed before +inference: `VLLM batch_invariant mode is not supported for GDN_ATTN`. A shared +provider integration must resolve this; disabling the check is not L2 evidence. diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/__init__.py b/rl_engine/kernels/ops/pytorch/linear_attn/__init__.py index 28602f18f..c974a89b1 100644 --- a/rl_engine/kernels/ops/pytorch/linear_attn/__init__.py +++ b/rl_engine/kernels/ops/pytorch/linear_attn/__init__.py @@ -3,13 +3,11 @@ """Linear-attention operators (Gated DeltaNet and friends).""" -from rl_engine.kernels.ops.pytorch.linear_attn.causal_conv1d import ( - CausalConv1dUpdateOp, -) +from rl_engine.kernels.ops.pytorch.linear_attn.causal_conv1d import CausalConv1dUpdateOp from rl_engine.kernels.ops.pytorch.linear_attn.gated_delta_rule import ( - GatedDeltaRuleRecurrentStepOp, NULL_BLOCK_ID, SOFTPLUS_THRESHOLD, + GatedDeltaRuleRecurrentStepOp, ) __all__ = [ diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py b/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py index ffbac6e13..fb171af49 100644 --- a/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py +++ b/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py @@ -6,42 +6,22 @@ The trainer-side reference for ``causal_conv1d_update``, which vLLM calls once per decode token before the Gated DeltaNet recurrence. -Semantics were established by differential testing against the provider rather -than transcribed, and the accumulation order matters: - -* ``x`` is cast to ``conv_state.dtype`` **before** anything else, so a bf16 - conv cache rounds the incoming token (``causal_conv1d.py:1160``). -* The window is ``[state[..., -(width-1):], x]``; the taps are accumulated - **sequentially in tap order**, ``acc = acc + win[t] * w[t]`` starting from - zero -- not a tree reduction and not an FMA. With an fp32 conv state this - reproduces the provider bitwise; a tree sum does not. -* Bias and the activation are applied in fp32, with one cast on the way out to - the input's original dtype. -* The new state is the window minus its oldest column. - -Measured against the provider on B200 (``conv_dim=8192``, ``W=4``, B up to 64): - -* **The rolled state is bitwise exact in every configuration**, fp32 and bf16 - cache alike -- ``max|diff| = 0``. That is the part the next token consumes, - so the recurrence carries no error from here. -* With an **fp32** conv state the output is bitwise on all but a handful of - elements (15 of 524288 at B=64), the residual being fp32 ULP. -* With a **bf16** conv state the output agrees on ~63% of elements, every - disagreement exactly one bf16 ULP. - -The bf16 output gap is a rounding-path difference, not a semantic one: none of -accumulate-in-bf16, bias-in-bf16 or activation-in-bf16 reproduces the provider, -so where it rounds is left unresolved rather than guessed at. An fp32 conv state -is what an exactness claim should use anyway -- the same conclusion the -recurrent state reaches in :mod:`.gated_delta_rule`. +The incoming token is rounded to the cache dtype. Products are rounded in +operand dtype before sequential FP32 accumulation, starting from bias (or zero). +This matters for BF16 caches: promoting operands before multiplication loses the +provider's product rounding. Activation may still differ at transcendental ULP +scale; this reference does not establish model-level bitwise equality. + """ from __future__ import annotations import torch -import torch.nn.functional as F -from rl_engine.kernels.ops.pytorch.linear_attn.gated_delta_rule import NULL_BLOCK_ID +from rl_engine.kernels.ops.pytorch.linear_attn.gated_delta_rule import ( + NULL_BLOCK_ID, + _validate_state_indices, +) __all__ = ["CausalConv1dUpdateOp"] @@ -154,6 +134,21 @@ def _update( # The "SD" layout stores (state_len, dim); the kernels want (dim, state_len). state = conv_state if dim_first else conv_state.transpose(-1, -2) dim, tail = state.shape[-2], state.shape[-1] + if weight.ndim != 2 or dim <= 0 or weight.shape[-1] <= 0: + raise ValueError("weight must be 2-D [dim, W] with positive dimensions") + if x.shape[1] != dim: + raise ValueError("x and conv_state must have the same dim") + _validate_state_indices(conv_state_indices, x.shape[0], state.shape[0], x.device) + for name, tensor in ( + ("x", x), + ("conv_state", conv_state), + ("weight", weight), + ("bias", bias), + ): + if tensor is not None and (tensor.device != x.device or not tensor.is_floating_point()): + raise ValueError(f"{name} must be floating point on the input device") + if bias is not None and bias.shape != (dim,): + raise ValueError("bias must have shape [dim]") width = weight.shape[-1] if weight.shape[0] != dim: raise ValueError(f"weight must be [dim, W] with dim={dim}, got {tuple(weight.shape)}") @@ -173,16 +168,15 @@ def _update( token = x[rows].to(conv_state.dtype) window = torch.cat([state[blocks], token.unsqueeze(-1)], dim=-1) # [R, dim, W] - # Sequential tap accumulation from zero, in fp32. A tree sum over the - # same four terms gives a different last bit and does NOT match. + # Match the provider's product rounding and bias-before-taps order. acc = torch.zeros(len(rows), dim, dtype=torch.float32, device=x.device) - w32 = weight.float() - for tap in range(width): - acc = acc + window[..., tap].float() * w32[:, tap].unsqueeze(0) if bias is not None: acc = acc + bias.float() + for tap in range(width): + product = window[..., tap] * weight[:, tap].unsqueeze(0) + acc = acc + product.float() if activation in ("silu", "swish"): - acc = F.silu(acc) + acc = acc / (1.0 + torch.exp(-acc)) out[rows] = acc rolled = window[..., 1:] diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py index cdf6f17b7..bfab76047 100644 --- a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py +++ b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py @@ -55,6 +55,21 @@ _REDUCTION_CHUNK = 32 +def _validate_state_indices(indices, batch, blocks, device): + """Each active row owns one cache block; inactive sentinels may repeat.""" + if indices.ndim != 1 or indices.shape[0] != batch: + raise ValueError("state indices must be 1-D with one entry per sequence") + if indices.dtype not in (torch.int32, torch.int64): + raise ValueError("state indices must have int32 or int64 dtype") + if indices.device != device: + raise ValueError("state indices must be on the input device") + active = indices[indices > NULL_BLOCK_ID] + if bool((active >= blocks).any()): + raise ValueError("active state index is out of range") + if active.unique().numel() != active.numel(): + raise ValueError("active state indices must be unique") + + def _chunked_sum(x: torch.Tensor) -> torch.Tensor: """Sum the last dim in a fixed 32-wide chunk order. @@ -67,7 +82,9 @@ def _chunked_sum(x: torch.Tensor) -> torch.Tensor: tail = x.shape[-1] if tail % _REDUCTION_CHUNK != 0: return x.sum(dim=-1) - return x.reshape(*x.shape[:-1], -1, _REDUCTION_CHUNK).sum(dim=-1).sum(dim=-1) + return ( + x.reshape(*x.shape[:-1], tail // _REDUCTION_CHUNK, _REDUCTION_CHUNK).sum(dim=-1).sum(dim=-1) + ) def _fixed_order_contract(mat: torch.Tensor, vec: torch.Tensor) -> torch.Tensor: @@ -220,15 +237,33 @@ def _step( if mixed_qkv.dim() != 2: raise ValueError(f"mixed_qkv must be 2-D [B, D], got {tuple(mixed_qkv.shape)}") if state.dim() != 4: - raise ValueError( - f"state must be 4-D [num_blocks, HV, V, K], got {tuple(state.shape)}" - ) + raise ValueError(f"state must be 4-D [num_blocks, HV, V, K], got {tuple(state.shape)}") if ssm_state_indices.dim() != 1: raise ValueError("ssm_state_indices must be 1-D [B] for packed decode") batch = mixed_qkv.shape[0] hv, v_dim, k_dim = state.shape[-3:] - heads = int(num_k_heads) + if isinstance(num_k_heads, bool) or not isinstance(num_k_heads, int) or num_k_heads <= 0: + raise ValueError("num_k_heads must be a positive integer") + if min(hv, v_dim, k_dim) <= 0: + raise ValueError("state head dimensions must be positive") + heads = num_k_heads + _validate_state_indices(ssm_state_indices, batch, state.shape[0], mixed_qkv.device) + for name, tensor in ( + ("a", a), + ("b", b), + ("A_log", A_log), + ("dt_bias", dt_bias), + ("state", state), + ): + if tensor.device != mixed_qkv.device: + raise ValueError(f"{name} must be on the input device") + if not tensor.is_floating_point(): + raise ValueError(f"{name} must be floating point") + if not mixed_qkv.is_floating_point(): + raise ValueError("mixed_qkv must be floating point") + if A_log.shape != (hv,) or dt_bias.shape != (hv,): + raise ValueError("A_log and dt_bias must have shape [HV]") if hv % heads != 0: raise ValueError(f"HV={hv} must be a multiple of num_k_heads={heads}") if a.shape != (batch, hv) or b.shape != (batch, hv): diff --git a/tests/check_gdn_recurrent_golden.py b/tests/check_gdn_recurrent_golden.py index e9e18487c..7c7e929ac 100644 --- a/tests/check_gdn_recurrent_golden.py +++ b/tests/check_gdn_recurrent_golden.py @@ -22,49 +22,11 @@ its contractions run in a fixed 32-wide chunk order rather than the kernel's tree. -Recurrent-state dtype, and how far a decode run actually drifts ----------------------------------------------------------------- -``FUSED_GDN_STATE_DTYPES`` allows the recurrent state to be fp32 or bf16, and a -bf16 store rounds the ``[HV, V, K]`` state once per token. The drift that causes -does **not** compound without bound, because the recurrence is contracting: the -per-step decay ``exp(g)`` averages ~0.47 here (max 0.996), so old error is -forgotten at roughly the rate old signal is. - -fp32 state vs bf16 state, identical inputs, B=8, B200: - -====== ================= =============== -step relative |d| state relative |d| out -====== ================= =============== -1 2.67e-03 3.47e-03 -64 1.52e-02 1.07e-02 -256 2.02e-02 1.09e-02 -1024 2.47e-02 1.57e-02 -====== ================= =============== - -The curve saturates: a 16x longer run past step 64 grows the state drift only -1.6x. The plateau is ~2% relative on the state and ~1-1.8% on the output. - -Chunked prefill vs step-by-step decode replay, single sequence, from a zero -state -- this is the quantity RFC #428 section 4.2 is about: - -====== ================== ================== -T max|diff|, fp32 max|diff|, bf16 -====== ================== ================== -8 3.66e-04 (7.0e-03) 5.49e-04 (1.0e-02) -64 4.88e-04 (6.6e-03) 5.49e-04 (7.5e-03) -256 3.66e-04 (5.0e-03) 3.66e-04 (5.0e-03) -====== ================== ================== - -(relative in parentheses). The gap is ~0.5-1% relative and **flat in T** -- at -T=256 it is smaller than at T=8 -- and the state dtype barely moves it. - -So the two paths are not bitwise, and ~1% relative on logits is still material -for RL importance ratios, but this is a bounded, characterizable error rather -than a divergence. An fp32 recurrent state is still the right choice for -exactness work; the reason is the 2% state plateau, not a blow-up. - -Note the chunked prefill kernel refuses fp32 q/k/v outright -(``chunk.py:213``), so the prefill side is bf16-only regardless. +The fixed-seed 128-step rounding test below is a bounded synthetic regression, +not evidence of a universal drift plateau or model-logit agreement. Earlier +1024-step and prefill tables had no reproducible runner and are withdrawn until +those experiments are checked in. Operator outputs are not model logits. + """ from __future__ import annotations @@ -74,9 +36,7 @@ pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required") -from rl_engine.kernels.ops.pytorch.linear_attn import ( # noqa: E402 - GatedDeltaRuleRecurrentStepOp, -) +from rl_engine.kernels.ops.pytorch.linear_attn import GatedDeltaRuleRecurrentStepOp # noqa: E402 # Qwen3-Next-80B-A3B-Instruct: linear_num_key_heads / linear_num_value_heads / # linear_key_head_dim / linear_value_head_dim. @@ -116,9 +76,7 @@ def _run_provider(inp, io_dtype=torch.bfloat16): """Returns (out, mutated_state). The kernel updates the state in place.""" step = _vllm_step() state = inp["state"].clone() - out = torch.empty( - inp["mixed_qkv"].shape[0], 1, _HV, _V, device="cuda", dtype=io_dtype - ) + out = torch.empty(inp["mixed_qkv"].shape[0], 1, _HV, _V, device="cuda", dtype=io_dtype) step( inp["mixed_qkv"], inp["a"], @@ -220,16 +178,8 @@ def test_golden_is_batch_invariant(state_dtype): # --------------------------------------------------------------------------- # # 3. The state-dtype rounding is modelled, not skipped # --------------------------------------------------------------------------- # -def test_bf16_state_rounding_is_visible_but_saturates(): - """A bf16 state rounds from the first store, and the drift then flattens. - - The recurrence is contracting -- ``exp(g)`` is well below 1 for most heads -- - so old rounding error is forgotten at roughly the rate old signal is. The - drift therefore grows quickly at first and then plateaus, rather than - compounding without bound. Asserting a bound is the honest form: asserting - monotone growth would have been true over a truncated run and false over a - long one. - """ +def test_bf16_state_rounding_stays_within_fixed_fixture_bound(): + """Bound this fixed 128-step fixture; no general contraction claim.""" batch, steps = 8, 128 inp = _inputs(batch, batch + 2, torch.float32, torch.bfloat16, seed=11) op = GatedDeltaRuleRecurrentStepOp() @@ -243,9 +193,7 @@ def test_bf16_state_rounding_is_visible_but_saturates(): drift = [] for step in range(steps): g = torch.Generator(device="cuda").manual_seed(100 + step) - token = torch.randn( - batch, _PACKED_DIM, device="cuda", dtype=torch.bfloat16, generator=g - ) + token = torch.randn(batch, _PACKED_DIM, device="cuda", dtype=torch.bfloat16, generator=g) common = (inp["a"], inp["b"], inp["A_log"], inp["dt_bias"]) _, state_fp32 = op.forward( token, *common, state_fp32, inp["indices"], scale=_SCALE, num_k_heads=_H @@ -257,8 +205,7 @@ def test_bf16_state_rounding_is_visible_but_saturates(): drift.append((state_fp32.float() - state_bf16.float()).abs().max().item() / scale) assert drift[0] > 0.0, "a bf16 state must round on the very first store" - # Measured plateau is ~2% relative; the bound leaves room without hiding a - # genuine divergence. + # Preserve the original regression bound for this fixed fixture. assert max(drift) < 0.05, f"relative state drift reached {max(drift):.3e}" # The back half must not be materially worse than the front half. assert max(drift[steps // 2 :]) < 2.0 * max(drift[: steps // 2]) + 1e-3 @@ -343,23 +290,18 @@ def test_conv_output_matches_provider_with_fp32_cache(batch): """ inp = _conv_inputs(batch, torch.float32, seed=batch) (out_ref, _), (out_got, _) = _run_conv_pair(inp) - mismatch = int( - (out_got.float().view(torch.int32) != out_ref.float().view(torch.int32)).sum() - ) + mismatch = int((out_got.float().view(torch.int32) != out_ref.float().view(torch.int32)).sum()) assert mismatch <= 32, f"{mismatch} of {out_got.numel()} elements differ" assert (out_got.float() - out_ref.float()).abs().max().item() <= 1e-2 @pytest.mark.parametrize("batch", [1, 17]) -def test_conv_output_bf16_cache_differs_only_by_one_ulp(batch): - """A bf16 cache disagrees often but never by more than a bf16 ULP. - - Bounded rather than asserted equal: where the provider rounds in this - configuration is not reproduced, and pretending otherwise would hide it. - """ +def test_conv_output_bf16_cache_matches_rounded_product_path(batch): + """Product rounding is reproduced; activation ULP residuals remain allowed.""" inp = _conv_inputs(batch, torch.bfloat16, seed=batch) (out_ref, _), (out_got, _) = _run_conv_pair(inp) assert (out_got.float() - out_ref.float()).abs().max().item() <= 7e-2 + assert int((out_got != out_ref).sum()) <= 32 def test_conv_null_block_id_is_skipped(): @@ -499,3 +441,16 @@ def test_conv_forward_fp32_and_call_entry_points(): assert torch.equal(out, called) assert fp32.dtype is torch.float32 torch.testing.assert_close(fp32, out.float(), atol=1e-2, rtol=1e-2) + + +def test_conv_provider_preserves_bf16_product_cancellation(): + inp = _conv_inputs(1, torch.bfloat16, seed=1, with_bias=False) + inp["state"].zero_() + inp["state"][1, :, -1] = 1.0078125 + inp["weight"].zero_() + inp["weight"][:, -2] = 1.0078125 + inp["weight"][:, -1] = 1.0 + inp["x"].fill_(-1.015625) + (provider, _), (golden, _) = _run_conv_pair(inp, activation=None) + assert torch.count_nonzero(provider) == 0 + assert torch.equal(provider, golden) diff --git a/tests/test_gdn_state_contract.py b/tests/test_gdn_state_contract.py new file mode 100644 index 000000000..9e84ba288 --- /dev/null +++ b/tests/test_gdn_state_contract.py @@ -0,0 +1,98 @@ +"""Provider-independent cache contracts, collected in the ordinary CPU suite.""" + +import pytest +import torch + +from rl_engine.kernels.ops.pytorch.linear_attn import ( + CausalConv1dUpdateOp, + GatedDeltaRuleRecurrentStepOp, +) + + +def _call(kind, indices, heads=1): + batch = indices.numel() + if kind == "conv": + state = torch.randn(4, 2, 3) + before = state.clone() + result = CausalConv1dUpdateOp()(torch.randn(batch, 2), state, torch.randn(2, 4), indices) + else: + state = torch.randn(4, 2, 2, 32) + before = state.clone() + result = GatedDeltaRuleRecurrentStepOp()( + torch.randn(batch, 68), + torch.randn(batch, 2), + torch.randn(batch, 2), + torch.zeros(2), + torch.zeros(2), + state, + indices, + scale=32**-0.5, + num_k_heads=heads, + ) + assert torch.equal(state, before) + return result, before + + +@pytest.mark.parametrize("kind", ["conv", "gdn"]) +@pytest.mark.parametrize( + "indices,message", + [ + (torch.tensor([1.5, 2.0]), "int32 or int64"), + (torch.tensor([True, False]), "int32 or int64"), + (torch.tensor([1, 1]), "unique"), + (torch.tensor([1, 4]), "out of range"), + ], +) +def test_invalid_cache_indices_are_rejected(kind, indices, message): + with pytest.raises(ValueError, match=message): + _call(kind, indices) + + +@pytest.mark.parametrize("kind", ["conv", "gdn"]) +@pytest.mark.parametrize("indices", [torch.tensor([0, -1, 0]), torch.empty(0, dtype=torch.int64)]) +def test_inactive_and_empty_batches_preserve_cache(kind, indices): + (out, state), before = _call(kind, indices) + assert torch.equal(state, before) + assert torch.count_nonzero(out) == 0 + + +@pytest.mark.parametrize("heads", [0, -1, 1.5, True]) +def test_gdn_requires_positive_integer_heads(heads): + with pytest.raises(ValueError, match="positive integer"): + _call("gdn", torch.tensor([1, 2]), heads=heads) + + +def test_conv_bias_is_accumulated_before_taps(): + # Adding bias last would yield 1.0 instead of 0.0. + out, _ = CausalConv1dUpdateOp()( + torch.tensor([[-1e8]]), + torch.zeros(2, 1, 1), + torch.tensor([[0.0, 1.0]]), + torch.tensor([1]), + bias=torch.tensor([1e8]), + activation=None, + ) + assert out.item() == 0.0 + out, _ = CausalConv1dUpdateOp()( + torch.tensor([[-1e8]]), + torch.ones(2, 1, 1), + torch.ones(1, 2), + torch.tensor([1]), + bias=torch.tensor([1e8]), + activation=None, + ) + assert out.item() == 0.0 + + +def test_conv_bf16_products_round_before_fp32_accumulation(): + state = torch.zeros(2, 1, 1, dtype=torch.bfloat16) + state[1, 0, 0] = 1.0078125 + out, _ = CausalConv1dUpdateOp().forward_fp32( + torch.tensor([[-1.015625]], dtype=torch.bfloat16), + state, + torch.tensor([[1.0078125, 1.0]], dtype=torch.bfloat16), + torch.tensor([1]), + activation=None, + ) + # The first exact product is 1.015686..., rounded to 1.015625 in BF16. + assert out.item() == 0.0 From dcb926310546af8f25b558e69014e052406b2c1b Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:35:04 +0000 Subject: [PATCH 19/44] fix(gdn): avoid overflow in inactive softplus gradient branch Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .../kernels/ops/pytorch/linear_attn/gated_delta_rule.py | 4 +++- tests/test_gdn_state_contract.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py index bfab76047..e2af3a0c3 100644 --- a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py +++ b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py @@ -104,7 +104,9 @@ def _l2_normalize(x: torch.Tensor, eps: float = 1e-6) -> torch.Tensor: def _softplus(x: torch.Tensor, threshold: float = SOFTPLUS_THRESHOLD) -> torch.Tensor: """The kernel's branched softplus; the branch matters for large ``a + dt_bias``.""" - return torch.where(x <= threshold, torch.log1p(torch.exp(x)), x) + small = x <= threshold + safe = torch.where(small, x, torch.zeros_like(x)) + return torch.where(small, torch.log1p(torch.exp(safe)), x) class GatedDeltaRuleRecurrentStepOp: diff --git a/tests/test_gdn_state_contract.py b/tests/test_gdn_state_contract.py index 9e84ba288..70255bb0d 100644 --- a/tests/test_gdn_state_contract.py +++ b/tests/test_gdn_state_contract.py @@ -96,3 +96,11 @@ def test_conv_bf16_products_round_before_fp32_accumulation(): ) # The first exact product is 1.015686..., rounded to 1.015625 in BF16. assert out.item() == 0.0 + + +def test_softplus_large_branch_has_finite_gradient(): + from rl_engine.kernels.ops.pytorch.linear_attn.gated_delta_rule import _softplus + + x = torch.tensor([100.0, 21.0, 0.0], requires_grad=True) + _softplus(x).sum().backward() + assert torch.equal(x.grad, torch.tensor([1.0, 1.0, 0.5])) From 16204ab1de6d3f22d5a568c56dd8866c2278e44f Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:42:32 +0000 Subject: [PATCH 20/44] ci(qwen3-next): separate audited provider runtime from legacy CUDA gate Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .github/workflows/qwen3-next-provider-gpu.yml | 42 +++++++++++++++++++ ci/run_ws1_gtest.sh | 4 -- 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/qwen3-next-provider-gpu.yml diff --git a/.github/workflows/qwen3-next-provider-gpu.yml b/.github/workflows/qwen3-next-provider-gpu.yml new file mode 100644 index 000000000..ee1851881 --- /dev/null +++ b/.github/workflows/qwen3-next-provider-gpu.yml @@ -0,0 +1,42 @@ +name: Qwen3-Next-provider-GPU + +on: + pull_request: + branches: [main] + paths: + - "rl_engine/kernels/ops/**" + - "rl_engine/integrations/qwen3_next_gdn.py" + - "rl_engine/testing/qwen3_next*" + - "tests/check_*qwen3_next*.py" + - "tests/check_gdn_recurrent_golden.py" + - "ci/run_qwen3_next_operator_gates.sh" + - ".github/workflows/qwen3-next-provider-gpu.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + provider: + # This label must identify a maintained CUDA 13 / vLLM 0.30.0 environment. + # Never run untrusted fork PR code on the self-hosted runner. + 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] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Require the audited runtime + run: | + python3 - <<'PY' + import torch, vllm + assert torch.__version__ == "2.13.0+cu130" + assert vllm.__version__ == "0.30.0" + assert torch.cuda.is_available() + PY + - name: Build the extension against that runtime + run: python3 setup.py build_ext --inplace + - name: Check pinned providers in their own process + run: | + python3 -m pytest -q tests/check_qwen3_next_norm_providers.py tests/check_gdn_recurrent_golden.py diff --git a/ci/run_ws1_gtest.sh b/ci/run_ws1_gtest.sh index c9daa1118..03b77173d 100755 --- a/ci/run_ws1_gtest.sh +++ b/ci/run_ws1_gtest.sh @@ -85,7 +85,3 @@ for op in qwen3_next_rms_norm rms_norm_gated; do --hidden 2048 --head-dim 128 done done -# Provider imports run in a separate process from framework-isolation tests. -echo "[ws1-gtest] Qwen3-Next provider comparisons (required)" -"$PY" -c 'import torch, vllm; assert torch.cuda.is_available(), "CUDA is required"' -"$PY" -m pytest -q tests/check_qwen3_next_norm_providers.py tests/check_gdn_recurrent_golden.py From 697bea887281354d13f7f81af5ed3417e8b1e994 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:26:46 +0000 Subject: [PATCH 21/44] test(gdn): assert the packed-decode provider leaves the null block untouched test_null_block_id_is_skipped_by_both checked that both sides write zeros for a NULL_BLOCK_ID row, but asserted "block untouched" only for the golden. The provider returns before any store for state_idx <= 0 (fused_recurrent.py:299-303 in vllm 0.30.0); assert it on the provider's state as well so the test name matches what it checks. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- tests/check_gdn_recurrent_golden.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/check_gdn_recurrent_golden.py b/tests/check_gdn_recurrent_golden.py index 7c7e929ac..aefecaddf 100644 --- a/tests/check_gdn_recurrent_golden.py +++ b/tests/check_gdn_recurrent_golden.py @@ -141,6 +141,7 @@ def test_null_block_id_is_skipped_by_both(): assert bool((out_ref[row] == 0).all()), f"provider row {row}" assert bool((out_got[row] == 0).all()), f"golden row {row}" assert torch.equal(state_got[0], inp["state"][0]), "block 0 must be untouched" + assert torch.equal(state_ref[0], inp["state"][0]), "provider must leave block 0 untouched" # --------------------------------------------------------------------------- # From 12a22256e69496a9600577bb267be2d97e0e1046 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:27:08 +0000 Subject: [PATCH 22/44] ci(qwen3-next): drop stale and forward-referencing workflow paths ws1-gtest-gpu.yml: 158fc75 added four Qwen3-Next/GDN test files to the pull_request and push path filters together with a step in ci/run_ws1_gtest.sh that ran the two check_ files. 03176a3 moved that step to qwen3-next-provider-gpu.yml but left the filters, so editing those files triggered a RunPod job that never executed them. Remove the filters; the file is back to its state on feat/cuda-qwen3-next-gated-rmsnorm. qwen3-next-provider-gpu.yml: drop rl_engine/integrations/qwen3_next_gdn.py and ci/run_qwen3_next_operator_gates.sh from the path filter. Neither file exists on this branch. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .github/workflows/qwen3-next-provider-gpu.yml | 2 -- .github/workflows/ws1-gtest-gpu.yml | 8 -------- 2 files changed, 10 deletions(-) diff --git a/.github/workflows/qwen3-next-provider-gpu.yml b/.github/workflows/qwen3-next-provider-gpu.yml index ee1851881..b53c73441 100644 --- a/.github/workflows/qwen3-next-provider-gpu.yml +++ b/.github/workflows/qwen3-next-provider-gpu.yml @@ -5,11 +5,9 @@ on: branches: [main] paths: - "rl_engine/kernels/ops/**" - - "rl_engine/integrations/qwen3_next_gdn.py" - "rl_engine/testing/qwen3_next*" - "tests/check_*qwen3_next*.py" - "tests/check_gdn_recurrent_golden.py" - - "ci/run_qwen3_next_operator_gates.sh" - ".github/workflows/qwen3-next-provider-gpu.yml" workflow_dispatch: diff --git a/.github/workflows/ws1-gtest-gpu.yml b/.github/workflows/ws1-gtest-gpu.yml index d8f32b714..646d80bdf 100644 --- a/.github/workflows/ws1-gtest-gpu.yml +++ b/.github/workflows/ws1-gtest-gpu.yml @@ -20,10 +20,6 @@ on: - "scripts/check_gradient_invariance.py" - "scripts/ws1_candidate_evidence.py" - "tests/test_ws1_*.py" - - "tests/test_gdn_state_contract.py" - - "tests/test_qwen3_next_norm.py" - - "tests/check_qwen3_next_norm_providers.py" - - "tests/check_gdn_recurrent_golden.py" - "tests/test_forward_invariance.py" - "tests/test_gradient_invariance.py" - "tests/test_four_judgment_matrix.py" @@ -42,10 +38,6 @@ on: - "scripts/check_gradient_invariance.py" - "scripts/ws1_candidate_evidence.py" - "tests/test_ws1_*.py" - - "tests/test_gdn_state_contract.py" - - "tests/test_qwen3_next_norm.py" - - "tests/check_qwen3_next_norm_providers.py" - - "tests/check_gdn_recurrent_golden.py" - "tests/test_forward_invariance.py" - "tests/test_gradient_invariance.py" - "tests/test_four_judgment_matrix.py" From b54ac72644cdb75680c94a04d07a4cc19780d031 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:29:46 +0000 Subject: [PATCH 23/44] feat(gdn): add a runner for the C6 provider-agreement measurements The design note quoted provider-vs-golden figures (max|d out|, max|d state|, conv mismatch counts) that no committed code printed, so they could not be reproduced. scripts/ws1_gdn_provider_agreement.py prints them as JSON, using tests/check_gdn_recurrent_golden.py's own input helpers and seeds (imported, not copied): - recurrent: per (batch, state dtype), max|d out|, max|d state| and bitwise mismatch counts, next to the bounds the check file asserts; - conv: per (batch, cache dtype), output mismatch count, max|diff| and whether the rolled state is bitwise equal; - the conv comparison again with triton.knobs.language.default_fp_fusion off, plus the fma.rn.f32 count in each compiled variant of the provider's conv-update kernel, to test whether the fp32-cache mismatches are FMA contraction. Provenance (git commit and dirty flag, torch/triton/vllm versions, device) is recorded with the results. Requires CUDA and vLLM; not run in CI. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- scripts/ws1_gdn_provider_agreement.py | 238 ++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 scripts/ws1_gdn_provider_agreement.py diff --git a/scripts/ws1_gdn_provider_agreement.py b/scripts/ws1_gdn_provider_agreement.py new file mode 100644 index 000000000..a3a433575 --- /dev/null +++ b/scripts/ws1_gdn_provider_agreement.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Measure how closely the GDN decode-step goldens track vLLM's providers (RFC #428 C6). + +``tests/check_gdn_recurrent_golden.py`` asserts loose bounds; this runner prints the +measured values behind them, so the numbers quoted in +``docs/design/ws1-c6-428-gdn-recurrent-replay.md`` can be reproduced. Inputs and seeds +are the check file's own helpers, imported from it rather than copied. + +Three measurements, emitted as one JSON document on stdout: + +* ``recurrent``: provider vs golden for the packed recurrent decode, per + (batch, state dtype): max|d out|, max|d state| and bitwise mismatch counts. +* ``conv``: provider vs golden for ``causal_conv1d_update``, per (batch, cache dtype): + output mismatch count and max|diff|, and whether the rolled state is bitwise equal. +* ``conv`` again with Triton FP fusion disabled + (``triton.knobs.language.default_fp_fusion = False``), plus ``conv_ptx``: the number + of ``fma.rn.f32`` instructions in each compiled variant of the provider's conv + kernel. If the fp32-cache mismatches are FMA contraction, they should vanish with + fusion off and the fusion-off PTX should contain no ``fma.rn.f32``. + +Requires CUDA and vLLM 0.30.0. Run it from a clean checkout so ``git_dirty`` is false: + + python scripts/ws1_gdn_provider_agreement.py > gdn_provider_agreement.json +""" + +from __future__ import annotations + +import argparse +import contextlib +import importlib.util +import json +import platform +import subprocess +import sys +from pathlib import Path +from typing import Any + +import torch + +REPO_ROOT = Path(__file__).resolve().parents[1] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +_CHECK_FILE = REPO_ROOT / "tests" / "check_gdn_recurrent_golden.py" +_STATE_DTYPES = {"fp32": torch.float32, "bf16": torch.bfloat16} +_INT_VIEW = {2: torch.int16, 4: torch.int32} + + +def _load_check_module(): + """Import the check file by path; ``tests/`` is not a package.""" + spec = importlib.util.spec_from_file_location("check_gdn_recurrent_golden", _CHECK_FILE) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _bit_mismatches(a: torch.Tensor, b: torch.Tensor) -> int: + """Elements whose bit patterns differ (NaN-safe, unlike ``a != b``).""" + assert a.dtype == b.dtype and a.shape == b.shape, (a.dtype, b.dtype, a.shape, b.shape) + view = _INT_VIEW[a.element_size()] + return int((a.contiguous().view(view) != b.contiguous().view(view)).sum()) + + +def _max_abs_diff(a: torch.Tensor, b: torch.Tensor) -> float: + return (a.float() - b.float()).abs().max().item() + + +def _git(*args: str) -> str: + try: + return subprocess.run( + ["git", *args], cwd=REPO_ROOT, capture_output=True, text=True, check=True + ).stdout.strip() + except (OSError, subprocess.CalledProcessError) as exc: + return f"unavailable: {exc}" + + +def _provenance() -> dict[str, Any]: + import triton + import vllm + + return { + "git_commit": _git("rev-parse", "HEAD"), + "git_dirty": bool(_git("status", "--porcelain")), + "python": platform.python_version(), + "torch": torch.__version__, + "triton": triton.__version__, + "vllm": vllm.__version__, + "device": torch.cuda.get_device_name(), + "capability": list(torch.cuda.get_device_capability()), + "default_fp_fusion": bool(triton.knobs.language.default_fp_fusion), + } + + +@contextlib.contextmanager +def _fp_fusion(enabled: bool): + """Set Triton's default FP fusion for kernels compiled inside the block. + + ``enable_fp_fusion`` is part of the options string in Triton's kernel cache key, + so flipping it compiles a fresh variant rather than reusing the fused one. + """ + import triton + + knobs = triton.knobs.language + previous = knobs.default_fp_fusion + knobs.default_fp_fusion = enabled + try: + yield + finally: + knobs.default_fp_fusion = previous + + +def _measure_recurrent(check, batches: list[int]) -> list[dict[str, Any]]: + bounds = {"fp32": (1e-3, 1e-5), "bf16": (1e-3, 5e-3)} # check file :117, :120 + rows = [] + for batch in batches: + for name, state_dtype in _STATE_DTYPES.items(): + # Same call as test_golden_matches_packed_decode_provider. + inp = check._inputs(batch, batch + 2, state_dtype, torch.bfloat16, seed=batch) + out_ref, state_ref = check._run_provider(inp) + out_got, state_got = check._run_golden(inp) + rows.append( + { + "batch": batch, + "state_dtype": name, + "max_abs_diff_out": _max_abs_diff(out_got, out_ref), + "max_abs_diff_state": _max_abs_diff(state_got, state_ref), + "out_mismatch_elements": _bit_mismatches(out_got, out_ref), + "out_elements": out_ref.numel(), + "state_mismatch_elements": _bit_mismatches(state_got, state_ref), + "state_elements": state_ref.numel(), + "asserted_out_atol": bounds[name][0], + "asserted_state_atol": bounds[name][1], + } + ) + return rows + + +def _measure_conv(check, batches: list[int], fp_fusion: bool) -> list[dict[str, Any]]: + rows = [] + with _fp_fusion(fp_fusion): + for batch in batches: + for name, cache_dtype in _STATE_DTYPES.items(): + # Same call as the test_conv_* provider comparisons. + inp = check._conv_inputs(batch, cache_dtype, seed=batch) + (out_ref, state_ref), (out_got, state_got) = check._run_conv_pair(inp) + rows.append( + { + "batch": batch, + "cache_dtype": name, + "fp_fusion": fp_fusion, + "out_mismatch_elements": _bit_mismatches(out_got, out_ref), + "out_elements": out_ref.numel(), + "max_abs_diff_out": _max_abs_diff(out_got, out_ref), + "state_bitwise_equal": torch.equal(state_got, state_ref), + } + ) + return rows + + +def _conv_ptx_fma_counts() -> list[dict[str, Any]] | dict[str, str]: + """``fma.rn.f32`` count per compiled variant of the provider's conv-update kernel.""" + try: + from vllm.model_executor.layers.mamba.ops import causal_conv1d as conv_module + + kernel = conv_module._causal_conv1d_update_kernel + while not hasattr(kernel, "device_caches") and hasattr(kernel, "fn"): + kernel = kernel.fn + variants = [] + for device, cache in kernel.device_caches.items(): + for compiled in cache[0].values(): + ptx = compiled.asm["ptx"] + variants.append( + { + "device": str(device), + "enable_fp_fusion": getattr(compiled.metadata, "enable_fp_fusion", None), + "fma_rn_f32": ptx.count("fma.rn.f32"), + "mul_rn_f32": ptx.count("mul.rn.f32"), + "add_rn_f32": ptx.count("add.rn.f32"), + } + ) + return variants + except Exception as exc: # introspection of Triton internals; report, do not fail + return {"unavailable": f"{type(exc).__name__}: {exc}"} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--batches", + default="1,4,17,64", + help="comma-separated batch sizes (default: the check file's 1,4,17,64)", + ) + parser.add_argument( + "--skip-fusion-off", + action="store_true", + help="do not rerun the conv comparison with Triton FP fusion disabled", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if not torch.cuda.is_available(): + print("CUDA is required", file=sys.stderr) + return 2 + try: + import vllm # noqa: F401 + except ImportError: + print("vLLM is required", file=sys.stderr) + return 2 + + batches = [int(b) for b in args.batches.split(",") if b.strip()] + check = _load_check_module() + report: dict[str, Any] = { + "runner": "scripts/ws1_gdn_provider_agreement.py", + "provenance": _provenance(), + "inputs": { + "source": "tests/check_gdn_recurrent_golden.py (_inputs, _conv_inputs)", + "seed": "seed=batch for every case", + "batches": batches, + "recurrent": "bf16 I/O, use_qk_l2norm_in_kernel=True, num_blocks=batch+2", + "conv": "bias=True, activation=silu, dim_first=True, W=4, dim=8192", + }, + "recurrent": _measure_recurrent(check, batches), + "conv": _measure_conv(check, batches, fp_fusion=True), + } + if not args.skip_fusion_off: + report["conv"] += _measure_conv(check, batches, fp_fusion=False) + report["conv_ptx"] = _conv_ptx_fma_counts() + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 7b30633a80b9192538176f8aa9798e7748c1ca9d Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:01:57 +0000 Subject: [PATCH 24/44] docs(gdn): correct the C6 design note against vLLM 0.30.0 source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review after 03176a3 found nine statements in the note that the source does not support. Fix them: - §1: the decode path is chosen per engine step and by the model, not by env defaults alone. Tabulate decode-only / mixed / draft-token steps with their conv and recurrent kernels. Qwen3-Next's gqa_interleaved_layout=True makes the fused CUDA decode unsupported, so the layer falls back to triton and fused_gdn_decode_post_conv_mtp is unreachable (:1834). The env-default assertion guards the packed-decode default but not VLLM_GDN_DECODE_KERNEL. - §2: the golden follows the kernel's arithmetic but is not a transcription everywhere: softplus uses log1p where the kernel uses log(1 + exp), and FLA_USE_FAST_OPS swaps in fast exp/log. - §3: split NULL_BLOCK_ID semantics. The recurrent provider skips <= 0 and writes zeros; the conv provider skips only == null_block_id (0) and does not write the output row; both goldens skip <= 0 and write zeros. - §4: replace the "measured" table, which had no runner, with the bounds the check file asserts and point to scripts/ws1_gdn_provider_agreement.py. State the fp32-cache conv mismatch and the FMA hypothesis the runner tests. - §5: add the mixed decode-and-prefill caveat on the provider side, including vllm-project/vllm#49827, and state that validate_data=True does not check index values either. - §6: restate the MTP deferral against RFC #428 §2.2 and the actual Qwen3-Next MTP path. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .../design/ws1-c6-428-gdn-recurrent-replay.md | 179 ++++++++++++++---- 1 file changed, 137 insertions(+), 42 deletions(-) diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/ws1-c6-428-gdn-recurrent-replay.md index 2aa1ddf23..7832bbb1b 100644 --- a/docs/design/ws1-c6-428-gdn-recurrent-replay.md +++ b/docs/design/ws1-c6-428-gdn-recurrent-replay.md @@ -2,34 +2,65 @@ Design notes for RFC #428 work item C6 (GDN recurrent response replay) on the CUDA track. Measured on 2× B200 (sm_100), torch 2.13.0+cu130, vllm 0.30.0, -transformers 5.17.0. +transformers 5.17.0. vLLM paths below are relative to the installed `vllm` 0.30.0 +package; two files are named `causal_conv1d.py`, and each citation says whether it +means vLLM's (`model_executor/layers/mamba/ops/causal_conv1d.py`) or the golden's +(`rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py`). Claim level reached: **L0 repeatable, L1 batch-invariant**. L2 is not claimed. ## 1. Which provider a rollout decode actually takes -vLLM 0.30.0 has three GDN decode paths, selected by env defaults rather than by the -model: +vLLM 0.30.0 picks the GDN decode path per engine step. The choice depends on env +defaults, on what else the step contains, and on how the model constructs the layer: -| condition | path | -|---|---| -| `VLLM_GDN_DECODE_KERNEL="cuda"` (default) + MTP | `torch.ops._C.fused_gdn_decode_post_conv_mtp` — conv, recurrence and the gated norm fused | -| `VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE=1` (default), decode-only, non-spec | `fused_recurrent_gated_delta_rule_packed_decode` | -| otherwise | `fused_sigmoid_gating_delta_rule_update` | - -A pure RL rollout decode — N sequences each emitting one token, no speculative -decoding — returns early at `qwen_gdn_linear_attn.py:1295-1307` into the **packed** -path. The golden targets that one. Aligning against the sigmoid-gating kernel would -validate a path production does not take. - -`tests/check_qwen3_next_norm_providers.py` asserts those two env defaults, so a vLLM -bump that flips either fails loudly rather than silently re-pointing the claim. - -## 2. Three places the kernel and the HF model disagree - -The golden is transcribed from the kernel, not from `modeling_qwen3_next.py`: - -1. **The gating is fused.** `beta = sigmoid(b)` and +| step contains | conv | recurrence | +|---|---|---| +| decodes only, no draft tokens | `causal_conv1d_update` | `fused_recurrent_gated_delta_rule_packed_decode` | +| decodes and at least one prefill, no draft tokens | `causal_conv1d_fn` | `fused_sigmoid_gating_delta_rule_update` for the cached decode rows | +| draft tokens (speculative decode / MTP) | `causal_conv1d_update` with `num_accepted_tokens` | `fused_sigmoid_gating_delta_rule_update`; plain decodes in the step are reclassified as prefills | + +- **Decode-only.** With `VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE` at its default of on + (`envs.py:1199-1200`), a decode-only step returns early at + `model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py:1295-1307` into + `_forward_core_decode_non_spec`, which calls the packed kernel (`:1697-1708`). **The + goldens target this path.** With the variable off, the same step goes through + `fused_sigmoid_gating_delta_rule_update` instead (`:1553-1572`). +- **Mixed decode and prefill.** `_forward_core` runs the conv for the whole non-spec + batch through `causal_conv1d_fn` (branch at `:1373`, call at `:1378-1388`) and the + cached decode rows through `fused_sigmoid_gating_delta_rule_update` + (`split_non_spec` defined at `:1408-1412`, branch at `:1493`, call at `:1497-1512`). + See §5 for what that does to the claim. +- **Draft tokens.** Spec rows go through `causal_conv1d_update` with + `num_accepted_tokens` (`:1357-1370`) and `fused_sigmoid_gating_delta_rule_update` + (`:1470-1487`). Plain decodes in the same step are reclassified as prefills + (`v1/attention/backends/gdn_attn.py:283-289`). A step with speculative decoding + enabled but zero draft tokens sets `spec_sequence_masks` to `None` + (`gdn_attn.py:236-243`) and is treated as decode-only. + +`VLLM_GDN_DECODE_KERNEL` defaults to `"cuda"`, but that is not what Qwen3-Next runs. +`model_executor/models/qwen3_next.py:495` constructs the layer with +`gqa_interleaved_layout=True`, which makes `_fused_gdn_decode_unsupported_reason` +(`qwen_gdn_linear_attn.py:535-554`) return a reason. The layer then logs a fallback +to `"triton"` (`:520-523`), or raises `ValueError` if `VLLM_GDN_DECODE_KERNEL` was +explicitly set to `cuda` (`:516-519`). As a consequence the fused +`torch.ops._C.fused_gdn_decode_post_conv_mtp` path is unreachable for Qwen3-Next: +`_can_use_fused_gdn_mtp_decode` requires `gdn_decode_kernel == "cuda"` (`:1834`). +This section is read from the source. + +`tests/check_qwen3_next_norm_providers.py` asserts both env defaults (`:124-131`). For +`VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE` that is a useful guard: a vLLM bump that +flips it fails loudly. For `VLLM_GDN_DECODE_KERNEL` it guards nothing for Qwen3-Next, +because the interleaved layout, not the default, decides the kernel. Neither +assertion checks which kernel actually runs. + +## 2. How the golden relates to the kernel + +The golden follows the kernel's arithmetic, not `modeling_qwen3_next.py`'s. Checked +line by line against `third_party/flash_linear_attention/ops/fused_recurrent.py:288-335`, +three places where the kernel and the HF model differ: + +1. **The gating is fused, in fp32.** `beta = sigmoid(b)` and `g = -exp(A_log) * softplus(a + dt_bias)` are computed inside the Triton kernel, with a `softplus` threshold branch at 20. HF computes them as separate PyTorch ops — a different rounding path. @@ -40,9 +71,19 @@ The golden is transcribed from the kernel, not from `modeling_qwen3_next.py`: `rsqrt`, which differs in the last bit. `scale` is applied to `q` *after* the norm; `k` is never scaled. -A fourth, checked and found **not** to be a divergence: prefill passes -`use_qk_l2norm_in_kernel=False` only because `fused_post_conv_prep(apply_l2norm=True)` -already normalized q/k. Both paths normalize exactly once. +Where the golden is **not** a transcription: + +- softplus: the kernel computes `tl.log(1.0 + tl.exp(x))` (`fused_recurrent.py:327`); + the golden computes `torch.log1p(torch.exp(safe))`, where `safe` is `x` on the taken + branch (`gated_delta_rule.py:107-109`). These round differently. +- The kernel's `exp`/`log` become `fast_expf`/`fast_logf` when `FLA_USE_FAST_OPS=1` + (`third_party/flash_linear_attention/ops/op.py:16-25`). The golden models the + default. + +Checked and found **not** to be a divergence: prefill passes +`use_qk_l2norm_in_kernel=False` (`qwen_gdn_linear_attn.py:1542`) only because +`fused_post_conv_prep(apply_l2norm=True)` (`:1450`) already normalized q/k. Decode +passes `True` (`:1707`). Both paths normalize exactly once. ## 3. State ABI @@ -51,30 +92,60 @@ Mirrored rather than reinvented: - recurrent state `[num_blocks, HV, V, K]`, V-major, addressed by `ssm_state_indices` - conv state `[num_blocks, dim, width-1]`, layout chosen by the global `is_conv_state_dim_first()`; the golden takes it as an argument and both are tested -- `NULL_BLOCK_ID` (index `<= 0`) means skip: zeros out, block untouched - the accumulator is fp32 for the whole step; the store rounds to the state tensor's - dtype, which `FUSED_GDN_STATE_DTYPES` allows to be fp32 **or** bf16 -- `causal_conv1d_update` casts `x` to the cache dtype before anything else + dtype, which `FUSED_GDN_STATE_DTYPES` (`qwen_gdn_linear_attn.py:91`) allows to be + fp32 **or** bf16 +- `causal_conv1d_update` casts `x` to the cache dtype before computing + +`NULL_BLOCK_ID` is **not** one contract across the two providers: + +| | recurrent provider | conv provider | both goldens | +|---|---|---|---| +| which indices skip | `<= 0` (`fused_recurrent.py:300`) | `== null_block_id` only, default `NULL_BLOCK_ID` = 0 (vLLM `causal_conv1d.py:835`; `v1/attention/backends/utils.py:47`) | `<= 0` | +| output for a skipped row | zeros (`fused_recurrent.py:301-302`) | not written (vLLM `causal_conv1d.py:835-839` returns before any store) | zeros | +| state block | not touched | not touched | not touched | -Contractions run in the repo's fixed 32-wide chunk order rather than `torch.matmul`, -whose reduction order is unspecified. That is what the L1 claim rests on, and also -why the golden is not bitwise against the kernel's tree. +A negative conv index is therefore inactive in the golden +(`tests/test_gdn_state_contract.py:51-56`) but a real, out-of-range index to the conv +provider. + +Contractions use `_chunked_sum` (`gated_delta_rule.py:73-87`): fixed 32-wide chunks, +so the reduction shape per row does not depend on the batch size, rather than +`torch.matmul`, whose reduction order is unspecified. That is the argument for the +L1 claim; L1 itself is established empirically by `test_golden_is_batch_invariant`. +The same choice is why the golden is not bitwise against the kernel's reduction. ## 4. Agreement with the provider -Qwen3-Next dims (H=16, HV=32, K=V=128), `use_qk_l2norm_in_kernel=True`: +Qwen3-Next dims (H=16, HV=32, K=V=128), bf16 I/O, `use_qk_l2norm_in_kernel=True`, +random inputs, B ∈ {1, 4, 17, 64}, one seed per batch. Bounds asserted by +`tests/check_gdn_recurrent_golden.py`: | | max\|diff\| out | max\|diff\| state | |---|---|---| -| fp32 state, B=1..64 | 1.5e-08 .. 6.1e-05 | ≤ 3.0e-07 | -| bf16 state, B=1..64 | 3.7e-09 .. 3.1e-05 | ≤ 2.0e-03 | +| fp32 state | ≤ 1e-3 | ≤ 1e-5 | +| bf16 state | ≤ 1e-3 | ≤ 5e-3 | + +`scripts/ws1_gdn_provider_agreement.py` prints the measured values behind these +bounds for the same inputs. An earlier table here quoted tighter figures from a +single run with no committed runner; it is withdrawn in favour of the runner's +output. Causal conv uses sequential FP32 accumulation **starting from bias**, with -products first rounded to the operand dtype. The previous BF16 path incorrectly -promoted both operands to FP32; its disagreements were not limited to one BF16 -ULP. Cancellation and bias-order CPU tests now cover these errors. Provider -comparisons preserve the existing absolute bounds and additionally limit BF16 -mismatches to 32 elements on the checked fixtures. This is not a bitwise claim. +products first rounded to the operand dtype (golden `causal_conv1d.py:167-177`; +the provider initialises from bias at vLLM `causal_conv1d.py:960-967`, `1000`). The +previous BF16 path incorrectly promoted both operands to FP32; its disagreements +were not limited to one BF16 ULP. The CPU tests in `tests/test_gdn_state_contract.py` +cover bias order and bf16 product rounding, and +`test_conv_provider_preserves_bf16_product_cancellation` checks the provider on a +constructed cancellation input. Provider comparisons limit mismatches to 32 elements +on the checked fixtures. This is not a bitwise claim. + +With an fp32 cache a few output elements still differ. The cause is not pinned. One +candidate is FP contraction: Triton's default `enable_fp_fusion=True` may contract the +provider's `acc += matrix_x * matrix_w` (vLLM `causal_conv1d.py:1061`) into an FMA, +whereas the golden rounds the product first. The runner reruns the comparison with +fusion off and counts `fma.rn.f32` in the compiled kernel to test this. ## 5. Decode versus chunked prefill @@ -83,14 +154,38 @@ they are withdrawn as acceptance evidence. The existing 128-step synthetic test only bounds its fixed seed and gate inputs. It does not establish a universal plateau, prefill/decode equality, or any bound on model logits. +**Mixed decode-and-prefill steps (provider side).** The goldens target the +decode-only path (§1). In a step that also holds a prefill, vLLM 0.30.0 sends the +cached decode rows through `causal_conv1d_fn` and +`fused_sigmoid_gating_delta_rule_update` instead, so in those steps neither golden +matches what rollout runs. The two recurrent kernels do not agree bitwise. A +measurement on B200 with Qwen3-Next TP1 shapes, driving the real +`GDNAttentionMetadataBuilder.build()` and `_forward_core`, found the step's bf16 +output equal but the fp32 state different in 75,146 of 524,288 elements (8.8e-08 +relative). In 4 of 20 seeds the difference reached a bf16 output within the next 16 +decode steps. This was measured outside this change and is not yet published; treat it +as a reported observation. + +vllm-project/vllm#49827, open and unmerged, would route mixed-step *recurrent* +decodes through the packed kernel too, according to its description. It was +validated on Qwen3.5 (non-interleaved), H100, TP1, and it does not change the conv +path in mixed steps, which stays `causal_conv1d_fn`. Disabling +`VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE` instead sends every decode row through +`fused_sigmoid_gating_delta_rule_update`, and the recurrent golden's target would +have to change. + The strict profile uses FP32 recurrent state. BF16 state remains a differential experiment. Full checkpoint prefill, response replay, optimizer updates and reload all remain required before L2 can pass. Cache indices must be int32/int64, on the input device, and positive active indices must be unique and in range. Nonpositive sentinels may repeat. The golden -validates before any cache update; raw provider calls remain the caller's -responsibility (the provider's default does not bounds-check). +validates before any cache update. Raw provider calls remain the caller's +responsibility: neither provider checks index values. `causal_conv1d_update`'s +`validate_data=True` adds only shape and stride asserts and a +`null_block_id is not None` assert (vLLM `causal_conv1d.py:1151-1153`, `1188-1201`), +so an out-of-range index is an unchecked memory access in either mode (read from +the source, not exercised). ## 6. Current boundary @@ -98,7 +193,7 @@ Not covered, with reasons: | deferred | why | |---|---| -| Speculative decode / MTP | `fused_gdn_decode_post_conv_mtp` fuses conv, recurrence and the gated norm; validating it needs a draft model | +| Speculative decode / MTP | RFC #428 §2.2 excludes speculative decoding from the first claim. Qwen3-Next's MTP head ships in the checkpoint (`mtp.*`, loaded by `model_executor/models/qwen3_next_mtp.py`) and is full attention (`qwen3_next_mtp.py:90-92`). Enabling it changes the target model's GDN path in steps that carry draft tokens (§1); `fused_gdn_decode_post_conv_mtp` is unreachable for Qwen3-Next | | Backward for the recurrent step | no upstream backward exists, and a naive BPTT through a sequential recurrence is not batch-invariant — it needs its own design | | Provider bridge / registry entry | the golden should survive a drift sweep against a real checkpoint first | | Paged block allocation policy | the ABI is mirrored; the allocator is not modelled | From 9da9c36e47241114b2bd980fb90bea5d6461480b Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:05:59 +0000 Subject: [PATCH 25/44] fix(gdn): report unknown git state as null, not dirty, in the C6 runner _git() returned the string "unavailable: ..." on failure, so bool(_git("status", "--porcelain")) turned "git missing / not a checkout" into git_dirty=True. Return None on failure and record git_dirty and git_commit as null in that case, so the provenance distinguishes unknown from dirty. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- scripts/ws1_gdn_provider_agreement.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/ws1_gdn_provider_agreement.py b/scripts/ws1_gdn_provider_agreement.py index a3a433575..393d342cd 100644 --- a/scripts/ws1_gdn_provider_agreement.py +++ b/scripts/ws1_gdn_provider_agreement.py @@ -68,22 +68,25 @@ def _max_abs_diff(a: torch.Tensor, b: torch.Tensor) -> float: return (a.float() - b.float()).abs().max().item() -def _git(*args: str) -> str: +def _git(*args: str) -> str | None: + """Stripped stdout, or ``None`` if git is missing or the command fails.""" try: return subprocess.run( ["git", *args], cwd=REPO_ROOT, capture_output=True, text=True, check=True ).stdout.strip() - except (OSError, subprocess.CalledProcessError) as exc: - return f"unavailable: {exc}" + except (OSError, subprocess.CalledProcessError): + return None def _provenance() -> dict[str, Any]: import triton import vllm + status = _git("status", "--porcelain") return { + # None means "unknown" (no git, or not a checkout), never "clean". "git_commit": _git("rev-parse", "HEAD"), - "git_dirty": bool(_git("status", "--porcelain")), + "git_dirty": None if status is None else bool(status), "python": platform.python_version(), "torch": torch.__version__, "triton": triton.__version__, From 5216a6c3443519e85f6d01a91500fcab60dbf450 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:06:16 +0000 Subject: [PATCH 26/44] docs(gdn): restate the backward deferral against RFC #428 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The §6 row deferred backward because "a naive BPTT through a sequential recurrence is not batch-invariant". RFC #428 does not ask backward to be: §2.2 item 4 says backward need not match rollout, only be correct for the replayed forward, and §9.1 makes the GDN backward its own work item, C7. Cite those instead, matching the PR description. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- docs/design/ws1-c6-428-gdn-recurrent-replay.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/ws1-c6-428-gdn-recurrent-replay.md index 7832bbb1b..efe2b08aa 100644 --- a/docs/design/ws1-c6-428-gdn-recurrent-replay.md +++ b/docs/design/ws1-c6-428-gdn-recurrent-replay.md @@ -194,7 +194,7 @@ Not covered, with reasons: | deferred | why | |---|---| | Speculative decode / MTP | RFC #428 §2.2 excludes speculative decoding from the first claim. Qwen3-Next's MTP head ships in the checkpoint (`mtp.*`, loaded by `model_executor/models/qwen3_next_mtp.py`) and is full attention (`qwen3_next_mtp.py:90-92`). Enabling it changes the target model's GDN path in steps that carry draft tokens (§1); `fused_gdn_decode_post_conv_mtp` is unreachable for Qwen3-Next | -| Backward for the recurrent step | no upstream backward exists, and a naive BPTT through a sequential recurrence is not batch-invariant — it needs its own design | +| Backward for the recurrent step | RFC #428 §2.2 item 4: backward need not match rollout, only be correct for the replayed forward. RFC §9.1 makes it a separate work item, **C7** (GDN backward/recompute adapter including prompt-state gradient). `supports_backward=false` here; `_softplus`'s NaN-gradient fix (`08969ac`) adds no backward claim | | Provider bridge / registry entry | the golden should survive a drift sweep against a real checkpoint first | | Paged block allocation policy | the ABI is mirrored; the allocator is not modelled | | TP sharding of `A_log` / `dt_bias` | single card only | From 10e523056bfcab6d16bc9e0e9791c1ceaa0e0cc2 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:47:57 +0000 Subject: [PATCH 27/44] fix(gdn): make the C6 runner's fusion-off arm and FMA count real A run of the runner at d66e595 showed both of its FMA probes were ineffective: - The fusion-off arm reused the fused kernel. Triton 3.7.1 keys its in-memory kernel cache on the launch kwargs; the default_fp_fusion knob is read in parse_options only after a cache miss. Flipping the knob in-process never missed, so all six recorded variants had enable_fp_fusion=True and the "off" counts were the "on" counts again. Run the fusion-off arm in a child process with TRITON_DEFAULT_FP_FUSION=0 and a private TRITON_CACHE_DIR instead. - fma.rn.f32 = 0 in the PTX does not rule out FMA. With fusion on, Triton emits plain mul.f32/add.f32 and lets ptxas contract them into FFMA; with fusion off it passes --fmad=false. Count plain and .rn PTX ops, and FFMA/FMUL/FADD in the SASS (Triton's bundled cuobjdump). Report per-arm kernel variants under conv_kernels and add fusion_check, which says whether the fusion-off variants were actually compiled without fusion, so a silent no-op cannot pass for a result again. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- scripts/ws1_gdn_provider_agreement.py | 166 +++++++++++++++++--------- 1 file changed, 109 insertions(+), 57 deletions(-) diff --git a/scripts/ws1_gdn_provider_agreement.py b/scripts/ws1_gdn_provider_agreement.py index 393d342cd..539fa9979 100644 --- a/scripts/ws1_gdn_provider_agreement.py +++ b/scripts/ws1_gdn_provider_agreement.py @@ -15,11 +15,19 @@ (batch, state dtype): max|d out|, max|d state| and bitwise mismatch counts. * ``conv``: provider vs golden for ``causal_conv1d_update``, per (batch, cache dtype): output mismatch count and max|diff|, and whether the rolled state is bitwise equal. -* ``conv`` again with Triton FP fusion disabled - (``triton.knobs.language.default_fp_fusion = False``), plus ``conv_ptx``: the number - of ``fma.rn.f32`` instructions in each compiled variant of the provider's conv - kernel. If the fp32-cache mismatches are FMA contraction, they should vanish with - fusion off and the fusion-off PTX should contain no ``fma.rn.f32``. +* ``conv`` again with Triton FP fusion disabled, plus ``conv_kernels``: per compiled + variant of the provider's conv-update kernel, its ``enable_fp_fusion`` option and + instruction counts from the PTX and from the SASS. + +The fusion-off arm runs in a child process with ``TRITON_DEFAULT_FP_FUSION=0`` and a +private ``TRITON_CACHE_DIR``. Flipping ``triton.knobs.language.default_fp_fusion`` +in-process does not work: Triton's in-memory kernel cache is keyed on the launch +kwargs, and the knob is read only after a cache miss, so the fused variant is reused. + +Count FMAs in the SASS, not only the PTX. With fusion on, Triton emits plain +``mul.f32``/``add.f32`` and leaves ptxas free to contract them into ``FFMA``; with +fusion off it passes ``--fmad=false`` to ptxas. A PTX with no ``fma.rn.f32`` can still +run FMAs. ``fusion_check`` reports whether each arm compiled what it claims to. Requires CUDA and vLLM 0.30.0. Run it from a clean checkout so ``git_dirty`` is false: @@ -29,12 +37,14 @@ from __future__ import annotations import argparse -import contextlib import importlib.util import json +import os import platform +import re import subprocess import sys +import tempfile from pathlib import Path from typing import Any @@ -47,6 +57,16 @@ _CHECK_FILE = REPO_ROOT / "tests" / "check_gdn_recurrent_golden.py" _STATE_DTYPES = {"fp32": torch.float32, "bf16": torch.bfloat16} _INT_VIEW = {2: torch.int16, 4: torch.int32} +# Opcode patterns. PTX: a rounding-qualified op (".rn") may not be contracted by ptxas; +# the plain form may. SASS: count opcode tokens, including modifiers such as FFMA.FTZ. +_PTX_OPS = { + "fma_rn_f32": r"\bfma\.rn\.f32\b", + "mul_f32": r"\bmul\.f32\b", + "add_f32": r"\badd\.f32\b", + "mul_rn_f32": r"\bmul\.rn\.f32\b", + "add_rn_f32": r"\badd\.rn\.f32\b", +} +_SASS_OPS = {"FFMA": r"\bFFMA[\w.]*", "FMUL": r"\bFMUL[\w.]*", "FADD": r"\bFADD[\w.]*"} def _load_check_module(): @@ -97,24 +117,6 @@ def _provenance() -> dict[str, Any]: } -@contextlib.contextmanager -def _fp_fusion(enabled: bool): - """Set Triton's default FP fusion for kernels compiled inside the block. - - ``enable_fp_fusion`` is part of the options string in Triton's kernel cache key, - so flipping it compiles a fresh variant rather than reusing the fused one. - """ - import triton - - knobs = triton.knobs.language - previous = knobs.default_fp_fusion - knobs.default_fp_fusion = enabled - try: - yield - finally: - knobs.default_fp_fusion = previous - - def _measure_recurrent(check, batches: list[int]) -> list[dict[str, Any]]: bounds = {"fp32": (1e-3, 1e-5), "bf16": (1e-3, 5e-3)} # check file :117, :120 rows = [] @@ -141,30 +143,36 @@ def _measure_recurrent(check, batches: list[int]) -> list[dict[str, Any]]: return rows -def _measure_conv(check, batches: list[int], fp_fusion: bool) -> list[dict[str, Any]]: +def _measure_conv(check, batches: list[int]) -> list[dict[str, Any]]: + import triton + + fp_fusion = bool(triton.knobs.language.default_fp_fusion) # this process's setting rows = [] - with _fp_fusion(fp_fusion): - for batch in batches: - for name, cache_dtype in _STATE_DTYPES.items(): - # Same call as the test_conv_* provider comparisons. - inp = check._conv_inputs(batch, cache_dtype, seed=batch) - (out_ref, state_ref), (out_got, state_got) = check._run_conv_pair(inp) - rows.append( - { - "batch": batch, - "cache_dtype": name, - "fp_fusion": fp_fusion, - "out_mismatch_elements": _bit_mismatches(out_got, out_ref), - "out_elements": out_ref.numel(), - "max_abs_diff_out": _max_abs_diff(out_got, out_ref), - "state_bitwise_equal": torch.equal(state_got, state_ref), - } - ) + for batch in batches: + for name, cache_dtype in _STATE_DTYPES.items(): + # Same call as the test_conv_* provider comparisons. + inp = check._conv_inputs(batch, cache_dtype, seed=batch) + (out_ref, state_ref), (out_got, state_got) = check._run_conv_pair(inp) + rows.append( + { + "batch": batch, + "cache_dtype": name, + "fp_fusion": fp_fusion, + "out_mismatch_elements": _bit_mismatches(out_got, out_ref), + "out_elements": out_ref.numel(), + "max_abs_diff_out": _max_abs_diff(out_got, out_ref), + "state_bitwise_equal": torch.equal(state_got, state_ref), + } + ) return rows -def _conv_ptx_fma_counts() -> list[dict[str, Any]] | dict[str, str]: - """``fma.rn.f32`` count per compiled variant of the provider's conv-update kernel.""" +def _count(patterns: dict[str, str], text: str) -> dict[str, int]: + return {name: len(re.findall(pattern, text)) for name, pattern in patterns.items()} + + +def _conv_kernel_variants() -> list[dict[str, Any]] | dict[str, str]: + """Options and instruction counts per compiled variant of the conv-update kernel.""" try: from vllm.model_executor.layers.mamba.ops import causal_conv1d as conv_module @@ -174,21 +182,55 @@ def _conv_ptx_fma_counts() -> list[dict[str, Any]] | dict[str, str]: variants = [] for device, cache in kernel.device_caches.items(): for compiled in cache[0].values(): - ptx = compiled.asm["ptx"] - variants.append( - { - "device": str(device), - "enable_fp_fusion": getattr(compiled.metadata, "enable_fp_fusion", None), - "fma_rn_f32": ptx.count("fma.rn.f32"), - "mul_rn_f32": ptx.count("mul.rn.f32"), - "add_rn_f32": ptx.count("add.rn.f32"), - } - ) + row: dict[str, Any] = { + "device": str(device), + "enable_fp_fusion": getattr(compiled.metadata, "enable_fp_fusion", None), + "ptx": _count(_PTX_OPS, compiled.asm["ptx"]), + } + try: + row["sass"] = _count(_SASS_OPS, compiled.asm["sass"]) + except Exception as exc: # needs cuobjdump; report, do not fail + row["sass"] = {"unavailable": f"{type(exc).__name__}: {exc}"} + variants.append(row) return variants except Exception as exc: # introspection of Triton internals; report, do not fail return {"unavailable": f"{type(exc).__name__}: {exc}"} +def _conv_report(check, batches: list[int]) -> dict[str, Any]: + return {"conv": _measure_conv(check, batches), "conv_kernels": _conv_kernel_variants()} + + +def _conv_report_without_fusion(batches: str) -> dict[str, Any]: + """Rerun the conv arm in a child process that compiles with fusion off.""" + with tempfile.TemporaryDirectory(prefix="triton-nofusion-") as cache_dir: + env = dict(os.environ, TRITON_DEFAULT_FP_FUSION="0", TRITON_CACHE_DIR=cache_dir) + child = subprocess.run( + [sys.executable, str(Path(__file__).resolve()), "--conv-only", "--batches", batches], + env=env, + stdout=subprocess.PIPE, + text=True, + check=True, + ) + return json.loads(child.stdout) + + +def _fusion_check(on: Any, off: Any) -> dict[str, Any]: + """Whether each arm's compiled variants carry the fusion setting it claims.""" + + def flags(variants: Any) -> list[Any] | None: + if not isinstance(variants, list): + return None + return [v["enable_fp_fusion"] for v in variants] + + on_flags, off_flags = flags(on), flags(off) + return { + "fusion_on_variants": on_flags, + "fusion_off_variants": off_flags, + "fusion_off_effective": bool(off_flags) and all(f is False for f in off_flags), + } + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument( @@ -201,6 +243,8 @@ def parse_args() -> argparse.Namespace: action="store_true", help="do not rerun the conv comparison with Triton FP fusion disabled", ) + # Internal: the fusion-off child process prints only the conv arm. + parser.add_argument("--conv-only", action="store_true", help=argparse.SUPPRESS) return parser.parse_args() @@ -217,6 +261,10 @@ def main() -> int: batches = [int(b) for b in args.batches.split(",") if b.strip()] check = _load_check_module() + if args.conv_only: + print(json.dumps(_conv_report(check, batches), sort_keys=True)) + return 0 + report: dict[str, Any] = { "runner": "scripts/ws1_gdn_provider_agreement.py", "provenance": _provenance(), @@ -228,11 +276,15 @@ def main() -> int: "conv": "bias=True, activation=silu, dim_first=True, W=4, dim=8192", }, "recurrent": _measure_recurrent(check, batches), - "conv": _measure_conv(check, batches, fp_fusion=True), } + fused = _conv_report(check, batches) + report["conv"] = fused["conv"] + report["conv_kernels"] = {"fusion_on": fused["conv_kernels"]} if not args.skip_fusion_off: - report["conv"] += _measure_conv(check, batches, fp_fusion=False) - report["conv_ptx"] = _conv_ptx_fma_counts() + unfused = _conv_report_without_fusion(args.batches) + report["conv"] += unfused["conv"] + report["conv_kernels"]["fusion_off"] = unfused["conv_kernels"] + report["fusion_check"] = _fusion_check(fused["conv_kernels"], unfused["conv_kernels"]) print(json.dumps(report, indent=2, sort_keys=True)) return 0 From 2d895d09da9c22c69594cc8b7c55e8ced7c519bb Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:48:57 +0000 Subject: [PATCH 28/44] docs(gdn): quote the runner's measured C6 agreement, not stale counts The runner on d66e595 (B200, clean checkout) reproduced the recurrent figures the note had withdrawn for lack of a runner, so restore them as a per-batch table with their provenance and bitwise output-mismatch counts. The conv counts quoted in test_conv_output_matches_provider_with_fp32_cache's docstring (1 of 8192 at B=1, 15 of 524288 at B=64) predate the product- rounding fix; the runner measures 0, 0, 1 and 5 for B = 1, 4, 17, 64. Replace them, and record the bf16-cache counts in the note. State plainly that the FMA explanation for the fp32-cache mismatches is not determined: that run's fusion-off arm reused the fused kernel, and a PTX without fma.rn.f32 cannot exclude ptxas contraction. The runner now tests both (previous commit). Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .../design/ws1-c6-428-gdn-recurrent-replay.md | 42 +++++++++++++++---- tests/check_gdn_recurrent_golden.py | 8 ++-- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/ws1-c6-428-gdn-recurrent-replay.md index efe2b08aa..44c6c5a36 100644 --- a/docs/design/ws1-c6-428-gdn-recurrent-replay.md +++ b/docs/design/ws1-c6-428-gdn-recurrent-replay.md @@ -126,10 +126,24 @@ random inputs, B ∈ {1, 4, 17, 64}, one seed per batch. Bounds asserted by | fp32 state | ≤ 1e-3 | ≤ 1e-5 | | bf16 state | ≤ 1e-3 | ≤ 5e-3 | -`scripts/ws1_gdn_provider_agreement.py` prints the measured values behind these -bounds for the same inputs. An earlier table here quoted tighter figures from a -single run with no committed runner; it is withdrawn in favour of the runner's -output. +Measured values for the same inputs, from `scripts/ws1_gdn_provider_agreement.py` on +B200 at commit `acf38b6` (clean checkout). The last column counts output elements +whose bits differ: + +| state | B | max\|diff\| out | max\|diff\| state | out elements differing | +|---|---|---|---|---| +| fp32 | 1 | 1.49e-08 | 1.19e-07 | 1 / 4096 | +| fp32 | 4 | 9.54e-07 | 1.79e-07 | 2 / 16384 | +| fp32 | 17 | 3.81e-06 | 2.38e-07 | 10 / 69632 | +| fp32 | 64 | 6.10e-05 | 2.98e-07 | 40 / 262144 | +| bf16 | 1 | 3.73e-09 | 9.77e-04 | 2 / 4096 | +| bf16 | 4 | 1.53e-05 | 9.77e-04 | 2 / 16384 | +| bf16 | 17 | 3.05e-05 | 1.95e-03 | 14 / 69632 | +| bf16 | 64 | 3.05e-05 | 1.95e-03 | 36 / 262144 | + +This reproduces the figures an earlier version of this note quoted without a runner +(out 1.5e-08 .. 6.1e-05 and state ≤ 3.0e-07 for fp32; out 3.7e-09 .. 3.1e-05 and +state ≤ 2.0e-03 for bf16). It is one seed per batch on one device. Causal conv uses sequential FP32 accumulation **starting from bias**, with products first rounded to the operand dtype (golden `causal_conv1d.py:167-177`; @@ -141,11 +155,21 @@ cover bias order and bf16 product rounding, and constructed cancellation input. Provider comparisons limit mismatches to 32 elements on the checked fixtures. This is not a bitwise claim. -With an fp32 cache a few output elements still differ. The cause is not pinned. One -candidate is FP contraction: Triton's default `enable_fp_fusion=True` may contract the -provider's `acc += matrix_x * matrix_w` (vLLM `causal_conv1d.py:1061`) into an FMA, -whereas the golden rounds the product first. The runner reruns the comparison with -fusion off and counts `fma.rn.f32` in the compiled kernel to test this. +Measured with the same runner and commit: the rolled conv state is bitwise equal in +all 8 (batch, cache dtype) cases. Output elements differing, with an fp32 cache: +0 / 8192 (B=1), 0 / 32768 (B=4), 1 / 139264 (B=17, max|diff| 2.44e-04) and +5 / 524288 (B=64, max|diff| 3.91e-03). With a bf16 cache: 0 at B=1, 4 and 17, and +3 / 524288 at B=64 (max|diff| 1.56e-02). + +Why a few fp32-cache elements differ is **not determined**. One candidate is FP +contraction: with Triton's default `enable_fp_fusion=True`, ptxas may contract the +provider's `acc += matrix_x * matrix_w` (vLLM `causal_conv1d.py:1061`) into an `FFMA`, +whereas the golden rounds the product first. A PTX without `fma.rn.f32` does not rule +this out, because the contraction can happen in ptxas. The runner tests it by +rerunning the comparison in a child process compiled with fusion off and by counting +`FFMA` in each variant's SASS; its `fusion_check` field says whether the fusion-off +variants were really compiled that way. The run above predates that arm working (its +fusion-off pass reused the fused kernel), so it is no evidence either way. ## 5. Decode versus chunked prefill diff --git a/tests/check_gdn_recurrent_golden.py b/tests/check_gdn_recurrent_golden.py index aefecaddf..103098fc5 100644 --- a/tests/check_gdn_recurrent_golden.py +++ b/tests/check_gdn_recurrent_golden.py @@ -282,10 +282,12 @@ def test_conv_state_update_is_bitwise_exact(batch): @pytest.mark.parametrize("batch", [1, 4, 17, 64]) def test_conv_output_matches_provider_with_fp32_cache(batch): - """An fp32 cache reproduces the provider up to fp32 ULP on a few elements. + """An fp32 cache reproduces the provider except on a few output elements. - Measured: 1 element of 8192 at B=1, 15 of 524288 at B=64. The bound is on - the magnitude and on a handful of elements rather than on a tight rate -- + Measured by scripts/ws1_gdn_provider_agreement.py (B200, acf38b6): 0 of 8192 + at B=1, 0 at B=4, 1 of 139264 at B=17, 5 of 524288 at B=64; max|diff| 3.9e-3. + The bound is on the magnitude and on a handful of elements rather than on a + tight rate -- at small batches a single straddling element is already 1.2e-4 of the tensor, which says nothing about accuracy. """ From 52f7f61951104f38ada05e71978a6445952ab690 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:08:52 +0000 Subject: [PATCH 29/44] docs(gdn): record that FP contraction does not explain the conv mismatches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixed runner (b54e50f) on 7622616, B200, settles the FMA hypothesis left open in §4. Its fusion-off arm took effect: all six variants compiled with enable_fp_fusion=False, and their PTX uses only .rn-qualified f32 mul/add, which ptxas may not contract. With contraction verifiably off, the fp32-cache mismatch counts (0, 0, 1, 5 for B = 1, 4, 17, 64) and max|diff| are identical to the fused run, and neither arm's SASS has an FFMA. So FP contraction is ruled out; the actual cause stays open. The note bases the conclusion on the A/B outcome, not on the opcode counts: each variant's PTX has only two f32 multiplies, too few to be the four tap products, so the counters do not show where the products are computed. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .../design/ws1-c6-428-gdn-recurrent-replay.md | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/ws1-c6-428-gdn-recurrent-replay.md index 44c6c5a36..25febbb8c 100644 --- a/docs/design/ws1-c6-428-gdn-recurrent-replay.md +++ b/docs/design/ws1-c6-428-gdn-recurrent-replay.md @@ -161,15 +161,23 @@ all 8 (batch, cache dtype) cases. Output elements differing, with an fp32 cache: 5 / 524288 (B=64, max|diff| 3.91e-03). With a bf16 cache: 0 at B=1, 4 and 17, and 3 / 524288 at B=64 (max|diff| 1.56e-02). -Why a few fp32-cache elements differ is **not determined**. One candidate is FP -contraction: with Triton's default `enable_fp_fusion=True`, ptxas may contract the -provider's `acc += matrix_x * matrix_w` (vLLM `causal_conv1d.py:1061`) into an `FFMA`, -whereas the golden rounds the product first. A PTX without `fma.rn.f32` does not rule -this out, because the contraction can happen in ptxas. The runner tests it by -rerunning the comparison in a child process compiled with fusion off and by counting -`FFMA` in each variant's SASS; its `fusion_check` field says whether the fusion-off -variants were really compiled that way. The run above predates that arm working (its -fusion-off pass reused the fused kernel), so it is no evidence either way. +**FP contraction is ruled out** as the cause of the few differing fp32-cache +elements; what does cause them is **not determined**. The candidate was that, with +Triton's default `enable_fp_fusion=True`, ptxas contracts the provider's +`acc += matrix_x * matrix_w` (vLLM `causal_conv1d.py:1061`) into an `FFMA`, whereas the +golden rounds the product first. The runner tests this by rerunning the comparison in +a child process compiled with fusion off. On B200 at commit `cf6be1a`: + +- the fusion-off arm took effect: all 6 compiled variants carry + `enable_fp_fusion=False`, and their PTX has only `.rn`-qualified f32 `mul`/`add`, + which ptxas may not contract; +- the mismatch counts and max\|diff\| are **identical** with fusion on and off + (0, 0, 1, 5 for B = 1, 4, 17, 64); +- neither arm's SASS contains `FFMA`. + +The conclusion rests on the second point. The opcode counts are supporting evidence +only: each variant's PTX has 2 f32 multiplies, too few to be the four tap products, so +the counters do not show which instructions compute those products. ## 5. Decode versus chunked prefill From 7862a1263e0079d4ae72fd6a7e3346d8214ef1c8 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:24:02 +0000 Subject: [PATCH 30/44] docs(gdn): scope the mixed-step caveat to what was measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §5 said a mixed decode-and-prefill step leaves the decode row's bf16 output equal. That holds only at TP1 head counts (H=16, HV=32). At TP4 per-rank head counts (H=4, HV=8) the same step already moves one output element (4.8e-05 relative) and 32,663 of 131,072 state elements (1.7e-07), with the same numbers for all three prefill-bearing compositions. Tabulate both head counts and state the measurement's limits: one standalone layer from config.json, synthetic parameters and cache, single process with no engine, scheduler or CUDA graph, metadata built by the test. The 4-of-20 propagation figure is TP1-only and does not carry over to real weights. Conv output and state matched bitwise, so the difference is in the recurrent kernels. vllm-project/vllm#49827's two commits on 0.30.0 close the decode-row gap at both head counts (scheduler part untested); with packed decode disabled the decode row is bitwise equal in every composition. Source: the GDN evidence branch's committed results (not yet published). Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .../design/ws1-c6-428-gdn-recurrent-replay.md | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/ws1-c6-428-gdn-recurrent-replay.md index 25febbb8c..21abba4aa 100644 --- a/docs/design/ws1-c6-428-gdn-recurrent-replay.md +++ b/docs/design/ws1-c6-428-gdn-recurrent-replay.md @@ -189,22 +189,41 @@ plateau, prefill/decode equality, or any bound on model logits. **Mixed decode-and-prefill steps (provider side).** The goldens target the decode-only path (§1). In a step that also holds a prefill, vLLM 0.30.0 sends the cached decode rows through `causal_conv1d_fn` and -`fused_sigmoid_gating_delta_rule_update` instead, so in those steps neither golden -matches what rollout runs. The two recurrent kernels do not agree bitwise. A -measurement on B200 with Qwen3-Next TP1 shapes, driving the real -`GDNAttentionMetadataBuilder.build()` and `_forward_core`, found the step's bf16 -output equal but the fp32 state different in 75,146 of 524,288 elements (8.8e-08 -relative). In 4 of 20 seeds the difference reached a bf16 output within the next 16 -decode steps. This was measured outside this change and is not yet published; treat it -as a reported observation. - -vllm-project/vllm#49827, open and unmerged, would route mixed-step *recurrent* -decodes through the packed kernel too, according to its description. It was -validated on Qwen3.5 (non-interleaved), H100, TP1, and it does not change the conv -path in mixed steps, which stays `causal_conv1d_fn`. Disabling -`VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE` instead sends every decode row through -`fused_sigmoid_gating_delta_rule_update`, and the recurrent golden's target would -have to change. +`fused_sigmoid_gating_delta_rule_update` instead, so in those steps the recurrent +golden does not target what rollout runs. + +A measurement on B200 drove the real `GDNAttentionMetadataBuilder.build()` and +`_forward_core` of **one standalone layer** built from the checkpoint's +`config.json`. Its limits: parameters and cache were synthetic (no weights loaded); it +ran in a single process, with no engine, scheduler or CUDA graph; and the test, not the +scheduler, built the attention metadata (decode rows first). For one target decode +request with an fp32 recurrent state, three prefill-bearing step compositions gave +identical numbers: + +| head counts | step's bf16 output | fp32 state | +|---|---|---| +| TP1 (H=16, HV=32) | matched | 75,146 of 524,288 elements differ (8.8e-08 relative) | +| TP4 per-rank (H=4, HV=8) | 1 element differs (4.8e-05 relative) | 32,663 of 131,072 differ (1.7e-07) | + +The convolution output and conv state matched bitwise in every composition, so the +difference comes from the recurrent kernels, not from `causal_conv1d_fn`. At TP1 the +state difference reached a bf16 output within the next 16 plain decode steps in 4 of +20 seeds; that was measured at TP1 only, and with synthetic parameters the frequency +does not carry over to real weights. At TP4 per-rank head counts it appears in the +same step's output. This is a provider-side batch-composition dependence: whether a +prefill shares the step changes a decode row's state, and at TP4 per-rank head counts +its output too, which no golden can fix. The measurement was made outside this change +and is not yet published; treat it as a reported observation. + +vllm-project/vllm#49827, open and unmerged, routes mixed-step *recurrent* decodes +through the packed kernel too. Its two commits, applied to 0.30.0, closed the gap to 0 +at both head counts above; its scheduler part was not tested. It does not change the +conv path in mixed steps, which stays `causal_conv1d_fn`, and its own validation is on +Qwen3.5 (non-interleaved), H100, TP1. Disabling +`VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE` instead sends every non-speculative decode +row through `fused_sigmoid_gating_delta_rule_update`; the same measurement found the +decode row's output and state bitwise equal in every step composition in that +configuration, and the recurrent golden's target would then have to change. The strict profile uses FP32 recurrent state. BF16 state remains a differential experiment. Full checkpoint prefill, response replay, optimizer updates and From 1d693124e6ba5d0ab24e2ecb6371e460d74e0c76 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:33:35 +0000 Subject: [PATCH 31/44] fix(gdn): address the C6 review (R3 F1-4, F6, F10-14, F17, F20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - F1: the runner's usage example writes its JSON to $TMPDIR, outside the checkout. Redirecting into the repo left an untracked file that made the next run report git_dirty: true. - F2: label the §5 mixed-step measurements and the §7 startup failure as reported observations with no checked-in runner, hence not acceptance evidence, by the same rule that withdrew the earlier tables. - F3: mark every bound in the check file as a regression bound against the provider (the drift bound: against the golden), not gate evidence, and give the contract's forward_accuracy/by_op_class/reduction row (tolerance_contract.json: fp32 1e-4/1e-4, bf16 5e-2/2e-2) for scale, in the check file and in design note §4. - F4: run tests/test_gdn_state_contract.py in ci.yml's unit-tests list. - F6: rename the note to docs/design/rfc428-c6-gdn-recurrent-replay.md, retitle it "RFC #428 C6", and write the backward item as "RFC #428 C7", so neither collides with upstream's WS1 C6 (#272) / C7 (#273). The runner's docstring was the only in-repo reference to the old path. - F10: _RECURRENT_BOUNDS is a module constant in the check file; the parametrization and the runner both read it instead of copying values. - F11: derive H, HV, K, V from qwen3_next_workload.FINGERPRINT; _CONV_DIM is _PACKED_DIM. - F12: runner provenance records rl_engine.__file__ and the FLA_USE_FAST_OPS / TRITON_DEFAULT_FP_FUSION environment. - F13: qwen3-next-provider-gpu.yml gets a header (purpose, security), concurrency, a fork-pr-notice job, and a check after the build that rl_engine._C was loaded from inside $GITHUB_WORKSPACE. - F14: the NULL_BLOCK_ID comment says <= 0 is the recurrent provider's semantics and vLLM's conv provider skips only == 0. - F17: drop two index checks in GatedDeltaRuleRecurrentStepOp that _validate_state_indices repeats. The length check could not fire; the 1-D check could, so a 2-D index now raises the validator's message ("state indices must be 1-D with one entry per sequence") instead. - F20: SPDX header on tests/test_gdn_state_contract.py. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/qwen3-next-provider-gpu.yml | 46 ++++++++++++++++++- ...y.md => rfc428-c6-gdn-recurrent-replay.md} | 23 +++++++--- .../ops/pytorch/linear_attn/causal_conv1d.py | 2 +- .../pytorch/linear_attn/gated_delta_rule.py | 9 ++-- scripts/ws1_gdn_provider_agreement.py | 22 ++++++--- tests/check_gdn_recurrent_golden.py | 46 +++++++++++++------ tests/test_gdn_state_contract.py | 3 ++ 8 files changed, 117 insertions(+), 36 deletions(-) rename docs/design/{ws1-c6-428-gdn-recurrent-replay.md => rfc428-c6-gdn-recurrent-replay.md} (92%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f8c900b3..814a4ae22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: run: | python -m pytest rl_engine/tests/test_dispatch.py -v PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest tests/test_attention_correctness.py -q -rs - python -m pytest tests/test_forward_invariance.py tests/test_tolerance_contract.py tests/test_ws1_workload.py tests/test_gradient_invariance.py tests/test_elementwise_inventory.py tests/test_four_judgment_matrix.py tests/test_op_checks.py tests/test_operator_inputs.py tests/test_profiler.py tests/test_kv_consistency.py tests/test_ws1_qwen3_dense.py tests/test_ws1_chain_integration.py tests/test_qwen3_next_norm.py -q + python -m pytest tests/test_forward_invariance.py tests/test_tolerance_contract.py tests/test_ws1_workload.py tests/test_gradient_invariance.py tests/test_elementwise_inventory.py tests/test_four_judgment_matrix.py tests/test_op_checks.py tests/test_operator_inputs.py tests/test_profiler.py tests/test_kv_consistency.py tests/test_ws1_qwen3_dense.py tests/test_ws1_chain_integration.py tests/test_qwen3_next_norm.py tests/test_gdn_state_contract.py -q - name: Run Cross-Configuration Contract Tests (CPU-safe) run: | diff --git a/.github/workflows/qwen3-next-provider-gpu.yml b/.github/workflows/qwen3-next-provider-gpu.yml index b53c73441..146818019 100644 --- a/.github/workflows/qwen3-next-provider-gpu.yml +++ b/.github/workflows/qwen3-next-provider-gpu.yml @@ -1,3 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 +# RFC #428 Qwen3-Next provider comparisons: the C1/C3/C4 norm providers and the C6 +# GDN decode-step goldens, each checked against the real vLLM 0.30.0 provider. +# +# These check_ files import vLLM, so the default pytest collection never runs them +# (tests/test_framework_operator_integrations.py asserts vLLM is not imported). This +# workflow runs them in their own process on a self-hosted runner that a maintainer +# registers with the labels below and keeps on the audited runtime (CUDA 13, +# torch 2.13.0+cu130, vLLM 0.30.0). Without such a runner the job queues rather than +# reporting a false pass. +# +# Security: do not use pull_request_target. Fork PRs never reach the self-hosted +# runner; a maintainer dispatches the reviewed commit from a trusted branch. + name: Qwen3-Next-provider-GPU on: @@ -11,13 +25,26 @@ on: - ".github/workflows/qwen3-next-provider-gpu.yml" workflow_dispatch: +concurrency: + group: qwen3-next-provider-gpu-${{ github.ref }} + cancel-in-progress: false + permissions: contents: read jobs: + fork-pr-notice: + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + steps: + - name: Report required trusted execution + run: | + echo "Fork code does not run on the self-hosted Qwen3-Next provider runner." + echo "A maintainer must dispatch this workflow from a trusted upstream branch." + echo "source_repository=${{ github.event.pull_request.head.repo.full_name }}" + echo "source_sha=${{ github.event.pull_request.head.sha }}" + provider: - # This label must identify a maintained CUDA 13 / vLLM 0.30.0 environment. - # Never run untrusted fork PR code on the self-hosted runner. 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] timeout-minutes: 30 @@ -35,6 +62,21 @@ jobs: PY - name: Build the extension against that runtime run: python3 setup.py build_ext --inplace + - name: Require the extension built from this checkout + # A stale editable install elsewhere on the runner would otherwise satisfy the + # import and test some other tree's _C. + run: | + python3 - <<'PY' + import os + from pathlib import Path + + from rl_engine import _C + + workspace = Path(os.environ["GITHUB_WORKSPACE"]).resolve() + loaded = Path(_C.__file__).resolve() + assert workspace in loaded.parents, f"rl_engine._C loaded from {loaded}, outside {workspace}" + print("rl_engine._C:", loaded) + PY - name: Check pinned providers in their own process run: | python3 -m pytest -q tests/check_qwen3_next_norm_providers.py tests/check_gdn_recurrent_golden.py diff --git a/docs/design/ws1-c6-428-gdn-recurrent-replay.md b/docs/design/rfc428-c6-gdn-recurrent-replay.md similarity index 92% rename from docs/design/ws1-c6-428-gdn-recurrent-replay.md rename to docs/design/rfc428-c6-gdn-recurrent-replay.md index 21abba4aa..718e5ab65 100644 --- a/docs/design/ws1-c6-428-gdn-recurrent-replay.md +++ b/docs/design/rfc428-c6-gdn-recurrent-replay.md @@ -1,4 +1,4 @@ -# WS1 C6 — Qwen3-Next Gated DeltaNet recurrent replay +# RFC #428 C6 — Qwen3-Next Gated DeltaNet recurrent replay Design notes for RFC #428 work item C6 (GDN recurrent response replay) on the CUDA track. Measured on 2× B200 (sm_100), torch 2.13.0+cu130, vllm 0.30.0, @@ -119,7 +119,12 @@ The same choice is why the golden is not bitwise against the kernel's reduction. Qwen3-Next dims (H=16, HV=32, K=V=128), bf16 I/O, `use_qk_l2norm_in_kernel=True`, random inputs, B ∈ {1, 4, 17, 64}, one seed per batch. Bounds asserted by -`tests/check_gdn_recurrent_golden.py`: +`tests/check_gdn_recurrent_golden.py` (`_RECURRENT_BOUNDS`). They, and every other +bound in that file, are **regression bounds against the provider, not gate evidence**; +they do not go through `resolve_tolerance`. For scale, the gate contract's +`forward_accuracy/by_op_class/reduction` row in +`rl_engine/kernels/gtest/tolerance_contract.json` is atol = rtol = 1e-4 for float32 +and atol = 5e-2, rtol = 2e-2 for bfloat16. | | max\|diff\| out | max\|diff\| state | |---|---|---| @@ -153,7 +158,7 @@ were not limited to one BF16 ULP. The CPU tests in `tests/test_gdn_state_contrac cover bias order and bf16 product rounding, and `test_conv_provider_preserves_bf16_product_cancellation` checks the provider on a constructed cancellation input. Provider comparisons limit mismatches to 32 elements -on the checked fixtures. This is not a bitwise claim. +on the checked fixtures (a regression bound, not gate evidence). This is not a bitwise claim. Measured with the same runner and commit: the rolled conv state is bitwise equal in all 8 (batch, cache dtype) cases. Output elements differing, with an fp32 cache: @@ -192,6 +197,10 @@ cached decode rows through `causal_conv1d_fn` and `fused_sigmoid_gating_delta_rule_update` instead, so in those steps the recurrent golden does not target what rollout runs. +*The numbers in the rest of this subsection are a reported observation with no +checked-in runner in this repository. By the same rule that withdrew the tables above, +they are not acceptance evidence.* + A measurement on B200 drove the real `GDNAttentionMetadataBuilder.build()` and `_forward_core` of **one standalone layer** built from the checkpoint's `config.json`. Its limits: parameters and cache were synthetic (no weights loaded); it @@ -213,7 +222,7 @@ does not carry over to real weights. At TP4 per-rank head counts it appears in t same step's output. This is a provider-side batch-composition dependence: whether a prefill shares the step changes a decode row's state, and at TP4 per-rank head counts its output too, which no golden can fix. The measurement was made outside this change -and is not yet published; treat it as a reported observation. +and is not yet published. vllm-project/vllm#49827, open and unmerged, routes mixed-step *recurrent* decodes through the packed kernel too. Its two commits, applied to 0.30.0, closed the gap to 0 @@ -245,7 +254,7 @@ Not covered, with reasons: | deferred | why | |---|---| | Speculative decode / MTP | RFC #428 §2.2 excludes speculative decoding from the first claim. Qwen3-Next's MTP head ships in the checkpoint (`mtp.*`, loaded by `model_executor/models/qwen3_next_mtp.py`) and is full attention (`qwen3_next_mtp.py:90-92`). Enabling it changes the target model's GDN path in steps that carry draft tokens (§1); `fused_gdn_decode_post_conv_mtp` is unreachable for Qwen3-Next | -| Backward for the recurrent step | RFC #428 §2.2 item 4: backward need not match rollout, only be correct for the replayed forward. RFC §9.1 makes it a separate work item, **C7** (GDN backward/recompute adapter including prompt-state gradient). `supports_backward=false` here; `_softplus`'s NaN-gradient fix (`08969ac`) adds no backward claim | +| Backward for the recurrent step | RFC #428 §2.2 item 4: backward need not match rollout, only be correct for the replayed forward. RFC §9.1 makes it a separate work item, **RFC #428 C7** (GDN backward/recompute adapter including prompt-state gradient). `supports_backward=false` here; `_softplus`'s NaN-gradient fix (`08969ac`) adds no backward claim | | Provider bridge / registry entry | the golden should survive a drift sweep against a real checkpoint first | | Paged block allocation policy | the ABI is mirrored; the allocator is not modelled | | TP sharding of `A_log` / `dt_bias` | single card only | @@ -264,4 +273,6 @@ synchronization and checkpoint reload. No operator-level test closes these gates The 2026-09-30 real-checkpoint startup attempt with vLLM 0.30.0 failed before inference: `VLLM batch_invariant mode is not supported for GDN_ATTN`. A shared -provider integration must resolve this; disabling the check is not L2 evidence. +provider integration must resolve this; disabling the check is not L2 evidence. *This +failure is a reported observation: the attempt's log and launcher are not checked into +this repository, so it is not acceptance evidence either.* diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py b/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py index fb171af49..99036f6de 100644 --- a/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py +++ b/rl_engine/kernels/ops/pytorch/linear_attn/causal_conv1d.py @@ -36,7 +36,7 @@ class CausalConv1dUpdateOp: ``conv_state`` ``[num_blocks, dim, W-1]`` paged; ``dim_first`` layout ``weight`` ``[dim, W]`` ``bias`` ``[dim]`` or ``None`` - ``indices`` ``[B]`` ``<= 0`` skips + ``indices`` ``[B]`` ``<= 0`` skips (vLLM: ``== 0``) ============== ========================== ========================== Returns ``(out, conv_state)``; the state is updated out of place. diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py index e2af3a0c3..27708f319 100644 --- a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py +++ b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py @@ -42,7 +42,10 @@ __all__ = ["GatedDeltaRuleRecurrentStepOp", "NULL_BLOCK_ID", "SOFTPLUS_THRESHOLD"] -#: Paged-state sentinel: a sequence pointing here is skipped. +#: Paged-state sentinel. Both goldens skip a row whose index is ``<= NULL_BLOCK_ID``: +#: that is the recurrent provider's semantics (``state_idx <= 0``). vLLM's conv +#: provider skips only ``== null_block_id`` (0), so a negative index is a real index +#: there; see the design note's NULL_BLOCK_ID table. NULL_BLOCK_ID = 0 #: Above this, softplus is the identity (matches the kernel's constexpr). @@ -240,8 +243,6 @@ def _step( raise ValueError(f"mixed_qkv must be 2-D [B, D], got {tuple(mixed_qkv.shape)}") if state.dim() != 4: raise ValueError(f"state must be 4-D [num_blocks, HV, V, K], got {tuple(state.shape)}") - if ssm_state_indices.dim() != 1: - raise ValueError("ssm_state_indices must be 1-D [B] for packed decode") batch = mixed_qkv.shape[0] hv, v_dim, k_dim = state.shape[-3:] @@ -278,8 +279,6 @@ def _step( f"mixed_qkv last dim must be {expected} (q|k|v packed), " f"got {mixed_qkv.shape[1]}" ) - if ssm_state_indices.shape[0] != batch: - raise ValueError("ssm_state_indices must have one entry per sequence") group = hv // heads qkv32 = mixed_qkv.float() diff --git a/scripts/ws1_gdn_provider_agreement.py b/scripts/ws1_gdn_provider_agreement.py index 539fa9979..83727fdb5 100644 --- a/scripts/ws1_gdn_provider_agreement.py +++ b/scripts/ws1_gdn_provider_agreement.py @@ -4,9 +4,9 @@ """Measure how closely the GDN decode-step goldens track vLLM's providers (RFC #428 C6). -``tests/check_gdn_recurrent_golden.py`` asserts loose bounds; this runner prints the -measured values behind them, so the numbers quoted in -``docs/design/ws1-c6-428-gdn-recurrent-replay.md`` can be reproduced. Inputs and seeds +``tests/check_gdn_recurrent_golden.py`` asserts loose regression bounds; this runner +prints the measured values behind them, so the numbers quoted in +``docs/design/rfc428-c6-gdn-recurrent-replay.md`` can be reproduced. Inputs and seeds are the check file's own helpers, imported from it rather than copied. Three measurements, emitted as one JSON document on stdout: @@ -29,9 +29,11 @@ fusion off it passes ``--fmad=false`` to ptxas. A PTX with no ``fma.rn.f32`` can still run FMAs. ``fusion_check`` reports whether each arm compiled what it claims to. -Requires CUDA and vLLM 0.30.0. Run it from a clean checkout so ``git_dirty`` is false: +Requires CUDA and vLLM 0.30.0. Run it from a clean checkout so ``git_dirty`` is false, +and write the result outside the checkout: an untracked file inside it would itself +make the next run report ``git_dirty: true``. - python scripts/ws1_gdn_provider_agreement.py > gdn_provider_agreement.json + python scripts/ws1_gdn_provider_agreement.py > "${TMPDIR:-/tmp}/gdn_provider_agreement.json" """ from __future__ import annotations @@ -57,6 +59,9 @@ _CHECK_FILE = REPO_ROOT / "tests" / "check_gdn_recurrent_golden.py" _STATE_DTYPES = {"fp32": torch.float32, "bf16": torch.bfloat16} _INT_VIEW = {2: torch.int16, 4: torch.int32} +# FLA_USE_FAST_OPS swaps the kernel's exp/log for fast_expf/fast_logf; +# TRITON_DEFAULT_FP_FUSION decides whether ptxas may contract mul/add. +_PROVENANCE_ENV = ("FLA_USE_FAST_OPS", "TRITON_DEFAULT_FP_FUSION") # Opcode patterns. PTX: a rounding-qualified op (".rn") may not be contracted by ptxas; # the plain form may. SASS: count opcode tokens, including modifiers such as FFMA.FTZ. _PTX_OPS = { @@ -102,11 +107,16 @@ def _provenance() -> dict[str, Any]: import triton import vllm + import rl_engine + status = _git("status", "--porcelain") return { # None means "unknown" (no git, or not a checkout), never "clean". "git_commit": _git("rev-parse", "HEAD"), "git_dirty": None if status is None else bool(status), + # Which tree was imported, and the env knobs that change the provider's code. + "rl_engine_file": rl_engine.__file__, + "env": {key: os.environ.get(key) for key in _PROVENANCE_ENV}, "python": platform.python_version(), "torch": torch.__version__, "triton": triton.__version__, @@ -118,7 +128,7 @@ def _provenance() -> dict[str, Any]: def _measure_recurrent(check, batches: list[int]) -> list[dict[str, Any]]: - bounds = {"fp32": (1e-3, 1e-5), "bf16": (1e-3, 5e-3)} # check file :117, :120 + bounds = {name: check._RECURRENT_BOUNDS[dtype] for name, dtype in _STATE_DTYPES.items()} rows = [] for batch in batches: for name, state_dtype in _STATE_DTYPES.items(): diff --git a/tests/check_gdn_recurrent_golden.py b/tests/check_gdn_recurrent_golden.py index 103098fc5..7815939c0 100644 --- a/tests/check_gdn_recurrent_golden.py +++ b/tests/check_gdn_recurrent_golden.py @@ -37,13 +37,33 @@ pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required") from rl_engine.kernels.ops.pytorch.linear_attn import GatedDeltaRuleRecurrentStepOp # noqa: E402 +from rl_engine.testing.qwen3_next_workload import FINGERPRINT # noqa: E402 -# Qwen3-Next-80B-A3B-Instruct: linear_num_key_heads / linear_num_value_heads / -# linear_key_head_dim / linear_value_head_dim. -_H, _HV, _K, _V = 16, 32, 128, 128 +# Qwen3-Next-80B-A3B-Instruct dims, from the pinned checkpoint fingerprint. +_H = FINGERPRINT["linear_num_key_heads"] +_HV = FINGERPRINT["linear_num_value_heads"] +_K = FINGERPRINT["linear_key_head_dim"] +_V = FINGERPRINT["linear_value_head_dim"] _SCALE = _K**-0.5 _PACKED_DIM = _H * _K * 2 + _HV * _V +# Every bound in this file is a regression bound against the vLLM provider (or, for +# the bf16-state drift test, against the golden itself), set from measurements. None +# is gate evidence, and none goes through rl_engine.kernels.gtest.tolerance's +# resolve_tolerance. For scale, the gate contract's +# forward_accuracy/by_op_class/reduction row in +# rl_engine/kernels/gtest/tolerance_contract.json is atol = rtol = 1e-4 for float32 +# and atol = 5e-2, rtol = 2e-2 for bfloat16. +# +# (max|d out|, max|d state|) per recurrent-state dtype. Read by +# scripts/ws1_gdn_provider_agreement.py, so the runner reports against the same values. +_RECURRENT_BOUNDS = { + # fp32 state: agreement is at fp32-ULP scale. + torch.float32: (1e-3, 1e-5), + # bf16 state: the store rounds every token, so the state carries a bf16 ULP. + torch.bfloat16: (1e-3, 5e-3), +} + def _vllm_step(): pytest.importorskip("vllm", reason="vLLM is required to compare against the provider") @@ -110,17 +130,9 @@ def _run_golden(inp): # 1. Against the provider # --------------------------------------------------------------------------- # @pytest.mark.parametrize("batch", [1, 4, 17, 64]) -@pytest.mark.parametrize( - "state_dtype, out_atol, state_atol", - [ - # fp32 state: agreement is at fp32-ULP scale. - (torch.float32, 1e-3, 1e-5), - # bf16 state: the store rounds every token, so the state carries a - # bf16 ULP. This is the configuration knob behind the drift probe. - (torch.bfloat16, 1e-3, 5e-3), - ], -) -def test_golden_matches_packed_decode_provider(batch, state_dtype, out_atol, state_atol): +@pytest.mark.parametrize("state_dtype", list(_RECURRENT_BOUNDS), ids=["fp32", "bf16"]) +def test_golden_matches_packed_decode_provider(batch, state_dtype): + out_atol, state_atol = _RECURRENT_BOUNDS[state_dtype] inp = _inputs(batch, batch + 2, state_dtype, torch.bfloat16, seed=batch) out_ref, state_ref = _run_provider(inp) out_got, state_got = _run_golden(inp) @@ -207,6 +219,7 @@ def test_bf16_state_rounding_stays_within_fixed_fixture_bound(): assert drift[0] > 0.0, "a bf16 state must round on the very first store" # Preserve the original regression bound for this fixed fixture. + # Regression bound for this fixture, golden against golden; not gate evidence. assert max(drift) < 0.05, f"relative state drift reached {max(drift):.3e}" # The back half must not be materially worse than the front half. assert max(drift[steps // 2 :]) < 2.0 * max(drift[: steps // 2]) + 1e-3 @@ -215,7 +228,8 @@ def test_bf16_state_rounding_stays_within_fixed_fixture_bound(): # --------------------------------------------------------------------------- # # 4. The causal-conv1d state update, the other half of a decode step # --------------------------------------------------------------------------- # -_CONV_DIM, _CONV_WIDTH = 8192, 4 # Qwen3-Next: key_dim*2 + value_dim, linear_conv_kernel_dim +# The conv runs over the packed q|k|v channels; Qwen3-Next's linear_conv_kernel_dim is 4. +_CONV_DIM, _CONV_WIDTH = _PACKED_DIM, 4 def _conv_update(): @@ -294,6 +308,7 @@ def test_conv_output_matches_provider_with_fp32_cache(batch): inp = _conv_inputs(batch, torch.float32, seed=batch) (out_ref, _), (out_got, _) = _run_conv_pair(inp) mismatch = int((out_got.float().view(torch.int32) != out_ref.float().view(torch.int32)).sum()) + # Regression bounds against the provider, not gate evidence (see the module note). assert mismatch <= 32, f"{mismatch} of {out_got.numel()} elements differ" assert (out_got.float() - out_ref.float()).abs().max().item() <= 1e-2 @@ -303,6 +318,7 @@ def test_conv_output_bf16_cache_matches_rounded_product_path(batch): """Product rounding is reproduced; activation ULP residuals remain allowed.""" inp = _conv_inputs(batch, torch.bfloat16, seed=batch) (out_ref, _), (out_got, _) = _run_conv_pair(inp) + # Regression bounds against the provider, not gate evidence (see the module note). assert (out_got.float() - out_ref.float()).abs().max().item() <= 7e-2 assert int((out_got != out_ref).sum()) <= 32 diff --git a/tests/test_gdn_state_contract.py b/tests/test_gdn_state_contract.py index 70255bb0d..7af1e083d 100644 --- a/tests/test_gdn_state_contract.py +++ b/tests/test_gdn_state_contract.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + """Provider-independent cache contracts, collected in the ordinary CPU suite.""" import pytest From c0ab21c20abacb0627e07df45fb49c5d6ebd3fc2 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:35:23 +0000 Subject: [PATCH 32/44] docs(gdn): refresh the design note's in-repo line references f128870 added three comment lines above NULL_BLOCK_ID and an SPDX header to tests/test_gdn_state_contract.py, and the gated merge (c4b451c) rewrote the docstring of the env-default test in tests/check_qwen3_next_norm_providers.py, so four references pointed at the wrong lines: - gated_delta_rule.py:107-109 -> :110-112 (_softplus body) - gated_delta_rule.py:73-87 -> :76-90 (_chunked_sum) - tests/test_gdn_state_contract.py:51-56 -> :54-59 (inactive-index test) - check_qwen3_next_norm_providers.py:124-131 -> :129-142 (env-default assert) The golden causal_conv1d.py:167-177 reference is still right. vLLM-package references are unaffected. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- docs/design/rfc428-c6-gdn-recurrent-replay.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/design/rfc428-c6-gdn-recurrent-replay.md b/docs/design/rfc428-c6-gdn-recurrent-replay.md index 718e5ab65..4b9ca767e 100644 --- a/docs/design/rfc428-c6-gdn-recurrent-replay.md +++ b/docs/design/rfc428-c6-gdn-recurrent-replay.md @@ -48,7 +48,7 @@ explicitly set to `cuda` (`:516-519`). As a consequence the fused `_can_use_fused_gdn_mtp_decode` requires `gdn_decode_kernel == "cuda"` (`:1834`). This section is read from the source. -`tests/check_qwen3_next_norm_providers.py` asserts both env defaults (`:124-131`). For +`tests/check_qwen3_next_norm_providers.py` asserts both env defaults (`:129-142`). For `VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE` that is a useful guard: a vLLM bump that flips it fails loudly. For `VLLM_GDN_DECODE_KERNEL` it guards nothing for Qwen3-Next, because the interleaved layout, not the default, decides the kernel. Neither @@ -75,7 +75,7 @@ Where the golden is **not** a transcription: - softplus: the kernel computes `tl.log(1.0 + tl.exp(x))` (`fused_recurrent.py:327`); the golden computes `torch.log1p(torch.exp(safe))`, where `safe` is `x` on the taken - branch (`gated_delta_rule.py:107-109`). These round differently. + branch (`gated_delta_rule.py:110-112`). These round differently. - The kernel's `exp`/`log` become `fast_expf`/`fast_logf` when `FLA_USE_FAST_OPS=1` (`third_party/flash_linear_attention/ops/op.py:16-25`). The golden models the default. @@ -106,10 +106,10 @@ Mirrored rather than reinvented: | state block | not touched | not touched | not touched | A negative conv index is therefore inactive in the golden -(`tests/test_gdn_state_contract.py:51-56`) but a real, out-of-range index to the conv +(`tests/test_gdn_state_contract.py:54-59`) but a real, out-of-range index to the conv provider. -Contractions use `_chunked_sum` (`gated_delta_rule.py:73-87`): fixed 32-wide chunks, +Contractions use `_chunked_sum` (`gated_delta_rule.py:76-90`): fixed 32-wide chunks, so the reduction shape per row does not depend on the batch size, rather than `torch.matmul`, whose reduction order is unspecified. That is the argument for the L1 claim; L1 itself is established empirically by `test_golden_is_batch_invariant`. From f8d93993d809ded2dc65391ac745bf5a32ca4da2 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:22:32 +0000 Subject: [PATCH 33/44] feat(gdn): localise the fp32-cache conv mismatches in the C6 runner The runner reported that a few conv outputs differ from vLLM with an fp32 cache but not why. Add two sections, reusing its inputs, provenance and the fusion-off child process: - conv_silu (fusion on): the conv inputs widened to fp32, fp32 cache and output. Compares the pre-activation values (activation off), the SiLU outputs with a ULP histogram, and four Triton SiLU formulations -- x / (1 + tl.exp(-x)) as in the provider, and the variants with div_rn and/or libdevice.exp -- applied to the golden's pre-activation values, each compared bitwise with both sides. - conv_noact_bf16 (fusion on and off): the no-activation conv with a bf16 output against the golden, whether it equals its fp32-output twin rounded to bf16, each mismatch's magnitude and size in bf16 ULPs, and the compiled variants of both specializations. The two run in separate loops so the variants each one adds can be told apart. The PTX counts gain the packed f32x2 forms, ex2.approx and div.full.f32. fusion_check now covers every variant an arm compiles. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- scripts/ws1_gdn_provider_agreement.py | 234 +++++++++++++++++++++++--- 1 file changed, 212 insertions(+), 22 deletions(-) diff --git a/scripts/ws1_gdn_provider_agreement.py b/scripts/ws1_gdn_provider_agreement.py index 83727fdb5..715cd5627 100644 --- a/scripts/ws1_gdn_provider_agreement.py +++ b/scripts/ws1_gdn_provider_agreement.py @@ -9,7 +9,7 @@ ``docs/design/rfc428-c6-gdn-recurrent-replay.md`` can be reproduced. Inputs and seeds are the check file's own helpers, imported from it rather than copied. -Three measurements, emitted as one JSON document on stdout: +Measurements, emitted as one JSON document on stdout: * ``recurrent``: provider vs golden for the packed recurrent decode, per (batch, state dtype): max|d out|, max|d state| and bitwise mismatch counts. @@ -18,6 +18,13 @@ * ``conv`` again with Triton FP fusion disabled, plus ``conv_kernels``: per compiled variant of the provider's conv-update kernel, its ``enable_fp_fusion`` option and instruction counts from the PTX and from the SASS. +* ``conv_silu`` localises the fp32-cache conv mismatches, on the same inputs widened to + fp32 with an fp32 output: the pre-activation comparison (activation off), the SiLU + comparison with its ULP histogram, and four Triton SiLU variants applied to the + golden's pre-activation values, each compared bitwise with both sides. +* ``conv_noact_bf16``: the no-activation, bf16-output specialization against the golden, + whether it equals the fp32-output run rounded to bf16, each mismatch's size, and the + compiled variants of both specializations; with fusion on and off. The fusion-off arm runs in a child process with ``TRITON_DEFAULT_FP_FUSION=0`` and a private ``TRITON_CACHE_DIR``. Flipping ``triton.knobs.language.default_fp_fusion`` @@ -41,6 +48,7 @@ import argparse import importlib.util import json +import math import os import platform import re @@ -70,8 +78,16 @@ "add_f32": r"\badd\.f32\b", "mul_rn_f32": r"\bmul\.rn\.f32\b", "add_rn_f32": r"\badd\.rn\.f32\b", + # Packed pairs (sm_100), approximate exp and division. + "fma_rn_f32x2": r"\bfma\.rn\.f32x2\b", + "mul_f32x2": r"\bmul(\.rn)?\.f32x2\b", + "add_f32x2": r"\badd(\.rn)?\.f32x2\b", + "ex2_approx": r"\bex2\.approx", + "div_full_f32": r"\bdiv\.full\.f32\b", } _SASS_OPS = {"FFMA": r"\bFFMA[\w.]*", "FMUL": r"\bFMUL[\w.]*", "FADD": r"\bFADD[\w.]*"} +# Triton SiLU variants for conv_silu, by MODE of _triton_silu's kernel. +_SILU_VARIANTS = ("div_exp", "divrn_exp", "div_libexp", "divrn_libexp") def _load_check_module(): @@ -181,34 +197,203 @@ def _count(patterns: dict[str, str], text: str) -> dict[str, int]: return {name: len(re.findall(pattern, text)) for name, pattern in patterns.items()} -def _conv_kernel_variants() -> list[dict[str, Any]] | dict[str, str]: - """Options and instruction counts per compiled variant of the conv-update kernel.""" - try: - from vllm.model_executor.layers.mamba.ops import causal_conv1d as conv_module +def _compiled_conv_variants() -> dict[tuple[str, Any], Any]: + """Every compiled variant of vLLM's conv-update kernel in this process, by cache key.""" + from vllm.model_executor.layers.mamba.ops import causal_conv1d as conv_module + + kernel = conv_module._causal_conv1d_update_kernel + while not hasattr(kernel, "device_caches") and hasattr(kernel, "fn"): + kernel = kernel.fn + return { + (str(device), key): compiled + for device, cache in kernel.device_caches.items() + for key, compiled in cache[0].items() + } + - kernel = conv_module._causal_conv1d_update_kernel - while not hasattr(kernel, "device_caches") and hasattr(kernel, "fn"): - kernel = kernel.fn +def _conv_kernel_variants(keys: Any = None) -> list[dict[str, Any]] | dict[str, str]: + """Options and instruction counts per compiled variant (only ``keys``, if given).""" + try: variants = [] - for device, cache in kernel.device_caches.items(): - for compiled in cache[0].values(): - row: dict[str, Any] = { - "device": str(device), - "enable_fp_fusion": getattr(compiled.metadata, "enable_fp_fusion", None), - "ptx": _count(_PTX_OPS, compiled.asm["ptx"]), - } - try: - row["sass"] = _count(_SASS_OPS, compiled.asm["sass"]) - except Exception as exc: # needs cuobjdump; report, do not fail - row["sass"] = {"unavailable": f"{type(exc).__name__}: {exc}"} - variants.append(row) + for (device, key), compiled in _compiled_conv_variants().items(): + if keys is not None and (device, key) not in keys: + continue + row: dict[str, Any] = { + "device": device, + "enable_fp_fusion": getattr(compiled.metadata, "enable_fp_fusion", None), + "ptx": _count(_PTX_OPS, compiled.asm["ptx"]), + } + try: + row["sass"] = _count(_SASS_OPS, compiled.asm["sass"]) + except Exception as exc: # needs cuobjdump; report, do not fail + row["sass"] = {"unavailable": f"{type(exc).__name__}: {exc}"} + variants.append(row) return variants except Exception as exc: # introspection of Triton internals; report, do not fail return {"unavailable": f"{type(exc).__name__}: {exc}"} +def _new_variant_keys(before: set[Any]) -> set[Any] | None: + try: + return set(_compiled_conv_variants()) - before + except Exception: # introspection of Triton internals; report, do not fail + return None + + +def _bf16_ulps(diff: float, ref: float) -> float | None: + """``diff`` in units of one bf16 ULP at the magnitude of ``ref`` (None at zero).""" + if ref == 0.0: + return None if diff else 0.0 + return diff / 2.0 ** (math.floor(math.log2(abs(ref))) - 7) + + +def _conv_noact_bf16(check, batches: list[int]) -> dict[str, Any]: + """The no-activation conv with a bf16 output, against its fp32-output twin. + + The provider casts x to the fp32 cache dtype before launching, so the two runs + compute the same thing and differ only in the output dtype, i.e. in which Triton + specialization runs. Each specialization runs in its own loop so that the compiled + variants it adds can be told apart. + """ + import triton + + fp_fusion = bool(triton.knobs.language.default_fp_fusion) + try: + before = set(_compiled_conv_variants()) + except Exception: # introspection of Triton internals; report, do not fail + before = set() + bf16_out = {} + for batch in batches: + inp = check._conv_inputs(batch, torch.float32, seed=batch) + bf16_out[batch] = check._run_conv_pair(inp, activation=None) + bf16_keys = _new_variant_keys(before) + fp32_out = {} + for batch in batches: + inp = check._conv_inputs(batch, torch.float32, seed=batch) + fp32_out[batch] = check._run_conv_pair(dict(inp, x=inp["x"].float()), activation=None) + fp32_keys = _new_variant_keys(before | (bf16_keys or set())) + + rows = [] + for batch in batches: + (b_ref, _), (b_got, _) = bf16_out[batch] + (d_ref, _), (d_got, _) = fp32_out[batch] + differ = torch.nonzero(b_ref.view(torch.int16) != b_got.view(torch.int16)).tolist() + mismatches = [] + for r, ch in differ: + provider, golden = b_ref[r, ch].item(), b_got[r, ch].item() + mismatches.append( + { + "abs_out": abs(golden), + "abs_diff": abs(provider - golden), + "bf16_ulps": _bf16_ulps(abs(provider - golden), golden), + } + ) + rows.append( + { + "batch": batch, + "fp_fusion": fp_fusion, + "out_mismatch_elements": len(differ), + "out_elements": b_ref.numel(), + "provider_eq_rne_of_fp32_out": torch.equal(b_ref, d_ref.to(torch.bfloat16)), + "fp32_out_mismatch_elements": _bit_mismatches(d_got, d_ref), + "mismatches": mismatches, + } + ) + return { + "batches": rows, + "bf16_out_kernels": _conv_kernel_variants(bf16_keys) if bf16_keys is not None else None, + "fp32_out_kernels": _conv_kernel_variants(fp32_keys) if fp32_keys is not None else None, + } + + +def _triton_silu(): + """``apply(x, mode)``: one of the four SiLU formulations, evaluated by Triton.""" + import triton + import triton.language as tl + from triton.language.extra import libdevice + + @triton.jit + def silu(x_ptr, y_ptr, n, MODE: tl.constexpr, BLOCK: tl.constexpr): + offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) + mask = offs < n + x = tl.load(x_ptr + offs, mask=mask, other=0.0) + if MODE == 0: # the provider's source: x / (1 + tl.exp(-x)) + y = x / (1 + tl.exp(-x)) + elif MODE == 1: + y = tl.math.div_rn(x, 1 + tl.exp(-x)) + elif MODE == 2: + y = x / (1 + libdevice.exp(-x)) + else: + y = tl.math.div_rn(x, 1 + libdevice.exp(-x)) + tl.store(y_ptr + offs, y, mask=mask) + + def apply(x: torch.Tensor, mode: int) -> torch.Tensor: + flat = x.contiguous().view(-1) + y = torch.empty_like(flat) + silu[(triton.cdiv(flat.numel(), 1024),)](flat, y, flat.numel(), MODE=mode, BLOCK=1024) + return y.view_as(x) + + return apply + + +def _ulp_histogram(a: torch.Tensor, b: torch.Tensor) -> dict[str, int]: + """Differing fp32 elements by bit-pattern distance.""" + d = (a.contiguous().view(torch.int32).long() - b.contiguous().view(torch.int32).long()).abs() + d = d[d != 0] + return { + "1": int((d == 1).sum()), + "2": int((d == 2).sum()), + "3-4": int(((d == 3) | (d == 4)).sum()), + ">4": int((d > 4).sum()), + } + + +def _conv_silu(check, batches: list[int]) -> list[dict[str, Any]]: + """Where the fp32-cache conv mismatches come from, on fp32 inputs and outputs.""" + silu = _triton_silu() + rows = [] + for batch in batches: + inp = check._conv_inputs(batch, torch.float32, seed=batch) + inp32 = dict(inp, x=inp["x"].float()) # the same values, kept in fp32 throughout + (pre_ref, _), (pre_got, _) = check._run_conv_pair(inp32, activation=None) + (out_ref, _), (out_got, _) = check._run_conv_pair(inp32) + variants = {name: silu(pre_got, mode) for mode, name in enumerate(_SILU_VARIANTS)} + rows.append( + { + "batch": batch, + "elements": out_ref.numel(), + "preactivation_mismatch_elements": _bit_mismatches(pre_got, pre_ref), + "silu_mismatch_elements": _bit_mismatches(out_got, out_ref), + "silu_ulp_histogram": _ulp_histogram(out_got, out_ref), + # Applied to the golden's pre-activation values. + "triton_silu_vs_provider": { + k: _bit_mismatches(v, out_ref) for k, v in variants.items() + }, + "triton_silu_vs_golden": { + k: _bit_mismatches(v, out_got) for k, v in variants.items() + }, + } + ) + return rows + + def _conv_report(check, batches: list[int]) -> dict[str, Any]: - return {"conv": _measure_conv(check, batches), "conv_kernels": _conv_kernel_variants()} + conv = _measure_conv(check, batches) + kernels = _conv_kernel_variants() # before the no-activation runs add their own + return { + "conv": conv, + "conv_kernels": kernels, + "conv_noact_bf16": _conv_noact_bf16(check, batches), + } + + +def _arm_variants(arm: dict[str, Any]) -> list[Any] | None: + """Every compiled variant an arm reports, or None if any listing is unavailable.""" + noact = arm["conv_noact_bf16"] + lists = [arm["conv_kernels"], noact["bf16_out_kernels"], noact["fp32_out_kernels"]] + if not all(isinstance(x, list) for x in lists): + return None + return [v for x in lists for v in x] def _conv_report_without_fusion(batches: str) -> dict[str, Any]: @@ -284,17 +469,22 @@ def main() -> int: "batches": batches, "recurrent": "bf16 I/O, use_qk_l2norm_in_kernel=True, num_blocks=batch+2", "conv": "bias=True, activation=silu, dim_first=True, W=4, dim=8192", + "conv_silu": "the conv inputs with x widened to fp32; fp32 cache and output", + "conv_noact_bf16": "the conv inputs with activation=None; fp32 cache", }, "recurrent": _measure_recurrent(check, batches), } fused = _conv_report(check, batches) report["conv"] = fused["conv"] report["conv_kernels"] = {"fusion_on": fused["conv_kernels"]} + report["conv_noact_bf16"] = {"fusion_on": fused["conv_noact_bf16"]} + report["conv_silu"] = _conv_silu(check, batches) if not args.skip_fusion_off: unfused = _conv_report_without_fusion(args.batches) report["conv"] += unfused["conv"] report["conv_kernels"]["fusion_off"] = unfused["conv_kernels"] - report["fusion_check"] = _fusion_check(fused["conv_kernels"], unfused["conv_kernels"]) + report["conv_noact_bf16"]["fusion_off"] = unfused["conv_noact_bf16"] + report["fusion_check"] = _fusion_check(_arm_variants(fused), _arm_variants(unfused)) print(json.dumps(report, indent=2, sort_keys=True)) return 0 From 610f1b7d8f8ba807ad4a0714c588a2a80a48eaed Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:36:59 +0000 Subject: [PATCH 34/44] docs(gdn): state the conv mismatch causes, measured by the runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §4 said FP contraction was ruled out and the cause of the few fp32-cache conv mismatches was not determined. The runner's conv_silu and conv_noact_bf16 sections (bb89750) now settle both, on B200: - With SiLU (Qwen3-Next's configuration): everything before the activation matches bitwise; the mismatches come from Triton lowering the activation's exp and division to ex2.approx and div.full.f32, where the golden matches libdevice exp with IEEE division. Triton's x / (1 + tl.exp(-x)) on the golden's pre-activation values reproduces the provider bitwise. No FFMA, and fusion off changes nothing, so "contraction ruled out" now holds for this path only. - Without an activation and with a bf16 output: that specialization does contract the tap multiply-adds (fma.rn.f32x2 / FFMA); its fp32-output twin does not and matches bitwise. 1/0/4/14 mismatches, mostly near-cancelling outputs; 0 with TRITON_DEFAULT_FP_FUSION=0. Not on Qwen3-Next's decode path. The recurrent provider's Triton exp/sigmoid is noted as open. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- docs/design/rfc428-c6-gdn-recurrent-replay.md | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/docs/design/rfc428-c6-gdn-recurrent-replay.md b/docs/design/rfc428-c6-gdn-recurrent-replay.md index 4b9ca767e..31475af42 100644 --- a/docs/design/rfc428-c6-gdn-recurrent-replay.md +++ b/docs/design/rfc428-c6-gdn-recurrent-replay.md @@ -166,23 +166,48 @@ all 8 (batch, cache dtype) cases. Output elements differing, with an fp32 cache: 5 / 524288 (B=64, max|diff| 3.91e-03). With a bf16 cache: 0 at B=1, 4 and 17, and 3 / 524288 at B=64 (max|diff| 1.56e-02). -**FP contraction is ruled out** as the cause of the few differing fp32-cache -elements; what does cause them is **not determined**. The candidate was that, with -Triton's default `enable_fp_fusion=True`, ptxas contracts the provider's -`acc += matrix_x * matrix_w` (vLLM `causal_conv1d.py:1061`) into an `FFMA`, whereas the -golden rounds the product first. The runner tests this by rerunning the comparison in -a child process compiled with fusion off. On B200 at commit `cf6be1a`: - -- the fusion-off arm took effect: all 6 compiled variants carry - `enable_fp_fusion=False`, and their PTX has only `.rn`-qualified f32 `mul`/`add`, - which ptxas may not contract; -- the mismatch counts and max\|diff\| are **identical** with fusion on and off - (0, 0, 1, 5 for B = 1, 4, 17, 64); -- neither arm's SASS contains `FFMA`. - -The conclusion rests on the second point. The opcode counts are supporting evidence -only: each variant's PTX has 2 f32 multiplies, too few to be the four tap products, so -the counters do not show which instructions compute those products. +**Why a few fp32-cache outputs differ.** There are two mechanisms, both on the provider +side, and which one applies depends on the Triton specialization. Measured by +`scripts/ws1_gdn_provider_agreement.py` (`conv_silu`, `conv_noact_bf16`) on B200 at commit +`bb89750`: + +*With SiLU -- Qwen3-Next's configuration and the check file's -- the activation's +implementation.* + +- With the activation off and the output kept in fp32, provider and golden agree bitwise + on every element at B = 1, 4, 17 and 64: the cast to the cache dtype, the bias start, + the product rounding and the tap order all match. +- Both sides compute `acc / (1 + exp(-acc))` (vLLM `causal_conv1d.py:1085`), but Triton + lowers the exp and the fp32 division to `ex2.approx` and `div.full.f32`, while the + golden's PyTorch result equals the same expression with `libdevice.exp` and IEEE + division (`div_rn`). Applying Triton's `x / (1 + tl.exp(-x))` to the golden's + pre-activation values reproduces the provider's fp32 output bitwise; replacing only the + exp, or only the division, reproduces neither side. +- In fp32 about 38% of outputs differ: 86% of those by 1 ULP, 12% by 2, 3% by 3 or 4, + 0.2% by more. Only values on opposite sides of a bf16 rounding midpoint survive the bf16 + store: 0, 0, 1 and 5 elements, each one bf16 ULP apart. +- FP contraction plays no part on this path. These specializations compute the tap + products as packed `mul.f32x2` with separate adds and contain no `FFMA`; recompiling + with `TRITON_DEFAULT_FP_FUSION=0` leaves every count unchanged. + +*Without an activation and with a bf16 output -- FP contraction.* + +- With fusion at its default, this specialization contracts each tap's multiply-add + (`fma.rn.f32x2` in the PTX, `FFMA` in the SASS). Its fp32-output twin computes the same + values (the provider casts x to the fp32 cache dtype first) but is not contracted, and + matches the golden bitwise. +- The bf16 outputs differ in 1, 0, 4 and 14 elements (max |diff| 3.9e-3). 18 of the 19 + have |out| < 0.11, where the taps nearly cancel; there the gap can span several bf16 + ULPs (up to 96 at |out| ≈ 2.4e-7) while staying at most 2.4e-7 in absolute terms. +- With `TRITON_DEFAULT_FP_FUSION=0` this specialization compiles to separate multiplies + and adds, and the mismatches drop to 0. The golden is self-consistent across output + dtypes. Qwen3-Next's conv applies SiLU, so this specialization is not on its decode + path. + +*Open:* the recurrent provider computes `exp(g)` and `sigmoid(b)` with Triton (`tl.exp` +via the vendored FLA `op.py`), the golden with PyTorch; the same kind of difference may +account for part of the recurrent output mismatches in the table above. Not +investigated. ## 5. Decode versus chunked prefill From 395fef838ad8cd2ba5ccd376b6151c2225bfd753 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:16:59 +0000 Subject: [PATCH 35/44] ci(qwen3-next): reconcile the WS1 gtest script with the stacked gated-norm branch Carried over from the former merges of the gated-norm branch into this one; the final tree is the same as before the re-stack. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- ci/run_ws1_gtest.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ci/run_ws1_gtest.sh b/ci/run_ws1_gtest.sh index 03b77173d..e6d8e0dc0 100755 --- a/ci/run_ws1_gtest.sh +++ b/ci/run_ws1_gtest.sh @@ -77,11 +77,12 @@ print("[ws1-gtest] C8 gate passed") PY echo "[ws1-gtest] Qwen3-Next C3/C4 norms (operator scope)" -for op in qwen3_next_rms_norm rms_norm_gated; do - for gate in forward gradient; do - "$PY" "scripts/check_${gate}_invariance.py" \ - --manifest rl_engine/testing/qwen3_next_norm_manifest.json \ - --op "$op" --candidate cuda --backend-profile cuda_bf16 \ - --hidden 2048 --head-dim 128 - done -done +QWEN3_NEXT_NORM_MANIFEST=rl_engine/testing/qwen3_next_norm_manifest.json +"$PY" scripts/check_forward_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op qwen3_next_rms_norm --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 +"$PY" scripts/check_gradient_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op qwen3_next_rms_norm --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 +"$PY" scripts/check_forward_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op rms_norm_gated --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 +"$PY" scripts/check_gradient_invariance.py --manifest "$QWEN3_NEXT_NORM_MANIFEST" \ + --op rms_norm_gated --candidate cuda --backend-profile cuda_bf16 --hidden 2048 --head-dim 128 From 0b41350536a738c76af5b20c5a409b166289f276 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Sun, 4 Oct 2026 11:46:39 +0000 Subject: [PATCH 36/44] fix(gdn): keep _chunked_sum fixed-order for widths not divisible by 32 The non-multiple-of-32 branch fell back to a whole-axis sum, whose reduction order is unspecified, which contradicts the batch-invariance contract the helper exists to enforce. Zero-pad the last dim to the next chunk boundary instead and reduce in the same fixed chunk order for every width. Qwen3-Next uses K=128 so the shipped goldens are unaffected; a CPU test covers an odd width. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .../ops/pytorch/linear_attn/gated_delta_rule.py | 6 +++++- tests/test_gdn_state_contract.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py index 27708f319..181bfca5d 100644 --- a/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py +++ b/rl_engine/kernels/ops/pytorch/linear_attn/gated_delta_rule.py @@ -84,7 +84,11 @@ def _chunked_sum(x: torch.Tensor) -> torch.Tensor: """ tail = x.shape[-1] if tail % _REDUCTION_CHUNK != 0: - return x.sum(dim=-1) + # Zero-pad to the next chunk boundary instead of falling back to an + # unpinned ``sum``: exact zeros add nothing, so the result is the same + # fixed chunk order for every width. + x = torch.nn.functional.pad(x, (0, _REDUCTION_CHUNK - tail % _REDUCTION_CHUNK)) + tail = x.shape[-1] return ( x.reshape(*x.shape[:-1], tail // _REDUCTION_CHUNK, _REDUCTION_CHUNK).sum(dim=-1).sum(dim=-1) ) diff --git a/tests/test_gdn_state_contract.py b/tests/test_gdn_state_contract.py index 7af1e083d..a154efd35 100644 --- a/tests/test_gdn_state_contract.py +++ b/tests/test_gdn_state_contract.py @@ -107,3 +107,18 @@ def test_softplus_large_branch_has_finite_gradient(): x = torch.tensor([100.0, 21.0, 0.0], requires_grad=True) _softplus(x).sum().backward() assert torch.equal(x.grad, torch.tensor([1.0, 1.0, 0.5])) + + +def test_chunked_sum_keeps_fixed_order_for_non_multiple_of_32_width(): + from rl_engine.kernels.ops.pytorch.linear_attn.gated_delta_rule import ( + _REDUCTION_CHUNK, + _chunked_sum, + ) + + torch.manual_seed(0) + x = torch.randn(3, 5, 2 * _REDUCTION_CHUNK + 7) + padded = torch.nn.functional.pad(x, (0, _REDUCTION_CHUNK - 7)) + chunked = padded.reshape(3, 5, 3, _REDUCTION_CHUNK).sum(dim=-1).sum(dim=-1) + assert torch.equal(_chunked_sum(x), chunked) + # The reduction must not depend on the leading shape either. + assert torch.equal(_chunked_sum(x[:1]), _chunked_sum(x)[:1]) From bec7e7adc01763c26dd7325689ed5f3b4ca06239 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:35:28 +0000 Subject: [PATCH 37/44] fix(norm): reject stale RMSNorm bindings and enable GPU coverage Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- .github/workflows/ws1-gtest-gpu.yml | 6 +++ ci/run_ws1_gtest.sh | 1 + csrc/ops.cpp | 2 + rl_engine/_C.pyi | 3 ++ rl_engine/kernels/ops/cuda/norm/rmsnorm.py | 29 ++++++------ tests/test_qwen3_next_norm.py | 6 ++- tests/test_rms_norm.py | 51 +++++++++++++++++++--- 7 files changed, 77 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ws1-gtest-gpu.yml b/.github/workflows/ws1-gtest-gpu.yml index 646d80bdf..f48d62382 100644 --- a/.github/workflows/ws1-gtest-gpu.yml +++ b/.github/workflows/ws1-gtest-gpu.yml @@ -12,6 +12,8 @@ on: pull_request: branches: [ main ] paths: + - "csrc/cuda/rmsnorm.cu" + - "csrc/ops.cpp" - "rl_engine/kernels/gtest/**" - "rl_engine/kernels/ops/**" - "rl_engine/testing/**" @@ -20,6 +22,7 @@ on: - "scripts/check_gradient_invariance.py" - "scripts/ws1_candidate_evidence.py" - "tests/test_ws1_*.py" + - "tests/test_qwen3_next_norm.py" - "tests/test_forward_invariance.py" - "tests/test_gradient_invariance.py" - "tests/test_four_judgment_matrix.py" @@ -30,6 +33,8 @@ on: push: branches: [ main ] paths: + - "csrc/cuda/rmsnorm.cu" + - "csrc/ops.cpp" - "rl_engine/kernels/gtest/**" - "rl_engine/kernels/ops/**" - "rl_engine/testing/**" @@ -38,6 +43,7 @@ on: - "scripts/check_gradient_invariance.py" - "scripts/ws1_candidate_evidence.py" - "tests/test_ws1_*.py" + - "tests/test_qwen3_next_norm.py" - "tests/test_forward_invariance.py" - "tests/test_gradient_invariance.py" - "tests/test_four_judgment_matrix.py" diff --git a/ci/run_ws1_gtest.sh b/ci/run_ws1_gtest.sh index 0e9b172cc..e4d8a36fa 100755 --- a/ci/run_ws1_gtest.sh +++ b/ci/run_ws1_gtest.sh @@ -19,6 +19,7 @@ echo "[ws1-gtest] interpreter=$PY out=$OUT" "$PY" -m pytest -q \ tests/test_ws1_gtest_gpu.py \ + tests/test_qwen3_next_norm.py \ tests/test_triton_batch_invariant_attention.py \ tests/test_four_judgment_matrix.py \ tests/test_ws1_candidate_evidence.py \ diff --git a/csrc/ops.cpp b/csrc/ops.cpp index 4e786e5b3..7ae1523f6 100644 --- a/csrc/ops.cpp +++ b/csrc/ops.cpp @@ -716,6 +716,8 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { &det_gemm_db_transposed, "Batch-invariant deterministic GEMM backward in canonical [N,K] layout"); // registry RMSNorm + // API version 2 includes weight_offset in both entry points. + m.attr("rmsnorm_api_version") = 2; m.def("rmsnorm_forward", &rmsnorm_forward, "Batch-invariant RMSNorm forward CUDA", py::arg("x"), py::arg("weight"), py::arg("eps"), py::arg("weight_offset") = 0.0); diff --git a/rl_engine/_C.pyi b/rl_engine/_C.pyi index b911feaf7..225f997b6 100644 --- a/rl_engine/_C.pyi +++ b/rl_engine/_C.pyi @@ -256,6 +256,9 @@ def swiglu_backward( gate: torch.Tensor, up: torch.Tensor, ) -> list[torch.Tensor]: ... + +rmsnorm_api_version: int + def rmsnorm_forward( x: torch.Tensor, weight: torch.Tensor, diff --git a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py index af9a31358..a0da2ddd4 100644 --- a/rl_engine/kernels/ops/cuda/norm/rmsnorm.py +++ b/rl_engine/kernels/ops/cuda/norm/rmsnorm.py @@ -4,9 +4,11 @@ from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE from rl_engine.kernels.ops.vjp_fp32 import reduce_rows_fp32, rmsnorm_dweight_rows_fp32 +_RMSNORM_API_VERSION = 2 -def _require_cuda_symbols(what: str, *names: str) -> None: - """Raise when the compiled kernels backing ``what`` are missing. + +def _require_cuda_rmsnorm() -> None: + """Raise when the compiled RMSNorm bindings are missing or incompatible. The registry treats a backend whose construction raises as unavailable and falls through to the next candidate, so calling this from ``__init__`` is @@ -15,13 +17,21 @@ def _require_cuda_symbols(what: str, *names: str) -> None: activation ops. """ if not _EXT_AVAILABLE or _C is None: - raise RuntimeError(f"{what} requires the compiled rl_engine._C extension.") + raise RuntimeError("CUDA RMSNorm requires the compiled rl_engine._C extension.") + names = ("rmsnorm_forward", "rmsnorm_backward_dx") missing = [name for name in names if not hasattr(_C, name)] if missing: raise RuntimeError( - f"{what} symbols ({', '.join(missing)}) are not compiled into _C. " + f"CUDA RMSNorm symbols ({', '.join(missing)}) are not compiled into _C. " "Rebuild the extension with csrc/cuda/rmsnorm.cu." ) + api_version = getattr(_C, "rmsnorm_api_version", None) + if api_version != _RMSNORM_API_VERSION: + raise RuntimeError( + f"CUDA RMSNorm requires rl_engine._C RMSNorm API version {_RMSNORM_API_VERSION} " + f"(loaded {api_version!r}). Rebuild the extension with csrc/cuda/rmsnorm.cu " + "for weight_offset support." + ) class RMSNormCuda(torch.autograd.Function): @@ -46,6 +56,7 @@ def forward(ctx, x, weight, mask=None, eps=1e-6, weight_offset=0.0): Output: y: [T, H] """ + _require_cuda_rmsnorm() assert x.is_cuda, "x must be CUDA tensor" assert weight.is_cuda, "weight must be CUDA tensor" assert x.is_contiguous(), "x must be contiguous" @@ -53,10 +64,6 @@ def forward(ctx, x, weight, mask=None, eps=1e-6, weight_offset=0.0): assert x.dim() == 2, "x must be [T, H]" assert weight.dim() == 1, "weight must be [H]" assert x.shape[1] == weight.shape[0], "hidden size mismatch" - assert _EXT_AVAILABLE and hasattr( - _C, "rmsnorm_forward" - ), "RMSNorm CUDA extension is unavailable. Please rebuild with rmsnorm.cu." - if mask is None: mask = torch.ones((x.shape[0],), device=x.device, dtype=torch.bool) else: @@ -124,11 +131,7 @@ class RMSNormCudaOp: backward_impl = "cuda_rmsnorm_dx_declared_fp32_rowfold_dw" def __init__(self) -> None: - _require_cuda_symbols( - "CUDA RMSNorm", - "rmsnorm_forward", - "rmsnorm_backward_dx", - ) + _require_cuda_rmsnorm() #: Added to the weight in fp32 inside the kernel. Subclasses override it; #: 0.0 is the plain convention. diff --git a/tests/test_qwen3_next_norm.py b/tests/test_qwen3_next_norm.py index a62d8410f..d45670a7d 100644 --- a/tests/test_qwen3_next_norm.py +++ b/tests/test_qwen3_next_norm.py @@ -355,7 +355,11 @@ def test_gated_shape_mismatch_raises(): try: from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE - _CUDA_RMSNORM = _EXT_AVAILABLE and hasattr(_C, "rmsnorm_forward") + _CUDA_RMSNORM = ( + _EXT_AVAILABLE + and getattr(_C, "rmsnorm_api_version", None) == 2 + and all(hasattr(_C, name) for name in ("rmsnorm_forward", "rmsnorm_backward_dx")) + ) except ImportError: # pragma: no cover _CUDA_RMSNORM = False diff --git a/tests/test_rms_norm.py b/tests/test_rms_norm.py index b17a3f302..ec3046593 100644 --- a/tests/test_rms_norm.py +++ b/tests/test_rms_norm.py @@ -17,10 +17,11 @@ try: from rl_engine.kernels.ops.base import _C, _EXT_AVAILABLE - # The same two symbols RMSNormCudaOp.__init__ requires; a build that has them - # dispatches to the CUDA op, so the dispatch test must agree with that guard. - _HAS_CUDA_RMSNORM = _EXT_AVAILABLE and all( - hasattr(_C, name) for name in ("rmsnorm_forward", "rmsnorm_backward_dx") + # Keep dispatch expectations aligned with the constructor's capability guard. + _HAS_CUDA_RMSNORM = ( + _EXT_AVAILABLE + and getattr(_C, "rmsnorm_api_version", None) == 2 + and all(hasattr(_C, name) for name in ("rmsnorm_forward", "rmsnorm_backward_dx")) ) except ImportError: # pragma: no cover - import can fail when the extension is not built. _HAS_CUDA_RMSNORM = False @@ -263,7 +264,7 @@ def test_cuda_op_construction_fails_when_symbols_missing(monkeypatch): from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm class _WithoutRMSNorm: # a built extension that lacks the rmsnorm symbols - pass + rmsnorm_api_version = 2 monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", True) monkeypatch.setattr(cuda_rmsnorm, "_C", _WithoutRMSNorm()) @@ -291,12 +292,43 @@ def test_registry_falls_back_when_required_symbol_is_missing(monkeypatch, missin symbols = {name: object() for name in ("rmsnorm_forward", "rmsnorm_backward_dx")} del symbols[missing] + symbols["rmsnorm_api_version"] = 2 monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", True) monkeypatch.setattr(cuda_rmsnorm, "_C", SimpleNamespace(**symbols)) assert isinstance(KernelRegistry().get_op("rms_norm", device="cuda"), NativeRMSNormOp) -def test_registry_cuda_requires_only_used_symbols_and_cpu_stays_native(monkeypatch): +@pytest.mark.parametrize("api_version", [None, 1, 3]) +def test_cuda_rejects_incompatible_rmsnorm_api_before_dispatch(monkeypatch, api_version): + from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm + from rl_engine.kernels.registry import KernelRegistry + + class _LegacyRMSNorm: + def rmsnorm_forward(self, x, weight, eps): + pytest.fail("an incompatible extension must not be invoked") + + def rmsnorm_backward_dx(self, dy, x, weight, rstd): + pytest.fail("an incompatible extension must not be invoked") + + extension = _LegacyRMSNorm() + if api_version is not None: + extension.rmsnorm_api_version = api_version + monkeypatch.setattr(cuda_rmsnorm, "_EXT_AVAILABLE", True) + monkeypatch.setattr(cuda_rmsnorm, "_C", extension) + + for op_type in (cuda_rmsnorm.RMSNormCudaOp, cuda_rmsnorm.Qwen3NextRMSNormCudaOp): + with pytest.raises(RuntimeError, match="RMSNorm API version 2.*Rebuild"): + op_type() + + registry_op = KernelRegistry().get_op("rms_norm", device="cuda") + assert isinstance(registry_op, NativeRMSNormOp) + x, weight = torch.ones(2, 8), torch.ones(8) + assert torch.isfinite(registry_op(x, weight)).all() + with pytest.raises(RuntimeError, match="RMSNorm API version 2.*Rebuild"): + rmsnorm_cuda(x, weight, weight_offset=1.0) + + +def test_registry_cuda_requires_current_api_and_only_used_symbols(monkeypatch): from types import SimpleNamespace from rl_engine.kernels.ops.cuda.norm import rmsnorm as cuda_rmsnorm @@ -306,10 +338,15 @@ def test_registry_cuda_requires_only_used_symbols_and_cpu_stays_native(monkeypat monkeypatch.setattr( cuda_rmsnorm, "_C", - SimpleNamespace(rmsnorm_forward=object(), rmsnorm_backward_dx=object()), + SimpleNamespace( + rmsnorm_api_version=2, + rmsnorm_forward=object(), + rmsnorm_backward_dx=object(), + ), ) registry = KernelRegistry() assert isinstance(registry.get_op("rms_norm", device="cuda"), RMSNormCudaOp) + cuda_rmsnorm.Qwen3NextRMSNormCudaOp() assert isinstance(registry.get_op("rms_norm", device="cpu"), NativeRMSNormOp) From 53767ab89110fe53a70d4c6abcfff626b10f37b5 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:27:59 +0000 Subject: [PATCH 38/44] feat(scripts): Qwen3-Next norm evidence runner vs existing implementations Compares the zero-centred RMSNorm CUDA op with the PyTorch reference, transformers, vLLM and FlashInfer (optional providers are skipped when absent): error vs an FP64 golden, row invariance (256 rows alone vs a 4096-row batch, three seeds, bitwise) and forward/backward latency. The plot script renders the report as one figure per op. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- scripts/plot_qwen3_next_norm_evidence.py | 117 ++++++++++ scripts/qwen3_next_norm_evidence.py | 276 +++++++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 scripts/plot_qwen3_next_norm_evidence.py create mode 100644 scripts/qwen3_next_norm_evidence.py diff --git a/scripts/plot_qwen3_next_norm_evidence.py b/scripts/plot_qwen3_next_norm_evidence.py new file mode 100644 index 000000000..54dab1a67 --- /dev/null +++ b/scripts/plot_qwen3_next_norm_evidence.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Plot a report from scripts/qwen3_next_norm_evidence.py: one 2x2 figure per op. + + python scripts/plot_qwen3_next_norm_evidence.py report.json # figure[-].png beside it +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 + +COLORS = ["#2a6fdb", "#7a7a7a", "#e07b39", "#3aa676", "#9b59b6", "#c0392b"] + + +def _short(name: str) -> str: + return name.replace(" (forward only)", "*") + + +def plot_op(op: str, data: dict, title: str, out: Path) -> None: + names = list(data["row_invariance"]) + color = {n: COLORS[i % len(COLORS)] for i, n in enumerate(names)} + fig, axes = plt.subplots(2, 2, figsize=(13, 9)) + fig.suptitle(title, fontsize=13) + + for ax, key, label in ( + (axes[0, 0], "forward_us", "forward latency"), + (axes[0, 1], "backward_us", "backward latency (training-capable only)"), + ): + for name in names: + rows = sorted(int(r) for r in data["latency"][name]) + ys = [data["latency"][name][str(r)].get(key) for r in rows] + if all(y is None for y in ys): + continue + ax.plot(rows, ys, "o-", color=color[name], label=_short(name)) + ax.set_xscale("log", base=2) + ax.set_yscale("log") + ax.set_xlabel("rows") + ax.set_ylabel("µs (median)") + ax.set_title(label) + ax.grid(True, which="both", alpha=0.3) + ax.legend(fontsize=8) + + ax = axes[1, 0] + acc = data["accuracy"][max(data["accuracy"], key=int)] + metrics = [ + ("forward_max_abs", "forward max |err|"), + ("dx_max_abs_over_absmax", "dx max |err| / max|dx|"), + ("dweight_max_abs_over_absmax", "dweight max |err| / max|dw|"), + ] + width = 0.8 / len(names) + for i, name in enumerate(names): + vals = [acc[name].get(m) for m, _ in metrics] + xs = [j + (i - (len(names) - 1) / 2) * width for j in range(len(metrics))] + ax.bar( + [x for x, v in zip(xs, vals) if v is not None], + [v for v in vals if v is not None], + width, + color=color[name], + label=_short(name), + ) + ax.set_xticks(range(len(metrics)), [label for _, label in metrics], fontsize=9) + ax.set_yscale("log") + ax.set_title(f"error vs FP64 golden ({max(data['accuracy'], key=int)} rows, BF16)") + ax.grid(True, axis="y", alpha=0.3) + ax.legend(fontsize=8) + + ax = axes[1, 1] + bi = data["row_invariance"] + checked = next(iter(bi.values()))["rows_checked"] + ys = range(len(names)) + fwd = [bi[n]["forward_rows_differing"] for n in names] + dx = [ + bi[n]["dx_rows_differing"] if bi[n]["dx_rows_differing"] is not None else 0 for n in names + ] + ax.barh([y - 0.2 for y in ys], fwd, 0.4, color="#2a6fdb", label="forward") + ax.barh([y + 0.2 for y in ys], dx, 0.4, color="#e07b39", label="dx") + for y, n, f, d in zip(ys, names, fwd, dx): + no_bwd = bi[n]["dx_rows_differing"] is None + ax.text(max(f, d) + 0.2, y, f"{f} / {'n/a' if no_bwd else d}", va="center", fontsize=8) + ax.set_yticks(list(ys), [_short(n) for n in names], fontsize=8) + ax.invert_yaxis() + ax.set_xlabel(f"rows differing (of {checked}; row alone vs inside a batch, bitwise)") + ax.set_title("row invariance (0 = batch-invariant)") + ax.legend(fontsize=8) + ax.grid(True, axis="x", alpha=0.3) + + fig.text(0.01, 0.005, "* forward only (no backward)", fontsize=8) + fig.tight_layout(rect=(0, 0.02, 1, 0.96)) + fig.savefig(out, dpi=130) + print(f"wrote {out}") + + +def main() -> None: + path = Path(sys.argv[1]) + report = json.loads(path.read_text()) + env = report["environment"] + ops = report["ops"] + for op, data in ops.items(): + name = "figure.png" if len(ops) == 1 else f"figure-{op}.png" + title = ( + f"Qwen3-Next {op.replace('_', ' ')} — {env['gpu']}, hidden {report['hidden']}, " + f"BF16, commit {report['git_commit'][:7]}" + ) + plot_op(op, data, title, path.parent / name) + + +if __name__ == "__main__": + main() diff --git a/scripts/qwen3_next_norm_evidence.py b/scripts/qwen3_next_norm_evidence.py new file mode 100644 index 000000000..27a063df6 --- /dev/null +++ b/scripts/qwen3_next_norm_evidence.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 RL-Kernel Contributors + +"""Accuracy, row invariance and latency of the Qwen3-Next norms vs existing implementations. + +Writes one JSON report (RFC #428, reuse rule of #420): every candidate is compared with +an FP64 golden, checked for row invariance (a row computed alone vs inside a batch, +bitwise), and timed. Optional providers (transformers, vLLM, FlashInfer) are skipped +when they are not installed; the report says which ran. + + python scripts/qwen3_next_norm_evidence.py --out report.json + python scripts/plot_qwen3_next_norm_evidence.py report.json +""" + +from __future__ import annotations + +import argparse +import json +import platform +import statistics +import subprocess +import sys +from pathlib import Path +from typing import Any, Callable + +import torch + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT)) + +from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormCudaOp # noqa: E402 +from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormOp # noqa: E402 + +EPS = 1e-6 +HIDDEN = 2048 # Qwen3-Next hidden size (decoder and final norms) +TIMED_ROWS = (1024, 4096, 16384, 65536) + + +# --------------------------------------------------------------------------- # +# Candidates: name -> (forward fn(x, w) -> y, has_backward) +# --------------------------------------------------------------------------- # + + +def zero_centred_candidates() -> dict[str, dict[str, Any]]: + cands: dict[str, dict[str, Any]] = { + "rl-kernel CUDA": {"fn": Qwen3NextRMSNormCudaOp(), "backward": True}, + "rl-kernel PyTorch reference": {"fn": Qwen3NextRMSNormOp(), "backward": True}, + } + try: + from transformers.models.qwen3_next.modeling_qwen3_next import Qwen3NextRMSNorm + + module = Qwen3NextRMSNorm(HIDDEN, eps=EPS).cuda() + + def hf(x, w, module=module): + return torch.func.functional_call(module, {"weight": w}, (x,)) + + cands["transformers Qwen3NextRMSNorm"] = {"fn": hf, "backward": True} + except ImportError: + pass + try: + from vllm.config import VllmConfig, set_current_vllm_config + + with set_current_vllm_config(VllmConfig()): + from vllm.model_executor.layers.layernorm import GemmaRMSNorm + + gemma = GemmaRMSNorm(HIDDEN, eps=EPS).cuda() + + def vllm_fwd(x, w, gemma=gemma): + gemma.weight.data = w.detach() + return gemma.forward_cuda(x) + + cands["vLLM GemmaRMSNorm (forward only)"] = {"fn": vllm_fwd, "backward": False} + except ImportError: + pass + try: + import flashinfer + + cands["FlashInfer gemma_rmsnorm (forward only)"] = { + "fn": lambda x, w: flashinfer.norm.gemma_rmsnorm(x, w, EPS), + "backward": False, + } + except ImportError: + pass + return cands + + +def zero_centred_golden(x: torch.Tensor, w: torch.Tensor) -> torch.Tensor: + x64 = x.double() + return x64 * torch.rsqrt(x64.square().mean(-1, keepdim=True) + EPS) * (1.0 + w.double()) + + +# --------------------------------------------------------------------------- # +# Measurements +# --------------------------------------------------------------------------- # + + +def _inputs(rows: int, seed: int, dtype=torch.bfloat16): + g = torch.Generator(device="cuda").manual_seed(seed) + x = (torch.randn(rows, HIDDEN, device="cuda", generator=g) * 2).to(dtype) + w = (torch.randn(HIDDEN, device="cuda", generator=g) * 0.1).to(dtype) + up = torch.randn(rows, HIDDEN, device="cuda", generator=g).to(dtype) + return x, w, up + + +def _grads(fn, x, w, up, dtype=None): + xl = (x if dtype is None else x.to(dtype)).detach().clone().requires_grad_(True) + wl = (w if dtype is None else w.to(dtype)).detach().clone().requires_grad_(True) + out = fn(xl, wl) + out.backward(up if dtype is None else up.to(dtype)) + return out.detach(), xl.grad, wl.grad + + +def accuracy(cands, golden, rows: int, seed: int) -> dict[str, Any]: + x, w, up = _inputs(rows, seed) + ref_out, ref_dx, ref_dw = _grads(golden, x, w, up, torch.float64) + result = {} + for name, c in cands.items(): + entry: dict[str, Any] = {} + if c["backward"]: + out, dx, dw = _grads(c["fn"], x, w, up) + for key, got, ref in (("dx", dx, ref_dx), ("dweight", dw, ref_dw)): + err = (got.double() - ref).abs().max().item() + entry[f"{key}_max_abs_over_absmax"] = err / ref.abs().max().item() + else: + with torch.no_grad(): + out = c["fn"](x, w) + err = (out.double() - ref_out).abs() + entry["forward_max_abs"] = err.max().item() + entry["forward_correctly_rounded_fraction"] = ( + (out == ref_out.to(out.dtype)).float().mean().item() + ) + result[name] = entry + return result + + +def row_invariance(cands, seeds=(3, 4, 5), rows: int = 4096, step: int = 16) -> dict[str, Any]: + """256 rows computed alone vs the same rows inside a batch, bitwise.""" + + result = {} + for name, c in cands.items(): + fwd_bad = dx_bad = checked = 0 + for seed in seeds: + x, w, up = _inputs(rows, seed) + if c["backward"]: + full_out, full_dx, _ = _grads(c["fn"], x, w, up) + else: + with torch.no_grad(): + full_out = c["fn"](x, w) + for i in range(0, rows, step): + sl = slice(i, i + 1) + if c["backward"]: + out, dx, _ = _grads(c["fn"], x[sl], w, up[sl]) + dx_bad += not torch.equal(dx[0], full_dx[i]) + else: + with torch.no_grad(): + out = c["fn"](x[sl], w) + fwd_bad += not torch.equal(out[0], full_out[i]) + checked += 1 + result[name] = { + "rows_checked": checked, + "forward_rows_differing": fwd_bad, + "dx_rows_differing": dx_bad if c["backward"] else None, + "batch_rows": rows, + "seeds": list(seeds), + } + return result + + +def _time_us(fn: Callable[[], Any], warmup: int = 10, iters: int = 50) -> float: + for _ in range(warmup): + fn() + torch.cuda.synchronize() + samples = [] + for _ in range(iters): + start, end = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True) + start.record() + fn() + end.record() + end.synchronize() + samples.append(start.elapsed_time(end) * 1e3) + return statistics.median(samples) + + +def latency(cands, rows_list=TIMED_ROWS) -> dict[str, Any]: + result: dict[str, Any] = {name: {} for name in cands} + for rows in rows_list: + x, w, up = _inputs(rows, seed=11) + for name, c in cands.items(): + row: dict[str, float] = {} + with torch.no_grad(): + row["forward_us"] = _time_us(lambda f=c["fn"]: f(x, w)) + if c["backward"]: + xl = x.detach().clone().requires_grad_(True) + wl = w.detach().clone().requires_grad_(True) + out = c["fn"](xl, wl) + row["backward_us"] = _time_us( + lambda o=out: torch.autograd.grad(o, (xl, wl), up, retain_graph=True) + ) + result[name][str(rows)] = row + return result + + +# --------------------------------------------------------------------------- # + + +def _git(*args: str) -> str: + try: + return subprocess.check_output(["git", *args], cwd=REPO_ROOT, text=True).strip() + except (OSError, subprocess.CalledProcessError): + return "" + + +def environment() -> dict[str, Any]: + env = { + "gpu": torch.cuda.get_device_name(), + "capability": list(torch.cuda.get_device_capability()), + "torch": torch.__version__, + "cuda": torch.version.cuda, + "python": platform.python_version(), + } + for mod in ("transformers", "vllm", "flashinfer"): + try: + env[mod] = __import__(mod).__version__ + except ImportError: + env[mod] = None + return env + + +def run_op(name: str, cands, golden) -> dict[str, Any]: + print(f"[{name}] candidates: {', '.join(cands)}", flush=True) + report = { + "accuracy": {str(r): accuracy(cands, golden, r, seed=r) for r in (257, 4096)}, + "row_invariance": row_invariance(cands), + "latency": latency(cands), + } + for cand, entry in report["row_invariance"].items(): + print(f" BI {cand}: {entry}", flush=True) + return report + + +def build_report(ops: dict[str, tuple[Callable, Callable]]) -> dict[str, Any]: + torch.backends.cuda.matmul.allow_tf32 = False + report = { + "kind": "qwen3_next_norm_evidence", + "rfc": "RL-Align/RL-Kernel#428", + "git_commit": _git("rev-parse", "HEAD") or "unknown", + "git_dirty": bool(_git("status", "--porcelain", "--untracked-files=no")), + "environment": environment(), + "hidden": HIDDEN, + "eps": EPS, + "dtype": "bfloat16", + "ops": {}, + } + for name, (make_cands, golden) in ops.items(): + report["ops"][name] = run_op(name, make_cands(), golden) + return report + + +OPS = {"zero_centred_rmsnorm": (zero_centred_candidates, zero_centred_golden)} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + if not torch.cuda.is_available(): + raise SystemExit("needs a CUDA device") + report = build_report(OPS) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(report, indent=2) + "\n") + print(f"wrote {args.out} (commit {report['git_commit'][:7]}, dirty={report['git_dirty']})") + + +if __name__ == "__main__": + main() From a441ff972d52cf56b180ea00758404e986f55013 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:10:56 +0000 Subject: [PATCH 39/44] feat(scripts): extend the Qwen3-Next norm evidence runner to the gated RMSNorm Adds the gated op (hidden 128, rows = tokens x heads) against the PyTorch reference, transformers' cast-first Qwen3NextRMSNormGated and vLLM's RMSNormGated, with an FP64 golden in vLLM's convention. Row inputs are now generic, so the gate is sliced, differentiated and row-checked like x. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- scripts/plot_qwen3_next_norm_evidence.py | 20 +-- scripts/qwen3_next_norm_evidence.py | 198 +++++++++++++++-------- 2 files changed, 146 insertions(+), 72 deletions(-) diff --git a/scripts/plot_qwen3_next_norm_evidence.py b/scripts/plot_qwen3_next_norm_evidence.py index 54dab1a67..41c444100 100644 --- a/scripts/plot_qwen3_next_norm_evidence.py +++ b/scripts/plot_qwen3_next_norm_evidence.py @@ -22,13 +22,13 @@ def _short(name: str) -> str: - return name.replace(" (forward only)", "*") + return name.replace(" (forward only)", "*").replace(" (cast-first)", "\n(cast-first)") def plot_op(op: str, data: dict, title: str, out: Path) -> None: names = list(data["row_invariance"]) color = {n: COLORS[i % len(COLORS)] for i, n in enumerate(names)} - fig, axes = plt.subplots(2, 2, figsize=(13, 9)) + fig, axes = plt.subplots(2, 2, figsize=(14, 10), layout="constrained") fig.suptitle(title, fontsize=13) for ax, key, label in ( @@ -52,10 +52,12 @@ def plot_op(op: str, data: dict, title: str, out: Path) -> None: ax = axes[1, 0] acc = data["accuracy"][max(data["accuracy"], key=int)] metrics = [ - ("forward_max_abs", "forward max |err|"), - ("dx_max_abs_over_absmax", "dx max |err| / max|dx|"), - ("dweight_max_abs_over_absmax", "dweight max |err| / max|dw|"), + ("forward_max_abs", "forward\nmax |err|"), + ("dx_max_abs_over_absmax", "dx\nmax |err| / max"), + ("dweight_max_abs_over_absmax", "dweight\nmax |err| / max"), + ("dgate_max_abs_over_absmax", "dgate\nmax |err| / max"), ] + metrics = [m for m in metrics if any(acc[n].get(m[0]) is not None for n in names)] width = 0.8 / len(names) for i, name in enumerate(names): vals = [acc[name].get(m) for m, _ in metrics] @@ -87,14 +89,13 @@ def plot_op(op: str, data: dict, title: str, out: Path) -> None: no_bwd = bi[n]["dx_rows_differing"] is None ax.text(max(f, d) + 0.2, y, f"{f} / {'n/a' if no_bwd else d}", va="center", fontsize=8) ax.set_yticks(list(ys), [_short(n) for n in names], fontsize=8) + ax.set_xlim(0, max(3, max(fwd + dx) * 1.4)) ax.invert_yaxis() ax.set_xlabel(f"rows differing (of {checked}; row alone vs inside a batch, bitwise)") ax.set_title("row invariance (0 = batch-invariant)") ax.legend(fontsize=8) ax.grid(True, axis="x", alpha=0.3) - fig.text(0.01, 0.005, "* forward only (no backward)", fontsize=8) - fig.tight_layout(rect=(0, 0.02, 1, 0.96)) fig.savefig(out, dpi=130) print(f"wrote {out}") @@ -106,9 +107,10 @@ def main() -> None: ops = report["ops"] for op, data in ops.items(): name = "figure.png" if len(ops) == 1 else f"figure-{op}.png" + hidden = data.get("hidden", report.get("hidden")) title = ( - f"Qwen3-Next {op.replace('_', ' ')} — {env['gpu']}, hidden {report['hidden']}, " - f"BF16, commit {report['git_commit'][:7]}" + f"Qwen3-Next {op.replace('_', ' ')} — {env['gpu']}, hidden {hidden}, " + f"BF16, commit {report['git_commit'][:7]} (* forward only)" ) plot_op(op, data, title, path.parent / name) diff --git a/scripts/qwen3_next_norm_evidence.py b/scripts/qwen3_next_norm_evidence.py index 27a063df6..d29f249ee 100644 --- a/scripts/qwen3_next_norm_evidence.py +++ b/scripts/qwen3_next_norm_evidence.py @@ -25,24 +25,30 @@ from typing import Any, Callable import torch +import torch.nn.functional as F REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT)) -from rl_engine.kernels.ops.cuda.norm.rmsnorm import Qwen3NextRMSNormCudaOp # noqa: E402 -from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import Qwen3NextRMSNormOp # noqa: E402 +from rl_engine.kernels.ops.cuda.norm.rmsnorm import ( # noqa: E402 + Qwen3NextRMSNormCudaOp, + Qwen3NextRMSNormGatedCudaOp, +) +from rl_engine.kernels.ops.pytorch.norm.qwen3_next_rms_norm import ( # noqa: E402 + Qwen3NextRMSNormGatedOp, + Qwen3NextRMSNormOp, +) EPS = 1e-6 -HIDDEN = 2048 # Qwen3-Next hidden size (decoder and final norms) -TIMED_ROWS = (1024, 4096, 16384, 65536) # --------------------------------------------------------------------------- # -# Candidates: name -> (forward fn(x, w) -> y, has_backward) +# Ops. Each candidate is fn(*row_inputs, weight) -> y, plus whether it has a +# backward. Row inputs are sliced together for the row-invariance check. # --------------------------------------------------------------------------- # -def zero_centred_candidates() -> dict[str, dict[str, Any]]: +def zero_centred_candidates(hidden: int) -> dict[str, dict[str, Any]]: cands: dict[str, dict[str, Any]] = { "rl-kernel CUDA": {"fn": Qwen3NextRMSNormCudaOp(), "backward": True}, "rl-kernel PyTorch reference": {"fn": Qwen3NextRMSNormOp(), "backward": True}, @@ -50,7 +56,7 @@ def zero_centred_candidates() -> dict[str, dict[str, Any]]: try: from transformers.models.qwen3_next.modeling_qwen3_next import Qwen3NextRMSNorm - module = Qwen3NextRMSNorm(HIDDEN, eps=EPS).cuda() + module = Qwen3NextRMSNorm(hidden, eps=EPS).cuda() def hf(x, w, module=module): return torch.func.functional_call(module, {"weight": w}, (x,)) @@ -64,7 +70,7 @@ def hf(x, w, module=module): with set_current_vllm_config(VllmConfig()): from vllm.model_executor.layers.layernorm import GemmaRMSNorm - gemma = GemmaRMSNorm(HIDDEN, eps=EPS).cuda() + gemma = GemmaRMSNorm(hidden, eps=EPS).cuda() def vllm_fwd(x, w, gemma=gemma): gemma.weight.data = w.detach() @@ -85,46 +91,121 @@ def vllm_fwd(x, w, gemma=gemma): return cands -def zero_centred_golden(x: torch.Tensor, w: torch.Tensor) -> torch.Tensor: +def zero_centred_golden(x, w): x64 = x.double() return x64 * torch.rsqrt(x64.square().mean(-1, keepdim=True) + EPS) * (1.0 + w.double()) +def gated_candidates(hidden: int) -> dict[str, dict[str, Any]]: + cuda_op, ref_op = Qwen3NextRMSNormGatedCudaOp(), Qwen3NextRMSNormGatedOp() + cands: dict[str, dict[str, Any]] = { + "rl-kernel CUDA": {"fn": lambda x, g, w: cuda_op(x, w, g, eps=EPS), "backward": True}, + "rl-kernel PyTorch reference": { + "fn": lambda x, g, w: ref_op(x, w, g, eps=EPS), + "backward": True, + }, + } + try: + from transformers.models.qwen3_next.modeling_qwen3_next import Qwen3NextRMSNormGated + + module = Qwen3NextRMSNormGated(hidden, eps=EPS).cuda() + + def hf(x, g, w, module=module): + return torch.func.functional_call(module, {"weight": w}, (x, g)) + + cands["transformers Qwen3NextRMSNormGated (cast-first)"] = {"fn": hf, "backward": True} + except ImportError: + pass + try: + from vllm.config import VllmConfig, set_current_vllm_config + + with set_current_vllm_config(VllmConfig()): + from vllm.model_executor.layers.layernorm import RMSNormGated + + gated = RMSNormGated(hidden, eps=EPS, norm_before_gate=True).cuda() + + def vllm_fwd(x, g, w, gated=gated): + gated.weight.data = w.detach() + return gated.forward_cuda(x, g) + + cands["vLLM RMSNormGated (forward only)"] = {"fn": vllm_fwd, "backward": False} + except ImportError: + pass + return cands + + +def gated_golden(x, g, w): + """vLLM's convention (the one #468 implements) in FP64: x * rstd * w * silu(gate).""" + + x64 = x.double() + return (x64 * torch.rsqrt(x64.square().mean(-1, keepdim=True) + EPS) * w.double()) * F.silu( + g.double() + ) + + +OPS: dict[str, dict[str, Any]] = { + "zero_centred_rmsnorm": { + "hidden": 2048, # decoder and final norms + "row_inputs": 1, + "weight": lambda h, gen: torch.randn(h, device="cuda", generator=gen) * 0.1, + "candidates": zero_centred_candidates, + "golden": zero_centred_golden, + "timed_rows": (1024, 4096, 16384, 65536), + }, + "gated_rmsnorm": { + "hidden": 128, # GDN value head dim; rows are tokens x heads + "row_inputs": 2, + "weight": lambda h, gen: 1.0 + torch.randn(h, device="cuda", generator=gen) * 0.1, + "candidates": gated_candidates, + "golden": gated_golden, + "timed_rows": (4096, 16384, 65536, 262144), + }, +} + + # --------------------------------------------------------------------------- # # Measurements # --------------------------------------------------------------------------- # -def _inputs(rows: int, seed: int, dtype=torch.bfloat16): +def _inputs(spec, rows: int, seed: int, dtype=torch.bfloat16): g = torch.Generator(device="cuda").manual_seed(seed) - x = (torch.randn(rows, HIDDEN, device="cuda", generator=g) * 2).to(dtype) - w = (torch.randn(HIDDEN, device="cuda", generator=g) * 0.1).to(dtype) - up = torch.randn(rows, HIDDEN, device="cuda", generator=g).to(dtype) - return x, w, up + h = spec["hidden"] + row_inputs = [(torch.randn(rows, h, device="cuda", generator=g) * 2).to(dtype)] + for _ in range(spec["row_inputs"] - 1): + row_inputs.append(torch.randn(rows, h, device="cuda", generator=g).to(dtype)) + w = spec["weight"](h, g).to(dtype) + up = torch.randn(rows, h, device="cuda", generator=g).to(dtype) + return row_inputs, w, up -def _grads(fn, x, w, up, dtype=None): - xl = (x if dtype is None else x.to(dtype)).detach().clone().requires_grad_(True) - wl = (w if dtype is None else w.to(dtype)).detach().clone().requires_grad_(True) - out = fn(xl, wl) +def _grads(fn, row_inputs, w, up, dtype=None): + def leaf(t): + return (t if dtype is None else t.to(dtype)).detach().clone().requires_grad_(True) + + rl, wl = [leaf(t) for t in row_inputs], leaf(w) + out = fn(*rl, wl) out.backward(up if dtype is None else up.to(dtype)) - return out.detach(), xl.grad, wl.grad + return out.detach(), [t.grad for t in rl], wl.grad -def accuracy(cands, golden, rows: int, seed: int) -> dict[str, Any]: - x, w, up = _inputs(rows, seed) - ref_out, ref_dx, ref_dw = _grads(golden, x, w, up, torch.float64) +def accuracy(spec, cands, rows: int, seed: int) -> dict[str, Any]: + row_inputs, w, up = _inputs(spec, rows, seed) + ref_out, ref_drows, ref_dw = _grads(spec["golden"], row_inputs, w, up, torch.float64) result = {} for name, c in cands.items(): entry: dict[str, Any] = {} if c["backward"]: - out, dx, dw = _grads(c["fn"], x, w, up) - for key, got, ref in (("dx", dx, ref_dx), ("dweight", dw, ref_dw)): + out, drows, dw = _grads(c["fn"], row_inputs, w, up) + pairs = [("dx", drows[0], ref_drows[0]), ("dweight", dw, ref_dw)] + if len(drows) > 1: + pairs.append(("dgate", drows[1], ref_drows[1])) + for key, got, ref in pairs: err = (got.double() - ref).abs().max().item() entry[f"{key}_max_abs_over_absmax"] = err / ref.abs().max().item() else: with torch.no_grad(): - out = c["fn"](x, w) + out = c["fn"](*row_inputs, w) err = (out.double() - ref_out).abs() entry["forward_max_abs"] = err.max().item() entry["forward_correctly_rounded_fraction"] = ( @@ -134,27 +215,27 @@ def accuracy(cands, golden, rows: int, seed: int) -> dict[str, Any]: return result -def row_invariance(cands, seeds=(3, 4, 5), rows: int = 4096, step: int = 16) -> dict[str, Any]: +def row_invariance(spec, cands, seeds=(3, 4, 5), rows: int = 4096, step: int = 16): """256 rows computed alone vs the same rows inside a batch, bitwise.""" result = {} for name, c in cands.items(): fwd_bad = dx_bad = checked = 0 for seed in seeds: - x, w, up = _inputs(rows, seed) + row_inputs, w, up = _inputs(spec, rows, seed) if c["backward"]: - full_out, full_dx, _ = _grads(c["fn"], x, w, up) + full_out, full_drows, _ = _grads(c["fn"], row_inputs, w, up) else: with torch.no_grad(): - full_out = c["fn"](x, w) + full_out = c["fn"](*row_inputs, w) for i in range(0, rows, step): - sl = slice(i, i + 1) + part = [t[i : i + 1] for t in row_inputs] if c["backward"]: - out, dx, _ = _grads(c["fn"], x[sl], w, up[sl]) - dx_bad += not torch.equal(dx[0], full_dx[i]) + out, drows, _ = _grads(c["fn"], part, w, up[i : i + 1]) + dx_bad += not all(torch.equal(d[0], fd[i]) for d, fd in zip(drows, full_drows)) else: with torch.no_grad(): - out = c["fn"](x[sl], w) + out = c["fn"](*part, w) fwd_bad += not torch.equal(out[0], full_out[i]) checked += 1 result[name] = { @@ -182,20 +263,19 @@ def _time_us(fn: Callable[[], Any], warmup: int = 10, iters: int = 50) -> float: return statistics.median(samples) -def latency(cands, rows_list=TIMED_ROWS) -> dict[str, Any]: +def latency(spec, cands) -> dict[str, Any]: result: dict[str, Any] = {name: {} for name in cands} - for rows in rows_list: - x, w, up = _inputs(rows, seed=11) + for rows in spec["timed_rows"]: + row_inputs, w, up = _inputs(spec, rows, seed=11) for name, c in cands.items(): row: dict[str, float] = {} with torch.no_grad(): - row["forward_us"] = _time_us(lambda f=c["fn"]: f(x, w)) + row["forward_us"] = _time_us(lambda f=c["fn"]: f(*row_inputs, w)) if c["backward"]: - xl = x.detach().clone().requires_grad_(True) - wl = w.detach().clone().requires_grad_(True) - out = c["fn"](xl, wl) + leaves = [t.detach().clone().requires_grad_(True) for t in [*row_inputs, w]] + out = c["fn"](*leaves) row["backward_us"] = _time_us( - lambda o=out: torch.autograd.grad(o, (xl, wl), up, retain_graph=True) + lambda o=out, lv=leaves: torch.autograd.grad(o, lv, up, retain_graph=True) ) result[name][str(rows)] = row return result @@ -227,19 +307,27 @@ def environment() -> dict[str, Any]: return env -def run_op(name: str, cands, golden) -> dict[str, Any]: +def run_op(name: str, spec) -> dict[str, Any]: + cands = spec["candidates"](spec["hidden"]) print(f"[{name}] candidates: {', '.join(cands)}", flush=True) report = { - "accuracy": {str(r): accuracy(cands, golden, r, seed=r) for r in (257, 4096)}, - "row_invariance": row_invariance(cands), - "latency": latency(cands), + "hidden": spec["hidden"], + "accuracy": {str(r): accuracy(spec, cands, r, seed=r) for r in (257, 4096)}, + "row_invariance": row_invariance(spec, cands), + "latency": latency(spec, cands), } for cand, entry in report["row_invariance"].items(): print(f" BI {cand}: {entry}", flush=True) return report -def build_report(ops: dict[str, tuple[Callable, Callable]]) -> dict[str, Any]: +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--ops", default=",".join(OPS), help="comma list of ops") + args = parser.parse_args() + if not torch.cuda.is_available(): + raise SystemExit("needs a CUDA device") torch.backends.cuda.matmul.allow_tf32 = False report = { "kind": "qwen3_next_norm_evidence", @@ -247,26 +335,10 @@ def build_report(ops: dict[str, tuple[Callable, Callable]]) -> dict[str, Any]: "git_commit": _git("rev-parse", "HEAD") or "unknown", "git_dirty": bool(_git("status", "--porcelain", "--untracked-files=no")), "environment": environment(), - "hidden": HIDDEN, "eps": EPS, "dtype": "bfloat16", - "ops": {}, + "ops": {name: run_op(name, OPS[name]) for name in args.ops.split(",")}, } - for name, (make_cands, golden) in ops.items(): - report["ops"][name] = run_op(name, make_cands(), golden) - return report - - -OPS = {"zero_centred_rmsnorm": (zero_centred_candidates, zero_centred_golden)} - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--out", type=Path, required=True) - args = parser.parse_args() - if not torch.cuda.is_available(): - raise SystemExit("needs a CUDA device") - report = build_report(OPS) args.out.parent.mkdir(parents=True, exist_ok=True) args.out.write_text(json.dumps(report, indent=2) + "\n") print(f"wrote {args.out} (commit {report['git_commit'][:7]}, dirty={report['git_dirty']})") From 3d0bae7174c09a133ea1288d820b63e4a7db8f57 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:11:14 +0000 Subject: [PATCH 40/44] fix(scripts): readable Qwen3-Next norm evidence figure layout Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- scripts/plot_qwen3_next_norm_evidence.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/plot_qwen3_next_norm_evidence.py b/scripts/plot_qwen3_next_norm_evidence.py index 54dab1a67..41c444100 100644 --- a/scripts/plot_qwen3_next_norm_evidence.py +++ b/scripts/plot_qwen3_next_norm_evidence.py @@ -22,13 +22,13 @@ def _short(name: str) -> str: - return name.replace(" (forward only)", "*") + return name.replace(" (forward only)", "*").replace(" (cast-first)", "\n(cast-first)") def plot_op(op: str, data: dict, title: str, out: Path) -> None: names = list(data["row_invariance"]) color = {n: COLORS[i % len(COLORS)] for i, n in enumerate(names)} - fig, axes = plt.subplots(2, 2, figsize=(13, 9)) + fig, axes = plt.subplots(2, 2, figsize=(14, 10), layout="constrained") fig.suptitle(title, fontsize=13) for ax, key, label in ( @@ -52,10 +52,12 @@ def plot_op(op: str, data: dict, title: str, out: Path) -> None: ax = axes[1, 0] acc = data["accuracy"][max(data["accuracy"], key=int)] metrics = [ - ("forward_max_abs", "forward max |err|"), - ("dx_max_abs_over_absmax", "dx max |err| / max|dx|"), - ("dweight_max_abs_over_absmax", "dweight max |err| / max|dw|"), + ("forward_max_abs", "forward\nmax |err|"), + ("dx_max_abs_over_absmax", "dx\nmax |err| / max"), + ("dweight_max_abs_over_absmax", "dweight\nmax |err| / max"), + ("dgate_max_abs_over_absmax", "dgate\nmax |err| / max"), ] + metrics = [m for m in metrics if any(acc[n].get(m[0]) is not None for n in names)] width = 0.8 / len(names) for i, name in enumerate(names): vals = [acc[name].get(m) for m, _ in metrics] @@ -87,14 +89,13 @@ def plot_op(op: str, data: dict, title: str, out: Path) -> None: no_bwd = bi[n]["dx_rows_differing"] is None ax.text(max(f, d) + 0.2, y, f"{f} / {'n/a' if no_bwd else d}", va="center", fontsize=8) ax.set_yticks(list(ys), [_short(n) for n in names], fontsize=8) + ax.set_xlim(0, max(3, max(fwd + dx) * 1.4)) ax.invert_yaxis() ax.set_xlabel(f"rows differing (of {checked}; row alone vs inside a batch, bitwise)") ax.set_title("row invariance (0 = batch-invariant)") ax.legend(fontsize=8) ax.grid(True, axis="x", alpha=0.3) - fig.text(0.01, 0.005, "* forward only (no backward)", fontsize=8) - fig.tight_layout(rect=(0, 0.02, 1, 0.96)) fig.savefig(out, dpi=130) print(f"wrote {out}") @@ -106,9 +107,10 @@ def main() -> None: ops = report["ops"] for op, data in ops.items(): name = "figure.png" if len(ops) == 1 else f"figure-{op}.png" + hidden = data.get("hidden", report.get("hidden")) title = ( - f"Qwen3-Next {op.replace('_', ' ')} — {env['gpu']}, hidden {report['hidden']}, " - f"BF16, commit {report['git_commit'][:7]}" + f"Qwen3-Next {op.replace('_', ' ')} — {env['gpu']}, hidden {hidden}, " + f"BF16, commit {report['git_commit'][:7]} (* forward only)" ) plot_op(op, data, title, path.parent / name) From db420ab3fcf0cb49cf667b27bd17d8ce9414eb07 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:18:42 +0000 Subject: [PATCH 41/44] test(gdn): L1 batch-invariance check for the causal-conv1d golden Every sequence of a batch of 257 is compared, bitwise, alone and inside sub-batches of 2, 7 and 64 at other offsets and a shuffled batch order, with contiguous and shuffled cache blocks, fp32 and bf16 caches, with and without bias and SiLU, over three seeds: output rows and rolled state blocks are unchanged. Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- tests/check_gdn_recurrent_golden.py | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/check_gdn_recurrent_golden.py b/tests/check_gdn_recurrent_golden.py index 7815939c0..d7aa3f34f 100644 --- a/tests/check_gdn_recurrent_golden.py +++ b/tests/check_gdn_recurrent_golden.py @@ -294,6 +294,52 @@ def test_conv_state_update_is_bitwise_exact(batch): assert torch.equal(state_got, state_ref), f"{state_dtype} batch={batch}" +@pytest.mark.parametrize("state_dtype", [torch.float32, torch.bfloat16]) +@pytest.mark.parametrize("activation", ["silu", None]) +@pytest.mark.parametrize("with_bias", [True, False]) +def test_conv_golden_is_batch_invariant(state_dtype, activation, with_bias): + """L1 for the conv golden, bitwise: a sequence's output row and state block + do not depend on which other sequences share the step. + + Every sequence of a batch of 257 is run alone; sub-batches of 2, 7 and 64 at + other offsets and a shuffled batch order are run too, with contiguous and + shuffled cache blocks, over three seeds. + """ + from rl_engine.kernels.ops.pytorch.linear_attn import CausalConv1dUpdateOp + + op = CausalConv1dUpdateOp() + batch = 257 + for seed in (3, 4, 5): + g = torch.Generator(device="cpu").manual_seed(seed) + perm = torch.randperm(batch, generator=g).cuda() + shuffled_blocks = (torch.randperm(batch, generator=g) + 1).to("cuda", torch.int32) + for indices in (None, shuffled_blocks): + inp = _conv_inputs(batch, state_dtype, seed, with_bias=with_bias, indices=indices) + + def run(rows, inp=inp): + return op.forward( + inp["x"][rows], + inp["state"].clone(), + inp["weight"], + inp["indices"][rows], + bias=inp["bias"], + activation=activation, + ) + + full_out, full_state = run(torch.arange(batch, device="cuda")) + picks = [torch.tensor([r], device="cuda") for r in range(batch)] + picks += [ + torch.arange(o, o + s, device="cuda") for s, o in ((2, 0), (7, 100), (64, 193)) + ] + picks.append(perm) + for rows in picks: + out, state = run(rows) + blocks = inp["indices"][rows].long() + where = f"seed={seed} rows[{int(rows[0])}..] n={rows.numel()}" + assert torch.equal(out, full_out[rows]), f"out {where}" + assert torch.equal(state[blocks], full_state[blocks]), f"state {where}" + + @pytest.mark.parametrize("batch", [1, 4, 17, 64]) def test_conv_output_matches_provider_with_fp32_cache(batch): """An fp32 cache reproduces the provider except on a few output elements. From ba467508cb935d4049552436c4beef2a60c1a75f Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:32:22 +0000 Subject: [PATCH 42/44] docs(norm): zero-centred RMSNorm evidence vs existing implementations on B200 Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- docs/operators/qwen3-next-rms-norm.md | 28 +++ .../qwen3-next-rms-norm-b200/figure.png | Bin 0 -> 241252 bytes .../qwen3-next-rms-norm-b200/report.json | 225 ++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 docs/usage/evidence/qwen3-next-rms-norm-b200/figure.png create mode 100644 docs/usage/evidence/qwen3-next-rms-norm-b200/report.json diff --git a/docs/operators/qwen3-next-rms-norm.md b/docs/operators/qwen3-next-rms-norm.md index 81fdde9b3..ede972b3a 100644 --- a/docs/operators/qwen3-next-rms-norm.md +++ b/docs/operators/qwen3-next-rms-norm.md @@ -98,6 +98,34 @@ The CUDA path reuses the existing `rmsnorm_fwd_kernel` reduction (`block_reduce_sum` over `choose_threads(H)`), so the offset costs one fp32 add per element and no extra memory traffic. +## Evidence + +![zero-centred RMSNorm vs existing implementations on B200](../usage/evidence/qwen3-next-rms-norm-b200/figure.png) + +[`report.json`](../usage/evidence/qwen3-next-rms-norm-b200/report.json) was written by +`scripts/qwen3_next_norm_evidence.py` from a clean tree at `3d0bae7`, on an otherwise idle +B200 (torch 2.13.0+cu130, transformers 5.17.0, vLLM 0.30.0, FlashInfer 0.6.18). Hidden 2048, +BF16. + +| | rl-kernel CUDA | PyTorch reference | transformers | vLLM `GemmaRMSNorm`* | FlashInfer `gemma_rmsnorm`* | +|---|---|---|---|---|---| +| rows differing alone vs in a batch, forward / `dx` (of 768) | **0 / 0** | 0 / 12 | 2 / 11 | 2 / n/a | 0 / n/a | +| forward, 65536 rows | 425 µs | 2148 µs | 1449 µs | 1449 µs | **90 µs** | +| backward, 65536 rows | 30.9 ms | **3.0 ms** | 3.1 ms | n/a | n/a | + +\* forward only, no backward. + +- **Accuracy is the same for every implementation:** forward max error 1.56e-2 against the + FP64 golden (BF16 output rounding; 99.999% of elements correctly rounded), `dx` and + `dweight` within 2.8e-3 and 2.1e-3 of their maximum. +- **Row invariance:** only this op and FlashInfer give every row the same bits alone and in a + batch; FlashInfer has no backward. transformers and vLLM differ on 2 forward rows in 768. +- **The backward is about 10× slower than transformers at 65536 rows.** `dweight` is folded + over rows in ascending order by `reduce_rows_fp32_left_fold`, one thread per column, so + that the batch `dweight` equals the in-order sum of single-row contributions bitwise (the + gradient-invariance contract's singleton-aggregate check). That serial fold over rows is + the cost; a faster tree fold would not pass that check. + ## Tests ```bash diff --git a/docs/usage/evidence/qwen3-next-rms-norm-b200/figure.png b/docs/usage/evidence/qwen3-next-rms-norm-b200/figure.png new file mode 100644 index 0000000000000000000000000000000000000000..a408aff9c5b251764da2c9a76b1b5bb69a61366f GIT binary patch literal 241252 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%0Au(Ut?9? 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

+TUKd)9XTh%C%rI3$4*JbUQq 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

u^9HK&R;~B`h~VB%OTU5%YaoX6h22hLRd# z;|$5wgBf8PVJ^mr)&^vYL>^DssonYz=Vk7We-Cuglgm9>8bRtX3X{?#t7e;5<#j`N zz^zj9Jj9w5=TDcu#<8{M$23I({S37cU0oBEPcWZj8fsKHeykdli6!#jg|-$pDPsjGC5iw zPZkr|0lJ8XNIB-i^rcQvX+vvd$`Qc=J1cV3Rog(bFycAG0)U{H%T>tef^kZQbzWyL z@jZ6o^;#)%{?qReU|MvP!pFH%gqDaeiErXY_7$X|vx_z|2|Ft9V@tzkavMI*x9cli zU22t%34W%1-f?ZI*mYqW_qN;f^mA^nbAEWRS$p2Ont$1HC6xKTgK@x?^&X3-KpX$- zKZQxU=y_Jkl{Ip2VQX@3rX^E|i`}*)D6>}%Hc-J}-~FpR=Y$#+;_`>!#o({6a2g?r zLFe-2<0YCGFcpDtuH;I-c-k4nw!qN(7vwf%-L$81Y*h-XHnxt^=!d^75ZuynQng<= zQ#UB_{A%%3&ehq8H2TBy?pLMh^Kil4?4x_H&E?AxW_t^mZ8WlnFa2e(aaBmSGsy+L z%rp_F+!h-600II6hy`^+L?f&z@$mtLS1Mp~XK>#luML<5FzBNu3+oeb)>)eQdXTqG zfRzD|U-D$oWur$`|36L^7pCHE?VD^w#r62kKkh59-5QWcG|jAz zH;&XvfSH%`RR0JDz+IRCgP41?#1D+wbZ=TgJ!wZ)H8j&2H&Q+^?A%?vlIubWvfjY3 z3KAb}#e+=GE7yMI5b3 zNyj6uQU5Nzrvm^s^IKcw9g&P+VAN!4lpRZ|Fb3F+t^+B3a9($H!N6 zMBoz^^&h?ZWOO)m`p7Wi8osJS%v&pXr1SOk$y0swO~)Y$pOUhbY|sM*GAu#kU%sdS zWd_FGg*1LnM&xCw{d=3DmUw$@R<{-m>;RMHNJ3~5;%bdnd02390r`5o1g>%6TOA|1 zpE6w7h0?SY+!Be+hV2wTzr%ZSA1T#%nhUK!))M-9($_*d^Rh7)t>aq+1rOK$a^B%C zaw1}2Sn=`36w^gzXEUJz!}!N-J@v^%&N9#g({TQ+P*b>#_#C>ZRehGhqU*J`wl?;o zVpIh}#%G;i;2lWAz&ID%`z~Tp1}Qr%RkJR)oCaa~v@i(Mxfn&?^Faa0@Y$Uy#|GBb z2=vjiq$i&Yjzo^MB@|mpd!;-*8Xs<-t9N7=ch{%Wl z^lC)48R{6tfAUVhuOBEoY8#w+5bw0-M7KG_XZ|7A?zQuHspU)}4`Z?{8^==FifV0J z+2Udqk6nl@;Nn~P7ItRqqjya26VQC53qA@6em%MOWDpyfKz6=g*eV?dzN6JRQgPR= zg8c*0OvQL}`m86(p#Yn30)&5%NhaN|dj}qr!mp#M@F)}h1LzS2L(Fdhv4$Wf*kyy^4%8s&V86{YZ4GWF&K@f8t1 ztPc-h+dQM8h|sfcGRFzQwy)HaUOp!ROgSfY}%ez6_s@# zgVDA7#L)PB=Y_{!p0Gy?bi!Pw5rX>Z%Pr}cS@dq;IkZ{j4^#Ay9~daPJu)r(OBR08 z+`s(oq*VldU?X@C;18DP+lty&ThFiC&$y6Bf^s5_))hJ96Q=*wXGDmHE8(%s4j}~O zx*0N#X7EHvC>D5Lm7!dF6^Welyp~MlV<|hgwz|T^Kvo=~axnu}+fir|y7XYhPNd?t zV7r}T;m#z$x}ef1K2UjChmiMb=G|*oRuk^uh#n(8d}GrDMZ8F(GP#v+&Xquu2P}{* zHS{@RR9&`-Ju$!Sl$!x)?2PK)*FPNdd5<3xfMq(TJs;AYp@#xRJEgHC_V>ml1`r4! zw@@>-GK(SUG&?l#BD+D{J{vgHpo@W>N!NS}b|x0tnejZMRDbrutR?}$G`>@`kgy2Y zRM-~6Po?Zh_%z|8R3i;0X3RRznMQKIIeju$^?#iD-|krSbc;Jdm~XGs--*gpzntw5 z{}l{|a%xR=Yh97abkH%VS+vUglxacZ#d*!Gp=u20Y!c)F0G59g3Piy2Tl7TI zk45cue%i4j<+q89pcm4pQx2c9RA_Tj6v&*D}#4y%{V5xSM?5co)T{rY703w7Ol8_I0Cy)v}GQg=Qc+}Uf zS#V=eXgd7@X}Sld!)EvE<*mbBCks zPVQ{;^Fb)bzk?-ncalkIIr8V_Jkt#O^@&mUn6aEviHyprws>C=>=S+5ETenjf|t`1 z65m!>=599%&z{?RDRxz+KPJHOPS~q#3Y|)+e_XU(XZobg8qO6_E>9L&=lbBlUA7wY zXTwq`h(-)+A0|gbxa$ZJ8glfP_NULEHCg-YmV1@R$Kid${4>657=y{niwsk%+w74| znIsPG#h23mg%Zgf9puUM7$?u`lUY1|Z9(eCvArw#c>7a}9zH9ICqG=^L+Q1QQ1c>! zAL<*zOxG*&Rx-6*GUx+*KD&}7=|_aji+~B3Y~tQJO2~GU-13agstLL!>d+4FQZpiB z(>{9Ow;a0q(C}V(B<>#n3!qy=5S7Bqlrj7MKT;5Ws}z)wF+$6Trf_m4cpA`5b6CZQ zrm&?nA{TKu{9PjD-T!5NfHRTl_ zAj88fQVt?%Q09k#|K~j10K!2;?F?&>?+4z~f51)p%3~9I$vQpqQUhDXraogub^jx_ohG9@!YPrhwaT-9A%J=^jXKS8c zw_I8rMrI~VMf}7-T+WRzFPb1ZKO1YuQSOqPigfl6Nu49kQ731Zu2`sleyvL_Ur*VG z)a3@^bC0(!BaO71UCS=%j7q1jc)c?*<|!!GaMvkX#U~Rwv8c3BrpDGI!%%h-%;;=6 zv`+b)eD{pFcx=&ym*!lsKD{TFR8ASJQ5Dra$1!V)`vk zUBdYBabr*yc0CQ0(fWm-~I~yo!#*EV(t#A6t@4F?kpbgO}bK+TUe=Kl(>aaFdC# zCgcugY+DL@_1iyi=d~^O95unf&-TC;`L}?p;9NUo0V7btrAwEnZCxZhYa6_IzV^VTufd3+sPBkY*qM&jYF5H zR$Q=n?7+$(`K?)oy7J5zgJqvgJBN5KqV9W+%?R{7((&Qlf8aL&!zZ9RZ-Y6TvmOvU z&g#7&48uuO;H^0mAkSF%_18Xyjap|T)q(2o>Hyc9`u7tj zkLXf1Y~MbH@Bu4vah&B9>8w9JoQITkQ84H_Z<>tC>N^NhI24k}rw)k7OHE zQsh&r+`(1@V00K*vw}%vN5;Ba1q2zTW$l=#91S|7$=-`@-p&)BKc&Tq8S8CfVghtX9aR<)BJiL1`MMSr~_j73BBv(-Sd55I8= zX;Ns>t-~^6KyPrdsfetjp^T|Pa<_gT7lD#sH;v$s2f0)i*c z&wzTf}!%%7j|4CW+RaMJRR=H{S3+E}`?RlZqVm6c&j{Z26ul2Bq zy36<9Tc~5?kmTUN##OIe=?K_ykzP1zDudbKq1~tRb!97i zCxd$Y_X?vT2$YNY=UTd3KQN!sqh8t_9LXBFfw^YHY_BD;tXOwMzktxWmNDrXk;l!; zxKy46oVpkOen`+X$6ia}$^!xo3J;kRFj5Cd1TKSLqWja?q;+4GH+Vfy*~?UQ2e<2U zHD+hlt5YKTl>1-zX){|q%uB=%IyVTiEKlPOiuct zUA)f8(mklIyX{$&bv61Uv0=J%{*0wiQ|dL{E@Mk}Yb z>ip0L&!pbK(y3DV8*2Y2+{O4@ltP`)bbHjod#pN6_f$Ygn(_Us168ScO1hy0 z_FVU(FfQ|ez&kb*0x+0c;K#X;DxesXc%Ih0qq(Y=YdQ9hcXm5(KU(_*(S~hC1nntU z=YPAVu1`qiLQ2?gpbZX&2pQE%jKjgg~n%kYw46V^%5TpGMfJNDSei zUA?ir-$|3s|6o`*+Y}B&O%*)j*%6qY)ya5zS4-4akO1f|E@&z;=@pQsqHGXMlu!oT z)R3c2Gl44%N=6__J- zbGZ6AB!kDaBt}f1g^xG;y2_VIU=8nHl)*27*QAl+K3BC8iSw(fBZi}-_`AMvXth%l zaa241&&|!>0TUphFVyf-!5|Wd5eb{D*YcCly?K~-bgkQR=2mqq(TBSBY1}@$KoUdr zRU%bfE@VHiTW|q!w&S2G%fg%_Z0D@55vs%;Vw;)_-kV3Ov&mL?XbEPCD~- zXzO!R$WL- z)PbED=xqZ>XY2_&fKBqY6~uQ(I#^2r&nYz3eMgL1q3ZMK~ z4rS3m^DZ-Nz#Ap1-|wV79Fj6JXP6HUvPyA7lHKCw1sD=XNydO3=K%y%1>tAt$ywBf- z)s2Q6Zr^GX1w$3L2 z_PK3msev4bdatXHvw_T3gFl3Y8R2rVMCWroRgdo1oZx*ZlOfD4~ zHzvL~&7~VuLr3@M$$-J#aGOu9M@%Hv)SJ)79mNpYj@~3<`pES}_vc$&eoqOyYP_xs zf*?+SKaKm*7?&GEUFU^M_muH&LRN;R8&M+bfdKluG6)jSX=;-G`o$f`!^Jg@d}kPC z!I^vk)7KPy5-TG#cPzW)W6&=xLB{GpwYI;XC9otl?zr&2>s2V6Qp2I_VlBGZl3<1& zhBjr(nguC)XYby8Qno;Sn6!&#>MpNZ`HI5?f>v99h=MusZx@9uSzKQfH2sLSp_JL@ zqAp*O2h0~LhG|+)KF>jO9sV*1UXf(mIN5~-oL6U8LcIjU+&r;jyd%p_fH&Tso&zgn zip>qm0;@ZqTaOy0Lt24~wbX9WAj8*^{-OH6|8g>S6a-XGR?nJR|ZjCU&7M1w{Hnw zAN)*#oaH;SmH(kB32~plq*RuM1I{wIM{rVaG$}L6^pm?+?}sI&F#;z44^&CK9oP^r zf#fMWi%6?9MUF@@UPlmG~?YXbMsAjZ0ZF1!jNnl798ntrYOwxjG7oSh2C@s@F~#^$W`LI`RS= zzt~5{s%uVZ-Te?TWvTX(tn*Hps>OFIj>tQ&O?jlW$#W;)vlBchjQ_?YKW&UAsGk8) ztm`ODLTRn2Uq3Uu?iHRJUSBU)`}5uDtxpAV$G_)Oc1bS4HQP4ds|v%bpmT288E6*{F~|Om+@uxwM$cUIO2^fsUvK)A=lK0Y^drsX5H(Qsu>LC95U?on%d*kM8F{EKZ zyNk$q{gSC$dHqharz=D2I69rB%CEIyQdU!BMtAu+RWaukX{@ubA)~8eWcoGra9$fw zoO{iMVV%4vOl*&iJ6-tbsE_(Uo1SnbA+k2|QmV)#2(Gq<`{WfAY8ChFQ0g(bfB>f7 z{R~M5x@Ft10z>n_xM$XXF+i_KZG+GAL~_<3tc3OU}h zS6(dzC+XHDw3PctR9=poTuT%E)AVlWsR8^tIk+cafq zu{m%+&^ndd}q|3s-*uP3)5UwMUv_18x^>d!G> zO0A~~Rh7Yz(#sRt(06_EGZHF`9IK>Rj_Wfi=a>rWt>)ou&mEj`t62fP&;&Sy&w*}% zn1p1dU4DNheU*jm=>CtW0i$2ZoENY8$_{`h_drgDK230?%6Gj zGwLrtqO`=zp%Sn?_{f=IzMPEY*!A?zI>@u)*s4nc07inu84z1Hf02!S`ATV4PCR;C zf2VA9ErQWnP+JAwkE)q{sJf^T-?i$Vx4WOOFYg?YSecLd$bBY>5r1+2rtnXL0_6qK zq-=aDUroj%GzZ!^7O2SX+JkD4%GCycvj$}4K{Em;^0!$Cu`ds7}f~<4U%xd@V zzJ4v{bFeeo4w0Gwfx*@7I^$22D;c}T5HWi%-iH;hjlki%aL_QzZ2UzO3LNhBJB!VT z!@V$hNf_gd{|`6~FiyvMy3d*aLDaff*ejY(zN?gBAGb6^(d`5s49a@#J&Z!dU#BYO zRf?E!!Kbz5)xG~XK+&5kfftuTc#*z6$-xGUXJFm}IkNYofM~2f4*_GFP_G0*-|QMu zL$C|W2X*QM{Y1ixu{dv9GO=zm{hG4cdQZuc4)XAZdCp@;&4b*p28(IMtX<=_E-F*7 z(otSmO>&6-{hCS#3mPo$LTJY5QWylLzI@r1wcC+3HaNP!;W~4$H+mq8)MDO?RYwn& z$q!JFRn=$eO)9@L(6byO=z|0KJvzMHof0tBl4_~pKn?}D7;=rxB>L}Lz?%g>+flEvKsY2{tj*Ex=f1!8| zcV|QP**CvFVbJjFI1HZy1x2O~c>z>bY;|>Y{dL~r zWGqs_FiQi6Fxj-O+Gj3lIL6o47y3+-<`~#^(75s)7L%nk0Ts`MQ>|+~=qD69Z|&7O zT$6h3@HGrem|)JZ@zG<28RE<6mW5O*Y@;dzjI3`Hz(ul zaJpfnQO^^$BLAtzD?Ayux8F3hkD5Gcwe4dpd2A?-&BDf2ea&b8tx;OIH$>)fMXS;I zJ8x|Y@Y&jCo|vRk)XC}#E$*x#xFRq6{B)CXK8_%w}CvhW9+^X=bp zTrwD1?82Jg{ruixs=~MwsVh^M(Z)BeN9qakPvy@-ywVRT?JpR)?G=y0N z@M~s1xuld(cPz4&n3wjFg3#lMJn>PCj7kY40~ocHskNV;JU^vO@3^E$bR`GF0_#~D z-LhbOO9ujbJQb6#tqtx1TAN#PL!`;hU3(SB>o`*9^WTCUR74i?>MoV7CeqR;REQk+JpIAELt~7T4hl<`!M66G%mr#{>H_8(J)42=E}?C( zEA|(c_7|yT`{)|Blx2RtJ!xSD#qHKRgL!WLkWs%W1@Nru zE8kPRt3ApTCvTP+Kr>lb?vqB6d)CI~7Rc4&_e?zodz!qz)7PfNLRpvP_K@QQLv_fe zV$HIt-A@CB9HNm=a34X4Hyr)FLB|vbzGO4-U8w7x3&VvsQ5k;h(as*_jk79!Z4j3f z5t?f9oRpY47R?hQu}dA%Y7W?2XB1`o`h4y)n#SDs@Xywko0ecHPE<7OVE2!{1{ery-qKOD&8nD7=lLMLNQYLNV+!;X?te*3MlBZRO?M#dj5GN zVdh;&ha$M{&h}A0z4IYKz*e(DAl)Vvx9g2u)3`Yx`nCn!>D-ELDd*eQw7cI>@v7AjkACOIw@ zA8qrfIc&_fw#nR_IgbX}2_J-O^mFe)@Svxg0ZMZisl~DV-xQS!+~1|l$5G>D3u%fb z7#`!MixNHC`)p>D&XFUXt?gGkQgLM#92L5aCH72L|M1o0EQs3Ki#U!ObtdKrFAInx zy}cxG0jPk(yzH0Juo=4M(!(tkH^ak$>K^Z!bgQf#H(OXTdDIT5M5TYp-K6yJX$D|; z<|WBDTbpm2$>5rqt(L8VF=>rt>2AFT=~~och)6dCg(8s7G;@1qWrmpL}`=uiqXENnE-ID;X{Q?%g|fLR<*BHL2a2 zh_YzkSQt2hGOYLU#M4)y@ddQ4>NiGKU%j%tAFXcQ`5OjqHFCEx5Ta})=ny5_{5IK? z8QpdMM9h_f%mVf9kR13`t|Uu-`_`Udc2EId0X3qjBN65G+Vj4tPvg-AF_|^1HtInf23s|NiK~C5ywx&m!`H ze;pCXU{}45AX3@(G)0$(48iSkT^)g_N3?121__a{OogtszG6c(7|IxRb>hW_13!dH zMnbVip^Aig@(3g(ztu!;E<_Kh@YAY-_jq%6aTu09OCkO9oOE+1`2(<>zE8*Owx(l1 z<8p@$>vMw49fI$@zV_gJK%dFK&o@gugzvjzKO5DjaINFb*cf|vMOpUx+a-wVBZ#w2 zr+Q8;A|j%rqZ0tR>imYqD5*>Vm-mw^V^G{yk4sfRsBqoGA4b3g`W?x6LIG$G*%lxY z;At1C>K~d9@z{GECDeXOC;j<@*_tOx*JG2-UPRc9+!?5ym5~nb{uqS30ELXN#U@X6 z*>4z${DiyBR+;ZuMD2VU>`t5R0{Ok`SL=NkP;gENK~&k-ryeloY61#sC%Z-Yy^r!1 z5b3xzvRNN?uoZWpaPHi>*^+~K|2@If3y%j(<#aC<*97h{_So#M{S~#|Gu7mwtv9mIJPG*xy(#32ep&q{rSX+3!g6xH-%g@bH8qu?w}&D= zAuEeKsOffB6^xF>LqR-})z{2e`*KEz7AEaFnOhZ;#*pp`O_HJvmJJ8g>M$s; z`$ntji!Pd9k9EsM+Y42tUe|RJC!_4Tg4EEH8MEyY;Z3wmLS0v$c0W;Ej5L4x>BR@@ zCuKwHh^Ow=Bxo({Zr(h$Crv?^4XJIY_>2r@F?XnrJz%EkN|&P_^{rdC=;`SLuy!pl z`7f$rZ?GZrvn#c7&`S%TapIAmT@&DpHsoHPpsc*2rLa-`IJ2O`n0cYqu$FQ1*Co(R zEptQq#}%F2SF=gH<5R8=eU*9)OqlsXRK3(^)F(lu(nxQHn=IUwTu08DPd^Xd#abf}JJ=4f^vt0P%_i(9x2^wQPr40E# z%L(m8gq}Lh>Pfvvf00;|Vrw5}_+coQ+n5|7p-bEQW;IDVGF%h1)YyRmqN2X2+FBXA za>#R9h1yqQkanSGOL6bhe)N^oR6ylIQ-n@|&{H^^Fo`QdKC&|fnLKvKdg+EqVP$WL zs`tuCgu=;GR$ot|Kfkuegu^Z@ObN-8Ie3$qPuR2j=1ZW_eF0tPpF9D&V$V5(_cb7x z?w`V&<*{&BHT8dQ4PX7`s(YE(TzA^Kr!|rJOyq~NI;T9HGQtT_J_3}(uFbPDqT$dK zEyqS(oF>GLf89?t*PfcnN_RI2fuciB3kcMIYbOj0sI9GKFeKGov-Ah`i004gvFnly z7evI^lpBR*e#j(ok^GuCOYu`bdyp;e6^n`8-ZE8aSe)sqEy!EKyo6|JXc{d`ijfc` zLO+ePqit7lYJK0_C7)w1j)@X#jd;SZp|Czoq>A?WS?cx2HKM&>dUN5Y)_1ODr$7|V z$T+YsfGiBNmV4dtKd^JzT<6gXx|IAh0Iwhao{Tkn$cNgWU zhB;9Sy{0xO0^=)SXc@I^YzYuEm$ftFSbjl;2`ycs~>1=y)M6KSZ)|hNhq>8*d)ioD)Qh= zIChNr9Jx1$!g1Cz*_7MA z!_m*$vh~FFtEZYL(c3~oZiaib^q#gz>563;J5;bRT`yWoqfw@m%X0kE$ML&~m!;X0 zQ+p4z%&jo;hW*~yBQcA`^ZfQ)#ou2_?0(xL;_W`BFR-$+y?L7liCLLG_ntF_^kFB@ z*ey9?$4;WRBP-WuPJJXES2GH9P$ups-|4;Bchu-%y?dTNKmjgHR%oAY))WKF;uV&XCxW@wrd2+5b0#z)S?p;Sw zH0nkPW>siwEPUxMM(@I`?`q1pk70|>O0yEO;fvKL!WW-9Rubn2MCa2YWjo{=gOY~Q z@D%dPhhWl^Sncu~5(_ZmvcR@{RZj~8Fvx(^%wH;!_`+J(jdE_Im)lrNv|bD z?$ko6F1d_b3ghR~pNd;ZP#NKkS!13aAH&ZNU9B8nEDaN zJHiCzX?7d*y<&D@pXABR?a%2wy#%Avj=&`kGgMUIGv9yLp~))t;8vSo)|KN?!Vi|f z9!*Uu{A#eE=zZHUEc0dOH!CF~&-$iUJNq}L*J8qP*i8!*)Vim-Td!3aTc=(gvQWaf z+*>#@*<=z88_d563bZqmj6uW@aKHft+Q7ArUivMrIJoWXxc@Z)mbUXnkur@kVIb<& z!22j);Rzed>Dw9}^TE_bY;M`3w0?OOe$R;gT^lH`@Ez-YYV_;2*fJ8W!(~%Cd$(f! zSZzw&8|UxzoI7g|6sESUqq%5vyRyQoPsrE$il9bY4m2Up>g~IC#6Qg6cXZrUKhBi0 zV#Q^Hj)0gLEQM&E(=RdKk`*oGiiM!i?-etn;aX?RonF)COEx`>g0Tiu`A-H!1Hxx% z-zG-q&|cx~u>0YrB`tVDIj%Mde)9KL8{SK`y+{%t`U;wn6rFDVT^&?2y62FbR+|Uv z`H{1$9}8>r-@ok3TB(oNkG|2LCCsrU1%ElCXhS(UvN56Z)SkcocK`Z2ClX?h3!bx= zALoudeiNWXDuIU@&nLuJRDc;t#Kgpis~6^x+LeDD*jBbtC~J#;83R8fA$FjgDiaAO z(~5JMR5R#+;F#uv_o-xjKd zNJ>AfY5lD@ML~SciIW_#?4HO@CEG zkP>_5PMjwenmDNFu_p4-0giKBtm@^sY58hRe$npvjpkpJ&GQe>Ff?^bK=fvh#4=QT zFo46N{ely*ObIuoHs}RtP6{?rwx$PklJ(?U!Qn>5pEUQd-Zo9+LGbGUq7>zm1imV7 z>!0>ryQrskj;IZ`JBW0<)_z8W+pskJc&`h#q1!)8cao>UiMb9t=*p4=0MgkAcrG~b z_{ZK$QTupr17gVOK5@f{>XVe;&~L((i>xB}KxVOd<5k_oU!t^+o~=v;abLR3sZd}s zh?i>!Tezi3tWuCQbZ3InLwX*^b1tSeF&ops!BDu5ap54+>iNVJb%s{MFmT0quN(5( zs7!G(^PVkfH8w0U-ZjXWR^Mu${e=6>fb0F6XW`+NaWwHly*HI+pp4F74X7JE`=hqy zy#$;o9PY}DdUWaiamYwa@Jmz!j2vU-R_O~B1I@fOmIN*-UiEbwmbee8B7@+#A? zfhUkHWx!EmaFY=h7QSx|1Bn7UtkXjPqgJLvJ{2a23Q2l~F{?)%B{sdY)bC=mYsY0c za<|{iY~&UA(X$i~^tw6_nifc0i)BYanCsL_Rb^9UMk>+Diy4QHB2W$bj{09tZ`tO1 zD$6-=vaFyINFA>r4}sd;SWA&s_Tuzg6oK_gsf1uHPOD3?M}$02D*E{N*lk+Elwp2a z61=rJsLTzf{=tNZCR8Hl0KYU={vfAxlxJ3I0G{{-yJiee--BhEvZ>W(6{x(-L)rLvlhmiCrzaL8@T}b2#4hof^6`Me(1(r-{8#P5v+4b<65;hE zdGSXs?g-l-Vo}(t>hEaX=j*pr>yMzIqa_ToyS}5Y$>C3pWy- z)pJyr1_$Nfz8qM7DDwSXD!}Zl`JA*#xF=UDk$ckIoWFL+67IL&+D zhRqHl$=4(~mNuPP0JZ@L7Ou+QX@-y(a&NaD<;8TqSF+?D_S!03hWYH2Rn%%K8d|t` zclYR#tAw!sZ^?l>RLS&ansS1 z$+kU)-w-j`z)a|;%Ow8uM0j7dt=>!TX|`IG%3b0sXX8%2rxy>S21y47;9?Kdmo_2> zps)MO&zZ9TEh0H6P{X;^_X&(DEWxO0crPmsIzPlZ zVzWZ?<}f!M0Y=~3p|zyDLA*q%eD+EaHF8S=8y(d3r8o-Dz;5#MTa!Zo<*k4Qtfe>v z`r@dTi&3HGD=O%l$3fYbOIiBXUFN%t$?_MdN(0prT$WS8gVK(_(^O=q0@y}kjSX+e z2f%^Hn|UP1%Ak+yPUjr#7^=3ekhiA{gSkRN`ZKcp{||BR0nYXRwvB%%rHrU#mr*K| znN=dBjLIr2duvdkLPkb*Lm7!eRuV$lBN>$vNfb(CR77Ux|9t6t|L))YJooY3&+$M0 z$MOFheZLL7$7@{Id7an!x~|uBUF!`Y>D`FpB`R{ZAJkobhX~`bSk!KawVm9*#O!Q* zZt!_Je+(tU2+RfI%Lr}I+GZd~ng|A=umURV*Co5n4ZmfZIfWA7G!78Ub@@LCSw0{c z0zII;7hRaKm{G~5C&g>-{XFHPvwBiX&+BAY4%b!2%@)tE(Vd#qSubK5c4UXqa`bna z4HbYciYU97_us*xqa}B=vdPtrX=9ya_!SzSQ;Fhhto#Hte^45ezkhG6_(&tP#-73SMU8kFt&~CtDe5O5O!2(9U^=I;g`L#%bQM~4G&M6 zeO|(*u*f`K&3Z4o<1O{{v{2K|A8#|&{S@-(qi*anD9vG8b3avyNx+F7XZ`bv-7xv6 zON(kH525O5-MLln4gI~b79K1O5FyeI*FE&%Z)_db>fOG|s=uUO#M$~MGf;lK9(6Ap zu-`-TqiD^&fsBBfPiuDE-@Zy!ZG&jKsKuTPHqb`b@_w4$w6<1EG!M3d`r-2Da?EM% zL9bQwtXTsxyL=)f{G1t!(&Fvj1Rnqb09N9IF`(AqCa#0I>FDW}yQ8R^j*RyG*u+cNcEzB z6`HCtktavm7YhIVNPDAac-gHEEy`U&vx&e(gk{nJmgeAvb4#;?(}dKe}3zt-L~_L3_e@^}+@o z%y}tlJv+722UV%Xhr7Z`Z{7)d2%p4v6{iaF|CgX~={SM2Ts-F8v0$cl&AlK}9@8^8 zR`?a$4>zlO7WB))(XQ#Rncz`79sV*&W``}o2L;2fD6t4TNe;W3ckCWgw(ax`Za@Ak zhHUXCC`S6z&10#J1)m0e{e`JJ>nd)iDv)D!TU*&iiRi328U0(my%liu{H@y{U+IwD zb>2yS*6R9o0;X3seNq&53}f1$$8)i!`k;Y}$a;>>pCW*SNsJYVum9&(ex+@!{BT$j z(N-Inwv6B1#4u#-Vwl#*X7qdQwLCs{*;l&eZ^Ez6G^?JIwLQo3H>HXcyZAqapF+Zf zK!wgGGAy(5&9Mp?WKW)0-YZzOefV^*NZyqChi4@iEF`4Bvkl0p0Y{d6Zncoiz-8J06@GqexKCmT{?UbFtqo@=0{>2pcurnx0K zX_mAF12kBjIMqfvK&p#ADacn;=n6d^c8`tUfL|tUGaHrSG0uBCiaA$pS)*7nV!EwI zGp`(bK4b^k^V%`F~PRPExgv? zu{AhaSG`a>IE%z@mwx~|mgBfu`^hI-mvajWScY>gtgAzg(Z+u=a zjL;ugIj)5M@{Rr(B-qBL(KJ@F5#>Y@Ur8Y%QR$DRYICoJ4bYU}!8?P7I%sSisPy8j z>QV^jat^S~zE6fLB4>HezyR&qZl#T?IUD#7*0HUBCV76H(9yRndWseHyGUN*iS_G+ zuc%b@=F3wVK1cw^r<*>BuI+en!Io)biPQJZR0ns66CZ=|wRjn_@DI}^{faa?@+MIRB`%j_jH9Q z2bur;T2yO5-_N7xw%bCLGc+`Wu6YNHjX6=cD$&QEzD+BP8OtU3vd70dkT~~DE{M2< z4*Lms)2!!MMp0amG&~_4Z7F5;H(C8}NNLO;Z2S2s}2O`kSq`b!@PY10*XHkNxrM`-NU^tj-zV|_xrw0jG= z_7?KxH5@rfYAT0*i@8{4iYkjqFngVyozMxapbT{H->K@%s(Ot8LTqC5d=vn^qr8--aR{L!Vy2t_)=}=y*`(gBJ9P6&v-dktSG9X@LEsa_)}cERy~2VA&-~>LVU@O6J)$f)Kacp#{n+9Qh`3WpgQ?rL^fT*Q(-oI~k!RuL)rhQYzV*tX+3)O$9O%=;6>k8E`lBq2bBO$RBHS zZF8wESE>uDPD?$d)w z+fc{@MKeGeO-{O9*prYWD>r@3kGXeMay|1ytyj94EL1)Md2GmHpr~n}z&WwyH0SBp zF>AXl)nibnc}|(Rtx7OEpu5=K@3C&CI;PZTg^=*QPovtke@Q97{u?P3f@bMIZr9Cw zc@JE;zeKB-_20QL3;>^uK`aFZ2I8Th_$WNS>2u09{#A!z zmjWDjH!LvIMqW``9=@^o*-@~Puud;y#5F2tupfuIpYDT}^6QxyLfR}*Zqmz}xMRc* zB(A@fE4jVps@Psxca0rn^D=A8JpB}o3S1p69i-S58l3ZeJ9?^zYOXt-J39{vkoAv+ zQR+tWruoTJrvly$-Vr|%KzVFl!xz8Ku$f8n>vOKfy1rP6t?DU{e0MJHq!oOk!i(Z< z&Pc=jGm&rJLgJYbnJUubBV3OH|m1;i&pX zGhIZtV+3~=A7WkO(p@idn~5pqZ4CEm`r|p>y}#meXXrGvOZRSd(TuXDFKAOzQ#~=& zQo=*UvqT3wkicx*Qeyps#k*y*j3>U&EK(K@Q(O4?tPUw;SxPz(#amm89~_;{ zI#uxTK|$90)?=SOc7M+wK9>2uReR;?uNw4-Zq)uMcCr z4zlMj4a-Z9Cd?L1aqX2|T13`ENy*7{yB1L|5R*q$beu6ji3-1pj_FO!t)JB|-7Ds>cAy~+B^)GrF%Ue681hWMsS?s;vHflr~= zeDO_l&)>blO@FN4CIwnaecx@DWZ5U*cKVi(KtPIu% zQkk?(nen#FmMfZ%E_|Oy>>#)lX$U4g)Z4`m-Ij_94}3wUryu(GMMTt@lt8WgYXTBtbTwWVc5S@;1jqq?;+4QGeS#pAxxdiCt8 zRd3!fvIy$=9Lo$)P4>mK&U#zf7-hA#^u1cnyPRYQI2c+S3sS1LyVc%E-g1j}$Dk{x zP~M6Fb|F_ZT9NUb7TqZCzMPl-iVPLSWB2amdTz?6O4i@hC}(8$peeMQmXB=zSXFL2 zcIPSEk0-u|WV6#guEATJct$KZvx&U3em3XPbL}O9+68JfdZ)hA+zm{~Dd;~UM)r5p zSdwX(d*PX$6>}q$Q)g(T#OI~<+iXrsDU47~4`tI+LIvs7@`1@r&+OO!ZcWylySSOT z+b;5YN~;p%4r}opy-fCcuQzlqi`Qzbs~vd!XN%0;)Q8(%#i*~o#nO6he8+f>;nQxw^gyB=z4oCUoyjeW0Ag?*0n)b}bug<@k&j6N~LycJ}xG z=q)PTJ)Ll~ZZP0uYGj)H<_v7~>vd^T?T~x9 zQ<+!)o?4_pKv$rqKx8phL2hnUu(w36xH!#q)^`iuagQjJlyLg8+a=~I2c%mbS<9=f zHBoBpu(la?D6RWgz%D8$O_ZGA(n9 zdji!|9LhBr9_Oi7oU#6u?1yID8pzERxnH?o}l=Y@@KsqGBG!Mx{K@k z6JO=dRxO$0TPH^_w8!da9tj55Kd^7PeP_AFaId+llwPc?&G+%e8$N=Ai{V@8Lz^!- zoQV{&&T-B0&GKUqk31Tneq50IPvp`unDJ7L-DS(fvpBYW3D_1(@1q*t61iBp%U^JT^>YrJh1 z?`rBTES5f5GZXK0P@1tKwBpXREZdfXQjrY2`S1bL`y7N1gzP{OS z+nCKF?n1>xO*u^A@zacotiO@6tc_RsXT+Vel*KJOT!rf6TOB( zX3RQ3bF=UuBt0D zQxM>1#@SY=FX(&`>u_cb@Kqy)clqNyNq3VgeZ$A};xoiPPNd)DYIw(;@%EKmfIiqZ#5{?qk}GBFC)S$cvjEsnb= zJV=C7?j6fa)X7kHt5&a5dcE_Ub3pS`s)ZBdtL$l*qVI%zjJ6+q)o_ubGnEw8tXlc$ z^$OE(>#xb#zbH64yf`;?e&OAIuXl7t=MzrNNw-hCu*IFWye(?iu5?(sLx0_;4b)2Z z=eoFF+jY8IZP>g2cwe1uU$NR%O$)!1+k9>;Ea)bp5jD!3YE-<}o6LRlaLtc~nVSJ} z5(THgE3V#`$x{EOVb=-)8;>I@<`T-6|#u= z%o$Mp=)kF;r+A`SMQLA1)MrPEP`-@()=>TIqPOjvy~KlZ$xVNa2*X|P1zFq1ge?82 zJ9omSr>EC;KB|4X%K1lF``D{Ew6gV}rreNN{v=+z=U1fT9<}#QTCM3kbv}JhXjkR< zHm6?rWZP(_k!=oPm_tT}vzyzK@3|XZAIkaOSt+{P*F^DR`H$@{#TV!G%g*gp`NHn6^LT~$@X zxXb85^5I#*oO522-YWSVL*7mne<#;rsl$S$BOLdhD)t51G7u5K-10$KW*6kkku)Snp*Lua(_R>qG_#)J>*kSiXSE9H2U&zj7P4r`F5%AYs3scTP2Zso_ZH{OUJ7jXvKO6f^XU@r}(}MJ@pSi z85w1Jp0t1b7&Yfw+Pp5Itg9xnV(%3u*)_NNQiP5;{t+mjCZEb*gSE^qHc zi62&jauT-&tnsEB)?M8CIrp2SW0qu@%#8?+f?E5tIhBtNq&!|+8(3SMDtziJXYBRO z<_n!691UZO-a;osqc-bh=mlM6b)InUT?2LwpC$=D%}%OWiA`S_N{;6Eb+gGBYJI)Z z(A~zpftJTaaLVPF`N7@N{!d&3(&(H9nPQWiodXVJZ@=*SLqE-%A5G_0_cHEGVPe6B z^1}7g8q@z1{zaQzK5&$^zE^xOT**pJab6|K!csdkcihL{=h?9Zv*@IY#cSq8_NCWL z9Pal1R#dX_Xj}b`vcHH!Wjstx{e$_N+PeCe!$JJ*ubC}9C|N9N-J3NI+;)DheMr|o z^Aj)2;bw7H!R=y?b!AIiTG-v5oXlJsTU})MrY&vMxI$_sHMyacaie_N!7=ip?<_EA zu2{c$pVzje7qrcFUtb$hFUh$(;3k(|pS#9egJ*BsJ_YC$l~JD+SoJs6kWT-}a}SN- zq5`9p;GG=9PVTt(orm6R`q;{{iQ!>o;;x{O$|A!fGA+M+^55;g=H{T#;t-tUD!4Lj zhWkrP@BVFEVORIx%2B`(DItu9-hBz|OVQ*GCZv$C8- z$%)UnXHem_W9fgK_UNFT`1tzujE|g8ty?|YH~$arU^IpS)}xMb_EgPi&ZFB^Q5L$^ ztEyb9&&y+Eab2L!@3*6iizm{2!qk+OmM345lE!PvY-zsun_ShZjGEJy^4Q>!-C5e}5_{sDq1T+F6k%uW#O5k)}{iP+ex} zb&}HR#JKKSc*T2CQLbQ(sh*OlFIC!p0PZQ}&p$t#-aFwmw3-r=H!AkH<(YXgZoG*; zo#*uda%a6(vF_=x|C2pi>GsAm@9$~np!9rPD&(5#;WjJ;=6>C4*B%6xtcYn|Nhy(l(qY}Z((64yw1OU{+o-m8^}@qE(X4&=2V>w_-iT(S_hGiWemuqe$V~K zhmLBs*Znm#|JWPyn@!>y%~NqiaIo}oANB+{86)rU#MBVAXTJ9IXvl9UFbYTQTv}f9 z6DN$ne@c616>qqyHFm<{U%%abtYwEb3aYPAo#^j%lKS6-u}hG8R^T|fcL!d(##C5b z{H(eiq0zhqBuJ{Qz&g8KdRfL zysoXSP5SSiWs}B-*bJ4l(wjX?^{MmbrKxY-cb?&pie7V@EucA~@jIob++vxO47Nq}znq_)%0<$v z<)Pua!-o%V2=1QyGsdRiwa05_aI56Wwzqs@TL1gKGz`!|6zXnpHCsz_v5)Vgb@GsNv|g?*S~#|_(;{(fB*b9PsgdTQZa=eXTPxln?IwNUze?7qmB zEi_P|QYc`m&YwS@JZ58S`>d<0TvC~T9hMvLIHlLK5i?=uQvUFvFa$mI){k|@cHdwqSMkiC zz|PJtbk#ZmS`2{2i4#Ib`S!dZ5qB_cAM~7Tw@X4E>RDssk;0-PcFo1UI42dH$k~rG zC;>`|+vX`RDo47j1JcscoX?(Ji}nZa;!<(Q^85SD{mH7~{?IKsySS|9R}EuQQ&Wpt z73I&QH2&+e*#&p^)dvn7U}j=67rlfAHAy&*`mDX(u&up47!wtYlbc37I?aIKVJHNc zH8L^5g2&h;`x!NI{X;`TDLgHXuG>`)UJM8f48bA_QNJY^43>;da}6ymlb4qldFtl= z-``$s2)gO=y){b+^@#%FB%W$JOv$##m>k-SBH@A~vMfIy7 zY~h9k`A-AmNIjUpxUuEn;ls6^CFe(bU#u$~t>sMN+VbrQTG^AjSy)`6vR98~)?{Xv zhv=JKDp=3YuiKhsd7|f>zzXy}Ws`Bf*EFb` zs#yh2kW}G7FEiDIEsNcY%=iRyU&6gp;c>F6_y@jqnHZ(Ukk4%1YvU+=O zpN^Bc-i-U?&tSZjB})1IdGplqy9+y7m zJEXAjew#o?a*~t7s*sSIN5>x0dw68#M>etCFg3gW>^52p^G~UKZ1ZXaTRaIh26CQ0 zt+Ts(DG-LV8(TbzF?=BQ%fLX;(adAKJUkTfSbF@a=H5O(=ij|*8dp^R#3*?0y)AWW zHOxBpU9!{X^1=(FNh#Gy)ShFnrLl$gXPcG@bFYpaL|18h z)L9?J;rzW^z`^4Ph*7{%)X|VLWIK26+`{x{ObAEJ z$Qia2jp%C$2@xE-9LeaVBe{^+dJdK0VJ7{rTI8*zAl0rB+f22?>h-ygYVJ+ z!h8Wns!_6)fkU#=hiX|^M1~;~@A8B_7cV0M@LZgq8kw3p0Ar`Y^SxgmhhrAz#*X~_ zT(L>|bcf=f-`|MwTb%#pJu)(~ZG5ISj2&xnngY1C-Pe8<1=cawYu=q4V#TphB@7!e zucZq!KkyVgT8ahrAJJp8!1aE?!9Oj*ThkLC%;@61@6D~^T`CGFdv^Kx;SOIob@*go zRx{7sw?E{Zzfe}tv-!Zv_rQT5tf+%^7IhyVSR2$4r1PWnR>*r4o(({yPm1}-U|VkT z1(LPt%qkwM=`!45xW|1wee?P5qCER1 zzT^oSMe=VrfyHJrjDZwotJcms7K)(>W#E*eg`32@);SAPAt@$dXeUxOe&a=tyv4KS z$yPFblV7Tq0Zs5MOjRu2QWLIbeFZJ$1IwnS%TWlc}_Ts^dN>ZMRNo)~NQER`aB zaR?yN^{6NT3@6WRNe7*09{W_RO<*wsGzBE4hbG3y8_tL2Vqvj|b+d7B2&#s$CF(zM z?pD&yJeIXKc>dzIU%#9%?NE^ko0WI;o&^Y9foJLK>8Tdu(l}V+HWbzxi-|y6i)$~w znOoD>btEMv`DbQkPNU+fI^WO!C^SM0=h!7~WCxeQly>BI&JC6 z!jh5_^ij}Q^{ZxVijyZ1Xgw~ymj)c&Z2X518flrb0dBO%_*Vd=d+9oWCE?k zN-oVlLG*|T1tw?RdL%)zxC0gd6B-kTTW-2Eew|E1aB%RJ0=mZdIBBO4@}60F8+I54 z%})K|G#Q3TRjqT*&&yi}SkgT83q@2mtPW7(l6TL4KDD1VqT9XzfXg2y&E-ccgM@8E z<(EUMji<(6_Vn~Z1RI@7M^|uhag6{Q zu{g|W@-8R+VNQN}!f^fibwDU#-}<{|WOL$l6xk*1d3IJn4;O|CKHSq2%ypk|6gGT( z|LqTebwLFC#3BN!CR^1-Fe%+s9B)iRCB*4Je=J%b8n)$FEJFZ43ITXE-iQ>-!V5Z0 z7JJQ&9soyQjm5Qc%|=r2mqNLMo}S4EuBtn-ChSHHCt6zC%Cx&)A*3Z3(Cu_@V|}AwGZ$+rPa> zC2;7rKQ7Td)~Ilx%zeTc&XRuP9yXZY0Mq{K{0DGQQRGXUQ+d_fL}g+dqXEB)&h@{A zebY?X!;VU5wdDP8?>g}nkrtb{^})mshPjsep={@c1B30Z1vF%4XE&UyQb+@-;LpUi z%W-`ax)lRV5g&<`G^$`^U{kFoi`TAOCwA(iBt}SwgH99|0MxF-{nVXB5=0O-!FCci z9ks(a5GM^DnwD{37bJ*P*y?VInfa{JYFYO)xiUQ@a@KNoB_EYw;GG3`O&;rsJ!Ux(}(?l*)G z$$4tn1l};?A!Zx9mJWT|{Xl;hi^_?o>ZTno^j<9Ar;`H=Fwm5|5?2ieo{lZs3tKqa zQ`ZA527synEF&XQ97&=&;Pny%AxfkJIuhUa8>?-tyj5ZpG~@p?LI$zXW!bV2jL3f`F3yH8trG&^7@=Qc)lreDmxYleCjn z-M^8iA*$s5<8Lc^F`#=Hgw(tl9x%f63Z|u5Zt)u0tMT>>^Iyu zVq^}Pi@!JL8|~;lJJzMFp`qay85ub~h2_uVZys01b~M|R8wO2up!`xpj2H~dHob(E zl-tryj|edO_!Gl^gga9<#yMXHA`?-gjcjbRA|fJwV!R+>uEETLWuPcxD^Mz-0R(Dy zs+gA|+Zz#5vhC~J7y9GRzXe+2yLwSI@b^N`L*2|{J7i>K**D)E{#R_TG^h z(NnDE3dGa)k{tA#_Hw};J;$y9*AR*@um0O_zKM%RYGXM&2U($OfXo3nO2s;GUdEzU(V3EaVXtWkQ8H zGAbiLElFTp$ZkVR^2>m}(c`E0=+Dj1heuVw0>|W zNQn?ol6L_^s_QN~2m8#Vq?n@94LC=-)I2zvY3n8)usTY*otA>z*ncjU$4`ux+kIWE zn|Y^p_%Jb>fO6(`c2|hM8u?O1oXXoQW9h;2*vw3!o9Z?%el?s^6o_P>Kjm`noHD== zv1nz_1OJj`E+P`>0F9B=i4WtHW<=nkiqh!U$>}UksV|?s7kAC>lM{@AR;Fn-7Soij zo5o1+L26oR>eq%?37y8R073|i`LG$cO{m;79{N}y=Fq&EIJhk({Ef`I75ZQ^5L(D# ziDpNi)Kpi026Tgni-Dr3r?>ueS84EHf{)#A1%nd8-lm6!64lg2jN~Tu`Qi~O)d;SG{{w=FyRH?3Eh=Fm%1;$CCga<_C%!x{h z`YjcNT{R$0bU$`0MBlh^!yiC;pfxMb^y0B&QB9KV$&#oiScB^R6E8t3pB#TGCMfuFz#Z{H4HlOMe{X*m z+=2h`ove2~e+Qcu4VSBRH3Du2Fez=`x;6BO!n_`-(qxGpWC?G>Cn51}ZOKSSk>{Vs z=n-p)rbq7I5HO3)e2SShZP?^J#|x{zlgwGM+mIk)wBwFvUip7f)N2d@8<>QiZ|MfP ze6Rp8oF@jarBh%>Re7OtlA#0G;vnSHOo7hA4{hN`v#{PeYfVZOm9@)k}m4 zV{A8gy-EzrDC&{36lc3X4kOJWFsbN7yj}I`##4jN_4IhIeT3)&l(Ym!$EAV>ib#6P zqWf7uP*|9Xme#eD>O^X58=FLgizr;V2ksGsh81}*R1E>;Ux&#fxF#9H8pRmAnw-zX z>gWNgAujJT6Zh+@_Q$Y4EjdM*61)3~B+OI{z)BM`*sofoP?xrp<^xOZ<*nrKR8{-<}zCWxaJW3t=cVM9TaLgYv7ZOjF8;3LHOn zyaj?GLhM9_A0q-IY~VWdzR5v}a{aX$sZH*R?qMSsH zZ%BsEME+%$lvCGn$7Mr0O{dx)(~uzm#%z6L@@l2n!%C84f)VY;My<(rcqh!w$f8K= zi$;vBq!X6cpKq)YB^F!C|MYRvbIYvrj#MAj~sA8tVe+JaIXw=L00LeC5g>1dVM^o$fZ} za%Y^Bb^X4cykVje4S7R+$Qu%Z8RPQo^XE-Kf0Aqpw_hf_iWcGDwvF~|5f{eV57AYM zvY?~@S5^nc3rl?^V7!k4*Rii)h@au_VjvCXKnb`no6u>`M=2z-PY2#-vI2fKQuSn2 z0v!8bsaib`V%Gd_?ECZQ$5<9q#`Wv{f#O@q1!4SZ(76ovV?fwb zW$q3%Hd3)T-$;EOORt24mSBH*w{5E*FgVM9AVLLE;tnSESW7#*l`v246W`e{dD!9T z4vEQV`1q|z>h{|=w7T2uGT2;We|b9oeAhm6^LsQ&{8nI0v@l1tC)n?*cU`EFZOPJawCHG0})(eFpKf>OO!EQj0u_Y6BALM;e zO7%S@!qsp{+MA!1tBVoxAXT!l2D*A(1cF~BAcnmM%p#Bmf-u@r(mH~eY4k^ykd3!e zz9|oekD0_ik?&%ig+02wW={dWe&}^^v)QkrH%ADd?Y=geD08@7Rk=?zitSCf+O@|u zNV!+eTGqNp^Sv4crJeI8tc>|^6e7YbYH!Z)33uhFb!=<~=J7{?J8vfOo`d2dr}DGv z2{kF|pE^s&`~RgENVsgVwR}m=@vr~c{$I%*{y$J9kVE+Y_W6G&ZTP>u$ip{)I%oPC z*pZ5(5JN|D^++B#B*1GNFL7fap(#YMRdNgSQ)b7DUDUEQ5so3w2?`Ew5^6+|4e)o! zFsAA6^jAa-i+!}Zz{uoy$&$hg>f~FSThm5qxMkm4ka#IdFhqOsvVA z8&oc-T8O00_)Fw1%;1?hP5tcU$;yV+69oWm(frotwV^9PkiYJowY0R>XHu4 z%W=%(82}SK!gS0k6PCZPYH}4GvwLtbSkCqPJ>;c``Gr!LA?uh>mes=CA6k0)mJ^jf z+w)c)x-Gd6cwl^O5O%&aVvfroveay_(fqTsMG*S1l5AlrXzs*sZ`^O~d+LzjKEkO@l9Zc<>{lC3pwXK#C0-sgh_GKu*I3 zJ_A^FzV80=h2rh)t(UGvg@r)oQ{xwfiCO;2lG-QJ6(r^z znKRVD@HK=+21p=TFM_AxpGJ{XA*;?~@~3ZTDBT7W-GF^tMgen*#8v7CA8fPAYrM)@ zpicdMBWGNN+WZvK1{!t^j`#TY6#~dGlO;h^DaHRe=dx};*>G;p4{qiTnrE1K{n@g2 zZbTW%Jpv}MmJ8j6I6##7eEDKdxHzEj<&^qwXGtsrh!^QD7!nlZ6Cck_SaiQhIm-6# znAq50#J9=7--P)F;Ws+a#7}l0aOv6Yn`j_gq$8R*>jJFda8O@r-oLPP5v(A2p$J~~ z{%^?kG~=Wgz;@x4sVKzbg0r~Eu@xF2$bE#1T?Q{>AE+JC>?*&+2@?8)htXqXI4Kll zWHK~&Mkf;{5@Vz`^!JDkbdkv5vVi-fD%#UOnWvtjge8rX!`Ki6(gf6s)-!FyO)7yYi?WWAx$u!WlH z9@5|G$Fgqk^Wdgn$w5Mecf9Q9N8%btwSe7mjONDCm# z^OdlB^98#W!J}_-GC#>rBb6|?ue)?r9o7ml*hI4sxMouX!A8HjxR=xb`-Pa9Srmw` z7>G^$@!bWMYsEV+pdX4NV(yg3unI)8fCt2b>+q*wXQu=6VVH6;1)x)pWp@J{Fj8^~ z3W#cF6e0&pFdz0G^S1*O>R8VHhZ_ig87*=ZIWuxC#Lca7PduhhBcC!@@PXvB9V z*M)-NsYwAmOV>)IK9+6D09J>dT^!Wp9D)La3NLw#)!Tv-`VL-L?~*54>5*PxwY0J# zNivOhB%wmoAyQHs)(L37{q%;!lkfu;U_ii-`(e9AY?Kc{FSN?0rregv=)XR?-e{@s z3?Ug2rF)Jy9Y_aD4&h!f@*Y$ajQAsIZ@_mF+S1T%xQ9SCCpY){ODyGZ#5?S;nMx47 zq}YJ1ia_vViFHM&Un~(KloLK)$V9nz`?itXZVi=jt|bhTzz*J##K$vJzpg?}L)8BziHuj+86@3t?bWw|mW)U}Le*jyYK-;vK`&Ou68iz`A@mHXVM#s-3`io> z!eX8Q--6g!&*>x5G-A$Q6wAa-jQ3zb5NYdN1Pw~44!CVF3&)(;ckkW}Qu1r$k-cJV zUchcm2!_9Ye4^m2j{Tdu3C3qIB#y0xl&A{c^^dPejbxC`JkavY@P%8laVud}d) zQJ;Aw@cuFgC^PmzB;)JrOY8)3%TclN9#YtmAThkZAY7oFEp}v=J-b=XWk9RJW9j@I z>>@XES47bO83w23Iak-3?&>QCv`SNH@4S3|tiYMcsQn#h-;M9d;U0QDe$ldhMsmRL^`7*$mzPM=3_dJ09H*FVRF z>0bbm{J}cDH;U2c@38*!|A(yqUr3q%FQn!FvmW~Y-;3OOfD9)gJ_K*Q{Aq)v15y!S zw)578J)G_+y||8wii#*5c5tuO&M+)==EavumRRG;(&>kklL+&GMC!0<90s*QI`c}M z_V2=MLek7DB=o8|aeXLB-{bnobVf0z98?!vu=~^TH0qMakF2LmSQ%g(AwcHKYa<%` zq{n^8*HmK7{*Lblv!Vof*Kd>BuC9#)!eOb3+BUF3VtD)bXTk&`eL@62klMl)C?mCD z>OQyyGG1z{h2O74a!474+Ga)u(;xLH8JNbaWPl<+TO?USF&xry8j9)||jA$g1>E&fUp6(A?lPElup zetHekhL@qf{T4)r^sxN%=g)a$W!o`-YHBr$!gps|A3FgL*APwJVA|cUCBMq^Tx3*e z)~Tr6PXkJ$RxG~@W5((jivk_Q9J8H-cuU)5=;Qm|vycYw^Bqmtd&!6#Jb2|f@m{~i zO<4VMTwEN52bJ*~Oa=J)l_^Y6-)~_v9EDHx2Qh0o@HWx*wOPze+^!Q*`1^|fVDRE* z-tz@bHduB1DC8NIU%1NQNVD(+%xAFo!k@H*uV$vF2X;i>xg!jL z2YuhUbLS+4x{$sN2E1VJj6%K$?Te)iY`;DaruG4$YIi*Xa{;WAM#RgMuhC{+xh~sk zh7G%(K5KR?;N#&@1T7W-W*YArNBKVK)J*c^-whGMAa>u+Q@RwFdX zb$FL*X>9j*5N#|zH^{Ss93!G%Ks}L^`k;^qlLu>=V{_2K6M+}!{5YuAN*hr1VMsp_ z9of~5^3RY=N0gRwaTf;zR9U{@IjaGSH+fC-f(7+~qab5|T;S8Lxmp0;FpgcTVds&E zyOcYu!15Y#{7T;Cz_kU`{W~=0ksD%w3BSxlO{6cxe`0Da;Nq||E=+ZvXXrXjy48bm zCe990!ev-`%1D9{jT|w`h%B;8f3HT8kNyc6cXoYnMP;8ON%9_|0J0eoy%1c3s5LL@ z^?W<{*RE1vVEp2ib0P5^@d7d2Jqhw2q8CQYv0#Z_dm3yWF)u{l29qL-_-T2#s#Q)0 z;3AdJ5mR#bN8WDl%m6>eibz;tQrhHS%^ov=d4V*PZv`V4E#$|3gG83{>8S|lrKZx0 z=SZ&}Bmiq~Y|;4A*B6M0(Hv90A*l2xN!8*#piQtI)LK1`mHu~+)?+ojKt;)c=!`r&pa3Z6O(glU;{&7{ z^jtDIm7`31=f`#L$a`!6W%~@096A;umI{CW<#T{K@Pzx2wxGKM>yyU0=Ik^7RR*OO za`n1^*@Lrk+En!i)1BKN2<_MBExtE zp~`@(M++XJbgIka+AD^;#wS|RnTbKn$q^;_N<5gvi6szh-3cfMqR(bfRQ)*|LBIq^ z`(-{ax|IplNWxagA86OCxq{f4au4>DWN8t@2O?S2gVkVdT(x@7tN!v`L&jLRL>vpF z{}ufh(2*(M<)DhdD8m6hp(LG*9H_L3$s#%oWKKmLTefk@I9CqLL4_JnQK5hbUrCw9 z&57oTq-cPkmtB)^yMwR6Rs~?lJ!b~fh`a+?+wrbIwL^z$q(6K5K@Q0bh-3dQm4T4Z z(64?pFTs+9AQYGh{VEVFfSNggn`&j7$|pP33=mQoU&giaUR?BNg3Vfn6*AG)ppKjb zuIG#G%ntzo61deu*`=3vooF?X6l(@5ot~bi)VC|}*l9ik1f@^}jwC#cZg}y! zj|dX4$Hs2I#aj0#R^P&ll^k6Iq9eH!?3AT+7Sh*{x|=w~gKM~gIU(5}zo9a8iX`u2 zZ=YV354mYIKm!8r_d*JI%-RU*^FHW_r&S z-8?BAhFg8b0SmAgl?Y?)UBXzP>^vj*e`49d~$kd!JCzE%(jqxorcRWYnOmAChzXC3#4 z&R5gGE8{OA!wN;hXA{XZU=o`i92FwD5+DnS;)IUT&I<2Muugk{w|%hSHHOr5XD9PN zNlabn-GH+XNFygU{C0(OMQnIXMt(}B6f1BVRhMj^I32_gjG59F@GPTU?LYo`m?2X zt}kI3kv#NYA1=w5g_8TFd1pYD6J@_#wr2NKv32=}0C`qqTba4I!bm_3=1t)0Chb`B zCPtIs`jg<_vatk2S_~Zf8}G7GRFEWKlRbOCYmP`+m1xCmUaqH7#ElJ?Ku`jtm~_1U zV9_@Kne8eziPaRq@=JSaf;!a{T7VrAL~;s4K7Z?1d25F50Sd`?iOPY)D5LgC*UwJS zb6Z@vqzy@WWL+BK9otPOIxmDF0@csmk(^nAEbnSwe;EkE)_=k&p@y!A!w?FPLtzBS zA?vtSi57v#Rlcj;Y%DA`E)S*f1m$zTH6nYqZkX&~dN$4$0k^XuAt6Ea!x)l!v0=P> z_nNwVZ+T(ec?D`=@VR9+XYVF|_L|LVI>Riam(ByBiD7z=3=SAjDGD1u5&ryVwXrug z{yGX-TQ6L{msb?kEpkeqzC+<5OjYu$0WW?^5Y!eKa}JFtVLWd#J&=4=v(*MBr>0_Y zK`3hsVH~v&P7^eLItES#2_{?&5@v|^n&P^EdZ-l92FXBh7Kr!IX;vR34;OVeE$fsC z!>K2MtJfhertkpd=%?GfW@cg8VqVJ@0mQ&Ud5)x5Xns8M&2*HnHLPaB&A&;!)tCv= zjE<6oSX<|l(iE0r1QTEX?K7}F0@=o(va+S0bY)j82vUNOwf=tU2jGDFhxQ_e0wZmt zo^eEKKQ!frxp5q84oN@h*m~EK4-yM|$-cW@Gipw;xa=y5<=phB>eG}adyv7|6drOW z@0N&Zsm$jSGk|Vv4>ednAJ}whGwLHu^erOgIM(?zt=-fj;>sI{NCjafnrynTt`#f4GK#PwuB;Pj!&M3j z6-wT#9i#hGGoz8)ecM}heTzmEn=mXf4Q^KGg)1sh^>&=Z;o-YA(8E3l3tIy_5&&+P!ULW+ z{pp>ZvG+<8FG6|z9Me&Q!w&c3CX4-(fs3)kNVVs$;0XBShCmJW(}g#1l;c1K2`eGVS$Zwwt6uupvNIM(?YCsk-EM3_uO|h`6jiU}vtthpctNr4B>%x|Y=w;7qRGGK|Do;8!*X8Rx9`h5E%TJj zBtldQWgeHxn95iz2_=#WrO3iEm7$a*LW4?52&IIj5-OF1R1_*16H$iubHx3--|c<2 z?R}nqp0?j_-D^=-*Y!Qm^BDGhKlT&&dOJ0>qKA-dJ-4+2t8b^+26x_S{7z!@PIBQM z;?6EG(GRJ819o!a4nHYi%DKLxb%zeB-tBds`69XLv$C^}waQSNhdpp#8$i*i7PtQ0 zprd1)yj&;g4%?=2Y|Y)fciV9wJ18cO9qUGWI_4&!Ac<5n5zb+1G7p)Q@3r>@fOis; zWH&jaNtX&Y4FK;Ntk{_`x@M~F;rNnL!um~`teaqT5Z41_inoD6bIHw}+l}NF z->V%G$rHJluG`SO$$ z6*V<|gPD6f51Y=J_>)@El&rA}X4VwF!B{S!yfU+9<OkolHS--=>e4xw~X0swv;w3&LojIi%usy1g=l>uldU`zurzN zOpOtr|8CQEZR|qck|CjMYL_va7RuXa*$s0&uzbtXf){PYwmUdrPWsbvCd^+TMOXBo zoD7{anwx4fAxERBp@o|e+)DsWP5YNq8N^ai>;x9@bI-`lo%4-jSoGyXvFvn%Cw`AF zoytEJn=+wsPZ`+`JHw;uArswU`9B^l4`{-e-3N$Wt4(G{(vy=X$A5mjvWVuQHE=>3 z;%GaPSZjr-9{zaT_3Kw83LmF!C{5Q@M`VjI&gQ-V6Y>~e2~w!AZn{+Wta{H9nkl+WncwmY|9V9z|}sAe;Y;ax~&Y}8{sxcP3FM^J2oN(9a8iVN$#9uHE)7h#7<_H20eoiOrj|M zBP43x`N;;APp-P=Ev|-Qvsn`zYB=!us#4!_)_y$xvVf@vjvb2}rmmx70{zr%`}m#+ z8wYz=y?!)X(TYegITuZ_)#uNSsS~<)?;bcKHR1c`Jk>*~N7lrzd*invH}a^W2TgO3 zipSek;k?gI@vh-Da6aQ$)hCM=pxQ&hFc~%G^r-mICV!euxOwN!o#V5PXReT8UJuop z-D_7j{%hp3&;@g737Zd!fO4F~CiF-?e&uOJMMWD}2+f^>fsYR}Zb~D~M0QZclaf-Z z)75AF-;Te87@%SH)J*Fb(C)^Hud2nnjW$!#sB6>-Us*rx@Q^?I@7cBMfM<*xHI$VH zmgHZy3tVGocVO>?x2N75d-+W}$-i)W!{*A3ue~o@*>juB@1g3a(PuXH`lgcUf0rHe z%Ndn6ilAoom`&P1A+a#*HLY2Lf|+WiX*Twy%C`-IjouAQLMe&9UQ&}Jhf4<2)Q{BI z6AWA~*{;43&0DQ|%c=K*Ll-h?oW8y|ge#wBoC_3f<%6@)|I}JmbUI}?{AEI9F1?0# ze-k(Y{Cm=VOvde?$ZjgPIBtpd#FgpMB!)69k z=^08BoYW>xACUl?z$c-2EYLQKQlUT?^3dn)n+($G$v>O3&pahG-M!qscL3ap~2c$d&O3Q=^zQuo;IW;MB8;+*Z)tZ9YHQp-2eP`P=x zk#nQs`;}ay)UB_GW;QDDPWy{j?Nb&#d^d00mxMu2J~!PsvZ-Fjwb=>Y=CNyE)Gg_A zPGv8aTn!w)@0oE_uRTam!)!>oX2{O{M>=J#81t>^_PT&w=2!DX1Pvx_F*CBfvkL+! zCI@tUe~?ZT3S=Ss_wVPyB82?9I@cHYpa-Zm;@gJbt8JfLZcPjleI9?V;+=bDK}TrI z)^*O9t~{H5!(Hc;%x_8^;KE*5zV@(Gzpw1`naLs5Bwyd!sEz_oWD*h;1=k-ak4;zF zh>GC`svwa|$(#pp(H6LAhEUyQ_!dxW{$}pDB;{?VT^D+LdqH2Bz^SM3)%ff0>q^bm z#y%jto8Ro?rrk0!T zy!_EFc#wN$&QGekx>V?*nH#?8+wdDVZV0wZJ;K)QP>b@tXy0^u<$?<0{E}U@=%&*W z8FL2A z;d)xyw5Yq-XHp&4Q*6~NV{sZN;BB^>J7FC*bk$>-_!ai3VfhvNk`@A|h?Gy10C230 z=;H(|R;)qG8~DMa(~yRs7lZGQ9+mdFsr|^kU3C9uHU7PG$8f%_d2GtuF?=+3lZ=u+ zqyE>?dxwsvVULn-yJH=4KO@bWe5zwi{(Q6=9TPshx%=IB_mD9RpO2+%Yku4D%`1>T z(wg^}G~LApf0d88bYoM-u?~s3)dm$`%F|02F6b4tHB`G6JMTZ2nH0rh)Kc_x#~kdu zY7zmvTaAB89obMn5S9Lo%uLO~Rm`tutoTZUtWfZep?V1fcP!S#IE}VA_|OVWP(knp zY-r>m^HVY`M9O((W&@FBSNDcxaHla3p1dSMy9uquCU?Aks*e$?11 zbPIPpDfD3Eu->h(BSI*v+j2?}eJDlbqPjzmCv3jfkRfRXbIU!ok$5)Uexr2cq%&jk zzk2%kEL(dF*ixY}_nnZ9=u{RX@t5i4w*?90(Je#*3-I%-s^06rxBK)>dq-GdgM8hm zZ{9SAfDXL#DqSZ(wvhsnNF4<{bv=blf`Dm?)}J5-RWy}b z?f$3(-$vG+BZrogE?Vc@UYbx6va;_Or;3TzB_*qSH|rU6(Q;Euo3?Y9(W__AI&6EUg}b<&8qsK~nvH9TsqaT?7z3t4J+8-7@zFGl zjkaz1v`X*Yhkk$LR}EqScK(^6g*QhZ=mTFz7W&!aJNs~1#*qb^nw7~>ZpXL^ak{}J+d?62iFbQPyH=N`y+ioO=;9Ju z3n5bpb0YAqr$9XP9&jj-K6?A^>nSOM7Hyxey52JjvJ3r@w%_Zt??aD8_icfcWAD?a zi*3+W(@Y#_qldjfTN@jj_YW4>b{c5U?^RqQ@$8~rI0=e}meyp8X8Eg!#zv2gzh?V) zb}9?$IJ9B@*G+t<)z4{~pYs_NAR>Mpbf~5*>07C%(fl)}QFS~Q%qq{|eJErI(JL=^ zT6WrzJP|g-=Yr1n;@aSXf&v*71@H*!`+9BPI(wWq`TcwU-QT(;UaRN7{g?_6MkygM zJp8@gm-l}m2T^PNDA=&0#B}5@chdSi&qdmP+gMv=J?HT_h_b%9rI!9>- z9zJ-Mi0lE>u%FNOIx`m+sn}k*1JOdrxq*$vS?5pIQ>jke%69A@GGvJ6+UHZS!wE#( zumMU-^s-di6Hd&0N}>=lCiU}C-KuH3?Kf`M?G_iOeW^s%EN_^OPWz67&9zsW+n0K( zq5+zGYEHOu69(YMH>RAIQe&uZ-^6vN)rfn7$fg}zw482ia^Cr`nvU@hif z->Fgmv%UKd9cpSYosE9X{!7AxIS-3xDaE}zx?@KpFAw98+AWQ8fk)y-W!Dv9UFv#s z3)_{WJUGgA-BMRK%-OB&tMs!#|XN$lNW%F9q?*w3O2aN5WSqZpAK6`>wijRz}9D6C(mv z7haW4y)tZGeymTGOVbuDl3AYC-MZ}s$*=e2UGvHL(+w`cwzvi908v!`v#j57jfKPCe)ulk|OE zKO%GH$B5%Y=f1UyjXFGP)TqqMb{66W!tR@Z%?CpBdo;TW-Q0B9fYgM&6Jlrk=ZAI3 zk%AKHT680{J4qyI!2C@-2EpTO>XgMoLQr#Y6DA`$_0F6Og~8xUX(5R>A0;p(&jy1KfBLocY&La9}1 zg}l|5B95?5-{buBTu}O!8xPjcaz#QTp}hI+J*P{$%%+A{&i`ujHeqxx!@udmNBRLvH@%fFJl*qQ3NMOVH03g?b8CnkF7>azP`ZQcCe zTJo{eoUgQYyMIeNr`5bThp^0wqLITK%IC|!_Pr`lK;WIStDRv=LOG&W3q7(*&rQm0 z@DfzlefnVQ?>Z282r0ST?FLV!`GYGwbRrfS^X%7yCcqqOf(|NlZ8cwRY7-S&Qj*Qf z48rq?*>>q^ww86mpiC2f*Q4OV6a6F-<(KMe1GB?2X2da`jC?Zlt?{&I1T%1N)FJg1 zq<}v&*GIN;FuT)X*;KEh2UphHDr6SfWAulJ&Lv4{X=$Ysy0FX*gtxkI&#YF|dB6(f z10V5!VMT(=KRl6HL?FmI8m?*amUlYH;1Q(i{%#fJ52JfiY&UaFTbLynzbrVAzlh?y zjqj%C5IS!CQIaD9L9OI&k86NMe{CH2NQ?>h5IT?Oyj+6uz+&npSmhh(>4OT_t$kat zs;SY;jUC+%YG|$N{)}Lm_4V+u=z{^!j@Ort&`ZpKo=q$GC!LjZA8j0VrLWjXg?Y@r zZ$EbZTR&&7vu8h<8S|(oXY49^^(dv?rl_B@@5Syv5_xo$O=)9&CRuc zLnetJS;ijV0w;mmx&J7x7X2F-L{^WNdgP6N!JHMoZ3Og`>+=2U=iP35a*A94{XiAvdiFI;KSqhw=S z;MyEZcFMp9(+ueQ??G})BBs9cDll%iwoMcsSatSin?yw`IhG{8tr;uCK+?Yy7%WQN z{n_0{pz_duLtCtbs+b9pR;^mO5zk(!pT5%Y2mCcsk;4-Z%%c_qSf72T5kUtwA=XapW0kDJv0^eauHt=JE`t_%L5Zw&^8n2Qba()SOX^)oy z2=VgJ1BbgVpXCD>w}dLd$UQwh{RWU2?RuTD{6nNx=W}xd|7=-jtjK%NSBd|QS_?Y- zS@?*+EA~mvmJ=U}dK*qG$H$-BwGsdjDyMQkTjQ{|%7n>U_H8C6EoycQ!aVb;>v0 zV}@43`vaq?UH5%K0PF(6HR%H|=WhyeI6WeSDB-kU6AJgXqXSx(LxA zF?xb~lGu(EQG+lFbCce5pZbD}N~2N=oy*nTl{D)jWx5OwhJRUIKzjG?W4b}j=XQ?L z9A@Yrfcg#?;2KspAb}z+3fq&3egFfBtmmD*;|85{`x*B*?fa#-(?(jDO^C5A>?VC4 zu(ud>Fh%;5TB6nXa7?ZZDXD3JXPJnLXWtbX$lxl}5V^ z7cKX-YuCm{<@!P>8|KBL4R-TCO0)77Z_bH z4>Qj^igp>8M*CqnW32d1&!QXqaGk8F2@om!1$C!*rE&j{Xu69TJ7-X);T>H)Jp)^B z6Ra2)|B>({H`VgQ2$QamOkyn~xbTk&5fKY6wSbmD<~Nb?3y7DN{Jjg^oX9PrQ%wEx z?ANZnLDv2NL3IM+4p1Zo@d>r+76^-{EJN;<(d(JP!JtECWXRv+Pv;a7K{tGP-;>!! zI`c-gzEig6UO5M+MtO|JPC}?hnPgyWa1Asaff-R>J%MgOJThC1S5q3Y><<*wEns}0 z`|AA$O2krxU*7rdqJ#Fwfe^%;LnfqE`t*55f@Y}ffU`&4ri%(6{8L!og~O&;^YmIB znV}JBcISXYp#wde{I&G#z+U~cfqMnoV$t}?&XsnNfkt7{CE}ZQq)X*jo~~eT2*vKk z%@x@dtAGQf@4K-{hXybDXW^hjalJPv%N!O4P6rM85JRm@-D!95DOhqKZdSsk?|O)o zIloIjD_%{Sry512B8y9PX;kI6U0@g)7G9FHd(tbti6vllGf(G8Z3MC)@0y9HIwXoD z{$kpobvoLYHAeyG-QVPIhO4%<;yV5IT&R za=e%CIQq@kChgv?AJ=#bHVoJ=)Z^_p*mS6En>Oz;_~82pRIr6Qb?ll-}4H&S2bI*=_)le=+*?Cv|CW#~07_Ou9AF$>J zIu&^n^v7U8ln{5;|ByEh)<#L*aKaj%ARf2v$4=Keem3`8oTES*--7vLmTz%fO>d{DQ_Xe{PIMix z>&;>Nk<7;Sz{hBo@I~caZ&}Si<8U33M(Oa^FAb(mBM%P7SY-{#u5O-p-0*~igk}-L z47E%L<193hkBwfqsf$~JM#Q(d&TL4&#We^`bSj)0Cw?{IY&%qI{{j$@?kkma3&Y8>PG~$Xi za0|WqE((}r9W&qHbMnobzK+FP<-zWZxFbj%Eu$D>I*+>EYSxD-zLqBZ?>)!wCTGi}_j`7VcRzPcJ0o@$0F>ecG5E z8pw-_f6Dg2+Pk;=*E~Gav$c1J{CCr6d>$?vtV&FillT6;n`mi=XZ-ht3>)rS`Jq@- zU4Bc{z;4+X|EW>0SE-CT4h{!o`JUY+xqiAE!^|SkrGDz#%R+erZ7;m&c#=7JT75GmaB_1Q*4T z56qg`DgNoxV$aTF_Tw8?S_3IBFT)eq= zp52AqM{#}GMh(jfY#baNCxMqa)%kOxb86U(^Hh zhbx!M`^;Q%d}cp`*UeK-t%|b$^{My2O`ELRc~@ve>i4{Z-rmFq*#Yc-#yOPJ+SR`v+F);EHbyRB7lyIIf5s-N)jetnf6JbH9hS*v3P z`ZqIVdXr5Gd5b&okU}RJL>zqe%gKYi`p46k^iolI9+vC1s7R}7QedmPgj5pTAmjB) z+m8nP`WQP)=XHR0ruvYv1B$*$$|Yw9PMJED4pNxlJZ;1Y^z3Q|aDgsY2 zMF%Im|CZ2*&a?&WQmwwWmqKUMgd9btai#O@p4$rI<5sPVd2HlVJb2Wou27jbk?rgf zo${}q;U+?|XxEx`@R9iCRsty$tc~ATHAv=-!A1h<8pLAp`Zq?g2f)&wwMc0ZnEm_B z>^#;NI92c{CPOLRnmX>g_0V;YMb%*>F&^os7W~QSxLS2-cg?2xfBlMZmec7 z7ATwx10cKOBZ(x}pzto)c=NMj4iR+GVRX|=7ku0DT{9v4i zD^Km$xph?L;?4?7T@Tu^%-n>bC-Y5TEjc0bUU7CyCN_TZ`9vv>fO8w@uYiJL79>Ix z#!$!mEk|<9Y^w|-1)oFSS43KFpb(WZrGpHv(+Y`gr5xS{s~ag~n1<$xd#Fw#%aB&e z;JMQ82c0DxC+#k}+-{US1GDtNXm$#4N0%}Io>GcencM{Efr6NTlMVx6jG2U1)t^hM zzR+^Zbh66f2_V7D#7(Yp9B4pNV4*7aHkYKsSLVl;NR>q0 zFZBt5vdlaOz4{s;Oqnu~n8qOMy@ZYHDFVyY6G3FeGZ5J?BEe=DPxci@2f9{;8`Eeq z&rkoMSX(fCB-(zk&-y(|ju~^!0|hs;AzKL8lJt6{$LHxA7|3YeJ*d5ro4zSw9hU+` zWY#O?S7hq%#RJ;ebHYC?-PI{~GVnx4^xBCnAZ?4{ns^yeVHA4|D-co-OkJ$8WN?Nd zJm)(<(cp=|+Wc@pKzA8iW8WF$SxHAFd`wW8{mZ-8sfXm(1GdOmIR|m>MVrI(G`WUB z_zUzd>RF>Z`tSl&$YP5U_L_H?1d>okA%koHe8GFk8R?*v7cG9{OsFl0G$*C zY9OXKZybHM4o8_LY}MQRh7{}Z^?Vm=cRjQpC(vBtl#Q*I>#FRvfbqjSW?2}O>mHA{<%ac zaQKEV#D=jFQuwCCE${#~`|7*5Z~uMh&>@F~b0X$$0RishZleD?{ZIU7TdRgu``zBnTRZzZTwmylsB$fv}0fYRo(ey21h=-#1JyRLu12d--Ph7TqdB@8~<6B-l zbbssH+I2o)lJ)6uR1GtJ3;a4T{7T|_O51p&QFoUf-ouzwf+W{0U43#mmzKzl2qV_L z-Vpvt)JP96+x14Cpv@<;p66}sC@mXEdokC!nPg#x`17fz6976TkZ_Q(1!mI zg9Gs@>CyKYQ+VqVK6FNU*^KMG&}=X<+ld9d4tis2>W+-d%^>*3h6JxE?K|4l zIdi>U_g_c;um#@8u}|J>O*%1i?1`Da2{t&^1P$v-#>c+h`YGrg_y3ON<>jUp7r8)>%GH2#nFQz{6E<=~kF%z#sw(3T-F_Er{GJ_K>NP3?>7$Ns zxPQpb4WPOnZ#NRQB-AAI6(F$o>sa69w`FpP1Nl&_VByVLQ#>**m~g_e;z~yF#vbvo zmx{HXzC5#u?m&BYS{QsX_=U1%vv9rLYiM)2U}js{lqAMh6eL;+_* zuAfU9r)m&>-Zabp`K{IrXf)(OPo@n@Dr)TOkhUamko(o?cbXjw`$MU3-paC8ceS*S zCj?K>$UD7Y#5}$F=i@)u?D1)_rQafIvjz%*Ty7j_)wawm92YtzhL=*>QvD@c(}OpIl1!zx2AvwRQaeoflcE1NpcEq0-9{%mvhEBGy3d zTk*9!)a%0yPac5$~Qu+>D zlTQ@H4aqOHj9~sJs{P$<)?F2)Wv=h#q~zr4^|HyQ}g<~QDKNq9{z^Hp;o@iX$ICs;XD!d zEg5Fvi1aBG_GdSEV;zMke1WzC!9nOO>MGDwMonz9H&|Uus|_=46I@(I+SqK+{f`Sq z+G^K_A@?uMTb2IIsPN_az3W%-WJ2%KAGk453@Npbj)$@lPrcA7LF)u{U}2s1r~8AMEC|fAXuvC4j@&NQ|#3npQV6%#Q+vsvAHj9@?#+Z>M(kANXz?Z(tDRx{gOb0Pj^PVOF!&s$p&;AL zyp9Lg3a&s>CAZfq?tu&n!RV*D?HZLtCej9wS4k$KlR)35P=MQtEi3%jJ%+Hies43r zhn}7?61mCbO0oU(sv(SwLgC#8R`>=vMqHnH)|0~bP>RTBRzF^wUOi6(p4#bRDY6+3IFn7=S5c8X?k+dQ=c$MaD z4mhnsC=Y8rp0`~`>b>8lHZ2ttZsJ!fH%8IjiLE)`_BSQ=Xj(;?c%``mq2Zxp6uRfW zci8+h$ufKjNQp46nA%^gpF}nn7%jFJWG9&wk?a6N8Au9J2r3}Gix4=wc5OLys3YdG z?pPE(Wk1SNBKs1jY&q!YiD_k0*Xt^zts2B7eT*cC_=dzIDt}6t?eu$`fUe;AH;{dZ z5l9j9B|b%6huSF7;fS;Db?%2^#BVH5K-zY-vJF_CF}u@>M@%qFhMv@5G)ewMarbeH z-|^Rb8u~mXjl~Kv&tfMqRw0FOmIIxGjLLIAODkzuYYj*UoE>PjlE=?F)UDP7Fm!4h z-67L$dkA#xt-eAaiWDC{MvPk-0gw(+QobA?&|wtn7i~Jp*7*&~kDXqm!9+EZhUfx4 zi(mHaYMKwAi6whyb=rSkK~ zcM(=FIrfJ|vip|tC$8=2zNuf=^$OpSPHV5;DU*GH2yDfZ%bITfac?oQ6yl00jkaUc z^9~UlMNgWEqRQ$KttwDW>)+fZ#rsw=&wU4Ka|NIk=pyvpMYF!C0G{K-YXzIB9fx5Ovp|u8Z_;9Vb_kE zrk*dgAPFEUsf!(j%`17}vNCRoX~Ac)`EM5?+HGl{&w^!pxr0 zxz^%`7jdW*&+Ja{f!ipa8u#q6%?*x2CKP&Z8&`EC1ZYv5BeInCislsySkZ9tREu!f zP9_YCg95n2zo4JjE_J5cY~pPYd})zfPA8YJ@kE~Soj$jL*&Nld?#G9AEDb>L(%uTS zzfJ|t>?hZ`8E)oHigzz)-~IB7I#C4aS8u9>)kd$im=B+&88Ismi-a0mukwC(*J6p0 zwPzIePPh@se1`mm)g!7livNX!TMvg@Ra1LYUS6~Lb^dynw344cKP+Fjbfg76yt$B7sAagL#@w@rtGN8*X&oxH8KZ0oI1OgXfc zZT|EV0odV=%S`R@QMncd%yv%+6S8HI(5c=SW`i~2mLHLB+QSXxkW z9yAip$1dG;O6k``Zry{BNIO<`_=#QZvm0BzmS13b`(Wzxcbib@zTj<5&j<2Z!HtXO zE?vr{efgQHk=?Ed`1$@peJ(1$2Sb$)=8?jMkvg9Gv@iE)l_M!gtUv*hFSe)G=x-dl zpP9>Q1B)XT%a`|M6vq*ZN)SrJbu}GzM~paP)3LhOfB^%LDc7H6+%9sdk;xT$pmT_8!aa%r!d;+T^B!m{>*;4bb(2V zz=8exP2n`pg1%^Lvw#Z^&X^}W_We^I`$`Qbe`T%Z=@BGM-%v1<|s4tJ`LC&}7S1tImL+b+VN6fl_eV zs|F1kSkb~V%5#AJJQAniQWEk{hs&6y?o}wBdbB&@5P_e9U9vghH3p&*Ki^rLC{yC) zCn~R%6mfbi(|~74?Lx5fQGIxkYG!`y+F<(IAZ`x3b>qf(RxFd1X|w9cLUN!H z`ZeQI)4RslJTf9-KaO`g-?Ux7;}l21&R0e{Rjj~g=T-KpeMIySlu70M|9o&nJRI&! zyyWQ`9e|N4HZ{}rHoQL$Oc`TeGX2G^0~3H^XTEq3O@=*PF@#d5h|JHlg2R_FIH%ZTc9f z#GZld7HjzSgUuI`E=*|L@pc@_$k}9y{`-veR6-DxQB_T@K@=0lj<>7+R8irHeoTKJ z2(JkiHN|!ZdQEb`JypjlYa)`LENYY-+Vk=!|vyLRo&Eyl~mXV5=k zq=sctSK{KrIjO3NUxeX-#0f^<9SU}8#qAA#nil`q*4?65qYA>WP8rLX7X2&@ zZnd?>+38$f?LhK?zbrm+@rQszC^#-q%1)X%aRqx+RsS08jT#|J^^N(aqnj#H`y!7> zKpr8B^p$gn!X;Jtcch=eisgbTAphveB|Y<)q4C0VIa>8nByFs`2J~HHg}n!9t!K*PkicMy(H=^ZVcV zA6xA<9Qxm18{U8xWa-sc=@p#OTB7~$uXxiA9XfCusA`$--lUs1`!Nyk)iaGZ&rwo(gCc=jN6p)5!kB0p=;6a#7#uiNJU|=#*YuCIMmZgXwI znkp1oM+t)Eczs%Rt-ZY@eCCP2sceJX+!UnsQ+)Uw@IQGLG%9KUNmvAA@$~!9PpQv4 zr*h_BFX9?(;t7|p70dunm56&EvjneIlPDO%sawkFH8rfRaOpCca!}5aSbPi^FN92lVgXe|iF8*NR?O)l~*uX$W&5$0`A(^|N@(q%lo( zq)|ki*-&!Ybb@kI+w=g*L+p?hStW#y3ycdGUsh9Z!mMs>3|V<~0WSM9lJb>qgG;UBwmo~d(hJJ*f}lEy4Xe*W;`07`FU%O)3A%nFRB8s$!6KO~uA7n)S#XV@=<{%=0CMY8=M zecH4vy2TsWup&N0qi^gI%(16Y*#~^b~$Svt)8lT|cuNg`x*yq9!CqQCH z5Me@rnkK*Ylsx9jJeD%I*l0$cGGtca_0H398- zy0nm`u$p_3Q4D)T-^dii)%tVV?W3|lhsZm~9HTTZi^|CW3@VobBU|sFLm%P~8%L~e zkO_n%bTA}7Wl`Y1K)<;-4}hyb--6@(l$=dONw-QjsSJCVsm(gMjj+To8BJDB&B&Mm zem9?4T&|=F7iSyl<=HH3AASDCi?yI6@K&3x`U$5JXi}{*dul&t9OOUop8MM()qwq? zPdLv0(a+)#nQ$BSDqOR$Uou8Q%oaRFhZ9OtyY1C?ALZ~soRwsAdscEVZ!xxjy08Dj z%V)1vzZ#IkwAWIdzhix+8Q{8ruQfE52YUB63fd{hf)|qolrzdyL5TRU=tsZD^anO`bWwh=+ zxfDp_L*$g^EnChZ>S!-d?*i^@%psAuo_hNTo(R&UR($v{Yw6M;q?ga1KMw+El%lMf z>s=1&+Aotz6Xk_9_wn#6&QOgba~jVCvDAf+K5QA5PwU>+Y*svYAMRK%-B)%|LMD(?P(}Q3M!A1{rIN~22tJp|#WX45Oc8MYd{ZB}l7FJ! zwlj!D%K%OFn8rNNrL*>m{Sjx*n9!=oq?cd;MNXezA4Yf{MMCT}U<~toQFR+MT|uaP z#gUBH9OGO$#q&Pmj^gGISwPtmQsjzTntp%gr{Udn$X3Maax65IN34sGAa{Ih9epDn zhVj|OfyXiDp8NA*l-7)9?Z$&KLwb7?kI za`5pZn`t?n@ylzm0f5DG>ACQsJ}`558APJlvWC)-h8l2ss;Q%alu(jHX@|^+X)K91 zWrpRt24cjLg9@s7_t6Xai{0mq*2(U`B?yGh)~5Aqwk-TYIEf^=T!+}U$n}$VZ7VaH z|KO9&1`Fz=&l!iqJTkQ!M@%;)XKhLb>MLybO46YHV{FI+Oj<{}7WS6JYY^=SN(^g` z6}5TdRMXvt8Dl0!6MV${hmCR{!5RRA^E1TmPO9fQjbm-j+C^&9DQE;VCw*;niL z1T||o8M}3zHkgT16)=Ovqy*YAyckYXPAQMMa+hl{d>I(+wA{Pq^yg;m;Gm8wUFs;V zB})1%ihrcksZ$s{>o^~?^YZrSqor?0F|V!vknnrN-GshW7KJ-BxP&_G`P;XVbo71+mnc#Q8c7?c2lE*Z zXj460(6YQ8M9~t80~7hgk0$w~6n3&emuz#XIL=U8s|@(jG2k?Z?GE#g8zPsyNIN2D zG>*w}Y5W1Z#$mr;_wq5xNdhfr=p=IQBMS|!r_u}Ou$JM3|NfLJs;BkZYB3CCnd&(S zIxq_a{)89~E{M1qLG37~o$>YKQh~u?*u7$KO>CH@UzT(OMmU`$l0%B2hIlvpQzL3% z{-BI^YQhpsi+uAidIINZnA5RB##5&u;eos}q1y7Opr*fBmWR!jpp1*6Xs=Yiw;won zCg0$(`ER#jV_#nkrRJ$KiajC08de*LAFNa+&X;MkK!WCzq%Ci3i1vTY6 zr$$m#=05b((?yz8?D9}bUpdXZXB`Z914ptL5~ZOPB;H$57+HhpolBV-Nh0<8=6~qe zPyg=-7(XG?(j7kBch#kJZ|3t|cV4f18I?7dJ-grhY;b?N*4$eJegAVr<5$?v2qH>T zZo=`>&uF`Ru=CgEqq*&vkEri?ZdqD5Ws@0XqbbZBMCBj>b5W5jD@M3+G4t&t9z!;r zLK}~-FE%Wh6MA^S9*+;yPit`yqt%cihpa9z6}^p%|BwNTx@7~?N(D$9lL#fq49{R_ z=Q%_Y%9ZoOt6G<%{F{7^0URTG`5cA;1hb)Im`7!W+%ucvF$M&VN~q6>AKQ+do=Z3~ z;sj~4rzG-2tleit(vnBwvB0~V&XB3L+KlN_c^=tP>;UR8uz$E~i6!ak5W{~W`D)f| z&+rBJuZE&SjoVli$@5X=L3jn-*aI3Hh?k?}>$pwd<4CQcjMd?AJk9Qvp02JHlZeWA ziGrKju6+Zozid&W=hR~xVYJTM9oe|ovm(BZtshvR6!CteUE_9lP!9)s3keNiN$*fg z0V#?Ai6ItwYts&EO(F!F5*a<-IYqa5{8=n;=in^kfApx-OKh?-s^Vc*3&M@nh+TIc z8jSqB@IjD3^q_d{`abG6>gQLb!i>b{QarG|0Cyd{Dkyu)cy^%f3FLNdPNH$A!I~|( zxY6D=dV%v8moFLx>&V_7>qq3wDeK6dnKF3pv&J-@UF`?dP1D`{BVPJ>fHXBCkY^A9 zE@;JF`V?jT%kZA33G(z_0VM4Xr&ho2^f#Q0M*90b9w|?J4u|#R*EMa@WEOWG7MXQD zupc$dX`nFx*L&1|IWXT$gN;K^ruu2goQ@2%^iNM1gW(Org?r%lD-N>Uok|>&BJWjx z`UL!TL~vY-JCEjgjOoeK^4Yp|t6htScYxu8nbWf(f2l4=Qq>7z7S0;)##fnvJGitt z3=09JU2nFT3#~EKRGV7Ql=MB50?e4saJb{DRjZ`ko93%=eTtr{DzK#F3ekY*0L26q4N1X3IqZW;7+M^77UplltUzg|J!4qDR39)cG(7{2KG!Q^3 zxlmuauNwdT`*$gDr9jtR`+R@9pBGa&SC8R*51e)W_9YsV7Tbt!SaKg{XZ+aRAArM5#>t`F*fSO#>D_tF7tqx(YXkz5?Di zX`+9@{PmG5Qo~ldjJSKyzvFuN-LCkBZxEYwGJ<+~_}RInUE{-AmfBnqZ!sVp0A>Y7 z9jXhqhJ>syaQc%_D`t3_Au6c7XJF|&{p=gcLPywDeNP-uskPdZ-^gJ%!)icf!)Sh$ zF)pU!@~EJnmuQJ>p3HP;*4vVjQ^Y+9fs=1!l5seTL(*+sa=t8bV3w0g_g0V%AKSU& z`7Q(-U2Ly_L6?$HhCVXYQPZT52qh;;t0{W|1>|We$>b+VS3ewmSRO+zk^Gyw&TVjK zno4%+a?g9NnY3w+Z=dLTokM6`N`sPoc+ueV(b1XcVRblPB#G>PwvWyQb6aT?D?Y{f zs^>a7lZuZY_nfb2J@$rx)VS2 zt5eqd>Z*MfiDUFb@^%_#BnF@hHRpZnR2PE5BPSMRdmrWXFe}GKI`v|7?ckGj=aN?E zcDWK& z0@o2xcdsAu@!+pwr*DM7jb(6^@quXKXEFe6Op#DDY6wlQi-Z$C+8F}+E3%w^!P|;S zAcU_TMh6C!*na6c%GnqjbJ1o~<*B;=i^e{S47L9NY&;Y1-#6`ZzvC zQb&c-iG1Jy0ZBt=KO{KM|c>jJsAj&4Rsxhnc=M%z*E!YgcE<1xz z-J3Dd*vpq!uv>5Ys2`DtMd0r2Xpv#(0;ec+pj2iR#7_UhUP4 zCUWjgceUw51>M}sBxS1>(|E#6u_DY;EZ3O zJ}>Tm$&A7qFMvg5luuf>DV$Ft=4blAKsG3!IS{p;U%p@au{YY}={ZO>3=-*{LZPB{ z^(QVjQ>6>K7_iq7P0))}uRnQwsxK(pq<^Ag^1>YP)Z*9}U6#ZwkG?wr=@cPbpx6 zo`+%hEd>AtS)cj2j~_pN-QfjrFMCO&mnJ*l;P&m?KWy1a=)Fzl3B9B93Lbu@6e*bR zv@z>bK=C}hq$@UqD*E~kL`zG&rg_tx9wBjF92 zw!6onc4StS26Qq{!xIDL5C5QCQa6zW`OLrNfPe{Y0&Z@N=GE?EU!7r*Oy?1Kqi8LZ zEum8V#%#VCZF0Ku+sFih{1KHZA;ftiYLsUZ4MFarEtk8v8XECC91u_n$VZ~fq(`L! z-*vg|&!1G=R;-lrDCY~PpHHJUw+4&>yqoR+F?FE2dd$+)HUuPKZ_})gCZ4q58cwsn znG^*8MayGMzhvtQO%u^|D;F}@P_Erw&d%#=d>jG8sXy8)DfNRP5A=u~tv+zz1x5}8SXIp^sngyXf<2M$U-0Pq9b$&<%;KOdB$nbu;Zc)^N51Z6Rf7cV{f;7hP$WBn{gs~fgU6jti%M%<#@Ic2A^VYb}pC_$ej zcWRjXNWUW|@&We3m!BU;5Xu4>8{%J0!pMF1yTK3V_&i zQB6vJ>^_veJ)6RMDVhgjPdG=yxw2kop`;1AWeMbGfcNa)8gzH((f9MYJ!D0yXBfoauf}e#Yhc_eUgyFmW@4piS&_?)A zRTacQ%ABl^Dht6C2t;Ob5@W$5r}#oFrc z!?+k_SbF}{erg`i|N8%OeOsNTxt3W?FNWL`OjR+(m@TKZ?CuJiEsYdYM$z7B%qx3{ z1rLScbYky)>3cb7W$gX_q`AKr^y`~Ls&_=PLA7YWc@d5ctD#7m0hQQ&FjKHm(_}?~ zl1|Z53;=vY6*qHB-3Icg^^M2}l_*_8r4NAEW%oz2I{okj7s6aWfQnMn)T|(J^CC1s zUgRXo(`7d5%1TP1)U`?aLk*1h4LK-UMa=he4bGOE{^Q$v6o$j<$^#*|ku2JAAXf~AR13Wb!-uAh0|$y$KCW3APzz~n-a+AkHugdh&p+x$x;^HT?zi>`Rmsa;2B(MR-mQ-0PtW2 zX-T8=J|I}G20e53?AiLoKl)nU_062C10AM_Pn|-M%Gdt7K;{|?%;^E!#c*n)Q;SLY zDMuo1L!3*@ITcT!u)E!~;~<-LHA`m9$QWMIG6mRCOLzIO(LO#tbXJ;z zW)CSS7eE~ru1(h{4$=qn^Or7dRxdHXx|_Q1lp`&X;@X%ST4#RK!SWv7zndhK=kKqv zyAS-AqE+l)BB|{^UqMR$FNRm4Hu?YO#~xU9<)^-s7=L|MyxHaMn1ZAGU%5(!?+xUzKr<01m?)A_3SeUXgFEF2|MH;D~PP%&VnN1ubNBCB*)EHWj#?X0y&^Z7u~F&Rq(|E()jxN z=58qY0K66;3@PCs-SR{VY!k|6M-s61u3tKc9tk~adtx?kNNDZ05|GI&XLZzVT6uI$ zLhsDFxFo5m6kMpg^v|xMXIqLMh_G@C_=AQIYmn`XtGU85|C|jBI_=eFm?(CnC)rnE z6Y9hx3lW0EHEh`}LTWyKLWEtMx|6i%Wem)j5E3ED)PSJ1OddO;_~wxrff|@xUh@~7 z$~>wxcl!W}Z^{TSi2wTt$YU7(l&RF+B^6Xik8yh!932cxjaHYw-~k99a|-LdM)M>F z!)Ak?=@yhQ5{3);)fmCXFiV9g)`DLQomGg@^vsC(m z)>5(Ep;3bdryCV@UEhY%3B}7!ClF- z5rP<3Yo4rg7|G``3XWNQwY80T2@?c@f-5!W&H>WCkTOh6j*@PbEic_0)y0q7Z?68R zUMX$0c=&*BNwT94(aY359}}ZJN)m`m!E6ND5locsFAP#n=wA_VL2FpS zZ-XojyIA6_(x_3R7ihmI)0;MHHk<7MZ{SxN|10#wmr1mAbLc~5Ck>$PW;;4$LFy6{ zH;3ve$Hy|6? zL8~e8?(-i)*%7w)vyY`K1RIv%I{9xPte-9~)&Wh(29a3A=&hn)!^|3zyy6KW>rBhwV5S3N{@^;VsMjD979LK6sHJ&IhcYkmEwiGI}5T?3_XNZ``_IRc3+yN- zNWZNmMgR4oAbp|WZq{5h)ye38oMH%JJdsirSnkp3bT1X;)7=cT2g)F8EuX8Ia;@B6Vrj!XBz z(UCK%thIC}`oPO>ZE*uzo&G@ez-&Nlv$E zHsUR#?T5h!2~$mc@jPbR=L#q1qOo8{$ab70-;IsSmZX3o8UZewHeEzzB#<95?y_qd zG;C;%!tND~!^ux{J^l1$-|2s)E#rom2FNx$Cb? z7cO0JuUUCG_8g&mj&(Z5|8St<|%JG;x zD*UfnkmH)6WCV(N#WLO(@l0`XAiL%8nY`oU>Y)HArx?{y+vo0nu-&i^D**==?y+3H zeEG$9&(qCbUyLHUXF~o@z5Q!j{J8V$i1mfeuBfWYEu>&a3Uvs$ zi;(NR!j)Ev5UvkPqoMx;)S@5Dr`qH!N8=9gx41DzS8IL_IDh`*%GiLLc;C*4sK~i& zS1d3?Y+Rf?y=CQ(A3l^HynUee4w>37-!&eIuDq?XegLC zd1$J<_AFBnctl@9Tuq)wHOozyiz6Pom{A)XgrUmSYgqfit|d14B|(<-FS0)*LGK@% z4l~H`l1s*N(X-!;Ts>qt)Ss-Oz!a30N&`A^4@EFXsPiYrF=WkUzX<6EVkIRY)GDW< zJm$%aC1}Fch?DZpQ1~8(w1oI17M8J({YGuP_^5nW+6$s5W%mGtv}I^8J%i{LB!Yw! z72`iHLe7pzl|GN+L zR}6x!qX3vMtb!~k=9PS_IvP`Jl5dl}qp-XgFs3?WhWl1;K^43oXP2#G;Tg-|lco|=%zCWROw$}Y0s^n0Cm zVb=5e9_#!4J;!>E@A3TlV~#=HeKzm+`?{|4I?wZhs499Lh02j&8!{B&;jzz~wCuX| zNsXzWZY{zHYky<|uvFZuTzNks$5P&E=kK+pyU2Vst9HI6uYWRDw?L&6iL^$f{1bww2-f3F?fWDrd)r<$j zjy$P8<=J$wl&hhhefy@^l$I2ozMJM+qx2o}-+xmz@xLoVUKZAD7to6h-E;eEpy-`&K)6>NAi6>kZ#+ert;O`Ld^gOcvzW{(9`7DdvhI0kPGxib7- z^CNh=urBd-f6#9illHyN^YZWC*Dl)+Yy^nnmsV*)=F}07soZy)mq{Vba%Va)bsbhu zUC;87v-tfs?7!!iOI}BlS;bJ$b*X0l;7)x@Q+dnUwO1RgpQx6nC!YqMNncaBE=#jU zbS}YW0uf2+JgNK7%#-Kb)#`uLpq;q&27sfX_h1Hu)OiJ)^HQ2jnzelhNe zoR<$eF1}XD4pUR`8uIntA~XKd=^w5>VQ2DfJKVSt%^2;Ro#Ug)C2VP4{^f%Yj-5U0 zCQ`29($4W+Rm}iK9N+xccJ%UuU^CwAD70NuB}Ok6`wShro?ozY=S~|i%&|Eg!Q1&Q zi!nn*LSTcCBrj*~E98zh0WU$}d+|yUqS^Dv(*ykQd^zt>GbIIkK6}oiZc|=Q+d+~7 z;=C;}@r+pfJ4O!_b&_^(j|Vl)g?qV3E3u%j3>;0powI9VoZ%MW5Tx?O!sWn7s!vov zqcaf`P;_+{q$Hue6T1?EF_7QRKV0kqc3|HXNZ zXfau+SM`d9l=7~@3(U-ZgXQF8vQBmSkFILJTiKQ7GDpjs-NrPYj# z?C-oPe&(|9)uMus{wv2MnAOFV!GcMWNP8Ax&rTA#UJ@pdC;c3~=Xyr<2f@N4iLo2L zKk$QcqUVqA#Noo@iatY~0kx>H#CIY|7xt&Z8Sw4M&)#r!rvR+O0I=%Zr3*?P@8=BJ zrXOW%?9!38*-09!s`C|;I8CSIM@%|HCZr3ZvR4)bx)3e02St1s3qu$KYdVgi1W@1< zC7mV~EB#>EpW}@{+}UnO7mNXBQdnBfISL}DT%4Z@RP0XRNm!2h(&sUE6c6JGA0Ad$6_LpvX-`0KJE9k>Gkxt7@a?mEZo1 zpBjP+OC(FiNQR$^0^S0Y3!^=!x>oxE?vJ_NW$eqkTvcK;p0FwZx@f^=R*&e897bW= zw{KStkM&c?;r`C4Z8-wCc56~Mg~ic(9xbJn6HVvl=%q{jaWrwJ3@_%Xu4wnEjZfJ5 zhf6BiWn{2)3`XM686No(y01L5o)F1<^Fq{A)KNj5VJ2i_@1vZIdO}G(^=u>wMLo_O zfQY8mB(D(`U(>>Z1g8J;`vbLeK`keDKQ$&9G(-9=+bMa``55;c!V{fPYH zC_9f_zGvpe(l8L2^z=!2f|kQKQq<7;nHK~e(|yZy;=hPNA{OyT%P3)Xx1KFsnptAG z-XN`w>egC2NYem*mK3+!=}F)5+l&F-M}_kd!@(P38GEXx3Bjd+jSQBE`_$RuY3a~! z4aY;4)uPF&cZ9e&Uq{cmaQXwqgCw$+01c?fBE(U zjh|h9$wZ+nhX>-rna-w0d>es>Z-($iwqWcu(+<*K?8$cmBNX0IDlE zEoR0Phy%bwL@{h5HHCstl*Xm~g$K<9$wrp>A4%yaw%}2ed1{v!uBiTDFWdws%@k7+ zXT2p2Je?6o^2F8}0k*spL#t8aE?{-m%~UL#rPj5#q3*O==oJ!Bv=dl* zrN2l}-Yu=XlfyEaC!*5t_xQo9^^yGtXLvXtZrRapf#2xSwMTpUw$u3_t}?=9ekYl@ zBgk!&*7;4=ZL#cU=F_3vQ){!Xrep*qk_VOUBiOUT1Sv053t&LPP8SBHn# zlh2+$ed)C|Wz=O>z{PJXj>%h;$Bb7MH@3dC+>MPb3y|yt1?yV4V{-r^G$+BKY z&to_{M5#H)_C)648vO1RT)jHDN&Z%nOUO14`VL+Iwpr+Ur!=Lmv;Mp@v+G{QNkIc+ zL+Z)iFbms+v?0NDriNO%a-}O~GYT;EJ-0;?q8}_w0pIteyjymu)?m%XeeGUOeuhn zNblknW63=Tkz%wFEivSG)9t103=AGi^AP_$g>VXxJ|t&ldf*|01}!dlbLkv0g-}#z zrv?KzCIW#D%~;i+vbu-!3pxWEsJ``Ygy8kyt5OIn7%3yns@dMW20A@2jSy{vEk~dN z2<;d&-Hc9)yUD$eAP}qPi|zU^z=5_Dy(Rd%f+PhhHzhXVbIH>uDT{MooNzF zT^&alSY@hnZ+Nk zyZY)xWcK;AQ}iiS+FfdpWNu6*1)M!>yT0PPKINXoPyXa^l2-|*$!S(g5612>YBL7+z>xOjy0sf5kMPfDLpL^H2Dfje=kiY#k>Pyo2 zO}QBXMeWTc%8ZsD2q4MrL@x=s-i?m~%iYphfG~HcPugmBCSS?rpMF@If$&T_iJ;ew z*J5+if^wNls_bjY>9~GkEAw~NsoW=eBL7$Ck)(DA6sAxma~7dxx1oGY{N%&^LEqgz znsR=K7ydn&Z4Bn0TJ$#&_4e2JEhne1(tS7sen0mZrW>e zn`%bOKL!2%Ub*|Twk0L0t9rW4N9IpLnb=C)uQy)~_7xTs{W`I5sSMbf*vZK8mVX`fl#~+f}gvqt83rcrBISSHnPUb->WVpK&)u@8uK#X z<=zDN9>AGB)@^w6dlA1__P4GX&+FqO>Z9Z&kLgWamba+}$qPV{8j+l#pg&18ubdk$ zg@N;1TAGd9i+6g**LO099vn++T$G*H%rgPyaWHX2Uv+9V039EBZkBoxO$bYY5N!$= zIcB7f^{{{gwrIf~Kuv(i<)IO+#(qstAPc`}OB7a>a)|+DU-xeq!^)@9-WUnj=IHXK z(E?T>TC~sI1yrHRmo&(Vl7j@o190gyN$6S5QSmhBk+dwJ&34{l@Cvl`z7L*~=Sv6E zffg5x(K}~HtHLh*3ivVd*i0|dX^d#h#WP5p%MayKc@&~YOEsc7-=Jv5Oezm`^8&Fy zu`1uSN|i7}>Q)9|=@QiRrTyVN{hK!*tOecQs-~@5Z0NOJkSi7$0#O2Y+16WBAZZDG zz?Fppq-~F6w4y*Qagt~)_DCBs_1S}={ZV;E-X?6BR;_MctyZm78(%wz?3w7gVtzzT z)RmI+e%se@Z{ia7+r{^m^)znV5*+lGaZ^l72d1(uZebaM_SJkfi$1#56$Mx z=ukV+9Crvkd`a=haYN4D6S15k8R=q&O!oPZP?m^0Rc865k4q7@^Z^2vfO1Tg9Y-9^ ze_%*Y>G6s8)O?P)_ZBW`E12t|do`h631eSPGSoc%ps*_2@)Bj>>9?0r@_c5V2y#a9 z^@Zc1yr)O@821vb0ma`um%G=lg$} z=X9MZJnic6v6e;MZtY)EZ(-smU!;9Gs$V33`{w?e414)xTAxy^Bl%C|Q)l++)2`=t zhYuTqj<`Bkm@CieMYCr=dw(JD8T4?Mi%;UFRDX8weBY+^3odVc6u13I0-tJJb?dVI zYEz|(MfV(9MP1LbXD7FjK>m}`O=|dYXx|U+$08#$7-4iT7~e3d0RgRKh{L;e5R|g7 z!|#3AkH*a0JyY|(dqFKX6jXrj%d69W*z6_cc|KkcI4q46wav0;FHllEmEdXXnUoN$fMgrLccfa=eM6+ZkgBrB5u6-=Q>Bqcnjvg&;k4AU&@tp zo@XX)l>WtxN&>##eEr}3F|fWXPM_0cfr@K%rt4b0Ygvrs`XW2Hx7ynU=~NhrwGv=L za^feY{c>Ht!$d>Pf$M<9pka3vyhB!OI`{jVJ1bW%WDG2L0f;S1IrWEs zU1+B4l)GPB?B0lWlbal9BgG6!O39j^P4_6r>6aqUUmUxt-Q=|}mmt)=T$eY1E-fg0 zCdX{asj+N>*9|#qnvJ>A31r9?lvb>J+5?LslhiY$>P;UY<(m>z3YudCQyO%Qch-WJ ztYlmzRv9+;2>Vg;tEp%E{n@#)E!{3qtd0ygHVRaNK=9eQ*|x0~Z+zK+3e}fbuXd?8pMa7E(yHz@i#^(#xB zo6($mmq)i-FlSCCi&HgVs|zz`{q&PKo}^!Ry(B4d5OS$v?75ULg2Sn-ms#AS52a%U z__22W>0IM>R5vP3!B&*cN^l5-WXng1h#T{QsXVfQVf?j>KJX!!{_J`cW9H2A73XbJ znuDhTvF{|cO=YTb?~CLdgpWI_Y^hn>zQrCYH@OtbSK{;M-|cKSa&zA9xp<$h1semRAD1BN$?!Hp)+ZO)751XszB zR9~7BaeEWW#Da3cu|d+Kx~;8^>ff;x+0S-LpBS0Cm?T(+$m*agm{4X$(mfM^owGM^ zcUk}s${mbY-A4+uqsNZfo*HjmaWFk^!nb&_f_>SVdA>Gn=|@U4c*@TMHRT$TT`8@p z4&3-9V(^8up%s9s@*eFx(laN)gT>Qq&yz65g4F5)s9Gn$d<#i}l&SWB9Bo8CK=?By zdF$a(hXlA*e^_((5+zJvDQmb6CDyUu?cByTs%Ol>CuQ%e$NkpjV%9US>5mmgSR3h& z_bMYV!?GO+n!dF7WqLfo1b5_!uwkz>&?4s}UhAUF6z#utESs*J+vmp*qL!G7NZHs+qg3>Ze;R>7mgW0-r0~*s>7fiv6T^{ zH-&PsB^?ibM$S*wLx?{lE+^^WS)M0+W>cj{awwc{(bk=Qii*_?`rfNiCuv%+{xJkQq;IP)T4! z$DrKEy8?d&GQt(h1R9x0n2|LI%(>G%tcegV8V*SAL+`!0&lC1m|Kxz)U~QQ^%;o|2 zuYBuKB=yI#d{TE2Ri!;A&xz$VK?iljh2ji^NIRq8bYXBt6ZmB$VTumERZgRG9DvH! zLDRZn*FX~e!SaUvS_onK;FFpswMQZEvK5X+-U0Q7`e9{3qfRODm@!+V6#8y771`a~ zJ#nJKas6gp(u>FiUMG0JQZ9uA5ju>C!0`Kz0Vh&hI8+@~F+Y@%KCT7Cy`R&M& zBTuhXT{K|KEOWt{{JX-A8VwtC>QDg0@GLmXeruIKn&m@$@!)3l@l;j6fvnQ35}QXe zLDwxL4LsaB!HOtbaA8M$5uAk-g+eqwb@7XlE?42y!QtmqK`=6!W18H5-}L_Q!;E`p z!{BfMN7QX#z_OXgS|GHOhZxWEEQCQVun}5ZCb%R63Yyl6oolUE@+ zMVT}aiY!%5XM!Yx^f|IULcxT;My_v1qhA;h497wG@JWz^H$g$?ihg`eDCN0S^~-{T z2+C7^A`A@4&mGLdN)D>3#(gl&A_Hu}fM^y>sKqs|jO6 zo=yJh!MA<0Z*6rM)qeVf&gXh(R2V(O@Xs{oFE`!3_VlZR`(7BG*<6=1JYn4tTrL;R!B1n^la|Mh@XabY{^VlCMCuATqXY{0xDH3mAT+DDW3K;UU_p<1p}a;q0Hl+%U<4KrE(S z@9>W^Wi;R1F~OEQ8V%gLk?m1+VMlCV2>U%UAD*2^rrC=R-cIDpOP>!~sNP zlj~C_O_(q*;kxgL$&)wg!?{C_^j_PZ8%MMqpJ&YQ7`u1b${vkqnj15AEJ-(6&dq*0 zU}#OcD3b#MB3pIP{pTS*CA17@BA#fSka`{w%;YCBA}28>y9FyE6dfj6*Nbjm9Rr`c zvZYQDGR5Qd`rxm1_Wb^lIbJ+n$ZA@Kxz@a(t!Z71()3EGQbw9n25nH})!_$O%}EW{ zGYQ`Ddyj-cyHRo-t&w$m#$C2k+e*GsRI7zl1Ikm^`l(qB3PR3vpx$nzyhH?nGKTZ# z2!cU05LY&>QL*2iWfd3$Abb^+&gp2y@I#?Vw*8Ves*0!LOWnD2W&S3pCF*0cikyym z<=5MuLZa`mK+NB-e*6!m(IURm`yWXLQ8dbBAL$?5*g7Zf);3EA)i#3rXLP3C5doOQ z#c|4XtEW^NB${KXLZ~rvp6&=Ld3|P&@k-?_r5VzqU(QE*!Gd&S?5oxg2#>6%?8xK= zn#*`d>OXdctP8C2KaV>6^JjGvr@xxQ;PY=NEtZnwDn^3Ajes8-Gu}&>mtu^Na^eia zb(gh%967q$!21V2*v04_{SkH;>b1kyZg(56%z*$LM>X^u%L)0(Z0fW1YtlW(M89!? zub<_EF1r&V)frQ-mQ4Jl6h<@+qo=fmj4HtgT+ox8W5TS|a+Up-JZh37ma$UACl&in z-3-?llX5v_yg?-|2FA{(yj-NASI?1{%D2WU<~0jHBynmAl)K}13kD4xYH5t^y>1pY z-SMRP?R(aA_%xuo%Jls;j-Nh#Iw0X{XEb%)CbW&Jav{aaLegDK%~+~FY7|J*OIQ`l z+p=5eNxOY72ODZw)U4^s@G4Ht<;8{YEoioehtjx_{vySCKQxjUMV!mi9?sue@_H)Y z7sH`pRo;$<8+;b~+@}dc(j>zE4L7g@FEoFIgKPt(Ncf&n@CEpnWxn^GF|krO>T{%1 zuRtBB^wjZRB>n)z^Toe<}r*ivAz{e)iMj z+ZV4|9X+|s&`3l9Ubnc6WI0DRgdh~7Pkh91H!|@U-4hBs3u`s4sdb36=JKTukJXIr{_*EXpT9>5c8{1R=rO3A?bGTx$TLG-S7YP z&qVeLbDw%_!FW2@SXk%vhyM3+8*+>*KU6a&q17K;|AY%pNH~upoWXeEs+$nG!o(Ci z0d$)k3+YZ!Xl~6scJIW~dr#3l$$3Z&J!nCh4!55~42?K?^Ulf}J34>!CNQ?{uU(+I*}S?U-f+0a7&gx5k%T;i3Wupg@W|GU8Je?K~GC2~F-DmuUKAYF$l4y~+CQE4p!=mubMf~OhqovkK{oJP5ya$z?4kn-SmdvRvt3MD6~M}dVDE^vGJyQ_SCy@FG#&7KKe;JRi} ztjym=Qxa6ivkh~PM}FK$5pX+ww7juj2cLkBLd5JX_Zpkk;2Ho!n>aeF50%`3eC+C- zIQk~yq!k4acob;K|Kzj>5EXUJ|LtwhXxW7}p4fa)Q~!n?9MRJ3D4LdLl>G?^ZG}72 zNVHUOQ%|H1!1#YQ^^Lf7@4tZLH&2!_+K8l9z$nhi6D5)-izT7;l`f9TN9mygxo!BUB*@+6T_K>e}PCm$^UO>cC7*ppZKy>F7Z8OD&G_ z3&dqGoF7ZUr&5F*#uHwR^s{7%bPffwqxo3}Tlf7J0!z1AARFvgv3si%fB+`BRM6`4 zG@W61h=^|hHgo_rKtiGt@64G9qTdh0f*5ZQ5x3 z_A6JPw@1i~mJ5bXt3t>85yoKk(fA-oa9&x+b93C{#mbbbTM9fLvY?B+9FlR#k!8Ne zV(HQBx3G)>^Lj#i5X%pUvTkh+zl7-x!J{Ob=O5AV= zg;Eho`G?+RA^`3If7#nnU5B)<_2#qg=z~S<6;q^|9(dpZZ8zI20+U2-~#T|rr-jf2TYPS zqA9E`=|4=x@V2yAZK}UxxI4sOz2toM=+SNxwpLIvfx6)o**8z2-cWpkWl2tbu`Kc{ z-41*Z!NkA}r{MsvETD+Qu2IHR@m;%gS+VlS(_KfbT3c9HK((9|EYuJi^V(>QHb{g?SL!Q5dgy^Y8P0TmaZGZjyOGH{To>O6WE z@YgJj*e8|u-^&vesKBjAHGBQal{!pXq)B81XLC_!4z{uDuFB%Mv*OMG&otvHuX{;F zpuUeG2H-5F#UB>Pz$*8a#+u|tNHFirOYFsYCNEgn;zGql`KMA$_FK1hZ8OG?sA(#h z5SE(rMshP}XknmD4-8~p9Hu*CfB1?SEInkTV1S}-%ZbBvgc?K^gT>Y5l9gx5k1u%MG&k0F+JQ*wWXVw__7t zCSr_Il9HvS?4*4kiOo%O<5zbPck&*Z+RP1HNt_FL01z)L40vWC7!OshS+jvrXc?Oz zG_vT;GTL7#ry?2U&|Ne^?WFCEbwz|>JG#&{D$N!r>xqr8yh_uJimx#h0`!6cNo^(Ya_hbSqBPvzQ(_egAY}Z zvpp_*Bp~6~qXH%Q9J*MYByT}aEDKu0e6s@2M+;LTKaEzUd=<&j$D#t)zoiH(kF*6CJluCd|;J0ZC+ z+qoV+dUW^&LI|q8IPjF13db-dz*KsANXy^h5o`cSay04V@7`@hiy;szQ!G*DxWv20 zIGgPk_K|qFlf}qvlRZ)WI?>=9C7>QNNKG8NK$$zhgLZoE%bM8x@`2;JOZ1m;4)YYQ z0tqE%+&${M^oNwH8@T6`JV{M~vP8v(7rg}pw4E$R-0bM5-|C0BZXK6(z_19hy0h;yp0 z2L?kyWyH1PB<+sOW=S0Rg^*>ONfu->OQP3bO)+Zw`E6(kxzR&FsIzAO{!Srgi3)(7 zbHi1D)$&2IGP9wfVo>F;*O7?PO(u&&=y0n?vP;p4Nkp>Bl9d*Fx`2^tU9vZNCJ1g4 z9tCL9o5w2hIdJcx$P8d)4FlCG=v~XgViTtb2W`ExcWF-~Pae>{MYqg;qxV=EvC-}F z!yQc)Amqp;V_19@TleF)@BW;&>AgB;IWs-)o^dqJ3^Al~GSg(E#S^|ZfNaPgne#82 zyQpD%RJ{!32(qJ)G?QCk4|^3?x!_M?rG1r)F_eNZ0AY06uPky||8jmu&zMmzqWx5N zDVNG4>GojXZ&$trAGMoF)&ZbbauJ_`;cN40PsfB9e5TnW`w;Q<97{J=?q`(DZs_Ed zdy+d;vu9LQC&U_7_GCM!pHA3`Uyl0Ln8o;jSBvKU|@kP4n;s0Wt4k{ekX!QfAEqz*}xS2EzJ&tW`tzfnt`C)M5k zC0x_lw5-+;Zf)z-=>$iuX+b3{tR#-(fWzsdnr45{FgndeDj@)K$tJhx=dJX+v%5*lRHYH0moJT80> zNzECuB-GSw?A34Oe}+Ht0=<5=L$@h=yG?+lp>8IJwjeUzQUM(TNiVdKb>ea34r8Wq zSiaV_27GTk{nMW_SW6lW9aOJ=Ib&%ndw)<4OUhtm?GZELMB}d>mmrEY3swRSrM?D@ zjo}zrA}?%86AW9dlKe1~pfBnsQ$V0SH`bs;EXGXkn47bf(3ViBd!z4WW0dXqHGHe- zkG=^{<}htG3|!W4>)@Y^Kh)d1take;QUwi1J1U20+3(!JhlgzA>@s^VsS(7^P-x|k z^Ij7qyjQM&DD?rAI=EO0v!iDo* z4-|(=5WE0&gN6?PA4iN0q+sIRtz3K@@Hbk=1r?>W?Q6YD+857ngpZceCuH*Wq!#Ui zS?bnuUc~^}!a5gVm@~b8ZoJk18NO?FmZ}MixZk9KOq8Js5LC%>TVAyotnHfb21gD^xaf2fsD1IwDSa)Cciw-nW z&Biq{s8Tq<>cUEJ)ASgY7Q`kel9+*AB1km!QCYO$i-B=RQn8v=#aDPGq#U}qoR>+$ z+zGt6=~X6vj3o5qZLv`eRgTFO7PTQ!1WB4C(-QE-Zj>le7(-d>rDt7JCFsEB2R++EWF zqxon^)XphO!{kwqS%D!99RfNpIyT{-Wt6!-slGu|8D>yZiF4~L2j0+eclYdE#*jSG zAart=&Oo?QE1*nMXi%Q<=XP1#(>Q9`wDemz!P*<>@)1prz z@`&dINW&1tBqJg8Xe%O0SiNYcPy1G|C2T;*KS_CHFw&?|r^|4QgGB{0GCi{=ELk*h zDhwY#w85szC9KhTcal>USlB|nm!NpaToIMUzmWql3q+cdl;Et~+yUZez;@A4)>flK zEiI!s9!AhdQR;_e;umV<0;=4WCmv`h7Gm#tS+iQJw1e!SsX-c>Gj?+p>OUM1WH!Q? z@(X)eUhmt6iVWhw*Dfmg1;x(xK+xSCa}C z61S{|ZjFZnx~PRXl|b0y)9X;EuuNfDr*TN7rP2_zvMLh8mWdxh2gKRH@-+ z7i)_6v9%TyPh0;BU<@@;!>L6Z4{)|i*rm!AO8mSa+Q<^OE#8ulm?$QB_mq)jTJpdF zZNb<&jy}w=igQfpHx^&XZcSpG(yblY9L2308~F@XnzR-=e(74vJ~V0c<;~ngz66%(26B!LJ2yD(w<27`S3$-Lz>&XXH9u3?P5DA8*1 z>U4)|RPDlRD+9hU#4#NGhC0xC&FiQD4eS@aM~beck+;g z_}NwVtpWnV6>pgHFmSR5&6h4Bg~Mnj=l1rkk1IY(3JF{rSZ6G)k6qjA?WomJtr73w zmYtwt!y>5oaK#EHAPHrD?euWPq}!VfS+Cd|u9RuiCY{)a-4i=pi23uailtqSpSvQf z?Otm&3j-7@3+8nsqy~h6-Nj@hnaoQJoNU{}m~t+Z!$q=YNqAYaF|Z7jU&%HnVeCwF zF2R;9_J(DA*vum%GqXvLt2S$%4o;~KN;w0|Aya-U=ySgI+9@=+mK*eJ>n#3hR1@4U_G20#n|h&5Z6qh-+%dBm99bz z2$a~3RhLR_fkmGLea=`FZ~KD0;aen;Z;jkXu^t`h>w$c|J{1}pYx#R(Ca%Z(% z_XU*%bo_hqT5R>i?IV`H`e5yQi)xla{x73F`*!G<)4?Wqj)W34=u?tbndT)tHpTIr zv@H~K)X7v{N{n(!vYHS1;tTfUi6P+`AR{*^3h{^>Gbg3zJPt6*utitSMU-cvshqtH zK5P$JqEZKZ;I-F_+j13^6TYQFWXMRu(pe4jo3QcXZ9y2A+QH96nD%>a2wu3@&b;x)Ydzi#MtA}GtM4hSPoIAPP;~V6$g?!iCry04O+|%0RaH*sF)Xz2`Y)Rpeb>kJ+~=Il#UL75Cnc3)%m; zr40{7>urO5fKFY_Zd>;f!@u}DkHQz7zsB+D(2qsYo1$mP=+W`4JDD!0!1Byl-abx4 zJqrwO+K=7SWr)4F}feJ56yGPOFh$lPFMu@YqvU%6hd zPGZSnqO0kx7Sxe)G)!u?hfJCQ81P#&fc-=|$fFQmc>UTn*@PQ8uUxpfVu)E%?sa*l zcLWN&fp97HqS3_nkw$r)kr0R8Y+sBGwsb>6%{S3OuK+9* z$D17%|0!l%8evpetlW@gfhKeCwuoDEWtBvh)`th&)TTtqF1mdwMR7L-HqLKw3{%X&I&IJ0Ucvz}(;(ue=vt>SP#t2GBs>^Yrrlz0! z)1nEboy_~ITQXNt$q!G`1CD1RXGOvzxQ(%@gd{bDKuMSN4BQXu89UEBd0oI@6pkV> zp*&|HY!+E*ioX5oH7z-34oz*HFp3pEKc7qz4oi zDefD{tdx&2YTViKb=bY3D2)ZGpc0wDxycdQ6PoYHA4X3QtO72`>E~whQl|xc{+ERv zo;&b8VPfNr^R7kHy)%7pUmIS~s-tKKZ_QlW-F^JtEw||A)6JX(3`kJ_F|l)r`@Xgvd6rBl zml*;DbTnXwIJL5(DUg$=?4IzO<#dnYOssanlUW_h{5bqQdCBR9-VdRKb8WvdWByvr z!!(n4k!iQBcO#hjk`vM>3U!T;o#4)>BDF5sw@C)$yiD%dKZSo#euwyC)I zmJ9IZSFnnfhJ>89JyIK)9W9<%wbIfq64(mJuEwsiHLB8(-v(U9Wy!rCi4VKm--!lA z^TAH_qRv0JTL|3chaxWH=cW*Eb$nC1En!L!IlgH5!!Emi$lNphQntaC0U@}>i@@Em z*UBlTO0c;WEn3tn>(HS?x@O!+s`Y!GGABNRrc%{`b&0@Tp0V#Hvh1mg zw8=|Jg+D+PAWzbIZD~egGdE;h%KDx0TO;RSC%e(5+MbA}ceC1=>@tt`SQs(xSkbA< zo?Y8J7LLiS`p)m?Y0LDHxm$NT@5vy#3IQMuYx<(?z;29`3>h=+W7_gzt8O&gyD!_b zRGwp^I_PflD<6>%MltL)aRu#7V}H0X34ZC857^>PCi|_&Bj#5rygsD+;Jh|%+N2o& z+NN#WWAKL}j~_W=nft91G5+|{@WL0J%WqoMG__R5*?U#H$(ZQ$Q9=fu_w)G8>j}ka zUMLXXUmbt#@MteDuOvJE6p-Q^eA=)#%)Si%74*%G+v>|cp1mg%LC7^9#z3Ce>C~q*2vxQ7PA@$(FFHdyqA{4XJ z^TSg6zWMr>uBZP++QR?5A6qsNMV{4%6ts9k7{pk8ONDq{d+HO5)ej$)1ih>l{E?@@ zf3OQu{1FQe|Ljz6&caO%V>g^~9uXQFyJAz);|)0=z!6RA9ZKUn|D)gMzgL5 zi&{TNLR7hW^;iZ5k%Oe={Qmae%lRy@bX#4QX`nFE9K|f$So7nz|GMb$vOkE{_WzUh zZJ$4?+se)TBj@J(f0Ds9_#;1M#y{!hwk(U-UPe<0s5WF|#{rXLUAOvIjsJ=uU$ zRlp)_+_xW^?ALg+&qDja4pp=Ae;oUl`%Sz`ft1oK{V)FVfAa4A{Ezay|AssL_y4kC z-gnAg)PVn}{d%06zvGV^EmB4vIMt}j$mhHIwUq`uxz6!Fco+Ylzn6di)&7e`@UMQE z|G7`DxP1mmfzHSXfL5(?uzXqrqwj2ayH3l+4W8xoH^IGvh9Kb!qLv@;Y5H}WEIxnx zjo-T+AJcse%|-luE=_nrs51Ku72h0mWW(EUDp%Oe?O+k@T-6bzjn)GPUe2%VvKATg z1Waq`Xfrg?I&IgiL9%iZrTlN7H_%{;jxLvbTGjaY&`EN(b!ZnlxP-s`(EV|Zj+F0u zsIjUjWkxWj$oY#O&O3ITdPtKoM{?bB=FAbFhVJOH;jbw7G+$bntAP}<3km*dzE@du z><$E zTc=ZS@-=MrI?;aKmf-knIAT!U8m7%@{*++rh~;UxAd!f0RoxQrQaDqJw}+ab;#zJL zWUt#99;g{wum_@c2|xj9uHbi!fPdScq*jT7>L94Zd(;yF%`{YN`}U#SZ{yheAZY8{ z=XeJ+Lz@hQy*c3{_ z+KFA~Rb7J{w3L*D&&A+Wa@c`(G#o%yKD?B=b&Xs*w*QkJym!AudI!pxr3QK3l178d z4PRT{?d=s_naIqHCGzE_OX7eZh11wW@ZIoS`H}iKO4J#cluay15LA3g>jvK6Av=k? zt=qBVz5wH~=xVdfD-L)TBqo$=GGpp0L@d*MXL)r~rba^4tk9e49oGSGQ(6iKpz2TB zN;HfdCTi4T>DT^ajk04VV=^i_Jr2-oU0c#&@}?KA5eaIjuwq3^0nC>FrCPPIre$e6 zQDm`z>DboA0CSl|9As<5Eo5skqq2C*l1alhS_^BWkd-VNKX6P)yU|1z1b}^FU%aD} z4XA9is>yrS1U}im9$jX?uCoOrSPwM2uoX~1s!vjTEe&A4erJXaMqXiK1jqD9_Tu(5 z--(92%cW5~B#q1v%}5F;`o{9RruZm+{}d!{wW&f3>UyYnnRxllGm!8pZKfB9igpuz zzwKSDCGCUf!(Ce$`@(ZxVO_&gGonou`cDA`EoKdG`}NhCyU&JW=uEgXYTnzCSMR7E zc5&ZUT)-9-k3%jpnuQVdKpvKjKhpmnUG&9|a_#oDru02a_C?(tdGqFQ^9=iwzx)Gi z@br!PuRB6WaS7@aPwf zJ+64M&*+P60ECF8Yeaz?9#wun>75G61A)~S;>C6Td>?+cPWtnA@8xwNjJKs|DdBn+ z9R<*c<3l7C1jIA8m@b<}pfo`KvsCTsSQ#@hYZP7}H*d(!&R{rS01{!kYrh*Q3^{A8RZ4K zmL(qxifD6R_SGW~lb=*xnm$t;V9ernTuL~JhWXLKOw~uR>Naq|(BS$`6o1t@JoG}) zznB|76?+i!uRF3 zUmE)yI9?{gROF9Ve)(=q)-k5VdzYF?n6D_<>~X38jhho3e2SPjfi2=1F$rZBFiXer zB|c|aar=WRmjb=z%Ju2YDpX&Y`;yeGV<>RTVUks6GPt5$a**sCvYJYLN<^`q=7~J z|9#rfJ+4<%98CN~Gu5>CYBhriyg6b{5eX!AN-_50Nf}1`1+>IH3dhZNg$v@4W9e%WK zDM^EuURC4QfS!572+E#T>8_^;8UX8drNKb5(?2y{?}IjHA;x=W z&}|-H0C;k^-wOK{E!?rLIY}A11U05iIUy`Z<rkk1D1LmU9*0G zQ!UdCGM!;pd!3ua`w2zuE%`Hs^4*H}Vpn_;et<;KGOg!e3u6{^vRO(>{8~qer9psz zE$&>IMm$nyJDO-3YF0HEkL;ZVNrHgc!46tXb^GL$@JVF~7-G1oY<5)xjKfGhZUlk7y5z@Cf+ya8|w z^K8v4J8oZkt}Uu3&s1E0qOP~g)8Ha@lZ)w?MU=a^AT$h5%cVLEDC5Aj3n5yNq_S4a z{(6ispZ$!m;sP^S{SVFKBIhvkKQxS^fHS5G%b+*^F6kpsau3*VSy=;vHc9piDE*w{ z+YK;K;;7{rbLpiZ8dhEnm@QsP?P>SF+LotoC~2nrV?SSbohADSZo!I6f;41e=h>d; zA$~0#zZU8Br{(YxB-oP2nugmWfr{a@vBf>XcOJ(}w4xTNP?Q(COJ}++XW$PDKhqz{ zJ|7*aGSHl82R=^}Fv-bNk0ogCWGnK^d_XP{B$m1*TteNqdCd;X;`nVw)~U_Il?9)Z z-H4>(L|5zVT<_I^!}@mFPGD5341YGnFQ5Om;g=A1v>u0(ZDfp5t{LC)&2Ey30E<$@ zX=ZDL6bCSZYQ5tlc1Wa;TD8u+{YL=%vG_h}{-~JMzRu&JIE_GMw)R;44lNzXfkD5Y!KgDxV#g_08&7Np}>wB&qpoIpIMxW4%?a&kPpf*1N zUUNNYz<1_q&akB5;tdsxfxY7lAy)KPB}S*l?k3~)Jm?{pq7}mflUtE4I)nbnmqO%K z<2kr{+leZpTeNNc1(es8yCX1jO}IX*;t2T#J|`~{`-AqPc=YnI3KT0`BcnYIP|(vu0qP`yKP7?XKvV)23tZG zT&It;1QCaTQBCufdo?+T4^@I@%6VRb-fAAj%uYE&7Qo<(v$vm;0f>G?8R1}30>F$_ zF})i6-Us)`0QlP@NQX0{d)gT#a-erF;?3zma?Q`d$(@MJE|6J?hG--5xbb~^S;+DKc<+N1O9*j= z9D3A|(VjcQ!aS0*XO>U@E!4TAzuE;fc+CqaM$gg(#K3cm(fhjZQ6uCdt{)Uh4N!6ZuRUbyHvd_L_M8;NI2l! zb}6ZXcAnZ*kgRTkX_JYLOP<0=ln8w+pK#uF7?T@lmfU0~44cT~+e~I^xbP%lPWXWp z6_g24M6B{^<}e>h(lIJ1T0nAKBR5+p$d?PM<{!U-Y+rlYJF9&s&Y9D%%O zX`T7~?moJeJ!dS1w>ABAHR?8Aq|y`r4W3J6s^f?g(CSH|x;^N7|FSb5mA%?>Zk^*C5FVzE2GR92xuF{nLZv$uKh(6R zBc=WTowgDBqi7D^Lc$}Wa$aCjtsVQWgp`#Il`Mj^5oOd`DV5x=vKBWzsI%%`(}a_| zuGm0)iV{bcOEN51-p52Y4RGg-r1X!Ws!;CZe9^nFuo-!%Ls^h%C2iVn6S7ZPli9~m z`^wEo?8qzKxA36I*b12RK}0ghoREY@H5~6{JqwvEkBy^frj6FBbin8A0>kiEkGYG&)0J|5Ek{-DoF zOS4YfR1oooePENw+$MV~9WD(J((EYPh2giBn(_$UK5GNRU{$2$nN$J@2a(o(;Fq(*}IE zb`-x~!%4(?Adw2c)42~7L4u`6-r@8n?I3VbE=IU6ybhIB#}cD{o~j z#s&1DwHzm(dxDmFdmFY8h2p5Ce{~u@&k#dCS|7UzPfvn?n|{qU&eT{c z>EHpi2L$n+L+Rf0d>=a}f@{o3^_>@;)#JeUt&k|?Cjytn6&C8I&%2%)@RfUE+ve5V zW+eLm)#kxsr(d>zb~rlpGzV3Wm(kw1=gnmX^+!vnu|s6B6Fj#u~9pQbdwv!}b)iu2z7kN;-_ z>3{5~{aac${|dGLpZ&|!lnQXGYDR6#dJYOVA~B4dZPw(Fva_lxH?8Tifim=_c4Eq{xieP{*e7o=2Ry=JZr)Qa$xDHAUgZ(`Q`0*m9up$`Y075GP7^4 zPEIj3IMjFos0h&T7o8ueeU6tGIYztj>M&TU$4(9Tu;~tt+On_Yqo$R=|H>d$ru=!z zRgcQbAf#S)ru5WfK+*U8TvZkpWJSZ-M)L8e+l_$W(6@yp01D5qh4jSmei&ye)-%#y z>9o9k`}IB3Bvd7^39NEX)EJ$-hR_^7%Fc7!x0K5s1RjGA)$)I8Hl)qWlf5f-fTP(Y ziRwu}x0DC8D?*%p<#rg9+~QQ;tU(SYJcLZXsqlx15JGOY8Bg%pae!hdM`m%Ap-}!V z!HXd065>=0zi~yu6oM$yqTnQ<76})?IYo5B%SHgM4no-yHhHIhyE1X%&dr8k=+7%-Hnj&@L z6TK9IvNsyvyvcG`_8HRJ*Gy%kBIw`>%gTXB(kO|Kq@=7ER?q7Em|YW;os+vnQ(G*v zm&dy)_YAYKH2%L^Af3yn(lsh%9h%WCnYYRTX-=D}7=-Cnku=hllC{3z zCp{{VA);M3hCUPbjo>5?~BR`KLLk(_ZUBml-+5ezL zb%VME))F=8IUXiUqnQE}@*HDtYnpaT3@#7+K>yHEMi2eW`U~btn6BNtx$MeVz_|9) zo}OHrOl8%@Z9At zABpNmc4t#?jV)z{0(^Z9f^9X<_agPI3aOp+3i3MIP|6ZB#qetG`F1;{F%*lnYk zn=HvgJ1s-=SEo0o3wAu&Tx@`C|pYR$L9>?Mcpp^>kXK9FFn=RSaQC z>c`QEO7~;@5B|{!?A38fJFm>fA4R~UDf-L#Y^19fTz8PrsCOxul9G54AAx^_tx)Zl zS)|q8wM)v`EE;~$9)wK&4cfXj?=7Pns7*p|$1-ONzZf-r9M`=8vUSk{a?w;S*J;A% z*F!Y^)ZD{pu=yU3dzopW>5OLRqU)d%>kTw?{KPXrx5&utCE&dGx{gMp`ArLyB%KGqcb9Ld#5R-n8U^DDh;tdui$d&^(Y z+e_Z{MR<6!{{5HvU*t*r&&jw3^ybi@#xb6z;@BNF?2e_h7r66gZ2$DiWI|@BIK^r< zfE7zs;W8nYq5>WI4)41iH|2-!d7YDs#E<^Z|50d7a?^YAJ&R-scjETq2enV z_LWsitaJ86kHLr=&=l8FfA{rJj|QRqm^oV2=Uz@(z`0PkHH3c5p1+HCLLC#pU;OgWu>c%a*%3bBsJA$mTT~o8t?^{@K)| zQJkR5&n|B~=m+vQ`N_kP{}%OMkHm~$hxV!1)*E582mleA3rt=<#UAzxT-VO1V3piS)4>tFyy5ocnXh^GR=^YTuH&A8 zx@NGvzT~IL)nY+DcymXat`0N3u(b)Ep8=90hd#Aam^aLjn{_b4rOcE!AT9n7ut=`T zduhN+a(*BiA3j(L8getuhBp|&V}z7zP>n4c2iHV*0OScIxTl}Z8YD$3Hn^dLWOk`Q zs^v#&X7wPDAYh8(a*QTDEg2YtBf1j>uHC-9Q43V+FfbI>{t~izEuT#PL>%TA6oO`d~&GdSTTFyW9EUZZH53Gq59b?6Jyr+3-kH5q8W^gzNNzU{2#ql1YD(DAptZMd z45ovQEeLePo{_kqDHS&tRFg=S*(==>H}Nr^+79e{jSZ$55{s@sDK*J2BN)=ghZJ{|W==O3_0x!GVjIg5wK37_kLkd*} zGWsM-ifH;aq~(Q#D@ELx6Va=u$3p25jgxMx3o6g!IZ2wD*CZ1QY?Jni{E!YzZ`e8? zIgD(~(3tqS-{qi_f&*I+Q!VR3f2&k)(Y1-lDeLDxZ5P;f{VTZCw(5`Mahh}DHr@RR z?=xXf#fI(eET4Fct55@*?gz^*`A?kawB>eM0HQA0BQRg6Gq!AQ9KXj?Y!Uhm%iq0o z=kgQGig5#E!R*D)J({2(hZ9dvTw<67_3*eo(9Efg7t_-l=2qBTp5~QH@=y<&p)1x# zdhtGG@*KWIhZGOMKY97Wv0|6e6>{a& z{vn`=nr!{eX{;}K{mgBT+9LP(p2HPJFI}+5fcckw=SPvLO&ib;9R1jC&H5=7AK+nZ zp8vIP`W~~wT&oX!;n24k#cQPSWgiv z2N0*BC_;bsutOMYwqU^5Tqug*JJQ90LpEsST_ORnnQ=j-hJDZY`t&pwEp6m<>~&_h zZujD=*zgWzGb5%b=jTY4l1-Zo`Ze6vS(};{uU@^CyGDJu}XX9(?EJ)4YWe&-1Fx?JHU?MZ?yLFJ|}Wvbg3{$!@@RNXazxM-)8R z3D&s}7E+F9f$3?Sf%v$3-{SO4xHaH*>LE=xQIU_HPp}nD=n*8g;!GuH&q;fR4Z=jQ z2f2o7QV)QG`qS-p=q~TZSFxnYLQ-{#&?_R2=F9Tfda;;YE~e0+Z_ZhnzN&pc8cK4u z5-n8fw+#CSRpO!HsH=~gin`V~_uHRJ&EPDRNS$VL^RyPTNu)W=Pi)*sQz)uhRPgrG z)Y?J<)d%4iVCqC0@eE;KWsLcWN71R^O_m-?K@Av)HYWVaSX`vh7=@D@=}%aDw@FMb?!%KDRo+aIWUwL<-#siCIvz?DdGMkOUi z(XWs%F3FgJ2xp$A(CYaZvi-l-dlKwG;jB~7Oc&!Rn!3Lz@XT3_vVp4$X^>Ey9C~q} z(davECBw|NFNTG{DP`UF25A#X!3dU(x!r=nI2-X;==+WvoIsIIPmjpVyOtGd`U~o>N5gbSQ*sdY2yc3{ttNfUr>CN zecewsv_52yl$%zMVu_s1#I8~1OVInG?d~dVTQ|WhMe2Q$T_v56@vJ?AIE)DYOO%_sgR6P(yqJD82uS$v?fj1_vWk&i&8?&#fBa_cM99V@zuZcLga=Hnmmo%5=i-0zyt;$W*3)k=6i|4_S zDmez9i-6M%=f^x%o}PkUfaK+Lz6yd$UaZ!*8_j+-Dc#riWj_f~@&>S>q*8e}x3wOu zBY|G>7)n9 z5kHcY!VHx|Dl~-57hMm%21T0uUkawbT~;%~`o~7!P>HIlm;51QQz0&bnovbSuObqj zRW!x71Ua4OR60dvwcPh=ljbUPL9E^n8^e5`Uy0C_Dpd+C@Q42)bZi8Lnz~DB)RJqf zxoKS05}FubQ-iaw-u`;Fu&ZTFVu1|IRFiZr-_v!!h#vcJwr=fB-HOHMMljPH*XfYtDOo?{vm0;1z3v6#B`*jXlPea;@(ad~ zM%ZIW6$d}avf4Eo#&LM7Ihb_bpe~p3t0@#q3PxodaeX3EJ6pN=;KP3vJszM!XJHvI z#Qw4Re_A{9u$=Sv?ca=Lj2L97>|`rcWKEVqQ?iwmtwL$EU~Gw$W$Y^46pAb@BO<9} zsVu3KC8Sa)ilGvsO;OM5YQE3!`}_W$f1c+!o;i-0IpprXKlkVJeqZnFIT&RZHW$BhRh#6F;j$opQP>n=;M1U>mvUb%f(=z25YRgBw#l5tG zWp7gtq=bJ1+{xm@GMU{hJ11ptw?mqOJmIW1-E8B3-C>1!pGRBm#EAz&mgaO<-tDuS zv}s0vC@4_AE*l}f763o0qx~@3bz4lR$rKm{TG0zm2dqKO$)L=i{EsU?46`%iVn~)H z2)o7c1MuYm#$Jt_`0oy8_i3Y?oB#VVUS>ATYyQ^{{)Z;`|GKAg??G;(Z&tVvnFz<% zDRacF2V*Hz532b3Um2+5$Hf{Mx5q@`D=;3!UE8FFa_wB_)U&VRVtz0_IWntt_avV= z2AkR&oLMK=Vze~cwMo78+jCvU{S-eq{pTp{*#?cfPdyszs_1`v)r8{P@jIGrdW(Ow zhcbBjn6J0f_R9^6*#6hphL4~}5JB&979EO1K)NxBH%*Iyz~n>4E?dgCtk%tMm~MUB zT+w2k^>Gs62Y7hr`z5c|cz9nN2IWaFlx$j(YR7*x z*U?pjy&V@F&oLR1&>hdMCo-$IRQJ>FesH;jL0zh=h}-Jsso2*hu#@6t4tN?n^&y_^;gm z|KxA|Wanh3wtZn~_kXQ7{uk%}|3ldRZv}Sv8xgU>^$@5VoDin_&DE8D zVro@h9_?xz;R`ddn8Q)9cqqJ3(cZwBfVMb6YGHBz9lO!fc}IB~#u`~*c;eBHF7qr% zAAv5?b#{ao9rn`aS7u6b1`pEW#JJ59OUA&DX76U8F_D>ubl|I!n%3;T0v=0+dJams z^fRRYo`qUfN@wvh6OZbqKeacG9u!Q0BV$q|=>cmx=JK6S5fTlbDfK6e+Oo!{HpJkZ z;mqfVcONuvO0oU+->Gfhf$oD7T2MBMQ+LPqjvLUPn|G(V$2hDDu4?Jq8u-m-phz(j z8tvO85?7$_+=U7^OyNe)m^krv%OVV~uS7J?q_>5of7q-OpT@ZOc4=;rG&qHYSidkWzRHf0z&}3~=ekAR!3>q?~6EcPLMQdIGIv+thU;M~52W zyX3p5W}ZEphZwW&U3Y{SC zWC`Gk1SQFafx5*gMi8`ee#|YjdMtb?+>>ST(QTF0)j%PWbkR4+-LM=pHb@A=AmBtj;QA-(6+oT7bC)CI z5IPash6wmUGA>2#EW^))iOdxiH8uLa67cJ=5RN!|O90%B%hu83dQ9B1dG;~qcqRa? zxI`hf?+5;3l10<KBw*K`KI}7D86|m5y{UT+UbVx{xvs%M! z?QSCd$$)@otDNgIrjMOPAP<~ZtN3*4DDGK8pFz?(EpW69dn|#3!T1Cv!;(-k&T!sQ zJy|T5>b_UmLTRxf0ennJxCTNuikBtaPYGw?T?(|9QqGF)E}DqM&z|Y;$TQ>@NwCF~ zSI|T$LD>{C8qRN4j3|$!H!c!I>6HoiPqul);f44fWVJ2}5!XX${EC|px({Wz0S1&} zoQRH6OhVhYX(Le+n;c{4x`pod`U-v{SIGC6I(DmxF+ndH98olYGFgrW>FT;~>%<3q zu*w9QOKzLZu0NMGEo^Z()l*mW%+gg6b`}b=&77R|M|*7;D!ZR6ljVVdXMp_+A>Z?) zg-I?*srdC1*`DxdG7r*?r=?538!HGimv?Xc} z9Vu*+l<(FlY{IwZ4L5F~bD* zx9hwFRM727A59c57k7xAXicj}+7()$+}Fg=kXl)5^yuevU&UWU65M8a=mb%}(epHI z7ttxbJ2);+GYjK3@kE_taMUVKuuMFm1Y^yc++NU4Mq}88+_70onJCQ{nBGgZ*yuyf z-MHy_-sCJ9vNXTmkC09nj|U2(II~WDLo50bE3jesq}%z(gvne%clhh5g?rD5Afp!a zImhOKC@oNoY1$+PS|AC_P@d@zu&OD&aZewjg3qbylh%9O53o$$7PyV$3!(5~{O3)7Y$Bv0S!_EDbW^`~dQ@Q(c@<1T)Uo_d}NH%V-R=mM}v^SKBB2dYQ z6pk0wvkaD=@=e*_a(SRw8z$#yM@4cy)8!EvfxcKRFSj($`E1tlv!r0F(Mx z5ChSy3#pTHpokP}x3_nUlZ$NQ0Th+&YQORkjoM&ThMC3W^>I})cb36OR#NW>wuOtI zl%kN417cLU3_!3Vp$ZeWpP+#i27rvprJfDTgMfqJw_U^ls#GE7Xodm(h2}H?ViwvR zo)@y&Qrvr*oVGQQ#?UaArzZs1;05x>9G#Pj+0)5*I5ve~B35ji5&Jcn^^+f-heR)g zb2f|z8rn^TWoZTs021X#D7JBXujm@Uut3rJLr&6WGhfH9u~{n(`(Xzza9Rmousg50 z8Y5gGI4a-&T{pA(*y_Z)^N@2d56o8M)00Huo5(NI2* z{-e0icI0;ZbJ-lFA*+k3wR8r~2gM_b0fUh5ZWEt;vBkjC>>((_(RWVmjB|zKLs=?~ zK2gqyXE-xR(vWF|>&7}!<>$lz5qKATy6 z9MTzl1zEb{3oji&c^#old(zM2*2gUxPj^VtR+&IaEmDJD#yy+Aju90o)G}%}(Qz*# zKsQ|MvKLNs&lqyCV6;QN?>vDyyUYvYx`Z(gJS>7d!qa2&x#LTwkch>Q%1YiO#j?;w zSa3dxfgl2-mQWdfL?lCwshH=0&i7GR18G-Wg?9~-%HYeXo?_i5lKPT!LV zjZU*Wv;a%|QPEKlBzNd8OGEdce)4Axzh(l0^A|F-R2qFb=4Xws5auPv!Eci~W!=)- z9#-r}Ey+OzTbGQM$6rjqlKDY#v)bCWZ|5^kYd-2*i5F!4_tWCz;zTV%ANPJZEk0|0 zaao)zC4k7{sr8%)h*B%DC1rerD13$Z7w;aM6EQ}1ql8|T#3915c!+}EPOiBg_T8l7 z8sj6Ecnwuf&;2zgdM<|TF2sCA!%`2tgi zv@EQP|MI|qi(Pw4PclcuWW%*}IFwk=?IS$F@9tM_#Xx8#S0K(FrrVn+Y@AlyUH2m1 zeRy6Z8m_~cn-@zj6qUw&Sv#a}4m|ETER-y$O5!3s&^FSI2r}rTO=Q3cbZk#j5a__F(Wd_yNI94T7y6llWC|yf>Jo!YC*j`ko?J84T*z zpr66{%eR+D%x%MjKeucVu96_(b9teZ{lr$0A4q(jwTK)(_}AwLY&o9_yWFMW3J!QL z?IM}!0t3z{@qiH43p6$96p{jH;lCqOZE03wwCP{k_wmUI&Bc8{y58I)p@L-u{;=Y@ z%j7fpgNrk=hB$p>b^W5T5f zfjCof=$}caRou6!4;$)f_7jt+dphLW`)+_&$TBo6o8 ztEP7=cVWWH0?V!9D4r7@0&`F*Dj}6|-_QM$r|n4a6v8>}s>PKPa^YSDRCe`P*|O1W zpX>{7OQ?y&BZbpXdLnLPRennoX^Q*?*zn>?jSh!Y_GpiVoU-NzkEGl2+sR+2ad-~( zeU_6`M7iW!K0@8Gm8)gZFK&~PJQh}$+juKi!<`b((eMGw`Un0czrjCpybB5bXIO@h z3)O#?=|VIw#QH!?Lmn6Qw!k z3s-w}wur~yFb?ituI`+KS*?qFt?;ADSo_0tcm&>b!e3*)&z7FuJ>TFvTEmeaS!x7%qnOl#ODGLR49^J2x^m)(W6N7@cDG!NIe? zyn0VEOv(}hzeDXXVUPFelf2sC0r=X?&hFAk1ic15m`s_a>l-mUvS-p#g`20an*cuh z_3L-%{h^&i3oom+04!omMHi?a>>r&-Z5;YGaFr@r9{Wf9=+ z6+>b=gWf}|P-Ip?#-aH{VL73sqkJiCJjp6Sp#Q+R(fw|24O*yHme=ccQaLJQnMmNE zCx=SGoIMl*?3Czt^A&DKs-Xcgf*Ri)nKCGF8RTVn2y=;UqUn<=K1EkW%3VBkIKg~n zh9n?F)F|$DAb^_VF@>$!SSZt5-Z10sAm(^+dPZr{G=^kA5hcqS%4^1uxQn+N$6o_a zE6jY`FDK2z0Ng+_GeD1IpqF&zMDoD_BJ@>bB_Fr=O+VXZ;+jS*(KNJiGH<}^*B`}U zH(CGW30iRiT0^)ol>A@l9?Sw2Jr11O`C=w9>-?+5xG(j{2=>0*&F;KS{9LFi95F+_ ze-c`*h|z$GBp1D^Q@|1o@R(MsGejk^{McteyWWO2V}^cM9z#AUZ97h}`<_qQ{tlr> zX!epY1J_(XeZh7;$K#ORD$|dR>RnX!w(9b%y%eSWITa-nldI&c6uS}ee*$SCB=49q zcGRb;s@MmU)D-Q?S@fNhUV?S_M$C;#GX_L`glk42fh(B`#T$&c6JXvDPI%`t>|28% zx@5a9)x*hAQ+h$q(f~o&zEI7G5?~>OZ{xvzrU$xM3cP5F-V(n}|%N4q6WA&@%iR2njX}HDQYQ8`%8T?R%7BjKM<-J91965# z(p4%Xvq+{D=Z#8G4c_D6%v#o~7vlTFN#e~>NOCfaTlagDX3aDR9A%3zY>T1Syvr?{ z=~T0VfI&l_QBMv*4(ClNb@%eBw?0KzAQw%7D?W_8pp}V3J-M7cqwD5?6i$Q17#F#r zCZDW(S*fmVCs?Z{o^Ln3hC`;t!rfW5)*l*PFLrwFt9T{S_9f7!`c_xh=hxT$zCM4? zRu-Ig>f48XuU|uWsmE^X8sWq0iv<1Bwn@7C7k7V`PZ)V>SROMz_P?&~TfVO7y4IEK zZ+uD3b#>fvo-A9Ta#3T?NmKmw`=_oj#1$y)1LwLoXI<~v%PBW3uQkIIXxD|<*hAN@ zxyQZfD*YZx<#)iGw2OUZx;IFhvjDg_GlkhuPAi|ExB`@-0gH|JzO=a4`EwvT$+Z$1w6&apzzbs0?DE20T14h@)!kw@-gc4#oHZ`m-{}M|8G49H_#_8y1jYo&<=Q zWazViLB-4p>#_M%YSNG83`WKx#^kd=5%iw@^(HEPOa|XkG-}+iJt6%R+K!BWVN_rQ zr%wq2L)k)53&+Yvmj>sY7xHxw+)A6((e)H}P?-Ispayw5qT4^qXE(Uf07?iVz$&@J zmBJc~49m;-_yg=L=|VelB@#}h86eXyf$kmdp64On&CDE{H&nz9tiaXRSiF*P%jg~{ zera04aouof^A8H`{s7Wq;l|1pbq43Opz4y!y}Y+VW+|<1F({x?v-0L-1K2cn&GLp& z9Av(l4qKpnAj`z?W4;B zRfAK~ukiiM`IqoJN^(@ZHbf=?#pOfCwjZ>ENHKQ#lpHv|i`h~BudByinUuNFdS%4d z76Y5s2__lFrWjybe~?~I-;w7zC_?KEMvM?{ft)!qm|1+5()JfA*=l7K`}ZhV^~SoQzKrUJ=xAq!56uC`&V;vpDbg zr_XDn`j5{}TxgzKVJk6d-B@2I>MkL>^JrwqmV@cde_F(&Tl!i2TUft9??35R7?bq6)qt={Zc^)sQ0j7Hl7b(zgV#7Cox#rhfZQYu>fCdv?H=Oh!AXyPz zJWqNwjtC&>dk3MwW8rtfD5aZfZ*RZ5SloEY^?c8rD_!NIa^rxc_f(c~564*1VPGEz zpI%0oPpB?vslL7JXwW~ToQRh4WxxFSlrEoT%-(!+J!x71*h{&I7BifEz3wqC8DVo{8IW&(BJRm z<>guY&H*DJ)%Mn1M0_ywD+h$ERjzaj09V-V?>sbNo}*3;23a`6PU9z5TeAJYB1kZ& zqp6mDoRlmOaAre-%3`dd%>0qjDD2V|XHwc+*iHg2=(&u)68H-|upcS`iAWM+gib`N zBk~rh-z9@R)~!lypC?BnDhYi8QJ3Z{z0QncrJ6S${7S81 z?hBRbXelNssz|V7G);ueEK_HI-O&llh)RdIkeRK);C(egW`z3f^!#!p%gEu(s!uP+ z3j>-Ckr`mx)Hf@trt=2o9!tkzqYFo+msHf!V*w(R`PI@8R0eYgop`yF(kS$$>+M=6 z7frc`@s_pqtP|rJ$xK8%vGp=r{P^PxXw(?rIJ?&Isp78G3imhIiebBJS+QdL1(Yg2 z3RGb!wY7V%U7{*;1{W98JUU7;hT%JOrQ-bS*YTASqBdQsCHTpO?H;d`k%vUUGzd92 z3*pY`SFC}sB2XZ~PCR#t$}m87lD4!MD6!z~=M~GJGsBt6 z1RP)`+2z&M^;HI`s1q!e1FjBh*m0jW=q9$UUx(+K5QC%P90^D1Uofh)VyRe&O(rtb zOVs`V4@h+$9h~n48s1fqcvh#3)DibWsnAG1hi1YzIwev{V9q0HBEFK5Cjg7aYc7^? zC|f}WjNuz_KuG&bn9y}pv2NKNXh(@d1T{k%vyOsO++$@d4nRvLEwVunu%a{-pDsRw zvd7CpiN3-B zKge8`U%LH=a3(j_Z2)NQF-uQ89Z^|`9G@gon@!YL|XBo|0TeUTu5pXlXd=TGTHMTIWVX%}>mmqhpu$P_oy8K$SrLX)VOYYj1t_89 zIFyd7;M&0VR;&}$6GlRh0!{~rA6X4DB~mkox9$*7>s(9Uzb_Xq3&4Vq_rj);KF1JI z#7S!=1{Ru9GD=FQlE_nB0XQPpBwbQyn(t_}YC#3Ws-e@zTw&{h1L3u?inw(My_B>J z@ggvDNK77nM`f!BN67SsqVDE6FqqOkc9Ogt8n21sPXaC7Kcq-tVz zmh%;d!q76@t71hX0}Lw$wx#{&fv$Q`uZK1X(i9Al8c`?`Gd-5#Ad^T1AqY3Q=ixN5 zQMI-ph*(>S2|>F?2roE^T9Wh*t3WcVvOiT^#Q zVu5!Lo33zBz>odGR2G-cu+h?4G~fJLir9;iXN`?@8zAx!L8FO&7hU_6jZS}Ro>94H z!_oMi3Dlsyx^?S@OYrL}6`S=JDe6~9ZKSErCLYccUQV)rqE~~+I*dfeWdoY((5vEI z-6Qod>=orwqlpvMU{>ftk6mt=u;fwC2}-@SALbX*BSG6b(u)p99-WCXKyUEZcFrW)Nv$i%MIXgLd3|ljNqeXX2S5 zq-8roY3N0k6V((FwNCW&bbZ&B=4L`S*wMC2mt*z;Xj_I!seoiCAdObIH_=4Bp(bN~ zD!F*-g~&)fIE;r}ykN>C<9s?L_Bv;DB9nZo2@+Em1D_41eD~CrMSL8;oR&ekhTQsfB@oc|Lli$JySo?@kE@tNb5^g)o_^v zTzPeE)dy?2P_>aO?Uki~8)|ei21_?G>NB~3ntUUUUT+TBLUD4F-&)^g+Pz?t1717< z2;bK?=HJ1a_EEUZJbw+(M1z2l%$mD@ejph}G7X&jx$_aY_DDTVNb>w99lyVyB=9`>~ey%4Jp0IajN6@(ik`Lx$M zQGDsjTD2U5c}pT4)2B$MSAdWt0%N?7S4ZS@4Sh4*@^5RZ@!>9n=ksZHw6F0XU`1s)cS-HON~HiRKHif z_UOP0H%Nw$Aaf!$q^u>u@PAnwO5UvoL?yaAM?y+UHF%CU`>%KH_w5 z?5H;=tW?^J+a>4QlVVOTFjP}razp=1?wIQZk-Q`Wo(x!4xGu>r_mb+>h>U_7^W5w& zFH1_q8L{w6<>ud0lxJ?#XJeF}Vg47@72*D>Ob`Xq_b-kkt&`wIg#3%*%0CTGJ%>K~ z=@egGOl$cK>A%nECW0@rCkWfarJcoA^B=h?H0Sfe{<00hl!a65@P?v5W@N-ryvjU2 zi#(DMD!BePFNtuyxNh^INzAj!8GM7&N$-9KPa@gWH$n*~7ReAR|A7V&Q-!=zf*1{S zXcmbXn6n&c8TY!Qp_SCI_vC{!DAI-gD~UKfo@ukfQIbpB@X@Jlj#nH^;Ko^yno*$k zWDx4p_ql;b+kOuQCleWHSaH1xPe+WSW|o&ki7%x9S4FNMVim09Gd$eTBfhL}sj{%w zEjT85zIfGfyWoB8mwo{Um`nt4b={l4{WbPIAQEPLq#VgYbK~%s$vPUCV040IC@54; zDXD^(p@wG1gU%GcSeu_oo4DZEb&@N=`b8d25P_iA_2Zg&N-qrn1P+-{cH29OJre4T zXRHR!PS|jTcM1U$^N;Vd7Ef(LSSD9iK_AjT4ST19BvehF@lAbmlpn%`A0*OaI1?t9 zmT_HE_u4@dkOYB^l1=!Yx~3p+@B3%5=`N|oEHg(?PN}W<5G-f~830`l6T#ei!6aeG zhk+OM7hiv}8Ue@4DjF$14ORmvfE9W#4KP+_&qrMyJ)1Y* z;Wke_c+G&UHn9h5#NT*DC(?_nTy31*lzS{nZe{$$y;VgSDeo&$U_2p}Wr5wbxI_cZ zTYZQTIk~>a7%4MEJxZ%M>I(<&8Q>YIPD86^j5~@eXFot<@;8r=S~p)--xAJeA@|XY zL+YKKUQ%Q8BtH|es7$Bi5o3$-_DW^byJf}AzHO8v=NhWw{+&7nxZQ0PX`&3#LW~H(sWN1fl+~z@_~V2m`P>PyW;kW|8()7PxUKZdCwR zGTN0|mL=;Ex>bva{wJAZindraYlhKfb}-$M;#-A(IzidJM`YX*%5*VKo+WHU9n<pB4#FGN z7Ac2dslTjWg)aWuF>p!_foVL+y;gOvJEgK3r0p*?EHu-pZ#d{6w@!@ngiml1uAI#B z0ZpF8l@9b7e$v**tJClYF2j_X^gR0JPBTy0bm&FK-g!l3cKRn%qBNsYmi?Ass6YFLR6G zk&#nQe@iVhq#s5E2!UUD9Yp~b=P5-4-m`~ovdMOqnZsC&MHmC;MrkNM)o_{K3H5dE zLTca;6SmbFsCiQM2z7-tBTQx*(IK134v<-X-}v&9UUnfg|W*aLUIa0Yjbp zP_2OqJSZ;=WKYU|#BnPC5O1Vk;RsjzOv2qo^S3HmpgA`YT-7uXJslgX9} z$<|3qrvXo1#x&>M%y(bgNu#|0jco5S7-Bjb|3PQmx;x4<<11Ot?5 z0}*hMeN5#PQile*} zb-qJX8|EW~knjjKNK}GFAMYS|f><*Ad%VeB)#FsG()5}W&xzXryxczBTX|Kdw2kbBu`S%`JiK7O=qkm0Zg z1MVCVQ^?in;t_oZqD{ zCX-CW(n!33it8xuPf6XT)Mv7Sw5OH96ctpAj-l)6<@iGlhemjYizp$zm_-Croj>OT zk+gqJZS=&I+26}Ojyy>t?AWgM+TaxnNVsmf<}`Dn`1b6jD1;*dUB!1FRISHdb^Kk*1UNqsa(j)mEOHhCKObvxUJVYNHV2C z?>_?0k zp(aIBj2>ttH=7Y~Gjj<3Q!9YCmTJ{=PPQ_UMq{f)w{Ig8h`wbtykWO7qjRb`Q|&He zTE1sFy*rZ+uhf3|#dpE+UElW@e1u(NgfVHmaCp;dQBN_ea+XdZulqA>8X0x%sYl{resa{=12$jma?7V>8xWOn2IG`5w2Wx; zmE@v#2()voS;tW+*B7cKgETq>gVowxljR0G{>b|nXk+*8P*}B9RaN~s8&1o;Wa&7y zsC@JCWt-(cSN_#oe^A(!tb^spo<8LmG{{>gX8L8q=B!;-J||_!(2d%=c6B-g5it2# zZ5C^}ngh`Mh3)gVNn7jc3P9ovZaU*^aduMG9H)@KuO3f`IB+fD_jNYL?>=gU?A`XH z&M+zdfpm=@FTA~~vih$)#Q;Bm6zeN1lgdp;nP^bbZyBB!{T9z@>49 zs`njhTVuo@3(nL;qo*Y2(5F(2^6V`fci0P;-Y+tD1ofkxU>aqc*}D(0z0%(G+zR&s z!=-F(@x20SbI$FxlGb{t?ej)D<|G5Zpk!!3J_LH@N!~vD40AoP0%mhP6zQ@!g+aG? zNQrBn`8xoINM{;+OPs4oMb=al&$>IPk1vN`U_p*|de4nac&AZaG#4Zp<15=#XsO{g zl&L}t6tp*g!xwKku+g7~aT0|te%(kBahw4IA|VnfKFq2&Y|WbI(239Hd)7`B*LRM) z5NL(?VP!@Cd>mnx=8^bVlH@4bY-h+6leO1E;ihS+Y3LaN$;eaq5OOM0JWaW~p-)Wu zaZOM{X~sZO6TFw)R!Ged8?3pcsmiy=Ikj|kA4c1L;r(P{eN8q^S(ClLFK^bYnZH9% z7+4K1XmDgDTXv)<(irO*M;0kFYGize!}p_bO+m3_Jh#lQx6gMz&QIiCl9ma6Ehr;d zQ>H|QrDNBahrE+Ld%*w2;eD6Yg)ue@FmdV| zF#|-Z4x2R?wAA_b&b9(Y3a>;`EyN8Dr7|~2pb3(f;MlzNres=HBq@Sai-DQL5bfm@ zyg+fHr|9o9@m%tV3t&LV&h{o>)_AwC98n>ICB9~WCm zAukTOT~M@AIUdyHq?-<2q-%Iyxkzn07Cf0?hk`EvO+8}q`ii?FE5afFNN>%dWhqBU zjNsUv=WYlOM|ftWYWj#xW*#F_hXDvkYUrrFRRD2Pa`BqR!z0K(0;!NIHLsGDazCxG z(uxpuF6=#iQRZZsD4=G_)DUse;ijVNs%&C4-Zbps{Iek*A8CN_#=f+#1qqZM#`OBPo%Rq5T7C@d%Oh2iIp-qRc)X!nX5o~F3*VQxuLeMvCo^pTL=oJN9 zkvxQ>#uKkL5Q_{#HbSx!O{(mtrg}K=xm6j;YYEYwHpZ#>LA^Il2M%Cv;j&MCQR_gP~fn`pjqeV=e*-~a8|X9QddP;F`VP>_hCV0oJ)eHc|+jU zR4>dJ(z#WluJE8=r}^IPCpe|hRsg?}X|_2s?C05!KmR=abFRvt>OO82hIdsMaq2~} z&~WmSsCfZvYKT}|4*dx=*VTPU@sj5)k6#=|lIAc(gZ73qs{PvmZ>sQ{9ifq2ILAk< zS%m+BaoOE!1RN^Z@$`bn=5YO+N8pgooq!Z&EKus@1I5}Q4Ob}CAAF5FNw)avv^<7R z^jeMBgWiBW{`V(C?OhjVTe@T0Hg)_4Ok6vZ|8rZ2p527OvmFm0QdsRE7Ksr;Jufzn z8bJJ1ZcAa#?;|)Avi8}(&%ZwQ(Wul4-^c1cv=()RbtQP75eSGE8aWMl2%s(tJIG*5 z75pL#NgYBjQoL%=fHk6`Jm9UW_~f)J;8k_`+d?2D2tY7$lILwc!GhbaNz+F?^djI` zM!%V%UPZ3#Wp6o(2}OftEVNcS0J0hU_KZ^c3}ECpZrpH->>Yb`#(XcTq&;f~EZc6p zXh`S64R~L*NJvQbzI!05K)&S!rNGl}-(`9f83p*91zPVp6O=hF4}%x&I(r>jbJS;! z(*Wzcz_G1Nn?j%88_+XZdwQu zoQoQLxWQZc+Cxb#?-j#Zdp1HK&3K4Oy|9+JnRBd#$@>HuK^RxI(>b5$F9{X6ujD-* zO3gy~U6n*#X-#G1Cy;R#4RLw?wvZw{wBMzzUHfth>1(Qwt^XDkHsEB=jIdo}KMZkX zD@J9ZTW8bEf9kqTiNVB5!i~ou~fnSb(A04}Dr^kbJUCqyC zDod7fk_MZHoem8(GQNA~j>zLue~-R&$$ZtSsLT;rd4E`#TlJ1S7xSH%Iq|KpukU_P znRnM7@s^Nn3nyH(dzx{NyL@9#e`g>!U}M*gMS+B!OY$JL#yq@|;(7_6qEw5zD7 zP+sg`G-O_m`SL3Xmn^RbKJWN?QGfNlm0pau9Mo1J?t3wzi#&E#{b64((Bjsc3--D6 z3sfq%&?7MY{QKg?H!%{eo^v*{fWUa?{{2Zymkxwfv6uOta%5ay_odIK2YsuCYP1r` z5)BuV5!opz%{hYya&T@1t*`@^-hH=Aw{ClP@BZl<9iP)@&fEg+`-IMScV)x{i{T3N zo11Pf*klxB*7-o>^#fjBDz8#Q&r9u}TDO~(_vg(8ZAy=guuE1s=;wFp#0iB{j0V8z zjy-!?sxgMC?`%5Ba?{m2^{tZa9TBee>pxu9^ylD~%pr`FMjC*o6f*8^0QjnHS}BQ^KCQ zmbSKZHKwo0xqg^?8<&J;s>Po-yVrDSJII0g z>%+iStyK?y++qPH4l70gQOf)fR5j}SxT;47C`mLTf zTtWEW!M!BiDL8CR`{@T_->vWI8m5t5J<82A`}Q^c90539eubawYIs*;`)OunwIe)y z06OqQ@UW+U1w}@-r8i$qwW!D0`sz7Z)cfaGR2MH^yu{IQ|BE3?z2v83pAiHmjt!_< zy}n>qUrqNe;Ak1u@V~@+x5vQVy_>?CIDPgkXVec`T3Y*9QpLqiai7z)mn>ZhC#|WN zldhj&N7sL+k5BTdbi*B_tlb9Yy_j=9>lH8p9 zcrD+bxbvs%z(y>9I&$a1gJgPS7l1^==f?b`_yl>Pmw`bi za*HI|Cwj2y4jw$X01#*A-n}Up1nD7U!JP8YA(w7*du1Pj{=Y3Ks8?6__j+bm4;|{* z)w6oxhMJcKq;0*} z!C`mh^roXe8cOB#F7IKdGHSv`F>}3XBv`JzCw`lu!RhZqqVq1jE3Mt4EL)Q9Hn{%$ zyVOpJ?<0~_ecs!q^BUJ)(v?i}EE>i&n4q=zuFL2Jvs{+w{PEm zI=dJQe;Nhl#iT;N;%2K8wk&h}_0PzSTY`f%HGB6qd3=0*aetFs_VeP1ZL}5_7EO;j z|JBG#Z8Nok@q<-J(4sAN_GW+iZrK0WH{@!gHUqO4ovo=^A3py|J82&sv>oIJ+0|tp zNN86M@I%2VNl85h4VqRxvS-i65YBF;J32aEjHID*Zkc)d;h`(-`U3n9vtFh$YQ%__ zo11rS6Vh8#vwM#ojqeNB#k$`>cHRc zJ8&SVUqr}ku#D?2o?qIHwMG}S5Rl+h^!chutZ@SRT~8Yxq4~_2zxevf=1V{JbKH^Z z7M+=4rWrBvTuH~h{rmTa*W0V|WBy)RhvUv!_Uhli1+jC2scB0hvF3&z&(_>B3wrdD zI-{Bi2lPD0cJJG_`Ou+54-Z?aL5lb%E)TN@tm5?w?Uu{(<;@?uS5$uftUR~loJYQDQc_YsEv;LSmACHbb)VX1+iYROyjy-J zI$9qpy97AeFu!_u80L9YS@R$l0AAgiScDOQgax)OUXYfq20w?quDN*SiaKk~)&01^ zv$RK#+QPpZ=jl0S=Z^rws^_=C|6B;9rWkdo3mqVfCx?hhoE{gdXDnMgul}idok7O6 ztE;^5<7$al;!_$^NlzzV>Mf*Ru_|`SPv`&La2=~eXWJBArWN89a zHkx<}D(D5**KF{L^9Pkzb30uN37J%>;kV~mr_!8^$j83(uiSV1yLRUCBQSaV(j6%DptA|Pwe{EhzOnO1$xQ$hSe|s^ssJ{ ztY)X8(6rqhkbiot=eNtR??(~i8?~8!gAhF-#C##?Ay8H`fWzkCuU=|;jnNg|xqCN} ztW1yLc&U>cvlrZ$D6!3CE9hVsg4Cq->#w#k^;s8l3J~A8>+X@f@xV;0+uw|4Sz31J zy8c~{oY8AN)tx_1+%fd;^?25rn|~}QSd?^p2p#&)V>YOfTAlK>bsy=3oA!QE6!@dADEmIwAx|t;_m9O?~Fu*3{G# zP`;f$f8KAjh4Zqr=g-az4i0_^o&Gm7v$mcL3tyP&bMRn$DH%o}e+TUL<^YX#i?ua1I)xoKy3;Bx1$zK=^P`VAVC43aP~q3EyI3%_v&kTd>3 zy_ZB5D!lD?t49G@Oi&I7nJ-Vqur-0oN4>bG8xdN{Jb zE0j{Ryu2mpfj++H#SzkENWS~M!X!Vpznx9btE`XjijVx}M@!nrJ3i}p@X0)%n3_iM z8*Se$8=X5O(8v~7W{$y*d z6{pUb-c#Mhl7-B&yVV$nR0mVNZ} z+Va$CG#*>B=;G1Oh}YZso1g9`=k*^lWaezYM{(_Ywe7*V$8xx( zHHa?rW?~ojyNrwa@om7AK2qm&?^#`2TXp%~+QG4jL()h08nCXV0-N6(Y#}cQDXCj~ zJ-pI;7hl11+T_Vw=AIL=!MKU1N>co3ncd9EQ3tmh)^*+Mwxd7Y_+cmBy>Uk`oO@&C z1Qe!wWJ)#CjJEJvuUbNb-}3F-26WD@wIgXPxxL>1o1CF6T>R-55hs<42j*jYma2Jd zowA1DKkQI`hsxwYeaCh?e*{NqbIE_n^5rSc5%GPFbpGgF`Mzj&5%g5+YI4zS$8~Jt zc2Y?ERB>@=pyk=7-LKy}vu^QGT5c!LoSAIA+{x({i)85__v!t@Vmmum{II?%;e@?B zbxcMT<6HyURt|sE*WkCo$u8zIW{hLHLz1;O7Cn|ATA!@FQ=iuRgDUM!OH2DOH;!iK zzMBh9`1$!=>u^BN;Tm5-{0u%g-2=;e3~cs?mEB+#&!uYQGc6QdAg!)^4TzG+Ns1p8qBd>!gMz`G<5xc*O=+P(%qQRw<`I5Wz%3Ls-B-`6 zql#~%p!k4lKc1qU>SGC;q{$|Pa06aXojSF5yp7JNjo>OxHi<9dY3r+5N%{H5Ir#Bq z+)5Lek9rz8;SMbYQ>Ip4eB-CRC*+$^(0;{3K6P6($5hLTu6m1_@NxU*o(qNCp{l?U zaZjdeubyYZ!g|EaohmBq*M|psEDvjSEefh&<8d5T(T8Pd2YlG6iaXiahxouHpI+W< zvZ-^IE}~%mHIIF8I z1ZO%y4e>8uA3wwAeBtJur{m*~HG`R?=-0jbxOR#2BerjEHtEgW`1L~@b*D0CrpQU~ zW-J}V_^t6c7SS?iesks_4~|DV3TEOh0~A^cy?YR@qly|DfWbVb${qT)sR(^Dv7ho1iP4 z^Guk3?WxpxZx%ht&d$J6=0^o*|04pw=m{=|W;(^}iyfYu;TO4R*A?q~RY!n4p_N*%(Sl5UoF!BGfUrx7TDT954!sJe$6A)W;PjCMIws zzuKIOoo_quY`6X2tD0V!-Edkt0w{xD8gppAxYV(v)96=s{uRTBW;F+6TBKA~x~|9X zg)!oj4sV)qHdCLTg_DcR!Poh?;C*W9K;62}X3m`cM{Ui`dyYz3qW>7a%(fN=Uo^9c zJz}v?a_L*21G^C5^xJUqr%j;8Jr0J%_U*iDXTrD7rcjpK6&&g7jKFD}v|#srj5zv7 zoJZ|2L&}O%>wkA}cns69g#!1q7b}C-><I4RTT8g{D_36Z7rJ-v&)_{% zhix=6YS!okqwdTPCg2y?cwAXo+3vK7Pe!(;)7g9Xv3>i-(M#Xmr}NVO*UzbrB1T5T zaWdJ+4c7{MH?$K2%XCw9hQErWgSN2e-EZkHUL1qlWdviwa_qIgRDC8dn&(uDQH7W$ zZTqr%R2Qal9U+YM^8Z7#c%6IhV9NnTxr3TJ-$NX;4I8NCC>vr!+-aTh@Y+PLDJ_lM*QYz!yT$6DHsItaIWGAMl|GZ|+8jfot z`Y@X3>{C~I22rDf1))~s2ViI3YBe^e|kHJ>$W8-M{^zD^{^?`UL*2U*Ia ztgJqcEln3k9;}DRSxr~0ZTt3HVcPEYp_|f_xA-;w(11CO|Gc>grt>1yLN6}OTez?X zhi@5jq*t82hdNt5uJX%$=+D>bJld@d;yN{K*H5iFbM~y+ym>9zxC?EKMqkbTx0I z>YAFK`ufq&2CotO^rHrATcgMLQAK6qZ@)EXnV-lmk*ZoT&e>TTsjU}Vx=E8JPv_T# zhW5eZVxntjOH0cFS`EfirfkLM<=t%V`(#to6?kCs!kv}c7H6WP+oR6d4go6D;~Uw{ z_A_U+kV%53wbAeneU|R$+|j+*X`t)crR>HnyfL-?gugOwF3~R_(q62(8+hq2W#ppN zW2GzQSy@`%%FfO%P$pfv?d;;R_0f{sP%2JjI_c4se^f@R=oBAFYw$NlP%fz-Kd&Y{ zDCh?p8=J!;R`g5&m0R}gp5OX;CC<(jQEw9#3}nTPX9z<2#=1Y>zjt9J^qc$Bk&0G< zf7t)G1(y@fds4+yeE9FXgc_$P{&coFM^tICoQeGq(E1@^R@I_|Bhp7 zc8ll>H4mMB+=pg!&wl-WjERZi^BO4zz*sM{@1=lg_Yydh{6iV5UvI^c+KAP;+_vMN zdRfgeVZ-b0_5tso4o~SLl>~u-YsqTmnYjPIQa&#|hbW*aJHqVKE35jV4OS@Tp6fvG zF%giz_VQ}E1=j-HZvDrv#L7KBb*eGAu;4dQQLAh_?vnRht3G{%WCZ{1#_1)$4=pZf ztouKIT6g6?-?ZWH9ftqsU2nL~9PhrJ)&KJ?)$Ckrb&?dP~ GTmBd03B~CE literal 0 HcmV?d00001 diff --git a/docs/usage/evidence/qwen3-next-rms-norm-b200/report.json b/docs/usage/evidence/qwen3-next-rms-norm-b200/report.json new file mode 100644 index 000000000..0fc8452ae --- /dev/null +++ b/docs/usage/evidence/qwen3-next-rms-norm-b200/report.json @@ -0,0 +1,225 @@ +{ + "kind": "qwen3_next_norm_evidence", + "rfc": "RL-Align/RL-Kernel#428", + "git_commit": "3d0bae7174c09a133ea1288d820b63e4a7db8f57", + "git_dirty": false, + "environment": { + "gpu": "NVIDIA B200", + "capability": [ + 10, + 0 + ], + "torch": "2.13.0+cu130", + "cuda": "13.0", + "python": "3.12.14", + "transformers": "5.17.0", + "vllm": "0.30.0", + "flashinfer": "0.6.18.post1" + }, + "hidden": 2048, + "eps": 1e-06, + "dtype": "bfloat16", + "ops": { + "zero_centred_rmsnorm": { + "accuracy": { + "257": { + "rl-kernel CUDA": { + "dx_max_abs_over_absmax": 0.002701418159021442, + "dweight_max_abs_over_absmax": 0.0023356239188215265, + "forward_max_abs": 0.01552825644384992, + "forward_correctly_rounded_fraction": 0.9999942779541016 + }, + "rl-kernel PyTorch reference": { + "dx_max_abs_over_absmax": 0.002701418159021442, + "dweight_max_abs_over_absmax": 0.0023356239188215265, + "forward_max_abs": 0.01552825644384992, + "forward_correctly_rounded_fraction": 0.9999905228614807 + }, + "transformers Qwen3NextRMSNorm": { + "dx_max_abs_over_absmax": 0.002701418159021442, + "dweight_max_abs_over_absmax": 0.0023356239188215265, + "forward_max_abs": 0.01552825644384992, + "forward_correctly_rounded_fraction": 0.9999905228614807 + }, + "vLLM GemmaRMSNorm (forward only)": { + "forward_max_abs": 0.01552825644384992, + "forward_correctly_rounded_fraction": 0.9999905228614807 + }, + "FlashInfer gemma_rmsnorm (forward only)": { + "forward_max_abs": 0.01552825644384992, + "forward_correctly_rounded_fraction": 0.9999886155128479 + } + }, + "4096": { + "rl-kernel CUDA": { + "dx_max_abs_over_absmax": 0.0027934846392837836, + "dweight_max_abs_over_absmax": 0.002058919545322908, + "forward_max_abs": 0.015599696412158082, + "forward_correctly_rounded_fraction": 0.999991774559021 + }, + "rl-kernel PyTorch reference": { + "dx_max_abs_over_absmax": 0.0027934846392837836, + "dweight_max_abs_over_absmax": 0.002058919545322908, + "forward_max_abs": 0.015599696412158082, + "forward_correctly_rounded_fraction": 0.9999927282333374 + }, + "transformers Qwen3NextRMSNorm": { + "dx_max_abs_over_absmax": 0.0027934846392837836, + "dweight_max_abs_over_absmax": 0.002058919545322908, + "forward_max_abs": 0.015599696412158082, + "forward_correctly_rounded_fraction": 0.9999921321868896 + }, + "vLLM GemmaRMSNorm (forward only)": { + "forward_max_abs": 0.015599696412158082, + "forward_correctly_rounded_fraction": 0.9999921321868896 + }, + "FlashInfer gemma_rmsnorm (forward only)": { + "forward_max_abs": 0.015599696412158082, + "forward_correctly_rounded_fraction": 0.999992847442627 + } + } + }, + "row_invariance": { + "rl-kernel CUDA": { + "rows_checked": 768, + "forward_rows_differing": 0, + "dx_rows_differing": 0, + "batch_rows": 4096, + "seeds": [ + 3, + 4, + 5 + ] + }, + "rl-kernel PyTorch reference": { + "rows_checked": 768, + "forward_rows_differing": 0, + "dx_rows_differing": 12, + "batch_rows": 4096, + "seeds": [ + 3, + 4, + 5 + ] + }, + "transformers Qwen3NextRMSNorm": { + "rows_checked": 768, + "forward_rows_differing": 2, + "dx_rows_differing": 11, + "batch_rows": 4096, + "seeds": [ + 3, + 4, + 5 + ] + }, + "vLLM GemmaRMSNorm (forward only)": { + "rows_checked": 768, + "forward_rows_differing": 2, + "dx_rows_differing": null, + "batch_rows": 4096, + "seeds": [ + 3, + 4, + 5 + ] + }, + "FlashInfer gemma_rmsnorm (forward only)": { + "rows_checked": 768, + "forward_rows_differing": 0, + "dx_rows_differing": null, + "batch_rows": 4096, + "seeds": [ + 3, + 4, + 5 + ] + } + }, + "latency": { + "rl-kernel CUDA": { + "1024": { + "forward_us": 24.70399998128414, + "backward_us": 568.7519907951355 + }, + "4096": { + "forward_us": 39.16800022125244, + "backward_us": 1259.3119740486145 + }, + "16384": { + "forward_us": 122.36800044775009, + "backward_us": 7879.024028778076 + }, + "65536": { + "forward_us": 425.80799758434296, + "backward_us": 30937.551498413086 + } + }, + "rl-kernel PyTorch reference": { + "1024": { + "forward_us": 70.54400071501732, + "backward_us": 644.7039842605591 + }, + "4096": { + "forward_us": 155.90400248765945, + "backward_us": 624.2719888687134 + }, + "16384": { + "forward_us": 577.5039792060852, + "backward_us": 986.7520034313202 + }, + "65536": { + "forward_us": 2148.0319499969482, + "backward_us": 2990.224003791809 + } + }, + "transformers Qwen3NextRMSNorm": { + "1024": { + "forward_us": 69.34399902820587, + "backward_us": 608.5599958896637 + }, + "4096": { + "forward_us": 117.3119992017746, + "backward_us": 606.3359975814819 + }, + "16384": { + "forward_us": 407.50400722026825, + "backward_us": 1027.888000011444 + }, + "65536": { + "forward_us": 1449.4240283966064, + "backward_us": 3144.6080207824707 + } + }, + "vLLM GemmaRMSNorm (forward only)": { + "1024": { + "forward_us": 65.61600044369698 + }, + "4096": { + "forward_us": 118.40000003576279 + }, + "16384": { + "forward_us": 405.90400993824005 + }, + "65536": { + "forward_us": 1449.6000409126282 + } + }, + "FlashInfer gemma_rmsnorm (forward only)": { + "1024": { + "forward_us": 14.448000118136406 + }, + "4096": { + "forward_us": 16.24000072479248 + }, + "16384": { + "forward_us": 32.207999378442764 + }, + "65536": { + "forward_us": 90.55999666452408 + } + } + } + } + } +} From 024823d81ec15c5722c1bcdc3e94cf102220bfa0 Mon Sep 17 00:00:00 2001 From: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:33:00 +0000 Subject: [PATCH 43/44] docs(norm): gated RMSNorm evidence vs existing implementations on B200 Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- docs/operators/qwen3-next-rms-norm-gated.md | 28 ++ .../figure-gated_rmsnorm.png | Bin 0 -> 246981 bytes .../figure-zero_centred_rmsnorm.png | Bin 0 -> 241545 bytes .../report.json | 400 ++++++++++++++++++ 4 files changed, 428 insertions(+) create mode 100644 docs/usage/evidence/qwen3-next-rms-norm-gated-b200/figure-gated_rmsnorm.png create mode 100644 docs/usage/evidence/qwen3-next-rms-norm-gated-b200/figure-zero_centred_rmsnorm.png create mode 100644 docs/usage/evidence/qwen3-next-rms-norm-gated-b200/report.json diff --git a/docs/operators/qwen3-next-rms-norm-gated.md b/docs/operators/qwen3-next-rms-norm-gated.md index 49221c8a0..ba5d13501 100644 --- a/docs/operators/qwen3-next-rms-norm-gated.md +++ b/docs/operators/qwen3-next-rms-norm-gated.md @@ -119,6 +119,34 @@ python scripts/check_operator.py --op rms_norm_gated --candidate cuda \ --device cuda --dtype bf16 --check-grad ``` +## Evidence + +![gated RMSNorm vs existing implementations on B200](../usage/evidence/qwen3-next-rms-norm-gated-b200/figure-gated_rmsnorm.png) + +[`report.json`](../usage/evidence/qwen3-next-rms-norm-gated-b200/report.json) was written by +`scripts/qwen3_next_norm_evidence.py` from a clean tree at `822b085`, on an otherwise idle +B200 (torch 2.13.0+cu130, transformers 5.17.0, vLLM 0.30.0). Head dim 128, BF16; the FP64 +golden uses vLLM's convention. The same report re-measures the zero-centred op +([figure](../usage/evidence/qwen3-next-rms-norm-gated-b200/figure-zero_centred_rmsnorm.png)). + +| | rl-kernel CUDA | PyTorch reference | transformers (cast-first) | vLLM `RMSNormGated`* | +|---|---|---|---|---| +| rows differing alone vs in a batch, forward / `dx`,`dgate` (of 768) | **0 / 0** | 0 / 0 | 0 / 0 | 0 / n/a | +| forward elements correctly rounded vs the golden | **99.999%** | 99.999% | 65.6% | 99.999% | +| forward, 262144 rows | 235 µs | 774 µs | 697 µs | **81 µs** | +| backward, 262144 rows | 114 ms | **1.4 ms** | 1.5 ms | n/a | + +\* forward only, no backward. + +- **Every implementation is row-invariant** for this op. +- **transformers computes a different function:** it casts to BF16 before the weight + multiply, so 34% of its forward elements differ from vLLM's convention, and its gradients + are about twice as far from the golden. +- **The backward is about 80× slower than transformers at 262144 rows**, for the same reason + as the zero-centred op: `dweight` (128 columns here) is folded over the rows in ascending + order, one thread per column, so that it meets the gradient-invariance contract's + singleton-aggregate check bitwise. + ## Tests ```bash diff --git a/docs/usage/evidence/qwen3-next-rms-norm-gated-b200/figure-gated_rmsnorm.png b/docs/usage/evidence/qwen3-next-rms-norm-gated-b200/figure-gated_rmsnorm.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf59c4182d415031d99472f07667b3529decd67 GIT binary patch literal 246981 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

E!(){*k$)^R2Y3xQMu` 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 literal 0 HcmV?d00001 diff --git a/docs/usage/evidence/qwen3-next-rms-norm-gated-b200/figure-zero_centred_rmsnorm.png b/docs/usage/evidence/qwen3-next-rms-norm-gated-b200/figure-zero_centred_rmsnorm.png new file mode 100644 index 0000000000000000000000000000000000000000..5cd70b5cfcd6c378a0d6ab1acc7a31d541222430 GIT binary patch literal 241545 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≻1Vtf|)KbfRA?6K)8}6x5Fctnye>k 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 Date: Thu, 8 Oct 2026 18:33:10 +0000 Subject: [PATCH 44/44] docs(norm): correct the gated backward slowdown ratio Signed-off-by: Wenbo Ji <36562829+fusheng-ji@users.noreply.github.com> --- docs/operators/qwen3-next-rms-norm-gated.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operators/qwen3-next-rms-norm-gated.md b/docs/operators/qwen3-next-rms-norm-gated.md index ba5d13501..41d7616a1 100644 --- a/docs/operators/qwen3-next-rms-norm-gated.md +++ b/docs/operators/qwen3-next-rms-norm-gated.md @@ -142,7 +142,7 @@ golden uses vLLM's convention. The same report re-measures the zero-centred op - **transformers computes a different function:** it casts to BF16 before the weight multiply, so 34% of its forward elements differ from vLLM's convention, and its gradients are about twice as far from the golden. -- **The backward is about 80× slower than transformers at 262144 rows**, for the same reason +- **The backward is about 75× slower than transformers at 262144 rows**, for the same reason as the zero-centred op: `dweight` (128 columns here) is folded over the rows in ascending order, one thread per column, so that it meets the gradient-invariance contract's singleton-aggregate check bitwise.