@@ -1153,7 +1153,8 @@ def _conv_transpose(
11531153 """Handles transposed convolution (conv_transpose1d and conv_transpose2d).
11541154
11551155 For 1D, expands to 2D, performs conv_transpose2d, then shrinks back.
1156- Handles output_padding via pre-padding input and post-cropping output.
1156+ ``padding`` and ``output_padding`` are handled natively by the Core AI
1157+ ``conv_transpose2d`` op (matching PyTorch semantics).
11571158 """
11581159 is_1d = x .type .rank == 3
11591160 if is_1d :
@@ -1165,66 +1166,29 @@ def _conv_transpose(
11651166 dilation = dilation + [1 ]
11661167 output_padding = output_padding + [0 ]
11671168
1168- x_rank = x .type .rank
1169- effective_padding = padding
1170- pre_pad_amt = [0 ] * (x_rank * 2 )
1171- post_crop_amt = [0 ] * (x_rank * 2 )
1172-
1173- if any (p > 0 for p in output_padding ):
1174- effective_padding = [0 ] * len (padding )
1175- pre_pad_amt = [0 ] * (x_rank * 2 )
1176- post_crop_amt = [0 ] * (x_rank * 2 )
1177- # For each spatial dim: initialize symmetric crop from padding,
1178- # then shift the output_padding amount from crop → pre-pad if needed
1179- for i , (p , op ) in enumerate (zip (padding , output_padding )):
1180- before = 4 + 2 * i
1181- after = 4 + 2 * i + 1
1182- post_crop_amt [before ] = p
1183- post_crop_amt [after ] = p
1184- if post_crop_amt [after ] >= op :
1185- post_crop_amt [after ] -= op
1186- else :
1187- pre_pad_amt [after ] = op - post_crop_amt [after ]
1188- post_crop_amt [after ] = 0
1189-
1190- if any (p > 0 for p in pre_pad_amt ):
1191- x = coreai .pad (
1192- x ,
1193- np .array (pre_pad_amt , dtype = np .uint32 ),
1194- coreai .constant (0 , dtype = x .type .element_type ),
1195- )
1196- stride = coreai .constant (stride , np .uint32 )
1197- effective_padding = coreai .constant (effective_padding , np .uint32 )
1198- dilation = coreai .constant (dilation , np .uint32 )
1199- output_padding = coreai .constant ([0 , 0 ], dtype = np .uint32 )
1200- groups = coreai .constant (groups , np .uint32 )
12011169 result = coreai .conv_transpose2d (
12021170 input = x ,
12031171 weight = weight ,
1204- stride = stride ,
1205- padding = effective_padding ,
1206- dilation = dilation ,
1207- output_pad = output_padding ,
1208- groups = groups ,
1172+ stride = coreai . constant ( stride , np . uint32 ) ,
1173+ padding = coreai . constant ( padding , np . uint32 ) ,
1174+ dilation = coreai . constant ( dilation , np . uint32 ) ,
1175+ output_pad = coreai . constant ( output_padding , np . uint32 ) ,
1176+ groups = coreai . constant ( groups , np . uint32 ) ,
12091177 )
12101178
1211- if any (p > 0 for p in post_crop_amt ):
1212- stop_val = coreai .sub (
1213- coreai .cast (coreai .get_shape (result ), dtype = np .int32 ),
1214- [post_crop_amt [2 * d + 1 ] for d in range (x_rank )],
1215- )
1216- result = coreai .slice_ (
1217- result ,
1218- [post_crop_amt [2 * d ] for d in range (x_rank )],
1219- stop_val ,
1220- [1 ] * x_rank ,
1221- )
1222-
12231179 if is_1d :
1224- # Shrink back to 3D: [N,C,W,1] → [N,C,W]
1225- result = coreai .reshape (
1226- result , coreai .slice_ (coreai .get_shape (result ), [0 ], [3 ], [1 ])
1227- )
1180+ # Shrink back to 3D: [N,C,W,1] → [N,C,W]. When the trailing (added) dim
1181+ # is statically 1, use shrink_dims — the inverse of the expand_dims
1182+ # above — which preserves the statically-known output shape so
1183+ # downstream ops (e.g. squeeze) stay static (rdar://181169322). Under a
1184+ # dynamic input the conv op reports every dim (incl. the added one) as
1185+ # dynamic, so fall back to a reshape driven by the runtime shape.
1186+ if result .type .shape [- 1 ] == 1 :
1187+ result = coreai .shrink_dims (result , [- 1 ])
1188+ else :
1189+ result = coreai .reshape (
1190+ result , coreai .slice_ (coreai .get_shape (result ), [0 ], [3 ], [1 ])
1191+ )
12281192
12291193 if bias is not None :
12301194 bias_shape = (
@@ -1552,6 +1516,124 @@ def replace_argmax(values_map: dict[str, Value], node: fx.Node, loc: Location) -
15521516 return result if keepdim else coreai .shrink_dims (result , [dim ])
15531517
15541518
1519+ def replace_atan2 (values_map : dict [str , Value ], node : fx .Node , loc : Location ) -> Value :
1520+ """Lower atan2(y, x) using atan(y/x) with quadrant correction.
1521+
1522+ CoreAI has no native atan2, so it is decomposed as:
1523+ - x != 0, finite: atan(y/x) adjusted by ±π for the correct quadrant.
1524+ - x == +0: ±π/2 for non-zero y, 0 for y = 0.
1525+ - x == -0: ±π for all y (including ±0 → ±π per IEEE-754).
1526+ - both infinite: ±π/4 or ±3π/4 per IEEE-754.
1527+ - either operand is NaN: NaN, checked last so it overrides every other
1528+ branch (comparisons against NaN are all False, which would otherwise
1529+ misclassify NaN as one of the zero/quadrant cases above).
1530+
1531+ Signed-zero handling: IEEE-754 treats -0.0 as distinct from +0.0 for atan2
1532+ (e.g. atan2(-0, -1) = -π, not +π). The 1/v trick — 1/-0.0 = -inf — is used
1533+ to detect the sign bit of zero inputs so that y_neg and x_neg are correct
1534+ for -0.0 inputs without misclassifying ±inf (which use the strict > path).
1535+
1536+ When x=0, x is replaced with 1 before the divide solely to avoid NaN/inf; that
1537+ intermediate result is discarded by the final where-select.
1538+ atan2(0, 0) = 0 by convention.
1539+ """
1540+ y , x = _get_operands (values_map , node , [0 , 1 ])
1541+ ele_type = x .type .element_type
1542+
1543+ zero = coreai .constant (0.0 , dtype = ele_type )
1544+ one = coreai .constant (1.0 , dtype = ele_type )
1545+ pi = coreai .constant (np .pi , dtype = ele_type )
1546+ neg_pi = coreai .constant (- np .pi , dtype = ele_type )
1547+ half_pi = coreai .constant (np .pi / 2.0 , dtype = ele_type )
1548+ neg_half_pi = coreai .constant (- np .pi / 2.0 , dtype = ele_type )
1549+ quarter_pi = coreai .constant (np .pi / 4.0 , dtype = ele_type )
1550+ neg_quarter_pi = coreai .constant (- np .pi / 4.0 , dtype = ele_type )
1551+ three_quarter_pi = coreai .constant (3.0 * np .pi / 4.0 , dtype = ele_type )
1552+ neg_three_quarter_pi = coreai .constant (- 3.0 * np .pi / 4.0 , dtype = ele_type )
1553+
1554+ # ── signed-zero-aware sign predicates ─────────────────────────────────────
1555+ # 1 / -0.0 = -inf (IEEE-754), so (0 > 1/v) is True iff v = -0.0. Combine with
1556+ # the strict > predicate (handles ±inf and non-zero finites) via OR.
1557+ y_is_zero = coreai .broadcasting_equal (y , zero )
1558+ x_is_zero = coreai .broadcasting_equal (x , zero )
1559+ y_neg = coreai .broadcasting_or (
1560+ coreai .broadcasting_greater (zero , y ),
1561+ coreai .broadcasting_and (
1562+ y_is_zero ,
1563+ coreai .broadcasting_greater (zero , coreai .broadcasting_divide (one , y )),
1564+ ),
1565+ )
1566+ x_neg = coreai .broadcasting_or (
1567+ coreai .broadcasting_greater (zero , x ),
1568+ coreai .broadcasting_and (
1569+ x_is_zero ,
1570+ coreai .broadcasting_greater (zero , coreai .broadcasting_divide (one , x )),
1571+ ),
1572+ )
1573+ x_is_neg_zero = coreai .broadcasting_and (
1574+ x_is_zero ,
1575+ coreai .broadcasting_greater (zero , coreai .broadcasting_divide (one , x )),
1576+ )
1577+
1578+ # ── NaN branch ─────────────────────────────────────────────────────────────
1579+ # NaN != NaN under IEEE-754, so this is a self-contained NaN check. Needed
1580+ # because the x=0 branch below classifies purely on comparisons, which are
1581+ # all False for NaN and would otherwise misclassify atan2(NaN, ±0).
1582+ any_nan = coreai .broadcasting_or (
1583+ coreai .broadcasting_not_equal (y , y ), coreai .broadcasting_not_equal (x , x )
1584+ )
1585+ nan_result = coreai .constant (float ("nan" ), dtype = ele_type )
1586+
1587+ # ── both-infinite branch ──────────────────────────────────────────────────
1588+ # atan(inf/inf) = atan(NaN) = NaN; handle before the divide.
1589+ pos_inf = coreai .constant (float ("inf" ), dtype = ele_type )
1590+ neg_inf = coreai .constant (float ("-inf" ), dtype = ele_type )
1591+ x_is_inf = coreai .broadcasting_or (
1592+ coreai .broadcasting_equal (x , pos_inf ), coreai .broadcasting_equal (x , neg_inf )
1593+ )
1594+ y_is_inf = coreai .broadcasting_or (
1595+ coreai .broadcasting_equal (y , pos_inf ), coreai .broadcasting_equal (y , neg_inf )
1596+ )
1597+ both_inf = coreai .broadcasting_and (x_is_inf , y_is_inf )
1598+ inf_result = coreai .broadcasting_where (
1599+ y_neg ,
1600+ coreai .broadcasting_where (x_neg , neg_three_quarter_pi , neg_quarter_pi ),
1601+ coreai .broadcasting_where (x_neg , three_quarter_pi , quarter_pi ),
1602+ )
1603+
1604+ # ── x = 0 branch ──────────────────────────────────────────────────────────
1605+ # x = +0: ±π/2 for strictly ±y, 0 when y = 0.
1606+ # x = -0: ±π for all y (y_neg covers y = -0.0 via the 1/y trick above).
1607+ y_pos_strict = coreai .broadcasting_greater (y , zero )
1608+ y_neg_strict = coreai .broadcasting_greater (zero , y )
1609+ pos_x_zero_result = coreai .broadcasting_where (
1610+ y_pos_strict ,
1611+ half_pi ,
1612+ coreai .broadcasting_where (y_neg_strict , neg_half_pi , zero ),
1613+ )
1614+ neg_x_zero_result = coreai .broadcasting_where (y_neg , neg_pi , pi )
1615+ zero_result = coreai .broadcasting_where (
1616+ x_is_neg_zero , neg_x_zero_result , pos_x_zero_result
1617+ )
1618+
1619+ # ── finite nonzero x branch ────────────────────────────────────────────────
1620+ # Avoid division by zero: substitute x = 1 when x = 0; result discarded by
1621+ # the outer where-select.
1622+ x_safe = coreai .broadcasting_where (x_is_zero , one , x )
1623+ base = coreai .atan (coreai .broadcasting_divide (y , x_safe ))
1624+ correction = coreai .broadcasting_where (
1625+ y_neg ,
1626+ coreai .broadcasting_sub (base , pi ),
1627+ coreai .broadcasting_add (base , pi ),
1628+ )
1629+ nonzero_result = coreai .broadcasting_where (x_neg , correction , base )
1630+
1631+ # ── combine ────────────────────────────────────────────────────────────────
1632+ result = coreai .broadcasting_where (x_is_zero , zero_result , nonzero_result )
1633+ result = coreai .broadcasting_where (both_inf , inf_result , result )
1634+ return coreai .broadcasting_where (any_nan , nan_result , result )
1635+
1636+
15551637def replace_gather (values_map : dict [str , Value ], node : fx .Node , loc : Location ) -> Value :
15561638 """Converts aten.gather to coreai.gather_along_axis."""
15571639 x , index = _get_operands (values_map , node , [0 , 2 ])
@@ -2114,11 +2196,11 @@ def replace_maxpool2d_with_indices(
21142196 x = _get_operand (values_map , node , 0 )
21152197 args = node .args
21162198 kernel_size = args [1 ]
2117- if isinstance (args [2 ], fx .Node ):
2199+ if len ( args ) > 2 and isinstance (args [2 ], fx .Node ):
21182200 raise ValueError (
21192201 f"Encountered dynamic stride at maxpool2d: node: { node } , name: { node .name } "
21202202 )
2121- stride = args [2 ]
2203+ stride = args [2 ] if len ( args ) >= 3 else kernel_size
21222204 padding = args [3 ] if len (args ) >= 4 else [0 , 0 ]
21232205 dilation = args [4 ] if len (args ) >= 5 else [1 , 1 ]
21242206 ceil_mode = args [5 ] if len (args ) >= 6 else False
@@ -2170,6 +2252,9 @@ def replace_mean_default(
21702252) -> Value :
21712253 """Computes global mean across all dimensions, returning a scalar tensor."""
21722254 x = _get_operand (values_map , node , 0 )
2255+ target_type = get_output_element_type_from_node (node )
2256+ if x .type .element_type != target_type :
2257+ x = coreai .cast (x , target_type )
21732258 all_dims = list (range (x .type .rank ))
21742259 return coreai .shrink_dims (coreai .reduce_mean (x , all_dims ), all_dims )
21752260
@@ -2179,6 +2264,9 @@ def replace_mean_dim(
21792264) -> Value :
21802265 """Computes mean along specified dimensions."""
21812266 x , axes = _get_operands (values_map , node , [0 , 1 ])
2267+ target_type = get_output_element_type_from_node (node )
2268+ if x .type .element_type != target_type :
2269+ x = coreai .cast (x , target_type )
21822270 keepdim = len (node .args ) >= 3 and bool (node .args [2 ])
21832271 result = coreai .reduce_mean (x , axes )
21842272 return result if keepdim else coreai .shrink_dims (result , axes )
@@ -2234,11 +2322,22 @@ def replace_min_dim(
22342322 dim = dim + x .type .rank if dim < 0 else dim
22352323
22362324 min_values = coreai .reduce_min (x , [dim ])
2325+
2326+ element_type = x .type .element_type
2327+ is_integer = isinstance (element_type , IntegerType )
2328+ is_bool = element_type == IntegerType .get_signless (1 )
2329+ if is_integer and not is_bool :
2330+ # ~x == x ^ ALL_ONES; -1 has all bits set in two's complement. Bitwise
2331+ # complement is a strictly order-reversing bijection over the whole
2332+ # integer range (no overflow), so argmax(~x) == argmin(x).
2333+ reversed_x = coreai .broadcasting_bitwise_xor (
2334+ x , coreai .constant (- 1 , dtype = element_type )
2335+ )
2336+ else :
2337+ reversed_x = coreai .broadcasting_mul (x , coreai .constant (- 1 , dtype = element_type ))
2338+
22372339 argmin_indices = coreai .cast (
2238- coreai .argmax (
2239- coreai .broadcasting_mul (x , coreai .constant (- 1 , dtype = x .type .element_type )),
2240- dim ,
2241- ),
2340+ coreai .argmax (reversed_x , dim ),
22422341 np .int32 ,
22432342 )
22442343
@@ -3470,6 +3569,7 @@ def sdpa_maskless(q: Value, k: Value, v: Value) -> Value:
34703569 "asin.default" : replace_unary_ops ,
34713570 "asinh.default" : replace_unary_ops ,
34723571 "atan.default" : replace_unary_ops ,
3572+ "atan2.default" : replace_atan2 ,
34733573 "atanh.default" : replace_unary_ops ,
34743574 "_adaptive_avg_pool2d.default" : replace_adaptive_avg_pool2d ,
34753575 "_unsafe_view.default" : replace_view ,
0 commit comments