zoukankan      html  css  js  c++  java
  • mxnet源码阅读笔记之include

    写在前面

    mxnet代码的规范性比Caffe2要好,看起来核心代码量也小很多,但由于对dmlc其它库的依赖太强,代码的独立性并不好。依赖的第三方库包括:

    cub
    dlpack
    dmlc-core
    googletest
    mkldnn
    mshadow
    onnx-tensorrt
    openmp
    ps-lite
    tvm
    

    如果对于这些第三方库没有足够的理解,mxnet的核心代码看起来比较费劲。因此时间原因,本篇仅解析了mxnet对外的接口include目录,并且对于严重依赖第三方库的文件没有深入探究,只能算作一篇不完整的源码阅读笔记了。后续有时间的话,再回来迭代。

    目录

    • storage
    • tensor_blob
    • ndarray
    • resource
    • kvstore
    • base
    • operator
    • engine
    • executor
    • rtc
    • graph_attr_types
    • op_attr_types
    • imperative
    • operator_util
    • c_api

    storage

    Storage是一个跨设备的内存管理类,它提供了内存分配和回收的功能,但并不存储分配的内存,真正的内存指针分配在Storage类内部的Handle结构体中:

    struct Handle {
        void * dptr{nullptr}; //内存地址
        size_t size{0};
        Context ctx;
        int shared_pid{-1};
        int shared_id{-1};
    };
    
    class Storage {
      public:
        Handle Alloc(size_t size, Context ctx) {...};
        virtual void Alloc(Handle* handle) = 0;
        virtual void Free(Handle handle) = 0;
    };
    

    tensor_blob

    TBlob类可以表示任意维度、在任意设备上、任意数据类型的张量,它是NDArray的内部存储,是mxnet中最底层的数据结构。但本质上它是对DLTensor的代理,DLTensor定义在第三方库dlpack中的dlpack.h文件中,以下是它们的关系:

    graph LR NDArray-->|包含|TBlob TBlob-->|包含|DLTensor

    ndarray

    ndarray是mxnet中的核心数据结构,代表了多维数据,类似于Tensorflow中的Tensor。本质上它借鉴了numpy中关于ndarray的定义,一部分ndarray是包含实际数据的,另外一些ndarray并不包含实际数据,它们只是其他ndarray的视图。举例说明,ndarrayA是一个[1x12]的多维数组,存储了12个元素,ndarrayB是一个[3x4]的多维数组,它底层的数据由ndarrayA提供,因此A和B共享了内存,B仅是A的一个视图。

    ndarray内部由chunk结构提供实际的数据存储,先来看下chunk:

    struct Chunk {
        Storage::Handle shandle;
        std::vector<Storage::Handle> aux_handles;
        bool static_data; //如果为真,表示该数据是静态的,并非来自Storage,不需要被释放
        bool delay_alloc; //数据分配是否需要延缓,注意对辅助数据aux data无效
        NDArrayStorageType storage_type = kDefaultStorage;
        std::vector<int> aux_types;
        Context ctx;
        TShape storage_shape;
        std::vector<TShape> aux_shapes;
    };
    

    可见,Chunk结构仍然不是最终的数据存储结构,本质上数据还是存储在Storage结构中,如下所示:

    graph LR NDArray-->|使用|Chunk Chunk-->|使用|Storage

    在ndarray中,我们发现数据分为数据本身,以及辅助数据。辅助数据主要用于存储稀疏数据的时候,数据本身放在data中,数据索引放在aux_data中。

    最后看下NDArray的数据结构:

    class NDArray {
        std::shared_ptr<Chunk> ptr_{nullptr};
        TShape shape_;
        size_t byte_offset_ = 0;
        int dtype_ = -1;
        bool reuse_ = false;
        nnvm::NodeEntry entry_;
        mutable TBlob tblob_;
    };
    

    resource

    在mxnet中,计算中用到的所有内容,除了ndarray之外,都可以被称为资源。其中最常用的资源,就是随机数生成器,分为CPU和GPU两个版本,如下:

    enum Type {
        kRandom, //CPU版本随机数生成器
        kTempSpace, //动态随机内存
        kParallelRandom //可以在GPU中使用的并行随机数生成器
    };
    

    另外,mxnet还为资源提供了一个管理器,ResourceManager,用于获取资源。

    kvstore

    kv存储的作用是存储模型参数,以便在分布式的计算中,在多个设备/机器之间进行数据同步。

    kv存储可以有多种类型,比如:

    • 'local'或者'local_update_cpu‘或者'local_allreduce_cpu',表明这是一个单机的kv存储,并且仅使用cpu做kv的allreduce;
    • 'device'或者'local_allreduce_device',也是单机的kv存储,只不过使用gpu做kv的allreduce;
    • 'dist_*',分布式的kv存储;

    每个kv存储中都有一个更新器,它定义了,针对指定的key,当新value来到时,如何与旧value进行融合。这一点非常重要,因为在深度学习模型的训练中,需要迭代式的对模型参数进行更新,而更新的方式就是通过更新器来定义。

    kv存储中,key通常是整型或者字符串,而value是NDArray,因此,有两种更新器的定义:

    typedef std::function<void(int, const NDArray&, NDArray*)> Updater;
    typedef std::function<void(const std::string&, const NDArray&, NDArray*)> StrUpdater;
    

    最后,kv存储在底层用到了ps-lite来作数据同步。

    class KVStore {
      public:
        static KVStore *Create(const char *type = "local");
        
        virtual void Init(const std::vector<int>& keys, const std::vector<NDArray>& values) = 0;
        virtual void Init(const std::vector<std::string>& str_keys, const std::vector<NDArray>& values) = 0;
        
        virtual void Push(...) = 0;
        virtual void Pull(...) = 0;
        virtual void PullRowSparse(...) = 0;
        
        virtual void set_updater(...);
    };
    

    base

    引入了两个类,执行环境的上下文信息类Context,实际执行时的上下文类RunContext,后者包含前者。首先看下Context类的定义:

    struct Context {
        DeviceType dev_type;
        int32_t dev_id;
        inline void Save(dmlc::Stream *strm) const {...}; //将Context信息记入二进制流
        inline bool Load(dmlc::Stream *strm) {...}; //从二进制流中载入Context信息
        inline static Context Create(DeviceType dev_type, int32_t dev_id = -1); //构造一个新的Context
        inline static Context CPU(int32_t dev_id = 0);
        inline static Context GPU(int32_t dev_id=-1);
        inline static int32_t GetGPUCount(); //获取GPU的数量
        inline static void GetGPUMemoryInformation(int dev, int *free, int *total);
        inline static Context CPUPinned(int32_t dev_id = -1);
        inline static Context CPUShared(int32_t dev_id = 0);
        inline static Context FromString(const std::string& str);
    };
    

    而RunContext就相对简单了,它包含了一个Context和一个流指针:

    struct RunContext {
        Context ctx;
        void *stream;
        //...
    };
    

    operator

    Operator定义了mxnet计算图中基础的操作单位。相当于Tensorflow中的kernel,和Caffe2中的Operator。但它与Tensorflow和Caffe2中的操作有本质区别,在Tensorflow中,操作本身和它对应的求导操作是分开的,而在mxnet中,这两者是结合在一起的,分别使用Forward和Backward两个函数实现,因此,mxnet在操作的实现上更加紧凑,与Tensorflow相比减少了一些对计算图进行裁剪的额外开销,性能上有优势,但也同时限制了自己的计算边界,灵活性不足。

    class Operator {
      public:
        //进行前向计算,将计算结果保存在TBlob中
        virtual void Forward(const OpContext &ctx, const std::vector<TBlob> &in_data, const std::vector<OpReqType> &req, const std::vector<TBlob> &out_data, const std::vector<TBlob> &aux_states) = 0;
        
        //进行后向计算,将梯度写入in_grad
        virtual void Backward(const OpContext &ctx, const std::vector<TBlob> &out_grad, const std::vector<TBlob> &in_data, const std::vector<TBlob> &out_data, const std::vector<OpReqType> &req, const std::vector<TBlob> &in_grad, const std::vector<TBlob> &aux_states);
    };
    

    Operator中仅包含了操作计算的接口,对于操作的描述保存在OperatorProperty类中,它负责保存所有与Operator有关的信息,且能够产生设备相关的Operator。同时,它还为计算引擎提供了一些可以优化操作计算的函数。

    class OperatorProperty {
      public:
        //初始化Operator时需要用到的参数
        virtual void Init(const std::vector<std::pair<std::string, std::string>>& kwargs) = 0;
        //获取为Operator准备的参数
        virtual std::map<std::string, std::string> GetParams() const = 0;
        
        virtual int NumOutputs() const {...}
        //进行Operator的形状推断,类似于Tensorflow的ShapeInference
        virtual bool InferShape(std::vector<TShape> *in_shape, std::vector<TShape> *out_shape, std::vector<TShape> *aux_shape) const = 0;
        //进行Operator的类型推断
        virtual bool InferType(...);
        
        //构建Operator
        virtual Operator* CreateOperator(Context ctx) const = 0;
    };
    

    目前看来,mxnet中Operator与OperatorProperty的关系,与Tensorflow中OpKernel与Op的关系不太一样,后者与Caffe2中的Operator和OpSchema的关系更加相似,有机会我们会详细比较下,这三种框架关于操作定义于使用的区别。

    engine

    引擎是执行核心之一,它负责对计算图中的操作进行调度。引擎中的两大关键元素是操作和变量,操作定义了计算图每一个节点需要实际执行的动作,变量定义了动作之间的依赖关系。

    首先,mxnet定义了一个,被异步函数在运行结束时调用的回调函数类,通过对()的重载,用类对回调函数进行了一层封装:

    class CallbackOnComplete {
      public:
        inline void operator()() const {
            (*callback_)(engine_, param_);
        }
      private:
        friend class ::mxnet::Engine;
        void (*callback_)(Engine *, void *);
        Engine* engine_;
        void* param_;
    };
    

    枚举类FnProperty介绍了常用的函数类型:

    enum class FnProperty {
        kNormal, //一般操作
        kCopyFromGPU, //从GPU上拷贝内容到其它设备的操作
        kCopyToGPU, //从其它设备向GPU拷贝内容的操作
        kCPUPrioritized, //CPU上优先选择的同步操作
        kAsync, //异步函数调用
        kDeleteVar, //用来删除变量的函数
        kGPUPrioritized, //GPU上优先选择的同步操作
    };
    

    engine的含义是,对操作进行调度执行的引擎。回想一下,在Tensorflow中,为了正确执行用户设计好的计算图,我们需要对原始计算图进行一些迭代修改,在Engine类中提供了这样的接口:

    class Engine {
      public:
        //定义运行结束时的回调类
        typedef engine::CallbackOnComplete CallbackOnComplete;
        //定义传递给引擎的同步操作函数
        typedef std::function<void(RunContext)> SyncFn;
        //定义传递给引擎的异步操作函数
        typedef std::function<void(RunContext, CallbackOnComplete)> AsyncFn;
        //定义变量指针
        typedef engine::VarHandle VarHandle;
        //定义操作指针
        typedef engine::OprHandle OprHandle;
        
        //停止引擎中的所有worker
        virtual void Stop() {}
        //启动引擎中的所有worker
        virtual void Start() {}
        
        //分配一个新的变量,该变量可以被用来根据依赖关系,辅助对引擎中的操作进行调度
        virtual VarHandle NewVariable() = 0;
        //构建一个操作,该操作定义在外部,从而我们可以在调度中重复使用
        virtual OprHandle NewOperator(...) = 0;
        //删除一个操作,它不会立刻进行,而是直到所有使用该操作的动作运行结束之后再进行
        virtual void DeleteOperator(OpHandle op) = 0;
        //将一个操作加入引擎
        virtual void Push(...);
        //将一个异步操作加入引擎
        virtual void PushAsync(...);
        //将一个同步操作加入引擎
        virtual void PushSync(...);
        //删除一个变量,它不会立刻进行,而是直到所有依赖该变量的操作完成之后再进行
        virtual void DeleteVariable(...) = 0;
        //等待一个变量准备完成
        virtual void WaitForVar(...) = 0;
        //等待引擎中所有的活动都结束时再返回
        virtual void WaitForAll() = 0;
        
        //返回引擎的单例对象
        static Engine* Get();
        //用来生成OnComplete回调的工厂函数
        inline CallbackOnComplete CreateCallback(...);
    };
    

    executor

    mxnet的执行器接口,用于对计算图进行执行。执行的机制与Operator的设计相合,同样提供了前向和后向两种接口,如下:

    class Executor {
      public:
        virtual void Forward(bool is_train) = 0;
        virtual void PartialForward(bool is_train, int step, int *step_left) = 0;
        virtual void Backward(const std::vector<NDArray> &head_grads, bool is_train = true) = 0;
    };
    

    rtc

    包含了Cuda运行时的编译模块CudaModule。

    graph_attr_types

    获取图相关属性的辅助结构。对于一张计算图中的节点,通常会关注两种信息,一种是计算图中节点的存储类型,一种是节点的调度模式,分别将结果存储在StorageTypeVector和DispatchModeVector中,这两种结构的定义如下:

    using StorageTypeVector = std::vector<int>;
    using DispatchModeVector = std::vector<DispatchMode>;
    

    op_attr_types

    有关操作的额外属性,与nvvm有关,目前看不懂。

    imperative

    与NDArray有关的运行时函数,目前看不懂。

    operator_util

    辅助快速构建operator的功能和注册器。

    c_api

    定义了mxnet后端"C++"代码的接口。

  • 相关阅读:
    聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类
    windows curl ssl版本号编译
    扩展MSEG 加入Z字段
    HDU 1565 1569 方格取数(最大点权独立集)
    Codeforces Round #277.5 (Div. 2)
    葡萄城公布新版ActiveReports 9报表控件和报表server
    we标签
    ADO.NET (二)—— ADO和ADO .NET对照
    补:小玩文件2--将文本文件里的全部行加上行号后写到新文件里
    poj3061 Subsequence ,尺取法
  • 原文地址:https://www.cnblogs.com/jicanghai/p/9692512.html
Copyright © 2011-2022 走看看