Skip to content

runtime

runtime中的代码主要干了三件事情

  • 初始化过程,解析graph,转换为内部数据结构,调用DeviceAPI,在device上分配内存空间
  • 参数设置过程,将参数copy到分配好的device内存空间上
  • 运行过程,遍历执行解析完的graph中的function

runtime-flow

example tvm:build with target cuda and target host llvm

(split_dev_host_funcs) -> (fdevice)
(split_dev_host_funcs) -> (fhost)
(lowered_funcs) -> (split_dev_host_funcs) : tvm::build

(fdevice) --> (codegen.build_cuda)
rectangle "device code" {
    (CUDAModuleNode : runtime::ModuleNode) as CUDAModuleNode
    (codegen.build_cuda) --> CUDAModuleNode
}

(fhost) --> (codegen.build_llvm)
rectangle "host code" {
    (LLVMModuleNode : runtime::ModuleNode) as LLVMModuleNode
    (codegen.build_llvm) --> LLVMModuleNode
    LLVMModuleNode --> (TVMBackendGetFuncFromEnv)
    (TVMBackendGetFuncFromEnv) -> (TVMFuncCall)
}

CUDAModuleNode .left.> LLVMModuleNode: LLVMModuleNode.Import(CUDAModuleNode)
(TVMBackendGetFuncFromEnv) .up.> CUDAModuleNode: CUDAModuleNode.GetFunction
(TVMFuncCall) .> CUDAModuleNode: PackedFunc(CUDAWrappedFunc)

runtime work flow

rectangle greaph_runtime.creat {
    (根据python传入的ctx, 构建c++内部定义的TVMContext vector) as creat_step1
    note left
        从python/tvm/contrib/graph_runtime.py
        进入src/runtime/graph/graph_runtime.cc
    endnote

    (实例化第三方软件dmlc的JSONReader) as creat_step2
    note right
        dmlc(Distributed Machine Learning Common Codebase)
        分布式机器学习通用代码库,
        提供了构建高效且可扩展的分布式机器学习库的能力
        GraphRuntime 依赖于 JSONReader
    endnote

    (利用JSONReader将graph.json转化为内部数据结构, 完成load过程) as creat_step3

    (将graph的attrs_.dltype转换为TVMType) as creat_step4
    note left
        "attrs": {
            "dltype": [
                "list_str", 
                [
                    "float32", 
                    "float32",
                ]
            ],
    endnote

    (构建池节点pool_entry) as creat_step5
    note left
        bytes = dltype[i] * shape[i]
        从所有节点中给出每个storage_id对应的节点的bytes最大值
    endnote

    (每个storage_id对应的storage_pool_使用DeviceAPI AllocDataSpace) as creat_step6
    note right
        所有的target的实现都继承于DeviceAPI接口
        DeviceAPI 受 DeviceAPIManager 管理
        通过 "device_api." + target 的方式可以找到具体 DeviceAPI
        具体 DeviceAPI 通过 TVM_REGISTER_GLOBAL 进行注册
        具体 DeviceAPI 分布在 src/runtime 主目录及各个子目录下
    endnote

    (根据storage_pool_ 构建 data_entry_, 完成SetupStorage过程) as creat_step7

    (遍历graph中所有非‘null’节点,\n \
    构建DLTensor向量, \n \
    并且获取每一个node的func组成op_execs_向量) as creat_step8

    creat_step1 -right-> creat_step2
    creat_step2 -down-> creat_step3
    creat_step3 -left-> creat_step4
    creat_step4 -down-> creat_step5
    creat_step5 -right-> creat_step6
    creat_step6 -down-> creat_step7
    creat_step7 -left-> creat_step8

    (creat end) as ce
    creat_step8 --> ce
}

rectangle "tvm build result"{
    (Graph)
    note left
        a string of dict, similar to str({})
        "{
            "nodes": [
                {
                    "op": "null",
                    "name": "x",
                    "inputs": []
                },
                {
                    "op": "tvm_op",
                    "name": "relu0",
                    "attrs": {
                        "flatten_data": "0",
                        "func_name": "fuse_l2_normalize_relu",
                        "num_inputs": "1",
                        "num_outputs": "1"
                    },
                    "inputs": [[0, 0, 0]]  
                }
            ],
            "arg_nodes": [0],
            "node_row_ptr": [0, 1, 2],
            "heads": [[1, 0, 0]],
            ......
        }"
    endnote
    (Graph) ..> creat_step1

    (Lib)
    note left
        a Module object, contain lib.so
    endnote
    (Lib) ..> creat_step1

    (params)
    note left
        a dict contains node params ...
        {'p22': <tvm.NDArray shape=(8, 16, 3, 3, 8, 32), cpu(0)>
            array([[[[[[-2.49230769e-02,  2.73413258e-03, ...,
             7.61547452e-03, -6.19848166e-03, -2.52313819e-02],
           [ 2.66786274e-02,  4.06193052e-04,  5.14294626e-03, ...,
            -3.45390639e-03,  4.50841105e-03,  5.40218735e-03],
         ......
        }
    endnote
}

(ctx)
note left
    a object class TVMContext or a list of TVMContext
    tvm.cpu(0)
    tvm.gpu(0)
    tvm.opencl(0)
endnote
(ctx) ..> creat_step1

(GraphModule)
note right
    a wrapper of class GraphRuntime in c++
    本质上是一个Module实例,这个实例作为container包含了GraphRuntime实例
endnote
ce --> (GraphModule): return a object of GraphModule

(picture image data) as input_data

(调用set_input完成参数输入) as set_input
note right
    call by PackedFunc GraphRuntime::GetFunction from python to c++
    将输入数据copy到data_entry_对应的node里面
    如果输入参数是param,循环是在python侧完成的
    c++ 接口每次只接受一个node的数据
endnote
params ..> set_input
input_data ..> set_input
(GraphModule) --> set_input


(GraphModule.run) as run
note right
    so easy.............

    void GraphRuntime::Run() {
    // setup the array and requirements.
    for (size_t i = 0; i < op_execs_.size(); ++i) {
        if (op_execs_[i]) op_execs_[i]();
    }
    }
endnote
set_input --> run

(GraphModule.get_output) as get_output
note right
    从data_entry_中获取输出节点数据
    graph 中heads字段保存了输出note信息
    heads is a list of entries as the output of the graph.
    在Init阶段便解析到了GraphRuntime的outputs_字段中
endnote
run --> get_output

code-flow

python/tvm/contrib/graph_runtime.py 中实现了runtime creat的python接口

def create(graph_json_str, libmod, ctx):
    fcreate = get_global_func("tvm.graph_runtime.create")
    return GraphModule(fcreate(graph_json_str, libmod, *device_type_id))

python/tvm/contrib/debugger/debug_runtime.py 中实现了涵盖debug功能的runtime creat python接口

def create(graph_json_str, libmod, ctx, dump_root=None):
    fcreate = get_global_func("tvm.graph_runtime_debug.create")
    func_obj = fcreate(graph_json_str, libmod, *device_type_id)
    return GraphModuleDebug(func_obj, ctx, graph_json_str, dump_root)

fcreate 返回包含c++代码中creat方法的handle的Function, 调用fcreate则陷入到c++中执行, c++的代码主要集中在**src/runtime/graph/graph_runtime.cc中**

Module

(host module\nclass tvm::runtime::Module) as hm
(device module\nclass tvm::runtime::Module) as dm

(param \n以graph中的node的name为key的参数字典) as param

(graph \n包含所有node的json表达) as graph

[device.so] --> dm

hm --> (runtime Init): input
dm <-- hm : call
(param) --> (runtime Init): input
(graph) --> (runtime Init): input

对于runtime输入Module来讲,它有很多子类,对应每一种target,除此之外RelayBuildModule和GraphRuntimeCodeGenModule也是module子类,其中RelayBuildModule是在relay前端之后创建的,实现在**relay.build_module._BuildModule**中,负责进行relay层build。GraphRuntimeCodeGenModule这个子类的作用后续补充

graph

官方文档有比较明确的介绍,其中attr中的storage_id是在编译时创建的存储ID,一个storage_id对应一块内存,storage_id与Tensor是一对多的关系。

param

a dict

deviceAPI

deviceAPI是一个接口,每种target都对应实现了该接口,deviceAPI 更多的是针对内存管理,部分API封装在**c_runtime_api.cc**中供上层调用,在python端可以通过_LIB.xxxx的方式进行调用

  • CPUDeviceAPI
  • CUDADeviceAPI
  • OpenCLWorkspace
  • OpenGLWorkspace
  • ROCMDeviceAPI
  • RPCDeviceAPI
  • VTADeviceAPI
class TVM_DLL DeviceAPI {
 public:
  virtual ~DeviceAPI() {}
  virtual void SetDevice(TVMContext ctx) = 0;
  virtual void GetAttr(TVMContext ctx, DeviceAttrKind kind, TVMRetValue* rv) = 0;
  virtual void* AllocDataSpace(TVMContext ctx,
                               size_t nbytes,
                               size_t alignment,
                               TVMType type_hint) = 0;
  virtual void FreeDataSpace(TVMContext ctx, void* ptr) = 0;
  virtual void CopyDataFromTo(const void* from,
                              size_t from_offset,
                              void* to,
                              size_t to_offset,
                              size_t num_bytes,
                              TVMContext ctx_from,
                              TVMContext ctx_to,
                              TVMType type_hint,
                              TVMStreamHandle stream) = 0;
  virtual TVMStreamHandle CreateStream(TVMContext ctx);
  virtual void FreeStream(TVMContext ctx, TVMStreamHandle stream);
  virtual void StreamSync(TVMContext ctx, TVMStreamHandle stream) = 0;
  virtual void SetStream(TVMContext ctx, TVMStreamHandle stream) {}
  virtual void SyncStreamFromTo(TVMContext ctx,
                                        TVMStreamHandle event_src,
                                        TVMStreamHandle event_dst);
  virtual void* AllocWorkspace(TVMContext ctx,
                                       size_t nbytes,
                                       TVMType type_hint = {});
  virtual void FreeWorkspace(TVMContext ctx, void* ptr);
  static DeviceAPI* Get(TVMContext ctx, bool allow_missing = false);
};

SetDevice

主要是处理device id, 对于cuda来讲,应该就是第几个板卡;对于cpu,这个接口并没有意义。

这个接口在整个runtime过程中没有地方进行主动调用,在cuda的实现中,对于deviceAPI的每个接口,里面都会调用自己的cudaSetDevice接口。

GetAttr

根据传入的不同的DeviceAttrKind,返回不同的设备属性,cuda每一个库函数都要求传入device_id,这个接口在设计时感觉像基于cuda库进行的封装,命名上跟cuda很像。

在python class TVMContext(python/tvm/_ffi/runtime_ctypes.py)中封装了GetAttr的上层调用,在图编译和执行的流程中不会被调用

AllocDataSpace

在device上申请内存空间

在runtime Init的时候,会调用该接口,在devie上为tensor分配空间

FreeDataSpace

在c++ 类class NDArray 的析构函数里面会进行调用, NDArray里面包含了DLTensor

CopyDataFromTo

实现了数据copy,支持host之间,device之间和host-device直接的数据copy

CreateStream

只有cuda支持,实际上也没有找到在什么地方有调用

AllocWorkspace

分配工作空间,内部实际上还是调用AllocDataSpace

什么情况下使用?