[PyTorch] torch.compile for an OperationFuser group holding one operation - #45
Open
pggPL wants to merge 21 commits into
Open
[PyTorch] torch.compile for an OperationFuser group holding one operation#45pggPL wants to merge 21 commits into
pggPL wants to merge 21 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>
num_grad_inputs leaves BasicOperation: on the autograd-free path it only validated the length of backward_compute's result, and a default of 1 was wrong for every op with a parameter. register_custom_op takes it as an optional check. fwd_kwarg_names is read off resolve_fwd_args's keyword-only parameters instead of being declared twice. The fuser gate now matches the title: a group of several operations runs the eager implementations. Multi-op groups without fusions were slipping through. The saved_tensors reset in backward no longer skips compile; the contexts are copies local to that subgraph, so the write is allowed, and gating it only suggested a constraint that is not there. Drop test_te_ops_setup_context_saves_parameter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
op_forward / compiled_op_forward and op_backward / compiled_op_backward share one body each, parametrized by the compute callable. Registration moves into _register_compile_ops. The base resolve_fwd_args no longer takes **kwargs, which the registration forbids on subclasses. Redundant checks and long comments removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
forward_compute / backward_compute become forward_impl / backward_impl, matching module/linear.py and register_custom_op's parameters. The fixed resolve_fwd_args parameters are read off the base signature instead of a separate constant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Skip tensor storage cleanup while tracing eager operation bodies. Reject delayed-scaling state in the fuser, including CustomRecipe, before compiled execution can omit its backward scale update. Add regression coverage for input preservation and built-in/custom delayed-scaling rejection after eager warmup. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Remove the test additions from 2f8582f at the requested scope. Keep both production fixes. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Move argument packing, compute dispatch, and context saving into FusibleOperation. Preserve nested custom-op results for extra outputs and gradients, while retaining existing pipeline compile gates. Validate shared fused interfaces and custom-op graph nodes. Workstation tests: 1620 passed, 1039 skipped, 1 xpassed; full Python lint reports only existing fused_mla_q_uproj diagnostics. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
for more information, see https://pre-commit.ci
Make register_custom_op accept one argument type, implementation, and fake implementation. Let FusibleOperation call it independently for forward and backward. Preserve nested tensor results through the same adapter in either direction. Validation: 13 focused compile tests passed; standalone eager/fullgraph op ran without a backward registration; changed-file pylint passed. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Inline forward/backward registration and forward result reconstruction in the autograd-wired adapter. Preserve registration options and gradient validation while removing three single-use helpers. Validation: 13 focused ops/Linear compile tests passed; changed-file pylint and formatting checks passed. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Remove flatten_in_body and always give wrapper bodies the same quantized-input slot adapter used by their dispatch rules. Already packed storage and ordinary tensors pass through unchanged. Validation: test_torch_compile.py 126 passed, 46 skipped, 1 xpassed. Six standalone autograd cases passed for ordinary tensors, FP8 wrappers, and bare FP8 storage in eager and Inductor. Pylint and repository Black checks passed. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Replace duplicated scale operations with one affine op and use a fixed fused contract. Compare outputs and random-gradient backward results against native PyTorch autograd. Parameterize FP32/BF16 pipeline cases and assert custom-op graph selection and reuse across tensor kwargs. Reduce the ops test section from 497 to 297 lines. Use exactly representable BF16 products to avoid eager versus fused-reduction rounding differences. Validation: test_torch_compile.py 128 passed, 46 skipped, 1 xpassed; repository Black and diff checks passed. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com>
…fely Raise on duplicate custom-op names instead of replacing registered kernels. Save bare quantized storage as buffers and metadata while compiling, then reconstruct a fresh storage object in backward. Validation: 8 focused regression tests passed; changed-file Black and pylint passed. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.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 have a custom op. Converting the operations themselves is a follow-up.Type of change
What an operation implements
A
BasicOperationopts in by declaring two argument containers and implementing the methods below.op_forward/op_backwardare then inherited from the base class; the operation does not write any custom-op plumbing itself.Declarations
fwd_args_type,bwd_args_type: dataclasses. Their fields define the custom-op schema (tensors, quantized tensors, quantizers, plain values), exactly asLinearFwdArgs/LinearBwdArgsdo forte.Linear.Traced part (runs in the Dynamo-traced region, may read
self, module state andctx)resolve_fwd_args(input_, *, requires_grad, prev_op_grad_output_quantizer, next_op_input_quantizer, <kwargs>) -> fwd_args_type: gathers everything the forward needs into the container. Forward kwargs the operation accepts are declared as additional keyword-only parameters with defaults;fwd_kwarg_namesis derived from this signature.resolve_bwd_args(ctx, grad_output) -> bwd_args_type: rebuilds the backward's inputs from the saved context.setup_context(ctx, args, aux)(optional): decides what to save for backward. The default savesaux. An operation whose backward needs an input or a parameter saves it here fromargs, because a custom op may not return one of its own inputs.Custom op implementation (classmethods; read nothing but
args, mutate nothing)forward_impl(args) -> (output, aux): the forward.auxis a tuple of fresh tensors produced inside the op that the backward needs (norm statistics, a quantized copy of the input). Inputs and parameters are notaux.forward_fake(args) -> (output, aux): the same overTensorSpec, allocation-free.backward_impl(args) -> (grad_input, *grad_params): the backward.grad_inputmay beNone, meaning "grad_output, unchanged".backward_fake(args): itsTensorSpectwin.The
*_implmethods are called directly on the eager path and are the bodies of the registered custom ops on the compiled path, so there is one implementation of the math. Because they are custom-op bodies, they may not mutate their arguments: noclear_tensor_dataon inputs or saved tensors, no in-place writes to tensors from an enclosing scope.Derived by the base class
compile_ops: the registered(forward_fn, backward_fn)pair, one registration per class, under the lower-cased class name.fwd_kwarg_names: the extra keyword-only parameters ofresolve_fwd_args.compile_unsupported_reason():None, or why the operation must run its eager implementation. The base class rejects operations without a custom op and quantizers that are not value-opaque (delayed scaling).Changes
register_custom_opnow defines an operation's forward and backward as two independent custom ops and returns both, leaving autograd to the caller. 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.BasicOperationgains the contract above;__init_subclass__registers the custom op once per class.OperationFuserdecides once per group whether to run its operations' custom ops. The group runs its eager implementations when: it holds more than one operation, either pass has a fusion, an operation has extra tensor inputs/outputs, a kwarg is undeclared or not a tensor, or an operation reportscompile_unsupported_reason. Only tensor kwargs are accepted because a value kwarg becomes a symbolic scalar on its second value, which the opaque value bundle cannot carry; 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:fullgraph=True;test_torch_compile.py+test_fusible_ops.py(without grouped / userbuffers cases): 1616 passed, 1039 skipped, 1 xpassed. 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: