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

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


The following commit(s) were added to refs/heads/main by this push:
     new 902081f43 [REFACTOR][TIRx] Analyze vectorization eligibility before 
rewriting (#70)
902081f43 is described below

commit 902081f4309bb908feeafb13272b857c3945f350
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Oct 25 14:47:32 2026 -0400

    [REFACTOR][TIRx] Analyze vectorization eligibility before rewriting (#70)
    
    Analyze each whole vectorized loop before constructing vector IR. A
    completed eligibility plan drives the established vector construction;
    otherwise the complete original loop retains scalar execution, including
    effects, bindings, ordering and any nested vectorized loops.
    
    The supported subset is straight-line scalar arithmetic, comparisons,
    select, numeric casts, bindings and one-dimensional accesses to
    layout-free parameter storage. Numeric elements are signed or unsigned
    8/16/32/64-bit integers and 16/32/64-bit floats; bool values are
    supported as masks, but bool tensor storage stays scalar. Loop indices
    are signed 32/64-bit, start at zero and step by one. Fixed widths are
    2/4/8/16/32/64; scalable factors are 2/4/8/16 on a VLA target. Stores
    require an injective affine index, all accesses to a written storage
    root must share its index expression, and scalable varying accesses must
    be contiguous. Accessing multiple tensors requires `tirx.noalias` unless
    they are views of the same storage.
    
    The following retain scalar execution:
    
    - Assertions, `If`/predication, nested `For`/`While`, and calls
    including `if_then_else`, `reinterpret` and LLVM pure intrinsics.
    - Local allocated tensors, tensors with layouts, bool storage, existing
    vector values and non-affine stores.
    - Unsupported loop shapes, including dynamic extents, `vscale` without a
    VLA target and scalable loops over vector elements; these previously
    could raise during vector construction.
    
    Use typed dispatch with a rejecting default and private semantic state
    behind a read-only plan. Keep fixed-width eligibility target-independent
    and leave backend-specific lowering to code generation. Remove the
    unused `tirx.enable_tensor_level_predication` option.
    
    The ARM multiply-accumulate fixture places the vector lane loop inside
    the serial reduction because nested loops now retain scalar execution.
    The RVV scalable-ramp fixture requests a scalable extent explicitly
    because fixed-width loops no longer become guarded scalable chunks.
---
 python/tvm_tirx/tirx/transform/transform.py   |    6 +
 src/tirx/ir/transform.cc                      |    1 -
 src/tirx/transform/vectorize_loop.cc          | 1273 ++++++-------------------
 tests/python/codegen/llvm/test_arm.py         |    6 +-
 tests/python/codegen/llvm/test_riscv.py       |   22 +-
 tests/python/tirx/transform/test_vectorize.py |  453 ++-------
 6 files changed, 413 insertions(+), 1348 deletions(-)

diff --git a/python/tvm_tirx/tirx/transform/transform.py 
b/python/tvm_tirx/tirx/transform/transform.py
index 59d18a0ab..47c35f5b5 100644
--- a/python/tvm_tirx/tirx/transform/transform.py
+++ b/python/tvm_tirx/tirx/transform/transform.py
@@ -113,6 +113,10 @@ def VectorizeLoop(enable_vectorize: bool = True):
     fixed-width loops do not require a target. Code needing different 
vectorization
     targets must be separated into functions before this pass.
 
+    Unsupported loops retain scalar execution. Loops containing assertions
+    always remain scalar. Accessing multiple tensors requires the function's
+    ``tirx.noalias`` attribute unless they are views of the same storage.
+
     Parameters
     ----------
     enable_vectorize : bool
@@ -137,6 +141,7 @@ def VectorizeLoop(enable_vectorize: bool = True):
         class Before:
             @T.function(private=True)
             def main(A: T.Tensor((4,), T.float32), B: T.Tensor((4,), 
T.float32)):
+                T.func_attr({"tirx.noalias": True})
                 for i in T.vectorized(0, 4):
                     B[i] = A[i]
 
@@ -144,6 +149,7 @@ def VectorizeLoop(enable_vectorize: bool = True):
         class Expected:
             @T.function(private=True)
             def main(A: T.Tensor((4,), T.float32), B: T.Tensor((4,), 
T.float32)):
+                T.func_attr({"tirx.noalias": True})
                 B[0:4] = A[T.Ramp(0, 1, 4)]
 
         def run_pass():
diff --git a/src/tirx/ir/transform.cc b/src/tirx/ir/transform.cc
index 44645baf5..c099c4585 100644
--- a/src/tirx/ir/transform.cc
+++ b/src/tirx/ir/transform.cc
@@ -37,7 +37,6 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   
::tvm::transform::PassContext::RegisterConfigOption<bool>(tvm::tirx::attr::kNoAlias.name);
   
::tvm::transform::PassContext::RegisterConfigOption<bool>("tirx.disable_assert");
   
::tvm::transform::PassContext::RegisterConfigOption<bool>("tirx.disable_vectorize");
-  
::tvm::transform::PassContext::RegisterConfigOption<bool>("tirx.enable_tensor_level_predication");
   
::tvm::transform::PassContext::RegisterConfigOption<bool>("tirx.disable_cse_tir");
   
::tvm::transform::PassContext::RegisterConfigOption<bool>("tirx.enable_debug");
   
::tvm::transform::PassContext::RegisterConfigOption<bool>("tirx.disable_storage_rewrite");
diff --git a/src/tirx/transform/vectorize_loop.cc 
b/src/tirx/transform/vectorize_loop.cc
index 786e3515e..96726e2da 100644
--- a/src/tirx/transform/vectorize_loop.cc
+++ b/src/tirx/transform/vectorize_loop.cc
@@ -19,33 +19,24 @@
 
 /*!
  * \file vectorize_loop.cc
+ * \brief Analyze original scalar loops before constructing vector IR.
  */
-// Loop vectorizer as in Halide pipeline.
-#include <tvm/backend/opencl/op/memory.h>
-#include <tvm/ffi/cast.h>
 #include <tvm/ffi/extra/structural_equal.h>
-#include <tvm/ffi/extra/structural_mutate.h>
-#include <tvm/ffi/function.h>
 #include <tvm/ffi/reflection/registry.h>
-#include <tvm/ir/function.h>
 #include <tvm/ir/prim/expr.h>
 #include <tvm/ir/prim/op.h>
-#include <tvm/runtime/logging.h>
 #include <tvm/sym/analyzer.h>
 #include <tvm/tirx/analysis.h>
-#include <tvm/tirx/op/abi.h>
-#include <tvm/tirx/op/attr.h>
 #include <tvm/tirx/op/memory.h>
-#include <tvm/tirx/op/vector.h>
 #include <tvm/tirx/stmt_functor.h>
 #include <tvm/tirx/transform.h>
 
+#include <algorithm>
+#include <initializer_list>
+#include <optional>
 #include <unordered_map>
 #include <vector>
 
-#include "tvm/ffi/dtype.h"
-#include "tvm/tirx/expr.h"
-
 namespace tvm {
 namespace tirx {
 using namespace tvm::prim;
@@ -91,23 +82,6 @@ bool TargetHasVLA(ffi::Optional<Target> target) {
   return has_vla;
 }
 
-bool ContainsCallNode(const Stmt& stmt) {
-  struct CallFinder : StmtExprVisitor {
-    ffi::Optional<VisitInterrupt> Visit(ffi::AnyView value) final {
-      if (auto expr = value.as<PrimExpr>(); expr && expr->as<CallObj>()) {
-        return VisitInterrupt();
-      }
-      return StmtExprVisitor::Visit(value);
-    }
-  };
-  return ffi::make_object<CallFinder>()->Visit(stmt).has_value();
-}
-
-PrimType GetTextureElementType(const Expr& texture) {
-  const auto* pointer_type = texture->ty.as<PtrTypeObj>();
-  TVM_FFI_ICHECK(pointer_type) << "Texture arguments must have PtrType";
-  return pointer_type->element_type.as_or_throw<PrimType>();
-}
 }  // namespace
 
 inline PrimExpr CreateNewLanes(bool is_scalable, int lanes_or_vscale_factor) {
@@ -140,390 +114,255 @@ inline PrimExpr BroadcastTo(PrimExpr e, int lanes, bool 
is_scalable) {
   return prim::Broadcast(e, CreateNewLanes(is_scalable, lanes));
 }
 
-bool EnableTensorLevelPredication(ffi::Optional<Target> target) {
-  transform::PassContext pass_ctx = transform::PassContext::Current();
-  ffi::Optional<bool> enable_tensor_predication =
-      pass_ctx->GetConfig<bool>("tirx.enable_tensor_level_predication");
-  if (enable_tensor_predication.has_value()) {
-    return enable_tensor_predication.value();
-  }
+// Admission is intentionally a small, closed set. Analyze the original whole 
loop:
+// no speculative rewriting, no statement splitting, and no inferred call 
support.
+class VectorizationAnalysis : private tvm::ExprFunctor<bool(const Expr&)>,
+                              private StmtFunctor<bool(const Stmt&)> {
+ private:
+  enum class Variation { kUniform, kVarying };
+  enum class AccessKind { kRead, kWrite };
 
-  // Use tensor-level predication by default for VLA targets
-  return TargetHasVLA(target);
-}
+  struct MemoryAccess {
+    PrimExpr index;
+    AccessKind kind;
+  };
 
-/*!
- * \brief A pass that tries to rewrite tensor accesses (loads and stores) with 
a
- * predicate expression where possible.
- *
- * \note For now we start with a minimal case targeting block-level predicates
- * produced by the split schedule primitive, with the potential for predicating
- * more complex terms in the future if needed.
- *
- * \example
- * Before:
- * for i_0 in T.serial(4):
- *     for i_1 in T.vectorized(4):
- *         if i_0 * 4 + i_1 < 14:
- *             B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
- *
- * After:
- * for i_0 in T.serial(4):
- *  predicate = T.get_active_lane_mask("uint1x4", i_0 * 4, 14)
- *  A_load = T.meta_var(T.call_intrin("float32x4", "tirx.masked_load", A,
- *                                    T.Ramp(i_0 * 4, 1, 4), predicate))
- *  T.evaluate(T.call_intrin("void", "tirx.masked_store", B, A_load,
- *                           T.Ramp(i_0 * 4, 1, 4), predicate))
- */
-class TryPredicateTensorAccesses : public StmtExprMutator {
  public:
-  explicit TryPredicateTensorAccesses(bool allow_offset_predication)
-      : allow_offset_predication_(allow_offset_predication) {}
-
-  /*!
-   * \brief Run the pass to try to exact predicates.
-   * \param stmt - The statement containing tensor accesses (loads and stores)
-   * we want to attempt to predicate.
-   * \param condition - The conditional expression (block-level predicate)
-   * that we will try to remove.
-   * \return pair<success, stmt> - Boolean value for success/failure, the 
rewritten
-   * stmt if successful.
-   */
-  std::pair<bool, Stmt> Run(Stmt stmt, PrimExpr condition) {
-    // Check that the condition provided is of the form a < b, for now.
-    if (!condition->IsInstance<prim::LTObj>()) {
-      return {false, stmt};
-    }
-
-    prim::LT lt = condition.as_or_throw<prim::LT>();
-
-    // Check the form of the vectorized condition, we're expecting
-    // Ramp(...) < Broadcast(...)
-    if (!lt->a->IsInstance<prim::RampObj>() || 
!lt->b->IsInstance<prim::BroadcastObj>()) {
-      return {false, stmt};
-    }
+  // Construction can query approved expressions, but cannot change the plan.
+  class Plan {
+   public:
+    bool IsUniform(const PrimExpr& expr) const {
+      auto it = variation_.find(expr.get());
+      TVM_FFI_ICHECK(it != variation_.end()) << "Expression missing from 
vectorization plan";
+      return it->second == Variation::kUniform;
+    }
+
+   private:
+    friend class VectorizationAnalysis;
+    std::unordered_map<const ExprObj*, Variation> variation_;
+  };
 
-    prim::Ramp pred_ramp = lt->a.as_or_throw<prim::Ramp>();
-    base_ = pred_ramp->base;
-    stride_ = pred_ramp->stride;
-    lanes_ = pred_ramp->lanes;
-    limit_ = lt->b.as_or_throw<prim::Broadcast>()->value;
-
-    // Now we can try to predicate
-    // Keep stmt intact when predication only succeeds for some accesses.
-    Stmt predicated_stmt = Mutate(stmt, 
InplaceMode::kDisallow).ValueOrUnchanged(stmt);
-    if (num_accesses_analyzed_ > 0 && num_accesses_analyzed_ == 
num_accesses_rewritten_) {
-      return {true, predicated_stmt};
+  VectorizationAnalysis(PrimVar var, const PrimExpr& extent, bool noalias,
+                        const std::unordered_map<const VarObj*, const 
VarObj*>& roots)
+      : ExprFunctor(ExpressionVTable()),
+        var_(var),
+        noalias_(noalias),
+        scalable_(!extent.as<IntImmObj>()),
+        roots_(roots) {
+    var_variation_[var.get()] = Variation::kVarying;
+  }
+
+  std::optional<Plan> Run(const Stmt& body) {
+    if (!StmtFunctor::Dispatch(body)) return std::nullopt;
+    // Without tirx.noalias, distinct parameter storage may overlap, so admit
+    // only one storage root. For a written root, every access must use the 
same
+    // index expression so lanes cannot read or write each other's elements.
+    if (!noalias_ && accesses_.size() > 1) return std::nullopt;
+    for (const auto& [tensor, accesses] : accesses_) {
+      bool written = std::any_of(accesses.begin(), accesses.end(), [](const 
MemoryAccess& access) {
+        return access.kind == AccessKind::kWrite;
+      });
+      if (!written) continue;
+      for (const MemoryAccess& access : accesses) {
+        if (!ffi::StructuralEqual()(access.index, accesses.front().index)) 
return std::nullopt;
+      }
     }
-    return {false, stmt};
+    return std::move(plan_);
   }
 
  private:
-  using StmtExprMutator::Mutate_;
-
-  UnchangedOr<PrimExpr> Mutate_(const TensorLoadObj* op, InplaceMode 
inplace_mode) final {
-    auto load = StmtExprMutator::Mutate_(op, inplace_mode)
-                    .ValueOrUnchanged(ffi::GetRef<PrimExpr>(op))
-                    .as_or_throw<TensorLoad>();
-    return TryPredicateTensorAccess(load);
+  static const ExprFunctor::VTable* ExpressionVTable() {
+    static const ExprFunctor::VTable table = [] {
+      ExprFunctor::VTable table;
+      ExprFunctor::InitVTable(&table);
+      // ExprFunctor, unlike StmtFunctor, throws on nodes with no registered 
hook.
+      // Route those expressions to DispatchDefault_ so unknown nodes reject 
the loop.
+      table.SetDispatch<ExprObj>([](const ffi::ObjectRef& node, ExprFunctor* 
self) {
+        return self->DispatchDefault_(node.get());
+      });
+      table.Finalize();
+      return table;
+    }();
+    return &table;
   }
 
-  UnchangedOr<Stmt> Mutate_(const TensorStoreObj* op, InplaceMode 
inplace_mode) final {
-    auto store = StmtExprMutator::Mutate_(op, inplace_mode)
-                     .ValueOrUnchanged(ffi::GetRef<Stmt>(op))
-                     .as_or_throw<TensorStore>();
-    return TryPredicateTensorAccess(store);
-  }
-
-  UnchangedOr<Expr> Mutate_(const CallObj* op, InplaceMode inplace_mode) final 
{
-    Call call = StmtExprMutator::Mutate_(op, inplace_mode)
-                    .ValueOrUnchanged(ffi::GetRef<Expr>(op))
-                    .as_or_throw<Call>();
-    if (!call->op.same_as(tirx::masked_load_op()) && 
!call->op.same_as(tirx::masked_store_op())) {
-      return call;
-    }
-
-    bool is_load = call->op.same_as(tirx::masked_load_op());
-    ffi::Array<PrimExpr> indices;
-    for (size_t i = is_load ? 1 : 2; i + 1 < call->args.size(); ++i) {
-      indices.push_back(call->args[i].as_or_throw<PrimExpr>());
+  bool IsVectorValueType(PrimType type) const {
+    if (!type.IsScalar()) return false;
+    if (type == PrimType::Bool()) return true;
+    if (type.MatchesCode(DLDataTypeCode::kDLInt, DLDataTypeCode::kDLUInt)) {
+      return type.bits() == 8 || type.bits() == 16 || type.bits() == 32 || 
type.bits() == 64;
     }
-    if (auto lane_mask = GetLaneMask(indices)) {
-      PrimExpr predicate = call->args.back().as_or_throw<PrimExpr>();
-      predicate = allow_offset_predication_ ? predicate & lane_mask.value() : 
lane_mask.value();
-      ffi::Array<Expr> args = call->args;
-      args.Set(args.size() - 1, predicate);
-      return Call(call->ty, call->op, args, call->attrs, call->ty_args, 
call->loc);
-    }
-    return call;
+    return type.MatchesCode(DLDataTypeCode::kDLFloat) &&
+           (type.bits() == 16 || type.bits() == 32 || type.bits() == 64);
   }
 
-  ffi::Optional<PrimExpr> GetLaneMask(const ffi::Array<PrimExpr>& indices) {
-    num_accesses_analyzed_ += 1;
-
-    // Do not try to predicate non-vectorized accesses
-    if (!indices.size() || !indices[0]->IsInstance<prim::RampObj>()) {
-      return std::nullopt;
-    }
-    prim::Ramp ramp = indices[0].as_or_throw<prim::Ramp>();
+  bool IsVectorStorageType(PrimType type) const {
+    // Mask support alone does not establish a vector storage representation 
for bool.
+    return type != PrimType::Bool() && IsVectorValueType(type);
+  }
 
-    if (!ffi::StructuralEqual()(ramp->stride, stride_) ||
-        !ffi::StructuralEqual()(ramp->lanes, lanes_)) {
-      return std::nullopt;
-    }
+  bool AnalyzeExpr(const PrimExpr& expr) {
+    return IsVectorValueType(expr.ty()) && ExprFunctor::Dispatch(expr);
+  }
 
-    bool same_base = ffi::StructuralEqual()(ramp->base, base_);
-    if (!same_base) {
-      // The lane mask describes which lanes are active, independent of the
-      // memory base.  This covers accesses such as A[offset + i] guarded by
-      // a predicate over i.
-      if (!allow_offset_predication_) {
-        return std::nullopt;
-      }
-    }
+  Variation VariationOf(const PrimExpr& expr) const { return 
plan_.variation_.at(expr.get()); }
 
-    PrimType buf_predicate_dtype =
-        ramp->ty.as_or_throw<PrimType>().IsScalableVector()
-            ? PrimType::ScalableVector(DLDataTypeCode::kDLUInt, 1,
-                                       
GetLanesOrVScaleFactor(ramp->ty.as_or_throw<PrimType>()))
-            : PrimType::UInt(1, 
GetLanesOrVScaleFactor(ramp->ty.as_or_throw<PrimType>()));
-    PrimExpr lane_mask = Call(buf_predicate_dtype, 
tirx::get_active_lane_mask_op(), {base_, limit_})
-                             .as_or_throw<PrimExpr>();
-
-    num_accesses_rewritten_ += 1;
-    return lane_mask;
-  }
-
-  PrimExpr TryPredicateTensorAccess(TensorLoad load) {
-    if (auto mask = GetLaneMask(load->indices)) {
-      ffi::Array<Expr> args{load->source.as_or_throw<tvm::tirx::TensorVar>()};
-      for (const PrimExpr& index : load->indices) args.push_back(index);
-      args.push_back(mask.value());
-      return Call(load->ty, tirx::masked_load_op(), args, {}, {}, load->loc)
-          .as_or_throw<PrimExpr>();
+  Variation Join(std::initializer_list<PrimExpr> operands) const {
+    for (const PrimExpr& operand : operands) {
+      if (VariationOf(operand) == Variation::kVarying) return 
Variation::kVarying;
     }
-    return load;
+    return Variation::kUniform;
   }
 
-  Stmt TryPredicateTensorAccess(TensorStore store) {
-    if (auto mask = GetLaneMask(store->indices)) {
-      ffi::Array<Expr> args{store->dest.as_or_throw<TensorVar>(), 
store->value};
-      for (const PrimExpr& index : store->indices) args.push_back(index);
-      args.push_back(mask.value());
-      return Evaluate(Call(PrimType::Void(), tirx::masked_store_op(), args, 
{}, {}, store->loc),
-                      store->loc);
-    }
-    return store;
+  bool Record(const ExprObj* expr, Variation variation) {
+    plan_.variation_[expr] = variation;
+    return true;
   }
 
-  /*! \brief The variable base expr of the predicate. */
-  PrimExpr base_{ffi::UnsafeInit{}};
-  /*! \brief The lane stride of the predicate. */
-  PrimExpr stride_{ffi::UnsafeInit{}};
-  /*! \brief The lane count of the predicate. */
-  PrimExpr lanes_{ffi::UnsafeInit{}};
-  /*! \brief The limit of the predicate. The expr specifies the upper bound of 
the base's
-   * evaluated value. */
-  PrimExpr limit_{ffi::UnsafeInit{}};
-  /*! \brief Whether to predicate offset tensor accesses that use the same 
lane layout. */
-  bool allow_offset_predication_;
-  /*! \brief The number of tensor accesses in the stmt we will analyze. */
-  size_t num_accesses_analyzed_ = 0;
-  /*! \brief The number of tensor accesses rewritten with predicates. */
-  size_t num_accesses_rewritten_ = 0;
-};
+  // The functors only dispatch: they never inherit recursive traversal that
+  // could admit unsupported nodes. AssertStmt, calls, existing vector forms,
+  // control flow and all other unhandled nodes retain the original scalar 
loop.
+  bool DispatchDefault_(const ffi::Object* op) final { return false; }
 
-// Rewrite vectorized allocation access
-// This is necessary for making each vector component containing its own 
workspace.
-// Originates from Halide's loop vectorizer
-//
-// s[i] = s[i * lanes + var]
-//
-// The same principle applies when using one thread to simulate multiple 
context.
-//
-class VecAllocAccess : public StmtExprMutator {
- public:
-  using StmtExprMutator::Mutate_;
+  bool Dispatch_(const IntImmObj* op) final { return Record(op, 
Variation::kUniform); }
+  bool Dispatch_(const FloatImmObj* op) final { return Record(op, 
Variation::kUniform); }
 
-  VecAllocAccess(const VarObj* buf, Var var, PrimExpr var_lanes)
-      : buf_(buf), var_(var), var_lanes_(var_lanes) {}
+  bool Dispatch_(const VarObj* op) final {
+    auto it = var_variation_.find(op);
+    return Record(op, it == var_variation_.end() ? Variation::kUniform : 
it->second);
+  }
 
-  UnchangedOr<PrimExpr> Mutate_(const TensorLoadObj* op, InplaceMode 
inplace_mode) final {
-    auto indices =
-        Mutate(op->indices, 
inplace_mode).as_or_throw<UnchangedOr<ffi::Array<PrimExpr>>>();
-    TensorLoad load = indices.UnchangedOrSameAs(op->indices)
-                          ? ffi::GetRef<TensorLoad>(op)
-                          : TensorLoad(op->source.as_or_throw<TensorVar>(),
-                                       std::move(indices).ValueUnchecked(), 
op->loc);
-    return UpdateTensorAccess(load);
+  bool Dispatch_(const TensorLoadObj* op) final {
+    if (!AnalyzeAccess(op->source, op->indices, AccessKind::kRead)) return 
false;
+    return Record(op, VariationOf(op->indices[0]));
   }
 
-  UnchangedOr<Stmt> Mutate_(const TensorStoreObj* op, InplaceMode 
inplace_mode) final {
-    auto value = Mutate(op->value, inplace_mode);
-    auto indices =
-        Mutate(op->indices, 
inplace_mode).as_or_throw<UnchangedOr<ffi::Array<PrimExpr>>>();
-    TensorStore store = value.UnchangedOrSameAs(op->value) && 
indices.UnchangedOrSameAs(op->indices)
-                            ? ffi::GetRef<TensorStore>(op)
-                            : TensorStore(op->dest.as_or_throw<TensorVar>(),
-                                          
std::move(indices).ValueOrUnchanged(op->indices),
-                                          
std::move(value).ValueOrUnchanged(op->value), op->loc);
-    return UpdateTensorAccess(store);
+  bool Dispatch_(const prim::CastObj* op) final {
+    // Scalar conversions between the supported numeric types (including bool
+    // masks) widen to the same fixed or scalable shape.
+    if (!AnalyzeExpr(op->value)) return false;
+    return Record(op, VariationOf(op->value));
   }
 
- private:
-  template <typename Node>
-  Node UpdateTensorAccess(Node node) {
-    // Only update the tensor that's being replaced.
-    if (node->dest.template as_or_throw<TensorVar>().get() != buf_) {
-      return node;
+  bool Dispatch_(const prim::SelectObj* op) final {
+    if (!AnalyzeExpr(op->condition) || !AnalyzeExpr(op->true_value) ||
+        !AnalyzeExpr(op->false_value))
+      return false;
+    return Record(op, Join({op->condition, op->true_value, op->false_value}));
+  }
+
+  bool Dispatch_(const prim::NotObj* op) final {
+    if (!AnalyzeExpr(op->a)) return false;
+    return Record(op, VariationOf(op->a));
+  }
+
+  bool Dispatch_(const prim::BitwiseNotObj* op) final {
+    if (!AnalyzeExpr(op->a)) return false;
+    return Record(op, VariationOf(op->a));
+  }
+
+  template <typename T>
+  bool AnalyzeBinary(const T* op) {
+    if (!AnalyzeExpr(op->a) || !AnalyzeExpr(op->b)) return false;
+    return Record(op, Join({op->a, op->b}));
+  }
+
+#define TVM_VECTOR_BINARY_DISPATCH(Node) \
+  bool Dispatch_(const prim::Node##Obj* op) final { return AnalyzeBinary(op); }
+  TVM_VECTOR_BINARY_DISPATCH(Add)
+  TVM_VECTOR_BINARY_DISPATCH(Sub)
+  TVM_VECTOR_BINARY_DISPATCH(Mul)
+  TVM_VECTOR_BINARY_DISPATCH(Div)
+  TVM_VECTOR_BINARY_DISPATCH(Mod)
+  TVM_VECTOR_BINARY_DISPATCH(FloorDiv)
+  TVM_VECTOR_BINARY_DISPATCH(FloorMod)
+  TVM_VECTOR_BINARY_DISPATCH(Min)
+  TVM_VECTOR_BINARY_DISPATCH(Max)
+  TVM_VECTOR_BINARY_DISPATCH(EQ)
+  TVM_VECTOR_BINARY_DISPATCH(NE)
+  TVM_VECTOR_BINARY_DISPATCH(LT)
+  TVM_VECTOR_BINARY_DISPATCH(LE)
+  TVM_VECTOR_BINARY_DISPATCH(GT)
+  TVM_VECTOR_BINARY_DISPATCH(GE)
+  TVM_VECTOR_BINARY_DISPATCH(And)
+  TVM_VECTOR_BINARY_DISPATCH(Or)
+  TVM_VECTOR_BINARY_DISPATCH(LShift)
+  TVM_VECTOR_BINARY_DISPATCH(RShift)
+  TVM_VECTOR_BINARY_DISPATCH(BitwiseAnd)
+  TVM_VECTOR_BINARY_DISPATCH(BitwiseOr)
+  TVM_VECTOR_BINARY_DISPATCH(BitwiseXor)
+#undef TVM_VECTOR_BINARY_DISPATCH
+
+  bool AnalyzeAccess(const Expr& tensor_expr, const ffi::Array<PrimExpr>& 
indices,
+                     AccessKind kind) {
+    auto tensor = tensor_expr.as<TensorVar>();
+    if (!tensor || !roots_.count(tensor->get()) || 
tensor->ty()->layout.has_value() ||
+        !IsVectorStorageType(tensor->dtype()) || indices.size() != 1 || 
!AnalyzeExpr(indices[0]))
+      return false;
+    bool varying = VariationOf(indices[0]) == Variation::kVarying;
+    if (kind == AccessKind::kWrite || (scalable_ && varying)) {
+      auto coefficients = DetectLinearEquation(indices[0], {var_});
+      if (coefficients.size() != 2) return false;
+      const auto* stride = coefficients[0].as<IntImmObj>();
+      if (!stride) return false;
+      // Writes must hit a distinct element per lane: stride * var + base, 
stride != 0.
+      if (kind == AccessKind::kWrite && stride->value == 0) return false;
+      // This vector construction path requires contiguous scalable accesses.
+      if (scalable_ && stride->value != 1) return false;
     }
+    accesses_[roots_.at(tensor->get())].push_back({indices[0], kind});
+    return true;
+  }
 
-    // Find/make a TensorVar object with the correct updated shape.
-    TensorVar buf{ffi::UnsafeInit{}};
-    ffi::Any mapped = VarRemapGet(node->dest.template 
as_or_throw<TensorVar>());
-    if (mapped != nullptr) {
-      buf = mapped.as_or_throw<TensorVar>();
-    } else {
-      // Extend the least significant dimension by a factor of
-      // var_lanes_.  Typically, this will be a 1-d index into a flat
-      // memory space.
-      ffi::Array<PrimExpr> shape = node->dest.template 
as_or_throw<TensorVar>().shape();
-      shape.Set(shape.size() - 1, analyzer_->Simplify(shape[shape.size() - 1] 
* var_lanes_));
-
-      // TODO(Lunderberg): Move this pass to be prior to
-      // FlattenTensor, implement by appending a
-      // dimension to the tensor.  Since it is currently after the
-      // flattening, the strides are not technically necessary, but
-      // are updated for consistency.
-
-      auto type =
-          ffi::make_object<TensorTypeObj>(*node->dest.template 
as_or_throw<TensorVar>().ty().get());
-      WidenLayout(type.get(), shape);
-      type->shape = shape;
-      buf =
-          TensorVar(node->dest.template as_or_throw<TensorVar>()->name, 
TensorType(std::move(type)),
-                    node->dest.template as_or_throw<TensorVar>()->loc);
-      VarRemapSet(node->dest.template as_or_throw<TensorVar>(), buf);
+  bool Dispatch_(const SeqStmtObj* op) final {
+    for (const Stmt& child : op->seq) {
+      if (!StmtFunctor::Dispatch(child)) return false;
     }
+    return true;
+  }
 
-    // Extend the last index by the number of lanes in the vectorized
-    // variable.
-    ffi::Array<PrimExpr> indices = node->indices;
-    indices.Set(indices.size() - 1, analyzer_->Simplify(indices[indices.size() 
- 1] * var_lanes_ +
-                                                        
var_.as_or_throw<PrimExpr>()));
-
-    auto writer = node.CopyOnWrite();
-    writer->dest = buf;
-    writer->indices = indices;
-    return node;
-  }
-
-  TensorLoad UpdateTensorAccess(TensorLoad node) {
-    TensorVar tensor = node->source.as_or_throw<tvm::tirx::TensorVar>();
-    if (tensor.get() != buf_) return node;
-    TensorVar buf{ffi::UnsafeInit{}};
-    auto mapped = VarRemapGet(tensor);
-    if (mapped != nullptr) {
-      buf = mapped.as_or_throw<TensorVar>();
-    } else {
-      ffi::Array<PrimExpr> shape = tensor.shape();
-      shape.Set(shape.size() - 1, analyzer_->Simplify(shape.back() * 
var_lanes_));
-      auto type = ffi::make_object<TensorTypeObj>(*tensor.ty().get());
-      WidenLayout(type.get(), shape);
-      type->shape = shape;
-      buf = TensorVar(tensor->name, TensorType(std::move(type)), tensor->loc);
-      VarRemapSet(tensor, buf);
-    }
-    ffi::Array<PrimExpr> indices = node->indices;
-    indices.Set(indices.size() - 1,
-                analyzer_->Simplify(indices.back() * var_lanes_ + 
var_.as_or_throw<PrimExpr>()));
-    return TensorLoad(buf, indices, node->loc);
-  }
-
-  void WidenLayout(TensorTypeObj* type, const ffi::Array<PrimExpr>& shape) {
-    if (!type->layout.has_value()) return;
-    auto tile = type->layout.value().as_or_throw<TileLayout>();
-    TVM_FFI_CHECK(
-        
MatchShapeStridePattern(TensorType(ffi::make_object<TensorTypeObj>(*type))).has_value(),
-        ValueError)
-        << "Vectorized allocation requires a regular memory layout";
-    ffi::Array<Iter> shard;
-    for (size_t i = 0; i < tile->shard.size(); ++i) {
-      PrimExpr stride = tile->shard[i]->stride;
-      if (i + 1 != tile->shard.size()) stride *= var_lanes_;
-      shard.push_back(Iter(shape[i], analyzer_->Simplify(stride), 
Axis::Get("m")));
-    }
-    tile.CopyOnWrite()->shard = std::move(shard);
-    type->layout = tile;
+  bool Dispatch_(const BindObj* op) final {
+    auto value = op->value.as<PrimExpr>();
+    if (!value || !AnalyzeExpr(*value)) return false;
+    var_variation_[op->var.get()] = VariationOf(*value);
+    return true;
   }
 
-  // tensor var
-  const VarObj* buf_;
-  // variable to be replaced
-  Var var_;
-  // the lanes.
-  PrimExpr var_lanes_;
-  // Analyzer for simplifications
-  sym::Analyzer analyzer_;
+  bool Dispatch_(const TensorStoreObj* op) final {
+    return AnalyzeExpr(op->value) && AnalyzeAccess(op->dest, op->indices, 
AccessKind::kWrite);
+  }
+
+  bool Dispatch_(const EvaluateObj* op) final {
+    auto value = op->value.as<PrimExpr>();
+    return value && AnalyzeExpr(*value);
+  }
+
+  PrimVar var_;
+  bool noalias_;
+  bool scalable_;
+  const std::unordered_map<const VarObj*, const VarObj*>& roots_;
+  Plan plan_;
+  std::unordered_map<const VarObj*, Variation> var_variation_;
+  std::unordered_map<const VarObj*, std::vector<MemoryAccess>> accesses_;
 };
 
-// Vectorization supplies its own dtype-aware expression traversal.
+// Construction consumes a completed plan. There is no scalarization path here.
 class Vectorizer : public StmtExprMutator {
  public:
   using StmtExprMutator::Mutate;
   using StmtExprMutator::Mutate_;
 
-  Vectorizer(Var var, PrimExpr var_lanes, ffi::Optional<Target> target)
-      : var_(var), var_lanes_(var_lanes), target_(target) {
-    ramp_ = MakeIndexRamp(1, var_lanes);
-    VarRemapSet(var_, ramp_);
+  Vectorizer(PrimVar var, PrimExpr lanes, const VectorizationAnalysis::Plan& 
plan) : plan_(plan) {
+    VarRemapSet(var, prim::Ramp(IntImm(var.ty(), 0), IntImm(var.ty(), 1), 
lanes));
   }
 
-  // Rewriting may change vector dtypes and later request scalarization of the
-  // original statement.  Keep the complete speculative traversal out of place.
   UnchangedOr<ffi::Any> Mutate(ffi::AnyView value, InplaceMode inplace_mode) 
final {
-    if (const auto* stmt = value.as<StmtObj>()) {
-      TVM_FFI_ICHECK(!need_scalarize_);
-      auto result = StmtExprMutator::Mutate(value, InplaceMode::kDisallow);
-      if (need_scalarize_) {
-        need_scalarize_ = false;
-        return ffi::Any(Scalarize(ffi::GetRef<Stmt>(stmt)));
-      }
-      return result;
+    if (auto expr = value.as<PrimExpr>()) {
+      if (plan_.IsUniform(*expr)) return ffi::Unchanged();
     }
     return StmtExprMutator::Mutate(value, InplaceMode::kDisallow);
   }
 
-  UnchangedOr<Expr> Mutate_(const OpaqueExprObj* op, InplaceMode inplace_mode) 
final {
-    TVM_FFI_THROW(InternalError) << "Do not have a default for " << 
op->GetTypeKey();
-    TVM_FFI_UNREACHABLE();
-  }
-
-  UnchangedOr<Expr> Mutate_(const TupleObj* op, InplaceMode inplace_mode) 
final {
-    TVM_FFI_THROW(InternalError) << "Do not have a default for " << 
op->GetTypeKey();
-    TVM_FFI_UNREACHABLE();
-  }
-
-  UnchangedOr<Expr> Mutate_(const TupleGetItemObj* op, InplaceMode 
inplace_mode) final {
-    TVM_FFI_THROW(InternalError) << "Do not have a default for " << 
op->GetTypeKey();
-    TVM_FFI_UNREACHABLE();
-  }
-
-  UnchangedOr<Expr> Mutate_(const TensorRegionObj* op, InplaceMode 
inplace_mode) final {
-    TVM_FFI_THROW(InternalError) << "Do not have a default for " << 
op->GetTypeKey();
-    TVM_FFI_UNREACHABLE();
-  }
-
-  UnchangedOr<Expr> Mutate_(const GlobalVarObj* op, InplaceMode inplace_mode) 
final {
-    TVM_FFI_THROW(InternalError) << "Do not have a default for " << 
op->GetTypeKey();
-    TVM_FFI_UNREACHABLE();
-  }
-
-  UnchangedOr<Expr> Mutate_(const OpObj* op, InplaceMode inplace_mode) final {
-    TVM_FFI_THROW(InternalError) << "Do not have a default for " << 
op->GetTypeKey();
-    TVM_FFI_UNREACHABLE();
-  }
-
   UnchangedOr<PrimExpr> Mutate_(const prim::AddObj* op, InplaceMode 
inplace_mode) final {
     return AddSubVec(op, [](PrimExpr a, PrimExpr b) { return a + b; }, 
inplace_mode);
   }
@@ -646,50 +485,6 @@ class Vectorizer : public StmtExprMutator {
     }
   }
 
-  UnchangedOr<PrimExpr> Mutate_(const prim::RampObj* op, InplaceMode 
inplace_mode) final {
-    PrimExpr base = this->Mutate(op->base, 
inplace_mode).ValueOrUnchanged(op->base);
-    PrimExpr stride = this->Mutate(op->stride, 
inplace_mode).ValueOrUnchanged(op->stride);
-    TVM_FFI_ICHECK(!base.ty().IsScalableVector())
-        << "Creating scalable vectors from existing vectors is not supported.";
-    TVM_FFI_ICHECK(!stride.ty().IsScalableVector())
-        << "Ramp stride with scalable dtype is not supported";
-    if (base.ty().IsFixedLengthVector() && stride.ty().IsScalar()) {
-      TVM_FFI_ICHECK(op->lanes->IsInstance<IntImmObj>())
-          << "Vectorizing over existing scalable vectors is not supported.";
-      const prim::RampObj* base_ramp = base.as<prim::RampObj>();
-      int op_lanes = op->lanes.as_or_throw<IntImm>()->value.as<int>().value();
-      int base_ramp_lanes = 
base_ramp->lanes.as_or_throw<IntImm>()->value.as<int>().value();
-      if (analyzer_->CanProve(base_ramp->stride ==
-                              stride * MakeConst(stride.ty(), 
base_ramp_lanes))) {
-        return prim::Ramp(base_ramp->base, stride, op_lanes * base_ramp_lanes);
-      }
-    }
-    int lanes = std::max(base.ty().lanes(), stride.ty().lanes());
-    base = BroadcastTo(base, lanes, false);
-    stride = BroadcastTo(stride, lanes, false);
-    ffi::Array<PrimExpr> elems;
-    for (int i = 0; i < lanes; ++i) {
-      elems.push_back(prim::Ramp(prim::Shuffle::ExtractElement(base, i),
-                                 prim::Shuffle::ExtractElement(stride, i), 
op->lanes));
-    }
-    return prim::Shuffle::Concat(elems);
-  }
-
-  UnchangedOr<PrimExpr> Mutate_(const prim::BroadcastObj* op, InplaceMode 
inplace_mode) final {
-    auto value_update = this->Mutate(op->value, inplace_mode);
-    bool value_unchanged = value_update.UnchangedOrSameAs(op->value);
-    PrimExpr value = std::move(value_update).ValueOrUnchanged(op->value);
-    if (value.ty().IsScalableVector() || value.ty().IsFixedLengthVector()) {
-      need_scalarize_ = true;
-      return ffi::Unchanged();
-    }
-    if (value_unchanged) {
-      return ffi::Unchanged();
-    } else {
-      return prim::Broadcast(op->value, op->lanes);
-    }
-  }
-
   UnchangedOr<PrimExpr> Mutate_(const prim::SelectObj* op, InplaceMode 
inplace_mode) final {
     auto cond_update = this->Mutate(op->condition, inplace_mode);
     bool cond_unchanged = cond_update.UnchangedOrSameAs(op->condition);
@@ -732,164 +527,6 @@ class Vectorizer : public StmtExprMutator {
     }
   }
 
-  // If expr
-  PrimExpr MutateIfThenElseExpr_(const CallObj* op, InplaceMode inplace_mode) {
-    PrimExpr cond = this->Mutate(op->args[0].as_or_throw<PrimExpr>())
-                        .ValueOrUnchanged(op->args[0].as_or_throw<PrimExpr>());
-    if (cond.ty().IsScalableVector() || cond.ty().IsFixedLengthVector()) {
-      need_scalarize_ = true;
-      return ffi::GetRef<Call>(op).as_or_throw<PrimExpr>();
-    }
-    PrimExpr t = this->Mutate(op->args[1].as_or_throw<PrimExpr>())
-                     .ValueOrUnchanged(op->args[1].as_or_throw<PrimExpr>());
-    PrimExpr f = this->Mutate(op->args[2].as_or_throw<PrimExpr>())
-                     .ValueOrUnchanged(op->args[2].as_or_throw<PrimExpr>());
-    if (cond.same_as(op->args[0]) && t.same_as(op->args[1]) && 
f.same_as(op->args[2])) {
-      return ffi::GetRef<Call>(op).as_or_throw<PrimExpr>();
-    } else {
-      int t_lanes = GetLanesOrVScaleFactor(t.ty());
-      int f_lanes = GetLanesOrVScaleFactor(f.ty());
-      int lanes = std::max(t_lanes, f_lanes);
-      bool is_scalable = t.ty().IsScalableVector() || 
f.ty().IsScalableVector();
-      t = BroadcastTo(t, lanes, is_scalable);
-      f = BroadcastTo(f, lanes, is_scalable);
-      PrimType op_ty = op->ty.as_or_throw<PrimType>();
-      if (is_scalable) {
-        return Call(PrimType::ScalableVector(op_ty.code(), op_ty.bits(), 
lanes), op->op,
-                    {cond, t, f}, op->attrs, {}, op->loc)
-            .as_or_throw<PrimExpr>();
-      } else {
-        return Call(op_ty.WithLanes(lanes), op->op, {cond, t, f}, op->attrs, 
{}, op->loc)
-            .as_or_throw<PrimExpr>();
-      }
-    }
-  }
-  // Reinterpret expr
-  PrimExpr MutateReinterpretExpr_(const CallObj* op, InplaceMode inplace_mode) 
{
-    TVM_FFI_ICHECK(op->op.same_as(tirx::reinterpret_op()));
-    PrimExpr input = op->args[0].as_or_throw<PrimExpr>();
-    PrimExpr value = this->Mutate(input).ValueOrUnchanged(input);
-    if (value.same_as(op->args[0])) {
-      return ffi::GetRef<Call>(op).as_or_throw<PrimExpr>();
-    } else {
-      int lanes = GetLanesOrVScaleFactor(value.ty());
-      PrimType op_ty = op->ty.as_or_throw<PrimType>();
-      if (value.ty().IsScalableVector()) {
-        return Call(PrimType::ScalableVector(op_ty.code(), op_ty.bits(), 
lanes), op->op, {value},
-                    op->attrs, {}, op->loc)
-            .as_or_throw<PrimExpr>();
-      } else {
-        int new_lanes = (op_ty.code() != DLDataTypeCode::kDLFloat4_e2m1fn &&
-                         input.ty().code() != DLDataTypeCode::kDLFloat4_e2m1fn)
-                            ? (value.ty().bits() * value.ty().lanes()) / 
op_ty.bits()
-                            : value.ty().lanes();
-        return Call(op_ty.WithLanes(new_lanes), op->op, {value}, op->attrs, 
{}, op->loc)
-            .as_or_throw<PrimExpr>();
-      }
-    }
-  }
-  // Call
-  UnchangedOr<Expr> Mutate_(const CallObj* op, InplaceMode inplace_mode) final 
{
-    auto optional_ret_ty = op->ty.as<PrimType>();
-    if (!optional_ret_ty) {
-      // Non-primitive calls are not vectorized themselves.  Visit their 
general Expr operands
-      // to preserve pointer values and rewrite scalar values, scalarizing the 
surrounding
-      // statement if rewriting produces a vector operand.
-      ffi::Array<Expr> new_args;
-      for (const Expr& arg : op->args) {
-        Expr new_arg = this->Mutate(arg).ValueOrUnchanged(arg);
-        if (auto prim_arg = new_arg.as<PrimExpr>();
-            prim_arg && (prim_arg.value().ty().IsScalableVector() ||
-                         prim_arg.value().ty().IsFixedLengthVector())) {
-          need_scalarize_ = true;
-          return ffi::Unchanged();
-        }
-        new_args.push_back(new_arg);
-      }
-      if (op->args.same_as(new_args)) {
-        return ffi::Unchanged();
-      }
-      return Call(op->ty, op->op, new_args, op->attrs, op->ty_args, op->loc);
-    }
-    PrimType ret_ty = optional_ret_ty.value();
-    if (op->op.same_as(prim::if_then_else_op())) {
-      return MutateIfThenElseExpr_(op, inplace_mode);
-    } else if (op->op.same_as(backend::opencl::texture2d_load_op())) {
-      int lane = 0;
-      ffi::Array<PrimExpr> fcd =
-          MutateArray({op->args.back().as_or_throw<PrimExpr>()}, &lane, 
inplace_mode);
-      PrimType dtype = GetTextureElementType(op->args[0]);
-      TVM_FFI_ICHECK(lane * dtype.bits() <= op->args[4].as<IntImmObj>()->value)
-          << "Expected Data to be Read is lesser than or equal to Texture Load 
length";
-
-      auto new_args = op->args;
-      new_args.pop_back();
-      new_args.push_back(fcd[0]);
-      return Call(ret_ty.WithLanes(lane), op->op, new_args, op->attrs, {}, 
op->loc);
-    } else if (op->op.same_as(backend::opencl::texture2d_store_op())) {
-      int lane = 0;
-      // Vectorize the value to store
-      ffi::Array<PrimExpr> value{op->args.back().as_or_throw<PrimExpr>()};
-      ffi::Array<PrimExpr> mutated_value = MutateArray(value, &lane, 
inplace_mode);
-      PrimType dtype = GetTextureElementType(op->args[0]);
-      TVM_FFI_ICHECK(lane * dtype.bits() == op->args[4].as<IntImmObj>()->value)
-          << "Expected Data to be Written equal to Texture Store length";
-      ffi::Array<Expr> new_args = op->args;
-      new_args.Set(new_args.size() - 1, mutated_value[0]);
-      return Call(ret_ty, op->op, new_args, op->attrs, {}, op->loc);
-    } else if (op->op.same_as(tirx::reinterpret_op())) {
-      return MutateReinterpretExpr_(op, inplace_mode);
-    }
-    auto optional_op = op->op.as<Op>();
-    bool vectorizable = optional_op && 
op_vectorizable_.get(optional_op.value(), false) &&
-                        !ret_ty.IsScalableVector();
-
-    if (!vectorizable) {
-      // Cannot vectorize this op
-      ffi::Array<Expr> new_args;
-      for (const Expr& arg : op->args) {
-        Expr new_arg = this->Mutate(arg).ValueOrUnchanged(arg);
-        if (auto prim_arg = new_arg.as<PrimExpr>();
-            prim_arg && (prim_arg.value().ty().IsScalableVector() ||
-                         prim_arg.value().ty().IsFixedLengthVector())) {
-          need_scalarize_ = true;
-          return ffi::Unchanged();
-        }
-        new_args.push_back(new_arg);
-      }
-      if (op->args.same_as(new_args)) {
-        return ffi::Unchanged();
-      } else {
-        return Call(ret_ty, op->op, new_args, op->attrs, {}, op->loc);
-      }
-    } else {
-      int lane = 0;
-      ffi::Array<Expr> new_args;
-      if (op->op.same_as(tirx::call_llvm_pure_intrin_op())) {
-        // op->args[1], will give us total number of arguments to intrinsic
-        ffi::Array<Expr> op_expr_args;
-        for (size_t i = 1; i < op->args.size(); ++i) {
-          // Collect all intrinsic arguments
-          op_expr_args.push_back(op->args[i]);
-        }
-        // Generate RAMP nodes for intrinsic arguments
-        ffi::Array<Expr> updated_args = MutateCallArgs(op_expr_args, &lane, 
inplace_mode);
-        new_args.push_back(op->args[0]);
-        // Collect updated intrinsic arguments
-        for (size_t i = 0; i < updated_args.size(); ++i) {
-          new_args.push_back(updated_args[i]);
-        }
-      } else {
-        new_args = MutateCallArgs(op->args, &lane, inplace_mode);
-      }
-      // normal code path.
-      if (op->args.same_as(new_args)) {
-        return ffi::Unchanged();
-      } else {
-        return Call(ret_ty.WithLanes(lane), op->op, new_args, op->attrs, {}, 
op->loc);
-      }
-    }
-  }
   // TensorLoad
   UnchangedOr<PrimExpr> Mutate_(const TensorLoadObj* op, InplaceMode 
inplace_mode) final {
     auto load = ffi::GetRef<TensorLoad>(op);
@@ -905,113 +542,7 @@ class Vectorizer : public StmtExprMutator {
 
     return load;
   }
-  // Let
-  UnchangedOr<PrimExpr> Mutate_(const prim::LetObj* op, InplaceMode 
inplace_mode) final {
-    auto value_update = this->Mutate(op->value, inplace_mode);
-    bool value_unchanged = value_update.UnchangedOrSameAs(op->value);
-    PrimExpr value = std::move(value_update).ValueOrUnchanged(op->value);
-    // Weaker SSA condition
-    // A single var can be binded in multiple lets
-    // but they have to bind to the same value.
-    // This is used to allow cases when we reuse a single let
-    // expression to cosntruct a nested expr.
-    // (let x = 1 in x + 1) * (let x = 1 in x + 1)
-    auto mapped = VarRemapGet(op->var);
-    if (mapped != nullptr) {
-      TVM_FFI_ICHECK(deep_equal_(mapped.as_or_throw<PrimExpr>(), value))
-          << "Let cannot bind the same var to two different values";
-    }
-    if (GetLanesOrVScaleFactor(value.ty()) != 
GetLanesOrVScaleFactor(op->value.ty())) {
-      Var new_var(op->var->name, value.ty());
-      VarRemapSet(op->var, new_var);
-      return prim::Let(new_var, value,
-                       this->Mutate(op->body, 
inplace_mode).ValueOrUnchanged(op->body));
-    } else {
-      VarRemapSet(op->var, op->var);
-      auto body_update = this->Mutate(op->body, inplace_mode);
-      bool body_unchanged = body_update.UnchangedOrSameAs(op->body);
-      PrimExpr body = std::move(body_update).ValueOrUnchanged(op->body);
-      if (value_unchanged && body_unchanged) {
-        return ffi::Unchanged();
-      } else {
-        return prim::Let(op->var, value, body);
-      }
-    }
-  }
-  UnchangedOr<PrimExpr> Mutate_(const prim::ShuffleObj* op, InplaceMode 
inplace_mode) final {
-    TVM_FFI_ICHECK(op->vectors.size() == 1 && op->indices.size() == 1)
-        << "Cannot vectorize ShuffleObj with multiple vectors or indices: the 
vector size is "
-        << op->vectors.size() << " and the index size is " << 
op->indices.size();
-    int lane_vectors = 0;
-    int lane_indices = 0;
-    ffi::Array<PrimExpr> vectors = MutateArray(op->vectors, &lane_vectors, 
inplace_mode);
-    ffi::Array<PrimExpr> indices = MutateArray(op->indices, &lane_indices, 
inplace_mode);
-    if (vectors.same_as(op->vectors) && indices.same_as(op->indices)) {
-      return ffi::Unchanged();
-    }
 
-    int new_vec_length =
-        var_lanes_.as_or_throw<IntImm>()->value.as<int>().value() / 
op->vectors[0].ty().lanes();
-    PrimExpr updated_index = indices[0];
-    // Check that the indices satisfy the specific patterns.
-    auto f_check_index = [this, op](const PrimExpr& index) {
-      // Allowing Ramp(0, 1, var_lanes_)
-      if (const auto* ramp = index.as<prim::RampObj>()) {
-        if (ramp->base->IsInstance<IntImmObj>() && 
ramp->base.as_or_throw<IntImm>()->value == 0 &&
-            ramp->stride->IsInstance<IntImmObj>() &&
-            ramp->stride.as_or_throw<IntImm>()->value == 1 &&
-            ramp->lanes->IsInstance<IntImmObj>() &&
-            ramp->lanes.as_or_throw<IntImm>()->value == 
var_lanes_.as_or_throw<IntImm>()->value) {
-          return true;
-        }
-      }
-      // Allowing FloorMod(Ramp(0, 1, var_lanes_), 
Broadcast(op->vectors[0]->lanes, var_lanes_))
-      if (const auto* floordiv = index.as<prim::FloorModObj>()) {
-        if (const auto* ramp = floordiv->a.as<prim::RampObj>()) {
-          if (const auto* broadcast = floordiv->b.as<prim::BroadcastObj>()) {
-            if (ramp->base->IsInstance<IntImmObj>() &&
-                ramp->base.as_or_throw<IntImm>()->value == 0 &&
-                ramp->stride->IsInstance<IntImmObj>() &&
-                ramp->stride.as_or_throw<IntImm>()->value == 1 &&
-                ramp->lanes->IsInstance<IntImmObj>() &&
-                ramp->lanes.as_or_throw<IntImm>()->value ==
-                    var_lanes_.as_or_throw<IntImm>()->value &&
-                broadcast->value->IsInstance<IntImmObj>() &&
-                broadcast->value.as_or_throw<IntImm>()->value == 
op->vectors[0].ty().lanes() &&
-                broadcast->lanes->IsInstance<IntImmObj>() &&
-                broadcast->lanes.as_or_throw<IntImm>()->value ==
-                    var_lanes_.as_or_throw<IntImm>()->value) {
-              return true;
-            }
-          }
-        }
-      }
-
-      return false;
-    };
-    TVM_FFI_ICHECK(f_check_index(updated_index));
-
-    if (new_vec_length == 1) {
-      PrimType var_ty = var_->ty.as_or_throw<PrimType>();
-      auto f_substitute = [old_var = var_, replacement = tvm::IntImm(var_ty, 
0)](
-                              const Var& var) -> 
ffi::Expected<ffi::UnchangedOr<ffi::Any>> {
-        if (var.same_as(old_var)) return ffi::Any(replacement);
-        return ffi::Unchanged();
-      };
-      return ffi::StructuralMap<ffi::WalkOrder::kPreOrder>(op->vectors[0], 
f_substitute)
-          .as_or_throw<PrimExpr>();
-    } else {
-      PrimExpr prev_ramp = ramp_;
-      PrimExpr prev_var_lanes = var_lanes_;
-      ramp_ = MakeIndexRamp(2, new_vec_length);
-      var_lanes_ = tvm::IntImm(var_lanes_.ty(), new_vec_length);
-      lane_vectors = 0;
-      vectors = MutateArray(op->vectors, &lane_vectors, inplace_mode);
-      ramp_ = prev_ramp;
-      var_lanes_ = prev_var_lanes;
-      return vectors[0];
-    }
-  }
   // TensorStore
   UnchangedOr<Stmt> Mutate_(const TensorStoreObj* op, InplaceMode 
inplace_mode) final {
     auto store = ffi::GetRef<TensorStore>(op);
@@ -1068,102 +599,15 @@ class Vectorizer : public StmtExprMutator {
 
     return store;
   }
-  // For
-  UnchangedOr<Stmt> Mutate_(const ForObj* op, InplaceMode inplace_mode) final {
-    if (op->kind == ForKind::kVectorized) {
-      LOG(WARNING) << "Detect vectorize inside vectorized loop, ignoring...";
-    }
-    TVM_FFI_ICHECK(IsZero(op->begin));
-    TVM_FFI_ICHECK(!op->end.ty().IsScalableVector() && 
!op->end.ty().IsFixedLengthVector());
-    auto end_update = this->Mutate(op->end, inplace_mode);
-    bool end_unchanged = end_update.UnchangedOrSameAs(op->end);
-    PrimExpr end = std::move(end_update).ValueOrUnchanged(op->end);
-    PrimExpr step = this->Mutate(op->step, 
inplace_mode).ValueOrUnchanged(op->step);
-    if (end.ty().IsScalableVector() || end.ty().IsFixedLengthVector() ||
-        step.ty().IsScalableVector() || step.ty().IsFixedLengthVector()) {
-      return Scalarize(ffi::GetRef<Stmt>(op));
-    }
-    auto body_update = this->Mutate(op->body, inplace_mode);
-    bool body_unchanged = body_update.UnchangedOrSameAs(op->body);
-    Stmt body = std::move(body_update).ValueOrUnchanged(op->body);
-    if (end_unchanged && step.same_as(op->step) && body_unchanged) {
-      return ffi::Unchanged();
-    } else {
-      auto n = ffi::make_object<ForObj>(*op);
-      n->end = end;
-      n->step = step;
-      n->body = body;
-      return For(n);
-    }
-  }
-  // If
-  UnchangedOr<Stmt> Mutate_(const IfObj* op, InplaceMode inplace_mode) final {
-    TVM_FFI_ICHECK(!op->condition.ty().IsScalableVector() &&
-                   !op->condition.ty().IsFixedLengthVector());
-    auto condition_update = this->Mutate(op->condition, inplace_mode);
-    bool condition_unchanged = 
condition_update.UnchangedOrSameAs(op->condition);
-    PrimExpr condition = 
std::move(condition_update).ValueOrUnchanged(op->condition);
-    // need scalarize can be marked as true during visit of condition
-    bool cond_need_scalarize = false;
-    std::swap(cond_need_scalarize, need_scalarize_);
-    // Clear the flag while recursively rewriting branch statements.
-    auto then_case_update = this->Mutate(op->then_case, inplace_mode);
-    bool then_case_unchanged = 
then_case_update.UnchangedOrSameAs(op->then_case);
-    Stmt then_case = 
std::move(then_case_update).ValueOrUnchanged(op->then_case);
-    ffi::Optional<SeqStmt> else_case = std::nullopt;
-    if (op->else_case) {
-      else_case =
-          this->Mutate(op->else_case.value(), 
inplace_mode).ValueOrUnchanged(op->else_case.value());
-    }
-    // Check if we can rewrite the condition with predicated tensors
-    if (EnableTensorLevelPredication(target_) &&
-        (condition.ty().IsScalableVector() || 
condition.ty().IsFixedLengthVector()) &&
-        !else_case.has_value()) {
-      std::pair<bool, Stmt> success_stmt_pair =
-          ffi::make_object<TryPredicateTensorAccesses>(TargetHasRVV(target_))
-              ->Run(then_case, condition);
-      bool can_remove_if_then_else = success_stmt_pair.first;
-      if (can_remove_if_then_else) {
-        return success_stmt_pair.second;
-      }
-    }
 
-    if (cond_need_scalarize || condition.ty().IsScalableVector() ||
-        condition.ty().IsFixedLengthVector()) {
-      return Scalarize(ffi::GetRef<Stmt>(op));
-    }
-    if (condition_unchanged && then_case_unchanged && 
else_case.same_as(op->else_case)) {
-      return ffi::Unchanged();
-    } else {
-      return If(condition, then_case, else_case);
-    }
-  }
-  // While
-  UnchangedOr<Stmt> Mutate_(const WhileObj* op, InplaceMode inplace_mode) 
final {
-    TVM_FFI_THROW(InternalError) << "A while loop inside a vectorized loop not 
supported.";
-    TVM_FFI_UNREACHABLE();
-  }
-  // Bind
   UnchangedOr<Stmt> Mutate_(const BindObj* op, InplaceMode inplace_mode) final 
{
-    auto prim_value = op->value.as<PrimExpr>();
-    if (!prim_value) {
-      return StmtExprMutator::Mutate_(op, inplace_mode);
-    }
-    PrimExpr value =
-        this->Mutate(prim_value.value(), 
inplace_mode).ValueOrUnchanged(prim_value.value());
-    // if visit of value triggers need scalarize
-    // we need to scalarize the let
-    if (need_scalarize_) {
-      need_scalarize_ = false;
-      return Scalarize(ffi::GetRef<Stmt>(op));
-    }
-    TVM_FFI_ICHECK(VarRemapGet(op->var) == nullptr)
-        << "SSA violation, a single var is binded twice";
-
-    if (GetLanesOrVScaleFactor(value.ty()) != 
GetLanesOrVScaleFactor(prim_value.value().ty())) {
-      Var new_var(op->var->name, value.ty());
+    PrimExpr original = op->value.as_or_throw<PrimExpr>();
+    PrimExpr value = Mutate(original).ValueOrUnchanged(original);
+    TVM_FFI_ICHECK(VarRemapGet(op->var) == nullptr) << "SSA violation, a 
single var is bound twice";
+    if (value.ty() != original.ty()) {
+      Var new_var(op->var->name, value.ty(), op->var->loc);
       VarRemapSet(op->var, new_var);
-      return Bind(new_var, value);
+      return Bind(new_var, value, op->loc);
     } else {
       VarRemapSet(op->var, op->var);
       if (value.same_as(op->value)) {
@@ -1173,105 +617,10 @@ class Vectorizer : public StmtExprMutator {
       }
     }
   }
-  // scalarize the statment
-  Stmt Scalarize(Stmt stmt) {
-    PrimType var_ty = var_->ty.as_or_throw<PrimType>();
-    Var idx(var_->name + ".s", var_ty);
-    auto substituter = ffi::make_object<StmtExprMutator>();
-    substituter->VarRemapSet(var_, idx);
-    stmt = substituter->Mutate(stmt).ValueOrUnchanged(stmt);
-    return For(idx.as_or_throw<PrimVar>(), IntImm(var_ty, 0), var_lanes_, 
IntImm(var_ty, 1),
-               ForKind::kDefault, stmt);
-  }
 
  private:
-  PrimExpr MakeIndexRamp(int stride, PrimExpr lanes) const {
-    PrimType var_ty = var_->ty.as_or_throw<PrimType>();
-    PrimType index_ty = var_ty.WithBits(std::max(32, var_ty.bits()));
-    PrimExpr ramp = prim::Ramp(IntImm(index_ty, 0), IntImm(index_ty, stride), 
lanes);
-    // Preserve narrow loop-variable types with an explicit vector cast.
-    return prim::cast(ramp, 
ramp.ty().WithCode(var_ty.code()).WithBits(var_ty.bits()));
-  }
-
-  // analyzer
+  const VectorizationAnalysis::Plan& plan_;
   sym::Analyzer analyzer_;
-  // deep equal
-  prim::ExprDeepEqual deep_equal_;
-  // variable to be replaced
-  Var var_;
-  // the lanes.
-  PrimExpr var_lanes_;
-  // ramp representing the var.
-  PrimExpr ramp_{ffi::UnsafeInit{}};
-  // flag to mark requirment of scalarization.
-  bool need_scalarize_{false};
-  // vectorizable property
-  decltype(Op::GetAttrMap(tvm::tirx::op_attr::kVectorizable)) op_vectorizable_ 
=
-      Op::GetAttrMap(tvm::tirx::op_attr::kVectorizable);
-  /*! \brief The current target context. */
-  ffi::Optional<Target> target_;
-
-  // mutate array, with given lane requirement
-  // when finished, p_lane updates the lane requirement.
-  ffi::Array<PrimExpr> MutateArray(const ffi::Array<PrimExpr>& arr, int* 
p_lanes,
-                                   InplaceMode inplace_mode) {
-    if (arr.size() == 0) return arr;
-    int& lanes = *p_lanes;
-    bool changed = false;
-    std::vector<PrimExpr> new_arr;
-    new_arr.reserve(arr.size());
-    for (size_t i = 0; i < arr.size(); i++) {
-      PrimExpr old_elem = arr[i];
-      auto new_elem_update = this->Mutate(old_elem);
-      bool new_elem_unchanged = new_elem_update.UnchangedOrSameAs(old_elem);
-      PrimExpr new_elem = 
std::move(new_elem_update).ValueOrUnchanged(old_elem);
-      if (!new_elem_unchanged) changed = true;
-      new_arr.push_back(new_elem);
-      lanes = std::max(lanes, new_elem.ty().lanes());
-    }
-
-    for (size_t i = 0; i < arr.size(); ++i) {
-      if (new_arr[i].ty().lanes() != lanes) {
-        new_arr[i] = BroadcastTo(new_arr[i], lanes, false);
-        changed = true;
-      }
-    }
-    if (!changed) return arr;
-    return ffi::Array<PrimExpr>(new_arr);
-  }
-
-  // Mutate primitive call operands together so their lanes stay aligned, while
-  // preserving pointer-valued operands as general Expr.  Pointer expressions
-  // may themselves request scalarization if an address depends on the
-  // vectorized loop variable.
-  ffi::Array<Expr> MutateCallArgs(const ffi::Array<Expr>& arr, int* p_lanes,
-                                  InplaceMode inplace_mode) {
-    if (arr.empty()) return arr;
-    int& lanes = *p_lanes;
-    bool changed = false;
-    std::vector<Expr> new_arr;
-    new_arr.reserve(arr.size());
-    for (size_t i = 0; i < arr.size(); ++i) {
-      const Expr& old_elem = arr[i];
-      auto new_elem_update = this->Mutate(old_elem);
-      bool new_elem_unchanged = new_elem_update.UnchangedOrSameAs(old_elem);
-      Expr new_elem = std::move(new_elem_update).ValueOrUnchanged(old_elem);
-      changed = changed || !new_elem_unchanged;
-      new_arr.push_back(new_elem);
-      if (auto prim_elem = new_elem.as<PrimExpr>()) {
-        lanes = std::max(lanes, prim_elem.value().ty().lanes());
-      }
-    }
-    for (size_t i = 0; i < new_arr.size(); ++i) {
-      if (auto prim_elem = new_arr[i].as<PrimExpr>();
-          prim_elem && prim_elem.value().ty().lanes() != lanes) {
-        new_arr[i] = BroadcastTo(prim_elem.value(), lanes, false);
-        changed = true;
-      }
-    }
-    if (!changed) return arr;
-    return ffi::Array<Expr>(new_arr);
-  }
   template <typename TOp, typename T>
   PrimExpr BinaryVec(const T* op, InplaceMode inplace_mode) {
     static_assert(std::is_same<typename TOp::ContainerType, T>::value, 
"constraint");
@@ -1323,153 +672,115 @@ class Vectorizer : public StmtExprMutator {
   }
 };
 
-// A vectorized loop disappears before UnrollLoop consumes scoped policy.  Keep
-// that policy on the first surviving loops; their normal inheritance handles
-// deeper loops and local overrides, including zero and false values.
-Stmt PreserveUnrollPolicy(Stmt body, const ffi::Map<ffi::String, ffi::Any>& 
annotations) {
-  struct PolicyTransfer : StmtExprMutator {
-    using StmtExprMutator::Mutate;
-    ffi::Map<ffi::String, ffi::Any> policy;
-
-    UnchangedOr<ffi::Any> Mutate(ffi::AnyView value, InplaceMode inplace_mode) 
final {
-      if (value.as<ExprObj>()) return ffi::Unchanged();
-      return StmtExprMutator::Mutate(value, inplace_mode);
-    }
+class VectorizeSkipper : public StmtExprMutator {
+ public:
+  using StmtExprMutator::Mutate;
+  UnchangedOr<ffi::Any> Mutate(ffi::AnyView value, InplaceMode inplace_mode) 
final {
+    if (value.as<ExprObj>()) return ffi::Unchanged();
+    return StmtExprMutator::Mutate(value, inplace_mode);
+  }
 
-    UnchangedOr<Stmt> Mutate_(const ForObj* op, InplaceMode inplace_mode) 
final {
-      auto annotations = op->annotations;
-      for (const auto& [key, value] : policy) {
-        auto existing = annotations.Get(key);
-        if (!existing.has_value() || existing.value() == nullptr) {
-          annotations.Set(key, value);
-        }
-      }
-      if (annotations.same_as(op->annotations)) return ffi::Unchanged();
+  UnchangedOr<Stmt> Mutate_(const ForObj* op, InplaceMode inplace_mode) final {
+    Stmt stmt = StmtExprMutator::Mutate_(op, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(op));
+    op = stmt.as<ForObj>();
+    if (op->kind == ForKind::kVectorized) {
       auto copy = ffi::make_object<ForObj>(*op);
-      copy->annotations = std::move(annotations);
+      copy->kind = ForKind::kDefault;
       return For(copy);
-    }
-  };
-  auto transfer = ffi::make_object<PolicyTransfer>();
-  for (const char* key : {attr::kAutoUnrollMaxStep.name, 
attr::kUnrollExplicit.name}) {
-    if (auto value = annotations.Get(key); value.has_value() && value.value() 
!= nullptr) {
-      transfer->policy.Set(key, value.value());
+    } else {
+      return stmt;
     }
   }
-  if (transfer->policy.empty()) return body;
-  return transfer->Mutate(body, InplaceMode::kDisallow).ValueOrUnchanged(body);
+};
+
+Stmt SkipVectorize(Stmt stmt) {
+  return ffi::make_object<VectorizeSkipper>()
+      ->Mutate(stmt, InplaceMode::kAllow)
+      .ValueOrUnchanged(std::move(stmt));
 }
 
 class LoopVectorizer : public StmtExprMutator {
  public:
   using StmtExprMutator::Mutate;
+  explicit LoopVectorizer(const Function& func)
+      : target_(func->attrs.GetAttr(tvm::attr::kTarget)),
+        noalias_(func->attrs.GetAttr<bool>("tirx.noalias").value_or(false)) {
+    for (const Var& param : func->params) {
+      if (param.as<TensorVar>()) roots_[param.get()] = param.get();
+    }
+  }
+
   UnchangedOr<ffi::Any> Mutate(ffi::AnyView value, InplaceMode inplace_mode) 
final {
     if (value.as<ExprObj>()) return ffi::Unchanged();
     return StmtExprMutator::Mutate(value, inplace_mode);
   }
 
-  explicit LoopVectorizer(DictAttrs attrs) : 
target_(attrs.GetAttr(tvm::attr::kTarget)) {}
+  UnchangedOr<Stmt> Mutate_(const BindObj* op, InplaceMode inplace_mode) final 
{
+    TrackParameterView(op);
+    return StmtExprMutator::Mutate_(op, inplace_mode);
+  }
 
   UnchangedOr<Stmt> Mutate_(const ForObj* op, InplaceMode inplace_mode) final {
-    if (op->kind == ForKind::kVectorized) {
+    if (op->kind != ForKind::kVectorized) return StmtExprMutator::Mutate_(op, 
inplace_mode);
+    if (IsSupportedLoop(op)) {
       PrimExpr extent = Range::GetExtentFromBeginEnd(op->begin, op->end);
-      auto* extent_as_int = extent.as<IntImmObj>();
-
-      TVM_FFI_ICHECK(IsZero(op->begin));
-      // General calls still have vectorization paths that query a compile-time
-      // lane count, so keep them on the existing fixed-width path for now.
-      if (extent_as_int && extent_as_int->value > 1 && TargetHasRVV(target_) &&
-          !ContainsCallNode(op->body)) {
-        return VectorizeFixedLoopForRVV(op, 
extent_as_int->value.as<int>().value());
+      if (auto plan = VectorizationAnalysis(op->var, extent, noalias_, 
roots_).Run(op->body)) {
+        return ffi::make_object<Vectorizer>(op->var, extent, *plan)
+            ->Mutate(op->body, InplaceMode::kDisallow)
+            .ValueOrUnchanged(op->body);
       }
-
-      if (!extent_as_int || extent_as_int->value < 1) {
-        struct VScaleFinder : StmtExprVisitor {
-          ffi::Optional<VisitInterrupt> Visit(ffi::AnyView value) final {
-            if (auto expr = value.as<PrimExpr>(); expr && IsVScaleCall(*expr)) 
{
-              return VisitInterrupt();
-            }
-            return StmtExprVisitor::Visit(value);
-          }
-        };
-        bool is_scalable_expr = 
ffi::make_object<VScaleFinder>()->Visit(extent).has_value();
-        TVM_FFI_ICHECK(is_scalable_expr && TargetHasVLA(target_))
-            << "Failed to vectorize loop with extent " << extent << " for 
target " << target_;
-      }
-      Stmt body = PreserveUnrollPolicy(op->body, op->annotations);
-      return ffi::make_object<Vectorizer>(op->var, extent, target_)
-          ->Mutate(body, inplace_mode)
-          .ValueOrUnchanged(body);
-    } else {
-      return StmtExprMutator::Mutate_(op, inplace_mode);
     }
+    // Preserve the complete original loop, including bindings, order, effects,
+    // multiplicity and annotations. Nested candidates in this rejected region
+    // also stay scalar; never split off or retry individual statements.
+    return SkipVectorize(ffi::GetRef<Stmt>(op));
   }
 
  private:
-  Stmt VectorizeFixedLoopForRVV(const ForObj* op, int64_t extent) {
-    // Match the existing TIRx scalable-vector convention.  LLVM/RVV still
-    // selects the runtime vector length with vsetvli.
-    static constexpr int kDefaultVScaleFactor = 4;
-    PrimType index_dtype = op->var.ty();
-    PrimExpr zero = IntImm(index_dtype, 0);
-    PrimExpr fixed_extent = IntImm(index_dtype, extent);
-    PrimExpr scalable_lanes = CreateNewLanes(/*is_scalable=*/true, 
kDefaultVScaleFactor);
-    PrimType lane_dtype = scalable_lanes.ty();
-    PrimExpr scalable_lanes_index = scalable_lanes;
-    if (scalable_lanes_index.ty() != index_dtype) {
-      scalable_lanes_index = prim::Cast(index_dtype, scalable_lanes_index);
+  void TrackParameterView(const BindObj* op) {
+    // FlattenTensor introduces layout-free views of parameter storage. Only
+    // identical element types and an unadjusted data pointer preserve the
+    // physical index map used by the dependence checks.
+    auto tensor = op->var.as<TensorVar>();
+    if (!tensor || tensor->ty()->layout.has_value()) return;
+    const auto* decl = op->value.as<CallObj>();
+    if (!decl || !decl->op.same_as(decl_tensor_op()) || decl->args.size() != 
1) return;
+    const auto* data = decl->args[0].as<CallObj>();
+    if (!data || !data->op.same_as(tensor_data_ptr_op()) || data->args.size() 
!= 1) return;
+    auto source = data->args[0].as<TensorVar>();
+    if (!source || source->dtype() != tensor->dtype()) return;
+    auto root = roots_.find(source->get());
+    if (root != roots_.end()) roots_[tensor->get()] = root->second;
+  }
+
+  bool IsSupportedLoop(const ForObj* op) const {
+    if (!IsZero(op->begin) || !op->HasTrivialStep()) return false;
+    PrimType index_type = op->var.ty();
+    if (!index_type.IsScalar() || 
!index_type.MatchesCode(DLDataTypeCode::kDLInt) ||
+        (index_type.bits() != 32 && index_type.bits() != 64))
+      return false;
+    PrimExpr lanes = Range::GetExtentFromBeginEnd(op->begin, op->end);
+    if (const auto* count = lanes.as<IntImmObj>()) {
+      const std::initializer_list<int64_t> supported = {2, 4, 8, 16, 32, 64};
+      return std::find(supported.begin(), supported.end(), count->value) != 
supported.end();
     }
-    PrimExpr num_chunks = ceildiv(fixed_extent, scalable_lanes_index);
-
-    PrimVar outer(op->var->name + ".vla.o", index_dtype);
-    PrimVar inner(op->var->name + ".vla.i", lane_dtype);
-    PrimExpr inner_index = inner;
-    if (inner_index.ty() != index_dtype) {
-      inner_index = prim::Cast(index_dtype, inner_index);
+    if (!TargetHasVLA(target_)) return false;
+    const auto* mul = lanes.as<prim::MulObj>();
+    if (!mul) return false;
+    const auto* count = mul->a.as<IntImmObj>();
+    if (!count || !IsVScaleCall(mul->b)) {
+      count = mul->b.as<IntImmObj>();
+      if (!count || !IsVScaleCall(mul->a)) return false;
     }
-    PrimExpr index = outer * scalable_lanes_index + inner_index;
-    auto substituter = ffi::make_object<StmtExprMutator>();
-    substituter->VarRemapSet(op->var, index);
-    Stmt body = substituter->Mutate(op->body).ValueOrUnchanged(op->body);
-    Stmt guarded_body = If(index < fixed_extent, body, std::nullopt, op->loc);
-    Stmt vector_loop = For(inner, IntImm(lane_dtype, 0), scalable_lanes, 
IntImm(lane_dtype, 1),
-                           ForKind::kVectorized, guarded_body, 
op->annotations, op->loc);
-    Stmt loop = For(outer, zero, num_chunks, IntImm(index_dtype, 1), 
ForKind::kDefault, vector_loop,
-                    {}, op->loc);
-
-    return this->Mutate(loop, InplaceMode::kDisallow).ValueOrUnchanged(loop);
+    const std::initializer_list<int64_t> supported = {2, 4, 8, 16};
+    return std::find(supported.begin(), supported.end(), count->value) != 
supported.end();
   }
 
   ffi::Optional<Target> target_;
+  bool noalias_;
+  std::unordered_map<const VarObj*, const VarObj*> roots_;
 };
 
-class VectorizeSkipper : public StmtExprMutator {
- public:
-  using StmtExprMutator::Mutate;
-  UnchangedOr<ffi::Any> Mutate(ffi::AnyView value, InplaceMode inplace_mode) 
final {
-    if (value.as<ExprObj>()) return ffi::Unchanged();
-    return StmtExprMutator::Mutate(value, inplace_mode);
-  }
-
-  UnchangedOr<Stmt> Mutate_(const ForObj* op, InplaceMode inplace_mode) final {
-    Stmt stmt = StmtExprMutator::Mutate_(op, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(op));
-    op = stmt.as<ForObj>();
-    if (op->kind == ForKind::kVectorized) {
-      auto copy = ffi::make_object<ForObj>(*op);
-      copy->kind = ForKind::kDefault;
-      return For(copy);
-    } else {
-      return stmt;
-    }
-  }
-};
-
-Stmt SkipVectorize(Stmt stmt) {
-  return ffi::make_object<VectorizeSkipper>()
-      ->Mutate(stmt, InplaceMode::kAllow)
-      .ValueOrUnchanged(std::move(stmt));
-}
-
 namespace transform {
 
 // TODO(tvm-team): Make it as a target property.
@@ -1477,7 +788,7 @@ Pass VectorizeLoop(bool enable_vectorize) {
   auto pass_func = [=](Function f, IRModule m, PassContext ctx) {
     auto* n = f.CopyOnWrite();
     if (enable_vectorize) {
-      n->body = ffi::make_object<LoopVectorizer>(n->attrs)
+      n->body = ffi::make_object<LoopVectorizer>(f)
                     ->Mutate(n->body, InplaceMode::kAllow)
                     .ValueOrUnchanged(n->body);
     } else {
diff --git a/tests/python/codegen/llvm/test_arm.py 
b/tests/python/codegen/llvm/test_arm.py
index 9b6b51d6e..78c13870e 100644
--- a/tests/python/codegen/llvm/test_arm.py
+++ b/tests/python/codegen/llvm/test_arm.py
@@ -74,7 +74,8 @@ def test_vmlal_s16():
 
                 for n in T.vectorized(N):
                     C[n] = 0
-                    for rv in range(K):
+                for rv in range(K):
+                    for n in T.vectorized(N):
                         C[n] = C[n] + T.Cast("int32", A[rv, n]) * 
T.Cast("int32", B[rv, n])
 
         f = tvm_tirx.tirx.compile(Module, target=target).mod
@@ -102,7 +103,8 @@ def test_vmlal_s16():
 
                 for n in T.vectorized(N):
                     C[n] = 0
-                    for rv in range(K):
+                for rv in range(K):
+                    for n in T.vectorized(N):
                         C[n] = C[n] + T.Cast("int32", A[rv, n]) * 
T.Cast("int32", B[rv])
 
         f = tvm_tirx.tirx.compile(Module, target=target).mod
diff --git a/tests/python/codegen/llvm/test_riscv.py 
b/tests/python/codegen/llvm/test_riscv.py
index 0c0ed986e..7763b10d7 100644
--- a/tests/python/codegen/llvm/test_riscv.py
+++ b/tests/python/codegen/llvm/test_riscv.py
@@ -128,7 +128,7 @@ def test_rvv_vscale_llvm_dbginfo(target):
 
 
 @pytest.mark.skipif(not env.has_llvm_min_version(14), reason="need llvm >= 14")
-def test_rvv_fixed_width_vectorized_loop_uses_scalable_chunks():
+def test_rvv_predicated_loop_fallback_and_fixed_width_vectorization():
     @T.function
     def fixed16_negative(
         A: T.Tensor((14, 23, 67, 99), "float32"),
@@ -141,6 +141,7 @@ def 
test_rvv_fixed_width_vectorized_loop_uses_scalable_chunks():
 
     @T.function
     def fixed16_negative_int64(A: T.Tensor((16,), "float32"), B: 
T.Tensor((16,), "float32")):
+        T.func_attr({"tirx.noalias": True})
         for wi in T.vectorized(T.int64(0), T.int64(16)):
             B[wi] = T.float32(0) - A[wi]
 
@@ -165,15 +166,22 @@ def 
test_rvv_fixed_width_vectorized_loop_uses_scalable_chunks():
         assert not re.search(r"\bfsub\.s\b", assembly)
         assert not re.search(r"\bfsw\b", assembly)
 
-    check_codegen(fixed16_negative)
+    # Predication is outside the conservative vectorizer subset. Preserve the
+    # complete scalar loop and verify it still lowers for RVV.
+    before = tvm_tirx.IRModule({"main": fixed16_negative})
+    tvm_tirx.ir.assert_structural_equal(
+        tvm_tirx.tirx.transform.VectorizeLoop()(before),
+        tvm_tirx.tirx.transform.VectorizeLoop(False)(before),
+    )
+    tvm_tirx.tirx.compile(before, target)
     check_codegen(fixed16_negative_int64)
 
 
 @pytest.mark.skipif(not env.has_llvm_min_version(14), reason="need llvm >= 14")
 def test_rvv_scalable_ramp_expression():
     @T.function
-    def ramp_compare(B: T.Tensor((16,), "int32")):
-        for i in T.vectorized(16):
+    def ramp_compare(B: T.Tensor((128,), "int32")):
+        for i in T.vectorized(T.vscale() * 4):
             B[i] = T.Select(i * 3 + 5 < 29, i * 3 + 5, -1)
 
     target = tvm_tirx.target.Target(
@@ -191,8 +199,10 @@ def test_rvv_scalable_ramp_expression():
 
     assembly = f.inspect_source("asm")
     assert "vid.v" in assembly
-    assert re.search(r"\bvmul\.v", assembly)
-    assert re.search(r"\bvadd\.v", assembly)
+    # LLVM may fuse the multiply/add into a vector multiply-accumulate.
+    assert re.search(r"\bvmacc\.v", assembly) or (
+        re.search(r"\bvmul\.v", assembly) and re.search(r"\bvadd\.v", assembly)
+    )
 
 
 if __name__ == "__main__":
diff --git a/tests/python/tirx/transform/test_vectorize.py 
b/tests/python/tirx/transform/test_vectorize.py
index 3ba3df580..98b355ef3 100644
--- a/tests/python/tirx/transform/test_vectorize.py
+++ b/tests/python/tirx/transform/test_vectorize.py
@@ -17,13 +17,14 @@
 
 # The parser consumes named IR bindings that Python sees as unused.
 # ruff: noqa: F841
+import numpy as np
 import pytest
 
 import tvm_tirx
 import tvm_tirx.testing
-from tvm_tirx.codegen import llvm
 from tvm_tirx.script import ir as I
 from tvm_tirx.script import tirx as T
+from tvm_tirx.testing import env
 
 simple_target = tvm_tirx.target.Target({"kind": "llvm", "mtriple": 
"x86_64-linux-gnu"})
 sve_target = tvm_tirx.target.Target(
@@ -36,6 +37,12 @@ sve_target = tvm_tirx.target.Target(
 )
 
 
+def _assert_scalar_fallback(mod):
+    after = tvm_tirx.tirx.transform.VectorizeLoop()(mod)
+    tvm_tirx.ir.assert_structural_equal(after, 
tvm_tirx.tirx.transform.VectorizeLoop(False)(mod))
+    return after
+
+
 @pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 
4, sve_target)])
 def test_vectorize_loop(extent, target):
     @I.ir_module
@@ -57,7 +64,7 @@ def test_vectorize_loop(extent, target):
     tvm_tirx.ir.assert_structural_equal(mod, After)
 
 
-def test_vectorize_vector():
+def test_fixed_vector_elements_keep_scalar_loop():
     @I.ir_module
     class Module:
         @T.function
@@ -66,18 +73,10 @@ def test_vectorize_vector():
                 for j in T.vectorized(4):
                     A[j] = T.Broadcast(T.float32(1), 4)
 
-    @I.ir_module
-    class Expected:
-        @T.function
-        def main(A: T.Tensor((4,), "float32x4"), n: T.int32):
-            for i in T.serial(n):
-                A[T.Ramp(0, 1, 4)] = T.Broadcast(T.float32(1), 16)
-
-    After = tvm_tirx.tirx.transform.VectorizeLoop()(Module)
-    tvm_tirx.ir.assert_structural_equal(After, Expected)
+    _assert_scalar_fallback(Module)
 
 
-def test_vectorize_vector_scalable_error():
+def test_fixed_vector_access_in_scalable_loop_keeps_scalar_loop():
     @I.ir_module
     class Module:
         @T.function
@@ -86,12 +85,10 @@ def test_vectorize_vector_scalable_error():
             for j in T.vectorized(T.vscale() * 4):
                 A[T.ramp(j * 4, 1, 4)] = T.Broadcast(T.float32(1), 4)
 
-    error_msg = "Creating scalable vectors from existing vectors is not 
supported."
-    with pytest.raises(RuntimeError, match=error_msg):
-        tvm_tirx.tirx.transform.VectorizeLoop()(Module)
+    _assert_scalar_fallback(Module)
 
 
-def test_vectorize_vector_scalable_error2():
+def test_scalable_vector_elements_in_fixed_loop_keep_scalar_loop():
     @I.ir_module
     class Module:
         @T.function
@@ -99,12 +96,10 @@ def test_vectorize_vector_scalable_error2():
             for j in T.vectorized(4):
                 A[j] = T.Broadcast(T.float32(1), T.vscale() * 4)
 
-    error_msg = "Vectorizing over scalable tensor elements is not supported in 
vectorizer."
-    with pytest.raises(RuntimeError, match=error_msg):
-        tvm_tirx.tirx.transform.VectorizeLoop()(Module)
+    _assert_scalar_fallback(Module)
 
 
-def test_vectorize_vector_scalable_error3():
+def test_scalable_vector_access_in_fixed_loop_keeps_scalar_loop():
     @I.ir_module
     class Module:
         @T.function
@@ -115,12 +110,10 @@ def test_vectorize_vector_scalable_error3():
                     T.float32(1), T.vscale() * 4
                 )
 
-    error_msg = "Vectorizing over existing scalable vectors is not supported."
-    with pytest.raises(RuntimeError, match=error_msg):
-        tvm_tirx.tirx.transform.VectorizeLoop()(Module)
+    _assert_scalar_fallback(Module)
 
 
-def test_vectorize_vector_scalable_error4():
+def test_scalable_vector_access_in_scalable_loop_keeps_scalar_loop():
     @I.ir_module
     class Module:
         @T.function(private=True)
@@ -131,12 +124,10 @@ def test_vectorize_vector_scalable_error4():
                     T.float32(1), T.vscale() * 4
                 )
 
-    error_msg = "Creating scalable vectors from existing vectors is not 
supported."
-    with pytest.raises(RuntimeError, match=error_msg):
-        tvm_tirx.tirx.transform.VectorizeLoop()(Module)
+    _assert_scalar_fallback(Module)
 
 
-def test_vectorize_with_if():
+def test_if_in_fixed_loop_keeps_scalar_loop():
     extent = 4
     target = simple_target
 
@@ -152,25 +143,10 @@ def test_vectorize_with_if():
                     if i < n:
                         A[i] = T.float32(2)
 
-    @I.ir_module
-    class After:
-        @T.function
-        def main(A: T.Tensor((25,), "float32"), n: T.int32, x: T.int32):
-            T.func_attr({"target": target})
-            if x < n:
-                A[T.Ramp(0, 1, extent)] = A[T.Ramp(0, 1, extent)] + 
T.Broadcast(
-                    T.float32(1), extent
-                )
-            else:
-                for i_s in range(extent):
-                    if i_s < n:
-                        A[i_s] = T.float32(2)
-
-    mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
-    tvm_tirx.ir.assert_structural_equal(mod, After)
+    _assert_scalar_fallback(Before)
 
 
-def test_vectorize_if_scalable_extent():
+def test_if_in_scalable_loop_keeps_scalar_loop():
     extent = T.vscale() * 4
     target = sve_target
 
@@ -186,29 +162,7 @@ def test_vectorize_if_scalable_extent():
                     if i < n:
                         A[i] = T.float32(2)
 
-    @I.ir_module
-    class After:
-        @T.function
-        def main(A: T.Tensor((25,), "float32"), n: T.int32, x: T.int32):
-            T.func_attr({"target": target})
-            if x < n:
-                A[T.Ramp(0, 1, extent)] = A[T.Ramp(0, 1, extent)] + 
T.Broadcast(
-                    T.float32(1), extent
-                )
-            else:
-                T.evaluate(
-                    T.call_intrin(
-                        "tirx.masked_store",
-                        A,
-                        T.Broadcast(T.float32(2), T.vscale() * 4),
-                        T.Ramp(0, 1, T.vscale() * 4),
-                        T.get_active_lane_mask(0, n, ty="uint1xvscalex4"),
-                        ty="void",
-                    )
-                )
-
-    mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
-    tvm_tirx.ir.assert_structural_equal(mod, After)
+    _assert_scalar_fallback(Before)
 
 
 @pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 
4, sve_target)])
@@ -268,7 +222,7 @@ def test_vectorize_with_bound_condition(extent, target, 
comparison):
 
 
 @pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 
4, sve_target)])
-def test_vectorize_if_then_else_scalarize(extent, target):
+def test_varying_if_then_else_keeps_scalar_loop(extent, target):
     @I.ir_module
     class Before:
         @T.function
@@ -290,7 +244,7 @@ def test_vectorize_if_then_else_scalarize(extent, target):
 
 
 @pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 
4, sve_target)])
-def test_vectorize_if_then_else_vector(extent, target):
+def test_uniform_if_then_else_keeps_scalar_loop(extent, target):
     @I.ir_module
     class Before:
         @T.function
@@ -300,18 +254,7 @@ def test_vectorize_if_then_else_vector(extent, target):
                 for j in T.vectorized(extent):
                     A[i * extent + j] = T.if_then_else(i > 0, A[i * extent + 
j], 0)
 
-    @I.ir_module
-    class After:
-        @T.function
-        def main(A: T.Tensor((25,), "float32"), n: T.int32):
-            T.func_attr({"target": target})
-            for i in range(n):
-                A[T.Ramp(i * extent, 1, extent)] = T.if_then_else(
-                    i > 0, A[T.Ramp(i * extent, 1, extent)], T.Broadcast(0, 
extent)
-                )
-
-    mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
-    tvm_tirx.ir.assert_structural_equal(mod, After)
+    _assert_scalar_fallback(Before)
 
 
 def test_vectorize_let_if_then_else():
@@ -338,9 +281,7 @@ def test_vectorize_let_if_then_else():
     tvm_tirx.ir.assert_structural_equal(mod, After)
 
 
-def test_vectorize_while_fail():
-    """A while loop inside a vectorized loop should fail."""
-
+def test_vectorize_while_fallback():
     @I.ir_module
     class Module:
         @T.function
@@ -353,7 +294,7 @@ def test_vectorize_while_fail():
             for j in range(64):
                 C[j] = T.float32(0)
 
-            # While loop inside vectorized loop (should fail)
+            # While loop keeps scalar execution
             i = T.alloc_tensor(ty_args=[T.Tensor((1,), "int32", 
scope="local")])
             i[0] = 0
             for j in T.vectorized(64):
@@ -361,32 +302,24 @@ def test_vectorize_while_fail():
                     C[j] = C[j] + A[j] + B[j]
                     i[0] = i[0] + 1
 
-    with pytest.raises(RuntimeError, match="A while loop inside a vectorized 
loop not supported"):
-        tvm_tirx.tirx.compile(Module, target="llvm")
+    after = _assert_scalar_fallback(Module)
+    tvm_tirx.tirx.compile(after, target="llvm")
 
 
 @pytest.mark.parametrize(
-    "extent, vec_str, target",
-    [(16, "float32x16", simple_target), (T.vscale() * 8, "float32xvscalex8", 
sve_target)],
+    "extent, target",
+    [(16, simple_target), (T.vscale() * 8, sve_target)],
 )
-def test_vectorize_with_reinterpret(extent, vec_str, target):
+def test_reinterpret_keeps_scalar_loop(extent, target):
     @I.ir_module
     class Before:
         @T.function
         def main(A: T.Tensor((16,), "int32"), B: T.Tensor((16,), "float32")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             for i in T.vectorized(0, extent):
                 B[i] = T.reinterpret(A[i], ty="float32")
 
-    @I.ir_module
-    class After:
-        @T.function
-        def main(A: T.Tensor((16,), "int32"), B: T.Tensor((16,), "float32")):
-            T.func_attr({"target": target})
-            B[T.Ramp(0, 1, extent)] = T.reinterpret(A[T.Ramp(0, 1, extent)], 
ty=vec_str)
-
-    mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
-    tvm_tirx.ir.assert_structural_equal(mod, After)
+    _assert_scalar_fallback(Before)
 
 
 @pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 
4, sve_target)])
@@ -415,7 +348,7 @@ def test_vectorize_binary(op, extent, target):
     class Before:
         @T.function
         def main(A: T.Tensor((25,), "float32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             for j in T.vectorized(extent):
                 A[j] = op(T.float32(3), B[j])
 
@@ -423,7 +356,7 @@ def test_vectorize_binary(op, extent, target):
     class After:
         @T.function
         def main(A: T.Tensor((25,), "float32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             A[T.Ramp(0, 1, extent)] = op(T.Broadcast(T.float32(3), extent), 
B[T.Ramp(0, 1, extent)])
 
     mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
@@ -432,24 +365,16 @@ def test_vectorize_binary(op, extent, target):
 
 @pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 
4, sve_target)])
 @pytest.mark.parametrize("op", (T.And, T.Or))
-def test_vectorize_logical(op, extent, target):
+def test_boolean_storage_keeps_scalar_loop(op, extent, target):
     @I.ir_module
     class Before:
         @T.function
         def main(A: T.Tensor((25,), "bool"), B: T.Tensor((25,), "bool")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             for j in T.vectorized(extent):
                 A[j] = op(T.bool(1), B[j])
 
-    @I.ir_module
-    class After:
-        @T.function
-        def main(A: T.Tensor((25,), "bool"), B: T.Tensor((25,), "bool")):
-            T.func_attr({"target": target})
-            A[T.Ramp(0, 1, extent)] = op(T.Broadcast(T.bool(1), extent), 
B[T.Ramp(0, 1, extent)])
-
-    mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
-    tvm_tirx.ir.assert_structural_equal(mod, After)
+    _assert_scalar_fallback(Before)
 
 
 @pytest.mark.parametrize("extent, target", [(4, simple_target), (T.vscale() * 
4, sve_target)])
@@ -458,7 +383,7 @@ def test_vectorize_select(extent, target):
     class Before:
         @T.function
         def main(A: T.Tensor((25,), "float32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             for j in T.vectorized(extent):
                 A[j] = T.Select(T.bool(True), A[j], B[j])
 
@@ -466,7 +391,7 @@ def test_vectorize_select(extent, target):
     class After:
         @T.function
         def main(A: T.Tensor((25,), "float32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             A[T.Ramp(0, 1, extent)] = T.Select(
                 T.Broadcast(T.bool(True), extent),
                 A[T.Ramp(0, 1, extent)],
@@ -486,7 +411,7 @@ def test_vectorize_cast(extent, vec_str, target):
     class Before:
         @T.function
         def main(A: T.Tensor((25,), "int32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             for j in T.vectorized(extent):
                 A[j] = T.Cast("int32", B[j])
 
@@ -494,14 +419,14 @@ def test_vectorize_cast(extent, vec_str, target):
     class After:
         @T.function
         def main(A: T.Tensor((25,), "int32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
+            T.func_attr({"target": target, "tirx.noalias": True})
             A[T.Ramp(0, 1, extent)] = T.Cast(vec_str, B[T.Ramp(0, 1, extent)])
 
     mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
     tvm_tirx.ir.assert_structural_equal(mod, After)
 
 
-def test_illegal_extent():
+def test_dynamic_extent_fallback():
     n = T.dynamic("n", "int32")
 
     @I.ir_module(check_well_formed=False)
@@ -511,12 +436,10 @@ def test_illegal_extent():
             for j in T.vectorized(n):
                 A[j] = 3
 
-    error_msg = r"Failed to vectorize loop with extent n for target None"
-    with pytest.raises(RuntimeError, match=error_msg):
-        tvm_tirx.tirx.transform.VectorizeLoop()(Mod)
+    _assert_scalar_fallback(Mod)
 
 
-def test_illegal_vscale_in_non_sve_compilation():
+def test_vscale_in_non_sve_vectorization_fallback():
     @I.ir_module
     class Mod:
         @T.function
@@ -525,12 +448,10 @@ def test_illegal_vscale_in_non_sve_compilation():
             for j in T.vectorized(0, 4 * T.vscale()):
                 A[j] = 13
 
-    msg = "Failed to vectorize loop with extent 4 \\* T.vscale\\(\\) for 
target"
-    with pytest.raises(RuntimeError, match=msg):
-        tvm_tirx.tirx.transform.VectorizeLoop()(Mod)
+    _assert_scalar_fallback(Mod)
 
 
-def test_vectorize_and_predicate_all_tensor_loads_stores():
+def test_conditional_load_store_keeps_scalar_loop():
     @T.function
     def before(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
         T.func_attr({"global_symbol": "main", "tirx.noalias": True})
@@ -539,45 +460,16 @@ def 
test_vectorize_and_predicate_all_tensor_loads_stores():
                 if i_0 * 4 + i_1 < 14:
                     B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
 
-    @T.function
-    def expected(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
-        T.func_attr({"global_symbol": "main", "tirx.noalias": True})
-        for i_0 in range(4):
-            T.evaluate(
-                T.call_intrin(
-                    "tirx.masked_store",
-                    B,
-                    (
-                        T.call_intrin(
-                            "tirx.masked_load",
-                            A,
-                            T.Ramp(i_0 * 4, 1, 4),
-                            T.get_active_lane_mask(i_0 * 4, 14, ty="uint1x4"),
-                            ty="float32x4",
-                        )
-                        + T.Broadcast(T.float32(1), 4)
-                    ),
-                    T.Ramp(i_0 * 4, 1, 4),
-                    T.get_active_lane_mask(i_0 * 4, 14, ty="uint1x4"),
-                    ty="void",
-                )
-            )
-
     input_func = before
 
     @I.ir_module(check_well_formed=False)
     class InputModule:
         main = input_func
 
-    mod = InputModule
-    with 
tvm_tirx.ir.transform.PassContext(config={"tirx.enable_tensor_level_predication":
 True}):
-        after = tvm_tirx.tirx.transform.VectorizeLoop()(mod)["main"]
-    tvm_tirx.ir.assert_structural_equal(after, expected)
+    _assert_scalar_fallback(InputModule)
 
 
-def test_vectorize_and_predicate_some_tensor_loads_stores():
-    # Currently revert to scalarizing the block if not all accesses
-    # have been predicated, otherwise incorrect code is generated.
+def test_conditional_uniform_load_keeps_scalar_loop():
     @T.function
     def before(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
         T.func_attr({"global_symbol": "main", "tirx.noalias": True})
@@ -600,12 +492,11 @@ def 
test_vectorize_and_predicate_some_tensor_loads_stores():
         main = input_func
 
     mod = InputModule
-    with 
tvm_tirx.ir.transform.PassContext(config={"tirx.enable_tensor_level_predication":
 True}):
-        after = tvm_tirx.tirx.transform.VectorizeLoop()(mod)["main"]
+    after = tvm_tirx.tirx.transform.VectorizeLoop()(mod)["main"]
     tvm_tirx.ir.assert_structural_equal(after, expected)
 
 
-def test_vectorize_and_predicate_multiple_access_statements():
+def test_conditional_multiple_stores_keep_scalar_loop():
     @T.function
     def before(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
         T.func_attr({"global_symbol": "main", "tirx.noalias": True})
@@ -615,44 +506,16 @@ def 
test_vectorize_and_predicate_multiple_access_statements():
                     A[i_0 * 4 + i_1] = 2.0
                     B[i_0 * 4 + i_1] = 1.0
 
-    @T.function
-    def expected(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
-        T.func_attr({"global_symbol": "main", "tirx.noalias": True})
-        for i_0 in range(4):
-            T.evaluate(
-                T.call_intrin(
-                    "tirx.masked_store",
-                    A,
-                    T.Broadcast(T.float32(2), 4),
-                    T.Ramp(i_0 * 4, 1, 4),
-                    T.get_active_lane_mask(i_0 * 4, 14, ty="uint1x4"),
-                    ty="void",
-                )
-            )
-            T.evaluate(
-                T.call_intrin(
-                    "tirx.masked_store",
-                    B,
-                    T.Broadcast(T.float32(1), 4),
-                    T.Ramp(i_0 * 4, 1, 4),
-                    T.get_active_lane_mask(i_0 * 4, 14, ty="uint1x4"),
-                    ty="void",
-                )
-            )
-
     input_func = before
 
     @I.ir_module(check_well_formed=False)
     class InputModule:
         main = input_func
 
-    before_mod = InputModule
-    with 
tvm_tirx.ir.transform.PassContext(config={"tirx.enable_tensor_level_predication":
 True}):
-        after = tvm_tirx.tirx.transform.VectorizeLoop()(before_mod)["main"]
-    tvm_tirx.ir.assert_structural_equal(after, expected)
+    _assert_scalar_fallback(InputModule)
 
 
-def test_vectorize_nested_predicates_preserve_both_masks():
+def test_nested_conditionals_keep_scalar_loop():
     rvv_target = tvm_tirx.target.Target(
         {"kind": "llvm", "mtriple": "riscv64-unknown-linux-gnu", "mattr": 
["+v"]}
     )
@@ -673,46 +536,10 @@ def 
test_vectorize_nested_predicates_preserve_both_masks():
     class InputModule:
         before = input_func
 
-    after = tvm_tirx.tirx.transform.VectorizeLoop()(InputModule)
-    after = tvm_tirx.tirx.transform.StmtSimplify()(after)["before"]
+    _assert_scalar_fallback(InputModule)
 
-    @I.ir_module
-    class Expected:
-        @T.function
-        def before(A: T.Tensor((16,), T.float32), B: T.Tensor((16,), 
T.float32)):
-            T.func_attr({"target": rvv_target})
-            for i_0 in range(4):
-                for i_1_vla_o in range((T.vscale() * 4 + 3) // (T.vscale() * 
4)):
-                    T.masked_store(
-                        A,
-                        T.Broadcast(T.float32(1.0), T.vscale() * 4),
-                        T.Ramp(i_0 * 4 + i_1_vla_o * T.vscale() * 4, 1, 
T.vscale() * 4),
-                        T.get_active_lane_mask(
-                            i_0 * 4 + i_1_vla_o * T.vscale() * 4, 14, 
ty="uint1xvscalex4"
-                        )
-                        & T.get_active_lane_mask(
-                            i_0 * 4 + i_1_vla_o * T.vscale() * 4, 15, 
ty="uint1xvscalex4"
-                        )
-                        & T.get_active_lane_mask(
-                            i_1_vla_o * T.vscale() * 4, 4, ty="uint1xvscalex4"
-                        ),
-                    )
-                    T.masked_store(
-                        B,
-                        T.Broadcast(T.float32(2.0), T.vscale() * 4),
-                        T.Ramp(i_0 * 4 + i_1_vla_o * T.vscale() * 4, 1, 
T.vscale() * 4),
-                        T.get_active_lane_mask(
-                            i_0 * 4 + i_1_vla_o * T.vscale() * 4, 15, 
ty="uint1xvscalex4"
-                        )
-                        & T.get_active_lane_mask(
-                            i_1_vla_o * T.vscale() * 4, 4, ty="uint1xvscalex4"
-                        ),
-                    )
-
-    tvm_tirx.ir.assert_structural_equal(after, Expected["before"])
-
-
-def test_vectorize_and_predicate_invalid_conditions():
+
+def test_multiple_conditions_keep_scalar_loop():
     @T.function
     def before(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
         T.func_attr({"global_symbol": "main", "tirx.noalias": True})
@@ -725,64 +552,16 @@ def test_vectorize_and_predicate_invalid_conditions():
                 if i_0 * 4 + i_1 < i_0 * 4 + i_1:
                     A[i_0 * 4 + i_1] = 2.0
 
-    @T.function
-    def expected(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
-        T.func_attr({"global_symbol": "main", "tirx.noalias": True})
-        for i_0 in range(4):
-            for i_1_s in range(4):
-                if i_0 * 4 + i_1_s > 14:
-                    A[i_0 * 4 + i_1_s] = T.float32(2)
-            for i_1_s in range(4):
-                if 14 < i_0 * 4 + i_1_s:
-                    A[i_0 * 4 + i_1_s] = T.float32(2)
-            for i_1_s in range(4):
-                if i_0 * 4 + i_1_s < i_0 * 4 + i_1_s:
-                    A[i_0 * 4 + i_1_s] = T.float32(2)
-
     input_func = before
 
     @I.ir_module(check_well_formed=False)
     class InputModule:
         main = input_func
 
-    before_mod = InputModule
-    with 
tvm_tirx.ir.transform.PassContext(config={"tirx.enable_tensor_level_predication":
 True}):
-        after = tvm_tirx.tirx.transform.VectorizeLoop()(before_mod)["main"]
-    tvm_tirx.ir.assert_structural_equal(after, expected)
+    _assert_scalar_fallback(InputModule)
 
 
-def test_vectorize_with_explicitly_disabled_tensor_level_predication():
-    # Since the target has the VLA feature, tensor level predication is enabled
-    # by default. However, it has been explicitly disabled by the pass context
-    # option, so no tensor-level predicates should be added.
-    @T.function
-    def before(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
-        T.func_attr({"target": sve_target, "global_symbol": "main", 
"tirx.noalias": True})
-        for i_0 in T.serial(T.ceildiv(14, 4)):
-            for i_1 in T.vectorized(4):
-                if i_0 * 4 + i_1 < 14:
-                    B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
-
-    @T.function
-    def expected(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
-        T.func_attr({"target": sve_target, "global_symbol": "main", 
"tirx.noalias": True})
-        for i_0, i_1_s in T.grid(4, 4):
-            if i_0 * 4 + i_1_s < 14:
-                B[i_0 * 4 + i_1_s] = A[i_0 * 4 + i_1_s] + T.float32(1)
-
-    input_func = before
-
-    @I.ir_module(check_well_formed=False)
-    class InputModule:
-        main = input_func
-
-    mod = InputModule
-    with 
tvm_tirx.ir.transform.PassContext(config={"tirx.enable_tensor_level_predication":
 False}):
-        after = tvm_tirx.tirx.transform.VectorizeLoop()(mod)["main"]
-    tvm_tirx.ir.assert_structural_equal(after, expected)
-
-
-def 
test_vectorize_and_predicate_tensor_load_stores_with_sve_func_attr_target():
+def test_conditional_load_store_on_sve_keeps_scalar_loop():
     @T.function
     def before(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
         T.func_attr({"global_symbol": "main", "tirx.noalias": True, "target": 
sve_target})
@@ -791,100 +570,58 @@ def 
test_vectorize_and_predicate_tensor_load_stores_with_sve_func_attr_target():
                 if i_0 * 4 + i_1 < 14:
                     B[i_0 * 4 + i_1] = A[i_0 * 4 + i_1] + 1.0
 
-    @T.function
-    def expected(A: T.Tensor((16,), "float32"), B: T.Tensor((16,), "float32")):
-        T.func_attr({"global_symbol": "main", "tirx.noalias": True, "target": 
sve_target})
-        for i_0 in range(4):
-            T.evaluate(
-                T.call_intrin(
-                    "tirx.masked_store",
-                    B,
-                    (
-                        T.call_intrin(
-                            "tirx.masked_load",
-                            A,
-                            T.Ramp(i_0 * 4, 1, 4),
-                            T.get_active_lane_mask(i_0 * 4, 14, ty="uint1x4"),
-                            ty="float32x4",
-                        )
-                        + T.Broadcast(T.float32(1), 4)
-                    ),
-                    T.Ramp(i_0 * 4, 1, 4),
-                    T.get_active_lane_mask(i_0 * 4, 14, ty="uint1x4"),
-                    ty="void",
-                )
-            )
-
     input_func = before
 
     @I.ir_module(check_well_formed=False)
     class InputModule:
         main = input_func
 
-    mod = InputModule
-    after = tvm_tirx.tirx.transform.VectorizeLoop()(mod)["main"]
-    tvm_tirx.ir.assert_structural_equal(after, expected)
+    _assert_scalar_fallback(InputModule)
 
 
[email protected](
-    "extent, vec_str, target",
-    [(4, "float32x4", simple_target)],
-)
-def test_vectorize_llvm_pure_intrin(extent, vec_str, target):
+def test_llvm_pure_intrin_keeps_scalar_loop():
     @I.ir_module
     class Before:
         @T.function
         def main(A: T.Tensor((25,), "float32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
-            for j in T.vectorized(extent):
+            T.func_attr({"target": simple_target, "tirx.noalias": True})
+            for j in T.vectorized(4):
                 A[j] = T.call_llvm_pure_intrin("llvm.sqrt", B[j], ty="float32")
 
-    @I.ir_module
-    class After:
-        @T.function
-        def main(A: T.Tensor((25,), "float32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
-            A[T.Ramp(0, 1, extent)] = T.call_llvm_pure_intrin(
-                "llvm.sqrt", B[T.Ramp(0, 1, extent)], ty=vec_str
-            )
-
-    mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
-    tvm_tirx.ir.assert_structural_equal(mod, After)
-    mod = tvm_tirx.tirx.compile(mod, target=target)
+    after = _assert_scalar_fallback(Before)
+    tvm_tirx.tirx.compile(after, target="llvm")
 
 
[email protected](
-    "extent, vec_str, target",
-    [(4, "int32x4", simple_target)],
-)
-def test_vectorize_llvm_pure_intrin_fail(extent, vec_str, target):
-    @I.ir_module
-    class Before:
-        @T.function
-        def main(A: T.Tensor((25,), "int32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
-            for j in T.vectorized(extent):
-                A[j] = T.call_llvm_pure_intrin("llvm.lround", B[j], ty="int32")
-
-    @I.ir_module
-    class After:
-        @T.function
-        def main(A: T.Tensor((25,), "int32"), B: T.Tensor((25,), "float32")):
-            T.func_attr({"target": target})
-            A[T.Ramp(0, 1, extent)] = T.call_llvm_pure_intrin(
-                "llvm.lround", B[T.Ramp(0, 1, extent)], ty=vec_str
-            )
-
-    mod = tvm_tirx.tirx.transform.VectorizeLoop()(Before)
-    tvm_tirx.ir.assert_structural_equal(mod, After)
-    # LLVM 20 added vector support for llvm.lround/llvm.llround.  The IR 
Verifier's
-    # "Intrinsic does not support vectors" check was removed in release/20.x, 
so
-    # compilation only fails on LLVM <= 19.
-    if llvm.version_major() >= 20:
-        tvm_tirx.tirx.compile(mod, target=target)
-    else:
-        with pytest.raises(Exception, match="Intrinsic does not support 
vectors"):
-            tvm_tirx.tirx.compile(mod, target=target)
[email protected](not env.has_llvm(), reason="need LLVM")
+def test_vectorize_assert_preserves_scalar_failure_order():
+    @T.function
+    def main(A: T.Tensor((4,), "int32")):
+        for i in T.vectorized(4):
+            value: T.let = i + 1
+            A[i] = value
+            assert i < 2, ("RuntimeError", ["stop"])
+            A[i] = value + 10
+
+    before = tvm_tirx.IRModule({"main": main})
+    _assert_scalar_fallback(before)
+    data = tvm_tirx.runtime.tensor(np.full(4, -1, dtype="int32"))
+    with pytest.raises(RuntimeError, match="stop"):
+        tvm_tirx.tirx.compile(main, target="llvm")(data)
+    np.testing.assert_array_equal(data.numpy(), [11, 12, 3, -1])
+
+
+def test_vectorize_unsupported_scalable_factor_keeps_scalar_loop():
+    @T.function
+    def main(A: T.Tensor((64,), "int32")):
+        T.func_attr({"target": sve_target})
+        for i in T.vectorized(T.Mul(T.vscale(), 1)):
+            value: T.let = i + 1
+            A[i] = value
+
+    before = tvm_tirx.IRModule({"main": main})
+    _assert_scalar_fallback(before)
+    if env.has_llvm():
+        tvm_tirx.tirx.compile(before, target=sve_target)
 
 
 if __name__ == "__main__":

Reply via email to