Skip to content

Close remaining DASC review gaps - #2408

Closed
kaix-nv wants to merge 1 commit into
feature/dasc-state-sparsityfrom
feature/dasc-state-sparsity-review-final2
Closed

kaix-nv wants to merge 1 commit into
feature/dasc-state-sparsityfrom
feature/dasc-state-sparsity-review-final2

Conversation

@kaix-nv

@kaix-nv kaix-nv commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix and documentation.

Closes the remaining review gaps on #2375. Distributed Megatron GDN policy export now fails closed for tensor or pipeline parallel configurations whose head and layer indices would be rank-local. The patch also validates standalone policy decay inputs, keeps policy attachment wrapper-safe, avoids refreshing unrelated mode metadata during recalibration, clarifies subclass remediation, and documents gate direction and active quality defaults.

Two review suggestions are intentionally not implemented: a head-count ratio is not a universal byte-savings bound for heterogeneous state geometry, and nonlinear measured quality is not guaranteed to be monotonic with DASC-NR window size.

Usage

No API changes. Megatron GDN calibration remains supported for TP=1 and PP=1 and fails closed otherwise.

Testing

  • python -m pytest -q tests/unit/torch/sparsity/state_sparsity/test_dasc.py: 60 passed, 1 optional Megatron skip
  • python -m pytest -q tests/unit/torch/sparsity/state_sparsity tests/unit/torch/sparsity/weight_sparsity tests/unit/torch/sparsity/attention_sparsity: 338 passed, 1 optional Megatron skip
  • pre-commit run on all five changed files: passed

Before your PR is Ready for review

  • Backward compatible: yes
  • Copied code or new dependency: no
  • New necessary tests: yes
  • Changelog update: N/A; these are fixes to the same unreleased feature
  • Claude approval: pending

Summary by CodeRabbit

  • New Features

    • Added validation for decay-analysis settings, including finite epsilon values and static gate inputs within supported ranges.
    • Added safeguards to prevent policy generation for unsupported distributed Megatron configurations.
  • Bug Fixes

    • Improved restoration and recalibration behavior when applying DASC policies.
    • Clarified error messages for unsupported GDN implementations.
  • Documentation

    • Documented sparsity, perplexity-retention, agreement, and checkpoint-savings thresholds.
    • Clarified conservative gate selection and supported export configurations.

Signed-off-by: Kai Xu <kaix@nvidia.com>
@kaix-nv
kaix-nv requested review from a team as code owners September 11, 2026 17:59
@kaix-nv
kaix-nv requested review from realAsma and removed request for a team September 11, 2026 17:59
@kaix-nv

kaix-nv commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

/claude review

@kaix-nv

kaix-nv commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1cd47730-2ba1-4cff-a78e-01be1e82ceed

📥 Commits

Reviewing files that changed from the base of the PR and between 0859c13 and a8c8a7d.

📒 Files selected for processing (5)
  • docs/source/guides/6_sparsity.rst
  • modelopt/torch/sparsity/state_sparsity/config.py
  • modelopt/torch/sparsity/state_sparsity/conversion.py
  • modelopt/torch/sparsity/state_sparsity/policy.py
  • tests/unit/torch/sparsity/state_sparsity/test_dasc.py

📝 Walkthrough

Walkthrough

The DASC updates validate policy fields, restrict Megatron GDN analysis to single-process model parallelism, preserve unrelated mode state during recalibration, and consistently unwrap models during policy attachment and restoration. Documentation and unit tests cover the new behavior.

Changes

DASC policy updates

Layer / File(s) Summary
DASC policy input validation
modelopt/torch/sparsity/state_sparsity/config.py, docs/source/guides/6_sparsity.rst, tests/unit/torch/sparsity/state_sparsity/test_dasc.py
DASCPolicy now rejects non-finite or out-of-range epsilon values and non-finite static_gate_input values. The documentation and tests describe the related calibration constraints.
Megatron GDN validation
modelopt/torch/sparsity/state_sparsity/policy.py, docs/source/guides/6_sparsity.rst, tests/unit/torch/sparsity/state_sparsity/test_dasc.py
GDN discovery now rejects unsupported subclasses with distinct diagnostics and blocks Megatron configurations with tensor or pipeline parallel sizes other than 1, 1.
State attachment and recalibration
modelopt/torch/sparsity/state_sparsity/conversion.py, tests/unit/torch/sparsity/state_sparsity/test_dasc.py
Policy attachment and restoration consistently operate on unwrapped models. Recalibration replaces DASC state without refreshing unrelated trailing modes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dasc-state-sparsity-review-final2

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

@kaix-nv
kaix-nv removed the request for review from realAsma September 11, 2026 18:00
@kaix-nv kaix-nv closed this Sep 11, 2026
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Pull request is closed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-11 18:01 UTC

]
_reject_incomplete_gdn_modules(identity_modules)
_reject_unconverted_gdn_subclasses(named_modules, supported_classes)
_reject_distributed_megatron_gdn(identity_modules, supported_classes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL ModeState] Putting the distributed guard in _get_gdn_modules makes it fire on the restore and save paths, not just policy derivation — which turns an existing recoverable-staleness case into an unloadable checkpoint.

_get_gdn_modules has four callers: analyze_gdn_decay (L297), build_dasc_policy (L461), validate_dasc_model_structure (L521) and validate_dasc_decay_parameters (L543). The last two are what restore_dasc_model and update_dasc_metadata call, and both of those only tolerate _DASCRecoverableStalenessError. The new check raises a plain ApplyModeError, so it propagates.

Failure scenario: calibrate a single-process Megatron GDN model, mto.save(). Later mto.restore() the same checkpoint into a TP=2 (or PP=2) rematerialization of the model:

  • restore_dasc_modelvalidate_dasc_model_structure_get_gdn_modulesApplyModeError("DASC policy export supports Megatron GDN only with tensor_model_parallel_size=1 …"), uncaught → restore aborts.
  • Before this PR, the sharded A_log.numel() / missing PP layers produced _DASCModelStructureMismatchError, a subclass of _DASCRecoverableStalenessError, so restore warned "the restored DASC policy is stale; re-run calibrate()" and proceeded. DASC is inert at runtime (convert_dasc_model: "attach the selected DASC policy without changing execution"), so proceeding was safe and intentional.
  • The same applies to saving: update_dasc_metadata catches recoverable staleness from validate_dasc_model_structure but not this new error, so mto.save() on a distributed model with DASC state now hard-fails.

The error text ("policy export supports …") is also misleading when it surfaces during restore.

Fix: call the guard only where a policy is actually derived, and leave the read-only validators alone:

def _get_gdn_modules(model: nn.Module, *, reject_distributed_megatron: bool = False) -> dict[str, nn.Module]:
    ...
    _reject_incomplete_gdn_modules(identity_modules)
    _reject_unconverted_gdn_subclasses(named_modules, supported_classes)
    if reject_distributed_megatron:
        _reject_distributed_megatron_gdn(identity_modules, supported_classes)

with reject_distributed_megatron=True passed from analyze_gdn_decay and build_dasc_policy only. If you do want restore to notice the geometry, raise _DASCModelStructureMismatchError there instead so it stays on the documented recoverable-staleness path. Worth a test that restores a TP=1-calibrated policy onto a TP=2 model and asserts a warning rather than a raise.

Comment on lines +187 to +196
megatron_class = next(
(
candidate
for candidate in supported_classes
if candidate.__module__ == module_name and candidate.__name__ == class_name
),
None,
)
if megatron_class is None:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] This lookup identifies the Megatron class by string-matching candidate.__module__ / __name__, and silently disables the whole guard when it doesn't match (if megatron_class is None: return). That is a fail-open path in a check whose entire purpose is to fail closed.

_supported_gdn_classes() resolves the class with getattr(importlib.import_module("megatron.core.ssm.gated_delta_net"), "GatedDeltaNet"). __module__ is the class's defining module, not the import path — so if Megatron ever defines GatedDeltaNet in a private submodule and re-exports it from megatron.core.ssm.gated_delta_net (a routine refactor upstream, and Megatron pins here are loose), megatron_class becomes None, _reject_distributed_megatron_gdn returns without checking anything, and a TP=2 / PP=2 policy exports with rank-local head and layer indices — exactly the corruption this PR adds. Nothing fails loudly, and no test catches it: test_declared_gdn_paths_resolve_when_framework_is_installed only asserts the path resolves to an nn.Module subclass, and the two new Megatron tests use a fake whose __module__ is hand-set to match.

Two ways to remove the string coupling:

  1. Have the resolver return the declared path alongside the class, so the Megatron class is identified by which entry it was resolved from rather than by where it happens to be defined — e.g. _supported_gdn_classes() returns tuple[tuple[tuple[str, str], type[nn.Module]], ...], and this function picks the entry whose key is _MEGATRON_GDN_CLASS_PATH.
  2. Or classify by package over the MRO, which survives re-exports:
megatron_classes = tuple(
    candidate
    for candidate in supported_classes
    if candidate.__module__.partition(".")[0] == "megatron"
)
if not megatron_classes:
    return

Either way, please also make the resolution-check test assert the identity actually used here (cls.__module__ == _MEGATRON_GDN_CLASS_PATH[0]) so a silent upstream move breaks a test instead of the guard.

widening cast contributes no additional slack. Decay tensors that are live in BF16 or FP16 are
still validated against that live dtype's rounding.
Choose ``static_gate_input`` as a conservative lower bound, such as a low percentile measured on the
calibration slices. Increasing it shortens the derived horizons and omits more heads; a value above

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] "omits more heads" is ambiguous in a way that can invert the reader's understanding. Every head still appears in the policy with its static_horizons; what shrinks is retained_heads (retained = [head for head, horizon in enumerate(values) if horizon > selected_wmax], L482-483 of policy.py), i.e. more head state gets discarded. A reader who parses "omits heads" as "omits heads from sparsification" will conclude the opposite of the next clause.

The direction itself is right — larger static_gate_input → larger softplus(dt_bias + g) → more negative decay → shorter log(eps)/decay horizon → fewer heads clear horizon > Wmax. Only the noun is off:

Suggested change
calibration slices. Increasing it shortens the derived horizons and omits more heads; a value above
calibration slices. Increasing it shortens the derived horizons and retains fewer heads; a value above

Same phrasing in config.py's static_gate_input description ("shorten horizons and omit more heads") is worth aligning.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude review — 5 files changed, all reviewed (+171/-15)

Scope: full review (the trigger comment carried no extra scoping). Reviewed all 5 changed files: modelopt/torch/sparsity/state_sparsity/{policy,config,conversion}.py, docs/source/guides/6_sparsity.rst, tests/unit/torch/sparsity/state_sparsity/test_dasc.py.

Findings: CRITICAL: 1, IMPORTANT: 1, SUGGESTION: 1

Most impactful

  1. [CRITICAL ModeState] The new distributed guard also fires on restore and save (policy.py:238). _reject_distributed_megatron_gdn was added inside _get_gdn_modules, which is called by validate_dasc_model_structure and validate_dasc_decay_parameters — the restore and metadata-refresh paths — not only by analyze_gdn_decay/build_dasc_policy. Those paths only tolerate _DASCRecoverableStalenessError, so the plain ApplyModeError propagates: a TP=1-calibrated checkpoint restored into a TP=2 or PP=2 rematerialization now aborts mto.restore(), where it previously warned about a stale policy and proceeded (DASC is inert at runtime by design). mto.save() on such a model breaks the same way, and the message reads "policy export supports …" while surfacing during restore. Gating the call behind a keyword argument passed only from the two derivation entrypoints — or raising _DASCModelStructureMismatchError on the validation path — keeps the export guard without regressing checkpoint loading.

  2. [IMPORTANT Compatibility] The guard can silently disable itself (policy.py:187-196). The Megatron class is located by comparing candidate.__module__/__name__ to the declared path, and megatron_class is None returns without checking anything. __module__ is the defining module, so an upstream re-export makes this fail-closed check a no-op and a distributed policy exports rank-local indices with no error. No test covers that: the new Megatron tests use a fake whose __module__ is hand-set to match, and the resolution test only asserts the path yields an nn.Module subclass. Identify the class by the declared path it was resolved from, or classify over the MRO by megatron package prefix.

Verified correct (no action needed)

  • Removing update_last_state_before_new_mode from replace_dasc_mode is right: that hook exists to refresh the previous last mode before a new mode is stacked on, and recalibration replaces DASC state in place. DASC's own entry is fully rewritten, so nothing is left stale, and the new test pins the trailing mode's metadata rather than asserting the call.
  • _attach_policy unwrapping with force_unwrap=True is symmetric with get_attached_dasc_policy and replace_dasc_mode, so wrapped-model attach/lookup round-trips; the new nn.DataParallel restore test covers it.
  • New DASCPolicy validators for epsilon/static_gate_input reuse the same _validate_analysis_arguments already used by DASCConfig and analyze_gdn_decay, so the schema boundary and the analysis boundary cannot disagree. No circular-import concern — the helper lives in config.py.
  • Gate values added to the docs snippet (0.995 / 0.98 / 0.2) match the actual DASCConfig defaults.
  • static_gate_input monotonicity in the docs is correct: larger gate input → larger softplus → more negative decay → shorter log(eps)/decay horizon → fewer heads clear horizon > Wmax. Only the wording is worth tightening (see the SUGGESTION on 6_sparsity.rst).

Risk

Moderate, and concentrated in one place. Both blocking findings are about where the new fail-closed check lives rather than what it computes: as placed it is simultaneously too broad (breaks restore/save on distributed geometry) and too narrow (silently skippable via an upstream re-export). Everything else — the in-place recalibration fix, the wrapper-safe attachment, the policy-schema validators, and the docs — looks sound and is well covered by the new tests.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.54839% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.77%. Comparing base (0859c13) to head (a8c8a7d).

Files with missing lines Patch % Lines
modelopt/torch/sparsity/state_sparsity/policy.py 90.00% 2 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##           feature/dasc-state-sparsity    #2408      +/-   ##
===============================================================
- Coverage                        78.78%   78.77%   -0.01%     
===============================================================
  Files                              548      549       +1     
  Lines                            64243    66562    +2319     
===============================================================
+ Hits                             50615    52436    +1821     
- Misses                           13628    14126     +498     
Flag Coverage Δ
examples-diffusers 20.82% <25.80%> (+<0.01%) ⬆️
examples-gpt-oss 13.39% <25.80%> (+<0.01%) ⬆️
examples-hf_ptq 21.78% <25.80%> (+<0.01%) ⬆️
examples-llm_distill 13.46% <25.80%> (+<0.01%) ⬆️
examples-llm_eval 17.26% <25.80%> (+<0.01%) ⬆️
examples-llm_qat 17.60% <25.80%> (+<0.01%) ⬆️
examples-llm_sparsity 15.95% <25.80%> (+<0.01%) ⬆️
examples-megatron_bridge 26.25% <25.80%> (+<0.01%) ⬆️
examples-specdec_bench 13.15% <25.80%> (+<0.01%) ⬆️
examples-speculative_decoding 17.68% <25.80%> (+<0.01%) ⬆️
examples-torch_onnx 21.82% <25.80%> (+<0.01%) ⬆️
examples-torch_trt 15.16% <25.80%> (+<0.01%) ⬆️
gpu 58.34% <25.80%> (-0.02%) ⬇️
regression 15.16% <25.80%> (+<0.01%) ⬆️
unit 58.37% <93.54%> (+0.89%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

1 participant