[PyTorch] [torch.compile] torch.compile support for Linear - #3053
Conversation
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>
98cd401 to
c6544d0
Compare
for more information, see https://pre-commit.ci
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>
Greptile SummaryThe PR adds full-graph
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains from the previously reported review findings. Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Linear
participant Wrapper as Custom-op wrapper
participant Base as Base custom op
participant Fake as TensorSpec fake implementation
participant CUDA as TE GEMM implementation
participant Autograd
User->>Linear: forward(input)
Linear->>Wrapper: flattened argument bundle
Wrapper->>Fake: describe outputs and saved tensors
Fake-->>Wrapper: TensorSpec structures
Wrapper->>Base: plain tensors, inner buffers, opaque values
Base->>CUDA: execute Linear forward
CUDA-->>Base: output, workspace, saved payload
Base-->>Linear: reconstructed outputs
Linear-->>User: output
User->>Autograd: backward(grad_output)
Autograd->>Base: reconstructed backward bundle
Base->>CUDA: execute Linear backward
CUDA-->>Autograd: weight, input, and bias gradients
Reviews (4): Last reviewed commit: "Restructure register_custom_op docstring..." | Re-trigger Greptile |
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>
for more information, see https://pre-commit.ci
… 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>
… 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>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…wo float8 reprs Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
_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>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…navailable 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>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
… module-docstring duplication Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
| if args.compile and args.use_cuda_graphs: | ||
| parser.error("--compile and --use-cuda-graphs are mutually exclusive.") |
There was a problem hiding this comment.
So how do we test the lower overhead mode? Shouldn't we just do that mode of the compilation if this
option is set?
| # Each parametrized case compiles the same module.forward code object with | ||
| # a different shape/recipe; with dynamic=False those guards accumulate and | ||
| # eventually trip Dynamo's recompile_limit. Reset so every case starts from | ||
| # a clean compile cache (mirrors the single-GPU torch.compile tests). |
There was a problem hiding this comment.
One concern here is that when more things are going to try testing the compilation flow we would
spend a lot of time in the compilation during the CI. Maybe it would be good to have some thoughts
on which tests should actually be run in L0 and which could be done in the nightly CI for torch.compile.
| # dynamic=False for now: a symbolic shape would land in an OpaqueValueBundle | ||
| # (value-opaque op arg) whose hash chokes on non-nested SymInt. Force static | ||
| # shapes (recompile per shape) until the bundle handles symbolic shapes. |
There was a problem hiding this comment.
Is the dynamic=False a limitation that we require users to set? If so, we need to have some documentation
about that somewhere.
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("compile_mode", ["default", "reduce-overhead"]) |
There was a problem hiding this comment.
Kind of a general comment, but do we expect to ever see a case that would work under reduce
overhead mode but not work under the default mode? If so then maybe we could just test the stricter
mode if things are supposed to work under both of them?
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def _assert_no_cudagraph_skips(enabled: bool): |
There was a problem hiding this comment.
Generally speaking I think it makes sense, but since the counters map is technically an internal
thing in PyTorch, I think that we should guard ourselves a little bit in case the API changes.
E.g. our CI should not fail (but probably issue some kind of warning?) if the counters map is not
there or the specific keys are not found.
|
|
||
|
|
||
| # bf16 output tolerance: eager and compiled run the same kernels, so they should | ||
| # agree closely; the slack only absorbs reduction-order / cuda-graph differences. |
There was a problem hiding this comment.
CUDA graphs and non-CUDA graph execution should actually be exactly identical. The compiled vs eager
could have some difference if you capture more that just TE, but should not have any difference in
the TE ops themselves.
| pytest.param(_blockwise, id="float8_blockwise"), | ||
| pytest.param(_current_scaling, id="float8_current_scaling"), | ||
| pytest.param(_nvfp4, id="nvfp4"), | ||
| pytest.param(_mxfp8, {"dtype": tex.DType.kFloat8E5M2}, id="mxfp8"), |
| a, b = factory(), factory() | ||
| # Same config -> equal, same hash, interchangeable as a dict/set key. | ||
| assert a is not b | ||
| assert a == b | ||
| assert hash(a) == hash(b) | ||
| assert {a: "x"}[b] == "x" | ||
| # Different config -> not equal. | ||
| assert a != factory(**other_kwargs) |
There was a problem hiding this comment.
I think this is an artifact of the rebase or something, I vaguely recall we got rid of something
very similar from the previous PR.
| if fp8_recipe is not None and not fp8_available: | ||
| pytest.skip(reason_for_no_fp8) |
There was a problem hiding this comment.
The fp8_recipe takes values from all_recipes which already skips unavailable recipes, so this
check is useless.
| for is_first in schedule: | ||
| base = torch.randn(32, 64, dtype=dtype, device=device) | ||
| _assert_close_eager_compiled(fn, compiled, model, base) | ||
|
|
There was a problem hiding this comment.
A similar test that should be here is to run the Linear in training and eval modes back to back to
see whether changing from training -> inference (and more importantly from inference -> training)
works well.
Description
This PR adds
torch.compilesupport forte.pytorch.Linear, building on theTensorSpecmechanism already inmain._Linear's forward and backward are registered astorch.librarycustom ops, so a module containingte.Lineartraces undertorch.compile(fullgraph=True)without graph breaks. The fake (meta) implementations describe the produced tensors throughTensorSpecinstead of allocating them, which is what makes the quantized outputs traceable — the compiler sees the full quantized-tensor structure (data, scales, transposes) without any device allocation at trace time.The bulk of the diff is
transformer_engine/pytorch/dynamo/custom_op.py: a declarativeregister_custom_ophelper. Custom ops require flat lists of tensors, while the TE forward/backward take dataclass "argument bundles" holding tensors, quantized tensors, quantizers, process groups and plain Python values. The helper derives the op schema from the dataclass field annotations, flattens each field to op slots via a per-kind adapter, and rebuilds the bundle on the other side, so ops are declared by writing a dataclass rather than by hand-maintaining a schema string.Type of change
Changes
dynamo/custom_op.py(new):register_custom_op— declarative registration of forward/backward custom ops from dataclass argument bundles. Handles per-field adapters for plain tensors, quantized tensors, quantizers, opaque value bundles and reference-opaque types (e.g. process groups), schema generation,TensorSpec-based fake outputs and autograd wiring. Falls back to eager with a single warning if registration fails.dynamo/__init__.py: exportregister_custom_op.module/linear.py: split the forward into pure computation and context saving, add allocation-free fake forward/backward onTensorSpec, and register_Linearthroughregister_custom_op. Eager behavior is unchanged.dynamo/quantizer_opaque.py,dynamo/tensor_spec.py,tensor/_quantization_helpers.py,tensor/float8_tensor.py,tensor/storage/float8_tensor_storage.py,utils.py: small supporting changes (idempotent spec conversion, weight-workspace quantizer preservation, keeping attributes attached to quantized parameters across_apply).tests/pytorch/test_torch_compile.py: coverage for the compiled Linear — fullgraph compilation, quantized FP8 weights, FP8 output,is_first_microbatch, dynamic shapes, parametrized over the supported recipes (FP8 per-tensor/current scaling, MXFP8, NVFP4).tests/pytorch/distributed/*: exercise the compiled path in the distributed numerics and comm-GEMM-overlap runs.Checklist: