Skip to content

Commit d69c31a

Browse files
committed
Expose propagate_nan and fix argmin/argmax NaN handling to match min/max
Signed-off-by: Qiqi Xiao <qiqix@nvidia.com>
1 parent 2601322 commit d69c31a

9 files changed

Lines changed: 162 additions & 36 deletions

File tree

changelog.d/propagate-nan.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- Added a ``propagate_nan`` option to ``ct.min()``, ``ct.max()``,
2+
``ct.argmin()``, ``ct.argmax()``, ``ct.minimum()`` and ``ct.maximum()``. By
3+
default ``NaN`` values are ignored; with ``propagate_nan=True`` a ``NaN``
4+
propagates -- ``min``/``max``/``minimum``/``maximum`` return ``NaN`` and
5+
``argmin``/``argmax`` return the index of the first ``NaN``.
6+
- Fixed ``ct.argmin()`` and ``ct.argmax()`` to ignore ``NaN`` values under the
7+
default ``propagate_nan=False``, consistent with ``ct.min()`` and ``ct.max()``.

experimental/cuda-lang/src/cuda/lang/_passes/flatten_cfg.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ def flatten_continue(self, op: Continue, current: Block) -> Block:
9191
fn="add",
9292
lhs=self.loop_iv,
9393
rhs=self.loop_step,
94-
rounding_mode=None,
95-
flush_to_zero=False,
9694
)
9795
)
9896
args = (next_iv,) + args

src/cuda/tile/_ir/arithmetic_ops.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,9 @@ def _bitwise_shift_tensorlike_impl(fn: str, x: Var[TensorLikeTy], y: Var[TensorL
357357
@dataclass(eq=False)
358358
class RawBinaryArithmeticOperation(Operation, opcode="raw_binary_arith"):
359359
fn: str = attribute()
360-
rounding_mode: RoundingMode | None = attribute()
361-
flush_to_zero: bool = attribute()
360+
rounding_mode: RoundingMode | None = attribute(default=None)
361+
flush_to_zero: bool = attribute(default=False)
362+
propagate_nan: bool = attribute(default=False)
362363
lhs: Var[TensorLikeTy] = operand()
363364
rhs: Var[TensorLikeTy] = operand()
364365

@@ -421,14 +422,14 @@ def generate_bytecode(self, ctx: BytecodeContext) -> bc.Value:
421422
signedness=get_signedness(dtype))
422423
case "min", "float":
423424
return bc.encode_MinFOp(ctx.builder, res_typeid, lhs, rhs,
424-
propagate_nan=False,
425+
propagate_nan=self.propagate_nan,
425426
flush_to_zero=self.flush_to_zero)
426427
case "max", "int":
427428
return bc.encode_MaxIOp(ctx.builder, res_typeid, lhs, rhs,
428429
signedness=get_signedness(dtype))
429430
case "max", "float":
430431
return bc.encode_MaxFOp(ctx.builder, res_typeid, lhs, rhs,
431-
propagate_nan=False,
432+
propagate_nan=self.propagate_nan,
432433
flush_to_zero=self.flush_to_zero)
433434
case "c_mod", "float":
434435
# C-style modulo
@@ -444,18 +445,21 @@ def generate_bytecode(self, ctx: BytecodeContext) -> bc.Value:
444445

445446
def binary_arithmetic_tensorlike_raw(fn: str, x: Var[TensorLikeTy], y: Var[TensorLikeTy],
446447
rounding_mode: RoundingMode | None = None,
447-
flush_to_zero: bool = False) -> Var[TensorLikeTy]:
448+
flush_to_zero: bool = False,
449+
propagate_nan: bool = False) -> Var[TensorLikeTy]:
448450
ty = x.get_type()
449451
assert ty == y.get_type(), f"{ty} != {y.get_type()}"
450452
# FIXME: remove cutile-specific check
451453
check_rd_and_ftz(fn, rounding_mode, flush_to_zero, ty.tensor_dtype())
452454
return add_operation(RawBinaryArithmeticOperation, ty, fn=fn, lhs=x, rhs=y,
453-
rounding_mode=rounding_mode, flush_to_zero=flush_to_zero)
455+
rounding_mode=rounding_mode, flush_to_zero=flush_to_zero,
456+
propagate_nan=propagate_nan)
454457

455458

456459
def binary_arithmetic_tensorlike(fn: str, x: Var[TensorLikeTy], y: Var[TensorLikeTy],
457460
rounding_mode: RoundingMode | None = None,
458-
flush_to_zero: bool = False) -> Var[TensorLikeTy]:
461+
flush_to_zero: bool = False,
462+
propagate_nan: bool = False) -> Var[TensorLikeTy]:
459463
x_ty = x.get_loose_type()
460464
y_ty = y.get_loose_type()
461465

@@ -481,7 +485,8 @@ def binary_arithmetic_tensorlike(fn: str, x: Var[TensorLikeTy], y: Var[TensorLik
481485
if x.is_constant() and y.is_constant():
482486
return binop_propagate_constant(fn, x.get_constant(), y.get_constant(), common_ty)
483487

484-
return binary_arithmetic_tensorlike_raw(fn, x, y, rounding_mode, flush_to_zero)
488+
return binary_arithmetic_tensorlike_raw(fn, x, y, rounding_mode, flush_to_zero,
489+
propagate_nan=propagate_nan)
485490

486491

487492
@impl(operator.add, fixed_args=["add"], overload=(TensorLikeTy, TensorLikeTy))
@@ -590,8 +595,8 @@ def where(cond: Var[TensorLikeTy],
590595
@dataclass(eq=False)
591596
class Unary(Operation, opcode="unaryop"):
592597
fn: str = attribute()
593-
rounding_mode: RoundingMode | None = attribute()
594-
flush_to_zero: bool = attribute()
598+
rounding_mode: RoundingMode | None = attribute(default=None)
599+
flush_to_zero: bool = attribute(default=False)
595600
operand: Var = operand()
596601

597602
@override
@@ -725,8 +730,7 @@ def logical_not_impl(x: Var[TensorLikeTy]) -> Var[TensorLikeTy]:
725730
if x.is_constant():
726731
return strictly_typed_const(not x.get_constant(), x.get_type())
727732

728-
return add_operation(Unary, x.get_type(), fn="invert", operand=x,
729-
rounding_mode=None, flush_to_zero=False)
733+
return add_operation(Unary, x.get_type(), fn="invert", operand=x)
730734

731735

732736
@impl(operator.pos, overload=(TensorLikeTy,))

src/cuda/tile/_ir/ops.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,11 @@ def atan2_impl(x1: Var, x2: Var) -> Var:
166166
@impl(ct.minimum, fixed_args=["min"])
167167
@impl(ct.maximum, fixed_args=["max"])
168168
def tile_binary_arithmetic_function_impl_with_ftz(fn: str, x: Var, y: Var,
169-
flush_to_zero: Var) -> Var:
169+
flush_to_zero: Var, propagate_nan: Var) -> Var:
170170
flush_to_zero = require_constant_bool(flush_to_zero)
171+
propagate_nan = require_constant_bool(propagate_nan)
171172
return binary_arithmetic_tensorlike(fn, ensure_tile(x), ensure_tile(y),
172-
flush_to_zero=flush_to_zero)
173+
flush_to_zero=flush_to_zero, propagate_nan=propagate_nan)
173174

174175

175176
@impl(ct.add, fixed_args=["add"])
@@ -2331,7 +2332,7 @@ def _get_reduction_shape(shape: Tuple[int, ...],
23312332

23322333
async def reduce_simple(fn: str, x: Var, axis: int | None | tuple[int, ...], keepdims: bool,
23332334
rounding_mode: Optional[RoundingMode] = None,
2334-
flush_to_zero: bool = False) -> Var:
2335+
flush_to_zero: bool = False, propagate_nan: bool = False) -> Var:
23352336
x_type = require_tile_type(x)
23362337
if not datatype.is_arithmetic(x_type.dtype):
23372338
raise TileTypeError(f"Non-arithmetic dtype {x_type.dtype} is unsupported for reduction")
@@ -2351,7 +2352,8 @@ async def reduce_simple(fn: str, x: Var, axis: int | None | tuple[int, ...], kee
23512352
async def body(lhs: tuple[Var], rhs: tuple[Var]) -> tuple[Var]:
23522353
[lhs], [rhs] = lhs, rhs
23532354
ret = binary_arithmetic_tensorlike(fn, lhs, rhs,
2354-
rounding_mode=rounding_mode, flush_to_zero=flush_to_zero)
2355+
rounding_mode=rounding_mode, flush_to_zero=flush_to_zero,
2356+
propagate_nan=propagate_nan)
23552357
return (ret,)
23562358

23572359
[ret] = await reduce((x,), (id_val,), axis, keepdims, body)
@@ -2399,14 +2401,17 @@ async def reduce_impl_with_rd_and_ftz(fn: str, x: Var, axis: Var, keepdims: Var,
23992401
@impl(ct.max, fixed_args=["max"])
24002402
@impl(ct.min, fixed_args=["min"])
24012403
async def reduce_impl_with_ftz(fn: str, x: Var, axis: Var, keepdims: Var,
2402-
flush_to_zero: Var) -> Var:
2404+
flush_to_zero: Var, propagate_nan: Var) -> Var:
24032405
axis = _parse_reduce_axis(axis)
24042406
keepdims = require_constant_bool(keepdims)
24052407
flush_to_zero = require_constant_bool(flush_to_zero)
2406-
return await reduce_simple(fn, x, axis, keepdims, flush_to_zero=flush_to_zero)
2408+
propagate_nan = require_constant_bool(propagate_nan)
2409+
return await reduce_simple(fn, x, axis, keepdims, flush_to_zero=flush_to_zero,
2410+
propagate_nan=propagate_nan)
24072411

24082412

2409-
async def argmax_argmin(fn: str, x: Var, axis: Optional[int], keepdims: bool) -> Var:
2413+
async def argmax_argmin(fn: str, x: Var, axis: Optional[int], keepdims: bool,
2414+
propagate_nan: bool = False) -> Var:
24102415
require_tile_type(x)
24112416
final_shape = None
24122417
if axis is None:
@@ -2434,14 +2439,34 @@ async def argmax_argmin(fn: str, x: Var, axis: Optional[int], keepdims: bool) ->
24342439
cmp = "gt"
24352440
case _: assert False
24362441

2442+
is_float_dtype = datatype.is_float(x_type.dtype)
2443+
24372444
async def body(lhs: tuple[Var, Var], rhs: tuple[Var, Var]) -> tuple[Var, Var]:
24382445
lhs_val, lhs_idx = lhs
24392446
rhs_val, rhs_idx = rhs
2440-
val_strict = compare_tensorlike_raw(cmp, lhs_val, rhs_val)
2447+
lhs_win = compare_tensorlike_raw(cmp, lhs_val, rhs_val)
24412448
val_equal = compare_tensorlike_raw("eq", lhs_val, rhs_val)
2449+
if is_float_dtype:
2450+
lhs_is_nan = compare_tensorlike_raw("ne", lhs_val, lhs_val)
2451+
rhs_is_nan = compare_tensorlike_raw("ne", rhs_val, rhs_val)
2452+
if propagate_nan:
2453+
# Mirror min/max's propagate_nan=True semantics by
2454+
# treating NaN as the best possible value.
2455+
rhs_not_nan = compare_tensorlike_raw("eq", rhs_val, rhs_val)
2456+
lhs_nan_rhs_finite = binary_bitwise_tensorlike_raw("and_", lhs_is_nan, rhs_not_nan)
2457+
lhs_win = binary_bitwise_tensorlike_raw("or_", lhs_win, lhs_nan_rhs_finite)
2458+
else:
2459+
# Mirror min/max's propagate_nan=False semantics by
2460+
# treating NaN as the worst possible value.
2461+
lhs_not_nan = compare_tensorlike_raw("eq", lhs_val, lhs_val)
2462+
lhs_finite_rhs_nan = binary_bitwise_tensorlike_raw("and_", lhs_not_nan, rhs_is_nan)
2463+
lhs_win = binary_bitwise_tensorlike_raw("or_", lhs_win, lhs_finite_rhs_nan)
2464+
# two NaNs count as "equal" so the index tiebreak (smallest index) decides.
2465+
both_nan = binary_bitwise_tensorlike_raw("and_", lhs_is_nan, rhs_is_nan)
2466+
val_equal = binary_bitwise_tensorlike_raw("or_", val_equal, both_nan)
24422467
index_lt = compare_tensorlike_raw("lt", lhs_idx, rhs_idx)
24432468
val_equal_and_index_lt = binary_bitwise_tensorlike_raw("and_", val_equal, index_lt)
2444-
cond = binary_bitwise_tensorlike_raw("or_", val_strict, val_equal_and_index_lt)
2469+
cond = binary_bitwise_tensorlike_raw("or_", lhs_win, val_equal_and_index_lt)
24452470
res = where_raw(cond, lhs_val, rhs_val)
24462471
idx = where_raw(cond, lhs_idx, rhs_idx)
24472472
return res, idx
@@ -2456,10 +2481,11 @@ async def body(lhs: tuple[Var, Var], rhs: tuple[Var, Var]) -> tuple[Var, Var]:
24562481

24572482
@impl(ct.argmax, fixed_args=["argmax"])
24582483
@impl(ct.argmin, fixed_args=["argmin"])
2459-
async def argmax_argmin_impl(fn: str, x: Var, axis: Var, keepdims: Var) -> Var:
2484+
async def argmax_argmin_impl(fn: str, x: Var, axis: Var, keepdims: Var, propagate_nan: Var) -> Var:
24602485
axis = require_optional_constant_int(axis)
24612486
keepdims = require_constant_bool(keepdims)
2462-
return await argmax_argmin(fn, x, axis, keepdims)
2487+
propagate_nan = require_constant_bool(propagate_nan)
2488+
return await argmax_argmin(fn, x, axis, keepdims, propagate_nan=propagate_nan)
24632489

24642490

24652491
@dataclass(eq=False)

src/cuda/tile/_passes/loop_split.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,20 +100,17 @@ def _split_loop(loop: Loop, cond: _Condition, if_ops_to_flatten: Set[IfElse], ne
100100
one_var.set_type(range_dtype)
101101
plus_one_var = new_block.make_temp_var(loc)
102102
new_block.append(RawBinaryArithmeticOperation(fn="add", lhs=split_value, rhs=one_var,
103-
rounding_mode=None, flush_to_zero=False,
104103
result_vars=(plus_one_var,),
105104
loc=loc))
106105
plus_one_var.set_type(range_dtype)
107106
split_value = plus_one_var
108107

109108
first_loop_stop = new_block.make_temp_var(loc)
110109
new_block.append(RawBinaryArithmeticOperation(fn="min", lhs=loop.stop, rhs=split_value,
111-
rounding_mode=None, flush_to_zero=False,
112110
result_vars=(first_loop_stop,), loc=loc))
113111

114112
second_loop_start = new_block.make_temp_var(loc)
115113
new_block.append(RawBinaryArithmeticOperation(fn="max", lhs=loop.start, rhs=split_value,
116-
rounding_mode=None, flush_to_zero=False,
117114
result_vars=(second_loop_start,), loc=loc))
118115

119116
for var in first_loop_stop, second_loop_start:

src/cuda/tile/_passes/rewrite_patterns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def fuse_mul_addsub(op: RawBinaryArithmeticOperation, ctx: MatchContext):
120120
neg_target = mul_op.lhs if rhs_is_mul else acc
121121
negated = ctx.make_temp_var(op.loc)
122122
ctx.set_type(negated, ctx.typeof(neg_target))
123-
new_ops.append(Unary(fn="neg", operand=neg_target, rounding_mode=None, flush_to_zero=False,
123+
new_ops.append(Unary(fn="neg", operand=neg_target,
124124
result_vars=(negated,), loc=op.loc))
125125
if rhs_is_mul:
126126
mul_lhs = negated

src/cuda/tile/_stub.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2638,6 +2638,12 @@ def _math_op_extra_block(f, indent):
26382638
f"{name} (const bool): If True, flushes subnormal inputs and results to "
26392639
"sign-preserving zero, default is False."
26402640
)
2641+
elif name == "propagate_nan":
2642+
extra.append(
2643+
f"{name} (const bool): Float types only. If False (the default), NaN inputs "
2644+
"are ignored. If True, NaN propagates: a NaN makes the result NaN for min/max, "
2645+
"and argmin/argmax return the index of the first NaN."
2646+
)
26412647
return ("\n" + textwrap.indent("\n".join(extra), indent)) if extra else ""
26422648

26432649

@@ -2725,7 +2731,8 @@ def sum(x, /, axis=None, *, keepdims=False, rounding_mode: Optional[RoundingMode
27252731

27262732
@_doc_reduce_op
27272733
@stub
2728-
def max(x, /, axis=None, *, keepdims=False, flush_to_zero: bool = False) -> Tile:
2734+
def max(x, /, axis=None, *, keepdims=False, flush_to_zero: bool = False,
2735+
propagate_nan: bool = False) -> Tile:
27292736
"""
27302737
Examples:
27312738
@@ -2777,7 +2784,8 @@ def max(x, /, axis=None, *, keepdims=False, flush_to_zero: bool = False) -> Tile
27772784

27782785
@_doc_reduce_op
27792786
@stub
2780-
def min(x, /, axis=None, *, keepdims=False, flush_to_zero: bool = False) -> Tile:
2787+
def min(x, /, axis=None, *, keepdims=False, flush_to_zero: bool = False,
2788+
propagate_nan: bool = False) -> Tile:
27812789
"""
27822790
Examples:
27832791
@@ -2865,7 +2873,7 @@ def prod(x, /, axis=None, *, keepdims=False, rounding_mode: Optional[RoundingMod
28652873

28662874
@_doc_reduce_op
28672875
@stub
2868-
def argmax(x, /, axis=None, *, keepdims=False) -> Tile:
2876+
def argmax(x, /, axis=None, *, keepdims=False, propagate_nan: bool = False) -> Tile:
28692877
"""
28702878
Examples:
28712879
@@ -2919,7 +2927,7 @@ def argmax(x, /, axis=None, *, keepdims=False) -> Tile:
29192927

29202928
@_doc_reduce_op
29212929
@stub
2922-
def argmin(x, /, axis=None, *, keepdims=False) -> Tile:
2930+
def argmin(x, /, axis=None, *, keepdims=False, propagate_nan: bool = False) -> Tile:
29232931
"""
29242932
Examples:
29252933
@@ -3462,7 +3470,7 @@ def bitwise_not(x, /) -> TileOrScalar:
34623470

34633471
@_doc_binary_op('min')
34643472
@stub
3465-
def minimum(x, y, /, *, flush_to_zero: bool = False) -> TileOrScalar:
3473+
def minimum(x, y, /, *, flush_to_zero: bool = False, propagate_nan: bool = False) -> TileOrScalar:
34663474
"""
34673475
Examples:
34683476
@@ -3482,7 +3490,7 @@ def minimum(x, y, /, *, flush_to_zero: bool = False) -> TileOrScalar:
34823490

34833491
@_doc_binary_op('max')
34843492
@stub
3485-
def maximum(x, y, /, *, flush_to_zero: bool = False) -> TileOrScalar:
3493+
def maximum(x, y, /, *, flush_to_zero: bool = False, propagate_nan: bool = False) -> TileOrScalar:
34863494
"""
34873495
Examples:
34883496

test/test_binary_elementwise.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,28 @@ def test_array_maxmin_flush_to_zero(shape, tile, dtype, op_func, tile_op, flush_
405405
launch_binary(kernel, x, y, z, tile)
406406

407407

408+
@pytest.mark.parametrize("propagate_nan", [False, True])
409+
@pytest.mark.parametrize("op_func, torch_op", [
410+
pytest.param("ct.maximum", torch.maximum, id="ct.maximum"),
411+
pytest.param("ct.minimum", torch.minimum, id="ct.minimum"),
412+
])
413+
@pytest.mark.parametrize("dtype", float_dtypes, ids=dtype_id)
414+
def test_array_maxmin_nan(shape, tile, dtype, op_func, torch_op, propagate_nan, tmp_path):
415+
x = make_tensor(shape, dtype=dtype, device='cuda')
416+
y = make_tensor(shape, dtype=dtype, device='cuda')
417+
x[1::3] = float("nan")
418+
y[0::3] = float("nan")
419+
z = torch.zeros_like(x)
420+
kernel = array_kernel("maxmin_nan",
421+
f"tz = {op_func}(tx, ty, propagate_nan={propagate_nan})", tmp_path)
422+
launch_binary(kernel, x, y, z, tile)
423+
if propagate_nan:
424+
ref = torch_op(x, y)
425+
else:
426+
ref = torch.where(torch.isnan(x), y, torch.where(torch.isnan(y), x, torch_op(x, y)))
427+
assert_equal(z, ref)
428+
429+
408430
@pytest.mark.parametrize("is_constant", [False, True])
409431
@pytest.mark.parametrize("x", [100, 100.0])
410432
@pytest.mark.parametrize("y", [23, 2.3])

0 commit comments

Comments
 (0)