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 32cde7d391 Migrate TIRx Axis to native tvm-ffi enum (#20470)
32cde7d391 is described below

commit 32cde7d3916d3fcc3b4458b6a68efd5b3d132038
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Sep 26 13:20:02 2026 -0400

    Migrate TIRx Axis to native tvm-ffi enum (#20470)
    
    Move TIRx Axis to native tvm-ffi Enum registration and shared attribute
    storage, removing its separate AttrRegistry machinery.
    
    - Preserve axis identity, thread and memory classification, typed scope
    and callback behavior, name-based creation, and serialization.
    - Keep Python access lazy through a small Enum subclass, including
    runtime-only imports.
---
 include/tvm/tirx/layout.h            | 131 +--------
 python/tvm/tirx/layout.py            |  33 +--
 src/tirx/analysis/exec_context.cc    |  10 +-
 src/tirx/ir/layout/axis_registry.cc  | 542 +++++++++++++++++------------------
 src/tirx/ir/layout/compose_layout.cc |   2 +-
 src/tirx/ir/layout/tile_core.cc      |  18 +-
 src/tirx/ir/layout/tile_tile_ops.cc  |   4 +-
 src/tirx/ir/layout/utils.cc          |   6 +-
 src/tirx/script/printer/buffer.cc    |  12 +-
 tests/python/test_tirx_axis_enum.py  |  65 +++++
 10 files changed, 382 insertions(+), 441 deletions(-)

diff --git a/include/tvm/tirx/layout.h b/include/tvm/tirx/layout.h
index 529ea35823..31729ef718 100644
--- a/include/tvm/tirx/layout.h
+++ b/include/tvm/tirx/layout.h
@@ -25,9 +25,9 @@
 
 #include <tvm/ffi/container/array.h>
 #include <tvm/ffi/container/tuple.h>
+#include <tvm/ffi/enum.h>
 #include <tvm/ffi/function.h>
 #include <tvm/ffi/object.h>
-#include <tvm/ir/attr_registry_map.h>
 #include <tvm/ir/module.h>
 #include <tvm/target/target.h>
 #include <tvm/tirx/exec_scope.h>
@@ -35,13 +35,7 @@
 
 namespace tvm {
 
-// Forward declaration
-template <typename, typename>
-class AttrRegistry;
-
 namespace tirx {
-template <typename>
-class AxisAttrMap;
 
 class Layout;
 class TileLayout;
@@ -146,15 +140,8 @@ using FAxisFuser = 
ffi::TypedFunction<ffi::Optional<Iter>(Target, ffi::String, f
 using FAxisSplitter = ffi::TypedFunction<ffi::Array<Iter, void>(Target, 
ffi::String, Iter)>;
 
 // Axis
-class AxisNode : public ffi::Object {
+class AxisNode : public ffi::EnumObj {
  public:
-  ffi::String name;
-
-  static void RegisterReflection() {
-    namespace refl = tvm::ffi::reflection;
-    refl::ObjectDef<AxisNode>().def_ro("name", &AxisNode::name);
-  }
-
   /*! \brief Check if the axis is a thread axis. */
   bool IsThreadAxis() const;
 
@@ -173,118 +160,26 @@ class AxisNode : public ffi::Object {
   /*! \brief Get the splitter of the (thread) axis. */
   ffi::Optional<FAxisSplitter> GetSplitter() const;
 
-  static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind = 
kTVMFFISEqHashKindTreeNode;
-  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("tirx.Axis", AxisNode, ffi::Object);
-
- private:
-  // Iternals necessary for AttrRegistry
-  template <typename>
-  friend class tvm::AttrRegistryMapContainerMap;
-  template <typename, typename>
-  friend class tvm::AttrRegistry;
-  friend class AxisRegEntry;
-  /*! \brief Program internal unique index of operator. */
-  uint32_t index_{0};
-  /*! \brief Return the index stored in attr registry */
-  uint32_t AttrRegistryIndex() const { return index_; }
-  /*! \brief Return the name stored in attr registry */
-  ffi::String AttrRegistryName() const { return name; }
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("tirx.Axis", AxisNode, ffi::EnumObj);
 };
 
-class Axis : public ffi::ObjectRef {
+class Axis : public ffi::Enum {
  public:
   Axis() = default;
 
-  /*! \brief Get the axis object by name. */
-  TVM_DLL static Axis Get(const ffi::String& name);
-
-  /*! \brief Get the attribute map for the axis. */
-  template <typename ValueType>
-  inline static AxisAttrMap<ValueType> GetAttrMap(const ffi::String& 
attr_name);
-
-  explicit Axis(ffi::ObjectPtr<AxisNode> data) : ObjectRef(ffi::UnsafeInit{}) {
-    TVM_FFI_ICHECK(data != nullptr);
-    data_ = std::move(data);
-  }
-
-  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(Axis, ffi::ObjectRef, 
AxisNode);
-
- private:
-  // Internals necessary for AttrRegistry
-  template <typename, typename>
-  friend class tvm::AttrRegistry;
-  friend class AxisRegEntry;
-};
-
-// AxisRegistry
-class AxisRegEntry {
- public:
-  /*! \brief List all axis names. */
-  TVM_DLL static ffi::Array<ffi::String> ListAxisNames();
+  /*! \brief Return the canonical axis name. */
+  const ffi::String& name() const { return get()->_str_index; }
 
-  /*! \brief Register or get the axis entry by name. */
-  TVM_DLL static AxisRegEntry& RegisterOrGet(const ffi::String& name);
-
-  /*! \brief Set the attribute for the axis. */
-  template <typename ValueType>
-  inline AxisRegEntry& set_attr(const ffi::String& attr_name, const ValueType& 
value,
-                                int plevel = 10);
-
-  /*! \brief Set the scope of the axis. */
-  inline AxisRegEntry& set_scope(const ffi::String& scope_name, int plevel = 
10);
-
-  /*! \brief Set the subscope of the axis. */
-  inline AxisRegEntry& set_subscope(const ffi::String& subscope_name, int 
plevel = 10);
-
-  /*! \brief Set the fuser of the axis. */
-  inline AxisRegEntry& set_fuser(const FAxisFuser& fuser);
-
-  /*! \brief Set the splitter of the axis. */
-  inline AxisRegEntry& set_splitter(const FAxisSplitter& splitter);
-
- private:
-  // return internal pointer to op.
-  inline AxisNode* get();
-  TVM_DLL void UpdateAttr(const ffi::String& key, ffi::Any value, int plevel);
-
-  // Internals necessary for AttrRegistry
-  Axis axis_;
-  ffi::String name;
-  explicit AxisRegEntry(uint32_t index);
-  template <typename, typename>
-  friend class tvm::AttrRegistry;
-  friend class Axis;
-};
-
-using AxisRegistry = AttrRegistry<AxisRegEntry, Axis>;
+  /*!
+   * \brief Get or create the axis with the given name.
+   * \param name Name of the axis to look up or register.
+   * \return The registered axis singleton.
+   */
+  TVM_DLL static Axis Get(const ffi::String& name);
 
-// AxisAttrffi::Map
-template <typename ValueType>
-class AxisAttrMap : public AttrRegistryMap<Axis, ValueType> {
- public:
-  using TParent = AttrRegistryMap<Axis, ValueType>;
-  using TParent::count;
-  using TParent::get;
-  using TParent::operator[];
-
- private:
-  friend class Axis;
-  explicit AxisAttrMap(const AttrRegistryMapContainerMap<Axis>& map) : 
TParent(map) {}
+  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NOTNULLABLE(Axis, ffi::Enum, AxisNode);
 };
 
-// Helper macro for token concatenation
-#ifndef TVM_STR_CONCAT
-#define TVM_STR_CONCAT_(__x, __y) __x##__y
-#define TVM_STR_CONCAT(__x, __y) TVM_STR_CONCAT_(__x, __y)
-#endif
-
-// Define a macro to register the axis entry.
-#define TVM_AXIS_REGISTER_VAR_DEF [[maybe_unused]] static 
::tvm::tirx::AxisRegEntry& __make_##Axis
-
-#define TVM_REGISTER_AXIS(AxisName)                        \
-  TVM_STR_CONCAT(TVM_AXIS_REGISTER_VAR_DEF, __COUNTER__) = \
-      ::tvm::tirx::AxisRegEntry::RegisterOrGet(AxisName)
-
 class IterNode : public ffi::Object {
  public:
   PrimExpr extent;
diff --git a/python/tvm/tirx/layout.py b/python/tvm/tirx/layout.py
index 02878df314..f1015092c0 100644
--- a/python/tvm/tirx/layout.py
+++ b/python/tvm/tirx/layout.py
@@ -21,9 +21,10 @@ import functools
 import operator
 import re
 from collections.abc import Sequence
-from typing import ClassVar, Optional, Union
+from typing import Optional, Union
 
 import tvm_ffi
+from tvm_ffi.dataclasses import Enum
 
 import tvm
 from tvm.runtime import Object
@@ -502,7 +503,7 @@ _AXIS_NAMES = (
 )
 
 
-class _AxisMeta(type(Object)):
+class _AxisMeta(type(Enum)):
     """Metaclass: lazy resolve `Axis.<name>` for registered axes."""
 
     def __getattr__(cls, name):
@@ -511,27 +512,21 @@ class _AxisMeta(type(Object)):
         raise AttributeError(f"type object 'Axis' has no attribute {name!r}")
 
 
-@tvm_ffi.register_object("tirx.Axis")
-class Axis(Object, metaclass=_AxisMeta):
+class Axis(Enum, metaclass=_AxisMeta, type_key="tirx.Axis", init=False):
     """Layout axis wrapper."""
 
-    # ---- forbid direct construction ----
-    def __init__(self, *args, **kwargs):
-        raise RuntimeError("Cannot create Axis directly; use Axis.get()")
+    @property
+    def name(self) -> str:
+        """Return the canonical axis name."""
+        return self._str_index
 
-    @staticmethod
-    def _register_axis(name: str) -> "Axis":
-        return _ffi_api.AxisGet(name)  # pylint: disable=no-member
-
-    # Singleton cache, populated lazily as names are accessed.
-    reg_dict: ClassVar[dict[str, "Axis"]] = {}
+    @classmethod
+    def get(cls, name: str) -> "Axis":
+        """Get or create the axis singleton named `name`.
 
-    @staticmethod
-    def get(name: str) -> "Axis":
-        """Get or create an axis by name. Unknown names are auto-registered."""
-        if name not in Axis.reg_dict:
-            Axis.reg_dict[name] = Axis._register_axis(name)
-        return Axis.reg_dict[name]
+        Unknown names are registered without thread or memory attributes.
+        """
+        return _ffi_api.AxisGet(name)  # pylint: disable=no-member
 
     def is_thread(self) -> bool:
         """Check if the axis is a thread axis."""
diff --git a/src/tirx/analysis/exec_context.cc 
b/src/tirx/analysis/exec_context.cc
index d47114bd6c..fcfffa387b 100644
--- a/src/tirx/analysis/exec_context.cc
+++ b/src/tirx/analysis/exec_context.cc
@@ -77,8 +77,8 @@ std::vector<std::pair<std::string, AxisRange>> 
AxisRanges(const ActiveSet& A) {
   std::vector<std::pair<std::string, AxisRange>> axes;
   for (const auto& iter : A.layout->shard) {
     AxisRange range;
-    TVM_FFI_ICHECK(A.GetAxis(iter->axis->name.operator std::string(), &range));
-    axes.push_back({iter->axis->name.operator std::string(), range});
+    TVM_FFI_ICHECK(A.GetAxis(iter->axis.name().operator std::string(), 
&range));
+    axes.push_back({iter->axis.name().operator std::string(), range});
   }
   return axes;
 }
@@ -381,10 +381,10 @@ bool AxisRange::Modulo(int64_t modulus, int64_t residue, 
AxisRange* out) const {
 bool ActiveSet::GetAxis(const std::string& axis, AxisRange* out) const {
   if (!layout.defined()) return false;
   for (const auto& iter : layout->shard) {
-    if (iter->axis->name != axis) continue;
+    if (iter->axis.name() != axis) continue;
     PrimExpr off = I64(0);
     for (const auto& kv : layout->offset) {
-      if (kv.first->name == axis) {
+      if (kv.first.name() == axis) {
         off = kv.second;
         break;
       }
@@ -418,7 +418,7 @@ std::vector<std::string> ActiveSet::AxisNames() const {
   std::vector<std::string> names;
   if (!layout.defined()) return names;
   for (const auto& iter : layout->shard) {
-    names.push_back(iter->axis->name.operator std::string());
+    names.push_back(iter->axis.name().operator std::string());
   }
   return names;
 }
diff --git a/src/tirx/ir/layout/axis_registry.cc 
b/src/tirx/ir/layout/axis_registry.cc
index 2474390789..ce8d8aaa5c 100644
--- a/src/tirx/ir/layout/axis_registry.cc
+++ b/src/tirx/ir/layout/axis_registry.cc
@@ -20,144 +20,84 @@
 /*
  * Axis definitions, attributes, fusers/splitters, and registrations.
  */
+#include <tvm/ffi/reflection/enum_def.h>
+
 #include "utils.h"
 
 namespace tvm {
 namespace tirx {
+namespace refl = tvm::ffi::reflection;
 
 /**************** Axis ****************/
-// AxisNode
 ffi::ObjectPtr<ffi::Object> CreateAxis(const std::string& name) {
-  // Hack use ffi::Any as exchange
   auto axis = Axis::Get(name);
-  TVM_FFI_ICHECK(axis.defined()) << "Cannot find axis '" << name << '\'';
   return ffi::details::ObjectUnsafe::ObjectPtrFromObjectRef<ffi::Object>(axis);
 }
 
+const ffi::EnumState& AxisEnumState() {
+  static refl::TypeAttrColumn state_column(refl::type_attr::kEnumState);
+  static ffi::EnumState state =
+      
ffi::Any(state_column[AxisNode::_GetOrAllocRuntimeTypeIndex()]).as_or_throw<ffi::EnumState>();
+  return state;
+}
+
+template <typename T>
+ffi::Optional<T> AxisAttr(const AxisNode* axis, const char* key) {
+  const ffi::EnumState& state = AxisEnumState();
+  auto column = state->attrs.Get(ffi::String(key));
+  if (!column) return std::nullopt;
+  auto value = column.value().Get(ffi::GetRef<Axis>(axis));
+  if (!value) return std::nullopt;
+  return value.value().cast<T>();
+}
+
 bool AxisNode::IsThreadAxis() const {
-  static const auto& thread_attr_map = Axis::GetAttrMap<bool>("thread");
-  return thread_attr_map[ffi::GetRef<Axis>(this)];
+  auto thread = AxisAttr<bool>(this, "thread");
+  TVM_FFI_ICHECK(thread.has_value()) << "Axis '" << _str_index << "' has no 
thread classification";
+  return thread.value();
 }
 
 bool AxisNode::IsMemoryAxis() const {
-  static const auto& thread_attr_map = Axis::GetAttrMap<bool>("thread");
-  return !thread_attr_map[ffi::GetRef<Axis>(this)];
+  auto thread = AxisAttr<bool>(this, "thread");
+  TVM_FFI_ICHECK(thread.has_value()) << "Axis '" << _str_index << "' has no 
thread classification";
+  return !thread.value();
 }
 
-ffi::Optional<ExecScope> AxisNode::GetScope() const {
-  static const auto& scope_attr_map = 
Axis::GetAttrMap<ffi::Optional<ExecScope>>("scope");
-  return scope_attr_map.get(ffi::GetRef<Axis>(this), std::nullopt);
-}
+ffi::Optional<ExecScope> AxisNode::GetScope() const { return 
AxisAttr<ExecScope>(this, "scope"); }
 
 ffi::Optional<ExecScope> AxisNode::GetSubscope() const {
-  static const auto& subscope_attr_map = 
Axis::GetAttrMap<ffi::Optional<ExecScope>>("subscope");
-  return subscope_attr_map.get(ffi::GetRef<Axis>(this), std::nullopt);
+  return AxisAttr<ExecScope>(this, "subscope");
 }
 
-ffi::Optional<FAxisFuser> AxisNode::GetFuser() const {
-  static const auto& fuser_attr_map = 
Axis::GetAttrMap<ffi::Optional<FAxisFuser>>("fuser");
-  return fuser_attr_map.get(ffi::GetRef<Axis>(this), std::nullopt);
-}
+ffi::Optional<FAxisFuser> AxisNode::GetFuser() const { return 
AxisAttr<FAxisFuser>(this, "fuser"); }
 
 ffi::Optional<FAxisSplitter> AxisNode::GetSplitter() const {
-  static const auto& splitter_attr_map = 
Axis::GetAttrMap<ffi::Optional<FAxisSplitter>>("splitter");
-  return splitter_attr_map.get(ffi::GetRef<Axis>(this), std::nullopt);
-}
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("tirx.AxisIsThreadAxis", [](Axis axis) { return 
axis->IsThreadAxis(); });
+  return AxisAttr<FAxisSplitter>(this, "splitter");
 }
 
 TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("tirx.AxisIsMemoryAxis", [](Axis axis) { return 
axis->IsMemoryAxis(); });
-}
+  refl::ObjectDef<AxisNode>(refl::init(false));
+  refl::TypeAttrDef<AxisNode>()
+      .def("__data_to_json__", [](const AxisNode* node) -> ffi::String { 
return node->_str_index; })
+      .def("__data_from_json__", [](const ffi::String& name) -> Axis { return 
Axis::Get(name); });
 
-TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("tirx.AxisGetScope", [](Axis axis) { return 
axis->GetScope(); });
-}
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("tirx.AxisGetSubscope", [](Axis axis) { return 
axis->GetSubscope(); });
+  refl::GlobalDef()
+      .def("tirx.AxisIsThreadAxis", [](Axis axis) { return 
axis->IsThreadAxis(); })
+      .def("tirx.AxisIsMemoryAxis", [](Axis axis) { return 
axis->IsMemoryAxis(); })
+      .def("tirx.AxisGetScope", [](Axis axis) { return axis->GetScope(); })
+      .def("tirx.AxisGetSubscope", [](Axis axis) { return axis->GetSubscope(); 
})
+      .def("tirx.AxisGet", [](ffi::String name) -> Axis { return 
Axis::Get(name); });
 }
 
 // Axis
 Axis Axis::Get(const ffi::String& name) {
-  const AxisRegEntry* reg = AxisRegistry::Global()->Get(name);
-  if (reg != nullptr) {
-    return reg->axis_;
+  const ffi::EnumState& state = AxisEnumState();
+  auto existing = state->indexes.Get(ffi::Any(name));
+  if (!existing) {
+    refl::EnumDef<AxisNode>(name.c_str());
+    existing = state->indexes.Get(ffi::Any(name));
   }
-  // Auto-register unknown axes on the fly
-  return AxisRegEntry::RegisterOrGet(name).axis_;
-}
-
-template <typename ValueType>
-inline AxisAttrMap<ValueType> Axis::GetAttrMap(const ffi::String& attr_name) {
-  return AxisAttrMap<ValueType>(AxisRegistry::Global()->GetAttrMap(attr_name));
-}
-
-// AxisRegEntry
-inline AxisNode* AxisRegEntry::get() { return 
const_cast<AxisNode*>(axis_.operator->()); }
-
-AxisRegEntry::AxisRegEntry(uint32_t index) {
-  ffi::ObjectPtr<AxisNode> n = ffi::make_object<AxisNode>();
-  n->index_ = index;
-  axis_ = Axis(n);
-}
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  AxisNode::RegisterReflection();
-  refl::TypeAttrDef<AxisNode>()
-      .def("__data_to_json__", [](const AxisNode* node) -> ffi::String { 
return node->name; })
-      .def("__data_from_json__", [](const ffi::String& name) -> Axis { return 
Axis::Get(name); });
-}
-
-AxisRegEntry& AxisRegEntry::RegisterOrGet(const ffi::String& name) {
-  auto& entry = AxisRegistry::Global()->RegisterOrGet(name);
-  entry.get()->name = name;
-  return entry;
-}
-
-ffi::Array<ffi::String> AxisRegEntry::ListAxisNames() {
-  return AxisRegistry::Global()->ListAllNames();
-}
-
-template <typename ValueType>
-inline AxisRegEntry& AxisRegEntry::set_attr(const ffi::String& key, const 
ValueType& value,
-                                            int plevel) {
-  TVM_FFI_ICHECK_GT(plevel, 0) << "plevel in set_attr must be greater than 0";
-  ffi::Any rv;
-  rv = value;
-  UpdateAttr(key, rv, plevel);
-  return *this;
-}
-
-AxisRegEntry& AxisRegEntry::set_scope(const ffi::String& scope_name, int 
plevel) {
-  set_attr<ffi::Optional<ExecScope>>("scope", ExecScope(scope_name), plevel);
-  return *this;
-}
-
-AxisRegEntry& AxisRegEntry::set_subscope(const ffi::String& subscope_name, int 
plevel) {
-  set_attr<ffi::Optional<ExecScope>>("subscope", ExecScope(subscope_name), 
plevel);
-  return *this;
-}
-
-AxisRegEntry& AxisRegEntry::set_fuser(const FAxisFuser& fuser) {
-  set_attr<ffi::Optional<FAxisFuser>>("fuser", fuser);
-  return *this;
-}
-
-AxisRegEntry& AxisRegEntry::set_splitter(const FAxisSplitter& splitter) {
-  set_attr<ffi::Optional<FAxisSplitter>>("splitter", splitter);
-  return *this;
-}
-
-void AxisRegEntry::UpdateAttr(const ffi::String& key, ffi::Any value, int 
plevel) {
-  AxisRegistry::Global()->UpdateAttr(key, axis_, value, plevel);
+  return existing.value().as_or_throw<Axis>();
 }
 
 // register thread axis split/fuse helpers
@@ -178,179 +118,225 @@ ffi::Array<Iter> SplitterGen(const Iter& iter, const 
Axis& axis_outer, const Axi
   return {};
 }
 
-// register thread axes
-TVM_REGISTER_AXIS("bx").set_attr<bool>("thread", 
true).set_scope("thread").set_subscope("cta");
-TVM_REGISTER_AXIS("by").set_attr<bool>("thread", 
true).set_scope("thread").set_subscope("cta");
-TVM_REGISTER_AXIS("bz").set_attr<bool>("thread", 
true).set_scope("thread").set_subscope("cta");
-TVM_REGISTER_AXIS("cbx").set_attr<bool>("thread", 
true).set_scope("cluster").set_subscope("cta");
-TVM_REGISTER_AXIS("cby").set_attr<bool>("thread", 
true).set_scope("cluster").set_subscope("cta");
-TVM_REGISTER_AXIS("cbz").set_attr<bool>("thread", 
true).set_scope("cluster").set_subscope("cta");
-TVM_REGISTER_AXIS("tx")
-    .set_attr<bool>("thread", true)
-    .set_scope("cta")
-    .set_subscope("thread")
-    .set_fuser([](Target target, ffi::String subscope, ffi::String scope,
-                  Iter iter) -> ffi::Optional<Iter> {
-      if (target->kind->default_device_type == kDLCUDA) {
-        return std::nullopt;
-      }
-      return std::nullopt;
-    })
-    .set_splitter([](Target target, ffi::String scope, Iter iter) -> 
ffi::Array<Iter> {
-      sym::Analyzer analyzer;
-      if (target->kind->default_device_type == kDLCUDA) {
-        if (scope == "warp") {
-          // tx -> warpid, laneid
-          return SplitterGen(iter, Axis::Get("warpid"), Axis::Get("laneid"), 
32);
-        } else if (scope == "warpgroup") {
-          // tx -> wgid, tid_in_wg
-          return SplitterGen(iter, Axis::Get("wgid"), Axis::Get("tid_in_wg"), 
128);
-        }
-        LOG(FATAL) << "Cannot split cta->thread axis into cta->" << scope << 
"->thread";
-      }
-      return {};
-    });
-TVM_REGISTER_AXIS("warpid")
-    .set_attr<bool>("thread", true)
-    .set_scope("cta")
-    .set_subscope("warp")
-    .set_fuser([](Target target, ffi::String subscope, ffi::String scope,
-                  Iter iter) -> ffi::Optional<Iter> {
-      if (target->kind->default_device_type == kDLCUDA) {
-        // cta->warp ===> cta->thread (tx)
-        if (subscope == "thread" && scope == "cta") {
-          return Iter(iter->extent, 32 * iter->stride, Axis::Get("tx"));
-        }
-        return std::nullopt;
-      }
-      return std::nullopt;
-    })
-    .set_splitter([](Target target, ffi::String scope, Iter iter) -> 
ffi::Array<Iter> {
-      sym::Analyzer analyzer;
-      if (target->kind->default_device_type == kDLCUDA) {
-        if (scope == "warp") {
-          // warpid -> wgid, wid_in_wg
-          return SplitterGen(iter, Axis::Get("wgid"), Axis::Get("wid_in_wg"), 
4);
-        }
-        LOG(FATAL) << "Cannot split cta->warp axis into cta->" << scope << 
"->warp";
-      }
-      return {};
-    });
-TVM_REGISTER_AXIS("laneid")
-    .set_attr<bool>("thread", true)
-    .set_scope("warp")
-    .set_subscope("thread")
-    .set_fuser([](Target target, ffi::String subscope, ffi::String scope,
-                  Iter iter) -> ffi::Optional<Iter> {
-      if (target->kind->default_device_type == kDLCUDA) {
-        if (subscope == "thread" && scope == "warpgroup") {
-          // warp->thread ===> warpgroup->thread (tid_in_wg)
-          return Iter(iter->extent, iter->stride, Axis::Get("tid_in_wg"));
-        } else if (subscope == "thread" && scope == "cta") {
-          // warp->thread ===> cta->thread (tx)
-          return Iter(iter->extent, iter->stride, Axis::Get("tx"));
-        }
-        return std::nullopt;
-      }
-      return std::nullopt;
-    })
-    .set_splitter([](Target target, ffi::String scope, Iter iter) -> 
ffi::Array<Iter> {
-      sym::Analyzer analyzer;
-      if (target->kind->default_device_type == kDLCUDA) {
-        LOG(FATAL) << "laneid can not be split any more";
-      }
-      return {};
-    });
-TVM_REGISTER_AXIS("wgid")
-    .set_attr<bool>("thread", true)
-    .set_scope("cta")
-    .set_subscope("warpgroup")
-    .set_fuser([](Target target, ffi::String subscope, ffi::String scope,
-                  Iter iter) -> ffi::Optional<Iter> {
-      if (target->kind->default_device_type == kDLCUDA) {
-        if (subscope == "thread" && scope == "cta") {
-          // cta->warpgroup ===> cta->thread (tx)
-          return Iter(iter->extent, iter->stride * 128, Axis::Get("tx"));
-        } else if (subscope == "warp" && scope == "cta") {
-          // cta->warpgroup ===> cta->warp (warpid)
-          return Iter(iter->extent, iter->stride * 4, Axis::Get("wgid"));
-        }
-      }
-      return std::nullopt;
-    })
-    .set_splitter([](Target target, ffi::String scope, Iter iter) -> 
ffi::Array<Iter> {
-      sym::Analyzer analyzer;
-      if (target->kind->default_device_type == kDLCUDA) {
-        LOG(FATAL) << "wgid can not be split any more";
-      }
-      return {};
-    });
-TVM_REGISTER_AXIS("tid_in_wg")
-    .set_attr<bool>("thread", true)
-    .set_scope("warpgroup")
-    .set_subscope("thread")
-    .set_fuser([](Target target, ffi::String subscope, ffi::String scope,
-                  Iter iter) -> ffi::Optional<Iter> {
-      if (target->kind->default_device_type == kDLCUDA) {
-        if (subscope == "thread" && scope == "cta") {
-          // warpgroup->thread ===> cta->thread (tx)
-          return Iter(iter->extent, iter->stride, Axis::Get("tx"));
-        }
-        return std::nullopt;
-      }
-      return std::nullopt;
-    })
-    .set_splitter([](Target target, ffi::String scope, Iter iter) -> 
ffi::Array<Iter> {
-      sym::Analyzer analyzer;
-      if (target->kind->default_device_type == kDLCUDA) {
-        if (scope == "warp") {
-          // tid_in_wg -> wid_in_wg, laneid
-          return SplitterGen(iter, Axis::Get("wid_in_wg"), 
Axis::Get("laneid"), 32);
-        }
-        LOG(FATAL) << "Cannot split warpgroup->thread axis into warpgroup->" 
<< scope << "->thread";
-      }
-      return {};
-    });
-TVM_REGISTER_AXIS("wid_in_wg")
-    .set_attr<bool>("thread", true)
-    .set_scope("warpgroup")
-    .set_subscope("warp")
-    .set_fuser([](Target target, ffi::String subscope, ffi::String scope,
-                  Iter iter) -> ffi::Optional<Iter> {
-      if (target->kind->default_device_type == kDLCUDA) {
-        if (subscope == "thread" && scope == "warpgroup") {
-          // warpgroup->warp ===> warpgroup->thread (tid_in_wg)
-          return Iter(iter->extent, iter->stride * 32, Axis::Get("tid_in_wg"));
-        } else if (subscope == "thread" && scope == "cta") {
-          // warpgroup->warp ===> cta->thread (tx)
-          return Iter(iter->extent, iter->stride * 32, Axis::Get("tx"));
-        } else if (subscope == "warp" && scope == "cta") {
-          // warpgroup->warp ===> cta->warp (warpid)
-          return Iter(iter->extent, iter->stride, Axis::Get("warpid"));
-        }
-        return std::nullopt;
-      }
-      return std::nullopt;
-    })
-    .set_splitter([](Target target, ffi::String scope, Iter iter) -> 
ffi::Array<Iter> {
-      sym::Analyzer analyzer;
-      if (target->kind->default_device_type == kDLCUDA) {
-        LOG(FATAL) << "wid_in_wg can not be split any more";
-      }
-      return {};
-    });
-
-// register memory axis
-TVM_REGISTER_AXIS("m").set_attr<bool>("thread", false);
-TVM_REGISTER_AXIS("P").set_attr<bool>("thread", false);
-TVM_REGISTER_AXIS("F").set_attr<bool>("thread", false);
-TVM_REGISTER_AXIS("Bank").set_attr<bool>("thread", false);
-TVM_REGISTER_AXIS("TCol").set_attr<bool>("thread", false);
-TVM_REGISTER_AXIS("TLane").set_attr<bool>("thread", false);
-
 TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def("tirx.AxisGet", [](ffi::String name) -> Axis { return 
Axis::Get(name); });
+  // Register thread axes in declaration order.
+  refl::EnumDef<AxisNode>("bx")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("thread"))
+      .set_attr("subscope", ExecScope("cta"));
+
+  refl::EnumDef<AxisNode>("by")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("thread"))
+      .set_attr("subscope", ExecScope("cta"));
+
+  refl::EnumDef<AxisNode>("bz")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("thread"))
+      .set_attr("subscope", ExecScope("cta"));
+
+  refl::EnumDef<AxisNode>("cbx")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("cluster"))
+      .set_attr("subscope", ExecScope("cta"));
+
+  refl::EnumDef<AxisNode>("cby")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("cluster"))
+      .set_attr("subscope", ExecScope("cta"));
+
+  refl::EnumDef<AxisNode>("cbz")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("cluster"))
+      .set_attr("subscope", ExecScope("cta"));
+
+  refl::EnumDef<AxisNode>("tx")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("cta"))
+      .set_attr("subscope", ExecScope("thread"))
+      .set_attr("fuser", FAxisFuser([](Target target, ffi::String subscope, 
ffi::String scope,
+                                       Iter iter) -> ffi::Optional<Iter> {
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    return std::nullopt;
+                  }
+                  return std::nullopt;
+                }))
+      .set_attr("splitter",
+                FAxisSplitter([](Target target, ffi::String scope, Iter iter) 
-> ffi::Array<Iter> {
+                  sym::Analyzer analyzer;
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    if (scope == "warp") {
+                      // tx -> warpid, laneid
+                      return SplitterGen(iter, Axis::Get("warpid"), 
Axis::Get("laneid"), 32);
+                    } else if (scope == "warpgroup") {
+                      // tx -> wgid, tid_in_wg
+                      return SplitterGen(iter, Axis::Get("wgid"), 
Axis::Get("tid_in_wg"), 128);
+                    }
+                    LOG(FATAL) << "Cannot split cta->thread axis into cta->" 
<< scope << "->thread";
+                  }
+                  return {};
+                }));
+
+  refl::EnumDef<AxisNode>("warpid")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("cta"))
+      .set_attr("subscope", ExecScope("warp"))
+      .set_attr("fuser", FAxisFuser([](Target target, ffi::String subscope, 
ffi::String scope,
+                                       Iter iter) -> ffi::Optional<Iter> {
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    // cta->warp ===> cta->thread (tx)
+                    if (subscope == "thread" && scope == "cta") {
+                      return Iter(iter->extent, 32 * iter->stride, 
Axis::Get("tx"));
+                    }
+                    return std::nullopt;
+                  }
+                  return std::nullopt;
+                }))
+      .set_attr("splitter",
+                FAxisSplitter([](Target target, ffi::String scope, Iter iter) 
-> ffi::Array<Iter> {
+                  sym::Analyzer analyzer;
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    if (scope == "warp") {
+                      // warpid -> wgid, wid_in_wg
+                      return SplitterGen(iter, Axis::Get("wgid"), 
Axis::Get("wid_in_wg"), 4);
+                    }
+                    LOG(FATAL) << "Cannot split cta->warp axis into cta->" << 
scope << "->warp";
+                  }
+                  return {};
+                }));
+
+  refl::EnumDef<AxisNode>("laneid")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("warp"))
+      .set_attr("subscope", ExecScope("thread"))
+      .set_attr("fuser", FAxisFuser([](Target target, ffi::String subscope, 
ffi::String scope,
+                                       Iter iter) -> ffi::Optional<Iter> {
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    if (subscope == "thread" && scope == "warpgroup") {
+                      // warp->thread ===> warpgroup->thread (tid_in_wg)
+                      return Iter(iter->extent, iter->stride, 
Axis::Get("tid_in_wg"));
+                    } else if (subscope == "thread" && scope == "cta") {
+                      // warp->thread ===> cta->thread (tx)
+                      return Iter(iter->extent, iter->stride, Axis::Get("tx"));
+                    }
+                    return std::nullopt;
+                  }
+                  return std::nullopt;
+                }))
+      .set_attr("splitter",
+                FAxisSplitter([](Target target, ffi::String scope, Iter iter) 
-> ffi::Array<Iter> {
+                  sym::Analyzer analyzer;
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    LOG(FATAL) << "laneid can not be split any more";
+                  }
+                  return {};
+                }));
+
+  refl::EnumDef<AxisNode>("wgid")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("cta"))
+      .set_attr("subscope", ExecScope("warpgroup"))
+      .set_attr("fuser", FAxisFuser([](Target target, ffi::String subscope, 
ffi::String scope,
+                                       Iter iter) -> ffi::Optional<Iter> {
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    if (subscope == "thread" && scope == "cta") {
+                      // cta->warpgroup ===> cta->thread (tx)
+                      return Iter(iter->extent, iter->stride * 128, 
Axis::Get("tx"));
+                    } else if (subscope == "warp" && scope == "cta") {
+                      // cta->warpgroup ===> cta->warp (warpid)
+                      return Iter(iter->extent, iter->stride * 4, 
Axis::Get("wgid"));
+                    }
+                  }
+                  return std::nullopt;
+                }))
+      .set_attr("splitter",
+                FAxisSplitter([](Target target, ffi::String scope, Iter iter) 
-> ffi::Array<Iter> {
+                  sym::Analyzer analyzer;
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    LOG(FATAL) << "wgid can not be split any more";
+                  }
+                  return {};
+                }));
+
+  refl::EnumDef<AxisNode>("tid_in_wg")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("warpgroup"))
+      .set_attr("subscope", ExecScope("thread"))
+      .set_attr("fuser", FAxisFuser([](Target target, ffi::String subscope, 
ffi::String scope,
+                                       Iter iter) -> ffi::Optional<Iter> {
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    if (subscope == "thread" && scope == "cta") {
+                      // warpgroup->thread ===> cta->thread (tx)
+                      return Iter(iter->extent, iter->stride, Axis::Get("tx"));
+                    }
+                    return std::nullopt;
+                  }
+                  return std::nullopt;
+                }))
+      .set_attr("splitter",
+                FAxisSplitter([](Target target, ffi::String scope, Iter iter) 
-> ffi::Array<Iter> {
+                  sym::Analyzer analyzer;
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    if (scope == "warp") {
+                      // tid_in_wg -> wid_in_wg, laneid
+                      return SplitterGen(iter, Axis::Get("wid_in_wg"), 
Axis::Get("laneid"), 32);
+                    }
+                    LOG(FATAL) << "Cannot split warpgroup->thread axis into 
warpgroup->" << scope
+                               << "->thread";
+                  }
+                  return {};
+                }));
+
+  refl::EnumDef<AxisNode>("wid_in_wg")
+      .set_attr("thread", true)
+      .set_attr("scope", ExecScope("warpgroup"))
+      .set_attr("subscope", ExecScope("warp"))
+      .set_attr("fuser", FAxisFuser([](Target target, ffi::String subscope, 
ffi::String scope,
+                                       Iter iter) -> ffi::Optional<Iter> {
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    if (subscope == "thread" && scope == "warpgroup") {
+                      // warpgroup->warp ===> warpgroup->thread (tid_in_wg)
+                      return Iter(iter->extent, iter->stride * 32, 
Axis::Get("tid_in_wg"));
+                    } else if (subscope == "thread" && scope == "cta") {
+                      // warpgroup->warp ===> cta->thread (tx)
+                      return Iter(iter->extent, iter->stride * 32, 
Axis::Get("tx"));
+                    } else if (subscope == "warp" && scope == "cta") {
+                      // warpgroup->warp ===> cta->warp (warpid)
+                      return Iter(iter->extent, iter->stride, 
Axis::Get("warpid"));
+                    }
+                    return std::nullopt;
+                  }
+                  return std::nullopt;
+                }))
+      .set_attr("splitter",
+                FAxisSplitter([](Target target, ffi::String scope, Iter iter) 
-> ffi::Array<Iter> {
+                  sym::Analyzer analyzer;
+                  if (target->kind->default_device_type == kDLCUDA) {
+                    LOG(FATAL) << "wid_in_wg can not be split any more";
+                  }
+                  return {};
+                }));
+
+  // Register memory axes.
+  // clang-format off
+  refl::EnumDef<AxisNode>("m")
+      .set_attr("thread", false);
+
+  refl::EnumDef<AxisNode>("P")
+      .set_attr("thread", false);
+
+  refl::EnumDef<AxisNode>("F")
+      .set_attr("thread", false);
+
+  refl::EnumDef<AxisNode>("Bank")
+      .set_attr("thread", false);
+
+  refl::EnumDef<AxisNode>("TCol")
+      .set_attr("thread", false);
+
+  refl::EnumDef<AxisNode>("TLane")
+      .set_attr("thread", false);
+  // clang-format on
 }
 
 }  // namespace tirx
diff --git a/src/tirx/ir/layout/compose_layout.cc 
b/src/tirx/ir/layout/compose_layout.cc
index 1f79b13e41..8a19e3fdc5 100644
--- a/src/tirx/ir/layout/compose_layout.cc
+++ b/src/tirx/ir/layout/compose_layout.cc
@@ -246,7 +246,7 @@ ffi::Map<ffi::String, PrimExpr> ApplyStructured(const 
ComposeLayoutNode* layout,
   }
 
   for (const auto& [axis, offset] : tile->offset) {
-    if (axis->name != "m") return fallback();
+    if (axis.name() != "m") return fallback();
     std::vector<PrimExpr> dynamic_terms;
     bool valid = true;
     CollectOffsetTerms(offset, 1, &dynamic_terms, &constant, &valid, analyzer);
diff --git a/src/tirx/ir/layout/tile_core.cc b/src/tirx/ir/layout/tile_core.cc
index e6681adee1..e3f4dedff4 100644
--- a/src/tirx/ir/layout/tile_core.cc
+++ b/src/tirx/ir/layout/tile_core.cc
@@ -212,7 +212,7 @@ bool TileLayoutNode::VerifyWellFormed() const {
   // std::unordered_map<String, std::vector<Iter>> thread_axes;
   // auto collect_thread_axis = [&thread_axes](const Iter& iter) {
   //   if (iter->axis->IsThreadAxis()) {
-  //     thread_axes[iter->axis->name].push_back(iter);
+  //     thread_axes[iter->axis.name()].push_back(iter);
   //   }
   // };
   // for (const auto& iter : shard) {
@@ -238,7 +238,7 @@ bool TileLayoutNode::VerifyWellFormed() const {
 
 PrimExpr TileLayoutNode::GetSize(ffi::Optional<ffi::String> axis_name) const {
   auto filter = [&](const Iter& iter, PrimExpr acc) {
-    if (!axis_name.has_value() || iter->axis->name == axis_name.value()) {
+    if (!axis_name.has_value() || iter->axis.name() == axis_name.value()) {
       return acc * iter->extent;
     }
     return acc;
@@ -314,20 +314,20 @@ ffi::Map<ffi::String, PrimExpr> 
TileLayoutNode::Apply(Array<PrimExpr> coord) con
       << "Coordinate size must match the number of shard axes";
   std::unordered_map<ffi::String, PrimExpr> result;
   for (size_t i = 0; i < shard.size(); ++i) {
-    auto it = result.find(shard[i]->axis->name);
+    auto it = result.find(shard[i]->axis.name());
     if (it == result.end()) {
-      result[shard[i]->axis->name] = analyzer->Simplify(coord[i] * 
shard[i]->stride);
+      result[shard[i]->axis.name()] = analyzer->Simplify(coord[i] * 
shard[i]->stride);
     } else {
-      result[shard[i]->axis->name] = analyzer->Simplify(it->second + coord[i] 
* shard[i]->stride);
+      result[shard[i]->axis.name()] = analyzer->Simplify(it->second + coord[i] 
* shard[i]->stride);
     }
   }
   // Add offset to the result
   for (const auto& [axis, off] : offset) {
-    auto it = result.find(axis->name);
+    auto it = result.find(axis.name());
     if (it == result.end()) {
-      result[axis->name] = analyzer->Simplify(off);
+      result[axis.name()] = analyzer->Simplify(off);
     } else {
-      result[axis->name] = analyzer->Simplify(it->second + off);
+      result[axis.name()] = analyzer->Simplify(it->second + off);
     }
   }
   return result;
@@ -387,7 +387,7 @@ ffi::Optional<ffi::Tuple<ExecScope, ExecScope>> 
TileLayoutNode::GetScope() const
     auto subtile_primitivet = axis->GetSubscope();
     auto tile_primitivet = axis->GetScope();
     TVM_FFI_ICHECK(subtile_primitivet.has_value() && 
tile_primitivet.has_value())
-        << "Thread axis " << axis->name << " has no subscope or scope";
+        << "Thread axis " << axis.name() << " has no subscope or scope";
 
     ffi::String subscope = subtile_primitivet.value()->name();
     ffi::String scope = tile_primitivet.value()->name();
diff --git a/src/tirx/ir/layout/tile_tile_ops.cc 
b/src/tirx/ir/layout/tile_tile_ops.cc
index 22450684f1..69c9f54afa 100644
--- a/src/tirx/ir/layout/tile_tile_ops.cc
+++ b/src/tirx/ir/layout/tile_tile_ops.cc
@@ -343,7 +343,7 @@ Layout TileLayoutNode::Tile(const TileLayout& outer_in, 
const Array<PrimExpr>& o
     auto inner_span_map = BuildSpanMap(inner);
     std::vector<Iter> new_shard;
     for (size_t i = 0; i < outer->shard.size(); ++i) {
-      auto it = inner_span_map.find(outer->shard[i]->axis->name);
+      auto it = inner_span_map.find(outer->shard[i]->axis.name());
       if (it != inner_span_map.end()) {
         new_shard.push_back(Iter(outer->shard[i]->extent, 
outer->shard[i]->stride * (*it).second,
                                  outer->shard[i]->axis));
@@ -493,7 +493,7 @@ ffi::Optional<TileLayout> TileLayoutNode::IsTileInner(
   // Get the span map of the inner layout of each axis
   auto inner_span_map = BuildSpanMap(layout);
   auto rescale_by_inner_span = [&](const Iter& iter) -> ffi::Optional<Iter> {
-    auto it = inner_span_map.find(iter->axis->name);
+    auto it = inner_span_map.find(iter->axis.name());
     if (it != inner_span_map.end() && !is_one(iter->extent)) {
       if (!analyzer->CanProveEqual(floormod(iter->stride, (*it).second), 0)) {
         return std::nullopt;
diff --git a/src/tirx/ir/layout/utils.cc b/src/tirx/ir/layout/utils.cc
index fe41905028..988c168549 100644
--- a/src/tirx/ir/layout/utils.cc
+++ b/src/tirx/ir/layout/utils.cc
@@ -44,8 +44,8 @@ PrimExpr FlattenCoord(const Array<PrimExpr>& coord, const 
Array<PrimExpr>& shape
 ffi::Map<ffi::String, PrimExpr> BuildSpanMap(const TileLayout& layout) {
   ffi::Map<ffi::String, PrimExpr> span_map;
   for (const auto& iter : layout->shard) {
-    if (span_map.find(iter->axis->name) == span_map.end()) {
-      span_map.Set(iter->axis->name, layout->GetSpan(iter->axis->name));
+    if (span_map.find(iter->axis.name()) == span_map.end()) {
+      span_map.Set(iter->axis.name(), layout->GetSpan(iter->axis.name()));
     }
   }
   return span_map;
@@ -74,7 +74,7 @@ std::vector<PrimExpr> GetDefaultStrides(const 
ffi::Array<PrimExpr>& data, PrimEx
 
 bool AxisMatchesFilter(const Axis& axis, const ffi::Optional<ffi::String>& 
axis_name) {
   return (!axis_name.has_value() && axis->IsMemoryAxis()) ||
-         (axis_name.has_value() && axis->name == axis_name.value());
+         (axis_name.has_value() && axis.name() == axis_name.value());
 }
 
 TileLayout IdentityTileLayout(const ffi::Array<PrimExpr>& shape) {
diff --git a/src/tirx/script/printer/buffer.cc 
b/src/tirx/script/printer/buffer.cc
index 96fe33a918..76f1a93611 100644
--- a/src/tirx/script/printer/buffer.cc
+++ b/src/tirx/script/printer/buffer.cc
@@ -419,7 +419,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 TVM_FFI_STATIC_INIT_BLOCK() {
   IRDocsifier::vtable().set_dispatch<tirx::Axis>(
       "", [](tirx::Axis axis, AccessPath p, IRDocsifier d) -> Doc {
-        return LiteralDoc::Str(axis->name, p->Attr("name"));
+        return LiteralDoc::Str(axis.name(), p->Attr("name"));
       });
 }
 
@@ -428,7 +428,7 @@ TVM_FFI_STATIC_INIT_BLOCK() {
       "", [](tirx::Iter iter, AccessPath p, IRDocsifier d) -> Doc {
         return TIR(d, "Iter")->Call({d->AsDoc<ExprDoc>(iter->extent, 
p->Attr("extent")),
                                      d->AsDoc<ExprDoc>(iter->stride, 
p->Attr("stride")),
-                                     d->AsDoc<ExprDoc>(iter->axis->name, 
p->Attr("axis"))},
+                                     d->AsDoc<ExprDoc>(iter->axis.name(), 
p->Attr("axis"))},
                                     {}, {});
       });
 }
@@ -438,8 +438,8 @@ Doc PrintTileLayout(tirx::TileLayout layout, IRDocsifier d, 
AccessPath p) {
 
   // `value @ Axis.<name>`, but elide `@m` (the default memory axis).
   auto bind_axis = [&](ExprDoc value, const tirx::Axis& axis) -> ExprDoc {
-    if (axis->name == "m") return value;
-    return OperationDoc(OpKind::kMatMul, {value, 
IdDoc("Axis")->Attr(axis->name)});
+    if (axis.name() == "m") return value;
+    return OperationDoc(OpKind::kMatMul, {value, 
IdDoc("Axis")->Attr(axis.name())});
   };
 
   // Build `head[(e0, e1, ...) : (s0@a0, s1@a1, ...)]` (or 1D shorthand
@@ -465,7 +465,7 @@ Doc PrintTileLayout(tirx::TileLayout layout, IRDocsifier d, 
AccessPath p) {
     if (layout->offset.size() > 0) {
       ffi::Array<ExprDoc> offset_keys, offset_values;
       for (const auto& [axis, off] : layout->offset) {
-        offset_keys.push_back(LiteralDoc::Str(axis->name, p->Attr("axis")));
+        offset_keys.push_back(LiteralDoc::Str(axis.name(), p->Attr("axis")));
         offset_values.push_back(d->AsDoc<ExprDoc>(off, p->Attr("offset")));
       }
       keys.push_back("offset");
@@ -496,7 +496,7 @@ Doc PrintTileLayout(tirx::TileLayout layout, IRDocsifier d, 
AccessPath p) {
     std::vector<std::pair<tirx::Axis, PrimExpr>> 
sorted_offset(layout->offset.begin(),
                                                                
layout->offset.end());
     std::sort(sorted_offset.begin(), sorted_offset.end(),
-              [](const auto& a, const auto& b) { return a.first->name < 
b.first->name; });
+              [](const auto& a, const auto& b) { return a.first.name() < 
b.first.name(); });
 
     // Build the offset as a single arithmetic expression first, then add it
     // to the spec in one `+`. Chaining `spec + term1 + term2` would re-enter
diff --git a/tests/python/test_tirx_axis_enum.py 
b/tests/python/test_tirx_axis_enum.py
new file mode 100644
index 0000000000..aa0329ffc3
--- /dev/null
+++ b/tests/python/test_tirx_axis_enum.py
@@ -0,0 +1,65 @@
+# 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.
+"""Axis enum identity and import behavior across compiler and runtime 
builds."""
+
+import os
+import subprocess
+import sys
+
+import pytest
+from tvm_ffi.dataclasses import Enum
+
+import tvm
+from tvm.tirx.layout import Axis, S, TileLayout, laneid
+
+
+def test_axis_enum_identity_and_attributes():
+    axis = Axis.laneid
+    assert isinstance(axis, Enum)
+    assert axis.same_as(Axis.get("laneid"))
+    assert axis.same_as(laneid)
+    assert axis.name == "laneid"
+    assert axis in Axis.all_entries()
+    assert hash(axis) == hash(Axis.get("laneid"))
+    assert Axis.def_attr("thread")[axis] is True
+    assert Axis.m.is_memory()
+    assert Axis.bx.get_scope().name == "thread"
+    assert Axis.bx.get_subscope().name == "cta"
+    assert TileLayout(S[4 : 1 @ axis]).shard[0].axis.same_as(axis)
+
+
+def test_axis_unknown_name_and_json_roundtrip():
+    axis = Axis.get("axis_enum_unknown_test")
+    assert axis.same_as(Axis.get("axis_enum_unknown_test"))
+    assert axis.get_scope() is None
+    assert axis.get_subscope() is None
+    with pytest.raises(Exception, match="has no thread classification"):
+        axis.is_thread()
+    with pytest.raises(Exception, match="has no thread classification"):
+        axis.is_memory()
+    assert tvm.ir.load_json(tvm.ir.save_json(axis)).same_as(axis)
+    assert tvm.ir.load_json(tvm.ir.save_json(Axis.laneid)).same_as(Axis.laneid)
+
+
+def test_axis_runtime_only_import():
+    env = os.environ.copy()
+    env["TVM_USE_RUNTIME_LIB"] = "1"
+    subprocess.run(
+        [sys.executable, "-c", "import tvm; import tvm.tirx.layout; assert 
tvm.base._RUNTIME_ONLY"],
+        env=env,
+        check=True,
+    )

Reply via email to