Skip to content

DSL-ify raw arith float ops in kernels (flash/mla/pa/moe), fastmath v… - #930

Open
xudoyuan wants to merge 12 commits into
mainfrom
dsl-ify-arith
Open

DSL-ify raw arith float ops in kernels (flash/mla/pa/moe), fastmath v…#930
xudoyuan wants to merge 12 commits into
mainfrom
dsl-ify-arith

Conversation

@xudoyuan

Copy link
Copy Markdown
Collaborator

…ia fast_fp_math

Motivation

Technical Details

Test Plan

Test Result

Submission Checklist

xudoyuan and others added 7 commits July 30, 2026 09:02
…ia fast_fp_math

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace raw MLIR integer arith in kernels with DSL operators:
- arith.cmpi(pred, a, b) -> a == / != / < / <= / > / >=
- arith.divsi/divui -> //, remsi/remui -> %
- arith.andi/ori/xori -> & / | / ^, shli -> <<, shrui/shrsi -> >>

Operands stay DSL values (drop _raw/as_mlir_value/.ir_value unwraps).
At raw-i1 consumers (scf.IfOp / llvm.intr_expect / arith.select cond),
use as_ir_value(...), which handles both ArithValue (comparison over
ArithValue operands) and Boolean (comparison over Numeric operands).

Deliberately left as raw arith, with reasons:
- unsigned compare where an operand can be negative (signed compare would
  misclassify) — must stay ult
- pow2 strength-reduced shift/and standing in for divide/remainder
- unsigned-division helpers whose contract is unsigned
- integer min/max (no DSL integer min/max), ceil-div (no operator)
- operands that are genuinely raw ir.Value (ballot / readfirstlane /
  scf result / dpp) with no DSL wrapper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
De-index kernel code by replacing raw MLIR index builders with DSL:
- arith.index(n) / arith.constant(v, index=True) -> fx.Index(v)
- arith.index_cast(T.i32/i64, x) -> fx.Int32(x) / fx.Int64(x)

At raw low-level consumers (get_llvm_ptr / llvm.* / rocdl.* / buffer
DMA offset), the DSL value is unwrapped with as_ir_value(...). scf.for
loop-carried counters that were arith.constant(0, index=True) become
fx.Index(0) (type-preserving).

Left as-is (with reasons):
- index values used as layout coordinates (vec_load/vec_store/
  linear_offset/cs_off/memref) and anything combined or compared with
  them: the layout shim casts coordinates from index, so they must stay
  index-typed
- arith.index_cast(T.index, x) producing grid/launch or coordinate
  index values
- raw index operands of raw shift/and builders (strength-reduced
  pow2 paths)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last raw arith.cmpi here needed unsigned ult (local_expert_id can be
negative for non-local experts; a signed compare would misclassify them).
fx.Uint32 reinterprets the same i32 bits as unsigned, so the DSL `<`
emits ult — behavior-preserving. Result feeds a dynamic if, so wrap with
as_ir_value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
arith.select unwraps DSL Numeric/Boolean conditions itself (via
_to_raw), so wrapping the condition in as_ir_value was unnecessary.
Pass the DSL comparison directly. as_ir_value stays only where the
consumer is a raw MLIR builder that requires an ir.Value (scf.IfOp,
llvm.*, rocdl.*, get_llvm_ptr, vector.*, buffer DMA offsets).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The as_ir_value wrapper was redundant: is_local feeds a dynamic `if`
(the AST rewriter converts the condition to i1 itself) and is_local.select(...)
(a DSL method). Keep it as the DSL Boolean from fx.Uint32 comparison.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The paged-decode accumulation/reduction ops originally used the narrow
`contract` fastmath flag (FMA fusion only, preserves accumulation order).
DSL-ifying them under a `fast_fp_math` hint widened the flag to `fast`,
whose reassoc reorders fp accumulation and loses precision on fp8 (bf16/
fp16 tolerate it). Switch the pa launchers' compile hint from
{"fast_fp_math": True} to {"fastmath": contract} so the DSL operators emit
`contract`, matching the original per-op flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@xudoyuan
xudoyuan marked this pull request as ready for review July 31, 2026 05:09
Comment thread kernels/attention/flash_attn_utils.py Outdated
m_new_raw = (m_running).maximumf(row_max)
if const_expr(traits.CAUSAL):
m_new_raw = _fmax(m_new_raw, ctx.c_neg_floor, fm_fast)
m_new_raw = (m_new_raw).maximumf(ctx.c_neg_floor)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fx.maximumf ?

Comment thread kernels/attention/flash_attn_utils.py Outdated
fmath.log(as_mlir_value(l_row), fastmath=self.fm_fast),
self.fm_fast,
)
lse_val = ((m_row) * (self.c_ln2_f)) + (fmath.log(as_mlir_value(l_row), fastmath=self.fm_fast))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also clean duplicated "()" ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I will remove it.

xudoyuan and others added 5 commits July 31, 2026 10:01
Behavior-preserving cleanup of the DSL-ified attention kernels:
- drop redundant parentheses left by the operand-wrapping conversion
  (e.g. `(a) - ((b) * (c))` -> `a - b * c`), keeping only precedence-
  required grouping
- replace the `X.maximumf(Y)` method form with the `fx.maxnumf(X, Y)`
  free function (and `.minimumf` -> `fx.minnumf`); maxnumf matches the
  original flash reduction op and differs from maximumf only on NaN,
  which softmax max operands never produce

Verified behavior-identical by AST equivalence (redundant parens do not
appear in the AST; only the exact maximumf->maxnumf transform differs),
plus flash gfx942 numerics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fmath is just `flydsl.expr.math`, and fx.rsqrt/fx.log/fx.fma/... are the
same functions re-exported at top level (fmath is fx.math; fx.rsqrt is
fmath.rsqrt). Use the fx.* names for consistency with the rest of the DSL
and drop the redundant as_mlir_value on fx.log args (fx.log unwraps its
argument internally). Behavior-identical (same function objects); flash
gfx942 numerics unchanged.

Note: rocdl.exp2/rcp are intentionally kept -- they lower to the bare
v_exp/v_rcp hardware instructions, whereas fx.exp2 lowers to an
__ocml_exp2_f32 call (slower, range-reduced), so they are not equivalent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The amax exponent extraction used raw arith.bitcast/shrui/shli on
_raw-unwrapped DSL operands. Rewrite with DSL ops (bit-exact):
- arith.bitcast(T.i32, amax_f) + arith.shrui(.., 16) -> amax_f.bitcast(Uint32) >> 16
  (Uint32 so >> emits the unsigned shift, matching shrui)
- arith.shli(amax_dpp, 16) -> amax_dpp << 16
- arith.bitcast(T.f32, f32b) -> f32b.bitcast(Float32)
- .maximumf() -> fx.maxnumf() for consistency
Pure bit manipulation, no fastmath involved; moe a8w4/fp4 numerics pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_fabs_f32 wrapped a raw llvm.call_intrinsic("llvm.fabs.f32"). fx.absf
(math.absf) lowers to the same llvm.fabs.f32 (via llvm.intr.fabs), so
replace the helper with fx.absf at all call sites and delete it. Also
`.maximumf()` -> fx.maxnumf() in gemm1 for consistency. Pure bit-op abs,
behavior-identical; moe a8w4/fp4 numerics pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full-scan follow-up. Replace raw ops whose operands are already DSL and
whose DSL form lowers to the same instruction (bit-exact / same intrinsic):
- mfma_preshuffle: arith.bitcast(T.i32/f32, x) -> x.bitcast(fx.Int32/Float32)
- silu_and_mul_fq: llvm.call_intrinsic("llvm.fabs.f32") -> fx.absf; and
  arith.maximumf -> fx.maxnumf
- pa_decode_swa: arith.bitcast(T.i32, weight_local) -> weight_local.bitcast(fx.Int32)

Left as-is (raw operands / no equivalent / unvalidatable): rocdl.exp2/rcp
(bare v_exp/v_rcp), arith.minsi/maxsi (no DSL int min/max), the shrui on a
raw index param, cmpf-result andi, ds_bpermute-result bitcast, and the
multi-gpu dispatch bitcasts (raw i64 operand, no local validation path).

Validated: test_preshuffle_gemm 58 passed; imports clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants