[PyTorch] torch.compile for an OperationFuser group holding one operation - #3496
Draft
pggPL wants to merge 6 commits into
Draft
[PyTorch] torch.compile for an OperationFuser group holding one operation#3496pggPL wants to merge 6 commits into
pggPL wants to merge 6 commits into
Conversation
register_custom_op now defines an operation's forward and backward as two independent two-tier custom ops and hands back both, leaving autograd to the caller. That is what lets a pipeline-level autograd.Function decide how the two are wired, and so group the forward and backward passes differently -- which is what ops.OperationFuser does. The variant that wires autograd itself keeps the old behaviour under register_custom_op_with_autograd, and is now built on the same registration: the pair is the primitive, autograd is what the other one adds. About two thirds of the two bodies were the same code before. BasicOperation gains the plumbing an operation needs to opt in: declare two argument containers and implement four compute classmethods, and __init_subclass__ registers the custom ops while op_forward / op_backward are written once in the base. compile_unsupported_reason lets an operation say why it cannot be compiled -- it sits here rather than on the args, as Linear has it, because in ops/ the compile boundary is the fuser group, not the operation. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
A group whose operations declare their compute halves now runs through their custom ops under torch.compile(fullgraph=True). The pipeline-level autograd.Function is traced as a higher-order op, which is what will later let its forward and backward walk different op groupings. Four side effects reached outside the higher-order op's scope and had to go: - OperationContext objects are created in the forward, but the backward is a separate subgraph, so writing to them there mutates an enclosing scope; the backward copies them into its own scope instead; - requires_grad_ on an output, which AOTAutograd's functionalization drops anyway -- autograd marks the outputs of an apply() itself; - _do_not_clear on inputs and outputs. They are gated on being traced rather than on using the custom ops. Under fullgraph there is no leaving the graph, so an unsupported operation does not fall back: the pipeline is traced either way and only the choice of implementation changes. The gate reports why a group runs eagerly through warn_compile_eager_fallback, which is safe to call from the traced region. Sequential builds its module groups outside the forward pass, since that constructs nn.Modules. Tested with a test-only operation, so the fuser's path does not depend on which real operations happen to declare their halves. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
An operation lists the forward kwargs it takes in fwd_kwarg_names. They are resolved into its args container like any other config, in the traced Python where Dynamo guards them, so they reach the custom op through the existing schema -- a value is guarded, a tensor is lifted into the graph, and a quantized one crosses as its inner buffers. An undeclared kwarg still sends the whole group to eager. That is not a schema limitation, as the old message implied: the kwargs that remain are the grouped operations' preallocated buffers, which the op writes to, and a custom op may not mutate a tensor from an enclosing scope. A kwarg carries no gradient. This matches the eager path, where kwargs never entered the autograd graph either, and is why only read-only ones are accepted. The fuser test helper now builds a separate model for the eager and the compiled pass. Previously both shared one model and the eager pass ran first to produce the reference, so the compiled pass was always traced on a model whose module groups, fusions and pre_first_fuser_forward had already run. Those paths are now traced as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
A value kwarg does not survive a second call. The other fields of an args container are read off the module and are constant across calls, so they are baked into the graph; a kwarg changes per call, and on the second value Dynamo hands over a symbolic scalar, which OpaqueValueBundle cannot carry -- it fails with AsPythonConstantNotImplementedError, not with a graph break. Measured on int and float alike; specialize_float=True cures only the float, and is global. The gate now takes tensor kwargs only, so a value sends the group to eager deterministically instead of failing on its second call. A 0-d tensor is the way to pass a scalar: it is a graph input, so it recompiles for no value at all. The test carries a quantized offset that changes on every call and confirms no recompilation, then adds a value kwarg to cover the gated path. That last call keeps its offset unquantized on purpose: a gated group runs the eager implementation, which is traced directly rather than hidden behind a custom op, and dequantize() graph-breaks there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
The forward/backward pair was the registration primitive, returned as an _OpPair carrying every object the function had created, because the autograd-wired variant finished the registration outside it. Forward and backward are registered almost identically, so the primitive is now one op: _register_op returns a _RegisteredOp with the plan and the base/wrapper handles, and the two entry points register the pair as two calls. This also makes a forward-only op possible later without a placeholder backward. ForwardResult is gone: the autograd-free forward returns a plain (output, aux) tuple, symmetric with the backward. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
First step towards
torch.compile(fullgraph=True)support fortransformer_engine.pytorch.ops.The approach: keep the pipeline-level
_OperationFuserAutogradFunctionand let Dynamo trace it as a higher-order op, with each fusible operation calling its own custom op inside. That keeps the forward and backward fusion layouts independent (so the backward-only fusions survive), keepsOperationContextinside the traced scope, and bounds op registration to one entry per op class.This PR makes that work for an
OperationFusergroup holding one operation. It converts no real operation: the fuser's path is exercised by a test-only operation, so it does not depend on which operations happen to declare their compute halves. Converting the operations themselves is a follow-up.Type of change
Changes
register_custom_opnow defines an operation's forward and backward as two independent custom ops and hands back both, leaving autograd to the caller. That is what lets a pipeline-levelautograd.Functiondecide how the two are wired. The variant that wires autograd itself keeps the old behaviour underregister_custom_op_with_autograd. Both are built on a single-op primitive (_register_opreturning a_RegisteredOp), so the pair is two calls and a forward-only op is possible later. The autograd-free forward returns a plain(output, aux)tuple, whereauxholds only fresh tensors produced inside the op.BasicOperationgains the plumbing an operation needs to opt in: declare two argument containers and implement four compute classmethods, and__init_subclass__registers the custom ops whileop_forward/op_backwardare written once in the base.setup_context(ctx, args, aux)decides what to save for backward, so inputs and parameters (which a custom op may not return) are saved fromargs.compile_unsupported_reasonlets an operation say why it cannot be compiled; it sits on the operation rather than on the args because inops/the compile boundary is the fuser group.OperationFuserruns a supported group through its operations' custom ops under compile. An operation may declare read-only forward kwargs (fwd_kwarg_names); they are resolved into its args container like any other config. Only tensor kwargs are accepted on the compiled path: a value kwarg becomes a symbolic scalar on its second value, which the opaque value bundle cannot carry, so it sends the group to eager deterministically instead. A 0-d tensor is the way to pass a scalar.Side effects that had to go
All of them reached outside the higher-order op's scope:
OperationContextobjects are created in the forward, but the backward is a separate subgraph, so writing to them there mutates an enclosing scope. The backward copies them into its own scope.requires_grad_on an output: AOTAutograd's functionalization drops it anyway, and autograd marks the outputs of anapply()itself._do_not_clearon inputs and outputs.They are gated on being traced, not on using the custom ops. Under
fullgraph=Truethere is no leaving the graph, so an unsupported operation does not "fall back": the pipeline is traced either way and only the choice of implementation changes. The gate reports its reason throughwarn_compile_eager_fallback, which is safe to call from the traced region.Testing
test_torch_compile.pygains two test-only operations and five tests: a single-operation group compiles and matches eager (output, input gradient, parameter gradient); a backward-only fusion sends the group to the eager implementations;setup_contextsaves the parameter so a mutation between forward and backward is detected; an operation without the compute halves still runs, on its eager implementation; and a quantized tensor forward kwarg reaches the op through its custom op, changes between calls without a recompilation, while a value kwarg is gated onto eager.test_torch_compile.py: 123 passed, 46 skipped, 1 xpassed.test_fusible_ops.pyunchanged. RTX Ada.Gated out and untouched: multi-operation groups, fused operations, operations with extra tensor inputs/outputs, grouped operations, userbuffers, delayed scaling, FP8 block scaling.
Checklist: