Skip to content

Support per expert weight quantizer in TEGroupedMLP#1550

Open
jenchen13 wants to merge 10 commits into
mainfrom
jennifchen/te_per_expert
Open

Support per expert weight quantizer in TEGroupedMLP#1550
jenchen13 wants to merge 10 commits into
mainfrom
jennifchen/te_per_expert

Conversation

@jenchen13

@jenchen13 jenchen13 commented May 27, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: ?

note: if different TP/EP is used during checkpoint restore, the per expert weight quantizer may not restore properly. TODO fix this

Usage

# Add a code snippet demonstrating how to use this

Testing

Before your PR is "Ready for review"

Make sure you read and follow Contributor guidelines and your commits are signed (git commit -s -S).

Make sure you read and follow the Security Best Practices (e.g. avoiding hardcoded trust_remote_code=True, torch.load(..., weights_only=False), pickle, etc.).

  • Is this change backward compatible?: ✅ / ❌ / N/A
  • 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?: ✅ / ❌ / N/A
  • Did you update Changelog?: ✅ / ❌ / N/A
  • Did you get Claude approval on this PR?: ✅ / ❌ / N/A

Additional Information

Summary by CodeRabbit

  • New Features
    • Added an opt-in per-expert (per-GEMM) weight-quantization mode for Transformer Engine grouped-linear layers, including automatic per-expert recalibration after model restore when enabled.
    • Added an opt-in environment setting to run the grouped per-expert weight quantization loop with torch.compile (default remains eager).
  • Bug Fixes
    • Improved max-calibration amax synchronization/recursion so grouped quantizers participate consistently across distributed calibration paths.
  • Tests
    • Expanded GPU MoE quantization tests covering grouped vs sequential amax behavior, optional compiled execution, and numerical finiteness.

@jenchen13 jenchen13 requested a review from a team as a code owner May 27, 2026 23:00
@jenchen13 jenchen13 requested a review from sychen52 May 27, 2026 23:00
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds per-GEMM weight quantizers for TEGroupedLinear, extends calibration to grouped quantizer wrappers, and updates MoE tests to cover restore, compilation, and output behavior. It also refreshes the related config text and changelog entry.

Changes

Per-expert weight quantization for TEGroupedLinear

Layer / File(s) Summary
Grouped quantizer container
modelopt/torch/quantization/nn/modules/tensor_quantizer.py
Adds GroupedQuantizer to the public exports and implements a module-list container that delegates lifecycle and config operations across contained quantizers.
Calibration support for grouped quantizers
modelopt/torch/quantization/model_calib.py, modelopt/torch/quantization/config.py
Calibration recursion and distributed amax syncing now treat GroupedQuantizer like SequentialQuantizer, and the sync_expert_weight_amax description is updated accordingly.
TEGrouped per-GEMM quantizer execution
modelopt/torch/quantization/plugins/transformer_engine.py, CHANGELOG.rst
_QuantTEGroupedLinear now builds per-GEMM weight quantizers, restores and recalibrates them per weight tensor, quantizes grouped GEMM weights with those quantizers, and optionally uses a compiled loop gated by MODELOPT_TEGROUPED_COMPILE_WEIGHT_LOOP.
MoE validation tests
tests/gpu_megatron/torch/quantization/plugins/test_megatron.py
Adds tests for _amax shape preservation, opt-in compilation, and TEGrouped-versus-Sequential MoE amax and loss comparisons under shared quantization settings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • NVIDIA/Model-Optimizer#1937: Also changes te_grouped_quantized_linear_fn in transformer_engine.py for TE GroupedLinear per-GEMM handling.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 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: adding per-expert weight quantizer support in TEGroupedMLP.
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 added banned patterns in changed modelopt/examples Python files; packaging files also unchanged in HEAD^..HEAD.
✨ 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 jennifchen/te_per_expert

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-1550/

Built to branch gh-pages at 2026-07-09 14:39 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modelopt/torch/quantization/plugins/transformer_engine.py (1)

147-166: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Per-expert restore is still unsafe when TP/EP changes.

This branch creates weight_quantizer_{i} modules, but modelopt_post_restore() still runs the base restore path that only knows about self.weight_quantizer. If a checkpoint is restored under different TP/EP, the per-expert _amax tensors can keep the old shape and no code here reinitializes them. Please either block this mode on topology changes or add a per-expert reset/recalibration path during restore.

🤖 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 `@modelopt/torch/quantization/plugins/transformer_engine.py` around lines 147 -
166, The per-expert weight-quantizer path (_per_expert_weight_quantizer and the
created modules weight_quantizer_{i}) is unsafe on TP/EP topology change because
modelopt_post_restore currently only runs the base path and does not
reinitialize per-expert _amax buffers; update modelopt_post_restore to detect
topology/shape mismatch and either (a) fail-fast by raising a clear error when
_per_expert_weight_quantizer is enabled and the restored per-expert _amax shapes
don't match the current weight shapes, or (b) perform a per-expert
recalibration: iterate over each created module name weight_quantizer_{i} (use
self.num_gemms to enumerate), reset/recreate their _amax buffers to the correct
shape for the current self.weight{i} and call the quantizer's max_calibrate (or
its equivalent reinit routine) so each per-expert quantizer is re-calibrated
after restore; implement one of these branches in modelopt_post_restore to
ensure shapes/amax are consistent after load.
🧹 Nitpick comments (1)
tests/gpu_megatron/torch/quantization/plugins/test_megatron.py (1)

738-750: ⚡ Quick win

Keep these GPU assertions on-device.

tensor.item() and math.isfinite(...) force host syncs on every worker here. Prefer tensor comparisons/reductions plus torch.isfinite, and only materialize Python scalars for failure messages or rank-0 logging.

As per coding guidelines, "Performance note: avoid GPU syncs in hot paths/tests (don’t use tensor.item()/float(tensor)/min(tensor))—use tensor ops instead."

Also applies to: 800-813

🤖 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/gpu_megatron/torch/quantization/plugins/test_megatron.py` around lines
738 - 750, The test is forcing GPU->CPU syncs by calling .item() inside the
assert message and in the divergence check; change the assert to keep
comparisons on-device and only materialize scalars when needed: replace the
assert torch.allclose(te_amax.view(()), seq_max, ...) with an if-not pattern (if
not torch.allclose(...): raise
AssertionError(f"...{te_amax.item()}...{seq_max.item()}...")) so the .item()
calls only run on failure, and compute the per-expert divergence using tensor
ops (delta = seq_amaxes.max() - seq_amaxes.min(); if (delta > 1e-5):
saw_per_expert_divergence = True) while ensuring you only call .item() when you
must convert to a Python bool for logging or failure paths; update references to
seq_amaxes, seq_mlp.local_experts, linear_name, te_amax, and
saw_per_expert_divergence accordingly.
🤖 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.

Inline comments:
In `@tests/gpu_megatron/torch/quantization/plugins/test_megatron.py`:
- Around line 698-761: The default-path helper
_test_te_grouped_vs_sequential_default_amax_helper must explicitly disable the
feature flag: set os.environ["MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER"]="0"
(saving the original and restoring it after the test) before calling
initialize_for_megatron so the shared-quantizer behavior is forced; then add a
new test variant that sets
os.environ["MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER"]="1" and runs the same
checks to cover the env-on path. Apply the same explicit env-off/restore and
corresponding env-on test change to the other similar helper/test pair
exercising TEGrouped vs Sequential (the companion helper referenced in the
review) so both default and env-enabled code paths have dedicated coverage.

---

Outside diff comments:
In `@modelopt/torch/quantization/plugins/transformer_engine.py`:
- Around line 147-166: The per-expert weight-quantizer path
(_per_expert_weight_quantizer and the created modules weight_quantizer_{i}) is
unsafe on TP/EP topology change because modelopt_post_restore currently only
runs the base path and does not reinitialize per-expert _amax buffers; update
modelopt_post_restore to detect topology/shape mismatch and either (a) fail-fast
by raising a clear error when _per_expert_weight_quantizer is enabled and the
restored per-expert _amax shapes don't match the current weight shapes, or (b)
perform a per-expert recalibration: iterate over each created module name
weight_quantizer_{i} (use self.num_gemms to enumerate), reset/recreate their
_amax buffers to the correct shape for the current self.weight{i} and call the
quantizer's max_calibrate (or its equivalent reinit routine) so each per-expert
quantizer is re-calibrated after restore; implement one of these branches in
modelopt_post_restore to ensure shapes/amax are consistent after load.

---

Nitpick comments:
In `@tests/gpu_megatron/torch/quantization/plugins/test_megatron.py`:
- Around line 738-750: The test is forcing GPU->CPU syncs by calling .item()
inside the assert message and in the divergence check; change the assert to keep
comparisons on-device and only materialize scalars when needed: replace the
assert torch.allclose(te_amax.view(()), seq_max, ...) with an if-not pattern (if
not torch.allclose(...): raise
AssertionError(f"...{te_amax.item()}...{seq_max.item()}...")) so the .item()
calls only run on failure, and compute the per-expert divergence using tensor
ops (delta = seq_amaxes.max() - seq_amaxes.min(); if (delta > 1e-5):
saw_per_expert_divergence = True) while ensuring you only call .item() when you
must convert to a Python bool for logging or failure paths; update references to
seq_amaxes, seq_mlp.local_experts, linear_name, te_amax, and
saw_per_expert_divergence accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5fbda55f-b0c7-4054-bc00-63b4bc3a150e

📥 Commits

Reviewing files that changed from the base of the PR and between b49f9b9 and 4a58f02.

📒 Files selected for processing (2)
  • modelopt/torch/quantization/plugins/transformer_engine.py
  • tests/gpu_megatron/torch/quantization/plugins/test_megatron.py

Comment on lines +698 to +761
def _test_te_grouped_vs_sequential_default_amax_helper(tp_size, ep_size, quant_cfg, rank, size):
"""TEGrouped per-linear amax should equal max-over-Sequential-experts under default sync=False."""
initialize_for_megatron(
tensor_model_parallel_size=tp_size,
expert_model_parallel_size=ep_size,
seed=SEED,
)

te_grouped = _gpt_model_provider(
tp_size=tp_size, ep_size=ep_size, hidden_size=32, moe_grouped_gemm=True,
transformer_impl="transformer_engine", num_moe_experts=4,
)
forward = get_forward(te_grouped, batch_size=8)

sequential = _gpt_model_provider(
tp_size=tp_size, ep_size=ep_size, hidden_size=32, moe_grouped_gemm=False,
num_moe_experts=4, transformer_impl="modelopt",
)
copy_weights_from_grouped_to_non_grouped(te_grouped, sequential)

for module in te_grouped.modules():
if isinstance(module, TopKRouter):
module.topk = module.num_experts
for module in sequential.modules():
if isinstance(module, TopKRouter):
module.topk = module.num_experts

mtq.quantize(te_grouped, quant_cfg, forward)
mtq.quantize(sequential, quant_cfg, forward)

te_modules = [m for m in te_grouped.modules() if isinstance(m, TEGroupedMLP)]
seq_modules = [m for m in sequential.modules() if isinstance(m, SequentialMLP)]
assert len(te_modules) == len(seq_modules)

saw_per_expert_divergence = False
for te_mlp, seq_mlp in zip(te_modules, seq_modules):
for linear_name in ("linear_fc1", "linear_fc2"):
te_amax = getattr(te_mlp, linear_name).weight_quantizer.amax
assert te_amax is not None and te_amax.numel() == 1

seq_amaxes = torch.stack([
getattr(expert, linear_name).weight_quantizer.amax.view(())
for expert in seq_mlp.local_experts
])
seq_max = seq_amaxes.max()

assert torch.allclose(te_amax.view(()), seq_max, atol=1e-5, rtol=1e-5), (
f"TEGrouped per-linear amax ({te_amax.item()}) != "
f"max-over-Sequential-experts ({seq_max.item()}) for {linear_name}"
)

if (seq_amaxes.max() - seq_amaxes.min()).item() > 1e-5:
saw_per_expert_divergence = True

assert saw_per_expert_divergence, (
"Expected per-expert weight amax to diverge across SequentialMLP experts."
)


@pytest.mark.parametrize("quant_cfg", [mtq.FP8_DEFAULT_CFG, mtq.NVFP4_DEFAULT_CFG])
def test_te_grouped_vs_sequential_default_amax(dist_workers_size_4, quant_cfg):
dist_workers_size_4.run(
partial(_test_te_grouped_vs_sequential_default_amax_helper, 1, 2, quant_cfg)
)

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make these “default” tests control the feature flag explicitly.

These helpers never clear or set MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER, so a worker process with that flag exported will exercise the new per-expert path instead of the default shared-quantizer path. That also means the env-enabled branch added in this PR still has no dedicated coverage. Please force the flag off in these default-behavior tests and add a separate env-on case for the new path.

Also applies to: 764-820

🤖 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/gpu_megatron/torch/quantization/plugins/test_megatron.py` around lines
698 - 761, The default-path helper
_test_te_grouped_vs_sequential_default_amax_helper must explicitly disable the
feature flag: set os.environ["MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER"]="0"
(saving the original and restoring it after the test) before calling
initialize_for_megatron so the shared-quantizer behavior is forced; then add a
new test variant that sets
os.environ["MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER"]="1" and runs the same
checks to cover the env-on path. Apply the same explicit env-off/restore and
corresponding env-on test change to the other similar helper/test pair
exercising TEGrouped vs Sequential (the companion helper referenced in the
review) so both default and env-enabled code paths have dedicated coverage.

@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.03704% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.57%. Comparing base (9038b71) to head (208b64c).

Files with missing lines Patch % Lines
.../torch/quantization/nn/modules/tensor_quantizer.py 30.76% 18 Missing ⚠️
...t/torch/quantization/plugins/transformer_engine.py 11.11% 16 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1550      +/-   ##
==========================================
+ Coverage   70.21%   74.57%   +4.35%     
==========================================
  Files         515      515              
  Lines       57244    57284      +40     
==========================================
+ Hits        40196    42721    +2525     
+ Misses      17048    14563    -2485     
Flag Coverage Δ
examples 42.94% <33.33%> (+10.07%) ⬆️
gpu 49.43% <29.62%> (-0.62%) ⬇️
regression 14.83% <22.22%> (+0.06%) ⬆️

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.

from modelopt.torch.quantization.utils import replace_function

from ..nn import QuantModuleRegistry
from ..nn import QuantModuleRegistry, SequentialQuantizer

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.

Should we create a new quantizer class say GroupedQuantizer which is ` Pytorch nn.ModuleList instead of [nn.Sequantial]?

Sequential container assumes it will be a called as seq(x) which does seq[1](seq[0](x)).

ModuleList simply groups quantizers. but does not define an inherent forward path.

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.

this implementation doesn't use SequentialQuantizer for the expert quantizers, it simply checks in row 174 if isinstance(wq_i, SequentialQuantizer) to decide how to call the quantizer.

We can add a GroupedQuantizer class if that would make things easier, but the current implementation doesn't really need it IMO

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.

I added a GroupedQuantizer class and changed the TEGroupedMLP implementation to use it

Comment thread modelopt/torch/quantization/plugins/transformer_engine.py Outdated

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.

Design looks good to me overall, left a comment here - https://github.com/NVIDIA/Model-Optimizer/pull/1550/changes#r3468773327

@jenchen13 jenchen13 requested a review from a team as a code owner June 30, 2026 20:34

@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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🧹 Nitpick comments (1)
modelopt/torch/quantization/plugins/transformer_engine.py (1)

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

Move this import to module scope or document why it must stay local.

The new function-local import has no circular/optional/heavy-import justification. As per coding guidelines, “Keep imports at the top of the file; place imports inside functions only when necessary … with explicit justification.”

Proposed fix
+from modelopt.torch.quantization.model_calib import max_calibrate
-            from modelopt.torch.quantization.model_calib import max_calibrate
-
             for i in range(self.num_gemms):
🤖 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 `@modelopt/torch/quantization/plugins/transformer_engine.py` at line 167, The
new function-local import of max_calibrate in transformer_engine.py should not
stay inside the function unless there is a documented need such as a circular
dependency or optional/heavy import. Move the import to module scope near the
other top-level imports, or add a clear in-code justification at the exact
import site if it must remain local. Use the surrounding transformer_engine
plugin code and the max_calibrate reference to locate and update the import
placement consistently.

Source: Coding guidelines

🤖 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.

Inline comments:
In `@modelopt/torch/quantization/plugins/transformer_engine.py`:
- Around line 147-150: Persist the per-expert quantizer mode alongside the
checkpoint metadata in transformer_engine.py so restore_quantizer_state() sees
the same module topology on load. Update the initialization path around
_per_expert_weight_quantizer_enabled() and add_module(...) to record the
MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER setting in the saved config or infer it
from restored state before quantizer modules are created, ensuring the number of
weight_quantizer_* modules matches at save and restore time.

---

Nitpick comments:
In `@modelopt/torch/quantization/plugins/transformer_engine.py`:
- Line 167: The new function-local import of max_calibrate in
transformer_engine.py should not stay inside the function unless there is a
documented need such as a circular dependency or optional/heavy import. Move the
import to module scope near the other top-level imports, or add a clear in-code
justification at the exact import site if it must remain local. Use the
surrounding transformer_engine plugin code and the max_calibrate reference to
locate and update the import placement consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d8a388b2-8131-43ce-84b3-1deafe9b0934

📥 Commits

Reviewing files that changed from the base of the PR and between b1e32d9 and 308d8ee.

📒 Files selected for processing (1)
  • modelopt/torch/quantization/plugins/transformer_engine.py

Comment on lines +147 to +150
self._per_expert_weight_quantizer = _per_expert_weight_quantizer_enabled()
if self._per_expert_weight_quantizer:
for i in range(self.num_gemms):
self.add_module(f"weight_quantizer_{i}", TensorQuantizer())

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file with line numbers and the surrounding restore/setup logic.
FILE="modelopt/torch/quantization/plugins/transformer_engine.py"

echo "== file length =="
wc -l "$FILE"

echo
echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== relevant slices =="
sed -n '1,260p' "$FILE" | cat -n

echo
echo "== search for related restore/load paths =="
rg -n "modelopt_post_restore|load_state|state_dict|load\(|amax|per_expert|weight_quantizer_|_per_expert_weight_quantizer_enabled|max_calibrate" modelopt/torch/quantization/plugins/transformer_engine.py modelopt/torch/quantization -S

Repository: NVIDIA/Model-Optimizer

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="modelopt/torch/quantization/plugins/transformer_engine.py"

# Read only the relevant ranges around the env-gated modules and the local import.
sed -n '120,240p' "$FILE" | cat -n

Repository: NVIDIA/Model-Optimizer

Length of output: 6927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("modelopt/torch/quantization/plugins/transformer_engine.py")
text = p.read_text()
for needle in [
    "def modelopt_post_restore",
    "max_calibrate",
    "_per_expert_weight_quantizer_enabled",
    "add_module(f\"weight_quantizer_",
]:
    idx = text.find(needle)
    print(f"\n## {needle} -> {idx}")
    if idx != -1:
        start = max(0, text.rfind("\n", 0, idx-400))
        end = min(len(text), text.find("\n", idx+400))
        print(text[start:end])
PY

Repository: NVIDIA/Model-Optimizer

Length of output: 3922


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the quantizer restore/load path and the base class used here.
sed -n '90,170p' modelopt/torch/quantization/conversion.py | cat -n
printf '\n====\n'
sed -n '1,240p' modelopt/torch/quantization/plugins/custom.py | cat -n
printf '\n====\n'
rg -n "def quantizer_state|class _ParallelLinear|modelopt_post_restore|load_state_dict|ApplyModeError" modelopt/torch/quantization -S

Repository: NVIDIA/Model-Optimizer

Length of output: 18270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' modelopt/torch/quantization/model_calib.py | cat -n
printf '\n====\n'
sed -n '1,120p' modelopt/torch/quantization/plugins/vllm.py | cat -n

Repository: NVIDIA/Model-Optimizer

Length of output: 10780


Persist the per-expert quantizer mode in checkpoint metadata. restore_quantizer_state() rejects unmatched/extra quantizer keys, so changing MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER between save and restore makes the same checkpoint fail to load. Store the mode in the saved config/metadata or derive it from the restored state before add_module(...) runs so the module topology matches.

🤖 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 `@modelopt/torch/quantization/plugins/transformer_engine.py` around lines 147 -
150, Persist the per-expert quantizer mode alongside the checkpoint metadata in
transformer_engine.py so restore_quantizer_state() sees the same module topology
on load. Update the initialization path around
_per_expert_weight_quantizer_enabled() and add_module(...) to record the
MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER setting in the saved config or infer it
from restored state before quantizer modules are created, ensuring the number of
weight_quantizer_* modules matches at save and restore time.

Source: Coding guidelines

jenchen13 and others added 5 commits June 30, 2026 13:46
…nfra fixes

modelopt/torch/quantization/plugins/transformer_engine.py:
  MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER=1 opts into per-gemm
  weight_quantizer_0..N-1 inside _QuantTEGroupedLinear (deepcopied from
  the shared weight_quantizer). Lets TEGroupedMLP recover per-expert
  amax granularity, matching SequentialMLP's default behavior.

modelopt/torch/distill/plugins/megatron.py:
  LogitsKLLoss.forward prints student/teacher logit stats (mean/std/
  min/max/shape) on rank 0 each call. Diagnostic for the QAD loss-spike
  investigation — confirms which spec produces which logits without
  changing the KL math.

tests/gpu_megatron/torch/quantization/plugins/test_megatron.py:
  New test_te_grouped_vs_sequential_default_amax + ..._default_loss
  cover the structural amax asymmetry between TEGroupedMLP and
  SequentialMLP (TEGrouped per-linear amax = max-over-Sequential-experts
  amax) and a finiteness sanity check on the resulting quant error.

tools/launcher/common/service_utils.sh:
  - Fall back to SLURM_PROCID / SLURM_LOCALID when PMIX_*/OMPI_* are
    unset, so `[[ "$mpi_local_rank" -eq 0 ]]` doesn't silently pass on
    every rank under plain srun.
  - util_install_extra_dep: per-node marker so concurrent ranks wait
    for rank 0 to finish installing (concurrent pip on a shared FS
    leaves a broken state); also installs nvidia-resiliency-ext.

Signed-off-by: Jennifer Chen <jennifchen@nvidia.com>
Signed-off-by: Jennifer Chen <jennifchen@nvidia.com>
- transformer_engine.py: dedup `import copy`/`import os` left over from the
  rebase, sort the four imports alphabetically.
- transformer_engine.py: comment near the per-expert weight_quantizer setup
  explaining that base modelopt_post_restore won't re-calibrate the
  weight_quantizer_{i} modules, so save/restore is only safe when TP/EP is
  unchanged. Per-channel _amax shape depends on the TP-sliced output dim.
- service_utils.sh: drop the duplicated mpi_rank / mpi_local_rank
  re-assignments — main already carries the SLURM fallback, the extra two
  lines were leftover rebase noise.

Signed-off-by: Jennifer Chen <jennifchen@nvidia.com>
Signed-off-by: Jennifer Chen <jennifchen@nvidia.com>
Co-authored-by: realAsma <86726418+realAsma@users.noreply.github.com>
Signed-off-by: Jenny Chen <jc4686@columbia.edu>
self._per_expert_weight_quantizer = _per_expert_weight_quantizer_enabled()
if self._per_expert_weight_quantizer:
for i in range(self.num_gemms):
self.add_module(f"weight_quantizer_{i}", TensorQuantizer())

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.

@realAsma previously I did a copy.deepcopy(self.weight_quantizer) but you suggested to remove it .. how does this new quantizer inherit the quant format now?

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.

We just setup and initialize the tensor quantizers here. (called here

replace_quant_module(model, version=ModeloptStateManager(model).state_version)
)

The actual config will be loaded here -

set_quantizer_by_cfg(model, config.get("quant_cfg", []))

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.

BB: Can you add more context if needed here?

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.

🤖 Bot comment.

Prepared a one-line context comment here explaining that the per-GEMM quantizers keep independent calibration state for each grouped weight.

Validation: git diff --check passed. I did not commit or push because this comment did not explicitly approve publishing; ready to push once approved.

jenchen13 and others added 2 commits July 1, 2026 11:18
Fused TEGroupedLinear experts previously shared a single weight quantizer.
Add GroupedQuantizer (an nn.ModuleList of TensorQuantizers) and have
_QuantTEGroupedLinear build one quantizer per expert in _setup, so each
fused expert gets an independent weight amax by default, matching
SequentialMLP. This replaces the MODELOPT_TEGROUPED_PER_EXPERT_QUANTIZER
env-var gate.

Storing the quantizers in a ModuleList named weight_quantizer surfaces them
as weight_quantizer.{i}, which the fused-experts name normalizer maps to
*weight_quantizer so the stock quant configs reach every expert. Teach the
calibration/sync paths (model_calib DP/EP/TP sync, megatron NVFP4-static TP
check, custom restore probe) to recurse into the container so per-expert
amaxes sync correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jennifer Chen <jennifchen@nvidia.com>
Update test_te_grouped_vs_sequential_default_amax to assert each fused
expert's weight amax matches its SequentialMLP counterpart (and that they
diverge across experts) now that TEGroupedMLP quantizes per-expert by
default, instead of comparing a single shared amax. Drop the
sync_expert_weight_amax=True override in test_te_grouped_vs_sequential_quantize
so both models quantize per-expert and produce identical outputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jennifer Chen <jennifchen@nvidia.com>
@jenchen13 jenchen13 changed the title WIP Support per expert amax in TEGroupedMLP Support per expert weight quantizer in TEGroupedMLP Jul 1, 2026
@jenchen13 jenchen13 force-pushed the jennifchen/te_per_expert branch from 308d8ee to 096d430 Compare July 1, 2026 18:34
jenchen13 and others added 2 commits July 2, 2026 12:35
Signed-off-by: Jenny Chen <jennifchen@nvidia.com>
Signed-off-by: Hung-Yueh Chiang <hungyuehc@nvidia.com>
@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.

@jenchen13 jenchen13 requested a review from realAsma July 9, 2026 13:26
@jenchen13

Copy link
Copy Markdown
Contributor Author

/claude review

@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 — Per-expert weight quantizer in TEGroupedMLP

Reviewed all 6 changed modelopt/ source files plus the two touched test files (9 files total). The core design — a GroupedQuantizer ModuleList holding one TensorQuantizer per fused expert, threaded through calibration/sync/config-matching — is coherent, and the model_calib.py recursion plus the custom.py/megatron.py isinstance updates are consistent. The opt-in torch.compile loop is well-gated (eager default, calibration excluded from capture). I found one blocking correctness bug in the restore path and one backward-compat concern.

Findings by severity: CRITICAL: 1, IMPORTANT: 1, SUGGESTION: 1

Most impactful:

[CRITICAL ModeState] Restore drops _amax for all experts except expert 0 (transformer_engine.py modelopt_post_restore). The base super().modelopt_post_restore() calls weight_quantizer.reset_amax(), which — because reset_amax is a GroupedQuantizer delegated method — wipes _amax on EVERY member, then recalibrates only member 0 (the forward->self[0] representative). The new per-expert loop is meant to recover the rest, but its if not hasattr(q, "_amax"): continue guard runs AFTER that wipe, so it skips exactly the experts that lost their _amax. Result: after restore(), experts 1..N-1 of every TEGroupedMLP have amax is None -> broken static quantization. Snapshot which members had loaded _amax BEFORE calling super() (which also deletes self.weight). The toy tests only cover quantize, not save->restore with num_gemms > 1, so this is not caught.

[IMPORTANT Compatibility] quantizer-state key schema change without migration (megatron.py _process_quantizer_amax). The single weight_quantizer._amax key becomes per-member weight_quantizer.{i}._amax. Existing pre-PR TEGroupedMLP checkpoints then fail restore — ApplyModeError (unmatched keys) on the non-sharded path, KeyError on the Megatron torch-dist path. Needs a migration shim or a state-version bump.

The SUGGESTION notes GroupedQuantizer.get_modelopt_state()/is_grouped_quantizer is currently dead code (no serializer collects the container, no restore code reads the flag).

The PR description already flags the TP/EP restore TODO — the CRITICAL finding shows the restore issue is broader (it affects same-topology restore for num_gemms > 1 too). Risk: high for anyone saving/restoring per-expert-quantized TEGroupedMLP checkpoints. Recommend addressing the restore ordering bug before merge.

continue
wq_i = self.weight_quantizer[i]
q = wq_i[0] if isinstance(wq_i, SequentialQuantizer) else wq_i
if not hasattr(q, "_amax"):

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] Per-expert restore loses _amax for every expert except expert 0.

What happens: super().modelopt_post_restore() (custom.py:177-181) runs before this loop and executes:

self.weight_quantizer.reset_amax()   # GroupedQuantizer delegated method -> broadcasts to ALL members
max_calibrate(self.weight_quantizer, lambda wq: wq(self.weight), distributed_sync=False)

reset_amax() is in GroupedQuantizer._delegated_methods, so it deletes _amax on every member. But the max_calibrate forward loop calls self.weight_quantizer(self.weight), which routes through GroupedQuantizer.forwardself[0](...), so only member 0 gets recalibrated. Members 1..num_gemms-1 are left with no _amax.

This loop is then supposed to recalibrate the rest, but the guard if not hasattr(q, "_amax"): continue sees exactly those members with the _amax that super() just deleted, and skips them.

Impact: After restore() (both the non-sharded restore_quantizer_state path and the Megatron torch-dist set_extra_state path, which both call modelopt_post_restore), experts 1..N-1 of every TEGroupedMLP have weight_quantizer[i].amax == None. For a static INT8/FP8/NVFP4 config that means broken/incorrect quantization on all but the first expert — restore is not functionally identical to the saved model. num_gemms == 1 happens to work, which is why the toy tests (which cover quantize, not save→restore) don't catch it.

Fix: determine which members had saved _amax before calling super(), e.g.:

had_amax = []
for i in range(self.num_gemms):
    wq_i = self.weight_quantizer[i]
    q = wq_i[0] if isinstance(wq_i, SequentialQuantizer) else wq_i
    had_amax.append(hasattr(q, "_amax"))

super().modelopt_post_restore(prefix=prefix)
delattr(self, "weight")

from modelopt.torch.quantization.model_calib import max_calibrate
for i in range(self.num_gemms):
    weight_i = getattr(self, f"weight{i}", None)
    if weight_i is None or not had_amax[i]:
        continue
    wq_i = self.weight_quantizer[i]
    wq_i.reset_amax()
    max_calibrate(wq_i, lambda wq, w=weight_i: wq(w), distributed_sync=False)

(Note super() deletes self.weight, so snapshotting must happen first anyway.) Please also add a save→restore test covering num_gemms > 1 — the current tests exercise quantize only.

if re.match(r"weight_quantizer\.\d+\..+_amax$", k):
quantizer_state_dict[k] = v
else:
quantizer_state_dict[k] = v.view(-1) if v.numel() == 1 else v

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 change alters the modelopt_state/quantizer-state key schema for TEGroupedLinear with no migration path.

Before: a single weight_quantizer (TensorQuantizer) → one key ...weight_quantizer._amax.
After: a GroupedQuantizer (ModuleList) → per-member keys ...weight_quantizer.0._amax, ...weight_quantizer.1._amax, …

quantizer_state() (conversion.py:202-208) only collects the leaf TensorQuantizer/SequentialQuantizer members, so a checkpoint saved before this PR has key ...weight_quantizer while the restored model now exposes ...weight_quantizer.0/.1/.... On restore:

  • Non-sharded path: restore_quantizer_state computes unmatched_keys = state.keys() - model.keys() and raises ApplyModeError on the mismatch (conversion.py:144-148).
  • Megatron torch-dist path: quant_module_set_extra_state does quantizer_state[name] for each weight_quantizer.i and hits a KeyError.

Either way, existing quantized TEGroupedMLP checkpoints become unloadable after this change. Please add a migration/back-compat shim (e.g. map a legacy scalar weight_quantizer._amax onto all weight_quantizer.{i} members during restore) or a state-version bump with an explicit remap, and document the break in the CHANGELOG if intentional.


def get_modelopt_state(self) -> dict[str, Any]:
"""Get meta state to be saved in checkpoint."""
return {"num_quantizers": len(self), "is_grouped_quantizer": True}

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] GroupedQuantizer.get_modelopt_state() and its is_grouped_quantizer flag appear to be dead code. quantizer_state() (conversion.py:202-208) and quant_module_get_extra_state() (megatron.py:145-148) only collect TensorQuantizer/SequentialQuantizer instances — a GroupedQuantizer itself is never serialized, only its leaf members are — and no restore-side code reads is_grouped_quantizer (unlike is_sequential_quantizer, which conversion.py:559 branches on). Either wire this into save/restore (which would also give you a natural home for the legacy-checkpoint migration noted in the megatron comment) or drop the method to avoid implying a round-trip that doesn't exist.

@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 (2)
modelopt/torch/quantization/model_calib.py (1)

356-357: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the type annotation to include GroupedQuantizer.

The function now handles GroupedQuantizer at line 364, but the type hint at line 357 still reads TensorQuantizer | SequentialQuantizer.

♻️ Proposed fix
 def sync_quantizer_amax_across_tp(
-    quantizer: TensorQuantizer | SequentialQuantizer,
+    quantizer: TensorQuantizer | SequentialQuantizer | GroupedQuantizer,
     linear_name: str,
🤖 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 `@modelopt/torch/quantization/model_calib.py` around lines 356 - 357, The type
annotation for sync_quantizer_amax_across_tp is outdated and should include
GroupedQuantizer now that the function handles it. Update the quantizer
parameter type hint alongside TensorQuantizer and SequentialQuantizer so the
signature matches the logic in sync_quantizer_across_tp and the GroupedQuantizer
branch.
modelopt/torch/quantization/plugins/transformer_engine.py (1)

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

Add a comment explaining the in-function import.

Per coding guidelines, imports inside functions require a brief comment naming the reason (e.g., circular import). This likely avoids a circular dependency between plugins/transformer_engine.py and model_calib.py, but the reason should be documented.

♻️ Proposed fix
-        from modelopt.torch.quantization.model_calib import max_calibrate
+        # Import inside function to avoid circular import: model_calib imports from the
+        # quantization package which loads this plugins module.
+        from modelopt.torch.quantization.model_calib import max_calibrate

As per coding guidelines, "Put an import inside a function only when there is a concrete reason: resolving a circular import that cannot be restructured... Add a brief comment in those cases naming the 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 `@modelopt/torch/quantization/plugins/transformer_engine.py` at line 187, The
in-function import of max_calibrate in transformer_engine.py needs a brief
explanatory comment per the import guidelines. Update the function containing
this import to note the concrete reason for keeping it local, likely the
circular dependency between transformer_engine and model_calib, so readers
understand why it cannot be moved to the module top level.

Source: Coding guidelines

🤖 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 `@modelopt/torch/quantization/model_calib.py`:
- Around line 356-357: The type annotation for sync_quantizer_amax_across_tp is
outdated and should include GroupedQuantizer now that the function handles it.
Update the quantizer parameter type hint alongside TensorQuantizer and
SequentialQuantizer so the signature matches the logic in
sync_quantizer_across_tp and the GroupedQuantizer branch.

In `@modelopt/torch/quantization/plugins/transformer_engine.py`:
- Line 187: The in-function import of max_calibrate in transformer_engine.py
needs a brief explanatory comment per the import guidelines. Update the function
containing this import to note the concrete reason for keeping it local, likely
the circular dependency between transformer_engine and model_calib, so readers
understand why it cannot be moved to the module top level.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9e78ab5d-dfba-434b-90ba-e77b357e483c

📥 Commits

Reviewing files that changed from the base of the PR and between 16c2f0b and 8f9a4db.

📒 Files selected for processing (6)
  • CHANGELOG.rst
  • modelopt/torch/quantization/config.py
  • modelopt/torch/quantization/model_calib.py
  • modelopt/torch/quantization/nn/modules/tensor_quantizer.py
  • modelopt/torch/quantization/plugins/transformer_engine.py
  • tests/gpu_megatron/torch/quantization/plugins/test_megatron.py
✅ Files skipped from review due to trivial changes (2)
  • modelopt/torch/quantization/config.py
  • CHANGELOG.rst

setattr(parent_module, name, sequential_quantizer)


class GroupedQuantizer(nn.ModuleList):

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.

nit: SequentialQuantizer and GroupedQuantizer share sever overlapping pieces (such delegation, attribute setter etc). Can we create a shared base-class which implements these overlapping pieces and subclass SequentialQuantizer and GroupedQuantizer from it to avoid code level redundancy?

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.

RB: Can you create a plan on how to do this and share in this thread?

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.

🤖 Bot comment.

🐝 I’m reviewing the overlapping behavior and will share a concrete refactoring plan here shortly.

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.

🤖 Bot comment.

Plan:

  1. Add a private _QuantizerContainerBase mixin before the two containers. It will own the common representative-property reads, property writes broadcast to every member, delegated lifecycle-method calls, attribute-config fan-out, and the common get_modelopt_state() shape (using a subclass-provided state-key).
  2. Make SequentialQuantizer inherit from the mixin plus nn.Sequential, and GroupedQuantizer from the mixin plus nn.ModuleList. Keep PyTorch container initialization in each subclass so module registration/state-dict keys remain unchanged.
  3. Keep only semantic differences in the subclasses: accepted member types, sequential chaining vs. grouped representative forward, disable_rotate delegation for sequential only, delegated-method return behavior, and convert_to_single_quantizer for sequential only.
  4. Add parameterized CPU tests for the shared contract (representative reads, broadcast writes/methods, scalar/list config application, and exact modelopt-state output), plus regression tests that sequential chaining, grouped first-member forward, nested SequentialQuantizer members, and checkpoint/state-dict layout are unchanged.

This keeps the refactor local to tensor_quantizer.py and its focused tests; downstream (SequentialQuantizer, GroupedQuantizer) handling does not need to change.

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.

@jenchen13 could you take a look at this comment?

# scalar, channel, or block shape instead of flattening them through the legacy
# single-quantizer path.
if re.match(r"weight_quantizer\.\d+\..+_amax$", k):
quantizer_state_dict[k] = v

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.

Should we do

Suggested change
quantizer_state_dict[k] = v
quantizer_state_dict[k] = v.view(-1) if v.numel() == 1 else v

# GroupedQuantizer (an nn.ModuleList) surfaces them as ``weight_quantizer.{i}``, which the
# fused-experts name normalizer maps to ``*weight_quantizer`` so the stock configs apply.
self.weight_quantizer = GroupedQuantizer(
*(copy.deepcopy(self.weight_quantizer) for _ in range(self.num_gemms))

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.

We dont need this deep copy here.

Suggested change
*(copy.deepcopy(self.weight_quantizer) for _ in range(self.num_gemms))
TensorQuantizer() for _ in range(self.num_gemms))

The TensorQuantizers are always initialized with default args; they are configured after _setup -> so we can safely initialize new TensorQuantizers here. deepcopy seems unwarranted to me.

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.

RB: Can you please explain concisely "The TensorQuantizers are always initialized with default args; they are configured after _setup -> so we can safely initialize new TensorQuantizers here. deepcopy seems unwarranted to me." with code pointers ?

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.

🤖 Bot comment.

🐝 I’m tracing the initialization and post-_setup configuration paths now; I’ll reply here shortly with exact code pointers.

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.

🤖 Bot comment.

Yes—the key is the conversion order:

  1. convert_to_quantized_model() first calls replace_quant_module(), then set_quantizer_by_cfg().
  2. Conversion invokes the module's _setup(). The base setup creates weight_quantizer with the standard 8-bit per-tensor descriptor (custom.py:113-117); that descriptor is just QuantizerAttributeConfig(num_bits=8) (tensor_quant.py:279), equivalent to TensorQuantizer()'s schema defaults.
  3. _QuantTEGroupedLinear._setup() builds the group immediately after that base setup (lines 147-156), still before user config is applied.
  4. Afterwards, set_quantizer_by_cfg() configures the registered weight_quantizer.0, .1, … children; the fused-expert name normalization makes them match stock *weight_quantizer rules (conversion.py:319-355).

So deepcopy is only cloning a pristine default quantizer (including an empty/default calibrator). Fresh TensorQuantizer() instances give each expert independent state and are configured identically in the next phase. deepcopy would matter only if setup had already attached non-default config/state, which this path does not.


def _iter_leaf_quantizers(quantizer):
if isinstance(quantizer, SequentialQuantizer):
if isinstance(quantizer, (SequentialQuantizer, GroupedQuantizer)):

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.

nit:
Create QuantizerContainerBase base class and use that here to avoid explicitly listing like this.

see https://github.com/NVIDIA/Model-Optimizer/pull/1550/changes#r3553373837

Comment thread modelopt/torch/quantization/plugins/transformer_engine.py
Comment thread tests/gpu_megatron/torch/quantization/plugins/test_megatron.py
if (stacked.max() - stacked.min()).item() > 1e-5:
saw_per_expert_divergence = True

assert saw_per_expert_divergence, (

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.

This is not a good test IMO. This can fail if magically all the expert weights were initialized the same.

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.

RB: Do you agree with my comment? Wdyt?

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.

🤖 Bot comment.

🐝 Looking into this now.

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.

🤖 Bot comment.

Yes, I agree with the underlying concern. The fixed seed makes this deterministic today, so it is not conventionally flaky, but saw_per_expert_divergence tests an incidental property of the fixture rather than the independence contract: a correct implementation would fail if the experts happened to produce equal amaxes. I would initialize each expert's weights to deliberately distinct known magnitudes, then assert the corresponding grouped/sequential amax values (and, if useful, that the quantizer objects are distinct). That makes divergence guaranteed by the test setup and the failure causal.

realAsma added a commit that referenced this pull request Jul 9, 2026
## Description

Clarify that mocks are appropriate for focused interface and wiring
coverage, but tests claiming backend or runtime behavior must execute
the real implementation. Document `torch.compile` as a concrete example:
call the real compiler, and wrap/delegate to it when call tracing is
needed.

This follows the testing guidance raised in #1550.

## Validation

- `git diff --check`
- `pre-commit run --files CONTRIBUTING.md`

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

## Summary by CodeRabbit

* **Documentation**
* Clarified test design guidance to encourage validating real behavior
end-to-end.
* Added advice to avoid substituting core runtime behavior with fakes
when tests should cover actual execution.
* Noted that wrapping or delegation is still appropriate when tests need
to observe calls or counts.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: realAsma <akuriparambi@nvidia.com>
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.

3 participants