Skip to content

[None][fix] Restore DSv4 NVFP4 routed swiglu_limit on TRTLLM-Gen - #18239

Open
Barry-Delaney wants to merge 4 commits into
NVIDIA:mainfrom
Barry-Delaney:fix/nvfp4-swiglu-limit-base
Open

[None][fix] Restore DSv4 NVFP4 routed swiglu_limit on TRTLLM-Gen#18239
Barry-Delaney wants to merge 4 commits into
NVIDIA:mainfrom
Barry-Delaney:fix/nvfp4-swiglu-limit-base

Conversation

@Barry-Delaney

@Barry-Delaney Barry-Delaney commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Description

Cherry-picks #14673 onto main after the original fix landed on feat/deepseek_v4 but was not carried over with the model split.

  • Restore the routed-expert swiglu_limit for TRTLLM-Gen NVFP4.
  • Move cubin-domain bias and clamp normalization into NVFP4TRTLLMGenFusedMoEBaseMethod so the plain DeepSeek-V4 SwiGLU path applies it.
  • Preserve the current FP8 scalar-only handling and account for the removal of WideEP.

Testing

  • Pre-merge CI

Dev Engineer Review

  • Restored routed-expert swiglu_limit support for TRTLLM-Gen NVFP4.
  • Moved NVFP4 cubin-domain bias, swiglu_beta, and swiglu_limit normalization into NVFP4TRTLLMGenFusedMoEBaseMethod.
  • Preserved FP8 scalar-only handling.
  • Removed the duplicate subclass override.
  • Accounted for WideEP removal.
  • No configuration or public API declaration changes were made.
  • Pre-merge CI failed and requires follow-up before merge.

QA Engineer Review

No test changes.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f96cc880-09d9-43a7-a80c-ad82bda6ef28

📥 Commits

Reviewing files that changed from the base of the PR and between 528a530 and 22188cc.

📒 Files selected for processing (2)
  • tensorrt_llm/_torch/models/modeling_deepseekv4.py
  • tensorrt_llm/_torch/modules/fused_moe/quantization.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tensorrt_llm/_torch/models/modeling_deepseekv4.py
  • tensorrt_llm/_torch/modules/fused_moe/quantization.py

Included review availability: Your plan provides up to 12 included reviews per hour; 10 remain after this review.


Walkthrough

The changes enable per-expert SwiGLU limit tensor construction for TRTLLM-Gen NVFP4 and move NVFP4 parameter normalization into shared quantization processing. TRTLLM-Gen FP8 block-scale remains scalar-only.

Changes

DeepSeek V4 MoE quantized processing

Layer / File(s) Summary
SwiGLU limit tensor support
tensorrt_llm/_torch/models/modeling_deepseekv4.py
TRTLLM-Gen NVFP4 now constructs the per-expert moe_swiglu_limit tensor. TRTLLM-Gen FP8 block-scale remains scalar-only.
Shared NVFP4 parameter normalization
tensorrt_llm/_torch/modules/fused_moe/quantization.py
Shared processing normalizes FC1/FC2 biases and SwiGLU beta/limit values by their alpha values. The duplicate subclass override is removed.

Estimated code review effort: 2 (Simple) | ~15 minutes

Merge Risk: ⚪ Minimal · up to 22188

This localized change restores NVFP4 DeepSeek-V4 routing behavior, and no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: wanli-jiang, yechank-nvidia, leslie-fang25

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required [None][fix] format and clearly identifies the restored DeepSeek-V4 NVFP4 routed swiglu_limit support.
Description check ✅ Passed The description explains the issue and solution and identifies pre-merge CI as testing. It does not include the full PR Checklist and uses a Testing heading instead of Test Coverage, but the core requ…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the issue and solution and identifies pre-merge CI as testing. It does not include the full PR Checklist and uses a Testing heading instead of Test Coverage, but the core required information is present.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@Barry-Delaney
Barry-Delaney force-pushed the fix/nvfp4-swiglu-limit-base branch from cc12adc to 80a5ec4 Compare August 26, 2026 07:38
@Barry-Delaney

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69369 [ run ] triggered by Bot. Commit: 80a5ec4 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69369 [ run ] completed with state FAILURE. Commit: 80a5ec4
/LLM/main/L0_MergeRequest_PR pipeline #56712 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Barry-Delaney

Copy link
Copy Markdown
Collaborator Author

/bot run

@Barry-Delaney
Barry-Delaney force-pushed the fix/nvfp4-swiglu-limit-base branch from 80a5ec4 to c4f265a Compare August 27, 2026 02:12
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69594 [ run ] triggered by Bot. Commit: c4f265a Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69594 [ run ] completed with state SUCCESS. Commit: c4f265a
/LLM/main/L0_MergeRequest_PR pipeline #56908 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@sunnyqgg sunnyqgg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@Barry-Delaney

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69650 [ run ] triggered by Bot. Commit: c4f265a Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69650 [ run ] completed with state FAILURE. Commit: c4f265a
/LLM/main/L0_MergeRequest_PR pipeline #56955 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Barry-Delaney

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69667 [ run ] triggered by Bot. Commit: c4f265a Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69667 [ run ] completed with state SUCCESS. Commit: c4f265a
/LLM/main/L0_MergeRequest_PR pipeline #56971 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@Barry-Delaney
Barry-Delaney force-pushed the fix/nvfp4-swiglu-limit-base branch from c4f265a to 22188cc Compare August 27, 2026 15:52
@Barry-Delaney

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69757 [ run ] triggered by Bot. Commit: 22188cc Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69757 [ run ] completed with state SUCCESS. Commit: 22188cc
/LLM/main/L0_MergeRequest_PR pipeline #57054 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

…DIA#14673)

Signed-off-by: Barry Kang <43644113+Barry-Delaney@users.noreply.github.com>
Signed-off-by: Barry Kang <43644113+Barry-Delaney@users.noreply.github.com>
@Barry-Delaney
Barry-Delaney force-pushed the fix/nvfp4-swiglu-limit-base branch from 22188cc to 8c8f0c1 Compare August 28, 2026 03:56
@Barry-Delaney

Copy link
Copy Markdown
Collaborator Author

/bot run

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69871 [ run ] triggered by Bot. Commit: 8c8f0c1 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #69871 [ run ] completed with state SUCCESS. Commit: 8c8f0c1
/LLM/main/L0_MergeRequest_PR pipeline #57160 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@brnguyen2 brnguyen2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — the comments below are optional touch-ups, not blockers.

The mechanism checks out: TRTLLMGenFusedMoE._get_quant_method returns NVFP4TRTLLMGenFusedMoEBaseMethod for plain SwiGLU NVFP4 (no swiglu_alpha, not SiTu, activation not Relu2/Silu) — that's the DeepSeek-V4 routed path — and the pre-dequant-domain division lived only in the padded NVFP4TRTLLMGenFusedMoEMethod subclass, so the limit reached the cubin in the wrong domain. Moving it to the base is the right place.

Two asks:

  • The guard being removed was justified by measured near-zero accuracy. Please attach before/after accuracy numbers for DSv4 NVFP4 on TRTLLM-Gen (the config that motivated the original guard), not just "pre-merge CI" — CI has no test that exercises this combination. Also, the release notes attached to the PR say pre-merge CI failed; worth confirming that's resolved or unrelated before merge.
  • This is a fix for a real accuracy regression that a model split silently dropped; it should carry an NVBug in the title tag rather than [None], so the same loss is traceable next time.

Comment thread tensorrt_llm/_torch/moe/fused_moe/quantization.py
Comment thread tensorrt_llm/_torch/models/modeling_deepseekv4.py
@xxi-nv

xxi-nv commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

#18409 depends on this PR.

xxi-nv added a commit to xxi-nv/TensorRT-LLM that referenced this pull request Aug 31, 2026
…checking exact classes

The MoE factory decided what a backend could accept by naming it. Fifteen
`moe_cls is <Class>` / `moe_cls in [...]` / `isinstance` gates across
`create_moe`, `moe_scheduler` and two model files spelled capability as class
identity, so a subclass of a supported backend was silently ineligible, and the
same eligibility rule lived in the factory, the constructor and a late runtime
error, free to disagree. It already did: MEGAMOE_DEEPGEMM reached SiTU only
because `activation="situ"` and `activation_type` were two parameters that
contradicted each other -- the factory built its problem from the unset
`activation_type` default (Swiglu), passed the gate, then handed the
constructor `"situ"` -- while the identical MEGAMOE_CUTEDSL request raised.

Each backend now declares what it accepts and the factory reads the declaration:

- `MoEActivationSupport` (new `activation.py`) states which `ActivationType`s a
  backend implements and, per constant, the shape its kernel boundary requires
  (`PER_EXPERT_TENSOR` / `UNIFORM_SCALAR` / `UNSUPPORTED`). The bias and
  swiglu-parameter allow-lists are gone.
- Selection reads that declaration in one place. `MoEProblem` carries
  `activation_constants` -- which registers the caller actually fills, a
  question the kind alone cannot answer, since clamped and unclamped SwiGLU
  share one `ActivationType` but not one set of eligible backends -- and
  `_reject_unsupported_activation` declines a candidate whose declaration cannot
  carry them. Previously such a mismatch raised from the adapter at
  construction, past the point where another candidate could still be chosen.
- `MoEStaticCapability.supports_eplb` replaces
  `supported_load_balancer_backends`, the same shape of list one layer down.
  `CuteDslB12xFusedMoE` now rejects `eplb_enabled` in `can_implement` instead of
  failing at construction.
- `MoEImplBase.try_fused_route_quant` replaces the
  `isinstance(moe.backend, TRTLLMGenFusedMoE)` branch in `moe_scheduler`, so a
  backend opts into the fused route+quant path by overriding it. The hook drops
  the model name the method carried while it lived on one class, because the
  contract is generic -- return what `routing_method.apply` and `quantize_input`
  would have produced, or `None` -- and only the implementation is specialized:
  the op it calls hardcodes 896 experts, top-16, hidden 3584 and 64 tokens, so
  the `TRTLLMGenFusedMoE` override still says Kimi K3. The second `isinstance` in
  that same branch went the other way, to `input_requirement.routing_scales_dtype`.
  Both replaced a class name, but one asked which dtype the backend reads, which
  is data a declaration can carry, and this one asks whether a faster path
  exists, which is behavior only the backend can answer.
- Kimi K3's explicit-backend requests pass `allow_backend_degradation=False`
  instead of asserting on the resolved class, so a silent substitution fails
  loudly rather than running a different kernel.

The `moe_cls in (...)` branches that remain in `create_moe` select which kwargs
each constructor takes. They are signature dispatch, not eligibility, and are
the factory's job.

A declaration is only worth reading if no backend inherits one it never made.
`CuteDslFusedMoE`, `DeepGemmFusedMoE` and `MarlinFusedMoE` derived from
`CutlassFusedMoE` for constructor reuse while overriding both entry points, so
each silently inherited its parent's capability object -- including
`supports_expert_bias=True`, which none of them implements and which this change
would otherwise have turned from a dormant inconsistency into a selection input.
They now derive from `MoEImplBase` and share the constructor through
`apply_moe_impl_construction_state`, and every impl declares `capabilities`,
`input_requirement` and `activation_support` itself, so no declared value
travels by inheritance. `CuteDslB12xFusedMoE` keeps its `CutlassFusedMoE`
parent, the one case where that is a real dependency rather than reuse:
`_route_to_cutlass` sends every NVFP4 prefill chunk through
`CutlassFusedMoE.quantize_input` / `run_moe`, which read the whole Cutlass
execution state.

This also closes TRTLLM-15649, because the exact-class gates were only needed
while one activation was described by several parameters at once:

- One carrier. `MoEActivation` (`SwigluActivation`, `SiTuActivation`, ...) holds
  type, alpha, beta and clamp together, and is what the layer hands to the
  factory and to selection. `trtllm_gen_activation_type/alpha/beta` and
  MegaMoE's `activation` string are deleted; `ActType_TrtllmGen` becomes a C++
  ABI encoding rather than a second user-facing enum. `build_moe_problem` and
  `resolve_moe_impl` take only the carrier: their `activation_type` parameter
  had no caller left, since the kind is derivable and the constants are not.
- One clamp, at every layer. `swiglu_limit` (per-expert tensor) and
  `swiglu_limit_scalar` merge into a single `clamp`, and the materialized view
  keeps one field too: which ABI a backend gets is already pinned by its
  declaration, so `materialize_activation_params` renders the shape and no
  backend chooses between two slots.
- Kind-neutral names above the ABI. The installed slots are `act_alpha` /
  `act_beta` / `act_clamp`, because SiTU fills the same registers the C++
  boundary spells `swiglu_*` and every non-SwiGLU backend was renaming it back
  on arrival. The op-call sites keep each kernel's own schema names. The
  MegaMoE CuteDSL op, whose signature we own, is renamed to match; DeepGEMM's
  `situ_beta` stays because `_import_deep_gemm` gates on the installed
  signature accepting exactly that.
- SiTU travels as `ActivationType.SiTu` with alpha/beta, collapsing MegaMoE's two
  translation tables into the declaration. Gates that meant "FC1 is gated" and
  were written `== ActivationType.Swiglu` now say `is_gated_activation`, which
  stops them from rejecting SiTU.

The slots are installed once per writer of an input the materialization reads,
under the rule that whoever writes that input last re-installs.
`apply_moe_impl_construction_state` establishes the invariant for an impl built
directly, which unit tests and microbenchmarks do and which never sees the
wrapper. `ConfigurableMoE.create_weights` re-installs after applying the
layerwise quant config, since `TRTLLMGenFusedMoE` narrows its clamp to a uniform
scalar on the FP8 block-scale path and would otherwise keep a tensor clamp that
kernel silently ignores; being the last write before `create_weights`, it is
also the value every reader in the wrapper path sees. The EPLB slot sync and
DWDP's `fixup_moe_backends` re-install because they rewrite
`expert_size_per_partition`, which sizes a per-expert constant.

`MoEActivationSupport.limit_when_absent` exists because CuteDSL's epilogue always
applies the clamp functor: at that boundary "no clamp" is `+inf`, not `None`. A
constructor fixup cannot express it, because `ConfigurableMoE` re-installs the
activation slots after syncing `expert_size_per_partition` and would overwrite it.

`tests/unittest/_torch/moe/test_fused_moe.py` drops the 15 tests that were
already permanently skipped as covered by `test_moe_backend.py` and
`test_moe_module.py`, together with the six helpers only those tests called
-- `run_fused_moe_nvfp4` alone was 240 lines -- and the module-level cloudpickle
/ `MPI.pickle` bootstrap, which existed so the deleted `MPIPoolExecutor` cases
could pickle test functions across ranks. The file goes 2639 -> 466 lines. The
three live tests and the two reference classes they use are byte-identical, and
no test list or CI stage referenced any deleted name.

Tested on GB300 (aga) and B300 at an earlier revision of this change:
`test_moe_backend` 363 passed, `test_moe_module` single_gpu 267 / multi_gpu 438
/ multi_gpu_eplb 18, `test_kimi_k3_situ_moe` 64 passed + 1 xfailed. multi_gpu
gives byte-identical counts on both clusters. Accuracy on B300:
`TestNemotronV3Nano::test_fp8`, `TestGPTOSS::test_w4_1gpu`,
`TestDeepSeekV4Flash::test_nvfp4_4gpus_static_eplb` and
`TestMiniMaxM3::test_nvfp4[use_msa=False]` all pass.

Re-verified on GB300 (aga) after rebasing onto origin/main, which is the run
that covers the selection-time activation gate, the two re-install points, the
single-field clamp and the reparent: `test_moe_backend` 371 + 8 passed,
`test_moe_module` single_gpu 278 / multi_gpu 470 / multi_gpu_eplb 18 / DEEPGEMM
14 / CUTEDSL-NVFP4 18 / DENSEGEMM 5, `test_kimi_k3_situ_moe` 69 passed + 1
xfailed, `test_configurable_moe` 2 passed, and the trimmed `test_fused_moe`
9 passed + 64 skipped. Every count is identical to the pre-rebase run of the
same matrix, which is the signal that neither the reparent nor the upstream span
the rebase pulled in -- the relocation of `_torch/modules/fused_moe` to
`_torch/moe/fused_moe` among it -- changed anything observable. The 21
architectural gates in that job all pass, including
`fc31_normalization_in_base_class` and `dsv4_swiglu_limit_war_removed`, which
are what the two carried commits from NVIDIA#18239 exist to satisfy.

`test_configurable_moe` goes 2 failed -> 2 passed relative to the pre-reparent
run: its wrapper fixture builds a `ConfigurableMoE` through `__new__` and never
set `ep_size`, which `_reject_non_divisible_ep_backend` reads. The accuracy
cases run on B300 rather than aga and are covered by the run above.

Signed-off-by: xxi <xxi@nvidia.com>
xxi-nv added a commit to xxi-nv/TensorRT-LLM that referenced this pull request Aug 31, 2026
…checking exact classes

The MoE factory decided what a backend could accept by naming it. Fifteen
`moe_cls is <Class>` / `moe_cls in [...]` / `isinstance` gates across
`create_moe`, `moe_scheduler` and two model files spelled capability as class
identity, so a subclass of a supported backend was silently ineligible, and the
same eligibility rule lived in the factory, the constructor and a late runtime
error, free to disagree. It already did: MEGAMOE_DEEPGEMM reached SiTU only
because `activation="situ"` and `activation_type` were two parameters that
contradicted each other -- the factory built its problem from the unset
`activation_type` default (Swiglu), passed the gate, then handed the
constructor `"situ"` -- while the identical MEGAMOE_CUTEDSL request raised.

Each backend now declares what it accepts and the factory reads the declaration:

- `MoEActivationSupport` (new `activation.py`) states which `ActivationType`s a
  backend implements and, per constant, the shape its kernel boundary requires
  (`PER_EXPERT_TENSOR` / `UNIFORM_SCALAR` / `UNSUPPORTED`). The bias and
  swiglu-parameter allow-lists are gone.
- Selection reads that declaration in one place. `MoEProblem` carries
  `activation_constants` -- which registers the caller actually fills, a
  question the kind alone cannot answer, since clamped and unclamped SwiGLU
  share one `ActivationType` but not one set of eligible backends -- and
  `_reject_unsupported_activation` declines a candidate whose declaration cannot
  carry them. Previously such a mismatch raised from the adapter at
  construction, past the point where another candidate could still be chosen.
- `MoEStaticCapability.supports_eplb` replaces
  `supported_load_balancer_backends`, the same shape of list one layer down.
  `CuteDslB12xFusedMoE` now rejects `eplb_enabled` in `can_implement` instead of
  failing at construction.
- `MoEImplBase.try_fused_route_quant` replaces the
  `isinstance(moe.backend, TRTLLMGenFusedMoE)` branch in `moe_scheduler`, so a
  backend opts into the fused route+quant path by overriding it. The hook drops
  the model name the method carried while it lived on one class, because the
  contract is generic -- return what `routing_method.apply` and `quantize_input`
  would have produced, or `None` -- and only the implementation is specialized:
  the op it calls hardcodes 896 experts, top-16, hidden 3584 and 64 tokens, so
  the `TRTLLMGenFusedMoE` override still says Kimi K3. The second `isinstance` in
  that same branch went the other way, to `input_requirement.routing_scales_dtype`.
  Both replaced a class name, but one asked which dtype the backend reads, which
  is data a declaration can carry, and this one asks whether a faster path
  exists, which is behavior only the backend can answer.
- Kimi K3's explicit-backend requests pass `allow_backend_degradation=False`
  instead of asserting on the resolved class, so a silent substitution fails
  loudly rather than running a different kernel.

The `moe_cls in (...)` branches that remain in `create_moe` select which kwargs
each constructor takes. They are signature dispatch, not eligibility, and are
the factory's job.

A declaration is only worth reading if no backend inherits one it never made.
`CuteDslFusedMoE`, `DeepGemmFusedMoE` and `MarlinFusedMoE` derived from
`CutlassFusedMoE` for constructor reuse while overriding both entry points, so
each silently inherited its parent's capability object -- including
`supports_expert_bias=True`, which none of them implements and which this change
would otherwise have turned from a dormant inconsistency into a selection input.
They now derive from `MoEImplBase` and share the constructor through
`apply_moe_impl_construction_state`, and every impl declares `capabilities`,
`input_requirement` and `activation_support` itself, so no declared value
travels by inheritance. `CuteDslB12xFusedMoE` keeps its `CutlassFusedMoE`
parent, the one case where that is a real dependency rather than reuse:
`_route_to_cutlass` sends every NVFP4 prefill chunk through
`CutlassFusedMoE.quantize_input` / `run_moe`, which read the whole Cutlass
execution state.

This also closes TRTLLM-15649, because the exact-class gates were only needed
while one activation was described by several parameters at once:

- One carrier. `MoEActivation` (`SwigluActivation`, `SiTuActivation`, ...) holds
  type, alpha, beta and clamp together, and is what the layer hands to the
  factory and to selection. `trtllm_gen_activation_type/alpha/beta` and
  MegaMoE's `activation` string are deleted; `ActType_TrtllmGen` becomes a C++
  ABI encoding rather than a second user-facing enum. `build_moe_problem` and
  `resolve_moe_impl` take only the carrier: their `activation_type` parameter
  had no caller left, since the kind is derivable and the constants are not.
- One clamp, at every layer. `swiglu_limit` (per-expert tensor) and
  `swiglu_limit_scalar` merge into a single `clamp`, and the materialized view
  keeps one field too: which ABI a backend gets is already pinned by its
  declaration, so `materialize_activation_params` renders the shape and no
  backend chooses between two slots.
- Kind-neutral names above the ABI. The installed slots are `act_alpha` /
  `act_beta` / `act_clamp`, because SiTU fills the same registers the C++
  boundary spells `swiglu_*` and every non-SwiGLU backend was renaming it back
  on arrival. The op-call sites keep each kernel's own schema names. The
  MegaMoE CuteDSL op, whose signature we own, is renamed to match; DeepGEMM's
  `situ_beta` stays because `_import_deep_gemm` gates on the installed
  signature accepting exactly that.
- SiTU travels as `ActivationType.SiTu` with alpha/beta, collapsing MegaMoE's two
  translation tables into the declaration. Gates that meant "FC1 is gated" and
  were written `== ActivationType.Swiglu` now say `is_gated_activation`, which
  stops them from rejecting SiTU.

The slots are installed once per writer of an input the materialization reads,
under the rule that whoever writes that input last re-installs.
`apply_moe_impl_construction_state` establishes the invariant for an impl built
directly, which unit tests and microbenchmarks do and which never sees the
wrapper. `ConfigurableMoE.create_weights` re-installs after applying the
layerwise quant config, since `TRTLLMGenFusedMoE` narrows its clamp to a uniform
scalar on the FP8 block-scale path and would otherwise keep a tensor clamp that
kernel silently ignores; being the last write before `create_weights`, it is
also the value every reader in the wrapper path sees. The EPLB slot sync and
DWDP's `fixup_moe_backends` re-install because they rewrite
`expert_size_per_partition`, which sizes a per-expert constant.

`MoEActivationSupport.limit_when_absent` exists because CuteDSL's epilogue always
applies the clamp functor: at that boundary "no clamp" is `+inf`, not `None`. A
constructor fixup cannot express it, because `ConfigurableMoE` re-installs the
activation slots after syncing `expert_size_per_partition` and would overwrite it.

`tests/unittest/_torch/moe/test_fused_moe.py` drops the 15 tests that were
already permanently skipped as covered by `test_moe_backend.py` and
`test_moe_module.py`, together with the six helpers only those tests called
-- `run_fused_moe_nvfp4` alone was 240 lines -- and the module-level cloudpickle
/ `MPI.pickle` bootstrap, which existed so the deleted `MPIPoolExecutor` cases
could pickle test functions across ranks. The file goes 2639 -> 466 lines. The
three live tests and the two reference classes they use are byte-identical, and
no test list or CI stage referenced any deleted name.

Tested on GB300 (aga) and B300 at an earlier revision of this change:
`test_moe_backend` 363 passed, `test_moe_module` single_gpu 267 / multi_gpu 438
/ multi_gpu_eplb 18, `test_kimi_k3_situ_moe` 64 passed + 1 xfailed. multi_gpu
gives byte-identical counts on both clusters. Accuracy on B300:
`TestNemotronV3Nano::test_fp8`, `TestGPTOSS::test_w4_1gpu`,
`TestDeepSeekV4Flash::test_nvfp4_4gpus_static_eplb` and
`TestMiniMaxM3::test_nvfp4[use_msa=False]` all pass.

Re-verified on GB300 (aga) after rebasing onto origin/main, which is the run
that covers the selection-time activation gate, the two re-install points, the
single-field clamp and the reparent: `test_moe_backend` 371 + 8 passed,
`test_moe_module` single_gpu 278 / multi_gpu 470 / multi_gpu_eplb 18 / DEEPGEMM
14 / CUTEDSL-NVFP4 18 / DENSEGEMM 5, `test_kimi_k3_situ_moe` 69 passed + 1
xfailed, `test_configurable_moe` 2 passed, and the trimmed `test_fused_moe`
9 passed + 64 skipped. Every count is identical to the pre-rebase run of the
same matrix, which is the signal that neither the reparent nor the upstream span
the rebase pulled in -- the relocation of `_torch/modules/fused_moe` to
`_torch/moe/fused_moe` among it -- changed anything observable. The 21
architectural gates in that job all pass, including
`fc31_normalization_in_base_class` and `dsv4_swiglu_limit_war_removed`, which
are what the two carried commits from NVIDIA#18239 exist to satisfy.

`test_configurable_moe` goes 2 failed -> 2 passed relative to the pre-reparent
run: its wrapper fixture builds a `ConfigurableMoE` through `__new__` and never
set `ep_size`, which `_reject_non_divisible_ep_backend` reads. The accuracy
cases run on B300 rather than aga and are covered by the run above.

Signed-off-by: xxi <xxi@nvidia.com>
xxi-nv added 2 commits August 31, 2026 08:50
Resolves the conflict introduced by NVIDIA#17952, which relocated
tensorrt_llm/_torch/modules/fused_moe/ to tensorrt_llm/_torch/moe/fused_moe/.
That turns this branch's quantization.py hunks into modify-vs-delete. Git
resolves it locally via directory-rename detection, but the merge-base..main
diff spans 10217 files, past the default diff.renameLimit, so GitHub skips
rename detection and reports the PR as unmergeable.

Merged rather than rebased so the branch needs no force-push.

Signed-off-by: xxi <xxi@nvidia.com>
Addresses review feedback on NVIDIA#18239.

process_weights_after_loading in NVFP4TRTLLMGenFusedMoEBaseMethod divided
w3_w1_bias / w2_bias by fc31_alpha / fc2_alpha whenever those attributes were
not None. Hoisting the block up from NVFP4TRTLLMGenFusedMoEMethod widened it to
W4A8NVFP4FP8TRTLLMGenFusedMoEMethod, which auto-creates all-zero bias tensors
that its kernel (fp8_fp4_block_scale_moe_runner) never receives. Dividing those
by an alpha of zero produces NaN. The same method's _shuffle_all_experts already
gates the bias on module.bias, so the two halves of one method disagreed about
what "has a bias" means.

Gate the division on module.bias instead. On every NVFP4 path the two
conditions are equivalent, because create_weights registers the bias parameters
only under `if module.bias` and sets them to None otherwise; behavior therefore
changes only for the auto-created zero tensors, which are now left alone.

Also add CPU-only regression tests pinning what DeepseekV4MoE hands to
create_moe: NVFP4 keeps the per-expert swiglu_limit tensor (sized by local
slots), while the TRTLLM-Gen FP8 path receives the scalar only. No test covered
this, which is how the model split silently dropped the tensor.

Signed-off-by: xxi <xxi@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@xxi-nv

xxi-nv commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Addressed both review comments (thanks @brnguyen2) directly on this branch, and
resolved the merge conflict while I was in there. Line references below are
post-merge, i.e. against the relocated tensorrt_llm/_torch/moe/fused_moe/ package.

0. The conflict was a path conflict, not a content one. #17952 moved
tensorrt_llm/_torch/modules/fused_moe/ to tensorrt_llm/_torch/moe/fused_moe/,
which turns both of this branch's quantization.py hunks into modify-vs-delete.
Git still resolves that locally through directory-rename detection, but the
merge-base..main diff spans 10217 files -- past the default diff.renameLimit,
git asks for at least 6480 -- so rename detection is skipped and the PR shows as
dirty. I merged main into the branch rather than rebasing, so no force-push to
your fork was needed, and re-applied both hunks at the new path; nothing else
moved. Since main is squash-merged, the merge commit does not survive to main.

I also checked the one thing a merge like this can silently get wrong: whether
kernel_requires_bias_for_swiglu_limit was already there at your branch point or
added to main afterwards. It is present at the merge base (96c8d567bd) and
your f46c93cc27 is what removes it, so the merge is not reverting a newer
upstream decision.

1. Bias gate (quantization.py) -- the div_ block now keys off module.bias
instead of attribute existence:

  • On every NVFP4 path the two conditions are equivalent: create_weights
    registers w3_w1_bias / w2_bias only under if module.bias and assigns
    None otherwise, and the NVFP4 shape helpers are gated the same way. So this
    is behavior-preserving for the path this PR restores.
  • The only place they diverge is W4A8NVFP4FP8TRTLLMGenFusedMoEMethod, which
    fused_moe_trtllm_gen.py:670-682 gives auto-created all-zero bias tensors
    when not self.bias. Those are exactly the tensors that must not be divided.
  • It also removes an inconsistency inside the base method itself:
    _shuffle_all_experts (quantization.py:5297, gate at :5313) already keys off
    module.bias, so before this change the auto-created zeros were divided but
    never shuffled.

Two notes on the risk assessment, for the record: the specific trigger in the
comment -- padded/unused slots under EPLB -- does not reach a zero alpha today
(quantization.py:2779 skips slots with no loaded scale, the alpha buffers are
initialized with torch.ones at :2292/:2297, and interface.py:552-555
asserts every slot maps to a real expert, so EPLB has no all-zero slots). And
even if a NaN were produced, fp8_fp4_block_scale_moe_runner
(fused_moe_trtllm_gen.py:1051-1078) never receives the bias, so it could not
reach the output. The gate is still worth tightening -- it is the correct
invariant and it makes the method self-consistent -- but it is not a live
correctness bug.

2. Regression tests (test_modeling_deepseekv4.py) -- three CPU-only tests
that pin what DeepseekV4MoE hands to create_moe:

  • NVFP4 keeps the per-expert swiglu_limit tensor (this is the assertion the
    model split would have failed);
  • the tensor is sized by local slots, so EPLB padding grows it;
  • the TRTLLM-Gen FP8 block-scale path gets swiglu_limit_scalar only.

Each test also asserts the backend the layer actually resolved to, so a
swiglu_limit is None assertion cannot pass merely because some other backend
ignores the argument.

I reverse-verified each of the three so none of them can be vacuously green.
Reverting the fix under test (dropping TRTLLMGenFusedMoE from the
supports_swiglu_limit tuple) fails tests 1 and 2 and leaves test 3 green;
forcing requires_scalar_only_swiglu_limit = False fails test 3 and leaves 1 and
2 green. On the actual branch all three pass, before and after the merge.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants