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

lunderberg 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 eb15d04c3b [TIR] In SplitHostDevice, check for variables in thread 
extents (#16250)
eb15d04c3b is described below

commit eb15d04c3bff76062e26d5647fb8af0323de1bed
Author: Eric Lunderberg <[email protected]>
AuthorDate: Wed Jan 3 08:03:24 2024 -0600

    [TIR] In SplitHostDevice, check for variables in thread extents (#16250)
    
    * [TIR] In SplitHostDevice, check for variables in thread extents
    
    Otherwise, they would be undefined after being de-duplicated by
    `ConvertSSA`.
    
    * Revert #16236
    
    The buf reported in #16237 can be resolved by tracking variable usage
    in a thread extent.
    
    * lint fixes
    
    * Update TIR well-formed checker for env thread SSA requirements
    
    Environment threads must reuse the same `tir::Var` across all
    `AttrStmt` instances in a `PrimFunc`, but must not reuse across
    separate `PrimFunc`s in an `IRModule`.
    
    * Update ConvertSSA to handle environment threads' SSA requirements
    
    * lint fix
    
    * Updated docstrings for VerifyWellFormed
    
    * Rely on script.Complete for read/writes
    
    Avoids issue in cortexm unit tests resulting from read/write
    annotations being present in the root block, followed by application
    of BindParams.
    
    * Typo fix
    
    * Added structural equal comparison in unit test
---
 include/tvm/tir/analysis.h                         |  29 +-
 src/te/operation/create_primfunc.cc                |  19 +-
 src/tir/analysis/verify_well_formed.cc             | 214 ++++++++++
 src/tir/ir/tir_visitor_with_path.cc                | 434 +++++++++++++++++++++
 src/tir/ir/tir_visitor_with_path.h                 | 210 ++++++++++
 src/tir/transforms/ir_utils.cc                     |  72 +++-
 src/tir/transforms/split_host_device.cc            |   2 +-
 .../test_tir_analysis_verify_well_formed.py        | 148 ++++++-
 .../test_tir_transform_convert_ssa.py              | 216 ++++++++++
 .../test_tir_transform_split_host_device.py        |  72 ++++
 10 files changed, 1383 insertions(+), 33 deletions(-)

diff --git a/include/tvm/tir/analysis.h b/include/tvm/tir/analysis.h
index 701e2a5143..c4ae5d573b 100644
--- a/include/tvm/tir/analysis.h
+++ b/include/tvm/tir/analysis.h
@@ -307,13 +307,40 @@ TVM_DLL Map<Buffer, Optional<Stmt>> 
DetectBufferAccessLCA(const PrimFunc& func);
 
 /*!
  * \brief Verify if the given TIR is well-formed. The verification includes:
- *        - Check if expressions not contain vars that is defined outside the 
block.
+ *
+ * - All variables are defined prior to their point of use.
+ *
+ * - No variables are used outside of the scope of their definition.
+ *
+ * - Each variable has a single point of definition.
+ *
+ * - Expressions within a tir::Block may not reference variables
+ *   defined outside the block.  For example, for a block with iter
+ *   vars `vi, vj = T.axis.remap('SS', [i,j])`, the statement
+ *   `B[i,j] = A[i,j]` would be ill-formed, because it uses the loop
+ *   variables `i` and `j` instead of the block variables `vi` and
+ *   `vj`.
+ *
  * \param func The PrimFunc to be verified.
  * \param assert_mode The indicator if it raises an error when the function is 
not well-formed.
  * \return Whether it is a well-formed TIR function.
  */
 TVM_DLL bool VerifyWellFormed(const PrimFunc& func, bool assert_mode = true);
 
+/*!
+ * \brief Verify if the TIR in the given IRMOdule is well-formed.
+ *
+ * In addition to the checks performed for each PrimFunc (see above),
+ * the following checks are performed:
+ *
+ * - The same TIR variable may not be defined in more than one function
+ *
+ * \param mod The IRModule to be verified.
+ * \param assert_mode The indicator if it raises an error when the function is 
not well-formed.
+ * \return Whether it is a well-formed TIR module.
+ */
+TVM_DLL bool VerifyWellFormed(const IRModule& mod, bool assert_mode = true);
+
 /*!
  * \brief Find the entry function of the given IRModule, i.e, functions marked 
by
  * `tir::attr::kIsEntryFunc`, whose name is `main` or being the only PrimeFunc.
diff --git a/src/te/operation/create_primfunc.cc 
b/src/te/operation/create_primfunc.cc
index dc0b1fbfb8..d862d9f676 100644
--- a/src/te/operation/create_primfunc.cc
+++ b/src/te/operation/create_primfunc.cc
@@ -424,15 +424,12 @@ Stmt GenerateStmtFromExternOp(const te::ExternOp& 
extern_op, CreateFuncInfo* inf
     }
   }
 
-  // Step 3. Collect Access Region
-  Array<BufferRegion> reads, writes;
-  for (const te::Tensor& tensor : extern_op->inputs) {
-    // We have ICHECK before so it is not needed here.
-    reads.push_back(BufferRegion::FullRegion(info->tensor2buffers[tensor]));
-  }
-  for (const Buffer& buffer : extern_op->output_placeholders) {
-    writes.push_back(BufferRegion::FullRegion(buffer));
-  }
+  // The access region does not need to be collected here, as it will
+  // be generated with the later application of "script.Complete" in
+  // GenerateAndCompletePrimFunc.  Waiting until later also handles
+  // the case where there is only a single BlockNode, which then
+  // becomes the root Block of the function, and should not have
+  // reads/writes filled in.
 
   BufferSubstituter substituter(var_map, input_buffer_map);
   Stmt body = substituter(extern_op->body);
@@ -442,8 +439,8 @@ Stmt GenerateStmtFromExternOp(const te::ExternOp& 
extern_op, CreateFuncInfo* inf
                       /*predicate=*/Bool(true),
                       /*block=*/
                       Block(/*iter_vars=*/{},
-                            /*reads=*/std::move(reads),
-                            /*writes=*/std::move(writes),
+                            /*reads=*/{},
+                            /*writes=*/{},
                             /*name_hint=*/info->FreshName(extern_op->name),
                             /*body=*/std::move(body),
                             /*init=*/NullOpt,
diff --git a/src/tir/analysis/verify_well_formed.cc 
b/src/tir/analysis/verify_well_formed.cc
index 898183533c..58eadb20fa 100644
--- a/src/tir/analysis/verify_well_formed.cc
+++ b/src/tir/analysis/verify_well_formed.cc
@@ -26,12 +26,97 @@
 #include <tvm/tir/stmt.h>
 #include <tvm/tir/stmt_functor.h>
 
+#include <exception>
+#include <optional>
+#include <tuple>
+#include <variant>
+
 #include "../ir/functor_common.h"
+#include "../ir/tir_visitor_with_path.h"
 #include "tvm/ir/module.h"
 
 namespace tvm {
 namespace tir {
 
+namespace {
+
+template <typename DerivedVerifier>
+class Verifier : protected TIRVisitorWithPath {
+ public:
+  template <typename TirNodeRef>
+  static bool Verify(const TirNodeRef& node, bool assert_on_error) {
+    DerivedVerifier verifier(assert_on_error);
+    verifier(node);
+    return !verifier.has_error_;
+  }
+
+ protected:
+  explicit Verifier(bool assert_on_error) : assert_on_error_(assert_on_error) 
{}
+
+  /* \brief Helper class to handle the bool-or-assert handles
+   *
+   * Each verifier can either return a boolean, or assert on failure.
+   * To avoid needing to duplicate this logic at every step, the
+   * Verify() method can be used.  Similar to `LOG(FATAL)` or
+   * `LOG(DEBUG)`, it returns an object that can accept streamed
+   * context information.
+   *
+   * If the error should be raised, then the context is collected
+   * identically to `LOG(FATAL)`.  If a boolean is returned, or if the
+   * condition passes, then the streamed context is discarded.
+   *
+   * Usage:
+   *
+   *     Verify(value == expected_value)
+   *            << "ValueError: " << value
+   *            << " was not the expected value of " << expected_value;
+   */
+  class VerifyStream {
+   public:
+    explicit VerifyStream(bool log_fatal) {
+      if (log_fatal) {
+        log_.emplace();
+      }
+    }
+
+    VerifyStream(const VerifyStream&) = delete;
+    VerifyStream& operator=(const VerifyStream&) = delete;
+    VerifyStream(VerifyStream&& other) { std::swap(log_, other.log_); }
+    VerifyStream& operator=(VerifyStream&& other) {
+      std::swap(log_, other.log_);
+      return *this;
+    }
+
+    template <typename T>
+    VerifyStream& operator<<(T&& t) {
+      if (log_.has_value()) {
+        log_.value() << std::forward<T>(t);
+      }
+      return *this;
+    }
+
+    ~VerifyStream() noexcept(false) {
+      if (log_.has_value()) {
+        LOG(FATAL) << log_->str();
+      }
+    }
+
+    std::optional<std::ostringstream> log_{std::nullopt};
+  };
+
+  // TODO(Lunderberg): Add the filename/linenum with
+  // std::source_location when C++20 is available.
+  VerifyStream Verify(bool condition) {
+    has_error_ = has_error_ || !condition;
+    return VerifyStream(!condition && assert_on_error_);
+  }
+
+  bool assert_on_error_;
+  bool has_error_{false};
+};
+
+}  // namespace
+
 /*! \brief Verify all Expr inside the block does not contain:
  *    1. loop vars outside the current block.
  *    2. block vars of parent blocks.
@@ -135,10 +220,135 @@ class BlockVarAccessVerifier : public StmtExprVisitor {
   bool has_error_{false};
 };
 
+class UndefinedVarVerifier : public Verifier<UndefinedVarVerifier> {
+ public:
+  // Until templated-this arrives in C++23, the CRTP can't inject a
+  // constructor into the child class.  Therefore, must explicitly add
+  // the constructor.
+  using Verifier::Verifier;
+
+ private:
+  void Visit(const PrimFunc& prim_func, ObjectPath path) override {
+    Verifier::Visit(prim_func, path);
+    redefine_allowed_within_function_.clear();
+  }
+
+  void EnterDef(const IterVar& iter_var, ObjectPath path) override {
+    Verifier::EnterDef(iter_var, path);
+    if (iter_var->iter_type == IterVarType::kThreadIndex) {
+      redefine_allowed_within_function_.insert(iter_var->var);
+    }
+  }
+
+  void EnterDef(const Var& var, ObjectPath path) override {
+    bool redefine_is_allowed = redefine_allowed_within_function_.count(var);
+    {
+      auto it = currently_defined_.find(var);
+      Verify(it == currently_defined_.end() || redefine_is_allowed)
+          << "ValueError: "
+          << "TIR is ill-formed, "
+          << "due to multiple nested definitions of variable " << var
+          << ".  It was first defined at " << it->second << ", and was 
re-defined at " << path;
+    }
+
+    {
+      auto it = previously_defined_.find(var);
+      Verify(it == previously_defined_.end() || redefine_is_allowed)
+          << "ValueError: "
+          << "TIR is ill-formed, "
+          << "due to multiple definitions of variable " << var << ".  It was 
first defined at "
+          << it->second << ", and was later re-defined at " << path;
+    }
+
+    currently_defined_.insert({var, path});
+  }
+
+  void ExitDef(const Var& var, ObjectPath path) override {
+    auto active_def = currently_defined_.find(var);
+
+    currently_defined_.erase(active_def);
+    previously_defined_.insert({var, path});
+  }
+
+  void VisitExpr_(const VarNode* op, ObjectPath path) override {
+    auto var = GetRef<Var>(op);
+
+    auto active_def = currently_defined_.find(var);
+    auto verify = Verify(active_def != currently_defined_.end());
+    verify << "ValueError: "
+           << "Invalid use of undefined variable " << var << " at " << path << 
".";
+
+    // Check if there was a previous definition, and append the
+    // location to the error message if there was.  This is to aid in
+    // debugging, by distinguishing between a variable that is
+    // currently out-of-scope, and a variable that never had a
+    // definition in the first place.
+    if (auto prev_def = previously_defined_.find(var); prev_def != 
previously_defined_.end()) {
+      verify << ".  While this variable was previously defined at " << 
prev_def->second
+             << ", this definition is no longer in-scope.";
+    }
+  }
+
+  // Variables that are defined in the currently-visited scope.
+  std::unordered_map<Var, ObjectPath, ObjectPtrHash, ObjectPtrEqual> 
currently_defined_;
+
+  // Variables that were previously defined, and are now out of scope.
+  std::unordered_map<Var, ObjectPath, ObjectPtrHash, ObjectPtrEqual> 
previously_defined_;
+
+  // Special variables that are allowed to be re-defined, so long as
+  // that re-definition occurs within the same PrimFunc.  For example
+  std::unordered_set<Var, ObjectPtrHash, ObjectPtrEqual> 
redefine_allowed_within_function_;
+};
+
+/* \brief Verify unique tir::Var for each environment thread
+ *
+ * Environment threads, such as CUDA's `threadIdx.x`, are defined in
+ * TIR using an `AttrStmt` with the key `attr::thread_extent`.  A
+ * `PrimFunc` may contain multiple such attributes for the same
+ * environment thread.  However, all such attributes must use the same
+ * `tir::Var` for a given thread.
+ */
+class SingleEnvThreadVerifier : public Verifier<SingleEnvThreadVerifier> {
+ public:
+  using Verifier::Verifier;
+
+ private:
+  void Visit(const PrimFunc& prim_func, ObjectPath path) override {
+    Verifier::Visit(prim_func, path);
+    env_thread_vars_.clear();
+  }
+
+  void EnterDef(const IterVar& iter_var, ObjectPath path) override {
+    if (iter_var->iter_type == IterVarType::kThreadIndex) {
+      if (auto it = env_thread_vars_.find(iter_var->thread_tag); it != 
env_thread_vars_.end()) {
+        const auto& [prev_var, prev_path] = it->second;
+        Verify(prev_var.same_as(iter_var->var))
+            << "ValueError: "
+            << "PrimFunc uses multiple distinct TIR variables "
+            << " for the environment thread \"" << iter_var->thread_tag << 
"\".  "
+            << "While multiple tir::AttrStmt may define the same environment 
thread, "
+            << "all definitions within a single PrimFunc must share the same 
tir::Var.  "
+            << "Binding of environment thread \"" << iter_var->thread_tag
+            << "\" to the TIR variable " << iter_var->var << " at " << path
+            << " conflicts with the previous binding to the TIR variable " << 
prev_var << " at "
+            << path;
+      } else {
+        env_thread_vars_.insert({iter_var->thread_tag, {iter_var->var, path}});
+      }
+    }
+  }
+
+  std::unordered_map<String, std::tuple<Var, ObjectPath>> env_thread_vars_;
+};
+
 bool VerifyWellFormed(const PrimFunc& func, bool assert_mode) {
   if (!BlockVarAccessVerifier::Verify(func, assert_mode)) {
     return false;
   }
+
+  if (!UndefinedVarVerifier::Verify(func, assert_mode)) return false;
+  if (!SingleEnvThreadVerifier::Verify(func, assert_mode)) return false;
+
   // TODO(Siyuan): add more checks here.
   return true;
 }
@@ -152,6 +362,10 @@ bool VerifyWellFormed(const IRModule& mod, bool 
assert_mode) {
       }
     }
   }
+
+  if (!UndefinedVarVerifier::Verify(mod, assert_mode)) return false;
+  if (!SingleEnvThreadVerifier::Verify(mod, assert_mode)) return false;
+
   return true;
 }
 
diff --git a/src/tir/ir/tir_visitor_with_path.cc 
b/src/tir/ir/tir_visitor_with_path.cc
new file mode 100644
index 0000000000..50c8b8f525
--- /dev/null
+++ b/src/tir/ir/tir_visitor_with_path.cc
@@ -0,0 +1,434 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file tir/ir/tir_visitor_with_path.cc
+ * \brief Provide a TIR visitor that tracks the current location
+ */
+
+#include "tir_visitor_with_path.h"
+
+#include <algorithm>
+#include <optional>
+#include <unordered_set>
+#include <variant>
+#include <vector>
+
+namespace tvm {
+namespace tir {
+
+void TIRVisitorWithPath::Visit(const IRModule& mod, ObjectPath path) {
+  // To ensure deterministic order of visits, sort the GlobalVar first
+  // by visibility (public then private), then alphabetically by name.
+  std::vector<GlobalVar> gvars;
+  std::unordered_set<GlobalVar, ObjectPtrHash, ObjectPtrEqual> 
externally_exposed;
+  for (const auto& [gvar, func] : mod->functions) {
+    gvars.push_back(gvar);
+    if (func->GetAttr<String>(tvm::attr::kGlobalSymbol).defined()) {
+      externally_exposed.insert(gvar);
+    }
+  }
+
+  std::sort(gvars.begin(), gvars.end(),
+            [&externally_exposed](const GlobalVar& a, const GlobalVar& b) {
+              bool a_exposed = externally_exposed.count(a);
+              bool b_exposed = externally_exposed.count(b);
+              if (a_exposed != b_exposed) {
+                return a < b;
+              } else {
+                return a->name_hint < b->name_hint;
+              }
+            });
+
+  std::vector<DefContext<GlobalVar>> context;
+
+  for (const auto& gvar : gvars) {
+    context.push_back(WithDef(gvar, 
path->Attr("global_var_map_")->MapValue(gvar->name_hint)));
+  }
+
+  for (const auto& gvar : gvars) {
+    auto base_func = mod->functions[gvar];
+    if (auto prim_func = base_func.as<PrimFunc>()) {
+      Visit(prim_func.value(), path->Attr("functions")->MapValue(gvar));
+    }
+  }
+
+  while (context.size()) context.pop_back();
+}
+
+void TIRVisitorWithPath::Visit(const PrimFunc& func, ObjectPath path) {
+  // The implicit definitions from a PrimFunc::buffer_map are pretty
+  // weird.  They only apply if no previous definition of that
+  // variable has occurred.  Therefore, to ensure that we only avoid
+  // duplicate calls to VisitVarDef, these semantics need to be
+  // checked.
+  std::unordered_set<Var, ObjectPtrHash, ObjectPtrEqual> defined_params;
+  std::vector<std::variant<DefContext<Var>, DefContext<Buffer>>> context;
+
+  auto ppath = path->Attr("params");
+  for (size_t i = 0; i < func->params.size(); i++) {
+    context.push_back(WithDef(func->params[i], ppath->ArrayIndex(i)));
+    defined_params.insert(func->params[i]);
+  }
+
+  auto try_visit_implicit_var_def = [this, &defined_params, &context](const 
PrimExpr& expr,
+                                                                      
ObjectPath path) {
+    if (auto opt = expr.as<Var>()) {
+      auto var = opt.value();
+      if (!defined_params.count(var)) {
+        context.push_back(WithDef(var, path));
+        defined_params.insert(var);
+      }
+    }
+  };
+  auto try_visit_implicit_var_def_array = [&try_visit_implicit_var_def](const 
Array<PrimExpr>& arr,
+                                                                        
ObjectPath path) {
+    for (size_t i = 0; i < arr.size(); i++) {
+      try_visit_implicit_var_def(arr[i], path->ArrayIndex(i));
+    }
+  };
+
+  auto buffer_map_path = path->Attr("buffer_map");
+  for (size_t i = 0; i < func->params.size(); i++) {
+    if (auto opt = func->buffer_map.Get(func->params[i])) {
+      auto buf = opt.value();
+      auto buf_path = buffer_map_path->MapValue(ppath->ArrayIndex(i));
+
+      // A buffer in the buffer_map always defines its data pointer
+      context.push_back(WithDef(buf->data, buf_path->Attr("data")));
+
+      // But other implicit definitions only apply if they weren't
+      // provided as explicit parameters, and they weren't defined
+      // implicitly by any previous buffer.
+      try_visit_implicit_var_def_array(buf->shape, buf_path->Attr("shape"));
+      try_visit_implicit_var_def_array(buf->strides, 
buf_path->Attr("strides"));
+      try_visit_implicit_var_def(buf->elem_offset, 
buf_path->Attr("elem_offset"));
+    }
+  }
+
+  // Only after all the implicit definitions have been visited can we
+  // visit the buffer definition itself.
+  for (size_t i = 0; i < func->params.size(); i++) {
+    if (auto opt = func->buffer_map.Get(func->params[i])) {
+      auto buf_path = buffer_map_path->MapValue(ppath->ArrayIndex(i));
+      EnterDef(opt.value(), buf_path);
+    }
+  }
+
+  Visit(func->body, path->Attr("body"));
+
+  while (context.size()) context.pop_back();
+}
+
+void TIRVisitorWithPath::EnterDef(const IterVar& iter_var, ObjectPath path) {
+  if (iter_var->dom.defined()) {
+    Visit(iter_var->dom, path->Attr("dom"));
+  }
+  EnterDef(iter_var->var, path->Attr("var"));
+}
+
+void TIRVisitorWithPath::ExitDef(const IterVar& iter_var, ObjectPath path) {
+  ExitDef(iter_var->var, path->Attr("var"));
+}
+
+void TIRVisitorWithPath::EnterDef(const Buffer& buffer, ObjectPath path) {
+  // Defining a buffer counts as using all parameters in the buffer
+  // (e.g. shape/strides).
+  Visit(buffer->data, path->Attr("data"));
+  Visit(buffer->shape, path->Attr("shape"));
+  Visit(buffer->strides, path->Attr("strides"));
+  Visit(buffer->elem_offset, path->Attr("elem_offset"));
+}
+void TIRVisitorWithPath::ExitDef(const Buffer& buffer, ObjectPath path) {}
+
+void TIRVisitorWithPath::Visit(const Buffer& buffer, ObjectPath path) {
+  // Using a buffer *also* counts as using all parameters in the buffer.
+  Visit(buffer->data, path->Attr("data"));
+  Visit(buffer->shape, path->Attr("shape"));
+  Visit(buffer->strides, path->Attr("strides"));
+  Visit(buffer->elem_offset, path->Attr("elem_offset"));
+}
+
+void TIRVisitorWithPath::Visit(const BufferRegion& region, ObjectPath path) {
+  Visit(region->buffer, path->Attr("buffer"));
+  Visit(region->region, path->Attr("region"));
+}
+
+void TIRVisitorWithPath::Visit(const MatchBufferRegion& match, ObjectPath 
path) {
+  Visit(match->source, path->Attr("source"));
+
+  // MatchBufferRegion define the match->buffer, but do not own the
+  // body in which the match->buffer is defined.  Therefore, the
+  // definitions are handled in the BlockNode visitor.
+}
+
+void TIRVisitorWithPath::Visit(const IterVar& iter_var, ObjectPath path) {
+  if (iter_var->dom.defined()) {
+    Visit(iter_var->dom, path->Attr("dom"));
+  }
+  Visit(iter_var->var, path->Attr("var"));
+}
+
+void TIRVisitorWithPath::Visit(const Range& range, ObjectPath path) {
+  Visit(range->min, path->Attr("min"));
+  Visit(range->extent, path->Attr("extent"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const LetStmtNode* op, ObjectPath path) {
+  Visit(op->value, path->Attr("value"));
+  auto context = WithDef(op->var, path->Attr("var"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const AttrStmtNode* op, ObjectPath path) {
+  Visit(op->value, path->Attr("value"));
+
+  std::optional<DefContext<IterVar>> context = std::nullopt;
+  if (auto iter_var = op->node.as<IterVar>();
+      iter_var && (op->attr_key == attr::thread_extent || op->attr_key == 
attr::virtual_thread)) {
+    // Some attributes serve as a source of definition for the
+    // tir::Var they annotate.
+    context = WithDef(iter_var.value(), path->Attr("node"));
+  } else if (auto expr = op->node.as<PrimExpr>()) {
+    Visit(expr.value(), path->Attr("node"));
+  }
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const ForNode* op, ObjectPath path) {
+  Visit(op->min, path->Attr("min"));
+  Visit(op->extent, path->Attr("extent"));
+  auto context = WithDef(op->loop_var, path->Attr("loop_var"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const WhileNode* op, ObjectPath path) {
+  Visit(op->condition, path->Attr("condition"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const AllocateNode* op, ObjectPath path) {
+  Visit(op->condition, path->Attr("condition"));
+  Visit(op->extents, path->Attr("extents"));
+  auto context = WithDef(op->buffer_var, path->Attr("buffer_var"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const AllocateConstNode* op, ObjectPath 
path) {
+  Visit(op->extents, path->Attr("extents"));
+  auto context = WithDef(op->buffer_var, path->Attr("buffer_var"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const DeclBufferNode* op, ObjectPath path) 
{
+  auto context = WithDef(op->buffer, path->Attr("buffer"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const BufferStoreNode* op, ObjectPath 
path) {
+  Visit(op->value, path->Attr("value"));
+  Visit(op->buffer, path->Attr("buffer"));
+  Visit(op->indices, path->Attr("indices"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const BufferRealizeNode* op, ObjectPath 
path) {
+  Visit(op->condition, path->Attr("condition"));
+  Visit(op->bounds, path->Attr("bounds"));
+  auto context = WithDef(op->buffer, path->Attr("buffer"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const IfThenElseNode* op, ObjectPath path) 
{
+  Visit(op->condition, path->Attr("condition"));
+  Visit(op->then_case, path->Attr("then_case"));
+  Visit(op->else_case, path->Attr("else_case"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const AssertStmtNode* op, ObjectPath path) 
{
+  Visit(op->condition, path->Attr("condition"));
+  Visit(op->message, path->Attr("message"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const ProducerStoreNode* op, ObjectPath 
path) {
+  Visit(op->indices, path->Attr("indices"));
+  Visit(op->value, path->Attr("value"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const ProducerRealizeNode* op, ObjectPath 
path) {
+  Visit(op->bounds, path->Attr("bounds"));
+  Visit(op->body, path->Attr("body"));
+  Visit(op->condition, path->Attr("condition"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const PrefetchNode* op, ObjectPath path) {
+  Visit(op->bounds, path->Attr("bounds"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const SeqStmtNode* op, ObjectPath path) {
+  Visit(op->seq, path->Attr("seq"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const EvaluateNode* op, ObjectPath path) {
+  Visit(op->value, path->Attr("value"));
+}
+
+void TIRVisitorWithPath::VisitStmt_(const BlockNode* op, ObjectPath path) {
+  std::vector<std::variant<DefContext<Var>, DefContext<IterVar>, 
DefContext<Buffer>>> context;
+
+  {
+    auto iter_path = path->Attr("iter_vars");
+    for (size_t i = 0; i < op->iter_vars.size(); i++) {
+      context.push_back(WithDef(op->iter_vars[i], iter_path->ArrayIndex(i)));
+    }
+  }
+  Visit(op->reads, path->Attr("reads"));
+  Visit(op->writes, path->Attr("writes"));
+
+  {
+    auto alloc_path = path->Attr("alloc_buffers");
+    for (size_t i = 0; i < op->alloc_buffers.size(); i++) {
+      auto buffer_path = alloc_path->ArrayIndex(i);
+      auto buf = op->alloc_buffers[i];
+      context.push_back(WithDef(buf->data, buffer_path->Attr("data")));
+      context.push_back(WithDef(buf, buffer_path));
+    }
+  }
+
+  {
+    auto match_path = path->Attr("match_buffers");
+    Visit(op->match_buffers, match_path);
+
+    for (size_t i = 0; i < op->match_buffers.size(); i++) {
+      auto buf = op->match_buffers[i]->buffer;
+      auto buffer_path = match_path->ArrayIndex(i)->Attr("buffer");
+      context.push_back(WithDef(buf->data, buffer_path->Attr("data")));
+      context.push_back(WithDef(buf, buffer_path));
+    }
+  }
+
+  Visit(op->init, path->Attr("init"));
+  Visit(op->body, path->Attr("body"));
+
+  while (context.size()) context.pop_back();
+}
+
+void TIRVisitorWithPath::VisitStmt_(const BlockRealizeNode* op, ObjectPath 
path) {
+  Visit(op->iter_values, path->Attr("iter_values"));
+  Visit(op->predicate, path->Attr("predicate"));
+  Visit(op->block, path->Attr("block"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const VarNode* op, ObjectPath path) {}
+
+void TIRVisitorWithPath::VisitExpr_(const SizeVarNode* op, ObjectPath path) {
+  VisitExpr_(static_cast<const VarNode*>(op), path);
+}
+
+void TIRVisitorWithPath::VisitExpr_(const AnyNode* op, ObjectPath path) {}
+
+void TIRVisitorWithPath::VisitExpr_(const BufferLoadNode* op, ObjectPath path) 
{
+  Visit(op->buffer, path->Attr("buffer"));
+  Visit(op->indices, path->Attr("indices"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const ProducerLoadNode* op, ObjectPath 
path) {
+  Visit(op->indices, path->Attr("indices"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const LetNode* op, ObjectPath path) {
+  Visit(op->value, path->Attr("value"));
+  auto context = WithDef(op->var, path->Attr("var"));
+  Visit(op->body, path->Attr("body"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const CallNode* op, ObjectPath path) {
+  if (auto gvar = op->op.as<GlobalVar>()) {
+    Visit(gvar.value(), path->Attr("op"));
+  }
+  Visit(op->args, path->Attr("args"));
+}
+
+#define DEFINE_BINOP_VISIT_(OP)                                        \
+  void TIRVisitorWithPath::VisitExpr_(const OP* op, ObjectPath path) { \
+    Visit(op->a, path->Attr("a"));                                     \
+    Visit(op->b, path->Attr("b"));                                     \
+  }
+
+DEFINE_BINOP_VISIT_(AddNode);
+DEFINE_BINOP_VISIT_(SubNode);
+DEFINE_BINOP_VISIT_(MulNode);
+DEFINE_BINOP_VISIT_(DivNode);
+DEFINE_BINOP_VISIT_(ModNode);
+DEFINE_BINOP_VISIT_(FloorDivNode);
+DEFINE_BINOP_VISIT_(FloorModNode);
+DEFINE_BINOP_VISIT_(MinNode);
+DEFINE_BINOP_VISIT_(MaxNode);
+DEFINE_BINOP_VISIT_(EQNode);
+DEFINE_BINOP_VISIT_(NENode);
+DEFINE_BINOP_VISIT_(LTNode);
+DEFINE_BINOP_VISIT_(LENode);
+DEFINE_BINOP_VISIT_(GTNode);
+DEFINE_BINOP_VISIT_(GENode);
+DEFINE_BINOP_VISIT_(AndNode);
+DEFINE_BINOP_VISIT_(OrNode);
+
+#undef DEFINE_BINOP_VISIT_
+
+void TIRVisitorWithPath::VisitExpr_(const IntImmNode* op, ObjectPath path) {}
+void TIRVisitorWithPath::VisitExpr_(const FloatImmNode* op, ObjectPath path) {}
+void TIRVisitorWithPath::VisitExpr_(const StringImmNode* op, ObjectPath path) 
{}
+
+void TIRVisitorWithPath::VisitExpr_(const ReduceNode* op, ObjectPath path) {
+  Visit(op->axis, path->Attr("axis"));
+  Visit(op->source, path->Attr("source"));
+  Visit(op->init, path->Attr("init"));
+  Visit(op->condition, path->Attr("condition"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const CastNode* op, ObjectPath path) {
+  Visit(op->value, path->Attr("value"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const NotNode* op, ObjectPath path) {
+  Visit(op->a, path->Attr("a"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const SelectNode* op, ObjectPath path) {
+  Visit(op->condition, path->Attr("condition"));
+  Visit(op->true_value, path->Attr("true_value"));
+  Visit(op->false_value, path->Attr("false_value"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const RampNode* op, ObjectPath path) {
+  Visit(op->base, path->Attr("base"));
+  Visit(op->stride, path->Attr("stride"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const ShuffleNode* op, ObjectPath path) {
+  Visit(op->indices, path->Attr("indices"));
+  Visit(op->vectors, path->Attr("vectors"));
+}
+
+void TIRVisitorWithPath::VisitExpr_(const BroadcastNode* op, ObjectPath path) {
+  Visit(op->value, path->Attr("value"));
+}
+
+}  // namespace tir
+}  // namespace tvm
diff --git a/src/tir/ir/tir_visitor_with_path.h 
b/src/tir/ir/tir_visitor_with_path.h
new file mode 100644
index 0000000000..dd0da1fe77
--- /dev/null
+++ b/src/tir/ir/tir_visitor_with_path.h
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*!
+ * \file tir/ir/tir_visitor_with_path.h
+ * \brief Provide a TIR visitor that tracks the current location
+ */
+#ifndef TVM_TIR_IR_TIR_VISITOR_WITH_PATH_H_
+#define TVM_TIR_IR_TIR_VISITOR_WITH_PATH_H_
+
+#include <tvm/ir/module.h>
+#include <tvm/tir/expr_functor.h>
+#include <tvm/tir/stmt_functor.h>
+
+#include <exception>
+#include <utility>
+
+namespace tvm {
+namespace tir {
+
+/*! \brief Visit TIR while tracking the ObjectPath */
+class TIRVisitorWithPath : protected ExprFunctor<void(const PrimExpr&, 
ObjectPath)>,
+                           protected StmtFunctor<void(const Stmt&, 
ObjectPath)> {
+ public:
+  template <typename TObjectRef>
+  void operator()(TObjectRef&& obj) {
+    Visit(std::forward<TObjectRef>(obj), ObjectPath::Root());
+  }
+
+ protected:
+  // Delegate to ExprFunctor::VisitExpr for PrimExpr, and any subclasses
+  inline void Visit(const PrimExpr& obj, ObjectPath path) { VisitExpr(obj, 
path); }
+  // Delegate to ExprFunctor::VisitStmt for Stmt, and any subclasses
+  inline void Visit(const Stmt& obj, ObjectPath path) { VisitStmt(obj, path); }
+
+  // Visitors for TIR constructs that are neither PrimExpr nor Stmt
+  virtual void Visit(const IRModule& obj, ObjectPath path);
+  virtual void Visit(const PrimFunc& obj, ObjectPath path);
+  virtual void Visit(const GlobalVar& obj, ObjectPath path) {}
+  virtual void Visit(const Range& obj, ObjectPath path);
+  virtual void Visit(const Buffer& obj, ObjectPath path);
+  virtual void Visit(const BufferRegion& obj, ObjectPath path);
+  virtual void Visit(const MatchBufferRegion& obj, ObjectPath path);
+  virtual void Visit(const IterVar& obj, ObjectPath path);
+
+  // Called when entering/exiting the scope of a GlobalVar definition.
+  virtual void EnterDef(const GlobalVar& var, ObjectPath path) {}
+  virtual void ExitDef(const GlobalVar& var, ObjectPath path) {}
+
+  // Called when entering/exiting the scope of a tir::Var definition.
+  virtual void EnterDef(const Var& var, ObjectPath path) {}
+  virtual void ExitDef(const Var& var, ObjectPath path) {}
+
+  // Called when entering/exiting the scope of an IterVar definition.
+  // By default, visits the `Range IterVarNode::dom`, then enters the
+  // scope of the internal `tir::Var`.
+  virtual void EnterDef(const IterVar& var, ObjectPath path);
+  virtual void ExitDef(const IterVar& var, ObjectPath path);
+
+  // Called when entering/exiting the scope of a Buffer definition.
+  // By default, visits the buffer's data pointer, shape, strides, and
+  // elem_offset, which must be defined prior to defining the Buffer.
+  virtual void EnterDef(const Buffer& buffer, ObjectPath path);
+  virtual void ExitDef(const Buffer& buffer, ObjectPath path);
+
+  // Utility to visit an array of nodes
+  template <typename T>
+  inline void Visit(const Array<T>& arr, ObjectPath path) {
+    for (size_t i = 0; i < arr.size(); i++) {
+      Visit(arr[i], path->ArrayIndex(i));
+    }
+  }
+
+  // Utility to visit an optional node nodes
+  template <typename T>
+  inline void Visit(const Optional<T>& opt, ObjectPath path) {
+    if (opt) {
+      Visit(opt.value(), path);
+    }
+  }
+
+  using StmtFunctor::VisitStmt;
+  void VisitStmt_(const AttrStmtNode* op, ObjectPath path) override;
+  void VisitStmt_(const IfThenElseNode* op, ObjectPath path) override;
+  void VisitStmt_(const LetStmtNode* op, ObjectPath path) override;
+  void VisitStmt_(const ForNode* op, ObjectPath path) override;
+  void VisitStmt_(const WhileNode* op, ObjectPath path) override;
+  void VisitStmt_(const AllocateNode* op, ObjectPath path) override;
+  void VisitStmt_(const AllocateConstNode* op, ObjectPath path) override;
+  void VisitStmt_(const DeclBufferNode* op, ObjectPath path) override;
+  void VisitStmt_(const BufferStoreNode* op, ObjectPath path) override;
+  void VisitStmt_(const BufferRealizeNode* op, ObjectPath path) override;
+  void VisitStmt_(const AssertStmtNode* op, ObjectPath path) override;
+  void VisitStmt_(const ProducerStoreNode* op, ObjectPath path) override;
+  void VisitStmt_(const ProducerRealizeNode* op, ObjectPath path) override;
+  void VisitStmt_(const PrefetchNode* op, ObjectPath path) override;
+  void VisitStmt_(const SeqStmtNode* op, ObjectPath path) override;
+  void VisitStmt_(const EvaluateNode* op, ObjectPath path) override;
+  void VisitStmt_(const BlockNode* op, ObjectPath path) override;
+  void VisitStmt_(const BlockRealizeNode* op, ObjectPath path) override;
+
+  using ExprFunctor::VisitExpr;
+  void VisitExpr_(const VarNode* op, ObjectPath path) override;
+  void VisitExpr_(const SizeVarNode* op, ObjectPath path) override;
+  void VisitExpr_(const BufferLoadNode* op, ObjectPath path) override;
+  void VisitExpr_(const ProducerLoadNode* op, ObjectPath path) override;
+  void VisitExpr_(const LetNode* op, ObjectPath path) override;
+  void VisitExpr_(const CallNode* op, ObjectPath path) override;
+  void VisitExpr_(const AddNode* op, ObjectPath path) override;
+  void VisitExpr_(const SubNode* op, ObjectPath path) override;
+  void VisitExpr_(const MulNode* op, ObjectPath path) override;
+  void VisitExpr_(const DivNode* op, ObjectPath path) override;
+  void VisitExpr_(const ModNode* op, ObjectPath path) override;
+  void VisitExpr_(const FloorDivNode* op, ObjectPath path) override;
+  void VisitExpr_(const FloorModNode* op, ObjectPath path) override;
+  void VisitExpr_(const MinNode* op, ObjectPath path) override;
+  void VisitExpr_(const MaxNode* op, ObjectPath path) override;
+  void VisitExpr_(const EQNode* op, ObjectPath path) override;
+  void VisitExpr_(const NENode* op, ObjectPath path) override;
+  void VisitExpr_(const LTNode* op, ObjectPath path) override;
+  void VisitExpr_(const LENode* op, ObjectPath path) override;
+  void VisitExpr_(const GTNode* op, ObjectPath path) override;
+  void VisitExpr_(const GENode* op, ObjectPath path) override;
+  void VisitExpr_(const AndNode* op, ObjectPath path) override;
+  void VisitExpr_(const OrNode* op, ObjectPath path) override;
+  void VisitExpr_(const ReduceNode* op, ObjectPath path) override;
+  void VisitExpr_(const CastNode* op, ObjectPath path) override;
+  void VisitExpr_(const NotNode* op, ObjectPath path) override;
+  void VisitExpr_(const SelectNode* op, ObjectPath path) override;
+  void VisitExpr_(const RampNode* op, ObjectPath path) override;
+  void VisitExpr_(const BroadcastNode* op, ObjectPath path) override;
+  void VisitExpr_(const ShuffleNode* op, ObjectPath path) override;
+  void VisitExpr_(const IntImmNode* op, ObjectPath path) override;
+  void VisitExpr_(const FloatImmNode* op, ObjectPath path) override;
+  void VisitExpr_(const StringImmNode* op, ObjectPath path) override;
+  void VisitExpr_(const AnyNode* op, ObjectPath path) override;
+
+  // Utility to call EnterDef/ExitDef.  Used in the implementation of
+  // WithDef.
+  template <typename T>
+  class DefContext {
+   public:
+    DefContext(DefContext&& other) { swap(std::move(other)); }
+    DefContext& operator=(DefContext&& other) {
+      swap(std::move(other));
+      return *this;
+    }
+
+    DefContext(const DefContext&) = delete;
+    DefContext& operator=(const DefContext&) = delete;
+    ~DefContext() noexcept(false) {
+      // Checks performed when a definition goes out of scope may
+      // raise an exception.  If the stack is already being unwound
+      // due to another exception being thrown, this would cause a
+      // segfault and terminate the program.  By checking that no
+      // additional exceptions have been thrown between the
+      // construction of the DefContext and the destruction, we avoid
+      // this case and allow the first error to propagate upward.
+      if (self_ && std::uncaught_exceptions() == uncaught_exceptions_) {
+        self_->ExitDef(obj_, path_);
+      }
+    }
+
+   private:
+    friend class TIRVisitorWithPath;
+
+    DefContext(TIRVisitorWithPath* self, T obj, ObjectPath path)
+        : self_(self), obj_(obj), path_(path), 
uncaught_exceptions_(std::uncaught_exceptions()) {
+      self_->EnterDef(obj_, path_);
+    }
+
+    void swap(DefContext&& other) {
+      std::swap(this->self_, other.self_);
+      std::swap(this->obj_, other.obj_);
+      std::swap(this->path_, other.path_);
+      std::swap(this->uncaught_exceptions_, other.uncaught_exceptions_);
+    }
+
+    TIRVisitorWithPath* self_{nullptr};
+    T obj_;
+    ObjectPath path_{ObjectPath::Root()};
+    int uncaught_exceptions_{-1};
+  };
+
+  // Utility to track the scope of a node's definition.
+  template <typename T>
+  DefContext<T> WithDef(T obj, ObjectPath path) {
+    return DefContext(this, obj, path);
+  }
+};
+
+}  // namespace tir
+}  // namespace tvm
+#endif  // TVM_TIR_IR_TIR_VISITOR_WITH_PATH_H_
diff --git a/src/tir/transforms/ir_utils.cc b/src/tir/transforms/ir_utils.cc
index 6b681c07e5..a85bde6787 100644
--- a/src/tir/transforms/ir_utils.cc
+++ b/src/tir/transforms/ir_utils.cc
@@ -207,6 +207,7 @@ class IRConvertSSA final : public StmtExprMutator {
     while (redefines.size()) {
       redefines.pop_back();
     }
+    function_scope_var_remap_.clear();
     return func;
   }
 
@@ -259,6 +260,9 @@ class IRConvertSSA final : public StmtExprMutator {
   Var GetRemappedVar(Var var) {
     if (auto it = scope_.find(var.get()); it != scope_.end() && 
it->second.size()) {
       return it->second.back();
+    } else if (auto it = function_scope_var_remap_.find(var.get());
+               it != function_scope_var_remap_.end()) {
+      return it->second;
     } else {
       return var;
     }
@@ -343,7 +347,53 @@ class IRConvertSSA final : public StmtExprMutator {
     }
   }
   Stmt VisitStmt_(const AttrStmtNode* op) final {
-    if (const VarNode* v = op->node.as<VarNode>()) {
+    if (const IterVarNode* iter_var = op->node.as<IterVarNode>()) {
+      Range dom = iter_var->dom;
+      if (dom.defined()) {
+        auto min = VisitExpr(dom->min);
+        auto extent = VisitExpr(dom->extent);
+        if (!min.same_as(iter_var->dom->min) || 
!extent.same_as(iter_var->dom->extent)) {
+          dom = Range::FromMinExtent(min, extent);
+        }
+      }
+
+      Var var = iter_var->var;
+      if (auto it = function_scope_var_remap_.find(var.get());
+          it != function_scope_var_remap_.end()) {
+        var = it->second;
+      } else if (defined_.count(var.get())) {
+        Var new_var = [&]() {
+          if (var->type_annotation.defined()) {
+            return Var(var->name_hint, var->type_annotation);
+          } else {
+            return Var(var->name_hint, var->dtype);
+          }
+        }();
+
+        function_scope_var_remap_.insert({var.get(), new_var});
+        var = new_var;
+      } else {
+        function_scope_var_remap_.insert({var.get(), var});
+        defined_.insert(var.get());
+      }
+
+      IterVar new_iter_var;
+      if (dom.same_as(iter_var->dom) && var.same_as(iter_var->var)) {
+        new_iter_var = GetRef<IterVar>(iter_var);
+      } else {
+        new_iter_var = IterVar(dom, var, iter_var->iter_type, 
iter_var->thread_tag, iter_var->span);
+      }
+
+      auto value = VisitExpr(op->value);
+      auto body = VisitStmt(op->body);
+
+      if (new_iter_var.get() == iter_var && body.same_as(op->body) && 
value.same_as(op->value)) {
+        return GetRef<Stmt>(op);
+      } else {
+        return AttrStmt(new_iter_var, op->attr_key, value, body, 
iter_var->span);
+      }
+
+    } else if (const VarNode* v = op->node.as<VarNode>()) {
       Stmt stmt = StmtExprMutator::VisitStmt_(op);
       op = stmt.as<AttrStmtNode>();
       if (scope_.count(v) && scope_[v].size() != 0) {
@@ -402,6 +452,8 @@ class IRConvertSSA final : public StmtExprMutator {
   std::unordered_map<const VarNode*, std::vector<Var>> scope_;
   std::unordered_set<const VarNode*> defined_;
   std::unordered_map<const BufferNode*, std::vector<Buffer>> buf_remap_;
+
+  std::unordered_map<const VarNode*, Var> function_scope_var_remap_;
 };
 
 Stmt ConvertSSA(Stmt stmt) { return IRConvertSSA()(std::move(stmt)); }
@@ -715,13 +767,8 @@ Pass ConvertSSA() {
     tir::IRConvertSSA converter;
     Map<GlobalVar, BaseFunc> functions;
     bool made_change = false;
-    // FIXME: This is just a temporal workaround to ensure free vars
-    // in device function have the same pointer as the host function
     for (auto [gvar, base_func] : mod->functions) {
       if (auto* ptr = base_func.as<tir::PrimFuncNode>()) {
-        if (!ptr->HasNonzeroAttr(tir::attr::kIsEntryFunc)) {
-          continue;
-        }
         auto updated = converter.VisitPrimFunc(GetRef<tir::PrimFunc>(ptr));
         if (!updated.same_as(base_func)) {
           made_change = true;
@@ -730,19 +777,6 @@ Pass ConvertSSA() {
       }
       functions.Set(gvar, base_func);
     }
-    for (auto [gvar, base_func] : mod->functions) {
-      if (auto* ptr = base_func.as<tir::PrimFuncNode>()) {
-        if (ptr->HasNonzeroAttr(tir::attr::kIsEntryFunc)) {
-          continue;
-        }
-        auto updated = converter.VisitPrimFunc(GetRef<tir::PrimFunc>(ptr));
-        if (!updated.same_as(base_func)) {
-          made_change = true;
-          base_func = updated;
-        }
-        functions.Set(gvar, base_func);
-      }
-    }
     if (made_change) {
       mod.CopyOnWrite()->functions = std::move(functions);
     }
diff --git a/src/tir/transforms/split_host_device.cc 
b/src/tir/transforms/split_host_device.cc
index b9fc056f19..c90384fea7 100644
--- a/src/tir/transforms/split_host_device.cc
+++ b/src/tir/transforms/split_host_device.cc
@@ -57,7 +57,7 @@ class HostDeviceSplitter : public StmtMutator {
  private:
   Stmt SplitDeviceFunc(Stmt body, Target device_target) {
     auto [params, buffers_to_declare] = [&]() -> std::tuple<Array<Var>, 
Array<Buffer>> {
-      VarUseDefAnalyzer use_def(/*defined_vars=*/{}, 
/*visit_thread_extent=*/false);
+      VarUseDefAnalyzer use_def(/*defined_vars=*/{}, 
/*visit_thread_extent=*/true);
       use_def(body);
 
       // Sort first by variable type, then by variable name
diff --git a/tests/python/tir-analysis/test_tir_analysis_verify_well_formed.py 
b/tests/python/tir-analysis/test_tir_analysis_verify_well_formed.py
index 4f88cc8be1..e839f44b33 100644
--- a/tests/python/tir-analysis/test_tir_analysis_verify_well_formed.py
+++ b/tests/python/tir-analysis/test_tir_analysis_verify_well_formed.py
@@ -14,9 +14,12 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+import pytest
+
 import tvm
 import tvm.testing
-from tvm.script import tir as T
+from tvm.script import ir as I, tir as T
 
 
 def test_pass_simple():
@@ -54,5 +57,148 @@ def test_fail_use_out_loop_var():
     assert not tvm.tir.analysis.verify_well_formed(element_wise, 
assert_mode=False)
 
 
+def test_error_for_out_of_scope_usage():
+    """A variable may not be used after its scope ends"""
+
+    @T.prim_func
+    def func():
+        i = T.int32()
+        with T.LetStmt(42, var=i):
+            T.evaluate(i)
+        T.evaluate(i)
+
+    with pytest.raises(
+        ValueError, match="Invalid use of undefined variable i at .* no longer 
in-scope."
+    ):
+        tvm.tir.analysis.verify_well_formed(func)
+
+
+def test_error_for_nested_rebind_usage():
+    """A variable may not be re-defined within the initial scope"""
+
+    @T.prim_func
+    def func():
+        i = T.int32()
+        with T.LetStmt(42, var=i):
+            with T.LetStmt(42, var=i):
+                T.evaluate(i)
+
+    with pytest.raises(
+        ValueError, match="ill-formed, due to multiple nested definitions of 
variable i"
+    ):
+        tvm.tir.analysis.verify_well_formed(func)
+
+
+def test_error_for_repeated_binding():
+    """A variable may not be re-defined after the scope ends"""
+
+    @T.prim_func
+    def func():
+        i = T.int32()
+        with T.LetStmt(42, var=i):
+            T.evaluate(i)
+        with T.LetStmt(17, var=i):
+            T.evaluate(i)
+
+    with pytest.raises(ValueError, match="multiple definitions of variable i"):
+        tvm.tir.analysis.verify_well_formed(func)
+
+
+def test_error_for_cross_function_reuse():
+    """A variable may not be re-defined in another function"""
+
+    i = tvm.tir.Var("i", "int32")
+
+    @I.ir_module
+    class mod:
+        @T.prim_func
+        def func1():
+            with T.LetStmt(42, var=i):
+                T.evaluate(i)
+
+        @T.prim_func
+        def func2():
+            with T.LetStmt(42, var=i):
+                T.evaluate(i)
+
+    with pytest.raises(ValueError, match="multiple definitions of variable i"):
+        tvm.tir.analysis.verify_well_formed(mod)
+
+
+def test_reuse_of_env_thread_in_function_is_well_formed():
+    """An env thread may be reused within a PrimFunc
+
+    The `T.env_thread` has unique semantics, and may be defined at
+    multiple locations without the TIR being considered ill-formed.
+    """
+
+    @T.prim_func
+    def func(A: T.Buffer([256], "float32")):
+        threadIdx_x = T.env_thread("threadIdx.x")
+        with T.launch_thread(threadIdx_x, 256):
+            A[threadIdx_x] = A[threadIdx_x] + 1.0
+
+        with T.launch_thread(threadIdx_x, 256):
+            A[threadIdx_x] = A[threadIdx_x] + 2.0
+
+    tvm.tir.analysis.verify_well_formed(func)
+
+
+def test_reuse_of_env_thread_in_function_is_mandatory():
+    """An env thread may be reused within a PrimFunc
+
+    Not only are environment threads allowed to have multiple
+    definition sites, it is mandatory for them to have multiple
+    definition sites.  If a PrimFunc contains more than one
+    `"thread_extent"` with the same name, but with different `tir.Var`
+    instances, it is ill-formed.
+    """
+
+    @T.prim_func
+    def func(A: T.Buffer([256], "float32")):
+        with T.launch_thread("threadIdx.x", 256) as threadIdx_x:
+            A[threadIdx_x] = A[threadIdx_x] + 1.0
+
+        with T.launch_thread("threadIdx.x", 256) as threadIdx_x:
+            A[threadIdx_x] = A[threadIdx_x] + 2.0
+
+    with pytest.raises(ValueError):
+        tvm.tir.analysis.verify_well_formed(func)
+
+
+def test_reuse_of_env_thread_across_functions_is_ill_formed():
+    """An env thread may not be reused across PrimFunc
+
+    However, each function must have its own `tir.Var` representing
+    the environment thread, and may not share these variables across
+    PrimFuncs.
+    """
+
+    threadIdx_x = tvm.tir.Var("threadIdx_x", "int32")
+
+    @I.ir_module
+    class mod:
+        @T.prim_func
+        def kernel_1(A: T.Buffer([256], "float32")):
+            T.attr(
+                T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                "thread_extent",
+                256,
+            )
+            A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+        @T.prim_func
+        def kernel_2(A: T.Buffer([256], "float32")):
+            T.attr(
+                T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                "thread_extent",
+                256,
+            )
+            A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+    with pytest.raises(ValueError, match="multiple definitions of variable 
threadIdx_x"):
+        tvm.tir.analysis.verify_well_formed(mod)
+
+
 if __name__ == "__main__":
     tvm.testing.main()
diff --git a/tests/python/tir-transform/test_tir_transform_convert_ssa.py 
b/tests/python/tir-transform/test_tir_transform_convert_ssa.py
index 38a93b199e..140adcd35b 100644
--- a/tests/python/tir-transform/test_tir_transform_convert_ssa.py
+++ b/tests/python/tir-transform/test_tir_transform_convert_ssa.py
@@ -269,5 +269,221 @@ class TestDedupAutoBroadcastBuffer(BaseBeforeAfter):
         return tvm.IRModule({"func_a": self._make_func(), "func_b": 
self._make_func()})
 
 
+class TestKeepDuplicateThreadIdxInSameFunction(BaseBeforeAfter):
+    """Environment threads are treated as being at function scope
+
+    The `"thread_extent"` attribute has some unique semantics.  It
+    serves as the definition of the `tir::Var` representing the
+    environment thread (e.g. `threadIdx.x` in CUDA).  However,
+    multiple `"thread_extent"` attributes may co-exist in the same
+    PrimFunc.  For the purpose of variable scope, use of the
+    `tir::Var` is only allowed within the body of the `AttrStmt`.
+    However, for the purpose of well-formed-ness, all
+    `"thread_extent"` attributes must use the same IterVar instance
+    (e.g. `WarpIndexFinder` in `lower_warp_memory.cc` may throw an
+    error if multiple IterVar instances occur).
+
+    If there are multiple `AttrStmt` with key `"thread_extent"` in a
+    single function (represented in TVMScript as `T.launch_thread`),
+    these should be treated as a definition of a single variable at
+    function scope, and should not be de-duplicated.
+    """
+
+    def before(self):
+        @I.ir_module
+        class mod:
+            @T.prim_func
+            def main(A: T.Buffer([256], "float32")):
+                threadIdx_x = T.env_thread("threadIdx.x")
+                with T.launch_thread(threadIdx_x, 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 1.0
+
+                with T.launch_thread(threadIdx_x, 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 2.0
+
+        return mod
+
+    expected = before
+
+
+class TestDeDuplicateThreadIdxAcrossMultipleFunctions(BaseBeforeAfter):
+    """Environment threads are treated as being at function scope
+
+    See `TestKeepDuplicateThreadIdxInSameFunction` for background
+    information.
+
+    If there are multiple functions in an IRModule, the `AttrStmt`
+    with key `"thread_extent"` in a single function (represented in
+    TVMScript as `T.launch_thread`), these should be treated as a
+    definition of a single variable at function scope, and should not
+    be de-duplicated.
+
+    For this test case, the `AttrStmt` for `"thread_extent"` are
+    written explicitly, without using the usual `T.env_thread` and
+    `T.launch_thread`, as they cannot represent the duplciate
+    Var/IterVar usage across the two PrimFuncs.
+    """
+
+    def before(self):
+        threadIdx_x = tvm.tir.Var("threadIdx_x", "int32")
+
+        @I.ir_module
+        class mod:
+            @T.prim_func
+            def kernel_1(A: T.Buffer([256], "float32")):
+                T.attr(
+                    T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                    "thread_extent",
+                    256,
+                )
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+            @T.prim_func
+            def kernel_2(A: T.Buffer([256], "float32")):
+                T.attr(
+                    T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                    "thread_extent",
+                    256,
+                )
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+        return mod
+
+    def expected(self):
+        @I.ir_module
+        class mod:
+            @T.prim_func
+            def kernel_1(A: T.Buffer([256], "float32")):
+                threadIdx_x = T.int32()
+                T.attr(
+                    T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                    "thread_extent",
+                    256,
+                )
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+            @T.prim_func
+            def kernel_2(A: T.Buffer([256], "float32")):
+                threadIdx_x = T.int32()
+                T.attr(
+                    T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                    "thread_extent",
+                    256,
+                )
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+        return mod
+
+
+class TestDeDuplicateThreadIdxIterVarAcrossMultipleFunctions(BaseBeforeAfter):
+    """Environment threads are treated as being at function scope
+
+    Like `TestDeDuplicateThreadIdxAcrossMultipleFunctions`, except the
+    `IterVar` for the environment thread is duplicated across multiple
+    PrimFuncs, not just the `tir.Var` inside the `IterVar`.
+    """
+
+    def before(self):
+        threadIdx_x = tvm.tir.Var("threadIdx_x", "int32")
+        iter_var = tvm.tir.IterVar(
+            tvm.ir.Range(0, 256), threadIdx_x, tvm.tir.IterVar.ThreadIndex, 
"threadIdx.x"
+        )
+
+        @I.ir_module
+        class mod:
+            @T.prim_func
+            def kernel_1(A: T.Buffer([256], "float32")):
+                T.attr(iter_var, "thread_extent", 256)
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+            @T.prim_func
+            def kernel_2(A: T.Buffer([256], "float32")):
+                T.attr(iter_var, "thread_extent", 256)
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+        return mod
+
+    def expected(self):
+        @I.ir_module
+        class mod:
+            @T.prim_func
+            def kernel_1(A: T.Buffer([256], "float32")):
+                threadIdx_x = T.int32()
+                T.attr(
+                    T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                    "thread_extent",
+                    256,
+                )
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+            @T.prim_func
+            def kernel_2(A: T.Buffer([256], "float32")):
+                threadIdx_x = T.int32()
+                T.attr(
+                    T.iter_var(threadIdx_x, T.Range(0, 256), "ThreadIndex", 
"threadIdx.x"),
+                    "thread_extent",
+                    256,
+                )
+                A[threadIdx_x] = A[threadIdx_x] + T.float32(1)
+
+        return mod
+
+
+class TestThreadIdxReusedWithinAndAcrossFunctions(BaseBeforeAfter):
+    """Environment threads are treated as being at function scope
+
+    A combination of
+    TestDeDuplicateThreadIdxIterVarAcrossMultipleFunctions and
+    TestKeepDuplicateThreadIdxInSameFunction.  The re-use within a
+    function should be maintained, while re-use across functions is
+    de-duplicated.
+    """
+
+    def before(self):
+        threadIdx_x = tvm.tir.Var("threadIdx_x", "int32")
+        iter_var = tvm.tir.IterVar(
+            tvm.ir.Range(0, 256), threadIdx_x, tvm.tir.IterVar.ThreadIndex, 
"threadIdx.x"
+        )
+
+        @I.ir_module
+        class mod:
+            @T.prim_func
+            def kernel_1(A: T.Buffer([256], "float32")):
+                with T.attr(iter_var, "thread_extent", 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 1.0
+                with T.attr(iter_var, "thread_extent", 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 2.0
+
+            @T.prim_func
+            def kernel_2(A: T.Buffer([256], "float32")):
+                with T.attr(iter_var, "thread_extent", 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 1.0
+                with T.attr(iter_var, "thread_extent", 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 2.0
+
+        return mod
+
+    def expected(self):
+        @I.ir_module
+        class mod:
+            @T.prim_func
+            def kernel_1(A: T.Buffer([256], "float32")):
+                threadIdx_x = T.env_thread("threadIdx.x")
+                with T.launch_thread(threadIdx_x, 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 1.0
+                with T.launch_thread(threadIdx_x, 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 2.0
+
+            @T.prim_func
+            def kernel_2(A: T.Buffer([256], "float32")):
+                threadIdx_x = T.env_thread("threadIdx.x")
+                with T.launch_thread(threadIdx_x, 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 1.0
+                with T.launch_thread(threadIdx_x, 256):
+                    A[threadIdx_x] = A[threadIdx_x] + 2.0
+
+        return mod
+
+
 if __name__ == "__main__":
     tvm.testing.main()
diff --git a/tests/python/tir-transform/test_tir_transform_split_host_device.py 
b/tests/python/tir-transform/test_tir_transform_split_host_device.py
index b61fcc6601..6adfbeb81d 100644
--- a/tests/python/tir-transform/test_tir_transform_split_host_device.py
+++ b/tests/python/tir-transform/test_tir_transform_split_host_device.py
@@ -273,5 +273,77 @@ class TestSplitHostDeviceNameCollision(BaseCompare):
         return mod
 
 
+def test_dynamic_launch_thread():
+    """Dynamic T.launch_thread may depend on host-side variable
+
+    A dynamic parameter for `T.launch_thread` may have an extent that
+    is computed using variables outside of the `T.target` section.
+
+    This is a regression test to catch a previous failure mode, in
+    which SplitHostDevice generated output with undefined variables,
+    if the only use of a variable occurred in the extent of a
+    `T.launch_thread` statement.
+
+    While the lowering pass `LowerDeviceKernelLaunch` will hoist the
+    computation of the extent from the device kernel to the host
+    function, the IRModule must be well-defined at all stages of
+    lowering.  Even if a variable is only used as part of a thread
+    extent, `SplitHostDevice` should treat it as a kernel parameter, to
+    provide a definition of the variable within the TIR device kernel.
+    """
+
+    @I.ir_module
+    class before:
+        @T.prim_func
+        def default_function(var_A: T.handle, var_B: T.handle, seq_len: 
T.int32):
+            T.func_attr({"target": T.target("cuda")})
+
+            A = T.match_buffer(var_A, [seq_len], "int32")
+            B = T.match_buffer(var_B, [seq_len], "int32")
+
+            num_blocks: T.int32 = (seq_len + 127) // 128
+            with T.attr(T.target("cuda"), "target", 0):
+                blockIdx_x = T.launch_thread("blockIdx.x", num_blocks)
+                threadIdx_x = T.launch_thread("threadIdx.x", 128)
+                if blockIdx_x * 128 + threadIdx_x < seq_len:
+                    B[blockIdx_x * 128 + threadIdx_x] = A[blockIdx_x * 128 + 
threadIdx_x]
+
+    @I.ir_module
+    class expected:
+        @T.prim_func
+        def default_function(var_A: T.handle, var_B: T.handle, seq_len: 
T.int32):
+            T.func_attr({"target": T.target("cuda")})
+            A = T.match_buffer(var_A, (seq_len,), "int32")
+            B = T.match_buffer(var_B, (seq_len,), "int32")
+            num_blocks: T.int32 = (seq_len + 127) // 128
+            expected.default_function_kernel(A.data, B.data, num_blocks, 
seq_len)
+
+        @T.prim_func(private=True)
+        def default_function_kernel(
+            A_data: T.handle("int32"),
+            B_data: T.handle("int32"),
+            num_blocks: T.int32,
+            seq_len: T.int32,
+        ):
+            T.func_attr(
+                {
+                    "target": T.target("cuda"),
+                    "tir.is_global_func": True,
+                    "tir.noalias": True,
+                }
+            )
+            A = T.decl_buffer(seq_len, "int32", data=A_data)
+            B = T.decl_buffer(seq_len, "int32", data=B_data)
+            blockIdx_x = T.launch_thread("blockIdx.x", num_blocks)
+            threadIdx_x = T.launch_thread("threadIdx.x", 128)
+            if blockIdx_x * 128 + threadIdx_x < seq_len:
+                B[blockIdx_x * 128 + threadIdx_x] = A[blockIdx_x * 128 + 
threadIdx_x]
+
+    after = tvm.tir.transform.SplitHostDevice()(before)
+
+    tvm.tir.analysis.verify_well_formed(after)
+    tvm.ir.assert_structural_equal(expected, after)
+
+
 if __name__ == "__main__":
     tvm.testing.main()

Reply via email to