Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/torch/_functorch/__init__.py +5 -0
- lib/python3.12/site-packages/torch/_functorch/__pycache__/autograd_function.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/__pycache__/config.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__init__.py +5 -0
- lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/__pycache__/knapsack.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_activation_checkpointing/knapsack.py +123 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__init__.py +5 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/autograd_cache.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/collect_metadata_analysis.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/dispatch_and_compile_graph.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/functional_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/input_output_analysis.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/jit_compile_runtime_wrappers.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/logging_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/runtime_wrappers.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/schemas.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/subclass_parametrization.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/subclass_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/traced_function_transforms.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/__pycache__/utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/autograd_cache.py +904 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/collect_metadata_analysis.py +813 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/dispatch_and_compile_graph.py +338 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/functional_utils.py +528 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/input_output_analysis.py +423 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/jit_compile_runtime_wrappers.py +907 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/logging_utils.py +147 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/runtime_wrappers.py +2202 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/schemas.py +911 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/subclass_parametrization.py +74 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/subclass_utils.py +453 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/traced_function_transforms.py +897 -0
- lib/python3.12/site-packages/torch/_functorch/_aot_autograd/utils.py +492 -0
- lib/python3.12/site-packages/torch/_functorch/aot_autograd.py +1664 -0
- lib/python3.12/site-packages/torch/_functorch/apis.py +449 -0
- lib/python3.12/site-packages/torch/_functorch/autograd_function.py +764 -0
- lib/python3.12/site-packages/torch/_functorch/batch_norm_replacement.py +29 -0
- lib/python3.12/site-packages/torch/_functorch/benchmark_utils.py +231 -0
- lib/python3.12/site-packages/torch/_functorch/compile_utils.py +176 -0
- lib/python3.12/site-packages/torch/_functorch/compilers.py +445 -0
- lib/python3.12/site-packages/torch/_functorch/config.py +219 -0
- lib/python3.12/site-packages/torch/_functorch/deprecated.py +172 -0
- lib/python3.12/site-packages/torch/_functorch/eager_transforms.py +1800 -0
- lib/python3.12/site-packages/torch/_functorch/functional_call.py +253 -0
- lib/python3.12/site-packages/torch/_functorch/fx_minifier.py +501 -0
- lib/python3.12/site-packages/torch/_functorch/make_functional.py +617 -0
- lib/python3.12/site-packages/torch/_functorch/partitioners.py +1911 -0
- 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)
|