Skip to content

Commit 92ca7ec

Browse files
authored
[Expr] Add type-aware extrema and integer ceiling division (#995)
1 parent 01d63f7 commit 92ca7ec

17 files changed

Lines changed: 927 additions & 28 deletions

File tree

.claude/skills/flydsl-kernel-authoring/SKILL.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,9 @@ mask = fx.Int32(0xFF) # i32 constant (preferred)
433433
result = a + b
434434
result = a * scale
435435
result = cond.select(true_val, false_val)
436+
largest = fx.max(a, b)
437+
smallest = fx.min(a, b)
438+
tiles = fx.ceildiv(count, tile_size) # signed/unsigned dispatch from typed operands
436439

437440
# Keep direct arith.*FOp only when explicit fastmath flags are required.
438441
```
@@ -484,7 +487,8 @@ Use `Vec.filled(...)` for splats and `Vec.from_elements(...)` for vectors from s
484487
| Add | `a + b` | Yes | Use direct FOp only for explicit fastmath |
485488
| Multiply | `a * b` | Yes | Use direct FOp only for explicit fastmath |
486489
| Negate | `-a` | Yes | |
487-
| Max | `a.maximumf(b)` | Yes | Good for ReLU |
490+
| Max / Min | `fx.max(a, b)` / `fx.min(a, b)` | Yes | Float forms propagate NaN; `fx.maxnumf` does not |
491+
| Integer ceil-div | `fx.ceildiv(a, b)` | Yes | Direct signed/unsigned op; distinct from layout `fx.ceil_div` |
488492
| Compare | `arith.cmpf(a, b, pred)` | Yes | Returns i1/vec<i1> |
489493
| Select | `cond.select(t, f)` | Yes | |
490494
| Abs | no direct helper | Use `-v`, comparison, and `cond.select(...)` |
@@ -783,7 +787,7 @@ vD = vAB + Vec(fx.memref_load_vec(rC))
783787
# --- ReLU: C = max(A, 0) ---
784788
vA = Vec(fx.memref_load_vec(rA))
785789
zero_vec = Vec.filled(vec_width, 0.0, fx.Float32)
786-
vC = vA.maximumf(zero_vec)
790+
vC = fx.max(vA, zero_vec)
787791

788792
# --- Abs: C = |A| (arith.absf does NOT exist) ---
789793
vA = fx.memref_load_vec(rA)

.claude/skills/kernel-code-cleanup/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ fx.copy(copy, fx.slice(tA, (None, tid)), rA) # after partitioning tA (§7b: pr
132132
| `arith.index_cast(T.i32, v)` | `fx.Int32(v)` |
133133
| `arith.select(cond, t, f)` | `cond.select(t, f)` |
134134
| `arith.cmpi(slt, a, b)` | `a < b` |
135-
| `arith.maxnumf(a,b)` | `a.maximumf(b)` |
135+
| `arith.maximumf/minimumf(a,b)` | `fx.max(a, b)` / `fx.min(a, b)` |
136+
| `arith.maxsi/maxui/minsi/minui(a,b)` | `fx.max(a, b)` / `fx.min(a, b)` |
137+
| `arith.maxnumf(a,b)` | `fx.maxnumf(a, b)` — different NaN semantics from `fx.max` |
138+
| `arith.ceildivsi/ceildivui(a,b)` | `fx.ceildiv(a, b)` |
136139

137140
Keep `arith.cmpf` / explicit `*FOp` only where no operator exists or fastmath is
138141
needed.
@@ -471,6 +474,7 @@ def _run_compiled(exe, *args): # in-tree
471474
| `arith.unwrap(v)` / `_to_raw(v)` | `v.ir_value()` (boundary only) |
472475
| `fx.Index(n)` / `arith.index` / `arith.index_cast` | explicit `fx.Int64/Int32(...)` |
473476
| `arith.mulf/addf/trunc_f/select` | `*`, `+`, `.to(ty)`, `.select(...)` |
477+
| raw integer min/max or ceil-div | `fx.max` / `fx.min` / `fx.ceildiv` |
474478
| `vector.extract/bitcast/splat` | `fx.Vector(v)[i]` / `.bitcast(ty)` / `.filled(...)` |
475479
| `scf.ForOp` / `scf.IfOp` | `range_constexpr` / `range(..., init=)` / Python `if` / `const_expr` |
476480
| `buffer_ops.*` + offsets | `fx.rocdl.make_buffer_tensor` + layout + `fx.copy` |

.github/workflows/pre-checks.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ jobs:
4646
REVIEWDOG_REPORTER: ${{ github.event_name == 'pull_request' && 'github-pr-review' || 'github-check' }}
4747
REVIEWDOG_FILTER_MODE: ${{ github.event_name == 'pull_request' && 'diff_context' || 'nofilter' }}
4848

49+
- name: Reject new legacy typed arithmetic
50+
run: python3 scripts/check_typed_arithmetic_usage.py
51+
env:
52+
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || '' }}
53+
HEAD_SHA: ${{ github.sha }}
54+
4955
cpp-style:
5056
name: Check C++ Code Style
5157
runs-on: ubuntu-24.04

docs/api_stability.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ excludes it from the catalog above.
162162
| `fx.index_cast` | `fx.Index(x)` | v0.4 |
163163
| `fx.constant_vector` | `Numeric` and `Vector` member functions | v0.4 |
164164
| `fx.tdm_ops` and content reached through this alias | `fx.rocdl.tdm_ops`; the latter is a target-specific unstable path | v0.4 |
165-
| `fx.Numeric.maximumf`, `fx.Numeric.minimumf` | `fx.arith.maximumf(x, y)`, `fx.arith.minimumf(x, y)` | v0.4 |
165+
| `fx.Numeric.maximumf`, `fx.Numeric.minimumf` | `fx.max(x, y)`, `fx.min(x, y)` | v0.4 |
166166
| `fx.Numeric.shrui`, `fx.Numeric.addf` | `fx.arith.shrui(x, amount)`, `x + y` with a `fastmath` context | v0.4 |
167167
| `fx.Numeric.exp2` | `fx.math.exp2(x)` | v0.4 |
168168
| `fx.Numeric.shuffle_xor` | `fx.gpu.shuffle_xor(x, offset, width)` | v0.4 |

docs/kernel_authoring_guide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ result = a + b
201201
result = a * 2
202202
result = a // 4
203203
result = a % 16
204+
largest = fx.max(a, b)
205+
smallest = fx.min(a, b)
206+
tiles = fx.ceildiv(count, tile_size)
204207

205208
# Cast (prefer DSL numeric constructors)
206209
i64_val = fx.Int64(int_val) # cast to 64-bit integer (fx.Index is deprecated)
@@ -216,6 +219,9 @@ result = a << 4
216219
```
217220

218221
Use direct `arith.*FOp(..., fastmath=...)` only where explicit fastmath flags are performance-critical.
222+
Use `fx.max` / `fx.min` for type-dispatched extrema and `fx.ceildiv` for
223+
overflow-safe integer ceil division. `fx.maxnumf` intentionally retains
224+
non-NaN-wins semantics, and `fx.ceil_div` remains the layout/int-tuple API.
219225

220226
### 4.2 Vector values (`Vector`)
221227

docs/language/arithmetic_types.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,33 @@ The following methods apply to both `Numeric` and `Vector`, and are elementwise
4747
| `as_numeric` / `Numeric.from_python_value(value)` | build a `Numeric` from a Python value | `as_numeric(5)``Int32(5)` |
4848
| `Numeric.from_ir_type(ir_type)` | the `Numeric` type for an MLIR type | `Numeric.from_ir_type(T.f32())``Float32` |
4949

50+
### Function-level typed arithmetic
51+
52+
`fx.max`, `fx.min`, and `fx.ceildiv` normalize Python literals and DSL operands,
53+
resolve one common type, broadcast scalar operands to `Vector` shapes, and emit
54+
the dedicated MLIR operation for that type:
55+
56+
| API | Float | Signed integer | Unsigned integer |
57+
|---|---|---|---|
58+
| `fx.max` | `arith.maximumf` | `arith.maxsi` | `arith.maxui` |
59+
| `fx.min` | `arith.minimumf` | `arith.minsi` | `arith.minui` |
60+
| `fx.ceildiv` | unsupported | `arith.ceildivsi` | `arith.ceildivui` |
61+
62+
`fx.max` and `fx.min` are variadic and accept nested lists/tuples. Their float
63+
forms propagate NaN and order signed zero as `-0.0 < +0.0`. This is deliberately
64+
different from `fx.maxnumf`, which returns the non-NaN input when exactly one
65+
operand is NaN.
66+
67+
`fx.ceildiv` rounds integer division toward positive infinity. It does not use
68+
`(a + b - 1) // b`, whose intermediate addition can overflow at run time, and
69+
it does not change the floor-division meaning of `//`. The similarly named
70+
`fx.ceil_div` remains the layout/int-tuple operation.
71+
72+
Boolean inputs to `fx.max` / `fx.min` widen to `Int32`. Boolean, `Index`, float,
73+
and narrow storage-float inputs to `fx.ceildiv` are rejected. `Index` and narrow
74+
storage floats are also rejected by `fx.max` / `fx.min`; cast them to an
75+
explicit supported arithmetic type first.
76+
5077
### Vector
5178

5279
`Vector` is a fixed-length sequence of `N` elements of a single `Numeric`

kernels/attention/pa_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,7 @@ def _num_part(batch_idx):
971971
# number of partition_size-token partitions for this batch =
972972
# ceil(context_len[batch_idx] / kv_granularity)
973973
ctxv = _load(ctx_rsrc, batch_idx)
974-
return fx.Int32(arith.ceildivui(ctxv.ir_value(), c_kvg.ir_value()))
974+
return fx.Int32(fx.ceildiv(fx.Uint32(ctxv), fx.Uint32(c_kvg)))
975975

976976
def _store(rsrc, off, val):
977977
# NOTE: no masked stores — masked buffer_store sets OOB offset

kernels/common/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ def exp2_f32_fast(value):
4141
return exp2_amdgcn_scalar(raw)
4242

4343

44-
def cdiv(numer: int, denom: int) -> int:
45-
return (numer + denom - 1) // denom
44+
def cdiv(numer, denom):
45+
"""Ceiling division for host integers and typed DSL integer values."""
46+
if isinstance(numer, (fx.Numeric, fx.Vector)) or isinstance(denom, (fx.Numeric, fx.Vector)):
47+
return fx.ceildiv(numer, denom)
48+
return -(-numer // denom)
4649

4750

4851
# Alias: several kernels historically spelled this ``ceildiv``.

kernels/moe/topk_gating_softmax_kernel.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def group_reduce(x, mode):
205205
off = fx.Int32(THREADS_PER_TOKEN // (2 << _sh))
206206
peer = w.shuffle_xor(off, width_i32)
207207
if mode == "max":
208-
w = w.maximumf(peer)
208+
w = fx.max(w, peer)
209209
else:
210210
w = w.addf(peer, fastmath=fm_fast)
211211
return w
@@ -314,7 +314,7 @@ def _store_scalar_i32(divided, index, val):
314314
val_e = vector.extract(as_ir_value(atom_vec), dynamic_position=[], static_position=[v])
315315
xv = val_e if dtype_str == "f32" else val_e.extf(compute_type)
316316
x_list.append(xv)
317-
thread_max = thread_max.maximumf(xv)
317+
thread_max = fx.max(thread_max, xv)
318318

319319
group_max = group_reduce(thread_max, "max")
320320

@@ -364,7 +364,7 @@ def _store_scalar_i32(divided, index, val):
364364

365365
# Pass 5: leader writes weights/indices/tei (with optional renorm).
366366
c_eps = fx.Float32(1e-20)
367-
denom = selected_sum.maximumf(c_eps)
367+
denom = fx.max(selected_sum, c_eps)
368368
inv_denom = c_one_f / denom
369369

370370
if (expert_lane == fx.Int32(0)) & (global_token < i32_num_tokens):
@@ -467,7 +467,7 @@ def group_reduce(x, mode):
467467
off = fx.Int32(THREADS_PER_TOKEN // (2 << _sh))
468468
peer = w.shuffle_xor(off, width_i32)
469469
if mode == "max":
470-
w = w.maximumf(peer)
470+
w = fx.max(w, peer)
471471
else:
472472
w = w.addf(peer, fastmath=fm_fast)
473473
return w
@@ -568,7 +568,7 @@ def _store_scalar_i32(divided, index, val):
568568
val_e = vector.extract(as_ir_value(atom_vec), dynamic_position=[], static_position=[v])
569569
xv = val_e if dtype_str == "f32" else val_e.extf(compute_type)
570570
x_list.append(xv)
571-
thread_max = thread_max.maximumf(xv)
571+
thread_max = fx.max(thread_max, xv)
572572

573573
group_max = group_reduce(thread_max, "max")
574574

@@ -632,7 +632,7 @@ def _store_scalar_i32(divided, index, val):
632632
# Pass 5: Leader writes weights/indices/tei (with optional renorm)
633633
# ==================================================================
634634
c_eps = fx.Float32(1e-20)
635-
denom = selected_sum.maximumf(c_eps)
635+
denom = fx.max(selected_sum, c_eps)
636636
inv_denom = c_one_f / denom
637637

638638
# Inline the leader-active predicate so the AST rewriter recognises it

kernels/norm/layernorm_kernel.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ def wave_reduce_max(x):
837837
for _sh_exp in range_constexpr(int(math.log2(WARP_SIZE))):
838838
off = WARP_SIZE // (2 << _sh_exp)
839839
peer = w.shuffle_xor(off, WARP_SIZE)
840-
w = w.maximumf(peer)
840+
w = fx.max(w, peer)
841841
return w
842842

843843
def block_reduce_add2(val0, val1):
@@ -960,7 +960,7 @@ def block_reduce_max(val):
960960
y_local.append(y)
961961
y_abs = (y.bitcast(fx.Uint32) & abs_mask).bitcast(fx.Float32)
962962
tile_max = y_abs.reduce(ReductionOp.MAX)
963-
thread_row_max = thread_row_max.maximumf(tile_max)
963+
thread_row_max = fx.max(thread_row_max, tile_max)
964964

965965
row_max = block_reduce_max(thread_row_max)
966966
scale = row_max / c_dtype_max
@@ -1050,7 +1050,7 @@ def _abs_scalar(val):
10501050
s = s_e if dtype_str == "f32" else s_e.to(fx.Float32)
10511051
y = y * s
10521052
y_abs = _abs_scalar(y)
1053-
thread_row_max = thread_row_max.maximumf(is_valid.select(y_abs, c_zero_f))
1053+
thread_row_max = fx.max(thread_row_max, is_valid.select(y_abs, c_zero_f))
10541054

10551055
row_max = block_reduce_max(thread_row_max)
10561056
scale = row_max / c_dtype_max
@@ -1186,7 +1186,7 @@ def wave_reduce_max(x):
11861186
for _sh_exp in range_constexpr(int(math.log2(WARP_SIZE))):
11871187
off = WARP_SIZE // (2 << _sh_exp)
11881188
peer = w.shuffle_xor(off, WARP_SIZE)
1189-
w = w.maximumf(peer)
1189+
w = fx.max(w, peer)
11901190
return w
11911191

11921192
def block_reduce_add2(val0, val1):
@@ -1318,7 +1318,7 @@ def block_reduce_max(val):
13181318
y_local.append(y)
13191319
y_abs = (y.bitcast(fx.Uint32) & abs_mask).bitcast(fx.Float32)
13201320
tile_max = y_abs.reduce(ReductionOp.MAX)
1321-
thread_row_max = thread_row_max.maximumf(tile_max)
1321+
thread_row_max = fx.max(thread_row_max, tile_max)
13221322

13231323
row_max = block_reduce_max(thread_row_max)
13241324
scale = row_max / c_dtype_max
@@ -1420,7 +1420,7 @@ def _abs_scalar(val):
14201420
s = s_e if dtype_str == "f32" else s_e.to(fx.Float32)
14211421
y = y * s
14221422
y_abs = _abs_scalar(y)
1423-
thread_row_max = thread_row_max.maximumf(is_valid.select(y_abs, c_zero_f))
1423+
thread_row_max = fx.max(thread_row_max, is_valid.select(y_abs, c_zero_f))
14241424

14251425
row_max = block_reduce_max(thread_row_max)
14261426
scale = row_max / c_dtype_max

0 commit comments

Comments
 (0)