@@ -3003,6 +3003,106 @@ 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+ @pytest .mark .parametrize ("keepdim" , [False , True ])
3086+ async def test_bool_input_reduce_min_unsupported (self , keepdim : bool ) -> None :
3087+ # torch.min(dim) on a bool tensor is legal in torch, and the argmin fix
3088+ # routes bool down the (mul-based) reversal branch — NOT the integer
3089+ # bitwise-complement branch — since ~x on i1 is not what we want. That
3090+ # branch is fine, but bool min.dim is independently unsupported at the
3091+ # *values* path: coreai.reduce_min rejects an i1 operand (unrelated to
3092+ # this argmin fix, and true on origin/main too). Pin that limitation so
3093+ # a future reduce_min-on-bool change is noticed here; if reduce_min
3094+ # gains i1 support, this test should flip to validate_numerical_output.
3095+ x = torch .tensor ([True , False , True , False ])
3096+ assert x .dtype == torch .bool
3097+ # First False is the minimum; torch returns its first index.
3098+ assert int (torch .min (x , dim = 0 ).indices ) == 1
3099+ model = self .MinDimModel (dim = 0 , keepdim = keepdim ).eval ()
3100+ # coreai.reduce_min's MLIR verifier rejects the i1 operand; save_asset
3101+ # surfaces it as a RuntimeError wrapping the underlying MLIRError.
3102+ with pytest .raises (RuntimeError ):
3103+ await validate_numerical_output (model = model , x = x )
3104+
3105+
30063106@pytest .mark .parametrize ("dynamic" , [False , True ])
30073107@pytest .mark .parametrize (
30083108 "shape,dim,keepdim,dtype" ,
0 commit comments