Skip to content

Harden DASC analysis and restore boundaries - #2394

Merged
kaix-nv merged 2 commits into
feature/dasc-state-sparsity-review-contractfrom
feature/dasc-state-sparsity-review-api
Sep 11, 2026
Merged

kaix-nv merged 2 commits into
feature/dasc-state-sparsity-review-contractfrom
feature/dasc-state-sparsity-review-api

Conversation

@kaix-nv

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

Copy link
Copy Markdown
Contributor

Summary

Addresses the remaining review findings on #2389:

  • validate public analysis arguments uniformly before model traversal, including unhashable storage-dtype inputs
  • validate original decay tensors before checkpoint-storage casting can mask invalid dtypes
  • treat temporarily unavailable decay tensors on an otherwise supported GDN identity as recoverable stale policy state on save and restore
  • retain fail-closed behavior when the supported GDN architecture is absent or replaced
  • document and test the checkpoint lifecycle boundary

Validation

  • python -m pytest -q tests/unit/torch/sparsity/state_sparsity/test_dasc.py (34 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 (312 passed, 1 optional Megatron skip)
  • pre-commit run --files ... on all changed files
  • signed commit with DCO sign-off

Summary by CodeRabbit

  • Bug Fixes

    • Improved recovery when decay data is temporarily unavailable, allowing affected policies to be restored with a warning.
    • Added stricter validation for decay-analysis settings, including tolerance values, gate inputs, storage types, and invalid tensor values.
    • Ensured policy validation and horizon calculations work correctly regardless of the default device.
    • Policies with unsupported architecture changes continue to fail safely during save and restore.
    • Stale policies remain blocked from export until recalibration.
  • Documentation

    • Clarified recoverable versus unsupported policy changes in sparsity guidance.

Signed-off-by: kaix-nv <kaix@nvidia.com>
@kaix-nv
kaix-nv requested review from a team as code owners September 11, 2026 04:01
@kaix-nv
kaix-nv requested review from realAsma and removed request for a team September 11, 2026 04:02
@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: c5055bfd-f64a-447f-bfe7-a350157956b9

📥 Commits

Reviewing files that changed from the base of the PR and between 4d05a1b and c2da5fa.

📒 Files selected for processing (3)
  • modelopt/torch/sparsity/state_sparsity/config.py
  • modelopt/torch/sparsity/state_sparsity/policy.py
  • tests/unit/torch/sparsity/state_sparsity/test_dasc.py

📝 Walkthrough

Walkthrough

DASC policy handling now classifies recoverable staleness, validates decay and analysis inputs before processing, updates restoration paths, documents serialization rules, and adds boundary and checkpoint recovery tests.

Changes

DASC staleness handling

Layer / File(s) Summary
Policy errors and validation
modelopt/torch/sparsity/state_sparsity/policy.py, modelopt/torch/sparsity/state_sparsity/config.py
DASC policy errors distinguish recoverable structure mismatches and unavailable decay tensors. Decay inputs, epsilon, static_gate_input, and storage-dtype arguments receive explicit validation. Horizon values use scalar math and CPU storage.
Conversion and serialization behavior
modelopt/torch/sparsity/state_sparsity/conversion.py, docs/source/guides/6_sparsity.rst
Restoration and metadata updates handle recoverable staleness. The sparsity guide documents recoverable stale-policy cases and fail-closed unsupported architecture changes.
Boundary and recovery tests
tests/unit/torch/sparsity/state_sparsity/test_dasc.py
Tests cover invalid public analysis arguments, meta-default-device behavior, and recovery when A_log is temporarily unavailable. Export still rejects the unrecovered policy.

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

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 4d05a

The updated DASC validation and stale-policy recovery paths preserve fail-closed export behavior for unavailable decay tensors, with no unresolved merge risk identified.

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: hardening DASC analysis validation and restore boundary handling.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 3 files. (1 skipped: 1…
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.
Security Anti-Patterns ✅ Passed PASS. The pull request changes only DASC Python logic in modelopt/torch/sparsity/state_sparsity/conversion.py and policy.py, plus documentation and tests. The added code contains no `torch.load(..…
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 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-api

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

@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 full review

Comment on lines +213 to +214
# Check the original tensors before a storage cast can hide an invalid integer dtype.
_validate_gdn_decay_tensors(a_log, dt_bias)

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] This hoist is a behavioral no-op, and the comment describes a guarantee that already existed.

  • With storage_dtype is not None, the pre-cast validation was already in place (it was the line removed from inside the if), so the "invalid integer dtype hidden by the cast" case was already rejected before this PR.
  • With storage_dtype is None, a_log/dt_bias are passed through unchanged and compute_gdn_decay_horizons runs the identical _validate_gdn_decay_tensors(a_log, dt_bias) at policy.py:121 — same function, same tensors, same message. So the only effect here is a second, redundant validation pass on the non-cast path, which cuts against the "Validate external input once" guideline in CONTRIBUTING.md.

Not harmful, but the PR summary lists this as a fix ("validate original decay tensors before checkpoint-storage casting can mask invalid dtypes"), and nothing about the observable contract changes. If the goal is a single explicit validation point in _analyze_gdn_modules, that's a reasonable readability choice — but then the comment should say that rather than implying a cast-masking hole was closed. If some other masking case was intended (e.g. a dtype that survives _validate_gdn_decay_tensors on the original but not after the cast), a test asserting the new rejection would pin it down; the added tests only cover the analyze_gdn_decay argument boundary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed and corrected in #2395. The hoist was redundant on the no-cast path and did not close a new masking hole. It is reverted; the original pre-cast validation remains only in the storage-cast branch, while direct computation validates its actual inputs once.

Comment on lines +240 to +251
try:
epsilon_is_valid = math.isfinite(epsilon) and 0.0 < epsilon < 1.0
except TypeError:
epsilon_is_valid = False
if not epsilon_is_valid:
raise ValueError("epsilon must be finite and in (0, 1)")
try:
static_gate_input_is_valid = math.isfinite(static_gate_input)
except TypeError:
static_gate_input_is_valid = False
if not static_gate_input_is_valid:
raise ValueError("static_gate_input must be finite")

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] The new argument contract is only enforced on one of the two public analysis entry points, so "validate public analysis arguments uniformly" is not quite achieved.

compute_gdn_decay_horizons is exported alongside analyze_gdn_decay (__all__ at policy.py:43, re-exported from state_sparsity/__init__.py) and takes the same epsilon/static_gate_input kwargs, but keeps the weaker checks:

  • compute_gdn_decay_horizons(a, b, epsilon=[])TypeError from 0.0 < [], not the ValueError this PR standardizes on.
  • epsilon=float("nan")ValueError("epsilon must be in (0, 1)"), a different message for the same defect (two strings to keep in sync).
  • static_gate_input is not validated at all, so static_gate_input=float("nan") falls through to the horizon check and reports "GDN decay parameters produced non-finite or non-positive horizons" — i.e. it blames the model's decay tensors for a bad caller argument, which is exactly the failure mode test_analysis_rejects_invalid_static_gate_input_at_the_public_boundary was added to prevent on the sibling API.

Extracting one helper keeps a single source of truth and closes the gap:

def _validate_analysis_arguments(epsilon: float, static_gate_input: float) -> None:
    """Normalize invalid public analysis arguments to the ValueError contract."""
    if not isinstance(epsilon, float | int) or not (math.isfinite(epsilon) and 0.0 < epsilon < 1.0):
        raise ValueError("epsilon must be finite and in (0, 1)")
    if not isinstance(static_gate_input, float | int) or not math.isfinite(static_gate_input):
        raise ValueError("static_gate_input must be finite")

Then call it at the top of both compute_gdn_decay_horizons and analyze_gdn_decay (replacing lines 118-119 and 240-251).

Secondary note on the current form: isinstance is the idiom used a few lines below for decay_parameter_storage_dtype, and it is also tighter than try/except TypeErrormath.isfinite does not raise TypeError for every non-scalar input (a multi-element tensor raises a different exception type from __float__), so the except TypeError normalization has holes that an explicit type check does not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #2395. Both exported entry points now call one _validate_analysis_arguments helper before tensor/model work, and direct compute regressions cover invalid epsilon and static_gate_input values. The explicit scalar type check also avoids exception-type holes from tensor-like inputs.

try:
validate_dasc_model_structure(model, policy)
except _DASCModelStructureMismatchError as error:
except _DASCRecoverableStalenessError as error:

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] Widening this except to the new base class is the right call for save/restore symmetry, and fail-closed is preserved for the hard cases (_get_gdn_modules still raises a plain ApplyModeError for "no supported GDN modules" and for unconverted subclasses, so neither is swallowed here). One follow-up on the user-facing guidance:

For the newly recoverable case, the advice is wrong. _DASCDecayParametersUnavailableError fires when a supported GDN identity is present but A_log/dt_bias are temporarily unavailable — parameters flattened by a sharding wrapper, or a model not yet materialized off the meta device. The remedy there is to materialize the parameters and re-save/re-validate, not to recalibrate: the policy itself is still valid for this checkpoint, and re-running calibrate() on a model whose decay tensors are missing fails with the same error. Concatenating "re-run calibrate() before deployment" onto that message (line 103, and again at line 117) sends users down a dead end.

Since both handlers now cover two distinct conditions, consider making the remedy condition-specific, e.g.:

_STALENESS_REMEDIES = {
    _DASCDecayParametersUnavailableError: (
        "materialize the GDN decay parameters before saving or deploying"
    ),
    _DASCModelStructureMismatchError: "re-run calibrate() before deployment",
}

and formatting the warning with _STALENESS_REMEDIES[type(error)]. The same distinction applies to the guide text added in docs/source/guides/6_sparsity.rst — "export_policy rejects it until recalibration" is accurate for geometry/decay drift, but recalibration is not the fix for temporarily unavailable tensors.

@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown

Claude review — DASC analysis and restore boundaries

Scope: full review (the trigger comment carried no scoping instructions). All 4 changed files reviewed (83 additions / 16 deletions): modelopt/torch/sparsity/state_sparsity/policy.py, .../conversion.py, tests/unit/torch/sparsity/state_sparsity/test_dasc.py, docs/source/guides/6_sparsity.rst. I also read policy.py end-to-end for the _get_gdn_modules / validate_dasc_* composition and conversion.py's save/restore/replace paths, since this change is about which errors are recoverable across the checkpoint boundary.

Findings: CRITICAL: 0, IMPORTANT: 0, SUGGESTION: 3

What I verified

  • Fail-closed boundary holds. The new _DASCRecoverableStalenessError base only widens the except in restore_dasc_model / update_dasc_metadata to also cover _DASCDecayParametersUnavailableError. The two hard failures in _get_gdn_modules — "no supported GDN modules" and _reject_unconverted_gdn_subclasses — still raise plain ApplyModeError, so removing or replacing the supported GDN architecture is not swallowed on save or restore, matching the guide text.
  • Save/restore symmetry is real. Both handlers now catch the same base class, and export_policy still re-enters _get_gdn_modules, so the deployment gate rejects the unavailable-tensor case (asserted by the new test_temporarily_unavailable_decay_tensors_are_recoverable_staleness). No modelopt_state schema change: the metadata payload is still {"policy": ...}, so existing DASC checkpoints round-trip unchanged.
  • The unhashable-input fix is correct. _STORAGE_DTYPES[[]] previously escaped as TypeError: unhashable type; the isinstance(..., str) guard before the membership test converts that to the documented ValueError, and the parametrized test covers both "float8" and [].
  • calibrate() still fails hard on missing decay tensors — build_dasc_policy does not catch the new subclass, and the existing match="without A_log and dt_bias tensors" test still pins that.

Most impactful suggestions

  1. The hoisted _validate_gdn_decay_tensors in _analyze_gdn_modules is a behavioral no-op (policy.py:213-214). The pre-cast validation already existed on the storage_dtype is not None path, and on the None path compute_gdn_decay_horizons runs the identical check on the identical tensors. Worth reconciling with the PR summary's claim that a cast-masking hole was closed, or adjusting the comment.
  2. The new argument contract covers only one of the two public analysis entry points (policy.py:240-251). compute_gdn_decay_horizons is exported alongside analyze_gdn_decay and takes the same kwargs, but a non-numeric epsilon still raises TypeError, nan yields a differently worded ValueError, and static_gate_input is unvalidated — so static_gate_input=nan still surfaces as "GDN decay parameters produced non-finite or non-positive horizons", blaming the model for a caller argument. A shared _validate_analysis_arguments helper closes the gap and removes the duplicated message.
  3. "re-run calibrate()" is the wrong remedy for the newly recoverable case (conversion.py:101/115, and the guide's "until recalibration"). Temporarily unavailable decay tensors need the parameters materialized, not a recalibration — which would hit the same error.

Risk: low. Tightly scoped hardening of an unreleased feature branch, additive exception hierarchy, no public signature/default/state-schema changes, and each new behavior has a test. All three findings are non-blocking quality points.

@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 passed — no blocking issues found. LGTM

@github-actions

github-actions Bot commented Sep 11, 2026

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

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.78%. Comparing base (5001316) to head (c2da5fa).
⚠️ Report is 1 commits behind head on feature/dasc-state-sparsity-review-contract.

Additional details and impacted files
@@                             Coverage Diff                              @@
##           feature/dasc-state-sparsity-review-contract    #2394   +/-   ##
============================================================================
  Coverage                                        78.78%   78.78%           
============================================================================
  Files                                              548      548           
  Lines                                            64227    64243   +16     
============================================================================
+ Hits                                             50599    50615   +16     
  Misses                                           13628    13628           
Flag Coverage Δ
examples-diffusers 20.81% <25.00%> (+<0.01%) ⬆️
examples-gpt-oss 13.39% <25.00%> (+<0.01%) ⬆️
examples-hf_ptq 21.78% <25.00%> (+<0.01%) ⬆️
examples-llm_distill 13.46% <25.00%> (+<0.01%) ⬆️
examples-llm_eval 17.25% <25.00%> (+<0.01%) ⬆️
examples-llm_qat 17.59% <25.00%> (+<0.01%) ⬆️
examples-llm_sparsity 15.94% <25.00%> (+<0.01%) ⬆️
examples-megatron_bridge 26.25% <25.00%> (+<0.01%) ⬆️
examples-specdec_bench 13.14% <25.00%> (+<0.01%) ⬆️
examples-speculative_decoding 17.67% <25.00%> (+<0.01%) ⬆️
examples-torch_onnx 21.82% <25.00%> (+<0.01%) ⬆️
examples-torch_trt 15.15% <25.00%> (+<0.01%) ⬆️
gpu 58.35% <25.00%> (-0.01%) ⬇️
regression 15.15% <25.00%> (+<0.01%) ⬆️
unit 57.47% <100.00%> (+0.01%) ⬆️

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.

## Summary

Addresses the two follow-up findings on #2394:

- use one argument validator for both exported analysis entry points
- normalize invalid epsilon and static gate inputs to ValueError before
tensor/model work
- remove the redundant validation hoist and retain the existing pre-cast
check only where a storage cast occurs
- add direct public-API regressions for `compute_gdn_decay_horizons`

## Validation

- `python -m pytest -q
tests/unit/torch/sparsity/state_sparsity/test_dasc.py` (36 passed, 1
optional Megatron skip)
- pre-commit hooks on both changed files
- signed commit with DCO sign-off

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved validation for sparsity analysis inputs, including finite
numeric values and supported epsilon ranges.
- Ensured invalid decay-tensor values are detected consistently across
analysis workflows.
  - Standardized error handling for direct decay-horizon calculations.

- **Tests**
- Added coverage for invalid epsilon and static gate input values,
including verification of consistent error messages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: kaix-nv <kaix@nvidia.com>
@kaix-nv
kaix-nv merged commit b453370 into feature/dasc-state-sparsity-review-contract Sep 11, 2026
9 of 11 checks passed
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