Skip to content

perf(gated_delta_net): fold q/k L2-norm into the gated_delta_rule kernel - #5396

Open
yuchenwang3 wants to merge 3 commits into
NVIDIA:mainfrom
yuchenwang3:fix/gdn-qk-l2norm-in-kernel
Open

perf(gated_delta_net): fold q/k L2-norm into the gated_delta_rule kernel#5396
yuchenwang3 wants to merge 3 commits into
NVIDIA:mainfrom
yuchenwang3:fix/gdn-qk-l2norm-in-kernel

Conversation

@yuchenwang3

@yuchenwang3 yuchenwang3 commented Jun 17, 2026

Copy link
Copy Markdown

What

Move GatedDeltaNet q/k L2 normalization into the FLA gated_delta_rule kernel instead of materializing normalized q/k before the call.

The current GDN and GDN2 implementations share _prepare_input_for_gated_delta_rule, but GDN2's kernel path does not enable in-kernel normalization. The rebased change therefore makes deferral an explicit helper option:

  • GDN defers normalization and passes use_qk_l2norm_in_kernel=self.use_qk_l2norm.
  • GDN2 keeps its existing caller-side normalization and continues to pass False to its kernel.

This preserves GDN2 behavior added after the original PR rather than silently removing its normalization during the rebase.

Why

Caller-side normalization leaves an additional pre-split normalized query_key activation live for backward alongside the q/k tensors the gated-delta kernel already needs. Letting FLA own normalization removes that extra caller-side activation and keeps the normalization state with the kernel-owned q/k, which saves activation memory in long-context GDN-heavy hybrids.

The numerical contract is unchanged:

  • both caller-side FLA l2norm and the in-kernel path use eps=1e-6;
  • normalization is per head over the last dimension, so GQA head replication does not change the result;
  • use_qk_l2norm=False remains a no-op.

Validation

The original change ran in Qwen3.5-35B-A3B 128K SFT on 16×B200. The current-main replay passes Black, isort, Ruff, Pylint, py_compile, and git diff --check.

The final 398a42b4 head was also exercised with an 8-rank torchrun on 8×B200:

  • the GDN/GDN2 kernel flag-vs-explicit-normalization parity test passed on every rank;
  • the BF16 end-to-end GatedDeltaNet.forward comparison passed on every rank, covering outputs, input gradients, all parameter gradients, and both routing flags;
  • all 12 parameterized deterministic-mode GDN/GDN2 cases passed on every rank.

@copy-pr-bot

copy-pr-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@yuchenwang3
yuchenwang3 marked this pull request as ready for review July 10, 2026 01:39
@yuchenwang3
yuchenwang3 requested review from a team as code owners July 10, 2026 01:39
@svcnvidia-nemo-ci
svcnvidia-nemo-ci requested a review from a team July 10, 2026 01:40
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Jul 10, 2026
@yuchenwang3

Copy link
Copy Markdown
Author

@xuantengh pinging you as the most recent committer on gated_delta_net.py. This folds the explicit q/k l2norm into the FLA gated_delta_rule kernel via use_qk_l2norm_in_kernel=True, dropping a materialized intermediate at long context; it ran in real Qwen3.5-35B-A3B hybrid training at 128K. One-file change, open since Jun 17 — could you review or /ok to test?

@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Aug 6, 2026
@xuantengh

Copy link
Copy Markdown
Contributor

Thanks for the contribution! We had a refactor to the GDN module recently in #5843, would you please rebase that and apply this to the new file?

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Aug 6, 2026
@yuchenwang3

Copy link
Copy Markdown
Author

Done, @xuantengh — merged latest main and re-applied the change onto the refactored layout from #5843:

  • common.py: the explicit l2norm() in _GDNBase._prepare_input_for_gated_delta_rule is removed (replaced by a NOTE explaining the fold and the eps=1e-6 equivalence).
  • gdn.py: the gated_delta_rule call in GatedDeltaNet.forward now passes use_qk_l2norm_in_kernel=self.use_qk_l2norm.

Net diff vs main is the same two-line semantic change as before (+7/−4 with the comment block). Ready for another look.

@yuchenwang3

Copy link
Copy Markdown
Author

@xuantengh one more follow-up before I refresh this again: current main added GDN2 sharing _prepare_input_for_gated_delta_rule. I've kept GDN2's caller-side normalization and only defer GDN to the kernel. Does that split match what you'd expect? If so I'll update the branch and request CI.

@yuchenwang3
yuchenwang3 force-pushed the fix/gdn-qk-l2norm-in-kernel branch from b752055 to 39fd9a0 Compare August 25, 2026 07:43
@yuchenwang3

Copy link
Copy Markdown
Author

Rebased onto current main and adjusted the shared input helper added since the original PR: GDN now defers q/k normalization to its kernel, while GDN2 keeps the existing caller-side normalization. Local formatting, syntax, and routing checks pass. @xuantengh, could you re-review this head when you get a chance?

@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Aug 25, 2026
@xuantengh

Copy link
Copy Markdown
Contributor

Sorry for the delayed reply. Would you please add a test to tests/unit_tests/ssm/test_gated_delta_net.py to validate toggling use_qk_l2norm can produce on-par results?

@yuchenwang3
yuchenwang3 force-pushed the fix/gdn-qk-l2norm-in-kernel branch 2 times, most recently from 8206dc3 to 398a42b Compare August 25, 2026 10:37
@yuchenwang3

Copy link
Copy Markdown
Author

Done, @xuantengh. I added a small kernel-level parity check plus a BF16 end-to-end test that runs the real GatedDeltaNet.forward path. The latter compares the old caller-normalized route with kernel-owned normalization across outputs, input gradients, and every parameter gradient, and also asserts that both the helper and kernel receive the in-kernel flag and that raw q/k reach the kernel.

The new tests pass on all 8 ranks on 8×B200. While running the existing deterministic matrix I also caught and fixed a current FLA API mismatch in the newly activated fallback (l2norm has no dim argument); all 12 deterministic GDN/GDN2 cases now pass on every rank. The final head is 398a42b4 and DCO is green. Could you take another look and run /ok to test 398a42b4af753c76ffeeb14198d9b41a1bb2de1a when you get a chance?

@xuantengh

Copy link
Copy Markdown
Contributor

Hi @yuchenwang3 thanks for your contribution. Generally LGTM, but currently there is no way to specify use_qk_norm_in_kernel at the module spec level. So would you please also add this as an optional knob (default to False, aligned with upstream FLA interface signature) to TransformerConfig and ModuleSpec. And we can toggle this via config in test, rather than the install_route_probe hook.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Aug 27, 2026
Yuchen Wang added 3 commits August 27, 2026 16:50
Defer GDN q/k normalization to the gated delta rule kernel so the kernel owns normalization and caller autograd no longer retains an additional pre-split normalized query_key activation alongside the kernel q/k. Keep caller-side normalization as the default for GDN2, which shares the input-preparation helper but does not enable this path.

Signed-off-by: Yuchen Wang <yw.yy953e@alibaba-inc.com>
FLA l2norm normalizes the last dimension and does not accept a dim argument. Use its default eps=1e-6 API in the deterministic GDN and GDN2 fallbacks.

Add a BF16 end-to-end regression that compares the former caller-normalized route with kernel-owned normalization across outputs, input gradients, and parameter gradients, while asserting the helper and kernel routing flags.

Signed-off-by: Yuchen Wang <yw.yy953e@alibaba-inc.com>
Expose kernel-owned q/k L2 normalization as a TransformerConfig option that defaults to false and forward it through the GDN ModuleSpec. Replace the test-only route probe with config-selected BF16 end-to-end parity coverage.

Signed-off-by: Yuchen Wang <yw.yy953e@alibaba-inc.com>
@yuchenwang3
yuchenwang3 force-pushed the fix/gdn-qk-l2norm-in-kernel branch from 398a42b to edf2ff5 Compare August 27, 2026 23:55
@yuchenwang3
yuchenwang3 requested review from a team as code owners August 27, 2026 23:56
@yuchenwang3

Copy link
Copy Markdown
Author

Done, @xuantengh — I rebased onto current main and added TransformerConfig.use_qk_l2norm_in_kernel with a False default, using the exact upstream FLA argument name. The value is forwarded through the GDN ModuleSpec.params and remains available from the config for other construction paths.

The BF16 parity test now builds the caller-normalized and kernel-normalized modules by toggling that config value through the real ModuleSpec path; the route-probe hook is gone. It still compares outputs, input gradients, and every parameter gradient. The module-spec test file is 47/47 locally, and formatting/static checks pass.

The updated head is edf2ff50cbfdba177c5f27eb8d01a82d792a6bcd. Could you take another look and run /ok to test edf2ff50cbfdba177c5f27eb8d01a82d792a6bcd?

@yuchenwang3

Copy link
Copy Markdown
Author

The current head edf2ff50cbfdba177c5f27eb8d01a82d792a6bcd has now completed the reviewer-requested focused validation on 8×B200 with flash-linear-attention==0.5.1 (HAVE_FLA=True, HAVE_FLA_GDN2=True). Every rank finished with 15 passed, 0 failed, 0 skipped: the GDN/GDN2 normalization parity cases, the BF16 end-to-end config/ModuleSpec route, and all 12 deterministic GDN/GDN2 cases. The logs show the Blackwell FLA/TileLang kernel path executing; there were no tracebacks, NaNs, or OOMs, and the source tree remained clean.

Could you take another look and run /ok to test edf2ff50cbfdba177c5f27eb8d01a82d792a6bcd?

@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-customer Waiting on the original author to respond label Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants