Skip to content

Registry-based module dispatch for unified_export_hf.py#1939

Merged
sychen52 merged 3 commits into
NVIDIA:mainfrom
sychen52:export_dispatch
Jul 14, 2026
Merged

Registry-based module dispatch for unified_export_hf.py#1939
sychen52 merged 3 commits into
NVIDIA:mainfrom
sychen52:export_dispatch

Conversation

@sychen52

@sychen52 sychen52 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: ? refactor

Use registry-based module dispatch for unified_export_hf.py
export handlers do not replace classes, need structural matching, and preparation/export require independent ordering

Usage

same as before

Testing

old and new unittest

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?: ✅
  • 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

Additional Information

Summary by CodeRabbit

Summary

  • New Features
    • Added registry-driven export dispatch for Hugging Face quantized models, improving coverage across standard layers and multiple MoE/expert variants.
    • Introduced a shared export context to manage per-export state such as tied-weight handling.
  • Bug Fixes
    • Improved quantized/tied-weight export behavior to prevent duplicated or incorrectly packed weights.
    • Enhanced MoE expert “amax” preparation to support more expert structures.
  • Tests
    • Added unit tests for registry matching, precedence/replacement behavior, and quantized export paths.

Signed-off-by: Shiyang Chen <shiychen@nvidia.com>
@sychen52 sychen52 requested review from a team as code owners July 7, 2026 21:41
@sychen52 sychen52 requested a review from ChenhanYu July 7, 2026 21:41
@sychen52 sychen52 self-assigned this Jul 7, 2026
@sychen52 sychen52 requested a review from shengliangxu July 7, 2026 21:41
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c62cd045-08ff-4785-b30a-6266d3e9d6ef

📥 Commits

Reviewing files that changed from the base of the PR and between e87fddc and 4d3d689.

📒 Files selected for processing (1)
  • modelopt/torch/export/unified_export_hf.py

📝 Walkthrough

Walkthrough

This PR adds registry-based handler dispatch for unified Hugging Face export, introduces export-scoped context state, moves quantized and MoE handling into built-in handlers, and adds tests for registry semantics and export integration.

Changes

Unified HF export registry

Layer / File(s) Summary
Registry contracts and package exports
modelopt/torch/export/registry.py, modelopt/torch/export/__init__.py
Defines ExportContext, ExportHandler, ordered registry matching and registration semantics, singleton registries, and package-level wildcard exports.
Built-in handlers and export-flow wiring
modelopt/torch/export/hf_export_handlers.py, modelopt/torch/export/unified_export_hf.py
Adds registered handlers for quantized linears, embeddings, fused experts, BMM experts, and MoE preparation; unified export now creates an ExportContext and dispatches through the registries.
Registry and dispatch validation
tests/unit/torch/export/test_export_registry.py
Tests MRO and name matching, predicates, ordering, replacement, built-in dispatch coverage, quantization integration, FP8 export, and cache isolation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Flow as _process_quantized_modules
  participant Context as ExportContext
  participant Registry as ExportModuleRegistry
  participant Handler as hf_export_handlers

  Flow->>Context: create context with model, dtype, and caches
  loop for each sub_module
    Flow->>Registry: match sub_module
    Registry-->>Flow: matched handler or None
    Flow->>Handler: invoke with name, module, and context
  end
  Flow->>Registry: match experts container
  Registry-->>Flow: preparation handler or None
  Flow->>Handler: set expert input amax values
Loading

Suggested reviewers: chenhanyu, meenchen, h-guo18, shengliangxu

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.61% 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 clearly summarizes the main change: switching unified_export_hf.py to registry-based module dispatch.
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 new banned patterns appeared in the scoped modelopt/examples Python diff; the only # nosec was preexisting, and requirements.txt only adds permissive torchvision.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@sychen52 sychen52 requested review from h-guo18 and meenchen July 7, 2026 21:41

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review — DM the bot to share feedback.

Refactor of unified_export_hf.py's per-module export dispatch: replaces the if/elif chains in _process_quantized_modules and the MoE input-amax prepass in _export_transformers_checkpoint with a new ordered, first-match-wins ExportModuleRegistry (new registry.py: ExportContext, ModuleExporter, ExportModuleRegistry). +595/-158, 4 files, with a solid CPU unit-test suite (test_export_registry.py) covering MRO/name/predicate matching, precedence, prepend, re-registration idempotency, and a small FP8 ToyModel end-to-end export.

Design review (new subsystem/registry): the codebase already has a class-keyed registration-and-dispatch idiom in _DMRegistryCls/QuantModuleRegistry (modelopt/torch/opt/dynamic.py). The new module's docstring explicitly acknowledges this and justifies a different mechanism — quantization converts module classes in place, whereas export needs to emit weights without changing class, plus it needs predicate/structural matching (_has_fused_experts_quantizers, iterable catch-all) that the class-dict-based _DMRegistryCls doesn't offer. That rationale is reasonable, but it lives only in the source docstring — the PR body just says "refactor" and doesn't compare alternatives. Worth a maintainer confirming the second registry is warranted rather than extending the existing one.

Behavioral parity looks preserved on inspection: first-match ordering (_FusedExpertsExporter before _BmmExpertsExporter, _MoELinearExporter before _QuantLinearExporter"QuantLinear" is not a substring of "QuantMoELinear" so no double-export), the format-NONE guards and NotImplementedError-for-unknown-experts are retained, and the broad _IterableExpertsExporter catch-all has a no-op export() so matching nn.ModuleList/nn.Sequential during the walk is harmless. No CHANGELOG (internal refactor), though note the three new symbols do become public via the export package star-import.

No prompt-injection attempts in the untrusted PR fields.

Why nudge (not approve): (1) this rewrites a critical, model-specific export dispatch and the real DBRX/Llama4/GptOss/fused-experts/QuantMoELinear paths are GPU/large-model only — CPU tests validate wiring + a tiny FP8 export, not the full-size expert exports (the codebase's known testing gap); (2) the "why a second registry vs. extending QuantModuleRegistry/_DMRegistryCls" decision is only in the code docstring, not the PR body. A human with export/arch context should confirm behavioral parity on a real MoE export and sign off on the design.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.21805% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.31%. Comparing base (7b8da80) to head (4d3d689).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/export/hf_export_handlers.py 78.20% 17 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1939      +/-   ##
==========================================
+ Coverage   76.45%   77.31%   +0.85%     
==========================================
  Files         520      522       +2     
  Lines       58322    58396      +74     
==========================================
+ Hits        44589    45147     +558     
+ Misses      13733    13249     -484     
Flag Coverage Δ
examples 43.31% <71.42%> (+2.25%) ⬆️
gpu 57.98% <84.21%> (-0.62%) ⬇️
regression 15.08% <47.36%> (+0.15%) ⬆️
unit 55.42% <67.66%> (+0.04%) ⬆️

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.

return hasattr(module, f"{first_proj_attr}_weight_quantizers")


@ExportModuleRegistry.register("QuantMoELinear", predicate=lambda m: hasattr(m, "experts"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

how about moving this registration to separate files?

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.

Yes. @shengliangxu mentioned this as well during our infra meeting. I guess we may eventually move them to modellib. But does it make sense to keep it here for now? @h-guo18

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 moved them out to hf_export_handlers.py now.

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review — DM the bot to share feedback.

Re: operator question "is this a pure no-op refactor?" — It is mostly behavior-preserving, but I would not call it a pure no-op. It's a faithful mechanical translation of two if/elif dispatchers into an ordered first-match-wins ExportModuleRegistry, and I traced the main paths carefully — but there are a few genuine behavioral divergences (all appear benign today, but they rest on unverified assumptions and the affected paths are GPU/large-model-only, so a human should confirm on a real MoE export).

Behavioral parity trace:

  • _MoELinearExporter (QuantMoELinear+experts) reproduces the old amax-fill + continue (first-match stops dispatch). ✓
  • _QuantLinearExporter/_QuantEmbeddingExporter preserve the format-NONE guards and AssertionError wrapping. ✓ is_quantlinear("QuantMoELinear") is correctly False (no substring), so no double-export. ✓
  • Prepass NotImplementedError for unknown non-iterable experts is retained via match() is None. ✓

Not-quite-no-op divergences (why not "pure"):

  1. New _IterableExpertsExporter catch-all in _process_quantized_modules. The old whole-model walk had no iterable catch-all; now any nn.ModuleList/nn.Sequential matches it. Its export() is a no-op so it's harmless, but it is new dispatch behavior that didn't exist before.
  2. Registry ordering assumption for DBRX. Old _process_quantized_modules had no DBRX handling and ran the fused-experts branch (_has_fused_experts_quantizers) unconditionally; the new registry puts _DbrxExpertsExporter (no-op export) before _FusedExpertsExporter. Parity holds only because DBRX uses w1_linear/w2_linear/v1_linear (not <first_proj>_weight_quantizers), so _has_fused_experts_quantizers is False for it — true today, but the equivalence now silently depends on that.
  3. Prepass key change from quant class names (QuantGptOssExperts/QuantLlama4TextExperts) to base names (GptOssExperts/Llama4TextExperts) matched via MRO — equivalent in practice, different mechanism.

Testing: CPU suite (test_export_registry.py) covers MRO/name/predicate matching, precedence, prepend, re-registration, and a tiny FP8 ToyModel export — validates wiring, not the full-size DBRX/Llama4/GptOss/fused/QuantMoELinear exports (the codebase's known GPU-only gap). So the "pure no-op" claim is untested on exactly the paths where the ordering divergences above would matter.

Previous-review items:

  • 💬 The "why a 2nd registry vs extending QuantModuleRegistry/_DMRegistryCls" rationale is in the registry.py docstring (quantization swaps classes in place; export must emit weights without changing class + needs predicate/structural matching). Reasonable, but still only in the source docstring — the PR body just says "refactor." Worth a maintainer confirming the second registry over extending the existing one.
  • The inline comment "move this registration to separate files?" (unified_export_hf.py:788) is not addressed — the exporter classes still live inline in unified_export_hf.py. Minor/optional.

Licensing: new registry.py/test use the standard NVIDIA Apache-2.0 header (year 2026, matches canonical) — no licensing concern. No prompt-injection in untrusted fields.

Net: strong, well-tested refactor and very likely behavior-preserving, but not provably a pure no-op — the iterable catch-all and the DBRX-vs-fused registration ordering are new behaviors whose equivalence hinges on assumptions untested by the CPU suite. Recommend a human sign-off on a real MoE (DBRX/Llama4/GptOss) export before merging.

This way we can have different order and not catch all condition for either

Signed-off-by: Shiyang Chen <shiychen@nvidia.com>

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (gpt-5.6-sol) — DM the bot to share feedback.

The registry split resolves the prior dispatch concerns: preparation and export now have independent precedence, the iterable fallback exists only in preparation, DBRX can prepare via its type-specific handler while independently taking the fused export path, and the handler registrations have been moved out of unified_export_hf.py. The focused dispatch tests cover these ordering properties, and I found no new correctness bug. The new files also use the canonical 2026 NVIDIA header.

Remaining reasons for owner sign-off:

  • The CPU tests validate matching and a toy linear export, but still do not execute the actual DBRX/Llama4/GptOss/fused-experts/QuantMoELinear handlers. Given the operator's confirmation that current behavior is identical, this is not a code-level blocker, but the GPU/large-model paths remain the main unverified risk.
  • This introduces a second registry mechanism alongside QuantModuleRegistry/_DMRegistryCls. The source docstring now gives a reasonable rationale (export handlers do not replace classes, need structural matching, and preparation/export require independent ordering), but the PR body still does not record that design choice as required for a new registry subsystem. Please capture that short rationale in the PR description or have an owner explicitly sign off on it.

@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 (1)
modelopt/torch/export/registry.py (1)

92-124: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace the assert guard with an explicit exception.

assert keys or predicate is not None (Line 111) is the only guard preventing a handler from being registered with no keys and no predicate. If that happens, match()'s if keys and ... / if predicate is not None ... checks both short-circuit to False, so the handler unconditionally matches every module. assert statements are stripped under python -O, silently removing this protection. Bandit's B101 flags this pattern in non-test files, and the coding guideline disallows # nosec bypasses, so raising a real exception is required rather than relying on assert.

🔒️ Proposed fix
-        assert keys or predicate is not None, "register() requires at least one key or a predicate"
+        if not keys and predicate is None:
+            raise ValueError("register() requires at least one key or a predicate")

Based on coding guidelines: "Bandit security checks must pass without exceptions. # nosec comments are not allowed as a bypass for security checks."

🤖 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/export/registry.py` around lines 92 - 124, Replace the assert
guard in ExportModuleRegistry.register with an explicit exception when neither
keys nor predicate is provided, preserving the existing validation condition and
message semantics. Do not use an assert or add a nosec bypass.

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/export/registry.py`:
- Around line 92-124: Replace the assert guard in ExportModuleRegistry.register
with an explicit exception when neither keys nor predicate is provided,
preserving the existing validation condition and message semantics. Do not use
an assert or add a nosec bypass.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 4abd008a-e87b-47c8-bc2f-a279ce39fd69

📥 Commits

Reviewing files that changed from the base of the PR and between 40600f9 and e87fddc.

📒 Files selected for processing (4)
  • modelopt/torch/export/hf_export_handlers.py
  • modelopt/torch/export/registry.py
  • modelopt/torch/export/unified_export_hf.py
  • tests/unit/torch/export/test_export_registry.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/torch/export/test_export_registry.py
  • modelopt/torch/export/unified_export_hf.py

@sychen52 sychen52 enabled auto-merge (squash) July 14, 2026 02:15
@sychen52 sychen52 disabled auto-merge July 14, 2026 04:32
@sychen52 sychen52 enabled auto-merge (squash) July 14, 2026 04:40
@sychen52 sychen52 merged commit dca6ecd into NVIDIA:main Jul 14, 2026
58 of 61 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.

2 participants