Skip to content

Commit 556fdd8

Browse files
committed
Fix aten.min.dim argmin indices at dtype-extremal minima
Core AI has argmax but not argmin, so replace_min_dim derives argmin by negating x and taking argmax. Plain negation (x * -1) overflows at integer dtype extremes (e.g. int8 min -128, or uint8 min 0), producing wrong argmin indices. Reverse integer inputs with bitwise complement (~x) instead, which is exact and overflow-free across the full integer range; float/bool inputs keep the exact negation path.
1 parent 698f11a commit 556fdd8

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

coreai_torch/_aten_to_core.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2311,11 +2311,22 @@ def replace_min_dim(
23112311
dim = dim + x.type.rank if dim < 0 else dim
23122312

23132313
min_values = coreai.reduce_min(x, [dim])
2314+
2315+
element_type = x.type.element_type
2316+
is_integer = isinstance(element_type, IntegerType)
2317+
is_bool = element_type == IntegerType.get_signless(1)
2318+
if is_integer and not is_bool:
2319+
# ~x == x ^ ALL_ONES; -1 has all bits set in two's complement. Bitwise
2320+
# complement is a strictly order-reversing bijection over the whole
2321+
# integer range (no overflow), so argmax(~x) == argmin(x).
2322+
reversed_x = coreai.broadcasting_bitwise_xor(
2323+
x, coreai.constant(-1, dtype=element_type)
2324+
)
2325+
else:
2326+
reversed_x = coreai.broadcasting_mul(x, coreai.constant(-1, dtype=element_type))
2327+
23142328
argmin_indices = coreai.cast(
2315-
coreai.argmax(
2316-
coreai.broadcasting_mul(x, coreai.constant(-1, dtype=x.type.element_type)),
2317-
dim,
2318-
),
2329+
coreai.argmax(reversed_x, dim),
23192330
np.int32,
23202331
)
23212332

tests/ops/test_ops.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3003,6 +3003,86 @@ def forward(self, x: Tensor) -> tuple[Tensor, Tensor]:
30033003
await validate_numerical_output(model=model, x=x, dynamic_shapes=dynamic_shapes)
30043004

30053005

3006+
class TestMinDimArgminDtypeExtremes:
3007+
"""min.dim argmin indices must be correct even at dtype-extremal minima.
3008+
3009+
Core AI has argmax but not argmin, so ``replace_min_dim`` derives the
3010+
argmin index by reversing the order of ``x`` and taking ``argmax``. The
3011+
reversal must be exact at the extremes of the input dtype's range:
3012+
3013+
* uint8 minimum ``0`` — a plain ``-x`` (i.e. ``x * -1``) negation maps
3014+
``0`` to ``0`` rather than the top of the unsigned range, so the true
3015+
minimum stops being the argmax.
3016+
* int8 minimum ``-128`` — ``-128 * -1`` overflows the signed range and
3017+
(under wrapping arithmetic) lands back on ``-128``.
3018+
3019+
Both were previously wrong on interpreter/cpu/gpu. The fix reverses integer
3020+
inputs with the bitwise complement ``~x`` (a strictly decreasing bijection
3021+
over the whole integer range that never overflows), leaving the exact float
3022+
negation path untouched. Ground truth is ``torch.min(x, dim=...)``.
3023+
"""
3024+
3025+
class MinDimModel(nn.Module):
3026+
def __init__(self, dim: int, keepdim: bool) -> None:
3027+
super().__init__()
3028+
self.dim = dim
3029+
self.keepdim = keepdim
3030+
3031+
def forward(self, x: Tensor) -> tuple[Tensor, Tensor]:
3032+
return torch.min(x, dim=self.dim, keepdim=self.keepdim)
3033+
3034+
@pytest.mark.parametrize("keepdim", [False, True])
3035+
@pytest.mark.parametrize(
3036+
"values,dtype",
3037+
[
3038+
# uint8: true minimum is 0 (bottom of the unsigned range).
3039+
([3, 1, 0, 5], torch.uint8),
3040+
# Signed dtypes at their exact minimum (INT_MIN), which overflows
3041+
# under the old ``x * -1`` negation trick.
3042+
([3, 1, -128, 5], torch.int8),
3043+
([3, 1, -(2**15), 5], torch.int16),
3044+
([3, 1, -(2**31), 5], torch.int32),
3045+
# NOTE: coreai-torch narrows int64 inputs to int32 at the graph
3046+
# boundary, so the exercisable extreme for the int64 dtype *path*
3047+
# is int32's minimum (a true int64 min of -2**63 would be
3048+
# truncated at that boundary, unrelated to this argmin fix).
3049+
([3, 1, -(2**31), 5], torch.int64),
3050+
],
3051+
)
3052+
async def test_dtype_min_value(
3053+
self, values: list[int], dtype: torch.dtype, keepdim: bool
3054+
) -> None:
3055+
x = torch.tensor(values, dtype=dtype)
3056+
model = self.MinDimModel(dim=0, keepdim=keepdim).eval()
3057+
await validate_numerical_output(model=model, x=x)
3058+
3059+
@pytest.mark.parametrize("keepdim", [False, True])
3060+
@pytest.mark.parametrize(
3061+
"values,dtype",
3062+
[
3063+
# Duplicate minima at the dtype extreme: torch returns the FIRST
3064+
# index (index 0 here), which the reversal must preserve.
3065+
([-128, -128, 5], torch.int8),
3066+
([0, 0, 5], torch.uint8),
3067+
],
3068+
)
3069+
async def test_duplicate_minima_first_index(
3070+
self, values: list[int], dtype: torch.dtype, keepdim: bool
3071+
) -> None:
3072+
x = torch.tensor(values, dtype=dtype)
3073+
# Sanity-check the ground-truth tie-break we are asserting against.
3074+
assert int(torch.min(x, dim=0).indices) == 0
3075+
model = self.MinDimModel(dim=0, keepdim=keepdim).eval()
3076+
await validate_numerical_output(model=model, x=x)
3077+
3078+
@pytest.mark.parametrize("keepdim", [False, True])
3079+
async def test_float_path_untouched(self, keepdim: bool) -> None:
3080+
# Float negation is exact/no-overflow; confirm it still validates.
3081+
x = torch.tensor([3.0, 1.0, -5.0, 2.0], dtype=torch.float32)
3082+
model = self.MinDimModel(dim=0, keepdim=keepdim).eval()
3083+
await validate_numerical_output(model=model, x=x)
3084+
3085+
30063086
@pytest.mark.parametrize("dynamic", [False, True])
30073087
@pytest.mark.parametrize(
30083088
"shape,dim,keepdim,dtype",

0 commit comments

Comments
 (0)