Skip to content

Commit 0e482f4

Browse files
Merge branch 'main' into dev/gokul/fix_div
2 parents fbc1b47 + 28defcc commit 0e482f4

8 files changed

Lines changed: 960 additions & 398 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ open docs/_build/html/index.html
9696

9797
## Development
9898

99+
### Setting up pre-commit hooks
100+
101+
This repo uses [pre-commit](https://pre-commit.com) to run linting and
102+
formatting checks automatically. Install the hooks once after cloning:
103+
104+
```bash
105+
pre-commit install --hook-type pre-commit --hook-type pre-push
106+
```
107+
99108
### Running tests
100109

101110
```bash

coreai_torch/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55

66
"""coreai-torch: Convert PyTorch models to Core AI format."""
77

8+
import warnings as _warnings
9+
810
# Re-export MetalParameter so users don't need a separate coreai import.
911
from coreai.authoring import MetalParameter
12+
from packaging.version import Version as _Version
13+
from torch import __version__ as _torch_version
1014

1115
from .__version__ import __version__
1216
from ._composite_declaration import generate_composite_decl
@@ -24,3 +28,12 @@
2428
"get_decomp_table",
2529
"generate_composite_decl",
2630
]
31+
32+
_TORCH_MAX_VERSION = "2.13.0"
33+
34+
if _Version(_torch_version) > _Version(_TORCH_MAX_VERSION):
35+
_warnings.warn(
36+
f"coreai-torch has only been validated with torch<={_TORCH_MAX_VERSION}; "
37+
f"found torch {_torch_version}. Some functionality may not work as expected.",
38+
stacklevel=2,
39+
)

coreai_torch/_aten_to_core.py

Lines changed: 161 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
15551637
def 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,

coreai_torch/_custom_to_core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,11 @@ def _replace_quantize_or_dequantize(
295295
quant_elem_type = input_type.element_type # dequantize: quant→float
296296
float_elem_type = result_elem_type
297297

298-
# Extract axis; normalize negative axis the same way the C++ lowering does.
298+
# Extract axis; normalize a negative axis the same way the eager op does.
299299
axis_val = _get_optional_int_arg(node, axis_idx, default=0)
300300
input_rank = len(input_type.shape)
301301
if axis_val < 0:
302-
axis_val = axis_val + input_rank - 1
302+
axis_val = axis_val + input_rank
303303

304304
axis = coreai.constant(np.array(axis_val, dtype=np.int32), loc=loc)
305305

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies = [
2020
"packaging",
2121
"scipy",
2222
"sympy",
23-
"torch>=2.8.0,<=2.11.0",
23+
"torch>=2.8.0",
2424
"typing-extensions",
2525
"strenum",
2626
"rich>=13.0,<16.0",
@@ -38,8 +38,9 @@ test = [
3838
"pytest-rerunfailures",
3939
"pytest-sugar",
4040
"pytest-xdist",
41-
"torchaudio",
42-
"torchvision",
41+
"torch==2.13.0",
42+
"torchaudio==2.11.0",
43+
"torchvision==0.28.0",
4344
"transformers==4.57.3",
4445
]
4546
docs = [

0 commit comments

Comments
 (0)