[Solved] How is __tvm_module_startup invoked?

I’m studying apps/bundle_deploy example code which runs on crt runtime. This function was called at the beginning to pre-load all operator functions:

// src/runtime/crt/crt_backend_api.c:
int TVMBackendRegisterSystemLibSymbol(const char* name, void* ptr) {
  g_fexecs = vrealloc(g_fexecs, sizeof(TVMPackedFunc) * (g_fexecs_count + 1));
  snprintf(g_fexecs[g_fexecs_count].name, sizeof(g_fexecs[g_fexecs_count].name), "%s", name);
  g_fexecs[g_fexecs_count].fexec = ptr;
  g_fexecs_count++;
  return 0;
}

However, I cannot find where this function was called by browsing source code. A brutal grep search indicates this one might be built within llvm host-side.

$ grep -R SystemLibSymbol *
Binary file apps/bundle_deploy/build/demo_static matches
Binary file apps/bundle_deploy/build/model.o matches
Binary file apps/bundle_deploy/build/bundle_static.o matches
Binary file apps/bundle_deploy/build/bundle.so matches
Binary file build/libtvm.so matches
Binary file build/libtvm_runtime.so matches
Binary file build/CMakeFiles/tvm.dir/src/target/llvm/codegen_cpu.cc.o matches
Binary file build/CMakeFiles/tvm.dir/src/target/llvm/codegen_blob.cc.o matches
Binary file build/CMakeFiles/tvm.dir/src/target/codegen.cc.o matches
Binary file build/CMakeFiles/tvm.dir/src/runtime/system_library.cc.o matches
Binary file build/CMakeFiles/tvm_runtime.dir/src/runtime/system_library.cc.o matches
include/tvm/runtime/c_backend_api.h:TVM_DLL int TVMBackendRegisterSystemLibSymbol(const char* name, void* ptr);
rust/runtime/src/module/syslib.rs:pub extern "C" fn TVMBackendRegisterSystemLibSymbol(
src/target/llvm/codegen_blob.cc:    // Create TVMBackendRegisterSystemLibSymbol function
src/target/llvm/codegen_blob.cc:                               llvm::Twine("TVMBackendRegisterSystemLibSymbol"), module.get());
src/target/llvm/codegen_cpu.cc:        llvm::Function::ExternalLinkage, "TVMBackendRegisterSystemLibSymbol", module_.get());
src/target/codegen.cc:    os << "extern int TVMBackendRegisterSystemLibSymbol(const char*, void*);\n";
src/target/codegen.cc:       << "TVMBackendRegisterSystemLibSymbol(\"" << runtime::symbol::tvm_dev_mblob << "\", (void*)"
src/runtime/system_library.cc:int TVMBackendRegisterSystemLibSymbol(const char* name, void* ptr) {
src/runtime/crt/crt_backend_api.c:int TVMBackendRegisterSystemLibSymbol(const char* name, void* ptr) {

Gdb calling stack shows it was called by __tvm_module_startup:

TVMBackendRegisterSystemLibSymbol(const char * ....)
__tvm_module_startup
__libc_csu_init
libc.so.6!__libc_start_main(int (*)(int, char...)
_start

Can anyone help point out how is __tvm_module_startup being invoked in source code or linker script somewhere?

@liangfu, I think this question goes to you best, :slight_smile:

The above question originates when I looked at this function in src/runtime/crt/packed_func.h:

TVMPackedFunc* g_fexecs = 0;
uint32_t g_fexecs_count = 0;

// Implement TVMModule::GetFunction
// Put implementation in this file so we have seen the TVMPackedFunc
static inline void TVMModule_GetFunction(TVMModule* mod, const char* name, TVMPackedFunc* pf) {
  int idx;
  memset(pf, 0, sizeof(TVMPackedFunc));
  assert(strlen(name) <= sizeof(pf->name));
  snprintf(pf->name, strlen(name), "%s", name);
  pf->Call = TVMPackedFunc_Call;
  pf->SetArgs = TVMPackedFunc_SetArgs;
  pf->fexec = &TVMNoOperation;
  for (idx = 0; idx < g_fexecs_count; idx++) {
    if (!strcmp(g_fexecs[idx].name, name)) {
      pf->fexec = g_fexecs[idx].fexec;
      break;
    }
  }
  if (idx == g_fexecs_count) {
    fprintf(stderr, "function handle for %s not found\n", name);
  }
}

I cannot find where g_fexecs being initialized, which leads me to the TVMBackendRegisterSystemLibSymbol() function. However, I cannot find where it was invoked in TVM source tree. Thanks in advance.

I think I got it. The LLVM host module compilation generates it.

src/target/llvm/llvm_module.cc:

TVM_REGISTER_GLOBAL("target.build.llvm").set_body_typed([](IRModule mod, std::string target) {
  auto n = make_object<LLVMModuleNode>();
  n->Init(mod, target);
  return runtime::Module(n);
});
...
  void Init(const IRModule& mod, std::string target) {
    InitializeLLVM();
    tm_ = GetLLVMTargetMachine(target);
    ...
    module_ = cg->Finish();
    ...
}

The cg->Finish() calls src/target/llvm/codegen_llvm.cc:

std::unique_ptr<llvm::Module> CodeGenLLVM::Finish() {
  this->AddStartupFunction();
  for (size_t i = 0; i < link_modules_.size(); ++i) {
    CHECK(!llvm::Linker::linkModules(*module_, std::move(link_modules_[i])))
        << "Failed to link modules";
  }
  link_modules_.clear();
  // optimize
  this->Optimize();
  return std::move(module_);
}

The AddStartupFunction() is overridden in src/target/llvm/codegen_cpu.cc. Here you see the __tvm_module_startup.

void CodeGenCPU::AddStartupFunction() {
  if (export_system_symbols_.size() != 0) {
    llvm::FunctionType* ftype = llvm::FunctionType::get(t_void_, {}, false);
    function_ = llvm::Function::Create(ftype, llvm::Function::InternalLinkage,
                                       "__tvm_module_startup", module_.get());
    llvm::BasicBlock* startup_entry = llvm::BasicBlock::Create(*ctx_, "entry", function_);
    builder_->SetInsertPoint(startup_entry);
    for (const auto& kv : export_system_symbols_) {
      llvm::Value* name = GetConstString(kv.first);
      builder_->CreateCall(f_tvm_register_system_symbol_,
                           {name, builder_->CreateBitCast(kv.second, t_void_p_)});
    }
    llvm::appendToGlobalCtors(*module_, function_, 65535);
    builder_->CreateRet(nullptr);
  }
}

The “target.build.llvm” is invoked by src/target/codegen.cc:

runtime::Module Build(IRModule mod, const Target& target) {
  if (BuildConfig::Current()->disable_assert) {
    mod = tir::transform::SkipAssert()(mod);
  }

  std::string build_f_name = "target.build." + target->target_name;
  const PackedFunc* bf = runtime::Registry::Get(build_f_name);
  CHECK(bf != nullptr) << "target.build." << target << " is not enabled";
  return (*bf)(mod, target->str());
}

The codegen::Build() is invoked by src/driver/driver_api.cc:

// Build for heterogeneous execution.
runtime::Module build(const Map<Target, IRModule>& inputs, const Target& target_host,
                      const BuildConfig& config) {
...
  for (const auto& it : inputs) {

    auto pair = split_dev_host_funcs(it.second, it.first, target_host_val, config);
    auto& mhost = pair.first;
    auto& mdevice = pair.second;

    mhost_all->Update(mhost);

    if (mdevice->functions.size() != 0) {
      device_modules.push_back(codegen::Build(mdevice, it.first));
    }
  }

  runtime::Module mhost = codegen::Build(mhost_all, target_host_val);

One thing I still don’t understand is how __tvm_module_startup is being called at program startup though? @liangfu? Anyone?

From this stackoverflow page, I got the answer finally.

" You can put the code you want to run early into a function and add that function to llvm.global_ctors . This is the equivalent of using __attribute__((constructor)) in C or C++.

To do this from a pass, you can use the llvm::appendToGlobalCtors function, which is declared in llvm/Transforms/Utils/ModuleUtils.h ."

Hi @jinchenglee,

In my observation, the TVMBackendRegisterSystemLibSymbol function is called inside the generated code, so we don’t need to register the functions externally. This is typically helpful when we can’t load compiled functions with dlopen.