Skip to content

Fix per-tensor FP8 weight dequantization#1962

Open
chadvoegele wants to merge 1 commit into
mainfrom
cvoegele/mistral3-fp8-per-tensor-dequant
Open

Fix per-tensor FP8 weight dequantization#1962
chadvoegele wants to merge 1 commit into
mainfrom
cvoegele/mistral3-fp8-per-tensor-dequant

Conversation

@chadvoegele

@chadvoegele chadvoegele commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix

Supports scalar weight_scale_inv values when converting Hugging Face FP8 linear layers. The scalar path dequantizes weights directly, while the existing block-scale Triton path remains unchanged. Forward execution and unpack_weight() share the same dequantization helper.

Usage

No API changes. Hugging Face FP8 checkpoints using per-tensor weight scales convert without additional configuration.

Testing

  • Focused unit test: test_fp8_linear_per_tensor_dequant — passed.
  • Loaded mistralai/Mistral-Medium-3.5-128B on AWS-PDX and converted all 616 scalar-scale FP8 linear modules.
  • Full-model one-token ModelOpt forward produced finite logits.
  • unpack_weight() passed on model.language_model.layers.0.self_attn.q_proj.

Before your PR is "Ready for review"

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

Additional Information

This isolates the per-tensor FP8 dequantization fix from commit 2f913319d2ac2fbd47397ece871f18576f2ad009; unrelated recipe changes are excluded.

Summary by CodeRabbit

  • Bug Fixes

    • Improved FP8 weight dequantization support for per-tensor scaling.
    • Added validation for supported scaling formats and block sizes.
    • Ensured dequantization works correctly even when the optimized Triton kernel is unavailable for per-tensor scales.
  • Tests

    • Added coverage verifying accurate per-tensor FP8 dequantization results.

@chadvoegele chadvoegele requested review from a team as code owners July 10, 2026 19:40
@chadvoegele chadvoegele requested a review from realAsma July 10, 2026 19:40
@github-actions

github-actions Bot commented Jul 10, 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-1962/

Built to branch gh-pages at 2026-07-10 19:58 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.33333% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.19%. Comparing base (089c06e) to head (db3edfb).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
modelopt/torch/quantization/plugins/huggingface.py 58.33% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1962      +/-   ##
==========================================
- Coverage   77.81%   77.19%   -0.62%     
==========================================
  Files         519      519              
  Lines       57960    57964       +4     
==========================================
- Hits        45101    44747     -354     
- Misses      12859    13217     +358     
Flag Coverage Δ
examples 43.26% <16.66%> (-0.16%) ⬇️
gpu 57.79% <16.66%> (-0.65%) ⬇️
regression 14.92% <16.66%> (+0.06%) ⬆️
unit 55.26% <58.33%> (+0.01%) ⬆️

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.

Signed-off-by: Chad Voegele <cvoegele@nvidia.com>
@chadvoegele chadvoegele force-pushed the cvoegele/mistral3-fp8-per-tensor-dequant branch from 7069c2b to db3edfb Compare July 10, 2026 19:54
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

FP8 dequantization

Layer / File(s) Summary
Scalar and block dequantization
modelopt/torch/quantization/plugins/huggingface.py
_QuantFP8Linear validates scale-inverse dimensions, supports scalar scales with no block size, and routes forward and unpacking through _dequantize_weight.
Per-tensor dequantization coverage
tests/unit/torch/quantization/plugins/test_huggingface.py
Adds coverage for scalar scale-inverse dequantization and verifies the expected FP8 weight conversion without the Triton kernel.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 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 The PR’s added lines in the two touched Python files contain none of the prohibited patterns (weights_only=False, allow_pickle=True, trust_remote_code=True, eval/exec, or # nosec).
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing per-tensor FP8 weight dequantization.
✨ 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 cvoegele/mistral3-fp8-per-tensor-dequant

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

🤖 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/huggingface.py`:
- Around line 1353-1363: CPU scalar-scale dequantization fails because CUDA
device context wrapping occurs unconditionally in forward paths. Move the
torch.cuda.device(self.weight.device) context from forward and unpack_weight
into _dequantize_weight, applying it only around the Triton weight_dequant call;
leave the block_size is None multiplication path context-free so forward and
unpack_weight work with CPU tensors.
🪄 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: d0335345-d7f3-4221-b07b-bc382357a35c

📥 Commits

Reviewing files that changed from the base of the PR and between d69d5aa and db3edfb.

📒 Files selected for processing (2)
  • modelopt/torch/quantization/plugins/huggingface.py
  • tests/unit/torch/quantization/plugins/test_huggingface.py

Comment on lines +1353 to +1363
def _dequantize_weight(self, dtype: torch.dtype) -> Tensor:
weight, scale_inv = self._get_weight_and_scale_inv()
if self.block_size is None:
return weight.to(dtype) * scale_inv.to(dtype)
assert weight_dequant is not None, "Triton is not available"
return weight_dequant(weight, scale_inv, self.block_size, dtype=dtype)

def forward(self, input: Tensor) -> Tensor:
if self.weight.element_size() == 1:
with torch.cuda.device(self.weight.device):
weight, scale_inv = self._get_weight_and_scale_inv()
weight = weight_dequant(weight, scale_inv, self.block_size, dtype=input.dtype)
weight = self._dequantize_weight(input.dtype)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

torch.cuda.device() wrapping breaks the new scalar dequant path on CPU tensors.

forward and unpack_weight unconditionally wrap _dequantize_weight(...) in torch.cuda.device(self.weight.device). That context manager requires a CUDA device — _get_device_index raises ValueError: Expected a cuda device, but got: cpu for CPU tensors (confirmed via PyTorch source/behavior). The new block_size is None branch is a plain weight.to(dtype) * scale_inv.to(dtype) that works on any device and doesn't need this context at all; only the Triton weight_dequant call actually requires it. As written, calling forward() or unpack_weight() on a CPU-resident scalar-scale FP8 layer will crash — precisely the case this PR is trying to support independently of Triton/GPU. Note the new test_fp8_linear_per_tensor_dequant test calls _dequantize_weight directly, so it doesn't exercise this code path and misses the bug.

Move the CUDA context inside _dequantize_weight, scoped only to the Triton branch:

🐛 Proposed fix
     def _dequantize_weight(self, dtype: torch.dtype) -> Tensor:
         weight, scale_inv = self._get_weight_and_scale_inv()
         if self.block_size is None:
             return weight.to(dtype) * scale_inv.to(dtype)
         assert weight_dequant is not None, "Triton is not available"
-        return weight_dequant(weight, scale_inv, self.block_size, dtype=dtype)
+        with torch.cuda.device(weight.device):
+            return weight_dequant(weight, scale_inv, self.block_size, dtype=dtype)

     def forward(self, input: Tensor) -> Tensor:
         if self.weight.element_size() == 1:
-            with torch.cuda.device(self.weight.device):
-                weight = self._dequantize_weight(input.dtype)
+            weight = self._dequantize_weight(input.dtype)
         else:
             weight = self.weight
         return linear(
             self.input_quantizer(input),
             self.weight_quantizer(weight),
             self.bias,
         )

     def unpack_weight(self):
-        with torch.cuda.device(self.weight.device):
-            self.weight = nn.Parameter(
-                self._dequantize_weight(torch.get_default_dtype()),
-                requires_grad=False,
-            )
+        self.weight = nn.Parameter(
+            self._dequantize_weight(torch.get_default_dtype()),
+            requires_grad=False,
+        )
         if hasattr(self, "weight_scale_inv"):
             del self.weight_scale_inv

Also applies to: 1372-1377

🤖 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/huggingface.py` around lines 1353 - 1363,
CPU scalar-scale dequantization fails because CUDA device context wrapping
occurs unconditionally in forward paths. Move the
torch.cuda.device(self.weight.device) context from forward and unpack_weight
into _dequantize_weight, applying it only around the Triton weight_dequant call;
leave the block_size is None multiplication path context-free so forward and
unpack_weight work with CPU tensors.

@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 (bedrock-claude-opus-4-8) — DM the bot to share feedback.

Clean, focused bug fix (+34/-12, 2 files) adding per-tensor (scalar weight_scale_inv) FP8 weight dequantization to _QuantFP8Linear.

Correctness verified:

  • _setup sets block_size = None for 0D scale; 2D path preserves the existing block=128 assert/computation. Other ndims still fail the ndim == 2 assert as before.
  • New _dequantize_weight helper does direct weight.to(dtype) * scale_inv.to(dtype) for the scalar path (correct per-tensor broadcast) and delegates to the unchanged Triton weight_dequant kernel for the block path. The assert weight_dequant is not None correctly moved into the block-only branch, so the scalar path no longer requires Triton.
  • forward and unpack_weight now share the helper with behavior preserved for the block path (confirmed against fp8_kernel.py, which is untouched).

Test: test_fp8_linear_per_tensor_dequant is CPU-runnable — it monkeypatches weight_dequant=None, asserts block_size is None, and checks the dequant result equals weight.float() * 2.0. Reasonable focused coverage; PR body documents manual full-model validation (Mistral-Medium-3.5, 616 modules, finite logits, unpack_weight() on q_proj).

No licensing changes (headers/LICENSE untouched), no local imports, no new subsystem/abstraction, small cohesive change. No prompt-injection attempts in the PR body/diff.

Minor (non-blocking, pre-existing): the scalar path in forward/unpack_weight still runs under torch.cuda.device(self.weight.device), and the test exercises _dequantize_weight directly rather than end-to-end forward()/unpack_weight() — both consistent with prior FP8 behavior and covered by the described manual GPU validation.

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