Repository navigation
Conversation
LngelKyo
marked this pull request as draft
September 18, 2026 18:08
LngelKyo
marked this pull request as ready for review
September 18, 2026 20:52
LngelKyo
force-pushed
the
fix/cuda-min-max-nan
branch
from
September 21, 2026 22:53
c87055f to
4232dd4
Compare
LngelKyo
marked this pull request as draft
September 21, 2026 22:53
LngelKyo
force-pushed
the
fix/cuda-min-max-nan
branch
from
September 21, 2026 23:52
cbe6a06 to
5d7a3a0
Compare
The CUDA codegen prints bare min()/max() for T.min/T.max, so a NaN operand is dropped: with apache#20054's data only 4 of 8 lanes match bitwise, and the +-0 ties disagree with the C host. Emit ((a > b) || (a != a)) ? a : b for max (and the < form for min), matching apache#20054's host-side semantics: a NaN on either side survives and ties take b. Covers float16/bfloat16/float32/float64; integer and other non-float min/max keep the base CodeGenC path, including the per-lane vector expansion. Only the CUDA codegen is touched; the host side is apache#20054's. Operands are bound once via SSAGetID inside their own scope. The scope matters: SSAGetID caches by printed text within the live scope and the BufferStore dispatch does not invalidate it, so without it warp allreduce -- which emits red_buf[0] = max(red_buf[0], shuffle_down(...)) repeatedly -- reads the pre-write value from the second statement on. Constant operands take the simplified forms raised in the apache#20054 review: a constant NaN lhs is the result, a constant NaN rhs needs only the NaN test, and a constant non-NaN operand drops the NaN clause, using a reversed non-strict compare when the constant is on the right. Vector constants keep the general per-lane path. Tests go in tests/python/codegen/test_target_codegen_cuda.py, gpu-marked and covering both compile paths through the autouse fixture: apache#20054's data over four dtypes x {scalar, vec4}, the constant-operand matrix, nested uses that would expose a missing parenthesis, a chained-statement case that pins the SSA scoping, and an int32 vec4 regression against numpy.
LngelKyo
force-pushed
the
fix/cuda-min-max-nan
branch
from
September 22, 2026 00:08
b629c8e to
6810d51
Compare
The float_imm tests wrote `const_scalar = T.float32(...)` at the top of the prim_func. TVMScript parses that assignment as a one-element host-side local buffer plus a store, not as a constant: under CUDA the buffer is lowered to device workspace (cudaMalloc) and the host then writes the device pointer, which segfaults with no diagnostic. The kernel also read the operand back through a load, so the constant fast path was never exercised. The constants are now inlined into the min/max call, the vec4 form uses scalar indices, and the NaN lanes are checked with isnan instead of bitwise equality because CUDA emits a constant NaN as CUDART_NAN_F (0x7fffffff) while numpy uses 0x7fc00000. Source assertions now prove which path each form took. AsFloatImm narrows to scalar FloatImm: its Broadcast branch sat behind the lanes() == 1 guard and was unreachable; vector constants take the general per-lane path.
test_min_max_float_imm_operand_cuda compared every lane bitwise, and gave B's NaN lane its own payload (0x7FC00033) so that "which operand was selected" could be read off the payload. On sm_89 the nvcc-compiled min(c, B) and max(B, c) forms return the canonical NaN (0x7FFFFFFF) for that lane: nvcc lowers the compare-select to a min/max instruction that quiets the payload, which is allowed. The contract of the NaN-preserving min/max is that the result is NaN whenever an operand is NaN, not which NaN. The check now asserts isnan on every lane whose expected value is NaN, no NaN on the other lanes, and bitwise equality (sign of zero included) on the finite lanes. Which operand was selected is still visible from the finite lanes.
LngelKyo
marked this pull request as ready for review
September 24, 2026 10:02
…lections Main now builds the codegen tests as plain TIRx (apache#20437) and selects a Python branch in a prim_func body only when the condition is marked with T.constexpr (apache#20423). Write the min/max tests the same way: no S-TIR blocks, direct indexing inside the thread-bound and vectorized loops, and T.constexpr for the op, constant-side and form selections. What the tests check is unchanged. The float-imm test's comment no longer describes what a body assignment turns into; only the reason for writing the constant inline is kept.
LngelKyo
marked this pull request as draft
September 29, 2026 15:01
LngelKyo
marked this pull request as ready for review
October 2, 2026 21:51
Contributor
Author
|
@tvm-bot rerun |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug: on
main, the CUDA codegen prints baremin()/max()forT.min/T.maxon float32/float16/bfloat16, so a NaN operand is droppedWith #20054's test data, the CUDA target gives 4/8 bitwise matches: the NaN lanes are discarded and the ±0 ties disagree with the C host.
Fix
Same semantics as #20054: emit
(((a > b) || (a != a)) ? a : b)formax(and the<form formin), so a NaN on either side is preserved and ties takeb. Scalar form binds each operand once via SSA and references the temporaries; the vector form expands the same expression per lane. Covers float16/bfloat16/float32/float64. Integer and other non-float min/max keep the existing base-CodeGenCpath, including the per-lane vector expansion. Only the CUDA codegen is touched; the host side is #20054's.Generated source (scalar and per-lane vector), from the branch:
Tests
Folded into
tests/python/codegen/test_target_codegen_cuda.py: 4 dtypes (float32/float64/float16/bfloat16) × 2 ops × {scalar, vec4}, a composite case (C[i] = max(A[i], B[i]) + 1.0— catches the ternary being parsed as(x + cond) ? va : vb; NaN lanes assert NaN, finite lanes bitwise), and an int32 vec4 regression against numpy. Both compile paths (nvcc, nvrtc) are covered by the autouse fixture. Alsotest_min_max_float_imm_operand_cuda(constant NaN / non-NaN × lhs / rhs × min / max × scalar / vec4, once per compile path), with source assertions: the scalar constant fast path contains no||; a NaN lhs is the constant itself; a NaN rhs leaves only(a != a); a non-NaN constant leaves a single ordered compare (reversed non-strict when the constant is on the right); the vec4 form takes the general per-lane path. A nested case,max(0.0, B[i]) + 1.0, is added too. The constant has to be written inline asT.float32(c): ac = T.float32(...)statement in the prim_func body is a local buffer, and the earlier CI segfault was in the test prologue, not in the codegen.A further defect surfaced in CI: the scalar path's SSA bindings stayed alive across statements.
SSAGetIDcaches by printed text within the live scope and the base-codegenBufferStoredispatch does not invalidate it, so warp allreduce — which emitsred_buf[0] = max(red_buf[0], shuffle_down(...))repeatedly — read the pre-write value from the second statement on: the first CI run had 92 failures in test_gpu_codegen_allreduce, all finite values with max systematically too small. The scalar bindings are now scoped to the statement (BeginScope/EndScope, the same shape the vector path already had). A chained-statements test (C[v] = max(C[v], A[v]); C[v] = max(C[v], B[v]),Cpre-filled) fails on the pre-fix code and passes with the fix.Constant-operand fast paths (in response to tqchen's review on #20054): four cases — a constant NaN lhs is the result; a constant NaN rhs needs only the
(a != a)test; a constant non-NaN lhs keeps the ordered compare; a constant non-NaN rhs reverses it to a non-strict one (<→>=,>→<=). A vector constant takes the general per-lane path.The tests are written as plain TIRx (
@T.prim_funcwithT.constexprselections) for the transpiler parser (#20423 / #20437); the earlier S-TIR forms no longer parse on main.Verification
RTX A6000 (earlier rounds) and L40 (sm_89), CUDA 13.0.88:
-k min_max: 74 passed (nvcc + nvrtc);test_target_codegen_cuda.py: 440 passed, 6 skipped;return false): 68 failed / 6 passed — the 6 are the 4 int-vector and 2 chained cases, the latter green by construction since the control bypasses the new path;ruff@0.12.3check/format andclang-format 20.1.8 --dry-run --Werrorclean (per the run recorded in this thread).NaN constants and NaN results are only guaranteed to be NaN, not a particular payload (on sm_89 nvcc normalizes some forms to
0x7FFFFFFF); the tests assertisnanon the NaN lanes and bitwise equality on the finite lanes.Re-verified 2026-10-02 on an RTX A6000 (sm_86, CUDA 13.0.88, nvcc and NVRTC paths):
-k min_max74 passed; the whole codegen file 440 passed / 6 skipped;test_gpu_codegen_allreduce.py328 passed / 330 skipped; with the NaN-preserving predicate forced off, 68 of the 74 fail (negative control).Partially addresses #19579 (CUDA side); host side is #20054.
cc @tlopex @yongwww @swjng — CI will likely need approval as before.