11import inspect
2- from typing import Any , Callable , Dict , List , Optional
2+ from typing import Any , Callable , Dict , List , Optional , Set
3+ from collections import OrderedDict
4+ import logging
35
46import torch
57from torch .fx ._compatibility import compatibility
68from torch .fx .graph_module import GraphModule
79from torch .fx .node import Node
810
911__all__ = ["Partition" , "split_module" ]
10-
12+ _LOGGER = logging . getLogger ( __name__ )
1113
1214@compatibility (is_backward_compatible = True )
1315class Partition :
@@ -194,6 +196,76 @@ def instantiate_node_partition_mapping(node):
194196 partition .node_names .append (node .name )
195197 node ._fx_partition = partition_name
196198
199+ # Global State Nodes are nodes which by their global state effects,
200+ # "taint" all downstream nodes while they are active.
201+ GLOBAL_STATE_NODES = [
202+ torch .amp ._enter_autocast ,
203+ torch .amp ._exit_autocast ,
204+ torch ._C ._set_grad_enabled
205+ ]
206+
207+ # For grad regions:
208+ # ------------------------
209+ # 1. first region: we do nothing
210+ # 2. subsequent regions: we insert the set_grad at the beginning
211+ grad_regions : OrderedDict [Node , Set [int ]] = OrderedDict ()
212+
213+ # For autocast regions:
214+ # ------------------------
215+ # 1. first region: we will only insert the _exit at the end
216+ # 2. intermediate regions: we will insert both the
217+ # _enter at the beginning and _exit at the end
218+ # 3. last region: we will only insert _enter at the beginning
219+ # We will do so in the order in which the autocasts were instantiated.
220+ autocast_regions : OrderedDict [Node , Set [int ]] = OrderedDict ()
221+ autocast_exits : Dict [Node , Optional [Node ]] = {}
222+
223+ active_grad = None
224+ active_autocasts = set ()
225+
226+ for node in m .graph .nodes :
227+ if node .op in ["placeholder" , "get_attr" , "output" ]:
228+ continue
229+
230+ instantiate_node_partition_mapping (node )
231+
232+ if node .op == "call_function" and node .target in GLOBAL_STATE_NODES :
233+ if node .target == torch ._C ._set_grad_enabled :
234+ assert len (node .args ) == 1
235+ assert isinstance (node .args [0 ], bool )
236+ active_grad = node
237+ grad_regions [active_grad ] = set ({split_callback (node )})
238+ elif node .target == torch .amp ._enter_autocast :
239+ # Should all be python constants
240+ assert all (not isinstance (arg , Node ) for arg in node .args )
241+ active_autocasts .add (node )
242+ autocast_regions [node ] = set ({split_callback (node )})
243+ autocast_exits [node ] = None
244+ elif node .target == torch .amp ._exit_autocast :
245+ assert len (node .args ) == 1
246+ autocast_regions [node .args [0 ]].add (split_callback (node ))
247+ active_autocasts .remove (node .args [0 ])
248+ autocast_exits [node .args [0 ]] = node
249+
250+ if active_grad is not None :
251+ grad_regions [active_grad ].add (split_callback (node ))
252+
253+ for a in active_autocasts :
254+ autocast_regions [a ].add (split_callback (node ))
255+
256+ assert all (v is not None for v in autocast_exits .values ()), "autocast must exit"
257+
258+ autocast_regions = {k : sorted (v ) for k , v in autocast_regions .items ()}
259+ grad_regions = {k : sorted (v ) for k , v in grad_regions .items ()}
260+
261+ if _LOGGER .isEnabledFor (logging .DEBUG ):
262+ _LOGGER .debug ("autocast_regions: %s" , autocast_regions )
263+ _LOGGER .debug ("grad_regions: %s" , grad_regions )
264+
265+ assert_monotonically_increasing = bool (autocast_regions ) or bool (grad_regions )
266+
267+ # split nodes into partitions
268+ highest_partition = - 1
197269 for node in m .graph .nodes :
198270 orig_nodes [node .name ] = node
199271
@@ -207,14 +279,22 @@ def instantiate_node_partition_mapping(node):
207279 )
208280 continue
209281
210- instantiate_node_partition_mapping (node )
282+ if assert_monotonically_increasing :
283+ pid = split_callback (node )
284+ assert highest_partition <= pid ,\
285+ ("autocast or set_grad_enabled require monotonically increasing partitions:"
286+ f"highest: { highest_partition } , this node's: { pid } " )
287+ highest_partition = pid
211288
212- torch .fx .graph .map_arg (
213- node .args , lambda def_node : record_cross_partition_use (def_node , node )
214- )
215- torch .fx .graph .map_arg (
216- node .kwargs , lambda def_node : record_cross_partition_use (def_node , node )
217- ) # noqa: B950
289+ # do not capture cross-partition dependencies for global state nodes as they will be
290+ # self-contained - their setup and unwind will be isolated to each partition submodule.
291+ if node .target not in GLOBAL_STATE_NODES :
292+ torch .fx .graph .map_arg (
293+ node .args , lambda def_node : record_cross_partition_use (def_node , node )
294+ )
295+ torch .fx .graph .map_arg (
296+ node .kwargs , lambda def_node : record_cross_partition_use (def_node , node )
297+ ) # noqa: B950
218298
219299 original_partition_order = list (partitions .keys ())
220300 # find partitions with no dependencies
@@ -235,6 +315,23 @@ def instantiate_node_partition_mapping(node):
235315 if len (sorted_partitions ) != len (partitions ):
236316 raise RuntimeError ("cycle exists between partitions!" )
237317
318+ # Enter prelude
319+ for regions_mapping in [autocast_regions , grad_regions ]:
320+ for node , regions in regions_mapping .items ():
321+ assert len (regions ) > 0
322+ partitions [str (regions [0 ])].environment [node ] = node
323+ for r in regions [1 :]:
324+ partition = partitions [str (r )]
325+ new_node = partition .graph .create_node (
326+ op = node .op ,
327+ target = node .target ,
328+ args = tuple (arg for arg in node .args ),
329+ kwargs = {},
330+ type_expr = node .type ,
331+ )
332+ new_node .meta = node .meta .copy () # is it really a good idea to copy this?
333+ partition .environment [node ] = new_node
334+
238335 # add placeholders to partition inputs
239336 for partition_name in sorted_partitions :
240337 partition = partitions [partition_name ]
@@ -289,6 +386,24 @@ def instantiate_node_partition_mapping(node):
289386 new_node .meta = node .meta .copy ()
290387 partition .environment [node ] = new_node
291388
389+ # Exit epilogue
390+ for regions_mapping in [autocast_regions ]:
391+ for node in reversed (regions_mapping ):
392+ regions = regions_mapping [node ]
393+ assert len (regions ) > 0
394+ for r in regions [:- 1 ]:
395+ partition = partitions [str (r )]
396+ exit_node = autocast_exits [node ]
397+ assert exit_node is not None , "Missing exit node"
398+ new_node = partition .graph .create_node (
399+ op = exit_node .op ,
400+ target = exit_node .target ,
401+ args = (partition .environment [node ],),
402+ kwargs = {},
403+ type_expr = exit_node .type ,
404+ )
405+ new_node .meta = exit_node .meta .copy () # is it really a good idea to copy this?
406+
292407 # original module environment dict mapping node names to nodes
293408 orig_mod_env : Dict [str , Node ] = {}
294409 # Set up values to construct base module
0 commit comments