Skip to content

[6421642][ONNX][Quantization] Fix NVFP4 exporter node ordering#1985

Open
gcunhase wants to merge 5 commits into
NVIDIA:mainfrom
gcunhase:dev/agent/6421642_nvfp4_toposort_export
Open

[6421642][ONNX][Quantization] Fix NVFP4 exporter node ordering#1985
gcunhase wants to merge 5 commits into
NVIDIA:mainfrom
gcunhase:dev/agent/6421642_nvfp4_toposort_export

Conversation

@gcunhase

@gcunhase gcunhase commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix

  • Adds a protobuf-only stable topological sort before NVFP4QuantExporter.post_process() returns.
  • Ensures newly inserted DequantizeLinear and Cast producer nodes appear before the Transpose / MatMul nodes that consume them without round-tripping BF16 metadata through ONNX GraphSurgeon.
  • Reuses Cast nodes per input and precision dtype so multiple NVFP4-lowered MatMuls sharing an activation do not create duplicate tensor producers.
  • Adds CPU Torch ONNX export regression coverage for NVFP4 static weight export and shared-activation Cast reuse.

Usage

# Internal NVFP4 export path:
# 1. Quantize a torch model with an NVFP4 config.
# 2. Export it to ONNX so torch emits TRT_FP4QDQ nodes.
# 3. Lower those nodes with NVFP4QuantExporter.process_model(...).

import onnx

from modelopt.onnx.export import NVFP4QuantExporter

onnx_model = onnx.load("model_with_trt_fp4qdq.onnx")
onnx_model = NVFP4QuantExporter.process_model(onnx_model)
onnx.checker.check_model(onnx_model)

Testing

  • Reproduced the issue before the fix on a small NVFP4 ONNX export graph and on main before this patch.
  • Verified the fixed exporter returns producer_after_consumer_edges=0, onnx.checker.check_model(converted) passes, and the repro exits with rc=0.
  • Ran python -m pytest tests/unit/torch/quantization/test_onnx_export_cpu.py::test_nvfp4_exported_onnx_is_topologically_sorted tests/unit/torch/quantization/test_onnx_export_cpu.py::test_nvfp4_shared_activation_reuses_cast -q.
  • Ran ruff format modelopt/onnx/export/nvfp4_exporter.py modelopt/onnx/utils.py tests/unit/torch/quantization/test_onnx_export_cpu.py and ruff check modelopt/onnx/export/nvfp4_exporter.py modelopt/onnx/utils.py tests/unit/torch/quantization/test_onnx_export_cpu.py.
  • Addressed GPU BF16 CI failure tests/gpu/torch/quantization/test_nvfp4_onnx_export.py::test_simple_linear[BFloat16] by avoiding an ONNX GraphSurgeon import/export round-trip in the fix path.

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

Summary by CodeRabbit

  • Bug Fixes

    • Improved NVFP4 ONNX exports by deterministically reordering the graph so tensor producers come before consumers.
    • Enhanced NVFP4 conversion to reuse shared activation casts, avoiding duplicate casts; cast output naming is now precision-dependent.
    • Added stricter validation for graph dependency structure (including cycle detection).
  • Tests

    • Added CPU unit tests covering topological ordering, removal of TRT_FP4QDQ nodes after conversion, ONNX validity checks, and cast reuse behavior.

Ensure NVFP4 ONNX post-processing returns graphs with producers before consumers after TRT_FP4QDQ lowering and Cast insertion.

Signed-off-by: Gwena Cunha <4861122+gcunhase@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 16, 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: 3c422ed7-5d6f-49fe-8d6f-b51760c7b402

📥 Commits

Reviewing files that changed from the base of the PR and between b32e19e and c09279b.

📒 Files selected for processing (3)
  • modelopt/onnx/export/nvfp4_exporter.py
  • modelopt/onnx/utils.py
  • tests/unit/torch/quantization/test_onnx_export_cpu.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • modelopt/onnx/utils.py

📝 Walkthrough

Walkthrough

NVFP4 ONNX post-processing now reuses matching input casts and deterministically topologically sorts graph nodes after FP4QDQ replacement and initializer cleanup. CPU tests cover cast reuse, FP4QDQ removal, and ONNX model validation.

Changes

NVFP4 ONNX Export

Layer / File(s) Summary
Shared activation cast reuse
modelopt/onnx/export/nvfp4_exporter.py, tests/unit/torch/quantization/test_onnx_export_cpu.py
Caches casts by input and precision, assigns BF16/F16-specific names, and verifies that shared activation paths reuse one Cast node.
Topological graph sorting and validation
modelopt/onnx/utils.py, modelopt/onnx/export/nvfp4_exporter.py, tests/unit/torch/quantization/test_onnx_export_cpu.py
Adds stable dependency-based node ordering with duplicate-producer, self-dependency, and cycle validation; integrates sorting into NVFP4 post-processing and validates converted ONNX output.

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

Suggested reviewers: ajrasane, galagam

🚥 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 Clearly summarizes the main change: fixing NVFP4 exporter node ordering.
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 Touched Python files contain no torch.load/np.load allow_pickle, trust_remote_code, eval/exec, # nosec, or new deps.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@gcunhase
gcunhase force-pushed the dev/agent/6421642_nvfp4_toposort_export branch from 303dda2 to 226f0f3 Compare July 16, 2026 21:09
Keep the NVFP4 ONNX exporter regression with the existing CPU Torch ONNX export coverage instead of a standalone test file.

Signed-off-by: Gwena Cunha <4861122+gcunhase@users.noreply.github.com>
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.96970% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 77.14%. Comparing base (21d0069) to head (b32e19e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/onnx/utils.py 96.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1985      +/-   ##
==========================================
+ Coverage   76.50%   77.14%   +0.63%     
==========================================
  Files         525      525              
  Lines       59257    59429     +172     
==========================================
+ Hits        45337    45847     +510     
+ Misses      13920    13582     -338     
Flag Coverage Δ
unit 55.02% <96.96%> (+0.05%) ⬆️

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.

@gcunhase
gcunhase force-pushed the dev/agent/6421642_nvfp4_toposort_export branch from 226f0f3 to d3f7199 Compare July 16, 2026 22:07
@gcunhase
gcunhase marked this pull request as ready for review July 16, 2026 22:08
@gcunhase
gcunhase requested review from a team as code owners July 16, 2026 22:08
@gcunhase
gcunhase requested a review from ajrasane July 16, 2026 22:08

@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 fix is straightforward and consistent with existing exporter patterns: GraphSurgeon topologically sorts the graph after the NVFP4 post-processing code appends producer nodes, and the new CPU regression test exercises the real Torch→ONNX→NVFP4 conversion path and validates the result with ONNX checker. Imports are correctly placed, the existing ONNX optional dependency already includes GraphSurgeon, and no licensing changes are introduced.

@gcunhase

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

Scope: The PR's authoritative changed-file list is 2 files — modelopt/onnx/export/nvfp4_exporter.py and tests/unit/torch/quantization/test_onnx_export_cpu.py. (Local base drift also surfaced trt_utils.py/test_plugin.py hunks, but those are not part of this PR and were excluded.) Both PR files were reviewed in full.

Findings — CRITICAL: 0, IMPORTANT: 0, SUGGESTION: 0

Assessment (low risk):

  • The fix appends a GraphSurgeon import_onnx → toposort() → export_onnx round-trip at the end of NVFP4QuantExporter.post_process(), correctly reordering the newly-inserted DequantizeLinear/Cast producer nodes ahead of the Transpose/MatMul consumers they feed. This matches the established pattern used by the sibling FP8/INT4/INT8 exporters and graph_utils, and onnx-graphsurgeon>=0.6.1 is already a declared dependency (no new import risk).
  • The sole caller (_deploy/utils/torch_onnx.py:477) reassigns process_model()'s return, so returning a fresh ModelProto from gs.export_onnx() is safe — nothing relies on in-place identity.
  • The new CPU regression test drives the real Torch→ONNX→NVFP4 path, asserts TRT_FP4QDQ is present pre-conversion and gone post-conversion, and validates with onnx.checker.check_model (which itself enforces topological ordering) — a real guard for this bug.

@gcunhase
gcunhase force-pushed the dev/agent/6421642_nvfp4_toposort_export branch 2 times, most recently from 60e28c2 to 3ccb0bd Compare July 17, 2026 01:13
@copy-pr-bot

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

@gcunhase
gcunhase force-pushed the dev/agent/6421642_nvfp4_toposort_export branch from 3ccb0bd to 7c448e1 Compare July 17, 2026 01:14
Use a protobuf-only graph node topological sort so NVFP4 post-processing preserves BF16 ONNX metadata while still returning producer-before-consumer node order.

Signed-off-by: Gwena Cunha <4861122+gcunhase@users.noreply.github.com>

@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 (2)
modelopt/onnx/export/nvfp4_exporter.py (2)

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

Assert the ordering contract directly in the regression test.

The supplied test checks FP4QDQ removal and onnx.checker.check_model, but does not explicitly verify that every in-graph producer precedes its consumer. Add that edge-order assertion, ideally with a branched graph, so this new sorting call cannot silently regress.

🤖 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/onnx/export/nvfp4_exporter.py` at line 467, Extend the regression
test covering the export flow around _topologically_sort_graph_nodes to assert
that every in-graph producer node appears before its consumer, preferably using
a branched graph. Keep the existing FP4QDQ-removal and onnx.checker.check_model
checks, and make the new assertion validate the sorted node order directly.

62-71: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use a heap for the ready queue.

pop(0) shifts the remaining list, and ready.sort() runs on every iteration. For wide ONNX graphs this can degrade toward quadratic/logarithmic behavior and make export unnecessarily slow. Use heapq keyed by the original node index to preserve stable ordering with O((V+E) log V) behavior.

Proposed change
+import heapq
+
-    ready = [node_index for node_index, dependency in enumerate(dependencies) if not dependency]
+    ready = [node_index for node_index, dependency in enumerate(dependencies) if not dependency]
+    heapq.heapify(ready)
     sorted_nodes: list[onnx.NodeProto] = []
     while ready:
-        node_index = ready.pop(0)
+        node_index = heapq.heappop(ready)
         sorted_nodes.append(nodes[node_index])
         for dependent_index in sorted(dependents[node_index]):
             dependencies[dependent_index].remove(node_index)
             if not dependencies[dependent_index]:
-                ready.append(dependent_index)
-        ready.sort()
+                heapq.heappush(ready, dependent_index)
🤖 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/onnx/export/nvfp4_exporter.py` around lines 62 - 71, Replace the
list-based ready queue in the topological sorting flow with a heapq min-heap
keyed by original node indices. Heapify the initial ready indices, use heap
push/pop operations when nodes become ready, and remove the per-iteration
sorting while preserving stable ascending node ordering and the existing
dependency processing.
🤖 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/onnx/export/nvfp4_exporter.py`:
- Line 467: Update _cast_input_dtypes() to cache and reuse Cast outputs keyed by
(input_name, precision_dtype), ensuring shared activations produce only one Cast
node and tensor. Preserve the existing <input>_f16 naming where applicable, and
ensure _topologically_sort_graph_nodes() receives a graph without duplicate
producers.

---

Nitpick comments:
In `@modelopt/onnx/export/nvfp4_exporter.py`:
- Line 467: Extend the regression test covering the export flow around
_topologically_sort_graph_nodes to assert that every in-graph producer node
appears before its consumer, preferably using a branched graph. Keep the
existing FP4QDQ-removal and onnx.checker.check_model checks, and make the new
assertion validate the sorted node order directly.
- Around line 62-71: Replace the list-based ready queue in the topological
sorting flow with a heapq min-heap keyed by original node indices. Heapify the
initial ready indices, use heap push/pop operations when nodes become ready, and
remove the per-iteration sorting while preserving stable ascending node ordering
and the existing dependency processing.
🪄 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: 5894027d-c666-4213-b109-316bdf9515e4

📥 Commits

Reviewing files that changed from the base of the PR and between d3f7199 and 7c448e1.

📒 Files selected for processing (1)
  • modelopt/onnx/export/nvfp4_exporter.py

Comment thread modelopt/onnx/export/nvfp4_exporter.py Outdated
@gcunhase gcunhase changed the title [6421642][ONNX][Quantization] Fix NVFP4 exporter node ordering Draft: [6421642][ONNX][Quantization] Fix NVFP4 exporter node ordering Jul 17, 2026
Share the protobuf-only ONNX graph node sorter from modelopt.onnx.utils and document why it avoids GraphSurgeon dtype round-tripping for enum dtypes such as BF16.

Signed-off-by: Gwena Cunha <4861122+gcunhase@users.noreply.github.com>
@gcunhase gcunhase changed the title Draft: [6421642][ONNX][Quantization] Fix NVFP4 exporter node ordering [6421642][ONNX][Quantization] Fix NVFP4 exporter node ordering Jul 17, 2026

@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/onnx/utils.py (1)

69-78: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use a heap for the ready queue.

pop(0) plus sorting on every iteration can make wide graphs O(V² log V). A heapq min-heap preserves the current stable index ordering while reducing queue maintenance to O((V+E) log V).

🤖 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/onnx/utils.py` around lines 69 - 78, Replace the ready-list queue in
the topological sorting logic with a heapq min-heap, updating initialization,
extraction, and insertion accordingly. Preserve ascending node-index ordering
and the existing dependency processing in the loop, while removing repeated
pop(0) and ready.sort() operations.
🤖 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/onnx/utils.py`:
- Around line 59-67: Update topologically_sort_graph_nodes to account for
tensors captured by nested If/Loop subgraphs when building dependencies, either
by recursively collecting those outer-scope references or by explicitly
rejecting graphs containing unsupported control-flow nodes. Preserve direct
input dependency handling and add a regression test covering a producer and
control-flow consumer connected through an implicit capture.

---

Nitpick comments:
In `@modelopt/onnx/utils.py`:
- Around line 69-78: Replace the ready-list queue in the topological sorting
logic with a heapq min-heap, updating initialization, extraction, and insertion
accordingly. Preserve ascending node-index ordering and the existing dependency
processing in the loop, while removing repeated pop(0) and ready.sort()
operations.
🪄 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: 5d326a8d-4f4d-4e77-bc32-522926e15de9

📥 Commits

Reviewing files that changed from the base of the PR and between 7c448e1 and b32e19e.

📒 Files selected for processing (2)
  • modelopt/onnx/export/nvfp4_exporter.py
  • modelopt/onnx/utils.py

Comment thread modelopt/onnx/utils.py Outdated
Comment on lines +59 to +67
for consumer_index, node in enumerate(nodes):
for input_name in node.input:
producer_index = producer_by_tensor.get(input_name)
if producer_index is None:
continue
if producer_index == consumer_index:
raise ValueError(f"Node {node.name!r} consumes its own output {input_name!r}.")
dependencies[consumer_index].add(producer_index)
dependents[producer_index].add(consumer_index)

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files modelopt/onnx/utils.py modelopt/onnx
echo "--- utils.py outline ---"
ast-grep outline modelopt/onnx/utils.py --view expanded || true
echo "--- relevant lines ---"
wc -l modelopt/onnx/utils.py
sed -n '1,220p' modelopt/onnx/utils.py
echo "--- references to nested subgraphs / control-flow ---"
rg -n "subgraph|If\\(|Loop\\(|scan\\(|GraphProto|node.input|dependencies|producer_by_tensor" modelopt/onnx -S

Repository: NVIDIA/Model-Optimizer

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- walk_subgraphs_recursive ---"
sed -n '1,220p' modelopt/onnx/autocast/utils.py

echo "--- topological sort call sites ---"
rg -n "topologically_sort_graph_nodes\\(" -S modelopt tests || true

echo "--- tests mentioning subgraphs / control-flow ---"
rg -n "subgraph|If\\(|Loop\\(|walk_subgraphs_recursive|topologically_sort_graph_nodes" tests modelopt -g '*test*' -S || true

Repository: NVIDIA/Model-Optimizer

Length of output: 13016


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- nvfp4_exporter call site context ---"
sed -n '360,470p' modelopt/onnx/export/nvfp4_exporter.py

echo "--- subgraph-related utilities in onnx/utils.py ---"
rg -n "walk_subgraphs_recursive|subgraph|AttributeProto\\.GRAPH|AttributeProto\\.GRAPHS" modelopt/onnx/utils.py modelopt/onnx/export/nvfp4_exporter.py -S || true

echo "--- tests for topological sorting or nvfp4 exporter ---"
rg -n "topologically_sort_graph_nodes|nvfp4_exporter|topological sort" tests -S || true

Repository: NVIDIA/Model-Optimizer

Length of output: 6565


Account for captured outer-scope tensors in graph sorting. topologically_sort_graph_nodes only tracks direct node.input edges, so an If/Loop node can be moved ahead of a producer that its subgraph reads implicitly. Either recurse through subgraphs or reject control-flow graphs here, and add a regression test.

🤖 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/onnx/utils.py` around lines 59 - 67, Update
topologically_sort_graph_nodes to account for tensors captured by nested If/Loop
subgraphs when building dependencies, either by recursively collecting those
outer-scope references or by explicitly rejecting graphs containing unsupported
control-flow nodes. Preserve direct input dependency handling and add a
regression test covering a producer and control-flow consumer connected through
an implicit capture.

Cache Cast outputs per input and precision dtype so multiple NVFP4-lowered MatMul consumers reuse a single activation Cast instead of producing duplicate tensor names.

Signed-off-by: Gwena Cunha <4861122+gcunhase@users.noreply.github.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.

2 participants