proxy.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. # mypy: ignore-errors
  2. import collections
  3. import copy
  4. import dis
  5. import enum
  6. import inspect
  7. import logging
  8. import operator
  9. import sys
  10. import traceback
  11. from collections import OrderedDict
  12. from collections.abc import Callable, Iterator
  13. from dataclasses import fields, is_dataclass
  14. from typing import Any, Optional
  15. import torch
  16. import torch.fx.traceback as fx_traceback
  17. from torch._C import _fx_map_aggregate as map_aggregate, _fx_map_arg as map_arg
  18. from torch._library.opaque_object import is_opaque_value_type
  19. from torch._logging import getArtifactLogger
  20. from torch.utils._traceback import CapturedTraceback
  21. from ._compatibility import compatibility
  22. from .graph import Graph, magic_methods, reflectable_magic_methods
  23. from .immutable_collections import immutable_dict, immutable_list
  24. from .node import Argument, base_types, Node, Target
  25. from .operator_schemas import check_for_mutable_operation
  26. __all__ = [
  27. "TracerBase",
  28. "GraphAppendingTracer",
  29. "TraceError",
  30. "Proxy",
  31. "MetaProxy",
  32. "Attribute",
  33. "ParameterProxy",
  34. "Scope",
  35. "ScopeContextManager",
  36. ]
  37. log = logging.getLogger(__name__)
  38. annotation_log = getArtifactLogger(__name__, "annotation")
  39. @compatibility(is_backward_compatible=False)
  40. class Scope:
  41. """Scope object that records the module path and the module type
  42. of a module. Scope is used to track the information of the module
  43. that contains a Node in a Graph of GraphModule. For example::
  44. class Sub(torch.nn.Module):
  45. def forward(self, x):
  46. # This will be a call_method Node in GraphModule,
  47. # scope for this would be (module_path="sub", module_type=Sub)
  48. return x.transpose(1, 2)
  49. class M(torch.nn.Module):
  50. def __init__(self) -> None:
  51. self.sub = Sub()
  52. def forward(self, x):
  53. # This will be a call_method Node as well,
  54. # scope for this would be (module_path="", None)
  55. x = x.transpose(1, 2)
  56. x = self.sub(x)
  57. return x
  58. """
  59. def __init__(self, module_path: str, module_type: Any):
  60. super().__init__()
  61. self.module_path = module_path
  62. self.module_type = module_type
  63. @compatibility(is_backward_compatible=False)
  64. class ScopeContextManager:
  65. """A context manager to track the Scope of Node during symbolic tracing.
  66. When entering a forward function of a Module, we'll update the scope information of
  67. the current module, and when we exit, we'll restore the previous scope information.
  68. """
  69. def __init__(
  70. self,
  71. scope: Scope,
  72. current_scope: Scope,
  73. ):
  74. super().__init__()
  75. # Keep a copy of prev scope to restore on exit
  76. self._prev_scope = copy.copy(scope)
  77. # Update scope to current scope
  78. scope.module_path = current_scope.module_path
  79. scope.module_type = current_scope.module_type
  80. # Save a reference so we can restore it
  81. self._scope = scope
  82. def __enter__(self):
  83. return self._scope
  84. def __exit__(self, *args):
  85. self._scope.module_path = self._prev_scope.module_path
  86. self._scope.module_type = self._prev_scope.module_type
  87. return
  88. _COPY_META_FIELDS = [
  89. "nn_module_stack",
  90. "torch_fn",
  91. "source_fn_stack",
  92. "original_aten",
  93. "recompute",
  94. "ac_graph_id",
  95. "has_backward_hook",
  96. "from_node",
  97. "quantization_tag", # TODO deprecated
  98. "_numeric_debug_handle", # TODO deprecated
  99. "custom",
  100. "partitioner_tag",
  101. ]
  102. @compatibility(is_backward_compatible=True)
  103. class TracerBase:
  104. graph: Graph
  105. record_stack_traces: bool = False
  106. # When record_stack_traces is True, only reocrd stack traces
  107. # with forward function names.
  108. # This helps when we want stack trace back to model code
  109. _record_forward_stack_traces_only: bool = False
  110. # Feature flag for mutable schema checking
  111. # Enableby default in 1.12
  112. check_mutable_operations: bool = False
  113. # Feature flag for assert tracing
  114. trace_asserts: bool = False
  115. # Feature flag for proxying accesses to buffer values
  116. proxy_buffer_attributes: bool = False
  117. # Name of the function to be traced. It will only be used when
  118. # ``root`` is an instance of ``nn.Module``
  119. traced_func_name: str = "forward"
  120. # Maps the containing module's name to the operator name
  121. scope: Scope
  122. # Records the module call stack
  123. module_stack: OrderedDict[str, tuple[str, Any]]
  124. # Mapping of node name to module scope
  125. node_name_to_scope: dict[str, tuple[str, type]]
  126. @compatibility(is_backward_compatible=True)
  127. def create_node(
  128. self,
  129. kind: str,
  130. target: Target,
  131. args: tuple[Argument, ...],
  132. kwargs: dict[str, Argument],
  133. name: Optional[str] = None,
  134. type_expr: Optional[Any] = None,
  135. ) -> Node:
  136. """
  137. Inserts a graph node given target, args, kwargs, and name.
  138. This method can be overridden to do extra checking, validation, or
  139. modification of values used in node creation. For example, one might
  140. want to disallow in-place operations from being recorded.
  141. """
  142. if kind == "call_function" and self.check_mutable_operations:
  143. check_for_mutable_operation(target, args, kwargs)
  144. node = self.graph.create_node(kind, target, args, kwargs, name, type_expr)
  145. # TODO node_name_to_scope will be depreciated in favor of
  146. # node.meta['nn_module_stack']
  147. self.node_name_to_scope[node.name] = (
  148. self.scope.module_path,
  149. self.scope.module_type,
  150. )
  151. # Optionally set stack trace on the created Node for debugging purposes
  152. if fx_traceback.has_preserved_node_meta():
  153. current_meta: dict[str, Any] = fx_traceback.get_current_meta()
  154. stack_trace = current_meta.get("stack_trace")
  155. if stack_trace:
  156. node.stack_trace = stack_trace
  157. if fx_traceback.GRADIENT_ACC_SPECIAL_STACK in stack_trace:
  158. node.meta["is_gradient_acc"] = True
  159. # Explicitly set the stack_trace, nn_module_stack and source_fn on the node.meta
  160. # If other meta fields are needed, they can be added here
  161. for field in _COPY_META_FIELDS:
  162. if field in current_meta:
  163. node.meta[field] = copy.copy(current_meta[field])
  164. # Here we decrement to account for the sequence_nr having
  165. # just been incremented while tracing this lowered aten op.
  166. new_seq_nr = torch.autograd._get_sequence_nr() - 1
  167. # The sequence_nr increments every time a new autograd Node
  168. # is created. During the FWD pass we store the sequence_nr
  169. # corresponding to the last autograd Node created on this fx
  170. # node's meta. A single aten op can create multiple autograd
  171. # nodes as is the case with in-place foreach ops. During the
  172. # BWD pass we retrieve the sequence_nr stored on the current
  173. # executing autograd Node. See NOTE [ Sequence Number ].
  174. if current_meta.get("in_grad_fn", 0) > 0:
  175. annotation_log.debug("seq_nr from current_meta")
  176. new_seq_nr = current_meta["grad_fn_seq_nr"][-1]
  177. # See Note [Functionalization View Replay Annotation]
  178. # Overriding some node meta with the original node meta of the
  179. # regenerated node.
  180. replay_node: Node = fx_traceback.get_current_replay_node()
  181. if replay_node is not None:
  182. node.meta["is_functional_regenerated"] = True
  183. if "seq_nr" in replay_node.meta:
  184. annotation_log.debug("seq_nr from replay_node")
  185. new_seq_nr = replay_node.meta["seq_nr"]
  186. if "custom" in replay_node.meta:
  187. node.meta["custom"] = replay_node.meta.get("custom")
  188. if "stack_trace" in replay_node.meta:
  189. node.stack_trace = replay_node.meta.get("stack_trace")
  190. annotation_log.debug("Assigning new_seq_nr %s to %s", new_seq_nr, node.name)
  191. node.meta["seq_nr"] = new_seq_nr
  192. elif self.module_stack:
  193. node.meta["nn_module_stack"] = copy.copy(self.module_stack)
  194. if self.record_stack_traces and not node.stack_trace:
  195. user_stack_summary = CapturedTraceback.extract().summary()
  196. if user_stack_summary:
  197. user_stack_summary = self._filter_traceback_frames(user_stack_summary)
  198. if user_stack_summary:
  199. node.stack_trace = "".join(user_stack_summary.format()).strip()
  200. log.debug("create_node %s", node)
  201. return node
  202. def _filter_traceback_frames(
  203. self, user_stack_summary: traceback.StackSummary
  204. ) -> traceback.StackSummary:
  205. # This method can be overridden to customize the frame filtering logic
  206. # for the recorded stack trace
  207. user_frames = []
  208. if self._record_forward_stack_traces_only:
  209. user_frames = [
  210. frame
  211. for frame in user_stack_summary
  212. if (
  213. frame.name == "forward"
  214. or frame.filename.endswith("torch/__init__.py")
  215. )
  216. ]
  217. else:
  218. first_forward = -1
  219. for i, frame in enumerate(user_stack_summary):
  220. if frame.name == "forward":
  221. user_frames = user_stack_summary[i:]
  222. first_forward = i
  223. break
  224. # Not having a "forward" call in the stacktrace implies the
  225. # stacktrace will probably be irrelevant
  226. if first_forward == -1:
  227. user_frames = []
  228. from torch.fx.experimental.symbolic_shapes import uninteresting_files
  229. user_frames = [
  230. frame
  231. for frame in user_frames
  232. if frame.filename not in uninteresting_files()
  233. ]
  234. return traceback.StackSummary.from_list(user_frames)
  235. @compatibility(is_backward_compatible=True)
  236. def proxy(self, node: Node) -> "Proxy":
  237. return Proxy(node, self)
  238. @compatibility(is_backward_compatible=True)
  239. def create_proxy(
  240. self,
  241. kind: str,
  242. target: Target,
  243. args: tuple[Any, ...],
  244. kwargs: dict[str, Any],
  245. name: Optional[str] = None,
  246. type_expr: Optional[Any] = None,
  247. # fix noqa when updating bc tests
  248. proxy_factory_fn: Callable[[Node], "Proxy"] = None, # noqa: RUF013
  249. ):
  250. """
  251. Create a Node from the given arguments, then return the Node
  252. wrapped in a Proxy object.
  253. If kind = 'placeholder', then we're creating a Node that
  254. represents the parameter of a function. If we need to encode
  255. a default parameter, we use the ``args`` tuple. ``args`` is
  256. otherwise empty for ``placeholder`` Nodes.
  257. """
  258. args_ = self.create_arg(args)
  259. kwargs_ = self.create_arg(kwargs)
  260. assert isinstance(args_, tuple)
  261. assert isinstance(kwargs_, dict)
  262. node = self.create_node(kind, target, args_, kwargs_, name, type_expr)
  263. if not proxy_factory_fn:
  264. proxy = self.proxy(node)
  265. else:
  266. proxy = proxy_factory_fn(node)
  267. return proxy
  268. def _find_user_frame(self):
  269. """
  270. Find the Python stack frame executing the user code during
  271. symbolic tracing.
  272. """
  273. # We have to do a little dance here. Basically, walk up the callstack and
  274. # record the first frame not in the pytorch source. This is the frame executing
  275. # the user code during tracing.
  276. frame = inspect.currentframe()
  277. pt_files = [
  278. "torch/fx/proxy.py",
  279. "torch/fx/_symbolic_trace.py",
  280. "torch/fx/experimental/proxy_tensor.py",
  281. "torch/_ops.py",
  282. "torch/_tensor.py",
  283. "torch/utils/_python_dispatch.py",
  284. "torch/_prims_common/wrappers.py",
  285. "torch/_refs/__init__.py",
  286. "torch/_refs/nn/functional/__init__.py",
  287. "torch/utils/_stats.py",
  288. ]
  289. while frame:
  290. frame = frame.f_back
  291. if frame and all(
  292. not frame.f_code.co_filename.endswith(file) for file in pt_files
  293. ):
  294. break
  295. if not frame:
  296. return None
  297. return frame
  298. @compatibility(is_backward_compatible=True)
  299. def create_arg(self, a: Any) -> Argument:
  300. """
  301. A method that lowers the objects seen as arguments during symbolic evaluation
  302. into Argument types that can be stored in IR.
  303. Can be override to support more trace-specific types.
  304. """
  305. # IMPORTANT: Are you here because you are trying to proxy a new type into
  306. # the graph? Please Please Please contact someone on the PyTorch Compiler team;
  307. # the considerations are subtle.
  308. #
  309. # 1) When you add a new type, all of the downstream consumers and pass writers
  310. # need to handle the new type. torch.fx is intended to be easy to write
  311. # passes for, so we will push back against new types.
  312. # 2) In torch.compile's IR, there are only specific operations that go
  313. # into the graph. In particular, Tensor operations should go into the graph,
  314. # but non-Tensor operations shouldn't. What that means is that constructors
  315. # for new types *SHOULD NOT* become nodes in the FX graph.
  316. handler = _create_arg_bypass.get(type(a))
  317. if handler is not None:
  318. # this is just a performance optimization and can be removed if needed
  319. # for common types, we have a fast path to avoid isinstance() overhead
  320. # this doesn't remove the checks below since we need to handle subclasses
  321. return handler(self, a)
  322. if isinstance(a, Proxy):
  323. return a.node # most common arg type goes first
  324. elif hasattr(a, "__fx_create_arg__"):
  325. return a.__fx_create_arg__(self)
  326. # aggregates
  327. elif isinstance(a, tuple):
  328. if hasattr(a, "_fields"):
  329. # NamedTuple constructors don't seem to like getting a generator
  330. # expression as an argument to their constructor, so build this
  331. # intermediate tuple and unpack it into the NamedTuple constructor
  332. args = [self.create_arg(elem) for elem in a]
  333. return type(a)(*args) # type: ignore[arg-type]
  334. return type(a)([self.create_arg(elem) for elem in a])
  335. elif isinstance(a, list):
  336. return [self.create_arg(elem) for elem in a]
  337. elif isinstance(a, dict):
  338. return _create_arg_dict(self, a)
  339. elif isinstance(a, slice):
  340. return slice(
  341. self.create_arg(a.start),
  342. self.create_arg(a.stop),
  343. self.create_arg(a.step),
  344. )
  345. elif isinstance(a, range):
  346. return range(
  347. self.create_arg(a.start),
  348. self.create_arg(a.stop),
  349. self.create_arg(a.step),
  350. )
  351. elif isinstance(a, (torch._ops.OpOverload, torch._ops.HigherOrderOperator)):
  352. return a
  353. elif is_opaque_value_type(type(a)):
  354. return a
  355. elif is_dataclass(a):
  356. kwargs = {
  357. field.name: self.create_arg(getattr(a, field.name))
  358. for field in fields(a)
  359. }
  360. return self.create_node("call_function", a.__class__, (), kwargs)
  361. elif isinstance(a, (*base_types, enum.Enum)) or a is None or a is ...:
  362. return a
  363. raise NotImplementedError(f"argument of type: {type(a)}")
  364. @compatibility(is_backward_compatible=True)
  365. def to_bool(self, obj: "Proxy") -> bool:
  366. """Called when a proxy object is being converted to a boolean, such as
  367. when used in control flow. Normally we don't know what to do because
  368. we don't know the value of the proxy, but a custom tracer can attach more
  369. information to the graph node using create_node and can choose to return a value.
  370. """
  371. raise TraceError(
  372. "symbolically traced variables cannot be used as inputs to control flow"
  373. )
  374. @compatibility(is_backward_compatible=True)
  375. def iter(self, obj: "Proxy") -> Iterator:
  376. """Called when a proxy object is being iterated over, such as
  377. when used in control flow. Normally we don't know what to do because
  378. we don't know the value of the proxy, but a custom tracer can attach more
  379. information to the graph node using create_node and can choose to return an iterator.
  380. """
  381. raise TraceError(
  382. "Proxy object cannot be iterated. This can be "
  383. "attempted when the Proxy is used in a loop or"
  384. " as a *args or **kwargs function argument. "
  385. "See the torch.fx docs on pytorch.org for a "
  386. "more detailed explanation of what types of "
  387. "control flow can be traced, and check out the"
  388. " Proxy docstring for help troubleshooting "
  389. "Proxy iteration errors"
  390. )
  391. @compatibility(is_backward_compatible=True)
  392. def keys(self, obj: "Proxy") -> Any:
  393. """Called when a proxy object is has the keys() method called.
  394. This is what happens when ** is called on a proxy. This should return an
  395. iterator it ** is suppose to work in your custom tracer.
  396. """
  397. return Attribute(obj, "keys")()
  398. # used in Proxy object when just appending to the graph while not tracing.
  399. @compatibility(is_backward_compatible=True)
  400. class GraphAppendingTracer(TracerBase):
  401. def __init__(self, graph: Graph):
  402. super().__init__()
  403. self.graph = graph
  404. self.scope = Scope("", None)
  405. self.module_stack = collections.OrderedDict()
  406. self.node_name_to_scope = {}
  407. @compatibility(is_backward_compatible=False)
  408. def assert_fn(x):
  409. assert x
  410. @compatibility(is_backward_compatible=True)
  411. class TraceError(ValueError):
  412. pass
  413. @compatibility(is_backward_compatible=True)
  414. class Proxy:
  415. """
  416. ``Proxy`` objects are ``Node`` wrappers that flow through the
  417. program during symbolic tracing and record all the operations
  418. (``torch`` function calls, method calls, operators) that they touch
  419. into the growing FX Graph.
  420. If you're doing graph transforms, you can wrap your own ``Proxy``
  421. method around a raw ``Node`` so that you can use the overloaded
  422. operators to add additional things to a ``Graph``.
  423. ``Proxy`` objects cannot be iterated. In other words, the symbolic
  424. tracer will throw an error if a ``Proxy`` is used in a loop or as
  425. an ``*args``/``**kwargs`` function argument.
  426. There are two main ways around this:
  427. 1. Factor out the untraceable logic into a top-level function and
  428. use ``fx.wrap`` on it.
  429. 2. If the control flow is static (i.e. the loop trip count is
  430. based on some hyperparameter), the code can be kept in its original
  431. position and refactored into something like::
  432. for i in range(self.some_hyperparameter):
  433. indexed_item = proxied_value[i]
  434. For a more detailed description into the Proxy internals, check out
  435. the "Proxy" section in `torch/fx/README.md`
  436. """
  437. @compatibility(is_backward_compatible=True)
  438. def __init__(self, node: Node, tracer: "Optional[TracerBase]" = None):
  439. if tracer is None:
  440. # This allows you to create a Proxy object around a raw Node
  441. tracer = GraphAppendingTracer(node.graph)
  442. self.tracer = tracer
  443. self.node = node
  444. def __repr__(self) -> str:
  445. return f"Proxy({self.node.name})"
  446. def __getattr__(self, k) -> "Attribute":
  447. # note: not added to the graph yet, if this is a method call
  448. # we peephole optimize to the method invocation
  449. return Attribute(self, k)
  450. def __getstate__(self) -> dict:
  451. return self.__dict__
  452. def __deepcopy__(self, memo) -> dict:
  453. # We have to explicitly override this method, because otherwise deepcopy
  454. # will go to __getattr__(self, "__deepcopy__") and return a
  455. # Attribute(__deepcopy__), and may go into an infinite loop in some cases.
  456. import copy
  457. new_dict = {}
  458. for k, v in self.__dict__.items():
  459. try:
  460. new_obj = copy.deepcopy(v, memo)
  461. except Exception:
  462. log.warning(
  463. "Shallow copy %s of Proxy because it cannot be deepcopied. "
  464. "Proxy is created for node %s",
  465. k,
  466. self.node.name,
  467. )
  468. new_obj = copy.copy(v)
  469. new_dict[k] = new_obj
  470. assert "node" in new_dict
  471. assert "tracer" in new_dict
  472. new_proxy = Proxy(new_dict["node"], new_dict["tracer"])
  473. for k, v in new_dict.items():
  474. new_proxy.__dict__[k] = v
  475. return new_proxy
  476. def __setstate__(self, d):
  477. # This is called when being unpickled/loaded.
  478. self.__dict__ = d
  479. def __call__(self, *args, **kwargs) -> "Proxy":
  480. return self.tracer.create_proxy(
  481. "call_method", "__call__", (self,) + args, kwargs
  482. )
  483. def __iter__(self) -> Iterator["Proxy"]:
  484. frame = inspect.currentframe()
  485. assert frame is not None
  486. calling_frame = frame.f_back
  487. assert calling_frame is not None
  488. inst_list = list(dis.get_instructions(calling_frame.f_code))
  489. if sys.version_info >= (3, 11):
  490. from bisect import bisect_left
  491. inst_idx = bisect_left(
  492. inst_list, calling_frame.f_lasti, key=lambda x: x.offset
  493. )
  494. else:
  495. inst_idx = calling_frame.f_lasti // 2
  496. inst = inst_list[inst_idx]
  497. if inst.opname == "UNPACK_SEQUENCE":
  498. return (self[i] for i in range(inst.argval)) # type: ignore[index]
  499. return self.tracer.iter(self)
  500. def __abs__(self):
  501. return self.tracer.create_proxy("call_function", operator.abs, (self,), {})
  502. def __bool__(self) -> bool:
  503. if self.tracer.trace_asserts:
  504. # check if this boolean is used in an assertion, bytecode pattern for assertions
  505. # is pretty stable for Python 3.7--3.9
  506. frame = inspect.currentframe()
  507. assert frame is not None
  508. calling_frame = frame.f_back
  509. assert calling_frame is not None
  510. insts = list(dis.get_instructions(calling_frame.f_code))
  511. if sys.version_info >= (3, 11):
  512. from bisect import bisect_left
  513. cur = bisect_left(insts, calling_frame.f_lasti, key=lambda x: x.offset)
  514. else:
  515. cur = calling_frame.f_lasti // 2
  516. inst = insts[cur]
  517. if inst.opname == "POP_JUMP_IF_TRUE":
  518. first = insts[cur + 1]
  519. assert inst.arg is not None
  520. last = insts[inst.arg // 2 - 1]
  521. starts_with_assert = (
  522. first.opname == "LOAD_GLOBAL"
  523. and first.argval == "AssertionError"
  524. or first.opname == "LOAD_ASSERTION_ERROR"
  525. )
  526. if starts_with_assert and last.opname == "RAISE_VARARGS":
  527. self.tracer.create_proxy("call_function", assert_fn, (self,), {})
  528. return True
  529. return self.tracer.to_bool(self)
  530. @compatibility(is_backward_compatible=True)
  531. def keys(self):
  532. return self.tracer.keys(self)
  533. def __len__(self):
  534. raise RuntimeError(
  535. "'len' is not supported in symbolic tracing by default. If you want "
  536. "this call to be recorded, please call torch.fx.wrap('len') at "
  537. "module scope"
  538. )
  539. @classmethod
  540. def __torch_function__(cls, orig_method, types, args=None, kwargs=None):
  541. args = args if args else ()
  542. kwargs = kwargs if kwargs else {}
  543. tracers: dict[Any, None] = {}
  544. def find_tracer(a):
  545. if isinstance(a, cls):
  546. tracers[a.tracer] = None
  547. map_aggregate(args, find_tracer)
  548. map_aggregate(kwargs, find_tracer)
  549. if len(tracers) > 1:
  550. raise RuntimeError(
  551. f"Found multiple different tracers {list(tracers.keys())} while "
  552. f"trying to trace operations {orig_method}"
  553. )
  554. tracer = next(iter(tracers.keys()))
  555. if isinstance(orig_method, torch._C.ScriptMethod):
  556. args = (orig_method.owner,) + args
  557. return tracer.create_proxy("call_method", orig_method.name, args, kwargs)
  558. if torch.overrides.is_tensor_method_or_property(orig_method):
  559. return tracer.create_proxy(
  560. "call_method", orig_method.__name__, args, kwargs
  561. )
  562. else:
  563. if isinstance(orig_method, torch._ops.HigherOrderOperator):
  564. # TODO: Define how to symbolically trace HigherOrderOperators
  565. raise RuntimeError("Unable to symbolically trace HigherOrderOperators")
  566. return tracer.create_proxy(
  567. "call_function",
  568. orig_method,
  569. args,
  570. kwargs,
  571. name=tracer.graph._target_to_str(orig_method.__name__),
  572. )
  573. @compatibility(is_backward_compatible=False)
  574. class MetaProxy(Proxy):
  575. """
  576. A Proxy subclass that propagates metadata (meta['val']) during graph tracing.
  577. """
  578. def __init__(
  579. self, node: Node, tracer: "Optional[TracerBase]" = None, fake_mode=None
  580. ):
  581. super().__init__(node, tracer)
  582. self.fake_mode = fake_mode
  583. def __repr__(self) -> str:
  584. return f"MetaProxy({self.node.name})"
  585. @classmethod
  586. def __torch_function__(cls, orig_method, types, args=None, kwargs=None):
  587. args = args if args else ()
  588. kwargs = kwargs if kwargs else {}
  589. meta_proxy = None
  590. for arg in args:
  591. if isinstance(arg, MetaProxy):
  592. meta_proxy = arg
  593. break
  594. assert meta_proxy is not None, (
  595. "No MetaProxy found in arguments, but one is expected."
  596. )
  597. proxy = super().__torch_function__(orig_method, types, args, kwargs)
  598. with meta_proxy.fake_mode:
  599. proxy.node.meta["val"] = orig_method(
  600. *[a.node.meta["val"] if isinstance(a, Proxy) else a for a in args],
  601. **kwargs,
  602. )
  603. return MetaProxy(proxy.node, proxy.tracer, meta_proxy.fake_mode)
  604. @compatibility(is_backward_compatible=True)
  605. class Attribute(Proxy):
  606. @compatibility(is_backward_compatible=True)
  607. def __init__(self, root: Proxy, attr: str):
  608. self.root = root
  609. self.attr = attr
  610. self.tracer = root.tracer
  611. self._node: Optional[Node] = None
  612. @property
  613. def node(self):
  614. # the node for attributes is added lazily, since most will just be method calls
  615. # which do not rely on the getitem call
  616. if self._node is None:
  617. self._node = self.tracer.create_proxy(
  618. "call_function", getattr, (self.root, self.attr), {}
  619. ).node
  620. return self._node
  621. def __call__(self, *args, **kwargs):
  622. return self.tracer.create_proxy(
  623. "call_method", self.attr, (self.root,) + args, kwargs
  624. )
  625. @compatibility(is_backward_compatible=False)
  626. class ParameterProxy(Proxy):
  627. """
  628. A special proxy which lets "shape", "size", "dim", and a few other
  629. attribute accesses pass through to the underlying module parameter object,
  630. so that conditional tests on these attributes will not throw exception during tracing
  631. """
  632. def __init__(self, tracer: TracerBase, node: Node, name, param):
  633. super().__init__(node, tracer)
  634. assert isinstance(param, torch.nn.Parameter)
  635. self.param = param
  636. self.name = name
  637. def __repr__(self) -> str:
  638. return f"ParameterProxy({self.name})"
  639. @property
  640. def shape(self):
  641. return self.param.shape
  642. def size(self):
  643. return self.param.size()
  644. def dim(self):
  645. return self.param.dim()
  646. @property
  647. def ndim(self):
  648. return self.param.ndim
  649. def numel(self):
  650. return self.param.numel()
  651. def nelement(self):
  652. return self.param.nelement()
  653. for method in magic_methods:
  654. def _scope(method):
  655. def impl(*args, **kwargs):
  656. tracer = args[0].tracer
  657. target = getattr(operator, method)
  658. return tracer.create_proxy("call_function", target, args, kwargs)
  659. impl.__name__ = method
  660. as_magic = f"__{method.strip('_')}__"
  661. setattr(Proxy, as_magic, impl)
  662. _scope(method)
  663. def _define_reflectable(orig_method_name):
  664. method_name = f"__r{orig_method_name.strip('_')}__"
  665. def impl(self, rhs):
  666. target = getattr(operator, orig_method_name)
  667. return self.tracer.create_proxy("call_function", target, (rhs, self), {})
  668. impl.__name__ = method_name
  669. impl.__qualname__ = method_name
  670. setattr(Proxy, method_name, impl)
  671. for orig_method_name in reflectable_magic_methods:
  672. _define_reflectable(orig_method_name)
  673. def _no_nodes_error(arg):
  674. raise RuntimeError(
  675. "Keys for dictionaries used as an argument cannot contain a "
  676. f"Node. Got key: {arg}"
  677. )
  678. def _create_arg_dict(self, a):
  679. r = {}
  680. for k, v in a.items():
  681. if not isinstance(k, str):
  682. # Check for invalid dict keys. We do not want a Proxy to appear
  683. # anywhere within the key. Since keys can be collection types,
  684. # we iterate through the key with map_arg
  685. k = self.create_arg(k)
  686. map_arg(k, _no_nodes_error)
  687. r[k] = self.create_arg(v)
  688. return r
  689. _create_arg_bypass = {
  690. t: lambda self, a: a
  691. for t in [
  692. *base_types,
  693. type(None),
  694. type(...),
  695. torch._ops.OpOverload,
  696. torch._ops.HigherOrderOperator,
  697. ]
  698. }
  699. _create_arg_bypass[Proxy] = lambda self, a: a.node
  700. _create_arg_bypass[tuple] = lambda self, a: tuple(self.create_arg(elem) for elem in a)
  701. _create_arg_bypass[list] = lambda self, a: [self.create_arg(elem) for elem in a]
  702. _create_arg_bypass[dict] = _create_arg_dict
  703. _create_arg_bypass[immutable_list] = _create_arg_bypass[list]
  704. _create_arg_bypass[immutable_dict] = _create_arg_bypass[dict]