Skip to content

Save mutated arguments after the kernel call in generated autograd kernels - #193758

Open
yinjiew wants to merge 1 commit into
pytorch:mainfrom
yinjiew:fix/rrelu-saved-noise
Open

Save mutated arguments after the kernel call in generated autograd kernels#193758
yinjiew wants to merge 1 commit into
pytorch:mainfrom
yinjiew:fix/rrelu-saved-noise

Conversation

@yinjiew

@yinjiew yinjiew commented Aug 17, 2026

Copy link
Copy Markdown

Issue

Fixes #193671

Summary

Root cause and discussion are in the issue. In short: aten::rrelu_with_noise
takes noise as a mutable output argument, and the generated autograd kernel
saved it before the kernel filled it. That works only while the SavedVariable
aliases the buffer; a hook that materializes at pack time, or a non-reentrant
checkpoint replay that early-stops at that pack point, captures uninitialized
memory instead and backward silently scales gradients by garbage. This moves the
SavedVariable construction for operator-mutated arguments to after the call.

Only four generated kernels change: rrelu_with_noise, rrelu_with_noise_,
_native_batch_norm_legit and _batch_norm_with_update. The two batch norm ops
read the running stats in backward only when training=False, when the kernel
does not update them, so they are numerically unaffected.

Also fixes the rrelu_with_noise_functional derivative, which referenced the
untouched noise input rather than the noise_out return.

One thing worth flagging for review: the new save site does not apply the
guard_for predicate that emit_save_inputs uses to skip saving tensors a
backward will not need, because guard_for is local to that closure. It is a
no-op for all four ops today (rrelu has a single arg with a derivative, so
guard_for bails on len(args_with_derivatives) <= 1; the batch norm formulas
are multi-output without wrap_opt_if). Happy to hoist guard_for to
emit_body scope if you would rather have it applied uniformly.

Reproducer, before / after

Reproducer from the issue, on a CPU source build of 8f988c9c.

Before, five consecutive runs:

torch 2.15.0a0+git8f988c9
checkpoint    : 0.0
saved hooks   : nan
no early stop : 0.0
forward diff  : 0.0
--- run 1 ---  checkpoint : 1.2183011048223146e+20   saved hooks : 1.2183011048223146e+20
--- run 2 ---  checkpoint : 5.938071552281598e+34    saved hooks : 1.0
--- run 3 ---  checkpoint : 2.457677367050863e+21    saved hooks : 2.458235813404657e+21
--- run 4 ---  checkpoint : 6.766431746933327e+24    saved hooks : 6.767575445065897e+24
(no early stop : 0.0 on every run)

After, same five runs:

torch 2.15.0a0+git8f988c9
checkpoint    : 0.0
saved hooks   : 0.0
no early stop : 0.0
forward diff  : 0.0
(all five runs identical)

Checklist

  • Passes lint (lintrunner reports no issues on the changed files)
  • Added/updated tests
  • Updated documentation (if applicable)
  • Included benchmark results (for PRs impacting perf)

BC-breaking?

No.

@pytorch-bot

pytorch-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/193758

Note: Links to docs will display an error until the docs builds have been completed.

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@pytorch-bot pytorch-bot Bot added the release notes: nn release notes category label Aug 17, 2026
@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 17, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via:

Co-authored-by: name <email>

Supported Co-authored-by: formats include:

  1. Anything <id+login@users.noreply.github.com> - it will locate your GitHub user by id part.
  2. Anything <login@users.noreply.github.com> - it will locate your GitHub user by login part.
  3. Anything <public-email> - it will locate your GitHub user by public-email part. Note that this email must be made public on Github.
  4. Anything <other-email> - it will locate your GitHub user by other-email part but only if that email was used before for any other CLA as a main commit author.
  5. login <any-valid-email> - it will locate your GitHub user by login part, note that login part must be at least 3 characters long.

Alternatively, if the co-author should not be included, remove the Co-authored-by: line from the commit message.

Please update your commit message(s) by doing git commit --amend and then git push [--force] and then request re-running CLA check via commenting on this pull request:

/easycla

@yinjiew

yinjiew commented Aug 17, 2026

Copy link
Copy Markdown
Author

@pytorchbot label "ciflow/trunk"

@pytorch-bot

pytorch-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

The ciflow label(s) ciflow/trunk will be added, but CI won't be triggered until the workflows are approved (scroll to the bottom of this page).

Please ping one of the reviewers if you do not have access to approve and run workflows.

@pytorch-bot pytorch-bot Bot added the ciflow/trunk Trigger trunk jobs on your pull request label Aug 17, 2026
@pytorch-bot

pytorch-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

The following ciflow label(s) have been added but CI has not been triggered yet because the workflows are awaiting approval:

  • ciflow/trunk

Once a maintainer approves the workflows (scroll to the bottom of the PR page), the corresponding CI jobs will be triggered automatically. Please ping one of the reviewers if you do not have access to approve and run workflows.

…rnels

aten::rrelu_with_noise takes `noise` as a mutable output argument: the kernel
writes the sampled negative slopes into it, and rrelu_with_noise_backward scales
the incoming gradient with them. The generated autograd kernel built
SavedVariable(noise, false) in the setup_derivative block, before the kernel
call, so the packed value was the buffer's pre-call content.

Without saved tensor hooks that is harmless, because the SavedVariable holds on
to the argument and therefore aliases the buffer the kernel fills. With a hook
that materializes at pack time it is not. save_on_cpu copies uninitialized
memory. Non-reentrant checkpointing is worse: during recomputation the pack for
`noise` raises _StopRecomputationError once the saved-tensor counter reaches the
count from the original forward, so when `noise` is among the last saved tensors
of a segment the replay is aborted before the kernel runs and the buffer is
never filled. Backward then scales gradients by uninitialized memory while the
forward output stays bit-exact, so no RNG or output probe can see it. Because
rrelu_with_noise is SchemaKind.mutable rather than inplace/out, no
increment_version is emitted for `noise` and SavedVariable's staleness check
cannot catch it either.

Save arguments that the operator mutates after the call instead. That is the
value backward already reads today through aliasing, so the no-hook path is
unchanged. The filter uses post_self_positional_mutable, which excludes the
`self` argument of in-place ops; that one is already handled by original_self
and the saved outputs.

The derivative for the autogenerated rrelu_with_noise_functional also referred
to the `noise` input, which that op never writes: it clones the argument and
returns the filled clone as `noise_out`. Changed to use noise_out.

Besides the two rrelu ops, this moves the save point for running_mean and
running_var in _native_batch_norm_legit and _batch_norm_with_update, the only
other ops in derivatives.yaml with a saved mutated argument. Their backward
reads the running stats only when training=False, and the kernel does not update
them in that case, so there is no numerical change; the no-hook path already
observed the post-update values.

I considered the narrower fix of routing at::native::rrelu through
rrelu_with_noise_functional instead. It does not cover nn.RReLU(inplace=True),
which goes through rrelu_ -> rrelu_with_noise_ and has the same problem, without
adding an extra copy, so I fixed the save point in codegen instead.

The new tests pass a sentinel value as `noise` rather than relying on F.rrelu's
empty_like buffer. A stale read of that buffer is uninitialized memory, which can
happen to hold the correct slopes when the allocator hands back the block the
reference run just freed, so the sentinel is what makes the regression
deterministic. With it, all four failing tests report the same
6.898226737976074 maximum deviation on every run.

Test Plan:

```
python test/test_nn.py -k rrelu
python test/test_nn.py -k batchnorm
python test/test_autograd.py -k saved_tensor
python test/test_autograd.py -k checkpoint
python test/functorch/test_aotdispatch.py -k rrelu
python test/test_ops.py -k rrelu
python test/test_ops_gradients.py -k rrelu
python test/test_modules.py -k BatchNorm
python test/test_decomp.py -k batch_norm
```

All pass on a CPU build, with one exception that is not related to this change:
test_batchnorm_nhwc_cpu fails identically with and without it, down to the same
2.3365020751953125e-05 deviation at index 0. It compares channels_last against
contiguous BatchNorm3d gradients, and precisons[torch.float16] is None, so the
fp16 case runs at the float32 default tolerance while float32 and bfloat16 get a
relaxed 1e-4.

test_rrelu_saved_noise_hooks (both inplace values),
test_rrelu_saved_noise_non_reentrant_checkpoint with early_stop=True, and
test_rrelu_with_noise_functional_backward fail before this change and pass after.
The early_stop=False case passes either way and is there as a control: it is the
configuration where the replay reaches the kernel.

Fixes pytorch#193671

This change was prepared with the assistance of an AI coding assistant; the
analysis, code and tests were reviewed by the author.
@yinjiew
yinjiew force-pushed the fix/rrelu-saved-noise branch from d839c13 to 969ef0d Compare August 17, 2026 02:33
@yinjiew

yinjiew commented Aug 17, 2026

Copy link
Copy Markdown
Author

@pytorchbot label -ciflow/trunk

@pytorch-bot

pytorch-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

❌ 🤖 pytorchbot command failed:

@pytorchbot label: error: the following arguments are required: labels

usage: @pytorchbot label labels [labels ...]

Try @pytorchbot --help for more info.

@yinjiew

yinjiew commented Aug 17, 2026

Copy link
Copy Markdown
Author

@pytorchbot label "ciflow/trunk"

@pytorch-bot

pytorch-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

The ciflow label(s) ciflow/trunk will be added, but CI won't be triggered until the workflows are approved (scroll to the bottom of this page).

Please ping one of the reviewers if you do not have access to approve and run workflows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunk Trigger trunk jobs on your pull request open source release notes: nn release notes category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Silent wrong gradients: nn.RReLU under non-reentrant checkpointing saves uninitialized noise

2 participants