99
1010import torch
1111from executorch .backends .arm ._passes .arm_pass import ArmPass
12+ from executorch .exir .dialects ._ops import ops as exir_ops
1213from executorch .exir .pass_base import ExportPass , PassResult
13- from torch ._export .utils import is_buffer
14+ from torch ._export .utils import is_buffer , is_lifted_tensor_constant
1415from torch .export import ExportedProgram
1516
1617logger = logging .getLogger (__name__ )
1718
1819
1920class CastInt64BuffersToInt32Pass (ArmPass ):
20- """Cast int64 buffers to int32 if the int64 data is in int32 range."""
21+ """Cast int64 buffers and lifted tensor constants to int32 when in range."""
2122
2223 _passes_required_after : Set [Type [ExportPass ]] = set ()
2324
2425 def __init__ (self , exported_program : ExportedProgram , * args , ** kwargs ):
2526 super ().__init__ (* args , ** kwargs )
2627 self .exported_program = exported_program
2728
28- def should_run_pass (self , graph_module : torch .fx .GraphModule ) -> bool :
29- for node in graph_module .graph .nodes :
30- if len (node .users ) == 0 :
31- continue
32- fake_tensor = node .meta .get ("val" )
33- if not isinstance (fake_tensor , torch ._subclasses .fake_tensor .FakeTensor ):
34- continue
35- if fake_tensor .dtype == torch .int64 and is_buffer (
36- self .exported_program , node
37- ):
29+ @staticmethod
30+ def _is_index_tensor_input (node : torch .fx .Node ) -> bool :
31+ """Return True only when a constant is used exclusively as an index.
32+
33+ ``node.users`` tracks consumer operations, not operand occurrences.
34+ A consumer can use this node as both indexed data and an index.
35+
36+ """
37+ index_ops = {
38+ torch .ops .aten .index .Tensor ,
39+ exir_ops .edge .aten .index .Tensor ,
40+ }
41+
42+ def contains_node (value : object ) -> bool :
43+ if value is node :
3844 return True
39- return False
45+ if isinstance (value , (tuple , list )):
46+ return any (contains_node (item ) for item in value )
47+ if isinstance (value , dict ):
48+ return any (contains_node (item ) for item in value .values ())
49+ if isinstance (value , slice ):
50+ return any (
51+ contains_node (item )
52+ for item in (value .start , value .stop , value .step )
53+ )
54+ return False
55+
56+ if not node .users :
57+ return False
58+ for user in node .users :
59+ if (
60+ user .op != "call_function"
61+ or user .target not in index_ops
62+ or len (user .args ) < 2
63+ or not isinstance (user .args [1 ], (list , tuple ))
64+ or not any (index is node for index in user .args [1 ])
65+ ):
66+ return False
67+
68+ # args[1] is the index-list operand. All other uses are unsafe.
69+ if any (
70+ contains_node (argument )
71+ for i , argument in enumerate (user .args )
72+ if i != 1
73+ ) or contains_node (user .kwargs ):
74+ return False
75+ return True
76+
77+ def _is_cast_candidate (self , node : torch .fx .Node ) -> bool :
78+ """Select int64 buffers and index-only lifted tensor constants."""
79+ if not node .users :
80+ return False
81+ fake_tensor = node .meta .get ("val" )
82+ if not isinstance (fake_tensor , torch ._subclasses .fake_tensor .FakeTensor ):
83+ return False
84+ if fake_tensor .dtype != torch .int64 :
85+ return False
86+ if is_buffer (self .exported_program , node ):
87+ return True
88+ return is_lifted_tensor_constant (
89+ self .exported_program , node
90+ ) and self ._is_index_tensor_input (node )
91+
92+ def should_run_pass (self , graph_module : torch .fx .GraphModule ) -> bool :
93+ return any (self ._is_cast_candidate (node ) for node in graph_module .graph .nodes )
4094
4195 def _assert_within_int32 (self , tensor : torch .Tensor , node : torch .fx .Node ):
96+ # An empty tensor has no values outside the int32 range.
97+ if tensor .numel () == 0 :
98+ return
99+
42100 if torch .min (tensor ) < torch .iinfo (torch .int32 ).min :
43101 raise RuntimeError (
44102 f"Node { node .name } has value < { torch .iinfo (torch .int32 ).min } "
@@ -48,44 +106,42 @@ def _assert_within_int32(self, tensor: torch.Tensor, node: torch.fx.Node):
48106 f"Node { node .name } has value > { torch .iinfo (torch .int32 ).max } "
49107 )
50108
51- def _cast_buffer_to_int32 (self , node : torch .fx .Node ) -> bool :
52- buffer_name = self .exported_program .graph_signature .inputs_to_buffers [node .name ]
53- if buffer_name in self .exported_program .state_dict :
54- store = self .exported_program .state_dict
55- elif buffer_name in self .exported_program .constants :
56- # Non-persistent buffers are tracked by inputs_to_buffers, but their
57- # tensor values live in ExportedProgram.constants instead of the
58- # state_dict. Examples include transformer position_ids buffers.
109+ def _cast_constant_to_int32 (self , node : torch .fx .Node ) -> bool :
110+ signature = self .exported_program .graph_signature
111+ if is_buffer (self .exported_program , node ):
112+ constant_name = signature .inputs_to_buffers [node .name ]
113+ # Persistent buffers live in state_dict, non-persistent ones in constants.
114+ store = (
115+ self .exported_program .state_dict
116+ if constant_name in self .exported_program .state_dict
117+ else self .exported_program .constants
118+ )
119+ elif is_lifted_tensor_constant (self .exported_program , node ):
120+ constant_name = signature .inputs_to_lifted_tensor_constants [node .name ]
59121 store = self .exported_program .constants
60122 else :
123+ return False
124+
125+ if constant_name not in store :
61126 logger .warning (
62- "Skipping int64 buffer %s (%s): value not found in state_dict "
63- "or constants" ,
127+ "Skipping int64 constant %s (%s): value not found in its storage" ,
64128 node .name ,
65- buffer_name ,
129+ constant_name ,
66130 )
67131 return False
68132
69- buffer = store [buffer_name ]
70- self ._assert_within_int32 (buffer , node )
71- store [buffer_name ] = buffer .to (torch .int32 )
133+ value = store [constant_name ]
134+ # Do not truncate values outside the signed int32 range.
135+ self ._assert_within_int32 (value , node )
136+ store [constant_name ] = value .to (torch .int32 )
72137 node .meta ["val" ] = node .meta ["val" ].to (torch .int32 )
73138 return True
74139
75140 def _to_int32 (self , graph_module : torch .fx .GraphModule ) -> int :
76141 converted = 0
77142 for node in graph_module .graph .nodes :
78- if len (node .users ) == 0 :
79- continue
80- if "val" not in node .meta :
81- continue
82- fake_tensor = node .meta ["val" ]
83- if not isinstance (fake_tensor , torch ._subclasses .fake_tensor .FakeTensor ):
84- continue
85- if fake_tensor .dtype != torch .int64 :
86- continue
87- if is_buffer (self .exported_program , node ):
88- converted += self ._cast_buffer_to_int32 (node )
143+ if self ._is_cast_candidate (node ):
144+ converted += self ._cast_constant_to_int32 (node )
89145 return converted
90146
91147 def call (self , graph_module : torch .fx .GraphModule ):
@@ -94,7 +150,8 @@ def call(self, graph_module: torch.fx.GraphModule):
94150 if modified :
95151 graph_module = super ().call (graph_module ).graph_module
96152 logger .warning (
97- "CastInt64BuffersToInt32Pass: cast %d int64 buffer(s) to int32." ,
153+ "CastInt64BuffersToInt32Pass: cast %d int64 buffer(s) or "
154+ "lifted tensor constant(s) to int32." ,
98155 converted ,
99156 )
100157 return PassResult (graph_module , modified )
0 commit comments