This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-tirx.git


The following commit(s) were added to refs/heads/main by this push:
     new effeeca8 [FIX] Preserve NaNs in host floating-point min and max (#31)
effeeca8 is described below

commit effeeca8e20101ed705c43908bb044389116ec80
Author: Shushi Hong <[email protected]>
AuthorDate: Fri Oct 23 15:22:32 2026 -0400

    [FIX] Preserve NaNs in host floating-point min and max (#31)
    
    This PR fixes floating-point min/max NaN propagation in TVM-TIRx’s
    constant folding, C host codegen, and LLVM codegen. C host codegen emits
    NaN-preserving conditional expressions, simplifies cases involving
    floating-point constants, and prints valid NaN/Inf literals. LLVM
    codegen uses `llvm.minimum`/`llvm.maximum` for floating-point scalars
    and vectors, adopting their signed-zero rules. Tests cover NaNs in
    either operand, scalar and broadcast constants, infinities, signed zero,
    C host float32/64, LLVM float16/32/64 and legalized BF16 scalars and
    fixed-length vectors, scalable-vector codegen, and constant folding.
---
 include/tvm/ir/prim/const_fold.h          |  4 +-
 src/codegen/c/codegen_c_host.cc           | 76 +++++++++++++++++++++-----
 src/codegen/c/codegen_c_host.h            | 13 ++---
 src/codegen/llvm/codegen_llvm.cc          | 24 ++++++---
 src/codegen/llvm/codegen_llvm.h           |  1 +
 src/codegen/min_max_utils.h               | 70 ++++++++++++++++++++++++
 tests/python/codegen/c/test_basic.py      | 58 ++++++++++++++++++++
 tests/python/codegen/llvm/test_basic.py   | 89 +++++++++++++++++++++++++++++++
 tests/python/tirx/base/test_imm_values.py | 20 +++++++
 9 files changed, 327 insertions(+), 28 deletions(-)

diff --git a/include/tvm/ir/prim/const_fold.h b/include/tvm/ir/prim/const_fold.h
index fc435eda..6be3adb2 100644
--- a/include/tvm/ir/prim/const_fold.h
+++ b/include/tvm/ir/prim/const_fold.h
@@ -373,7 +373,7 @@ inline ffi::Optional<PrimExpr> 
TryConstFold<prim::Min>(PrimExpr a, PrimExpr b) {
   TVM_PRIM_CONST_PROPAGATION({
     PrimType result_ty = a.ty();
     if (pa && pb) return IntImm(result_ty, std::min(pa->value, pb->value));
-    if (fa && fb) return FloatImm(result_ty, std::min(fa->value, fb->value));
+    if (fa && fb) return std::isnan(fa->value) || fa->value < fb->value ? a : 
b;
   });
   if (a.same_as(b)) return a;
   return std::nullopt;
@@ -384,7 +384,7 @@ inline ffi::Optional<PrimExpr> 
TryConstFold<prim::Max>(PrimExpr a, PrimExpr b) {
   TVM_PRIM_CONST_PROPAGATION({
     PrimType result_ty = a.ty();
     if (pa && pb) return IntImm(result_ty, std::max(pa->value, pb->value));
-    if (fa && fb) return FloatImm(result_ty, std::max(fa->value, fb->value));
+    if (fa && fb) return std::isnan(fa->value) || fa->value > fb->value ? a : 
b;
   });
   if (a.same_as(b)) return a;
   return std::nullopt;
diff --git a/src/codegen/c/codegen_c_host.cc b/src/codegen/c/codegen_c_host.cc
index 4e56650f..92b367fd 100644
--- a/src/codegen/c/codegen_c_host.cc
+++ b/src/codegen/c/codegen_c_host.cc
@@ -31,10 +31,13 @@
 #include <tvm/tirx/op/abi.h>
 
 #include <algorithm>
+#include <cmath>
 #include <string>
 #include <utility>
 #include <vector>
 
+#include "../min_max_utils.h"
+
 namespace tvm {
 namespace codegen {
 
@@ -350,26 +353,71 @@ void CodeGenCHost::Dispatch_(const AssertStmtObj* op) {  
// NOLINT(*)
   }
 }
 
+void CodeGenCHost::Dispatch_(const FloatImmObj* op, std::ostream& os) {  // 
NOLINT(*)
+  if (std::isfinite(op->value)) {
+    CodeGenC::Dispatch_(op, os);
+    return;
+  }
+
+  const char* literal;
+  if (std::isnan(op->value)) {
+    literal = "NAN";
+  } else if (op->value < 0) {
+    literal = "-INFINITY";
+  } else {
+    literal = "INFINITY";
+  }
+  os << "((";
+  PrintType(op->ty.as_or_throw<PrimType>(), os);
+  os << ")" << literal << ")";
+}
+
 void CodeGenCHost::Dispatch_(const prim::MinObj* op, std::ostream& os) {  // 
NOLINT(*)
-  PrintTernaryCondExpr(op, "<", os);
+  PrintMinMax(op->a, op->b, /*is_min=*/true, os);
 }
 
 void CodeGenCHost::Dispatch_(const prim::MaxObj* op, std::ostream& os) {  // 
NOLINT(*)
-  PrintTernaryCondExpr(op, ">", os);
+  PrintMinMax(op->a, op->b, /*is_min=*/false, os);
 }
 
-template <typename T>
-inline void CodeGenCHost::PrintTernaryCondExpr(const T* op, const char* 
compare,
-                                               std::ostream& os) {  // 
NOLINT(*)
-  std::ostringstream temp_a;
-  Dispatch(op->a, temp_a);
-  std::string a_id = SSAGetID(temp_a.str(), op->a.ty());
-  std::ostringstream temp_b;
-  Dispatch(op->b, temp_b);
-  std::string b_id = SSAGetID(temp_b.str(), op->b.ty());
-
-  os << "((" << a_id << ") " << compare << " (" << b_id << ") "
-     << "? (" << a_id << ") : (" << b_id << "))";
+void CodeGenCHost::PrintMinMax(const PrimExpr& lhs, const PrimExpr& rhs, bool 
is_min,
+                               std::ostream& os) {  // NOLINT(*)
+  std::ostringstream temp_lhs;
+  Dispatch(lhs, temp_lhs);
+  std::string lhs_value = "(" + SSAGetID(temp_lhs.str(), lhs.ty()) + ")";
+  std::ostringstream temp_rhs;
+  Dispatch(rhs, temp_rhs);
+  std::string rhs_value = "(" + SSAGetID(temp_rhs.str(), rhs.ty()) + ")";
+  PrimType dtype = lhs.ty().as_or_throw<PrimType>();
+  FloatNaNStatus lhs_nan_status = GetFloatNaNStatus(lhs);
+  FloatNaNStatus rhs_nan_status = GetFloatNaNStatus(rhs);
+  const char* compare = is_min ? "<" : ">";
+  const char* take_rhs_compare = is_min ? ">=" : "<=";
+
+  std::string condition;
+  std::string true_value = lhs_value;
+  std::string false_value = rhs_value;
+  if (!dtype.MatchesCode(DLDataTypeCode::kDLFloat) ||
+      lhs_nan_status == FloatNaNStatus::kKnownNotNaN) {
+    // Integers and a known non-NaN lhs need only the strict comparison.
+    condition = lhs_value + " " + compare + " " + rhs_value;
+  } else if (lhs_nan_status == FloatNaNStatus::kKnownNaN) {
+    os << lhs_value;
+    return;
+  } else if (rhs_nan_status == FloatNaNStatus::kKnownNaN) {
+    // isnan(lhs) ? lhs : rhs: select lhs only if it is also NaN.
+    condition = lhs_value + " != " + lhs_value;
+  } else if (rhs_nan_status == FloatNaNStatus::kKnownNotNaN) {
+    // For min: lhs >= rhs ? rhs : lhs. A NaN lhs makes >= false and is kept.
+    condition = lhs_value + " " + take_rhs_compare + " " + rhs_value;
+    true_value = rhs_value;
+    false_value = lhs_value;
+  } else {
+    // For min: (lhs < rhs || isnan(lhs)) ? lhs : rhs. Max uses > instead.
+    condition = "(" + lhs_value + " " + compare + " " + rhs_value + ") || (" + 
lhs_value +
+                " != " + lhs_value + ")";
+  }
+  os << "((" << condition << ") ? " << true_value << " : " << false_value << 
")";
 }
 
 ffi::Module BuildCHost(IRModule mod, Target target) {
diff --git a/src/codegen/c/codegen_c_host.h b/src/codegen/c/codegen_c_host.h
index 3c296694..abee26ad 100644
--- a/src/codegen/c/codegen_c_host.h
+++ b/src/codegen/c/codegen_c_host.h
@@ -62,6 +62,7 @@ class CodeGenCHost : public CodeGenC {
   // overload visitor functions
   void Dispatch_(const prim::BroadcastObj* op, std::ostream& os) final;  // 
NOLINT(*)
   void Dispatch_(const CallObj* op, std::ostream& os) override;          // 
NOLINT(*)
+  void Dispatch_(const FloatImmObj* op, std::ostream& os) final;         // 
NOLINT(*)
   // overload min and max to use the ternary operator, so we don't rely on the
   // standard library implementations
   void Dispatch_(const prim::MinObj* op, std::ostream& os) final;  // NOLINT(*)
@@ -96,15 +97,15 @@ class CodeGenCHost : public CodeGenC {
   void PrintGetFuncFromBackend(const std::string& func_name, const 
std::string& packed_func_name);
   void PrintCallPacked(const CallObj* op);
   /*!
-   * \brief Print ternary conditional operator implementing binary `op`
+   * \brief Print min/max, propagating floating-point NaNs and selecting rhs 
on ties.
    * Forces the operands to be in SSA form.
-   * \param op binary operator being expressed
-   * \param compare string representation of comparison operator
+   * \param lhs Left operand.
+   * \param rhs Right operand.
+   * \param is_min Whether to print min (true) or max (false).
    * \param os stream reference to print into
    */
-  template <typename T>
-  inline void PrintTernaryCondExpr(const T* op, const char* compare,
-                                   std::ostream& os);  // NOLINT(*)
+  void PrintMinMax(const PrimExpr& lhs, const PrimExpr& rhs, bool is_min,
+                   std::ostream& os);  // NOLINT(*)
 };
 
 }  // namespace codegen
diff --git a/src/codegen/llvm/codegen_llvm.cc b/src/codegen/llvm/codegen_llvm.cc
index b72c37ba..85093f12 100644
--- a/src/codegen/llvm/codegen_llvm.cc
+++ b/src/codegen/llvm/codegen_llvm.cc
@@ -1640,15 +1640,27 @@ llvm::Value* CodeGenLLVM::Dispatch_(const prim::ModObj* 
op) {
 }
 
 llvm::Value* CodeGenLLVM::Dispatch_(const prim::MinObj* op) {
-  llvm::Value* a = MakeValue(op->a);
-  llvm::Value* b = MakeValue(op->b);
-  return builder_->CreateSelect(CreateLT(PrimType(op->a.ty()->dtype), a, b), 
a, b);
+  return CreateMinMax(op->a, op->b, /*is_min=*/true);
 }
 
 llvm::Value* CodeGenLLVM::Dispatch_(const prim::MaxObj* op) {
-  llvm::Value* a = MakeValue(op->a);
-  llvm::Value* b = MakeValue(op->b);
-  return builder_->CreateSelect(CreateGT(PrimType(op->a.ty()->dtype), a, b), 
a, b);
+  return CreateMinMax(op->a, op->b, /*is_min=*/false);
+}
+
+llvm::Value* CodeGenLLVM::CreateMinMax(const PrimExpr& lhs, const PrimExpr& 
rhs, bool is_min) {
+  llvm::Value* lhs_value = MakeValue(lhs);
+  llvm::Value* rhs_value = MakeValue(rhs);
+  if (lhs_value->getType()->isFPOrFPVectorTy()) {
+    // Propagate NaNs lane-wise for scalars, fixed-length vectors, and 
scalable vectors.
+    // On a signed-zero tie, minimum selects -0 and maximum selects +0.
+    return is_min ? builder_->CreateMinimum(lhs_value, rhs_value)
+                  : builder_->CreateMaximum(lhs_value, rhs_value);
+  }
+
+  PrimType dtype(lhs.ty()->dtype);
+  llvm::Value* take_lhs =
+      is_min ? CreateLT(dtype, lhs_value, rhs_value) : CreateGT(dtype, 
lhs_value, rhs_value);
+  return builder_->CreateSelect(take_lhs, lhs_value, rhs_value);
 }
 
 llvm::Value* CodeGenLLVM::Dispatch_(const prim::EQObj* op) {
diff --git a/src/codegen/llvm/codegen_llvm.h b/src/codegen/llvm/codegen_llvm.h
index 9e2355ba..82a44cc1 100644
--- a/src/codegen/llvm/codegen_llvm.h
+++ b/src/codegen/llvm/codegen_llvm.h
@@ -492,6 +492,7 @@ class CodeGenLLVM : public 
tvm::ExprFunctor<llvm::Value*(const Expr&)>,
   llvm::Value* CreateLE(PrimType t, llvm::Value* a, llvm::Value* b);
   llvm::Value* CreateGT(PrimType t, llvm::Value* a, llvm::Value* b);
   llvm::Value* CreateGE(PrimType t, llvm::Value* a, llvm::Value* b);
+  llvm::Value* CreateMinMax(const PrimExpr& lhs, const PrimExpr& rhs, bool 
is_min);
   llvm::Value* CreateAdd(PrimType t, llvm::Value* a, llvm::Value* b);
   llvm::Value* CreateSub(PrimType t, llvm::Value* a, llvm::Value* b);
   llvm::Value* CreateMul(PrimType t, llvm::Value* a, llvm::Value* b);
diff --git a/src/codegen/min_max_utils.h b/src/codegen/min_max_utils.h
new file mode 100644
index 00000000..b78c1e1d
--- /dev/null
+++ b/src/codegen/min_max_utils.h
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file min_max_utils.h
+ * \brief Common utilities for lowering floating-point min and max.
+ */
+#ifndef TVM_CODEGEN_MIN_MAX_UTILS_H_
+#define TVM_CODEGEN_MIN_MAX_UTILS_H_
+
+#include <tvm/ir/prim/expr.h>
+
+#include <cmath>
+
+namespace tvm {
+namespace codegen {
+
+/*! \brief Compile-time NaN information about an expression. */
+enum class FloatNaNStatus {
+  /*! \brief Whether the expression is NaN has not been determined. */
+  kUnknown,
+  /*! \brief The scalar value or every vector lane is known to be NaN. */
+  kKnownNaN,
+  /*! \brief The scalar value or every vector lane is known not to be NaN. */
+  kKnownNotNaN,
+};
+
+/*!
+ * \brief Determine an expression's NaN status from floating-point constants.
+ *
+ * Recognizes a scalar FloatImm or a Broadcast of a FloatImm, including
+ * fixed-length and scalable broadcasts. Non-NaN constants include infinities.
+ * All other expression forms have unknown NaN status.
+ *
+ * \param expr The expression to classify.
+ * \return FloatNaNStatus::kKnownNaN or FloatNaNStatus::kKnownNotNaN for a 
recognized
+ *         constant; FloatNaNStatus::kUnknown otherwise.
+ */
+inline FloatNaNStatus GetFloatNaNStatus(const PrimExpr& expr) {
+  const FloatImmObj* value = expr.as<FloatImmObj>();
+  if (const auto* broadcast = expr.as<prim::BroadcastObj>()) {
+    // Recognize uniform vector constants, including scalable broadcasts.
+    value = broadcast->value.as<FloatImmObj>();
+  }
+  if (value == nullptr) {
+    return FloatNaNStatus::kUnknown;
+  }
+  return std::isnan(value->value) ? FloatNaNStatus::kKnownNaN : 
FloatNaNStatus::kKnownNotNaN;
+}
+
+}  // namespace codegen
+}  // namespace tvm
+
+#endif  // TVM_CODEGEN_MIN_MAX_UTILS_H_
diff --git a/tests/python/codegen/c/test_basic.py 
b/tests/python/codegen/c/test_basic.py
index 52d5bde2..2ab26b80 100644
--- a/tests/python/codegen/c/test_basic.py
+++ b/tests/python/codegen/c/test_basic.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import numpy as np
+import pytest
 
 import tvm_tirx
 import tvm_tirx.testing
@@ -299,5 +300,62 @@ def test_if_then_else_avoids_extraneous_parentheses():
     )
 
 
[email protected]("dtype", ["float32", "float64"])
+def test_min_max_nan_preserving(dtype):
+    n = 12
+    nan = float("nan")
+    infinity = float("inf")
+
+    @T.function
+    def main(A: T.Tensor((n,), dtype), B: T.Tensor((n,), dtype), C: 
T.Tensor((14, n), dtype)):
+        for i in range(n):
+            # Rows 0-4: min(a, b), min(0, a), min(a, 0), min(NaN, a), min(a, 
NaN).
+            C[0, i] = T.min(A[i], B[i])
+            C[1, i] = T.min(T.cast(0, dtype), A[i])
+            C[2, i] = T.min(A[i], T.cast(0, dtype))
+            C[3, i] = T.min(T.cast(nan, dtype), A[i])
+            C[4, i] = T.min(A[i], T.cast(nan, dtype))
+            # Rows 5-9: max with the same operand pairs.
+            C[5, i] = T.max(A[i], B[i])
+            C[6, i] = T.max(T.cast(0, dtype), A[i])
+            C[7, i] = T.max(A[i], T.cast(0, dtype))
+            C[8, i] = T.max(T.cast(nan, dtype), A[i])
+            C[9, i] = T.max(A[i], T.cast(nan, dtype))
+            # Rows 10-11: parentheses for general and constant max inside 
arithmetic.
+            C[10, i] = T.max(A[i], B[i]) + T.cast(1, dtype)
+            C[11, i] = T.max(T.cast(0, dtype), A[i]) + T.cast(1, dtype)
+            # Rows 12-13: positive and negative infinity constants.
+            C[12, i] = T.min(A[i], T.cast(infinity, dtype))
+            C[13, i] = T.max(A[i], T.cast(-infinity, dtype))
+
+    compiled = tvm_tirx.tirx.compile(main, target="c")
+    a = np.array([np.nan, 1, np.nan, 0.0, -0.0, 3, 2, -5, np.inf, -np.inf, 1, 
-1], dtype=dtype)
+    b = np.array([1, np.nan, np.nan, -0.0, 0.0, 2, 2, -4, 1, 1, np.inf, 
-np.inf], dtype=dtype)
+    out = tvm_tirx.runtime.empty((14, n), dtype)
+    compiled(tvm_tirx.runtime.tensor(a), tvm_tirx.runtime.tensor(b), out)
+
+    zero, nan_array = np.zeros_like(a), np.full_like(a, np.nan)
+    operand_pairs = ((a, b), (zero, a), (a, zero), (nan_array, a), (a, 
nan_array))
+    expected_by_op = {}
+    for name, compare in (("min", np.less), ("max", np.greater)):
+        expected_by_op[name] = [
+            np.where(compare(lhs, rhs) | np.isnan(lhs), lhs, rhs) for lhs, rhs 
in operand_pairs
+        ]
+    expected = expected_by_op["min"] + expected_by_op["max"]
+    max_ab, max_zero_a = expected_by_op["max"][:2]
+    expected.extend([max_ab + np.ones_like(a), max_zero_a + np.ones_like(a)])
+    # min(a, +Inf) and max(a, -Inf) both preserve a, including NaNs and signed 
zero.
+    expected.extend([a, a])
+    expected = np.asarray(expected, dtype=dtype)
+    got = out.numpy()
+    nan_mask = np.isnan(expected)
+    np.testing.assert_array_equal(np.isnan(got), nan_mask)
+    # NaN payloads may be canonicalized; preserve exact non-NaN values and 
signed zero.
+    uint_dtype = f"uint{a.dtype.itemsize * 8}"
+    np.testing.assert_array_equal(
+        got.view(uint_dtype)[~nan_mask], expected.view(uint_dtype)[~nan_mask]
+    )
+
+
 if __name__ == "__main__":
     tvm_tirx.testing.main()
diff --git a/tests/python/codegen/llvm/test_basic.py 
b/tests/python/codegen/llvm/test_basic.py
index 36b7fe9a..533a3c55 100644
--- a/tests/python/codegen/llvm/test_basic.py
+++ b/tests/python/codegen/llvm/test_basic.py
@@ -1314,5 +1314,94 @@ def test_llvm_module_properties_and_formats(tmp_path):
             np.testing.assert_equal(data.numpy(), np.arange(10))
 
 
[email protected](not env.has_llvm(), reason="need llvm")
[email protected](
+    "dtype,lanes",
+    [(dtype, lanes) for dtype in ("float16", "float32", "float64", "bfloat16") 
for lanes in (1, 4)]
+    + [("float32", "scalable")],
+)
+def test_llvm_min_max_nan_preserving(dtype, lanes):
+    scalable = lanes == "scalable"
+    extent = 4 * tvm_tirx.tirx.vscale() if scalable else lanes
+    n = 128 if scalable else 12
+    nan = float("nan")
+    infinity = float("inf")
+
+    @T.function
+    def main(A: T.Tensor((n,), dtype), B: T.Tensor((n,), dtype), C: 
T.Tensor((14, n), dtype)):
+        for outer in range(n // extent):
+            i = outer if T.constexpr(lanes == 1) else T.ramp(outer * extent, 
1, extent)
+            # Rows 0-4: min(a, b), min(0, a), min(a, 0), min(NaN, a), min(a, 
NaN).
+            C[0, i] = T.min(A[i], B[i])
+            C[1, i] = T.min(T.cast(0, dtype), A[i])
+            C[2, i] = T.min(A[i], T.cast(0, dtype))
+            C[3, i] = T.min(T.cast(nan, dtype), A[i])
+            C[4, i] = T.min(A[i], T.cast(nan, dtype))
+            # Rows 5-9: max with the same operand pairs.
+            C[5, i] = T.max(A[i], B[i])
+            C[6, i] = T.max(T.cast(0, dtype), A[i])
+            C[7, i] = T.max(A[i], T.cast(0, dtype))
+            C[8, i] = T.max(T.cast(nan, dtype), A[i])
+            C[9, i] = T.max(A[i], T.cast(nan, dtype))
+            # Rows 10-11: use max results inside arithmetic.
+            C[10, i] = T.max(A[i], B[i]) + T.cast(1, dtype)
+            C[11, i] = T.max(T.cast(0, dtype), A[i]) + T.cast(1, dtype)
+            # Rows 12-13: positive and negative infinity constants.
+            C[12, i] = T.min(A[i], T.cast(infinity, dtype))
+            C[13, i] = T.max(A[i], T.cast(-infinity, dtype))
+
+    target = {"kind": "llvm", "opt-level": 0}
+    if scalable:
+        target.update(mtriple="aarch64-linux-gnu", mattr=["+sve"])
+    compiled = tvm_tirx.tirx.compile(main, target=target)
+    # BF16 arithmetic is legalized to float32 before LLVM codegen.
+    llvm_type = {"float16": "half", "float32": "float", "float64": "double", 
"bfloat16": "float"}[
+        dtype
+    ]
+    if scalable:
+        llvm_type = f"<vscale x 4 x {llvm_type}>"
+    elif lanes != 1:
+        llvm_type = f"<{lanes} x {llvm_type}>"
+    llvm_ir = compiled.mod.inspect_source("ll")
+    for intrinsic in ("minimum", "maximum"):
+        assert f"call {llvm_type} @llvm.{intrinsic}." in llvm_ir
+    if scalable:
+        return  # Cross-compile SVE; execution requires SVE hardware.
+
+    np_dtype = pytest.importorskip("ml_dtypes").bfloat16 if dtype == 
"bfloat16" else dtype
+    a = np.array([np.nan, 1, np.nan, 0.0, -0.0, 3, 2, -5, np.inf, -np.inf, 1, 
-1], dtype=np_dtype)
+    b = np.array([1, np.nan, np.nan, -0.0, 0.0, 2, 2, -4, 1, 1, np.inf, 
-np.inf], dtype=np_dtype)
+    out = tvm_tirx.runtime.empty((14, n), dtype)
+    compiled(tvm_tirx.runtime.tensor(a), tvm_tirx.runtime.tensor(b), out)
+
+    zero, nan_array = np.zeros_like(a), np.full_like(a, np.nan)
+    operand_pairs = ((a, b), (zero, a), (a, zero), (nan_array, a), (a, 
nan_array))
+    expected_by_op = {}
+    for name, compare in (("min", np.less), ("max", np.greater)):
+        values = []
+        for lhs, rhs in operand_pairs:
+            take_lhs = compare(lhs, rhs) | np.isnan(lhs)
+            # IEEE minimum selects -0 and maximum selects +0 on a zero tie.
+            both_zero = (lhs == 0) & (rhs == 0)
+            preferred_zero = np.signbit(lhs) if name == "min" else 
~np.signbit(lhs)
+            take_lhs |= both_zero & preferred_zero
+            values.append(np.where(take_lhs, lhs, rhs))
+        expected_by_op[name] = values
+    expected = expected_by_op["min"] + expected_by_op["max"]
+    max_ab, max_zero_a = expected_by_op["max"][:2]
+    expected.extend([max_ab + np.ones_like(a), max_zero_a + np.ones_like(a)])
+    # min(a, +Inf) and max(a, -Inf) both preserve a, including NaNs and signed 
zero.
+    expected.extend([a, a])
+    expected = np.asarray(expected, dtype=np_dtype)
+    got = out.numpy()
+    nan_mask = np.isnan(expected)
+    np.testing.assert_array_equal(np.isnan(got), nan_mask)
+    # NaN payloads may be canonicalized; preserve exact non-NaN values and 
signed zero.
+    uint_dtype = f"uint{a.dtype.itemsize * 8}"
+    np.testing.assert_array_equal(
+        got.view(uint_dtype)[~nan_mask], expected.view(uint_dtype)[~nan_mask]
+    )
+
+
 if __name__ == "__main__":
     tvm_tirx.testing.main()
diff --git a/tests/python/tirx/base/test_imm_values.py 
b/tests/python/tirx/base/test_imm_values.py
index 1f12b304..4c8904d8 100644
--- a/tests/python/tirx/base/test_imm_values.py
+++ b/tests/python/tirx/base/test_imm_values.py
@@ -121,6 +121,26 @@ def test_tir_integer_cast_folding():
     assert scalable.ty == tvm_tirx.ir.PrimType("int8xvscalex4")
 
 
[email protected]("dtype", ["float16", "bfloat16", "float32", 
"float64"])
+def test_min_max_floatimm_const_fold(dtype):
+    for operation in (tirx.min, tirx.max):
+        for lhs, rhs, expected_side in (
+            (np.nan, 1.0, "lhs"),
+            (1.0, np.nan, "rhs"),
+            (np.nan, np.nan, "lhs"),
+            (0.0, -0.0, "rhs"),
+            (-0.0, 0.0, "rhs"),
+        ):
+            a, b = tirx.const(lhs, dtype), tirx.const(rhs, dtype)
+            result = operation(a, b)
+            expected = a if expected_side == "lhs" else b
+            if math.isnan(expected.value):
+                assert math.isnan(result.value)
+            else:
+                assert result.value == expected.value
+                assert np.signbit(result.value) == np.signbit(expected.value)
+
+
 def compare_float_value(value, expect, msg):
     if math.isfinite(value):
         assert np.abs(value - expect) < 1e-5, f"{value} vs {expect}, {msg}"

Reply via email to