Skip to content

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

Open
gcunhase wants to merge 3 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 3 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.
  • Adds CPU Torch ONNX export regression coverage for NVFP4 static weight export.

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 -q.
  • Ran ruff format modelopt/onnx/export/nvfp4_exporter.py tests/unit/torch/quantization/test_onnx_export_cpu.py and ruff check modelopt/onnx/export/nvfp4_exporter.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 so graph operations are consistently ordered, ensuring producers run before their consumers.
    • Added validation to detect invalid or cyclic graph dependencies during export.
  • Tests

    • Added coverage confirming converted NVFP4 ONNX models are topologically ordered and pass ONNX validation.

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

📝 Walkthrough

Walkthrough

NVFP4 ONNX post-processing now deterministically topologically sorts graph nodes after FP4QDQ replacement and initializer cleanup. A CPU test exports, converts, and validates an NVFP4 ONNX model.

Changes

NVFP4 ONNX Export

Layer / File(s) Summary
Topological export and validation
modelopt/onnx/export/nvfp4_exporter.py, tests/unit/torch/quantization/test_onnx_export_cpu.py
Adds dependency-based stable node ordering with duplicate-producer, self-dependency, and cycle checks; integrates it into NVFP4 post-processing; and adds CPU coverage for FP4QDQ removal and ONNX model validation.

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

Suggested reviewers: ajrasane, galagam

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns ✅ Passed No security anti-patterns found in changed Python/dependency files: no unsafe loads, eval/exec, nosec, trust_remote_code, or new deps.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing NVFP4 exporter node ordering.
✨ 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 (7c448e1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/onnx/export/nvfp4_exporter.py 96.96% 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 Δ
examples 42.73% <96.96%> (-0.70%) ⬇️
gpu 58.16% <96.96%> (-0.02%) ⬇️
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

graph.initializer.extend(new_initializers)
logger.info(f"Removed {len(initializers_to_delete)} initializers")

_topologically_sort_graph_nodes(graph)

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and nearby symbols first.
ast-grep outline modelopt/onnx/export/nvfp4_exporter.py --view expanded

echo
echo "---- lines around _cast_input_dtypes ----"
sed -n '360,430p' modelopt/onnx/export/nvfp4_exporter.py

echo
echo "---- lines around the added topological sort call ----"
sed -n '440,490p' modelopt/onnx/export/nvfp4_exporter.py

echo
echo "---- search for Duplicate producer / cast insertion helpers ----"
rg -n "Duplicate producer|_cast_input_dtypes|topologically_sort|Cast" modelopt/onnx/export -S

Repository: NVIDIA/Model-Optimizer

Length of output: 8139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- _topologically_sort_graph_nodes ----"
sed -n '1,80p' modelopt/onnx/export/nvfp4_exporter.py

echo
echo "---- _replace_fp4qdq_with_2dq ----"
sed -n '113,235p' modelopt/onnx/export/nvfp4_exporter.py

echo
echo "---- consumer map helper ----"
rg -n "def get_tensor_consumer_nodes|get_tensor_consumer_nodes\(" -S modelopt | head -n 20

Repository: NVIDIA/Model-Optimizer

Length of output: 8802


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- graph consumer helper ----"
sed -n '260,340p' modelopt/onnx/quantization/graph_utils.py

echo
echo "---- surrounding export logic in nvfp4_exporter ----"
sed -n '330,470p' modelopt/onnx/export/nvfp4_exporter.py

echo
echo "---- search for any reuse/dedup of cast outputs in exporter code ----"
rg -n "_f16\"|cast_output_name|make_node\\(\"Cast\"|Cast nodes for each input" modelopt/onnx/export modelopt/onnx/quantization -S

Repository: NVIDIA/Model-Optimizer

Length of output: 10110


Prevent duplicate Cast outputs for shared activations. _cast_input_dtypes() always names the cast tensor <input>_f16, so two MatMul nodes that consume the same activation will insert duplicate Cast producers and make _topologically_sort_graph_nodes() fail. Cache/reuse the Cast per (input_name, precision_dtype) or make the output name unique per consumer.

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

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