Skip to content

[PyTorch] Fix RecursionError for ops on CPU-resident QuantizedTensors - #3812

Merged
vthumbe1503 merged 5 commits into
NVIDIA:mainfrom
siddhant-rajhans:fix/cpu-quantized-tensor-dequantize-recursion
Nov 7, 2026
Merged

vthumbe1503 merged 5 commits into
NVIDIA:mainfrom
siddhant-rajhans:fix/cpu-quantized-tensor-dequantize-recursion

Conversation

@siddhant-rajhans

@siddhant-rajhans siddhant-rajhans commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

Ops on a CPU-resident QuantizedTensor that have no quantized implementation (for example x_cpu + 0, or torch.testing.assert_close(x_cpu, ref)) fail with RecursionError for MXFP8 and NVFP4 tensors.

QuantizedTensor.__torch_dispatch__ dequantizes the arguments of such ops. MXFP8 and NVFP4 dequantize with tex.dequantize, which needs GPU buffers, so for a CPU tensor they first call tensor.to(device=...). Inside __torch_dispatch__ the autograd keys are excluded, so that call reaches the dispatcher as aten.to.dtype_layout instead of being decomposed into aten._to_copy. Only aten._to_copy was handled, so the call fell back into the dequantizing path and recursed. FP8 and FP8 block scaling are not affected because they dequantize the CPU buffers directly.

The fix follows #3279 by @janekb04.

Fixes #3294

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Infra/Build change
  • Code refactoring

Changes

  • QuantizedTensor.__torch_dispatch__ handles the aten.to.* overloads the same way as aten._to_copy: the subclass is preserved and the internal buffers move to the target device.
  • test_cpu_torch_ops in tests/pytorch/test_quantized_tensor.py, parametrized over all quantization formats: arithmetic and assert_close on a CPU tensor, .float(), and a round trip back to CUDA.

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

I don't have a Blackwell GPU, so I could not run the MXFP8 and NVFP4 cases locally. I reproduced the recursion with a minimal wrapper subclass on CPU (handling only aten._to_copy recurses on x + 0; handling the aten.to.* overloads as well gives the expected results), and the changed files pass black and pylint with the repo's configuration.

@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Nov 4, 2026
@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issue was found.

Summary

Prevents recursive dequantization when CPU-resident quantized tensors reach plain PyTorch operations.

  • CPU quantized tensors now handle plain PyTorch operations without recursive dispatch.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[PyTorch operation on CPU quantized tensor] --> B[Dequantize arguments]
  B --> C[MXFP8 or NVFP4 moves buffers to CUDA]
  C --> D[Handle aten.to overload directly]
  D --> E[Rebuild quantized tensor with CUDA buffers]
  E --> F[Dequantize on CUDA]
  F --> G[Return dense result to CPU]
Loading

Reviews (6) · Last reviewed commit: "Merge branch 'main' into fix/cpu-quantiz..." · Reviewed by Greptile

_FromNVFP4Func.forward() and _FromMXFP8Func.forward() call
tensor.to(device=cuda) on the full QuantizedTensor when
dequantizing a CPU-resident quantized tensor. This triggers
__torch_dispatch__, which calls maybe_unwrap -> dequantize() ->
forward() -> tensor.to() -> __torch_dispatch__ in an infinite loop.

Move the raw inner data tensors to CUDA individually via
get_metadata() and construct a plain TensorStorage for the
tex.dequantize call. Plain torch.Tensor.to() on the inner
buffers does not enter __torch_dispatch__, breaking the cycle.

Fixes NVIDIA#3294

Signed-off-by: Siddhant Rajhans <51508945+siddhant-rajhans@users.noreply.github.com>
- MXFP8Quantizer requires fp8_dtype argument; pass kFloat8E4M3
- Skip NVFP4/MXFP8 tests on GPUs that lack support via
  is_nvfp4_available() / is_mxfp8_available()

Signed-off-by: Siddhant Rajhans <51508945+siddhant-rajhans@users.noreply.github.com>
@siddhant-rajhans
siddhant-rajhans force-pushed the fix/cpu-quantized-tensor-dequantize-recursion branch from bf75cd5 to 1066e76 Compare November 4, 2026 09:26
@vthumbe1503

vthumbe1503 commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for the PR. There is a previously existing PR #3279 from @janekb04 to fix this problem. I think that PR also handles copy implementation for generic quantized_tensor. We also want to keep the test in test_quantized_tensor.py rather than introduce a new test file.

Could you please use that as reference and modify this PR? Although, I dont see fix/implementation for Float8Tensor and Float8BlockWiseTensor there. My guess is that base implementation in QuantizedTensor already handles the copy method for them. And this makes me wonder whether we even need the standalone implementations for MXFP8/NVFP4. And base QuantizedTensor's implementation should be able to handle it by itself for all QuantizedTensor.

Calling .to() on a QuantizedTensor from inside __torch_dispatch__ reaches
the dispatcher as an aten.to.* overload rather than aten._to_copy,
because the CompositeImplicitAutograd decomposition is skipped there.
Those overloads fell through to the dequantizing fallback, so an op such
as `x_cpu + 0` on a CPU-resident MXFP8 or NVFP4 tensor recursed: the
fallback dequantizes, dequantization moves the tensor to the GPU with
.to(), and .to() dispatches back into the fallback.

Handle the aten.to.* overloads like aten._to_copy in the base class, as
proposed in NVIDIA#3279. This replaces the MXFP8- and NVFP4-specific buffer
copies from the earlier commits of this PR, and the test moves into
test_quantized_tensor.py, parametrized over all quantization formats.

Co-authored-by: Jan Bielak <jbielak@nvidia.com>
Signed-off-by: Siddhant Rajhans <51508945+siddhant-rajhans@users.noreply.github.com>
@siddhant-rajhans siddhant-rajhans changed the title Fix CPU QuantizedTensor dequantize RecursionError [PyTorch] Fix RecursionError for ops on CPU-resident QuantizedTensors Nov 5, 2026
@siddhant-rajhans

Copy link
Copy Markdown
Contributor Author

Thanks for pointing me to #3279. I reworked the PR along those lines in f9380ee:

On your question: as you suspected, the MXFP8/NVFP4-specific changes aren't needed with the base-class fix, so I dropped them. The recursion comes from the base class. When the dequantizing fallback in __torch_dispatch__ moves a CPU tensor to the GPU with .to(), that call arrives as aten.to.dtype_layout (the decomposition into aten._to_copy is skipped inside dispatch), misses the _to_copy branch, and goes back into the fallback. FP8 and FP8 block scaling never reach that path: _FromFloat8Func has a CPU branch and the blockwise dequantization is plain tensor ops, while MXFP8 and NVFP4 need tex.dequantize on the GPU.

I don't have a Blackwell GPU, so I couldn't run the MXFP8/NVFP4 cases here (details in the description). Could you trigger CI when you get a chance?

Comment thread tests/pytorch/test_quantized_tensor.py Resolved
@siddhant-rajhans
siddhant-rajhans force-pushed the fix/cpu-quantized-tensor-dequantize-recursion branch from f9380ee to 52c5faa Compare November 6, 2026 00:32
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Signed-off-by: vthumbe1503 <vthumbe@nvidia.com>
@vthumbe1503

Copy link
Copy Markdown
Collaborator

/te-ci pytorch

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

LGTM

@vthumbe1503
vthumbe1503 merged commit c1c2d4d into NVIDIA:main Nov 7, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Using QuantizedTensor on CPU causes a RecursionError

2 participants