anhtld commited on
Commit
fa56a4b
·
verified ·
1 Parent(s): 1a23920

Auto-sync: 2026-06-25 22:43:25 (part 17)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/memory.py +1108 -0
  2. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/metrics.py +484 -0
  3. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_ir.py +1361 -0
  4. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_lowerings.py +1399 -0
  5. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mock_cache.py +274 -0
  6. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/ops_handler.py +1207 -0
  7. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/optimize_indexing.py +126 -0
  8. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/output_code.py +1202 -0
  9. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/__init__.py +1 -0
  10. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/build_package.py +15 -0
  11. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/package.py +138 -0
  12. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/pattern_matcher.py +2495 -0
  13. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/quantized_lowerings.py +171 -0
  14. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_cache.py +432 -0
  15. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_gemm_autotune_cache.py +20 -0
  16. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/rocm_multiarch_utils.py +310 -0
  17. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/__init__.py +0 -0
  18. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/autotune_cache.py +667 -0
  19. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/benchmarking.py +549 -0
  20. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/cache_dir_utils.py +54 -0
  21. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/__init__.py +32 -0
  22. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/config.py +167 -0
  23. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/context.py +310 -0
  24. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/encoders.py +68 -0
  25. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/exceptions.py +96 -0
  26. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/implementations.py +422 -0
  27. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/interfaces.py +1280 -0
  28. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/locks.py +207 -0
  29. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/memoizers.py +7 -0
  30. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/utils.py +66 -0
  31. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/compile_tasks.py +150 -0
  32. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/coordinate_descent_tuner.py +412 -0
  33. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/debug_utils.py +138 -0
  34. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/halide_helpers.py +155 -0
  35. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/hints.py +236 -0
  36. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/proton_utils.py +325 -0
  37. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/runtime_utils.py +829 -0
  38. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/static_triton_launcher.py +351 -0
  39. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_compat.py +186 -0
  40. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_helpers.py +850 -0
  41. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_heuristics.py +0 -0
  42. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_lazy_compile.py +243 -0
  43. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/scheduler.py +0 -0
  44. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/script.ld +8 -0
  45. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/select_algorithm.py +0 -0
  46. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/shape_propagation.py +148 -0
  47. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/sizevars.py +1193 -0
  48. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/standalone_compile.py +491 -0
  49. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_constants.py +5 -0
  50. outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_utils.py +39 -0
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/memory.py ADDED
@@ -0,0 +1,1108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import collections
4
+ import dataclasses
5
+ import heapq
6
+ import logging
7
+ from typing import TYPE_CHECKING, TypedDict
8
+
9
+ import torch
10
+ from torch._environment import is_fbcode
11
+ from torch._utils_internal import signpost_event
12
+ from torch.utils._ordered_set import OrderedSet
13
+
14
+ from . import config
15
+ from .ir import MultiOutputLayout, NoneLayout
16
+ from .utils import get_dtype_size, is_nonfreeable_buffers
17
+ from .virtualized import V
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Callable
22
+
23
+ from .dependencies import Dep
24
+ from .scheduler import BaseSchedulerNode, SchedulerBuffer
25
+
26
+ from .dependencies import WeakDep
27
+
28
+
29
+ torch_log = logging.getLogger(__name__)
30
+
31
+
32
+ @dataclasses.dataclass
33
+ class PeakMemoryResult:
34
+ order: list[BaseSchedulerNode]
35
+ peak_memory: int
36
+ method: str
37
+
38
+
39
+ @dataclasses.dataclass
40
+ class MemoryPlanningInfoForBuffer:
41
+ size_alloc: int = 0
42
+ size_free: int = 0
43
+ # succ_nodes used for buffer lifetime/freeing (excludes is_fake WeakDeps)
44
+ succ_nodes: OrderedSet[BaseSchedulerNode] = dataclasses.field(
45
+ default_factory=OrderedSet
46
+ )
47
+ # succ_nodes used for node ordering (includes is_fake WeakDeps)
48
+ succ_nodes_for_ordering: OrderedSet[BaseSchedulerNode] = dataclasses.field(
49
+ default_factory=OrderedSet
50
+ )
51
+
52
+ def __post_init__(self) -> None:
53
+ torch._check(
54
+ len(self.succ_nodes) <= len(self.succ_nodes_for_ordering),
55
+ lambda: f"succ_nodes must be a subset of succ_nodes_for_ordering. "
56
+ f"len(succ_nodes)={len(self.succ_nodes)}, len(succ_nodes_for_ordering)={len(self.succ_nodes_for_ordering)}",
57
+ )
58
+
59
+
60
+ @dataclasses.dataclass
61
+ class MemoryPlanningInfoForNode:
62
+ index: int = 0
63
+ size: int = 0
64
+ pred_buffers: OrderedSet[SchedulerBuffer | FreeableInputBuffer] = dataclasses.field(
65
+ default_factory=OrderedSet
66
+ )
67
+ pred_nodes: OrderedSet[BaseSchedulerNode] = dataclasses.field(
68
+ default_factory=OrderedSet
69
+ )
70
+ succ_nodes: OrderedSet[BaseSchedulerNode] = dataclasses.field(
71
+ default_factory=OrderedSet
72
+ )
73
+
74
+
75
+ @dataclasses.dataclass
76
+ class FreeableInputBuffer:
77
+ name: str
78
+ mpi_buffer: MemoryPlanningInfoForBuffer = dataclasses.field(
79
+ default_factory=MemoryPlanningInfoForBuffer
80
+ )
81
+
82
+ def get_name(self) -> str:
83
+ return self.name
84
+
85
+ def __hash__(self) -> int:
86
+ return hash(self.name)
87
+
88
+
89
+ def get_freeable_input_buf(
90
+ nodes: list[BaseSchedulerNode],
91
+ graph_inputs: OrderedSet[str],
92
+ ) -> dict[str, FreeableInputBuffer]:
93
+ """
94
+ Create and keep track of all input buffers that can be freed during the program
95
+
96
+ Returns:
97
+ A dictionary containing all freeable input buffers, keyed by their names.
98
+ """
99
+
100
+ def _dep_size_hint(dep: Dep) -> int:
101
+ return V.graph.get_dep_size_hint(dep)
102
+
103
+ # get freeable input buffers' successor nodes for memory lifetime (excludes is_fake WeakDeps)
104
+ # and for ordering (includes all deps)
105
+ dep_name_to_succ_nodes: dict[str, OrderedSet[BaseSchedulerNode]] = (
106
+ collections.defaultdict(OrderedSet)
107
+ )
108
+ dep_name_to_succ_nodes_for_ordering: dict[str, OrderedSet[BaseSchedulerNode]] = (
109
+ collections.defaultdict(OrderedSet)
110
+ )
111
+ dep_name_to_size: dict[str, int] = dict()
112
+
113
+ for node in nodes:
114
+ for dep in node.read_writes.reads:
115
+ if dep.name in graph_inputs:
116
+ if not is_nonfreeable_buffers(dep):
117
+ # All deps contribute to ordering, but fake weak deps do not contribute to
118
+ # memory liveness
119
+ dep_name_to_succ_nodes_for_ordering[dep.name].add(node)
120
+ dep_name_to_size[dep.name] = _dep_size_hint(dep)
121
+ if not (isinstance(dep, WeakDep) and dep.is_fake):
122
+ dep_name_to_succ_nodes[dep.name].add(node)
123
+
124
+ # create FreeableInputBuffer objects and add them to the returned dictionary
125
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer] = dict()
126
+ for dep_name in dep_name_to_succ_nodes_for_ordering:
127
+ name_to_freeable_input_buf[dep_name] = FreeableInputBuffer(
128
+ dep_name,
129
+ MemoryPlanningInfoForBuffer(
130
+ size_free=dep_name_to_size[dep_name],
131
+ succ_nodes=dep_name_to_succ_nodes[dep_name],
132
+ succ_nodes_for_ordering=dep_name_to_succ_nodes_for_ordering[dep_name],
133
+ ),
134
+ )
135
+ return name_to_freeable_input_buf
136
+
137
+
138
+ def compute_size_for_scheduler_buffer(
139
+ name_to_buf: dict[str, SchedulerBuffer],
140
+ ) -> dict[str, tuple[int, int]]:
141
+ """
142
+ Compute the size of each scheduler buffer, including (1) memory allocated when
143
+ it is created and (2) memory deallocated when it is freed.
144
+
145
+ We specially handle the case of MultiOutputLayout.
146
+ Consider the following case:
147
+ buf0 = some_ops_with_multi_outputs(...)
148
+ buf1 = buf0[0] # assume 10 bytes
149
+ buf2 = buf0[1] # assume 20 bytes
150
+ In such cases,
151
+ buf0: at creation, 30 bytes allocated, when deleted, 0 bytes freed
152
+ buf1: at creation, 0 bytes allocated, when deleted, 10 bytes freed
153
+ buf2: at creation, 0 bytes allocated, when deleted, 20 bytes freed
154
+
155
+ When an operation mutates a buffer in-place, the scheduler creates a new buffer name
156
+ to track the "before" and "after" states, even though they share the same memory.
157
+
158
+ The mutated buffer represents a rename with zero allocation and deallocation cost.
159
+ During dependency tracking, we transfer dependencies from the mutated name back to
160
+ the original buffer, ensuring the original memory is only freed when all aliases
161
+ are done.
162
+
163
+ This handles cases where a buffer has multiple non-overlapping aliases - rather than
164
+ trying to assign free costs to individual aliases, we forward all alias dependencies
165
+ to the original buffer.
166
+
167
+ Consider:
168
+ buf0 = op0()
169
+ buf1 = mutation_op_(buf0)
170
+ del buf0
171
+ ...
172
+ op(buf1)
173
+ del buf1
174
+
175
+ The only memory events are the creation prior to op0, and the deletion following buf1.
176
+
177
+ Returns:
178
+ A dictionary mapping a scheduler buffer to a tuple of (size_alloc, size_free).
179
+ """
180
+ from .ir import MultiOutput
181
+ from .scheduler import OutputNode
182
+
183
+ sched_buf_to_size: dict[str, tuple[int, int]] = dict()
184
+
185
+ def _compute_and_update_buf_size(
186
+ sched_buf: SchedulerBuffer, user_of_MultiOutputLayout: bool = False
187
+ ) -> int:
188
+ if sched_buf.get_name() in V.graph.scheduler.mutation_real_name:
189
+ sched_buf_to_size[sched_buf.get_name()] = (0, 0)
190
+ return 0
191
+ elif isinstance(sched_buf.node.layout, NoneLayout):
192
+ sched_buf_to_size[sched_buf.get_name()] = (0, 0)
193
+ return 0
194
+ elif isinstance(sched_buf.node.layout, MultiOutputLayout):
195
+ size_alloc = 0
196
+ for user in sched_buf.users:
197
+ if isinstance(user.node, OutputNode):
198
+ continue
199
+ for buf in user.node.get_outputs():
200
+ if isinstance(buf.node, MultiOutput):
201
+ size_alloc += _compute_and_update_buf_size(buf, True)
202
+ sched_buf_to_size[sched_buf.get_name()] = (
203
+ 0 if user_of_MultiOutputLayout else size_alloc,
204
+ 0,
205
+ )
206
+ return size_alloc
207
+ else:
208
+ buf_size = V.graph.sizevars.optimization_hint(
209
+ sched_buf.node.get_numel(), fallback=0
210
+ ) * get_dtype_size(sched_buf.node.get_dtype())
211
+ sched_buf_to_size[sched_buf.get_name()] = (
212
+ 0 if user_of_MultiOutputLayout else buf_size,
213
+ buf_size,
214
+ )
215
+ return buf_size
216
+
217
+ for sched_buf in name_to_buf.values():
218
+ # skip if sched_buf is already processed as an user of another SchedulerBuffer
219
+ # whose layout is of the type MultiOutputLayout
220
+ if sched_buf.get_name() not in sched_buf_to_size:
221
+ _compute_and_update_buf_size(sched_buf)
222
+
223
+ return sched_buf_to_size
224
+
225
+
226
+ def assign_memory_planning_info_for_scheduler_buffers(
227
+ nodes: list[BaseSchedulerNode],
228
+ name_to_buf: dict[str, SchedulerBuffer],
229
+ ) -> None:
230
+ """
231
+ For each SchedulerBuffer, assign its size info and successor nodes.
232
+ A buffer's successor nodes determines when a buffer can be freed.
233
+ """
234
+ # get buffer sizes
235
+ sched_buf_to_size = compute_size_for_scheduler_buffer(name_to_buf)
236
+
237
+ # get buffer's successor nodes for memory lifetime (excludes is_fake WeakDeps)
238
+ # and for ordering (includes all deps)
239
+ dep_name_to_succ_nodes: dict[str, OrderedSet[BaseSchedulerNode]] = (
240
+ collections.defaultdict(OrderedSet)
241
+ )
242
+ dep_name_to_succ_nodes_for_ordering: dict[str, OrderedSet[BaseSchedulerNode]] = (
243
+ collections.defaultdict(OrderedSet)
244
+ )
245
+ for node in nodes:
246
+ for dep in node.unmet_dependencies:
247
+ # All deps contribute to ordering, but fake weak deps do not contribute to
248
+ # memory liveness
249
+ dep_name_to_succ_nodes_for_ordering[dep.name].add(node)
250
+ if not (isinstance(dep, WeakDep) and dep.is_fake):
251
+ dep_name_to_succ_nodes[dep.name].add(node)
252
+
253
+ # iterate in reverse, so dependencies are picked up transitively.
254
+ for mutating_buf_name, real_buf_name in reversed(
255
+ V.graph.scheduler.mutation_real_name.items()
256
+ ):
257
+ dep_name_to_succ_nodes[real_buf_name] |= dep_name_to_succ_nodes[
258
+ mutating_buf_name
259
+ ]
260
+ dep_name_to_succ_nodes_for_ordering[real_buf_name] |= (
261
+ dep_name_to_succ_nodes_for_ordering[mutating_buf_name]
262
+ )
263
+
264
+ # populate the MemoryPlanningInfoForBuffer attribute to each scheduler buffer
265
+ # note: there are scheduler buffers not in dep_name_to_succ_nodes (e.g., graph outputs)
266
+ for buf_name in name_to_buf:
267
+ name_to_buf[buf_name].mpi_buffer = MemoryPlanningInfoForBuffer(
268
+ size_alloc=sched_buf_to_size[buf_name][0],
269
+ size_free=sched_buf_to_size[buf_name][1],
270
+ succ_nodes=dep_name_to_succ_nodes[buf_name],
271
+ succ_nodes_for_ordering=dep_name_to_succ_nodes_for_ordering[buf_name],
272
+ )
273
+
274
+
275
+ def assign_memory_planning_info_for_scheduler_nodes(
276
+ nodes: list[BaseSchedulerNode],
277
+ name_to_fused_node: dict[str, BaseSchedulerNode],
278
+ name_to_buf: dict[str, SchedulerBuffer],
279
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
280
+ ) -> None:
281
+ """
282
+ Assign to each scheduler node its predecessor and successor nodes.
283
+ """
284
+
285
+ node_to_pred_nodes: dict[BaseSchedulerNode, OrderedSet[BaseSchedulerNode]] = (
286
+ collections.defaultdict(OrderedSet)
287
+ )
288
+ node_to_succ_nodes: dict[BaseSchedulerNode, OrderedSet[BaseSchedulerNode]] = {}
289
+ node_to_pred_buffers: dict[
290
+ BaseSchedulerNode, OrderedSet[SchedulerBuffer | FreeableInputBuffer]
291
+ ] = collections.defaultdict(OrderedSet)
292
+
293
+ # collect all predecessors using existing successor mappings
294
+ for node in nodes:
295
+ succ_nodes = OrderedSet(
296
+ succ_node
297
+ for buffer in node.get_outputs()
298
+ for succ_node in buffer.mpi_buffer.succ_nodes_for_ordering
299
+ )
300
+ node_to_succ_nodes[node] = succ_nodes
301
+
302
+ # For each successor, add current node as its predecessor
303
+ for succ_node in succ_nodes:
304
+ node_to_pred_nodes[succ_node].add(node)
305
+
306
+ # For each output buffer, add it as predecessor to its successor nodes
307
+ # Use succ_nodes (not succ_nodes_for_ordering) since pred_buffers is used
308
+ # for memory lifetime tracking, not ordering
309
+ for buffer in node.get_outputs():
310
+ for succ_node in buffer.mpi_buffer.succ_nodes:
311
+ node_to_pred_buffers[succ_node].add(buffer)
312
+
313
+ for freeable_buffer in name_to_freeable_input_buf.values():
314
+ for succ_node in freeable_buffer.mpi_buffer.succ_nodes:
315
+ node_to_pred_buffers[succ_node].add(freeable_buffer)
316
+
317
+ # Second pass: assign memory planning info using completed predecessor mappings
318
+ for index, node in enumerate(nodes):
319
+ size_alloc = sum(buffer.mpi_buffer.size_alloc for buffer in node.get_outputs())
320
+ succ_nodes = node_to_succ_nodes[node]
321
+ pred_nodes = node_to_pred_nodes[node]
322
+
323
+ # make sure we do not make node a successor or predecessor of itself
324
+ succ_nodes.discard(node)
325
+ pred_nodes.discard(node)
326
+
327
+ node.mpi_node = MemoryPlanningInfoForNode(
328
+ index=index,
329
+ size=size_alloc,
330
+ pred_buffers=node_to_pred_buffers[node],
331
+ pred_nodes=node_to_pred_nodes[node],
332
+ succ_nodes=succ_nodes,
333
+ )
334
+
335
+
336
+ # map each scheduler buffer to its size, start step, and end step
337
+ @dataclasses.dataclass
338
+ class BufferInfo:
339
+ buffer: SchedulerBuffer | FreeableInputBuffer
340
+ size_alloc: int
341
+ size_free: int
342
+ start_step: int
343
+ end_step: int
344
+
345
+
346
+ def compute_memory_timeline(
347
+ nodes: list[BaseSchedulerNode],
348
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
349
+ graph_outputs: OrderedSet[str],
350
+ ) -> tuple[
351
+ list[BufferInfo],
352
+ dict[BaseSchedulerNode, int],
353
+ dict[FreeableInputBuffer | SchedulerBuffer, BaseSchedulerNode],
354
+ ]:
355
+ """
356
+ Compute buffer allocation and deallocation sizes and map their
357
+ lifetime to the node schedule
358
+ """
359
+
360
+ # get the execution step of each node, this will be used to determine
361
+ # the end_step of buffers
362
+ node_to_step: dict[BaseSchedulerNode, int] = {
363
+ node: step for step, node in enumerate(nodes)
364
+ }
365
+
366
+ # get buffers' size and liveliness information
367
+ buf_info_list: list[BufferInfo] = []
368
+ buf_to_snode_last_use: dict[
369
+ FreeableInputBuffer | SchedulerBuffer, BaseSchedulerNode
370
+ ] = {}
371
+
372
+ def _get_end_step_and_snode(
373
+ buf: FreeableInputBuffer | SchedulerBuffer,
374
+ ) -> tuple[int, BaseSchedulerNode | None]:
375
+ max_step: int = -1
376
+ max_step_snode: BaseSchedulerNode | None = None
377
+ succ_nodes = buf.mpi_buffer.succ_nodes
378
+ if succ_nodes:
379
+ for succ_node in succ_nodes:
380
+ step = node_to_step[succ_node]
381
+ if step > max_step:
382
+ max_step = step
383
+ max_step_snode = succ_node
384
+ assert max_step_snode is not None
385
+ return max_step, max_step_snode
386
+
387
+ # 1. for freeable input buffers
388
+ for buf_name, input_buf in name_to_freeable_input_buf.items():
389
+ end_step = -1
390
+ if buf_name not in graph_outputs:
391
+ end_step, end_step_snode = _get_end_step_and_snode(input_buf)
392
+ assert end_step_snode is not None
393
+ buf_to_snode_last_use[input_buf] = end_step_snode
394
+
395
+ buf_info_list.append(
396
+ BufferInfo(
397
+ input_buf,
398
+ input_buf.mpi_buffer.size_free,
399
+ input_buf.mpi_buffer.size_free,
400
+ 0,
401
+ end_step,
402
+ )
403
+ )
404
+
405
+ # 2. for scheduler buffers
406
+ for step, node in enumerate(nodes):
407
+ for sched_buf in node.get_outputs():
408
+ # note: it is possible for a non-graph-output sched_buf to have no succ_nodes and
409
+ # to be only used by its defining op (e.g., due to fusion when all consumers of
410
+ # the buffer are fused with its defining op). In such cases, end_step is step.
411
+ buf_name = sched_buf.get_name()
412
+ end_step = -1
413
+ if buf_name not in graph_outputs:
414
+ end_step, end_step_snode = _get_end_step_and_snode(sched_buf)
415
+ if end_step == -1:
416
+ end_step = step
417
+ buf_to_snode_last_use[sched_buf] = node
418
+ else:
419
+ assert end_step_snode is not None
420
+ buf_to_snode_last_use[sched_buf] = end_step_snode
421
+
422
+ buf_info_list.append(
423
+ BufferInfo(
424
+ sched_buf,
425
+ sched_buf.mpi_buffer.size_alloc,
426
+ sched_buf.mpi_buffer.size_free,
427
+ step,
428
+ end_step,
429
+ )
430
+ )
431
+
432
+ return buf_info_list, node_to_step, buf_to_snode_last_use
433
+
434
+
435
+ def estimate_peak_memory(
436
+ nodes: list[BaseSchedulerNode],
437
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
438
+ graph_outputs: OrderedSet[str],
439
+ ) -> tuple[int, list[int]]:
440
+ """
441
+ Given a list of nodes in their execution order, estimate the peak memory, by
442
+ keeping track of the liveliness of SchedulerBuffers and FreeableInputBuffers.
443
+
444
+ Returns:
445
+ int: peak memory
446
+ List[int]: memory usage at each node (or each step).
447
+ """
448
+
449
+ buf_info_list, _, _ = compute_memory_timeline(
450
+ nodes, name_to_freeable_input_buf, graph_outputs
451
+ )
452
+
453
+ # incremental memory changes at each step
454
+ memory = [0 for _ in range(len(nodes) + 1)]
455
+
456
+ # for each buffer, update memory when created and when freed
457
+ for buf_info in buf_info_list:
458
+ memory[buf_info.start_step] += buf_info.size_alloc
459
+ memory[buf_info.end_step + 1] -= buf_info.size_free
460
+
461
+ # get peak memory by compute the cumulative memories
462
+ max_memory = 0
463
+ cur_memory = 0
464
+ memories_at_nodes = []
465
+ for t in range(len(nodes) + 1):
466
+ cur_memory += memory[t]
467
+ memories_at_nodes.append(cur_memory)
468
+ max_memory = max(max_memory, cur_memory)
469
+
470
+ return (max_memory, memories_at_nodes)
471
+
472
+
473
+ @dataclasses.dataclass
474
+ class SNodeMemory:
475
+ size_alloc: int
476
+ size_free: int
477
+
478
+
479
+ def estimate_peak_memory_allocfree(
480
+ nodes: list[BaseSchedulerNode],
481
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
482
+ graph_outputs: OrderedSet[str],
483
+ ) -> tuple[
484
+ int,
485
+ list[tuple[int, int]],
486
+ dict[BaseSchedulerNode, SNodeMemory],
487
+ dict[FreeableInputBuffer | SchedulerBuffer, BaseSchedulerNode],
488
+ ]:
489
+ """
490
+ Alternative version of estimate_peak_memory, that respects the fact,
491
+ that every SchedulerNode has multiple phases:
492
+ 1. alloc ( outputs )
493
+ 2. run_kernel
494
+ 3. dealloc last_use buffers
495
+ estimate_peak_memory collapses memory into one value: size_alloc - size_free
496
+ While peak memory happens after alloc.
497
+
498
+ Duplicating the code to not migrate all callsites at once,
499
+ In future usages of estimate_peak_memory will migrate to this version.
500
+ """
501
+
502
+ buf_info_list, _, buf_to_snode_last_use = compute_memory_timeline(
503
+ nodes, name_to_freeable_input_buf, graph_outputs
504
+ )
505
+
506
+ # incremental memory changes at each step
507
+ step_idx_allocfree = [SNodeMemory(0, 0) for _ in range(len(nodes))]
508
+
509
+ # for each buffer, update memory when created and when freed
510
+ for buf_info in buf_info_list:
511
+ step_idx_allocfree[buf_info.start_step].size_alloc += buf_info.size_alloc
512
+ if buf_info.end_step != -1:
513
+ step_idx_allocfree[buf_info.end_step].size_free += buf_info.size_free
514
+
515
+ snodes_allocfree = {}
516
+ for i, node in enumerate(nodes):
517
+ snodes_allocfree[node] = step_idx_allocfree[i]
518
+
519
+ max_memory = 0
520
+ cur_memory = 0
521
+ snodes_curr_memory = []
522
+ for t in range(len(nodes)):
523
+ alloc = step_idx_allocfree[t].size_alloc
524
+ free = step_idx_allocfree[t].size_free
525
+ cur_memory += alloc
526
+ post_alloc = cur_memory
527
+ max_memory = max(max_memory, cur_memory)
528
+ cur_memory -= free
529
+ post_free = cur_memory
530
+ snodes_curr_memory.append((post_alloc, post_free))
531
+
532
+ return (
533
+ max_memory,
534
+ snodes_curr_memory,
535
+ snodes_allocfree,
536
+ buf_to_snode_last_use,
537
+ )
538
+
539
+
540
+ def topological_sort_lpmf(
541
+ nodes: list[BaseSchedulerNode],
542
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
543
+ name_to_buf: dict[str, SchedulerBuffer],
544
+ graph_outputs: OrderedSet[str],
545
+ ) -> list[BaseSchedulerNode]:
546
+ """
547
+ A bfs-based greedy topological order. LPMF stands for "Least Peak Memory First".
548
+
549
+ The idea is from this paper:
550
+ Buffer memory optimization for video codec application modeled in Simulink
551
+ https://www.cs.york.ac.uk/rts/docs/DAC-1964-2006/PAPERS/2006/DAC06/PDFFILES/P0689.PDF
552
+
553
+ The algorithm maintains the max memory so far.
554
+ At every iteration, for each scheduleable node, it computes:
555
+ - how much memory needs to be allocated for the output buffers of this node;
556
+ - how much memory can be freed as a result of executing this node.
557
+ This gives us two values for each node:
558
+ (1) mem1: memory during the execution of the node;
559
+ (2) mem2: memory after executing the node, after some input buffers are freed.
560
+ The greedy approach select as follows:
561
+ (i) if there are nodes whose mem1 values are below the max memory so far,
562
+ then pick the node with the lowest mem2 value;
563
+ (ii) otherwise, pick the one with the lowest mem1 value.
564
+ """
565
+
566
+ class NodeInfo(TypedDict):
567
+ indegree: int
568
+ memory_to_free: int
569
+
570
+ class BufferInfo(TypedDict):
571
+ outdegree: int
572
+
573
+ node_info: dict[BaseSchedulerNode, NodeInfo] = dict()
574
+ buf_info: dict[SchedulerBuffer | FreeableInputBuffer, BufferInfo] = dict()
575
+
576
+ # compute nodes' number of unmet dependencies (for schedulability)
577
+ # initialize the list of nodes ready to be scheduled
578
+ nodes_to_schedule: OrderedSet[BaseSchedulerNode] = OrderedSet()
579
+ for node in nodes:
580
+ node_info[node] = {
581
+ "indegree": len(node.mpi_node.pred_nodes),
582
+ "memory_to_free": 0,
583
+ }
584
+ if node_info[node]["indegree"] == 0:
585
+ nodes_to_schedule.add(node)
586
+
587
+ # compute buffers' number of unmet successors (used to decide when to free)
588
+ for buf in list(name_to_buf.values()) + list(name_to_freeable_input_buf.values()):
589
+ buf_info[buf] = {
590
+ "outdegree": len(buf.mpi_buffer.succ_nodes)
591
+ + (1 if buf.get_name() in graph_outputs else 0)
592
+ }
593
+
594
+ # initialize memory estimations
595
+ live_memory = sum(
596
+ input_buf.mpi_buffer.size_free
597
+ for input_buf in name_to_freeable_input_buf.values()
598
+ )
599
+
600
+ # this is the total output memory, which is a lower bound for peak memory
601
+ # we do not include the memory of non freeable input buffers
602
+ output_memory = 0
603
+ for buf_name in graph_outputs:
604
+ if buf_name in name_to_buf:
605
+ output_memory += name_to_buf[buf_name].mpi_buffer.size_free
606
+ elif buf_name in name_to_freeable_input_buf:
607
+ output_memory += name_to_freeable_input_buf[buf_name].mpi_buffer.size_free
608
+ max_memory = max(live_memory, output_memory)
609
+ memory_gap = max_memory - live_memory
610
+
611
+ # compute the amount of memory that is allocated when a node is scheduled
612
+ # and the amount of memory that can be freed when a node is scheduled
613
+ for node in nodes:
614
+ # 1. if a buffer read by this node is last used by this node
615
+ for buf in node.mpi_node.pred_buffers:
616
+ if buf_info[buf]["outdegree"] == 1:
617
+ node_info[node]["memory_to_free"] += buf.mpi_buffer.size_free
618
+ # 2. if a buffer written by this node is used internally and not used later
619
+ for buf in node.get_outputs():
620
+ if buf_info[buf]["outdegree"] == 0:
621
+ node_info[node]["memory_to_free"] += buf.mpi_buffer.size_free
622
+
623
+ # schedule nodes one at a time
624
+ schedule: list[BaseSchedulerNode] = []
625
+ size_threshold = config.size_threshold_for_succ_based_strategy
626
+ num_iters: int = 0
627
+ while num_iters < len(nodes) and nodes_to_schedule:
628
+ # select a node to schedule:
629
+ if (
630
+ size_threshold > 0
631
+ and min(node.mpi_node.size for node in nodes_to_schedule) > size_threshold
632
+ ):
633
+ selected_node = min(
634
+ nodes_to_schedule,
635
+ key=lambda node: min(
636
+ (
637
+ succ_node.mpi_node.index
638
+ for succ_node in node.mpi_node.succ_nodes
639
+ ),
640
+ default=len(nodes),
641
+ ),
642
+ )
643
+ else:
644
+ selected_node = min(
645
+ nodes_to_schedule,
646
+ key=lambda node: (
647
+ node.mpi_node.size if node.mpi_node.size > memory_gap else 0,
648
+ node.mpi_node.size - node_info[node]["memory_to_free"],
649
+ node.mpi_node.index,
650
+ ),
651
+ )
652
+ nodes_to_schedule.remove(selected_node)
653
+ schedule.append(selected_node)
654
+ num_iters += 1
655
+
656
+ # update memory usage
657
+ live_memory += selected_node.mpi_node.size
658
+ max_memory = max(max_memory, live_memory)
659
+ live_memory -= node_info[selected_node]["memory_to_free"]
660
+ memory_gap = max_memory - live_memory
661
+
662
+ # update successor nodes and nodes_to_schedule
663
+ for succ_node in selected_node.mpi_node.succ_nodes:
664
+ assert node_info[succ_node]["indegree"] > 0
665
+ node_info[succ_node]["indegree"] -= 1
666
+ if node_info[succ_node]["indegree"] == 0:
667
+ nodes_to_schedule.add(succ_node)
668
+
669
+ # update predecessor nodes
670
+ for buf in selected_node.mpi_node.pred_buffers:
671
+ assert buf_info[buf]["outdegree"] > 0
672
+ buf_info[buf]["outdegree"] -= 1
673
+ if buf_info[buf]["outdegree"] == 1:
674
+ for succ_node in buf.mpi_buffer.succ_nodes:
675
+ node_info[succ_node]["memory_to_free"] += buf.mpi_buffer.size_free
676
+
677
+ if num_iters > len(nodes):
678
+ raise RuntimeError("Failed to schedule, while loop ran too long for lpmf")
679
+
680
+ return schedule
681
+
682
+
683
+ def topological_sort_bfs(nodes: list[BaseSchedulerNode]) -> list[BaseSchedulerNode]:
684
+ """
685
+ A BFS topological sort that selects nodes whose dependencies are executed the
686
+ earliest. This follows a FIFO idea. Specifically, at every iteration, for each node
687
+ that is schedulable, we gather the order in which its predecessor nodes are executed,
688
+ and this sorted list of execution orders of predecessor nodes defines the priority.
689
+ We select the node whose predecessors nodes are executed the earliest. The FIFO
690
+ idea aims to reduce the liveness duration of buffers created.
691
+ """
692
+
693
+ class NodeInfo(TypedDict):
694
+ indegree: int
695
+ order: int
696
+
697
+ node_info: dict[BaseSchedulerNode, NodeInfo] = dict()
698
+
699
+ @dataclasses.dataclass
700
+ class NodeWithPriority:
701
+ priority: list[int]
702
+ node: BaseSchedulerNode
703
+
704
+ def __lt__(self, other: NodeWithPriority) -> bool:
705
+ if self.priority == other.priority:
706
+ return self.node.mpi_node.index < other.node.mpi_node.index
707
+ return self.priority < other.priority
708
+
709
+ def _node_priority(node: BaseSchedulerNode) -> list[int]:
710
+ # priority is the order in which predecessor nodes are executed
711
+ assert node_info[node]["indegree"] == 0
712
+ exec_orders = sorted(
713
+ OrderedSet(
714
+ node_info[pred_node]["order"] for pred_node in node.mpi_node.pred_nodes
715
+ )
716
+ )
717
+ return exec_orders
718
+
719
+ # compute nodes' number of unmet dependencies (for schedulability)
720
+ # initialize the list of nodes ready to be scheduled
721
+ nodes_to_schedule: list[NodeWithPriority] = []
722
+ for node in nodes:
723
+ node_info[node] = {"indegree": len(node.mpi_node.pred_nodes), "order": -1}
724
+ if node_info[node]["indegree"] == 0:
725
+ heapq.heappush(
726
+ nodes_to_schedule, NodeWithPriority(_node_priority(node), node)
727
+ )
728
+
729
+ # schedule nodes one at a time
730
+ schedule: list[BaseSchedulerNode] = []
731
+ num_iters: int = 0
732
+ while num_iters < len(nodes) and nodes_to_schedule:
733
+ # select a node to schedule
734
+ selected_node = heapq.heappop(nodes_to_schedule).node
735
+ node_info[selected_node]["order"] = len(schedule)
736
+ schedule.append(selected_node)
737
+ num_iters += 1
738
+
739
+ # update successor nodes and nodes_to_schedule
740
+ for succ_node in selected_node.mpi_node.succ_nodes:
741
+ assert node_info[succ_node]["indegree"] > 0
742
+ node_info[succ_node]["indegree"] -= 1
743
+ if node_info[succ_node]["indegree"] == 0:
744
+ heapq.heappush(
745
+ nodes_to_schedule,
746
+ NodeWithPriority(_node_priority(succ_node), succ_node),
747
+ )
748
+
749
+ if num_iters > len(nodes):
750
+ raise RuntimeError("Failed to schedule, while loop ran too long for bfs")
751
+
752
+ return schedule
753
+
754
+
755
+ def topological_sort_dfs(nodes: list[BaseSchedulerNode]) -> list[BaseSchedulerNode]:
756
+ """
757
+ This is a DFS topological sort. The setup is similar to `topological_sort_schedule`
758
+ in scheduler.py. The difference is the order nodes are visited in the outer loop.
759
+ In `topological_sort_schedule`, nodes are visited in their original order.
760
+ In this function, nodes are visited based on their priority -- for each node, we
761
+ compute the total memory of all buffers it reads from or writes to, and we visit
762
+ the nodes in ascending order of this priority.
763
+ """
764
+ seen: OrderedSet[BaseSchedulerNode] = OrderedSet()
765
+ name_to_node: dict[str, BaseSchedulerNode] = dict()
766
+ result: list[BaseSchedulerNode] = []
767
+ size_with_reads: dict[BaseSchedulerNode, int] = dict()
768
+
769
+ def visit(n: BaseSchedulerNode) -> None:
770
+ if n not in seen:
771
+ seen.add(n)
772
+ dep_nodes = [
773
+ name_to_node[dep.name]
774
+ for dep in n.unmet_dependencies
775
+ if dep.name in name_to_node
776
+ ]
777
+ for node in sorted(
778
+ dep_nodes, key=lambda n: (size_with_reads[n], n.mpi_node.index)
779
+ ):
780
+ visit(node)
781
+ result.append(n)
782
+
783
+ for node in nodes:
784
+ for name in node.get_buffer_names():
785
+ name_to_node[name] = node
786
+
787
+ for node in nodes:
788
+ size_with_reads[node] = node.mpi_node.size + sum(
789
+ pred_buf.mpi_buffer.size_free for pred_buf in node.mpi_node.pred_buffers
790
+ )
791
+ for node in sorted(nodes, key=lambda n: (size_with_reads[n], n.mpi_node.index)):
792
+ visit(node)
793
+
794
+ return result
795
+
796
+
797
+ def validate_graph_acyclic(nodes: list[BaseSchedulerNode]) -> None:
798
+ """
799
+ Validate that the graph is acyclic by checking predecessor relationships.
800
+
801
+ Raises:
802
+ RuntimeError: If a cycle is detected in the graph
803
+ """
804
+ # DFS coloring scheme for cycle detection:
805
+ # WHITE (0): Node has not been visited yet
806
+ # GRAY (1): Node is currently being processed (in the recursion stack)
807
+ # BLACK (2): Node has been completely processed (finished exploring all its predecessors)
808
+ # A back edge (cycle) is detected when we encounter a GRAY node during DFS traversal
809
+ WHITE, GRAY, BLACK = 0, 1, 2
810
+ color = dict.fromkeys(nodes, WHITE)
811
+ path: list[BaseSchedulerNode] = [] # Track current DFS path
812
+
813
+ def dfs_visit(node: BaseSchedulerNode) -> None:
814
+ if color[node] == BLACK:
815
+ return
816
+
817
+ if color[node] == GRAY:
818
+ path.append(node)
819
+ path_info = " -> ".join([node.get_name() for node in path])
820
+
821
+ raise RuntimeError(
822
+ f"Cycle detected in memory planning graph"
823
+ f"Path containing cycle (i -> j: j is a dependency of i): {path_info} "
824
+ f"This indicates invalid dependency relationships in the scheduler graph"
825
+ )
826
+
827
+ color[node] = GRAY
828
+ path.append(node)
829
+
830
+ for pred_node in node.mpi_node.pred_nodes:
831
+ assert pred_node != node
832
+ dfs_visit(pred_node)
833
+
834
+ path.pop()
835
+ color[node] = BLACK
836
+
837
+ # Start DFS from all unvisited nodes
838
+ for node in nodes:
839
+ if color[node] == WHITE:
840
+ dfs_visit(node)
841
+
842
+
843
+ def validate_unique_buffer_names(
844
+ nodes: list[BaseSchedulerNode],
845
+ name_to_buf: dict[str, SchedulerBuffer],
846
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
847
+ ) -> None:
848
+ """
849
+ Validate that for each node's output buffer, the name_to_buf mapping is correct.
850
+ For each output buffer buf, we should have name_to_buf[buf.get_name()] == buf.
851
+ Also validate that no buffer names overlap with freeable input buffer names.
852
+
853
+ Raises:
854
+ RuntimeError: If buffer name mapping is incorrect or names overlap
855
+ """
856
+ for node in nodes:
857
+ for buf in node.get_outputs():
858
+ buf_name = buf.get_name()
859
+
860
+ # Check if buffer name exists in the mapping
861
+ if buf_name not in name_to_buf:
862
+ raise RuntimeError(
863
+ f"{buf_name} from {node.get_name()} is not found in name_to_buf mapping."
864
+ f" This indicates a missing buffer mapping."
865
+ )
866
+
867
+ # Check if the mapping points to the correct buffer object
868
+ if name_to_buf[buf_name] != buf:
869
+ raise RuntimeError(
870
+ f"Buffer name mapping is incorrect for '{buf_name}'."
871
+ f"Expected name_to_buf['{buf_name}'] to be {buf.debug_str()}"
872
+ f"but got {name_to_buf[buf_name].debug_str()}"
873
+ f"This indicates some buffers share the same name"
874
+ )
875
+
876
+ # Check if buffer name conflicts with freeable input buffer names
877
+ if buf_name in name_to_freeable_input_buf:
878
+ raise RuntimeError(
879
+ f"Buffer name conflict detected: '{buf_name}' from node {node.get_name()} "
880
+ f"is also used as a freeable input buffer name. "
881
+ )
882
+
883
+
884
+ def prepare_planning_info(
885
+ nodes: list[BaseSchedulerNode],
886
+ name_to_buf: dict[str, SchedulerBuffer],
887
+ name_to_fused_node: dict[str, BaseSchedulerNode],
888
+ graph_inputs: OrderedSet[str],
889
+ graph_outputs: OrderedSet[str],
890
+ ) -> tuple[int, dict[str, FreeableInputBuffer]]:
891
+ """
892
+ Prepare planning info. As nodes are scheduled one at a time, these help
893
+ keep track of when a buffer can be freed, and when a node can be scheduled
894
+
895
+ Returns:
896
+ int: peak memory estimation
897
+ dict[str, FreeableInputBuffer]: name to freeable input buffer
898
+ """
899
+ name_to_freeable_input_buf = get_freeable_input_buf(nodes, graph_inputs)
900
+ assign_memory_planning_info_for_scheduler_buffers(nodes, name_to_buf)
901
+ assign_memory_planning_info_for_scheduler_nodes(
902
+ nodes, name_to_fused_node, name_to_buf, name_to_freeable_input_buf
903
+ )
904
+
905
+ # the default
906
+ estimated_peak_memory, _ = estimate_peak_memory(
907
+ nodes, name_to_freeable_input_buf, graph_outputs
908
+ )
909
+
910
+ return estimated_peak_memory, name_to_freeable_input_buf
911
+
912
+
913
+ def reorder_for_peak_memory(
914
+ nodes: list[BaseSchedulerNode],
915
+ name_to_buf: dict[str, SchedulerBuffer],
916
+ name_to_fused_node: dict[str, BaseSchedulerNode],
917
+ graph_inputs: OrderedSet[str],
918
+ graph_outputs: OrderedSet[str],
919
+ methods: list[Callable[..., list[BaseSchedulerNode]]] = [ # noqa: B006
920
+ topological_sort_lpmf,
921
+ topological_sort_bfs,
922
+ topological_sort_dfs,
923
+ ],
924
+ ) -> list[BaseSchedulerNode]:
925
+ """
926
+ Try a few heuristics based topological sort algorithms, and pick the one whose
927
+ resulting topological order has the lowest peak memory estimation.
928
+ """
929
+
930
+ torch_log.info("Reordering for peak memory -- %d nodes", len(nodes))
931
+
932
+ estimated_peak_memory, name_to_freeable_input_buf = prepare_planning_info(
933
+ nodes,
934
+ name_to_buf,
935
+ name_to_fused_node,
936
+ graph_inputs,
937
+ graph_outputs,
938
+ )
939
+
940
+ # export graph for simulator if needed
941
+ if config.reorder_for_peak_memory_debug:
942
+ export_graph_for_simulator(
943
+ nodes,
944
+ name_to_freeable_input_buf,
945
+ name_to_fused_node,
946
+ graph_inputs,
947
+ graph_outputs,
948
+ )
949
+
950
+ # Validate planning info before proceeding with reordering
951
+ try:
952
+ validate_graph_acyclic(nodes)
953
+ validate_unique_buffer_names(nodes, name_to_buf, name_to_freeable_input_buf)
954
+ except RuntimeError:
955
+ torch_log.exception("Memory planning validation failed")
956
+ if not is_fbcode(): # TODO: remove after ensuring OSS side is safe
957
+ raise
958
+
959
+ # keep track of the peak memory estimates of different methods
960
+ peak_memory_diff_methods: list[PeakMemoryResult] = []
961
+ peak_memory_diff_methods.append(
962
+ PeakMemoryResult(nodes, estimated_peak_memory, "baseline")
963
+ )
964
+ torch_log.info("Baseline peak memory: %d", estimated_peak_memory)
965
+
966
+ # other methods
967
+ for method in methods:
968
+ try:
969
+ if method is topological_sort_lpmf:
970
+ order = method(
971
+ nodes, name_to_freeable_input_buf, name_to_buf, graph_outputs
972
+ )
973
+ else:
974
+ order = method(nodes)
975
+ assert len(order) == len(nodes)
976
+ peak_memory, _ = estimate_peak_memory(
977
+ order, name_to_freeable_input_buf, graph_outputs
978
+ )
979
+ peak_memory_diff_methods.append(
980
+ PeakMemoryResult(order, peak_memory, method.__name__)
981
+ )
982
+ torch_log.info("%s peak memory: %d", method.__name__, peak_memory)
983
+ except Exception:
984
+ torch_log.exception("Failed to reorder for %s", method.__name__)
985
+ if not is_fbcode(): # TODO: remove after ensuring OSS side is safe
986
+ raise
987
+
988
+ signpost_event(
989
+ category="inductor",
990
+ name="memory",
991
+ parameters={
992
+ "orm": {elem.method: elem.peak_memory for elem in peak_memory_diff_methods},
993
+ },
994
+ )
995
+
996
+ # get the optimal one
997
+ best_result = min(peak_memory_diff_methods, key=lambda x: x.peak_memory)
998
+
999
+ return best_result.order
1000
+
1001
+
1002
+ def export_graph_for_simulator(
1003
+ nodes: list[BaseSchedulerNode],
1004
+ name_to_freeable_input_buf: dict[str, FreeableInputBuffer],
1005
+ name_to_fused_node: dict[str, BaseSchedulerNode],
1006
+ graph_inputs: OrderedSet[str],
1007
+ graph_outputs: OrderedSet[str],
1008
+ ) -> None:
1009
+ """
1010
+ This is for debugging purposes. It will dump a json file that records graph information.
1011
+ The graph can then be used in a simulator: https://fburl.com/code/3l3d3qi4
1012
+ """
1013
+
1014
+ class ORMBuffer(TypedDict):
1015
+ name: str
1016
+ size_alloc: int
1017
+ size_free: int
1018
+ size: int # for backward compatibility
1019
+ is_input: bool
1020
+ is_output: bool
1021
+ deps: list[str]
1022
+ unmet_deps: list[str]
1023
+
1024
+ class ORMNode(TypedDict):
1025
+ name: str
1026
+ buffer_names: list[str]
1027
+
1028
+ class ORMGraph(TypedDict):
1029
+ nodes: list[ORMNode]
1030
+ buffers: list[ORMBuffer]
1031
+
1032
+ orm_buffers: list[ORMBuffer] = []
1033
+ orm_nodes: list[ORMNode] = []
1034
+
1035
+ # get orm buffers for freeable input buffers
1036
+ for buf_name, input_buf in name_to_freeable_input_buf.items():
1037
+ orm_buf_input_buffer: ORMBuffer = {
1038
+ "name": buf_name,
1039
+ "size_alloc": input_buf.mpi_buffer.size_free,
1040
+ "size_free": input_buf.mpi_buffer.size_free,
1041
+ "size": input_buf.mpi_buffer.size_free,
1042
+ "is_input": True,
1043
+ "is_output": buf_name in graph_outputs,
1044
+ "deps": [],
1045
+ "unmet_deps": [],
1046
+ }
1047
+ orm_buffers.append(orm_buf_input_buffer)
1048
+
1049
+ # get orm buffers for scheduler buffers
1050
+ name_to_buf: dict[str, SchedulerBuffer] = {
1051
+ buf.get_name(): buf for node in nodes for buf in node.get_outputs()
1052
+ } # need to reassign due to probably node pruning
1053
+ for buf_name, sched_buf in name_to_buf.items():
1054
+ if sched_buf.defining_op is None:
1055
+ continue
1056
+ deps = [
1057
+ pred_buf.get_name()
1058
+ for pred_buf in name_to_fused_node[
1059
+ sched_buf.defining_op.get_name()
1060
+ ].mpi_node.pred_buffers
1061
+ ]
1062
+ orm_buf_scheduler_buffer: ORMBuffer = {
1063
+ "name": buf_name,
1064
+ "size_alloc": sched_buf.mpi_buffer.size_alloc,
1065
+ "size_free": sched_buf.mpi_buffer.size_free,
1066
+ "size": sched_buf.mpi_buffer.size_free,
1067
+ "is_input": False,
1068
+ "is_output": buf_name in graph_outputs,
1069
+ "deps": deps,
1070
+ "unmet_deps": [
1071
+ buf_name for buf_name in deps if buf_name not in graph_inputs
1072
+ ],
1073
+ }
1074
+ orm_buffers.append(orm_buf_scheduler_buffer)
1075
+
1076
+ # get orm nodes
1077
+ for node in nodes:
1078
+ orm_node: ORMNode = {
1079
+ "name": node.get_name(),
1080
+ "buffer_names": list(node.get_buffer_names()),
1081
+ }
1082
+ orm_nodes.append(orm_node)
1083
+
1084
+ # create the graph object
1085
+ g: ORMGraph = {
1086
+ "nodes": orm_nodes,
1087
+ "buffers": orm_buffers,
1088
+ }
1089
+
1090
+ # dump the graph
1091
+ import json
1092
+ import os
1093
+
1094
+ import torch
1095
+ from functorch.compile import get_graph_being_compiled
1096
+
1097
+ name = os.path.splitext(get_graph_being_compiled())[0] + "_fused"
1098
+
1099
+ g_str = json.dumps(g, indent=2)
1100
+
1101
+ torch._logging.trace_structured(
1102
+ "artifact",
1103
+ metadata_fn=lambda: {
1104
+ "name": name,
1105
+ "encoding": "string",
1106
+ },
1107
+ payload_fn=lambda: g_str,
1108
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/metrics.py ADDED
@@ -0,0 +1,484 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import dataclasses
5
+ import inspect
6
+ import os
7
+ import re
8
+ from dataclasses import dataclass
9
+ from functools import lru_cache
10
+ from typing import TYPE_CHECKING
11
+
12
+ from torch._inductor import config
13
+ from torch._inductor.utils import get_benchmark_name
14
+ from torch.utils._ordered_set import OrderedSet
15
+
16
+
17
+ # Prevent circular import
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Callable
20
+
21
+ from torch._inductor.runtime.triton_compat import Config
22
+ from torch._inductor.scheduler import BaseSchedulerNode
23
+
24
+ # counter for tracking how many kernels have been generated
25
+ generated_kernel_count = 0
26
+ generated_cpp_vec_kernel_count = 0
27
+ num_bytes_accessed = 0
28
+ nodes_num_elem: list[
29
+ tuple[
30
+ BaseSchedulerNode,
31
+ int,
32
+ ]
33
+ ] = []
34
+ node_runtimes: list[tuple[BaseSchedulerNode, float]] = []
35
+
36
+ # counters for tracking fusions
37
+ ir_nodes_pre_fusion = 0
38
+
39
+ # counters for tracking to_dtype inserted
40
+ cpp_to_dtype_count = 0
41
+
42
+
43
+ @dataclasses.dataclass
44
+ class CppOuterLoopFusedCount:
45
+ inner_kernel_number: int
46
+ local_buffer_number: int = 0
47
+
48
+
49
+ # The length counts the number of outer loop fusions.
50
+ cpp_outer_loop_fused_inner_counts: list[CppOuterLoopFusedCount] = []
51
+
52
+ num_comprehensive_padding = 0
53
+ num_matches_for_scatter_upon_const_tensor = 0
54
+
55
+ num_loop_reordering = 0
56
+ num_auto_chunking: int = 0
57
+
58
+ # counter for parallel reduction.
59
+ parallel_reduction_count = 0
60
+
61
+ codegen_mix_order_reduction = 0
62
+
63
+
64
+ # reset all counters
65
+ def reset() -> None:
66
+ global generated_kernel_count
67
+ global generated_cpp_vec_kernel_count
68
+ global num_bytes_accessed, nodes_num_elem
69
+ global ir_nodes_pre_fusion
70
+ global cpp_to_dtype_count
71
+ global cpp_outer_loop_fused_inner_counts
72
+ global num_comprehensive_padding
73
+ global num_matches_for_scatter_upon_const_tensor
74
+ global num_loop_reordering
75
+ global parallel_reduction_count
76
+ global codegen_mix_order_reduction
77
+ global num_auto_chunking
78
+
79
+ generated_kernel_count = 0
80
+ generated_cpp_vec_kernel_count = 0
81
+ num_bytes_accessed = 0
82
+ nodes_num_elem.clear()
83
+ node_runtimes.clear()
84
+ ir_nodes_pre_fusion = 0
85
+ cpp_to_dtype_count = 0
86
+ cpp_outer_loop_fused_inner_counts.clear()
87
+ num_comprehensive_padding = 0
88
+ num_matches_for_scatter_upon_const_tensor = 0
89
+ num_loop_reordering = 0
90
+ parallel_reduction_count = 0
91
+ codegen_mix_order_reduction = 0
92
+ num_auto_chunking = 0
93
+
94
+
95
+ @dataclass
96
+ class CachedMetricsDeltas:
97
+ """
98
+ The subset of metrics we want update across cache hits, e.g., the
99
+ FxGraphCache.
100
+ """
101
+
102
+ generated_kernel_count: int
103
+ generated_cpp_vec_kernel_count: int
104
+ ir_nodes_pre_fusion: int
105
+ cpp_to_dtype_count: int
106
+ num_bytes_accessed: int
107
+ num_matches_for_scatter_upon_const_tensor: int
108
+
109
+
110
+ def get_metric_fields() -> list[str]:
111
+ return [field.name for field in dataclasses.fields(CachedMetricsDeltas)]
112
+
113
+
114
+ class CachedMetricsHelper:
115
+ """
116
+ A helper class to help calculate and apply counter deltas for those
117
+ metrics we want to save with cache entries (e.g., FxGraphCache) and
118
+ apply on a cache hit.
119
+ """
120
+
121
+ def __init__(self) -> None:
122
+ self.cached_metrics = {}
123
+ for metric in get_metric_fields():
124
+ self.cached_metrics[metric] = globals()[metric]
125
+
126
+ def get_deltas(self) -> CachedMetricsDeltas:
127
+ delta_metrics = {}
128
+ for metric in get_metric_fields():
129
+ delta_metrics[metric] = globals()[metric] - self.cached_metrics[metric]
130
+
131
+ return CachedMetricsDeltas(**delta_metrics)
132
+
133
+ @staticmethod
134
+ def apply_deltas(delta: CachedMetricsDeltas) -> None:
135
+ for metric in get_metric_fields():
136
+ globals()[metric] += getattr(delta, metric)
137
+
138
+
139
+ REGISTERED_METRIC_TABLES: dict[str, MetricTable] = {}
140
+
141
+
142
+ @dataclass
143
+ class MetricTable:
144
+ table_name: str
145
+ column_names: list[str]
146
+
147
+ num_rows_added: int = 0
148
+
149
+ def add_row(self, row_fn: Callable[[], dict[str, str | float | None]]) -> None:
150
+ if self.table_name not in enabled_metric_tables():
151
+ return
152
+
153
+ row_dict = row_fn()
154
+ assert len(self.column_names) == len(row_dict), (
155
+ f"{len(self.column_names)} v.s. {len(row_dict)}"
156
+ )
157
+ assert OrderedSet(self.column_names) == OrderedSet(row_dict.keys()), (
158
+ f"{OrderedSet(self.column_names)} v.s. {OrderedSet(row_dict.keys())}"
159
+ )
160
+
161
+ bn = get_benchmark_name()
162
+ # assert bn is not None
163
+ row = [bn] + [row_dict[column_name] for column_name in self.column_names]
164
+ assert all(isinstance(i, (str, float, type(None))) for i in row)
165
+ self._write_row(row)
166
+
167
+ def output_filename(self) -> str:
168
+ return f"metric_table_{self.table_name}.csv"
169
+
170
+ def write_header(self) -> None:
171
+ filename = self.output_filename()
172
+ with open(filename, "w") as fd:
173
+ writer = csv.writer(fd, lineterminator="\n")
174
+ writer.writerow(["model_name"] + self.column_names)
175
+
176
+ def _write_row(self, row: list[str | float | None]) -> None:
177
+ filename = self.output_filename()
178
+ if self.num_rows_added == 0 and not os.path.exists(filename):
179
+ self.write_header()
180
+
181
+ self.num_rows_added += 1
182
+
183
+ for idx, orig_val in enumerate(row):
184
+ if isinstance(orig_val, float):
185
+ new_val = f"{orig_val:.6f}"
186
+ elif orig_val is None:
187
+ new_val = ""
188
+ else:
189
+ new_val = orig_val
190
+ row[idx] = new_val
191
+
192
+ with open(filename, "a") as fd:
193
+ writer = csv.writer(fd, lineterminator="\n")
194
+ writer.writerow(row)
195
+
196
+ @staticmethod
197
+ def register_table(name: str, column_names: list[str]) -> None:
198
+ table = MetricTable(name, column_names)
199
+ REGISTERED_METRIC_TABLES[name] = table
200
+
201
+
202
+ MetricTable.register_table(
203
+ "slow_fusion",
204
+ [
205
+ "kernel1_path",
206
+ "kernel1_latency",
207
+ "kernel2_path",
208
+ "kernel2_latency",
209
+ "fused_kernel_path",
210
+ "fused_kernel_latency",
211
+ "slow_down_ratio",
212
+ ],
213
+ )
214
+
215
+ # track the fusion statistics for each graph
216
+ MetricTable.register_table(
217
+ "graph_stats",
218
+ [
219
+ "graph_id",
220
+ "num_nodes_before_fusion",
221
+ "num_nodes_after_fusion",
222
+ ],
223
+ )
224
+
225
+ # track the perf difference between persistent reduction and non-persistent
226
+ # reductions
227
+ MetricTable.register_table(
228
+ "persistent_red_perf",
229
+ [
230
+ "kernel0_path",
231
+ "kernel1_path",
232
+ "kernel2_path",
233
+ "kernel3_path",
234
+ "kernel0_latency",
235
+ "kernel1_latency",
236
+ "kernel2_latency",
237
+ "kernel3_latency",
238
+ "size_hints",
239
+ "reduction_hint",
240
+ ],
241
+ )
242
+
243
+ # Log the fusion failures due to indexing mismatch
244
+ MetricTable.register_table(
245
+ "fusion_failure_due_to_indexing_mismatch",
246
+ [
247
+ "pre_grad_graph_id",
248
+ "post_grad_graph_id",
249
+ "node1_name",
250
+ "node2_name",
251
+ "node1_debug_str",
252
+ "node2_debug_str",
253
+ "common_buffer_names",
254
+ "failure_reason",
255
+ ],
256
+ )
257
+
258
+ # Log metadata for pointwise/reduction kernels. E.g., model name, kernel path, numel, rnumel, reduction hint
259
+ MetricTable.register_table(
260
+ "kernel_metadata",
261
+ [
262
+ "kernel_name",
263
+ "kernel_path",
264
+ "kernel_category", # pointwise/reduction/foreach etc.
265
+ "size_hints",
266
+ "reduction_hint",
267
+ "line_of_code",
268
+ "num_load",
269
+ "num_store",
270
+ "num_for_loop",
271
+ "num_atomic_add",
272
+ "num_args",
273
+ # xyz numel can be different to size_hints since size_hints are rounded
274
+ # up to the nearest power of 2.
275
+ # Inductor kernel will burn in the xyz numel in kernel code for static
276
+ # shape kernels.
277
+ # Logging them will be helpful to find unaligned shape for reduction
278
+ "xnumel",
279
+ "ynumel",
280
+ "rnumel",
281
+ "kernel_args_num_gb",
282
+ ],
283
+ )
284
+
285
+
286
+ def _parse_kernel_fn_code(kernel_module_code: str) -> str:
287
+ """
288
+ The kernel_module_code is the python module that contains kernel function code.
289
+ kernel function is the proper triton kernel function annotated with
290
+ @triton.jit
291
+ """
292
+ from .codecache import PyCodeCache
293
+ from .wrapper_benchmark import get_triton_kernel
294
+
295
+ mod = PyCodeCache.load(kernel_module_code)
296
+ kernel = get_triton_kernel(mod)
297
+ # kernel is a CachingAutotune; kernel.fn is the JITFunction;
298
+ # kernel.fn.fn is the function being decorate by triton.jit
299
+ return inspect.getsource(kernel.fn.fn)
300
+
301
+
302
+ def _parse_kernel_line_of_code(proper_kernel_fn_code: str) -> int:
303
+ """
304
+ Return the line of code for the kernel excluding the decorators.
305
+ """
306
+ return len(proper_kernel_fn_code.splitlines())
307
+
308
+
309
+ def _parse_size_hints(kernel_module_code: str, kernel_category: str) -> str | None:
310
+ if kernel_category == "foreach":
311
+ # foreach kernel does not have size_hints
312
+ return None
313
+ m = re.search(r"size_hints=(\[[0-9, ]*\]),", kernel_module_code)
314
+ assert m, "size_hints missing!"
315
+ return m.group(1)
316
+
317
+
318
+ def _parse_reduction_hint(kernel_category: str, kernel_module_code: str) -> str | None:
319
+ if kernel_category not in ("reduction", "persistent_reduction"):
320
+ return None
321
+ m = re.search(r"reduction_hint=ReductionHint\.(\w*),", kernel_module_code)
322
+ assert m, "reduction_hint not found in kernel source code!"
323
+ return m.group(1)
324
+
325
+
326
+ def _count_pattern(proper_kernel_fn_code: str, pattern: str) -> int:
327
+ return proper_kernel_fn_code.count(pattern)
328
+
329
+
330
+ def _count_args(proper_kernel_fn_code: str) -> int:
331
+ def_line = proper_kernel_fn_code.splitlines()[0]
332
+ assert def_line.startswith("def ")
333
+ start_idx = def_line.index("(")
334
+ end_idx = def_line.index("):")
335
+ decl_csv = def_line[start_idx + 1 : end_idx]
336
+ comps = decl_csv.split(",")
337
+ return len(comps)
338
+
339
+
340
+ def _parse_proper_kernel_fn_code(kernel_fn_code: str) -> str:
341
+ """
342
+ Skip decorators.
343
+ """
344
+ start_pos = kernel_fn_code.index("def ")
345
+ return kernel_fn_code[start_pos:]
346
+
347
+
348
+ def _parse_numel(proper_kernel_fn_code: str, numel_arg_name: str) -> int | None:
349
+ m = re.search(f"{numel_arg_name} = ([\\d]+)", proper_kernel_fn_code)
350
+ if m:
351
+ return int(m.group(1))
352
+ else:
353
+ return None
354
+
355
+
356
+ def _parse_kernel_args_num_gb(
357
+ kernel_fn_code: str, kernel_category: str
358
+ ) -> float | None:
359
+ """
360
+ inductor meta looks like:
361
+ inductor_meta={... 'mutated_arg_names': [], 'no_x_dim': False, 'kernel_num_gb': 2.0},
362
+ """
363
+ m = re.search(r".kernel_num_gb.:\s*([0-9.]+)", kernel_fn_code)
364
+ if m:
365
+ return float(m.group(1))
366
+ else:
367
+ """
368
+ There are a few cases that kernel_num_gdb field can be missing:
369
+ 1. the field will be missing if config.benchmark_kernel and
370
+ config.profile_bandwidth are false
371
+ 2. even if config.benchmark_kernel or config.profile_bandwidth is true.
372
+ foreach kernel does not have kernel_num_gb field in the metadata
373
+ """
374
+ return None
375
+
376
+
377
+ def log_kernel_metadata(
378
+ kernel_name: str, kernel_path: str, kernel_module_code: str
379
+ ) -> None:
380
+ """
381
+ An utility to log kernel metadata. We may parse metadata from kernel source code here.
382
+
383
+ It's fine to parse the generated kernel code here since the logging is
384
+ disabled by default. It would hurt compilation time.
385
+ """
386
+ from .wrapper_benchmark import get_kernel_category_by_source_code
387
+
388
+ kernel_category = get_kernel_category_by_source_code(kernel_module_code)
389
+ reduction_hint = _parse_reduction_hint(kernel_category, kernel_module_code)
390
+ size_hints = _parse_size_hints(kernel_module_code, kernel_category)
391
+ kernel_fn_code = _parse_kernel_fn_code(kernel_module_code)
392
+
393
+ proper_kernel_fn_code = _parse_proper_kernel_fn_code(kernel_fn_code)
394
+
395
+ # the line of code excluding the decortors
396
+ kernel_line_of_code = _parse_kernel_line_of_code(proper_kernel_fn_code)
397
+
398
+ get_metric_table("kernel_metadata").add_row(
399
+ lambda: {
400
+ "kernel_name": kernel_name,
401
+ "kernel_path": kernel_path,
402
+ "kernel_category": kernel_category,
403
+ "size_hints": size_hints,
404
+ "reduction_hint": reduction_hint,
405
+ "line_of_code": kernel_line_of_code,
406
+ "num_load": _count_pattern(proper_kernel_fn_code, "tl.load"),
407
+ "num_store": _count_pattern(proper_kernel_fn_code, "tl.store"),
408
+ "num_for_loop": _count_pattern(proper_kernel_fn_code, "for "),
409
+ "num_atomic_add": _count_pattern(proper_kernel_fn_code, "tl.atomic_add"),
410
+ "num_args": _count_args(proper_kernel_fn_code),
411
+ "xnumel": _parse_numel(proper_kernel_fn_code, "xnumel"),
412
+ "ynumel": _parse_numel(proper_kernel_fn_code, "ynumel"),
413
+ "rnumel": _parse_numel(proper_kernel_fn_code, "rnumel"),
414
+ "kernel_args_num_gb": _parse_kernel_args_num_gb(
415
+ kernel_fn_code, kernel_category
416
+ ),
417
+ }
418
+ )
419
+
420
+
421
+ def purge_old_log_files() -> None:
422
+ """
423
+ Purge the old log file at the beginning when the benchmark script runs.
424
+ Should do it in the parent process rather than the child processes running
425
+ each individual model.
426
+ """
427
+ for name, table in REGISTERED_METRIC_TABLES.items():
428
+ if name in enabled_metric_tables():
429
+ filename = table.output_filename()
430
+ if os.path.exists(filename):
431
+ os.unlink(filename)
432
+
433
+ table.write_header()
434
+
435
+
436
+ def enabled_metric_tables() -> OrderedSet[str]:
437
+ return enabled_metric_tables_impl(config.enabled_metric_tables)
438
+
439
+
440
+ @lru_cache
441
+ def enabled_metric_tables_impl(config_str: str) -> OrderedSet[str]:
442
+ enabled: OrderedSet[str] = OrderedSet()
443
+ for name in config_str.split(","):
444
+ name = name.strip()
445
+ if not name:
446
+ continue
447
+ assert name in REGISTERED_METRIC_TABLES, (
448
+ f"Metric table name {name} is not registered"
449
+ )
450
+ enabled.add(name)
451
+ return enabled
452
+
453
+
454
+ def is_metric_table_enabled(name: str) -> bool:
455
+ return name in enabled_metric_tables()
456
+
457
+
458
+ def get_metric_table(name: str) -> MetricTable:
459
+ assert name in REGISTERED_METRIC_TABLES, f"Metric table {name} is not defined"
460
+ return REGISTERED_METRIC_TABLES[name]
461
+
462
+
463
+ MetricTable.register_table(
464
+ "kernel_autotune",
465
+ [
466
+ "kernel_path",
467
+ "kernel_name",
468
+ "triton_config",
469
+ "latency_ms",
470
+ ],
471
+ )
472
+
473
+
474
+ def log_kernel_autotune_result(
475
+ kernel_path: str, kernel_name: str, config: Config, latency: float
476
+ ) -> None:
477
+ get_metric_table("kernel_autotune").add_row(
478
+ lambda: {
479
+ "kernel_path": kernel_path,
480
+ "kernel_name": kernel_name,
481
+ "triton_config": str(config),
482
+ "latency_ms": latency,
483
+ }
484
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_ir.py ADDED
@@ -0,0 +1,1361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from collections.abc import Sequence
3
+ from typing import Any, Optional
4
+
5
+ import sympy
6
+
7
+ import torch
8
+ from torch._prims_common import make_channels_last_strides_for, StrideType
9
+ from torch.utils._ordered_set import OrderedSet
10
+
11
+ from .ir import (
12
+ ExternKernelAlloc,
13
+ FixedLayout,
14
+ FlexibleLayout,
15
+ get_device_type,
16
+ ir_node_to_tensor,
17
+ IRNode,
18
+ is_contiguous_storage_and_layout,
19
+ Layout,
20
+ may_convert_to_optional,
21
+ MultiOutput,
22
+ MultiOutputLayout,
23
+ MutationOutput,
24
+ NoneLayout,
25
+ TensorBox,
26
+ )
27
+ from .utils import convert_shape_to_inductor, pad_listlike, SUPPORTED_MKLDNN_DEVICES
28
+ from .virtualized import V
29
+
30
+
31
+ def _prepare_convolution_fusion_create(
32
+ cls,
33
+ x: "TensorBox",
34
+ weight: "TensorBox",
35
+ bias: "TensorBox",
36
+ padding: Sequence[int],
37
+ stride: Sequence[int],
38
+ dilation: Sequence[int],
39
+ groups: int,
40
+ transposed: bool = False,
41
+ output_padding: Sequence[int] | None = None,
42
+ quantize_args: list["TensorBox"] | None = None,
43
+ other: Optional["TensorBox"] = None,
44
+ ):
45
+ """
46
+ This function is a helper function to prepare inputs, layout and constant args
47
+ for convolution post-op fusion's create function, including deciding the output
48
+ layout (channels first or channels last), realizing inputs and make them etc. The
49
+ function only supports the CPU/XPU device since conv post-op fusion kernel is only
50
+ supported on CPU/XPU right now.
51
+ """
52
+
53
+ # Port from aten/src/ATen/native/ConvUtils.h: _conv_input_size
54
+ def _conv_input_size(
55
+ output_size, weight_size, padding, output_padding, stride, dilation, groups
56
+ ):
57
+ assert len(output_size) == len(weight_size), "Expect input dim == weight dim"
58
+ dim = len(output_size)
59
+ assert dim > 2, "Expect input dim > 2"
60
+
61
+ BATCH_DIM = 0
62
+ WEIGHT_INPUT_CHANNELS_DIM = 1
63
+ input_size = []
64
+ input_size.append(output_size[BATCH_DIM])
65
+ input_size.append(weight_size[WEIGHT_INPUT_CHANNELS_DIM] * groups)
66
+ for d in range(2, dim):
67
+ kernel = (weight_size[d] - 1) * dilation[d - 2] + 1
68
+ input_size_d = (
69
+ (output_size[d] - 1) * stride[d - 2]
70
+ - (padding[d - 2] * 2)
71
+ + kernel
72
+ + output_padding[d - 2]
73
+ )
74
+ input_size.append(input_size_d)
75
+ return list(map(int, input_size))
76
+
77
+ # Port from aten/src/ATen/native/ConvUtils.h: _conv_output_size
78
+ def _conv_output_size(input_size, weight_size, padding, stride, dilation=None):
79
+ has_dilation = dilation is not None
80
+ dim = len(input_size)
81
+ output_size = []
82
+ output_size.append(input_size[0])
83
+ output_size.append(weight_size[0])
84
+ for d in range(2, dim):
85
+ # pyrefly: ignore [unsupported-operation]
86
+ dilation_ = dilation[d - 2] if has_dilation else 1
87
+ kernel = dilation_ * (weight_size[d] - 1) + 1
88
+ output_size_d = (input_size[d] + (2 * padding[d - 2]) - kernel) // stride[
89
+ d - 2
90
+ ] + 1
91
+ output_size.append(output_size_d)
92
+ return output_size
93
+
94
+ # The size of prepacked_weight is the prepacked weight size of deconv:
95
+ # Groups > 1: [g*o, i/g, ...]
96
+ # Groups == 1: [o, i, ...]
97
+ # Returns original weight size in [i, o, ...]
98
+ def _original_deconv_weight_size(
99
+ prepacked_weight,
100
+ groups,
101
+ ):
102
+ prepacked_weight_size = prepacked_weight.size()
103
+ dim = len(prepacked_weight_size)
104
+ assert dim > 2, "Expect weight dim > 2"
105
+ if groups > 1:
106
+ weight_size = []
107
+ weight_size.append(prepacked_weight_size[1] * groups)
108
+ weight_size.append(prepacked_weight_size[0] / groups)
109
+ weight_size.extend(prepacked_weight_size[d] for d in range(2, dim))
110
+ else:
111
+ weight_size = prepacked_weight.transpose(0, 1).size()
112
+ return weight_size
113
+
114
+ x.realize()
115
+ weight.realize()
116
+ if bias is not None:
117
+ bias.realize()
118
+ with V.graph.fake_mode:
119
+ # TODO <Leslie> cleaned up the fake_tensor trace as Linear implementation
120
+ x_fake = ir_node_to_tensor(x)
121
+ weight_fake = ir_node_to_tensor(weight)
122
+ dims = len(x_fake.size()) - 2
123
+ assert 0 < len(padding) <= dims
124
+ assert 0 < len(dilation) <= dims
125
+ assert 0 < len(stride) <= dims
126
+ padding = pad_listlike(padding, dims)
127
+ dilation = pad_listlike(dilation, dims)
128
+ stride = pad_listlike(stride, dims)
129
+ if output_padding is None:
130
+ output_padding = pad_listlike([0], dims)
131
+ else:
132
+ assert 0 < len(output_padding) <= dims
133
+ output_padding = pad_listlike(output_padding, dims)
134
+ assert isinstance(groups, (int, sympy.core.numbers.Integer))
135
+ if transposed:
136
+ # When transposed, the size of the prepacked oneDNN weight is different
137
+ # from the PyTorch weight. We're not able to run aten conv with such
138
+ # size. We infer the output size from the input params here:
139
+ weight_size = _original_deconv_weight_size(weight_fake, groups)
140
+ input_size = x_fake.size()
141
+ output_size = _conv_input_size(
142
+ input_size,
143
+ weight_size,
144
+ padding,
145
+ output_padding,
146
+ stride,
147
+ dilation,
148
+ groups,
149
+ )
150
+ else:
151
+ x_shape = list(x_fake.shape)
152
+ weight_shape = list(weight_fake.shape)
153
+ if len(x_shape) != len(weight_shape):
154
+ assert len(x_shape) == 3 and len(weight_shape) == 4
155
+ weight_shape.pop(2)
156
+ output_size = _conv_output_size(
157
+ x_shape,
158
+ weight_shape,
159
+ padding,
160
+ stride,
161
+ dilation,
162
+ )
163
+
164
+ req_stride_order = [0] + list(reversed(range(1, len(stride) + 1)))
165
+ req_stride_order = [len(req_stride_order)] + req_stride_order
166
+
167
+ x = cls.require_stride_order(x, req_stride_order)
168
+
169
+ # We won't do weight prepack for Conv if dynamic_shapes or if is xpu.
170
+ # In static shape cases, since weight is prepacked, we'll always force output to be channels last in the Conv kernel.
171
+ # In dynamic shape cases, for input with channels = 1, like tensor of size (s0, 1, 28, 28) and stride (784, 784, 28, 1),
172
+ # x = cls.require_stride_order(x, req_stride_order) where req_stride_order is in the channels last order
173
+ # won't change the stride of this tensor since stride for dimensions of size 1 is ignored. While in Conv kernel,
174
+ # this tensor is considered as channels first and the output will be in contiguous format.
175
+ # To align the behavior of the Conv kernel, we set the output_stride in such case to be contiguous instead of channels last.
176
+ dynamic_shapes = not all(isinstance(i, int) for i in (output_size))
177
+ if (
178
+ dynamic_shapes or get_device_type(x) == "xpu"
179
+ ) and is_contiguous_storage_and_layout(x):
180
+ output_stride: StrideType = FlexibleLayout.contiguous_strides(output_size)
181
+ # Currently we don't support channel last for the situation that stride of input's batch dim is 0,
182
+ # eg. input_size = (1, 1280, 64, 64), but input_stride=(0, 1, 81920, 1280).
183
+ # So we use NCHW hear instead.
184
+ # Different with cpu, cpu conv always use channels_last for convolution when weight is prepacked,
185
+ # but xpu does not do the prepack, so the problem exposed here is only for xpu.
186
+ # TODO support channels_last for such zero stride input.
187
+ elif get_device_type(x) == "xpu" and x.get_stride()[0] == 0:
188
+ output_stride = FlexibleLayout.contiguous_strides(output_size)
189
+ else:
190
+ output_stride = make_channels_last_strides_for(output_size)
191
+
192
+ assert get_device_type(x) == get_device_type(weight)
193
+ assert get_device_type(x) in SUPPORTED_MKLDNN_DEVICES
194
+ inputs = [x]
195
+
196
+ if quantize_args is not None:
197
+ x_scale, x_zero_point, w_scale, w_zero_point = quantize_args
198
+ x_scale.realize()
199
+ x_zero_point.realize()
200
+ w_scale.realize()
201
+ w_zero_point.realize()
202
+ inputs = inputs + [x_scale, x_zero_point] + [weight] + [w_scale, w_zero_point]
203
+ else:
204
+ inputs += [weight]
205
+
206
+ if other is not None:
207
+ other = cls.require_stride_order(other, req_stride_order)
208
+ assert isinstance(other, TensorBox)
209
+ inputs += [other]
210
+
211
+ kernel_layout = FixedLayout(
212
+ x.get_device_or_error(),
213
+ x.get_dtype(),
214
+ convert_shape_to_inductor(output_size),
215
+ convert_shape_to_inductor(output_stride),
216
+ )
217
+ constant_args = [padding, stride, dilation, groups]
218
+ if transposed:
219
+ constant_args.insert(1, output_padding)
220
+
221
+ if bias is not None:
222
+ inputs.append(bias)
223
+ else:
224
+ constant_args.insert(0, bias)
225
+ return inputs, constant_args, kernel_layout, req_stride_order, other
226
+
227
+
228
+ def _prepare_linear_fusion_create(
229
+ cls,
230
+ x: "TensorBox",
231
+ weight: "TensorBox",
232
+ bias: "TensorBox",
233
+ quantize_args: list["TensorBox"] | None = None,
234
+ other: Optional["TensorBox"] = None,
235
+ binary_sum: bool = False,
236
+ ):
237
+ """
238
+ This function is a helper function to prepare inputs, layout and constant args
239
+ for linear post-op fusion's create function. The function only supports the CPU device
240
+ since linear post-op fusion kernel is only supported on CPU right now.
241
+ """
242
+ x.realize()
243
+ weight.realize()
244
+ if bias is not None:
245
+ bias.realize()
246
+
247
+ *m, _ = x.get_size()
248
+ # The weight has been transposed during the qlinear weight prepack process.
249
+ # https://github.com/pytorch/pytorch/blob/4979f9c0d72490970e2019bb1d2284f83d93f76b/
250
+ # aten/src/ATen/native/quantized/cpu/qlinear_prepack.cpp#L291
251
+ _, oc = weight.get_size()
252
+ output_size = list(m) + [oc]
253
+ req_stride_order = list(reversed(range(len(x.get_size()))))
254
+
255
+ x = cls.require_stride_order(x, req_stride_order)
256
+ assert get_device_type(x) == get_device_type(weight)
257
+ assert get_device_type(x) in SUPPORTED_MKLDNN_DEVICES
258
+ inputs = [x]
259
+
260
+ if quantize_args is not None:
261
+ x_scale, x_zero_point, w_scale, w_zero_point = quantize_args
262
+ x_scale.realize()
263
+ x_zero_point.realize()
264
+ w_scale.realize()
265
+ w_zero_point.realize()
266
+ inputs = inputs + [x_scale, x_zero_point] + [weight] + [w_scale, w_zero_point]
267
+ else:
268
+ inputs += [weight]
269
+
270
+ if other is not None:
271
+ if binary_sum:
272
+ other = cls.require_stride_order(other, req_stride_order)
273
+ inputs = inputs + [other]
274
+
275
+ output_stride = FlexibleLayout.contiguous_strides(output_size)
276
+ kernel_layout = FixedLayout(
277
+ x.get_device(),
278
+ x.get_dtype(),
279
+ output_size,
280
+ output_stride,
281
+ )
282
+ constant_args: list[Any] = []
283
+
284
+ if bias is not None:
285
+ inputs.append(bias)
286
+ else:
287
+ constant_args.insert(0, bias)
288
+ return inputs, constant_args, kernel_layout, req_stride_order, other
289
+
290
+
291
+ def _create_output_node(packed):
292
+ output_ir = MultiOutput(
293
+ packed.get_layout(),
294
+ packed,
295
+ [],
296
+ )
297
+ packed.layout = MultiOutputLayout(device=packed.get_device())
298
+ packed.outputs = [output_ir]
299
+ return output_ir
300
+
301
+
302
+ class ConvolutionUnary(ExternKernelAlloc):
303
+ def __init__(
304
+ self,
305
+ layout,
306
+ inputs,
307
+ constant_args=(),
308
+ ) -> None:
309
+ self.device_type = get_device_type(inputs[0])
310
+ super().__init__(
311
+ layout,
312
+ inputs,
313
+ constant_args,
314
+ None,
315
+ op_overload=torch.ops.mkldnn._convolution_pointwise.default,
316
+ cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_pointwise",
317
+ )
318
+
319
+ def codegen(self, wrapper):
320
+ wrapper.include_extra_header(
321
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
322
+ )
323
+ super().codegen(wrapper)
324
+
325
+ @classmethod
326
+ def create(
327
+ cls,
328
+ x: "TensorBox",
329
+ weight: "TensorBox",
330
+ bias: "TensorBox",
331
+ padding_: list[int],
332
+ stride_: list[int],
333
+ dilation_: list[int],
334
+ groups: int,
335
+ attr,
336
+ scalars: list[Any] | None,
337
+ algorithm,
338
+ ):
339
+ (
340
+ inputs,
341
+ constant_args,
342
+ kernel_layout,
343
+ _,
344
+ _,
345
+ ) = _prepare_convolution_fusion_create(
346
+ cls, x, weight, bias, padding_, stride_, dilation_, groups
347
+ )
348
+ constant_args = constant_args + [
349
+ attr,
350
+ may_convert_to_optional(scalars),
351
+ algorithm,
352
+ ]
353
+ packed = ConvolutionUnary(
354
+ layout=kernel_layout,
355
+ inputs=inputs,
356
+ constant_args=constant_args,
357
+ )
358
+ return _create_output_node(packed)
359
+
360
+
361
+ class ConvolutionBinary(ExternKernelAlloc):
362
+ def __init__(
363
+ self,
364
+ layout,
365
+ inputs,
366
+ constant_args=(),
367
+ cpp_constant_args=(),
368
+ ) -> None:
369
+ self.device_type = get_device_type(inputs[0])
370
+ super().__init__(
371
+ layout,
372
+ inputs,
373
+ constant_args,
374
+ None,
375
+ op_overload=torch.ops.mkldnn._convolution_pointwise.binary,
376
+ cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_pointwise_binary",
377
+ )
378
+ self.cpp_constant_args = cpp_constant_args
379
+
380
+ def codegen(self, wrapper):
381
+ wrapper.include_extra_header(
382
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
383
+ )
384
+ super().codegen(wrapper)
385
+
386
+ @classmethod
387
+ def create(
388
+ cls,
389
+ x: "TensorBox",
390
+ other: "TensorBox",
391
+ weight: "TensorBox",
392
+ bias: "TensorBox",
393
+ padding_: list[int],
394
+ stride_: list[int],
395
+ dilation_: list[int],
396
+ groups: int,
397
+ binary_attr: str,
398
+ binary_alpha: float | None,
399
+ unary_attr: str | None,
400
+ unary_scalars: list[Any] | None,
401
+ unary_algorithm: str | None,
402
+ ):
403
+ (
404
+ inputs,
405
+ constant_args,
406
+ kernel_layout,
407
+ req_stride_order,
408
+ _,
409
+ ) = _prepare_convolution_fusion_create(
410
+ cls, x, weight, bias, padding_, stride_, dilation_, groups
411
+ )
412
+ # pyrefly: ignore [bad-assignment]
413
+ other = cls.require_stride_order(other, req_stride_order)
414
+ inputs.insert(1, other)
415
+ constant_args = constant_args + [
416
+ binary_attr,
417
+ binary_alpha,
418
+ unary_attr,
419
+ may_convert_to_optional(unary_scalars),
420
+ unary_algorithm,
421
+ ]
422
+ packed = ConvolutionBinary(
423
+ layout=kernel_layout,
424
+ inputs=inputs,
425
+ constant_args=constant_args,
426
+ )
427
+ return _create_output_node(packed)
428
+
429
+
430
+ class ConvolutionBinaryInplace(ExternKernelAlloc):
431
+ def __init__(
432
+ self,
433
+ kernel_layout,
434
+ inputs,
435
+ constant_args=(),
436
+ ) -> None:
437
+ # Due to constrain of op.call, other (Tensor&) should be at input[0]
438
+ self.device_type = get_device_type(inputs[0])
439
+ reordered_inputs = [inputs[1], inputs[0]] + inputs[2:]
440
+
441
+ super().__init__(
442
+ kernel_layout,
443
+ reordered_inputs,
444
+ constant_args,
445
+ None,
446
+ op_overload=torch.ops.mkldnn._convolution_pointwise_.binary,
447
+ cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_pointwise_binary_",
448
+ )
449
+
450
+ self.mutation_outputs = [
451
+ MutationOutput(NoneLayout(device=inputs[0].get_device()), inputs[0], self),
452
+ MutationOutput(NoneLayout(device=inputs[1].get_device()), inputs[1], self),
453
+ ]
454
+
455
+ def codegen(self, wrapper):
456
+ wrapper.include_extra_header(
457
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
458
+ )
459
+ super().codegen(wrapper)
460
+
461
+ def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]:
462
+ return OrderedSet()
463
+
464
+ @classmethod
465
+ def create(
466
+ cls,
467
+ x: "TensorBox",
468
+ other: "TensorBox",
469
+ weight: "TensorBox",
470
+ bias: "TensorBox",
471
+ padding_: list[int],
472
+ stride_: list[int],
473
+ dilation_: list[int],
474
+ groups: int,
475
+ binary_attr: str,
476
+ binary_alpha: float | None,
477
+ unary_attr: str | None,
478
+ unary_scalars: list[Any] | None,
479
+ unary_algorithm: str | None,
480
+ ):
481
+ (
482
+ inputs,
483
+ constant_args,
484
+ _,
485
+ req_stride_order,
486
+ _,
487
+ ) = _prepare_convolution_fusion_create(
488
+ cls, x, weight, bias, padding_, stride_, dilation_, groups
489
+ )
490
+ # pyrefly: ignore [bad-assignment]
491
+ other = cls.require_stride_order(other, req_stride_order)
492
+ inputs.insert(1, other)
493
+ constant_args = constant_args + [
494
+ binary_attr,
495
+ binary_alpha,
496
+ unary_attr,
497
+ may_convert_to_optional(unary_scalars),
498
+ unary_algorithm,
499
+ ]
500
+ packed = ConvolutionBinaryInplace(
501
+ kernel_layout=NoneLayout(device=inputs[1].get_device()), # type: ignore[arg-type]
502
+ inputs=inputs,
503
+ constant_args=constant_args,
504
+ )
505
+ # This op mutates in place which means that the result is not the
506
+ # target but rather the input that is being mutated
507
+ # init reorders the inputs, so inputs[1] becomes packed.inputs[0]
508
+ return packed.inputs[0]
509
+
510
+
511
+ class ConvolutionTransposeUnary(ExternKernelAlloc):
512
+ def __init__(
513
+ self,
514
+ layout,
515
+ inputs,
516
+ constant_args=(),
517
+ ) -> None:
518
+ self.device_type = get_device_type(inputs[0])
519
+ super().__init__(
520
+ layout,
521
+ inputs,
522
+ constant_args,
523
+ None,
524
+ op_overload=torch.ops.mkldnn._convolution_transpose_pointwise.default,
525
+ cpp_kernel_name=f"aoti_torch_{self.device_type}_mkldnn__convolution_transpose_pointwise",
526
+ )
527
+
528
+ def codegen(self, wrapper):
529
+ wrapper.include_extra_header(
530
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
531
+ )
532
+ super().codegen(wrapper)
533
+
534
+ @classmethod
535
+ def create(
536
+ cls,
537
+ x: "TensorBox",
538
+ weight: "TensorBox",
539
+ bias: "TensorBox",
540
+ padding_: list[int],
541
+ output_padding_: list[int],
542
+ stride_: list[int],
543
+ dilation_: list[int],
544
+ groups_: int,
545
+ attr,
546
+ scalars: list[Any] | None,
547
+ algorithm,
548
+ ):
549
+ transposed = True
550
+ (
551
+ inputs,
552
+ constant_args,
553
+ kernel_layout,
554
+ _,
555
+ _,
556
+ ) = _prepare_convolution_fusion_create(
557
+ cls,
558
+ x,
559
+ weight,
560
+ bias,
561
+ padding_,
562
+ stride_,
563
+ dilation_,
564
+ groups_,
565
+ transposed,
566
+ output_padding_,
567
+ )
568
+ constant_args = constant_args + [
569
+ attr,
570
+ may_convert_to_optional(scalars),
571
+ algorithm,
572
+ ]
573
+ packed = ConvolutionTransposeUnary(
574
+ layout=kernel_layout,
575
+ inputs=inputs,
576
+ constant_args=constant_args,
577
+ )
578
+ return _create_output_node(packed)
579
+
580
+
581
+ class QConvPointWisePT2E(ExternKernelAlloc):
582
+ def __init__(
583
+ self,
584
+ layout,
585
+ inputs,
586
+ constant_args=(),
587
+ ) -> None:
588
+ """
589
+ if bias is not None
590
+ - inputs = [x, w, b, weight_scale, weight_zp]
591
+ - const_args is: [stride, padding, dilation, groups, x_scale, x_zp, o_scale, o_zp,
592
+ fp32_output, unary_attr, unary_scalars, unary_algorithm]
593
+ else
594
+ - inputs = [x, w, weight_scale, weight_zp]
595
+ - const_args is: [bias, stride, padding, dilation, groups, x_scale, x_zp, o_scale, o_zp,
596
+ fp32_output, unary_attr, unary_scalars, unary_algorithm]
597
+ """
598
+ self.device_type = get_device_type(inputs[0])
599
+ self.has_bias = len(inputs) == 5
600
+ super().__init__(
601
+ layout,
602
+ inputs,
603
+ constant_args,
604
+ None,
605
+ op_overload=torch.ops.onednn.qconv_pointwise.tensor,
606
+ cpp_kernel_name=f"aoti_torch_{self.device_type}__qconv_pointwise_tensor",
607
+ )
608
+
609
+ def codegen(self, wrapper):
610
+ wrapper.include_extra_header(
611
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
612
+ )
613
+ super().codegen(wrapper)
614
+ if isinstance(self.layout, Layout):
615
+ self.codegen_size_asserts(wrapper)
616
+
617
+ @classmethod
618
+ def create(
619
+ cls,
620
+ qx: "TensorBox",
621
+ x_scale: "TensorBox",
622
+ x_zero_point: "TensorBox",
623
+ qw: "TensorBox", # qw
624
+ w_scale: "TensorBox",
625
+ w_zero_point,
626
+ bias: "TensorBox",
627
+ stride: list[int],
628
+ padding: list[int],
629
+ dilation: list[int],
630
+ groups: int,
631
+ output_scale: float,
632
+ output_zero_point: int,
633
+ output_dtype,
634
+ attr,
635
+ scalars,
636
+ algorithm,
637
+ ):
638
+ transposed = False
639
+ output_padding = None
640
+ (
641
+ inputs,
642
+ constant_args,
643
+ kernel_layout,
644
+ _,
645
+ _,
646
+ ) = _prepare_convolution_fusion_create(
647
+ cls,
648
+ qx,
649
+ qw,
650
+ bias,
651
+ padding,
652
+ stride,
653
+ dilation,
654
+ groups,
655
+ transposed,
656
+ output_padding,
657
+ [x_scale, x_zero_point, w_scale, w_zero_point], # type: ignore[list-item]
658
+ )
659
+ # swap padding and stride to align with functional conv arg order
660
+ if bias is None:
661
+ constant_args[1], constant_args[2] = constant_args[2], constant_args[1]
662
+ else:
663
+ constant_args[0], constant_args[1] = constant_args[1], constant_args[0]
664
+
665
+ constant_args = constant_args + [
666
+ output_scale,
667
+ output_zero_point,
668
+ output_dtype,
669
+ attr,
670
+ may_convert_to_optional(scalars),
671
+ algorithm,
672
+ ]
673
+
674
+ assert output_dtype is not None
675
+ if output_dtype in [torch.float32, torch.bfloat16]:
676
+ # in _prepare_convolution_fusion_create, we use x.dtype (uint8) to create kernel_layout
677
+ # if we set output_dtype is not None, the output buf should be output_dtype instead of uint8.
678
+ kernel_layout.dtype = output_dtype
679
+
680
+ return QConvPointWisePT2E(
681
+ layout=kernel_layout,
682
+ inputs=inputs,
683
+ constant_args=constant_args,
684
+ )
685
+
686
+
687
+ class QConvPointWiseBinaryPT2E(ExternKernelAlloc):
688
+ def __init__(
689
+ self,
690
+ layout,
691
+ inputs,
692
+ constant_args=(),
693
+ ) -> None:
694
+ """
695
+ Needs input/weight/output qparams
696
+ if bias is not None
697
+ - inputs = [x, x_scale, x_zp, w, w_scale, w_zp, accum, b]
698
+ - const_args = [stride, padding, dilation, groups, o_scale, o_zp,
699
+ output_dtype, accum_scale, accum_zp, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
700
+ else
701
+ - inputs = [x, x_scale, x_zp, w, w_scale, w_zp, accum]
702
+ - const_args [b, stride, padding, dilation, groups, o_scale, o_zp,
703
+ output_dtype, accum_scale, accum_zp, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
704
+ """
705
+ self.device_type = get_device_type(inputs[0])
706
+ self.has_bias = len(inputs) == 8
707
+ self.idx_for_inplace_sum = 6
708
+ super().__init__(
709
+ layout,
710
+ inputs,
711
+ constant_args,
712
+ None,
713
+ op_overload=torch.ops.onednn.qconv2d_pointwise.binary_tensor,
714
+ cpp_kernel_name=(
715
+ f"aoti_torch_{self.device_type}__qconv2d_pointwise_binary_tensor"
716
+ ),
717
+ )
718
+
719
+ def codegen(self, wrapper):
720
+ wrapper.include_extra_header(
721
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
722
+ )
723
+ super().codegen(wrapper)
724
+ if isinstance(self.layout, Layout):
725
+ self.codegen_size_asserts(wrapper)
726
+
727
+ def get_mutation_names(self) -> Sequence[str]:
728
+ return [self.input_name(self.idx_for_inplace_sum)]
729
+
730
+ def get_unbacked_symbol_defs(self) -> OrderedSet[sympy.Symbol]:
731
+ return OrderedSet()
732
+
733
+ @classmethod
734
+ def create(
735
+ cls,
736
+ qx: "TensorBox",
737
+ x_scale: "TensorBox",
738
+ x_zero_point: "TensorBox",
739
+ qw: "TensorBox", # packed_weight
740
+ w_scale,
741
+ w_zero_point,
742
+ qaccum: "TensorBox",
743
+ bias: "TensorBox",
744
+ stride: list[int],
745
+ padding: list[int],
746
+ dilation: list[int],
747
+ groups: int,
748
+ output_scale: "TensorBox",
749
+ output_zero_point: "TensorBox",
750
+ output_dtype,
751
+ accum_scale,
752
+ accum_zero_point,
753
+ binary_attr,
754
+ alpha,
755
+ unary_attr,
756
+ unary_scalars,
757
+ unary_algorithm,
758
+ ):
759
+ transposed = False
760
+ output_padding = None
761
+ (
762
+ inputs,
763
+ constant_args,
764
+ _kernel_layout,
765
+ req_stride_order,
766
+ qaccum,
767
+ ) = _prepare_convolution_fusion_create(
768
+ cls,
769
+ qx,
770
+ qw,
771
+ bias,
772
+ padding,
773
+ stride,
774
+ dilation,
775
+ groups,
776
+ transposed,
777
+ output_padding,
778
+ [x_scale, x_zero_point, w_scale, w_zero_point],
779
+ qaccum,
780
+ )
781
+
782
+ # swap padding and stride to align with functional conv arg order
783
+ if bias is None:
784
+ constant_args[1], constant_args[2] = constant_args[2], constant_args[1]
785
+ else:
786
+ constant_args[0], constant_args[1] = constant_args[1], constant_args[0]
787
+
788
+ constant_args = constant_args + [
789
+ output_scale,
790
+ output_zero_point,
791
+ output_dtype,
792
+ accum_scale,
793
+ accum_zero_point,
794
+ binary_attr,
795
+ alpha,
796
+ unary_attr,
797
+ may_convert_to_optional(unary_scalars),
798
+ unary_algorithm,
799
+ ]
800
+
801
+ assert binary_attr == "sum", (
802
+ "For now, only post op sum is supported in QConvPointWiseBinaryPT2E."
803
+ )
804
+
805
+ V.graph.mark_buffer_mutated(qaccum.get_name())
806
+ packed = QConvPointWiseBinaryPT2E(
807
+ layout=NoneLayout(device=qaccum.get_device()),
808
+ inputs=inputs,
809
+ constant_args=constant_args,
810
+ )
811
+
812
+ # Return accum since it has been inplace changed.
813
+ return packed.inputs[packed.idx_for_inplace_sum]
814
+
815
+
816
+ class MKLPackedLinear(ExternKernelAlloc):
817
+ def __init__(
818
+ self,
819
+ layout,
820
+ inputs,
821
+ constant_args=(),
822
+ ) -> None:
823
+ super().__init__(
824
+ layout,
825
+ inputs,
826
+ constant_args,
827
+ None,
828
+ op_overload=torch.ops.mkl._mkl_linear.default,
829
+ )
830
+
831
+ def codegen(self, wrapper):
832
+ wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h")
833
+ super().codegen(wrapper)
834
+
835
+ @classmethod
836
+ def create(cls, x, packed_w, orig_w, B, batch_size):
837
+ x = cls.require_stride1(cls.realize_input(x))
838
+ orig_w = cls.require_stride1(cls.realize_input(orig_w))
839
+ *m, _ = x.get_size()
840
+ oc, _ = orig_w.get_size()
841
+ output_size = list(m) + [oc]
842
+ output_stride = FlexibleLayout.contiguous_strides(output_size)
843
+ inputs = [x, packed_w, orig_w]
844
+ constant_args = [batch_size]
845
+ if B is not None:
846
+ inputs += [B]
847
+ else:
848
+ constant_args.insert(0, None)
849
+
850
+ device = x.get_device()
851
+ assert device is not None
852
+ return MKLPackedLinear(
853
+ layout=FixedLayout(device, x.get_dtype(), output_size, output_stride),
854
+ inputs=inputs,
855
+ constant_args=constant_args,
856
+ )
857
+
858
+
859
+ class LinearUnary(ExternKernelAlloc):
860
+ def __init__(
861
+ self,
862
+ layout,
863
+ inputs,
864
+ constant_args=(),
865
+ ) -> None:
866
+ self.device_type = get_device_type(inputs[0])
867
+ super().__init__(
868
+ layout,
869
+ inputs,
870
+ constant_args,
871
+ None,
872
+ op_overload=torch.ops.mkldnn._linear_pointwise.default,
873
+ cpp_kernel_name=f"aoti_torch_{self.device_type}__linear_pointwise",
874
+ )
875
+
876
+ def codegen(self, wrapper):
877
+ wrapper.include_extra_header(
878
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
879
+ )
880
+ super().codegen(wrapper)
881
+
882
+ @classmethod
883
+ def create(cls, x, w, B, attr, scalars, algorithm):
884
+ x = cls.require_contiguous(cls.realize_input(x))
885
+ w = cls.require_contiguous(cls.realize_input(w))
886
+
887
+ *m, _ic = x.get_size()
888
+ oc, _ic = w.get_size()
889
+ output_size = list(m) + [oc]
890
+ inputs = [x, w]
891
+ constant_args = [attr, scalars if scalars else [-1], algorithm]
892
+ if B is not None:
893
+ B = cls.require_contiguous(cls.realize_input(B))
894
+ inputs.append(B)
895
+ else:
896
+ constant_args.insert(0, None)
897
+
898
+ device = x.get_device()
899
+ assert device is not None
900
+
901
+ packed = LinearUnary(
902
+ layout=FixedLayout(
903
+ device=device,
904
+ dtype=x.get_dtype(),
905
+ size=output_size,
906
+ ),
907
+ inputs=inputs,
908
+ constant_args=constant_args,
909
+ )
910
+ return _create_output_node(packed)
911
+
912
+ def apply_constraint(self):
913
+ pass
914
+
915
+
916
+ class LinearBinary(ExternKernelAlloc):
917
+ kernel = "torch.ops.mkldnn._linear_pointwise.binary"
918
+
919
+ def __init__(
920
+ self,
921
+ layout,
922
+ inputs,
923
+ constant_args=(),
924
+ ) -> None:
925
+ self.device_type = get_device_type(inputs[0])
926
+ super().__init__(
927
+ layout,
928
+ inputs,
929
+ constant_args,
930
+ None,
931
+ op_overload=torch.ops.mkldnn._linear_pointwise.binary,
932
+ cpp_kernel_name=f"aoti_torch_{self.device_type}__linear_pointwise_binary",
933
+ )
934
+
935
+ def codegen(self, wrapper):
936
+ wrapper.include_extra_header(
937
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
938
+ )
939
+ super().codegen(wrapper)
940
+
941
+ @classmethod
942
+ def create(cls, x, y, w, B, attr):
943
+ x = cls.require_contiguous(cls.realize_input(x))
944
+ y = cls.require_contiguous(cls.realize_input(y))
945
+ w = cls.require_contiguous(cls.realize_input(w))
946
+
947
+ *m, _ic = x.get_size()
948
+ oc, _ic = w.get_size()
949
+ output_size = list(m) + [oc]
950
+ inputs = [x, y, w]
951
+ constant_args = [attr]
952
+ if B is not None:
953
+ B = cls.require_contiguous(cls.realize_input(B))
954
+ inputs.append(B)
955
+ else:
956
+ constant_args.insert(0, B)
957
+
958
+ device = x.get_device()
959
+ assert device is not None
960
+ packed = LinearBinary(
961
+ layout=FixedLayout(
962
+ device=device,
963
+ dtype=x.get_dtype(),
964
+ size=output_size,
965
+ ),
966
+ inputs=inputs,
967
+ constant_args=constant_args,
968
+ )
969
+ return _create_output_node(packed)
970
+
971
+ def apply_constraint(self):
972
+ pass
973
+
974
+
975
+ class QLinearPointwisePT2E(ExternKernelAlloc):
976
+ def __init__(
977
+ self,
978
+ layout,
979
+ inputs,
980
+ constant_args=(),
981
+ has_bias=True,
982
+ ) -> None:
983
+ """
984
+ if bias is not None
985
+ - inputs = [x, w, b, weight_scale, weight_zp]
986
+ - const_args is: [x_scale, x_zp, o_scale, o_zp,
987
+ fp32_output, unary_attr, unary_scalars, unary_algorithm]
988
+ else
989
+ - inputs = [x, w, weight_scale, weight_zp]
990
+ - const_args is: [bias, x_scale, x_zp, o_scale, o_zp,
991
+ fp32_output, unary_attr, unary_scalars, unary_algorithm]
992
+ """
993
+ self.device_type = get_device_type(inputs[0])
994
+ self.has_bias = has_bias
995
+ super().__init__(
996
+ layout,
997
+ inputs,
998
+ constant_args,
999
+ None,
1000
+ op_overload=(torch.ops.onednn.qlinear_pointwise.tensor),
1001
+ cpp_kernel_name=(
1002
+ f"aoti_torch_{self.device_type}__qlinear_pointwise_tensor"
1003
+ ),
1004
+ )
1005
+
1006
+ def codegen(self, wrapper):
1007
+ wrapper.include_extra_header(
1008
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
1009
+ )
1010
+ super().codegen(wrapper)
1011
+
1012
+ if isinstance(self.layout, Layout):
1013
+ self.codegen_size_asserts(wrapper)
1014
+
1015
+ @classmethod
1016
+ def create(
1017
+ cls,
1018
+ qx: "TensorBox",
1019
+ x_scale: "TensorBox",
1020
+ x_zero_point: "TensorBox",
1021
+ qw: "TensorBox", # packed_weight
1022
+ w_scale: "TensorBox",
1023
+ w_zero_point: "TensorBox",
1024
+ bias: "TensorBox",
1025
+ output_scale: float,
1026
+ output_zero_point: int,
1027
+ output_dtype,
1028
+ post_op_name,
1029
+ post_op_args,
1030
+ post_op_algorithm,
1031
+ ):
1032
+ (inputs, constant_args, kernel_layout, _, _) = _prepare_linear_fusion_create(
1033
+ cls,
1034
+ qx,
1035
+ qw,
1036
+ bias,
1037
+ [x_scale, x_zero_point, w_scale, w_zero_point],
1038
+ )
1039
+
1040
+ constant_args = constant_args + [
1041
+ output_scale,
1042
+ output_zero_point,
1043
+ output_dtype,
1044
+ post_op_name,
1045
+ may_convert_to_optional(post_op_args),
1046
+ post_op_algorithm,
1047
+ ]
1048
+
1049
+ assert output_dtype is not None
1050
+ if output_dtype in [torch.float32, torch.bfloat16]:
1051
+ # in _prepare_linear_fusion_create, we use x.dtype (uint8) to create kernel_layout
1052
+ # if we set fp32_output, the output buf should be dtype float32 instead of uint8.
1053
+ kernel_layout.dtype = output_dtype
1054
+
1055
+ return QLinearPointwisePT2E(
1056
+ layout=kernel_layout,
1057
+ inputs=inputs,
1058
+ constant_args=constant_args,
1059
+ has_bias=(bias is not None),
1060
+ )
1061
+
1062
+
1063
+ class QLinearPointwiseBinaryPT2E(ExternKernelAlloc):
1064
+ def __init__(
1065
+ self,
1066
+ layout,
1067
+ inputs,
1068
+ constant_args=(),
1069
+ has_bias=True,
1070
+ ) -> None:
1071
+ """
1072
+ if bias is not None
1073
+ - inputs = [x, w, x_scale, x_zp, weight_scale, weight_zp, x2, bias]
1074
+ - const_args is: [o_scale, o_zp,
1075
+ fp32_output, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
1076
+ else
1077
+ - inputs = [x, w, x_scale, x_zp, weight_scale, weight_zp, x2]
1078
+ - const_args is: [bias, o_scale, o_zp,
1079
+ fp32_output, binary_attr, alpha, unary_attr, unary_scalars, unary_algorithm]
1080
+ """
1081
+ self.device_type = get_device_type(inputs[0])
1082
+ self.has_bias = has_bias
1083
+ self.idx_for_inplace_sum = 6
1084
+ super().__init__(
1085
+ layout,
1086
+ inputs,
1087
+ constant_args,
1088
+ None,
1089
+ op_overload=(torch.ops.onednn.qlinear_pointwise.binary_tensor),
1090
+ cpp_kernel_name=f"aoti_torch_{self.device_type}__qlinear_pointwise_binary_tensor",
1091
+ )
1092
+
1093
+ def codegen(self, wrapper):
1094
+ wrapper.include_extra_header(
1095
+ f"torch/csrc/inductor/aoti_torch/c/shim_{self.device_type}.h"
1096
+ )
1097
+ super().codegen(wrapper)
1098
+ if isinstance(self.layout, Layout):
1099
+ self.codegen_size_asserts(wrapper)
1100
+
1101
+ def get_mutation_names(self) -> Sequence[str]:
1102
+ binary_post_op = self.constant_args[-5]
1103
+ if binary_post_op == "sum":
1104
+ input = self.inputs[self.idx_for_inplace_sum]
1105
+ assert isinstance(input, IRNode)
1106
+ return [input.get_name()]
1107
+ else:
1108
+ return []
1109
+
1110
+ @classmethod
1111
+ def create(
1112
+ cls,
1113
+ qx: "TensorBox",
1114
+ x_scale: "TensorBox",
1115
+ x_zero_point: "TensorBox",
1116
+ qw: "TensorBox", # packed_weight
1117
+ w_scale: "TensorBox",
1118
+ w_zero_point: "TensorBox",
1119
+ other: "TensorBox",
1120
+ bias: "TensorBox",
1121
+ output_scale: float,
1122
+ output_zero_point: int,
1123
+ output_dtype,
1124
+ other_scale,
1125
+ other_zp,
1126
+ binary_post_op,
1127
+ binary_alpha,
1128
+ unary_post_op,
1129
+ unary_post_op_args,
1130
+ unary_post_op_algorithm,
1131
+ ):
1132
+ (
1133
+ inputs,
1134
+ constant_args,
1135
+ kernel_layout,
1136
+ req_stride_order,
1137
+ other,
1138
+ ) = _prepare_linear_fusion_create(
1139
+ cls,
1140
+ qx,
1141
+ qw,
1142
+ bias,
1143
+ [x_scale, x_zero_point, w_scale, w_zero_point],
1144
+ other,
1145
+ binary_post_op == "sum",
1146
+ )
1147
+
1148
+ constant_args = constant_args + [
1149
+ output_scale,
1150
+ output_zero_point,
1151
+ output_dtype,
1152
+ other_scale,
1153
+ other_zp,
1154
+ binary_post_op,
1155
+ binary_alpha,
1156
+ unary_post_op,
1157
+ may_convert_to_optional(unary_post_op_args),
1158
+ unary_post_op_algorithm,
1159
+ ]
1160
+
1161
+ if binary_post_op == "sum":
1162
+ V.graph.mark_buffer_mutated(other.get_name())
1163
+ packed = QLinearPointwiseBinaryPT2E(
1164
+ layout=NoneLayout(device=other.get_device()),
1165
+ inputs=inputs,
1166
+ constant_args=constant_args,
1167
+ has_bias=(bias is not None),
1168
+ )
1169
+ # Return other since it has been inplace changed.
1170
+ return packed.inputs[packed.idx_for_inplace_sum]
1171
+
1172
+ assert output_dtype is not None
1173
+ if output_dtype in [torch.float32, torch.bfloat16]:
1174
+ # in _prepare_linear_fusion_create, we use x.dtype (uint8) to create kernel_layout
1175
+ # if we set fp32_output, the output buf should be dtype float32 instead of uint8.
1176
+ kernel_layout.dtype = output_dtype
1177
+
1178
+ return QLinearPointwiseBinaryPT2E(
1179
+ layout=kernel_layout,
1180
+ inputs=inputs,
1181
+ constant_args=constant_args,
1182
+ has_bias=(bias is not None),
1183
+ )
1184
+
1185
+
1186
+ class MkldnnRnnLayer(ExternKernelAlloc):
1187
+ def __init__(
1188
+ self,
1189
+ layout,
1190
+ inputs,
1191
+ constant_args=(),
1192
+ ) -> None:
1193
+ super().__init__(
1194
+ layout,
1195
+ inputs,
1196
+ constant_args,
1197
+ None,
1198
+ op_overload=torch.ops.aten.mkldnn_rnn_layer.default,
1199
+ )
1200
+
1201
+ @classmethod
1202
+ def create(
1203
+ cls,
1204
+ x: "TensorBox",
1205
+ w0: "TensorBox",
1206
+ w1: "TensorBox",
1207
+ w2: "TensorBox",
1208
+ w3: "TensorBox",
1209
+ hx: "TensorBox",
1210
+ cx: "TensorBox",
1211
+ reverse: bool,
1212
+ batch_sizes: list[int],
1213
+ mode: int,
1214
+ hidden_size: int,
1215
+ num_layers: int,
1216
+ has_biases: bool,
1217
+ bidirectional: bool,
1218
+ batch_first: bool,
1219
+ train: bool,
1220
+ ):
1221
+ # pyrefly: ignore [bad-assignment]
1222
+ x = cls.require_stride1(cls.realize_input(x))
1223
+ # If batch_first, x has been permuted in lstm before entering the mkldnn_rnn_layer.
1224
+ # Make sure x is contiguous in batch_first case.
1225
+ x.freeze_layout()
1226
+ # pyrefly: ignore [bad-assignment]
1227
+ w0 = cls.require_stride1(cls.realize_input(w0))
1228
+ # pyrefly: ignore [bad-assignment]
1229
+ w1 = cls.require_stride1(cls.realize_input(w1))
1230
+ # pyrefly: ignore [bad-assignment]
1231
+ w2 = cls.require_stride1(cls.realize_input(w2))
1232
+ # pyrefly: ignore [bad-assignment]
1233
+ w3 = cls.require_stride1(cls.realize_input(w3))
1234
+ # pyrefly: ignore [bad-assignment]
1235
+ hx = cls.require_stride1(cls.realize_input(hx))
1236
+ hx.freeze_layout()
1237
+ # pyrefly: ignore [bad-assignment]
1238
+ cx = cls.require_stride1(cls.realize_input(cx))
1239
+ cx.freeze_layout()
1240
+
1241
+ input_size = x.get_size()
1242
+ assert len(input_size) == 3, "Expect lstm input to be 3D"
1243
+ # batch_first is handled in the lstm OP. When entering
1244
+ # rnn_layer here, we'll always have batch_first = False
1245
+ seq_length, mini_batch, input_size = input_size
1246
+ output_shape = [seq_length, mini_batch, hidden_size]
1247
+
1248
+ hy_shape = hx.get_size()
1249
+ cy_shape = cx.get_size()
1250
+
1251
+ inputs = [x, w0, w1, w2, w3, hx, cx]
1252
+ constant_args = [
1253
+ reverse,
1254
+ batch_sizes,
1255
+ mode,
1256
+ hidden_size,
1257
+ num_layers,
1258
+ has_biases,
1259
+ bidirectional,
1260
+ batch_first,
1261
+ train,
1262
+ ]
1263
+
1264
+ device = x.get_device()
1265
+ assert device is not None
1266
+ packed = MkldnnRnnLayer(
1267
+ MultiOutputLayout(device=device),
1268
+ inputs=inputs,
1269
+ constant_args=constant_args,
1270
+ )
1271
+
1272
+ def get_strides_of_lstm_output(output_shape, batch_first):
1273
+ assert len(output_shape) == 3, "Expect output_shape to be 3D"
1274
+ return FlexibleLayout.contiguous_strides(output_shape)
1275
+
1276
+ # C shim call requires all the outputs to be passed in, and thus the last
1277
+ # dummy return value is added.
1278
+ output_sizes = [output_shape, hy_shape, cy_shape, [1]]
1279
+ output_strides = [
1280
+ get_strides_of_lstm_output(output_shape, batch_first),
1281
+ FlexibleLayout.contiguous_strides(hy_shape),
1282
+ FlexibleLayout.contiguous_strides(cy_shape),
1283
+ [1],
1284
+ ]
1285
+ output_ir = [
1286
+ MultiOutput(
1287
+ FixedLayout(
1288
+ x.get_device(), # type: ignore[arg-type]
1289
+ x.get_dtype(),
1290
+ output_size,
1291
+ output_stride,
1292
+ ),
1293
+ packed,
1294
+ [(tuple, i)],
1295
+ )
1296
+ for i, (output_size, output_stride) in enumerate(
1297
+ zip(output_sizes, output_strides)
1298
+ )
1299
+ ]
1300
+ packed.outputs = output_ir
1301
+
1302
+ return output_ir
1303
+
1304
+ def codegen(self, wrapper):
1305
+ wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h")
1306
+ return super().codegen(wrapper)
1307
+
1308
+
1309
+ # Add this IR so that we can include shim_cpu.h for cpp_wrapper
1310
+ class WeightInt4PackMatmul(ExternKernelAlloc):
1311
+ def __init__(
1312
+ self,
1313
+ layout,
1314
+ inputs,
1315
+ constant_args=(),
1316
+ ) -> None:
1317
+ """
1318
+ inputs = [x, w, qGroupSize, qScalesAndZeros]
1319
+ constant_args = ()
1320
+ """
1321
+ assert len(inputs) == 4
1322
+ assert len(constant_args) == 0
1323
+ super().__init__(
1324
+ layout,
1325
+ inputs,
1326
+ constant_args,
1327
+ None,
1328
+ op_overload=(torch.ops.quantized.int4mm_packed_weight_cpu.default),
1329
+ cpp_kernel_name=("aoti_torch_cpu__weight_int4pack_mm_cpu_tensor"),
1330
+ )
1331
+
1332
+ def codegen(self, wrapper):
1333
+ wrapper.include_extra_header("torch/csrc/inductor/aoti_torch/c/shim_cpu.h")
1334
+ super().codegen(wrapper)
1335
+
1336
+ if isinstance(self.layout, Layout):
1337
+ self.codegen_size_asserts(wrapper)
1338
+
1339
+ @classmethod
1340
+ def create(
1341
+ cls,
1342
+ x: "TensorBox",
1343
+ w: "TensorBox",
1344
+ qGroupSize: "TensorBox",
1345
+ qScalesAndZeros: "TensorBox",
1346
+ ):
1347
+ inputs = [x, w, qGroupSize, qScalesAndZeros]
1348
+ *m, _ = x.get_size()
1349
+ n, _ = w.get_size()
1350
+ output_size = list(m) + [n]
1351
+ output_stride = FlexibleLayout.contiguous_strides(output_size)
1352
+ kernel_layout = FixedLayout(
1353
+ x.get_device(), # type: ignore[arg-type]
1354
+ x.get_dtype(),
1355
+ output_size,
1356
+ output_stride,
1357
+ )
1358
+ return WeightInt4PackMatmul(
1359
+ layout=kernel_layout,
1360
+ inputs=inputs,
1361
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mkldnn_lowerings.py ADDED
@@ -0,0 +1,1399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ import functools
3
+
4
+ import torch
5
+ import torch.utils._pytree as pytree
6
+ from torch._inductor.kernel.mm_common import mm_args
7
+
8
+ from . import config, ir
9
+ from .codegen.cpp_gemm_template import CppGemmTemplate
10
+ from .codegen.cpp_grouped_gemm_template import CppGroupedGemmTemplate
11
+ from .codegen.cpp_utils import create_epilogue_with_attr
12
+ from .ir import TensorBox
13
+ from .lowering import (
14
+ add,
15
+ add_needs_realized_inputs,
16
+ aten,
17
+ permute,
18
+ register_lowering,
19
+ to_dtype,
20
+ view,
21
+ )
22
+ from .select_algorithm import (
23
+ autotune_select_algorithm,
24
+ ChoiceCaller,
25
+ ExternKernelChoice,
26
+ )
27
+ from .utils import use_aten_gemm_kernels, use_cpp_gemm_template
28
+ from .virtualized import ops, OpsValue, V
29
+
30
+
31
+ def create_int8_compensation(
32
+ W_tensor: torch.Tensor,
33
+ packed_weight: ir.TensorBox,
34
+ x_scale: ir.TensorBox,
35
+ x_zp: ir.TensorBox,
36
+ w_scale: ir.TensorBox,
37
+ ) -> tuple[
38
+ bool,
39
+ ir.TensorBox,
40
+ ir.TensorBox | None,
41
+ ]:
42
+ x_w_scale: ir.TensorBox | None = None
43
+ use_int8_fast_compensation_path = all(
44
+ isinstance(item, ir.TensorBox)
45
+ and item.get_name() in V.graph.constants
46
+ and hasattr(item.data, "data")
47
+ and isinstance(item.data.data, ir.ConstantBuffer)
48
+ for item in [x_scale, x_zp, w_scale]
49
+ )
50
+ if use_int8_fast_compensation_path:
51
+ x_w_scale_tensor = (
52
+ V.graph.constants[x_scale.get_name()]
53
+ * V.graph.constants[w_scale.get_name()]
54
+ )
55
+ x_w_scale = V.graph.add_tensor_constant(
56
+ x_w_scale_tensor,
57
+ name=packed_weight.get_name() + "_x_w_compens",
58
+ )
59
+ weight_compens_tensor = torch.sum(W_tensor.to(torch.float), dim=0)
60
+ x_zp_tensor = V.graph.constants[x_zp.get_name()]
61
+ weight_compens_tensor = weight_compens_tensor * x_w_scale_tensor * x_zp_tensor
62
+ weight_compens = V.graph.add_tensor_constant(
63
+ weight_compens_tensor,
64
+ name=packed_weight.get_name() + "_BMatrixCompens",
65
+ )
66
+ else:
67
+ weight_compens_tensor = torch.sum(W_tensor.to(torch.float), dim=0)
68
+ weight_compens = V.graph.add_tensor_constant(
69
+ weight_compens_tensor,
70
+ name=packed_weight.get_name() + "_BMatrixCompens",
71
+ )
72
+ return ( # type: ignore[return-type]
73
+ use_int8_fast_compensation_path,
74
+ weight_compens,
75
+ x_w_scale,
76
+ )
77
+
78
+
79
+ def codegen_int8_gemm_template_compensation(
80
+ use_int8_fast_compensation_path: bool,
81
+ input: OpsValue,
82
+ _weight_compo: OpsValue,
83
+ _x_scale: OpsValue | None,
84
+ _x_zp: OpsValue | None,
85
+ _w_scale: OpsValue | None,
86
+ _x_w_scale: OpsValue | None,
87
+ ) -> OpsValue:
88
+ if use_int8_fast_compensation_path:
89
+ temp = ops.sub(
90
+ ops.mul(
91
+ input,
92
+ _x_w_scale,
93
+ ),
94
+ _weight_compo,
95
+ )
96
+ else:
97
+ temp = ops.mul(
98
+ ops.mul(
99
+ input,
100
+ _x_scale,
101
+ ),
102
+ _w_scale,
103
+ )
104
+ # NOTE: We will apply compensation even if the x_zp is 0 for int8 quantization.
105
+ # That's because when torch.compile is invoked for dynamic quantization,
106
+ # x might coincidentally have such values that x_zp might be zero despite
107
+ # asymmetric quantization.
108
+ # Besides, if x_zp is dummy for int8 x, or if x is statically quantized,
109
+ # we'd still perform that redundant compute to avoid making the code messy
110
+ # because we discovered that redundant computation of compensation did not
111
+ # lead to performance degradation with the input shapes tested.
112
+ temp = ops.sub(
113
+ temp,
114
+ ops.mul(
115
+ ops.mul(
116
+ ops.mul(
117
+ _x_scale,
118
+ _w_scale,
119
+ ),
120
+ _x_zp,
121
+ ),
122
+ _weight_compo,
123
+ ),
124
+ )
125
+ return temp
126
+
127
+
128
+ def grouped_gemm_lowering(
129
+ x: TensorBox,
130
+ w: list[TensorBox],
131
+ b: list[TensorBox],
132
+ attr=None,
133
+ scalars=None,
134
+ algorithm=None,
135
+ layout=None,
136
+ ):
137
+ x_size = x.get_size()
138
+ if len(x_size) > 2:
139
+ # GEMM template needs 2D input, normalize input shape here
140
+ x = view(x, [-1, x_size[-1]])
141
+ num_gemm = len(w)
142
+
143
+ assert config.max_autotune or config.max_autotune_gemm
144
+ # pyrefly: ignore [bad-assignment]
145
+ b = [bias if bias is None else ir.ExternKernel.realize_input(bias) for bias in b]
146
+
147
+ choices: list[ChoiceCaller] = []
148
+ *_, layout, x, _ = mm_args(x, permute(w[0], [1, 0]), layout=layout)
149
+
150
+ kwargs = {
151
+ "has_bias": [bias is not None for bias in b],
152
+ "trans_w": True,
153
+ "epilogue_creator": None,
154
+ "act_mapping": dict.fromkeys(range(num_gemm), x),
155
+ }
156
+
157
+ input_nodes = [x, *w]
158
+ input_nodes.extend([bias for bias in b if bias is not None])
159
+
160
+ CppGroupedGemmTemplate.add_choices(
161
+ choices,
162
+ layout,
163
+ input_nodes,
164
+ **kwargs, # type: ignore[arg-type]
165
+ )
166
+
167
+ assert len(choices) != 0
168
+ result, _ = autotune_select_algorithm(
169
+ "grouped_gemm",
170
+ choices,
171
+ input_nodes,
172
+ layout,
173
+ )
174
+ template_buf = result.data.data
175
+ return_bufs = [
176
+ ir.MultiOutput(layout, template_buf, [(list, gemm_idx)])
177
+ for gemm_idx in range(num_gemm)
178
+ ]
179
+ # pyrefly: ignore [bad-argument-type]
180
+ template_buf.layout = ir.MultiOutputLayout(device=input_nodes[0].get_device())
181
+ template_buf.outputs = return_bufs
182
+ return_tensors = [
183
+ ir.TensorBox.create(return_bufs[gemm_idx]) for gemm_idx in range(num_gemm)
184
+ ]
185
+ if len(x_size) > 2:
186
+ for gemm_idx in range(num_gemm):
187
+ return_tensors[gemm_idx] = view(
188
+ return_tensors[gemm_idx], # type: ignore[arg-type]
189
+ (*x_size[:-1], return_tensors[gemm_idx].get_size()[-1]),
190
+ )
191
+ return return_tensors
192
+
193
+
194
+ grouped_gemm_lowering._inductor_lowering_function = True # type: ignore[attr-defined]
195
+
196
+
197
+ def register_onednn_fusion_ops():
198
+ if torch._C._has_mkldnn:
199
+ from . import mkldnn_ir
200
+
201
+ aten_mkldnn_linear_unary = ExternKernelChoice(
202
+ torch.ops.mkldnn._linear_pointwise,
203
+ "mkldnn::_linear_pointwise",
204
+ has_out_variant=False,
205
+ kernel_creator=mkldnn_ir.LinearUnary.create,
206
+ )
207
+ aten_mkldnn_linear_binary = ExternKernelChoice(
208
+ torch.ops.mkldnn._linear_pointwise.binary,
209
+ "mkldnn::_linear_pointwise",
210
+ has_out_variant=False,
211
+ kernel_creator=mkldnn_ir.LinearBinary.create,
212
+ )
213
+ aten_mkldnn_qlinear_unary = ExternKernelChoice(
214
+ torch.ops.onednn.qlinear_pointwise,
215
+ "onednn::qlinear_pointwise",
216
+ has_out_variant=False,
217
+ kernel_creator=mkldnn_ir.QLinearPointwisePT2E.create,
218
+ )
219
+ aten_mkldnn_qlinear_binary = ExternKernelChoice(
220
+ torch.ops.onednn.qlinear_pointwise.binary,
221
+ "onednn::qlinear_pointwise",
222
+ has_out_variant=False,
223
+ kernel_creator=mkldnn_ir.QLinearPointwiseBinaryPT2E.create,
224
+ )
225
+ cpu_needs_realized_inputs: list[
226
+ torch._ops.OpOverload | torch._ops.OpOverloadPacket
227
+ ] = [
228
+ torch.ops.mkldnn._convolution_pointwise,
229
+ torch.ops.mkldnn._convolution_pointwise_,
230
+ torch.ops.mkldnn._convolution_transpose_pointwise,
231
+ torch.ops.mkldnn._linear_pointwise,
232
+ aten.mkldnn_rnn_layer.default,
233
+ torch.ops.onednn.qconv_pointwise,
234
+ ]
235
+
236
+ @register_lowering(torch.ops.mkldnn._convolution_pointwise)
237
+ def convolution_unary(
238
+ x: TensorBox,
239
+ weight: TensorBox,
240
+ bias: TensorBox,
241
+ padding,
242
+ stride,
243
+ dilation,
244
+ groups,
245
+ attr,
246
+ scalars,
247
+ algorithm,
248
+ ):
249
+ return TensorBox.create(
250
+ mkldnn_ir.ConvolutionUnary.create(
251
+ x,
252
+ weight,
253
+ bias,
254
+ padding,
255
+ stride,
256
+ dilation,
257
+ groups,
258
+ attr,
259
+ scalars,
260
+ algorithm,
261
+ )
262
+ )
263
+
264
+ @register_lowering(torch.ops.mkldnn._convolution_pointwise.binary)
265
+ def convolution_binary(
266
+ x: TensorBox,
267
+ other: TensorBox,
268
+ weight: TensorBox,
269
+ bias: TensorBox,
270
+ padding,
271
+ stride,
272
+ dilation,
273
+ groups,
274
+ binary_attr,
275
+ binary_alpha,
276
+ unary_attr,
277
+ unary_scalars,
278
+ unary_algorithm,
279
+ ):
280
+ return TensorBox.create(
281
+ mkldnn_ir.ConvolutionBinary.create(
282
+ x,
283
+ other,
284
+ weight,
285
+ bias,
286
+ padding,
287
+ stride,
288
+ dilation,
289
+ groups,
290
+ binary_attr,
291
+ binary_alpha,
292
+ unary_attr,
293
+ unary_scalars,
294
+ unary_algorithm,
295
+ )
296
+ )
297
+
298
+ @register_lowering(torch.ops.mkldnn._convolution_pointwise_.binary)
299
+ def convolution_binary_inplace(
300
+ x: TensorBox,
301
+ other: TensorBox,
302
+ weight: TensorBox,
303
+ bias: TensorBox,
304
+ padding,
305
+ stride,
306
+ dilation,
307
+ groups,
308
+ binary_attr,
309
+ binary_alpha,
310
+ unary_attr,
311
+ unary_scalars,
312
+ unary_algorithm,
313
+ ):
314
+ return TensorBox.create(
315
+ mkldnn_ir.ConvolutionBinaryInplace.create(
316
+ x,
317
+ other,
318
+ weight,
319
+ bias,
320
+ padding,
321
+ stride,
322
+ dilation,
323
+ groups,
324
+ binary_attr,
325
+ binary_alpha,
326
+ unary_attr,
327
+ unary_scalars,
328
+ unary_algorithm,
329
+ )
330
+ )
331
+
332
+ @register_lowering(torch.ops.mkldnn._linear_pointwise)
333
+ def linear_unary(
334
+ x: TensorBox,
335
+ w: TensorBox,
336
+ b: TensorBox,
337
+ attr,
338
+ scalars,
339
+ algorithm,
340
+ layout=None,
341
+ ):
342
+ x_size = x.get_size()
343
+ if len(x_size) > 2:
344
+ # GEMM template needs 2D input, normalize input shape here
345
+ x = view(x, [-1, x_size[-1]])
346
+ if b is not None:
347
+ b = ir.ExternKernel.realize_input(b) # type: ignore[assignment]
348
+ choices: list[ChoiceCaller] = []
349
+ if config.max_autotune or config.max_autotune_gemm:
350
+ transposed_w = permute(w, [1, 0])
351
+ *_, layout, x, transposed_w = mm_args(x, transposed_w, layout=layout)
352
+ if use_cpp_gemm_template(layout, x, transposed_w):
353
+
354
+ def epilogue_creator(buf):
355
+ return create_epilogue_with_attr(
356
+ buf, attr, scalars=scalars, algorithm=algorithm
357
+ )
358
+
359
+ kwargs = {
360
+ "has_bias": b is not None,
361
+ "trans_w": True,
362
+ "epilogue_creator": (
363
+ None if attr == "none" else epilogue_creator
364
+ ),
365
+ }
366
+ if b is not None:
367
+ kwargs["input_indices"] = [2, 0, 1] # type: ignore[assignment]
368
+ CppGemmTemplate.add_choices(
369
+ choices,
370
+ layout,
371
+ [x, w] if b is None else [x, w, b],
372
+ **kwargs, # type: ignore[arg-type]
373
+ )
374
+ if len(choices) == 0 or use_aten_gemm_kernels():
375
+ kwargs = dict(attr=attr, scalars=scalars, algorithm=algorithm)
376
+ if b is None:
377
+ kwargs["B"] = None
378
+ choices.append(
379
+ aten_mkldnn_linear_unary.bind(
380
+ [x, w] if b is None else [x, w, b],
381
+ layout,
382
+ **kwargs,
383
+ )
384
+ )
385
+ assert w.get_name() in V.graph.constants
386
+ input_gen_fns = {
387
+ 1: lambda x: V.graph.constants[x.get_name()],
388
+ }
389
+ result, _ = autotune_select_algorithm(
390
+ "linear_unary",
391
+ choices,
392
+ [x, w] if b is None else [x, w, b],
393
+ layout,
394
+ input_gen_fns=input_gen_fns,
395
+ )
396
+ if len(x_size) > 2:
397
+ result = view(result, (*x_size[:-1], result.get_size()[-1]))
398
+ return result
399
+
400
+ @register_lowering(torch.ops.mkldnn._linear_pointwise.binary)
401
+ def linear_binary(
402
+ x: TensorBox, y: TensorBox, w: TensorBox, b: TensorBox, attr, layout=None
403
+ ):
404
+ x_size = x.get_size()
405
+ if len(x_size) > 2:
406
+ # GEMM template needs 2D input, normalize input shape here
407
+ x = view(x, [-1, x_size[-1]])
408
+ y_size = y.get_size()
409
+ if len(y_size) > 2:
410
+ y = view(y, [-1, y_size[-1]])
411
+ if b is not None:
412
+ b = ir.ExternKernel.realize_input(b) # type: ignore[assignment]
413
+ choices: list[ChoiceCaller] = []
414
+ if config.max_autotune or config.max_autotune_gemm:
415
+ transposed_w = permute(w, [1, 0])
416
+ *_, layout, x, transposed_w, y = mm_args(
417
+ x, transposed_w, y, layout=layout
418
+ )
419
+ if use_cpp_gemm_template(layout, x, transposed_w):
420
+
421
+ def epilogue_creator(buf):
422
+ return create_epilogue_with_attr(buf, attr, other=y)
423
+
424
+ kwargs = {
425
+ "has_bias": b is not None,
426
+ "trans_w": True,
427
+ "epilogue_creator": epilogue_creator,
428
+ }
429
+
430
+ # pyrefly: ignore [bad-typed-dict-key, unsupported-operation]
431
+ kwargs["input_indices"] = [0, 2, 1] if b is None else [3, 0, 2, 1]
432
+ CppGemmTemplate.add_choices(
433
+ choices,
434
+ layout,
435
+ [x, y, w] if b is None else [x, y, w, b],
436
+ **kwargs, # type: ignore[arg-type]
437
+ )
438
+ if len(choices) == 0 or use_aten_gemm_kernels():
439
+ kwargs = dict(attr=attr)
440
+ if b is None:
441
+ kwargs["B"] = None
442
+ choices.append(
443
+ aten_mkldnn_linear_binary.bind(
444
+ [x, y, w] if b is None else [x, y, w, b],
445
+ layout,
446
+ **kwargs,
447
+ )
448
+ )
449
+ assert w.get_name() in V.graph.constants
450
+ input_gen_fns = {
451
+ 2: lambda x: V.graph.constants[x.get_name()],
452
+ }
453
+ result, _ = autotune_select_algorithm(
454
+ "linear_binary",
455
+ choices,
456
+ [x, y, w] if b is None else [x, y, w, b],
457
+ layout,
458
+ input_gen_fns=input_gen_fns,
459
+ )
460
+ if len(x_size) > 2:
461
+ result = view(result, (*x_size[:-1], result.get_size()[-1]))
462
+ return result
463
+
464
+ @register_lowering(torch.ops.mkldnn._convolution_transpose_pointwise)
465
+ def convolution_transpose_unary(
466
+ x: TensorBox,
467
+ weight: TensorBox,
468
+ bias: TensorBox,
469
+ padding,
470
+ output_padding,
471
+ stride,
472
+ dilation,
473
+ groups,
474
+ attr,
475
+ scalars,
476
+ algorithm,
477
+ ):
478
+ return TensorBox.create(
479
+ mkldnn_ir.ConvolutionTransposeUnary.create(
480
+ x,
481
+ weight,
482
+ bias,
483
+ padding,
484
+ output_padding,
485
+ stride,
486
+ dilation,
487
+ groups,
488
+ attr,
489
+ scalars,
490
+ algorithm,
491
+ )
492
+ )
493
+
494
+ @register_lowering(aten.mkldnn_rnn_layer.default)
495
+ def mkldnn_rnn_layer(
496
+ x: TensorBox,
497
+ w0: TensorBox,
498
+ w1: TensorBox,
499
+ w2: TensorBox,
500
+ w3: TensorBox,
501
+ hx: TensorBox,
502
+ cx: TensorBox,
503
+ reverse: bool,
504
+ batch_sizes: list[int],
505
+ mode: int,
506
+ hidden_size: int,
507
+ num_layers: int,
508
+ has_biases: bool,
509
+ bidirectional: bool,
510
+ batch_first: bool,
511
+ train: bool,
512
+ ):
513
+ return pytree.tree_map(
514
+ TensorBox.create,
515
+ mkldnn_ir.MkldnnRnnLayer.create(
516
+ x,
517
+ w0,
518
+ w1,
519
+ w2,
520
+ w3,
521
+ hx,
522
+ cx,
523
+ reverse,
524
+ batch_sizes,
525
+ mode,
526
+ hidden_size,
527
+ num_layers,
528
+ has_biases,
529
+ bidirectional,
530
+ batch_first,
531
+ train,
532
+ ),
533
+ )
534
+
535
+ @register_lowering(torch.ops.onednn.qconv_pointwise, type_promotion_kind=None)
536
+ def qconvolution_unary(
537
+ x: TensorBox,
538
+ x_scale,
539
+ x_zp,
540
+ packed_weight: TensorBox,
541
+ w_scale: TensorBox,
542
+ w_zp,
543
+ bias: TensorBox,
544
+ stride,
545
+ padding,
546
+ dilation,
547
+ groups,
548
+ o_inv_scale,
549
+ o_zero_point,
550
+ output_dtype,
551
+ attr,
552
+ scalars,
553
+ algorithm,
554
+ ):
555
+ if not isinstance(x_scale, ir.TensorBox):
556
+ assert type(x_scale) is float
557
+ x_scale = V.graph.add_tensor_constant(
558
+ torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
559
+ )
560
+
561
+ if x_zp is None:
562
+ x_zp = V.graph.add_tensor_constant(
563
+ torch.tensor(0, dtype=torch.int32), name="x_zp"
564
+ )
565
+ if not isinstance(x_zp, ir.TensorBox):
566
+ assert type(x_zp) is int
567
+ x_zp = V.graph.add_tensor_constant(
568
+ torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
569
+ )
570
+
571
+ if w_zp is None:
572
+ w_zp = V.graph.add_tensor_constant(
573
+ torch.tensor(0, dtype=torch.int32), name="w_zp"
574
+ )
575
+
576
+ return TensorBox.create(
577
+ mkldnn_ir.QConvPointWisePT2E.create(
578
+ x,
579
+ x_scale,
580
+ x_zp,
581
+ packed_weight,
582
+ w_scale,
583
+ w_zp,
584
+ bias,
585
+ stride,
586
+ padding,
587
+ dilation,
588
+ groups,
589
+ o_inv_scale,
590
+ o_zero_point,
591
+ output_dtype,
592
+ attr,
593
+ scalars,
594
+ algorithm,
595
+ )
596
+ )
597
+
598
+ @register_lowering(
599
+ torch.ops.onednn.qconv2d_pointwise.binary, type_promotion_kind=None
600
+ )
601
+ @register_lowering(
602
+ torch.ops.onednn.qconv2d_pointwise.binary_tensor, type_promotion_kind=None
603
+ )
604
+ def qconvolution_binary(
605
+ x: TensorBox,
606
+ x_scale,
607
+ x_zp,
608
+ packed_weight: TensorBox,
609
+ w_scale: TensorBox,
610
+ w_zp,
611
+ accum: TensorBox,
612
+ bias: TensorBox,
613
+ stride,
614
+ padding,
615
+ dilation,
616
+ groups,
617
+ o_inv_scale,
618
+ o_zero_point,
619
+ output_dtype,
620
+ accum_scale,
621
+ accum_zp,
622
+ binary_attr,
623
+ alpha,
624
+ unary_attr,
625
+ unary_scalars,
626
+ unary_algorithm,
627
+ ):
628
+ if not isinstance(x_scale, ir.TensorBox):
629
+ assert type(x_scale) is float
630
+ x_scale = V.graph.add_tensor_constant(
631
+ torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
632
+ )
633
+
634
+ if x_zp is None:
635
+ x_zp = V.graph.add_tensor_constant(
636
+ torch.tensor(0, dtype=torch.int32), name="x_zp"
637
+ )
638
+ if not isinstance(x_zp, ir.TensorBox):
639
+ assert type(x_zp) is int
640
+ x_zp = V.graph.add_tensor_constant(
641
+ torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
642
+ )
643
+
644
+ if w_zp is None:
645
+ w_zp = V.graph.add_tensor_constant(
646
+ torch.tensor(0, dtype=torch.int32), name="w_zp"
647
+ )
648
+
649
+ if (
650
+ binary_attr == "sum"
651
+ and output_dtype in [torch.float32, torch.bfloat16]
652
+ and accum.get_dtype() in [torch.float32, torch.bfloat16]
653
+ and accum.get_dtype() != output_dtype
654
+ ):
655
+ # For int8-mixed-bf16 quantization and inplace add,
656
+ # there is case when accum dtype is float32 but output dtype is bfloat16.
657
+ # Since the accum will be inplaced changed with post op sum,
658
+ # we will do accum dtype conversion here.
659
+ accum = to_dtype(accum, output_dtype)
660
+ return TensorBox.create(
661
+ mkldnn_ir.QConvPointWiseBinaryPT2E.create(
662
+ x,
663
+ x_scale, # type: ignore[arg-type]
664
+ x_zp, # type: ignore[arg-type]
665
+ packed_weight,
666
+ w_scale,
667
+ w_zp,
668
+ accum,
669
+ bias,
670
+ stride,
671
+ padding,
672
+ dilation,
673
+ groups,
674
+ o_inv_scale,
675
+ o_zero_point,
676
+ output_dtype,
677
+ accum_scale,
678
+ accum_zp,
679
+ binary_attr,
680
+ alpha,
681
+ unary_attr,
682
+ unary_scalars,
683
+ unary_algorithm,
684
+ )
685
+ )
686
+
687
+ @register_lowering(torch.ops.onednn.qlinear_pointwise, type_promotion_kind=None)
688
+ def qlinear_unary(
689
+ x: TensorBox,
690
+ x_scale,
691
+ x_zp,
692
+ packed_weight: TensorBox,
693
+ w_scale: TensorBox,
694
+ w_zp: TensorBox,
695
+ bias: TensorBox,
696
+ o_scale,
697
+ o_zero_point,
698
+ output_dtype,
699
+ attr,
700
+ scalars,
701
+ algorithm,
702
+ layout=None,
703
+ ):
704
+ assert packed_weight.get_dtype() in [torch.int8, torch.float8_e4m3fn], (
705
+ "Only int8 and e4m3fn weights are supported by oneDNN qlinear."
706
+ )
707
+ x_size = x.get_size()
708
+ if len(x_size) > 2:
709
+ # GEMM template needs 2D input, normalize input shape here
710
+ x = view(x, [-1, x_size[-1]])
711
+ if not isinstance(x_scale, ir.TensorBox):
712
+ assert type(x_scale) is float
713
+ x_scale = V.graph.add_tensor_constant(
714
+ torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
715
+ )
716
+ else:
717
+ x_scale.realize()
718
+ if all(dim == 1 for dim in x_scale.get_size()):
719
+ # Corner-case discovered with LLaMA series.
720
+ # If all outer dims of x_scale are 1, make it a 0D tensor.
721
+ # Otherwise, epilogue creator will run into indexing issues.
722
+ x_scale = view(x_scale, [])
723
+ assert len(x_scale.get_size()) in [0, 1], "x_scale must be 0D or 1D"
724
+
725
+ if x_zp is None:
726
+ # If x_zp is None, x is int8 quantized per-tensor and its scale is not reshaped,
727
+ # then the codegened code would segfault if we don't create a tensor for x_zp.
728
+ # It's safe to do so since x is a symmetrically quantized int8 tensor.
729
+ # Moreover, oneDNN qlinear API doesn't accept None value for zp
730
+ x_zp = V.graph.add_tensor_constant(
731
+ torch.tensor(0, dtype=torch.int32), name="x_zp"
732
+ )
733
+ if not isinstance(x_zp, ir.TensorBox):
734
+ assert type(x_zp) is int
735
+ x_zp = V.graph.add_tensor_constant(
736
+ torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
737
+ )
738
+ else:
739
+ x_zp.realize()
740
+
741
+ assert x_zp.get_numel() == 1, "x_zp is incompatible with oneDNN qlinear"
742
+
743
+ # When channels less than 8, w_scale/w_zp is Pointwise instead of ConstantBuffer
744
+ # Refer to
745
+ # https://github.com/pytorch/pytorch/blob/f353d17755ed23b02924c962a86ff99a3405fe10/torch/_inductor/graph.py#L570-L577 # noqa: B950
746
+ if w_zp is None:
747
+ # If w_zp is None, then it's a dummy tensor created to denote the
748
+ # absence of a zero point, and thus w is int8 symmetrically quantized.
749
+ # Moreover, oneDNN qlinear API doesn't accept None value for zp
750
+
751
+ w_zp = V.graph.add_tensor_constant(
752
+ torch.tensor(0, dtype=torch.int32), name="w_zp"
753
+ )
754
+ w_scale.realize()
755
+ w_zp.realize()
756
+ if w_zp.get_dtype() != torch.int32 and isinstance(
757
+ ir.InputsKernel.unwrap_storage_for_input(w_zp),
758
+ ir.ConstantBuffer,
759
+ ):
760
+ # W_zp might be a ConstantBuffer with int64, convert it to int32
761
+ w_zp_tensor = V.graph.constants[w_zp.get_name()].to(torch.int32)
762
+ w_zp = V.graph.add_tensor_constant( # type: ignore[assignment]
763
+ torch.tensor(w_zp_tensor, dtype=torch.int32), name=w_zp.get_name()
764
+ )
765
+
766
+ bias_dtype = None if bias is None else bias.get_dtype()
767
+ choices: list[ChoiceCaller] = []
768
+
769
+ if config.max_autotune or config.max_autotune_gemm:
770
+ *_, layout, x, packed_weight = mm_args(
771
+ x, packed_weight, layout=layout, out_dtype=output_dtype
772
+ )
773
+
774
+ if (
775
+ # GEMM template currently only supports symmetrically quantized weights
776
+ isinstance(
777
+ ir.InputsKernel.unwrap_storage_for_input(w_zp),
778
+ ir.ConstantBuffer,
779
+ )
780
+ and torch.equal(
781
+ torch.zeros_like(V.graph.constants[w_zp.get_name()]),
782
+ V.graph.constants[w_zp.get_name()],
783
+ )
784
+ ) and use_cpp_gemm_template(layout, x, packed_weight):
785
+ W_tensor = V.graph.constants[packed_weight.get_name()].to_dense()
786
+
787
+ (
788
+ use_int8_fast_compensation_path,
789
+ weight_compens,
790
+ x_w_scale,
791
+ ) = create_int8_compensation(
792
+ W_tensor,
793
+ packed_weight,
794
+ x_scale,
795
+ x_zp,
796
+ w_scale,
797
+ )
798
+
799
+ def epilogue_creator(input_buffer):
800
+ # Epilogue to convert from s32 to f32 for u8s8f32
801
+ assert output_dtype in [
802
+ torch.float32,
803
+ torch.bfloat16,
804
+ torch.uint8,
805
+ torch.int8,
806
+ ]
807
+ input_loader = input_buffer.make_loader()
808
+ weight_compens_loader = weight_compens.make_loader()
809
+ x_w_scale_loader = None
810
+ if use_int8_fast_compensation_path:
811
+ assert x_w_scale is not None
812
+ x_w_scale_loader = x_w_scale.make_loader()
813
+ x_scale_loader = x_scale.make_loader()
814
+ w_scale_loader = w_scale.make_loader()
815
+ x_zp_loader = x_zp.make_loader()
816
+ nonlocal bias
817
+ bias_loader = None
818
+ if bias is not None:
819
+ bias_loader = bias.make_loader()
820
+
821
+ def inner_fn(index):
822
+ nonlocal bias
823
+ input = input_loader(index)
824
+ # MicroKernel Output is with int32
825
+ # cvt to FP32 before doing compensation
826
+ input = ops.to_dtype(input, torch.float32)
827
+ weight_compens_index = (index[-1],)
828
+
829
+ _x_scale = None
830
+ _x_zp = None
831
+ _w_scale = None
832
+ if not use_int8_fast_compensation_path:
833
+ _x_scale = x_scale_loader(())
834
+ _x_zp = x_zp_loader(())
835
+ _w_scale = w_scale_loader(weight_compens_index)
836
+ _weight_compo = weight_compens_loader(weight_compens_index)
837
+ _x_w_scale = None
838
+ if use_int8_fast_compensation_path:
839
+ assert x_w_scale_loader is not None
840
+ _x_w_scale = x_w_scale_loader(weight_compens_index)
841
+ # Step 1: Compute s8s8->s32 or u8s8->s32 GEMM & then apply compensation
842
+ temp = codegen_int8_gemm_template_compensation(
843
+ use_int8_fast_compensation_path,
844
+ input,
845
+ _weight_compo,
846
+ _x_scale,
847
+ _x_zp,
848
+ _w_scale,
849
+ _x_w_scale,
850
+ )
851
+ # Step 2: add Bias if applicable
852
+ if bias is not None:
853
+ # pyrefly: ignore [not-callable]
854
+ _bias = bias_loader(weight_compens_index)
855
+ nonlocal bias_dtype
856
+ assert bias_dtype in [torch.float32, torch.bfloat16]
857
+ if bias_dtype == torch.bfloat16:
858
+ _bias = ops.to_dtype(_bias, torch.float32)
859
+ temp = ops.add(temp, _bias)
860
+
861
+ return temp
862
+
863
+ output_buf = ir.Pointwise(
864
+ device=input_buffer.get_device(),
865
+ dtype=torch.float32, # Hardcode to FP32 for u8s8f32 & s8s8f32
866
+ inner_fn=inner_fn,
867
+ ranges=input_buffer.get_size(),
868
+ )
869
+
870
+ # Step 3: Doing the unary post op fusion
871
+ if attr != "none":
872
+ output_buf = create_epilogue_with_attr(
873
+ output_buf, attr, scalars=scalars, algorithm=algorithm
874
+ )
875
+
876
+ # Step 4: Cast output to Target Dtype
877
+ if output_dtype == torch.bfloat16:
878
+ output_cast_loader = output_buf.make_loader()
879
+
880
+ def inner_fn_cast_output_to_bf16(index):
881
+ input = output_cast_loader(index)
882
+ return ops.to_dtype(input, output_dtype)
883
+
884
+ output_buf = ir.Pointwise(
885
+ device=output_buf.get_device_or_error(),
886
+ dtype=output_dtype,
887
+ inner_fn=inner_fn_cast_output_to_bf16,
888
+ ranges=output_buf.get_size(),
889
+ )
890
+ elif output_dtype in [torch.uint8, torch.int8]:
891
+ from .lowering import _create_constants
892
+
893
+ requant_input_loader = output_buf.make_loader()
894
+
895
+ def inner_fn_requant(index, scale, zero_point):
896
+ input = requant_input_loader(index)
897
+ inv_scale, zero_point = _create_constants(
898
+ 1.0 / scale, zero_point, dtype=torch.float32
899
+ )
900
+ val = ops.round(input * inv_scale) + zero_point
901
+ if output_dtype == torch.uint8:
902
+ qmin, qmax = _create_constants(
903
+ 0, 255, dtype=torch.float32
904
+ )
905
+ else:
906
+ qmin, qmax = _create_constants(
907
+ -128, 127, dtype=torch.float32
908
+ )
909
+ clamped = ops.minimum(ops.maximum(val, qmin), qmax)
910
+ return ops.to_dtype(clamped, output_dtype)
911
+
912
+ output_buf = ir.Pointwise(
913
+ device=output_buf.get_device_or_error(),
914
+ dtype=output_dtype,
915
+ inner_fn=functools.partial(
916
+ inner_fn_requant,
917
+ scale=float(o_scale),
918
+ zero_point=int(o_zero_point),
919
+ ),
920
+ ranges=output_buf.get_size(),
921
+ )
922
+
923
+ return output_buf
924
+
925
+ assert x.get_dtype() in [torch.uint8, torch.int8]
926
+ CppGemmTemplate.add_choices(
927
+ choices,
928
+ layout,
929
+ [x, x_scale, x_zp, packed_weight, w_scale, w_zp]
930
+ if bias is None
931
+ else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, bias],
932
+ has_bias=bias is not None,
933
+ epilogue_creator=epilogue_creator,
934
+ input_indices=[0, 3, 1, 2, 4, 5]
935
+ if bias is None
936
+ else [6, 0, 3, 1, 2, 4, 5],
937
+ )
938
+ if len(choices) == 0 or use_aten_gemm_kernels():
939
+ kwargs = dict(
940
+ output_scale=o_scale,
941
+ output_zero_point=o_zero_point,
942
+ output_dtype=output_dtype,
943
+ post_op_name=attr,
944
+ post_op_args=scalars,
945
+ post_op_algorithm=algorithm,
946
+ )
947
+ if bias is None:
948
+ kwargs["bias"] = None
949
+ choices.append(
950
+ aten_mkldnn_qlinear_unary.bind(
951
+ (x, x_scale, x_zp, packed_weight, w_scale, w_zp)
952
+ if bias is None
953
+ else (x, x_scale, x_zp, packed_weight, w_scale, w_zp, bias),
954
+ layout,
955
+ **kwargs,
956
+ )
957
+ )
958
+ assert packed_weight.get_name() in V.graph.constants
959
+ input_gen_fns = {
960
+ 3: lambda x: V.graph.constants[x.get_name()], # packed weight
961
+ 4: lambda x: V.graph.constants[x.get_name()], # weight scale
962
+ 5: lambda x: V.graph.constants[x.get_name()], # weight zp
963
+ 6: lambda x: V.graph.constants[x.get_name()], # bias
964
+ }
965
+ if isinstance(
966
+ ir.InputsKernel.unwrap_storage_for_input(x_scale),
967
+ ir.ConstantBuffer,
968
+ ):
969
+ # x is statically quantized
970
+ input_gen_fns[1] = lambda x: V.graph.constants[x.get_name()]
971
+ if isinstance(
972
+ ir.InputsKernel.unwrap_storage_for_input(x_zp),
973
+ ir.ConstantBuffer,
974
+ ):
975
+ input_gen_fns[2] = lambda x: V.graph.constants[x.get_name()]
976
+
977
+ result, _ = autotune_select_algorithm(
978
+ "qlinear_unary",
979
+ choices,
980
+ [x, x_scale, x_zp, packed_weight, w_scale, w_zp]
981
+ if bias is None
982
+ else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, bias],
983
+ layout,
984
+ input_gen_fns=input_gen_fns,
985
+ )
986
+ if len(x_size) > 2:
987
+ result = view(result, (*x_size[:-1], result.get_size()[-1]))
988
+ return result
989
+
990
+ @register_lowering(
991
+ torch.ops.onednn.qlinear_pointwise.binary, type_promotion_kind=None
992
+ )
993
+ @register_lowering(
994
+ torch.ops.onednn.qlinear_pointwise.binary_tensor, type_promotion_kind=None
995
+ )
996
+ def qlinear_binary(
997
+ x: TensorBox,
998
+ x_scale,
999
+ x_zp,
1000
+ packed_weight: TensorBox,
1001
+ w_scale: TensorBox,
1002
+ w_zp: TensorBox,
1003
+ x2: TensorBox,
1004
+ bias: TensorBox,
1005
+ o_scale,
1006
+ o_zero_point,
1007
+ output_dtype,
1008
+ x2_scale,
1009
+ x2_zp,
1010
+ binary_attr,
1011
+ alpha,
1012
+ unary_attr,
1013
+ unary_scalars,
1014
+ unary_algorithm,
1015
+ layout=None,
1016
+ ):
1017
+ x_size = x.get_size()
1018
+ x2_size = x2.get_size()
1019
+ assert len(x_size) == len(x2_size)
1020
+ if len(x_size) > 2 and binary_attr in ["add", "sum"]:
1021
+ # GEMM template needs 2D input, normalize input shape here
1022
+ x = view(x, [-1, x_size[-1]])
1023
+ x2 = view(x2, [-1, x2_size[-1]])
1024
+ if not isinstance(x_scale, ir.TensorBox):
1025
+ assert type(x_scale) is float
1026
+ x_scale = V.graph.add_tensor_constant(
1027
+ torch.tensor(x_scale, dtype=torch.float32), name="x_scale"
1028
+ )
1029
+ else:
1030
+ x_scale.realize()
1031
+ if all(dim == 1 for dim in x_scale.get_size()):
1032
+ # Corner-case discovered with LLaMA series.
1033
+ # If all outer dims of x_scale are 1, make it a 0D tensor.
1034
+ # Otherwise, epilogue creator will run into indexing issues.
1035
+ x_scale = view(x_scale, [])
1036
+ assert len(x_scale.get_size()) in [0, 1], "x_scale must be 0D or 1D"
1037
+
1038
+ if x_zp is None:
1039
+ x_zp = V.graph.add_tensor_constant(
1040
+ torch.tensor(0, dtype=torch.int32), name="x_zp"
1041
+ )
1042
+
1043
+ if w_zp is None:
1044
+ w_zp = V.graph.add_tensor_constant(
1045
+ torch.tensor(0, dtype=torch.int32), name="w_zp"
1046
+ )
1047
+
1048
+ if not isinstance(x_zp, ir.TensorBox):
1049
+ assert type(x_zp) is int
1050
+ x_zp = V.graph.add_tensor_constant(
1051
+ torch.tensor(x_zp, dtype=torch.int32), name="x_zp"
1052
+ )
1053
+ else:
1054
+ x_zp.realize()
1055
+
1056
+ # When channels less than 8, w_scale/w_zp is Pointwise instead of ConstantBuffer
1057
+ # Refer to
1058
+ # https://github.com/pytorch/pytorch/blob/f353d17755ed23b02924c962a86ff99a3405fe10/torch/_inductor/graph.py#L570-L577 # noqa: B950
1059
+ w_scale.realize()
1060
+ w_zp.realize()
1061
+ if w_zp.get_dtype() != torch.int32 and isinstance(
1062
+ ir.InputsKernel.unwrap_storage_for_input(w_zp),
1063
+ ir.ConstantBuffer,
1064
+ ):
1065
+ w_zp_tensor = V.graph.constants[w_zp.get_name()].to(torch.int32)
1066
+ w_zp = V.graph.add_tensor_constant( # type: ignore[assignment]
1067
+ torch.tensor(w_zp_tensor, dtype=torch.int32), name=w_zp.get_name()
1068
+ )
1069
+ if binary_attr == "sum":
1070
+ if output_dtype in [
1071
+ torch.float32,
1072
+ torch.bfloat16,
1073
+ ] and x2.get_dtype() in [torch.float32, torch.bfloat16]:
1074
+ if x2.get_dtype() != output_dtype:
1075
+ # For int8-mixed-bf16 quantization and inplace add,
1076
+ # there is case when accum dtype is float32 but output dtype is bfloat16.
1077
+ # Since the accum will be inplaced changed with post op sum,
1078
+ # we will do accum dtype conversion here.
1079
+ x2 = to_dtype(x2, output_dtype)
1080
+ else:
1081
+ assert x2.get_dtype() == output_dtype, (
1082
+ "dtype of accum for qlinear post op sum should be the same as output"
1083
+ )
1084
+ x2_dtype = x2.get_dtype()
1085
+ bias_dtype = bias.get_dtype() if bias is not None else None
1086
+ choices: list[ChoiceCaller] = []
1087
+ if (config.max_autotune or config.max_autotune_gemm) and binary_attr in [
1088
+ "add",
1089
+ "sum",
1090
+ ]:
1091
+ *_, layout, x, packed_weight, x2 = mm_args(
1092
+ x, packed_weight, x2, layout=layout, out_dtype=output_dtype
1093
+ )
1094
+ if (
1095
+ isinstance(
1096
+ ir.InputsKernel.unwrap_storage_for_input(x_zp),
1097
+ ir.ConstantBuffer,
1098
+ )
1099
+ and len(x_zp.get_layout().size) == 0 # Per tensor quant of act
1100
+ and isinstance(
1101
+ ir.InputsKernel.unwrap_storage_for_input(w_zp),
1102
+ ir.ConstantBuffer,
1103
+ )
1104
+ and torch.equal(
1105
+ torch.zeros_like(V.graph.constants[w_zp.get_name()]),
1106
+ V.graph.constants[w_zp.get_name()],
1107
+ ) # We only compensate MatrixB and assume B_zp is 0 to avoid the compensation of MatrixA
1108
+ and use_cpp_gemm_template(layout, x, packed_weight)
1109
+ ):
1110
+ W_tensor = V.graph.constants[packed_weight.get_name()]
1111
+ W_tensor = W_tensor.to_dense()
1112
+ (
1113
+ use_int8_fast_compensation_path,
1114
+ weight_compens,
1115
+ x_w_scale,
1116
+ ) = create_int8_compensation(
1117
+ W_tensor,
1118
+ packed_weight,
1119
+ x_scale,
1120
+ x_zp,
1121
+ w_scale,
1122
+ )
1123
+
1124
+ def epilogue_creator(input_buffer):
1125
+ # Epilogue to convert from s32 to f32 for u8s8f32
1126
+ assert output_dtype in [
1127
+ torch.float32,
1128
+ torch.bfloat16,
1129
+ torch.uint8,
1130
+ torch.int8,
1131
+ ]
1132
+
1133
+ input_loader = input_buffer.make_loader()
1134
+ x2_loader = x2.make_loader()
1135
+ weight_compens_loader = weight_compens.make_loader()
1136
+ x_w_scale_loader = None
1137
+ if use_int8_fast_compensation_path:
1138
+ assert x_w_scale is not None
1139
+ x_w_scale_loader = x_w_scale.make_loader()
1140
+ x_scale_loader = x_scale.make_loader()
1141
+ w_scale_loader = w_scale.make_loader()
1142
+ x_zp_loader = x_zp.make_loader()
1143
+ nonlocal bias
1144
+ bias_loader = None
1145
+ if bias is not None:
1146
+ bias_loader = bias.make_loader()
1147
+
1148
+ def inner_fn(index):
1149
+ nonlocal bias
1150
+ input = input_loader(index)
1151
+ _x2 = x2_loader(index)
1152
+ _x_scale = None
1153
+ _x_zp = None
1154
+ _w_scale = None
1155
+ weight_compens_index = (index[-1],)
1156
+ if not use_int8_fast_compensation_path:
1157
+ _x_scale = x_scale_loader(())
1158
+ _x_zp = x_zp_loader(())
1159
+ _w_scale = w_scale_loader(weight_compens_index)
1160
+ # MicroKernel Output is with int32: cvt to FP32 before doing compensation
1161
+ input = ops.to_dtype(input, torch.float32)
1162
+ _weight_compo = weight_compens_loader(weight_compens_index)
1163
+ _x_w_scale = None
1164
+ if use_int8_fast_compensation_path:
1165
+ assert x_w_scale_loader is not None
1166
+ _x_w_scale = x_w_scale_loader(weight_compens_index)
1167
+ # Step 1: Doing compensation to cvt fp32
1168
+ temp = codegen_int8_gemm_template_compensation(
1169
+ use_int8_fast_compensation_path,
1170
+ input,
1171
+ _weight_compo,
1172
+ _x_scale,
1173
+ _x_zp,
1174
+ _w_scale,
1175
+ _x_w_scale,
1176
+ )
1177
+ # Step 2: add Bias if applicable
1178
+ if bias is not None:
1179
+ # pyrefly: ignore [not-callable]
1180
+ _bias = bias_loader(weight_compens_index)
1181
+ nonlocal bias_dtype
1182
+ assert bias_dtype in [torch.float32, torch.bfloat16]
1183
+ if bias_dtype == torch.bfloat16:
1184
+ _bias = ops.to_dtype(_bias, torch.float32)
1185
+ temp = ops.add(temp, _bias)
1186
+
1187
+ # Step 3: Binary add
1188
+ nonlocal x2_dtype
1189
+ assert x2_dtype in [torch.float32, torch.bfloat16]
1190
+ if x2_dtype == torch.bfloat16:
1191
+ _x2 = ops.to_dtype(_x2, torch.float32)
1192
+ temp = ops.add(temp, _x2)
1193
+
1194
+ return temp
1195
+
1196
+ output_buf = ir.Pointwise(
1197
+ device=input_buffer.get_device(),
1198
+ dtype=torch.float32, # Hardcode to FP32 for u8s8f32
1199
+ inner_fn=inner_fn,
1200
+ ranges=input_buffer.get_size(),
1201
+ )
1202
+
1203
+ # Step 4: Unary post op if has
1204
+ if unary_attr != "none":
1205
+ output_buf = create_epilogue_with_attr(
1206
+ output_buf,
1207
+ unary_attr,
1208
+ scalars=unary_scalars,
1209
+ algorithm=unary_algorithm,
1210
+ )
1211
+
1212
+ # Step 5: Cast output to Target Dtype
1213
+ if output_dtype == torch.bfloat16:
1214
+ output_cast_loader = output_buf.make_loader()
1215
+
1216
+ def inner_fn_cast_output_to_bf16(index):
1217
+ input = output_cast_loader(index)
1218
+ return ops.to_dtype(input, output_dtype)
1219
+
1220
+ output_buf = ir.Pointwise(
1221
+ device=output_buf.get_device_or_error(),
1222
+ dtype=output_dtype,
1223
+ inner_fn=inner_fn_cast_output_to_bf16,
1224
+ ranges=output_buf.get_size(),
1225
+ )
1226
+ elif output_dtype in [torch.uint8, torch.int8]:
1227
+ from .lowering import _create_constants
1228
+
1229
+ requant_input_loader = output_buf.make_loader()
1230
+
1231
+ def inner_fn_requant(index, scale, zero_point):
1232
+ input = requant_input_loader(index)
1233
+ inv_scale, zero_point = _create_constants(
1234
+ 1.0 / scale, zero_point, dtype=torch.float32
1235
+ )
1236
+ val = ops.round(input * inv_scale) + zero_point
1237
+ if output_dtype == torch.uint8:
1238
+ qmin, qmax = _create_constants(
1239
+ 0, 255, dtype=torch.float32
1240
+ )
1241
+ else:
1242
+ qmin, qmax = _create_constants(
1243
+ -128, 127, dtype=torch.float32
1244
+ )
1245
+ clamped = ops.minimum(ops.maximum(val, qmin), qmax)
1246
+ return ops.to_dtype(clamped, torch.uint8)
1247
+
1248
+ output_buf = ir.Pointwise(
1249
+ device=output_buf.get_device_or_error(),
1250
+ dtype=torch.uint8,
1251
+ inner_fn=functools.partial(
1252
+ inner_fn_requant,
1253
+ scale=float(o_scale),
1254
+ zero_point=int(o_zero_point),
1255
+ ),
1256
+ ranges=output_buf.get_size(),
1257
+ )
1258
+
1259
+ return output_buf
1260
+
1261
+ CppGemmTemplate.add_choices(
1262
+ choices,
1263
+ layout,
1264
+ [x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2]
1265
+ if bias is None
1266
+ else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2, bias],
1267
+ has_bias=bias is not None,
1268
+ epilogue_creator=epilogue_creator,
1269
+ # Reorder bias and x2
1270
+ input_indices=[0, 3, 1, 2, 4, 5, 6]
1271
+ if bias is None
1272
+ else [7, 0, 3, 1, 2, 4, 5, 6],
1273
+ )
1274
+
1275
+ if len(choices) == 0 or use_aten_gemm_kernels():
1276
+ kwargs = dict(
1277
+ output_scale=o_scale,
1278
+ output_zero_point=o_zero_point,
1279
+ output_dtype=output_dtype,
1280
+ other_scale=x2_scale,
1281
+ other_zp=x2_zp,
1282
+ binary_post_op=binary_attr,
1283
+ binary_alpha=alpha,
1284
+ unary_post_op=unary_attr,
1285
+ unary_post_op_args=unary_scalars,
1286
+ unary_post_op_algorithm=unary_algorithm,
1287
+ )
1288
+ if bias is None:
1289
+ kwargs["bias"] = None
1290
+ choices.append(
1291
+ aten_mkldnn_qlinear_binary.bind(
1292
+ (x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2)
1293
+ if bias is None
1294
+ else (x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2, bias),
1295
+ layout,
1296
+ **kwargs,
1297
+ )
1298
+ )
1299
+ assert packed_weight.get_name() in V.graph.constants
1300
+ input_gen_fns = {
1301
+ 3: lambda x: V.graph.constants[x.get_name()],
1302
+ 4: lambda x: V.graph.constants[x.get_name()],
1303
+ 5: lambda x: V.graph.constants[x.get_name()],
1304
+ }
1305
+ if bias is not None:
1306
+ input_gen_fns[7] = lambda x: V.graph.constants[x.get_name()] # For bias
1307
+ result, _ = autotune_select_algorithm(
1308
+ "qlinear_binary",
1309
+ choices,
1310
+ [x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2]
1311
+ if bias is None
1312
+ else [x, x_scale, x_zp, packed_weight, w_scale, w_zp, x2, bias],
1313
+ layout,
1314
+ input_gen_fns=input_gen_fns,
1315
+ )
1316
+ if (
1317
+ isinstance(result.data.data, ir.CppTemplateBuffer)
1318
+ and binary_attr == "sum"
1319
+ and result.data.data.layout == x2.get_layout()
1320
+ ):
1321
+ # In this case, since x2 is inplace updated when binary_attr is "sum"
1322
+ # we update the layout of result to view of x2
1323
+ result = ir.TensorBox.create(
1324
+ ir.CppTemplateBuffer(
1325
+ layout=ir.NonOwningLayout(
1326
+ ir.ReinterpretView(data=x2, layout=x2.get_layout())
1327
+ ),
1328
+ inputs=result.data.data.inputs, # type: ignore[arg-type]
1329
+ make_kernel_render=result.data.data.make_kernel_render, # type: ignore[arg-type]
1330
+ template=result.data.data.template,
1331
+ choice=result.data.data.choice,
1332
+ )
1333
+ )
1334
+ if len(x_size) > 2 and binary_attr in ["add", "sum"]:
1335
+ result = view(result, (*x_size[:-1], result.get_size()[-1])) # type: ignore[arg-type]
1336
+ return result
1337
+
1338
+ if torch._C.has_mkl:
1339
+ aten_mkl_linear = ExternKernelChoice(
1340
+ torch.ops.mkl._mkl_linear,
1341
+ "mkl::_mkl_linear",
1342
+ has_out_variant=False,
1343
+ kernel_creator=mkldnn_ir.MKLPackedLinear.create,
1344
+ )
1345
+ cpu_needs_realized_inputs.append(torch.ops.mkl._mkl_linear)
1346
+
1347
+ @register_lowering(torch.ops.mkl._mkl_linear)
1348
+ def mkl_packed_linear(
1349
+ x: TensorBox,
1350
+ packed_w: TensorBox,
1351
+ orig_w: TensorBox,
1352
+ b: TensorBox | None,
1353
+ batch_size,
1354
+ *,
1355
+ layout=None,
1356
+ ):
1357
+ choices: list[ChoiceCaller] = []
1358
+ if config.max_autotune or config.max_autotune_gemm:
1359
+ transposed_w = permute(orig_w, [1, 0])
1360
+ *_, layout, x, transposed_w = mm_args(
1361
+ x, transposed_w, layout=layout
1362
+ )
1363
+ if use_cpp_gemm_template(layout, x, transposed_w):
1364
+ CppGemmTemplate.add_choices(
1365
+ choices,
1366
+ layout,
1367
+ [x, packed_w, orig_w],
1368
+ trans_w=True,
1369
+ input_indices=[0, 2],
1370
+ )
1371
+
1372
+ if len(choices) == 0 or use_aten_gemm_kernels():
1373
+ choices.append(
1374
+ aten_mkl_linear.bind(
1375
+ (x, packed_w, orig_w), layout, B=None, batch_size=batch_size
1376
+ )
1377
+ )
1378
+
1379
+ assert packed_w.get_name() in V.graph.constants
1380
+ assert orig_w.get_name() in V.graph.constants
1381
+ # packed_w is a mkldnn tensor which we can't generate directly
1382
+ # so we use the weights from the original tensor in autotune.
1383
+ input_gen_fns = {
1384
+ 1: lambda x: V.graph.constants[x.get_name()],
1385
+ 2: lambda x: V.graph.constants[x.get_name()],
1386
+ }
1387
+ result: TensorBox # annotation on separate line since tuple unpacking doesn't support inline annotation
1388
+ result, _ = autotune_select_algorithm(
1389
+ "packed_linear",
1390
+ choices,
1391
+ [x, packed_w, orig_w],
1392
+ layout,
1393
+ input_gen_fns=input_gen_fns,
1394
+ )
1395
+ if b is not None:
1396
+ result = add(result, b)
1397
+ return result
1398
+
1399
+ add_needs_realized_inputs(cpu_needs_realized_inputs)
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/mock_cache.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: ignore-errors
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import dataclasses
7
+ import sys
8
+ import threading
9
+ from typing import Any, TYPE_CHECKING
10
+ from typing_extensions import override, Self
11
+ from unittest.mock import patch
12
+
13
+ from torch._inductor import config
14
+ from torch._inductor.remote_cache import RemoteCacheBackend
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+ from types import TracebackType
20
+
21
+
22
+ @dataclasses.dataclass
23
+ class Stats:
24
+ num_put: int = 0
25
+ num_get_hit: int = 0
26
+ num_get_miss: int = 0
27
+
28
+ def __iadd__(self, other: Stats) -> Self:
29
+ self.num_put += other.num_put
30
+ self.num_get_hit += other.num_get_hit
31
+ self.num_get_miss += other.num_get_miss
32
+ return self
33
+
34
+ def reset(self) -> None:
35
+ self.num_put = 0
36
+ self.num_get_hit = 0
37
+ self.num_get_miss = 0
38
+
39
+ def __str__(self) -> str:
40
+ return "".join(
41
+ (
42
+ f"puts: {self.num_put}, ",
43
+ f"misses: {self.num_get_miss}, ",
44
+ f"hits: {self.num_get_hit}, ",
45
+ )
46
+ )
47
+
48
+ def __eq__(self, other: object) -> bool:
49
+ # Dataclass's default __eq__ checks that the types are the same so can't
50
+ # be used with _GlobalItemStats.
51
+ return (
52
+ isinstance(other, (Stats, _GlobalItemStats))
53
+ and self.num_put == other.num_put
54
+ and self.num_get_hit == other.num_get_hit
55
+ and self.num_get_miss == other.num_get_miss
56
+ )
57
+
58
+
59
+ class _GlobalItemStats(Stats):
60
+ cache: dict[str, object]
61
+
62
+ def __init__(self) -> None:
63
+ super().__init__()
64
+ self.cache = {}
65
+
66
+ def reset(self) -> None:
67
+ super().reset()
68
+ self.cache = {}
69
+
70
+
71
+ # The cache states are thread-local so if we're running multiple tests at once
72
+ # they won't cross contaminate. However - it needs to be "global" because we
73
+ # allow code to create new cache clients which refer to the same cache (because
74
+ # it's a remote cache).
75
+
76
+
77
+ class _GlobalStats(threading.local):
78
+ def __init__(self) -> None:
79
+ self.autotune_local = _GlobalItemStats()
80
+ self.autotune_remote = _GlobalItemStats()
81
+ self.bundled_autotune = _GlobalItemStats()
82
+ self.fx_graph = _GlobalItemStats()
83
+ self.triton = _GlobalItemStats()
84
+ self.aot_autograd = _GlobalItemStats()
85
+ self.dynamo_pgo = _GlobalItemStats()
86
+
87
+ def reset(self) -> None:
88
+ self.autotune_local.reset()
89
+ self.autotune_remote.reset()
90
+ self.bundled_autotune.reset()
91
+ self.fx_graph.reset()
92
+ self.triton.reset()
93
+ self.aot_autograd.reset()
94
+ self.dynamo_pgo.reset()
95
+
96
+ def get_stat(self, name: str) -> _GlobalItemStats:
97
+ return getattr(self, name)
98
+
99
+ def report(self):
100
+ subs = (
101
+ ("autotune_local", self.autotune_local),
102
+ ("autotune_remote", self.autotune_remote),
103
+ ("bundled_autotune", self.bundled_autotune),
104
+ ("fx_graph", self.fx_graph),
105
+ ("triton", self.triton),
106
+ ("aot_autograd", self.aot_autograd),
107
+ ("dynamo_pgo", self.dynamo_pgo),
108
+ )
109
+
110
+ print("Cache Stats:", file=sys.stderr)
111
+ for name, sub in subs:
112
+ print(f" {name}: {sub}", file=sys.stderr)
113
+
114
+ print("Cache Entries:", file=sys.stderr)
115
+ for name, sub in subs:
116
+ if sub.cache:
117
+ print(f" {name}:", file=sys.stderr)
118
+ for k, v in sorted(sub.cache.items()):
119
+ v = repr(v)
120
+ if len(v) > 100:
121
+ v = v[:100] + "..."
122
+ print(f" {k!r}: {v}", file=sys.stderr)
123
+
124
+
125
+ global_stats = _GlobalStats()
126
+
127
+
128
+ class MockBackend(RemoteCacheBackend[Any]):
129
+ def __init__(self, name: str) -> None:
130
+ self._name = name
131
+
132
+ @staticmethod
133
+ def with_name(name: str) -> Callable[[], MockBackend]:
134
+ def wrapper() -> MockBackend:
135
+ return MockBackend(name)
136
+
137
+ return wrapper
138
+
139
+ @override
140
+ def _get(self, key: str) -> Any | None:
141
+ stat = global_stats.get_stat(self._name)
142
+ if key in stat.cache:
143
+ stat += Stats(num_get_hit=1)
144
+ return stat.cache.get(key)
145
+ else:
146
+ stat += Stats(num_get_miss=1)
147
+ return None
148
+
149
+ @override
150
+ def _put(self, key: str, data: Any) -> None:
151
+ stat = global_stats.get_stat(self._name)
152
+ stat += Stats(num_put=1)
153
+ stat.cache[key] = data
154
+
155
+
156
+ # List of configs for each cache
157
+ _CACHE_CONFIG_EN = (
158
+ "fx_graph_cache",
159
+ "fx_graph_remote_cache",
160
+ "autotune_local_cache",
161
+ "autotune_remote_cache",
162
+ "bundled_autotune_remote_cache",
163
+ )
164
+
165
+
166
+ class PatchCaches(contextlib.AbstractContextManager):
167
+ @classmethod
168
+ def setUp(cls):
169
+ # If this test is using PatchCaches then disable all the caches by
170
+ # default, letting the tests turn them on explicitly. This is because
171
+ # tests using PatchCaches will often want to check stats explicitly.
172
+ cls._savedCacheState = {}
173
+ for name in _CACHE_CONFIG_EN:
174
+ if hasattr(config, name):
175
+ cls._savedCacheState[name] = getattr(config, name)
176
+ setattr(config, name, False)
177
+
178
+ @classmethod
179
+ def tearDown(cls):
180
+ # Restore cache defaults
181
+ for name in _CACHE_CONFIG_EN:
182
+ delattr(config, name)
183
+ if name in cls._savedCacheState:
184
+ setattr(config, name, cls._savedCacheState[name])
185
+
186
+ def __init__(self) -> None:
187
+ self._stack = contextlib.ExitStack()
188
+
189
+ def __enter__(self) -> Self:
190
+ global_stats.reset()
191
+ self._stack.__enter__()
192
+
193
+ ctx = patch(
194
+ "torch._inductor.runtime.autotune_cache.LocalAutotuneCache.backend_override_cls",
195
+ MockBackend.with_name("autotune_local"),
196
+ )
197
+ self._stack.enter_context(ctx)
198
+
199
+ ctx = patch(
200
+ "torch._inductor.remote_cache.RemoteAutotuneCache.backend_override_cls",
201
+ MockBackend.with_name("autotune_remote"),
202
+ )
203
+ self._stack.enter_context(ctx)
204
+
205
+ ctx = patch(
206
+ "torch._inductor.remote_cache.RemoteBundledAutotuneCache.backend_override_cls",
207
+ MockBackend.with_name("bundled_autotune"),
208
+ )
209
+ self._stack.enter_context(ctx)
210
+
211
+ ctx = patch(
212
+ "torch._inductor.remote_cache.RemoteFxGraphCache.backend_override_cls",
213
+ MockBackend.with_name("fx_graph"),
214
+ )
215
+ self._stack.enter_context(ctx)
216
+
217
+ ctx = patch(
218
+ "torch._inductor.remote_cache.RemoteAOTAutogradCache.backend_override_cls",
219
+ MockBackend.with_name("aot_autograd"),
220
+ )
221
+ self._stack.enter_context(ctx)
222
+
223
+ ctx = patch(
224
+ "torch._inductor.remote_cache.RemoteDynamoPGOCache.backend_override_cls",
225
+ MockBackend.with_name("dynamo_pgo"),
226
+ )
227
+ self._stack.enter_context(ctx)
228
+
229
+ if config.is_fbcode():
230
+ ctx = patch(
231
+ "torch._inductor.fb.remote_cache.FbRemoteAutotuneCache.backend_override_cls",
232
+ MockBackend.with_name("autotune_remote"),
233
+ )
234
+ self._stack.enter_context(ctx)
235
+
236
+ ctx = patch(
237
+ "torch._inductor.fb.remote_cache.FbRemoteBundledAutotuneCache.backend_override_cls",
238
+ MockBackend.with_name("bundled_autotune"),
239
+ )
240
+ self._stack.enter_context(ctx)
241
+
242
+ ctx = patch(
243
+ "torch._inductor.fb.remote_cache.FbRemoteFxGraphCache.backend_override_cls",
244
+ MockBackend.with_name("fx_graph"),
245
+ )
246
+ self._stack.enter_context(ctx)
247
+
248
+ ctx = patch(
249
+ "triton.fb.fb_memcache.FbMemcacheRemoteKernelCache.backend_override_cls",
250
+ MockBackend.with_name("triton"),
251
+ )
252
+ self._stack.enter_context(ctx)
253
+
254
+ ctx = patch(
255
+ "torch._inductor.fb.remote_cache.FbRemoteAOTAutogradCache.backend_override_cls",
256
+ MockBackend.with_name("aot_autograd"),
257
+ )
258
+ self._stack.enter_context(ctx)
259
+
260
+ ctx = patch(
261
+ "torch._inductor.fb.remote_cache.FbRemoteDynamoPGOCache.backend_override_cls",
262
+ MockBackend.with_name("dynamo_pgo"),
263
+ )
264
+ self._stack.enter_context(ctx)
265
+
266
+ return self
267
+
268
+ def __exit__(
269
+ self,
270
+ exc_type: type[BaseException] | None,
271
+ exc_value: BaseException | None,
272
+ traceback: TracebackType | None,
273
+ ) -> None:
274
+ self._stack.__exit__(exc_type, exc_value, traceback)
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/ops_handler.py ADDED
@@ -0,0 +1,1207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import itertools
6
+ import re
7
+ import warnings
8
+ from io import StringIO
9
+ from typing import Any, Generic, Literal, NamedTuple, TYPE_CHECKING, TypeVar
10
+ from unittest.mock import patch
11
+
12
+ import sympy
13
+
14
+ import torch
15
+ import torch.utils._pytree as pytree
16
+
17
+ from ..utils._ordered_set import OrderedSet
18
+ from .utils import IndentedBuffer, reduction_num_outputs, sympy_index_symbol, sympy_str
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
24
+
25
+ T = TypeVar("T")
26
+ AtomicMode = Literal[
27
+ "atomic_add",
28
+ "atomic_max",
29
+ "atomic_min",
30
+ "atomic_and",
31
+ "atomic_or",
32
+ "atomic_xor",
33
+ "atomic_cas",
34
+ "atomic_xchg",
35
+ ]
36
+ StoreMode = AtomicMode | Literal["tma"] | None
37
+ ReductionType = Literal[
38
+ "argmax",
39
+ "argmin",
40
+ "welford_reduce",
41
+ "welford_combine",
42
+ "any",
43
+ "max",
44
+ "min",
45
+ "prod",
46
+ "sum",
47
+ "dot",
48
+ "xor_sum",
49
+ "online_softmax_reduce",
50
+ ]
51
+
52
+
53
+ def _arg_str(a: object) -> str:
54
+ if isinstance(a, sympy.Expr):
55
+ return sympy_str(a)
56
+ return str(a)
57
+
58
+
59
+ # See OpDecompositions for superclass that desugars operations like reciprocal/square.
60
+ class OpsHandler(Generic[T]):
61
+ """
62
+ Protocol describing the set of valid operations on ``torch._inductor.virtualized.ops``,
63
+ as well as the contract for op handlers. The type T signifies the domain
64
+ of the abstract analysis AKA what all the functions return / take as arguments
65
+ anywhere compute occurs.
66
+
67
+ While these operators are typically dtype polymorphic (e.g., you can use mul
68
+ on both integers and floats), they do NOT do promotion and usually return the
69
+ same dtype as the input. You are expected to have handled type promotion
70
+ during ATen decompositions. Most operators correspond exactly to pointwise
71
+ operations as defined by torch, so when in doubt about semantics, check the
72
+ corresponding torch documentation. These are all scalar operations (so they
73
+ are defined to operate on a single element at a time.)
74
+
75
+ For convenience, many operators take a src_dtype which indicates what the dtype
76
+ of the input argument is. Although in principle this can be derived by an
77
+ analysis, providing this for ops where it is useful helps avoid having to repeatedly
78
+ recompute dtype in code generation.
79
+
80
+ Note that this often describes a class of static methods, for stateless
81
+ ops handlers.
82
+
83
+ Handlers are often defined using metaprogramming (e.g. _initialize_pointwise_overrides),
84
+ which means you will not get type errors for those methods. We have tests in
85
+ test/inductor/test_op_completeness.py which check that all operators are implemented after
86
+ all the metaprogramming has run.
87
+ """
88
+
89
+ def constant(self, value: bool | float | int, dtype: torch.dtype) -> T:
90
+ """Produces a scalar constant of type dtype."""
91
+ raise NotImplementedError
92
+
93
+ def load_seed(self, name: str, offset: T) -> T:
94
+ """Computes inductor_prims.lookup_seed."""
95
+ raise NotImplementedError
96
+
97
+ def rand(self, seed: T, offset: T) -> T:
98
+ """Computes inductor_prims.random with mode="rand". offset has dtype int32."""
99
+ raise NotImplementedError
100
+
101
+ def rand_eager(
102
+ self, seed: T, base_offset: T, threads_per_round: T, tid: T, vec: T
103
+ ) -> T:
104
+ """Computes inductor_prims.random with mode="rand_eager". offset has dtype int32."""
105
+ raise NotImplementedError
106
+
107
+ def randn(self, seed: T, offset: T) -> T:
108
+ """Computes inductor_prims.random with mode="randn". offset has dtype int32."""
109
+ raise NotImplementedError
110
+
111
+ def randint64(self, seed: T, offset: T, low: T, high: T) -> T:
112
+ """Computes inductor_prims.randint. offset has dtype int32."""
113
+ raise NotImplementedError
114
+
115
+ def masked(self, mask: T, body: Callable[[], T], other: T) -> T:
116
+ """
117
+ Computes body, but only perform loads/stores if the boolean mask
118
+ evaluates to true. For example, you would use this if you needed to
119
+ perform an indirect load that may not be valid on some elements;
120
+ without masking, invalid accesses can cause IMAs. When mask is true,
121
+ the result is the result of body; otherwise it is other. Here, `other`
122
+ needs to be a constant.
123
+
124
+ Contrast this with ops.where, which can multiplex between two values
125
+ that have been unconditionally computed.
126
+ """
127
+ raise NotImplementedError
128
+
129
+ def where(self, condition: T, input: T, other: T) -> T:
130
+ """
131
+ Computes torch.where: when condition is true, return input; otherwise return other.
132
+ """
133
+ raise NotImplementedError
134
+
135
+ def index_expr(self, expr: sympy.Expr, dtype: torch.dtype) -> T:
136
+ """
137
+ Converts a sympy expression into a scalar of type dtype. expr is typically
138
+ an indexing expression, thus the name; however, it can also be used in
139
+ non-indexing situations.
140
+ """
141
+ raise NotImplementedError
142
+
143
+ def to_dtype(
144
+ self,
145
+ x: T,
146
+ dtype: torch.dtype,
147
+ src_dtype: torch.dtype | None = None,
148
+ use_compute_types: bool = True,
149
+ ) -> T:
150
+ """
151
+ Convert x to dtype. src_dtype can be optionally set to specify what the original
152
+ dtype of x was, which can improve code generation (used by torch to(dtype=dtype)).
153
+ """
154
+ raise NotImplementedError
155
+
156
+ def trunc_to_int(self, x: T, dtype: torch.dtype) -> T:
157
+ """
158
+ Convert x to dtype with truncation semantics (similar to how the int
159
+ constructor works in Python). In Inductor codegen, this just decays
160
+ to trunc and then to_dtype, but this composite operation helps
161
+ roundtrips for Sympy evaluation.
162
+
163
+ dtype is taken as an explicit parameter because the desired output
164
+ dtype is typically the index dtype, which may vary between int32 and
165
+ int64 depending on if we've shown that all the indexing operations can
166
+ be done in int32.
167
+ """
168
+ raise NotImplementedError
169
+
170
+ def ceil_to_int(self, x: T, dtype: torch.dtype) -> T:
171
+ """
172
+ Convert x to dtype with ceiling semantics. See also trunc_to_int.
173
+ """
174
+ raise NotImplementedError
175
+
176
+ def floor_to_int(self, x: T, dtype: torch.dtype) -> T:
177
+ """
178
+ Convert x to dtype with ceiling semantics. See also trunc_to_int.
179
+ """
180
+ raise NotImplementedError
181
+
182
+ def round_to_int(self, x: T, dtype: torch.dtype) -> T:
183
+ """
184
+ Convert x to dtype with round-to-even semantics. See also trunc_to_int.
185
+ """
186
+ raise NotImplementedError
187
+
188
+ def to_dtype_bitcast(self, x: T, dtype: torch.dtype, src_dtype: torch.dtype) -> T:
189
+ """
190
+ Reinterpret cast x to dtype (reinterpreting the bits in memory as another dtype.)
191
+ src_dtype must be the original type of x.
192
+ """
193
+ raise NotImplementedError
194
+
195
+ def identity(self, x: T) -> T:
196
+ """
197
+ Returns x as is. This is used to trigger CSE.
198
+ """
199
+ raise NotImplementedError
200
+
201
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
202
+ # These operations are only available in a "kernel" context. Check
203
+ # torch._inductor.codegen.common.CSEProxy for their typical implementation
204
+ # in op handler (routing to their respective implementations in the kernel
205
+ # handler)
206
+ #
207
+ # Importantly, inside a kernel, indexing and mask variables are available
208
+ # in scope, which are typically used by sympy.Expr indexing.
209
+
210
+ def indirect_indexing(
211
+ self, x: T, size: sympy.Expr, check: bool = True, wrap_neg=True
212
+ ) -> sympy.Expr:
213
+ """
214
+ Convert an integral x into a sympy.Expr that can be subsequently used in
215
+ indexing computation. 'size' represents an upper bound on what valid
216
+ indexes can be; when 'check' is True, we check that the x is in bounds.
217
+
218
+ NB: This is typically mandatory to implement for any analysis, because you
219
+ MUST return a valid sympy.Expr of some sort (even if it's a meaningless symbol).
220
+ """
221
+ raise NotImplementedError
222
+
223
+ def load(self, name: str, index: sympy.Expr) -> T:
224
+ """
225
+ Load from the memory location 'name', offset by some indexing expression 'index'.
226
+ """
227
+ raise NotImplementedError
228
+
229
+ def store(
230
+ self,
231
+ name: str,
232
+ index: sympy.Expr,
233
+ value: T,
234
+ mode: StoreMode = None,
235
+ ) -> None:
236
+ """
237
+ Store 'value' to the memory location 'name' offset by 'expr'. If
238
+ specified, 'mode' can require the store to be an atomic addition.
239
+ """
240
+ raise NotImplementedError
241
+
242
+ # TODO: Better explain how the "collective" semantics of these ops;
243
+ # remember that the input value is a scalar, you can't reduce on it in the
244
+ # traditional sense!
245
+ def reduction(
246
+ self,
247
+ dtype: torch.dtype,
248
+ src_dtype: torch.dtype,
249
+ reduction_type: ReductionType,
250
+ value: T,
251
+ ) -> T | tuple[T, ...]:
252
+ """
253
+ Perform a 'reduction_type' reduction on 'value' of dtype 'src_dtype',
254
+ using 'dtype' as the accumulation dtype for the reduction. The result
255
+ is an intermediate computation which should be stored to the final
256
+ location using 'ops.store_reduction'.
257
+
258
+ Valid reduction types are . For Welford reduction types, this
259
+ function returns multiple outputs; consult reduction_num_outputs to
260
+ determine the amount in metaprogramming applications.
261
+ """
262
+ raise NotImplementedError
263
+
264
+ # TODO: in practice, this seems to actually return None, but not returning
265
+ # a T makes common __getattr__ idioms not type correctly. Figure out if
266
+ # this should be returning something.
267
+ def store_reduction(self, name: str, index: sympy.Expr, value: T) -> None:
268
+ """
269
+ Store the fully accumulated result of 'reduction' to the memory
270
+ location 'name' offset by 'expr'.
271
+ """
272
+ raise NotImplementedError
273
+
274
+ def scan(
275
+ self,
276
+ dtypes: tuple[torch.dtype, ...],
277
+ combine_fn: Callable[[tuple[T, ...], tuple[T, ...]], tuple[T, ...]],
278
+ values: tuple[T, ...],
279
+ ) -> tuple[T, ...]:
280
+ """
281
+ Perform an associative scan on 'value'.
282
+ """
283
+ # TODO: Improve the description with some pseudocode
284
+ raise NotImplementedError
285
+
286
+ def sort(
287
+ self,
288
+ dtypes: tuple[torch.dtype, ...],
289
+ values: tuple[T, ...],
290
+ stable: bool,
291
+ descending: bool,
292
+ ) -> tuple[T, ...]:
293
+ """
294
+ Sort values along the reduction dimension.
295
+ """
296
+ raise NotImplementedError
297
+
298
+ def bucketize(
299
+ self,
300
+ values: T,
301
+ boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr],
302
+ boundary_indices: T,
303
+ indexing_dtype: torch.dtype,
304
+ right: bool,
305
+ sorter: tuple[str, sympy.Expr] | None = None,
306
+ sorter_indices: T | None = None,
307
+ ) -> T:
308
+ # See [Note: Inductor bucketize op]
309
+ raise NotImplementedError
310
+
311
+ def partial_accumulate(
312
+ self,
313
+ name: str,
314
+ reduction_type: ReductionType,
315
+ value: T,
316
+ extra_meta: dict[str, Any],
317
+ ) -> None:
318
+ raise NotImplementedError
319
+
320
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
321
+ # The following ops have semantics that correspond exactly to the torch
322
+ # operation with the same corresponding name.
323
+
324
+ def abs(self, x0: T) -> T:
325
+ raise NotImplementedError
326
+
327
+ def exp(self, x0: T) -> T:
328
+ raise NotImplementedError
329
+
330
+ def exp2(self, x0: T) -> T:
331
+ raise NotImplementedError
332
+
333
+ def expm1(self, x0: T) -> T:
334
+ raise NotImplementedError
335
+
336
+ def sqrt(self, x0: T) -> T:
337
+ raise NotImplementedError
338
+
339
+ def relu(self, x0: T) -> T:
340
+ raise NotImplementedError
341
+
342
+ def minimum(self, x0: T, x1: T) -> T:
343
+ raise NotImplementedError
344
+
345
+ def maximum(self, x0: T, x1: T) -> T:
346
+ raise NotImplementedError
347
+
348
+ def cos(self, x0: T) -> T:
349
+ raise NotImplementedError
350
+
351
+ def sin(self, x0: T) -> T:
352
+ raise NotImplementedError
353
+
354
+ def lgamma(self, x0: T) -> T:
355
+ raise NotImplementedError
356
+
357
+ def erf(self, x0: T) -> T:
358
+ raise NotImplementedError
359
+
360
+ def cosh(self, x0: T) -> T:
361
+ raise NotImplementedError
362
+
363
+ def sinh(self, x0: T) -> T:
364
+ raise NotImplementedError
365
+
366
+ def acos(self, x0: T) -> T:
367
+ raise NotImplementedError
368
+
369
+ def acosh(self, x0: T) -> T:
370
+ raise NotImplementedError
371
+
372
+ def asin(self, x0: T) -> T:
373
+ raise NotImplementedError
374
+
375
+ def asinh(self, x0: T) -> T:
376
+ raise NotImplementedError
377
+
378
+ def atan2(self, x0: T, x1: T) -> T:
379
+ raise NotImplementedError
380
+
381
+ def atan(self, x0: T) -> T:
382
+ raise NotImplementedError
383
+
384
+ def atanh(self, x0: T) -> T:
385
+ raise NotImplementedError
386
+
387
+ def copysign(self, x0: T, x1: T) -> T:
388
+ raise NotImplementedError
389
+
390
+ def erfc(self, x0: T) -> T:
391
+ raise NotImplementedError
392
+
393
+ def erfinv(self, x0: T) -> T:
394
+ raise NotImplementedError
395
+
396
+ def frexp(self, x0: T):
397
+ raise NotImplementedError
398
+
399
+ def hypot(self, x0: T, x1: T) -> T:
400
+ raise NotImplementedError
401
+
402
+ def log10(self, x0: T) -> T:
403
+ raise NotImplementedError
404
+
405
+ def log2(self, x0: T) -> T:
406
+ raise NotImplementedError
407
+
408
+ def ldexp(self, x0: T, n: T) -> T:
409
+ raise NotImplementedError
410
+
411
+ def nextafter(self, x0: T, x1: T) -> T:
412
+ raise NotImplementedError
413
+
414
+ def logical_and(self, x0: T, x1: T) -> T:
415
+ raise NotImplementedError
416
+
417
+ def logical_not(self, x0: T) -> T:
418
+ raise NotImplementedError
419
+
420
+ def logical_or(self, x0: T, x1: T) -> T:
421
+ raise NotImplementedError
422
+
423
+ def logical_xor(self, x0: T, x1: T) -> T:
424
+ raise NotImplementedError
425
+
426
+ def bitwise_and(self, x0: T, x1: T) -> T:
427
+ raise NotImplementedError
428
+
429
+ def bitwise_not(self, x0: T) -> T:
430
+ raise NotImplementedError
431
+
432
+ def bitwise_or(self, x0: T, x1: T) -> T:
433
+ raise NotImplementedError
434
+
435
+ def bitwise_xor(self, x0: T, x1: T) -> T:
436
+ raise NotImplementedError
437
+
438
+ def bitwise_left_shift(self, x0: T, x1: T) -> T:
439
+ raise NotImplementedError
440
+
441
+ def bitwise_right_shift(self, x0: T, x1: T) -> T:
442
+ raise NotImplementedError
443
+
444
+ def rsqrt(self, x0: T) -> T:
445
+ raise NotImplementedError
446
+
447
+ def log1p(self, x0: T) -> T:
448
+ raise NotImplementedError
449
+
450
+ def tan(self, x0: T) -> T:
451
+ raise NotImplementedError
452
+
453
+ def tanh(self, x0: T) -> T:
454
+ raise NotImplementedError
455
+
456
+ def sigmoid(self, x0: T) -> T:
457
+ raise NotImplementedError
458
+
459
+ def signbit(self, x0: T) -> T:
460
+ raise NotImplementedError
461
+
462
+ def fmod(self, x0: T, x1: T) -> T:
463
+ raise NotImplementedError
464
+
465
+ def log(self, x0: T) -> T:
466
+ raise NotImplementedError
467
+
468
+ def isinf(self, x0: T) -> T:
469
+ raise NotImplementedError
470
+
471
+ def isnan(self, x0: T) -> T:
472
+ raise NotImplementedError
473
+
474
+ # NB: this returns a float, like the torch operation
475
+ # This rounds half to even to break ties
476
+ def round(self, x0: T) -> T:
477
+ raise NotImplementedError
478
+
479
+ # NB: this returns a float, like the torch operation
480
+ def floor(self, x0: T) -> T:
481
+ raise NotImplementedError
482
+
483
+ def sign(self, x0: T) -> T:
484
+ raise NotImplementedError
485
+
486
+ # NB: this returns a float, like the torch operation
487
+ def trunc(self, x0: T) -> T:
488
+ raise NotImplementedError
489
+
490
+ # NB: this returns a float, like the torch operation
491
+ def ceil(self, x0: T) -> T:
492
+ raise NotImplementedError
493
+
494
+ def neg(self, x0: T) -> T:
495
+ raise NotImplementedError
496
+
497
+ def reciprocal(self, x0: T) -> T:
498
+ raise NotImplementedError
499
+
500
+ def eq(self, x0: T, x1: T) -> T:
501
+ raise NotImplementedError
502
+
503
+ def ne(self, x0: T, x1: T) -> T:
504
+ raise NotImplementedError
505
+
506
+ def lt(self, x0: T, x1: T) -> T:
507
+ raise NotImplementedError
508
+
509
+ def gt(self, x0: T, x1: T) -> T:
510
+ raise NotImplementedError
511
+
512
+ def le(self, x0: T, x1: T) -> T:
513
+ raise NotImplementedError
514
+
515
+ def ge(self, x0: T, x1: T) -> T:
516
+ raise NotImplementedError
517
+
518
+ def add(self, x0: T, x1: T) -> T:
519
+ raise NotImplementedError
520
+
521
+ def sub(self, x0: T, x1: T) -> T:
522
+ raise NotImplementedError
523
+
524
+ def mul(self, x0: T, x1: T) -> T:
525
+ raise NotImplementedError
526
+
527
+ # NB: this returns a float, like the torch operation
528
+ def pow(self, x0: T, x1: T) -> T:
529
+ raise NotImplementedError
530
+
531
+ def and_(self, x0: T, x1: T) -> T:
532
+ raise NotImplementedError
533
+
534
+ def or_(self, x0: T, x1: T) -> T:
535
+ raise NotImplementedError
536
+
537
+ def xor(self, x0: T, x1: T) -> T:
538
+ raise NotImplementedError
539
+
540
+ # These are metaprogrammed by MockHandler._init_cls
541
+ def lshift(self, x0: T, x1: T) -> T:
542
+ raise NotImplementedError
543
+
544
+ def rshift(self, x0: T, x1: T) -> T:
545
+ raise NotImplementedError
546
+
547
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
548
+ # These are "special" operators. These only exist if the target
549
+ # language actually supports the operator. Keep this in sync with
550
+ # pointwise_overrides_data.
551
+
552
+ def airy_ai(self, x: T) -> T:
553
+ raise NotImplementedError
554
+
555
+ def bessel_j0(self, x: T) -> T:
556
+ raise NotImplementedError
557
+
558
+ def bessel_j1(self, x: T) -> T:
559
+ raise NotImplementedError
560
+
561
+ def bessel_y0(self, x: T) -> T:
562
+ raise NotImplementedError
563
+
564
+ def bessel_y1(self, x: T) -> T:
565
+ raise NotImplementedError
566
+
567
+ def digamma(self, x: T) -> T:
568
+ raise NotImplementedError
569
+
570
+ def erfcx(self, x: T) -> T:
571
+ raise NotImplementedError
572
+
573
+ def fma(self, x: T, y: T, z: T) -> T:
574
+ raise NotImplementedError
575
+
576
+ def mul_rn(self, x: T, y: T) -> T:
577
+ """Multiplication with round-to-nearest, preventing fusion with subsequent ops."""
578
+ raise NotImplementedError
579
+
580
+ def igamma(self, x: T, y: T) -> T:
581
+ raise NotImplementedError
582
+
583
+ def igammac(self, x: T, y: T) -> T:
584
+ raise NotImplementedError
585
+
586
+ def gammainc(self, x: T, y: T) -> T:
587
+ raise NotImplementedError
588
+
589
+ def gammaincc(self, x: T, y: T) -> T:
590
+ raise NotImplementedError
591
+
592
+ def i0(self, x: T) -> T:
593
+ raise NotImplementedError
594
+
595
+ def i0e(self, x: T) -> T:
596
+ raise NotImplementedError
597
+
598
+ def i1(self, x: T) -> T:
599
+ raise NotImplementedError
600
+
601
+ def i1e(self, x: T) -> T:
602
+ raise NotImplementedError
603
+
604
+ def log_ndtr(self, x: T) -> T:
605
+ raise NotImplementedError
606
+
607
+ def modified_bessel_i0(self, x: T) -> T:
608
+ raise NotImplementedError
609
+
610
+ def modified_bessel_i1(self, x: T) -> T:
611
+ raise NotImplementedError
612
+
613
+ def modified_bessel_k0(self, x: T) -> T:
614
+ raise NotImplementedError
615
+
616
+ def modified_bessel_k1(self, x: T) -> T:
617
+ raise NotImplementedError
618
+
619
+ def ndtr(self, x: T) -> T:
620
+ raise NotImplementedError
621
+
622
+ def ndtri(self, x: T) -> T:
623
+ raise NotImplementedError
624
+
625
+ def polygamma(self, x: T, y: T) -> T:
626
+ raise NotImplementedError
627
+
628
+ def scaled_modified_bessel_k0(self, x: T) -> T:
629
+ raise NotImplementedError
630
+
631
+ def scaled_modified_bessel_k1(self, x: T) -> T:
632
+ raise NotImplementedError
633
+
634
+ def spherical_bessel_j0(self, x: T) -> T:
635
+ raise NotImplementedError
636
+
637
+ def zeta(self, x: T, y: T) -> T:
638
+ raise NotImplementedError
639
+
640
+ def chebyshev_polynomial_t(self, x: T, y: T) -> T:
641
+ raise NotImplementedError
642
+
643
+ def chebyshev_polynomial_u(self, x: T, y: T) -> T:
644
+ raise NotImplementedError
645
+
646
+ def chebyshev_polynomial_v(self, x: T, y: T) -> T:
647
+ raise NotImplementedError
648
+
649
+ def chebyshev_polynomial_w(self, x: T, y: T) -> T:
650
+ raise NotImplementedError
651
+
652
+ def legendre_polynomial_p(self, x: T, y: T) -> T:
653
+ raise NotImplementedError
654
+
655
+ def shifted_chebyshev_polynomial_t(self, x: T, y: T) -> T:
656
+ raise NotImplementedError
657
+
658
+ def shifted_chebyshev_polynomial_u(self, x: T, y: T) -> T:
659
+ raise NotImplementedError
660
+
661
+ def shifted_chebyshev_polynomial_v(self, x: T, y: T) -> T:
662
+ raise NotImplementedError
663
+
664
+ def shifted_chebyshev_polynomial_w(self, x: T, y: T) -> T:
665
+ raise NotImplementedError
666
+
667
+ def hermite_polynomial_h(self, x: T, y: T) -> T:
668
+ raise NotImplementedError
669
+
670
+ def hermite_polynomial_he(self, x: T, y: T) -> T:
671
+ raise NotImplementedError
672
+
673
+ def laguerre_polynomial_l(self, x: T, y: T) -> T:
674
+ raise NotImplementedError
675
+
676
+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
677
+ # These operators are a bit special, because they are conventionally
678
+ # natively supported in both Python and C, but the semantics differ so
679
+ # care must be taken
680
+
681
+ def truncdiv(self, x0: T, x1: T) -> T:
682
+ """C-style trunc division between integers only. Computes the true
683
+ division of two numbers and rounds the result to zero.
684
+ """
685
+ raise NotImplementedError
686
+
687
+ def floordiv(self, x0: T, x1: T) -> T:
688
+ """Python-style floor division between integers only. Computes the
689
+ true division of two numbers and floors the result. If you want
690
+ floor division for floats, do regular truediv and floor the result.
691
+ """
692
+ raise NotImplementedError
693
+
694
+ def truediv(self, x0: T, x1: T) -> T:
695
+ """True division between floats. Integer inputs are NOT valid. To
696
+ do Python-style (int, int) -> float division, use int_truediv"""
697
+ raise NotImplementedError
698
+
699
+ def div_rn(self, x0: T, x1: T) -> T:
700
+ """Division with round-to-nearest rounding mode. Used for matching
701
+ eager CUDA semantics where division uses IEEE round-to-nearest."""
702
+ raise NotImplementedError
703
+
704
+ def int_truediv(self, x0: T, x1: T) -> T:
705
+ """True division between integers. This is NOT the same as promoting
706
+ to float and doing integer division, there is a bespoke algorithm for
707
+ doing the division in higher precision than the above.
708
+ """
709
+ raise NotImplementedError
710
+
711
+ def mod(self, x0: T, x1: T) -> T:
712
+ """C-style modulus, take sign from LHS (x0)."""
713
+ raise NotImplementedError
714
+
715
+ def remainder(self, x0: T, x1: T) -> T:
716
+ """Python-style modulus, take sign from RHS (x1)."""
717
+ raise NotImplementedError
718
+
719
+ def square(self, x0: T) -> T:
720
+ raise NotImplementedError
721
+
722
+ def check_bounds(
723
+ self, expr: sympy.Expr, size: sympy.Expr, lower: bool, upper: bool
724
+ ) -> None:
725
+ raise NotImplementedError
726
+
727
+ # halide-only
728
+ def halide_clamp(self, value: T, size: sympy.Expr, check: bool) -> T:
729
+ raise NotImplementedError
730
+
731
+ # triton-only
732
+ def dot(self, x: T, y: T) -> T:
733
+ raise NotImplementedError
734
+
735
+ # triton-only
736
+ def inline_asm_elementwise(
737
+ self,
738
+ *inputs: T,
739
+ asm: str,
740
+ constraints: str | None = None,
741
+ dtype: torch.dtype = torch.float32,
742
+ is_pure: bool = True,
743
+ pack: int = 1,
744
+ input_dtypes: tuple[torch.dtype, ...] | None = None,
745
+ ) -> T:
746
+ raise NotImplementedError
747
+
748
+ def output(self, *args: T) -> None:
749
+ """This is a fake op used in analysis but not codegen"""
750
+ raise NotImplementedError
751
+
752
+ def placeholder(self, index: int) -> T:
753
+ """This is a fake op used in analysis but not codegen"""
754
+ raise NotImplementedError
755
+
756
+ def device_assert_async(self, cond: T, msg: str) -> T:
757
+ raise NotImplementedError
758
+
759
+
760
+ _ignore_op_re = re.compile(r"_.*|paren").fullmatch
761
+
762
+
763
+ def list_ops(cls: type[Any]):
764
+ return OrderedSet([x for x in dir(cls) if not _ignore_op_re(x)])
765
+
766
+
767
+ OP_NAMES = list_ops(OpsHandler)
768
+
769
+
770
+ class DefaultHandler(OpsHandler[Any]):
771
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
772
+ """
773
+ Default implementation for all ops. Override in a subclass to
774
+ provide generic op behavior.
775
+
776
+ Args:
777
+ name: name of the op, see OpHandler.{name}
778
+ args: positional args passed to the op
779
+ kwargs: keyword args passed to the op
780
+
781
+ Returns:
782
+ return value of the op
783
+
784
+ """
785
+ raise NotImplementedError
786
+
787
+ def __getattr__(self, name: str) -> Any:
788
+ def fallback(*args: Any, **kwargs: Any) -> Any:
789
+ return self._default(name, args, kwargs)
790
+
791
+ # would like to remove this function entirely, but it's used in MTIA backend
792
+ warnings.warn(f"undefined OpHandler.{name}, please add missing op schema")
793
+ return fallback
794
+
795
+ @staticmethod
796
+ def _call_default(target: str):
797
+ def call_default(self, *args, **kwargs):
798
+ return self._default(target, args, kwargs)
799
+
800
+ call_default.__name__ = target
801
+ return call_default
802
+
803
+ @classmethod
804
+ def _init_cls(cls):
805
+ """
806
+ Here we codegen many functions of the form:
807
+
808
+ def add(self, a, b):
809
+ return self._default('add', (a, b), {})
810
+
811
+ and install them in cls. This is the same as _call_default above,
812
+ but is about 1.2x faster since CPython varargs parsing is slow.
813
+ """
814
+ code = StringIO()
815
+ for target in OP_NAMES:
816
+ sig = inspect.signature(getattr(OpsHandler, target))
817
+ if all(
818
+ p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
819
+ and p.default is inspect.Parameter.empty
820
+ for p in sig.parameters.values()
821
+ ):
822
+ self_arg, *args = sig.parameters.keys()
823
+ assert self_arg == "self"
824
+ code.write(
825
+ f"""
826
+ def {target}(self, {", ".join(args)}):
827
+ return self._default({target!r}, ({", ".join(args)}, ), {{}})
828
+ """.strip()
829
+ )
830
+ code.write("\n\n")
831
+ else:
832
+ # slower fallback for ops with default or variadic arguments
833
+ setattr(cls, target, cls._call_default(target))
834
+
835
+ ctx: dict[str, Any] = {}
836
+ exec(code.getvalue(), ctx)
837
+ for target, impl in ctx.items():
838
+ if target in OP_NAMES:
839
+ setattr(cls, target, impl)
840
+
841
+
842
+ DefaultHandler._init_cls()
843
+
844
+
845
+ class NoopHandler(DefaultHandler):
846
+ name = "NoopHandler"
847
+
848
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
849
+ return None
850
+
851
+ @staticmethod
852
+ def masked(mask, body, other) -> None:
853
+ return None
854
+
855
+ @staticmethod
856
+ # pyrefly: ignore [bad-override]
857
+ def frexp(x) -> tuple[None, None]:
858
+ return (None, None)
859
+
860
+ @staticmethod
861
+ def scan(dtypes, combine_fn, values) -> tuple[None, ...]:
862
+ return (None,) * len(values)
863
+
864
+ @staticmethod
865
+ def sort(dtypes, values, stable, descending) -> tuple[None, ...]:
866
+ return (None,) * len(values)
867
+
868
+ @staticmethod
869
+ # pyrefly: ignore [bad-override]
870
+ def indirect_indexing(index_var, size, check=True, wrap_neg=True) -> sympy.Symbol:
871
+ return sympy.S.Zero
872
+
873
+
874
+ class BasicMathOpsMixin:
875
+ @staticmethod
876
+ def add(a, b):
877
+ return f"{a} + {b}"
878
+
879
+ @staticmethod
880
+ def sub(a, b):
881
+ return f"{a} - {b}"
882
+
883
+ @staticmethod
884
+ def mul(a, b):
885
+ return f"{a} * {b}"
886
+
887
+ @staticmethod
888
+ def floordiv(a, b):
889
+ return f"{a} // {b}"
890
+
891
+ @staticmethod
892
+ def truediv(a, b):
893
+ return f"{a} / {b}"
894
+
895
+ @staticmethod
896
+ def mod(a, b):
897
+ # careful, depending on target semantics varies
898
+ return f"{a} % {b}"
899
+
900
+ @staticmethod
901
+ def pow(a, b):
902
+ return f"{a} ** {b}"
903
+
904
+ @staticmethod
905
+ def lshift(a, b):
906
+ return f"{a} << {b}"
907
+
908
+ @staticmethod
909
+ def rshift(a, b):
910
+ return f"{a} >> {b}"
911
+
912
+ @staticmethod
913
+ def and_(a, b):
914
+ return f"{a} & {b}"
915
+
916
+ @staticmethod
917
+ def or_(a, b):
918
+ return f"{a} | {b}"
919
+
920
+ @staticmethod
921
+ def xor(a, b):
922
+ return f"{a} ^ {b}"
923
+
924
+ @staticmethod
925
+ def eq(a, b):
926
+ return f"{a} == {b}"
927
+
928
+ @staticmethod
929
+ def ne(a, b):
930
+ return f"{a} != {b}"
931
+
932
+ @staticmethod
933
+ def lt(a, b):
934
+ return f"{a} < {b}"
935
+
936
+ @staticmethod
937
+ def gt(a, b):
938
+ return f"{a} > {b}"
939
+
940
+ @staticmethod
941
+ def le(a, b):
942
+ return f"{a} <= {b}"
943
+
944
+ @staticmethod
945
+ def ge(a, b):
946
+ return f"{a} >= {b}"
947
+
948
+ @staticmethod
949
+ def neg(a):
950
+ return f"-{a}"
951
+
952
+
953
+ class MockHandler(BasicMathOpsMixin, DefaultHandler):
954
+ name = "MockHandler"
955
+
956
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
957
+ fargs = [*map(_arg_str, args)]
958
+ for k, v in kwargs.items():
959
+ fargs.append(f"{k}={_arg_str(v)}")
960
+ return f"ops.{name}({', '.join(fargs)})"
961
+
962
+ @staticmethod
963
+ def masked(mask, body, other) -> str:
964
+ return f"ops.masked({mask}, {body()}, {other})"
965
+
966
+ @staticmethod
967
+ # pyrefly: ignore [bad-override]
968
+ def frexp(x):
969
+ return (f"ops.frexp({x})[0]", f"ops.frexp({x})[1]")
970
+
971
+ @staticmethod
972
+ def scan(dtypes, combine_fn, values):
973
+ return tuple(
974
+ f"ops.scan({dtypes}, {combine_fn}, {values})[{i}]"
975
+ for i in range(len(values))
976
+ )
977
+
978
+ @staticmethod
979
+ def sort(dtypes, values, stable, descending):
980
+ return tuple(
981
+ f"ops.sort({dtypes}, {values}, stable={stable}, descending={descending})[{i}]"
982
+ for i in range(len(values))
983
+ )
984
+
985
+ @staticmethod
986
+ # pyrefly: ignore [bad-override]
987
+ def indirect_indexing(index_var, size, check=True, wrap_neg=True) -> sympy.Symbol:
988
+ return sympy_index_symbol(str(index_var))
989
+
990
+
991
+ class KernelFormatterHandler(DefaultHandler):
992
+ def __init__(self, parent_handler: OpsHandler[Any]):
993
+ self.parent_handler = parent_handler
994
+ self._output = IndentedBuffer(1)
995
+ self.var_counter = itertools.count()
996
+
997
+ @staticmethod
998
+ def ir_to_string(ir_fn, index, rindex=None) -> str:
999
+ from .ir import FlexibleLayout
1000
+ from .virtualized import V
1001
+
1002
+ args = [index, rindex] if rindex is not None else [index]
1003
+ names = ["index", "rindex"] if rindex is not None else ["index"]
1004
+ formatter = KernelFormatterHandler(MockHandler())
1005
+
1006
+ with formatter._output.indent(-1):
1007
+ formatter._output.writeline(f"def inner_fn({', '.join(names)}):")
1008
+ for name, arg in zip(names, args):
1009
+ if arg:
1010
+ lhs = ", ".join(
1011
+ [
1012
+ str("_" if isinstance(v, (int, sympy.Integer)) else v)
1013
+ for v in arg
1014
+ ]
1015
+ )
1016
+ formatter._output.writeline(f"{lhs} = {name}")
1017
+
1018
+ with (
1019
+ V.set_ops_handler(formatter),
1020
+ patch.object(FlexibleLayout, "allow_indexing", True),
1021
+ ):
1022
+ result = ir_fn(*args)
1023
+ return formatter.getvalue(result)
1024
+
1025
+ def indirect_indexing(self, *args, **kwargs) -> sympy.Symbol:
1026
+ return self.parent_handler.indirect_indexing(*args, **kwargs)
1027
+
1028
+ def _write(self, line):
1029
+ # replace line with a new variable name
1030
+ varname = f"tmp{next(self.var_counter)}"
1031
+ self._output.writeline(f"{varname} = {line}")
1032
+ return varname
1033
+
1034
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
1035
+ return pytree.tree_map(
1036
+ self._write, getattr(self.parent_handler, name)(*args, **kwargs)
1037
+ )
1038
+
1039
+ def reduction(
1040
+ self,
1041
+ dtype: torch.dtype,
1042
+ src_dtype: torch.dtype,
1043
+ reduction_type: ReductionType,
1044
+ value: str | tuple[str, ...],
1045
+ ) -> str | tuple[str, ...]:
1046
+ line = self.parent_handler.reduction(dtype, src_dtype, reduction_type, value)
1047
+ num_values = reduction_num_outputs(reduction_type)
1048
+ varnames = [f"tmp{next(self.var_counter)}" for _ in range(num_values)]
1049
+ self._output.writeline(f"{','.join(varnames)} = {line}")
1050
+ return tuple(varnames) if num_values > 1 else varnames[0]
1051
+
1052
+ def getvalue(self, result):
1053
+ self._output.writeline(f"return {result}")
1054
+ return self._output.getvalue()
1055
+
1056
+
1057
+ class WrapperHandler(DefaultHandler):
1058
+ def __init__(self, inner: OpsHandler[Any]):
1059
+ self._inner = inner
1060
+
1061
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
1062
+ return getattr(self._inner, name)(*args, **kwargs)
1063
+
1064
+
1065
+ class AddParenHandler(WrapperHandler):
1066
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
1067
+ val = getattr(self._inner, name)(*args, **kwargs)
1068
+ if not val or isinstance(val, (sympy.Expr, tuple, list)):
1069
+ return val
1070
+ return f"({val})"
1071
+
1072
+
1073
+ class OpCountResult(NamedTuple):
1074
+ num_ops: int
1075
+ used_ops: OrderedSet[str]
1076
+ read_buffers: list[str]
1077
+ nontrivial_read_count: int
1078
+
1079
+
1080
+ class OpCounterCSE(DefaultHandler):
1081
+ """Shim to count how many ops are used"""
1082
+
1083
+ def __init__(self, inner: OpsHandler[Any]):
1084
+ super().__init__()
1085
+ self.parent_handler = inner
1086
+ self.op_count = 0
1087
+ self.var_names: dict[str, str] = {}
1088
+ self._used_ops: OrderedSet[str] = OrderedSet()
1089
+ self._read_names: list[str] = []
1090
+ self._nontrivial_read_count = 0
1091
+
1092
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
1093
+ self._used_ops.add(name)
1094
+ return pytree.tree_map(
1095
+ self._update_count, getattr(self.parent_handler, name)(*args, **kwargs)
1096
+ )
1097
+
1098
+ def _update_count(self, val):
1099
+ varname = self.var_names.get(val)
1100
+ if not varname:
1101
+ varname = f"tmp{self.op_count}"
1102
+ self.op_count += 1
1103
+ self.var_names[val] = varname
1104
+ return varname
1105
+
1106
+ def indirect_indexing(self, *args, **kwargs):
1107
+ self._used_ops.add("indirect_indexing")
1108
+ return self.parent_handler.indirect_indexing(*args, **kwargs)
1109
+
1110
+ def load(self, name: str, index: sympy.Expr) -> str:
1111
+ val = self.parent_handler.load(name, index)
1112
+ if val not in self.var_names:
1113
+ self._used_ops.add("load")
1114
+ self._read_names.append(name)
1115
+ if not isinstance(index, (sympy.Integer, int)):
1116
+ self._nontrivial_read_count += 1
1117
+ return self._update_count(val)
1118
+
1119
+ def load_seed(self, name: str, offset: T):
1120
+ val = self.parent_handler.load_seed(name, offset)
1121
+ if val not in self.var_names:
1122
+ self._used_ops.add("load_seed")
1123
+ self._read_names.append(name)
1124
+ return self._update_count(val)
1125
+
1126
+ def bucketize(
1127
+ self,
1128
+ values: T,
1129
+ boundaries: tuple[str, sympy.Expr, sympy.Expr, sympy.Expr],
1130
+ boundary_indices: T,
1131
+ indexing_dtype: torch.dtype,
1132
+ right: bool,
1133
+ sorter: tuple[str, sympy.Expr] | None = None,
1134
+ sorter_indices: T | None = None,
1135
+ ) -> T:
1136
+ """
1137
+ See [Note: Inductor bucketize op]
1138
+ """
1139
+ val = self.parent_handler.bucketize(
1140
+ values,
1141
+ boundaries,
1142
+ boundary_indices,
1143
+ indexing_dtype,
1144
+ right,
1145
+ sorter,
1146
+ sorter_indices,
1147
+ )
1148
+ if val not in self.var_names:
1149
+ self._used_ops.add("bucketize")
1150
+ self._read_names.append(boundaries[0])
1151
+ if sorter is not None:
1152
+ self._read_names.append(sorter[0])
1153
+ return self._update_count(val)
1154
+
1155
+ def getvalue(self):
1156
+ return OpCountResult(
1157
+ self.op_count, self._used_ops, self._read_names, self._nontrivial_read_count
1158
+ )
1159
+
1160
+
1161
+ class ExtractConstantsHandler(NoopHandler):
1162
+ def __init__(self, device: torch.device | None):
1163
+ self.device = device
1164
+
1165
+ def constant(self, value: Any, dtype: torch.dtype) -> torch._inductor.ir.Constant:
1166
+ from torch._inductor import ir
1167
+
1168
+ return ir.Constant(
1169
+ value=value, dtype=dtype, device=self.device or torch.get_default_device()
1170
+ )
1171
+
1172
+
1173
+ class SimpleCSEHandler(WrapperHandler):
1174
+ """Wraps the underlying handler with a CSE pass
1175
+
1176
+ NOTE: Compared to codegen level CSE this is simplified as it
1177
+ doesn't support stores which require load cache invalidation.
1178
+ """
1179
+
1180
+ def __init__(self, inner: Any):
1181
+ super().__init__(inner)
1182
+ self.cse_cache: dict[str, Any | tuple[Any, ...]] = {}
1183
+ self.mock = MockHandler()
1184
+
1185
+ def indirect_indexing(self, *args, **kwargs) -> sympy.Expr:
1186
+ return super().indirect_indexing(*args, **kwargs) # type: ignore[misc]
1187
+
1188
+ def store(self, *args, **kwargs) -> None:
1189
+ raise NotImplementedError("store not implemented")
1190
+
1191
+ def store_reduction(self, *args, **kwargs) -> None:
1192
+ raise NotImplementedError("store not implemented")
1193
+
1194
+ def _default(self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
1195
+ key = getattr(self.mock, name)(*args, **kwargs)
1196
+ val = self.cse_cache.get(key)
1197
+ if val is not None:
1198
+ return val
1199
+
1200
+ val = getattr(self._inner, name)(*args, **kwargs)
1201
+ self.cse_cache[key] = val
1202
+ return val
1203
+
1204
+ def device_assert_async(self, *args, **kwargs) -> None:
1205
+ raise NotImplementedError(
1206
+ f"{type(self).__name__}: device_assert_async should be handled by CSEProxy"
1207
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/optimize_indexing.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Any
3
+
4
+ import sympy
5
+
6
+ import torch
7
+ from torch.utils._sympy.value_ranges import ValueRanges
8
+
9
+ from .loop_body import LoopBody
10
+ from .utils import dominated_nodes
11
+
12
+
13
+ def val_expressable_in_32_bits(val: Any) -> bool:
14
+ if getattr(val, "is_Boolean", False):
15
+ return True
16
+
17
+ if isinstance(val, sympy.Expr):
18
+ assert val.is_number
19
+ if val.is_Integer or val.is_Boolean:
20
+ val = int(val)
21
+ else:
22
+ val = float(val)
23
+
24
+ # bound within mantissa
25
+ if isinstance(val, float):
26
+ return val <= (2**24) and val >= -(2**24)
27
+
28
+ if isinstance(val, int):
29
+ iinfo = torch.iinfo(torch.int32)
30
+ return val <= iinfo.max and val >= iinfo.min
31
+
32
+ raise TypeError(f"Unexpected value {val}")
33
+
34
+
35
+ def range_expressable_in_32_bits(range: ValueRanges[sympy.Expr]) -> bool:
36
+ return val_expressable_in_32_bits(range.lower) and val_expressable_in_32_bits(
37
+ range.upper
38
+ )
39
+
40
+
41
+ def try_to_reduce_precision(
42
+ node: Any,
43
+ bounds: dict[Any, Any],
44
+ indirect_vars: list[Any],
45
+ indices: dict[Any, sympy.Expr],
46
+ replacement_vals: dict[Any, ValueRanges[sympy.Expr]],
47
+ ) -> None:
48
+ # if a downstream use of a node explicitly converts to int32, or float16/float32/float64,
49
+ # then it's precision is set for that chain of uses, and we don't need to consider those
50
+ # dominated values
51
+ def skip_filter(node: Any) -> bool:
52
+ return node.target == "to_dtype" and node.args[2] in (
53
+ torch.int32,
54
+ torch.float32,
55
+ torch.float64,
56
+ )
57
+
58
+ # TODO - there are dominated uses whose dtype does not depend on whether
59
+ # we reduce the precision here, e.g. add(int64, int64) one of the args can be reduced to
60
+ # int32 without changing the output precision of the node. this case hasn't shown up
61
+ for dominated in dominated_nodes([node], skip_filter):
62
+ if dominated.target in ["store", "output"]:
63
+ continue
64
+
65
+ if isinstance(dominated.target, str) and "set_indirect" in dominated.target:
66
+ idx = int(dominated.target[len("set_indirect") :])
67
+ indirect_var = indirect_vars[idx]
68
+
69
+ # We check that we can compute all the indices it's involved in with int32
70
+ for index, expr in indices.items():
71
+ if indirect_var in expr.free_symbols:
72
+ index_val = replacement_vals[index]
73
+
74
+ if math.isinf(index_val.lower) or math.isinf(index_val.upper):
75
+ return
76
+
77
+ # all indices are integers, so make sure that we
78
+ # use the bounds of integers instead of floats.
79
+ # TODO - not sure if we should be doing int/float casts while tracing,
80
+ # might interfere with sympy.
81
+
82
+ index_val_int = ValueRanges[sympy.Expr](
83
+ int(index_val.lower), int(index_val.upper)
84
+ )
85
+ if not range_expressable_in_32_bits(index_val_int):
86
+ return
87
+
88
+ if not range_expressable_in_32_bits(bounds[dominated]):
89
+ return
90
+
91
+ args = list(node.args)
92
+ args[2] = torch.int32
93
+ node.args = tuple(args)
94
+
95
+
96
+ def indexing_dtype_strength_reduction(loop_body: LoopBody) -> None:
97
+ """
98
+ Performs Value Range Analysis on LoopBody's fx graph to reduce precision of
99
+ intermediaries from int64 to int32
100
+ """
101
+ bv = loop_body.bounds()
102
+
103
+ int64_dtype_nodes = [
104
+ node
105
+ for node in loop_body.get_nodes()
106
+ if (
107
+ node.target == "to_dtype"
108
+ and node.args[2] == torch.int64
109
+ and node not in bv.unbounded_vars
110
+ )
111
+ ]
112
+ if not int64_dtype_nodes:
113
+ return
114
+
115
+ bounds = bv.get_bounds()
116
+
117
+ # TODO - if dominated node of one to_dtype is not expressible in int32,
118
+ # we should short circuit another to_dtype node if that node also dominates
119
+ for node in int64_dtype_nodes:
120
+ try_to_reduce_precision(
121
+ node,
122
+ bounds,
123
+ loop_body.indirect_vars,
124
+ loop_body.indexing_exprs,
125
+ bv.replacement_vals,
126
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/output_code.py ADDED
@@ -0,0 +1,1202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This provides an abstract class which parametrizes over an "output code" concept
3
+ for Inductor. Intuitively, this represents the compiled callable which Inductor
4
+ produces which you can call to get optimized code. However, this callable
5
+ has some other capabilities:
6
+
7
+ - It is serializable, so you can save/load this product from disk without
8
+ having to do compilation again.
9
+
10
+ - (When using remote cache) it is addressable, so you can save just a key
11
+ which you can use to load this product from remote cache later.
12
+
13
+ This class is abstract because we have several different implementations of
14
+ serialized format:
15
+
16
+ - Python wrapper (the default)
17
+
18
+ - AOTInductor (this produces ABI stable binaries which work across PyTorch
19
+ versions)
20
+
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import dataclasses
26
+ import logging
27
+ import os
28
+ from functools import partial
29
+ from typing import Any, TYPE_CHECKING, TypeAlias
30
+
31
+ import torch
32
+ from torch._dynamo.utils import counters, get_runtime_metrics_context
33
+ from torch._higher_order_ops.wrap import inductor_compiled_code
34
+ from torch._inductor.cudagraph_utils import (
35
+ BoxedDeviceIndex,
36
+ CudagraphCachedInfo,
37
+ CudagraphMetadata,
38
+ get_partition_cudagraph_metadata,
39
+ get_placeholder_info,
40
+ log_cudagraph_skip_and_bump_counter,
41
+ )
42
+ from torch._inductor.freezing_utils import has_frozen_params, is_frozen_param
43
+ from torch._inductor.utils import (
44
+ _unstable_customized_partition_wrapper,
45
+ align_inputs_from_check_idxs,
46
+ BoxedBool,
47
+ CUDAGraphWrapperMetadata,
48
+ GraphPartitionMap,
49
+ InputType,
50
+ output_node,
51
+ set_tracing_context_output_strides,
52
+ )
53
+ from torch._opaque_base import OpaqueBase
54
+ from torch.fx._graph_pickler import _node_metadata_key_filter_safe, _ops_filter_safe
55
+ from torch.utils._ordered_set import OrderedSet
56
+ from torch.utils._python_dispatch import is_in_torch_dispatch_mode
57
+
58
+ from . import config
59
+ from .runtime.autotune_cache import AutotuneCacheBundler
60
+
61
+
62
+ if TYPE_CHECKING:
63
+ from collections import Counter
64
+ from collections.abc import Callable, Sequence
65
+
66
+ from torch._inductor import metrics
67
+ from torch._inductor.graph import GraphLowering
68
+ from torch._library.fake_class_registry import FakeScriptObject
69
+ from torch.export.pt2_archive._package_weights import Weights
70
+
71
+ from .compile_fx import _CompileFxKwargs
72
+ from .triton_bundler import TritonBundle
73
+
74
+ log = logging.getLogger(__name__)
75
+
76
+
77
+ @dataclasses.dataclass
78
+ class OutputCode:
79
+ # TODO: Remove underscores here
80
+
81
+ # None if the output is not remote cacheable
82
+ _fx_graph_cache_key: str | None = dataclasses.field(default=None, init=False)
83
+ _fx_graph_cache_debug_lines: list[str] | None = dataclasses.field(
84
+ default=None, init=False
85
+ )
86
+
87
+ # How long it took to compile this OutputCode, end to end
88
+ _time_taken_ns: int | None = dataclasses.field(default=None, init=False)
89
+
90
+ def __call__(self, inputs: Sequence[Any]) -> Any:
91
+ raise NotImplementedError(type(self))
92
+
93
+ def prepare_for_serialization(self) -> None:
94
+ raise NotImplementedError(type(self))
95
+
96
+ def post_compile(
97
+ self,
98
+ example_inputs: Sequence[InputType],
99
+ constants: CompiledFxGraphConstants,
100
+ graph_kwargs: _CompileFxKwargs,
101
+ ) -> None:
102
+ raise NotImplementedError(type(self))
103
+
104
+ # TODO: Get rid of this
105
+ def set_triton_bundle(self, triton_bundle: Any) -> None:
106
+ raise NotImplementedError(type(self))
107
+
108
+
109
+ _StrideExprStr: TypeAlias = str
110
+
111
+
112
+ # copy_ fails when trying to write to tensors with memory overlap,
113
+ # for expanded dimensions (a dimension which used to have size 1 -> ?)
114
+ # we can select one element from that dimension and write to it
115
+ # to achieve writing to all values of that dimension of the input tensor
116
+ def get_expanded_dims(t: torch.Tensor) -> list[int]:
117
+ if not isinstance(t, torch.Tensor):
118
+ # pyrefly: ignore [bad-return]
119
+ return None
120
+ return [i for i in range(t.ndim) if t.stride(i) == 0 and t.size(i) != 1]
121
+
122
+
123
+ def index_expanded_dims(t: torch.Tensor, expanded_dims: list[int]) -> torch.Tensor:
124
+ for expanded_dim in expanded_dims:
125
+ t = torch.ops.aten.slice(t, expanded_dim, 0, 1)
126
+ return t
127
+
128
+
129
+ def complex_memory_overlap(t: torch.Tensor) -> bool:
130
+ if config.always_complex_memory_overlap_TESTING_ONLY:
131
+ return True
132
+
133
+ # if torch._debug_has_internal_overlap thinks this tensor potentially has
134
+ # memory overlap internally, let's dig deeper to find out whether it's true.
135
+ #
136
+ # Call squeeze() so that dimension with size 1 does not cause false positive.
137
+ t = index_expanded_dims(t, get_expanded_dims(t)).squeeze()
138
+ if torch._debug_has_internal_overlap(t) != 0:
139
+ strides = t.stride()
140
+ sizes = t.shape
141
+ indices = list(range(len(strides)))
142
+ indices = [x for _, x in sorted(zip(strides, indices))]
143
+ for i in range(len(strides)):
144
+ prev_stride = 1 if i == 0 else strides[indices[i - 1]]
145
+ prev_size = 1 if i == 0 else sizes[indices[i - 1]]
146
+ if strides[indices[i]] < prev_stride * prev_size:
147
+ return True
148
+ return False
149
+
150
+
151
+ def maybe_handle_backward_generation(
152
+ compiled_graph: CompiledFxGraph,
153
+ boxed_forward_device_index: BoxedDeviceIndex | None,
154
+ ) -> None:
155
+ assert compiled_graph.current_callable is not None
156
+ is_backward = compiled_graph.fx_kwargs["is_backward"]
157
+
158
+ # See [Backward Generation Handling]
159
+ # if cudagraph'd the forward and set the device, we need to let the cudagraph manager
160
+ # know we are we running the backward even if we will not run it in cudagraphs
161
+ if is_backward and config.triton.cudagraph_trees:
162
+ assert boxed_forward_device_index is not None
163
+ assert boxed_forward_device_index.value is not None
164
+ compiled_graph_callable = compiled_graph.current_callable
165
+
166
+ manager = torch._inductor.cudagraph_trees.get_manager(
167
+ boxed_forward_device_index.value, create_if_none_exists=False
168
+ )
169
+ # should already exist from forward
170
+ assert manager is not None
171
+
172
+ def compiled_artifact(new_inputs: list[Any]) -> Callable[..., Any]:
173
+ manager.set_to_running_backward() # type: ignore[union-attr]
174
+ return compiled_graph_callable(new_inputs)
175
+
176
+ compiled_graph.current_callable = compiled_artifact
177
+
178
+
179
+ def prepare_cudagraph_post_compile(
180
+ compiled_graph: CompiledFxGraph,
181
+ example_inputs: Sequence[InputType],
182
+ boxed_forward_device_index: BoxedDeviceIndex | None,
183
+ ) -> None:
184
+ if not config.triton.cudagraph_trees:
185
+ # Force specialize all inputs so that CUDA graphs will work
186
+ for t in example_inputs:
187
+ if isinstance(t, torch.SymInt):
188
+ int(t) # guard
189
+
190
+ is_inference = compiled_graph.fx_kwargs["is_inference"]
191
+ is_backward = compiled_graph.fx_kwargs["is_backward"]
192
+ if boxed_forward_device_index is not None and not is_inference and not is_backward:
193
+ boxed_forward_device_index.set(next(iter(compiled_graph.device_idxs)))
194
+
195
+
196
+ def cudagraph_post_compile(
197
+ example_inputs: Sequence[InputType],
198
+ compiled_graph: CompiledFxGraph,
199
+ cudagraphs: BoxedBool,
200
+ constants: dict[str, torch.Tensor | type],
201
+ boxed_forward_device_index: BoxedDeviceIndex | None,
202
+ ) -> None:
203
+ """
204
+ Checks for any reasons not to run cudagraphs and then
205
+ runs it on compiled_graph.
206
+ Mutates the `compiled_graph.current_callable` and `cudagraphs`
207
+ """
208
+ from torch._inductor.compiler_bisector import CompilerBisector
209
+
210
+ assert compiled_graph.current_callable is not None
211
+ assert compiled_graph.cudagraph_info is not None
212
+ cached_info = compiled_graph.cudagraph_info
213
+ cudagraph_fail_reasons = cached_info.cudagraph_fail_reasons
214
+ is_inference = compiled_graph.fx_kwargs["is_inference"]
215
+ is_backward = compiled_graph.fx_kwargs["is_backward"]
216
+
217
+ # Check if bisector wants to disable cudagraphs for this graph
218
+ if CompilerBisector.disable_subsystem("inductor", "cudagraphs"):
219
+ BoxedBool.disable(cudagraphs)
220
+ maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
221
+ log_cudagraph_skip_and_bump_counter("skipping cudagraphs due to bisector")
222
+ return
223
+
224
+ if not cudagraph_fail_reasons:
225
+ fx_kwargs = compiled_graph.fx_kwargs
226
+ static_input_idxs = fx_kwargs["static_input_idxs"]
227
+
228
+ placeholders = cached_info.placeholders
229
+ stack_traces = cached_info.stack_traces
230
+ assert stack_traces is not None, (
231
+ "stack_traces should not be None in cudagraph_post_compile"
232
+ )
233
+
234
+ prepare_cudagraph_post_compile(
235
+ compiled_graph, example_inputs, boxed_forward_device_index
236
+ )
237
+
238
+ current_callable = compiled_graph.current_callable
239
+ assert current_callable is not None
240
+ # Filter to only tensor constants (exclude opaque value type classes)
241
+ tensor_constants = {
242
+ k: v for k, v in constants.items() if isinstance(v, torch.Tensor)
243
+ }
244
+
245
+ device_index = next(iter(compiled_graph.device_idxs))
246
+ cudagraphify_kwargs = dict(
247
+ device_index=device_index,
248
+ stack_traces=stack_traces,
249
+ is_backward=is_backward,
250
+ is_inference=is_inference,
251
+ constants=tuple(tensor_constants.values()),
252
+ placeholders=placeholders,
253
+ mutated_input_idxs=tuple(compiled_graph.mutated_input_idxs),
254
+ )
255
+
256
+ policy = config.cudagraph_policy
257
+ if policy is not None:
258
+ compiled_graph.current_callable = policy.cudagraphify(
259
+ current_callable,
260
+ example_inputs,
261
+ static_input_idxs or (),
262
+ **cudagraphify_kwargs,
263
+ )
264
+ else:
265
+ from .compile_fx import cudagraphify
266
+
267
+ compiled_graph.current_callable = cudagraphify(
268
+ current_callable,
269
+ static_input_idxs=static_input_idxs or (),
270
+ **cudagraphify_kwargs,
271
+ )
272
+
273
+ else:
274
+ BoxedBool.disable(cudagraphs)
275
+ maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
276
+
277
+ if "cuda" in compiled_graph.device_types:
278
+ # prefer better disable_cudagraphs_reason bc stack trace
279
+ # TODO: migrate all disable reasons to stack trace, refactor
280
+ if compiled_graph.disabled_cudagraphs_reason:
281
+ log_cudagraph_skip_and_bump_counter(
282
+ compiled_graph.disabled_cudagraphs_reason
283
+ )
284
+ else:
285
+ log_cudagraph_skip_and_bump_counter(
286
+ f"skipping cudagraphs due to {cudagraph_fail_reasons}"
287
+ )
288
+
289
+
290
+ def cudagraph_partition_post_compile(
291
+ example_inputs: Sequence[InputType],
292
+ compiled_graph: CompiledFxGraph,
293
+ cudagraphs: BoxedBool,
294
+ constants: dict[str, torch.Tensor | type],
295
+ boxed_forward_device_index: BoxedDeviceIndex | None,
296
+ ) -> None:
297
+ """
298
+ Cudagraphify each partition functions, which first prepares the necessary
299
+ metadata and then applies the cudagraphify function to each partition.
300
+
301
+ Assuming all partition functions are cudagraphified and share the same order
302
+ as `compiled_graph.partition_maps`. See [Note: Graph Partition Map for CUDAGraph].
303
+ """
304
+ from torch._inductor.compiler_bisector import CompilerBisector
305
+
306
+ if CompilerBisector.disable_subsystem("inductor", "cudagraphs"):
307
+ BoxedBool.disable(cudagraphs)
308
+ maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
309
+ log_cudagraph_skip_and_bump_counter("skipping cudagraphs due to bisector")
310
+ return
311
+
312
+ assert compiled_graph.cudagraph_info is not None
313
+ cudagraph_fail_reasons = compiled_graph.cudagraph_info.cudagraph_fail_reasons
314
+
315
+ if (
316
+ cudagraph_fail_reasons
317
+ or compiled_graph.partition_maps is None
318
+ or len(compiled_graph.partition_maps) == 0
319
+ ):
320
+ # cudagraphify is not called if there are no partitions
321
+ BoxedBool.disable(cudagraphs)
322
+ maybe_handle_backward_generation(compiled_graph, boxed_forward_device_index)
323
+ return
324
+
325
+ assert compiled_graph.current_callable is not None
326
+ assert compiled_graph.recursively_apply_fns is not None
327
+ is_inference = compiled_graph.fx_kwargs["is_inference"]
328
+ is_backward = compiled_graph.fx_kwargs["is_backward"]
329
+ static_input_idxs = OrderedSet(compiled_graph.fx_kwargs["static_input_idxs"] or ())
330
+ mutated_input_idxs = compiled_graph.mutated_input_idxs
331
+ device_index = next(iter(compiled_graph.device_idxs))
332
+
333
+ # Filter to only tensor constants (exclude opaque value type classes)
334
+ tensor_constants = {
335
+ k: v for k, v in constants.items() if isinstance(v, torch.Tensor)
336
+ }
337
+
338
+ assert compiled_graph.cudagraph_info.stack_traces is not None, (
339
+ "stack_traces should not be None in cudagraph_partition_post_compile"
340
+ )
341
+ graph_metadata = CudagraphMetadata(
342
+ compiled_graph.cudagraph_info.placeholders,
343
+ static_input_idxs,
344
+ mutated_input_idxs,
345
+ compiled_graph.cudagraph_info.stack_traces,
346
+ tensor_constants,
347
+ )
348
+
349
+ prepare_cudagraph_post_compile(
350
+ compiled_graph, example_inputs, boxed_forward_device_index
351
+ )
352
+
353
+ from .compile_fx import cudagraphify
354
+
355
+ # cudagraphify each partition function, assuming every graph partition function
356
+ # is cudagraphable. Non-cudagraphable ops (e.g., cpu ops) are inlined into
357
+ # `call` function and not included in partition functions.
358
+ cudagraphify_fns = []
359
+ for partition_map in compiled_graph.partition_maps:
360
+ partition_metadata = get_partition_cudagraph_metadata(
361
+ partition_map,
362
+ graph_metadata,
363
+ )
364
+
365
+ cudagraphify_fn = partial(
366
+ cudagraphify,
367
+ static_input_idxs=tuple(partition_metadata.static_input_idxs),
368
+ device_index=device_index,
369
+ stack_traces=partition_metadata.stack_traces,
370
+ is_backward=is_backward,
371
+ is_inference=is_inference,
372
+ constants=tuple(partition_metadata.constants.values()),
373
+ placeholders=partition_metadata.placeholders,
374
+ mutated_input_idxs=tuple(partition_metadata.mutated_input_idxs),
375
+ )
376
+ cudagraphify_fns.append(cudagraphify_fn)
377
+
378
+ compiled_graph.recursively_apply_fns(cudagraphify_fns)
379
+
380
+
381
+ def maybe_realign_inputs(
382
+ ran_cudagraphs: BoxedBool,
383
+ compiled_graph: CompiledFxGraph,
384
+ inputs_to_check: Sequence[int],
385
+ mutated_inputs_idxs: OrderedSet[int],
386
+ ) -> None:
387
+ """
388
+ Realigns input strides from inputs_to_check if
389
+ we didn't end up running cudagraphs. Mutates
390
+ `compiled_graph.current_callable` if cudagraphs
391
+ was run. Otherwise, does nothing.
392
+
393
+ Non-mutated inputs are handled by deferred alignment copies
394
+ in the generated code. Only mutated inputs need the wrapper
395
+ for writeback.
396
+ """
397
+ if not ran_cudagraphs:
398
+ check_idxs = inputs_to_check
399
+ if compiled_graph._defers_input_alignment:
400
+ # Non-mutated inputs are handled by deferred alignment copies
401
+ # in the generated Python code. Only mutated inputs need the wrapper
402
+ # for writeback. Backends that don't emit deferred copies (cpp_wrapper,
403
+ # FXIR) need the full wrapper.
404
+ check_idxs = [i for i in inputs_to_check if i in mutated_inputs_idxs]
405
+ if check_idxs:
406
+ assert compiled_graph.current_callable is not None
407
+ new_callable = align_inputs_from_check_idxs(
408
+ compiled_graph.current_callable,
409
+ check_idxs,
410
+ mutated_inputs_idxs,
411
+ )
412
+ if new_callable is not compiled_graph.current_callable:
413
+ compiled_graph.current_callable = new_callable
414
+
415
+
416
+ class CompiledFxGraphConstants:
417
+ """Wrapper class that unwraps constants from a compiled fx graph. This
418
+ version of the class only supports directly grabbing the saved constants off of
419
+ a CompiledFxGraph.
420
+
421
+ With freezing, FxGraphCache doesn't store the constants of the input
422
+ GraphModule it gets from AOTAutograd. Instead, it saves just the **names**
423
+ of those constants, and grabs the constant values directly from the graph module
424
+ passed in at runtime.
425
+
426
+ Thing is, we don't always *have* the graph module available at runtime, hence
427
+ the existence of this class and its CompiledFxGraphConstantsWithGm counterpart.
428
+
429
+ To support freezing, FXGraphCache gets passed a CompiledFxGraphConstantsWithGm during
430
+ post compile. Otherwise, CompiledFxGraphConstants supports the basic case of loading
431
+ the value of constants directly off of the original saved object.
432
+ """
433
+
434
+ def unwrap(self, g: CompiledFxGraph) -> dict[str, torch.Tensor | type]:
435
+ assert g.constants is not None
436
+ return {**g.constants, **g.opaque_value_type_classes}
437
+
438
+
439
+ class CompiledFxGraphConstantsWithGm(CompiledFxGraphConstants):
440
+ """
441
+ This version of CompiledFxGraphConstants, instead of grabbing constants
442
+ directly saved on CompiledFxGraphs, will just grab their names. Then, it takes
443
+ a second GraphModule to grab the corresponding constant values out of.
444
+
445
+ This is necessary for supporting freezing in FxGraphCache.
446
+ """
447
+
448
+ def __init__(self, gm: torch.fx.GraphModule) -> None:
449
+ self.gm = gm
450
+
451
+ def unwrap(self, g: CompiledFxGraph) -> dict[str, torch.Tensor | type]:
452
+ frozen_params = {
453
+ name: getattr(self.gm, orig_name)
454
+ for name, orig_name in g.frozen_param_names.items()
455
+ }
456
+ constants = g.constants or {}
457
+ return {**constants, **frozen_params, **g.opaque_value_type_classes}
458
+
459
+
460
+ @dataclasses.dataclass
461
+ class CompiledFxGraph(OutputCode):
462
+ """
463
+ Class holding a compiled FX graph. This is the object serialized on disk
464
+ to support FxGraph caching.
465
+ """
466
+
467
+ current_callable: Callable[..., Any] | None
468
+ recursively_apply_fns: Callable[..., Any] | None
469
+ compiled_fn_runner: Any | None
470
+ cache_key: str
471
+ source_code: str = dataclasses.field(repr=False) # Do not display source_code
472
+ runnable_graph_str: str = dataclasses.field(repr=False) # Do not display graph
473
+ inductor_post_grad_graph_str: str = dataclasses.field(
474
+ repr=False
475
+ ) # Do not display graph
476
+ cache_linemap: list[tuple[int, str]] | None
477
+ device_types: OrderedSet[str]
478
+ device_idxs: OrderedSet[int]
479
+ mutated_inputs: OrderedSet[str]
480
+ mutated_input_idxs: OrderedSet[int]
481
+ constants: dict[str, torch.Tensor] | None
482
+ frozen_param_names: dict[str, str]
483
+ torchbind_constants: dict[str, torch._C.ScriptObject | FakeScriptObject]
484
+ opaque_value_type_classes: dict[str, type]
485
+ output_strides: list[tuple[_StrideExprStr, ...] | None] | None
486
+ disabled_cudagraphs_reason: str | None
487
+ metrics_deltas: metrics.CachedMetricsDeltas
488
+ counter_deltas: Counter[str]
489
+ # This is a string representation of an expression we serialize
490
+ # with the object so the guards can be evaluated in a different
491
+ # context in order to verify the validity of serving a cached
492
+ # fx graph. The expression must be generated by:
493
+ # ShapeEnv.produce_guards_expression()
494
+ guards_expr: str | None
495
+ extern_libs_key: str | None
496
+ inductor_provenance_mapping_str: str | None
497
+ inductor_provenance_stack_traces_str: str | None
498
+
499
+ cudagraph_info: CudagraphCachedInfo | None
500
+ partition_maps: list[GraphPartitionMap] | None
501
+ compile_region_name: str | None
502
+ fx_kwargs: _CompileFxKwargs
503
+ inputs_to_check: Sequence[int]
504
+
505
+ _boxed_call: bool | None = None
506
+ _triton_bundle: TritonBundle | None = None
507
+ _wrap_compiled_regions: bool = False
508
+ _defers_input_alignment: bool = False
509
+ # Metadata-stripped copy of the FX graph for fake tensor propagation.
510
+ # Running this graph under FakeTensorMode re-derives output shapes
511
+ # (including aliasing) from the input shapes.
512
+ _original_gm: torch.fx.GraphModule | None = None
513
+
514
+ def __init__(
515
+ self,
516
+ current_callable: Callable[..., Any] | None,
517
+ graph: GraphLowering,
518
+ gm: torch.fx.GraphModule,
519
+ output_strides: list[tuple[_StrideExprStr, ...] | None],
520
+ disabled_cudagraphs_reason: str | None,
521
+ metrics_deltas: metrics.CachedMetricsDeltas,
522
+ counter_deltas: Counter[str],
523
+ cudagraphs: BoxedBool,
524
+ example_inputs: Sequence[InputType],
525
+ static_input_idxs: Sequence[int],
526
+ compile_region_name: str | None,
527
+ fx_kwargs: _CompileFxKwargs,
528
+ inputs_to_check: Sequence[int],
529
+ runnable_graph_str: str,
530
+ inductor_post_grad_graph_str: str,
531
+ compiled_fn_runner: Any | None = None,
532
+ inductor_provenance_mapping_str: str | None = None,
533
+ inductor_provenance_stack_traces_str: str | None = None,
534
+ ) -> None:
535
+ self.current_callable = current_callable
536
+ self.compiled_fn_runner = compiled_fn_runner
537
+ self.recursively_apply_fns = (
538
+ compiled_fn_runner.recursively_apply_fns
539
+ if compiled_fn_runner is not None
540
+ else None
541
+ )
542
+ self.cache_key = graph.cache_key
543
+ if graph.cache_path:
544
+ with open(graph.cache_path) as f:
545
+ self.source_code = f.read()
546
+ self.runnable_graph_str = runnable_graph_str
547
+ self.inductor_post_grad_graph_str = inductor_post_grad_graph_str
548
+ self.inductor_provenance_mapping_str = inductor_provenance_mapping_str
549
+ self.inductor_provenance_stack_traces_str = inductor_provenance_stack_traces_str
550
+ self.cache_linemap = graph.cache_linemap
551
+ # TODO - ordered set
552
+ self.device_types = OrderedSet(graph.device_types)
553
+ self.device_idxs = OrderedSet(graph.device_idxs)
554
+ self.mutated_inputs = OrderedSet(graph.mutated_inputs)
555
+ self.mutated_input_idxs = OrderedSet(graph.mutated_input_idxs)
556
+
557
+ # We store the constant attributes in the cache entry and re-attach them
558
+ # to the module created in PyCodeCache.load_by_key_path. In the case that
559
+ # the graph has frozen parameters, we save the mapping from the attribute
560
+ # names in the GraphLowering to the original name of the attribute in the
561
+ # GraphModule. When we create the module from the cache entry, we then
562
+ # look up the constants from the current GraphModule. This scheme allows
563
+ # us to support caching with freezing.
564
+ if not has_frozen_params(gm):
565
+ self.constants = graph.constants
566
+ self.frozen_param_names = {}
567
+ else:
568
+ self.constants = {}
569
+ self.frozen_param_names = {}
570
+ for k, v in graph.constants.items():
571
+ if is_frozen_param(v):
572
+ self.frozen_param_names[k] = graph.allocated_constant_name[k]
573
+ else:
574
+ self.constants[k] = v
575
+
576
+ self.torchbind_constants = graph.torchbind_constants
577
+ self.opaque_value_type_classes = graph.opaque_value_type_classes
578
+ self.output_strides = output_strides
579
+ self.disabled_cudagraphs_reason = disabled_cudagraphs_reason
580
+ self.metrics_deltas = metrics_deltas
581
+ self.counter_deltas = counter_deltas
582
+ self.guards_expr = None
583
+ self.extern_libs_key = None
584
+ self.cudagraph_info = None
585
+ self.partition_maps = graph.partition_maps
586
+ self._defers_input_alignment = getattr(graph, "_defers_input_alignment", False)
587
+ self.fx_kwargs = {}
588
+ self.inputs_to_check = ()
589
+
590
+ cudagraph_info = None
591
+ if cudagraphs:
592
+ # check cudagraph disabling reasons from inductor lowering
593
+ if self.disabled_cudagraphs_reason:
594
+ if "cuda" in self.device_types:
595
+ log_cudagraph_skip_and_bump_counter(
596
+ f"skipping cudagraphs due to {self.disabled_cudagraphs_reason}"
597
+ )
598
+ else:
599
+ counters["inductor"]["cudagraph_skips"] += 1
600
+ BoxedBool.disable(cudagraphs)
601
+ else:
602
+ complex_memory_overlap_inputs = any(
603
+ complex_memory_overlap(t)
604
+ for t in example_inputs
605
+ if isinstance(t, torch.Tensor)
606
+ )
607
+
608
+ if not config.triton.cudagraph_support_input_mutation:
609
+ # Skip supports for cudagraph-managed tensors
610
+ from torch._inductor.cudagraph_utils import (
611
+ check_for_mutation_ignore_cuda_graph_managed_tensor,
612
+ )
613
+
614
+ has_mutation_str = (
615
+ check_for_mutation_ignore_cuda_graph_managed_tensor(
616
+ gm,
617
+ self.mutated_inputs,
618
+ self.mutated_input_idxs,
619
+ static_input_idxs,
620
+ )
621
+ )
622
+ has_mutation = has_mutation_str is not None
623
+
624
+ if has_mutation:
625
+ self.disabled_cudagraphs_reason = has_mutation_str
626
+ else:
627
+ # Check mutation later to support cudagraph-managed tensors
628
+ has_mutation = None
629
+
630
+ cudagraph_tests = [
631
+ (not has_mutation, "mutated inputs"),
632
+ (not complex_memory_overlap_inputs, "complex memory overlap"),
633
+ (
634
+ all(
635
+ isinstance(
636
+ t,
637
+ (
638
+ torch.Tensor,
639
+ torch.SymInt,
640
+ torch.Generator,
641
+ OpaqueBase,
642
+ ),
643
+ )
644
+ for t in example_inputs
645
+ ),
646
+ "non-Tensor inputs",
647
+ ),
648
+ ]
649
+ output = output_node(gm)
650
+ # output args are tuple of first argument
651
+ assert len(output.args) == 1
652
+ # Use stack traces captured on the output node before
653
+ # post-grad passes, which may strip stack_trace from
654
+ # individual arg nodes.
655
+ stack_traces = output.meta.get("output_stack_traces") or [
656
+ (arg.stack_trace if isinstance(arg, torch.fx.node.Node) else None)
657
+ for arg in output.args[0] # type: ignore[union-attr]
658
+ ]
659
+ cudagraph_fail_reasons = [s for b, s in cudagraph_tests if not b]
660
+ placeholders = tuple(get_placeholder_info(gm.graph))
661
+ cudagraph_info = CudagraphCachedInfo(
662
+ placeholders, stack_traces, cudagraph_fail_reasons
663
+ )
664
+
665
+ self.cudagraph_info = cudagraph_info
666
+ self.compile_region_name = compile_region_name
667
+ self.inputs_to_check = inputs_to_check
668
+ self.fx_kwargs = fx_kwargs
669
+
670
+ # aot autograd needs to know to pass in inputs as a list
671
+ self._boxed_call = True
672
+
673
+ # Store whether to wrap compiled regions in inductor_compiled_code HOP
674
+ # This is set at compile time to avoid runtime overhead
675
+ self._wrap_compiled_regions = config.wrap_inductor_compiled_regions
676
+
677
+ if self._wrap_compiled_regions:
678
+ # Store a metadata-stripped copy of the FX graph. Running this
679
+ # under FakeTensorMode re-derives output shapes and aliasing
680
+ # from the input fake tensors.
681
+ import copy
682
+
683
+ gm_copy = copy.deepcopy(gm)
684
+ for node in gm_copy.graph.nodes:
685
+ node.meta.clear()
686
+ self._original_gm = gm_copy
687
+
688
+ def __del__(self) -> None:
689
+ if self.compiled_fn_runner is not None:
690
+ # For torch._inductor.config.graph_partition = True,
691
+ # self.compiled_fn_runner.partitions hold cudagraphified functions
692
+ # which prevents deallocation. When CompiledFxGraph is deleted,
693
+ # self.compiled_fn_runner will not be called in the future so we
694
+ # should also delete these partitions.
695
+ del self.compiled_fn_runner.partitions
696
+
697
+ def __call__(self, inputs: Sequence[Any]) -> Any:
698
+ assert self.current_callable is not None
699
+
700
+ if (
701
+ torch._inductor.debug.RECORD_GRAPH_EXECUTION
702
+ and torch._inductor.debug.GRAPH_EXECUTION_ORDER is not None
703
+ ):
704
+ graph_id = self.fx_kwargs.get("graph_id")
705
+ compile_id = (
706
+ torch._inductor.debug.GRAPH_COMPILE_IDS.get(graph_id)
707
+ if graph_id is not None
708
+ and torch._inductor.debug.GRAPH_COMPILE_IDS is not None
709
+ else None
710
+ )
711
+ torch._inductor.debug.GRAPH_EXECUTION_ORDER.append(
712
+ {
713
+ "compile_id": compile_id,
714
+ }
715
+ )
716
+ try:
717
+ # Checking the profiler directly is faster than nullcontext
718
+ if torch.autograd.profiler._is_profiler_enabled:
719
+ with torch._C._profiler._RecordFunctionFast(
720
+ f"## Call CompiledFxGraph {self._fx_graph_cache_key} ##",
721
+ keyword_values={"scope": "user_scope"},
722
+ ):
723
+ return self.current_callable(inputs)
724
+ else:
725
+ return self.current_callable(inputs)
726
+ finally:
727
+ get_runtime_metrics_context().finish()
728
+ AutotuneCacheBundler.end_compile()
729
+
730
+ def post_compile(
731
+ self,
732
+ example_inputs: Sequence[InputType],
733
+ constants: CompiledFxGraphConstants,
734
+ graph_kwargs: _CompileFxKwargs,
735
+ ) -> None:
736
+ """
737
+ Run a set of post processing steps after loading from the cache. These involve:
738
+ - Setting the tracing context output strides
739
+ - Running cudagraphs if enabled
740
+ - Realigning inputs
741
+
742
+ This runs whether or not we have a cache hit, and always runs directly after we get a CompiledFxGraph.
743
+ The results of this function are *not* saved in the cache itself.
744
+ """
745
+ if config.graph_partition and _unstable_customized_partition_wrapper.wrapper:
746
+ # Mechanically apply user-specified cudagraph wrappers without modification
747
+ assert self.recursively_apply_fns is not None
748
+ assert self.compiled_fn_runner is not None
749
+ num_partitions = len(self.compiled_fn_runner.partitions)
750
+ wrapper_metadatas = [
751
+ CUDAGraphWrapperMetadata(num_partitions, i)
752
+ for i in range(num_partitions)
753
+ ]
754
+ customized_wrapper = _unstable_customized_partition_wrapper.wrapper
755
+ customized_wrappers_with_metadata = [
756
+ lambda f, m=metadata: customized_wrapper(f, m)
757
+ for metadata in wrapper_metadatas
758
+ ]
759
+ self.recursively_apply_fns(customized_wrappers_with_metadata)
760
+ return
761
+
762
+ set_tracing_context_output_strides(example_inputs, self)
763
+ assert graph_kwargs["cudagraphs"] is not None
764
+ assert graph_kwargs["is_backward"] is not None
765
+ is_backward = graph_kwargs["is_backward"]
766
+ cudagraphs: BoxedBool = graph_kwargs["cudagraphs"]
767
+
768
+ # When a CUDAGraphPolicy is set and it says not to wrap this
769
+ # inner CompiledFxGraph (e.g. because wrapping happens at the
770
+ # outer level via policy.wrap_output), disable cudagraphs for
771
+ # this graph so the rest of post_compile (input realignment,
772
+ # _wrap_compiled_regions) still runs normally.
773
+ policy = config.cudagraph_policy
774
+ if policy is not None and not policy.should_wrap(self):
775
+ counters["inductor"]["cudagraph_skips"] += 1
776
+ BoxedBool.disable(cudagraphs)
777
+
778
+ if cudagraphs:
779
+ # It's possible that cudagraphs is enabled, but was disabled
780
+ # during a previous compilation we're loading from the cache.
781
+ # If so, we need to disable it on this new process too.
782
+ if self.disabled_cudagraphs_reason:
783
+ if "cuda" in self.device_types:
784
+ log_cudagraph_skip_and_bump_counter(
785
+ f"skipping cudagraphs due to {self.disabled_cudagraphs_reason}"
786
+ )
787
+ else:
788
+ counters["inductor"]["cudagraph_skips"] += 1
789
+ BoxedBool.disable(cudagraphs)
790
+ else:
791
+ if is_backward:
792
+ assert "boxed_forward_device_index" in graph_kwargs
793
+ boxed_forward_device_index = graph_kwargs[
794
+ "boxed_forward_device_index"
795
+ ]
796
+ else:
797
+ # On the forward we don't know whether or not
798
+ # boxed_forward_device_index is set yet
799
+ boxed_forward_device_index = graph_kwargs.get(
800
+ "boxed_forward_device_index", None
801
+ )
802
+
803
+ if config.graph_partition and policy is None:
804
+ # With graph_partition=True, we skip some cudagraph checks
805
+ # if it's supported with partition, so we use
806
+ # cudagraph_partition_post_compile. When a CUDAGraphPolicy
807
+ # is active, we use cudagraph_post_compile instead so the
808
+ # policy controls wrapping via policy.cudagraphify().
809
+ cudagraph_partition_post_compile(
810
+ example_inputs,
811
+ self,
812
+ cudagraphs,
813
+ constants.unwrap(self),
814
+ boxed_forward_device_index,
815
+ )
816
+ else:
817
+ cudagraph_post_compile(
818
+ example_inputs,
819
+ self,
820
+ cudagraphs,
821
+ constants.unwrap(self),
822
+ boxed_forward_device_index,
823
+ )
824
+ inputs_to_check = self.inputs_to_check
825
+ # cudagraphs could have been disabled from the earlier conditions
826
+ # so we still need to realign inputs if that happens
827
+ maybe_realign_inputs(
828
+ cudagraphs,
829
+ self,
830
+ inputs_to_check,
831
+ self.mutated_input_idxs,
832
+ )
833
+
834
+ # Apply inductor_compiled_code HOP wrapper if configured
835
+ # This is done in post_compile to ensure it works with cached artifacts
836
+ if self._wrap_compiled_regions and self.current_callable is not None:
837
+ from torch._higher_order_ops.wrap import InductorCompiledCallable
838
+
839
+ original_callable = self.current_callable
840
+
841
+ inductor_callable = InductorCompiledCallable(
842
+ original_callable,
843
+ self._original_gm,
844
+ compile_region_name=self.compile_region_name,
845
+ )
846
+
847
+ def wrapped_callable(inputs):
848
+ if is_in_torch_dispatch_mode():
849
+ kwargs = (
850
+ {"name": self.compile_region_name}
851
+ if self.compile_region_name is not None
852
+ else {}
853
+ )
854
+ return inductor_compiled_code(inductor_callable, inputs, **kwargs)
855
+ else:
856
+ return original_callable(inputs)
857
+
858
+ self.current_callable = wrapped_callable
859
+
860
+ def set_triton_bundle(self, triton_bundle: Any) -> None:
861
+ self._triton_bundle = triton_bundle
862
+
863
+ def prepare_for_serialization(self) -> None:
864
+ # We can't really serialize callables that may be C++/Triton/etc.,
865
+ # so we serialize their PyCodeCache disk cache location instead.
866
+ # TODO: This could be better if we're ever able to serialize compiled
867
+ # models to disk.
868
+ self.current_callable = None
869
+ self.recursively_apply_fns = None
870
+ self.compiled_fn_runner = None
871
+ # Note: _original_gm is already picklable (metadata stripped at creation)
872
+ # Note: _serialized_fx_graph is already in serializable form (SerializedGraphModule)
873
+ # so it doesn't need to be cleared
874
+
875
+ def write_to_disk(self) -> str:
876
+ from torch._dynamo.utils import counters
877
+ from torch._inductor.codecache import get_path, write_atomic
878
+
879
+ # See _save_graph(); we don't store the callable in the cache entry so
880
+ # recreate it here from the PyCodeCache disk cache.
881
+ artifact_path = get_path(self.cache_key, "py")[2]
882
+ code = self.source_code
883
+ if not os.path.exists(artifact_path):
884
+ counters["inductor"]["fxgraph_lookup_write_file"] += 1
885
+ write_atomic(artifact_path, code, make_dirs=True)
886
+ return artifact_path
887
+
888
+ def after_deserialization(self, constants: CompiledFxGraphConstants) -> str:
889
+ from torch._dynamo.utils import dynamo_timed
890
+ from torch._inductor.codecache import PyCodeCache
891
+
892
+ artifact_path = self.write_to_disk()
893
+
894
+ try:
895
+ with dynamo_timed(
896
+ "PyCodeCache.load_by_key_path",
897
+ log_pt2_compile_event=True,
898
+ ):
899
+ code_cache = PyCodeCache.load_by_key_path(
900
+ self.cache_key,
901
+ artifact_path,
902
+ self.cache_linemap,
903
+ constants.unwrap(self),
904
+ )
905
+ self.current_callable = code_cache.call
906
+ self.recursively_apply_fns = getattr(
907
+ code_cache, "recursively_apply_fns", None
908
+ )
909
+ self.compiled_fn_runner = getattr(code_cache, "runner", None)
910
+ except OSError:
911
+ log.error("Failed to load artifact: %s", artifact_path)
912
+ raise
913
+
914
+ return artifact_path
915
+
916
+
917
+ @dataclasses.dataclass
918
+ class CompiledAOTI(OutputCode):
919
+ """
920
+ Class holding an AOTInductor compiled so.
921
+ """
922
+
923
+ filename: str | list[str | Weights] | torch.fx.GraphModule
924
+ device_type: str
925
+ current_callable: Callable[..., Any] | None = None
926
+ _cached_files: dict[str, bytes] = dataclasses.field(default_factory=dict)
927
+
928
+ def __post_init__(self):
929
+ if not config.aot_inductor.link_libtorch:
930
+ return
931
+
932
+ if (
933
+ torch._inductor.cpp_builder._IS_MACOS
934
+ or torch._inductor.cpp_builder._IS_WINDOWS
935
+ ):
936
+ return
937
+
938
+ if config.aot_inductor.cross_target_platform == "windows":
939
+ return
940
+
941
+ if config.aot_inductor.package_cpp_only:
942
+ return
943
+
944
+ if not config.enable_autograd_for_aot:
945
+ return
946
+
947
+ if isinstance(self.filename, list):
948
+ current_callable = next(
949
+ fn for fn in self.filename if isinstance(fn, str) and fn.endswith(".so")
950
+ )
951
+ else:
952
+ current_callable = self.filename
953
+
954
+ if isinstance(current_callable, torch.fx.GraphModule):
955
+ # pyrefly: ignore [bad-assignment]
956
+ self.current_callable = current_callable
957
+ return
958
+
959
+ if self.device_type.startswith("cuda"):
960
+ current_callable = (
961
+ torch._C._aoti.AOTIModelContainerRunnerCuda( # type: ignore[call-arg]
962
+ current_callable,
963
+ 1,
964
+ self.device_type,
965
+ "",
966
+ True,
967
+ ).run # type: ignore[attr-defined]
968
+ ) # type: ignore[attr-defined]
969
+ elif self.device_type.startswith("xpu"):
970
+ current_callable = (
971
+ torch._C._aoti.AOTIModelContainerRunnerXpu( # type: ignore[call-arg]
972
+ current_callable,
973
+ 1,
974
+ self.device_type,
975
+ "",
976
+ ).run # type: ignore[attr-defined]
977
+ ) # type: ignore[attr-defined]
978
+ elif self.device_type == "cpu":
979
+ current_callable = (
980
+ torch._C._aoti.AOTIModelContainerRunnerCpu( # type: ignore[call-arg]
981
+ current_callable, 1
982
+ ).run # type: ignore[attr-defined]
983
+ ) # type: ignore[attr-defined]
984
+ else:
985
+ raise RuntimeError(f"unsupported device type {self.device_type}")
986
+ self.current_callable = current_callable
987
+ self._boxed_call = True
988
+ for file in self._cached_files:
989
+ if not os.path.exists(file):
990
+ with open(file, "wb") as f:
991
+ f.write(self._cached_files[file])
992
+
993
+ def __call__(self, inputs: Sequence[Any]) -> Any:
994
+ if self.current_callable is None:
995
+ raise RuntimeError("AOTInductor compiled so is not loaded")
996
+ return self.current_callable(inputs)
997
+
998
+ def prepare_for_serialization(self) -> None:
999
+ self.current_callable = None
1000
+ self._cached_files = {}
1001
+ filenames: list[str] = []
1002
+ if isinstance(self.filename, list):
1003
+ filenames = self.filename # type: ignore[assignment]
1004
+ elif isinstance(self.filename, str):
1005
+ filenames = [self.filename]
1006
+ for name in filenames:
1007
+ with open(name, "rb") as f:
1008
+ self._cached_files[name] = f.read()
1009
+
1010
+ def __getstate__(self):
1011
+ state = self.__dict__.copy()
1012
+ state["current_callable"] = None
1013
+ return state
1014
+
1015
+ def post_compile(
1016
+ self,
1017
+ example_inputs: Sequence[InputType],
1018
+ constants: CompiledFxGraphConstants,
1019
+ graph_kwargs: _CompileFxKwargs,
1020
+ ) -> None:
1021
+ if self.current_callable is None:
1022
+ self.__post_init__()
1023
+
1024
+ def set_triton_bundle(self, triton_bundle: Any) -> None:
1025
+ pass
1026
+
1027
+
1028
+ @dataclasses.dataclass
1029
+ class MockFXGraphCacheOutput(OutputCode):
1030
+ gm: Any = None
1031
+
1032
+ def __post_init__(self) -> None:
1033
+ self._boxed_call = True
1034
+
1035
+ def post_compile(
1036
+ self,
1037
+ example_inputs: Sequence[InputType],
1038
+ constants: CompiledFxGraphConstants,
1039
+ graph_kwargs: _CompileFxKwargs,
1040
+ ) -> None:
1041
+ pass
1042
+
1043
+ def __call__(self, inputs: Sequence[Any]) -> Any:
1044
+ return self.gm(inputs)
1045
+
1046
+ def set_triton_bundle(self, triton_bundle: Any) -> None:
1047
+ pass
1048
+
1049
+
1050
+ @dataclasses.dataclass
1051
+ class RegionalOutputCode(OutputCode):
1052
+ """
1053
+ OutputCode for regional inductor compilation results.
1054
+
1055
+ Regional inductor returns a torch.fx.GraphModule that contains both
1056
+ compiled regions (via standalone_compile) and eager regions. This needs
1057
+ special serialization using GraphPickler instead of standard pickle.
1058
+
1059
+ The serialization strategy stores the GraphModule as bytes using
1060
+ GraphPickler.dumps(), which handles FakeTensors, AOTCompiledArtifacts,
1061
+ and other special objects that standard pickle cannot handle.
1062
+ """
1063
+
1064
+ # The serialized graph module as bytes (using GraphPickler)
1065
+ _serialized_graph_module: bytes | None = None
1066
+
1067
+ # The actual graph module (cleared during serialization)
1068
+ _graph_module: torch.nn.Module | None = None
1069
+
1070
+ # Optional filter for ops during serialization
1071
+ _ops_filter: Callable[[str], bool] | None = None
1072
+
1073
+ def __init__(
1074
+ self,
1075
+ graph_module: torch.fx.GraphModule,
1076
+ ops_filter: Callable[[str], bool] = _ops_filter_safe,
1077
+ node_metadata_key_filter: Callable[[str], bool] | None = (
1078
+ _node_metadata_key_filter_safe
1079
+ ),
1080
+ ):
1081
+ """
1082
+ Args:
1083
+ graph_module: The torch.fx.GraphModule returned by regional_inductor
1084
+ ops_filter: Optional filter function for op names during serialization.
1085
+ If provided, only ops whose name passes the filter will be serialized.
1086
+ """
1087
+ super().__init__()
1088
+ self._graph_module = graph_module
1089
+ self._serialized_graph_module = None
1090
+ self._serialized_wrappers = []
1091
+ self._boxed_call = True
1092
+ _, module = self._unwrap_graph_module()
1093
+ self._inner_boxed_call = isinstance(
1094
+ module.graph._codegen, torch.fx.graph._BoxedCodeGen
1095
+ )
1096
+ self._ops_filter = ops_filter
1097
+ self._node_metadata_key_filter = node_metadata_key_filter
1098
+
1099
+ def __call__(self, inputs: list[Any]) -> Any:
1100
+ """
1101
+ Execute the regional compiled graph.
1102
+
1103
+ Args:
1104
+ inputs: A mutable list of inputs. Must be a list (not an arbitrary
1105
+ Sequence) because the boxed calling convention allows the callee
1106
+ to mutably clear the list when inputs become dead, enabling early
1107
+ memory deallocation.
1108
+ """
1109
+ if self._graph_module is None:
1110
+ raise RuntimeError(
1111
+ "RegionalOutputCode has no graph module loaded. "
1112
+ "Did you forget to call post_compile()?"
1113
+ )
1114
+
1115
+ if self._inner_boxed_call:
1116
+ return self._graph_module(inputs)
1117
+ return self._graph_module(*inputs)
1118
+
1119
+ @property
1120
+ def graph(self):
1121
+ _, module = self._unwrap_graph_module()
1122
+ return module.graph
1123
+
1124
+ def _unwrap_graph_module(
1125
+ self,
1126
+ ) -> tuple[list[tuple[Callable, dict[str, Any]]], torch.fx.GraphModule]:
1127
+ module = self._graph_module
1128
+ serialized_wrappers = []
1129
+ if isinstance(module, torch._dynamo.OptimizedModule):
1130
+ dynamo_ctx = module.dynamo_ctx
1131
+ assert isinstance(dynamo_ctx, torch._dynamo.eval_frame.DisableContext)
1132
+ serialized_wrappers.append(
1133
+ (
1134
+ torch._dynamo.disable,
1135
+ {"reason": dynamo_ctx.msg, "wrapping": dynamo_ctx.wrapping},
1136
+ )
1137
+ )
1138
+ module = module._orig_mod
1139
+ assert isinstance(module, torch.fx.GraphModule)
1140
+ return serialized_wrappers, module
1141
+
1142
+ def post_compile(
1143
+ self,
1144
+ example_inputs: Sequence[InputType],
1145
+ constants: CompiledFxGraphConstants,
1146
+ graph_kwargs: _CompileFxKwargs,
1147
+ ) -> None:
1148
+ """
1149
+ Post-compile processing for regional inductor.
1150
+
1151
+ This deserializes the GraphModule from bytes using GraphPickler,
1152
+ extracting the fake_mode from example_inputs.
1153
+ """
1154
+ if self._graph_module is not None:
1155
+ return
1156
+ assert self._serialized_graph_module is not None
1157
+ # Get fake mode from example inputs
1158
+ from torch._guards import detect_fake_mode
1159
+
1160
+ fake_mode = detect_fake_mode(example_inputs)
1161
+ if fake_mode is None:
1162
+ raise RuntimeError(
1163
+ "Could not detect fake mode from example inputs. "
1164
+ "Regional inductor requires fake mode for deserialization."
1165
+ )
1166
+
1167
+ # Deserialize the graph module
1168
+ from torch.fx._graph_pickler import GraphPickler
1169
+
1170
+ gm = GraphPickler.loads(self._serialized_graph_module, fake_mode)
1171
+ assert isinstance(gm, torch.fx.GraphModule)
1172
+ gm.recompile()
1173
+ for fn, kwargs in reversed(self._serialized_wrappers):
1174
+ gm = fn(gm, **kwargs)
1175
+ self._graph_module = gm
1176
+
1177
+ def set_triton_bundle(self, triton_bundle: Any) -> None:
1178
+ """Regional inductor doesn't use triton bundles directly."""
1179
+
1180
+ def prepare_for_serialization(self) -> None:
1181
+ """
1182
+ Prepare for serialization by converting the GraphModule to bytes.
1183
+
1184
+ This uses GraphPickler to serialize the graph module since it contains
1185
+ special objects like FakeTensors and AOTCompiledArtifacts that need
1186
+ custom pickling.
1187
+ """
1188
+ if self._graph_module is not None:
1189
+ from torch.fx._graph_pickler import GraphPickler, Options
1190
+
1191
+ self._serialized_graph_module = None
1192
+ self._serialized_wrappers, graph_module = self._unwrap_graph_module()
1193
+
1194
+ self._serialized_graph_module = GraphPickler.dumps(
1195
+ graph_module,
1196
+ options=Options(
1197
+ ops_filter=self._ops_filter,
1198
+ node_metadata_key_filter=self._node_metadata_key_filter,
1199
+ ),
1200
+ )
1201
+ # Clear the graph module to avoid pickling it with standard pickle
1202
+ self._graph_module = None
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .package import AOTICompiledModel, load_package, package_aoti
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/build_package.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ build_package_contents = """
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from torch._inductor.package.package import compile_so
6
+
7
+ curr_dir = Path(__file__).parent
8
+ aoti_files = [
9
+ os.path.join(root, file)
10
+ for root, dirs, files in os.walk(curr_dir)
11
+ for file in files
12
+ ]
13
+
14
+ output_so = compile_so(curr_dir, aoti_files, curr_dir)
15
+ """
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/package/package.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import json
3
+ import logging
4
+ import os
5
+ import tempfile
6
+ from typing import IO
7
+
8
+ import torch
9
+ from torch._inductor import config
10
+ from torch._inductor.cpp_builder import BuildOptionsBase, CppBuilder
11
+ from torch.export.pt2_archive._package import (
12
+ AOTI_FILES,
13
+ AOTICompiledModel,
14
+ load_pt2,
15
+ package_pt2,
16
+ )
17
+ from torch.types import FileLike
18
+
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+
23
+ def compile_so(aoti_dir: str, aoti_files: list[str], so_path: str) -> str:
24
+ def get_aoti_file_with_suffix(suffix: str) -> str:
25
+ for file in aoti_files:
26
+ if file.endswith(suffix):
27
+ return file
28
+ raise RuntimeError(f"Unable to find file with suffix {suffix}")
29
+
30
+ # Compile all the files into a .so
31
+ cpp_file = os.path.join(aoti_dir, get_aoti_file_with_suffix(".cpp"))
32
+ consts_o = os.path.join(aoti_dir, get_aoti_file_with_suffix(".o"))
33
+
34
+ file_name = os.path.splitext(cpp_file)[0]
35
+
36
+ # Parse compile flags and build the .o file
37
+ with open(file_name + "_compile_flags.json") as f:
38
+ compile_flags = json.load(f)
39
+
40
+ compile_options = BuildOptionsBase(
41
+ **compile_flags, use_relative_path=config.is_fbcode()
42
+ )
43
+ object_builder = CppBuilder(
44
+ name=file_name,
45
+ sources=cpp_file,
46
+ BuildOption=compile_options,
47
+ )
48
+ output_o = object_builder.get_target_file_path()
49
+ object_builder.build()
50
+
51
+ # Parse linker flags and build the .so file
52
+ with open(file_name + "_linker_flags.json") as f:
53
+ linker_flags = json.load(f)
54
+
55
+ linker_options = BuildOptionsBase(
56
+ **linker_flags, use_relative_path=config.is_fbcode()
57
+ )
58
+ so_builder = CppBuilder(
59
+ name=os.path.split(so_path)[-1],
60
+ sources=[output_o, consts_o],
61
+ BuildOption=linker_options,
62
+ output_dir=so_path,
63
+ )
64
+ output_so = so_builder.get_target_file_path()
65
+ so_builder.build()
66
+
67
+ # mmapped weights
68
+ serialized_weights_filename = file_name + "_serialized_weights.bin"
69
+ if serialized_weights_filename in aoti_files:
70
+ with open(serialized_weights_filename, "rb") as f_weights:
71
+ serialized_weights = f_weights.read()
72
+
73
+ with open(output_so, "a+b") as f_so:
74
+ so_size = f_so.tell()
75
+ # Page align the weights
76
+ f_so.write(b" " * (16384 - so_size % 16384))
77
+ f_so.write(serialized_weights)
78
+
79
+ return output_so
80
+
81
+
82
+ def package_aoti(
83
+ archive_file: FileLike,
84
+ aoti_files: AOTI_FILES,
85
+ ) -> FileLike:
86
+ """
87
+ Saves the AOTInductor generated files to the PT2Archive format.
88
+
89
+ Args:
90
+ archive_file: The file name to save the package to.
91
+ aoti_files: This can either be a singular path to a directory containing
92
+ the AOTInductor files, or a dictionary mapping the model name to the
93
+ path to its AOTInductor generated files.
94
+ """
95
+
96
+ return package_pt2(
97
+ archive_file,
98
+ aoti_files=aoti_files,
99
+ )
100
+
101
+
102
+ def load_package(
103
+ path: FileLike,
104
+ model_name: str = "model",
105
+ run_single_threaded: bool = False,
106
+ num_runners: int = 1,
107
+ device_index: int = -1,
108
+ ) -> AOTICompiledModel:
109
+ try:
110
+ pt2_contents = load_pt2(
111
+ path,
112
+ run_single_threaded=run_single_threaded,
113
+ num_runners=num_runners,
114
+ device_index=device_index,
115
+ )
116
+ if model_name not in pt2_contents.aoti_runners:
117
+ raise RuntimeError(f"Model {model_name} not found in package")
118
+ return pt2_contents.aoti_runners[model_name]
119
+ except RuntimeError:
120
+ log.warning("Loading outdated pt2 file. Please regenerate your package.")
121
+
122
+ if isinstance(path, (io.IOBase, IO)):
123
+ with tempfile.NamedTemporaryFile(suffix=".pt2") as f:
124
+ # TODO(angelayi): We shouldn't need to do this -- miniz should
125
+ # handle reading the buffer. This is just a temporary workaround
126
+ path.seek(0)
127
+ f.write(path.read())
128
+ log.debug("Writing buffer to tmp file located at %s.", f.name)
129
+ loader = torch._C._aoti.AOTIModelPackageLoader(
130
+ f.name, model_name, run_single_threaded, num_runners, device_index
131
+ )
132
+ return AOTICompiledModel(loader)
133
+
134
+ path = os.fspath(path) # AOTIModelPackageLoader expects (str, str)
135
+ loader = torch._C._aoti.AOTIModelPackageLoader(
136
+ path, model_name, run_single_threaded, num_runners, device_index
137
+ )
138
+ return AOTICompiledModel(loader)
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/pattern_matcher.py ADDED
@@ -0,0 +1,2495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ # Inductor Pattern Matcher
3
+
4
+ The pattern matcher enables search/replace within an FX graph.
5
+
6
+ The main entrypoint to the pattern matcher is register_replacement(). Given a
7
+ search function and a replacement function this will register a replacement with
8
+ a pass (such as torch._inductor.fx_passes.joint_graph.patterns).
9
+
10
+ Internally the pattern matcher represents patterns as a graph (a DAG). Creating
11
+ new patterns manually as a graph is cumbersome and error-prone so the standard
12
+ way to create patterns (using register_replacement()) is to provide a search
13
+ function and a replacement function which is traced and converted into a graph.
14
+
15
+ Because the search functions are built somewhat generic (they tend to ignore
16
+ tensor sizes, for example) register_replacement() allows you to specify an
17
+ `extra_check` function which performs additional checks to verify that the
18
+ matched pattern fully matches before returning it.
19
+
20
+ ## Precompiled Patterns
21
+
22
+ New patterns are added using register_replacement(). Patterns added in this way
23
+ can have a compile-time overhead because they need to be traced before
24
+ use. Patterns can be precompiled and added using gen_register_replacement()
25
+ instead. To do this you call gen_register_replacement() instead of
26
+ register_replacement(). The arguments are the same except for an additional
27
+ unique name which is used as a lookup key.
28
+
29
+ ## Internals
30
+
31
+ The match DAG is represented by a graph of `PatternExpr` nodes. Each PatternExpr
32
+ implements a `_match` method which returns either a `Match` object for a
33
+ successful match or a `FailedMatch` object for a failure to match.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import contextlib
39
+ import dataclasses
40
+ import functools
41
+ import importlib
42
+ import inspect
43
+ import itertools
44
+ import logging
45
+ import operator
46
+ import os
47
+ import re
48
+ import textwrap
49
+ import typing
50
+ from abc import ABC, abstractmethod
51
+ from collections import defaultdict
52
+ from collections.abc import Callable, Collection, Generator, Iterable, Mapping, Sequence
53
+ from pathlib import Path
54
+ from typing import Any, NoReturn, Protocol, TypeVar
55
+ from typing_extensions import Self, TypeIs
56
+
57
+ import torch
58
+ import torch._guards
59
+ import torch.fx
60
+ import torch.utils._pytree as pytree
61
+ from torch._dispatch.python import enable_python_dispatcher
62
+ from torch._dynamo.utils import counters
63
+ from torch._prims_common import is_integer_dtype
64
+ from torch._subclasses.fake_tensor import unset_fake_temporarily
65
+ from torch.fx.experimental.proxy_tensor import make_fx
66
+ from torch.fx.experimental.symbolic_shapes import guard_or_false, statically_known_true
67
+ from torch.fx.graph_module import _get_attr
68
+ from torch.fx.immutable_collections import immutable_dict, immutable_list
69
+ from torch.fx.passes.graph_transform_observer import GraphTransformObserver
70
+ from torch.fx.traceback import preserve_node_meta
71
+ from torch.utils._ordered_set import OrderedSet
72
+
73
+ from .._functorch import config as functorch_config
74
+ from .._functorch.aot_autograd import aot_function, make_boxed_func
75
+ from .._functorch.partitioners import default_partition
76
+ from .._subclasses import FakeTensor, FakeTensorMode
77
+ from ..fx import Transformer
78
+ from . import config
79
+ from .decomposition import select_decomp_table
80
+ from .lowering import fallback_node_due_to_unsupported_type
81
+
82
+
83
+ log = logging.getLogger(__name__)
84
+ aten = torch.ops.aten
85
+ prims = torch.ops.prims
86
+
87
+ Constant = Any
88
+ NodeOrConstant = Constant | torch.fx.Node
89
+
90
+ backend = os.environ.get("TORCHINDUCTOR_PATTERN_MATCH_BACKEND", "inductor")
91
+
92
+
93
+ _debug_nodes_cache: bool | OrderedSet[str] | None = None
94
+ _debug_nodes_env_value_cache: str | None = None
95
+
96
+
97
+ def _should_debug_node(node_name: str) -> bool:
98
+ def _get_debug_nodes() -> bool | OrderedSet[str]:
99
+ global _debug_nodes_cache, _debug_nodes_env_value_cache
100
+
101
+ def parse_debug_env(env_value: str | None) -> bool | OrderedSet[str]:
102
+ if not env_value:
103
+ return False
104
+ if env_value == "all":
105
+ return True
106
+ return OrderedSet(env_value.split(","))
107
+
108
+ current_env = os.environ.get("TORCHINDUCTOR_PATTERN_MATCH_DEBUG")
109
+
110
+ # Recompute only if env changed
111
+ if current_env != _debug_nodes_env_value_cache or _debug_nodes_cache is None:
112
+ _debug_nodes_cache = parse_debug_env(current_env)
113
+ _debug_nodes_env_value_cache = current_env
114
+
115
+ return _debug_nodes_cache
116
+
117
+ debug_nodes = _get_debug_nodes()
118
+ if isinstance(debug_nodes, bool):
119
+ return debug_nodes
120
+ return node_name in debug_nodes
121
+
122
+
123
+ class SearchFn(Protocol):
124
+ __name__: str
125
+
126
+ def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
127
+
128
+
129
+ class ReplaceFn(Protocol):
130
+ def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
131
+
132
+
133
+ class TraceFn(Protocol):
134
+ def __call__(
135
+ self, fn: SearchFn | ReplaceFn, *args: Any, **kwargs: Any
136
+ ) -> torch.fx.GraphModule: ...
137
+
138
+
139
+ T = TypeVar("T")
140
+
141
+ # What's a better name for this?
142
+ FnsType = torch.fx.node.Target | str
143
+
144
+
145
+ class Multiple:
146
+ def __init__(self) -> None:
147
+ # Ensure we're really a singleton.
148
+ assert "MULTIPLE" not in globals() or self is MULTIPLE
149
+
150
+
151
+ # Sentinel indicating multiple quantities can be matched
152
+ MULTIPLE = Multiple()
153
+
154
+
155
+ def _transfer_meta(
156
+ new_meta: dict[str, Any], old_node: torch.fx.Node, pass_name: str = ""
157
+ ) -> None:
158
+ from torch.fx.traceback import NodeSource, NodeSourceAction
159
+
160
+ # Transfer metadata after pattern matching occurs.
161
+ # Copies _COPY_META_FIELDS, stack_trace, and (if missing) val/tensor_meta.
162
+ if config.trace.provenance_tracking_level == 1:
163
+ new_from_node = new_meta.get("from_node", []).copy()
164
+ new_from_node.append(NodeSource(old_node, pass_name, NodeSourceAction.REPLACE))
165
+ new_meta.update(
166
+ (k, v)
167
+ for k, v in old_node.meta.items()
168
+ if k in torch.fx.proxy._COPY_META_FIELDS
169
+ )
170
+ new_meta["from_node"] = new_from_node
171
+ else:
172
+ new_meta.update(
173
+ (k, v)
174
+ for k, v in old_node.meta.items()
175
+ if k in torch.fx.proxy._COPY_META_FIELDS
176
+ )
177
+ if "stack_trace" in old_node.meta:
178
+ new_meta["stack_trace"] = old_node.meta["stack_trace"]
179
+ # Copy val/tensor_meta only when the new node doesn't already have them
180
+ # (e.g. from tracing the replacement graph). Don't overwrite if present
181
+ # since the replacement's own val is more accurate.
182
+ if "val" not in new_meta and "val" in old_node.meta:
183
+ new_meta["val"] = old_node.meta["val"]
184
+ if "tensor_meta" not in new_meta and "tensor_meta" in old_node.meta:
185
+ new_meta["tensor_meta"] = old_node.meta["tensor_meta"]
186
+
187
+
188
+ class Match:
189
+ """
190
+ Represents a successfully matched pattern.
191
+
192
+ The `Match` object is returned to represent a successfully matched
193
+ pattern. Included in the Match are the pattern that was matched, the graph
194
+ nodes matched, and any args that were used during the matching.
195
+
196
+ The args and kwargs are specific to the type of pattern that was matched and
197
+ provide hints about what was matched.
198
+ """
199
+
200
+ pattern: PatternExpr
201
+ args: list[Any]
202
+ kwargs: dict[str, Any]
203
+ nodes: list[torch.fx.Node]
204
+ targets: dict[_TargetExpr, torch.fx.node.Target]
205
+ ctx: MatchContext
206
+ replacement_graph: torch.fx.GraphModule | None
207
+
208
+ def __init__(
209
+ self,
210
+ ctx: MatchContext,
211
+ pattern: PatternExpr,
212
+ args: Sequence[Any] | None = None,
213
+ kwargs: dict[str, Any] | None = None,
214
+ ) -> None:
215
+ super().__init__()
216
+ self.pattern = pattern
217
+ # The input nodes that must be passed in to the result
218
+ self.args = list(args or [])
219
+ self.kwargs = kwargs or {}
220
+ # The nodes matched in this expression
221
+ self.nodes = []
222
+ # Mapping CallFunction to the node.target
223
+ self.targets = {}
224
+ self.ctx = ctx
225
+ self.replacement_graph = None
226
+
227
+ @property
228
+ def graph(self) -> torch.fx.Graph:
229
+ return self.ctx.graph
230
+
231
+ def extend(self, other: Match) -> None:
232
+ if self.kwargs:
233
+ for key in OrderedSet(self.kwargs.keys()) & OrderedSet(other.kwargs.keys()):
234
+ if self.kwargs[key] != other.kwargs[key]:
235
+ raise FailedMatch("kwarg mismatch: {}", key)
236
+ self.args.extend(other.args)
237
+ self.nodes.extend(other.nodes)
238
+ self.kwargs.update(other.kwargs)
239
+ self.targets.update(other.targets)
240
+
241
+ def bundle(self) -> Match:
242
+ # Wrap args in an extra list
243
+ self.args = [tuple(self.args)] if self.args else []
244
+ return self
245
+
246
+ def __repr__(self) -> str:
247
+ return f"Match(..., {self.args}, {self.kwargs})"
248
+
249
+ def erase_nodes(self) -> None:
250
+ graph = self.graph
251
+ for n in reversed(self.nodes):
252
+ if not n._erased and not n.users:
253
+ graph.erase_node(n)
254
+
255
+ def output_nodes(self) -> list[torch.fx.Node | None]:
256
+ return [
257
+ (self.ctx.pattern_to_node[p] if p is not None else None)
258
+ for p in self.ctx.outputs
259
+ ]
260
+
261
+ def output_node(self) -> torch.fx.Node:
262
+ return next(p for p in self.output_nodes() if p)
263
+
264
+ def replace_with_graph(
265
+ self, replacement_graph: torch.fx.Graph, args: Sequence[Any]
266
+ ) -> None:
267
+ ReplacementPatternEntry.replace_with_graph(
268
+ self, self.ctx.graph, replacement_graph, args
269
+ )
270
+
271
+ def replace_by_example(
272
+ self,
273
+ replacement_fn: ReplaceFn,
274
+ args: Sequence[Any],
275
+ trace_fn: TraceFn | None = None,
276
+ run_functional_passes: bool = True,
277
+ ) -> None:
278
+ """Replace with a graph generated by tracing the replacement_fn.
279
+
280
+ Args:
281
+ run_functional_passes (bool). If we should run passes that
282
+ assume functional IR (like DCE, remove_noop_ops), on the
283
+ replacement graph.
284
+
285
+ """
286
+ from torch._inductor.virtualized import NullHandler, V
287
+
288
+ context = (
289
+ V.fake_mode
290
+ if (not isinstance(V.fake_mode, NullHandler) or (V.fake_mode is None))
291
+ else contextlib.nullcontext()
292
+ )
293
+
294
+ def should_propagate_eager_input_vals(nodes: list[torch.fx.Node]) -> bool:
295
+ if len(nodes) != 1:
296
+ return False
297
+ node = nodes[0]
298
+ if "eager_input_vals" not in node.meta:
299
+ return False
300
+ return node.target in OrderedSet(
301
+ [
302
+ torch.ops.higher_order.triton_kernel_wrapper_functional,
303
+ torch.ops.higher_order.auto_functionalized,
304
+ torch.ops.higher_order.auto_functionalized_v2,
305
+ ]
306
+ )
307
+
308
+ # pyrefly: ignore [bad-context-manager]
309
+ with context:
310
+ if trace_fn is None:
311
+ trace_fn = functools.partial(
312
+ fwd_only, run_functional_passes=run_functional_passes
313
+ )
314
+
315
+ if should_propagate_eager_input_vals(self.nodes):
316
+ # Our strategy is:
317
+ # 1) trace out the graph with eager_input_vals (which have accurate eager-mode metadata)
318
+ # 2) trace out the graph with vals (which have the accurate Inductor metadata)
319
+ # 3) Propagate the eager_input_vals from the first graph to the second.
320
+ # 4) Use the second graph as the replacement graph.
321
+
322
+ # Construct a map of node -> FakeTensor val in eager_input_vals
323
+ node_to_val = {}
324
+
325
+ fake_args, fake_kwargs = self.nodes[0].meta["eager_input_vals"]
326
+ fake_kwargs = {**fake_kwargs}
327
+ match_args, match_kwargs = tuple(self.args), self.kwargs
328
+
329
+ def record(node: torch.fx.Node, val: Any) -> None:
330
+ if isinstance(node, torch.fx.Node):
331
+ node_to_val[node] = val
332
+
333
+ torch.utils._pytree.tree_map(
334
+ record, (match_args, match_kwargs), (fake_args, fake_kwargs)
335
+ )
336
+ # map args to their FakeTensor val in eager_input_vals
337
+ example_vals = torch.fx.map_arg(args, lambda arg: node_to_val[arg])
338
+
339
+ # first graph
340
+ graph_with_eager_vals = trace_fn(replacement_fn, example_vals)
341
+
342
+ # second graph
343
+ example_vals = torch.fx.map_arg(args, lambda arg: arg.meta["val"])
344
+ # pyrefly: ignore [bad-argument-type]
345
+ replacement = trace_fn(graph_with_eager_vals, example_vals)
346
+
347
+ # propagate metadata from first graph to second
348
+ # NB: This assertion might not be true in general, but it is true for
349
+ # the two use cases we have
350
+ # (triton_kernel_wrapper_functional, auto_functionalized)
351
+ assert len(graph_with_eager_vals.graph.nodes) == len(
352
+ replacement.graph.nodes
353
+ )
354
+ for old_node, new_node in zip(
355
+ graph_with_eager_vals.graph.nodes, replacement.graph.nodes
356
+ ):
357
+ if "eager_input_vals" in old_node.meta:
358
+ new_node.meta["eager_input_vals"] = old_node.meta[
359
+ "eager_input_vals"
360
+ ]
361
+
362
+ else:
363
+ example_vals = torch.fx.map_arg(
364
+ args,
365
+ lambda arg: arg.meta["val"]
366
+ if "val" in arg.meta
367
+ else arg.meta["example_value"],
368
+ )
369
+ fake_mode = torch._dynamo.utils.detect_fake_mode(example_vals)
370
+ if fake_mode is not None:
371
+
372
+ def _convert_to_fake_mode(it):
373
+ if isinstance(it, FakeTensor) and fake_mode is not None:
374
+ return fake_mode.from_tensor(it)
375
+ return it
376
+
377
+ example_vals = torch.fx.node.map_aggregate(
378
+ example_vals, _convert_to_fake_mode
379
+ )
380
+ replacement = trace_fn(replacement_fn, example_vals)
381
+ if len(self.nodes) == 1:
382
+ for n in replacement.graph.nodes:
383
+ _transfer_meta(
384
+ new_meta=n.meta,
385
+ old_node=self.nodes[0],
386
+ pass_name="replace_by_example",
387
+ )
388
+
389
+ ReplacementPatternEntry.replace_with_graph(
390
+ self,
391
+ self.ctx.graph,
392
+ replacement,
393
+ args,
394
+ )
395
+
396
+
397
+ class FailedMatch(RuntimeError):
398
+ """
399
+ Represents a unsuccessful match.
400
+
401
+ The `FailedMatch` object is returned to represent a failure to match a
402
+ pattern.
403
+ """
404
+
405
+ format_string: str
406
+
407
+ def __init__(self, format_string: str, *args: Any, **kwargs: Any) -> None:
408
+ self.format_string = format_string
409
+ # We want to construct error messages lazily instead of eagerly, as
410
+ # constructing them eagerly can significantly worsen compile times.
411
+ if len(format_string) > 200:
412
+ raise RuntimeError(
413
+ f"Format string too long - use lazy construction of strings instead. Format string is\n {format_string}"
414
+ )
415
+ self.args = args
416
+ self.kwargs = kwargs
417
+
418
+ def __str__(self) -> str:
419
+ return self.format_string.format(*self.args, **self.kwargs)
420
+
421
+ def __bool__(self) -> bool:
422
+ return False
423
+
424
+
425
+ MatchResult = Match | FailedMatch
426
+
427
+
428
+ def is_match(m: MatchResult) -> TypeIs[Match]:
429
+ """
430
+ TypeIs cannot act on `self`. Thus this function exists to let mypy
431
+ recognize FailedMatch.__bool__ as a TypeIs.
432
+ """
433
+ return bool(m)
434
+
435
+
436
+ class MatchContext:
437
+ """
438
+ Internal state needed while running PatternExpr._match().
439
+ """
440
+
441
+ outputs: list[PatternExpr | None]
442
+ pattern_to_node: dict[PatternExpr, torch.fx.Node | None]
443
+ graph: torch.fx.Graph
444
+ exclusive_node_set: list[NodeOrConstant]
445
+
446
+ def __init__(
447
+ self,
448
+ outputs: list[PatternExpr | None],
449
+ pattern_to_node: dict[PatternExpr, torch.fx.Node] | None = None,
450
+ *,
451
+ graph: torch.fx.Graph,
452
+ ) -> None:
453
+ self.outputs = outputs
454
+ self.pattern_to_node = {} if pattern_to_node is None else dict(pattern_to_node)
455
+ self.graph = graph
456
+ self.exclusive_node_set = []
457
+
458
+ def match(self, pattern: PatternExpr, node: NodeOrConstant) -> MatchResult:
459
+ """wrapper to check reused nodes in patterns"""
460
+ if pattern in self.pattern_to_node:
461
+ if self.pattern_to_node[pattern] == node:
462
+ return Match(self, pattern) # already checked this node
463
+ else:
464
+ return FailedMatch("repeated pattern differs")
465
+ m = pattern._match(node, self)
466
+ assert pattern not in self.pattern_to_node
467
+ self.pattern_to_node[pattern] = node if m else None
468
+ return m
469
+
470
+ def filter_multi_user_patterns(self) -> dict[PatternExpr, torch.fx.Node]:
471
+ return {
472
+ pattern: node
473
+ for pattern, node in self.pattern_to_node.items()
474
+ if pattern.has_multiple_users() and node is not None
475
+ }
476
+
477
+
478
+ class PatternExpr(ABC):
479
+ """
480
+ Base class for types of patterns.
481
+ """
482
+
483
+ @abstractmethod
484
+ def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult: ...
485
+
486
+ def match(self, node: torch.fx.Node) -> MatchResult:
487
+ try:
488
+ return MatchContext([self], graph=node.graph).match(self, node)
489
+ except FailedMatch as e:
490
+ return e
491
+
492
+ def has_multiple_users(self) -> bool:
493
+ return False
494
+
495
+ def __repr__(self) -> str:
496
+ return self.__class__.__name__ + "()"
497
+
498
+ def find_anchor_nodes(
499
+ self, ctx: MatchContext, searched: OrderedSet[torch.fx.Node]
500
+ ) -> Generator[torch.fx.Node | None, None, None]:
501
+ if self in ctx.pattern_to_node:
502
+ yield ctx.pattern_to_node[self]
503
+
504
+ def pattern_eq(self, other: Any) -> bool:
505
+ """
506
+ Compare two `PatternExpr`s and return true if they are the
507
+ same. Note this is NOT matching a pattern - it is comparing the pattern
508
+ structures (for debugging).
509
+ """
510
+ return isinstance(other, self.__class__)
511
+
512
+
513
+ class Arg(PatternExpr):
514
+ """
515
+ Capture an arg which will become an input to the handler. Args are
516
+ passed in depth first order.
517
+ """
518
+
519
+ def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
520
+ return Match(ctx, self, args=[node]) # matches anything
521
+
522
+
523
+ class Ignored(PatternExpr):
524
+ """
525
+ Match an arg, but don't pass it to handler
526
+ """
527
+
528
+ def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
529
+ return Match(ctx, self) # matches anything
530
+
531
+ def __repr__(self) -> str:
532
+ return "*"
533
+
534
+ def pretty_print(self, pp: PatternPrettyPrinter) -> str:
535
+ return "Ignored()"
536
+
537
+
538
+ class KeywordArg(PatternExpr):
539
+ """
540
+ Capture a kwarg which will become an input to the handler.
541
+ """
542
+
543
+ def __init__(self, name: str) -> None:
544
+ super().__init__()
545
+ self.name = name
546
+
547
+ def __repr__(self) -> str:
548
+ return f"KeywordArg({self.name!r})"
549
+
550
+ def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
551
+ return Match(ctx, self, kwargs={self.name: node}) # matches anything
552
+
553
+ def pattern_eq(self, other: Any) -> bool:
554
+ other = typing.cast(Self, other) # super makes sure this is true
555
+ return super().pattern_eq(other) and self.name == other.name
556
+
557
+
558
+ class ExclusiveKeywordArg(PatternExpr):
559
+ """
560
+ Capture a kwarg which will become an input to the handler.
561
+ """
562
+
563
+ name: str
564
+
565
+ def __init__(self, name: str) -> None:
566
+ super().__init__()
567
+ self.name = name
568
+
569
+ def __repr__(self) -> str:
570
+ return f"ExclusiveKeywordArg({self.name!r})"
571
+
572
+ def _match(self, node: NodeOrConstant, ctx: MatchContext) -> MatchResult:
573
+ if node in ctx.exclusive_node_set:
574
+ return FailedMatch("exclusive arg appears twice")
575
+
576
+ ctx.exclusive_node_set.append(node)
577
+ return Match(ctx, self, kwargs={self.name: node}) # matches anything
578
+
579
+ def pattern_eq(self, other: Any) -> bool:
580
+ other = typing.cast(Self, other) # super makes sure this is true
581
+ return super().pattern_eq(other) and self.name == other.name
582
+
583
+
584
+ class _TargetExpr(PatternExpr):
585
+ """
586
+ Base class for filtering match by node.target
587
+ """
588
+
589
+ fns: list[FnsType]
590
+ fns_set: OrderedSet[FnsType]
591
+
592
+ def __init__(
593
+ self, fns: FnsType | Sequence[FnsType], users: Multiple | int = 1
594
+ ) -> None:
595
+ super().__init__()
596
+ fns = [fns] if callable(fns) or isinstance(fns, str) else list(fns)
597
+ for fn in fns:
598
+ if isinstance(fn, torch._ops.OpOverloadPacket):
599
+ fns.extend(getattr(fn, overload) for overload in fn.overloads()) # noqa: B909
600
+
601
+ self.fns = fns
602
+ self.fns_set = OrderedSet(fns)
603
+ self.users = users
604
+
605
+ @property
606
+ @abstractmethod
607
+ def op(self) -> str: ...
608
+
609
+ def fns_repr(self) -> str:
610
+ first_repr = self.fns[0]
611
+ if not isinstance(first_repr, str):
612
+ first_repr = first_repr.__name__
613
+
614
+ if len(self.fns) > 1:
615
+ return f"[{first_repr}, ...]"
616
+ elif self.fns[0] is getattr(torch, first_repr, None):
617
+ return f"torch.{first_repr}"
618
+ elif self.fns[0] is getattr(operator, first_repr, None):
619
+ return f"operator.{first_repr}"
620
+ elif isinstance(self.fns[0], torch._ops.OpOverload):
621
+ return str(self.fns[0])
622
+ else:
623
+ return first_repr
624
+
625
+ def __repr__(self) -> str:
626
+ if self.users is MULTIPLE:
627
+ comma_users = ", MULTIPLE"
628
+ elif self.users != 1:
629
+ comma_users = f", {self.users})"
630
+ else:
631
+ comma_users = ""
632
+ return f"{self.__class__.__name__}({self.fns_repr()}{comma_users})"
633
+
634
+ def has_multiple_users(self) -> bool:
635
+ return isinstance(self.users, Multiple) or self.users > 1
636
+
637
+ def find_anchor_nodes(
638
+ self, ctx: MatchContext, searched: OrderedSet[torch.fx.Node]
639
+ ) -> Generator[torch.fx.Node | None, None, None]:
640
+ raise NotImplementedError
641
+
642
+ def _match_fns(self, node: torch.fx.Node) -> bool:
643
+ return (
644
+ isinstance(node, torch.fx.Node)
645
+ and node.op == self.op
646
+ and extract_target(node) in self.fns_set
647
+ )
648
+
649
+ def _match_users(self, node: torch.fx.Node, ctx: MatchContext) -> bool:
650
+ return (
651
+ self in ctx.outputs
652
+ or self.users is MULTIPLE
653
+ or len(node.users) == self.users
654
+ )
655
+
656
+ def pattern_eq(self, other: Any) -> bool:
657
+ other = typing.cast(Self, other) # super makes sure this is true
658
+ return (
659
+ super().pattern_eq(other)
660
+ and self.op == other.op
661
+ and self.fns == other.fns
662
+ and self.users == other.users
663
+ )
664
+
665
+
666
+ _SimpleSpec = tuple[Any, ...]
667
+
668
+
669
+ class _TargetArgsExpr(_TargetExpr):
670
+ """
671
+ Base class for filtering match by node.{target,args,kwargs}
672
+ """
673
+
674
+ def __init__(
675
+ self,
676
+ fns: torch.fx.node.Target | str | Sequence[Any],
677
+ *args: Any,
678
+ _users: int | Multiple = 1,
679
+ **kwargs: Any,
680
+ ) -> None:
681
+ super().__init__(fns, _users)
682
+ self.args = tuple(args)
683
+ self.kwargs = dict(kwargs)
684
+ if any(
685
+ isinstance(x, (dict, list, tuple))
686
+ for x in itertools.chain(args, kwargs.values())
687
+ ):
688
+ self.flatten = self.pytree_flatten
689
+ else:
690
+ self.flatten = self.simple_flatten
691
+ self.flat_args_kwargs = self.flatten(self.args, self.kwargs)
692
+
693
+ @staticmethod
694
+ def simple_flatten(
695
+ args: Sequence[Any], kwargs: Mapping[Any, Any]
696
+ ) -> tuple[Sequence[Any], _SimpleSpec | pytree.TreeSpec]:
697
+ values = (*args, *kwargs.values())
698
+ spec = (len(args), *kwargs.keys())
699
+ return values, spec
700
+
701
+ @staticmethod
702
+ def pytree_flatten(
703
+ args: Sequence[Any], kwargs: Mapping[Any, Any]
704
+ ) -> tuple[Sequence[Any], _SimpleSpec | pytree.TreeSpec]:
705
+ type_mapping: dict[type, type] = {
706
+ immutable_list: tuple,
707
+ list: tuple,
708
+ immutable_dict: dict,
709
+ }
710
+
711
+ def convert_type(x: Any) -> Any:
712
+ cls = type(x)
713
+ convert_fn = type_mapping.get(cls)
714
+ if convert_fn is not None:
715
+ return pytree.tree_map(
716
+ convert_type,
717
+ convert_fn(x),
718
+ is_leaf=lambda x: type(x) in type_mapping,
719
+ )
720
+ return x
721
+
722
+ normalized_args_tree = pytree.tree_map(
723
+ convert_type,
724
+ (args, kwargs),
725
+ is_leaf=lambda x: type(x) in type_mapping,
726
+ )
727
+ flat, spec = pytree.tree_flatten(normalized_args_tree)
728
+ return flat, spec
729
+
730
+ def __repr__(self) -> str:
731
+ args = [
732
+ self.fns_repr(),
733
+ *map(repr, self.args),
734
+ *[f"{k}={v}" for k, v in self.kwargs.items()],
735
+ ]
736
+ if self.users is MULTIPLE:
737
+ args.append("_users=MULTIPLE")
738
+ elif self.users != 1:
739
+ args.append(f"_users={self.users}")
740
+ return f"{self.__class__.__name__}({', '.join(args)})"
741
+
742
+ def pretty_print(self, pp: PatternPrettyPrinter) -> str:
743
+ args = [
744
+ self.fns_repr(),
745
+ *(pp.pretty_print(x) for x in self.args),
746
+ *[f"{k}={pp.pretty_print(v)}" for k, v in self.kwargs.items()],
747
+ ]
748
+ if self.users is MULTIPLE:
749
+ args.append("_users=MULTIPLE")
750
+ elif self.users != 1:
751
+ args.append(f"_users={self.users}")
752
+
753
+ joiner_str = ", "
754
+ return f"{self.__class__.__name__}({joiner_str.join(args)})"
755
+
756
+ def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
757
+ if not self._match_fns(node) or len(node.args) != len(self.args):
758
+ return FailedMatch("function_mismatch: node={}, pattern={}", node, self)
759
+
760
+ if not self._match_users(node, ctx):
761
+ return FailedMatch("multiple_users {}", self)
762
+
763
+ _args = node.args
764
+ _kwargs = node.kwargs
765
+ if len(_kwargs) < len(self.kwargs):
766
+ from torch.fx.operator_schemas import normalize_function
767
+
768
+ assert callable(node.target)
769
+ normalized_args_and_kwargs = normalize_function(
770
+ node.target, node.args, node.kwargs
771
+ )
772
+
773
+ if normalized_args_and_kwargs is None:
774
+ return FailedMatch("function_mismatch: node={}, pattern={}", node, self)
775
+ else:
776
+ _args, _kwargs = normalized_args_and_kwargs
777
+ if len(_args) == len(self.args) and len(_kwargs) >= len(self.kwargs):
778
+ _kwargs = {i: _kwargs[i] for i in _kwargs if i in self.kwargs}
779
+ else:
780
+ return FailedMatch(
781
+ "function_mismatch: node={}, pattern={}", node, self
782
+ )
783
+ else:
784
+ _kwargs = {i: _kwargs[i] for i in _kwargs if i in self.kwargs}
785
+
786
+ node_items, node_spec = self.flatten(_args, _kwargs)
787
+ self_items, self_spec = self.flat_args_kwargs
788
+ if node_spec != self_spec:
789
+ return FailedMatch("args_structure {} {}", node_spec, self_spec)
790
+ assert len(node_items) == len(self_items)
791
+
792
+ m = Match(ctx, self)
793
+ for pattern, child_node in zip(self_items, node_items):
794
+ if isinstance(pattern, PatternExpr):
795
+ child_match = ctx.match(pattern, child_node)
796
+ if not is_match(child_match):
797
+ return child_match
798
+ m.extend(child_match)
799
+ elif isinstance(child_node, torch.fx.Node) or child_node != pattern:
800
+ return FailedMatch(
801
+ "constant_args: {} {!r}!={pattern!r}",
802
+ node,
803
+ child_node,
804
+ pattern=pattern,
805
+ )
806
+ m.nodes.append(node)
807
+ m.targets[self] = node.target
808
+ return m
809
+
810
+ def find_anchor_nodes(
811
+ self, ctx: MatchContext, searched: OrderedSet[torch.fx.Node]
812
+ ) -> Generator[torch.fx.Node | None, None, None]:
813
+ """
814
+ This is used when we are matching a pattern with multiple outputs.
815
+ There is a partial match (stored in ctx) and we want to walk
816
+ this pattern to find a connection to an already-matched node.
817
+
818
+ Yields candidate nodes that `self._match` might like.
819
+ """
820
+ if self in ctx.pattern_to_node:
821
+ yield ctx.pattern_to_node[self]
822
+ return
823
+
824
+ for pattern in self.flat_args_kwargs[0]:
825
+ if isinstance(pattern, PatternExpr):
826
+ for other_node in pattern.find_anchor_nodes(ctx, searched):
827
+ if not isinstance(other_node, torch.fx.Node):
828
+ continue
829
+ for node in other_node.users:
830
+ if node not in searched:
831
+ if self._match_fns(node):
832
+ yield node
833
+ searched.add(node)
834
+
835
+ def pattern_eq(self, other: Any) -> bool:
836
+ other = typing.cast(Self, other) # super makes sure this is true
837
+ return (
838
+ super().pattern_eq(other)
839
+ and self.flat_args_kwargs[1] == other.flat_args_kwargs[1]
840
+ and all(
841
+ a.pattern_eq(b) if isinstance(a, PatternExpr) else a == b
842
+ for a, b in zip(self.flat_args_kwargs[0], other.flat_args_kwargs[0])
843
+ )
844
+ )
845
+
846
+
847
+ class CallFunction(_TargetArgsExpr):
848
+ """
849
+ Matches a call_function node in the FX graphs: `fns[i](*args, **kwargs)`
850
+ """
851
+
852
+ op = "call_function"
853
+
854
+
855
+ class CallMethod(_TargetArgsExpr):
856
+ """
857
+ Matches a call_method node in the FX graphs: `fns[i].method(*args, **kwargs)`
858
+ """
859
+
860
+ op = "call_method"
861
+
862
+
863
+ class CallModule(_TargetArgsExpr):
864
+ """
865
+ Matches a call_module node in the FX graphs: `module(*args, **kwargs)`
866
+ """
867
+
868
+ op = "call_module"
869
+
870
+
871
+ class _TargetExprVarArgs(_TargetExpr):
872
+ """
873
+ Matches a call_function node with any arguments which are passed into the pattern
874
+ """
875
+
876
+ def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
877
+ if not self._match_fns(node):
878
+ return FailedMatch("function_mismatch")
879
+
880
+ if not self._match_users(node, ctx):
881
+ return FailedMatch("multiple_users")
882
+
883
+ m = Match(ctx, self)
884
+ m.nodes.append(node)
885
+ m.targets[self] = node.target
886
+ m.args.extend(node.args)
887
+ m.kwargs.update(node.kwargs)
888
+ return m
889
+
890
+
891
+ class CallFunctionVarArgs(_TargetExprVarArgs):
892
+ op = "call_function"
893
+
894
+
895
+ class CallMethodVarArgs(_TargetExprVarArgs):
896
+ op = "call_method"
897
+
898
+
899
+ class CallModuleVarArgs(_TargetExprVarArgs):
900
+ op = "call_module"
901
+
902
+
903
+ class ListOf(PatternExpr):
904
+ """
905
+ Matches a repeated pattern
906
+ """
907
+
908
+ def __init__(self, pattern: PatternExpr, partial: bool = False) -> None:
909
+ super().__init__()
910
+ assert isinstance(pattern, PatternExpr)
911
+ self.pattern = pattern
912
+ self.partial = partial
913
+
914
+ def __repr__(self) -> str:
915
+ return f"{self.__class__.__name__}({self.pattern})"
916
+
917
+ def _match(self, node: list[torch.fx.Node], ctx: MatchContext) -> MatchResult: # type: ignore[override]
918
+ if not isinstance(node, (list, tuple)) or len(node) == 0:
919
+ return FailedMatch("non_list")
920
+ m = Match(ctx, self)
921
+ # Propagating patterns with multiple users will ensure we don't revisit
922
+ # the same nodes
923
+ pattern_to_node = ctx.filter_multi_user_patterns()
924
+ matched = False
925
+ for i, child_node in enumerate(node):
926
+ child_ctx = MatchContext(
927
+ ctx.outputs, pattern_to_node, graph=child_node.graph
928
+ )
929
+ child_match = child_ctx.match(self.pattern, child_node)
930
+ pattern_to_node = child_ctx.filter_multi_user_patterns()
931
+ if not is_match(child_match):
932
+ if not self.partial:
933
+ return FailedMatch("list[{}]: {}", i, child_match)
934
+ continue
935
+ matched = True
936
+ m.extend(child_match.bundle())
937
+ if not matched:
938
+ return FailedMatch("list: no_match")
939
+ return m.bundle()
940
+
941
+ def pattern_eq(self, other: Any) -> bool:
942
+ other = typing.cast(Self, other) # super makes sure this is true
943
+ return (
944
+ super().pattern_eq(other)
945
+ and self.pattern.pattern_eq(other.pattern)
946
+ and self.partial == other.partial
947
+ )
948
+
949
+
950
+ class MultiOutputPattern(PatternExpr):
951
+ outputs: list[PatternExpr | None]
952
+
953
+ def __init__(self, outputs: Sequence[PatternExpr | None]) -> None:
954
+ super().__init__()
955
+ assert isinstance(outputs[0], _TargetExpr)
956
+ assert all(x is None or isinstance(x, PatternExpr) for x in outputs), outputs
957
+ self.outputs = list(outputs)
958
+ self.op = outputs[0].op
959
+
960
+ @property
961
+ def fns(self) -> Callable[..., Any] | str | Sequence[Any]:
962
+ # This cast is checked above in __init__()
963
+ output = typing.cast(_TargetExpr, self.outputs[0])
964
+ return output.fns
965
+
966
+ def __repr__(self) -> str:
967
+ return f"{self.__class__.__name__}({self.outputs})"
968
+
969
+ def pretty_print(self, pp: PatternPrettyPrinter) -> str:
970
+ args = [pp.pretty_print(x) for x in self.outputs]
971
+ joiner_str = f",\n{' '}"
972
+ str_out = f"{self.__class__.__name__}([{joiner_str.join(args)}"
973
+ str_out = f"{str_out}\n])"
974
+ return str_out
975
+
976
+ def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
977
+ output = typing.cast(_TargetExpr, self.outputs[0])
978
+ m = ctx.match(output, node)
979
+ if not is_match(m):
980
+ return m
981
+
982
+ for pattern in self.outputs[1:]:
983
+ if pattern is None:
984
+ continue
985
+ child_match = self._match_from_anchors(pattern, ctx)
986
+ if not is_match(child_match):
987
+ return child_match
988
+ m.extend(child_match)
989
+
990
+ return m
991
+
992
+ def _match_from_anchors(
993
+ self, pattern: PatternExpr, ctx: MatchContext
994
+ ) -> MatchResult:
995
+ prior = dict(ctx.pattern_to_node)
996
+ m: MatchResult = FailedMatch("no anchor found")
997
+ for node in pattern.find_anchor_nodes(ctx, OrderedSet()):
998
+ m = ctx.match(pattern, node)
999
+ if is_match(m):
1000
+ return m
1001
+ # revert any partial matches
1002
+ ctx.pattern_to_node = dict(prior)
1003
+ return m
1004
+
1005
+ def match(self, node: torch.fx.Node) -> MatchResult:
1006
+ try:
1007
+ return MatchContext(self.outputs, graph=node.graph).match(self, node)
1008
+ except FailedMatch as e:
1009
+ return e
1010
+
1011
+ def pattern_eq(self, other: Any) -> bool:
1012
+ other = typing.cast(Self, other) # super makes sure this is true
1013
+ return (
1014
+ super().pattern_eq(other)
1015
+ and len(self.outputs) == len(other.outputs)
1016
+ and all(
1017
+ a.pattern_eq(b) if isinstance(a, PatternExpr) else a == b
1018
+ for a, b in zip(self.outputs, other.outputs)
1019
+ )
1020
+ )
1021
+
1022
+
1023
+ class RepeatedExpr(PatternExpr):
1024
+ """
1025
+ Checks for a repeated pattern. Useful for repeated operations after a node such as `split` or `unbind`
1026
+ """
1027
+
1028
+ def __init__(self, inner_pattern: _TargetExpr) -> None:
1029
+ super().__init__()
1030
+ self.inner_pattern = inner_pattern
1031
+ self.op = inner_pattern.op
1032
+
1033
+ @property
1034
+ def fns(self) -> Sequence[FnsType]:
1035
+ return self.inner_pattern.fns
1036
+
1037
+ def _match(self, node: torch.fx.Node, ctx: MatchContext) -> MatchResult:
1038
+ m = ctx.match(self.inner_pattern, node)
1039
+ if not is_match(m):
1040
+ return m
1041
+ ctx.pattern_to_node.pop(
1042
+ self.inner_pattern,
1043
+ )
1044
+ # Check all anchor nodes match the pattern
1045
+ for anchor_node in self.inner_pattern.find_anchor_nodes(
1046
+ ctx, OrderedSet([node])
1047
+ ):
1048
+ anchor_m = MatchContext([self], graph=node.graph).match(
1049
+ self.inner_pattern, anchor_node
1050
+ )
1051
+ if not is_match(anchor_m):
1052
+ return anchor_m
1053
+ m.extend(anchor_m)
1054
+ return m
1055
+
1056
+ def pattern_eq(self, other: Any) -> bool:
1057
+ other = typing.cast(Self, other) # super makes sure this is true
1058
+ return super().pattern_eq(other) and self.inner_pattern.pattern_eq(
1059
+ other.inner_pattern
1060
+ )
1061
+
1062
+
1063
+ class PatternPrettyPrinter:
1064
+ """
1065
+ Serializes Patterns to executable python.
1066
+ XXX: currently only used and tested for fuse attention patterns. May not cover
1067
+ all patterns.
1068
+ """
1069
+
1070
+ def __init__(self) -> None:
1071
+ self.namespace = torch.fx.graph._Namespace()
1072
+ self.memoized_objs_names: dict[PatternExpr, str] = {}
1073
+ self.memoized_objs_pp: dict[PatternExpr, str] = {}
1074
+
1075
+ @staticmethod
1076
+ @functools.cache
1077
+ def run(obj: PatternExpr, output_name: str = "output") -> str:
1078
+ """
1079
+ Serializes obj to python code with obj written out to `output_name`
1080
+ """
1081
+
1082
+ pp = PatternPrettyPrinter()
1083
+ assert hasattr(obj, "pretty_print")
1084
+ out_str = obj.pretty_print(pp=pp)
1085
+
1086
+ output = [
1087
+ f"{pp.memoized_objs_names[key]} = {pp.memoized_objs_pp[key]}"
1088
+ for key in pp.memoized_objs_names
1089
+ ]
1090
+
1091
+ output.append(f"{output_name} = {out_str}")
1092
+
1093
+ return "\n".join(output)
1094
+
1095
+ def pretty_print(self, obj: Any) -> str:
1096
+ if isinstance(obj, _TargetArgsExpr):
1097
+ if memoized_name := self.memoized_objs_names.get(obj):
1098
+ return memoized_name
1099
+ else:
1100
+ return self.memoize(obj)
1101
+ if hasattr(obj, "pretty_print"):
1102
+ return obj.pretty_print(self)
1103
+
1104
+ return repr(obj)
1105
+
1106
+ def memoize(self, obj: _TargetArgsExpr) -> str:
1107
+ obj_str = obj.pretty_print(self)
1108
+ obj_name = obj.fns_repr()
1109
+ for prefix in ("aten.", "torch.", "prims."):
1110
+ obj_name = obj_name.replace(prefix, "")
1111
+
1112
+ tmp_name = self.namespace.create_name(obj_name, None)
1113
+ self.memoized_objs_names[obj] = tmp_name
1114
+ self.memoized_objs_pp[obj] = obj_str
1115
+ return tmp_name
1116
+
1117
+
1118
+ class _PassDictsType(Protocol):
1119
+ def __getitem__(
1120
+ self, k: tuple[str, torch.fx.node.Target]
1121
+ ) -> list[PatternEntry]: ...
1122
+
1123
+
1124
+ @dataclasses.dataclass
1125
+ class PatternEntry:
1126
+ pattern: PatternExpr
1127
+ extra_check: Callable[[Match], bool]
1128
+
1129
+ def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
1130
+ raise NotImplementedError
1131
+
1132
+ def register(
1133
+ self,
1134
+ pass_dicts: _PassDictsType | Sequence[_PassDictsType],
1135
+ target: torch.fx.node.Target | None = None,
1136
+ prepend: bool = False,
1137
+ ) -> None:
1138
+ if target is None:
1139
+ assert hasattr(self.pattern, "fns")
1140
+ for fn in self.pattern.fns:
1141
+ self.register(pass_dicts, fn, prepend=prepend)
1142
+ elif isinstance(pass_dicts, (dict, PatternMatcherPass)):
1143
+ assert hasattr(self.pattern, "op")
1144
+ if prepend:
1145
+ pass_dicts[(self.pattern.op, target)].insert(0, self)
1146
+ else:
1147
+ pass_dicts[(self.pattern.op, target)].append(self)
1148
+ else:
1149
+ pass_dicts = typing.cast(Sequence[_PassDictsType], pass_dicts)
1150
+ for x in pass_dicts:
1151
+ self.register(x, target, prepend=prepend)
1152
+
1153
+
1154
+ @dataclasses.dataclass
1155
+ class LoweringPatternEntry(PatternEntry):
1156
+ handler: Callable[..., Any]
1157
+
1158
+ def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
1159
+ handler = functools.wraps(self.handler)(functools.partial(self.handler, match))
1160
+ with graph.inserting_before(node):
1161
+ replacement = graph.call_function(handler, tuple(match.args), match.kwargs)
1162
+ _transfer_meta(replacement.meta, node)
1163
+ node.replace_all_uses_with(replacement)
1164
+ assert match.nodes[-1] is node
1165
+ match.erase_nodes()
1166
+
1167
+
1168
+ @dataclasses.dataclass
1169
+ class GraphPatternEntry(PatternEntry):
1170
+ """
1171
+ A pattern that runs a function on the FX graph
1172
+ """
1173
+
1174
+ handler: Callable[..., Any]
1175
+
1176
+ def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
1177
+ with graph.inserting_before(node):
1178
+ self.handler(match, *match.args, **match.kwargs)
1179
+
1180
+
1181
+ @dataclasses.dataclass
1182
+ class ReplacementPatternEntry(PatternEntry):
1183
+ """
1184
+ The replacement pattern for the graph
1185
+ """
1186
+
1187
+ normalize_args: Callable[..., list[Any]]
1188
+ pattern_name: str | None = None # Unique identifier for per-pattern telemetry
1189
+
1190
+ @staticmethod
1191
+ def replace_with_graph(
1192
+ match: Match,
1193
+ graph: torch.fx.Graph,
1194
+ replacement_graph: torch.fx.Graph | torch.fx.GraphModule,
1195
+ args: Sequence[torch.fx.Node],
1196
+ pass_name: str | None = None,
1197
+ ) -> None:
1198
+ """
1199
+ Inserts the replacement graph into the toplevel graph at the match
1200
+ """
1201
+
1202
+ added_replacement_nodes: list[torch.fx.Node] = []
1203
+
1204
+ class Replacer(torch.fx.Interpreter):
1205
+ call_method = None # type: ignore[assignment]
1206
+ call_module = None # type: ignore[assignment]
1207
+ get_attr = None # type: ignore[assignment]
1208
+
1209
+ def run_node(self, node: torch.fx.Node) -> Any:
1210
+ if node.op in ("placeholder", "output"):
1211
+ return super().run_node(node)
1212
+ target = node.target
1213
+ args, kwargs = self.fetch_args_kwargs_from_env(node)
1214
+ if node.op == "call_function":
1215
+ assert callable(target)
1216
+ result = graph.call_function(target, args, kwargs)
1217
+ added_replacement_nodes.append(result)
1218
+ _transfer_meta(
1219
+ new_meta=result.meta,
1220
+ old_node=node,
1221
+ pass_name=pass_name or "",
1222
+ )
1223
+ # This function copy-pastes the replacement graph into
1224
+ # the graph. If the replacement graph had any eager_input_vals,
1225
+ # we propagate those over (val/tensor_meta are handled by
1226
+ # _transfer_meta above).
1227
+ if "eager_input_vals" in node.meta:
1228
+ result.meta["eager_input_vals"] = node.meta["eager_input_vals"]
1229
+ return result
1230
+ if node.op == "get_attr":
1231
+ # If the replacement graph contains a HOP, the subgraphs of the HOP are "get_attr" nodes.
1232
+ # We need to fetch the subgraph of the HOP then register the subgraph to the replaced graph's root.
1233
+ from torch._higher_order_ops.utils import (
1234
+ unique_graph_name_with_root,
1235
+ )
1236
+
1237
+ sub_gm = super().get_attr(target, args, kwargs)
1238
+ if not isinstance(sub_gm, torch.fx.GraphModule):
1239
+ raise NotImplementedError(
1240
+ f"NYI: replacement_graph.{target} is not a graph module. Got {sub_gm}."
1241
+ )
1242
+ assert graph.owning_module is not None
1243
+ graph_name = None
1244
+ for n, mod in graph.owning_module.named_modules():
1245
+ if sub_gm is mod:
1246
+ graph_name = n
1247
+ break
1248
+ if graph_name is None:
1249
+ assert isinstance(target, str)
1250
+ _, graph_name = unique_graph_name_with_root(
1251
+ # pyrefly: ignore [unbound-name]
1252
+ graph.owning_module,
1253
+ target,
1254
+ )
1255
+ # pyrefly: ignore [unbound-name]
1256
+ graph.owning_module.register_module(graph_name, sub_gm)
1257
+ # pyrefly: ignore [unbound-name]
1258
+ getattr_node = graph.get_attr(graph_name)
1259
+ added_replacement_nodes.append(getattr_node)
1260
+ return getattr_node
1261
+
1262
+ raise NotImplementedError(f"unhandled {node}")
1263
+
1264
+ output_nodes = match.output_nodes()
1265
+
1266
+ if len(output_nodes) == 1:
1267
+ last_node = output_nodes[0]
1268
+ else:
1269
+ assert output_nodes[0]
1270
+ nodes = list(output_nodes[0].graph.nodes)
1271
+ indices = [
1272
+ (nodes.index(n), n)
1273
+ for n in output_nodes
1274
+ if isinstance(n, torch.fx.Node)
1275
+ ]
1276
+ last_node = min(indices, key=operator.itemgetter(0))[1]
1277
+
1278
+ def percolate_tags(
1279
+ node: torch.fx.Node,
1280
+ tag_name: str,
1281
+ tag_value: str,
1282
+ input_stops: OrderedSet[torch.fx.Node],
1283
+ ) -> None:
1284
+ queue = [node]
1285
+ visited = OrderedSet[torch.fx.Node]()
1286
+
1287
+ while queue:
1288
+ arg = queue.pop()
1289
+ if (
1290
+ arg not in visited
1291
+ and arg not in input_stops
1292
+ and hasattr(arg, "meta")
1293
+ ):
1294
+ visited.add(arg)
1295
+ arg.meta[tag_name] = tag_value
1296
+ queue.extend(arg.all_input_nodes)
1297
+
1298
+ with graph.inserting_before(last_node):
1299
+ assert isinstance(replacement_graph, torch.fx.GraphModule)
1300
+ replacement = Replacer(replacement_graph).run(*args)
1301
+ if isinstance(replacement, torch.fx.Node):
1302
+ replacement = [replacement]
1303
+
1304
+ def maybe_getitem(node: torch.fx.Node) -> Any:
1305
+ if node.op != "call_function":
1306
+ return None
1307
+ if node.target != operator.getitem:
1308
+ return None
1309
+ assert len(node.args) == 2
1310
+ return node.args[1]
1311
+
1312
+ def replace(
1313
+ old: torch.fx.Node | None,
1314
+ new: torch.fx.Node | Sequence[torch.fx.Node] | None,
1315
+ ) -> None:
1316
+ def filter_nodes_in_newly_added_nodes(node: torch.fx.Node) -> bool:
1317
+ # Do not replace the use of a node if it is being used by
1318
+ # nodes in the replaced graph
1319
+ return node not in added_replacement_nodes
1320
+
1321
+ if old is None:
1322
+ assert new is None
1323
+ return
1324
+ assert isinstance(old, torch.fx.Node)
1325
+ if new is None:
1326
+ old.replace_all_uses_with(
1327
+ None, # type: ignore[arg-type]
1328
+ delete_user_cb=filter_nodes_in_newly_added_nodes,
1329
+ )
1330
+ if len(old.users) == 0:
1331
+ graph.erase_node(old)
1332
+ return
1333
+ if isinstance(new, torch.fx.Node):
1334
+ _transfer_meta(new.meta, old, pass_name=pass_name or "")
1335
+
1336
+ # Preserve the recompute tags in the replacement graph. We
1337
+ # look at the recompute tags of the original output node to
1338
+ # propagate the tag from the output all the way to the input
1339
+ # args (named as args in the replace_with_graph).
1340
+ # Note that this is best effort. Since patterns are from
1341
+ # many to many, there is no easy way to correctly map the
1342
+ # recomputable tags. It is possible in some scenarios that we
1343
+ # incorrectly tag some nodes as recomputables.
1344
+ for tag_name in ["recompute", "ac_graph_id"]:
1345
+ if tag_name in old.meta:
1346
+ percolate_tags(
1347
+ new, tag_name, old.meta[tag_name], OrderedSet(args)
1348
+ )
1349
+
1350
+ old.replace_all_uses_with(
1351
+ new, delete_user_cb=filter_nodes_in_newly_added_nodes
1352
+ )
1353
+ if len(old.users) == 0:
1354
+ graph.erase_node(old)
1355
+ return
1356
+
1357
+ # `new` is not a node: it's a list of nodes.
1358
+ #
1359
+ # This happens when we want to replace a node that has a single
1360
+ # packed return with multiple unpacked returns. We need to do
1361
+ # some graph surgery here.
1362
+ #
1363
+ # Example:
1364
+ # def original_graph(x):
1365
+ # a = op(x)
1366
+ # b = a[0]
1367
+ # c = a[1]
1368
+ # ...
1369
+ #
1370
+ # Assume that we want to replace op(x) with the graph
1371
+ # def new_op(x):
1372
+ # w = x + 1
1373
+ # z = x + 2
1374
+ # return (w, z)
1375
+ #
1376
+ # We need to replace `op` with the contents of `new_op`,
1377
+ # and then rewrite a[0] to be w and a[1] to be z, as so:
1378
+ # def new_graph(x):
1379
+ # w = x + 1
1380
+ # z = x + 2
1381
+ # b = w
1382
+ # c = z
1383
+ # ...
1384
+ old_uses = list(old.users.keys())
1385
+ for user in old_uses:
1386
+ idx = maybe_getitem(user)
1387
+ if idx is None:
1388
+ # Output is used directly
1389
+ # pyrefly: ignore [bad-argument-type]
1390
+ old.replace_all_uses_with(new)
1391
+ else:
1392
+ replace(user, new[idx])
1393
+ graph.erase_node(old)
1394
+
1395
+ if len(output_nodes) == len(replacement):
1396
+ for old, new in zip(output_nodes, replacement):
1397
+ replace(old, new)
1398
+ else:
1399
+ assert len(output_nodes) == 1
1400
+ replace(output_nodes[0], replacement)
1401
+
1402
+ match.erase_nodes()
1403
+
1404
+ # Remove dead replacement nodes so they don't inflate user counts
1405
+ # in later lowering heuristics (e.g. should_realize_on_reuse).
1406
+ for node in reversed(added_replacement_nodes):
1407
+ if (
1408
+ not node.users
1409
+ and not node.is_impure()
1410
+ and not isinstance(node.target, torch._ops.HigherOrderOperator)
1411
+ ):
1412
+ graph.erase_node(node)
1413
+
1414
+ def apply(self, match: Match, graph: torch.fx.Graph, node: torch.fx.Node) -> None:
1415
+ assert match.replacement_graph is not None
1416
+ self.replace_with_graph(
1417
+ match,
1418
+ graph,
1419
+ match.replacement_graph,
1420
+ self.normalize_args(*match.args, **match.kwargs),
1421
+ pass_name=self.pattern_name or "replace_with_graph",
1422
+ )
1423
+
1424
+
1425
+ def _return_true(match: Match) -> bool:
1426
+ return True
1427
+
1428
+
1429
+ def log_trace_failure(search_fn: Callable[..., Any], e: RuntimeError) -> None:
1430
+ log.info(
1431
+ "Replacement pattern %s failed to apply due to shape mismatch: %s",
1432
+ search_fn.__name__,
1433
+ e,
1434
+ )
1435
+
1436
+
1437
+ def check_and_add_duplicate_pattern(
1438
+ pattern: PatternExpr,
1439
+ graph: torch.fx.Graph | None,
1440
+ seen_patterns: dict[str, list[str | None]],
1441
+ skip_duplicates: bool = False,
1442
+ ) -> bool:
1443
+ """
1444
+ Check if a pattern is a duplicate. Because we ignore certain types in searching, but not
1445
+ in matching, use the graph to distinguish equivalent search patterns.
1446
+
1447
+ Returns True if a duplicate is found and `skip_duplicates=True` is passed in. Errors if
1448
+ `skip_duplicates` is False and a duplicate is found.
1449
+ """
1450
+
1451
+ pattern_repr = PatternPrettyPrinter.run(pattern)
1452
+ equiv_pattern_reprs = seen_patterns.get(pattern_repr)
1453
+ if not equiv_pattern_reprs:
1454
+ seen_patterns[pattern_repr].append(str(graph) if graph else None)
1455
+ return False
1456
+
1457
+ if graph is None:
1458
+ if skip_duplicates:
1459
+ return True
1460
+ torch._check(
1461
+ False,
1462
+ lambda: f"Duplicate pattern: {pattern_repr} with no graph",
1463
+ )
1464
+
1465
+ new_graph_str = str(graph)
1466
+ for graph_str in equiv_pattern_reprs:
1467
+ if new_graph_str != graph_str:
1468
+ continue
1469
+ if skip_duplicates:
1470
+ return True
1471
+ torch._check(
1472
+ False,
1473
+ lambda: f"Duplicate pattern: {pattern_repr} with duplicated match graph {graph_str} ",
1474
+ )
1475
+ equiv_pattern_reprs.append(new_graph_str)
1476
+ return False
1477
+
1478
+
1479
+ def register_replacement(
1480
+ search_fn: SearchFn,
1481
+ replace_fn: ReplaceFn,
1482
+ example_inputs: list[Any] | tuple[Any, ...],
1483
+ trace_fn: TraceFn,
1484
+ pass_dicts: _PassDictsType | Sequence[_PassDictsType],
1485
+ extra_check: Callable[[Match], bool] = _return_true,
1486
+ scalar_workaround: dict[str, float | int] | None = None,
1487
+ exclusive_arg_names: Sequence[str] = (),
1488
+ search_fn_pattern: PatternExpr | None = None,
1489
+ skip_duplicates: bool = False,
1490
+ pattern_name: str | None = None,
1491
+ get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table,
1492
+ ) -> bool:
1493
+ """
1494
+ Create a replacement rule based on example functions that get traced
1495
+ to create patterns. This supports both training and inference when
1496
+ run on a joint forward+backward graph.
1497
+
1498
+ Args:
1499
+ search_fn: traced to give original pattern
1500
+ replace_fn: traced to give replacement graph
1501
+ example_inputs: example inputs for initial trace
1502
+ trace_fn: fwd_only or joint_fwd_bwd
1503
+ pass_dict: dict of passes to register to
1504
+ extra_check: additional check to run on match(using real shapes)
1505
+ """
1506
+ argnames_static = [*inspect.signature(search_fn).parameters.keys()]
1507
+
1508
+ if inspect.ismethod(search_fn):
1509
+ search_fn = _wrap_bound_method(search_fn, argnames_static)
1510
+
1511
+ if inspect.ismethod(replace_fn):
1512
+ replace_argnames = [*inspect.signature(replace_fn).parameters.keys()]
1513
+ replace_fn = _wrap_bound_method(replace_fn, replace_argnames)
1514
+
1515
+ if not isinstance(example_inputs, (list, tuple)):
1516
+ raise TypeError(
1517
+ f"example_inputs must be a list or tuple, got {type(example_inputs)}"
1518
+ )
1519
+
1520
+ def check_fn(match: Match) -> bool:
1521
+ """
1522
+ Often shapes get burned into the pattern, so our initial match ran with
1523
+ `ignore_types=(int, ...)`.
1524
+
1525
+ Recheck the match with the correct shapes.
1526
+ """
1527
+ argnames = list(argnames_static)
1528
+ for name in argnames:
1529
+ if name not in match.kwargs:
1530
+ raise RuntimeError(
1531
+ f"Not all inputs to pattern found in match.kwargs. Perhaps one "
1532
+ f"of the inputs is unused? argnames={argnames}, match.kwargs={match.kwargs}"
1533
+ )
1534
+
1535
+ args = list(
1536
+ torch.fx.map_arg(
1537
+ [match.kwargs[name] for name in argnames], lambda n: n.meta["val"]
1538
+ )
1539
+ )
1540
+
1541
+ sym_args: list[torch.SymInt] = []
1542
+ fake_mode = torch._dynamo.utils.detect_fake_mode(args)
1543
+ assert fake_mode is not None
1544
+ with fake_mode:
1545
+ for i, grad in enumerate(requires_grad):
1546
+ if isinstance(args[i], torch.Tensor):
1547
+ # pyrefly: ignore [missing-attribute]
1548
+ if grad and is_integer_dtype(args[i].dtype):
1549
+ return False
1550
+
1551
+ args[i] = torch.empty_strided(
1552
+ # pyrefly: ignore [missing-attribute]
1553
+ args[i].size(),
1554
+ # pyrefly: ignore [missing-attribute]
1555
+ args[i].stride(),
1556
+ # pyrefly: ignore [missing-attribute]
1557
+ dtype=args[i].dtype,
1558
+ # pyrefly: ignore [missing-attribute]
1559
+ device=args[i].device,
1560
+ requires_grad=grad,
1561
+ )
1562
+ # pyrefly: ignore [missing-attribute]
1563
+ for v in itertools.chain(args[i].shape, args[i].stride()):
1564
+ if isinstance(v, torch.SymInt) and all(
1565
+ statically_known_true(v != a) for a in sym_args
1566
+ ):
1567
+ sym_args.append(v)
1568
+
1569
+ # If we were given a pre-traced pattern then use that instead of
1570
+ # retracing. Note that this means the pattern has to be independent
1571
+ # of its args.
1572
+ specific_pattern = search_fn_pattern
1573
+
1574
+ if not specific_pattern:
1575
+ if sym_args:
1576
+ # AOT Autograd and make fx will dedupe symbolic shape size
1577
+ # accesses of sym ints that appear as inputs
1578
+ # We don't want the sym_size uses to interfere with pattern matching
1579
+ # so we provide them as inputs.
1580
+ # Later, when we actually do the replacement, the symbolic shape
1581
+ # sizes will get re-traced and added to the graph.
1582
+
1583
+ def search_fn_new(*args_new: Any, **_: Any) -> Any:
1584
+ return search_fn(*args_new[len(args_new) - len(args) :])
1585
+
1586
+ try:
1587
+ specific_graph = trace_fn(
1588
+ search_fn_new,
1589
+ sym_args + args,
1590
+ get_decomp_fn=get_decomp_fn,
1591
+ )
1592
+ except RuntimeError as e:
1593
+ log_trace_failure(search_fn, e)
1594
+ return False
1595
+
1596
+ # correct argnames in the graph
1597
+ sym_arg_names = []
1598
+ for i, placeholder in zip(
1599
+ range(len(sym_args) + len(args)),
1600
+ specific_graph.graph.nodes,
1601
+ ):
1602
+ if i < len(sym_args):
1603
+ sym_arg_names.append(placeholder.target)
1604
+ continue
1605
+
1606
+ with specific_graph.graph.inserting_after(placeholder):
1607
+ new_node = specific_graph.graph.placeholder(
1608
+ argnames[i - len(sym_args)]
1609
+ )
1610
+ new_node.target = new_node.name
1611
+ placeholder.replace_all_uses_with(new_node)
1612
+ specific_graph.graph.erase_node(placeholder)
1613
+
1614
+ argnames = sym_arg_names + argnames
1615
+ else:
1616
+ try:
1617
+ specific_graph = trace_fn(
1618
+ search_fn, args, get_decomp_fn=get_decomp_fn
1619
+ )
1620
+ except RuntimeError as e:
1621
+ log_trace_failure(search_fn, e)
1622
+ return False
1623
+
1624
+ specific_pattern = fx_to_pattern(
1625
+ specific_graph,
1626
+ argnames=argnames,
1627
+ exclusive_arg_names=exclusive_arg_names,
1628
+ scalar_workaround=scalar_workaround,
1629
+ )
1630
+
1631
+ node = match.output_nodes()[0]
1632
+ assert node is not None
1633
+ specific_pattern_match = specific_pattern.match(node)
1634
+
1635
+ if _should_debug_node(node.name):
1636
+ log.warning(
1637
+ "Specific pattern match: %s%s %s %s",
1638
+ node,
1639
+ node.args,
1640
+ specific_pattern_match,
1641
+ specific_pattern,
1642
+ )
1643
+
1644
+ if is_match(specific_pattern_match) and extra_check(specific_pattern_match):
1645
+ # trace the pattern using the shapes from the user program
1646
+ match.replacement_graph = trace_fn(
1647
+ replace_fn, args, get_decomp_fn=get_decomp_fn
1648
+ )
1649
+ if len(match.nodes) == 1:
1650
+ for n in match.replacement_graph.graph.nodes:
1651
+ _transfer_meta(
1652
+ new_meta=n.meta,
1653
+ old_node=match.nodes[0],
1654
+ pass_name="replacement",
1655
+ )
1656
+ return True
1657
+ return False
1658
+
1659
+ def normalize_args(**kwargs: Any) -> list[Any]:
1660
+ args = [kwargs.pop(name) for name in argnames_static]
1661
+ for i in range(1, len(kwargs) + 1):
1662
+ if f"tangents_{i}" not in kwargs:
1663
+ break
1664
+ args.append(kwargs.pop(f"tangents_{i}"))
1665
+ assert not kwargs, f"leftover kwargs: {kwargs!r}"
1666
+ return args
1667
+
1668
+ if trace_fn is joint_fwd_bwd:
1669
+ # If inference mode is enabled during compilation, assume that we don't
1670
+ # want to match on any training graph patterns
1671
+ if torch.is_inference_mode_enabled():
1672
+ return False
1673
+
1674
+ # TODO: Revisit the functionalize_rng_ops for lowmem dropout
1675
+ with functorch_config.patch(functionalize_rng_ops=False):
1676
+ requires_grad: list[bool] = [
1677
+ isinstance(x, torch.Tensor) and x.requires_grad for x in example_inputs
1678
+ ]
1679
+ if search_fn_pattern is None:
1680
+ pattern, gm = gen_pattern_and_search_gm(
1681
+ search_fn,
1682
+ example_inputs,
1683
+ trace_fn,
1684
+ scalar_workaround,
1685
+ exclusive_arg_names,
1686
+ get_decomp_fn=get_decomp_fn,
1687
+ )
1688
+ else:
1689
+ pattern = search_fn_pattern
1690
+ gm = None
1691
+
1692
+ for pattern_matcher_pass in (
1693
+ pass_dicts if isinstance(pass_dicts, Sequence) else [pass_dicts]
1694
+ ):
1695
+ if isinstance(pattern_matcher_pass, PatternMatcherPass):
1696
+ if check_and_add_duplicate_pattern(
1697
+ pattern,
1698
+ gm.graph if gm else None,
1699
+ pattern_matcher_pass.seen_patterns,
1700
+ skip_duplicates=skip_duplicates,
1701
+ ):
1702
+ return False
1703
+
1704
+ pattern = ReplacementPatternEntry(
1705
+ pattern=pattern,
1706
+ extra_check=check_fn,
1707
+ normalize_args=normalize_args,
1708
+ pattern_name=pattern_name,
1709
+ )
1710
+ pattern.register(pass_dicts)
1711
+ return pattern.pattern # type: ignore[return-value]
1712
+
1713
+
1714
+ _serialized_patterns: OrderedSet[str] = OrderedSet()
1715
+
1716
+
1717
+ def _serialize_pattern(
1718
+ unique_name: str,
1719
+ search_fn: SearchFn,
1720
+ example_inputs: Sequence[Any],
1721
+ trace_fn: TraceFn,
1722
+ scalar_workaround: dict[str, float | int] | None,
1723
+ ) -> PatternExpr:
1724
+ def get_file_template() -> str:
1725
+ auto_generated_msg = textwrap.dedent(
1726
+ """\
1727
+ # This is an auto-generated file. Please do not modify it by hand.
1728
+ # To re-generate, run:
1729
+ # cd ~/pytorch && python torchgen/fuse/gen_patterns.py
1730
+ """
1731
+ )
1732
+
1733
+ file_template = textwrap.dedent(
1734
+ """\
1735
+ # mypy: ignore-errors
1736
+
1737
+ # noqa: F401, E501
1738
+ {msg}
1739
+ import torch
1740
+ import torch._inductor
1741
+ import operator
1742
+
1743
+ aten = torch.ops.aten
1744
+ prims = torch.ops.prims
1745
+
1746
+ """
1747
+ ).format(msg=auto_generated_msg)
1748
+
1749
+ pattern_matcher_imports = []
1750
+ for name in dir(torch._inductor.pattern_matcher):
1751
+ attr = getattr(torch._inductor.pattern_matcher, name)
1752
+ try:
1753
+ if isinstance(attr, type) and issubclass(
1754
+ attr, (PatternExpr, _TargetExpr)
1755
+ ):
1756
+ # pyrefly: ignore [bad-argument-type]
1757
+ pattern_matcher_imports.append(name)
1758
+ except TypeError:
1759
+ pass
1760
+
1761
+ formatted_imports = ",\n ".join(pattern_matcher_imports)
1762
+ formatted_imports = f"from torch._inductor.pattern_matcher import (\n {formatted_imports},\n)\n"
1763
+ return f"{file_template}{formatted_imports}"
1764
+
1765
+ if not SERIALIZED_PATTERN_PATH.is_dir():
1766
+ raise RuntimeError(
1767
+ f"Could not find serialized patterns directory at {SERIALIZED_PATTERN_PATH}"
1768
+ )
1769
+
1770
+ pattern_name = search_fn.__name__
1771
+
1772
+ from torch._functorch import config as functorch_config
1773
+
1774
+ with functorch_config.patch(functionalize_rng_ops=False):
1775
+ pattern = gen_pattern(search_fn, example_inputs, trace_fn, scalar_workaround)
1776
+
1777
+ serialized_pattern = PatternPrettyPrinter.run(pattern, output_name=unique_name)
1778
+ if pattern_name not in _serialized_patterns:
1779
+ write_mode = "w"
1780
+ _serialized_patterns.add(pattern_name)
1781
+ else:
1782
+ write_mode = "a"
1783
+
1784
+ file_template = get_file_template()
1785
+
1786
+ with open(SERIALIZED_PATTERN_PATH / f"{pattern_name}.py", write_mode) as f:
1787
+ if write_mode == "w":
1788
+ f.write(file_template)
1789
+ else:
1790
+ f.write("\n\n")
1791
+ f.write(serialized_pattern)
1792
+ f.write("\n")
1793
+
1794
+ return pattern
1795
+
1796
+
1797
+ SERIALIZED_PATTERN_PATH = Path(__file__).parent / "fx_passes" / "serialized_patterns"
1798
+
1799
+ # This is the set of serialized patterns that we've registered. Used by
1800
+ # test_serialized_patterns_up_to_date() to ensure the patterns are up
1801
+ # to date.
1802
+ _known_precompiled_patterns: list[
1803
+ tuple[
1804
+ Any,
1805
+ Iterable[Any],
1806
+ Callable[[Callable[..., Any], Iterable[Any]], torch.fx.GraphModule],
1807
+ Any,
1808
+ PatternExpr,
1809
+ ]
1810
+ ] = []
1811
+
1812
+
1813
+ def gen_register_replacement(
1814
+ unique_name: str,
1815
+ search_fn: SearchFn,
1816
+ replace_fn: ReplaceFn,
1817
+ example_inputs: Iterable[Any],
1818
+ trace_fn: TraceFn,
1819
+ pass_dicts: _PassDictsType | Sequence[_PassDictsType],
1820
+ extra_check: Callable[[Match], bool] = _return_true,
1821
+ scalar_workaround: dict[str, float | int] | None = None,
1822
+ exclusive_arg_names: Sequence[str] = (),
1823
+ skip_duplicates: bool = False,
1824
+ get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table,
1825
+ ) -> None:
1826
+ # Make sure the example_inputs is materialized.
1827
+ example_inputs = tuple(example_inputs)
1828
+
1829
+ if "PYTORCH_GEN_PATTERNS" in os.environ:
1830
+ pat = _serialize_pattern(
1831
+ unique_name, search_fn, example_inputs, trace_fn, scalar_workaround
1832
+ )
1833
+ else:
1834
+ pattern_name = search_fn.__name__
1835
+ m = importlib.import_module(
1836
+ f"torch._inductor.fx_passes.serialized_patterns.{pattern_name}"
1837
+ )
1838
+ if not m or not hasattr(m, unique_name):
1839
+ log.warning(
1840
+ "Precompiled pattern %r not found. Run torchgen/fuse/gen_patterns.py.",
1841
+ unique_name,
1842
+ )
1843
+ pat = getattr(m, unique_name)
1844
+
1845
+ for arg in pytree.tree_iter(example_inputs):
1846
+ if isinstance(arg, FakeTensor) and arg.constant is not None:
1847
+ # This can be a problem - small fake tensors (e.g. `tensor(2)`) will
1848
+ # hold onto their original constant value - and by stashing it here
1849
+ # will cause a memory leak if the constant value is on GPU.
1850
+ # Since this is just an optimization we can clear it out.
1851
+ arg.constant = None
1852
+
1853
+ _known_precompiled_patterns.append(
1854
+ (search_fn, example_inputs, trace_fn, scalar_workaround, pat)
1855
+ )
1856
+ register_replacement(
1857
+ search_fn,
1858
+ replace_fn,
1859
+ example_inputs,
1860
+ trace_fn,
1861
+ pass_dicts,
1862
+ extra_check,
1863
+ scalar_workaround,
1864
+ exclusive_arg_names,
1865
+ search_fn_pattern=pat,
1866
+ skip_duplicates=skip_duplicates,
1867
+ pattern_name=unique_name,
1868
+ get_decomp_fn=get_decomp_fn,
1869
+ )
1870
+
1871
+
1872
+ @functorch_config.patch(functionalize_rng_ops=False) # type: ignore[misc]
1873
+ def gen_pattern_and_search_gm(
1874
+ search_fn: SearchFn,
1875
+ example_inputs: Sequence[Any],
1876
+ trace_fn: TraceFn,
1877
+ scalar_workaround: dict[str, float | int] | None = None,
1878
+ exclusive_arg_names: Sequence[str] = (),
1879
+ get_decomp_fn: Callable[..., dict[Any, Callable[..., Any]]] = select_decomp_table,
1880
+ ) -> tuple[PatternExpr, torch.fx.GraphModule]:
1881
+ argnames = [*inspect.signature(search_fn).parameters.keys()]
1882
+
1883
+ if scalar_workaround is None:
1884
+ scalar_workaround = {}
1885
+ flat_inputs = []
1886
+ input_idx = 0 # Positional arguments index
1887
+
1888
+ for argname in argnames:
1889
+ if argname in scalar_workaround:
1890
+ flat_inputs.append(scalar_workaround[argname])
1891
+ else:
1892
+ flat_inputs.append(example_inputs[input_idx])
1893
+ input_idx += 1
1894
+
1895
+ search_gm = trace_fn(search_fn, flat_inputs, get_decomp_fn=get_decomp_fn)
1896
+ return (
1897
+ fx_to_pattern(
1898
+ search_gm,
1899
+ ignore_types=(int, float, list, torch.device, torch.dtype),
1900
+ argnames=argnames,
1901
+ scalar_workaround=scalar_workaround,
1902
+ exclusive_arg_names=exclusive_arg_names,
1903
+ ),
1904
+ search_gm,
1905
+ )
1906
+
1907
+
1908
+ def gen_pattern(
1909
+ search_fn: SearchFn,
1910
+ example_inputs: Sequence[Any],
1911
+ trace_fn: TraceFn,
1912
+ scalar_workaround: dict[str, float | int] | None = None,
1913
+ exclusive_arg_names: Sequence[str] = (),
1914
+ ) -> PatternExpr:
1915
+ return gen_pattern_and_search_gm(
1916
+ search_fn, example_inputs, trace_fn, scalar_workaround, exclusive_arg_names
1917
+ )[0]
1918
+
1919
+
1920
+ def register_lowering_pattern(
1921
+ pattern: PatternExpr,
1922
+ extra_check: Callable[[Match], bool] = _return_true,
1923
+ *,
1924
+ pass_dict: _PassDictsType,
1925
+ prepend: bool = False,
1926
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
1927
+ """
1928
+ Register an aten to inductor IR replacement pattern. The decorated
1929
+ function is saved and then called a lowering time allowing direct
1930
+ pattern to inductor IR conversion.
1931
+ """
1932
+
1933
+ def decorator(handler: Callable[..., Any]) -> Callable[..., Any]:
1934
+ assert callable(handler)
1935
+ LoweringPatternEntry(
1936
+ pattern=pattern, extra_check=extra_check, handler=handler
1937
+ ).register(pass_dict, prepend=prepend)
1938
+ handler._inductor_lowering_function = True # type: ignore[attr-defined]
1939
+ return handler
1940
+
1941
+ return decorator
1942
+
1943
+
1944
+ def register_graph_pattern(
1945
+ pattern: PatternExpr,
1946
+ extra_check: Callable[[Match], bool] = _return_true,
1947
+ *,
1948
+ pass_dict: _PassDictsType,
1949
+ prepend: bool = False,
1950
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
1951
+ """
1952
+ Register a pattern that runs a function on the FX graph, allowing
1953
+ custom transformation code.
1954
+ """
1955
+
1956
+ def decorator(handler: Callable[..., Any]) -> Callable[..., Any]:
1957
+ assert callable(handler)
1958
+ GraphPatternEntry(
1959
+ pattern=pattern, extra_check=extra_check, handler=handler
1960
+ ).register(pass_dict, prepend=prepend)
1961
+ return handler
1962
+
1963
+ return decorator
1964
+
1965
+
1966
+ def is_start_of_fx_graph(graph: torch.fx.Graph, node: torch.fx.Node) -> bool:
1967
+ # first node in the graph
1968
+ return node is next(iter(graph.nodes))
1969
+
1970
+
1971
+ # match: copy_, relu_, _set_grad_enabled, manual_seed, _enter_autocast, etc
1972
+ # doesn't match: __rshift__, etc
1973
+ _mutation_op_re = re.compile(r"(?<!_)(_$|_[.]|(\b|_)(set|enter|exit|seed)(\b|_))(?!_)")
1974
+
1975
+
1976
+ def fixme_incorrect_inductor_schema_op(op: torch._ops.OpOverload) -> bool:
1977
+ if op.namespace != "inductor":
1978
+ return False
1979
+
1980
+ # TODO - fix schema
1981
+ # Dont add any more !
1982
+ return op in (
1983
+ torch.ops.inductor.accumulate_grad_.default,
1984
+ torch.ops.inductor.resize_storage_bytes_.default,
1985
+ )
1986
+
1987
+
1988
+ def is_mutation_op(node: torch.fx.Node) -> bool:
1989
+ if isinstance(
1990
+ node.target, torch._ops.OpOverload
1991
+ ) and not fixme_incorrect_inductor_schema_op(node.target):
1992
+ return node.target._schema.is_mutable
1993
+ elif isinstance(
1994
+ node.target, torch._higher_order_ops.auto_functionalize.AutoFunctionalized
1995
+ ):
1996
+ return False
1997
+ if node.op == "call_function":
1998
+ assert callable(node.target)
1999
+ if _mutation_op_re.search(node.target.__name__):
2000
+ return True
2001
+ elif node.op == "call_method":
2002
+ assert isinstance(node.target, str)
2003
+ if _mutation_op_re.search(node.target):
2004
+ return True
2005
+ return node.kwargs.get("out") is not None
2006
+
2007
+
2008
+ def same_mutation_regions(a: torch.fx.Node, b: torch.fx.Node) -> bool:
2009
+ assert "mutation_region_id" in a.meta
2010
+ assert "mutation_region_id" in b.meta
2011
+ return a.meta["mutation_region_id"] == b.meta["mutation_region_id"]
2012
+
2013
+
2014
+ def get_mutation_region_id(graph: torch.fx.Graph, node: torch.fx.Node) -> int:
2015
+ n = node
2016
+ while "mutation_region_id" not in n.meta and not is_start_of_fx_graph(graph, n):
2017
+ n = n.prev
2018
+ mutation_region_id = n.meta.get("mutation_region_id", 0)
2019
+ while n is not node:
2020
+ n = n.next
2021
+ if is_mutation_op(n):
2022
+ mutation_region_id += 1
2023
+ n.meta["mutation_region_id"] = mutation_region_id
2024
+ return mutation_region_id
2025
+
2026
+
2027
+ def should_compute_mutation_region_ids(graph: torch.fx.Graph) -> bool:
2028
+ return "mutation_region_id" not in next(iter(graph.nodes)).meta
2029
+
2030
+
2031
+ def compute_mutation_region_ids(graph: torch.fx.Graph) -> None:
2032
+ mutation_region_id = 0
2033
+ for nd in graph.nodes:
2034
+ if is_mutation_op(nd):
2035
+ mutation_region_id += 1
2036
+ nd.meta["mutation_region_id"] = mutation_region_id
2037
+
2038
+
2039
+ def _wrap_bound_method(fn: Any, argnames: list[str]) -> Any:
2040
+ """
2041
+ Wrap a bound method to remove 'self' from its signature for FX tracing.
2042
+ """
2043
+
2044
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
2045
+ return fn(*args, **kwargs)
2046
+
2047
+ params = [
2048
+ inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD)
2049
+ for name in argnames
2050
+ ]
2051
+ wrapper.__signature__ = inspect.Signature(params) # type: ignore[attr-defined]
2052
+ return wrapper
2053
+
2054
+
2055
+ class PatternMatcherPass:
2056
+ """
2057
+ Registry of patterns to match and replace in FX graphs.
2058
+ """
2059
+
2060
+ def __init__(
2061
+ self,
2062
+ pass_name: str | None = None,
2063
+ subsystem: str | None = None,
2064
+ ) -> None:
2065
+ super().__init__()
2066
+ self.patterns: defaultdict[
2067
+ tuple[str, torch.fx.node.Target], list[PatternEntry]
2068
+ ] = defaultdict(list)
2069
+ self.pass_name = pass_name
2070
+ self.subsystem = subsystem
2071
+
2072
+ # For a particular generated pattern repr, store all of the str representations
2073
+ # of the graph used to generate them. Because we ignore certain patterns
2074
+ # in searching, but not in matching, use the graph to distinguish if two equivalent
2075
+ # searches are actually different.
2076
+ self.seen_patterns: dict[str, list[str | None]] = defaultdict(list)
2077
+
2078
+ def __getitem__(self, item: tuple[str, torch.fx.node.Target]) -> list[PatternEntry]:
2079
+ return self.patterns[item]
2080
+
2081
+ def apply(self, gm: torch.fx.GraphModule | torch.fx.Graph) -> int:
2082
+ """Apply all registered patterns to the graph, returning the number of matches."""
2083
+ if not self.patterns:
2084
+ return 0
2085
+ if isinstance(gm, torch.fx.GraphModule):
2086
+ graph = gm.graph
2087
+ elif isinstance(gm, torch.fx.Graph):
2088
+ graph = gm
2089
+ gm = graph.owning_module
2090
+ else:
2091
+ raise RuntimeError(
2092
+ f"The input to PatternMatcherPass must be a GraphModule or a Graph, but got {type(gm)}"
2093
+ )
2094
+ if should_compute_mutation_region_ids(graph):
2095
+ compute_mutation_region_ids(graph)
2096
+ get_mutation_region_id_partial = functools.partial(
2097
+ get_mutation_region_id, graph
2098
+ )
2099
+ count = 0
2100
+ nodes = []
2101
+ has_call_module = False
2102
+ for op, target in self.patterns:
2103
+ if op == "call_module":
2104
+ has_call_module = True
2105
+ else:
2106
+ nodes.append(graph.find_nodes(op=op, target=target, sort=False))
2107
+ if has_call_module:
2108
+ nodes.append(graph.find_nodes(op="call_module", sort=False))
2109
+ pass_name = self.pass_name if self.pass_name is not None else "pattern_matcher"
2110
+ assert isinstance(gm, torch.fx.GraphModule)
2111
+ with GraphTransformObserver(gm, pass_name, self.subsystem):
2112
+ for node in sorted(itertools.chain.from_iterable(nodes), reverse=True):
2113
+ target = extract_target(node)
2114
+ if node.op == "call_module":
2115
+ if (node.op, target) not in self.patterns:
2116
+ continue
2117
+
2118
+ # conservatively not applying pattern for cpu input,
2119
+ # since some of the patterns induce codegen and split nodes.
2120
+ # Note: we will only skip cpu compute if disable_cpp_codegen=True
2121
+ if fallback_node_due_to_unsupported_type(node, allow_cpu_inputs=False):
2122
+ continue
2123
+
2124
+ for entry in self.patterns[(node.op, target)]:
2125
+ if node._erased:
2126
+ break
2127
+ m = entry.pattern.match(node)
2128
+ # pattern match crosses mutation barrier - discard
2129
+ if (
2130
+ is_match(m)
2131
+ and len(
2132
+ OrderedSet(map(get_mutation_region_id_partial, m.nodes))
2133
+ )
2134
+ != 1
2135
+ ):
2136
+ continue
2137
+ # pattern match crosses stream boundary - discard
2138
+ if (
2139
+ is_match(m)
2140
+ and len(
2141
+ OrderedSet(
2142
+ n.meta.get("custom", {}).get("stream", 0)
2143
+ for n in m.nodes
2144
+ )
2145
+ )
2146
+ != 1
2147
+ ):
2148
+ continue
2149
+ if _should_debug_node(node.name):
2150
+ log.warning("%s%s %s %s", node, node.args, m, entry.pattern)
2151
+
2152
+ if is_match(m) and guard_or_false(entry.extra_check(m)):
2153
+ count += 1
2154
+ entry.apply(m, graph, node)
2155
+ counters[backend]["pattern_matcher_count"] += 1
2156
+ counters[backend]["pattern_matcher_nodes"] += len(m.nodes)
2157
+
2158
+ # Track per-pattern counts when debug mode is active
2159
+ if os.environ.get("TORCHINDUCTOR_PATTERN_MATCH_DEBUG"):
2160
+ if getattr(entry, "pattern_name", None):
2161
+ pattern_name = entry.pattern_name
2162
+ else:
2163
+ # Fallback: use pattern class name + operation target
2164
+ pattern_class = entry.pattern.__class__.__name__
2165
+ target = str(node.target)
2166
+ pattern_name = f"{pattern_class}_{target}"
2167
+
2168
+ pattern_key = f"{backend}_pattern_matcher_per_pattern"
2169
+ counters[pattern_key][pattern_name] += 1
2170
+ return count
2171
+
2172
+ def clear(self) -> None:
2173
+ self.patterns.clear()
2174
+
2175
+
2176
+ def _not_implemented(*args: Any, **kwargs: Any) -> NoReturn:
2177
+ raise NotImplementedError
2178
+
2179
+
2180
+ def fx_to_pattern(
2181
+ gm: torch.fx.GraphModule | torch.fx.Graph,
2182
+ ignore_types: Sequence[type[Any]] = (),
2183
+ argnames: Sequence[str] = (),
2184
+ scalar_workaround: dict[str, float | int] | None = None,
2185
+ exclusive_arg_names: Sequence[str] = (),
2186
+ ) -> PatternExpr:
2187
+ """
2188
+ Convert an FX graph into a PatternExpr. This is useful for simple
2189
+ patterns that can only match single functions and fixed-length lists.
2190
+ """
2191
+ # scalar_workaround is a hack to capture dropout_p
2192
+ # see https://github.com/pytorch/pytorch/issues/97894
2193
+ scalar_workaround = scalar_workaround or {}
2194
+ inv_scalar_workaround = {v: k for k, v in scalar_workaround.items()}
2195
+ assert len(inv_scalar_workaround) == len(scalar_workaround)
2196
+
2197
+ def process_arg(
2198
+ x: T, ignore_types_override: Sequence[type[Any]] | None = None
2199
+ ) -> T | KeywordArg | Ignored:
2200
+ current_ignore_types = (
2201
+ ignore_types_override if ignore_types_override is not None else ignore_types
2202
+ )
2203
+ if isinstance(x, (float, int)) and x in inv_scalar_workaround:
2204
+ return KeywordArg(inv_scalar_workaround[x])
2205
+ if type(x) in current_ignore_types:
2206
+ return Ignored()
2207
+ if isinstance(x, list) and all(isinstance(y, Ignored) for y in x) and x:
2208
+ return Ignored()
2209
+ return x
2210
+
2211
+ argnum = itertools.count()
2212
+
2213
+ class Converter(torch.fx.Interpreter):
2214
+ # pyrefly: ignore [bad-override]
2215
+ call_method = _not_implemented
2216
+ # pyrefly: ignore [bad-override]
2217
+ call_module = _not_implemented
2218
+ # pyrefly: ignore [bad-override]
2219
+ get_attr = _not_implemented
2220
+
2221
+ # pyrefly: ignore [bad-override]
2222
+ def placeholder(
2223
+ self,
2224
+ target: str, # type: ignore[override]
2225
+ args: Sequence[Any],
2226
+ kwargs: Mapping[str, Any],
2227
+ ) -> ExclusiveKeywordArg | KeywordArg:
2228
+ n = next(argnum)
2229
+ if n < len(argnames):
2230
+ name = argnames[n]
2231
+ elif argnames:
2232
+ assert target.startswith("tangent")
2233
+ name = target
2234
+ else:
2235
+ target = re.sub(r"_\d+$", "", target) # de-mangle arg name
2236
+ name = target
2237
+ if name in exclusive_arg_names:
2238
+ return ExclusiveKeywordArg(name)
2239
+ else:
2240
+ return KeywordArg(name)
2241
+
2242
+ # pyrefly: ignore [bad-override]
2243
+ def call_function(
2244
+ self,
2245
+ target: str, # type: ignore[override]
2246
+ args: Sequence[Any],
2247
+ kwargs: Mapping[str, Any],
2248
+ ) -> PatternExpr:
2249
+ process_arg_fn = process_arg
2250
+ # Indexing is critical for matching getitem nodes, so we can't ignore int args here
2251
+ if target is operator.getitem:
2252
+
2253
+ def process_arg_fn_impl(
2254
+ x: T,
2255
+ ignore_types_override: Sequence[type[Any]] | None = tuple(
2256
+ t for t in ignore_types if t is not int
2257
+ ),
2258
+ ) -> T | KeywordArg | Ignored:
2259
+ return process_arg(x, ignore_types_override)
2260
+
2261
+ process_arg_fn = process_arg_fn_impl
2262
+
2263
+ args, kwargs = pytree.tree_map(process_arg_fn, (args, kwargs))
2264
+ if list in ignore_types:
2265
+ # Handle a burned in tensor size which are now [Ignored(), Ignored(), ...]
2266
+ args = [process_arg_fn(a) for a in args]
2267
+ kwargs = {k: process_arg_fn(a) for k, a in kwargs.items()}
2268
+ return CallFunction(target, *args, **kwargs)
2269
+
2270
+ def run_node(self, n: torch.fx.Node) -> Any:
2271
+ rv = super().run_node(n)
2272
+ if n.op == "output" and isinstance(rv, tuple):
2273
+ args = n.args[0]
2274
+ assert isinstance(args, Collection)
2275
+ assert len(rv) == len(args)
2276
+ for r, arg in zip(rv, args):
2277
+ # pyrefly: ignore [missing-attribute]
2278
+ r.users = len(arg.users)
2279
+ else:
2280
+ rv.users = len(n.users)
2281
+ return rv
2282
+
2283
+ assert isinstance(gm, torch.fx.GraphModule)
2284
+ pattern = Converter(gm).run()
2285
+ if not isinstance(pattern, PatternExpr):
2286
+ return MultiOutputPattern(pytree.tree_leaves(pattern))
2287
+ return pattern
2288
+
2289
+
2290
+ @torch.no_grad()
2291
+ def fwd_only(
2292
+ fn: Callable[..., Any],
2293
+ args: Sequence[Any],
2294
+ *,
2295
+ run_functional_passes: bool = True,
2296
+ get_decomp_fn: Callable[..., Any] = select_decomp_table,
2297
+ ) -> torch.fx.GraphModule:
2298
+ """Build a normalized inference graph, for use with fx_to_pattern"""
2299
+ # TODO - look into using aot autograd, asserting no mutating ops here
2300
+ with enable_python_dispatcher(), preserve_node_meta():
2301
+ gm = make_fx(fn, get_decomp_fn(), tracing_mode="real")(*args)
2302
+
2303
+ from .fx_passes.post_grad import remove_noop_ops
2304
+
2305
+ if run_functional_passes:
2306
+ remove_noop_ops(gm.graph)
2307
+
2308
+ # NOTE: applying early_patterns to user patterns cause
2309
+ # duplicate patterns being found in vllm. Check
2310
+ # https://github.com/pytorch/pytorch/pull/170649#issuecomment-3693427775
2311
+ # for more details.
2312
+ # from .fx_passes.joint_graph import early_patterns
2313
+ # early_patterns.apply(gm.graph)
2314
+
2315
+ gm.graph.eliminate_dead_code()
2316
+
2317
+ gm.recompile()
2318
+ return gm
2319
+
2320
+
2321
+ @torch.enable_grad()
2322
+ def joint_fwd_bwd(
2323
+ fn: Callable[..., Any],
2324
+ args: Sequence[Any],
2325
+ *,
2326
+ get_decomp_fn: Callable[..., Any] = select_decomp_table,
2327
+ ) -> torch.fx.GraphModule:
2328
+ """Build a normalized training graph, for use with fx_to_pattern"""
2329
+ gm: torch.fx.GraphModule | None = None
2330
+
2331
+ def record_joint_graph(
2332
+ joint_graph: torch.fx.GraphModule, inputs: Sequence[Any], **kwargs: Any
2333
+ ) -> tuple[torch.fx.GraphModule, torch.fx.GraphModule]:
2334
+ nonlocal gm
2335
+ assert not gm
2336
+ gm = clone_graph(joint_graph)
2337
+ return default_partition(joint_graph, inputs, **kwargs)
2338
+
2339
+ with torch._guards.tracing(None):
2340
+ aot_function(
2341
+ fn,
2342
+ # pyrefly: ignore[bad-argument-type]
2343
+ lambda gm, example_inputs: make_boxed_func(gm),
2344
+ partition_fn=record_joint_graph,
2345
+ decompositions=get_decomp_fn(),
2346
+ keep_inference_input_mutations=True,
2347
+ enable_log=False,
2348
+ )(*args)
2349
+ assert gm
2350
+
2351
+ from .fx_passes.post_grad import remove_noop_ops
2352
+
2353
+ remove_noop_ops(gm.graph)
2354
+
2355
+ from .fx_passes.joint_graph import early_patterns
2356
+
2357
+ early_patterns.apply(gm.graph)
2358
+
2359
+ # remove in/out specs
2360
+ gm.graph._codegen = torch.fx.graph.CodeGen()
2361
+ gm.graph.eliminate_dead_code()
2362
+ gm.recompile()
2363
+ return gm
2364
+
2365
+
2366
+ def _args(n: torch.fx.Node) -> list[torch.fx.node.Argument]:
2367
+ args: list[torch.fx.node.Argument] = []
2368
+ torch.fx.map_arg((n.args, n.kwargs), args.append)
2369
+ return args
2370
+
2371
+
2372
+ def stable_topological_sort(graph: torch.fx.Graph) -> None:
2373
+ # Nodes are in exactly one of these three collections:
2374
+
2375
+ # - Nodes in `pending` are waiting to be processed (in reverse order):
2376
+ pending = list(reversed(graph.nodes))
2377
+
2378
+ # - Nodes in `ready` have been processed and are already in the correct
2379
+ # order.
2380
+ ready = OrderedSet[torch.fx.Node]()
2381
+
2382
+ # - `waiting` is a mapping from a dependency to nodes which depend on that
2383
+ # dependency.
2384
+ waiting = defaultdict(list)
2385
+
2386
+ # The cursor indicates the last processed node so we can add new nodes
2387
+ # after it.
2388
+ cursor = None
2389
+ while pending:
2390
+ node = pending.pop()
2391
+ waiting_for = [x for x in _args(node) if x not in ready]
2392
+ if waiting_for:
2393
+ # We have unprocessed input nodes. Might as well wait for the last
2394
+ # arg so an already sorted list will only recheck this node once.
2395
+ waiting[waiting_for[-1]].append(node)
2396
+ else:
2397
+ ready.add(node)
2398
+ if cursor and cursor.next is not node:
2399
+ cursor.append(node)
2400
+ cursor = node
2401
+ # Mark the nodes that have been waiting for this node to finish as
2402
+ # ready to check again.
2403
+ pending.extend(reversed(waiting.pop(node, ())))
2404
+
2405
+ assert not waiting and len(ready) == len(graph.nodes)
2406
+
2407
+
2408
+ def init_once_fakemode(fn: Callable[..., Any]) -> Callable[..., Any]:
2409
+ """Wrapper around lazy init functions in fx_passes/"""
2410
+
2411
+ _fn_params = inspect.signature(fn).parameters
2412
+
2413
+ @functools.cache
2414
+ @functools.wraps(fn)
2415
+ def lazy_init(
2416
+ input_device: Any | None = None,
2417
+ get_decomp_fn: Callable[
2418
+ ..., dict[Any, Callable[..., Any]]
2419
+ ] = select_decomp_table,
2420
+ ) -> Any:
2421
+ counters_ref = counters[backend].copy()
2422
+
2423
+ kwargs: dict[str, Any] = {}
2424
+ if "input_device" in _fn_params:
2425
+ kwargs["input_device"] = input_device
2426
+ if "get_decomp_fn" in _fn_params:
2427
+ kwargs["get_decomp_fn"] = get_decomp_fn
2428
+
2429
+ with torch._guards.tracing(None), unset_fake_temporarily(), FakeTensorMode():
2430
+ result = fn(**kwargs)
2431
+
2432
+ # clear view matches encountered during tracing
2433
+ counters[backend] = counters_ref
2434
+
2435
+ return result
2436
+
2437
+ return lazy_init
2438
+
2439
+
2440
+ def config_flag(name: str) -> Callable[[Match], Any]:
2441
+ """Function for extra_check to put pass behind a flag"""
2442
+
2443
+ def flag_check(match: Match) -> Any:
2444
+ return getattr(config, name)
2445
+
2446
+ return flag_check
2447
+
2448
+
2449
+ def clone_graph(input_graph: torch.fx.GraphModule) -> torch.fx.GraphModule:
2450
+ class CopyGraph(Transformer):
2451
+ def run_node(self, old_node: torch.fx.Node) -> torch.fx.Node:
2452
+ new_node = super().run_node(old_node)
2453
+ if isinstance(new_node, torch.fx.Proxy):
2454
+ new_node.node.meta.update(old_node.meta)
2455
+ new_node.node.name = self.new_graph._graph_namespace.create_name(
2456
+ old_node.name, None
2457
+ )
2458
+
2459
+ return new_node
2460
+
2461
+ return CopyGraph(input_graph).transform()
2462
+
2463
+
2464
+ # TODO: remove in follow up diff, used internally
2465
+ _seen_patterns: OrderedSet[str] = OrderedSet()
2466
+
2467
+
2468
+ def get_arg_value(
2469
+ node: torch.fx.Node, arg_number: int, kwarg_name: str | None = None
2470
+ ) -> Any:
2471
+ if len(node.args) > arg_number:
2472
+ return node.args[arg_number]
2473
+ elif kwarg_name is None:
2474
+ return None
2475
+ else:
2476
+ return node.kwargs.get(kwarg_name)
2477
+
2478
+
2479
+ def filter_nodes(nodes: Iterable[torch.fx.Node], fn: Any) -> list[torch.fx.Node]:
2480
+ fns = [fn]
2481
+ if isinstance(fn, torch._ops.OpOverloadPacket):
2482
+ fns.extend([getattr(fn, overload) for overload in fn.overloads()])
2483
+
2484
+ return [node for node in nodes if node.target in fns]
2485
+
2486
+
2487
+ def extract_target(node: torch.fx.Node) -> torch.fx.node.Target:
2488
+ """For call_function and call_method, we directly use the target function;
2489
+ For call_module, the target is string, and we treat the module class
2490
+ as a function.
2491
+ """
2492
+ if node.op == "call_module":
2493
+ assert isinstance(node.target, str)
2494
+ return _get_attr(node.graph.owning_module, node.target).__class__
2495
+ return node.target
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/quantized_lowerings.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Any
3
+
4
+ import torch
5
+ from torch._inductor.kernel.mm_common import mm_args
6
+
7
+ from . import config, lowering
8
+ from .codegen.cpp_gemm_template import CppGemmTemplate, CppWoqInt4GemmTemplate
9
+ from .codegen.cpp_utils import create_epilogue_with_attr
10
+ from .lowering import expand, register_lowering
11
+ from .mkldnn_ir import WeightInt4PackMatmul
12
+ from .select_algorithm import (
13
+ autotune_select_algorithm,
14
+ ExternKernelChoice,
15
+ realize_inputs,
16
+ )
17
+ from .utils import use_aten_gemm_kernels, use_cpp_gemm_template
18
+ from .virtualized import V
19
+
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ aten__weight_int8pack_mm = ExternKernelChoice(
24
+ torch._weight_int8pack_mm, "at::_weight_int8pack_mm", has_out_variant=False
25
+ )
26
+
27
+ aten__weight_int4pack_mm_cpu = ExternKernelChoice(
28
+ torch.ops.quantized.int4mm_packed_weight_cpu,
29
+ "at::native::_weight_int4pack_mm_cpu_tensor",
30
+ has_out_variant=False,
31
+ kernel_creator=WeightInt4PackMatmul.create,
32
+ )
33
+
34
+ quantized = torch.ops.quantized
35
+ _quantized = torch.ops._quantized
36
+ aten = torch.ops.aten
37
+
38
+
39
+ def register_quantized_ops() -> None:
40
+ lowering.add_needs_realized_inputs(
41
+ [
42
+ quantized.max_pool2d,
43
+ _quantized.wrapped_fbgemm_pack_gemm_matrix_fp16,
44
+ _quantized.wrapped_fbgemm_linear_fp16_weight,
45
+ ]
46
+ )
47
+ lowering.make_fallback(quantized.max_pool2d)
48
+ lowering.make_fallback(_quantized.wrapped_fbgemm_pack_gemm_matrix_fp16)
49
+ lowering.make_fallback(_quantized.wrapped_fbgemm_linear_fp16_weight)
50
+
51
+
52
+ def register_woq_mm_ops() -> None:
53
+ @register_lowering(aten._weight_int8pack_mm, type_promotion_kind=None) # type: ignore[misc]
54
+ def int8pack_mm(
55
+ input: torch.Tensor,
56
+ weight: torch.Tensor,
57
+ scale: torch.Tensor,
58
+ *,
59
+ layout: Any = None,
60
+ ) -> Any:
61
+ _, _, _, layout, mat1, mat2 = mm_args(
62
+ input, weight, layout=layout, mat2_transposed=True
63
+ )
64
+ assert (
65
+ mat1.get_dtype() in [torch.bfloat16, torch.float16, torch.float]
66
+ and mat2.get_dtype() == torch.int8
67
+ )
68
+ aten_layout = layout
69
+
70
+ # options to tune from
71
+ choices = (
72
+ [aten__weight_int8pack_mm.bind((mat1, mat2, scale), aten_layout)]
73
+ if use_aten_gemm_kernels()
74
+ else []
75
+ )
76
+
77
+ # scale is applied as an epilogue, and the scale tensor is expanded (with a view op)
78
+ # for broadcasting, as it's 1D.
79
+ def _mul_epilogue(buf: torch.Tensor) -> Any:
80
+ return create_epilogue_with_attr(
81
+ buf, "mul", other=realize_inputs(expand(scale, layout.size))
82
+ )
83
+
84
+ if use_cpp_gemm_template(aten_layout, mat1, mat2, mat2_transposed=True):
85
+ CppGemmTemplate.add_choices(
86
+ choices,
87
+ aten_layout,
88
+ [mat1, mat2, scale],
89
+ trans_w=True,
90
+ epilogue_creator=_mul_epilogue, # type: ignore[arg-type]
91
+ )
92
+
93
+ node, _ = autotune_select_algorithm(
94
+ "_weight_int8pack_mm", choices, [mat1, mat2, scale], aten_layout
95
+ )
96
+ return node
97
+
98
+ @register_lowering(aten._weight_int4pack_mm_for_cpu, type_promotion_kind=None) # type: ignore[misc]
99
+ def int4pack_mm_cpu(
100
+ input: torch.Tensor,
101
+ weight: torch.Tensor,
102
+ qGroupSize: int,
103
+ qScaleAndZeros: torch.Tensor,
104
+ *,
105
+ layout: Any = None,
106
+ ) -> Any:
107
+ _, _, _, layout, mat1, mat2 = mm_args(
108
+ input, weight, layout=layout, use_4x2_dim=True, mat2_transposed=True
109
+ )
110
+ assert (
111
+ mat1.get_dtype() in [torch.bfloat16, torch.float16, torch.float]
112
+ and mat2.get_dtype() == torch.uint8
113
+ )
114
+ group_size = V.graph.add_tensor_constant(
115
+ torch.tensor(qGroupSize, dtype=torch.int64), name=None
116
+ )
117
+ aten_layout = layout
118
+
119
+ # options to tune from
120
+ choices = (
121
+ [
122
+ aten__weight_int4pack_mm_cpu.bind(
123
+ (mat1, mat2, group_size, qScaleAndZeros), aten_layout
124
+ )
125
+ ]
126
+ if use_aten_gemm_kernels()
127
+ else []
128
+ )
129
+ if (
130
+ (config.max_autotune or config.max_autotune_gemm)
131
+ and use_cpp_gemm_template(
132
+ aten_layout,
133
+ mat1,
134
+ mat2,
135
+ mat2_transposed=True,
136
+ is_woq_int4=True,
137
+ q_group_size=qGroupSize,
138
+ )
139
+ and mat2.get_layout().is_contiguous()
140
+ ):
141
+ # pyrefly: ignore [bad-specialization, missing-attribute, not-a-type]
142
+ CppWoqInt4GemmTemplate[qGroupSize].add_choices(
143
+ choices,
144
+ aten_layout,
145
+ [mat1, mat2, group_size, qScaleAndZeros],
146
+ )
147
+
148
+ # define functions to generate example inputs for weight and group size
149
+ # otherwise, autotuner generates example inputs of all zeros for them
150
+ def get_example_weight(x: torch._inductor.ir.IRNode) -> torch.Tensor:
151
+ assert x.get_layout().is_contiguous()
152
+ shape = x.get_size()
153
+ device = x.get_device()
154
+ return torch.randint(0, 255, shape, dtype=torch.uint8, device=device)
155
+
156
+ input_gen_fns = {
157
+ 1: get_example_weight, # packed weight
158
+ 2: lambda x: V.graph.constants[x.get_name()], # group size
159
+ }
160
+
161
+ node, _ = autotune_select_algorithm(
162
+ "_weight_int4pack_mm_for_cpu",
163
+ choices,
164
+ [mat1, mat2, group_size, qScaleAndZeros],
165
+ aten_layout,
166
+ input_gen_fns=input_gen_fns,
167
+ )
168
+ return node
169
+
170
+ lowering.make_fallback(aten._dyn_quant_matmul_4bit)
171
+ lowering.make_fallback(aten._dyn_quant_pack_4bit_weight)
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_cache.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import collections
5
+ import dataclasses
6
+ import functools
7
+ import json
8
+ import logging
9
+ import os
10
+ import sys
11
+ import typing
12
+ from abc import abstractmethod
13
+ from typing import Any, Generic, TypeAlias, TypeVar, Union
14
+ from typing_extensions import override
15
+
16
+ from torch._dynamo.utils import dynamo_timed
17
+ from torch._inductor import config
18
+ from torch.monitor import _WaitCounter
19
+
20
+
21
+ if typing.TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
24
+
25
+ try:
26
+ import redis
27
+ except ImportError:
28
+ redis = None # type: ignore[assignment]
29
+
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+
34
+ if config.is_fbcode():
35
+ from rfe.scubadata.scubadata_py3 import ( # type: ignore[import-not-found]
36
+ Sample as Sample_,
37
+ )
38
+
39
+ Sample: TypeAlias = Sample_
40
+ else:
41
+ Sample: TypeAlias = type[object] # type: ignore[misc,no-redef]
42
+
43
+
44
+ _T = TypeVar("_T")
45
+ _U = TypeVar("_U")
46
+
47
+
48
+ remote_fx_cache_get_timed = functools.partial(
49
+ dynamo_timed,
50
+ "FbRemoteFxGraphCache.get",
51
+ phase_name="remote_fx_graph_cache_get",
52
+ log_pt2_compile_event=False,
53
+ dynamo_compile_column_us="remote_fx_graph_cache_get_time_us",
54
+ log_waitcounter=True,
55
+ )
56
+ remote_fx_cache_put_timed = functools.partial(
57
+ dynamo_timed,
58
+ "FbRemoteFxGraphCache.put",
59
+ phase_name="remote_fx_graph_cache_put",
60
+ log_pt2_compile_event=False,
61
+ dynamo_compile_column_us="remote_fx_graph_cache_put_time_us",
62
+ log_waitcounter=True,
63
+ )
64
+
65
+
66
+ class RemoteCacheBackend(Generic[_T]):
67
+ """
68
+ A backend implementation for accessing a remote/distributed cache. Only
69
+ works with bytes in/out. For structured data use a RemoteCache.
70
+ """
71
+
72
+ def __init__(self) -> None:
73
+ self._name = f"backend:{type(self).__name__}"
74
+
75
+ @abstractmethod
76
+ def _get(self, key: str) -> _T | None:
77
+ pass
78
+
79
+ @abstractmethod
80
+ def _put(self, key: str, data: _T) -> None:
81
+ pass
82
+
83
+ def get(self, key: str) -> _T | None:
84
+ try:
85
+ value = self._get(key)
86
+ cache_stats.get(self._name, value)
87
+ except Exception:
88
+ cache_stats.exception(self._name)
89
+ raise
90
+ return value
91
+
92
+ def put(self, key: str, data: _T) -> None:
93
+ try:
94
+ self._put(key, data)
95
+ cache_stats.put(self._name)
96
+ except Exception:
97
+ cache_stats.exception(self._name)
98
+ raise
99
+
100
+
101
+ # Serde that encodes from _T to _U and decodes from _U to _T.
102
+ class RemoteCacheSerde(Generic[_T, _U]):
103
+ @abstractmethod
104
+ def encode(self, data: _T) -> _U:
105
+ pass
106
+
107
+ @abstractmethod
108
+ def decode(self, data: _U) -> _T:
109
+ pass
110
+
111
+
112
+ JsonDataTy: TypeAlias = Union[ # noqa: UP007
113
+ int, float, str, bool, dict[str, "JsonDataTy"], list["JsonDataTy"], None
114
+ ]
115
+
116
+
117
+ class RemoteCacheJsonSerde(RemoteCacheSerde[JsonDataTy, bytes]):
118
+ def encode(self, data: JsonDataTy) -> bytes:
119
+ return bytes(json.dumps(data), "ascii")
120
+
121
+ def decode(self, data: bytes) -> JsonDataTy:
122
+ return json.loads(data)
123
+
124
+
125
+ class RemoteCachePassthroughSerde(RemoteCacheSerde[_T, _T]):
126
+ def encode(self, data: _T) -> _T:
127
+ return data
128
+
129
+ def decode(self, data: _T) -> _T:
130
+ return data
131
+
132
+
133
+ # This class is the top of a RemoteCache. A RemoteCache is fundamentally made of
134
+ # three parts:
135
+ #
136
+ # 1. The controller (this class).
137
+ # 2. A serializer/deserializer (instance of RemoteCacheSerde).
138
+ # 3. A backend (instance of RemoteCacheBackend).
139
+ #
140
+ # To write (`put`), the RemoteCache takes data, uses the RemoteCacheSerde to
141
+ # convert it for the backend and passes it to the backend.
142
+ #
143
+ # Conversely when reading (`get`), the RemoteCache takes data from the backend,
144
+ # uses the RemoteCacheSerde to convert it and returns it.
145
+ #
146
+ # The RemoteCacheBackend is generic on _U - which is the type of data the
147
+ # backend can directly cache (usually `bytes`).
148
+ #
149
+ # The RemoteCacheSerde is responsible for converting between _T (the type of
150
+ # data the RemoteCache accepts in `put` and returns in `get`) and _U.
151
+ #
152
+ # When instantiating a RemoteCache you should override, not directly create a
153
+ # RemoteCache. The reason is that when logging cache use (`TORCH_LOGS=cache`) we
154
+ # use the concrete type of the RemoteCache as the reported cache. See
155
+ # RemoteFxGraphCache below as an example.
156
+ class RemoteCache(Generic[_T]):
157
+ backend_override_cls: Callable[[], RemoteCacheBackend[Any]] | None = None
158
+
159
+ def __init__(
160
+ self, backend: RemoteCacheBackend[_U], serde: RemoteCacheSerde[_T, _U]
161
+ ) -> None:
162
+ # Support for testing to mock out the backend on a class-by-class basis.
163
+ if (override_cls := self.__class__.backend_override_cls) is not None:
164
+ self.backend = override_cls()
165
+ else:
166
+ self.backend = backend
167
+ # pyrefly: ignore [invalid-type-var]
168
+ self.serde = serde
169
+
170
+ # See if the cache contains `key`. Returns `None` if the value is not
171
+ # present in the cache.
172
+ def get(self, key: str) -> _T | None:
173
+ with _WaitCounter("pytorch.remote_cache.get").guard():
174
+ sample = self._create_sample()
175
+ try:
176
+ result = self._get(key, sample)
177
+ cache_stats.get(type(self).__name__, result)
178
+ except Exception as e:
179
+ cache_stats.exception(type(self).__name__)
180
+ if sample:
181
+ sample.fail_reason = str(e)
182
+ raise
183
+ finally:
184
+ self._log_sample(sample)
185
+ return result
186
+
187
+ # Add `value` to the cache with the key `key`. Note that `None` is not a
188
+ # valid value even if _T supports it (because you can't tell the difference
189
+ # between `None` and a missing cache entry).
190
+ def put(self, key: str, value: _T) -> None:
191
+ with _WaitCounter("pytorch.remote_cache.put").guard():
192
+ assert value is not None
193
+ sample = self._create_sample()
194
+ try:
195
+ self._put(key, value, sample)
196
+ cache_stats.put(type(self).__name__)
197
+ except Exception as e:
198
+ cache_stats.exception(type(self).__name__)
199
+ if sample:
200
+ sample.fail_reason = str(e)
201
+ raise
202
+ finally:
203
+ self._log_sample(sample)
204
+
205
+ # Used to convert data from the cache into structured data.
206
+ def _decode(self, data: _U, sample: Sample | None) -> _T: # type: ignore[override]
207
+ return self.serde.decode(data) # type: ignore[arg-type]
208
+
209
+ # Used to convert structured data into data for the cache.
210
+ def _encode(self, value: _T, sample: Sample | None) -> object: # returns _U
211
+ return self.serde.encode(value)
212
+
213
+ # Get structured data from the cache.
214
+ # Separate from `get` so that it can be overridden.
215
+ def _get(self, key: str, sample: Sample | None) -> _T | None:
216
+ if data := self._backend_get(key):
217
+ return self._decode(data, sample)
218
+ return None
219
+
220
+ # Get unstructured data from the cache.
221
+ # Separate from `get` so that it can be overridden.
222
+ # Returns _U - but we aren't actually generic on _U
223
+ def _backend_get(self, key: str) -> object:
224
+ return self.backend.get(key)
225
+
226
+ # Put structured data into the cache.
227
+ # Separate from `put` so that it can be overridden.
228
+ def _put(self, key: str, value: _T, sample: Sample | None) -> None:
229
+ data = self._encode(value, sample)
230
+ self._backend_put(key, data)
231
+
232
+ # Put unstructured data into the cache.
233
+ # Separate from `put` so that it can be overridden.
234
+ # Takes data: _U - but we aren't actually generic on _U
235
+ def _backend_put(self, key: str, data: object) -> None:
236
+ self.backend.put(key, data)
237
+
238
+ # Create a logging Sample - used with internal loggers to monitor cache
239
+ # effectiveness.
240
+ def _create_sample(self) -> Sample | None:
241
+ return None
242
+
243
+ # Write the logging Sample to the logger.
244
+ def _log_sample(self, sample: Sample | None) -> None:
245
+ pass
246
+
247
+
248
+ class RedisRemoteCacheBackend(RemoteCacheBackend[bytes]):
249
+ """
250
+ A Redis implementation of a remote/distributed cache.
251
+ """
252
+
253
+ # pyrefly: ignore [missing-attribute]
254
+ _redis: redis.Redis | None = None
255
+
256
+ def __init__(self, cache_id: str) -> None:
257
+ super().__init__()
258
+ if not redis:
259
+ raise RuntimeError("redis not available but required for remote cache")
260
+
261
+ if "TORCHINDUCTOR_REDIS_URL" in os.environ:
262
+ self._redis = redis.Redis.from_url(os.environ["TORCHINDUCTOR_REDIS_URL"])
263
+ else:
264
+ self._redis = redis.Redis(
265
+ host=os.environ.get("TORCHINDUCTOR_REDIS_HOST", "localhost"),
266
+ port=int(os.environ.get("TORCHINDUCTOR_REDIS_PORT", 6379)),
267
+ )
268
+
269
+ @override
270
+ def _get(self, key: str) -> bytes | None:
271
+ if not self._redis:
272
+ # Either redis wasn't found or we already had some trouble...
273
+ return None
274
+
275
+ try:
276
+ # pyrefly: ignore [missing-attribute]
277
+ value = self._redis.get(key)
278
+ # pyrefly: ignore [missing-attribute]
279
+ except redis.exceptions.ConnectionError:
280
+ # Redis is lazy and doesn't actually attempt to connect until the
281
+ # first use. Mark is as unavailable now.
282
+ self._redis = None
283
+ return None
284
+
285
+ # In theory redis.get() can return an Awaitable as well...
286
+ assert value is None or isinstance(value, bytes)
287
+ return value
288
+
289
+ @override
290
+ def _put(self, key: str, data: bytes) -> None:
291
+ if not self._redis:
292
+ # Either redis wasn't found or we already had some trouble...
293
+ return
294
+
295
+ try:
296
+ # pyrefly: ignore [missing-attribute]
297
+ self._redis.set(key, data)
298
+ # pyrefly: ignore [missing-attribute]
299
+ except redis.exceptions.ConnectionError:
300
+ # Redis is lazy and doesn't actually attempt to connect until the
301
+ # first use. Mark is as unavailable now.
302
+ self._redis = None
303
+
304
+
305
+ class RedisRemoteCache(RemoteCache[JsonDataTy]):
306
+ def __init__(self, cache_id: str) -> None:
307
+ # Special test handling: If we're just going to override the backend
308
+ # anyway don't require redis
309
+ if self.__class__.backend_override_cls:
310
+ # This is totally bogus but it works for now...
311
+ backend = typing.cast(RemoteCacheBackend[bytes], None)
312
+ else:
313
+ backend = RedisRemoteCacheBackend(cache_id)
314
+ serde = RemoteCacheJsonSerde()
315
+ super().__init__(backend, serde)
316
+ version = 1 # consistency between various types of keys
317
+ self._key_fmt = f"pt2:{cache_id}::{{key}}:c{version}"
318
+
319
+ def _get_key(self, key: str) -> str:
320
+ return self._key_fmt.format(key=key)
321
+
322
+ @override
323
+ def _get(self, key: str, sample: Sample | None) -> JsonDataTy | None:
324
+ key = self._get_key(key)
325
+ return super()._get(key, sample)
326
+
327
+ @override
328
+ def _put(self, key: str, value: JsonDataTy, sample: Sample | None) -> None:
329
+ key = self._get_key(key)
330
+ super()._put(key, value, sample)
331
+
332
+
333
+ class RemoteAutotuneCache(RedisRemoteCache):
334
+ pass
335
+
336
+
337
+ class RemoteBundledAutotuneCache(RedisRemoteCache):
338
+ pass
339
+
340
+
341
+ class RemoteFxGraphCache(RedisRemoteCache):
342
+ pass
343
+
344
+
345
+ class RemoteAOTAutogradCache(RedisRemoteCache):
346
+ pass
347
+
348
+
349
+ class RemoteDynamoPGOCache(RedisRemoteCache):
350
+ pass
351
+
352
+
353
+ def create_cache(
354
+ key: str,
355
+ is_fbcode: bool,
356
+ fb_cache_cls: str,
357
+ oss_cache_cls: str,
358
+ ) -> RemoteCache[JsonDataTy] | None:
359
+ try:
360
+ if is_fbcode:
361
+ import torch._inductor.fb.remote_cache
362
+
363
+ cache_cls = getattr(torch._inductor.fb.remote_cache, fb_cache_cls)
364
+ return cache_cls(key)
365
+ else:
366
+ this_module = sys.modules[__name__]
367
+
368
+ cache_cls = getattr(this_module, oss_cache_cls)
369
+ return cache_cls(key)
370
+
371
+ except Exception:
372
+ log.warning("Unable to create a remote cache", exc_info=True)
373
+ return None
374
+
375
+
376
+ # Some simple stat capture
377
+ @dataclasses.dataclass
378
+ class _CacheStat:
379
+ miss: int = 0
380
+ hit: int = 0
381
+ put: int = 0
382
+ exception: int = 0
383
+
384
+ def __str__(self) -> str:
385
+ return f"{{hit: {self.hit}, miss: {self.miss}, put: {self.put}, exception: {self.exception}}}"
386
+
387
+
388
+ class _CacheStats:
389
+ _stats: dict[str, _CacheStat]
390
+
391
+ def __init__(self) -> None:
392
+ self._stats = collections.defaultdict(_CacheStat)
393
+
394
+ def miss(self, name: str, count: int = 1) -> None:
395
+ self._stats[name].miss += count
396
+
397
+ def hit(self, name: str, count: int = 1) -> None:
398
+ self._stats[name].hit += count
399
+
400
+ def get(self, name: str, value: object | None) -> None:
401
+ if value is None:
402
+ self.miss(name)
403
+ else:
404
+ self.hit(name)
405
+
406
+ def put(self, name: str, count: int = 1) -> None:
407
+ self._stats[name].put += count
408
+
409
+ def exception(self, name: str, count: int = 1) -> None:
410
+ self._stats[name].exception += count
411
+
412
+
413
+ cache_stats = _CacheStats()
414
+
415
+
416
+ @atexit.register
417
+ def dump_cache_stats() -> None:
418
+ if not log.isEnabledFor(logging.INFO):
419
+ return
420
+
421
+ import io
422
+
423
+ out = io.StringIO()
424
+
425
+ if not cache_stats._stats:
426
+ print(" None", file=out)
427
+ else:
428
+ print(file=out)
429
+ for k, v in sorted(cache_stats._stats.items()):
430
+ print(f" {k}: {v}", file=out)
431
+
432
+ log.info("Cache Metrics:%s", out.getvalue())
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/remote_gemm_autotune_cache.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import TypeVar
3
+
4
+ import torch._inductor.config as config
5
+ from torch._inductor import ir
6
+
7
+
8
+ _T = TypeVar("_T")
9
+
10
+
11
+ def gen_best_config(mat1: ir.StorageBox, mat2: ir.StorageBox) -> asyncio.Task[_T]:
12
+ """
13
+ Generate the best GEMM autotune config for the given matrices.
14
+ """
15
+ if config.is_fbcode():
16
+ from torch._inductor.fb.remote_gemm_autotune_cache import gen_best_config
17
+
18
+ return gen_best_config(mat1, mat2)
19
+ else:
20
+ raise NotImplementedError("Function gen_best_config is not yet implemented")
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/rocm_multiarch_utils.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ROCm Multi-Architecture Support Utilities
3
+ Compile LLVM IR to multi-arch bundles that HIP can load automatically.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ import re
9
+ import subprocess
10
+
11
+ import torch
12
+ from torch.utils.cpp_extension import _join_rocm_home, ROCM_HOME
13
+
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ def get_rocm_compiler() -> str:
19
+ """
20
+ Get path to ROCm's clang compiler.
21
+ Uses PyTorch's ROCM_HOME detection.
22
+
23
+ Returns:
24
+ Path to clang compiler
25
+
26
+ Raises:
27
+ RuntimeError: If ROCm is not found
28
+ """
29
+ if ROCM_HOME is None:
30
+ raise RuntimeError(
31
+ "ROCm installation not found. "
32
+ "PyTorch was not built with ROCm support or ROCM_HOME is not set."
33
+ )
34
+
35
+ # ROCm's clang is at <ROCM_HOME>/llvm/bin/clang
36
+ clang_path = _join_rocm_home("llvm", "bin", "clang")
37
+
38
+ if not os.path.exists(clang_path):
39
+ raise RuntimeError(
40
+ f"ROCm clang not found at {clang_path}. ROCM_HOME is set to {ROCM_HOME}"
41
+ )
42
+
43
+ return clang_path
44
+
45
+
46
+ def get_rocm_bundler() -> str:
47
+ """
48
+ Get path to clang-offload-bundler.
49
+ Uses PyTorch's ROCM_HOME detection.
50
+
51
+ Returns:
52
+ Path to bundler
53
+
54
+ Raises:
55
+ RuntimeError: If bundler is not found
56
+ """
57
+ if ROCM_HOME is None:
58
+ raise RuntimeError(
59
+ "ROCm installation not found. "
60
+ "PyTorch was not built with ROCm support or ROCM_HOME is not set."
61
+ )
62
+
63
+ # Bundler is at <ROCM_HOME>/llvm/bin/clang-offload-bundler
64
+ bundler_path = _join_rocm_home("llvm", "bin", "clang-offload-bundler")
65
+
66
+ if not os.path.exists(bundler_path):
67
+ raise RuntimeError(
68
+ f"clang-offload-bundler not found at {bundler_path}. "
69
+ f"ROCM_HOME is set to {ROCM_HOME}"
70
+ )
71
+
72
+ return bundler_path
73
+
74
+
75
+ def get_rocm_target_archs() -> list[str]:
76
+ env_archs = os.environ.get("PYTORCH_ROCM_ARCH", "").strip()
77
+ if env_archs:
78
+ archs = [arch.strip() for arch in env_archs.replace(";", ",").split(",")]
79
+ archs = [arch for arch in archs if arch]
80
+ if archs:
81
+ # Ensure current device arch is included
82
+ if torch.cuda.is_available():
83
+ for dev_idx in range(torch.cuda.device_count()):
84
+ current_arch = torch.cuda.get_device_properties(
85
+ dev_idx
86
+ ).gcnArchName.split(":")[0]
87
+ if current_arch not in archs:
88
+ archs.append(current_arch)
89
+ return archs
90
+
91
+ try:
92
+ from torch._inductor import config
93
+
94
+ if hasattr(config, "rocm") and hasattr(config.rocm, "target_archs"):
95
+ archs = config.rocm.target_archs
96
+ if archs:
97
+ return archs
98
+
99
+ except Exception:
100
+ pass
101
+
102
+ return torch.cuda.get_arch_list()
103
+
104
+
105
+ def _sanitize_llvm_ir_for_rocm(llvm_ir_path: str) -> str:
106
+ """
107
+ Sanitize LLVM IR to be compatible with ROCm's clang.
108
+
109
+ Triton's LLVM (upstream) may emit attributes and metadata that ROCm's
110
+ older clang does not yet support. Only strips attributes confirmed to
111
+ cause parse errors — preserves all others to maintain correct codegen.
112
+
113
+ Currently strips:
114
+ - nocreateundeforpoison: function attribute (upstream LLVM, not in ROCm)
115
+ - dwarfAddressSpace: debug metadata field (upstream LLVM, not in ROCm)
116
+
117
+ Returns:
118
+ Path to sanitized .ll file, or original path if no changes needed.
119
+ """
120
+ with open(llvm_ir_path) as f:
121
+ content = f.read()
122
+
123
+ sanitized = content
124
+ sanitized = re.sub(r"\bnocreateundeforpoison\b\s*", "", sanitized)
125
+ sanitized = re.sub(r",\s*dwarfAddressSpace:\s*\d+", "", sanitized)
126
+
127
+ if sanitized == content:
128
+ return llvm_ir_path
129
+
130
+ sanitized_path = llvm_ir_path + ".sanitized.ll"
131
+ with open(sanitized_path, "w") as f:
132
+ f.write(sanitized)
133
+
134
+ log.debug(
135
+ "Sanitized LLVM IR for ROCm clang compatibility: %s -> %s",
136
+ llvm_ir_path,
137
+ sanitized_path,
138
+ )
139
+ return sanitized_path
140
+
141
+
142
+ def compile_llvm_ir_to_code_object(
143
+ llvm_ir_path: str, output_path: str, target_arch: str
144
+ ) -> bool:
145
+ """
146
+ Compile unbundled LLVM IR to a single-arch code object.
147
+
148
+ Args:
149
+ llvm_ir_path: Path to .ll file
150
+ output_path: Where to write .hsaco file
151
+ target_arch: Target architecture (e.g., 'gfx90a')
152
+
153
+ Returns:
154
+ True if successful
155
+ """
156
+ if not os.path.exists(llvm_ir_path):
157
+ return False
158
+
159
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
160
+
161
+ try:
162
+ clang = get_rocm_compiler()
163
+ except RuntimeError:
164
+ return False
165
+
166
+ # Sanitize LLVM IR to remove attributes unsupported by ROCm's clang
167
+ llvm_ir_path = _sanitize_llvm_ir_for_rocm(llvm_ir_path)
168
+
169
+ # Using clang and not hipcc since we are not compiling source code
170
+ # Instead we use the LLVM IR (.ll) provided by triton
171
+ cmd = [
172
+ clang,
173
+ "-target",
174
+ "amdgcn-amd-amdhsa",
175
+ f"-mcpu={target_arch}",
176
+ llvm_ir_path,
177
+ "-o",
178
+ output_path,
179
+ ]
180
+
181
+ try:
182
+ subprocess.run(cmd, capture_output=True, text=True, check=True)
183
+
184
+ if not os.path.exists(output_path):
185
+ return False
186
+
187
+ return True
188
+
189
+ except subprocess.CalledProcessError:
190
+ return False
191
+
192
+
193
+ def create_multiarch_bundle(code_objects: dict, output_bundle_path: str) -> bool:
194
+ """
195
+ Bundle multiple architecture code objects into a single multi-arch bundle.
196
+
197
+ Uses clang-offload-bundler to create a fat binary that HIP runtime can load.
198
+ The runtime automatically selects the correct architecture at load time.
199
+
200
+ Args:
201
+ code_objects: Dict mapping architecture to code object path
202
+ output_bundle_path: Path for output bundle
203
+
204
+ Returns:
205
+ True if successful
206
+ """
207
+ if not code_objects:
208
+ return False
209
+
210
+ os.makedirs(os.path.dirname(output_bundle_path), exist_ok=True)
211
+
212
+ try:
213
+ bundler = get_rocm_bundler()
214
+ except RuntimeError:
215
+ return False
216
+
217
+ # Build targets and inputs lists for clang-offload-bundler
218
+ targets = ["host-x86_64-unknown-linux-gnu"]
219
+
220
+ # We include a dummy host entry to satisfy the bundler format
221
+ inputs = ["/dev/null"]
222
+
223
+ for arch, path in sorted(code_objects.items()):
224
+ if not os.path.exists(path):
225
+ continue
226
+ # hipv4 = HIP version 4 code object format
227
+ # amdgcn-amd-amdhsa = target triple for ROCm/HSA runtime
228
+ # arch = specific GPU (gfx90a, gfx942, etc.)
229
+ targets.append(f"hipv4-amdgcn-amd-amdhsa--{arch}")
230
+ inputs.append(path)
231
+
232
+ if len(inputs) == 1: # Only host, no device code
233
+ return False
234
+
235
+ cmd = [
236
+ bundler,
237
+ "--type=o",
238
+ # CRITICAL: HIP runtime expects 4096-byte alignment for loading bundles
239
+ # Without this, hipModuleLoadData gives segmentation fault
240
+ "-bundle-align=4096", # CRITICAL: Required by HIP runtime!
241
+ f"--targets={','.join(targets)}",
242
+ ]
243
+
244
+ for input_file in inputs:
245
+ cmd.append(f"--input={input_file}")
246
+
247
+ cmd.append(f"--output={output_bundle_path}")
248
+
249
+ try:
250
+ subprocess.run(cmd, capture_output=True, text=True, check=True)
251
+
252
+ if not os.path.exists(output_bundle_path):
253
+ return False
254
+
255
+ return True
256
+
257
+ except subprocess.CalledProcessError:
258
+ return False
259
+
260
+
261
+ def compile_multiarch_bundle_from_llvm_ir(
262
+ llvm_ir_path: str, output_bundle_path: str, target_archs: list[str] | None = None
263
+ ) -> bool:
264
+ """
265
+ Complete workflow: LLVM IR → multiple code objects → bundle.
266
+
267
+ This is the main entry point for multi-arch compilation.
268
+
269
+ Args:
270
+ llvm_ir_path: Path to .ll file
271
+ output_bundle_path: Where to write bundle
272
+ target_archs: Optional list of architectures
273
+
274
+ Returns:
275
+ True if successful
276
+ """
277
+ if target_archs is None:
278
+ # Get architectures from environment variable or config
279
+ target_archs = get_rocm_target_archs()
280
+
281
+ # Step 1: Compile LLVM IR to code object for each architecture
282
+ code_objects = {}
283
+ temp_dir = os.path.dirname(output_bundle_path)
284
+ kernel_name = os.path.splitext(os.path.basename(llvm_ir_path))[0]
285
+
286
+ for arch in target_archs:
287
+ # Create temporary single-architecture code object
288
+ # Format: kernel_name_gfx90a.co, kernel_name_gfx942.co, etc.
289
+ co_path = os.path.join(temp_dir, f"{kernel_name}_{arch}.co")
290
+
291
+ # Compile with clang backend: LLVM IR → GPU machine code
292
+ if compile_llvm_ir_to_code_object(llvm_ir_path, co_path, arch):
293
+ code_objects[arch] = co_path
294
+
295
+ if not code_objects:
296
+ return False
297
+
298
+ # Step 2: Bundle all code objects together
299
+ # Uses clang-offload-bundler to create fat binary
300
+ success = create_multiarch_bundle(code_objects, output_bundle_path)
301
+
302
+ # Step 3: Clean up temporary single-arch code objects
303
+ # The bundle contains all the code, so intermediates are no longer needed
304
+ for co_path in code_objects.values():
305
+ try:
306
+ os.remove(co_path)
307
+ except Exception:
308
+ pass
309
+
310
+ return success
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/__init__.py ADDED
File without changes
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/autotune_cache.py ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PyTorch Inductor Autotuning Cache System
3
+
4
+ This module implements a caching system for autotuning configurations in PyTorch's Inductor compiler.
5
+ It provides mechanisms to store and retrieve optimal kernel configurations both locally and remotely,
6
+ which significantly speeds up compilation by reusing previously discovered optimal parameters.
7
+
8
+ The caching system includes:
9
+ - Local filesystem caching for individual machine reuse
10
+ - Remote caching for sharing optimizations across machines
11
+ - Bundled caching to efficiently store multiple related configurations
12
+ - Cache invalidation based on PyTorch versions and backend changes
13
+ - Serialization/deserialization support for worker processes
14
+
15
+ Key components:
16
+ - AutotuneCache: Main class for managing cache access and storage
17
+ - AutotuneCacheBundler: Bundles multiple cache entries for efficient storage
18
+ - LocalAutotuneCache: Handles filesystem-based caching
19
+ - _LocalAutotuneCacheBackend: Low-level file operations for cache storage
20
+ - AutotuneCacheArtifact: Integration with PyTorch's artifact system
21
+
22
+ This caching system is critical for performance as it eliminates the need to re-run
23
+ expensive autotuning operations when the same kernels are compiled multiple times.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import dataclasses
29
+ import hashlib
30
+ import logging
31
+ import os
32
+ import os.path
33
+ import re
34
+ from typing import Any, TYPE_CHECKING
35
+ from typing_extensions import override
36
+
37
+ import torch
38
+ from torch._inductor.runtime.runtime_utils import cache_dir
39
+ from torch.compiler._cache import (
40
+ CacheArtifact,
41
+ CacheArtifactFactory,
42
+ CacheArtifactManager,
43
+ )
44
+ from torch.utils._triton import has_triton
45
+
46
+ from ..remote_cache import (
47
+ create_cache,
48
+ JsonDataTy,
49
+ RemoteCache,
50
+ RemoteCacheBackend,
51
+ RemoteCacheJsonSerde,
52
+ )
53
+ from .triton_compat import Config, HAS_WARP_SPEC
54
+
55
+
56
+ if TYPE_CHECKING:
57
+ from ..remote_cache import Sample
58
+
59
+ log = logging.getLogger(__name__)
60
+
61
+
62
+ _InductorMetaTy = dict[str, object]
63
+
64
+
65
+ def inductor_meta_from_config() -> _InductorMetaTy:
66
+ from torch._inductor import config
67
+
68
+ backend_hash = None
69
+ if has_triton():
70
+ try:
71
+ backend_hash = torch.utils._triton.triton_hash_with_backend()
72
+ except RuntimeError:
73
+ # This can get the error:
74
+ # RuntimeError: 0 active drivers ([]). There should only be one.
75
+ pass
76
+
77
+ is_hip = None
78
+ if torch.version.hip is not None:
79
+ is_hip = True
80
+
81
+ return {
82
+ "autotune_local_cache": config.autotune_local_cache,
83
+ "autotune_remote_cache": config.autotune_remote_cache,
84
+ "backend_hash": backend_hash,
85
+ "bundled_autotune_remote_cache": config.bundled_autotune_remote_cache,
86
+ "coordinate_descent_tuning": config.coordinate_descent_tuning,
87
+ "is_fbcode": config.is_fbcode(),
88
+ "is_hip": is_hip,
89
+ }
90
+
91
+
92
+ @CacheArtifactFactory.register
93
+ class AutotuneCacheArtifact(CacheArtifact):
94
+ @override
95
+ def populate_cache(self) -> None:
96
+ autotune_cache = _LocalAutotuneCacheBackend()
97
+ key = os.path.join(cache_dir(), self.key)
98
+ autotune_cache._put(key, self.content)
99
+
100
+ @override
101
+ @staticmethod
102
+ def type() -> str:
103
+ return "autotune"
104
+
105
+ @override
106
+ @staticmethod
107
+ def encode(content: JsonDataTy) -> bytes:
108
+ assert not isinstance(content, bytes)
109
+ serde = RemoteCacheJsonSerde()
110
+ content_bytes = serde.encode(content)
111
+ assert isinstance(content_bytes, bytes)
112
+ return content_bytes
113
+
114
+
115
+ @dataclasses.dataclass
116
+ class AutotuneCache:
117
+ configs_hash: str
118
+ local_cache: tuple[RemoteCache[JsonDataTy], str] | None = None
119
+ remote_cache: tuple[RemoteCache[JsonDataTy], str] | None = None
120
+
121
+ # Create a AutotuneCache. Returns None if none of the caches can be used.
122
+ @staticmethod
123
+ def create(
124
+ inductor_meta: _InductorMetaTy, filename: str, configs_hash: str
125
+ ) -> AutotuneCache | None:
126
+ cache = AutotuneCache(configs_hash)
127
+ key = AutotuneCache._prepare_key(filename)
128
+
129
+ cache._setup_local_cache(inductor_meta, os.path.dirname(filename), key)
130
+ cache._setup_remote_autotune_cache(inductor_meta, key)
131
+ if cache.local_cache or cache.remote_cache:
132
+ return cache
133
+ else:
134
+ return None
135
+
136
+ @staticmethod
137
+ def _prepare_key(filename: str) -> str:
138
+ from torch.compiler import config as cconfig
139
+
140
+ # base of filename is already sha256 hash the source contents
141
+ key = f"{os.path.basename(filename)}:{cconfig.cache_key_tag}"
142
+ return hashlib.sha256(key.encode("utf-8")).hexdigest()
143
+
144
+ # Read the best config options from the most local cache and return it.
145
+ def _read(self) -> dict[str, JsonDataTy] | None:
146
+ if local_cache := self.local_cache:
147
+ cache, key = local_cache
148
+ if best_config := cache.get(key):
149
+ if isinstance(best_config, dict):
150
+ return best_config
151
+
152
+ if remote_cache := self.remote_cache:
153
+ cache, key = remote_cache
154
+ if best_config := cache.get(key):
155
+ if isinstance(best_config, dict):
156
+ return best_config
157
+
158
+ return None
159
+
160
+ # Read the best config options from the most local cache and figure out
161
+ # which `configs` represents that option.
162
+ def read_best(
163
+ self, inductor_meta: _InductorMetaTy, configs: list[Config]
164
+ ) -> Config | None:
165
+ if best := self._read():
166
+ return _load_cached_autotuning(
167
+ best, self.configs_hash, configs, inductor_meta
168
+ )
169
+ return None
170
+
171
+ # Set up local filesystem caching information
172
+ def _setup_local_cache(
173
+ self, inductor_meta: _InductorMetaTy, dirname: str, cache_key: str
174
+ ) -> None:
175
+ if not inductor_meta.get("autotune_local_cache", True):
176
+ return
177
+
178
+ from ..codecache import torch_key
179
+
180
+ """
181
+ [Note: torch_key in autotune cache key]
182
+ Include torch_key() in the cache key so that different versions
183
+ of torch result in cache invalidation. This is important in case
184
+ of changes to the best_config format or other code changes that
185
+ are not backward compatible w.r.t. the cache.
186
+ """
187
+ hasher = hashlib.sha256()
188
+ hasher.update(cache_key.encode("utf-8"))
189
+ hasher.update(torch_key())
190
+ updated_cache_key = hasher.hexdigest()
191
+
192
+ cache_filename = f"{dirname}/{updated_cache_key}.best_config"
193
+ local_cache = LocalAutotuneCache()
194
+ self.local_cache = (local_cache, cache_filename)
195
+
196
+ # Set up remote caching information
197
+ def _setup_remote_autotune_cache(
198
+ self, inductor_meta: _InductorMetaTy, cache_key: str
199
+ ) -> None:
200
+ if not _should_use_remote_autotune_cache(inductor_meta):
201
+ return
202
+
203
+ if (backend_hash := inductor_meta.get("backend_hash", None)) is None:
204
+ log.debug(
205
+ "backend_hash is not passed on the inductor_meta, unable to use autotune remote cache"
206
+ )
207
+ return
208
+ assert isinstance(backend_hash, str)
209
+
210
+ from ..codecache import torch_key
211
+
212
+ is_fbcode = bool(inductor_meta.get("is_fbcode", False))
213
+
214
+ salt = "autotune-best-config-v2"
215
+ # re: torch_key - see [Note: torch_key in autotune cache key]
216
+ key = torch_key().hex() + backend_hash + self.configs_hash + salt
217
+ key = hashlib.sha256(key.encode("utf-8")).hexdigest()
218
+
219
+ remote_cache = create_cache(
220
+ key,
221
+ is_fbcode,
222
+ "FbRemoteAutotuneCache",
223
+ "RemoteAutotuneCache",
224
+ )
225
+ if not remote_cache:
226
+ return
227
+
228
+ # Save the args passed to create_cache
229
+ # in case AutotuneCache needs to be pickled
230
+ self.remote_cache_full_key = key
231
+ self.is_fbcode = is_fbcode
232
+ self.remote_cache = (remote_cache, cache_key)
233
+
234
+ # The AutotuneCache may be serialized/deserialized if we're using
235
+ # AsyncCompile worker processes to run triton compilation.
236
+ # This is because AutotuneCache instances are created on the worker
237
+ # process, but we need to run AutotuneCache.save on the parent process
238
+ # when actually doing autotuning.
239
+ def __getstate__(self) -> dict[str, Any]:
240
+ # The remote cache handles themselves may not be serializable
241
+ # So clear it and reconstruct it on setstate
242
+ remote_cache = getattr(self, "remote_cache", None)
243
+ return {
244
+ **self.__dict__,
245
+ # Save the cache_key portion
246
+ "remote_cache": remote_cache and remote_cache[1],
247
+ }
248
+
249
+ def __setstate__(self, state: dict[str, Any]) -> None:
250
+ # Reconstruct the remote cache on the parent class
251
+ self.__dict__.update(state)
252
+ if self.remote_cache is not None:
253
+ assert isinstance(self.remote_cache, str)
254
+ assert hasattr(self, "remote_cache_full_key")
255
+ assert hasattr(self, "is_fbcode")
256
+ cache_key = self.remote_cache
257
+ remote_cache = create_cache(
258
+ self.remote_cache_full_key,
259
+ self.is_fbcode,
260
+ "FbRemoteAutotuneCache",
261
+ "RemoteAutotuneCache",
262
+ )
263
+ if remote_cache is not None:
264
+ self.remote_cache = (remote_cache, cache_key)
265
+ else:
266
+ log.warning("Warning, failed to recreate remote cache after pickling")
267
+ self.remote_cache = None
268
+
269
+ # Save the config in the caches
270
+ def save(
271
+ self,
272
+ config: Config,
273
+ time_taken_ns: int,
274
+ found_by_coordesc: bool = False,
275
+ triton_cache_hash: str | None = None,
276
+ ) -> None:
277
+ data = {
278
+ # pyrefly: ignore [missing-attribute]
279
+ **config.kwargs,
280
+ # pyrefly: ignore [missing-attribute]
281
+ "num_warps": config.num_warps,
282
+ # pyrefly: ignore [missing-attribute]
283
+ "num_stages": config.num_stages,
284
+ "configs_hash": self.configs_hash,
285
+ "found_by_coordesc": found_by_coordesc,
286
+ "time_taken_ms": time_taken_ns // 1000000, # Convert from NS to MS
287
+ "triton_cache_hash": triton_cache_hash,
288
+ }
289
+ # Save extra_options if present on the config. This allows third-party
290
+ # backends to store custom tuned options alongside the standard config.
291
+ if extra_options := getattr(config, "extra_options", None):
292
+ data["extra_options"] = extra_options
293
+ if HAS_WARP_SPEC:
294
+ data.update(
295
+ {
296
+ "num_consumer_groups": getattr(config, "num_consumer_groups", 0),
297
+ "num_buffers_warp_spec": getattr(
298
+ config, "num_buffers_warp_spec", 0
299
+ ),
300
+ }
301
+ )
302
+
303
+ if local_cache := self.local_cache:
304
+ cache, key = local_cache
305
+ # pyrefly: ignore [bad-argument-type]
306
+ cache.put(key, data)
307
+ # pyrefly: ignore [bad-argument-type]
308
+ AutotuneCacheBundler.put(key, data)
309
+ autotune_artifact_key = os.path.join(*key.split(os.sep)[-2:])
310
+ CacheArtifactManager.record_artifact(
311
+ AutotuneCacheArtifact.type(), autotune_artifact_key, data
312
+ )
313
+
314
+ if log.isEnabledFor(logging.DEBUG):
315
+ type_str = "coordesc" if found_by_coordesc else "heuristic"
316
+ log.debug("Save %s tuning result to %s", type_str, key)
317
+
318
+ if remote_cache := self.remote_cache:
319
+ cache, key = remote_cache
320
+ # pyrefly: ignore [bad-argument-type]
321
+ cache.put(key, data)
322
+
323
+
324
+ class _AutotuneCacheBundlerImpl:
325
+ """
326
+ Caches a set of LocalAutotuneCacheBackend entries together in a single
327
+ cache.
328
+ """
329
+
330
+ _key: str
331
+ _cache: RemoteCache[JsonDataTy]
332
+
333
+ # All known entries from LocalAutotuneCache.put()
334
+ _entries: dict[str, JsonDataTy]
335
+
336
+ def end_compile(self) -> None:
337
+ # TODO: Do we need to compute time_taken_ms and encode that somehow?
338
+ if self._entries:
339
+ self._cache.put(self._key, self._entries)
340
+
341
+ def put(self, basename: str, data: JsonDataTy) -> None:
342
+ # Do we need to worry about duplicates? We only have a single local fs
343
+ # entry - so probably not.
344
+ self._entries[basename] = data
345
+
346
+ def __init__(self, key: str, cache: RemoteCache[JsonDataTy]) -> None:
347
+ self._key = key
348
+ self._cache = cache
349
+ self._entries = {}
350
+
351
+ def sync(self) -> None:
352
+ # We don't currently use this - but we could async load starting at
353
+ # `begin_compile` and wait for the load to be finished here.
354
+ pass
355
+
356
+ @classmethod
357
+ def _should_use_bundled_autotune_remote_cache(
358
+ cls, inductor_meta: _InductorMetaTy
359
+ ) -> bool:
360
+ # The bundled autotune cache is only available if you've also got local
361
+ # caching enabled (because we feed the bundled data to the local cache).
362
+ if not inductor_meta.get("autotune_local_cache", True):
363
+ return False
364
+
365
+ # Check if the we're enabled via config
366
+ if (
367
+ bundled_autotune_remote_cache := inductor_meta.get(
368
+ "bundled_autotune_remote_cache"
369
+ )
370
+ ) is not None:
371
+ return bool(bundled_autotune_remote_cache)
372
+
373
+ if not cls._get_is_fbcode(inductor_meta):
374
+ return False
375
+ if torch._utils_internal.is_fb_unit_test():
376
+ return False
377
+ if inductor_meta.get("is_hip"):
378
+ return False
379
+
380
+ try:
381
+ from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION
382
+ except ModuleNotFoundError:
383
+ return False
384
+
385
+ jk = torch._utils_internal.justknobs_getval_int(
386
+ "pytorch/remote_cache:bundled_autotune_remote_cache_version"
387
+ )
388
+ return REMOTE_CACHE_VERSION >= jk
389
+
390
+ def _load_cache(self) -> bool:
391
+ from torch._inductor import codecache
392
+
393
+ # The single key is defined on construction of the cache.
394
+ entries = self._cache.get(self._key)
395
+ if entries is None or not isinstance(entries, dict):
396
+ # We couldn't load the cache - so mark _entries as non-None so we
397
+ # store local cache values.
398
+ return False
399
+
400
+ # Go through the entries we got from the cache and save them locally.
401
+ time_saved_ns = 0
402
+ for basename, data in entries.items():
403
+ # Reconstruct the final filename (see put())
404
+ root, ext = _splitext_nodot(basename)
405
+ _, _, filename = codecache.get_path(root, ext)
406
+ if isinstance(data, dict) and (tsns := data.get("time_saved_ns")):
407
+ time_saved_ns += int(tsns) # type: ignore[arg-type]
408
+ local_cache = LocalAutotuneCache()
409
+ local_cache.put(filename, data)
410
+
411
+ codecache.add_ephemeral_timeout_increase_for_distributed(time_saved_ns)
412
+
413
+ return True
414
+
415
+ @staticmethod
416
+ def _get_is_fbcode(inductor_meta: _InductorMetaTy) -> bool:
417
+ return bool(inductor_meta.get("is_fbcode", False))
418
+
419
+ @staticmethod
420
+ def _get_backend_hash(inductor_meta: _InductorMetaTy) -> str:
421
+ backend_hash = inductor_meta["backend_hash"]
422
+ assert isinstance(backend_hash, str)
423
+ return backend_hash
424
+
425
+
426
+ class AutotuneCacheBundler:
427
+ _bundler: _AutotuneCacheBundlerImpl | None = None
428
+
429
+ def __init__(self) -> None:
430
+ pass
431
+
432
+ # Call this before we start any autotune computation for an inductor python
433
+ # file. On a cache hit it copies the individual results into the local
434
+ # autotune caches.
435
+ @classmethod
436
+ def begin_compile(
437
+ cls,
438
+ inductor_meta: _InductorMetaTy,
439
+ *,
440
+ code: str | None = None,
441
+ code_hash: str | None = None,
442
+ ) -> None:
443
+ assert cls._bundler is None
444
+
445
+ if code is not None:
446
+ assert code_hash is None, "Cannot specify both code and code_hash"
447
+ code_hash = _comment_stripped_hash(code)
448
+ assert code_hash is not None
449
+
450
+ if not _AutotuneCacheBundlerImpl._should_use_bundled_autotune_remote_cache(
451
+ inductor_meta
452
+ ):
453
+ return
454
+
455
+ cache = create_cache(
456
+ "bundled-autotune-v1",
457
+ _AutotuneCacheBundlerImpl._get_is_fbcode(inductor_meta),
458
+ "FbRemoteBundledAutotuneCache",
459
+ "RemoteBundledAutotuneCache",
460
+ )
461
+ if not cache:
462
+ return
463
+
464
+ # We're starting a compilation phase. We have a cache key for the code
465
+ # we're compiling. We'll get the individual autotune bundles later (via
466
+ # self.put()). For now create the AutotuneCacheBundler and try to load
467
+ # from the cache.
468
+
469
+ salt = "bundled-autotune-best-configs-v1"
470
+ backend_hash = _AutotuneCacheBundlerImpl._get_backend_hash(inductor_meta)
471
+ # TODO: The autotune cache includes configs_hash in the key. The problem
472
+ # is that the configs_hash includes info from the individual pointwise()
473
+ # calls (size_hints, for example) which we can't know yet. I *think*
474
+ # that info is basically present in the `code_hash` (since it's a
475
+ # parameter to the pointwise decorator) - but is there other info we
476
+ # need to include from inductor_meta?
477
+ key = code_hash + backend_hash + salt
478
+ key = hashlib.sha256(key.encode("utf-8")).hexdigest()
479
+
480
+ bundler = _AutotuneCacheBundlerImpl(key, cache)
481
+ if not bundler._load_cache():
482
+ # We couldn't load from the cache - so save the data so we can store
483
+ # the saved autotunes.
484
+ cls._bundler = bundler
485
+
486
+ # If we get a cache hit don't bother saving any of the individual
487
+ # autotune results.
488
+
489
+ # Call this after all individual autotune results are finished for a
490
+ # inductor python file. If we gathered any individual results then we bundle
491
+ # those and put it into the cache.
492
+ @classmethod
493
+ def end_compile(cls) -> None:
494
+ if bundler := cls._bundler:
495
+ cls._bundler = None
496
+ bundler.end_compile()
497
+
498
+ @classmethod
499
+ def sync(cls) -> None:
500
+ if bundler := cls._bundler:
501
+ bundler.sync()
502
+
503
+ @classmethod
504
+ def put(cls, filename: str, data: JsonDataTy) -> None:
505
+ if bundler := cls._bundler:
506
+ # The filename comes in as something like
507
+ # "/tmp/tmp{random}/{aa}/{basename}.py" (where aa is
508
+ # basename[1:3]). Strip it down and make sure that it looks like a path
509
+ # we could reconstruct (because it's possible for the caller to
510
+ # customize the path).
511
+ basename = os.path.basename(filename)
512
+
513
+ # TODO: check cache_dir() vs filename, then strip dirname
514
+ bundler.put(basename, data)
515
+
516
+
517
+ # Remove the comments from the code (which include things like run ids and file
518
+ # paths) and then hash the result.
519
+ def _comment_stripped_hash(code: str) -> str:
520
+ code = re.sub(r"#.*$", "", code, count=0, flags=re.MULTILINE)
521
+ return torch._inductor.codecache.code_hash(code)
522
+
523
+
524
+ def _should_use_remote_autotune_cache(inductor_meta: _InductorMetaTy) -> bool:
525
+ if (config := inductor_meta.get("autotune_remote_cache")) is not None:
526
+ return bool(config)
527
+ if not inductor_meta.get("is_fbcode"):
528
+ return False
529
+ if torch._utils_internal.is_fb_unit_test():
530
+ return False
531
+ if inductor_meta.get("is_hip"):
532
+ return False
533
+
534
+ try:
535
+ from torch._inductor.fb.remote_cache import REMOTE_CACHE_VERSION
536
+ except ModuleNotFoundError:
537
+ return False
538
+
539
+ return REMOTE_CACHE_VERSION >= torch._utils_internal.justknobs_getval_int(
540
+ "pytorch/remote_cache:autotune_memcache_version"
541
+ )
542
+
543
+
544
+ def _load_cached_autotuning(
545
+ best_config: dict[str, JsonDataTy],
546
+ configs_hash: str,
547
+ configs: list[Config],
548
+ inductor_meta: _InductorMetaTy,
549
+ ) -> Config | None:
550
+ if best_config is None:
551
+ return None
552
+ if best_config.pop("configs_hash", None) != configs_hash:
553
+ return None
554
+
555
+ # Remove time taken for comparison
556
+ best_config.pop("time_taken_ms", None)
557
+
558
+ best_config.pop("triton_cache_hash", None)
559
+
560
+ # Extract extra_options if present. This allows third-party backends
561
+ # to restore custom tuned options from the cache.
562
+ extra_options = best_config.pop("extra_options", None)
563
+
564
+ if inductor_meta.get("coordinate_descent_tuning") and best_config.pop(
565
+ "found_by_coordesc", False
566
+ ):
567
+ num_warps = best_config.pop("num_warps")
568
+ num_stages = best_config.pop("num_stages")
569
+
570
+ # Extract common arguments
571
+ config_args = {
572
+ "num_warps": num_warps,
573
+ "num_stages": num_stages,
574
+ }
575
+
576
+ if HAS_WARP_SPEC:
577
+ config_args.update(
578
+ {
579
+ "num_consumer_groups": best_config.pop("num_consumer_groups", 0),
580
+ "num_buffers_warp_spec": best_config.pop(
581
+ "num_buffers_warp_spec", 0
582
+ ),
583
+ }
584
+ )
585
+
586
+ # Create the triton_config with the appropriate arguments
587
+ # pyrefly: ignore [bad-argument-count, unexpected-keyword]
588
+ triton_config = Config(best_config, **config_args)
589
+ # pyrefly: ignore [missing-attribute]
590
+ triton_config.found_by_coordesc = True
591
+ # Restore extra_options (may be None if not used by backend)
592
+ # pyrefly: ignore [missing-attribute]
593
+ triton_config.extra_options = extra_options
594
+ return triton_config
595
+
596
+ matching_configs = [
597
+ cfg
598
+ for cfg in configs
599
+ # pyrefly: ignore [missing-attribute]
600
+ if all(val == best_config.get(key) for key, val in cfg.kwargs.items())
601
+ # pyrefly: ignore [missing-attribute]
602
+ and cfg.num_warps == best_config.get("num_warps")
603
+ # pyrefly: ignore [missing-attribute]
604
+ and cfg.num_stages == best_config.get("num_stages")
605
+ ]
606
+ if len(matching_configs) != 1:
607
+ return None
608
+
609
+ matched_config = matching_configs[0]
610
+ # Restore extra_options (may be None if not used by backend)
611
+ # pyrefly: ignore [missing-attribute]
612
+ matched_config.extra_options = extra_options
613
+ return matched_config
614
+
615
+
616
+ class _LocalAutotuneCacheBackend(RemoteCacheBackend[bytes]):
617
+ @override
618
+ def _get(self, key: str) -> bytes | None:
619
+ try:
620
+ with open(key, "rb") as fd:
621
+ return fd.read()
622
+ except FileNotFoundError:
623
+ return None
624
+
625
+ @override
626
+ def _put(self, key: str, data: bytes) -> None:
627
+ os.makedirs(os.path.dirname(key), exist_ok=True)
628
+ from torch._inductor import codecache
629
+
630
+ codecache.write_atomic(key, data)
631
+
632
+
633
+ class LocalAutotuneCache(RemoteCache[JsonDataTy]):
634
+ def __init__(self) -> None:
635
+ backend = _LocalAutotuneCacheBackend()
636
+ serde = RemoteCacheJsonSerde()
637
+ super().__init__(backend, serde)
638
+
639
+ @override
640
+ def _get(self, key: str, sample: Sample | None) -> JsonDataTy | None:
641
+ AutotuneCacheBundler.sync()
642
+ result = super()._get(key, sample)
643
+ if result is not None:
644
+ assert isinstance(result, dict)
645
+ # What? Why are we doing a put() here? Imagine we have a new model
646
+ # that reuses some existing kernels that have already been
647
+ # compiled. If we didn't do a `put` here (on cache hit) then the new
648
+ # model would only bundle *newly* compiled kernels, not existing
649
+ # kernels that were already compiled and cached.
650
+ AutotuneCacheBundler.put(key, result)
651
+ autotune_artifact_key = os.path.join(*key.split(os.sep)[-2:])
652
+ CacheArtifactManager.record_artifact(
653
+ AutotuneCacheArtifact.type(), autotune_artifact_key, result
654
+ )
655
+ return result
656
+
657
+ @override
658
+ def _put(self, key: str, value: JsonDataTy, sample: Sample | None) -> None:
659
+ AutotuneCacheBundler.put(key, value)
660
+ super()._put(key, value, sample)
661
+
662
+
663
+ def _splitext_nodot(basename: str) -> tuple[str, str]:
664
+ root, ext = os.path.splitext(basename)
665
+ if ext:
666
+ ext = ext[1:]
667
+ return root, ext
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/benchmarking.py ADDED
@@ -0,0 +1,549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import inspect
3
+ import time
4
+ from collections.abc import Callable
5
+ from functools import cached_property, wraps
6
+ from itertools import chain
7
+ from statistics import median
8
+ from typing import Any, Concatenate
9
+ from typing_extensions import ParamSpec, Self, TypeVar
10
+
11
+ import torch
12
+ import torch._inductor.config as inductor_config
13
+ import torch.utils._pytree as pytree
14
+ from torch._dynamo.utils import counters
15
+ from torch.utils._debug_mode import DebugMode
16
+
17
+
18
+ logger = torch._logging.getArtifactLogger(__name__, "benchmarking")
19
+ use_experimental_benchmarker = (
20
+ inductor_config.use_experimental_benchmarker and torch.cuda.is_available()
21
+ )
22
+
23
+
24
+ MILLISECONDS_PER_SECOND = 1000
25
+
26
+ P = ParamSpec("P")
27
+ T = TypeVar("T")
28
+
29
+
30
+ # Device-type → benchmarking function registry.
31
+ # Keys must match torch.device.type (e.g., "cpu", "cuda", "mps", "xpu", ...).
32
+ # Values are callables with signature:
33
+ # fn(self: Benchmarker, _callable: Callable[..., Any], *, warmup: int, rep: int, **kwargs) -> Any
34
+ _BENCHMARK_DISPATCH: dict[str, Callable[..., Any]] = {}
35
+
36
+
37
+ def register_benchmarker(
38
+ device_type: str,
39
+ fn: Callable[..., Any],
40
+ *,
41
+ override: bool = False,
42
+ ) -> None:
43
+ """
44
+ Register a device-type specific benchmarker.
45
+
46
+ Args:
47
+ device_type: torch.device.type string (e.g., "cuda", "cpu", "mps", "xpu").
48
+ fn: callable(self, _callable, *, warmup, rep, **kwargs) -> Any
49
+ override: allow overriding an existing registration.
50
+ """
51
+ if not isinstance(device_type, str) or not device_type:
52
+ raise ValueError(
53
+ "device_type must be a non-empty string matching torch.device.type"
54
+ )
55
+ if not callable(fn):
56
+ raise TypeError("fn must be callable")
57
+ if not override and device_type in _BENCHMARK_DISPATCH:
58
+ raise ValueError(
59
+ f"Benchmarker for device_type '{device_type}' already registered"
60
+ )
61
+ _BENCHMARK_DISPATCH[device_type] = fn
62
+
63
+
64
+ def may_distort_benchmarking_result(fn: Callable[..., Any]) -> Callable[..., Any]:
65
+ from torch._inductor import config
66
+
67
+ if config.test_configs.distort_benchmarking_result == "":
68
+ return fn
69
+
70
+ def distort(
71
+ ms: list[float] | tuple[float, ...] | float,
72
+ ) -> list[float] | tuple[float, ...] | float:
73
+ if isinstance(ms, (list, tuple)):
74
+ return type(ms)(distort(val) for val in ms) # type: ignore[misc]
75
+
76
+ distort_method = config.test_configs.distort_benchmarking_result
77
+ assert isinstance(ms, float)
78
+ if distort_method == "inverse":
79
+ return 1.0 / ms if ms else 0.0
80
+ elif distort_method == "random":
81
+ import random
82
+
83
+ return random.random()
84
+ else:
85
+ raise RuntimeError(f"Unrecognized distort method {distort_method}")
86
+
87
+ @functools.wraps(fn)
88
+ def wrapper(
89
+ *args: list[Any], **kwargs: dict[str, Any]
90
+ ) -> list[float] | tuple[float, ...] | float:
91
+ ms = fn(*args, **kwargs)
92
+
93
+ return distort(ms)
94
+
95
+ return wrapper
96
+
97
+
98
+ def may_ban_benchmarking() -> None:
99
+ if torch._inductor.config.deterministic:
100
+ raise RuntimeError("""In the deterministic mode of Inductor, we will avoid those
101
+ benchmarkings that would cause non deterministic results. Only benchmarkings in the vetted
102
+ scenarios are allowed. Example include autotuning for triton configs of pointwise kernels.
103
+
104
+ When you see this exception, you can do one of the following two things:
105
+ 1. if the benchmarking you are doing does not introduce any non-determinism, you can just
106
+ add is_vetted_benchmarking=True to you benchmark_gpu call. That would solve the issue.
107
+
108
+ 2. if the benchmarking you are doing indeed introduces non-determinism, you'll need to disable
109
+ such feature in deterministic mode or find an alternative implementation that is deterministic.
110
+ """)
111
+
112
+
113
+ def time_and_count(
114
+ fn: Callable[Concatenate[Any, P], T],
115
+ ) -> Callable[Concatenate[Any, P], T]:
116
+ """
117
+ Wraps `fn` to increment the appropriate dynamo counters. It is expected that `fn`
118
+ is a method of `Benchmarker` or one of its subclasses; typing limitations prevent
119
+ us from declaring this directly.
120
+
121
+ NOTE: If you're tempted to add a dynamo_timed call here, this function can be
122
+ called enough that the dynamo_timed overhead is not negligible.
123
+ """
124
+
125
+ @wraps(fn)
126
+ def wrapper(self: Any, *args: P.args, **kwargs: P.kwargs) -> T:
127
+ fn_qual_name = f"{self.__class__.__name__}.{fn.__name__}"
128
+ counters["inductor"][f"benchmarking.{fn_qual_name}"] += 1
129
+ return fn(self, *args, **kwargs)
130
+
131
+ return wrapper
132
+
133
+
134
+ class Benchmarker:
135
+ """
136
+ A device-agnostic benchmarking utility for measuring the runtime of
137
+ inductor generated callables.
138
+ """
139
+
140
+ def __init__(self: Self) -> None:
141
+ pass
142
+
143
+ def infer_device(self, *fn_args: Any, **fn_kwargs: Any) -> torch.device:
144
+ inferred_device: torch.device | None = None
145
+ for arg_or_kwarg in chain(fn_args, fn_kwargs.values()):
146
+ # Some callables take nested structures as arguments so use the
147
+ # flattened form to find any tensors
148
+ for arg_or_kwarg_leaf in pytree.tree_leaves(arg_or_kwarg):
149
+ if not isinstance(arg_or_kwarg_leaf, torch.Tensor):
150
+ continue
151
+ if inferred_device is None:
152
+ inferred_device = arg_or_kwarg_leaf.device
153
+ elif arg_or_kwarg_leaf.device != inferred_device:
154
+ raise ValueError(
155
+ "Can't safely infer the device type of `fn` with multiple device types in `fn_args` and `fn_kwargs`!"
156
+ )
157
+
158
+ if inferred_device is None:
159
+ raise ValueError(
160
+ "Can't safely infer the device type of `fn` with no device types"
161
+ " in `fn_args` or `fn_kwargs`. Use a direct benchmarking method instead e.g. "
162
+ "`Benchmarker.benchmark_cpu` or `Benchmarker.benchmark_gpu`."
163
+ )
164
+
165
+ return inferred_device
166
+
167
+ @time_and_count
168
+ def benchmark(
169
+ self: Self,
170
+ fn: Callable[..., Any],
171
+ fn_args: tuple[Any, ...] | None = None,
172
+ fn_kwargs: dict[str, Any] | None = None,
173
+ device: str | torch.device | None = None,
174
+ **kwargs: Any,
175
+ ) -> float:
176
+ """Benchmark `fn(*fn_args, *fn_kwargs)` and return the runtime, in milliseconds (the
177
+ actual runtime calculation is dictated by the benchmarking implementation, but may be
178
+ one of [mean, median, minimum, etc.]). Functions as a convenience wrapper around
179
+ device-specific implementations, like `benchmark_cpu` and `benchmark_gpu`. Raises
180
+ `ValueError(...)` if we can't safely infer the device type of `fn`; for example,
181
+ if multiple device types are found in `fn_args` and `fn_kwargs`, or if no device
182
+ types are found. To bypass device inference, provide the device to the `device`
183
+ parameter.
184
+
185
+ WARNING: if `fn` mutates `fn_args` or `fn_kwargs`, benchmarking may fail unexpectedly.
186
+ For example, if `fn` clears a mutable object, subsequent invocations of `fn` during
187
+ benchmarking will fail. In such cases, `fn` should handle cloning its arguments internally.
188
+ If device inference is required, `Benchmarker.infer_device` can be used prior to calling
189
+ this method without any arguments for `fn_args` and `fn_kwargs`.
190
+
191
+ Arguments:
192
+ - fn: The function to benchmark.
193
+ - fn_args: The function's arguments.
194
+ - fn_kwargs: The function's kwargs.
195
+
196
+ Keyword Arguments:
197
+ - device: Which device to use for benchmarking. If not provided the device will be attempted
198
+ to be inferred from `fn_args` and `fn_kwargs`.
199
+ - **kwargs: The benchmarking implementation's kwargs.
200
+
201
+ Returns:
202
+ - The runtime of `fn(*fn_args, **fn_kwargs)`, in milliseconds.
203
+ """
204
+ inferred_device: torch.device | None = None
205
+ if device is not None:
206
+ inferred_device = (
207
+ torch.device(device) if isinstance(device, str) else device
208
+ )
209
+ else:
210
+ if fn_args is None and fn_kwargs is None:
211
+ raise ValueError(
212
+ "`fn_args` and `fn_kwargs` cannot both be None if `device` is not provided."
213
+ )
214
+
215
+ fn_args = fn_args or tuple()
216
+ fn_kwargs = fn_kwargs or {}
217
+ inferred_device = self.infer_device(*fn_args, **fn_kwargs)
218
+
219
+ assert isinstance(inferred_device, torch.device)
220
+
221
+ fn_args = fn_args or tuple()
222
+ fn_kwargs = fn_kwargs or {}
223
+
224
+ # No need to wrap if the callable takes no arguments
225
+ if len(fn_args) == 0 and len(fn_kwargs) == 0:
226
+ # Keep a true zero-arg callable type to satisfy type checkers.
227
+ def _callable() -> Any:
228
+ return fn()
229
+ else:
230
+ _args = fn_args
231
+ _kwargs = fn_kwargs
232
+
233
+ def _callable() -> Any:
234
+ return fn(*_args, **_kwargs)
235
+
236
+ warmup = kwargs.pop("warmup", inductor_config.inductor_default_autotune_warmup)
237
+ rep = kwargs.pop("rep", inductor_config.inductor_default_autotune_rep)
238
+
239
+ # Surfacing all kernels during autotuning is super noisy; filtering these out.
240
+ with DebugMode._benchmarking_inductor():
241
+ # First, try a registered device-specific benchmarker
242
+ benchmark_fn: Callable[..., Any] | None = _BENCHMARK_DISPATCH.get(
243
+ inferred_device.type
244
+ )
245
+ if benchmark_fn is not None:
246
+ return benchmark_fn(self, _callable, warmup=warmup, rep=rep, **kwargs)
247
+
248
+ # Backward-compatible default:
249
+ # - CPU -> CPU benchmark path
250
+ # - else -> GPU benchmark path (legacy behavior retained for non-CPU)
251
+ if inferred_device == torch.device("cpu"):
252
+ return self.benchmark_cpu(_callable, warmup=warmup, rep=rep, **kwargs)
253
+ return self.benchmark_gpu(_callable, warmup=warmup, rep=rep, **kwargs)
254
+
255
+ @time_and_count
256
+ def benchmark_cpu(
257
+ self: Self, _callable: Callable[[], Any], warmup: int = 20, rep: int = 100
258
+ ) -> float:
259
+ """Benchmark the CPU callable, `_callable`, and return the median runtime,
260
+ in milliseconds.
261
+
262
+ Arguments:
263
+ - _callable: The CPU callable to benchmark.
264
+
265
+ Keyword Arguments:
266
+ - warmup: Optionally, the duration, in milliseconds, to run `_callable`
267
+ before benchmarking starts.
268
+ - rep: Optionally, the duration, in milliseconds, to run `_callable`
269
+ during benchmarking.
270
+
271
+ Returns:
272
+ - The median runtime of `_callable`, in milliseconds.
273
+ """
274
+
275
+ def run_for(ms: int) -> list[float]:
276
+ timings = []
277
+ run_start_t = time.perf_counter()
278
+ while True:
279
+ start_t = time.perf_counter()
280
+ _callable()
281
+ end_t = time.perf_counter()
282
+ timings.append((end_t - start_t) * MILLISECONDS_PER_SECOND)
283
+ if ((end_t - run_start_t) * MILLISECONDS_PER_SECOND) > ms:
284
+ break
285
+ return timings
286
+
287
+ run_for(warmup)
288
+ return median(run_for(rep))
289
+
290
+ @time_and_count
291
+ def benchmark_gpu(self: Self, *args: Any, **kwargs: Any) -> float:
292
+ raise NotImplementedError
293
+
294
+ @time_and_count
295
+ def benchmark_gpu_with_cuda_graph(
296
+ self: Self,
297
+ _callable: Callable[[], Any],
298
+ **kwargs: Any,
299
+ ) -> float:
300
+ """Benchmark a GPU callable using CUDA graph capture and replay.
301
+
302
+ This captures the callable into a CUDA graph and benchmarks the graph replay,
303
+ which eliminates kernel launch overhead for fair comparison between different
304
+ implementations.
305
+ """
306
+ # Warmup
307
+ _callable()
308
+ torch.cuda.synchronize()
309
+
310
+ # Capture into CUDA graph
311
+ cuda_graph = torch.cuda.CUDAGraph()
312
+ with torch.cuda.graph(cuda_graph, capture_error_mode="thread_local"):
313
+ _callable()
314
+ torch.cuda.synchronize()
315
+
316
+ return self.benchmark_gpu(cuda_graph.replay, **kwargs)
317
+
318
+
319
+ # Make built-in defaults explicit via the registry
320
+ def _default_cpu_bench(self, f, *, warmup, rep, **kw):
321
+ return self.benchmark_cpu(f, warmup=warmup, rep=rep, **kw)
322
+
323
+
324
+ def _default_cuda_bench(self, f, *, warmup, rep, **kw):
325
+ return self.benchmark_gpu(f, warmup=warmup, rep=rep, **kw)
326
+
327
+
328
+ def _default_xpu_bench(self, f, *, warmup, rep, **kw):
329
+ return self.benchmark_gpu(f, warmup=warmup, rep=rep, **kw)
330
+
331
+
332
+ register_benchmarker("cpu", _default_cpu_bench, override=True)
333
+ register_benchmarker("cuda", _default_cuda_bench, override=True)
334
+ register_benchmarker("xpu", _default_xpu_bench, override=True)
335
+
336
+
337
+ class TritonBenchmarker(Benchmarker):
338
+ @cached_property
339
+ def triton_do_bench(self: Self) -> Callable[..., Any]:
340
+ """Lazily import Triton's `do_bench`."""
341
+ try:
342
+ from triton.testing import do_bench
343
+ except ImportError as e:
344
+ raise NotImplementedError("requires Triton") from e
345
+ return do_bench
346
+
347
+ @may_distort_benchmarking_result
348
+ @time_and_count
349
+ # pyrefly: ignore [bad-override]
350
+ def benchmark_gpu(
351
+ self: Self,
352
+ _callable: Callable[[], Any],
353
+ is_vetted_benchmarking: bool = False,
354
+ **kwargs: Any,
355
+ ) -> float:
356
+ """Benchmark the GPU callable, `_callable`, and return the runtime, in milliseconds.
357
+
358
+ Arguments:
359
+ - _callable: The GPU callable to benchmark.
360
+
361
+ Keyword Arguments:
362
+ - quantiles: Optionally, a tuple of floats denoting the requested quantiles.
363
+ - return_mode: Optionally, the requested return mode. Currently, Triton's
364
+ `do_bench` supports min, max, mean, and median return modes.
365
+ - **kwargs: Additional kwargs passed to Triton's `do_bench`.
366
+
367
+ Returns:
368
+ - The runtime of `callable`, in milliseconds. If `kwargs["quantiles"]` is specified,
369
+ this is the first requested quantile. Else, if `kwargs["return_mode"]` is specified,
370
+ this is the requested return mode. Otherwise, this is the median.
371
+ """
372
+ if not is_vetted_benchmarking:
373
+ may_ban_benchmarking()
374
+
375
+ do_bench_params = inspect.signature(self.triton_do_bench).parameters
376
+ for kwarg in list(kwargs.keys()):
377
+ if kwarg not in do_bench_params:
378
+ del kwargs[kwarg]
379
+ try:
380
+ if "quantiles" in kwargs:
381
+ return self.triton_do_bench(_callable, **kwargs)[0]
382
+ elif "return_mode" in kwargs:
383
+ return self.triton_do_bench(_callable, **kwargs)
384
+ return self.triton_do_bench(_callable, **kwargs, return_mode="median")
385
+ except Exception as e:
386
+ # ErrorInvalidConfiguration
387
+ # Return inf to skip this config during autotuning
388
+ error_str = str(e).lower()
389
+ if "invalid configuration" in error_str:
390
+ logger.warning(
391
+ "Skipping benchmark due to invalid configuration error: %s",
392
+ error_str,
393
+ )
394
+ return float("inf")
395
+ raise
396
+
397
+
398
+ class InductorBenchmarker(TritonBenchmarker): # noqa: docstring_linter
399
+ @cached_property
400
+ def L2_cache_size(self: Self) -> int:
401
+ """Get the L2 cache size, in bytes, of the current device."""
402
+ device = torch.cuda.current_device()
403
+ props = torch.cuda.get_device_properties(device)
404
+ return props.L2_cache_size
405
+
406
+ def get_event_pairs(
407
+ self: Self, iters: int
408
+ ) -> list[tuple[torch.cuda.Event, torch.cuda.Event]]:
409
+ """Get `iters` pairs of CUDA events."""
410
+ return [
411
+ (
412
+ torch.cuda.Event(enable_timing=True),
413
+ torch.cuda.Event(enable_timing=True),
414
+ )
415
+ for _ in range(iters)
416
+ ]
417
+
418
+ def get_event_pairs_min_timing(
419
+ self: Self, event_pairs: list[tuple[torch.cuda.Event, torch.cuda.Event]]
420
+ ) -> float:
421
+ """Get the minimum timing, in milliseconds, for a group of CUDA event pairs."""
422
+ return min(
423
+ [
424
+ start_event.elapsed_time(end_event)
425
+ for start_event, end_event in event_pairs
426
+ ]
427
+ )
428
+
429
+ @may_distort_benchmarking_result
430
+ @time_and_count
431
+ def benchmark_gpu( # type: ignore[override]
432
+ self: Self,
433
+ _callable: Callable[[], Any],
434
+ estimation_iters: int = 5,
435
+ memory_warmup_iters: int = 100,
436
+ benchmark_iters: int = 100,
437
+ max_benchmark_duration: int = 25,
438
+ return_mode: str = "min",
439
+ grad_to_none: list[torch.Tensor] | None = None,
440
+ is_vetted_benchmarking: bool = False,
441
+ **kwargs: Any,
442
+ ) -> float | list[float]:
443
+ """Benchmark a GPU callable using a custom benchmarking implementation.
444
+
445
+ Arguments:
446
+ - _callable: The callable to benchmark.
447
+
448
+ Keyword Arguments:
449
+ - estimation_iters: Optionally, the number of iterations to run `_callable`
450
+ during runtime estimation.
451
+ - memory_warmup_iters: Optionally, the number of iterations to flush the L2
452
+ cache before starting benchmarking.
453
+ - benchmark_iters: Optionally, the number of iterations to run `_callable`
454
+ during the benchmarking.
455
+ - max_benchmark_duration: Optionally, the maximum duration of the benchmarking,
456
+ in milliseconds. An estimated duration is calculated based on the values
457
+ of `memory_warmup_iters` and `benchmark_iters`, along with the estimated
458
+ runtime of `_callable` and various other factors, and we then shrink
459
+ `benchmark_iters` to fit in the allotted maximum duration.
460
+ - return_mode: Return mode for benchmark results. Options are "min" (default),
461
+ "all" (returns all measurements).
462
+ - grad_to_none: Optionally, a list of tensors whose gradients should be cleared
463
+ before each benchmark iteration.
464
+ - is_vetted_benchmarking: in deterministic mode, we only allow
465
+ benchmarking in vetted cases.
466
+ - **kwargs: Additional kwargs that may be passed to the fallback.
467
+
468
+ Returns:
469
+ - If return_mode="min": The minimum runtime of `_callable`, in milliseconds.
470
+ - If return_mode="all": List of all runtime measurements, in milliseconds.
471
+ """
472
+
473
+ if not is_vetted_benchmarking:
474
+ may_ban_benchmarking()
475
+
476
+ # we don't want any outside errors propagating into benchmarking
477
+ torch.cuda.synchronize()
478
+
479
+ # warmup `_callable` (and catches any failures in the process)
480
+ _callable()
481
+ torch.cuda.synchronize()
482
+
483
+ # see https://github.com/triton-lang/triton/pull/840 for why `dtype=torch.int`
484
+ buffer = torch.empty(self.L2_cache_size // 4, dtype=torch.int, device="cuda")
485
+ buffer.zero_()
486
+
487
+ # estimate the runtime of `_callable`
488
+ event_pairs = self.get_event_pairs(estimation_iters)
489
+ for start_event, end_event in event_pairs:
490
+ # Clear gradients before timing (matches triton.testing.do_bench)
491
+ if grad_to_none is not None:
492
+ for x in grad_to_none:
493
+ x.grad = None
494
+ buffer.zero_()
495
+ start_event.record()
496
+ _callable()
497
+ end_event.record()
498
+ torch.cuda.synchronize()
499
+ estimated_timing = self.get_event_pairs_min_timing(event_pairs)
500
+
501
+ # adjust `benchmark_iters` to fit in the maximum benchmarking duration
502
+ if estimated_timing > 0:
503
+ benchmark_iters = max(
504
+ min(benchmark_iters, int(max_benchmark_duration // estimated_timing)), 1
505
+ )
506
+
507
+ # do the memory warmup
508
+ for _ in range(memory_warmup_iters):
509
+ buffer.zero_()
510
+
511
+ # benchmark `_callable`
512
+ event_pairs = self.get_event_pairs(benchmark_iters)
513
+ for start_event, end_event in event_pairs:
514
+ # Clear gradients before timing (matches triton.testing.do_bench)
515
+ if grad_to_none is not None:
516
+ for x in grad_to_none:
517
+ x.grad = None
518
+ buffer.zero_()
519
+ start_event.record()
520
+ _callable()
521
+ end_event.record()
522
+ torch.cuda.synchronize()
523
+
524
+ # explicitly delete the buffer, sometimes helps memory
525
+ # footprint metrics in OSS Inductor performance benchmarks
526
+ del buffer
527
+
528
+ # Return based on the requested mode
529
+ if return_mode == "all":
530
+ # Get all timings from event pairs
531
+ all_timings = [
532
+ start_event.elapsed_time(end_event)
533
+ for start_event, end_event in event_pairs
534
+ ]
535
+ return all_timings
536
+ elif return_mode == "min":
537
+ benchmarked_timing = self.get_event_pairs_min_timing(event_pairs)
538
+ # return the minimum of `estimated_timing` and `benchmarked_timing`,
539
+ # we just want the minimum timing overall so we might as well check both
540
+ return min(estimated_timing, benchmarked_timing)
541
+ else:
542
+ raise ValueError(
543
+ f"Unsupported return_mode: {return_mode}. Use 'min' or 'all'."
544
+ )
545
+
546
+
547
+ benchmarker = (
548
+ InductorBenchmarker() if use_experimental_benchmarker else TritonBenchmarker()
549
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/cache_dir_utils.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import getpass
2
+ import os
3
+ import re
4
+ import tempfile
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+
8
+ from torch._environment import is_fbcode
9
+
10
+
11
+ # Factoring out to file without torch dependencies
12
+
13
+
14
+ def cache_dir() -> str:
15
+ cache_dir = os.environ.get("TORCHINDUCTOR_CACHE_DIR")
16
+ if cache_dir is None:
17
+ os.environ["TORCHINDUCTOR_CACHE_DIR"] = cache_dir = default_cache_dir()
18
+ os.makedirs(cache_dir, exist_ok=True)
19
+ return cache_dir
20
+
21
+
22
+ def default_cache_dir() -> str:
23
+ sanitized_username = re.sub(r'[\\/:*?"<>|]', "_", getpass.getuser())
24
+ return os.path.join(
25
+ tempfile.gettempdir() if not is_fbcode() else "/var/tmp",
26
+ "torchinductor_" + sanitized_username,
27
+ )
28
+
29
+
30
+ def triton_cache_dir(device: int) -> str:
31
+ if (directory := os.getenv("TRITON_CACHE_DIR")) is not None:
32
+ return directory
33
+ return os.path.join(
34
+ cache_dir(),
35
+ "triton",
36
+ str(device),
37
+ )
38
+
39
+
40
+ @contextmanager
41
+ def temporary_cache_dir(directory: str) -> Generator[None, None, None]:
42
+ from torch._inductor.utils import clear_caches
43
+
44
+ original = os.environ.get("TORCHINDUCTOR_CACHE_DIR")
45
+ os.environ["TORCHINDUCTOR_CACHE_DIR"] = directory
46
+ try:
47
+ clear_caches()
48
+ yield
49
+ finally:
50
+ clear_caches()
51
+ if original is None:
52
+ del os.environ["TORCHINDUCTOR_CACHE_DIR"]
53
+ else:
54
+ os.environ["TORCHINDUCTOR_CACHE_DIR"] = original
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/__init__.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import config, encoders, memoizers
2
+ from .context import IsolationSchema, SelectedCompileContext, SelectedRuntimeContext
3
+ from .exceptions import (
4
+ CacheError,
5
+ FileLockTimeoutError,
6
+ KeyEncodingError,
7
+ LockTimeoutError,
8
+ SystemError,
9
+ UserError,
10
+ ValueDecodingError,
11
+ ValueEncodingError,
12
+ )
13
+ from .interfaces import Memoizer, PersistentMemoizer
14
+
15
+
16
+ __all__ = [
17
+ "CacheError",
18
+ "FileLockTimeoutError",
19
+ "IsolationSchema",
20
+ "KeyEncodingError",
21
+ "LockTimeoutError",
22
+ "Memoizer",
23
+ "PersistentMemoizer",
24
+ "SelectedCompileContext",
25
+ "SelectedRuntimeContext",
26
+ "SystemError",
27
+ "UserError",
28
+ "ValueDecodingError",
29
+ "ValueEncodingError",
30
+ "encoders",
31
+ "memoizers",
32
+ ]
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/config.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from collections.abc import Callable
3
+ from functools import cache, partial
4
+ from typing import TypeVar
5
+
6
+ import torch
7
+ from torch._environment import is_fbcode
8
+
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ def _is_force_disable_caches() -> bool:
14
+ """Check if caching is force disabled via inductor config.
15
+
16
+ This defers importing torch._inductor.config to avoid circular imports.
17
+
18
+ Returns:
19
+ True if force_disable_caches is set in inductor config, False otherwise.
20
+ """
21
+ from torch._inductor import config as inductor_config
22
+
23
+ return inductor_config.force_disable_caches
24
+
25
+
26
+ @cache
27
+ def _env_var_val(env_var: str, default: T) -> str | T:
28
+ """Get the value of an environment variable or return the default.
29
+
30
+ Args:
31
+ env_var: Environment variable name to check
32
+ default: Default value to return if environment variable is not set
33
+
34
+ Returns:
35
+ The value from the environment variable as a string, or the default if not set
36
+ """
37
+ return os.environ.get(env_var, default)
38
+
39
+
40
+ @cache
41
+ def _env_var_config(env_var: str, default: bool) -> bool:
42
+ env_val = _env_var_val(env_var, None)
43
+ if env_val is not None:
44
+ return env_val == "1"
45
+ return default
46
+
47
+
48
+ @cache
49
+ def _versioned_config(
50
+ jk_name: str,
51
+ this_version: int,
52
+ oss_default: bool,
53
+ env_var_override: str | None = None,
54
+ ) -> bool:
55
+ """
56
+ A versioned configuration utility that determines boolean settings based on:
57
+ 1. Environment variable override (highest priority)
58
+ 2. JustKnobs version comparison in fbcode environments
59
+ 3. OSS default fallback
60
+
61
+ This function enables gradual rollouts of features in fbcode by comparing
62
+ a local version against a JustKnobs-controlled remote version, while
63
+ allowing environment variable overrides for testing and OSS defaults
64
+ for non-fbcode environments.
65
+
66
+ Args:
67
+ jk_name: JustKnobs key name (e.g., "pytorch/inductor:feature_version")
68
+ this_version: Local version number to compare against JustKnobs version
69
+ oss_default: Default value to use in non-fbcode environments
70
+ env_var_override: Optional environment variable name that, when set,
71
+ overrides all other logic
72
+
73
+ Returns:
74
+ bool: Configuration value determined by the priority order above
75
+ """
76
+ if (
77
+ env_var_override
78
+ and (env_var_value := os.environ.get(env_var_override)) is not None
79
+ ):
80
+ return env_var_value == "1"
81
+ elif is_fbcode():
82
+ # this method returns 0 on failure, which we should check for specifically.
83
+ # in the case of JK failure, the safe bet is to simply disable the config
84
+ jk_version: int = torch._utils_internal.justknobs_getval_int(jk_name)
85
+ return (this_version >= jk_version) and (jk_version != 0)
86
+ return oss_default
87
+
88
+
89
+ # toggles the entire caching module, but only when calling through the
90
+ # public facing interfaces. get/insert operations become no-ops in the sense
91
+ # that get will always miss and insert will never insert; record becomes a
92
+ # no-op in the sense that the function will always be called and the cache
93
+ # will never be accessed
94
+ _CACHING_MODULE_VERSION: int = 0
95
+ _CACHING_MODULE_VERSION_JK: str = "pytorch/inductor:caching_module_version"
96
+ _CACHING_MODULE_OSS_DEFAULT: bool = False
97
+ _CACHING_MODULE_ENV_VAR_OVERRIDE: str = "TORCHINDUCTOR_ENABLE_CACHING_MODULE"
98
+
99
+
100
+ def _is_caching_module_enabled_base() -> bool:
101
+ """Base check for caching module enablement via versioned config."""
102
+ return _versioned_config(
103
+ _CACHING_MODULE_VERSION_JK,
104
+ _CACHING_MODULE_VERSION,
105
+ _CACHING_MODULE_OSS_DEFAULT,
106
+ _CACHING_MODULE_ENV_VAR_OVERRIDE,
107
+ )
108
+
109
+
110
+ def IS_CACHING_MODULE_ENABLED() -> bool:
111
+ """Check if the caching module is enabled.
112
+
113
+ Returns False if:
114
+ - The versioned config disables it
115
+ - force_disable_caches is set in inductor config
116
+
117
+ Returns:
118
+ True if caching module is enabled, False otherwise.
119
+ """
120
+ if not _is_caching_module_enabled_base():
121
+ return False
122
+ if _is_force_disable_caches():
123
+ return False
124
+ return True
125
+
126
+
127
+ # Controls whether the Memoizer dumps its cache to a JSON file on destruction.
128
+ # When enabled, the Memoizer will write its in-memory cache to a JSON file
129
+ # (memoizer_cache.json in the cache directory) on destruction. This dump file
130
+ # can later be used to pre-populate Memoizers via CACHE_DUMP_FILE_PATH.
131
+ #
132
+ # This is useful for:
133
+ # - Debugging and inspection of cached values
134
+ # - Creating cache snapshots that can be reused across runs
135
+ # - Pre-warming caches for subsequent executions
136
+ #
137
+ # Set via environment variable: TORCHINDUCTOR_DUMP_MEMOIZER_CACHE=1
138
+ _DUMP_MEMOIZER_CACHE_ENV_VAR: str = "TORCHINDUCTOR_DUMP_MEMOIZER_CACHE"
139
+ _DUMP_MEMOIZER_CACHE_DEFAULT: bool = False
140
+ IS_DUMP_MEMOIZER_CACHE_ENABLED: Callable[[], bool] = partial(
141
+ _env_var_config,
142
+ _DUMP_MEMOIZER_CACHE_ENV_VAR,
143
+ _DUMP_MEMOIZER_CACHE_DEFAULT,
144
+ )
145
+
146
+
147
+ # Path to a cache dump file to pre-populate Memoizers on initialization.
148
+ # This should point to a JSON file produced by IS_DUMP_MEMOIZER_CACHE_ENABLED.
149
+ #
150
+ # When set, Memoizers will load cached entries from this file on initialization,
151
+ # allowing cache values to be reused across separate runs without recomputation.
152
+ # This is particularly useful for:
153
+ # - Pre-warming caches with known-good values
154
+ # - Reproducing behavior from a previous run
155
+ # - Sharing cached computations across different environments
156
+ #
157
+ # The dump file format is produced by the Memoizer's _dump_to_disk method when
158
+ # IS_DUMP_MEMOIZER_CACHE_ENABLED is set to true.
159
+ #
160
+ # Set via environment variable: TORCHINDUCTOR_CACHE_DUMP_FILE_PATH=/path/to/dump.json
161
+ _CACHE_DUMP_FILE_PATH_ENV_VAR: str = "TORCHINDUCTOR_CACHE_DUMP_FILE_PATH"
162
+ _CACHE_DUMP_FILE_PATH_DEFAULT: str | None = None
163
+ CACHE_DUMP_FILE_PATH: Callable[[], str | None] = partial(
164
+ _env_var_val,
165
+ _CACHE_DUMP_FILE_PATH_ENV_VAR,
166
+ _CACHE_DUMP_FILE_PATH_DEFAULT,
167
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/context.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Context management for PyTorch Inductor runtime caching.
2
+
3
+ This module provides context classes for collecting configuration and environment
4
+ information used in caching decisions for PyTorch's Inductor runtime.
5
+ """
6
+
7
+ import json
8
+ from abc import ABC, abstractmethod
9
+ from base64 import b64encode
10
+ from collections.abc import Sequence
11
+ from functools import cache
12
+ from hashlib import sha256
13
+ from typing_extensions import override, TypedDict
14
+
15
+ import torch
16
+
17
+
18
+ class _Context(ABC):
19
+ """Abstract base class for context providers.
20
+
21
+ Context providers collect specific configuration and environment information
22
+ that affects compilation and runtime behavior.
23
+ """
24
+
25
+ @staticmethod
26
+ @abstractmethod
27
+ def forms_of_context() -> Sequence[str]:
28
+ """Return a sequence of context form names provided by this context class.
29
+
30
+ Returns:
31
+ A sequence of strings representing the available context forms.
32
+ """
33
+
34
+
35
+ class _RuntimeContext(_Context):
36
+ """Context provider for runtime configuration and environment settings.
37
+
38
+ Collects configuration settings that affect runtime behavior but not
39
+ compilation, such as Inductor configs, determinism settings, and CUDA
40
+ matmul precision configurations.
41
+ """
42
+
43
+ @override
44
+ @staticmethod
45
+ def forms_of_context() -> Sequence[str]:
46
+ """Return the runtime context forms provided by this class.
47
+
48
+ Returns:
49
+ A sequence containing the available runtime context forms:
50
+ - "inductor_configs": PyTorch Inductor configuration settings
51
+ - "torch_determinism_configs": Deterministic algorithm settings
52
+ - "cuda_matmul_precision_configs": CUDA matrix multiplication precision settings
53
+ """
54
+ return (
55
+ "inductor_configs",
56
+ "torch_determinism_configs",
57
+ "cuda_matmul_precision_configs",
58
+ )
59
+
60
+ @staticmethod
61
+ def inductor_configs() -> dict[str, object]:
62
+ """Get portable Inductor configuration settings.
63
+
64
+ Returns:
65
+ A dictionary containing Inductor configuration settings,
66
+ including private configs.
67
+ """
68
+ from torch._inductor import config
69
+
70
+ return config.save_config_portable(ignore_private_configs=False)
71
+
72
+ @staticmethod
73
+ def torch_determinism_configs() -> dict[str, object]:
74
+ """Get PyTorch deterministic algorithm configuration settings.
75
+
76
+ Returns:
77
+ A dictionary containing deterministic algorithm settings:
78
+ - Whether deterministic algorithms are enabled
79
+ - Whether deterministic algorithm warnings are enabled
80
+ - Fill uninitialized memory setting
81
+ """
82
+ return {
83
+ "torch.are_deterministic_algorithms_enabled": torch.are_deterministic_algorithms_enabled(),
84
+ "torch.is_deterministic_algorithms_warn_only_enabled": (
85
+ torch.is_deterministic_algorithms_warn_only_enabled()
86
+ ),
87
+ "torch.utils.deterministic.fill_uninitialized_memory": getattr(
88
+ torch.utils.deterministic, "fill_uninitialized_memory", None
89
+ ),
90
+ }
91
+
92
+ @staticmethod
93
+ def cuda_matmul_precision_configs() -> dict[str, object]:
94
+ """Get CUDA matrix multiplication precision configuration settings.
95
+
96
+ Returns:
97
+ A dictionary containing CUDA matmul precision settings:
98
+ - FP32 precision setting
99
+ - FP16 reduced precision reduction allowance
100
+ - BF16 reduced precision reduction allowance
101
+ """
102
+ return {
103
+ "torch.backends.cuda.matmul.fp32_precision": torch.backends.cuda.matmul.fp32_precision,
104
+ "torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction": (
105
+ torch.backends.cuda.matmul.allow_fp16_reduced_precision_reduction
106
+ ),
107
+ "torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction": (
108
+ torch.backends.cuda.matmul.allow_bf16_reduced_precision_reduction
109
+ ),
110
+ }
111
+
112
+
113
+ class _CompileContext(_Context):
114
+ """Context provider for compilation-related configuration and environment settings.
115
+
116
+ Collects information that affects compilation behavior, such as PyTorch and Triton
117
+ versions, runtime environment, and accelerator properties.
118
+ """
119
+
120
+ @override
121
+ @staticmethod
122
+ def forms_of_context() -> Sequence[str]:
123
+ """Return the compile context forms provided by this class.
124
+
125
+ Returns:
126
+ A sequence containing the available compile context forms:
127
+ - "torch_version_hash": PyTorch version hash
128
+ - "triton_version_hash": Triton version hash (if available)
129
+ - "runtime": Runtime type (CUDA/HIP/None)
130
+ - "runtime_version": Runtime version string
131
+ - "accelerator_properties": GPU/accelerator properties
132
+ """
133
+ return (
134
+ "torch_version_hash",
135
+ "triton_version_hash",
136
+ "runtime",
137
+ "runtime_version",
138
+ "accelerator_properties",
139
+ )
140
+
141
+ @cache
142
+ @staticmethod
143
+ def torch_version_hash() -> str:
144
+ """Get base64-encoded PyTorch version hash.
145
+
146
+ Returns:
147
+ A base64-encoded string representing the PyTorch version hash.
148
+ """
149
+ from torch._inductor.codecache import torch_key
150
+
151
+ return b64encode(torch_key()).decode()
152
+
153
+ @cache
154
+ @staticmethod
155
+ def triton_version_hash() -> str | None:
156
+ """Get Triton version key if Triton is available.
157
+
158
+ Returns:
159
+ Triton version key if Triton is available, None otherwise.
160
+ """
161
+ from torch._inductor.runtime.triton_compat import HAS_TRITON, triton_key
162
+
163
+ return triton_key() if HAS_TRITON else None
164
+
165
+ @cache
166
+ @staticmethod
167
+ def runtime() -> str | None:
168
+ """Determine the runtime type based on available backends.
169
+
170
+ Returns:
171
+ "CUDA" if CUDA is available, "HIP" if HIP is available, None otherwise.
172
+ """
173
+ return "CUDA" if torch.version.cuda else "HIP" if torch.version.hip else None
174
+
175
+ @cache
176
+ @staticmethod
177
+ def runtime_version() -> str | None:
178
+ """Get the version string for the detected runtime.
179
+
180
+ Returns:
181
+ Version string for the current runtime (CUDA or HIP), or None if
182
+ no supported runtime is detected.
183
+ """
184
+ return {
185
+ "CUDA": torch.version.cuda,
186
+ "HIP": torch.version.hip,
187
+ "None": None,
188
+ }.get(_CompileContext.runtime() or "None")
189
+
190
+ @cache
191
+ @staticmethod
192
+ def accelerator_properties() -> str | None:
193
+ """Get string representation of CUDA device properties.
194
+
195
+ Returns:
196
+ String representation of CUDA device properties if a runtime is
197
+ available, None otherwise.
198
+ """
199
+ return (
200
+ repr(torch.cuda.get_device_properties())
201
+ if _CompileContext.runtime() and torch.cuda.is_available()
202
+ else None
203
+ )
204
+
205
+
206
+ class SelectedRuntimeContext(TypedDict):
207
+ inductor_configs: bool
208
+ torch_determinism_configs: bool
209
+ cuda_matmul_precision_configs: bool
210
+
211
+
212
+ class SelectedCompileContext(TypedDict):
213
+ torch_version_hash: bool
214
+ triton_version_hash: bool
215
+ runtime: bool
216
+ runtime_version: bool
217
+ accelerator_properties: bool
218
+
219
+
220
+ class IsolationSchema(TypedDict):
221
+ """Schema for specifying which context forms to include in cache isolation.
222
+
223
+ Attributes:
224
+ runtime_context: Either True (include all runtime context), False (exclude all),
225
+ or a SelectedRuntimeContext dict specifying which forms to include.
226
+ compile_context: Either True (include all compile context), False (exclude all),
227
+ or a SelectedCompileContext dict specifying which forms to include.
228
+ """
229
+
230
+ runtime_context: SelectedRuntimeContext | bool
231
+ compile_context: SelectedCompileContext | bool
232
+
233
+
234
+ _DEFAULT_ISOLATION_SCHEMA: IsolationSchema = IsolationSchema(
235
+ runtime_context=True, compile_context=True
236
+ )
237
+
238
+
239
+ def _collect_runtime_context(
240
+ selection: SelectedRuntimeContext | bool,
241
+ ) -> dict[str, object] | None:
242
+ """Collect runtime context based on selection.
243
+
244
+ Args:
245
+ selection: True to include all, False to exclude all, or a dict
246
+ specifying which forms to include.
247
+
248
+ Returns:
249
+ Dictionary of selected context data, or None if excluded.
250
+ """
251
+ return {
252
+ form: getattr(_RuntimeContext, form)()
253
+ for form in _RuntimeContext.forms_of_context()
254
+ if selection is True or (selection and selection.get(form, False))
255
+ } or None
256
+
257
+
258
+ def _collect_compile_context(
259
+ selection: SelectedCompileContext | bool,
260
+ ) -> dict[str, object] | None:
261
+ """Collect compile context based on selection.
262
+
263
+ Args:
264
+ selection: True to include all, False to exclude all, or a dict
265
+ specifying which forms to include.
266
+
267
+ Returns:
268
+ Dictionary of selected context data, or None if excluded.
269
+ """
270
+ return {
271
+ form: getattr(_CompileContext, form)()
272
+ for form in _CompileContext.forms_of_context()
273
+ if selection is True or (selection and selection.get(form, False))
274
+ } or None
275
+
276
+
277
+ def _isolation_context(
278
+ ischema: IsolationSchema = _DEFAULT_ISOLATION_SCHEMA,
279
+ ) -> dict[str, object]:
280
+ """Generate context data based on the isolation schema.
281
+
282
+ Args:
283
+ ischema: Schema specifying which context forms to include.
284
+ Defaults to including all runtime and compile context.
285
+
286
+ Returns:
287
+ A dictionary containing the selected context data with keys
288
+ "runtime_context" and "compile_context", where each value is
289
+ either None (if excluded) or a dict of context form data.
290
+ """
291
+ return {
292
+ "runtime_context": _collect_runtime_context(ischema["runtime_context"]),
293
+ "compile_context": _collect_compile_context(ischema["compile_context"]),
294
+ }
295
+
296
+
297
+ def _isolation_key(ischema: IsolationSchema = _DEFAULT_ISOLATION_SCHEMA) -> str:
298
+ """Generate a unique key for the given isolation schema.
299
+
300
+ Args:
301
+ ischema: Schema specifying which context forms to include.
302
+ Defaults to including all runtime and compile context.
303
+
304
+ Returns:
305
+ A 32-character hexadecimal string that uniquely identifies
306
+ the context specified by the isolation schema.
307
+ """
308
+ return sha256(
309
+ json.dumps(_isolation_context(ischema), sort_keys=True).encode()
310
+ ).hexdigest()[:32]
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/encoders.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom encoder functions
3
+
4
+ This module provides reusable encoder functions that convert function parameters
5
+ into JSON-serializable dictionaries for caching purposes.
6
+ """
7
+
8
+ from typing_extensions import TypedDict
9
+
10
+ import torch
11
+ from torch import Tensor
12
+ from torch._inductor.pattern_matcher import Match
13
+ from torch._inductor.runtime.caching.utils import _encode_tensor, EncodedTensor
14
+
15
+
16
+ class ShouldPadEncodedParams(TypedDict):
17
+ """TypedDict for encoded should_pad parameters."""
18
+
19
+ mat1: EncodedTensor
20
+ mat2: EncodedTensor
21
+ op: str
22
+ input: EncodedTensor | None
23
+ mat1_exclude_padding_time: bool
24
+ mat2_exclude_padding_time: bool
25
+ tf32: bool
26
+
27
+
28
+ def should_pad_params_encoder(
29
+ match: Match,
30
+ mat1: Tensor,
31
+ mat2: Tensor,
32
+ op: torch._ops.OpOverloadPacket,
33
+ input: Tensor | None = None,
34
+ ) -> ShouldPadEncodedParams:
35
+ """Encode parameters for _should_pad into a human-readable dict.
36
+
37
+ This encoder extracts only the information needed for caching:
38
+ - Tensor shape, stride, and dtype (not the actual data)
39
+ - Whether padding time should be excluded for mat1 and mat2
40
+ - The operation as a string
41
+
42
+ Args:
43
+ match: The pattern match object
44
+ mat1: First matrix tensor
45
+ mat2: Second matrix tensor
46
+ op: The operation being performed
47
+ input: Optional input tensor for addmm
48
+
49
+ Returns:
50
+ A dict containing the encoded parameters in human-readable form
51
+ """
52
+ # Import here to avoid circular dependency
53
+ from torch._inductor.fx_passes.pad_mm import should_exclude_padding_time
54
+
55
+ return ShouldPadEncodedParams(
56
+ mat1=_encode_tensor(mat1),
57
+ mat2=_encode_tensor(mat2),
58
+ op=str(op),
59
+ input=_encode_tensor(input) if input is not None else None,
60
+ mat1_exclude_padding_time=should_exclude_padding_time(match, "mat1"),
61
+ mat2_exclude_padding_time=should_exclude_padding_time(match, "mat2"),
62
+ tf32=False
63
+ if mat1.dtype != torch.float32
64
+ else bool(
65
+ torch.backends.cuda.matmul.fp32_precision == "tf32"
66
+ or torch.backends.mkldnn.fp32_precision == "tf32"
67
+ ),
68
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/exceptions.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exception classes for PyTorch Inductor runtime caching.
2
+
3
+ This module defines a hierarchy of exceptions used throughout the caching system.
4
+ All custom exceptions inherit from CacheError, with UserError serving as a base
5
+ for user-facing errors that also inherit from TypeError for compatibility.
6
+ """
7
+
8
+ from threading import Lock
9
+
10
+ from filelock import BaseFileLock
11
+
12
+
13
+ class CacheError(Exception):
14
+ """Base class for all caching-related errors.
15
+
16
+ This is the root exception class for all custom exceptions raised by the caching
17
+ module, providing a common interface for error handling and logging.
18
+ """
19
+
20
+
21
+ class SystemError(CacheError, RuntimeError):
22
+ """Base class for system-level caching errors.
23
+
24
+ This class represents errors that occur during cache operations, such as
25
+ storage or retrieval failures. It inherits from RuntimeError to indicate
26
+ that the error is not caused by user input.
27
+ """
28
+
29
+
30
+ class LockTimeoutError(SystemError):
31
+ """Error raised when a lock operation times out.
32
+
33
+ This exception is raised when a lock operation exceeds the specified timeout
34
+ limit, indicating that the lock could not be acquired within the allotted time.
35
+ """
36
+
37
+ def __init__(self, lock: Lock, timeout: float) -> None:
38
+ """Initialize the lock timeout error with detailed lock information.
39
+
40
+ Args:
41
+ lock: The lock object that timed out.
42
+ timeout: The timeout limit that was exceeded.
43
+ """
44
+ super().__init__(f"Failed to acquire lock {lock} within {timeout} seconds.")
45
+
46
+
47
+ class FileLockTimeoutError(SystemError):
48
+ """Error raised when a file lock operation times out.
49
+
50
+ This exception is raised when a file lock operation exceeds the specified timeout
51
+ limit, indicating that the lock could not be acquired within the allotted time.
52
+ """
53
+
54
+ def __init__(self, flock: BaseFileLock, timeout: float) -> None:
55
+ """Initialize the file lock timeout error with detailed lock information.
56
+
57
+ Args:
58
+ flock: The file lock object that timed out.
59
+ timeout: The timeout limit that was exceeded.
60
+ """
61
+ super().__init__(
62
+ f"Failed to acquire file lock {flock} within {timeout} seconds."
63
+ )
64
+
65
+
66
+ class UserError(CacheError, TypeError):
67
+ """Base class for user-facing cache errors that also inherit from TypeError.
68
+
69
+ This class combines CacheError with TypeError to provide compatibility
70
+ with existing exception handling patterns while maintaining the cache
71
+ error hierarchy. All user-facing cache errors should inherit from this class.
72
+ """
73
+
74
+
75
+ class KeyEncodingError(UserError):
76
+ """Base class for errors that occur during cache key encoding operations.
77
+
78
+ Raised when cache keys cannot be properly encoded for storage or transmission.
79
+ This includes serialization, hashing, or other encoding-related failures.
80
+ """
81
+
82
+
83
+ class ValueEncodingError(UserError):
84
+ """Base class for errors that occur during cache value encoding operations.
85
+
86
+ Raised when cache values cannot be properly encoded for storage or transmission.
87
+ This includes serialization, compression, or other encoding-related failures.
88
+ """
89
+
90
+
91
+ class ValueDecodingError(UserError):
92
+ """Base class for errors that occur during cache value decoding operations.
93
+
94
+ Raised when cached values cannot be properly decoded during retrieval.
95
+ This includes deserialization, decompression, or other decoding-related failures.
96
+ """
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/implementations.py ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cache implementation classes
2
+
3
+ This module provides concrete implementations of caching backends including
4
+ in-memory, on-disk, and remote caching strategies. Each implementation follows
5
+ the abstract _CacheImpl interface and provides thread-safe operations with
6
+ appropriate locking mechanisms.
7
+ """
8
+
9
+ from abc import ABC, abstractmethod
10
+ from collections.abc import Generator
11
+ from contextlib import contextmanager
12
+ from dataclasses import dataclass
13
+ from hashlib import sha256
14
+ from io import BufferedReader, BufferedWriter
15
+ from os import PathLike
16
+ from pathlib import Path
17
+ from threading import Lock
18
+ from typing import Generic, TypeVar
19
+ from typing_extensions import override
20
+
21
+ from filelock import BaseFileLock, FileLock
22
+
23
+ from . import locks
24
+
25
+
26
+ # Type variable for cache value types
27
+ _V = TypeVar("_V")
28
+
29
+
30
+ @dataclass
31
+ class Hit(Generic[_V]):
32
+ """Result wrapper for hits on cache get operations.
33
+
34
+ Allows distinguishing between a cache miss and a cached None value.
35
+
36
+ Attributes:
37
+ value: The cached value. The type depends on the cache implementation:
38
+ - InMemoryCache: object (any Python object)
39
+ - OnDiskCache: bytes
40
+ - RemoteCache: bytes
41
+ """
42
+
43
+ value: _V
44
+
45
+
46
+ class Miss:
47
+ """Sentinel class representing a cache miss.
48
+
49
+ Used to distinguish between a cached None value and a cache miss
50
+ when None is a valid cached value.
51
+ """
52
+
53
+
54
+ # Singleton instance for cache miss sentinel
55
+ miss = Miss()
56
+
57
+
58
+ class _CacheImpl(ABC, Generic[_V]):
59
+ """Abstract base class for cache implementations.
60
+
61
+ This class defines the interface that all cache implementations must follow.
62
+ It provides thread-safe operations through a locking mechanism and supports
63
+ both get and insert operations.
64
+
65
+ Type Parameters:
66
+ _V: The type of values stored in the cache. Each implementation specifies
67
+ its own value type (e.g., object for in-memory, bytes for on-disk).
68
+ """
69
+
70
+ def __init__(self) -> None:
71
+ """Initialize the cache implementation with a threading lock."""
72
+ self._lock: Lock = Lock()
73
+
74
+ @property
75
+ def lock(self) -> locks._LockProtocol:
76
+ """Get a context manager for acquiring the cache lock.
77
+
78
+ Locking of the cache is not done by the implementation itself, but by the
79
+ interface that uses it. The interface may want to hold the lock for longer
80
+ than a single cache operation, for example when dealing with multiple
81
+ cache implementations at once, so we leave that decision up to the interface.
82
+
83
+ Args:
84
+ timeout: Optional timeout in seconds (float) for acquiring the lock.
85
+
86
+ Returns:
87
+ A callable that returns a context manager for the lock.
88
+ """
89
+
90
+ def _lock_with_timeout(
91
+ timeout: float | None = None,
92
+ ) -> locks._LockContextManager:
93
+ return locks._acquire_lock_with_timeout(self._lock, timeout)
94
+
95
+ return _lock_with_timeout
96
+
97
+ @abstractmethod
98
+ def get(self, key: str) -> Hit[_V] | None:
99
+ """Retrieve a value from the cache.
100
+
101
+ Args:
102
+ key: The key to look up in the cache (must be str).
103
+
104
+ Returns:
105
+ A Hit object on cache hit where Hit.value is the cached value,
106
+ or None on cache miss.
107
+ """
108
+
109
+ @abstractmethod
110
+ def insert(self, key: str, value: _V) -> bool:
111
+ """Insert a key-value pair into the cache.
112
+
113
+ Args:
114
+ key: The key to insert (must be str).
115
+ value: The value to associate with the key.
116
+
117
+ Returns:
118
+ True if the insertion was successful, False if not inserted.
119
+ """
120
+
121
+
122
+ class _InMemoryCacheImpl(_CacheImpl[_V], Generic[_V]):
123
+ """In-memory cache implementation using a dictionary.
124
+
125
+ This implementation stores key-value pairs in a Python dictionary,
126
+ with keys being pickled for consistent hashing. It provides fast
127
+ access but is limited by available memory and process lifetime.
128
+
129
+ Type Parameters:
130
+ _V: The type of values stored in the cache.
131
+ """
132
+
133
+ def __init__(self) -> None:
134
+ """Initialize the in-memory cache with an empty dictionary."""
135
+ super().__init__()
136
+ self._memory: dict[str, _V] = {}
137
+
138
+ @override
139
+ def get(self, key: str) -> Hit[_V] | None:
140
+ """Retrieve a value from the in-memory cache.
141
+
142
+ Args:
143
+ key: The key to look up (must be str).
144
+
145
+ Returns:
146
+ A Hit object on cache hit where Hit.value is the cached value,
147
+ or None on cache miss.
148
+ """
149
+ if key in self._memory:
150
+ return Hit(value=self._memory[key])
151
+ return None
152
+
153
+ @override
154
+ def insert(self, key: str, value: _V) -> bool:
155
+ """Insert a key-value pair into the in-memory cache.
156
+
157
+ Args:
158
+ key: The key to insert (must be str).
159
+ value: The value to associate with the key.
160
+
161
+ Returns:
162
+ True if the insertion was successful (key was new),
163
+ False if not inserted (key already existed).
164
+ """
165
+ if key not in self._memory:
166
+ self._memory[key] = value
167
+ return True
168
+ return False
169
+
170
+
171
+ class _OnDiskCacheImpl(_CacheImpl[bytes]):
172
+ """On-disk cache implementation using file system storage.
173
+
174
+ This implementation stores cached data as files on disk, with version
175
+ headers to handle cache invalidation. It uses file locking to ensure
176
+ thread safety across processes and provides persistent storage that
177
+ survives process restarts.
178
+
179
+ The value type is 'bytes' since data must be serialized to bytes for disk storage.
180
+
181
+ Attributes:
182
+ _version: _Version number for cache format compatibility.
183
+ _version_header_length: Length of the version header in bytes.
184
+ """
185
+
186
+ _version: int = 0
187
+ _version_header_length: int = 4
188
+
189
+ def __init__(self, sub_dir: PathLike[str] | None = None) -> None:
190
+ """Initialize the on-disk cache with a specified subdirectory.
191
+
192
+ Args:
193
+ sub_dir: Subdirectory name within the cache directory.
194
+ Defaults to empty string if not specified.
195
+ """
196
+ self._cache_dir: Path = self._base_dir / (sub_dir or "")
197
+ self._flock: BaseFileLock = FileLock(str(self._cache_dir / "dir.lock"))
198
+
199
+ @property
200
+ def _base_dir(self) -> Path:
201
+ """Get the base directory for cache storage.
202
+
203
+ Returns:
204
+ Path to the cache directory based on the default cache dir
205
+ and the specified subdirectory.
206
+ """
207
+ from torch._inductor.runtime.runtime_utils import default_cache_dir
208
+
209
+ return Path(default_cache_dir(), "cache")
210
+
211
+ def _fpath_from_key(self, key: str) -> Path:
212
+ """Generate a file path from a cache key.
213
+
214
+ Args:
215
+ key: The cache key to convert to a file path (must be str).
216
+
217
+ Returns:
218
+ A Path object representing the file location for this key.
219
+ """
220
+ return self._cache_dir / key
221
+
222
+ @classmethod
223
+ def _version_header(cls) -> bytes:
224
+ """Generate the version header bytes.
225
+
226
+ Returns:
227
+ A byte string representing the current cache version header.
228
+ """
229
+ return sha256(str(cls._version).encode()).digest()[: cls._version_header_length]
230
+
231
+ def _version_header_matches(self, fp: BufferedReader) -> bool:
232
+ """Check if the file's version header matches the current version.
233
+
234
+ Args:
235
+ fp: File pointer positioned at the start of the file.
236
+
237
+ Returns:
238
+ True if the version header matches, False otherwise.
239
+ """
240
+ return fp.read(self._version_header_length) == self._version_header()
241
+
242
+ def _write_version_header(self, fp: BufferedWriter) -> None:
243
+ """Write the version header to a file.
244
+
245
+ Args:
246
+ fp: File pointer where the version header should be written.
247
+ """
248
+ fp.write(self._version_header())
249
+
250
+ @override
251
+ @property
252
+ def lock(self) -> locks._LockProtocol:
253
+ """Get a context manager for acquiring the file lock.
254
+
255
+ Uses file locking to ensure thread safety across processes.
256
+
257
+ Args:
258
+ timeout: Optional timeout in seconds (float) for acquiring the file lock.
259
+
260
+ Returns:
261
+ A callable that returns a context manager for the file lock.
262
+ """
263
+
264
+ def _lock_with_timeout(
265
+ timeout: float | None = None,
266
+ ) -> locks._LockContextManager:
267
+ return locks._acquire_flock_with_timeout(self._flock, timeout)
268
+
269
+ return _lock_with_timeout
270
+
271
+ @override
272
+ def get(self, key: str) -> Hit[bytes] | None:
273
+ """Retrieve a value from the on-disk cache.
274
+
275
+ Args:
276
+ key: The key to look up in the cache (must be str).
277
+
278
+ Returns:
279
+ A Hit object on cache hit where Hit.value is the cached value (bytes),
280
+ or None on cache miss or version mismatch.
281
+ """
282
+ fpath: Path = self._fpath_from_key(key)
283
+
284
+ if not fpath.is_file():
285
+ return None
286
+
287
+ pickled_value: bytes | None = None
288
+ with open(fpath, "rb") as fp:
289
+ if self._version_header_matches(fp):
290
+ pickled_value = fp.read()
291
+
292
+ if not pickled_value:
293
+ # if pickled_value is still None, even though the file exists, then
294
+ # we know that the version header did not match. in this case implementation
295
+ # is up to preference, we choose to remove entries that do not match
296
+ # the version header so that the key can be re-cached later with the correct
297
+ # version header
298
+ fpath.unlink()
299
+ return None
300
+
301
+ return Hit(value=pickled_value)
302
+
303
+ @override
304
+ def insert(self, key: str, value: bytes) -> bool:
305
+ """Insert a key-value pair into the on-disk cache.
306
+
307
+ Args:
308
+ key: The key to insert (must be str).
309
+ value: The value to associate with the key (must be bytes).
310
+
311
+ Returns:
312
+ True if successfully inserted, False if the key already exists
313
+ with a valid version.
314
+ """
315
+ fpath: Path = self._fpath_from_key(key)
316
+ fpath.parent.mkdir(parents=True, exist_ok=True)
317
+
318
+ r_fp, w_fp, inserted = None, None, False
319
+ try:
320
+ w_fp = open(fpath, "xb") # noqa: SIM115
321
+ except FileExistsError:
322
+ is_stale: bool = False
323
+ with open(fpath, "rb") as r_fp:
324
+ is_stale = not self._version_header_matches(r_fp)
325
+
326
+ if is_stale:
327
+ # same story as above, in this case the version header doesn't
328
+ # match so we choose to remove the old entry so that the new
329
+ # k/v pair can be cached
330
+ fpath.unlink()
331
+ w_fp = open(fpath, "xb") # noqa: SIM115
332
+ else:
333
+ w_fp = None
334
+ finally:
335
+ if w_fp:
336
+ try:
337
+ self._write_version_header(w_fp)
338
+ w_fp.write(value)
339
+ inserted = True
340
+ finally:
341
+ w_fp.close()
342
+
343
+ return inserted
344
+
345
+
346
+ try:
347
+ from .fb.implementations import _RemoteCacheImpl
348
+ except ModuleNotFoundError:
349
+
350
+ class _RemoteCacheImpl(_CacheImpl[bytes]):
351
+ """Fallback remote cache implementation for non-Facebook environments.
352
+
353
+ This is a no-op implementation that always raises NotImplementedError.
354
+ The actual remote cache implementation is provided in the `.fb` module
355
+ for Facebook-specific environments.
356
+
357
+ The value type is 'bytes' for consistency with the FB implementation.
358
+
359
+ Attributes:
360
+ _version: _Version number for cache format compatibility.
361
+ has_strong_consistency: Whether the remote cache provides strong
362
+ consistency guarantees.
363
+ """
364
+
365
+ _version: int = 0
366
+ has_strong_consistency: bool = False
367
+
368
+ def __init__(self) -> None:
369
+ """Initialize the fallback remote cache implementation.
370
+
371
+ Note: We don't need to initialize any form of lock since this
372
+ implementation provides a pseudo-lock context manager.
373
+ """
374
+
375
+ @override
376
+ @property
377
+ def lock(self) -> locks._LockProtocol:
378
+ """Get a pseudo lock that does nothing.
379
+
380
+ Most remote cache implementations don't have an ability to implement
381
+ any form of locking, so we provide a no-op pseudo-lock for consistency
382
+ with the interface.
383
+
384
+ Args:
385
+ timeout: Optional timeout in seconds (float). Ignored in this
386
+
387
+ Returns:
388
+ A callable that returns a no-op context manager.
389
+ """
390
+
391
+ @contextmanager
392
+ def pseudo_lock(
393
+ timeout: float | None = None,
394
+ ) -> Generator[None, None, None]:
395
+ yield
396
+
397
+ return pseudo_lock
398
+
399
+ @override
400
+ def get(self, key: str) -> Hit[bytes] | None:
401
+ """Raise NotImplementedError for remote cache get operations.
402
+
403
+ Args:
404
+ key: The key to look up (must be str, ignored).
405
+
406
+ Raises:
407
+ NotImplementedError: Always raised as this is a fallback implementation.
408
+ """
409
+ raise NotImplementedError
410
+
411
+ @override
412
+ def insert(self, key: str, value: bytes) -> bool:
413
+ """Raise NotImplementedError for remote cache insert operations.
414
+
415
+ Args:
416
+ key: The key to insert (must be str, ignored).
417
+ value: The value to insert (must be bytes, ignored).
418
+
419
+ Raises:
420
+ NotImplementedError: Always raised as this is a fallback implementation.
421
+ """
422
+ raise NotImplementedError
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/interfaces.py ADDED
@@ -0,0 +1,1280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public interfaces for PyTorch Inductor runtime caching.
2
+
3
+ This module provides high-level caching interfaces for memoization and
4
+ result caching functionality.
5
+ """
6
+
7
+ import atexit
8
+ import functools
9
+ import json
10
+ import logging
11
+ import pickle
12
+ import shutil
13
+ import threading
14
+ import weakref
15
+ from collections.abc import Callable
16
+ from dataclasses import dataclass, field
17
+ from hashlib import sha256
18
+ from os import PathLike
19
+ from pathlib import Path
20
+ from typing import cast, Generic, Protocol, TypedDict
21
+ from typing_extensions import ParamSpec, TypeVar
22
+
23
+ from filelock import FileLock
24
+
25
+ import torch
26
+ from torch._inductor.runtime.runtime_utils import cache_dir
27
+ from torch._inductor.utils import clear_on_fresh_cache
28
+
29
+ from . import config, implementations, locks
30
+
31
+
32
+ logger = torch._logging.getArtifactLogger(__name__, "caching")
33
+
34
+ # Type variable for function parameters
35
+ _P = ParamSpec("_P")
36
+ # Type variable for function return type
37
+ _R = TypeVar("_R")
38
+ # Type variable for encoded result type
39
+ _EncodedR = TypeVar("_EncodedR")
40
+
41
+
42
+ class CacheDumpEntry(TypedDict):
43
+ """A single cache entry in the dump format.
44
+
45
+ Attributes:
46
+ params: The encoded function parameters.
47
+ result: The encoded function result.
48
+ """
49
+
50
+ params: object
51
+ result: object
52
+
53
+
54
+ class CacheDump(TypedDict):
55
+ """The structure of the memoizer cache dump file.
56
+
57
+ Cache entries are organized by sub_key in the collections field.
58
+ Memoizers without a sub_key use None as their key in collections.
59
+
60
+ Multiple Memoizer instances with different sub_keys can coexist in the same file.
61
+
62
+ Attributes:
63
+ collections: Dictionary mapping sub_keys (or None for root entries)
64
+ to their cache entries.
65
+ cache_size: The total number of cache entries across all collections.
66
+ """
67
+
68
+ collections: dict[str | None, dict[str, CacheDumpEntry]]
69
+ cache_size: int
70
+
71
+
72
+ @dataclass
73
+ class CacheEntry:
74
+ """A cache entry containing encoded parameters and result.
75
+
76
+ This dataclass stores the encoded form of function parameters and
77
+ the (possibly encoded) result for human-readable cache dumps.
78
+
79
+ Attributes:
80
+ encoded_params: The encoded function parameters used for debugging/inspection.
81
+ encoded_result: The (possibly encoded) result of the function call.
82
+ """
83
+
84
+ encoded_params: object
85
+ encoded_result: object
86
+
87
+
88
+ @dataclass
89
+ class InterimResult(Generic[_R]):
90
+ """Wrapper for an available interim result from a DeferredRecording.
91
+
92
+ When get_interim_result() returns an InterimResult, it means the
93
+ make_interim_result callable was provided and has been invoked.
94
+ The wrapped value is the result of that invocation.
95
+
96
+ This allows distinguishing between:
97
+ - No interim result configured: get_interim_result() returns None
98
+ - Interim result available: get_interim_result() returns InterimResult(value)
99
+ where value may itself be None if the callable returns None.
100
+
101
+ Attributes:
102
+ value: The value returned by calling make_interim_result().
103
+ May legitimately be None if the callable returns None.
104
+ """
105
+
106
+ value: _R
107
+
108
+
109
+ @dataclass
110
+ class DeferredRecording(Generic[_R, _EncodedR]):
111
+ """Signals that recording should happen at a later time.
112
+
113
+ When returned from a custom_result_encoder, the memoizer will:
114
+ 1. Skip immediate caching
115
+ 2. Register a completion callback on this object
116
+
117
+ The encoder is responsible for calling `finalize(encoded_result)`
118
+ when the actual result is ready to be cached. This is useful when
119
+ the function returns a value whose final encoded form is not yet
120
+ available (e.g., an object containing a pending computation),
121
+ allowing the expensive work to remain hidden while still enabling caching.
122
+
123
+ This class handles the race condition where the computation might complete
124
+ before the memoizer has registered its callback. If finalize() is called
125
+ first, the result is stored and the callbacks are invoked when registered.
126
+
127
+ Multiple callbacks can be registered - they will all be invoked when
128
+ finalize() is called (or immediately if already completed). Callbacks are
129
+ guaranteed to be invoked in registration order.
130
+
131
+ Optionally, a `make_interim_result` callable can be provided to handle
132
+ subsequent calls while the deferred recording is pending. When a memoized
133
+ function is called again with the same parameters while a deferred recording
134
+ is still pending, the memoizer can use this callable to construct an
135
+ appropriate return value instead of re-executing the function. The callable
136
+ does not necessarily return the same object - it provides a way to derive
137
+ a suitable response based on the pending deferred recording's context.
138
+
139
+ This class is thread-safe: concurrent calls to finalize() and
140
+ register_callback() are properly synchronized.
141
+
142
+ WARNING: Callbacks are executed while holding the internal lock to ensure
143
+ ordering guarantees. Callbacks MUST NOT call back into this DeferredRecording
144
+ instance (e.g., calling finalize() or register_callback() from within a
145
+ callback) as this will cause a deadlock.
146
+
147
+ Example usage in an encoder:
148
+ def my_encoder(*args, **kwargs):
149
+ def encode(result: _R) -> DeferredRecording[_R, _EncodedR]:
150
+ deferred: DeferredRecording[_R, _EncodedR] = DeferredRecording(
151
+ # Construct an appropriate return for subsequent calls while pending
152
+ make_interim_result=lambda: result
153
+ )
154
+
155
+ def on_complete():
156
+ encoded_result: _EncodedR = _encode_result(result)
157
+ deferred.finalize(encoded_result)
158
+
159
+ result.add_completion_callback(on_complete)
160
+ return deferred
161
+
162
+ return encode
163
+
164
+ Attributes:
165
+ _callbacks: List of callbacks to invoke when finalize() is called.
166
+ Set to None after finalize() is called to indicate completion.
167
+ _encoded_result: The encoded result passed to finalize().
168
+ _lock: Lock for thread-safe access to mutable state.
169
+ make_interim_result: Optional callable that constructs a return value for subsequent
170
+ calls while the deferred recording is pending. If set, the memoizer
171
+ calls this instead of re-executing the function. The callable may
172
+ return the same object or construct a new appropriate response.
173
+ """
174
+
175
+ _callbacks: list[Callable[[_EncodedR], None]] | None = field(
176
+ default_factory=list, repr=False
177
+ )
178
+ _encoded_result: _EncodedR | None = field(default=None, repr=False)
179
+ _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
180
+ make_interim_result: Callable[[], _R] | None = field(default=None, repr=False)
181
+
182
+ def finalize(self, encoded_result: _EncodedR) -> None:
183
+ """Finalize the deferred recording with the encoded result.
184
+
185
+ This method should be called by the encoder (typically via a completion
186
+ callback) when the actual result is ready to be cached.
187
+
188
+ All registered callbacks are invoked with the encoded result in
189
+ registration order. If no callbacks are registered yet, the result is
190
+ stored and callbacks will be invoked when registered.
191
+
192
+ This method is thread-safe.
193
+
194
+ WARNING: Callbacks are executed while holding the internal lock.
195
+ Do not call finalize() or register_callback() from within a callback,
196
+ as this will cause a deadlock.
197
+
198
+ Args:
199
+ encoded_result: The encoded result to cache.
200
+ """
201
+ with self._lock:
202
+ if self._callbacks is None:
203
+ raise RuntimeError(
204
+ "finalize() called multiple times on DeferredRecording"
205
+ )
206
+
207
+ # Store the result and get callbacks to invoke
208
+ self._encoded_result = encoded_result
209
+ callbacks_to_invoke = self._callbacks
210
+ self._callbacks = None
211
+
212
+ # Execute callbacks inside the lock to ensure ordering:
213
+ # - All pre-finalize callbacks execute in registration order
214
+ # - Any callback registered during/after finalize will see
215
+ # _callbacks=None and execute immediately (also under lock)
216
+ for callback in callbacks_to_invoke:
217
+ callback(encoded_result)
218
+
219
+ def register_callback(self, callback: Callable[[_EncodedR], None]) -> None:
220
+ """Register a completion callback.
221
+
222
+ This method is called by memoizers to register callbacks that
223
+ will insert cache entries. Multiple callbacks can be registered.
224
+
225
+ If finalize() has already been called (e.g., the computation was fast),
226
+ the callback is invoked immediately with the stored result.
227
+
228
+ This method is thread-safe. Callbacks are guaranteed to be invoked
229
+ in registration order.
230
+
231
+ WARNING: Callbacks are executed while holding the internal lock.
232
+ Do not call finalize() or register_callback() from within a callback,
233
+ as this will cause a deadlock.
234
+
235
+ Args:
236
+ callback: The function to call with the encoded result.
237
+ """
238
+ with self._lock:
239
+ if self._callbacks is None:
240
+ # Already finalized - invoke immediately while holding lock
241
+ # to maintain ordering with respect to other callbacks
242
+ # we need to cast here, we can't assert because the encoded
243
+ # result may genuinely be None
244
+ callback(cast(_EncodedR, self._encoded_result))
245
+ else:
246
+ self._callbacks.append(callback)
247
+
248
+ def get_interim_result(self) -> InterimResult[_R] | None:
249
+ """Try to get an interim result for use while deferred recording is pending.
250
+
251
+ When a memoized function is called again with the same parameters while
252
+ a deferred recording is still pending, the memoizer uses this method to
253
+ check if an interim result can be returned instead of re-executing the
254
+ function.
255
+
256
+ This method provides thread-safe access to the make_interim_result
257
+ callable. It atomically checks if the callable was provided and invokes
258
+ it if so.
259
+
260
+ This method is thread-safe.
261
+
262
+ Returns:
263
+ InterimResult wrapping the value if make_interim_result was provided,
264
+ or None if no interim result is available.
265
+
266
+ Note:
267
+ The callable is invoked each time this method is called, so if
268
+ make_interim_result returns a new object each time, callers will
269
+ receive different objects. To return the same object, the callable
270
+ should capture and return a reference to a single object.
271
+ """
272
+ with self._lock:
273
+ if self.make_interim_result is not None:
274
+ return InterimResult(self.make_interim_result())
275
+ return None
276
+
277
+
278
+ class ResultEncoderFactory(Protocol[_P, _R, _EncodedR]):
279
+ """Protocol for custom result encoder factories.
280
+
281
+ A result encoder factory is a callable with the following signature:
282
+ factory(fn) -> params_to_encoder(*args, **kwargs) -> encoder(result) -> encoded
283
+
284
+ The three levels are:
285
+ 1. factory(fn): Takes the underlying unwrapped function, returns a params_to_encoder
286
+ 2. params_to_encoder(*args, **kwargs): Takes the memoized function's arguments,
287
+ returns an encoder function
288
+ 3. encoder(result): Converts the result R -> _EncodedR (or DeferredRecording)
289
+
290
+ The `fn` parameter allows the encoder to call the underlying function without
291
+ triggering memoization, which is useful when the encoder needs to re-execute
292
+ the function (e.g., to get a fresh result for DeferredRecording).
293
+
294
+ Example:
295
+ def my_encoder_factory(fn: Callable) -> Callable:
296
+ def params_to_encoder(*args, **kwargs) -> Callable:
297
+ def encode(result: R) -> EncodedR:
298
+ return {"encoded": result}
299
+ return encode
300
+ return params_to_encoder
301
+
302
+ @memoizer.record(custom_result_encoder=my_encoder_factory)
303
+ def compute(x: int) -> int:
304
+ return x * 2
305
+ """
306
+
307
+ def __call__(
308
+ self,
309
+ fn: Callable[_P, _R],
310
+ ) -> Callable[_P, Callable[[_R], _EncodedR | DeferredRecording[_R, _EncodedR]]]: ...
311
+
312
+
313
+ # pyrefly: ignore [variance-mismatch]
314
+ class ResultDecoderFactory(Protocol[_P, _R, _EncodedR]):
315
+ """Protocol for custom result decoder factories.
316
+
317
+ A result decoder factory is a callable with the following signature:
318
+ factory(fn) -> params_to_decoder(*args, **kwargs) -> decoder(encoded) -> result
319
+
320
+ The three levels are:
321
+ 1. factory(fn): Takes the underlying unwrapped function, returns a params_to_decoder
322
+ 2. params_to_decoder(*args, **kwargs): Takes the memoized function's arguments,
323
+ returns a decoder function
324
+ 3. decoder(encoded): Converts the encoded result _EncodedR -> R
325
+
326
+ The `fn` parameter allows the decoder to call the underlying function without
327
+ triggering memoization, which is useful for fallback paths when the cached
328
+ result cannot be fully decoded.
329
+
330
+ Example:
331
+ def my_decoder_factory(fn: Callable) -> Callable:
332
+ def params_to_decoder(*args, **kwargs) -> Callable:
333
+ def decode(encoded_result: EncodedR) -> R:
334
+ return encoded_result["value"]
335
+ return decode
336
+ return params_to_decoder
337
+
338
+ @memoizer.replay(custom_result_decoder=my_decoder_factory)
339
+ def compute(x: int) -> int:
340
+ return x * 2
341
+ """
342
+
343
+ def __call__(
344
+ self,
345
+ fn: Callable[_P, _R],
346
+ ) -> Callable[_P, Callable[[_EncodedR], _R]]: ...
347
+
348
+
349
+ class _BaseMemoizer:
350
+ """Base class for memoization interfaces.
351
+
352
+ This class provides the common memoize method that orchestrates
353
+ record and replay functionality.
354
+ """
355
+
356
+ @staticmethod
357
+ def _make_key(
358
+ custom_params_encoder: Callable[..., object] | None,
359
+ *args: object,
360
+ **kwargs: object,
361
+ ) -> str:
362
+ """Generate a cache key from function parameters.
363
+
364
+ Args:
365
+ custom_params_encoder: Optional encoder to apply to function parameters.
366
+ If None, params are pickled directly.
367
+ *args: Positional arguments to encode.
368
+ **kwargs: Keyword arguments to encode.
369
+
370
+ Returns:
371
+ A 32-character hex string suitable for use as a cache key.
372
+ """
373
+ if custom_params_encoder is None:
374
+ # Pickle the parameters directly
375
+ pickled_params: bytes = pickle.dumps((args, kwargs))
376
+ else:
377
+ # Encode the parameters using the custom encoder
378
+ encoded_params = custom_params_encoder(*args, **kwargs)
379
+ # Pickle the encoded output
380
+ pickled_params = pickle.dumps(encoded_params)
381
+
382
+ # Hash the pickled bytes with SHA256
383
+ hash_obj = sha256(pickled_params)
384
+
385
+ # Get hex digest and truncate to 32 characters
386
+ return hash_obj.hexdigest()[:32]
387
+
388
+ def record(
389
+ self,
390
+ custom_params_encoder: Callable[_P, object] | None = None,
391
+ custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
392
+ ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
393
+ """Record a function call result. Must be implemented by subclasses.
394
+
395
+ This can be used standalone to cache results without necessarily
396
+ replaying them, for example for logging or analytics purposes.
397
+
398
+ See memoize() for a description of the arguments.
399
+ """
400
+ raise NotImplementedError
401
+
402
+ def replay(
403
+ self,
404
+ custom_params_encoder: Callable[_P, object] | None = None,
405
+ custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
406
+ ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
407
+ """Replay a cached function result. Must be implemented by subclasses.
408
+
409
+ This can be used standalone to use cached values that are otherwise
410
+ too hard to automatically record.
411
+
412
+ See memoize() for a description of the arguments.
413
+ """
414
+ raise NotImplementedError
415
+
416
+ def memoize(
417
+ self,
418
+ custom_params_encoder: Callable[_P, object] | None = None,
419
+ custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
420
+ custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
421
+ ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
422
+ """Memoize a function with record and replay functionality.
423
+
424
+ This is a decorator that attempts to replay cached results first.
425
+ If a cache miss occurs, it records the result by executing the wrapped function.
426
+
427
+ Args:
428
+ custom_params_encoder: Optional encoder for function parameters.
429
+ If None, parameters are pickled directly.
430
+ custom_result_encoder: Optional encoder factory for function results.
431
+ Takes function parameters and returns an encoder
432
+ function that converts R -> _EncodedR.
433
+ custom_result_decoder: Optional decoder factory for cached results.
434
+ Takes function parameters and returns a decoder
435
+ function that converts _EncodedR -> R.
436
+
437
+ Returns:
438
+ A decorator function that can be applied to functions.
439
+
440
+ Example:
441
+ @memoizer.memoize(
442
+ custom_params_encoder=my_param_encoder,
443
+ custom_result_encoder=my_result_encoder_factory,
444
+ custom_result_decoder=my_result_decoder_factory,
445
+ )
446
+ def expensive_function(x, y):
447
+ return x + y
448
+ """
449
+
450
+ def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
451
+ """Wrap the function to enable memoization with replay and record.
452
+
453
+ Args:
454
+ fn: The function to wrap.
455
+
456
+ Returns:
457
+ A wrapped version of the function.
458
+ """
459
+ # If caching is disabled, return the original function unchanged
460
+ if not config.IS_CACHING_MODULE_ENABLED():
461
+ return fn
462
+
463
+ # Create decorated versions using record and replay
464
+ replay_fn = self.replay(
465
+ custom_params_encoder,
466
+ custom_result_decoder,
467
+ )(fn)
468
+ record_fn = self.record(
469
+ custom_params_encoder,
470
+ custom_result_encoder,
471
+ )(fn)
472
+
473
+ @functools.wraps(fn)
474
+ def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
475
+ """Attempt to replay from cache, or record on cache miss.
476
+
477
+ Args:
478
+ *args: Positional arguments to pass to the function.
479
+ **kwargs: Keyword arguments to pass to the function.
480
+
481
+ Returns:
482
+ The result from cache (if hit) or from executing the function (if miss).
483
+ """
484
+ # Try to replay first
485
+ try:
486
+ return replay_fn(*args, **kwargs)
487
+ except KeyError:
488
+ # Cache miss - record the result
489
+ return record_fn(*args, **kwargs)
490
+
491
+ return inner
492
+
493
+ return wrapper
494
+
495
+
496
+ class Memoizer(_BaseMemoizer):
497
+ """In-memory memoization interface for caching function results.
498
+
499
+ This class provides methods for recording, retrieving, and managing
500
+ cached function results in memory with custom encoding/decoding logic.
501
+
502
+ Cache entries are stored as CacheEntry objects containing both the encoded
503
+ parameters and the encoded result. This makes debugging easier since entries
504
+ can be inspected with full context about what inputs produced each result.
505
+
506
+ On memoizer destruction, the cache is automatically dumped to a shared JSON file
507
+ in the cache directory for debugging and inspection purposes. Multiple Memoizer
508
+ instances contribute to the same file additively.
509
+
510
+ Note: Use this over functools.cache when you need to support parameters
511
+ that functools.cache cannot handle, or when you need custom encoding/decoding
512
+ of results.
513
+ """
514
+
515
+ def __init__(self, sub_key: str | None = None) -> None:
516
+ """Initialize the Memoizer instance with an in-memory cache.
517
+
518
+ Args:
519
+ sub_key: Optional key for organizing cache entries in the JSON dump.
520
+ If provided, cache entries are stored under cache_entries[sub_key].
521
+ If None, cache entries are merged directly into root cache_entries.
522
+ """
523
+ self._cache: implementations._InMemoryCacheImpl[CacheEntry] = (
524
+ implementations._InMemoryCacheImpl()
525
+ )
526
+ # Optional sub_key for nested cache structure
527
+ self._sub_key: str | None = sub_key
528
+ # Track pending deferred recordings by cache key.
529
+ # Uses WeakValueDictionary to prevent memory leaks: if the DeferredRecording
530
+ # is no longer referenced elsewhere (e.g., the async computation was cancelled
531
+ # or failed and garbage collected), the entry is automatically removed.
532
+ self._pending_deferred: weakref.WeakValueDictionary[str, DeferredRecording] = (
533
+ weakref.WeakValueDictionary()
534
+ )
535
+ # Register atexit handler to dump cache on program exit
536
+ if config.IS_DUMP_MEMOIZER_CACHE_ENABLED():
537
+ atexit.register(self._dump_to_disk)
538
+ # Pre-populate cache from dump file if configured (with sub_key now set)
539
+ self._maybe_prepopulate_from_dump()
540
+ # Register with clear_on_fresh_cache for fresh_cache() integration
541
+ clear_on_fresh_cache(self)
542
+
543
+ def cache_clear(self) -> None:
544
+ """Clear the in-memory cache.
545
+
546
+ This method resets the in-memory cache to empty. It is called by
547
+ the fresh_cache() context manager via clear_on_fresh_cache registration.
548
+ """
549
+ self._cache._memory.clear()
550
+
551
+ def _finalize_deferred_recording(
552
+ self,
553
+ cache_key: str,
554
+ encoded_params: object,
555
+ encoded_result: _EncodedR,
556
+ ) -> None:
557
+ """Finalize a deferred recording by inserting the cache entry.
558
+
559
+ This method is called when a DeferredRecording's finalize() method is invoked.
560
+ It creates the cache entry and inserts it into the in-memory cache.
561
+
562
+ Args:
563
+ cache_key: The cache key for the entry.
564
+ encoded_params: The encoded function parameters.
565
+ encoded_result: The final encoded result to cache.
566
+ """
567
+ # Remove from pending tracking
568
+ self._pending_deferred.pop(cache_key, None)
569
+ # Insert into cache
570
+ cache_entry = CacheEntry(
571
+ encoded_params=encoded_params,
572
+ encoded_result=encoded_result,
573
+ )
574
+ self._cache.insert(cache_key, cache_entry)
575
+
576
+ @functools.cached_property
577
+ def _shared_cache_filepath(self) -> Path:
578
+ """Get the shared cache filepath for memoizer cache dumps.
579
+
580
+ Returns:
581
+ The path to the shared memoizer cache JSON file.
582
+ """
583
+ return Path(cache_dir()) / "memoizer_cache.json"
584
+
585
+ @functools.cached_property
586
+ def _shared_cache_lockfile(self) -> Path:
587
+ """Get the lock file path for the shared memoizer cache.
588
+
589
+ Returns:
590
+ The path to the lock file for the shared cache.
591
+ """
592
+ return Path(cache_dir()) / "memoizer_cache.lock"
593
+
594
+ def _read_dump_from_disk(self, filepath: Path | None = None) -> CacheDump | None:
595
+ """Read a cache dump from disk.
596
+
597
+ Attempts to read and parse a cache JSON file.
598
+
599
+ Args:
600
+ filepath: Path to the dump file to read. If None, uses the
601
+ shared cache filepath (self._shared_cache_filepath).
602
+
603
+ Returns:
604
+ The cache dump if the file exists and is valid JSON, None otherwise.
605
+ """
606
+ target_path = filepath if filepath is not None else self._shared_cache_filepath
607
+ try:
608
+ with open(target_path) as f:
609
+ data = json.load(f)
610
+ return data
611
+ except FileNotFoundError:
612
+ return None
613
+ except json.JSONDecodeError:
614
+ return None
615
+
616
+ def _write_dump_to_disk(self, dump: CacheDump) -> None:
617
+ """Write the cache dump to disk.
618
+
619
+ Writes the provided dump to the shared cache JSON file and logs the result.
620
+
621
+ Args:
622
+ dump: The cache dump to write.
623
+ """
624
+ try:
625
+ with open(self._shared_cache_filepath, "w") as f:
626
+ json.dump(dump, f, indent=2)
627
+
628
+ # Log the filepath
629
+ if self._sub_key:
630
+ logger.log(
631
+ logging.INFO,
632
+ "Memoizer cache (sub_key=%s) dumped to: %s",
633
+ self._sub_key,
634
+ self._shared_cache_filepath,
635
+ )
636
+ else:
637
+ logger.log(
638
+ logging.INFO,
639
+ "Memoizer cache dumped to: %s",
640
+ self._shared_cache_filepath,
641
+ )
642
+ except Exception as e:
643
+ # If dumping fails, just log it and don't crash the program
644
+ logger.log(
645
+ logging.WARNING,
646
+ "Warning: Failed to dump memoizer cache: %s",
647
+ e,
648
+ )
649
+
650
+ def _prepare_dump(self, existing_dump: CacheDump | None) -> CacheDump:
651
+ """Prepare a cache dump from the current Memoizer state.
652
+
653
+ Takes the existing dump (if any) and merges it with the current
654
+ in-memory cache entries.
655
+
656
+ Args:
657
+ existing_dump: The existing dump to merge with, or None if starting fresh.
658
+
659
+ Returns:
660
+ The prepared cache dump ready to be written to disk.
661
+ """
662
+ # Start with existing data or empty structure
663
+ if existing_dump is not None:
664
+ dump = existing_dump
665
+ else:
666
+ dump: CacheDump = {"collections": {}, "cache_size": 0}
667
+
668
+ # Ensure collections field exists
669
+ if "collections" not in dump:
670
+ dump["collections"] = {}
671
+
672
+ # JSON serializes None keys as "null" string, so we need to handle both cases
673
+ # When reading from JSON, the key will be "null" (string), not None
674
+ lookup_key = "null" if self._sub_key is None else self._sub_key
675
+
676
+ # Get existing entries for this sub_key to merge with
677
+ existing_entries = dump["collections"].get(lookup_key, {})
678
+
679
+ # Format cache entries as {"params": ..., "result": ...}
680
+ formatted_cache: dict[str, CacheDumpEntry] = dict(existing_entries)
681
+ for key, value in self._cache._memory.items():
682
+ entry = value
683
+ formatted_cache[key] = CacheDumpEntry(
684
+ params=entry.encoded_params,
685
+ result=entry.encoded_result,
686
+ )
687
+
688
+ # Store under sub_key in collections (use lookup_key to maintain JSON compatibility)
689
+ dump["collections"][lookup_key] = formatted_cache
690
+
691
+ # Calculate total cache size
692
+ total_size = sum(len(collection) for collection in dump["collections"].values())
693
+ dump["cache_size"] = total_size
694
+
695
+ return dump
696
+
697
+ def _dump_to_disk(self) -> None:
698
+ """Dump the in-memory cache to a shared JSON file.
699
+
700
+ This method is automatically called on program exit via atexit.
701
+ It reads any existing cache data, merges it with this instance's cache,
702
+ and writes the combined result back. Multiple Memoizer instances
703
+ contribute to the same file additively.
704
+
705
+ If self._sub_key is set and non-empty, cache entries are stored under
706
+ cache_entries[sub_key]. Otherwise, they're merged directly into cache_entries.
707
+
708
+ Cache entries are formatted as {"params": <encoded_params>, "result": <encoded_result>}
709
+ for better human readability.
710
+
711
+ The filepath where the cache was dumped is logged.
712
+ """
713
+ # Skip if cache is empty
714
+ if not self._cache._memory:
715
+ return
716
+
717
+ # Ensure parent directory exists
718
+ self._shared_cache_filepath.parent.mkdir(parents=True, exist_ok=True)
719
+
720
+ # Acquire file lock to ensure thread/process safety
721
+ flock = FileLock(str(self._shared_cache_lockfile))
722
+ with locks._acquire_flock_with_timeout(flock):
723
+ existing_dump = self._read_dump_from_disk()
724
+ dump = self._prepare_dump(existing_dump)
725
+ self._write_dump_to_disk(dump)
726
+
727
+ def _maybe_prepopulate_from_dump(self) -> None:
728
+ """Pre-populate cache entries from a dump file if configured.
729
+
730
+ Checks the CACHE_DUMP_FILE_PATH config option for a path to a JSON dump file
731
+ produced by IS_DUMP_MEMOIZER_CACHE_ENABLED. If a valid path is provided and
732
+ the file exists, loads cache entries from it into the in-memory cache.
733
+
734
+ For Memoizer instances without a sub_key, loads entries from the root cache_entries.
735
+ For Memoizer instances with a sub_key, loads entries from cache_entries[sub_key].
736
+
737
+ This method is called during __init__ to pre-populate the cache.
738
+ """
739
+ dump_file_path = config.CACHE_DUMP_FILE_PATH()
740
+
741
+ # Skip if no dump file configured
742
+ if not dump_file_path:
743
+ return
744
+
745
+ # Read the dump file using the helper method
746
+ dump = self._read_dump_from_disk(Path(dump_file_path))
747
+ if dump is None:
748
+ return
749
+
750
+ # Extract entries to load from the dump
751
+ entries_to_load = self._extract_entries_from_dump(dump)
752
+ if not entries_to_load:
753
+ return
754
+
755
+ # Populate the cache
756
+ self._populate_cache_from_entries(entries_to_load)
757
+
758
+ # Log the result
759
+ if self._sub_key:
760
+ logger.log(
761
+ logging.INFO,
762
+ "Loaded %d cache entries from %s (sub_key=%s)",
763
+ len(entries_to_load),
764
+ dump_file_path,
765
+ self._sub_key,
766
+ )
767
+ else:
768
+ logger.log(
769
+ logging.INFO,
770
+ "Loaded %d cache entries from %s",
771
+ len(entries_to_load),
772
+ dump_file_path,
773
+ )
774
+
775
+ def _extract_entries_from_dump(self, dump: CacheDump) -> dict[str, CacheDumpEntry]:
776
+ """Extract cache entries from a dump based on sub_key.
777
+
778
+ Args:
779
+ dump: The cache dump to extract entries from.
780
+
781
+ Returns:
782
+ Dictionary of cache entries to load.
783
+ """
784
+ collections = dump.get("collections", {})
785
+ # JSON serializes None keys as "null" string, so we need to handle both cases
786
+ # When reading from JSON, the key will be "null" (string), not None
787
+ lookup_key = "null" if self._sub_key is None else self._sub_key
788
+ return collections.get(lookup_key, {})
789
+
790
+ def _populate_cache_from_entries(self, entries: dict[str, CacheDumpEntry]) -> None:
791
+ """Populate the in-memory cache from dump entries.
792
+
793
+ Args:
794
+ entries: Dictionary of cache entries to load.
795
+ """
796
+ for key, entry in entries.items():
797
+ cache_entry = CacheEntry(
798
+ encoded_params=entry["params"],
799
+ encoded_result=entry["result"],
800
+ )
801
+ self._cache.insert(key, cache_entry)
802
+
803
+ def record(
804
+ self,
805
+ custom_params_encoder: Callable[_P, object] | None = None,
806
+ custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
807
+ ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
808
+ """Record a function call result with custom encoding.
809
+
810
+ This is a decorator that wraps a function to enable memoization
811
+ with custom encoding/decoding logic.
812
+
813
+ Args:
814
+ custom_params_encoder: Optional encoder for function parameters.
815
+ If None, parameters are pickled directly.
816
+ custom_result_encoder: Optional encoder factory for function results.
817
+ First receives the underlying function, then takes
818
+ function parameters and returns an encoder function
819
+ that converts R -> _EncodedR.
820
+
821
+ Returns:
822
+ A decorator function that can be applied to functions.
823
+
824
+ Example:
825
+ @memoizer.record(
826
+ custom_params_encoder=my_param_encoder,
827
+ custom_result_encoder=my_result_encoder_factory,
828
+ )
829
+ def expensive_function(x, y):
830
+ return x + y
831
+ """
832
+
833
+ def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
834
+ """Wrap the function to enable memoization.
835
+
836
+ Args:
837
+ fn: The function to wrap.
838
+
839
+ Returns:
840
+ A wrapped version of the function.
841
+ """
842
+ # If caching is disabled, return the original function unchanged
843
+ if not config.IS_CACHING_MODULE_ENABLED():
844
+ return fn
845
+
846
+ @functools.wraps(fn)
847
+ def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
848
+ """Call the original function and cache the result.
849
+
850
+ Args:
851
+ *args: Positional arguments to pass to the function.
852
+ **kwargs: Keyword arguments to pass to the function.
853
+
854
+ Returns:
855
+ The result of calling the original function.
856
+ """
857
+ # Call the function to compute the result
858
+ result = fn(*args, **kwargs)
859
+
860
+ # Generate cache key from parameters
861
+ cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
862
+
863
+ # Encode params for human-readable dump
864
+ if custom_params_encoder is not None:
865
+ encoded_params = custom_params_encoder(*args, **kwargs)
866
+ else:
867
+ encoded_params = {
868
+ "args": args,
869
+ "kwargs": kwargs,
870
+ }
871
+
872
+ # Encode the result if encoder is provided
873
+ if custom_result_encoder is not None:
874
+ # Get the encoder function by calling the factory factory:
875
+ # 1. First call with fn to get params_to_encoder factory
876
+ # 2. Second call with params to get the encoder function
877
+ params_to_encoder = custom_result_encoder(fn)
878
+ encoder_fn = params_to_encoder(*args, **kwargs)
879
+ encoded_result = encoder_fn(result)
880
+ else:
881
+ encoded_result = result
882
+
883
+ # Check for deferred recording
884
+ if isinstance(encoded_result, DeferredRecording):
885
+ # Track the pending deferred recording
886
+ self._pending_deferred[cache_key] = encoded_result
887
+ # Register the callback - handles race condition if encoded_result already completed
888
+ encoded_result.register_callback(
889
+ functools.partial(
890
+ self._finalize_deferred_recording,
891
+ cache_key,
892
+ encoded_params,
893
+ )
894
+ )
895
+ return result # Return without caching
896
+
897
+ # Store CacheEntry in cache
898
+ cache_entry = CacheEntry(
899
+ encoded_params=encoded_params,
900
+ encoded_result=encoded_result,
901
+ )
902
+ self._cache.insert(cache_key, cache_entry)
903
+
904
+ # Return the original result (not the encoded version)
905
+ return result
906
+
907
+ return inner
908
+
909
+ return wrapper
910
+
911
+ def replay(
912
+ self,
913
+ custom_params_encoder: Callable[_P, object] | None = None,
914
+ custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
915
+ ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
916
+ """Replay a cached function result without executing the function.
917
+
918
+ This is a decorator that retrieves cached results instead of executing
919
+ the function. Raises an exception if no cached result exists.
920
+
921
+ Args:
922
+ custom_params_encoder: Optional encoder for function parameters.
923
+ If None, parameters are pickled directly.
924
+ custom_result_decoder: Optional decoder factory for cached results.
925
+ First receives the underlying function, then takes
926
+ function parameters and returns a decoder function
927
+ that converts _EncodedR -> R.
928
+
929
+ Returns:
930
+ A decorator function that can be applied to functions.
931
+
932
+ Example:
933
+ @memoizer.replay(
934
+ custom_params_encoder=my_param_encoder,
935
+ custom_result_decoder=my_result_decoder_factory,
936
+ )
937
+ def expensive_function(x, y):
938
+ return x + y
939
+ """
940
+
941
+ def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
942
+ """Wrap the function to retrieve from cache.
943
+
944
+ Args:
945
+ fn: The function to wrap (not actually called).
946
+
947
+ Returns:
948
+ A wrapped version of the function.
949
+ """
950
+ # If caching is disabled, always raise KeyError (cache miss)
951
+ if not config.IS_CACHING_MODULE_ENABLED():
952
+
953
+ @functools.wraps(fn)
954
+ def always_miss(*args: _P.args, **kwargs: _P.kwargs) -> _R:
955
+ raise KeyError("Caching is disabled")
956
+
957
+ return always_miss
958
+
959
+ @functools.wraps(fn)
960
+ def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
961
+ """Retrieve the cached result without calling the function.
962
+
963
+ Args:
964
+ *args: Positional arguments to generate the cache key.
965
+ **kwargs: Keyword arguments to generate the cache key.
966
+
967
+ Returns:
968
+ The cached result (decoded if decoder is provided).
969
+
970
+ Raises:
971
+ KeyError: If no cached result exists for the given parameters.
972
+ """
973
+ # Generate cache key from parameters
974
+ cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
975
+
976
+ # Check if result is cached
977
+ cached_hit = self._cache.get(cache_key)
978
+ if cached_hit is not None:
979
+ # Extract the cached value
980
+ cache_entry = cached_hit.value
981
+
982
+ # Decode and return the cached result
983
+ if custom_result_decoder is not None:
984
+ # Get the decoder function by calling the factory factory:
985
+ # 1. First call with fn to get params_to_decoder factory
986
+ # 2. Second call with params to get the decoder function
987
+ params_to_decoder = custom_result_decoder(fn)
988
+ decoder_fn = params_to_decoder(*args, **kwargs)
989
+ return decoder_fn(cast(_EncodedR, cache_entry.encoded_result))
990
+ return cast(_R, cache_entry.encoded_result)
991
+
992
+ # Check for pending deferred recording with interim result
993
+ pending = self._pending_deferred.get(cache_key)
994
+ if pending is not None:
995
+ interim = pending.get_interim_result()
996
+ if interim is not None:
997
+ return interim.value
998
+
999
+ raise KeyError(f"No cached result found for key: {cache_key}")
1000
+
1001
+ return inner
1002
+
1003
+ return wrapper
1004
+
1005
+
1006
+ class PersistentMemoizer(_BaseMemoizer):
1007
+ """Persistent memoization interface for caching function results to disk.
1008
+
1009
+ This class provides methods for recording, retrieving, and managing
1010
+ cached function results using a two-level cache strategy:
1011
+ 1. In-memory cache (fast, checked first) - via Memoizer instance
1012
+ 2. On-disk cache (persistent, checked on memory miss)
1013
+
1014
+ Results are persisted across process restarts.
1015
+
1016
+ On program exit, the in-memory cache entries are automatically dumped to
1017
+ the shared JSON file. If sub_dir is non-empty, entries are stored under
1018
+ a nested structure based on the sub_dir. If sub_dir is empty, entries are
1019
+ merged directly into the root cache_entries.
1020
+
1021
+ Note: Use this over functools.cache when you need to support parameters
1022
+ that functools.cache cannot handle, custom result encoding and/or decoding,
1023
+ or when you need disk caching to persist results across program boundaries.
1024
+ """
1025
+
1026
+ def __init__(self, sub_dir: PathLike[str] | None = None) -> None:
1027
+ """Initialize the PersistentMemoizer with two-level caching.
1028
+
1029
+ Args:
1030
+ sub_dir: Optional subdirectory within the cache directory for
1031
+ organizing cached results. Defaults to empty string if not specified.
1032
+ If non-empty, cache entries will be stored under cache_entries[sub_dir].
1033
+ If empty, cache entries are merged into root cache_entries.
1034
+ """
1035
+ # Use a Memoizer instance for in-memory caching
1036
+ self._memoizer: Memoizer = Memoizer(sub_key=str(sub_dir) if sub_dir else None)
1037
+ # Store on-disk cache as a separate attribute
1038
+ self._disk_cache: implementations._OnDiskCacheImpl = (
1039
+ implementations._OnDiskCacheImpl(sub_dir=sub_dir)
1040
+ )
1041
+ # Register with clear_on_fresh_cache for fresh_cache() integration
1042
+ clear_on_fresh_cache(self)
1043
+
1044
+ def cache_clear(self) -> None:
1045
+ """Clear the on-disk cache.
1046
+
1047
+ This method removes the on-disk cache directory. The in-memory cache
1048
+ is cleared automatically by the underlying Memoizer instance, which
1049
+ registers itself with clear_on_fresh_cache.
1050
+
1051
+ This is called by the fresh_cache() context manager via clear_on_fresh_cache
1052
+ registration.
1053
+ """
1054
+ # Clear on-disk cache by removing the cache directory
1055
+ # Note: in-memory cache is cleared by the Memoizer's own cache_clear
1056
+ if self._disk_cache._cache_dir.exists():
1057
+ shutil.rmtree(self._disk_cache._cache_dir, ignore_errors=True)
1058
+
1059
+ def record(
1060
+ self,
1061
+ custom_params_encoder: Callable[_P, object] | None = None,
1062
+ custom_result_encoder: ResultEncoderFactory[_P, _R, _EncodedR] | None = None,
1063
+ ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
1064
+ """Record a function call result with custom encoding to both caches.
1065
+
1066
+ This is a decorator that wraps a function to enable memoization
1067
+ with custom encoding/decoding logic. Results are stored in both
1068
+ the in-memory cache and the on-disk cache.
1069
+
1070
+ This method delegates to the underlying Memoizer for memory caching,
1071
+ then adds disk persistence. For deferred recordings, it registers
1072
+ an additional callback to persist to disk when the recording completes.
1073
+
1074
+ Args:
1075
+ custom_params_encoder: Optional encoder for function parameters.
1076
+ If None, parameters are pickled directly.
1077
+ custom_result_encoder: Optional encoder factory for function results.
1078
+ First receives the underlying function, then takes
1079
+ function parameters and returns an encoder function
1080
+ that converts R -> _EncodedR.
1081
+
1082
+ Returns:
1083
+ A decorator function that can be applied to functions.
1084
+
1085
+ Example:
1086
+ @persistent_memoizer.record(
1087
+ custom_params_encoder=my_param_encoder,
1088
+ custom_result_encoder=my_result_encoder_factory,
1089
+ )
1090
+ def expensive_function(x, y):
1091
+ return x + y
1092
+ """
1093
+
1094
+ def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
1095
+ """Wrap the function to enable memoization.
1096
+
1097
+ Args:
1098
+ fn: The function to wrap.
1099
+
1100
+ Returns:
1101
+ A wrapped version of the function.
1102
+ """
1103
+ # If caching is disabled, return the original function unchanged
1104
+ if not config.IS_CACHING_MODULE_ENABLED():
1105
+ return fn
1106
+
1107
+ # Get the memory-cached version from the memoizer
1108
+ memory_record_fn = self._memoizer.record(
1109
+ custom_params_encoder, custom_result_encoder
1110
+ )(fn)
1111
+
1112
+ @functools.wraps(fn)
1113
+ def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
1114
+ """Call the original function and cache the result in both caches.
1115
+
1116
+ Args:
1117
+ *args: Positional arguments to pass to the function.
1118
+ **kwargs: Keyword arguments to pass to the function.
1119
+
1120
+ Returns:
1121
+ The result of calling the original function.
1122
+ """
1123
+ # Call the memory-cached version (which calls fn and caches in memory)
1124
+ result = memory_record_fn(*args, **kwargs)
1125
+
1126
+ # Also store in disk cache
1127
+ cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
1128
+
1129
+ # Check if this was a deferred recording
1130
+ pending = self._memoizer._pending_deferred.get(cache_key)
1131
+ if pending is not None:
1132
+ # Deferred recording - register our own callback for disk persistence
1133
+ # By the time our callback runs, Memoizer's callback will have
1134
+ # already inserted the cache entry, so we just read and persist it
1135
+ pending.register_callback(
1136
+ functools.partial(self._persist_to_disk, cache_key)
1137
+ )
1138
+ return result
1139
+
1140
+ # Persist the cache entry to disk
1141
+ self._persist_to_disk(cache_key)
1142
+
1143
+ return result
1144
+
1145
+ return inner
1146
+
1147
+ return wrapper
1148
+
1149
+ def _persist_to_disk(
1150
+ self,
1151
+ cache_key: str,
1152
+ _callback_result: object = None,
1153
+ ) -> None:
1154
+ """Persist a cache entry to disk.
1155
+
1156
+ This method handles disk persistence for both immediate and deferred recordings:
1157
+ - For immediate recordings: called directly after memory caching
1158
+ - For deferred recordings: registered as a callback that runs after the
1159
+ Memoizer's callback has inserted the entry into memory
1160
+
1161
+ Args:
1162
+ cache_key: The cache key for the entry.
1163
+ _callback_result: Unused. When called as a callback from DeferredRecording,
1164
+ this receives the encoded result, but we ignore it and
1165
+ read the full CacheEntry from the Memoizer's cache instead.
1166
+ """
1167
+ # Always read from memory cache - Memoizer's callback has already inserted it
1168
+ cached_hit = self._memoizer._cache.get(cache_key)
1169
+ assert cached_hit, "Cache entry must exist in memory cache"
1170
+ cache_entry = cached_hit.value
1171
+
1172
+ # Store the full CacheEntry in disk cache for easier debugging
1173
+ pickled_entry: bytes = pickle.dumps(cache_entry)
1174
+ self._disk_cache.insert(cache_key, pickled_entry)
1175
+
1176
+ def replay(
1177
+ self,
1178
+ custom_params_encoder: Callable[_P, object] | None = None,
1179
+ custom_result_decoder: ResultDecoderFactory[_P, _R, _EncodedR] | None = None,
1180
+ ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
1181
+ """Replay a cached function result without executing the function.
1182
+
1183
+ This is a decorator that retrieves cached results using a two-level
1184
+ cache strategy. It checks the in-memory cache first (fast), then
1185
+ falls back to the on-disk cache. If found on disk, the result is
1186
+ cached in memory for future access.
1187
+
1188
+ Args:
1189
+ custom_params_encoder: Optional encoder for function parameters.
1190
+ If None, parameters are pickled directly.
1191
+ custom_result_decoder: Optional decoder factory for cached results.
1192
+ First receives the underlying function, then takes
1193
+ function parameters and returns a decoder function
1194
+ that converts _EncodedR -> R.
1195
+
1196
+ Returns:
1197
+ A decorator function that can be applied to functions.
1198
+
1199
+ Example:
1200
+ @persistent_memoizer.replay(
1201
+ custom_params_encoder=my_param_encoder,
1202
+ custom_result_decoder=my_result_decoder_factory,
1203
+ )
1204
+ def expensive_function(x, y):
1205
+ return x + y
1206
+ """
1207
+
1208
+ def wrapper(fn: Callable[_P, _R]) -> Callable[_P, _R]:
1209
+ """Wrap the function to retrieve from cache.
1210
+
1211
+ Args:
1212
+ fn: The function to wrap (not actually called).
1213
+
1214
+ Returns:
1215
+ A wrapped version of the function.
1216
+ """
1217
+ # If caching is disabled, always raise KeyError (cache miss)
1218
+ if not config.IS_CACHING_MODULE_ENABLED():
1219
+
1220
+ @functools.wraps(fn)
1221
+ def always_miss(*args: _P.args, **kwargs: _P.kwargs) -> _R:
1222
+ raise KeyError("Caching is disabled")
1223
+
1224
+ return always_miss
1225
+
1226
+ # Get the memory replay function
1227
+ memory_replay_fn = self._memoizer.replay(
1228
+ custom_params_encoder, custom_result_decoder
1229
+ )(fn)
1230
+
1231
+ @functools.wraps(fn)
1232
+ def inner(*args: _P.args, **kwargs: _P.kwargs) -> _R:
1233
+ """Retrieve the cached result without calling the function.
1234
+
1235
+ Checks memory cache first, then disk cache. Populates memory
1236
+ cache from disk on a disk hit.
1237
+
1238
+ Args:
1239
+ *args: Positional arguments to generate the cache key.
1240
+ **kwargs: Keyword arguments to generate the cache key.
1241
+
1242
+ Returns:
1243
+ The cached result (decoded if decoder is provided).
1244
+
1245
+ Raises:
1246
+ KeyError: If no cached result exists for the given parameters.
1247
+ """
1248
+ # Try memory cache first via memoizer
1249
+ try:
1250
+ return memory_replay_fn(*args, **kwargs)
1251
+ except KeyError:
1252
+ pass # Memory miss, check disk
1253
+
1254
+ # Memory miss - check disk cache
1255
+ cache_key = self._make_key(custom_params_encoder, *args, **kwargs)
1256
+ disk_hit = self._disk_cache.get(cache_key)
1257
+ if disk_hit is not None:
1258
+ # Disk cache hit - unpickle the CacheEntry
1259
+ pickled_value = disk_hit.value
1260
+ cache_entry = cast(CacheEntry, pickle.loads(pickled_value))
1261
+
1262
+ # Populate memory cache for future access
1263
+ self._memoizer._cache.insert(cache_key, cache_entry)
1264
+
1265
+ # Decode and return
1266
+ if custom_result_decoder is not None:
1267
+ # Get the decoder function by calling the factory factory:
1268
+ # 1. First call with fn to get params_to_decoder factory
1269
+ # 2. Second call with params to get the decoder function
1270
+ params_to_decoder = custom_result_decoder(fn)
1271
+ decoder_fn = params_to_decoder(*args, **kwargs)
1272
+ return decoder_fn(cast(_EncodedR, cache_entry.encoded_result))
1273
+ return cast(_R, cache_entry.encoded_result)
1274
+
1275
+ # Complete miss
1276
+ raise KeyError(f"No cached result found for key: {cache_key}")
1277
+
1278
+ return inner
1279
+
1280
+ return wrapper
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/locks.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lock acquisition utilities
2
+
3
+ This module provides safe and unsafe lock acquisition functions for both threading.Lock
4
+ and FileLock objects, with configurable timeout behaviors. It supports three timeout modes:
5
+ blocking (infinite wait), non-blocking (immediate), and blocking with timeout (finite wait).
6
+
7
+ The module offers both context manager and manual acquisition patterns:
8
+ - Safe acquisition: Uses context managers that automatically handle lock release
9
+ - Unsafe acquisition: Manual acquisition that requires explicit release by the caller
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from contextlib import _GeneratorContextManager, contextmanager, ExitStack
15
+ from typing import TYPE_CHECKING, TypeAlias
16
+ from typing_extensions import Protocol
17
+
18
+ from filelock import BaseFileLock, Timeout
19
+
20
+ from . import exceptions
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Generator
25
+ from threading import Lock
26
+
27
+ from .implementations import _CacheImpl
28
+
29
+
30
+ _LockContextManager: TypeAlias = _GeneratorContextManager[None, None, None]
31
+
32
+
33
+ class _LockProtocol(Protocol): # noqa: PYI046
34
+ def __call__(self, timeout: float | None = None) -> _LockContextManager: ...
35
+
36
+
37
+ # Infinite timeout - blocks indefinitely until lock is acquired.
38
+ _BLOCKING: float = -1
39
+ # No timeout - returns immediately if lock cannot be acquired.
40
+ _NON_BLOCKING: float = 0
41
+ # Finite timeout - blocks for a specified duration before raising a timeout error.
42
+ _BLOCKING_WITH_TIMEOUT: float = 60.0
43
+ # Default timeout for lock acquisition.
44
+ _DEFAULT_TIMEOUT: float = _BLOCKING_WITH_TIMEOUT
45
+
46
+
47
+ @contextmanager
48
+ def _acquire_lock_with_timeout(
49
+ lock: Lock,
50
+ timeout: float | None = None,
51
+ ) -> Generator[None, None, None]:
52
+ """Context manager that safely acquires a threading.Lock with timeout and automatically releases it.
53
+
54
+ This function provides a safe way to acquire a lock with timeout support, ensuring
55
+ the lock is always released even if an exception occurs during execution.
56
+
57
+ Args:
58
+ lock: The threading.Lock object to acquire
59
+ timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
60
+ - Use _BLOCKING (-1.0) for infinite wait
61
+ - Use _NON_BLOCKING (0.0) for immediate return
62
+ - Use positive value for finite timeout
63
+
64
+ Yields:
65
+ None: Yields control to the caller while holding the lock
66
+
67
+ Raises:
68
+ LockTimeoutError: If the lock cannot be acquired within the timeout period
69
+
70
+ Example:
71
+ with _acquire_lock_with_timeout(my_lock, timeout=30.0):
72
+ # Critical section - lock is held
73
+ perform_critical_operation()
74
+ # Lock is automatically released here
75
+ """
76
+ _unsafe_acquire_lock_with_timeout(lock, timeout=timeout)
77
+
78
+ try:
79
+ yield
80
+ finally:
81
+ lock.release()
82
+
83
+
84
+ def _unsafe_acquire_lock_with_timeout(lock: Lock, timeout: float | None = None) -> None:
85
+ """Acquire a threading.Lock with timeout without automatic release (unsafe).
86
+
87
+ This function acquires a lock with timeout support but does NOT automatically
88
+ release it. The caller is responsible for releasing the lock explicitly.
89
+ Use this only when you need manual control over lock lifetime.
90
+
91
+ Args:
92
+ lock: The threading.Lock object to acquire
93
+ timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
94
+ - Use _BLOCKING (-1.0) for infinite wait
95
+ - Use _NON_BLOCKING (0.0) for immediate return
96
+ - Use positive value for finite timeout
97
+
98
+ Raises:
99
+ LockTimeoutError: If the lock cannot be acquired within the timeout period
100
+
101
+ Warning:
102
+ This is an "unsafe" function because it does not automatically release
103
+ the lock. Always call lock.release() when done, preferably in a try/finally
104
+ block or use the safe _acquire_lock_with_timeout context manager instead.
105
+
106
+ Example:
107
+ lock = Lock()
108
+ try:
109
+ _unsafe_acquire_lock_with_timeout(lock, timeout=30.0)
110
+ # Critical section - lock is held
111
+ perform_critical_operation()
112
+ finally:
113
+ lock.release() # Must manually release!
114
+ """
115
+ _timeout: float = timeout if timeout is not None else _DEFAULT_TIMEOUT
116
+ if not lock.acquire(timeout=_timeout):
117
+ raise exceptions.LockTimeoutError(lock, _timeout)
118
+
119
+
120
+ @contextmanager
121
+ def _acquire_flock_with_timeout(
122
+ flock: BaseFileLock,
123
+ timeout: float | None = None,
124
+ ) -> Generator[None, None, None]:
125
+ """Context manager that safely acquires a FileLock with timeout and automatically releases it.
126
+
127
+ This function provides a safe way to acquire a file lock with timeout support, ensuring
128
+ the lock is always released even if an exception occurs during execution.
129
+
130
+ Args:
131
+ flock: The FileLock object to acquire
132
+ timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
133
+ - Use _BLOCKING (-1.0) for infinite wait
134
+ - Use _NON_BLOCKING (0.0) for immediate return
135
+ - Use positive value for finite timeout
136
+
137
+ Yields:
138
+ None: Yields control to the caller while holding the file lock
139
+
140
+ Raises:
141
+ FileLockTimeoutError: If the file lock cannot be acquired within the timeout period
142
+
143
+ Example:
144
+ flock = FileLock("/tmp/my_process.lock")
145
+ with _acquire_flock_with_timeout(flock, timeout=30.0):
146
+ # Critical section - file lock is held
147
+ perform_exclusive_file_operation()
148
+ # File lock is automatically released here
149
+ """
150
+ _unsafe_acquire_flock_with_timeout(flock, timeout=timeout)
151
+
152
+ try:
153
+ yield
154
+ finally:
155
+ flock.release()
156
+
157
+
158
+ def _unsafe_acquire_flock_with_timeout(
159
+ flock: BaseFileLock,
160
+ timeout: float | None,
161
+ ) -> None:
162
+ """Acquire a FileLock with timeout without automatic release (unsafe).
163
+
164
+ This function acquires a file lock with timeout support but does NOT automatically
165
+ release it. The caller is responsible for releasing the lock explicitly.
166
+ Use this only when you need manual control over lock lifetime.
167
+
168
+ Args:
169
+ flock: The FileLock object to acquire
170
+ timeout: Timeout in seconds. If None, uses _DEFAULT_TIMEOUT.
171
+ - Use _BLOCKING (-1.0) for infinite wait
172
+ - Use _NON_BLOCKING (0.0) for immediate return
173
+ - Use positive value for finite timeout
174
+
175
+ Raises:
176
+ FileLockTimeoutError: If the file lock cannot be acquired within the timeout period
177
+
178
+ Warning:
179
+ This is an "unsafe" function because it does not automatically release
180
+ the lock. Always call flock.release() when done, preferably in a try/finally
181
+ block or use the safe _acquire_flock_with_timeout context manager instead.
182
+
183
+ Example:
184
+ flock = FileLock("/tmp/my_process.lock")
185
+ try:
186
+ _unsafe_acquire_flock_with_timeout(flock, timeout=30.0)
187
+ # Critical section - file lock is held
188
+ perform_exclusive_file_operation()
189
+ finally:
190
+ flock.release() # Must manually release!
191
+ """
192
+ _timeout: float = timeout if timeout is not None else _DEFAULT_TIMEOUT
193
+ try:
194
+ _ = flock.acquire(timeout=_timeout)
195
+ except Timeout as err:
196
+ raise exceptions.FileLockTimeoutError(flock, _timeout) from err
197
+
198
+
199
+ @contextmanager
200
+ def _acquire_many_impl_locks_with_timeout(
201
+ *impls: _CacheImpl,
202
+ timeout: float | None = None,
203
+ ) -> Generator[None, None, None]:
204
+ with ExitStack() as stack:
205
+ for impl in impls:
206
+ stack.enter_context(impl.lock(timeout))
207
+ yield
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/memoizers.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from .interfaces import PersistentMemoizer
4
+
5
+
6
+ # Memoizer for _should_pad in pad_mm
7
+ should_pad_memoizer = PersistentMemoizer(sub_dir=Path("should_pad"))
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/caching/utils.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility functions
2
+
3
+ This module provides helper functions for LRU caching decorators used
4
+ throughout the caching system.
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ from functools import lru_cache, wraps
9
+ from typing_extensions import ParamSpec, TypedDict, TypeVar
10
+
11
+ from torch import Tensor
12
+
13
+
14
+ # Type specification for function parameters
15
+ P = ParamSpec("P")
16
+ # Type variable for function return values
17
+ R = TypeVar("R")
18
+
19
+
20
+ def _lru_cache(fn: Callable[P, R]) -> Callable[P, R]:
21
+ """LRU cache decorator with TypeError fallback.
22
+
23
+ Provides LRU caching with a fallback mechanism that calls the original
24
+ function if caching fails due to unhashable arguments. Uses a cache
25
+ size of 64 with typed comparison.
26
+
27
+ Args:
28
+ fn: The function to be cached.
29
+
30
+ Returns:
31
+ A wrapper function that attempts caching with fallback to original function.
32
+ """
33
+ cached_fn = lru_cache(maxsize=64, typed=True)(fn)
34
+
35
+ @wraps(fn)
36
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
37
+ try:
38
+ return cached_fn(*args, **kwargs)
39
+ except TypeError:
40
+ return fn(*args, **kwargs)
41
+
42
+ return wrapper
43
+
44
+
45
+ class EncodedTensor(TypedDict):
46
+ """TypedDict for encoded tensor metadata."""
47
+
48
+ shape: tuple[int, ...]
49
+ stride: tuple[int, ...]
50
+ dtype: str
51
+
52
+
53
+ def _encode_tensor(t: Tensor) -> EncodedTensor:
54
+ """Encode a tensor's metadata into a JSON-serializable dict.
55
+
56
+ Args:
57
+ t: PyTorch tensor to encode
58
+
59
+ Returns:
60
+ Dict containing shape, stride, and dtype information
61
+ """
62
+ return EncodedTensor(
63
+ shape=tuple(t.shape),
64
+ stride=tuple(t.stride()),
65
+ dtype=str(t.dtype),
66
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/compile_tasks.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import linecache
5
+ import os
6
+ import sys
7
+ import time
8
+ import warnings
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ from torch._utils_internal import log_triton_builds
14
+
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+
19
+ from torch._inductor.runtime.triton_heuristics import CachingAutotuner
20
+
21
+
22
+ def _reload_python_module(
23
+ key: str, path: str, set_sys_modules: bool = True
24
+ ) -> ModuleType:
25
+ with open(path) as f:
26
+ try:
27
+ code = compile(f.read(), path, "exec", dont_inherit=True)
28
+ except Exception as e:
29
+ raise RuntimeError(
30
+ f"Failed to import {path}\n{type(e).__name__}: {e}"
31
+ ) from None
32
+ mod = ModuleType(f"{__name__}.{key}")
33
+ mod.__file__ = path
34
+ mod.key = key # type: ignore[attr-defined]
35
+ exec(code, mod.__dict__, mod.__dict__)
36
+ if set_sys_modules:
37
+ sys.modules[mod.__name__] = mod
38
+ return mod
39
+
40
+
41
+ @functools.cache
42
+ def _set_triton_ptxas_path() -> None:
43
+ if os.environ.get("TRITON_PTXAS_PATH") is not None:
44
+ return
45
+ ptxas = Path(__file__).absolute().parents[2] / "bin" / "ptxas"
46
+ if not ptxas.exists():
47
+ return
48
+ if ptxas.is_file() and os.access(ptxas, os.X_OK):
49
+ os.environ["TRITON_PTXAS_PATH"] = str(ptxas)
50
+ else:
51
+ warnings.warn(f"{ptxas} exists but is not an executable")
52
+
53
+
54
+ def _set_triton_libdevice_path() -> None:
55
+ """
56
+ Use the CUDA toolkit's libdevice instead of Triton's bundled version.
57
+ This ensures Triton's pow matches CUDA's powf for bitwise precision.
58
+ Gated by config.eager_numerics.use_pytorch_libdevice.
59
+ """
60
+ from torch._inductor import config
61
+
62
+ if not config.eager_numerics.use_pytorch_libdevice:
63
+ return
64
+
65
+ _set_triton_libdevice_path_impl()
66
+
67
+
68
+ def _set_triton_libdevice_path_impl() -> None:
69
+ try:
70
+ from triton import knobs
71
+ except ImportError:
72
+ return
73
+
74
+ env_path = os.environ.get("TRITON_LIBDEVICE_PATH")
75
+ if env_path is not None:
76
+ knobs.nvidia.libdevice_path = env_path
77
+ return
78
+
79
+ if knobs.nvidia.libdevice_path is not None:
80
+ return
81
+
82
+ try:
83
+ from torch.utils.cpp_extension import CUDA_HOME
84
+
85
+ if CUDA_HOME is None:
86
+ warnings.warn(
87
+ "CUDA_HOME not set; using Triton's bundled libdevice which may "
88
+ "cause minor precision differences in pow operations. "
89
+ "To fix: set TRITON_LIBDEVICE_PATH to your CUDA toolkit's libdevice, "
90
+ "e.g., export TRITON_LIBDEVICE_PATH=/usr/local/cuda/nvvm/libdevice/libdevice.10.bc",
91
+ stacklevel=3,
92
+ )
93
+ return
94
+ libdevice = Path(CUDA_HOME) / "nvvm" / "libdevice" / "libdevice.10.bc"
95
+ if libdevice.is_file():
96
+ knobs.nvidia.libdevice_path = str(libdevice)
97
+ # Also set env var so subprocess compile workers inherit it
98
+ os.environ["TRITON_LIBDEVICE_PATH"] = str(libdevice)
99
+ else:
100
+ warnings.warn(
101
+ f"CUDA libdevice not found at {libdevice}; using Triton's bundled "
102
+ "libdevice which may cause minor precision differences in pow operations. "
103
+ "To fix: set TRITON_LIBDEVICE_PATH to your CUDA toolkit's libdevice, "
104
+ "e.g., export TRITON_LIBDEVICE_PATH=/usr/local/cuda/nvvm/libdevice/libdevice.10.bc",
105
+ stacklevel=3,
106
+ )
107
+ except ImportError:
108
+ warnings.warn(
109
+ "torch.utils.cpp_extension not available; using Triton's bundled "
110
+ "libdevice which may cause minor precision differences in pow operations. "
111
+ "To fix: set TRITON_LIBDEVICE_PATH to your CUDA toolkit's libdevice, "
112
+ "e.g., export TRITON_LIBDEVICE_PATH=/usr/local/cuda/nvvm/libdevice/libdevice.10.bc",
113
+ stacklevel=3,
114
+ )
115
+
116
+
117
+ def _worker_compile_triton(
118
+ load_kernel: Callable[[], CachingAutotuner],
119
+ extra_env: dict[str, str],
120
+ extra_config: dict[str, Any],
121
+ ) -> tuple[CachingAutotuner, int]:
122
+ _set_triton_ptxas_path()
123
+ os.environ.update(extra_env)
124
+ # Set libdevice path if passed via env from main process
125
+ libdevice_path = extra_env.get("TRITON_LIBDEVICE_PATH")
126
+ if libdevice_path:
127
+ try:
128
+ from triton import knobs
129
+
130
+ knobs.nvidia.libdevice_path = libdevice_path
131
+ except ImportError:
132
+ pass
133
+ from torch._inductor import config
134
+
135
+ with config.patch(extra_config):
136
+ fail = None
137
+ try:
138
+ start_ns = time.time_ns()
139
+ kernel = load_kernel()
140
+ kernel.precompile(warm_cache_only=True)
141
+ elapsed_ns = time.time_ns() - start_ns
142
+ kernel.prepare_for_pickle()
143
+ # We can release this memory in the compile subprocesses:
144
+ linecache.clearcache()
145
+ return kernel, elapsed_ns // 1000
146
+ except Exception as e:
147
+ fail = str(e)
148
+ raise
149
+ finally:
150
+ log_triton_builds(fail=fail)
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/coordinate_descent_tuner.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ import copy
3
+ import itertools
4
+ import logging
5
+ from collections.abc import Callable
6
+ from typing import TYPE_CHECKING
7
+
8
+ from torch.utils._ordered_set import OrderedSet
9
+
10
+ from ..utils import get_max_numwarps
11
+ from .hints import TRITON_MAX_BLOCK
12
+ from .runtime_utils import red_text, triton_config_to_hashable
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from .triton_compat import triton
17
+
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ def get_field(config, name):
23
+ if name == "num_warps":
24
+ return config.num_warps
25
+ elif name == "num_stages":
26
+ return config.num_stages
27
+ elif name == "waves_per_eu":
28
+ return config.kwargs.get(name, int(8 // config.num_warps))
29
+ else:
30
+ return config.kwargs.get(name, None)
31
+
32
+
33
+ def set_field(config, name, value):
34
+ if name == "num_warps":
35
+ config.num_warps = value
36
+ elif name == "num_stages":
37
+ config.num_stages = value
38
+ else:
39
+ config.kwargs[name] = value
40
+
41
+
42
+ class CoordescTuner:
43
+ """
44
+ The coordinate descent tuner. Tune one field/coordinate at a time.
45
+
46
+ TODO will it be necessary to tune multiple fields simultaneously.
47
+
48
+
49
+ TODO: what if both increasing and decreasing a field can improve perf.
50
+ i.e., there are multiple local optima..
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ is_mm=False,
56
+ is_native_matmul=False,
57
+ is_mix_order_reduction=False,
58
+ name="unknown",
59
+ size_hints=None,
60
+ inductor_meta=None,
61
+ frozen_fields=None,
62
+ ):
63
+ self.is_mm = is_mm # we will tune num_stages for mm
64
+
65
+ # Native matmul codegen assumes ZBLOCK=1 always.
66
+ # This is because 3d tl.dot is slow and so we want to tile y and x only.
67
+ # tl.dot also does not support size smaller than 16; we put this restriction.
68
+ self.is_native_matmul = is_native_matmul
69
+ assert not (self.is_mm and self.is_native_matmul)
70
+ self.is_mix_order_reduction = is_mix_order_reduction
71
+ self.cached_benchmark_results = {}
72
+ self.name = name
73
+ self.size_hints = size_hints
74
+ self.inductor_meta = inductor_meta or {}
75
+ self.frozen_fields: OrderedSet[str] = (
76
+ OrderedSet(frozen_fields) if frozen_fields is not None else OrderedSet()
77
+ )
78
+
79
+ def get_config_max(self, prefix: str) -> int:
80
+ max_block = TRITON_MAX_BLOCK[prefix.upper()]
81
+ size_hint = self.size_hints.get(prefix) if self.size_hints is not None else None
82
+ return min(max_block, size_hint) if size_hint is not None else max_block
83
+
84
+ def get_warpsmax(self):
85
+ # Avoid querying device directly if device properties are populated in inductor_meta
86
+ warp_size = self.inductor_meta.get("warp_size")
87
+ max_threads_per_block = self.inductor_meta.get("max_threads_per_block")
88
+ if warp_size and max_threads_per_block:
89
+ return max_threads_per_block // warp_size
90
+ else:
91
+ return get_max_numwarps()
92
+
93
+ def cache_benchmark_result(self, config, timing):
94
+ self.cached_benchmark_results[triton_config_to_hashable(config)] = timing
95
+
96
+ def lookup_in_cache(self, config):
97
+ return self.cached_benchmark_results.get(triton_config_to_hashable(config))
98
+
99
+ def call_func(self, func, config):
100
+ found = self.lookup_in_cache(config)
101
+ if found is not None:
102
+ log.debug(" CACHED")
103
+ return found
104
+ timing = func(config)
105
+ self.cache_benchmark_result(config, timing)
106
+ return timing
107
+
108
+ @property
109
+ def tunable_fields(self):
110
+ out = [
111
+ "XBLOCK",
112
+ "YBLOCK",
113
+ "ZBLOCK",
114
+ # NOTE: we should not tune R0_BLOCK for persistent reduction.
115
+ # We rely on the fact that persistent reduction's triton.Config
116
+ # does not have the R0_BLOCK field to guarantee that.
117
+ "R0_BLOCK",
118
+ "R1_BLOCK",
119
+ # the following 3 are for mm
120
+ "BLOCK_M",
121
+ "BLOCK_N",
122
+ "BLOCK_K",
123
+ "num_warps",
124
+ ]
125
+ if self.is_mm:
126
+ out.append("num_stages")
127
+ if self.inductor_meta.get("is_hip") is True:
128
+ out.append("waves_per_eu")
129
+ if self.is_native_matmul:
130
+ out.append("num_stages")
131
+ out.remove("ZBLOCK") # ZBLOCK=1 always in native matmul
132
+
133
+ if self.is_mix_order_reduction:
134
+ # unlike TritonConfig.num_stages, this one is
135
+ # put in TritonConfig.kwargs["NUM_STAGES"] and is used to
136
+ # control the stage of pipelining of tl.range.
137
+ out.append("NUM_STAGES")
138
+
139
+ return [f for f in out if f not in self.frozen_fields]
140
+
141
+ def value_too_large(self, name: str, val: int) -> bool:
142
+ block_suffix = "BLOCK"
143
+ if name.endswith(block_suffix):
144
+ prefix = name.strip(block_suffix).lower()
145
+ return val > self.get_config_max(prefix)
146
+ if name == "num_warps":
147
+ return val > self.get_warpsmax()
148
+ if name == "waves_per_eu":
149
+ return val > 8
150
+
151
+ return False
152
+
153
+ def value_too_small(self, name: str, val: int) -> bool:
154
+ # In native matmul, block size should be >= 16 for tl.dot
155
+ if self.is_native_matmul:
156
+ if name in ["YBLOCK", "XBLOCK", "R0_BLOCK"]:
157
+ return val < 16
158
+
159
+ # Break if value becomes 0/neg
160
+ return val <= 0
161
+
162
+ def get_neighbour_values(self, name, orig_val, radius=None, include_self=False):
163
+ """
164
+ Get neighbour values in 'radius' steps. The original value is not
165
+ returned as it's own neighbour.
166
+ """
167
+ if radius is None:
168
+ radius = 1
169
+ if name == "NUM_STAGES":
170
+ # we see cases that
171
+ # NUM_STAGES=1 is better than NUM_STAGES=2
172
+ # while NUM_STAGES=1 is worse than NUM_STAGES=3
173
+ radius = max(radius, 2)
174
+
175
+ assert radius >= 1
176
+
177
+ def update(cur_val, inc=True):
178
+ if name in ["num_stages", "NUM_STAGES"]:
179
+ if inc:
180
+ return cur_val + 1
181
+ else:
182
+ return cur_val - 1
183
+ else:
184
+ if inc:
185
+ return cur_val * 2
186
+ else:
187
+ return cur_val // 2
188
+
189
+ out = []
190
+ # increment loop
191
+ cur_val = orig_val
192
+ for _ in range(radius):
193
+ cur_val = update(cur_val, True)
194
+ if self.value_too_large(name, cur_val):
195
+ break
196
+ out.append(cur_val)
197
+
198
+ # decrement loop
199
+ cur_val = orig_val
200
+ for _ in range(radius):
201
+ cur_val = update(cur_val, False)
202
+ if self.value_too_small(name, cur_val):
203
+ break
204
+ out.append(cur_val)
205
+
206
+ if include_self:
207
+ out.append(orig_val)
208
+ return out
209
+
210
+ @staticmethod
211
+ def has_improvement(baseline, test):
212
+ threshold = 0.001 # 0.1%
213
+ return test is not None and test < baseline * (1 - threshold)
214
+
215
+ def is_valid_config(self, config) -> bool:
216
+ if self.is_mix_order_reduction:
217
+ # Mix order reduction has an extra constraint that
218
+ # we should not tune XBLOCK beyond RSPLIT_SIZE
219
+ xblock = config.kwargs["XBLOCK"]
220
+ split_size = config.kwargs["RSPLIT_SIZE"]
221
+ return xblock <= split_size
222
+ return True
223
+
224
+ def check_all_tuning_directions(
225
+ self,
226
+ # pyrefly: ignore [missing-attribute]
227
+ func: Callable[["triton.Config"], float],
228
+ best_config,
229
+ best_timing,
230
+ ):
231
+ """
232
+ Check all directions. We only do this once the regular coordinate
233
+ descent tuning find no better choices any more.
234
+ We only have a few tunable fields, so this should be fine.
235
+ """
236
+ candidate_values_list = []
237
+ effective_fields = []
238
+ for field in self.tunable_fields:
239
+ old_value = get_field(best_config, field)
240
+ if old_value is None:
241
+ continue
242
+ radius = self.inductor_meta.get("coordinate_descent_search_radius", 1)
243
+ candidate_values = self.get_neighbour_values(
244
+ field,
245
+ old_value,
246
+ radius=radius,
247
+ include_self=True,
248
+ )
249
+ candidate_values_list.append(candidate_values)
250
+ effective_fields.append(field)
251
+
252
+ choices = itertools.product(*candidate_values_list)
253
+ improved = False
254
+ for choice in choices:
255
+ assert len(choice) == len(effective_fields)
256
+ candidate_config = copy.deepcopy(best_config)
257
+ for new_val, field in zip(choice, effective_fields):
258
+ set_field(candidate_config, field, new_val)
259
+ if not self.is_valid_config(candidate_config):
260
+ continue
261
+ cmp_res, candidate_timing = self.compare_config(
262
+ func, candidate_config, best_config, best_timing
263
+ )
264
+ if cmp_res:
265
+ improved = True
266
+ best_config = candidate_config
267
+ best_timing = candidate_timing
268
+
269
+ return improved, best_config, best_timing
270
+
271
+ def compare_config(self, func, candidate_config, best_config, best_timing):
272
+ """
273
+ Check if candidate_config is better than best_config.
274
+
275
+ Return a tuple of (compare_result, candidate_timing).
276
+ compare_result is true iff candidate_config is better.
277
+ """
278
+ log.debug("Try config %s", candidate_config)
279
+ try:
280
+ candidate_timing = self.call_func(func, candidate_config)
281
+ except Exception as e:
282
+ log.debug("Got exception %s", e) # noqa: G200
283
+ return False, float("inf")
284
+
285
+ if self.has_improvement(best_timing, candidate_timing):
286
+ log.debug(
287
+ "Tune from %s %f -> %s %f",
288
+ best_config,
289
+ best_timing,
290
+ candidate_config,
291
+ candidate_timing,
292
+ )
293
+
294
+ return True, candidate_timing
295
+ return False, candidate_timing
296
+
297
+ def autotune(
298
+ self,
299
+ # pyrefly: ignore [missing-attribute]
300
+ func: Callable[["triton.Config"], float],
301
+ # pyrefly: ignore [missing-attribute]
302
+ baseline_config: "triton.Config",
303
+ baseline_timing: float | None = None,
304
+ ) -> "triton.Config": # pyrefly: ignore # missing-attribute
305
+ if baseline_timing is None:
306
+ baseline_timing = self.call_func(func, baseline_config)
307
+
308
+ log.debug("= Do coordinate descent tuning for %s =", self.name)
309
+ log.debug(
310
+ "%s: Baseline Config %s, baseline timing %f",
311
+ self.name,
312
+ baseline_config,
313
+ baseline_timing,
314
+ )
315
+ improved = True
316
+ best_config = baseline_config
317
+ best_timing = baseline_timing
318
+ tunable_fields = self.tunable_fields
319
+
320
+ while improved:
321
+ improved = False
322
+
323
+ for name in tunable_fields:
324
+ cur_val = get_field(best_config, name)
325
+ # some kernel don't have R0_BLOCK/YBLOCK/ZBLOCK. So cur_val may be None
326
+ if cur_val is None:
327
+ continue
328
+
329
+ # It's possible that candidate_values is empty.
330
+ # E.g., if XBLOCK is 1 initially and size_hint for x is also 1.
331
+ # We would not try either larger or smaller XBLOCK in this case.
332
+ candidate_values = self.get_neighbour_values(name, cur_val)
333
+
334
+ for next_val in candidate_values:
335
+ candidate_config = copy.deepcopy(best_config)
336
+ set_field(candidate_config, name, next_val)
337
+
338
+ if not self.is_valid_config(candidate_config):
339
+ continue
340
+ cmp_res, candidate_timing = self.compare_config(
341
+ func, candidate_config, best_config, best_timing
342
+ )
343
+ if cmp_res:
344
+ improved = True
345
+ best_config, best_timing = candidate_config, candidate_timing
346
+
347
+ if not improved and self.inductor_meta.get(
348
+ "coordinate_descent_check_all_directions"
349
+ ):
350
+ old_best_timing = best_timing
351
+ improved, best_config, best_timing = self.check_all_tuning_directions(
352
+ func, best_config, best_timing
353
+ )
354
+
355
+ if improved:
356
+ msg = red_text(
357
+ "%s: Coordinate descend tuning found improvement of %.3fx by looking in all directions."
358
+ )
359
+ log.debug(
360
+ msg,
361
+ self.name,
362
+ old_best_timing / best_timing,
363
+ )
364
+
365
+ log.debug(
366
+ "%s: Improve from %s %f -> %s %f, %.3fx",
367
+ self.name,
368
+ baseline_config,
369
+ baseline_timing,
370
+ best_config,
371
+ best_timing,
372
+ baseline_timing / best_timing,
373
+ )
374
+
375
+ return best_config
376
+
377
+ @staticmethod
378
+ def autotune_single_field(fn, init_val, min_val=None, max_val=None):
379
+ """
380
+ fn is a function that takes the field value and returns the benchmarking result
381
+ init_val is the starting point of autotuning.
382
+
383
+ Should work well for parabola like curve. Here is a real example
384
+ for split-size of mix-order-reduction: https://github.com/pytorch/pytorch/pull/166461
385
+ """
386
+ cache = {}
387
+
388
+ def _bench(val):
389
+ if val not in cache:
390
+ cache[val] = fn(val)
391
+ # print(f"split size {val} -> {cache[val]:.3f} ms")
392
+ return cache[val]
393
+
394
+ if min_val is None:
395
+ min_val = 1
396
+ if max_val is None:
397
+ max_val = 2**30 # some arbitrary large value
398
+
399
+ best_val = init_val
400
+ improved = True
401
+ while improved:
402
+ improved = False
403
+ candlist = [best_val // 2, best_val * 2]
404
+ for cand in candlist:
405
+ cand = max(cand, min_val)
406
+ cand = min(cand, max_val)
407
+
408
+ if _bench(cand) < _bench(best_val):
409
+ best_val = cand
410
+ improved = True
411
+
412
+ return best_val
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/debug_utils.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import logging
3
+ import threading
4
+ import weakref
5
+
6
+ import torch
7
+ from torch.utils._ordered_set import OrderedSet
8
+
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ local = threading.local()
13
+ local.memory_tracker = None
14
+
15
+
16
+ class BufferMemoryTracker:
17
+ """
18
+ Tracks inductor runtime allocations and deallocations to compare against
19
+ expected behavior.
20
+ """
21
+
22
+ def __init__(self) -> None:
23
+ self.tensor_tracker: dict[str, torch.storage.UntypedStorage] = (
24
+ weakref.WeakValueDictionary() # type: ignore[assignment]
25
+ )
26
+ self.died_since_last_step: OrderedSet[str] = OrderedSet()
27
+ self.added_since_last_step: OrderedSet[str] = OrderedSet()
28
+ self.error = (
29
+ torch._inductor.config.test_configs.track_memory_lifecycle == "assert"
30
+ )
31
+
32
+ def set_tensor(self, name: str, tensor: torch.Tensor) -> None:
33
+ storage = tensor.untyped_storage()
34
+
35
+ self.added_since_last_step.add(name)
36
+ self.tensor_tracker[name] = storage
37
+
38
+ def on_tensor_death() -> None:
39
+ self.died_since_last_step.add(name)
40
+
41
+ weakref.finalize(storage, on_tensor_death)
42
+
43
+ def advance_step(self) -> None:
44
+ self.died_since_last_step.clear()
45
+ self.added_since_last_step.clear()
46
+
47
+ def log_or_raise(self, msg: str) -> None:
48
+ if self.error:
49
+ raise RuntimeError(msg)
50
+ else:
51
+ log.info(msg)
52
+
53
+ def check_step_delta(
54
+ self,
55
+ expected_allocated: list[str],
56
+ expected_freed: list[str],
57
+ is_final_step: bool,
58
+ ) -> None:
59
+ """Check only the delta changes since last step"""
60
+
61
+ # Check expected deaths - we dont currently distinguish between nodes which die in last step
62
+ # and are returned as outputs, so skip if final_step.
63
+ if not is_final_step:
64
+ missing_deaths = OrderedSet(expected_freed) - self.died_since_last_step
65
+ if missing_deaths:
66
+ self.log_or_raise(
67
+ f"Expected tensors to die but still alive: {missing_deaths}"
68
+ )
69
+
70
+ # Check for unexpected deaths
71
+ unexpected_deaths = self.died_since_last_step - OrderedSet(expected_freed)
72
+ if unexpected_deaths:
73
+ self.log_or_raise(f"Unexpected tensor deaths: {unexpected_deaths}")
74
+
75
+ # Check newly alive tensors - separate messages like deaths
76
+ actual_allocated = self.added_since_last_step
77
+ expected_allocated_set = OrderedSet(expected_allocated)
78
+
79
+ extra_alive = actual_allocated - expected_allocated_set
80
+ if extra_alive:
81
+ self.log_or_raise(f"Unexpected allocated tensors: {extra_alive}")
82
+
83
+ missing_alive = expected_allocated_set - actual_allocated
84
+ if missing_alive:
85
+ self.log_or_raise(
86
+ f"Expected allocated tensors but missing: {missing_alive}"
87
+ )
88
+
89
+ # Reset for next step
90
+ self.advance_step()
91
+
92
+ if is_final_step:
93
+ local.memory_tracker = None
94
+
95
+
96
+ def get_mem_tracker() -> BufferMemoryTracker:
97
+ if local.memory_tracker is None:
98
+ local.memory_tracker = BufferMemoryTracker()
99
+ return local.memory_tracker
100
+
101
+
102
+ def track_tensor(tensor: torch.Tensor, name: str) -> None:
103
+ get_mem_tracker().set_tensor(name, tensor)
104
+
105
+
106
+ def tracked_empty_strided(
107
+ size: list[int],
108
+ stride: list[int],
109
+ *,
110
+ dtype: torch.dtype,
111
+ device: torch.device,
112
+ name: str,
113
+ ) -> torch.Tensor:
114
+ o = torch.empty_strided(size, stride, dtype=dtype, device=device)
115
+ track_tensor(o, name)
116
+ return o
117
+
118
+
119
+ def check_memory_step(
120
+ allocated: list[str], freed: list[str], is_final_step: bool = False
121
+ ) -> None:
122
+ tracker = get_mem_tracker()
123
+ tracker.check_step_delta(allocated, freed, is_final_step)
124
+
125
+
126
+ @functools.lru_cache(None)
127
+ def register_check_mem_op() -> None:
128
+ lib = torch.library.Library("_inductor_debug", "FRAGMENT") # noqa: TOR901
129
+ lib.define(
130
+ "check_memory_step(str[] allocated, str[] freed, bool is_final_step) -> ()"
131
+ )
132
+ lib.impl("check_memory_step", check_memory_step, "BackendSelect")
133
+ from torch._higher_order_ops.effects import _EffectType, _register_effectful_op
134
+
135
+ _register_effectful_op(
136
+ torch.ops._inductor_debug.check_memory_step.default,
137
+ _EffectType.ORDERED,
138
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/halide_helpers.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ import math
3
+
4
+
5
+ try:
6
+ import halide as hl # type: ignore[import-untyped, import-not-found]
7
+ except ImportError:
8
+ hl = None
9
+
10
+ PHILOX_N_ROUNDS_DEFAULT = 10 # Default number of rounds for philox
11
+
12
+ if hl is not None:
13
+ PHILOX_KEY_A_U32 = hl.u32(0x9E3779B9)
14
+ PHILOX_KEY_B_U32 = hl.u32(0xBB67AE85)
15
+ PHILOX_ROUND_A_U32 = hl.u32(0xD2511F53)
16
+ PHILOX_ROUND_B_U32 = hl.u32(0xCD9E8D57)
17
+ else:
18
+ PHILOX_KEY_A_U32 = None
19
+ PHILOX_KEY_B_U32 = None
20
+ PHILOX_ROUND_A_U32 = None
21
+ PHILOX_ROUND_B_U32 = None
22
+
23
+
24
+ def _pair_uniform_to_normal(u1, u2):
25
+ """Box-Muller transform"""
26
+ u1 = hl.max(hl.f32(1.0e-7), u1)
27
+ th = hl.f32(math.tau) * u2
28
+ r = hl.sqrt(hl.f32(-2.0) * hl.log(u1))
29
+ return r * hl.cos(th), r * hl.sin(th)
30
+
31
+
32
+ def _uint_to_uniform_float(x):
33
+ """
34
+ Numerically stable function to convert a random uint into a random float uniformly sampled in [0, 1).
35
+ """
36
+
37
+ # TODO:
38
+ # conditions can be simplified
39
+ # scale is ((2**23 - 1) / 2**23) * 2**(N_BITS - 1)
40
+ # https://github.com/triton-lang/triton/blob/e4a0d93ff1a367c7d4eeebbcd7079ed267e6b06f/python/triton/language/random.py#L116-L132.
41
+ assert x.type() == hl.UInt(32) or x.type() == hl.Int(32)
42
+ x = hl.cast(hl.Int(32), x)
43
+ scale = hl.f64(4.6566127342e-10)
44
+ x = hl.select(x < 0, -x - 1, x)
45
+ return x * scale
46
+
47
+
48
+ def philox_impl(c0, c1, c2, c3, k0, k1, n_rounds):
49
+ def umulhi(a, b):
50
+ a = hl.cast(hl.UInt(64), a)
51
+ b = hl.cast(hl.UInt(64), b)
52
+ return hl.cast(hl.UInt(32), ((a * b) >> 32) & hl.u64(0xFFFFFFFF))
53
+
54
+ for _ in range(n_rounds):
55
+ _c0, _c2 = c0, c2
56
+
57
+ c0 = umulhi(PHILOX_ROUND_B_U32, _c2) ^ c1 ^ k0
58
+ c2 = umulhi(PHILOX_ROUND_A_U32, _c0) ^ c3 ^ k1
59
+ c1 = PHILOX_ROUND_B_U32 * _c2
60
+ c3 = PHILOX_ROUND_A_U32 * _c0
61
+ # raise key
62
+ k0 = k0 + PHILOX_KEY_A_U32
63
+ k1 = k1 + PHILOX_KEY_B_U32
64
+
65
+ return c0, c1, c2, c3
66
+
67
+
68
+ def halide_philox(seed, c0, c1, c2, c3, n_rounds):
69
+ seed = hl.cast(hl.UInt(64), seed)
70
+
71
+ assert c0.type().bits() == 32
72
+
73
+ seed_hi = hl.cast(hl.UInt(32), (seed >> 32) & hl.u64(0xFFFFFFFF))
74
+ seed_lo = hl.cast(hl.UInt(32), seed & hl.u64(0xFFFFFFFF))
75
+
76
+ return philox_impl(c0, c1, c2, c3, seed_lo, seed_hi, n_rounds)
77
+
78
+
79
+ def randint4x(seed, offset, n_rounds):
80
+ offset = hl.cast(hl.UInt(32), offset)
81
+ _0 = hl.u32(0)
82
+ return halide_philox(seed, offset, _0, _0, _0, n_rounds)
83
+
84
+
85
+ def rand4x(seed, offset, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
86
+ i1, i2, i3, i4 = randint4x(seed, offset, n_rounds)
87
+ u1 = _uint_to_uniform_float(i1)
88
+ u2 = _uint_to_uniform_float(i2)
89
+ u3 = _uint_to_uniform_float(i3)
90
+ u4 = _uint_to_uniform_float(i4)
91
+ return u1, u2, u3, u4
92
+
93
+
94
+ def randint(seed, offset, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
95
+ ret, _, _, _ = randint4x(seed, offset, n_rounds)
96
+ return ret
97
+
98
+
99
+ def rand(seed, offset, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
100
+ source = randint(seed, offset, n_rounds)
101
+ return _uint_to_uniform_float(source)
102
+
103
+
104
+ def rand_eager_kernel(seed, offset_blocks, tid, VEC, n_rounds=PHILOX_N_ROUNDS_DEFAULT):
105
+ inv = hl.cast(hl.Float(32), 1.0 / 4294967296.0) # 2^-32
106
+ half = hl.cast(hl.Float(32), 0.5) * inv
107
+
108
+ tid_u64 = hl.cast(hl.UInt(64), tid)
109
+ VEC_u64 = hl.cast(hl.UInt(64), VEC)
110
+ subseq = tid_u64 // VEC_u64
111
+ which4 = (tid_u64 % VEC_u64) // hl.cast(hl.UInt(64), 4)
112
+ lane = tid_u64 % hl.cast(hl.UInt(64), 4)
113
+
114
+ offblk = hl.cast(hl.UInt(64), offset_blocks) + which4
115
+
116
+ c0 = hl.cast(hl.UInt(32), offblk & hl.cast(hl.UInt(64), 0xFFFFFFFF))
117
+ c1 = hl.cast(
118
+ hl.UInt(32),
119
+ (offblk >> hl.cast(hl.UInt(64), 32)) & hl.cast(hl.UInt(64), 0xFFFFFFFF),
120
+ )
121
+ c2 = hl.cast(hl.UInt(32), subseq & hl.cast(hl.UInt(64), 0xFFFFFFFF))
122
+ c3 = hl.cast(
123
+ hl.UInt(32),
124
+ (subseq >> hl.cast(hl.UInt(64), 32)) & hl.cast(hl.UInt(64), 0xFFFFFFFF),
125
+ )
126
+
127
+ u0, u1, u2, u3 = halide_philox(seed, c0, c1, c2, c3, n_rounds)
128
+
129
+ v01 = hl.select(lane == hl.cast(hl.UInt(64), 0), u0, u1)
130
+ v23 = hl.select(lane == hl.cast(hl.UInt(64), 2), u2, u3)
131
+ rand_int = hl.select(
132
+ (lane == hl.cast(hl.UInt(64), 0)) | (lane == hl.cast(hl.UInt(64), 1)), v01, v23
133
+ )
134
+
135
+ return hl.cast(hl.Float(32), 1.0) - (hl.cast(hl.Float(32), rand_int) * inv + half)
136
+
137
+
138
+ def randn(seed, offset):
139
+ i1, i2, _, _ = randint4x(seed, offset, PHILOX_N_ROUNDS_DEFAULT)
140
+ u1 = _uint_to_uniform_float(i1)
141
+ u2 = _uint_to_uniform_float(i2)
142
+ n1, _ = _pair_uniform_to_normal(u1, u2)
143
+ return n1
144
+
145
+
146
+ def randint64(seed, offset, low, high):
147
+ r0, r1, _r2, _r3 = randint4x(seed, offset, PHILOX_N_ROUNDS_DEFAULT)
148
+ r0 = hl.cast(hl.UInt(64), r0)
149
+ r1 = hl.cast(hl.UInt(64), r1)
150
+
151
+ result = r0 | (r1 << 32)
152
+ size = high - low
153
+ result = result % hl.cast(hl.UInt(64), size)
154
+ result = hl.cast(hl.Int(64), result) + low
155
+ return result
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/hints.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ from __future__ import annotations
3
+
4
+ import collections
5
+ import functools
6
+ import typing
7
+ from enum import auto, Enum
8
+
9
+ import torch
10
+ from torch.utils._triton import has_triton_package
11
+
12
+
13
+ # The following maximums only apply to runtime autotuning, when using FixedTritonConfig one may see larger values
14
+ # NOTE: if these fail asserts submit a PR to increase them
15
+ TRITON_MAX_BLOCK = {
16
+ "X": 8192 if torch.version.hip else 4096,
17
+ "Y": 1024,
18
+ "Z": 1024,
19
+ "R0_": 4096 * 16, # * 16 is multi-kernel only
20
+ "R1_": 2048 * 16, # * 16 is multi-kernel only
21
+ }
22
+ TRITON_MAX_RSPLIT = 64
23
+
24
+
25
+ class ReductionHint(Enum):
26
+ INNER = 0
27
+ OUTER = 1
28
+ OUTER_TINY = 2
29
+ DEFAULT = 3
30
+
31
+
32
+ class TileHint(Enum):
33
+ SQUARE = 0
34
+ DEFAULT = 1
35
+
36
+
37
+ # Define `AttrsDescriptorWrapper` function with clear conditional handling
38
+ if has_triton_package():
39
+ import triton
40
+ import triton.backends.compiler
41
+ import triton.compiler.compiler
42
+
43
+ if hasattr(triton.backends.compiler, "AttrsDescriptor"):
44
+ # Triton 3.2.0 - the second implementation
45
+ from triton.backends.compiler import AttrsDescriptor
46
+
47
+ def AttrsDescriptorWrapper(
48
+ divisible_by_16=None,
49
+ equal_to_1=None,
50
+ pointer_range_32=None,
51
+ ):
52
+ # Prepare the arguments for AttrsDescriptor
53
+ kwargs = {
54
+ "tt.divisibility": divisible_by_16,
55
+ "tt.equal_to": equal_to_1,
56
+ }
57
+
58
+ # Instantiate AttrsDescriptor with the prepared arguments
59
+ res = AttrsDescriptor.from_dict(
60
+ {"arg_properties": kwargs, "cls": AttrsDescriptor.__name__}
61
+ )
62
+ assert res.property_values["tt.divisibility"] == 16
63
+ assert res.property_values["tt.equal_to"] == 1
64
+ return res
65
+
66
+ elif hasattr(triton.compiler.compiler, "AttrsDescriptor"):
67
+ # Triton 3.0.0 - the original implementation
68
+ from triton.compiler.compiler import AttrsDescriptor
69
+
70
+ def AttrsDescriptorWrapper(
71
+ divisible_by_16=None,
72
+ equal_to_1=None,
73
+ pointer_range_32=None,
74
+ ):
75
+ # Prepare the arguments for AttrsDescriptor
76
+ kwargs = {
77
+ "divisible_by_16": divisible_by_16,
78
+ "equal_to_1": equal_to_1,
79
+ }
80
+
81
+ # Instantiate AttrsDescriptor with the prepared arguments
82
+ return AttrsDescriptor(**kwargs)
83
+
84
+ else:
85
+ # Triton in 2025:
86
+ # note: there's also a range of triton commits not currently supported
87
+ # from ~Dec 9, 2024 to Jan 1 2025, in which AttrsDescriptors are still
88
+ # used, but the contents are different.
89
+
90
+ def AttrsDescriptorWrapper(
91
+ divisible_by_16=None,
92
+ equal_to_1=None,
93
+ pointer_range_32=None,
94
+ ):
95
+ # pyrefly: ignore [not-iterable]
96
+ # Build attr dict merging divisibility and pointer_range per arg index,
97
+ # since a single arg can carry both attributes.
98
+ result = {(x,): [["tt.divisibility", 16]] for x in (divisible_by_16 or ())}
99
+ for x in pointer_range_32 or ():
100
+ key = (x,)
101
+ if key in result:
102
+ result[key].append(["tt.pointer_range", 32])
103
+ else:
104
+ result[key] = [["tt.pointer_range", 32]]
105
+ return result
106
+
107
+ else:
108
+ # Define a namedtuple as a fallback when AttrsDescriptor is not available
109
+ AttrsDescriptorWrapper = collections.namedtuple( # type: ignore[no-redef, name-match]
110
+ # pyrefly: ignore [invalid-argument]
111
+ "AttrsDescriptor",
112
+ ["divisible_by_16", "equal_to_1", "pointer_range_32"],
113
+ defaults=[(), (), ()],
114
+ )
115
+
116
+
117
+ _NUM_THREADS_PER_WARP = 32
118
+
119
+
120
+ class HeuristicType(Enum):
121
+ PERSISTENT_REDUCTION = auto()
122
+ POINTWISE = auto()
123
+ REDUCTION = auto()
124
+ SPLIT_SCAN = auto()
125
+ TEMPLATE = auto()
126
+ USER_AUTOTUNE = auto()
127
+ FIXED = auto()
128
+
129
+
130
+ class AutotuneHint(Enum):
131
+ ONE_ELEMENT_PER_THREAD = 0
132
+
133
+ # Triton codegen tries to codegen set of AutotuneHints.
134
+ # Enum.__repr__ looks like "<AutotuneHint.ELEMENTS_PER_WARP_32: 0>""
135
+ # which isn't valid python.
136
+ # Enum.__str__ will just return "AutotuneHint.ELEMENTS_PER_WARP_32".
137
+ __repr__ = Enum.__str__
138
+
139
+
140
+ class DeviceProperties(typing.NamedTuple):
141
+ """Copy device properties into a data structure not requiring torch to be imported"""
142
+
143
+ type: str # type: ignore[assignment]
144
+ index: int # type: ignore[assignment]
145
+ multi_processor_count: int
146
+ cc: int
147
+ major: int | None = None
148
+ regs_per_multiprocessor: int | None = None
149
+ max_threads_per_multi_processor: int | None = None
150
+ max_threads_per_block: int | None = None
151
+ warp_size: int | None = None
152
+
153
+ @classmethod
154
+ @functools.cache
155
+ def create(cls, device) -> DeviceProperties:
156
+ import torch
157
+ from torch._dynamo.device_interface import get_interface_for_device
158
+
159
+ device_type = device.type
160
+
161
+ if torch.version.hip and device_type == "cuda":
162
+ device_type = "hip"
163
+
164
+ device_interface = get_interface_for_device(device)
165
+ props = device_interface.get_device_properties(device)
166
+ try:
167
+ multi_processor_count = props.multi_processor_count
168
+ except AttributeError:
169
+ if device_type == "xpu":
170
+ multi_processor_count = props.gpu_subslice_count
171
+ elif device_type == "mtia":
172
+ multi_processor_count = 64
173
+ else:
174
+ raise
175
+ return cls(
176
+ type=device_type,
177
+ index=device.index,
178
+ multi_processor_count=multi_processor_count,
179
+ cc=device_interface.get_compute_capability(device),
180
+ major=getattr(props, "major", None),
181
+ regs_per_multiprocessor=getattr(props, "regs_per_multiprocessor", None),
182
+ max_threads_per_multi_processor=getattr(
183
+ props, "max_threads_per_multi_processor", None
184
+ ),
185
+ max_threads_per_block=getattr(props, "max_threads_per_block", 1024),
186
+ warp_size=getattr(props, "warp_size", 32 if device_type != "cpu" else None),
187
+ )
188
+
189
+
190
+ class HalideInputSpec(typing.NamedTuple):
191
+ ctype: str
192
+ name: str
193
+ shape: list[str] | None = None
194
+ stride: list[str] | None = None
195
+ offset: str | None = None
196
+ alias_of: str | None = None
197
+
198
+ def bindings_type(self) -> str:
199
+ if self.ctype in ("at::Half*", "at::BFloat16*"):
200
+ return "uint16_t*" # half not defined
201
+ return self.ctype
202
+
203
+ def halide_type(self) -> str:
204
+ if self.ctype == "at::Half*":
205
+ return "halide_type_t(halide_type_float, 16)" # half not defined
206
+ if self.ctype == "at::BFloat16*":
207
+ return "halide_type_t(halide_type_bfloat, 16)" # half not defined
208
+ return f"halide_type_of<{self.ctype.replace('*', '')}>()"
209
+
210
+ def is_scalar(self) -> bool:
211
+ return self.shape is None
212
+
213
+ def is_buffer(self) -> bool:
214
+ return self.shape is not None
215
+
216
+
217
+ class HalideMeta(typing.NamedTuple):
218
+ argtypes: list[HalideInputSpec]
219
+ target: str
220
+ scheduler: str | None = None
221
+ scheduler_flags: dict[str, int | str] | None = None
222
+ cuda_device: int | None = None
223
+
224
+ def args(self) -> list[str]:
225
+ """Command line args to pass to halide generator"""
226
+ args = [f"target={self.target}"]
227
+ if self.scheduler:
228
+ args.append(f"autoscheduler={self.scheduler}")
229
+ if self.scheduler_flags:
230
+ assert self.scheduler
231
+ for k, v in self.scheduler_flags.items():
232
+ args.append(f"autoscheduler.{k}={v}")
233
+ return args
234
+
235
+ def is_cuda(self) -> bool:
236
+ return self.cuda_device is not None
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/proton_utils.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utilities for post-processing Triton Proton profiling trace files.
3
+
4
+ This module provides functions to transform proton-generated Chrome trace files
5
+ for better visualization in Perfetto/Chrome tracing tools.
6
+ """
7
+
8
+ import gzip
9
+ import json
10
+ import os
11
+ import re
12
+ from typing import Any
13
+
14
+ from torch.utils._ordered_set import OrderedSet
15
+
16
+
17
+ def _write_trace(path: str, data: dict[str, Any]) -> None:
18
+ """Write trace data to a gzipped JSON file."""
19
+ with gzip.open(path, "wt", encoding="utf-8") as f:
20
+ json.dump(data, f)
21
+
22
+
23
+ def _read_trace(path: str) -> dict[str, Any]:
24
+ """Read trace data from a JSON file (gzipped or not)."""
25
+ if path.endswith(".gz"):
26
+ with gzip.open(path, "rt", encoding="utf-8") as f:
27
+ return json.load(f)
28
+ else:
29
+ with open(path) as f:
30
+ return json.load(f)
31
+
32
+
33
+ def _get_base_name(trace_path: str) -> str:
34
+ """Get the base name of a trace file, stripping known extensions."""
35
+ base_name = os.path.basename(trace_path)
36
+ for ext in [".trace.json.gz", ".chrome_trace", ".json.gz", ".json"]:
37
+ if base_name.endswith(ext):
38
+ return base_name[: -len(ext)]
39
+ return os.path.splitext(base_name)[0]
40
+
41
+
42
+ def _group_events_by_sm(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
43
+ """
44
+ Group events so all CTAs on the same SM share one track.
45
+
46
+ Transforms:
47
+ - pid: "Core0 CTA6" -> "Core0"
48
+ - tid: "warp0" -> "CTA6 warp0"
49
+ """
50
+ core_cta_pattern = re.compile(r"^(.*?)\s*(Core\d+)\s+(CTA\d+)$")
51
+
52
+ for event in events:
53
+ pid = event.get("pid", "")
54
+ tid = event.get("tid", "")
55
+
56
+ match = core_cta_pattern.match(str(pid))
57
+ if match:
58
+ prefix = match.group(1).strip()
59
+ core = match.group(2)
60
+ cta = match.group(3)
61
+ event["pid"] = f"{prefix} {core}" if prefix else core
62
+ event["tid"] = f"{cta} {tid}" if tid else cta
63
+
64
+ return events
65
+
66
+
67
+ def _group_events_per_cta_occupancy(
68
+ events: list[dict[str, Any]],
69
+ ) -> list[dict[str, Any]]:
70
+ """
71
+ Process warp events into CTA-level events with slot assignment.
72
+
73
+ Each CTA gets: min warp start, max warp end.
74
+ CTAs are assigned to slots per SM such that non-overlapping CTAs share slots.
75
+ """
76
+ core_cta_pattern = re.compile(r"^(.*?)\s*(Core\d+)\s+(CTA\d+)$")
77
+
78
+ # Group events by (prefix, SM, CTA)
79
+ cta_events: dict[tuple[str, str, str], list[dict[str, Any]]] = {}
80
+
81
+ for event in events:
82
+ pid = event.get("pid", "")
83
+ match = core_cta_pattern.match(str(pid))
84
+ if match:
85
+ prefix = match.group(1).strip()
86
+ core = match.group(2)
87
+ cta = match.group(3)
88
+ key = (prefix, core, cta)
89
+ if key not in cta_events:
90
+ cta_events[key] = []
91
+ cta_events[key].append(event)
92
+
93
+ # For each CTA, compute min start and max end
94
+ cta_intervals: dict[tuple[str, str, str], tuple[float, float]] = {}
95
+ for key, evts in cta_events.items():
96
+ min_start = float("inf")
97
+ max_end = float("-inf")
98
+ for evt in evts:
99
+ ts = evt.get("ts", 0)
100
+ dur = evt.get("dur", 0)
101
+ min_start = min(min_start, ts)
102
+ max_end = max(max_end, ts + dur)
103
+ if min_start != float("inf"):
104
+ cta_intervals[key] = (min_start, max_end)
105
+
106
+ # Group CTAs by (prefix, SM) and assign to slots
107
+ sm_ctas: dict[tuple[str, str], list[tuple[str, float, float]]] = {}
108
+ for (prefix, core, cta), (start, end) in cta_intervals.items():
109
+ sm_key = (prefix, core)
110
+ if sm_key not in sm_ctas:
111
+ sm_ctas[sm_key] = []
112
+ sm_ctas[sm_key].append((cta, start, end))
113
+
114
+ # Assign CTAs to slots using interval scheduling (greedy)
115
+ cta_slot_assignments: dict[tuple[str, str, str], int] = {}
116
+ for sm_key, ctas in sm_ctas.items():
117
+ prefix, core = sm_key
118
+ sorted_ctas = sorted(ctas, key=lambda x: x[1])
119
+ slots: list[float] = []
120
+ for cta, start, end in sorted_ctas:
121
+ assigned_slot = None
122
+ for i, slot_end in enumerate(slots):
123
+ if start >= slot_end:
124
+ assigned_slot = i
125
+ slots[i] = end
126
+ break
127
+ if assigned_slot is None:
128
+ assigned_slot = len(slots)
129
+ slots.append(end)
130
+ cta_slot_assignments[(prefix, core, cta)] = assigned_slot
131
+
132
+ # Build numeric ID mappings for pids and tids
133
+ # Chrome trace format requires numeric pid/tid values
134
+ pid_names: dict[str, int] = {}
135
+ tid_names: dict[tuple[int, str], int] = {} # (pid, tid_name) -> tid_num
136
+
137
+ for key in cta_intervals:
138
+ prefix, core, cta = key
139
+ pid_name = f"{prefix} {core}" if prefix else core
140
+ if pid_name not in pid_names:
141
+ pid_names[pid_name] = len(pid_names)
142
+
143
+ new_events: list[dict[str, Any]] = []
144
+
145
+ # Add process name metadata events
146
+ for pid_name, pid_num in pid_names.items():
147
+ new_events.append(
148
+ {
149
+ "name": "process_name",
150
+ "ph": "M",
151
+ "pid": pid_num,
152
+ "args": {"name": pid_name},
153
+ }
154
+ )
155
+
156
+ # Create CTA events with numeric IDs and collect tid mappings
157
+ for key, (start, end) in cta_intervals.items():
158
+ prefix, core, cta = key
159
+ slot = cta_slot_assignments[key]
160
+ pid_name = f"{prefix} {core}" if prefix else core
161
+ tid_name = f"slot{slot}"
162
+ pid_num = pid_names[pid_name]
163
+
164
+ if (pid_num, tid_name) not in tid_names:
165
+ tid_num = len(tid_names)
166
+ tid_names[(pid_num, tid_name)] = tid_num
167
+ # Add thread name metadata event
168
+ new_events.append(
169
+ {
170
+ "name": "thread_name",
171
+ "ph": "M",
172
+ "pid": pid_num,
173
+ "tid": tid_num,
174
+ "args": {"name": tid_name},
175
+ }
176
+ )
177
+ else:
178
+ tid_num = tid_names[(pid_num, tid_name)]
179
+
180
+ new_events.append(
181
+ {
182
+ "name": cta,
183
+ "cat": "cta",
184
+ "ph": "X",
185
+ "ts": start,
186
+ "dur": end - start,
187
+ "pid": pid_num,
188
+ "tid": tid_num,
189
+ }
190
+ )
191
+
192
+ return new_events
193
+
194
+
195
+ def _apply_grouping(
196
+ events: list[dict[str, Any]], group_by_sm: bool, per_cta_occupancy: bool
197
+ ) -> list[dict[str, Any]]:
198
+ """Apply grouping transformation to events."""
199
+ if per_cta_occupancy:
200
+ return _group_events_per_cta_occupancy(events)
201
+ elif group_by_sm:
202
+ return _group_events_by_sm(events)
203
+ return events
204
+
205
+
206
+ def _split_events_by_invocation(
207
+ events: list[dict[str, Any]],
208
+ gap_threshold_ns: float = 1000.0,
209
+ ) -> list[list[dict[str, Any]]]:
210
+ """Split events into separate invocations based on time gaps."""
211
+ if not events:
212
+ return []
213
+
214
+ events_sorted = sorted(events, key=lambda e: e.get("ts", 0))
215
+ invocations: list[list[dict[str, Any]]] = [[]]
216
+ prev_end = events_sorted[0].get("ts", 0)
217
+
218
+ for event in events_sorted:
219
+ ts = event.get("ts", 0)
220
+ dur = event.get("dur", 0)
221
+ if ts - prev_end > gap_threshold_ns and invocations[-1]:
222
+ invocations.append([])
223
+ invocations[-1].append(event)
224
+ prev_end = max(prev_end, ts + dur)
225
+
226
+ return [inv for inv in invocations if inv]
227
+
228
+
229
+ def _normalize_timestamps(
230
+ events: list[dict[str, Any]], scale_factor: float = 1.0
231
+ ) -> list[dict[str, Any]]:
232
+ """Normalize timestamps to start at 0 and optionally scale."""
233
+ if not events:
234
+ return events
235
+
236
+ # Only consider duration events (ph=X) for min timestamp calculation
237
+ duration_events = [e for e in events if e.get("ph") == "X"]
238
+ if not duration_events:
239
+ return events
240
+
241
+ min_ts = min(e.get("ts", 0) for e in duration_events)
242
+ for event in events:
243
+ if event.get("ph") == "X":
244
+ event["ts"] = (event.get("ts", 0) - min_ts) * scale_factor
245
+ if "dur" in event:
246
+ event["dur"] = event["dur"] * scale_factor
247
+
248
+ return events
249
+
250
+
251
+ def process_proton_trace(
252
+ trace_path: str,
253
+ output_dir: str | None = None,
254
+ group_by_sm: bool = True,
255
+ split_invocations: bool = True,
256
+ scale_factor: float = 1.0,
257
+ gap_threshold_ns: float = 1000.0,
258
+ per_cta_occupancy: bool = True,
259
+ ) -> list[str]:
260
+ """
261
+ Process a proton trace file with various transformations.
262
+
263
+ Always produces 1 + N files: one complete trace and N per-invocation traces.
264
+ Grouping options (group_by_sm, per_cta_occupancy) apply uniformly to all outputs.
265
+
266
+ Args:
267
+ trace_path: Path to the input Chrome trace file
268
+ output_dir: Directory to write output files. If None, uses same directory as input.
269
+ group_by_sm: If True, group CTAs by SM into single tracks.
270
+ split_invocations: If True, also produce per-invocation trace files.
271
+ scale_factor: Factor to scale durations by (helps visibility in Perfetto).
272
+ gap_threshold_ns: Time gap (in nanoseconds) that indicates a new invocation.
273
+ per_cta_occupancy: If True, process warp tracks into CTA tracks and assign
274
+ CTAs to slots per SM such that CTAs do not overlap.
275
+
276
+ Returns:
277
+ List of paths to the output files.
278
+ """
279
+ if output_dir is None:
280
+ output_dir = os.path.dirname(trace_path) or "."
281
+
282
+ os.makedirs(output_dir, exist_ok=True)
283
+ base_name = _get_base_name(trace_path)
284
+
285
+ data = _read_trace(trace_path)
286
+ events = data.get("traceEvents", [])
287
+
288
+ # Split into invocations first (before grouping, to avoid merging across invocations)
289
+ invocations = _split_events_by_invocation(events, gap_threshold_ns)
290
+
291
+ # Apply grouping transformation to each invocation
292
+ invocations = [
293
+ _apply_grouping(inv_events, group_by_sm, per_cta_occupancy)
294
+ for inv_events in invocations
295
+ ]
296
+
297
+ output_files = []
298
+
299
+ # Write complete trace (dedupe metadata events)
300
+ complete_events = []
301
+ seen_metadata: OrderedSet[tuple[str | None, int | None, int | None]] = OrderedSet()
302
+ for inv_events in invocations:
303
+ for event in inv_events:
304
+ if event.get("ph") == "M":
305
+ # Dedupe metadata events by (name, pid, tid)
306
+ key = (event.get("name"), event.get("pid"), event.get("tid"))
307
+ if key in seen_metadata:
308
+ continue
309
+ seen_metadata.add(key)
310
+ complete_events.append(event)
311
+ complete_path = os.path.join(output_dir, f"{base_name}.trace.json.gz")
312
+ _write_trace(complete_path, {"traceEvents": complete_events})
313
+ output_files.append(complete_path)
314
+
315
+ # Write per-invocation traces
316
+ if split_invocations:
317
+ for i, inv_events in enumerate(invocations):
318
+ inv_events = _normalize_timestamps(list(inv_events), scale_factor)
319
+ inv_path = os.path.join(
320
+ output_dir, f"{base_name}_invocation_{i}.trace.json.gz"
321
+ )
322
+ _write_trace(inv_path, {"traceEvents": inv_events})
323
+ output_files.append(inv_path)
324
+
325
+ return output_files
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/runtime_utils.py ADDED
@@ -0,0 +1,829 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import math
5
+ import operator
6
+ from typing import Any, TYPE_CHECKING
7
+
8
+ import sympy
9
+
10
+ import torch
11
+
12
+ # NOTE: other files rely on the imports below
13
+ from torch._dynamo import callback as compilation_callback # noqa: F401
14
+ from torch._inductor.runtime.cache_dir_utils import ( # noqa: F401
15
+ cache_dir,
16
+ default_cache_dir,
17
+ triton_cache_dir,
18
+ )
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Hashable
23
+
24
+ from .triton_compat import Config
25
+
26
+
27
+ def conditional_product(*args: int) -> int:
28
+ return functools.reduce(operator.mul, [x for x in args if x])
29
+
30
+
31
+ def ceildiv(number: int, denom: int) -> int:
32
+ return -(number // -denom)
33
+
34
+
35
+ def is_power_of_2(n: int) -> bool:
36
+ """Returns whether n = 2 ** m for some integer m."""
37
+ return n > 0 and n & n - 1 == 0
38
+
39
+
40
+ def next_power_of_2(n: int) -> int:
41
+ """Return the smallest power of 2 greater than or equal to n"""
42
+ if isinstance(n, sympy.Integer):
43
+ n = int(n)
44
+ if n <= 0:
45
+ return 1
46
+ return 1 << (n - 1).bit_length()
47
+
48
+
49
+ def last_power_of_2(n: int) -> int:
50
+ """Return the largest power of 2 less than or equal to n"""
51
+ next_pow2 = next_power_of_2(n)
52
+ return next_pow2 // 2 if next_pow2 > n else next_pow2
53
+
54
+
55
+ def get_num_bytes(*args: torch.Tensor, num_in_out_args: int = 0) -> int:
56
+ """
57
+ Return the total number of bytes the arguments of tensor type takes.
58
+
59
+ For in/out args, tensor sizes are counted twice: once for reading and
60
+ once for writing.
61
+
62
+ The first num_in_out_args arguments are in out tensors.
63
+ """
64
+ return sum(
65
+ arg.numel() * arg.element_size() * (1 + int(i < num_in_out_args))
66
+ for i, arg in enumerate(args)
67
+ if isinstance(arg, torch.Tensor)
68
+ )
69
+
70
+
71
+ def triton_config_to_hashable(cfg: Config) -> Hashable:
72
+ """
73
+ Convert triton config to a tuple that can uniquely identify it. We can use
74
+ the return value as a dictionary key.
75
+ """
76
+ # pyrefly: ignore [missing-attribute]
77
+ items = sorted(cfg.kwargs.items())
78
+ # pyrefly: ignore [missing-attribute]
79
+ items.append(("num_warps", cfg.num_warps))
80
+ # pyrefly: ignore [missing-attribute]
81
+ items.append(("num_stages", cfg.num_stages))
82
+ return tuple(items)
83
+
84
+
85
+ def validate_triton_config(cfg: Config) -> None:
86
+ # [Note: Triton pre_hook in inductor]
87
+ # pre-hook is a lambda function, which we don't attempt to serialize.
88
+ # right now, if a pre-hook is attached to the config, it will not be saved;
89
+ # and then it won't be used when the config is loaded from cache.
90
+ # So we assert - if we do get a pre_hook, it might get ignored after caching.
91
+ assert getattr(cfg, "pre_hook", None) is None, (
92
+ "triton configs with pre_hooks not supported"
93
+ )
94
+
95
+
96
+ def create_bandwidth_info_str(
97
+ ms: float,
98
+ num_gb: float,
99
+ gb_per_s: float,
100
+ prefix: str = "",
101
+ suffix: str = "",
102
+ color: bool = True,
103
+ ) -> str:
104
+ info_str = f"{prefix}{ms:.3f}ms \t{num_gb:.3f} GB \t {gb_per_s:7.2f}GB/s{suffix}"
105
+ slow = ms > 0.012 and gb_per_s < 650
106
+ return red_text(info_str) if color and slow else info_str
107
+
108
+
109
+ def get_max_y_grid() -> int:
110
+ return 65535
111
+
112
+
113
+ try:
114
+ import colorama
115
+
116
+ HAS_COLORAMA = True
117
+ except ModuleNotFoundError:
118
+ HAS_COLORAMA = False
119
+ colorama = None # type: ignore[assignment]
120
+
121
+
122
+ if HAS_COLORAMA:
123
+
124
+ def _color_text(msg: str, color: str) -> str:
125
+ # pyrefly: ignore [missing-attribute]
126
+ return getattr(colorama.Fore, color.upper()) + msg + colorama.Fore.RESET
127
+
128
+ else:
129
+
130
+ def _color_text(msg: str, color: str) -> str:
131
+ return msg
132
+
133
+
134
+ def green_text(msg: str) -> str:
135
+ return _color_text(msg, "green")
136
+
137
+
138
+ def yellow_text(msg: str) -> str:
139
+ return _color_text(msg, "yellow")
140
+
141
+
142
+ def red_text(msg: str) -> str:
143
+ return _color_text(msg, "red")
144
+
145
+
146
+ def blue_text(msg: str) -> str:
147
+ return _color_text(msg, "blue")
148
+
149
+
150
+ def get_first_attr(obj: Any, *attrs: str) -> Any:
151
+ """
152
+ Return the first available attribute or throw an exception if none is present.
153
+ """
154
+ for attr in attrs:
155
+ if hasattr(obj, attr):
156
+ return getattr(obj, attr)
157
+
158
+ raise AssertionError(f"{obj} does not has any of the attributes: {attrs}")
159
+
160
+
161
+ dynamo_timed = torch._dynamo.utils.dynamo_timed # type: ignore[has-type]
162
+
163
+
164
+ def triton_hash_to_path_key(key: str) -> str:
165
+ # In early versions of Triton, the hash is directly used in the path name.
166
+ # Later, the hash is converted to base64 before being used in the path name.
167
+ # Later, the base64 conversion was replaced to the base32
168
+ #
169
+ # This code tries to import _base64 and falls back to _base32 if _base64 is unavailable.
170
+ #
171
+ # To handle this, try to import the to-base64-conversion function.
172
+ # If it exists, use it; otherwise, try using _base32; if both are unavailable, use the hash directly.
173
+ try:
174
+ from triton.runtime.cache import _base64
175
+
176
+ return _base64(key)
177
+ except Exception:
178
+ try:
179
+ from triton.runtime.cache import _base32
180
+
181
+ return _base32(key)
182
+ except Exception:
183
+ return key
184
+
185
+
186
+ def compile_mps_shader(source: str) -> Any:
187
+ """
188
+ Compiles shader source but raise more actionable error message when needed
189
+ """
190
+ try:
191
+ return torch.mps.compile_shader(source)
192
+ except SyntaxError as err:
193
+ raise SyntaxError(f"failed to compile {source} with {err.msg}") from err
194
+
195
+
196
+ def compile_mps_shaders(
197
+ kernels: list[tuple[str, str, list[str]]],
198
+ ) -> dict[str, Any]:
199
+ """Compile a batch of Metal kernels into one library.
200
+
201
+ Args:
202
+ kernels: list of (kernel_name, metal_source, headers) tuples.
203
+ headers are bare names resolved as <c10/metal/{name}.h>.
204
+
205
+ Returns:
206
+ dict mapping each kernel_name to its compiled function handle.
207
+ """
208
+ from torch.utils._ordered_set import OrderedSet
209
+
210
+ all_headers = sorted(OrderedSet(h for _, _, hs in kernels for h in hs))
211
+ header_src = "\n".join(f"#include <c10/metal/{h}.h>" for h in all_headers)
212
+ body_src = "\n".join(src for _, src, _ in kernels)
213
+ lib = compile_mps_shader(header_src + "\n" + body_src)
214
+ return {name: getattr(lib, name) for name, _, _ in kernels}
215
+
216
+
217
+ def torch_dtype_to_jax_runtime(dtype: torch.dtype) -> Any:
218
+ """
219
+ Map PyTorch dtype to actual JAX dtype object at runtime.
220
+
221
+ This helper is used in generated Pallas kernels at runtime to convert
222
+ PyTorch dtypes to JAX dtype objects (not string representations).
223
+
224
+ Args:
225
+ dtype: PyTorch dtype to convert
226
+
227
+ Returns:
228
+ JAX dtype object (e.g., jnp.float32 object itself)
229
+ """
230
+ import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
231
+
232
+ dtype_map = {
233
+ torch.float32: jnp.float32,
234
+ torch.float64: jnp.float64,
235
+ torch.float16: jnp.float16,
236
+ torch.bfloat16: jnp.bfloat16,
237
+ torch.int32: jnp.int32,
238
+ torch.int64: jnp.int64,
239
+ torch.int16: jnp.int16,
240
+ torch.int8: jnp.int8,
241
+ torch.uint8: jnp.uint8,
242
+ torch.bool: jnp.bool_,
243
+ torch.complex64: jnp.complex64,
244
+ torch.complex128: jnp.complex128,
245
+ }
246
+ if dtype not in dtype_map:
247
+ raise ValueError(f"Unsupported dtype for JAX conversion: {dtype}")
248
+ return dtype_map[dtype]
249
+
250
+
251
+ def torch_dtype_to_jax(dtype: torch.dtype) -> str:
252
+ """
253
+ Map PyTorch dtype to JAX dtype expression string.
254
+
255
+ This helper is used at compile time in codegen to generate
256
+ JAX dtype expressions for Pallas kernels.
257
+
258
+ Args:
259
+ dtype: PyTorch dtype to convert
260
+
261
+ Returns:
262
+ JAX dtype expression as string (e.g., "jnp.float32")
263
+ """
264
+ jax_dtype = torch_dtype_to_jax_runtime(dtype)
265
+ dtype_name = jax_dtype.__name__
266
+ if dtype_name == "bool":
267
+ dtype_name = "bool_"
268
+ return f"jnp.{dtype_name}"
269
+
270
+
271
+ def pallas_gpu_pad_inputs(inputs: list[Any], alignment: int = 128) -> list[Any]:
272
+ """Flatten and pad each input JAX array to a multiple of alignment."""
273
+ import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
274
+
275
+ padded = []
276
+ for inp in inputs:
277
+ flat = inp.flatten()
278
+ orig_size = flat.size
279
+ aligned_size = ((orig_size + alignment - 1) // alignment) * alignment
280
+ if orig_size != aligned_size:
281
+ padded.append(jnp.pad(flat, (0, aligned_size - orig_size)))
282
+ else:
283
+ padded.append(flat)
284
+ return padded
285
+
286
+
287
+ def pallas_gpu_align_output_specs(
288
+ out_shapes: tuple[Any, ...],
289
+ out_dtypes: tuple[Any, ...],
290
+ alignment: int = 128,
291
+ ) -> tuple[tuple[Any, ...], list[bool]]:
292
+ """Build aligned output ShapeDtypeStruct specs for GPU kernels.
293
+
294
+ Returns (aligned_specs, is_scalar_output) where is_scalar_output[i] is True
295
+ when the i-th output is scalar and should not be padded/unpadded.
296
+ """
297
+ import jax # pyrefly: ignore [import-error, missing-import]
298
+
299
+ aligned_specs = []
300
+ is_scalar = []
301
+ for shape, dtype in zip(out_shapes, out_dtypes):
302
+ numel = math.prod(shape)
303
+ if numel <= 1:
304
+ aligned_specs.append(jax.ShapeDtypeStruct(shape, dtype))
305
+ is_scalar.append(True)
306
+ else:
307
+ aligned_numel = ((numel + alignment - 1) // alignment) * alignment
308
+ aligned_specs.append(jax.ShapeDtypeStruct((aligned_numel,), dtype))
309
+ is_scalar.append(False)
310
+ return tuple(aligned_specs), is_scalar
311
+
312
+
313
+ def pallas_gpu_unpad_results(
314
+ results: Any,
315
+ orig_shapes: tuple[Any, ...],
316
+ is_scalar_output: list[bool] | None = None,
317
+ ) -> Any:
318
+ """Remove padding from GPU kernel results and reshape to original shapes.
319
+
320
+ If is_scalar_output is None, all outputs are treated as non-scalar.
321
+ """
322
+ if not isinstance(results, tuple):
323
+ results = (results,)
324
+ unpadded = []
325
+ for i, (res, shape) in enumerate(zip(results, orig_shapes)):
326
+ if is_scalar_output is not None and is_scalar_output[i]:
327
+ unpadded.append(res)
328
+ else:
329
+ orig_numel = math.prod(shape)
330
+ unpadded.append(res[:orig_numel].reshape(shape))
331
+ return unpadded[0] if len(unpadded) == 1 else tuple(unpadded)
332
+
333
+
334
+ # ---------------------------------------------------------------------------
335
+ # Pallas CPU / TPU tiling helpers
336
+ # ---------------------------------------------------------------------------
337
+ # TPU alignment: last dim must be full or a multiple of 128,
338
+ # second-to-last dim must be full or a multiple of 8.
339
+ _TPU_ALIGN_LAST = 128
340
+ _TPU_ALIGN_SECOND_LAST = 8
341
+
342
+
343
+ def _pallas_tile_size(
344
+ dim: int, alignment: int, max_tile: int = 1024, is_tpu: bool = False
345
+ ) -> int:
346
+ """Pick the largest aligned tile size <= max_tile for *dim*.
347
+
348
+ If *dim* is already <= alignment the full dimension is used (no tiling
349
+ on this axis).
350
+ """
351
+ if dim == 0:
352
+ # Tile size >= 1 avoids division by zero in JAX's _pad_to_block_dimension
353
+ return alignment if is_tpu else 1
354
+
355
+ if is_tpu:
356
+ # On TPU, Mosaic requires block dimensions to perfectly align to hardware
357
+ # registers (128 for inner, 8 for outer). We MUST pad the block spec up to
358
+ # the next alignment boundary (Mosaic handles the OOB masking).
359
+ if dim <= alignment:
360
+ return alignment
361
+ t = min(max_tile, dim)
362
+ # Use ceiling division to ensure the tile covers the dimension or aligns upward
363
+ t = ((t + alignment - 1) // alignment) * alignment
364
+ return t
365
+
366
+ if dim <= alignment:
367
+ return dim
368
+ t = min(max_tile, dim)
369
+ t = (t // alignment) * alignment
370
+ return max(alignment, t)
371
+
372
+
373
+ def pallas_permute(x, perm):
374
+ """Permute array x according to perm, working around Mosaic TPU bugs.
375
+
376
+ Mosaic's jnp.permute_dims produces corrupted output for certain 3D+
377
+ permutations at large sizes. Decomposes into: loop over smallest output
378
+ dim via unrolled Python loop + stack, with recursive sub-permutation.
379
+ At the 2D base case, uses jnp.permute_dims (which works on Mosaic).
380
+
381
+ Callers should ensure tiles are small enough via tiling so that the
382
+ Mosaic bug doesn't trigger (threshold ~ 256K elements).
383
+ """
384
+ import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
385
+
386
+ ndim = len(perm)
387
+ if ndim <= 2:
388
+ return jnp.permute_dims(x, perm)
389
+ if perm == tuple(range(ndim)):
390
+ return x # identity
391
+
392
+ shape = x.shape
393
+ out_shape = tuple(shape[p] for p in perm)
394
+
395
+ # Choose the output dimension with the smallest size to loop over.
396
+ loop_out_dim = min(range(ndim), key=lambda d: out_shape[d])
397
+ loop_size = out_shape[loop_out_dim]
398
+ loop_in_dim = perm[loop_out_dim]
399
+
400
+ # Build the sub-permutation for the remaining (ndim-1) dimensions.
401
+ remaining_in_dims = [d for d in range(ndim) if d != loop_in_dim]
402
+ remaining_out_perm_raw = [perm[d] for d in range(ndim) if d != loop_out_dim]
403
+ dim_map = {old: new for new, old in enumerate(remaining_in_dims)}
404
+ sub_perm = tuple(dim_map[d] for d in remaining_out_perm_raw)
405
+
406
+ # Unrolled loop: extract slices with static indices, apply sub-perm,
407
+ # then stack along the loop output dimension.
408
+ slices = []
409
+ # pyrefly: ignore [bad-argument-type]
410
+ for i in range(loop_size):
411
+ idx: list[Any] = [slice(None)] * ndim
412
+ idx[loop_in_dim] = i
413
+ slc = x[tuple(idx)] # static index, removes loop_in_dim
414
+ slc_p = pallas_permute(slc, sub_perm)
415
+ slices.append(slc_p)
416
+
417
+ return jnp.stack(slices, axis=loop_out_dim)
418
+
419
+
420
+ # Maximum tile elements for permutation kernels. Mosaic's
421
+ # jnp.permute_dims produces corrupted output above ~1.5M elements for
422
+ # certain 3D+ permutations. Keep tiles well under that threshold.
423
+ _PERMUTE_MAX_TILE_ELEMS = 1 << 18 # ~256K elements
424
+
425
+
426
+ def pallas_compute_tiling(
427
+ ref_shape: tuple[int, ...],
428
+ transpose: bool = False, # constrain tile size for pallas_permute VMEM
429
+ skip_last_n: int = 0,
430
+ exact_only: bool = False,
431
+ is_tpu: bool = False,
432
+ permutations: list[tuple[int, ...]] | None = None,
433
+ max_grid_product: int | None = None,
434
+ ) -> tuple[tuple[int, ...], tuple[int, ...], dict[int, int]]:
435
+ """Compute tile shape, grid and axis→grid-dim mapping for CPU/TPU.
436
+
437
+ Always uses TPU-compatible alignment (last dim multiple of 128,
438
+ second-to-last multiple of 8) so that the same generated kernel works
439
+ on both CPU-interpret and real TPU.
440
+
441
+ *transpose* constrains tile sizes to keep VMEM usage within safe
442
+ limits for pallas_permute (gather-based permutation).
443
+
444
+ *skip_last_n* prevents tiling the last N dimensions (used when those
445
+ dims correspond to internal reduction ranges that must remain full).
446
+
447
+ *exact_only* restricts tiling to dimensions that divide evenly by the
448
+ tile size (no remainder blocks). Required on TPU where Mosaic needs
449
+ block shapes to match the XLA memory layout.
450
+
451
+ Returns ``(tile_shape, grid, axis_to_grid)`` where *axis_to_grid*
452
+ maps each tiled reference-shape axis index to its position in the
453
+ grid tuple.
454
+
455
+ When no dimension benefits from tiling the grid is ``(1,)`` and the
456
+ tile covers the full tensor.
457
+ """
458
+ nd = len(ref_shape)
459
+ if nd == 0:
460
+ return (), (1,), {}
461
+
462
+ # Effective number of dims eligible for tiling
463
+ tileable_nd = nd - skip_last_n
464
+
465
+ tile = list(ref_shape)
466
+ grid_parts: list[int] = []
467
+ axis_to_grid: dict[int, int] = {} # ref axis → grid dim
468
+
469
+ # Pick alignment based on the physical position of the axis in the
470
+ # tensor, not its position in the tileable subset. The TPU requires
471
+ # the physical last dim to be a multiple of 128 and the physical
472
+ # second-to-last dim to be a multiple of 8.
473
+ #
474
+ # When permutations are present, an output dim `ax` maps to input
475
+ # dim `perm[ax]`. If `perm[ax]` is the input's last dim, that
476
+ # output dim must also satisfy _TPU_ALIGN_LAST. Compute the
477
+ # strictest alignment across all buffers.
478
+ def _input_align(input_ax: int, input_nd: int) -> int:
479
+ return _TPU_ALIGN_LAST if input_ax == input_nd - 1 else _TPU_ALIGN_SECOND_LAST
480
+
481
+ def _align(ax: int) -> int:
482
+ out_align = _TPU_ALIGN_LAST if ax == nd - 1 else _TPU_ALIGN_SECOND_LAST
483
+ if permutations:
484
+ for perm in permutations:
485
+ if perm is not None and len(perm) == nd:
486
+ in_ax = perm[ax]
487
+ in_align = _input_align(in_ax, len(perm))
488
+ out_align = max(out_align, in_align)
489
+ return out_align
490
+
491
+ def _can_tile_ax(ax: int, dim: int, t: int) -> bool:
492
+ """Check if tiling dim to t is valid."""
493
+ if t >= dim:
494
+ # For TPU padding, we allow tiles >= dimension for the aligned axes
495
+ if _align(ax) == _TPU_ALIGN_LAST or _align(ax) == _TPU_ALIGN_SECOND_LAST:
496
+ return True
497
+ return False
498
+ if exact_only and dim % t != 0:
499
+ if _align(ax) == _TPU_ALIGN_LAST or _align(ax) == _TPU_ALIGN_SECOND_LAST:
500
+ # TPU DMA `#tpu.element_window` natively masks out-of-bounds remainder tiles
501
+ return True
502
+ return False
503
+ return True
504
+
505
+ # Second-to-last tileable dim (added first so it becomes grid dim 0)
506
+ if tileable_nd >= 2:
507
+ ax = tileable_nd - 2
508
+ t = _pallas_tile_size(ref_shape[ax], _align(ax), is_tpu=is_tpu)
509
+ if _can_tile_ax(ax, ref_shape[ax], t):
510
+ tile[ax] = t
511
+ axis_to_grid[ax] = len(grid_parts)
512
+ grid_parts.append((ref_shape[ax] + t - 1) // t)
513
+
514
+ # Last tileable dim
515
+ if tileable_nd >= 1:
516
+ ax = tileable_nd - 1
517
+ t = _pallas_tile_size(ref_shape[ax], _align(ax), is_tpu=is_tpu)
518
+ if _can_tile_ax(ax, ref_shape[ax], t):
519
+ tile[ax] = t
520
+ axis_to_grid[ax] = len(grid_parts)
521
+ grid_parts.append((ref_shape[ax] + t - 1) // t)
522
+
523
+ # When transpose is active, tile dimensions to keep the total tile
524
+ # elements below the safe threshold for Mosaic's permute_dims.
525
+ # Mosaic pads the last dimension to _TPU_ALIGN_LAST (128 for f32),
526
+ # so when the last dim is small (e.g. 2), VMEM usage is much higher
527
+ # than the logical tile size. We compute an effective element count
528
+ # that accounts for this padding.
529
+ if transpose:
530
+ tile_elems = 1
531
+ for t in tile:
532
+ tile_elems *= t
533
+ last_dim = tile[nd - 1] if nd > 0 else 1
534
+ padded_last = max(last_dim, _TPU_ALIGN_LAST)
535
+ # pallas_permute decomposes ndim>2 permutations by looping over
536
+ # the smallest dim and recursing, creating (ndim-1)-D intermediates
537
+ # at each level. Mosaic pads the last physical dim of each
538
+ # intermediate to _TPU_ALIGN_LAST (128), which can cause VMEM OOM
539
+ # when a small dim (e.g. 4) ends up as the last dim.
540
+ #
541
+ # For ndim >= 3, compute effective_max from the worst-case VMEM
542
+ # of the (ndim-1)-D intermediate created by pallas_permute.
543
+ # The intermediate removes the loop dim (smallest output dim) and
544
+ # may have a small dim as its last physical dim -> padded to 128.
545
+ if nd >= 4:
546
+ # For 4D+, pallas_permute creates (nd-1)-D intermediates
547
+ # that can have small dims as their last physical dim,
548
+ # which Mosaic pads to _TPU_ALIGN_LAST (128). This can
549
+ # cause VMEM OOM. (3D pallas_permute only creates 2D
550
+ # intermediates which Mosaic handles efficiently.)
551
+ sorted_dims = sorted(tile)
552
+ # The loop dim is the smallest; intermediate has the rest.
553
+ inter_dims = sorted_dims[1:] # (nd-1) dims, sorted
554
+ smallest_inter = inter_dims[0]
555
+ # Padded intermediate: smallest remaining dim padded to 128.
556
+ padded_inter_elems = 1
557
+ for d in inter_dims:
558
+ padded_inter_elems *= d
559
+ pad_factor = max(smallest_inter, _TPU_ALIGN_LAST) // max(smallest_inter, 1)
560
+ padded_inter_elems *= pad_factor
561
+ padded_inter_bytes = padded_inter_elems * 4
562
+ # Conservative VMEM budget: 2MB per intermediate buffer.
563
+ # pallas_permute creates multiple live intermediates (input
564
+ # slices, sub-permutation results, stack temps). Empirically,
565
+ # 2MB per worst-case intermediate avoids OOM on TPU v4 (64MB VMEM).
566
+ vmem_per_buf = 2 * 1024 * 1024 # 2MB
567
+ if padded_inter_bytes > vmem_per_buf:
568
+ scale = vmem_per_buf / padded_inter_bytes
569
+ effective_max = max(int(tile_elems * scale), 1024)
570
+ else:
571
+ effective_max = _PERMUTE_MAX_TILE_ELEMS
572
+ else:
573
+ # For ndim <= 3: pallas_permute creates 2D intermediates
574
+ # (fine) but jnp.stack creates a 3D result with the same
575
+ # shape as the output. If the last dim is small, Mosaic
576
+ # pads it to _TPU_ALIGN_LAST (128), which can exhaust VMEM.
577
+ # Empirically: 8MB per padded buffer keeps us safe on TPU v4
578
+ # (64MB VMEM, multiple buffers live simultaneously).
579
+ vmem_per_buf = 8 * 1024 * 1024 # 8MB
580
+ max_nonlast_product = vmem_per_buf // (padded_last * 4)
581
+ effective_max = min(
582
+ max_nonlast_product * last_dim,
583
+ _PERMUTE_MAX_TILE_ELEMS,
584
+ )
585
+ effective_max = min(effective_max, _PERMUTE_MAX_TILE_ELEMS)
586
+ effective_max = max(effective_max, 1024)
587
+
588
+ if tile_elems > effective_max:
589
+ # Collect tileable dims. For ndim >= 4 with VMEM-constrained
590
+ # intermediates, include ALL tileable dims (even the last)
591
+ # since we may need to shrink every dim to fit in VMEM.
592
+ # For ndim <= 3, exclude the last dim (it's often small
593
+ # and already at minimum).
594
+ if nd >= 4:
595
+ tileable_axes = [
596
+ ax for ax in range(tileable_nd - 1, -1, -1) if ref_shape[ax] > 1
597
+ ]
598
+ else:
599
+ tileable_axes = [
600
+ ax for ax in range(tileable_nd - 2, -1, -1) if ref_shape[ax] > 1
601
+ ]
602
+ # Distribute reduction proportionally across dims.
603
+ reduction = tile_elems / effective_max
604
+ n = len(tileable_axes)
605
+ per_dim = reduction ** (1.0 / max(n, 1))
606
+
607
+ for ax in tileable_axes:
608
+ if tile_elems <= effective_max:
609
+ break
610
+ dim = ref_shape[ax]
611
+ cur_t = tile[ax]
612
+ align = _align(ax)
613
+ # Target tile: proportional reduction, but also check
614
+ # against the remaining budget for subsequent dims.
615
+ target = int(cur_t / per_dim)
616
+ remaining_budget = effective_max * cur_t // tile_elems
617
+ target = min(target, remaining_budget)
618
+ target = (target // align) * align
619
+ target = max(target, align)
620
+ # Find largest aligned divisor of dim <= target.
621
+ best_t = cur_t
622
+ for candidate in range(target, 0, -align):
623
+ if candidate >= cur_t:
624
+ continue
625
+ if dim % candidate != 0:
626
+ continue
627
+ best_t = candidate
628
+ break
629
+ if best_t < cur_t:
630
+ if ax in axis_to_grid:
631
+ grid_parts[axis_to_grid[ax]] = dim // best_t
632
+ else:
633
+ axis_to_grid[ax] = len(grid_parts)
634
+ grid_parts.append(dim // best_t)
635
+ tile_elems = tile_elems * best_t // cur_t
636
+ tile[ax] = best_t
637
+
638
+ grid = tuple(grid_parts) if grid_parts else (1,)
639
+
640
+ # Enforce max_grid_product by scaling up tiles on tiled axes.
641
+ if max_grid_product is not None and grid_parts:
642
+ grid_product = 1
643
+ for g in grid_parts:
644
+ grid_product *= g
645
+ if grid_product > max_grid_product:
646
+ # Sort tiled axes by their grid contribution (descending)
647
+ # and increase tiles to reduce the grid.
648
+ tiled_axes = sorted(
649
+ axis_to_grid.keys(),
650
+ key=lambda ax: grid_parts[axis_to_grid[ax]],
651
+ reverse=True,
652
+ )
653
+ for ax in tiled_axes:
654
+ if grid_product <= max_grid_product:
655
+ break
656
+ gi = axis_to_grid[ax]
657
+ cur_grid = grid_parts[gi]
658
+ cur_tile = tile[ax]
659
+ dim = ref_shape[ax]
660
+ align = _align(ax)
661
+ # Target grid for this axis: proportional share of reduction
662
+ target_grid = max(1, int(cur_grid * max_grid_product / grid_product))
663
+ target_tile = (dim + target_grid - 1) // target_grid
664
+ # Round up to alignment
665
+ target_tile = ((target_tile + align - 1) // align) * align
666
+ target_tile = min(target_tile, dim)
667
+ # Find smallest aligned value >= target_tile that divides dim
668
+ # (or just use the rounded-up value if exact_only is False)
669
+ best_tile = dim # fallback: full dim
670
+ if exact_only:
671
+ for candidate in range(target_tile, dim + 1, align):
672
+ if dim % candidate == 0:
673
+ best_tile = candidate
674
+ break
675
+ else:
676
+ best_tile = target_tile
677
+ if best_tile > cur_tile:
678
+ new_grid = (dim + best_tile - 1) // best_tile
679
+ grid_product = grid_product * new_grid // cur_grid
680
+ grid_parts[gi] = new_grid
681
+ tile[ax] = best_tile
682
+ grid = tuple(grid_parts)
683
+
684
+ return tuple(tile), grid, axis_to_grid
685
+
686
+
687
+ def pallas_make_block_spec(
688
+ buf_shape: tuple[int, ...],
689
+ ref_shape: tuple[int, ...],
690
+ tile_shape: tuple[int, ...],
691
+ axis_to_grid: dict[int, int],
692
+ n_grid: int,
693
+ permutation: tuple[int, ...] | None = None,
694
+ is_output: bool = False,
695
+ ) -> Any:
696
+ """Build a ``pl.BlockSpec`` for *buf_shape* given tiling of *ref_shape*.
697
+
698
+ Lower-ndim buffers are right-aligned with the reference shape (numpy
699
+ broadcast rules). Dimensions that match a tiled reference dimension
700
+ are tiled; broadcast dimensions (size 1 or absent) are kept full.
701
+
702
+ When *buf_nd > ref_nd* (reduction inputs), we find an alignment offset
703
+ so the ref dims map into the buffer. Extra dims are kept at full size
704
+ with index 0 in the index_map.
705
+
706
+ When *permutation* is given (a tuple mapping ref axis -> buf axis),
707
+ the buffer dimensions are accessed in permuted order relative to the
708
+ reference shape. For example, permutation=(1, 0) swaps the last two.
709
+
710
+ When *is_output* is True and *buf_nd < ref_nd*, left-alignment is used
711
+ as a fallback (for reduction outputs whose trailing dims were reduced).
712
+ """
713
+ from jax.experimental import ( # pyrefly: ignore [import-error, missing-import]
714
+ pallas as pl,
715
+ )
716
+
717
+ buf_nd = len(buf_shape)
718
+ ref_nd = len(ref_shape)
719
+
720
+ if buf_nd == 0:
721
+ # Scalar — untouched regardless of grid shape.
722
+ return pl.BlockSpec((1,), _make_index_map([], 1, n_grid))
723
+
724
+ bs = list(buf_shape)
725
+ tiled_pairs: list[tuple[int, int]] = []
726
+
727
+ if buf_nd > ref_nd:
728
+ # Reduction input: find alignment offset k where ref dims map into buf.
729
+ align_k = 0
730
+ for k in range(buf_nd - ref_nd + 1):
731
+ ok = True
732
+ for i in range(ref_nd):
733
+ if ref_shape[i] == 1:
734
+ continue
735
+ if buf_shape[k + i] != ref_shape[i]:
736
+ ok = False
737
+ break
738
+ if ok:
739
+ align_k = k
740
+ break
741
+
742
+ for ref_ax, grid_dim in axis_to_grid.items():
743
+ buf_ax = align_k + ref_ax
744
+ if 0 <= buf_ax < buf_nd and buf_shape[buf_ax] == ref_shape[ref_ax]:
745
+ bs[buf_ax] = tile_shape[ref_ax]
746
+ tiled_pairs.append((buf_ax, grid_dim))
747
+
748
+ elif permutation is not None and buf_nd == ref_nd:
749
+ # Permuted buffer: map ref axes through the permutation.
750
+ for ref_ax, grid_dim in axis_to_grid.items():
751
+ buf_ax = permutation[ref_ax]
752
+ if 0 <= buf_ax < buf_nd and buf_shape[buf_ax] == ref_shape[ref_ax]:
753
+ bs[buf_ax] = tile_shape[ref_ax]
754
+ tiled_pairs.append((buf_ax, grid_dim))
755
+
756
+ else:
757
+ # Standard right-alignment, with left-alignment fallback for
758
+ # reduction outputs (e.g. sum(dim=-1) on (10,10) → (10,)).
759
+ for ref_ax, grid_dim in axis_to_grid.items():
760
+ buf_ax = ref_ax - (ref_nd - buf_nd)
761
+ if 0 <= buf_ax < buf_nd and buf_shape[buf_ax] == ref_shape[ref_ax]:
762
+ bs[buf_ax] = tile_shape[ref_ax]
763
+ tiled_pairs.append((buf_ax, grid_dim))
764
+ elif (
765
+ is_output
766
+ and buf_nd < ref_nd
767
+ and 0 <= ref_ax < buf_nd
768
+ and buf_shape[ref_ax] == ref_shape[ref_ax]
769
+ ):
770
+ # Left-aligned match for output: buf dim i matches ref dim i
771
+ # (reduction output whose trailing dims were reduced away)
772
+ bs[ref_ax] = tile_shape[ref_ax]
773
+ tiled_pairs.append((ref_ax, grid_dim))
774
+
775
+ return pl.BlockSpec(
776
+ tuple(bs),
777
+ _make_index_map(tiled_pairs, buf_nd, n_grid),
778
+ )
779
+
780
+
781
+ def _make_index_map(
782
+ tiled_pairs: list[tuple[int, int]],
783
+ buf_nd: int,
784
+ n_grid: int,
785
+ ) -> Any:
786
+ """Return an index_map callable for ``pl.BlockSpec``.
787
+
788
+ *tiled_pairs* is a list of ``(buf_axis, grid_dim)`` indicating which
789
+ buffer axes receive a grid index. All other axes return 0 (full block).
790
+
791
+ All returned values are explicitly ``jnp.int32`` so that TPU Mosaic
792
+ lowering (which rejects 64-bit types) works when ``jax_enable_x64`` is
793
+ active. The casts are created inside the function body (not captured)
794
+ to satisfy JAX's "index_map must not capture constants" rule.
795
+ """
796
+ import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
797
+
798
+ # Pre-build the mapping so the returned lambda is a plain lookup.
799
+ mapping = dict(tiled_pairs)
800
+
801
+ if n_grid == 0 or (n_grid == 1 and not mapping):
802
+ return lambda _i: tuple(jnp.int32(0) for _ in range(buf_nd))
803
+
804
+ def index_map(*grid_args):
805
+ return tuple(
806
+ jnp.int32(grid_args[mapping[d]]) if d in mapping else jnp.int32(0)
807
+ for d in range(buf_nd)
808
+ )
809
+
810
+ return index_map
811
+
812
+
813
+ def pallas_ensure_nonzero_rank(x: torch.Tensor) -> torch.Tensor:
814
+ if len(x.shape) == 0:
815
+ return x.reshape((1,))
816
+ return x
817
+
818
+
819
+ def pallas_make_block_spec_non_tiled(shape: tuple[int, ...]) -> Any:
820
+ import jax.numpy as jnp # pyrefly: ignore [import-error, missing-import]
821
+ from jax.experimental import ( # pyrefly: ignore [import-error, missing-import]
822
+ pallas as pl,
823
+ )
824
+
825
+ nonzero_rank_shape = shape if len(shape) > 0 else (1,)
826
+ return pl.BlockSpec(
827
+ nonzero_rank_shape,
828
+ lambda i: [jnp.int32(i)] * len(nonzero_rank_shape),
829
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/static_triton_launcher.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ import os
3
+ from functools import cached_property
4
+ from typing import Any
5
+ from typing_extensions import Unpack
6
+
7
+ from ..utils import is_rocm
8
+ from .triton_compat import ASTSource, CompiledKernel, knobs as triton_knobs
9
+ from .triton_helpers import get_constexprs
10
+
11
+
12
+ class StaticallyLaunchedTritonKernel:
13
+ """
14
+ Parses the metadata of a CompiledKernel from Triton into a structure that can
15
+ launch the cuda kernel directly. Only works for triton kernels compiled to cubin.
16
+
17
+ Doing this avoids C++ codegen and compilation during compile, since we can use a
18
+ statically compiled library to launch the kernel. To avoid mallocing for the arguments,
19
+ we have a launcher for different numbers of arguments up to a max. StaticCudaLauncher
20
+ only supports # of arguments up until 10 for now.
21
+
22
+ Workflow:
23
+ Compile time:
24
+ 1. Compile a kernel with triton and get a CompiledKernel
25
+ 2. Instantiate kernel = StaticallyLaunchedTritonKernel(triton_kernel)
26
+ 3. Write to a cubin file: kernel.write_cubin_to_file(filepath)
27
+ 4. Call kernel.load_kernel() (CUDA should be initialized by this point) to load the cubin
28
+ Runtime:
29
+ 5. Call kernel.run(grid, stream, args) to launch the kernel
30
+
31
+ Note that after step 3, StaticallyLaunchedTritonKernel is fully pickleable/serializable.
32
+ This allows it to be cached by FXGraphCache/TritonBundler, as well as sent from the worker
33
+ to the parent process in inductor.
34
+
35
+ There are two main versions of triton that we wish to support: 3.3 and 3.2. Triton makes considerable changes
36
+ to how it handles constants in 3.3, so there's some special logic necessary to handle both versions.
37
+ """
38
+
39
+ @cached_property
40
+ def C_impl(self):
41
+ raise NotImplementedError
42
+
43
+ def __init__(self, kernel: CompiledKernel) -> None:
44
+ # pyrefly: ignore [missing-attribute]
45
+ self.name = kernel.src.fn.__name__
46
+ # pyrefly: ignore [missing-attribute]
47
+ self.cubin_path = kernel._cubin_path
48
+
49
+ # Used by torch.compile to filter constants in older triton versions
50
+ # pyrefly: ignore [missing-attribute]
51
+ self.arg_names = kernel.src.fn.arg_names
52
+
53
+ # Const exprs that are declared by the triton kernel directly
54
+ # Used to generate the kernel launcher's def args
55
+ # pyrefly: ignore [missing-attribute]
56
+ self.declared_constexprs = get_constexprs(kernel.src.fn)
57
+
58
+ # pyrefly: ignore [missing-attribute]
59
+ self.hash = kernel.hash
60
+
61
+ if triton_knobs is None:
62
+ # pyrefly: ignore [missing-attribute]
63
+ launch_enter = kernel.__class__.launch_enter_hook
64
+ # pyrefly: ignore [missing-attribute]
65
+ launch_exit = kernel.__class__.launch_exit_hook
66
+ else:
67
+ launch_enter = triton_knobs.runtime.launch_enter_hook
68
+ launch_exit = triton_knobs.runtime.launch_exit_hook
69
+
70
+ def hook_is_empty(hook: Any) -> bool:
71
+ if hook is None:
72
+ return True
73
+ if (
74
+ triton_knobs
75
+ and (HookChain := getattr(triton_knobs, "HookChain", None)) is not None
76
+ and isinstance(hook, HookChain)
77
+ ):
78
+ # Support hooks after https://github.com/triton-lang/triton/pull/7866
79
+ return len(hook.calls) == 0
80
+ return False
81
+
82
+ if not hook_is_empty(launch_enter) or not hook_is_empty(launch_exit):
83
+ raise NotImplementedError(
84
+ "We don't support launch enter or launch exit hooks"
85
+ )
86
+ # pyrefly: ignore [missing-attribute]
87
+ self.num_warps = kernel.metadata.num_warps
88
+ self.shared = (
89
+ # pyrefly: ignore [missing-attribute]
90
+ kernel.shared if hasattr(kernel, "shared") else kernel.metadata.shared
91
+ )
92
+
93
+ def needs_scratch_arg(scratch_name: str, param_name: str) -> bool:
94
+ # pyrefly: ignore [missing-attribute]
95
+ if hasattr(kernel.metadata, param_name):
96
+ # pyrefly: ignore [missing-attribute]
97
+ if getattr(kernel.metadata, param_name) > 0:
98
+ raise NotImplementedError(
99
+ f"{scratch_name} scratch not yet supported"
100
+ )
101
+ return True
102
+ return False
103
+
104
+ # Newer triton versions pass an extra global scratch parameter to the compiled cuda kernel.
105
+ # Inductor never uses this field or enables it, but we still have to pass
106
+ # an extra None into the set of params if its enabled
107
+ self.has_global_scratch = needs_scratch_arg("Global", "global_scratch_size")
108
+ # same situation for profile scratch - triton-lang/triton#7258
109
+ self.has_profile_scratch = needs_scratch_arg("Profile", "profile_scratch_size")
110
+
111
+ # pyrefly: ignore [missing-attribute]
112
+ self.arg_tys = self.arg_ty_from_signature(kernel.src)
113
+ self.function: int | None = None # Loaded by load_kernel(on the parent process)
114
+ num_ctas = 1
115
+ if hasattr(kernel, "num_ctas"):
116
+ num_ctas = kernel.num_ctas
117
+ elif hasattr(kernel, "metadata"):
118
+ num_ctas = kernel.metadata.num_ctas
119
+
120
+ if num_ctas != 1:
121
+ raise NotImplementedError(
122
+ "Static cuda launcher only supports num_ctas == 1"
123
+ )
124
+
125
+ def reload_cubin_from_raw(self, filepath: str) -> str:
126
+ """
127
+ If the cubin file triton generated gets deleted under us, we can
128
+ reload it from the raw cubin file.
129
+ """
130
+ if self.cubin_path is None:
131
+ assert self.cubin_raw is not None
132
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
133
+ with open(filepath, "wb") as f:
134
+ f.write(self.cubin_raw)
135
+ self.cubin_path = filepath # pyre-ignore
136
+ return self.cubin_path
137
+
138
+ def load_kernel(self, device: int) -> None:
139
+ if self.function is not None:
140
+ return
141
+
142
+ assert hasattr(self, "cubin_path")
143
+ assert self.cubin_path is not None
144
+ (self.function, self.n_regs, self.n_spills) = self.C_impl._load_kernel(
145
+ self.cubin_path, self.name, self.shared, device
146
+ )
147
+ # Don't need the cubin path anymore now that we've loaded
148
+ self.cubin_path = None
149
+ self.cubin_raw = None
150
+
151
+ @staticmethod
152
+ @functools.lru_cache
153
+ def type_mappings() -> dict[str, str]:
154
+ return {
155
+ "i1": "i",
156
+ "i8": "b",
157
+ "i16": "h",
158
+ "i32": "i",
159
+ "i64": "l",
160
+ "u1": "I",
161
+ "u8": "B",
162
+ "u16": "H",
163
+ "u32": "I",
164
+ "u64": "K",
165
+ "fp16": "f",
166
+ "bf16": "f",
167
+ "fp32": "f",
168
+ "f32": "f",
169
+ "fp64": "d",
170
+ # TODO handle nvTmaDesc/CUtensormap
171
+ }
172
+
173
+ def extract_type(self, ty: str) -> str:
174
+ """
175
+ Takes a triton type from CompiledKernel.signature and
176
+ converts it into a single char encoding. _StaticCudaLauncher
177
+ will switch on this char to figure out what type the underlying
178
+ value should be passed to the triton kernel as.
179
+ """
180
+ if ty[0] == "*":
181
+ return "O"
182
+ elif ty == "nvTmaDesc":
183
+ raise NotImplementedError("nvTmaDesc kernels are not yet supported")
184
+ return StaticallyLaunchedTritonKernel.type_mappings()[ty]
185
+
186
+ def arg_ty_from_signature(self, src: ASTSource) -> str:
187
+ def index_key(i: Any) -> int:
188
+ if isinstance(i, str):
189
+ # pyrefly: ignore [missing-attribute]
190
+ return src.fn.arg_names.index(i)
191
+ elif isinstance(i, tuple):
192
+ # In triton 3.3, src.fn.constants has tuples as a key
193
+ return i[0]
194
+ else:
195
+ return i
196
+
197
+ # pyrefly: ignore [missing-attribute]
198
+ signature = {index_key(key): value for key, value in src.signature.items()}
199
+ # Triton uses these as the main way to filter out constants passed to their cubin
200
+ constants = [index_key(key) for key in getattr(src, "constants", dict())]
201
+ # This value is always a superset of kernel.fn.constexprs: kernel.fn.constexprs are
202
+ # constants declared by the triton kernel directly, whereas this list can have
203
+ # constants that are unused by the triton kernel that triton figured out during
204
+ # compilation.
205
+ self.full_constexprs = constants
206
+ # Despite requiring them to be passed in, the triton CUDA launcher
207
+ # completely ignores the constexprs passed into it when generating code.
208
+ # So we can ignore them here too
209
+ params = []
210
+
211
+ for i in sorted(signature.keys()):
212
+ ty = signature[i]
213
+ # In newer triton versions, constants are passed in to signature with type `constexpr`
214
+ # In older triton versions, there can be constants in src.constants that are not `constexpr` in signature
215
+ # so we check both here
216
+ if ty == "constexpr" or i in constants:
217
+ pass
218
+ else:
219
+ # pyrefly: ignore [bad-argument-type]
220
+ params.append(self.extract_type(ty))
221
+ return "".join(params)
222
+
223
+ def __getstate__(self) -> dict[str, Any]:
224
+ # Remove objects that are no longer valid for pickling
225
+ state = self.__dict__.copy()
226
+ state["function"] = None
227
+ # Cubin paths aren't consistent across processes, so we clear
228
+ # and reload them.
229
+ state["cubin_path"] = None
230
+ return state
231
+
232
+ def run(
233
+ self,
234
+ grid_x: int,
235
+ grid_y: int,
236
+ grid_z: int,
237
+ stream: int,
238
+ *args: Unpack[tuple[object, ...]],
239
+ ) -> None:
240
+ """Actually run the kernel at runtime. This function is the hot codepath."""
241
+
242
+ # Assert load_kernel() has been called and args match
243
+ assert self.function is not None
244
+
245
+ # TODO: actually, if the args *don't* match, we probably should
246
+ # throw an exception. But if inductor is the only one calling this
247
+ # thing, it should always match.
248
+ # Get rid of constants before passing to cubin launcher
249
+
250
+ arg_tys = self.arg_tys
251
+
252
+ if is_rocm():
253
+ # ROCm/HIP kernel ABI: The Triton HIP backend ALWAYS includes both
254
+ # global_scratch and profile_scratch parameters in the kernel signature,
255
+ # even when the kernel doesn't use them (i.e., when has_*_scratch is False).
256
+ #
257
+ # This differs fundamentally from CUDA, where these parameters are only
258
+ # present in the signature if the corresponding has_*_scratch flag is True.
259
+ #
260
+ # The flags indicate whether memory will be allocated/used:
261
+ # - has_global_scratch: Whether global scratch workspace is needed
262
+ # - has_profile_scratch: Whether profiling instrumentation is enabled
263
+ #
264
+ # However, regardless of flag values, we MUST always pass both parameters
265
+ # to match the HIP kernel ABI. Passing None is safe:
266
+ #
267
+ # - If scratch is not needed (has_*_scratch=False or scratch_size=0):
268
+ # The None becomes nullptr, which the kernel never dereferences
269
+ #
270
+ # - If scratch is needed (has_*_scratch=True and scratch_size>0):
271
+ # The None becomes nullptr initially, but the HIP runtime intercepts
272
+ # the kernel launch, allocates the required scratch memory based on
273
+ # kernel metadata, and replaces the nullptr with a valid pointer before
274
+ # the kernel actually executes
275
+ #
276
+ # Not passing both parameters causes segmentation faults because the kernel
277
+ # expects them at specific positions in the argument array.
278
+ arg_tys = arg_tys + "OO"
279
+ args = (*args, None, None)
280
+
281
+ else:
282
+ for has_scratch in [self.has_global_scratch, self.has_profile_scratch]:
283
+ if has_scratch:
284
+ arg_tys = arg_tys + "O"
285
+ args = (*args, None)
286
+ # pyrefly: ignore [bad-argument-type]
287
+ assert len(args) == len(arg_tys)
288
+
289
+ # TODO: can handle grid functions here or in C++, so
290
+ # that we don't need the grid handler above.
291
+ self.C_impl._launch_kernel(
292
+ self.function,
293
+ grid_x,
294
+ grid_y,
295
+ grid_z,
296
+ self.num_warps,
297
+ self.shared,
298
+ arg_tys,
299
+ args,
300
+ stream,
301
+ )
302
+
303
+
304
+ class StaticallyLaunchedCudaKernel(StaticallyLaunchedTritonKernel):
305
+ @cached_property
306
+ def C_impl(self):
307
+ from torch._C import _StaticCudaLauncher
308
+
309
+ return _StaticCudaLauncher
310
+
311
+ def __init__(self, kernel: CompiledKernel) -> None:
312
+ # pyrefly: ignore [missing-attribute]
313
+ if "hsaco" in kernel.asm:
314
+ # pyrefly: ignore [missing-attribute]
315
+ self.cubin_raw = kernel.asm["hsaco"]
316
+
317
+ # pyrefly: ignore [missing-attribute]
318
+ elif "cubin" in kernel.asm:
319
+ # pyrefly: ignore [missing-attribute]
320
+ self.cubin_raw = kernel.asm["cubin"]
321
+ else:
322
+ raise RuntimeError(
323
+ "Expected either 'hsaco' (ROCm) or 'cubin' (CUDA) in kernel.asm"
324
+ )
325
+ super().__init__(kernel)
326
+
327
+
328
+ class StaticallyLaunchedXpuKernel(StaticallyLaunchedTritonKernel):
329
+ @cached_property
330
+ def C_impl(self):
331
+ from torch._C import _StaticXpuLauncher
332
+
333
+ return _StaticXpuLauncher
334
+
335
+ def __init__(self, kernel: CompiledKernel) -> None:
336
+ # pyrefly: ignore [missing-attribute]
337
+ self.cubin_raw = kernel.asm.get("zebin", None)
338
+ super().__init__(kernel)
339
+
340
+
341
+ def statically_launched_kernel_by_device(
342
+ kernel: CompiledKernel, device_type: str = "cuda"
343
+ ) -> StaticallyLaunchedTritonKernel:
344
+ if device_type in ("cuda", "hip"):
345
+ return StaticallyLaunchedCudaKernel(kernel)
346
+ elif device_type == "xpu":
347
+ return StaticallyLaunchedXpuKernel(kernel)
348
+ else:
349
+ raise NotImplementedError(
350
+ f"Device type {device_type} is not supported for static launcher"
351
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_compat.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any
5
+
6
+ import torch
7
+
8
+
9
+ try:
10
+ import triton
11
+ except ImportError:
12
+ triton = None
13
+
14
+
15
+ if triton is not None:
16
+ import triton.language as tl
17
+ from triton import Config
18
+ from triton.compiler import CompiledKernel
19
+ from triton.runtime.autotuner import OutOfResources
20
+ from triton.runtime.jit import JITFunction, KernelInterface
21
+
22
+ try:
23
+ from triton.runtime.autotuner import PTXASError
24
+ except ImportError:
25
+
26
+ class PTXASError(Exception): # type: ignore[no-redef]
27
+ pass
28
+
29
+ try:
30
+ from triton.compiler.compiler import ASTSource
31
+ except ImportError:
32
+ ASTSource = None
33
+
34
+ try:
35
+ from triton.backends.compiler import GPUTarget
36
+ except ImportError:
37
+
38
+ def GPUTarget(
39
+ backend: str,
40
+ arch: int | str,
41
+ warp_size: int,
42
+ ) -> Any:
43
+ if torch.version.hip:
44
+ return [backend, arch, warp_size]
45
+ return (backend, arch)
46
+
47
+ # In the latest triton, math functions were shuffled around into different modules:
48
+ # https://github.com/triton-lang/triton/pull/3172
49
+ try:
50
+ from triton.language.extra import libdevice
51
+
52
+ libdevice = tl.extra.libdevice # noqa: F811
53
+ math = tl.math
54
+ except ImportError:
55
+ if hasattr(tl.extra, "cuda") and hasattr(tl.extra.cuda, "libdevice"):
56
+ libdevice = tl.extra.cuda.libdevice
57
+ math = tl.math
58
+ elif hasattr(tl.extra, "intel") and hasattr(tl.extra.intel, "libdevice"):
59
+ libdevice = tl.extra.intel.libdevice
60
+ math = tl.math
61
+ else:
62
+ libdevice = tl.math
63
+ math = tl
64
+
65
+ try:
66
+ from triton.language.standard import _log2
67
+ except ImportError:
68
+
69
+ def _log2(x: Any) -> Any:
70
+ raise NotImplementedError
71
+
72
+ def _triton_config_has(param_name: str) -> bool:
73
+ if not hasattr(triton, "Config"):
74
+ return False
75
+ if not hasattr(triton.Config, "__init__"):
76
+ return False
77
+ return param_name in inspect.signature(triton.Config.__init__).parameters
78
+
79
+ # Drop the legacy support of autoWS
80
+ HAS_WARP_SPEC = False
81
+
82
+ try:
83
+ from triton import knobs
84
+ except ImportError:
85
+ knobs = None
86
+
87
+ try:
88
+ from triton.runtime.cache import triton_key # type: ignore[attr-defined]
89
+ except ImportError:
90
+ from triton.compiler.compiler import (
91
+ triton_key, # type: ignore[attr-defined,no-redef]
92
+ )
93
+
94
+ try:
95
+ from triton.runtime.errors import IntelGPUError
96
+ except ImportError:
97
+
98
+ class IntelGPUError(Exception): # type: ignore[no-redef]
99
+ pass
100
+
101
+ builtins_use_semantic_kwarg = (
102
+ "_semantic" in inspect.signature(triton.language.core.view).parameters
103
+ )
104
+ HAS_TRITON = True
105
+ else:
106
+
107
+ def _raise_error(*args: Any, **kwargs: Any) -> Any:
108
+ raise RuntimeError("triton package is not installed")
109
+
110
+ class OutOfResources(Exception): # type: ignore[no-redef]
111
+ pass
112
+
113
+ class PTXASError(Exception): # type: ignore[no-redef]
114
+ pass
115
+
116
+ class IntelGPUError(Exception): # type: ignore[no-redef]
117
+ pass
118
+
119
+ Config = object
120
+ CompiledKernel = object
121
+ KernelInterface = object
122
+ ASTSource = None
123
+ GPUTarget = None
124
+ _log2 = _raise_error
125
+ libdevice = None
126
+ math = None
127
+ knobs = None
128
+ builtins_use_semantic_kwarg = False
129
+
130
+ class triton: # type: ignore[no-redef]
131
+ @staticmethod
132
+ def jit(*args: Any, **kwargs: Any) -> Any:
133
+ return _raise_error
134
+
135
+ class tl: # type: ignore[no-redef]
136
+ @staticmethod
137
+ def constexpr(val: Any) -> Any:
138
+ return val
139
+
140
+ tensor = Any
141
+ dtype = Any
142
+
143
+ class JITFunction: # type: ignore[no-redef]
144
+ pass
145
+
146
+ HAS_WARP_SPEC = False
147
+ triton_key = _raise_error
148
+ HAS_TRITON = False
149
+
150
+
151
+ def cc_warp_size(cc: str | int) -> int:
152
+ if torch.version.hip:
153
+ if "gfx9" in str(cc):
154
+ return 64
155
+ else:
156
+ return 32
157
+ else:
158
+ return 32
159
+
160
+
161
+ try:
162
+ autograd_profiler = torch.autograd.profiler
163
+ except AttributeError: # Compile workers only have a mock version of torch
164
+
165
+ class autograd_profiler: # type: ignore[no-redef]
166
+ _is_profiler_enabled = False
167
+
168
+
169
+ __all__ = [
170
+ "Config",
171
+ "CompiledKernel",
172
+ "OutOfResources",
173
+ "KernelInterface",
174
+ "PTXASError",
175
+ "IntelGPUError",
176
+ "ASTSource",
177
+ "GPUTarget",
178
+ "tl",
179
+ "_log2",
180
+ "libdevice",
181
+ "math",
182
+ "triton",
183
+ "cc_warp_size",
184
+ "knobs",
185
+ "triton_key",
186
+ ]
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_helpers.py ADDED
@@ -0,0 +1,850 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-decorators
2
+ # mypy: allow-untyped-defs
3
+ import math as pymath
4
+ import warnings
5
+ from collections.abc import Callable
6
+ from typing import Any, TypeVar
7
+
8
+ from .triton_compat import ( # noqa: F401
9
+ _log2,
10
+ builtins_use_semantic_kwarg,
11
+ JITFunction,
12
+ libdevice,
13
+ math,
14
+ tl,
15
+ triton,
16
+ )
17
+
18
+
19
+ _T = TypeVar("_T")
20
+ _LOG_2_E: tl.constexpr = tl.constexpr(pymath.log2(pymath.e))
21
+
22
+
23
+ def set_driver_to_cpu():
24
+ driver = triton.runtime.driver
25
+ if backend := triton.backends.backends.get("cpu", None):
26
+ if isinstance(driver.active, backend.driver):
27
+ # Don't re-initialize backend if it is already active
28
+ return
29
+ driver.set_active(backend.driver())
30
+ return
31
+ # This can be a hard error once triton-cpu is merged into fbcode
32
+ warnings.warn(
33
+ "Could not find an active CPU backend. Generated kernels will not be executable!"
34
+ )
35
+
36
+
37
+ def _is_backend_active(name, backend):
38
+ if backend.driver.is_active():
39
+ return True
40
+ # Triton may fail to detect the GPU in subprocess workers when using
41
+ # ctypes-based driver detection (triton-lang/triton#9578). Fall back
42
+ # to torch's own device checks which are more reliable in these environments.
43
+ if name == "nvidia":
44
+ import torch
45
+
46
+ return torch.cuda.is_available() and torch.version.hip is None
47
+ if name == "amd":
48
+ import torch
49
+
50
+ return torch.cuda.is_available() and torch.version.hip is not None
51
+ return False
52
+
53
+
54
+ def set_driver_to_gpu():
55
+ driver = triton.runtime.driver
56
+ for name, backend in triton.backends.backends.items():
57
+ if _is_backend_active(name, backend) and name != "cpu":
58
+ # After https://github.com/triton-lang/triton/commit/b844d519bc5e86edf00fe6b3c6c2d1badcd509a4,
59
+ # `driver.active` can be of `LazyProxy` type and the sign of this - `_obj` attribute.
60
+ if (
61
+ isinstance(driver.active, backend.driver)
62
+ or hasattr(driver.active, "_obj")
63
+ and isinstance(driver.active._obj, backend.driver)
64
+ ):
65
+ # Don't re-initialize backend if it is already active
66
+ return
67
+ driver.set_active(backend.driver())
68
+ return
69
+ raise RuntimeError("Could not find an active GPU backend")
70
+
71
+
72
+ def get_backend_options():
73
+ from triton.runtime import driver
74
+
75
+ target = driver.active.get_current_target()
76
+ backend = triton.compiler.compiler.make_backend(target)
77
+ options = backend.parse_options(dict())
78
+ return options.__dict__
79
+
80
+
81
+ def get_constexprs(kernel: JITFunction) -> list[int]:
82
+ return [p.num for p in kernel.params if p.is_constexpr]
83
+
84
+
85
+ @triton.jit
86
+ def promote_to_tensor(x):
87
+ # Addition promotes to tensor for us
88
+ return x + tl.zeros((1,), tl.int1)
89
+
90
+
91
+ @triton.jit
92
+ def div_floor_integer(a, b):
93
+ # NOTE: a // b is C division, but we want floor division
94
+ # Based on c10::div_floor_integer
95
+ quot = a // b
96
+ remainder = a % b
97
+ fixed = tl.where(remainder != 0, quot - 1, quot)
98
+ return tl.where((a < 0) != (b < 0), fixed, quot)
99
+
100
+
101
+ @triton.jit
102
+ def remainder_integer(a, b):
103
+ # NOTE: a % b matches C division, not floor division
104
+ remainder = a % b
105
+ return tl.where((remainder != 0) & ((a < 0) != (b < 0)), remainder + b, remainder)
106
+
107
+
108
+ @triton.jit
109
+ def pow_integer(base, exponent):
110
+ # Triton has no exact integer pow primitive; use repeated squaring for
111
+ # nonnegative integer exponents so integral scalar pow does not round
112
+ # through libdevice.pow before casting back to int.
113
+ exponent_dtype: tl.constexpr = tl.core.get_int_dtype(
114
+ exponent.dtype.primitive_bitwidth, signed=False
115
+ )
116
+ exp = exponent.to(exponent_dtype)
117
+ result = tl.full(base.shape, 1, base.dtype)
118
+ for _ in tl.static_range(exponent_dtype.primitive_bitwidth):
119
+ result = tl.where((exp & 1) != 0, result * base, result)
120
+ exp = exp >> 1
121
+ base = base * base
122
+ return result
123
+
124
+
125
+ @triton.jit
126
+ def is_floating(x):
127
+ return promote_to_tensor(x).dtype.is_floating()
128
+
129
+
130
+ @triton.jit
131
+ def _prod_accumulate(a, b):
132
+ return a * b
133
+
134
+
135
+ @triton.jit
136
+ def prod(input, axis):
137
+ return tl.reduce(input, axis, _prod_accumulate)
138
+
139
+
140
+ @triton.jit
141
+ def minimum(a, b):
142
+ mask = a < b
143
+ if is_floating(a):
144
+ mask |= a != a
145
+ return tl.where(mask, a, b)
146
+
147
+
148
+ @triton.jit
149
+ def maximum(a, b):
150
+ mask = a > b
151
+ if is_floating(a):
152
+ mask |= a != a
153
+ return tl.where(mask, a, b)
154
+
155
+
156
+ @triton.jit
157
+ def min2(a, dim):
158
+ return tl.reduce(a, dim, minimum)
159
+
160
+
161
+ @triton.jit
162
+ def max2(a, dim):
163
+ return tl.reduce(a, dim, maximum)
164
+
165
+
166
+ @triton.jit
167
+ def minimum_with_index(a_value, a_index, b_value, b_index):
168
+ mask = a_value < b_value
169
+ equal = a_value == b_value
170
+ if is_floating(a_value):
171
+ a_isnan = a_value != a_value
172
+ b_isnan = b_value != b_value
173
+ mask |= a_isnan & (not b_isnan)
174
+ # Consider NaNs as equal
175
+ equal |= a_isnan & b_isnan
176
+
177
+ # Prefer lowest index if values are equal
178
+ mask |= equal & (a_index < b_index)
179
+ return tl.where(mask, a_value, b_value), tl.where(mask, a_index, b_index)
180
+
181
+
182
+ @triton.jit
183
+ def maximum_with_index(a_value, a_index, b_value, b_index):
184
+ mask = a_value > b_value
185
+ equal = a_value == b_value
186
+ if is_floating(a_value):
187
+ a_isnan = a_value != a_value
188
+ b_isnan = b_value != b_value
189
+ mask |= a_isnan & (not b_isnan)
190
+ # Consider NaNs as equal
191
+ equal |= a_isnan & b_isnan
192
+
193
+ # Prefer lowest index if values are equal
194
+ mask |= equal & (a_index < b_index)
195
+ return tl.where(mask, a_value, b_value), tl.where(mask, a_index, b_index)
196
+
197
+
198
+ @triton.jit
199
+ def min_with_index(value, index, dim):
200
+ return tl.reduce((value, index), dim, minimum_with_index)
201
+
202
+
203
+ @triton.jit
204
+ def max_with_index(value, index, dim):
205
+ return tl.reduce((value, index), dim, maximum_with_index)
206
+
207
+
208
+ @triton.jit
209
+ def exp(x, use_fast_math: tl.constexpr):
210
+ if use_fast_math:
211
+ return math.exp(x)
212
+ else:
213
+ return libdevice.exp(x)
214
+
215
+
216
+ @triton.jit
217
+ def online_softmax_reduce(lhs_max, lhs_sum, dim, use_fast_math: tl.constexpr):
218
+ out_max = max2(lhs_max, dim)
219
+ out_max_keepdim = tl.expand_dims(out_max, dim)
220
+ delta = tl.where(out_max_keepdim == float("-inf"), 0, lhs_max - out_max_keepdim)
221
+ out_sum = tl.sum(lhs_sum * exp(delta, use_fast_math), dim)
222
+ return out_max, out_sum
223
+
224
+
225
+ @triton.jit
226
+ def online_softmax_combine(lhs_max, lhs_sum, rhs_max, use_fast_math: tl.constexpr):
227
+ """
228
+ When we do combine, we assume lhs is the accumulator and rhs is the next
229
+ block of data.
230
+ Then rhs_sum is always 1. With that assumption, we can save some registers
231
+ and computation.
232
+ """
233
+ out_max = maximum(lhs_max, rhs_max)
234
+
235
+ lhs_scale = tl.where(
236
+ out_max == float("-inf"), 1.0, exp(lhs_max - out_max, use_fast_math)
237
+ )
238
+ rhs_scale = tl.where(
239
+ out_max == float("-inf"), 1.0, exp(rhs_max - out_max, use_fast_math)
240
+ )
241
+
242
+ # Should be
243
+ # out_sum = lhs_sum * lhs_scale + rhs_sum * rhs_scale
244
+ # but since rhs_sum is all 1, we can simplify it.
245
+ out_sum = lhs_sum * lhs_scale + rhs_scale
246
+ return out_max, out_sum
247
+
248
+
249
+ @triton.jit
250
+ def welford_reduce(value, mean, m2, weight, first_iteration):
251
+ if first_iteration:
252
+ new_weight = tl.full(weight.shape, 1, weight.dtype)
253
+ new_mean = value
254
+ new_m2 = tl.zeros_like(m2)
255
+ else:
256
+ delta = value - mean
257
+ new_weight = weight + 1
258
+ new_mean = mean + delta / new_weight
259
+ new_m2 = m2 + delta * (value - new_mean)
260
+ return new_mean, new_m2, new_weight
261
+
262
+
263
+ @triton.jit
264
+ def welford_combine(mean_1, m2_1, weight_1, mean_2, m2_2, weight_2):
265
+ delta = mean_2 - mean_1
266
+ new_weight = weight_1 + weight_2
267
+ w2_over_w = tl.where(new_weight == 0.0, 0.0, weight_2 / new_weight)
268
+ return (
269
+ mean_1 + delta * w2_over_w,
270
+ m2_1 + m2_2 + delta * delta * weight_1 * w2_over_w,
271
+ new_weight,
272
+ )
273
+
274
+
275
+ @triton.jit
276
+ def welford(mean, m2, weight, dim):
277
+ return tl.reduce((mean, m2, weight), dim, welford_combine)
278
+
279
+
280
+ @triton.jit
281
+ def device_assert_then(cond, msg, r):
282
+ tl.device_assert(cond, msg)
283
+ return r
284
+
285
+
286
+ @triton.jit
287
+ def rand_eager_kernel(seed, offset_blocks, tid: tl.tensor, VEC: tl.constexpr):
288
+ inv = 1.0 / 4294967296.0
289
+ half = inv * 0.5
290
+
291
+ tid_u64 = tid.to(tl.uint64)
292
+
293
+ subseq = tid_u64 // VEC
294
+ which4 = (tid_u64 % VEC) // 4
295
+ lane = tid_u64 % 4
296
+
297
+ offblk = offset_blocks.to(tl.uint64) + which4
298
+
299
+ u0, u1, u2, u3 = tl.philox(
300
+ seed,
301
+ (offblk & 0xFFFFFFFF).to(tl.uint32),
302
+ ((offblk >> 32) & 0xFFFFFFFF).to(tl.uint32),
303
+ (subseq & 0xFFFFFFFF).to(tl.uint32),
304
+ ((subseq >> 32) & 0xFFFFFFFF).to(tl.uint32),
305
+ )
306
+
307
+ v01 = tl.where(lane == 0, u0, u1)
308
+ v23 = tl.where(lane == 2, u2, u3)
309
+ rand_int = tl.where((lane == 0) | (lane == 1), v01, v23)
310
+
311
+ return 1.0 - (rand_int.to(tl.float32) * inv + half)
312
+
313
+
314
+ @triton.jit
315
+ def randint64(seed, offset, low, high):
316
+ r0, r1, _r2, _r3 = tl.randint4x(seed, offset)
317
+ r0 = r0.to(tl.uint64)
318
+ r1 = r1.to(tl.uint64)
319
+ result = r0 | (r1 << 32)
320
+ size = high - low
321
+ result = result % size.to(tl.uint64)
322
+ result = result.to(tl.int64) + low
323
+ return result
324
+
325
+
326
+ @triton.jit
327
+ def _any_combine(a, b):
328
+ return a | b
329
+
330
+
331
+ @triton.jit
332
+ def any(a, dim):
333
+ return tl.reduce(a, dim, _any_combine)
334
+
335
+
336
+ @triton.jit
337
+ def bucketize_binary_search(
338
+ values: tl.tensor,
339
+ boundaries_ptr: tl.tensor,
340
+ BOUNDARIES_SIZE: int,
341
+ BOUNDARIES_UNDERLYING_NUMEL: int,
342
+ BOUNDARIES_STRIDE: int,
343
+ boundary_indices: tl.tensor,
344
+ indexing_dtype: tl.dtype,
345
+ right: "bool", # triton can't handle the unquoted bool annotation
346
+ sorter_ptr: tl.tensor,
347
+ SORTER_STRIDE: int,
348
+ sorter_indices: tl.tensor,
349
+ ):
350
+ """
351
+ See [Note: Inductor bucketize op]
352
+
353
+ Inputs:
354
+ -------
355
+ values: the values to bucketize.
356
+ boundaries_ptr: a pointer to the beginning of the boundaries tensor, in 1-D.
357
+ BOUNDARIES_SIZE: the length of the last dimension of the boundaries tensor (i.e. one
358
+ individual set of boundaries).
359
+ BOUNDARIES_UNDERLYING_NUMEL: the length of the boundaries tensor, in 1-D, ignoring
360
+ any striding.
361
+ BOUNDARIES_STRIDE: the stride of the last dimension of the boundaries tensor
362
+ boundary_indices: a tensor of the same size as "values"; each element is an index
363
+ into a 1-D, un-strided boundaries tensor, pointing to the first element in the set
364
+ of boundaries used for that value.
365
+ indexing_dtype: the dtype used for indexing into the boundaries tensor, and the
366
+ return dtype.
367
+ right: if true, use boundary intervals closed on the left; otherwise use intervals
368
+ closed on the right.
369
+ sorter_ptr: an optional pointer to a sorter tensor of the same shape as boundaries,
370
+ but potentially different striding. If present, this allows us to treat boundaries
371
+ as sorted even if the elements of boundaries are unsorted.
372
+ SORTER_STRIDE: must be present if sorter_ptr is non-None; the stride of the last
373
+ dimension of the sorter tensor.
374
+ sorter_indices: must be present if sorter_ptr is non-None; see "boundary_indices".
375
+ BLOCK_SHAPE: the shape of the data block being processed.
376
+ """
377
+
378
+ low = tl.zeros(values.shape, dtype=indexing_dtype)
379
+ high = tl.full(values.shape, BOUNDARIES_SIZE, dtype=indexing_dtype)
380
+
381
+ full_range = BOUNDARIES_SIZE + 1
382
+ while full_range > 1:
383
+ mid = (high + low) // 2
384
+ mask = (
385
+ (mid * BOUNDARIES_STRIDE + boundary_indices) < BOUNDARIES_UNDERLYING_NUMEL
386
+ ).logical_and(mid < BOUNDARIES_SIZE)
387
+ mid_indices = (
388
+ mid
389
+ if sorter_ptr is None or SORTER_STRIDE is None
390
+ else tl.load(
391
+ sorter_ptr + sorter_indices + SORTER_STRIDE * mid,
392
+ mask=mask,
393
+ other=0,
394
+ )
395
+ )
396
+
397
+ bucket_upper_bound = tl.load(
398
+ boundaries_ptr + boundary_indices + BOUNDARIES_STRIDE * mid_indices,
399
+ mask=mask,
400
+ other=0,
401
+ )
402
+ if right:
403
+ is_above = values >= bucket_upper_bound
404
+ else:
405
+ is_above = values > bucket_upper_bound
406
+
407
+ if is_floating(values):
408
+ is_above = is_above | (values != values)
409
+
410
+ low = tl.where(is_above & mask, mid + 1, low)
411
+ high = tl.where(is_above, high, mid)
412
+
413
+ full_range = (full_range + 1) // 2
414
+
415
+ return low
416
+
417
+
418
+ @triton.jit
419
+ def pack_value_flag(
420
+ value,
421
+ flag,
422
+ DTYPE_VALUE_AS_UINT: tl.constexpr,
423
+ DTYPE_PACK: tl.constexpr,
424
+ ):
425
+ # Workaround for triton bug, tensor.to doesn't unwrap constexpr values
426
+ DTYPE_VALUE_AS_UINT = tl.core._unwrap_if_constexpr(DTYPE_VALUE_AS_UINT)
427
+ bitwidth = DTYPE_VALUE_AS_UINT.primitive_bitwidth
428
+ uv = value.to(DTYPE_VALUE_AS_UINT, bitcast=True).to(DTYPE_PACK)
429
+ return flag.to(DTYPE_PACK) | (uv << bitwidth)
430
+
431
+
432
+ @triton.jit
433
+ def unpack_value(
434
+ pack,
435
+ DTYPE_VALUE,
436
+ DTYPE_VALUE_AS_UINT,
437
+ ):
438
+ # Workaround for triton bug, tensor.to doesn't unwrap constexpr values
439
+ DTYPE_VALUE = tl.core._unwrap_if_constexpr(DTYPE_VALUE)
440
+ DTYPE_VALUE_AS_UINT = tl.core._unwrap_if_constexpr(DTYPE_VALUE_AS_UINT)
441
+ bitwidth = DTYPE_VALUE_AS_UINT.primitive_bitwidth
442
+ value_uint = (pack >> bitwidth).to(DTYPE_VALUE_AS_UINT)
443
+ return value_uint.to(DTYPE_VALUE, bitcast=True)
444
+
445
+
446
+ @triton.jit
447
+ def unpack_flag(pack, DTYPE_FLAG):
448
+ return pack.to(DTYPE_FLAG)
449
+
450
+
451
+ @triton.jit
452
+ def exclusive_scan_decoupled_lookback(
453
+ scratch_base,
454
+ block_value,
455
+ index,
456
+ combine_fn,
457
+ DTYPE_VALUE_AS_UINT: tl.constexpr,
458
+ DTYPE_PACK: tl.constexpr,
459
+ ):
460
+ """Compute exclusive scan of a scalar value between blocks
461
+
462
+ Ref: https://research.nvidia.com/publication/2016-03_single-pass-parallel-prefix-scan-decoupled-look-back
463
+
464
+ scratch_base: Pointer to scratch space in global memory
465
+ block_value: Scalar value for this block
466
+ index: Scalar index of this block relative to the current scan
467
+ combine_fn: Function ``(value, value) -> value`` which is scanned over
468
+ DTYPE_VALUE_AS_UINT: A tl.uint{n} type equal in size to ``block_value``
469
+ DTYPE_PACK: Unsigned type twice the width of block_value
470
+
471
+ NOTE: This function is limited to values which are 32-bits or less because
472
+ we need to pack (value, flag) into a single unsigned int.
473
+ """
474
+ # Publish block sum so subsequent blocks don't get stuck waiting for us
475
+ DTYPE_VALUE = block_value.dtype
476
+ pack = pack_value_flag(
477
+ block_value,
478
+ tl.full(block_value.shape, 1, DTYPE_VALUE_AS_UINT),
479
+ DTYPE_VALUE_AS_UINT,
480
+ DTYPE_PACK,
481
+ )
482
+ if index > 0:
483
+ tl.atomic_xchg(scratch_base + index, pack, sem="relaxed")
484
+
485
+ # Calculate exclusive prefix scan
486
+ exclusive_prefix = tl.zeros([], DTYPE_VALUE)
487
+ prefix_valid = False
488
+ test_target = index - 1
489
+ while test_target >= 0:
490
+ # tl.atomic_load
491
+ flag = tl.full([], 0, DTYPE_VALUE_AS_UINT)
492
+ while flag == 0:
493
+ pack = tl.atomic_add(scratch_base + test_target, 0, sem="relaxed")
494
+ flag = unpack_flag(pack, DTYPE_VALUE_AS_UINT)
495
+
496
+ value = unpack_value(pack, DTYPE_VALUE, DTYPE_VALUE_AS_UINT)
497
+ if prefix_valid:
498
+ exclusive_prefix = combine_fn(value, exclusive_prefix)
499
+ else:
500
+ exclusive_prefix = value
501
+ prefix_valid = True
502
+
503
+ if flag == 2:
504
+ test_target = -1
505
+ else:
506
+ test_target = test_target - 1
507
+
508
+ # Make inclusive block sum visible to other blocks
509
+ if prefix_valid:
510
+ inclusive_prefix = combine_fn(exclusive_prefix, block_value)
511
+ else:
512
+ inclusive_prefix = block_value
513
+ pack = pack_value_flag(
514
+ inclusive_prefix,
515
+ tl.full([], 2, DTYPE_VALUE_AS_UINT),
516
+ DTYPE_VALUE_AS_UINT,
517
+ DTYPE_PACK,
518
+ )
519
+ tl.atomic_xchg(scratch_base + index, pack, sem="relaxed")
520
+ return exclusive_prefix
521
+
522
+
523
+ @triton.jit
524
+ def exclusive_scan_decoupled_lookback_64(scratch_base, block_value, index, combine_fn):
525
+ """Compute exclusive scan of a scalar value between blocks
526
+
527
+ Ref: https://research.nvidia.com/publication/2016-03_single-pass-parallel-prefix-scan-decoupled-look-back
528
+
529
+ scratch_base: Pointer to scratch space in global memory
530
+ block_value: Scalar value for this block, must be 64-bits wide
531
+ index: Scalar index of this block relative to the current scan
532
+ combine_fn: Function ``(value, value) -> value`` which is scanned over
533
+ init: Scalar value equal to the identity of combine_fn
534
+ """
535
+ # Publish block sum so subsequent blocks don't get stuck waiting for us
536
+ if index > 0:
537
+ block_value_u64 = block_value.to(tl.uint64, bitcast=True)
538
+ tl.store(scratch_base + 3 * index + 1, block_value_u64)
539
+ tl.debug_barrier()
540
+ flag_one = tl.full([], 1, tl.uint64)
541
+ tl.atomic_xchg(scratch_base + 3 * index + 0, flag_one, sem="release")
542
+
543
+ # Calculate exclusive prefix scan
544
+ exclusive_prefix = tl.zeros([], block_value.dtype)
545
+ prefix_valid = False
546
+ test_target = index - 1
547
+ while test_target >= 0:
548
+ flag = tl.full([], 0, tl.uint64)
549
+ while flag == 0:
550
+ flag = tl.atomic_add(scratch_base + 3 * test_target + 0, 0, sem="acquire")
551
+
552
+ value_u64 = tl.load(scratch_base + 3 * test_target + flag.to(tl.int32))
553
+ value = value_u64.to(block_value.dtype, bitcast=True)
554
+ if prefix_valid:
555
+ exclusive_prefix = combine_fn(value, exclusive_prefix)
556
+ else:
557
+ exclusive_prefix = value
558
+ prefix_valid = True
559
+
560
+ if flag == 2:
561
+ test_target = tl.full([], -1, index.dtype) # Match the original type
562
+ else:
563
+ test_target = test_target - 1
564
+
565
+ # Make inclusive block sum visible to other blocks
566
+ if prefix_valid:
567
+ inclusive_prefix = combine_fn(exclusive_prefix, block_value)
568
+ else:
569
+ inclusive_prefix = block_value
570
+ inclusive_prefix_u64 = inclusive_prefix.to(tl.uint64, bitcast=True)
571
+ tl.store(scratch_base + 3 * index + 2, inclusive_prefix_u64)
572
+ tl.debug_barrier()
573
+ flag_two = tl.full([], 2, tl.uint64)
574
+ tl.atomic_xchg(scratch_base + 3 * index + 0, flag_two, sem="release")
575
+
576
+ return exclusive_prefix
577
+
578
+
579
+ @triton.jit
580
+ def frexp(x):
581
+ # TODO(isuruf): use inline_asm_elementwise here
582
+ y = libdevice.ilogb(x) + 1
583
+ exponent = tl.where(x == 0, 0, y)
584
+ mantissa = tl.where(x == 0, 0, libdevice.ldexp(x, -y))
585
+ return mantissa, exponent
586
+
587
+
588
+ @triton.jit
589
+ def _compare_and_swap_with_index(
590
+ x,
591
+ idxs,
592
+ rnumel,
593
+ flip,
594
+ i: tl.constexpr,
595
+ n_dims: tl.constexpr,
596
+ stable: tl.constexpr,
597
+ descending: tl.constexpr,
598
+ ):
599
+ n_outer: tl.constexpr = x.numel >> n_dims
600
+ shape: tl.constexpr = [n_outer * 2**i, 2, 2 ** (n_dims - i - 1)]
601
+
602
+ idtype = tl.core.get_int_dtype(bitwidth=x.dtype.primitive_bitwidth, signed=True)
603
+
604
+ y = tl.reshape(x, shape)
605
+ iy = y.to(idtype, bitcast=True)
606
+ # slice left/right with 'stride' 2**(n_dims - i - 1)
607
+ right_mask = tl.arange(0, 2)[None, :, None].to(idtype)
608
+ left_mask = (1 - right_mask).to(idtype)
609
+ ileft = tl.broadcast_to(tl.sum(iy * left_mask, 1).to(idtype)[:, None, :], shape)
610
+ iright = tl.broadcast_to(tl.sum(iy * right_mask, 1).to(idtype)[:, None, :], shape)
611
+ ileft = tl.reshape(ileft, x.shape)
612
+ iright = tl.reshape(iright, x.shape)
613
+ left = ileft.to(x.dtype, bitcast=True)
614
+ right = iright.to(x.dtype, bitcast=True)
615
+
616
+ # idx
617
+ y_idx = tl.reshape(idxs, shape)
618
+ left_idx = tl.broadcast_to(
619
+ tl.sum(y_idx * left_mask.to(y_idx.dtype), 1)[:, None, :], shape
620
+ )
621
+ right_idx = tl.broadcast_to(
622
+ tl.sum(y_idx * right_mask.to(y_idx.dtype), 1)[:, None, :], shape
623
+ )
624
+ left_idx = tl.reshape(left_idx, x.shape)
625
+ right_idx = tl.reshape(right_idx, x.shape)
626
+
627
+ # valid
628
+ if rnumel is None:
629
+ left_valid_mask = tl.full(x.shape, True, tl.int1)
630
+ right_valid_mask = tl.full(x.shape, True, tl.int1)
631
+ else:
632
+ left_valid_mask = left_idx < rnumel
633
+ right_valid_mask = right_idx < rnumel
634
+
635
+ # actual compare-and-swap
636
+ ix = x.to(idtype, bitcast=True)
637
+
638
+ # sort treats nan as having the higher value. comparisons with nan always return False.
639
+ # to align with sort semantics, we need to update descending to check if right_isnan,
640
+ # and ascending to check if left_isnan.
641
+ left_isnan = left != left
642
+ right_isnan = right != right
643
+
644
+ if descending:
645
+ cond = left < right
646
+ if is_floating(left):
647
+ if not stable:
648
+ cond = cond | right_isnan
649
+ else:
650
+ cond = cond | (right_isnan & (~left_isnan))
651
+
652
+ else:
653
+ cond = left > right
654
+ if is_floating(left):
655
+ if not stable:
656
+ cond = cond | left_isnan
657
+ else:
658
+ cond = cond | (left_isnan & (~right_isnan))
659
+
660
+ if stable:
661
+ # When stable sorting, tie break by index
662
+ eq = left == right
663
+ if is_floating(left):
664
+ eq = eq | (left_isnan & right_isnan)
665
+ cond = cond | (eq & (left_idx > right_idx))
666
+
667
+ cond = (right_valid_mask > left_valid_mask) | (
668
+ (right_valid_mask == left_valid_mask) & cond
669
+ )
670
+ cond = (cond ^ flip).to(tl.int1)
671
+ ret = ix ^ tl.where(cond, ileft ^ iright, tl.zeros_like(ix))
672
+ new_idxs = idxs ^ tl.where(cond, left_idx ^ right_idx, tl.zeros_like(idxs))
673
+
674
+ return ret.to(x.dtype, bitcast=True), new_idxs
675
+
676
+
677
+ @triton.jit
678
+ def _bitonic_merge_with_index(
679
+ x,
680
+ idxs,
681
+ rnumel,
682
+ stage: tl.constexpr,
683
+ alternating: tl.constexpr,
684
+ n_dims: tl.constexpr,
685
+ stable: tl.constexpr,
686
+ descending: tl.constexpr,
687
+ ):
688
+ n_outer: tl.constexpr = x.numel >> n_dims
689
+ tl.static_assert(stage <= n_dims)
690
+ # flip denotes whether to re-arrange sub-sequences of elements in ascending or
691
+ # descending order.
692
+ # if flip = 00000000... then all elements will be re-arranged ascendingly at this stage
693
+ # if flip = 00110011... then all the elements will be re-arranged alternatingly (with
694
+ # a stride of 2) at this stage
695
+ if alternating:
696
+ shape: tl.constexpr = [n_outer * 2 ** (n_dims - 1 - stage), 2, 2**stage]
697
+ flip = tl.reshape(
698
+ tl.broadcast_to(tl.arange(0, 2)[None, :, None], shape), x.shape
699
+ )
700
+ else:
701
+ flip = False
702
+ # perform `stage` rounds of `compare-and-swap`
703
+ for i in tl.static_range(stage):
704
+ x, idxs = _compare_and_swap_with_index(
705
+ x, idxs, rnumel, flip, i + (n_dims - stage), n_dims, stable, descending
706
+ )
707
+ return x, idxs
708
+
709
+
710
+ @triton.jit
711
+ def sort_with_index(
712
+ x, # value
713
+ idxs, # index
714
+ rnumel, # number of elements
715
+ dim: tl.constexpr = None,
716
+ stable: tl.constexpr = tl.constexpr(False),
717
+ descending: tl.constexpr = tl.constexpr(False),
718
+ ):
719
+ x, idxs = tl.broadcast(x, idxs)
720
+ # handle default dimension or check that it is the most minor dim
721
+ _dim: tl.constexpr = len(x.shape) - 1 if dim is None else dim
722
+ tl.static_assert(
723
+ _dim == len(x.shape) - 1, "only minor dimension is currently supported"
724
+ )
725
+ # iteratively run bitonic merge-sort steps
726
+ n_dims: tl.constexpr = _log2(x.shape[_dim])
727
+
728
+ for i in tl.static_range(1, n_dims + 1):
729
+ x, idxs = _bitonic_merge_with_index(
730
+ x,
731
+ idxs,
732
+ rnumel,
733
+ i,
734
+ alternating=i < n_dims,
735
+ n_dims=n_dims,
736
+ stable=stable,
737
+ descending=descending,
738
+ )
739
+ return x, idxs
740
+
741
+
742
+ @triton.jit
743
+ def select_one(x, mask, dim, keep_dims=False):
744
+ idtype = tl.core.get_int_dtype(x.dtype.primitive_bitwidth, signed=False)
745
+ ix = x.to(idtype, bitcast=True)
746
+ iy = tl.sum(ix * mask, dim, keep_dims=keep_dims)
747
+ return iy.to(idtype).to(x.dtype, bitcast=True)
748
+
749
+
750
+ @triton.jit
751
+ def x_grid_barrier(sem):
752
+ """
753
+ Wait for all other thread blocks in grid sharing same y/z program_id
754
+ to reach this barrier before returning.
755
+
756
+ Args:
757
+ sem: an uint32 semaphores, zero or 0x80000000 initialized. Must be unique to each y/z program ID.
758
+ """
759
+ # ensure stores before this are visible
760
+ tl.debug_barrier()
761
+
762
+ one_i32 = 1
763
+ one_u32 = one_i32.to(tl.uint32) # type: ignore[attr-defined]
764
+ expected = tl.num_programs(0).to(tl.uint32)
765
+ if tl.program_id(0) == 0:
766
+ nb = 0x80000000 - (expected - one_u32)
767
+ else:
768
+ nb = one_u32
769
+
770
+ old_arrive = tl.atomic_add(sem, nb, sem="release")
771
+
772
+ bar_flipped = False
773
+ while not bar_flipped:
774
+ # want a `ld.acquire.gpu.u32 $0,[$1];` but Triton doesn't have it
775
+ current_arrive = tl.atomic_add(sem, 0, sem="acquire")
776
+ # current_arrive = tl.load(sem, volatile=True)
777
+ bar_flipped = ((old_arrive ^ current_arrive) & 0x80000000) != 0
778
+
779
+ # TODO(jansel): is this needed?
780
+ tl.debug_barrier()
781
+
782
+
783
+ def triton_builtin(f: Callable[..., _T]) -> Callable[..., _T]:
784
+ """
785
+ Decorator to mark a function as a Triton built-in function. These functions
786
+ are evaluated at compile time.
787
+
788
+ Args:
789
+ f (function): The function to be marked as a Triton built-in.
790
+
791
+ Returns:
792
+ function: The same function, marked as a Triton built-in.
793
+ """
794
+ if builtins_use_semantic_kwarg:
795
+ # support Triton before and after https://github.com/triton-lang/triton/pull/7054
796
+ # and after https://github.com/triton-lang/triton/pull/7239
797
+ def wrapper(*args, _semantic, **kwargs):
798
+ kwargs["_builder"] = _semantic
799
+ return f(*args, **kwargs)
800
+ else:
801
+ wrapper = f # type: ignore[assignment]
802
+
803
+ wrapper.__triton_builtin__ = True # type: ignore[attr-defined]
804
+ return wrapper
805
+
806
+
807
+ @triton_builtin
808
+ def constexpr_next_power_of_2(
809
+ n: tl.constexpr, *, _builder: object = None
810
+ ) -> tl.constexpr:
811
+ """
812
+ A version triton.next_power_of_two that can be used within a kernel on constants.
813
+ """
814
+ assert isinstance(n, tl.constexpr)
815
+ return tl.constexpr(triton.next_power_of_2(n.value))
816
+
817
+
818
+ @triton_builtin
819
+ def if_mask(mask: Any, val, *, _builder: object = None) -> tl.constexpr:
820
+ """
821
+ Work around triton compile error: `ValueError: `other` cannot be provided without `mask``
822
+ A compile-time to check to return either `val` or `None` depending on the value of mask.
823
+ """
824
+ if isinstance(mask, tl.constexpr) and mask.value is None:
825
+ return tl.constexpr(None)
826
+ return val
827
+
828
+
829
+ @triton.jit
830
+ def inline_asm_pack(x, pack: tl.constexpr):
831
+ """Ravel to 1D and pad (via join with zeros) so numel is divisible by pack."""
832
+ result = x.ravel()
833
+ # Only pad when the block size is smaller than pack. When block >= pack
834
+ # the numel is already divisible by pack (both are powers of 2).
835
+ n_pad: tl.constexpr = _log2(pack) - _log2(result.numel)
836
+ for _ in tl.static_range(n_pad):
837
+ result = tl.reshape(
838
+ tl.join(result, tl.zeros_like(result)), (result.shape[0] * 2,)
839
+ )
840
+ return result
841
+
842
+
843
+ @triton.jit
844
+ def inline_asm_unpack(x, orig, pack: tl.constexpr):
845
+ """Unpad and reshape back to orig's shape."""
846
+ result = x
847
+ n_pad: tl.constexpr = _log2(pack) - _log2(orig.numel)
848
+ for _ in tl.static_range(n_pad):
849
+ result, _ = tl.split(tl.reshape(result, (result.shape[0] // 2, 2)))
850
+ return tl.reshape(result, orig.shape)
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_heuristics.py ADDED
The diff for this file is too large to render. See raw diff
 
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/runtime/triton_lazy_compile.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Lazy Triton kernel compilation for C++ wrapper.
3
+
4
+ This module provides functionality to compile and autotune Triton kernels at runtime
5
+ when the C++ wrapper is used without autotune_at_compile_time.
6
+
7
+ The workflow is:
8
+ 1. At model initialization: Call start_kernel_compile() for all kernels to start
9
+ parallel compilation using multi-process async_compile
10
+ 2. At kernel execution time: Call run_triton_kernel_with_autotune() which waits
11
+ for the specific kernel to be ready, then runs it with autotuning
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import dataclasses
17
+ import logging
18
+ import re
19
+ from typing import Any
20
+
21
+ from .triton_heuristics import CachingAutotuner
22
+
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclasses.dataclass
28
+ class TritonKernelCompileResult:
29
+ cubin_path: str
30
+ mangled_name: str
31
+ num_warps: int
32
+ shared_mem: int
33
+ xblock: int
34
+ yblock: int
35
+ zblock: int
36
+ r0block: int
37
+ rsplit: int
38
+ rsplit_size: int
39
+ config_index: int | None
40
+ global_scratch: int | None
41
+ profile_scratch: int | None
42
+
43
+
44
+ _async_compile: Any = None
45
+
46
+
47
+ def _get_async_compile() -> Any:
48
+ """Get or create the shared AsyncCompile instance."""
49
+ global _async_compile
50
+ if _async_compile is None:
51
+ from torch._inductor.async_compile import AsyncCompile
52
+
53
+ _async_compile = AsyncCompile()
54
+ return _async_compile
55
+
56
+
57
+ def _wrap_tma_args(args: list[Any], kernel_fn: CachingAutotuner) -> list[Any]:
58
+ """Wrap tensor args with TMA descriptors where the signature requires them."""
59
+ signature = kernel_fn.triton_meta.get("signature", {})
60
+ sig_items = list(signature.items())
61
+
62
+ # Track args index separately from sig_items index since the signature
63
+ # may include constexpr entries that are not present in args.
64
+ tma_indices = []
65
+ arg_idx = 0
66
+ for name, sig_type in sig_items:
67
+ if isinstance(sig_type, str) and sig_type == "constexpr":
68
+ continue
69
+ if isinstance(sig_type, str) and sig_type == "nvTmaDesc":
70
+ raise RuntimeError(
71
+ f"nvTmaDesc (experimental TMA API) is not supported in lazy compile "
72
+ f"for arg '{name}'. Use the stable tensordesc API instead."
73
+ )
74
+ if isinstance(sig_type, str) and sig_type.startswith("tensordesc<"):
75
+ tma_indices.append((arg_idx, name, sig_type))
76
+ arg_idx += 1
77
+
78
+ if not tma_indices:
79
+ return args
80
+
81
+ from triton.tools.tensor_descriptor import TensorDescriptor
82
+
83
+ wrapped = list(args)
84
+ for arg_idx, name, sig_type in tma_indices:
85
+ if arg_idx >= len(wrapped):
86
+ raise RuntimeError(
87
+ f"TMA arg index {arg_idx} for '{name}' exceeds arg count {len(wrapped)}"
88
+ )
89
+ tensor = wrapped[arg_idx]
90
+ # Parse block_shape from tensordesc<dtype[dim0, dim1, ...]>
91
+ match = re.match(r"tensordesc<[^[]*\[([^\]]*)\]", sig_type)
92
+ if match:
93
+ block_shape = [int(x.strip()) for x in match.group(1).split(",")]
94
+ wrapped[arg_idx] = TensorDescriptor.from_tensor(tensor, block_shape)
95
+
96
+ return wrapped
97
+
98
+
99
+ def start_kernel_compile(
100
+ pending_kernels: dict[str, Any], kernel_name: str, kernel_source: str
101
+ ) -> None:
102
+ """
103
+ This function is called from C++ at model initialization time for each kernel.
104
+ It starts the compilation in a background process but does NOT wait for it.
105
+ The actual kernel execution happens later in run_triton_kernel_with_autotune().
106
+
107
+ The pending_kernels dict is per-module, created in C++ and passed through
108
+ to avoid global state collisions across compiled modules.
109
+ """
110
+ if kernel_name in pending_kernels:
111
+ return
112
+
113
+ async_compile = _get_async_compile() # noqa: F841 (used by eval below)
114
+
115
+ # Evaluate the kernel source to get the Future or CachingAutotuner
116
+ # The kernel_source is like: async_compile.triton('name', '''...''', ...)
117
+ kernel_obj = eval(kernel_source.strip()) # noqa: S307
118
+
119
+ pending_kernels[kernel_name] = kernel_obj
120
+
121
+
122
+ def run_triton_kernel_with_autotune(
123
+ pending_kernels: dict[str, Any],
124
+ kernel_name: str,
125
+ stream: Any,
126
+ args: list[Any],
127
+ ) -> TritonKernelCompileResult:
128
+ """
129
+ Run a Triton kernel with full autotuning using actual tensor arguments.
130
+ """
131
+ from torch._inductor.codecache import CodeCacheFuture, CudaKernelParamCache
132
+ from torch._inductor.runtime.triton_heuristics import config_to_dict
133
+
134
+ if kernel_name not in pending_kernels:
135
+ raise RuntimeError(f"Kernel {kernel_name} not found in pending kernels.")
136
+ kernel_obj = pending_kernels[kernel_name]
137
+
138
+ if isinstance(kernel_obj, CodeCacheFuture):
139
+ kernel_fn = kernel_obj.result()
140
+ elif isinstance(kernel_obj, CachingAutotuner):
141
+ kernel_fn = kernel_obj
142
+ else:
143
+ raise RuntimeError(f"Unexpected kernel object type: {type(kernel_obj)}")
144
+
145
+ assert isinstance(kernel_fn, CachingAutotuner)
146
+
147
+ inductor_meta = kernel_fn.inductor_meta
148
+ inductor_meta["store_cubin"] = True
149
+
150
+ # For TMA kernels, wrap tensor args with TMA descriptors
151
+ args = _wrap_tma_args(args, kernel_fn)
152
+
153
+ # Run the kernel with the provided arguments
154
+ # This will trigger autotuning if there are multiple configs
155
+ kernel_fn.run(*args, stream=stream)
156
+ if not kernel_fn.launchers:
157
+ raise RuntimeError("Kernel run did not produce any launchers")
158
+ launcher = kernel_fn.launchers[0]
159
+
160
+ cached_params: dict[str, Any] | None = CudaKernelParamCache.get(kernel_name)
161
+ if cached_params is None:
162
+ raise RuntimeError(f"Failed to get cached params for kernel {kernel_name}")
163
+
164
+ from torch._inductor.codecache import get_cpp_wrapper_cubin_path_name
165
+
166
+ cubin_path_name = get_cpp_wrapper_cubin_path_name()
167
+ for key_name in (cubin_path_name, "mangled_name", "num_warps", "shared_mem"):
168
+ if key_name not in cached_params:
169
+ raise RuntimeError(
170
+ f"{key_name} not found in cached params for {kernel_name}"
171
+ )
172
+ cubin_path = cached_params[cubin_path_name]
173
+ mangled_name = cached_params["mangled_name"]
174
+ num_warps = cached_params["num_warps"]
175
+ shared_mem = cached_params["shared_mem"]
176
+
177
+ config = config_to_dict(launcher.config) if launcher.config else {}
178
+
179
+ # For combo/foreach kernels, the autotuned config may have empty kwargs
180
+ # (e.g., the foreach heuristic only tunes num_warps, not XBLOCK).
181
+ # In that case, use the default_config from combo_grid_meta
182
+ combo_grid_meta = inductor_meta.get("combo_grid_meta") if inductor_meta else None
183
+ default_config = combo_grid_meta.get("default_config") if combo_grid_meta else None
184
+ if default_config:
185
+ config = {**default_config, **config}
186
+
187
+ xblock = config.get("XBLOCK", 128)
188
+ yblock = config.get("YBLOCK", 1)
189
+ zblock = config.get("ZBLOCK", 1)
190
+ r0block = config.get("R0_BLOCK", 1)
191
+ rsplit = config.get("RSPLIT", 1)
192
+ rsplit_size = config.get("RSPLIT_SIZE", 1)
193
+
194
+ config_index = None
195
+ grid_type = inductor_meta.get("grid_type") if inductor_meta else None
196
+ if grid_type == "PrecomputedGrid" and inductor_meta:
197
+ # PrecomputedGrid selects one of precomputed_grids. We use config_index
198
+ # to remember which grid is chosen.
199
+ precomputed_grids = inductor_meta.get("precomputed_grids", [])
200
+ for idx, entry in enumerate(precomputed_grids):
201
+ entry_config = entry.get("config", {})
202
+ if all(config.get(k) == v for k, v in entry_config.items()):
203
+ config_index = idx
204
+ break
205
+
206
+ global_scratch: int | None = cached_params.get("global_scratch")
207
+ profile_scratch: int | None = cached_params.get("profile_scratch")
208
+
209
+ log.debug(
210
+ "Successfully autotuned Triton kernel: cubin_path=%s, mangled_name=%s, "
211
+ "num_warps=%d, shared_mem=%d, xblock=%d, yblock=%d, zblock=%d, r0block=%d, "
212
+ "rsplit=%d, rsplit_size=%d, config_index=%s, global_scratch=%s, profile_scratch=%s",
213
+ cubin_path,
214
+ mangled_name,
215
+ num_warps,
216
+ shared_mem,
217
+ xblock,
218
+ yblock,
219
+ zblock,
220
+ r0block,
221
+ rsplit,
222
+ rsplit_size,
223
+ config_index,
224
+ global_scratch,
225
+ profile_scratch,
226
+ )
227
+
228
+ result = TritonKernelCompileResult(
229
+ cubin_path=cubin_path,
230
+ mangled_name=mangled_name,
231
+ num_warps=num_warps,
232
+ shared_mem=shared_mem,
233
+ xblock=xblock,
234
+ yblock=yblock,
235
+ zblock=zblock,
236
+ r0block=r0block,
237
+ rsplit=rsplit,
238
+ rsplit_size=rsplit_size,
239
+ config_index=config_index,
240
+ global_scratch=global_scratch,
241
+ profile_scratch=profile_scratch,
242
+ )
243
+ return result
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/scheduler.py ADDED
The diff for this file is too large to render. See raw diff
 
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/script.ld ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ SECTIONS {
2
+ /* By default, in LLD 16, .lrodata is placed immediately after .rodata.
3
+ * However, .lrodata can be very large in our compiled models, which leads to
4
+ * relocation out-of-range errors for relative relocations. So we place it
5
+ * after other the sections that are referenced from .text using relative
6
+ * relocations. This is the default behavior in GNU ld. */
7
+ .lrodata : { *(.lrodata) }
8
+ } INSERT AFTER .bss;
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/select_algorithm.py ADDED
The diff for this file is too large to render. See raw diff
 
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/shape_propagation.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ from collections.abc import Callable, Sequence
3
+ from typing import Protocol
4
+
5
+ import sympy
6
+
7
+ import torch
8
+
9
+ from .virtualized import OpsValue, V
10
+
11
+
12
+ BlockShapeType = Sequence[int | str] | None
13
+
14
+
15
+ class ShapeVar(Protocol):
16
+ @property
17
+ def shape(self) -> BlockShapeType: ...
18
+
19
+
20
+ ShapeArg = ShapeVar | torch.types.Number | str | OpsValue | torch.dtype
21
+
22
+ # Inputs need to be cacheable (e.g., not a CSEVar) in order for the cache to be effective
23
+ # So first decompose CSEVars -> tuple before calling this
24
+
25
+
26
+ @functools.lru_cache(None)
27
+ def get_broadcasted_shape(a: BlockShapeType, b: BlockShapeType) -> BlockShapeType:
28
+ assert isinstance(a, Sequence)
29
+ assert isinstance(b, Sequence)
30
+ if len(a) > len(b):
31
+ return get_broadcasted_shape(a, (*[1] * (len(a) - len(b)), *b))
32
+ elif len(a) < len(b):
33
+ b, a = a, b
34
+ return get_broadcasted_shape(a, (*[1] * (len(a) - len(b)), *b))
35
+ else:
36
+
37
+ def _get_broadcasted_dim(d1: int | str, d2: int | str) -> int | str:
38
+ if str(d1) == "1":
39
+ return d2
40
+ elif str(d2) == "1":
41
+ return d1
42
+ assert str(d1) == str(d2)
43
+ return d1
44
+
45
+ return tuple(_get_broadcasted_dim(d1, d2) for d1, d2 in zip(a, b))
46
+
47
+
48
+ def broadcast_shapes_for_args(args: Sequence[ShapeArg]) -> BlockShapeType:
49
+ result_shape: BlockShapeType = None
50
+
51
+ for arg in args:
52
+ if hasattr(arg, "shape"):
53
+ shape = arg.shape
54
+ if shape is None:
55
+ return None
56
+ elif result_shape is None:
57
+ result_shape = tuple(shape)
58
+ else:
59
+ result_shape = get_broadcasted_shape(result_shape, tuple(shape))
60
+ elif isinstance(arg, (int, float)):
61
+ if result_shape is None:
62
+ result_shape = ()
63
+ elif isinstance(arg, torch.dtype):
64
+ continue
65
+ else:
66
+ from torch._inductor.loop_body import LoopBody, LoopBodyBlock
67
+
68
+ if isinstance(arg, (LoopBodyBlock, LoopBody, OpsValue)):
69
+ # TODO: fix me
70
+ return None
71
+ raise TypeError(f"Unknown type: {type(arg)}")
72
+
73
+ return result_shape
74
+
75
+
76
+ class ShapePropagationOpsHandler:
77
+ """
78
+ Propagate shape from args to output
79
+ """
80
+
81
+ @staticmethod
82
+ def constant(value: torch.types.Number, dtype: torch.dtype) -> BlockShapeType:
83
+ # TritonKernelOverrides.constant uses tl.full with shape=[1]*ndim for all types
84
+ from torch._inductor.codegen.triton import TritonKernel
85
+
86
+ if isinstance(V.kernel, TritonKernel):
87
+ ndim = V.kernel.triton_tensor_ndim()
88
+ return tuple([1] * ndim)
89
+ else:
90
+ return ()
91
+
92
+ @staticmethod
93
+ def store_reduction(name: str, index: int, value: ShapeArg) -> None:
94
+ return None
95
+
96
+ @staticmethod
97
+ def reduction(
98
+ dtype: torch.dtype,
99
+ src_dtype: torch.dtype,
100
+ reduction_type: str,
101
+ value: ShapeArg | tuple[ShapeArg, ...],
102
+ ) -> BlockShapeType | tuple[BlockShapeType, ...]:
103
+ raise NotImplementedError
104
+
105
+ @staticmethod
106
+ def store(name: str, index: int, value: ShapeArg, mode: str | None = None) -> None:
107
+ return None
108
+
109
+ @staticmethod
110
+ def to_dtype(
111
+ value: ShapeVar,
112
+ dtype: torch.dtype,
113
+ src_dtype: torch.dtype | None = None,
114
+ use_compute_types: bool = True,
115
+ ) -> BlockShapeType:
116
+ return value.shape
117
+
118
+ @staticmethod
119
+ def dot(a: sympy.Expr, b: sympy.Expr) -> BlockShapeType:
120
+ from torch._inductor.codegen.triton import TritonKernel
121
+
122
+ assert isinstance(V.kernel, TritonKernel), "dot supports Triton only"
123
+ return ("YBLOCK", "XBLOCK")
124
+
125
+ @staticmethod
126
+ def index_expr(expr: sympy.Expr, dtype: torch.dtype) -> BlockShapeType:
127
+ # shape is implicitly embedded in expr.
128
+ return None
129
+
130
+ @staticmethod
131
+ def load_seed(name: str, offset: int) -> BlockShapeType:
132
+ return ()
133
+
134
+ @staticmethod
135
+ def indirect_indexing(
136
+ var: ShapeArg,
137
+ size: sympy.Expr | int,
138
+ check: bool = True,
139
+ wrap_neg: bool = True,
140
+ ) -> None:
141
+ return None
142
+
143
+ def __getattr__(self, name: str) -> Callable[..., BlockShapeType]:
144
+ return lambda *args, **kwargs: broadcast_shapes_for_args(args)
145
+
146
+ @staticmethod
147
+ def device_assert_async(cond: ShapeArg, msg: str) -> None:
148
+ return None
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/sizevars.py ADDED
@@ -0,0 +1,1193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mypy: allow-untyped-defs
2
+ import functools
3
+ import itertools
4
+ import logging
5
+ from collections.abc import Callable, Iterable, Sequence
6
+ from typing import Any, cast
7
+
8
+ import sympy
9
+ from sympy import Expr
10
+
11
+ from torch import SymInt
12
+ from torch.fx.experimental._size_hinting import (
13
+ _guarding_hint_or_throw_base,
14
+ _maybe_realize_expr,
15
+ _optimization_hint_base,
16
+ )
17
+ from torch.fx.experimental.symbolic_shapes import (
18
+ free_symbols,
19
+ free_unbacked_symbols,
20
+ IterateExprs,
21
+ ShapeEnv,
22
+ SymNode,
23
+ )
24
+ from torch.utils._ordered_set import OrderedSet
25
+ from torch.utils._sympy.functions import FloorDiv, Mod, ModularIndexing
26
+ from torch.utils._sympy.symbol import symbol_is_type, SymT
27
+ from torch.utils._sympy.value_ranges import IntInfinity, ValueRanges
28
+
29
+ from . import config
30
+ from .runtime.runtime_utils import is_power_of_2
31
+ from .utils import (
32
+ has_free_symbols,
33
+ sympy_index_symbol,
34
+ sympy_index_symbol_with_prefix,
35
+ sympy_subs,
36
+ VarRanges,
37
+ )
38
+ from .virtualized import V
39
+
40
+
41
+ log = logging.getLogger(__name__)
42
+
43
+ # Symbols created by CppTemplateKernel.slice_nd → parse_expr_with_index_symbols
44
+ # and are internal to C++ GEMM codegen (not tracked by ShapeEnv).
45
+ _GEMM_TEMPLATE_SYMBOL_NAMES = OrderedSet(
46
+ [
47
+ "m_start",
48
+ "m_end",
49
+ "n_start",
50
+ "n_end",
51
+ "k_start",
52
+ "k_end",
53
+ "m_size",
54
+ "n_size",
55
+ "m_offset",
56
+ "m_start_unsliced",
57
+ "m_end_unsliced",
58
+ "m_size_unsliced",
59
+ ]
60
+ )
61
+
62
+
63
+ def statically_known_true(
64
+ shape_env: ShapeEnv,
65
+ expr: sympy.Basic | bool,
66
+ axioms: tuple[sympy.Expr] | None = None,
67
+ var_to_range: tuple[tuple[sympy.Symbol, ValueRanges[Any]]] | None = None,
68
+ ) -> bool:
69
+ if expr in (True, False):
70
+ return bool(expr)
71
+
72
+ try:
73
+ simplified = shape_env._maybe_evaluate_static(
74
+ expr,
75
+ axioms=axioms,
76
+ var_to_range=var_to_range,
77
+ )
78
+ if simplified is not None:
79
+ return bool(simplified)
80
+ except Exception:
81
+ log.debug("Could not simplify %s", expr, exc_info=True)
82
+
83
+ return False
84
+
85
+
86
+ # This class is a little awkward, because ShapeEnv is doing most of the heavy
87
+ # lifting and in some cases we should be directly passing through to ShapeEnv,
88
+ # but there is some extra inductor logic that needs to be handled here
89
+ class SizeVarAllocator:
90
+ """
91
+ A class that manages symbolic size variables and their relationships.
92
+
93
+ This class works with the ShapeEnv to handle symbolic shape expressions,
94
+ simplify them, and provide utilities for guarding, checking, and evaluating
95
+ symbolic expressions. It also manages precomputed replacements and stride
96
+ calculations for tensor operations.
97
+ """
98
+
99
+ def __init__(self, shape_env=None) -> None:
100
+ super().__init__()
101
+ # Note: this can lead to bugs. Reasoning APIs depends on existing information in
102
+ # in the shape_env. For example! var_to_ranges can't be empty!
103
+ if shape_env is None:
104
+ shape_env = ShapeEnv()
105
+ self.shape_env = shape_env
106
+ self.backed_var_to_val = self.shape_env.backed_var_to_val
107
+ self.var_to_hint_override = self.shape_env.var_to_hint_override
108
+ self.replacements: dict[sympy.Symbol, Expr] = self.shape_env.replacements
109
+ self.unbacked_replacements: dict[Expr, Expr] | None = None
110
+ # Maps of dynamic sizes that have to be precomputed on the host to the kernel args.
111
+ # The basic idea is if we have some complicated sympy expression
112
+ # f(s0), we may choose to precompute it on the host and then replace
113
+ # all occurrences of that sympy expression with ps0, so that when we
114
+ # codegen we simply reference ps0 directly without repeating
115
+ # f(s0). Unlike regular size variables, ps variables cannot be
116
+ # guarded upon; so if we are asked to guard on a Sympy expression
117
+ # which potentially could have already had a precomputed replacement
118
+ # on it, we are obligated to invert the precomputed replacements
119
+ # (inv_precomputed_replacements).
120
+ self.precomputed_replacements: dict[Expr, sympy.Symbol] = {}
121
+ self.inv_precomputed_replacements: dict[sympy.Symbol, Expr] = {}
122
+ self.stride_vars = self.make_stride_vars_cache()
123
+ self.simplify_with_ranges = self.make_simplify_with_ranges_cache()
124
+ self._simplify_loops = self.make_simplify_loops_cache()
125
+
126
+ def simplify(self, expr: Expr):
127
+ return sympy.expand(expr).xreplace(self.replacements)
128
+
129
+ def make_simplify_with_ranges_cache(self) -> Callable[[Expr, VarRanges], Expr]:
130
+ """
131
+ self._simplify_with_ranges() can be expensive, cache its results
132
+ """
133
+ cache: dict[tuple[Any, ...], Expr] = {}
134
+ replacement_count = len(self.replacements)
135
+
136
+ def simplify_with_ranges(expr: Expr, var_ranges: VarRanges) -> Expr:
137
+ nonlocal replacement_count
138
+ if replacement_count != len(self.replacements):
139
+ # new replacements invalidates cached results
140
+ cache.clear()
141
+ replacement_count = len(self.replacements)
142
+ key = (expr, *var_ranges.items())
143
+ result = cache.get(key)
144
+ if result is None:
145
+ result = self._simplify_with_ranges(expr, var_ranges)
146
+ cache[key] = result
147
+ if result != expr:
148
+ cache[(result, *var_ranges.items())] = result
149
+ return result
150
+
151
+ return simplify_with_ranges
152
+
153
+ def make_simplify_loops_cache(self):
154
+ """
155
+ self._simplify_with_ranges() can be expensive, cache its results
156
+ """
157
+ cache: dict[tuple[Any, ...], Any] = {}
158
+ replacement_count = len(self.replacements)
159
+
160
+ def simplify_loops(index_vars, sizes, index_formulas):
161
+ nonlocal replacement_count
162
+ if replacement_count != len(self.replacements):
163
+ # new replacements invalidates cached results
164
+ cache.clear()
165
+ replacement_count = len(self.replacements)
166
+ key = (*index_vars, *sizes, *index_formulas)
167
+ result = cache.get(key)
168
+ if result is None:
169
+ result = self._simplify_loops_impl(index_vars, sizes, index_formulas)
170
+ cache[key] = result
171
+ return result
172
+
173
+ return simplify_loops
174
+
175
+ def _simplify_with_ranges(self, expr: Expr, var_ranges: VarRanges) -> Expr:
176
+ """
177
+ Simplify indexing expression with knowledge of the ranges of
178
+ iteration variables.
179
+ """
180
+
181
+ expr = join_dimensions(self.simplify(expr))
182
+ original_expr = expr
183
+
184
+ var_to_range = dict(self.shape_env.var_to_range)
185
+ var_to_range.update(
186
+ {
187
+ k: ValueRanges(
188
+ 0, max(0, v - 1) if not has_free_symbols([v]) else IntInfinity()
189
+ )
190
+ for k, v in var_ranges.items()
191
+ }
192
+ )
193
+ for var in expr.free_symbols:
194
+ if var not in var_to_range:
195
+ var_to_range[var] = ValueRanges(0, IntInfinity())
196
+
197
+ var_to_range_tuple = cast(
198
+ tuple[tuple[sympy.Symbol, ValueRanges[sympy.Expr]]],
199
+ tuple(var_to_range.items()),
200
+ )
201
+
202
+ axioms = []
203
+ for var, upper_bound in var_ranges.items():
204
+ axioms.append(0 <= var)
205
+ axioms.append(var < upper_bound)
206
+ axioms = tuple(axioms) + self.shape_env.get_axioms()
207
+
208
+ def statically_known(expr):
209
+ evaluated = self.shape_env._maybe_evaluate_static(
210
+ expr,
211
+ # pyrefly: ignore [bad-argument-type]
212
+ axioms=axioms,
213
+ var_to_range=var_to_range_tuple,
214
+ )
215
+ return bool(evaluated)
216
+
217
+ def remove_zero_terms(base, divisor):
218
+ """Symbols smaller than the divisor are zero"""
219
+ if not statically_known(base >= 0):
220
+ return base
221
+
222
+ for v in base.free_symbols:
223
+ if v in var_ranges:
224
+ rest = sympy.Wild("_rest", exclude=[v])
225
+ m = base.match(v + rest)
226
+ if m and v not in m[rest].free_symbols:
227
+ # v can be removed if it doesn't affect the FloorDiv.
228
+ # rest is always a multiple of gcd(rest, divisor), so
229
+ # rest % divisor is also a multiple of that gcd. The
230
+ # worst case is rest % divisor == divisor - gcd, so
231
+ # adding v is safe when v < gcd.
232
+ gcd = sympy.gcd(m[rest], divisor)
233
+ if statically_known(v < gcd):
234
+ base = m[rest]
235
+ return base
236
+
237
+ def visit_indexing_div(base, divisor):
238
+ base = remove_zero_terms(base, divisor)
239
+ if statically_known(base >= 0) and statically_known(base < divisor):
240
+ return sympy.S.Zero
241
+ # FloorDiv(ModularIndexing(b, d1, m), d2) = ModularIndexing(b, d1*d2, m//d2)
242
+ if isinstance(base, ModularIndexing) and isinstance(divisor, sympy.Integer):
243
+ b, d1, m = base.args
244
+ if m % divisor == 0:
245
+ return ModularIndexing(b, d1 * divisor, FloorDiv(m, divisor))
246
+ return FloorDiv(base, divisor)
247
+
248
+ def visit_modular_indexing(base, divisor, modulus):
249
+ base = remove_zero_terms(base, divisor)
250
+
251
+ can_remove_mod = statically_known(base >= 0) and statically_known(
252
+ base < modulus * divisor
253
+ )
254
+
255
+ if can_remove_mod:
256
+ return FloorDiv(base, divisor)
257
+ return ModularIndexing(base, divisor, modulus)
258
+
259
+ if expr.has(ModularIndexing):
260
+ expr = expr.replace(
261
+ ModularIndexing(
262
+ sympy.Wild("base", integer=True),
263
+ sympy.Wild("divisor", integer=True),
264
+ sympy.Wild("modulus", integer=True),
265
+ ),
266
+ visit_modular_indexing,
267
+ )
268
+
269
+ if expr.has(FloorDiv):
270
+ expr = expr.replace(
271
+ FloorDiv(
272
+ sympy.Wild("base", integer=True),
273
+ sympy.Wild("divisor", integer=True),
274
+ ),
275
+ visit_indexing_div,
276
+ )
277
+
278
+ if expr != original_expr:
279
+ return self._simplify_with_ranges(expr, var_ranges)
280
+ return expr
281
+
282
+ def _simplify_loops_impl(
283
+ self, index_vars: list[sympy.Symbol], sizes, index_formulas
284
+ ):
285
+ """
286
+ Try to remove as many axis from loop iterations as possible, by:
287
+ 1) removing size==1 dimensions
288
+ 2) fuse contiguous dimensions into a single loop
289
+ If channel_last = True, we will prevent the last dim fused with other dims
290
+ """
291
+ sizes = list(map(self.simplify, sizes))
292
+
293
+ strides = [
294
+ # index_formulas may contain boolean expressions (e.g. s0 < 10),
295
+ # for which "strides" don't make sense so we ignore them here.
296
+ # NOTE: These expressions may still block merging dims in the sound
297
+ # substitution test performed in can_merge_dims.
298
+ (
299
+ self.stride_vars(x, index_vars)
300
+ if isinstance(x, sympy.Expr)
301
+ else [0] * len(index_vars)
302
+ )
303
+ for x in index_formulas
304
+ ]
305
+ assert len(sizes) == len(strides[0]), (len(sizes), len(strides[0]))
306
+
307
+ for i in range(len(sizes)):
308
+ if sizes[i] == 1:
309
+ # remove dim
310
+ sizes[i] = None
311
+
312
+ def can_merge_dims(a, b):
313
+ for k in range(len(strides)):
314
+ if self.simplify(strides[k][a] * sizes[a]) == self.simplify(
315
+ strides[k][b]
316
+ ):
317
+ # approximate test passed, try sound version
318
+ va = index_vars[a]
319
+ vb = index_vars[b]
320
+ m1 = sympy_index_symbol("_merge_tester1")
321
+ m2 = sympy_index_symbol("_merge_tester2")
322
+ # NOTE: can't sub vb=0 here in case va * vb appears in the expression,
323
+ # in which case both expr1 and expr2 would be zero!
324
+ expr1 = sympy_subs(index_formulas[k], {va: m1 * sizes[a], vb: m2})
325
+ expr2 = sympy_subs(index_formulas[k], {va: 0, vb: (m1 + m2)})
326
+ if self.simplify(expr1) == self.simplify(expr2):
327
+ continue
328
+ return False
329
+ return True
330
+
331
+ changed = True
332
+ while changed:
333
+ changed = False
334
+ for i, j in itertools.product(
335
+ reversed(range(len(sizes))), reversed(range(len(sizes)))
336
+ ):
337
+ if i == j or sizes[i] is None or sizes[j] is None:
338
+ continue
339
+ if can_merge_dims(i, j):
340
+ changed = True
341
+ sizes[i] = sizes[i] * sizes[j]
342
+ sizes[j] = None
343
+
344
+ def reindex(index):
345
+ it = list(reversed(index))
346
+ new_index = []
347
+ for size in sizes:
348
+ if size is None:
349
+ new_index.append(sympy.S.Zero)
350
+ else:
351
+ new_index.append(it.pop())
352
+ assert not it
353
+ return new_index
354
+
355
+ def prune(index):
356
+ assert len(index) == len(sizes)
357
+ return [i for i, s in zip(index, sizes) if s is not None]
358
+
359
+ return [x for x in sizes if x is not None], reindex, prune
360
+
361
+ # Note - [On Statically Known]
362
+ # The statically_known_* family of functions below NEVER guard, they could return True if the
363
+ # asked questions can be answered without guarding otherwise they return False.
364
+ # Those are similar to statically_known_true in symbolic_shapes.py but operate on sympy
365
+ # expressions instead of symnodes.
366
+ def statically_known_true(self, expr: sympy.Basic | bool) -> bool:
367
+ """
368
+ Returns true if an expression is always true (symbolically or via guards),
369
+ false otherwise. Never add guards, or throw data dependent errors.
370
+ """
371
+ return statically_known_true(self.shape_env, expr)
372
+
373
+ def statically_known_equals(self, left: Expr | int, right: Expr | int) -> bool:
374
+ """
375
+ Returns a bool indicating if it is sound to optimize as if left and right are equal.
376
+ """
377
+ return self.statically_known_true(sympy.Eq(left, right)) # type: ignore[arg-type]
378
+
379
+ def statically_known_list_equals(
380
+ self, left: Sequence[Expr], right: Sequence[Expr]
381
+ ) -> bool:
382
+ """
383
+ Returns a bool indicating if it is sound to optimize as if left and right lists are equal.
384
+ """
385
+ return len(left) == len(right) and all(
386
+ self.statically_known_equals(l, r) for l, r in zip(left, right)
387
+ )
388
+
389
+ def statically_known_leq(self, left: Expr, right: Expr | int) -> bool:
390
+ """
391
+ Returns a bool indicating if it is sound to optimize as if left is less than or equal to right.
392
+ """
393
+ expr = left <= right
394
+ return self.statically_known_true(expr)
395
+
396
+ def statically_known_geq(self, left: Expr, right: Expr | int) -> bool:
397
+ """
398
+ Returns a bool indicating if it is sound to optimize as if left is greater than or equal to right.
399
+ """
400
+ expr = left >= right
401
+ return self.statically_known_true(expr)
402
+
403
+ def statically_known_lt(self, left: Expr, right: Expr | int) -> bool:
404
+ """
405
+ Returns a bool indicating if it is sound to optimize as if left is less than right.
406
+ """
407
+ expr = left < right
408
+ return self.statically_known_true(expr)
409
+
410
+ def statically_known_gt(self, left: Expr, right: Expr | int) -> bool:
411
+ """
412
+ Returns a bool indicating if it is sound to optimize as if left is greater than right.
413
+ """
414
+ expr = left > right
415
+ return self.statically_known_true(expr)
416
+
417
+ def _is_multiple_of(self, numerator: Expr, denominator: int) -> bool:
418
+ """
419
+ Structural divisibility check: returns True only if numerator is
420
+ provably a multiple of denominator. Recurses over sympy expression
421
+ structure before falling back to statically_known_true.
422
+ """
423
+ # Rule 1 — concrete value
424
+ if isinstance(numerator, (int, sympy.Integer)):
425
+ return int(numerator) % denominator == 0
426
+
427
+ # Rule 2 — product: any factor divisible → product divisible
428
+ if isinstance(numerator, sympy.Mul):
429
+ for factor in numerator.args:
430
+ if self._is_multiple_of(factor, denominator):
431
+ return True
432
+ # Also check if combined constant factors are divisible
433
+ const = 1
434
+ for factor in numerator.args:
435
+ if isinstance(factor, (int, sympy.Integer)):
436
+ const *= int(factor)
437
+ if const != 1 and const % denominator == 0:
438
+ return True
439
+
440
+ # Rule 3 — sum: all terms divisible → sum divisible
441
+ if isinstance(numerator, sympy.Add):
442
+ if all(self._is_multiple_of(term, denominator) for term in numerator.args):
443
+ return True
444
+
445
+ # Rule 4 — FloorDiv(a, b): if a is multiple of b*n
446
+ if isinstance(numerator, FloorDiv):
447
+ a, b = numerator.args
448
+ if isinstance(b, (int, sympy.Integer)):
449
+ if self._is_multiple_of(a, int(b) * denominator):
450
+ return True
451
+
452
+ # Rule 5 — Mod(a, b): Mod(a,b) = a - b*floor(a/b), so if both a and b
453
+ # are multiples of n, then Mod(a,b) is too.
454
+ if isinstance(numerator, (Mod, sympy.Mod)):
455
+ a, b = numerator.args
456
+ if self._is_multiple_of(a, denominator) and self._is_multiple_of(
457
+ b, denominator
458
+ ):
459
+ return True
460
+
461
+ # Rule 6 — axiom fallback: ask ShapeEnv
462
+ expr = sympy.Eq(Mod(numerator, denominator), 0)
463
+ return self.statically_known_true(expr)
464
+
465
+ def statically_known_multiple_of(
466
+ self, numerator: Expr, denominator: Expr | int
467
+ ) -> bool:
468
+ """
469
+ Return a bool indicating if it is sound to optimize for the numerator being a multiple of the denominator.
470
+ """
471
+ # The reason we skip compute here is to avoid the cost of trying to eval this symbolically.
472
+ # see https://github.com/sympy/sympy/issues/28200
473
+
474
+ if len(free_symbols(numerator)) > 20:
475
+ return False
476
+
477
+ if isinstance(denominator, (int, sympy.Integer)):
478
+ return self._is_multiple_of(numerator, int(denominator))
479
+
480
+ # For symbolic denominators, fall back to direct sympy check
481
+ expr = sympy.Eq(Mod(numerator, denominator), 0)
482
+ return self.statically_known_true(expr) # type: ignore[arg-type]
483
+
484
+ def statically_known_power_of_2(self, expr: Expr) -> bool:
485
+ """
486
+ Returns a bool indicating if x is known to be a power of 2.
487
+ """
488
+ return isinstance(expr, sympy.Integer) and is_power_of_2(int(expr))
489
+
490
+ # The expect/check functions require you to ALREADY KNOW that a particular
491
+ # condition holds. They are similar to expect_true in symbolic_shapes.py and
492
+ # torch.check but operates on sympy expressions instead of symnodes.
493
+ def expect_true(self, expr: Expr) -> bool:
494
+ """
495
+ Use it when you already know that expr is true or should be true and want to
496
+ ensure that guards/runtime assertions are in place to ensure this in compiled
497
+ function. Unlike check, this WON'T raise an error if expr isn't actually true.
498
+ check Note [expect_true].
499
+ """
500
+ if not self.statically_known_true(expr):
501
+ return self.shape_env.guard_or_defer_runtime_assert(
502
+ expr, "sizevars.expect_true"
503
+ )
504
+ return True
505
+
506
+ def check(self, expr: Expr) -> None:
507
+ """
508
+ Use it when you already know that expr is true or should be true and want to
509
+ ensure that guards/runtime assertions are in place to ensure this in compiled
510
+ function. Unlike expect_true, this WILL raise an error if expr isn't actually true.
511
+ check Note [expect_true].
512
+ """
513
+ expr = sympy_subs(expr, self.inv_precomputed_replacements)
514
+ assert self.expect_true(expr)
515
+
516
+ def check_equals(self, left: Expr, right: Expr) -> None:
517
+ """
518
+ check(sympy.Eq(left, right)).
519
+
520
+ """
521
+ self.check(sympy.Eq(left, right))
522
+ return left
523
+
524
+ def check_equals_and_simplify(self, left: Expr, right: Expr) -> Expr:
525
+ """
526
+ check(sympy.Eq(left, right)) and returns left after applying
527
+ inv_precomputed_replacements.
528
+ """
529
+ self.check(sympy.Eq(left, right))
530
+ return sympy_subs(left, self.inv_precomputed_replacements)
531
+
532
+ def check_leq(self, left: Expr, right: Expr) -> None:
533
+ self.check(sympy.Le(left, right))
534
+
535
+ def check_lt(self, left: Expr, right: Expr) -> None:
536
+ self.check(sympy.Lt(left, right))
537
+
538
+ # Similar to the functions guard_or_false/guard_or_true in symbolic_shapes.py
539
+ # but operates on sympy expressions instead of symnodes. see Note [guard_or_].
540
+ def guard_or_false(self, left):
541
+ import torch.fx.experimental._config as exp_config
542
+
543
+ if exp_config.backed_size_oblivious:
544
+ static_val = self.shape_env._maybe_evaluate_static(left)
545
+ if static_val is not None:
546
+ return static_val
547
+ return False
548
+ return self.evaluate_expr(left, fallback_value=False)
549
+
550
+ def guard_or_true(self, left):
551
+ import torch.fx.experimental._config as exp_config
552
+
553
+ if exp_config.backed_size_oblivious:
554
+ static_val = self.shape_env._maybe_evaluate_static(left)
555
+ if static_val is not None:
556
+ return static_val
557
+ return True
558
+ return self.evaluate_expr(left, fallback_value=True)
559
+
560
+ # The evaluate functions evaluate some symbolic sympy expression
561
+ # (NB: not necessarily an Expr) and return what the concrete result
562
+ # is, guarding on the expression being that result
563
+
564
+ # NB: write evaluate_expr(sympy.Lt(a, b)) rather than evaluate_expr(a < b)
565
+ # as this will ensure that you actually have a sympy'ified expression,
566
+ # and will prevent you from incorrectly writing evaluate_expr(a == b)
567
+ # which does the wrong thing if a or b is a sympy expression
568
+ def evaluate_expr(
569
+ self,
570
+ left: Expr | sympy.logic.boolalg.Boolean,
571
+ size_oblivious: bool = False,
572
+ fallback_value: bool | None = None,
573
+ ) -> bool:
574
+ assert isinstance(left, (Expr, sympy.logic.boolalg.Boolean)), type(left)
575
+ return self.shape_env.evaluate_expr(
576
+ sympy.sympify(left),
577
+ size_oblivious=size_oblivious,
578
+ fallback_value=fallback_value,
579
+ )
580
+
581
+ def is_size_one_or_false(self, size: Expr) -> bool:
582
+ """Return True if size equals 1.
583
+
584
+ Unbacked symbolic sizes return False without introducing a guard.
585
+ """
586
+ return self.guard_or_false(sympy.Eq(size, 1))
587
+
588
+ def evaluate_min(self, left: Expr, right: Expr) -> Expr:
589
+ """Return the smaller of left and right, and guard on that choice."""
590
+ if isinstance(left, Expr):
591
+ left = sympy_subs(left, self.inv_precomputed_replacements) # type: ignore[arg-type]
592
+ if isinstance(right, Expr):
593
+ right = sympy_subs(right, self.inv_precomputed_replacements) # type: ignore[arg-type]
594
+ if self.guard_or_false(sympy.Le(left, right)):
595
+ return left
596
+ if self.guard_or_false(sympy.Le(right, left)):
597
+ return right
598
+
599
+ # GCD fallback: if gcd(a, b) == a then a divides b, implying a <= b.
600
+ #
601
+ # TODO: This is NOT always sound for unbacked symints. It can
602
+ # produce wrong results when:
603
+ # - inputs can be negative: gcd(u0, 10*u0) = u0, returns u0,
604
+ # but if u0 < 0 then u0 > 10*u0 (e.g. u0=-1: min(-1,-10) = -10)
605
+ # - a factor can be zero: gcd(u0, u0*u1) = u0, returns u0,
606
+ # but if u1=0 then u0*u1=0 < u0 (e.g. u0=5,u1=0: min(5,0) = 0)
607
+ # TODO shall we add a runtime assertion at least.
608
+ gcd = sympy.gcd(left, right)
609
+ if left == gcd:
610
+ return left
611
+ if right == gcd:
612
+ return right
613
+
614
+ # Min/Max fallback: we can prove Min(a, b) <= c when any arg <= c, but
615
+ # sympy doesn't simplify this yet. So, evaluate it here. Same for Max.
616
+ for lhs, rhs in [(left, right), (right, left)]:
617
+
618
+ def le_rhs(a: Expr) -> bool:
619
+ return self.guard_or_false(sympy.Le(a, rhs))
620
+
621
+ # Min(Min(a, b), c) ==> Min(a, b) if (a <= c) or (b <= c).
622
+ if isinstance(lhs, sympy.Min) and any(le_rhs(a) for a in lhs.args):
623
+ return lhs
624
+ # Min(Max(a, b), c) ==> Max(a, b) if (a <= c) and (b <= c).
625
+ if isinstance(lhs, sympy.Max) and all(le_rhs(a) for a in lhs.args):
626
+ return lhs
627
+
628
+ raise TypeError(
629
+ f"evaluate_min({left}, {right}) with unbacked symints"
630
+ ) from None
631
+
632
+ def evaluate_max(self, left: Expr, right: Expr) -> Expr:
633
+ """Return the larger of left and right, and guard on that choice."""
634
+ # Always choose the opposite of eval min for consistency
635
+ # This means min(a, b) and max(a, b) produce the same guards
636
+ min_val = self.evaluate_min(left, right)
637
+ return right if min_val is left else left
638
+
639
+ def guard_int(self, expr: Expr | int) -> int:
640
+ """
641
+ Similar to guard_int in symbolic_shapes.py, except this function works with SymPy
642
+ expressions instead of SymNodes. It extracts the value represented by expr from shapeEnv
643
+ and specialize the compiled graph on it. Raises an error if the result cannot be
644
+ determined due to unhinted or unbacked symbols.
645
+ """
646
+ if isinstance(expr, int):
647
+ return expr
648
+ val = self.guarding_hint_or_throw(expr)
649
+ self.check_equals(expr, sympy.Integer(val))
650
+ return int(val)
651
+
652
+ def guard_int_seq(self, left: Sequence[Expr | int]) -> list[int]:
653
+ """
654
+ Apply guard_int on a sequence of inputs.
655
+ """
656
+ return [self.guard_int(x) for x in left]
657
+
658
+ def remove_precomputed_replacements(self, expr: Expr) -> Expr:
659
+ if any(symbol_is_type(s, SymT.PRECOMPUTED_SIZE) for s in expr.free_symbols): # type: ignore[attr-defined]
660
+ return sympy_subs(expr, self.inv_precomputed_replacements) # type: ignore[arg-type]
661
+ return expr
662
+
663
+ def replace_backed_symbols_with_hints(
664
+ self,
665
+ expr: Expr | int,
666
+ ) -> Expr | int:
667
+ """
668
+ Replace all backed symbols in an expression with their concrete hint values.
669
+
670
+ This function substitutes backed symbolic variables with their known concrete
671
+ values from the shape environment, while leaving unbacked symbols (data-dependent
672
+ values) untouched. Not recommended to be used except as last resort!
673
+ was added for existing usages compatibility.
674
+ """
675
+ if isinstance(expr, int):
676
+ return expr
677
+ # Substitute all hints into expr, but leave unbacked symints alone
678
+ expr = self.simplify(expr)
679
+ if not isinstance(expr, Expr):
680
+ assert isinstance(expr, int)
681
+ return expr
682
+
683
+ expr = self.remove_precomputed_replacements(expr)
684
+ expr = sympy_subs(expr, self.backed_var_to_val)
685
+ if isinstance(expr, Expr):
686
+ expr = expr.expand(identity=True)
687
+
688
+ free_symbols = expr.free_symbols
689
+ if not free_symbols:
690
+ try:
691
+ return int(expr)
692
+ except TypeError:
693
+ return expr # inf/nan/I
694
+
695
+ return expr
696
+
697
+ def to_symint_or_int(self, expr: Expr | int) -> SymInt | int:
698
+ """Convert a sympy expression to SymInt, or return int as is."""
699
+ if isinstance(expr, int):
700
+ return expr
701
+
702
+ from torch.fx.experimental.sym_node import SymNode
703
+
704
+ expr = self.simplify(expr)
705
+ if isinstance(expr, (int, sympy.Integer)):
706
+ return int(expr)
707
+
708
+ # Remove precomputed replacements to get canonical form
709
+ # (consistent with symbolic_hint behavior)
710
+ expr = self.remove_precomputed_replacements(expr)
711
+
712
+ try:
713
+ hint = self.guarding_hint_or_throw(expr)
714
+ except Exception:
715
+ hint = None
716
+ node = SymNode(expr, self.shape_env, int, hint)
717
+ return SymInt(node)
718
+
719
+ def to_symints_or_ints(self, exprs: Sequence[Expr | int]) -> list[SymInt | int]:
720
+ """Convert a sequence of sympy expressions to SymInts, or return ints as is."""
721
+ return [self.to_symint_or_int(e) for e in exprs]
722
+
723
+ def guarding_hint_or_throw(self, expr: Expr | int) -> int:
724
+ """
725
+ Return a concrete integer hint for an expression that is safe to use for guarding.
726
+
727
+ This method evaluates the expression using only backed-symbols hints. Unlike
728
+ optimization_hint(), this method does NOT use heuristics or fallback values
729
+ for unbacked symbols.
730
+
731
+ Use this method when you need a hint value that will be used for guarding decision.
732
+
733
+ Args:
734
+ expr: A sympy expression or integer to evaluate.
735
+
736
+ Returns:
737
+ The concrete integer value of the expression based on backed symbol hints.
738
+
739
+ Raises:
740
+ GuardOnDataDependentSymNode: If the expression contains unbacked symbols
741
+ (data-dependent values) that cannot be resolved to concrete values.
742
+
743
+ See Also:
744
+ optimization_hint: For cases where fallback/heuristic values are acceptable
745
+ for unbacked symbols.
746
+ """
747
+ if isinstance(expr, SymNode):
748
+ raise TypeError(
749
+ f"guarding_hint_or_throw expects a sympy Expr or int, not {type(expr)}. "
750
+ "Use expr.expr to extract the sympy expression from a SymNode."
751
+ )
752
+ return _guarding_hint_or_throw_base(
753
+ self.shape_env, expr, self.inv_precomputed_replacements
754
+ )
755
+
756
+ def optimization_hint(self, expr: Expr | int, fallback: int | None = None) -> int:
757
+ """
758
+ Return a concrete integer hint for an expression.
759
+
760
+ This function should be used for non-guarding based optimizations. If you
761
+ want a hint that you can guard on, use the guarding_hint API instead.
762
+
763
+ This function will hint unbacked symbols using user provided optimization
764
+ hints. If not provided, fallback will be used along with some heuristics
765
+ that try to maximize consistency with the shape environment.
766
+
767
+ Special cases:
768
+ - Complex numbers (containing sympy.I): raises an error since tensor
769
+ dimensions cannot be complex.
770
+ - Infinity (int_oo, sympy.oo): returns sys.maxsize.
771
+ - NaN (sympy.nan): returns the fallback value.
772
+ """
773
+ return _optimization_hint_base(
774
+ self.shape_env, expr, self.inv_precomputed_replacements, fallback
775
+ )
776
+
777
+ def optimization_hints(
778
+ self,
779
+ exprs: Iterable[Expr | int],
780
+ fallback: int | None = None,
781
+ ) -> tuple[int, ...]:
782
+ """
783
+ Like optimization_hint but for a sequence of expressions.
784
+ Returns a tuple of concrete integer hints.
785
+ """
786
+ # Read config at call time to respect runtime patches (e.g., in tests)
787
+ if fallback is None:
788
+ fallback = config.unbacked_symint_fallback
789
+ return tuple(self.optimization_hint(x, fallback=fallback) for x in exprs)
790
+
791
+ def all_unbacked_explicitly_hinted(self, exprs: IterateExprs) -> bool:
792
+ """
793
+ Return True if every unbacked symbol in *exprs* has an explicit
794
+ user-provided hint in var_to_hint_override. If there are no
795
+ unbacked symbols at all, returns True (vacuously).
796
+ *exprs* can be a single expression or any iterable accepted by
797
+ free_unbacked_symbols (list, tuple, etc.).
798
+ """
799
+ unbacked = free_unbacked_symbols(exprs)
800
+ return unbacked.issubset(self.var_to_hint_override.keys())
801
+
802
+ def optimization_hint_with_override(
803
+ self,
804
+ expr: Expr | int,
805
+ hint_override: int | None,
806
+ ) -> int:
807
+ r"""Return a concrete integer hint for an expression, with optional override.
808
+ This is used in dynamic dispatch scenarios where callers may want to
809
+ provide a specific hint value rather than computing one from the expression.
810
+ The resolution order is:
811
+ 1. If ``expr`` simplifies to a static integer, return that value
812
+ (``hint_override`` is ignored for static shapes).
813
+ 2. If ``expr`` is dynamic and ``hint_override`` is not ``None``,
814
+ return ``hint_override``.
815
+ 3. Otherwise, compute a hint via :meth:`optimization_hint`
816
+ Args:
817
+ expr (Expr or int): The expression to get a hint for.
818
+ hint_override (int, optional): If provided and ``expr`` is dynamic,
819
+ this value is returned instead of computing a hint.
820
+ Returns:
821
+ int: A concrete integer hint for the expression.
822
+ """
823
+ simplified = _maybe_realize_expr(self.simplify(expr), None)
824
+
825
+ if simplified is not None:
826
+ return simplified
827
+
828
+ # Dynamic shape: use hint_override if set,
829
+ # else return optimization_hint
830
+ if hint_override is not None:
831
+ return hint_override
832
+
833
+ return self.optimization_hint(expr)
834
+
835
+ def optimization_hints_with_override(
836
+ self,
837
+ exprs: Iterable[Expr | int],
838
+ hint_override: int | None,
839
+ ) -> tuple[int, ...]:
840
+ """
841
+ Like optimization_hint_with_override but for a sequence of expressions.
842
+ Returns a tuple of concrete integer hints.
843
+ """
844
+ return tuple(
845
+ self.optimization_hint_with_override(e, hint_override) for e in exprs
846
+ )
847
+
848
+ def guarding_hints_or_throw(
849
+ self,
850
+ exprs: Iterable[Expr | int],
851
+ ) -> tuple[int, ...]:
852
+ return tuple(self.guarding_hint_or_throw(x) for x in exprs)
853
+
854
+ def _lru_cache(self, fn, maxsize=None):
855
+ """
856
+ Wrapper around functools.lru_cache that clears when replacements
857
+ has been invalidated.
858
+ """
859
+ fn_cache = functools.lru_cache(maxsize)(fn)
860
+ prior_len = len(self.replacements)
861
+
862
+ @functools.wraps(fn)
863
+ def wrapper(*args, **kwargs):
864
+ nonlocal prior_len
865
+ if prior_len != len(self.replacements):
866
+ prior_len = len(self.replacements)
867
+ fn_cache.cache_clear()
868
+ return fn_cache(*args, **kwargs)
869
+
870
+ return wrapper
871
+
872
+ def make_stride_vars_cache(self):
873
+ cache = self._lru_cache(self._stride_vars)
874
+
875
+ def stride_vars(
876
+ index: Expr,
877
+ vars: Sequence[sympy.Symbol],
878
+ support_vars: Sequence[sympy.Symbol] | None = None,
879
+ ) -> list[Expr]:
880
+ if not support_vars:
881
+ support_vars = vars
882
+ return cache(index, tuple(vars), tuple(support_vars))
883
+
884
+ return stride_vars
885
+
886
+ def _stride_vars(
887
+ self,
888
+ index: Expr,
889
+ vars: Sequence[sympy.Symbol],
890
+ support_vars: Sequence[sympy.Symbol],
891
+ ) -> list[Expr]:
892
+ """Convert an indexing expression back into strides
893
+
894
+ NOTE: This is only valid if the index is a standard strided offset
895
+ calculation. e.g. 10 * ModularIndexing(i0 + 1, 1, 2) would give a
896
+ stride of -10 because the index wraps around after the first element
897
+
898
+ """
899
+ strides = []
900
+ index = self.simplify(index)
901
+ # remove any offset
902
+ index = index - sympy_subs(
903
+ index, {v: sympy.S.Zero for v in support_vars if v != 0}
904
+ )
905
+ for i in range(len(vars)):
906
+ # drop all the other dims
907
+ index_dim = sympy_subs(
908
+ index,
909
+ {
910
+ support_vars[j]: sympy.S.Zero
911
+ for j in range(len(support_vars))
912
+ if vars[i] != support_vars[j] and support_vars[j] != 0
913
+ },
914
+ )
915
+ v = vars[i]
916
+ if v == 0:
917
+ strides.append(sympy.S.Zero)
918
+ else:
919
+ # TODO(jansel): should we use sympy.diff here?
920
+ strides.append(
921
+ sympy_subs(index_dim, {v: sympy.S.One})
922
+ - sympy_subs(index_dim, {v: sympy.S.Zero})
923
+ )
924
+ return strides
925
+
926
+ def offset_var(self, index: Expr, vars: Sequence[sympy.Symbol]) -> Expr:
927
+ """Extract offset part of an indexing expression"""
928
+ index = self.simplify(index)
929
+ return sympy_subs(index, {v: sympy.S.Zero for v in vars if v != 0})
930
+
931
+ # Return stride optimizaitons hints,
932
+ # only used for optimizations.
933
+ def stride_hints(
934
+ self,
935
+ index: Expr,
936
+ vars: Sequence[sympy.Symbol],
937
+ support_vars: Sequence[sympy.Symbol] | None = None,
938
+ ) -> list[int]:
939
+ for v in index.free_symbols:
940
+ if symbol_is_type(v, SymT.INDIRECT): # type: ignore[attr-defined]
941
+ index = sympy_subs(index, {v: 0}) # type: ignore[dict-item]
942
+ result = []
943
+ for s in self.stride_vars(index, vars, support_vars):
944
+ result.append(self.optimization_hint(s, fallback=0))
945
+ return result
946
+
947
+ def stride_order(self, index: Expr, vars: list[sympy.Symbol]) -> list[int]:
948
+ strides = tuple(map(abs, self.stride_hints(index, vars)))
949
+ order = list(range(len(strides)))
950
+ order.sort(key=lambda x: (strides[x] == 0, strides[x]))
951
+ return order
952
+
953
+ def lookup_precomputed_size(self, expr: Expr) -> Expr:
954
+ if (
955
+ isinstance(expr, (int, sympy.Symbol, sympy.Number))
956
+ or expr.is_number
957
+ or expr.is_symbol
958
+ ):
959
+ return expr
960
+ expr = self.remove_precomputed_replacements(expr)
961
+ if expr not in self.precomputed_replacements:
962
+ sym = sympy_index_symbol_with_prefix(
963
+ SymT.PRECOMPUTED_SIZE, len(self.precomputed_replacements)
964
+ )
965
+ self.precomputed_replacements[expr] = sym
966
+ self.inv_precomputed_replacements[sym] = expr
967
+ return self.precomputed_replacements[expr]
968
+
969
+ def free_symbols(self) -> OrderedSet[sympy.Symbol]:
970
+ return OrderedSet(self.backed_var_to_val.keys()) - OrderedSet(
971
+ self.replacements.keys()
972
+ )
973
+
974
+ def combine_modular_indexing_pairs(self, index: sympy.Expr) -> sympy.Expr:
975
+ """
976
+ A pair of special ModularIndexing can be combined.
977
+
978
+ E.g. ModularIndexing(ModularIndexing(x, 1, a), 1, b)
979
+ We can simplify this to ModuleIndexing(x, 1, b), if
980
+ 1. x is non negative integer
981
+ 2. a and b are positive integers
982
+ 3. a is a multiple of b.
983
+ """
984
+
985
+ def _check_args(x, div, mod, is_first):
986
+ if not isinstance(div, sympy.Integer) or not isinstance(mod, sympy.Integer):
987
+ return False
988
+ if div != 1:
989
+ return False
990
+ if mod <= 0:
991
+ return False
992
+
993
+ if is_first:
994
+ # first ModularIndexing should contains a nested ModularIndex
995
+ if not isinstance(x, ModularIndexing):
996
+ return False
997
+ else:
998
+ # second ModularIndexing should contains a non-negative
999
+ # symbol
1000
+ if not isinstance(x, sympy.Symbol) or not self.statically_known_geq(
1001
+ x, 0
1002
+ ):
1003
+ return False
1004
+ return True
1005
+
1006
+ if isinstance(index, ModularIndexing):
1007
+ x, div, mod = index.args
1008
+
1009
+ if not _check_args(x, div, mod, True):
1010
+ return index
1011
+
1012
+ x2, div2, mod2 = x.args
1013
+
1014
+ if not _check_args(x2, div2, mod2, False):
1015
+ return index
1016
+
1017
+ if Mod(mod2, mod) != 0:
1018
+ return index
1019
+
1020
+ return ModularIndexing(x2, 1, mod)
1021
+
1022
+ return index
1023
+
1024
+ def expand_floor_div(
1025
+ self, index: sympy.Expr
1026
+ ) -> bool | tuple[sympy.Expr, sympy.Expr]:
1027
+ """
1028
+ Expand the FloorDiv to the entire expression so that the expression may
1029
+ be simplified.
1030
+
1031
+ E.g., for a 2D contiguous tensor with shape [a, 2 * b], and index variables
1032
+ x1, x2, index expression 'x1 * 2b + x2' can be easily combined.
1033
+ But index expression 'x1 * b + x2 // 2' can not.
1034
+ By expanding the FloorDiv to the entire expression, we get
1035
+ '(x1 * 2b + x2) // 2'. This transformation allows us to merge loops
1036
+ for the numerator!
1037
+
1038
+ Return false if this optimization can be applied;
1039
+ Return the new expression and the denominator otherwise.
1040
+ The original expression will be equivalent to 'new_expression // denominator'
1041
+ """
1042
+ if not isinstance(index, sympy.Add):
1043
+ return False
1044
+ terms = index.args
1045
+
1046
+ if len(terms) < 2:
1047
+ return False
1048
+ floor_div_index = -1
1049
+ varlist = []
1050
+ factorlist = []
1051
+ for idx, term in enumerate(terms):
1052
+ if isinstance(term, sympy.Mul):
1053
+ # For dynamic shape, term like '2*s1*x1' has 3 child nodes.
1054
+ # - A integer for 2
1055
+ # - A symbol for s1
1056
+ # - A symbol for x1
1057
+ # Skip for now.
1058
+ if len(term.args) != 2:
1059
+ return False
1060
+ factor, var = term.args
1061
+ varlist.append(var)
1062
+ factorlist.append(factor)
1063
+ if not isinstance(factor, sympy.Integer) or not isinstance(
1064
+ var, sympy.Symbol
1065
+ ):
1066
+ return False
1067
+ # It's easier to reason about the correceness of the transformation
1068
+ # for non-negative integers.
1069
+ if not self.statically_known_geq(var, 0):
1070
+ return False
1071
+ elif isinstance(term, FloorDiv):
1072
+ var, factor = term.args
1073
+ if not isinstance(factor, sympy.Integer) or not isinstance(
1074
+ var, sympy.Symbol
1075
+ ):
1076
+ return False
1077
+ if not self.statically_known_geq(var, 0):
1078
+ return False
1079
+ if floor_div_index >= 0:
1080
+ # can not handle multi FloorDiv yet
1081
+ return False
1082
+
1083
+ floor_div_index = idx
1084
+ varlist.append(var)
1085
+ # this factor is denominator
1086
+ factorlist.append(factor)
1087
+ else:
1088
+ return False
1089
+
1090
+ if floor_div_index < 0:
1091
+ return False
1092
+
1093
+ # Construct the new expression and remember the denominator
1094
+ denominator = factorlist[floor_div_index]
1095
+ new_index = sympy.S.Zero
1096
+
1097
+ for var, factor, idx in zip(varlist, factorlist, itertools.count()):
1098
+ if idx == floor_div_index:
1099
+ new_index += var
1100
+ else:
1101
+ new_index += (factor * denominator) * var
1102
+
1103
+ return new_index, denominator
1104
+
1105
+
1106
+ def join_dimensions(expr: Expr) -> Expr:
1107
+ if not isinstance(expr, sympy.Add) or not expr.has(ModularIndexing):
1108
+ return expr # fast exit path
1109
+ return _join_dimensions_cached(expr)
1110
+
1111
+
1112
+ @functools.lru_cache(256)
1113
+ def _join_dimensions_cached(expr: Expr) -> Expr:
1114
+ """
1115
+ ModularIndexing(i0, 1, 32) + 32 * ModularIndexing(i0, 32, 4)
1116
+ becomes
1117
+ ModularIndexing(i0, 1, 128)
1118
+ ModularIndexing(i0, 1, 32) + 32 * FloorDiv(i0, 32)
1119
+ becomes i0
1120
+
1121
+
1122
+ This type of pattern can come from view operations
1123
+ """
1124
+ assert isinstance(expr, sympy.Add)
1125
+
1126
+ scale = sympy.Wild("scale", exclude=[0], integer=True)
1127
+ base = sympy.Wild("base", integer=True)
1128
+ divisor = sympy.Wild("divisor", integer=True)
1129
+ mod1 = sympy.Wild("modulus", integer=True)
1130
+ mod2 = sympy.Wild("modulus2", integer=True)
1131
+ for term1 in expr.args:
1132
+ m1 = term1.match(scale * ModularIndexing(base, divisor, mod1))
1133
+ if m1:
1134
+ for term2 in expr.args:
1135
+ m2 = term2.match(
1136
+ m1[scale]
1137
+ * m1[mod1]
1138
+ * ModularIndexing(m1[base], m1[divisor] * m1[mod1], mod2)
1139
+ )
1140
+ if m2 and term1 != term2:
1141
+ expr = join_dimensions(
1142
+ expr
1143
+ - term1
1144
+ - term2
1145
+ + m1[scale]
1146
+ * ModularIndexing(m1[base], m1[divisor], m1[mod1] * m2[mod2])
1147
+ )
1148
+ return expr
1149
+ for term1 in expr.args:
1150
+ m1 = term1.match(scale * ModularIndexing(base, divisor, mod1))
1151
+ if m1:
1152
+ for term2 in expr.args:
1153
+ m2 = term2.match(
1154
+ m1[scale] * m1[mod1] * FloorDiv(m1[base], m1[divisor] * m1[mod1])
1155
+ )
1156
+ if m2 is not None: # in case of success we get an empty dict here
1157
+ expr = join_dimensions(
1158
+ expr
1159
+ - term1
1160
+ - term2
1161
+ + m1[scale] * FloorDiv(m1[base], m1[divisor])
1162
+ )
1163
+ return expr
1164
+ return expr
1165
+
1166
+
1167
+ class SimplifyIndexing(V.WrapperHandler): # type: ignore[name-defined]
1168
+ """
1169
+ A wrapper around .virtualize.ops that uses var range information to
1170
+ simplify ModularIndexing/FloorDiv.
1171
+ """
1172
+
1173
+ def __init__(self, inner, var_ranges: VarRanges) -> None:
1174
+ super().__init__(inner)
1175
+ self.name = "SimplifyIndexing"
1176
+ self._simplify: Callable[[Expr], Expr] = (
1177
+ lambda index: V.graph.sizevars.simplify_with_ranges(index, var_ranges)
1178
+ )
1179
+
1180
+ def load(self, name: str, index: sympy.Expr):
1181
+ return self._inner.load(name, self._simplify(index))
1182
+
1183
+ def store(self, name, index, value, mode=None):
1184
+ return self._inner.store(name, self._simplify(index), value, mode=mode)
1185
+
1186
+ def store_reduction(self, name, index, value):
1187
+ return self._inner.store_reduction(name, self._simplify(index), value)
1188
+
1189
+ def index_expr(self, index, dtype):
1190
+ return self._inner.index_expr(self._simplify(index), dtype)
1191
+
1192
+ def check_bounds(self, index, size, lower, upper):
1193
+ return self._inner.check_bounds(self._simplify(index), size, lower, upper)
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/standalone_compile.py ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import copy
5
+ import logging
6
+ import os
7
+ import pickle
8
+ import shutil
9
+ from abc import ABC, abstractmethod
10
+ from contextlib import AbstractContextManager, nullcontext
11
+ from typing import Any, Literal, TYPE_CHECKING
12
+
13
+ import torch.fx
14
+ from torch._dynamo.aot_compile_types import BundledAOTAutogradSerializableCallable
15
+ from torch._dynamo.utils import dynamo_timed
16
+ from torch._inductor.cpp_builder import normalize_path_separator
17
+ from torch._inductor.cudagraph_utils import BoxedDeviceIndex
18
+ from torch._inductor.runtime.cache_dir_utils import temporary_cache_dir
19
+ from torch._inductor.utils import BoxedBool, InputType
20
+ from torch._subclasses import FakeTensorMode
21
+ from torch.fx.experimental.symbolic_shapes import ShapeEnv
22
+
23
+ from . import config
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Callable, Sequence
28
+
29
+ from torch.compiler._cache import CacheInfo
30
+ from torch.fx import GraphModule
31
+
32
+
33
+ log = logging.getLogger(__name__)
34
+
35
+
36
+ class CompiledArtifact(ABC):
37
+ """
38
+ CompiledArtifact class represents the inductor cache artifacts that
39
+ can be invoked in order to avoid repeated compilation.
40
+
41
+ CompiledArtifact can be obtained by calling standalone_compile(gm, example_inputs)
42
+ to create a fresh CompiledArtifact from a GraphModule and example inputs.
43
+
44
+ Later this CompiledArtifact can be saved to disk, either as a binary or unpacked
45
+ into the provided folder via the CompiledArtifact.save function.
46
+
47
+ CompiledArtifact.load provides a way to create a CompiledArtifact from the
48
+ binary or unpacked data.
49
+
50
+ Finally, the CompiledArtifact can be invoked via the __call__ method
51
+ to execute the cached artifact.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ compiled_fn: Callable[..., Any],
57
+ artifacts: tuple[bytes, CacheInfo] | None,
58
+ ):
59
+ self._compiled_fn = compiled_fn
60
+ self._artifacts = artifacts
61
+
62
+ @abstractmethod
63
+ def __call__(self, *args: Any) -> Any: ...
64
+
65
+ @abstractmethod
66
+ def save(
67
+ self, *, path: str, format: Literal["binary", "unpacked"] = "binary"
68
+ ) -> None: ...
69
+
70
+ @staticmethod
71
+ def load(
72
+ *, path: str, format: Literal["binary", "unpacked"] = "binary"
73
+ ) -> CompiledArtifact:
74
+ if format == "unpacked":
75
+ # If format is unpacked, it must be a CacheCompiledArtifact
76
+ return CacheCompiledArtifact.load(path=path, format=format)
77
+
78
+ assert format == "binary"
79
+ with open(path, "rb") as file:
80
+ from torch.utils._appending_byte_serializer import BytesReader
81
+
82
+ from .codecache import torch_key
83
+
84
+ result_bytes = file.read()
85
+ reader = BytesReader(result_bytes)
86
+ header = reader.read_bytes()
87
+ if header == AOTCompiledArtifact.AOT_HEADER:
88
+ assert reader.read_bytes() == torch_key()
89
+ artifact = reader.read_bytes()
90
+ assert reader.is_finished()
91
+ return AOTCompiledArtifact.deserialize(artifact)
92
+ # Otherwise, it's in the CacheCompiledArtifact format
93
+ elif header == CacheCompiledArtifact.CACHE_HEADER:
94
+ assert reader.read_bytes() == torch_key()
95
+ key = reader.read_str()
96
+ artifact_bytes = reader.read_bytes()
97
+ assert reader.is_finished()
98
+ torch.compiler.load_cache_artifacts(artifact_bytes)
99
+ return CacheCompiledArtifact._load_impl(nullcontext(), key)
100
+ else:
101
+ raise RuntimeError(
102
+ "Invalid header, expected CacheCompiledArtifact or AOTCompiledArtifact, got: "
103
+ + header.decode("utf-8")
104
+ )
105
+
106
+
107
+ class CacheCompiledArtifact(CompiledArtifact):
108
+ """
109
+ CompiledArtifact that depends on torch.compiler.save_cache_artifacts
110
+ """
111
+
112
+ CACHE_HEADER = bytes("CacheCompiledArtifact", "utf-8")
113
+
114
+ def __init__(
115
+ self,
116
+ compiled_fn: Callable[..., Any],
117
+ artifacts: tuple[bytes, CacheInfo] | None,
118
+ ):
119
+ self._compiled_fn = compiled_fn
120
+ self._artifacts = artifacts
121
+
122
+ def __call__(self, *args: Any) -> Any:
123
+ return self._compiled_fn(*args)
124
+
125
+ def is_saveable(self) -> bool:
126
+ if self._artifacts is None:
127
+ return False
128
+ _, cache_info = self._artifacts
129
+ # 0 means nothing was saved
130
+ # >1 means multiple artifacts were saved, which is concerning
131
+ # (we only expect one)
132
+ return len(cache_info.aot_autograd_artifacts) == 1
133
+
134
+ def save(
135
+ self, *, path: str, format: Literal["binary", "unpacked"] = "binary"
136
+ ) -> None:
137
+ with dynamo_timed("CompiledArtifact.save"):
138
+ if self._artifacts is None:
139
+ raise RuntimeError(
140
+ "CompiledArtifact.save failed to save since there's no artifact to save"
141
+ )
142
+ artifact_bytes, cache_info = self._artifacts
143
+ if len(cache_info.aot_autograd_artifacts) == 0:
144
+ raise RuntimeError(
145
+ f"CompiledArtifact.save failed to save due to no aot_autograd artifacts. "
146
+ f"This likely means there was something that was not serializable in the "
147
+ f"graph passed to standalone_compile. This can generally be fixed by "
148
+ f"ensuring that your model only uses constructs that are serializable. "
149
+ f"{cache_info}"
150
+ )
151
+ if len(cache_info.aot_autograd_artifacts) > 1:
152
+ raise AssertionError(
153
+ f"CompiledArtifact.save failed to save because there was more than one "
154
+ f"artifact but we only expected one. {cache_info}"
155
+ )
156
+ key = cache_info.aot_autograd_artifacts[0]
157
+
158
+ if format == "binary":
159
+ # can't assert that it is a file since it might not exist yet
160
+ assert not os.path.isdir(path)
161
+
162
+ from torch.utils._appending_byte_serializer import BytesWriter
163
+
164
+ from .codecache import torch_key
165
+
166
+ writer = BytesWriter()
167
+ writer.write_bytes(CacheCompiledArtifact.CACHE_HEADER)
168
+ writer.write_bytes(torch_key())
169
+ writer.write_str(key)
170
+ writer.write_bytes(artifact_bytes)
171
+
172
+ from torch._inductor.codecache import write_atomic
173
+
174
+ write_atomic(path, writer.to_bytes())
175
+ else:
176
+ assert format == "unpacked"
177
+ if os.path.exists(path):
178
+ assert os.path.isdir(path)
179
+ shutil.rmtree(path, ignore_errors=True)
180
+
181
+ from .codecache import FxGraphCache
182
+
183
+ with temporary_cache_dir(path):
184
+ # This function unpacks the cache artifacts to disk
185
+ loaded_cache_info = torch.compiler.load_cache_artifacts(
186
+ artifact_bytes
187
+ )
188
+ assert loaded_cache_info is not None
189
+ # Now write all the output_code artifacts to disk so that
190
+ # they can be inspected and modified
191
+ for key in loaded_cache_info.inductor_artifacts:
192
+ subdir = FxGraphCache._get_tmp_dir_for_key(key)
193
+ assert os.path.exists(subdir)
194
+ for path in sorted(os.listdir(subdir)):
195
+ with open(os.path.join(subdir, path), "rb") as f:
196
+ graph = pickle.load(f)
197
+ output_file = graph.write_to_disk()
198
+ log.info("Output code written to: %s", output_file)
199
+
200
+ @staticmethod
201
+ def _load_impl(
202
+ cache_dir_ctx: AbstractContextManager[Any], key: str
203
+ ) -> CompiledArtifact:
204
+ with (
205
+ cache_dir_ctx,
206
+ config.patch(unsafe_skip_cache_dynamic_shape_guards=True),
207
+ ):
208
+ with torch._functorch.config.patch(strict_autograd_cache=True):
209
+ from torch._functorch._aot_autograd.autograd_cache import (
210
+ AOTAutogradCache,
211
+ )
212
+
213
+ result = AOTAutogradCache._lookup(
214
+ key,
215
+ local=True,
216
+ remote=False,
217
+ args=[],
218
+ cache_info={},
219
+ aot_config=None,
220
+ )
221
+
222
+ assert result is not None
223
+ (entry, _) = result
224
+
225
+ from .compile_fx import _CompileFxKwargs
226
+
227
+ fx_config = _CompileFxKwargs(
228
+ cudagraphs=BoxedBool(False),
229
+ boxed_forward_device_index=BoxedDeviceIndex(0),
230
+ )
231
+
232
+ context = torch._guards.TracingContext(FakeTensorMode(shape_env=ShapeEnv()))
233
+ with torch._guards.tracing(context):
234
+ compiled_fn = entry.wrap_post_compile(
235
+ [], entry.sanitized_aot_config, fx_config
236
+ )
237
+ return CacheCompiledArtifact(lambda *args: compiled_fn(list(args)), None)
238
+
239
+ @staticmethod
240
+ def _prepare_load(
241
+ *, path: str, format: Literal["binary", "unpacked"] = "binary"
242
+ ) -> tuple[str, AbstractContextManager[Any]]:
243
+ """
244
+ Do format specific prep and loads, return a context manager and key
245
+ """
246
+ path = normalize_path_separator(path)
247
+ with dynamo_timed("CompiledArtifact.load"):
248
+ if format == "binary":
249
+ # can't assert that it is a file since it might not exist yet
250
+ assert not os.path.isdir(path)
251
+ with open(path, "rb") as file:
252
+ artifacts = file.read()
253
+ from torch.utils._appending_byte_serializer import BytesReader
254
+
255
+ from .codecache import torch_key
256
+
257
+ reader = BytesReader(artifacts)
258
+ assert reader.read_bytes() == torch_key()
259
+ key = reader.read_str()
260
+ artifact_bytes = reader.read_bytes()
261
+ assert reader.is_finished()
262
+
263
+ torch.compiler.load_cache_artifacts(artifact_bytes)
264
+ return key, nullcontext()
265
+ else:
266
+ assert format == "unpacked"
267
+ assert os.path.isdir(path)
268
+ autograd_cache_dir = os.path.join(path, "aotautograd")
269
+ assert os.path.isdir(autograd_cache_dir)
270
+ files = list(os.listdir(autograd_cache_dir))
271
+ assert len(files) == 1
272
+ key = files[0]
273
+ cache_dir_ctx = temporary_cache_dir(path)
274
+ return key, cache_dir_ctx
275
+
276
+ @staticmethod
277
+ def load(
278
+ *, path: str, format: Literal["binary", "unpacked"] = "binary"
279
+ ) -> CompiledArtifact:
280
+ key, cache_dir_ctx = CacheCompiledArtifact._prepare_load(
281
+ path=path, format=format
282
+ )
283
+ return CacheCompiledArtifact._load_impl(cache_dir_ctx, key)
284
+
285
+
286
+ class AOTCompiledArtifact(CompiledArtifact):
287
+ """
288
+ Similar to CompiledArtifact, but the object is a single, bundled precompiled function.
289
+ This object is always a serializable callable function.
290
+
291
+ This object is essentially a wrapper for BundledAOTAutogradSerializableCallable, which
292
+ is used by torch._dynamo.aot_compile for AOT Precompilation.
293
+ """
294
+
295
+ AOT_HEADER = bytes("AOTCompiledArtifact", "utf-8")
296
+
297
+ def __init__(
298
+ self,
299
+ compiled_fn: Callable[..., Any],
300
+ ):
301
+ self.inner_fn = BundledAOTAutogradSerializableCallable(compiled_fn)
302
+ self._artifacts = (
303
+ None # We don't need artifacts, the inner object handles everything
304
+ )
305
+
306
+ @staticmethod
307
+ def from_bundled_callable(
308
+ bundled_fn: BundledAOTAutogradSerializableCallable,
309
+ ) -> AOTCompiledArtifact:
310
+ return AOTCompiledArtifact(bundled_fn.compiled_fn)
311
+
312
+ def __call__(self, *args: Any) -> Any:
313
+ return self.inner_fn(*args)
314
+
315
+ def save(
316
+ self, *, path: str, format: Literal["binary", "unpacked"] = "binary"
317
+ ) -> None:
318
+ if format == "unpacked":
319
+ raise RuntimeError(
320
+ "AOTCompiledArtifact does not support unpacked format yet"
321
+ )
322
+ result_bytes = self.serialize()
323
+ from torch.utils._appending_byte_serializer import BytesWriter
324
+
325
+ from .codecache import torch_key
326
+
327
+ writer = BytesWriter()
328
+ writer.write_bytes(AOTCompiledArtifact.AOT_HEADER)
329
+ writer.write_bytes(torch_key())
330
+ writer.write_bytes(result_bytes)
331
+
332
+ from torch._inductor.codecache import write_atomic
333
+
334
+ # Save a sentinel file to indicate that this is AOT
335
+ write_atomic(path, writer.to_bytes())
336
+
337
+ def serialize(self) -> bytes:
338
+ return BundledAOTAutogradSerializableCallable.serialize_compile_artifacts(
339
+ self.inner_fn
340
+ )
341
+
342
+ @staticmethod
343
+ def deserialize(result_bytes: bytes) -> AOTCompiledArtifact:
344
+ deserialized = (
345
+ BundledAOTAutogradSerializableCallable.deserialize_compile_artifacts(
346
+ result_bytes
347
+ )
348
+ )
349
+ assert isinstance(deserialized, BundledAOTAutogradSerializableCallable)
350
+ return AOTCompiledArtifact.from_bundled_callable(deserialized)
351
+
352
+ @staticmethod
353
+ def load(
354
+ *, path: str, format: Literal["binary", "unpacked"] = "binary"
355
+ ) -> CompiledArtifact:
356
+ if format == "unpacked":
357
+ raise RuntimeError(
358
+ "AOTCompiledArtifact does not support unpacked format yet"
359
+ )
360
+ with open(path, "rb") as file:
361
+ from torch.utils._appending_byte_serializer import BytesReader
362
+
363
+ from .codecache import torch_key
364
+
365
+ result_bytes = file.read()
366
+ reader = BytesReader(result_bytes)
367
+ header = reader.read_bytes()
368
+ assert header == AOTCompiledArtifact.AOT_HEADER
369
+ assert reader.read_bytes() == torch_key()
370
+ artifact = reader.read_bytes()
371
+ assert reader.is_finished()
372
+ return AOTCompiledArtifact.deserialize(artifact)
373
+
374
+
375
+ def _resolve_ignore_shape_env(dynamic_shapes: Any):
376
+ # tells compile_fx to ignore the shape_envs on the ambient context
377
+ # and the graph_module.
378
+ return dynamic_shapes == "from_example_inputs"
379
+
380
+
381
+ def _resolve_fake_mode(gm: GraphModule, dynamic_shapes: Any) -> FakeTensorMode:
382
+ if dynamic_shapes == "from_example_inputs":
383
+ return FakeTensorMode(shape_env=ShapeEnv())
384
+ elif dynamic_shapes == "from_tracing_context":
385
+ # Reuse fake_mode from the TracingContext.
386
+ # NB: The TracingContext only exists if we're currently in a torch.compile backend.
387
+ context = torch._guards.TracingContext.get()
388
+ assert context.fake_mode is not None
389
+ return context.fake_mode
390
+ elif dynamic_shapes == "from_graph":
391
+ # Strategy: find a FakeTensor in the graph output, grab its FakeTensorMode.
392
+ # The graph passed to standalone_compile must be an Inductor-approved graph,
393
+ # which means that there is at least one Tensor output and the output node
394
+ # contains a flat list of Tensors.
395
+ last_node = next(iter(reversed(gm.graph.nodes)))
396
+ assert last_node.op == "output"
397
+ assert len(last_node.args) == 1
398
+
399
+ # If gm came from Dynamo, then last_node.args[0] is always a list,
400
+ # even in single-Tensor returns.
401
+ #
402
+ # It's possible to get into a situation where last_node.args[0]
403
+ # is a Node (and not a list!). This happens if you call split_module
404
+ # on the graph. We allow for this case since it is common.
405
+ nodes = (
406
+ [last_node.args[0]]
407
+ if isinstance(last_node.args[0], torch.fx.Node)
408
+ else last_node.args[0]
409
+ )
410
+ for node in nodes:
411
+ if "example_value" in node.meta:
412
+ maybe_tensor = node.meta["example_value"]
413
+ if isinstance(maybe_tensor, torch._subclasses.fake_tensor.FakeTensor):
414
+ return maybe_tensor.fake_mode
415
+
416
+ return FakeTensorMode(shape_env=ShapeEnv())
417
+ else:
418
+ raise ValueError(
419
+ f"standalone_compile got unsupported `dynamic_shapes` value: dynamic_shapes={dynamic_shapes}."
420
+ )
421
+
422
+
423
+ @contextlib.contextmanager
424
+ def _standalone_context(gm: GraphModule, dynamic_shapes: Any, aot: bool):
425
+ from torch.compiler._cache import CacheArtifactManager
426
+
427
+ fake_mode = _resolve_fake_mode(gm, dynamic_shapes)
428
+ tracing_context = torch._guards.TracingContext(fake_mode)
429
+ with (
430
+ torch._guards.tracing(tracing_context),
431
+ CacheArtifactManager.with_fresh_cache(),
432
+ config.patch("triton.autotune_at_compile_time", True),
433
+ torch._functorch.config.patch("bundled_autograd_cache", aot),
434
+ ):
435
+ yield
436
+
437
+
438
+ def standalone_compile(
439
+ gm: GraphModule,
440
+ example_inputs: Sequence[InputType],
441
+ *,
442
+ dynamic_shapes: Any,
443
+ options: Any,
444
+ aot: bool = False, # AOT mode, which uses BundledAOTAutogradCache
445
+ donate_graph_module: bool = False,
446
+ ) -> CompiledArtifact:
447
+ """
448
+ Implementation of torch.inductor.standalone_compile
449
+ """
450
+ from .compile_fx import compile_fx
451
+
452
+ ignore_shape_env = _resolve_ignore_shape_env(dynamic_shapes)
453
+ with _standalone_context(gm, dynamic_shapes, aot):
454
+ # compile_fx takes ownership of gm and may mutate it on cache miss.
455
+ if not donate_graph_module:
456
+ gm = copy.deepcopy(gm)
457
+ compiled_fn = compile_fx(
458
+ gm, example_inputs, ignore_shape_env=ignore_shape_env, **options
459
+ )
460
+ assert callable(compiled_fn)
461
+ if aot:
462
+ if not hasattr(compiled_fn, "serialize"):
463
+ raise RuntimeError(
464
+ "Compiled function should have serialize method when aot=True"
465
+ )
466
+ return AOTCompiledArtifact(compiled_fn)
467
+ artifacts = torch.compiler.save_cache_artifacts()
468
+ if artifacts is None:
469
+ log.warning(
470
+ "standalone_compile artifact generation failed, cannot save. "
471
+ "Run with TORCH_LOGS=+torch._inductor.codecache to identify the problem"
472
+ )
473
+
474
+ return CacheCompiledArtifact(compiled_fn, artifacts)
475
+
476
+
477
+ def autograd_cache_key(
478
+ graph,
479
+ example_inputs,
480
+ dynamic_shapes: Any,
481
+ aot: bool = False, # AOT mode, which uses BundledAOTAutogradCache
482
+ ):
483
+ from . import compile_fx
484
+
485
+ ignore_shape_env = _resolve_ignore_shape_env(dynamic_shapes)
486
+ with _standalone_context(graph, dynamic_shapes, aot):
487
+ return compile_fx.autograd_cache_key(
488
+ graph,
489
+ example_inputs,
490
+ ignore_shape_env=ignore_shape_env,
491
+ )
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_constants.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Constants for CUDA stream naming in Inductor codegen."""
2
+
3
+ DEFAULT_STREAM: str = "default_stream"
4
+ DEFAULT_STREAM_IDX: int = 0
5
+ STREAM_NAME_TEMPLATE: str = "stream{stream_idx:d}"
outputs/audit_venv/lib/python3.11/site-packages/torch/_inductor/stream_utils.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stream utilities for Inductor codegen."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+
7
+ from torch._inductor.stream_constants import (
8
+ DEFAULT_STREAM,
9
+ DEFAULT_STREAM_IDX,
10
+ STREAM_NAME_TEMPLATE,
11
+ )
12
+
13
+
14
+ __all__ = [
15
+ "DEFAULT_STREAM",
16
+ "DEFAULT_STREAM_IDX",
17
+ "STREAM_NAME_TEMPLATE",
18
+ "get_raw_stream_name",
19
+ "get_stream_name",
20
+ ]
21
+
22
+
23
+ @functools.lru_cache
24
+ def get_stream_name(stream_idx: int) -> str:
25
+ """Generate CUDA Stream name from stream index number.
26
+
27
+ Args:
28
+ stream_idx: Non-negative index number. 0 refers to the default stream, others refer to side
29
+ streams.
30
+ """
31
+ if stream_idx == 0:
32
+ return DEFAULT_STREAM
33
+ else:
34
+ return STREAM_NAME_TEMPLATE.format(stream_idx=stream_idx)
35
+
36
+
37
+ def get_raw_stream_name(device_idx: int) -> str:
38
+ """Generate variable name for a raw stream handle on the given device."""
39
+ return f"raw_stream{device_idx}"