Skip to content

Commit 5e2a2d7

Browse files
_aten_to_core: fix IEEE-754 edge cases in replace_atan2
- Signed zeros: use 1/v trick (1/-0.0 = -inf) combined with strict > to make y_neg and x_neg correct for -0.0 inputs without misclassifying ±inf values which use the strict > path directly - x = -0.0 branch: split x=0 into +0 (returns ±π/2) and -0 (returns ±π) per IEEE-754 atan2 specification - Both infinite: add explicit branch for atan2(±inf, ±inf) → ±π/4 or ±3π/4; previously produced NaN via atan(inf/inf) = atan(NaN) - Add test_signed_zeros and test_infinities to lock in correct behaviour - Update IR tests to reflect the expanded op sequence
1 parent ab8cdb3 commit 5e2a2d7

3 files changed

Lines changed: 170 additions & 88 deletions

File tree

coreai_torch/_aten_to_core.py

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,55 +1526,105 @@ def replace_atan2(values_map: dict[str, Value], node: fx.Node, loc: Location) ->
15261526
"""Lower atan2(y, x) using atan(y/x) with quadrant correction.
15271527
15281528
CoreAI has no native atan2, so it is decomposed as:
1529-
- x != 0: atan(y/x) adjusted by ±π to place the result in the correct quadrant.
1530-
- x == 0: ±π/2 or 0 based on sign of y.
1529+
- x != 0, finite: atan(y/x) adjusted by ±π for the correct quadrant.
1530+
- x == +0: ±π/2 for non-zero y, 0 for y = 0.
1531+
- x == -0: ±π for all y (including ±0 → ±π per IEEE-754).
1532+
- both infinite: ±π/4 or ±3π/4 per IEEE-754.
1533+
1534+
Signed-zero handling: IEEE-754 treats -0.0 as distinct from +0.0 for atan2
1535+
(e.g. atan2(-0, -1) = -π, not +π). The 1/v trick — 1/-0.0 = -inf — is used
1536+
to detect the sign bit of zero inputs so that y_neg and x_neg are correct
1537+
for -0.0 inputs without misclassifying ±inf (which use the strict > path).
15311538
15321539
When x=0, x is replaced with 1 before the divide solely to avoid NaN/inf; that
1533-
intermediate result is discarded by the final where-select in favour of the x=0 branch.
1540+
intermediate result is discarded by the final where-select.
15341541
atan2(0, 0) = 0 by convention.
1535-
1536-
IEEE-754 limitations:
1537-
- Signed zeros: ``-0.0`` is treated the same as ``+0.0`` because the
1538-
``0 > y`` predicate is false for ``y = -0.0``. Results are numerically
1539-
equal to PyTorch for finite inputs but the sign bit may differ
1540-
(e.g. ``atan2(-0.0, -1.0)`` returns ``+π`` here, ``-π`` in PyTorch).
1541-
- Infinities: ``atan2(±inf, ±inf)`` returns NaN because ``inf/inf``
1542-
produces NaN before ``atan`` is applied. PyTorch returns ``±π/4``
1543-
or ``±3π/4`` per IEEE-754. Do not pass infinite inputs to this op.
15441542
"""
15451543
y, x = _get_operands(values_map, node, [0, 1])
15461544
ele_type = x.type.element_type
15471545

15481546
zero = coreai.constant(0.0, dtype=ele_type)
1547+
one = coreai.constant(1.0, dtype=ele_type)
15491548
pi = coreai.constant(np.pi, dtype=ele_type)
1549+
neg_pi = coreai.constant(-np.pi, dtype=ele_type)
15501550
half_pi = coreai.constant(np.pi / 2.0, dtype=ele_type)
15511551
neg_half_pi = coreai.constant(-np.pi / 2.0, dtype=ele_type)
1552-
1553-
# Avoid division by zero when x = 0 by substituting x = 1 for the ratio.
1552+
quarter_pi = coreai.constant(np.pi / 4.0, dtype=ele_type)
1553+
neg_quarter_pi = coreai.constant(-np.pi / 4.0, dtype=ele_type)
1554+
three_quarter_pi = coreai.constant(3.0 * np.pi / 4.0, dtype=ele_type)
1555+
neg_three_quarter_pi = coreai.constant(-3.0 * np.pi / 4.0, dtype=ele_type)
1556+
1557+
# ── signed-zero-aware sign predicates ─────────────────────────────────────
1558+
# 1 / -0.0 = -inf (IEEE-754), so (0 > 1/v) is True iff v = -0.0. Combine with
1559+
# the strict > predicate (handles ±inf and non-zero finites) via OR.
1560+
y_is_zero = coreai.broadcasting_equal(y, zero)
15541561
x_is_zero = coreai.broadcasting_equal(x, zero)
1555-
x_safe = coreai.broadcasting_where(
1556-
x_is_zero, coreai.constant(1.0, dtype=ele_type), x
1562+
y_neg = coreai.broadcasting_or(
1563+
coreai.broadcasting_greater(zero, y),
1564+
coreai.broadcasting_and(
1565+
y_is_zero,
1566+
coreai.broadcasting_greater(zero, coreai.broadcasting_divide(one, y)),
1567+
),
1568+
)
1569+
x_neg = coreai.broadcasting_or(
1570+
coreai.broadcasting_greater(zero, x),
1571+
coreai.broadcasting_and(
1572+
x_is_zero,
1573+
coreai.broadcasting_greater(zero, coreai.broadcasting_divide(one, x)),
1574+
),
1575+
)
1576+
x_is_neg_zero = coreai.broadcasting_and(
1577+
x_is_zero,
1578+
coreai.broadcasting_greater(zero, coreai.broadcasting_divide(one, x)),
1579+
)
1580+
1581+
# ── both-infinite branch ──────────────────────────────────────────────────
1582+
# atan(inf/inf) = atan(NaN) = NaN; handle before the divide.
1583+
pos_inf = coreai.constant(float("inf"), dtype=ele_type)
1584+
neg_inf = coreai.constant(float("-inf"), dtype=ele_type)
1585+
x_is_inf = coreai.broadcasting_or(
1586+
coreai.broadcasting_equal(x, pos_inf), coreai.broadcasting_equal(x, neg_inf)
1587+
)
1588+
y_is_inf = coreai.broadcasting_or(
1589+
coreai.broadcasting_equal(y, pos_inf), coreai.broadcasting_equal(y, neg_inf)
1590+
)
1591+
both_inf = coreai.broadcasting_and(x_is_inf, y_is_inf)
1592+
inf_result = coreai.broadcasting_where(
1593+
y_neg,
1594+
coreai.broadcasting_where(x_neg, neg_three_quarter_pi, neg_quarter_pi),
1595+
coreai.broadcasting_where(x_neg, three_quarter_pi, quarter_pi),
15571596
)
1558-
base = coreai.atan(coreai.broadcasting_divide(y, x_safe))
15591597

1560-
# Quadrant correction: x < 0 shifts the result by ±π.
1561-
x_neg = coreai.broadcasting_greater(zero, x)
1562-
y_neg = coreai.broadcasting_greater(zero, y)
1563-
y_pos = coreai.broadcasting_greater(y, zero)
1598+
# ── x = 0 branch ──────────────────────────────────────────────────────────
1599+
# x = +0: ±π/2 for strictly ±y, 0 when y = 0.
1600+
# x = -0: ±π for all y (y_neg covers y = -0.0 via the 1/y trick above).
1601+
y_pos_strict = coreai.broadcasting_greater(y, zero)
1602+
y_neg_strict = coreai.broadcasting_greater(zero, y)
1603+
pos_x_zero_result = coreai.broadcasting_where(
1604+
y_pos_strict,
1605+
half_pi,
1606+
coreai.broadcasting_where(y_neg_strict, neg_half_pi, zero),
1607+
)
1608+
neg_x_zero_result = coreai.broadcasting_where(y_neg, neg_pi, pi)
1609+
zero_result = coreai.broadcasting_where(
1610+
x_is_neg_zero, neg_x_zero_result, pos_x_zero_result
1611+
)
1612+
1613+
# ── finite nonzero x branch ────────────────────────────────────────────────
1614+
# Avoid division by zero: substitute x = 1 when x = 0; result discarded by
1615+
# the outer where-select.
1616+
x_safe = coreai.broadcasting_where(x_is_zero, one, x)
1617+
base = coreai.atan(coreai.broadcasting_divide(y, x_safe))
15641618
correction = coreai.broadcasting_where(
15651619
y_neg,
15661620
coreai.broadcasting_sub(base, pi),
15671621
coreai.broadcasting_add(base, pi),
15681622
)
15691623
nonzero_result = coreai.broadcasting_where(x_neg, correction, base)
15701624

1571-
# x = 0: result is π/2, −π/2, or 0 based on sign of y.
1572-
zero_result = coreai.broadcasting_where(
1573-
y_pos,
1574-
half_pi,
1575-
coreai.broadcasting_where(y_neg, neg_half_pi, zero),
1576-
)
1577-
return coreai.broadcasting_where(x_is_zero, zero_result, nonzero_result)
1625+
# ── combine ────────────────────────────────────────────────────────────────
1626+
result = coreai.broadcasting_where(x_is_zero, zero_result, nonzero_result)
1627+
return coreai.broadcasting_where(both_inf, inf_result, result)
15781628

15791629

15801630
def replace_gather(values_map: dict[str, Value], node: fx.Node, loc: Location) -> Value:

tests/ops/test_ops.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,22 @@ async def test_broadcast_shapes(self) -> None:
652652
x = torch.randn(4)
653653
await validate_numerical_output(model=model, y=y, x=x)
654654

655+
async def test_signed_zeros(self) -> None:
656+
"""IEEE-754 signed-zero cases: atan2(-0, x) and atan2(y, -0)."""
657+
model = self.Atan2Model().eval()
658+
# y = -0.0 with various x signs
659+
y = torch.tensor([-0.0, -0.0, -0.0, 0.0])
660+
x = torch.tensor([-1.0, 1.0, -0.0, -0.0])
661+
await validate_numerical_output(model=model, y=y, x=x)
662+
663+
async def test_infinities(self) -> None:
664+
"""IEEE-754 both-infinite cases: atan2(±inf, ±inf) → ±π/4 or ±3π/4."""
665+
model = self.Atan2Model().eval()
666+
inf = float("inf")
667+
y = torch.tensor([inf, inf, -inf, -inf])
668+
x = torch.tensor([inf, -inf, inf, -inf])
669+
await validate_numerical_output(model=model, y=y, x=x)
670+
655671

656672
@pytest.mark.parametrize(
657673
"x",

0 commit comments

Comments
 (0)