[BugFix][Arith] Fold float Min/Max commutatively on NaN - #63
[BugFix][Arith] Fold float Min/Max commutatively on NaN#63sohampanda000 wants to merge 1 commit into
Conversation
TryConstFold<Min>/<Max> use std::min/std::max on the float-constant branch. Those are defined as a < b ? b : a, and every comparison against NaN is false, so the result depends on argument order: max(1.0, nan) folds to 1.0 while max(nan, 1.0) folds to nan. The same expression is order-independent once the operands are not compile-time constants, because the runtime lowers to fmaxf/fminf. The fold therefore disagrees with the runtime path and with itself under operand swap. Use std::fmax/std::fmin on the float branch so the fold matches the runtime semantics and returns the non-NaN operand either way. The integer branch is unchanged, as integers have no NaN. <cmath> is already included.
There was a problem hiding this comment.
Pull request overview
This PR fixes incorrect constant folding of tirx::Min/tirx::Max for floating-point compile-time constants when NaN is involved, making the folded result order-independent and consistent with the runtime lowering (fminf/fmaxf).
Changes:
- Replace
std::min/std::maxwithstd::fmin/std::fmaxfor the float-constant folding path inTryConstFold<tirx::Min>andTryConstFold<tirx::Max>. - Keep integer constant-folding behavior unchanged.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (pa && pb) return IntImm(rtype, std::min(pa->value, pb->value)); | ||
| if (fa && fb) return FloatImm(rtype, std::min(fa->value, fb->value)); | ||
| if (fa && fb) return FloatImm(rtype, std::fmin(fa->value, fb->value)); |
| if (pa && pb) return IntImm(rtype, std::max(pa->value, pb->value)); | ||
| if (fa && fb) return FloatImm(rtype, std::max(fa->value, fb->value)); | ||
| if (fa && fb) return FloatImm(rtype, std::fmax(fa->value, fb->value)); |
|
Thanks for the review — both points are fair. Signed zero. Correct, and it applies to So this case is also currently order-dependent and is also fixed. I have updated the PR description with it rather than claiming the change is NaN-only. Regression test. Agreed it needs one. I would rather add a test I have actually executed than one written against a guessed API, so I will push it in a follow-up commit on this branch once I can run it — covering both operand orders for Happy to hold the PR until that lands if you would prefer to review it as one piece. |
Problem
T.max/T.minon two float compile-time constants produce a result that depends on argument order when one operand isNaN:(same for
T.min). The kernel compiles without error and the wrong value is baked into the emitted code as a constant.The identical expression is order-independent once the operands are not compile-time constants: the runtime path lowers to
fmaxf/fminf, which are NaN-quiet and return the non-NaN operand regardless of order. So the constant fold disagrees both with the runtime lowering of the same operation and with itself under operand swap.Reported downstream as tile-ai/tilelang#2882.
Root cause
TryConstFold<tirx::Min>andTryConstFold<tirx::Max>insrc/arith/const_fold.husestd::min/std::maxon the float-constant branch:std::max(a, b)is specified asa < b ? b : a. Every comparison againstNaNis false, so it returnsa— whichever operand happens to be first.std::fmax/std::fminare the NaN-quiet counterparts and return the non-NaN operand in either order.Fix
Use
std::fmax/std::fminon the float branch only. Two lines.The integer branch is deliberately unchanged — integers have no
NaN, andstd::min/std::maxare correct and cheaper there.<cmath>is already included by this header, so there is no new dependency.Behaviour change
Two cases, both currently order-dependent — so no well-defined behaviour is being altered.
NaN. The motivating case:
Signed zero. Raised in review, and it applies equally.
+0.0 < -0.0and-0.0 < +0.0are both false, sostd::max/std::minreturn whichever operand came first there too:So the change makes both cases order-independent. Note the C standard leaves
fmax(+0.0, -0.0)implementation-defined as to which zero is returned; what it guarantees, and what matters here, is that the result no longer depends on operand order.