Skip to content

test: targeted unit tests for codecov-uncovered lines across eight modules#1927

Open
arham766 wants to merge 1 commit into
NVIDIA:mainfrom
arham766:tests/targeted-coverage
Open

test: targeted unit tests for codecov-uncovered lines across eight modules#1927
arham766 wants to merge 1 commit into
NVIDIA:mainfrom
arham766:tests/targeted-coverage

Conversation

@arham766

@arham766 arham766 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: New tests

Overview: Consolidates all open test-suite PRs into a single targeted PR, per maintainer feedback in #1902:

Please only add targeted tests for features which are not covered by any tests... Prefer tests that actually test meaningful logic instead of adding tests just for the sake of coverage and dedupe tests as many as possible. Also please merge all test related PRs into a single PR

Every test in this PR exercises lines the codecov report marks uncovered on main; suites for modules already >=90% covered by GPU/e2e CI were dropped entirely, and the rest were trimmed and deduplicated hard (parametrization clusters merged, redundant variants removed). Result: 95 tests, ~1.4k lines, <1s runtime (down from 914 tests / ~9.4k lines across the original PRs).

Module codecov before uncovered lines hit tests
torch/export/postprocess.py 26.6% 151/248 33
torch/utils/graph.py 26.4% 39/39 (module -> 100%) 17
torch/quantization/calib/bias.py 70.0% 21/21 6
torch/utils/logging.py 74.3% 25/26 8
torch/utils/distributed.py 81.4% 29/31 9
torch/distill/losses.py 81.9% 13/13 7
torch/quantization/conversion.py 87.0% 27/28 10
torch/utils/robust_json.py 87.0% 5/6 5

Lines deliberately left uncovered, so nothing is fake-covered:

  • postprocess.py 297-471, 488-542, 577-623: the TP/PP shared-memory merge paths require a real multi-rank process group (get_tensors_parallel asserts len(ranks) > 1) — GPU/multi-process CI territory.
  • distributed.py 165-166: CUDA allgather padding branch, needs world_size > 1.
  • logging.py 87: the positional-arg branch of the tqdm silencer overwrites tqdm's ascii argument instead of disable (off-by-one in the positional index), so covering it would pin broken behavior. Happy to fix in a follow-up.
  • conversion.py 303 and robust_json.py 65: unreachable defensive branches (pydantic coerces dict cfgs before the isinstance check; the __class__.__module__ fallback accepts every real object).

Also folds in the test_mode_registry.py assertion fix (was #1921).

Replaces (closing in favor of this PR): #1903, #1904, #1905, #1906, #1907, #1913, #1914, #1915, #1916, #1921, #1922, #1923, #1924, #1925.

The open bug-fix PRs (#1908-#1912, #1917-#1920) are unaffected — their regression tests live in existing test files. Two tests here pin current arguably-buggy behavior with NOTE: comments (postprocess odd-row padding mutating the input config; MGD loss not detaching teacher features, see #1926) and will be flipped if/when fixes land.

Testing

pytest tests/unit/torch/export/test_postprocess.py tests/unit/torch/distill/test_losses.py   tests/unit/torch/utils/test_graph.py tests/unit/torch/utils/test_logging.py   tests/unit/torch/utils/test_distributed.py tests/unit/torch/utils/test_robust_json.py   tests/unit/torch/quantization/test_conversion.py tests/unit/torch/quantization/test_bias_calib.py   tests/unit/torch/opt/test_mode_registry.py
# 96 passed in 0.98s

Each file was also mutation-checked (seeded logic breaks in the target module -> the intended test fails, source reverted).

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A
  • Did you get Claude approval on this PR?: N/A (external contributor)

Additional Information

Issue: #1902

Summary by CodeRabbit

  • Tests
    • Expanded automated coverage across distillation losses, export post-processing, quantization conversion and calibration, distributed utilities, graph matching, logging, and JSON serialization.
    • Added checks for edge cases such as tensor cloning, shape mismatches, quantizer state restoration, warning behavior, and single-process distributed fallbacks.
    • Improved confidence in model optimization workflows by validating gradients, masking, padding, and configuration handling across several core utilities.

…dules

Consolidates the standalone test-suite PRs into a single PR per
maintainer feedback in NVIDIA#1902: every test here exercises lines the
codecov report marks uncovered, asserts meaningful logic rather than
coverage for its own sake, and parametrization clusters are deduplicated.

Modules targeted (codecov coverage before):
- torch/export/postprocess.py (26.6%): TP/PP split layouts, padding
  math, quantization-scale resharding
- torch/utils/graph.py (26.4%): subgraph matching, memoization,
  arity/connectivity mismatches
- torch/quantization/calib/bias.py (70.0%): method dispatch, running
  averages, bias round-trips, reset
- torch/utils/logging.py (74.3%): tqdm silencing, tty ANSI wrapping,
  capture_io, warning filters
- torch/utils/distributed.py (81.4%): FileLock wait loop, master_only,
  rank fallbacks
- torch/distill/losses.py (81.9%): MGD forward math, MFT threshold
  branch
- torch/quantization/conversion.py (87.0%): SVDQuant entrypoint,
  restore error paths, SequentialQuantizer edge branches, context
  manager module-type restoration
- torch/utils/robust_json.py (87.0%): encoder special-type fallbacks

Also folds in the test_mode_registry assertion fix (was NVIDIA#1921).

Lines that remain uncovered need multi-rank process groups or are
unreachable defensive branches; they are enumerated in the PR body.

Signed-off-by: arham766 <arhamislam766@yahoo.com>
@arham766 arham766 requested a review from a team as a code owner July 6, 2026 09:53
@copy-pr-bot

copy-pr-bot Bot commented Jul 6, 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.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds new unit test modules and expands existing tests covering distillation losses (MFTLoss/MGDLoss), export postprocessing (TP/PP splitting, padding, LM-head quantization), mode registry cleanup, quantization bias calibration and conversion, and torch utils (distributed, graph matching, logging, robust JSON). No non-test source files are modified.

Changes

Test Coverage Additions

Layer / File(s) Summary
Distillation loss tests
tests/unit/torch/distill/test_losses.py
New tests for MFTLoss threshold logic and MGDLoss covering MSE equivalence, alpha scaling, masking, channel alignment/gradient flow, spatial mismatch, and teacher gradient propagation.
Export postprocess tests
tests/unit/torch/export/test_postprocess.py
New tests for tensor helpers, FP8 view roundtrips, TP/PP config splitting, model config postprocessing, embedding/LM-head padding, LM-head quantization updates, weight shape validation, and view-tensor cloning.
Quantization conversion tests
tests/unit/torch/quantization/test_conversion.py
New tests for SVDQuant conversion, restore_quantizer_state errors, SequentialQuantizer partial/full updates, context-manager restoration, and deprecated attribute-setting shim.
Bias calibration tests
tests/unit/torch/quantization/test_bias_calib.py
New tests for compute_bias/subtract_bias/add_bias round-trip and BiasCalibrator running-average, dtype preservation, statelessness, and reset.
Mode registry cleanup assertions
tests/unit/torch/opt/test_mode_registry.py
Expanded assertions for removed-mode KeyError and explicit registry teardown patching __del__.
Distributed utility tests
tests/unit/torch/utils/test_distributed.py
New tests for local_rank fallback, master_only, DistributedProcessGroup rank fallback, get_group, is_dtensor_sharded, and FileLock behavior.
Graph matching tests
tests/unit/torch/utils/test_graph.py
New module fixtures and tests validating FX graph match() equivalence, mismatch, empty/multiple patterns, and untraceable modules.
Logging and robust JSON tests
tests/unit/torch/utils/test_logging.py, tests/unit/torch/utils/test_robust_json.py
New tests for logging context managers/warnings and json_dumps encoding of special types.

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

Possibly related issues

Suggested reviewers: ChenhanYu

🚥 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 accurately summarizes the main change: targeted unit tests added across eight modules.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 No modelopt/examples Python or dependency changes are present; only tests were added, and SECURITY.md excludes tests from these security rules.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
tests/unit/torch/opt/test_mode_registry.py (1)

54-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cleanup won't run if an earlier assertion fails, leaking registry into _all_registries for other tests.

If the assertions on Lines 55-57 fail, the explicit removal on Line 66 and the __del__ patch on Lines 67-68 never execute, leaving registry in _ModeRegistryCls._all_registries for the rest of the test session — exactly the residue this code is trying to prevent. Wrapping the mode-registration/removal logic in try/finally would guarantee cleanup runs regardless of assertion outcome.

♻️ Proposed fix using try/finally
-    registry.remove_mode("a_test_mode")
-
-    # after removal, the mode must no longer resolve via any registry
-    assert not _ModeRegistryCls.contained_in_any("a_test_mode")
-    with pytest.raises(KeyError, match="a_test_mode"):
-        _ModeRegistryCls.get_from_any("a_test_mode")
-
-    # NOTE: `del registry` alone does NOT remove the registry from
-    # `_ModeRegistryCls._all_registries`: the class-level list holds a strong reference, so the
-    # local `del` never drops the refcount to zero and `__del__` (which would remove it from the
-    # list) is unreachable. Remove it explicitly so this test leaves no residue behind for other
-    # tests sharing the process. Since `__del__` unconditionally calls `list.remove`, it would
-    # then raise ValueError once the orphaned object is garbage collected, so neutralize it for
-    # the actual destruction.
-    _ModeRegistryCls._all_registries.remove(registry)
-    with mock.patch.object(_ModeRegistryCls, "__del__", return_value=None):
-        del registry  # refcount now drops to zero -> patched `__del__` runs here
-
-    assert all(r._registry_name != "test_registry" for r in _ModeRegistryCls._all_registries)
+    try:
+        registry.remove_mode("a_test_mode")
+
+        # after removal, the mode must no longer resolve via any registry
+        assert not _ModeRegistryCls.contained_in_any("a_test_mode")
+        with pytest.raises(KeyError, match="a_test_mode"):
+            _ModeRegistryCls.get_from_any("a_test_mode")
+    finally:
+        # NOTE: `del registry` alone does NOT remove the registry from
+        # `_ModeRegistryCls._all_registries`: the class-level list holds a strong reference, so the
+        # local `del` never drops the refcount to zero and `__del__` (which would remove it from the
+        # list) is unreachable. Remove it explicitly so this test leaves no residue behind for other
+        # tests sharing the process. Since `__del__` unconditionally calls `list.remove`, it would
+        # then raise ValueError once the orphaned object is garbage collected, so neutralize it for
+        # the actual destruction.
+        _ModeRegistryCls._all_registries.remove(registry)
+        with mock.patch.object(_ModeRegistryCls, "__del__", return_value=None):
+            del registry  # refcount now drops to zero -> patched `__del__` runs here
+
+    assert all(r._registry_name != "test_registry" for r in _ModeRegistryCls._all_registries)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/torch/opt/test_mode_registry.py` around lines 54 - 70, The cleanup
in test_mode_registry relies on assertions completing first, so a failure before
the explicit removal can leave `registry` behind in
`_ModeRegistryCls._all_registries`. Wrap the registration/assertion block around
the `registry` lifecycle in a `try/finally` so the cleanup always runs, and keep
the explicit `_all_registries.remove(registry)` plus the
`mock.patch.object(_ModeRegistryCls, "__del__", return_value=None)` teardown in
the `finally` block to ensure no residue remains for later tests.
tests/unit/torch/utils/test_distributed.py (1)

74-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Timer-based synchronization risks flakiness under CI load.

This test relies on a real threading.Timer(0.2, ...) racing against a polling loop (poll_time=0.02) inside the with block. Under a slow/contended CI runner, the timer thread could be delayed enough for the polling FileLock to time out (if it has an internal timeout) or the test to hang longer than expected, since real wall-clock timing is used rather than a deterministic signal.
[recommended_refactor_reason]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/torch/utils/test_distributed.py` around lines 74 - 86, The test
test_filelock_all_acquire_waits_for_release is flaky because it depends on real
wall-clock timing via threading.Timer and a polling FileLock acquire loop.
Refactor it to use deterministic synchronization in the test, such as an Event
or a mocked release trigger, so the first FileLock is released only after the
second acquire attempt is definitely waiting. Keep the assertions around
dist.FileLock, try_acquire, and the all_acquire context, but remove reliance on
timing-sensitive poll_time behavior.
tests/unit/torch/export/test_postprocess.py (1)

552-556: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reliance on private Tensor._is_view() API.

_is_view() is an internal/private PyTorch tensor method (underscore-prefixed), not part of the documented public API. It could change or be removed in a future torch release. A more stable check is verifying tensor._base is not None is avoided too (also private) — consider comparing data_ptr()/storage().data_ptr() equality with the base tensor, or checking tensor.is_contiguous()/shape post-clone, to avoid depending on a private method.

♻️ Alternative using storage identity instead of private API
 def test_postprocess_tensors_clones_views():
     base = torch.randn(4, 4)
     view = base[:2]
-    assert view._is_view()
+    assert view.storage().data_ptr() == base.storage().data_ptr()
     weights = {"a.weight": view}
     postprocess_tensors(weights, torch.float32)
-    assert not weights["a.weight"]._is_view()
+    assert weights["a.weight"].storage().data_ptr() != base.storage().data_ptr()
     assert torch.equal(weights["a.weight"], base[:2])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/torch/export/test_postprocess.py` around lines 552 - 556, Replace
the private Tensor._is_view() assertions in test_postprocess with a public,
stable check in the postprocess_tensors test case. Use the existing view/base
tensors in the test to verify aliasing via storage or data pointer identity
before and after postprocess_tensors, and keep the final equality assertion
against base[:2] to confirm the cloned tensor preserves values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/torch/export/test_postprocess.py`:
- Around line 552-556: Replace the private Tensor._is_view() assertions in
test_postprocess with a public, stable check in the postprocess_tensors test
case. Use the existing view/base tensors in the test to verify aliasing via
storage or data pointer identity before and after postprocess_tensors, and keep
the final equality assertion against base[:2] to confirm the cloned tensor
preserves values.

In `@tests/unit/torch/opt/test_mode_registry.py`:
- Around line 54-70: The cleanup in test_mode_registry relies on assertions
completing first, so a failure before the explicit removal can leave `registry`
behind in `_ModeRegistryCls._all_registries`. Wrap the registration/assertion
block around the `registry` lifecycle in a `try/finally` so the cleanup always
runs, and keep the explicit `_all_registries.remove(registry)` plus the
`mock.patch.object(_ModeRegistryCls, "__del__", return_value=None)` teardown in
the `finally` block to ensure no residue remains for later tests.

In `@tests/unit/torch/utils/test_distributed.py`:
- Around line 74-86: The test test_filelock_all_acquire_waits_for_release is
flaky because it depends on real wall-clock timing via threading.Timer and a
polling FileLock acquire loop. Refactor it to use deterministic synchronization
in the test, such as an Event or a mocked release trigger, so the first FileLock
is released only after the second acquire attempt is definitely waiting. Keep
the assertions around dist.FileLock, try_acquire, and the all_acquire context,
but remove reliance on timing-sensitive poll_time behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 96c3e069-5874-4268-bdec-d9e8ae1badc9

📥 Commits

Reviewing files that changed from the base of the PR and between 795c589 and 2f8c3b7.

📒 Files selected for processing (9)
  • tests/unit/torch/distill/test_losses.py
  • tests/unit/torch/export/test_postprocess.py
  • tests/unit/torch/opt/test_mode_registry.py
  • tests/unit/torch/quantization/test_bias_calib.py
  • tests/unit/torch/quantization/test_conversion.py
  • tests/unit/torch/utils/test_distributed.py
  • tests/unit/torch/utils/test_graph.py
  • tests/unit/torch/utils/test_logging.py
  • tests/unit/torch/utils/test_robust_json.py

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