44# LICENSE file in the root directory of this source tree.
55
66
7- from typing import Set , Type
7+ from typing import cast , Literal , Set , Type
88
99import torch
1010from executorch .backends .arm ._passes .arm_pass import ArmOpTargetedPass
1111from executorch .backends .arm ._passes .decompose_div_pass import DecomposeDivPass
12+ from executorch .backends .arm .tosa .specification import get_context_spec
1213from executorch .exir .dialects ._ops import ops as exir_ops
1314from executorch .exir .pass_base import ExportPass
1415
1516edge_div_mode_ops = (exir_ops .edge .aten .div .Tensor_mode ,)
1617aten_div_mode_ops = (torch .ops .aten .div .Tensor_mode ,)
18+ RoundingMode = Literal ["trunc" , "floor" ]
1719
1820edge_unary = {
1921 "div" : exir_ops .edge .aten .div .Tensor ,
2022 "floor" : exir_ops .edge .aten .floor .default ,
2123 "ceil" : exir_ops .edge .aten .ceil .default ,
24+ "eq" : exir_ops .edge .aten .eq .Tensor ,
2225 "full" : exir_ops .edge .aten .full .default ,
2326 "gt" : exir_ops .edge .aten .gt .Tensor ,
27+ "logical_and" : exir_ops .edge .aten .logical_and .default ,
28+ "logical_not" : exir_ops .edge .aten .logical_not .default ,
29+ "logical_xor" : exir_ops .edge .aten .logical_xor .default ,
30+ "intdiv" : exir_ops .backend .tosa .INTDIV .default ,
31+ "mul" : exir_ops .edge .aten .mul .Tensor ,
32+ "sub" : exir_ops .edge .aten .sub .Tensor ,
33+ "to" : exir_ops .edge .dim_order_ops ._to_dim_order_copy .default ,
2434 "where" : exir_ops .edge .aten .where .self ,
2535}
2636
2737aten_unary = {
2838 "div" : torch .ops .aten .div .Tensor ,
2939 "floor" : torch .ops .aten .floor .default ,
3040 "ceil" : torch .ops .aten .ceil .default ,
41+ "eq" : torch .ops .aten .eq .Tensor ,
3142 "full" : torch .ops .aten .full .default ,
3243 "gt" : torch .ops .aten .gt .Tensor ,
44+ "logical_and" : torch .ops .aten .logical_and .default ,
45+ "logical_not" : torch .ops .aten .logical_not .default ,
46+ "logical_xor" : torch .ops .aten .logical_xor .default ,
47+ "mul" : torch .ops .aten .mul .Tensor ,
48+ "sub" : torch .ops .aten .sub .Tensor ,
49+ "to" : torch .ops .aten .to .dtype ,
3350 "where" : torch .ops .aten .where .self ,
3451}
3552
@@ -43,9 +60,9 @@ def _get_opset(op):
4360
4461
4562class DecomposeDivTensorModePass (ArmOpTargetedPass ):
46- """Rewrites aten.div.Tensor_mode into.
63+ """Rewrites aten.div.Tensor_mode into supported arithmetic ops .
4764
48- Example :
65+ Floating-point flow :
4966 rounding_mode=None -> div(a, b)
5067 rounding_mode="floor" -> floor(div(a, b))
5168 rounding_mode="trunc" -> where(
@@ -54,48 +71,213 @@ class DecomposeDivTensorModePass(ArmOpTargetedPass):
5471 floor(div(a, b)),
5572 )
5673
74+ Integer flow:
75+ During transform-for-annotation, keep div.Tensor_mode intact, don't quantize it.
76+ During backend lowering, rewrite the div to a TOSA INTDIV (corresponding to trunc rounding_mode)
77+ + correcting factor for floor mode.
78+
5779 """
5880
5981 _passes_required_after : Set [Type [ExportPass ]] = {DecomposeDivPass }
6082 target_ops = edge_div_mode_ops + aten_div_mode_ops
6183 check_allowed_to_transform = True
6284
85+ def _is_integer_tensor (self , arg ) -> bool :
86+ data = getattr (arg , "data" , None )
87+ if data is not None :
88+ return arg .data .dtype in {
89+ torch .uint8 ,
90+ torch .int8 ,
91+ torch .int16 ,
92+ torch .int32 ,
93+ torch .int64 ,
94+ }
95+ return isinstance (arg , int )
96+
97+ def _cast (self , opset , arg , dtype : torch .dtype , meta ):
98+ if isinstance (arg , int ):
99+ if dtype .is_floating_point :
100+ return float (arg )
101+ else :
102+ return arg
103+ if isinstance (arg , float ):
104+ if dtype .is_floating_point :
105+ return arg
106+ else :
107+ return int (arg )
108+ data = getattr (arg , "data" , None )
109+ if data is not None and data .dtype == dtype :
110+ return arg
111+ return super ().call_operator (
112+ opset ["to" ],
113+ (arg ,),
114+ {"dtype" : dtype },
115+ meta ,
116+ updated = True ,
117+ )
118+
119+ def _full (self , opset , value , dtype : torch .dtype , meta ):
120+ return super ().call_operator (
121+ opset ["full" ],
122+ args = ((1 ,) * len (meta ["val" ].size ()), value ),
123+ kwargs = {"dtype" : dtype , "device" : meta ["val" ].device },
124+ meta = meta ,
125+ updated = True ,
126+ )
127+
128+ def _correct_intdiv_floor (
129+ self , opset , numerator , denominator , trunced_quotient , meta
130+ ):
131+ """Apply a correcting factor for converting the truncated division to
132+ floored division.
133+
134+ Done by subtracting one from the result when, elementwise,
135+ - The remainder is nonzero (otherwise the division is even and the rounding trivial)
136+ - The numerator and denominator have different signs (causing a negative quotient)
137+ The sign of the quotient can't be checked directly, there are cases when it is 0 and still needs correction.
138+
139+ """
140+ # Condition 1: non-zero remainder
141+ product = super ().call_operator (
142+ opset ["mul" ], (trunced_quotient , denominator ), {}, meta , updated = True
143+ )
144+ remainder = super ().call_operator (
145+ opset ["sub" ], (numerator , product ), {}, meta , updated = True
146+ )
147+ zero = self ._full (opset , 0 , torch .int32 , meta )
148+ remainder_is_zero = super ().call_operator (
149+ opset ["eq" ], (remainder , zero ), {}, meta , updated = True
150+ )
151+ remainder_is_nonzero = super ().call_operator (
152+ opset ["logical_not" ], (remainder_is_zero ,), {}, meta , updated = True
153+ )
154+ # Condition 2: un-rounded quotient is negative
155+ a_is_negative = super ().call_operator (
156+ opset ["gt" ], (zero , numerator ), {}, meta , updated = True
157+ )
158+ b_is_negative = super ().call_operator (
159+ opset ["gt" ], (zero , denominator ), {}, meta , updated = True
160+ )
161+ signs_differ = super ().call_operator (
162+ opset ["logical_xor" ],
163+ (a_is_negative , b_is_negative ),
164+ {},
165+ meta ,
166+ updated = True ,
167+ )
168+ # Use conditions to correct quotient.
169+ needs_correction = super ().call_operator (
170+ opset ["logical_and" ],
171+ (remainder_is_nonzero , signs_differ ),
172+ {},
173+ meta ,
174+ updated = True ,
175+ )
176+ # (TOSA spec enforces that int(bool_var) == 1 ? bool_var : 0)
177+ correction = self ._cast (opset , needs_correction , torch .int32 , meta )
178+ return super ().call_operator (
179+ opset ["sub" ], (trunced_quotient , correction ), {}, meta , updated = True
180+ )
181+
182+ def _call_integer_div (self , opset , a , b , rounding_mode : RoundingMode , meta ):
183+ """Cast inputs to int32, do TOSA INTDIV, and apply correcting factor for
184+ floor rounding mode.
185+ """
186+
187+ a_int32 = self ._cast (opset , a , torch .int32 , meta )
188+ b_int32 = self ._cast (opset , b , torch .int32 , meta )
189+ intdiv = super ().call_operator (
190+ opset ["intdiv" ],
191+ (a_int32 , b_int32 ),
192+ {},
193+ meta ,
194+ updated = True ,
195+ )
196+ if rounding_mode == "floor" :
197+ intdiv = self ._correct_intdiv_floor (opset , a_int32 , b_int32 , intdiv , meta )
198+
199+ output_dtype = meta ["val" ].dtype
200+ return self ._cast (opset , intdiv , output_dtype , meta )
201+
202+ def _call_fp_div (self , opset , a , b , rounding_mode : RoundingMode | None , meta ):
203+ q = super ().call_operator (opset ["div" ], (a , b ), {}, meta , updated = True )
204+
205+ match rounding_mode :
206+ case None :
207+ return q
208+ case "floor" :
209+ return super ().call_operator (
210+ opset ["floor" ], (q ,), {}, meta , updated = True
211+ )
212+ case "trunc" :
213+ zero = self ._full (opset , 0.0 , torch .float32 , meta )
214+ is_neg = super ().call_operator (
215+ opset ["gt" ], (zero , q ), {}, meta , updated = True
216+ )
217+ ceilq = super ().call_operator (
218+ opset ["ceil" ], (q ,), {}, meta , updated = True
219+ )
220+ floorq = super ().call_operator (
221+ opset ["floor" ], (q ,), {}, meta , updated = True
222+ )
223+ return super ().call_operator (
224+ opset ["where" ], (is_neg , ceilq , floorq ), {}, meta , updated = True
225+ )
226+
63227 def call_operator (self , op , args , kwargs , meta ):
64228 if op not in self .target_ops or not self .allowed_to_transform (meta ):
65229 return super ().call_operator (op , args , kwargs , meta )
66230
67231 opset = _get_opset (op )
68232
69233 a , b = args [0 ], args [1 ]
234+ a_is_int = self ._is_integer_tensor (a )
235+ b_is_int = self ._is_integer_tensor (b )
70236 rounding_mode = kwargs .get ("rounding_mode" , None )
71237 if rounding_mode is None and len (args ) > 2 :
72238 rounding_mode = args [2 ]
239+ if rounding_mode not in ("floor" , "trunc" , None ):
240+ raise RuntimeError (
241+ "Integer div.Tensor_mode requires rounding_mode floor, trunc, or None."
242+ f"got { rounding_mode !r} "
243+ )
244+ rounding_mode = cast (RoundingMode | None , rounding_mode )
73245
74- q = super ().call_operator (opset ["div" ], (a , b ), {}, meta , updated = True )
246+ int_operation = rounding_mode is not None and a_is_int and b_is_int
247+ sufficient_int_support = (
248+ rounding_mode == "trunc" or get_context_spec ().support_integer ()
249+ )
250+ sufficient_int_support &= not get_context_spec ().is_U55_subset
75251
76- if rounding_mode is None :
77- return q
252+ if int_operation and sufficient_int_support :
253+ """Integer operation and necessary int ops supported -> pure integer
254+ path.
255+ """
256+ if self .is_tfa_pass :
257+ # No quantization neccessary, so don't do anything in TFA.
258+ return super ().call_operator (op , args , kwargs , meta )
259+ return self ._call_integer_div (opset , a , b , rounding_mode , meta )
260+ else :
261+ """Otherwise floating point operation -> do fp path.
78262
79- if rounding_mode == "floor" :
80- return super ().call_operator (opset ["floor" ], (q ,), {}, meta , updated = True )
81-
82- if rounding_mode == "trunc" :
83- zero = super ().call_operator (
84- opset ["full" ],
85- args = ((1 ,) * len (meta ["val" ].size ()), 0.0 ),
86- kwargs = {"dtype" : torch .float32 , "device" : meta ["val" ].device },
87- meta = meta ,
88- updated = True ,
89- )
90- is_neg = super ().call_operator (
91- opset ["gt" ], (zero , q ), {}, meta , updated = True
92- )
93- ceilq = super ().call_operator (opset ["ceil" ], (q ,), {}, meta , updated = True )
94- floorq = super ().call_operator (opset ["floor" ], (q ,), {}, meta , updated = True )
95- return super ().call_operator (
96- opset ["where" ], (is_neg , ceilq , floorq ), {}, meta , updated = True
263+ Cast to and from fp if neccessary.
264+
265+ """
266+ if a_is_int :
267+ a = self ._cast (opset , a , torch .float32 , meta )
268+ if b_is_int :
269+ b = self ._cast (opset , b , torch .float32 , meta )
270+
271+ result = self ._call_fp_div (
272+ opset ,
273+ a ,
274+ b ,
275+ rounding_mode ,
276+ meta ,
97277 )
98278
99- raise RuntimeError (
100- f"Unsupported rounding_mode for div.Tensor_mode: { rounding_mode !r} "
101- )
279+ output_dtype = meta ["val" ].dtype
280+ if output_dtype != torch .float32 :
281+ result = self ._cast (opset , result , output_dtype , meta )
282+
283+ return result
0 commit comments