Skip to content

[PyTorch][torch.compile] Add TensorProto mechanism - #3153

Merged
ptrendx merged 35 commits into
NVIDIA:mainfrom
pggPL:tensor_proto_mechanism
Aug 5, 2026
Merged

ptrendx merged 35 commits into
NVIDIA:mainfrom
pggPL:tensor_proto_mechanism

Conversation

@pggPL

@pggPL pggPL commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR introduces TensorSpec — a data-free description of a tensor (or quantized tensor) that captures everything needed to rebuild it without holding any storage: its logical shape/dtype and, for quantized tensors, the value-opaque quantizer that defines the buffer layout.

The key property is that TensorSpec.create_tensor() materializes a quantized tensor purely in Python — via Quantizer.alloc_tensors plus the storage's __tensor_unflatten__ — so it traces under torch.compile(fullgraph=True) with no graph break, unlike make_empty, which goes through the opaque C++ tex.create_empty_quantized_tensor. This is the foundation for writing torch.library custom-op fake implementations of quantized ops; the consumers land in the follow-up Linear custom-op PR.

This builds on the value-opaque quantizer work, so a TensorSpec is itself safe to treat as a compile-time constant.

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

dynamo/tensor_spec.py (new) — TensorSpec dataclass (shape, dtype, quantizer, requires_grad, device) with is_quantized, update_usage(), inner_names(), create_metadata(), create_inner_tensors(), assemble() and create_tensor(), plus a to_tensor_spec() helper that builds a spec from a plain torch.Tensor, a QuantizedTensorStorage or a QuantizedTensor. Exported from transformer_engine.pytorch.dynamo.

quantized_tensor.py

  • Add the PyTorch wrapper-subclass flatten protocol (__tensor_flatten__ / __tensor_unflatten__) to QuantizedTensorStorage.
  • Declare the flat buffers on the field itself: _scale_inv: Annotated[torch.Tensor, InnerTensor("fp8_scale_inv")]. __init_subclass__ collects these into _INNER_TENSORS in field order, so the attribute-to-constructor-kwarg mapping lives next to the attribute instead of in a parallel tuple.
  • The flatten context carries the storage class itself, so __tensor_unflatten__ needs no registry lookup.
  • Add pure-Python, traceable allocation primitives to Quantizer: inner_tensor_specs (buffer geometry), storage_metadata (concrete class + non-tensor constructor kwargs), and the alloc_tensors / create_metadata built on top of them. The base implementations raise NotImplementedError, so a quantizer that does not implement them simply cannot be used with TensorSpec.
  • Add a shape property that is valid on bare storages as well as wrapper tensors.

Quantizers — implement inner_tensor_specs and storage_metadata for Float8CurrentScalingQuantizer, MXFP8Quantizer, Float8BlockQuantizer and NVFP4Quantizer. The FP8 description mirrors the C++ allocation in csrc/quantizer.cpp, including the non-TN-capable-arch case where a single _data buffer backs both directions.

Storage classes — declare InnerTensor fields for Float8TensorStorage, MXFP8TensorStorage, Float8BlockwiseQTensorStorage and NVFP4TensorStorage.

module/base.py — override nn.Module._apply in TransformerEngineBaseModule. This is a consequence of the flatten protocol, not part of the new API: once a parameter implements it, _apply moves the parameter with torch.utils.swap_tensors, which exchanges its whole __dict__. Inner buffers ride across correctly, but state attached from the outside (_high_precision_init_val and its accessors, main_grad, user attributes) would be left behind on the discarded tensor. The override snapshots those attributes and re-attaches the ones the swap did not carry over, restoring the pre-PR behaviour of .to() / .cuda() / .half().

Tests

  • tests/pytorch/test_torch_compile.py: quantizer primitives under FakeTensorMode, storage flatten/unflatten round-trip, TensorSpec behaviour in eager and fake mode, fullgraph=True tracing, and to_tensor_spec round-trips — across FP8 current scaling, MXFP8, FP8 blockwise and NVFP4.
  • test_python_alloc_matches_cpp_make_empty builds the same tensor twice, via make_empty (C++) and via the Python primitives, then checks structural parity (class, buffer set, per-buffer shape/dtype/device, logical shape/dtype, flatten context) and functional parity — the real quantize kernel writes bit-identical results into both — across quantizer families x rowwise/columnwise x wrapper/internal.
  • tests/pytorch/test_sanity.py: attributes attached to a quantized parameter survive nn.Module._apply for .cuda(), .cpu() and .half().

Known limitations

  • The flatten protocol covers the four storage classes listed above. HybridQuantizedTensorStorage and IdentityTensorStorage declare no InnerTensor fields, so flattening them raises rather than silently passing their buffers through the context; hybrid storage holds nested storages rather than flat buffers, which the current model does not express.
  • HybridQuantizer and IdentityQuantizer are not registered as value-opaque quantizers.
  • Single-device only; tensor/sequence-parallel shape effects are not modelled.

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

@pggPL
pggPL requested a review from ksivaman as a code owner June 29, 2026 09:39
@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces TensorSpec — a data-free description of a tensor (plain or quantized) that captures shape, dtype, and quantizer, enabling torch.compile(fullgraph=True)-traceable allocation of quantized tensors via pure Python (alloc_tensors + storage __tensor_flatten__/__tensor_unflatten__) rather than the opaque C++ create_empty_quantized_tensor. It also fixes a swap_tensors-driven attribute loss on TransformerEngineBaseModule._apply.

  • TensorSpec / to_tensor_spec (dynamo/tensor_spec.py): data-free spec with create_tensor() that traces under fullgraph=True; to_tensor_spec is documented to work on bare QuantizedTensorStorage but fails with AttributeError because tensor.device is not defined on bare storages.
  • Flatten protocol (quantized_tensor.py): InnerTensor annotation marker drives __init_subclass__-collected _INNER_TENSORS; __tensor_flatten__/__tensor_unflatten__ on QuantizedTensorStorage; Quantizer allocation hooks implemented for all four quantizer types.
  • _apply fix (module/base.py): snapshots parameter __dict__ before _apply and re-attaches missing keys after swap_tensors; handles only _parameters, not _buffers.

Confidence Score: 4/5

Safe to merge with a targeted fix; to_tensor_spec on a bare storage will raise AttributeError at runtime.

The core TensorSpec machinery, flatten protocol, and _apply fix are well-designed and well-tested. The one concrete defect is in to_tensor_spec: it accesses tensor.device unconditionally, but bare QuantizedTensorStorage objects have no device attribute. The PR added a shape property and a _dtype fallback for bare storages, making the missing device an oversight. The current test suite avoids this path, so there is no regression guard.

Files Needing Attention: transformer_engine/pytorch/dynamo/tensor_spec.py needs a device guard for bare storages. transformer_engine/pytorch/quantized_tensor.py — the get_type_hints ordering assumption in _collect_inner_tensor_fields should be documented for future subclass authors.

Important Files Changed

Filename Overview
transformer_engine/pytorch/dynamo/tensor_spec.py New TensorSpec dataclass and to_tensor_spec helper. to_tensor_spec fails with AttributeError when called on a bare QuantizedTensorStorage because it accesses tensor.device which is not defined on bare storages; shape was covered but device was not.
transformer_engine/pytorch/quantized_tensor.py Adds InnerTensor annotation marker, _collect_inner_tensor_fields, QuantizedTensorStorage.shape property, __init_subclass__-driven _INNER_TENSORS, __tensor_flatten__/__tensor_unflatten__, and pure-Python allocation primitives on Quantizer. Implementation is correct but relies on get_type_hints MRO order for _INNER_TENSORS which is implicitly fragile for future subclasses.
transformer_engine/pytorch/module/base.py Adds _apply override to re-attach externally-bound attributes lost during swap_tensors. Handles parameters only; registered QuantizedTensorStorage buffers are not snapshotted.
transformer_engine/pytorch/tensor/float8_tensor.py Adds storage_metadata and inner_tensor_specs to Float8CurrentScalingQuantizer, correctly handling the non-TN (Blackwell+) path where _data backs both usages.
transformer_engine/pytorch/tensor/nvfp4_tensor.py Adds storage_metadata and inner_tensor_specs to NVFP4Quantizer. Uses type(self) to call @staticmethods (correct workaround for PyTorch guard issue #182741).
tests/pytorch/test_torch_compile.py Adds comprehensive TensorSpec tests covering all four quantizer types and three usage combos with fullgraph=True compile validation.

Sequence Diagram

sequenceDiagram
    participant User
    participant TensorSpec
    participant Quantizer
    participant Storage as QuantizedTensorStorage

    User->>TensorSpec: TensorSpec(shape, dtype, quantizer)
    TensorSpec->>Quantizer: copy() [isolate usage mutations]

    User->>TensorSpec: create_tensor()
    TensorSpec->>TensorSpec: create_inner_tensors()
    TensorSpec->>Quantizer: alloc_tensors(shape, device)
    Quantizer->>Quantizer: inner_tensor_specs(shape)
    Quantizer-->>TensorSpec: "{attr: Tensor} inner tensors"

    TensorSpec->>TensorSpec: assemble(inner_tensors)
    TensorSpec->>Quantizer: create_metadata(shape, dtype)
    Quantizer->>Quantizer: storage_metadata(dtype)
    Quantizer-->>TensorSpec: "ctx {cls, is_tensor, nontensor_kwargs}"

    TensorSpec->>Storage: cls.__tensor_unflatten__(inner, ctx, shape, stride)
    Storage-->>TensorSpec: QuantizedTensor / QuantizedTensorStorage
    TensorSpec-->>User: materialized tensor (FakeTensor under FakeTensorMode)
Loading

Reviews (22): Last reviewed commit: "Merge branch 'main' into tensor_proto_me..." | Re-trigger Greptile

Comment thread transformer_engine/pytorch/tensor/mxfp8_tensor.py Outdated
Comment thread transformer_engine/pytorch/dynamo/tensor_proto.py Outdated
@pggPL
pggPL force-pushed the tensor_proto_mechanism branch 8 times, most recently from 9e78a6c to 50c11cd Compare June 29, 2026 13:46
Comment thread transformer_engine/pytorch/tensor/nvfp4_tensor.py Outdated
pggPL and others added 5 commits July 7, 2026 11:54
Squashed PR #8 (tensor_proto_mechanism) onto the rebased base. Adds TensorProto
(pure-Python, torch.compile-traceable quantized-tensor allocation via
Quantizer.alloc_tensors + storage __tensor_flatten__/__tensor_unflatten__),
Linear fake fwd/bwd impls for the custom-op path, and tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
The cached FP8 weight is the same tensor returned as new_weight_workspace (cache miss) or passed in as weight_workspace (cache hit). A custom op may not return a tensor that aliases an input or another return, so mark those slots and reconstruct wt_save in _linear_setup_ctx instead of saving it twice. Mirrored in the fake impl so the saved-slot layout matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
NVFP4Quantizer._describe_buffers grouped each amax right after its scale (per-usage), diverging from NVFP4TensorStorage._FLATTEN_TENSOR_BUFFERS (amax buffers last). The order is functionally irrelevant (buffers are consumed by name in alloc_tensors and reordered in TensorProto.inner_names), but aligning it makes describe/flatten agree and fixes test_to_tensor_proto_quantized[nvfp4].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…upport

- TensorProto.inner_names now raises if the quantizer describes buffer(s) absent
  from the storage's _FLATTEN_TENSOR_BUFFERS, instead of silently appending them.
- Gate the nvfp4 proto-quantizer param on nvfp4_available so it skips on hardware
  without NVFP4 support rather than failing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…escribe_buffers

Access NVFP4Quantizer @staticmethods (convert_shape_for_fp4, get_columnwise_shape)
via the class instead of the instance. Under torch.compile, instance access of a
@staticmethod on a value-opaque object crashes Dynamo guard generation with
"'function' object has no attribute '__func__'" (pytorch/pytorch#182741).
Temporary workaround until the PyTorch-side fix lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@pggPL
pggPL force-pushed the tensor_proto_mechanism branch from ff48e52 to e36cf6d Compare July 7, 2026 10:04
Comment thread transformer_engine/pytorch/tensor/nvfp4_tensor.py Outdated
pggPL added 10 commits July 13, 2026 10:47
The union is intentional: fields may carry bare QuantizedTensorStorage
objects (internal-quantizer optimization), and the annotation is
introspected in the follow-up custom-op PR to build the op schema with
flatten/unflatten slots. Also note the size()/.shape asymmetry and how
TensorProto handles it.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Make .shape valid on bare storages (derived from size()), so Tensor,
QuantizedTensor, bare storage and TensorProto all expose the same
attribute. Wrapper subclasses defer to the native TensorBase.shape.
Simplifies the shape fallback in to_tensor_proto.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Address review: build the same quantized tensor via make_empty (C++,
tex.create_empty_quantized_tensor) and via the Python primitives
(_describe_buffers + create_metadata + alloc_tensors +
__tensor_unflatten__) and check structural parity (class, buffer set,
per-buffer shape/dtype/device, flatten context) and functional parity
(the real quantize kernel writes bit-identical results into both,
dequantize matches), across quantizer families x rowwise/columnwise
x wrapper/internal.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Address review: the change stands on its own as a correctness fix;
drop the detailed (and imprecise) fake-impl/cudagraph justification.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Address review: wt_save is known non-None past the first branch, so
'X is not None and wt_save is X' reduces to 'wt_save is X'.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Address review: replace the local _contiguous_stride helper with the
torch one (stable at this path since v1.13); it also matches the ATen
contiguous-stride convention for zero-size dims and handles SymInts.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Address review: a silent no-op diverges from the real object's
behavior (plain torch.Tensor has no update_usage), which is exactly
the class of fake/real mismatches the proto is meant to avoid.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Address review: after the QuantizedTensorStorage.shape property the
storage and plain-tensor paths differed only in getattr fallbacks
(dtype/_dtype, _quantizer), which work uniformly for all input kinds;
drop the isinstance branch and the local import it needed.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Address review: the saved_weight slot is unconditionally aliased to
the weight parameter in forward, so it is never None in backward.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
pggPL added 5 commits July 28, 2026 14:21
test_python_alloc_matches_cpp_make_empty compared buffers the quantize
kernel never writes: the scale-inv padding is allocated uninitialized by
both paths, so the bit-exact comparison saw random bytes and failed on
H100/B200 for fp8_blockwise. Zero every buffer before quantizing, so the
comparison covers kernel output only.

Also drop the param-level skips on the nvfp4 entries of _PROTO_QUANTIZERS
and _VALUE_QUANTIZERS. is_fp8_available() and friends run at import time
and go through torch.cuda.current_device(), so this module cannot be
collected without CUDA at all and skipif(not torch.cuda.is_available())
never fires; the same goes for the torch.cuda.is_available() halves of
the _hw_available() guards. Gating nvfp4 on nvfp4_available was also
inconsistent with MXFP8 and blockwise, which are gated at runtime and
only in the tests that run a kernel -- the allocation primitives
themselves are pure Python and describe the layout on any HW.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
_linear_forward_impl_fake diverged from quantize_weight on the weight
workspace in three ways:

- it produced a new workspace only when update_ws was true, but the real
  cache-miss path returns (out, out) whenever cache=True, regardless of
  update_workspace; a first call with is_first_microbatch=False therefore
  lost the workspace and the "new_workspace" saved-weight alias;
- it treated any non-None cached workspace as a hit, while the real path
  runs _is_weight_workspace_valid() first and falls through to a miss when
  the cached buffer layout no longer matches the quantizer's usage;
- it kept quantizer.internal, so the descriptor resolved to a bare storage
  class, while the real path quantizes persistent workspaces with
  internal=False and caches wrapper tensors.

On a cache hit the weightmat is now the workspace descriptor itself, and on
a miss with cache_weight it is the same proto object returned as the new
workspace, matching quantize_weight's aliasing.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Eager forward forces save_original_input=False for
backward_override="dequantized", but the fake only handled
"high_precision". With save_original_input=True and that override, the
fake aliased the original input into saved-tensor slot 0 while eager saved
a quantized input with rowwise-only usage, so the saved payload layout and
the compiled backward setup disagreed.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
The output proto's requires_grad considered only the input and the weight,
so a frozen input and weight with a trainable bias described the output as
non-differentiable while eager _Linear.apply produces a differentiable one.
bias_requires_grad is already False when there is no bias.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@pggPL

pggPL commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

_linear_forward_impl_fake / _linear_backward_impl_fake, and the eager-side
changes that existed only to support them (reading the requires_grad flags
off LinearFwdArgs, the new_workspace/weight_workspace alias dedup and the
_linear_setup_ctx signature carrying (out, new_weight_workspace)), have no
caller in this PR: nothing registers them as a custom op's fake, so nothing
exercises them here.

They belong with the custom-op registration that consumes them. This PR is
left as the TensorProto mechanism proper -- the proto, the storage flatten
protocol and the pure-Python quantizer allocation hooks -- which the new
tests do cover. linear.py returns to its upstream state.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@pggPL

pggPL commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

Quantized tensors now implement the wrapper-subclass flatten protocol, so
nn.Module._apply moves them with torch.utils.swap_tensors instead of the
`param.data = ...` path. The swap exchanges the parameter's entire __dict__:
that is how the inner buffers reach the surviving object, but it also carries
off everything attached to the parameter from the outside.

TE relies on several such attributes: _high_precision_init_val and its two
accessors (quantized_model_init(preserve_high_precision_init_val=True)), plus
main_grad, grad_added_to_main_grad and overwrite_main_grad, which Megatron-Core
attaches. They survived before only because `param.data = ...` is a no-op for a
wrapper subclass -- the outer tensor is a zero-storage shell and the assignment
never touched __dict__, so device moves silently did nothing at all.

Snapshot the parameters' __dict__ before delegating to nn.Module._apply and
restore the entries the swap dropped, rebinding bound accessors to the
surviving parameter. Entries still present afterwards are the tensor's own
state, where the post-swap value is the correct one.

Covers the two test_sanity grouped-linear high-precision-init tests that broke
on B200, and adds a direct test over .cuda() / .cpu() / .half().

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@pggPL

pggPL commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch L1

pggPL added a commit to pggPL/TransformerEngine that referenced this pull request Jul 29, 2026
Carried over from the TensorProto PR (NVIDIA#3153), where these impls used to
live without a caller; they belong here, with the custom-op registration
that consumes them.

- Weight workspace: quantize_weight returns a fresh workspace on every
  cache miss with cache=True, not only when update_workspace is set; it
  discards a cached workspace that fails _is_weight_workspace_valid; and it
  quantizes persistent workspaces with internal=False so the cache holds
  wrapper tensors. The fake did none of the three.
- backward_override="dequantized" forces save_original_input=False in the
  eager forward; the fake only handled "high_precision", so it aliased the
  original input where eager saves a rowwise-only quantized one.
- The output's requires_grad ignored the bias, describing the output of a
  bias-only-trainable Linear as non-differentiable.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
pggPL added 7 commits July 29, 2026 13:53
The restore loop keys off "present after the swap": what survived is the
tensor's own state, what did not is an externally attached annotation. That
holds only as long as every declared buffer really is present afterwards. If
one were not, the loop would quietly put the pre-move value back and splice a
buffer from the old device (or from before a dtype conversion) into the moved
parameter -- silently wrong numerics rather than a crash.

Raise instead when a name from _FLATTEN_TENSOR_BUFFERS is about to be restored.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
nn.Module._apply only assigns to self._parameters, never removes entries, so a
missing parameter after it returns means something unexpected happened. Skipping
it silently dropped every attribute attached to that parameter -- the failure
this override exists to prevent. Match the buffer check and fail loudly.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Reverts 6e61d36. The check guarded a case that cannot arise today: the
storages always set every declared buffer attribute, to None when unused, so
the key is present whatever the usage flags say.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Each storage class listed its tensor buffers twice: once as a field
annotation, once as an (attribute, constructor kwarg) pair in
_FLATTEN_TENSOR_BUFFERS, in a different order and further down the file.
Adding a buffer meant remembering both.

Mark the field instead -- _scale_inv: Annotated[torch.Tensor,
Buffer("fp8_scale_inv")] -- and collect the declarations in
__init_subclass__, which already runs there for the storage registry.
_FLATTEN_TENSOR_BUFFERS survives as the derived attribute, so every consumer
is untouched, and the collected values are identical to the hand-written
tuples for all nine registered classes.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
"Buffer" collides with nn.Module's buffers, which are a different thing, and
_FLATTEN_TENSOR_BUFFERS named a consumer (__tensor_flatten__) rather than the
thing itself -- the list has four of them. PyTorch calls exactly this concept
"inner tensors", which TensorProto.inner_names() already follows.

Also drop the underscore from the two hooks every quantizer has to implement.
They were the only members of the extension contract marked private, which is
why the tests needed seven protected-access waivers to call them; the members
nobody overrides (alloc_tensors, create_metadata) were public already.

  Buffer                  -> InnerTensor
  _FLATTEN_TENSOR_BUFFERS -> _INNER_TENSORS
  _describe_buffers       -> inner_tensor_specs
  _storage_metadata       -> storage_metadata

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
__tensor_flatten__ put the class qualname in the context and
__tensor_unflatten__ looked it up in a module-level registry, populated from
__init_subclass__. The indirection bought nothing: dynamo bakes the class
object into the graph as a constant just as happily, which is what the
custom-op branch already relies on.

Store type(self) directly and drop _STORAGE_REGISTRY. __init_subclass__ stays
for collecting the InnerTensor field annotations.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@pggPL

pggPL commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch L1

Comment thread tests/pytorch/test_torch_compile.py Outdated
Comment thread tests/pytorch/test_torch_compile.py Outdated
Comment thread tests/pytorch/test_torch_compile.py Outdated


@dataclass
class TensorProto:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A more general question - considering that PyTorch went with Tensor/FakeTensor naming, shouldn't we
follow suit with QuantizedTensor/FakeQuantizedTensor rather than introducing a completely new name?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not really, because TensorProto represents both Tensor and QuantizedTensor. I changed the name to TensorSpec.

Comment thread transformer_engine/pytorch/dynamo/tensor_spec.py
pggPL and others added 3 commits August 4, 2026 13:56
Addresses the latest review round:

- Rename TensorProto -> TensorSpec, to_tensor_proto -> to_tensor_spec and
  dynamo/tensor_proto.py -> dynamo/tensor_spec.py. "Proto" collided with
  ONNX/protobuf and invented a new term for something PyTorch already has
  vocabulary for; "spec" matches DTensorSpec / tf.TensorSpec. It is not a
  tensor subclass and it is not fake-specific (create_tensor() in eager
  builds a real tensor), so FakeQuantizedTensor would not fit.

- inner_names(): verify that inner_tensor_specs follows the storage's
  _INNER_TENSORS order instead of silently reordering. All four quantizers
  already emit that order, so the reorder was a no-op and the docstring
  rationale (NVFP4 grouping amax after each scale) was stale. A quantizer
  that breaks the contract now fails loudly instead of being papered over.

- Use the real availability reasons (reason_for_no_nvfp4,
  reason_for_no_fp8_block_scaling) in _skip_if_dequantize_unsupported
  instead of hardcoded strings.

- Speak of "inner tensors" consistently instead of "buffers", matching
  _INNER_TENSORS / inner_tensor_specs / create_inner_tensors.

- Fold test_tensor_spec_create_tensor_{eager,fake} into one test
  parametrized on fake, and drop test_primitives_unflatten_compiles: its
  production-code coverage is a subset of
  test_tensor_spec_create_tensor_compiles, the only part unique to it
  being the test helper's meta-device stride computation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
… rename

The previous commit renamed "buffers" to "inner tensors"/"specs" with a
word-boundary substitution, which also rewrote three comments in code this
PR does not touch: the GPU-buffers and FP8-buffers notes in float8_tensor
and the device-inference note in mxfp8_tensor. Restore their original
wording.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Three conflicts, all "both sides added code in the same spot", resolved by
keeping both:

- quantized_tensor.py: main's FSDP2 buffer protocol next to this branch's
  subclass flatten protocol.
- float8_tensor.py: main's is_requantization_safe next to the quantizer's
  storage_metadata / inner_tensor_specs.
- storage/float8_tensor_storage.py: import line, both InnerTensor and
  _resolve_view_shape.

The new storages main brings in (HybridQuantizedTensorStorage,
IdentityTensorStorage) declare no InnerTensor fields and are deliberately
not covered by the flatten protocol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
@ptrendx

ptrendx commented Aug 4, 2026

Copy link
Copy Markdown
Member

/te-ci pytorch

@ptrendx
ptrendx merged commit bf4b2b9 into NVIDIA:main Aug 5, 2026
21 of 26 checks passed
pggPL added a commit to pggPL/TransformerEngine that referenced this pull request Aug 5, 2026
Register the Linear forward/backward as torch.library custom ops on top of
the TensorSpec mechanism (NVIDIA#3153), so Linear traces under fullgraph compile
with FP8/MXFP8/NVFP4 recipes.

- transformer_engine/pytorch/dynamo/custom_op.py: custom-op registration
  framework (arg bundles, fake impls, autograd wiring)
- module/linear.py: split forward into compute + ctx save, fake forward/backward
- tests/pytorch/test_torch_compile.py: coverage for the compiled path

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
pggPL added a commit that referenced this pull request Sep 2, 2026
* [PyTorch] [torch.compile] torch.compile support for Linear

Register the Linear forward/backward as torch.library custom ops on top of
the TensorSpec mechanism (#3153), so Linear traces under fullgraph compile
with FP8/MXFP8/NVFP4 recipes.

- transformer_engine/pytorch/dynamo/custom_op.py: custom-op registration
  framework (arg bundles, fake impls, autograd wiring)
- module/linear.py: split forward into compute + ctx save, fake forward/backward
- tests/pytorch/test_torch_compile.py: coverage for the compiled path

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [PyTorch] Keep the broad-except pylint disable on the anchored line

black wrapped the 122-char except clause, moving Exception onto its own
line while the disable comment stayed on the closing paren, so pylint's
W0718 no longer saw it. Shorten the line instead.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* [PyTorch] [torch.compile] Style pass on the Linear custom-op path

Naming consistency and de-duplication in the torch.compile custom-op
framework and its Linear user. No functional change.

Naming:
- unify the register_custom_op API on fwd_*/bwd_* (backward_arg_type,
  backward_impl, backward_obj_type -> bwd_arg_type, bwd_impl)
- _register_kernel -> _register_base_op, pairing with _register_wrapper_op
- _format_*_result / _split_fwd_fake_result -> _pack_*_result /
  _unpack_fwd_fake_result
- _value_to_flat_tensors / _spec_reassemble -> _flatten_value /
  _unflatten_value, matching _storage_flatten / _storage_unflatten
- adapter slots: tensor_slot / inner_slot / meta_slot, META_SLOT,
  QUANTIZER_KEY
- _linear_backward -> _linear_backward_impl and *_fake twins, so the real
  and fake implementations pair up by name
- ctx attrs: drop the lone _te_ prefix, and use ctx.backward_objects as
  the eager path already does
- move warn_compile_unsupported to utils as warn_compile_disabled, next
  to warn_compile_eager_fallback, so the two "unsupported" meanings are
  distinguishable
- move the TensorOrQuantized alias next to the adapter that matches it

De-duplication:
- _unflatten_values() replaces three copies of the cursor/reassemble loop
- _make_slot_forwarder() / _make_dispatch_rule() replace three copies of
  the subclass-flattening forward path
- _sp_out_leading() / _sp_inp_leading() replace three copies of the
  sequence-parallel leading-dim arithmetic (two of them inverses)
- check_gemm_dims() moves the fp8 dimension checks to utils
- drop the duplicate backward_needs_input assignment in the forward impl

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address review: fix recompile assert, empty-batch sentinel collision, dim checks and cleanups

- check_gemm_dims: restore assert_dim_for_fp8_exec semantics (per-tensor
  leading%8 / last%16, out_features%8 not %16); rich error messages with
  dims on the eager path, constant torch._check messages under compile
  (Dynamo forbids tensor closures in _check message lambdas).
- test_te_linear_dynamic_shapes: the recompile assertion compared a
  nonexistent counter (always 0==0); use stats/unique_graphs and absorb
  the one-time lazy is_fsdp2 hasattr-guard recompile with a warmup.
- custom_op: None-sentinel dtype uint8 -> complex32; a genuinely empty
  FP8 uint8 buffer (batch=0) decoded as None and broke compilation.
- OpaqueValueBundle: type-tag _to_hashable (list/tuple/Size no longer
  compare equal), guard __getattr__ against copy/pickle recursion on
  underscored probes, render non-finite floats evaluably in __fx_repr__.
- Linear.forward: fetch the cuBLAS workspace only after the eager-fallback
  decision; explicit torch._dynamo.graph_break(msg=...) so fullgraph=True
  errors carry the fallback reason instead of breaking on warnings.warn.
- warn_compile_disabled: move the 'use a newer PyTorch build' advice to
  the version-related call sites only.
- Comment/docstring/typography/pylint-disable cleanups in custom_op;
  test cosmetics (use_compile arg name, argparse-time validation of
  --compile/--use-cuda-graphs, merged NVINSPECT skips, docstring fixes);
  export get_cublas_workspace from cpp_extensions.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Address review: simplify check_gemm_dims, trim test comments, restore eager dim asserts

- check_gemm_dims is now a compile-only torch._check guard emitter, called
  from the compiled-op branch; eager dim validation returns to the op impl
  (assert + assert_dim_for_fp8_exec, as on main) so eager pays no overhead
  and keeps full error messages with dims.
- Trim verbose test docstrings/comments (te.Linear section, warmup helper,
  cudagraph-skip helper); describe the dynamic-shape scope (leading dims)
  instead of the fix history.
- Drop the stale 'FP8 with symbolic shapes unsupported' comments: FP8 with a
  mark_dynamic batch works on current nightly (verified: one graph reused
  across batch sizes, numerics match eager).

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Shorten check_gemm_dims docstring

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Drop tensor_can_be_materialized: inline an exact-class check in the two float8 reprs

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Trim paraphrase comments in the Linear fake impls

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Rename SP leading-dim helpers for direction clarity, trim two comments

_sp_out_leading/_sp_inp_leading -> _out_leading_from_inp/_inp_leading_from_out;
shorten the weight_workspace field comment; drop the to_tensor_spec caveat
paragraph.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Tighten custom_op module docstring intro

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Merge and simplify custom_op docstring paragraphs 2-3

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Keep the impl-vs-op contrast as two paragraphs

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Drop reference to a PyTorch PR that will not land

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Shorten _ensure_distributed_opaque_types docstring

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fall back to eager cleanly when ProcessGroup opaque registration is unavailable

PG_REFERENCE_OPAQUE is computed once at import (Dynamo-friendly constant);
compile_unsupported_reason reports a tp_group it cannot carry instead of the
misleading _UnsupportedAdapter TypeError at trace time.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fix leftover 'priority order' wording at _FIELD_ADAPTERS

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Restructure register_custom_op docstring: caller contract first, drop module-docstring duplication

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fix two fake/impl divergences found in multi-agent review

- Backward fake now returns grad_bias whenever bias is used on the FP8
  backward path (grad_output_preprocess computes bgrad independent of
  requires_wgrad); previously a frozen weight silently dropped the bias
  gradient under torch.compile.
- Forward fake now mirrors quantize_weight's workspace invalidation: a
  cached workspace missing buffers for the quantizer's current usage is
  dropped and a fresh new_weight_workspace is declared, instead of always
  assuming a cache hit (previously crashed with an output size/stride
  mismatch when a rowwise-only cache met a training step).

Both verified against eager on RTX Ada.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Drop the global copyreg ProcessGroup reducer

copyreg.pickle is process-wide: with the reducer installed, torch.save of
any object graph reaching a ProcessGroup silently succeeded and the
checkpoint failed only at torch.load (the reconstruct stub raises).
Restore the loud failure at save time; the cost is that inductor bypasses
the FX disk cache for compiled distributed graphs (with its own warning)
until the cache-key pickler handles real opaque objects upstream.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Skip use_compile numerics cases for DelayedScaling

DelayedScaling quantizers are not value-opaque, so the compiled path falls
back to eager, which errors under fullgraph=True.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fix backward fake for UB reduce-scatter dgrad and extend the compiled UB test to FP8

Under ub_overlap_rs_dgrad the impl returns the plain high-precision
reduce-scatter output as dgrad (the grad_input_quantizer only feeds the
communication buffer), while the fake declared a quantized dgrad spec --
an op output-contract mismatch.

test_linear_with_overlap_compile now also runs fp8_current_scaling and
mxfp8 for the column-parallel cases (bulk and DGRAD+RS); FP8 row-parallel
stays skipped (forced differentiable fp8_output is unsupported under
compile) and delayed scaling is excluded like elsewhere.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fall back to eager for DistributedWeight (GTP) under torch.compile

The compiled path handles neither the external weight subclass at the op
boundary nor the materialize/refresh logic in the fakes; gate it in
compile_unsupported_reason like the other unsupported configs.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Widen two eager-fallback conditions

- fsdp_group: fall back regardless of grad mode; the adapter rejects the
  field for any non-trivial value, so inference with manual TE FSDP could
  reach the op and fail there instead.
- fp8_output: also fall back when only the bias requires grad; the
  backward tangent for the quantized output was mis-guessed by AOTAutograd
  (RuntimeError: Expected a Float8Tensor tangent but got a plain Tensor).

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Defer the compile-disabled warning from import time to first compile use

Registration failures now only record the reason; importing TE on a build
without opaque-object support stays silent. The warning is emitted from
Linear.forward when a compiled call finds the op unregistered; under
fullgraph=True the resulting error names warn_if_compile_disabled.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Log the compile-disabled reason at registration time (INFO, TransformerEngine logger)

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Release ctx.backward_objects after the compiled-op backward

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Mirror the save_original_input runtime flip in the forward fake

The impl disables save_original_input when the input quantizer cannot
reconstruct the wgrad operand from the original input (e.g. NVFP4 with
stochastic rounding); the fake kept it on and declared the saved-input
slot as an alias while the impl saved a quantized storage.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fall back to eager for quantized input tensors under torch.compile

The inp field crosses the op boundary as a plain Tensor slot, so a
quantized activation (e.g. the fp8_output of a previous layer) breaks
fake propagation even under no_grad; gate it until the boundary supports
quantized inputs.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Test hardening: exact eager-vs-compiled comparison, counters guard, real microbatch cache checks

- Tolerances tightened to exact: all compute runs inside the op and the
  loss grad is ones, so eager and compiled are bit-identical (measured on
  both compile modes, bf16 and fp8).
- torch._dynamo counters reads degrade with a warning instead of failing
  when the private API changes.
- is_first_microbatch test: eager reference on a separate module (shared
  cache made it unable to catch corruption or rebuilds), structural
  asserts that the compiled step creates the cache and later steps reuse
  the same object, eager priming of FP8 state before tracing (in-graph
  quantizer creation breaks recompiles; upstream Dynamo bug).

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Distributed tests: compare input gradients; exercise cudagraph replay under reduce-overhead

run_numerics now checks dgrad (gathered per parallel mode) in every linear
case; run_layer_with_overlap warms the compiled model up under
reduce-overhead so the measured run replays captured graphs, and asserts
inductor recorded no cudagraph skips.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Address review comments: re-drop the value-equality boilerplate, remove a redundant skip

The a==b/hash/dict-key block and its other_kwargs parametrization were
already removed once (6f66c3e) as covered by the __fx_repr__ round-trip;
a rebase resurrected them. The fp8_available skip in test_te_linear_compiles
is dead: _all_recipes is availability-gated at construction.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fix reduce-overhead UB warmup: mark step boundaries and drop grads between iterations

The warmup iterations kept warmup gradients alive in the cudagraph pool,
tripping cudagraph_trees' check_memory_pool on the next capture
(Detected N tensor(s) in the cudagraph pool not tracked as outputs).

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Carry ProcessGroup through the op boundary by c10d registry name

Replace the reference-opaque ProcessGroup graph input with the pattern
traceable functional collectives use: the adapter ships pg.group_name (a
plain string in the value bundle) and re-resolves the live group from the
c10d registry inside the op, in the same process -- from_slots(to_slots(pg))
is the identical object by construction.

This removes the opaque PG from example_inputs entirely, so inductor's FX
disk cache works for compiled distributed graphs again (verified: second
process gets fxgraph_cache_hit=2, no pickle bypass) without any upstream
change. The reference-opaque registration machinery and the
PG_REFERENCE_OPAQUE fallback gate are no longer needed.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Add train/eval mode-switch test as xfail

Blocked by an upstream Dynamo bug: FP8 state created inside the first
compiled call comes back as FakeScriptObject/None in the graph outputs,
so any later recompile crashes.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Remove dynamic=False from distributed compile runners

Each parametrized case resets dynamo and compiles once with a single
shape, so automatic dynamic shapes never trigger; verified on 4xGB200
(run_numerics 126/126, comm-GEMM overlap compile suite green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Eagerly pre-allocate cuBLAS workspaces in Linear.reset_parameters

The previous fetch in forward never executed for real: Dynamo ignores
the lru_cache wrapper and traces the wrapped function, so the workspace
was first allocated by the op impl at runtime - under reduce-overhead
on capture-first torch builds that lands in the CUDA-graph pool and
trips 'cudagraph pool not tracked as outputs'. Allocate both variants
(plain and UB) in reset_parameters, which always runs eagerly, drop the
dead cublas_workspace bundle field, and fail fast if a workspace would
first be allocated during stream capture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* [PyTorch] [torch.compile] Address trivial review comments

- drop unused Float8Quantizer import; import all quantizers from their
  tensor modules
- import the local tests utils.py by explicit sys.path so a cutedsl
  top-level utils package cannot shadow it
- cache OpaqueValueBundle hash at construction
- guard is_simple_value when the opaque-object API is unavailable
- inline the one-line _unflatten_value helper
- point the --compile/--use-cuda-graphs error at --compile-mode
  reduce-overhead

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Reference the upstream PyTorch fixes in the train/eval xfail

pytorch/pytorch#187041; #187057 fixes the cold-compile path (merged),
#193190 fixes the FX-graph-cache-hit path (in review). Both verified
against this test on nightly 2.15.0.dev20260815.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fold quantizers into the shared simple-value bundle

The dedicated _QuantizerAdapter only existed because the bundle matches
fields by annotation and quantizer fields are annotated with the abstract
Quantizer base, which is not a registered opaque type itself. Match the
base class in _SimpleBundleAdapter instead and drop the adapter; the
per-quantizer schema slots carried no gradients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Make the eager-fallback warnings actually fire under torch.compile

Dynamo silently drops warnings.warn in traced code, and the graph break
inside the forward's try/finally makes it skip the whole frame and re-run
it with is_compiling() == False, so neither warn_compile_eager_fallback
nor warn_if_compile_disabled ever emitted. Emit them at trace time via
torch._dynamo.comptime instead (once per compilation) and warn before the
explicit graph break, which ends the trace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Test the eager-fallback paths of the compiled Linear op

Parametrized test over the single-GPU-constructible reasons rejected by
compile_unsupported_reason (differentiable fp8_output, wgrad fusion /
delay, quantized input): fallback warning fires, numerics match eager,
fullgraph=True fails with the explicit reason. Delayed scaling is a hard
error under fullgraph (check_recipe_support) and is tested separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fold the compile-aware branching into the warn helper

Both warn functions duplicated the is_compiling()/comptime dispatch;
move it into _compile_safe_warn so callers just pass the message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Simplify adapter selection and drop the bundle's __getattr__

Replace the _FIELD_ADAPTERS registry + per-class try_build with a single
_build_field_adapter factory dispatching on the annotation. Remove
OpaqueValueBundle.__getattr__: nothing uses attribute access (consumers
go through __getitem__/get/as_dict), and without it default
copy/deepcopy/pickle work with no special-casing -- which matters since
Dynamo's guard machinery deepcopies value-opaque objects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Trim OpaqueValueBundle to its actual consumers

Drop get(): the __kind__ tag is set at every construction site, so plain
indexing (loud KeyError) is the right access. _storage_unflatten's only
caller always passes a bundle, so drop the dead dict(meta) branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fold ProcessGroup fields into the shared simple-value bundle

A live group can't cross as a value, so the bundle stores its c10d
registry name and the op re-resolves it (same scheme the dedicated
adapter used). Drops _ProcessGroupAdapter and the tp_group__pg schema
slot; per-field adapters are now only the two tensor kinds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Reset FP8 global state between torch.compile tests

Tests merged from main leave pending delayed-scaling amax reductions in
FP8GlobalStateManager; a later autocast __exit__ then calls raw tex
bindings, graph-breaking the fullgraph=True Linear tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Rewrite the custom-op arg boundary as a parsed plan

Parse the args dataclass's annotations once, at registration, into an
immutable _ArgPlan: per-field _FieldPlan records (_FieldKind + schema
slots) plus the derived layout -- schema string, slot order, gradient
placement, tensor-or-quantized offsets -- with duplicate-slot-name
validation. pack/unpack interpret the plan on each call. Replaces the
adapter classes and the four layout helpers that each re-walked them;
the op schema and Linear semantics are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Add an output plan and lift the single-grad-output limit

Parse the fwd fake-impl result into a per-trace _OutputPlan (logical
outputs / saved tensors with their flat Tensor[] ranges) and use it as
the single structure behind forward_fn, setup_context and backward.
Backward now slices grads per user output from the plan stashed on ctx:
a grad_outputs field on the backward args receives the whole tuple,
otherwise grad_output receives the first output's grad -- removing the
flat_grads[0] single-output assumption. Also reject unions mixing
tensor types with other members at registration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Trim derived state from the plans

tq offsets join grad targets as on-demand derivations from fields; the
shared-bundle slot presence is implied by the packed dict itself; only
the output ranges (not the whole output plan, which references specs
and their quantizers) are stashed on ctx for backward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fix post-merge breakage: _linear_backward rename and NVFP4 swizzle scale dtype

- fused_mla_q_uproj: use _linear_backward_impl (renamed in this branch)
- swizzle_scales_for_gemm: allocate swizzled scale buffers as uint8 so the
  python-visible scale_inv dtype matches quantizer allocations (the compiled
  op's fake declares uint8; the e4m3-dtyped buffer broke NVFP4 under
  torch.compile on Blackwell)
- silence pylint false positive on type.__new__ via attribute

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Trim swizzle.cpp comments

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Derive tensor_field_names from fields instead of storing it on _ArgPlan

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fall back to eager for FP8 weight caching under torch.compile

The cached weight workspace is updated in place on the first microbatch,
which the functional custom op (mutates_args=()) can't express. Covers
skip_fp8_weight_update too (it implies caching).

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Strengthen compile tests: bias grads in the main matrix, stateful side effects in fallback cases

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Narrow NVTE_TORCH_COMPILE doc to its actual scope (internal jit fusions)

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fix rank-1 input under torch.compile

The fwd fake declared (inp.shape[0], out_features) while the real impl views
the output to (1, out_features) for 1D inputs. The bwd impl rederives the
input shape from grad_output, which cannot recover rank-1, so the autograd
glue now stashes the true input shapes on ctx (SymInt-safe) and views the
returned grads back to them.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Drop is_first_microbatch from the train/eval switch test

Weight caching now falls back to eager under torch.compile, so with the
argument the xfail test never reached the train/eval switching it covers.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Bwd fake: dgrad is a plain tensor under ub_bulk_wgrad too

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Limit the weight-caching compile fallback to FP8

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fall back to eager for fp8_grad=True under torch.compile

A quantized dgrad can't cross the op boundary (grads are packed one plain
Tensor[] slot each), so AOT tracing crashed instead of falling back.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Protect the custom ops from dead-code elimination

An op with an unused output is legally DCE'd (mutates_args=()), silently
dropping its collectives and state updates. Register the ops in FX's
side-effect registry by default; NVTE_COMPILE_OP_SIDE_EFFECTS selects
token (ordered effect tokens, blocks reordering too, but incompatible
with cudagraph trees today) or 0 (off).

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Preserve _transpose_invalid across the flatten/unflatten round trip

An allocated transpose may be stale (invalidated by _reset_caches);
reconstruction derived validity from presence and silently revalidated
it, so the compiled path could consume a pre-update transpose.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Rework cuBLAS workspace preallocation

- skip under fake tensors/modes (a fake workspace poisoned the
  process-global cache)
- also preallocate in _apply, covering .to()/.cuda()/to_empty() flows
  (meta-device init never ran the reset_parameters path)
- preallocate the UB workspace only when comm overlap is actually
  enabled, not on ub_name alone (MHA sets it unconditionally)
- drop the stream-capture assert: it broke previously-working eager
  CUDA-graph captures, while the cudagraph-trees hazard it aimed at
  (warmup-pool allocation) never triggers it anyway

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Decide the compile fallback before the try/finally in Linear.forward

A graph break inside try/finally cannot build a resume function, so
Dynamo marked the shared Linear.forward code object SKIP: one
unsupported config silently reverted every te.Linear in the process to
eager. Hoist the config checks into _compile_eager_fallback_reason and
exit through a dynamo-disabled eager re-entry; only quantizer-dependent
conditions remain in the late check.

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

* Fix stale amax groups in compiled Linear

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>

---------

Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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.

3 participants