proxy.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. # mypy: ignore-errors
  2. import enum
  3. import dis
  4. import copy
  5. import sys
  6. import torch
  7. import inspect
  8. import operator
  9. import collections
  10. from dataclasses import is_dataclass, fields
  11. from .graph import magic_methods, reflectable_magic_methods, Graph
  12. from torch.utils._traceback import CapturedTraceback
  13. from typing import Tuple, Dict, OrderedDict, Optional, Any, Iterator, Callable
  14. from .node import Target, Node, Argument, base_types, map_aggregate
  15. from ._compatibility import compatibility
  16. from .operator_schemas import check_for_mutable_operation
  17. import torch.fx.traceback as fx_traceback
  18. __all__ = ['TracerBase', 'GraphAppendingTracer', 'TraceError',
  19. 'Proxy', 'Attribute', 'ParameterProxy', 'Scope',
  20. 'ScopeContextManager']
  21. @compatibility(is_backward_compatible=False)
  22. class Scope:
  23. """ Scope object that records the module path and the module type
  24. of a module. Scope is used to track the information of the module
  25. that contains a Node in a Graph of GraphModule. For example::
  26. class Sub(torch.nn.Module):
  27. def forward(self, x):
  28. # This will be a call_method Node in GraphModule,
  29. # scope for this would be (module_path="sub", module_type=Sub)
  30. return x.transpose(1, 2)
  31. class M(torch.nn.Module):
  32. def __init__(self):
  33. self.sub = Sub()
  34. def forward(self, x):
  35. # This will be a call_method Node as well,
  36. # scope for this would be (module_path="", None)
  37. x = x.transpose(1, 2)
  38. x = self.sub(x)
  39. return x
  40. """
  41. def __init__(self, module_path: str, module_type: Any):
  42. super().__init__()
  43. self.module_path = module_path
  44. self.module_type = module_type
  45. @compatibility(is_backward_compatible=False)
  46. class ScopeContextManager:
  47. """ A context manager to track the Scope of Node during symbolic tracing.
  48. When entering a forward function of a Module, we'll update the scope information of
  49. the current module, and when we exit, we'll restore the previous scope information.
  50. """
  51. def __init__(
  52. self,
  53. scope: Scope,
  54. current_scope: Scope,
  55. ):
  56. super().__init__()
  57. # Keep a copy of prev scope to restore on exit
  58. self._prev_scope = copy.copy(scope)
  59. # Update scope to current scope
  60. scope.module_path = current_scope.module_path
  61. scope.module_type = current_scope.module_type
  62. # Save a reference so we can restore it
  63. self._scope = scope
  64. def __enter__(self):
  65. return self._scope
  66. def __exit__(self, *args):
  67. self._scope.module_path = self._prev_scope.module_path
  68. self._scope.module_type = self._prev_scope.module_type
  69. return
  70. _COPY_META_FIELDS = [
  71. "nn_module_stack",
  72. "torch_fn",
  73. "source_fn_stack",
  74. "original_aten",
  75. "recompute",
  76. "from_node",
  77. "quantization_tag",
  78. ]
  79. @compatibility(is_backward_compatible=True)
  80. class TracerBase:
  81. graph: Graph
  82. record_stack_traces : bool = False
  83. # Feature flag for mutable schema checking
  84. # Enableby default in 1.12
  85. check_mutable_operations : bool = False
  86. # Feature flag for assert tracing
  87. trace_asserts : bool = False
  88. # Feature flag for proxying accesses to buffer values
  89. proxy_buffer_attributes : bool = False
  90. # Name of the function to be traced. It will only be used when
  91. # ``root`` is an instance of ``nn.Module``
  92. traced_func_name: str = "forward"
  93. # Maps the containing module's name to the operator name
  94. scope : Scope
  95. # Records the module call stack
  96. module_stack: OrderedDict[str, Tuple[str, Any]]
  97. # Mapping of node name to module scope
  98. node_name_to_scope: Dict[str, Tuple[str, type]]
  99. @compatibility(is_backward_compatible=True)
  100. def create_node(self, kind : str, target : Target,
  101. args : Tuple[Argument, ...], kwargs : Dict[str, Argument], name : Optional[str] = None,
  102. type_expr : Optional[Any] = None) -> Node:
  103. """
  104. Inserts a graph node given target, args, kwargs, and name.
  105. This method can be overridden to do extra checking, validation, or
  106. modification of values used in node creation. For example, one might
  107. want to disallow in-place operations from being recorded.
  108. """
  109. if kind == 'call_function' and self.check_mutable_operations:
  110. check_for_mutable_operation(target, args, kwargs)
  111. node = self.graph.create_node(kind, target, args, kwargs, name, type_expr)
  112. # TODO node_name_to_scope will be depreciated in favor of
  113. # node.meta['nn_module_stack']
  114. self.node_name_to_scope[node.name] = (
  115. self.scope.module_path,
  116. self.scope.module_type,
  117. )
  118. # Optionally set stack trace on the created Node for debugging purposes
  119. if fx_traceback.has_preserved_node_meta():
  120. current_meta: Dict[str, Any] = fx_traceback.get_current_meta()
  121. stack_trace = current_meta.get("stack_trace")
  122. if stack_trace:
  123. node.stack_trace = stack_trace
  124. # Explicitly set the stack_trace, nn_module_stack and source_fn on the node.meta
  125. # If other meta fields are needed, they can be added here
  126. for field in _COPY_META_FIELDS:
  127. if field in current_meta:
  128. node.meta[field] = copy.copy(current_meta[field])
  129. # Here we decrement to account for the sequence_nr having
  130. # just been incremented while tracing this lowered aten op.
  131. new_seq_nr = torch.autograd._get_sequence_nr() - 1
  132. # The sequence_nr increments every time a new autograd Node
  133. # is created. During the FWD pass we store the sequence_nr
  134. # corresponding to the last autograd Node created on this fx
  135. # node's meta. A single aten op can create multiple autograd
  136. # nodes as is the case with in-place foreach ops. During the
  137. # BWD pass we retrieve the sequence_nr stored on the current
  138. # executing autograd Node. See NOTE [ Sequence Number ].
  139. if current_meta.get("in_grad_fn", 0) > 0:
  140. new_seq_nr = current_meta["grad_fn_seq_nr"][-1]
  141. node.meta["seq_nr"] = new_seq_nr
  142. elif self.module_stack:
  143. node.meta['nn_module_stack'] = copy.copy(self.module_stack)
  144. return node
  145. @compatibility(is_backward_compatible=True)
  146. def proxy(self, node: Node) -> 'Proxy':
  147. return Proxy(node, self)
  148. @compatibility(is_backward_compatible=True)
  149. def create_proxy(self, kind: str, target: Target, args: Tuple[Any, ...], kwargs: Dict[str, Any],
  150. name: Optional[str] = None, type_expr : Optional[Any] = None,
  151. proxy_factory_fn: Callable[[Node], 'Proxy'] = None):
  152. '''
  153. Create a Node from the given arguments, then return the Node
  154. wrapped in a Proxy object.
  155. If kind = 'placeholder', then we're creating a Node that
  156. represents the parameter of a function. If we need to encode
  157. a default parameter, we use the ``args`` tuple. ``args`` is
  158. otherwise empty for ``placeholder`` Nodes.
  159. '''
  160. args_ = self.create_arg(args)
  161. kwargs_ = self.create_arg(kwargs)
  162. assert isinstance(args_, tuple)
  163. assert isinstance(kwargs_, dict)
  164. node = self.create_node(kind, target, args_, kwargs_, name, type_expr)
  165. if not proxy_factory_fn:
  166. proxy = self.proxy(node)
  167. else:
  168. proxy = proxy_factory_fn(node)
  169. if self.record_stack_traces and not proxy.node.stack_trace:
  170. proxy.node.stack_trace = ''.join(CapturedTraceback.extract().format())
  171. return proxy
  172. def _find_user_frame(self):
  173. """
  174. Find the Python stack frame executing the user code during
  175. symbolic tracing.
  176. """
  177. # We have to do a little dance here. Basically, walk up the callstack and
  178. # record the first frame not in the pytorch source. This is the frame executing
  179. # the user code during tracing.
  180. frame = inspect.currentframe()
  181. pt_files = ['torch/fx/proxy.py',
  182. 'torch/fx/_symbolic_trace.py',
  183. 'torch/fx/experimental/proxy_tensor.py',
  184. 'torch/_ops.py',
  185. 'torch/_tensor.py',
  186. 'torch/utils/_python_dispatch.py',
  187. 'torch/_prims_common/wrappers.py',
  188. 'torch/_refs/__init__.py',
  189. 'torch/_refs/nn/functional/__init__.py',
  190. 'torch/utils/_stats.py',
  191. ]
  192. while frame:
  193. frame = frame.f_back
  194. if frame and all(not frame.f_code.co_filename.endswith(file) for file in pt_files):
  195. break
  196. if not frame:
  197. return None
  198. return frame
  199. @compatibility(is_backward_compatible=True)
  200. def create_arg(self, a: Any) -> Argument:
  201. """
  202. A method that lowers the objects seen as arguments during symbolic evaluation
  203. into Argument types that can be stored in IR.
  204. Can be override to support more trace-specific types.
  205. """
  206. if not isinstance(a, Proxy) and hasattr(a, '__fx_create_arg__'):
  207. return a.__fx_create_arg__(self)
  208. # aggregates
  209. elif isinstance(a, tuple) and hasattr(a, '_fields'):
  210. # NamedTuple constructors don't seem to like getting a generator
  211. # expression as an argument to their constructor, so build this
  212. # intermediate tuple and unpack it into the NamedTuple constructor
  213. args = tuple(self.create_arg(elem) for elem in a)
  214. return type(a)(*args) # type: ignore[arg-type]
  215. elif isinstance(a, (tuple, list)):
  216. return type(a)(self.create_arg(elem) for elem in a)
  217. elif isinstance(a, dict):
  218. r = {}
  219. for k, v in a.items():
  220. # Check for invalid dict keys. We do not want a Proxy to appear
  221. # anywhere within the key. Since keys can be collection types,
  222. # we iterate through the key with map_aggregate
  223. k = self.create_arg(k)
  224. def no_node(arg):
  225. if isinstance(arg, Node):
  226. raise RuntimeError("Keys for dictionaries used as an argument cannot contain a "
  227. f"Node. Got key: {k}")
  228. map_aggregate(k, no_node)
  229. r[k] = self.create_arg(v)
  230. return r
  231. elif isinstance(a, slice):
  232. return slice(self.create_arg(a.start), self.create_arg(a.stop), self.create_arg(a.step))
  233. elif isinstance(a, range):
  234. return range(self.create_arg(a.start), self.create_arg(a.stop), self.create_arg(a.step))
  235. elif isinstance(a, torch._ops.OpOverload):
  236. return a
  237. if isinstance(a, Proxy):
  238. # base case: we unwrap the Proxy object
  239. return a.node
  240. if is_dataclass(a):
  241. kwargs = {field.name: self.create_arg(getattr(a, field.name)) for field in fields(a)}
  242. return self.create_node("call_function", a.__class__, (), kwargs)
  243. elif isinstance(a, (*base_types, enum.Enum)) or a is None or a is ...:
  244. return a
  245. raise NotImplementedError(f"argument of type: {type(a)}")
  246. @compatibility(is_backward_compatible=True)
  247. def to_bool(self, obj: 'Proxy') -> bool:
  248. """Called when a proxy object is being converted to a boolean, such as
  249. when used in control flow. Normally we don't know what to do because
  250. we don't know the value of the proxy, but a custom tracer can attach more
  251. information to the graph node using create_node and can choose to return a value.
  252. """
  253. raise TraceError('symbolically traced variables cannot be used as inputs to control flow')
  254. @compatibility(is_backward_compatible=True)
  255. def iter(self, obj: 'Proxy') -> Iterator:
  256. """Called when a proxy object is being iterated over, such as
  257. when used in control flow. Normally we don't know what to do because
  258. we don't know the value of the proxy, but a custom tracer can attach more
  259. information to the graph node using create_node and can choose to return an iterator.
  260. """
  261. raise TraceError('Proxy object cannot be iterated. This can be '
  262. 'attempted when the Proxy is used in a loop or'
  263. ' as a *args or **kwargs function argument. '
  264. 'See the torch.fx docs on pytorch.org for a '
  265. 'more detailed explanation of what types of '
  266. 'control flow can be traced, and check out the'
  267. ' Proxy docstring for help troubleshooting '
  268. 'Proxy iteration errors')
  269. @compatibility(is_backward_compatible=True)
  270. def keys(self, obj: 'Proxy') -> Any:
  271. """Called when a proxy object is has the keys() method called.
  272. This is what happens when ** is called on a proxy. This should return an
  273. iterator it ** is suppose to work in your custom tracer.
  274. """
  275. return Attribute(obj, 'keys')()
  276. # used in Proxy object when just appending to the graph while not tracing.
  277. @compatibility(is_backward_compatible=True)
  278. class GraphAppendingTracer(TracerBase):
  279. def __init__(self, graph: Graph):
  280. super().__init__()
  281. self.graph = graph
  282. self.scope = Scope("", None)
  283. self.module_stack = collections.OrderedDict()
  284. self.node_name_to_scope = {}
  285. @compatibility(is_backward_compatible=False)
  286. def assert_fn(x):
  287. assert x
  288. @compatibility(is_backward_compatible=True)
  289. class TraceError(ValueError):
  290. pass
  291. @compatibility(is_backward_compatible=True)
  292. class Proxy:
  293. """
  294. ``Proxy`` objects are ``Node`` wrappers that flow through the
  295. program during symbolic tracing and record all the operations
  296. (``torch`` function calls, method calls, operators) that they touch
  297. into the growing FX Graph.
  298. If you're doing graph transforms, you can wrap your own ``Proxy``
  299. method around a raw ``Node`` so that you can use the overloaded
  300. operators to add additional things to a ``Graph``.
  301. ``Proxy`` objects cannot be iterated. In other words, the symbolic
  302. tracer will throw an error if a ``Proxy`` is used in a loop or as
  303. an ``*args``/``**kwargs`` function argument.
  304. There are two main ways around this:
  305. 1. Factor out the untraceable logic into a top-level function and
  306. use ``fx.wrap`` on it.
  307. 2. If the control flow is static (i.e. the loop trip count is
  308. based on some hyperparameter), the code can be kept in its original
  309. position and refactored into something like::
  310. for i in range(self.some_hyperparameter):
  311. indexed_item = proxied_value[i]
  312. For a more detailed description into the Proxy internals, check out
  313. the "Proxy" section in `torch/fx/README.md`
  314. """
  315. @compatibility(is_backward_compatible=True)
  316. def __init__(self, node: Node, tracer: 'Optional[TracerBase]' = None):
  317. if tracer is None:
  318. # This allows you to create a Proxy object around a raw Node
  319. tracer = GraphAppendingTracer(node.graph)
  320. self.tracer = tracer
  321. self.node = node
  322. def __repr__(self) -> str:
  323. return f'Proxy({self.node.name})'
  324. def __getattr__(self, k) -> 'Attribute':
  325. # note: not added to the graph yet, if this is a method call
  326. # we peephole optimize to the method invocation
  327. return Attribute(self, k)
  328. def __call__(self, *args, **kwargs) -> 'Proxy':
  329. return self.tracer.create_proxy('call_method', '__call__', (self,) + args, kwargs)
  330. def __iter__(self) -> Iterator['Proxy']:
  331. frame = inspect.currentframe()
  332. assert frame is not None
  333. calling_frame = frame.f_back
  334. assert calling_frame is not None
  335. inst_list = list(dis.get_instructions(calling_frame.f_code))
  336. if sys.version_info >= (3, 11):
  337. from bisect import bisect_left
  338. inst_idx = bisect_left(inst_list, calling_frame.f_lasti, key=lambda x: x.offset)
  339. else:
  340. inst_idx = calling_frame.f_lasti // 2
  341. inst = inst_list[inst_idx]
  342. if inst.opname == 'UNPACK_SEQUENCE':
  343. return (self[i] for i in range(inst.argval)) # type: ignore[index]
  344. return self.tracer.iter(self)
  345. def __abs__(self):
  346. return self.tracer.create_proxy('call_function', operator.abs, (self,), {})
  347. def __bool__(self) -> bool:
  348. if self.tracer.trace_asserts:
  349. # check if this boolean is used in an assertion, bytecode pattern for assertions
  350. # is pretty stable for Python 3.7--3.9
  351. frame = inspect.currentframe()
  352. assert frame is not None
  353. calling_frame = frame.f_back
  354. assert calling_frame is not None
  355. insts = list(dis.get_instructions(calling_frame.f_code))
  356. if sys.version_info >= (3, 11):
  357. from bisect import bisect_left
  358. cur = bisect_left(insts, calling_frame.f_lasti, key=lambda x: x.offset)
  359. else:
  360. cur = calling_frame.f_lasti // 2
  361. inst = insts[cur]
  362. if inst.opname == 'POP_JUMP_IF_TRUE':
  363. first = insts[cur + 1]
  364. assert inst.arg is not None
  365. last = insts[inst.arg // 2 - 1]
  366. starts_with_assert = (first.opname == 'LOAD_GLOBAL' and first.argval == 'AssertionError'
  367. or first.opname == 'LOAD_ASSERTION_ERROR')
  368. if starts_with_assert and last.opname == 'RAISE_VARARGS':
  369. self.tracer.create_proxy('call_function', assert_fn, (self,), {})
  370. return True
  371. return self.tracer.to_bool(self)
  372. @compatibility(is_backward_compatible=True)
  373. def keys(self):
  374. return self.tracer.keys(self)
  375. def __len__(self):
  376. raise RuntimeError("'len' is not supported in symbolic tracing by default. If you want "
  377. "this call to be recorded, please call torch.fx.wrap('len') at "
  378. "module scope")
  379. @classmethod
  380. def __torch_function__(cls, orig_method, types, args=None, kwargs=None):
  381. args = args if args else ()
  382. kwargs = kwargs if kwargs else {}
  383. tracers : Dict[Any, None] = {}
  384. def find_tracer(a):
  385. if isinstance(a, cls):
  386. tracers[a.tracer] = None
  387. torch.fx.node.map_aggregate(args, find_tracer)
  388. torch.fx.node.map_aggregate(kwargs, find_tracer)
  389. if len(tracers) > 1:
  390. raise RuntimeError(f'Found multiple different tracers {list(tracers.keys())} while '
  391. f'trying to trace operations {orig_method}')
  392. tracer = next(iter(tracers.keys()))
  393. if isinstance(orig_method, torch._C.ScriptMethod):
  394. args = (orig_method.owner,) + args
  395. return tracer.create_proxy('call_method', orig_method.name, args, kwargs)
  396. if torch.overrides.is_tensor_method_or_property(orig_method):
  397. return tracer.create_proxy('call_method', orig_method.__name__, args, kwargs)
  398. else:
  399. if isinstance(orig_method, torch._ops.HigherOrderOperator):
  400. # TODO: Define how to symbolically trace HigherOrderOperators
  401. raise RuntimeError("Unable to symbolically trace HigherOrderOperators")
  402. return tracer.create_proxy('call_function', orig_method, args, kwargs,
  403. name=tracer.graph._target_to_str(orig_method.__name__))
  404. @compatibility(is_backward_compatible=True)
  405. class Attribute(Proxy):
  406. @compatibility(is_backward_compatible=True)
  407. def __init__(self, root: Proxy, attr: str):
  408. self.root = root
  409. self.attr = attr
  410. self.tracer = root.tracer
  411. self._node: Optional[Node] = None
  412. @property
  413. def node(self):
  414. # the node for attributes is added lazily, since most will just be method calls
  415. # which do not rely on the getitem call
  416. if self._node is None:
  417. self._node = self.tracer.create_proxy('call_function', getattr, (self.root, self.attr), {}).node
  418. return self._node
  419. def __call__(self, *args, **kwargs):
  420. return self.tracer.create_proxy('call_method', self.attr, (self.root,) + args, kwargs)
  421. @compatibility(is_backward_compatible=False)
  422. class ParameterProxy(Proxy):
  423. """
  424. A special proxy which lets "shape", "size", "dim", and a few other
  425. attribute accesses pass through to the underlying module parameter object,
  426. so that conditional tests on these attributes will not throw exception during tracing
  427. """
  428. def __init__(self, tracer: TracerBase, node: Node, name, param):
  429. super().__init__(node, tracer)
  430. assert isinstance(param, torch.nn.Parameter)
  431. self.param = param
  432. self.name = name
  433. def __repr__(self) -> str:
  434. return f'ParameterProxy({self.name})'
  435. @property
  436. def shape(self):
  437. return self.param.shape
  438. def size(self):
  439. return self.param.size()
  440. def dim(self):
  441. return self.param.dim()
  442. @property
  443. def ndim(self):
  444. return self.param.ndim
  445. def numel(self):
  446. return self.param.numel()
  447. def nelement(self):
  448. return self.param.nelement()
  449. for method in magic_methods:
  450. def _scope(method):
  451. def impl(*args, **kwargs):
  452. tracer = args[0].tracer
  453. target = getattr(operator, method)
  454. return tracer.create_proxy('call_function', target, args, kwargs)
  455. impl.__name__ = method
  456. as_magic = f'__{method.strip("_")}__'
  457. setattr(Proxy, as_magic, impl)
  458. _scope(method)
  459. def _define_reflectable(orig_method_name):
  460. method_name = f'__r{orig_method_name.strip("_")}__'
  461. def impl(self, rhs):
  462. target = getattr(operator, orig_method_name)
  463. return self.tracer.create_proxy('call_function', target, (rhs, self), {})
  464. impl.__name__ = method_name
  465. impl.__qualname__ = method_name
  466. setattr(Proxy, method_name, impl)
  467. for orig_method_name in reflectable_magic_methods:
  468. _define_reflectable(orig_method_name)