Cannot list attrs of tvm.ir.Array via dir

Hello all, this day when I try to use dir() to list all valid attributes of a tvm.ir.container.Array object, I get a TypeError which tells me Array is not registered via TVM_REGISTER_NODE_TYPE, and I get a similar result of listing tvm.ir.container.Map.

Here is my script and output:

import tvm

test_array = [1,2,3]

tvm_test_array = tvm.runtime.convert(test_array)

test_key = 'key'
tvm_test_key = tvm.runtime.convert(test_key)
test_dict = {tvm_test_key: 1}
tvm_test_dict = tvm.runtime.convert(test_dict)

try:
    print(type(tvm_test_array))
    dir(tvm_test_array)
except Exception as e:
    print(e)
    
try:
    print(type(tvm_test_dict))
    dir(tvm_test_dict)
except Exception as e:
    print(e)
python3 test_attr.py
# output of print(type(tvm_test_array))
<class 'tvm.ir.container.Array'>
# Exception of dir(tvm_test_array)
Traceback (most recent call last):
  4: TVMFuncCall
  3: _ZNSt17_Function_handlerIFvN3tvm7runtime7TVMArgsEPNS1
  2: tvm::NodeListAttrNames(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*)
  1: tvm::ReflectionVTable::ListAttrNames[abi:cxx11](tvm::runtime::Object*) const
  0: tvm::ReflectionVTable::VisitAttrs(tvm::runtime::Object*, tvm::AttrVisitor*) const
  File "/home/syang/tvm/include/tvm/node/reflection.h", line 390
TypeError: Array is not registered via TVM_REGISTER_NODE_TYPE
# output of print(type(tvm_test_dict))
<class 'tvm.ir.container.Map'>
# Exception of dir(tvm_test_dict)
Traceback (most recent call last):
  4: TVMFuncCall
  3: _ZNSt17_Function_handlerIFvN3tvm7runtime7TVMArgsEPNS1
  2: tvm::NodeListAttrNames(tvm::runtime::TVMArgs, tvm::runtime::TVMRetValue*)
  1: tvm::ReflectionVTable::ListAttrNames[abi:cxx11](tvm::runtime::Object*) const
  0: tvm::ReflectionVTable::VisitAttrs(tvm::runtime::Object*, tvm::AttrVisitor*) const
  File "/home/syang/tvm/include/tvm/node/reflection.h", line 390
TypeError: Map is not registered via TVM_REGISTER_NODE_TYPE

To further explore the root cause, I read the source code and follow call its chains:

First, since tvm.ir.container.Array extends tvm.runtime.Object, when we call dir(), we actually call __dir__(self):

  class Object(ObjectBase):
      """Base class for all tvm's runtime objects."""  
      def __dir__(self):
          class_names = dir(self.__class__)
          fnames = _ffi_node_api.NodeListAttrNames(self)
          size = fnames(-1)
          return sorted([fnames(i) for i in range(size)] + class_names)

Then, we will execute _ffi_node_api.NodeListAttrNames(self), whose backend implementation is tvm::ReflectionVTable::ListAttrNames in src/node/reflection.cc.

std::vector<std::string> ReflectionVTable::ListAttrNames(Object* self) const {
  std::vector<std::string> names;
  AttrDir dir;
  dir.names = &names;

  if (!self->IsInstance<DictAttrsNode>()) {
    VisitAttrs(self, &dir);
  } else {
    // specially handle dict attr
    DictAttrsNode* dnode = static_cast<DictAttrsNode*>(self);
    for (const auto& kv : dnode->dict) {
      names.push_back(kv.first);
    }
  }
  return names;
}

According to its source code we can find that it will execute tvm::ReflectionVTable::VisitAttrs implemented in include/tvm/node/reflection.h. We can find the error occurs when fvisit_attrs_[tindex] == nullptr is true.

inline void ReflectionVTable::VisitAttrs(Object* self, AttrVisitor* visitor) const {
  uint32_t tindex = self->type_index();
  if (tindex >= fvisit_attrs_.size() || fvisit_attrs_[tindex] == nullptr) {
    LOG(FATAL) << "TypeError: " << self->GetTypeKey()
               << " is not registered via TVM_REGISTER_NODE_TYPE";
  }
  fvisit_attrs_[tindex](self, visitor);
}

So does Array and Map don’t register themselves? To answer the question, I read their source codes and find that actually they finish their registering in src/runtime/container.cc, Line 38 and Line 123 respectively. And TVM_REGISTER_OBJECT_TYPE will call ReflectionVTable::Register() which is defined in include/tvm/node/reflection.h:

template <typename T, typename TraitName>
inline ReflectionVTable::Registry ReflectionVTable::Register() {
  uint32_t tindex = T::RuntimeTypeIndex();
  if (tindex >= fvisit_attrs_.size()) {
    fvisit_attrs_.resize(tindex + 1, nullptr);
    fcreate_.resize(tindex + 1, nullptr);
    frepr_bytes_.resize(tindex + 1, nullptr);
    fsequal_reduce_.resize(tindex + 1, nullptr);
    fshash_reduce_.resize(tindex + 1, nullptr);
  }
  // functor that implements the redirection.
  fvisit_attrs_[tindex] = ::tvm::detail::SelectVisitAttrs<T, TraitName>::VisitAttrs;

  fsequal_reduce_[tindex] = ::tvm::detail::SelectSEqualReduce<T, TraitName>::SEqualReduce;

  fshash_reduce_[tindex] = ::tvm::detail::SelectSHashReduce<T, TraitName>::SHashReduce;

  return Registry(this, tindex);
}

Now we find that all functions in fvisit_attrs_ are actually equal to ::tvm::detail::SelectVisitAttrs<T, TraitName>::VisitAttrs, which is defined in src/node/structural_hash.cc, for Array and Map, they are defined in Line 370 and Line 394:

struct ArrayNodeTrait {
  static constexpr const std::nullptr_t VisitAttrs = nullptr;
	// ignores others
};
TVM_REGISTER_REFLECTION_VTABLE(ArrayNode, ArrayNodeTrait)
    .set_creator([](const std::string&) -> ObjectPtr<Object> {
      return ::tvm::runtime::make_object<ArrayNode>();
    });

struct MapNodeTrait {
  static constexpr const std::nullptr_t VisitAttrs = nullptr;
	// ignore others
};
TVM_REGISTER_REFLECTION_VTABLE(MapNode, MapNodeTrait)
    .set_creator([](const std::string&) -> ObjectPtr<Object> { return MapNode::Empty(); });

Now the root cause is clear since there is a nullptr as a default value for ArrayNode and MapNode, every time when we call tvm::ReflectionVTable::VisitAttrs, the fvisit_attrs_[tindex] == nullptr will be always true, and there is no doubt that we will get a type error like XXX is not registered via TVM_REGISTER_NODE_TYPE.

1 Like

Is my analysis correct?And if yes, how can we fix this unexpected behavior? I think it will be a bug which may affect our use :rofl:

Thanks for the analysis, indeed we might need special case handling for the containers, at least have a proper error message saying that dir is not supported

Thank you for your reply!

Through further study, I found ReflectionVTable::VisitAttrs is not only used in ReflectionVTable::ListAttrNames , but also called in another functionReflectionVTable::GetAttr , a backend implementation of python API __getattr__(self, name) defined in tvm.runtime.Object . It means the same error may occur when we try to get an attr of the containers. So I totally agree that a special case handling for these containers might help a lot for developers :grinning_face_with_smiling_eyes:

Would you be interested in send a PR? cc @zxybazh @junrushao @vinx13

Yep I am aware of this bug and encountered it several times before :slight_smile: Thanks for the in-depth analysis!

CC: @comaniac i think we discussed about it before

Thanks for the analysis and it would be great if you could send a fix :slight_smile:

Thank you all for your reply!

I think we can fix this incorrect behavior by adding a special function VisitAttrs, which does nothing but returns an empty set of attrs. And what about your suggestions to fix this bug? Let’s fix it in an appropriate way. :grinning_face_with_smiling_eyes: @junrushao @comaniac

And I have sent a PR to fix this bug in https://github.com/apache/tvm/pull/8920. Could you please take a look at this PR?