tvm::runtime::Object

tvm::runtime::Object#

参考:tvm/include/tvm/runtime/object.h

TypeIndex#

源码中为什么要将 enum 类型放在 struct 结构体中呢?

参考:C++ enum 命名冲突问题

包含在 struct{} 中,相当于在一个命名空间下,这样已经能够避免命名冲突的问题,而且 TypeIndex::ENumName 使用起来比较方便。

TypeIndex 用于 tvm::runtime::Object 的成员变量 _type_index

使用 Python 模拟 TypeIndex(共 4 类):

from enum import Enum

class TypeIndex(Enum):
    kRoot: int = 0 # 1. root object 类型
    # 2. 标准的静态索引赋值,前端可以利用这些常量。
    kRuntimeModule: int = 1 # runtime::Module
    kRuntimeNDArray: int = 2 # runtime::NDArray
    kRuntimeString: int = 3 # runtime::String
    kRuntimeArray: int = 4 # runtime::Array
    kRuntimeMap: int = 5 # runtime::Map
    kRuntimeShapeTuple: int = 6 # runtime::ShapeTuple
    kRuntimePackedFunc: int = 7 # runtime::PackedFunc
    # 3. 可能需要更改的静态赋值。
    kRuntimeClosure: int = 8
    kRuntimeADT: int = 9
    kStaticIndexEnd: int = 10
    kDynamic = kStaticIndexEnd # 4. 类型索引在运行时分配。

Object#

tvm::runtime::Object 是 TVM 对象容器的基类。

子类应声明以下静态常量字段:

  • _type_index: 对象的静态类型索引,如果分配给 TypeIndex::kDynamic,则在运行时分配类型索引。可以通过 ObjectType::TypeIndex() 访问运行时类型索引。

  • _type_key: 类型的唯一字符串标识符。

  • _type_final: 该类型是否为终端类型(对象系统中没有该类型的子类)。此字段由宏 TVM_DECLARE_FINAL_OBJECT_INFO 自动设置。仍然可以对终端对象类型 T 进行子类化,并使用 make_object 构造它。但是 IsInstance 检查只会显示对象类型是 T (而不是子类)。

如何定义 tvm::runtime::Object 子类?

需要包含两个字段:_type_child_slots_type_child_slots_can_overflow

  • _type_child_slots:表示为当前对象类型预留的子类类型索引槽位数,用于运行时优化 IsInstance 中的类型检查。如果对象的类型索引在 [type_index, type_index + _type_child_slots] 范围内,则可以快速判断该对象是否为当前对象类型的子类。否则,将使用回退机制来检查全局类型表。建议将其设置为估计所需的子类数量。

  • _type_child_slots_can_overflow:表示是否可以在子类数量超过 _type_child_slots 的情况下添加额外的子类。如果为 true,则会使用回退机制来检查全局类型表。建议将其设置为 false,以获得最优的运行时速度(如果我们知道确切的子类数量)。

此外,还介绍了两个宏:TVM_DECLARE_BASE_OBJECT_INFOTVM_DECLARE_FINAL_OBJECT_INFO,用于声明可以被子类化的对象和不可被子类化的对象的辅助函数。

也可以使用:

  • make_object:用于创建具有给定 type_index 和 deleter 的新对象的函数。它用于创建动态类型的对象,这些对象可以被其他对象子类化。

  • ObjectPtr:表示指向对象的指针的类。它提供了管理由指针指向的对象生命周期的方法。

  • ObjectRef:表示引用对象的类。

示例:

class BaseObj :public Object {
public:
    // 对象字段
    int field0;

    // 对象属性
    static constexpr const uint32_t _type_index = TypeIndex::kDynamic;
    static constexpr const char* _type_key = "test.BaseObj";
    // 告诉 TVM 编译器,BaseObj 类是 Object 类的子类,并且需要在编译时进行一些特殊的处理。
    TVM_DECLARE_BASE_OBJECT_INFO(BaseObj, Object);
};

class LeafObj :public BaseObj {
public:
    // 字段
    int child_field0;
    // 对象属性
    static constexpr const uint32_t _type_index = TypeIndex::kDynamic;
    static constexpr const char* _type_key = "test.LeafObj";
    TVM_DECLARE_BASE_OBJECT_INFO(LeafObj, Object);
};

还需要注册:

TVM_REGISTER_OBJECT_TYPE(BaseObj);
TVM_REGISTER_OBJECT_TYPE(LeafObj);

接下来,便可使用:

void TestObjects() {
    // 创建对象
    ObjectRef leaf_ref(make_object<LeafObj>());
    // 转换为特定实例
    const LeafObj* leaf_ptr = leaf_ref.as<LeafObj>();
    ICHECK(leaf_ptr != nullptr);
    // 也可以转换为基类
    ICHECK(leaf_ref.as<BaseObj>() != nullptr);
}

小技巧

TVM 里有个不成文的约定,所有以 Node 为结尾的类名都是继承自 Object,不以 Node 结尾的类名都是继承自 ObjectRef

对象系统常用宏#

  • TVM_DECLARE_BASE_OBJECT_INFO(TypeName, ParentType) 是辅助宏,用于声明可以被继承的基础对象类型。它接受两个参数 TypeNameParentType,分别表示当前类型的名称和父类型的名称。 在宏内部,首先使用 static_assert 进行编译时断言,确保父类型没有被标记为 final(即不可继承)。然后定义了名为 RuntimeTypeIndex() 的静态函数,用于获取对象的运行时类型索引。在 RuntimeTypeIndex() 函数内部,再次使用 static_assert 进行编译时断言,确保当父类型指定了子类型插槽数时,当前类型也指定了相应的子类型插槽数。然后通过判断当前类型的索引是否为动态类型来确定是否需要调用 _GetOrAllocRuntimeTypeIndex() 函数来获取或分配运行时类型索引。_GetOrAllocRuntimeTypeIndex() 函数内部使用了 Object::GetOrAllocRuntimeTypeIndex() 函数来获取或分配运行时类型索引,并返回该索引。

  • TVM_DECLARE_FINAL_OBJECT_INFO(TypeName, ParentType) 是辅助宏,用于在最终类中声明类型信息。它接受两个参数 TypeNameParentType,分别表示当前类型的名称和父类型的名称。 在宏内部,首先使用 static const constexpr 定义了两个静态常量变量 _type_final_type_child_slots,分别表示当前类型是否为最终类型和子类型插槽数。然后,调用TVM_DECLARE_BASE_OBJECT_INFO(TypeName, ParentType)宏来声明基础对象类型信息。

  • TVM_ATTRIBUTE_UNUSED 是宏定义,用于消除未使用变量或函数的警告。根据编译器的不同,宏的定义方式也不同。在支持 GCC 编译器的平台上,使用 __attribute__((unused)) 来定义该宏;在其他平台上,则直接定义为空。

  • TVM_STR_CONCAT_(__x, __y)TVM_STR_CONCAT(__x, __y) 是两个字符串连接的宏定义。它们的作用是将两个字符串连接起来,生成新的字符串。

  • TVM_OBJECT_REG_VAR_DEF 是宏定义,用于定义静态的、未使用的变量。它使用了 TVM_ATTRIBUTE_UNUSED 宏来消除未使用变量的警告。这个变量的类型为 uint32_t,名称为 __make_Object_tid

  • TVM_REGISTER_OBJECT_TYPE(TypeName) 是辅助宏,用于将对象类型注册到运行时。它接受参数 TypeName,表示要注册的对象类型的名称。在宏内部,使用 TVM_STR_CONCAT 宏将 TVM_OBJECT_REG_VAR_DEF__COUNTER__ 连接起来,生成新的字符串。然后,将该字符串赋值为 TypeName::_GetOrAllocRuntimeTypeIndex() 的返回值,即该对象的运行时类型索引。这个宏的作用是确保每个终端类都被正确地注册到运行时类型表中。

  • TVM_DEFINE_DEFAULT_COPY_MOVE_AND_ASSIGN(TypeName) 是辅助宏,用于定义默认的拷贝/移动构造函数和赋值运算符。它接受参数 TypeName,表示要定义构造函数和赋值运算符的类的名称。在宏内部,使用 TypeName(const TypeName& other) = default;TypeName(TypeName&& other) = default;TypeName& operator=(const TypeName& other) = default;TypeName& operator=(TypeName&& other) = default; 语句分别定义了拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符的默认实现。

  • TVM_DEFINE_OBJECT_REF_METHODS(TypeName, ParentType, ObjectName) 是辅助宏,用于定义对象引用的方法。它接受三个参数 TypeNameParentTypeObjectName,分别表示对象类型名称、父类型名称和对象名称。在宏内部,首先使用 TypeName() = default; 语句定义了默认构造函数。然后,使用 explicit TypeName(::tvm::runtime::ObjectPtr<::tvm::runtime::Object> n) : ParentType(n) {} 语句定义了带有 ::tvm::runtime::ObjectPtr<::tvm::runtime::Object> 参数的构造函数,并将传入的参数赋值给 ParentType 成员变量。接着,使用 TVM_DEFINE_DEFAULT_COPY_MOVE_AND_ASSIGN(TypeName); 语句重新定义了默认的拷贝/移动构造函数和赋值运算符。最后,使用 const ObjectName* operator->() const { return static_cast<const ObjectName*>(data_.get()); }const ObjectName* get() const { return operator->(); } 语句定义了对象引用的方法,包括箭头运算符重载和 operator->() 方法。