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

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


The following commit(s) were added to refs/heads/main by this push:
     new 3dc160cdff [REFACTOR][TIR] Store loop thread binding in annotations 
(#20606)
3dc160cdff is described below

commit 3dc160cdffd2d156a90e2d839fa717b4b5afd864
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Oct 8 11:17:59 2026 -0400

    [REFACTOR][TIR] Store loop thread binding in annotations (#20606)
    
    Thread-bound loops now use the parallel kind and store their thread tag
    in `annotations["thread_binding"]`. The dedicated field and loop kind
    are removed, while `kDefault` / `ForKind.DEFAULT` retain ordinary
    sequential iteration semantics.
    
    Update native and Python construction, scheduling, analysis, lowering,
    and script conversion to use the annotation consistently. Preserve
    `T.thread_binding` syntax and distinguish explicit thread placement from
    unbound CPU parallel loops.
---
 include/tvm/tirx/stmt.h                            | 39 +++++--------
 python/tvm/s_tir/dlight/gpu/fallback.py            |  5 +-
 python/tvm/tirx/stmt.py                            | 21 +++----
 src/relax/transform/split_call_tir_by_pattern.cc   |  4 +-
 .../analysis/sblock_buffer_access_lca_detector.cc  |  5 +-
 .../feature_extractor/per_store_feature.cc         | 33 +++++------
 .../postproc/rewrite_parallel_vectorize_unroll.cc  |  2 +-
 .../meta_schedule/schedule/cuda/thread_bind.cc     |  2 +-
 src/s_tir/schedule/analysis/analysis.cc            |  4 +-
 src/s_tir/schedule/ir_comparator.cc                | 13 -----
 src/s_tir/schedule/primitive/annotate.cc           |  4 ++
 src/s_tir/schedule/primitive/blockize_tensorize.cc |  2 +-
 src/s_tir/schedule/primitive/cache_index.cc        |  2 +-
 src/s_tir/schedule/primitive/cache_read_write.cc   |  6 +-
 src/s_tir/schedule/primitive/compute_at.cc         |  2 +-
 src/s_tir/schedule/primitive/compute_inline.cc     |  2 +-
 src/s_tir/schedule/primitive/decompose_padding.cc  |  6 +-
 src/s_tir/schedule/primitive/for_kind.cc           | 25 +++++----
 .../schedule/primitive/layout_transformation.cc    | 10 ++--
 .../schedule/primitive/loop_transformation.cc      | 24 ++++----
 src/s_tir/schedule/primitive/pad_einsum.cc         |  2 +-
 src/s_tir/schedule/primitive/read_write_at.cc      |  2 +-
 src/s_tir/schedule/primitive/reduction.cc          |  4 +-
 src/s_tir/schedule/utils.h                         |  4 +-
 src/s_tir/transform/annotate_irregular_loop.cc     |  2 +-
 src/s_tir/transform/compact_buffer_region.cc       |  8 +--
 src/s_tir/transform/default_gpu_schedule.cc        |  4 +-
 src/s_tir/transform/inject_software_pipeline.cc    |  2 +-
 src/s_tir/transform/inject_virtual_thread.cc       |  4 +-
 src/s_tir/transform/lift_thread_binding.cc         | 15 ++---
 src/s_tir/transform/loop_partition.cc              | 17 +++---
 .../transform/lower_cross_thread_reduction.cc      | 34 ++++++-----
 src/s_tir/transform/lower_opaque_block.cc          |  6 +-
 src/s_tir/transform/lower_thread_binding.cc        | 11 ++--
 .../manifest_shared_memory_local_stage.cc          |  2 +-
 src/s_tir/transform/memhammer_coalesce.cc          | 11 ++--
 .../transform/memhammer_intermediate_stage.cc      |  9 ++-
 src/s_tir/transform/memhammer_lower_auto_copy.cc   | 10 ++--
 .../transform/memhammer_tensorcore_rewrite.cc      | 16 +++---
 src/target/llvm/codegen_cpu.cc                     |  4 +-
 src/target/llvm/codegen_llvm.cc                    |  2 +-
 src/te/operation/create_function.cc                |  2 +-
 src/tirx/ir/stmt.cc                                | 51 +++++++----------
 src/tirx/script/ir_builder/ir.cc                   | 65 ++++++++++++----------
 src/tirx/script/printer/for_loop.cc                | 29 +++++-----
 src/tirx/transform/bind_target.cc                  |  4 +-
 src/tirx/transform/lower_tirx_opaque.cc            | 15 ++---
 src/tirx/transform/lower_tvm_builtin.cc            |  2 +-
 src/tirx/transform/make_packed_api.cc              |  6 +-
 src/tirx/transform/storage_rewrite.cc              |  4 +-
 src/tirx/transform/unroll_loop.cc                  |  7 ++-
 src/tirx/transform/vectorize_loop.cc               | 10 ++--
 tests/python/ir/test_nonnullable_ir.py             |  2 +-
 tests/python/relax/test_pipeline.py                |  2 +-
 tests/python/runtime/test_runtime_module_load.py   |  2 +-
 .../test_s_tir_analysis_verify_block_scope.py      |  2 +-
 .../s_tir/analysis/test_sblock_access_region.py    |  2 +-
 .../s_tir/schedule/test_tir_schedule_analysis.py   |  2 +-
 .../s_tir/schedule/test_tir_schedule_state.py      |  3 +-
 .../s_tir/script/test_s_tir_script_basic_usage.py  |  8 +--
 .../test_tir_analysis_verify_well_formed.py        |  2 +-
 tests/python/tirx-base/test_tir_constructor.py     |  2 +-
 tests/python/tirx-base/test_tir_nodes.py           |  2 +-
 .../test_tir_transform_common_subexpr_elim.py      |  8 +--
 .../test_tir_transform_convert_ssa.py              |  2 +-
 .../test_tir_transform_lower_intrin.py             |  2 +-
 .../test_tir_transform_remove_no_op.py             |  8 +--
 .../tirx/script/test_tirx_script_basic_usage.py    |  4 +-
 .../tirx/script/test_tirx_script_ir_builder.py     |  8 +--
 69 files changed, 306 insertions(+), 330 deletions(-)

diff --git a/include/tvm/tirx/stmt.h b/include/tvm/tirx/stmt.h
index 1e90a815d3..2a2df56114 100644
--- a/include/tvm/tirx/stmt.h
+++ b/include/tvm/tirx/stmt.h
@@ -431,9 +431,9 @@ class IfThenElse : public Stmt {
  *  in all TIR passes.
  */
 enum class ForKind : int {
-  /*! \brief default semantics -- serial execution. */
-  kSerial = 0,
-  /*! \brief Parallel execution on CPU. */
+  /*! \brief Ordinary loop with sequential iteration semantics. */
+  kDefault = 0,
+  /*! \brief Parallel execution, optionally placed by the thread_binding 
annotation. */
   kParallel = 1,
   /*!
    * \brief Vector SIMD loop.
@@ -442,13 +442,6 @@ enum class ForKind : int {
   kVectorized = 2,
   /*! \brief The loop body must be unrolled. */
   kUnrolled = 3,
-  /*!
-   * \brief The loop variable is bound to a thread in
-   * an environment. In the final stage of lowering,
-   * the loop is simply removed and the loop variable is
-   * mapped to the corresponding context thread.
-   */
-  kThreadBinding = 4
 };
 
 /*!
@@ -481,18 +474,12 @@ class ForNode : public StmtNode {
   ForKind kind;
   /*! \brief The body of the for loop. */
   SeqStmt body;
-  /*!
-   * \brief Only valid when kind == ForKind::kThreadBinding
-   * The hardware thread tag to which this loop variable is bound.
-   */
-  ffi::Optional<ffi::String> thread_binding;
   /*!
    * \brief Additional annotations about the loop.
    *
-   *  These annotations can be used as auxiliary hint
-   *  to future transformations. An annotation should
-   *  not change the control flow semantics of the loop
-   *  and can be ignored in most passes.
+   *  The reserved thread_binding entry is a string specifying the thread tag
+   *  for a parallel loop. It determines execution placement and must be
+   *  preserved by transformations. Other entries provide auxiliary hints.
    */
   ffi::Map<ffi::String, ffi::Any> annotations;
   /*!
@@ -508,11 +495,18 @@ class ForNode : public StmtNode {
         .def_ro("extent", &ForNode::extent)
         .def_ro("kind", &ForNode::kind)
         .def_ro("body", &ForNode::body)
-        .def_ro("thread_binding", &ForNode::thread_binding)
         .def_ro("annotations", &ForNode::annotations)
         .def_ro("step", &ForNode::step);
   }
 
+  /*! \brief Return the explicit thread placement, if present. */
+  ffi::Optional<ffi::String> GetThreadBinding() const {
+    if (auto value = annotations.Get("thread_binding")) {
+      return value->cast<ffi::String>();
+    }
+    return std::nullopt;
+  }
+
   /*! \brief Check it is a loop without nontrivial loop step. */
   bool HasTrivialStep() const;
 
@@ -526,7 +520,6 @@ class ForNode : public StmtNode {
 class For : public Stmt {
  public:
   TVM_DLL For(PrimVar loop_var, PrimExpr min, PrimExpr extent, ForKind kind, 
SeqStmt body,
-              ffi::Optional<ffi::String> thread_binding = std::nullopt,
               ffi::Map<ffi::String, ffi::Any> annotations = {},
               ffi::Optional<PrimExpr> step = std::nullopt, Span span = Span());
 
@@ -741,7 +734,7 @@ TVM_DLL std::ostream& operator<<(std::ostream& os, ForKind 
kind);
 // inline implementations
 inline const char* ForKind2String(ForKind t) {
   switch (t) {
-    case ForKind::kSerial:
+    case ForKind::kDefault:
       return "serial";
     case ForKind::kParallel:
       return "parallel";
@@ -749,8 +742,6 @@ inline const char* ForKind2String(ForKind t) {
       return "vectorized";
     case ForKind::kUnrolled:
       return "unroll";
-    case ForKind::kThreadBinding:
-      return "thread_binding";
   }
   TVM_FFI_THROW(InternalError) << "Unknown ForKind" << t;
   TVM_FFI_UNREACHABLE();
diff --git a/python/tvm/s_tir/dlight/gpu/fallback.py 
b/python/tvm/s_tir/dlight/gpu/fallback.py
index c5dd78b63f..8fd2c87755 100644
--- a/python/tvm/s_tir/dlight/gpu/fallback.py
+++ b/python/tvm/s_tir/dlight/gpu/fallback.py
@@ -42,7 +42,7 @@ def _has_internal_thread_env(stmt: tirx.Stmt) -> bool:
 
     def visit_for(node: tirx.For):
         nonlocal found
-        if node.kind == tirx.ForKind.THREAD_BINDING:
+        if node.annotations.get("thread_binding") is not None:
             found = True
 
     tvm_ffi.structural_walk(
@@ -100,7 +100,8 @@ class Fallback(GPUScheduleRule):
             block = block.block_rv
 
             if any(
-                [sch.get(loop_rv).thread_binding is not None for loop_rv in 
sch.get_loops(block)]
+                sch.get(loop_rv).annotations.get("thread_binding") is not None
+                for loop_rv in sch.get_loops(block)
             ):
                 continue
 
diff --git a/python/tvm/tirx/stmt.py b/python/tvm/tirx/stmt.py
index 48c0e03d79..9026d76411 100644
--- a/python/tvm/tirx/stmt.py
+++ b/python/tvm/tirx/stmt.py
@@ -130,11 +130,10 @@ class ForKind(IntEnum):
     of the loop and need to be considered in all TIR passes.
     """
 
-    SERIAL = 0
-    PARALLEL = 1
+    DEFAULT = 0  # Ordinary loop with sequential iteration semantics.
+    PARALLEL = 1  # Parallel execution, optionally with explicit thread 
placement.
     VECTORIZED = 2
     UNROLLED = 3
-    THREAD_BINDING = 4  # pylint: disable=invalid-name
 
 
 @tvm_ffi.register_object("tirx.For")
@@ -158,16 +157,13 @@ class For(Stmt):
     body : Stmt | Sequence[Stmt]
         The body statement.
 
-    thread_binding: Optional[str]
-        The thread this loop binds to. Only valid
-        if kind is ThreadBinding
+    annotations: Optional[Mapping[str, Object]]
+        Additional loop annotations. For parallel loops, the optional
+        ``thread_binding`` entry is a string naming the bound thread.
+        Parallel loops without this entry use CPU parallel execution.
 
     step : Expr
-        The loop step. Default to none which
-        represent one.
-
-    annotations: Optional[Mapping[str, Object]]
-        Additional annotation hints.
+        The loop step. Defaults to None, which represents one.
 
     span : Optional[Span]
         The location of the stmt in the source code.
@@ -178,7 +174,6 @@ class For(Stmt):
     extent: Expr
     kind: ForKind
     body: "SeqStmt"
-    thread_binding: str | None
     annotations: Mapping[str, Object]
     step: Expr | None
     span: Span | None
@@ -190,7 +185,6 @@ class For(Stmt):
         extent: Expr,
         kind: ForKind,
         body: Stmt | Sequence[Stmt],
-        thread_binding: str | None = None,
         annotations: Mapping[str, Object] | None = None,
         step: Expr | None = None,
         span: Span | None = None,
@@ -202,7 +196,6 @@ class For(Stmt):
             extent,
             kind,
             body,
-            thread_binding,
             annotations,
             step,
             span,
diff --git a/src/relax/transform/split_call_tir_by_pattern.cc 
b/src/relax/transform/split_call_tir_by_pattern.cc
index 7be76c2c46..e7a192a870 100644
--- a/src/relax/transform/split_call_tir_by_pattern.cc
+++ b/src/relax/transform/split_call_tir_by_pattern.cc
@@ -274,8 +274,8 @@ class ForMatcher : public TensorizeComparator {
     if (!DefEqual(op->loop_var, rhs->loop_var)) return false;
     // Only handle the case where the loop start from 0
     if (!IsZero(op->min) || !IsZero(rhs->min)) return false;
-    if (op->thread_binding.has_value() || rhs->thread_binding.has_value()) 
return false;
-    if (op->kind != ForKind::kSerial || op->kind != rhs->kind) return false;
+    if (op->GetThreadBinding().has_value() || 
rhs->GetThreadBinding().has_value()) return false;
+    if (op->kind != ForKind::kDefault || op->kind != rhs->kind) return false;
     if (!op->annotations.empty() || !rhs->annotations.empty()) return false;
     // Match the extents of loops
     if (!Dispatch(op->extent, rhs->extent)) return false;
diff --git a/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc 
b/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc
index 3568e948ca..4c52ec2126 100644
--- a/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc
+++ b/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc
@@ -100,8 +100,9 @@ class LCADetector : public s_tir::StmtExprVisitor {
     const ScopeInfo* parent_scope = ancestor_scopes_.back();
     auto* current_scope = arena_.make<ScopeInfo>(parent_scope, op, n);
 
-    if (op->thread_binding.has_value()) {
-      const runtime::ThreadScope& scope = 
runtime::ThreadScope::Create(op->thread_binding.value());
+    if (op->GetThreadBinding().has_value()) {
+      const runtime::ThreadScope& scope =
+          runtime::ThreadScope::Create(op->GetThreadBinding().value());
       if (scope.rank == 0) {
         blockidx_scopes_.push_back(current_scope);
       }
diff --git a/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc 
b/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc
index c4c1a1c4ed..6499f1b304 100644
--- a/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc
+++ b/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc
@@ -311,8 +311,8 @@ Pass SimplifyForFeatureExtraction(bool 
normalize_thread_bindings = false) {
       if (!normalize_thread_bindings_ && IsZero(loop->extent)) {
         return Evaluate(0);
       }
-      if (normalize_thread_bindings_ && loop->kind == ForKind::kThreadBinding) 
{
-        const auto& tag = loop->thread_binding.value();
+      if (normalize_thread_bindings_ && loop->GetThreadBinding().has_value()) {
+        const auto tag = loop->GetThreadBinding().value();
         if (support::StartsWith(tag, "vthread")) {
           // Virtual axes are independent iterations and isolate hardware 
aliases.
           thread_bindings_.push_back(nullptr);
@@ -322,7 +322,7 @@ Pass SimplifyForFeatureExtraction(bool 
normalize_thread_bindings = false) {
         }
         for (auto it = thread_bindings_.rbegin(); it != 
thread_bindings_.rend() && *it; ++it) {
           const ForNode* outer = *it;
-          if (outer->thread_binding.value() != tag) continue;
+          if (outer->GetThreadBinding().value() != tag) continue;
           // Feature counts describe hardware work, not repeated lexical names
           // for one axis. Normalize only nested aliases in this analysis 
input.
           sym::Analyzer analyzer;
@@ -337,10 +337,11 @@ Pass SimplifyForFeatureExtraction(bool 
normalize_thread_bindings = false) {
                                  .ValueOrUnchanged(loop->annotations);
           Stmt body = Mutate(loop->body, 
inplace_mode).ValueOrUnchanged(loop->body);
           VarRemapSet(loop->loop_var, previous_remap);
+          annotations.erase("thread_binding");
           if (!annotations.empty()) {
             PrimType ty = loop->loop_var.ty();
-            body = For(PrimVar("annotation", ty), IntImm(ty, 0), IntImm(ty, 
1), ForKind::kSerial,
-                       std::move(body), std::nullopt, std::move(annotations), 
std::nullopt);
+            body = For(PrimVar("annotation", ty), IntImm(ty, 0), IntImm(ty, 
1), ForKind::kDefault,
+                       std::move(body), std::move(annotations), std::nullopt);
           }
           return body;
         }
@@ -350,7 +351,7 @@ Pass SimplifyForFeatureExtraction(bool 
normalize_thread_bindings = false) {
         return result;
       }
       if (!normalize_thread_bindings_ && IsZero(loop->min) && 
IsOne(loop->extent) &&
-          loop->kind == ForKind::kSerial && loop->annotations.empty()) {
+          loop->kind == ForKind::kDefault && loop->annotations.empty()) {
         VarRemapSet(loop->loop_var, MakeConst(loop->loop_var.ty(), 0.0));
         return Mutate(loop->body, inplace_mode).ValueOrUnchanged(loop->body);
       } else {
@@ -420,13 +421,13 @@ struct LoopNest {
   ForVec parallel;     // The loops whose ForKind are kParallel
   ForVec vectorize;    // The loops whose ForKind are kVectorized
   ForVec unroll;       // The loops whose ForKind are kUnrolled
-  ForVec blockIdx_x;   // The loops whose ForKind are kThreadBinding to 
blockIdx.x
-  ForVec blockIdx_y;   // The loops whose ForKind are kThreadBinding to 
blockIdx.y
-  ForVec blockIdx_z;   // The loops whose ForKind are kThreadBinding to 
blockIdx.z
-  ForVec threadIdx_x;  // The loops whose ForKind are kThreadBinding to 
threadIdx.x
-  ForVec threadIdx_y;  // The loops whose ForKind are kThreadBinding to 
threadIdx.y
-  ForVec threadIdx_z;  // The loops whose ForKind are kThreadBinding to 
threadIdx.z
-  ForVec vthread;      // The loops whose ForKind are kThreadBinding to 
vthread.*
+  ForVec blockIdx_x;   // The loops explicitly bound to blockIdx.x
+  ForVec blockIdx_y;   // The loops explicitly bound to blockIdx.y
+  ForVec blockIdx_z;   // The loops explicitly bound to blockIdx.z
+  ForVec threadIdx_x;  // The loops explicitly bound to threadIdx.x
+  ForVec threadIdx_y;  // The loops explicitly bound to threadIdx.y
+  ForVec threadIdx_z;  // The loops explicitly bound to threadIdx.z
+  ForVec vthread;      // The loops explicitly bound to vthread.*
 
   /*!
    * \brief Push a new loop into the loop nest
@@ -445,14 +446,14 @@ struct LoopNest {
       this->auto_unroll.push_back(*auto_unroll_attr);
     }
     ForVec* ref_loops = nullptr;
-    if (loop->kind == ForKind::kParallel) {
+    if (loop->kind == ForKind::kParallel && 
!loop->GetThreadBinding().has_value()) {
       ref_loops = &parallel;
     } else if (loop->kind == ForKind::kVectorized) {
       ref_loops = &vectorize;
     } else if (loop->kind == ForKind::kUnrolled) {
       ref_loops = &unroll;
-    } else if (loop->kind == ForKind::kThreadBinding) {
-      std::string thread_tag = loop->thread_binding.value();
+    } else if (loop->GetThreadBinding().has_value()) {
+      std::string thread_tag = loop->GetThreadBinding().value();
       if (thread_tag == "blockIdx.x") {
         ref_loops = &blockIdx_x;
       } else if (thread_tag == "blockIdx.y") {
diff --git 
a/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc 
b/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc
index 74be92bc71..02a3feaa03 100644
--- a/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc
+++ b/src/s_tir/meta_schedule/postproc/rewrite_parallel_vectorize_unroll.cc
@@ -33,7 +33,7 @@ using namespace tvm::tirx;
  * \return Whether the loop has any annotation
  */
 inline bool HasAnnOrBinding(const ForNode* loop) {
-  return loop->kind == ForKind::kThreadBinding || !loop->annotations.empty();
+  return loop->GetThreadBinding().has_value() || !loop->annotations.empty();
 }
 
 /*! \brief The visitor for extracting the stride of a var in a PrimExpr. */
diff --git a/src/s_tir/meta_schedule/schedule/cuda/thread_bind.cc 
b/src/s_tir/meta_schedule/schedule/cuda/thread_bind.cc
index 843cbe5a7e..2ac35735ca 100644
--- a/src/s_tir/meta_schedule/schedule/cuda/thread_bind.cc
+++ b/src/s_tir/meta_schedule/schedule/cuda/thread_bind.cc
@@ -127,7 +127,7 @@ void BindBlockThreadIdx(Schedule sch, SBlockRV block_rv,  //
         i_thread_idx = i;
       }
     }
-    if (loop->kind != ForKind::kSerial) {
+    if (loop->kind != ForKind::kDefault) {
       if (i_multi_child == -1) {
         i_multi_child = i;
       }
diff --git a/src/s_tir/schedule/analysis/analysis.cc 
b/src/s_tir/schedule/analysis/analysis.cc
index 1707a65a54..84fefce476 100644
--- a/src/s_tir/schedule/analysis/analysis.cc
+++ b/src/s_tir/schedule/analysis/analysis.cc
@@ -705,8 +705,8 @@ ffi::Map<Var, Range> LoopDomainOfSRefTreePath(const 
StmtSRef& low_inclusive,
   if (extra_relax_scope.rank != runtime::StorageRank::kGlobal) {
     for (; p; p = p->parent) {
       if (const ForNode* loop = p->StmtAs<ForNode>()) {
-        if (loop->kind == ForKind::kThreadBinding) {
-          const ffi::String& thread_tag = loop->thread_binding.value();
+        if (loop->GetThreadBinding().has_value()) {
+          const ffi::String thread_tag = loop->GetThreadBinding().value();
           if (CanRelaxStorageUnderThread(extra_relax_scope,
                                          
runtime::ThreadScope::Create(thread_tag))) {
             result.Set(loop->loop_var, Range::FromMinExtent(loop->min, 
loop->extent));
diff --git a/src/s_tir/schedule/ir_comparator.cc 
b/src/s_tir/schedule/ir_comparator.cc
index bfacc7b061..4e4f00fef4 100644
--- a/src/s_tir/schedule/ir_comparator.cc
+++ b/src/s_tir/schedule/ir_comparator.cc
@@ -207,19 +207,6 @@ bool TensorizeComparator::Dispatch_(const ForNode* op, 
const Stmt& other) {
     }
     return false;
   }
-  if (op->thread_binding.has_value() != rhs->thread_binding.has_value()) {
-    if (assert_mode_) {
-      std::ostringstream os;
-      os << "ForNode thread_bindings do not match: 
op->thread_binding.has_value()="
-         << op->thread_binding.has_value()
-         << " vs rhs->thread_binding.has_value()=" << 
rhs->thread_binding.has_value();
-      EmitError(os.str());
-    }
-    return false;
-  }
-  if (op->thread_binding.has_value() && op->thread_binding.value() != 
rhs->thread_binding.value()) {
-    return false;
-  }
   if (op->kind != rhs->kind) {
     if (assert_mode_) {
       std::ostringstream os;
diff --git a/src/s_tir/schedule/primitive/annotate.cc 
b/src/s_tir/schedule/primitive/annotate.cc
index 170c308ea9..7cd8eb8db7 100644
--- a/src/s_tir/schedule/primitive/annotate.cc
+++ b/src/s_tir/schedule/primitive/annotate.cc
@@ -30,6 +30,8 @@ void Annotate(ScheduleState self, const StmtSRef& sref, const 
ffi::String& ann_k
   // Extract annotation
   const ffi::Map<ffi::String, ffi::Any>* annotations = nullptr;
   if (const auto* loop = sref->StmtAs<ForNode>()) {
+    TVM_FFI_CHECK(ann_key != "thread_binding", ValueError)
+        << "Use loop scheduling primitives to change thread binding";
     annotations = &loop->annotations;
   } else if (const auto* block = sref->StmtAs<SBlockNode>()) {
     annotations = &block->annotations;
@@ -63,6 +65,8 @@ void Unannotate(ScheduleState self, const StmtSRef& sref, 
const ffi::String& ann
   // Extract annotation
   const ffi::Map<ffi::String, ffi::Any>* annotations = nullptr;
   if (const auto* loop = sref->StmtAs<ForNode>()) {
+    TVM_FFI_CHECK(ann_key != "thread_binding", ValueError)
+        << "Use loop scheduling primitives to change thread binding";
     annotations = &loop->annotations;
   } else if (const auto* block = sref->StmtAs<SBlockNode>()) {
     annotations = &block->annotations;
diff --git a/src/s_tir/schedule/primitive/blockize_tensorize.cc 
b/src/s_tir/schedule/primitive/blockize_tensorize.cc
index cfeaa2ff3b..94e7b6440e 100644
--- a/src/s_tir/schedule/primitive/blockize_tensorize.cc
+++ b/src/s_tir/schedule/primitive/blockize_tensorize.cc
@@ -760,7 +760,7 @@ class BlockizeRewriter : public StmtExprMutator {
   UnchangedOr<Stmt> Mutate_(const ForNode* loop, InplaceMode inplace_mode) 
final {
     if (loop == lca_->stmt) {
       return For(loop->loop_var, loop->min, loop->extent, loop->kind, 
RewriteSeq(loop->body),
-                 loop->thread_binding, loop->annotations, loop->step, 
loop->span);
+                 loop->annotations, loop->step, loop->span);
     }
     return StmtExprMutator::Mutate_(loop, inplace_mode);
   }
diff --git a/src/s_tir/schedule/primitive/cache_index.cc 
b/src/s_tir/schedule/primitive/cache_index.cc
index d925841f85..eef296fc51 100644
--- a/src/s_tir/schedule/primitive/cache_index.cc
+++ b/src/s_tir/schedule/primitive/cache_index.cc
@@ -359,7 +359,7 @@ ffi::Array<SBlock> MakeIndexCacheStage(IndexInfo* info, 
const ffi::String& stora
       body = For(/*loop_var=*/loop_vars[i - 1],
                  /*min=*/0,
                  /*extent=*/info->range_map.at(iter_vars[i - 1])->extent,
-                 /*kind=*/ForKind::kSerial,
+                 /*kind=*/ForKind::kDefault,
                  /*body=*/body);
     }
     bodies.push_back(body);
diff --git a/src/s_tir/schedule/primitive/cache_read_write.cc 
b/src/s_tir/schedule/primitive/cache_read_write.cc
index 70b2eb9c9b..7598bcef06 100644
--- a/src/s_tir/schedule/primitive/cache_read_write.cc
+++ b/src/s_tir/schedule/primitive/cache_read_write.cc
@@ -236,7 +236,7 @@ SBlock MakeReindexCacheStage(const TensorRegion& 
cache_region, ReindexCacheStage
     body = For(/*loop_var=*/loop_vars[i - 1],
                /*min=*/info->loop_ranges[i - 1]->min,
                /*extent=*/info->loop_ranges[i - 1]->extent,
-               /*kind=*/ForKind::kSerial,
+               /*kind=*/ForKind::kDefault,
                /*body=*/body);
   }
   info->cache_stage = std::move(body);
@@ -339,7 +339,7 @@ SBlock MakeCacheStage(const TensorRegion& cache_region, 
CacheStageInfo* info,
     body = For(/*loop_var=*/loop_vars[i - 1],
                /*min=*/0,
                /*extent=*/cache_region->region[i - 1]->extent,
-               /*kind=*/ForKind::kSerial,
+               /*kind=*/ForKind::kDefault,
                /*body=*/body);
   }
   info->cache_stage = std::move(body);
@@ -457,7 +457,7 @@ SBlock MakeReIndexStage(const SBlock& block, 
CacheStageInfo* info,
     body = For(/*loop_var=*/loop_vars[i],
                /*min=*/new_block_iters[i]->dom->min,
                /*extent=*/new_block_iters[i]->dom->extent,
-               /*kind=*/ForKind::kSerial,
+               /*kind=*/ForKind::kDefault,
                /*body=*/std::move(body));
   }
   // Update cache info, which will be used in the later rewriting.
diff --git a/src/s_tir/schedule/primitive/compute_at.cc 
b/src/s_tir/schedule/primitive/compute_at.cc
index 8fac5f6301..d5e41006e6 100644
--- a/src/s_tir/schedule/primitive/compute_at.cc
+++ b/src/s_tir/schedule/primitive/compute_at.cc
@@ -311,7 +311,7 @@ class ScopeReconstructor : public StmtExprMutator {
       new_subtree = For(/*loop_var=*/loop_var.as_or_throw<PrimVar>(),
                         /*min=*/IntImm::Int32(0),
                         /*extent=*/loop_extent,
-                        /*ForKind=*/ForKind::kSerial,
+                        /*ForKind=*/ForKind::kDefault,
                         /*body=*/std::move(new_subtree));
     }
     ffi::Array<Stmt> subtrees = AsArray(loop_->body);
diff --git a/src/s_tir/schedule/primitive/compute_inline.cc 
b/src/s_tir/schedule/primitive/compute_inline.cc
index f3a2b2d4de..369920ef58 100644
--- a/src/s_tir/schedule/primitive/compute_inline.cc
+++ b/src/s_tir/schedule/primitive/compute_inline.cc
@@ -1812,7 +1812,7 @@ class SingleBlockFusionReplacer : public StmtExprMutator {
     }
 
     return For(loop->loop_var, loop->min, loop->extent, loop->kind, 
mutated_body,
-               loop->thread_binding, loop->annotations);
+               loop->annotations);
   }
 
   UnchangedOr<Stmt> Mutate_(const SBlockRealizeNode* realize, InplaceMode 
inplace_mode) final {
diff --git a/src/s_tir/schedule/primitive/decompose_padding.cc 
b/src/s_tir/schedule/primitive/decompose_padding.cc
index 348652efd7..61b85fba06 100644
--- a/src/s_tir/schedule/primitive/decompose_padding.cc
+++ b/src/s_tir/schedule/primitive/decompose_padding.cc
@@ -278,7 +278,7 @@ static std::pair<Stmt, SBlockRealize> 
CreateConstBlock(const SBlockRealizeNode*
   for (size_t i = 0; i < new_loop_vars.size(); ++i) {
     For loop = loops[i];
     nest_stmt_root = For(new_loop_vars[i].as_or_throw<PrimVar>(), loop->min, 
loop->extent,
-                         ForKind::kSerial, nest_stmt_root);
+                         ForKind::kDefault, nest_stmt_root);
   }
 
   return {nest_stmt_root, new_realize};
@@ -374,8 +374,8 @@ static std::pair<Stmt, SBlockRealize> 
CreateInBoundBlock(const SBlockRealizeNode
     auto it = new_loop_ranges.find(loop->loop_var);
     PrimExpr min = it == new_loop_ranges.end() ? loop->min : (*it).second->min;
     PrimExpr extent = it == new_loop_ranges.end() ? loop->extent : 
(*it).second->extent;
-    nest_stmt_root = For(loop->loop_var, min, extent, loop->kind, 
nest_stmt_root,
-                         loop->thread_binding, loop->annotations, loop->step, 
loop->span);
+    nest_stmt_root = For(loop->loop_var, min, extent, loop->kind, 
nest_stmt_root, loop->annotations,
+                         loop->step, loop->span);
     if (loop.same_as(highest_pos_inclusive)) {
       break;
     }
diff --git a/src/s_tir/schedule/primitive/for_kind.cc 
b/src/s_tir/schedule/primitive/for_kind.cc
index a5d35fc4f0..a18bdd7ae4 100644
--- a/src/s_tir/schedule/primitive/for_kind.cc
+++ b/src/s_tir/schedule/primitive/for_kind.cc
@@ -28,9 +28,11 @@ using namespace tvm::tirx;
 
 class WrongBlockIterTypeError : public ScheduleErrorContextObj {
  public:
-  explicit WrongBlockIterTypeError(IRModule mod, ForKind for_kind, Var 
loop_var, SBlock block)
+  explicit WrongBlockIterTypeError(IRModule mod, ForKind for_kind, Var 
loop_var, SBlock block,
+                                   bool is_binding)
       : mod_(std::move(mod)), loop_var_(std::move(loop_var)), 
block_(std::move(block)) {
-    op_str_ = for_kind == ForKind::kParallel
+    op_str_ = is_binding ? "bind"
+              : for_kind == ForKind::kParallel
                   ? "parallel"
                   : (for_kind == ForKind::kVectorized ? "vectorize" : "bind");
   }
@@ -73,7 +75,7 @@ class WrongBlockIterTypeError : public 
ScheduleErrorContextObj {
  *   - the block iter is a reduction block iter, and the input `thread_tag` 
starts with "threadIdx"
  *   in case of cross-thread reduction.
  * \param self The schedule state
- * \param for_kind The desired ForKind (only `kParallel`, `kVectorized` and 
`kThreadBinding` are
+ * \param for_kind The desired ForKind (only `kParallel` and `kVectorized` are
  * allowed)
  * \param loop_var The loop variable of the loop to be checked
  * \param block_realize The block-realize of the block to be checked
@@ -112,7 +114,8 @@ void CheckLoopParallelizableInBlock(const ScheduleState& 
self, ForKind for_kind,
     IterVarType iter_type = iter_var->iter_type;
     if (!(iter_type == kDataPar ||
           (iter_type == kCommReduce && thread_scope.rank == 1 && 
thread_scope.dim_index != -1))) {
-      throw MakeScheduleError<WrongBlockIterTypeError>(self->mod, for_kind, 
loop_var, block);
+      throw MakeScheduleError<WrongBlockIterTypeError>(self->mod, for_kind, 
loop_var, block,
+                                                       thread_scope.rank != 
-1);
     }
   }
 }
@@ -122,7 +125,7 @@ void CheckLoopParallelizableInBlock(const ScheduleState& 
self, ForKind for_kind,
  * parallelized/vectorized/bound with regard to the block
  * \param self The schedule state
  * \param loop The loop to be parallelized/vectorized/bound
- * \param for_kind The desired ForKind (only `kParallel`, `kVectorized` and 
`kThreadBinding` are
+ * \param for_kind The desired ForKind (only `kParallel` and `kVectorized` are
  * allowed)
  * \param thread_scope The thread scope of the thread axis to be bound, which 
is an invalid value if
  * the operation is not "bind"
@@ -145,10 +148,10 @@ void CheckParallelizability(const ScheduleState& self, 
const For& loop, ForKind
  * \brief The implementation of parallelizing/vectorizing/binding a given loop
  * \param self The schedule state
  * \param loop_sref The sref of the loop to be parallelized/vectorized/bound
- * \param for_kind The type of the operation (only `kParallel`, `kVectorized` 
and `kThreadBinding`
+ * \param for_kind The type of the operation (only `kParallel` and 
`kVectorized`
  * are allowed)
  * \param thread_axis The thread axis that the input loop is bound to, which 
is defined only when
- * `for_kind` is `kThreadBinding`
+ * binding a parallel loop to an explicit thread
  */
 void ParallelizeComputation(const ScheduleState& self, const StmtSRef& 
loop_sref, ForKind for_kind,
                             ffi::Optional<ffi::String> thread_axis) {
@@ -178,9 +181,9 @@ void ParallelizeComputation(const ScheduleState& self, 
const StmtSRef& loop_sref
   ffi::ObjectPtr<ForNode> new_loop = ffi::make_object<ForNode>(*loop);
   new_loop->kind = for_kind;
   if (thread_axis.has_value()) {
-    new_loop->thread_binding = thread_axis.value();
+    new_loop->annotations.Set("thread_binding", thread_axis.value());
   } else {
-    new_loop->thread_binding = std::nullopt;
+    new_loop->annotations.erase("thread_binding");
   }
   self->Replace(loop_sref, For(new_loop), {});
 }
@@ -194,14 +197,14 @@ void Vectorize(ScheduleState self, const StmtSRef& 
loop_sref) {
 }
 
 void Bind(ScheduleState self, const StmtSRef& loop_sref, const ffi::String& 
thread_axis) {
-  ParallelizeComputation(self, loop_sref, ForKind::kThreadBinding, 
thread_axis);
+  ParallelizeComputation(self, loop_sref, ForKind::kParallel, thread_axis);
 }
 
 void Unroll(ScheduleState self, const StmtSRef& loop_sref) {
   const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
   ffi::ObjectPtr<ForNode> new_loop = ffi::make_object<ForNode>(*loop);
   new_loop->kind = ForKind::kUnrolled;
-  new_loop->thread_binding = std::nullopt;
+  new_loop->annotations.erase("thread_binding");
   self->Replace(loop_sref, For(new_loop), {});
 }
 
diff --git a/src/s_tir/schedule/primitive/layout_transformation.cc 
b/src/s_tir/schedule/primitive/layout_transformation.cc
index 4e9754bdce..0dc6232c73 100644
--- a/src/s_tir/schedule/primitive/layout_transformation.cc
+++ b/src/s_tir/schedule/primitive/layout_transformation.cc
@@ -204,7 +204,7 @@ class TransformLayoutPlanner : public StmtExprVisitor {
                              .as_or_throw<PrimExpr>();
         bool is_loop_over_axis = index.same_as(loop->loop_var) && 
IsConstInt(loop->min, 0) &&
                                  prim::ExprDeepEqual()(loop->extent, 
buffer_dim) &&
-                                 loop->kind == ForKind::kSerial;
+                                 loop->kind == ForKind::kDefault;
         if (!is_loop_over_axis) {
           return false;
         }
@@ -540,7 +540,7 @@ class TransformLayoutPlanner : public StmtExprVisitor {
       size_t i = (inverse->initial_indices.size() - 1) - rev_i;
       Var loop_var = inverse->initial_indices[i];
       PrimExpr extent = new_buffer->shape[i];
-      stmt = For(loop_var.as_or_throw<PrimVar>(), 0, extent, ForKind::kSerial, 
stmt);
+      stmt = For(loop_var.as_or_throw<PrimVar>(), 0, extent, 
ForKind::kDefault, stmt);
     }
     return ProloguePlan{stmt};
   }
@@ -574,7 +574,7 @@ class TransformLayoutPlanner : public StmtExprVisitor {
         size_t i = (inverse->initial_indices.size() - 1) - rev_i;
         Var loop_var = inverse->initial_indices[i];
         PrimExpr extent = new_buffer->shape[i];
-        stmt = For(loop_var.as_or_throw<PrimVar>(), 0, extent, 
ForKind::kSerial, stmt);
+        stmt = For(loop_var.as_or_throw<PrimVar>(), 0, extent, 
ForKind::kDefault, stmt);
       }
 
       return stmt;
@@ -634,7 +634,7 @@ class TransformLayoutPlanner : public StmtExprVisitor {
       size_t i = (inverse->initial_indices.size() - 1) - rev_i;
       Var loop_var = inverse->initial_indices[i];
       PrimExpr extent = new_buffer->shape[i];
-      stmt = For(loop_var.as_or_throw<PrimVar>(), 0, extent, ForKind::kSerial, 
stmt);
+      stmt = For(loop_var.as_or_throw<PrimVar>(), 0, extent, 
ForKind::kDefault, stmt);
     }
 
     const auto& info = write_info_.back();
@@ -1572,7 +1572,7 @@ void TransformBlockLayout(ScheduleState self, const 
StmtSRef& block_sref,
   Stmt body = ffi::GetRef<Stmt>(new_block_realize);
   for (int i = static_cast<int>(new_loop_vars.size()) - 1; i >= 0; --i) {
     body = For(new_loop_vars[i].as_or_throw<PrimVar>(), 0, 
new_block_iter_range[i],
-               ForKind::kSerial, std::move(body));
+               ForKind::kDefault, std::move(body));
   }
 
   // Step 6: Do the actual replacement
diff --git a/src/s_tir/schedule/primitive/loop_transformation.cc 
b/src/s_tir/schedule/primitive/loop_transformation.cc
index 40b0c1f5f0..22a553d1b8 100644
--- a/src/s_tir/schedule/primitive/loop_transformation.cc
+++ b/src/s_tir/schedule/primitive/loop_transformation.cc
@@ -437,7 +437,7 @@ ffi::Array<StmtSRef> Split(ScheduleState self, const 
StmtSRef& loop_sref,
   // order with before.
   // Step 1. Check correctness
   const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
-  if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
+  if (!loop->annotations.empty()) {
     throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod, 
ffi::GetRef<For>(loop));
   }
   // Currently, loops not starting with 0 are not supported
@@ -480,7 +480,7 @@ ffi::Array<StmtSRef> Split(ScheduleState self, const 
StmtSRef& loop_sref,
   // Step 4. Generate nested loops to replace the original loop and simplify 
the binding
   for (int i = n - 1; i >= 0; i--) {
     new_stmt =
-        For(new_loop_vars[i].as_or_throw<PrimVar>(), 0, factors[i], 
ForKind::kSerial, new_stmt);
+        For(new_loop_vars[i].as_or_throw<PrimVar>(), 0, factors[i], 
ForKind::kDefault, new_stmt);
   }
   new_stmt = 
IterMapSimplifyBlockBinding::SimplifyBindings(std::move(new_stmt), 
GetLoops(loop_sref),
                                                            
opaque_block_reuse.CopyOnWrite(),
@@ -716,7 +716,7 @@ const ffi::String get_sblock_name(SeqStmt loop_body) {
 ffi::Array<StmtSRef> LoopPartition(ScheduleState self, const StmtSRef& 
loop_sref,
                                    const ffi::Array<PrimExpr>& factors, bool 
preserve_unit_iters) {
   const ForNode* loop = TVM_SREF_TO_FOR(loop_sref);
-  if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
+  if (!loop->annotations.empty()) {
     throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod, 
ffi::GetRef<For>(loop));
   }
 
@@ -759,7 +759,7 @@ ffi::Array<StmtSRef> LoopPartition(ScheduleState self, 
const StmtSRef& loop_sref
                     .ValueOrUnchanged(std::move(loop_body));
     // Create new for loop with appropriate range
     auto for_node = For(new_loop_var.as_or_throw<PrimVar>(), min_value, 
extent_value - min_value,
-                        ForKind::kSerial, loop_body);
+                        ForKind::kDefault, loop_body);
 
     const auto& partition_block_name = block_name + std::to_string(i) + 
"_partition";
     // Create partition_block for the partitioned for loop
@@ -838,13 +838,13 @@ class LoopReconstructor : public StmtExprMutator {
       this->need_remove_loop_.push_back(loops_[i].back());
     }
     auto new_loop = For(new_loop_vars[0].as_or_throw<PrimVar>(), 
IntImm::Int32(0),
-                        new_loop_extents[0], ForKind::kSerial, 
SeqStmt(std::move(new_stmts)));
+                        new_loop_extents[0], ForKind::kDefault, 
SeqStmt(std::move(new_stmts)));
     this->new_inner_loop_ = new_loop;
     for (size_t i = 1; i < new_loop_vars.size(); ++i) {
       const Var& loop_var = new_loop_vars[i];
       const PrimExpr& loop_extent = new_loop_extents[i];
       new_loop = For(loop_var.as_or_throw<PrimVar>(), IntImm::Int32(0), 
loop_extent,
-                     ForKind::kSerial, new_loop);
+                     ForKind::kDefault, new_loop);
     }
     this->new_outer_loop_ = new_loop;
   }
@@ -899,7 +899,7 @@ StmtSRef Merge(ScheduleState self, const 
ffi::Array<StmtSRef>& loop_srefs) {
     std::vector<For> nest_loop_i_loops;
     for (auto p = sref.get(); p != lca.get(); p = p->parent) {
       if (auto loop = p->StmtAs<ForNode>()) {
-        if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
+        if (!loop->annotations.empty()) {
           throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod,
                                                                      
ffi::GetRef<For>(loop));
         }
@@ -967,7 +967,7 @@ StmtSRef Fuse(ScheduleState self, const 
ffi::Array<StmtSRef>& loop_srefs,
   // Step 1. check correctness
   for (const StmtSRef& sref : loop_srefs) {
     const ForNode* loop = TVM_SREF_TO_FOR(sref);
-    if (!loop->annotations.empty() || loop->thread_binding.has_value()) {
+    if (!loop->annotations.empty()) {
       throw MakeScheduleError<HasAnnotationOrThreadBindingError>(self->mod, 
ffi::GetRef<For>(loop));
     }
     if (outer_loop_sref.defined()) {
@@ -1036,7 +1036,7 @@ StmtSRef Fuse(ScheduleState self, const 
ffi::Array<StmtSRef>& loop_srefs,
     fused_extent *= loops[i]->extent;
   }
   fused_extent = analyzer->Simplify(fused_extent);
-  new_stmt = For(fused_var.as_or_throw<PrimVar>(), 0, fused_extent, 
ForKind::kSerial, new_stmt);
+  new_stmt = For(fused_var.as_or_throw<PrimVar>(), 0, fused_extent, 
ForKind::kDefault, new_stmt);
   new_stmt = IterMapSimplifyBlockBinding::SimplifyBindings(
       std::move(new_stmt), GetLoops(loop_srefs[0]), 
opaque_block_reuse.CopyOnWrite(),
       preserve_unit_iters);
@@ -1225,8 +1225,8 @@ void Reorder(ScheduleState self, const 
ffi::Array<StmtSRef>& ordered_loop_srefs)
 
 StmtSRef AddUnitLoop(ScheduleState self, StmtSRef sref) {
   if (sref->stmt->IsInstance<ForNode>()) {
-    For new_loop =
-        For(PrimVar("u", PrimType::Int(32)), 0, 1, ForKind::kSerial, 
ffi::GetRef<Stmt>(sref->stmt));
+    For new_loop = For(PrimVar("u", PrimType::Int(32)), 0, 1, 
ForKind::kDefault,
+                       ffi::GetRef<Stmt>(sref->stmt));
     self->Replace(sref, new_loop, {});
     return self->stmt2ref.at(new_loop.get());
   }
@@ -1243,7 +1243,7 @@ StmtSRef AddUnitLoop(ScheduleState self, StmtSRef sref) {
 
     UnchangedOr<Stmt> Mutate_(const SBlockRealizeNode* realize, InplaceMode 
inplace_mode) final {
       if (realize->block.get() == src_block_) {
-        new_loop_ = For(PrimVar("u", PrimType::Int(32)), 0, 1, 
ForKind::kSerial,
+        new_loop_ = For(PrimVar("u", PrimType::Int(32)), 0, 1, 
ForKind::kDefault,
                         ffi::GetRef<SBlockRealize>(realize));
         return new_loop_.value();
       }
diff --git a/src/s_tir/schedule/primitive/pad_einsum.cc 
b/src/s_tir/schedule/primitive/pad_einsum.cc
index 912f78d141..05e6b682e5 100644
--- a/src/s_tir/schedule/primitive/pad_einsum.cc
+++ b/src/s_tir/schedule/primitive/pad_einsum.cc
@@ -204,7 +204,7 @@ struct BufferPadding {
     body = SBlockRealize(prim_loop_vars, IntImm::Bool(true), new_block);
     for (int i = ndim - 1; i >= 0; --i) {
       body = For(loop_vars[i].as_or_throw<PrimVar>(), loop_doms[i]->min, 
loop_doms[i]->extent,
-                 ForKind::kSerial, std::move(body));
+                 ForKind::kDefault, std::move(body));
     }
     return body;
   }
diff --git a/src/s_tir/schedule/primitive/read_write_at.cc 
b/src/s_tir/schedule/primitive/read_write_at.cc
index e14179881b..b9a46e3354 100644
--- a/src/s_tir/schedule/primitive/read_write_at.cc
+++ b/src/s_tir/schedule/primitive/read_write_at.cc
@@ -337,7 +337,7 @@ struct ReadWriteAtImpl {
         TensorStore(copy_to, /*value=*/MakeTensorLoad(copy_from, indices), 
/*indices=*/indices);
     for (int i = n - 1; i >= 0; --i) {
       stmt = For(loop_vars[i].as_or_throw<PrimVar>(), IntImm::Int32(0), 
domain[i]->extent,
-                 ForKind::kSerial, stmt);
+                 ForKind::kDefault, stmt);
     }
     return SBlockRealize(
         /*values=*/iter_values,
diff --git a/src/s_tir/schedule/primitive/reduction.cc 
b/src/s_tir/schedule/primitive/reduction.cc
index a2f595d6df..cf2c5f36e2 100644
--- a/src/s_tir/schedule/primitive/reduction.cc
+++ b/src/s_tir/schedule/primitive/reduction.cc
@@ -311,10 +311,8 @@ StmtSRef DecomposeReduction(ScheduleState self, const 
StmtSRef& block_sref,
     Var old_loop_var = old_loop->loop_var;
     PrimVar new_loop_var = old_loop->loop_var.CopyWithSuffix("_init");
     loop_var_map.insert_or_assign(old_loop_var, new_loop_var);
-    ffi::Optional<ffi::String> opt_thread_binding = old_loop->thread_binding;
     auto new_loop = old_loop.CopyOnWrite();
     new_loop->loop_var = new_loop_var;
-    new_loop->thread_binding = opt_thread_binding;
     new_loop->body = body;
     body = ffi::GetRef<For>(new_loop);
   }
@@ -1377,7 +1375,7 @@ StmtSRef RFactor(ScheduleState self, const StmtSRef& 
rf_loop_sref, int factor_ax
     CheckReductionBlock(self, block_sref, scope_root);
   }
   const ForNode* rf_loop = TVM_SREF_TO_FOR(rf_loop_sref);
-  if (rf_loop->kind != ForKind::kSerial) {
+  if (rf_loop->kind != ForKind::kDefault) {
     throw MakeScheduleError<NotSerialLoopKindError>(self->mod, 
ffi::GetRef<For>(rf_loop));
   }
 
diff --git a/src/s_tir/schedule/utils.h b/src/s_tir/schedule/utils.h
index 773f22cc6e..fbec8ac634 100644
--- a/src/s_tir/schedule/utils.h
+++ b/src/s_tir/schedule/utils.h
@@ -166,8 +166,8 @@ inline IterVar IterVarFromLoop(const For& loop, ffi::String 
name, IterVarType it
  * \return The thread scope bound to the loop
  */
 inline runtime::ThreadScope GetThreadScope(const ForNode* loop) {
-  if (loop->kind == ForKind::kThreadBinding) {
-    return runtime::ThreadScope::Create(loop->thread_binding.value());
+  if (loop->GetThreadBinding().has_value()) {
+    return runtime::ThreadScope::Create(loop->GetThreadBinding().value());
   }
   return runtime::ThreadScope{-1, -1};
 }
diff --git a/src/s_tir/transform/annotate_irregular_loop.cc 
b/src/s_tir/transform/annotate_irregular_loop.cc
index e0d0a0b6a2..7161f0a0cf 100644
--- a/src/s_tir/transform/annotate_irregular_loop.cc
+++ b/src/s_tir/transform/annotate_irregular_loop.cc
@@ -55,7 +55,7 @@ class IrregularLoopAnnotator : public StmtExprMutator {
                   .ValueOrUnchanged(ffi::GetRef<Stmt>(op))
                   .as_or_throw<For>();
     if (has_jump_) {
-      TVM_FFI_ICHECK(op->kind == ForKind::kSerial)
+      TVM_FFI_ICHECK(op->kind == ForKind::kDefault)
           << "Loop kind " << op->kind << " is invalid for irregular loop " << 
op->loop_var;
       for (const char* key :
            {tirx::attr::auto_unroll_max_step, tirx::attr::unroll_explicit,
diff --git a/src/s_tir/transform/compact_buffer_region.cc 
b/src/s_tir/transform/compact_buffer_region.cc
index d862066cf1..1112ba1966 100644
--- a/src/s_tir/transform/compact_buffer_region.cc
+++ b/src/s_tir/transform/compact_buffer_region.cc
@@ -184,10 +184,10 @@ class BufferAccessRegionCollector : public 
StmtExprVisitor {
 
   ffi::Optional<VisitInterrupt> Visit_(const ForNode* op) final {
     Range loop_range = Range::FromMinExtent(op->min, op->extent);
-    IterVar iter =
-        op->kind == ForKind::kThreadBinding
-            ? IterVar(Range(), op->loop_var, IterVarType::kThreadIndex, 
op->thread_binding.value())
-            : IterVar(Range(), op->loop_var, IterVarType::kDataPar);
+    IterVar iter = op->GetThreadBinding().has_value()
+                       ? IterVar(Range(), op->loop_var, 
IterVarType::kThreadIndex,
+                                 op->GetThreadBinding().value())
+                       : IterVar(Range(), op->loop_var, IterVarType::kDataPar);
     ancestor_iters_.push_back(iter);
     dom_analyzer_->Bind(op->loop_var, loop_range);
     dom_map_.emplace(op->loop_var.get(), sym::IntSet::FromRange(loop_range));
diff --git a/src/s_tir/transform/default_gpu_schedule.cc 
b/src/s_tir/transform/default_gpu_schedule.cc
index a430675125..efd769cc25 100644
--- a/src/s_tir/transform/default_gpu_schedule.cc
+++ b/src/s_tir/transform/default_gpu_schedule.cc
@@ -40,7 +40,7 @@ void ThreadBind(s_tir::Schedule sch, const s_tir::SBlockRV& 
block, int64_t max_t
   ffi::Array<s_tir::LoopRV> loops = sch->GetLoops(block);
   for (const s_tir::LoopRV& loop : loops) {
     // skip block if already scheduled
-    if (sch->Get(loop)->thread_binding.has_value()) {
+    if (sch->Get(loop)->GetThreadBinding().has_value()) {
       return;
     }
   }
@@ -147,7 +147,7 @@ tirx::Function WrapBareSBlockBody(const tirx::Function& 
func) {
       
/*iter_values=*/ffi::Array<tvm::PrimExpr>{loop_var.as_or_throw<tvm::PrimExpr>()},
       /*predicate=*/realize->predicate, inner_block);
   tirx::Stmt for_stmt =
-      tirx::For(loop_var.as_or_throw<PrimVar>(), zero, one, 
tirx::ForKind::kSerial, inner_realize);
+      tirx::For(loop_var.as_or_throw<PrimVar>(), zero, one, 
tirx::ForKind::kDefault, inner_realize);
   s_tir::SBlock root_block(/*iter_vars=*/ffi::Array<s_tir::IterVar>{},
                            /*reads=*/ffi::Array<tvm::TensorRegion>{},
                            /*writes=*/ffi::Array<tvm::TensorRegion>{},
diff --git a/src/s_tir/transform/inject_software_pipeline.cc 
b/src/s_tir/transform/inject_software_pipeline.cc
index ac3107478e..2c7e77ae72 100644
--- a/src/s_tir/transform/inject_software_pipeline.cc
+++ b/src/s_tir/transform/inject_software_pipeline.cc
@@ -1010,7 +1010,7 @@ class PipelineRewriter : public StmtExprMutator {
     if (!is_unit_loop) {
       new_loop = For(new_loop_var.as_or_throw<PrimVar>(), pipeline_loop_->min, 
extent,
                      unroll_loop ? ForKind::kUnrolled : pipeline_loop_->kind, 
std::move(new_loop),
-                     std::nullopt, preserved_annotations_, std::nullopt);
+                     preserved_annotations_, std::nullopt);
     }
 
     // Update producer heads in the global async states.
diff --git a/src/s_tir/transform/inject_virtual_thread.cc 
b/src/s_tir/transform/inject_virtual_thread.cc
index 23c4a16c78..4357be5ecc 100644
--- a/src/s_tir/transform/inject_virtual_thread.cc
+++ b/src/s_tir/transform/inject_virtual_thread.cc
@@ -678,7 +678,7 @@ class VTInjector : public s_tir::IRMutatorWithAnalyzer {
       stmt = ffi::StructuralMap<ffi::WalkOrder::kPreOrder>(stmt, 
f_substitute).as_or_throw<Stmt>();
       PrimType idx_dtype = idx->ty.as_or_throw<PrimType>();
       return For(idx.as_or_throw<PrimVar>(), IntImm(idx_dtype, 0),
-                 prim::MakeConst(idx_dtype, num_threads_), ForKind::kSerial, 
stmt);
+                 prim::MakeConst(idx_dtype, num_threads_), ForKind::kDefault, 
stmt);
     }
   }
 
@@ -737,7 +737,7 @@ class VirtualThreadInjector : public 
s_tir::IRMutatorWithAnalyzer {
       if (vs->has_opaque_region_) {
         // Keep unknown operations and their result definitions inside one
         // lexical loop; they have no sharing or distribution semantics.
-        return For(var, IntImm(var.ty(), 0), 
op->args[1].as_or_throw<PrimExpr>(), ForKind::kSerial,
+        return For(var, IntImm(var.ty(), 0), 
op->args[1].as_or_throw<PrimExpr>(), ForKind::kDefault,
                    op->body);
       }
       auto injector =
diff --git a/src/s_tir/transform/lift_thread_binding.cc 
b/src/s_tir/transform/lift_thread_binding.cc
index 476108beba..f4ec631bbd 100644
--- a/src/s_tir/transform/lift_thread_binding.cc
+++ b/src/s_tir/transform/lift_thread_binding.cc
@@ -48,7 +48,7 @@ FindLoopLCA(const Stmt& root) {
     ffi::Optional<VisitInterrupt> Visit_(const ForNode* op) final {
       stack.push_back(ffi::GetRef<Stmt>(op));
       TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(StmtExprVisitor::Visit_(op));
-      if (op->kind == ForKind::kThreadBinding) {
+      if (op->GetThreadBinding().has_value()) {
         UpdateLCA(op);
       }
       stack.pop_back();
@@ -56,7 +56,7 @@ FindLoopLCA(const Stmt& root) {
     }
 
     void UpdateLCA(const ForNode* loop) {
-      std::string thread_tag = loop->thread_binding.value();
+      std::string thread_tag = loop->GetThreadBinding().value();
       {
         ffi::Map<ffi::String, ffi::Any>* tgt = &annotations[thread_tag];
         for (const auto& kv : loop->annotations) {
@@ -134,7 +134,7 @@ class ThreadBindingLifter : public StmtExprMutator {
   UnchangedOr<Stmt> Mutate_(const ForNode* _op, InplaceMode inplace_mode) 
final {
     For op = ffi::GetRef<For>(_op);
     bool is_kernel_root = false;
-    if (op->kind == ForKind::kThreadBinding) {
+    if (op->GetThreadBinding().has_value()) {
       if (iter_lca.empty()) {
         is_kernel_root = true;
         SetKernelRoot(_op);
@@ -146,15 +146,16 @@ class ThreadBindingLifter : public StmtExprMutator {
     Stmt body = std::move(new_op.CopyOnWrite()->body);
     if (auto it = iter_lca.find(op); it != iter_lca.end()) {
       for (const auto& [iter_var, annotation] : it->second) {
-        body =
-            For(iter_var->var, iter_var->dom->min, iter_var->dom->extent, 
ForKind::kThreadBinding,
-                std::move(body), iter_var->thread_tag, annotation, 
std::nullopt);
+        auto annotations = annotation;
+        annotations.Set("thread_binding", iter_var->thread_tag);
+        body = For(iter_var->var, iter_var->dom->min, iter_var->dom->extent, 
ForKind::kParallel,
+                   std::move(body), std::move(annotations), std::nullopt);
       }
     }
     if (is_kernel_root) {
       iter_lca.clear();
     }
-    if (op->kind == ForKind::kThreadBinding) {
+    if (op->GetThreadBinding().has_value()) {
       return body;
     } else {
       new_op.CopyOnWrite()->body = std::move(body);
diff --git a/src/s_tir/transform/loop_partition.cc 
b/src/s_tir/transform/loop_partition.cc
index 26b8536f79..fe7c2bd0be 100644
--- a/src/s_tir/transform/loop_partition.cc
+++ b/src/s_tir/transform/loop_partition.cc
@@ -109,8 +109,8 @@ using ExpressionSet = std::unordered_set<PrimExpr, 
ffi::ObjectPtrHash, ffi::Obje
 
 // Virtual threads are lowered separately and are not hardware partition 
scopes.
 static bool IsVirtualThread(const ForNode* op) {
-  if (op->kind != ForKind::kThreadBinding) return false;
-  const auto& tag = op->thread_binding.value();
+  if (!op->GetThreadBinding().has_value()) return false;
+  const auto tag = op->GetThreadBinding().value();
   return std::string(tag).rfind("vthread", 0) == 0;
 }
 
@@ -165,8 +165,8 @@ class CandidateSelector final : public StmtExprVisitor {
       candidates.insert(ffi::GetRef<Stmt>(op));
       return StmtExprVisitor::Visit_(op);
     }
-    if (op->kind == ForKind::kThreadBinding &&
-        runtime::ThreadScope::Create(op->thread_binding.value()).rank != 0) {
+    if (op->GetThreadBinding().has_value() &&
+        runtime::ThreadScope::Create(op->GetThreadBinding().value()).rank != 
0) {
       return StmtExprVisitor::Visit_(op);
     }
     // partition const loop when sets partition_const_loop_
@@ -451,7 +451,7 @@ class ThreadPartitionInserter : public StmtExprMutator {
       : ps_(ps), cond_(cond), innermost_thread_scope_(false) {}
 
   UnchangedOr<Stmt> Mutate_(const ForNode* op, InplaceMode inplace_mode) final 
{
-    if (op->kind != ForKind::kThreadBinding || IsVirtualThread(op)) {
+    if (!op->GetThreadBinding().has_value() || IsVirtualThread(op)) {
       return StmtExprMutator::Mutate_(op, inplace_mode);
     }
     innermost_thread_scope_ = true;
@@ -521,7 +521,7 @@ class LoopPartitioner : public StmtExprMutator {
 
   UnchangedOr<Stmt> Mutate_(const ForNode* op, InplaceMode inplace_mode) final 
{
     if (IsVirtualThread(op)) return StmtExprMutator::Mutate_(op, inplace_mode);
-    bool thread_scope = op->kind == ForKind::kThreadBinding;
+    bool thread_scope = op->GetThreadBinding().has_value();
     analyzer_->Bind(op->loop_var, Range::FromMinExtent(op->min, op->extent), 
true);
     auto fs = ffi::GetRef<Stmt>(op);
     if (selector->candidates.count(fs)) {
@@ -531,7 +531,8 @@ class LoopPartitioner : public StmtExprMutator {
     }
 
     // Relax threadIdx ranges to avoid introducing divergent partition 
branches.
-    bool relax = thread_scope && 
runtime::ThreadScope::Create(op->thread_binding.value()).rank == 1;
+    bool relax =
+        thread_scope && 
runtime::ThreadScope::Create(op->GetThreadBinding().value()).rank == 1;
     auto& ranges = relax ? relax_map_ : hint_map_;
     ranges.insert({op->loop_var.get(), IntSet::Interval(op->min, op->min + 
op->extent - 1)});
     Stmt res = StmtExprMutator::Mutate_(op, InplaceMode::kDisallow)
@@ -923,7 +924,7 @@ inline Stmt LoopPartitioner::MakeFor(const ffi::Object* 
node, PrimExpr extent, S
     };
     return ffi::StructuralMap<ffi::WalkOrder::kPreOrder>(body, 
f_substitute).as_or_throw<Stmt>();
   } else {
-    TVM_FFI_ICHECK(for_node->kind != ForKind::kThreadBinding);
+    TVM_FFI_ICHECK(!for_node->GetThreadBinding().has_value());
     auto new_loop = ffi::make_object<ForNode>(*for_node);
     new_loop->min = IntImm(for_node->min.ty(), 0);
     new_loop->extent = extent;
diff --git a/src/s_tir/transform/lower_cross_thread_reduction.cc 
b/src/s_tir/transform/lower_cross_thread_reduction.cc
index e60bafb1c0..7796f78e5f 100644
--- a/src/s_tir/transform/lower_cross_thread_reduction.cc
+++ b/src/s_tir/transform/lower_cross_thread_reduction.cc
@@ -66,10 +66,10 @@ struct ThreadScopeEqual {
  * \return True if the loop is bound to threadIdx.x/y/z
  */
 bool IsBoundToThreadIdx(const ForNode* loop) {
-  if (!loop->thread_binding.has_value()) {
+  if (!loop->GetThreadBinding().has_value()) {
     return false;
   }
-  runtime::ThreadScope scope = 
runtime::ThreadScope::Create(loop->thread_binding.value());
+  runtime::ThreadScope scope = 
runtime::ThreadScope::Create(loop->GetThreadBinding().value());
   return scope.rank == 1 && scope.dim_index >= 0;
 }
 
@@ -296,7 +296,7 @@ class InThreadReducerMaker : public StmtExprMutator {
     if (!body.value().same_as(loop->body)) {
       res.CopyOnWrite()->body = body.value();
     }
-    if (res->thread_binding.has_value() &&
+    if (res->GetThreadBinding().has_value() &&
         UnderLoopReductionBlockVarCollector::CheckHasReductionBlocks(res)) {
       return res->body;
     }
@@ -429,7 +429,7 @@ Stmt TransformReductionBlock(const SBlockRealizeNode* 
realize,
     }
     ffi::Array<PrimExpr> thread_axes;
     for (const ForNode* reduction_loop : reduction_loops) {
-      if (reduction_loop->thread_binding.has_value()) {
+      if (reduction_loop->GetThreadBinding().has_value()) {
         thread_axes.push_back(reduction_loop->loop_var);
       }
     }
@@ -559,7 +559,7 @@ Stmt TransformReductionBlock(const SBlockRealizeNode* 
realize,
     ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(realize->predicate, 
walk_fn);
     if (wb_buffers[0].scope() != "local") {
       for (const ForNode* loop : reduction_loops) {
-        if (loop->thread_binding.has_value()) {
+        if (loop->GetThreadBinding().has_value()) {
           wb_predicate = wb_predicate &&
                          (static_cast<PrimExpr>(loop->loop_var) == 
IntImm(loop->loop_var.ty(), 0));
         }
@@ -580,7 +580,7 @@ Stmt TransformReductionBlock(const SBlockRealizeNode* 
realize,
   Stmt new_stmt = SeqStmt(std::move(stmts));
   for (auto rit = reduction_loops.rbegin(); rit != reduction_loops.rend(); 
++rit) {
     const ForNode* loop = *rit;
-    if (loop->thread_binding.has_value()) {
+    if (loop->GetThreadBinding().has_value()) {
       ffi::ObjectPtr<ForNode> n = ffi::make_object<ForNode>(*loop);
       n->body = std::move(new_stmt);
       new_stmt = For(n);
@@ -627,7 +627,7 @@ class CrossThreadReductionTransformer : public 
StmtExprMutator {
         // Step 3. Collect the loop.
         reduction_loops.push_back(loop);
         // Step 4. See whether the loop is bound to some thread axis.
-        if (loop->thread_binding.has_value()) {
+        if (loop->GetThreadBinding().has_value()) {
           need = true;
         }
       }
@@ -669,8 +669,8 @@ class CrossThreadReductionTransformer : public 
StmtExprMutator {
 
     // Erase those threads which are not free to this block.
     for (const ForNode* loop : loop_stack_) {
-      if (loop->thread_binding.has_value()) {
-        ThreadScope scope = ThreadScope::Create(loop->thread_binding.value());
+      if (loop->GetThreadBinding().has_value()) {
+        ThreadScope scope = 
ThreadScope::Create(loop->GetThreadBinding().value());
         thread2range.erase(scope);
       }
     }
@@ -724,7 +724,7 @@ class CrossThreadReductionTransformer : public 
StmtExprMutator {
     // bound to `threadIdx.x/y/z`.
     int n_bound_reduction_loops = 0;
     for (const ForNode* reduction_loop : reduction_loops) {
-      if (reduction_loop->thread_binding.has_value()) {
+      if (reduction_loop->GetThreadBinding().has_value()) {
         ++n_bound_reduction_loops;
         TVM_FFI_CHECK(IsBoundToThreadIdx(reduction_loop), ValueError)
             << "Cross-thread reduction requires all the reduction-related 
loops that "
@@ -807,8 +807,8 @@ class CrossThreadReductionTransformer : public 
StmtExprMutator {
     // - we are careful about thread block boundary for safety.
     bool is_block_idx = false;
     bool is_thread_idx = false;
-    if (loop->kind == ForKind::kThreadBinding) {
-      ThreadScope scope = ThreadScope::Create(loop->thread_binding.value());
+    if (loop->GetThreadBinding().has_value()) {
+      ThreadScope scope = 
ThreadScope::Create(loop->GetThreadBinding().value());
       if (scope.rank == 1 && scope.dim_index >= 0) {
         is_thread_idx = true;
         ++thread_idx_depth;
@@ -905,8 +905,8 @@ class CrossThreadReductionTransformer : public 
StmtExprMutator {
     std::vector<std::pair<ThreadScope, Range>> reduction_threads;
     reduction_threads.reserve(reduction_loops.size());
     for (const ForNode* loop : reduction_loops) {
-      if (loop->thread_binding.has_value()) {
-        
reduction_threads.emplace_back(ThreadScope::Create(loop->thread_binding.value()),
+      if (loop->GetThreadBinding().has_value()) {
+        
reduction_threads.emplace_back(ThreadScope::Create(loop->GetThreadBinding().value()),
                                        Range::FromMinExtent(loop->min, 
loop->extent));
       }
     }
@@ -942,11 +942,9 @@ class CrossThreadReductionTransformer : public 
StmtExprMutator {
           /*loop_var=*/loop_vars[i].as_or_throw<PrimVar>(),   //
           /*min=*/unbound_thread2range[i].second->min,        //
           /*extent=*/unbound_thread2range[i].second->extent,  //
-          /*kind=*/ForKind::kThreadBinding,                   //
+          /*kind=*/ForKind::kParallel,                        //
           /*body=*/body,                                      //
-          /*thread_binding=*/
-          ffi::String("threadIdx." + dim_index),
-          /*annotations=*/{},
+          /*annotations=*/{{"thread_binding", ffi::String("threadIdx." + 
dim_index)}},
           /*step=*/std::nullopt);
     }
     return body;
diff --git a/src/s_tir/transform/lower_opaque_block.cc 
b/src/s_tir/transform/lower_opaque_block.cc
index 5599d824a1..7ea2d3b321 100644
--- a/src/s_tir/transform/lower_opaque_block.cc
+++ b/src/s_tir/transform/lower_opaque_block.cc
@@ -111,7 +111,7 @@ class OpaqueBlockLower : public StmtExprMutator {
                     .as_or_throw<UnchangedOr<ffi::Optional<PrimExpr>>>()
                     .ValueOrUnchanged(op->step);
     auto previous_remap = VarRemapGet(op->loop_var);
-    if (op->kind != ForKind::kThreadBinding && IsOne(extent) && 
op->annotations.empty()) {
+    if (!op->GetThreadBinding().has_value() && IsOne(extent) && 
op->annotations.empty()) {
       // handling unit loop
       VarRemapSet(op->loop_var, prim::cast(op->loop_var.ty(), min));
     }
@@ -137,12 +137,12 @@ class OpaqueBlockLower : public StmtExprMutator {
     VarRemapSet(op->loop_var, previous_remap);
 
     // Step 2. Keep thread-binding loops until LowerThreadBinding.
-    if (op->kind != ForKind::kThreadBinding && IsOne(extent) && 
op->annotations.empty() &&
+    if (!op->GetThreadBinding().has_value() && IsOne(extent) && 
op->annotations.empty() &&
         !op->annotations.count(s_tir::attr::irregular_loop_mark)) {
       return body;
     }
     return For(op->loop_var, std::move(min), std::move(extent), op->kind, 
std::move(body),
-               op->thread_binding, std::move(new_annotations), 
std::move(step), op->span);
+               std::move(new_annotations), std::move(step), op->span);
   }
 
   void UpdateUnrollPolicy(const ffi::Map<ffi::String, ffi::Any>& annotations) {
diff --git a/src/s_tir/transform/lower_thread_binding.cc 
b/src/s_tir/transform/lower_thread_binding.cc
index 1c3b46d9b2..427f16cd66 100644
--- a/src/s_tir/transform/lower_thread_binding.cc
+++ b/src/s_tir/transform/lower_thread_binding.cc
@@ -45,7 +45,7 @@ class ThreadBindingLowerer : public StmtExprMutator {
   }
 
   UnchangedOr<Stmt> Mutate_(const ForNode* op, InplaceMode inplace_mode) final 
{
-    if (op->kind != ForKind::kThreadBinding) {
+    if (!op->GetThreadBinding().has_value()) {
       auto annotations = Mutate(op->annotations, inplace_mode)
                              .as_or_throw<UnchangedOr<ffi::Map<ffi::String, 
ffi::Any>>>();
       auto result = StmtExprMutator::Mutate_(op, inplace_mode);
@@ -57,7 +57,7 @@ class ThreadBindingLowerer : public StmtExprMutator {
     PrimExpr min = Mutate(op->min, inplace_mode).ValueOrUnchanged(op->min);
     PrimExpr extent = Mutate(op->extent, 
inplace_mode).ValueOrUnchanged(op->extent);
     TVM_FFI_ICHECK(IsZero(min)) << "Thread binding loops must start at zero";
-    TVM_FFI_ICHECK(op->thread_binding.has_value());
+    TVM_FFI_ICHECK(op->GetThreadBinding().has_value());
     TVM_FFI_ICHECK(!op->annotations.count("loop_partition_hint") ||
                    op->annotations.at("loop_partition_hint") == nullptr)
         << "Run LoopPartition before LowerThreadBinding";
@@ -69,12 +69,13 @@ class ThreadBindingLowerer : public StmtExprMutator {
                            .ValueOrUnchanged(op->annotations);
     Stmt body = Mutate(op->body, inplace_mode).ValueOrUnchanged(op->body);
     VarRemapSet(op->loop_var, previous_remap);
+    annotations.erase("thread_binding");
     if (!annotations.empty()) {
       PrimType ty = op->loop_var.ty();
-      body = For(PrimVar("annotation", ty), IntImm(ty, 0), IntImm(ty, 1), 
ForKind::kSerial,
-                 std::move(body), std::nullopt, std::move(annotations), 
std::nullopt);
+      body = For(PrimVar("annotation", ty), IntImm(ty, 0), IntImm(ty, 1), 
ForKind::kDefault,
+                 std::move(body), std::move(annotations), std::nullopt);
     }
-    return RegionStmt(tirx::launch_thread_op(), 
{StringImm(op->thread_binding.value()), extent},
+    return RegionStmt(tirx::launch_thread_op(), 
{StringImm(op->GetThreadBinding().value()), extent},
                       {launch_var}, DictAttrs(), std::move(body), {}, 
op->span);
   }
 };
diff --git a/src/s_tir/transform/manifest_shared_memory_local_stage.cc 
b/src/s_tir/transform/manifest_shared_memory_local_stage.cc
index 251734e7ad..eecd626a98 100644
--- a/src/s_tir/transform/manifest_shared_memory_local_stage.cc
+++ b/src/s_tir/transform/manifest_shared_memory_local_stage.cc
@@ -92,7 +92,7 @@ class IntermediateStageRewriter {
       const Stmt& ancestor = ancestor_loop_or_blocks_[i];
       if (const ForNode* ancestor_loop = ancestor.as<ForNode>()) {
         TVM_FFI_CHECK(
-            ancestor_loop->kind == ForKind::kSerial || ancestor_loop->kind == 
ForKind::kVectorized,
+            ancestor_loop->kind == ForKind::kDefault || ancestor_loop->kind == 
ForKind::kVectorized,
             ValueError)
             << "Expect the ancestor loops to be serial or vectorized, got " << 
ancestor_loop->kind;
         relaxed_loops.push_back(ancestor.as<ForNode>());
diff --git a/src/s_tir/transform/memhammer_coalesce.cc 
b/src/s_tir/transform/memhammer_coalesce.cc
index 0e010d7beb..e810fa4d0e 100644
--- a/src/s_tir/transform/memhammer_coalesce.cc
+++ b/src/s_tir/transform/memhammer_coalesce.cc
@@ -60,7 +60,7 @@ Stmt FuseNestLoops(Stmt body) {
   }
   body = ffi::StructuralMap<ffi::WalkOrder::kPreOrder>(std::move(body), 
f_substitute)
              .as_or_throw<Stmt>();
-  return For(fused_var, 0, fused_extent, ForKind::kSerial, std::move(body));
+  return For(fused_var, 0, fused_extent, ForKind::kDefault, std::move(body));
 }
 
 /*!
@@ -134,10 +134,11 @@ Stmt SplitBindVectorize(const Stmt& stmt, const 
ConstraintSet& constraints) {
   body = For(new_loop_vars.back().as_or_throw<PrimVar>(), 0, vector_len, 
ForKind::kVectorized,
              std::move(body));
   for (int i = n - 2; i >= 1; i--) {
-    body = For(new_loop_vars[i].as_or_throw<PrimVar>(), 0, factors[i], 
ForKind::kThreadBinding,
-               std::move(body), ffi::String(thread_axis[i - 1]), {}, 
std::nullopt);
+    body =
+        For(new_loop_vars[i].as_or_throw<PrimVar>(), 0, factors[i], 
ForKind::kParallel,
+            std::move(body), {{"thread_binding", ffi::String(thread_axis[i - 
1])}}, std::nullopt);
   }
-  return For(new_loop_vars[0].as_or_throw<PrimVar>(), 0, factors[0], 
ForKind::kSerial,
+  return For(new_loop_vars[0].as_or_throw<PrimVar>(), 0, factors[0], 
ForKind::kDefault,
              std::move(body));
 }
 
@@ -242,7 +243,7 @@ Stmt InverseMapping::Rewrite(const Stmt& stmt, const 
ConstraintSet& constraints,
   // Step 3.3 construct loop body
   for (int i = static_cast<int>(new_loop_vars.size()) - 1; i >= 0; i--) {
     PrimExpr extent = write_region->region[i]->extent;
-    ret = For(new_loop_vars[i], 0, extent, ForKind::kSerial, std::move(ret));
+    ret = For(new_loop_vars[i], 0, extent, ForKind::kDefault, std::move(ret));
   }
   return ret;
 }
diff --git a/src/s_tir/transform/memhammer_intermediate_stage.cc 
b/src/s_tir/transform/memhammer_intermediate_stage.cc
index 95d484404a..d26a78a0d1 100644
--- a/src/s_tir/transform/memhammer_intermediate_stage.cc
+++ b/src/s_tir/transform/memhammer_intermediate_stage.cc
@@ -51,7 +51,7 @@ std::pair<Stmt, ffi::Optional<For>> 
LiftThreadBindingLoops(Stmt stmt) {
   std::vector<const ForNode*> thread_binding_loops;
   Stmt body = stmt;
   while (const ForNode* loop = body.as<ForNode>()) {
-    if (loop->kind == ForKind::kThreadBinding) {
+    if (loop->GetThreadBinding().has_value()) {
       thread_binding_loops.push_back(loop);
     } else {
       normal_loops.push_back(loop);
@@ -275,8 +275,8 @@ std::pair<Stmt, SeqStmt> InsertCacheStage(Stmt stmt, bool 
is_write_cache, ffi::S
     body = op->then_case->seq[0];
   }
   for (const For& loop : outer_loops) {
-    if (loop->kind == ForKind::kThreadBinding) {
-      const ffi::String& thread_tag = loop->thread_binding.value();
+    if (loop->GetThreadBinding().has_value()) {
+      const ffi::String thread_tag = loop->GetThreadBinding().value();
       auto thread_scope = runtime::ThreadScope::Create(thread_tag);
       if 
(CanRelaxStorageUnderThread(runtime::StorageScope::Create(storage_scope), 
thread_scope)) {
         if (is_write_cache && thread_scope.dim_index == 0) {
@@ -430,8 +430,7 @@ std::pair<Stmt, SeqStmt> InsertCacheStage(Stmt stmt, bool 
is_write_cache, ffi::S
     ffi::ObjectPtr<ForNode> new_loop = ffi::make_object<ForNode>(*orig_loop);
     new_loop->loop_var = new_loop_vars[i];
     new_loop->body = generate_body;
-    new_loop->kind = ForKind::kSerial;
-    new_loop->thread_binding = std::nullopt;
+    new_loop->kind = ForKind::kDefault;
     new_loop->annotations = {};
     generate_body = For(new_loop);
   }
diff --git a/src/s_tir/transform/memhammer_lower_auto_copy.cc 
b/src/s_tir/transform/memhammer_lower_auto_copy.cc
index c64e87d4e2..c95536cb0f 100644
--- a/src/s_tir/transform/memhammer_lower_auto_copy.cc
+++ b/src/s_tir/transform/memhammer_lower_auto_copy.cc
@@ -516,10 +516,10 @@ class AutoPadder {
     }
 
     ffi::Optional<VisitInterrupt> Visit_(const ForNode* op) final {
-      if (op->kind != ForKind::kThreadBinding) {
+      if (!op->GetThreadBinding().has_value()) {
         substitute_map_.Set(op->loop_var, op->min);
       } else {
-        int64_t extent = 
warp_thread_extent_.Get(op->thread_binding.value()).value_or(1);
+        int64_t extent = 
warp_thread_extent_.Get(op->GetThreadBinding().value()).value_or(1);
         var_range_.Set(op->loop_var, Range::FromMinExtent(op->min, 
IntImm::Int64(extent)));
       }
       if (op->kind == ForKind::kVectorized) {
@@ -530,7 +530,7 @@ class AutoPadder {
       if (op->kind == ForKind::kVectorized) {
         vector_length_ = -1;
       }
-      if (op->kind != ForKind::kThreadBinding) {
+      if (!op->GetThreadBinding().has_value()) {
         substitute_map_.erase(op->loop_var);
       }
       return std::nullopt;
@@ -831,9 +831,9 @@ class ThreadExtentCollector : public StmtExprVisitor {
     return StmtExprVisitor::Visit_(op);
   }
   ffi::Optional<VisitInterrupt> Visit_(const ForNode* op) final {
-    if (op->thread_binding.has_value()) {
+    if (op->GetThreadBinding().has_value()) {
       if (const auto* extent = op->extent.as<IntImmNode>()) {
-        thread_extent_.Set(op->thread_binding.value(), 
static_cast<int64_t>(extent->value));
+        thread_extent_.Set(op->GetThreadBinding().value(), 
static_cast<int64_t>(extent->value));
       }
     }
     return StmtExprVisitor::Visit_(op);
diff --git a/src/s_tir/transform/memhammer_tensorcore_rewrite.cc 
b/src/s_tir/transform/memhammer_tensorcore_rewrite.cc
index e69f23a916..121bc1e509 100644
--- a/src/s_tir/transform/memhammer_tensorcore_rewrite.cc
+++ b/src/s_tir/transform/memhammer_tensorcore_rewrite.cc
@@ -78,13 +78,13 @@ std::pair<Stmt, ffi::Optional<For>> TileWmmaBlock(Stmt 
stmt) {
         /*3:*/ 16,                          //
         /*4:*/ 16,                          //
     };
-    body = For(new_loop_vars[3].as_or_throw<PrimVar>(), 0, factor[3], 
ForKind::kSerial,
+    body = For(new_loop_vars[3].as_or_throw<PrimVar>(), 0, factor[3], 
ForKind::kDefault,
                std::move(body));
-    body = For(new_loop_vars[2].as_or_throw<PrimVar>(), 0, factor[2], 
ForKind::kSerial,
+    body = For(new_loop_vars[2].as_or_throw<PrimVar>(), 0, factor[2], 
ForKind::kDefault,
                std::move(body));
-    body = For(new_loop_vars[1].as_or_throw<PrimVar>(), 0, factor[1], 
ForKind::kSerial,
+    body = For(new_loop_vars[1].as_or_throw<PrimVar>(), 0, factor[1], 
ForKind::kDefault,
                std::move(body));
-    body = For(new_loop_vars[0].as_or_throw<PrimVar>(), 0, factor[0], 
ForKind::kSerial,
+    body = For(new_loop_vars[0].as_or_throw<PrimVar>(), 0, factor[0], 
ForKind::kDefault,
                std::move(body));
   }
   For compute_location = body.as_or_throw<For>();
@@ -409,13 +409,13 @@ std::pair<Stmt, ffi::Optional<For>> 
TileMmaToGlobalBlock(Stmt stmt) {
         /*3:*/ 8,                          //
         /*4:*/ 8,                          //
     };
-    body = For(new_loop_vars[3].as_or_throw<PrimVar>(), 0, factor[3], 
ForKind::kSerial,
+    body = For(new_loop_vars[3].as_or_throw<PrimVar>(), 0, factor[3], 
ForKind::kDefault,
                std::move(body));
-    body = For(new_loop_vars[2].as_or_throw<PrimVar>(), 0, factor[2], 
ForKind::kSerial,
+    body = For(new_loop_vars[2].as_or_throw<PrimVar>(), 0, factor[2], 
ForKind::kDefault,
                std::move(body));
-    body = For(new_loop_vars[1].as_or_throw<PrimVar>(), 0, factor[1], 
ForKind::kSerial,
+    body = For(new_loop_vars[1].as_or_throw<PrimVar>(), 0, factor[1], 
ForKind::kDefault,
                std::move(body));
-    body = For(new_loop_vars[0].as_or_throw<PrimVar>(), 0, factor[0], 
ForKind::kSerial,
+    body = For(new_loop_vars[0].as_or_throw<PrimVar>(), 0, factor[0], 
ForKind::kDefault,
                std::move(body));
   }
   For compute_location = body.as_or_throw<For>();
diff --git a/src/target/llvm/codegen_cpu.cc b/src/target/llvm/codegen_cpu.cc
index f2d75a5e72..82ad33fffc 100644
--- a/src/target/llvm/codegen_cpu.cc
+++ b/src/target/llvm/codegen_cpu.cc
@@ -1147,9 +1147,9 @@ void CodeGenCPU::Dispatch_(const RegionStmtNode* op) {
 
 void CodeGenCPU::Dispatch_(const ForNode* op) {
   EmitDebugLocation(op);
-  if (op->kind == ForKind::kSerial || op->kind == ForKind::kUnrolled) {
+  if (op->kind == ForKind::kDefault || op->kind == ForKind::kUnrolled) {
     CodeGenLLVM::Dispatch_(op);
-  } else if (op->kind == ForKind::kParallel) {
+  } else if (op->kind == ForKind::kParallel && 
!op->GetThreadBinding().has_value()) {
     TVM_FFI_ICHECK(IsZero(op->min))
         << "Parallel launch require canonical loop with zero start index";
     TVM_FFI_ICHECK(op->HasTrivialStep())
diff --git a/src/target/llvm/codegen_llvm.cc b/src/target/llvm/codegen_llvm.cc
index 972c8d1199..afcb507c8d 100644
--- a/src/target/llvm/codegen_llvm.cc
+++ b/src/target/llvm/codegen_llvm.cc
@@ -2100,7 +2100,7 @@ void CodeGenLLVM::Dispatch_(const ForNode* op) {
     LOG(WARNING) << "Unroll hint get ignore at CodeGenLLVM backend, "
                  << " consider set unroll_explicit=True";
   } else {
-    TVM_FFI_ICHECK(op->kind == ForKind::kSerial);
+    TVM_FFI_ICHECK(op->kind == ForKind::kDefault);
   }
   PrimExpr step = op->step.value_or(IntImm(op->extent.ty(), 1));
   PrimExpr end = IsZero(op->min) ? op->extent : analyzer_->Simplify(op->min + 
op->extent);
diff --git a/src/te/operation/create_function.cc 
b/src/te/operation/create_function.cc
index 1f2b80fb46..5ac18fd6cc 100644
--- a/src/te/operation/create_function.cc
+++ b/src/te/operation/create_function.cc
@@ -743,7 +743,7 @@ Stmt GenerateStmtFromCompute(const te::ComputeOp& 
compute_op, CreateFuncInfo* in
     }
     for (size_t j = cur.loop_vars.size(); j > 0; --j) {
       const auto& [loop_var, dom] = cur.loop_vars[j - 1];
-      body = For(loop_var.as_or_throw<PrimVar>(), dom->min, dom->extent, 
ForKind::kSerial, body);
+      body = For(loop_var.as_or_throw<PrimVar>(), dom->min, dom->extent, 
ForKind::kDefault, body);
     }
   }
   return body;
diff --git a/src/tirx/ir/stmt.cc b/src/tirx/ir/stmt.cc
index 377957433f..270ffd5674 100644
--- a/src/tirx/ir/stmt.cc
+++ b/src/tirx/ir/stmt.cc
@@ -283,7 +283,6 @@ TVM_FFI_INLINE 
ffi::Expected<ffi::Optional<ffi::VisitInterrupt>> ForVisit(
   TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->min));
   TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->extent));
   TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->body));
-  
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->thread_binding));
   TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->step));
   return std::nullopt;
 }
@@ -303,16 +302,11 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
ForMutate(
                                     mutator->MutateExpected(self->extent));
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Stmt>, mapped_body,
                                     mutator->MutateExpected(self->body));
-  
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<ffi::String>>,
-                                    mapped_thread_binding,
-                                    
mutator->MutateExpected(self->thread_binding));
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<PrimExpr>>, 
mapped_step,
                                     mutator->MutateExpected(self->step));
   if (mapped_loop_var.UnchangedOrSameAs(self->loop_var) &&
       mapped_min.UnchangedOrSameAs(self->min) && 
mapped_extent.UnchangedOrSameAs(self->extent) &&
-      mapped_body.UnchangedOrSameAs(self->body) &&
-      mapped_thread_binding.UnchangedOrSameAs(self->thread_binding) &&
-      mapped_step.UnchangedOrSameAs(self->step)) {
+      mapped_body.UnchangedOrSameAs(self->body) && 
mapped_step.UnchangedOrSameAs(self->step)) {
     return ffi::Unchanged();
   }
   ffi::ObjectPtr<ForNode> copy = ffi::make_object<ForNode>(*self);
@@ -320,8 +314,6 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
ForMutate(
   copy->min = std::move(mapped_min).ValueOrUnchanged(std::move(copy->min));
   copy->extent = 
std::move(mapped_extent).ValueOrUnchanged(std::move(copy->extent));
   copy->body = std::move(mapped_body).ValueOrUnchanged(std::move(copy->body));
-  copy->thread_binding =
-      
std::move(mapped_thread_binding).ValueOrUnchanged(std::move(copy->thread_binding));
   copy->step = std::move(mapped_step).ValueOrUnchanged(std::move(copy->step));
   return ffi::Any(std::move(copy));
 }
@@ -343,25 +335,17 @@ TVM_FFI_INLINE ffi::Expected<ffi::UnchangedOr<ffi::Any>> 
ForMaybeInplaceMutate(
       mutator->MutateExpected(self->extent, ffi::InplaceMode::kAllow));
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Stmt>, mapped_body,
                                     mutator->MutateExpected(self->body, 
ffi::InplaceMode::kAllow));
-  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
-      ffi::UnchangedOr<ffi::Optional<ffi::String>>, mapped_thread_binding,
-      mutator->MutateExpected(self->thread_binding, ffi::InplaceMode::kAllow));
   TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<PrimExpr>>, 
mapped_step,
                                     mutator->MutateExpected(self->step, 
ffi::InplaceMode::kAllow));
   if (mapped_loop_var.UnchangedOrSameAs(self->loop_var) &&
       mapped_min.UnchangedOrSameAs(self->min) && 
mapped_extent.UnchangedOrSameAs(self->extent) &&
-      mapped_body.UnchangedOrSameAs(self->body) &&
-      mapped_thread_binding.UnchangedOrSameAs(self->thread_binding) &&
-      mapped_step.UnchangedOrSameAs(self->step)) {
+      mapped_body.UnchangedOrSameAs(self->body) && 
mapped_step.UnchangedOrSameAs(self->step)) {
     return ffi::Unchanged();
   }
   if (!mapped_loop_var.IsUnchanged()) self->loop_var = 
std::move(mapped_loop_var).ValueUnchecked();
   if (!mapped_min.IsUnchanged()) self->min = 
std::move(mapped_min).ValueUnchecked();
   if (!mapped_extent.IsUnchanged()) self->extent = 
std::move(mapped_extent).ValueUnchecked();
   if (!mapped_body.IsUnchanged()) self->body = 
SeqStmt(std::move(mapped_body).ValueUnchecked());
-  if (!mapped_thread_binding.IsUnchanged()) {
-    self->thread_binding = std::move(mapped_thread_binding).ValueUnchecked();
-  }
   if (!mapped_step.IsUnchanged()) self->step = 
std::move(mapped_step).ValueUnchecked();
   return ffi::Unchanged();
 }
@@ -855,8 +839,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 
 // For
 For::For(PrimVar loop_var, PrimExpr min, PrimExpr extent, ForKind kind, 
SeqStmt body,
-         ffi::Optional<ffi::String> thread_binding, ffi::Map<ffi::String, Any> 
annotations,
-         ffi::Optional<PrimExpr> step, Span span)
+         ffi::Map<ffi::String, Any> annotations, ffi::Optional<PrimExpr> step, 
Span span)
     : Stmt(ffi::UnsafeInit{}) {
   TVM_FFI_ICHECK(loop_var.defined());
   TVM_FFI_ICHECK(min.defined());
@@ -908,8 +891,15 @@ For::For(PrimVar loop_var, PrimExpr min, PrimExpr extent, 
ForKind kind, SeqStmt
 
   ffi::ObjectPtr<ForNode> node = 
ffi::make_object<ForNode>(std::move(loop_var), std::move(min),
                                                            std::move(extent), 
std::move(body));
+  if (auto tag = annotations.Get("thread_binding")) {
+    TVM_FFI_CHECK(kind == ForKind::kParallel, ValueError)
+        << "thread_binding requires a parallel loop";
+    TVM_FFI_CHECK(tag->as<ffi::String>().has_value(), TypeError)
+        << "thread_binding annotation must be a string";
+    TVM_FFI_CHECK(!step.has_value() || IsOne(*step), ValueError)
+        << "Thread binding loops require a unit step";
+  }
   node->kind = kind;
-  node->thread_binding = std::move(thread_binding);
   node->annotations = std::move(annotations);
   node->step = std::move(step);
   node->span = std::move(span);
@@ -925,20 +915,20 @@ TVM_FFI_STATIC_INIT_BLOCK() {
       .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
             ffi::FStructuralMutate::FromNative<&ForMaybeInplaceMutate>());
 
-  refl::GlobalDef().def("tirx.For", [](PrimVar loop_var, PrimExpr min, 
PrimExpr extent, int kind,
-                                       SeqStmt body, 
ffi::Optional<ffi::String> thread_binding,
-                                       ffi::Optional<ffi::Map<ffi::String, 
Any>> annotations,
-                                       ffi::Optional<PrimExpr> step, Span 
span) {
-    return For(loop_var, min, extent, static_cast<ForKind>(kind), body, 
thread_binding,
-               annotations.value_or(ffi::Map<ffi::String, Any>()), step, span);
-  });
+  refl::GlobalDef().def(
+      "tirx.For", [](PrimVar loop_var, PrimExpr min, PrimExpr extent, int 
kind, SeqStmt body,
+                     ffi::Optional<ffi::Map<ffi::String, Any>> annotations,
+                     ffi::Optional<PrimExpr> step, Span span) {
+        return For(loop_var, min, extent, static_cast<ForKind>(kind), body,
+                   annotations.value_or(ffi::Map<ffi::String, Any>()), step, 
span);
+      });
 }
 
 bool ForNode::HasTrivialStep() const { return !step.has_value() || 
IsOne(*step); }
 
 std::ostream& operator<<(std::ostream& out, ForKind type) {  // NOLINT(*)
   switch (type) {
-    case ForKind::kSerial:
+    case ForKind::kDefault:
       out << "for";
       break;
     case ForKind::kParallel:
@@ -950,9 +940,6 @@ std::ostream& operator<<(std::ostream& out, ForKind type) { 
 // NOLINT(*)
     case ForKind::kVectorized:
       out << "vectorized";
       break;
-    case ForKind::kThreadBinding:
-      out << "launch_thread";
-      break;
   }
   return out;
 }
diff --git a/src/tirx/script/ir_builder/ir.cc b/src/tirx/script/ir_builder/ir.cc
index 8ca0a71a61..2a8af91843 100644
--- a/src/tirx/script/ir_builder/ir.cc
+++ b/src/tirx/script/ir_builder/ir.cc
@@ -281,34 +281,34 @@ PrimExpr ConvertLoopBound(const PrimExpr& e, const 
PrimType& var_ty) {
   return tvm::prim::Cast(var_ty, e);
 }
 
-#define TVM_TIRX_IR_BUILDER_FOR_FRAME(Method, Kind)                            
                  \
-  ForFrame Method(PrimExpr start, PrimExpr stop,                               
                  \
-                  ffi::Optional<ffi::Map<ffi::String, Any>> annotations,       
                  \
-                  ffi::Optional<PrimExpr> step, ffi::Optional<PrimType> dtype) 
{                 \
-    PrimType var_ty = InferLoopVarDtype(start, stop, dtype);                   
                  \
-    PrimExpr min = ConvertLoopBound(start, var_ty);                            
                  \
-    PrimExpr extent = sym::Analyzer()->Simplify(ConvertLoopBound(stop, var_ty) 
- min);           \
-    if (step.has_value()) {                                                    
                  \
-      step = ConvertLoopBound(step.value(), var_ty);                           
                  \
-    }                                                                          
                  \
-    ffi::ObjectPtr<ForFrameNode> n = ffi::make_object<ForFrameNode>();         
                  \
-    n->vars = {Var("v", var_ty)};                                              
                  \
-    n->doms = {Range::FromMinExtent(min, extent)};                             
                  \
-    n->steps = {step};                                                         
                  \
-    n->f_make_for_loop = [annotations](ffi::Array<Var> vars, ffi::Array<Range> 
doms,             \
-                                       ffi::Array<ffi::Optional<PrimExpr>> 
steps,                \
-                                       tvm::tirx::SeqStmt body, Span span) {   
                  \
-      TVM_FFI_ICHECK_EQ(vars.size(), 1);                                       
                  \
-      TVM_FFI_ICHECK_EQ(doms.size(), 1);                                       
                  \
-      TVM_FFI_ICHECK_EQ(steps.size(), 1);                                      
                  \
-      return tvm::tirx::For(vars[0].as_or_throw<tvm::PrimVar>(), doms[0]->min, 
doms[0]->extent,  \
-                            Kind, body, std::nullopt,                          
                  \
-                            annotations.value_or(ffi::Map<ffi::String, 
Any>()), steps[0], span); \
-    };                                                                         
                  \
-    return ForFrame(n);                                                        
                  \
+#define TVM_TIRX_IR_BUILDER_FOR_FRAME(Method, Kind)                            
                 \
+  ForFrame Method(PrimExpr start, PrimExpr stop,                               
                 \
+                  ffi::Optional<ffi::Map<ffi::String, Any>> annotations,       
                 \
+                  ffi::Optional<PrimExpr> step, ffi::Optional<PrimType> dtype) 
{                \
+    PrimType var_ty = InferLoopVarDtype(start, stop, dtype);                   
                 \
+    PrimExpr min = ConvertLoopBound(start, var_ty);                            
                 \
+    PrimExpr extent = sym::Analyzer()->Simplify(ConvertLoopBound(stop, var_ty) 
- min);          \
+    if (step.has_value()) {                                                    
                 \
+      step = ConvertLoopBound(step.value(), var_ty);                           
                 \
+    }                                                                          
                 \
+    ffi::ObjectPtr<ForFrameNode> n = ffi::make_object<ForFrameNode>();         
                 \
+    n->vars = {Var("v", var_ty)};                                              
                 \
+    n->doms = {Range::FromMinExtent(min, extent)};                             
                 \
+    n->steps = {step};                                                         
                 \
+    n->f_make_for_loop = [annotations](ffi::Array<Var> vars, ffi::Array<Range> 
doms,            \
+                                       ffi::Array<ffi::Optional<PrimExpr>> 
steps,               \
+                                       tvm::tirx::SeqStmt body, Span span) {   
                 \
+      TVM_FFI_ICHECK_EQ(vars.size(), 1);                                       
                 \
+      TVM_FFI_ICHECK_EQ(doms.size(), 1);                                       
                 \
+      TVM_FFI_ICHECK_EQ(steps.size(), 1);                                      
                 \
+      return tvm::tirx::For(vars[0].as_or_throw<tvm::PrimVar>(), doms[0]->min, 
doms[0]->extent, \
+                            Kind, body, 
annotations.value_or(ffi::Map<ffi::String, Any>()),     \
+                            steps[0], span);                                   
                 \
+    };                                                                         
                 \
+    return ForFrame(n);                                                        
                 \
   }
 
-TVM_TIRX_IR_BUILDER_FOR_FRAME(Serial, tvm::tirx::ForKind::kSerial);
+TVM_TIRX_IR_BUILDER_FOR_FRAME(Serial, tvm::tirx::ForKind::kDefault);
 TVM_TIRX_IR_BUILDER_FOR_FRAME(Parallel, tvm::tirx::ForKind::kParallel);
 TVM_TIRX_IR_BUILDER_FOR_FRAME(Vectorized, tvm::tirx::ForKind::kVectorized);
 TVM_TIRX_IR_BUILDER_FOR_FRAME(Unroll, tvm::tirx::ForKind::kUnrolled);
@@ -334,9 +334,14 @@ ForFrame ThreadBinding(PrimExpr start, PrimExpr stop, 
ffi::String thread,
     TVM_FFI_ICHECK_EQ(vars.size(), 1);
     TVM_FFI_ICHECK_EQ(doms.size(), 1);
     TVM_FFI_ICHECK(steps.size() == 1 && (!steps[0].has_value() || 
IsOne(*steps[0])));
+    auto loop_annotations = annotations.value_or(ffi::Map<ffi::String, 
ffi::Any>());
+    if (auto existing = loop_annotations.Get("thread_binding")) {
+      TVM_FFI_CHECK(existing->cast<ffi::String>() == thread, ValueError)
+          << "Conflicting thread_binding annotation and thread argument";
+    }
+    loop_annotations.Set("thread_binding", thread);
     return For(vars[0].as_or_throw<tvm::PrimVar>(), doms[0]->min, 
doms[0]->extent,
-               ForKind::kThreadBinding, body, thread,
-               annotations.value_or(ffi::Map<ffi::String, ffi::Any>()), 
std::nullopt, span);
+               ForKind::kParallel, body, std::move(loop_annotations), 
std::nullopt, span);
   };
   return ForFrame(n);
 }
@@ -377,9 +382,9 @@ ForFrame Grid(ffi::Array<ffi::Variant<PrimExpr, 
ffi::Tuple<PrimExpr, PrimExpr>>>
     for (int i = n - 1; i >= 0; --i) {
       Range dom = doms[i];
       Var var = vars[i];
-      result = For(var.as_or_throw<tvm::PrimVar>(), dom->min, dom->extent, 
ForKind::kSerial,
+      result = For(var.as_or_throw<tvm::PrimVar>(), dom->min, dom->extent, 
ForKind::kDefault,
                    SeqStmt(std::move(result)),
-                   /*thread_binding=*/std::nullopt, /*annotations=*/{}, 
/*step=*/steps[i], span);
+                   /*annotations=*/{}, /*step=*/steps[i], span);
     }
     return result;
   };
diff --git a/src/tirx/script/printer/for_loop.cc 
b/src/tirx/script/printer/for_loop.cc
index 48aa0e2357..df89d500da 100644
--- a/src/tirx/script/printer/for_loop.cc
+++ b/src/tirx/script/printer/for_loop.cc
@@ -40,13 +40,14 @@ ffi::Optional<ExprDoc> ForDocTranslate(DocTranslatorObj* d, 
ffi::AnyView input,
       ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
tirx::ForNode>(input);
   TVM_FFI_CHECK(destination == nullptr, TypeError)
       << "printer statement-only node cannot fulfill a destination";
+  bool use_thread_binding = loop->GetThreadBinding().has_value() && 
!loop->step.has_value();
   ffi::String method;
   switch (loop->kind) {
-    case tirx::ForKind::kSerial:
+    case tirx::ForKind::kDefault:
       method = "serial";
       break;
     case tirx::ForKind::kParallel:
-      method = "parallel";
+      method = use_thread_binding ? "thread_binding" : "parallel";
       break;
     case tirx::ForKind::kVectorized:
       method = "vectorized";
@@ -54,9 +55,6 @@ ffi::Optional<ExprDoc> ForDocTranslate(DocTranslatorObj* d, 
ffi::AnyView input,
     case tirx::ForKind::kUnrolled:
       method = "unroll";
       break;
-    case tirx::ForKind::kThreadBinding:
-      method = "thread_binding";
-      break;
     default:
       TVM_FFI_THROW(TypeError) << "printer unknown loop kind";
   }
@@ -72,14 +70,14 @@ ffi::Optional<ExprDoc> ForDocTranslate(DocTranslatorObj* d, 
ffi::AnyView input,
   }
   ffi::Array<ffi::String> keys;
   ffi::Array<ExprDoc> values;
-  if (loop->kind == tirx::ForKind::kThreadBinding) {
-    TVM_FFI_CHECK(loop->thread_binding.has_value(), TypeError)
+  if (use_thread_binding) {
+    TVM_FFI_CHECK(loop->GetThreadBinding().has_value(), TypeError)
         << "printer thread-binding loop lacks thread tag";
     keys.push_back("thread");
-    values.push_back(LiteralDoc::Str(loop->thread_binding.value(), 
std::nullopt));
+    values.push_back(LiteralDoc::Str(loop->GetThreadBinding().value(), 
std::nullopt));
   }
   bool unroll_option = false;
-  if (loop->kind == tirx::ForKind::kSerial && loop->annotations.size() == 1) {
+  if (loop->kind == tirx::ForKind::kDefault && loop->annotations.size() == 1) {
     const auto& [key, value] = *loop->annotations.begin();
     auto boolean = value.as<bool>();
     auto integer = value.as<int64_t>();
@@ -94,9 +92,11 @@ ffi::Optional<ExprDoc> ForDocTranslate(DocTranslatorObj* d, 
ffi::AnyView input,
       unroll_option = true;
     }
   }
-  if (!unroll_option && !loop->annotations.empty()) {
+  auto annotations = loop->annotations;
+  if (use_thread_binding) annotations.erase("thread_binding");
+  if (!unroll_option && !annotations.empty()) {
     std::vector<std::pair<ffi::String, ffi::Any>> sorted;
-    for (const auto& [key, value] : loop->annotations) 
sorted.emplace_back(key, value);
+    for (const auto& [key, value] : annotations) sorted.emplace_back(key, 
value);
     std::sort(sorted.begin(), sorted.end(),
               [](const auto& a, const auto& b) { return a.first < b.first; });
     ffi::Array<ExprDoc> annotation_keys;
@@ -109,8 +109,6 @@ ffi::Optional<ExprDoc> ForDocTranslate(DocTranslatorObj* d, 
ffi::AnyView input,
     values.push_back(DictDoc(annotation_keys, annotation_values));
   }
   if (loop->step.has_value()) {
-    TVM_FFI_CHECK(loop->kind != tirx::ForKind::kThreadBinding, TypeError)
-        << "printer thread-binding loop cannot have a step";
     keys.push_back("step");
     values.push_back(d->Translate(loop->step.value()).value());
   }
@@ -120,12 +118,11 @@ ffi::Optional<ExprDoc> ForDocTranslate(DocTranslatorObj* 
d, ffi::AnyView input,
   {
     IdDoc var = VarDoc(d, loop->loop_var);
     ffi::Array<ExprDoc> bounds = {min, end};
-    if (loop->kind == tirx::ForKind::kThreadBinding && prim::IsZero(loop->min) 
&&
-        loop->min.ty() == loop->extent.ty()) {
+    if (use_thread_binding && prim::IsZero(loop->min) && loop->min.ty() == 
loop->extent.ty()) {
       bounds = {end};
     }
     ExprDoc callee = NamespaceDoc("tirx")->Attr(method);
-    if (loop->kind == tirx::ForKind::kSerial && loop->annotations.empty()) {
+    if (loop->kind == tirx::ForKind::kDefault && loop->annotations.empty()) {
       callee = IdDoc("range");
       // range's step is positional. Retain even an explicit unit step because
       // it is part of the source For node, unlike an absent step.
diff --git a/src/tirx/transform/bind_target.cc 
b/src/tirx/transform/bind_target.cc
index b36a32e8f1..278d9b303d 100644
--- a/src/tirx/transform/bind_target.cc
+++ b/src/tirx/transform/bind_target.cc
@@ -102,7 +102,7 @@ class FunctionClassifierVisitor : public StmtExprVisitor {
   }
 
   ffi::Optional<VisitInterrupt> Visit_(const ForNode* op) final {
-    if (op->kind == ForKind::kThreadBinding) {
+    if (op->GetThreadBinding().has_value()) {
       // Enter GPU scope for thread binding loops
       bool last_is_under_gpu_scope = is_under_gpu_scope_;
       is_under_gpu_scope_ = true;
@@ -188,7 +188,7 @@ class CallSubstitutor : public StmtExprMutator {
   }
 
   UnchangedOr<Stmt> Mutate_(const ForNode* op, InplaceMode inplace_mode) final 
{
-    if (op->kind == ForKind::kThreadBinding) {
+    if (op->GetThreadBinding().has_value()) {
       // Enter GPU scope for thread binding loops
       bool last_is_under_gpu_scope = is_under_gpu_scope_;
       is_under_gpu_scope_ = true;
diff --git a/src/tirx/transform/lower_tirx_opaque.cc 
b/src/tirx/transform/lower_tirx_opaque.cc
index 1d3cc54d94..903e451483 100644
--- a/src/tirx/transform/lower_tirx_opaque.cc
+++ b/src/tirx/transform/lower_tirx_opaque.cc
@@ -71,7 +71,7 @@ class TIRxOpaqueLower : public StmtExprMutator {
                     .ValueOrUnchanged(op->step);
     ffi::Any previous_remap = VarRemapGet(op->loop_var);
     PrimVar launch_var(ffi::UnsafeInit{});
-    if (op->kind == ForKind::kThreadBinding) {
+    if (op->GetThreadBinding().has_value()) {
       TVM_FFI_ICHECK(IsZero(min)) << "Thread binding must have zero minimum";
       launch_var = PrimVar(op->loop_var->name, extent.ty());
       VarRemapSet(op->loop_var, prim::cast(op->loop_var.ty(), launch_var));
@@ -101,24 +101,25 @@ class TIRxOpaqueLower : public StmtExprMutator {
     VarRemapSet(op->loop_var, previous_remap);
 
     // Step 2. Create the lowered loop or launch region.
-    if (op->kind == ForKind::kThreadBinding) {
+    if (op->GetThreadBinding().has_value()) {
       // Case 1. Thread binding → RegionStmt(launch_thread)
       TVM_FFI_ICHECK(!op->annotations.count("loop_partition_hint") ||
                      op->annotations.at("loop_partition_hint") == nullptr)
           << "Run LoopPartition before opaque lowering of a thread-binding 
loop with "
              "loop_partition_hint";
-      TVM_FFI_ICHECK(op->thread_binding.has_value());
+      TVM_FFI_ICHECK(op->GetThreadBinding().has_value());
     } else if (IsOne(extent) && op->annotations.empty()) {
       // Case 2. Unit loop elimination
       return body;
     } else {
       // Case 3. An ordinary loop
       body = For(op->loop_var, std::move(min), std::move(extent), op->kind, 
std::move(body),
-                 std::nullopt, FilterAnnotations(annotations), step);
+                 FilterAnnotations(annotations), step);
     }
-    if (op->kind == ForKind::kThreadBinding) {
-      return RegionStmt(tirx::launch_thread_op(), 
{StringImm(op->thread_binding.value()), extent},
-                        {launch_var}, DictAttrs(), body, {}, op->span);
+    if (op->GetThreadBinding().has_value()) {
+      return RegionStmt(tirx::launch_thread_op(),
+                        {StringImm(op->GetThreadBinding().value()), extent}, 
{launch_var},
+                        DictAttrs(), body, {}, op->span);
     }
     return body;
   }
diff --git a/src/tirx/transform/lower_tvm_builtin.cc 
b/src/tirx/transform/lower_tvm_builtin.cc
index a4197e7788..03ce57b729 100644
--- a/src/tirx/transform/lower_tvm_builtin.cc
+++ b/src/tirx/transform/lower_tvm_builtin.cc
@@ -374,7 +374,7 @@ class BuiltinLower : public StmtExprMutator {
     PrimExpr extent = std::move(extent_result).ValueOrUnchanged(op->extent);
     Stmt body = op->body;
 
-    if (op->kind == ForKind::kParallel) {
+    if (op->kind == ForKind::kParallel && !op->GetThreadBinding().has_value()) 
{
       body = this->VisitBodyAndRealizeAlloca(op->body);
     } else {
       body = scope_.WithNewScope([&]() -> Stmt {
diff --git a/src/tirx/transform/make_packed_api.cc 
b/src/tirx/transform/make_packed_api.cc
index ae433c2f89..d6acde6604 100644
--- a/src/tirx/transform/make_packed_api.cc
+++ b/src/tirx/transform/make_packed_api.cc
@@ -57,10 +57,12 @@ class ReturnRewriter : public StmtExprMutator {
   explicit ReturnRewriter(Var ret_var) : ret_var_(ret_var) {}
 
   UnchangedOr<Stmt> Mutate_(const ForNode* node, InplaceMode inplace_mode) 
override {
-    if (node->kind == ForKind::kParallel) in_parallel_ += 1;
+    if (node->kind == ForKind::kParallel && 
!node->GetThreadBinding().has_value())
+      in_parallel_ += 1;
     Stmt ret =
         StmtExprMutator::Mutate_(node, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(node));
-    if (node->kind == ForKind::kParallel) in_parallel_ -= 1;
+    if (node->kind == ForKind::kParallel && 
!node->GetThreadBinding().has_value())
+      in_parallel_ -= 1;
     return ret;
   }
 
diff --git a/src/tirx/transform/storage_rewrite.cc 
b/src/tirx/transform/storage_rewrite.cc
index 7c0ad7a6d1..a4023ec519 100644
--- a/src/tirx/transform/storage_rewrite.cc
+++ b/src/tirx/transform/storage_rewrite.cc
@@ -688,7 +688,7 @@ class StoragePlanRewriter : public StmtExprMutator {
           StmtExprMutator::Mutate_(op, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(op));
       op = stmt.as<ForNode>();
       return For(op->loop_var, op->min, op->extent, op->kind, MakeAttach(svec, 
op->body),
-                 op->thread_binding, op->annotations, op->step);
+                 op->annotations, op->step);
     } else {
       return StmtExprMutator::Mutate_(op, inplace_mode);
     }
@@ -1161,7 +1161,7 @@ class StoragePlanRewriter : public StmtExprMutator {
         PlanNewScope(s.stmt);
       } else if (s.stmt->IsInstance<ForNode>()) {
         const auto* op = static_cast<const ForNode*>(s.stmt);
-        if (op->kind == ForKind::kParallel) {
+        if (op->kind == ForKind::kParallel && 
!op->GetThreadBinding().has_value()) {
           if (thread_scope_ == nullptr || thread_scope_ == op) {
             PlanNewScope(op);
           }
diff --git a/src/tirx/transform/unroll_loop.cc 
b/src/tirx/transform/unroll_loop.cc
index 044b229ec6..0465e64aa3 100644
--- a/src/tirx/transform/unroll_loop.cc
+++ b/src/tirx/transform/unroll_loop.cc
@@ -148,9 +148,14 @@ class LoopUnroller : public StmtExprMutator {
       node->annotations.erase(attr::unroll_explicit);
       op = node;
     }
+    if (op->GetThreadBinding().has_value()) {
+      // Explicit thread placement must survive until thread-binding lowering.
+      normal_loop_depth_ += 1;
+      return result;
+    }
     int value = GetExtent(op);
     // condition for auto unroll
-    bool auto_unroll = (op->kind == ForKind::kSerial && value >= 0 && 
normal_loop_depth_ == 0 &&
+    bool auto_unroll = (op->kind == ForKind::kDefault && value >= 0 && 
normal_loop_depth_ == 0 &&
                         unroll_depth_ <= auto_max_depth_);
 
     const auto& policy = unroll_policy_.Current();
diff --git a/src/tirx/transform/vectorize_loop.cc 
b/src/tirx/transform/vectorize_loop.cc
index 46ba771923..933c6a2b29 100644
--- a/src/tirx/transform/vectorize_loop.cc
+++ b/src/tirx/transform/vectorize_loop.cc
@@ -1166,7 +1166,7 @@ class Vectorizer : public StmtExprMutator {
     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_, 
ForKind::kSerial, stmt);
+    return For(idx.as_or_throw<PrimVar>(), IntImm(var_ty, 0), var_lanes_, 
ForKind::kDefault, stmt);
   }
 
  private:
@@ -1371,9 +1371,9 @@ class LoopVectorizer : public StmtExprMutator {
     Stmt body = substituter->Mutate(op->body).ValueOrUnchanged(op->body);
     Stmt guarded_body = IfThenElse(index < fixed_extent, body, std::nullopt, 
op->span);
     Stmt vector_loop = For(inner, IntImm(lane_dtype, 0), scalable_lanes, 
ForKind::kVectorized,
-                           guarded_body, std::nullopt, op->annotations, 
std::nullopt, op->span);
-    Stmt loop = For(outer, zero, num_chunks, ForKind::kSerial, vector_loop, 
std::nullopt, {},
-                    std::nullopt, op->span);
+                           guarded_body, op->annotations, std::nullopt, 
op->span);
+    Stmt loop =
+        For(outer, zero, num_chunks, ForKind::kDefault, vector_loop, {}, 
std::nullopt, op->span);
 
     return this->Mutate(loop, InplaceMode::kDisallow).ValueOrUnchanged(loop);
   }
@@ -1393,7 +1393,7 @@ class VectorizeSkipper : public StmtExprMutator {
     Stmt stmt = StmtExprMutator::Mutate_(op, 
inplace_mode).ValueOrUnchanged(ffi::GetRef<Stmt>(op));
     op = stmt.as<ForNode>();
     if (op->kind == ForKind::kVectorized) {
-      return For(op->loop_var, op->min, op->extent, ForKind::kSerial, 
op->body);
+      return For(op->loop_var, op->min, op->extent, ForKind::kDefault, 
op->body);
     } else {
       return stmt;
     }
diff --git a/tests/python/ir/test_nonnullable_ir.py 
b/tests/python/ir/test_nonnullable_ir.py
index e159c45697..435570b867 100644
--- a/tests/python/ir/test_nonnullable_ir.py
+++ b/tests/python/ir/test_nonnullable_ir.py
@@ -45,7 +45,7 @@ def test_optional_statement_fields_and_roundtrip():
     body = tvm.tirx.Evaluate(0)
     conditional = tvm.tirx.IfThenElse(True, body, None)
     assert conditional.else_case is None
-    loop = tvm.tirx.For(tvm.tirx.Var("i", "int32"), 0, 4, 
tvm.tirx.ForKind.SERIAL, body, step=None)
+    loop = tvm.tirx.For(tvm.tirx.Var("i", "int32"), 0, 4, 
tvm.tirx.ForKind.DEFAULT, body, step=None)
     assert loop.step is None
     declaration = tvm.tirx.Function([], None)
     assert declaration.body is None
diff --git a/tests/python/relax/test_pipeline.py 
b/tests/python/relax/test_pipeline.py
index 69eda6256b..a27caa23e7 100644
--- a/tests/python/relax/test_pipeline.py
+++ b/tests/python/relax/test_pipeline.py
@@ -174,7 +174,7 @@ def _has_thread_binding(func: tvm.tirx.Function) -> bool:
 
     def _visit(node):
         nonlocal found
-        if isinstance(node, tvm.tirx.For) and node.kind == 
tvm.tirx.ForKind.THREAD_BINDING:
+        if isinstance(node, tvm.tirx.For) and 
node.annotations.get("thread_binding") is not None:
             found = True
 
     tvm_ffi.structural_walk(func.body, _visit)
diff --git a/tests/python/runtime/test_runtime_module_load.py 
b/tests/python/runtime/test_runtime_module_load.py
index 6b3f85e839..4daabf11da 100644
--- a/tests/python/runtime/test_runtime_module_load.py
+++ b/tests/python/runtime/test_runtime_module_load.py
@@ -58,7 +58,7 @@ def test_dso_module_load():
             i,
             0,
             n - 1,
-            tvm.tirx.ForKind.SERIAL,
+            tvm.tirx.ForKind.DEFAULT,
             tvm.tirx.TensorStore(Ab, tvm.tirx.TensorLoad(Ab, [i]) + 1, [i + 
1]),
         )
         mod = tvm.IRModule.from_expr(
diff --git 
a/tests/python/s_tir/analysis/test_s_tir_analysis_verify_block_scope.py 
b/tests/python/s_tir/analysis/test_s_tir_analysis_verify_block_scope.py
index 0d0d7daabf..0272df8a3f 100644
--- a/tests/python/s_tir/analysis/test_s_tir_analysis_verify_block_scope.py
+++ b/tests/python/s_tir/analysis/test_s_tir_analysis_verify_block_scope.py
@@ -142,7 +142,7 @@ def test_error_undeclared_buffer_in_schedulable_tir():
 
     function = tvm.tirx.Function(
         params=[A, B_data],
-        body=tvm.tirx.For(i, 0, n, tvm.tirx.ForKind.SERIAL, block_realize),
+        body=tvm.tirx.For(i, 0, n, tvm.tirx.ForKind.DEFAULT, block_realize),
         # Note: B is NOT a function parameter, so its declaration scope is only
         # within a DeclTensor node (which we intentionally omit here).
     )
diff --git a/tests/python/s_tir/analysis/test_sblock_access_region.py 
b/tests/python/s_tir/analysis/test_sblock_access_region.py
index 5e7a9d6e77..9668845868 100644
--- a/tests/python/s_tir/analysis/test_sblock_access_region.py
+++ b/tests/python/s_tir/analysis/test_sblock_access_region.py
@@ -506,7 +506,7 @@ def test_conditional_inequality_access_regions(case):
         ]
     )
     for var, (minimum, extent) in reversed(list(zip(variables, domains))):
-        body = tirx.For(var, minimum, extent, tirx.ForKind.SERIAL, body)
+        body = tirx.For(var, minimum, extent, tirx.ForKind.DEFAULT, body)
     block = s_tir.SBlock([], [], [], "conditional", body)
     # Unbounded access sets conservatively cover the whole buffer.
     outside_expected = [(0, 256)] if case == "unbounded" else domains
diff --git a/tests/python/s_tir/schedule/test_tir_schedule_analysis.py 
b/tests/python/s_tir/schedule/test_tir_schedule_analysis.py
index 4be08f067a..c13a481b89 100644
--- a/tests/python/s_tir/schedule/test_tir_schedule_analysis.py
+++ b/tests/python/s_tir/schedule/test_tir_schedule_analysis.py
@@ -63,7 +63,7 @@ def _make_loops(loop_vars: list[Var], extents: list[int]) -> 
list[For]:
             loop_var=loop_var,
             min=0,
             extent=extent,
-            kind=ForKind.SERIAL,
+            kind=ForKind.DEFAULT,
             body=Evaluate(0),
         )
         for loop_var, extent in zip(loop_vars, extents)
diff --git a/tests/python/s_tir/schedule/test_tir_schedule_state.py 
b/tests/python/s_tir/schedule/test_tir_schedule_state.py
index f74340afdb..9ed2c6ce04 100644
--- a/tests/python/s_tir/schedule/test_tir_schedule_state.py
+++ b/tests/python/s_tir/schedule/test_tir_schedule_state.py
@@ -338,9 +338,8 @@ def test_replace_block_in_opaque_block():
         loop_var=for_loop.loop_var,
         min=0,
         extent=128,
-        kind=tirx.ForKind.SERIAL,
+        kind=tirx.ForKind.DEFAULT,
         body=tirx.Evaluate(0),
-        thread_binding=None,
         annotations=None,
     )
     s.replace(sref, new_for_loop)
diff --git a/tests/python/s_tir/script/test_s_tir_script_basic_usage.py 
b/tests/python/s_tir/script/test_s_tir_script_basic_usage.py
index 7d8c32346b..207dddf504 100644
--- a/tests/python/s_tir/script/test_s_tir_script_basic_usage.py
+++ b/tests/python/s_tir/script/test_s_tir_script_basic_usage.py
@@ -470,11 +470,11 @@ def test_for_thread_binding():
     tvm.ir.assert_structural_equal(func, rt_func)
 
     assert isinstance(rt_func.body[0], tirx.stmt.For)
-    assert rt_func.body[0].kind == 4
-    assert rt_func.body[0].thread_binding == "threadIdx.x"
+    assert rt_func.body[0].kind == tvm.tirx.ForKind.PARALLEL
+    assert rt_func.body[0].annotations.get("thread_binding") == "threadIdx.x"
     assert isinstance(rt_func.body[0].body[0], tirx.stmt.For)
-    assert rt_func.body[0].body[0].kind == 4
-    assert rt_func.body[0].body[0].thread_binding == "threadIdx.y"
+    assert rt_func.body[0].body[0].kind == tvm.tirx.ForKind.PARALLEL
+    assert rt_func.body[0].body[0].annotations.get("thread_binding") == 
"threadIdx.y"
     assert rt_func.body[0].body[0].annotations["attr_key"] == "attr_value"
 
 
diff --git a/tests/python/tirx-analysis/test_tir_analysis_verify_well_formed.py 
b/tests/python/tirx-analysis/test_tir_analysis_verify_well_formed.py
index bbd149088e..45b78d6746 100644
--- a/tests/python/tirx-analysis/test_tir_analysis_verify_well_formed.py
+++ b/tests/python/tirx-analysis/test_tir_analysis_verify_well_formed.py
@@ -58,7 +58,7 @@ def test_error_for_out_of_scope_usage():
         tvm.tirx.Var("j", "int32"),
         0,
         1,
-        tvm.tirx.ForKind.SERIAL,
+        tvm.tirx.ForKind.DEFAULT,
         tvm.tirx.SeqStmt([tvm.tirx.Bind(i, 42), tvm.tirx.Evaluate(i)]),
     )
     # Use i outside the For loop — this is out of scope
diff --git a/tests/python/tirx-base/test_tir_constructor.py 
b/tests/python/tirx-base/test_tir_constructor.py
index b9be658e28..20d45c6056 100644
--- a/tests/python/tirx-base/test_tir_constructor.py
+++ b/tests/python/tirx-base/test_tir_constructor.py
@@ -306,7 +306,7 @@ def test_stmt_constructor():
     assert len(x.message_parts) == 1
     assert x.message_parts[0].value == "hellow"
 
-    x = tvm.tirx.For(tvm.tirx.Var("x", "int32"), 0, 10, 
tvm.tirx.ForKind.SERIAL, nop)
+    x = tvm.tirx.For(tvm.tirx.Var("x", "int32"), 0, 10, 
tvm.tirx.ForKind.DEFAULT, nop)
     assert isinstance(x, tvm.tirx.For)
     assert x.min.value == 0
     assert x.extent.value == 10
diff --git a/tests/python/tirx-base/test_tir_nodes.py 
b/tests/python/tirx-base/test_tir_nodes.py
index 5c212e1ff1..0a49ae7ddf 100644
--- a/tests/python/tirx-base/test_tir_nodes.py
+++ b/tests/python/tirx-base/test_tir_nodes.py
@@ -129,7 +129,7 @@ def test_basic():
 
 def test_stmt():
     x = tvm.tirx.Evaluate(0)
-    tvm.tirx.For(tvm.tirx.Var("i", "int32"), 0, 1, tvm.tirx.ForKind.SERIAL, x)
+    tvm.tirx.For(tvm.tirx.Var("i", "int32"), 0, 1, tvm.tirx.ForKind.DEFAULT, x)
     tvm.tirx.For(tvm.tirx.Var("i", "int32"), 0, 1, tvm.tirx.ForKind.UNROLLED, 
x, step=2)
 
 
diff --git 
a/tests/python/tirx-transform/test_tir_transform_common_subexpr_elim.py 
b/tests/python/tirx-transform/test_tir_transform_common_subexpr_elim.py
index 010fc533fe..e0b2895d82 100644
--- a/tests/python/tirx-transform/test_tir_transform_common_subexpr_elim.py
+++ b/tests/python/tirx-transform/test_tir_transform_common_subexpr_elim.py
@@ -596,7 +596,7 @@ def test_let_body_no_extraction():
         i,
         tvm.tirx.const(0, "int32"),
         tvm.tirx.const(10, "int32"),
-        tvm.tirx.ForKind.SERIAL,
+        tvm.tirx.ForKind.DEFAULT,
         store,
     )
     func = tvm.tirx.Function([buf, y], loop)
@@ -626,7 +626,7 @@ def test_let_value_cse():
         i,
         tvm.tirx.const(0, "int32"),
         tvm.tirx.const(10, "int32"),
-        tvm.tirx.ForKind.SERIAL,
+        tvm.tirx.ForKind.DEFAULT,
         store,
     )
     func = tvm.tirx.Function([buf, y, z], loop)
@@ -659,7 +659,7 @@ def test_nested_let_no_extraction():
         i,
         tvm.tirx.const(0, "int32"),
         tvm.tirx.const(10, "int32"),
-        tvm.tirx.ForKind.SERIAL,
+        tvm.tirx.ForKind.DEFAULT,
         store,
     )
     func = tvm.tirx.Function([buf, z], loop)
@@ -702,7 +702,7 @@ def test_let_floordiv_pattern():
         i,
         tvm.tirx.const(0, "int32"),
         tvm.tirx.const(10, "int32"),
-        tvm.tirx.ForKind.SERIAL,
+        tvm.tirx.ForKind.DEFAULT,
         store,
     )
     func = tvm.tirx.Function([buf_a, buf_b, buf_c], loop)
diff --git a/tests/python/tirx-transform/test_tir_transform_convert_ssa.py 
b/tests/python/tirx-transform/test_tir_transform_convert_ssa.py
index 37b2d64237..adbb983b61 100644
--- a/tests/python/tirx-transform/test_tir_transform_convert_ssa.py
+++ b/tests/python/tirx-transform/test_tir_transform_convert_ssa.py
@@ -510,7 +510,7 @@ def test_reused_loop_var_in_decl_buffer_elem_offset():
         loop_var,
         0,
         128,
-        tirx.ForKind.SERIAL,
+        tirx.ForKind.DEFAULT,
         tirx.SeqStmt(
             [
                 tirx.Bind(
diff --git a/tests/python/tirx-transform/test_tir_transform_lower_intrin.py 
b/tests/python/tirx-transform/test_tir_transform_lower_intrin.py
index 064d2c3999..2b451a768b 100644
--- a/tests/python/tirx-transform/test_tir_transform_lower_intrin.py
+++ b/tests/python/tirx-transform/test_tir_transform_lower_intrin.py
@@ -78,7 +78,7 @@ def check_value(expr, variables, data, fref):
         loop_var,
         tvm.tirx.const(0, "int32"),
         tvm.tirx.const(n, "int32"),
-        tvm.tirx.ForKind.SERIAL,
+        tvm.tirx.ForKind.DEFAULT,
         make_store(loop_var),
     )
 
diff --git a/tests/python/tirx-transform/test_tir_transform_remove_no_op.py 
b/tests/python/tirx-transform/test_tir_transform_remove_no_op.py
index ccdf8df88a..6f3409ca85 100644
--- a/tests/python/tirx-transform/test_tir_transform_remove_no_op.py
+++ b/tests/python/tirx-transform/test_tir_transform_remove_no_op.py
@@ -38,17 +38,17 @@ def test_remove_no_op():
         i,
         0,
         4,
-        tvm.tirx.ForKind.SERIAL,
+        tvm.tirx.ForKind.DEFAULT,
         tvm.tirx.For(
             j,
             0,
             n,
-            tvm.tirx.ForKind.SERIAL,
+            tvm.tirx.ForKind.DEFAULT,
             tvm.tirx.For(
                 k,
                 0,
                 m,
-                tvm.tirx.ForKind.SERIAL,
+                tvm.tirx.ForKind.DEFAULT,
                 tvm.tirx.IfThenElse(
                     (i * m + j + k < n), tvm.tirx.Evaluate(m), 
tvm.tirx.Evaluate(n)
                 ),
@@ -68,7 +68,7 @@ def test_remove_no_op():
     assert len(ret) == 1 and ret[0] == store
 
     # remove zero extent loop
-    stmt3 = tvm.tirx.For(i, 0, 0, tvm.tirx.ForKind.SERIAL, store)
+    stmt3 = tvm.tirx.For(i, 0, 0, tvm.tirx.ForKind.DEFAULT, store)
     mod = tvm.IRModule.from_expr(tvm.tirx.Function([Ab], stmt3))
     ret = tvm.tirx.transform.RemoveNoOp()(mod)["main"].body
     assert isinstance(ret, tvm.tirx.SeqStmt) and len(ret) == 0
diff --git a/tests/python/tirx/script/test_tirx_script_basic_usage.py 
b/tests/python/tirx/script/test_tirx_script_basic_usage.py
index cd8c85a166..7ac1c50d7e 100644
--- a/tests/python/tirx/script/test_tirx_script_basic_usage.py
+++ b/tests/python/tirx/script/test_tirx_script_basic_usage.py
@@ -929,7 +929,7 @@ def test_thread_binding_has_no_dtype_parameter():
 def test_hand_built_for_promotes_int_literal_bounds_to_uint32():
     """The For constructor retypes literal bounds to the loop var's dtype."""
     loop_var = tvm.tirx.Var("i", "uint32")
-    loop = tvm.tirx.For(loop_var, 0, 128, tvm.tirx.ForKind.SERIAL, 
tvm.tirx.Evaluate(0))
+    loop = tvm.tirx.For(loop_var, 0, 128, tvm.tirx.ForKind.DEFAULT, 
tvm.tirx.Evaluate(0))
     assert loop.min.ty == PrimType("uint32")
     assert loop.extent.ty == PrimType("uint32")
 
@@ -937,4 +937,4 @@ def 
test_hand_built_for_promotes_int_literal_bounds_to_uint32():
 def test_hand_built_for_rejects_negative_literal_for_uint32():
     loop_var = tvm.tirx.Var("i", "uint32")
     with pytest.raises(Exception, match="not representable"):
-        tvm.tirx.For(loop_var, -1, 128, tvm.tirx.ForKind.SERIAL, 
tvm.tirx.Evaluate(0))
+        tvm.tirx.For(loop_var, -1, 128, tvm.tirx.ForKind.DEFAULT, 
tvm.tirx.Evaluate(0))
diff --git a/tests/python/tirx/script/test_tirx_script_ir_builder.py 
b/tests/python/tirx/script/test_tirx_script_ir_builder.py
index 219fe8cc0c..ff8f189197 100644
--- a/tests/python/tirx/script/test_tirx_script_ir_builder.py
+++ b/tests/python/tirx/script/test_tirx_script_ir_builder.py
@@ -57,9 +57,9 @@ def test_ir_builder_tir_for():
         loop_var=tirx.Var("", "int32"),
         min=0,
         extent=8,
-        kind=tirx.ForKind.THREAD_BINDING,
+        kind=tirx.ForKind.PARALLEL,
         body=tirx.Evaluate(0),
-        thread_binding="threadIdx.x",
+        annotations={"thread_binding": "threadIdx.x"},
     )
     unroll_expected = tirx.For(
         loop_var=tirx.Var("", "int32"),
@@ -86,7 +86,7 @@ def test_ir_builder_tir_for():
         loop_var=tirx.Var("", "int32"),
         min=0,
         extent=128,
-        kind=tirx.ForKind.SERIAL,
+        kind=tirx.ForKind.DEFAULT,
         body=parallel_expected,
     )
 
@@ -106,7 +106,7 @@ def test_ir_builder_tir_for_uint():
         loop_var=tirx.Var("", "uint32"),
         min=tirx.const(0, "uint32"),
         extent=tirx.const(128, "uint32"),
-        kind=tirx.ForKind.SERIAL,
+        kind=tirx.ForKind.DEFAULT,
         body=tirx.Evaluate(0),
     )
 

Reply via email to