ZhengyangZhang commited on
Commit
be40555
·
verified ·
1 Parent(s): 436daa3

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/torch/_functorch/__init__.py +5 -0
  2. lib/python3.12/site-packages/torch/_functorch/__pycache__/autograd_function.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/torch/_functorch/__pycache__/config.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__init__.py +5 -0
  5. lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__pycache__/__init__.cpython-312.pyc +0 -0
  6. lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__pycache__/knapsack.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/knapsack.py +123 -0
  8. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__init__.py +5 -0
  9. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/__init__.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/autograd_cache.cpython-312.pyc +0 -0
  11. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/collect_metadata_analysis.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/dispatch_and_compile_graph.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/functional_utils.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/input_output_analysis.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/jit_compile_runtime_wrappers.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/logging_utils.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/runtime_wrappers.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/schemas.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/subclass_parametrization.cpython-312.pyc +0 -0
  20. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/subclass_utils.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/traced_function_transforms.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/utils.cpython-312.pyc +0 -0
  23. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/autograd_cache.py +904 -0
  24. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/collect_metadata_analysis.py +813 -0
  25. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/dispatch_and_compile_graph.py +338 -0
  26. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/functional_utils.py +528 -0
  27. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/input_output_analysis.py +423 -0
  28. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/jit_compile_runtime_wrappers.py +907 -0
  29. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/logging_utils.py +147 -0
  30. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/runtime_wrappers.py +2202 -0
  31. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/schemas.py +911 -0
  32. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/subclass_parametrization.py +74 -0
  33. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/subclass_utils.py +453 -0
  34. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/traced_function_transforms.py +897 -0
  35. lib/python3.12/site-packages/torch/_functorch/_aot_autograd/utils.py +492 -0
  36. lib/python3.12/site-packages/torch/_functorch/aot_autograd.py +1664 -0
  37. lib/python3.12/site-packages/torch/_functorch/apis.py +449 -0
  38. lib/python3.12/site-packages/torch/_functorch/autograd_function.py +764 -0
  39. lib/python3.12/site-packages/torch/_functorch/batch_norm_replacement.py +29 -0
  40. lib/python3.12/site-packages/torch/_functorch/benchmark_utils.py +231 -0
  41. lib/python3.12/site-packages/torch/_functorch/compile_utils.py +176 -0
  42. lib/python3.12/site-packages/torch/_functorch/compilers.py +445 -0
  43. lib/python3.12/site-packages/torch/_functorch/config.py +219 -0
  44. lib/python3.12/site-packages/torch/_functorch/deprecated.py +172 -0
  45. lib/python3.12/site-packages/torch/_functorch/eager_transforms.py +1800 -0
  46. lib/python3.12/site-packages/torch/_functorch/functional_call.py +253 -0
  47. lib/python3.12/site-packages/torch/_functorch/fx_minifier.py +501 -0
  48. lib/python3.12/site-packages/torch/_functorch/make_functional.py +617 -0
  49. lib/python3.12/site-packages/torch/_functorch/partitioners.py +1911 -0
  50. lib/python3.12/site-packages/torch/_functorch/pyfunctorch.py +294 -0
lib/python3.12/site-packages/torch/_functorch/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
lib/python3.12/site-packages/torch/_functorch/__pycache__/autograd_function.cpython-312.pyc ADDED
Binary file (25.3 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/__pycache__/config.cpython-312.pyc ADDED
Binary file (2.85 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (221 Bytes). View file
 
lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__pycache__/knapsack.cpython-312.pyc ADDED
Binary file (4.83 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/knapsack.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Tuple
2
+
3
+ import torch
4
+
5
+
6
+ def greedy_knapsack(
7
+ memory: List[float], runtimes: List[float], max_memory: float
8
+ ) -> Tuple[float, List[int], List[int]]:
9
+ n = len(runtimes)
10
+ items = list(range(n))
11
+
12
+ # Sort items based on the ratio of runtime to memory in descending order
13
+ items = sorted(items, key=lambda i: runtimes[i] / memory[i], reverse=True)
14
+
15
+ total_memory = 0.0
16
+ total_runtime = 0.0
17
+ items_to_save = []
18
+ items_to_allow_recomputing = []
19
+
20
+ for i in items:
21
+ if total_memory + memory[i] <= max_memory:
22
+ total_memory += memory[i]
23
+ total_runtime += runtimes[i]
24
+ items_to_save.append(i)
25
+ else:
26
+ items_to_allow_recomputing.append(i)
27
+ return total_runtime, items_to_save, items_to_allow_recomputing
28
+
29
+
30
+ def ilp_knapsack(
31
+ memory: List[float], runtimes: List[float], max_memory: float
32
+ ) -> Tuple[float, List[int], List[int]]:
33
+ import numpy as np
34
+
35
+ try:
36
+ from scipy.optimize import Bounds, LinearConstraint, milp
37
+ except ImportError:
38
+ raise RuntimeError(
39
+ "To use the ILP for memory budget checkpointing you need to install scipy"
40
+ ) from None
41
+
42
+ np_memory = np.array(memory)
43
+ np_runtimes = np.array(runtimes)
44
+ c = -np_runtimes # type: ignore[operator]
45
+
46
+ memory_constraint = LinearConstraint(A=np_memory, ub=np.array(max_memory))
47
+ constraints = [memory_constraint]
48
+
49
+ integrality = np.ones_like(c)
50
+ res = milp(
51
+ c=c, constraints=constraints, integrality=integrality, bounds=Bounds(0, 1)
52
+ )
53
+ if not res.success:
54
+ raise RuntimeError("Somehow scipy solving failed")
55
+
56
+ items_to_save = []
57
+ items_to_allow_recomputing = []
58
+ for idx, i in enumerate(res.x):
59
+ if i == 1:
60
+ items_to_save.append(idx)
61
+ else:
62
+ items_to_allow_recomputing.append(idx)
63
+ return -res.fun, items_to_save, items_to_allow_recomputing
64
+
65
+
66
+ def dp_knapsack(
67
+ memory: List[float], runtime: List[float], max_memory: float
68
+ ) -> Tuple[float, List[int], List[int]]:
69
+ # Scaling factor to convert floating point weights to integers
70
+ S = 10000
71
+
72
+ # Quantize the memory weights
73
+ quantized_memory = torch.tensor(
74
+ [int(round(m * S)) for m in memory], dtype=torch.long, device="cpu"
75
+ )
76
+ runtimes = torch.tensor(runtime, dtype=torch.float32, device="cpu")
77
+
78
+ # Quantized pseudopolynomial DP for 0-1 Knapsack
79
+ quantized_max_memory = int(round(max_memory * S))
80
+
81
+ n = len(memory)
82
+
83
+ # Initialize the DP table
84
+ # TODO(chilli): I think if needed, this memory can be optimized with sliding
85
+ # window trick + Hirschberg trick:
86
+ # https://codeforces.com/blog/entry/47247?#comment-316200
87
+ dp = torch.zeros(
88
+ (n + 1, quantized_max_memory + 1), dtype=torch.float32, device="cpu"
89
+ )
90
+
91
+ for i in range(1, n + 1):
92
+ current_memory = quantized_memory[i - 1]
93
+ current_runtime = runtimes[i - 1]
94
+
95
+ # Copy the previous row
96
+ dp[i, :] = dp[i - 1, :]
97
+
98
+ # Update dp[i, j] for all j >= current_memory
99
+ if current_memory == 0:
100
+ dp[i, :] = dp[i - 1, :] + current_runtime
101
+ else:
102
+ dp[i, current_memory:] = torch.maximum(
103
+ dp[i - 1, current_memory:],
104
+ dp[i - 1, :-current_memory] + current_runtime,
105
+ )
106
+
107
+ # Backtrack to find the items included in the knapsack
108
+ saved_items = []
109
+ recomputable_items = []
110
+ j: int = quantized_max_memory
111
+ for i in range(n, 0, -1):
112
+ if dp[i][j] != dp[i - 1][j]:
113
+ saved_items.append(i - 1) # Include this item (indexing from 0)
114
+ j -= int(quantized_memory[i - 1].item())
115
+ else:
116
+ recomputable_items.append(i - 1)
117
+
118
+ saved_items.reverse() # To get items in the order they were added
119
+
120
+ # The maximum runtime that can be achieved within the max_memory constraint
121
+ max_runtime = dp[n][quantized_max_memory].item()
122
+
123
+ return max_runtime, saved_items, recomputable_items
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (209 Bytes). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/autograd_cache.cpython-312.pyc ADDED
Binary file (36.8 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/collect_metadata_analysis.cpython-312.pyc ADDED
Binary file (20.2 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/dispatch_and_compile_graph.cpython-312.pyc ADDED
Binary file (10.4 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/functional_utils.cpython-312.pyc ADDED
Binary file (22.2 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/input_output_analysis.cpython-312.pyc ADDED
Binary file (16.6 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/jit_compile_runtime_wrappers.cpython-312.pyc ADDED
Binary file (25.4 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/logging_utils.cpython-312.pyc ADDED
Binary file (6.71 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/runtime_wrappers.cpython-312.pyc ADDED
Binary file (71.6 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/schemas.cpython-312.pyc ADDED
Binary file (28.5 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/subclass_parametrization.cpython-312.pyc ADDED
Binary file (4.18 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/subclass_utils.cpython-312.pyc ADDED
Binary file (15.7 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/traced_function_transforms.cpython-312.pyc ADDED
Binary file (31.1 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/utils.cpython-312.pyc ADDED
Binary file (20.2 kB). View file
 
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/autograd_cache.py ADDED
@@ -0,0 +1,904 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ Utils for caching the outputs of AOTAutograd
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import base64
8
+ import contextlib
9
+ import functools
10
+ import json
11
+ import logging
12
+ import os
13
+ import pickle
14
+ import shutil
15
+ import time
16
+ from dataclasses import dataclass
17
+ from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING, Union
18
+
19
+ import torch
20
+ from torch._dynamo.utils import counters, get_chromium_event_logger
21
+ from torch._functorch import config
22
+ from torch._inductor.codecache import (
23
+ _ident,
24
+ add_ephemeral_timeout_increase_for_distributed,
25
+ BypassFxGraphCache,
26
+ create_cache,
27
+ extract_tensor_metadata_for_cache_key,
28
+ FxGraphCache,
29
+ FxGraphCachePickler,
30
+ FxGraphHashDetails,
31
+ write_atomic,
32
+ )
33
+ from torch._inductor.output_code import CompiledFxGraphConstants
34
+ from torch._inductor.runtime.runtime_utils import cache_dir
35
+ from torch._inductor.utils import should_use_remote_fx_graph_cache
36
+ from torch._logging import LazyString
37
+ from torch._utils_internal import log_cache_bypass
38
+ from torchgen.utils import dataclass_repr
39
+
40
+ from .runtime_wrappers import (
41
+ AOTDispatchAutograd,
42
+ AOTDispatchSubclassWrapper,
43
+ CompilerWrapper,
44
+ FunctionalizedRngRuntimeWrapper,
45
+ post_compile,
46
+ RuntimeWrapper,
47
+ SubclassMeta,
48
+ )
49
+ from .schemas import AOTAutogradCacheInfo, AOTConfig, ViewAndMutationMeta # noqa: F401
50
+
51
+
52
+ if TYPE_CHECKING:
53
+ from torch._inductor.compile_fx import _CompileFxKwargs
54
+ from torch._inductor.output_code import CompiledFxGraph
55
+ from torch._inductor.remote_cache import JsonDataTy, RemoteCache
56
+ from torch._inductor.utils import BoxedBool
57
+ from torch.fx.node import Node
58
+
59
+ log = logging.getLogger(__name__)
60
+
61
+
62
+ class BypassAOTAutogradCache(Exception):
63
+ pass
64
+
65
+
66
+ # Used to signify when FXGraphCache missed when AOTAutogradCache uses it
67
+ class FXGraphCacheMiss(BypassAOTAutogradCache):
68
+ pass
69
+
70
+
71
+ def should_use_remote_autograd_cache():
72
+ if torch._inductor.config.force_disable_caches:
73
+ return False
74
+ if config.enable_remote_autograd_cache is not None:
75
+ return config.enable_remote_autograd_cache
76
+ if not config.is_fbcode():
77
+ return False
78
+
79
+ if torch._utils_internal.is_fb_unit_test():
80
+ return False
81
+
82
+ try:
83
+ from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION
84
+ except ModuleNotFoundError:
85
+ return False
86
+
87
+ jk_name = "pytorch/remote_cache:aot_autograd_cache_version"
88
+
89
+ return REMOTE_CACHE_VERSION >= torch._utils_internal.justknobs_getval_int(jk_name)
90
+
91
+
92
+ def should_use_local_autograd_cache():
93
+ if torch._inductor.config.force_disable_caches:
94
+ return False
95
+ return config.enable_autograd_cache
96
+
97
+
98
+ def autograd_cache_enabled():
99
+ return should_use_local_autograd_cache() or should_use_remote_autograd_cache()
100
+
101
+
102
+ def check_node_safe(node: Node):
103
+ """
104
+ Checks that the node only uses supported operators. We are starting with very
105
+ conservative cacheability constraints, and incrementally adding more support as we expand.
106
+
107
+ [Note: AOTAutograd Cacheability checks]
108
+ - Our cache key is computed from the FX graph produced by Dynamo and the input example values
109
+ - A node is "safe" if the same cache key results in a compiled artifact that has the same behavior
110
+ (i.e, the set of inputs that go into our cache key is sufficient to distinguish its behavior)
111
+
112
+ To accomplish this safety check, we consider the following functions to be safe:
113
+ - Public functions under modules torch, torch.functional, and torch.nn.functional: these are
114
+ allowed in the graph by dynamo, so we can assume they are safe to cache.
115
+ - method calls on base tensor types
116
+ - Any call_module that dynamo deemed safe to allow AOTAutograd to trace
117
+ - Non callable nodes, such as placeholder, output, get_attr
118
+
119
+ The test suite test_aot_autograd_cache.py::AOTAutogradCachePicklerTests tries its best to fully cover/specify this behavior.
120
+ """
121
+ SAFE_TORCH_MODULES = ("torch.functional", "torch.nn.functional")
122
+ SAFE_TORCH_FUNCTIONS = (
123
+ "torch.Size",
124
+ "torch.sym_int",
125
+ "torch._sym_sqrt",
126
+ "torch.sym_float",
127
+ "torch.sym_sum",
128
+ "einops.einops.rearrange",
129
+ )
130
+
131
+ def is_public_torch_api(target):
132
+ # Don't blindly allow private functions in the torch namespace
133
+ is_private = target.__name__.startswith("_")
134
+
135
+ return (
136
+ getattr(target, "__module__", None) in SAFE_TORCH_MODULES and not is_private
137
+ )
138
+
139
+ def is_safe_torch_function(target):
140
+ """Allowlisted torch functions"""
141
+ return f"{target.__module__}.{target.__name__}" in SAFE_TORCH_FUNCTIONS
142
+
143
+ def is_torch_function(target):
144
+ if isinstance(target, (torch._ops.OpOverload, torch._ops.OpOverloadPacket)):
145
+ return True
146
+ if is_public_torch_api(target):
147
+ return True
148
+ is_builtin_fun_or_type = type(target).__name__ == "builtin_function_or_method"
149
+ if is_builtin_fun_or_type:
150
+ return True
151
+ if is_safe_torch_function(target):
152
+ return True
153
+ return False
154
+
155
+ def is_tensor(target):
156
+ # Tensors always have example values in meta field
157
+ return "example_value" in target.meta
158
+
159
+ # I'd love to use a match statement here, but it wasn't introduced until py3.10
160
+ if node.op == "call_function":
161
+ # We support only torch.* functions for now
162
+ # We can probably add an allowlist of safe non-torch implementations as well
163
+ if not is_torch_function(node.target):
164
+ module = getattr(node.target, "__module__", None)
165
+ name = getattr(node.target, "__name__", None)
166
+ raise BypassAOTAutogradCache(
167
+ f"Unsupported call_function target {node.target}. \n Function module: {module}, \nFunction name: {name}"
168
+ )
169
+ elif node.op == "call_method":
170
+ method_name = node.target
171
+ method_target = node.args[0]
172
+ # Only support method calls on base tensors
173
+ if not is_tensor(method_target):
174
+ module = getattr(method_target, "__module__", None)
175
+ name = getattr(method_target, "__name__", None)
176
+ raise BypassAOTAutogradCache(
177
+ f"Unsupported call_method target {method_target}. \nMethod module: {module}, \nMethod name: {name}"
178
+ )
179
+ if (
180
+ type(method_name) != str
181
+ and type(method_name).__name__ != "method_descriptor"
182
+ ):
183
+ raise BypassAOTAutogradCache(
184
+ f"Unsupported call_method method {node.target}: {method_name}"
185
+ )
186
+ # Cache safe
187
+ elif node.op in ("placeholder", "get_attr", "call_module", "output"):
188
+ # Assumption today for call_module being a safe op:
189
+ # (1) today the only call_module ops that can show up in a graph come from "built-in-nn-modules"
190
+ # that dynamo assumes are safe to trace. If dynamo assumes they are safely to blindly trace, then
191
+ # they should be safe to cache as well.
192
+ # (2) in the steady-state (some time in H2?) we shouldn't see these anymore, once inline builtin nn modules by default
193
+ # (3) We do not allow user made nn modules in the graph today, only function calls.
194
+ pass
195
+ else:
196
+ raise BypassAOTAutogradCache(f"Unsupported node op {node.op}")
197
+
198
+
199
+ def check_cacheable(gm: torch.fx.GraphModule):
200
+ """
201
+ Checks that the graph module only uses supported operators
202
+ """
203
+ nodes = gm.graph.nodes
204
+ if torch._dynamo.compiled_autograd.in_compiled_autograd_region:
205
+ raise BypassAOTAutogradCache(
206
+ "Cannot cache a graph with compiled autograd enabled"
207
+ )
208
+ if torch._inductor.config.freezing:
209
+ raise BypassAOTAutogradCache("Cannot cache a graph with freezing enabled")
210
+
211
+ if not (
212
+ torch._inductor.config.fx_graph_cache or should_use_remote_fx_graph_cache()
213
+ ):
214
+ raise BypassAOTAutogradCache("FX graph cache is not enabled")
215
+
216
+ tracing_context = torch._guards.TracingContext.try_get()
217
+ if tracing_context and tracing_context.fakify_first_call:
218
+ raise BypassAOTAutogradCache(
219
+ "Won't cache a graph with fakify_first_call enabled"
220
+ )
221
+ for node in nodes:
222
+ check_node_safe(node)
223
+
224
+
225
+ def check_metadata_cacheable(metadata: ViewAndMutationMeta):
226
+ """
227
+ When view replay is turned on, we bypass autograd cache if
228
+ the output is aliased.
229
+ """
230
+ if config.view_replay_for_aliased_outputs:
231
+ for info in metadata.output_info:
232
+ if info.functional_tensor is not None:
233
+ raise BypassAOTAutogradCache(
234
+ "Cannot cache a graph with functional tensor"
235
+ )
236
+
237
+
238
+ class AOTAutogradCacheDetails(FxGraphHashDetails):
239
+ """
240
+ Object to capture all the details for a dynamo graph module relevant to computing
241
+ a safe and stable cache key for AOTAutograd.
242
+ """
243
+
244
+ def __init__(
245
+ self,
246
+ gm: torch.fx.GraphModule,
247
+ example_inputs,
248
+ aot_config: AOTConfig,
249
+ fx_config: _CompileFxKwargs,
250
+ ):
251
+ # FxGraphHashDetails contains all the keys related to inductor. Also includes some system info
252
+ self.aot_config = aot_config
253
+ self.grad_enabled = torch.is_grad_enabled()
254
+ self.disable_amp = torch._C._is_any_autocast_enabled()
255
+ self.deterministic_algorithms = torch.are_deterministic_algorithms_enabled()
256
+ self.autograd_config = config.save_config()
257
+ try:
258
+ # FXGraphCache has constraints on what can be pickled in its inductor
259
+ # config. Check that the gm is cacheable by inductor first,
260
+ # and if it raises an exception, also bypass on our end.
261
+ FxGraphCache._check_can_cache(gm)
262
+ super().__init__(gm, example_inputs, fx_config, [])
263
+ except BypassFxGraphCache as e:
264
+ # Sometimes inductor configs are unpickleable and can fail
265
+ raise BypassAOTAutogradCache from e
266
+
267
+
268
+ class AOTAutogradCachePickler(FxGraphCachePickler):
269
+ def __init__(self, gm: torch.fx.GraphModule):
270
+ super().__init__(gm)
271
+ self.dispatch_table: Dict
272
+ self.dispatch_table.update(
273
+ {
274
+ AOTConfig: functools.partial(self._reduce_aot_config),
275
+ torch.Tensor: functools.partial(self._reduce_tensor),
276
+ }
277
+ )
278
+
279
+ def _reduce_aot_config(self, aot_config: AOTConfig):
280
+ """
281
+ Reduce the config to a stable key for caching.
282
+ """
283
+ return (
284
+ _ident,
285
+ (
286
+ aot_config.num_params_buffers,
287
+ aot_config.keep_inference_input_mutations,
288
+ aot_config.is_export,
289
+ aot_config.no_tangents,
290
+ aot_config.dynamic_shapes,
291
+ aot_config.aot_autograd_arg_pos_to_source,
292
+ aot_config.enable_log,
293
+ aot_config.pre_dispatch,
294
+ ),
295
+ )
296
+
297
+ def _reduce_tensor(self, tensor):
298
+ """
299
+ Reduce the tensor to a stable key for caching.
300
+ """
301
+ metadata = extract_tensor_metadata_for_cache_key(tensor)
302
+ return (_ident, (metadata,))
303
+
304
+
305
+ def autograd_cache_key(
306
+ gm: torch.fx.GraphModule,
307
+ example_inputs,
308
+ config: AOTConfig,
309
+ fx_config: _CompileFxKwargs,
310
+ # TODO: add args and parameters
311
+ ) -> Tuple[str, List[str]]:
312
+ """
313
+ Generate a unique hash of the FX graph for caching.
314
+ """
315
+ check_cacheable(gm)
316
+ details = AOTAutogradCacheDetails(gm, example_inputs, config, fx_config)
317
+ pickler = AOTAutogradCachePickler(gm)
318
+ # The prefix distinguishes among the other kinds of objects we cache
319
+ key = "a" + pickler.get_hash(details)
320
+ debug_lines = pickler.debug_lines(details)
321
+ log.debug(
322
+ "Autograd graph cache hash details for key %s:\n%s",
323
+ key,
324
+ LazyString(lambda: "\n".join(debug_lines)),
325
+ )
326
+ return key, debug_lines
327
+
328
+
329
+ @dataclass
330
+ class FXGraphCacheLoadable:
331
+ fx_graph_cache_key: str
332
+
333
+ def is_backward(self):
334
+ return False
335
+
336
+ def load(self, example_inputs, fx_config: _CompileFxKwargs) -> CompiledFxGraph:
337
+ # [Note: AOTAutogradCache and FXGraphCache Guard interactions]
338
+ # As mentioned, AOTAutograd takes in the symint inputs from dynamo's list of arguments.
339
+ # FXGraphCache serializes guards that are needed in the shape_env based on these symint inputs to the graph.
340
+ # The invariant that AOTAutograd uses here is that the sources for symints given to it by dynamo are exactly
341
+ # the same as the ones it passes to inductor, for both the forward and backward passes.
342
+ # (This does not mean that the tensor values passed in are the same: only that their symints are).
343
+ # That is, AOTAutograd and Inductor never create new guards based on symints with different sources
344
+ # than those passed to it by inductor.
345
+
346
+ # TODO: We don't cache debug lines for now, but we should for improved debugging
347
+ remote_cache = None
348
+ constants = CompiledFxGraphConstants()
349
+ if should_use_remote_fx_graph_cache():
350
+ remote_cache = FxGraphCache.get_remote_cache()
351
+
352
+ result, cache_info = FxGraphCache.load_with_key(
353
+ self.fx_graph_cache_key,
354
+ [],
355
+ example_inputs,
356
+ local=True,
357
+ remote_cache=remote_cache,
358
+ is_backward=self.is_backward(),
359
+ constants=constants,
360
+ )
361
+ if result is None:
362
+ log.info("FXGraphCache cache miss for key %s", self.fx_graph_cache_key)
363
+ raise FXGraphCacheMiss
364
+
365
+ # No need to log chromium event because AOTAutograd will log that immediately for us
366
+ torch._logging.trace_structured(
367
+ "artifact",
368
+ metadata_fn=lambda: {
369
+ "name": "fx_graph_cache_hit", # always a hit
370
+ "encoding": "json",
371
+ },
372
+ payload_fn=lambda: json.dumps(cache_info),
373
+ )
374
+
375
+ # TODO: How come cudagraphs could be None here?
376
+ result.post_compile(example_inputs, fx_config["cudagraphs"], constants) # type: ignore[arg-type]
377
+ return result
378
+
379
+
380
+ @dataclass
381
+ class CompiledForward(FXGraphCacheLoadable):
382
+ """
383
+ Cacheable entry for a forward function
384
+ """
385
+
386
+ def is_backward(self):
387
+ return False
388
+
389
+
390
+ @dataclass
391
+ class CompiledBackward(FXGraphCacheLoadable):
392
+ """
393
+ Cacheable entry for a forward function
394
+ """
395
+
396
+ # Used by AOTDispatchAutograd.post_compile
397
+ backward_state_indices: List[int]
398
+ num_symints_saved_for_bw_: int
399
+
400
+ def is_backward(self):
401
+ return True
402
+
403
+
404
+ @dataclass
405
+ class AOTAutogradCacheEntry:
406
+ """A single entry into the cache."""
407
+
408
+ # Forward and Backward info
409
+ compiled_fw: CompiledForward
410
+ compiled_bw: Optional[CompiledBackward]
411
+
412
+ # Code of the joint graph using print_readable()
413
+ # Used for logging purposes
414
+ aot_joint_graph_str: Optional[str]
415
+ aot_forward_graph_str: Optional[str]
416
+ aot_backward_graph_str: Optional[str]
417
+
418
+ # Runtime_metadata saved right before compilation
419
+ runtime_metadata: ViewAndMutationMeta
420
+
421
+ # Wrappers that run after each aot_dispatch_* function
422
+ dispatch_wrappers: List[CompilerWrapper]
423
+
424
+ # Used by AOTSubclassWrapper
425
+ maybe_subclass_meta: Optional[SubclassMeta]
426
+ num_fw_outs_saved_for_bw: Optional[int]
427
+
428
+ # Used by RuntimeWrapepr
429
+ indices_of_inps_to_detach: List[int]
430
+
431
+ # Time taken to trace/compile the forward
432
+ # forward_time_taken includes AOTAutograd tracing time + inductor compilation time
433
+ # backward_time_taken is essentially just the time inductor took to compile
434
+ forward_time_taken_ns: int
435
+ backward_time_taken_ns: int
436
+
437
+ # Turn cache entry into the original callable
438
+ def wrap_post_compile(
439
+ self,
440
+ args: List[torch.Tensor],
441
+ aot_config: AOTConfig,
442
+ fx_config: _CompileFxKwargs,
443
+ ) -> Callable:
444
+ """
445
+ This function takes a cache entry and carefully reconstructs the original callable
446
+ that AOTAutograd returned the first time it was run. It does this by running the various
447
+ post compile steps that AOTAutograd runs on its compiled artifact after running the fw/bw compilers.
448
+
449
+ In the inference path, this consists of the Subclass, FunctionalzedRngRuntime, and RuntimeWrappers.
450
+ In the autograd path, this consists of AOTAutogradDispatch.post_compile.
451
+
452
+ The steps here should match exactly the steps that are run in aot_dispatch_base and aot_dispatch_autograd.
453
+
454
+ Notably absent from the cached path are:
455
+ - DebugAssertWrapper
456
+ - FakifiedOutWrapper
457
+
458
+ Which we'll handle separately later on, if necessary.
459
+ """
460
+
461
+ # Log the output of AOTAutogradCache
462
+ if aot_config.enable_log:
463
+ # TODO: maybe also log to aot_graphs_log
464
+ # Unfortunately aot_graphs_log uses
465
+ # slightly different formatting though
466
+ if self.aot_joint_graph_str is not None:
467
+ torch._logging.trace_structured(
468
+ "aot_joint_graph", payload_fn=lambda: self.aot_joint_graph_str
469
+ )
470
+
471
+ if self.aot_forward_graph_str is not None:
472
+ torch._logging.trace_structured(
473
+ "artifact",
474
+ metadata_fn=lambda: {
475
+ "name": "aot_forward_graph_fw_metadata",
476
+ "encoding": "string",
477
+ },
478
+ payload_fn=lambda: dataclass_repr(self.runtime_metadata),
479
+ )
480
+ if self.maybe_subclass_meta is not None:
481
+ torch._logging.trace_structured(
482
+ "artifact",
483
+ metadata_fn=lambda: {
484
+ "name": "aot_forward_graph_fw_subclass_metadata",
485
+ "encoding": "string",
486
+ },
487
+ payload_fn=lambda: dataclass_repr(self.maybe_subclass_meta),
488
+ )
489
+
490
+ # It's called an inference graph if not running with autograd
491
+ name = (
492
+ "aot_forward_graph"
493
+ if self.aot_backward_graph_str is not None
494
+ else "aot_inference_graph"
495
+ )
496
+ torch._logging.trace_structured(
497
+ name, payload_fn=lambda: self.aot_forward_graph_str
498
+ )
499
+
500
+ if self.aot_backward_graph_str is not None:
501
+ torch._logging.trace_structured(
502
+ "aot_backward_graph", payload_fn=lambda: self.aot_backward_graph_str
503
+ )
504
+
505
+ compiled_fw_func = self.compiled_fw.load(args, fx_config)
506
+ compiled_bw_func = None
507
+ chromium_log = get_chromium_event_logger()
508
+ if self.compiled_bw is not None:
509
+ compiled_bw_func = self.compiled_bw.load(args, fx_config)
510
+ needs_autograd = True
511
+ chromium_log.try_add_event_data("backend_compile", dispatch_mode="autograd")
512
+ else:
513
+ needs_autograd = False
514
+ chromium_log.try_add_event_data(
515
+ "backend_compile", dispatch_mode="inference"
516
+ )
517
+
518
+ # Wrap the forward function in post compile wrappers
519
+ compiled_fw_func = AOTDispatchSubclassWrapper(
520
+ trace_joint=needs_autograd,
521
+ fw_only=None,
522
+ maybe_subclass_meta=self.maybe_subclass_meta,
523
+ num_fw_outs_saved_for_bw=self.num_fw_outs_saved_for_bw,
524
+ ).post_compile(
525
+ compiled_fw_func, aot_config, runtime_metadata=self.runtime_metadata
526
+ )
527
+
528
+ req_subclass_dispatch = self.maybe_subclass_meta is not None
529
+ chromium_log.add_event_data(
530
+ "backend_compile", requires_subclass_dispatch=req_subclass_dispatch
531
+ )
532
+
533
+ # In autograd case, functionalizedRngWrapper should not modify outs
534
+ return_new_outs = not needs_autograd
535
+ compiled_fw_func = FunctionalizedRngRuntimeWrapper(
536
+ return_new_outs=return_new_outs
537
+ ).post_compile(
538
+ compiled_fw_func, aot_config, runtime_metadata=self.runtime_metadata
539
+ )
540
+ disable_amp = torch._C._is_any_autocast_enabled()
541
+
542
+ if needs_autograd:
543
+ assert self.compiled_bw is not None
544
+ # This function is run on both cache miss and cache hit, either here
545
+ # or in aot_dispatch_autograd. On a cache hit,
546
+ # 1. the bw is already compiled
547
+ # 2. we don't need to save to the cache again
548
+ # so those corresponding arguments are set to None.
549
+ compiled_function = AOTDispatchAutograd.post_compile(
550
+ compiled_fw_func,
551
+ compiled_bw_func,
552
+ self.maybe_subclass_meta,
553
+ self.compiled_bw.num_symints_saved_for_bw_,
554
+ self.compiled_bw.backward_state_indices,
555
+ disable_amp,
556
+ self.indices_of_inps_to_detach,
557
+ None, # lazy_backward_info
558
+ aot_config,
559
+ fw_metadata=self.runtime_metadata,
560
+ try_save_cache_entry=None,
561
+ )
562
+ else:
563
+ compiled_function = RuntimeWrapper(
564
+ indices_of_inps_to_detach=self.indices_of_inps_to_detach,
565
+ trace_joint=False,
566
+ disable_amp=disable_amp,
567
+ ).post_compile(
568
+ compiled_fw_func, aot_config, runtime_metadata=self.runtime_metadata
569
+ )
570
+
571
+ compiled_function, _ = post_compile(
572
+ self.dispatch_wrappers,
573
+ compiled_function,
574
+ aot_config,
575
+ runtime_metadata=self.runtime_metadata,
576
+ )
577
+
578
+ return compiled_function
579
+
580
+
581
+ @contextlib.contextmanager
582
+ def sanitize_gm_for_cache(gm: torch.fx.GraphModule):
583
+ """
584
+ Clears a few fields in a dynamo supplied Graph Module that are not stable between graph inputs, but don't
585
+ affect inductor or aotdispatch correctness.
586
+
587
+ These fields **can** be used by code calling into aotdispatch (namely, dynamo), so we can't null them out completely.
588
+
589
+ To ensure that these fields are not accessed by inductor or aotdispatch, we clear them during AOTAutogradCache.load,
590
+ and then put them back before returning. This way, we generate a cache key based off of a canonical graph
591
+ without these fields, and also guarantee they aren't used to affect the cache's output.
592
+ """
593
+ IGNORED_FIELDS = (
594
+ "meta", # metadata used by export
595
+ "compile_subgraph_reason", # Used by dynamo only for logging, no change in inductor/autograd behavior
596
+ "_param_name_to_source", # Encapsulated by aot_config.aot_autograd_arg_pos_to_source
597
+ )
598
+ saved_fields = {}
599
+ for field in IGNORED_FIELDS:
600
+ saved_fields[field] = getattr(gm, field, None)
601
+ # Clear the field
602
+ setattr(gm, field, None)
603
+ try:
604
+ yield
605
+ finally:
606
+ # Put the fields back after dispatch_and_compile is complete
607
+ for field, value in saved_fields.items():
608
+ setattr(gm, field, value)
609
+
610
+
611
+ class AOTAutogradCache:
612
+ """
613
+ Caches the results of running AOTAutograd. This class mostly handles the save and load logic, whereas
614
+ AOTAutogradCacheEntry handles the wrapping/unwrapping logic.
615
+
616
+ Cache Inputs (AOTAutogradCacheDetails)
617
+ - AOTAutogradCache takes in the following inputs, which are analogous to inputs given
618
+ to AOTAutograd by dynamo:
619
+ - A fx graph module generated by dynamo
620
+ - A list of args, which consists of:
621
+ - Symint inputs to the graph, generated by dynamo
622
+ - The **real tensor** inputs, which inductor uses for cudagraphs
623
+ - Notably, the real tensor inputs don't have symints in their metadata.
624
+ AOTAutograd then retraces those real tensor arguments into FakeTensors later during execution.
625
+ - A set of global configurations that affect AOTAutograd or Inductor behavior.
626
+
627
+ It then generates a cache key given these values. Notably, this means AOTAutogradCache currently
628
+ specializes on the sizes and strides of the real tensor inputs when dynamic shapes are turned on.
629
+ In a later PR, we'll likely generate the cache key based on the FakeTensors AOTAutograd generates
630
+ based on the real tensor inputs, which can contain symints.
631
+
632
+ # Cache Outputs (AOTAutogradCacheEntry)
633
+ - AOTAutogradCache caches the following values:
634
+ - The compiled forward and backward functions from inductor, via keys to the FXGraphCache
635
+ - Metadata to reconstruct the AOTModule from the compiled inductor artifacts
636
+ - See AOTAutogradCacheEntry for more info
637
+
638
+ [Note: Caching guards generated by AOTAutograd and Inductor]
639
+ AOTAutograd and inductor both can introduce new guards to the shape environment. FXGraphCache saves guards with each
640
+ compiled graph inductor generates. On a cache hit, AOTAutograd reloads the compiled forward and backward functions
641
+ from FXGraphCache, giving it new symint arguments from the input args.
642
+ FXGraphCache uses those symints and its saved guards to repopulate the ShapeEnv with guards.
643
+ **No new guards are generated into the shape env after inductor finishes compiling**, so the guards
644
+ saved by inductor are sufficient for correctness for both AOTAutograd and Inductor's caches.
645
+ """
646
+
647
+ @staticmethod
648
+ def clear():
649
+ """Clear the cache"""
650
+ try:
651
+ shutil.rmtree(AOTAutogradCache._get_tmp_dir())
652
+ except FileNotFoundError:
653
+ pass
654
+
655
+ @staticmethod
656
+ def load(
657
+ dispatch_and_compile: Callable,
658
+ mod: Union[torch.fx.GraphModule, torch._dynamo.utils.GmWrapper],
659
+ args,
660
+ aot_config: AOTConfig,
661
+ cudagraphs: BoxedBool,
662
+ local: bool,
663
+ remote: bool,
664
+ ) -> Callable:
665
+ """
666
+ Load a result from the cache, and reconstruct a runtime wrapper around the object
667
+ """
668
+
669
+ gm = mod.gm if isinstance(mod, torch._dynamo.utils.GmWrapper) else mod
670
+ with sanitize_gm_for_cache(gm):
671
+ compiled_fn = None
672
+ cache_info: Dict[str, Any] = {}
673
+ cache_key = None
674
+ debug_lines: List[str] = []
675
+ cache_event_time = time.time_ns()
676
+ cache_state = None
677
+ fx_config: _CompileFxKwargs = {"cudagraphs": cudagraphs}
678
+ try:
679
+ cache_key, debug_lines = autograd_cache_key(
680
+ gm, args, aot_config, fx_config
681
+ )
682
+ entry: Optional[AOTAutogradCacheEntry] = AOTAutogradCache._lookup(
683
+ cache_key, local, remote
684
+ )
685
+ if entry is not None:
686
+ compiled_fn = entry.wrap_post_compile(args, aot_config, fx_config)
687
+ log.info("AOTAutograd cache hit for key %s", cache_key)
688
+ counters["aot_autograd"]["autograd_cache_hit"] += 1
689
+ cache_state = "hit"
690
+ cache_event_time = time.time_ns()
691
+ forward_time_saved = entry.forward_time_taken_ns // 1e6
692
+ backward_time_saved = entry.backward_time_taken_ns // 1e6
693
+ cache_info.update(
694
+ {
695
+ "forward_time_saved_ms": forward_time_saved,
696
+ "backward_time_saved_ms": backward_time_saved,
697
+ "time_saved_ms": forward_time_saved + backward_time_saved,
698
+ }
699
+ )
700
+ time_saved_ns = (
701
+ entry.forward_time_taken_ns + entry.backward_time_taken_ns
702
+ )
703
+ # TODO: should we use the same field for remote cache time saved for both
704
+ # FXGraphCache and AOTAutogradCache?
705
+ # get_metrics_context().increment(...)
706
+ if (
707
+ ephemeral_increase := add_ephemeral_timeout_increase_for_distributed(
708
+ time_saved_ns
709
+ )
710
+ ) != 0:
711
+ cache_info["ephemeral_timeout_increase"] = ephemeral_increase
712
+
713
+ if compiled_fn is None:
714
+ log.info("AOTAutograd cache miss for key %s", cache_key)
715
+ counters["aot_autograd"]["autograd_cache_miss"] += 1
716
+ cache_state = "miss"
717
+ cache_event_time = time.time_ns()
718
+ # Count missing the FXGraphCache as a miss not a bypass
719
+ except FXGraphCacheMiss as e:
720
+ counters["aot_autograd"]["autograd_cache_miss"] += 1
721
+ # Special counter when we pass autograd cache but
722
+ # fail when on inductor guards
723
+ counters["aot_autograd"]["autograd_cache_guard_miss"] += 1
724
+ cache_state = "miss"
725
+ if config.strict_autograd_cache:
726
+ raise e
727
+ # Most often this is BypassAOTAutogradCache, but
728
+ # if there's ever different reason we can't cache,
729
+ # we still never want to hard throw an exception, since
730
+ # we can always fallback to a cache bypass.
731
+ # As an example, if the user calls autograd via
732
+ # standalone inductor, we will sometimes get a GraphModule
733
+ # that doesn't actually have a `.graph` on it. Instead
734
+ # of checking every single case, we safely catch the exception
735
+ # in those cases.
736
+ except Exception as e:
737
+ cache_key = None
738
+ counters["aot_autograd"]["autograd_cache_bypass"] += 1
739
+ cache_state = "bypass"
740
+ cache_event_time = time.time_ns()
741
+ cache_info["cache_bypass_reason"] = str(e)
742
+ # TODO: this gets logged implicitly by cache_bypass_reason,
743
+ # and here we explicitly log it into tlparse.
744
+ # We may want to log this as an extra column in Scuba, though.
745
+ cache_info["cache_bypass_hard_exception"] = not isinstance(
746
+ e, BypassAOTAutogradCache
747
+ )
748
+ if remote:
749
+ log_cache_bypass("bypass_aot_autograd", str(e))
750
+ if config.strict_autograd_cache:
751
+ raise e
752
+ if compiled_fn is None:
753
+ # Set the cache key so we can save a cache result later
754
+ if cache_key is not None:
755
+ aot_config.cache_info = AOTAutogradCacheInfo(
756
+ cache_key, time.time_ns()
757
+ )
758
+ compiled_fn = dispatch_and_compile()
759
+
760
+ cache_info.update(
761
+ {
762
+ "key": cache_key,
763
+ "cache_state": cache_state,
764
+ "components": debug_lines,
765
+ }
766
+ )
767
+ chromium_log = get_chromium_event_logger()
768
+ chromium_log.log_instant_event(
769
+ f"autograd_cache_{cache_state}", cache_event_time, metadata=cache_info
770
+ )
771
+ chromium_log.try_add_event_data(
772
+ "backend_compile",
773
+ cache_state=cache_state,
774
+ cache_event_time=cache_event_time,
775
+ key=cache_info.get("key"),
776
+ components=cache_info.get("components"),
777
+ cache_bypass_reason=cache_info.get("cache_bypass_reason"),
778
+ remote_cache_enabled=remote,
779
+ local_cache_enabled=local,
780
+ )
781
+
782
+ torch._logging.trace_structured(
783
+ "artifact",
784
+ metadata_fn=lambda: {
785
+ "name": "aotautograd_cache_hash",
786
+ "encoding": "json",
787
+ },
788
+ payload_fn=lambda: json.dumps(cache_info),
789
+ )
790
+ return compiled_fn
791
+
792
+ @staticmethod
793
+ def _get_tmp_dir() -> str:
794
+ """
795
+ Get the toplevel temporary directory for storing compiled graphs.
796
+ """
797
+ return os.path.join(cache_dir(), "aotautograd")
798
+
799
+ @staticmethod
800
+ def _lookup(key: str, local: bool, remote: bool) -> Optional[AOTAutogradCacheEntry]:
801
+ """Given a key generated by AOTAutogradCachePickler, look up its location in the cache."""
802
+
803
+ if local:
804
+ subdir = os.path.join(AOTAutogradCache._get_tmp_dir(), key)
805
+ # If the directory doesn't exist, we didn't cache this key locally
806
+ if os.path.exists(subdir):
807
+ path = os.path.join(subdir, "entry")
808
+ try:
809
+ with open(path, "rb") as f:
810
+ entry: AOTAutogradCacheEntry = pickle.load(f)
811
+ return entry
812
+ except Exception as e:
813
+ log.warning(
814
+ "AOTAutograd cache unable to load compiled graph: %s", e
815
+ )
816
+ if config.strict_autograd_cache:
817
+ raise e
818
+
819
+ # Prefer local cache to remote, fallback to remote if local missed
820
+ if remote:
821
+ remote_cache: Optional[
822
+ RemoteCache[JsonDataTy]
823
+ ] = AOTAutogradCache.get_remote_cache()
824
+
825
+ if remote_cache is not None:
826
+ try:
827
+ if (cache_data := remote_cache.get(key)) is not None:
828
+ assert isinstance(cache_data, dict)
829
+ data = cache_data["data"]
830
+ assert isinstance(data, (str, bytes))
831
+ content = base64.b64decode(data)
832
+ # TODO: we currently don't have a way of logging the AOTAutograd output on a
833
+ # cache hit, because we never save it to the cache
834
+ # If we need to do that, we should do it here
835
+ return pickle.loads(content)
836
+ except Exception as e:
837
+ log_cache_bypass(
838
+ "bypass_aot_autograd", "Unable to deserialize: " + str(e)
839
+ )
840
+ log.warning(
841
+ "remote autograd cache unable to load compiled graph",
842
+ exc_info=True,
843
+ )
844
+
845
+ # Otherwise both caches missed
846
+ return None
847
+
848
+ @staticmethod
849
+ def save(key: str, entry: AOTAutogradCacheEntry, remote: bool):
850
+ """Save a single entry into the cache."""
851
+ try:
852
+ check_metadata_cacheable(entry.runtime_metadata)
853
+ content = pickle.dumps(entry)
854
+ except BypassAOTAutogradCache as e:
855
+ counters["aot_autograd"]["autograd_cache_bypass"] += 1
856
+ log.warning("Bypassing autograd cache due to: %s", e)
857
+ if remote:
858
+ log_cache_bypass("bypass_aot_autograd", str(e))
859
+ return None
860
+ except Exception as e:
861
+ log.warning("AOTAutograd cache unable to serialize compiled graph: %s", e)
862
+ if remote:
863
+ log_cache_bypass(
864
+ "bypass_aot_autograd", "Unable to serialize: " + str(e)
865
+ )
866
+ if config.strict_autograd_cache:
867
+ raise e
868
+ return None
869
+
870
+ subdir = os.path.join(AOTAutogradCache._get_tmp_dir(), key)
871
+ if not os.path.exists(subdir):
872
+ os.makedirs(subdir, exist_ok=True)
873
+ path = os.path.join(subdir, "entry")
874
+ log.info("Writing AOTAutograd cache entry to %s", path)
875
+ write_atomic(path, content)
876
+ counters["aot_autograd"]["autograd_cache_saved"] += 1
877
+
878
+ if remote:
879
+ remote_cache: Optional[
880
+ RemoteCache[JsonDataTy]
881
+ ] = AOTAutogradCache.get_remote_cache()
882
+ if remote_cache is not None:
883
+ time_taken_ms = int(
884
+ (entry.forward_time_taken_ns + entry.backward_time_taken_ns) // 1e6
885
+ )
886
+ cache_data: JsonDataTy = {
887
+ "data": base64.b64encode(content).decode("ascii"),
888
+ "time_taken_ms": time_taken_ms,
889
+ }
890
+ remote_cache.put(key, cache_data)
891
+
892
+ @staticmethod
893
+ @functools.lru_cache(None)
894
+ def get_remote_cache() -> Optional[RemoteCache[JsonDataTy]]:
895
+ """
896
+ Attempts to load the remote cache, returns None on error.
897
+ """
898
+ cache_id = "autograd-experimental"
899
+ return create_cache(
900
+ cache_id,
901
+ config.is_fbcode(),
902
+ "FbRemoteAOTAutogradCache",
903
+ "RemoteAOTAutogradCache",
904
+ )
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/collect_metadata_analysis.py ADDED
@@ -0,0 +1,813 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ This module is one of the analysis modules - it takes as input a function or graph
4
+ and some preexisting properties, and returns some data that is useful for deciding
5
+ how to further proceed with compilation or construct runtime wrappers.
6
+
7
+ In particular, the analysis here constructs view and mutation metadata from running
8
+ a functionalized version of the graph under compilation.
9
+ """
10
+
11
+ import collections
12
+ import contextlib
13
+ import logging
14
+ from functools import wraps
15
+ from typing import Callable, DefaultDict, Dict, List, Optional, Set
16
+
17
+ import torch
18
+ import torch.utils._pytree as pytree
19
+ from torch import Tensor
20
+ from torch._guards import detect_fake_mode
21
+ from torch._logging import getArtifactLogger
22
+ from torch._subclasses.functional_tensor import FunctionalTensor, FunctionalTensorMode
23
+ from torch._subclasses.meta_utils import safe_is_leaf
24
+ from torch.fx.experimental.symbolic_shapes import is_concrete_int
25
+ from torch.multiprocessing.reductions import StorageWeakRef
26
+ from torch.utils._python_dispatch import (
27
+ is_traceable_wrapper_subclass,
28
+ transform_subclass,
29
+ )
30
+
31
+ from .functional_utils import (
32
+ are_all_mutations_hidden_from_autograd,
33
+ are_all_mutations_under_no_grad_or_inference_mode,
34
+ from_fun,
35
+ has_data_mutation,
36
+ has_metadata_mutation,
37
+ MetadataKey,
38
+ to_fun,
39
+ was_inductor_storage_resized,
40
+ )
41
+ from .schemas import (
42
+ FunctionalTensorMetadataEq,
43
+ InputAliasInfo,
44
+ MutationType,
45
+ OutputAliasInfo,
46
+ OutputType,
47
+ ViewAndMutationMeta,
48
+ )
49
+ from .subclass_utils import create_subclass_meta
50
+ from .utils import _get_autocast_states, KNOWN_TYPES, strict_zip
51
+
52
+
53
+ zip = strict_zip
54
+
55
+ log = logging.getLogger(__name__)
56
+ static_input_logger = getArtifactLogger("torch._dynamo", "cudagraph_static_inputs")
57
+
58
+
59
+ # Note [Tangents memory format]
60
+ # We assume tangents memory format to be similar to corresponding output's memory_format.
61
+ # The idea is that we are technically making a guess about the strides of our tangents,
62
+ # while we trace out the joint.
63
+ # If runtime specfied tangents will not have the same memory format as predicted traced tangents,
64
+ # we coerce them at runtime to traced tangents memory format.
65
+
66
+
67
+ # Coercing and collecting traced tangents memory format in one recursive traversal
68
+ # mypy: ignore-errors
69
+ def coerce_tangent_and_suggest_memory_format(x: Tensor):
70
+ updated = False
71
+ if not isinstance(x, Tensor):
72
+ return x, None, updated
73
+
74
+ out = x.detach()
75
+
76
+ suggest_memory_format = torch._prims_common.suggest_memory_format
77
+ is_subclass = is_traceable_wrapper_subclass(out)
78
+
79
+ memory_format = suggest_memory_format(out)
80
+
81
+ was = out
82
+ out = out.contiguous(memory_format=memory_format)
83
+ updated = out is not was
84
+
85
+ # For subclass we keep memory format of outer strides at the beggining of the list
86
+ out_memory_format = [memory_format] if is_subclass else memory_format
87
+
88
+ # Note [Tangents memory format, Part 2]
89
+ # In the same way that "what strides do we assigns to our tangents" is a question
90
+ # that we can not answer (and therefore have to guess) as we trace the backward ahead-of-time,
91
+ # The same applies to any tensor subclass metadata, when we have tangents that are subclasses.
92
+ # To handle this situation, we have two new methods that a tensor subclass can implement:
93
+ # (1) __coerce_tangent_metadata__(self)
94
+ # Given a subclass with "non-standard" metadata, turn it into a new subclass with "normal" metadata.
95
+ # The main example here is a DTensor with the "_Partial" placement.
96
+ # If we have a forward output with a _Partial placement, and corresponding tangent
97
+ # with a Replicate/Shard placement, we have no way to convert the tangent "back" to a _Partial placement.
98
+ # This method lets us avoid the problem entirely by allowing subclasses to ensure that we can never
99
+ # have a tangent with "problematic" metadata, that we cannot convert to.
100
+ # (1) __coerce_same_metadata_as_tangent__(self, metadata)
101
+ # Given a subclass, and a target differing metadata,
102
+ # convert self to have the same metadata as the target.
103
+ # With DTensor being the main example, we can use this to convert a DTensor with a Replicate()
104
+ # placement into one with a Shard() placement, in the case that we "guessed wrong",
105
+ # and traced tangents with a Shard() placement at compile time.
106
+ #
107
+ if is_subclass and hasattr(out, "__coerce_tangent_metadata__"):
108
+ out = out.__coerce_tangent_metadata__() # type: ignore[attr-defined]
109
+
110
+ if is_subclass:
111
+ attrs = out.__tensor_flatten__()[0]
112
+
113
+ for attr in attrs:
114
+ elem = getattr(out, attr)
115
+ (
116
+ new_elem,
117
+ new_elem_memory_format,
118
+ elem_updated,
119
+ ) = coerce_tangent_and_suggest_memory_format(elem)
120
+ out_memory_format.append(new_elem_memory_format)
121
+ if elem_updated:
122
+ setattr(out, attr, new_elem)
123
+
124
+ return out, out_memory_format, updated
125
+
126
+
127
+ # This is a version of functionalization that is specifically designed
128
+ # for the AOTAutograd use case.
129
+ #
130
+ # Unlike functorch's variant, this doesn't use the functorch level system,
131
+ # instead it directly uses PyTorch's conventional dispatcher to hit the
132
+ # functionalization key. In particular, this means that FunctionalTensorWrapper
133
+ # can have autograd data stored directly on it.
134
+ #
135
+ # In typical AOTAutograd usage, the dispatch key order will look like:
136
+ #
137
+ # Autograd - Functionalization ~~~~> Proxy Mode - Fake Tensor
138
+ # outer tensor inner tensor
139
+ #
140
+ # Returns:
141
+ # - ViewAndMutationMeta, telling us metadata about the inputs and outputs, and
142
+ # The list of outputs from the forward, but **only** the outputs that we need
143
+ # to pass in as tangents into the backward.
144
+ # Specifically, aliased outputs from the forward get regenerated, and don't participate
145
+ # in the compiled backward function.
146
+ def run_functionalized_fw_and_collect_metadata(
147
+ f,
148
+ *,
149
+ keep_input_mutations: bool,
150
+ # TODO: refactor to kill this flag
151
+ is_train: bool = False,
152
+ # Note: this is guaranteed to be set when running under dynamo
153
+ static_input_indices: Optional[List[int]] = None,
154
+ pre_dispatch: bool = False,
155
+ # is_export is technically only needed to avoid using functionalization V2
156
+ # during analysis
157
+ is_export: bool = False,
158
+ ) -> Callable[..., ViewAndMutationMeta]:
159
+ memo: Dict[Tensor, Tensor] = {}
160
+
161
+ def _to_fun(t):
162
+ if isinstance(t, Tensor):
163
+ if t in memo:
164
+ return memo[t]
165
+ r = to_fun(t)
166
+ memo[t] = r
167
+ return r
168
+ else:
169
+ return t
170
+
171
+ @wraps(f)
172
+ def inner(*flat_args):
173
+ # This function is meant to be run with the forward, which expects a flat list of tensor/symint/other args.
174
+ assert all(isinstance(a, tuple(KNOWN_TYPES)) for a in flat_args)
175
+
176
+ input_info: List[InputAliasInfo] = []
177
+ output_info: List[OutputAliasInfo] = []
178
+
179
+ prior_grad_enabled = torch.is_grad_enabled()
180
+ prior_autocast_states = _get_autocast_states()
181
+
182
+ # See Note [Disabling Functionalize TLS Above Python Functionalization]
183
+ disable_above = torch._C._ExcludeDispatchKeyGuard(
184
+ torch._C.DispatchKeySet(torch._C.DispatchKey.Functionalize)
185
+ )
186
+
187
+ # It doesn't matter if we run this under predispatch or not because it is
188
+ # only for figuring out metadata
189
+ mode = FunctionalTensorMode(_allow_token_discovery=True, export=is_export)
190
+ suppress_pending = contextlib.nullcontext()
191
+ fake_mode = detect_fake_mode()
192
+ if fake_mode and (shape_env := fake_mode.shape_env):
193
+ suppress_pending = shape_env.ignore_fresh_unbacked_symbols()
194
+ with disable_above, mode, suppress_pending:
195
+ # precondition: The passed in function already handles unflattening inputs + flattening outputs
196
+ flat_f_args = pytree.tree_map(_to_fun, flat_args)
197
+ flat_f_outs = f(*flat_f_args)
198
+ # We didn't do any tracing, so we don't need to process the
199
+ # unbacked symbols, they will just disappear into the ether.
200
+ # Also, prevent memoization from applying.
201
+ if fake_mode:
202
+ fake_mode.epoch += 1
203
+ fake_mode.reset_nt_tensor_id_counter()
204
+
205
+ if prior_autocast_states != _get_autocast_states():
206
+ raise RuntimeError(
207
+ "AOTAutograd does not support tracing graphs that mutate the autocast state. "
208
+ "Dynamo will only insert autocast context managers (e.g. with torch.autocast(..)) into the graph, "
209
+ "which will unwind all of their mutations to autocast state before the graph exits. "
210
+ "If you encounter this error while using torch.compile, please file a bug."
211
+ )
212
+
213
+ # Inspect the state of the input tensor functional wrapper to detect input mutation info
214
+ # If inp[i] has a metadata-only mutation, then maybe_inputs_with_mutated_metadata[i] contains the updated version
215
+ for i, (arg, f_arg) in enumerate(zip(flat_args, flat_f_args)):
216
+ # NB: Mutation of non-contiguous tensor subclass input can result in a mismatch in
217
+ # strides between the functionalized arg inner tensors and non-functionalized arg inner
218
+ # tensors. This is a problem as the inner tensor stride change may not be reflected
219
+ # correctly in the outer tensor, so disallow this for now.
220
+ mutates_data = has_data_mutation(f_arg)
221
+ mutates_metadata = has_metadata_mutation(
222
+ f_arg, arg, check_only_storage_mutation=False
223
+ )
224
+ if mutates_metadata and is_traceable_wrapper_subclass(arg):
225
+ raise RuntimeError(
226
+ "Metadata mutations are currently not allowed on tensor subclasses"
227
+ )
228
+ mutates_storage_metadata = has_metadata_mutation(
229
+ f_arg, arg, check_only_storage_mutation=True
230
+ )
231
+ mutations_hidden_from_autograd = are_all_mutations_hidden_from_autograd(
232
+ f_arg
233
+ )
234
+ mutations_under_no_grad_or_inference_mode = (
235
+ mutates_data
236
+ and are_all_mutations_under_no_grad_or_inference_mode(f_arg)
237
+ )
238
+ mutation_inductor_storage_resize = was_inductor_storage_resized(f_arg)
239
+
240
+ if mutates_storage_metadata:
241
+ mutates_data = False
242
+
243
+ requires_grad = isinstance(f_arg, torch.Tensor) and f_arg.requires_grad
244
+
245
+ input_info.append(
246
+ InputAliasInfo(
247
+ is_leaf=isinstance(arg, Tensor) and safe_is_leaf(arg),
248
+ mutates_data=mutates_data,
249
+ mutates_metadata=mutates_metadata,
250
+ mutations_hidden_from_autograd=mutations_hidden_from_autograd,
251
+ mutates_storage_metadata=mutates_storage_metadata,
252
+ mutations_under_no_grad_or_inference_mode=mutations_under_no_grad_or_inference_mode,
253
+ mutation_inductor_storage_resize=mutation_inductor_storage_resize,
254
+ requires_grad=requires_grad,
255
+ keep_input_mutations=keep_input_mutations,
256
+ )
257
+ )
258
+
259
+ # If a function involves creating a tensor, and returning a view of it, such that its _base is the intermediate,
260
+ # We need to make sure our graph returns the _base as a graph output, and we manually recreate the view
261
+ # to return to the user. Why? The backend compiler is free to (incorrectly) not set requires_grad
262
+ # on the base tensor, but we are obligated to properly set requires-gradness on the real output.
263
+
264
+ inp_storage_refs = {
265
+ StorageWeakRef(inpt.untyped_storage()): idx
266
+ for idx, inpt in enumerate(flat_f_args)
267
+ if isinstance(inpt, Tensor)
268
+ }
269
+
270
+ # We need inp tensor id's to be able to tell if an outputs **are** inputs.
271
+ inp_tensor_ids = {id(inpt) for inpt in flat_f_args if isinstance(inpt, Tensor)}
272
+ # We need output tensor id's to tell if any output._base` attributes **are** other outputs.
273
+ # (This is also a dict because we need to know that output's index, so we can regenerate
274
+ # the alias from it).
275
+ out_tensor_ids = {id(o): i for i, o in enumerate(flat_f_outs)}
276
+
277
+ # Keep track of which outputs alias other outputs
278
+ out_tensor_alias_counts: DefaultDict = collections.defaultdict(int)
279
+ # This tells us, for a given group of outputs that alias each other,
280
+ # whether they e.g. all came from an unbind call
281
+ num_aliased_tensors_that_are_multi_output_views: DefaultDict = (
282
+ collections.defaultdict(int)
283
+ )
284
+
285
+ out_storage_to_metadata_key_to_tensors: DefaultDict[
286
+ Optional[StorageWeakRef], DefaultDict[MetadataKey, Set[torch.Tensor]]
287
+ ] = collections.defaultdict(lambda: collections.defaultdict(set))
288
+
289
+ curr_storage = None
290
+ for o in flat_f_outs:
291
+ if isinstance(o, torch.Tensor):
292
+ curr_storage = StorageWeakRef(o.untyped_storage())
293
+ out_tensor_alias_counts[curr_storage] += 1
294
+ # Note: [AOTAutograd: differentiable outputs that alias each other from a multi-output view call]
295
+ # This is an optimization on top of the "alias of intermediates" logic,
296
+ # which you can read more about under Note [AOT Autograd: outputs aliasing inputs or intermediates!]
297
+ #
298
+ # Before describing the optimization: this is important for AOTAutograd to have good
299
+ # perf around, multi-output views. HOWEVER:
300
+ # - There is a more generic change to AOTAutograd that we'd like to make, that subsumes this case,
301
+ # around using pre-dispatch tracing to partition out a graph so we can faithfully replay all
302
+ # views without having to regenerate them at runtime.
303
+ # - It's loosely described in this doc (more details will be added soon):
304
+ # https://docs.google.com/document/d/1DlfFq8TKbuAn2zyJxLfoW-X1qkkm5PLdHFtySo03QAk/edit
305
+ # - Once that change lands, we should just rip out this "optimization", since:
306
+ # (1) It will be fully unnecessary
307
+ # (2) Although it is only a few lines of code, it is a bit difficult to reason about
308
+ # its correctness with the autograd engine in all cases.
309
+ #
310
+ #
311
+ # What is this optimization? Consider the below case:
312
+ # def f(x):
313
+ # intermediate = x.mul(2)
314
+ # # x and intermediate here require grad
315
+ # o1, o2, ... o10 = intermediate.unbind(-1)
316
+ # return intermediate, o1, o2, ... o10
317
+ # Now, the "intermediate base" handling in AOTAutograd implies that we must do the following:
318
+ # (1) return "intermediate as an extra output of the compiled graph
319
+ # (2) regenerate each aliased output off of "intermediate", **outside** of the autograd.Function.
320
+ # The reason AOTAutograd ordinarily does this is for safety: the autograd engine needs to know
321
+ # that o1 through o10 are all aliased, and if we blindly return o1 through o10 from the autograd.Function,
322
+ # this information will be hidden.
323
+ # In particular, mutating one alias might require autograd to update autograd metadata on the other aliases
324
+ # (like their grad_fn, for example, when the autograd engine needs to do view-replay).
325
+ #
326
+ # However, intermediate_base logic can be bad for backward performance (we sometimes generate
327
+ # as_strided calls during the intermediate base logic, which can have a slow backward formula).
328
+ # Is it possible to find a set of conditions where it is **safe** to hide the output aliasing from autograd?
329
+ #
330
+ # For a set of outputs of the graph that alias each other, o_1...o_k, consider:
331
+ # (1) They came from the same multi-output view op, e.g. o_1, ..., o_k = intermediate.unbind(0)
332
+ # (2) If there are any other aliases of o_1 through o_k (in the example above, intermediate),
333
+ # **at most** 1 can escape from the graph (e.g. there is not some other graph input/output
334
+ # o_other, that aliases these outputs)
335
+ # (3) o_1...o_k all require_grad, they all share the same ._base, and their ._base requires grad.
336
+ # This condition is important because it's what causes slowness in the intermediate_base
337
+ # codepath of aot_autograd. Ordinarily, o_1...o_k would all get a grad_fn, and
338
+ # aot_autograd's view-replay might give each output an AsStridedBackward as its grad_fn.
339
+ # "K" AsStridedBackward calls will be *much* slower than a single UnbindBackward.
340
+ # In this setup, is it possible to mutate one of the outputs o_i in a way that would affect the autograd meta
341
+ # of the other aliases?
342
+ #
343
+ # Claim: No! Consider a few example (which I'm pretty sure cover all cases of mutation w.r.t. autograd):
344
+ # (a) What happens if we mutate any of o_1 through o_k directly?
345
+ # Autograd raises an error:
346
+ # "RuntimeError: Output 0 of UnbindBackward0 is a view and is being modified inplace. This view is
347
+ # the output of a function that returns multiple views. Such functions do not allow the output
348
+ # views to be modified inplace. You should replace the inplace operation by an out-of-place one."
349
+ # (b) What if we take a view of o_k and mutate it, o_k.view(o_k.shape).mul_(2)?
350
+ # Autograd raises the same error- the "multi-output-view"ness of an alias propagates to future views.
351
+ # (c) What if we mutate o_k under no_grad?
352
+ # Autograd raises the same error
353
+ # (d) What if we detach and mutate, e.g. o_k.detach().mul_(2)?
354
+ # Autograd allows this, *but* autograd updates all alias's grad_fn's to be error functions when accessed.
355
+ # Autograd raises the same error
356
+ # (e) What if we try to mutate another alias of o_1...o_k, that was **not** created from a multi-output view?
357
+ # We promised that there is at most **one** such alias, e.g. intermediate in the example above.
358
+ # You can mutate intermediate, but in eager mode this will change the grad_fn of o_1...o_k
359
+ # to be error fn's.
360
+ # Since intermediate was the *only* non-multi-output-alias, there are no other aliases
361
+ # of `intermediate` around that were produced by the compiled fn and have a valid grad_fn.
362
+ #
363
+ # Coming back to this optimization:
364
+ # Given that it is not possible for mutating one of these aliases to affect the autograd metadata of another alias
365
+ # without causing an error in eager mode, we will simple hide the aliasing from autograd during torch.compile
366
+ # if all of the above conditions are met.
367
+ # This has the slight downside that it's possible to write some "bad" code that autograd will raise an error on
368
+ # in eager but fail to during torch.compile, but it has the benefit that this code has much better performance.
369
+ # NOTE: if and when we eventually update AOTAutograd to do the "view graph slicing" defined here:
370
+ # https://docs.google.com/document/d/1DlfFq8TKbuAn2zyJxLfoW-X1qkkm5PLdHFtySo03QAk/edit,
371
+ # then this optimization will probably matter less and might be ok to remove.
372
+ is_cur_tensor_multi_out_view = isinstance(
373
+ o, FunctionalTensor
374
+ ) and torch._functionalize_is_multi_output_view( # type: ignore[attr-defined]
375
+ o.elem
376
+ )
377
+ if is_cur_tensor_multi_out_view:
378
+ num_aliased_tensors_that_are_multi_output_views[curr_storage] += 1
379
+ if o.requires_grad:
380
+ out_storage_to_metadata_key_to_tensors[curr_storage][
381
+ MetadataKey.make(o)
382
+ ].add(o)
383
+
384
+ # maps the id of an intermediate base to its index in the output of the compiled forward
385
+ intermediate_base_tensor_id_to_output_idx: Dict[int, int] = {}
386
+ intermediate_bases: List[torch.Tensor] = []
387
+ # Why Do We Care If Storage Changed?
388
+ # It's important to understand the implications of storage changes in complex scenarios. Take this example:
389
+ #
390
+ # def f(x):
391
+ # x_storage = x.untyped_storage()
392
+ # non_leaf_tensor = torch.ones(4, requires_grad=True).clone()
393
+ #
394
+ # # Using no_grad() and _unsafe_preserve_version_counter to simulate the .data = operation
395
+ # with torch.no_grad(), torch.autograd._unsafe_preserve_version_counter(x):
396
+ # x.set_(non_leaf_tensor.untyped_storage())
397
+ #
398
+ # out = x.view(-1)
399
+ #
400
+ # # Restoring x to its original storage, again simulating .data = operation
401
+ # with torch.no_grad(), torch.autograd._unsafe_preserve_version_counter(x):
402
+ # x.set_(x_storage)
403
+ #
404
+ # return out
405
+ #
406
+ # In this scenario, 'x' and 'out' have different shapes and are stored at different memory addresses, aka no aliasing.
407
+ # However, due to how set_() and more specificlaly, set is functionalized, is defined to preserve eager semantics,
408
+ # the autograd engine mistakenly assumes that 'x' and 'out' are aliased, treating 'x' as 'out._base'.
409
+ # This misinterpretation leads to an 'alias_of_input' flag, causing an unnecessary as_strided() call to be generated,
410
+ # which could lead to issues later in the code.
411
+ for o in flat_f_outs:
412
+ functional_tensor_storage_changed = isinstance(
413
+ o, FunctionalTensor
414
+ ) and torch._functionalize_was_storage_changed( # type: ignore[attr-defined]
415
+ o.elem
416
+ )
417
+ curr_storage = (
418
+ None
419
+ if not isinstance(o, torch.Tensor)
420
+ else StorageWeakRef(o.untyped_storage())
421
+ )
422
+ outs_with_identical_metadata_that_require_grad = (
423
+ []
424
+ if not isinstance(o, Tensor)
425
+ else [
426
+ curr
427
+ for curr in out_storage_to_metadata_key_to_tensors[curr_storage][
428
+ MetadataKey.make(o)
429
+ ]
430
+ if o is not curr
431
+ ]
432
+ )
433
+
434
+ # See Note [Accessing .grad_fn on FunctionalTensor]
435
+ # In-place operations on views will trigger a lazy rebase of the autograd graph;
436
+ # this runs during access to the .grad_fn. The rebase logic will invoke view ops
437
+ # on FunctionalTensors, so we must enable a FunctionalTensorMode here to ensure
438
+ # these op calls succeed.
439
+ grad_fn = None
440
+ if isinstance(o, Tensor):
441
+ with FunctionalTensorMode():
442
+ grad_fn = o.grad_fn
443
+
444
+ is_result_of_custom_autograd_fn = False
445
+ # Need to check for both custom cpp (CppFunction) and python (BackwardCFunction)
446
+ # autograd fns
447
+ if type(grad_fn).__name__ == "CppFunction":
448
+ is_result_of_custom_autograd_fn = True
449
+ if isinstance(grad_fn, torch.autograd.function.BackwardCFunction):
450
+ is_result_of_custom_autograd_fn = True
451
+
452
+ if not isinstance(o, Tensor):
453
+ output_type = OutputType.non_alias
454
+ base_idx = None
455
+ elif (
456
+ curr_storage in inp_storage_refs
457
+ and grad_fn is not None
458
+ and is_result_of_custom_autograd_fn
459
+ ):
460
+ output_type = OutputType.custom_function_view
461
+ base_idx = None
462
+ elif (
463
+ curr_storage in inp_storage_refs
464
+ and not functional_tensor_storage_changed
465
+ ):
466
+ base_idx = inp_storage_refs[curr_storage]
467
+ is_input_tensor = id(o) in inp_tensor_ids
468
+ num_aliased_outs = out_tensor_alias_counts[curr_storage]
469
+ num_multi_output_view_outs = (
470
+ num_aliased_tensors_that_are_multi_output_views[curr_storage]
471
+ )
472
+ num_aliased_outs_that_are_not_multi_output_views = (
473
+ num_aliased_outs - num_multi_output_view_outs
474
+ )
475
+ if (
476
+ grad_fn is not None
477
+ and num_aliased_outs_that_are_not_multi_output_views == 0
478
+ ):
479
+ # See Note: [AOTAutograd: differentiable outputs that alias each other from a multi-output view call]
480
+ # In particular, given:
481
+ # def f(x):
482
+ # return list(x.unbind(0))
483
+ # The main reason we ordinarily try to regenerate these output aliases outside of the
484
+ # compiled autograd.Function is because if any of the outputs are later mutated,
485
+ # autograd needs to perform view-replay to regenerate them.
486
+ # However, autograd does not allow users to mutate multi-output views
487
+ # in any way that can change the autograd metadata of other aliases.
488
+ # So we hide this aliasing from autograd here.
489
+ log.debug(
490
+ "Encountered AOTAutograd case: differentiable outputs that \
491
+ alias each other from a multi-output view call"
492
+ )
493
+ output_type = OutputType.non_alias
494
+ elif is_input_tensor:
495
+ output_type = OutputType.is_input
496
+ else:
497
+ output_type = OutputType.alias_of_input
498
+ elif functional_tensor_storage_changed and id(o) in inp_tensor_ids:
499
+ # When there is a set_() on an input, we cannot rely on checking storages
500
+ # to detect if we are returning an input (since the inputs storage is different)
501
+ assert curr_storage is not None
502
+ base_idx = inp_storage_refs[curr_storage]
503
+ output_type = OutputType.is_input
504
+
505
+ # We only need to handle the intermediate base case when both
506
+ # the intermediate base and the output require gradients.
507
+ # See Note [AOT Autograd: outputs aliasing inputs or intermediates!]
508
+ elif o._base is not None and o.requires_grad and o._base.requires_grad:
509
+ num_aliased_outs = out_tensor_alias_counts[curr_storage]
510
+ num_multi_output_view_outs = (
511
+ num_aliased_tensors_that_are_multi_output_views[curr_storage]
512
+ )
513
+ num_aliased_outs_that_are_not_multi_output_views = (
514
+ num_aliased_outs - num_multi_output_view_outs
515
+ )
516
+ # Note: [AOTAutograd: differentiable outputs that alias each other from a multi-output view call]
517
+ if (
518
+ out_tensor_alias_counts[curr_storage] == 1
519
+ or num_aliased_outs_that_are_not_multi_output_views <= 1
520
+ ):
521
+ # Note [Intermediate Bases Optimization]
522
+ # Normally if we have an output that aliases an intermediate,
523
+ # we need to add the extra "intermediate base" logic further down
524
+ # to prevent autograd from yelling at us if the user later tries to
525
+ # mutate that output.
526
+ # However, the common case here is if we have an output that aliases an intermediate,
527
+ # but doesn't alias any other outputs.
528
+ # In that case, autograd shouldn't have to worry about the aliasing at all
529
+ # (if that output is mutated, there are no other live aliases for autograd to worry about).
530
+ # The "intermediate bases" can hurt inductor perf by forcing more variables to become outputs.
531
+ # So as an optimization, we won't do intermediate base handling in this case.
532
+ # Instead, we'll hide the aliasing from autograd using aten._unsafe_view().
533
+ if (
534
+ out_tensor_alias_counts[curr_storage] != 1
535
+ and num_aliased_outs_that_are_not_multi_output_views <= 1
536
+ ):
537
+ log.debug(
538
+ "Encountered AOTAutograd case: differentiable outputs that alias each other \
539
+ from a multi-output view call"
540
+ )
541
+ output_type = OutputType.unsafe_view_alias
542
+ base_idx = None
543
+ else:
544
+ # First, check if o's ._base is an existing output
545
+ maybe_existing_out_idx = out_tensor_ids.get(id(o._base), None)
546
+ if maybe_existing_out_idx is not None:
547
+ # Special case where the output is an alias of a graph intermediate, but that intermediate
548
+ # is itself also a user output.
549
+ output_type = (
550
+ OutputType.alias_of_intermediate_base_is_user_output
551
+ )
552
+ base_idx = maybe_existing_out_idx
553
+ else:
554
+ # Next, check if o's ._base is an intermediate base that we already returned
555
+ maybe_existing_base_output_idx = (
556
+ intermediate_base_tensor_id_to_output_idx.get(
557
+ id(o._base), None
558
+ )
559
+ )
560
+ if maybe_existing_base_output_idx is not None:
561
+ output_type = OutputType.alias_of_intermediate
562
+ base_idx = maybe_existing_base_output_idx
563
+ else:
564
+ # Otherwise, take o._base and explicitly return it as an output in the compiled graph
565
+ new_out_idx = len(intermediate_bases)
566
+ base_idx = new_out_idx
567
+ # Indicate to the logic later on (when we trace the joint)
568
+ # that this particular output should get it's ._base appended to the forward graph outputs
569
+ output_type = (
570
+ OutputType.alias_of_intermediate_save_as_output
571
+ )
572
+ intermediate_base_tensor_id_to_output_idx[
573
+ id(o._base)
574
+ ] = new_out_idx
575
+ intermediate_bases.append(o._base)
576
+ elif (
577
+ # See https://github.com/pytorch/pytorch/issues/100348 for this case.
578
+ # This protects against the specific case where a user fn returns (output, output.detach())
579
+ out_tensor_alias_counts[curr_storage] > 1
580
+ and len(outs_with_identical_metadata_that_require_grad) > 0
581
+ and not o.requires_grad
582
+ ):
583
+ # In theory we could use any of these tensors to regenerate the aliased outputs from,
584
+ # since they all alias each other and have identical metatadata
585
+ out_alias = outs_with_identical_metadata_that_require_grad[0]
586
+ existing_out_idx = out_tensor_ids[id(out_alias)]
587
+ output_type = OutputType.alias_of_intermediate_base_is_user_output
588
+ base_idx = existing_out_idx
589
+ else:
590
+ output_type = OutputType.non_alias
591
+ base_idx = None
592
+
593
+ if isinstance(o, torch.Tensor):
594
+ dynamic_dims = {
595
+ i for i, s in enumerate(o.shape) if not is_concrete_int(s)
596
+ }
597
+ else:
598
+ dynamic_dims = None
599
+
600
+ # Save the current FunctionalTensor output.
601
+ #
602
+ # This will be used at runtime for reconstructing output views from
603
+ # their respective base tensors.
604
+ #
605
+ # The FunctionalTensor will be saved if one of the 2 conditions below
606
+ # is true:
607
+ functional_tensor = None
608
+ if (
609
+ # 1. If the output_type is either of:
610
+ # (i) alias_of_intermediate;
611
+ # (ii) alias_of_intermediate_save_as_output; or
612
+ # (iii) alias_of_intermediate_base_is_user_output.
613
+ #
614
+ # No need to worry about in-place view operations here, since
615
+ # this functionalization step elimitates mutations.
616
+ #
617
+ # i.e. we have access to the actual base tensor, before the
618
+ # in-place operation was applied.
619
+ output_type
620
+ in (
621
+ OutputType.alias_of_intermediate,
622
+ OutputType.alias_of_intermediate_save_as_output,
623
+ OutputType.alias_of_intermediate_base_is_user_output,
624
+ )
625
+ ) or (
626
+ # 2. If the output_type is alias_of_input, and no in-place view
627
+ # operationthe was run on the input (base tensor).
628
+ #
629
+ # In this case, we need to check for metadata mutation because
630
+ # the runtime explicitly reconstructs the inputs, before actually
631
+ # reconstructing the outputs. Due to in-place view operations, the
632
+ # fully reconstructed input may not be this output base tensor
633
+ # anymore.
634
+ output_type == OutputType.alias_of_input
635
+ and base_idx is not None
636
+ and not input_info[base_idx].mutates_metadata
637
+ ):
638
+ if isinstance(o, FunctionalTensor):
639
+ functional_tensor = FunctionalTensorMetadataEq(o.elem)
640
+
641
+ out_info = OutputAliasInfo(
642
+ output_type=output_type,
643
+ raw_type=type(o),
644
+ base_idx=base_idx,
645
+ dynamic_dims=dynamic_dims,
646
+ requires_grad=isinstance(o, torch.Tensor) and o.requires_grad,
647
+ functional_tensor=functional_tensor,
648
+ )
649
+ output_info.append(out_info)
650
+
651
+ # See Note [AOT Autograd: Views to avoid tangents aliasing inputs]
652
+ def view_avoid_dupes_with_primals(t):
653
+ if isinstance(t, Tensor) and is_traceable_wrapper_subclass(t):
654
+ return transform_subclass(
655
+ t, lambda _, inner_t: view_avoid_dupes_with_primals(inner_t)
656
+ )
657
+ if isinstance(t, Tensor):
658
+ return t.view(t.shape)
659
+ return t
660
+
661
+ # This analysis function returns *only* the outputs that are meant to be tangents to the backwards.
662
+ # Anything that aliases (inputs returned in the fw due to metadata mutations, or outputs that alias inputs/intermediates)
663
+ # are *regenerated* later, and not used directly in the autograd graph
664
+ def _plain_fake_tensor_like_subclass(x):
665
+ with detect_fake_mode():
666
+ return torch.empty(
667
+ x.shape, dtype=x.dtype, device=x.device, layout=x.layout
668
+ )
669
+
670
+ def _is_subclass_mutated_input_tangent_always_subclass(inp):
671
+ return (
672
+ isinstance(inp, torch.nested._internal.nested_tensor.NestedTensor)
673
+ or torch._functorch.config.disable_guess_zero_tangent_for_mutated_input_subclass
674
+ )
675
+
676
+ f_input_tangents = [
677
+ # Note: [AOTAutograd Tangent Subclassness for mutated inputs]
678
+ # Generally when creating tangents to trace with, we assume that tangents will have
679
+ # the same subclass-ness as their forward outs
680
+ # however: for tangents that correspond to input mutations, in practice it is more likely
681
+ # that these tangents will be plain tensors of zeros at runtime, so we tweak our guess
682
+ # to assume that these tangents should always be plaint tensors.
683
+ # Example:
684
+ # def f(x):
685
+ # x.mul_(2)
686
+ # return x + 1
687
+ # out = f(x)
688
+ # out.sum().backward()
689
+ # In the above code, we will have a tangent "x_updated_tangent",
690
+ # which will be a plain tensor of zeros, *unless* x is used in some compute after executing f
691
+ #
692
+ # However, there are exceptions to this logic. If a view is created from mutated input and is used in backward,
693
+ # The tangent for this subclass input will be a subclass tensor.
694
+ # Example:
695
+ # def f(a, b):
696
+ # a.mul_(2)
697
+ # b.mul_(3)
698
+ # return b.view(b.shape), a + b
699
+ # a_out, b_out = f(..., Subclass)
700
+ # (a * b).sum().backward()
701
+ #
702
+ # We can not deduce it easily now, so introducing a debug config to be able to turn off this for specific cases.
703
+ # NJT gurantees to have its tangent as NJT, because it has dedicated integration in Autograd
704
+ # See torch/csrc/autograd/python_function.cpp, use_zeros_like.
705
+ (
706
+ _plain_fake_tensor_like_subclass(inp)
707
+ if is_traceable_wrapper_subclass(inp)
708
+ and not _is_subclass_mutated_input_tangent_always_subclass(inp)
709
+ else inp
710
+ )
711
+ for inp, info in zip(flat_f_args, input_info)
712
+ if info.mutation_type == MutationType.MUTATED_OUT_GRAPH
713
+ and info.mutates_data
714
+ and info.requires_grad
715
+ ]
716
+ f_output_tangents = [
717
+ o
718
+ for o, info in zip(flat_f_outs, output_info)
719
+ if info.output_type
720
+ in [
721
+ OutputType.non_alias,
722
+ OutputType.unsafe_view_alias,
723
+ OutputType.custom_function_view,
724
+ ]
725
+ and issubclass(info.raw_type, torch.Tensor)
726
+ and info.requires_grad
727
+ ]
728
+ # intermediate bases are also included in the backward graph
729
+ f_tangents = f_input_tangents + f_output_tangents + intermediate_bases
730
+ traced_tangents = pytree.tree_map(from_fun, f_tangents)
731
+ traced_tangents = pytree.tree_map(
732
+ view_avoid_dupes_with_primals, traced_tangents
733
+ )
734
+
735
+ output_tangents_start_idx = len(f_input_tangents)
736
+ output_tangents_end_idx = output_tangents_start_idx + len(f_output_tangents)
737
+ traced_tangents = [
738
+ coerce_tangent_and_suggest_memory_format(tt)[0]
739
+ for i, tt in enumerate(traced_tangents)
740
+ ]
741
+ nonlocal static_input_indices
742
+ static_input_indices = static_input_indices or []
743
+ if torch._dynamo.compiled_autograd.in_compiled_autograd_region:
744
+ passed_indices = set(static_input_indices)
745
+ static_input_indices = [
746
+ i
747
+ for i, arg in enumerate(flat_args)
748
+ if (isinstance(arg, torch.nn.Parameter) or i in passed_indices)
749
+ ]
750
+
751
+ static_input_logger.debug(
752
+ "static input indices metadata analysis: %s", static_input_indices
753
+ )
754
+
755
+ f_mutated_inputs = [
756
+ inp
757
+ for inp, info in zip(flat_f_args, input_info)
758
+ if info.mutation_type == MutationType.MUTATED_OUT_GRAPH
759
+ ]
760
+ f_metadata_mutated_inputs = [
761
+ inp for inp, info in zip(flat_f_args, input_info) if info.mutates_metadata
762
+ ]
763
+ # This logic (annoyingly) re-figures out exactly what the outputs to the compiled fw graph will be.
764
+ # When handling subclasses, we need info about **all** outputs of compiled forward graph,
765
+ # so we know precisely which graph outputs to wrap back into tensor subclasses
766
+ # Ideally we would refactor this so not have an is_train flag, and have the separate
767
+ # inference and training paths decide which inputs/output to ask for subclass info on.
768
+ # However, we currently stash indexing information on each SubclassMeta about its order
769
+ # in the graph outputs list.
770
+ f_fw_graph_outs = list(flat_f_outs)
771
+ if is_train or not keep_input_mutations:
772
+ f_fw_graph_outs = f_mutated_inputs + f_fw_graph_outs
773
+ else:
774
+ # even when "keep_input_mutations" is True,
775
+ # we never keep metadata-only mutations in the fw graph
776
+ f_fw_graph_outs = f_metadata_mutated_inputs + f_fw_graph_outs
777
+ if is_train:
778
+ f_fw_graph_outs = f_fw_graph_outs + intermediate_bases
779
+ fw_graph_outs = pytree.tree_map(from_fun, f_fw_graph_outs)
780
+
781
+ grad_enabled_mutation = None
782
+ if torch.is_grad_enabled() != prior_grad_enabled:
783
+ grad_enabled_mutation = torch.is_grad_enabled()
784
+ torch.set_grad_enabled(
785
+ prior_grad_enabled
786
+ ) # Restore the prior state after tracing it
787
+ log.debug(
788
+ (
789
+ "grad_mode mutation encountered in graph. "
790
+ "Will emit mutation epilogue, to set grad_mode=%s"
791
+ ),
792
+ grad_enabled_mutation,
793
+ )
794
+
795
+ metadata = ViewAndMutationMeta(
796
+ input_info=input_info,
797
+ output_info=output_info,
798
+ num_intermediate_bases=len(intermediate_bases),
799
+ keep_input_mutations=keep_input_mutations,
800
+ traced_tangents=traced_tangents,
801
+ subclass_inp_meta=create_subclass_meta(flat_args),
802
+ subclass_fw_graph_out_meta=create_subclass_meta(fw_graph_outs),
803
+ subclass_tangent_meta=create_subclass_meta(
804
+ traced_tangents, count_symints=False, with_memory_format=True
805
+ ),
806
+ is_train=is_train,
807
+ grad_enabled_mutation=grad_enabled_mutation,
808
+ static_input_indices=static_input_indices,
809
+ tokens=mode._tokens,
810
+ )
811
+ return metadata
812
+
813
+ return inner
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/dispatch_and_compile_graph.py ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ This module dispatches the graphs to either the forward-only or joint compilation
4
+ pathways, taking into account the AOTConfig and the collected ViewAndMutationMetadata.
5
+ """
6
+
7
+ import dataclasses
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+ import torch
11
+ import torch.utils._pytree as pytree
12
+ import torch.utils.dlpack
13
+ from torch import Tensor
14
+ from torch._dispatch.python import enable_python_dispatcher
15
+ from torch._dynamo.utils import detect_fake_mode, lazy_format_graph_code
16
+ from torch._logging import getArtifactLogger, trace_structured
17
+ from torch._subclasses.functional_tensor import FunctionalTensorMode
18
+ from torch.fx.experimental.proxy_tensor import make_fx
19
+ from torchgen.utils import dataclass_repr
20
+
21
+ from .. import config
22
+ from .functional_utils import (
23
+ assert_functional_graph,
24
+ propagate_input_mutation_stacktraces,
25
+ )
26
+ from .schemas import AOTConfig, SubclassMeta, ViewAndMutationMeta
27
+ from .traced_function_transforms import (
28
+ aot_dispatch_subclass,
29
+ create_functionalized_fn,
30
+ create_joint,
31
+ fn_input_mutations_to_outputs,
32
+ fn_prepped_for_autograd,
33
+ handle_effect_tokens_fn,
34
+ )
35
+ from .utils import (
36
+ copy_fwd_metadata_to_bw_nodes,
37
+ register_buffer_assignment_hook,
38
+ root_module_when_exporting_non_strict,
39
+ unlift_tokens,
40
+ )
41
+
42
+
43
+ aot_graphs_log = getArtifactLogger(__name__, "aot_graphs")
44
+
45
+
46
+ def _create_graph(f, args, *, aot_config: AOTConfig) -> torch.fx.GraphModule:
47
+ # FunctionalTensorMode must be enabled here.
48
+ # See Note [Accessing .grad_fn on FunctionalTensor]
49
+ with enable_python_dispatcher(), FunctionalTensorMode(
50
+ pre_dispatch=aot_config.pre_dispatch,
51
+ export=aot_config.is_export,
52
+ # Allow token discovery for joint fn tracing as tokens can be used in backward.
53
+ _allow_token_discovery=True,
54
+ ):
55
+ fx_g = make_fx(
56
+ f,
57
+ decomposition_table=aot_config.decompositions,
58
+ record_module_stack=True,
59
+ pre_dispatch=aot_config.pre_dispatch,
60
+ )(*args)
61
+
62
+ return fx_g
63
+
64
+
65
+ # TODO: Refactor the following code so detach() persists item_memo
66
+ def _detach_and_copy_item_memo(t):
67
+ detached_t = t.detach()
68
+ if hasattr(t, "item_memo"):
69
+ detached_t.item_memo = t.item_memo
70
+ return detached_t
71
+
72
+
73
+ def aot_dispatch_base_graph(
74
+ flat_fn,
75
+ flat_args: List[Tensor],
76
+ aot_config: AOTConfig,
77
+ *,
78
+ fw_metadata: ViewAndMutationMeta,
79
+ ) -> Tuple[torch.fx.GraphModule, List[Any], Optional[SubclassMeta]]:
80
+ # aot_dispatch_base requires functionalization, but doesn't need to handle as many cases as the autograd case.
81
+ # The cases that aot_dispatch_base doesn't need to handle include:
82
+ # - outputs that are aliases of graph intermediates
83
+ # - outputs that are aliases of graph inputs
84
+ # While cases that it does need to handle include:
85
+ # - input mutations (including when inputs are aliases of each other)
86
+ # - input metadata mutations
87
+ fn_to_trace = fn_input_mutations_to_outputs(
88
+ flat_fn,
89
+ fw_metadata,
90
+ keep_data_input_mutations=aot_config.keep_inference_input_mutations,
91
+ )
92
+
93
+ fn_to_trace, updated_flat_args = create_functionalized_fn(
94
+ fn_to_trace,
95
+ flat_args,
96
+ meta=fw_metadata,
97
+ aot_config=aot_config,
98
+ trace_joint=False,
99
+ )
100
+
101
+ # TODO: replace with AOTDispatchSubclassWrapper once we refactor
102
+ # fn_input_mutations_to_outputs and create_functionalized_fn
103
+ # into CompilerWrappers.
104
+ (
105
+ fn_to_trace,
106
+ updated_flat_args_subclasses_desugared,
107
+ maybe_subclass_meta,
108
+ ) = aot_dispatch_subclass(
109
+ fn_to_trace,
110
+ updated_flat_args,
111
+ is_joint_structure=False,
112
+ meta=fw_metadata,
113
+ fw_only=flat_fn,
114
+ )
115
+
116
+ (fn_to_trace, updated_flat_args_subclasses_desugared) = handle_effect_tokens_fn(
117
+ fn_to_trace,
118
+ updated_flat_args_subclasses_desugared,
119
+ meta=fw_metadata,
120
+ trace_joint=False,
121
+ )
122
+
123
+ aot_graphs_log.debug(
124
+ "aot_config id: %s, fw_metadata=%s,subclass_metadata=%s",
125
+ str(aot_config.aot_id),
126
+ str(fw_metadata),
127
+ str(maybe_subclass_meta),
128
+ )
129
+
130
+ # We track buffer assignments when exporting in non-strict mode.
131
+ # (In contrast, strict mode errors on any attribute assignment.)
132
+ mod_when_exporting_non_strict = root_module_when_exporting_non_strict(flat_fn)
133
+ if aot_config.is_export and mod_when_exporting_non_strict is not None:
134
+ # For any buffer that is assigned, we want to associate it to the final proxy node
135
+ # that it is assigned to. This node can then be added as a buffer mutation output.
136
+ assigned_buffers: Dict[str, str] = {}
137
+ hook = register_buffer_assignment_hook(
138
+ mod_when_exporting_non_strict, assigned_buffers
139
+ )
140
+
141
+ fake_mode = detect_fake_mode()
142
+ if fake_mode:
143
+ saved_updated_flat_args_subclasses_desugared = pytree.tree_map_only(
144
+ torch.Tensor,
145
+ _detach_and_copy_item_memo,
146
+ updated_flat_args_subclasses_desugared,
147
+ )
148
+ else:
149
+ saved_updated_flat_args_subclasses_desugared = pytree.tree_map_only(
150
+ torch.Tensor, lambda t: t.detach(), updated_flat_args_subclasses_desugared
151
+ )
152
+
153
+ fw_module = _create_graph(
154
+ fn_to_trace,
155
+ updated_flat_args_subclasses_desugared,
156
+ aot_config=aot_config,
157
+ )
158
+
159
+ if aot_config.is_export and mod_when_exporting_non_strict is not None:
160
+ # We update metadata to consider any assigned buffers as buffer mutations.
161
+ i = len(dict(mod_when_exporting_non_strict.named_parameters()))
162
+ for name, _ in mod_when_exporting_non_strict.named_buffers():
163
+ if name in assigned_buffers and not fw_metadata.input_info[i].mutates_data: # type: ignore[possibly-undefined]
164
+ fw_metadata.input_info[i] = dataclasses.replace(
165
+ fw_metadata.input_info[i], mutates_data=True
166
+ )
167
+ fw_metadata.num_mutated_inp_runtime_indices += 1
168
+ i += 1
169
+
170
+ # We add nodes corresponding to buffer assignments as output nodes in the graph.
171
+ add_nodes = []
172
+ output_node = list(fw_module.graph.nodes)[-1]
173
+ for name in assigned_buffers.values(): # type: ignore[possibly-undefined]
174
+ for node in fw_module.graph.nodes:
175
+ if node.name == name:
176
+ add_nodes.append(node)
177
+ node.users[output_node] = None
178
+ output_node.args = ((*add_nodes, *output_node.args[0]),)
179
+
180
+ hook.remove() # type: ignore[possibly-undefined]
181
+
182
+ # As long as we opted to remove input mutations, then
183
+ # there should be *NO* mutating ops in the graph at this point.
184
+ copy_count = assert_functional_graph(fw_module.graph)
185
+ fw_module.graph.eliminate_dead_code()
186
+ fw_module.recompile()
187
+
188
+ copy_count2 = assert_functional_graph(fw_module.graph)
189
+ propagate_input_mutation_stacktraces(fw_module.graph)
190
+
191
+ # See Note [Side-Effectful Tokens in AOTAutograd]
192
+ num_tokens = len(fw_metadata.tokens)
193
+ if num_tokens != 0 and config.unlift_effect_tokens:
194
+ unlift_tokens(fw_module, fw_metadata, aot_config)
195
+ saved_updated_flat_args_subclasses_desugared = (
196
+ saved_updated_flat_args_subclasses_desugared[num_tokens:]
197
+ )
198
+
199
+ assert copy_count == copy_count2
200
+
201
+ if aot_config.enable_log:
202
+ aot_graphs_log.info(
203
+ "%s",
204
+ lazy_format_graph_code(
205
+ "Forward graph",
206
+ fw_module,
207
+ aot_config.aot_id,
208
+ include_stride=True,
209
+ include_device=True,
210
+ colored=True,
211
+ ),
212
+ )
213
+
214
+ trace_structured(
215
+ "artifact",
216
+ metadata_fn=lambda: {
217
+ "name": "aot_forward_graph_fw_metadata",
218
+ "encoding": "string",
219
+ },
220
+ payload_fn=lambda: dataclass_repr(fw_metadata),
221
+ )
222
+ if maybe_subclass_meta is not None:
223
+ trace_structured(
224
+ "artifact",
225
+ metadata_fn=lambda: {
226
+ "name": "aot_forward_graph_fw_subclass_metadata",
227
+ "encoding": "string",
228
+ },
229
+ payload_fn=lambda: dataclass_repr(maybe_subclass_meta),
230
+ )
231
+
232
+ trace_structured(
233
+ "aot_inference_graph",
234
+ payload_fn=lambda: fw_module.print_readable(
235
+ print_output=False, include_stride=True, include_device=True
236
+ ),
237
+ )
238
+
239
+ # TODO: should factor this into a separate function for export that always only returns just the graph.
240
+ if aot_config.is_export:
241
+ assert (
242
+ maybe_subclass_meta is None
243
+ ), "aot_export_module does not support tensor subclass inputs for now."
244
+ return fw_module, saved_updated_flat_args_subclasses_desugared, maybe_subclass_meta
245
+
246
+
247
+ # Has the precondition that there
248
+ # are no duplicate arguments in flat_args (e.g., the same Tensor
249
+ # object never shows up twice. However, two tensor inputs MAY alias
250
+ # the same storage, so long as they have separate TensorImpls.)
251
+ def aot_dispatch_autograd_graph(
252
+ flat_fn,
253
+ flat_args: List[Any],
254
+ aot_config: AOTConfig,
255
+ *,
256
+ fw_metadata: ViewAndMutationMeta,
257
+ ) -> Tuple[torch.fx.GraphModule, Tuple[List[Any], List[Any]], Optional[SubclassMeta]]:
258
+ # traced_tangents corresponds to the set of outputs in the traced forward that should get grad_outputs in the traced backward.
259
+ # It includes outputs of the original forward, *and* any updated inputs due to input mutations.
260
+ # However, it does *not* include any outputs that are aliases of inputs or intermediates, or any metadata-only input mutations.
261
+ joint_inputs = (flat_args, fw_metadata.traced_tangents)
262
+
263
+ fn_prepared_for_autograd = fn_prepped_for_autograd(
264
+ flat_fn,
265
+ fw_metadata,
266
+ )
267
+ joint_fn_to_trace = create_joint(fn_prepared_for_autograd, aot_config=aot_config)
268
+
269
+ joint_fn_to_trace, updated_joint_inputs = create_functionalized_fn(
270
+ joint_fn_to_trace,
271
+ joint_inputs,
272
+ meta=fw_metadata,
273
+ aot_config=aot_config,
274
+ trace_joint=True,
275
+ )
276
+
277
+ # TODO: replace with AOTDispatchSubclassWrapper once we refactor
278
+ # fn_input_mutations_to_outputs and create_functionalized_fn
279
+ # into CompilerWrappers.
280
+ subclass_tracing_info = aot_dispatch_subclass(
281
+ joint_fn_to_trace,
282
+ updated_joint_inputs,
283
+ is_joint_structure=True,
284
+ meta=fw_metadata,
285
+ fw_only=flat_fn,
286
+ )
287
+
288
+ joint_fn_to_trace = subclass_tracing_info.plain_tensor_trace_fn
289
+ updated_joint_inputs = subclass_tracing_info.plain_tensor_args
290
+
291
+ (joint_fn_to_trace, updated_joint_inputs) = handle_effect_tokens_fn(
292
+ joint_fn_to_trace,
293
+ updated_joint_inputs,
294
+ meta=fw_metadata,
295
+ trace_joint=True,
296
+ )
297
+
298
+ # When we call _create_graph, this may mutate the metadata of joint
299
+ # inputs. But callers are expecting to get the original joint inputs. So
300
+ # we make aliases of all the inputs to make sure we have a copy that
301
+ # doesn't get modified.
302
+ #
303
+ # This destroys requires_grad/grad_fn information. However, backends
304
+ # beneath AOTAutograd are indifferent to this information, so it doesn't
305
+ # matter.
306
+
307
+ fake_mode = detect_fake_mode()
308
+ if fake_mode:
309
+ saved_updated_joint_inputs = pytree.tree_map_only(
310
+ torch.Tensor, _detach_and_copy_item_memo, updated_joint_inputs
311
+ )
312
+ else:
313
+ saved_updated_joint_inputs = pytree.tree_map_only(
314
+ torch.Tensor, lambda t: t.detach(), updated_joint_inputs
315
+ )
316
+ maybe_subclass_meta = subclass_tracing_info.maybe_subclass_meta
317
+
318
+ fx_g = _create_graph(joint_fn_to_trace, updated_joint_inputs, aot_config=aot_config)
319
+
320
+ # There should be *NO* mutating ops in the graph at this point.
321
+ assert_functional_graph(fx_g.graph)
322
+
323
+ # Redundant with the check above, but worth having in case tracing introduced
324
+ # a fake tensor. Unlikely.
325
+ # See Note: [Fake Modules and AOTAutograd]
326
+ torch._dynamo.utils.assert_no_fake_params_or_buffers(fx_g)
327
+ fx_g.graph.eliminate_dead_code()
328
+ copy_fwd_metadata_to_bw_nodes(fx_g)
329
+ fx_g.recompile()
330
+
331
+ # TODO: in AOTAutograd, we create metadata like _indices_of_inps_to_detach to detect
332
+ # when we need to manually detach() some inputs in the forward.
333
+ # Higher order ops might eventually need to do the same.
334
+ if aot_config.is_export:
335
+ assert (
336
+ maybe_subclass_meta is None
337
+ ), "aot_export_module does not support tensor subclass inputs for now."
338
+ return fx_g, saved_updated_joint_inputs, maybe_subclass_meta
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/functional_utils.py ADDED
@@ -0,0 +1,528 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ This file contains utilities related to functionalization in AOTAutograd:
4
+ 1. converting to/from functional tensors
5
+ 2. detecting Tensor mutations - both metadata and Tensor value
6
+ 3. regenerating/replaying views from their base
7
+ 4. checking if a graph is functional i.e. whether it contains any mutation ops
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Optional, Tuple
13
+
14
+ import torch
15
+ from torch import Tensor
16
+ from torch._logging import getArtifactLogger
17
+ from torch._subclasses.fake_tensor import FakeTensor
18
+ from torch._subclasses.functional_tensor import FunctionalTensor
19
+ from torch._subclasses.meta_utils import is_sparse_any
20
+ from torch.fx.experimental.symbolic_shapes import (
21
+ definitely_true,
22
+ sym_eq,
23
+ SymIntEqByExpr,
24
+ )
25
+ from torch.multiprocessing.reductions import StorageWeakRef
26
+ from torch.utils._python_dispatch import (
27
+ is_traceable_wrapper_subclass,
28
+ transform_subclass,
29
+ )
30
+
31
+
32
+ aot_joint_log = getArtifactLogger(__name__, "aot_joint_graph")
33
+
34
+
35
+ def to_fun(t):
36
+ if isinstance(t, Tensor):
37
+ if is_traceable_wrapper_subclass(t):
38
+ # See Note [Functionalization always runs last]
39
+ # This means that if we want to "functionalize" a subclass, we need to ensure that the functional wrapper
40
+ # goes at the bottom.
41
+ # recurse here, so we can support nested wrapper subclasses
42
+ out = transform_subclass(t, lambda _, inner_t: to_fun(inner_t))
43
+ torch._mirror_autograd_meta_to(t, out) # type: ignore[attr-defined]
44
+ return out
45
+ else:
46
+ return FunctionalTensor.to_functional(t)
47
+ else:
48
+ return t
49
+
50
+
51
+ def sync_functional_tensor(t):
52
+ if is_traceable_wrapper_subclass(t):
53
+ attrs, ctx = t.__tensor_flatten__() # type: ignore[attr-defined]
54
+ for attr in attrs:
55
+ sync_functional_tensor(getattr(t, attr))
56
+ else:
57
+ torch._sync(t)
58
+
59
+
60
+ # When subclasses are involved, t here will usually look something like:
61
+ # SubclassA(SubclassB(FunctionalTensor(_to_fun_tensor(FakeTensor))))
62
+ def from_fun(t):
63
+ if isinstance(t, Tensor) and is_traceable_wrapper_subclass(t):
64
+ # See Note [Functionalization always runs last]
65
+ # This means that if we want to "functionalize" a subclass, we need to ensure that the functional wrapper
66
+ # goes at the bottom.
67
+ # recurse here, so we can support nested wrapper subclasses
68
+ out = transform_subclass(t, lambda _, inner_t: from_fun(inner_t))
69
+ torch._mirror_autograd_meta_to(t, out) # type: ignore[attr-defined]
70
+ return out
71
+
72
+ if not isinstance(t, FunctionalTensor):
73
+ # quick sanity assert
74
+ if isinstance(t, torch.Tensor):
75
+ assert not torch._is_functional_tensor(t) # type: ignore[attr-defined]
76
+ return t
77
+ sync_functional_tensor(t)
78
+ return torch._from_functional_tensor(t.elem)
79
+
80
+
81
+ def is_fun(t):
82
+ if isinstance(t, Tensor) and is_traceable_wrapper_subclass(t):
83
+ # See Note [Functionalization always runs last]
84
+ # This means that if we want to "functionalize" a subclass, we need to ensure that the functional wrapper
85
+ # goes at the bottom.
86
+ # recurse here, so we can support nested wrapper subclasses
87
+ t_attrs, _ = t.__tensor_flatten__() # type: ignore[attr-defined]
88
+ t_inners = [getattr(t, attr) for attr in t_attrs]
89
+ any_fun = any(is_fun(x) for x in t_inners)
90
+ all_fun = all(is_fun(x) for x in t_inners)
91
+ assert any_fun == all_fun
92
+ return any_fun
93
+
94
+ return isinstance(t, FunctionalTensor)
95
+
96
+
97
+ # t here is either
98
+ # (1) A FunctionalTensor(_to_functional_tensor(FakeTensor))
99
+ # (2) A traceable tensor subclass that holds a FunctionalTensor
100
+ # (3) Not a tensor
101
+ def has_data_mutation(t):
102
+ if is_traceable_wrapper_subclass(t):
103
+ attrs, _ = t.__tensor_flatten__()
104
+ # A tensor subclass was updated if any of its inner elements were updated
105
+ return any(has_data_mutation(getattr(t, attr)) for attr in attrs)
106
+ else:
107
+ if isinstance(t, torch.Tensor):
108
+ assert isinstance(t, FunctionalTensor)
109
+ return torch._functionalize_has_data_mutation(t.elem) # type: ignore[attr-defined]
110
+ return False
111
+
112
+
113
+ def are_all_mutations_hidden_from_autograd(t):
114
+ if is_traceable_wrapper_subclass(t):
115
+ attrs, _ = t.__tensor_flatten__()
116
+ # If all inner elements are mutations hidden from autograd, then it is a mutation hidden from autograd.
117
+ return all(
118
+ are_all_mutations_hidden_from_autograd(getattr(t, attr)) for attr in attrs
119
+ )
120
+ elif isinstance(t, torch.Tensor):
121
+ assert isinstance(t, FunctionalTensor)
122
+ return torch._functionalize_are_all_mutations_hidden_from_autograd(t.elem)
123
+ else:
124
+ return False
125
+
126
+
127
+ def are_all_mutations_under_no_grad_or_inference_mode(t):
128
+ if is_traceable_wrapper_subclass(t):
129
+ attrs, _ = t.__tensor_flatten__()
130
+ return all(
131
+ are_all_mutations_under_no_grad_or_inference_mode(getattr(t, attr))
132
+ for attr in attrs
133
+ )
134
+ else:
135
+ assert isinstance(t, FunctionalTensor)
136
+ return torch._functionalize_are_all_mutations_under_no_grad_or_inference_mode(
137
+ t.elem
138
+ )
139
+
140
+
141
+ def was_inductor_storage_resized(t):
142
+ if is_traceable_wrapper_subclass(t):
143
+ attrs, _ = t.__tensor_flatten__()
144
+ if any(was_inductor_storage_resized(getattr(t, attr)) for attr in attrs):
145
+ raise RuntimeError(
146
+ f"storage resizing is not supported on tensor subclass: {type(t)}"
147
+ )
148
+ elif not isinstance(t, torch.Tensor):
149
+ return False
150
+ else:
151
+ assert isinstance(t, FunctionalTensor)
152
+ return torch._functionalize_was_inductor_storage_resized(t.elem)
153
+
154
+
155
+ # f_arg here is either
156
+ # (1) A FunctionalTensor(_to_functional_tensor(FakeTensor))
157
+ # (2) A traceable tensor subclass that holds a FunctionalTensor
158
+ # (3) Not a tensor
159
+ # Assumption: arg promises to be the "original" tensor wrapped by f_arg
160
+ # Note: "storage mutations" coming from set_() are a type of metadata mutation. So:
161
+ # - check_only_storage_mutation=True: only return true if there was a storage mutation
162
+ # - check_only_storage_mutation=Flse: return true if there was any metadata mutation (including a storage mutation)
163
+ def has_metadata_mutation(f_arg, arg, *, check_only_storage_mutation: bool):
164
+ if is_traceable_wrapper_subclass(f_arg):
165
+ attrs, _ = f_arg.__tensor_flatten__()
166
+ # A tensor subclass was updated if any of its inner elements were updated
167
+ f_inner_ts = [getattr(f_arg, attr) for attr in attrs]
168
+ inner_ts = [getattr(arg, attr) for attr in attrs]
169
+ return any(
170
+ has_metadata_mutation(
171
+ f_inner_t,
172
+ inner_t,
173
+ check_only_storage_mutation=check_only_storage_mutation,
174
+ )
175
+ for f_inner_t, inner_t in zip(f_inner_ts, inner_ts)
176
+ )
177
+ else:
178
+ if not isinstance(f_arg, torch.Tensor):
179
+ assert not isinstance(arg, torch.Tensor)
180
+ return False
181
+ assert isinstance(f_arg, FunctionalTensor)
182
+ assert isinstance(arg, FakeTensor)
183
+
184
+ arg_after = torch._from_functional_tensor(f_arg.elem)
185
+ # This is true if the current tensor experienced at least one set_() call
186
+ maybe_storage_changed = torch._functionalize_was_storage_changed(f_arg.elem) # type: ignore[attr-defined]
187
+ # However, multiple set_() calls can cancel out. So we also check whether the
188
+ # storage of the tensor has changed.
189
+ # Note: if an input experienced two set_() calls that cancel out, **and**
190
+ # it experiences an data mutation, we pessimistically think that the set_()
191
+ # call is necessary here. We could in theory fix this, but this will
192
+ # hopefully never happen in user code, and is not needed for fsdp.
193
+ if is_sparse_any(arg):
194
+ # TODO:add sparse tensors support to functionalization
195
+ same_storages = False
196
+ else:
197
+ same_storages = StorageWeakRef(arg.untyped_storage()) == StorageWeakRef(
198
+ arg_after.untyped_storage()
199
+ )
200
+ has_storage_metadata_mutation = maybe_storage_changed and not same_storages
201
+ if check_only_storage_mutation:
202
+ return has_storage_metadata_mutation
203
+
204
+ # storage metadata mutation is a type of metadata mutation, so return true if we saw one
205
+ if has_storage_metadata_mutation:
206
+ return True
207
+
208
+ maybe_metadata_mutated = torch._functionalize_has_metadata_mutation(f_arg.elem) # type: ignore[attr-defined]
209
+ # This is true if the current tensor experienced at least one metadata mutation.
210
+ # So if false, we know there was no metadata mutation
211
+ if not maybe_metadata_mutated:
212
+ return False
213
+
214
+ # However, multi metadata mutations can cancel out.
215
+ # So we also check if the concrete sizes/strides on the tensor have changed.
216
+ same_sizes = arg.shape == arg_after.shape
217
+ same_strides = arg.stride() == arg_after.stride()
218
+ same_offsets = arg.storage_offset() == arg_after.storage_offset()
219
+ has_metadata_mutation_ = maybe_metadata_mutated and not (
220
+ same_sizes and same_strides and same_offsets
221
+ )
222
+ # We consider a tensor to have been metadata mutated if its storage was mutated through a set_() call.
223
+ return has_metadata_mutation_
224
+
225
+
226
+ def gen_alias_from_base(
227
+ aliased_base_tensor,
228
+ target_meta_tensor,
229
+ target_requires_grad,
230
+ target_functional_tensor: Optional[FunctionalTensorMetadataEq] = None,
231
+ *,
232
+ replay_views,
233
+ ):
234
+ # Patch the correct requires_grad field of the output tensor, depending on whether:
235
+ # (i) the reconstructed output (out) was came from a tensor that requires grad or not;
236
+ # and (ii) the concrete returned output does require grad or not.
237
+ def patch_requires_grad(out):
238
+ if aliased_base_tensor.requires_grad and not target_requires_grad:
239
+ out = out.detach()
240
+ elif not aliased_base_tensor.requires_grad and target_requires_grad:
241
+ out.requires_grad_(True)
242
+ return out
243
+
244
+ # If provided, use the target functional tensor for replaying the views.
245
+ #
246
+ # In summary, we use the fact that FunctionalTensorWrapper saves the view
247
+ # functions applied to itself (collected during functionalization) so as
248
+ # to replay them (view functions) on the aliased_base_tensor.
249
+ if (
250
+ replay_views
251
+ and target_functional_tensor is not None
252
+ and not torch._functionalize_is_symbolic(target_functional_tensor.tensor)
253
+ ):
254
+ functional_tensor = target_functional_tensor.tensor
255
+
256
+ out = torch._functionalize_apply_view_metas(
257
+ functional_tensor, aliased_base_tensor
258
+ )
259
+ # If re-applying the ViewMeta sequence succeeded, there should be no more
260
+ # problems going forward. We just check we got to the target shape and
261
+ # patch requires_grad flag.
262
+ assert out.shape == target_meta_tensor.shape, (
263
+ "incorrect out shape after application of ViewMeta sequence: "
264
+ f"{tuple(out.shape)} (actual) vs {tuple(target_meta_tensor.shape)} (expected)"
265
+ )
266
+ return patch_requires_grad(out)
267
+
268
+ # Try to do view-replay if possible.
269
+ # fall back to .as_strided() if we can't.
270
+ if target_meta_tensor._base is not None:
271
+ # The base that we want to replay our view off of might have a different shape than the view's original base.
272
+ b = target_meta_tensor._base
273
+ abt = aliased_base_tensor
274
+ # Don't unnecessarily call as_strided if nothing changed; as_strided's
275
+ # backward is poorly implemented and slow
276
+ if abt is not b and (
277
+ abt.size() != b.size()
278
+ or abt.stride() != b.stride()
279
+ or abt.storage_offset() != b.storage_offset()
280
+ ):
281
+ reshaped_base_tensor = aliased_base_tensor.as_strided(
282
+ b.size(), b.stride(), b.storage_offset()
283
+ )
284
+ else:
285
+ reshaped_base_tensor = aliased_base_tensor
286
+ out = target_meta_tensor._view_func(reshaped_base_tensor)
287
+ # This shape mismatch can happen due to a bug in inplace/view handling in autograd.
288
+ # Try putting a breakpoint here and running
289
+ # `test/functorch/test_aotdispatch TestAOTAutograd.test_output_all_alias_types`
290
+ # Also, https://github.com/pytorch/pytorch/issues/49825
291
+ #
292
+ # As a stopgap, we'll fall back to as_strided.
293
+ if out is not None and out.shape == target_meta_tensor.shape:
294
+ return patch_requires_grad(out)
295
+
296
+ size = target_meta_tensor.size()
297
+ stride = target_meta_tensor.stride()
298
+ storage_offset = target_meta_tensor.storage_offset()
299
+ if aliased_base_tensor.is_complex() and not target_meta_tensor.is_complex():
300
+ aliased_out = torch.view_as_real(aliased_base_tensor).as_strided(
301
+ size, stride, storage_offset
302
+ )
303
+ elif not aliased_base_tensor.is_complex() and target_meta_tensor.is_complex():
304
+ aliased_out = torch.view_as_complex(aliased_base_tensor).as_strided(
305
+ size, stride, storage_offset
306
+ )
307
+ else:
308
+ aliased_out = aliased_base_tensor.as_strided(size, stride, storage_offset)
309
+ # For outputs aliasing inputs, we need to check if the requires-gradness has changed.
310
+ aliased_out = patch_requires_grad(aliased_out)
311
+ # For outputs aliasing inputs, we need to check if the dtype has changed.
312
+ # as_strided() is the "most generic" view, but it does not cover cross-dtype views
313
+ if aliased_out.dtype != target_meta_tensor.dtype:
314
+ aliased_out = aliased_out.view(target_meta_tensor.dtype)
315
+ return aliased_out
316
+
317
+
318
+ def has_same_metadata(t1, t2):
319
+ return (
320
+ definitely_true(sym_eq(t1.size(), t2.size()))
321
+ and definitely_true(t1.layout == t2.layout)
322
+ and (
323
+ is_sparse_any(t1)
324
+ or (
325
+ definitely_true(sym_eq(t1.stride(), t2.stride()))
326
+ and definitely_true(t1.storage_offset() == t2.storage_offset())
327
+ )
328
+ )
329
+ and t1.is_conj() == t2.is_conj()
330
+ and t1.is_neg() == t2.is_neg()
331
+ )
332
+
333
+
334
+ @dataclass(frozen=True)
335
+ class MetadataKey:
336
+ """
337
+ This should be equal whenever has_same_metadata would return True
338
+ """
339
+
340
+ size: Tuple[SymIntEqByExpr, ...]
341
+ layout: torch.layout
342
+ is_sparse: bool
343
+ # these are empty when is_sparse
344
+ stride: Optional[Tuple[SymIntEqByExpr, ...]]
345
+ storage_offset: Optional[SymIntEqByExpr]
346
+ is_conj: bool
347
+ is_neg: bool
348
+
349
+ @staticmethod
350
+ def make(t):
351
+ is_sparse = is_sparse_any(t)
352
+ return MetadataKey(
353
+ size=tuple(SymIntEqByExpr(s) for s in t.size()),
354
+ layout=t.layout,
355
+ is_sparse=is_sparse,
356
+ stride=None if is_sparse else tuple(SymIntEqByExpr(s) for s in t.stride()),
357
+ storage_offset=None if is_sparse else SymIntEqByExpr(t.storage_offset()),
358
+ is_conj=t.is_conj(),
359
+ is_neg=t.is_neg(),
360
+ )
361
+
362
+
363
+ # Wrapper around a FunctionalTensorWrapper for comparing only the resulting metadata
364
+ # after applying all the ViewMeta operations.
365
+ class FunctionalTensorMetadataEq:
366
+ def __init__(self, tensor: torch.Tensor) -> None:
367
+ assert torch._is_functional_tensor(tensor)
368
+ self.tensor = tensor
369
+
370
+ def __eq__(self, other: object) -> bool:
371
+ # If other is None, then it probably means that we weren't able to recreate
372
+ # the FunctionalTensorMetadataEq. One of this cases is when we update the
373
+ # view metadata by calling: create_synthetic_base_metadata.
374
+ if other is None:
375
+ return True
376
+
377
+ # Comparison agains any other type is not implemented.
378
+ if not isinstance(other, FunctionalTensorMetadataEq):
379
+ return NotImplemented
380
+
381
+ return has_same_metadata(self.tensor, other.tensor)
382
+
383
+
384
+ # new_arg and arg here are either:
385
+ # (1) both a FakeTensor
386
+ # (2) both a traceable tensor subclass that holds a FakeTensor
387
+ # Pre-condition: the two args are the "old" and "new" inputs from running functionalization.
388
+ # When we run functionalization and wrap our inputs into FunctionalTensors,
389
+ # we can detect whether or not an input was mutated by checking to see if the inner tensor has changed
390
+ #
391
+ # Normally it would be enough just to check if arg is new_arg, which is normally enough for functionalization
392
+ # to confirm that inputs were not mutated when running the user's model with functionalization on.
393
+ # But when we have subclass inputs, we can't rely on that:
394
+ # `from_fun(to_fun(x)) is x` will return False, because the call to `from_fun` constructs
395
+ # a brand new subclass instance: we are calling __tensor_unflatten__, and going
396
+ # from Subclass(FakeTensor) to Subclass(FunctionalTensor(FakeTensor))
397
+ def was_tensor_updated(arg, new_arg):
398
+ if is_traceable_wrapper_subclass(arg):
399
+ assert is_traceable_wrapper_subclass(new_arg)
400
+ attrs, _ = arg.__tensor_flatten__()
401
+ new_attrs, _ = new_arg.__tensor_flatten__()
402
+ assert attrs == new_attrs
403
+ # A tensor subclass was updated if any of its inner elements were updated
404
+ return any(
405
+ was_tensor_updated(getattr(arg, attr), getattr(new_arg, attr))
406
+ for attr in attrs
407
+ )
408
+ else:
409
+ return arg is not new_arg
410
+
411
+
412
+ # new_arg and arg here are either:
413
+ # (1) both a FakeTensor
414
+ # (2) both a traceable tensor subclass that holds a FakeTensor
415
+ # Pre-condition: the two args are the "old" and "new" inputs from running functionalization.
416
+ # When we run functionalization and wrap our inputs into FunctionalTensors,
417
+ # we can detect whether or not an input was mutated by checking to see if the inner tensor has changed,
418
+ # but shares storage with the old input
419
+ def was_tensor_metadata_updated(arg, new_arg):
420
+ if is_traceable_wrapper_subclass(arg):
421
+ assert is_traceable_wrapper_subclass(new_arg)
422
+ attrs, _ = arg.__tensor_flatten__()
423
+ new_attrs, _ = new_arg.__tensor_flatten__()
424
+ assert attrs == new_attrs
425
+ # A tensor subclass was updated if any of its inner elements were updated
426
+ return any(
427
+ was_tensor_metadata_updated(getattr(arg, attr), getattr(new_arg, attr))
428
+ for attr in attrs
429
+ )
430
+ else:
431
+ return arg is not new_arg and StorageWeakRef(
432
+ arg.untyped_storage()
433
+ ) == StorageWeakRef(new_arg.untyped_storage())
434
+
435
+
436
+ # Returns the number of detected copy_
437
+ def assert_functional_graph(fx_g: torch.fx.Graph) -> int:
438
+ allowed_mutation_ops = [
439
+ torch.ops.aten.copy_.default,
440
+ torch.ops.aten.set_.source_Tensor,
441
+ ]
442
+ if hasattr(torch.ops.fsdp, "copy_"):
443
+ allowed_mutation_ops.append(torch.ops.fsdp.copy_.default)
444
+
445
+ placeholders = set()
446
+ mutation_count = 0
447
+ # NB: It would also be nice to verify that the mutations all happen at the
448
+ # end, but we also do some administrative views after mutations so this
449
+ # isn't actually true. (TODO: Could this cause problems for Inductor?)
450
+ for n in fx_g.nodes:
451
+ if n.op == "placeholder":
452
+ placeholders.add(n)
453
+ if isinstance(n.target, torch._ops.OpOverload):
454
+ if n.target in allowed_mutation_ops:
455
+ suffix = True
456
+ # Can only copy_/set_ into an input
457
+ # this is mostly a hack to avoid failing XLA tests.
458
+ # See https://github.com/pytorch/pytorch/pull/122434#issuecomment-2101012113
459
+ if "set_buffer_donor_" not in str(n.args[0]):
460
+ assert (
461
+ n.args[0] in placeholders
462
+ ), f"n={str(n)}, n.args[0]={str(n.args[0])}, placeholders={str(placeholders)}, graph={str(fx_g)}"
463
+ mutation_count += 1
464
+ else:
465
+ assert (
466
+ not n.target._schema.is_mutable
467
+ ), f"aot_autograd expected to have an entirely functional graph, but found {n.format_node()}"
468
+ return mutation_count
469
+
470
+
471
+ def propagate_input_mutation_stacktraces(fx_g: torch.fx.Graph) -> None:
472
+ placeholders = set()
473
+ for n in fx_g.nodes:
474
+ if n.op == "placeholder":
475
+ placeholders.add(n)
476
+ if isinstance(n.target, torch._ops.OpOverload):
477
+ if n.target is torch.ops.aten.copy_.default:
478
+ # Can only copy_ into an input, and can only do so once
479
+ if "set_buffer_donor_" not in str(n.args[0]):
480
+ assert (
481
+ n.args[0] in placeholders
482
+ ), f"n={str(n)}, n.args[0]={str(n.args[0])}, placeholders={str(placeholders)}, graph={str(fx_g)}"
483
+ placeholders.remove(n.args[0])
484
+ copy_from_node = n.args[1]
485
+ # Pre-condition: every node has a "stack_trace" field in its meta,
486
+ # but copy_() nodes do not (since we manually added them during functionalization).
487
+ # Instead, we manually propagate here.
488
+ if "stack_trace" in copy_from_node.meta:
489
+ n.meta["stack_trace"] = copy_from_node.meta["stack_trace"]
490
+
491
+
492
+ def _check_if_mutation_can_be_in_graph(
493
+ keep_input_mutations: bool,
494
+ mutates_data,
495
+ mutates_metadata,
496
+ mutations_hidden_from_autograd,
497
+ mutations_under_no_grad_or_inference_mode,
498
+ mutates_storage_metadata,
499
+ mutation_inductor_storage_resize,
500
+ requires_grad,
501
+ ):
502
+ if keep_input_mutations:
503
+ in_graph = (
504
+ mutates_data or mutates_storage_metadata or mutation_inductor_storage_resize
505
+ ) and (
506
+ (not mutates_metadata and not requires_grad)
507
+ or mutations_hidden_from_autograd
508
+ or mutations_under_no_grad_or_inference_mode
509
+ )
510
+ else:
511
+ in_graph = False
512
+ # See Note [set_() Input Mutations in AOTAutograd]
513
+ # If there was a `set_()`, we require that all mutations were under no_grad,
514
+ # so we can (safely) emit the set_() in the graph at runtime
515
+ # resize_() gets the same treatment
516
+ if mutation_inductor_storage_resize or mutates_storage_metadata:
517
+ op_name = "resize_" if mutation_inductor_storage_resize else "set_"
518
+ assert in_graph, f"""\
519
+ Encountered a {op_name} on a graph input, but the input has other mutations that we cannot
520
+ keep in the graph. This is not supported today. Current state:
521
+ keep_input_mutations={keep_input_mutations}
522
+ mutates_data={mutates_data}
523
+ mutates_metadata={mutates_metadata}
524
+ mutations_hidden_from_autograd={mutations_hidden_from_autograd}
525
+ mutations_under_no_grad_or_inference_mode={mutations_under_no_grad_or_inference_mode}
526
+ mutation_inductor_storage_resize={mutation_inductor_storage_resize}
527
+ requires_grad={requires_grad}"""
528
+ return in_graph
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/input_output_analysis.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ This module is one of the analysis modules - it takes as input a function or graph
4
+ and some preexisting properties, and returns some data that is useful for deciding
5
+ how to further proceed with compilation or construct runtime wrappers.
6
+
7
+ In particular, the following analyses are provided:
8
+ 1. Refine the view and mutation metadata collected previously - removing duplicate
9
+ inputs or mapping views to their bases.
10
+ 2. We also analyze the function signature for export graphs.
11
+ """
12
+
13
+ import contextlib
14
+ import itertools
15
+ from typing import Any, Dict, List, Optional, Tuple, Union
16
+
17
+ import torch
18
+ import torch.utils._pytree as pytree
19
+ from torch import Tensor
20
+ from torch._C._dynamo.guards import compute_overlapping_tensors
21
+ from torch._functorch._aot_autograd.schemas import PlainTensorMeta
22
+ from torch._guards import StorageOverlap
23
+ from torch._subclasses.functional_tensor import FunctionalTensor
24
+ from torch.fx.experimental.symbolic_shapes import is_concrete_int
25
+
26
+ from .collect_metadata_analysis import coerce_tangent_and_suggest_memory_format
27
+ from .schemas import (
28
+ BackwardSignature,
29
+ GraphSignature,
30
+ InputAliasInfo,
31
+ OutputAliasInfo,
32
+ OutputType,
33
+ ViewAndMutationMeta,
34
+ )
35
+ from .utils import strict_zip
36
+
37
+
38
+ zip = strict_zip
39
+
40
+
41
+ def remove_dupe_metadata(
42
+ m: ViewAndMutationMeta,
43
+ keep_arg_mask: List[bool],
44
+ add_dupe_map: List[int],
45
+ ) -> ViewAndMutationMeta:
46
+ assert len(m.input_info) == len(keep_arg_mask)
47
+ # Easy invariant: the first argument should never be a dupe (it will be kept)
48
+ assert len(keep_arg_mask) > 0 and keep_arg_mask[0]
49
+
50
+ # Filter dupe'd mutated inputs out of traced_tangents
51
+ num_data_mutations = len([x for x in m.input_info if x.mutates_data])
52
+ other_traced_tangents = m.traced_tangents[num_data_mutations:]
53
+ inp_traced_tangents = m.traced_tangents[:num_data_mutations]
54
+ filtered_inp_traced_tangents = [
55
+ # See Note [Tangents memory format]
56
+ x
57
+ for i, x in enumerate(inp_traced_tangents)
58
+ if keep_arg_mask[m.mutated_inp_runtime_indices[i]]
59
+ ]
60
+ traced_tangents = filtered_inp_traced_tangents + other_traced_tangents
61
+
62
+ assert m.subclass_tangent_meta is not None
63
+ subclass_tangent_meta = [
64
+ PlainTensorMeta(0, memory_format=torch.contiguous_format)
65
+ ] * len(filtered_inp_traced_tangents) + m.subclass_tangent_meta[num_data_mutations:]
66
+
67
+ return ViewAndMutationMeta(
68
+ input_info=[x for i, x in enumerate(m.input_info) if keep_arg_mask[i]],
69
+ # For outputs that are views of inputs, we store the index of the input that the output
70
+ # was generated from. Need to update that index to account for removed dupes.
71
+ output_info=[
72
+ OutputAliasInfo(
73
+ output_type=o.output_type,
74
+ raw_type=o.raw_type,
75
+ dynamic_dims=o.dynamic_dims,
76
+ base_idx=None if o.base_idx is None else add_dupe_map[o.base_idx],
77
+ requires_grad=o.requires_grad,
78
+ functional_tensor=o.functional_tensor,
79
+ )
80
+ for o in m.output_info
81
+ ],
82
+ num_intermediate_bases=m.num_intermediate_bases,
83
+ keep_input_mutations=m.keep_input_mutations,
84
+ traced_tangents=traced_tangents,
85
+ # We are guaranteed not to get here, since dupes are not supported today with subclass inputs.
86
+ subclass_inp_meta=[],
87
+ subclass_fw_graph_out_meta=[],
88
+ subclass_tangent_meta=subclass_tangent_meta,
89
+ is_train=m.is_train,
90
+ )
91
+
92
+
93
+ # Given our ViewAndMutation metadata, this fn constructs a new set of metadata,
94
+ # after adding synthetic base arguments to the function.
95
+ # Most of the work in this fn is slogging through all of the metadata corresponding to inputs,
96
+ # and updating it with our synthetic base calling convention.
97
+ #
98
+ # When config.debug_assert is set, we automatically regenerate the metadata
99
+ # and compare it to this output for sanity.
100
+ #
101
+ # In addition to the updated metadata, also return the list of input indices
102
+ # that will need to be updated in the synthetic base epilogue
103
+ def create_synthetic_base_metadata(
104
+ m: ViewAndMutationMeta,
105
+ # Maps each outer argument idx to its inner idx (or, if this outer arg is generated from a
106
+ # synthetic base, you get a tuple of (i, TensorMeta), telling you the base tensor idx, and view metadata)
107
+ synthetic_base_info: List[Union[int, Tuple[int, torch.Tensor]]],
108
+ outer_args: List[Any],
109
+ inner_args: List[Any],
110
+ ) -> Tuple[ViewAndMutationMeta, List[int]]:
111
+ # maps inner arg indices to outer arg indices
112
+ synthetic_base_to_indices: Dict[int, List[int]] = {}
113
+ for inner_idx in range(len(inner_args)):
114
+ outer_aliased_indices_of_current_base_arg = [
115
+ outer_idx
116
+ for outer_idx, inner_idx_or_tuple in enumerate(synthetic_base_info)
117
+ if (isinstance(inner_idx_or_tuple, int) and inner_idx_or_tuple == inner_idx)
118
+ or (
119
+ isinstance(inner_idx_or_tuple, tuple)
120
+ and inner_idx_or_tuple[0] == inner_idx
121
+ )
122
+ ]
123
+ synthetic_base_to_indices[inner_idx] = outer_aliased_indices_of_current_base_arg
124
+
125
+ # given the requires_grad info on mutated inputs,
126
+ # generate the requires_grad info on those same mutated inputs, but after constructing synthetic bases.
127
+ input_infos = []
128
+ for outer_indices in synthetic_base_to_indices.values():
129
+ # leaf-ness should be all-or-nothing for aliased tensor.
130
+ # (aka if "a" and "b" are views, then a.is_leaf == b.is_leaf)
131
+ any_leaf = any(m.input_info[x].is_leaf for x in outer_indices)
132
+ all_leaf = all(m.input_info[x].is_leaf for x in outer_indices)
133
+ assert any_leaf == all_leaf
134
+
135
+ mutates_data = (
136
+ True
137
+ if len(outer_indices) > 1
138
+ else m.input_info[outer_indices[0]].mutates_data
139
+ )
140
+ mutates_metadata = (
141
+ False
142
+ if len(outer_indices) > 1
143
+ else m.input_info[outer_indices[0]].mutates_metadata
144
+ )
145
+ requires_grad = any(m.input_info[x].requires_grad for x in outer_indices)
146
+ mutations_hidden_from_autograd = all(
147
+ m.input_info[x].mutations_hidden_from_autograd for x in outer_indices
148
+ )
149
+ mutations_under_no_grad_or_inference_mode = all(
150
+ m.input_info[x].mutations_under_no_grad_or_inference_mode
151
+ for x in outer_indices
152
+ )
153
+
154
+ mutation_inductor_storage_resize = all(
155
+ m.input_info[x].mutation_inductor_storage_resize for x in outer_indices
156
+ )
157
+
158
+ inpt_info = InputAliasInfo(
159
+ # If len(outer_indices) > 1, then this input is a synthetic base.
160
+ # The invariant is that to the rest of aot autograd, synthetic bases only show up if
161
+ # one of their aliases gets a data mutation. And if any of their aliases get metadata
162
+ # mutations, they will be hidden from the rest of aot autograd.
163
+ mutates_data=mutates_data,
164
+ mutates_metadata=mutates_metadata,
165
+ mutations_hidden_from_autograd=all(
166
+ m.input_info[x].mutations_hidden_from_autograd for x in outer_indices
167
+ ),
168
+ mutates_storage_metadata=(
169
+ False
170
+ if len(outer_indices) > 1
171
+ else m.input_info[outer_indices[0]].mutates_storage_metadata
172
+ ),
173
+ mutations_under_no_grad_or_inference_mode=mutations_under_no_grad_or_inference_mode,
174
+ mutation_inductor_storage_resize=mutation_inductor_storage_resize,
175
+ is_leaf=any_leaf,
176
+ requires_grad=requires_grad,
177
+ keep_input_mutations=m.keep_input_mutations,
178
+ )
179
+ input_infos.append(inpt_info)
180
+
181
+ # Find any inputs that fulfill the following criteria:
182
+ # (1) They are part of a synthetic base (because they alias another input,
183
+ # and at least one input experiences a data mutation)
184
+ # (2) They experience a metadata mutation
185
+ outer_aliased_arg_idx_with_metadata_mutations = [
186
+ outer_idx
187
+ for outer_idx, inpt_info in enumerate(m.input_info)
188
+ if inpt_info.mutates_metadata
189
+ and not isinstance(synthetic_base_info[outer_idx], int)
190
+ ]
191
+
192
+ # grab the original requires grad info on the outputs, except the ones from the mutated inputs
193
+ input_metadata_output_info = [
194
+ OutputAliasInfo(
195
+ output_type=OutputType.alias_of_input,
196
+ raw_type=FunctionalTensor,
197
+ dynamic_dims={
198
+ i
199
+ for i, s in enumerate(outer_args[outer_idx].shape)
200
+ if not is_concrete_int(s)
201
+ },
202
+ base_idx=synthetic_base_info[outer_idx][0], # type: ignore[index]
203
+ requires_grad=outer_args[outer_idx].requires_grad,
204
+ )
205
+ for outer_idx in outer_aliased_arg_idx_with_metadata_mutations
206
+ ]
207
+ existing_output_infos = []
208
+ for o in m.output_info:
209
+ new_base_idx = (
210
+ None
211
+ if o.base_idx is None
212
+ else (
213
+ synthetic_base_info[o.base_idx]
214
+ if isinstance(synthetic_base_info[o.base_idx], int)
215
+ else synthetic_base_info[o.base_idx][0] # type: ignore[index]
216
+ )
217
+ )
218
+ # If base_idx is changed for OutputType.is_input, we need to update the output type to reflect the change
219
+ new_output_type = (
220
+ OutputType.alias_of_input
221
+ if o.output_type == OutputType.is_input and o.base_idx != new_base_idx
222
+ else o.output_type
223
+ )
224
+ existing_output_infos.append(
225
+ OutputAliasInfo(
226
+ output_type=new_output_type,
227
+ raw_type=o.raw_type,
228
+ dynamic_dims=o.dynamic_dims,
229
+ # Map the input idx pre-synthetic-bases to the new idx post-synthetic-bases
230
+ base_idx=new_base_idx, # type: ignore[arg-type]
231
+ requires_grad=o.requires_grad,
232
+ functional_tensor=o.functional_tensor,
233
+ )
234
+ )
235
+
236
+ inner_mutated_tangents_and_memory_formats = [
237
+ # See Note [Tangents memory format]
238
+ coerce_tangent_and_suggest_memory_format(x)
239
+ for inner_idx, x in enumerate(inner_args)
240
+ if input_infos[inner_idx].mutates_data and input_infos[inner_idx].requires_grad
241
+ ]
242
+ inner_mutated_tangents = [x[0] for x in inner_mutated_tangents_and_memory_formats]
243
+ inner_mutated_tangents_memory_formats = [
244
+ x[1] for x in inner_mutated_tangents_and_memory_formats
245
+ ]
246
+
247
+ output_info = existing_output_infos + input_metadata_output_info
248
+ # Regenerate traced tangents to include mutated inputs including synthetic bases
249
+ traced_tangents = (
250
+ inner_mutated_tangents + m.traced_tangents[len(inner_mutated_tangents) :]
251
+ )
252
+ assert m.subclass_tangent_meta is not None
253
+ subclass_tangent_meta = [
254
+ PlainTensorMeta(0, memory_format=x)
255
+ for x in inner_mutated_tangents_memory_formats
256
+ ] + m.subclass_tangent_meta[len(inner_mutated_tangents) :]
257
+
258
+ return (
259
+ ViewAndMutationMeta(
260
+ input_info=input_infos,
261
+ output_info=output_info,
262
+ num_intermediate_bases=m.num_intermediate_bases,
263
+ keep_input_mutations=m.keep_input_mutations,
264
+ traced_tangents=traced_tangents,
265
+ # We are guaranteed not to get here, since synthetic_base codepaths are not supported today with subclass inputs.
266
+ subclass_inp_meta=[],
267
+ subclass_fw_graph_out_meta=[],
268
+ subclass_tangent_meta=subclass_tangent_meta,
269
+ is_train=m.is_train,
270
+ ),
271
+ outer_aliased_arg_idx_with_metadata_mutations,
272
+ )
273
+
274
+
275
+ def compute_overlapping_inputs(aot_config, fwd_inputs, aliased_input_indices):
276
+ num_aliases = len(aliased_input_indices)
277
+
278
+ shape_env = None
279
+ maybe_suppress_guards = contextlib.nullcontext
280
+ tracing_context = torch._guards.TracingContext.try_get()
281
+
282
+ if tracing_context is not None:
283
+ shape_env = tracing_context.fake_mode.shape_env
284
+
285
+ # Check whether we can actually get the dynamo sources from within AOTAutograd.
286
+ if aot_config.aot_autograd_arg_pos_to_source and shape_env is not None:
287
+ maybe_suppress_guards = shape_env.suppress_guards
288
+
289
+ # Check whether there are any symbolic values being used.
290
+ # We do this for 2 reasons:
291
+ # 1. StorageOverlap guard is only issued whenever dynamic shapes is turned on
292
+ # 2. Triggers the fast-path for computing storage overlapping
293
+ symbolic = any(
294
+ isinstance(x, torch.SymInt)
295
+ for i in aliased_input_indices
296
+ for x in [
297
+ *fwd_inputs[i].shape,
298
+ *fwd_inputs[i].stride(),
299
+ fwd_inputs[i].storage_offset(),
300
+ ]
301
+ )
302
+
303
+ with maybe_suppress_guards():
304
+ aliased_fwd_inputs = [fwd_inputs[i] for i in aliased_input_indices]
305
+ actual_aliased_indices = {
306
+ aliased_input_indices[i]
307
+ for i in compute_overlapping_tensors(aliased_fwd_inputs, symbolic=symbolic)
308
+ }
309
+
310
+ # Add the StorageOverlap AOTAutograd guard only if we are actually keeping track of
311
+ # dynamo sources inside AOTAutograd.
312
+ if (
313
+ tracing_context is not None
314
+ # Make sure dynamic shapes is currently being used.
315
+ and symbolic
316
+ # We check that we have more than 1 aliased tensor, which should be true at
317
+ # this point, anyway.
318
+ and num_aliases > 1
319
+ and aot_config.aot_autograd_arg_pos_to_source
320
+ ):
321
+ no_overlap_indices = list(set(aliased_input_indices) - actual_aliased_indices)
322
+
323
+ overlapping_sources = [
324
+ aot_config.aot_autograd_arg_pos_to_source[i] for i in actual_aliased_indices
325
+ ]
326
+ non_overlapping_sources = [
327
+ aot_config.aot_autograd_arg_pos_to_source[i] for i in no_overlap_indices
328
+ ]
329
+
330
+ tracing_context.guards_context.aotautograd_guards.append(
331
+ StorageOverlap(overlapping_sources, non_overlapping_sources)
332
+ )
333
+
334
+ return actual_aliased_indices
335
+
336
+
337
+ def _graph_input_names(gm):
338
+ return [node.name for node in gm.graph.find_nodes(op="placeholder")]
339
+
340
+
341
+ def _graph_output_names(gm):
342
+ output_node = next(iter(reversed(gm.graph.nodes)))
343
+ assert output_node.op == "output" and len(output_node.args) == 1
344
+ return_args = output_node.args[0]
345
+ return [getattr(return_arg, "name", None) for return_arg in return_args]
346
+
347
+
348
+ def create_graph_signature(
349
+ fx_g: torch.fx.GraphModule,
350
+ fw_metadata: ViewAndMutationMeta,
351
+ in_spec: pytree.TreeSpec,
352
+ out_spec: pytree.TreeSpec,
353
+ *,
354
+ user_args_flat: List[Tensor],
355
+ params_and_buffers_flat: List[Tensor],
356
+ param_names: List[str],
357
+ buffer_names: List[str],
358
+ trace_joint: bool,
359
+ num_user_fw_outs: Optional[int],
360
+ loss_index: Optional[int],
361
+ ) -> GraphSignature:
362
+ # Retrieve graph input names
363
+ graph_input_names = _graph_input_names(fx_g)
364
+ # Retrieve graph output names
365
+ graph_output_names = _graph_output_names(fx_g)
366
+
367
+ num_params_buffers = len(param_names) + len(buffer_names)
368
+ num_tokens = len(fw_metadata.tokens)
369
+ # We have enough restrictions on the graph (no de-duping, synthetic bases, etc),
370
+ # Such that # graph inps = # user inps + # params + # buffers
371
+ num_user_args = len(graph_input_names) - num_params_buffers - num_tokens
372
+
373
+ if trace_joint:
374
+ assert num_user_fw_outs is not None
375
+ num_fw_outs = num_user_fw_outs + fw_metadata.num_mutated_inp_runtime_indices
376
+ backward_output_names = graph_output_names[num_fw_outs:]
377
+
378
+ grad_index = itertools.count(0)
379
+ gradients_to_parameters = {
380
+ backward_output_names[next(grad_index)]: param_names[i]
381
+ for i, param in enumerate(params_and_buffers_flat)
382
+ if param.requires_grad
383
+ }
384
+
385
+ gradients_to_user_inputs = {
386
+ backward_output_names[next(grad_index)]: graph_input_names[
387
+ i + len(params_and_buffers_flat)
388
+ ]
389
+ for i, user_input in enumerate(user_args_flat)
390
+ if user_input.requires_grad
391
+ }
392
+
393
+ assert len(gradients_to_parameters) + len(gradients_to_user_inputs) == len(
394
+ backward_output_names
395
+ )
396
+
397
+ # Check that we have fully accounted for all graph outputs
398
+ backward_signature = BackwardSignature(
399
+ gradients_to_parameters,
400
+ gradients_to_user_inputs,
401
+ graph_output_names[loss_index],
402
+ )
403
+ else:
404
+ backward_signature = None
405
+ num_user_fw_outs = (
406
+ len(graph_output_names)
407
+ - fw_metadata.num_mutated_inp_runtime_indices
408
+ - num_tokens
409
+ )
410
+
411
+ return GraphSignature.from_tracing_metadata(
412
+ in_spec=in_spec,
413
+ out_spec=out_spec,
414
+ graph_input_names=graph_input_names,
415
+ graph_output_names=graph_output_names,
416
+ view_mutation_metadata=fw_metadata,
417
+ named_parameters=param_names,
418
+ named_buffers=buffer_names,
419
+ num_user_inputs=num_user_args,
420
+ num_user_outputs=num_user_fw_outs,
421
+ loss_index=loss_index,
422
+ backward_signature=backward_signature,
423
+ )
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/jit_compile_runtime_wrappers.py ADDED
@@ -0,0 +1,907 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ Functions in this module do most of the "work" of AOTAutograd.
4
+ An aot_dispatch_* function:
5
+ - Takes in the input flat_fn, flat_args, and some metadata
6
+ - Runs a set of pre compile wrappers (e.g. argument deduping)
7
+ - Runs the actual compiler
8
+ - Wraps the returned callable in a set of post compile wrappers
9
+ - Returns the wrapped callable and metadata.
10
+ """
11
+
12
+ import itertools
13
+ import logging
14
+ import time
15
+ import traceback
16
+ from contextlib import nullcontext
17
+ from typing import Any, Callable, List, Optional, Sequence, Tuple
18
+
19
+ import torch
20
+ import torch.utils.dlpack
21
+ from torch import Tensor
22
+ from torch._dynamo.utils import detect_fake_mode, lazy_format_graph_code
23
+ from torch._guards import CompileContext, TracingContext
24
+ from torch._logging import getArtifactLogger, trace_structured
25
+ from torch._subclasses import FakeTensor
26
+ from torch._subclasses.meta_utils import is_sparse_any
27
+ from torch.fx.experimental._backward_state import BackwardState
28
+ from torch.fx.experimental.proxy_tensor import is_sym_node
29
+ from torch.fx.experimental.symbolic_shapes import fx_placeholder_vals
30
+ from torch.fx.graph_module import GraphModule
31
+ from torch.fx.passes._tensorify_python_scalars import tensorify_python_scalars
32
+ from torch.multiprocessing.reductions import StorageWeakRef
33
+ from torchgen.utils import dataclass_repr
34
+
35
+ from .. import config
36
+ from .autograd_cache import (
37
+ AOTAutogradCache,
38
+ AOTAutogradCacheEntry,
39
+ autograd_cache_enabled,
40
+ CompiledBackward,
41
+ CompiledForward,
42
+ should_use_remote_autograd_cache,
43
+ )
44
+ from .dispatch_and_compile_graph import (
45
+ aot_dispatch_autograd_graph,
46
+ aot_dispatch_base_graph,
47
+ )
48
+ from .logging_utils import track_graph_compiling
49
+ from .runtime_wrappers import (
50
+ AOTDedupeWrapper,
51
+ AOTDispatchAutograd,
52
+ AOTDispatchSubclassWrapper,
53
+ AOTSyntheticBaseWrapper,
54
+ AutogradLazyBackwardCompileInfo,
55
+ CompilerWrapper,
56
+ DebugAssertWrapper,
57
+ EffectTokensWrapper,
58
+ FakifiedOutWrapper,
59
+ FunctionalizedRngRuntimeWrapper,
60
+ make_runtime_safe,
61
+ post_compile,
62
+ pre_compile,
63
+ RuntimeWrapper,
64
+ )
65
+ from .schemas import AOTConfig, MutationType, ViewAndMutationMeta
66
+ from .subclass_utils import compute_inner_mutated_inp_indices_from_subclass_meta
67
+ from .utils import (
68
+ _get_symint_hints,
69
+ contain_metadata_mutation_ops,
70
+ make_boxed_func,
71
+ strict_zip,
72
+ unlift_tokens,
73
+ )
74
+
75
+
76
+ zip = strict_zip
77
+
78
+ log = logging.getLogger(__name__)
79
+ aot_joint_log = getArtifactLogger(__name__, "aot_joint_graph")
80
+ aot_graphs_log = getArtifactLogger(__name__, "aot_graphs")
81
+
82
+ aten = torch.ops.aten
83
+
84
+ # Returns a Callable and a ViewAndMutationMeta.
85
+ # Currently, only export needs the ViewAndMutationMeta after this function.
86
+ DispatchReturn = Tuple[Callable, ViewAndMutationMeta]
87
+
88
+
89
+ def _create_wrappers_for_dispatch(needs_autograd: bool) -> List[CompilerWrapper]:
90
+ """
91
+ Wrappers that run on every dispatch function
92
+ """
93
+ return [AOTDedupeWrapper(), AOTSyntheticBaseWrapper(trace_joint=needs_autograd)]
94
+
95
+
96
+ # Export's dispatching logic is unique in a few ways: it only needs the "graph"
97
+ # bits of aot_autograd, and doesn't need to do any specific wrapping.
98
+ def aot_dispatch_export(
99
+ flat_fn: Callable,
100
+ flat_args: List[Any],
101
+ aot_config: AOTConfig,
102
+ *,
103
+ fw_metadata: ViewAndMutationMeta,
104
+ needs_autograd: bool,
105
+ ) -> DispatchReturn:
106
+ wrappers = _create_wrappers_for_dispatch(needs_autograd)
107
+ flat_fn, flat_args, fw_metadata = pre_compile(
108
+ wrappers,
109
+ flat_fn,
110
+ flat_args,
111
+ aot_config,
112
+ fw_metadata=fw_metadata,
113
+ )
114
+ if needs_autograd and not aot_config.pre_dispatch:
115
+ graph, _, _ = aot_dispatch_autograd_graph(
116
+ flat_fn, flat_args, aot_config, fw_metadata=fw_metadata
117
+ )
118
+ else:
119
+ graph, _, _ = aot_dispatch_base_graph(
120
+ flat_fn, flat_args, aot_config, fw_metadata=fw_metadata
121
+ )
122
+
123
+ # NB: the wrappers that run in pre_compile for export are
124
+ # either a no-op, because they're not needed, or will raise a runtime error,
125
+ # since they don't support export.
126
+ # We still run these wrappers to make sure that they're not needed pre compile,
127
+ # but we technically don't need to run them post compile at all here.
128
+ compiled_fn, fw_metadata = post_compile(
129
+ wrappers, graph, aot_config, runtime_metadata=fw_metadata
130
+ )
131
+
132
+ # Therefore, since no wrapperes run, we don't get back a callable - we get back the raw fx graph
133
+ # (either a joint or an inference-only graph)
134
+ assert isinstance(compiled_fn, torch.fx.GraphModule)
135
+ return compiled_fn, fw_metadata
136
+
137
+
138
+ def aot_dispatch_base(
139
+ flat_fn,
140
+ flat_args: List[Any],
141
+ aot_config: AOTConfig,
142
+ *,
143
+ fw_metadata: ViewAndMutationMeta,
144
+ ) -> DispatchReturn:
145
+ """
146
+ Handles functions that don't need autograd. Runs wrappers and compiles with fw_compiler.
147
+ """
148
+ wrappers = _create_wrappers_for_dispatch(needs_autograd=False)
149
+ flat_fn, flat_args, fw_metadata = pre_compile(
150
+ wrappers, flat_fn, flat_args, aot_config, fw_metadata=fw_metadata
151
+ )
152
+
153
+ fw_module, updated_flat_args, maybe_subclass_meta = aot_dispatch_base_graph( # type: ignore[misc]
154
+ flat_fn, flat_args, aot_config, fw_metadata=fw_metadata
155
+ )
156
+ # Save the forward_graph_str right after aot_dispatch_base_graph,
157
+ # to save in the cache
158
+ aot_forward_graph_str = None
159
+ if autograd_cache_enabled():
160
+ aot_forward_graph_str = fw_module.print_readable(
161
+ print_output=False, include_stride=True, include_device=True
162
+ )
163
+
164
+ fakified_out_wrapper = FakifiedOutWrapper()
165
+ (
166
+ fw_module,
167
+ updated_flat_args,
168
+ fw_metadata,
169
+ ) = fakified_out_wrapper.pre_compile(
170
+ fw_module, updated_flat_args, aot_config, fw_metadata=fw_metadata
171
+ )
172
+ functionalized_rng_wrapper = FunctionalizedRngRuntimeWrapper()
173
+ (
174
+ fw_module,
175
+ updated_flat_args,
176
+ fw_metadata,
177
+ ) = functionalized_rng_wrapper.pre_compile(
178
+ fw_module, updated_flat_args, aot_config, fw_metadata=fw_metadata
179
+ )
180
+
181
+ disable_amp = torch._C._is_any_autocast_enabled()
182
+ context = torch._C._DisableAutocast if disable_amp else nullcontext
183
+
184
+ with context(), track_graph_compiling(aot_config, "inference"):
185
+ compiler = (
186
+ aot_config.inference_compiler
187
+ if aot_config.inference_compiler is not None
188
+ else aot_config.fw_compiler
189
+ )
190
+
191
+ if tracing_context := torch._guards.TracingContext.try_get():
192
+ tracing_context.fw_metadata = (
193
+ fw_metadata
194
+ if maybe_subclass_meta is None
195
+ else maybe_subclass_meta.fw_metadata
196
+ )
197
+
198
+ with TracingContext.report_output_strides() as fwd_output_strides:
199
+ fake_mode = detect_fake_mode()
200
+ if fake_mode is not None and fake_mode.shape_env is not None:
201
+ assert isinstance(fw_module, GraphModule)
202
+ tensorify_python_scalars(fw_module, fake_mode.shape_env, fake_mode)
203
+ compiled_fw = compiler(fw_module, updated_flat_args)
204
+
205
+ if fakified_out_wrapper.needs_post_compile:
206
+ fakified_out_wrapper.set_fwd_output_strides(fwd_output_strides)
207
+
208
+ make_runtime_safe(fw_metadata, maybe_subclass_meta)
209
+
210
+ # However, RuntimeWrapper does not expect the rng offsets in the
211
+ # output. So, we have to create another wrapper and take out the offset. As
212
+ # a result, we have to account for not boxed_call compilers as well.
213
+ if not hasattr(compiled_fw, "_boxed_call"):
214
+ compiled_fw = make_boxed_func(compiled_fw)
215
+
216
+ # Create a wrapper to set up the rng functionalize and fakified out bits
217
+ compiled_fw = functionalized_rng_wrapper.post_compile(
218
+ compiled_fw, aot_config, runtime_metadata=fw_metadata
219
+ )
220
+ cache_info = aot_config.cache_info
221
+ if autograd_cache_enabled() and cache_info:
222
+ if fw_key := getattr(compiled_fw, "_fx_graph_cache_key", None):
223
+ time_taken_ns = time.time_ns() - cache_info.start_time_ns
224
+ entry = AOTAutogradCacheEntry(
225
+ compiled_fw=CompiledForward(fw_key),
226
+ compiled_bw=None,
227
+ aot_joint_graph_str=None,
228
+ aot_forward_graph_str=aot_forward_graph_str,
229
+ aot_backward_graph_str=None,
230
+ runtime_metadata=fw_metadata,
231
+ dispatch_wrappers=wrappers,
232
+ maybe_subclass_meta=maybe_subclass_meta,
233
+ num_fw_outs_saved_for_bw=None,
234
+ indices_of_inps_to_detach=[],
235
+ forward_time_taken_ns=time_taken_ns,
236
+ backward_time_taken_ns=0,
237
+ )
238
+ AOTAutogradCache.save(
239
+ cache_info.cache_key, entry, remote=should_use_remote_autograd_cache()
240
+ )
241
+
242
+ compiled_fw = fakified_out_wrapper.post_compile(
243
+ compiled_fw,
244
+ aot_config,
245
+ runtime_metadata=fw_metadata,
246
+ )
247
+
248
+ compiled_fw = EffectTokensWrapper().post_compile(
249
+ compiled_fw,
250
+ aot_config,
251
+ runtime_metadata=fw_metadata,
252
+ )
253
+
254
+ # Why do we need to pass in num_fw_outs_saved_for_bw?
255
+ # See Note: [Partitioner handling for Subclasses, Part 2]
256
+ compiled_fw = AOTDispatchSubclassWrapper(
257
+ trace_joint=False,
258
+ # TODO: once we use pre_compile this will be flat_fn at the top of this function
259
+ fw_only=None,
260
+ maybe_subclass_meta=maybe_subclass_meta,
261
+ num_fw_outs_saved_for_bw=None,
262
+ ).post_compile(
263
+ compiled_fw,
264
+ aot_config, # not used
265
+ runtime_metadata=fw_metadata,
266
+ )
267
+
268
+ if not hasattr(compiled_fw, "_boxed_call"):
269
+ compiled_fw = make_boxed_func(compiled_fw)
270
+
271
+ compiled_fn = RuntimeWrapper(
272
+ indices_of_inps_to_detach=[],
273
+ trace_joint=False,
274
+ disable_amp=disable_amp,
275
+ ).post_compile(
276
+ compiled_fw,
277
+ aot_config,
278
+ runtime_metadata=fw_metadata,
279
+ )
280
+
281
+ compiled_fn = post_compile(
282
+ wrappers, compiled_fn, aot_config, runtime_metadata=fw_metadata
283
+ )
284
+ return compiled_fn
285
+
286
+
287
+ def collect_fw_donated_buffer_idxs(
288
+ fw_ins: List[Optional[FakeTensor]],
289
+ user_fw_outs: List[Optional[FakeTensor]],
290
+ bw_outs: List[Optional[FakeTensor]],
291
+ saved_tensors: List[FakeTensor],
292
+ ) -> List[int]:
293
+ """
294
+ Checks if the saved tensors are donated buffers, which means a saved tensor is not
295
+ an alias of any tensors in fw_ins, user_fw_outs, and bw_outs.
296
+ """
297
+
298
+ storage_refs = set()
299
+ for t in itertools.chain(fw_ins, user_fw_outs, bw_outs):
300
+ # Only access storage if a tensor has storage (not sparse)
301
+ if t is not None and isinstance(t, FakeTensor) and not is_sparse_any(t):
302
+ storage_refs.add(StorageWeakRef(t.untyped_storage()))
303
+
304
+ num_saved_tensor = len(saved_tensors)
305
+ donated_buffer_idxs = []
306
+ for i in range(num_saved_tensor):
307
+ t = saved_tensors[i]
308
+ if (
309
+ t is not None
310
+ and not is_sparse_any(t)
311
+ and StorageWeakRef(t.untyped_storage()) not in storage_refs
312
+ ):
313
+ donated_buffer_idxs.append(i)
314
+
315
+ return donated_buffer_idxs
316
+
317
+
318
+ def collect_bw_donated_buffer_idxs(
319
+ fw_module: torch.fx.GraphModule,
320
+ bw_module: torch.fx.GraphModule,
321
+ fw_metadata: ViewAndMutationMeta,
322
+ ) -> List[int]:
323
+ """
324
+ Collects backward donated buffer indexes from fw_module and bw_module.
325
+ """
326
+
327
+ # [Note: Metadata mutation in proxy tracing]
328
+ # node.meta["val"] is a snapshot of the tensor value when tracing a graph,
329
+ # instead of the final state after the graph has run. node.meta["val"] is
330
+ # not updated even if later there is a metadata mutation op.
331
+ # See: https://github.com/pytorch/pytorch/pull/141308#issuecomment-2495798947
332
+ #
333
+ # Currently, metadata mutation op happens only for sacrificial parameter
334
+ # specifically the `set_` op. This motivates banning metadata mutation from
335
+ # proxy tracing.
336
+ #
337
+ # Since node.meta["val"] is used to detect donated buffer, we return an empty
338
+ # list if there exists metadata mutation op.
339
+ if contain_metadata_mutation_ops(fw_module) or contain_metadata_mutation_ops(
340
+ bw_module
341
+ ):
342
+ return []
343
+
344
+ fw_ins = fw_module.graph.find_nodes(op="placeholder")
345
+ bw_outs = next(reversed(bw_module.graph.find_nodes(op="output"))).args[0]
346
+ fw_outs = next(reversed(fw_module.graph.find_nodes(op="output"))).args[0]
347
+
348
+ fw_ins = [
349
+ n.meta["val"] if (hasattr(n, "meta") and "val" in n.meta) else None
350
+ for n in fw_ins
351
+ ]
352
+ fw_outs = [
353
+ n.meta["val"] if (hasattr(n, "meta") and "val" in n.meta) else None
354
+ for n in fw_outs
355
+ ]
356
+ bw_outs = [
357
+ n.meta["val"] if (hasattr(n, "meta") and "val" in n.meta) else None
358
+ for n in bw_outs
359
+ ]
360
+
361
+ user_fw_outs = fw_outs[: fw_metadata.num_forward]
362
+ saved_tensors = fw_outs[fw_metadata.tensors_saved_for_backwards_slice]
363
+
364
+ fw_donated_buffer = collect_fw_donated_buffer_idxs(
365
+ fw_ins,
366
+ user_fw_outs,
367
+ bw_outs,
368
+ saved_tensors,
369
+ )
370
+
371
+ assert fw_metadata.num_symints_saved_for_bw is not None
372
+ return [fw_metadata.num_symints_saved_for_bw + i for i in fw_donated_buffer]
373
+
374
+
375
+ def aot_dispatch_autograd(
376
+ flat_fn,
377
+ flat_args: List[Any],
378
+ aot_config: AOTConfig,
379
+ *,
380
+ fw_metadata: ViewAndMutationMeta,
381
+ ) -> DispatchReturn:
382
+ """
383
+ Autograd logic. Generates a joint graph, partitions it, manipulates the input with various wrappers,
384
+ and returns a wrapped torch.autograd.Function with a forward and backward.
385
+ """
386
+ wrappers = _create_wrappers_for_dispatch(needs_autograd=True)
387
+ flat_fn, flat_args, fw_metadata = pre_compile(
388
+ wrappers,
389
+ flat_fn,
390
+ flat_args,
391
+ aot_config,
392
+ fw_metadata=fw_metadata,
393
+ )
394
+
395
+ fw_metadata.deterministic = torch.are_deterministic_algorithms_enabled()
396
+ fx_g, joint_inputs, maybe_subclass_meta = aot_dispatch_autograd_graph(
397
+ flat_fn, flat_args, aot_config, fw_metadata=fw_metadata
398
+ )
399
+
400
+ # Copied from aot_dispatch_autograd_graph.
401
+ disable_amp = torch._C._is_any_autocast_enabled()
402
+ joint_graph_str = None
403
+ if aot_config.enable_log:
404
+ aot_joint_log.info(
405
+ "%s",
406
+ lazy_format_graph_code(
407
+ "Joint graph",
408
+ fx_g,
409
+ aot_config.aot_id,
410
+ include_stride=True,
411
+ include_device=True,
412
+ colored=True,
413
+ ),
414
+ )
415
+ joint_graph_str = fx_g.print_readable(
416
+ print_output=False, include_stride=True, include_device=True
417
+ )
418
+ trace_structured(
419
+ "aot_joint_graph",
420
+ payload_fn=lambda: joint_graph_str,
421
+ )
422
+
423
+ with torch.no_grad():
424
+ inner_meta = (
425
+ fw_metadata
426
+ if maybe_subclass_meta is None
427
+ else maybe_subclass_meta.fw_metadata
428
+ )
429
+ with track_graph_compiling(aot_config, "joint"):
430
+ # See Note: [Partitioner handling for Subclasses, Part 1]
431
+ # See Note: [Recomputing subclass mutation handling]
432
+ mutated_inp_runtime_indices = (
433
+ compute_inner_mutated_inp_indices_from_subclass_meta(
434
+ fw_metadata, inner_meta
435
+ )
436
+ )
437
+ num_tokens = len(fw_metadata.tokens)
438
+ num_mutated_inp_runtime_indices = len(mutated_inp_runtime_indices)
439
+ num_inner_fwd_outputs = (
440
+ num_mutated_inp_runtime_indices
441
+ + inner_meta.num_outputs
442
+ + inner_meta.num_intermediate_bases
443
+ + inner_meta.num_outputs_rng_offset
444
+ + num_tokens # See Note [Side-Effectful Tokens in AOTAutograd]
445
+ )
446
+ fake_mode = detect_fake_mode()
447
+ if fake_mode is not None and fake_mode.shape_env is not None:
448
+ tensorify_python_scalars(fx_g, fake_mode.shape_env, fake_mode)
449
+ fw_module, bw_module = aot_config.partition_fn(
450
+ fx_g, joint_inputs, num_fwd_outputs=num_inner_fwd_outputs
451
+ )
452
+
453
+ # See Note [Side-Effectful Tokens in AOTAutograd]
454
+ if config.unlift_effect_tokens and (
455
+ num_tokens > 0 or fw_metadata.num_backward_tokens > 0
456
+ ):
457
+ unlift_tokens(fw_module, fw_metadata, aot_config, bw_module)
458
+
459
+ num_inner_fwd_outputs -= num_tokens
460
+ joint_inputs = (
461
+ joint_inputs[0][num_tokens:],
462
+ joint_inputs[1],
463
+ )
464
+
465
+ fw_outs = next(iter(fw_module.graph.find_nodes(op="output"))).args[0]
466
+ # we only need to bookkeep the symints that are saved for bw, not any symints
467
+ # the user forward might have returned in its own output
468
+ fw_outs_saved_for_bw = fw_outs[num_inner_fwd_outputs:]
469
+ num_fw_outs_saved_for_bw = len(fw_outs_saved_for_bw)
470
+ symint_outs_saved_for_bw = [
471
+ n for n in fw_outs_saved_for_bw if is_sym_node(n)
472
+ ]
473
+ fw_metadata.num_symints_saved_for_bw = len(symint_outs_saved_for_bw)
474
+ inner_meta.num_symints_saved_for_bw = len(symint_outs_saved_for_bw)
475
+ num_symints_saved_for_bw = len(symint_outs_saved_for_bw)
476
+
477
+ if torch._functorch.config.donated_buffer:
478
+ fw_metadata.bw_donated_idxs = collect_bw_donated_buffer_idxs(
479
+ fw_module,
480
+ bw_module,
481
+ inner_meta,
482
+ )
483
+ inner_meta.bw_donated_idxs = fw_metadata.bw_donated_idxs
484
+
485
+ if aot_config.enable_log:
486
+ aot_graphs_log.info(
487
+ "aot_config id: %s, fw_metadata=%s, inner_meta=%s",
488
+ str(aot_config.aot_id),
489
+ str(fw_metadata),
490
+ str(inner_meta),
491
+ )
492
+
493
+ # Note [Detaching inputs that never need gradients]
494
+ # See https://github.com/pytorch/pytorch/issues/97745
495
+ # Suppose we have a function like this that we want to compile:
496
+ #
497
+ # def f(x, y):
498
+ # return torch.mul(x, y.detach())
499
+ #
500
+ # What gradients should we compute for x and y?
501
+ # By default, AOTAutograd will compute a gradient for **every** input that requires gradients,
502
+ # and so we'll compute:
503
+ # x_grad_input = y
504
+ # y_grad_input = None
505
+ # Does this preserve the semantics of eager mode?
506
+ # Unfortunately, no.
507
+ # Doing the above will cause autograd to **continue** to backprop the autograd tape
508
+ # that was generated from constructing y.
509
+ #
510
+ # This is **different** from what would have happened in eager mode.
511
+ # In eager mode, if we backprop through the output of this function, autograd will only traverse
512
+ # the bit of the autograd tape corresponding to "x".
513
+ # In particular, if a user had previously backpropped through y's autograd tape,
514
+ # And then they try to backprop through the output of the above function,
515
+ # then we'll hit the dreaded "Trying to backward through the graph a second time" error.
516
+ #
517
+ # You might think: If autograd sees that a gradient is None, shouldn't it stop early,
518
+ # instead of continuing the backprop through the ancestors of that node in the graph?
519
+ #
520
+ # Autograd has two passes:
521
+ # (1) a first pass that traverses the autograd graph and figures out which nodes need to be executed
522
+ # (2) a second pass that actually goes ahead and executes each node when it becomes ready,
523
+ # propagating gradients
524
+ # By the time we're executing a node and we see that it produces a None, the set of nodes to execute
525
+ # is already locked-in.
526
+ #
527
+ # The fix: instead, we can recognize statically that the graph we're compiling will never contribute
528
+ # gradients to y, and prevent autograd from trying to traverse y's autograd tape at all.
529
+ # We can do this by manually detach'ing y before sending it through the `CompiledFunction`.
530
+ #
531
+ # Note that this solution is not bulletproof.
532
+ # It's possible to construct a case where eager may or may not have have tried to autograd through y,
533
+ # depending on the actual grad_outputs that were passed in during the backward.
534
+ # There is no easy fix for this: the simplest fix would be to run with `retain_graph=True`,
535
+ # allowing autograd to re-use the graph.
536
+ #
537
+ # An example of this case is:
538
+ # def f(x):
539
+ # return x.detach() * 2, x * 3
540
+ # If we were to only backprop through outs[0], in eager, we would stop
541
+ # If we backward only on the first output, we shouldn't send a grad through x.
542
+ # But the custom autograd function doesn't know that: it will materialize zero grads for x * 3
543
+ # and we will end up with a zero grad at x.
544
+ # If we later backprop through the second output, this will also require backprop'ing through x.
545
+ # Meaning we'll need to use `retain_graph=True` to be able to backprop through x the second time.
546
+ _indices_of_inps_to_detach: List[int] = []
547
+
548
+ # reversed() since we expect output at end of graph
549
+ bw_output = next(reversed(bw_module.graph.find_nodes(op="output")))
550
+ bw_outs: Sequence[torch.fx.Node] = bw_output.args[0] # type: ignore[assignment]
551
+
552
+ # TODO: we should apply the below "detach inputs if their gradients are statically known to be None"
553
+ # optimization even if we have subclass inputs/outputs (we do not handle this today).
554
+ # Computing which our our inputs get None gradients is a bit more complicated,
555
+ # if any of our inputs are subclasses. Why?
556
+ # (a) we need to make sure that we call .detach() on the input subclasses, since autograd sees subclasses.
557
+ # (b) The grad_outputs that we AOT computed in our backward graph are the desugared tensor tensors,
558
+ # so we need to figure out which subclass fw inputs they map to.
559
+ if maybe_subclass_meta is None:
560
+ num_backward_tokens: int = inner_meta.num_backward_tokens
561
+ assert (
562
+ len(bw_outs)
563
+ == len(fw_metadata.input_info)
564
+ + inner_meta.num_outputs_rng_offset
565
+ + num_backward_tokens
566
+ )
567
+ bw_outs_no_rng_no_tokens = bw_outs
568
+ if (inner_meta.num_outputs_rng_offset + num_backward_tokens) > 0:
569
+ bw_outs_no_rng_no_tokens = bw_outs[
570
+ : -(inner_meta.num_outputs_rng_offset + num_backward_tokens)
571
+ ]
572
+ assert len(bw_outs_no_rng_no_tokens) == len(fw_metadata.input_info)
573
+
574
+ for i, (bw_out) in enumerate(bw_outs_no_rng_no_tokens):
575
+ # If our input experiences a metadata mutation inside the graph (e.g. set_()),
576
+ # we *must* not detach, otherwise it will be the detach'd input that gets the metadata mutation
577
+ metadata_mutation_in_graph = (
578
+ fw_metadata.input_info[i].mutation_type
579
+ == MutationType.MUTATED_IN_GRAPH
580
+ and fw_metadata.input_info[i].mutates_storage_metadata
581
+ )
582
+ is_non_leaf = (
583
+ fw_metadata.input_info[i].requires_grad
584
+ and not fw_metadata.input_info[i].is_leaf
585
+ )
586
+ if bw_out is None and not metadata_mutation_in_graph and is_non_leaf:
587
+ _indices_of_inps_to_detach.append(i)
588
+
589
+ fw_module_str = None
590
+ bw_module_str = None
591
+ if aot_config.enable_log:
592
+ aot_graphs_log.info(
593
+ "%s",
594
+ lazy_format_graph_code(
595
+ "Forward graph",
596
+ fw_module,
597
+ aot_config.aot_id,
598
+ include_stride=True,
599
+ include_device=True,
600
+ colored=True,
601
+ ),
602
+ )
603
+ aot_graphs_log.info(
604
+ "%s",
605
+ lazy_format_graph_code(
606
+ "Backward graph",
607
+ bw_module,
608
+ aot_config.aot_id,
609
+ include_stride=True,
610
+ include_device=True,
611
+ colored=True,
612
+ ),
613
+ )
614
+ fw_module_str = fw_module.print_readable(
615
+ print_output=False, include_stride=True, include_device=True
616
+ )
617
+ bw_module_str = bw_module.print_readable(
618
+ print_output=False, include_stride=True, include_device=True
619
+ )
620
+
621
+ trace_structured(
622
+ "artifact",
623
+ metadata_fn=lambda: {
624
+ "name": "aot_forward_graph_fw_metadata",
625
+ "encoding": "string",
626
+ },
627
+ payload_fn=lambda: dataclass_repr(fw_metadata),
628
+ )
629
+ if maybe_subclass_meta is not None:
630
+ trace_structured(
631
+ "artifact",
632
+ metadata_fn=lambda: {
633
+ "name": "aot_forward_graph_fw_subclass_metadata",
634
+ "encoding": "string",
635
+ },
636
+ payload_fn=lambda: dataclass_repr(maybe_subclass_meta),
637
+ )
638
+
639
+ trace_structured(
640
+ "aot_forward_graph",
641
+ payload_fn=lambda: fw_module_str,
642
+ )
643
+ trace_structured(
644
+ "aot_backward_graph",
645
+ payload_fn=lambda: bw_module_str,
646
+ )
647
+
648
+ # AMP is already traced out in joint graph. we do not wish to reapply it accidentally
649
+ # in the compiler.
650
+ with track_graph_compiling(aot_config, "forward"), torch._C._DisableAutocast():
651
+ # flat_args at this point might still be subclasses-
652
+ # make sure to pass the unwrapped fake tensors into the compiler!
653
+ adjusted_flat_args = joint_inputs[0]
654
+
655
+ fakified_out_wrapper = FakifiedOutWrapper()
656
+ (
657
+ fw_module,
658
+ adjusted_flat_args,
659
+ fw_metadata,
660
+ ) = fakified_out_wrapper.pre_compile(
661
+ fw_module, adjusted_flat_args, aot_config, fw_metadata=fw_metadata
662
+ )
663
+
664
+ functionalized_rng_wrapper = FunctionalizedRngRuntimeWrapper(
665
+ return_new_outs=False
666
+ )
667
+ (
668
+ fw_module,
669
+ adjusted_flat_args,
670
+ fw_metadata,
671
+ ) = functionalized_rng_wrapper.pre_compile(
672
+ fw_module, adjusted_flat_args, aot_config, fw_metadata=fw_metadata
673
+ )
674
+ if tracing_context := torch._guards.TracingContext.try_get():
675
+ tracing_context.fw_metadata = inner_meta
676
+
677
+ with TracingContext.report_output_strides() as fwd_output_strides:
678
+ compiled_fw_func = aot_config.fw_compiler(fw_module, adjusted_flat_args)
679
+
680
+ if not hasattr(compiled_fw_func, "_boxed_call"):
681
+ compiled_fw_func = make_boxed_func(compiled_fw_func)
682
+
683
+ if fakified_out_wrapper.needs_post_compile:
684
+ fakified_out_wrapper.set_fwd_output_strides(fwd_output_strides)
685
+
686
+ compiled_fw_func = EffectTokensWrapper().post_compile(
687
+ compiled_fw_func,
688
+ aot_config,
689
+ runtime_metadata=fw_metadata,
690
+ )
691
+
692
+ compiled_fw_func = AOTDispatchSubclassWrapper(
693
+ fw_only=None,
694
+ trace_joint=False,
695
+ maybe_subclass_meta=maybe_subclass_meta,
696
+ num_fw_outs_saved_for_bw=num_fw_outs_saved_for_bw,
697
+ ).post_compile(
698
+ compiled_fw_func,
699
+ aot_config, # not used
700
+ runtime_metadata=fw_metadata,
701
+ )
702
+
703
+ compiled_fw_func = functionalized_rng_wrapper.post_compile(
704
+ compiled_fw_func, aot_config, runtime_metadata=fw_metadata
705
+ )
706
+ compiled_fw_func = fakified_out_wrapper.post_compile(
707
+ compiled_fw_func,
708
+ aot_config,
709
+ runtime_metadata=fw_metadata,
710
+ )
711
+
712
+ # NB: It's important to compile backwards ahead of time, as this may
713
+ # add extra guards which we need to apply to the Dynamo cache at
714
+ # forwards
715
+ with track_graph_compiling(aot_config, "backward"), torch._C._DisableAutocast():
716
+ placeholder_list = fx_placeholder_vals(bw_module)
717
+
718
+ forward_saved_for_backwards_strides = None
719
+ if fwd_output_strides is not None:
720
+ forward_saved_for_backwards_strides = fwd_output_strides[
721
+ inner_meta.tensors_saved_for_backwards_slice
722
+ ]
723
+
724
+ # saved activations can have different stride to eager if
725
+ # the compiler does layout optimization. We should restride the
726
+ # tensor passed in for compiling the backward graph using the
727
+ # saved tensor's stride.
728
+ for i in range(len(placeholder_list)):
729
+ ph_arg = placeholder_list[i]
730
+ if not isinstance(ph_arg, torch.Tensor):
731
+ continue
732
+
733
+ if forward_saved_for_backwards_strides is None:
734
+ continue
735
+
736
+ real_stride = None
737
+ # Per all_args calling convention
738
+ j = i - num_symints_saved_for_bw
739
+ if 0 <= j < len(forward_saved_for_backwards_strides):
740
+ real_stride = forward_saved_for_backwards_strides[j]
741
+ if real_stride is None:
742
+ continue
743
+
744
+ # Comparing ph_arg.stride() with real_stride directly may
745
+ # cause dynamic dimensions in ph_arg being specialized to static
746
+ # value. Using the hints to avoid that.
747
+ if _get_symint_hints(ph_arg.stride()) != real_stride:
748
+ # Note that here we use the stride of the real tensor to
749
+ # restride a FakeTensor. This does not cause trouble
750
+ # for dynamic shape since this code path only get
751
+ # executed if layout optimization is enabled. And we
752
+ # disable layout optimization for dynamic shape right
753
+ # now.
754
+ #
755
+ # A solution that decide stride order based on real
756
+ # tensor's stride and then apply that stride order to
757
+ # the FakeTensor does not work smoothly since some
758
+ # tensor's layout is not 'dense'. E.g. mixnet_l has a
759
+ # tensor with size [8, 64, 112, 112] and strides
760
+ # (2408448, 1, 21504, 192). The solution mentioned will
761
+ # decide a stride of (802816, 1, 7168, 64) for this
762
+ # tensor which is wrong.
763
+ placeholder_list[i] = ph_arg.as_strided(ph_arg.size(), real_stride)
764
+
765
+ compiled_bw_func = None
766
+ if num_symints_saved_for_bw > 0:
767
+ try:
768
+ compiled_bw_func = aot_config.bw_compiler(
769
+ bw_module, placeholder_list
770
+ )
771
+ except Exception as e:
772
+ exc = e
773
+ trace_structured(
774
+ "artifact",
775
+ metadata_fn=lambda: {
776
+ "name": "eager_compile_backwards_failure",
777
+ "encoding": "string",
778
+ },
779
+ payload_fn=lambda: "\n".join(
780
+ traceback.format_exception(
781
+ type(exc), exc, exc.__traceback__
782
+ )
783
+ ),
784
+ )
785
+ log.warning(
786
+ "failed to eagerly compile backwards for dynamic, suppressing in case backwards not needed",
787
+ exc_info=True,
788
+ )
789
+ # Compiled autograd will run the bw_module in the backward pass,
790
+ # so recompilation need happen anyway if the backward pass is ever
791
+ # called.
792
+ #
793
+ # The reason we do the GraphModule recompilation here is because
794
+ # the lazy recompilation will cause issue in the backward pass
795
+ # with compiled autograd.
796
+ #
797
+ # Do the _LazyGraphModule.force_recompile here rather than when
798
+ # bw_module is first generated by the partitioner because the bw_module.recompile
799
+ # may be called in some code path later and cause the _LazyGraphModule.forward
800
+ # becomes the lazy version again. One example is when dynamic shape is enabled
801
+ # upfront, the bw_compiler will be called above which can cause extra
802
+ # graph module recompilation on bw_module.
803
+ if torch._dynamo.compiled_autograd.in_compiled_autograd_region:
804
+ from torch.fx._lazy_graph_module import _LazyGraphModule
805
+
806
+ _LazyGraphModule.force_recompile(bw_module)
807
+
808
+ saved_context = TracingContext.try_get()
809
+ saved_compile_context = CompileContext.try_get()
810
+
811
+ backward_state_indices = [
812
+ idx for idx, x in enumerate(flat_args) if isinstance(x, BackwardState)
813
+ ]
814
+ assert len(backward_state_indices) <= 1
815
+
816
+ lazy_backward_info = AutogradLazyBackwardCompileInfo(
817
+ bw_module,
818
+ placeholder_list,
819
+ saved_context,
820
+ saved_compile_context,
821
+ )
822
+
823
+ make_runtime_safe(fw_metadata, maybe_subclass_meta)
824
+
825
+ try_save_cache_entry: Optional[Callable] = None
826
+
827
+ if autograd_cache_enabled():
828
+ cache_info = aot_config.cache_info
829
+ if cache_info is not None:
830
+ forward_time_taken_ns = time.time_ns() - cache_info.start_time_ns
831
+ else:
832
+ forward_time_taken_ns = None
833
+
834
+ def try_save_cache_entry( # noqa: F811
835
+ compiled_bw_func, _fw_metadata, aot_config
836
+ ):
837
+ fw_key = getattr(compiled_fw_func, "_fx_graph_cache_key", None)
838
+ bw_key = getattr(compiled_bw_func, "_fx_graph_cache_key", None)
839
+ cache_info = aot_config.cache_info
840
+ if cache_info is not None and fw_key and bw_key:
841
+ assert forward_time_taken_ns is not None
842
+ # TODO: technically, AOTAutograd does a *little* bit of post processing work
843
+ # in the backward that isn't measured here. But it's small enough that it's not worth
844
+ # the complexity of threading a bunch of times through the code, so we
845
+ # use the compiled_bw_func's inductor compile time instead.
846
+ # It's possible this changes in the future, in which case we should
847
+ # update backward_time_taken_ns to be more inclusive
848
+ backward_time_taken_ns = getattr(compiled_bw_func, "_time_taken_ns", 0)
849
+
850
+ aot_forward_graph_str: Optional[str] = fw_module_str
851
+ aot_backward_graph_str: Optional[str] = bw_module_str
852
+ aot_joint_graph_str: Optional[str] = joint_graph_str
853
+ entry = AOTAutogradCacheEntry(
854
+ CompiledForward(fw_key),
855
+ CompiledBackward(
856
+ bw_key,
857
+ backward_state_indices,
858
+ num_symints_saved_for_bw,
859
+ ),
860
+ aot_joint_graph_str,
861
+ aot_forward_graph_str,
862
+ aot_backward_graph_str,
863
+ _fw_metadata,
864
+ wrappers,
865
+ maybe_subclass_meta,
866
+ num_fw_outs_saved_for_bw,
867
+ _indices_of_inps_to_detach,
868
+ forward_time_taken_ns,
869
+ backward_time_taken_ns,
870
+ )
871
+ remote = should_use_remote_autograd_cache()
872
+ AOTAutogradCache.save(cache_info.cache_key, entry, remote)
873
+
874
+ if compiled_bw_func is not None:
875
+ # If we already compiled it we can just run it right now without waiting
876
+ try_save_cache_entry(compiled_bw_func, fw_metadata, aot_config)
877
+ try_save_cache_entry = None
878
+
879
+ compiled_fn = AOTDispatchAutograd.post_compile(
880
+ compiled_fw_func,
881
+ compiled_bw_func,
882
+ maybe_subclass_meta,
883
+ num_symints_saved_for_bw,
884
+ backward_state_indices,
885
+ disable_amp,
886
+ _indices_of_inps_to_detach,
887
+ lazy_backward_info,
888
+ aot_config,
889
+ fw_metadata=fw_metadata,
890
+ try_save_cache_entry=try_save_cache_entry,
891
+ )
892
+
893
+ if config.debug_assert:
894
+ flat_requires_grad: List[Optional[bool]] = [
895
+ a.requires_grad if isinstance(a, Tensor) else None for a in flat_args
896
+ ]
897
+ compiled_fn = DebugAssertWrapper(
898
+ flat_requires_grad=flat_requires_grad
899
+ ).post_compile(compiled_fn, aot_config, runtime_metadata=fw_metadata)
900
+
901
+ compiled_fn = post_compile(
902
+ wrappers,
903
+ compiled_fn,
904
+ aot_config,
905
+ runtime_metadata=fw_metadata,
906
+ )
907
+ return compiled_fn
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/logging_utils.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ Contains utils for logging in AOTAutograd, including managing the names of the graphs under
4
+ compilation, capturing user-friendly tracebacks, and debug messages.
5
+ """
6
+
7
+ import collections
8
+ from contextlib import contextmanager
9
+ from typing import List, Tuple
10
+
11
+ import torch
12
+ import torch.fx.traceback as fx_traceback
13
+
14
+
15
+ # This is a list since looking forward, we can have this arbitrarily nested.
16
+ graph_being_compiled: List[str] = []
17
+ # TODO: It would be nice to reset the numbering every time aot_id goes
18
+ # up, but this is annoying to do right now (because we don't know if
19
+ # an aot_id will come back from the dead), so right now this also happens
20
+ # to be a globally unique number too (at the cost of wobbling if you change
21
+ # how the graphs compile)
22
+ nth_graph: int = 0
23
+ model_name: str = "model"
24
+
25
+
26
+ def set_model_name(name):
27
+ global model_name
28
+ model_name = name
29
+
30
+
31
+ def get_aot_compilation_context() -> Tuple[List[str], str, int]:
32
+ return list(graph_being_compiled), model_name, nth_graph
33
+
34
+
35
+ def get_aot_graph_name() -> str:
36
+ """
37
+ Returns the name of the graph being compiled.
38
+ """
39
+ global model_name, graph_being_compiled, nth_graph
40
+ return f"{model_name}__{'_'.join(graph_being_compiled)}_{nth_graph}"
41
+
42
+
43
+ get_graph_being_compiled = get_aot_graph_name
44
+
45
+
46
+ @contextmanager
47
+ def track_graph_compiling(aot_config, graph_name):
48
+ global graph_being_compiled
49
+ # TODO: Don't shove the aot_id in here; set it in the context
50
+ graph_being_compiled = [f"{aot_config.aot_id}_{graph_name}"]
51
+ old_name = None
52
+ if tracing_context := torch._guards.TracingContext.try_get():
53
+ old_name = tracing_context.aot_graph_name
54
+ tracing_context.aot_graph_name = graph_being_compiled
55
+ has_tracing_context = True
56
+ else:
57
+ has_tracing_context = False
58
+ try:
59
+ yield
60
+ finally:
61
+ global nth_graph
62
+ nth_graph += 1
63
+ graph_being_compiled = []
64
+ if has_tracing_context:
65
+ if tracing_context := torch._guards.TracingContext.try_get():
66
+ tracing_context.aot_graph_name = old_name
67
+
68
+
69
+ # Set up hooks so that during backward the fx's stack_trace is properly set
70
+ callback_set = False
71
+
72
+
73
+ def setup_stacktrace_preservation_hooks(roots: List):
74
+ def iter_graph(roots):
75
+ if not roots:
76
+ return
77
+ seen = set()
78
+ q = collections.deque() # type: ignore[var-annotated]
79
+ for node in roots:
80
+ if node is not None and node not in seen:
81
+ seen.add(node)
82
+ q.append(node)
83
+
84
+ while q:
85
+ node = q.popleft()
86
+ for fn, _idx in node.next_functions:
87
+ if fn in seen or fn is None:
88
+ continue
89
+ seen.add(fn)
90
+ q.append(fn)
91
+
92
+ yield node
93
+
94
+ def get_callback(saved_stack_):
95
+ def callback():
96
+ global callback_set
97
+ fx_traceback.set_stack_trace(saved_stack_)
98
+ callback_set = False
99
+
100
+ return callback
101
+
102
+ def get_prehook(stack_, seq_nr):
103
+ def prehook(grad_output):
104
+ global callback_set
105
+
106
+ if not callback_set:
107
+ torch.autograd.variable.Variable._execution_engine.queue_callback( # type: ignore[attr-defined]
108
+ get_callback(fx_traceback.format_stack())
109
+ )
110
+ callback_set = True
111
+
112
+ fx_traceback.set_stack_trace(stack_)
113
+ fx_traceback.set_grad_fn_seq_nr(seq_nr)
114
+
115
+ return prehook
116
+
117
+ def get_posthook(special_stack_, seq_nr):
118
+ def posthook(grad_input, grad_output):
119
+ fx_traceback.set_stack_trace(special_stack_)
120
+ fx_traceback.reset_grad_fn_seq_nr()
121
+
122
+ return posthook
123
+
124
+ for node in iter_graph(roots):
125
+ forward_node_stack = node.metadata.get("traceback_", [])
126
+ node.register_prehook(get_prehook(forward_node_stack, node._sequence_nr()))
127
+
128
+ special_stack = forward_node_stack.copy()
129
+ special_stack.append(
130
+ "Gradient addition node due to multiple use of tensor around:"
131
+ )
132
+ node.register_hook(get_posthook(special_stack, node._sequence_nr()))
133
+
134
+
135
+ def describe_input(i, aot_config):
136
+ if i < aot_config.num_params_buffers:
137
+ return f"parameter/buffer {i}"
138
+ else:
139
+ return f"input {i - aot_config.num_params_buffers}"
140
+
141
+
142
+ def format_guard_bug_msg(aot_config, expected):
143
+ return (
144
+ f"At compilation time, graph {aot_config.aot_id} was compiled under the "
145
+ f"assumption that {expected}, but at runtime this was not the case. "
146
+ "This indicates a guard bug in AOTAutograd or Dynamo, please file a bug to PyTorch."
147
+ )
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/runtime_wrappers.py ADDED
@@ -0,0 +1,2202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ This module defines runtime wrappers, which, based on previous analysis attempts to:
4
+ 1. process the inputs and outputs
5
+ 2. apply mutations
6
+ 3. handle functionalized randomness
7
+ 4. deduplicate inputs and consolidate views into their bases (see input_output_analysis)
8
+ """
9
+ import builtins
10
+ import collections
11
+ import itertools
12
+ import pprint
13
+ from contextlib import nullcontext
14
+ from dataclasses import dataclass, field
15
+ from functools import wraps
16
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
17
+
18
+ import torch
19
+ import torch.utils.dlpack
20
+ from torch import Tensor
21
+ from torch._dynamo.utils import dynamo_timed, get_metrics_context
22
+ from torch._guards import (
23
+ compile_context,
24
+ CompileContext,
25
+ detect_fake_mode,
26
+ DuplicateInputs,
27
+ tracing,
28
+ TracingContext,
29
+ )
30
+ from torch._prims_common import CUDARngStateHelper
31
+ from torch._subclasses import FakeTensor
32
+ from torch.fx.experimental._backward_state import BackwardState
33
+ from torch.multiprocessing.reductions import StorageWeakRef
34
+ from torch.utils._python_dispatch import is_traceable_wrapper_subclass
35
+
36
+ from .. import config
37
+ from .collect_metadata_analysis import run_functionalized_fw_and_collect_metadata
38
+ from .functional_utils import gen_alias_from_base
39
+ from .input_output_analysis import (
40
+ compute_overlapping_inputs,
41
+ create_synthetic_base_metadata,
42
+ remove_dupe_metadata,
43
+ )
44
+ from .logging_utils import describe_input, format_guard_bug_msg, track_graph_compiling
45
+ from .schemas import (
46
+ AOTConfig,
47
+ InputAliasInfo,
48
+ MutationType,
49
+ OutputType,
50
+ PlainTensorMeta,
51
+ SubclassCreationMeta,
52
+ SubclassMeta,
53
+ TensorAlias,
54
+ ViewAndMutationMeta,
55
+ )
56
+ from .subclass_utils import (
57
+ requires_subclass_dispatch,
58
+ runtime_unwrap_tensor_subclasses,
59
+ wrap_tensor_subclasses,
60
+ )
61
+ from .traced_function_transforms import aot_dispatch_subclass
62
+ from .utils import (
63
+ call_func_at_runtime_with_args,
64
+ make_boxed_func,
65
+ normalize_as_list,
66
+ partial_flatten_asdict,
67
+ strict_zip,
68
+ )
69
+
70
+
71
+ zip = strict_zip
72
+
73
+
74
+ class CompilerWrapper:
75
+ """
76
+ A wrapper around the inputs and outputs to the compiler_fn. We separate these into two parts:
77
+
78
+ 1. The prologue, which edits the input to the compiler_fn(flat_fn, flat_args, etc)
79
+ 2. The epilogue, which edits the outputs of the compiler_fn (compiled_fn, real arguments)
80
+
81
+ Each wrapper below should be implemented as a CompilerWrapper, so that we can facilitate
82
+ caching on the compiled output, and re-wrapping the output via epilogues.
83
+ Extra metadata that is needed to compute pre or post compile can be passed in via attributes.
84
+ """
85
+
86
+ def pre_compile(
87
+ self,
88
+ flat_fn,
89
+ flat_args: List[Tensor],
90
+ aot_config: AOTConfig,
91
+ *,
92
+ fw_metadata: ViewAndMutationMeta,
93
+ ) -> Tuple[Callable, List[Tensor], ViewAndMutationMeta]:
94
+ """
95
+ Process the inputs to the compiler_fn. You can pass in extra metadata via kwargs.
96
+ Args:
97
+ flat_fn: The function to compile
98
+ flat_args: Metadata from example inputs of the function to compile
99
+ aot_config: AOTConfig passed in at compile time
100
+ fw_metadata: ViewAndMutationMeta generated from flat_fn and flat_args
101
+ """
102
+ return flat_fn, flat_args, fw_metadata
103
+
104
+ def post_compile(self, compiled_fn, aot_config, *, runtime_metadata) -> Callable:
105
+ """
106
+ Given an output of the compiler, wrap it with information received from prologue.
107
+ Args:
108
+ compiled_fn: Callable after calling compiler_fn
109
+ aot_config: AOTConfig after calling prologue
110
+ runtime_metadata: ViewAndMutationMeta after calling all wrappers's pre_compile steps.
111
+ Example:
112
+
113
+ def wrapped_compiled_fn(args):
114
+ # do something with args, aot_config, fw_metadata
115
+ return compiled_fn(args)
116
+
117
+ return wrapped_compiled_fn
118
+ """
119
+ return compiled_fn
120
+
121
+
122
+ # The wrapper created by this function handles all of the runtime aliasing and mutation "epilogue" logic
123
+ # that needs to run after the compiled function.
124
+ #
125
+ # This function accepts a trace_joint flag, indicating whether or not we're generating the runtime
126
+ # epilogue for a forward-only inference graph, or for an autograd.Function.apply function.
127
+ # This is because there are some minor differences in how we treat these cases at runtime:
128
+ # - resize_() is currently handled in the inference case, but not fully handled in the autograd case.
129
+ # - the autograd cases inserts TensorAlias wrapper objects for outputs that alias inputs
130
+ @dataclass
131
+ class RuntimeWrapper(CompilerWrapper):
132
+ indices_of_inps_to_detach: List[int]
133
+ trace_joint: bool
134
+ disable_amp: bool
135
+
136
+ def post_compile(
137
+ self,
138
+ compiled_fn,
139
+ aot_config: AOTConfig,
140
+ *,
141
+ runtime_metadata: ViewAndMutationMeta,
142
+ ):
143
+ return _create_runtime_wrapper(
144
+ compiled_fn,
145
+ runtime_metadata=runtime_metadata,
146
+ indices_of_inps_to_detach=self.indices_of_inps_to_detach,
147
+ trace_joint=self.trace_joint,
148
+ keep_input_mutations=aot_config.keep_inference_input_mutations,
149
+ disable_amp=self.disable_amp,
150
+ )
151
+
152
+
153
+ class NoopAliasHandler:
154
+ def __init__(self, info, runtime_metadata, trace_joint):
155
+ pass
156
+
157
+ def __call__(self, orig_inputs, fw_outs, out):
158
+ return out
159
+
160
+
161
+ def _unwrap_tensoralias(x):
162
+ assert isinstance(x, TensorAlias)
163
+ return x.alias
164
+
165
+
166
+ def _identity(x):
167
+ return x
168
+
169
+
170
+ class AliasOfInputHandler:
171
+ def __init__(self, info, runtime_metadata, trace_joint):
172
+ self.base_idx = info.base_idx
173
+ self.unwrap_out = _unwrap_tensoralias if trace_joint else _identity
174
+ self.requires_grad = info.requires_grad
175
+ self.functional_tensor = info.functional_tensor
176
+ self.replay_views = config.view_replay_for_aliased_outputs
177
+
178
+ def __call__(self, orig_inputs, fw_outs, out):
179
+ aliased_base_tensor = orig_inputs[self.base_idx]
180
+ return gen_alias_from_base(
181
+ aliased_base_tensor,
182
+ self.unwrap_out(out),
183
+ self.requires_grad,
184
+ self.functional_tensor,
185
+ replay_views=self.replay_views,
186
+ )
187
+
188
+
189
+ class IsInputHandler:
190
+ def __init__(self, info, runtime_metadata, trace_joint):
191
+ self.base_idx = info.base_idx
192
+ self.unwrap_out = _unwrap_tensoralias if trace_joint else _identity
193
+
194
+ def __call__(self, orig_inputs, fw_outs, out):
195
+ aliased_base_tensor = orig_inputs[self.base_idx]
196
+ return aliased_base_tensor
197
+
198
+
199
+ class AliasOfIntermediateHandler:
200
+ def __init__(self, info, runtime_metadata, trace_joint):
201
+ if info.output_type in (
202
+ OutputType.alias_of_intermediate,
203
+ OutputType.alias_of_intermediate_save_as_output,
204
+ ):
205
+ num_user_outputs = len(runtime_metadata.output_info)
206
+ self.base_idx = info.base_idx + num_user_outputs
207
+ else:
208
+ self.base_idx = info.base_idx
209
+
210
+ self.unwrap_out = _unwrap_tensoralias if trace_joint else _identity
211
+ self.requires_grad = info.requires_grad
212
+ self.functional_tensor = info.functional_tensor
213
+ self.replay_views = config.view_replay_for_aliased_outputs
214
+
215
+ def __call__(self, orig_inputs, fw_outs, out):
216
+ aliased_base_tensor = fw_outs[self.base_idx]
217
+ return gen_alias_from_base(
218
+ aliased_base_tensor,
219
+ self.unwrap_out(out),
220
+ self.requires_grad,
221
+ self.functional_tensor,
222
+ replay_views=self.replay_views,
223
+ )
224
+
225
+
226
+ _HANDLER_MAP = {
227
+ OutputType.non_alias: NoopAliasHandler,
228
+ OutputType.unsafe_view_alias: NoopAliasHandler,
229
+ OutputType.custom_function_view: NoopAliasHandler,
230
+ OutputType.alias_of_input: AliasOfInputHandler,
231
+ OutputType.is_input: IsInputHandler,
232
+ OutputType.alias_of_intermediate: AliasOfIntermediateHandler,
233
+ OutputType.alias_of_intermediate_save_as_output: AliasOfIntermediateHandler,
234
+ OutputType.alias_of_intermediate_base_is_user_output: AliasOfIntermediateHandler,
235
+ }
236
+
237
+
238
+ def make_output_handler(info, runtime_metadata, trace_joint):
239
+ handler_type = _HANDLER_MAP[info.output_type]
240
+ return handler_type(info, runtime_metadata, trace_joint)
241
+
242
+
243
+ def _create_runtime_wrapper(
244
+ compiled_fn,
245
+ *,
246
+ runtime_metadata: ViewAndMutationMeta,
247
+ indices_of_inps_to_detach: List[int],
248
+ trace_joint: bool,
249
+ keep_input_mutations: bool,
250
+ disable_amp: bool,
251
+ ):
252
+ if not hasattr(compiled_fn, "_boxed_call"):
253
+ compiled_fn = make_boxed_func(compiled_fn)
254
+
255
+ # Note [Inputs needed in runtime epilogue after list clearing]
256
+ # In Python functions, you can't free the input arguments of a function within the scope of that function. A workaround is to
257
+ # wrap the input arguments in a list, and clear the list from within the function.
258
+ # Here, this is implemented as `call_func_at_runtime_with_args(..., steal_args=True)`.
259
+ #
260
+ # This is needed for Compiled Autograd since some of the inputs (activations) should be freed early.
261
+ # However, we cannot blindly clear the entire list, because AOTAutograd may need access to some of the graph inputs
262
+ # **after** the compiled function has finished running. There are two main cases:
263
+ # (1) Input mutations: If there are an input mutations that we must run outside of the graph, we need access to the input.
264
+ # (2) Output aliasing: Outputs that aliases graph inputs generally must be regenerated outside of the `autograd.Function`,
265
+ # and doing so requires us accessing the corresponding input after the compiled artifact has run.
266
+ epilogue_args_idx = []
267
+ epilogue_args_idx.extend(runtime_metadata.mutated_inp_runtime_indices)
268
+ for info in runtime_metadata.output_info:
269
+ if (
270
+ info.output_type == OutputType.alias_of_input
271
+ or info.output_type == OutputType.is_input
272
+ ):
273
+ assert isinstance(info.base_idx, int)
274
+ epilogue_args_idx.append(info.base_idx)
275
+
276
+ if config.unlift_effect_tokens:
277
+ assert len(runtime_metadata.tokens) == 0
278
+
279
+ replay_views = config.view_replay_for_aliased_outputs
280
+ if runtime_metadata.num_outputs_aliased > 0:
281
+ output_handlers = tuple(
282
+ make_output_handler(info, runtime_metadata, trace_joint)
283
+ for info in runtime_metadata.output_info
284
+ )
285
+
286
+ def runtime_wrapper(args: List[Any]):
287
+ # stash a ref to each input tensor we plan to use after the compiled function
288
+ orig_inputs = {i: args[i] for i in epilogue_args_idx}
289
+
290
+ if keep_input_mutations:
291
+ mutated_args = (
292
+ args[i]
293
+ for i in runtime_metadata.mutated_graph_handled_indices_seen_by_autograd
294
+ )
295
+ torch.autograd.graph.increment_version(mutated_args)
296
+
297
+ if trace_joint:
298
+ args_ = list(args)
299
+ # See Note [Detaching inputs that never need gradients]
300
+ for idx in indices_of_inps_to_detach:
301
+ if isinstance(args_[idx], torch.Tensor):
302
+ args_[idx] = args_[idx].detach()
303
+
304
+ # It's possible to have trace_joint inside user specified with no_grad() region,
305
+ # if there is a nested with enable_grad(), that forces some outputs to require gradients.
306
+ # Therefore, we unconditionally turn on enable_grad() for compiled_fn execution.
307
+ with torch.autograd._force_original_view_tracking(
308
+ True
309
+ ), torch.enable_grad():
310
+ all_outs = call_func_at_runtime_with_args(
311
+ compiled_fn, args_, disable_amp=disable_amp, steal_args=True
312
+ )
313
+ else:
314
+ # When we have an inference graph, we run with grad disabled.
315
+ # It's possible to get an inference graph with inputs that require grad,
316
+ # in which case we want to make sure autograd is disabled
317
+ # (since e.g., inductor will generate aten.addmm.out calls which autograd will complain on)
318
+ # NOTE: We use _set_grad_enabled directly to reduce runtime overhead
319
+ grad_enabled = torch.is_grad_enabled()
320
+ try:
321
+ if grad_enabled:
322
+ torch._C._set_grad_enabled(False)
323
+ all_outs = call_func_at_runtime_with_args(
324
+ compiled_fn, args, disable_amp=disable_amp, steal_args=True
325
+ )
326
+ finally:
327
+ if grad_enabled:
328
+ torch._C._set_grad_enabled(True)
329
+ del args
330
+
331
+ num_mutated_runtime_inps = runtime_metadata.num_mutated_inp_runtime_indices
332
+ num_intermediate_bases = runtime_metadata.num_intermediate_bases
333
+
334
+ assert (
335
+ len(all_outs)
336
+ == num_mutated_runtime_inps
337
+ + runtime_metadata.num_outputs
338
+ + num_intermediate_bases
339
+ )
340
+
341
+ # Step 3: After running the compiled fw, apply updates to mutated inputs
342
+ num_mutations_to_apply = runtime_metadata.num_mutated_inp_runtime_indices
343
+ if num_mutations_to_apply > 0:
344
+ updated_inputs = all_outs[:num_mutations_to_apply]
345
+ fw_outs = all_outs[num_mutations_to_apply:]
346
+
347
+ for i, inpt_idx in enumerate(runtime_metadata.mutated_inp_runtime_indices):
348
+ meta = runtime_metadata.input_info[inpt_idx]
349
+ if not meta.mutates_data and not meta.mutates_metadata:
350
+ continue
351
+ original_inpt = orig_inputs[inpt_idx]
352
+ updated_inpt = updated_inputs[i]
353
+ if meta.mutates_storage_metadata:
354
+ # See Note [set_() Input Mutations in AOTAutograd]
355
+ # mutates_storage_metadata means our input saw a x.set_(y) call.
356
+ # What if x **also** saw a data and/or a metadata mutation?
357
+ # (1) If the [meta]data mutation occurred after the set_(),
358
+ # then there is no need to copy_() the data.
359
+ # When we perform x.set_(x_updated), we are guaranteed that
360
+ # x_updated already has the final version of the data/metadata
361
+ # (2) If a data mutation occurred before the set_().
362
+ # This case seems very difficult to support.
363
+ # TODO: discuss on the PR and decide if we want to tr to
364
+ # either support it, or detect and ban it.
365
+ if trace_joint:
366
+ assert isinstance(updated_inpt, TensorAlias)
367
+ updated_inpt = updated_inpt.alias
368
+ with torch.no_grad():
369
+ original_inpt.set_(updated_inpt)
370
+ continue
371
+ if meta.mutates_metadata and not meta.mutates_data:
372
+ if trace_joint:
373
+ assert isinstance(updated_inpt, TensorAlias)
374
+ updated_inpt = updated_inpt.alias
375
+ # We need to grab the size/stride/storage_offset from the compiled forward,
376
+ # and use that to mutate the metadata of the input
377
+ original_inpt.as_strided_(
378
+ updated_inpt.size(),
379
+ updated_inpt.stride(),
380
+ updated_inpt.storage_offset(),
381
+ )
382
+ else:
383
+ if meta.mutates_data and meta.mutates_metadata:
384
+ original_inpt.as_strided_(
385
+ updated_inpt.size(),
386
+ updated_inpt.stride(),
387
+ updated_inpt.storage_offset(),
388
+ )
389
+ else:
390
+ assert meta.mutates_data
391
+ if meta.is_leaf and original_inpt.requires_grad:
392
+ # We can hit this situation in this case:
393
+ # def f(x):
394
+ # x.detach().mul_(2)
395
+ # return x + 1
396
+ # AOTAutograd will see a mutation in the above case, and try to
397
+ # apply a copy_() here, in the epilogue.
398
+ # But if x required gradients, and is a leaf, then autograd
399
+ # will yell at us for trying to mutate it.
400
+ # However, it's only possible to end up in this scenario (like the above)
401
+ # if all of the mutations to the leaf input were non-autograd-tracking mutations
402
+ # (aka mutations under no_grad(), or on detached views).
403
+ # In that case, we fully want to hide the mutation from autograd, so detaching is ok.
404
+ original_inpt.detach().copy_(updated_inpt)
405
+ else:
406
+ original_inpt.copy_(updated_inpt)
407
+ else:
408
+ fw_outs = all_outs
409
+
410
+ # Step 4: Manually regenerate any outputs that are aliased to inputs, instead of
411
+ # compiling them.
412
+ if runtime_metadata.num_outputs_aliased > 0:
413
+ # The compiled forward also returned intermediate bases. We don't want to return them to the user.
414
+ expect_num_outputs = (
415
+ len(output_handlers) + runtime_metadata.num_intermediate_bases
416
+ )
417
+ assert len(fw_outs) == expect_num_outputs
418
+ ret_outs = [
419
+ handler(orig_inputs, fw_outs, out)
420
+ for out, handler in builtins.zip(fw_outs, output_handlers)
421
+ ]
422
+ else:
423
+ ret_outs = fw_outs
424
+
425
+ if runtime_metadata.dynamic_outputs:
426
+ for t, o in zip(ret_outs, runtime_metadata.output_info):
427
+ if o.dynamic_dims is None:
428
+ continue
429
+ if hasattr(t, "_dynamo_weak_dynamic_indices"):
430
+ t._dynamo_weak_dynamic_indices |= o.dynamic_dims
431
+ else:
432
+ t._dynamo_weak_dynamic_indices = o.dynamic_dims.copy()
433
+ if runtime_metadata.grad_enabled_mutation is not None:
434
+ torch._C._set_grad_enabled(runtime_metadata.grad_enabled_mutation)
435
+ return ret_outs
436
+
437
+ return runtime_wrapper
438
+
439
+
440
+ @dataclass
441
+ class FunctionalizedRngRuntimeWrapper(CompilerWrapper):
442
+ # TODO: I would love to get rid of this argument, but it's
443
+ # Wrapped pretty tightly around our aot_dispatch_autograd logic.
444
+ # Specifically, tensors_saved_for_backwards_slice's value is both used for calculating indices
445
+ # for setting placeholder strides(which is done before runtime, before this wrapper runs)
446
+ # and for saving tensors for backward (which is done during runtime, after this wrapper runs)
447
+ # So in aot_dispatch_autograd, this wrapper can't edit the set of outs without making one
448
+ # of those two indices incorrect.
449
+ return_new_outs: bool = True
450
+
451
+ def pre_compile(
452
+ self,
453
+ flat_fn,
454
+ flat_args,
455
+ aot_config,
456
+ *,
457
+ fw_metadata,
458
+ ) -> Tuple[Callable, List[Tensor], ViewAndMutationMeta]:
459
+ if config.functionalize_rng_ops:
460
+ # Update example inputs for the fw_compiler
461
+ fake_mode = detect_fake_mode()
462
+ seed, offset = CUDARngStateHelper.get_torch_state_as_tuple(fake_mode)
463
+ flat_args.extend([seed, offset])
464
+ # We are not clearing flat_args here because
465
+ # 1) There is a check in the debug compiler at the end
466
+ # 2) It does not matter as these are fake tensors
467
+ return flat_fn, flat_args, fw_metadata
468
+
469
+ def post_compile(
470
+ self,
471
+ compiled_fn,
472
+ aot_config: AOTConfig,
473
+ *,
474
+ runtime_metadata: ViewAndMutationMeta,
475
+ ):
476
+ @wraps(compiled_fn)
477
+ def wrapper(runtime_args: List[Any]):
478
+ if runtime_metadata.is_rng_op_functionalized:
479
+ # Add the seed and offset to args
480
+ seed, offset = CUDARngStateHelper.get_torch_state_as_tuple()
481
+ runtime_args.extend([seed, offset])
482
+ out = compiled_fn(runtime_args)
483
+ out = self._functionalized_rng_runtime_epilogue(
484
+ runtime_metadata,
485
+ out,
486
+ # TODO: this won't be right for the backward when we convert the call_compiled_backward to use the wrapper
487
+ runtime_metadata.num_forward_returns,
488
+ )
489
+ return out
490
+ return compiled_fn(runtime_args)
491
+
492
+ return wrapper
493
+
494
+ # Calling convention: If we are running functionalized RNG, then outs consists
495
+ # of (user_outs, rng_offset)
496
+ def _functionalized_rng_runtime_epilogue(
497
+ self,
498
+ metadata: ViewAndMutationMeta,
499
+ outs,
500
+ offset_index,
501
+ ):
502
+ if metadata.is_rng_op_functionalized:
503
+ assert metadata.num_outputs_rng_offset == 1
504
+ new_rng_offset = outs[offset_index]
505
+ CUDARngStateHelper.set_new_offset(new_rng_offset)
506
+ if self.return_new_outs:
507
+ user_outs = outs[:offset_index] + outs[offset_index + 1 :]
508
+ return user_outs
509
+ else:
510
+ return outs
511
+
512
+ return outs
513
+
514
+
515
+ @dataclass
516
+ class FakifiedOutWrapper(CompilerWrapper):
517
+ out_metas: List[torch.Tensor] = field(default_factory=list)
518
+ # TracingContext.fwd_output_strides
519
+ # Generated from actually doing compile
520
+ fwd_output_strides: Optional[List[List[int]]] = None
521
+ needs_post_compile: bool = True
522
+
523
+ def pre_compile(
524
+ self,
525
+ fw_module, # Must be fw_module from aot_dispatch_*_graph
526
+ flat_args,
527
+ aot_config,
528
+ *,
529
+ fw_metadata,
530
+ ) -> Tuple[Callable, List[Tensor], ViewAndMutationMeta]:
531
+ tracing_context = torch._guards.TracingContext.try_get()
532
+ if tracing_context and tracing_context.fakify_first_call:
533
+ self.out_metas = [
534
+ n.meta["val"] for n in (list(fw_module.graph.nodes)[-1].args[0])
535
+ ]
536
+ else:
537
+ self.needs_post_compile = False
538
+ return fw_module, flat_args, fw_metadata
539
+
540
+ def _compute_output_meta_with_inductor_strides(self):
541
+ out = self.out_metas
542
+ fwd_output_strides = self.fwd_output_strides
543
+ if not fwd_output_strides:
544
+ return out
545
+
546
+ from torch.fx.experimental.symbolic_shapes import statically_known_true
547
+
548
+ for i in range(len(out)):
549
+ if not isinstance(out[i], Tensor):
550
+ continue
551
+ if all(
552
+ statically_known_true(s1 == s2)
553
+ for s1, s2 in zip(out[i].stride(), fwd_output_strides[i])
554
+ ):
555
+ continue
556
+ out[i] = out[i].as_strided(out[i].shape, fwd_output_strides[i])
557
+ return out
558
+
559
+ # To be called post compile
560
+ def set_fwd_output_strides(self, fwd_output_strides):
561
+ self.fwd_output_strides = fwd_output_strides
562
+
563
+ def post_compile(
564
+ self,
565
+ compiled_fn,
566
+ aot_config: AOTConfig,
567
+ *,
568
+ runtime_metadata: ViewAndMutationMeta,
569
+ ):
570
+ if self.needs_post_compile:
571
+ assert self.fwd_output_strides is not None
572
+ fakified_out = self._compute_output_meta_with_inductor_strides()
573
+
574
+ @wraps(compiled_fn)
575
+ def wrapper(runtime_args):
576
+ nonlocal fakified_out
577
+ if fakified_out is not None:
578
+ out = fakified_out
579
+ fakified_out = None
580
+ return out
581
+ return compiled_fn(runtime_args)
582
+
583
+ return wrapper
584
+ # If we don't need to fakify, we can just return the original compiled function
585
+ return compiled_fn
586
+
587
+
588
+ # This wrapper handles the AOTDispatch runtime logic for tensor subclasses.
589
+ # At runtime, we have a compiled function that knows how to operate on the domain of DenseTensor -> DenseTensor,
590
+ # But the user might have passed us some tensor subclass inputs (or expect some subclass tensor outputs).
591
+ # This function handles the wrapping and unwrapping of tensor subclasses at runtime.
592
+ @dataclass
593
+ class AOTDispatchSubclassWrapper(CompilerWrapper):
594
+ trace_joint: bool
595
+ fw_only: Optional[Callable] # Not cached, only used in pre_compile
596
+ maybe_subclass_meta: Optional[SubclassMeta]
597
+ num_fw_outs_saved_for_bw: Optional[int]
598
+
599
+ def pre_compile(
600
+ self,
601
+ flat_fn,
602
+ flat_args: List[Tensor],
603
+ aot_config: AOTConfig,
604
+ *,
605
+ fw_metadata: ViewAndMutationMeta,
606
+ ):
607
+ (new_flat_fn, new_flat_args, subclass_meta) = aot_dispatch_subclass(
608
+ flat_fn,
609
+ flat_args,
610
+ is_joint_structure=self.trace_joint,
611
+ meta=fw_metadata,
612
+ fw_only=self.fw_only, # type: ignore[arg-type]
613
+ )
614
+ self.maybe_subclass_meta = subclass_meta
615
+ return new_flat_fn, new_flat_args, fw_metadata
616
+
617
+ def post_compile(
618
+ self,
619
+ compiled_fn,
620
+ _aot_config: AOTConfig,
621
+ *,
622
+ runtime_metadata: ViewAndMutationMeta,
623
+ ):
624
+ if self.maybe_subclass_meta is None:
625
+ return compiled_fn
626
+
627
+ subclass_metas = runtime_metadata.subclass_fw_graph_out_meta
628
+
629
+ @wraps(compiled_fn)
630
+ def inner_fn(args: List[Any]):
631
+ unwrapped_args = runtime_unwrap_tensor_subclasses(
632
+ args,
633
+ subclass_metas=runtime_metadata.subclass_inp_meta,
634
+ append_symints=True,
635
+ )
636
+ args.clear()
637
+ # expectation: runtime_fn is a boxed fn
638
+ unwrapped_outs = compiled_fn(unwrapped_args)
639
+ wrapped_outs = wrap_tensor_subclasses(
640
+ unwrapped_outs,
641
+ subclass_metas=subclass_metas,
642
+ num_fw_outs_saved_for_bw=self.num_fw_outs_saved_for_bw,
643
+ is_runtime=True,
644
+ included_subclass_symints=True,
645
+ )
646
+ return wrapped_outs
647
+
648
+ # box it
649
+ inner_fn._boxed_call = True # type: ignore[attr-defined]
650
+ return inner_fn
651
+
652
+
653
+ @dataclass
654
+ class EffectTokensWrapper(CompilerWrapper):
655
+ def post_compile(
656
+ self,
657
+ compiled_fn,
658
+ _aot_config,
659
+ *,
660
+ runtime_metadata: ViewAndMutationMeta,
661
+ ):
662
+ num_tokens = len(runtime_metadata.tokens)
663
+
664
+ @wraps(compiled_fn)
665
+ def inner_fn(args: List[Any]):
666
+ if num_tokens > 0:
667
+ # Pass in forward effect tokens (See Note [Side-Effectful Tokens in AOTAutograd])
668
+ old_args = args
669
+ args = [*([None] * num_tokens), *args]
670
+ old_args.clear()
671
+
672
+ outs = compiled_fn(args)
673
+
674
+ # Inductor cache DummyModule can return None
675
+ if outs is None:
676
+ return None
677
+ # Toss out the effect tokens (See Note [Side-Effectful Tokens in AOTAutograd])
678
+ return outs[num_tokens:] if num_tokens != 0 else outs
679
+
680
+ # box it
681
+ inner_fn._boxed_call = True # type: ignore[attr-defined]
682
+ return inner_fn
683
+
684
+
685
+ # MOTIVATION:
686
+ #
687
+ # When tracing functions for future execution, one must be careful not to pass
688
+ # in the same input tensor multiple times (e.g., f(x, x), as this can result
689
+ # in graphs that are ONLY valid if you later pass a new tensor in exactly the
690
+ # same way (e.g., f(y, y)). (NB: we really mean duplicate; two distinct
691
+ # tensors that alias each other is a different situation that is covered by
692
+ # aot_dispatch_deduplicated_autograd). Here are two examples:
693
+ #
694
+ # (1) Suppose you have a function:
695
+ #
696
+ # def f(x, y):
697
+ # return x + y
698
+ #
699
+ # If you make_fx(f)(x, x), you will trace out:
700
+ #
701
+ # def f(x, y):
702
+ # return y + y
703
+ #
704
+ # Oops!
705
+ #
706
+ # (2) For most tensors x and y, you can compute f's gradient with respect to
707
+ # these to inputs by saying torch.autograd.grad(f(x, y), (x, y)). However,
708
+ # if x is y, you will trace out a program that gets incorrect gradients:
709
+ #
710
+ # >>> x = torch.randn(1, requires_grad=True)
711
+ # >>> torch.autograd.grad(x + x, (x, x))
712
+ # (tensor([2.]), tensor([2.]))
713
+ #
714
+ # In other words, the gradient is double-counted. Deduplicating the arguments
715
+ # gives you an appropriate gradient:
716
+ #
717
+ # >>> y = torch.randn(1, requires_grad=True)
718
+ # >>> torch.autograd.grad(x + y, (x, y))
719
+ # (tensor([1.]), tensor([1.]))
720
+ #
721
+ # HOW TO DEDUPLICATE:
722
+ #
723
+ # There are a few strategies, in order of preference:
724
+ #
725
+ # 1. For every duplicate argument to the function, detach it into
726
+ # a separate leaf tensor, so that it is no longer duplicated.
727
+ #
728
+ # PRO: The resulting compiled graph works for any configuration
729
+ # of duplicated arguments.
730
+ #
731
+ # CON: It does not (naively) work if you mutate the metadata of inputs:
732
+ #
733
+ # def f(x, y):
734
+ # x.transpose_(0, 1)
735
+ # y.transpose_(0, 2)
736
+ #
737
+ # x = torch.randn(2, 3, 4)
738
+ # f(x, x)
739
+ #
740
+ # The ordering of the transposes inside f dictates whether or not
741
+ # you get [4, 2, 3] or [3, 4, 2]. This means that you cannot precompute
742
+ # what metadata mutations should get applied to each input; you need to
743
+ # assume they aren't duplicates (what we do today) or preserve
744
+ # the original metadata mutations exactly in order, so that they work
745
+ # for any duplicate configuration.
746
+ #
747
+ # CON: It does not (naively) work if you mutate the data of inputs.
748
+ # In particular, leaf tensors that require grad cannot be mutated,
749
+ # this makes it impossible to differentiate with respect to the original
750
+ # base.
751
+ #
752
+ # 2. For every duplicate argument to the function, remove it, so it is
753
+ # no longer part of the "true" signature:
754
+ #
755
+ # PRO: Implemented naively, it still works for metadata/data mutation.
756
+ #
757
+ # CON: The resulting compiled graph is duplicate-specialized: it only
758
+ # works if future calls duplicate arguments in exactly the same way.
759
+ # Horribly, Dynamo doesn't guard on this at the moment. But even if
760
+ # it did, you could still end up recompiling a bunch of each duplicate.
761
+ #
762
+ # Our strategy is to do (1) if we can, and do (2) otherwise, erroring if
763
+ # Dynamo's guards are not enough. In practice, this seems to cover
764
+ # everything.
765
+ #
766
+ @dataclass
767
+ class AOTDedupeWrapper(CompilerWrapper):
768
+ keep_arg_mask: List[bool] = field(default_factory=list)
769
+ add_dupe_map: List[int] = field(default_factory=list)
770
+ old_input_metadata: List[InputAliasInfo] = field(default_factory=list)
771
+ needs_post_compile: bool = True
772
+
773
+ # NB: Hot path, avoid set lookups here
774
+ # TODO: Can avoid the zip here too, probably
775
+ def remove_dupe_args(self, args):
776
+ return [t for t, keep in zip(args, self.keep_arg_mask) if keep]
777
+
778
+ def add_dupe_args(self, args):
779
+ return [args[i] for i in self.add_dupe_map]
780
+
781
+ def pre_compile(
782
+ self,
783
+ flat_fn,
784
+ flat_args: List[Tensor],
785
+ aot_config: AOTConfig,
786
+ *,
787
+ fw_metadata: ViewAndMutationMeta,
788
+ ) -> Tuple[Callable, List[Tensor], ViewAndMutationMeta]:
789
+ # Use information about whether or not flat_fn mutates its arguments
790
+ # or not to handle dupe args
791
+
792
+ # Strategy 1: For any input that is not mutated, we can leafify it if we
793
+ # need to remove a duplicate.
794
+ leaf_flat_args = []
795
+ args_set = set()
796
+ ok = True
797
+
798
+ for i, a in enumerate(flat_args):
799
+ if not isinstance(a, torch.Tensor):
800
+ leaf_flat_args.append(a)
801
+ elif a not in args_set:
802
+ args_set.add(a)
803
+ leaf_flat_args.append(a)
804
+ elif (
805
+ not fw_metadata.input_info[i].mutates_data
806
+ and not fw_metadata.input_info[i].mutates_metadata
807
+ ):
808
+ leaf_flat_args.append(a.detach().requires_grad_(a.requires_grad))
809
+ else:
810
+ ok = False
811
+ break
812
+
813
+ if ok:
814
+ self.needs_post_compile = False
815
+ return flat_fn, leaf_flat_args, fw_metadata
816
+
817
+ if requires_subclass_dispatch(leaf_flat_args, fw_metadata):
818
+ raise RuntimeError(
819
+ """\
820
+ Encountered duplicate inputs that are mutated in the graph, but at least one input/output
821
+ to the graph is a tensor subclass. This is not supported today. You can try to
822
+ remove the aliasing yourself as a workaround, or otherwise file an issue on github."""
823
+ )
824
+
825
+ # export path: ban duplicate inputs for now, add later if requested.
826
+ if aot_config.is_export:
827
+ raise RuntimeError(
828
+ f"""\
829
+ Encountered duplicated inputs that are mutated in the graph you are trying to export.
830
+ This functionality is currently not supported. If needed, please file a github issue.
831
+
832
+ fw_metadata={str(fw_metadata)}
833
+ """
834
+ )
835
+
836
+ # Strategy 2: Duplicate specialize.
837
+ #
838
+ # In Haskell types, suppose you have:
839
+ #
840
+ # add_dupe_args :: DedupedArgs -> Args
841
+ # remove_dupe_args :: Args -> DedupedArgs
842
+ #
843
+ # compiler_fn
844
+ # :: (DedupedArgs -> R) -> DedupedArgs -> AOTConfig -> (DedupedArgs -> R)
845
+ # deped_compiler_fn
846
+ # :: (Args -> R) -> Args -> AOTConfig -> (Args -> R)
847
+ #
848
+ # Then the code below can be written in point-free style as:
849
+ #
850
+ # deduped_compiler_fn f a c =
851
+ # compiler_fn (f . add_dupe_args) (remove_dupe_args a) c . remove_dupe_args
852
+ #
853
+ # Suppose you have:
854
+ #
855
+ # [a, b, a, c]
856
+ #
857
+ # We want:
858
+ #
859
+ # remove_dupe_args([a, b, a, c]) == [a, b, c]
860
+ # add_dupe_args([a, b, c]) == [a, b, a, c]
861
+ #
862
+ # This is done via (respectively):
863
+ #
864
+ # seen_args = {a: 0, b: 1, c: 2}
865
+ # enumerate(add_dupe_map) = [ # how to get args from the deduped list
866
+ # (0, 0),
867
+ # (1, 1),
868
+ # (2, 0),
869
+ # (3, 2),
870
+ # ]
871
+ # keep_arg_mask = [True, True, False, True]
872
+
873
+ seen_args: Dict[Tensor, int] = {}
874
+ # Implicitly map duped arg position (list index) to de-duped arg position
875
+ keep_arg_mask: List[bool] = []
876
+ add_dupe_map: List[int] = []
877
+ duped_arg_len = len(flat_args)
878
+
879
+ j = 0 # index into deduped_flat_args
880
+ for t in flat_args:
881
+ if isinstance(t, torch.Tensor):
882
+ if t in seen_args:
883
+ keep_arg_mask.append(False)
884
+ add_dupe_map.append(seen_args[t])
885
+ continue
886
+ seen_args[t] = j
887
+
888
+ keep_arg_mask.append(True)
889
+ add_dupe_map.append(j)
890
+ j += 1
891
+ assert (
892
+ len(add_dupe_map) == duped_arg_len
893
+ ), f"Expects add_dupe_map to have length {duped_arg_len} but got {len(add_dupe_map)}"
894
+
895
+ self.keep_arg_mask = keep_arg_mask
896
+ self.add_dupe_map = add_dupe_map
897
+
898
+ deduped_flat_args = self.remove_dupe_args(flat_args)
899
+
900
+ # Update our input metadata to remove duped input metadata.
901
+ updated_fw_metadata = remove_dupe_metadata(
902
+ fw_metadata, keep_arg_mask, add_dupe_map
903
+ )
904
+
905
+ if (
906
+ tracing_context := TracingContext.try_get()
907
+ and aot_config.aot_autograd_arg_pos_to_source
908
+ ):
909
+ # TODO(voz): This structure is 1:1, we could consider an alternate structure like
910
+ # kept_pos:[dupe_arg_pos], however, add_dupe_map is 1:1 so we would need a new structure there,
911
+ # which feels like needless complexity for a tiny bit of efficiency at this point.
912
+ for dupe_arg_pos, (kept_pos, keep_arg) in enumerate(
913
+ zip(add_dupe_map, keep_arg_mask)
914
+ ):
915
+ if not keep_arg:
916
+ dupe_arg_source = aot_config.aot_autograd_arg_pos_to_source[
917
+ dupe_arg_pos
918
+ ]
919
+ kept_arg_source = aot_config.aot_autograd_arg_pos_to_source[
920
+ kept_pos
921
+ ]
922
+ tracing_context.guards_context.aotautograd_guards.append( # type: ignore[attr-defined]
923
+ DuplicateInputs(kept_arg_source, dupe_arg_source)
924
+ )
925
+
926
+ @wraps(flat_fn)
927
+ def wrapped_flat_fn(*args):
928
+ return flat_fn(*self.add_dupe_args(args))
929
+
930
+ if config.debug_assert:
931
+ ref_fw_metadata = run_functionalized_fw_and_collect_metadata(
932
+ wrapped_flat_fn,
933
+ static_input_indices=aot_config.static_input_indices,
934
+ keep_input_mutations=fw_metadata.keep_input_mutations,
935
+ is_train=fw_metadata.is_train,
936
+ )(*deduped_flat_args)
937
+ assert (
938
+ ref_fw_metadata == updated_fw_metadata
939
+ ), f"ref_metadata={str(ref_fw_metadata)}, actual_metadata={str(updated_fw_metadata)}"
940
+
941
+ return wrapped_flat_fn, deduped_flat_args, updated_fw_metadata
942
+
943
+ def post_compile(
944
+ self,
945
+ compiled_fn,
946
+ aot_config: AOTConfig,
947
+ *,
948
+ runtime_metadata: ViewAndMutationMeta,
949
+ ):
950
+ if not self.needs_post_compile:
951
+ return compiled_fn
952
+
953
+ @wraps(compiled_fn)
954
+ def wrapped_compiled_fn(args: List[Any]):
955
+ deduped_args = self.remove_dupe_args(args)
956
+ args.clear()
957
+ return compiled_fn(deduped_args)
958
+
959
+ wrapped_compiled_fn._boxed_call = True # type: ignore[attr-defined]
960
+
961
+ # This can be uncommented when we properly guard for duplicates,
962
+ # but right now we must not do it.
963
+ # if not config.debug_assert:
964
+ # return wrapped_compiled_fn
965
+
966
+ @wraps(wrapped_compiled_fn)
967
+ def debugged_compiled_fn(args):
968
+ # Test that the computed remove/add arg functions are an inverse
969
+ new_args = self.add_dupe_args(self.remove_dupe_args(args))
970
+ seen: Dict[Any, None] = {}
971
+ for i, (x, y) in enumerate(zip(new_args, args)):
972
+ seen[y] = None
973
+ assert x is y, format_guard_bug_msg(
974
+ aot_config,
975
+ f"{describe_input(i, aot_config)} would be a duplicate of "
976
+ f"{describe_input(self.add_dupe_map[i], aot_config)}",
977
+ )
978
+ # This is only an error if there is metadata mutation on both of
979
+ # the duped arguments; in this case, we need to know what order
980
+ # the metadata mutation applies in. You'll get the correct result
981
+ # otherwise, because a graph that assumes distinct inputs works if
982
+ # you dupe the inputs (the gradient contributions from each input
983
+ # will get summed up appropriately.)
984
+ #
985
+ # TODO: work out how to setup this assert correctly
986
+ """
987
+ assert len(seen) == unique_args, format_guard_bug_msg(aot_config,
988
+ f"there would be {unique_args} distinct arguments"
989
+ )
990
+ """
991
+ return wrapped_compiled_fn(args)
992
+
993
+ debugged_compiled_fn._boxed_call = True # type: ignore[attr-defined]
994
+
995
+ return debugged_compiled_fn
996
+
997
+
998
+ # This layer handles the situation where you have two inputs that alias each other,
999
+ # and one of the inputs is mutated.
1000
+ # We need to take special care to ensure that the mutation is applied to the other aliases in the graph.
1001
+ #
1002
+ # pre-condition: AOTDedupWrapper has already run.
1003
+ # (This function will in theory work if there are duplicate args.
1004
+ # However, the synthetic base code path is a bit sub-optimal, and running with dupe'd inputs
1005
+ # would cause us to hit that path more frequently).
1006
+ @dataclass
1007
+ class AOTSyntheticBaseWrapper(CompilerWrapper):
1008
+ # Currently, the only reason we need to plumb this bool is because
1009
+ # the synthetic base code prohibits more cases in the autograd case than the inference case.
1010
+ trace_joint: bool # TODO: refactor trace_joint
1011
+ needs_post_compile: bool = True
1012
+ aliased_arg_idx_with_metadata_mutations: List[int] = field(default_factory=list)
1013
+
1014
+ def pre_compile(
1015
+ self,
1016
+ flat_fn,
1017
+ flat_args: List[Any],
1018
+ aot_config: AOTConfig,
1019
+ *,
1020
+ fw_metadata: ViewAndMutationMeta,
1021
+ ) -> Tuple[Callable, List[Tensor], ViewAndMutationMeta]:
1022
+ is_inference = not self.trace_joint
1023
+ flat_args_with_synthetic_bases, synthetic_base_info = merge_view_inputs(
1024
+ aot_config,
1025
+ flat_args,
1026
+ fw_metadata.input_info,
1027
+ is_inference=is_inference,
1028
+ )
1029
+
1030
+ # Happy path: we don't need synthetic bases
1031
+ if synthetic_base_info is None:
1032
+ self.needs_post_compile = False
1033
+ return flat_fn, flat_args, fw_metadata
1034
+
1035
+ # export path: ban synthetic bases for now, add later if requested.
1036
+ if requires_subclass_dispatch(flat_args, fw_metadata):
1037
+ raise RuntimeError(
1038
+ """\
1039
+ Encountered aliased inputs that are mutated in the graph, but at least one input/output
1040
+ to the graph is a tensor subclass. This is not supported today. You can try to
1041
+ remove the aliasing yourself as a workaround, or otherwise file an issue on github."""
1042
+ )
1043
+
1044
+ if aot_config.is_export:
1045
+ raise RuntimeError(
1046
+ f"""\
1047
+ Encountered aliased inputs that are mutated in the graph you are trying to export.
1048
+ This functionality is currently not supported. If needed, please file a github issue.
1049
+
1050
+ synthetic_base_info={str(synthetic_base_info)}
1051
+
1052
+ fw_metadata={str(fw_metadata)}
1053
+ """
1054
+ )
1055
+
1056
+ assert len(fw_metadata.input_info) == len(synthetic_base_info)
1057
+
1058
+ # Update our forward metadata to take synthetic bases into account
1059
+ (
1060
+ fw_metadata_updated,
1061
+ aliased_arg_idx_with_metadata_mutations,
1062
+ ) = create_synthetic_base_metadata(
1063
+ fw_metadata, synthetic_base_info, flat_args, flat_args_with_synthetic_bases
1064
+ )
1065
+ # Save old input args for post-compile
1066
+ self.old_input_info = fw_metadata.input_info
1067
+
1068
+ self.aliased_arg_idx_with_metadata_mutations = (
1069
+ aliased_arg_idx_with_metadata_mutations
1070
+ )
1071
+
1072
+ num_aliased_args_with_metadata_mutations = len(
1073
+ aliased_arg_idx_with_metadata_mutations
1074
+ )
1075
+
1076
+ replay_views = config.view_replay_for_aliased_outputs
1077
+
1078
+ def _unpack_synthetic_bases(primals: Tuple[Any, ...]) -> List[Any]:
1079
+ f_args_inner = []
1080
+ for inner_idx_or_tuple in synthetic_base_info:
1081
+ if isinstance(inner_idx_or_tuple, int):
1082
+ f_args_inner.append(primals[inner_idx_or_tuple])
1083
+ else:
1084
+ inner_base_idx, view_tensor = inner_idx_or_tuple
1085
+ base = primals[inner_base_idx]
1086
+ view_arg = gen_alias_from_base(
1087
+ base,
1088
+ view_tensor,
1089
+ view_tensor.requires_grad,
1090
+ replay_views=replay_views,
1091
+ )
1092
+ f_args_inner.append(view_arg)
1093
+ return f_args_inner
1094
+
1095
+ @wraps(flat_fn)
1096
+ def wrapped_flat_fn(*args):
1097
+ unpacked_args = _unpack_synthetic_bases(args)
1098
+ # This is a bit subtle. The goal of this entire function (aot_dispatch_synthetic_bases)
1099
+ # is to relieve the downstream logic from having to reason about mutations on inputs that alias
1100
+ # each other, by replacing aliased inputs with a synthetic base.
1101
+ # One area where this breaks down a bit however is if one of those aliased inputs
1102
+ # experienced a metadata mutation.
1103
+ # We are now obligated to reapply the metadata mutation directly to the user's input;
1104
+ # it isn't enough to apply mutations back to the synthetic base in the downstream logic.
1105
+ #
1106
+ # The way we handle this is by pretending that those aliased inputs that experience metadata mutations
1107
+ # are additional outputs in the user's forward function.
1108
+ # The downstream logic will just treat these as "user outputs that alias inputs".
1109
+ # However, we will manually grab them at runtime here, use them to reapply the metadata mutation
1110
+ # to the user inputs, and not return them to the user.
1111
+ aliased_args_with_metadata_mutations = [
1112
+ x
1113
+ for i, x in enumerate(unpacked_args)
1114
+ if i in self.aliased_arg_idx_with_metadata_mutations
1115
+ ]
1116
+ if len(aliased_args_with_metadata_mutations) > 0:
1117
+ return *(flat_fn(*unpacked_args)), *aliased_args_with_metadata_mutations
1118
+ else:
1119
+ return flat_fn(*unpacked_args)
1120
+
1121
+ if config.debug_assert:
1122
+ ref_fw_metadata = run_functionalized_fw_and_collect_metadata(
1123
+ wrapped_flat_fn,
1124
+ static_input_indices=aot_config.static_input_indices,
1125
+ keep_input_mutations=fw_metadata.keep_input_mutations,
1126
+ is_train=fw_metadata.is_train,
1127
+ )(*flat_args_with_synthetic_bases)
1128
+ assert ref_fw_metadata == fw_metadata_updated, (
1129
+ f"ref_metadata={pprint.pformat(partial_flatten_asdict(ref_fw_metadata))}, "
1130
+ f"\nactual_metadata={pprint.pformat(partial_flatten_asdict(fw_metadata_updated))}"
1131
+ )
1132
+ return (
1133
+ wrapped_flat_fn,
1134
+ flat_args_with_synthetic_bases,
1135
+ fw_metadata_updated,
1136
+ )
1137
+
1138
+ def post_compile(
1139
+ self,
1140
+ compiled_fn,
1141
+ aot_config: AOTConfig,
1142
+ *,
1143
+ runtime_metadata: ViewAndMutationMeta,
1144
+ ):
1145
+ if not self.needs_post_compile:
1146
+ return compiled_fn
1147
+
1148
+ is_inference = not self.trace_joint
1149
+
1150
+ @wraps(compiled_fn)
1151
+ def wrapped_compiled_fn(args):
1152
+ args_with_synthetic_bases, synthetic_base_info = merge_view_inputs(
1153
+ aot_config, args, self.old_input_info, is_inference=is_inference
1154
+ )
1155
+ assert synthetic_base_info is not None
1156
+ aliased_args_w_metadata_mutations = [
1157
+ args[i] for i in self.aliased_arg_idx_with_metadata_mutations
1158
+ ]
1159
+ num_aliased_args_with_metadata_mutations = len(
1160
+ aliased_args_w_metadata_mutations
1161
+ )
1162
+ args.clear()
1163
+ outs = compiled_fn(args_with_synthetic_bases)
1164
+ if num_aliased_args_with_metadata_mutations > 0:
1165
+ # This code does not handle **all** input metadata mutations.
1166
+ # Instead, it only handles metadata mutations on inputs that were converted into synthetic bases
1167
+ # (which only happens if at least one aliased input experienced a data mutation).
1168
+ # e.g:
1169
+ # def f(a, b):
1170
+ # a.mul_(2)
1171
+ # b.t_(1, 0)
1172
+ # f(x.view(2, 2), x.view(2, 2))
1173
+ mutated_metadata_inps = outs[-num_aliased_args_with_metadata_mutations:]
1174
+ user_outs = outs[:-num_aliased_args_with_metadata_mutations]
1175
+ for inp, mutated_inp in zip(
1176
+ aliased_args_w_metadata_mutations, mutated_metadata_inps
1177
+ ):
1178
+ inp.as_strided_(
1179
+ mutated_inp.size(),
1180
+ mutated_inp.stride(),
1181
+ mutated_inp.storage_offset(),
1182
+ )
1183
+ return user_outs
1184
+ return outs
1185
+
1186
+ return wrapped_compiled_fn
1187
+
1188
+
1189
+ # Note [Handling mutations on an input that aliases other inputs]
1190
+ # The easiest example to show-case this edge case is here:
1191
+ #
1192
+ # def f(a, b):
1193
+ # a.mul_(2)
1194
+ # out = a + b
1195
+ # return out
1196
+ # b = torch.ones(...)
1197
+ # a = b.view(-1)
1198
+ # f(a, b)
1199
+ #
1200
+ # In this situation, if a and b happened to be aliased, we need to trace something different!
1201
+ # Suppose we had b = a.view(-1)
1202
+ # (In this case, that means that `a._base is b`)
1203
+ #
1204
+ # We need to ensure that the aliasing relationship between a and b is preserved.
1205
+ # We do that detecting the specific situation above (mutate an input that aliases another input),
1206
+ # and when we do that, we create a synthetic base argument. Then inside of the traced forward,
1207
+ # we regenerate a and b off of that base.
1208
+ # The complete example of the transformed function looks like this:
1209
+ #
1210
+ # // The traced forward takes in a synthetic base, and regenerates the aliased inputs as views
1211
+ # // We could consider getting view-replay support here to minimize as_strided_scatter ops in the graph
1212
+ # def traced_forward(base):
1213
+ # a = base.as_strided(...)
1214
+ # b = base.as_strided(...)
1215
+ # a_updated = a.mul(2)
1216
+ # base_updated = torch.as_strided_scatter(base, a_updated, ...)
1217
+ # b_updated = base_updated.as_strided(...)
1218
+ # out = a_updated + b_updated
1219
+ # return a_updated, out
1220
+ #
1221
+ # def compiled_fn(a, b):
1222
+ # // we detect that a is the "differentiable base" here
1223
+ # base = a
1224
+ # // In other situations, we might do either:
1225
+ # // (1) a and b are both views off of some larger differentiable base
1226
+ # // assert a._base is b._base and a._base is not None
1227
+ # // base = a._base
1228
+ # // (2) a and b both don't require gradients. Create a base from the storage
1229
+ # // assert a._base is None and b._base is None
1230
+ # // base = torch.Tensor(a.storage())
1231
+ # a_updated, out = traced_forward(base)
1232
+ # a.copy_(a_updated)
1233
+ # return out
1234
+ #
1235
+ # This function:
1236
+ # (1) Merges input views into a synthetic base argument, when any of those input views are mutated
1237
+ # (2) Returns metadata telling the autograd.Function how to modify their arguments properly,
1238
+ # to respect the new calling convention.
1239
+ #
1240
+ # The calling convention is as follows.
1241
+ # Any inputs that were originally views of one another get yanked, and replaced with a synthetic base.
1242
+ # The argument list ordering goes [base1, ..., baseN], [arg1, ..., argN],
1243
+ # Where the ordering of the bases is determined from the ordering of the original view args.
1244
+ # baseA will come before baseB if the earliest original argument coming from baseA
1245
+ # showed up earlier in the argument list than the earliest original argument coming from baseB.
1246
+ #
1247
+ # Example, given some tensors a, b, c, d
1248
+ # call site:
1249
+ # f(a, c.view(-1), b.view(-1), b, c, d)
1250
+ # Modified argument list:
1251
+ # c_base comes first because the first c view came earlier in arg list than the first b view
1252
+ # a and d still show up in the modified arg list, but b and c don't- they're regenerated from their bases
1253
+ # b_base = torch.Tensor(b.storage())
1254
+ # c_base = torch.Tensor(c.storage())
1255
+ # f(c_base, b_base, a, d)
1256
+ def merge_view_inputs(
1257
+ aot_config: AOTConfig,
1258
+ fwd_inputs: List[Any],
1259
+ mutated_input_info: List[InputAliasInfo],
1260
+ *,
1261
+ # The autograd case currently has more restrictions than the inference case.
1262
+ is_inference: bool,
1263
+ ) -> Tuple[List[Any], Optional[List[Union[int, Tuple[int, torch.Tensor]]]]]:
1264
+ def _are_differentiable_views(view1, view2):
1265
+ if view1 is view2:
1266
+ return True
1267
+ if view1._base is None and view2._base is None:
1268
+ return False
1269
+ if view1._base is view2._base or view1._base is view2 or view1 is view2._base:
1270
+ return True
1271
+ return False
1272
+
1273
+ def _same_dtype_views(view1, view2):
1274
+ if view1.dtype != view2.dtype:
1275
+ return False
1276
+ if view1._base is not None and view1.dtype != view1._base.dtype:
1277
+ return False
1278
+ if view2._base is not None and view2.dtype != view2._base.dtype:
1279
+ return False
1280
+ return True
1281
+
1282
+ assert len(fwd_inputs) == len(mutated_input_info)
1283
+ if not [info for info in mutated_input_info if info.mutates_data]:
1284
+ # Return early when there are no mutations.
1285
+ return fwd_inputs, None
1286
+
1287
+ storage_ref_to_idx: Dict[StorageWeakRef, List[int]] = collections.defaultdict(list)
1288
+ base_args = []
1289
+ other_args = []
1290
+ for i, inpt in enumerate(fwd_inputs):
1291
+ if isinstance(inpt, Tensor):
1292
+ storage_ref = StorageWeakRef(inpt.untyped_storage())
1293
+ storage_ref_to_idx[storage_ref].append(i)
1294
+ else:
1295
+ other_args.append(inpt)
1296
+ # Note [Synthetic Base Info Metadata]
1297
+ # This list contains metadata that tells you what the i'th argument in the inner calling convention should be.
1298
+ # It's either:
1299
+ # - another int (corresponding to the index in the argument list of the element from the outer calling convention)
1300
+ # - idx, view_tensor, where we can generate the new output with view_tensor._view_func(old_args[idx])
1301
+ # idx corresponds to which synthetic base from the outer calling context to view
1302
+ inner_calling_convention_meta: Dict[int, Union[int, Tuple[int, torch.Tensor]]] = {}
1303
+ for aliased_input_indices in storage_ref_to_idx.values():
1304
+ if len(aliased_input_indices) <= 1 or not any(
1305
+ # We only care about mutations that affect all aliases,
1306
+ # so metadata mutations on an input doesn't require us to do synthetic base handling.
1307
+ mutated_input_info[inpt_idx].mutates_data
1308
+ for inpt_idx in aliased_input_indices
1309
+ ):
1310
+ other_args.extend(
1311
+ fwd_inputs[curr_idx] for curr_idx in aliased_input_indices
1312
+ )
1313
+ continue
1314
+
1315
+ # Here, we attempt to do a more complicated check to detect false aliasing
1316
+ # (e.g. if all the tensors have the same storage, but don't actually overlap)
1317
+ # In theory, we could have a large group of tensors that all share storages, where only *some* of them
1318
+ # have overlapping memory.
1319
+ # I don't bother with that case for now: here, we only bail out earlier if we detect that **every** pair
1320
+ # of tensors in the current group that shares a storage is non-overlapping.
1321
+ aliased_input_indices_no_false_sharing = compute_overlapping_inputs(
1322
+ aot_config, fwd_inputs, aliased_input_indices
1323
+ )
1324
+ if len(aliased_input_indices_no_false_sharing) <= 1:
1325
+ other_args.extend(
1326
+ fwd_inputs[curr_idx] for curr_idx in aliased_input_indices
1327
+ )
1328
+ continue
1329
+
1330
+ # We detected an input that was mutated, AND aliases with another input.
1331
+ # we need to replace this set of aliased inputs with a single synthetic base.
1332
+ # For now, I'm banning a bunch of cases. We expect dynamo to properly detect these cases
1333
+ # and error out. We can fix them later.
1334
+ # These checks are transitive, so we don't need to check every pair.
1335
+ for idx1, idx2 in zip(
1336
+ aliased_input_indices, aliased_input_indices[1:], strict=False
1337
+ ):
1338
+ view1 = fwd_inputs[idx1]
1339
+ view2 = fwd_inputs[idx2]
1340
+ # The "inputs that are aliased but have different differentiable bases" case
1341
+ # is more complicated and hopefully pretty rare. Not currently handled.
1342
+ if not is_inference:
1343
+ assert _are_differentiable_views(
1344
+ view1, view2
1345
+ ), "aot_autograd() does not yet handle non-differentiable view input mutations."
1346
+ # Regenerating views when reinterpreting complex / real tensors seems non-trivial,
1347
+ # not handling for now
1348
+ assert _same_dtype_views(
1349
+ view1, view2
1350
+ ), "aot_autograd() does not yet handle input mutations on views with different dtypes."
1351
+ non_none_bases = [
1352
+ fwd_inputs[i]._base
1353
+ for i in aliased_input_indices
1354
+ if fwd_inputs[i]._base is not None
1355
+ ]
1356
+ aliases_with_none_bases = [
1357
+ fwd_inputs[i] for i in aliased_input_indices if fwd_inputs[i]._base is None
1358
+ ]
1359
+ if len(non_none_bases) == 0:
1360
+ # Case where none of the aliases have a ._base
1361
+ # we generate a synthetic base without gradients, and generate views off of it
1362
+ # We hit this case when we have input tensors to the graph that share a storage,
1363
+ # but do not have a ._base field.
1364
+ # Wondering when we hit this case?
1365
+ # The _base field simply says that autograd knows about the aliasing relationship,
1366
+ # but sometimes we create tensors which are aliased out of the same storage but guaranteed
1367
+ # to be disjoint. In these cases, we will skip setting up the _base relationship
1368
+ # for performance reasons (because the fact that the tensors share the same storage
1369
+ # is unobservable unless you (1) do naughty things with resize_/as_strided
1370
+ # or (2) look at the storage--as we are doing here.)
1371
+ # One particular example of this is optimizer steps on the LSTM module:
1372
+ # LSTM parameters are packed into a contiguous storage for efficiency reasons when
1373
+ # calling cuDNN kernels, so when these parameters get passed to the optimizer we will
1374
+ # find they share the same storage, but do not have _base set since they are all disjoint.
1375
+ #
1376
+ # NOTE: There is one case where this is unsafe:
1377
+ # torch.Tensor(storage) will ALWAYS create a 1D tensor, which is not necessarily
1378
+ # the same shape as the "actual" base that the tensor came from.
1379
+ # For the most part this is fine, because we always use as_strided()
1380
+ # to generate the original aliased inputs again.
1381
+ # If we were to use view-replay though, this could cause the aliased views
1382
+ # to have incorrect sizes.
1383
+ example_idx = aliased_input_indices[0]
1384
+ example_alias = fwd_inputs[example_idx]
1385
+ # Note that this function is re-used at both trace time and runtime.
1386
+ # At trace time, we're under a FakeMode so synthetic_base becomes a FakeTensor.
1387
+ synthetic_base = torch.empty(
1388
+ (0,), dtype=example_alias.dtype, device=example_alias.device
1389
+ )
1390
+ # We don't actually have a convenient way of going from storage -> tensor,
1391
+ # So using set_() here (we suffer some minor overhead, but this case is rare).
1392
+ synthetic_base.set_(example_alias.untyped_storage())
1393
+ else:
1394
+ # Case where all of the aliases require gradients, and have the same _base.
1395
+ synthetic_base = non_none_bases[0]
1396
+ for other_base in non_none_bases[1:]:
1397
+ assert (
1398
+ other_base is synthetic_base
1399
+ ), "aot_autograd() does not yet handle non-differentiable view input mutations."
1400
+ for alias in aliases_with_none_bases:
1401
+ assert (
1402
+ alias is synthetic_base
1403
+ ), "aot_autograd() does not yet handle non-differentiable view input mutations."
1404
+ base_args.append(synthetic_base)
1405
+ for curr_view_idx in aliased_input_indices:
1406
+ curr_view = fwd_inputs[curr_view_idx]
1407
+ base_idx = len(base_args) - 1
1408
+ # We store just enough info here so that we can regenerate the view later.
1409
+ # Regeneration: curr_view._view_func(args[base_idx])
1410
+ inner_calling_convention_meta[curr_view_idx] = (base_idx, curr_view)
1411
+ if len(base_args) == 0:
1412
+ assert len(other_args) == len(fwd_inputs)
1413
+ # If no synthetic bases are necessary, just return the original inputs.
1414
+ return fwd_inputs, None
1415
+ else:
1416
+ from torch.fx.experimental.symbolic_shapes import SymIntEqByExpr
1417
+
1418
+ def make_hashable(arg):
1419
+ if isinstance(arg, torch.SymInt):
1420
+ # Since only nested SymInt objects can be hashed, we wrap them with
1421
+ # SymIntEqByExpr, which is a hashable wrapper of SymInts.
1422
+ return SymIntEqByExpr(arg)
1423
+ return arg
1424
+
1425
+ # Otherwise, return:
1426
+ # (1) The new args according to the updated calling convention: (synthetic_bases, other_args)
1427
+ # (2) Metadata telling functionalization how to generate the inner argument list given the outer calling convention.
1428
+ # We post-process it into a list, where meta[i] tells you info about the i'th argument in the inner calling convention.
1429
+ args_to_functionalization = base_args + other_args
1430
+ arg_to_old_idx_map = {
1431
+ make_hashable(arg): i for (i, arg) in enumerate(fwd_inputs)
1432
+ }
1433
+ for i, other_arg in enumerate(other_args):
1434
+ new_idx = len(base_args) + i
1435
+ old_idx = arg_to_old_idx_map[make_hashable(other_arg)]
1436
+ inner_calling_convention_meta[old_idx] = new_idx
1437
+ # post process into a list
1438
+ post_processed_calling_convention_meta: List[
1439
+ Union[int, Tuple[int, torch.Tensor]]
1440
+ ] = [-1 for _ in range(len(inner_calling_convention_meta))]
1441
+ for k, v in inner_calling_convention_meta.items():
1442
+ post_processed_calling_convention_meta[k] = v
1443
+ # Quick assert: every argument in the inner calling convention should be accounted for.
1444
+ for x in post_processed_calling_convention_meta:
1445
+ assert x != -1
1446
+ return args_to_functionalization, post_processed_calling_convention_meta
1447
+
1448
+
1449
+ @dataclass
1450
+ class AutogradLazyBackwardCompileInfo:
1451
+ bw_module: Callable
1452
+ placeholder_list: List[Any]
1453
+ saved_context: Optional[TracingContext]
1454
+ saved_compile_context: Optional[CompileContext]
1455
+
1456
+
1457
+ # This is wrapped in a class just for namespacing purposes
1458
+ # No need to make it into an actual CompilerWrapper because it doesn't fit the abstract as cleanly
1459
+ class AOTDispatchAutograd:
1460
+ @staticmethod
1461
+ def process_runtime_tangent(x, meta: Union[PlainTensorMeta, SubclassCreationMeta]):
1462
+ if not isinstance(x, torch.Tensor):
1463
+ return x, [x]
1464
+
1465
+ if isinstance(x, FakeTensor):
1466
+ if not x.is_contiguous(memory_format=meta.memory_format):
1467
+ x = x.contiguous(memory_format=meta.memory_format)
1468
+ return x, [x]
1469
+
1470
+ expected_type: Optional[type] = torch.Tensor
1471
+ expected_meta = None
1472
+ if isinstance(meta, SubclassCreationMeta):
1473
+ expected_type = meta.original_subclass_type
1474
+ expected_meta = meta.meta
1475
+
1476
+ runtime_type = type(x)
1477
+ runtime_meta = None
1478
+ runtime_subclass_keys: Sequence[str] = []
1479
+
1480
+ if is_traceable_wrapper_subclass(x):
1481
+ runtime_subclass_keys, runtime_meta = x.__tensor_flatten__()
1482
+
1483
+ def maybe_coerce(x):
1484
+ same_type: bool = expected_type == runtime_type
1485
+ same_meta: bool = expected_meta == runtime_meta
1486
+
1487
+ if same_type and same_meta:
1488
+ return x
1489
+
1490
+ if not hasattr(x, "__coerce_same_metadata_as_tangent__"):
1491
+ return None
1492
+
1493
+ if same_type:
1494
+ # Backward Compatibility, as some Subclass impls can have original 1-arg function.
1495
+ return x.__coerce_same_metadata_as_tangent__(expected_meta)
1496
+
1497
+ return x.__coerce_same_metadata_as_tangent__(expected_meta, expected_type)
1498
+
1499
+ # Coerce to expected type and metadata
1500
+ orig_x = x
1501
+ x = maybe_coerce(x)
1502
+ if x is None:
1503
+ raise RuntimeError(
1504
+ f"""
1505
+ During the backward, we encountered a tensor subclass where we guessed its
1506
+ metadata incorrectly.
1507
+
1508
+ Expected metadata: {str(expected_meta)}, expected type: {str(expected_type)}
1509
+
1510
+ Runtime metadata: {str(runtime_meta)}, runtime type: {str(runtime_type)}
1511
+
1512
+ shape: {str(orig_x.shape)}
1513
+ To fix this, your tensor subclass must implement the dunder method __force_to_same_metadata__.
1514
+ """
1515
+ )
1516
+
1517
+ # Coerce to expected memory format
1518
+ if not x.is_contiguous(memory_format=meta.memory_format):
1519
+ x = x.contiguous(memory_format=meta.memory_format)
1520
+
1521
+ if not is_traceable_wrapper_subclass(x):
1522
+ return x, [x]
1523
+
1524
+ assert isinstance(meta, SubclassCreationMeta)
1525
+ if orig_x is not x:
1526
+ runtime_subclass_keys = x.__tensor_flatten__()[0]
1527
+
1528
+ assert len(meta.attrs) == len(runtime_subclass_keys)
1529
+ leaves = []
1530
+ for i, (attr, attr_meta) in enumerate(meta.attrs.items()):
1531
+ elem = getattr(x, attr)
1532
+ new_elem, elem_leaves = AOTDispatchAutograd.process_runtime_tangent(
1533
+ elem, attr_meta
1534
+ )
1535
+ if new_elem is not elem:
1536
+ setattr(x, attr, new_elem)
1537
+ leaves.extend(elem_leaves)
1538
+
1539
+ return x, leaves
1540
+
1541
+ @staticmethod
1542
+ def post_compile(
1543
+ compiled_fw_func, # fw_module after compilation + wrappers
1544
+ compiled_bw_func, # bw_module after compilation + wrappers
1545
+ maybe_subclass_meta: Optional[SubclassMeta],
1546
+ num_symints_saved_for_bw_: int,
1547
+ backward_state_indices: List[int],
1548
+ disable_amp: bool,
1549
+ indices_of_inps_to_detach: List[int],
1550
+ lazy_backward_info: Optional[AutogradLazyBackwardCompileInfo],
1551
+ aot_config: AOTConfig,
1552
+ *,
1553
+ fw_metadata: ViewAndMutationMeta, # runtime metadata
1554
+ try_save_cache_entry: Optional[Callable], # Save cache entry after compilation
1555
+ ):
1556
+ class CompiledFunction(torch.autograd.Function):
1557
+ compiled_fw = compiled_fw_func
1558
+ compiled_bw = compiled_bw_func
1559
+ metadata: ViewAndMutationMeta = fw_metadata # type: ignore[assignment]
1560
+ maybe_subclass_metadata: Optional[SubclassMeta] = maybe_subclass_meta
1561
+ num_symints_saved_for_bw = num_symints_saved_for_bw_
1562
+ _compiled_autograd_should_lift = False
1563
+ _aot_id = aot_config.aot_id
1564
+ _lazy_backward_info = lazy_backward_info
1565
+
1566
+ @staticmethod
1567
+ def _compiled_autograd_key(ctx):
1568
+ return (ctx._autograd_function_id, *ctx.symints)
1569
+
1570
+ @staticmethod
1571
+ def forward(ctx, *deduped_flat_tensor_args):
1572
+ args = deduped_flat_tensor_args
1573
+ if backward_state_indices:
1574
+ bw_state = args[backward_state_indices[0]]
1575
+ assert isinstance(bw_state, BackwardState)
1576
+ ctx._compiled_autograd_backward_state = bw_state
1577
+
1578
+ # There is a pretty complicated calling convention around what the compiled fw returns.
1579
+ # The full list of outputs and their relative order is:
1580
+ # (*tokens, *mutated_inputs, *fw_outs, *fw_intermediate_bases, *saved_tensors, *saved_symints)
1581
+ # - Note that in the synthetic bases case, mutated_inputs will correspond to an updated version
1582
+ # of the original view, and not the synthetic base
1583
+ # - Note that donated buffer logic requires (*saved_tensors, *saved_symints) showing up last
1584
+ # in the fw output order.
1585
+ fw_outs = call_func_at_runtime_with_args(
1586
+ CompiledFunction.compiled_fw,
1587
+ args,
1588
+ disable_amp=disable_amp,
1589
+ )
1590
+
1591
+ num_outputs = CompiledFunction.metadata.num_outputs
1592
+ num_outputs_aliased = CompiledFunction.metadata.num_outputs_aliased
1593
+ num_mutated_runtime_inps = (
1594
+ CompiledFunction.metadata.num_mutated_inp_runtime_indices
1595
+ )
1596
+ num_forward_returns = CompiledFunction.metadata.num_forward_returns
1597
+
1598
+ # Partitioners must put symint arguments at the end separate from tensor arguments
1599
+ tensors_saved_for_backwards = fw_outs[
1600
+ CompiledFunction.metadata.tensors_saved_for_backwards_slice
1601
+ ]
1602
+ assert all(
1603
+ isinstance(x, torch.Tensor) for x in tensors_saved_for_backwards
1604
+ )
1605
+ # See Note [Detaching saved tensors in AOTAutograd]
1606
+ ctx.save_for_backward(
1607
+ *(
1608
+ x.detach() if x._is_view() else x
1609
+ for x in tensors_saved_for_backwards
1610
+ )
1611
+ )
1612
+ symint_outs = fw_outs[
1613
+ CompiledFunction.metadata.symints_saved_for_backwards_slice
1614
+ ]
1615
+ assert all(
1616
+ isinstance(x, (int, float, torch.SymInt, torch.SymFloat))
1617
+ for x in symint_outs
1618
+ ), str([type(x) for x in symint_outs])
1619
+ ctx.symints = symint_outs
1620
+
1621
+ raw_returns = fw_outs[0:num_forward_returns]
1622
+
1623
+ # Wrap all autograd.Function.forward() outputs that are aliases
1624
+ # so that autograd.Function doesn't treat them as tensors
1625
+ if num_mutated_runtime_inps > 0:
1626
+ for i, idx in enumerate(
1627
+ CompiledFunction.metadata.mutated_inp_runtime_indices
1628
+ ):
1629
+ # We could make this faster by only looping over inputs with metadata-only mutations
1630
+ # (instead of looping over inputs with either data or metadata mutations), but there shouldn't be many.
1631
+ info = CompiledFunction.metadata.input_info[idx]
1632
+ if info.mutates_metadata and not info.mutates_data:
1633
+ raw_return_idx = i
1634
+ raw_returns[raw_return_idx] = TensorAlias(
1635
+ raw_returns[raw_return_idx]
1636
+ )
1637
+
1638
+ if config.debug_assert:
1639
+ user_mutated_inputs_raw = raw_returns[
1640
+ 0:num_mutated_runtime_inps
1641
+ ]
1642
+ mut_inp_infos = [
1643
+ x
1644
+ for x in CompiledFunction.metadata.input_info
1645
+ if x.mutates_data or x.mutates_metadata
1646
+ ]
1647
+ assert len(user_mutated_inputs_raw) == len(mut_inp_infos)
1648
+
1649
+ if CompiledFunction.metadata.num_unsafe_view_outputs > 0:
1650
+ for idx in CompiledFunction.metadata.unsafe_view_out_indices:
1651
+ raw_return_idx = num_mutated_runtime_inps + idx
1652
+ o = raw_returns[raw_return_idx]
1653
+ raw_returns[raw_return_idx] = torch.ops.aten._unsafe_view(
1654
+ o, o.shape
1655
+ )
1656
+
1657
+ if num_outputs_aliased > 0:
1658
+ for idx in CompiledFunction.metadata.aliased_out_indices:
1659
+ raw_return_idx = num_mutated_runtime_inps + idx
1660
+ raw_returns[raw_return_idx] = TensorAlias(
1661
+ raw_returns[raw_return_idx]
1662
+ )
1663
+
1664
+ if config.debug_assert:
1665
+ intermediates_raw = raw_returns[
1666
+ num_mutated_runtime_inps + num_outputs :
1667
+ ]
1668
+ assert not any(
1669
+ isinstance(x, TensorAlias) for x in intermediates_raw
1670
+ )
1671
+
1672
+ # invariant: intermediate bases always require gradients, so we don't have to
1673
+ # consider marking them as non-differentiable.
1674
+ raw_returns_not_including_intermediate_bases = raw_returns[
1675
+ : num_mutated_runtime_inps + num_outputs
1676
+ ]
1677
+ raw_returns_meta = [
1678
+ x
1679
+ for x in CompiledFunction.metadata.input_info
1680
+ if x.mutation_type == MutationType.MUTATED_OUT_GRAPH
1681
+ ] + CompiledFunction.metadata.output_info
1682
+
1683
+ fw_outs_not_requiring_grad = [
1684
+ x
1685
+ for (i, x) in enumerate(
1686
+ raw_returns_not_including_intermediate_bases
1687
+ )
1688
+ if isinstance(x, torch.Tensor)
1689
+ and not raw_returns_meta[i].requires_grad
1690
+ ]
1691
+ ctx.mark_non_differentiable(*fw_outs_not_requiring_grad)
1692
+ ctx._materialize_non_diff_grads = False
1693
+ return tuple(raw_returns)
1694
+
1695
+ @staticmethod
1696
+ def backward(ctx, *flat_args):
1697
+ all_args = CompiledFunction._backward_prologue(ctx, *flat_args)
1698
+
1699
+ def impl_fn(double_ctx=None):
1700
+ out = CompiledFunction._backward_impl(ctx, all_args)
1701
+ return CompiledFunction._backward_epilogue(ctx, out)
1702
+
1703
+ needs_grad = torch.is_grad_enabled() and any(
1704
+ t.requires_grad for t in all_args if isinstance(t, torch.Tensor)
1705
+ )
1706
+ if needs_grad:
1707
+ # double backward
1708
+ return CompiledFunction._double_backward(ctx, impl_fn, all_args)
1709
+ else:
1710
+ return impl_fn()
1711
+
1712
+ @staticmethod
1713
+ def _double_backward(ctx, impl_fn, all_args):
1714
+ # Ensure that the graph is connected, and error if double backward is performed.
1715
+ # See comment for why once_differentiable is not sufficient:
1716
+ # https://github.com/pytorch/pytorch/pull/92348/files#r1072962107
1717
+ class CompiledFunctionBackward(torch.autograd.Function):
1718
+ # CompiledFunctionBackward is not yet supported in dynamo skipfiles
1719
+ _compiled_autograd_should_lift = False
1720
+ _aot_id = aot_config.aot_id
1721
+
1722
+ @staticmethod
1723
+ def forward(double_ctx, *unused_args):
1724
+ return impl_fn(double_ctx)
1725
+
1726
+ @staticmethod
1727
+ def backward(double_ctx, *args):
1728
+ raise RuntimeError(
1729
+ "torch.compile with aot_autograd does not currently support double backward"
1730
+ )
1731
+
1732
+ CompiledFunctionBackward._compiled_autograd_key = ( # type: ignore[method-assign]
1733
+ CompiledFunction._compiled_autograd_key
1734
+ )
1735
+
1736
+ return CompiledFunctionBackward.apply(*all_args)
1737
+
1738
+ @staticmethod
1739
+ def _raise_if_functorch_active():
1740
+ # not ideal but prevent the user from seeing a nasty traceback - See #138422
1741
+ stack = torch._C._functorch.peek_interpreter_stack()
1742
+ torch._check(
1743
+ stack is None,
1744
+ lambda: (
1745
+ "It looks like you're trying to call a compiled backward function within vmap/grad/vjp, "
1746
+ "which isn't supported. Try wrapping vmap inside torch.compile, or skip compiling the "
1747
+ "backward function."
1748
+ ),
1749
+ )
1750
+
1751
+ @staticmethod
1752
+ def _backward_prologue(ctx, *flat_args):
1753
+ # Calling convention: we expect a grad_out passed to the backward:
1754
+ # - for every output of the fw that does *not* alias an input or graph intermediate
1755
+ # - for every updated_input generated by the fw that does *not* alias an input (aka only data-mutations)
1756
+ # - for every graph intermediate that we need to use to generate an output later.
1757
+ # The other outputs in the autograd.Function.forward that do *not* show up in the backward include:
1758
+ # - outputs that alias inputs or graph intermediates
1759
+ # - updated inputs due to metadata-only mutations.
1760
+ # We need to return them in the forward, but ensure that they all do not get gradients in the backward,
1761
+ # and we filter them out here before passing the remaining grad_outputs into the compiled backward.
1762
+ CompiledFunction._raise_if_functorch_active()
1763
+
1764
+ num_intermediate_bases = (
1765
+ CompiledFunction.metadata.num_intermediate_bases
1766
+ )
1767
+ num_mutated_runtime_inps = (
1768
+ CompiledFunction.metadata.num_mutated_inp_runtime_indices
1769
+ )
1770
+ expected_grad_outs = (
1771
+ CompiledFunction.metadata.num_outputs
1772
+ + num_mutated_runtime_inps
1773
+ + num_intermediate_bases
1774
+ )
1775
+ deterministic = CompiledFunction.metadata.deterministic
1776
+ global_deterministic = torch.are_deterministic_algorithms_enabled()
1777
+ if deterministic is not None:
1778
+ torch._check(
1779
+ not (not deterministic and global_deterministic),
1780
+ lambda: (
1781
+ "This compiled backward function is being run with "
1782
+ "torch.use_deterministic_algorithms(True), "
1783
+ "but it was previously generated during the forward function while "
1784
+ "torch.use_deterministic_algorithms(False) was set."
1785
+ ),
1786
+ )
1787
+
1788
+ assert len(flat_args) == expected_grad_outs
1789
+ out_info = CompiledFunction.metadata.output_info
1790
+
1791
+ inp_tangents, out_tangents, intermediate_base_tangents = (
1792
+ flat_args[:num_mutated_runtime_inps],
1793
+ flat_args[
1794
+ num_mutated_runtime_inps : num_mutated_runtime_inps
1795
+ + CompiledFunction.metadata.num_outputs
1796
+ ],
1797
+ flat_args[
1798
+ num_mutated_runtime_inps
1799
+ + CompiledFunction.metadata.num_outputs :
1800
+ ],
1801
+ )
1802
+ # input_info contains info on *every* input,
1803
+ # But in the backward(), we are only given grad outputs for every mutated input
1804
+ # We then need to filter out the grad outputs that correspond to metadata-only mutations or don't require grad
1805
+ input_info = CompiledFunction.metadata.input_info
1806
+ inp_tangents_filtered = [
1807
+ x
1808
+ for x, info_idx in zip(
1809
+ inp_tangents,
1810
+ CompiledFunction.metadata.mutated_inp_runtime_indices,
1811
+ )
1812
+ if input_info[info_idx].mutates_data
1813
+ and input_info[info_idx].requires_grad
1814
+ ]
1815
+ # We also need to filter out grad outputs that correspond to outputs aliasing inputs/intermediates
1816
+ out_tangents_filtered = [
1817
+ x
1818
+ for x, info in zip(out_tangents, out_info)
1819
+ if info.output_type
1820
+ in [
1821
+ OutputType.non_alias,
1822
+ OutputType.unsafe_view_alias,
1823
+ OutputType.custom_function_view,
1824
+ ]
1825
+ and issubclass(info.raw_type, torch.Tensor)
1826
+ and info.requires_grad
1827
+ ]
1828
+ # intermediate bases always require gradients, and always participate in the backward graph.
1829
+ flat_bw_args_with_grads = [
1830
+ *inp_tangents_filtered,
1831
+ *out_tangents_filtered,
1832
+ *intermediate_base_tangents,
1833
+ ]
1834
+ num_flat_bw_args_with_grads = len(flat_bw_args_with_grads)
1835
+
1836
+ # sanity asserts
1837
+ # metadata_only_inps = [
1838
+ # x for x, info_idx in zip(inp_tangents, mutated_inp_indices)
1839
+ # if not input_info[info_idx].mutates_data
1840
+ # ]
1841
+ # aliased_outputs = [
1842
+ # x for x, info in zip(out_tangents, out_info) if info.output_type != OutputType.non_alias]
1843
+ # assert all(x is None for x in metadata_only_inps)
1844
+ # assert all(x is None for x in aliased_outputs)
1845
+ # TODO: replace this with FunctionalizedRngRuntimeWrapper
1846
+ rng_args = []
1847
+ if CompiledFunction.metadata.is_rng_op_functionalized:
1848
+ # Add the seed and offset to args
1849
+ rng_args = CUDARngStateHelper.get_torch_state_as_tuple()
1850
+
1851
+ bw_tokens = [None] * CompiledFunction.metadata.num_backward_tokens
1852
+
1853
+ # - note: donated buffer logic requires (*ctx.symints, *ctx.saved_tensors) showing up first
1854
+ # in the bw output order.
1855
+
1856
+ # Every dereference of ctx.saved_tensors incurs saved_tensors_hooks calls
1857
+ # There are tests that count these calls, saving to var.
1858
+ ctx_saved_tensors = ctx.saved_tensors
1859
+ num_ctx_saved_tensors = len(ctx_saved_tensors)
1860
+ all_args = [
1861
+ *ctx.symints,
1862
+ *ctx_saved_tensors,
1863
+ *flat_bw_args_with_grads,
1864
+ *bw_tokens,
1865
+ *rng_args,
1866
+ ]
1867
+ del ctx_saved_tensors
1868
+
1869
+ # Note: [AOTAutograd Backward Guards]
1870
+ # During AOTDispatch, we eagerly create and trace out a joint fw-bw graph.
1871
+ # Doing so requires us to "guess" about some of the metadata of our grad_outputs.
1872
+ #
1873
+ # In particular: if an output to the forward is a plain tensor or a subclass,
1874
+ # its corresponding grad_output in the backward **may or may not** be
1875
+ # a plain tensor or a subclass. The main cases are:
1876
+ # (1) If an output is a plain tensor, its grad_out will also be a plain tensor,
1877
+ # *unless* the output is used in some subclass compute later in the forward graph,
1878
+ # which will cause its grad_output to become a subclass
1879
+ # (2) If an output is a subclass, its grad_out will also be a subclass,
1880
+ # *unless* the output of the forward did not actually participate in the gradient computation,
1881
+ # in which case autograd will insert a plain tensor of zeros for the grad_output.
1882
+ # We could avoid this case with `torch.autograd.Function.set_materialize_grads`,
1883
+ # although this is not turned on today in AOTAutgrad and would require more work.
1884
+ #
1885
+ # Today, we make a guess on subclass-ness based on the above examples,
1886
+ # and hard-error in the backward if we guessed wrong.
1887
+ #
1888
+ # In the future, we should add backward guards that would allow us to
1889
+ # properly handle this case instead of erroring: we would need to retrace the backward graph,
1890
+ # since we might produce an entirely different trace if our grad_outputs are subclass or not.
1891
+ del flat_bw_args_with_grads
1892
+
1893
+ tangents_start_idx = (
1894
+ len(all_args)
1895
+ - num_flat_bw_args_with_grads
1896
+ - len(rng_args)
1897
+ - len(bw_tokens)
1898
+ )
1899
+ assert tangents_start_idx == len(ctx.symints) + num_ctx_saved_tensors
1900
+ tangents_end_idx = len(all_args) - len(rng_args) - len(bw_tokens)
1901
+
1902
+ # TODO: figure out how to refactor the backward properly
1903
+ # so I can use aot_dispatch_subclass_wrapper() here.
1904
+ if CompiledFunction.maybe_subclass_metadata is not None:
1905
+ tangents = all_args[tangents_start_idx:tangents_end_idx]
1906
+
1907
+ if len(tangents) != len(
1908
+ CompiledFunction.metadata.subclass_tangent_meta
1909
+ ):
1910
+ raise RuntimeError(
1911
+ "The grad inputs should be same number as forward output tangents"
1912
+ )
1913
+
1914
+ flat_processed_tangents = list(
1915
+ itertools.chain.from_iterable(
1916
+ (
1917
+ AOTDispatchAutograd.process_runtime_tangent(
1918
+ t,
1919
+ m,
1920
+ )[1]
1921
+ )
1922
+ for t, m in zip(
1923
+ tangents,
1924
+ CompiledFunction.metadata.subclass_tangent_meta,
1925
+ )
1926
+ )
1927
+ )
1928
+
1929
+ all_args = (
1930
+ runtime_unwrap_tensor_subclasses(
1931
+ all_args[:tangents_start_idx],
1932
+ # SymInts that are inputs to the backward graph are
1933
+ # already included in the "all_args" list.
1934
+ # Any symints coming from tensor subclasses should always
1935
+ # come from primals, and so they will show up as extra
1936
+ # arguments to the forward graph, and they will be saved
1937
+ # as activation in the backward graph.
1938
+ append_symints=False,
1939
+ )
1940
+ + flat_processed_tangents
1941
+ + runtime_unwrap_tensor_subclasses(
1942
+ all_args[tangents_end_idx:],
1943
+ append_symints=False,
1944
+ )
1945
+ )
1946
+ else:
1947
+ all_args = [
1948
+ (
1949
+ AOTDispatchAutograd.process_runtime_tangent(
1950
+ t,
1951
+ CompiledFunction.metadata.subclass_tangent_meta[
1952
+ i - tangents_start_idx
1953
+ ],
1954
+ )[0]
1955
+ if (tangents_start_idx <= i < tangents_end_idx)
1956
+ else t
1957
+ )
1958
+ for i, t in enumerate(all_args)
1959
+ ]
1960
+
1961
+ # Backward with forward inputs mutations is not supported in double backward.
1962
+ if (
1963
+ torch.is_grad_enabled()
1964
+ and CompiledFunction.metadata.indices_of_inputs_that_requires_grad_with_mutations_in_bw
1965
+ ):
1966
+ raise RuntimeError(
1967
+ "aot_autograd does not support input mutations with requires_grad in backward for create_graph=True"
1968
+ )
1969
+
1970
+ return all_args
1971
+
1972
+ @staticmethod
1973
+ def _backward_impl(ctx, all_args):
1974
+ if ctx._is_compiled_autograd_tracing():
1975
+ if lazy_backward_info is None:
1976
+ raise RuntimeError(
1977
+ """This compiled backward function was saved by AOTAutogradCache, which does not support
1978
+ compiled autograd. Please turn off AOTAutogradCache using `TORCHINDUCTOR_AUTOGRAD_CACHE=0`."""
1979
+ )
1980
+ bw_module = lazy_backward_info.bw_module
1981
+ # For compiled autograd, run raw FX graph so that it can be inlined into the larger graph
1982
+ symints = ctx._get_compiled_autograd_symints()
1983
+ assert len(symints) == len(ctx.symints)
1984
+ all_args[: len(symints)] = symints
1985
+ if backward_state_indices:
1986
+ assert ctx._compiled_autograd_backward_state.proxy is not None
1987
+ all_args.append(ctx._compiled_autograd_backward_state)
1988
+ context = torch._C._DisableAutocast if disable_amp else nullcontext
1989
+ with context():
1990
+ return normalize_as_list(bw_module(*all_args))
1991
+
1992
+ assert (
1993
+ not backward_state_indices
1994
+ ), "BackwardState requires CompiledAutograd"
1995
+ ctx.maybe_clear_saved_tensors()
1996
+
1997
+ saved_tensors_use_once = (
1998
+ not torch._C._autograd._get_current_graph_task_keep_graph()
1999
+ )
2000
+
2001
+ if CompiledFunction.compiled_bw is None:
2002
+ assert lazy_backward_info is not None
2003
+
2004
+ if not saved_tensors_use_once:
2005
+ fw_metadata.bw_donated_idxs = []
2006
+ # Update bw_donated_idxs if using lazy_backward_info from `aot_dispatch_autograd`
2007
+ if (
2008
+ hasattr(lazy_backward_info, "saved_context")
2009
+ and hasattr(lazy_backward_info.saved_context, "fw_metadata")
2010
+ and hasattr(
2011
+ lazy_backward_info.saved_context.fw_metadata, # type: ignore[union-attr]
2012
+ "bw_donated_idxs",
2013
+ )
2014
+ ):
2015
+ lazy_backward_info.saved_context.fw_metadata.bw_donated_idxs = ( # type: ignore[union-attr]
2016
+ []
2017
+ )
2018
+
2019
+ bw_module = lazy_backward_info.bw_module
2020
+ placeholder_list = lazy_backward_info.placeholder_list
2021
+ saved_context = lazy_backward_info.saved_context
2022
+ saved_compile_context = lazy_backward_info.saved_compile_context
2023
+
2024
+ context = torch._C._DisableAutocast if disable_amp else nullcontext
2025
+ metrics_context = get_metrics_context()
2026
+ with tracing(saved_context), compile_context(
2027
+ saved_compile_context
2028
+ ), context(), track_graph_compiling(
2029
+ aot_config, "backward"
2030
+ ), metrics_context, dynamo_timed(
2031
+ "backward._backward_impl",
2032
+ phase_name="entire_backward_compile",
2033
+ log_pt2_compile_event=True,
2034
+ dynamo_compile_column_us="backward_cumulative_compile_time_us",
2035
+ ):
2036
+ metrics_context.update_outer({"is_forward": False})
2037
+ CompiledFunction.compiled_bw = aot_config.bw_compiler(
2038
+ bw_module, placeholder_list
2039
+ )
2040
+ # Maybe save cache entry
2041
+ if try_save_cache_entry is not None:
2042
+ try_save_cache_entry(
2043
+ CompiledFunction.compiled_bw,
2044
+ fw_metadata,
2045
+ aot_config,
2046
+ )
2047
+
2048
+ if (
2049
+ torch._functorch.config.donated_buffer
2050
+ and not saved_tensors_use_once
2051
+ and fw_metadata.bw_donated_idxs != []
2052
+ ):
2053
+ torch._check(
2054
+ False,
2055
+ lambda: (
2056
+ "This backward function was compiled with non-empty donated "
2057
+ "buffers which requires create_graph=False and retain_graph=False. "
2058
+ "Please keep backward(create_graph=False, retain_graph=False) "
2059
+ "across all backward() function calls, or set "
2060
+ "torch._functorch.config.donated_buffer=False to disable "
2061
+ "donated buffer."
2062
+ ),
2063
+ )
2064
+
2065
+ out = call_func_at_runtime_with_args(
2066
+ CompiledFunction.compiled_bw,
2067
+ all_args,
2068
+ steal_args=True,
2069
+ disable_amp=disable_amp,
2070
+ )
2071
+ return out
2072
+
2073
+ @staticmethod
2074
+ def _backward_epilogue(ctx, out):
2075
+ # Toss out the backward output tokens
2076
+ num_bw_tokens = CompiledFunction.metadata.num_backward_tokens
2077
+ if num_bw_tokens > 0:
2078
+ out = out[:-num_bw_tokens]
2079
+
2080
+ # TODO: replace this with FunctionalizedRngRuntimeWrapper.post_compile
2081
+ out = FunctionalizedRngRuntimeWrapper()._functionalized_rng_runtime_epilogue(
2082
+ CompiledFunction.metadata, out, offset_index=len(out) - 1
2083
+ )
2084
+ out = tuple(out)
2085
+
2086
+ # TODO: figure out how to refactor the backward properly so I can use aot_dispatch_subclass_wrapper() here.
2087
+ if CompiledFunction.maybe_subclass_metadata is not None:
2088
+ assert (
2089
+ CompiledFunction.maybe_subclass_metadata.grad_input_metas
2090
+ is not None
2091
+ )
2092
+ outs_wrapped = wrap_tensor_subclasses(
2093
+ out,
2094
+ subclass_metas=CompiledFunction.maybe_subclass_metadata.grad_input_metas,
2095
+ included_subclass_symints=True,
2096
+ is_runtime=True,
2097
+ )
2098
+ return outs_wrapped
2099
+ return out
2100
+
2101
+ compiled_function = RuntimeWrapper(
2102
+ indices_of_inps_to_detach=indices_of_inps_to_detach,
2103
+ trace_joint=True,
2104
+ disable_amp=disable_amp,
2105
+ ).post_compile(
2106
+ CompiledFunction.apply,
2107
+ aot_config,
2108
+ runtime_metadata=fw_metadata,
2109
+ )
2110
+
2111
+ return compiled_function
2112
+
2113
+
2114
+ @dataclass
2115
+ class DebugAssertWrapper(CompilerWrapper):
2116
+ flat_requires_grad: List[Optional[bool]] = field(default_factory=list)
2117
+
2118
+ def post_compile(
2119
+ self,
2120
+ compiled_fn,
2121
+ aot_config: AOTConfig,
2122
+ *,
2123
+ runtime_metadata: ViewAndMutationMeta,
2124
+ ):
2125
+ @wraps(compiled_fn)
2126
+ def debug_compiled_function(args: List[Any]):
2127
+ # TODO: Check aliasing relationships
2128
+ # TODO: Check strides for metadata mutation
2129
+ # (NB: ideally, this logic is factored out of this function and
2130
+ # you move these debug checks there)
2131
+
2132
+ # Check requires grad. Bad case is when we compiled with
2133
+ # requires_grad = False, but input requires_grad = True
2134
+ # (vice versa is OK; we compute a gradient and then throw
2135
+ # it away when it hits the input.)
2136
+ for i, a in enumerate(args):
2137
+ can_require_grad = self.flat_requires_grad[i]
2138
+ if can_require_grad is None:
2139
+ assert not isinstance(a, Tensor)
2140
+ elif not can_require_grad:
2141
+ assert not a.requires_grad, format_guard_bug_msg(
2142
+ aot_config,
2143
+ f"{describe_input(i, aot_config)} would not require grad",
2144
+ )
2145
+
2146
+ return compiled_fn(args)
2147
+
2148
+ return debug_compiled_function
2149
+
2150
+
2151
+ def pre_compile(
2152
+ wrappers: List[CompilerWrapper],
2153
+ flat_fn: Callable,
2154
+ flat_args: List[Any],
2155
+ aot_config: AOTConfig,
2156
+ *,
2157
+ fw_metadata: ViewAndMutationMeta,
2158
+ ) -> Tuple[Callable, List[Tensor], ViewAndMutationMeta]:
2159
+ """
2160
+ Runs a sequence of wrappers on the given function and arguments.
2161
+ Mutates wrappers in place.
2162
+ """
2163
+ for wrapper in wrappers:
2164
+ flat_fn, flat_args, fw_metadata = wrapper.pre_compile(
2165
+ flat_fn, flat_args, aot_config, fw_metadata=fw_metadata
2166
+ )
2167
+ return flat_fn, flat_args, fw_metadata
2168
+
2169
+
2170
+ def post_compile(
2171
+ wrappers: List[CompilerWrapper],
2172
+ compiled_fn: Callable,
2173
+ aot_config: AOTConfig,
2174
+ *,
2175
+ runtime_metadata: ViewAndMutationMeta,
2176
+ ) -> Tuple[Callable, ViewAndMutationMeta]:
2177
+ """
2178
+ Runs a sequence of wrappers on the given function. Should be called after pre_compile()
2179
+ """
2180
+ for wrapper in reversed(wrappers):
2181
+ compiled_fn = wrapper.post_compile(
2182
+ compiled_fn, aot_config, runtime_metadata=runtime_metadata
2183
+ )
2184
+ return compiled_fn, runtime_metadata
2185
+
2186
+
2187
+ def make_runtime_safe(
2188
+ fw_metadata: ViewAndMutationMeta,
2189
+ maybe_subclass_meta: Optional[SubclassMeta],
2190
+ ):
2191
+ """
2192
+ Calls make_runtime_safe on all ViewAndMutationMetas.
2193
+ Modifies both arguments. Allows ViewAndMutationMetas to
2194
+ be safely cached in AOTAutogradCache.
2195
+ """
2196
+ fw_metadata.make_runtime_safe()
2197
+ if maybe_subclass_meta is not None:
2198
+ maybe_subclass_meta.fw_metadata.make_runtime_safe()
2199
+ if maybe_subclass_meta.grad_input_metas:
2200
+ for meta in maybe_subclass_meta.grad_input_metas:
2201
+ if isinstance(meta, SubclassCreationMeta):
2202
+ meta.make_runtime_safe()
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/schemas.py ADDED
@@ -0,0 +1,911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ The various dataclasses, Enums, namedtuples etc used in AOTAutograd. This includes
4
+ input/output types, metadata, config, function signatures etc.
5
+ """
6
+
7
+ import collections
8
+ import dataclasses
9
+ import functools
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import Any, Callable, Dict, Iterable, List, NewType, Optional, Set, Union
13
+
14
+ import torch
15
+ import torch.utils._pytree as pytree
16
+ from torch._guards import Source
17
+ from torch._ops import OpOverload
18
+ from torch._subclasses import FakeTensor
19
+ from torch._subclasses.fake_tensor import is_fake
20
+ from torch.utils._python_dispatch import is_traceable_wrapper_subclass
21
+
22
+ from .. import config
23
+ from .functional_utils import (
24
+ _check_if_mutation_can_be_in_graph,
25
+ FunctionalTensorMetadataEq,
26
+ )
27
+ from .utils import strict_zip
28
+
29
+
30
+ zip = strict_zip
31
+
32
+
33
+ OutputType = Enum(
34
+ "OutputType",
35
+ (
36
+ # output is not an alias
37
+ "non_alias",
38
+ # output aliases an input
39
+ "alias_of_input",
40
+ # output **is** an input tensor
41
+ "is_input",
42
+ # output has a ._base tensor, which is a graph intermediate.
43
+ # We need to return its ._base as a graph output,
44
+ # so its requires_grad info is populated correctly.
45
+ # Instructs the runtime code to regenerate the current output
46
+ # from a base tensor, graph_intermediates[base_idx]
47
+ "alias_of_intermediate_save_as_output",
48
+ # Same as above; but we don't need to explicitly add its ._base
49
+ # as a graph output, because it already **is** a graph output.
50
+ "alias_of_intermediate",
51
+ # Same as above; but the output's ._base is **already** a user output.
52
+ # Instructs the runtime code to regenerate the current output from
53
+ # a base tensor, user_outputs[base_idx]
54
+ "alias_of_intermediate_base_is_user_output",
55
+ # See Note [Intermediate Bases Optimization]
56
+ "unsafe_view_alias",
57
+ # output is an alias, but has a custom autograd.Function backward.
58
+ # In this case, we don't want to do view-replay, since we won't be able to replay the custom function.
59
+ # Instead, we'll treat this output "normally", and trace its backward into the graph.
60
+ "custom_function_view",
61
+ ),
62
+ )
63
+
64
+
65
+ # This class stores info about every user output.
66
+ @dataclass(frozen=True)
67
+ class OutputAliasInfo:
68
+ # Tells us if this output is:
69
+ # (1) a regular (non-aliased) output
70
+ # (2) an alias of a forward input
71
+ # (3) **is** a forward input (special case of "alias_of_input")
72
+ # (4) an alias of an intermediate (aka an alias of an output of the inner traced forward)
73
+ # (5) an alias of an intermediate, that explicitly requires returning the intermediate
74
+ # as a graph output
75
+ # (6) an alias of an intermediate, where that intermediate is also a user output
76
+ output_type: OutputType
77
+ # The raw type of the output (torch.Tensor, SymInt, etc)
78
+ raw_type: type
79
+ # If (1) above, then
80
+ # - base_idx is None
81
+ # If (2) or (3) above, then
82
+ # - Tells us that the base of this alias is user_fwd_input[base_idx]
83
+ # (This is an index into the inputs *before* we make synthetic bases)
84
+ # If (4) or (5) above, then
85
+ # - Tells us that the base of this alias is output_graph_intermediates[base_idx]
86
+ # here, this refers to the index of the *direct* traced
87
+ # If (6) above, then:
88
+ # - Tells us that the base of this alias is output_user_fwds[base_idx]
89
+ # here, this refers to the index of the *direct* traced
90
+ base_idx: Optional[int]
91
+ # If it is a Tensor, what the dynamic dims are (otherwise is None)
92
+ dynamic_dims: Optional[Set[int]]
93
+ # requires_grad
94
+ requires_grad: bool
95
+ # FunctionalTensorWrapper that represents this output.
96
+ #
97
+ # Provides us the means to replay views from it.
98
+ #
99
+ # We need to wrap the actual FunctionalTensorWrapper with this class so that
100
+ # we only compare the tensor's metadata. That's because with the transformations
101
+ # of the model throughout AOTAutograd, the sequence of ViewMeta and the base
102
+ # tensor might change.
103
+ functional_tensor: Optional[FunctionalTensorMetadataEq] = None
104
+
105
+
106
+ class MutationType(Enum):
107
+ NOT_MUTATED = 1
108
+ MUTATED_IN_GRAPH = 2
109
+ MUTATED_OUT_GRAPH = 3
110
+
111
+
112
+ # This class tells us info about user inputs.
113
+ @dataclass(frozen=True)
114
+ class InputAliasInfo:
115
+ is_leaf: bool
116
+ mutates_data: bool
117
+ mutates_metadata: bool
118
+ mutations_hidden_from_autograd: bool
119
+ mutations_under_no_grad_or_inference_mode: bool
120
+ mutation_inductor_storage_resize: bool
121
+ mutates_storage_metadata: bool
122
+ requires_grad: bool
123
+ keep_input_mutations: bool
124
+
125
+ def __post_init__(self):
126
+ if self.mutates_storage_metadata:
127
+ # For convenience, we guarantee that this is always true.
128
+ # In practice, If we call .set_(), then at runtime there is no need
129
+ # to additionally fix up the tensor metadata, since our runtime
130
+ # call to inp.set_(updated_inp) will already have the right metadata
131
+ assert self.mutates_metadata
132
+
133
+ @functools.cached_property
134
+ def mutation_type(self) -> MutationType:
135
+ if (
136
+ (not self.mutates_data)
137
+ and (not self.mutates_metadata)
138
+ and not (self.mutation_inductor_storage_resize)
139
+ ):
140
+ return MutationType.NOT_MUTATED
141
+
142
+ if _check_if_mutation_can_be_in_graph(
143
+ self.keep_input_mutations,
144
+ self.mutates_data,
145
+ self.mutates_metadata,
146
+ self.mutations_hidden_from_autograd,
147
+ self.mutations_under_no_grad_or_inference_mode,
148
+ self.mutates_storage_metadata,
149
+ self.mutation_inductor_storage_resize,
150
+ self.requires_grad,
151
+ ):
152
+ return MutationType.MUTATED_IN_GRAPH
153
+
154
+ return MutationType.MUTATED_OUT_GRAPH
155
+
156
+
157
+ @dataclass
158
+ class PlainTensorMeta:
159
+ unwrapped_idx: int
160
+ memory_format: Optional[torch.memory_format] = None
161
+
162
+
163
+ @dataclass
164
+ class SubclassCreationMeta:
165
+ """
166
+ Used for AOTDispatch.
167
+ This dataclass gives us the information we need to reconstruct a tensor subclass
168
+ from our flat inputs.
169
+ Why is this important? The graph that we'd like to trace out contains flat tensor inputs,
170
+ But the user's original model may have subclass inputs and outputs.
171
+ So we need to wrap/unwrap subclasses as necessary to translate between the user's
172
+ view (subclass inps/outs), and the backend compiler's view (graph with no subclass args).
173
+
174
+ Complications arise mostly from the fact that a subclass can hold more than one inner tensor;
175
+ So for a given subclass input/output, we need to carefully track which indices map
176
+ to the subclass tensor in the corresponding "dense-tensor-only" graph.
177
+ """
178
+
179
+ # In the inner graph that only takes in dense tensor inputs,
180
+ # this maps to the first index of "tensors that should go in this subclass wrapper"
181
+ flat_tensor_start_idx: int
182
+ # arg_count is inclusive of the arg_counts of any
183
+ # inner tensor subclasses: If I have a TwoTensor and
184
+ # both of its inner elements are TwoTensors, then the
185
+ # arg_count of the outer-most sublass will be 4
186
+ arg_count: int
187
+ # Mark where or not symints were included. This flag is only used in one assertion
188
+ # in "wrap_tensor_subclasses"
189
+ included_subclass_symints: bool
190
+ # meta and attrs are produced by the subclass's __tensor_flatten__.
191
+ # We need to keep them around along with outer_size / outer_stride to plumb them
192
+ # into __tensor_unflatten__
193
+ attrs: Dict[str, Union["SubclassCreationMeta", PlainTensorMeta]]
194
+ outer_size: Iterable[Union[None, int, torch.SymInt]]
195
+ outer_stride: Iterable[Union[None, int, torch.SymInt]]
196
+ meta: Any
197
+ # Stores the original subclass itself.
198
+ # This is needed because we need the autograd metadata on the original subclass
199
+ # (this is guaranteed to be a wrapper subclass that holds a fake tensor,
200
+ # so holding onto this at runtime shouldn't leak memory)
201
+ # This field is nulled out after calling make_runtime_safe()
202
+ original_subclass: Optional[torch.Tensor]
203
+
204
+ # Used at runtime to determine the subclass type, so we don't need to save the original subclass
205
+ original_subclass_type: Optional[type] = None
206
+ memory_format: Optional[torch.memory_format] = None
207
+
208
+ def compute_outer_size_and_stride(
209
+ self,
210
+ all_args,
211
+ *,
212
+ curr_start_idx: int,
213
+ ):
214
+ from .subclass_utils import compute_symint_placeholders
215
+
216
+ def compute(outer, start_idx):
217
+ placeholders = compute_symint_placeholders(outer)
218
+ has_symbolic = any(placeholders)
219
+
220
+ if has_symbolic:
221
+ start = curr_start_idx
222
+ end = start_idx + sum(placeholders)
223
+ it_args = iter(all_args[start:end])
224
+ it_placeholders = iter(placeholders)
225
+ return pytree.tree_map_only(
226
+ lambda _: next(it_placeholders), lambda _: next(it_args), outer
227
+ ), start + len(placeholders)
228
+ else:
229
+ return outer, start_idx
230
+
231
+ outer_size, next_idx = compute(self.outer_size, curr_start_idx)
232
+ outer_stride, _ = compute(self.outer_stride, next_idx)
233
+ return outer_size, outer_stride
234
+
235
+ def creation_fn(
236
+ self,
237
+ all_args,
238
+ *,
239
+ is_runtime: bool,
240
+ ):
241
+ inner_tensors = {}
242
+
243
+ curr_start_idx = self.flat_tensor_start_idx
244
+ for attr, creation_meta in self.attrs.items():
245
+ if isinstance(creation_meta, PlainTensorMeta):
246
+ subclass = all_args[curr_start_idx]
247
+ curr_start_idx += 1
248
+ else:
249
+ subclass = creation_meta.creation_fn(
250
+ all_args,
251
+ is_runtime=is_runtime,
252
+ )
253
+ curr_start_idx += creation_meta.arg_count
254
+ inner_tensors[attr] = subclass
255
+
256
+ if is_runtime:
257
+ assert self.original_subclass_type is not None
258
+ original_subclass_type = self.original_subclass_type
259
+ else:
260
+ original_subclass_type = type(self.original_subclass)
261
+
262
+ if is_runtime:
263
+ outer_size, outer_stride = self.compute_outer_size_and_stride(
264
+ all_args,
265
+ curr_start_idx=curr_start_idx,
266
+ )
267
+ else:
268
+ outer_size, outer_stride = self.outer_size, self.outer_stride
269
+
270
+ rebuilt = original_subclass_type.__tensor_unflatten__( # type: ignore[attr-defined]
271
+ inner_tensors, self.meta, outer_size, outer_stride
272
+ )
273
+
274
+ if not is_runtime:
275
+ # After wrapping up the inner dense tensors into a subclass, we need to make sure that our new wrapper
276
+ # has correct autograd metadata, since we'll be tracing through the autograd engine with the subclass.
277
+ # We don't trace through the autograd engine at runtime though, so no need
278
+ # to compute this extra metadata then!
279
+ torch._mirror_autograd_meta_to(self.original_subclass, rebuilt) # type: ignore[attr-defined]
280
+
281
+ return rebuilt
282
+
283
+ def make_runtime_safe(self):
284
+ def _make_size_runtime_safe(x: Union[None, int, torch.SymInt]) -> Optional[int]:
285
+ dummy = -1
286
+ if isinstance(x, torch.SymInt):
287
+ # Replace nested ints by a dummy value (-1) as NJT ignores
288
+ # the outer_size/outer_stride at runtime.
289
+ return dummy if x.node.is_nested_int() else None
290
+ return x
291
+
292
+ assert self.original_subclass is not None
293
+ self.original_subclass_type = type(self.original_subclass)
294
+ self.original_subclass = None
295
+
296
+ # Note: NJT outer_size in AOTDispatcher
297
+ # `_make_size_runtime_safe` replaces any nested int with a dummy value (-1)
298
+ # to prevent serializing a SymInt at runtime. Internally, nested tensor __tensor_unflatten__
299
+ # is designed to safely ignore this dummy value.
300
+ # For more details, see: https://github.com/pytorch/pytorch/blob/5141ade8e30c64e873e14dcc8de233da45d15025/torch/nested/_internal/nested_tensor.py#L266-L299 # noqa: B950
301
+ self.outer_size = tuple(map(_make_size_runtime_safe, self.outer_size))
302
+ self.outer_stride = tuple(map(_make_size_runtime_safe, self.outer_stride))
303
+
304
+ # Recurse on nested subclass info
305
+ for creation_meta in self.attrs.values():
306
+ if isinstance(creation_meta, SubclassCreationMeta):
307
+ creation_meta.make_runtime_safe()
308
+
309
+ def __post_init__(self):
310
+ # sanity assert to make sure we don't leak memory
311
+ assert is_fake(self.original_subclass)
312
+
313
+ # This saves the type of subclass nested structure to compare
314
+ # against runtime tangent inputs. We do wanna compute this at AOT
315
+ # time as it is invoked in hot-path
316
+ from .subclass_utils import get_types_for_subclass
317
+
318
+ self.subclass_type = get_types_for_subclass(self.original_subclass)
319
+
320
+
321
+ # This class encapsulates all aliasing + mutation info we need about the forward graph
322
+ # See a more detailed overview of the edge case handling at
323
+ # https://docs.google.com/document/d/19UoIh_SVrMy_b2Sx5ZaeOJttm6P0Qmyss2rdBuyfoic/edit
324
+ @dataclass(eq=False)
325
+ class ViewAndMutationMeta:
326
+ # length = # user inputs
327
+ # This gives us info about every input, and what sort of mutation happened to it (if any)
328
+ input_info: List[InputAliasInfo]
329
+
330
+ # length = # user outputs
331
+ # This gives us info about every output (mostly around whether it aliases other tensors)
332
+ output_info: List[OutputAliasInfo]
333
+
334
+ # length = the number of intermediate bases appended as outputs to the end of the forward graph.
335
+ # Note: this is not necessarily the same thing as:
336
+ # len([x for x in output_info if x.output_type == OutputType.alias_of_intermediate])
337
+ # Because outputs might share a ._base, or an output's ._base might itself be
338
+ # another user output (in both cases, we won't redundantly append bases to the end of the graph)
339
+ num_intermediate_bases: int
340
+
341
+ # For inference only: instructs us to keep data-only input mutations directly in the graph
342
+ keep_input_mutations: bool
343
+
344
+ # length = (# inputs w data mutations) + (# user outputs that are non_aliasing tensors)
345
+ # + (# intermediate bases)
346
+ # These are the FakeTensor (or potential SymInt) outputs that we traced from our
347
+ # metadata pass of the user's forward function.
348
+ # Their only use today is to pass them as a best-guess for tangents when tracing the joint.
349
+ # Stashing them as part of our "metadata" makes it simpler if we want to run our analysis
350
+ # pass once, and re-use the output throughout AOTAutograd
351
+ traced_tangents: List[Any]
352
+
353
+ # Each of these is a list telling us about subclasses for the inputs/outputs/grad_outs
354
+ # They are used throughout AOTDispatch to tell us how to generate a list of subclass tensors,
355
+ # Given a (potentially larger) list of plain torch tensors.
356
+
357
+ # Taking subclass_inp_meta as an example:
358
+ # subclass_inp_meta[i] = j (an int) tells us:
359
+ # "The i'th user input is not a subclass, and corresponds to inputs[j] of the plain-tensor graph."
360
+ # subclass_inp_meta[i] = SubclassCreationMeta(flat_tensor_start_idx=3, arg_count=2)
361
+ # "The i'th user input is subclass holding two inner tensors, which are
362
+ # inputs[3] and inputs[4] of the plain-tensor graph".
363
+
364
+ # length = # user inputs
365
+ subclass_inp_meta: List[Union[PlainTensorMeta, SubclassCreationMeta]]
366
+ # So, the full set of outputs to the forward graph looks something like:
367
+ # (*mutated_inps, *user_outs, *intermediate_bases, *saved_for_bw_tensors)
368
+ # where the first 3 of those 4 can be subclasses
369
+ # (but not saved_for_bw tensors, since these are internal to the compiler
370
+ # and not user visible, so there's no point in wrapping/unwrapping them at runtime).
371
+ # This list contains subclass information on all of the fw graph outputs
372
+ # except for saved_for_bw_tensors.
373
+ subclass_fw_graph_out_meta: List[Union[PlainTensorMeta, SubclassCreationMeta]]
374
+ # length = # backward graph inputs
375
+ subclass_tangent_meta: List[Union[PlainTensorMeta, SubclassCreationMeta]]
376
+ # TODO: we should kill this
377
+ # (need to default it to not break internal)
378
+ is_train: bool = False
379
+
380
+ # length = (# inputs w data mutations) + (# user outputs that are non_aliasing tensors)
381
+ # + (# intermediate bases)
382
+ # At runtime, we don't keep the traced_tangents around since they're not serializable.
383
+ # Instead, we keep any necessary subclass metadata necessary about each traced_tangent.
384
+ # This list is generated after calling make_runtime_safe().
385
+ traced_tangent_metas: Optional[List[Any]] = None
386
+
387
+ num_symints_saved_for_bw: Optional[int] = None
388
+
389
+ # The grad_enabled mutation that will be emitted in the runtime_wrapper epilogue
390
+ # NOTE: AOTAutograd will assume that the ambient `is_grad_enabled` is the grad mode
391
+ # that is intended to be in effect prior to running the graph, in keeping with
392
+ # equivalence to eager mode. It is the responsibility of upstream graph acquisition
393
+ # to reset the grad mode to its pre-graph value prior to calling aot_autograd.
394
+ grad_enabled_mutation: Optional[bool] = None
395
+
396
+ # Keeps track of whether `torch.use_deterministic_algorithms` was turned on
397
+ # when the forward was run. If deterministic mode was turned off during the
398
+ # forward, but is turned on during the backward call, then an error is
399
+ # raised
400
+ deterministic: Optional[bool] = None
401
+
402
+ # Keeps track of which input indices store parameters (which we will treat as static)
403
+ static_input_indices: List[int] = field(default_factory=list)
404
+
405
+ # Map of effect type (ex. _EffectType.ORDERED) to token. If there are
406
+ # side-effectful operators, FunctionalTensorMode will populate this
407
+ # dictionary telling us how many tokens we will need during tracing.
408
+ tokens: Dict[Any, torch.Tensor] = field(default_factory=dict)
409
+
410
+ # Only filled in if/when we trace the joint function
411
+ # If an input requires grad and is mutated in the backward, it is only safe to keep the mutation
412
+ # in the graph if gradients are disabled while the backward runs
413
+ # (grad mode is disabled by default when users run the backward, but can be turned on with create_graph=True)
414
+ # At runtime during the backward, we use this list of indices to error properly if we find out
415
+ # that it was not safe to include a backward mutation in the graph.
416
+ indices_of_inputs_that_requires_grad_with_mutations_in_bw: List[int] = field(
417
+ default_factory=list
418
+ )
419
+
420
+ # Indexes of saved tensors which are donated buffer.
421
+ # Donated buffer means the tensor is not alias of any forward user input, forward user output,
422
+ # and backward output.
423
+ bw_donated_idxs: Optional[List[int]] = None
424
+
425
+ # Number of tokens used in backward, appended at the end of backward outputs.
426
+ # Filled after tracing joint function.
427
+ num_backward_tokens: int = 0
428
+
429
+ def __post_init__(self):
430
+ # pre-compute the indices of the inputs that are mutated.
431
+ # When keep_input_mutations is set, we don't need to worry about our epilogue
432
+ # handling data-only mutations, because we keep them directly in the graph.
433
+
434
+ mutated_inp_runtime_indices = [
435
+ i
436
+ for i, m in enumerate(self.input_info)
437
+ if (m.mutation_type == MutationType.MUTATED_OUT_GRAPH)
438
+ ]
439
+
440
+ mutated_graph_handled_indices = [
441
+ i
442
+ for i, m in enumerate(self.input_info)
443
+ if m.mutation_type == MutationType.MUTATED_IN_GRAPH
444
+ ]
445
+ self.mutated_graph_handled_indices = mutated_graph_handled_indices
446
+ self.num_mutated_graph_handled_indices = len(self.mutated_graph_handled_indices)
447
+
448
+ mutated_graph_handled_indices_seen_by_autograd = [
449
+ i
450
+ for i in mutated_graph_handled_indices
451
+ if not self.input_info[i].mutations_hidden_from_autograd
452
+ ]
453
+
454
+ self.mutated_graph_handled_indices_seen_by_autograd = (
455
+ mutated_graph_handled_indices_seen_by_autograd
456
+ )
457
+ self.num_mutated_graph_handled_indices_seen_by_autograd = len(
458
+ self.mutated_graph_handled_indices_seen_by_autograd
459
+ )
460
+
461
+ aliased_out_indices = [
462
+ i
463
+ for i, m in enumerate(self.output_info)
464
+ if m.output_type
465
+ not in [
466
+ OutputType.non_alias,
467
+ OutputType.unsafe_view_alias,
468
+ OutputType.custom_function_view,
469
+ ]
470
+ ]
471
+ unsafe_view_out_indices = [
472
+ i
473
+ for i, m in enumerate(self.output_info)
474
+ if m.output_type is OutputType.unsafe_view_alias
475
+ ]
476
+
477
+ # This is pre-computed in post_init for perf.
478
+ # It contains the index of every element
479
+ # of input_info that corresponds to a mutation (data or metadata or both)
480
+ self.mutated_inp_runtime_indices = mutated_inp_runtime_indices
481
+ self.num_mutated_inp_runtime_indices = len(self.mutated_inp_runtime_indices)
482
+
483
+ # This is pre-computed for perf.
484
+ # It contains the index of every element
485
+ # of output_info that corresponds to an alias (either of an input or intermediate)
486
+ self.aliased_out_indices = aliased_out_indices
487
+ self.unsafe_view_out_indices = unsafe_view_out_indices
488
+ self.num_outputs = len(self.output_info)
489
+ self.num_outputs_non_aliased = len(
490
+ [
491
+ x
492
+ for x in self.output_info
493
+ if x.output_type
494
+ in [
495
+ OutputType.non_alias,
496
+ OutputType.unsafe_view_alias,
497
+ OutputType.custom_function_view,
498
+ ]
499
+ ]
500
+ )
501
+ self.num_outputs_aliased_to_inputs = len(
502
+ [
503
+ x
504
+ for x in self.output_info
505
+ if x.output_type
506
+ in [
507
+ OutputType.alias_of_input,
508
+ OutputType.is_input,
509
+ ]
510
+ ]
511
+ )
512
+ self.num_unsafe_view_outputs = len(self.unsafe_view_out_indices)
513
+ self.num_outputs_aliased_to_intermediates = len(
514
+ [
515
+ x
516
+ for x in self.output_info
517
+ if x.output_type
518
+ in [
519
+ OutputType.alias_of_intermediate,
520
+ OutputType.alias_of_intermediate_save_as_output,
521
+ OutputType.alias_of_intermediate_base_is_user_output,
522
+ ]
523
+ ]
524
+ )
525
+ self.num_outputs_aliased = (
526
+ self.num_outputs_aliased_to_inputs
527
+ + self.num_outputs_aliased_to_intermediates
528
+ )
529
+
530
+ self.dynamic_outputs = any(o.dynamic_dims for o in self.output_info)
531
+ # See Note: [AOTAutograd Backward Guards]
532
+ # This is pre-computed for fast asserts on the types of our grad_outputs in the backward.
533
+ # Eventually, we should kill this and replace with real backward guards.
534
+ # (we want to precompute the "runtime" types, so replace FakeTensor with torch.Tensor)
535
+ self.output_types = [
536
+ torch.Tensor if isinstance(x, FakeTensor) else type(x)
537
+ for x in self.traced_tangents
538
+ ]
539
+
540
+ self.is_rng_op_functionalized = config.functionalize_rng_ops
541
+ # All of the above metadata is collected by tracing the fw function.
542
+ # However, extra outputs for rng offsets behave differently. Both fwd
543
+ # and bwd graphs have their own outputs for the total consumed offsets.
544
+ # Unlike mutated inputs, we don't have to worry about sending the right
545
+ # set of tensors between fwd and bwd. Fwd and bwd offsets are
546
+ # independent and simpler to handle. Therefore, we track them
547
+ # separately.
548
+ self.num_outputs_rng_offset = 1 if self.is_rng_op_functionalized else 0
549
+
550
+ # Our forward() returns both (tokens, mutated_inputs, outputs, output_intermediate_bases, saved_tensors, saved_symints)
551
+ # Tokens will be split out before mutations/view handling and we do not count them here.
552
+ self.num_forward_returns = (
553
+ self.num_mutated_inp_runtime_indices
554
+ + self.num_outputs
555
+ + self.num_intermediate_bases
556
+ )
557
+ # In case of functionalization of rng ops, the fw_module returns one
558
+ # additional output for rng offset. This rng offset is used right
559
+ # away to advance the rng state, and is not passed on to the raw
560
+ # outputs. However, we need to know the exact boundary to identify
561
+ # which tensors to be saved for the bwd graph. num_forward captures
562
+ # this information.
563
+ self.num_forward = self.num_forward_returns + self.num_outputs_rng_offset
564
+
565
+ def make_runtime_safe(self):
566
+ """
567
+ There are various fields in ViewAndMutationMeta that aren't serializable. This function is called after all tracing
568
+ is completed to simplify certain fields in the metadata so that they can be safely cached.
569
+
570
+ Doing so may lose information (in the case of traced_tangents), but none of the information is needed at runtime.
571
+ """
572
+ # TODO: This function is only a best effort: there are other fields that may not be cache safe
573
+ # (i.e., there's no guarantee that tensor_flatten() returns a serializable result), or that
574
+ # SubclassCreationMeta is cache safe.
575
+ assert self.traced_tangent_metas is None
576
+
577
+ def extract_metadata(t):
578
+ if isinstance(t, torch.Tensor) and is_traceable_wrapper_subclass(t):
579
+ (inner_tensors, flatten_spec) = t.__tensor_flatten__() # type: ignore[attr-defined]
580
+ # Technically, we only need the flatten_spec, not the inner tensors.
581
+ # However, some Tensor subclasses (like TwoTensor) may have flatten_spec = None.
582
+ # And we want to be able to assert that this metadata is non-None,
583
+ # to distinguish between "this was a tensor subclass with no metadata" vs.
584
+ # "this wasn't a tensor subclass at all".
585
+ return (inner_tensors, flatten_spec)
586
+ else:
587
+ return None
588
+
589
+ self.traced_tangent_metas = [extract_metadata(t) for t in self.traced_tangents]
590
+ # Clear traced tangents at runtime
591
+ self.traced_tangents = []
592
+ new_output_info = []
593
+ for out in self.output_info:
594
+ if config.view_replay_for_aliased_outputs:
595
+ new_out = out
596
+ else:
597
+ # If we're not using view_replay, remove the functional tensor.
598
+ # Functional tensors are unfortunately not serializable,
599
+ # so doing this is required for AOTAutograd caching.
600
+ new_out = dataclasses.replace(out, functional_tensor=None)
601
+ new_output_info.append(new_out)
602
+ self.output_info = new_output_info
603
+ for inp_meta in self.subclass_inp_meta:
604
+ if isinstance(inp_meta, SubclassCreationMeta):
605
+ inp_meta.make_runtime_safe()
606
+ for inp_meta in self.subclass_fw_graph_out_meta:
607
+ if isinstance(inp_meta, SubclassCreationMeta):
608
+ inp_meta.make_runtime_safe()
609
+ for inp_meta in self.subclass_tangent_meta:
610
+ if isinstance(inp_meta, SubclassCreationMeta):
611
+ inp_meta.make_runtime_safe()
612
+
613
+ @property
614
+ def tensors_saved_for_backwards_slice(self):
615
+ assert self.num_symints_saved_for_bw is not None
616
+ if self.num_symints_saved_for_bw > 0:
617
+ return slice(self.num_forward, -self.num_symints_saved_for_bw)
618
+ else:
619
+ return slice(self.num_forward, None)
620
+
621
+ @property
622
+ def symints_saved_for_backwards_slice(self):
623
+ assert self.num_symints_saved_for_bw is not None
624
+ if self.num_symints_saved_for_bw > 0:
625
+ return slice(-self.num_symints_saved_for_bw, None)
626
+ else:
627
+ return slice(0, 0) # empty slice
628
+
629
+ def __eq__(self, other):
630
+ if not isinstance(other, ViewAndMutationMeta):
631
+ return NotImplemented
632
+ return (
633
+ self.input_info == other.input_info
634
+ and self.output_info == other.output_info
635
+ and self.num_intermediate_bases == other.num_intermediate_bases
636
+ and self.keep_input_mutations == other.keep_input_mutations
637
+ and self.is_rng_op_functionalized == other.is_rng_op_functionalized
638
+ and self.num_outputs_rng_offset == other.num_outputs_rng_offset
639
+ and len(self.traced_tangents) == len(other.traced_tangents)
640
+ and all(
641
+ x.shape == y.shape and x.dtype == y.dtype
642
+ for x, y, in zip(self.traced_tangents, other.traced_tangents)
643
+ )
644
+ and self.num_backward_tokens == other.num_backward_tokens
645
+ )
646
+
647
+
648
+ @dataclass(eq=False)
649
+ class SubclassMeta:
650
+ # A copy of all forward metadata, but computed on the *dense* tensor forward (after desugaring subclasses)
651
+ # So for example, if the user had a model containing two `TwoTensor` inputs,
652
+ # Then `SubclassMeta.fw_metadata.input_infos` would have length 4 here.
653
+ fw_metadata: ViewAndMutationMeta
654
+
655
+ # Note: [Computing Subclass Metadata about grad_inputs]
656
+ # Given a list of flattened, plain tensor grad_inputs, this tells us how to reconstruct the grad_input subclasses
657
+ #
658
+ # You might think: why not just assume that all grad_inputs will have the same subclass-ness as the original inputs?
659
+ # (AOTAutograd generally assumes other properties, e.g. that grad_outputs are contiguous)
660
+ #
661
+ # This doesn't really work though. take this example:
662
+ #
663
+ # def f(DoubleTensor, DenseTensor):
664
+ # return DoubleTensor * DenseTensor
665
+ #
666
+ # In the above example, the .grad field of *both* DoubleTensor and DenseTensor will be a DoubleTensor.
667
+ # When we trace out a joint fw-bw graph, we'll end up returning two subclasses for the two grad_inputs.
668
+ # This means that our backward graph will return 4 outputs (two dense tensors for each DoubleTensor grad_input)
669
+ # and we need to properly store the metadata that tells us how to turn these 4 outputs back into DoubleTensors.
670
+ #
671
+ # Note that this info **cannot** easily be figured out from ViewAndMutationMeta.
672
+ # We can only compute this info by tracing the entire joint and examining the grad_inputs that we computed.
673
+ #
674
+ # See Note: [AOTAutograd Backward Guards]
675
+ # This will also eventually require us to install backward guards,
676
+ # in case we made incorrect assumptions about the subclass-ness of our grad_outputs
677
+ #
678
+ # Optional field because we don't compute for inference graphs
679
+ grad_input_metas: Optional[
680
+ List[Union[PlainTensorMeta, SubclassCreationMeta]]
681
+ ] = None
682
+
683
+ def __init__(self) -> None:
684
+ # The fields in this class get set after its construction.
685
+ pass
686
+
687
+
688
+ # This class exists because:
689
+ # - the autograd.Function.forward() in aot autograd returns outputs that might alias inputs
690
+ # - we only care about the metadata on those aliases, so we can regenerate them.
691
+ # We do not want them to participate in the autograd.Function.
692
+ # We do that by wrapping them in an opaque class, so the autograd.Function
693
+ # does not know to treat them as tensors.
694
+ @dataclass(frozen=True)
695
+ class TensorAlias:
696
+ alias: torch.Tensor
697
+
698
+
699
+ @dataclass
700
+ class BackwardSignature:
701
+ """
702
+ Provides information about the backward section of an exported
703
+ joint forward-backward graph.
704
+ For a particular fx GraphModule, this class contains information on:
705
+ (1) A mapping from each gradient (backwards output) to the parameter
706
+ it corresponds to (forward input)
707
+ (2) A mapping from each gradient (backwards output) to the user input
708
+ it corresponds to (forward input)
709
+ (3) Which of the forward outputs corresponds to the loss, that we backprop on.
710
+
711
+ Each string name is the `node.name` of the corresponding node in the fx graph.
712
+ """
713
+
714
+ gradients_to_parameters: Dict[str, str]
715
+ gradients_to_user_inputs: Dict[str, str]
716
+ loss_output: str
717
+
718
+
719
+ GraphOutputName = NewType("GraphOutputName", str)
720
+ GraphInputName = NewType("GraphInputName", str)
721
+ FQN = NewType("FQN", str)
722
+
723
+
724
+ @dataclass
725
+ class GraphSignature:
726
+ """
727
+ Provides information about an exported module.
728
+ For a particular fx GraphModule, this class contains information on:
729
+ (1) Which graph inputs are parameters, buffers, or user inputs
730
+ (2) (for params/buffers) a mapping from the name of each graph argument
731
+ to its parameter/buffer FQN in the original nn.Module.
732
+ (3) If there are input mutations, these are represented as extra outputs
733
+ in the fx GraphModule. We provide a mapping from these
734
+ extra output names to the names of the actual inputs.
735
+ (4) The pytree metadata on how to flatten/unflatten inputs and outputs.
736
+ The corresponding FX GraphModule only accepts and returns
737
+ pytree-flattened inputs/outputs.
738
+ (5) (Optionally) if the FX is a joint forward-backward graph, we provide
739
+ a signature on the backward section of the joint graph.
740
+ """
741
+
742
+ parameters: List[FQN]
743
+ buffers: List[FQN]
744
+
745
+ user_inputs: List[GraphInputName]
746
+ user_outputs: List[GraphOutputName]
747
+ inputs_to_parameters: Dict[GraphInputName, FQN]
748
+ inputs_to_buffers: Dict[GraphInputName, FQN]
749
+
750
+ # If the user's module mutates a buffer,
751
+ # it's represented in the graph as an extra graph output.
752
+ # This dict is a mapping from
753
+ # "graph outputs that correspond to updated buffers"
754
+ # to the FQN names of those mutated buffers.
755
+ buffers_to_mutate: Dict[GraphOutputName, FQN]
756
+ user_inputs_to_mutate: Dict[GraphOutputName, GraphInputName]
757
+
758
+ in_spec: pytree.TreeSpec
759
+ out_spec: pytree.TreeSpec
760
+
761
+ backward_signature: Optional[BackwardSignature]
762
+
763
+ input_tokens: List[GraphInputName]
764
+ output_tokens: List[GraphOutputName]
765
+
766
+ @classmethod
767
+ def from_tracing_metadata(
768
+ cls,
769
+ *,
770
+ in_spec: pytree.TreeSpec,
771
+ out_spec: pytree.TreeSpec,
772
+ graph_input_names: List[str],
773
+ graph_output_names: List[str],
774
+ view_mutation_metadata: ViewAndMutationMeta,
775
+ named_parameters: List[str],
776
+ named_buffers: List[str],
777
+ num_user_inputs: int,
778
+ num_user_outputs: int,
779
+ loss_index: Optional[int],
780
+ backward_signature: Optional[BackwardSignature],
781
+ ) -> "GraphSignature":
782
+ graph_inputs = graph_input_names
783
+ graph_outputs = graph_output_names
784
+ parameters = list(named_parameters)
785
+ buffers = list(named_buffers)
786
+ num_tokens = len(view_mutation_metadata.tokens)
787
+
788
+ # Calling convention assumptions:
789
+ # (1) graph inputs = (input_tokens, params, buffers, user_inputs)
790
+ # (2) graph outputs = (output_tokens, mutated_inputs, user_outs, param_gradients)
791
+ # (If we are capturing an inference graph, this convention is identical
792
+ # except that param_gradients is empty)
793
+ # See Note [Side-Effectful Tokens in AOTAutograd] for information on tokens
794
+
795
+ # Address input calling conventions:
796
+ start, stop = 0, num_tokens
797
+ input_tokens = graph_inputs[start:stop]
798
+
799
+ start, stop = stop, stop + len(parameters)
800
+ inputs_to_parameters = dict(zip(graph_inputs[start:stop], parameters))
801
+
802
+ start, stop = stop, stop + len(buffers)
803
+ inputs_to_buffers = dict(
804
+ zip(
805
+ graph_inputs[start:stop],
806
+ buffers,
807
+ )
808
+ )
809
+
810
+ start, stop = stop, stop + num_user_inputs
811
+ user_inputs = graph_inputs[start:stop]
812
+
813
+ # We should've gone through all the inputs now
814
+ assert len(graph_inputs) - stop == 0
815
+
816
+ # Address output calling conventions:
817
+ start, stop = 0, num_tokens
818
+ output_tokens = graph_outputs[start:stop]
819
+
820
+ names = [*input_tokens, *parameters, *buffers, *user_inputs]
821
+ mutations = []
822
+ for idx, input_info in enumerate(view_mutation_metadata.input_info):
823
+ if input_info.mutates_data:
824
+ # Only buffers can be mutated, not parameters
825
+ assert idx >= len(parameters)
826
+ mutations.append(names[idx + num_tokens])
827
+
828
+ assert len(mutations) == view_mutation_metadata.num_mutated_inp_runtime_indices
829
+
830
+ start, stop = (
831
+ stop,
832
+ stop + view_mutation_metadata.num_mutated_inp_runtime_indices,
833
+ )
834
+ outputs_to_mutations = dict(zip(graph_outputs[start:stop], mutations))
835
+
836
+ user_inputs_to_mutate = {}
837
+ buffers_to_mutate = {}
838
+ for output_name, mutation_name in outputs_to_mutations.items():
839
+ if mutation_name in user_inputs:
840
+ user_inputs_to_mutate[output_name] = mutation_name
841
+ else:
842
+ assert mutation_name in buffers
843
+ buffers_to_mutate[output_name] = mutation_name
844
+
845
+ start, stop = stop, stop + num_user_outputs
846
+ user_outputs = graph_outputs[start:stop]
847
+
848
+ unused_outputs = len(graph_outputs) - stop
849
+ if backward_signature is not None:
850
+ unused_outputs -= len(backward_signature.gradients_to_parameters) + len(
851
+ backward_signature.gradients_to_user_inputs
852
+ )
853
+ assert unused_outputs == 0
854
+
855
+ return GraphSignature(
856
+ parameters=parameters, # type: ignore[arg-type]
857
+ buffers=buffers, # type: ignore[arg-type]
858
+ user_inputs=user_inputs, # type: ignore[arg-type]
859
+ user_outputs=user_outputs, # type: ignore[arg-type]
860
+ inputs_to_buffers=inputs_to_buffers, # type: ignore[arg-type]
861
+ inputs_to_parameters=inputs_to_parameters, # type: ignore[arg-type]
862
+ user_inputs_to_mutate=user_inputs_to_mutate,
863
+ buffers_to_mutate=buffers_to_mutate, # type: ignore[arg-type]
864
+ in_spec=in_spec,
865
+ out_spec=out_spec,
866
+ backward_signature=backward_signature,
867
+ input_tokens=input_tokens, # type: ignore[arg-type]
868
+ output_tokens=output_tokens, # type: ignore[arg-type]
869
+ )
870
+
871
+
872
+ @dataclass
873
+ class AOTAutogradCacheInfo:
874
+ cache_key: str
875
+ start_time_ns: int
876
+
877
+
878
+ @dataclass
879
+ class AOTConfig:
880
+ """
881
+ Configuration for AOTDispatcher
882
+ """
883
+
884
+ fw_compiler: Callable
885
+ bw_compiler: Callable
886
+ partition_fn: Callable
887
+ decompositions: Dict[OpOverload, Callable]
888
+ num_params_buffers: int
889
+ aot_id: int
890
+ keep_inference_input_mutations: bool
891
+ is_export: bool = False
892
+ no_tangents: bool = False
893
+ dynamic_shapes: bool = False
894
+ aot_autograd_arg_pos_to_source: Optional[List[Source]] = None
895
+ static_input_indices: Optional[List[int]] = None
896
+ inference_compiler: Optional[Callable] = None
897
+ enable_log: bool = True
898
+ # this is always false outside of export.
899
+ pre_dispatch: bool = False
900
+ # Key to use for AOTAutogradCache
901
+ cache_info: Optional[AOTAutogradCacheInfo] = None
902
+
903
+ def __post_init__(self):
904
+ if self.pre_dispatch:
905
+ assert self.is_export, "Can only have pre_dispatch IR for export."
906
+
907
+
908
+ SubclassTracingInfo = collections.namedtuple(
909
+ "SubclassTracingInfo",
910
+ ["plain_tensor_trace_fn", "plain_tensor_args", "maybe_subclass_meta"],
911
+ )
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/subclass_parametrization.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Tuple
2
+
3
+ import torch
4
+ from torch.utils._python_dispatch import is_traceable_wrapper_subclass
5
+
6
+
7
+ class UnwrapTensorSubclass(torch.nn.Module):
8
+ def forward(self, *tensors) -> torch.Tensor: # type: ignore[no-untyped-def]
9
+ todo: List[torch.Tensor] = list(tensors)
10
+ for tp, meta, inner_tensors_attrs in reversed(self.rebuild_stack):
11
+ num_children: int = len(inner_tensors_attrs)
12
+ d = { # noqa: C416
13
+ a: b for a, b in zip(inner_tensors_attrs, todo[-num_children:])
14
+ }
15
+ todo = todo[:-num_children]
16
+ rebuilt = tp.__tensor_unflatten__(d, meta, None, None) # type: ignore[attr-defined]
17
+ todo.append(rebuilt)
18
+
19
+ assert len(todo) == 1
20
+ return todo[0]
21
+
22
+ def right_inverse(self, tensor: torch.Tensor) -> List[torch.Tensor]:
23
+ assert type(tensor) is not torch.Tensor
24
+ rebuild_stack = []
25
+ plain_tensors = []
26
+ todo = [tensor]
27
+ while todo:
28
+ obj = todo.pop()
29
+ inner_tensors_attrnames, metadata = obj.__tensor_flatten__() # type: ignore[attr-defined]
30
+ inner_tensors_attrnames_stack_order = []
31
+ subclasses_attrnames = []
32
+ for attr_name in inner_tensors_attrnames:
33
+ val = getattr(obj, attr_name)
34
+ if type(val) is torch.Tensor:
35
+ plain_tensors.append(val)
36
+ inner_tensors_attrnames_stack_order.append(attr_name)
37
+ else:
38
+ assert isinstance(val, torch.Tensor)
39
+ todo.append(val)
40
+ subclasses_attrnames.append(attr_name)
41
+ inner_tensors_attrnames_stack_order.extend(subclasses_attrnames)
42
+ rebuild_stack.append(
43
+ (type(obj), metadata, inner_tensors_attrnames_stack_order)
44
+ )
45
+
46
+ self.rebuild_stack = rebuild_stack
47
+ return plain_tensors
48
+
49
+
50
+ def unwrap_tensor_subclass_parameters(module: torch.nn.Module) -> torch.nn.Module:
51
+ """
52
+ Model transformation that replaces all the parameters that are subclasses to plain tensors.
53
+ This reduces runtime overhead of flattening/unflattening the parameters.
54
+
55
+ This transformation adds parametrization with `torch.nn.utils.parametrize`.
56
+ The FQNs of the subclass parameters will be changed and state_dict will become incompatible with the original model.
57
+ E.g.
58
+ Original model state_dict: {"p1": torch.testing._internal.TwoTensor}
59
+ becomes: {"parametrizations.p2.original0": torch.Tensor, "parametrizations.p2.original1": torch.Tensor}
60
+
61
+ """
62
+ name_param: List[Tuple[str, torch.nn.Parameter]] = list(
63
+ module.named_parameters(recurse=False)
64
+ )
65
+ for name, param in name_param:
66
+ if is_traceable_wrapper_subclass(param):
67
+ torch.nn.utils.parametrize.register_parametrization(
68
+ module, name, UnwrapTensorSubclass()
69
+ )
70
+
71
+ for name, child in module.named_children():
72
+ unwrap_tensor_subclass_parameters(child)
73
+
74
+ return module
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/subclass_utils.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ This file contains utilities for tracing through __torch_dispatch__ based tensor subclasses and modes.
4
+ AOTAutograd's responsibility is to trace through all pytorch capabilities that live in the pytorch dispatcher,
5
+ and this includes tensor subclasses that implement __torch_dispatch__.
6
+ """
7
+
8
+ import typing
9
+ from typing import Any, Iterable, List, Optional, Tuple, Union
10
+
11
+ import torch
12
+ import torch.utils._pytree as pytree
13
+ from torch import SymInt, Tensor
14
+ from torch._subclasses.fake_tensor import get_plain_tensors
15
+ from torch.utils._python_dispatch import is_traceable_wrapper_subclass
16
+
17
+ from .schemas import (
18
+ MutationType,
19
+ PlainTensorMeta,
20
+ SubclassCreationMeta,
21
+ ViewAndMutationMeta,
22
+ )
23
+ from .utils import strict_zip
24
+
25
+
26
+ zip = strict_zip
27
+
28
+
29
+ def requires_subclass_dispatch(args, fw_metadata: ViewAndMutationMeta) -> bool:
30
+ args_flattened = pytree.arg_tree_leaves(*args)
31
+ any_subclass_args = any(
32
+ is_traceable_wrapper_subclass(x)
33
+ for x in args_flattened
34
+ if isinstance(x, Tensor)
35
+ )
36
+ from torch._functorch._aot_autograd.schemas import SubclassCreationMeta
37
+
38
+ any_subclass_outputs = any(
39
+ type(x) is SubclassCreationMeta for x in fw_metadata.subclass_fw_graph_out_meta
40
+ )
41
+ # This tells us whether or not we need to perform any unwrapping/wrapping of tensor subclasses at runtime.
42
+ return any_subclass_args or any_subclass_outputs
43
+
44
+
45
+ suggest_memory_format = torch._prims_common.suggest_memory_format
46
+
47
+
48
+ def maybe_suggest_memory_format(
49
+ t, with_memory_format: bool
50
+ ) -> Optional[torch.memory_format]:
51
+ if not with_memory_format:
52
+ return None
53
+
54
+ return suggest_memory_format(t)
55
+
56
+
57
+ def get_types_for_subclass(tensor_subclass):
58
+ if not is_traceable_wrapper_subclass(tensor_subclass):
59
+ return ["Tensor"]
60
+ inner_keys, _ = tensor_subclass.__tensor_flatten__()
61
+ result = []
62
+ for key in inner_keys:
63
+ inner_tensor = getattr(tensor_subclass, key)
64
+ result.extend(get_types_for_subclass(inner_tensor))
65
+ return result
66
+
67
+
68
+ def create_subclass_metadata(
69
+ a: Any, start_idx: int, count_symints: bool, with_memory_format: bool = False
70
+ ):
71
+ if not is_traceable_wrapper_subclass(a):
72
+ idx = start_idx + 1
73
+ return (
74
+ PlainTensorMeta(
75
+ idx,
76
+ memory_format=maybe_suggest_memory_format(a, with_memory_format),
77
+ ),
78
+ idx,
79
+ )
80
+
81
+ inner_keys, metadata = a.__tensor_flatten__()
82
+ new_start_idx = start_idx
83
+ attrs = {}
84
+
85
+ for key in inner_keys:
86
+ new_subclass_meta, new_start_idx = create_subclass_metadata(
87
+ getattr(a, key),
88
+ new_start_idx,
89
+ count_symints=count_symints,
90
+ with_memory_format=with_memory_format,
91
+ )
92
+ attrs[key] = new_subclass_meta
93
+
94
+ # It *must* be because is_traceable_wrapper_subclass() - but mypy is not smart.
95
+ assert isinstance(a, Tensor)
96
+
97
+ new_start_idx = (
98
+ new_start_idx
99
+ + count_symints * len(filter_symints(a.size()))
100
+ + count_symints * len(filter_symints(a.stride()))
101
+ )
102
+
103
+ return (
104
+ SubclassCreationMeta(
105
+ flat_tensor_start_idx=start_idx,
106
+ arg_count=new_start_idx - start_idx,
107
+ included_subclass_symints=count_symints,
108
+ attrs=attrs,
109
+ meta=metadata,
110
+ outer_size=a.size(), # type: ignore[attr-defined, arg-type]
111
+ outer_stride=a.stride(), # type: ignore[arg-type]
112
+ original_subclass=a,
113
+ memory_format=maybe_suggest_memory_format(a, with_memory_format),
114
+ ),
115
+ new_start_idx,
116
+ )
117
+
118
+
119
+ # Given a flat list of arguments, some of which may be tensor subclasses,
120
+ # computes metadata about "how to reconstruct the current list of subclasses,
121
+ # if we were given their flattened dense tensors instead"
122
+ def create_subclass_meta(
123
+ curr_args: Union[List[Any], Tuple[Any, ...]],
124
+ *,
125
+ count_symints: bool = True,
126
+ with_memory_format: bool = False,
127
+ ) -> List[Union[PlainTensorMeta, SubclassCreationMeta]]:
128
+ idx = 0
129
+ infos: List[Union[PlainTensorMeta, SubclassCreationMeta]] = []
130
+ for a in curr_args:
131
+ if is_traceable_wrapper_subclass(a):
132
+ assert isinstance(a, Tensor)
133
+ start_idx = idx
134
+ subclass_meta, _ = create_subclass_metadata(
135
+ a,
136
+ start_idx,
137
+ count_symints=count_symints,
138
+ with_memory_format=with_memory_format,
139
+ )
140
+ infos.append(subclass_meta)
141
+ cnt = subclass_meta.arg_count
142
+ else:
143
+ infos.append(
144
+ PlainTensorMeta(
145
+ idx,
146
+ memory_format=maybe_suggest_memory_format(a, with_memory_format),
147
+ )
148
+ )
149
+ cnt = 1
150
+ idx += cnt
151
+ return infos
152
+
153
+
154
+ def filter_symints(lst: Iterable[Union[int, SymInt]]):
155
+ # Capture all SymInts from the iterable.
156
+ def symint_check(s: Union[int, SymInt]) -> bool:
157
+ return isinstance(s, SymInt) and not s.node.is_nested_int()
158
+
159
+ return [s for s in lst if symint_check(s)]
160
+
161
+
162
+ def compute_symint_placeholders(lst: Iterable[Union[None, int, SymInt]]) -> List[bool]:
163
+ # Non-nested symints are replaced with None in `make_runtime_safe()`
164
+ return [s is None for s in lst]
165
+
166
+
167
+ # This function takes in a pytree of arguments and unwraps any tensor
168
+ # subclasses.
169
+ #
170
+ # NOTE: The reason for "append_symints":
171
+ #
172
+ # * At compile time: we append extra symint args when unwrapping primals
173
+ # (but not tangents, because they should always share symints with primals).
174
+ # We also append extra symints when unwrapping the subclass outputs of the
175
+ # traced function, so we can return them as extra outputs
176
+ #
177
+ # * At runtime: we similarly append subclass sizes when we unwrap subclass
178
+ # primals (but not tangents) on entry to the forward. See the runtime version of
179
+ # this function below.
180
+ def unwrap_tensor_subclasses(
181
+ wrapped_args: List[Union[Tensor, int]],
182
+ *,
183
+ append_symints: bool,
184
+ ):
185
+ def flatten_subclass(t: Union[Tensor, int], *, out=None):
186
+ # unwrap a subclass into plain tensors and their size/stride if "append_symint"
187
+ # is True
188
+ if not is_traceable_wrapper_subclass(t):
189
+ out.append(t)
190
+ return
191
+
192
+ attrs, _ = t.__tensor_flatten__()
193
+
194
+ for attr in attrs:
195
+ inner_tensor = getattr(t, attr)
196
+ flatten_subclass(inner_tensor, out=out)
197
+
198
+ if append_symints:
199
+ out.extend(filter_symints(t.size()))
200
+ out.extend(filter_symints(t.stride()))
201
+
202
+ xs_inner: List[Union[int, Tensor, SymInt]] = []
203
+
204
+ for x in wrapped_args:
205
+ flatten_subclass(typing.cast(Tensor, x), out=xs_inner)
206
+
207
+ return xs_inner
208
+
209
+
210
+ # subclass_metas is needed at runtime to compute which indices are symints in
211
+ # the outer_size/outer_stride
212
+ def runtime_unwrap_tensor_subclasses(
213
+ wrapped_args: List[Union[Tensor, int]],
214
+ *,
215
+ append_symints: bool,
216
+ subclass_metas: Optional[List[Union[PlainTensorMeta, SubclassCreationMeta]]] = None,
217
+ ):
218
+ def flatten_subclass(x: Tensor, meta: Optional[SubclassCreationMeta], *, out):
219
+ if not is_traceable_wrapper_subclass(x):
220
+ out.append(x)
221
+ return out
222
+
223
+ assert isinstance(x, Tensor)
224
+
225
+ attrs, _ = x.__tensor_flatten__()
226
+
227
+ for attr in attrs:
228
+ inner_tensor = getattr(x, attr)
229
+ inner_meta = meta.attrs.get(attr)
230
+ flatten_subclass(inner_tensor, inner_meta, out=out)
231
+
232
+ if append_symints:
233
+ assert isinstance(meta, SubclassCreationMeta)
234
+ # outer_size
235
+ size = x.size()
236
+ symint_placeholders = compute_symint_placeholders(meta.outer_size)
237
+ assert len(size) == len(symint_placeholders)
238
+ out.extend(
239
+ [r for (r, is_symint) in zip(size, symint_placeholders) if is_symint]
240
+ )
241
+
242
+ # outer_stride
243
+ stride = x.stride()
244
+ symint_placeholders = compute_symint_placeholders(meta.outer_stride)
245
+ assert len(stride) == len(symint_placeholders)
246
+ out.extend(
247
+ [r for (r, is_symint) in zip(stride, symint_placeholders) if is_symint]
248
+ )
249
+ return out
250
+
251
+ xs_inner: List[Union[int, Tensor, SymInt]] = []
252
+
253
+ if append_symints:
254
+ assert subclass_metas is not None
255
+
256
+ for idx, x in enumerate(wrapped_args):
257
+ if not is_traceable_wrapper_subclass(x):
258
+ xs_inner.append(x)
259
+ continue
260
+
261
+ if subclass_metas is None:
262
+ get_plain_tensors(typing.cast(Tensor, x), out=xs_inner)
263
+ else:
264
+ meta = subclass_metas[idx]
265
+ assert isinstance(meta, SubclassCreationMeta)
266
+ flatten_subclass(typing.cast(Tensor, x), meta, out=xs_inner)
267
+
268
+ return xs_inner
269
+
270
+
271
+ def unwrap_tensor_subclasses_with_indices_to_original(wrapped_args):
272
+ ret_unwrapped = []
273
+ ret_indices_to_original = []
274
+ for i, a in enumerate(wrapped_args):
275
+ a_unwrapped = unwrap_tensor_subclasses([a], append_symints=False)
276
+ ret_unwrapped.extend(a_unwrapped)
277
+ n = len(a_unwrapped)
278
+ ret_indices_to_original.extend([i] * n)
279
+
280
+ return ret_unwrapped, ret_indices_to_original
281
+
282
+
283
+ def remap_unwrapped_subclass_arg_indices(wrapped_args, static_input_indices):
284
+ static_input_indices = set(static_input_indices)
285
+ new_ind = 0
286
+ remapped_static_indices = []
287
+ for i, arg in enumerate(wrapped_args):
288
+ num_indices = 1
289
+ if is_traceable_wrapper_subclass(arg):
290
+ num_indices = (
291
+ len(get_plain_tensors(typing.cast(Tensor, arg), out=[]))
292
+ + len(filter_symints(arg.size()))
293
+ + len(filter_symints(arg.stride()))
294
+ )
295
+
296
+ for _ in range(num_indices):
297
+ if i in static_input_indices:
298
+ remapped_static_indices.append(new_ind)
299
+
300
+ new_ind += 1
301
+
302
+ return remapped_static_indices
303
+
304
+
305
+ # Turns a flattened list of tensor arguments into (maybe) subclass tensors.
306
+ # This function is used both at trace time and runtime, so we have an is_runtime flag telling us which context we're in.
307
+ def wrap_tensor_subclasses(
308
+ unwrapped_args: Union[Tuple[Any, ...], List[Any]],
309
+ *,
310
+ subclass_metas: List[Union[PlainTensorMeta, SubclassCreationMeta]],
311
+ num_fw_outs_saved_for_bw: Optional[int] = None,
312
+ included_subclass_symints: bool = False,
313
+ is_runtime: bool = False,
314
+ ) -> Tuple[Any, ...]:
315
+ wrapped_args = []
316
+ num_args_tallied = 0
317
+ for subclass_meta in subclass_metas:
318
+ if isinstance(subclass_meta, PlainTensorMeta):
319
+ wrapped_args.append(unwrapped_args[subclass_meta.unwrapped_idx])
320
+ num_args_tallied += 1
321
+ else:
322
+ assert isinstance(subclass_meta, SubclassCreationMeta)
323
+ assert subclass_meta.included_subclass_symints == included_subclass_symints
324
+ wrapped_args.append(
325
+ subclass_meta.creation_fn(unwrapped_args, is_runtime=is_runtime)
326
+ )
327
+ num_args_tallied += subclass_meta.arg_count
328
+
329
+ # Note: [Partitioner handling for Subclasses, Part 2]
330
+ # At the beginning of AOTAutograd, we collect metadata on the inputs and outputs of the user fw,
331
+ # to figure out which inputs/outputs are subclasses, and how to reconstruct the subclasses after flattening them.
332
+ #
333
+ # When this function is called at runtime in the forward,
334
+ # we have been passed a list of (flattened) dense-tensor fw-outs, and need to reconstruct any subclass fw outs.
335
+ #
336
+ # One reasonable question that you should ask: when should the dense_tensor -> subclass_tensor wrapping happen?
337
+ # Answer: we do it **inside of our compiled autograd.Function**.
338
+ # This seems like morally the right place: autograd happens above subclass desugaring,
339
+ # so autograd should see actual tensor subclasses at runtime, and not flattened dense tensors.
340
+ #
341
+ # This causes a tricky interaction though: when we run the min-cut partitioner to divvy up the joint graph
342
+ # into a forward and backward graph, we end up with some activations that show up as extra outputs
343
+ # in the compiled forward graph, that are **not** user outputs.
344
+ # These activations are not visible to the user, and so there's no need for us to wrap them back into subclasses.
345
+ #
346
+ # On top of that, when we first computed subclass metadata (in `run_functionalized_fw_and_collect_metadata`),
347
+ # we computed subclass metadata on every forward output, but this did **not** include activations
348
+ # created by the partitioner.
349
+ # as a result, `unwrapped_args` here will correspond to (*unwrapped_user_fw_outs, *activations),
350
+ # but `subclass_metas` will only correspond to subclass metatadata on `user_fw_outs`.
351
+ # We then need to make sure that we return (*wrapped_user_fw_outs, *activations).
352
+ if num_fw_outs_saved_for_bw is not None:
353
+ assert len(unwrapped_args) == num_args_tallied + num_fw_outs_saved_for_bw, (
354
+ f"Expected the number actual unwrapped-subclass outputs {len(unwrapped_args)} to equal "
355
+ f"the number of args calculated from subclasses ({num_args_tallied}) plus the number of "
356
+ f"additional activations saved for the backward pass ({num_fw_outs_saved_for_bw})"
357
+ )
358
+ activations = unwrapped_args[num_args_tallied:]
359
+ if isinstance(wrapped_args, tuple) and isinstance(activations, tuple):
360
+ return wrapped_args + activations
361
+ return tuple(list(wrapped_args) + list(activations))
362
+ else:
363
+ assert (
364
+ len(unwrapped_args) == num_args_tallied
365
+ ), f"Expected {len(unwrapped_args)} == {num_args_tallied}"
366
+ return tuple(wrapped_args)
367
+
368
+
369
+ # Given a bunch of "dense" tensor arguments, this function (potentially) wraps them into tensor subclasses.
370
+ # This function carefully handles the inference vs. joint cases:
371
+ # - when is_joint_structure is True, args is (primals, tangents)
372
+ # - when is_joint_structure is False, args is [*primals]
373
+ def wrap_tensor_subclasses_maybe_joint(
374
+ unwrapped_args, *, is_joint_structure: bool, meta: ViewAndMutationMeta
375
+ ) -> Union[Tuple[Any, ...], List[Any]]:
376
+ # Since this function is re-used for both inference and joint graphs,
377
+ if is_joint_structure:
378
+ assert isinstance(unwrapped_args, tuple) and len(unwrapped_args) == 2
379
+ assert isinstance(unwrapped_args[0], (tuple, list)) and isinstance(
380
+ unwrapped_args[1], (tuple, list)
381
+ )
382
+ primals, tangents = unwrapped_args[0], unwrapped_args[1]
383
+ wrapped_primals = wrap_tensor_subclasses(
384
+ primals,
385
+ subclass_metas=meta.subclass_inp_meta,
386
+ included_subclass_symints=True,
387
+ )
388
+ wrapped_tangents = wrap_tensor_subclasses(
389
+ tangents,
390
+ subclass_metas=meta.subclass_tangent_meta,
391
+ included_subclass_symints=False,
392
+ )
393
+ return (wrapped_primals, wrapped_tangents)
394
+ else:
395
+ wrapped_args = wrap_tensor_subclasses(
396
+ unwrapped_args,
397
+ subclass_metas=meta.subclass_inp_meta,
398
+ included_subclass_symints=True,
399
+ )
400
+ return wrapped_args
401
+
402
+
403
+ def compute_inner_mutated_inp_indices_from_subclass_meta(
404
+ fw_metadata: ViewAndMutationMeta,
405
+ inner_metadata: ViewAndMutationMeta,
406
+ ) -> List[int]:
407
+ # Note: [Recomputing subclass mutation handling]
408
+ #
409
+ # Generally, if a subclass requires grad, its components will not require grad.
410
+ # But for the purposes of tracking returned tensors, we should treat those component
411
+ # tensors as if they require grad.
412
+ #
413
+ # For example, if the subclass tensor requires grad and will be mutated in a way that
414
+ # requires us to handle the mutation outside of the graph, we need to return it
415
+ # from the forward graph. The inner_meta data won't consider the component tensors
416
+ # as if they need to be returned, because they don't require grad; but really, we
417
+ # should handle those tensors the same way we handle the subclass tensor itself; i.e.
418
+ # if we'd include the subclass tensor as part of the outputs, then we should also
419
+ # include the component tensors.
420
+ #
421
+ # To do this, we patch num_mutated_inp_runtime_indices below by expanding the inputs
422
+ # from the outer subclass tensors and propagating
423
+
424
+ updated_input_info = []
425
+ inner_idx = 0
426
+ if not fw_metadata.subclass_inp_meta:
427
+ # Sometimes we don't have subclass info, e.g. synthetic_base codepaths
428
+ return inner_metadata.mutated_inp_runtime_indices
429
+ assert len(fw_metadata.subclass_inp_meta) == len(fw_metadata.input_info)
430
+ for outer_idx, inp_meta in enumerate(fw_metadata.subclass_inp_meta):
431
+ if isinstance(inp_meta, PlainTensorMeta):
432
+ assert outer_idx < len(fw_metadata.input_info)
433
+ if inner_metadata is not None:
434
+ assert inner_idx < len(inner_metadata.input_info)
435
+ assert (
436
+ inner_metadata.input_info[inner_idx]
437
+ == fw_metadata.input_info[outer_idx]
438
+ )
439
+ updated_input_info.append(fw_metadata.input_info[outer_idx])
440
+ inner_idx += 1
441
+ else:
442
+ assert inp_meta.original_subclass is not None
443
+ for _ in range(inp_meta.arg_count):
444
+ updated_input_info.append(fw_metadata.input_info[outer_idx])
445
+ inner_idx += 1
446
+ if inner_metadata is not None:
447
+ assert len(inner_metadata.input_info) == len(updated_input_info)
448
+
449
+ return [
450
+ i
451
+ for i, inp in enumerate(updated_input_info)
452
+ if inp.mutation_type == MutationType.MUTATED_OUT_GRAPH
453
+ ]
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/traced_function_transforms.py ADDED
@@ -0,0 +1,897 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ This module is responsible for transforming functions to be traced into a form
4
+ that is easier for the downstream infra (e.g. Autograd, FX, AOTAutograd analysis)
5
+ to handle.
6
+
7
+ It does so by:
8
+ 1. functionalization (including RNG functionalzation)
9
+ 2. creating a joint graph when required
10
+ 3. transforming mutations into extra outputs
11
+ 4. dispatching subclasses
12
+ """
13
+
14
+ import warnings
15
+ from contextlib import contextmanager, nullcontext
16
+ from functools import wraps
17
+ from typing import Any, Callable, List, Tuple, Union
18
+ from unittest.mock import patch
19
+
20
+ import torch
21
+ import torch.fx.traceback as fx_traceback
22
+ import torch.utils._pytree as pytree
23
+ from torch import Tensor
24
+ from torch._decomp.decompositions_for_rng import PhiloxStateTracker
25
+ from torch._guards import detect_fake_mode
26
+ from torch._prims_common import CUDARngStateHelper
27
+ from torch.fx.experimental.proxy_tensor import (
28
+ maybe_disable_thunkify,
29
+ maybe_enable_thunkify,
30
+ )
31
+ from torch.fx.experimental.symbolic_shapes import (
32
+ definitely_false,
33
+ PropagateUnbackedSymInts,
34
+ sym_eq,
35
+ )
36
+ from torch.nn.utils import stateless
37
+
38
+ from .. import config
39
+ from .collect_metadata_analysis import run_functionalized_fw_and_collect_metadata
40
+ from .functional_utils import (
41
+ from_fun,
42
+ has_data_mutation,
43
+ has_metadata_mutation,
44
+ is_fun,
45
+ sync_functional_tensor,
46
+ to_fun,
47
+ was_inductor_storage_resized,
48
+ )
49
+ from .logging_utils import setup_stacktrace_preservation_hooks
50
+ from .schemas import (
51
+ AOTConfig,
52
+ MutationType,
53
+ OutputType,
54
+ SubclassMeta,
55
+ SubclassTracingInfo,
56
+ ViewAndMutationMeta,
57
+ )
58
+ from .subclass_utils import (
59
+ create_subclass_meta,
60
+ remap_unwrapped_subclass_arg_indices,
61
+ requires_subclass_dispatch,
62
+ unwrap_tensor_subclasses,
63
+ wrap_tensor_subclasses_maybe_joint,
64
+ )
65
+ from .utils import maybe_to_fresh_input
66
+
67
+
68
+ # This function returns a new function that returns mutated inputs as outputs.
69
+ # if keep_data_input_mutations is set, then we assume that data-only mutations
70
+ # will be left in the graph, and we only return metadata-mutated inputs as outputs.
71
+ def fn_input_mutations_to_outputs(
72
+ fn: Callable,
73
+ meta: ViewAndMutationMeta,
74
+ keep_data_input_mutations: bool,
75
+ ) -> Any:
76
+ @wraps(fn)
77
+ def inner_fn(*args):
78
+ outs = fn(*args)
79
+ assert len(meta.output_info) == len(outs)
80
+ # The compiled fw will return mutated input tensors, *including* metadata-only mutation.
81
+ # However, if keep_data_input_mutations is set, the compiled fw only needs to return metadata-mutated inputs.
82
+ # (because data-only input mutations are handled directly in the compiled graph)
83
+ mutated_inputs_to_return = [
84
+ x for (i, x) in enumerate(args) if i in meta.mutated_inp_runtime_indices
85
+ ]
86
+ return *mutated_inputs_to_return, *outs
87
+
88
+ return inner_fn
89
+
90
+
91
+ # This function takes in a fn with external aliasing and mutation,
92
+ # and returns a new fn with no external aliasing and mutation,
93
+ # as needed for autograd.
94
+ # The main transformations are:
95
+ # - Return mutated inputs as extra outputs
96
+ # - Clone mutated inputs that require gradients,
97
+ # because autograd will require us to pass the pre-mutated inputs into autograd.grad
98
+ # - Return intermediate bases of outputs as additional outputs,
99
+ # needed to appease autograd.Function
100
+ # The new function returns:
101
+ # (1) The updated outputs
102
+ # (2) A boolean mask of len(new_fn_outputs),
103
+ # that can be used to tell autograd.grad which outputs should get tangents
104
+ # if we trace the backward.
105
+ def fn_prepped_for_autograd(
106
+ fn: Callable,
107
+ meta: ViewAndMutationMeta,
108
+ ) -> Any:
109
+ @wraps(fn)
110
+ def inner_fn(*args):
111
+ args_maybe_cloned = [
112
+ maybe_to_fresh_input(i, t, meta) for i, t in enumerate(args)
113
+ ]
114
+
115
+ outs = fn(*args_maybe_cloned)
116
+ assert isinstance(outs, (tuple, list))
117
+ outs = list(outs)
118
+ assert len(meta.output_info) == len(outs)
119
+
120
+ mutated_inputs_to_return = [
121
+ x
122
+ for (i, x) in enumerate(args_maybe_cloned)
123
+ if i in meta.mutated_inp_runtime_indices
124
+ ]
125
+
126
+ intermediate_bases = []
127
+ for i, (o, info) in enumerate(zip(outs, meta.output_info)):
128
+ if info.output_type == OutputType.alias_of_intermediate_save_as_output:
129
+ intermediate_bases.append(o._base)
130
+
131
+ assert meta.num_intermediate_bases == len(intermediate_bases)
132
+
133
+ # the compiled forward should return (mutated_inputs, user_outs, intermediate_bases)
134
+ fw_outs_to_return = *mutated_inputs_to_return, *outs, *intermediate_bases
135
+
136
+ # Also return a boolean mask specifying which outputs to this function will be used as tangents
137
+ mutated_inputs_grad_mask = [
138
+ meta.input_info[meta.mutated_inp_runtime_indices[i]].mutates_data
139
+ and meta.input_info[meta.mutated_inp_runtime_indices[i]].requires_grad
140
+ for (i, x) in enumerate(mutated_inputs_to_return)
141
+ ]
142
+
143
+ # Pass any (non-aliased) outputs in as tangents, since they'll be returned as outputs in the fw
144
+ # For outputs that are aliases of intermediates, we will have returned the output's _base as an output in the graph instead,
145
+ # which we *should* send to grad()
146
+ output_grad_mask = [
147
+ meta.output_info[i].output_type
148
+ in [
149
+ OutputType.non_alias,
150
+ OutputType.unsafe_view_alias,
151
+ OutputType.custom_function_view,
152
+ ]
153
+ # Also, only tensor outputs should participate in the backward
154
+ # (in particular, Symint outputs in the forward graph shouldn't get tangents)
155
+ and issubclass(meta.output_info[i].raw_type, Tensor)
156
+ and meta.output_info[i].requires_grad
157
+ for (i, x) in enumerate(outs)
158
+ ]
159
+
160
+ intermediate_base_grad_mask = [True for _ in range(len(intermediate_bases))]
161
+
162
+ out_grad_mask = (
163
+ mutated_inputs_grad_mask + output_grad_mask + intermediate_base_grad_mask
164
+ )
165
+ assert len(out_grad_mask) == len(fw_outs_to_return)
166
+
167
+ # Take care to grab and sync the updated inputs from primals_after_cloning (the inputs we actually mutate!)
168
+ # and not primals (the preserved inputs, pre-mutation, that we pass to grad())
169
+ # This is annoying: our joint function needs to be aware of functionalization
170
+ # (syncing mutated inputs before calling autograd.grad())
171
+ # In theory, we could make the autograd engine do this automatically, although that probably isn't any cleaner.
172
+ for arg in args_maybe_cloned:
173
+ if not isinstance(arg, Tensor):
174
+ continue
175
+ sync_functional_tensor(arg)
176
+
177
+ return fw_outs_to_return, out_grad_mask
178
+
179
+ return inner_fn
180
+
181
+
182
+ # Given a fn, computes the joint.
183
+ # NOTE: fn is expects the following behavior:
184
+ # (1) fn() needs to return a tuple of (outs, mask),
185
+ # where `mask` tells us which outputs are meant to have tangents.
186
+ # we don't know this info automatically, because we don't actually want to blindly
187
+ # compute tangents for every output that requires grad.
188
+ # Specifically, outputs that alias inputs won't participate in the backward and get tangents.
189
+ # (2) fn() cannot mutate any inputs that require gradient.
190
+ # otherwise, when we compute autograd.grad(), we will not take those input mutations into account
191
+ # (the way this is handled is that we ensure any inputs that normally get mutated are cloned first)
192
+ def create_joint(fn: Callable, *, aot_config: AOTConfig) -> Any:
193
+ def inner_fn(primals: List[Any], tangents: List[Any]):
194
+ outs, tangent_mask = fn(*primals)
195
+
196
+ assert len(tangent_mask) == len(outs)
197
+ outs_to_grad = [
198
+ o for needs_tangent, o in zip(tangent_mask, outs) if needs_tangent
199
+ ]
200
+ assert len(outs_to_grad) == len(tangents)
201
+
202
+ # Get the inputs that need gradients
203
+ grad_primals = []
204
+ inputs_needs_grads = []
205
+ # Note that we're not using primals here,
206
+ # being carefully not to pass any mutated inputs into autograd.grad()
207
+ for p in primals:
208
+ is_grad_tensor = isinstance(p, Tensor) and p.requires_grad
209
+ inputs_needs_grads.append(is_grad_tensor)
210
+ if is_grad_tensor:
211
+ grad_primals.append(p)
212
+
213
+ # Get the outputs that need gradients
214
+ needed_outs = []
215
+ needed_tangents = []
216
+ for out, tangent in zip(outs_to_grad, tangents):
217
+ if isinstance(out, Tensor) and out.requires_grad:
218
+ # A bit sketchy, but fixes e.g. test_aot_autograd_exhaustive_matmul_cpu_float32
219
+ # The issue is that we are sensitive to decomps that don't accurately maintain
220
+ # their output's _base.shape compared to eager mode, and this helps mitigate a bit.
221
+ # The not definitely_false is also sketchy; if unbacked
222
+ # symints are involved, we're just going to assume that the
223
+ # decomps setup the base shape correctly
224
+ needed_outs.append(
225
+ out
226
+ if not definitely_false(sym_eq(out.shape, tangent.shape))
227
+ else out.view(tangent.shape)
228
+ )
229
+ needed_tangents.append(tangent)
230
+
231
+ setup_stacktrace_preservation_hooks([out.grad_fn for out in needed_outs])
232
+
233
+ if config.functionalize_rng_ops:
234
+ PhiloxStateTracker.mark_beginning_of_backward()
235
+ backward_out: Tuple[Tensor, ...] = ()
236
+ # Call the backwards pass
237
+ if grad_primals:
238
+ functional_tensor_mode = torch.utils._python_dispatch._detect_infra_mode(
239
+ torch._C._TorchDispatchModeKey.FUNCTIONAL
240
+ )
241
+ if functional_tensor_mode is not None:
242
+ # Side-Effect Tokens:
243
+ # We want to have independent chains of tokens for forward and backward.
244
+ # functional_tensor_mode._tokens is used by both.
245
+ # We memoize the result tokens of forward in functional_tensor_mode._tokens_forward_output,
246
+ # to return them as joint graph outputs.
247
+ # We clean functional_tensor_mode._tokens before backward, to prevent reuse of forward tokens in backward.
248
+ # Joint graph tracing allows tokens discovery,
249
+ # So all the tokens in backward will be created and added as a graph inputs during tracing.
250
+ functional_tensor_mode._tokens_forward_output = (
251
+ functional_tensor_mode._tokens
252
+ )
253
+ functional_tensor_mode._tokens = {}
254
+
255
+ with set_partitioner_tag_is_backward(), fx_traceback.preserve_node_meta():
256
+ # for full graph export, we always export a joint graph where we assume no tangents are needed.
257
+ if aot_config.no_tangents:
258
+ assert len(needed_tangents) == 1 and needed_tangents[0].numel() == 1
259
+ backward_out = torch.autograd.grad(
260
+ needed_outs,
261
+ grad_primals,
262
+ allow_unused=True,
263
+ )
264
+ else:
265
+ backward_out = torch.autograd.grad(
266
+ needed_outs,
267
+ grad_primals,
268
+ grad_outputs=needed_tangents,
269
+ allow_unused=True,
270
+ )
271
+ backward_out_iter = iter(backward_out)
272
+ return outs, [
273
+ next(backward_out_iter) if i else None for i in inputs_needs_grads
274
+ ]
275
+
276
+ def inner_fn_with_anomaly(*args):
277
+ with fx_traceback.preserve_node_meta(), warnings.catch_warnings():
278
+ warnings.filterwarnings("ignore", "Anomaly Detection has been enabled.")
279
+ with torch.autograd.detect_anomaly(check_nan=False):
280
+ return inner_fn(*args)
281
+
282
+ return inner_fn_with_anomaly
283
+
284
+
285
+ def create_functionalized_rng_ops_wrapper(func, args, trace_joint=True) -> Any:
286
+ # Functionalization of rng ops changes the calling convention of the joint graph.
287
+ # It goes from (primals, tangents) to (seed, offset, primals, tangents)
288
+ # At runtime, we pass on the current seed and offset. This is hidden from
289
+ # the user.
290
+ fake_mode = detect_fake_mode()
291
+ if fake_mode is None:
292
+ fake_mode = nullcontext()
293
+
294
+ def override_get_rng_state(device: Union[int, str, torch.device] = "cuda"):
295
+ out = PhiloxStateTracker.get_state_as_tensor()
296
+ return out
297
+
298
+ def override_set_rng_state(x, device: Union[int, str, torch.device] = "cuda"):
299
+ PhiloxStateTracker.set_state_from_tensor(x)
300
+
301
+ def append_rng_offsets(args):
302
+ if trace_joint:
303
+ # args signature before: Tuple(fwd_outputs), Tuple(bwd_outputs)
304
+ # args signature after: Tuple(fwd_outputs, new_fwd_rng_offset), Tuple(bwd_offset, new_bwd_rng_offset)
305
+ return (
306
+ (*args[0], PhiloxStateTracker.get_updated_fwd_offset()),
307
+ (*args[1], PhiloxStateTracker.get_updated_bwd_offset()),
308
+ )
309
+ else:
310
+ # args signature before: Tuple(fwd_outputs)
311
+ # args signature after: Tuple(fwd_outputs, new_fwd_rng_offset)
312
+ return (*args, PhiloxStateTracker.get_updated_fwd_offset())
313
+
314
+ def traced_joint(
315
+ primals, tangents, fwd_seed, fwd_base_offset, bwd_seed, bwd_base_offset
316
+ ):
317
+ with patch("torch.cuda.get_rng_state", override_get_rng_state), patch(
318
+ "torch.cuda.set_rng_state", override_set_rng_state
319
+ ):
320
+ return append_rng_offsets(func(primals, tangents))
321
+
322
+ def traced_forward(*primals_fwd_seed_fwd_base_offset):
323
+ # The signature is (*primals, seed, offset)
324
+ with patch("torch.cuda.get_rng_state", override_get_rng_state), patch(
325
+ "torch.cuda.set_rng_state", override_set_rng_state
326
+ ):
327
+ return append_rng_offsets(func(*primals_fwd_seed_fwd_base_offset[:-2]))
328
+
329
+ if trace_joint:
330
+ # Get the current seed and offset to setup tracing.
331
+ fwd_seed, fwd_base_offset = CUDARngStateHelper.get_torch_state_as_tuple(
332
+ fake_mode
333
+ )
334
+ bwd_seed, bwd_base_offset = CUDARngStateHelper.get_torch_state_as_tuple(
335
+ fake_mode
336
+ )
337
+ PhiloxStateTracker.record_state(fwd_seed, fwd_base_offset, "forward")
338
+ PhiloxStateTracker.record_state(bwd_seed, bwd_base_offset, "backward")
339
+ return traced_joint, (
340
+ *args,
341
+ fwd_seed,
342
+ fwd_base_offset,
343
+ bwd_seed,
344
+ bwd_base_offset,
345
+ )
346
+ else:
347
+ # Get the current seed and offset to setup tracing.
348
+ fwd_seed, fwd_base_offset = CUDARngStateHelper.get_torch_state_as_tuple(
349
+ fake_mode
350
+ )
351
+ PhiloxStateTracker.record_state(fwd_seed, fwd_base_offset, "forward")
352
+ return traced_forward, (*args, fwd_seed, fwd_base_offset)
353
+
354
+
355
+ @contextmanager
356
+ def set_partitioner_tag(tag: str):
357
+ meta_key = "partitioner_tag"
358
+ assert fx_traceback.has_preserved_node_meta()
359
+
360
+ original_val = fx_traceback.current_meta.get(meta_key, None)
361
+ fx_traceback.current_meta[meta_key] = tag
362
+ try:
363
+ yield
364
+ finally:
365
+ fx_traceback.current_meta[meta_key] = original_val
366
+
367
+
368
+ def set_partitioner_tag_is_backward():
369
+ return set_partitioner_tag("is_backward")
370
+
371
+
372
+ def set_partitioner_tag_must_be_in_backward():
373
+ return set_partitioner_tag("must_be_in_backward")
374
+
375
+
376
+ # This creates the final function that we want to trace using make_fx(),
377
+ # in both aot_dispatch_autograd and aot_dispatch_base.
378
+ # Preconditions:
379
+ # - fn corresponds to the user's fw function
380
+ # - fn arguments have been flattened, duplicate arguments have been handled
381
+ # - In the returned function, the "primals" arguments *includes* synthetic bases.
382
+ # This function does the work of functionalizing the input function,
383
+ # and performing copy_() calls at the end of the function if `keep_input_mutations` is set.
384
+ # The function returned has signature that is either:
385
+ # (1) "traced_fn(primals: List[Any])" if trace_joint is False
386
+ # (2) "traced_fn(primals: List[Any], tangents: List[Any])" if trace_joint is True
387
+ # Returns a new (functionalized) function, and updated arguments to call it with.
388
+ def create_functionalized_fn(
389
+ fn,
390
+ args,
391
+ *,
392
+ meta: ViewAndMutationMeta,
393
+ aot_config: AOTConfig,
394
+ trace_joint: bool,
395
+ ) -> Any:
396
+ @wraps(fn)
397
+ def _functionalized_f_helper(*args):
398
+ with maybe_enable_thunkify():
399
+ # See Note [Disabling Functionalize TLS Above Python Functionalization]
400
+ disable_above = torch._C._ExcludeDispatchKeyGuard(
401
+ torch._C.DispatchKeySet(torch._C.DispatchKey.Functionalize)
402
+ )
403
+
404
+ with disable_above:
405
+ # The functionalization code here can potentially trigger traces
406
+ # into the graph, but we'd prefer to NOT do this, because if we
407
+ # trace them now, we will end up with FX nodes that don't have
408
+ # module stack annotations, which makes unflattener unhappy.
409
+ # Wrap inputs into functional wrappers
410
+ f_args = pytree.tree_map(to_fun, args)
411
+
412
+ # Run the joint
413
+ f_outs = fn(*f_args)
414
+
415
+ if trace_joint:
416
+ # We support a limited amount of mutation of graph inputs during the backward pass.
417
+ # (This is used e.g. by Float8, which needs to update buffers during the backward pass)
418
+ # Here, we perform extra checks for primals that were mutated in the **backward**
419
+ # We're doing the checks here instead of doing them with the rest of the input mutation handling because:
420
+ # - We need to detect inputs that were mutated in the backward **separately** from mutations that happened
421
+ # during the forward, because the handling is different: some input mutations from the the forward
422
+ # can be only handled in a fw-only runtime epilogue, and in theory if we wanted to handle those same
423
+ # types of mutations in the backward we would need a bw-only runtime epilogue.
424
+ # - We could in theory have our analysis pass differentiate mutations in the fw from mutations in
425
+ # the bw by running our analysis first on the fw-only graph, and then on the joint graph. This would
426
+ # require an extra round of tracing though, so it's more efficient to do in-line here.
427
+ assert (
428
+ isinstance(args, tuple)
429
+ and len(args) == 2
430
+ and isinstance(args[0], (list, tuple))
431
+ )
432
+ # Only look at mutations that happened to forward inputs (e.g. fw buffers that were saved for bw)
433
+ primals_before = args[0]
434
+ primals_after = pytree.tree_map(from_fun, f_args[0])
435
+ for idx, (f_inpt, before, after, inpt_info) in enumerate(
436
+ zip(f_args[0], primals_before, primals_after, meta.input_info)
437
+ ):
438
+ # Store information about mutations in joint(for backward analysis)
439
+ joint_mutates_data = has_data_mutation(f_inpt)
440
+
441
+ joint_mutates_metadata = has_metadata_mutation(
442
+ f_inpt, before, check_only_storage_mutation=False
443
+ )
444
+
445
+ # Ban metadata mutations on fw inputs during the bw
446
+ if not inpt_info.mutates_metadata:
447
+ assert (
448
+ not joint_mutates_metadata
449
+ ), "Found a graph input that had its metadata mutated in the backward. This is not supported"
450
+
451
+ # Ban storage resizing on fw inputs during the bw
452
+ if not inpt_info.mutation_inductor_storage_resize:
453
+ assert not was_inductor_storage_resized(
454
+ f_inpt
455
+ ), "Found a graph input that had storage resizing in the backward. This is not supported"
456
+
457
+ # Allow data mutations on fw inputs during the bw, but only if they do not require grad
458
+ # So we can guarantee that we can keep the mutations in the graph
459
+ if (
460
+ joint_mutates_data
461
+ and not inpt_info.mutates_data
462
+ and not inpt_info.mutates_storage_metadata
463
+ ):
464
+ # Not banning here mutations on inpt_info.requires_grad -
465
+ # we'll check at runtime and fail only when backward is under torch.is_grad_enabled (create_graph)
466
+ # Add node meta for copy_ for partitioner that this node should be in backward graph.
467
+ with torch.fx.traceback.preserve_node_meta(), set_partitioner_tag_must_be_in_backward():
468
+ before.copy_(after)
469
+ meta.indices_of_inputs_that_requires_grad_with_mutations_in_bw.append(
470
+ idx
471
+ )
472
+ # Now that we covered mutations to *forward* inputs during the backward,
473
+ # we also need to cover mutations to *backward-only* inputs during the backward (e.g. mutation to a grad_out).
474
+ # Today, we will just error in all cases of this happening unless someone needs us to support it.
475
+ tangents_before = args[1]
476
+ tangents_after = pytree.tree_map(from_fun, f_args[1])
477
+ for f_inpt, before, after in zip(
478
+ f_args[1], tangents_before, tangents_after
479
+ ):
480
+ assert not has_metadata_mutation(
481
+ f_inpt, before, check_only_storage_mutation=False
482
+ ) and not has_data_mutation(
483
+ f_inpt
484
+ ), "Found an input to the backward that was mutated during the backward pass. This is not supported"
485
+
486
+ if aot_config.keep_inference_input_mutations:
487
+ # Note: This is a bit annoying. There's a layering issue here, where:
488
+ # (1) functionalization needs to operate on **synthetic base** inputs, before unpacking them into the "real" inputs.
489
+ # (2) For keep_input_mutations, we support tracing a call to copy_() directly on mutated inputs.
490
+ # However, we **only** want to support this for inputs that have data-only (and no metadata) mutations,
491
+ # because inductor (and backends in generally) would prefer not to see these (e.g. as_strided_(), resize_()).
492
+ # This makes it pretty difficult for this logic to operate on synthetic bases.
493
+ # (3) In addition, there are cases where it's significantly cheaper to perform the copy on the individual
494
+ # (unpacked) input aliases, instead of the synthetic base.
495
+ # Example case where (3) could be important:
496
+ #
497
+ # def f(x, y):
498
+ # x.mul_(2)
499
+ # y.mul_(3)
500
+ # return x, y
501
+ # a = torch.ones(1'000'000)
502
+ # x, y = out(a[0:9], a[1:10])
503
+ #
504
+ # It would be much better to add copy_() calls into the graph for the two tiny slices, instead of materializing
505
+ # a giant "updated synthetic base" and copying into a's entire storage.
506
+ #
507
+ # For now, we are pessimistically not performing the optimization from (3);
508
+ # we will materialize an "updated" synthetic base, and copy it back to the synthetic input base.
509
+ # This allows us to factor aot autograd much more nicely, since only one area of the code needs to worry
510
+ # about synthetic bases.
511
+ for i, (inpt_old, inpt_f) in enumerate(
512
+ zip(args, f_args) if not trace_joint else zip(args[0], f_args[0])
513
+ ):
514
+ if not isinstance(inpt_f, torch.Tensor):
515
+ continue
516
+ assert is_fun(inpt_f)
517
+ inpt_new = from_fun(inpt_f)
518
+ if (
519
+ meta.input_info[i].mutation_type
520
+ == MutationType.MUTATED_IN_GRAPH
521
+ ):
522
+ # See Note [set_() Input Mutations in AOTAutograd]
523
+ # all mutations on the input must be under no_grad, so it is safe to put in the graph
524
+ # Here, we're saying that if an input experienced a set call, inp.set_(other),
525
+ # then we can effectively not have to worry about whether its data was mutated.
526
+ # There are 3 cases:
527
+ # (1) We mutate inp *after* the set_() call. other is a graph intermediate.
528
+ # In this case, we're not really mutating the input storage of "inp";
529
+ # we're mutating the storage of an intermdiate value (other),
530
+ # and slamming that storage into the input tensor. So no data mutation is necessary.
531
+ # (2) We mutate inp *after* the set_() call. other is a graph *input*.
532
+ # In this case, the data mutation will be properly handled in the runtime
533
+ # epilogue during the processing of "other"
534
+ # (3) We mutate inp *before* the set_() call.
535
+ # This case is *not* currently handled.
536
+ if meta.input_info[i].mutates_storage_metadata:
537
+ with torch.no_grad():
538
+ inpt_old.set_(inpt_new)
539
+
540
+ # Note [Ordering of resize_() and set_()]
541
+ # Importantly: the common usage in FSDP is that we have a dummy parameter
542
+ # that sees a set_() and **Then** a resize_().
543
+ # We must put those mutations into the graph in the same order,
544
+ # Since running them in the opposite order will have different behavior.
545
+ # We fully ban resize_() followed by set_() for now, although in principal
546
+ # we could support this
547
+ if meta.input_info[i].mutation_inductor_storage_resize:
548
+ # resizing is not supported on subclasses (we error earlier if this happens)
549
+ from torch._subclasses.functional_tensor import (
550
+ FunctionalTensor,
551
+ )
552
+
553
+ assert isinstance(inpt_f, FunctionalTensor)
554
+ old_storage_size = torch._functionalize_get_storage_size( # type: ignore[attr-defined]
555
+ inpt_f.elem, before=True
556
+ )
557
+ new_storage_size = torch._functionalize_get_storage_size( # type: ignore[attr-defined]
558
+ inpt_f.elem, before=False
559
+ )
560
+ if old_storage_size != new_storage_size:
561
+ assert (
562
+ old_storage_size == 0 or new_storage_size == 0
563
+ ), f"""\
564
+ Encountered a storage resize during tracing on input {i}. Old nbytes={old_storage_size}, new nbytes={new_storage_size}
565
+ We only support storage resizing on graph inputs as long as the input either starts or ends with a storage size of 0
566
+ (the case for FSDP)"""
567
+ torch.ops.inductor.resize_storage_bytes_(
568
+ inpt_old, new_storage_size
569
+ )
570
+ if new_storage_size == 0:
571
+ # Even if we marked the input as having a data mutation (thus needing a copy_()),
572
+ # We should **ignore** it if our input has no storage
573
+ # (this can happen if, e.g. we temporarily resize our input, copy data into it,
574
+ # and resize it back down to zero)
575
+ continue
576
+ # Optimization: if the copy_() is a no-op then don't include it in the graph.
577
+ # In theory inductor could optimize this away, however in fsdp, we end up with
578
+ # param.copy_(param), where param is a zero-storage-size tensor,
579
+ # and running this op in eager mode (using the aot_eager backend) will result in a segfault.
580
+ # So we may as well optimize it away here.
581
+ if inpt_old is inpt_new:
582
+ # (This check needs to be done after putting resize_() in the graph,
583
+ # since a resize_(0) doesn't actually change the FunctionalTensor's inner tensor)
584
+ continue
585
+ # We found an input that had a (data-only) mutation.
586
+ # Since keep_input_mutations is set, we need to faithfully apply a copy_()
587
+ # so the compiler will see the input mutation in the graph.
588
+ if (
589
+ meta.input_info[i].mutates_data
590
+ and meta.input_info[i].mutations_hidden_from_autograd
591
+ ):
592
+ # Hidden from autograd = run under no_grad, **and** don't bump VC
593
+ # (although if the tensor was created in inference mode, it has no VC)
594
+ if inpt_old.is_inference():
595
+ maybe_preserve_vc = nullcontext()
596
+ else:
597
+ maybe_preserve_vc = torch.autograd._unsafe_preserve_version_counter(
598
+ inpt_old # type: ignore[assignment]
599
+ )
600
+ with torch.no_grad(), maybe_preserve_vc:
601
+ inpt_old.copy_(inpt_new)
602
+ elif (
603
+ meta.input_info[i].mutates_data
604
+ and meta.input_info[
605
+ i
606
+ ].mutations_under_no_grad_or_inference_mode
607
+ ):
608
+ # Under no_grad = run under no_grad (we still bump the VC though)
609
+ # (inference_mode will also bump the VC, as long as the tensor in question
610
+ # was created outside of inference_mode)
611
+ with torch.no_grad():
612
+ inpt_old.copy_(inpt_new)
613
+ elif meta.input_info[i].mutates_data:
614
+ inpt_old.copy_(inpt_new)
615
+
616
+ # When an output tensor is a functionalized mutated input, and we
617
+ # were able to move the mutation in to the graph then we can return
618
+ # the mutated input directly. This prevents duplicating the
619
+ # tensors contents.
620
+ flat_outs, outs_spec = pytree.tree_flatten(f_outs)
621
+ flat_outs = [from_fun(o) for o in flat_outs]
622
+ num_outs = len(meta.output_info)
623
+
624
+ for i, outp in enumerate(flat_outs[:num_outs]):
625
+ info = meta.output_info[i]
626
+ if info.output_type != OutputType.is_input:
627
+ continue
628
+
629
+ assert info.base_idx is not None
630
+ if (
631
+ meta.input_info[info.base_idx].mutation_type
632
+ == MutationType.MUTATED_IN_GRAPH
633
+ ):
634
+ fw_args = args[0] if trace_joint else args
635
+ flat_outs[i] = fw_args[info.base_idx]
636
+ return pytree.tree_unflatten(flat_outs, outs_spec)
637
+
638
+ return pytree.tree_map(from_fun, f_outs)
639
+
640
+ # Kinda annoying, but needed to make sure that the fx graph we trace out has "primals"
641
+ # and "tangents" as its input names (which are special-cased by the partitioner)
642
+ # TODO (tmanlaibaatar) revisit this if we ever need to turn on non-strict joint graph export
643
+ def joint_helper(primals, tangents):
644
+ return _functionalized_f_helper(primals, tangents)
645
+
646
+ helper = joint_helper if trace_joint else _functionalized_f_helper
647
+ if config.functionalize_rng_ops:
648
+ # Setup the wrapper for functionalization of rng ops
649
+ helper, args = create_functionalized_rng_ops_wrapper(helper, args, trace_joint)
650
+
651
+ return helper, args
652
+
653
+
654
+ def handle_effect_tokens_fn(
655
+ fn,
656
+ args,
657
+ *,
658
+ meta: ViewAndMutationMeta,
659
+ trace_joint: bool,
660
+ ) -> Any:
661
+ num_tokens = len(meta.tokens)
662
+
663
+ @wraps(fn)
664
+ def inner_fn(*args):
665
+ # See Note [Disabling Functionalize TLS Above Python Functionalization]
666
+ disable_above = torch._C._ExcludeDispatchKeyGuard(
667
+ torch._C.DispatchKeySet(torch._C.DispatchKey.Functionalize)
668
+ )
669
+
670
+ with disable_above:
671
+ # See Note [Side-Effectful Tokens in AOTAutograd]
672
+ if trace_joint:
673
+ assert isinstance(args, tuple) and isinstance(args[0], (list, tuple))
674
+ tokens = args[0][:num_tokens]
675
+ assert all(token.numel() == 0 for token in tokens)
676
+ args = (args[0][num_tokens:], *args[1:])
677
+ else:
678
+ tokens = args[:num_tokens]
679
+ assert all(token.numel() == 0 for token in tokens)
680
+ args = args[num_tokens:]
681
+
682
+ # Populate the current FunctionalTensorMode with the tokens per
683
+ # operator. See Note [FunctionalTensorMode is Stateful]
684
+ functional_tensor_mode = torch.utils._python_dispatch._detect_infra_mode(
685
+ torch._C._TorchDispatchModeKey.FUNCTIONAL
686
+ )
687
+ assert functional_tensor_mode is not None
688
+ f_tokens = pytree.tree_map(to_fun, tokens)
689
+ for i, k in enumerate(meta.tokens.keys()):
690
+ functional_tensor_mode._tokens[k] = f_tokens[i]
691
+
692
+ # Run the joint
693
+ outs = fn(*args)
694
+
695
+ # Return both the tokens and the outputs
696
+ # See Note [Side-Effectful Tokens in AOTAutograd]
697
+ if trace_joint:
698
+ assert len(outs) == 2
699
+ assert len(functional_tensor_mode._tokens_forward_output) == num_tokens
700
+ fwd_out_tokens = functional_tensor_mode._tokens_forward_output.values()
701
+
702
+ bwd_out_tokens = functional_tensor_mode._tokens.values()
703
+
704
+ f_fwd_out_tokens = [from_fun(t) for t in fwd_out_tokens]
705
+ f_bwd_out_tokens = [from_fun(t) for t in bwd_out_tokens]
706
+
707
+ meta.num_backward_tokens = len(bwd_out_tokens)
708
+ return ((*f_fwd_out_tokens, *outs[0]), (*outs[1], *f_bwd_out_tokens))
709
+
710
+ out_tokens = [from_fun(t) for t in functional_tensor_mode._tokens.values()]
711
+ return (*out_tokens, *outs)
712
+
713
+ # Additionally pass in tokens as inputs
714
+ # See Note [Side-Effectful Tokens in AOTAutograd]
715
+ additional_fwd_token_inputs = [torch.tensor([])] * num_tokens
716
+
717
+ if trace_joint:
718
+ args = ([*additional_fwd_token_inputs, *args[0]], *args[1:])
719
+ else:
720
+ args = [*additional_fwd_token_inputs, *args]
721
+ return inner_fn, args
722
+
723
+
724
+ # Given a function operating on Subclass -> Subclass, returns an function that operates on Tensor -> Tensor
725
+ # Also returns:
726
+ # - the new set of arguments to pass into this function (now that tensor subclasses have been eliminated)
727
+ # - the updated ViewAndMutationMeta for this dense -> dense function.
728
+ # The other important arguments are:
729
+ # - flat_fn_maybe_joint: when is_joint_structure=True, this is the joint fw-bw function.
730
+ # when is_joint_structure=False, this is just the forward function.
731
+ # - fw_only: this is *always* the forward-only function.
732
+ # Why do we need this? We need to collect updated ViewAndMutationMeta on our new dense -> dense functions.
733
+ # In particular, we need this to tell the partitioner how many dense forward outputs there are.
734
+ def aot_dispatch_subclass(
735
+ flat_fn_maybe_joint,
736
+ args: List[Any],
737
+ *,
738
+ is_joint_structure: bool,
739
+ meta: ViewAndMutationMeta,
740
+ fw_only: Callable,
741
+ ) -> SubclassTracingInfo:
742
+ # Skip logic if we don't need to trace through any subclasses
743
+ req_subclass_dispatch = requires_subclass_dispatch(args, meta)
744
+ if not req_subclass_dispatch:
745
+ return SubclassTracingInfo(
746
+ plain_tensor_trace_fn=flat_fn_maybe_joint,
747
+ plain_tensor_args=args,
748
+ maybe_subclass_meta=None,
749
+ )
750
+
751
+ # TODO: add subclass guards (later PR).
752
+
753
+ # What's going on here? We need to compute subclass metadata about the outputs of the joint (grad_inputs).
754
+ # Annoying: we don't know the grad input metas until we're in the middle of tracing the joint,
755
+ # so we set it later, while we're tracing the joint (see inner_fn() below).
756
+ # Another option would be to run our run_functionalized_fw_and_collect_metadata() function
757
+ # directly on the joint, but this would hurt compile time (adding yet another pass through the joint).
758
+ subclass_meta = SubclassMeta()
759
+
760
+ def inner_fn(fn, args, *, use_trace_joint: bool):
761
+ # Step 1: wrap tensor inputs into subclasses if necessary
762
+ all_args = wrap_tensor_subclasses_maybe_joint(
763
+ args, is_joint_structure=use_trace_joint, meta=meta
764
+ )
765
+
766
+ # Step 2: call the inner function, with our (maybe subclass) inputs
767
+ wrapped_outs = fn(*all_args)
768
+
769
+ if use_trace_joint:
770
+ # See Note: [Computing Subclass Metadata about grad_inputs]
771
+ # We also stash subclass info on our grad_inputs, if we're tracing the joint.
772
+ nonlocal subclass_meta
773
+ assert isinstance(wrapped_outs, tuple) and len(wrapped_outs) == 2
774
+ # Don't need fw outs since we already have subclass metadata on them
775
+ grad_inputs = wrapped_outs[1]
776
+ subclass_meta.grad_input_metas = create_subclass_meta(grad_inputs)
777
+
778
+ # Add extra symints as outputs to the forward/backward graphs
779
+ # ignore nested ints here
780
+ forward_outs = unwrap_tensor_subclasses(
781
+ wrapped_outs[0], append_symints=True
782
+ )
783
+ # ignore nested ints here
784
+ backward_outs = unwrap_tensor_subclasses(
785
+ wrapped_outs[1], append_symints=True
786
+ )
787
+ return (forward_outs, backward_outs)
788
+
789
+ # Step 3: Unwrap any subclass outputs back into dense tensors
790
+ unwrapped_outs = unwrap_tensor_subclasses(wrapped_outs, append_symints=True)
791
+ return unwrapped_outs
792
+
793
+ def joint_fn(primals, tangents):
794
+ with maybe_enable_thunkify():
795
+ return inner_fn(
796
+ flat_fn_maybe_joint, (primals, tangents), use_trace_joint=True
797
+ )
798
+
799
+ def fw_fn(*primals):
800
+ with maybe_enable_thunkify():
801
+ return inner_fn(flat_fn_maybe_joint, primals, use_trace_joint=False)
802
+
803
+ def metadata_fn(*primals):
804
+ return inner_fn(fw_only, primals, use_trace_joint=False)
805
+
806
+ if is_joint_structure:
807
+ args_unwrapped = (
808
+ # Add extra symints (size/strides) as input to the forward graph
809
+ unwrap_tensor_subclasses(args[0], append_symints=True),
810
+ # We pass append_symints=False here because the partitioner will
811
+ # capture and add any extra argument
812
+ unwrap_tensor_subclasses(args[1], append_symints=False),
813
+ )
814
+ else:
815
+ args_unwrapped = unwrap_tensor_subclasses(args, append_symints=True)
816
+ remapped_static_indices = remap_unwrapped_subclass_arg_indices(
817
+ args, meta.static_input_indices
818
+ )
819
+
820
+ if is_joint_structure:
821
+ primals_unwrapped = args_unwrapped[0]
822
+ fn_to_trace = joint_fn
823
+ else:
824
+ primals_unwrapped = args_unwrapped
825
+ fn_to_trace = fw_fn
826
+
827
+ # Note: [Partitioner handling for Subclasses, Part 1]
828
+ # The way the partitioner works is that:
829
+ # (1) we pass is a single graph containing the joint fw/bw,
830
+ # where the # of graph outputs corresponds to # fw_outputs + # grad_inputs
831
+ # (2) The partitioner accepts an arguments, num_fwd_outputs,
832
+ # and assumes that the first "num_fwd_outputs" graph outputs correspond
833
+ # to outputs of the forward graph.
834
+ # How do tensor subclasses enter the picture?
835
+ # the num_fwd_outputs in the final graph is actually non-trivial to compute,
836
+ # because it can be influenced by input mutations and intermediate bases.
837
+ # So we compute it by inspecting the current ViewAndMutationMeta object.
838
+ # However, the original ViewAndMutationMeta that we computed was created
839
+ # on the subclass -> subclass graph,
840
+ # which can have a different number of outputs than the dense -> dense graph.
841
+ # That's why we created a fresh metadata object on the dense -> dense function here,
842
+ # and plumb it back up to the partitioner.
843
+ # See Note: [Partitioner handling for Subclasses, Part 2] for more info.
844
+ meta_updated = run_functionalized_fw_and_collect_metadata(
845
+ metadata_fn,
846
+ static_input_indices=remapped_static_indices,
847
+ keep_input_mutations=meta.keep_input_mutations,
848
+ is_train=meta.is_train,
849
+ )(*primals_unwrapped)
850
+
851
+ subclass_meta.fw_metadata = meta_updated
852
+
853
+ return SubclassTracingInfo(
854
+ plain_tensor_trace_fn=fn_to_trace,
855
+ plain_tensor_args=args_unwrapped,
856
+ maybe_subclass_meta=subclass_meta,
857
+ )
858
+
859
+
860
+ def create_functional_call(mod, params_spec, params_len, store_orig_mod=False):
861
+ # Redundant with dynamo, but worth having in case this gets invoked elsewhere.
862
+ # https://github.com/pytorch/pytorch/issues/103569
863
+
864
+ def functional_call(*args, **kwargs):
865
+ with stateless._reparametrize_module(
866
+ mod, pytree.tree_unflatten(args[:params_len], params_spec)
867
+ ), maybe_disable_thunkify():
868
+ if isinstance(mod, torch.fx.GraphModule):
869
+ with fx_traceback.preserve_node_meta(), warnings.catch_warnings():
870
+ warnings.filterwarnings(
871
+ "ignore", "Anomaly Detection has been enabled."
872
+ )
873
+ with torch.autograd.detect_anomaly(check_nan=False):
874
+ detect_fake_mode().epoch += 1
875
+ out = PropagateUnbackedSymInts(mod).run(
876
+ *args[params_len:], **kwargs
877
+ )
878
+ else:
879
+ out = mod(*args[params_len:], **kwargs)
880
+
881
+ if not isinstance(out, (tuple, list)):
882
+ raise RuntimeError(
883
+ "Graph output must be a (). This is so that we can avoid "
884
+ "pytree processing of the outputs. Please change the module to "
885
+ "have tuple outputs or use aot_module instead."
886
+ )
887
+ return out
888
+
889
+ # Note [Preserving the nn module stack metadata during export non-strict mode]
890
+ # This path is currently only used by the non-strict export flow,
891
+ # where we cannot rely on dynamo to preserve nn stack metadata in our captured graph.
892
+ # Instead, we stash the original user nn module here, and rely on `make_fx` to grab
893
+ # this stashed module and use it to track nn module stack metadata
894
+ if store_orig_mod and not hasattr(functional_call, "_orig_mod"):
895
+ functional_call._orig_mod = mod # type: ignore[attr-defined]
896
+
897
+ return functional_call
lib/python3.12/site-packages/torch/_functorch/_aot_autograd/utils.py ADDED
@@ -0,0 +1,492 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ Contains various utils for AOTAutograd, including those for handling collections.
4
+ """
5
+
6
+ import dataclasses
7
+ import operator
8
+ import warnings
9
+ from contextlib import nullcontext
10
+ from functools import wraps
11
+ from typing import Any, Callable, List, Optional, Tuple, Union
12
+
13
+ import torch
14
+ import torch.utils._pytree as pytree
15
+ from torch._library.fake_class_registry import FakeScriptObject
16
+ from torch._logging import getArtifactLogger
17
+ from torch._subclasses.fake_tensor import FakeTensor
18
+ from torch._subclasses.functional_tensor import FunctionalTensor
19
+ from torch.fx.experimental._backward_state import BackwardState
20
+ from torch.fx.experimental.proxy_tensor import py_sym_types
21
+
22
+
23
+ KNOWN_TYPES = [
24
+ torch.Tensor,
25
+ BackwardState,
26
+ int,
27
+ str,
28
+ float,
29
+ bool,
30
+ type(None),
31
+ *py_sym_types,
32
+ FakeScriptObject,
33
+ torch.ScriptObject,
34
+ ]
35
+
36
+ original_zip = zip
37
+
38
+ aot_graphs_effects_log = getArtifactLogger(__name__, "aot_graphs_effects")
39
+
40
+
41
+ def strict_zip(*iterables, strict=True, **kwargs):
42
+ if not strict:
43
+ return original_zip(*iterables, **kwargs)
44
+
45
+ length = len(iterables[0])
46
+ for iterable in iterables[1:]:
47
+ if len(iterable) != length:
48
+ raise ValueError(
49
+ "The iterables have different lengths and strict mode is enabled."
50
+ )
51
+
52
+ return original_zip(*iterables, **kwargs)
53
+
54
+
55
+ def _get_symint_hints(exprs):
56
+ """
57
+ Get the hints of a list/tuple of int/SymInt.
58
+ """
59
+ if isinstance(exprs, (list, tuple)):
60
+ return type(exprs)(_get_symint_hints(e) for e in exprs)
61
+ elif isinstance(exprs, torch.SymInt):
62
+ return exprs.node.shape_env.size_hint(exprs.node.expr)
63
+ else:
64
+ return exprs
65
+
66
+
67
+ def partial_flatten_asdict(obj: Any) -> Any:
68
+ if dataclasses.is_dataclass(obj):
69
+ return {
70
+ field.name: getattr(obj, field.name) for field in dataclasses.fields(obj)
71
+ }
72
+ elif isinstance(obj, (list, tuple)):
73
+ return obj.__class__([partial_flatten_asdict(item) for item in obj])
74
+ elif isinstance(obj, dict):
75
+ return {k: partial_flatten_asdict(v) for k, v in obj.items()}
76
+ else:
77
+ return obj
78
+
79
+
80
+ def normalize_as_list(x):
81
+ if isinstance(x, tuple):
82
+ return list(x)
83
+ elif isinstance(x, list):
84
+ return x
85
+ return [x]
86
+
87
+
88
+ def _get_autocast_states():
89
+ return [
90
+ torch.is_autocast_enabled("cuda"),
91
+ torch.is_autocast_enabled("cpu"),
92
+ torch.get_autocast_dtype("cuda"),
93
+ torch.get_autocast_dtype("cpu"),
94
+ torch.is_autocast_cache_enabled(),
95
+ ]
96
+
97
+
98
+ def make_boxed_func(f):
99
+ def g(args):
100
+ return f(*args)
101
+
102
+ g._boxed_call = True # type: ignore[attr-defined]
103
+ return g
104
+
105
+
106
+ def make_boxed_compiler(compiler):
107
+ @wraps(compiler)
108
+ def f(fx_g, inps):
109
+ out_f = compiler(fx_g, inps)
110
+ fx_g = make_boxed_func(out_f)
111
+ return fx_g
112
+
113
+ return f
114
+
115
+
116
+ def call_func_at_runtime_with_args(
117
+ f, args: Union[Tuple[Any], List[Any]], steal_args=False, disable_amp=False
118
+ ):
119
+ if not steal_args:
120
+ args = list(args)
121
+ assert isinstance(args, list)
122
+
123
+ context = torch._C._DisableAutocast if disable_amp else nullcontext
124
+ with context():
125
+ if hasattr(f, "_boxed_call"):
126
+ out = normalize_as_list(f(args))
127
+ else:
128
+ # TODO: Please remove soon
129
+ # https://github.com/pytorch/pytorch/pull/83137#issuecomment-1211320670
130
+ warnings.warn(
131
+ "Your compiler for AOTAutograd is returning a function that doesn't take boxed arguments. "
132
+ "Please wrap it with functorch.compile.make_boxed_func or handle the boxed arguments yourself. "
133
+ "See https://github.com/pytorch/pytorch/pull/83137#issuecomment-1211320670 for rationale."
134
+ )
135
+ out = normalize_as_list(f(*args))
136
+ return out
137
+
138
+
139
+ # Inspired by autodidax (thanks!)
140
+ class PytreeThunk:
141
+ spec: Optional[pytree.TreeSpec] = None
142
+ # These are some kinda dumb microoptimizations that save about 3-4 us of overhead.
143
+ is_simple: Optional[
144
+ bool
145
+ ] = None # if the output spec is a tuple/list, we won't bother unflattening it.
146
+ is_really_simple: Optional[bool] = None # if the output spec is a LeafSpec
147
+
148
+ def set(self, spec: pytree.TreeSpec) -> None:
149
+ assert self.spec is None or self.spec == spec
150
+ assert spec is not None
151
+ self.spec: pytree.TreeSpec = spec
152
+ if self.spec.type in {tuple, list} and all(
153
+ child.is_leaf() for child in spec.children_specs
154
+ ):
155
+ self.is_simple = True
156
+ if self.spec.is_leaf():
157
+ self.is_really_simple = True
158
+
159
+ def unflatten(self, x: List[Any]) -> Any:
160
+ if self.is_really_simple:
161
+ return x[0]
162
+ if self.is_simple:
163
+ return x
164
+ assert self.spec is not None
165
+ return pytree.tree_unflatten(x, self.spec)
166
+
167
+
168
+ # Creates a function that returns flattened inputs and outputs
169
+ # Also returns the output tree spec, which is needed to recover the "unflattened"
170
+ # output tree structure later.
171
+ def create_tree_flattened_fn(fn, args, kwargs=None) -> Tuple[Callable, PytreeThunk]:
172
+ if kwargs is None:
173
+ kwargs = {}
174
+ # Save the args_spec for flat_tensor_args to unflatten while tracing
175
+ _, tensor_args_spec = pytree.tree_flatten((args, kwargs))
176
+ out_spec = PytreeThunk()
177
+
178
+ def flat_fn(*flat_args):
179
+ # The input are flattened tensor args. Prepare the args in the
180
+ # order that original function expects. Add static args as well.
181
+ # They will appear as tensor constants in the traced graph.
182
+ nonlocal out_spec
183
+ args, kwargs = pytree.tree_unflatten(flat_args, tensor_args_spec)
184
+ tree_out = fn(*args, **kwargs)
185
+ flat_out, spec = pytree.tree_flatten(tree_out)
186
+ for i in flat_out:
187
+ is_known_type = False
188
+ for j in KNOWN_TYPES:
189
+ if isinstance(i, j):
190
+ is_known_type = True
191
+ break
192
+ if not is_known_type:
193
+ raise RuntimeError(
194
+ f"Found {type(i)} in output, which is not a known type. "
195
+ "If this type holds tensors, you need to register a pytree for it. "
196
+ "See https://github.com/pytorch/functorch/issues/475 for a brief "
197
+ "explanation why. If you don't need to register a pytree, please "
198
+ "leave a comment explaining your use case and we'll make this more "
199
+ "ergonomic to deal with"
200
+ )
201
+ out_spec.set(spec)
202
+ return flat_out
203
+
204
+ # Can't use functools.wraps here because the wrapper has different
205
+ # calling convention
206
+ if hasattr(fn, "_orig_mod"):
207
+ flat_fn._orig_mod = fn._orig_mod # type: ignore[attr-defined]
208
+
209
+ return flat_fn, out_spec
210
+
211
+
212
+ # This function takes in a tensor t, and returns one of t, t.view(), or t.clone().
213
+ # When tracing the joint forward + backward, for any inputs in the graph that are mutated,
214
+ # we need to clone them first (and similarly for metadata-only mutations, we need to view them first).
215
+ # The idea is that when we trace the backward, we need to pass in the *original* primals
216
+ # to autograd.grad(), before they were mutated.
217
+ # Note: when we have synthetic base inputs, we need to clone them *before* creating views off of them.
218
+ # This means that "idx" here represents the index of the (potentially) synthetic base.
219
+ # What we need to do is:
220
+ # (1) map the current (post-synthetic-base calling convention) input argument index
221
+ # to int index pre-synthetic-base-calling-convention.
222
+ # (2) There could be multiple, if this index corresponds to a synthetic base
223
+ # that has multiple input aliases.
224
+ # (3) If any of those corresponding inputs get metadata mutations, then we clone the base.
225
+ def maybe_to_fresh_input(idx, t, meta):
226
+ if not isinstance(t, torch.Tensor):
227
+ return t
228
+ if idx in meta.mutated_inp_runtime_indices:
229
+ # We only need to bother cloning mutated inputs that participate in autograd.
230
+ mutated_inp_idx = meta.mutated_inp_runtime_indices.index(idx)
231
+ if meta.input_info[idx].requires_grad and meta.input_info[idx].mutates_data:
232
+ # Make sure the primal we pass to autograd.grad()
233
+ # sees the tensor before the mutation
234
+ return t.clone()
235
+ if meta.input_info[idx] and meta.input_info[idx].mutates_metadata:
236
+ # Make sure the primal we pass to autograd.grad()
237
+ # sees the tensor before the metadata mutation
238
+ return t.view(t.shape)
239
+ return t
240
+
241
+
242
+ def is_with_effects(node):
243
+ return (
244
+ node.op == "call_function"
245
+ and node.target == torch.ops.higher_order.with_effects
246
+ )
247
+
248
+
249
+ def is_with_effects_op(node, op):
250
+ return is_with_effects(node) and node.args[1] == op
251
+
252
+
253
+ def unlift_tokens(fw_module, fw_metadata, aot_config, bw_module=None):
254
+ # Remove the tokens from the inputs/outputs of the graph since inductor does
255
+ # not want these extra inputs/outputs, and replace them with
256
+ # _make_token() to create a token, and _sink_tokens() to collect the
257
+ # tokens. See Note [Side-Effectful Tokens in AOTAutograd]
258
+ # Logic:
259
+ # 1. Inputs identified as input tokens:
260
+ # - If used as a first argument in with_effects
261
+ #
262
+ # 2. Outputs identified as output tokens:
263
+ # - If Produced by getitem(with_effects, 0)
264
+ #
265
+ # 3. Checks invariants of number input output tokens:
266
+ # forward:
267
+ # expected_num_erased_inputs == len(fw_metadata.tokens)
268
+ # expected_num_erased_outputs == len(fw_metadata.tokens)
269
+ # backward:
270
+ # expected_num_erased_inputs == fw_metadata.num_backward_tokens
271
+ # expected_num_erased_outputs == fw_metadata.num_backward_tokens
272
+ num_forward_tokens = len(fw_metadata.tokens)
273
+ num_backward_tokens = fw_metadata.num_backward_tokens
274
+
275
+ def rewrite_with_effects_input_token(module, node):
276
+ with module.graph.inserting_before(node):
277
+ new_token_node = module.graph.call_function(
278
+ torch.ops.prims._make_token.default, ()
279
+ )
280
+ new_token_node.meta["val"] = torch.tensor([])
281
+ new_token_node.meta["tensor_meta"] = torch.tensor([])
282
+
283
+ args = list(node.args)
284
+ args[0] = new_token_node
285
+ node.args = tuple(args)
286
+
287
+ def rewrite_output(module, node, output_token_nodes, other_output_args):
288
+ for output_token_node in output_token_nodes:
289
+ assert (
290
+ output_token_node.op == "call_function"
291
+ and output_token_node.target == operator.getitem
292
+ and output_token_node.args[1] == 0
293
+ )
294
+ with module.graph.inserting_before(node):
295
+ module.graph.call_function(
296
+ torch.ops.prims._sink_tokens.default,
297
+ (output_token_nodes,),
298
+ )
299
+ node.args = (other_output_args,)
300
+
301
+ def do(module, subgraph, expected_num_erased):
302
+ num_erased_inputs = 0
303
+ num_erased_outs = 0
304
+ input_nodes = []
305
+ input_token_nodes = set()
306
+ with_effect_nodes = []
307
+ output_token_nodes = []
308
+ other_output_nodes = []
309
+ for i, node in enumerate(module.graph.nodes):
310
+ if node.op == "placeholder":
311
+ input_nodes.append(node)
312
+ elif is_with_effects(node):
313
+ with_effect_nodes.append(node)
314
+ if node.args[0] in input_nodes:
315
+ input_token_nodes.add(node.args[0])
316
+ rewrite_with_effects_input_token(module, node)
317
+ elif node.op == "output":
318
+ outs = node.args[0]
319
+ for out in outs:
320
+ if (
321
+ isinstance(out, torch.fx.node.Node)
322
+ and out.op == "call_function"
323
+ and out.target == operator.getitem
324
+ and out.args[1] == 0
325
+ and out.args[0] in with_effect_nodes
326
+ ):
327
+ output_token_nodes.append(out)
328
+ else:
329
+ other_output_nodes.append(out)
330
+
331
+ rewrite_output(module, node, output_token_nodes, other_output_nodes)
332
+ num_erased_outs = len(output_token_nodes)
333
+
334
+ for input_token_node in input_token_nodes:
335
+ module.graph.erase_node(input_token_node)
336
+
337
+ num_erased_inputs = len(input_token_nodes)
338
+
339
+ assert (
340
+ num_erased_inputs == expected_num_erased
341
+ ), f"{subgraph} num_erased_inputs:{num_erased_inputs} {input_token_nodes}!=expected {expected_num_erased}"
342
+ assert (
343
+ num_erased_outs == expected_num_erased
344
+ ), f"{subgraph} num_erased_outs:{num_erased_outs} {output_token_nodes}!=expected {expected_num_erased}"
345
+
346
+ module.recompile()
347
+
348
+ if num_forward_tokens > 0:
349
+ if aot_config.enable_log:
350
+ from torch._dynamo.utils import lazy_format_graph_code
351
+
352
+ aot_graphs_effects_log.debug(
353
+ "%s",
354
+ lazy_format_graph_code(
355
+ "Forward graph before unlifting tokens",
356
+ fw_module,
357
+ aot_config.aot_id,
358
+ include_stride=True,
359
+ include_device=True,
360
+ colored=True,
361
+ ),
362
+ )
363
+ do(
364
+ fw_module,
365
+ "forward",
366
+ num_forward_tokens,
367
+ )
368
+
369
+ if bw_module is not None and num_backward_tokens > 0:
370
+ if aot_config.enable_log:
371
+ from torch._dynamo.utils import lazy_format_graph_code
372
+
373
+ aot_graphs_effects_log.debug(
374
+ "%s",
375
+ lazy_format_graph_code(
376
+ "Backward graph before unlifting tokens",
377
+ bw_module,
378
+ aot_config.aot_id,
379
+ include_stride=True,
380
+ include_device=True,
381
+ colored=True,
382
+ ),
383
+ )
384
+ do(bw_module, "backward", num_backward_tokens)
385
+
386
+ # This is sad, but we need to update the metadata to get rid of
387
+ # the tokens.
388
+ fw_metadata.tokens = {}
389
+ fw_metadata.num_backward_tokens = 0
390
+
391
+
392
+ def root_module_when_exporting_non_strict(flat_fn):
393
+ # When exporting in non-strict mode, we wrap the root module in a specific pattern.
394
+ # See `_aot_export_non_strict` in torch.export._trace.py.
395
+ # We look for that wrapping pattern here.
396
+ if hasattr(flat_fn, "_orig_mod") and hasattr(flat_fn._orig_mod, "_export_root"):
397
+ return flat_fn._orig_mod._export_root
398
+ else:
399
+ return None
400
+
401
+
402
+ def copy_fwd_metadata_to_bw_nodes(fx_g):
403
+ """
404
+ Input: `fx_g` which contains the joint fwd+bwd FX graph created by
405
+ aot_autograd.
406
+
407
+ This function walks the graph and copies over metadata from forward nodes
408
+ to backward nodes, using the `seq_nr` field as a one-to-many mapping
409
+ from forward node to backward node. This metadata is useful for performance
410
+ profiling and debugging.
411
+ """
412
+
413
+ def _is_forward_node_with_seq_nr(node):
414
+ # For now, assume that if nn_module_stack_metadata is populated, this
415
+ # node is from the forward. Ignore nodes without `seq_nr`.
416
+ # TODO(future): there is likely a less brittle way to do this by walking
417
+ # the descendants of graph inputs corresponding to fwd inputs, didn't
418
+ # seem obvious at first glance on how to partition graph inputs into
419
+ # fwd vs bwd without relying on string names.
420
+ return "nn_module_stack" in node.meta and "seq_nr" in node.meta
421
+
422
+ def _is_backward_node_with_seq_nr(node):
423
+ # For now, assume that if nn_module_stack_metadata is not populated,
424
+ # this node is from the backward. Ignore nodes without `seq_nr`.
425
+ # TODO(future): there is likely a less brittle way to do this, same
426
+ # as with the forward.
427
+ return ("nn_module_stack" not in node.meta) and "seq_nr" in node.meta
428
+
429
+ fwd_seq_nr_to_node = {}
430
+ for node in fx_g.graph.nodes:
431
+ if not _is_forward_node_with_seq_nr(node):
432
+ continue
433
+ seq_nr = node.meta["seq_nr"]
434
+ if seq_nr in fwd_seq_nr_to_node:
435
+ # If we already saw an op with the current `seq_nr`, that means
436
+ # that the current op did not create an autograd node, and there
437
+ # is no corresponding backward node, so we skip.
438
+ continue
439
+ fwd_seq_nr_to_node[node.meta["seq_nr"]] = node
440
+
441
+ for node in fx_g.graph.nodes:
442
+ if not _is_backward_node_with_seq_nr(node):
443
+ continue
444
+ # fwd_node should always exist, but handle non-existence just in case
445
+ fwd_node = fwd_seq_nr_to_node.get(node.meta["seq_nr"])
446
+ if fwd_node is not None:
447
+ node.meta["fwd_nn_module_stack"] = fwd_node.meta["nn_module_stack"]
448
+ node.meta["fwd_source_fn_stack"] = fwd_node.meta.get("source_fn_stack")
449
+
450
+
451
+ def register_buffer_assignment_hook(mod, assigned_buffers):
452
+ """
453
+ Register a hook that intercepts buffer assignments.
454
+ This is used to detect when a buffer is assigned to, and then we can
455
+ map that buffer to the corresponding proxy node in the graph.
456
+ """
457
+
458
+ def _map_assigned_buffer_to_proxy(_mod, name, buffer):
459
+ # We intercept buffer assignments on the root module through this hook.
460
+ if _mod._buffers is mod._buffers:
461
+ # either buffer is a functional tensor, which wraps a fake tensor
462
+ if isinstance(buffer, FunctionalTensor):
463
+ buffer = buffer.from_functional()
464
+ # or buffer is a fake tensor
465
+ assert isinstance(buffer, FakeTensor)
466
+ # The fake tensor in turn is associated with a proxy node.
467
+ proxy_mode = torch.fx.experimental.proxy_tensor.get_proxy_mode()
468
+ assert proxy_mode is not None
469
+ proxy = torch.fx.experimental.proxy_tensor.get_proxy_slot(
470
+ buffer, proxy_mode.tracer
471
+ ).proxy.node
472
+ # We map the assigned buffer to this proxy node.
473
+ assigned_buffers[name] = proxy.name
474
+ return buffer
475
+
476
+ return torch.nn.modules.module.register_module_buffer_registration_hook(
477
+ _map_assigned_buffer_to_proxy
478
+ )
479
+
480
+
481
+ def contain_metadata_mutation_ops(module: torch.fx.GraphModule) -> bool:
482
+ """
483
+ Checks if the module contains any metadata mutation ops.
484
+ """
485
+ for node in module.graph.nodes:
486
+ if (
487
+ node.op == "call_function"
488
+ and hasattr(node.target, "tags")
489
+ and torch.Tag.inplace_view in node.target.tags
490
+ ):
491
+ return True
492
+ return False
lib/python3.12/site-packages/torch/_functorch/aot_autograd.py ADDED
@@ -0,0 +1,1664 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: ignore-errors
2
+
3
+ import itertools
4
+ from contextlib import contextmanager, nullcontext
5
+ from functools import partial, wraps
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Dict,
10
+ List,
11
+ NewType,
12
+ Optional,
13
+ Protocol,
14
+ Sequence,
15
+ Tuple,
16
+ Type,
17
+ TypeVar,
18
+ )
19
+ from unittest.mock import patch
20
+
21
+ import torch
22
+ import torch._dynamo.logging
23
+ import torch.nn as nn
24
+ import torch.utils._pytree as pytree
25
+ import torch.utils.dlpack
26
+ from torch import Tensor
27
+ from torch._decomp.decompositions_for_rng import PhiloxStateTracker, rng_decompositions
28
+ from torch._dispatch.python import enable_python_dispatcher
29
+ from torch._dynamo import compiled_autograd
30
+ from torch._dynamo.utils import (
31
+ dynamo_timed,
32
+ get_chromium_event_logger,
33
+ preserve_rng_state,
34
+ )
35
+ from torch._guards import detect_fake_mode
36
+ from torch._inductor.output_code import OutputCode
37
+ from torch._inductor.utils import BoxedBool, InputType
38
+ from torch._subclasses import FakeTensor, FakeTensorMode
39
+ from torch.fx.experimental.proxy_tensor import make_fx
40
+ from torch.fx.experimental.symbolic_shapes import ShapeEnv
41
+ from torch.utils._python_dispatch import is_traceable_wrapper_subclass
42
+
43
+
44
+ static_inputs_log = torch._logging.getArtifactLogger(
45
+ __name__, "cudagraph_static_inputs"
46
+ )
47
+ from . import config
48
+ from ._aot_autograd.autograd_cache import ( # noqa: F401
49
+ AOTAutogradCache,
50
+ autograd_cache_key,
51
+ should_use_local_autograd_cache,
52
+ should_use_remote_autograd_cache,
53
+ )
54
+ from ._aot_autograd.collect_metadata_analysis import ( # noqa: F401
55
+ run_functionalized_fw_and_collect_metadata,
56
+ )
57
+ from ._aot_autograd.functional_utils import ( # noqa: F401
58
+ _check_if_mutation_can_be_in_graph,
59
+ are_all_mutations_hidden_from_autograd,
60
+ are_all_mutations_under_no_grad_or_inference_mode,
61
+ assert_functional_graph,
62
+ from_fun,
63
+ gen_alias_from_base,
64
+ has_data_mutation,
65
+ has_metadata_mutation,
66
+ is_fun,
67
+ sync_functional_tensor,
68
+ to_fun,
69
+ )
70
+ from ._aot_autograd.input_output_analysis import ( # noqa: F401
71
+ compute_overlapping_inputs,
72
+ create_graph_signature,
73
+ create_synthetic_base_metadata,
74
+ remove_dupe_metadata,
75
+ )
76
+ from ._aot_autograd.jit_compile_runtime_wrappers import ( # noqa: F401
77
+ aot_dispatch_autograd,
78
+ aot_dispatch_base,
79
+ aot_dispatch_export,
80
+ )
81
+ from ._aot_autograd.logging_utils import ( # noqa: F401
82
+ callback_set,
83
+ describe_input,
84
+ format_guard_bug_msg,
85
+ get_aot_compilation_context,
86
+ get_aot_graph_name,
87
+ get_graph_being_compiled,
88
+ graph_being_compiled,
89
+ model_name,
90
+ nth_graph,
91
+ set_model_name,
92
+ setup_stacktrace_preservation_hooks,
93
+ track_graph_compiling,
94
+ )
95
+ from ._aot_autograd.runtime_wrappers import ( # noqa: F401
96
+ AOTDedupeWrapper,
97
+ AOTSyntheticBaseWrapper,
98
+ )
99
+ from ._aot_autograd.schemas import ( # noqa: F401
100
+ AOTConfig,
101
+ BackwardSignature,
102
+ FQN,
103
+ GraphInputName,
104
+ GraphOutputName,
105
+ GraphSignature,
106
+ InputAliasInfo,
107
+ MutationType,
108
+ OutputAliasInfo,
109
+ OutputType,
110
+ SubclassCreationMeta,
111
+ SubclassMeta,
112
+ TensorAlias,
113
+ ViewAndMutationMeta,
114
+ )
115
+ from ._aot_autograd.subclass_utils import ( # noqa: F401
116
+ requires_subclass_dispatch,
117
+ unwrap_tensor_subclasses,
118
+ unwrap_tensor_subclasses_with_indices_to_original,
119
+ wrap_tensor_subclasses,
120
+ wrap_tensor_subclasses_maybe_joint,
121
+ )
122
+ from ._aot_autograd.traced_function_transforms import ( # noqa: F401
123
+ aot_dispatch_subclass,
124
+ create_functional_call,
125
+ create_functionalized_fn,
126
+ create_functionalized_rng_ops_wrapper,
127
+ create_joint,
128
+ fn_input_mutations_to_outputs,
129
+ fn_prepped_for_autograd,
130
+ )
131
+ from ._aot_autograd.utils import ( # noqa: F401
132
+ _get_autocast_states,
133
+ _get_symint_hints,
134
+ call_func_at_runtime_with_args,
135
+ create_tree_flattened_fn,
136
+ KNOWN_TYPES,
137
+ make_boxed_compiler,
138
+ make_boxed_func,
139
+ maybe_to_fresh_input,
140
+ normalize_as_list,
141
+ partial_flatten_asdict,
142
+ root_module_when_exporting_non_strict,
143
+ strict_zip,
144
+ )
145
+ from .partitioners import default_partition
146
+
147
+
148
+ zip = strict_zip
149
+
150
+ # This global counter increments every time we compile a graph with
151
+ # AOTAutograd. You can use this to correlate runtime error messages
152
+ # with compile time (e.g., if you get an error at runtime saying
153
+ # compiled graph 3 failed, you can set a breakpoint at compile time
154
+ # for this graph number to investigate further at compile time.)
155
+ #
156
+ # NB: this is different from get_aot_compilation_context, which tracks
157
+ # each underlying graph that is compiled. In contrast, AOT_COUNTER
158
+ # corresponds to top-level invocations of aot_module/aot_function;
159
+ # one counter is allocated per entire compiled block (but this block
160
+ # may involve compiling multiple subgraphs; e.g., for forwards/backwards)
161
+ AOT_COUNTER = itertools.count()
162
+
163
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
164
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
165
+ #
166
+ # AOT Autograd contains a pretty non-trivial amount of logic to handle edge cases around aliasing and mutation
167
+ # that are external to the graph (they show up as side effects in some way when you run the graph).
168
+ #
169
+ # Take a look at `test_aotdispatch.py TestAOTAutograd.test_input_mutation*` tests for some examples functions
170
+ # and what they're compiled graphs looks like.
171
+ # Below is a very long comment detailing several edge cases, and showing how AOT Autograd handles them.
172
+ #
173
+ # Note [AOT Autograd: input data mutations]
174
+ #
175
+ # If we compile a function that mutates inputs, then those input mutations are real side effects
176
+ # that a user expects to see after running the compiled graph.
177
+ # However, the graph that we want to send to a backend needs to be *entirely* functional.
178
+ # The way we reconcile this difference is that we remove the mutations completely from the graph that we compile
179
+ # but we update the graph to return (updated_inputs, user_outputs).
180
+ # In the epilogue that runs after the compiled graph is executed, we copy the updated inputs back to the originals.
181
+ #
182
+ # Example: original user code:
183
+ # def f(x):
184
+ # x.mul_(2)
185
+ # out = x.mul(3)
186
+ # return out
187
+ #
188
+ # After AOT Autograd compiles, we end up with a:
189
+ # (a) compiled graph
190
+ # (b) autograd.Function.forward() method, that executes the compiled graph
191
+ # (c) wrapper function, that calls the autograd.Function.forward() and performs the epilogue
192
+ #
193
+ # The output of (a, b, c) are all written below.
194
+ #
195
+ # def compiled_forward_graph(x):
196
+ # x_updated = x.mul(2)
197
+ # out = x_updated.mul(3)
198
+ # return x_updated, out
199
+ #
200
+ # # x_updated gets a gradient in the compiled backward
201
+ # def compiled_backward_graph(grad_x_updated, grad_out):
202
+ # grad_x = ...
203
+ # return grad_x
204
+ #
205
+ # def autograd.Function.forward(x):
206
+ # x_updated, out = compiled_forward_graph(x)
207
+ # return x_updated, out
208
+ #
209
+ # def compiled_wrapper(x):
210
+ # x_updated, out = autograd.Function.apply(x)
211
+ # x.copy_(x_updated)
212
+ # return out
213
+ #
214
+ # Another important thing to note is that updated inputs (due to data mutations) *do* participate
215
+ # in the compiled backward graph! Since the compiled forward graph gets N extra outputs
216
+ # (due to updated inputs showing up as graph outputs),
217
+ # The compiled backward gets an additional N inputs.
218
+ # That way, during the x.copy_(x_updated) bit in the epilogue, gradients will flow from the updated input
219
+ # back to the original input.
220
+
221
+
222
+ # Note [AOT Autograd: input metadata mutations]
223
+ #
224
+ # For the same reason as input mutations, we also don't put input metadata mutations in the graph.
225
+ # Instead, we return the updated version of the input (a view), and mutate the input's metadata outside of the graph
226
+ #
227
+ # Example: original user code:
228
+ # def f(x):
229
+ # x.t_()
230
+ # out = x.mul(3)
231
+ # return out
232
+ #
233
+ # AOT Autograd output (compiled graph, autograd.Function.forward(), wrapper function):
234
+ # def compiled_forward_graph(x):
235
+ # x_updated = x.t()
236
+ # out = x_updated.mul(3)
237
+ # return x_updated, out
238
+ #
239
+ # # x_updated does *not* get a gradient in the compiled backward
240
+ # def compiled_backward_graph(grad_out):
241
+ # grad_x = ...
242
+ # return grad_x
243
+ #
244
+ # def autograd.Function.forward(x):
245
+ # x_updated, out = compiled_forward_graph(x)
246
+ # return x_updated, out
247
+ #
248
+ # def compiled_wrapper(x):
249
+ # x_updated, out = autograd.Function.apply(x)
250
+ # x.as_strided_(x_updated)
251
+ # return out
252
+
253
+
254
+ # Note [AOT Autograd: outputs aliasing inputs or intermediates!]
255
+ #
256
+ # AOT Autograd needs special handling for outputs that alias graph inputs or intermediates!
257
+ # Why?
258
+ # (1) autograd.Function.forward() has a limitation, where views that returned in the forward cannot later be mutated.
259
+ # (2) views don't need to be compiled in the graph anyway - it's cheap to generate them outside of the compiled graph,
260
+ # in an epilogue.
261
+ # For outputs that alias inputs, we do the following:
262
+ # (a) *still* return the aliased output as a graph output
263
+ # (b) In the AOT Autograd wrapper/epilogue, we don't return that aliased output. Instead, we use it to regenerate the output.
264
+ #
265
+ # For outputs that alias *intermediates*, we do the following:
266
+ # (a) Return the output in the compiled forward, **and** return it's ._base (a graph intermediates) as an output in the forward
267
+ # (b) Use (output, graph_intermediate) to regenerate the alias, and return that to the user (instead of the compiled fw output).
268
+ # You might wonder why we return the aliased output directly in the graph (and making the graph compute it),
269
+ # only to not return it and instead generate a fresh alias off of the intermediate,
270
+ # instead of (say) just storing metadata about the size/stride of the output somewhere to generate the alias. There are two reasons:
271
+ # (1) Getting the actual alias tensor allows us to use view-replay to generate the alias, instead of an as_strided() call
272
+ # (2) Inductor (and other backends) are free to change the memory format of graph outputs, if it results in better performance.
273
+ # This can result in problems if a user later tries to .view() that output expecting it to have one set of strides,
274
+ # when it has a different set of strides.
275
+ # By including the view op directly in the graph, inductor takes that into account when deciding what memory format
276
+ # the graph intermediate should be.
277
+ #
278
+ # Another important thing to note is how our traced backward() graph handles aliases.
279
+ # (this applies to outputs aliasing inputs, outputs aliasing intermediates,
280
+ # *and* updated inputs returned in the compiled forward due to metadata-only mutations).
281
+ # Any outputs that alias (either inputs or intermediates) do NOT participate in the compiled backward graph
282
+ # It would be wasteful to include them in the compiled backward(), because we regenerate them eagerly
283
+ # at the end of the forward.
284
+ #
285
+ # Example: original user code:
286
+ # def f(x):
287
+ # out1 = x.t()
288
+ # intermediate = x.mul(2)
289
+ # out2 = intermediate.view(-1)
290
+ # return out1, out2
291
+ #
292
+ # AOT Autograd output (compiled graph, autograd.Function.forward(), wrapper function):
293
+ # def compiled_forward_graph(x):
294
+ # out1 = x.t()
295
+ # intermediate = x.mul(2)
296
+ # out2 = intermediate.view(-1)
297
+ # # the compiled graph also returns the intermediate
298
+ # return out1, out2, intermediate
299
+ #
300
+ # # intermediate gets a gradient in the compiled backward.
301
+ # # both output aliases (out1 and out2) do not.
302
+ # def compiled_backward_graph(grad_intermediate):
303
+ # grad_x = ...
304
+ # return grad_x
305
+ #
306
+ # def autograd.Function.forward(x):
307
+ # out1, out2, intermediate = compiled_forward_graph(x)
308
+ # return out1, out2, intermediate
309
+ #
310
+ # def compiled_wrapper(x):
311
+ # out1, out2, intermediate = autograd.Function.apply(x)
312
+ # # regenerate out1 from the input
313
+ # out1_regenerated = out1._view_func(x)
314
+ # # regenerate out1 from the intermediate
315
+ # out2_regenerated = out2._view_func(intermediate)
316
+ # return out1_regenerated, out2_regenerated
317
+
318
+
319
+ # Note [AOT Autograd: mutations to inputs that alias other inputs]
320
+ #
321
+ # Another edge case that is (only partially) handled today is when an input is mutated, but itself aliases another input.
322
+ # AOT Autograd needs to **ensure** that functionalization knows that the two inputs are aliased to each other.
323
+ # That way, when the aliased input is accessed later in the graph, functionalization knows to "update" the alias
324
+ # given the mutation that occurred.
325
+ #
326
+ # This is handled by updating the calling convention: we create a "synthetic base" that becomes a new input
327
+ # in the compiled function, and we regenerate the original (aliased) inputs directly off of the base
328
+ # inside of the compiled function.
329
+ #
330
+ # This logic is fully encapsulated in aot_wrapper_synthetic_base()
331
+ #
332
+ # Example: original user code:
333
+ # def f(x, x_view):
334
+ # x.mul_(2)
335
+ # out = x * x_view
336
+ # return out
337
+ # f(x, x.view(-1))
338
+ #
339
+ # AOT Autograd output (compiled graph, autograd.Function.forward(), wrapper function):
340
+ # def compiled_forward_graph(base)
341
+ # x = generate_x(base)
342
+ # x_view = generate_x_view(base)
343
+ # x_updated = x.mul(2)
344
+ # x_view_updated = x_updated.view(-1)
345
+ # out = x_updated * x_view_updated
346
+ # return x_updated, out
347
+ #
348
+ # # The calling convention change from (aliases) -> (base) happens
349
+ # # *outside* of the autograd.Function.forward().
350
+ # # That means the forward() only has 1 input (base),
351
+ # # and the backward() only has 1 output (grad_base)
352
+ # def compiled_backward_graph(grad_out):
353
+ # grad_base = ...
354
+ # return grad_base
355
+ #
356
+ # def autograd.Function.forward(base):
357
+ # x_updated, out = compiled_forward_graph(base)
358
+ # return x_updated, out
359
+ #
360
+ # # The compiled wrapper is where we create synthetic bases.
361
+ # # The info on which inputs are mutated is also tracked *before* synthetic base creation.
362
+ # def compiled_wrapper(x, x_view):
363
+ # base = merge_view_inputs(x, x_view)
364
+ # x_updated, out = autograd.Function.apply(base)
365
+ # # x and x_view are aliased in eager mode, so this mutation to x will automatically affect x_view.
366
+ # x.copy_(x_updated)
367
+ # return out
368
+
369
+
370
+ # Note [AOT Autograd: Views to avoid tangents aliasing inputs]
371
+ #
372
+ # We view every forward output when creating out tangent tensors to handle the problematic
373
+ # case in which a subclass does extra aliasing between graph outputs/inputs in a way that
374
+ # is not visible above the sublass.
375
+ #
376
+ # Ordinarily, when constructing the joint function that we want to trace in AOTAutograd,
377
+ # we're guaranteed that the tangent tensors that we pass
378
+ # into the joint are distinct tensors from the primals. This is because when
379
+ # decide which forward outputs to create tangents for, we only create tangents
380
+ # for forward outputs that are not aliases of inputs (See Note
381
+ # [AOT Autograd: outputs aliasing inputs or intermediates!]).
382
+ #
383
+ # However, when wrapper tensor subclasses enter the picture, it is possible
384
+ # to have an output of the forward that is a subclass that is not an
385
+ # input / alias of an input, but one of its inner tensors is an alias!
386
+ # NestedTensor is an example: Performing an out-of-place pointwise op on a
387
+ # NestedTensor constructs a fresh NestedTensor that holds onto the input's
388
+ # offsets tensor directly.
389
+ #
390
+ # Having tangent tensors that are the same as the (primal) forward inputs,
391
+ # can cause problems during tracing as make_fx() will specialize on our
392
+ # duplicate inputs: If we passed in the same tensor for primals_1 and
393
+ # tangents_1 during tracing, make_fx() will happily sub out all usages of
394
+ # tangents_1 with primals_1 in the graph, which is not what we want.
395
+ #
396
+ # To work around this, we view every forward output when creating out tangent
397
+ # tensors so that tangents can never be the same as forward inputs even if
398
+ # forward inputs alias forward outputs.
399
+
400
+ # Note [Side-Effectful Tokens in AOTAutograd]
401
+ #
402
+ # We allow some some side-effectful operators in
403
+ # the post-AOTAutograd (functional) graph, such as prints and torchbind operations.
404
+ # To ensure that these side-effects are compatible to future graph passes that
405
+ # assume that the graph is functional, we will thread "effect tokens" to show
406
+ # data dependence between these side-effectful operators. Practically speaking,
407
+ # effect tokens are just dummy values (torch.tensor([])). The graph would look
408
+ # like the following:
409
+ #
410
+ # def gm(self, token0, reader):
411
+ # token1, frame = with_token(ordered_effect_op, (reader,), token0)
412
+ # frame = frame * 2
413
+ # token2, frame2 = with_token(ordered_effect_op, (reader,), token1)
414
+ # frame2 = frame2 * 2
415
+ # return token2, frame, frame2
416
+ #
417
+ # We will pass the token as an input to the graph, thread it through
418
+ # side-effectful operators using the `with_effects` high order operator, and then
419
+ # return the updated token as an output.
420
+ # So the signature of the graph input would look something like
421
+ # (*tokens, *params_buffers, *user_inputs), and the signature of the graph
422
+ # output would look something like (*tokens, *outputs).
423
+ #
424
+ # However, Inductor does not want the concept of tokens in the final generated
425
+ # code's input and output. Since changing the graph signature inside of inductor
426
+ # is difficult, after generating the forward graph, we will run a pass to
427
+ # remove the tokens from the inputgenerate the following graph for Inductor, where
428
+ # the tokens are created and sunk within the graph, rather than as inputs and
429
+ # outputs:
430
+ #
431
+ # def gm(self, reader):
432
+ # token0 = torch.ops.prims._make_token()
433
+ # token1, frame = with_token(ordered_effect_op, (reader,), token0)
434
+ # frame = frame * 2
435
+ # token2, frame2 = with_token(ordered_effect_op, (reader,), token1)
436
+ # frame2 = frame2 * 2
437
+ # sink_token = torch.ops.prims._sink_tokens([token2])
438
+ # return frame, frame2
439
+
440
+ #
441
+ #
442
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
443
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
444
+
445
+
446
+ aot_autograd_decompositions = {}
447
+
448
+ FakifiedFlatArgs = NewType("FakifiedFlatArgs", List[Any])
449
+
450
+
451
+ TOutputCode = TypeVar("TOutputCode", bound=OutputCode)
452
+
453
+
454
+ class AOTDispatchCompiler(Protocol):
455
+ """
456
+ Represents a fw or bw_compiler passed to AOTAutograd.
457
+ """
458
+
459
+ def __call__(
460
+ self,
461
+ gm: torch.fx.GraphModule,
462
+ example_inputs: Sequence[InputType],
463
+ ) -> Any:
464
+ ...
465
+
466
+
467
+ # TODO: bikeshed on this name
468
+ class SerializableAOTDispatchCompiler(AOTDispatchCompiler):
469
+ """
470
+ Represents an AOTDispatchCompiler that returns an OutputCode, and is
471
+ therefore cacheable. SerializableAOTDispatchCompiler always return an OutputCode.
472
+ A _CompileFxCallable usually gets converted into an AOTDispatchCompiler after binding all of
473
+ the kwargs in _CompileFxKwargs.
474
+ """
475
+
476
+ def __init__(
477
+ self,
478
+ output_code_ty: Type[TOutputCode],
479
+ compiler_fn: Callable[[torch.fx.GraphModule, Sequence[InputType]], TOutputCode],
480
+ ):
481
+ self.output_code_ty = output_code_ty
482
+ self.compiler_fn = compiler_fn
483
+
484
+ def __call__(
485
+ self,
486
+ gm: torch.fx.GraphModule,
487
+ example_inputs: Sequence[InputType],
488
+ ) -> OutputCode:
489
+ return self.compiler_fn(gm, example_inputs)
490
+
491
+
492
+ def process_inputs(
493
+ flat_args: List[Any],
494
+ aot_config: AOTConfig,
495
+ fake_mode: FakeTensorMode,
496
+ shape_env: Optional[ShapeEnv],
497
+ ) -> FakifiedFlatArgs:
498
+ with fake_mode:
499
+
500
+ def convert(idx, x):
501
+ if shape_env is not None:
502
+ from torch._dynamo.source import ConstantSource
503
+
504
+ if isinstance(x, int):
505
+ # We always specialize on scalar values in export.
506
+ if aot_config.is_export:
507
+ return x
508
+ source = ConstantSource(f"sym_{idx}")
509
+ return shape_env.create_symintnode(
510
+ shape_env.create_symbol(x, source), hint=x, source=source
511
+ )
512
+ if isinstance(x, torch.ScriptObject):
513
+ return torch._library.fake_class_registry.maybe_to_fake_obj(
514
+ fake_mode, x
515
+ )
516
+ if not isinstance(x, torch.Tensor):
517
+ return x
518
+ if isinstance(x, FakeTensor):
519
+ assert x.fake_mode is fake_mode
520
+ return x
521
+ if is_traceable_wrapper_subclass(x):
522
+ attrs, _ = x.__tensor_flatten__()
523
+ if all(isinstance(getattr(x, attr), FakeTensor) for attr in attrs):
524
+ assert all(
525
+ getattr(x, attr).fake_mode is fake_mode for attr in attrs
526
+ )
527
+ return x
528
+
529
+ # see note [Tensor Fakification and Symbol Caching]
530
+ symbolic_context = None
531
+ source = None
532
+ trace = True
533
+ if tracing_context := torch._guards.TracingContext.try_get():
534
+ if x in tracing_context.tensor_to_context:
535
+ symbolic_context = tracing_context.tensor_to_context[x]
536
+ source = symbolic_context.tensor_source
537
+ # We already fakeified this tensor in Dynamo, don't
538
+ # dump the trace for it again
539
+ trace = False
540
+ if (
541
+ idx < aot_config.num_params_buffers
542
+ and config.static_weight_shapes
543
+ and not symbolic_context
544
+ ):
545
+ # TODO: Ensure that this codepath is never exercised from
546
+ # Dynamo
547
+ return fake_mode.from_tensor(x, static_shapes=True)
548
+
549
+ return fake_mode.from_tensor(
550
+ x,
551
+ static_shapes=False,
552
+ symbolic_context=symbolic_context,
553
+ source=source,
554
+ trace=trace,
555
+ )
556
+
557
+ return FakifiedFlatArgs([convert(idx, x) for idx, x in enumerate(flat_args)])
558
+
559
+
560
+ def construct_fake_mode(
561
+ flat_args: List[Any], aot_config: AOTConfig
562
+ ) -> Tuple[FakeTensorMode, Optional[ShapeEnv]]:
563
+ fake_mode = detect_fake_mode(flat_args)
564
+ if fake_mode is None:
565
+ shape_env = ShapeEnv() if aot_config.dynamic_shapes else None
566
+ fake_mode = FakeTensorMode(shape_env=shape_env)
567
+ else:
568
+ shape_env = fake_mode.shape_env
569
+ return (fake_mode, shape_env)
570
+
571
+
572
+ def create_aot_dispatcher_function(
573
+ flat_fn,
574
+ fake_flat_args: FakifiedFlatArgs,
575
+ aot_config: AOTConfig,
576
+ fake_mode: FakeTensorMode,
577
+ shape_env: Optional[ShapeEnv],
578
+ ) -> Tuple[Callable, ViewAndMutationMeta]:
579
+ with dynamo_timed("create_aot_dispatcher_function", log_pt2_compile_event=True):
580
+ return _create_aot_dispatcher_function(
581
+ flat_fn, fake_flat_args, aot_config, fake_mode, shape_env
582
+ )
583
+
584
+
585
+ def _create_aot_dispatcher_function(
586
+ flat_fn,
587
+ fake_flat_args: FakifiedFlatArgs,
588
+ aot_config: AOTConfig,
589
+ fake_mode: FakeTensorMode,
590
+ shape_env: Optional[ShapeEnv],
591
+ ) -> Tuple[Callable, ViewAndMutationMeta]:
592
+ """
593
+ Traces the forward and backward graphs of the attr:`flat_fn` to generate a
594
+ joint graph. The joint graph is an Fx graph with Aten ops. Please refer to
595
+ the tracing mechanism to understand the graph capturing details.
596
+
597
+ The joint graph is then passed through attr:`partition_fn` to isolate the
598
+ forward and backward portions, which are then respectively compiled via the
599
+ provided attr:`fw_compiler` and attr:`bw_compiler`.
600
+
601
+ The resulting compiled forward and backward graphs are then wrapped up in a
602
+ ``torch.autograd.Function`` object.
603
+
604
+ The calling convention here is that the first aot_config.num_params_buffers
605
+ inputs in flat_args are parameters and buffers, and the rest are inputs.
606
+
607
+ We use this to assume that parameters/buffer's shapes don't change.
608
+
609
+ Note: this function is used both by aot_function and aot_export (controlled by aot_config.is_export)
610
+ When aot_config.is_export is True, we return an FX graph + metadata
611
+ When aot_config.is_export is False, we return an ordinary runtime function
612
+ """
613
+
614
+ # This is the main entry point.
615
+ # TODO: Chillee argues that dynamo itself should pass in fake tensors to
616
+ # the list of arguments when compiling; at the moment we do not do this
617
+
618
+ if aot_config.decompositions is None:
619
+ aot_config.decompositions = {}
620
+
621
+ aot_config.decompositions = {
622
+ **aot_autograd_decompositions,
623
+ **aot_config.decompositions,
624
+ }
625
+
626
+ if config.functionalize_rng_ops:
627
+ # Update the decompositions with functionalized random decompositions
628
+ aot_config.decompositions = {
629
+ **rng_decompositions,
630
+ **aot_config.decompositions,
631
+ }
632
+
633
+ # Check flat_args to see if they're already fake. If so, use that fake
634
+ # mode instead.
635
+
636
+ python_dispatcher_mode = (
637
+ enable_python_dispatcher() if shape_env is not None else nullcontext()
638
+ )
639
+ chromium_log = get_chromium_event_logger()
640
+ # See NOTE: [Deferring tensor pack/unpack hooks until runtime]
641
+ # If any saved tensor hooks are active, we **don't** want to trace them.
642
+ # Instead, we'll let them run at runtime, around the custom autograd.Function
643
+ # that we generate in torch.compile.
644
+ with torch.autograd.set_multithreading_enabled(
645
+ False
646
+ ), preserve_rng_state(), (
647
+ fake_mode
648
+ ), (
649
+ python_dispatcher_mode
650
+ ), PhiloxStateTracker(), torch._dynamo.utils._disable_saved_tensors_hooks_during_tracing():
651
+ from torch._library.fake_class_registry import (
652
+ FakeScriptObject,
653
+ maybe_to_fake_obj,
654
+ )
655
+
656
+ # Tracing may mutate the states the fake script object,
657
+ # so we need to duplicate the fake script objects so that subsequent tracing
658
+ # won't be affected.
659
+ def _dup_fake_script_obj(fake_flat_args):
660
+ return [
661
+ maybe_to_fake_obj(detect_fake_mode(fake_flat_args), arg.real_obj)
662
+ if isinstance(arg, FakeScriptObject)
663
+ else arg
664
+ for arg in fake_flat_args
665
+ ]
666
+
667
+ needs_autograd = any(
668
+ x.requires_grad for x in fake_flat_args if isinstance(x, Tensor)
669
+ )
670
+
671
+ with enable_python_dispatcher():
672
+ # Patch set_rng_state as set_rng_state with fake tensors is
673
+ # nonsensical. This does not affect the collection of metadata.
674
+ with patch("torch.cuda.set_rng_state", lambda *args: None):
675
+ mod = root_module_when_exporting_non_strict(flat_fn)
676
+ if mod is not None:
677
+ ctx = _detect_attribute_assignment(mod)
678
+ else:
679
+ ctx = nullcontext()
680
+ with ctx:
681
+ fw_metadata = run_functionalized_fw_and_collect_metadata(
682
+ flat_fn,
683
+ static_input_indices=aot_config.static_input_indices,
684
+ keep_input_mutations=aot_config.keep_inference_input_mutations,
685
+ is_train=needs_autograd,
686
+ pre_dispatch=aot_config.pre_dispatch,
687
+ is_export=aot_config.is_export,
688
+ )(*_dup_fake_script_obj(fake_flat_args))
689
+
690
+ req_subclass_dispatch = requires_subclass_dispatch(
691
+ fake_flat_args, fw_metadata
692
+ )
693
+ chromium_log.try_add_event_data(
694
+ "backend_compile", requires_subclass_dispatch=req_subclass_dispatch
695
+ )
696
+
697
+ output_and_mutation_safe = not any(
698
+ x.requires_grad
699
+ # view-type operations preserve requires_grad even in no_grad.
700
+ # Do not count aliases of inputs with requires_grad as reason to make a training graph,
701
+ # as AOTAutograd will perform view-replay to regenerate the view outputs at runtime,
702
+ # setting their grad_fn properly.
703
+ and not (
704
+ x.output_type
705
+ in (OutputType.alias_of_input, OutputType.is_input)
706
+ and fw_metadata.input_info[x.base_idx].requires_grad
707
+ )
708
+ for x in fw_metadata.output_info
709
+ ) and not any(
710
+ x.requires_grad
711
+ and x.mutates_data
712
+ and not x.mutations_under_no_grad_or_inference_mode
713
+ and not x.mutations_hidden_from_autograd
714
+ for x in fw_metadata.input_info
715
+ )
716
+
717
+ if needs_autograd and output_and_mutation_safe:
718
+ # We realized that none of the outputs require grad,
719
+ # and none of the inputs that require grad are mutated.
720
+ # so we actually have an inference graph.
721
+ needs_autograd = False
722
+ # A bit silly: right now in the subclass codepath, our ViewAndMutationMeta
723
+ # changes depending on whether we pass in is_train / keep_input_mutations,
724
+ # so we're forced to recompute the metadata.
725
+ # TODO: refactor the subclass path of run_functionalized_fw_and_collect_metadata
726
+ # so that this is unnecessary.
727
+ if req_subclass_dispatch:
728
+ fw_metadata = run_functionalized_fw_and_collect_metadata(
729
+ flat_fn,
730
+ keep_input_mutations=aot_config.keep_inference_input_mutations,
731
+ is_train=False,
732
+ pre_dispatch=aot_config.pre_dispatch,
733
+ static_input_indices=aot_config.static_input_indices,
734
+ )(*fake_flat_args)
735
+ else:
736
+ fw_metadata = ViewAndMutationMeta(
737
+ input_info=fw_metadata.input_info,
738
+ output_info=fw_metadata.output_info,
739
+ num_intermediate_bases=fw_metadata.num_intermediate_bases,
740
+ keep_input_mutations=aot_config.keep_inference_input_mutations,
741
+ traced_tangents=fw_metadata.traced_tangents,
742
+ subclass_inp_meta=fw_metadata.subclass_inp_meta,
743
+ subclass_fw_graph_out_meta=fw_metadata.subclass_fw_graph_out_meta,
744
+ subclass_tangent_meta=fw_metadata.subclass_tangent_meta,
745
+ is_train=False,
746
+ tokens=fw_metadata.tokens,
747
+ static_input_indices=fw_metadata.static_input_indices,
748
+ )
749
+
750
+ if fw_metadata.num_intermediate_bases > 0:
751
+ assert not req_subclass_dispatch, f"""\
752
+ torch.compile is currently being used with tensor subclass inputs:
753
+ {','.join([str(type(x)) for x in fake_flat_args])}. We are attempting to a compile a graph with two graph outputs
754
+ that alias one another, which is currently unsupported in the subclass use case. If you run into this,
755
+ please file a github issue"""
756
+
757
+ if aot_config.is_export:
758
+ # aot_export: ban input metadata mutations for now to keep shared code paths simpler.
759
+ # Keeping .resize_() in the graph will require some work
760
+ # Allowing it but keeping the graph functional will require some calling convention changes.
761
+ if len([x for x in fw_metadata.input_info if x.mutates_metadata]) != 0:
762
+ raise RuntimeError(
763
+ f"""\
764
+ Found an input that received a metadata mutation, through e.g. a call to `.resize_()` or `.transpose_()`.
765
+ This is currently banned in the aot_export workflow. If you need this functionality, please file a github issue.
766
+
767
+ fw_metadata={str(fw_metadata)}"""
768
+ )
769
+ # In export, banning data mutations on inputs that require grad for now.
770
+ # This should be rare, and is tricky to get right. When we trace the backward,
771
+ # we currently trace with autograd.grad instead of .backward(), which makes it difficult
772
+ # to ensure that we run autograd all the way through the input **before** it saw the mutation.
773
+ if (
774
+ len(
775
+ [
776
+ x
777
+ for x in fw_metadata.input_info
778
+ if x.requires_grad and x.mutates_data
779
+ ]
780
+ )
781
+ != 0
782
+ ):
783
+ raise RuntimeError(
784
+ f"""\
785
+ Found a graph input that requires gradients, and received a mutation.
786
+ This is currently banned in the aot_export workflow. If you need this functionality, please file a github issue.
787
+
788
+ fw_metadata={str(fw_metadata)}"""
789
+ )
790
+ if req_subclass_dispatch:
791
+ raise RuntimeError(
792
+ """\
793
+ aot_export is not currently supported with traceable tensor subclass.
794
+ If you need this feature, please comment on <CREATE_ISSUE_LINK>"""
795
+ )
796
+
797
+ # Need to decide on a strategy for functionalized RNG: toggling via global config seems bad,
798
+ # and turning it on will require a non-trivial calling convention change for any export runtime.
799
+ if config.functionalize_rng_ops:
800
+ raise RuntimeError(
801
+ """\
802
+ Functionalized RNG is not currently supported in the aot_export workflow. Please file a github issue,
803
+ or otherwise set torch._functorch.config.functionalize_rng_ops = False."""
804
+ )
805
+
806
+ def choose_dispatcher(needs_autograd, aot_config):
807
+ """
808
+ Pick a dispatcher based on the config rules.
809
+ """
810
+ if aot_config.is_export:
811
+ # export uses just the "graph bits", whereas the other
812
+ # two dispatchers include some extra work around handling a runtime epilogue
813
+ chromium_log.try_add_event_data(
814
+ "backend_compile", dispatch_mode="export"
815
+ )
816
+ return partial(aot_dispatch_export, needs_autograd=needs_autograd)
817
+ elif needs_autograd and not aot_config.pre_dispatch:
818
+ chromium_log.try_add_event_data(
819
+ "backend_compile", dispatch_mode="autograd"
820
+ )
821
+ return aot_dispatch_autograd
822
+ else:
823
+ chromium_log.try_add_event_data(
824
+ "backend_compile", dispatch_mode="inference"
825
+ )
826
+ return aot_dispatch_base
827
+
828
+ compiler_fn = choose_dispatcher(needs_autograd, aot_config)
829
+
830
+ compiled_fn, fw_metadata = compiler_fn(
831
+ flat_fn,
832
+ _dup_fake_script_obj(fake_flat_args),
833
+ aot_config,
834
+ fw_metadata=fw_metadata,
835
+ )
836
+ return compiled_fn, fw_metadata
837
+
838
+
839
+ def aot_function(
840
+ fn: Callable,
841
+ fw_compiler: Callable,
842
+ bw_compiler: Optional[Callable] = None,
843
+ partition_fn: Callable = default_partition,
844
+ decompositions: Optional[Dict] = None,
845
+ num_params_buffers: int = 0,
846
+ keep_inference_input_mutations: bool = False,
847
+ inference_compiler: Optional[Callable] = None,
848
+ *,
849
+ # Whether or not to trace with dynamic shapes
850
+ dynamic=False,
851
+ enable_log=True,
852
+ ) -> Callable:
853
+ """
854
+ Traces the forward and backward graph of :attr:`fn` using torch dispatch
855
+ mechanism, and then compiles the generated forward and backward graphs
856
+ through :attr:`fw_compiler` and :attr:`bw_compiler`.
857
+
858
+ :func:`aot_function` traces the forward and backward graph ahead of time,
859
+ and generates a joint forward and backward graph. :attr:`partition_fn` is
860
+ then used to separate out forward and backward graphs. The partitioner
861
+ function can be used to perform optimizations such as recomputation. One can
862
+ set `decompositions` dictionary to decompose the operators into a sequence
863
+ of core or simpler operators supported by the backend compilers.
864
+
865
+ .. warning::
866
+ This API is experimental and likely to change.
867
+
868
+ Args:
869
+ fn (Callable): A Python function that takes one ore more arguments. Must
870
+ return one or more Tensors.
871
+ fw_compiler (Callable): A Python function that accepts an Fx graph with
872
+ Aten ops and input args, and returns a Callable that semantically is
873
+ equivalent to the input Fx graph.
874
+ bw_compiler (Optional[Callable]): A Python function that accepts an
875
+ Fx graph with Aten ops and input args, and returns a Callable that
876
+ semantically is equivalent to the input Fx graph. Default: None
877
+ (when None, it defaults to the :attr:`fw_compiler`)
878
+ partition_fn (Callable): A Python function that takes a joint forward
879
+ and backward graph, and partitions it into separate forward and
880
+ backward graphs.
881
+ decompositions (Dict): A dictionary to define the decomposition of
882
+ larger Aten ops into simpler or core Aten ops.
883
+ inference_compiler (Optional[Callable]): A Python function that accepts an
884
+ Fx graph with Aten ops and input args, and returns a Callable that
885
+ semantically is equivalent to the input Fx graph. inference_compiler is invoked
886
+ if no autograd is needed. Default: None
887
+ (when None, it defaults to the :attr:`fw_compiler`)
888
+ Returns:
889
+ Returns a ``Callable`` that retains the eager behavior of the original
890
+ :attr:`fn`, but with forward and backward graph compiled via
891
+ :attr:`fw_compile` and :attr:`bw_compile`.
892
+
893
+ A simple example usage of :func:`aot_function` is as follows. This example
894
+ will print the forward and backward graphs of the function ``fn``
895
+
896
+ >>> fn = lambda x : x.sin().cos()
897
+ >>> def print_compile_fn(fx_module, args):
898
+ >>> print(fx_module)
899
+ >>> return fx_module
900
+ >>> aot_fn = aot_function(fn, print_compile_fn)
901
+ >>> x = torch.randn(4, 5, requires_grad=True)
902
+ >>> aot_fn(x)
903
+ """
904
+
905
+ if bw_compiler is None:
906
+ bw_compiler = fw_compiler
907
+ if inference_compiler is None:
908
+ inference_compiler = fw_compiler
909
+ aot_config = AOTConfig(
910
+ fw_compiler=fw_compiler,
911
+ bw_compiler=bw_compiler,
912
+ inference_compiler=inference_compiler,
913
+ partition_fn=partition_fn,
914
+ decompositions=decompositions,
915
+ num_params_buffers=num_params_buffers,
916
+ aot_id=next(AOT_COUNTER),
917
+ keep_inference_input_mutations=keep_inference_input_mutations,
918
+ dynamic_shapes=dynamic,
919
+ aot_autograd_arg_pos_to_source=None,
920
+ is_export=False,
921
+ no_tangents=False,
922
+ enable_log=enable_log,
923
+ )
924
+ cached_res = None
925
+
926
+ @wraps(fn)
927
+ def returned_function(*args, **kwargs):
928
+ nonlocal cached_res
929
+ # Now flatten the tensor args
930
+ flat_args = pytree.arg_tree_leaves(*args, **kwargs)
931
+
932
+ # Compile the function and save it in the cache
933
+ if cached_res is None:
934
+ flat_fn, out_spec = create_tree_flattened_fn(fn, args, kwargs)
935
+ (fake_mode, shape_env) = construct_fake_mode(flat_args, aot_config)
936
+ fake_flat_args: FakifiedFlatArgs = process_inputs(
937
+ flat_args, aot_config, fake_mode, shape_env
938
+ )
939
+ compiled_fn, _ = create_aot_dispatcher_function(
940
+ flat_fn,
941
+ fake_flat_args,
942
+ aot_config,
943
+ fake_mode,
944
+ shape_env,
945
+ )
946
+ cached_res = (compiled_fn, out_spec)
947
+
948
+ cached_fn, out_spec = cached_res
949
+ out = cached_fn(flat_args)
950
+ return out_spec.unflatten(out)
951
+
952
+ return returned_function
953
+
954
+
955
+ def aot_module(mod: nn.Module, *args, **kwargs) -> nn.Module:
956
+ """
957
+ Traces the forward and backward graph of :attr:`mod` using torch dispatch
958
+ tracing mechanism. It is wrapper function, that underneath uses
959
+ :func:`aot_function` to perform tracing and compilation.
960
+
961
+ :func:`aot_module` lifts the parameters and buffers of ``nn.Module`` as inputs
962
+ to a new callable which is then compiled through :func:`aot_function`.
963
+
964
+ .. warning::
965
+ This API is experimental and likely to change.
966
+
967
+ Args:
968
+ mod (Callable): A ``nn.Module`` module.
969
+ args : args to be passed to :func:`aot_function`
970
+ kwargs : kwargs to be passed to :func:`aot_function`
971
+
972
+ Returns:
973
+ Returns a ``nn.Module`` that retains the eager behavior of the original
974
+ :attr:`mod`, but with forward and backward graph compiled.
975
+
976
+ """
977
+ # See Note: [Fake Modules and AOTAutograd]
978
+ torch._dynamo.utils.assert_no_fake_params_or_buffers(mod)
979
+
980
+ def functional_call(named_params, named_buffers, *args, **kwargs):
981
+ params_and_buffers = {**named_params, **named_buffers}
982
+ return torch.func.functional_call(mod, params_and_buffers, args, kwargs)
983
+
984
+ named_params = dict(mod.named_parameters(remove_duplicate=False))
985
+ named_buffers = dict(mod.named_buffers(remove_duplicate=False))
986
+ num_params_buffers = len(named_params) + len(named_buffers)
987
+ compiled_f = aot_function(
988
+ functional_call, *args, num_params_buffers=num_params_buffers, **kwargs
989
+ )
990
+
991
+ class AOTModule(nn.Module):
992
+ def __init__(self) -> None:
993
+ super().__init__()
994
+ self.orig_module = mod
995
+
996
+ def forward(self, *args, **kwargs):
997
+ return compiled_f(
998
+ named_params,
999
+ named_buffers,
1000
+ *args,
1001
+ **kwargs,
1002
+ )
1003
+
1004
+ return AOTModule()
1005
+
1006
+
1007
+ def aot_module_simplified(
1008
+ mod: nn.Module,
1009
+ args,
1010
+ fw_compiler: AOTDispatchCompiler,
1011
+ bw_compiler: Optional[AOTDispatchCompiler] = None,
1012
+ partition_fn: Callable = default_partition,
1013
+ decompositions: Optional[Dict] = None,
1014
+ keep_inference_input_mutations=False,
1015
+ inference_compiler: Optional[AOTDispatchCompiler] = None,
1016
+ cudagraphs: Optional[BoxedBool] = None,
1017
+ ) -> nn.Module:
1018
+ """
1019
+ This is the simplified or low overhead version of aot_module. For frontends
1020
+ like TorchDynamo, the input functions/modules to AOT are static and have
1021
+ unpacked inputs/outputs. This gives us an opportunity to remove the
1022
+ (1) pytree overhead to parse inputs/outputs,
1023
+ (2) AOT Autograd cache,
1024
+ (3) Reading of params/buffers in every forward call
1025
+
1026
+ :func:`aot_module_simplified` removes these overheads.
1027
+ """
1028
+ params = {
1029
+ **dict(mod.named_parameters(remove_duplicate=False)),
1030
+ **dict(mod.named_buffers(remove_duplicate=False)),
1031
+ }
1032
+ params_flat, params_spec = pytree.tree_flatten(params)
1033
+ params_flat = list(params_flat)
1034
+ params_len = len(params_flat)
1035
+
1036
+ if cudagraphs is None:
1037
+ cudagraphs = BoxedBool(torch._inductor.config.triton.cudagraphs)
1038
+
1039
+ if bw_compiler is None:
1040
+ bw_compiler = fw_compiler
1041
+ if inference_compiler is None:
1042
+ inference_compiler = fw_compiler
1043
+
1044
+ seen_sources = set()
1045
+
1046
+ full_args = []
1047
+ # First, the params
1048
+ full_args.extend(params_flat)
1049
+
1050
+ if tracing_context := torch._guards.TracingContext.try_get():
1051
+ tracing_context.params_flat = params_flat
1052
+ (
1053
+ tracing_context.params_flat_unwrap_subclasses,
1054
+ tracing_context.params_unwrapped_to_flat_index,
1055
+ ) = unwrap_tensor_subclasses_with_indices_to_original(params_flat)
1056
+
1057
+ aot_autograd_arg_pos_to_source = None
1058
+ # Then, the params 1:1 mapped sources, if relevant.
1059
+ if hasattr(mod, "_param_name_to_source"):
1060
+ aot_autograd_arg_pos_to_source = []
1061
+ # We now know this came from dynamo, and (1) we care about guards,
1062
+ # so setting up aot_autograd_arg_pos_to_source for downstream dedup guards
1063
+ # can now be done safely. (2) Dynamo logic protects the 1:1 sizing below.
1064
+ for name in params.keys():
1065
+ assert name in mod._param_name_to_source, f"{name} not found."
1066
+ source = mod._param_name_to_source[name]
1067
+ assert source not in seen_sources, source
1068
+ seen_sources.add(source)
1069
+ aot_autograd_arg_pos_to_source.append(source)
1070
+
1071
+ # Next, the input args
1072
+ full_args.extend(args)
1073
+
1074
+ static_input_indices = []
1075
+ if hasattr(mod, "graph"):
1076
+ # Non dynamo entrypoints can get to here...
1077
+ for pos, node in enumerate(mod.graph.find_nodes(op="placeholder")):
1078
+ if hasattr(node, "_dynamo_source"):
1079
+ # ... but not here!
1080
+ if aot_autograd_arg_pos_to_source is None:
1081
+ aot_autograd_arg_pos_to_source = []
1082
+ source = node._dynamo_source
1083
+ assert source not in seen_sources, source
1084
+ seen_sources.add(source)
1085
+ aot_autograd_arg_pos_to_source.append(source)
1086
+ source_name = source.name() if source else str(source)
1087
+
1088
+ if "tensor_dict" in node.meta and node.meta["tensor_dict"].get(
1089
+ "_dynamo_static_input_type", None
1090
+ ):
1091
+ static_inputs_log.debug(
1092
+ "Adding static input pos %s for source %s", pos, source_name
1093
+ )
1094
+ static_input_indices.append(pos)
1095
+ else:
1096
+ static_inputs_log.debug(
1097
+ "Non-static input pos %s for source %s", pos, source_name
1098
+ )
1099
+
1100
+ if aot_autograd_arg_pos_to_source is not None:
1101
+ assert len(full_args) == len(aot_autograd_arg_pos_to_source)
1102
+
1103
+ dynamic_shapes = False
1104
+ for x in full_args:
1105
+ if isinstance(x, FakeTensor):
1106
+ dynamic_shapes = x.fake_mode.shape_env is not None
1107
+ break
1108
+
1109
+ aot_config = AOTConfig(
1110
+ fw_compiler=fw_compiler,
1111
+ bw_compiler=bw_compiler,
1112
+ inference_compiler=inference_compiler,
1113
+ partition_fn=partition_fn,
1114
+ decompositions=decompositions,
1115
+ num_params_buffers=params_len,
1116
+ aot_id=next(AOT_COUNTER),
1117
+ keep_inference_input_mutations=keep_inference_input_mutations,
1118
+ dynamic_shapes=dynamic_shapes,
1119
+ aot_autograd_arg_pos_to_source=aot_autograd_arg_pos_to_source,
1120
+ static_input_indices=static_input_indices,
1121
+ is_export=False,
1122
+ no_tangents=False,
1123
+ cache_info=None,
1124
+ )
1125
+ fake_mode, shape_env = construct_fake_mode(full_args, aot_config)
1126
+ fake_flat_args = process_inputs(full_args, aot_config, fake_mode, shape_env)
1127
+
1128
+ def dispatch_and_compile():
1129
+ functional_call = create_functional_call(mod, params_spec, params_len)
1130
+ with compiled_autograd._disable():
1131
+ compiled_fn, _ = create_aot_dispatcher_function(
1132
+ functional_call,
1133
+ fake_flat_args,
1134
+ aot_config,
1135
+ fake_mode,
1136
+ shape_env,
1137
+ )
1138
+ return compiled_fn
1139
+
1140
+ # Autograd cache stuff
1141
+ remote = should_use_remote_autograd_cache()
1142
+ local = should_use_local_autograd_cache()
1143
+ # We only care if the forward will return an OutputCode.
1144
+ if (local or remote) and isinstance(fw_compiler, SerializableAOTDispatchCompiler):
1145
+ compiled_fn = AOTAutogradCache.load(
1146
+ dispatch_and_compile,
1147
+ mod,
1148
+ fake_flat_args,
1149
+ aot_config,
1150
+ cudagraphs,
1151
+ local,
1152
+ remote,
1153
+ )
1154
+ else:
1155
+ compiled_fn = dispatch_and_compile()
1156
+
1157
+ if isinstance(mod, torch._dynamo.utils.GmWrapper):
1158
+ # This function is called by the flatten_graph_inputs wrapper, which boxes
1159
+ # the inputs so that they can be freed before the end of this scope.
1160
+ # For overhead reasons, this is not the default wrapper, see comment:
1161
+ # https://github.com/pytorch/pytorch/pull/122535/files#r1560096481
1162
+ def boxed_forward(runtime_args: List[Any]):
1163
+ flat_args = []
1164
+ flat_args.extend(params_flat)
1165
+ flat_args.extend(runtime_args)
1166
+ runtime_args.clear()
1167
+ return compiled_fn(flat_args)
1168
+
1169
+ # Just for convenience
1170
+ boxed_forward.zero_grad = mod.zero_grad
1171
+ boxed_forward.named_parameters = mod.named_parameters
1172
+ boxed_forward.named_buffers = mod.named_buffers
1173
+ return boxed_forward
1174
+
1175
+ # TODO: There is something deeply wrong here; compiled_fn running with
1176
+ # the boxed calling convention, but aot_module_simplified somehow
1177
+ # historically returned a function that was not the boxed calling
1178
+ # convention. This should get fixed...
1179
+ # NB: GraphModule/nn.Module rely on the non-boxed calling convention here
1180
+ def forward(*runtime_args: Tuple[Any]):
1181
+ full_args = []
1182
+ full_args.extend(params_flat)
1183
+ full_args.extend(runtime_args)
1184
+ return compiled_fn(full_args)
1185
+
1186
+ # Just for convenience
1187
+ forward.zero_grad = mod.zero_grad
1188
+ forward.named_parameters = mod.named_parameters
1189
+ forward.named_buffers = mod.named_buffers
1190
+
1191
+ return forward
1192
+
1193
+
1194
+ def aot_export_module(
1195
+ mod: nn.Module,
1196
+ args,
1197
+ *,
1198
+ decompositions: Optional[Dict] = None,
1199
+ # If true, we'll return a joint forward-backward graph,
1200
+ # As well as metadata on the loss + gradients in the backward.
1201
+ trace_joint: bool,
1202
+ # If trace_joint is True, we expect your module to return a scalar loss.
1203
+ # Your module can return multiple outputs, so you must specify which output the loss is.
1204
+ output_loss_index: Optional[int] = None,
1205
+ pre_dispatch: bool = False,
1206
+ # If None, will be infered from inputs and mod.graph.nodes if mod is a graph module, but the inferred result might be wrong.
1207
+ dynamic_shapes: Optional[bool] = None,
1208
+ kwargs=None,
1209
+ ) -> Tuple[torch.fx.GraphModule, GraphSignature]:
1210
+ """
1211
+ This function takes in a module, and returns:
1212
+ (1) an FX graph that can be exported
1213
+ (2) some metadata about the graph
1214
+
1215
+ If `trace_joint=True` we will return a joint graph of the forward + backward.
1216
+
1217
+ The traced FX graph will have the following properties compared to the original module:
1218
+ (1) Inputs and outputs to the module will be pytree-flattened
1219
+ (2) Parameters and buffers on the module will be lifted into graph inputs,
1220
+ graph_inputs = (*parameters, *buffers, *user_inputs)
1221
+ (3) The graph will be fully functionalized
1222
+ (4) Any input mutations will be converted into additional outputs in the graph,
1223
+ meaning whoever calls this graph is responsible for applying the mutations
1224
+ back to the original inputs.
1225
+ (5) If is_joint is provided the graph will return parameter gradients in addition to user outputs.
1226
+ The graph output will look like:
1227
+ graph_outputs = (*updated_inputs, *user_outputs, *param_gradients)
1228
+
1229
+ There are also several restrictions on what modules can use this API. In particular:
1230
+ (1) If trace_joint is specified, we expect the loss function to be **fused**
1231
+ into the module forward. One of the outputs to the forward must be a scalar loss,
1232
+ which is specified with `output_loss_index`.
1233
+ All other outputs to the forward are presumed to not require gradients.
1234
+ (2) This API cannot capture optimizers (although in theory we could build an API for this).
1235
+ (3) Metadata mutations on params/buffers/inputs are banned.
1236
+ (4) Data mutations on anything that requires gradients are banned (parameters)
1237
+ (5) If an input is mutated, it is not allowed to alias any other inputs.
1238
+ (6) Parameters must not be duplicated.
1239
+ """
1240
+ if pre_dispatch and trace_joint:
1241
+ raise RuntimeError("pre_dispatch is not supported when trace_joint is True.")
1242
+ named_parameters = dict(mod.named_parameters(remove_duplicate=False))
1243
+ named_buffers = dict(mod.named_buffers(remove_duplicate=False))
1244
+
1245
+ params_and_buffers = {
1246
+ **dict(named_parameters),
1247
+ **dict(named_buffers),
1248
+ }
1249
+ params_and_buffers_flat, params_spec = pytree.tree_flatten(params_and_buffers)
1250
+ params_and_buffers_flat = tuple(params_and_buffers_flat)
1251
+ params_len = len(params_and_buffers_flat)
1252
+
1253
+ kwargs = kwargs or {}
1254
+
1255
+ functional_call = create_functional_call(
1256
+ mod, params_spec, params_len, store_orig_mod=True
1257
+ )
1258
+
1259
+ num_fw_outs = None
1260
+
1261
+ if trace_joint:
1262
+ # This helper effectively just adds some extra asserts about what the backward will look like:
1263
+ # Outputs must include a scalar loss, that we compute gradients w.r.t.
1264
+ # We don't compute gradients w.r.t. anything else: so just in case we detach()
1265
+ # and other output tensors.
1266
+ def fn_to_trace(*args):
1267
+ nonlocal num_fw_outs
1268
+ out = functional_call(*args)
1269
+ if output_loss_index is None:
1270
+ raise RuntimeError(
1271
+ """\
1272
+ If trace_joint=Trueit is required that one of your forward outputs must be a scalar loss.
1273
+ You must specify the which (index) output is the loss with output_loss_index."""
1274
+ )
1275
+ if isinstance(out, (torch.Tensor)):
1276
+ out = (out,)
1277
+ if not isinstance(out, (tuple, list)):
1278
+ raise RuntimeError(
1279
+ f"Expected forward output to be either a tensor or a list/tuple of tensors. found {type(out)}"
1280
+ )
1281
+
1282
+ for i, o in enumerate(out):
1283
+ # We only want to create a backward graph w.r.t. the loss that the user passed in.
1284
+ # This implies that every other output should not require gradients.
1285
+ # Instead of making this an error (and forcing the user to detach all other outputs
1286
+ # of their forward),
1287
+ # we'll automatically detach them here.
1288
+ if o.requires_grad and i != output_loss_index:
1289
+ raise RuntimeError(
1290
+ f"""\
1291
+ Found an output of the forward that requires gradients, that was not the scalar loss.
1292
+ We require all outputs to the forward that are not the scalar loss to not require gradient,
1293
+ because we will only compute a backward graph against the scalar loss.
1294
+ You can fix this by calling .detach() on each of your forward outputs that is not the loss.
1295
+ You specified that output index {output_loss_index} is the loss, but we found that
1296
+ the output at index {i} requires gradients."""
1297
+ )
1298
+ out_loss = out[output_loss_index]
1299
+ num_fw_outs = len(out)
1300
+ if not out_loss.requires_grad:
1301
+ raise RuntimeError(
1302
+ f"""\
1303
+ The output at index {output_loss_index} was marked as the loss, but it does not require gradients"""
1304
+ )
1305
+ if out_loss.numel() != 1:
1306
+ raise RuntimeError(
1307
+ f"""\
1308
+ We require the output marked as the loss (at index {output_loss_index}) to be a scalar, but it has shape {out_loss.shape}"""
1309
+ )
1310
+ return out
1311
+
1312
+ ctx = nullcontext
1313
+ else:
1314
+ # Run under no_grad, so our tracing machinery only traces an inference graph.
1315
+ # However if pre_dispatch=True, we want to correctly trace set_grad_enabled calls for training.
1316
+ ctx = nullcontext if pre_dispatch else torch.no_grad
1317
+ fn_to_trace = functional_call
1318
+
1319
+ full_args = []
1320
+ # First, the params
1321
+ # NB: It is REQUIRED that parameters come first, Inductor infers "fixed"
1322
+ # parameters by looking at the difference in parameter count outside
1323
+ # and inside AOTAutograd, and assumes the prefix of arguments are fixed
1324
+ # arguments
1325
+ full_args.extend(params_and_buffers_flat)
1326
+ # Next, the input args
1327
+ full_args.extend(args)
1328
+
1329
+ with ctx():
1330
+ fx_g, metadata, in_spec, out_spec = _aot_export_function(
1331
+ fn_to_trace,
1332
+ full_args,
1333
+ decompositions=decompositions,
1334
+ num_params_buffers=params_len,
1335
+ no_tangents=True,
1336
+ pre_dispatch=pre_dispatch,
1337
+ dynamic_shapes=dynamic_shapes,
1338
+ kwargs=kwargs,
1339
+ )
1340
+ if trace_joint:
1341
+
1342
+ @wraps(functional_call)
1343
+ def flattened_joint(*args):
1344
+ # The idea here is that the joint graph that AOTAutograd creates has some strict properties:
1345
+ # (1) It accepts two arguments (primals, tangents), and pytree_flattens them
1346
+ # (2) It returns a tuple of (fw_outs, gradients)
1347
+ # This is a very useful convention for anyone who wants to partition the joint graph
1348
+ # into a separate forward and backward graph.
1349
+ # However,
1350
+ # (1) for people exporting a single joint graph, it would be preferable not to have
1351
+ # any pytrees in the graph.
1352
+ # (2) We are guaranteed in the aot_export_module case that the forward outputs a loss,
1353
+ # and there are therefore no tangents that are needed to run the joint graph.
1354
+ # (3) AOTAutograd creates a grad_input for every input in the forward,
1355
+ # including None's for inputs that are not grad-requiring tensors.
1356
+ # we don't want these in our export graph.
1357
+ # and there are therefore no tangents that are needed to run the joint graph.
1358
+ # This function "fixes" both of the above by removing any tangent inputs,
1359
+ # and removing pytrees from the original FX graph.
1360
+ fake_tangents = [
1361
+ None
1362
+ for _ in range(
1363
+ metadata.num_outputs + metadata.num_mutated_inp_runtime_indices
1364
+ )
1365
+ ]
1366
+ fw_outs, gradients = fx_g(args, fake_tangents)
1367
+ assert len(gradients) == len(args)
1368
+ output_gradients = []
1369
+ for i, (a, grad) in enumerate(zip(args, gradients)):
1370
+ if isinstance(a, torch.Tensor) and a.requires_grad:
1371
+ assert (
1372
+ grad is not None
1373
+ ), """\
1374
+ Found a parameter that did not receive a gradient.
1375
+ "This is most likely a bug, but if this needs to be supported please comment on this Github issue:
1376
+ https://github.com/pytorch/pytorch/issues/101192
1377
+ """
1378
+ output_gradients.append(grad)
1379
+ else:
1380
+ assert grad is None
1381
+ return *fw_outs, *output_gradients
1382
+
1383
+ fx_g = make_fx(flattened_joint, record_module_stack=True)(*full_args)
1384
+
1385
+ user_args_flat = pytree.arg_tree_leaves(*args, **kwargs)
1386
+ return fx_g, create_graph_signature(
1387
+ fx_g,
1388
+ metadata,
1389
+ in_spec,
1390
+ out_spec,
1391
+ user_args_flat=user_args_flat,
1392
+ params_and_buffers_flat=params_and_buffers_flat,
1393
+ param_names=list(named_parameters.keys()),
1394
+ buffer_names=list(named_buffers.keys()),
1395
+ trace_joint=trace_joint,
1396
+ num_user_fw_outs=num_fw_outs,
1397
+ loss_index=output_loss_index,
1398
+ )
1399
+
1400
+
1401
+ def aot_export_joint_simple(
1402
+ func: Callable,
1403
+ args,
1404
+ *,
1405
+ trace_joint: bool,
1406
+ # It looks like the main consequence of this API is that for dynamic shapes,
1407
+ # it will assume that parms/buffers are static.
1408
+ # With the new inferred dynamic shapes API, maybe this doesn't matter?
1409
+ num_params_buffers: int = 0,
1410
+ decompositions: Optional[Dict] = None,
1411
+ ) -> torch.fx.GraphModule:
1412
+ """
1413
+ A simplified version of export. Used by higher order operators.
1414
+
1415
+ This function makes a high-level "no calling convention changes" guarantee:
1416
+ - If no inputs require grad (so we export an inference graph),
1417
+ there are *no* calling convention change between the exported graph, and "func".
1418
+ - If at least one input requires grad (so we trace out and export a joint fw-bw graph),
1419
+ Then if you were partition the graph into a separate forward and backward graph,
1420
+ The forward graph will have no calling convention changes compared to "func".
1421
+
1422
+ The above also relies on some strong restrictions around which functions this API accepts:
1423
+ (1) `args` cannot contain any pytrees (they must have been pytree_flattened already)
1424
+ (2) `func` cannot mutate any inputs
1425
+ (3) The outputs of `func` cannot alias any inputs.
1426
+
1427
+ Note: this function is only lightly tested today. It will probably be tested more heavily by higher order ops.
1428
+ """
1429
+ if trace_joint:
1430
+ ctx = nullcontext
1431
+ else:
1432
+ # Run under no_grad, so our tracing machinery only traces an inference graph.
1433
+ ctx = torch.no_grad
1434
+
1435
+ with ctx():
1436
+ fx_g, metadata, in_spec, out_spec = _aot_export_function(
1437
+ func,
1438
+ args,
1439
+ decompositions=decompositions,
1440
+ )
1441
+ in_spec, _kw_in_spec = in_spec.children_specs
1442
+ # At this point, we can just directly return the (joint or inference graph) that we traced.
1443
+ # First though: a bunch of assertions to make sure that our graph doesn't require
1444
+ # any calling convention changes compared to the original function.
1445
+ # These restrictions are *in addition to* the general restrictions on export.
1446
+
1447
+ # No input mutations
1448
+ if (
1449
+ len([x for x in metadata.input_info if x.mutates_data or x.mutates_metadata])
1450
+ != 0
1451
+ ):
1452
+ raise RuntimeError(
1453
+ f"aot_export_joint_simple does not support input mutations. {str(metadata)}"
1454
+ )
1455
+ # No output aliasing
1456
+ if (
1457
+ len([x for x in metadata.output_info if x.output_type != OutputType.non_alias])
1458
+ != 0
1459
+ ):
1460
+ raise RuntimeError(
1461
+ f"aot_export_joint_simple does not support outputs that alias inputs. {str(metadata)}"
1462
+ )
1463
+ # No pytrees
1464
+ if in_spec.is_leaf():
1465
+ raise RuntimeError(
1466
+ f"aot_export_joint_simple requires inputs to be a single list/tuple. in_spec={str(in_spec)}"
1467
+ )
1468
+ if not all(child.is_leaf() for child in in_spec.children_specs):
1469
+ raise RuntimeError(
1470
+ f"aot_export_joint_simple requires individual inputs not to be pytrees. in_spec={str(in_spec)}"
1471
+ )
1472
+ if out_spec.is_leaf():
1473
+ raise RuntimeError(
1474
+ f"aot_export_joint_simple requires outputs to be a single list/tuple. out_spec={str(out_spec)}"
1475
+ )
1476
+ if not all(child.is_leaf() for child in out_spec.children_specs):
1477
+ raise RuntimeError(
1478
+ f"aot_export_joint_simple requires individual outputs not to be pytrees. out_spec={str(out_spec)}"
1479
+ )
1480
+ # TODO: we might have to temporarily patch config.functionalize_rng
1481
+ # so that it doesn't run when we're exporting a higher order op.
1482
+
1483
+ if config.debug_assert:
1484
+ # Smoke test that after partitioning, we can run the forward without any calling convention changes.
1485
+ fw_module, bw_module = aot_config.default_partition( # noqa: F821
1486
+ fx_g, args, num_fwd_outputs=len(fw_metadata.output_infos) # noqa: F821
1487
+ )
1488
+ # Attempt to run the fw_module with the original user inputs
1489
+ fake_mode = detect_fake_mode(args)
1490
+ if fake_mode is None:
1491
+ fake_mode = FakeTensorMode()
1492
+ with fake_mode:
1493
+ fw_module(*args)
1494
+ return fx_g
1495
+
1496
+
1497
+ # Private for now because we aren't providing a contract on what to return
1498
+ # for joint graphs (we could when there's a clearer use case)
1499
+ # In the future, we may need to add more export API's that provide their own strong guarantees.
1500
+ # This is meant as a general helper function for handling various export-y use cases.
1501
+ def _aot_export_function(
1502
+ func: Callable,
1503
+ args,
1504
+ *,
1505
+ num_params_buffers: int = 0,
1506
+ decompositions: Optional[Dict] = None,
1507
+ # If we're exporting a joint graph and we don't want any tangent inputs in the graph
1508
+ # (because we are backpropping through a scalar 1 loss),
1509
+ # we need to explicitly specify not to include tangents in the graph.
1510
+ # It's not enough just to check that our tangent is a scalar, since we also
1511
+ # need to know if it is a 1 (no need to make it a graph input), or something else
1512
+ # (requiring it to be a graph input).
1513
+ # We don't know this info at trace time though, so we need to make it an explicit config.
1514
+ no_tangents: bool = False,
1515
+ pre_dispatch: bool = False,
1516
+ # If None, `dynamic_shapes` will be infered from inputs, but the inferred result might be wrong.
1517
+ dynamic_shapes: Optional[bool] = None,
1518
+ kwargs=None,
1519
+ ) -> Tuple[torch.fx.GraphModule, ViewAndMutationMeta, pytree.TreeSpec, pytree.TreeSpec]:
1520
+ kwargs = kwargs or {}
1521
+
1522
+ flat_fn, out_spec = create_tree_flattened_fn(func, args, kwargs)
1523
+ flat_args, in_spec = pytree.tree_flatten((args, kwargs))
1524
+
1525
+ fake_mode = None
1526
+ if dynamic_shapes is None:
1527
+ # Try to infer `dynamic_shapes from inputs and graph nodes
1528
+ fake_mode = detect_fake_mode(flat_args)
1529
+ if (
1530
+ fake_mode is None
1531
+ and hasattr(func, "_orig_mod")
1532
+ and isinstance(func._orig_mod, torch.fx.GraphModule)
1533
+ ):
1534
+ vals = [
1535
+ node.meta["val"]
1536
+ for node in func._orig_mod.graph.nodes
1537
+ if "val" in node.meta
1538
+ ]
1539
+ fake_mode = detect_fake_mode(vals)
1540
+ dynamic_shapes = fake_mode is not None and fake_mode.shape_env is not None
1541
+
1542
+ # The export use case doesn't care about several bits of AOTConfig
1543
+ # (1) compilers (we just export the graph)
1544
+ # (2) partitioners (export is only full graph, user can partition themselves)
1545
+ aot_config = AOTConfig(
1546
+ fw_compiler=None,
1547
+ bw_compiler=None,
1548
+ inference_compiler=None,
1549
+ partition_fn=None,
1550
+ decompositions=decompositions,
1551
+ num_params_buffers=num_params_buffers,
1552
+ aot_id=next(AOT_COUNTER),
1553
+ # For now there's no use case involving keeping input mutations in the graph
1554
+ # (which we can only do in the inference case anyway).
1555
+ # We can add this later if we need to.
1556
+ keep_inference_input_mutations=False,
1557
+ dynamic_shapes=dynamic_shapes,
1558
+ aot_autograd_arg_pos_to_source=None,
1559
+ is_export=True,
1560
+ no_tangents=no_tangents,
1561
+ pre_dispatch=pre_dispatch,
1562
+ )
1563
+ if fake_mode is None:
1564
+ fake_mode, shape_env = construct_fake_mode(flat_args, aot_config)
1565
+ else:
1566
+ shape_env = fake_mode.shape_env
1567
+ fake_flat_args = process_inputs(flat_args, aot_config, fake_mode, shape_env)
1568
+
1569
+ fx_g, meta = create_aot_dispatcher_function(
1570
+ flat_fn,
1571
+ fake_flat_args,
1572
+ aot_config,
1573
+ fake_mode,
1574
+ shape_env,
1575
+ )
1576
+ return fx_g, meta, in_spec, out_spec.spec
1577
+
1578
+
1579
+ @contextmanager
1580
+ def _detect_attribute_assignment(mod: torch.nn.Module):
1581
+ # Do not allow assignment of tensor attributes during export unless
1582
+ # the attribute is registered as a buffer.
1583
+
1584
+ NN_MODULE_STD_ATTRS = [
1585
+ "_backward_hooks",
1586
+ "_backward_pre_hooks",
1587
+ "_buffers",
1588
+ "_forward_hooks",
1589
+ "_forward_hooks_always_called",
1590
+ "_forward_hooks_with_kwargs",
1591
+ "_forward_pre_hooks",
1592
+ "_forward_pre_hooks_with_kwargs",
1593
+ "_is_full_backward_hook",
1594
+ "_load_state_dict_post_hooks",
1595
+ "_load_state_dict_pre_hooks",
1596
+ "_modules",
1597
+ "_non_persistent_buffers_set",
1598
+ "_parameters",
1599
+ "_state_dict_hooks",
1600
+ "_state_dict_pre_hooks",
1601
+ "training",
1602
+ ]
1603
+ NN_MODULE_LAZY_STD_ATTRS = [
1604
+ "_initialize_hook",
1605
+ "_load_hook",
1606
+ ]
1607
+ STD_ATTRS = {
1608
+ *NN_MODULE_STD_ATTRS,
1609
+ *NN_MODULE_LAZY_STD_ATTRS,
1610
+ }
1611
+
1612
+ def _get_attributes(mod):
1613
+ # return any attributes of a module that are not standard attributes
1614
+ return {k: v for k, v in mod.__dict__.items() if k not in STD_ATTRS}
1615
+
1616
+ def is_leaf(x):
1617
+ # Ideally is_leaf should not be needed when mapping, but it seems that
1618
+ # subclasses of a standard container X may sometimes map to X, which
1619
+ # destroys information and can cause future mapping to fail.
1620
+ known_subclasses_that_lose_info = (
1621
+ torch.Size,
1622
+ # add more here if needed
1623
+ )
1624
+ return isinstance(x, known_subclasses_that_lose_info)
1625
+
1626
+ # save state of attributes before enter
1627
+ snapshot = pytree.tree_map(lambda x: x, _get_attributes(mod), is_leaf=is_leaf)
1628
+ try:
1629
+ yield
1630
+ finally:
1631
+ # after exit, compare state of attributes with snapshot
1632
+ # to detect which tensor attributes were assigned
1633
+ assigned_tensor_attributes = []
1634
+
1635
+ def _collect_assigned_tensor_attributes(kp, v, _v):
1636
+ if _v is not v:
1637
+ attr, *rest = kp
1638
+ if isinstance(v, torch.Tensor):
1639
+ assigned_tensor_attributes.append(
1640
+ f"self.{attr.key}{pytree.keystr(rest)}"
1641
+ )
1642
+ # TODO(avik): Assigning all other types are allowed right now.
1643
+ # Maybe in the future we want to limit this to primitive types?
1644
+
1645
+ pytree.tree_map_with_path(
1646
+ _collect_assigned_tensor_attributes, snapshot, _get_attributes(mod)
1647
+ )
1648
+ # restore state of all attributes (including, e.g., of primitive types)
1649
+ mod.__dict__.update(snapshot)
1650
+
1651
+ if assigned_tensor_attributes:
1652
+ if len(assigned_tensor_attributes) > 1:
1653
+ noun, verb = "attributes", "were"
1654
+ else:
1655
+ noun, verb = "attribute", "was"
1656
+ raise ValueError(
1657
+ f"The tensor {noun} {', '.join(assigned_tensor_attributes)} {verb} assigned during export. "
1658
+ "Such attributes must be registered as buffers using the `register_buffer` API "
1659
+ "(https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.register_buffer)."
1660
+ )
1661
+
1662
+
1663
+ compiled_function = aot_function
1664
+ compiled_module = aot_module
lib/python3.12/site-packages/torch/_functorch/apis.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-decorators
2
+ # mypy: allow-untyped-defs
3
+ # NOTE: We allow Dynamo to see this file (via torch/_dynamo/trace_rules.py) so that it can
4
+ # trace through functorch transforms.
5
+ # Currently, we can't allow Dynamo to see `eager_transforms.py`/`vmap.py` as that break a lot of thing
6
+ # and there isn't a mechanism to selectively expose only some functions (eg. grad) from a file
7
+ # to Dynamo.
8
+ import functools
9
+
10
+ from torch._functorch.utils import argnums_t, exposed_in
11
+ from torch._functorch.vmap import (
12
+ _check_out_dims_is_int_or_int_pytree,
13
+ _check_randomness_arg,
14
+ _chunked_vmap,
15
+ _process_batched_inputs,
16
+ Callable,
17
+ in_dims_t,
18
+ out_dims_t,
19
+ vmap_impl,
20
+ )
21
+
22
+
23
+ # vmap(func)(inputs) wraps all Tensor inputs to be batched in BatchedTensors,
24
+ # sends those into func, and then unwraps the output BatchedTensors. Operations
25
+ # on BatchedTensors perform the batched operations that the user is asking for.
26
+ #
27
+ # vmap's randomness behavior differs from JAX's, which would require a PRNG key
28
+ # to be passed everywhere.
29
+
30
+
31
+ @exposed_in("torch.func")
32
+ def vmap(
33
+ func: Callable,
34
+ in_dims: in_dims_t = 0,
35
+ out_dims: out_dims_t = 0,
36
+ randomness: str = "error",
37
+ *,
38
+ chunk_size=None,
39
+ ) -> Callable:
40
+ """
41
+ vmap is the vectorizing map; ``vmap(func)`` returns a new function that
42
+ maps ``func`` over some dimension of the inputs. Semantically, vmap
43
+ pushes the map into PyTorch operations called by ``func``, effectively
44
+ vectorizing those operations.
45
+
46
+ vmap is useful for handling batch dimensions: one can write a function
47
+ ``func`` that runs on examples and then lift it to a function that can
48
+ take batches of examples with ``vmap(func)``. vmap can also be used to
49
+ compute batched gradients when composed with autograd.
50
+
51
+ .. note::
52
+ :func:`torch.vmap` is aliased to :func:`torch.func.vmap` for
53
+ convenience. Use whichever one you'd like.
54
+
55
+ Args:
56
+ func (function): A Python function that takes one or more arguments.
57
+ Must return one or more Tensors.
58
+ in_dims (int or nested structure): Specifies which dimension of the
59
+ inputs should be mapped over. ``in_dims`` should have a
60
+ structure like the inputs. If the ``in_dim`` for a particular
61
+ input is None, then that indicates there is no map dimension.
62
+ Default: 0.
63
+ out_dims (int or Tuple[int]): Specifies where the mapped dimension
64
+ should appear in the outputs. If ``out_dims`` is a Tuple, then
65
+ it should have one element per output. Default: 0.
66
+ randomness (str): Specifies whether the randomness in this
67
+ vmap should be the same or different across batches. If 'different',
68
+ the randomness for each batch will be different. If 'same', the
69
+ randomness will be the same across batches. If 'error', any calls to
70
+ random functions will error. Default: 'error'. WARNING: this flag
71
+ only applies to random PyTorch operations and does not apply to
72
+ Python's random module or numpy randomness.
73
+ chunk_size (None or int): If None (default), apply a single vmap over inputs.
74
+ If not None, then compute the vmap :attr:`chunk_size` samples at a time.
75
+ Note that :attr:`chunk_size=1` is equivalent to computing the vmap with a for-loop.
76
+ If you run into memory issues computing the vmap, please try a non-None chunk_size.
77
+
78
+ Returns:
79
+ Returns a new "batched" function. It takes the same inputs as
80
+ ``func``, except each input has an extra dimension at the index
81
+ specified by ``in_dims``. It takes returns the same outputs as
82
+ ``func``, except each output has an extra dimension at the index
83
+ specified by ``out_dims``.
84
+
85
+ .. warning:
86
+ :func:`vmap` works best with functional-style code. Please do not
87
+ perform any side-effects in ``func``, with the exception of
88
+ in-place PyTorch operations. Examples of side-effects include mutating
89
+ Python data structures and assigning values to variables not captured
90
+ in ``func``.
91
+
92
+ One example of using :func:`vmap` is to compute batched dot products. PyTorch
93
+ doesn't provide a batched ``torch.dot`` API; instead of unsuccessfully
94
+ rummaging through docs, use :func:`vmap` to construct a new function.
95
+
96
+ >>> torch.dot # [D], [D] -> []
97
+ >>> batched_dot = torch.func.vmap(torch.dot) # [N, D], [N, D] -> [N]
98
+ >>> x, y = torch.randn(2, 5), torch.randn(2, 5)
99
+ >>> batched_dot(x, y)
100
+
101
+ :func:`vmap` can be helpful in hiding batch dimensions, leading to a simpler
102
+ model authoring experience.
103
+
104
+ >>> batch_size, feature_size = 3, 5
105
+ >>> weights = torch.randn(feature_size, requires_grad=True)
106
+ >>>
107
+ >>> def model(feature_vec):
108
+ >>> # Very simple linear model with activation
109
+ >>> return feature_vec.dot(weights).relu()
110
+ >>>
111
+ >>> examples = torch.randn(batch_size, feature_size)
112
+ >>> result = torch.vmap(model)(examples)
113
+
114
+ :func:`vmap` can also help vectorize computations that were previously difficult
115
+ or impossible to batch. One example is higher-order gradient computation.
116
+ The PyTorch autograd engine computes vjps (vector-Jacobian products).
117
+ Computing a full Jacobian matrix for some function f: R^N -> R^N usually
118
+ requires N calls to ``autograd.grad``, one per Jacobian row. Using :func:`vmap`,
119
+ we can vectorize the whole computation, computing the Jacobian in a single
120
+ call to ``autograd.grad``.
121
+
122
+ >>> # Setup
123
+ >>> N = 5
124
+ >>> f = lambda x: x ** 2
125
+ >>> x = torch.randn(N, requires_grad=True)
126
+ >>> y = f(x)
127
+ >>> I_N = torch.eye(N)
128
+ >>>
129
+ >>> # Sequential approach
130
+ >>> jacobian_rows = [torch.autograd.grad(y, x, v, retain_graph=True)[0]
131
+ >>> for v in I_N.unbind()]
132
+ >>> jacobian = torch.stack(jacobian_rows)
133
+ >>>
134
+ >>> # vectorized gradient computation
135
+ >>> def get_vjp(v):
136
+ >>> return torch.autograd.grad(y, x, v)
137
+ >>> jacobian = torch.vmap(get_vjp)(I_N)
138
+
139
+ :func:`vmap` can also be nested, producing an output with multiple batched dimensions
140
+
141
+ >>> torch.dot # [D], [D] -> []
142
+ >>> batched_dot = torch.vmap(torch.vmap(torch.dot)) # [N1, N0, D], [N1, N0, D] -> [N1, N0]
143
+ >>> x, y = torch.randn(2, 3, 5), torch.randn(2, 3, 5)
144
+ >>> batched_dot(x, y) # tensor of size [2, 3]
145
+
146
+ If the inputs are not batched along the first dimension, ``in_dims`` specifies
147
+ the dimension that each inputs are batched along as
148
+
149
+ >>> torch.dot # [N], [N] -> []
150
+ >>> batched_dot = torch.vmap(torch.dot, in_dims=1) # [N, D], [N, D] -> [D]
151
+ >>> x, y = torch.randn(2, 5), torch.randn(2, 5)
152
+ >>> batched_dot(x, y) # output is [5] instead of [2] if batched along the 0th dimension
153
+
154
+ If there are multiple inputs each of which is batched along different dimensions,
155
+ ``in_dims`` must be a tuple with the batch dimension for each input as
156
+
157
+ >>> torch.dot # [D], [D] -> []
158
+ >>> batched_dot = torch.vmap(torch.dot, in_dims=(0, None)) # [N, D], [D] -> [N]
159
+ >>> x, y = torch.randn(2, 5), torch.randn(5)
160
+ >>> batched_dot(x, y) # second arg doesn't have a batch dim because in_dim[1] was None
161
+
162
+ If the input is a Python struct, ``in_dims`` must be a tuple containing a struct
163
+ matching the shape of the input:
164
+
165
+ >>> f = lambda dict: torch.dot(dict['x'], dict['y'])
166
+ >>> x, y = torch.randn(2, 5), torch.randn(5)
167
+ >>> input = {'x': x, 'y': y}
168
+ >>> batched_dot = torch.vmap(f, in_dims=({'x': 0, 'y': None},))
169
+ >>> batched_dot(input)
170
+
171
+ By default, the output is batched along the first dimension. However, it can be batched
172
+ along any dimension by using ``out_dims``
173
+
174
+ >>> f = lambda x: x ** 2
175
+ >>> x = torch.randn(2, 5)
176
+ >>> batched_pow = torch.vmap(f, out_dims=1)
177
+ >>> batched_pow(x) # [5, 2]
178
+
179
+ For any function that uses kwargs, the returned function will not batch the kwargs but will
180
+ accept kwargs
181
+
182
+ >>> x = torch.randn([2, 5])
183
+ >>> def fn(x, scale=4.):
184
+ >>> return x * scale
185
+ >>>
186
+ >>> batched_pow = torch.vmap(fn)
187
+ >>> assert torch.allclose(batched_pow(x), x * 4)
188
+ >>> batched_pow(x, scale=x) # scale is not batched, output has shape [2, 2, 5]
189
+
190
+ .. note::
191
+ vmap does not provide general autobatching or handle variable-length
192
+ sequences out of the box.
193
+ """
194
+ from torch._dynamo import is_compiling
195
+
196
+ _check_randomness_arg(randomness)
197
+ if not (chunk_size is None or chunk_size > 0):
198
+ raise ValueError(
199
+ f"vmap: chunk_size should be None or greater than 0. (got {chunk_size})"
200
+ )
201
+
202
+ def wrapped(*args, **kwargs):
203
+ return vmap_impl(
204
+ func, in_dims, out_dims, randomness, chunk_size, *args, **kwargs
205
+ )
206
+
207
+ if not is_compiling():
208
+ wrapped = functools.wraps(func)(wrapped)
209
+
210
+ return wrapped
211
+
212
+
213
+ def chunk_vmap(
214
+ func: Callable,
215
+ in_dims: in_dims_t = 0,
216
+ out_dims: out_dims_t = 0,
217
+ randomness: str = "error",
218
+ chunks=2,
219
+ ) -> Callable:
220
+ """
221
+ chunk_vmap is the vectorizing map (vmap) using chunks of input data. It is a mix of vmap (which vectorizes
222
+ everything) and map (which executes things sequentially). ``chunk_vmap`` vectorizes the input with number of
223
+ chunks at a time. For more details about vectorizing map, see :func:`vmap`.
224
+
225
+ .. note::
226
+ Please use :func:`vmap` with ``chunk_size`` argument instead of this API.
227
+
228
+ Args:
229
+ func (function): A Python function that takes one or more arguments.
230
+ Must return one or more Tensors.
231
+ in_dims (int or nested structure): Specifies which dimension of the
232
+ inputs should be mapped over. ``in_dims`` should have a
233
+ structure like the inputs. If the ``in_dim`` for a particular
234
+ input is None, then that indicates there is no map dimension.
235
+ Default: 0.
236
+ out_dims (int or Tuple[int]): Specifies where the mapped dimension
237
+ should appear in the outputs. If ``out_dims`` is a Tuple, then
238
+ it should have one element per output. Default: 0.
239
+ randomness (str): Specifies whether the randomness in this
240
+ vmap should be the same or different across batches. If 'different',
241
+ the randomness for each batch will be different. If 'same', the
242
+ randomness will be the same across batches. If 'error', any calls to
243
+ random functions will error. Default: 'error'. WARNING: this flag
244
+ only applies to random PyTorch operations and does not apply to
245
+ Python's random module or numpy randomness.
246
+ chunks (int): Number of chunks to use to split the input data. Default is 2.
247
+ If equals to 1 then :func:`vmap` is called.
248
+
249
+ Returns:
250
+ Returns a new "batched" function. It takes the same inputs as
251
+ ``func``, except each input has an extra dimension at the index
252
+ specified by ``in_dims``. It takes returns the same outputs as
253
+ ``func``, except each output has an extra dimension at the index
254
+ specified by ``out_dims``.
255
+ """
256
+ _check_randomness_arg(randomness)
257
+
258
+ if chunks == 1:
259
+ return vmap(func, in_dims=in_dims, out_dims=out_dims, randomness=randomness)
260
+
261
+ def _get_chunk_flat_args(flat_args_, flat_in_dims_, chunks_):
262
+ flat_args_chunks = tuple(
263
+ t.chunk(chunks_, dim=in_dim)
264
+ if in_dim is not None
265
+ else [
266
+ t,
267
+ ]
268
+ * chunks_
269
+ for t, in_dim in zip(flat_args_, flat_in_dims_)
270
+ )
271
+ # transpose chunk dim and flatten structure
272
+ # chunks_flat_args is a list of flatten args
273
+ chunks_flat_args = zip(*flat_args_chunks)
274
+ return chunks_flat_args
275
+
276
+ @functools.wraps(func)
277
+ def wrapped_with_chunks(*args, **kwargs):
278
+ _check_out_dims_is_int_or_int_pytree(out_dims, func)
279
+ _, flat_in_dims, flat_args, args_spec = _process_batched_inputs(
280
+ in_dims, args, func
281
+ )
282
+ # Chunk flat arguments
283
+ chunks_flat_args = _get_chunk_flat_args(flat_args, flat_in_dims, chunks)
284
+
285
+ # Apply vmap on chunks
286
+ return _chunked_vmap(
287
+ func,
288
+ flat_in_dims,
289
+ chunks_flat_args,
290
+ args_spec,
291
+ out_dims,
292
+ randomness,
293
+ **kwargs,
294
+ )
295
+
296
+ return wrapped_with_chunks
297
+
298
+
299
+ @exposed_in("torch.func")
300
+ def grad(func: Callable, argnums: argnums_t = 0, has_aux: bool = False) -> Callable:
301
+ """``grad`` operator helps computing gradients of ``func`` with respect to the
302
+ input(s) specified by ``argnums``. This operator can be nested to
303
+ compute higher-order gradients.
304
+
305
+ Args:
306
+ func (Callable): A Python function that takes one or more arguments.
307
+ Must return a single-element Tensor. If specified ``has_aux`` equals ``True``,
308
+ function can return a tuple of single-element Tensor and other auxiliary objects:
309
+ ``(output, aux)``.
310
+ argnums (int or Tuple[int]): Specifies arguments to compute gradients with respect to.
311
+ ``argnums`` can be single integer or tuple of integers. Default: 0.
312
+ has_aux (bool): Flag indicating that ``func`` returns a tensor and other
313
+ auxiliary objects: ``(output, aux)``. Default: False.
314
+
315
+ Returns:
316
+ Function to compute gradients with respect to its inputs. By default, the output of
317
+ the function is the gradient tensor(s) with respect to the first argument.
318
+ If specified ``has_aux`` equals ``True``, tuple of gradients and output auxiliary objects
319
+ is returned. If ``argnums`` is a tuple of integers, a tuple of output gradients with
320
+ respect to each ``argnums`` value is returned.
321
+
322
+ Example of using ``grad``:
323
+
324
+ >>> # xdoctest: +SKIP
325
+ >>> from torch.func import grad
326
+ >>> x = torch.randn([])
327
+ >>> cos_x = grad(lambda x: torch.sin(x))(x)
328
+ >>> assert torch.allclose(cos_x, x.cos())
329
+ >>>
330
+ >>> # Second-order gradients
331
+ >>> neg_sin_x = grad(grad(lambda x: torch.sin(x)))(x)
332
+ >>> assert torch.allclose(neg_sin_x, -x.sin())
333
+
334
+ When composed with ``vmap``, ``grad`` can be used to compute per-sample-gradients:
335
+
336
+ >>> # xdoctest: +SKIP
337
+ >>> from torch.func import grad, vmap
338
+ >>> batch_size, feature_size = 3, 5
339
+ >>>
340
+ >>> def model(weights, feature_vec):
341
+ >>> # Very simple linear model with activation
342
+ >>> assert feature_vec.dim() == 1
343
+ >>> return feature_vec.dot(weights).relu()
344
+ >>>
345
+ >>> def compute_loss(weights, example, target):
346
+ >>> y = model(weights, example)
347
+ >>> return ((y - target) ** 2).mean() # MSELoss
348
+ >>>
349
+ >>> weights = torch.randn(feature_size, requires_grad=True)
350
+ >>> examples = torch.randn(batch_size, feature_size)
351
+ >>> targets = torch.randn(batch_size)
352
+ >>> inputs = (weights, examples, targets)
353
+ >>> grad_weight_per_example = vmap(grad(compute_loss), in_dims=(None, 0, 0))(*inputs)
354
+
355
+ Example of using ``grad`` with ``has_aux`` and ``argnums``:
356
+
357
+ >>> # xdoctest: +SKIP
358
+ >>> from torch.func import grad
359
+ >>> def my_loss_func(y, y_pred):
360
+ >>> loss_per_sample = (0.5 * y_pred - y) ** 2
361
+ >>> loss = loss_per_sample.mean()
362
+ >>> return loss, (y_pred, loss_per_sample)
363
+ >>>
364
+ >>> fn = grad(my_loss_func, argnums=(0, 1), has_aux=True)
365
+ >>> y_true = torch.rand(4)
366
+ >>> y_preds = torch.rand(4, requires_grad=True)
367
+ >>> out = fn(y_true, y_preds)
368
+ >>> # > output is ((grads w.r.t y_true, grads w.r.t y_preds), (y_pred, loss_per_sample))
369
+
370
+ .. note::
371
+ Using PyTorch ``torch.no_grad`` together with ``grad``.
372
+
373
+ Case 1: Using ``torch.no_grad`` inside a function:
374
+
375
+ >>> # xdoctest: +SKIP
376
+ >>> def f(x):
377
+ >>> with torch.no_grad():
378
+ >>> c = x ** 2
379
+ >>> return x - c
380
+
381
+ In this case, ``grad(f)(x)`` will respect the inner ``torch.no_grad``.
382
+
383
+ Case 2: Using ``grad`` inside ``torch.no_grad`` context manager:
384
+
385
+ >>> # xdoctest: +SKIP
386
+ >>> with torch.no_grad():
387
+ >>> grad(f)(x)
388
+
389
+ In this case, ``grad`` will respect the inner ``torch.no_grad``, but not the
390
+ outer one. This is because ``grad`` is a "function transform": its result
391
+ should not depend on the result of a context manager outside of ``f``.
392
+
393
+ """
394
+ # To avoid cyclical dependency.
395
+ import torch._functorch.eager_transforms as eager_transforms
396
+ from torch._dynamo import is_compiling
397
+
398
+ def wrapper(*args, **kwargs):
399
+ return eager_transforms.grad_impl(func, argnums, has_aux, args, kwargs)
400
+
401
+ if not is_compiling():
402
+ wrapper = functools.wraps(func)(wrapper)
403
+
404
+ return wrapper
405
+
406
+
407
+ @exposed_in("torch.func")
408
+ def grad_and_value(
409
+ func: Callable, argnums: argnums_t = 0, has_aux: bool = False
410
+ ) -> Callable:
411
+ """
412
+ Returns a function to compute a tuple of the gradient and primal, or
413
+ forward, computation.
414
+
415
+ Args:
416
+ func (Callable): A Python function that takes one or more arguments.
417
+ Must return a single-element Tensor. If specified ``has_aux``
418
+ equals ``True``, function can return a tuple of single-element
419
+ Tensor and other auxiliary objects: ``(output, aux)``.
420
+ argnums (int or Tuple[int]): Specifies arguments to compute gradients
421
+ with respect to. ``argnums`` can be single integer or tuple of
422
+ integers. Default: 0.
423
+ has_aux (bool): Flag indicating that ``func`` returns a tensor and
424
+ other auxiliary objects: ``(output, aux)``. Default: False.
425
+
426
+ Returns:
427
+ Function to compute a tuple of gradients with respect to its inputs
428
+ and the forward computation. By default, the output of the function is
429
+ a tuple of the gradient tensor(s) with respect to the first argument
430
+ and the primal computation. If specified ``has_aux`` equals
431
+ ``True``, tuple of gradients and tuple of the forward computation with
432
+ output auxiliary objects is returned. If ``argnums`` is a tuple of
433
+ integers, a tuple of a tuple of the output gradients with respect to
434
+ each ``argnums`` value and the forward computation is returned.
435
+
436
+ See :func:`grad` for examples
437
+ """
438
+ from torch._dynamo import is_compiling
439
+ from torch._functorch import eager_transforms
440
+
441
+ def wrapper(*args, **kwargs):
442
+ return eager_transforms.grad_and_value_impl(
443
+ func, argnums, has_aux, args, kwargs
444
+ )
445
+
446
+ if not is_compiling():
447
+ wrapper = functools.wraps(func)(wrapper)
448
+
449
+ return wrapper
lib/python3.12/site-packages/torch/_functorch/autograd_function.py ADDED
@@ -0,0 +1,764 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from typing import Any, NamedTuple, Tuple
3
+
4
+ import torch
5
+ import torch.utils._pytree as pytree
6
+ from torch._C._functorch import (
7
+ _unwrap_for_grad,
8
+ _wrap_for_grad,
9
+ current_level,
10
+ TransformType,
11
+ )
12
+ from torch._functorch.apis import vmap
13
+ from torch._functorch.utils import enable_single_level_autograd_function
14
+ from torch._functorch.vmap import (
15
+ _add_batch_dim,
16
+ _broadcast_to_and_flatten,
17
+ restore_vmap,
18
+ unwrap_batched,
19
+ wrap_batched,
20
+ )
21
+ from torch._ops import HigherOrderOperator
22
+ from torch.autograd.forward_ad import _set_fwd_grad_enabled
23
+
24
+
25
+ # autograd.Function technically runs before the regular PyTorch dispatcher.
26
+ # This is how features like autocast and torch_dispatch (e.g. PythonTLSSnapshot)
27
+ # work with it. One day we might decide to change this, but until then,
28
+ # we need to give the illusion that autograd.Function runs before those things.
29
+ #
30
+ # We do this by using creating a custom HigherOrderOperator that only functorch
31
+ # dispatches specially.
32
+ class CustomFunctionHigherOrderOperator(HigherOrderOperator):
33
+ def __init__(self) -> None:
34
+ super().__init__("custom_function_call")
35
+
36
+ def __call__(self, autograd_function, *args, **kwargs):
37
+ # When custom_function_call is done dispatching through functorch,
38
+ # it should just invoke the autograd.Function. This is consistent
39
+ # with the autograd.Function behavior of being invoked before the
40
+ # PyTorch dispatcher.
41
+ #
42
+ # This will lead us into trouble later down the line, but this is
43
+ # pre-existing. There is an invariant that a function traced by
44
+ # make_fx should have the same behavior when provided the same
45
+ # Tensor. However, make_fx sees autograd.Function as a composite
46
+ # (because autograd.Function happens before the Python dispatch key)
47
+ # and only traces the forward pass.
48
+ if torch._C._are_functorch_transforms_active():
49
+ return super().__call__(autograd_function, *args, **kwargs)
50
+ return autograd_function.apply(*args, **kwargs)
51
+
52
+
53
+ # "custom_function_call"
54
+ # This is the mechanism for an autograd.Function that works with functorch transforms.
55
+ # It wraps an autograd.Function; interactions with functorch transforms are defined
56
+ # via PyDispatcher and HigherOrderOperator rather than through the traditional PyTorch
57
+ # dispatcher.
58
+ custom_function_call = CustomFunctionHigherOrderOperator()
59
+
60
+
61
+ # The grad rule for custom_function_call is to construct a new _SingleLevelFunction
62
+ # (autograd.Function that only works with a single layer (level) of functorch) that:
63
+ # - unwraps the inputs
64
+ # - redispatches to custom_function_call
65
+ # - wraps the outputs
66
+ # and whose backward pass calls the original autograd.Function's backward.
67
+ #
68
+ # Why do we need to redispatch to custom_function_call?
69
+ # -----------------------------------------------------
70
+ # This is consistent with how ATen operators work with functorch's grad transform:
71
+ # they always redispatch to the original operator.
72
+ # Consider torch.sin, and let's say we do grad0(grad1(torch.sin))(x)
73
+ #
74
+ # grad1 will:
75
+ # - set up the autograd graph
76
+ # - unwrap the inputs
77
+ # - redispatch to at::sin (*)
78
+ # - rewrap the outputs on the return
79
+ #
80
+ # On the redispatch in (*), grad0 will:
81
+ # - set up the autograd graph
82
+ # - unwrap the inputs
83
+ # - redispatch to at::sin
84
+ # - rewrap the outputs on the return
85
+ #
86
+ # To "set up the autograd graph", we generate a _SingleLevelFunction
87
+ # and apply it.
88
+ @custom_function_call.py_impl(TransformType.Grad)
89
+ @custom_function_call.py_impl(TransformType.Jvp)
90
+ def custom_function_call_grad(interpreter, autograd_function, *operands):
91
+ Generated = generate_single_level_function(interpreter, autograd_function)
92
+ with enable_single_level_autograd_function():
93
+ flat_out = Generated.apply(*operands)
94
+ return flat_out
95
+
96
+
97
+ def generate_single_level_function(interpreter, autograd_function):
98
+ level = interpreter.level()
99
+
100
+ def forward(*operands):
101
+ unwrapped_operands = pytree.tree_map_only(
102
+ torch.Tensor, lambda x: _unwrap_for_grad(x, level), operands
103
+ )
104
+ # Both enable_grad() and _set_fwd_grad_enabled() are necessary no matter
105
+ # the transform. _SingleLevelFunction will turn off both fwd and bwd
106
+ # gradient computation and we need to turn it back on here.
107
+ with torch.enable_grad(), _set_fwd_grad_enabled(True), interpreter.lower():
108
+ unwrapped_output = custom_function_call(
109
+ autograd_function, *unwrapped_operands
110
+ )
111
+
112
+ # See NOTE [mark_dirty object identity check]
113
+ def wrap_fn(output):
114
+ return _wrap_for_grad(output, level)
115
+
116
+ return wrap_outputs_maintaining_identity(
117
+ unwrapped_output, unwrapped_operands, operands, wrap_fn
118
+ )
119
+
120
+ def setup_context(ctx, inputs, output):
121
+ return autograd_function.setup_context(ctx, inputs, output)
122
+
123
+ # backward is only used if the transform is TransformType.Grad
124
+ def backward(ctx, *grads):
125
+ result = autograd_function.backward(ctx, *grads)
126
+ return result
127
+
128
+ # jvp is only used if the transform is TransformType.Jvp
129
+ def jvp(ctx, *tangents):
130
+ result = autograd_function.jvp(ctx, *tangents)
131
+ return result
132
+
133
+ # This is the sequence of magic words to dynamically generate a Subclass with
134
+ # a given name. A Tensor's .grad_fn field has a class name that is the original
135
+ # autograd.Function's name + Backward, so we do this to generate some
136
+ # meaningful name.
137
+ name = f"{autograd_function.__name__}Generated"
138
+ Generated = type(
139
+ name,
140
+ (torch.autograd.function._SingleLevelFunction,),
141
+ {
142
+ "forward": staticmethod(forward),
143
+ "backward": staticmethod(backward),
144
+ "jvp": staticmethod(jvp),
145
+ "setup_context": staticmethod(setup_context),
146
+ },
147
+ )
148
+ return Generated
149
+
150
+
151
+ # wrap_outputs_maintaining_identity handles outputs from the vmap,
152
+ # backward (vjp), and jvp staticmethod. The way it distinguishes
153
+ # between the vmap case and the {backward, jvp} case is if the out_dims
154
+ # are specified or not.
155
+ #
156
+ # NB: we cannot use out_dims=None as the deciding factor. This because
157
+ # out_dims=None can still happen in the vmap staticmethod! What the
158
+ # user is saying in that case is that their output does not have a
159
+ # dimension that is being vmapped over, which is valid.
160
+ NO_OUT_DIMS = "not specified"
161
+
162
+
163
+ # NOTE [mark_dirty object identity check]
164
+ # autograd.Function's ctx.mark_dirty expect a returned input
165
+ # to have the same object identity as the input.
166
+ # Mode-only functorch will greatly simplify this logic.
167
+ def wrap_outputs_maintaining_identity(
168
+ outputs, unwrapped_inputs, orig_inputs, wrap_fn, out_dims=NO_OUT_DIMS
169
+ ):
170
+ flat_unwrapped_inputs = pytree.arg_tree_leaves(*unwrapped_inputs)
171
+ flat_orig_inputs = pytree.arg_tree_leaves(*orig_inputs)
172
+
173
+ unwrapped_input_to_orig_input = {
174
+ id(unwrapped): orig
175
+ for unwrapped, orig in zip(flat_unwrapped_inputs, flat_orig_inputs)
176
+ }
177
+
178
+ flat_outputs, spec = pytree.tree_flatten(outputs)
179
+ result = []
180
+
181
+ out_dims_specified = out_dims != NO_OUT_DIMS
182
+
183
+ if out_dims_specified:
184
+ flat_out_dims = _broadcast_to_and_flatten(out_dims, spec)
185
+ # _broadcast_to_and_flatten returns None if it is unable to broadcast.
186
+ # TODO: update following link from master to stable once that's out
187
+ if flat_out_dims is None:
188
+ raise RuntimeError(
189
+ f"The autograd.Function's vmap staticmethod returned an "
190
+ f"incompatible (output, out_dims) tuple. "
191
+ f"Expected out_dims={out_dims} "
192
+ f"to be compatible with the structure of `output`. "
193
+ f"out_dims has structure {pytree.tree_flatten(out_dims)[1]} "
194
+ f"but output has structure {spec}. "
195
+ f"For more details, please see "
196
+ f"https://pytorch.org/docs/main/notes/extending.func.html"
197
+ )
198
+
199
+ for i, output in enumerate(flat_outputs):
200
+ if not isinstance(output, torch.Tensor):
201
+ result.append(output)
202
+ continue
203
+ if id(output) in unwrapped_input_to_orig_input:
204
+ result.append(unwrapped_input_to_orig_input[id(output)])
205
+ continue
206
+ if out_dims_specified:
207
+ result.append(wrap_fn(output, flat_out_dims[i])) # type: ignore[possibly-undefined, index]
208
+ else:
209
+ result.append(wrap_fn(output))
210
+
211
+ return pytree.tree_unflatten(result, spec)
212
+
213
+
214
+ # NOTE: [functorch vjp and autograd interaction]
215
+ # There's an edge case with the functorch vjp and autograd interaction
216
+ # that will eventually be fixed by mode-only functorch.
217
+ # The TL;DR is that there's no way to unwrap a dead GradTensorWrapper,
218
+ # so we (the framework) need to do it manually. Regular PyTorch operators
219
+ # automatically do so this is consistent.
220
+ #
221
+ # class MyExp(torch.autograd.Function):
222
+ # @staticmethod
223
+ # def forward(x):
224
+ # return x.exp()
225
+ #
226
+ # @staticmethod
227
+ # def setup_context(ctx, inputs, output):
228
+ # y = output
229
+ # ctx.save_for_backward(y)
230
+ #
231
+ # @staticmethod
232
+ # def backward(gy):
233
+ # y, = ctx.saved_tensors()
234
+ # return MyMul.apply(gy, y)
235
+ #
236
+ # x = torch.randn([], requires_grad=True)
237
+ # gy = torch.randn([], requires_grad=True)
238
+ # _, vjp_fn = vjp(MySin.apply, x)
239
+ # result = vjp_fn(gy)
240
+ #
241
+ # MyMul is an autograd.Function that is not shown here.
242
+ # It saves a `y` for backward (since gy requires grad).
243
+ #
244
+ # in vjp_fn(gy), we get:
245
+ # > MyMul.apply(gy, GradTensorWrapper(y, level=dead))
246
+ # Because the y that is saved for backward by MyExp is a GradTensorWrapper
247
+ # but is now dead since we are outside the vjp context.
248
+ #
249
+ # PyTorch dispatcher operations, upon seeing a dead GradTensorWrapper,
250
+ # will automatically unwrap the GradTensorWrapper when applied.
251
+ # But since autograd.Function technically sits above the regular PyTorch
252
+ # dispatcher, it doesn't get this treatment. So we manually do
253
+ # the unwrapping to be consistent with regular PyTorch dispatcher operations.
254
+
255
+
256
+ class VmapInfo(NamedTuple):
257
+ batch_size: int
258
+ randomness: str
259
+
260
+
261
+ def has_overriden_vmap_rule(autograd_function):
262
+ return autograd_function.vmap is not torch.autograd.Function.vmap
263
+
264
+
265
+ def validate_vmap_returns_tuple_of_two_elements(result):
266
+ base_error_msg = (
267
+ "Expected the vmap staticmethod to have two returns, an output "
268
+ "and out_dims with pytree structure compatible with the output. "
269
+ )
270
+ if not isinstance(result, tuple):
271
+ raise RuntimeError(base_error_msg + f"Got a {type(result)} instead")
272
+ if not len(result) == 2:
273
+ raise RuntimeError(base_error_msg + f"Got {len(result)} returns instead")
274
+
275
+
276
+ @custom_function_call.py_impl(TransformType.Vmap)
277
+ def custom_function_call_vmap(interpreter, autograd_function, *operands, **kwargs):
278
+ if any(
279
+ isinstance(val, torch.Tensor)
280
+ for val in torch.utils._pytree.tree_flatten(kwargs)[0]
281
+ ):
282
+ raise NotImplementedError(
283
+ f"Run vmap on autograd.Function with kwarg-only Tensor args. "
284
+ f"Please do not pass kwarg-only Tensors to autograd.Function. "
285
+ f"Got: {kwargs}"
286
+ )
287
+
288
+ if autograd_function.generate_vmap_rule:
289
+ if has_overriden_vmap_rule(autograd_function):
290
+ # TODO: Update link to stable once that's out
291
+ # https://github.com/pytorch/pytorch/issues/92029
292
+ raise RuntimeError(
293
+ f"You tried to vmap over {autograd_function.__name__}, but "
294
+ f"it has both generate_vmap_rule=True and an overriden vmap "
295
+ f"staticmethod. Please set generate_vmap_rule=False or delete "
296
+ f"the overriden vmap staticmethod to avoid ambiguity. "
297
+ f"For more details, please see "
298
+ f"https://pytorch.org/docs/main/notes/extending.func.html"
299
+ )
300
+ return custom_function_call_vmap_generate_rule(
301
+ interpreter, autograd_function, *operands
302
+ )
303
+
304
+ if not has_overriden_vmap_rule(autograd_function):
305
+ # TODO: Update link to stable once that's out
306
+ # https://github.com/pytorch/pytorch/issues/92029
307
+ raise RuntimeError(
308
+ f"You tried to vmap over {autograd_function.__name__}, but "
309
+ f"it does not have vmap support. Please override and implement the "
310
+ f"vmap staticmethod or set generate_vmap_rule=True. "
311
+ f"For more details, please see "
312
+ f"https://pytorch.org/docs/main/notes/extending.func.html"
313
+ )
314
+
315
+ return custom_function_call_vmap_helper(
316
+ interpreter, autograd_function.vmap, autograd_function, *operands, **kwargs
317
+ )
318
+
319
+
320
+ def custom_function_call_vmap_helper(
321
+ interpreter, vmap_function, op, *operands, **kwargs
322
+ ):
323
+ current_level = interpreter.level()
324
+ info = VmapInfo(
325
+ batch_size=interpreter.batch_size(),
326
+ randomness=interpreter.randomness(),
327
+ )
328
+ # We're either in the autograd.Function case (vmap staticmethod)
329
+ # or the torch.library.register_vmap case.
330
+ autograd_function_case = isinstance(op, torch.autograd.function.FunctionMeta)
331
+
332
+ def lower_to_next():
333
+ if autograd_function_case:
334
+ return interpreter.lower()
335
+ else:
336
+ return torch._C._ExcludeDispatchKeyGuard(
337
+ torch._C.DispatchKeySet(torch._C.DispatchKey.FuncTorchBatched)
338
+ )
339
+
340
+ unwrapped_operands, in_dims = unwrap_batched(operands, current_level)
341
+ # If none of the tensors are batched at the current level, then we skip the
342
+ # current level. This saves the user from needing to handle this case in
343
+ # their vmap staticmethod (and is consistent with our C++ batching rule API)
344
+ if pytree.tree_all(lambda dim: dim is None, in_dims):
345
+ with lower_to_next():
346
+ if autograd_function_case:
347
+ return custom_function_call(op, *operands)
348
+ else:
349
+ return op(*operands, **kwargs)
350
+
351
+ with lower_to_next():
352
+ result = vmap_function(info, in_dims, *unwrapped_operands, **kwargs)
353
+ validate_vmap_returns_tuple_of_two_elements(result)
354
+ unwrapped_output, out_dims = result
355
+
356
+ # See NOTE [mark_dirty object identity check]
357
+ def wrap_fn(output, out_dim):
358
+ return (
359
+ output
360
+ if out_dim is None
361
+ else _add_batch_dim(output, out_dim, current_level)
362
+ )
363
+
364
+ return wrap_outputs_maintaining_identity(
365
+ unwrapped_output, unwrapped_operands, operands, wrap_fn, out_dims=out_dims
366
+ )
367
+
368
+
369
+ def custom_function_call_vmap_generate_rule(interpreter, autograd_function, *operands):
370
+ unwrapped_operands, in_dims = unwrap_batched(operands, interpreter.level())
371
+ vmapped_function, get_out_dims = vmapify_autograd_function(
372
+ autograd_function, in_dims, interpreter.batch_size(), interpreter.randomness()
373
+ )
374
+
375
+ with interpreter.lower():
376
+ output = custom_function_call(vmapped_function, *unwrapped_operands)
377
+
378
+ out_dims = get_out_dims()
379
+ return wrap_batched(output, out_dims, interpreter.level())
380
+
381
+
382
+ @custom_function_call.py_impl(TransformType.Functionalize)
383
+ def custom_function_call_functionalize(
384
+ interpreter, autograd_function, generate_vmap_rule, *operands
385
+ ):
386
+ raise RuntimeError("NYI: Functionalize rule for custom_function_call")
387
+
388
+
389
+ def vmapify_autograd_function(autograd_function, in_dims, batch_size, randomness):
390
+ # The following values are saved from the forward() and setup_context()
391
+ # and used in backward().
392
+ # Why do we save the values out here instead of on the ctx object?
393
+ # - out_dims: There's no way to retrieve this from forward()
394
+ # - input_shapes, saved_tensors_bdims: I'm a bit scared of nesting
395
+ # vmap(vmap( but not completely sure if it is a problem. If we
396
+ # assigned those fields to the ctx object, the worry is that they
397
+ # get overwritten.
398
+ init_val = "not populated"
399
+ out_dims = init_val
400
+ input_shapes: Any = init_val
401
+ saved_tensors_bdims: Any = init_val
402
+
403
+ def forward(*operands):
404
+ nonlocal out_dims
405
+ outputs, out_dims = restore_vmap(
406
+ autograd_function.forward, in_dims, batch_size, randomness
407
+ )(*operands)
408
+ return outputs
409
+
410
+ def setup_context(ctx, inputs, outputs):
411
+ input_shapes_ = None
412
+ saved_tensors_bdims_ = None
413
+
414
+ def inner(inputs, outputs):
415
+ # wrapped_ctx.save_for_backward will:
416
+ # - unwrap batchedtensors into (tensor, bdim)
417
+ # - save_for_backward(*unwrapped_tensors)
418
+ # - assign the bdims to wrapped_ctx._pt_saved_tensors_bdims
419
+ wrapped_ctx = CtxCustomSave(ctx, current_level())
420
+ autograd_function.setup_context(wrapped_ctx, inputs, outputs)
421
+
422
+ # input_shapes are used for reductify later to reduce expanded gradients
423
+ # to the correct shape.
424
+ # See NOTE: [Why can't we rely on autograd to reduce expanded gradients?]
425
+ # for more details
426
+ nonlocal input_shapes_
427
+ input_shapes_ = tuple(
428
+ inp.shape if isinstance(inp, torch.Tensor) else None for inp in inputs
429
+ )
430
+ nonlocal saved_tensors_bdims_
431
+ saved_tensors_bdims_ = wrapped_ctx._pt_saved_tensors_bdims
432
+
433
+ # See NOTE: [Why do we need to run setup_context under a vmap?]
434
+ restore_vmap(
435
+ inner,
436
+ (in_dims, out_dims),
437
+ batch_size,
438
+ randomness,
439
+ )(inputs, outputs)
440
+
441
+ nonlocal input_shapes
442
+ input_shapes = input_shapes_
443
+ nonlocal saved_tensors_bdims
444
+ saved_tensors_bdims = saved_tensors_bdims_
445
+
446
+ def jvp(ctx, *tangents):
447
+ assert out_dims != init_val
448
+ assert saved_tensors_bdims != init_val
449
+
450
+ def jvp_no_context(saved_tensors, tangents):
451
+ wrapped_ctx = CtxWithSavedTensors(ctx, saved_tensors)
452
+ return autograd_function.jvp(wrapped_ctx, *tangents)
453
+
454
+ tangent_in_dims = get_tangents_in_dims(in_dims, tangents)
455
+ out_tangents, out_tangents_dims = restore_vmap(
456
+ jvp_no_context,
457
+ (saved_tensors_bdims, tangent_in_dims),
458
+ batch_size,
459
+ randomness,
460
+ )(ctx.saved_tensors, tangents)
461
+
462
+ result = reductify(out_tangents, out_tangents_dims, out_dims, batch_size)
463
+ return result
464
+
465
+ def backward(ctx, *grad_outputs):
466
+ assert out_dims != init_val
467
+ assert input_shapes != init_val
468
+ assert saved_tensors_bdims != init_val
469
+
470
+ def backward_no_context(inputs):
471
+ saved_tensors, grad_outputs = inputs
472
+ wrapped_ctx = CtxWithSavedTensors(ctx, saved_tensors)
473
+ return autograd_function.backward(wrapped_ctx, *grad_outputs)
474
+
475
+ grad_ins, grad_ins_dims = restore_vmap(
476
+ backward_no_context,
477
+ ((saved_tensors_bdims, out_dims),),
478
+ batch_size,
479
+ randomness,
480
+ )((ctx.saved_tensors, grad_outputs))
481
+ result = reductify(grad_ins, grad_ins_dims, in_dims, batch_size, input_shapes)
482
+ return result
483
+
484
+ name = f"Vmapped{autograd_function.__name__}"
485
+ Generated = type(
486
+ name,
487
+ (torch.autograd.Function,),
488
+ {
489
+ "forward": staticmethod(forward),
490
+ "backward": staticmethod(backward),
491
+ "jvp": staticmethod(jvp),
492
+ "setup_context": staticmethod(setup_context),
493
+ "generate_vmap_rule": True,
494
+ },
495
+ )
496
+
497
+ def get_out_dims():
498
+ assert out_dims != init_val
499
+ return out_dims
500
+
501
+ return Generated, get_out_dims
502
+
503
+
504
+ # tangents might be None, so we need to replace
505
+ # the corresponding in_dims with None.
506
+ def get_tangents_in_dims(input_dims, tangents):
507
+ flat_in_dims, spec = pytree.tree_flatten(input_dims)
508
+ flat_tangents = pytree.arg_tree_leaves(*tangents)
509
+ result = [
510
+ None if tangent is None else in_dim
511
+ for in_dim, tangent in zip(flat_in_dims, flat_tangents)
512
+ ]
513
+ return pytree.tree_unflatten(result, spec)
514
+
515
+
516
+ # NOTE: [Why do we need to run setup_context under a vmap?]
517
+ # Consider the following autograd.Function
518
+ #
519
+ # class Sum(torch.autograd.Function):
520
+ # @staticmethod
521
+ # def forward(x):
522
+ # return x.sum()
523
+ # @staticmethod
524
+ # def setup_context(ctx, inputs, outputs):
525
+ # ctx.x_shape = inputs[0]
526
+ # @staticmethod
527
+ # def backward(ctx, gy):
528
+ # return gy.expand(ctx.x_shape)
529
+ #
530
+ # x = torch.randn(B, 4)
531
+ # in_dims = 0
532
+ # vmap(Sum.apply, in_dims)(x)
533
+ #
534
+ # Let's assume for a moment that we didn't vmap setup_context in VmappedSum:
535
+ #
536
+ # class VmappedSum(torch.autograd.Function):
537
+ # @staticmethod
538
+ # def forward(x):
539
+ # return vmap(Sum.forward, in_dims)(x)
540
+ #
541
+ # @staticmethod
542
+ # def setup_context(ctx, inputs, outputs):
543
+ # Sum.setup_context(ctx, inputs, outputs)
544
+ #
545
+ # @staticmethod
546
+ # def backward(ctx, gy):
547
+ # def backward_no_context(gy):
548
+ # return gy.expand(ctx.x_shape)
549
+ #
550
+ # dims = (0,)
551
+ # gx = vmap(backward_no_context, dims)(gy)
552
+ # return gx
553
+ #
554
+ # We end up saving [B, 4] as x_shape. In the backward, gy has shape [B],
555
+ # and we're doing:
556
+ #
557
+ # def backward_no_context(gy):
558
+ # return gy.expand([B, 4])
559
+ #
560
+ # gx = vmap(backward_no_context, dims)(gy: "Tensor[B]")
561
+ #
562
+ # This gives us the wrong result (gx has shape [B, B, 4], but it should
563
+ # have shape [4]). Performing vmap over setup_context means the shape
564
+ # saved has shape [4] and leads to a correct result shape for gx.
565
+
566
+
567
+ # Wraps a ctx object. Forwards all attr accesses to the underlying object
568
+ # except for the attrs in _pt_attrs
569
+ class WrappedCtx:
570
+ _pt_reserved_attrs: Tuple[str, ...] = ("_pt_reserved_attrs", "_pt_inner_ctx")
571
+
572
+ def __init__(self, ctx):
573
+ if not isinstance(ctx, WrappedCtx):
574
+ reserved_attrs = type(self)._pt_reserved_attrs
575
+ for name in reserved_attrs:
576
+ if not hasattr(ctx, name):
577
+ continue
578
+ raise RuntimeError(
579
+ f"PyTorch reserves the {reserved_attrs} field on ctx. "
580
+ "Please name your fields on ctx something else to avoid name "
581
+ "collision."
582
+ )
583
+ self._pt_inner_ctx = ctx
584
+
585
+ def __getattr__(self, name):
586
+ return getattr(self._pt_inner_ctx, name)
587
+
588
+ def __setattr__(self, name, value):
589
+ if name in type(self)._pt_reserved_attrs:
590
+ self.__dict__[name] = value
591
+ return
592
+ return setattr(self._pt_inner_ctx, name, value)
593
+
594
+
595
+ # Wraps ctx to create a new ctx object that overrides saved_tensors.
596
+ class CtxWithSavedTensors(WrappedCtx):
597
+ _pt_reserved_attrs = ("_pt_new_saved_tensors", *WrappedCtx._pt_reserved_attrs)
598
+
599
+ def __init__(self, ctx, new_saved_tensors):
600
+ super().__init__(ctx)
601
+ self._pt_new_saved_tensors = new_saved_tensors
602
+
603
+ @property
604
+ def saved_tensors(self):
605
+ return self._pt_new_saved_tensors
606
+
607
+
608
+ class CtxCustomSave(WrappedCtx):
609
+ _pt_reserved_attrs = (
610
+ "_pt_saved_tensors_bdims",
611
+ "_pt_current_level",
612
+ *WrappedCtx._pt_reserved_attrs,
613
+ )
614
+
615
+ def __init__(self, ctx, current_level):
616
+ super().__init__(ctx)
617
+ self._pt_saved_tensors_bdims = ()
618
+ self._pt_current_level = current_level
619
+
620
+ def save_for_backward(self, *tensors):
621
+ unwrapped_tensors, bdims = unwrap_batched(tensors, self._pt_current_level)
622
+ self._pt_inner_ctx.save_for_backward(*unwrapped_tensors)
623
+ self._pt_saved_tensors_bdims = bdims
624
+
625
+ def save_for_forward(self, *tensors):
626
+ unwrapped_tensors, bdims = unwrap_batched(tensors, self._pt_current_level)
627
+ self._pt_inner_ctx.save_for_forward(*unwrapped_tensors)
628
+ self._pt_saved_tensors_bdims = bdims
629
+
630
+
631
+ def reductify(
632
+ grad_input,
633
+ grad_input_bdim,
634
+ input_bdim,
635
+ batch_size,
636
+ target_shape_without_bdim_to_reduce_to=None,
637
+ ):
638
+ if not isinstance(grad_input, tuple):
639
+ grad_input = (grad_input,)
640
+ if not isinstance(grad_input_bdim, tuple):
641
+ grad_input_bdim = (grad_input_bdim,)
642
+ if not isinstance(input_bdim, tuple):
643
+ input_bdim = (input_bdim,)
644
+
645
+ if target_shape_without_bdim_to_reduce_to is None:
646
+ target_shape_without_bdim_to_reduce_to = len(grad_input) * (None,)
647
+ result = tuple(
648
+ reductify_leaf(gi, gi_bdim, i_bdim, batch_size, maybe_ishape)
649
+ for gi, gi_bdim, i_bdim, maybe_ishape in zip(
650
+ grad_input,
651
+ grad_input_bdim,
652
+ input_bdim,
653
+ target_shape_without_bdim_to_reduce_to,
654
+ )
655
+ )
656
+ return result
657
+
658
+
659
+ def reductify_leaf(
660
+ grad_input,
661
+ grad_input_bdim,
662
+ input_bdim,
663
+ batch_size,
664
+ target_shape_without_bdim_to_reduce_to=None,
665
+ ):
666
+ if grad_input is None:
667
+ return None
668
+
669
+ if grad_input_bdim is None and input_bdim is None:
670
+ return grad_input
671
+
672
+ if grad_input_bdim is not None and input_bdim is None:
673
+ return grad_input.sum(grad_input_bdim)
674
+
675
+ # NOTE: [Why can't we rely on autograd to reduce expanded gradients?]
676
+ # For reverse-mode AD,
677
+ # given a grad_input and input, it is valid for the user to return a
678
+ # grad_input that has a broadcasted shape when compared to the input.
679
+ # In this situation, autograd automatically reduces the grad_input to
680
+ # the shape of the input.
681
+ #
682
+ # However, when input_bdim is not None, we have problems.
683
+ #
684
+ # [example 1]
685
+ # grad_input: Tensor[3, 4], input: Tensor[B, 4]
686
+ # We can expand grad_input to Tensor[B, 3, 4], but that isn't broadcastable
687
+ # from [B, 4].
688
+ #
689
+ # [example 2]
690
+ # grad_input: Tensor[3, B, 4], input: Tensor[B, 4]
691
+ # We can swizzle grad_input to Tensor[B, 3, 4], but that isn't broadcastable
692
+ # from [B, 4].
693
+ #
694
+ # This means that we need to also reduce the grad_input to the shape of the
695
+ # input. This behavior is controlled by the `target_shape_without_bdim_to_reduce_to` flag;
696
+ # if not-None then we do the reducing manually, otherwise, we do not do a reduction.
697
+ assert input_bdim is not None
698
+
699
+ if grad_input_bdim is None:
700
+ grad_input = grad_input.unsqueeze(input_bdim)
701
+ new_shape = list(grad_input.shape)
702
+ new_shape[input_bdim] = batch_size
703
+ grad_input = grad_input.expand(new_shape)
704
+ grad_input_bdim = input_bdim
705
+
706
+ if target_shape_without_bdim_to_reduce_to is not None:
707
+ return vmap(
708
+ torch.Tensor.sum_to_size,
709
+ in_dims=(grad_input_bdim, None),
710
+ out_dims=input_bdim,
711
+ )(grad_input, target_shape_without_bdim_to_reduce_to)
712
+
713
+ if input_bdim != grad_input_bdim:
714
+ grad_input = grad_input.movedim(grad_input_bdim, input_bdim)
715
+ return grad_input
716
+
717
+
718
+ def autograd_function_forward_rewritten(original_forward, original_setup_context):
719
+ def new_forward(ctx, *args, **kwargs):
720
+ output = original_forward(*args, **kwargs)
721
+ original_setup_context(ctx, args, output)
722
+ return output
723
+
724
+ return new_forward
725
+
726
+
727
+ class AutogradFunctionApply(HigherOrderOperator):
728
+ def __init__(self) -> None:
729
+ super().__init__("autograd_function_apply")
730
+
731
+ def __call__(self, fwd, bwd, *fwd_args, **fwd_kwargs):
732
+ saved_values = None
733
+ args_tensor_mask = fwd_kwargs["args_tensor_mask"]
734
+ non_differentiable_idx = fwd_kwargs["non_differentiable_idx"]
735
+ length_of_tensor_args = sum(args_tensor_mask)
736
+ # Filter out the original tensor args from fwd_args,
737
+ # lifted freevars should not be args of ApplyTemplate.apply
738
+ # since we don't need to calculate the gradients of them.
739
+ new_fwd_args = fwd_args[:length_of_tensor_args]
740
+
741
+ class ApplyTemplate(torch.autograd.Function):
742
+ @staticmethod
743
+ def forward(ctx, *args):
744
+ nonlocal saved_values
745
+ output, saved_values = fwd(None, *fwd_args)
746
+
747
+ # If users call ctx.mark_non_differentiable() in the original fwd function.
748
+ if len(non_differentiable_idx) > 0:
749
+ non_differentiable_output = []
750
+ for i, x in enumerate(output):
751
+ if i in non_differentiable_idx:
752
+ non_differentiable_output.append(x)
753
+ ctx.mark_non_differentiable(*non_differentiable_output)
754
+
755
+ return output
756
+
757
+ @staticmethod
758
+ def backward(ctx, *grad):
759
+ return bwd(None, *grad, *saved_values)
760
+
761
+ return ApplyTemplate.apply(*new_fwd_args)
762
+
763
+
764
+ autograd_function_apply = AutogradFunctionApply()
lib/python3.12/site-packages/torch/_functorch/batch_norm_replacement.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-decorators
2
+ # mypy: allow-untyped-defs
3
+ import torch.nn as nn
4
+ from torch._functorch.utils import exposed_in
5
+
6
+
7
+ def batch_norm_without_running_stats(module: nn.Module):
8
+ if (
9
+ isinstance(module, nn.modules.batchnorm._BatchNorm)
10
+ and module.track_running_stats
11
+ ):
12
+ module.running_mean = None
13
+ module.running_var = None
14
+ module.num_batches_tracked = None
15
+ module.track_running_stats = False
16
+
17
+
18
+ @exposed_in("torch.func")
19
+ def replace_all_batch_norm_modules_(root: nn.Module) -> nn.Module:
20
+ """
21
+ In place updates :attr:`root` by setting the ``running_mean`` and ``running_var`` to be None and
22
+ setting track_running_stats to be False for any nn.BatchNorm module in :attr:`root`
23
+ """
24
+ # base case
25
+ batch_norm_without_running_stats(root)
26
+
27
+ for obj in root.modules():
28
+ batch_norm_without_running_stats(obj)
29
+ return root
lib/python3.12/site-packages/torch/_functorch/benchmark_utils.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: ignore-errors
2
+
3
+ import contextlib
4
+ import json
5
+ import operator
6
+ import os
7
+ import time
8
+
9
+ import torch
10
+ from torch.profiler import profile, ProfilerActivity
11
+
12
+
13
+ def synchronize():
14
+ pass
15
+
16
+
17
+ def dump_chrome_trace(
18
+ f,
19
+ input,
20
+ trace_filename,
21
+ optimize_ctx,
22
+ activities,
23
+ num_runs=1,
24
+ devices=None,
25
+ kwargs_for_f=None,
26
+ kwargs_for_profiler=None,
27
+ ):
28
+ """
29
+ Output the chrome trace of running f(input, **kwargs_for_f) with [optimize_ctx]
30
+ [num_runs] times to [trace_filename].
31
+
32
+ [activities] are the activities that the profiler will record, e.g. ProfilerActivity.CUDA.
33
+ Return total runtime without the profiler
34
+
35
+ Outputs to trace_filename
36
+ """
37
+
38
+ if devices is None:
39
+ devices = ["cuda"]
40
+
41
+ global synchronize
42
+ if devices != ["cpu"] and torch.cuda.is_available():
43
+ synchronize = torch.cuda.synchronize
44
+
45
+ if kwargs_for_f is None:
46
+ kwargs_for_f = {}
47
+ if kwargs_for_profiler is None:
48
+ kwargs_for_profiler = {}
49
+
50
+ with optimize_ctx:
51
+ torch.manual_seed(1337)
52
+ for _ in range(5): # warmup runs
53
+ f(input, **kwargs_for_f)
54
+ synchronize()
55
+ torch.manual_seed(1337)
56
+ t0 = time.perf_counter()
57
+ for _ in range(num_runs):
58
+ f(input, **kwargs_for_f)
59
+ synchronize()
60
+ t1 = time.perf_counter()
61
+ timing = t1 - t0
62
+
63
+ with profile(activities=activities, **kwargs_for_profiler) as prof:
64
+ with optimize_ctx:
65
+ synchronize()
66
+ torch.manual_seed(1337)
67
+ for _ in range(num_runs):
68
+ f(input, **kwargs_for_f)
69
+ synchronize()
70
+ prof.export_chrome_trace(trace_filename)
71
+
72
+ return timing
73
+
74
+
75
+ def get_chrome_trace_events(filename):
76
+ f = open(filename)
77
+ data = json.load(f)
78
+ events = data["traceEvents"]
79
+ return events
80
+
81
+
82
+ def is_gpu_compute_event(event):
83
+ global gpu_pids
84
+ return (
85
+ "pid" in event
86
+ and event["pid"] in gpu_pids
87
+ and "ph" in event
88
+ and event["ph"] == "X"
89
+ )
90
+
91
+
92
+ def get_sorted_gpu_events(events):
93
+ sorted_gpu_events = []
94
+ for event in events:
95
+ if not is_gpu_compute_event(event):
96
+ continue
97
+ sorted_gpu_events.append(event)
98
+ return sorted(sorted_gpu_events, key=operator.itemgetter("ts"))
99
+
100
+
101
+ def get_duration(sorted_gpu_events):
102
+ if len(sorted_gpu_events) == 0:
103
+ return 0
104
+ event = sorted_gpu_events[0]
105
+ current_end_time = event["ts"] + event["dur"]
106
+ total_duration = event["dur"]
107
+ for event in sorted_gpu_events[1:]:
108
+ start_time = max(event["ts"], current_end_time)
109
+ end_time = event["ts"] + event["dur"]
110
+ total_duration = total_duration + max(end_time - start_time, 0)
111
+ current_end_time = max(current_end_time, end_time)
112
+ return total_duration
113
+
114
+
115
+ def get_sorted_gpu_mm_conv_events(events):
116
+ def is_mm_conv_event(event):
117
+ return "name" in event and (
118
+ "gemm" in event["name"]
119
+ or "conv" in event["name"]
120
+ or "cutlass" in event["name"]
121
+ or "wgrad" in event["name"]
122
+ )
123
+
124
+ gpu_events = get_sorted_gpu_events(events)
125
+ sorted_events = []
126
+ for event in gpu_events:
127
+ if not is_mm_conv_event(event):
128
+ continue
129
+ sorted_events.append(event)
130
+ return sorted_events
131
+
132
+
133
+ gpu_pids = []
134
+
135
+
136
+ def compute_utilization(filename: str, total_length: float):
137
+ """
138
+ Process the chrome traces outputs by the pytorch profiler to compute GPU Utilization
139
+ and percent of times spent on matmul and convolution
140
+
141
+ Args:
142
+ filename(str): Name of chrome traces file produced by pytorch profiler
143
+
144
+ total_length(float): total length of the process without profiler in second
145
+
146
+ Return:
147
+ tuple: (GPU Utilization, percent of time spent on matmul and convolution)
148
+ """
149
+ events = get_chrome_trace_events(filename)
150
+
151
+ # get pids of GPU events
152
+ global gpu_pids
153
+ gpu_pids = []
154
+ for event in events:
155
+ if "name" not in event:
156
+ continue
157
+ if event["name"] == "process_labels" and "GPU" in event["args"]["labels"]:
158
+ gpu_pids.append(event["pid"])
159
+
160
+ total_length = total_length * 1e6
161
+ sorted_gpu_events = get_sorted_gpu_events(events)
162
+ utilization = get_duration(sorted_gpu_events) / total_length
163
+
164
+ sorted_gpu_mm_conv_events = get_sorted_gpu_mm_conv_events(events)
165
+ mm_conv_utilization = get_duration(sorted_gpu_mm_conv_events) / total_length
166
+
167
+ return utilization, mm_conv_utilization
168
+
169
+
170
+ def benchmark_utilization(
171
+ f,
172
+ input,
173
+ trace_folder,
174
+ optimize_ctx=None,
175
+ trace_file_name="tmp_chrome_trace",
176
+ num_runs=1,
177
+ ):
178
+ """
179
+ Benchmark the GPU Utilization and percent of time spent on matmul and convolution operations of
180
+ running f(input, **kwargs_for_f) with [optimize_ctx] [num_runs] times.
181
+ It will produce a chrome trace file in trace_folder/trace_file_name.json
182
+
183
+ Example:
184
+
185
+ ```
186
+ def f(a):
187
+ return a.sum()
188
+ a = torch.rand(2**20, device="cuda")
189
+ utilization, mm_conv_utilization = benchmark_utilization(f, a, "tmp", trace_file_name = "tmp_chrome_trace")
190
+ ```
191
+
192
+ Args:
193
+ f: function to benchmark
194
+
195
+ input: input to :attr:`f`
196
+
197
+ trace_folder: name of the folder to store the chrome trace
198
+
199
+ optimize_ctx: the context in which f will run
200
+
201
+ trace_file_name: name of the dumped chrome trace file, default to "tmp_chrome_trace"
202
+
203
+ num_runs: number of times to run f, excluding the warm-up runs, default to 1.
204
+
205
+ Return:
206
+ tuple: (GPU Utilization, percent of time spent on matmul and convolution)
207
+
208
+ """
209
+ isExist = os.path.exists(trace_folder)
210
+ if not isExist:
211
+ os.makedirs(trace_folder)
212
+ print("create folder " + trace_folder)
213
+
214
+ if optimize_ctx is None:
215
+ optimize_ctx = contextlib.nullcontext()
216
+
217
+ chrome_trace_file_name = os.path.join(trace_folder, trace_file_name + ".json")
218
+ total_length = dump_chrome_trace(
219
+ f,
220
+ input,
221
+ chrome_trace_file_name,
222
+ optimize_ctx,
223
+ [ProfilerActivity.CUDA],
224
+ num_runs=num_runs,
225
+ devices=["cuda"],
226
+ )
227
+ utilization, mm_conv_utilization = compute_utilization(
228
+ chrome_trace_file_name, total_length
229
+ )
230
+
231
+ return utilization, mm_conv_utilization
lib/python3.12/site-packages/torch/_functorch/compile_utils.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: ignore-errors
2
+
3
+
4
+ from typing import Callable
5
+
6
+ import torch
7
+ import torch.fx as fx
8
+ from torch.multiprocessing.reductions import StorageWeakRef
9
+ from torch.utils import _pytree as pytree
10
+ from torch.utils._pytree import tree_flatten
11
+
12
+
13
+ aten = torch.ops.aten
14
+
15
+
16
+ def get_aten_target(node: fx.Node) -> Callable:
17
+ if hasattr(node.target, "overloadpacket"):
18
+ return node.target.overloadpacket
19
+ return node.target
20
+
21
+
22
+ rand_ops = [
23
+ aten.dropout,
24
+ aten._fused_dropout,
25
+ aten._standard_gamma,
26
+ aten.bernoulli,
27
+ aten.multinomial,
28
+ aten.native_dropout,
29
+ aten.normal,
30
+ aten.poisson,
31
+ aten.binomial,
32
+ aten.rrelu,
33
+ aten.rand_like,
34
+ aten.rand,
35
+ aten.randint,
36
+ aten.randn,
37
+ aten.randperm,
38
+ ]
39
+
40
+
41
+ # return a new copy of torch.fx.graph.Graph with CSE applied to the input graph
42
+ def fx_graph_cse(fx_g: torch.fx.graph.Graph):
43
+ new_graph = fx.Graph()
44
+ env = {} # map from node in the old graph to node in the new graph
45
+ hash_env = {} # map from hash to a node in the new graph
46
+ token_map = {} # map from hash to token
47
+
48
+ from torch._inductor.pattern_matcher import (
49
+ compute_mutation_region_ids,
50
+ same_mutation_regions,
51
+ )
52
+
53
+ compute_mutation_region_ids(fx_g) # type: ignore[arg-type]
54
+
55
+ # Make a set of separate storages returned from the output, which will be preserved
56
+ # when pruning. This prevents us from deduplicating returned tensors which have
57
+ # experienced identical operations, but are separate data structures in eager mode.
58
+ output_node: fx.Node = list(fx_g.nodes)[-1]
59
+ assert output_node.op == "output"
60
+
61
+ def checkable_node(node: fx.Node) -> bool:
62
+ """We can evaluate only nodes that represent tensors with defined storage."""
63
+ if "val" not in node.meta or not isinstance(node.meta["val"], torch.Tensor):
64
+ return False
65
+
66
+ try:
67
+ node.meta["val"].untyped_storage()
68
+ except NotImplementedError:
69
+ return False
70
+
71
+ return True
72
+
73
+ output_storages = {
74
+ StorageWeakRef(n.meta["val"].untyped_storage())
75
+ for n in output_node.all_input_nodes
76
+ if checkable_node(n)
77
+ }
78
+ nodes_that_alias_outputs = {
79
+ n
80
+ for n in fx_g.nodes
81
+ if checkable_node(n)
82
+ and StorageWeakRef(n.meta["val"].untyped_storage()) in output_storages
83
+ }
84
+
85
+ for n in fx_g.nodes:
86
+ # The placeholder, output, and get_attr nodes are copied to the new graph without change
87
+ # do not CSE away random operations
88
+ if (
89
+ n.op == "placeholder"
90
+ or n.op == "output"
91
+ or n.op == "get_attr"
92
+ or get_aten_target(n) in rand_ops
93
+ # aten.empty is non-deterministic, so don't CSE it.
94
+ # Also, aten.empty is almost always fusible into its consumer,
95
+ # so it's not worth CSEing.
96
+ or get_aten_target(n) is aten.empty
97
+ or n in nodes_that_alias_outputs
98
+ ):
99
+ new_node = new_graph.node_copy(n, lambda x: env[x])
100
+ env[n] = new_node
101
+ else: # n.op == 'call_function', should never see n.op == 'call_module' or 'call_method'
102
+ # substitute args and kwargs members to their mapping in env if exists
103
+ # specs can be used to reconstruct nested list/dictionaries
104
+ def substitute(arg_list):
105
+ arg_list, spec = tree_flatten(arg_list)
106
+ for i in range(len(arg_list)):
107
+ v = arg_list[i]
108
+ if isinstance(v, torch.fx.node.Node) and v in env:
109
+ arg_list[i] = env[v]
110
+ if isinstance(v, (torch.SymBool, torch.SymInt, torch.SymFloat)):
111
+ arg_list[i] = v.node
112
+ return tuple(arg_list), spec
113
+
114
+ args, args_spec = substitute(n.args)
115
+ kwargs, kwargs_spec = substitute(n.kwargs)
116
+
117
+ # each token corresponds to a unique node
118
+ # nodes with the same token can be substituted
119
+ token = {
120
+ "target": n.target,
121
+ "args": args,
122
+ "args_spec": args_spec,
123
+ "kwargs": kwargs,
124
+ "kwargs_spec": kwargs_spec,
125
+ }
126
+
127
+ # hash substituted args to a number, do not hash specs because specs are not hashable
128
+ # We need to add type into hash to avoid situations like:
129
+ # hash((primals_2, 1.0)) == hash((primals_2, 1))
130
+ hash_arg = hash(
131
+ (tuple((a, type(a)) for a in args), tuple((a, type(a)) for a in kwargs))
132
+ )
133
+ hash_val = (n.target, hash_arg)
134
+
135
+ # check if a node has a substitute and can be eliminated
136
+ hash_val_in_hash_env = hash_val in hash_env
137
+ overwrite_due_to_mutation = False
138
+ if hash_val_in_hash_env and token_map[hash_val] == token:
139
+ duplicate_n_prev = hash_env[hash_val]
140
+ if same_mutation_regions(n, duplicate_n_prev):
141
+ env[n] = duplicate_n_prev
142
+ continue
143
+ else:
144
+ # any futures duplicates should replace with n, not duplicate_n_prev
145
+ overwrite_due_to_mutation = True
146
+
147
+ new_node = new_graph.node_copy(n, lambda x: env[x])
148
+ env[n] = new_node
149
+ if overwrite_due_to_mutation or not hash_val_in_hash_env:
150
+ hash_env[hash_val] = new_node
151
+ token_map[hash_val] = token
152
+
153
+ return new_graph
154
+
155
+
156
+ def strip_overloads(gm):
157
+ """
158
+ Modifies the target of graph nodes in :attr:`gm` to strip overloads.
159
+
160
+ Args:
161
+ gm(fx.GraphModule): The input Fx graph module to be modified
162
+ """
163
+ for node in gm.graph.nodes:
164
+ if isinstance(node.target, torch._ops.OpOverload):
165
+ node.target = node.target.overloadpacket
166
+ gm.recompile()
167
+
168
+
169
+ def get_placeholders(graph):
170
+ return graph.find_nodes(op="placeholder")
171
+
172
+
173
+ def get_outputs(graph):
174
+ for node in graph.find_nodes(op="output"):
175
+ return pytree.tree_leaves(node.args[0])
176
+ raise AssertionError("No output node found")
lib/python3.12/site-packages/torch/_functorch/compilers.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: ignore-errors
2
+
3
+ import copy
4
+ import logging
5
+ import os
6
+ import pickle
7
+ import random
8
+ from contextlib import contextmanager
9
+ from functools import partial
10
+ from typing import Callable, Union
11
+
12
+ import sympy
13
+
14
+ import torch
15
+ import torch.fx as fx
16
+ import torch.nn as nn
17
+ import torch.utils._pytree as pytree
18
+ from torch import SymInt
19
+ from torch._decomp import get_decompositions
20
+ from torch.fx.experimental.symbolic_shapes import bind_symbols
21
+
22
+ from .aot_autograd import aot_function, aot_module, make_boxed_compiler
23
+ from .compile_utils import strip_overloads
24
+ from .partitioners import (
25
+ default_partition,
26
+ draw_graph,
27
+ min_cut_rematerialization_partition,
28
+ )
29
+
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+
34
+ # These canonicalizations are needed here (and not decompositions), as the ops
35
+ # we're trying to canonicalize to CompositeImplicitAutograd.
36
+ def _canonicalize(fx_g):
37
+ for node in fx_g.graph.find_nodes(
38
+ op="call_function", target=torch.ops.aten._to_copy
39
+ ):
40
+ node.target = torch.ops.aten.to
41
+ fx_g.recompile()
42
+ return fx_g
43
+
44
+
45
+ @contextmanager
46
+ def _disable_jit_autocast():
47
+ old_jit_autocast_flag = torch._C._jit_set_autocast_mode(False)
48
+ try:
49
+ yield
50
+ finally:
51
+ torch._C._jit_set_autocast_mode(old_jit_autocast_flag)
52
+
53
+
54
+ @make_boxed_compiler
55
+ def ts_compile(fx_g: fx.GraphModule, inps) -> Callable:
56
+ """
57
+ Compiles the :attr:`fx_g` with Torchscript compiler.
58
+
59
+ .. warning::
60
+ This API is experimental and likely to change.
61
+
62
+ Args:
63
+ fx_g(fx.GraphModule): The input Fx graph module to be compiled.
64
+
65
+ Returns:
66
+ Torch scripted model.
67
+ """
68
+
69
+ with _disable_jit_autocast():
70
+ strip_overloads(fx_g)
71
+
72
+ for node in fx_g.graph.find_nodes(
73
+ op="call_function", target=torch.ops.aten._to_copy
74
+ ):
75
+ if len(node.args) == 1 and len(node.kwargs) == 1 and "dtype" in node.kwargs:
76
+ node.target = torch.ops.aten.to
77
+
78
+ for node in fx_g.graph.nodes:
79
+ new_kwargs = {}
80
+ for k, v in node.kwargs.items():
81
+ if isinstance(v, torch.device):
82
+ v = v.type
83
+ new_kwargs[k] = v
84
+ node.kwargs = new_kwargs
85
+
86
+ fx_g.graph.lint()
87
+
88
+ fx_g.recompile()
89
+
90
+ f = torch.jit.script(fx_g)
91
+
92
+ torch._C._jit_pass_remove_mutation(f.graph)
93
+
94
+ f = torch.jit.freeze(f.eval())
95
+ f = torch.jit.optimize_for_inference(f)
96
+ if not any(isinstance(t, torch._subclasses.FakeTensor) for t in inps):
97
+ f(*inps)
98
+ return f
99
+
100
+
101
+ def _draw_graph_compile(fx_g, _, name, clear_meta=True):
102
+ print(fx_g.code)
103
+ draw_graph(fx_g, name, clear_meta=clear_meta)
104
+ return fx_g
105
+
106
+
107
+ def draw_graph_compile(name):
108
+ return make_boxed_compiler(partial(_draw_graph_compile, name=name))
109
+
110
+
111
+ @make_boxed_compiler
112
+ def nop(fx_g: fx.GraphModule, _) -> Callable:
113
+ """
114
+ Returns the :attr:`fx_g` Fx graph module as it is. This is a no-op compiler
115
+ and can be used to check accuracy.
116
+
117
+ .. warning::
118
+ This API is experimental and likely to change.
119
+
120
+ """
121
+ return fx_g
122
+
123
+
124
+ class DebugInterpreter(fx.Interpreter):
125
+ def run(self, *args):
126
+ self.symbol_mapping = bind_symbols(self.module, *args)
127
+ super().run(*args)
128
+
129
+ def run_node(self, n):
130
+ def subst_symint(ni):
131
+ if not isinstance(ni, SymInt):
132
+ return ni
133
+ r = sympy.expand(ni.node.expr.xreplace(self.symbol_mapping))
134
+ assert r.is_number, r
135
+ return int(r)
136
+
137
+ def subst_symint_tuple(nis):
138
+ return tuple(subst_symint(ni) for ni in nis)
139
+
140
+ def check_significant_strides(a, b):
141
+ if subst_symint(a.numel()) > 0:
142
+ for idx in range(a.ndim):
143
+ if (
144
+ subst_symint(a.stride(idx)) != b.stride(idx)
145
+ and subst_symint(a.size(idx)) > 1
146
+ ):
147
+ return False
148
+ return True
149
+
150
+ def check(nv, rv, desc):
151
+ assert callable(desc)
152
+ assert nv.dtype == rv.dtype, f"{desc()}: {nv.dtype} != {rv.dtype}"
153
+ assert (
154
+ subst_symint_tuple(nv.size()) == rv.size()
155
+ ), f"{desc()}: {nv.size()} aka {subst_symint_tuple(nv.size())} != {rv.size()}"
156
+ same_strides = check_significant_strides(nv, rv)
157
+ assert (
158
+ same_strides
159
+ ), f"{desc()}: {nv.stride()} aka {subst_symint_tuple(nv.stride())} != {rv.stride()}"
160
+
161
+ r = super().run_node(n)
162
+ if "val" in n.meta:
163
+ n_vals, n_spec = pytree.tree_flatten(n.meta["val"])
164
+ r_vals, r_spec = pytree.tree_flatten(r)
165
+ # TODO: There is some sort of problem where we record that an
166
+ # operator returned a tuple/list, and then later it turns out the
167
+ # real version of the operator returned a list/tuple. Need to
168
+ # figure out what's actually going on here, the error itself is
169
+ # harmless enough as we only getitem out the outputs.
170
+ # assert n_spec == r_spec, f"{n_spec} != {r_spec}"
171
+ assert len(n_vals) == len(r_vals), f"{len(n_vals)} != {len(r_vals)}"
172
+ for i, nv, rv in zip(range(len(n_vals)), n_vals, r_vals):
173
+ if not isinstance(rv, torch.Tensor):
174
+ continue
175
+ check(nv, rv, lambda: f"output {i} where {self.symbol_mapping}")
176
+ return r
177
+
178
+
179
+ @make_boxed_compiler
180
+ def debug_nop(fx_g: fx.GraphModule, _) -> Callable:
181
+ """
182
+ Returns a (slow) interpreter over the FX graph module that also checks
183
+ various debugging properties (e.g., that tracing strides matched real
184
+ strides.)
185
+ """
186
+ return DebugInterpreter(fx_g).run
187
+
188
+
189
+ @make_boxed_compiler
190
+ def simple_ts_compile(fx_g, _):
191
+ strip_overloads(fx_g)
192
+ f = torch.jit.script(fx_g)
193
+ f = torch.jit.freeze(f.eval())
194
+ return f
195
+
196
+
197
+ def nnc_jit(f):
198
+ return aot_function(f, simple_ts_compile)
199
+
200
+
201
+ aten = torch.ops.aten
202
+ default_decompositions = {
203
+ aten.detach,
204
+ aten.gelu_backward,
205
+ aten.leaky_relu_backward,
206
+ aten.sigmoid_backward,
207
+ aten.threshold_backward,
208
+ aten.hardtanh_backward,
209
+ aten.hardsigmoid_backward,
210
+ aten.hardswish_backward,
211
+ aten.tanh_backward,
212
+ aten.silu_backward,
213
+ aten.elu_backward,
214
+ aten.cudnn_batch_norm,
215
+ aten.cudnn_batch_norm_backward,
216
+ aten.masked_fill.Scalar,
217
+ aten.masked_fill.Tensor,
218
+ aten.elu,
219
+ aten.leaky_relu,
220
+ aten.hardtanh,
221
+ aten.hardswish,
222
+ aten.hardsigmoid,
223
+ aten.conj_physical,
224
+ aten.is_same_size,
225
+ }
226
+
227
+ default_decompositions = get_decompositions(default_decompositions)
228
+
229
+
230
+ @make_boxed_compiler
231
+ def print_compile(fx_g, _):
232
+ print(fx_g.code)
233
+ return fx_g
234
+
235
+
236
+ def memory_efficient_fusion(
237
+ fn: Union[Callable, nn.Module],
238
+ **kwargs,
239
+ ):
240
+ """
241
+ Wrapper function over :func:`aot_function` and :func:`aot_module` to perform
242
+ memory efficient fusion. It uses the
243
+ :func:`min_cut_rematerialization_partition` partitioner to perform efficient
244
+ recomputation. It uses NVFuser to compile the generated forward and backward
245
+ graphs.
246
+
247
+ .. warning::
248
+ This API is experimental and likely to change.
249
+
250
+ Args:
251
+ fn (Union[Callable, nn.Module]): A Python function or a ``nn.Module``
252
+ that takes one ore more arguments. Must return one or more Tensors.
253
+ **kwargs: Any other overrides you want to make to the settings
254
+
255
+ Returns:
256
+ Returns a ``Callable`` or ``nn.Module`` that retains the eager behavior
257
+ of the original :attr:`fn`, but whose forward and backward graphs have
258
+ gone through recomputation optimizations, and the graphs have been
259
+ compiled with nvfuser.
260
+
261
+ """
262
+ config = {
263
+ "fw_compiler": ts_compile,
264
+ "bw_compiler": ts_compile,
265
+ "partition_fn": min_cut_rematerialization_partition,
266
+ "decompositions": default_decompositions,
267
+ }
268
+ config.update(kwargs)
269
+ if isinstance(fn, torch.nn.Module):
270
+ return aot_module(fn, **config)
271
+ else:
272
+ return aot_function(fn, **config)
273
+
274
+
275
+ def debug_compile(fx_g, inps):
276
+ fx_g.to_folder("foo")
277
+ print(
278
+ f"""
279
+ ##############################################################
280
+ # To minimize FX graph, copy and paste the below and run it #
281
+ ##############################################################
282
+
283
+ import torch
284
+ import torch.fx as fx
285
+ from functorch.compile import minifier, check_nvfuser_subprocess, check_nvfuser_correctness_subprocess
286
+
287
+ inps = {[(i.shape, i.dtype) for i in inps]}
288
+ inps = [torch.ones(shape, dtype=dtype, device='cuda') for (shape, dtype) in inps]
289
+ from foo import FxModule
290
+ mod = FxModule().cuda()
291
+
292
+ with torch.jit.fuser("fuser2"):
293
+ # check_nvfuser_subprocess can be replaced with check_nvfuser_correctness_subprocess
294
+ minifier(fx.symbolic_trace(mod), inps, check_nvfuser_subprocess)
295
+ """
296
+ )
297
+ from foo import FxModule
298
+
299
+ FxModule().cuda()(*inps)
300
+
301
+ return ts_compile(fx_g, inps)
302
+
303
+
304
+ graph_index = 0
305
+
306
+
307
+ def get_inputs(input_data_path):
308
+ """
309
+ Return a random input for the given inputs meta generated from _save_fx_default.
310
+ """
311
+ inputs = []
312
+ with open(input_data_path, "rb") as f:
313
+ inputs_meta = pickle.load(f)
314
+ inputs = []
315
+ for meta in inputs_meta:
316
+ if len(meta) == 1:
317
+ type = meta
318
+ input = type(random.rand())
319
+ else:
320
+ type, shape, stride, dtype, device = meta
321
+ if dtype in {
322
+ torch.int,
323
+ torch.int32,
324
+ torch.int64,
325
+ torch.bool,
326
+ torch.int,
327
+ torch.uint8,
328
+ int,
329
+ float,
330
+ }:
331
+ input = torch.randint(0, 1, shape, dtype=dtype, device=device)
332
+ else:
333
+ input = torch.rand(shape, dtype=dtype, device=device)
334
+ inputs.append(input)
335
+ return inputs
336
+
337
+
338
+ def _save_fx_default(current_name, folder_name, dump_example_input, gm, example_inputs):
339
+ """
340
+ The forward, backward, and joint computation graph will be stored in
341
+ {folder_name}/{current_name}/{current_name}_forward_{graph_index},
342
+ {folder_name}/{current_name}/{current_name}_backward_{graph_index}, and
343
+ {folder_name}/{current_name}/{current_name}_joint_{graph_index} respectively.
344
+ The input shape of the graphs will be stored in the .input files.
345
+ These files can be loaded with pickle,
346
+ and is a list of format (type, shape, stride, dtype, device).
347
+ In the case of type = int or float, it is just (type,).
348
+ For joint graph input, it is a nested list [[],[]]
349
+ where the two inner lists have the same format.
350
+ If dump_example_input is True, example_inputs will be stored in .pt file.
351
+ Since each function might produce multiple graphs,
352
+ the graph_index is used to distinguish difference graphs
353
+ """
354
+ from functorch.compile import aot_module_simplified
355
+
356
+ def get_input_meta(args):
357
+ input_meta = []
358
+ if len(args) > 0 and isinstance(args[0], tuple): # joint input
359
+ input_meta += get_input_meta(args[0])
360
+ input_meta += get_input_meta(args[1])
361
+ return input_meta
362
+ for arg in args:
363
+ if type(arg) == int or type(arg) == float:
364
+ input_meta.append((type(arg),))
365
+ else:
366
+ input_meta.append(
367
+ (type(arg), arg.shape, arg.stride(), arg.dtype, arg.device)
368
+ )
369
+ return input_meta
370
+
371
+ def graph_saver_helper(gm_to_save, args, type_name):
372
+ global graph_index
373
+ if len(gm_to_save.graph.nodes) == 0:
374
+ log.log(
375
+ logging.WARNING,
376
+ "No nodes in graph {%s}_{%s}_{%s}.",
377
+ current_name,
378
+ type_name,
379
+ graph_index,
380
+ )
381
+ return
382
+
383
+ gm = copy.deepcopy(gm_to_save)
384
+ gm.graph.set_codegen(torch.fx.graph.CodeGen()) # remove codegen
385
+ gm.recompile()
386
+
387
+ input_meta = get_input_meta(args)
388
+
389
+ os.makedirs(f"{folder_name}/{current_name}", exist_ok=True)
390
+ gm.to_folder(
391
+ f"{folder_name}/{current_name}/{current_name}_{type_name}_{graph_index}"
392
+ )
393
+ pickle.dump(
394
+ input_meta,
395
+ open(
396
+ f"{folder_name}/{current_name}/{current_name}_{type_name}_{graph_index}/{current_name}_{type_name}_{graph_index}.input", # noqa: B950
397
+ "wb",
398
+ ),
399
+ ) # noqa: E501
400
+ if dump_example_input:
401
+ torch.save(
402
+ args,
403
+ f"{folder_name}/{current_name}/{current_name}_{type_name}_{graph_index}/{current_name}_{type_name}_{graph_index}.pt", # noqa: B950
404
+ ) # noqa: E501
405
+
406
+ def graph_saver_forward(gm, fw_args):
407
+ graph_saver_helper(gm, fw_args, "forward")
408
+ return gm
409
+
410
+ def graph_saver_backward(gm, bw_args):
411
+ graph_saver_helper(gm, bw_args, "backward")
412
+ global graph_index
413
+ graph_index += 1
414
+ return gm
415
+
416
+ def graph_saver_joint(gm, joint_args):
417
+ graph_saver_helper(gm, joint_args, "joint")
418
+ return default_partition(gm, joint_args)
419
+
420
+ return aot_module_simplified(
421
+ gm,
422
+ example_inputs,
423
+ fw_compiler=graph_saver_forward,
424
+ bw_compiler=graph_saver_backward,
425
+ partition_fn=graph_saver_joint,
426
+ decompositions=default_decompositions,
427
+ )
428
+
429
+
430
+ # WARNING: This isn't tested anywhere!!
431
+ def graph_dumper_aot(current_name, folder_name, dump_example_input=False):
432
+ """
433
+ Dump the forward, backward, and joint computation graph.
434
+ Example Usage:
435
+ save_fx_func = graph_dumper_aot(current_name, folder_name, dump_example_input = False)
436
+ optimize_ctx = torchdynamo.optimize(
437
+ save_fx_func
438
+ )
439
+ with torch.enable_grad():
440
+ with optimize_ctx:
441
+ result = forward_and_backward_pass(model, example_inputs)
442
+ """
443
+ global graph_index
444
+ graph_index = 0
445
+ return partial(_save_fx_default, current_name, folder_name, dump_example_input)
lib/python3.12/site-packages/torch/_functorch/config.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Global flags for aot autograd
9
+ """
10
+ import os
11
+ import sys
12
+ from typing import Optional, TYPE_CHECKING
13
+
14
+
15
+ # Converts torch rng ops to their functional philox rng equivalents. Note that
16
+ # we functionalize only CUDA rng ops today.
17
+ functionalize_rng_ops = False
18
+
19
+ # can be useful for debugging if we are incorrectly creating meta fake tensors
20
+ fake_tensor_allow_meta = os.environ.get("FAKE_ALLOW_META", "1") != "0"
21
+
22
+ # Enables optional asserts in hotpath code to check for errors. If
23
+ # you are seeing weird accuracy problems, try turning this on.
24
+ # This is currently off by default as it will harm tracing time,
25
+ # but it is on by default for aot_eager.
26
+ debug_assert = False
27
+
28
+ debug_partitioner = os.environ.get("AOT_PARTITIONER_DEBUG", "0") != "0"
29
+
30
+ static_weight_shapes = True
31
+
32
+ # Applies CSE to the graph before partitioning
33
+ cse = True
34
+
35
+
36
+ enable_autograd_cache = os.environ.get("TORCHINDUCTOR_AUTOGRAD_CACHE", "0") == "1"
37
+
38
+
39
+ def remote_autograd_cache_default() -> Optional[bool]:
40
+ if os.environ.get("TORCHINDUCTOR_AUTOGRAD_REMOTE_CACHE") == "1":
41
+ return True
42
+ if os.environ.get("TORCHINDUCTOR_AUTOGRAD_REMOTE_CACHE") == "0":
43
+ return False
44
+ return None
45
+
46
+
47
+ enable_remote_autograd_cache = remote_autograd_cache_default()
48
+
49
+
50
+ # When AOTAutograd regenerates aliased graph outputs,
51
+ # attempt to use functionalization's view-replay logic
52
+ # before falling back to the autograd engine's view replay or as_strided.
53
+ # This can have some perf implications
54
+ # (although for many models this will not matter).
55
+ # (1) If you have many view ops chained together, replaying all of them
56
+ # at runtime can have more overhead compared to a single as_strided call
57
+ # (2) If you are doing training, AsStridedBackward is quite slow,
58
+ # and the individual view op backward formulas will likely be faster.
59
+ # (3) Some backends like XLA do not support as_strided
60
+
61
+ # Temporary hack: disable this flag for internal
62
+ # (needed to fix an internal issue while avoiding bumping XLA pin)
63
+ # eventually: either default this config to false completely
64
+ # once XLA pin update works,
65
+ # or default config to true and fix relevant bugs
66
+ from torch._inductor.config import is_fbcode
67
+
68
+
69
+ # View replay is currently not compatible with AOTAutogradCache, since
70
+ # FunctionalTensors are not serializable. We'll need to make them
71
+ # serializable before enabling warm cache with this config turned on.
72
+ view_replay_for_aliased_outputs = (not is_fbcode()) and (not enable_autograd_cache)
73
+
74
+ # Restricts the amount of computation AOTAutograd can do.
75
+ # NB: We have essentially disabled this heuristic now. However, this is kept
76
+ # here for now in case it's useful. Setting it low can artificially reduce the
77
+ # amount of recomputation AOTAutograd performs, although not in any kind of
78
+ # principled way.
79
+ max_dist_from_bw = 1000
80
+
81
+
82
+ # Bans recomputation of nodes that are reading from nodes that is far before
83
+ # the current node
84
+ ban_recompute_used_far_apart = True
85
+ # Breaks up long chain of fusible ops, as otherwise we can have an arbitrarily
86
+ # long chain of recomputation in the backwards pass.
87
+ ban_recompute_long_fusible_chains = True
88
+ # Bans recomputation of nodes that must be materialized in the backwards pass
89
+ # (used by a non-fusible node)
90
+ ban_recompute_materialized_backward = True
91
+ # Chooses to ban recomputation of nodes based off an allowlist. Setting it to
92
+ # False changes it to use a denylist. Main change is on operators like
93
+ # sort/pool/stuff that isn't cheap enough to be fusible for free but also isn't
94
+ # that expensive
95
+ ban_recompute_not_in_allowlist = True
96
+ # Chooses to ban recomputation of reductions. This is generally a good idea, as
97
+ # the result of reductions is generally very small but recomputing reductions in
98
+ # a fusion can be expensive.
99
+ ban_recompute_reductions = True
100
+ # Prevents the partitioner from ever saving views (i.e. always recompute them).
101
+ # Generally a good idea since views are free to recompute.
102
+ recompute_views = False
103
+
104
+ # By default, the partitioner is purely trying to optimize for runtime (although
105
+ # it should always use less memory than eager)
106
+ # This knob controls the partitioner to make that tradeoff for you, choosing the
107
+ # fastest option that saves less activations than the memory budget.
108
+ # Specifically, 0.0 corresponds to the activation memory from applying
109
+ # activation checkpointing to the full compiled region, and 1.0 corresponds to
110
+ # the activation memory from the default runtime-optimized strategy. So, 0.4
111
+ # would result in a strategy that saves 40% of the activations compared to the
112
+ # default strategy.
113
+ # It solves a 0-1 knapsack to find the minimum recompute necessary to stay below
114
+ # the activation memory budget.
115
+ # NOTE: This *cannot* be treated as
116
+ activation_memory_budget = 1.0
117
+
118
+ # This controls how we estimate the runtime when deciding what the cheapest
119
+ # operators to recompute are. The 3 options are
120
+ # "flops": Bases it off of the flop count provided by torch.utils.flop_counter
121
+ # "profile": Benchmarks each operator to come up with a runtime
122
+ # "testing": Returns 1 for everything
123
+ activation_memory_budget_runtime_estimator = "flops"
124
+
125
+ # This controls the solver used for the 0-1 knapsack. By default we use a
126
+ # quantized DP solution ("dp"). The other approaches are a "greedy" and a "ilp"
127
+ # (which has a scipy dependency).
128
+ activation_memory_budget_solver = "dp"
129
+
130
+ # This dumps out a png visualization of the expected runtime vs. activation
131
+ # memory tradeoffs for all memory budget values from 0 to 1 in increments of
132
+ # 0.5. See an example here:
133
+ # https://github.com/pytorch/pytorch/pull/126320#discussion_r1625104015
134
+ visualize_memory_budget_pareto = (
135
+ os.environ.get("PARTITIONER_MEMORY_BUDGET_PARETO", "0") == "1"
136
+ )
137
+
138
+ # Sets all of the ban_recompute heuristics to False except ban_recompute_reductions
139
+ # Generally, this will probably result in some memory improvement, but at the
140
+ # cost of some performance
141
+ aggressive_recomputation = False
142
+
143
+ # If FakeTensor.data_ptr() should error.
144
+ # This option is independent of AOTAutograd and torch.compile, but our policy
145
+ # is to turn it off during torch.compile.
146
+ fake_tensor_allow_unsafe_data_ptr_access = True
147
+
148
+ # Unlifts effect tokens from the inputs/outputs in the traced graph and instead
149
+ # inserts make_token/sink_token calls in the graph to create tokens and then
150
+ # sink them at the end. Note that this means the graph is no longer functional
151
+ # which may lead to silent errors unless the backend knows how to handle the
152
+ # tokens.
153
+ unlift_effect_tokens = False
154
+
155
+
156
+ # Run aot eager decomp partition with CrossRefFakeMode
157
+ # options = False, "all", "custom_ops"
158
+ fake_tensor_crossref = False
159
+
160
+ # This mode specifies that we should also keep track of the real
161
+ # tensor along with the fake tensor, and do real compute. While
162
+ # seemingly this eliminates the whole point of fake tensors, there are
163
+ # two obvious use cases for it:
164
+ #
165
+ # 1. When users call item()/other data dependent operations,
166
+ # if we propagate_real_tensors we are able to determine what
167
+ # the true value is and keep going.
168
+ #
169
+ # 2. It can be useful for testing, when you want to see if the fake
170
+ # and real tensors agree with each other. (Note that there are
171
+ # currently known inaccuracies in how we clone real tensors, that
172
+ # would have to be tightened up for this to be useful in this
173
+ # case.)
174
+ #
175
+ # Note that fake tensors are typically understood to be cheap to store
176
+ # indefinitely, so we tend to hold on to them longer than we would
177
+ # hold onto the real tensors. So we also support you explicitly
178
+ # deallocating the real tensor associated with a fake tensor, at which
179
+ # point we will stop propagating real tensors.
180
+ #
181
+ # One more thing: when you provide a real tensor to fakeify, we will
182
+ # clone it, so that we can safely perform mutations on it if necessary.
183
+ # This will increase live memory usage. This could potentially be
184
+ # optimized by using COW. We also currently do not faithfully
185
+ # maintain autograd metadata on the real tensor; this is fine because
186
+ # AOTAutograd will only use the fake tensor to determine leafness/etc
187
+ # of tensors in question.
188
+ fake_tensor_propagate_real_tensors = False
189
+
190
+ # This controls whether we collect donated buffer. This flag must be set
191
+ # False if a user wants to retain_graph=True for backward.
192
+ donated_buffer = False if is_fbcode() else True
193
+
194
+ # Controls the default graph output format used by draw_graph
195
+ # Supported formats are defined here https://graphviz.org/docs/outputs/
196
+ torch_compile_graph_format = os.environ.get("TORCH_COMPILE_GRAPH_FORMAT", "svg")
197
+
198
+ # Valid only if fake_tensor_propagate_real_tensors = True; if a fake-real
199
+ # kernel mismatch is detected, bypasses by making a fake kernel from the
200
+ # real tensor outputs.
201
+ generate_fake_kernels_from_real_mismatches = False
202
+
203
+
204
+ # Error on BypassAOTAutogradCache instead of just a warning
205
+ # Used for tests
206
+ strict_autograd_cache = False
207
+
208
+ # See Note [AOTAutograd Tangent Subclassness for mutated inputs]
209
+ # TODO(ivankobzarev): Remove this config, being able to deduce it compile time.
210
+ disable_guess_zero_tangent_for_mutated_input_subclass = False
211
+
212
+ if TYPE_CHECKING:
213
+ from torch.utils._config_typing import * # noqa: F401, F403
214
+
215
+ from torch.utils._config_module import install_config_module
216
+
217
+
218
+ # adds patch, save_config, invalid config checks, etc
219
+ install_config_module(sys.modules[__name__])
lib/python3.12/site-packages/torch/_functorch/deprecated.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ """
3
+ The APIs in this file are exposed as `functorch.*`. They are thin wrappers
4
+ around the torch.func.* APIs that have deprecation warnings -- we're trying
5
+ to move people to the torch.func.* equivalents.
6
+
7
+ NB: We don't use *args, **kwargs in the signatures because that changes the
8
+ documentation.
9
+ """
10
+
11
+ import textwrap
12
+ import warnings
13
+ from typing import Any, Callable, Optional, Tuple, Union
14
+
15
+ import torch._functorch.apis as apis
16
+ import torch._functorch.eager_transforms as _impl
17
+ import torch._functorch.make_functional as _nn_impl
18
+ import torch.nn as nn
19
+ from torch._functorch.eager_transforms import argnums_t
20
+ from torch._functorch.vmap import in_dims_t, out_dims_t
21
+
22
+
23
+ def get_warning(api, new_api=None, replace_newlines=False):
24
+ if new_api is None:
25
+ new_api = f"torch.func.{api}"
26
+ warning = (
27
+ f"We've integrated functorch into PyTorch. As the final step of the \n"
28
+ f"integration, `functorch.{api}` is deprecated as of PyTorch \n"
29
+ f"2.0 and will be deleted in a future version of PyTorch >= 2.3. \n"
30
+ f"Please use `{new_api}` instead; see the PyTorch 2.0 release notes \n"
31
+ f"and/or the `torch.func` migration guide for more details \n"
32
+ f"https://pytorch.org/docs/main/func.migrating.html"
33
+ )
34
+ if replace_newlines:
35
+ warning = warning.replace("\n", "")
36
+ return warning
37
+
38
+
39
+ def warn_deprecated(api, new_api=None):
40
+ warning = get_warning(api, new_api, replace_newlines=True)
41
+ warnings.warn(warning, FutureWarning, stacklevel=3)
42
+
43
+
44
+ def setup_docs(functorch_api, torch_func_api=None, new_api_name=None):
45
+ api_name = functorch_api.__name__
46
+ if torch_func_api is None:
47
+ torch_func_api = getattr(_impl, api_name)
48
+ # See https://docs.python.org/3/using/cmdline.html#cmdoption-OO
49
+ if torch_func_api.__doc__ is None:
50
+ return
51
+
52
+ warning = get_warning(api_name, new_api_name)
53
+ warning_note = "\n.. warning::\n\n" + textwrap.indent(warning, " ")
54
+ warning_note = textwrap.indent(warning_note, " ")
55
+ functorch_api.__doc__ = torch_func_api.__doc__ + warning_note
56
+
57
+
58
+ def vmap(
59
+ func: Callable,
60
+ in_dims: in_dims_t = 0,
61
+ out_dims: out_dims_t = 0,
62
+ randomness: str = "error",
63
+ *,
64
+ chunk_size=None,
65
+ ) -> Callable:
66
+ warn_deprecated("vmap", "torch.vmap")
67
+ return apis.vmap(func, in_dims, out_dims, randomness, chunk_size=chunk_size)
68
+
69
+
70
+ def grad(func: Callable, argnums: argnums_t = 0, has_aux: bool = False) -> Callable:
71
+ warn_deprecated("grad")
72
+ return apis.grad(func, argnums, has_aux)
73
+
74
+
75
+ def grad_and_value(
76
+ func: Callable, argnums: argnums_t = 0, has_aux: bool = False
77
+ ) -> Callable:
78
+ warn_deprecated("grad_and_value")
79
+ return apis.grad_and_value(func, argnums, has_aux)
80
+
81
+
82
+ def vjp(func: Callable, *primals, has_aux: bool = False):
83
+ warn_deprecated("vjp")
84
+ return _impl.vjp(func, *primals, has_aux=has_aux)
85
+
86
+
87
+ def jvp(
88
+ func: Callable,
89
+ primals: Any,
90
+ tangents: Any,
91
+ *,
92
+ strict: bool = False,
93
+ has_aux: bool = False,
94
+ ):
95
+ warn_deprecated("jvp")
96
+ return _impl.jvp(func, primals, tangents, strict=strict, has_aux=has_aux)
97
+
98
+
99
+ def jacrev(
100
+ func: Callable,
101
+ argnums: Union[int, Tuple[int]] = 0,
102
+ *,
103
+ has_aux=False,
104
+ chunk_size: Optional[int] = None,
105
+ _preallocate_and_copy=False,
106
+ ):
107
+ warn_deprecated("jacrev")
108
+ return _impl.jacrev(
109
+ func,
110
+ argnums,
111
+ has_aux=has_aux,
112
+ chunk_size=chunk_size,
113
+ _preallocate_and_copy=_preallocate_and_copy,
114
+ )
115
+
116
+
117
+ def jacfwd(
118
+ func: Callable,
119
+ argnums: argnums_t = 0,
120
+ has_aux: bool = False,
121
+ *,
122
+ randomness: str = "error",
123
+ ):
124
+ warn_deprecated("jacfwd")
125
+ return _impl.jacfwd(func, argnums, has_aux, randomness=randomness)
126
+
127
+
128
+ def hessian(func, argnums=0):
129
+ warn_deprecated("hessian")
130
+ return _impl.hessian(func, argnums=argnums)
131
+
132
+
133
+ def functionalize(func: Callable, *, remove: str = "mutations") -> Callable:
134
+ warn_deprecated("functionalize")
135
+ return _impl.functionalize(func, remove=remove)
136
+
137
+
138
+ def make_functional(model: nn.Module, disable_autograd_tracking: bool = False):
139
+ warn_deprecated("make_functional", "torch.func.functional_call")
140
+ return _nn_impl.make_functional(model, disable_autograd_tracking)
141
+
142
+
143
+ def make_functional_with_buffers(
144
+ model: nn.Module, disable_autograd_tracking: bool = False
145
+ ):
146
+ warn_deprecated("make_functional_with_buffers", "torch.func.functional_call")
147
+ return _nn_impl.make_functional_with_buffers(model, disable_autograd_tracking)
148
+
149
+
150
+ def combine_state_for_ensemble(models):
151
+ warn_deprecated("combine_state_for_ensemble", "torch.func.stack_module_state")
152
+ return _nn_impl.combine_state_for_ensemble(models)
153
+
154
+
155
+ setup_docs(vmap, apis.vmap, "torch.vmap")
156
+ setup_docs(grad, apis.grad)
157
+ setup_docs(grad_and_value, apis.grad_and_value)
158
+ setup_docs(vjp)
159
+ setup_docs(jvp)
160
+ setup_docs(jacrev)
161
+ setup_docs(jacfwd)
162
+ setup_docs(hessian)
163
+ setup_docs(functionalize)
164
+ setup_docs(make_functional, _nn_impl.make_functional, "torch.func.functional_call")
165
+ setup_docs(
166
+ make_functional_with_buffers, _nn_impl.make_functional, "torch.func.functional_call"
167
+ )
168
+ setup_docs(
169
+ combine_state_for_ensemble,
170
+ _nn_impl.combine_state_for_ensemble,
171
+ "torch.func.stack_module_state",
172
+ )
lib/python3.12/site-packages/torch/_functorch/eager_transforms.py ADDED
@@ -0,0 +1,1800 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: ignore-errors
2
+
3
+ # Copyright (c) Facebook, Inc. and its affiliates.
4
+ # All rights reserved.
5
+ #
6
+ # This source code is licensed under the BSD-style license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+
9
+ import contextlib
10
+ from functools import partial, wraps
11
+ from typing import Any, Callable, List, Optional, Tuple, Union
12
+
13
+ import torch
14
+ import torch.autograd.forward_ad as fwAD
15
+ from torch._C._functorch import (
16
+ _assert_wrapped_functional,
17
+ _func_decrement_nesting,
18
+ _func_increment_nesting,
19
+ _grad_decrement_nesting,
20
+ _grad_increment_nesting,
21
+ _jvp_decrement_nesting,
22
+ _jvp_increment_nesting,
23
+ _propagate_functional_input_mutation,
24
+ _unwrap_for_grad,
25
+ _unwrap_functional_tensor,
26
+ _wrap_for_grad,
27
+ _wrap_functional_tensor,
28
+ get_inplace_requires_grad_allowed,
29
+ set_inplace_requires_grad_allowed,
30
+ )
31
+ from torch._functorch.utils import argnums_t, exposed_in
32
+ from torch._subclasses.functional_tensor import FunctionalTensor
33
+ from torch.fx.experimental import const_fold
34
+ from torch.fx.experimental.proxy_tensor import make_fx
35
+ from torch.utils import _pytree as pytree
36
+ from torch.utils._pytree import (
37
+ tree_flatten,
38
+ tree_map,
39
+ tree_map_,
40
+ tree_map_only,
41
+ tree_unflatten,
42
+ treespec_pprint,
43
+ )
44
+
45
+ from .apis import vmap
46
+ from .vmap import doesnt_support_saved_tensors_hooks, get_chunk_sizes
47
+
48
+
49
+ def lazy_dynamo_disallow(func):
50
+ import torch._dynamo
51
+
52
+ return torch._dynamo.disallow_in_graph(func)
53
+
54
+
55
+ @contextlib.contextmanager
56
+ def enable_inplace_requires_grad(enabled):
57
+ prev_state = get_inplace_requires_grad_allowed()
58
+ set_inplace_requires_grad_allowed(enabled)
59
+ try:
60
+ yield
61
+ finally:
62
+ set_inplace_requires_grad_allowed(prev_state)
63
+
64
+
65
+ def _set_tensor_requires_grad(x):
66
+ # avoid graph-break on x.requires_grad_()
67
+ # https://github.com/pytorch/pytorch/pull/110053
68
+ return x.requires_grad_()
69
+
70
+
71
+ def _create_differentiable(inps, level=None):
72
+ def create_differentiable(x):
73
+ if isinstance(x, torch.Tensor):
74
+ with enable_inplace_requires_grad(True):
75
+ return _set_tensor_requires_grad(x)
76
+ raise ValueError(
77
+ f"Thing passed to transform API must be Tensor, " f"got {type(x)}"
78
+ )
79
+
80
+ return tree_map(create_differentiable, inps)
81
+
82
+
83
+ def _undo_create_differentiable(inps, level=None):
84
+ def unwrap_tensors(x):
85
+ if isinstance(x, torch.Tensor):
86
+ return _unwrap_for_grad(x, level)
87
+ # TODO: Remove the following hack for namedtuples
88
+ if isinstance(x, tuple):
89
+ return tree_map(unwrap_tensors, tuple(x))
90
+
91
+ raise RuntimeError(f"Expected tensors, got unsupported type {type(x)}")
92
+
93
+ return tree_map(unwrap_tensors, inps)
94
+
95
+
96
+ def _is_differentiable(maybe_tensor):
97
+ if not isinstance(maybe_tensor, torch.Tensor):
98
+ return False
99
+ return maybe_tensor.requires_grad
100
+
101
+
102
+ def _any_differentiable(tensor_or_tuple_of_tensors):
103
+ flat_args, _ = tree_unflatten(tensor_or_tuple_of_tensors)
104
+ return any(tuple(map(_is_differentiable, flat_args)))
105
+
106
+
107
+ def _wrap_tensor_for_grad(maybe_tensor, level):
108
+ if not isinstance(maybe_tensor, torch.Tensor):
109
+ return maybe_tensor
110
+ return _wrap_for_grad(maybe_tensor, level)
111
+
112
+
113
+ def _wrap_all_tensors(tensor_pytree, level):
114
+ return tree_map(partial(_wrap_tensor_for_grad, level=level), tensor_pytree)
115
+
116
+
117
+ def _as_tuple(val):
118
+ if isinstance(val, tuple):
119
+ return val
120
+ return (val,)
121
+
122
+
123
+ # Version of autograd.grad that handles outputs that don't depend on inputs
124
+
125
+
126
+ def _autograd_grad(
127
+ outputs, inputs, grad_outputs=None, retain_graph=False, create_graph=True
128
+ ):
129
+ if grad_outputs is None:
130
+ diff_outputs = tuple(out for out in outputs if out.requires_grad)
131
+ else:
132
+ result = tuple(
133
+ (out, go) for out, go in zip(outputs, grad_outputs) if out.requires_grad
134
+ )
135
+ if len(result) == 0:
136
+ diff_outputs, grad_outputs = (), ()
137
+ else:
138
+ diff_outputs, grad_outputs = zip(*result)
139
+ if len(diff_outputs) == 0:
140
+ return tuple(torch.zeros_like(inp) for inp in inputs)
141
+ grad_inputs = torch.autograd.grad(
142
+ diff_outputs,
143
+ inputs,
144
+ grad_outputs,
145
+ retain_graph=retain_graph,
146
+ create_graph=create_graph,
147
+ allow_unused=True,
148
+ )
149
+ grad_inputs = tuple(
150
+ torch.zeros_like(inp) if gi is None else gi
151
+ for gi, inp in zip(grad_inputs, inputs)
152
+ )
153
+ return grad_inputs
154
+
155
+
156
+ # NOTE [grad and vjp interaction with no_grad]
157
+ #
158
+ # def f(x):
159
+ # with torch.no_grad():
160
+ # c = x ** 2
161
+ # return x - c
162
+ #
163
+ # The thing to consider is if enable_grad is on/off before grad gets called.
164
+ #
165
+ # Case 1: enable_grad is on.
166
+ # grad(f)(x)
167
+ # In this case, `grad` should respect the inner torch.no_grad.
168
+ #
169
+ # Case 2: enable_grad is off
170
+ # with torch.no_grad():
171
+ # grad(f)(x)
172
+ # In this case, `grad` should respect the inner torch.no_grad, but not the
173
+ # outer one. This is because `grad` is a "function transform": its result
174
+ # should not depend on the result of a context manager outside of `f`.
175
+ #
176
+ # This gives us the following desired behavior:
177
+ # - (nested) grad transforms must obey torch.no_grad inside them
178
+ # - (nested) grad transforms should not obey torch.no_grad outside them
179
+ #
180
+ # To achieve this behavior, upon entering grad/vjp:
181
+ # - we save the current ("previous") is_grad_enabled (*)
182
+ # - we unconditionally enable grad.
183
+ #
184
+ # Inside DynamicLayerBackFallback, when we're temporarily popping `grad` layer
185
+ # off the stack:
186
+ # - if grad_mode is disabled, then we do nothing. (there is a torch.no_grad
187
+ # active, all subsequent grad transforms must obey it).
188
+ # - if grad_mode is enabled, and the previous is_grad_enabled (*) is False,
189
+ # then we temporarily restore the previous `is_grad_enabled`. This is
190
+ # because we're crossing the boundary from a `grad` outside the
191
+ # no_grad to a `grad` inside the no_grad.
192
+ #
193
+ # NB: vjp has some interesting behavior because the vjp's callable can be called
194
+ # under a different grad_mode than the forward computation...
195
+ #
196
+ # NB: forward-mode AD: forward-mode AD doesn't respect torch.no_grad, but
197
+ # it respects c10::AutoFwGradMode. We've implemented the same logic for
198
+ # our jvp transform (it will have special handling if FwGradMode is disabled).
199
+
200
+
201
+ # How do we increment and decrement the nesting? I don't think we can.
202
+ @exposed_in("torch.func")
203
+ def vjp(func: Callable, *primals, has_aux: bool = False):
204
+ """
205
+ Standing for the vector-Jacobian product, returns a tuple containing the
206
+ results of ``func`` applied to ``primals`` and a function that, when
207
+ given ``cotangents``, computes the reverse-mode Jacobian of ``func`` with
208
+ respect to ``primals`` times ``cotangents``.
209
+
210
+ Args:
211
+ func (Callable): A Python function that takes one or more arguments. Must
212
+ return one or more Tensors.
213
+ primals (Tensors): Positional arguments to ``func`` that must all be
214
+ Tensors. The returned function will also be computing the
215
+ derivative with respect to these arguments
216
+ has_aux (bool): Flag indicating that ``func`` returns a
217
+ ``(output, aux)`` tuple where the first element is the output of
218
+ the function to be differentiated and the second element is
219
+ other auxiliary objects that will not be differentiated.
220
+ Default: False.
221
+
222
+ Returns:
223
+ Returns a ``(output, vjp_fn)`` tuple containing the output of ``func``
224
+ applied to ``primals`` and a function that computes the vjp of
225
+ ``func`` with respect to all ``primals`` using the cotangents passed
226
+ to the returned function. If ``has_aux is True``, then instead returns a
227
+ ``(output, vjp_fn, aux)`` tuple.
228
+ The returned ``vjp_fn`` function will return a tuple of each VJP.
229
+
230
+ When used in simple cases, :func:`vjp` behaves the same as :func:`grad`
231
+
232
+ >>> x = torch.randn([5])
233
+ >>> f = lambda x: x.sin().sum()
234
+ >>> (_, vjpfunc) = torch.func.vjp(f, x)
235
+ >>> grad = vjpfunc(torch.tensor(1.))[0]
236
+ >>> assert torch.allclose(grad, torch.func.grad(f)(x))
237
+
238
+ However, :func:`vjp` can support functions with multiple outputs by
239
+ passing in the cotangents for each of the outputs
240
+
241
+ >>> x = torch.randn([5])
242
+ >>> f = lambda x: (x.sin(), x.cos())
243
+ >>> (_, vjpfunc) = torch.func.vjp(f, x)
244
+ >>> vjps = vjpfunc((torch.ones([5]), torch.ones([5])))
245
+ >>> assert torch.allclose(vjps[0], x.cos() + -x.sin())
246
+
247
+ :func:`vjp` can even support outputs being Python structs
248
+
249
+ >>> x = torch.randn([5])
250
+ >>> f = lambda x: {'first': x.sin(), 'second': x.cos()}
251
+ >>> (_, vjpfunc) = torch.func.vjp(f, x)
252
+ >>> cotangents = {'first': torch.ones([5]), 'second': torch.ones([5])}
253
+ >>> vjps = vjpfunc(cotangents)
254
+ >>> assert torch.allclose(vjps[0], x.cos() + -x.sin())
255
+
256
+ The function returned by :func:`vjp` will compute the partials with
257
+ respect to each of the ``primals``
258
+
259
+ >>> x, y = torch.randn([5, 4]), torch.randn([4, 5])
260
+ >>> (_, vjpfunc) = torch.func.vjp(torch.matmul, x, y)
261
+ >>> cotangents = torch.randn([5, 5])
262
+ >>> vjps = vjpfunc(cotangents)
263
+ >>> assert len(vjps) == 2
264
+ >>> assert torch.allclose(vjps[0], torch.matmul(cotangents, y.transpose(0, 1)))
265
+ >>> assert torch.allclose(vjps[1], torch.matmul(x.transpose(0, 1), cotangents))
266
+
267
+ ``primals`` are the positional arguments for ``f``. All kwargs use their
268
+ default value
269
+
270
+ >>> x = torch.randn([5])
271
+ >>> def f(x, scale=4.):
272
+ >>> return x * scale
273
+ >>>
274
+ >>> (_, vjpfunc) = torch.func.vjp(f, x)
275
+ >>> vjps = vjpfunc(torch.ones_like(x))
276
+ >>> assert torch.allclose(vjps[0], torch.full(x.shape, 4.))
277
+
278
+ .. note::
279
+ Using PyTorch ``torch.no_grad`` together with ``vjp``.
280
+ Case 1: Using ``torch.no_grad`` inside a function:
281
+
282
+ >>> def f(x):
283
+ >>> with torch.no_grad():
284
+ >>> c = x ** 2
285
+ >>> return x - c
286
+
287
+ In this case, ``vjp(f)(x)`` will respect the inner ``torch.no_grad``.
288
+
289
+ Case 2: Using ``vjp`` inside ``torch.no_grad`` context manager:
290
+
291
+ >>> # xdoctest: +SKIP(failing)
292
+ >>> with torch.no_grad():
293
+ >>> vjp(f)(x)
294
+
295
+ In this case, ``vjp`` will respect the inner ``torch.no_grad``, but not the
296
+ outer one. This is because ``vjp`` is a "function transform": its result
297
+ should not depend on the result of a context manager outside of ``f``.
298
+ """
299
+ return _vjp_with_argnums(func, *primals, has_aux=has_aux)
300
+
301
+
302
+ @contextlib.contextmanager
303
+ def grad_increment_nesting():
304
+ try:
305
+ grad_level = _grad_increment_nesting()
306
+ yield grad_level
307
+ finally:
308
+ _grad_decrement_nesting()
309
+
310
+
311
+ def enter_jvp_nesting():
312
+ global JVP_NESTING
313
+ jvp_level = _jvp_increment_nesting()
314
+ JVP_NESTING += 1
315
+ return jvp_level
316
+
317
+
318
+ def exit_jvp_nesting():
319
+ global JVP_NESTING
320
+ _jvp_decrement_nesting()
321
+ JVP_NESTING -= 1
322
+
323
+
324
+ @contextlib.contextmanager
325
+ def jvp_increment_nesting():
326
+ try:
327
+ yield enter_jvp_nesting()
328
+ finally:
329
+ exit_jvp_nesting()
330
+
331
+
332
+ @doesnt_support_saved_tensors_hooks
333
+ def _vjp_with_argnums(
334
+ func: Callable, *primals, argnums: Optional[argnums_t] = None, has_aux: bool = False
335
+ ):
336
+ # This is the same function as vjp but also accepts an argnums argument
337
+ # All args are the same as vjp except for the added argument
338
+ # argnums (Optional[int or tuple[int]]): Optional, specifies the argument(s) to compute gradients with respect to.
339
+ # If None, computes the gradients with respect to all inputs (used for vjp). Default: None
340
+ #
341
+ # WARN: Users should NOT call this function directly and should just be calling vjp.
342
+ # It is only separated so that inputs passed to jacrev but not differentiated get the correct wrappers.
343
+ #
344
+ # NOTE: All error messages are produced as if vjp was being called, even if this was called by jacrev
345
+ #
346
+ # Returns the same two elements as :func:`vjp` but the function returned, vjp_fn, returns a tuple of VJPs
347
+ # for only the primal elements given by argnums.
348
+ with grad_increment_nesting() as level:
349
+ # See NOTE [grad and vjp interaction with no_grad]
350
+ with torch.enable_grad():
351
+ primals = _wrap_all_tensors(primals, level)
352
+ if argnums is None:
353
+ diff_primals = _create_differentiable(primals, level)
354
+ else:
355
+ diff_primals = _slice_argnums(primals, argnums, as_tuple=False)
356
+ tree_map_(partial(_create_differentiable, level=level), diff_primals)
357
+ primals_out = func(*primals)
358
+
359
+ if has_aux:
360
+ if not (isinstance(primals_out, tuple) and len(primals_out) == 2):
361
+ raise RuntimeError(
362
+ "vjp(f, *primals): output of function f should be a tuple: (output, aux) "
363
+ "if has_aux is True"
364
+ )
365
+ primals_out, aux = primals_out
366
+ aux = _undo_create_differentiable(aux, level)
367
+
368
+ flat_primals_out, primals_out_spec = tree_flatten(primals_out)
369
+ assert_non_empty_tensor_output(flat_primals_out, "vjp(f, *primals)")
370
+ flat_diff_primals, primals_spec = tree_flatten(diff_primals)
371
+ results = _undo_create_differentiable(primals_out, level)
372
+
373
+ for primal_out in flat_primals_out:
374
+ assert isinstance(primal_out, torch.Tensor)
375
+ if primal_out.is_floating_point() or primal_out.is_complex():
376
+ continue
377
+ raise RuntimeError(
378
+ "vjp(f, ...): All outputs of f must be "
379
+ "floating-point or complex Tensors, got Tensor "
380
+ f"with dtype {primal_out.dtype}"
381
+ )
382
+
383
+ def wrapper(cotangents, retain_graph=True, create_graph=None):
384
+ if create_graph is None:
385
+ create_graph = torch.is_grad_enabled()
386
+ flat_cotangents, cotangents_spec = tree_flatten(cotangents)
387
+ if primals_out_spec != cotangents_spec:
388
+ raise RuntimeError(
389
+ f"Expected pytree structure of cotangents to be the same "
390
+ f"as pytree structure of outputs to the function. "
391
+ f"cotangents: {treespec_pprint(cotangents_spec)}, "
392
+ f"primal output: {treespec_pprint(primals_out_spec)}"
393
+ )
394
+ result = _autograd_grad(
395
+ flat_primals_out,
396
+ flat_diff_primals,
397
+ flat_cotangents,
398
+ retain_graph=retain_graph,
399
+ create_graph=create_graph,
400
+ )
401
+ return tree_unflatten(result, primals_spec)
402
+
403
+ if has_aux:
404
+ return results, wrapper, aux
405
+ else:
406
+ return results, wrapper
407
+
408
+
409
+ def _safe_zero_index(x):
410
+ assert len(x) == 1
411
+ return x[0]
412
+
413
+
414
+ # jacrev and jacfwd don't support complex functions
415
+ # Helper function to throw appropriate error.
416
+ def error_if_complex(func_name, args, is_input):
417
+ flat_args = pytree.tree_leaves(args)
418
+ for idx, arg in enumerate(flat_args):
419
+ if isinstance(arg, torch.Tensor) and arg.dtype.is_complex:
420
+ input_or_output = "inputs" if is_input else "outputs"
421
+ err_msg = (
422
+ f"{func_name}: Expected all {input_or_output} "
423
+ f"to be real but received complex tensor at flattened input idx: {idx}"
424
+ )
425
+ raise RuntimeError(err_msg)
426
+
427
+
428
+ @exposed_in("torch.func")
429
+ def jacrev(
430
+ func: Callable,
431
+ argnums: Union[int, Tuple[int]] = 0,
432
+ *,
433
+ has_aux=False,
434
+ chunk_size: Optional[int] = None,
435
+ _preallocate_and_copy=False,
436
+ ):
437
+ """
438
+ Computes the Jacobian of ``func`` with respect to the arg(s) at index
439
+ ``argnum`` using reverse mode autodiff
440
+
441
+ .. note::
442
+ Using :attr:`chunk_size=1` is equivalent to computing the jacobian
443
+ row-by-row with a for-loop i.e. the constraints of :func:`vmap` are
444
+ not applicable.
445
+
446
+ Args:
447
+ func (function): A Python function that takes one or more arguments,
448
+ one of which must be a Tensor, and returns one or more Tensors
449
+ argnums (int or Tuple[int]): Optional, integer or tuple of integers,
450
+ saying which arguments to get the Jacobian with respect to.
451
+ Default: 0.
452
+ has_aux (bool): Flag indicating that ``func`` returns a
453
+ ``(output, aux)`` tuple where the first element is the output of
454
+ the function to be differentiated and the second element is
455
+ auxiliary objects that will not be differentiated.
456
+ Default: False.
457
+ chunk_size (None or int): If None (default), use the maximum chunk size
458
+ (equivalent to doing a single vmap over vjp to compute the jacobian).
459
+ If 1, then compute the jacobian row-by-row with a for-loop.
460
+ If not None, then compute the jacobian :attr:`chunk_size` rows at a time
461
+ (equivalent to doing multiple vmap over vjp). If you run into memory issues computing
462
+ the jacobian, please try to specify a non-None chunk_size.
463
+
464
+ Returns:
465
+ Returns a function that takes in the same inputs as ``func`` and
466
+ returns the Jacobian of ``func`` with respect to the arg(s) at
467
+ ``argnums``. If ``has_aux is True``, then the returned function
468
+ instead returns a ``(jacobian, aux)`` tuple where ``jacobian``
469
+ is the Jacobian and ``aux`` is auxiliary objects returned by ``func``.
470
+
471
+ A basic usage with a pointwise, unary operation will give a diagonal array
472
+ as the Jacobian
473
+
474
+ >>> from torch.func import jacrev
475
+ >>> x = torch.randn(5)
476
+ >>> jacobian = jacrev(torch.sin)(x)
477
+ >>> expected = torch.diag(torch.cos(x))
478
+ >>> assert torch.allclose(jacobian, expected)
479
+
480
+ If you would like to compute the output of the function as well as the
481
+ jacobian of the function, use the ``has_aux`` flag to return the output
482
+ as an auxiliary object:
483
+
484
+ >>> from torch.func import jacrev
485
+ >>> x = torch.randn(5)
486
+ >>>
487
+ >>> def f(x):
488
+ >>> return x.sin()
489
+ >>>
490
+ >>> def g(x):
491
+ >>> result = f(x)
492
+ >>> return result, result
493
+ >>>
494
+ >>> jacobian_f, f_x = jacrev(g, has_aux=True)(x)
495
+ >>> assert torch.allclose(f_x, f(x))
496
+
497
+ :func:`jacrev` can be composed with vmap to produce batched
498
+ Jacobians:
499
+
500
+ >>> from torch.func import jacrev, vmap
501
+ >>> x = torch.randn(64, 5)
502
+ >>> jacobian = vmap(jacrev(torch.sin))(x)
503
+ >>> assert jacobian.shape == (64, 5, 5)
504
+
505
+ Additionally, :func:`jacrev` can be composed with itself to produce
506
+ Hessians
507
+
508
+ >>> from torch.func import jacrev
509
+ >>> def f(x):
510
+ >>> return x.sin().sum()
511
+ >>>
512
+ >>> x = torch.randn(5)
513
+ >>> hessian = jacrev(jacrev(f))(x)
514
+ >>> assert torch.allclose(hessian, torch.diag(-x.sin()))
515
+
516
+ By default, :func:`jacrev` computes the Jacobian with respect to the first
517
+ input. However, it can compute the Jacboian with respect to a different
518
+ argument by using ``argnums``:
519
+
520
+ >>> from torch.func import jacrev
521
+ >>> def f(x, y):
522
+ >>> return x + y ** 2
523
+ >>>
524
+ >>> x, y = torch.randn(5), torch.randn(5)
525
+ >>> jacobian = jacrev(f, argnums=1)(x, y)
526
+ >>> expected = torch.diag(2 * y)
527
+ >>> assert torch.allclose(jacobian, expected)
528
+
529
+ Additionally, passing a tuple to ``argnums`` will compute the Jacobian
530
+ with respect to multiple arguments
531
+
532
+ >>> from torch.func import jacrev
533
+ >>> def f(x, y):
534
+ >>> return x + y ** 2
535
+ >>>
536
+ >>> x, y = torch.randn(5), torch.randn(5)
537
+ >>> jacobian = jacrev(f, argnums=(0, 1))(x, y)
538
+ >>> expectedX = torch.diag(torch.ones_like(x))
539
+ >>> expectedY = torch.diag(2 * y)
540
+ >>> assert torch.allclose(jacobian[0], expectedX)
541
+ >>> assert torch.allclose(jacobian[1], expectedY)
542
+
543
+ .. note::
544
+ Using PyTorch ``torch.no_grad`` together with ``jacrev``.
545
+ Case 1: Using ``torch.no_grad`` inside a function:
546
+
547
+ >>> def f(x):
548
+ >>> with torch.no_grad():
549
+ >>> c = x ** 2
550
+ >>> return x - c
551
+
552
+ In this case, ``jacrev(f)(x)`` will respect the inner ``torch.no_grad``.
553
+
554
+ Case 2: Using ``jacrev`` inside ``torch.no_grad`` context manager:
555
+
556
+ >>> with torch.no_grad():
557
+ >>> jacrev(f)(x)
558
+
559
+ In this case, ``jacrev`` will respect the inner ``torch.no_grad``, but not the
560
+ outer one. This is because ``jacrev`` is a "function transform": its result
561
+ should not depend on the result of a context manager outside of ``f``.
562
+ """
563
+ if not (chunk_size is None or chunk_size > 0):
564
+ raise ValueError("jacrev: `chunk_size` should be greater than 0.")
565
+
566
+ @wraps(func)
567
+ def wrapper_fn(*args):
568
+ error_if_complex("jacrev", args, is_input=True)
569
+ vjp_out = _vjp_with_argnums(func, *args, argnums=argnums, has_aux=has_aux)
570
+ if has_aux:
571
+ output, vjp_fn, aux = vjp_out
572
+ else:
573
+ output, vjp_fn = vjp_out
574
+
575
+ # See NOTE: [Computing jacobian with vmap and vjp for multiple outputs]
576
+ flat_output, output_spec = tree_flatten(output)
577
+
578
+ error_if_complex("jacrev", flat_output, is_input=False)
579
+
580
+ # NB: vjp already checks that all outputs are tensors
581
+ # Step 1: Construct grad_outputs by splitting the standard basis
582
+ flat_output_numels = tuple(out.numel() for out in flat_output)
583
+
584
+ primals = _slice_argnums(args, argnums)
585
+ flat_primals, primals_spec = tree_flatten(primals)
586
+
587
+ def compute_jacobian_stacked():
588
+ # Helper function to compute chunked Jacobian
589
+ # The intermediate chunked calculation are only
590
+ # scoped at this function level.
591
+ chunked_results = []
592
+ for flat_basis_chunk in _chunked_standard_basis_for_(
593
+ flat_output, flat_output_numels, chunk_size=chunk_size
594
+ ):
595
+ if chunk_size == 1:
596
+ # sanity check.
597
+ for t in flat_basis_chunk:
598
+ assert t.size(0) == 1
599
+
600
+ flat_basis_chunk = tree_map(
601
+ lambda t: torch.squeeze(t, 0), flat_basis_chunk
602
+ )
603
+
604
+ basis = tree_unflatten(flat_basis_chunk, output_spec)
605
+
606
+ if chunk_size == 1:
607
+ # Behaviour with `chunk_size=1` is same as `for-loop`
608
+ # i.e. user shouldn't deal with the limitations of vmap.
609
+ chunked_result = vjp_fn(basis)
610
+ else: # chunk_size is None or chunk_size != 1
611
+ chunked_result = vmap(vjp_fn)(basis)
612
+
613
+ flat_results = pytree.tree_leaves(chunked_result)
614
+
615
+ if chunk_size == 1:
616
+ flat_results = tree_map(
617
+ lambda t: torch.unsqueeze(t, 0), flat_results
618
+ )
619
+
620
+ chunked_results.append(flat_results)
621
+
622
+ if len(chunked_results) == 1:
623
+ # Short-circuit if we used a single chunk
624
+ return chunked_results[0]
625
+
626
+ # Concatenate chunks.
627
+ flat_results = []
628
+ # Iterate and concat the jacobians of different
629
+ # inputs.
630
+ for idx in range(len(flat_primals)):
631
+ r = tuple(r_[idx] for r_ in chunked_results)
632
+ flat_results.append(torch.cat(r, 0))
633
+
634
+ return flat_results
635
+
636
+ def compute_jacobian_preallocate_and_copy():
637
+ # Helper function to compute chunked Jacobian
638
+ # The intermediate chunked calculation are only
639
+ # scoped at this function level.
640
+ out_vec_size = sum(flat_output_numels)
641
+
642
+ # Don't pre-allocate if we have a single chunk.
643
+ if not (chunk_size is None or chunk_size >= out_vec_size):
644
+ stacked_results = [
645
+ primal.new_zeros(out_vec_size, *primal.shape)
646
+ for primal in flat_primals
647
+ ]
648
+
649
+ for idx, flat_basis_chunk in enumerate(
650
+ _chunked_standard_basis_for_(
651
+ flat_output, flat_output_numels, chunk_size=chunk_size
652
+ )
653
+ ):
654
+ if chunk_size == 1:
655
+ # sanity check.
656
+ for t in flat_basis_chunk:
657
+ assert t.size(0) == 1
658
+
659
+ flat_basis_chunk = [torch.squeeze(t, 0) for t in flat_basis_chunk]
660
+
661
+ basis = tree_unflatten(flat_basis_chunk, output_spec)
662
+
663
+ if chunk_size == 1:
664
+ # Behaviour with `chunk_size=1` is same as `for-loop`
665
+ # i.e. user shouldn't deal with the limitations of vmap.
666
+ chunked_result = vjp_fn(basis)
667
+ else: # chunk_size is None or chunk_size != 1
668
+ chunked_result = vmap(vjp_fn)(basis)
669
+
670
+ flat_results = pytree.tree_leaves(chunked_result)
671
+
672
+ # Short-circuit if we have a single chunk.
673
+ if chunk_size is None or chunk_size >= out_vec_size:
674
+ if chunk_size == 1: # and out_vec_size == 1
675
+ # Since we squeezed the output dim
676
+ flat_results = tree_map(
677
+ lambda t: torch.unsqueeze(t, 0), flat_results
678
+ )
679
+ return flat_results
680
+
681
+ for r, sr in zip(flat_results, stacked_results):
682
+ sr[idx * chunk_size : (idx + 1) * chunk_size].copy_(r)
683
+
684
+ return stacked_results
685
+
686
+ if _preallocate_and_copy:
687
+ flat_jacobians_per_input = compute_jacobian_preallocate_and_copy()
688
+ else:
689
+ flat_jacobians_per_input = compute_jacobian_stacked()
690
+
691
+ # Step 2: The returned jacobian is one big tensor per input. In this step,
692
+ # we split each Tensor by output.
693
+ flat_jacobians_per_input = [
694
+ result.split(flat_output_numels, dim=0)
695
+ for result in flat_jacobians_per_input
696
+ ]
697
+ flat_input_flat_output = [
698
+ tuple(
699
+ split.view(out.shape + primal.shape)
700
+ for split, out in zip(splits, flat_output)
701
+ )
702
+ for splits, primal in zip(flat_jacobians_per_input, flat_primals)
703
+ ]
704
+
705
+ # Step 3: Right now, `jacobian` is a List[List[Tensor]].
706
+ # The outer List corresponds to the number of primals,
707
+ # the inner List corresponds to the number of outputs.
708
+ # We need to:
709
+ # a. Exchange the order of the outer List and inner List
710
+ # b. tree_unflatten the inner Lists (which correspond to the primals)
711
+ # c. handle the argnums=int case
712
+ # d. tree_unflatten the outer List (which corresponds to the outputs)
713
+ flat_output_flat_input = tuple(zip(*flat_input_flat_output))
714
+
715
+ flat_output_input = tuple(
716
+ tree_unflatten(flat_input, primals_spec)
717
+ for flat_input in flat_output_flat_input
718
+ )
719
+
720
+ if isinstance(argnums, int):
721
+ flat_output_input = tuple(
722
+ _safe_zero_index(flat_input) for flat_input in flat_output_input
723
+ )
724
+ output_input = tree_unflatten(flat_output_input, output_spec)
725
+ if has_aux:
726
+ return output_input, aux
727
+ return output_input
728
+
729
+ return wrapper_fn
730
+
731
+
732
+ # NOTE: [Computing jacobian with vmap and vjp for multiple outputs]
733
+ #
734
+ # Let's consider f(x) = (x**2, x.sum()) and let x = torch.randn(3).
735
+ # It turns out we can compute the jacobian of this function with a single
736
+ # call to autograd.grad by using vmap over the correct grad_outputs.
737
+ #
738
+ # Firstly, one way to compute the jacobian is to stack x**2 and x.sum()
739
+ # into a 4D vector. E.g., use g(x) = torch.stack([x**2, x.sum()])
740
+ #
741
+ # To get the first row of the jacobian, we call
742
+ # >>> autograd.grad(g(x), x, grad_outputs=torch.tensor([1, 0, 0, 0]))
743
+ # To get the 2nd row of the jacobian, we call
744
+ # >>> autograd.grad(g(x), x, grad_outputs=torch.tensor([0, 1, 0, 0]))
745
+ # and so on.
746
+ #
747
+ # Using vmap, we can vectorize all 4 of these computations into one by
748
+ # passing the standard basis for R^4 as the grad_output.
749
+ # vmap(partial(autograd.grad, g(x), x))(torch.eye(4)).
750
+ #
751
+ # Now, how do we compute the jacobian *without stacking the output*?
752
+ # We can just split the standard basis across the outputs. So to
753
+ # compute the jacobian of f(x), we'd use
754
+ # >>> autograd.grad(f(x), x, grad_outputs=_construct_standard_basis_for(...))
755
+ # The grad_outputs looks like the following:
756
+ # ( torch.tensor([[1, 0, 0],
757
+ # [0, 1, 0],
758
+ # [0, 0, 1],
759
+ # [0, 0, 0]]),
760
+ # torch.tensor([[0],
761
+ # [0],
762
+ # [0],
763
+ # [1]]) )
764
+ #
765
+ # But we're not done yet!
766
+ # >>> vmap(partial(autograd.grad(f(x), x, grad_outputs=...)))
767
+ # returns a Tensor of shape [4, 3]. We have to remember to split the
768
+ # jacobian of shape [4, 3] into two:
769
+ # - one of shape [3, 3] for the first output
770
+ # - one of shape [ 3] for the second output
771
+
772
+
773
+ def _chunked_standard_basis_for_(tensors, tensor_numels, chunk_size=None):
774
+ # This function:
775
+ # - constructs a N=sum(tensor_numels) standard basis. i.e. an NxN identity matrix.
776
+ # - Splits the identity matrix into chunks with each chunk size determined by `tensor_numels`.
777
+ # - Each chunk corresponds to one tensor. The chunk has the same dtype and
778
+ # device as the tensor
779
+ #
780
+ # For example, with tensor_numels = [1, 2, 1], this function returns:
781
+ # ( tensor([[1], tensor([[0, 0], tensor([[0],
782
+ # [0], [1, 0], [0],
783
+ # [0], [0, 1], [0],
784
+ # [0]]) , [0, 0]]) , [1]]) )
785
+ #
786
+ # Precondition: tensor_numels == tuple(tensor.numel() for tensor in tensors)
787
+ # Precondition: tensors always has at least one element.
788
+ #
789
+ # See NOTE: [Computing jacobian with vmap and grad for multiple tensors]
790
+ # for context behind this function.
791
+ # NOTE: Argument `chunk_size` is used to generate chunked basis instead of
792
+ # one huge basis matrix. `chunk_size` dictates the maximum size of the
793
+ # basis matrix along dim=0.
794
+ assert len(tensors) == len(tensor_numels)
795
+ assert len(tensors) > 0
796
+ assert chunk_size is None or chunk_size > 0
797
+ total_numel = sum(tensor_numels)
798
+ if chunk_size and chunk_size < total_numel:
799
+ chunk_numels = get_chunk_sizes(total_numel, chunk_size)
800
+ else: # chunk_size is None or chunk_size >= total_numel
801
+ chunk_size = total_numel
802
+ chunk_numels = [total_numel]
803
+
804
+ diag_start_indices = (
805
+ 0,
806
+ *torch.tensor(tensor_numels).cumsum(dim=0)[:-1].neg().unbind(),
807
+ )
808
+
809
+ for chunk_idx, total_numel in enumerate(chunk_numels):
810
+ chunks = tuple(
811
+ tensor.new_zeros(total_numel, tensor_numel)
812
+ for tensor, tensor_numel in zip(tensors, tensor_numels)
813
+ )
814
+
815
+ for chunk, diag_start_idx in zip(chunks, diag_start_indices):
816
+ chunk.diagonal(diag_start_idx + chunk_idx * chunk_size).fill_(1)
817
+ chunks = tuple(
818
+ chunk.view(total_numel, *tensor.shape)
819
+ for chunk, tensor in zip(chunks, tensors)
820
+ )
821
+ yield chunks
822
+
823
+
824
+ def _construct_standard_basis_for(tensors, tensor_numels):
825
+ for basis in _chunked_standard_basis_for_(tensors, tensor_numels, chunk_size=None):
826
+ return basis
827
+
828
+
829
+ def _validate_and_wrap_argnum(argnum, num_args):
830
+ if not isinstance(argnum, int):
831
+ raise RuntimeError(f"argnum must be int, got: {type(argnum)}")
832
+ if argnum >= 0 and argnum < num_args:
833
+ return argnum
834
+ if argnum < 0 and argnum >= -num_args:
835
+ return argnum + num_args
836
+ raise RuntimeError(f"Got argnum={argnum}, but only {num_args} positional inputs")
837
+
838
+
839
+ def _check_unique_non_empty(argnums):
840
+ if isinstance(argnums, tuple):
841
+ if len(argnums) == 0:
842
+ raise RuntimeError("argnums must be non-empty")
843
+ if len(set(argnums)) != len(argnums):
844
+ raise RuntimeError(f"argnums elements must be unique, got {argnums}")
845
+
846
+
847
+ def _replace_args(old_args, new_args, argnums):
848
+ if isinstance(argnums, int):
849
+ if len(new_args) != 1:
850
+ raise RuntimeError(
851
+ f"new_args should be of size 1, was of size {len(new_args)}"
852
+ )
853
+ return tuple(
854
+ new_args[0] if i == argnums else old_args[i] for i in range(len(old_args))
855
+ )
856
+ if isinstance(argnums, tuple):
857
+ if len(new_args) != len(argnums):
858
+ raise RuntimeError(
859
+ "new_args should have the same size as argnums. "
860
+ f"Argnums size {len(argnums)}, new_args size {len(new_args)}"
861
+ )
862
+
863
+ def get_right_elem(i):
864
+ return new_args[argnums.index(i)] if i in argnums else old_args[i]
865
+
866
+ return tuple(get_right_elem(i) for i in range(len(old_args)))
867
+ raise RuntimeError(f"argnums must be int or Tuple[int, ...], got: {type(argnums)}")
868
+
869
+
870
+ def _validate_and_wrap_argnums(argnums, num_args):
871
+ if isinstance(argnums, int):
872
+ return _validate_and_wrap_argnum(argnums, num_args)
873
+ if isinstance(argnums, tuple):
874
+ return tuple(_validate_and_wrap_argnum(argnum, num_args) for argnum in argnums)
875
+ raise AssertionError("Should never get here")
876
+
877
+
878
+ def _slice_argnums(args, argnums, as_tuple=True):
879
+ if not isinstance(argnums, int) and not isinstance(argnums, tuple):
880
+ raise RuntimeError(
881
+ f"argnums must be int or Tuple[int, ...], got: {type(argnums)}"
882
+ )
883
+ argnums = _validate_and_wrap_argnums(argnums, len(args))
884
+ _check_unique_non_empty(argnums)
885
+ if isinstance(argnums, int):
886
+ if as_tuple:
887
+ return (args[argnums],)
888
+ else:
889
+ return args[argnums]
890
+ return tuple(args[i] for i in argnums)
891
+
892
+
893
+ JVP_NESTING = 0
894
+
895
+
896
+ def assert_flat_tuple_of_tensors(elts: Any, api: str, argname: str) -> None:
897
+ if not isinstance(elts, tuple):
898
+ raise RuntimeError(
899
+ f"{api}: Expected {argname} to be a tuple of Tensors, got {type(elts)}"
900
+ )
901
+ for elt in elts:
902
+ if isinstance(elt, torch.Tensor):
903
+ continue
904
+ raise RuntimeError(
905
+ f"{api}: Expected {argname} to be a tuple of Tensors, got "
906
+ f"a tuple with an element of type {type(elt)}"
907
+ )
908
+ if len(elts) == 0:
909
+ raise RuntimeError(
910
+ f"{api}: Expected {argname} to be a non-empty tuple of Tensors."
911
+ )
912
+
913
+
914
+ def assert_non_empty_tensor_output(output: List[Any], api: str) -> None:
915
+ if (len(output) == 1 and output[0] is None) or len(output) < 1:
916
+ raise RuntimeError(
917
+ f"{api}: Expected f to be a function that has non-empty output (got output = {output})"
918
+ )
919
+ for o in output:
920
+ if not isinstance(o, torch.Tensor):
921
+ raise RuntimeError(
922
+ f"{api}: expected f(*primals) to return only tensors"
923
+ f", got unsupported type {type(o)}"
924
+ )
925
+
926
+
927
+ def assert_output_is_tensor_or_tensors(output: Any, api: str) -> None:
928
+ if isinstance(output, torch.Tensor):
929
+ return
930
+ if not isinstance(output, tuple):
931
+ raise RuntimeError(
932
+ f"{api}: Expected output of f to be a Tensor or Tensors, got "
933
+ f"{type(output)}"
934
+ )
935
+ if len(output) == 0:
936
+ raise RuntimeError(
937
+ f"{api}: Expected output of f to be a non-empty tuple of Tensors."
938
+ )
939
+ for out in output:
940
+ if isinstance(out, torch.Tensor):
941
+ continue
942
+ raise RuntimeError(
943
+ f"{api}: Expected output of f to be a Tensor or Tensors, got "
944
+ f"{type(out)} as an output"
945
+ )
946
+
947
+
948
+ def assert_non_empty_list_of_tensors(
949
+ output: List[torch.Tensor], api: str, argname: str
950
+ ) -> None:
951
+ if len(output) == 0:
952
+ raise RuntimeError(f"{api}: Expected {argname} to contain at least one Tensor.")
953
+ for out in output:
954
+ if isinstance(out, torch.Tensor):
955
+ continue
956
+ raise RuntimeError(
957
+ f"{api}: Expected {argname} to only contain Tensors, got " f"{type(out)}"
958
+ )
959
+
960
+
961
+ jvp_str = "jvp(f, primals, tangents)"
962
+
963
+
964
+ def safe_unpack_dual(dual, strict):
965
+ if not isinstance(dual, torch.Tensor):
966
+ raise RuntimeError(
967
+ f"{jvp_str}: expected f(*args) to return only tensors"
968
+ f", got unsupported type {type(dual)}"
969
+ )
970
+
971
+ primal, tangent = fwAD.unpack_dual(dual)
972
+ if tangent is None:
973
+ if strict:
974
+ raise RuntimeError(
975
+ "jvp(f, primals, tangents, strict=True): "
976
+ "The output of f is independent of "
977
+ "the inputs. This is not allowed with strict=True."
978
+ )
979
+ tangent = torch.zeros_like(primal)
980
+ return primal, tangent
981
+
982
+
983
+ @exposed_in("torch.func")
984
+ def jvp(
985
+ func: Callable,
986
+ primals: Any,
987
+ tangents: Any,
988
+ *,
989
+ strict: bool = False,
990
+ has_aux: bool = False,
991
+ ):
992
+ """
993
+ Standing for the Jacobian-vector product, returns a tuple containing
994
+ the output of `func(*primals)` and the "Jacobian of ``func`` evaluated at
995
+ ``primals``" times ``tangents``. This is also known as forward-mode autodiff.
996
+
997
+ Args:
998
+ func (function): A Python function that takes one or more arguments,
999
+ one of which must be a Tensor, and returns one or more Tensors
1000
+ primals (Tensors): Positional arguments to ``func`` that must all be
1001
+ Tensors. The returned function will also be computing the
1002
+ derivative with respect to these arguments
1003
+ tangents (Tensors): The "vector" for which Jacobian-vector-product is
1004
+ computed. Must be the same structure and sizes as the inputs to
1005
+ ``func``.
1006
+ has_aux (bool): Flag indicating that ``func`` returns a
1007
+ ``(output, aux)`` tuple where the first element is the output of
1008
+ the function to be differentiated and the second element is
1009
+ other auxiliary objects that will not be differentiated.
1010
+ Default: False.
1011
+
1012
+ Returns:
1013
+ Returns a ``(output, jvp_out)`` tuple containing the output of ``func``
1014
+ evaluated at ``primals`` and the Jacobian-vector product.
1015
+ If ``has_aux is True``, then instead returns a ``(output, jvp_out, aux)`` tuple.
1016
+
1017
+ .. note::
1018
+ You may see this API error out with "forward-mode AD not implemented
1019
+ for operator X". If so, please file a bug report and we will prioritize it.
1020
+
1021
+ jvp is useful when you wish to compute gradients of a function R^1 -> R^N
1022
+
1023
+ >>> from torch.func import jvp
1024
+ >>> x = torch.randn([])
1025
+ >>> f = lambda x: x * torch.tensor([1., 2., 3])
1026
+ >>> value, grad = jvp(f, (x,), (torch.tensor(1.),))
1027
+ >>> assert torch.allclose(value, f(x))
1028
+ >>> assert torch.allclose(grad, torch.tensor([1., 2, 3]))
1029
+
1030
+ :func:`jvp` can support functions with multiple inputs by passing in the
1031
+ tangents for each of the inputs
1032
+
1033
+ >>> from torch.func import jvp
1034
+ >>> x = torch.randn(5)
1035
+ >>> y = torch.randn(5)
1036
+ >>> f = lambda x, y: (x * y)
1037
+ >>> _, output = jvp(f, (x, y), (torch.ones(5), torch.ones(5)))
1038
+ >>> assert torch.allclose(output, x + y)
1039
+
1040
+ """
1041
+
1042
+ return _jvp_with_argnums(
1043
+ func, primals, tangents, argnums=None, strict=strict, has_aux=has_aux
1044
+ )
1045
+
1046
+
1047
+ def _jvp_with_argnums(
1048
+ func: Callable,
1049
+ primals: Any,
1050
+ tangents: Any,
1051
+ argnums: Optional[argnums_t],
1052
+ *,
1053
+ strict: bool = False,
1054
+ has_aux: bool,
1055
+ ):
1056
+ # This is the same function as jvp but also accepts an argnums argument
1057
+ # Most args are the same as jvp except for the added argument
1058
+ # argnums (Optional[int or tuple[int]]): Optional, specifies the argument(s) to compute gradients with respect to.
1059
+ # If None, computes the gradients with respect to all inputs (used for jvp). Default: None
1060
+ # Because of this, tangents must be of length argnums and matches up to the corresponding primal whose index is
1061
+ # given by argnums
1062
+ #
1063
+ # WARN: Users should NOT call this function directly and should just be calling jvp.
1064
+ # It is only separated so that inputs passed to jacfwd but not differentiated get the correct wrappers.
1065
+ #
1066
+ # NOTE: All error messages are produced as if jvp was being called, even if this was called by jacfwd
1067
+ #
1068
+ # Returns the same two elements as :func:`jvp` but the returned tuple, ``jvp_out``, only has JVPs with respect to
1069
+ # the primals given by argnums
1070
+ if not isinstance(primals, tuple):
1071
+ raise RuntimeError(
1072
+ f"{jvp_str}: Expected primals to be a tuple. "
1073
+ f"E.g. it should be valid to call f(*primals)."
1074
+ )
1075
+ diff_args = primals if argnums is None else _slice_argnums(primals, argnums)
1076
+ flat_primals, primals_spec = tree_flatten(diff_args)
1077
+ flat_tangents, tangents_spec = tree_flatten(tangents)
1078
+ if primals_spec != tangents_spec:
1079
+ raise RuntimeError(
1080
+ f"{jvp_str}: Expected primals and tangents to have the same python "
1081
+ f"structure. For example, if primals is a tuple of 3 tensors, "
1082
+ f"tangents also must be. Got primals with structure {primals_spec} "
1083
+ f"and tangents with structure {tangents_spec}"
1084
+ )
1085
+ assert_non_empty_list_of_tensors(flat_primals, jvp_str, "primals")
1086
+ assert_non_empty_list_of_tensors(flat_tangents, jvp_str, "tangents")
1087
+
1088
+ global JVP_NESTING
1089
+
1090
+ with jvp_increment_nesting() as level:
1091
+ with fwAD._set_fwd_grad_enabled(True):
1092
+ ctx = fwAD.dual_level if JVP_NESTING == 1 else contextlib.nullcontext
1093
+ with ctx():
1094
+ flat_duals = tuple(
1095
+ fwAD.make_dual(p, t) for p, t in zip(flat_primals, flat_tangents)
1096
+ )
1097
+ duals = tree_unflatten(flat_duals, primals_spec)
1098
+ if argnums is not None:
1099
+ primals = _wrap_all_tensors(primals, level)
1100
+ duals = _replace_args(primals, duals, argnums)
1101
+ result_duals = func(*duals)
1102
+ if has_aux:
1103
+ if not (isinstance(result_duals, tuple) and len(result_duals) == 2):
1104
+ raise RuntimeError(
1105
+ f"{jvp_str}: output of function f should be a tuple: (output, aux) "
1106
+ "if has_aux is True"
1107
+ )
1108
+ result_duals, aux = result_duals
1109
+ aux = _undo_create_differentiable(aux, level)
1110
+
1111
+ result_duals, spec = tree_flatten(result_duals)
1112
+ assert_non_empty_tensor_output(result_duals, jvp_str)
1113
+
1114
+ primals_out, tangents_out = zip(
1115
+ *[safe_unpack_dual(dual, strict) for dual in result_duals]
1116
+ )
1117
+ primals_out = tree_map(
1118
+ partial(_undo_create_differentiable, level=level), primals_out
1119
+ )
1120
+ tangents_out = tree_map(
1121
+ partial(_undo_create_differentiable, level=level), tangents_out
1122
+ )
1123
+
1124
+ primals_out_unflatten = tree_unflatten(primals_out, spec)
1125
+ tangents_out_unflatten = tree_unflatten(tangents_out, spec)
1126
+ if has_aux:
1127
+ return primals_out_unflatten, tangents_out_unflatten, aux
1128
+
1129
+ return primals_out_unflatten, tangents_out_unflatten
1130
+
1131
+
1132
+ def safe_unflatten(tensor, dim, shape):
1133
+ if len(shape) == 0:
1134
+ assert tensor.shape[dim] == 1
1135
+ return tensor.squeeze(dim)
1136
+ return tensor.unflatten(dim, shape)
1137
+
1138
+
1139
+ @exposed_in("torch.func")
1140
+ def jacfwd(
1141
+ func: Callable,
1142
+ argnums: argnums_t = 0,
1143
+ has_aux: bool = False,
1144
+ *,
1145
+ randomness: str = "error",
1146
+ ):
1147
+ """
1148
+ Computes the Jacobian of ``func`` with respect to the arg(s) at index
1149
+ ``argnum`` using forward-mode autodiff
1150
+
1151
+ Args:
1152
+ func (function): A Python function that takes one or more arguments,
1153
+ one of which must be a Tensor, and returns one or more Tensors
1154
+ argnums (int or Tuple[int]): Optional, integer or tuple of integers,
1155
+ saying which arguments to get the Jacobian with respect to.
1156
+ Default: 0.
1157
+ has_aux (bool): Flag indicating that ``func`` returns a
1158
+ ``(output, aux)`` tuple where the first element is the output of
1159
+ the function to be differentiated and the second element is
1160
+ auxiliary objects that will not be differentiated.
1161
+ Default: False.
1162
+ randomness(str): Flag indicating what type of randomness to use.
1163
+ See :func:`vmap` for more detail. Allowed: "different", "same", "error".
1164
+ Default: "error"
1165
+
1166
+ Returns:
1167
+ Returns a function that takes in the same inputs as ``func`` and
1168
+ returns the Jacobian of ``func`` with respect to the arg(s) at
1169
+ ``argnums``. If ``has_aux is True``, then the returned function
1170
+ instead returns a ``(jacobian, aux)`` tuple where ``jacobian``
1171
+ is the Jacobian and ``aux`` is auxiliary objects returned by ``func``.
1172
+
1173
+ .. note::
1174
+ You may see this API error out with "forward-mode AD not implemented
1175
+ for operator X". If so, please file a bug report and we will prioritize it.
1176
+ An alternative is to use :func:`jacrev`, which has better operator coverage.
1177
+
1178
+ A basic usage with a pointwise, unary operation will give a diagonal array
1179
+ as the Jacobian
1180
+
1181
+ >>> from torch.func import jacfwd
1182
+ >>> x = torch.randn(5)
1183
+ >>> jacobian = jacfwd(torch.sin)(x)
1184
+ >>> expected = torch.diag(torch.cos(x))
1185
+ >>> assert torch.allclose(jacobian, expected)
1186
+
1187
+ :func:`jacfwd` can be composed with vmap to produce batched
1188
+ Jacobians:
1189
+
1190
+ >>> from torch.func import jacfwd, vmap
1191
+ >>> x = torch.randn(64, 5)
1192
+ >>> jacobian = vmap(jacfwd(torch.sin))(x)
1193
+ >>> assert jacobian.shape == (64, 5, 5)
1194
+
1195
+ If you would like to compute the output of the function as well as the
1196
+ jacobian of the function, use the ``has_aux`` flag to return the output
1197
+ as an auxiliary object:
1198
+
1199
+ >>> from torch.func import jacfwd
1200
+ >>> x = torch.randn(5)
1201
+ >>>
1202
+ >>> def f(x):
1203
+ >>> return x.sin()
1204
+ >>>
1205
+ >>> def g(x):
1206
+ >>> result = f(x)
1207
+ >>> return result, result
1208
+ >>>
1209
+ >>> jacobian_f, f_x = jacfwd(g, has_aux=True)(x)
1210
+ >>> assert torch.allclose(f_x, f(x))
1211
+
1212
+ Additionally, :func:`jacrev` can be composed with itself or :func:`jacrev`
1213
+ to produce Hessians
1214
+
1215
+ >>> from torch.func import jacfwd, jacrev
1216
+ >>> def f(x):
1217
+ >>> return x.sin().sum()
1218
+ >>>
1219
+ >>> x = torch.randn(5)
1220
+ >>> hessian = jacfwd(jacrev(f))(x)
1221
+ >>> assert torch.allclose(hessian, torch.diag(-x.sin()))
1222
+
1223
+ By default, :func:`jacfwd` computes the Jacobian with respect to the first
1224
+ input. However, it can compute the Jacboian with respect to a different
1225
+ argument by using ``argnums``:
1226
+
1227
+ >>> from torch.func import jacfwd
1228
+ >>> def f(x, y):
1229
+ >>> return x + y ** 2
1230
+ >>>
1231
+ >>> x, y = torch.randn(5), torch.randn(5)
1232
+ >>> jacobian = jacfwd(f, argnums=1)(x, y)
1233
+ >>> expected = torch.diag(2 * y)
1234
+ >>> assert torch.allclose(jacobian, expected)
1235
+
1236
+ Additionally, passing a tuple to ``argnums`` will compute the Jacobian
1237
+ with respect to multiple arguments
1238
+
1239
+ >>> from torch.func import jacfwd
1240
+ >>> def f(x, y):
1241
+ >>> return x + y ** 2
1242
+ >>>
1243
+ >>> x, y = torch.randn(5), torch.randn(5)
1244
+ >>> jacobian = jacfwd(f, argnums=(0, 1))(x, y)
1245
+ >>> expectedX = torch.diag(torch.ones_like(x))
1246
+ >>> expectedY = torch.diag(2 * y)
1247
+ >>> assert torch.allclose(jacobian[0], expectedX)
1248
+ >>> assert torch.allclose(jacobian[1], expectedY)
1249
+
1250
+ """
1251
+
1252
+ @wraps(func)
1253
+ def wrapper_fn(*args):
1254
+ error_if_complex("jacfwd", args, is_input=True)
1255
+ primals = args if argnums is None else _slice_argnums(args, argnums)
1256
+ flat_primals, primals_spec = tree_flatten(primals)
1257
+ flat_primals_numels = tuple(p.numel() for p in flat_primals)
1258
+ flat_basis = _construct_standard_basis_for(flat_primals, flat_primals_numels)
1259
+ basis = tree_unflatten(flat_basis, primals_spec)
1260
+
1261
+ def push_jvp(basis):
1262
+ output = _jvp_with_argnums(
1263
+ func, args, basis, argnums=argnums, has_aux=has_aux
1264
+ )
1265
+ # output[0] is the output of `func(*args)`
1266
+ error_if_complex("jacfwd", output[0], is_input=False)
1267
+ if has_aux:
1268
+ _, jvp_out, aux = output
1269
+ return jvp_out, aux
1270
+ _, jvp_out = output
1271
+ return jvp_out
1272
+
1273
+ results = vmap(push_jvp, randomness=randomness)(basis)
1274
+ if has_aux:
1275
+ results, aux = results
1276
+ # aux is in the standard basis format, e.g. NxN matrix
1277
+ # We need to fetch the first element as original `func` output
1278
+ flat_aux, aux_spec = tree_flatten(aux)
1279
+ flat_aux = [value[0] for value in flat_aux]
1280
+ aux = tree_unflatten(flat_aux, aux_spec)
1281
+
1282
+ jac_outs, spec = tree_flatten(results)
1283
+ # Most probably below output check can never raise an error
1284
+ # as jvp should test the output before
1285
+ # assert_non_empty_output(jac_outs, 'jacfwd(f, ...)(*args)')
1286
+
1287
+ jac_outs_ins = tuple(
1288
+ tuple(
1289
+ safe_unflatten(jac_out_in, -1, primal.shape)
1290
+ for primal, jac_out_in in zip(
1291
+ flat_primals,
1292
+ jac_out.movedim(0, -1).split(flat_primals_numels, dim=-1),
1293
+ )
1294
+ )
1295
+ for jac_out in jac_outs
1296
+ )
1297
+ jac_outs_ins = tuple(
1298
+ tree_unflatten(jac_ins, primals_spec) for jac_ins in jac_outs_ins
1299
+ )
1300
+
1301
+ if isinstance(argnums, int):
1302
+ jac_outs_ins = tuple(jac_ins[0] for jac_ins in jac_outs_ins)
1303
+ if has_aux:
1304
+ return tree_unflatten(jac_outs_ins, spec), aux
1305
+ return tree_unflatten(jac_outs_ins, spec)
1306
+
1307
+ return wrapper_fn
1308
+
1309
+
1310
+ @exposed_in("torch.func")
1311
+ def hessian(func, argnums=0):
1312
+ """
1313
+ Computes the Hessian of ``func`` with respect to the arg(s) at index
1314
+ ``argnum`` via a forward-over-reverse strategy.
1315
+
1316
+ The forward-over-reverse strategy (composing ``jacfwd(jacrev(func))``) is
1317
+ a good default for good performance. It is possible to compute Hessians
1318
+ through other compositions of :func:`jacfwd` and :func:`jacrev` like
1319
+ ``jacfwd(jacfwd(func))`` or ``jacrev(jacrev(func))``.
1320
+
1321
+ Args:
1322
+ func (function): A Python function that takes one or more arguments,
1323
+ one of which must be a Tensor, and returns one or more Tensors
1324
+ argnums (int or Tuple[int]): Optional, integer or tuple of integers,
1325
+ saying which arguments to get the Hessian with respect to.
1326
+ Default: 0.
1327
+
1328
+ Returns:
1329
+ Returns a function that takes in the same inputs as ``func`` and
1330
+ returns the Hessian of ``func`` with respect to the arg(s) at
1331
+ ``argnums``.
1332
+
1333
+ .. note::
1334
+ You may see this API error out with "forward-mode AD not implemented
1335
+ for operator X". If so, please file a bug report and we will prioritize it.
1336
+ An alternative is to use ``jacrev(jacrev(func))``, which has better
1337
+ operator coverage.
1338
+
1339
+ A basic usage with a R^N -> R^1 function gives a N x N Hessian:
1340
+
1341
+ >>> from torch.func import hessian
1342
+ >>> def f(x):
1343
+ >>> return x.sin().sum()
1344
+ >>>
1345
+ >>> x = torch.randn(5)
1346
+ >>> hess = hessian(f)(x) # equivalent to jacfwd(jacrev(f))(x)
1347
+ >>> assert torch.allclose(hess, torch.diag(-x.sin()))
1348
+
1349
+ """
1350
+ return jacfwd(jacrev(func, argnums), argnums)
1351
+
1352
+
1353
+ @doesnt_support_saved_tensors_hooks
1354
+ def grad_and_value_impl(func, argnums, has_aux, args, kwargs) -> Callable:
1355
+ with grad_increment_nesting() as level:
1356
+ output, aux, grad_input = None, None, None
1357
+ # See NOTE [grad and vjp interaction with no_grad]
1358
+ with torch.enable_grad():
1359
+ args = _wrap_all_tensors(args, level)
1360
+ kwargs = _wrap_all_tensors(kwargs, level)
1361
+ diff_args = _slice_argnums(args, argnums, as_tuple=False)
1362
+ tree_map_(partial(_create_differentiable, level=level), diff_args)
1363
+
1364
+ output = func(*args, **kwargs)
1365
+ if has_aux:
1366
+ if not (isinstance(output, tuple) and len(output) == 2):
1367
+ raise RuntimeError(
1368
+ "grad_and_value(f)(*args): output of function f should be a tuple: (output, aux) "
1369
+ "if has_aux is True"
1370
+ )
1371
+ output, aux = output
1372
+
1373
+ if not isinstance(output, torch.Tensor):
1374
+ raise RuntimeError(
1375
+ "grad_and_value(f)(*args): Expected f(*args) "
1376
+ f"to return a Tensor, got {type(output)}"
1377
+ )
1378
+ if output.dim() != 0:
1379
+ raise RuntimeError(
1380
+ "grad_and_value(f)(*args): Expected f(*args) "
1381
+ "to return a scalar Tensor, got tensor with "
1382
+ f"{output.dim()} dims. Maybe you wanted to "
1383
+ "use the vjp or jacrev APIs instead?"
1384
+ )
1385
+
1386
+ flat_diff_args, spec = tree_flatten(diff_args)
1387
+
1388
+ # NB: need create_graph so that backward pass isn't run in no_grad mode
1389
+ flat_outputs = _as_tuple(output)
1390
+ flat_grad_input = _autograd_grad(
1391
+ flat_outputs, flat_diff_args, create_graph=True
1392
+ )
1393
+ grad_input = tree_unflatten(flat_grad_input, spec)
1394
+
1395
+ grad_input = _undo_create_differentiable(grad_input, level)
1396
+ output = _undo_create_differentiable(output, level)
1397
+ if has_aux:
1398
+ aux = _undo_create_differentiable(aux, level)
1399
+
1400
+ if has_aux:
1401
+ return grad_input, (output, aux)
1402
+ return grad_input, output
1403
+
1404
+
1405
+ def grad_impl(func: Callable, argnums: argnums_t, has_aux: bool, args, kwargs):
1406
+ results = grad_and_value_impl(func, argnums, has_aux, args, kwargs)
1407
+ if has_aux:
1408
+ grad, (_, aux) = results
1409
+ return grad, aux
1410
+ grad, _ = results
1411
+ return grad
1412
+
1413
+
1414
+ def _maybe_wrap_functional_tensor(
1415
+ maybe_tensor, level, *, _python_functionalize: bool = False
1416
+ ):
1417
+ if not isinstance(maybe_tensor, torch.Tensor):
1418
+ return maybe_tensor
1419
+ wrapped = _wrap_functional_tensor(maybe_tensor, level)
1420
+ _assert_wrapped_functional(maybe_tensor, wrapped)
1421
+ if _python_functionalize:
1422
+ out = FunctionalTensor(wrapped)
1423
+ torch._mirror_autograd_meta_to(maybe_tensor, out)
1424
+ return out
1425
+ return wrapped
1426
+
1427
+
1428
+ def _wrap_all_tensors_to_functional(
1429
+ tensor_pytree, level, *, _python_functionalize: bool = False
1430
+ ):
1431
+ return tree_map(
1432
+ partial(
1433
+ lambda x: _maybe_wrap_functional_tensor(
1434
+ x, level, _python_functionalize=_python_functionalize
1435
+ )
1436
+ ),
1437
+ tensor_pytree,
1438
+ )
1439
+
1440
+
1441
+ def _maybe_unwrap_functional_tensor(maybe_tensor, *, reapply_views: bool):
1442
+ if not isinstance(maybe_tensor, torch.Tensor):
1443
+ return maybe_tensor
1444
+ if isinstance(maybe_tensor, FunctionalTensor):
1445
+ maybe_tensor = maybe_tensor.elem
1446
+
1447
+ if not torch._is_functional_tensor(maybe_tensor):
1448
+ # If it's not a functional tensor, just return it.
1449
+ # This can happen if we functionalize a fn that returns a global,
1450
+ # which was never wrapped properly.
1451
+ return maybe_tensor
1452
+ # Sync any pending updates on the output tensor
1453
+ torch._sync(maybe_tensor)
1454
+ return _unwrap_functional_tensor(maybe_tensor, reapply_views)
1455
+
1456
+
1457
+ def _unwrap_all_tensors_from_functional(tensor_pytree, *, reapply_views: bool):
1458
+ return tree_map(
1459
+ lambda t: _maybe_unwrap_functional_tensor(t, reapply_views=reapply_views),
1460
+ tensor_pytree,
1461
+ )
1462
+
1463
+
1464
+ @exposed_in("torch.func")
1465
+ def functionalize(func: Callable, *, remove: str = "mutations") -> Callable:
1466
+ """
1467
+ functionalize is a transform that can be used to remove (intermediate)
1468
+ mutations and aliasing from a function, while preserving the function's
1469
+ semantics.
1470
+
1471
+ ``functionalize(func)`` returns a new function with the same semantics
1472
+ as ``func``, but with all intermediate mutations removed.
1473
+ Every inplace operation performed on an intermediate tensor:
1474
+ ``intermediate.foo_()``
1475
+ gets replaced by its out-of-place equivalent:
1476
+ ``intermediate_updated = intermediate.foo()``.
1477
+
1478
+ functionalize is useful for shipping a pytorch program off to
1479
+ backends or compilers that aren't able to easily represent
1480
+ mutations or aliasing operators.
1481
+
1482
+ Args:
1483
+ func (Callable): A Python function that takes one or more arguments.
1484
+ remove (str): An optional string argument, that takes on either
1485
+ the value 'mutations' or 'mutations_and_views'.
1486
+ If 'mutations' is passed in then all mutating operators
1487
+ will be replaced with their non-mutating equivalents.
1488
+ If 'mutations_and_views' is passed in, then additionally, all aliasing
1489
+ operators will be replaced with their non-aliasing equivalents.
1490
+ Default: 'mutations'.
1491
+
1492
+ Returns:
1493
+ Returns a new "functionalized" function. It takes the same inputs as
1494
+ ``func``, and has the same behavior, but any mutations
1495
+ (and optionally aliasing) performed on intermediate tensors
1496
+ in the function will be removed.
1497
+
1498
+ functionalize will also remove mutations (and views) that were performed on function inputs.
1499
+ However to preserve semantics, functionalize will "fix up" the mutations after
1500
+ the transform has finished running, by detecting if any tensor inputs "should have"
1501
+ been mutated, and copying the new data back to the inputs if necessary.
1502
+
1503
+
1504
+ Example::
1505
+
1506
+ >>> # xdoctest: +SKIP
1507
+ >>> import torch
1508
+ >>> from torch.fx.experimental.proxy_tensor import make_fx
1509
+ >>> from torch.func import functionalize
1510
+ >>>
1511
+ >>> # A function that uses mutations and views, but only on intermediate tensors.
1512
+ >>> def f(a):
1513
+ ... b = a + 1
1514
+ ... c = b.view(-1)
1515
+ ... c.add_(1)
1516
+ ... return b
1517
+ ...
1518
+ >>> inpt = torch.randn(2)
1519
+ >>>
1520
+ >>> out1 = f(inpt)
1521
+ >>> out2 = functionalize(f)(inpt)
1522
+ >>>
1523
+ >>> # semantics are the same (outputs are equivalent)
1524
+ >>> print(torch.allclose(out1, out2))
1525
+ True
1526
+ >>>
1527
+ >>> f_traced = make_fx(f)(inpt)
1528
+ >>> f_no_mutations_traced = make_fx(functionalize(f))(inpt)
1529
+ >>> f_no_mutations_and_views_traced = make_fx(functionalize(f, remove='mutations_and_views'))(inpt)
1530
+ >>>
1531
+ >>> print(f_traced.code)
1532
+
1533
+
1534
+
1535
+ def forward(self, a_1):
1536
+ add = torch.ops.aten.add(a_1, 1); a_1 = None
1537
+ view = torch.ops.aten.view(add, [-1])
1538
+ add_ = torch.ops.aten.add_(view, 1); view = None
1539
+ return add
1540
+
1541
+ >>> print(f_no_mutations_traced.code)
1542
+
1543
+
1544
+
1545
+ def forward(self, a_1):
1546
+ add = torch.ops.aten.add(a_1, 1); a_1 = None
1547
+ view = torch.ops.aten.view(add, [-1]); add = None
1548
+ add_1 = torch.ops.aten.add(view, 1); view = None
1549
+ view_1 = torch.ops.aten.view(add_1, [2]); add_1 = None
1550
+ return view_1
1551
+
1552
+ >>> print(f_no_mutations_and_views_traced.code)
1553
+
1554
+
1555
+
1556
+ def forward(self, a_1):
1557
+ add = torch.ops.aten.add(a_1, 1); a_1 = None
1558
+ view_copy = torch.ops.aten.view_copy(add, [-1]); add = None
1559
+ add_1 = torch.ops.aten.add(view_copy, 1); view_copy = None
1560
+ view_copy_1 = torch.ops.aten.view_copy(add_1, [2]); add_1 = None
1561
+ return view_copy_1
1562
+
1563
+
1564
+ >>> # A function that mutates its input tensor
1565
+ >>> def f(a):
1566
+ ... b = a.view(-1)
1567
+ ... b.add_(1)
1568
+ ... return a
1569
+ ...
1570
+ >>> f_no_mutations_and_views_traced = make_fx(functionalize(f, remove='mutations_and_views'))(inpt)
1571
+ >>> #
1572
+ >>> # All mutations and views have been removed,
1573
+ >>> # but there is an extra copy_ in the graph to correctly apply the mutation to the input
1574
+ >>> # after the function has completed.
1575
+ >>> print(f_no_mutations_and_views_traced.code)
1576
+
1577
+
1578
+
1579
+ def forward(self, a_1):
1580
+ view_copy = torch.ops.aten.view_copy(a_1, [-1])
1581
+ add = torch.ops.aten.add(view_copy, 1); view_copy = None
1582
+ view_copy_1 = torch.ops.aten.view_copy(add, [2]); add = None
1583
+ copy_ = torch.ops.aten.copy_(a_1, view_copy_1); a_1 = None
1584
+ return view_copy_1
1585
+
1586
+
1587
+ There are a few "failure modes" for functionalize that are worth calling out:
1588
+ (1) Like other torch.func transforms, `functionalize()` doesn't work with functions
1589
+ that directly use `.backward()`. The same is true for torch.autograd.grad.
1590
+ If you want to use autograd, you can compute gradients directly
1591
+ with `functionalize(grad(f))`.
1592
+ (2) Like other torch.func transforms, `functionalize()` doesn't work with global state.
1593
+ If you call `functionalize(f)` on a function that takes views / mutations of
1594
+ non-local state, functionalization will simply no-op and pass the view/mutation
1595
+ calls directly to the backend.
1596
+ One way to work around this is is to ensure that any non-local state creation
1597
+ is wrapped into a larger function, which you then call functionalize on.
1598
+ (3) `resize_()` has some limitations: functionalize will only work on programs
1599
+ that use resize_()` as long as the tensor being resized is not a view.
1600
+ (4) `as_strided()` has some limitations: functionalize will not work on
1601
+ `as_strided()` calls that result in tensors with overlapping memory.
1602
+
1603
+
1604
+ Finally, a helpful mental model for understanding functionalization is that
1605
+ most user pytorch programs are writing with the public torch API.
1606
+ When executed, torch operators are generally decomposed into
1607
+ our internal C++ "ATen" API.
1608
+ The logic for functionalization happens entirely at the level of ATen.
1609
+ Functionalization knows how to take every aliasing operator in ATen,
1610
+ and map it to its non-aliasing equivalent
1611
+ (e.g. ``tensor.view({-1})`` -> ``at::view_copy(tensor, {-1})``),
1612
+ and how to take every mutating operator in ATen,
1613
+ and map it to its non-mutating equivalent
1614
+ (e.g. ``tensor.add_(1)`` -> ``at::add(tensor, -1)``),
1615
+ while tracking aliases and mutations out-of-line to know when to fix things up.
1616
+ Information about which ATen operators are aliasing or mutating all comes from
1617
+ https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/native/native_functions.yaml.
1618
+ """
1619
+ if remove == "mutations":
1620
+ reapply_views = True
1621
+ elif remove == "mutations_and_views":
1622
+ reapply_views = False
1623
+ else:
1624
+ raise RuntimeError(
1625
+ f"functionalize(f, remove='mutations'): received invalid argument for remove={remove}."
1626
+ " Valid options are:\n"
1627
+ " remove='mutations': all inplace and out= operators will be removed from the program, and replaced"
1628
+ " with their out-of-place equivalents.\n"
1629
+ " remove='mutations_and_views': In addition to the above, all aliasing operators {view} will be"
1630
+ " replaced with their non-aliasing counterparts, {view}_copy.\n"
1631
+ )
1632
+
1633
+ @wraps(func)
1634
+ def wrapped(*args, **kwargs):
1635
+ try:
1636
+ func_level = _func_increment_nesting(reapply_views)
1637
+ func_args = _wrap_all_tensors_to_functional(args, func_level)
1638
+ func_kwargs = _wrap_all_tensors_to_functional(kwargs, func_level)
1639
+
1640
+ flattened_unwrapped_args = pytree.arg_tree_leaves(*args)
1641
+ flattened_wrapped_args = pytree.arg_tree_leaves(*func_args)
1642
+ flattened_unwrapped_kwargs = pytree.arg_tree_leaves(**kwargs)
1643
+ flattened_wrapped_kwargs = pytree.arg_tree_leaves(**func_kwargs)
1644
+
1645
+ func_outputs = func(*func_args, **func_kwargs)
1646
+ outputs = _unwrap_all_tensors_from_functional(
1647
+ func_outputs, reapply_views=reapply_views
1648
+ )
1649
+ flat_outputs, func_out_spec = tree_flatten(outputs)
1650
+
1651
+ for a in flattened_wrapped_args + flattened_wrapped_kwargs:
1652
+ if isinstance(a, torch.Tensor):
1653
+ # Call sync_() on the inputs, to ensure that any pending mutations have been applied.
1654
+ torch._sync(a)
1655
+
1656
+ # And if any mutations were applied to the inputs, we need to propagate them back to the user.
1657
+ for unwrapped, wrapped in zip(
1658
+ flattened_unwrapped_args, flattened_wrapped_args
1659
+ ):
1660
+ if isinstance(unwrapped, torch.Tensor) and isinstance(
1661
+ wrapped, torch.Tensor
1662
+ ):
1663
+ _propagate_functional_input_mutation(unwrapped, wrapped)
1664
+ for unwrapped, wrapped in zip(
1665
+ flattened_unwrapped_kwargs, flattened_wrapped_kwargs
1666
+ ):
1667
+ if isinstance(unwrapped, torch.Tensor) and isinstance(
1668
+ wrapped, torch.Tensor
1669
+ ):
1670
+ _propagate_functional_input_mutation(unwrapped, wrapped)
1671
+
1672
+ return outputs
1673
+ finally:
1674
+ _func_decrement_nesting()
1675
+
1676
+ return wrapped
1677
+
1678
+
1679
+ @exposed_in("torch.func")
1680
+ def linearize(func: Callable, *primals) -> Tuple[Any, Callable]:
1681
+ """
1682
+ Returns the value of ``func`` at ``primals`` and linear approximation
1683
+ at ``primals``.
1684
+
1685
+ Args:
1686
+ func (Callable): A Python function that takes one or more arguments.
1687
+ primals (Tensors): Positional arguments to ``func`` that must all be
1688
+ Tensors. These are the values at which the function is linearly approximated.
1689
+
1690
+ Returns:
1691
+ Returns a ``(output, jvp_fn)`` tuple containing the output of ``func``
1692
+ applied to ``primals`` and a function that computes the jvp of
1693
+ ``func`` evaluated at ``primals``.
1694
+
1695
+ linearize is useful if jvp is to be computed multiple times at ``primals``. However,
1696
+ to achieve this, linearize saves intermediate computation and has higher memory requirements
1697
+ than directly applying `jvp`. So, if all the ``tangents`` are known, it maybe more efficient
1698
+ to compute vmap(jvp) instead of using linearize.
1699
+
1700
+ .. note::
1701
+ linearize evaluates ``func`` twice. Please file an issue for an implementation
1702
+ with a single evaluation.
1703
+
1704
+ Example::
1705
+ >>> import torch
1706
+ >>> from torch.func import linearize
1707
+ >>> def fn(x):
1708
+ ... return x.sin()
1709
+ ...
1710
+ >>> output, jvp_fn = linearize(fn, torch.zeros(3, 3))
1711
+ >>> jvp_fn(torch.ones(3, 3))
1712
+ tensor([[1., 1., 1.],
1713
+ [1., 1., 1.],
1714
+ [1., 1., 1.]])
1715
+ >>>
1716
+
1717
+ """
1718
+ # Note: We evaluate `fn` twice.
1719
+ # Once for returning the output and other while
1720
+ # tracing the graph.
1721
+ # If this becomes a bottle-neck, we should update
1722
+ # make_fx such that it also returns the output.
1723
+
1724
+ output = func(*primals)
1725
+ _, output_spec = tree_flatten(output)
1726
+
1727
+ flat_primals, primals_argspec = tree_flatten(primals)
1728
+
1729
+ # tangents for tracing
1730
+ flat_tangents = tuple(p.new_empty(()).expand_as(p) for p in flat_primals)
1731
+
1732
+ # function to trace
1733
+ def trace_fn(flat_tangents):
1734
+ with fwAD.dual_level():
1735
+ flat_duals = tuple(
1736
+ fwAD.make_dual(p, t) for p, t in zip(flat_primals, flat_tangents)
1737
+ )
1738
+ duals = tree_unflatten(flat_duals, primals_argspec)
1739
+ output = func(*duals)
1740
+ tangents = tree_map_only(
1741
+ torch.Tensor, lambda dual: safe_unpack_dual(dual, False)[1], output
1742
+ )
1743
+
1744
+ return tangents
1745
+
1746
+ jvp_graph = lazy_dynamo_disallow(make_fx)(trace_fn)(flat_tangents)
1747
+ const_folded_jvp_graph = lazy_dynamo_disallow(const_fold.split_const_subgraphs)(
1748
+ jvp_graph
1749
+ )
1750
+
1751
+ # Hold only the meta-data regarding the primals.
1752
+ flat_primals_shape = tuple(p.shape for p in flat_primals)
1753
+ flat_primals_device = tuple(p.device for p in flat_primals)
1754
+ flat_primals_dtype = tuple(p.dtype for p in flat_primals)
1755
+
1756
+ def forward_ad_checks(flat_tangents):
1757
+ for idx, t in enumerate(flat_tangents):
1758
+ if t.shape != flat_primals_shape[idx]:
1759
+ msg = (
1760
+ f"tangent:{idx} with shape {t.shape} in flattened "
1761
+ f"pytree doesn't match the shape {flat_primals_shape[idx]} "
1762
+ "of the corresponding primal."
1763
+ )
1764
+ raise RuntimeError(msg)
1765
+
1766
+ if t.device != flat_primals_device[idx]:
1767
+ msg = (
1768
+ f"tangent:{idx} with device {t.device} in flattened "
1769
+ f"pytree doesn't match the device {flat_primals_device[idx]} "
1770
+ "of the corresponding primal."
1771
+ )
1772
+ raise RuntimeError(msg)
1773
+
1774
+ if t.dtype != flat_primals_dtype[idx]:
1775
+ msg = (
1776
+ f"tangent:{idx} with dtype {t.dtype} in flattened "
1777
+ f"pytree doesn't match the dtype {flat_primals_dtype[idx]} "
1778
+ "of the corresponding primal."
1779
+ )
1780
+ raise RuntimeError(msg)
1781
+
1782
+ # jvp_fn : callable to return
1783
+ # It takes care of checking the argspec of tangents,
1784
+ # calling the folded fx graph and unflattening fx graph output
1785
+ def jvp_fn(*tangents):
1786
+ flat_tangents, tangent_argspec = tree_flatten(tangents)
1787
+ if tangent_argspec != primals_argspec:
1788
+ raise RuntimeError(
1789
+ f"Expected the tangents {tangent_argspec} to have "
1790
+ f"the same argspec as the primals {primals_argspec}"
1791
+ )
1792
+
1793
+ forward_ad_checks(flat_tangents)
1794
+
1795
+ flat_output = const_folded_jvp_graph(*flat_tangents)
1796
+ # const folded graph can return flat output,
1797
+ # so transform output.
1798
+ return tree_unflatten(flat_output, output_spec)
1799
+
1800
+ return output, jvp_fn
lib/python3.12/site-packages/torch/_functorch/functional_call.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-decorators
2
+ # mypy: allow-untyped-defs
3
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ from torch import Tensor
8
+ from torch._functorch.utils import exposed_in
9
+
10
+
11
+ @exposed_in("torch.func")
12
+ def functional_call(
13
+ module: "torch.nn.Module",
14
+ parameter_and_buffer_dicts: Union[Dict[str, Tensor], Sequence[Dict[str, Tensor]]],
15
+ args: Optional[Union[Any, Tuple]] = None,
16
+ kwargs: Optional[Dict[str, Any]] = None,
17
+ *,
18
+ tie_weights: bool = True,
19
+ strict: bool = False,
20
+ ):
21
+ r"""Performs a functional call on the module by replacing the module parameters
22
+ and buffers with the provided ones.
23
+
24
+ .. note:: If the module has active parametrizations, passing a value in the
25
+ :attr:`parameter_and_buffer_dicts` argument with the name set to the regular parameter
26
+ name will completely disable the parametrization.
27
+ If you want to apply the parametrization function to the value passed
28
+ please set the key as ``{submodule_name}.parametrizations.{parameter_name}.original``.
29
+
30
+ .. note:: If the module performs in-place operations on parameters/buffers, these will be reflected
31
+ in the ``parameter_and_buffer_dicts`` input.
32
+
33
+
34
+ Example::
35
+
36
+ >>> a = {'foo': torch.zeros(())}
37
+ >>> # xdoctest: +SKIP
38
+ >>> mod = Foo() # does self.foo = self.foo + 1
39
+ >>> print(mod.foo) # tensor(0.)
40
+ >>> functional_call(mod, a, torch.ones(()))
41
+ >>> print(mod.foo) # tensor(0.)
42
+ >>> print(a['foo']) # tensor(1.)
43
+
44
+ .. note:: If the module has tied weights, whether or not functional_call respects the tying is determined by the
45
+ tie_weights flag.
46
+
47
+ Example::
48
+
49
+ >>> a = {'foo': torch.zeros(())}
50
+ >>> # xdoctest: +SKIP
51
+ >>> mod = Foo() # has both self.foo and self.foo_tied which are tied. Returns x + self.foo + self.foo_tied
52
+ >>> print(mod.foo) # tensor(1.)
53
+ >>> mod(torch.zeros(())) # tensor(2.)
54
+ >>> functional_call(mod, a, torch.zeros(())) # tensor(0.) since it will change self.foo_tied too
55
+ >>> functional_call(mod, a, torch.zeros(()), tie_weights=False) # tensor(1.)--self.foo_tied is not updated
56
+ >>> new_a = {'foo': torch.zeros(()), 'foo_tied': torch.zeros(())}
57
+ >>> functional_call(mod, new_a, torch.zeros()) # tensor(0.)
58
+
59
+ An example of passing multiple dictionaries
60
+
61
+ .. code-block:: python
62
+
63
+ a = ({'weight': torch.ones(1, 1)}, {'buffer': torch.zeros(1)}) # two separate dictionaries
64
+ mod = nn.Bar(1, 1) # return self.weight @ x + self.buffer
65
+ print(mod.weight) # tensor(...)
66
+ print(mod.buffer) # tensor(...)
67
+ x = torch.randn((1, 1))
68
+ print(x)
69
+ functional_call(mod, a, x) # same as x
70
+ print(mod.weight) # same as before functional_call
71
+
72
+
73
+ And here is an example of applying the grad transform over the parameters
74
+ of a model.
75
+
76
+ .. code-block:: python
77
+
78
+ import torch
79
+ import torch.nn as nn
80
+ from torch.func import functional_call, grad
81
+
82
+ x = torch.randn(4, 3)
83
+ t = torch.randn(4, 3)
84
+ model = nn.Linear(3, 3)
85
+
86
+ def compute_loss(params, x, t):
87
+ y = functional_call(model, params, x)
88
+ return nn.functional.mse_loss(y, t)
89
+
90
+ grad_weights = grad(compute_loss)(dict(model.named_parameters()), x, t)
91
+
92
+ .. note:: If the user does not need grad tracking outside of grad transforms, they can detach all of the
93
+ parameters for better performance and memory usage
94
+
95
+ Example::
96
+
97
+ >>> detached_params = {k: v.detach() for k, v in model.named_parameters()}
98
+ >>> grad_weights = grad(compute_loss)(detached_params, x, t)
99
+ >>> grad_weights.grad_fn # None--it's not tracking gradients outside of grad
100
+
101
+ This means that the user cannot call ``grad_weight.backward()``. However, if they don't need autograd tracking
102
+ outside of the transforms, this will result in less memory usage and faster speeds.
103
+
104
+ Args:
105
+ module (torch.nn.Module): the module to call
106
+ parameters_and_buffer_dicts (Dict[str, Tensor] or tuple of Dict[str, Tensor]): the parameters that will be used in
107
+ the module call. If given a tuple of dictionaries, they must have distinct keys so that all dictionaries can
108
+ be used together
109
+ args (Any or tuple): arguments to be passed to the module call. If not a tuple, considered a single argument.
110
+ kwargs (dict): keyword arguments to be passed to the module call
111
+ tie_weights (bool, optional): If True, then parameters and buffers tied in the original model will be treated as
112
+ tied in the reparameterized version. Therefore, if True and different values are passed for the tied
113
+ parameters and buffers, it will error. If False, it will not respect the originally tied parameters and
114
+ buffers unless the values passed for both weights are the same. Default: True.
115
+ strict (bool, optional): If True, then the parameters and buffers passed in must match the parameters and
116
+ buffers in the original module. Therefore, if True and there are any missing or unexpected keys, it will
117
+ error. Default: False.
118
+
119
+ Returns:
120
+ Any: the result of calling ``module``.
121
+ """
122
+ if isinstance(parameter_and_buffer_dicts, dict):
123
+ parameters_and_buffers = parameter_and_buffer_dicts
124
+ elif isinstance(parameter_and_buffer_dicts, Sequence):
125
+ if not all(isinstance(d, dict) for d in parameter_and_buffer_dicts):
126
+ raise ValueError(
127
+ "Expected all elements of parameter_and_buffer_dicts to be dictionaries"
128
+ )
129
+ all_keys = [k for d in parameter_and_buffer_dicts for k in d.keys()]
130
+ all_keys_counter: Dict[str, int] = {}
131
+ for k in all_keys:
132
+ v = all_keys_counter.get(k, 0)
133
+ all_keys_counter[k] = v + 1
134
+ repeated_keys = [key for key, n in all_keys_counter.items() if n > 1]
135
+ if len(repeated_keys) > 0:
136
+ raise ValueError(
137
+ f"{repeated_keys} appeared in multiple dictionaries; behavior of functional call is ambiguous"
138
+ )
139
+ parameters_and_buffers = {
140
+ k: v for d in parameter_and_buffer_dicts for k, v in d.items()
141
+ }
142
+ else:
143
+ raise ValueError(
144
+ f"Expected parameter_and_buffer_dicts to be a dict, or a list/tuple of dicts, "
145
+ f"but got {type(parameter_and_buffer_dicts)}"
146
+ )
147
+
148
+ return nn.utils.stateless._functional_call(
149
+ module,
150
+ parameters_and_buffers,
151
+ args,
152
+ kwargs,
153
+ tie_weights=tie_weights,
154
+ strict=strict,
155
+ )
156
+
157
+
158
+ @exposed_in("torch.func")
159
+ def stack_module_state(
160
+ models: Union[Sequence[nn.Module], nn.ModuleList],
161
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
162
+ """stack_module_state(models) -> params, buffers
163
+
164
+ Prepares a list of torch.nn.Modules for ensembling with :func:`vmap`.
165
+
166
+ Given a list of ``M`` ``nn.Modules`` of the same class, returns two dictionaries
167
+ that stack all of their parameters and buffers together, indexed by name.
168
+ The stacked parameters are optimizable (i.e. they are new leaf nodes in the
169
+ autograd history that are unrelated to the original parameters and can be
170
+ passed directly to an optimizer).
171
+
172
+ Here's an example of how to ensemble over a very simple model:
173
+
174
+ .. code-block:: python
175
+
176
+ num_models = 5
177
+ batch_size = 64
178
+ in_features, out_features = 3, 3
179
+ models = [torch.nn.Linear(in_features, out_features) for i in range(num_models)]
180
+ data = torch.randn(batch_size, 3)
181
+
182
+ def wrapper(params, buffers, data):
183
+ return torch.func.functional_call(models[0], (params, buffers), data)
184
+
185
+ params, buffers = stack_module_state(models)
186
+ output = vmap(wrapper, (0, 0, None))(params, buffers, data)
187
+
188
+ assert output.shape == (num_models, batch_size, out_features)
189
+
190
+ When there's submodules, this follows state dict naming conventions
191
+
192
+ .. code-block:: python
193
+
194
+ import torch.nn as nn
195
+ class Foo(nn.Module):
196
+ def __init__(self, in_features, out_features):
197
+ super().__init__()
198
+ hidden = 4
199
+ self.l1 = nn.Linear(in_features, hidden)
200
+ self.l2 = nn.Linear(hidden, out_features)
201
+
202
+ def forward(self, x):
203
+ return self.l2(self.l1(x))
204
+
205
+ num_models = 5
206
+ in_features, out_features = 3, 3
207
+ models = [Foo(in_features, out_features) for i in range(num_models)]
208
+ params, buffers = stack_module_state(models)
209
+ print(list(params.keys())) # "l1.weight", "l1.bias", "l2.weight", "l2.bias"
210
+
211
+ .. warning::
212
+ All of the modules being stacked together must be the same (except for
213
+ the values of their parameters/buffers). For example, they should be in the
214
+ same mode (training vs eval).
215
+ """
216
+ if len(models) == 0:
217
+ raise RuntimeError("stack_module_state: Expected at least one model, got 0.")
218
+ if not (all(m.training for m in models) or all(not m.training for m in models)):
219
+ raise RuntimeError(
220
+ "stack_module_state: Expected all models to have the same training/eval mode."
221
+ )
222
+ model0_typ = type(models[0])
223
+ if not all(type(m) == model0_typ for m in models):
224
+ raise RuntimeError(
225
+ "stack_module_state: Expected all models to be of the same class."
226
+ )
227
+ all_params = [dict(model.named_parameters()) for model in models]
228
+ params = {
229
+ k: construct_stacked_leaf(tuple(params[k] for params in all_params), k)
230
+ for k in all_params[0]
231
+ }
232
+ all_buffers = [dict(model.named_buffers()) for model in models]
233
+ buffers = {
234
+ k: construct_stacked_leaf(tuple(buffers[k] for buffers in all_buffers), k)
235
+ for k in all_buffers[0]
236
+ }
237
+
238
+ return params, buffers
239
+
240
+
241
+ def construct_stacked_leaf(
242
+ tensors: Union[Tuple[Tensor, ...], List[Tensor]], name: str
243
+ ) -> Tensor:
244
+ all_requires_grad = all(t.requires_grad for t in tensors)
245
+ none_requires_grad = all(not t.requires_grad for t in tensors)
246
+ if not all_requires_grad and not none_requires_grad:
247
+ raise RuntimeError(
248
+ f"Expected {name} from each model to have the same .requires_grad"
249
+ )
250
+ result = torch.stack(tensors)
251
+ if all_requires_grad:
252
+ result = result.detach().requires_grad_()
253
+ return result
lib/python3.12/site-packages/torch/_functorch/fx_minifier.py ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: ignore-errors
2
+
3
+ import copy
4
+ import math
5
+ import os
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from functools import partial, wraps
9
+ from typing import Callable, List
10
+
11
+ import torch
12
+ import torch.fx as fx
13
+ from torch.hub import tqdm
14
+ from torch.multiprocessing.reductions import StorageWeakRef
15
+ from torch.utils._content_store import ContentStoreWriter
16
+
17
+ from .compile_utils import get_outputs, get_placeholders
18
+
19
+
20
+ is_tuple = object()
21
+
22
+
23
+ @dataclass
24
+ class LoadTensorMeta:
25
+ size: List[int]
26
+ stride: List[int]
27
+ dtype: torch.dtype
28
+ device: torch.device
29
+
30
+
31
+ class ConcreteProp(torch.fx.Interpreter):
32
+ def __init__(self, mod, *, writer=None, skip_offload=False):
33
+ super().__init__(mod)
34
+ self.writer = writer
35
+ self.skip_offload = skip_offload
36
+ self.seen_storages = set()
37
+
38
+ def run_node(self, n):
39
+ self.pbar.update(1)
40
+ r = super().run_node(n)
41
+ name = n.name
42
+
43
+ if isinstance(r, torch.Tensor):
44
+ if self.writer is None:
45
+ n.meta["concrete_value"] = r
46
+ else:
47
+ if StorageWeakRef(r.untyped_storage()) in self.seen_storages:
48
+ # Refuse to offload tensors which alias other live
49
+ # tensors, because this will violate operator contracts
50
+ n.meta["concrete_value"] = None
51
+ else:
52
+ if not self.skip_offload:
53
+ self.writer.write_tensor(os.path.join("eager", name), r)
54
+ n.meta["concrete_value"] = LoadTensorMeta(
55
+ r.size(), r.stride(), r.dtype, r.device
56
+ )
57
+ self.seen_storages.add(StorageWeakRef(r.untyped_storage()))
58
+ else:
59
+ n.meta["concrete_value"] = is_tuple
60
+
61
+ return r
62
+
63
+ def propagate(self, *args):
64
+ with tqdm(
65
+ desc="Saving intermediates for delta debugging",
66
+ total=len(self.module.graph.nodes),
67
+ disable=self.writer is None,
68
+ ) as pbar:
69
+ self.pbar = pbar
70
+ r = super().run(*args)
71
+ if not self.skip_offload:
72
+ pbar.set_description(
73
+ "Saved! To skip next time, run with --skip-saving-eager-intermediates"
74
+ )
75
+ return r
76
+
77
+
78
+ def is_load_tensor_node(node):
79
+ return (
80
+ node.op == "call_function"
81
+ and node.target is torch.ops.debugprims.load_tensor.default
82
+ )
83
+
84
+
85
+ # inplace modifies node/inps
86
+ def _convert_node_to_placeholder(graph, node, inps):
87
+ if node.op == "output" or node.op == "placeholder":
88
+ return False
89
+
90
+ if is_load_tensor_node(node):
91
+ return False
92
+
93
+ concrete_val = node.meta.get("concrete_value", None)
94
+
95
+ if isinstance(concrete_val, torch.Tensor):
96
+ node.op = "placeholder"
97
+ node.target = node.name
98
+ node.args = ()
99
+ node.kwargs = {}
100
+
101
+ inps.append(concrete_val)
102
+ return True
103
+
104
+ elif concrete_val is None:
105
+ return False
106
+
107
+ elif concrete_val is is_tuple:
108
+ r = False
109
+ for tuple_user in list(node.users):
110
+ r = _convert_node_to_placeholder(graph, tuple_user, inps) or r
111
+ # NB: We must not erase the node at this point, because
112
+ # we are iterating over the nodes and this would change
113
+ # the iteration order
114
+ # graph.erase_node(node)
115
+ return r
116
+
117
+ elif isinstance(concrete_val, LoadTensorMeta):
118
+ node.op = "call_function"
119
+ node.target = torch.ops.debugprims.load_tensor.default
120
+ node.args = (
121
+ os.path.join("eager", node.name),
122
+ concrete_val.size,
123
+ concrete_val.stride,
124
+ )
125
+ node.kwargs = {
126
+ "device": concrete_val.device,
127
+ "dtype": concrete_val.dtype,
128
+ }
129
+ return True
130
+
131
+ return False
132
+
133
+
134
+ def create_minified_hlo_graph(minified_fx_graph, inputs):
135
+ """
136
+ Takes minified FX graph as primary input, and ports it to HLO via StableHLO
137
+ Provides minified HLO graph as output, and archive them to local directory
138
+ """
139
+ hlo_dir = f"{os.getcwd()}/hlo_files"
140
+ os.makedirs(hlo_dir, exists_ok=True)
141
+
142
+ from torch_xla.stablehlo import save_torch_model_as_stablehlo
143
+
144
+ save_torch_model_as_stablehlo(minified_fx_graph, inputs, hlo_dir)
145
+
146
+
147
+ def dump_state(fx_g, inps):
148
+ print(
149
+ f"""
150
+ # Working Repro with {len(fx_g.graph.nodes)} nodes
151
+ inps = {[(i.shape, i.dtype, i.device.type) for i in inps]}
152
+ inps = [torch.zeros(())] + [torch.ones(shape, dtype=dtype, device=device) for (shape, dtype, device) in inps]
153
+ {fx_g.code}
154
+ """
155
+ )
156
+
157
+
158
+ def is_power_of_two(n):
159
+ if n == 0:
160
+ return False
161
+ return (n & (n - 1)) == 0
162
+
163
+
164
+ @dataclass
165
+ class ReproState:
166
+ graph: fx.Graph
167
+ inps: List[torch.Tensor]
168
+
169
+ def __post_init__(self):
170
+ ph_nodes = get_placeholders(self.graph)
171
+ assert len(ph_nodes) == len(self.inps)
172
+
173
+
174
+ def minifier(
175
+ fail_f: fx.GraphModule,
176
+ inps,
177
+ module_fails,
178
+ dump_state: Callable = dump_state,
179
+ *,
180
+ save_dir=None,
181
+ offload_to_disk=False,
182
+ skip_offload=False,
183
+ skip_sanity=False,
184
+ max_granularity=None,
185
+ ):
186
+ """
187
+ Minimizes a FX graph with given inputs, such that the resulting FX graph still returns True for module_fails.
188
+
189
+ Does 2 main strategies:
190
+ 1. Truncates suffix: Removes some suffix from the graph and sets a new output.
191
+ 2. Delta Debugging: Tries replacing half of the graph with inputs. If fails,
192
+ tries replacing quarter of the graph, etc.
193
+
194
+ >>> # xdoctest: +SKIP(failing)
195
+ >>> failing_function = fx.symbolic_trace(f)
196
+ >>> minimize(failing_function, [torch.randn(5)], lambda fx_g, inps: fx_g(*inps))
197
+
198
+ note: module_fails returns True if it fails.
199
+ """
200
+ assert isinstance(inps, (tuple, list))
201
+
202
+ failing_graph = fail_f.graph
203
+ cur_size = len(failing_graph.nodes)
204
+
205
+ if max_granularity is not None and not is_power_of_two(max_granularity):
206
+ raise RuntimeError(f"max_granularity {max_granularity} not power of two")
207
+
208
+ num_queries = 0
209
+
210
+ def deepcopy_fx_graph(fx_graph):
211
+ return fx.GraphModule(fail_f, copy.deepcopy(fx_graph)).graph
212
+
213
+ def graph_fails(graph, inps):
214
+ nonlocal num_queries
215
+ graph = copy.deepcopy(graph)
216
+ num_queries += 1
217
+ mod = fx.GraphModule(fail_f, graph)
218
+ mod.graph.lint()
219
+ return module_fails(mod, inps)
220
+
221
+ writer = None
222
+ if offload_to_disk:
223
+ writer = ContentStoreWriter(save_dir)
224
+
225
+ ConcreteProp(fail_f, writer=writer, skip_offload=skip_offload).propagate(*inps)
226
+ if not skip_sanity and not graph_fails(failing_graph, inps):
227
+ raise RuntimeError("Input graph did not fail the tester")
228
+ print(f"Started off with {cur_size} nodes", file=sys.stderr)
229
+
230
+ def _register_strategy(strategy: Callable, name: str):
231
+ @wraps(strategy)
232
+ def new_func(old_state: ReproState, granularity=1):
233
+ print(file=sys.stderr)
234
+ print(
235
+ f"Strategy: {name} (G: {granularity}) "
236
+ f"({len(old_state.graph.nodes)} nodes, {len(old_state.inps)} inputs)",
237
+ file=sys.stderr,
238
+ )
239
+ new_state = strategy(
240
+ deepcopy_fx_graph(old_state.graph), list(old_state.inps), granularity
241
+ )
242
+ if new_state is not None:
243
+ new_nodes = len(new_state.graph.nodes)
244
+ old_nodes = len(old_state.graph.nodes)
245
+ new_inps = len(new_state.inps)
246
+ old_inps = len(old_state.inps)
247
+ new_outs = len(get_outputs(new_state.graph))
248
+ old_outs = len(get_outputs(old_state.graph))
249
+ progress_made = False
250
+ if new_nodes < old_nodes:
251
+ progress_made = True
252
+ print(
253
+ f"SUCCESS: Went from {old_nodes} to {new_nodes} nodes",
254
+ file=sys.stderr,
255
+ )
256
+ if new_inps > old_inps:
257
+ progress_made = True
258
+ print(
259
+ f"SUCCESS: Went from {old_inps} to {new_inps} inputs",
260
+ file=sys.stderr,
261
+ )
262
+ if new_outs < old_outs:
263
+ progress_made = True
264
+ print(
265
+ f"SUCCESS: Went from {old_outs} to {new_outs} outputs",
266
+ file=sys.stderr,
267
+ )
268
+
269
+ if not progress_made:
270
+ raise RuntimeError("Success raised but no progress made?")
271
+
272
+ if not graph_fails(new_state.graph, new_state.inps):
273
+ print(
274
+ "WARNING: Something went wrong, not applying this minification",
275
+ file=sys.stderr,
276
+ )
277
+ return None
278
+ return new_state
279
+ else:
280
+ print(f"FAIL: {name}", file=sys.stderr)
281
+ return None
282
+
283
+ return new_func
284
+
285
+ def register_strategy(name: str):
286
+ return partial(_register_strategy, name=name)
287
+
288
+ @register_strategy("Truncate suffix")
289
+ def remove_suffix(cur_graph, cur_inps, granularity):
290
+ tested = set()
291
+ new_graph = fx.Graph()
292
+ env = {}
293
+ for idx, node in enumerate(cur_graph.nodes):
294
+ new_node = new_graph.node_copy(node, lambda x: env[x])
295
+ if node.op not in ["placeholder", "output"]:
296
+ # If idx is divisible by (granularity * 2), it would have been checked already.
297
+ if (
298
+ idx % granularity == 0
299
+ and (idx % (granularity * 2) != 0)
300
+ and idx not in tested
301
+ ):
302
+ output_node = new_graph.output((new_node,))
303
+ if len(new_graph.nodes) < len(cur_graph.nodes) and graph_fails(
304
+ new_graph, cur_inps
305
+ ):
306
+ return ReproState(new_graph, cur_inps)
307
+ else:
308
+ tested.add(idx)
309
+ new_graph.erase_node(output_node)
310
+ env[node] = new_node
311
+ return None
312
+
313
+ @register_strategy("Remove outputs")
314
+ def remove_outputs(cur_graph, cur_inps, granularity):
315
+ granularity = max(1, granularity // 2)
316
+ for idx, node in enumerate(cur_graph.nodes):
317
+ node.idx = idx
318
+ if node.op == "output":
319
+ output = node
320
+ break
321
+
322
+ if isinstance(output.args[0], fx.Node):
323
+ return None
324
+
325
+ output_args = sorted(
326
+ output.args[0], key=lambda x: x.idx if isinstance(x, fx.Node) else int(1e9)
327
+ )
328
+ if len(output_args) == 1:
329
+ return None
330
+
331
+ for idx in range(0, len(output_args), granularity):
332
+ output.args = (output_args[:idx] + output_args[idx + granularity :],)
333
+ if graph_fails(cur_graph, cur_inps):
334
+ return ReproState(cur_graph, cur_inps)
335
+ return None
336
+
337
+ def remove_unused_inputs_unchecked(cur_state: ReproState):
338
+ cur_graph = cur_state.graph
339
+ cur_inps = cur_state.inps
340
+ ph_nodes = get_placeholders(cur_graph)
341
+ assert len(ph_nodes) == len(cur_inps)
342
+
343
+ new_inps = []
344
+ for idx in range(len(ph_nodes)):
345
+ if len(ph_nodes[idx].users) == 0:
346
+ cur_graph.erase_node(ph_nodes[idx])
347
+ else:
348
+ new_inps.append(cur_inps[idx])
349
+ if len(new_inps) < len(cur_inps):
350
+ return ReproState(cur_graph, new_inps)
351
+ return None
352
+
353
+ def remove_unused_inputs_checked(cur_state: ReproState):
354
+ new_state = remove_unused_inputs_unchecked(cur_state)
355
+ if new_state is not None and graph_fails(new_state.graph, new_state.inps):
356
+ return new_state
357
+ return None
358
+
359
+ def _remove_unused_wrapper(cur_graph, cur_inps, granularity):
360
+ return remove_unused_inputs_checked(ReproState(cur_graph, cur_inps))
361
+
362
+ remove_unused_inputs = register_strategy("Remove unused inputs")(
363
+ _remove_unused_wrapper
364
+ )
365
+
366
+ @register_strategy("Eliminate dead code")
367
+ def eliminate_dead_code(cur_graph, cur_inps, granularity):
368
+ if cur_graph.eliminate_dead_code() and graph_fails(cur_graph, cur_inps):
369
+ return ReproState(cur_graph, cur_inps)
370
+ return None
371
+
372
+ def _consolidate_placeholders(cur_graph, inps):
373
+ new_graph = fx.Graph()
374
+ env = {}
375
+ seen_non_placeholder = False
376
+
377
+ # Move all placeholders to the front; also, if any load_tensor
378
+ # is at the front, convert it into an input (because it can be live
379
+ # all the time)
380
+ for node in cur_graph.nodes:
381
+ if node.op == "placeholder":
382
+ new_node = new_graph.node_copy(node, lambda x: env[x])
383
+ env[node] = new_node
384
+ elif not seen_non_placeholder and is_load_tensor_node(node):
385
+ new_node = new_graph.placeholder(node.name)
386
+ env[node] = new_node
387
+ inps.append(
388
+ torch.ops.debugprims.load_tensor.default(*node.args, **node.kwargs)
389
+ )
390
+ else:
391
+ seen_non_placeholder = True
392
+
393
+ # Move everyone else
394
+ for node in cur_graph.nodes:
395
+ if node not in env:
396
+ new_node = new_graph.node_copy(node, lambda x: env[x])
397
+ env[node] = new_node
398
+ return new_graph
399
+
400
+ @register_strategy("Delta Debugging")
401
+ def delta_debugging(cur_graph: fx.Graph, cur_inps, granularity):
402
+ num_nodes = len(cur_graph.nodes)
403
+ for start_range in range(0, num_nodes, granularity):
404
+ is_removing = False
405
+ new_graph = deepcopy_fx_graph(cur_graph)
406
+ new_inps = cur_inps[:]
407
+ end_range = min(num_nodes, start_range + granularity)
408
+ for idx in range(start_range, end_range):
409
+ new_node = list(new_graph.nodes)[idx]
410
+ if _convert_node_to_placeholder(new_graph, new_node, new_inps):
411
+ is_removing = True
412
+ if not is_removing:
413
+ continue
414
+ new_graph.eliminate_dead_code()
415
+ new_graph = _consolidate_placeholders(new_graph, new_inps)
416
+ new_state = remove_unused_inputs_unchecked(ReproState(new_graph, new_inps))
417
+ if new_state is None:
418
+ new_state = ReproState(new_graph, new_inps)
419
+ if graph_fails(new_state.graph, new_state.inps):
420
+ return ReproState(new_state.graph, new_state.inps)
421
+
422
+ return None
423
+
424
+ @register_strategy("Consolidate Inputs")
425
+ def consolidate_inputs(cur_graph, cur_inps, granularity):
426
+ old_len = len(cur_inps)
427
+ cur_graph = _consolidate_placeholders(cur_graph, cur_inps)
428
+ if len(cur_inps) > old_len and graph_fails(cur_graph, cur_inps):
429
+ return ReproState(cur_graph, cur_inps)
430
+ return None
431
+
432
+ failing_state = ReproState(failing_graph, inps)
433
+
434
+ def try_granularity(failing_state, granularity, use_non_granular):
435
+ print(f"Trying granularity {granularity}", file=sys.stderr)
436
+
437
+ strategies = []
438
+ num_nodes = len(failing_state.graph.nodes)
439
+ num_outputs = len(get_outputs(failing_state.graph))
440
+ if num_outputs > num_nodes // 2:
441
+ strategies += [remove_outputs]
442
+
443
+ if use_non_granular:
444
+ strategies += [
445
+ eliminate_dead_code,
446
+ remove_unused_inputs,
447
+ consolidate_inputs,
448
+ ]
449
+
450
+ strategies += [remove_suffix, delta_debugging]
451
+
452
+ for strategy in strategies:
453
+ new_state = strategy(failing_state, granularity)
454
+ if new_state is not None:
455
+ return new_state
456
+ return None
457
+
458
+ while True:
459
+ dump_state(fx.GraphModule(fail_f, failing_state.graph), failing_state.inps)
460
+ granularity = int(2 ** (math.floor(math.log2(len(failing_state.graph.nodes)))))
461
+ if max_granularity is not None:
462
+ granularity = min(max_granularity, granularity)
463
+ new_state = try_granularity(failing_state, granularity, use_non_granular=True)
464
+ if new_state is not None:
465
+ failing_state = new_state
466
+ continue
467
+
468
+ granularity //= 2
469
+ has_progress = False
470
+ while granularity >= 1:
471
+ new_state = try_granularity(
472
+ failing_state, granularity, use_non_granular=False
473
+ )
474
+ if new_state is not None:
475
+ failing_state = new_state
476
+ has_progress = True
477
+ break
478
+ granularity //= 2
479
+ if has_progress:
480
+ continue
481
+
482
+ new_state = remove_outputs(failing_state, 1)
483
+ if new_state is not None:
484
+ failing_state = new_state
485
+ continue
486
+
487
+ break
488
+
489
+ if not graph_fails(failing_state.graph, failing_state.inps):
490
+ raise RuntimeError("Uh oh, something went wrong :( Final graph is not failing")
491
+
492
+ print(f"Made {num_queries} queries", file=sys.stderr)
493
+ failing_fx = fx.GraphModule(fail_f, failing_state.graph)
494
+
495
+ # If XLA debugging environment is enabled, create minified HLO graph as well
496
+ if "XLA_HLO_DEBUG" in os.environ:
497
+ create_minified_hlo_graph(failing_fx, failing_state.inps)
498
+
499
+ dump_state(failing_fx, failing_state.inps)
500
+ print("Wrote minimal repro out to repro.py", file=sys.stderr)
501
+ return failing_fx, failing_state.inps
lib/python3.12/site-packages/torch/_functorch/make_functional.py ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ # Copyright (c) Facebook, Inc. and its affiliates.
3
+ # All rights reserved.
4
+ #
5
+ # This source code is licensed under the BSD-style license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ import copy
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Dict,
13
+ Iterable,
14
+ List,
15
+ NoReturn,
16
+ Sequence,
17
+ Tuple,
18
+ Type,
19
+ Union,
20
+ )
21
+
22
+ import torch
23
+ import torch.nn as nn
24
+ from torch import Tensor
25
+ from torch.nn.utils._named_member_accessor import NamedMemberAccessor
26
+
27
+
28
+ # Utilities to make nn.Module "functional"
29
+ # In particular the goal is to be able to provide a function that takes as input
30
+ # the parameters and evaluate the nn.Module using fixed inputs.
31
+
32
+
33
+ def raise_parameter_tying_error() -> NoReturn:
34
+ raise RuntimeError(
35
+ "make_functional(module): we don't yet support models that "
36
+ "do parameter tying (also sometimes known as weight sharing). "
37
+ "Please try to rewrite your model by replacing all instances of the "
38
+ "tied parameter with another and/or comment your support in "
39
+ "https://github.com/pytorch/functorch/issues/446"
40
+ )
41
+
42
+
43
+ def create_names_map(
44
+ named_params: Union[Dict[str, Tensor], Iterable[Tuple[str, Tensor]]],
45
+ tied_named_params: Union[Dict[str, Tensor], Iterable[Tuple[str, Tensor]]],
46
+ ) -> Dict[str, List[str]]:
47
+ """
48
+ named_params is a dictionary of tensors: {'A': A, 'B': B}
49
+ tied_named_params is another dictionary of tensors {'A': A, 'B': B, 'B_tied': B}
50
+ with potentially tied (or 'duplicated') tensors
51
+
52
+ This function creates a mapping from the names in named_params to the
53
+ names in tied_named_params: {'A': ['A'], 'B': ['B', 'B_tied']}.
54
+ """
55
+ named_params = dict(named_params)
56
+ tied_named_params = dict(tied_named_params)
57
+
58
+ tensors_dict_keys = set(named_params.keys())
59
+ tied_tensors_dict_keys = set(tied_named_params.keys())
60
+ assert tensors_dict_keys.issubset(tied_tensors_dict_keys)
61
+
62
+ tensor_to_mapping: Dict[Tensor, Tuple[str, List[str]]] = {}
63
+ for key, tensor in named_params.items():
64
+ tensor_to_mapping[tensor] = (key, [])
65
+ for key, tensor in tied_named_params.items():
66
+ assert tensor in tensor_to_mapping
67
+ tensor_to_mapping[tensor][1].append(key)
68
+ return dict(tensor_to_mapping.values())
69
+
70
+
71
+ def _extract_members(
72
+ mod: nn.Module,
73
+ named_members: Callable[..., Iterable[Tuple[str, Tensor]]],
74
+ subclass: Callable[[Tensor], Tensor],
75
+ ) -> Tuple[Tuple[Tensor, ...], Tuple[str, ...], Dict[str, List[str]]]:
76
+ all_named_members = tuple(named_members(remove_duplicate=False))
77
+ unique_named_members = tuple(named_members(remove_duplicate=True))
78
+ names_map = create_names_map(unique_named_members, all_named_members)
79
+
80
+ # Remove all the members in the model
81
+ memo = {}
82
+ accessor = NamedMemberAccessor(mod)
83
+ for name, p in all_named_members:
84
+ if p not in memo:
85
+ memo[p] = subclass(torch.empty_like(p, device="meta"))
86
+ replacement = memo[p]
87
+ accessor.set_tensor(name, replacement)
88
+
89
+ if len(unique_named_members) == 0:
90
+ names, params = (), ()
91
+ else:
92
+ names, params = zip(*unique_named_members) # type: ignore[assignment]
93
+ return params, names, names_map
94
+
95
+
96
+ def extract_weights(
97
+ mod: nn.Module,
98
+ ) -> Tuple[Tuple[Tensor, ...], Tuple[str, ...], Dict[str, List[str]]]:
99
+ """
100
+ This function removes all the Parameters from the model and
101
+ return them as a tuple as well as their original attribute names.
102
+ The weights must be re-loaded with `load_weights` before the model
103
+ can be used again.
104
+ Note that this function modifies the model in place and after this
105
+ call, mod.parameters() will be empty.
106
+ """
107
+ return _extract_members(mod, mod.named_parameters, nn.Parameter)
108
+
109
+
110
+ def extract_buffers(
111
+ mod: nn.Module,
112
+ ) -> Tuple[Tuple[Tensor, ...], Tuple[str, ...], Dict[str, List[str]]]:
113
+ return _extract_members(mod, mod.named_buffers, lambda x: x)
114
+
115
+
116
+ def load_weights(
117
+ mod: nn.Module,
118
+ names: Sequence[str],
119
+ params: Sequence[Tensor],
120
+ as_params: bool = False,
121
+ ) -> None:
122
+ """
123
+ Reload a set of weights so that `mod` can be used again to perform a forward pass.
124
+ Note that the `params` are regular Tensors (that can have history) and so are left
125
+ as Tensors. This means that mod.parameters() will still be empty after this call.
126
+ """
127
+ accessor = NamedMemberAccessor(mod)
128
+ if as_params:
129
+ params = [nn.Parameter(p) for p in params]
130
+ accessor.set_tensors(names, params)
131
+
132
+
133
+ def _swap_state(
134
+ mod: nn.Module, names_map: Dict[str, List[str]], elems: Iterable[Tensor]
135
+ ) -> List[Tensor]:
136
+ result: List[Tensor] = []
137
+ accessor = NamedMemberAccessor(mod)
138
+ for (_, attr_names), elem in zip(names_map.items(), elems):
139
+ for i, attr_name in enumerate(attr_names):
140
+ if i == 0:
141
+ result.append(accessor.swap_tensor(attr_name, elem))
142
+ else:
143
+ accessor.set_tensor(attr_name, elem)
144
+ return result
145
+
146
+
147
+ def load_buffers(
148
+ mod: nn.Module,
149
+ names: Sequence[str],
150
+ buffers: Sequence[Tensor],
151
+ as_params: bool = False,
152
+ ) -> None:
153
+ accessor = NamedMemberAccessor(mod)
154
+ accessor.set_tensors(names, buffers)
155
+
156
+
157
+ def load_state(
158
+ model: nn.Module,
159
+ weights: Sequence[Tensor],
160
+ weight_names: Sequence[str],
161
+ buffers: Sequence[Tensor] = (),
162
+ buffer_names: Sequence[str] = (),
163
+ ) -> nn.Module:
164
+ """load_state(model, weights, weight_names, buffers=(), buffer_names=()) -> model
165
+
166
+ load_state takes `weights` and `buffers` and assigns them to the model.
167
+ This is the inverse operation of `make_functional_deprecated_v1`.
168
+ """
169
+ assert len(weight_names) == len(weights)
170
+ load_weights(model, weight_names, weights)
171
+ if len(buffers) > 0:
172
+ assert len(buffer_names) == len(buffers)
173
+ load_buffers(model, buffer_names, buffers)
174
+ return model
175
+
176
+
177
+ def make_functional_deprecated_v1(model: nn.Module):
178
+ """make_functional_deprecated_v1(model) -> weights, func, weight_names
179
+
180
+ Given an nn.Module, make_functional_deprecated_v1 extracts the state (weights)
181
+ and returns a functional version of the model, `func`. This makes
182
+ it so that it is possible use transforms over the parameters of
183
+ `model`.
184
+
185
+ `func` can be invoked as follows:
186
+ ```
187
+ x = torch.randn(4, 3)
188
+ model = nn.Linear(3, 3)
189
+ weights, func, _ = make_functional_deprecated_v1(model)
190
+ func(weights, (x,))
191
+ ```
192
+
193
+ And here is an example of applying the grad transform:
194
+ ```
195
+ x = torch.randn(4, 3)
196
+ model = nn.Linear(3, 3)
197
+ weights, _, func = make_functional_deprecated_v1(model)
198
+ grad_weights = grad(func)(weights, (x,))
199
+ ```
200
+
201
+ To put the state back into a model, use `load_state`.
202
+ """
203
+ buffers = list(model.buffers())
204
+ if len(buffers) > 0:
205
+ raise RuntimeError(
206
+ "make_functional_deprecated_v1(model): `model` has buffers. Please use "
207
+ "make_functional_with_buffers_deprecated_v1(model) instead."
208
+ )
209
+ weights, descriptors, _ = extract_weights(model)
210
+
211
+ def fun(weights, data):
212
+ mutable_model = copy.deepcopy(model)
213
+ load_weights(mutable_model, descriptors, weights)
214
+ return mutable_model(*data)
215
+
216
+ return weights, fun, descriptors
217
+
218
+
219
+ def make_functional_with_buffers_deprecated_v1(model: nn.Module):
220
+ """make_functional_with_buffers_deprecated_v1(model) -> weights, buffers, func, weight_names, buffer_names
221
+
222
+ Given an nn.Module, make_functional_with_buffers_deprecated_v1 extracts the state (weights and buffers)
223
+ and returns a functional version of the model, `func`.
224
+
225
+ `func` can be invoked as follows:
226
+ ```
227
+ x = torch.randn(4, 3)
228
+ model = nn.Linear(3, 3)
229
+ weights, buffers, func, _, _ = make_functional_with_buffers_deprecated_v1(model)
230
+ func(weights, buffers, (x,))
231
+ ```
232
+
233
+ And here is an example of applying the grad transform:
234
+ ```
235
+ x = torch.randn(4, 3)
236
+ model = nn.Linear(3, 3)
237
+ weights, buffers, func, _, _ = make_functional_with_buffers_deprecated_v1(model)
238
+ func(weights, buffers, (x,))
239
+ grad_weights = grad(func)(weights, buffers, (x,))
240
+ ```
241
+
242
+ To put the state back into a model, use `load_state`.
243
+ """
244
+ weights, weight_descriptors, _ = extract_weights(model)
245
+ buffers, buf_descriptors, _ = extract_buffers(model)
246
+
247
+ def fun(weights, buffers, data):
248
+ mutable_model = copy.deepcopy(model)
249
+ load_weights(mutable_model, weight_descriptors, weights)
250
+ load_buffers(mutable_model, buf_descriptors, buffers)
251
+ return mutable_model(*data)
252
+
253
+ return weights, buffers, fun, weight_descriptors, buf_descriptors
254
+
255
+
256
+ class FunctionalModuleWithBuffers(nn.Module):
257
+ """
258
+ This is the callable object returned by :func:`make_functional_with_buffers`.
259
+ """
260
+
261
+ def __init__(
262
+ self,
263
+ stateless_model: nn.Module,
264
+ param_names: Tuple[str, ...],
265
+ buffer_names: Tuple[str, ...],
266
+ param_names_map: Dict[str, List[str]],
267
+ buffer_names_map: Dict[str, List[str]],
268
+ ) -> None:
269
+ super().__init__()
270
+ self.stateless_model = stateless_model
271
+ self.param_names = param_names
272
+ self.buffer_names = buffer_names
273
+
274
+ self.all_names_map = dict(param_names_map)
275
+ self.all_names_map.update(buffer_names_map)
276
+
277
+ @staticmethod
278
+ def _create_from(
279
+ model: nn.Module, disable_autograd_tracking: bool = False
280
+ ) -> Tuple["FunctionalModuleWithBuffers", Tuple[Tensor, ...], Tuple[Tensor, ...]]:
281
+ # TODO: We don't need to copy the model to create a stateless copy
282
+ model_copy = copy.deepcopy(model)
283
+ params, param_names, param_names_map = extract_weights(model_copy)
284
+ buffers, buffer_names, buffer_names_map = extract_buffers(model_copy)
285
+ if disable_autograd_tracking:
286
+ for param in params:
287
+ param.requires_grad_(False)
288
+ return (
289
+ FunctionalModuleWithBuffers(
290
+ model_copy, param_names, buffer_names, param_names_map, buffer_names_map
291
+ ),
292
+ params,
293
+ buffers,
294
+ )
295
+
296
+ def forward(
297
+ self, params: Iterable[Tensor], buffers: Iterable[Tensor], *args, **kwargs
298
+ ) -> Any:
299
+ # Temporarily load the state back onto self.stateless_model
300
+ old_state = _swap_state(
301
+ self.stateless_model,
302
+ self.all_names_map,
303
+ tuple(params) + tuple(buffers),
304
+ )
305
+ try:
306
+ return self.stateless_model(*args, **kwargs)
307
+ finally:
308
+ # Remove the loaded state on self.stateless_model
309
+ _swap_state(self.stateless_model, self.all_names_map, old_state)
310
+
311
+
312
+ class FunctionalModule(nn.Module):
313
+ """
314
+ This is the callable object returned by :func:`make_functional`.
315
+ """
316
+
317
+ def __init__(
318
+ self,
319
+ stateless_model: nn.Module,
320
+ param_names: Tuple[str, ...],
321
+ names_map: Dict[str, List[str]],
322
+ ) -> None:
323
+ super().__init__()
324
+ self.stateless_model = stateless_model
325
+ self.param_names = param_names
326
+ self.names_map = names_map
327
+
328
+ @staticmethod
329
+ def _create_from(
330
+ model: nn.Module, disable_autograd_tracking: bool = False
331
+ ) -> Tuple["FunctionalModule", Tuple[Tensor, ...]]:
332
+ # TODO: We don't need to copy the model to create a stateless copy
333
+ model_copy = copy.deepcopy(model)
334
+ params, param_names, names_map = extract_weights(model_copy)
335
+ if disable_autograd_tracking:
336
+ for param in params:
337
+ param.requires_grad_(False)
338
+ return FunctionalModule(model_copy, param_names, names_map), params
339
+
340
+ def forward(self, params: Iterable[Tensor], *args, **kwargs) -> Any:
341
+ # Temporarily load the state back onto self.stateless_model
342
+ old_state = _swap_state(self.stateless_model, self.names_map, params)
343
+ try:
344
+ return self.stateless_model(*args, **kwargs)
345
+ finally:
346
+ # Remove the loaded state on self.stateless_model
347
+ _swap_state(self.stateless_model, self.names_map, old_state)
348
+
349
+
350
+ def make_functional(
351
+ model: nn.Module, disable_autograd_tracking: bool = False
352
+ ) -> Tuple[FunctionalModule, Tuple[Tensor, ...]]:
353
+ """make_functional(model, disable_autograd_tracking=False) -> func, params
354
+
355
+ Given a ``torch.nn.Module``, :func:`make_functional` extracts the state
356
+ (params) and returns a functional version of the model, ``func``. This
357
+ makes it so that it is possible use transforms over the parameters of
358
+ ``model``.
359
+
360
+ ``func`` can be invoked as follows:
361
+
362
+ .. code-block:: python
363
+
364
+ import torch
365
+ import torch.nn as nn
366
+ from functorch import make_functional
367
+
368
+ x = torch.randn(4, 3)
369
+ model = nn.Linear(3, 3)
370
+ func, params = make_functional(model)
371
+ func(params, x)
372
+
373
+ And here is an example of applying the grad transform over the parameters
374
+ of a model.
375
+
376
+ .. code-block:: python
377
+
378
+ import torch
379
+ import torch.nn as nn
380
+ from functorch import make_functional, grad
381
+
382
+ x = torch.randn(4, 3)
383
+ t = torch.randn(4, 3)
384
+ model = nn.Linear(3, 3)
385
+ func, params = make_functional(model)
386
+
387
+ def compute_loss(params, x, t):
388
+ y = func(params, x)
389
+ return nn.functional.mse_loss(y, t)
390
+
391
+ grad_weights = grad(compute_loss)(params, x, t)
392
+
393
+ If the model has any buffers, please use :func:`make_functional_with_buffers` instead.
394
+
395
+ Args:
396
+ model (torch.nn.Module): Input model.
397
+ disable_autograd_tracking (bool): Flag to disable gradients tracking for output parameters.
398
+ The returned params are unrelated to the set of params from the original model. If False (default),
399
+ the params will have ``requires_grad=True`` on them (aka they will be trackable with regular
400
+ PyTorch autograd), matching the requires_grad-ness of the params from the original model.
401
+ Otherwise, the returned params will have ``requires_grad=False``. Default, False.
402
+ If you plan on using regular PyTorch autograd (e.g., if you want to call ``.backward()`` or
403
+ ``torch.autograd.grad()``, then set ``disable_autograd_tracking=False``.
404
+ Otherwise, if you're only planning on using functorch's gradient transforms,
405
+ then please set ``disable_autograd_tracking=True`` to avoid unnecessarily tracking
406
+ history with PyTorch autograd.
407
+
408
+ """
409
+ buffers = list(model.buffers())
410
+ if len(buffers) > 0:
411
+ raise RuntimeError(
412
+ "make_functional(model): `model` has buffers. Please use "
413
+ "make_functional_with_buffers(model) instead."
414
+ )
415
+ return FunctionalModule._create_from(
416
+ model, disable_autograd_tracking=disable_autograd_tracking
417
+ )
418
+
419
+
420
+ def make_functional_with_buffers(
421
+ model: nn.Module, disable_autograd_tracking: bool = False
422
+ ) -> Tuple[FunctionalModuleWithBuffers, Tuple[Tensor, ...], Tuple[Tensor, ...]]:
423
+ """make_functional_with_buffers(model, disable_autograd_tracking=False) -> func, params, buffers
424
+
425
+ Given a ``torch.nn.Module``, make_functional_with_buffers extracts the
426
+ state (params and buffers) and returns a functional version of the model
427
+ ``func`` that can be invoked like a function.
428
+
429
+ ``func`` can be invoked as follows:
430
+
431
+ .. code-block:: python
432
+
433
+ import torch
434
+ import torch.nn as nn
435
+ from functorch import make_functional_with_buffers
436
+
437
+ x = torch.randn(4, 3)
438
+ model = nn.Linear(3, 3)
439
+ func, params, buffers = make_functional_with_buffers(model)
440
+ func(params, buffers, x)
441
+
442
+ And here is an example of applying the grad transform over the parameters
443
+ of a model:
444
+
445
+ .. code-block:: python
446
+
447
+ import torch
448
+ import torch.nn as nn
449
+ from functorch import make_functional_with_buffers, grad
450
+
451
+ x = torch.randn(4, 3)
452
+ t = torch.randn(4, 3)
453
+ model = nn.Linear(3, 3)
454
+ func, params, buffers = make_functional_with_buffers(model)
455
+
456
+ def compute_loss(params, buffers, x, t):
457
+ y = func(params, buffers, x)
458
+ return nn.functional.mse_loss(y, t)
459
+
460
+ grad_weights = grad(compute_loss)(params, buffers, x, t)
461
+
462
+ Args:
463
+ model (torch.nn.Module): Input model.
464
+ disable_autograd_tracking (bool): Flag to disable gradients tracking for output parameters.
465
+ The returned params are unrelated to the set of params from the original model. If False (default),
466
+ the params will have ``requires_grad=True`` on them (aka they will be trackable with regular
467
+ PyTorch autograd), matching the requires_grad-ness of the params from the original model.
468
+ Otherwise, the returned params will have ``requires_grad=False``. Default, False.
469
+ If you plan on using regular PyTorch autograd (e.g., if you want to call ``.backward()`` or
470
+ ``torch.autograd.grad()``, then set ``disable_autograd_tracking=False``.
471
+ Otherwise, if you're only planning on using functorch's gradient transforms,
472
+ then please set ``disable_autograd_tracking=True`` to avoid unnecessarily tracking
473
+ history with PyTorch autograd.
474
+
475
+ """
476
+ return FunctionalModuleWithBuffers._create_from(
477
+ model, disable_autograd_tracking=disable_autograd_tracking
478
+ )
479
+
480
+
481
+ def transpose_stack(
482
+ tuple_of_tuple_of_tensors: Tuple[Tuple[Tensor, ...], ...]
483
+ ) -> Tuple[Tensor, ...]:
484
+ tuple_of_tuple_of_tensors = tuple(zip(*tuple_of_tuple_of_tensors))
485
+ results = tuple(
486
+ torch.stack(shards).detach() for shards in tuple_of_tuple_of_tensors
487
+ )
488
+ return results
489
+
490
+
491
+ def combine_state_for_ensemble(
492
+ models: Sequence[nn.Module],
493
+ ) -> Tuple[FunctionalModuleWithBuffers, Tuple[Tensor, ...], Tuple[Tensor, ...]]:
494
+ """combine_state_for_ensemble(models) -> func, params, buffers
495
+
496
+ Prepares a list of torch.nn.Modules for ensembling with :func:`vmap`.
497
+
498
+ Given a list of ``M`` ``nn.Modules`` of the same class, stacks all of their
499
+ parameters and buffers together to make ``params`` and ``buffers``.
500
+ Each parameter and buffer in the result will have an additional dimension
501
+ of size ``M``.
502
+
503
+ :func:`combine_state_for_ensemble` also returns ``func``, a functional
504
+ version of one of the models in :attr:`models`. One cannot directly run
505
+ ``func(params, buffers, *args, **kwargs)`` directly, you probably want to
506
+ use ``vmap(func, ...)(params, buffers, *args, **kwargs)``
507
+
508
+ Here's an example of how to ensemble over a very simple model:
509
+
510
+ .. code-block:: python
511
+
512
+ num_models = 5
513
+ batch_size = 64
514
+ in_features, out_features = 3, 3
515
+ models = [torch.nn.Linear(in_features, out_features) for i in range(num_models)]
516
+ data = torch.randn(batch_size, 3)
517
+
518
+ fmodel, params, buffers = combine_state_for_ensemble(models)
519
+ output = vmap(fmodel, (0, 0, None))(params, buffers, data)
520
+
521
+ assert output.shape == (num_models, batch_size, out_features)
522
+
523
+ .. warning::
524
+ All of the modules being stacked together must be the same (except for
525
+ the values of their parameters/buffers). For example, they should be in the
526
+ same mode (training vs eval).
527
+
528
+ This API is subject to change -- we're investigating better ways to
529
+ create ensembles and would love your feedback how to improve this.
530
+ """
531
+ if len(models) == 0:
532
+ raise RuntimeError(
533
+ "combine_state_for_ensemble: Expected at least one model, got 0."
534
+ )
535
+ if not (all(m.training for m in models) or all(not m.training for m in models)):
536
+ raise RuntimeError(
537
+ "combine_state_for_ensemble: Expected all models to "
538
+ "have the same training/eval mode."
539
+ )
540
+ model0_typ = type(models[0])
541
+ if not all(type(m) == model0_typ for m in models):
542
+ raise RuntimeError(
543
+ "combine_state_for_ensemble: Expected all models to be of the same class."
544
+ )
545
+ funcs, params, buffers = zip(
546
+ *[make_functional_with_buffers(model) for model in models]
547
+ )
548
+ params = transpose_stack(params)
549
+ buffers = transpose_stack(buffers)
550
+ return funcs[0], params, buffers
551
+
552
+
553
+ def functional_init(
554
+ model_class: Type[nn.Module],
555
+ ensemble_shape: Union[Tuple[()], Tuple[int]] = (),
556
+ device: torch.types.Device = "cpu",
557
+ ):
558
+ def wrapped(*args, **kwargs):
559
+ if len(ensemble_shape) >= 2:
560
+ raise ValueError("NYI: ensemble_shape with more than 1 element")
561
+ if len(ensemble_shape) == 0:
562
+ model = model_class(*args, **kwargs).to(device)
563
+ return make_functional_deprecated_v1(model)
564
+ num_models = ensemble_shape[0] # type: ignore[misc]
565
+ if num_models <= 0:
566
+ raise ValueError(f"num_models {num_models} should be > 0")
567
+ # NB: Not very efficient, more of a POC
568
+ models = tuple(
569
+ model_class(*args, **kwargs).to(device) for _ in range(num_models)
570
+ )
571
+ _, fn, names = make_functional_deprecated_v1(model_class(*args, **kwargs))
572
+ weights = tuple(make_functional_deprecated_v1(model)[0] for model in models)
573
+ weights = tuple(zip(*weights))
574
+ weights = tuple(torch.stack(shards).detach() for shards in weights)
575
+ return weights, fn, names
576
+
577
+ return wrapped
578
+
579
+
580
+ def functional_init_with_buffers(
581
+ model_class: Type[nn.Module],
582
+ ensemble_shape: Union[Tuple[()], Tuple[int]] = (),
583
+ device: torch.types.Device = "cpu",
584
+ ):
585
+ def wrapped(*args, **kwargs):
586
+ if len(ensemble_shape) >= 2:
587
+ raise ValueError("NYI: ensemble_shape with more than 1 element")
588
+ if len(ensemble_shape) == 0:
589
+ model = model_class(*args, **kwargs).to(device)
590
+ return make_functional_deprecated_v1(model)
591
+ num_models = ensemble_shape[0] # type: ignore[misc]
592
+ if num_models <= 0:
593
+ raise ValueError(f"num_models {num_models} should be > 0")
594
+ # NB: Not very efficient, more of a POC
595
+ models = tuple(
596
+ model_class(*args, **kwargs).to(device) for _ in range(num_models)
597
+ )
598
+ (
599
+ _,
600
+ _,
601
+ fn,
602
+ weight_names,
603
+ buffer_names,
604
+ ) = make_functional_with_buffers_deprecated_v1(model_class(*args, **kwargs))
605
+ weights, buffers = zip(
606
+ *tuple(
607
+ make_functional_with_buffers_deprecated_v1(model)[:2]
608
+ for model in models
609
+ )
610
+ )
611
+ weights = tuple(zip(*weights))
612
+ weights = tuple(torch.stack(shards).detach() for shards in weights)
613
+ buffers = tuple(zip(*buffers))
614
+ buffers = tuple(torch.stack(shards).detach() for shards in buffers)
615
+ return weights, buffers, fn, weight_names, buffer_names
616
+
617
+ return wrapped
lib/python3.12/site-packages/torch/_functorch/partitioners.py ADDED
@@ -0,0 +1,1911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ import copy
3
+ import functools
4
+ import heapq
5
+ import itertools
6
+ import logging
7
+ import math
8
+ import operator
9
+ import os
10
+ from collections import defaultdict
11
+ from dataclasses import dataclass, replace
12
+ from typing import Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING, Union
13
+
14
+ import torch
15
+ import torch._inductor.inductor_prims
16
+ import torch.fx as fx
17
+ import torch.utils._pytree as pytree
18
+ from torch.fx.experimental._backward_state import BackwardState
19
+ from torch.fx.experimental.proxy_tensor import is_sym_node, py_sym_types
20
+ from torch.fx.experimental.sym_node import magic_methods, method_to_operator
21
+ from torch.fx.experimental.symbolic_shapes import (
22
+ find_symbol_binding_fx_nodes,
23
+ free_symbols,
24
+ hint_int,
25
+ is_symbol_binding_fx_node,
26
+ )
27
+ from torch.fx.passes import graph_drawer
28
+ from torch.utils.checkpoint import CheckpointPolicy
29
+
30
+ from . import config
31
+ from ._activation_checkpointing.knapsack import (
32
+ dp_knapsack,
33
+ greedy_knapsack,
34
+ ilp_knapsack,
35
+ )
36
+ from ._aot_autograd.logging_utils import get_aot_graph_name
37
+ from ._aot_autograd.utils import is_with_effects
38
+ from .compile_utils import fx_graph_cse, get_aten_target
39
+
40
+
41
+ if TYPE_CHECKING:
42
+ import sympy
43
+
44
+
45
+ AOT_PARTITIONER_DEBUG: bool = config.debug_partitioner
46
+ log: logging.Logger = logging.getLogger(__name__)
47
+
48
+ aten = torch.ops.aten
49
+ prims = torch.ops.prims
50
+
51
+
52
+ @dataclass
53
+ class OpTypes:
54
+ """Class for keeping track of different operator categories"""
55
+
56
+ fusible_ops: Set[Callable]
57
+ compute_intensive_ops: Set[Callable]
58
+ random_ops: Set[Callable]
59
+ view_ops: Set[Callable]
60
+ recomputable_ops: Set[Callable]
61
+
62
+ def is_fusible(self, node: fx.Node):
63
+ return get_aten_target(node) in self.fusible_ops
64
+
65
+ def is_compute_intensive(self, node: fx.Node):
66
+ return get_aten_target(node) in self.compute_intensive_ops
67
+
68
+ def is_random(self, node: fx.Node):
69
+ return get_aten_target(node) in self.random_ops
70
+
71
+ def is_view(self, node: fx.Node):
72
+ return get_aten_target(node) in self.view_ops
73
+
74
+ def is_recomputable(self, node: fx.Node):
75
+ return get_aten_target(node) in self.recomputable_ops
76
+
77
+
78
+ @dataclass
79
+ class NodeInfo:
80
+ # Be careful about iterating over these explicitly, as their order may not
81
+ # be deterministic
82
+ inputs: List[fx.Node]
83
+ _required_fw_nodes: Set[fx.Node]
84
+ required_bw_nodes: Set[fx.Node]
85
+ unclaimed_nodes: Set[fx.Node]
86
+ fw_order: Dict[fx.Node, int]
87
+
88
+ @functools.cached_property
89
+ def required_fw_nodes(self) -> List[fx.Node]:
90
+ return sorted(
91
+ (n for n in self._required_fw_nodes), key=lambda n: self.fw_order[n]
92
+ )
93
+
94
+ def is_required_fw(self, n: fx.Node) -> bool:
95
+ return n in self._required_fw_nodes
96
+
97
+ def is_required_bw(self, n: fx.Node) -> bool:
98
+ return n in self.required_bw_nodes
99
+
100
+ def is_unclaimed(self, n: fx.Node) -> bool:
101
+ return n in self.unclaimed_nodes
102
+
103
+ def get_fw_order(self, n: fx.Node) -> int:
104
+ assert n in self._required_fw_nodes, f"Node {n} not in fw nodes!"
105
+ return self.fw_order[n]
106
+
107
+
108
+ @dataclass
109
+ class MinCutOptions:
110
+ ban_if_used_far_apart: bool
111
+ ban_if_long_fusible_chains: bool
112
+ ban_if_materialized_backward: bool
113
+ ban_if_not_in_allowlist: bool
114
+ ban_if_reduction: bool
115
+
116
+
117
+ def must_recompute(node: fx.Node) -> bool:
118
+ return node.meta.get("recompute", None) in [
119
+ CheckpointPolicy.MUST_RECOMPUTE,
120
+ CheckpointPolicy.PREFER_RECOMPUTE,
121
+ ]
122
+
123
+
124
+ def has_recomputable_ops(fx_g: fx.GraphModule) -> bool:
125
+ found = False
126
+ for node in fx_g.graph.nodes:
127
+ if must_recompute(node):
128
+ return True
129
+ return False
130
+
131
+
132
+ def has_recomputable_rng_ops(fx_g: fx.GraphModule) -> bool:
133
+ for node in fx_g.graph.nodes:
134
+ if (
135
+ must_recompute(node)
136
+ and hasattr(node.target, "tags")
137
+ and torch.Tag.nondeterministic_seeded in node.target.tags
138
+ ):
139
+ return True
140
+ return False
141
+
142
+
143
+ def sym_node_size(node: fx.Node) -> int:
144
+ if isinstance(node.meta["val"], (torch.SymInt, torch.SymBool)):
145
+ return 1
146
+ assert isinstance(node.meta["val"], torch.SymFloat)
147
+ return 4
148
+
149
+
150
+ class InvalidNodeBase:
151
+ def __repr__(self):
152
+ return "Invalid Node"
153
+
154
+
155
+ InvalidNode = InvalidNodeBase()
156
+
157
+
158
+ def _extract_graph_with_inputs_outputs(
159
+ joint_graph: fx.Graph,
160
+ inputs: List[fx.Node],
161
+ outputs: List[fx.Node],
162
+ subgraph: Optional[str] = None,
163
+ ) -> fx.Graph:
164
+ """
165
+ Given a graph, extracts out a subgraph that takes the specified nodes as
166
+ inputs and returns the specified outputs.
167
+
168
+ This includes specifying non-placeholder nodes as inputs.
169
+
170
+ The general strategy is to initialize all inputs with proxies as we
171
+ encounter them, and trace through the graph, only keeping values which take
172
+ in valid proxies. Then, all dead code is eliminated.
173
+ """
174
+ new_graph = fx.Graph()
175
+ env = {}
176
+
177
+ # Add new placeholder nodes in the order specified by the inputs
178
+ for node in inputs:
179
+ new_node = new_graph.placeholder(node.name)
180
+ # Can't use node_copy here as we may be turning previous call_function into placeholders
181
+ new_node.meta = node.meta
182
+ env[node] = new_node
183
+
184
+ for node in joint_graph.nodes:
185
+ if _must_be_in_backward(node) and subgraph != "backward":
186
+ env[node] = InvalidNode # type: ignore[assignment]
187
+ continue
188
+
189
+ if node in env:
190
+ # Node must be one of our inputs. (Any member of env which wasn't an
191
+ # input to start must have been created by this loop and won't be in
192
+ # joint_graph.nodes).
193
+ continue
194
+ elif node.op == "placeholder":
195
+ env[node] = InvalidNode # type: ignore[assignment]
196
+ elif node.op == "call_function":
197
+ all_args = pytree.arg_tree_leaves(*node.args, **node.kwargs)
198
+ all_args = [
199
+ isinstance(env[x], InvalidNodeBase)
200
+ for x in all_args
201
+ if isinstance(x, fx.Node)
202
+ ]
203
+ if any(all_args):
204
+ env[node] = InvalidNode # type: ignore[assignment]
205
+ continue
206
+ env[node] = new_graph.node_copy(node, lambda x: env[x])
207
+ elif node.op == "get_attr":
208
+ env[node] = new_graph.node_copy(node, lambda x: env[x])
209
+ elif node.op == "output":
210
+ pass
211
+ output_values = []
212
+ for x in outputs:
213
+ if isinstance(x, fx.Node):
214
+ if x not in env:
215
+ raise RuntimeError(f"Node {x} couldn't be found in env")
216
+ assert not isinstance(
217
+ env[x], InvalidNodeBase
218
+ ), f"Node {x} was invalid, but is output"
219
+ output_values.append(env[x])
220
+ else:
221
+ output_values.append(x)
222
+ new_graph.output(tuple(output_values))
223
+
224
+ new_graph.eliminate_dead_code()
225
+ new_graph.lint()
226
+ return new_graph
227
+
228
+
229
+ def _is_primal(node: fx.Node) -> bool:
230
+ return (
231
+ node.op == "placeholder"
232
+ and "tangents" not in str(node.target)
233
+ and not _is_bwd_seed_offset(node)
234
+ and not _is_fwd_seed_offset(node)
235
+ )
236
+
237
+
238
+ def _is_tangent(node: fx.Node) -> bool:
239
+ return node.op == "placeholder" and "tangents" in str(node.target)
240
+
241
+
242
+ def _is_bwd_seed_offset(node: fx.Node) -> bool:
243
+ return node.op == "placeholder" and (
244
+ "bwd_seed" in str(node.target) or "bwd_base_offset" in str(node.target)
245
+ )
246
+
247
+
248
+ def _is_fwd_seed_offset(node: fx.Node) -> bool:
249
+ return node.op == "placeholder" and (
250
+ "fwd_seed" in str(node.target) or "fwd_base_offset" in str(node.target)
251
+ )
252
+
253
+
254
+ def _is_backward_state(node: fx.Node) -> bool:
255
+ return node.op == "placeholder" and isinstance(node.meta.get("val"), BackwardState)
256
+
257
+
258
+ def _has_tag_is_backward(node: fx.Node) -> bool:
259
+ return node.meta.get("partitioner_tag", None) == "is_backward"
260
+
261
+
262
+ def _has_tag_must_be_in_backward(node: fx.Node) -> bool:
263
+ return node.meta.get("partitioner_tag", None) == "must_be_in_backward"
264
+
265
+
266
+ def _must_be_in_backward(node: fx.Node) -> bool:
267
+ return _has_tag_must_be_in_backward(node) or (
268
+ _has_tag_is_backward(node) and is_with_effects(node)
269
+ )
270
+
271
+
272
+ def _extract_fwd_bwd_outputs(
273
+ joint_module: fx.GraphModule, *, num_fwd_outputs
274
+ ) -> Tuple[List[fx.Node], List[fx.Node]]:
275
+ outputs = pytree.arg_tree_leaves(
276
+ *(node.args for node in joint_module.graph.find_nodes(op="output"))
277
+ )
278
+ fwd_outputs = outputs[:num_fwd_outputs]
279
+ bwd_outputs = outputs[num_fwd_outputs:]
280
+ return fwd_outputs, bwd_outputs
281
+
282
+
283
+ def _remove_by_name(saved_values: List[fx.Node], name: str):
284
+ for saved_value in saved_values:
285
+ if saved_value.name == name:
286
+ saved_values.remove(saved_value)
287
+ break
288
+
289
+
290
+ def _extract_fwd_bwd_modules(
291
+ joint_module: fx.GraphModule,
292
+ saved_values: List[fx.Node],
293
+ saved_sym_nodes: List[fx.Node],
294
+ *,
295
+ num_fwd_outputs: int,
296
+ ) -> Tuple[fx.GraphModule, fx.GraphModule]:
297
+ fwd_outputs, bwd_outputs = _extract_fwd_bwd_outputs(
298
+ joint_module, num_fwd_outputs=num_fwd_outputs
299
+ )
300
+ placeholders = joint_module.graph.find_nodes(op="placeholder")
301
+ primal_inputs = [*filter(_is_primal, placeholders)]
302
+ tangent_inputs = [*filter(_is_tangent, placeholders)]
303
+ fwd_seed_offset_inputs = [*filter(_is_fwd_seed_offset, placeholders)]
304
+ bwd_seed_offset_inputs = [*filter(_is_bwd_seed_offset, placeholders)]
305
+ backward_state_inputs = [*filter(_is_backward_state, placeholders)]
306
+
307
+ bwd_graph = _extract_graph_with_inputs_outputs(
308
+ joint_module.graph,
309
+ saved_sym_nodes + saved_values + tangent_inputs + bwd_seed_offset_inputs,
310
+ bwd_outputs,
311
+ "backward",
312
+ )
313
+
314
+ for node in bwd_graph.find_nodes(op="placeholder"):
315
+ # This is to filter out saved values that don't actually end up being used by the backwards pass
316
+ if not node.users:
317
+ _remove_by_name(saved_values, node.name)
318
+ _remove_by_name(saved_sym_nodes, node.name)
319
+ elif _is_backward_state(node):
320
+ # BackwardState is saved directly
321
+ _remove_by_name(saved_values, node.name)
322
+ assert backward_state_inputs
323
+
324
+ # Now that we have the finalized list of saved values, we need to ensure
325
+ # we propagate all symbols which are referenced by backwards inputs.
326
+ # These are not directly used in the graph but are required for downstream
327
+ # sizevar assignment
328
+ saved_symbols: Set[sympy.Symbol] = set()
329
+ saved_sym_nodes_binding = []
330
+ saved_sym_nodes_derived = []
331
+
332
+ # Some symbols may already be bound in the directly saved_sym_nodes,
333
+ # keep track of them so we don't re-bind them
334
+ for node in saved_sym_nodes:
335
+ symbol = is_symbol_binding_fx_node(node)
336
+ if symbol:
337
+ saved_symbols.add(symbol)
338
+ saved_sym_nodes_binding.append(node)
339
+ else:
340
+ saved_sym_nodes_derived.append(node)
341
+
342
+ # Now go through all of the prospective backward inputs and track any
343
+ # other symbols we need to bind
344
+ symbol_bindings = find_symbol_binding_fx_nodes(joint_module.graph)
345
+ for node in itertools.chain(saved_sym_nodes_derived, saved_values, tangent_inputs):
346
+ if "val" not in node.meta:
347
+ continue
348
+ new_symbols = free_symbols(node.meta["val"]) - saved_symbols
349
+ # NB: Deterministic order please!
350
+ for s in sorted(new_symbols, key=lambda s: s.name):
351
+ # NB: For well formed graphs, the symbol should always be present,
352
+ # but we also have ways to produce ill-formed graphs, e.g., direct
353
+ # make_fx usages, so don't choke in this case
354
+ if s not in symbol_bindings:
355
+ continue
356
+ saved_sym_nodes_binding.append(symbol_bindings[s])
357
+ saved_symbols |= new_symbols
358
+
359
+ # Update saved_sym_nodes that are now reordered to have all bindings at
360
+ # front. This can also be used later on to figure out the position of saved
361
+ # sym nodes in the output of fwd graph.
362
+ saved_sym_nodes.clear()
363
+ saved_sym_nodes.extend(saved_sym_nodes_binding + saved_sym_nodes_derived)
364
+
365
+ # Now, we re-generate the fwd/bwd graphs.
366
+ # NB: This might increase compilation time, but I doubt it matters
367
+ fwd_graph = _extract_graph_with_inputs_outputs(
368
+ joint_module.graph,
369
+ primal_inputs + fwd_seed_offset_inputs,
370
+ fwd_outputs + saved_values + saved_sym_nodes,
371
+ "forward",
372
+ )
373
+ bwd_graph = _extract_graph_with_inputs_outputs(
374
+ joint_module.graph,
375
+ saved_sym_nodes
376
+ + saved_values
377
+ + tangent_inputs
378
+ + bwd_seed_offset_inputs
379
+ + backward_state_inputs,
380
+ bwd_outputs,
381
+ "backward",
382
+ )
383
+
384
+ fwd_module = fx._lazy_graph_module._make_graph_module(joint_module, fwd_graph)
385
+ bwd_module = fx._lazy_graph_module._make_graph_module(joint_module, bwd_graph)
386
+ return fwd_module, bwd_module
387
+
388
+
389
+ def default_partition(
390
+ joint_module: fx.GraphModule, _joint_inputs, *, num_fwd_outputs
391
+ ) -> Tuple[fx.GraphModule, fx.GraphModule]:
392
+ """
393
+ Partitions the :attr:`joint_module` in a manner that closely resembles the
394
+ behavior observed in the original ``.forward()`` and ``.backward()`` of the
395
+ callable, i.e., the resulting forward graph contains those operators that
396
+ are executed in the original ``.forward()`` callable passed to
397
+ :func:`aot_function`.
398
+
399
+ The default partitioner collects the operators that are between the forward
400
+ inputs and the forward outputs. This helps in finding the tensors which have
401
+ to be stashed for the backward pass. These stashed tensors become the output
402
+ of the generated forward graph. The remaining operators are then placed in
403
+ the backward graph.
404
+
405
+ .. warning::
406
+ This API is experimental and likely to change.
407
+
408
+ Args:
409
+ joint_module(fx.GraphModule): The joint forward and backward graph. This
410
+ is the result of AOT Autograd tracing.
411
+
412
+ Returns:
413
+ Returns the generated forward and backward Fx graph modules.
414
+ """
415
+ if has_recomputable_ops(joint_module):
416
+ return min_cut_rematerialization_partition(
417
+ joint_module, _joint_inputs, num_fwd_outputs=num_fwd_outputs
418
+ )
419
+ primal_inputs = list(filter(_is_primal, joint_module.graph.nodes))
420
+ fwd_seed_offset_inputs = list(filter(_is_fwd_seed_offset, joint_module.graph.nodes))
421
+ inputs = primal_inputs + fwd_seed_offset_inputs
422
+ fwd_outputs, bwd_outputs = _extract_fwd_bwd_outputs(
423
+ joint_module, num_fwd_outputs=num_fwd_outputs
424
+ )
425
+ forward_only_graph = _extract_graph_with_inputs_outputs(
426
+ joint_module.graph, inputs, fwd_outputs, "forward"
427
+ )
428
+ forward_node_names = {
429
+ node.name for node in forward_only_graph.nodes if node.op != "output"
430
+ }
431
+ saved_values = []
432
+ saved_sym_nodes = []
433
+
434
+ for node in joint_module.graph.nodes:
435
+ if node.name not in forward_node_names:
436
+ continue
437
+ if is_sym_node(node):
438
+ # Symints must be kept separate from tensors so that PythonFunction only calls
439
+ # save_for_backward on tensors and stashes symints in autograd .ctx
440
+ saved_sym_nodes.append(node)
441
+ elif "tensor_meta" not in node.meta and node.op == "call_function":
442
+ # Since we can't save tuple of tensor values, we need to flatten out what we're saving
443
+ users = node.users
444
+ assert all(user.target == operator.getitem for user in users)
445
+ saved_values.extend(users)
446
+ else:
447
+ backward_usages = [
448
+ n for n in node.users if n.name not in forward_node_names
449
+ ]
450
+ if "tensor_meta" in node.meta and all(
451
+ is_sym_node(n) for n in backward_usages
452
+ ):
453
+ # If we have a tensor in the forward, where only its sizes/strides are needed in the backward,
454
+ # and not the actual tensor data,
455
+ # then it will be a lot cheaper to save only the sizes/strides, and not the actual tensor.
456
+ #
457
+ # Note that saving the tensor could also cause compilation problems:
458
+ # If the user mutated an input in the forward and uses its sizes/strides in the backward,
459
+ # then we would be obligated to clone the input before saving it to appease autograd.
460
+ # (This is how we originally found this bug).
461
+ saved_sym_nodes.extend(backward_usages)
462
+ else:
463
+ saved_values.append(node)
464
+ saved_values = list(dict.fromkeys(saved_values).keys())
465
+ saved_sym_nodes = list(dict.fromkeys(saved_sym_nodes).keys())
466
+
467
+ return _extract_fwd_bwd_modules(
468
+ joint_module,
469
+ saved_values,
470
+ saved_sym_nodes=saved_sym_nodes,
471
+ num_fwd_outputs=num_fwd_outputs,
472
+ )
473
+
474
+
475
+ INT_INF = int(1e6)
476
+
477
+
478
+ def _tensor_nbytes(numel: int, dtype) -> int:
479
+ return numel * dtype.itemsize
480
+
481
+
482
+ def _size_of(node: fx.Node) -> int:
483
+ def object_nbytes(x) -> int:
484
+ if not isinstance(x, torch.Tensor):
485
+ return 0
486
+ return _tensor_nbytes(hint_int(x.numel(), fallback=4096), x.dtype)
487
+
488
+ if "val" in node.meta:
489
+ val = node.meta["val"]
490
+ if isinstance(val, py_sym_types):
491
+ return 1
492
+ # NB: The fallback values here are meaningless, maybe we should respect
493
+ # torch._inductor.config.unbacked_symint_fallback (but this is a
494
+ # layering violation)
495
+ elif isinstance(val, (list, tuple)):
496
+ return sum(object_nbytes(n) for n in val)
497
+ elif isinstance(val, dict):
498
+ return sum(object_nbytes(n) for _, n in val.items())
499
+ elif isinstance(val, torch.Tensor):
500
+ return object_nbytes(val)
501
+
502
+ raise RuntimeError(f"Unknown metadata type {type(val)} on node {node}")
503
+ if node.op == "get_attr":
504
+ return 0
505
+ raise RuntimeError(
506
+ f"Node {node} didn't have `val` metadata; we should always have `val` metadata on the nodes."
507
+ )
508
+
509
+
510
+ # Used for some investigative purposes
511
+ def _count_ops(graph: fx.Graph):
512
+ from collections import defaultdict
513
+
514
+ cnt: Dict[str, int] = defaultdict(int)
515
+ for node in graph.nodes:
516
+ if node.op == "call_function":
517
+ cnt[node.target.__name__] += 1
518
+ log.info("%s", sorted(cnt.items(), key=lambda x: x[1], reverse=True))
519
+
520
+
521
+ @functools.lru_cache(None)
522
+ def pointwise_ops():
523
+ ops = []
524
+ for attr_name in dir(torch.ops.aten):
525
+ opoverloadpacket = getattr(torch.ops.aten, attr_name)
526
+ if not isinstance(opoverloadpacket, torch._ops.OpOverloadPacket):
527
+ continue
528
+
529
+ for overload in opoverloadpacket.overloads():
530
+ op_overload = getattr(opoverloadpacket, overload)
531
+ if torch.Tag.pointwise in op_overload.tags:
532
+ # currently aot autograd uses packet not overload
533
+ ops.append(opoverloadpacket)
534
+ break
535
+
536
+ return ops
537
+
538
+
539
+ def sort_depths(args, depth_map: Dict[fx.Node, int]) -> List[Tuple[fx.Node, int]]:
540
+ arg_depths = {
541
+ arg: depth_map[arg] for arg in args if isinstance(arg, torch.fx.node.Node)
542
+ }
543
+ return sorted(arg_depths.items(), key=lambda x: x[1], reverse=True)
544
+
545
+
546
+ def reordering_to_mimic_autograd_engine(gm: fx.GraphModule) -> fx.GraphModule:
547
+ """
548
+ This pass finds the first bwd node in the graph (by looking at users of
549
+ tangents) and then reorders the graph by walking from this node to all the
550
+ way to the end of the graph. At each op in this traveral, we insert this op
551
+ in a new graph and try to bring only the relevant subgraph from the other
552
+ non-bwd edges relevant for this op. This closely mimics the behavior of
553
+ autograd engine.
554
+
555
+ Why is this pass required in the first place?
556
+
557
+ This is an artifact of how partitioners work today. The starting point of
558
+ partitioner is a joint graph, which is fwd and then bwd graph. In the case
559
+ of checkpointing, we keep portions of fwd graph in their original place in
560
+ the joint graph, while obtaining a bwd graph. As a result, the resulting bwd
561
+ graph has copies of recomputed fwd subgraphs followed by the original bwd
562
+ graph. If we run this naively, this leads to bad memory footprint, because
563
+ the fwd subgraphs are live for way longer duration than necessary. This pass
564
+ reorders the operations such that we prioritize the ops for the original bwd
565
+ graph while only realizing those ops from the fwd graph that are necessary
566
+ at any given point in the graph.
567
+ """
568
+
569
+ new_graph = fx.Graph()
570
+ env: Dict[fx.Node, fx.Node] = {}
571
+
572
+ # Add new placeholder nodes in the order specified by the inputs
573
+ for node in gm.graph.find_nodes(op="placeholder"):
574
+ env[node] = new_graph.node_copy(node, lambda x: env[x])
575
+
576
+ order = {}
577
+ for idx, node in enumerate(gm.graph.nodes):
578
+ order[node] = idx
579
+
580
+ def insert_node_in_graph(node):
581
+ cur_nodes = [node]
582
+ insertable_nodes = set()
583
+ while len(cur_nodes) > 0:
584
+ node = cur_nodes.pop()
585
+ if node in insertable_nodes or node in env:
586
+ continue
587
+ insertable_nodes.add(node)
588
+
589
+ # Bias traversal towards the nodes that have higher depth - prioritizes
590
+ # critical path first.
591
+ cur_nodes += node.all_input_nodes
592
+
593
+ insertable_nodes = sorted(insertable_nodes, key=lambda n: order[n])
594
+ for node in insertable_nodes:
595
+ env[node] = new_graph.node_copy(node, lambda x: env[x])
596
+
597
+ # Find first bwd node in the graph
598
+ tangent_inputs = list(filter(_is_tangent, gm.graph.nodes))
599
+ first_node_in_bwd = None
600
+ minimum_order = math.inf
601
+ for tangent in tangent_inputs:
602
+ for user in tangent.users:
603
+ if order[user] < minimum_order:
604
+ minimum_order = order[user]
605
+ first_node_in_bwd = user
606
+
607
+ # If gradInp does not depend upon gradOut, we may not find any nodes in the "backwards pass"
608
+ if first_node_in_bwd is None:
609
+ return gm
610
+
611
+ # Build the graph op-by-op by starting from the node all the way to the end
612
+ for node in list(gm.graph.nodes)[order[first_node_in_bwd] :]:
613
+ insert_node_in_graph(node)
614
+
615
+ # The output node is already built by the traversal.
616
+ new_gm = torch.fx.GraphModule(gm, new_graph)
617
+ return new_gm
618
+
619
+
620
+ def functionalize_rng_ops(
621
+ joint_module: fx.GraphModule,
622
+ fw_module: fx.GraphModule,
623
+ bw_module: fx.GraphModule,
624
+ num_sym_nodes: int,
625
+ ) -> Tuple[fx.GraphModule, fx.GraphModule]:
626
+ # During user-driven activation checkpointing, we have to ensure that a rng
627
+ # op in fwd yields the same output as the recomputed rng op in the bwd. To
628
+ # do this, we use functionalize wrappers to wrap the random ops and share
629
+ # rng state between the fwd and bwd graphs.
630
+
631
+ # There are 3 main steps to do this
632
+ # Step 1 - Construct a mapping of rng node between the fwd and its counterpart in bwd.
633
+ # Step 2 - Modify the fwd pass such that
634
+ # 1) Replace rand with run_and_save_rng_state wrapper
635
+ # 2) Replace the users of the original op with the output[1] of this op.
636
+ # 3) Collect all the rng_state - output[0] of each op, and make them
637
+ # output nodes. Special care needs to be taken here because fwd outputs
638
+ # has symints at the very end.
639
+ # Step 3 - Modify the bwd pass such that
640
+ # 1) Add the input nodes just before the tangents for the stashed rng states
641
+ # 2) Replace rand with run_with_save_rng_state wrappers
642
+ # 3) Use the stashed states as inputs to these ops
643
+
644
+ # Unique id to generate name
645
+ uid = itertools.count()
646
+
647
+ def get_rng_ops(gmod):
648
+ random_nodes = {}
649
+ for node in gmod.graph.nodes:
650
+ if (
651
+ node.op == "call_function"
652
+ and hasattr(node.target, "tags")
653
+ and torch.Tag.nondeterministic_seeded in node.target.tags
654
+ ):
655
+ random_nodes[node.name] = node
656
+ return random_nodes
657
+
658
+ def get_device(node):
659
+ """
660
+ Check the example value of the node outputs to find the device type.
661
+ """
662
+ if "val" not in node.meta:
663
+ return None
664
+
665
+ candidates = node.meta["val"]
666
+ if not isinstance(candidates, tuple):
667
+ candidates = (candidates,)
668
+
669
+ for candidate in candidates:
670
+ if isinstance(candidate, torch.Tensor):
671
+ if candidate.device.type == "cuda":
672
+ return "cuda"
673
+
674
+ return "cpu"
675
+
676
+ def get_sample_rng_state(device):
677
+ if device == "cuda":
678
+ return torch.cuda.get_rng_state()
679
+ return torch.get_rng_state()
680
+
681
+ # Step 1 - Construct a mapping of rng node between the fwd and its counterpart in bwd.
682
+ joint_graph_rng_ops = get_rng_ops(joint_module)
683
+ fw_graph_rng_ops = get_rng_ops(fw_module)
684
+ bw_graph_rng_ops = get_rng_ops(bw_module)
685
+ recomputable_rng_ops_map = {}
686
+ for node in joint_module.graph.nodes:
687
+ if (
688
+ must_recompute(node)
689
+ and hasattr(node.target, "tags")
690
+ and torch.Tag.nondeterministic_seeded in node.target.tags
691
+ ):
692
+ base_node = joint_graph_rng_ops[node.name]
693
+ fw_node = fw_graph_rng_ops[node.name]
694
+ bw_node = bw_graph_rng_ops[node.name]
695
+ recomputable_rng_ops_map[base_node] = {"fwd": fw_node, "bwd": bw_node}
696
+
697
+ run_and_save_rng = torch._prims.rng_prims.run_and_save_rng_state
698
+ run_with_rng_state = torch._prims.rng_prims.run_with_rng_state
699
+ bw_tangent_start_node = None
700
+ for node in bw_module.graph.find_nodes(op="placeholder"):
701
+ if "tangent" in node.name:
702
+ bw_tangent_start_node = node
703
+ break
704
+ if bw_tangent_start_node is None:
705
+ raise RuntimeError(
706
+ "Couldn't find tangent node in graph inputs. This is unexpected, please file a bug if you see this"
707
+ )
708
+
709
+ fw_rng_state_outputs = []
710
+ for base_node, node_pair in recomputable_rng_ops_map.items():
711
+ # Step 2 - Modify the fwd pass such that
712
+ fw_node = node_pair["fwd"]
713
+ bw_node = node_pair["bwd"]
714
+ fw_graph = fw_module.graph
715
+ with fw_graph.inserting_before(fw_node):
716
+ functional_fw_node = fw_graph.create_node(
717
+ "call_function",
718
+ run_and_save_rng,
719
+ args=(fw_node.target, *fw_node.args),
720
+ kwargs=fw_node.kwargs,
721
+ )
722
+ state = fw_graph.create_node(
723
+ "call_function",
724
+ operator.getitem,
725
+ args=(functional_fw_node, 0),
726
+ kwargs={},
727
+ )
728
+ rng_output = fw_graph.create_node(
729
+ "call_function",
730
+ operator.getitem,
731
+ args=(
732
+ functional_fw_node,
733
+ 1,
734
+ ),
735
+ kwargs={},
736
+ )
737
+ fw_node.replace_all_uses_with(rng_output)
738
+ fw_graph.erase_node(fw_node)
739
+ fw_rng_state_outputs.append(state)
740
+
741
+ # Step 3 - Modify the bwd pass such that
742
+ bw_graph = bw_module.graph
743
+ with bw_graph.inserting_before(bw_tangent_start_node):
744
+ state_name = f"rng_state_output_{next(uid)}"
745
+ bw_rng_state_node = bw_graph.placeholder(state_name)
746
+ bw_rng_state_node.meta["val"] = get_sample_rng_state(get_device(fw_node))
747
+
748
+ with bw_graph.inserting_before(bw_node):
749
+ rng_output = bw_graph.create_node(
750
+ "call_function",
751
+ run_with_rng_state,
752
+ args=(bw_rng_state_node, bw_node.target, *bw_node.args),
753
+ kwargs=bw_node.kwargs,
754
+ )
755
+
756
+ bw_node.replace_all_uses_with(rng_output)
757
+ bw_graph.erase_node(bw_node)
758
+
759
+ # Add the rng states in the output of the fwd graph. AOT Autograd assumes
760
+ # that symints are at the end of forward graph outputs. So, insert the new
761
+ # rng states accordingly.
762
+ fw_output_node = next(iter(fw_module.graph.find_nodes(op="output")))
763
+ fw_outputs = fw_output_node.args[0]
764
+ sym_node_start_idx = len(fw_outputs) - num_sym_nodes
765
+ outputs = (
766
+ fw_outputs[:sym_node_start_idx]
767
+ + tuple(fw_rng_state_outputs)
768
+ + fw_outputs[sym_node_start_idx:]
769
+ )
770
+ fw_module.graph.output(outputs)
771
+ fw_module.graph.erase_node(fw_output_node)
772
+ fw_module.recompile()
773
+ bw_module.recompile()
774
+ return fw_module, bw_module
775
+
776
+
777
+ def cleanup_recompute_tags(joint_module: fx.GraphModule) -> fx.GraphModule:
778
+ """
779
+ If there are two consecutive checkpointed blocks with no operator in
780
+ between, we would still want to stash the tensor at the boundary of
781
+ checkpointed blocks. The following pass makes the last output node
782
+ non-recomputable to allow for that.
783
+ """
784
+ for node in joint_module.graph.nodes:
785
+ if must_recompute(node):
786
+ for user in node.users:
787
+ if (
788
+ must_recompute(user)
789
+ and user.meta["ac_graph_id"] > node.meta["ac_graph_id"]
790
+ ):
791
+ node.meta["recompute"] = CheckpointPolicy.MUST_SAVE
792
+ if node.meta.get("has_backward_hook", False) and not any(
793
+ must_recompute(user) for user in node.users
794
+ ):
795
+ # If node is AC region output and has a backward hook on it, we intentionally choose to save it.
796
+ # This is to work around circular dependencies in Traceable FSDP2+AC.
797
+ # Example:
798
+ # ```
799
+ # out = fully_shard(utils.checkpoint(module))(x)
800
+ # norm_out = layer_norm(out)
801
+ # ```
802
+ # Here there is a circular dependency:
803
+ # 1. In backward, grad_input of layer_norm aka. `out_grad` is actually dependent on `out`.
804
+ # 2. `out` depends on `out`'s backward hook created by FSDP2 (which does all-gather for `module` weights)
805
+ # in order to be recomputed.
806
+ # 3. `out`'s backward hook, as is the case for all eager backward hooks, depends on `out_grad`
807
+ # -> circular dependency with (1)!
808
+ #
809
+ # Solution: check whether `out` has a backward hook, and if so, intentionally save `out`
810
+ # in forward graph outputs. With this, we can break the above circular dependency.
811
+ node.meta["recompute"] = CheckpointPolicy.MUST_SAVE
812
+ return joint_module
813
+
814
+
815
+ def solve_min_cut(
816
+ joint_graph: fx.Graph,
817
+ node_info: NodeInfo,
818
+ min_cut_options: MinCutOptions,
819
+ dont_ban=None,
820
+ ):
821
+ if dont_ban is None:
822
+ dont_ban = set()
823
+ op_types = get_default_op_list()
824
+
825
+ if AOT_PARTITIONER_DEBUG:
826
+ joint_module_ops = {
827
+ str(node.target._overloadpacket)
828
+ for node in joint_graph.nodes
829
+ if node.op == "call_function" and hasattr(node.target, "_overloadpacket")
830
+ }
831
+ ops_ignored = joint_module_ops - {str(i) for i in op_types.recomputable_ops}
832
+ log.info("Ops banned from re-materialization: %s", ops_ignored)
833
+
834
+ def can_fuse_into_auto_functionalized(a, b):
835
+ if b.target != torch.ops.higher_order.auto_functionalized:
836
+ return False
837
+ mutable_op = b.args[0]
838
+ (
839
+ mutable_arg_names,
840
+ _,
841
+ ) = torch._higher_order_ops.auto_functionalize.get_mutable_args(mutable_op)
842
+ for name in mutable_arg_names:
843
+ arg = b.kwargs[name]
844
+ if a is arg:
845
+ return True
846
+ if isinstance(arg, list):
847
+ if a in arg:
848
+ return True
849
+ return False
850
+
851
+ def can_fuse_into_triton_kernel_wrapper_functional(a, b):
852
+ if b.target != torch.ops.higher_order.triton_kernel_wrapper_functional:
853
+ return False
854
+ mutable_arg_names = b.kwargs["tensors_to_clone"]
855
+ for name in mutable_arg_names:
856
+ arg = b.kwargs["kwargs"][name]
857
+ if a is arg:
858
+ return True
859
+ return False
860
+
861
+ def is_fusible(a, b):
862
+ # We can perform "memory fusion" into a cat, but cat cannot be a
863
+ # producer to a fusion
864
+ if get_aten_target(b) == aten.cat:
865
+ return True
866
+ if can_fuse_into_auto_functionalized(a, b):
867
+ return True
868
+ if can_fuse_into_triton_kernel_wrapper_functional(a, b):
869
+ return True
870
+ if (
871
+ a.target is operator.getitem
872
+ and a.args[0].target
873
+ is torch.ops.higher_order.triton_kernel_wrapper_functional
874
+ ):
875
+ # if a is the output of a user triton kernel,
876
+ # then (by default) we will not be able to fuse b into it
877
+ return False
878
+ return op_types.is_fusible(a) and op_types.is_fusible(b)
879
+
880
+ try:
881
+ import networkx as nx
882
+ except ImportError as e:
883
+ raise RuntimeError(
884
+ "Need networkx installed to perform smart recomputation " "heuristics"
885
+ ) from e
886
+
887
+ def is_materialized_backwards(node):
888
+ if op_types.is_view(node):
889
+ return False
890
+ cur_nodes = {node}
891
+ while len(cur_nodes) > 0:
892
+ cur = cur_nodes.pop()
893
+ for user in cur.users:
894
+ if not node_info.is_required_fw(user) and not is_fusible(cur, user):
895
+ return True
896
+ if op_types.is_view(user):
897
+ cur_nodes.add(user)
898
+
899
+ return False
900
+
901
+ def should_ban_recomputation(node):
902
+ if node.op != "call_function":
903
+ return False
904
+ if node.target == operator.getitem:
905
+ return False
906
+ if node.meta.get("recompute", None) == CheckpointPolicy.MUST_SAVE:
907
+ return True
908
+ if config.recompute_views and op_types.is_view(node):
909
+ return False
910
+ if node.target in [aten.lift_fresh_copy.default, aten.lift_fresh.default]:
911
+ return False
912
+
913
+ if min_cut_options.ban_if_not_in_allowlist:
914
+ if not op_types.is_recomputable(node):
915
+ return True
916
+ else:
917
+ if op_types.is_random(node) or op_types.is_compute_intensive(node):
918
+ return True
919
+
920
+ # If a node *must* be materialized in the backwards pass, then we
921
+ # should never recompute it. This is a pretty subtle point. In
922
+ # general, the assumption we make is that recomputing a node in the
923
+ # backwards pass is "free". However, if a node must be materialized
924
+ # in the backwards pass, then recomputing it is never free.
925
+ if min_cut_options.ban_if_materialized_backward and is_materialized_backwards(
926
+ node
927
+ ):
928
+ log.debug("materialized backwards: %s %s", node, tuple(node.users))
929
+ return True
930
+
931
+ # Arbitrary hack that sometimes seems to help things. The above
932
+ # modification appears to have made this heuristic a lot less critical
933
+ # for performance.
934
+ # NB: As of PR #121692, this hack no longer seems necessary.
935
+ if node.dist_from_bw < 1000 and node.dist_from_bw > config.max_dist_from_bw:
936
+ return True
937
+
938
+ # If the output of an op is 4x smaller (arbitrary choice),
939
+ # then we don't allow recomputation. The idea here is that for
940
+ # things like reductions, saving the output of the reduction is very
941
+ # cheap/small, and it makes sure we don't do things like recompute
942
+ # normalizations in the backwards.
943
+ if min_cut_options.ban_if_reduction:
944
+ input_tensors_size = sum(
945
+ _size_of(i) for i in node.args if isinstance(i, fx.Node)
946
+ )
947
+ output_size = _size_of(node)
948
+ return output_size * 4 < input_tensors_size
949
+ return False
950
+
951
+ def is_materialized(node):
952
+ if node.op == "placeholder":
953
+ return True
954
+
955
+ return not all(is_fusible(node, user) for user in node.users)
956
+
957
+ def get_node_weight(node) -> float:
958
+ mem_sz = _size_of(node)
959
+ if config.recompute_views and op_types.is_view(node):
960
+ # If `config.recompute_views=True`, we don't save views. This is generally
961
+ # a good idea since views are free to recompute, and it makes it a bit simpler
962
+ # to analyze.
963
+ # NB: If they're not free to recompute (e.g. nested tensors)... I
964
+ # think we should modify checks for view_ops to `is_view` and check
965
+ # that. Basically, with nested tensors, `aten.view` is not a "view
966
+ # op".
967
+ return math.inf
968
+
969
+ if isinstance(node.meta["val"], py_sym_types):
970
+ # We never want to save symfloats
971
+ if not isinstance(node.meta["val"], torch.SymInt):
972
+ return INT_INF
973
+
974
+ # Heuristic to bias towards nodes closer to the backwards pass
975
+ # Complete guess about current value
976
+ mem_sz = int(mem_sz * (1.1 ** max(min(node.dist_from_bw, 100), 1)))
977
+ if is_materialized(node):
978
+ return mem_sz
979
+ else:
980
+ return mem_sz * 2
981
+
982
+ nx_graph = nx.DiGraph()
983
+ banned_nodes = set()
984
+
985
+ def ban_recomputation_if_allowed(node):
986
+ if op_types.is_view(node):
987
+ return False
988
+ if node in dont_ban:
989
+ return False
990
+ # This bans recomputation of the node unless we've been forced not to by
991
+ # user annotation
992
+ if must_recompute(node):
993
+ return False
994
+
995
+ if "val" in node.meta and isinstance(node.meta["val"], torch.SymFloat):
996
+ return False
997
+
998
+ banned_nodes.add(node)
999
+ # A node will only ever be recomputed if there is a path from an
1000
+ # ancestor of this node to the backwards path through this node that
1001
+ # doesn't go through any saved value. If this node is saved, then that
1002
+ # condition is not possible.
1003
+ nx_graph.add_edge("source", node.name + "_in", capacity=math.inf)
1004
+ return True
1005
+
1006
+ for node in joint_graph.nodes:
1007
+ if node.op == "output":
1008
+ continue
1009
+
1010
+ if node in node_info.required_bw_nodes:
1011
+ if node not in node_info.inputs:
1012
+ nx_graph.add_edge(node.name + "_in", "sink", capacity=math.inf)
1013
+ continue
1014
+ # If someone saves a input for backward as-is and backward
1015
+ # returns that tensor as-is as a grad input, then the node x would
1016
+ # be both a required_bw_node and an input. In this case we
1017
+ # (1) connect x_in to to the source, (2) x_out to the sink, and
1018
+ # (3) assign the proper weight to the x_in-x_out edge, so that
1019
+ # x would be part of cut nodes. A case where this happens is if
1020
+ # NestedTensor saves a offset tensor as part of the singleton int
1021
+ # in sizes.
1022
+ nx_graph.add_edge(node.name + "_out", "sink", capacity=math.inf)
1023
+
1024
+ if must_recompute(node):
1025
+ # If user explicitly says they want to recompute a node, we honor it
1026
+ # by adding an inf-capacity edge from X_in to the sink.
1027
+ # This way, X_in node is guaranteed to be part of the subgraph that contains "sink"
1028
+ # after the cut, thus guaranteeing that X op will be recomputed.
1029
+ nx_graph.add_edge(node.name + "_in", "sink", capacity=math.inf)
1030
+ continue
1031
+
1032
+ if _is_primal(node) or _is_fwd_seed_offset(node):
1033
+ ban_recomputation_if_allowed(node)
1034
+
1035
+ # If a node can't be recomputed (too expensive or involves randomness),
1036
+ # we prevent it from being recomputed by adding an inf edge to the source
1037
+ # We only need to ban nodes in the fw pass, as those are the only ones that would be recomputed.
1038
+ if node_info.is_required_fw(node) and should_ban_recomputation(node):
1039
+ ban_recomputation_if_allowed(node)
1040
+
1041
+ # Checks if a node is actually a tuple. Can be simplified to just an isinstance check if we always use faketensors.
1042
+ is_non_tensor_node = (
1043
+ "val" not in node.meta and "tensor_meta" not in node.meta
1044
+ ) or ("val" in node.meta and not isinstance(node.meta["val"], torch.Tensor))
1045
+
1046
+ if is_sym_node(node):
1047
+ weight = float(sym_node_size(node))
1048
+ elif is_non_tensor_node:
1049
+ weight = (
1050
+ 0.0 if isinstance(node.meta.get("val"), BackwardState) else math.inf
1051
+ )
1052
+ else:
1053
+ weight = get_node_weight(node)
1054
+ # Creates the weights on the "node" edge
1055
+ nx_graph.add_edge(node.name + "_in", node.name + "_out", capacity=weight)
1056
+ for user in node.users:
1057
+ nx_graph.add_edge(node.name + "_out", user.name + "_in", capacity=math.inf)
1058
+
1059
+ # todo(chilli): This is the most questionable of the 3 heuristics for banning recompute.
1060
+ # Some example models to look at where this helps perf: poolformer_m36,
1061
+ # mixer_b16_224, cait_m36_384
1062
+
1063
+ # The "rough" idea here is that if you have some node that is used by both a
1064
+ # node nearby downstream as well as a node far downstream, if we recompute
1065
+ # both of the downstream nodes, we're unlikely to be able to fuse both
1066
+ # downstream nodes together.
1067
+
1068
+ # Thus, we shouldn't aim to recompute far downstream nodes that depend on
1069
+ # this node. That intuition of "far downstream" is captured by whether
1070
+ # there's an unfusible op along the chain somewhere
1071
+
1072
+ # It could probably be improved by properly analyzing what's going on in the
1073
+ # backwards pass instead of only relying on whether it's unfusible in the
1074
+ # forwards.
1075
+
1076
+ def find_first_unfusible(start_nodes: List[fx.Node], max_range: int) -> int:
1077
+ """
1078
+ Finds the first unfusible node in the chain of nodes starting from
1079
+ `start_nodes` and returns its position.
1080
+ """
1081
+ sorted_nodes: List[Tuple[int, fx.Node, bool]] = []
1082
+ for n in start_nodes:
1083
+ heapq.heappush(sorted_nodes, (node_info.get_fw_order(n), n, True))
1084
+
1085
+ while len(sorted_nodes) > 0:
1086
+ _, node, node_is_fusible = heapq.heappop(sorted_nodes)
1087
+ if not node_is_fusible:
1088
+ return node_info.get_fw_order(node)
1089
+ for user in node.users:
1090
+ if node_info.is_required_fw(user):
1091
+ if node_info.get_fw_order(user) > max_range:
1092
+ continue
1093
+ heapq.heappush(
1094
+ sorted_nodes,
1095
+ (node_info.get_fw_order(user), user, is_fusible(node, user)),
1096
+ )
1097
+ return max_range
1098
+
1099
+ if min_cut_options.ban_if_used_far_apart:
1100
+ for used_node in node_info.required_fw_nodes:
1101
+ orders = [
1102
+ node_info.get_fw_order(user)
1103
+ for user in used_node.users
1104
+ if node_info.is_required_fw(user)
1105
+ ]
1106
+ fw_users = [
1107
+ user for user in used_node.users if node_info.is_required_fw(user)
1108
+ ]
1109
+ if len(orders) > 0:
1110
+ first_unfusible_use = find_first_unfusible(fw_users, max(orders))
1111
+ for user in tuple(used_node.users):
1112
+ if (
1113
+ node_info.is_required_fw(user)
1114
+ and node_info.get_fw_order(user) > first_unfusible_use
1115
+ and is_fusible(used_node, user)
1116
+ ):
1117
+ if user in banned_nodes:
1118
+ continue
1119
+ log.info(
1120
+ "used above/below fusible %s:(%s) -> %s -> %s:(%s)",
1121
+ used_node,
1122
+ node_info.get_fw_order(used_node),
1123
+ first_unfusible_use,
1124
+ user,
1125
+ node_info.get_fw_order(user),
1126
+ )
1127
+ ban_recomputation_if_allowed(user)
1128
+
1129
+ # This heuristic is fairly straightforward. The idea is that although it is
1130
+ # cheap to recompute bandwidth-bound ops, we don't want to end up in a situation
1131
+ # where we have a long chain of pointwise ops from the beginning to the end
1132
+ # of the model (like say, residual connections)
1133
+
1134
+ # todo: I'm not totally sure why this heuristic matters. It's possible that this is
1135
+ # working around Inductor fusion decisions, or that it's a patch over
1136
+ # suboptimal partitioning decisions
1137
+
1138
+ # Some models it improves perf on are cait_m36_384, mixer_b16_224, poolformer_m36
1139
+
1140
+ if min_cut_options.ban_if_long_fusible_chains:
1141
+ visited = set()
1142
+ for start_node in joint_graph.nodes:
1143
+ if not node_info.is_required_fw(start_node):
1144
+ continue
1145
+ fusible = [(node_info.get_fw_order(start_node), start_node)]
1146
+ start_order = node_info.get_fw_order(start_node)
1147
+ while len(fusible) > 0:
1148
+ _, cur = heapq.heappop(fusible)
1149
+ if cur in visited:
1150
+ continue
1151
+ visited.add(cur)
1152
+ # 100 is arbitrary choice to try and prevent degenerate cases
1153
+ if (
1154
+ node_info.get_fw_order(cur) > start_order + 100
1155
+ and len(fusible) == 0
1156
+ ):
1157
+ log.info(
1158
+ "too long %s %s %s %s",
1159
+ cur,
1160
+ start_node,
1161
+ node_info.get_fw_order(cur),
1162
+ node_info.get_fw_order(start_node),
1163
+ )
1164
+ ban_recomputation_if_allowed(cur)
1165
+ break
1166
+
1167
+ for user in cur.users:
1168
+ if (
1169
+ node_info.is_required_fw(user)
1170
+ and is_fusible(cur, user)
1171
+ and user not in banned_nodes
1172
+ ):
1173
+ heapq.heappush(fusible, (node_info.get_fw_order(user), user))
1174
+
1175
+ try:
1176
+ cut_value, partition = nx.minimum_cut(nx_graph, "source", "sink")
1177
+ except Exception:
1178
+ log.info("Failed to compute min-cut on following graph:")
1179
+ log.info("\n".join(nx.readwrite.edgelist.generate_edgelist(nx_graph)))
1180
+ visualize_min_cut_graph(nx_graph)
1181
+ raise
1182
+
1183
+ reachable, non_reachable = partition
1184
+ cutset: Set[Tuple[str, str]] = set()
1185
+ for u, nbrs in ((n, nx_graph[n]) for n in reachable):
1186
+ cutset.update((u, v) for v in nbrs if v in non_reachable)
1187
+
1188
+ cut_nodes = set()
1189
+ for node_in, node_out in cutset:
1190
+ assert node_in[:-3] == node_out[:-4]
1191
+ node_name = node_in[:-3]
1192
+ cut_nodes.add(node_name)
1193
+
1194
+ name_to_node = get_name_to_node(joint_graph)
1195
+ # To make this stuff deterministic
1196
+ node_idx = {node: idx for idx, node in enumerate(joint_graph.nodes)}
1197
+ saved_values = sorted(
1198
+ (name_to_node[node] for node in cut_nodes), key=lambda x: node_idx[x]
1199
+ )
1200
+ return saved_values, banned_nodes
1201
+
1202
+
1203
+ def visualize_min_cut_graph(nx_graph):
1204
+ import networkx as nx
1205
+ import pydot
1206
+
1207
+ dot_format = nx.nx_pydot.to_pydot(nx_graph).to_string()
1208
+ dot_graph = pydot.graph_from_dot_data(dot_format)[0]
1209
+ for edge in dot_graph.get_edges():
1210
+ weight = nx_graph[edge.get_source()][edge.get_destination()]["capacity"]
1211
+ # Set edge label to weight
1212
+ edge.set_label(str(weight))
1213
+ # Color edges with weight 'inf' as red
1214
+ if weight == float("inf"):
1215
+ edge.set_color("red")
1216
+ log.info("Visualizing the failed graph to min_cut_failed.svg")
1217
+ dot_graph.write_svg("min_cut_failed.svg")
1218
+
1219
+
1220
+ def get_default_op_list() -> OpTypes:
1221
+ default_recomputable_ops: List[Callable] = [
1222
+ aten.add,
1223
+ aten.sub,
1224
+ aten.div,
1225
+ aten.atan2,
1226
+ aten.mul,
1227
+ aten.max,
1228
+ aten.min,
1229
+ aten.pow,
1230
+ aten.remainder,
1231
+ aten.fmod,
1232
+ aten.__and__,
1233
+ aten.__or__,
1234
+ aten.__xor__,
1235
+ aten.__lshift__,
1236
+ aten.__rshift__,
1237
+ aten.eq,
1238
+ aten.ne,
1239
+ aten.ge,
1240
+ aten.gt,
1241
+ aten.le,
1242
+ aten.lt,
1243
+ aten.abs,
1244
+ aten.bitwise_not,
1245
+ aten.ceil,
1246
+ aten.floor,
1247
+ aten.frac,
1248
+ aten.neg,
1249
+ aten.relu,
1250
+ aten.round,
1251
+ aten.silu,
1252
+ aten.trunc,
1253
+ aten.log,
1254
+ aten.log10,
1255
+ aten.log1p,
1256
+ aten.log2,
1257
+ aten.lgamma,
1258
+ aten.exp,
1259
+ aten.expm1,
1260
+ aten.erf,
1261
+ aten.erfc,
1262
+ aten.cos,
1263
+ aten.acos,
1264
+ aten.cosh,
1265
+ aten.sin,
1266
+ aten.asin,
1267
+ aten.sinh,
1268
+ aten.tan,
1269
+ aten.atan,
1270
+ aten.tanh,
1271
+ aten.atanh,
1272
+ aten.sqrt,
1273
+ aten.rsqrt,
1274
+ aten.reciprocal,
1275
+ aten.sigmoid,
1276
+ aten.softplus,
1277
+ aten.threshold,
1278
+ aten.threshold_backward,
1279
+ aten.clamp,
1280
+ aten.where,
1281
+ aten.lerp,
1282
+ aten.addcmul,
1283
+ aten.gelu,
1284
+ aten.gelu_backward,
1285
+ aten.sum,
1286
+ aten.mean,
1287
+ aten._grad_sum_to_size,
1288
+ aten.sum_to_size,
1289
+ aten.amax,
1290
+ aten.to,
1291
+ aten.type_as,
1292
+ operator.getitem,
1293
+ aten.squeeze,
1294
+ aten.unsqueeze,
1295
+ aten.rsub,
1296
+ aten._to_copy,
1297
+ ] # noqa: E501,B950
1298
+ recomputable_view_ops = [aten.squeeze, aten.unsqueeze, aten.alias]
1299
+ recomputable_view_ops += [
1300
+ aten.view,
1301
+ aten.slice,
1302
+ aten.t,
1303
+ prims.broadcast_in_dim,
1304
+ aten.expand,
1305
+ aten.as_strided,
1306
+ aten.permute,
1307
+ aten.select,
1308
+ ]
1309
+ view_ops = recomputable_view_ops
1310
+ default_recomputable_ops += [
1311
+ prims.div,
1312
+ prims.convert_element_type,
1313
+ aten.clone,
1314
+ aten._to_copy,
1315
+ aten.full_like,
1316
+ prims.var,
1317
+ prims.sum,
1318
+ aten.var,
1319
+ aten.std,
1320
+ prims.broadcast_in_dim,
1321
+ aten.select,
1322
+ aten._unsafe_view,
1323
+ aten.view,
1324
+ aten.expand,
1325
+ aten.slice,
1326
+ aten.reshape,
1327
+ aten.broadcast_tensors,
1328
+ aten.scalar_tensor,
1329
+ aten.ones,
1330
+ aten.new_zeros,
1331
+ aten.lift_fresh_copy,
1332
+ aten.arange,
1333
+ aten.triu,
1334
+ aten.var_mean,
1335
+ aten.isinf,
1336
+ aten.any,
1337
+ aten.full,
1338
+ aten.as_strided,
1339
+ aten.zeros,
1340
+ aten.empty,
1341
+ aten.empty_like,
1342
+ aten.argmax,
1343
+ aten.maximum,
1344
+ prims.iota,
1345
+ prims._low_memory_max_pool2d_offsets_to_indices,
1346
+ ] # noqa: E501,B950
1347
+ # Natalia said that we should allow recomputing indexing :)
1348
+ default_recomputable_ops += [aten.index, aten.gather]
1349
+ default_recomputable_ops += view_ops
1350
+
1351
+ default_recomputable_ops += pointwise_ops()
1352
+
1353
+ default_recomputable_ops += [
1354
+ aten.zeros_like,
1355
+ ]
1356
+
1357
+ default_recomputable_ops += [method_to_operator(m) for m in magic_methods]
1358
+ recomputable_ops = set(default_recomputable_ops)
1359
+
1360
+ random_ops = [aten.native_dropout, aten.rand_like, aten.randn_like]
1361
+ compute_intensive_ops = [
1362
+ aten.mm,
1363
+ aten.convolution,
1364
+ aten.convolution_backward,
1365
+ aten.bmm,
1366
+ aten.addmm,
1367
+ aten._scaled_dot_product_flash_attention,
1368
+ aten._scaled_dot_product_efficient_attention,
1369
+ aten._flash_attention_forward,
1370
+ aten._efficient_attention_forward,
1371
+ aten.upsample_bilinear2d,
1372
+ aten._scaled_mm,
1373
+ ] # noqa: E501,B950
1374
+
1375
+ fusible_ops = recomputable_ops | set(random_ops)
1376
+ return OpTypes(
1377
+ set(fusible_ops),
1378
+ set(compute_intensive_ops),
1379
+ set(random_ops),
1380
+ set(view_ops),
1381
+ set(recomputable_ops),
1382
+ )
1383
+
1384
+
1385
+ def get_name_to_node(graph: fx.Graph):
1386
+ name_to_node = {}
1387
+ for node in graph.nodes:
1388
+ name_to_node[node.name] = node
1389
+ return name_to_node
1390
+
1391
+
1392
+ def _optimize_runtime_with_given_memory(
1393
+ joint_graph: fx.Graph,
1394
+ memory: List[float],
1395
+ runtimes: List[float],
1396
+ max_memory: float,
1397
+ node_info: NodeInfo,
1398
+ all_recomputable_banned_nodes: List[fx.Node],
1399
+ ) -> Tuple[float, List[int], List[int]]:
1400
+ SOLVER = config.activation_memory_budget_solver
1401
+ if SOLVER == "greedy":
1402
+ return greedy_knapsack(memory, runtimes, max_memory)
1403
+ elif SOLVER == "ilp":
1404
+ return ilp_knapsack(memory, runtimes, max_memory)
1405
+ elif SOLVER == "dp":
1406
+ return dp_knapsack(memory, runtimes, max_memory)
1407
+ elif callable(SOLVER):
1408
+ saved_node_idx, recomp_node_idx = SOLVER(
1409
+ memory, joint_graph, max_memory, node_info, all_recomputable_banned_nodes
1410
+ )
1411
+ return (0.0, saved_node_idx, recomp_node_idx)
1412
+ else:
1413
+ raise RuntimeError(f"Not aware of memory budget knapsack solver: {SOLVER}")
1414
+
1415
+
1416
+ from torch.utils._mode_utils import no_dispatch
1417
+
1418
+
1419
+ def estimate_runtime(node):
1420
+ RUNTIME_MODE = config.activation_memory_budget_runtime_estimator
1421
+
1422
+ def materialize_arg(x):
1423
+ if isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.Tensor):
1424
+ shape = list(x.meta["val"].shape)
1425
+
1426
+ def realize_symbol(d):
1427
+ return hint_int(d, fallback=4096)
1428
+
1429
+ shape = [realize_symbol(s) for s in shape]
1430
+ return x.meta["val"].new_empty_strided(
1431
+ shape, stride=x.meta["tensor_meta"].stride
1432
+ )
1433
+ elif isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.SymInt):
1434
+ return hint_int(x.meta["val"], fallback=4096)
1435
+ elif isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.SymFloat):
1436
+ return 1.0
1437
+ elif isinstance(x, fx.Node) and isinstance(x.meta["val"], torch.SymBool):
1438
+ return True
1439
+ else:
1440
+ return x
1441
+
1442
+ if RUNTIME_MODE == "testing":
1443
+ return 1
1444
+
1445
+ elif RUNTIME_MODE == "profile":
1446
+ with no_dispatch():
1447
+ from torch._inductor.runtime.benchmarking import benchmarker
1448
+
1449
+ args, kwargs = pytree.tree_map(materialize_arg, (node.args, node.kwargs))
1450
+ ms = benchmarker.benchmark_gpu(lambda: node.target(*args, **kwargs))
1451
+ return ms
1452
+
1453
+ elif RUNTIME_MODE == "flops":
1454
+ # todo(chilli): Normalize this to also return ms
1455
+ from torch.utils.flop_counter import FlopCounterMode
1456
+
1457
+ args, kwargs = pytree.tree_map(materialize_arg, (node.args, node.kwargs))
1458
+ with FlopCounterMode(display=False) as mode:
1459
+ node.target(*args, **kwargs)
1460
+ counted_flops = mode.get_total_flops()
1461
+ return max(counted_flops, 1)
1462
+ else:
1463
+ raise RuntimeError(f"Not aware of runtime estimator: {RUNTIME_MODE}")
1464
+
1465
+
1466
+ def choose_saved_values_set(
1467
+ joint_graph: fx.Graph,
1468
+ node_info: NodeInfo,
1469
+ memory_budget=1,
1470
+ ) -> List[fx.Node]:
1471
+ if memory_budget > 1 or memory_budget < 0:
1472
+ raise RuntimeError(
1473
+ f"The valid ranges for memory budget are 0 <= m <= 1. The provided value is {memory_budget}"
1474
+ )
1475
+ min_cut_options = MinCutOptions(
1476
+ ban_if_used_far_apart=config.ban_recompute_used_far_apart,
1477
+ ban_if_long_fusible_chains=config.ban_recompute_long_fusible_chains,
1478
+ ban_if_materialized_backward=config.ban_recompute_materialized_backward,
1479
+ ban_if_not_in_allowlist=config.ban_recompute_not_in_allowlist,
1480
+ ban_if_reduction=config.ban_recompute_reductions,
1481
+ )
1482
+
1483
+ if config.aggressive_recomputation:
1484
+ min_cut_options = replace(
1485
+ min_cut_options,
1486
+ ban_if_used_far_apart=False,
1487
+ ban_if_long_fusible_chains=False,
1488
+ ban_if_materialized_backward=False,
1489
+ ban_if_not_in_allowlist=False,
1490
+ )
1491
+ if memory_budget == 0:
1492
+ return node_info.inputs
1493
+
1494
+ runtime_optimized_saved_values, _ = solve_min_cut(
1495
+ joint_graph,
1496
+ node_info,
1497
+ min_cut_options,
1498
+ )
1499
+ # return runtime_optimized_saved_values
1500
+ if memory_budget == 1:
1501
+ return runtime_optimized_saved_values
1502
+
1503
+ def estimate_activations_size(saved_values: List[fx.Node]) -> float:
1504
+ return sum(map(_size_of, saved_values)) / 1e9
1505
+
1506
+ min_act_size = estimate_activations_size(node_info.inputs)
1507
+ max_act_size = estimate_activations_size(runtime_optimized_saved_values)
1508
+ # The optimized choice is smaller than the inputs anyways
1509
+ if max_act_size <= min_act_size:
1510
+ return runtime_optimized_saved_values
1511
+
1512
+ def get_normalized_size(sz):
1513
+ return (sz / 1e9) / (max_act_size - min_act_size)
1514
+
1515
+ def get_mem_ratio(activations: List[fx.Node]):
1516
+ return (estimate_activations_size(activations) - min_act_size) / (
1517
+ max_act_size - min_act_size
1518
+ )
1519
+
1520
+ more_aggressive_options = replace(
1521
+ min_cut_options,
1522
+ ban_if_used_far_apart=False,
1523
+ ban_if_long_fusible_chains=False,
1524
+ ban_if_materialized_backward=False,
1525
+ )
1526
+ more_aggressive_saved_values, _ = solve_min_cut(
1527
+ joint_graph, node_info, more_aggressive_options
1528
+ )
1529
+ if get_mem_ratio(more_aggressive_saved_values) < memory_budget:
1530
+ return more_aggressive_saved_values
1531
+
1532
+ aggressive_options = replace(
1533
+ more_aggressive_options,
1534
+ ban_if_not_in_allowlist=False,
1535
+ )
1536
+ aggressive_recomputation_saved_values, banned_nodes = solve_min_cut(
1537
+ joint_graph, node_info, aggressive_options
1538
+ )
1539
+
1540
+ if get_mem_ratio(aggressive_recomputation_saved_values) < memory_budget:
1541
+ return aggressive_recomputation_saved_values
1542
+
1543
+ from torch._inductor.fx_utils import get_node_storage
1544
+
1545
+ input_storages = {get_node_storage(node) for node in node_info.inputs}
1546
+
1547
+ def get_recomputable_banned_nodes(banned_nodes: Set[fx.Node]) -> List[fx.Node]:
1548
+ return [
1549
+ i
1550
+ for i in banned_nodes
1551
+ if (
1552
+ # Only allow recomputing nodes that are actually required for BW
1553
+ i.dist_from_bw < int(1e9) # type: ignore[attr-defined]
1554
+ and get_node_storage(i) not in input_storages
1555
+ )
1556
+ ]
1557
+
1558
+ recomputable_banned_nodes = get_recomputable_banned_nodes(banned_nodes)
1559
+ # sort first by name, to ensure determinism when multiple nodes have same size
1560
+ recomputable_banned_nodes = sorted(recomputable_banned_nodes, key=lambda x: x.name)
1561
+
1562
+ # default: runtime_optimized_saved_values
1563
+ # more aggressive: more_aggressive_saved_values
1564
+ # full aggressive: aggressive_recomputation_saved_values
1565
+
1566
+ all_recomputable_banned_nodes = sorted(
1567
+ recomputable_banned_nodes, key=_size_of, reverse=True
1568
+ )
1569
+ if len(all_recomputable_banned_nodes) == 0:
1570
+ return node_info.inputs
1571
+ memories_banned_nodes = [
1572
+ get_normalized_size(_size_of(i)) for i in all_recomputable_banned_nodes
1573
+ ]
1574
+ runtimes_banned_nodes = [
1575
+ estimate_runtime(node) for node in all_recomputable_banned_nodes
1576
+ ]
1577
+ from torch.utils._mode_utils import no_dispatch
1578
+
1579
+ def get_saved_values_knapsack(memory_budget, node_info, joint_graph):
1580
+ with no_dispatch():
1581
+ (
1582
+ expected_runtime,
1583
+ saved_node_idxs,
1584
+ recomputable_node_idxs,
1585
+ ) = _optimize_runtime_with_given_memory(
1586
+ joint_graph,
1587
+ memories_banned_nodes,
1588
+ runtimes_banned_nodes,
1589
+ max(memory_budget, 0),
1590
+ node_info,
1591
+ all_recomputable_banned_nodes,
1592
+ )
1593
+ if AOT_PARTITIONER_DEBUG:
1594
+ max_runtime = max(
1595
+ runtimes_banned_nodes
1596
+ ) # For normalizing runtimes in logs
1597
+ input_summary = [
1598
+ f"\n\t\t\t{index}, {memory}, {runtime / max_runtime}, {node.op}, {node.target}, {node.meta}, {node.args}"
1599
+ for index, (memory, runtime, node) in enumerate(
1600
+ zip(
1601
+ memories_banned_nodes,
1602
+ runtimes_banned_nodes,
1603
+ all_recomputable_banned_nodes,
1604
+ )
1605
+ )
1606
+ ]
1607
+ joint_graph_nodes = [node.name for node in joint_graph.nodes]
1608
+ joint_graph_edges = [
1609
+ (inp.name, node.name)
1610
+ for node in joint_graph.nodes
1611
+ for inp in node.all_input_nodes
1612
+ ]
1613
+ knapsack_summary = f"""
1614
+ Activation Checkpointing - Knapsack Problem Summary:
1615
+ Input:
1616
+ Solver: {config.activation_memory_budget_solver}
1617
+ Max Memory: {max(config.activation_memory_budget, 0)}
1618
+ Graph Nodes: {joint_graph_nodes}
1619
+ Graph Edges: {joint_graph_edges}
1620
+ (Index, Memory, Runtime, Node.Op, Node.Target, Metadata): {"".join(input_summary)}
1621
+ Output:
1622
+ Expected Runtime: {expected_runtime}
1623
+ Saved Nodes: {saved_node_idxs}
1624
+ Recomputable Nodes: {recomputable_node_idxs}
1625
+ """
1626
+ torch._logging.trace_structured(
1627
+ name="artifact",
1628
+ payload_fn=lambda: knapsack_summary,
1629
+ )
1630
+ log.info(knapsack_summary)
1631
+ dont_ban = set()
1632
+ for idx in recomputable_node_idxs:
1633
+ # if idx in all_recomputable_banned_nodes:
1634
+ try:
1635
+ dont_ban.add(all_recomputable_banned_nodes[idx])
1636
+ except BaseException:
1637
+ pass
1638
+
1639
+ assert dont_ban.issubset(all_recomputable_banned_nodes)
1640
+
1641
+ saved_values, _ = solve_min_cut(
1642
+ joint_graph,
1643
+ node_info,
1644
+ aggressive_options,
1645
+ dont_ban,
1646
+ )
1647
+ return saved_values, expected_runtime
1648
+
1649
+ if config.visualize_memory_budget_pareto:
1650
+ options = []
1651
+ for sweep_memory_budget in range(100, -1, -5):
1652
+ saved_values, expected_runtime = get_saved_values_knapsack(
1653
+ sweep_memory_budget / 100, node_info=node_info, joint_graph=joint_graph
1654
+ )
1655
+ options.append(
1656
+ (
1657
+ sweep_memory_budget,
1658
+ sum(runtimes_banned_nodes) - expected_runtime,
1659
+ get_mem_ratio(saved_values),
1660
+ )
1661
+ )
1662
+
1663
+ import matplotlib.pyplot as plt
1664
+
1665
+ x_values = [item[2] for item in options]
1666
+ y_values = [item[1] for item in options]
1667
+
1668
+ # Plotting the values with updated axis labels and chart title
1669
+ plt.figure(figsize=(10, 6))
1670
+ plt.plot(x_values, y_values, marker="o")
1671
+
1672
+ # Adding labels for each point
1673
+ for i, txt in enumerate(x_values):
1674
+ plt.annotate(
1675
+ f"{txt:.2f}",
1676
+ (txt, y_values[i]),
1677
+ textcoords="offset points",
1678
+ xytext=(0, 10),
1679
+ ha="center",
1680
+ )
1681
+
1682
+ plt.xlabel("Memory Budget")
1683
+ plt.ylabel("Runtime of Recomputed Components")
1684
+ plt.title("Pareto Frontier of Memory Budget vs. Recomputation Runtime")
1685
+ plt.grid(True)
1686
+ fig = plt.gcf()
1687
+ plt.show()
1688
+ fig_name = f"memory_budget_pareto_{get_aot_graph_name()}.png"
1689
+ fig.savefig(fig_name)
1690
+ log.warning("Generated Pareto frontier curve at %s", fig_name)
1691
+
1692
+ # todo(chilli): Estimated doesn't align exactly with actual - actual is
1693
+ # usually less memory than estimated. i'm guessing (actually quite
1694
+ # unsure about this) that's because estimated is just only including
1695
+ # tensors we actually banned from recompute, but there may be other
1696
+ # tensors that we choose to save.
1697
+
1698
+ return get_saved_values_knapsack(
1699
+ memory_budget=memory_budget, node_info=node_info, joint_graph=joint_graph
1700
+ )[0]
1701
+
1702
+
1703
+ def min_cut_rematerialization_partition(
1704
+ joint_module: fx.GraphModule,
1705
+ _joint_inputs,
1706
+ compiler="inductor",
1707
+ *,
1708
+ num_fwd_outputs,
1709
+ ) -> Tuple[fx.GraphModule, fx.GraphModule]:
1710
+ """
1711
+ Partitions the joint graph such that the backward recomputes the forward.
1712
+ Recomputing helps in trading off memory bandwidth with computation.
1713
+
1714
+ To create the fwd and bwd graph, we copy the joint graph, manually set the
1715
+ outputs to just original forward or backward outputs. And then we run the
1716
+ resulting graphs through dead code elimination.
1717
+
1718
+ .. warning::
1719
+ This API is experimental and likely to change.
1720
+
1721
+ Args:
1722
+ joint_module(fx.GraphModule): The joint forward and backward graph. This
1723
+ is the result of AOT Autograd tracing.
1724
+ _joint_inputs: The inputs to the joint graph. This is unused.
1725
+ compiler: This option determines the default set of recomputable ops.
1726
+ Currently, there are two options: ``nvfuser`` and ``inductor``.
1727
+ recomputable_ops: This is an optional set of recomputable ops. If this
1728
+ is not None, then this set of ops will be used instead of the
1729
+ default set of ops.
1730
+ num_fwd_outputs: The number of outputs from the forward graph.
1731
+
1732
+ Returns:
1733
+ Returns the generated forward and backward Fx graph modules.
1734
+ """
1735
+
1736
+ joint_module.graph.eliminate_dead_code()
1737
+ joint_module.recompile()
1738
+
1739
+ fx_g = joint_module.graph
1740
+
1741
+ # add the CSE pass
1742
+ if config.cse:
1743
+ cse_graph = fx_graph_cse(fx_g)
1744
+ joint_module.graph = cse_graph
1745
+ joint_graph = joint_module.graph
1746
+
1747
+ graph_has_recomputable_ops = has_recomputable_ops(joint_module)
1748
+ graph_has_recomputable_rng_ops = has_recomputable_rng_ops(joint_module)
1749
+ if graph_has_recomputable_ops:
1750
+ joint_module = cleanup_recompute_tags(joint_module)
1751
+
1752
+ def classify_nodes(joint_module):
1753
+ name_to_node = get_name_to_node(joint_module.graph)
1754
+ required_bw_nodes = set()
1755
+ for node in joint_module.graph.nodes:
1756
+ if node.op == "placeholder" and "tangents" in node.target:
1757
+ required_bw_nodes.add(node)
1758
+ elif _must_be_in_backward(node):
1759
+ required_bw_nodes.add(node)
1760
+
1761
+ if node in required_bw_nodes:
1762
+ for user in node.users:
1763
+ required_bw_nodes.add(user)
1764
+
1765
+ primal_inputs = list(filter(_is_primal, joint_module.graph.nodes))
1766
+ fwd_seed_offset_inputs = list(
1767
+ filter(_is_fwd_seed_offset, joint_module.graph.nodes)
1768
+ )
1769
+ inputs = primal_inputs + fwd_seed_offset_inputs
1770
+ fwd_outputs, bwd_outputs = _extract_fwd_bwd_outputs(
1771
+ joint_module, num_fwd_outputs=num_fwd_outputs
1772
+ )
1773
+ required_bw_nodes.update(
1774
+ o for o in bwd_outputs if o is not None and o.op != "output"
1775
+ )
1776
+ forward_only_graph = _extract_graph_with_inputs_outputs(
1777
+ joint_module.graph, inputs, fwd_outputs, "forward"
1778
+ )
1779
+ required_fw_nodes: Set[fx.Node] = {
1780
+ name_to_node[node.name]
1781
+ for node in forward_only_graph.nodes
1782
+ if node.op != "output"
1783
+ }
1784
+ unclaimed_nodes = {
1785
+ node
1786
+ for node in joint_module.graph.nodes
1787
+ if node not in required_fw_nodes and node not in required_bw_nodes
1788
+ }
1789
+ fw_cnt = 0
1790
+ fw_order = {}
1791
+ for node in joint_module.graph.nodes:
1792
+ if node in required_fw_nodes:
1793
+ fw_order[node] = fw_cnt
1794
+ fw_cnt += 1
1795
+ return NodeInfo(
1796
+ inputs, required_fw_nodes, required_bw_nodes, unclaimed_nodes, fw_order
1797
+ )
1798
+
1799
+ node_info = classify_nodes(joint_module)
1800
+
1801
+ # networkx blows up on graphs with no required backward nodes
1802
+ # Since there's nothing to partition anyway, and the default partitioner can "handle"
1803
+ # this case, send our graph over to the default partitioner.
1804
+ if len(node_info.required_bw_nodes) == 0:
1805
+ return default_partition(
1806
+ joint_module, _joint_inputs, num_fwd_outputs=num_fwd_outputs
1807
+ )
1808
+
1809
+ for node in reversed(joint_module.graph.nodes):
1810
+ if node.op == "output":
1811
+ node.dist_from_bw = int(1e9)
1812
+ elif not node_info.is_required_fw(node):
1813
+ node.dist_from_bw = 0
1814
+ else:
1815
+ node.dist_from_bw = int(1e9)
1816
+ for user in node.users:
1817
+ node.dist_from_bw = min(node.dist_from_bw, user.dist_from_bw + 1)
1818
+
1819
+ memory_budget = config.activation_memory_budget
1820
+ for node in joint_graph.nodes:
1821
+ if isinstance(node.meta.get("memory_budget", None), float):
1822
+ memory_budget = node.meta["memory_budget"]
1823
+ break
1824
+ saved_values = choose_saved_values_set(
1825
+ joint_graph,
1826
+ node_info,
1827
+ memory_budget=memory_budget,
1828
+ )
1829
+ # save_for_backward on tensors and stashes symints in autograd .ctx
1830
+ saved_sym_nodes = list(filter(is_sym_node, saved_values))
1831
+ saved_values = list(filter(lambda n: not is_sym_node(n), saved_values))
1832
+
1833
+ # NB: saved_sym_nodes will be mutated to reflect the actual saved symbols
1834
+ fw_module, bw_module = _extract_fwd_bwd_modules(
1835
+ joint_module,
1836
+ saved_values,
1837
+ saved_sym_nodes=saved_sym_nodes,
1838
+ num_fwd_outputs=num_fwd_outputs,
1839
+ )
1840
+
1841
+ if graph_has_recomputable_ops:
1842
+ if graph_has_recomputable_rng_ops:
1843
+ fw_module, bw_module = functionalize_rng_ops(
1844
+ joint_module, fw_module, bw_module, len(saved_sym_nodes)
1845
+ )
1846
+ bw_module = reordering_to_mimic_autograd_engine(bw_module)
1847
+
1848
+ if AOT_PARTITIONER_DEBUG:
1849
+ # Calculate sorted sizes of saved values
1850
+ sorted_sizes = sorted([(_size_of(i), str(i)) for i in saved_values])
1851
+
1852
+ # Log total theoretical activations stored
1853
+ total_activations_size_gb = sum(_size_of(i) for i in saved_values) / 1e9
1854
+ log.debug("Theoretical Activations Stored: %.2f GB", total_activations_size_gb)
1855
+
1856
+ # Log theoretical per activation storage sizes
1857
+ log.debug("Theoretical Per Activation Storage Sizes: %s", sorted_sizes)
1858
+ fw_module_nodes = {
1859
+ node.name for node in fw_module.graph.nodes if node.op == "call_function"
1860
+ }
1861
+ bw_module_nodes = {
1862
+ node.name for node in bw_module.graph.nodes if node.op == "call_function"
1863
+ }
1864
+ remat_nodes = fw_module_nodes & bw_module_nodes
1865
+
1866
+ counts: Dict[str, int] = defaultdict(int)
1867
+ for node in fw_module.graph.nodes:
1868
+ if node.name in remat_nodes and hasattr(node.target, "_overloadpacket"):
1869
+ counts[str(node.target._overloadpacket)] += 1
1870
+ log.debug(
1871
+ "# remat/fw/bw: %d/%d/%d",
1872
+ len(remat_nodes),
1873
+ len(fw_module_nodes),
1874
+ len(bw_module_nodes),
1875
+ )
1876
+ rematerialized_ops = sorted(counts.items(), key=lambda x: x[1], reverse=True)
1877
+ log.debug("Count of Ops Rematerialized: %s", rematerialized_ops)
1878
+ return fw_module, bw_module
1879
+
1880
+
1881
+ def draw_graph(
1882
+ traced: torch.fx.GraphModule,
1883
+ fname: str,
1884
+ figname: str = "fx_graph",
1885
+ clear_meta: bool = True,
1886
+ prog: Optional[Union[str, List[str]]] = None,
1887
+ parse_stack_trace: bool = False,
1888
+ dot_graph_shape: Optional[str] = None,
1889
+ ) -> None:
1890
+ if clear_meta:
1891
+ new_graph = copy.deepcopy(traced.graph)
1892
+ traced = fx.GraphModule(traced, new_graph)
1893
+ for node in traced.graph.nodes:
1894
+ node.meta = {}
1895
+ base, ext = os.path.splitext(fname)
1896
+ if not ext:
1897
+ ext = "." + config.torch_compile_graph_format
1898
+ log.info("Writing FX graph to file: %s%s", base, ext)
1899
+ g = graph_drawer.FxGraphDrawer(
1900
+ traced,
1901
+ figname,
1902
+ parse_stack_trace=parse_stack_trace,
1903
+ dot_graph_shape=dot_graph_shape,
1904
+ )
1905
+ x = g.get_main_dot_graph()
1906
+ write_method = getattr(x, "write_" + ext.lstrip("."))
1907
+ fname = f"{base}{ext}"
1908
+ if prog is None:
1909
+ write_method(fname)
1910
+ else:
1911
+ write_method(fname, prog=prog)
lib/python3.12/site-packages/torch/_functorch/pyfunctorch.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ import contextlib
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, List, Tuple
5
+
6
+ import torch
7
+ import torch.utils._pytree as pytree
8
+ from torch._C._functorch import (
9
+ CFunctionalizeInterpreterPtr,
10
+ CGradInterpreterPtr,
11
+ CInterpreter,
12
+ CJvpInterpreterPtr,
13
+ CVmapInterpreterPtr,
14
+ pop_dynamic_layer_stack,
15
+ push_dynamic_layer_stack,
16
+ RandomnessType,
17
+ TransformType,
18
+ )
19
+ from torch.autograd.forward_ad import _set_fwd_grad_enabled
20
+
21
+
22
+ """
23
+ This file contains the functorch integration with PyDispatcher.
24
+
25
+ PyDispatcher does not understand functorch's DynamicLayerStack dispatching
26
+ logic because it is entirely implemented in C++ in the fallbacks for two
27
+ dispatch keys, FuncTorchDynamicLayer{Front, Back}Mode (PyDispatcher is unable
28
+ to directly reuse C++ boxed fallbacks).
29
+
30
+ Instead of trying to hammer PyDispatcher into understanding those fallbacks,
31
+ we re-implement the logic of peeking the top of the stack for an interpreter,
32
+ selecting the interpreter to dispatch on, etc, in Python. This leads to a
33
+ simpler design.
34
+
35
+ The main difference between C++ functorch and PyDispatcher's functorch logic
36
+ is that:
37
+ - C++ functorch needs to manually tweak dispatch keys to ping-pong between
38
+ DynamicLayerFrontMode and DynamicLayerBackMode.
39
+ - PyDispatcher's functorch logic pops an Interpreter from the top of the stack
40
+ and asks it to execute the rule associated with the Interpreter.
41
+
42
+ In C++ we do the ping-pong because e.g. vmap rules are associated with the
43
+ batched DispatchKey, but in PyDispatcher we are able to avoid this by asking
44
+ the user to register a batching rule directly to a transform that an
45
+ interpreter then invokes.
46
+ """
47
+
48
+
49
+ # FuncTorchInterpreter is the Python version of Interpreter (recall that
50
+ # the DynamicLayerStack is a stack of interpreters).
51
+ # It is a wrapper around the actual C++ Interpreter object.
52
+ #
53
+ # Keep the methods in sync with aten/src/ATen/functorch/Interpreter.h
54
+ class FuncTorchInterpreter(ABC):
55
+ def __init__(self, cptr: Any):
56
+ self._cptr = cptr
57
+
58
+ # Process an operation. eg for vmap, this is invoking a batching rule.
59
+ # Conceptually this is analogous to Interpreter::process in C++
60
+ @abstractmethod
61
+ def process(self, op, args, kwargs):
62
+ pass
63
+
64
+ # lower an operation from this Interpreter to the next Interpreter on the stack.
65
+ # Concretely, this involves temporarily popping the current Interpreter.
66
+ # Conceptually this is analogous to Interpreter::sendToNextInterpreter in C++
67
+ def lower(self):
68
+ return temporarily_pop_interpreter_stack()
69
+
70
+ def level(self):
71
+ return self._cptr.level()
72
+
73
+ def key(self):
74
+ return self._cptr.key()
75
+
76
+ def get_state(self):
77
+ raise NotImplementedError
78
+
79
+ def check_state(self, state):
80
+ return state == self.get_state()
81
+
82
+
83
+ @contextlib.contextmanager
84
+ def temporarily_pop_interpreter_stack():
85
+ try:
86
+ saved = pop_dynamic_layer_stack()
87
+ yield
88
+ finally:
89
+ push_dynamic_layer_stack(saved)
90
+
91
+
92
+ @contextlib.contextmanager
93
+ def temporarily_clear_interpreter_stack():
94
+ stack = []
95
+ try:
96
+ while torch._C._functorch.peek_interpreter_stack() is not None:
97
+ stack.append(pop_dynamic_layer_stack())
98
+ yield list(stack)
99
+ finally:
100
+ while stack:
101
+ push_dynamic_layer_stack(stack.pop())
102
+
103
+
104
+ @contextlib.contextmanager
105
+ def temporarily_restore_interpreter_stack(stack):
106
+ pushed = []
107
+ try:
108
+ for s in reversed(stack):
109
+ push_dynamic_layer_stack(s)
110
+ pushed.append(s)
111
+ yield
112
+ finally:
113
+ for s in reversed(pushed):
114
+ # TODO: would be nice to assert that the layers are the same, but
115
+ # Python object identity is not preserved
116
+ pop_dynamic_layer_stack()
117
+
118
+
119
+ class VmapInterpreter(FuncTorchInterpreter):
120
+ def __init__(self, cdata: CInterpreter):
121
+ assert cdata.key() == TransformType.Vmap
122
+ # NOTE: [Interpreter cdata vs cptr]
123
+ # cdata is a generic CInterpreter. We wrap it in a CVmapInterpreterPtr
124
+ # so that we can access methods specific to the vmap interpreter
125
+ self._cdata = cdata
126
+ self._cptr = CVmapInterpreterPtr(cdata)
127
+
128
+ def process(self, op, args, kwargs):
129
+ kernel = op.functorch_table[TransformType.Vmap]
130
+ return kernel(self, *args, **kwargs)
131
+
132
+ def batch_size(self):
133
+ return self._cptr.batchSize()
134
+
135
+ def randomness(self):
136
+ typ = self._cptr.randomness()
137
+ if typ == RandomnessType.Error:
138
+ return "error"
139
+ elif typ == RandomnessType.Same:
140
+ return "same"
141
+ elif typ == RandomnessType.Different:
142
+ return "different"
143
+ raise RuntimeError(f"Unknown RandomnessType: {typ}")
144
+
145
+ def get_state(self):
146
+ return (self.key().name, self.level(), self.randomness())
147
+
148
+
149
+ @contextlib.contextmanager
150
+ def nested(*contexts):
151
+ with contextlib.ExitStack() as stack:
152
+ for ctx in contexts:
153
+ stack.enter_context(ctx)
154
+ yield contexts
155
+
156
+
157
+ class GradInterpreter(FuncTorchInterpreter):
158
+ def __init__(self, cdata: CInterpreter):
159
+ assert cdata.key() == TransformType.Grad
160
+ # See NOTE: [Interpreter cdata vs cptr]
161
+ self._cdata = cdata
162
+ self._cptr = CGradInterpreterPtr(cdata)
163
+
164
+ def lift(self, args, kwargs):
165
+ args, kwargs = pytree.tree_map_only(
166
+ torch.Tensor, self._cptr.lift, [args, kwargs]
167
+ )
168
+ return args, kwargs
169
+
170
+ def process(self, op, args, kwargs):
171
+ kernel = op.functorch_table[TransformType.Grad]
172
+ args, kwargs = self.lift(args, kwargs)
173
+ return kernel(self, *args, **kwargs)
174
+
175
+ # GradInterpreter has custom lower because of the no_grad interaction
176
+ # See NOTE [grad and vjp interaction with no_grad]
177
+ # This logic is mirrored from C++ GradInterpreterPtr::sendToNextInterpreter
178
+ def lower(self):
179
+ prev_grad_mode = self.prev_grad_mode()
180
+ if not prev_grad_mode:
181
+ return nested(torch.no_grad(), super().lower())
182
+ return super().lower()
183
+
184
+ def prev_grad_mode(self):
185
+ return self._cptr.prevGradMode()
186
+
187
+ def get_state(self):
188
+ return (self.key().name, self.level(), self.prev_grad_mode())
189
+
190
+
191
+ class JvpInterpreter(FuncTorchInterpreter):
192
+ def __init__(self, cdata: CInterpreter):
193
+ assert cdata.key() == TransformType.Jvp
194
+ # See NOTE: [Interpreter cdata vs cptr]
195
+ self._cdata = cdata
196
+ self._cptr = CJvpInterpreterPtr(cdata)
197
+
198
+ def lift(self, args, kwargs):
199
+ args, kwargs = pytree.tree_map_only(
200
+ torch.Tensor, self._cptr.lift, [args, kwargs]
201
+ )
202
+ return args, kwargs
203
+
204
+ def process(self, op, args, kwargs):
205
+ kernel = op.functorch_table[TransformType.Jvp]
206
+ args, kwargs = self.lift(args, kwargs)
207
+ return kernel(self, *args, **kwargs)
208
+
209
+ # Jvp has custom lower because of the no_fwd_grad interaction
210
+ # See NOTE [grad and vjp interaction with no_grad] for related info.
211
+ # This logic is mirrored from C++ JvpInterpreterPtr::sendToNextInterpreter
212
+ def lower(self):
213
+ prev_fwd_grad_mode = self.prev_fwd_grad_mode()
214
+ if not prev_fwd_grad_mode:
215
+ return nested(_set_fwd_grad_enabled(False), super().lower())
216
+ return super().lower()
217
+
218
+ def prev_fwd_grad_mode(self):
219
+ return self._cptr.prevFwdGradMode()
220
+
221
+ def get_state(self):
222
+ return (self.key().name, self.level(), self.prev_fwd_grad_mode())
223
+
224
+
225
+ class FunctionalizeInterpreter(FuncTorchInterpreter):
226
+ def __init__(self, cdata: CInterpreter):
227
+ assert cdata.key() == TransformType.Functionalize
228
+ self._cdata = cdata
229
+ self._cptr = CFunctionalizeInterpreterPtr(cdata)
230
+
231
+ def process(self, op, args, kwargs):
232
+ kernel = op.functorch_table[TransformType.Functionalize]
233
+ return kernel(self, *args, **kwargs)
234
+
235
+ def functionalize_add_back_views(self):
236
+ return self._cptr.functionalizeAddBackViews()
237
+
238
+ def get_state(self):
239
+ return (self.key().name, self.level())
240
+
241
+
242
+ def coerce_cinterpreter(cinterpreter: CInterpreter) -> FuncTorchInterpreter:
243
+ key = cinterpreter.key()
244
+ if key == TransformType.Grad:
245
+ return GradInterpreter(cinterpreter)
246
+ if key == TransformType.Vmap:
247
+ return VmapInterpreter(cinterpreter)
248
+ if key == TransformType.Jvp:
249
+ return JvpInterpreter(cinterpreter)
250
+ if key == TransformType.Functionalize:
251
+ return FunctionalizeInterpreter(cinterpreter)
252
+ raise RuntimeError(f"NYI: PyDispatcher has not implemented support for {key}")
253
+
254
+
255
+ def retrieve_current_functorch_interpreter() -> FuncTorchInterpreter:
256
+ interpreter = torch._C._functorch.peek_interpreter_stack()
257
+ assert interpreter is not None
258
+ return coerce_cinterpreter(interpreter)
259
+
260
+
261
+ def retrieve_all_functorch_interpreters() -> List[FuncTorchInterpreter]:
262
+ cis = torch._C._functorch.get_interpreter_stack()
263
+ if cis is None:
264
+ return []
265
+ return [coerce_cinterpreter(ci) for ci in cis]
266
+
267
+
268
+ def compare_functorch_state(states: List[Tuple[Any, ...]]) -> bool:
269
+ # There are four possible cases covered here:
270
+ # 1. Current stack empty AND stack when generated not empty -> Invalidate
271
+ # 2. Current stack not empty AND stack when generated empty -> Invalidate
272
+ # 3. Current stack and generated stack empty -> Valid FX graph
273
+ # 4. Current stack and generated stack not empty -> Valid if both states match
274
+ peek = torch._C._functorch.peek_interpreter_stack()
275
+ if (peek is None and len(states) != 0) or (peek is not None and len(states) == 0):
276
+ return False
277
+
278
+ cis = retrieve_all_functorch_interpreters()
279
+ return len(cis) == len(states) and all(
280
+ ci.check_state(state) for ci, state in zip(cis, states)
281
+ )
282
+
283
+
284
+ def dispatch_functorch(op, args, kwargs):
285
+ interpreter = retrieve_current_functorch_interpreter()
286
+ # In traditional PyTorch operators, DispatchKey::FuncTorchTensorWrapper's
287
+ # unwrap_dead_tensors fallback handles unwrapping dead tensor wrappers.
288
+ # PyDispatcher sidesteps the PyTorch dispatcher when dealing with functorch
289
+ # transforms, so we manually unwrap the dead tensors here.
290
+ # This logic won't need to exist when we have mode-only functorch.
291
+ args, kwargs = pytree.tree_map_only(
292
+ torch.Tensor, torch._C._functorch.unwrap_if_dead, (args, kwargs)
293
+ )
294
+ return interpreter.process(op, args, kwargs)