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.git


The following commit(s) were added to refs/heads/main by this push:
     new ebeebf1116 [REFACTOR][TIRx] Reduce conditional hoisting to HoistIf 
(#20636)
ebeebf1116 is described below

commit ebeebf1116fb2360fa74a82e5f07ffaa7916e87b
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Oct 10 10:30:13 2026 -0400

    [REFACTOR][TIRx] Reduce conditional hoisting to HoistIf (#20636)
---
 include/tvm/s_tir/function.h                       |   2 -
 include/tvm/s_tir/transform.h                      |  22 -
 include/tvm/tirx/transform.h                       |   6 +
 python/tvm/backend/trn/pipeline.py                 |   2 +-
 python/tvm/s_tir/backend/adreno/pipeline.py        |   2 +-
 python/tvm/s_tir/pipeline.py                       |   2 +-
 python/tvm/s_tir/transform/transform.py            |  33 -
 python/tvm/tirx/transform/transform.py             |  11 +
 src/ir/side_effect.cc                              |  16 +-
 src/s_tir/transform/hoist_expression.cc            | 686 ---------------------
 src/tirx/transform/hoist_if.cc                     | 264 ++++++++
 src/tirx/transform/remove_no_op.cc                 |   1 -
 .../python/s_tir/dlight/test_gpu_low_batch_gemv.py |   8 +-
 .../test_s_tir_transform_hoist_expression.py       | 590 ------------------
 .../transform/test_s_tir_transform_hoist_if.py     | 562 -----------------
 .../transform/test_s_tir_transform_thread_sync.py  |  25 +
 .../tirx/transform/test_transform_hoist_if.py      | 423 +++++++++++++
 17 files changed, 749 insertions(+), 1906 deletions(-)

diff --git a/include/tvm/s_tir/function.h b/include/tvm/s_tir/function.h
index f75cf7405a..b5044fbf91 100644
--- a/include/tvm/s_tir/function.h
+++ b/include/tvm/s_tir/function.h
@@ -40,8 +40,6 @@ constexpr const char* kLayoutFreeBuffers = 
"layout_free_buffers";
 constexpr const char* kLayoutTransforms = "layout_transforms";
 constexpr const char* kEstimatedFlops = "estimated_flops";
 
-constexpr const char* kHoistIfThenElseExprWithBlock = 
"tirx.HoistIfThenElseExprWithBlock";
-
 }  // namespace attr
 }  // namespace s_tir
 }  // namespace tvm
diff --git a/include/tvm/s_tir/transform.h b/include/tvm/s_tir/transform.h
index cc22793fc3..6f166ab1ef 100644
--- a/include/tvm/s_tir/transform.h
+++ b/include/tvm/s_tir/transform.h
@@ -229,28 +229,6 @@ TVM_DLL Pass LoopPartition();
  */
 TVM_DLL Pass InjectVirtualThread();
 
-/*!
- * \brief Hoist loop-invariant If nodes to
- * outside the eligible loops.
- *
- * \param variant The variant of the pass.
- *        variant can have any one of following values ["basic", ""(Default)].
- * \return The pass.
- */
-TVM_DLL Pass HoistIfThenElse(tvm::ffi::String variant = "");
-
-/*!
- * \brief Hoist loop-invariant expressions to outside the eligible loops.
- *
- * Can hoist conditionals used in If statements and
- * expressions, bindings of variables in Let statements and
- * expressions, or boolean expressions, configurable to enable/disable
- * each hoistable type.
- *
- * \return The pass.
- */
-TVM_DLL Pass HoistExpression();
-
 /*!
  * \brief Detect and rewrite unsafe select that contains memory access.
  * \return The pass.
diff --git a/include/tvm/tirx/transform.h b/include/tvm/tirx/transform.h
index 74ba52ade0..6cb8a2d5ec 100644
--- a/include/tvm/tirx/transform.h
+++ b/include/tvm/tirx/transform.h
@@ -98,6 +98,12 @@ TVM_DLL Pass UnrollLoop();
  */
 TVM_DLL Pass RemoveNoOp();
 
+/*!
+ * \brief Hoist loop-invariant If statements outside eligible loops.
+ * \return The pass.
+ */
+TVM_DLL Pass HoistIf();
+
 /*!
  * \brief Renormalize the split pattern from floordiv(floormod()) to 
floormod(floordiv()).
  * \return The pass.
diff --git a/python/tvm/backend/trn/pipeline.py 
b/python/tvm/backend/trn/pipeline.py
index dd9b252e1c..9d8b6b334b 100644
--- a/python/tvm/backend/trn/pipeline.py
+++ b/python/tvm/backend/trn/pipeline.py
@@ -38,7 +38,7 @@ def trn_pipeline():
             tirx.transform.StmtSimplify(),
             tvm.s_tir.transform.LoopPartition(),
             tirx.transform.LowerTIRxOpaque(),
-            tvm.s_tir.transform.HoistIfThenElse(),
+            tirx.transform.HoistIf(),
             tirx.transform.StmtSimplify(),
             tirx.transform.RemoveNoOp(),
             tirx.transform.AnnotateEntryFunc(),
diff --git a/python/tvm/s_tir/backend/adreno/pipeline.py 
b/python/tvm/s_tir/backend/adreno/pipeline.py
index 749a245ca5..3d6edad5e3 100644
--- a/python/tvm/s_tir/backend/adreno/pipeline.py
+++ b/python/tvm/s_tir/backend/adreno/pipeline.py
@@ -62,7 +62,7 @@ def default_tir_pipeline(*, prepare_only=False):
             passes.append(tirx.transform.StorageRewrite())
         passes.extend(
             [
-                s_tir.transform.HoistIfThenElse(),
+                tirx.transform.HoistIf(),
                 tirx.transform.UnrollLoop(),
                 tirx.transform.RenormalizeSplitPattern(),
                 s_tir.transform.StmtSimplify(),
diff --git a/python/tvm/s_tir/pipeline.py b/python/tvm/s_tir/pipeline.py
index 09e36aeaa7..ea9ee4d2e5 100644
--- a/python/tvm/s_tir/pipeline.py
+++ b/python/tvm/s_tir/pipeline.py
@@ -62,7 +62,7 @@ def default_s_tir_pipeline(*, prepare_only=False):
             passes.append(tirx.transform.StorageRewrite())
         passes.extend(
             [
-                s_tir.transform.HoistIfThenElse(),
+                tirx.transform.HoistIf(),
                 tirx.transform.UnrollLoop(),
                 tirx.transform.RenormalizeSplitPattern(),
                 s_tir.transform.StmtSimplify(),
diff --git a/python/tvm/s_tir/transform/transform.py 
b/python/tvm/s_tir/transform/transform.py
index 1b4fc61f48..46eedbc80f 100644
--- a/python/tvm/s_tir/transform/transform.py
+++ b/python/tvm/s_tir/transform/transform.py
@@ -257,39 +257,6 @@ def InjectVirtualThread():
     return _ffi_api.InjectVirtualThread()  # type: ignore
 
 
-def HoistIfThenElse(variant=None):
-    """Hoist loop-invariant If nodes to outside the eligible loops.
-
-    Parameters
-    ----------
-    variant : Optional[String]
-        The variant of the pass.
-        variant can have any one of following values ["basic", None(Default)].
-
-    Returns
-    -------
-    fpass : tvm.transform.Pass
-        The result pass
-    """
-    if variant == "basic":
-        return _ffi_api.HoistIfThenElseBasic()  # type: ignore
-    elif variant is None:
-        return _ffi_api.HoistIfThenElse()  # type: ignore
-    else:
-        raise ValueError("wrong variant of HoistIfThenElse, " + variant)
-
-
-def HoistExpression():
-    """Hoist loop-invariant expressions to outside the eligible loops.
-
-    Returns
-    -------
-    fpass : tvm.transform.Pass
-        The result pass
-    """
-    return _ffi_api.HoistExpression()  # type: ignore
-
-
 def RewriteUnsafeSelect():
     """Detect and rewrite unsafe select that contains memory access.
 
diff --git a/python/tvm/tirx/transform/transform.py 
b/python/tvm/tirx/transform/transform.py
index 12e69f0551..2909d9107b 100644
--- a/python/tvm/tirx/transform/transform.py
+++ b/python/tvm/tirx/transform/transform.py
@@ -133,6 +133,17 @@ def RemoveNoOp():
     return _ffi_api.RemoveNoOp()  # type: ignore
 
 
+def HoistIf():
+    """Hoist loop-invariant If statements outside eligible loops.
+
+    Returns
+    -------
+    fpass : tvm.transform.Pass
+        The result pass.
+    """
+    return _ffi_api.HoistIf()
+
+
 def RemoveAssume():
     """Remove all instances of prim::assume_op
 
diff --git a/src/ir/side_effect.cc b/src/ir/side_effect.cc
index 7173f29543..6a3bd54418 100644
--- a/src/ir/side_effect.cc
+++ b/src/ir/side_effect.cc
@@ -25,12 +25,21 @@
 #include <tvm/ir/op.h>
 
 #include <algorithm>
+#include <unordered_set>
 
 namespace tvm {
 
 CallEffectKind SideEffect(const Expr& expr) {
   static auto effects = 
Op::GetAttrMap<TCallEffectKind>(tvm::op_attr::kCallEffectKind);
   CallEffectKind kind = CallEffectKind::kPure;
+  std::unordered_set<const ExprNode*> visited;
+  auto visit_expr =
+      [&](const ExprNode* node,
+          ffi::StructuralVisitorObj* visitor) -> 
ffi::Expected<ffi::Optional<ffi::VisitInterrupt>> {
+    // Effects are context-independent; inspect each shared expression only 
once.
+    if (!visited.insert(node).second) return std::nullopt;
+    return visitor->DefaultVisitExpected(node);
+  };
   ffi::StructuralVisit(
       expr,
       [&](const CallNode* node,
@@ -39,18 +48,19 @@ CallEffectKind SideEffect(const Expr& expr) {
             effects.get(node->op, 
static_cast<TCallEffectKind>(CallEffectKind::kOpaque)));
         kind = std::max(kind, std::min(effect, CallEffectKind::kUpdateState));
         if (kind == CallEffectKind::kUpdateState) return ffi::VisitInterrupt();
-        return visitor->DefaultVisitExpected(node);
+        return visit_expr(node, visitor);
       },
       [&](const TensorLoadNode* node,
           ffi::StructuralVisitorObj* visitor) -> 
ffi::Expected<ffi::Optional<ffi::VisitInterrupt>> {
         kind = std::max(kind, CallEffectKind::kReadState);
-        return visitor->DefaultVisitExpected(node);
+        return visit_expr(node, visitor);
       },
       [](const TypeNode*,
          ffi::StructuralVisitorObj*) -> 
ffi::Expected<ffi::Optional<ffi::VisitInterrupt>> {
         // Effects describe evaluated expressions, not expressions embedded in 
types.
         return std::nullopt;
-      });
+      },
+      visit_expr);
   return kind;
 }
 
diff --git a/src/s_tir/transform/hoist_expression.cc 
b/src/s_tir/transform/hoist_expression.cc
deleted file mode 100644
index d93f2cbe59..0000000000
--- a/src/s_tir/transform/hoist_expression.cc
+++ /dev/null
@@ -1,686 +0,0 @@
-/*
- * 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 hoist_expression.cc
- */
-#include <tvm/ffi/cast.h>
-#include <tvm/ffi/extra/structural_visit.h>
-#include <tvm/ffi/function.h>
-#include <tvm/ffi/reflection/registry.h>
-#include <tvm/ir/analysis.h>
-#include <tvm/ir/prim/expr.h>
-#include <tvm/ir/prim/op.h>
-#include <tvm/s_tir/analysis.h>
-#include <tvm/s_tir/function.h>
-#include <tvm/s_tir/stmt_functor.h>
-#include <tvm/s_tir/transform.h>
-#include <tvm/sym/analyzer.h>
-#include <tvm/tirx/analysis.h>
-#include <tvm/tirx/op/memory.h>
-#include <tvm/tirx/op/region.h>
-
-#include <queue>
-#include <unordered_map>
-#include <unordered_set>
-#include <utility>
-
-#include "../../runtime/thread_storage_scope.h"
-#include "../../s_tir/ir/ir_mutator_with_analyzer.h"
-#include "../../sym/interval_set.h"
-#include "ir_utils.h"
-
-namespace tvm {
-namespace s_tir {
-using namespace tvm::tirx;
-
-enum class HoistedConditionals : int {
-  kNone = 0,
-  kIfElseStmt = (1 << 0),
-  kIfElseExpr = (1 << 1),
-  kBooleanExpression = (1 << 2),
-  kUsingBlockVar = (1 << 3),
-};
-
-enum class HoistedLetBindings : int {
-  kNone = 0,
-  kRequiredByCondition = (1 << 0),
-  kBind = (1 << 1),
-  kLetExpr = (1 << 2),
-};
-
-struct HoistExpressionConfigNode : public ffi::Object {
-  int hoisted_conditionals;
-  int hoisted_let_bindings;
-
-  static void RegisterReflection() {
-    namespace refl = tvm::ffi::reflection;
-    refl::ObjectDef<HoistExpressionConfigNode>()
-        .def_ro("hoisted_conditionals", 
&HoistExpressionConfigNode::hoisted_conditionals,
-                "Bitflags for the types of boolean expressions to hoist",
-                
refl::DefaultValue(static_cast<int>(HoistedConditionals::kIfElseStmt) |
-                                   
static_cast<int>(HoistedConditionals::kIfElseExpr) |
-                                   
static_cast<int>(HoistedConditionals::kBooleanExpression)))
-        .def_ro("hoisted_let_bindings", 
&HoistExpressionConfigNode::hoisted_let_bindings,
-                "Bitflags for the types of let bindings to hoist",
-                
refl::DefaultValue(static_cast<int>(HoistedLetBindings::kRequiredByCondition) |
-                                   static_cast<int>(HoistedLetBindings::kBind) 
|
-                                   
static_cast<int>(HoistedLetBindings::kLetExpr)));
-  }
-
-  bool FlagSet(HoistedConditionals flag) const {
-    return static_cast<int>(flag) & hoisted_conditionals;
-  }
-  bool FlagSet(HoistedLetBindings flag) const {
-    return static_cast<int>(flag) & hoisted_let_bindings;
-  }
-  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("s_tir.transform.HoistExpressionConfig",
-                                    HoistExpressionConfigNode, ffi::Object);
-};
-
-class HoistExpressionConfig : public ffi::ObjectRef {
- public:
-  HoistExpressionConfig(int hoisted_conditionals, int hoisted_let_bindings) {
-    auto node = ffi::make_object<HoistExpressionConfigNode>();
-    node->hoisted_conditionals = hoisted_conditionals;
-    node->hoisted_let_bindings = hoisted_let_bindings;
-    data_ = std::move(node);
-  }
-  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(HoistExpressionConfig, 
ffi::ObjectRef,
-                                                HoistExpressionConfigNode);
-};
-
-TVM_FFI_STATIC_INIT_BLOCK() { HoistExpressionConfigNode::RegisterReflection(); 
}
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  ::tvm::transform::PassContext::RegisterConfigOption<HoistExpressionConfig>(
-      "s_tir.HoistExpression");
-}
-
-struct HoistIfThenElseConfigNode : public ffi::Object {
-  bool support_block_scope_hoisting;
-
-  static void RegisterReflection() {
-    namespace refl = tvm::ffi::reflection;
-    refl::ObjectDef<HoistIfThenElseConfigNode>().def_ro(
-        "support_block_scope_hoisting", 
&HoistIfThenElseConfigNode::support_block_scope_hoisting,
-        "Hoist if cond with block scope variables", refl::DefaultValue(false));
-  }
-  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("s_tir.transform.HoistIfThenElseConfig",
-                                    HoistIfThenElseConfigNode, ffi::Object);
-};
-
-class HoistIfThenElseConfig : public ffi::ObjectRef {
- public:
-  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(HoistIfThenElseConfig, 
ffi::ObjectRef,
-                                                HoistIfThenElseConfigNode);
-};
-
-TVM_FFI_STATIC_INIT_BLOCK() { HoistIfThenElseConfigNode::RegisterReflection(); 
}
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  ::tvm::transform::PassContext::RegisterConfigOption<HoistIfThenElseConfig>(
-      "s_tir.HoistIfThenElse");
-}
-
-class HoistInfoCollector : public StmtExprVisitor {
- public:
-  using StmtExprVisitor::Visit_;
-  struct ConditionInfo {
-    ConditionInfo(PrimExpr condition, HoistedConditionals hoist_from, bool 
uses_block_var,
-                  std::unordered_set<const VarNode*> required_let_bindings, 
bool generate_else_case)
-        : condition(condition),
-          hoist_from(hoist_from),
-          uses_block_var(uses_block_var),
-          required_let_bindings(required_let_bindings),
-          generate_else_case(generate_else_case) {}
-    PrimExpr condition;
-    HoistedConditionals hoist_from;
-    bool uses_block_var;
-    std::unordered_set<const VarNode*> required_let_bindings;
-    bool generate_else_case;
-
-    bool IsEnabled(const HoistExpressionConfig& config) const {
-      bool valid_source = config->FlagSet(hoist_from);
-
-      bool all_required_bindings_are_hoisted =
-          required_let_bindings.empty() ||
-          config->FlagSet(HoistedLetBindings::kRequiredByCondition) ||
-          config->FlagSet(HoistedLetBindings::kBind);
-
-      bool valid_block_var_usage =
-          config->FlagSet(HoistedConditionals::kUsingBlockVar) || 
!uses_block_var;
-      return valid_source && all_required_bindings_are_hoisted && 
valid_block_var_usage;
-    }
-  };
-
-  struct LetBindingInfo {
-    LetBindingInfo(Var var, PrimExpr value, HoistedLetBindings hoist_from)
-        : var(var), value(value), hoist_from(hoist_from) {}
-    Var var;
-    PrimExpr value;
-    HoistedLetBindings hoist_from;
-
-    bool IsEnabled(const HoistExpressionConfig& config) const {
-      return config->FlagSet(hoist_from);
-    }
-  };
-
-  struct HoistInfo {
-    // The loop variable
-    Var loop_var;
-
-    // The loop or scope that defines the loop var.
-    Stmt loop_def;
-
-    // Bindings defined in Bind nodes inside the for-loop whose value
-    // does not depend on the loop variable.  These can be hoisted
-    // outside this for-loop.
-    std::vector<LetBindingInfo> let_bindings;
-
-    // Conditions evaluated inside the for-loop whose value does not
-    // depend on the loop variable.  These can be hoisted outside this
-    // for loop.  These may depend on the let_bindings.
-    std::vector<ConditionInfo> conditions;
-
-    // Only conditions that impact the entire body of the loop
-    // hoisted.  Conditionals may not be hoisted from inside a
-    // sequential node to outside.
-    bool reached_sequential_node{false};
-
-    // True if the loop variable representing a block variable
-    // (e.g. blockIdx.x, threadIdx.x), false otherwise.
-    bool IsBlockVariable() const { return !loop_def.as<ForNode>(); }
-  };
-
-  static std::vector<HoistInfo> Collect(Stmt stmt, HoistExpressionConfig 
config) {
-    auto collector = ffi::make_object<HoistInfoCollector>(config);
-    collector->Visit(stmt);
-    return collector->completed_loops;
-  }
-
- private:
-  using Parent = StmtExprVisitor;
-
- public:
-  explicit HoistInfoCollector(HoistExpressionConfig config) : config(config) {}
-
- private:
-  void AttemptHoistConditional(PrimExpr cond, HoistedConditionals hoist_from,
-                               bool generate_else_block = true) {
-    if (SideEffect(cond) > CallEffectKind::kPure) {
-      return;
-    }
-    if (auto info = FindHoistDestination(cond)) {
-      if (!info->reached_sequential_node) {
-        // Record whether this conditional uses any block variables.
-        auto walkfn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
-          return active_block_vars.count(var.get())
-                     ? ffi::WalkResult::Interrupt(ffi::VisitInterrupt(var))
-                     : ffi::WalkResult::Advance();
-        };
-        bool uses_block_var =
-            active_block_vars.size() &&
-            ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(cond, 
walkfn).has_value();
-
-        std::unordered_set<const VarNode*> let_bindings_used;
-
-        for (Var var : UndefinedVars(cond)) {
-          auto it = let_var_to_let_vars.find(var.get());
-          if (it != let_var_to_let_vars.end()) {
-            let_bindings_used.insert(it->first);
-            for (auto used : it->second) {
-              let_bindings_used.insert(used);
-            }
-          }
-        }
-        info->conditions.push_back(ConditionInfo(cond, hoist_from, 
uses_block_var,
-                                                 let_bindings_used, 
generate_else_block));
-      }
-    }
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const prim::AndNode* op) final {
-    AttemptHoistConditional(op->a, HoistedConditionals::kBooleanExpression);
-    AttemptHoistConditional(op->b, HoistedConditionals::kBooleanExpression);
-    return Parent::Visit_(op);
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const prim::OrNode* op) final {
-    AttemptHoistConditional(op->a, HoistedConditionals::kBooleanExpression);
-    AttemptHoistConditional(op->b, HoistedConditionals::kBooleanExpression);
-    return Parent::Visit_(op);
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const ForNode* op) final {
-    active_loops.push_back({op->loop_var, ffi::GetRef<Stmt>(op)});
-    active_loop_vars.insert(op->loop_var.get());
-
-    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Parent::Visit_(op));
-    completed_loops.push_back(active_loops.back());
-
-    active_loop_vars.erase(op->loop_var.get());
-    active_loops.pop_back();
-    return std::nullopt;
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const RegionStmtNode* op) final {
-    if (op->op.same_as(tirx::launch_thread_op())) {
-      Var var = op->body_params[0].as_or_throw<PrimVar>();
-      active_block_vars.insert(var.get());
-      active_loop_vars.insert(var.get());
-      active_loops.push_back({var, ffi::GetRef<Stmt>(op)});
-      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Parent::Visit_(op));
-      completed_loops.push_back(active_loops.back());
-      active_loops.pop_back();
-      active_loop_vars.erase(var.get());
-      active_block_vars.erase(var.get());
-      return std::nullopt;
-    }
-    // Unknown operations have no code-motion semantics. Analyze their inner
-    // loops independently without hoisting across the region boundary.
-    auto outer_loops = std::move(active_loops);
-    auto outer_loop_vars = std::move(active_loop_vars);
-    auto outer_block_vars = std::move(active_block_vars);
-    active_loops.clear();
-    active_loop_vars.clear();
-    active_block_vars.clear();
-    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Parent::Visit_(op));
-    active_loops = std::move(outer_loops);
-    active_loop_vars = std::move(outer_loop_vars);
-    active_block_vars = std::move(outer_block_vars);
-    if (!active_loops.empty()) active_loops.back().reached_sequential_node = 
true;
-    return std::nullopt;
-  }
-
-  void VisitBinding(Var var, PrimExpr value, HoistedLetBindings hoist_from) {
-    TVM_FFI_ICHECK_EQ(let_var_to_loop_vars.count(var.get()), 0)
-        << "Multiple nested definitions of variable " << var;
-    TVM_FFI_ICHECK_EQ(let_var_to_let_vars.count(var.get()), 0)
-        << "Multiple nested definitions of variable " << var;
-
-    if (auto info = FindHoistDestination(value)) {
-      if (!info->reached_sequential_node) {
-        info->let_bindings.push_back(LetBindingInfo(var, value, hoist_from));
-      }
-    }
-
-    // Walk through the loop binding
-    std::unordered_set<const VarNode*> loop_vars_used;
-    std::unordered_set<const VarNode*> let_bindings_used;
-    for (Var var : UndefinedVars(value)) {
-      if (active_loop_vars.count(var.get())) {
-        loop_vars_used.insert(var.get());
-      } else {
-        auto it = let_var_to_loop_vars.find(var.get());
-        if (it != let_var_to_loop_vars.end()) {
-          for (const VarNode* used : it->second) {
-            loop_vars_used.insert(used);
-          }
-        }
-      }
-
-      auto it = let_var_to_let_vars.find(var.get());
-      if (it != let_var_to_let_vars.end()) {
-        let_bindings_used.insert(it->first);
-        for (const VarNode* used : it->second) {
-          let_bindings_used.insert(used);
-        }
-      }
-    }
-
-    let_var_to_loop_vars[var.get()] = std::move(loop_vars_used);
-    let_var_to_let_vars[var.get()] = std::move(let_bindings_used);
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const BindNode* op) final {
-    if (auto value = op->value.as<PrimExpr>()) {
-      VisitBinding(op->var, value.value(), HoistedLetBindings::kBind);
-    }
-    return Parent::Visit_(op);
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const SeqStmtNode* op) final {
-    if (active_loops.size()) {
-      int non_bind_count = 0;
-      for (size_t i = 0; i < op->seq.size(); ++i) {
-        const auto* bind = op->seq[i].as<BindNode>();
-        if (!bind) {
-          non_bind_count++;
-        } else if (const auto* call = bind->value.as<CallNode>();
-                   call && (call->op.same_as(tirx::alloc_tensor_op()) ||
-                            call->op.same_as(tirx::decl_tensor_op()))) {
-          non_bind_count++;
-        }
-      }
-      if (non_bind_count > 1) {
-        active_loops.back().reached_sequential_node = true;
-      }
-    }
-    std::vector<const VarNode*> seq_bind_vars;
-    for (size_t i = 0; i < op->seq.size(); ++i) {
-      if (auto* bind = op->seq[i].as<BindNode>()) {
-        seq_bind_vars.push_back(bind->var.get());
-      }
-      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Visit(op->seq[i]));
-    }
-    for (auto* var : seq_bind_vars) {
-      let_var_to_loop_vars.erase(var);
-      let_var_to_let_vars.erase(var);
-    }
-    return std::nullopt;
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const prim::LetNode* op) final {
-    VisitBinding(op->var, op->value, HoistedLetBindings::kLetExpr);
-
-    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Parent::Visit_(op));
-
-    let_var_to_loop_vars.erase(op->var.get());
-    let_var_to_let_vars.erase(op->var.get());
-    return std::nullopt;
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const IfNode* op) final {
-    AttemptHoistConditional(op->condition, HoistedConditionals::kIfElseStmt,
-                            op->else_case.has_value());
-    return Parent::Visit_(op);
-  }
-
-  ffi::Optional<VisitInterrupt> Visit_(const CallNode* op) final {
-    if (op->op.same_as(prim::if_then_else_op())) {
-      PrimExpr cond = op->args[0].as_or_throw<PrimExpr>();
-      AttemptHoistConditional(cond, HoistedConditionals::kIfElseExpr);
-    }
-    return Parent::Visit_(op);
-  }
-
-  // Find the loop above which this expression could be hoisted.  If
-  // nullptr, the expression cannot be hoisted.
-  HoistInfo* FindHoistDestination(PrimExpr expr) {
-    // Cannot hoist above a loop if we aren't already in a loop.
-    if (active_loops.empty()) {
-      return nullptr;
-    }
-
-    for (auto it = active_loops.rbegin(); it != active_loops.rend(); it++) {
-      Var loop_var = it->loop_var;
-      auto walkfn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
-        bool matches = var.get() == loop_var.get();
-        if (!matches) {
-          auto it = let_var_to_loop_vars.find(var.get());
-          matches = it != let_var_to_loop_vars.end() && 
it->second.count(loop_var.get());
-        }
-        return matches ? ffi::WalkResult::Interrupt(ffi::VisitInterrupt(var))
-                       : ffi::WalkResult::Advance();
-      };
-      bool uses_loop_var = 
ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(expr, walkfn).has_value();
-
-      bool is_disabled_hoist_across_block_var =
-          !config->FlagSet(HoistedConditionals::kUsingBlockVar) && 
it->IsBlockVariable();
-
-      if (it->reached_sequential_node || uses_loop_var || 
is_disabled_hoist_across_block_var) {
-        if (it == active_loops.rbegin()) {
-          // Cannot hoist beyond the innermost loop iterator.
-          return nullptr;
-        } else {
-          // Hoist to just below the loop iterator that is required.
-          it--;
-          return &(*it);
-        }
-      }
-    }
-
-    // If no loop variables are used, can hoist above the outermost
-    // loop.
-    return &active_loops.front();
-  }
-
-  // The user-provided config describing which expressions should be
-  // hoisted.
-  HoistExpressionConfig config;
-
-  // Current hardware launch bindings of block variables.
-  std::unordered_set<const VarNode*> active_block_vars;
-
-  // An ordered list of loops that are currently being visited.
-  std::vector<HoistInfo> active_loops;
-
-  // Loops that have already been visited
-  std::vector<HoistInfo> completed_loops;
-
-  // Map from a bound variable to the loop variables it depends on.
-  // Includes indirect usage.
-  std::unordered_map<const VarNode*, std::unordered_set<const VarNode*>> 
let_var_to_loop_vars;
-
-  // Map from a bound variable to the other let bindings it depends on.
-  // Includes indirect usage.
-  std::unordered_map<const VarNode*, std::unordered_set<const VarNode*>> 
let_var_to_let_vars;
-
-  // Lookup table for the currently active loops.
-  std::unordered_set<const VarNode*> active_loop_vars;
-};
-
-class ExpressionHoister : public s_tir::IRMutatorWithAnalyzer {
- public:
-  using s_tir::IRMutatorWithAnalyzer::Mutate;
-  using s_tir::IRMutatorWithAnalyzer::Mutate_;
-
-  static Stmt Hoist(Stmt stmt, HoistExpressionConfig config) {
-    auto loop_info = HoistInfoCollector::Collect(stmt, config);
-
-    sym::Analyzer analyzer;
-    auto hoister = ffi::make_object<ExpressionHoister>(std::move(loop_info), 
config, analyzer);
-    stmt = hoister->Mutate(stmt, 
InplaceMode::kAllow).ValueOrUnchanged(std::move(stmt));
-    stmt = s_tir::ConvertSSA(std::move(stmt));
-    return stmt;
-  }
-
- private:
-  using Parent = s_tir::IRMutatorWithAnalyzer;
-
- public:
-  explicit ExpressionHoister(std::vector<HoistInfoCollector::HoistInfo> 
loop_info,
-                             HoistExpressionConfig config, const 
sym::Analyzer& analyzer)
-      : Parent(analyzer), config_(config) {
-    for (auto& info : loop_info) {
-      // Mark let bindings to use if they are enabled on their own.
-      for (const auto& binding : info.let_bindings) {
-        if (binding.IsEnabled(config)) {
-          hoisted_let_bindings.insert(binding.var.get());
-        }
-      }
-
-      // Or if they are required by a conditional
-      if (config->FlagSet(HoistedLetBindings::kRequiredByCondition)) {
-        for (const auto& conditional : info.conditions) {
-          if (conditional.IsEnabled(config)) {
-            for (const auto& var : conditional.required_let_bindings) {
-              hoisted_let_bindings.insert(var);
-            }
-          }
-        }
-      }
-
-      loop_info_lookup.insert_or_assign(info.loop_def.get(), std::move(info));
-    }
-  }
-
- private:
-  Stmt WrapHoistedStatements(Stmt stmt, const HoistInfoCollector::HoistInfo& 
info) {
-    for (auto cond_it = info.conditions.rbegin(); cond_it != 
info.conditions.rend(); cond_it++) {
-      if (cond_it->IsEnabled(config_)) {
-        if (cond_it->generate_else_case) {
-          stmt = If(cond_it->condition, stmt, SeqStmt(stmt));
-        } else {
-          stmt = If(cond_it->condition, stmt);
-        }
-      }
-    }
-    {
-      ffi::Array<Stmt> binds;
-      for (auto let_it = info.let_bindings.begin(); let_it != 
info.let_bindings.end(); let_it++) {
-        if (hoisted_let_bindings.count(let_it->var.get())) {
-          binds.push_back(Bind(let_it->var, let_it->value));
-        }
-      }
-      if (!binds.empty()) {
-        binds.push_back(stmt);
-        stmt = SeqStmt(binds);
-      }
-    }
-
-    return stmt;
-  }
-
-  UnchangedOr<Stmt> Mutate_(const ForNode* op, InplaceMode inplace_mode) final 
{
-    Stmt stmt = Parent::Mutate_(op, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(op));
-
-    auto it = loop_info_lookup.find(op);
-    TVM_FFI_ICHECK(it != loop_info_lookup.end())
-        << "Could not find pre-pass information for loop over " << 
op->loop_var;
-    return WrapHoistedStatements(stmt, it->second);
-  }
-
-  UnchangedOr<Stmt> Mutate_(const RegionStmtNode* op, InplaceMode 
inplace_mode) final {
-    Stmt stmt = Parent::Mutate_(op, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(op));
-    auto it = loop_info_lookup.find(op);
-    return it == loop_info_lookup.end() ? stmt : WrapHoistedStatements(stmt, 
it->second);
-  }
-
-  UnchangedOr<Stmt> Mutate_(const BindNode* op, InplaceMode inplace_mode) 
final {
-    if (hoisted_let_bindings.count(op->var.get())) {
-      // The binding was hoisted; remove it from this location.
-      return Evaluate(0);
-    } else {
-      return Parent::Mutate_(op, inplace_mode);
-    }
-  }
-
-  UnchangedOr<PrimExpr> Mutate_(const prim::LetNode* op, InplaceMode 
inplace_mode) final {
-    if (hoisted_let_bindings.count(op->var.get())) {
-      return this->Mutate(op->body, inplace_mode).ValueOrUnchanged(op->body);
-    } else {
-      return Parent::Mutate_(op, inplace_mode);
-    }
-  }
-
-  HoistExpressionConfig config_;
-
-  std::unordered_map<const StmtNode*, HoistInfoCollector::HoistInfo> 
loop_info_lookup;
-  std::unordered_set<const VarNode*> hoisted_let_bindings;
-};
-
-Stmt HoistExpression(Stmt stmt, HoistExpressionConfig config) {
-  return ExpressionHoister::Hoist(stmt, config);
-}
-
-namespace transform {
-
-Pass HoistExpression() {
-  auto pass_func = [=](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
-    auto* n = f.CopyOnWrite();
-    auto cfg = 
ctx->GetConfigOrDefault<HoistExpressionConfig>("s_tir.HoistExpression");
-    n->body = ExpressionHoister::Hoist(std::move(n->body).value(), cfg);
-    return f;
-  };
-  auto insertion_pass = CreateFunctionPass(pass_func, 0, 
"s_tir.InsertHoistedExpression");
-
-  return tvm::transform::Sequential(
-      {
-          insertion_pass,
-          s_tir::transform::StmtSimplify(),
-          tirx::transform::RemoveNoOp(),
-      },
-      "s_tir.HoistExpression");
-}
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("s_tir.transform.HoistExpression", HoistExpression);
-}
-
-static Pass HoistIfThenElseImpl() {
-  auto pass_func = [=](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
-    auto* n = f.CopyOnWrite();
-    auto cfg = 
ctx->GetConfigOrDefault<HoistIfThenElseConfig>("s_tir.HoistIfThenElse");
-    auto flag = 
f->GetAttr<int64_t>(tvm::s_tir::attr::kHoistIfThenElseExprWithBlock);
-    if (flag && flag.value() == 1) {
-      HoistExpressionConfig 
config(static_cast<int>(HoistedConditionals::kUsingBlockVar) |
-                                       
static_cast<int>(HoistedConditionals::kIfElseExpr),
-                                   
static_cast<int>(HoistedLetBindings::kNone));
-      n->body = ExpressionHoister::Hoist(std::move(n->body).value(), config);
-      return f;
-    }
-    int block_var =
-        static_cast<int>(cfg->support_block_scope_hoisting ? 
HoistedConditionals::kUsingBlockVar
-                                                           : 
HoistedConditionals::kNone);
-    HoistExpressionConfig config(block_var | 
static_cast<int>(HoistedConditionals::kIfElseStmt),
-                                 static_cast<int>(HoistedLetBindings::kNone));
-    n->body = ExpressionHoister::Hoist(std::move(n->body).value(), config);
-    return f;
-  };
-  auto insertion_pass = CreateFunctionPass(pass_func, 0, 
"s_tir.InsertHoistIfThenElse");
-  return tvm::transform::Sequential(
-      {
-          insertion_pass,
-          s_tir::transform::StmtSimplify(),
-          tirx::transform::RemoveNoOp(),
-      },
-      "s_tir.HoistIfThenElse");
-}
-
-static Pass HoistIfThenElseBasicImpl() {
-  auto pass_func = [=](Function f, IRModule m, PassContext ctx) {
-    if (!f->body.has_value()) return f;
-    auto* n = f.CopyOnWrite();
-    HoistExpressionConfig 
config(static_cast<int>(HoistedConditionals::kIfElseStmt),
-                                 static_cast<int>(HoistedLetBindings::kNone));
-    n->body = ExpressionHoister::Hoist(std::move(n->body).value(), config);
-    return f;
-  };
-  auto insertion_pass = CreateFunctionPass(pass_func, 0, 
"s_tir.InsertHoistIfThenElseBasic");
-  return tvm::transform::Sequential(
-      {
-          insertion_pass,
-          s_tir::transform::StmtSimplify(),
-          tirx::transform::RemoveNoOp(),
-      },
-      "s_tir.HoistIfThenElseBasic");
-}
-
-Pass HoistIfThenElse(tvm::ffi::String variant) {
-  if (variant == "basic") {
-    return HoistIfThenElseBasicImpl();
-  }
-  return HoistIfThenElseImpl();
-}
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("s_tir.transform.HoistIfThenElse", 
HoistIfThenElseImpl);
-  refl::GlobalDef().def("s_tir.transform.HoistIfThenElseBasic", 
HoistIfThenElseBasicImpl);
-}
-
-}  // namespace transform
-
-}  // namespace s_tir
-}  // namespace tvm
diff --git a/src/tirx/transform/hoist_if.cc b/src/tirx/transform/hoist_if.cc
new file mode 100644
index 0000000000..09b01166f6
--- /dev/null
+++ b/src/tirx/transform/hoist_if.cc
@@ -0,0 +1,264 @@
+/*
+ * 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 hoist_if.cc
+ * \brief Hoist loop-invariant If statements.
+ */
+#include <tvm/ffi/reflection/registry.h>
+#include <tvm/ir/prim/op.h>
+#include <tvm/ir/scope_stack.h>
+#include <tvm/tirx/analysis.h>
+#include <tvm/tirx/op/region.h>
+#include <tvm/tirx/stmt_functor.h>
+#include <tvm/tirx/transform.h>
+
+#include <algorithm>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "ir_utils.h"
+
+namespace tvm {
+namespace tirx {
+
+class IfHoister : public StmtExprMutator {
+ public:
+  using StmtExprMutator::Mutate;
+  using StmtExprMutator::Mutate_;
+
+  UnchangedOr<ffi::Any> Mutate(ffi::AnyView input, InplaceMode inplace_mode) 
final {
+    // Expressions are inspected only as whole If predicates, never split or 
hoisted.
+    if (input.as<ExprNode>()) return ffi::Unchanged();
+    if (input.as<StmtNode>() && !input.as<ForNode>() && !input.as<IfNode>() &&
+        !input.as<SeqStmtNode>()) {
+      // Bindings, regions and unknown statements have no code-motion contract.
+      // Hide outer loops and their variable depths while visiting this 
subtree;
+      // active_loops_/split_in_nest_ still bound duplication of the enclosing 
nest.
+      return scopes_.WithNewScope([&] { return StmtExprMutator::Mutate(input, 
inplace_mode); });
+    }
+    return StmtExprMutator::Mutate(input, inplace_mode);
+  }
+
+ private:
+  struct LoopState {
+    // Enclosing alternate-branch depth at loop entry, before visiting its 
body.
+    int else_depth_at_entry;
+    // Predicates lifted to this loop, in traversal order; bool retains a 
false-path copy.
+    std::vector<std::pair<PrimExpr, bool>> conditions;
+  };
+
+  struct ScopeState {
+    // Active destination loops, outermost first, within the current motion 
boundary.
+    std::vector<LoopState*> loops;
+    // Identity-based indices into loops for variables defined by those loops.
+    std::unordered_map<const VarNode*, size_t> loop_depths;
+  };
+
+  UnchangedOr<Stmt> Mutate_(const SeqStmtNode* op, InplaceMode inplace_mode) 
final {
+    // Sibling statements isolate loop placement; a singleton stays 
transparent.
+    // An If inside one sibling must not move outside the loop executing all 
siblings.
+    if (op->seq.size() == 1) return StmtExprMutator::Mutate_(op, inplace_mode);
+    return scopes_.WithNewScope([&] { return StmtExprMutator::Mutate_(op, 
inplace_mode); });
+  }
+
+  // Purity alone does not permit evaluating a predicate before its guards or
+  // before a zero-trip loop.  Check safety and loop dependencies in one walk.
+  size_t FindLiftDestination(const PrimExpr& condition) const {
+    const auto& scope = scopes_.Current();
+    // First loop the predicate can cross: loops [i, j, k] and i + j select 
index 2 (k).
+    size_t destination = 0;
+    std::unordered_set<const ExprNode*> visited;
+    auto advance = [&](const ExprNode* op) -> ffi::Expected<ffi::WalkResult> {
+      // StructuralWalk visits occurrences. For Add(e, e), safety and 
dependencies
+      // need checking only once per shared e, within this predicate's fixed 
scope.
+      return visited.insert(op).second ? ffi::WalkResult::Advance() : 
ffi::WalkResult::Skip();
+    };
+    auto division = [&](const ExprNode* op,
+                        const PrimExpr& divisor) -> 
ffi::Expected<ffi::WalkResult> {
+      // Positive constant divisors exclude both zero and signed min / -1.
+      const auto* value = divisor.as<IntImmNode>();
+      if (!value || value->value <= 0) return ffi::WalkResult::Interrupt();
+      return advance(op);
+    };
+    auto blocked = ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(
+        condition,
+        [&](const VarNode* op) -> ffi::Expected<ffi::WalkResult> {
+          // Stay inside every referenced loop variable's definition. Other
+          // variables remain in scope at every candidate destination.
+          auto it = scope.loop_depths.find(op);
+          if (it != scope.loop_depths.end()) destination = 
std::max(destination, it->second + 1);
+          // An innermost-loop dependency already rules out every destination.
+          if (destination == scope.loops.size()) return 
ffi::WalkResult::Interrupt();
+          // Variable types describe metadata, not evaluated predicate 
dependencies.
+          return ffi::WalkResult::Skip();
+        },
+        [](const TypeNode*) -> ffi::Expected<ffi::WalkResult> { return 
ffi::WalkResult::Skip(); },
+        [](const TensorLoadNode*) -> ffi::Expected<ffi::WalkResult> {
+          // Memory reads can change across iterations or depend on an 
execution guard.
+          return ffi::WalkResult::Interrupt();
+        },
+        [&](const prim::DivNode* op) { return division(op, op->b); },
+        [&](const prim::ModNode* op) { return division(op, op->b); },
+        [&](const prim::FloorDivNode* op) { return division(op, op->b); },
+        [&](const prim::FloorModNode* op) { return division(op, op->b); },
+        [](const prim::LShiftNode*) -> ffi::Expected<ffi::WalkResult> {
+          // A shift count may be outside the defined range.
+          return ffi::WalkResult::Interrupt();
+        },
+        [](const prim::RShiftNode*) -> ffi::Expected<ffi::WalkResult> {
+          // A shift count may be outside the defined range.
+          return ffi::WalkResult::Interrupt();
+        },
+        [&](const prim::CastNode* op) -> ffi::Expected<ffi::WalkResult> {
+          // Floating-to-integer conversion may be undefined outside the 
integer range.
+          if (ffi::GetRef<PrimExpr>(op).ty().MatchesCode(kDLInt, kDLUInt) &&
+              !op->value.ty().MatchesCode(kDLInt, kDLUInt))
+            return ffi::WalkResult::Interrupt();
+          return advance(op);
+        },
+        [&](const CallNode* op) -> ffi::Expected<ffi::WalkResult> {
+          // Even pure calls may trap; only the transparent likely hint is 
known safe.
+          if (!op->op.same_as(prim::likely_op())) return 
ffi::WalkResult::Interrupt();
+          return advance(op);
+        },
+        advance);
+    return blocked.has_value() ? scope.loops.size() : destination;
+  }
+
+  UnchangedOr<Stmt> Mutate_(const RegionStmtNode* op, InplaceMode 
inplace_mode) final {
+    // Moving guards inside a thread launch can prevent later synchronization
+    // insertion, even when the guards themselves are loop-invariant.
+    if (op->op.same_as(launch_thread_op())) return ffi::Unchanged();
+    return StmtExprMutator::Mutate_(op, inplace_mode);
+  }
+
+  UnchangedOr<Stmt> Mutate_(const ForNode* op, InplaceMode inplace_mode) final 
{
+    // Preserve the same synchronization boundary before thread-binding 
lowering.
+    if (GetThreadBinding(op).has_value()) return ffi::Unchanged();
+    // Only ordinary serial loops permit code motion across their execution 
scope.
+    // Hoisting must not suppress evaluation of an effectful loop header 
either.
+    if (op->kind != ForKind::kDefault || SideEffect(op->min) > 
CallEffectKind::kReadState ||
+        SideEffect(op->extent) > CallEffectKind::kReadState ||
+        (op->step && SideEffect(op->step.value()) > 
CallEffectKind::kReadState)) {
+      return scopes_.WithNewScope([&] { return StmtExprMutator::Mutate_(op, 
inplace_mode); });
+    }
+    auto& scope = scopes_.Current();
+    // A new outermost eligible loop gets one two-sided split. Do not reset at
+    // scope barriers: an outer i / SeqStmt / inner j still shares this budget.
+    if (active_loops_++ == 0) split_in_nest_ = false;
+    // Snapshot ancestors outside this loop so they do not require a 
false-path copy.
+    LoopState loop{else_depth_, {}};
+    const VarNode* var = op->loop_var.get();
+    // Register this destination and its variable together. An index in loops
+    // identifies both where to wrap a lifted If and which variables it must 
avoid.
+    scope.loop_depths[var] = scope.loops.size();
+    scope.loops.push_back(&loop);
+    Stmt stmt = StmtExprMutator::Mutate_(op, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(op));
+    // Only descendants can lift to this loop; remove both entries before 
siblings.
+    // The local LoopState remains alive below to emit its collected 
conditions.
+    scope.loops.pop_back();
+    scope.loop_depths.erase(var);
+    // Reaching zero lets the next independent nest start with a fresh split 
budget.
+    --active_loops_;
+    // Collected p then q must become if p: if q: loop, so wrap q before p.
+    for (auto it = loop.conditions.rbegin(); it != loop.conditions.rend(); 
++it) {
+      // Duplicate only when an alternate branch must remain reachable.
+      const auto& [condition, preserve_else] = *it;
+      stmt = preserve_else ? If(condition, stmt, SeqStmt(stmt)) : 
If(condition, stmt);
+    }
+    return stmt;
+  }
+
+  UnchangedOr<Stmt> Mutate_(const IfNode* op, InplaceMode inplace_mode) final {
+    auto& scope = scopes_.Current();
+    // Moving a one-sided inner If could otherwise skip this predicate's 
effects.
+    if (!scope.loops.empty() && SideEffect(op->condition) > 
CallEffectKind::kReadState) {
+      return scopes_.WithNewScope([&] { return StmtExprMutator::Mutate_(op, 
inplace_mode); });
+    }
+    bool has_else = op->else_case.has_value();
+    if (!scope.loops.empty()) {
+      size_t destination = FindLiftDestination(op->condition);
+      if (destination < scope.loops.size()) {
+        // A false predicate must still execute an enclosing alternate branch
+        // inside the destination loop, even when this If has no else of its 
own.
+        // In for i: if p(i) { if q { A } } else { B }, q=false still needs B.
+        // The entry snapshot excludes Ifs outside this loop,
+        // whose alternate branches remain reachable without duplicating it.
+        bool preserve_else =
+            has_else || else_depth_ > 
scope.loops[destination]->else_depth_at_entry;
+        auto* loop = scope.loops[destination];
+        // Each two-sided predicate doubles the enclosing loop subtree. Limit
+        // splitting to once per loop nest, even across code-motion barriers.
+        if (!preserve_else || !split_in_nest_) {
+          // Record at the destination now; its For unwind wraps the complete
+          // rewritten body. Preorder collection preserves enclosing predicate 
order.
+          loop->conditions.emplace_back(op->condition, preserve_else);
+          // Hoists needing no false-path copy spend no budget. A copy consumes
+          // it for the rest of this nest, including loops behind scope 
barriers.
+          split_in_nest_ |= preserve_else;
+        }
+      }
+    }
+    // Descendant hoists must preserve this If's alternate arm, in either 
branch.
+    else_depth_ += has_else;
+    auto result = StmtExprMutator::Mutate_(op, inplace_mode);
+    // Restore the ancestor count so following siblings do not inherit this If.
+    else_depth_ -= has_else;
+    return result;
+  }
+
+  // Number of enclosing Ifs with an else; each loop saves its entry baseline.
+  int else_depth_{0};
+  // Active eligible loops across motion boundaries; zero starts a new split 
budget.
+  // Unlike scoped destinations, this stays nonzero inside a nested SeqStmt 
barrier.
+  int active_loops_{0};
+  // Whether this loop nest has used its one two-sided split, bounding subtree 
growth.
+  bool split_in_nest_{false};
+  // Scoped destinations/dependencies; barriers hide outer loops without 
resetting the budget.
+  ScopeStack<ScopeState> scopes_;
+};
+
+namespace transform {
+
+Pass HoistIf() {
+  auto pass_func = [](Function f, IRModule m, PassContext ctx) {
+    if (!f->body.has_value()) return f;
+    auto* n = f.CopyOnWrite();
+    Stmt body = ffi::make_object<IfHoister>()
+                    ->Mutate(n->body.value(), InplaceMode::kAllow)
+                    .ValueOrUnchanged(n->body.value());
+    n->body = tirx::ConvertSSA(std::move(body));
+    return f;
+  };
+  return tvm::transform::Sequential(
+      {CreateFunctionPass(pass_func, 0, "tirx.InsertHoistedIf"), 
StmtSimplify(), RemoveNoOp()},
+      "tirx.HoistIf");
+}
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  refl::GlobalDef().def("tirx.transform.HoistIf", HoistIf);
+}
+
+}  // namespace transform
+}  // namespace tirx
+}  // namespace tvm
diff --git a/src/tirx/transform/remove_no_op.cc 
b/src/tirx/transform/remove_no_op.cc
index 1a1790d21c..6e7419d48c 100644
--- a/src/tirx/transform/remove_no_op.cc
+++ b/src/tirx/transform/remove_no_op.cc
@@ -26,7 +26,6 @@
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/op.h>
 #include <tvm/ir/prim/op.h>
-#include <tvm/s_tir/stmt.h>
 #include <tvm/sym/analyzer.h>
 #include <tvm/tirx/analysis.h>
 #include <tvm/tirx/stmt.h>
diff --git a/tests/python/s_tir/dlight/test_gpu_low_batch_gemv.py 
b/tests/python/s_tir/dlight/test_gpu_low_batch_gemv.py
index 8cf6eebf66..ba2d320160 100644
--- a/tests/python/s_tir/dlight/test_gpu_low_batch_gemv.py
+++ b/tests/python/s_tir/dlight/test_gpu_low_batch_gemv.py
@@ -34,7 +34,7 @@ def test_batch_decode_gemv():
 
     @Ts.function(private=True)
     def before(lv429: T.Tensor((T.int64(4096), T.int64(3584)), "uint32"), 
lv430: T.Tensor((T.int64(4096), T.int64(896)), "float16"), lv807: 
T.Tensor((batch_size, T.int64(1), T.int64(28672)), 'float16'), 
NT_matmul_intermediate: T.Tensor((batch_size, T.int64(1), T.int64(4096)), 
'float16')):
-        T.func_attr({"tirx.noalias": True, 
"tirx.HoistIfThenElseExprWithBlock": 1})
+        T.func_attr({"tirx.noalias": True})
 
         # with Ts.sblock("root"):
         compute = Ts.sblock_alloc_buffer((T.int64(4096), T.int64(28672)), 
"float16")
@@ -64,7 +64,7 @@ def test_batch_decode_gemv():
 
     @Ts.function(private=True)
     def expected(lv429: T.Tensor((T.int64(4096), T.int64(3584)), "uint32"), 
lv430: T.Tensor((T.int64(4096), T.int64(896)), "float16"), lv807: 
T.Tensor((batch_size, T.int64(1), T.int64(28672)), 'float16'), 
NT_matmul_intermediate: T.Tensor((batch_size, T.int64(1), T.int64(4096)), 
'float16')):
-        T.func_attr({"tirx.HoistIfThenElseExprWithBlock": 1, 
"tirx.is_scheduled": True, "tirx.noalias": True})
+        T.func_attr({"tirx.is_scheduled": True, "tirx.noalias": True})
 
         # with Ts.sblock("root"):
         dequantize_intermediate_intermediate_local = 
Ts.sblock_alloc_buffer((T.int64(4096), T.int64(28672)), "float16", 
scope="local")
@@ -162,7 +162,7 @@ def test_batch_gemv():
 
     @Ts.function(private=True)
     def before(A: T.Tensor((batch_size, T.int64(1), T.int64(K)), 'float16'), 
B: T.Tensor((T.int64(N), T.int64(K)), "float16"), NT_matmul: 
T.Tensor((batch_size, T.int64(1), T.int64(N)), 'float16')):
-        T.func_attr({"tirx.noalias": True, 
"tirx.HoistIfThenElseExprWithBlock": 1})
+        T.func_attr({"tirx.noalias": True})
 
         # with Ts.sblock("root"):
         for i0, i1, i2, k in T.grid(batch_size, T.int64(1), T.int64(N), 
T.int64(K)):
@@ -178,7 +178,7 @@ def test_batch_gemv():
 
     @Ts.function(private=True)
     def expected(A: T.Tensor((batch_size, T.int64(1), T.int64(4096)), 
'float16'), B: T.Tensor((T.int64(4096), T.int64(4096)), "float16"), NT_matmul: 
T.Tensor((batch_size, T.int64(1), T.int64(4096)), 'float16')):
-        T.func_attr({"tirx.HoistIfThenElseExprWithBlock": 1, 
"tirx.is_scheduled": True, "tirx.noalias": True})
+        T.func_attr({"tirx.is_scheduled": True, "tirx.noalias": True})
 
         # with Ts.sblock("root"):
         NT_matmul_pad_local = Ts.sblock_alloc_buffer(((batch_size + 
T.int64(3)) // T.int64(4) * T.int64(4), T.int64(1), T.int64(4096)), "float16", 
scope="local")
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_hoist_expression.py 
b/tests/python/s_tir/transform/test_s_tir_transform_hoist_expression.py
deleted file mode 100644
index 4de0d34327..0000000000
--- a/tests/python/s_tir/transform/test_s_tir_transform_hoist_expression.py
+++ /dev/null
@@ -1,590 +0,0 @@
-# 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.
-# ruff: noqa: F401
-import enum
-
-import tvm
-import tvm.testing
-from tvm import s_tir
-from tvm.script import s_tir as Ts
-from tvm.script import tirx as T
-
-
-class HoistedConditionals(enum.Flag):
-    """Test flags for the native hoisted_conditionals pass option
-
-    Each bitflag represents a type of expression that should be
-    hoisted to the outermost loop possible.
-    """
-
-    Never = 0
-    """ No hoisting of conditionals """
-
-    IfElseStmt = 1
-    """ If set, look for hoist candidates in IfElseStmt """
-
-    IfElseExpr = 2
-    """ If set, look for hoist candidates in tirx.if_then_else """
-
-    BooleanExpression = 4
-    """ If set, look for hoist candidates in all boolean expressions """
-
-    UsingBlockVar = 8
-    """ If set, allow hoisting of conditionals that use a block variable (e.g. 
threadIdx.x)  """
-
-    All = IfElseStmt | IfElseExpr | BooleanExpression | UsingBlockVar
-    """ Enable all hoisting of conditionals"""
-
-
-class HoistedLetBindings(enum.Flag):
-    """Test flags for the native hoisted_let_bindings pass option
-
-    Each bitflag represents a type of let binding expression that should be
-    hoisted to the outermost loop possible.
-    """
-
-    Never = 0
-    """ No hoisting of let bindings """
-
-    RequiredByConditional = 1
-    """ Bindings that are used by a hoisted conditional """
-
-    Bind = 2
-    """ Bindings occurring in Bind nodes """
-
-    LetExpr = 4
-    """ Bindings occurring in Let expressions """
-
-    All = RequiredByConditional | Bind | LetExpr
-    """ Enable all hoisting of let bindings """
-
-
-def _run_transform(before, hoisted_conditionals, hoisted_let_bindings):
-    """Run HoistExpression transform with the given configuration."""
-    before_mod = tvm.IRModule.from_expr(before)
-
-    config = {
-        "s_tir.HoistExpression": {
-            "hoisted_conditionals": hoisted_conditionals.value,
-            "hoisted_let_bindings": hoisted_let_bindings.value,
-        }
-    }
-
-    with tvm.transform.PassContext(config=config):
-        after_mod = tvm.s_tir.transform.HoistExpression()(before_mod)
-
-    return after_mod["main"]
-
-
-def test_hoist_to_top_if_else_stmt():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((16,), "float32"), n: T.int32):
-        for i in T.serial(16):
-            if n != 0:
-                A[i] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((16,), "float32"), n: T.int32):
-        if n != 0:
-            for i in T.serial(16):
-                A[i] = 0.0
-
-    after = _run_transform(before, HoistedConditionals.IfElseStmt, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_to_top_all():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((16,), "float32"), n: T.int32):
-        for i in T.serial(16):
-            if n != 0:
-                A[i] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((16,), "float32"), n: T.int32):
-        if n != 0:
-            for i in T.serial(16):
-                A[i] = 0.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_suppress_hoist_if_else_never():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((16,), "float32"), n: T.int32):
-        for i in T.serial(16):
-            if n != 0:
-                A[i] = 0.0
-
-    expected = before
-
-    after = _run_transform(before, HoistedConditionals.Never, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_suppress_hoist_if_else_expr_only():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((16,), "float32"), n: T.int32):
-        for i in T.serial(16):
-            if n != 0:
-                A[i] = 0.0
-
-    expected = before
-
-    after = _run_transform(before, HoistedConditionals.IfElseExpr, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_block_var():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((128, 16), "float32"), n: T.int32):
-        i = T.launch_thread("threadIdx.x", 128)
-
-        for j in T.serial(16):
-            if i < 32:
-                A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((128, 16), "float32"), n: T.int32):
-        i = T.launch_thread("threadIdx.x", 128)
-
-        if i < 32:
-            for j in T.serial(16):
-                A[i, j] = 0.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_suppress_hoist_block_var():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((128, 16), "float32"), n: T.int32):
-        T.launch_thread("threadIdx.x", 128)
-
-        for i in T.thread_binding(0, 128, thread="threadIdx.x"):
-            if i < 32:
-                for j in T.serial(16):
-                    A[i, j] = 0.0
-
-    expected = before
-
-    after = _run_transform(
-        before,
-        HoistedConditionals.All & ~HoistedConditionals.UsingBlockVar,
-        HoistedLetBindings.All,
-    )
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_across_block_var():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((128, 16), "float32"), n: T.int32):
-        T.launch_thread("threadIdx.x", 128)
-
-        for i in T.thread_binding(0, 128, thread="threadIdx.x"):
-            if n == 0:
-                for j in T.serial(16):
-                    A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((128, 16), "float32"), n: T.int32):
-        if n == 0:
-            T.launch_thread("threadIdx.x", 128)
-            for i in T.thread_binding(0, 128, thread="threadIdx.x"):
-                for j in T.serial(16):
-                    A[i, j] = 0.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_suppress_hoist_across_block_var():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((128, 16), "float32"), n: T.int32):
-        T.launch_thread("threadIdx.x", 128)
-
-        for i in T.thread_binding(0, 128, thread="threadIdx.x"):
-            for j in T.serial(16):
-                if n == 0:
-                    A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((128, 16), "float32"), n: T.int32):
-        T.launch_thread("threadIdx.x", 128)
-        if n == 0:
-            for i in T.thread_binding(0, 128, thread="threadIdx.x"):
-                for j in T.serial(16):
-                    A[i, j] = 0.0
-
-    after = _run_transform(
-        before,
-        HoistedConditionals.All & ~HoistedConditionals.UsingBlockVar,
-        HoistedLetBindings.All,
-    )
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_to_middle():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                if i < 3:
-                    A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            if i < 3:
-                for j in T.serial(4):
-                    A[i, j] = 0.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_with_let():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                condition: T.let[T.bool] = i < 3
-                if condition:
-                    A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            condition: T.let[T.bool] = i < 3  # noqa: F841
-            if i < 3:
-                for j in T.serial(4):
-                    A[i, j] = T.float32(0.0)
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_disable_let():
-    """As test_hoist_with_let, but forbid hoisting of Bind
-
-    Because the condition depends on the let binding, it should no
-    longer be hoisted.  With Bind lifecycle management, the condition
-    var is erased at sequence boundaries, so the if-condition uses
-    the raw expression.
-    """
-
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                condition: T.let[T.bool] = i < 3
-                if condition:
-                    A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i, j in T.grid(4, 4):
-            condition: T.let[T.bool] = i < 3  # noqa: F841
-            if i < 3:
-                A[i, j] = T.float32(0.0)
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.Never)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_if_else():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                if i < 3:
-                    A[i, j] = 0.0
-                else:
-                    A[i, j] = 1.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            if i < 3:
-                for j in T.serial(4):
-                    A[i, j] = 0.0
-            else:
-                for j in T.serial(4):
-                    A[i, j] = 1.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_sequential_assign():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32"), B: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                if i < 3:
-                    A[i, j] = 0.0
-                    B[i, j] = 0.0
-                else:
-                    A[i, j] = 1.0
-                    B[i, j] = 1.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32"), B: T.Tensor((4, 4), 
"float32")):
-        for i in T.serial(4):
-            if i < 3:
-                for j in T.serial(4):
-                    A[i, j] = 0.0
-                    B[i, j] = 0.0
-            else:
-                for j in T.serial(4):
-                    A[i, j] = 1.0
-                    B[i, j] = 1.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_multi_if():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                for k in T.serial(4):
-                    if j < 3:
-                        if i < 2:
-                            A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            if i < 2:
-                for j in T.serial(4):
-                    if j < 3:
-                        for k in T.serial(4):
-                            A[i, j] = 0.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_complex_conditional():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i, j, k in T.grid(4, 4, 4):
-            if j < 3 and i < 2:
-                A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            if i < 2:
-                for j in T.serial(4):
-                    if j < 3:
-                        for k in T.serial(4):
-                            A[i, j] = 0.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_suppress_splitting_conditional():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i, j, k in T.grid(4, 4, 4):
-            if j < 3 and i < 2:
-                A[i, j] = 0.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i, j in T.grid(4, 4):
-            if j < 3 and i < 2:
-                for k in T.serial(4):
-                    A[i, j] = 0.0
-
-    after = _run_transform(
-        before,
-        HoistedConditionals.All & ~HoistedConditionals.BooleanExpression,
-        HoistedLetBindings.All,
-    )
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_multi_if_else():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                for k in T.serial(4):
-                    if j < 3:
-                        if i < 2:
-                            A[i, j] = 0.0
-                        else:
-                            A[i, j] = 1.0
-                    else:
-                        if i < 2:
-                            A[i, j] = 2.0
-                        else:
-                            A[i, j] = 3.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            if i < 2:
-                for j in T.serial(4):
-                    if j < 3:
-                        for k in T.serial(4):
-                            A[i, j] = 0.0
-                    else:
-                        for k in T.serial(4):
-                            A[i, j] = 2.0
-            else:
-                for j in T.serial(4):
-                    if j < 3:
-                        for k in T.serial(4):
-                            A[i, j] = 1.0
-                    else:
-                        for k in T.serial(4):
-                            A[i, j] = 3.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_multi_if_else_different_branches():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            for j in T.serial(4):
-                for k in T.serial(4):
-                    if j < 3:
-                        if i < 2:
-                            A[i, j] = 0.0
-                        else:
-                            A[i, j] = 1.0
-                    else:
-                        if i < 1:
-                            A[i, j] = 2.0
-                        else:
-                            A[i, j] = 3.0
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            if i < 2:
-                if i < 1:
-                    for j in T.serial(4):
-                        if j < 3:
-                            for k in T.serial(4):
-                                A[i, j] = 0.0
-                        else:
-                            for k in T.serial(4):
-                                A[i, j] = 2.0
-                else:
-                    for j in T.serial(4):
-                        if j < 3:
-                            for k in T.serial(4):
-                                A[i, j] = 0.0
-                        else:
-                            for k in T.serial(4):
-                                A[i, j] = 3.0
-            else:
-                for j in T.serial(4):
-                    if j < 3:
-                        for k in T.serial(4):
-                            A[i, j] = 1.0
-                    else:
-                        for k in T.serial(4):
-                            A[i, j] = 3.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_if_else_expr():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i, j in T.grid(4, 4):
-            A[i, j] = T.if_then_else(i < 2, 1.0, 2.0)
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            if i < 2:
-                for j in T.serial(4):
-                    A[i, j] = 1.0
-            else:
-                for j in T.serial(4):
-                    A[i, j] = 2.0
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_suppress_hoist_if_else_expr():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i, j in T.grid(4, 4):
-            A[i, j] = T.if_then_else(i < 2, 1.0, 2.0)
-
-    expected = before
-
-    after = _run_transform(
-        before,
-        HoistedConditionals.All & ~HoistedConditionals.IfElseExpr,
-        HoistedLetBindings.All,
-    )
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_hoist_let_expr():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i, j in T.grid(4, 4):
-            x = T.float32()
-            A[i, j] = T.Let(5.0 * x + T.cast(j, "float32"), where={x: T.cast(i 
+ 1, "float32")})
-
-    @Ts.function(private=True)
-    def expected(A: T.Tensor((4, 4), "float32")):
-        for i in T.serial(4):
-            x: T.let[T.float32] = T.cast(i + 1, "float32")  # noqa: F841
-            for j in T.serial(4):
-                A[i, j] = T.float32(5.0) * T.cast(i + 1, "float32") + 
T.cast(j, "float32")
-
-    after = _run_transform(before, HoistedConditionals.All, 
HoistedLetBindings.All)
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-def test_suppress_hoist_let_expr():
-    @Ts.function(private=True)
-    def before(A: T.Tensor((4, 4), "float32")):
-        for i, j in T.grid(4, 4):
-            x = T.float32()
-            A[i, j] = T.Let(5.0 * x + T.cast(j, "float32"), where={x: T.cast(i 
+ 1, "float32")})
-
-    expected = before
-
-    after = _run_transform(
-        before,
-        HoistedConditionals.All,
-        HoistedLetBindings.All & ~HoistedLetBindings.LetExpr,
-    )
-    tvm.ir.assert_structural_equal(after, expected)
-
-
-if __name__ == "__main__":
-    tvm.testing.main()
diff --git a/tests/python/s_tir/transform/test_s_tir_transform_hoist_if.py 
b/tests/python/s_tir/transform/test_s_tir_transform_hoist_if.py
deleted file mode 100644
index 10116492b9..0000000000
--- a/tests/python/s_tir/transform/test_s_tir_transform_hoist_if.py
+++ /dev/null
@@ -1,562 +0,0 @@
-# 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.
-# ruff: noqa: E741, F401
-import numpy as np
-import pytest
-import tvm_ffi
-
-import tvm
-from tvm import s_tir
-from tvm.script import ir as I
-from tvm.script import s_tir as Ts
-from tvm.script import tirx as T
-from tvm.testing import enabled_targets
-
-var_list = []
-
-
-def verify_structure(stmt, expected_struct):
-    node_dict = {}
-    struct = {}
-
-    def _extract_vars(op):
-        global var_list
-        if isinstance(op, tvm.tirx.Var):
-            var_list.append(op.name)
-
-    def _visit(op):
-        key = op
-        if isinstance(op, tvm.ir.If):
-            global var_list
-            tvm_ffi.structural_walk(op.condition, _extract_vars)
-            val = [(op.then_case, op.else_case), ("ir.If", tuple(var_list))]
-            var_list.clear()
-        elif isinstance(op, tvm.ir.For):
-            val = [(op.body,), ("ir.For", op.loop_var.name)]
-        elif isinstance(op, tvm.ir.RegionStmt):
-            assert op.op.name == "tirx.launch_thread"
-            val = [(op.body,), ("ir.RegionStmt", op.op.name, int(op.args[1]))]
-        else:
-            return
-        node_dict[key] = val
-
-    tvm_ffi.structural_walk(stmt, _visit)
-    for key, val in node_dict.items():
-        struct[val[1]] = tuple(
-            node_dict[child[0]][1]
-            if child is not None and len(child) == 1 and child[0] in node_dict
-            else None
-            for child in val[0]
-        )
-
-    assert struct == expected_struct, (
-        f"Structure mismatch: expect {expected_struct} but got {struct}"
-    )
-    var_list.clear()
-
-
-def _opaque_eval(var):
-    return tvm.ir.Evaluate(tvm.tirx.call_extern("int32", "dummy", var))
-
-
-def test_hoist_top_for():
-    @Ts.function(private=True)
-    def func(l: T.int32, m: T.int32, n: T.int32):
-        for i in T.serial(l):
-            for j in T.serial(m):
-                for k in T.serial(n):
-                    if T.likely(i < 2):
-                        T.evaluate(T.call_extern("dummy", m, ty="int32"))
-                    else:
-                        T.evaluate(T.call_extern("dummy", n, ty="int32"))
-
-    mod = tvm.IRModule.from_expr(func)
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    expected_struct = {
-        ("ir.For", "k"): (None,),
-        ("ir.For", "j"): (("ir.For", "k"),),
-        ("ir.If", ("i",)): (("ir.For", "j"), ("ir.For", "j")),
-        ("ir.For", "i"): (("ir.If", ("i",)),),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_hoist_multi_var_if():
-    @Ts.function(private=True)
-    def func(l: T.int32, m: T.int32, n: T.int32):
-        for i in T.serial(l):
-            for j in T.serial(m):
-                for k in T.serial(n):
-                    if T.likely(i + j < 2):
-                        T.evaluate(T.call_extern("dummy", m, ty="int32"))
-                    else:
-                        T.evaluate(T.call_extern("dummy", n, ty="int32"))
-
-    mod = tvm.IRModule.from_expr(func)
-    new_mod = tvm.s_tir.transform.HoistIfThenElse()(mod)
-    new_stmt = new_mod["main"].body
-    expected_struct = {
-        ("ir.For", "k"): (None,),
-        ("ir.If", ("i", "j")): (("ir.For", "k"), ("ir.For", "k")),
-        ("ir.For", "j"): (("ir.If", ("i", "j")),),
-        ("ir.For", "i"): (("ir.For", "j"),),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_hoist_no_match_for():
-    @Ts.function(private=True)
-    def func(data: T.handle("float32"), l: T.int32, m: T.int32, n: T.int32):
-        data_ptr = T.decl_tensor(1, "float32", data=data)
-        for i in T.serial(l):
-            for j in T.serial(m):
-                data_ptr[i * 3 + j] = data_ptr[i * 3 + j] + T.float32(0.5)
-                for k in T.serial(n):
-                    if T.likely(i < 2):
-                        T.evaluate(T.call_extern("dummy", m, ty="int32"))
-                    else:
-                        T.evaluate(T.call_extern("dummy", n, ty="int32"))
-
-    mod = tvm.IRModule.from_expr(func)
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    expected_struct = {
-        ("ir.For", "k"): (None,),
-        ("ir.If", ("i",)): (("ir.For", "k"), ("ir.For", "k")),
-        ("ir.For", "j"): (None,),
-        ("ir.For", "i"): (("ir.For", "j"),),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_no_else():
-    @Ts.function(private=True)
-    def func(l: T.int32, m: T.int32, n: T.int32):
-        for i in T.serial(l):
-            for j in T.serial(m):
-                for k in T.serial(n):
-                    if T.likely(i < 2):
-                        T.evaluate(T.call_extern("dummy", m, ty="int32"))
-
-    mod = tvm.IRModule.from_expr(func)
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    expected_struct = {
-        ("ir.For", "k"): (None,),
-        ("ir.For", "j"): (("ir.For", "k"),),
-        ("ir.If", ("i",)): (("ir.For", "j"), None),
-        ("ir.For", "i"): (("ir.If", ("i",)),),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_thread_launch():
-    dshape = (32, 64)
-
-    @Ts.function(private=True)
-    def func(data: T.handle("float32"), l: T.int32, m: T.int32, n: T.int32):
-        data_ptr = T.decl_tensor(1, "float32", data=data)
-        tx = T.launch_thread("threadIdx.x", dshape[0])
-        bx = T.launch_thread("blockIdx.x", dshape[1])
-        for i in T.serial(l):
-            for j in T.serial(m):
-                for k in T.serial(n):
-                    if i < 4 or j >= 8:
-                        data_ptr[bx * j + tx * j * k] = data_ptr[bx * j + tx * 
j * k] + T.float32(
-                            0.5
-                        )
-                    else:
-                        data_ptr[bx * j + tx * j * k] = data_ptr[bx * j + tx * 
j * k] + T.float32(
-                            1.0
-                        )
-
-    mod = tvm.IRModule.from_expr(func)
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    expected_struct = {
-        ("ir.For", "k"): (None,),
-        ("ir.If", ("i", "j")): (("ir.For", "k"), ("ir.For", "k")),
-        ("ir.For", "j"): (("ir.If", ("i", "j")),),
-        ("ir.For", "i"): (("ir.For", "j"),),
-        ("ir.RegionStmt", "tirx.launch_thread", 64): (("ir.For", "i"),),
-        ("ir.RegionStmt", "tirx.launch_thread", 32): (("ir.RegionStmt", 
"tirx.launch_thread", 64),),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_nested_for():
-    @Ts.function(private=True)
-    def func(data: T.handle("float32")):
-        data_ptr = T.decl_tensor(1, "float32", data=data)
-        for i in range(5):
-            for j in range(10):
-                if i >= 3:
-                    data_ptr[i * 3 + j] = data_ptr[i * 3 + j] + T.float32(0.5)
-                    for k in range(15):
-                        for l in range(20):
-                            if i < 4 or j >= 8:
-                                data_ptr[i * 3 + j + k + l] = data_ptr[
-                                    i * 3 + j + k + l
-                                ] * T.float32(2)
-                            else:
-                                data_ptr[i * 3 + j + k + l] = data_ptr[
-                                    i * 3 + j + k + l
-                                ] * T.float32(1.5)
-
-    mod = tvm.IRModule.from_expr(func)
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    expected_struct = {
-        ("ir.For", "l"): (None,),
-        ("ir.For", "k"): (("ir.For", "l"),),
-        ("ir.If", ("i", "j")): (("ir.For", "k"), ("ir.For", "k")),
-        ("ir.For", "j"): (None,),
-        ("ir.If", ("i",)): (("ir.For", "j"), None),
-        ("ir.For", "i"): (("ir.If", ("i",)),),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_if_block():
-    # Use different variable names for second loop nest to avoid dict key 
collision
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), n: T.int32):
-            # First loop nest: i, j, k, l
-            for i in T.serial(5):
-                for j in T.serial(10):
-                    if i >= 3:
-                        data[i * 3 + j] = data[i * 3 + j] + T.float32(0.5)
-                        for k in T.serial(15):
-                            for l in T.serial(20):
-                                if i < 4 or j >= 8:
-                                    data[i * 3 + j + k + l] = data[i * 3 + j + 
k + l] * T.float32(2)
-                                else:
-                                    data[i * 3 + j + k + l] = data[i * 3 + j + 
k + l] * T.float32(
-                                        1.5
-                                    )
-                                if j < 5:
-                                    data[i * 3 + j + k + l] = data[i * 3 + j + 
k + l] - T.float32(1)
-
-            # Second loop nest: i2, j2, k2 (different names)
-            for i2 in T.serial(5):
-                for j2 in T.serial(10):
-                    for k2 in T.serial(15):
-                        if n >= 3:
-                            data[i2 * 3 + j2 + k2] = data[i2 * 3 + j2 + k2] + 
T.float32(0.6)
-
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    # Updated expected_struct with renamed second nest variables
-    expected_struct = {
-        ("ir.If", ("i", "j")): (None, None),
-        ("ir.If", ("j",)): (None, None),
-        ("ir.For", "l"): (None,),
-        ("ir.For", "k"): (("ir.For", "l"),),
-        ("ir.For", "j"): (None,),
-        ("ir.If", ("i",)): (("ir.For", "j"), None),
-        ("ir.For", "i"): (("ir.If", ("i",)),),
-        ("ir.For", "k2"): (None,),
-        ("ir.For", "j2"): (("ir.For", "k2"),),
-        ("ir.For", "i2"): (("ir.For", "j2"),),
-        ("ir.If", ("n",)): (("ir.For", "i2"), None),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_multi_if():
-    @Ts.function(private=True)
-    def func(data: T.handle("float32")):
-        data_ptr = T.decl_tensor(1, "float32", data=data)
-        for i in range(10):
-            for j in range(10):
-                for k in range(10):
-                    if 3 <= i:
-                        if 3 <= j:
-                            data_ptr[i * 100 + j * 10 + k] = data_ptr[
-                                i * 100 + j * 10 + k
-                            ] + T.float32(0.5)
-
-    mod = tvm.IRModule.from_expr(func)
-    new_mod = tvm.s_tir.transform.HoistIfThenElse()(mod)
-    new_stmt = new_mod["main"].body
-    expected_struct = {
-        ("ir.For", "k"): (None,),
-        ("ir.If", ("j",)): (("ir.For", "k"), None),
-        ("ir.For", "j"): (("ir.If", ("j",)),),
-        ("ir.If", ("i",)): (("ir.For", "j"), None),
-        ("ir.For", "i"): (("ir.If", ("i",)),),
-    }
-    verify_structure(new_stmt, expected_struct)
-
-
-def test_no_hoisting_1():
-    @Ts.function(private=True)
-    def func(data: T.handle("float32")):
-        data_ptr = T.decl_tensor(1, "float32", data=data)
-        for i in range(10):
-            for j in range(10):
-                for k in range(10):
-                    if k <= 3:
-                        data_ptr[i * 100 + j * 10 + k] = data_ptr[i * 100 + j 
* 10 + k] + T.float32(
-                            0.5
-                        )
-
-    mod = tvm.IRModule.from_expr(func)
-    stmt = mod["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-
-def test_no_hoisting_2():
-    @Ts.function(private=True)
-    def func(data: T.handle("float32")):
-        data_ptr = T.decl_tensor(1, "float32", data=data)
-        for i in range(10):
-            for j in range(10):
-                for k in range(10):
-                    if i <= 3:
-                        data_ptr[i * 100 + j * 10 + k] = data_ptr[i * 100 + j 
* 10 + k] + T.float32(
-                            0.3
-                        )
-                    data_ptr[i * 100 + j * 10 + k] = data_ptr[i * 100 + j * 10 
+ k] + T.float32(0.5)
-
-    mod = tvm.IRModule.from_expr(func)
-    stmt = mod["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-
-def test_no_hoisting_4():
-    dshape = (32, 64)
-    dshape_inner = (33, 63)
-
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), l: T.int32, m: T.int32, n: 
T.int32):
-            bx = T.launch_thread("blockIdx.x", dshape[1])
-            for i in T.serial(l):
-                for j in T.serial(m):
-                    for k in T.serial(n):
-                        tx_var = T.launch_thread("threadIdx.x", 
dshape_inner[0])
-                        if tx_var < 3:
-                            data[bx * j + tx_var * j * k] = data[
-                                bx * j + tx_var * j * k
-                            ] + T.float32(0.3)
-                        else:
-                            data[bx * j + tx_var * j * k] = data[
-                                bx * j + tx_var * j * k
-                            ] + T.float32(1.3)
-
-    stmt = Module["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-
-def test_no_hoisting_6():
-    dshape = (32, 64)
-
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), l: T.int32, m: T.int32, n: 
T.int32):
-            tx = T.launch_thread("threadIdx.x", dshape[0])
-            bx = T.launch_thread("blockIdx.x", dshape[1])
-            for i in T.serial(l):
-                for j in T.serial(m):
-                    for k in T.serial(n):
-                        if tx + k < 3:
-                            data[bx * j + tx * j * k] = data[bx * j + tx * j * 
k] + T.float32(0.3)
-                        else:
-                            data[bx * j + tx * j * k] = data[bx * j + tx * j * 
k] + T.float32(1.3)
-
-    stmt = Module["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-
-def test_no_hoisting_7():
-    dshape = (32, 64)
-
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), l: T.int32, m: T.int32, n: 
T.int32):
-            tx = T.launch_thread("threadIdx.x", dshape[0])
-            bx = T.launch_thread("blockIdx.x", dshape[1])
-            for i in T.serial(l):
-                for j in T.serial(m):
-                    if tx + j < 9:
-                        for k in T.serial(n):
-                            if tx + k < 3:
-                                data[bx * j + tx * j * k] = data[bx * j + tx * 
j * k] + T.float32(
-                                    0.3
-                                )
-
-    stmt = Module["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-
-def test_hoisting_block_scope_2():
-    dshape = (32, 64)
-
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), l: T.int32, m: T.int32, n: 
T.int32):
-            tx = T.launch_thread("threadIdx.x", dshape[0])
-            for i in T.serial(l):
-                for j in T.serial(m):
-                    for k in T.serial(n):
-                        bx_var = T.launch_thread("blockIdx.x", dshape[1])
-                        if tx < 3:
-                            data[bx_var * j + tx * j * k] = data[
-                                bx_var * j + tx * j * k
-                            ] + T.float32(0.3)
-                        else:
-                            data[bx_var * j + tx * j * k] = data[
-                                bx_var * j + tx * j * k
-                            ] + T.float32(1.3)
-
-    mod = Module
-    mod = tvm.tirx.transform.StmtSimplify()(mod)
-    mod = tvm.tirx.transform.RemoveNoOp()(mod)
-    stmt = mod["main"].body
-
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    assert not tvm_ffi.structural_equal(new_stmt, stmt)
-
-
-def test_hoisting_block_scope_5():
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), l: T.int32, m: T.int32, n: 
T.int32, g: T.int32):
-            for i in T.serial(l):
-                for j in T.serial(m):
-                    for k in T.serial(n):
-                        if data[g] < T.float32(3):
-                            data[9 * j + 3 * j * k] = data[9 * j + 3 * j * k] 
+ T.float32(0.3)
-                        else:
-                            data[9 * j + 3 * j * k] = data[9 * j + 3 * j * k] 
+ T.float32(1.3)
-
-    stmt = Module["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    assert not tvm_ffi.structural_equal(new_stmt, stmt)
-
-    mod = tvm.IRModule.from_expr(tvm.tirx.Function([], new_stmt))
-    stmt = new_stmt
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(mod)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-
-def test_hoisting_block_scope_6():
-    dshape = (32, 64)
-
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), l: T.int32, m: T.int32, n: 
T.int32):
-            tx = T.launch_thread("threadIdx.x", dshape[0])
-            bx = T.launch_thread("blockIdx.x", dshape[1])
-            for i in T.serial(l):
-                for j in T.serial(m):
-                    for k in T.serial(n):
-                        if tx + n < 3:
-                            data[bx * j + tx * j * k] = data[bx * j + tx * j * 
k] + T.float32(0.3)
-                        else:
-                            data[bx * j + tx * j * k] = data[bx * j + tx * j * 
k] + T.float32(1.3)
-
-    stmt = Module["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    assert not tvm_ffi.structural_equal(new_stmt, stmt)
-
-
-def test_hoisting_block_scope_7():
-    dshape = (32, 64)
-
-    @I.ir_module
-    class Module:
-        @Ts.function(private=True)
-        def main(data: T.Tensor((1,), "float32"), l: T.int32, m: T.int32, n: 
T.int32):
-            tx = T.launch_thread("threadIdx.x", dshape[0])
-            bx = T.launch_thread("blockIdx.x", dshape[1])
-            for i in T.serial(l):
-                for j in T.serial(m):
-                    for k in T.serial(n):
-                        if tx + i < 3:
-                            data[bx * j + tx * j * k] = data[bx * j + tx * j * 
k] + T.float32(0.3)
-                        else:
-                            data[bx * j + tx * j * k] = data[bx * j + tx * j * 
k] + T.float32(1.3)
-
-    stmt = Module["main"].body
-    new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    tvm.ir.assert_structural_equal(new_stmt, stmt)
-
-    with tvm.transform.PassContext(
-        config={"s_tir.HoistIfThenElse": {"support_block_scope_hoisting": 
True}}
-    ):
-        new_stmt = tvm.s_tir.transform.HoistIfThenElse()(Module)["main"].body
-    assert not tvm_ffi.structural_equal(new_stmt, stmt)
-
-
-if __name__ == "__main__":
-    tvm.testing.main()
diff --git a/tests/python/s_tir/transform/test_s_tir_transform_thread_sync.py 
b/tests/python/s_tir/transform/test_s_tir_transform_thread_sync.py
index 5501667a35..82c0fd3d25 100644
--- a/tests/python/s_tir/transform/test_s_tir_transform_thread_sync.py
+++ b/tests/python/s_tir/transform/test_s_tir_transform_thread_sync.py
@@ -20,6 +20,7 @@ import pytest
 import tvm
 import tvm.testing
 from tvm import s_tir
+from tvm.script import ir as I
 from tvm.script import s_tir as Ts
 from tvm.script import tirx as T
 from tvm.testing import env
@@ -237,6 +238,30 @@ def test_sync_bind():
     tvm.ir.assert_structural_equal(mod["main"], expected)
 
 
[email protected]("loop", [T.serial, T.unroll])
+def test_hoist_if_preserves_sync_placement(loop):
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32")):
+            B = T.alloc_tensor((4,), "int32", scope="shared")
+            tx = T.launch_thread("threadIdx.x", 4)
+            B[tx] = A[tx]
+            for i in loop(1, 4):
+                if tx == 0:
+                    B[i] = B[i - 1] + 1
+            A[tx] = B[tx]
+
+    # Each iteration must keep its own guard so synchronization can be inserted
+    # between shared-memory accesses, outside conditional control flow.
+    finish = tvm.transform.Sequential(
+        [tvm.tirx.transform.UnrollLoop(), 
tvm.s_tir.transform.ThreadSync("shared")]
+    )
+    Expected = finish(Before)
+    After = finish(tvm.tirx.transform.HoistIf()(Before))
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
 if __name__ == "__main__":
     test_thread_storage_sync()
     test_sync_else_branch()
diff --git a/tests/python/tirx/transform/test_transform_hoist_if.py 
b/tests/python/tirx/transform/test_transform_hoist_if.py
new file mode 100644
index 0000000000..cd0fb8cd5f
--- /dev/null
+++ b/tests/python/tirx/transform/test_transform_hoist_if.py
@@ -0,0 +1,423 @@
+# 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.
+import pytest
+
+import tvm
+import tvm.testing
+from tvm.script import ir as I
+from tvm.script import tirx as T
+
+
+def test_outermost_independent_loop():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32")):
+            for i in T.serial(4):
+                for j in T.serial(4):
+                    for k in T.serial(4):
+                        if i < 2:
+                            A[i, j, k] = 1
+                        else:
+                            A[i, j, k] = 2
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32")):
+            for i in T.serial(4):
+                if i < 2:
+                    for j in T.serial(4):
+                        for k in T.serial(4):
+                            A[i, j, k] = 1
+                else:
+                    for j in T.serial(4):
+                        for k in T.serial(4):
+                            A[i, j, k] = 2
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_multiple_loop_dependencies():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32")):
+            for i in T.serial(4):
+                for j in T.serial(4):
+                    for k in T.serial(4):
+                        if i + j < 2:
+                            A[i, j, k] = 1
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32")):
+            for i in T.serial(4):
+                for j in T.serial(4):
+                    if i + j < 2:
+                        for k in T.serial(4):
+                            A[i, j, k] = 1
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_nested_conditions():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32"), n: T.int32):
+            for i in T.serial(4):
+                if i < 2:
+                    for j in T.serial(4):
+                        if 0 < n:
+                            A[i, j] = 1
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32"), n: T.int32):
+            if 0 < n:
+                for i in T.serial(4):
+                    if i < 2:
+                        for j in T.serial(4):
+                            A[i, j] = 1
+
+    # One-sided conditions must not clone the loop before cleanup: a chain of
+    # such conditions would otherwise produce exponentially many loop copies.
+    inserted = tvm.tirx.transform.HoistIf().passes[0](Before)
+    assert inserted["main"].body[0].else_case is None
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_dependent_condition():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32")):
+            for i in T.serial(4):
+                if i < 2:
+                    A[i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_sibling_statements():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32"), n: T.int32):
+            for i in T.serial(4):
+                A[i, 0] = 0
+                for j in T.serial(4):
+                    if 0 < n:
+                        A[i, j] = 1
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32"), n: T.int32):
+            for i in T.serial(4):
+                A[i, 0] = 0
+                if 0 < n:
+                    for j in T.serial(4):
+                        A[i, j] = 1
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_binding_scope():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32")):
+            for i in T.serial(4):
+                x: T.let[T.int32] = A[i, 0]
+                for j in T.serial(4):
+                    if 0 < x:
+                        A[i, j] = 1
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32")):
+            for i in T.serial(4):
+                x: T.let[T.int32] = A[i, 0]
+                if 0 < x:
+                    for j in T.serial(4):
+                        A[i, j] = 1
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_memory_condition():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32")):
+            for i in T.serial(4):
+                if 0 < A[0]:
+                    A[i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_effectful_condition():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32")):
+            for i in T.serial(4):
+                if 0 < T.call_extern("predicate", ty="int32"):
+                    A[i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_thread_region_boundary():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32"), n: T.int32):
+            for i in T.serial(4):
+                with T.launch_thread("threadIdx.x", 4) as tx:
+                    for j in T.serial(4):
+                        if 0 < n:
+                            A[i, tx] = j
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_guarded_division():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32):
+            for i in T.serial(4):
+                if i < n:
+                    if 1 < 100 // n:
+                        A[i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_zero_trip_division():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32):
+            for i in T.serial(n):
+                if 1 < 100 // n:
+                    A[i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_enclosing_else_is_preserved():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32):
+            for i in T.serial(4):
+                if i < 2:
+                    if 0 < n:
+                        A[i] = 1
+                else:
+                    A[i] = 2
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32):
+            if 0 < n:
+                for i in T.serial(4):
+                    if i < 2:
+                        A[i] = 1
+                    else:
+                        A[i] = 2
+            else:
+                for i in T.serial(4):
+                    if not i < 2:
+                        A[i] = 2
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_effectful_enclosing_condition():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32):
+            for i in T.serial(4):
+                if 0 < i + T.call_extern("predicate", ty="int32"):
+                    if 0 < n:
+                        A[i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
[email protected]("field", ["min", "extent", "step"])
+def test_effectful_loop_header(field):
+    value = tvm.tirx.call_extern("int32", "loop_header")
+    start = value if field == "min" else 0
+    stop = value if field == "extent" else 4
+    step = value if field == "step" else 1
+
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32):
+            for i in T.serial(start, stop, step=step):
+                if 0 < n:
+                    A[i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
[email protected]("loop", [T.parallel, T.vectorized, T.unroll])
+def test_non_default_loop_boundary(loop):
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32"), n: T.int32):
+            for i in T.serial(4):
+                for j in loop(4):
+                    for k in T.serial(4):
+                        if 0 < n:
+                            A[i, j, k] = 1
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32"), n: T.int32):
+            for i in T.serial(4):
+                for j in loop(4):
+                    if 0 < n:
+                        for k in T.serial(4):
+                            A[i, j, k] = 1
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_nested_else_splits_once():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32, m: T.int32):
+            for i in T.serial(4):
+                if 0 < n:
+                    if 0 < m:
+                        A[i] = 1
+                    else:
+                        A[i] = 2
+                else:
+                    A[i] = 3
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4,), "int32"), n: T.int32, m: T.int32):
+            if 0 < n:
+                for i in T.serial(4):
+                    if 0 < m:
+                        A[i] = 1
+                    else:
+                        A[i] = 2
+            else:
+                for i in T.serial(4):
+                    A[i] = 3
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_nested_loop_splits_once():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32")):
+            for i in T.serial(4):
+                for j in T.serial(4):
+                    for k in T.serial(4):
+                        if i < 2:
+                            A[i, j, k] = 1
+                        elif j < 2:
+                            A[i, j, k] = 2
+                        else:
+                            A[i, j, k] = 3
+
+    @I.ir_module
+    class Expected:
+        @T.function
+        def main(A: T.Tensor((4, 4, 4), "int32")):
+            for i in T.serial(4):
+                if i < 2:
+                    for j in T.serial(4):
+                        for k in T.serial(4):
+                            A[i, j, k] = 1
+                else:
+                    for j in T.serial(4):
+                        for k in T.serial(4):
+                            if j < 2:
+                                A[i, j, k] = 2
+                            else:
+                                A[i, j, k] = 3
+
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+def test_thread_binding_boundary():
+    @I.ir_module
+    class Before:
+        @T.function
+        def main(A: T.Tensor((4, 4), "int32"), n: T.int32):
+            for tx in T.thread_binding(4, thread="threadIdx.x"):
+                for i in T.serial(4):
+                    if 0 < n:
+                        A[tx, i] = 1
+
+    Expected = Before
+    After = tvm.tirx.transform.HoistIf()(Before)
+    tvm.ir.assert_structural_equal(After, Expected)
+
+
+if __name__ == "__main__":
+    tvm.testing.main()

Reply via email to