zoukankan      html  css  js  c++  java
  • mshadow的原理--MXNet

    mshadow的原理--MXNet

    这文章主要解释了表达式模板的工作原理(也是mshadow的主要原理),文章的前半部分是翻译自exp-template/README.md。我们会解释它为什么会影响编译代码的性能,表达式模板也是C++矩阵运算库的用到的主要技巧,比如Eigen,GSL,boost.uBLAS。

    如何写出机器学习的高效代码?

    在开始之前,我们先考一个问题,假如更新的规则如下:(这里是为了达到解释的目的,通常更新规则是这样的:weight += - eta * (grad + lambda * weight)

    weight =  - eta * (grad + lambda * weight);
    

    这里权重与梯度都是长度为n的向量。当你选择C++作为你的编程语言时,我想你主要考虑是效率。下面这个很重要并且用在大多数C/C++程序中:

    • 预先分配必要的内存,在程序运行的过程中没有临时内存

    这里是一个例子:

    void UpdateWeight (const float *grad, float eta, float lambda,
                       int n, float *weight) {
      for (int i = 0; i < n; ++i) {
        weight[i] =  - eta * (grad[i] + lambda * weight[i]);
      }
    }
    

    这个函数用了预先分配的梯度、权重空间来计算,写这样的一个函数十分简单,然而当我们要重复这样写的时候会十分烦恼。所以问题是如果我们写成以下的样子,能得到和上面代码一样的性能吗?

    void UpdateWeight (const Vec& grad, float eta, float lambda, Vec& weight) {
      weight = -eta * (grad + lambda * weight);
    } 
    

    答案是可以的,但这不是最显然的答案。

    一个原始的方法

    让我们首先来看一下最直接的解决方法:运算符重载。

    // Naive solution for vector operation overloading 
    struct Vec {
      int len;
      float* dptr;
      Vec(int len) : len(len) { 
        dptr = new float[len];
      }
      Vec(const Vec& src) : len(src.len) {
        dptr = new float[len];
        memcpy(dptr, src.dptr, sizeof(float)*len ); 
      }
      ~Vec(void) {
        delete [] dptr;
      }
    };
    
    inline Vec operator+(const Vec &lhs, const Vec &rhs) {
      Vec res(lhs.len);
      for (int i = 0; i < lhs.len; ++i) {
        res.dptr[i] = lhs.dptr[i] + rhs.dptr[i];
      } 
      return res;
    } 
    

    如果我们用同样的方式增加更多的运算符重载,我们就可以得到我们的想要的直接写等式而不是循环的方法。然而,这种方法明显是低效的,因为中间有临时内存被分配与释放,所以我们能做得更好。

    有一种更高效的选择是:我们可以仅重载运算符+=,-=,这两个运算符是不用分配临时内存的,但是这会限制我们写等式。等会我们将会讨论为什么我们需要表达式模板,虽然C++11在本教程的末尾提供了移动赋值运算符和右值引用。

    延迟计算

    在做运算符+时,为什么我们要分配临时内存呢?这是因为我们不知道将在运算符+中分配的目标,否则我们可以直接将结果存入到目标中,而不是放在临时变量中。

    但是如果我们知道目标呢?这个结代码的实现在exp_lazy.cpp中:

    // Example Lazy evaluation code
    // for simplicity, we use struct and make all members public
    #include <cstdio>
    struct Vec;
    // expression structure holds the expression
    struct BinaryAddExp {
      const Vec &lhs;
      const Vec &rhs;
      BinaryAddExp(const Vec &lhs, const Vec &rhs)
      : lhs(lhs), rhs(rhs) {}
    };
    // no constructor and destructor to allocate and de-allocate memory,
    //  allocation done by user
    struct Vec {
      int len;
      float* dptr;
      Vec(void) {}
      Vec(float *dptr, int len)
          : len(len), dptr(dptr) {}
      // here is where evaluation happens
      inline Vec &operator=(const BinaryAddExp &src) {
        for (int i = 0; i < len; ++i) {
          dptr[i] = src.lhs.dptr[i] + src.rhs.dptr[i];
        }
        return *this;
      }
    };
    // no evaluation happens here
    inline BinaryAddExp operator+(const Vec &lhs, const Vec &rhs) {
      return BinaryAddExp(lhs, rhs);
    }
    
    const int n = 3;
    int main(void) {
      float sa[n] = {1, 2, 3};
      float sb[n] = {2, 3, 4};
      float sc[n] = {3, 4, 5};
      Vec A(sa, n), B(sb, n), C(sc, n);
      // run expression
      A = B + C;
      for (int i = 0; i < n; ++i) {
        printf("%d:%f==%f+%f
    ", i, A.dptr[i], B.dptr[i], C.dptr[i]);
      }
      return 0;
    }
    

    我们实现的思想是在运算符+并没有直接的计算,而是返回一个表达的对象(像抽象语法树),当我们重载运算符=时,我们就可以知道目标和所有的操作时,这样我们就可以直接计算而且不需要临时变量。同样地,我们定义DotDxp和在运算符=上定义延迟计算,并将矩阵(向量)的乘法定向的BLAS库上计算。

    更长的表达式与表达式模板

    使用延迟计算,我们可以很好地避免了临时变量的分配,但是代码的扩展能力被限制了:

    • 我们只能写出A=B+C,不能写出更出的表达式了。
    • 当我们加入表达式时,我们要重载更多的运算符=来计算每一个等式。

    这里我们实现了一个魔法模板程序来解决这两个问题,代码(exp_template.cpp)如下,代码虽然有点长,但可以允许你写更多的等式。

    // Example code, expression template, and more length equations
    // for simplicity, we use struct and make all members public
    #include <cstdio>
    
    // this is expression, all expressions must inheritate it,
    //  and put their type in subtype
    template<typename SubType>
    struct Exp {
      // returns const reference of the actual type of this expression
      inline const SubType& self(void) const {
        return *static_cast<const SubType*>(this);
      }
    };
    
    // binary add expression
    // note how it is inheritates from Exp
    // and put its own type into the template argument
    template<typename TLhs, typename TRhs>
    struct BinaryAddExp: public Exp<BinaryAddExp<TLhs, TRhs> > {
      const TLhs &lhs;
      const TRhs &rhs;
      BinaryAddExp(const TLhs& lhs, const TRhs& rhs)
          : lhs(lhs), rhs(rhs) {}
      // evaluation function, evaluate this expression at position i
      inline float Eval(int i) const {
        return lhs.Eval(i) + rhs.Eval(i);
      }
    };
    // no constructor and destructor to allocate
    // and de-allocate memory, allocation done by user
    struct Vec: public Exp<Vec> {
      int len;
      float* dptr;
      Vec(void) {}
      Vec(float *dptr, int len)
          :len(len), dptr(dptr) {}
      // here is where evaluation happens
      template<typename EType>
      inline Vec& operator= (const Exp<EType>& src_) {
        const EType &src = src_.self();
        for (int i = 0; i < len; ++i) {
          dptr[i] = src.Eval(i);
        }
        return *this;
      }
      // evaluation function, evaluate this expression at position i
      inline float Eval(int i) const {
        return dptr[i];
      }
    };
    // template add, works for any expressions
    template<typename TLhs, typename TRhs>
    inline BinaryAddExp<TLhs, TRhs>
    operator+(const Exp<TLhs> &lhs, const Exp<TRhs> &rhs) {
      return BinaryAddExp<TLhs, TRhs>(lhs.self(), rhs.self());
    }
    
    const int n = 3;
    int main(void) {
      float sa[n] = {1, 2, 3};
      float sb[n] = {2, 3, 4};
      float sc[n] = {3, 4, 5};
      Vec A(sa, n), B(sb, n), C(sc, n);
      // run expression, this expression is longer:)
      A = B + C + C;
      for (int i = 0; i < n; ++i) {
        printf("%d:%f == %f + %f + %f
    ", i,
               A.dptr[i], B.dptr[i],
               C.dptr[i], C.dptr[i]);
      }
      return 0;
    }
    

    关键的思想是模板Exp<SubType>将派生的类作为模板参数,这样就可以将这个模板的自身通过self()转换成SubTpye(就是派生类)。BinaryAddExp现在是一个模板类,可以将表达式复合在一起,就像一个复合模式的模板版本一样。计算通过函数Eval完成,它在BinaryAddExp中以递归的方式完成。

    • 由于内联,当函数在运算符=调用src.Eval(i) 会在编译时被编译成B.dptr[i] + C.dptr[i] + C.dptr[i]
    • 我们可以像循环一样高效地将等式写成逐元素的方式。

    使它更灵活

    通过上面的例子,模板编程编译时可以强大地更程序更加灵活,最后的例子比较接近mshadow了,可以请允许用户使用双目运算符(exp_template_op.cpp)。

    // Example code, expression template
    // with binary operator definition and extension
    // for simplicity, we use struct and make all members public
    #include <cstdio>
    
    // this is expression, all expressions must inheritate it,
    // and put their type in subtype
    template<typename SubType>
    struct Exp{
      // returns const reference of the actual type of this expression
      inline const SubType& self(void) const {
        return *static_cast<const SubType*>(this);
      }
    };
    
    // binary operators
    struct mul{
      inline static float Map(float a, float b) {
        return a * b;
      }
    };
    
    // binary add expression
    // note how it is inheritates from Exp
    // and put its own type into the template argument
    template<typename OP, typename TLhs, typename TRhs>
    struct BinaryMapExp: public Exp<BinaryMapExp<OP, TLhs, TRhs> >{
      const TLhs& lhs;
      const TRhs& rhs;
      BinaryMapExp(const TLhs& lhs, const TRhs& rhs)
          :lhs(lhs), rhs(rhs) {}
      // evaluation function, evaluate this expression at position i
      inline float Eval(int i) const {
        return OP::Map(lhs.Eval(i), rhs.Eval(i));
      }
    };
    // no constructor and destructor to allocate and de-allocate memory
    // allocation done by user
    struct Vec: public Exp<Vec>{
      int len;
      float* dptr;
      Vec(void) {}
      Vec(float *dptr, int len)
          : len(len), dptr(dptr) {}
      // here is where evaluation happens
      template<typename EType>
      inline Vec& operator=(const Exp<EType>& src_) {
        const EType &src = src_.self();
        for (int i = 0; i < len; ++i) {
          dptr[i] = src.Eval(i);
        }
        return *this;
      }
      // evaluation function, evaluate this expression at position i
      inline float Eval(int i) const {
        return dptr[i];
      }
    };
    // template binary operation, works for any expressions
    template<typename OP, typename TLhs, typename TRhs>
    inline BinaryMapExp<OP, TLhs, TRhs>
    F(const Exp<TLhs>& lhs, const Exp<TRhs>& rhs) {
      return BinaryMapExp<OP, TLhs, TRhs>(lhs.self(), rhs.self());
    }
    
    template<typename TLhs, typename TRhs>
    inline BinaryMapExp<mul, TLhs, TRhs>
    operator*(const Exp<TLhs>& lhs, const Exp<TRhs>& rhs) {
      return F<mul>(lhs, rhs);
    }
    
    // user defined operation
    struct maximum{
      inline static float Map(float a, float b) {
        return a > b ? a : b;
      }
    };
    
    const int n = 3;
    int main(void) {
      float sa[n] = {1, 2, 3};
      float sb[n] = {2, 3, 4};
      float sc[n] = {3, 4, 5};
      Vec A(sa, n), B(sb, n), C(sc, n);
      // run expression, this expression is longer:)
      A = B * F<maximum>(C, B);
      for (int i = 0; i < n; ++i) {
        printf("%d:%f == %f * max(%f, %f)
    ",
               i, A.dptr[i], B.dptr[i], C.dptr[i], B.dptr[i]);
      }
      return 0;
    }
    

    总结

    到这里为止,你应该明白它工作的基本思想:

    • 延迟计算,使得我们能知道所有的操作数和目标。
    • 复合模板和递归计算,使得我们能够计算逐元素操作的任意复合表达式。
    • 由于模板和内联的设计,我们写出来的表达式像用循环实现更新规则的一样高效。

    所以在编写机器学习代码时写表达式,并将精力集中在重要的算法部分上。

    在MShadow中的表达式模板

    在Mshadow的表达式模板用到的上面我们介绍的关键思想,但有几个小的不同点:

    • 我们将评估代码与表达式构建和组成代码分开:
      • 在表达中创建Plan类用来替代Exp类的计算函数Eval,用来计算结果。
      • 这允许我们在Plan中放置较少的变量,例如,当我们评估数据时,我们不需要数组长度。
      • 一个重要的原因是CUDA内核不能使用const引用来接受类。
      • 虽然这种设计选择是有争议的,但我们发现迄今为止还是有用的。
    • 延迟还支持复式的表达式,比如矩阵的乘法
      • 除了逐元素的表达式,我们还支持比这样A = dot(B.T(), C)的运算,同样延迟表达是不需要分配临时内存的。
    • 类型检查和数组长度检查。

    备注

    • 表达式模板与C++11:在C ++ 11中,移动构造函数可以用来保存重复的分配内存,这样就省去了一些需要的表达模板。然后,仍然要分配最少一次的空间。
      • 这只是删除了表达式模板中表达式所需的内存,比如dst = A+B+Cdst并没有包括赋值前所分配的空间。
      • 如果我们想保留所有的变量预先分配内存的语法,并且表达式执行时没有内存分配(这是我们在mshadow中所做的),我们仍然需要表达式模板。

    Mshadow源码解析

    Mshadow采用了表达式模板增强了c++矩阵库的性能,四个主要的数据类型的继承关系如下:

    Tensor --> TRValue --> RValueExp --> Exp
    

    基类Exp

    可以看到基类是Exp,除了一些基本的数据类型(如intfloat等),其它的数据类型都是继承于Exp,Exp的设计特殊之处在于可以将它的派生类作为模板参数,这样就可以将这个模板的自身通过self()(返回的是一个不可修改的实例)或者ptrself()(返回的是一个可修改的指针)转换成SubTpye(就是派生类)

    template<typename SubType, typename DType, int exp_type>
    struct Exp {
     public:
      /*! 
    eturn  subtype instance of current class */
      inline const SubType& self(void) const {
        return *static_cast<const SubType*>(this);
      }
      /*! 
    eturn reference of subtype instance of current class */
      inline SubType* ptrself(void) {
        return static_cast<SubType*>(this);
      }
    };
    

    RValueExp

    RValueExp仅定义了一些赋值函数、重载运算符等,要注意的是它将表达式的类型写成默认的kRValue=0,后面所有的数据定义时表达类型都是这个,真正改变表达式类型的是运算符,比如dot、一些重载的运算符。

    template<typename Container, typename DType>
    class RValueExp: public Exp<Container, DType, type::kRValue> {
    	//...
    }
    
    • 来看一下表达式的定义,可以看到的是等级高的表达式包括了等级低的,总结起来就是
      • kRValue = 0,直接对应一个数据类,可以用来分配数据,比。
      • kMapper = 1,表达式包含元素张量运算,将表达式映射到相同的形状。
      • kChainer = 3,表达式可以被写成与其他表达式的链接,通常它具有定义的函数Eval(i,j)中所定义,这个能从输入的表达式中抽出结果(i,j)并和输出到特定位置中。
      • kComplex = 7,用在其它运算中,比如dot
    namespace type {
    // type expression type are defined as bitmask
    // subtype relationshop kRValue < kMapper < kMapper < kComplex
    /*!
     * rief this expression directly correspnds to a data class,
     *   can be used to assign data
     */
    const int kRValue = 0;
    /*!
     * rief expression contains element-wise tensor operations,
     *   map a expression to same shape
     */
    const int kMapper = 1;
    /*!
     * rief expression that can be chained with other expressiones
     *    Usually it have function Eval(i,j) defined, which pulls the result (i, j) from input
     *    expression and output the result at certain position.
     */
    const int kChainer = 3;
    /*! rief othercase: e.g dot product */
    const int kComplex = 7;
    }  // namespace type
    

    TRValue

    TRValue并没有什么实现的内容,但它是所有可能Teson的超类:

    template<typename Container, typename Device, int dimension, typename DType>
    struct TRValue: public expr::RValueExp<Container, DType> {
    };
    

    Tensor

    Tensor是我们的计算基本数据类型,在mshadow中,除了基本类型,其它数据结果最终于都要在回到Tensor中计算,包括继承它了类TensorContainer和用于图模型中更抽象灵活的数据结构TBlob

    template<typename Device, int dimension,
             typename DType MSHADOW_DEFAULT_DTYPE>
    struct Tensor: public TRValue<Tensor<Device, dimension, DType>,
                                  Device, dimension, DType> {
     public:
      //--------------------------------
      // struct memembers
      //--------------------------------
      /*! rief whether current type lies in cpu */
      static const bool kDevCPU = Device::kDevCPU;
      /*! rief dimension of subtype */
      static const int  kSubdim = dimension - 1;
      //--------------------------------
      // struct memembers
      //--------------------------------
      /*! rief pointer to the data */
      DType *dptr_;
      /*! rief shape of the tensor */
      Shape<dimension> shape_;
      /*!
       * rief storing the stride information in x dimension
       *    this is used to deal with pitch allocation in gpu or sse(align x dimension to 64bit) for efficiency
       */
      index_t stride_;
      /*!
       * rief stream where the computation lies
       * stream is a device dependency concept where each computation
       */
      Stream<Device> *stream_;
      //--------------------------------
      // functions
      //--------------------------------
      ...
    }
    

    TensorContainer

    TensorContainer继承自Tensor,最大的区别是TensorContainer在创建对象的时候会分配相应的空间,使用时相对比较方便。

    template<typename Device, int dimension, typename DType = default_real_t>
    class TensorContainer: public Tensor<Device, dimension, DType> {
    }
    

    TBlob

    对于Tensor来说,它的Shape是不能改变的,但对于深度学习的图模型来说,这显然是不合适的。为了适应这个需求,设计了用于图模型中更抽象灵活的数据结构TBlob。通过TBlob的成员函数能获取它在储存的数据并转于相应的Tensor

    class TBlob {
     public:
      /*! rief pointer to the data */
      void *dptr_;
      /*! rief shape of the tensor */
      TShape shape_;
      /*!
       * rief storing the stride information in x dimension
       */
      index_t stride_;
      /*! rief device mask of the corresponding device */
      int dev_mask_;
      /*! rief type flag of the tensor blob */
      int type_flag_;
      ...
    }
    

    计算的过程

    表达式和对应的计算是分开的,而且用了延迟计算的思想,到赋值运算时会一起计算。新构建一个运算符的表达式有如下步骤:

    • 先定义表达类,要继承于Exp,内建构造函数存储相应的数据。
    • 构建运算符,生成表达类。
    • 重载该表达类的Plan类,生成表达类的Plan对象,内建Eval函数计算Plan对象的值。
    • 重载该表达类的MakePlan类,用来生成该表达类的Plan类。
    • 重载该表达类的ExpInfo类,用来存储该表达类的相关信息。

    下面我们用basic.cpp内的一段函数来说明上面的过程及原理:

    40	TensorContainer<cpu, 2> lhs(Shape2(2, 3)), rhs(Shape2(2, 3)), ret(Shape2(2,2));
    41	lhs = 1.0;
    42	rhs = 1.0;
    43	ret = implicit_dot(lhs, rhs.T());
    
    • 第40行:
      这个是声明变量,正如上面所说,这里为TensorContainer变量分配了相应的空间。

    • 第41、42行:
      这两行说的是同一个东西,我们就以41行lhs = 1.0为例说明(过程有省略,因为调用堆栈比较深)。

      • 操作数1已经是一个完整的对象了,不会再有操作,所以直接调用赋值运算符=
      • =两边的变量,左边个是常用的类型double或者float(看编译器),右边是TensorContainer对象,所以调用Tensor_container.cpp中的函数:
      inline Tensor<Device, dimension, DType> &operator=(DType s) {
      	return this->__assign(s);
      }
      
      • __assign在父类RValueExp(在文件Expresion.h)中定义了,查看数据类型,调用的是以下函数,其中saveto是一个运算符(也可以说成操作符)。要注意的是,这个函数内有一个操作scalar<DType>(s),这个操作将类型从DTpye转变成ScalarExp<Dtype>,而且运算符的类型变成了KMapper。另外,this->ptrself()指的是lhsscalar<DType>(s)则是1
      inline Container &__assign(DType s) {
      	ExpEngine<sv::saveto, Container, DType>::Eval(this->ptrself(), scalar<DType>(s));
      	return *(this->ptrself());
      }
      
      template<typename E>
      inline static void Eval(RV *dst, const Exp<E, DType, type::kMapper> &exp) {
      	MapExp<SV>(dst, exp);
      }
      
      template<typename Saver, typename R, int dim, typename DType, typename E, int etype>
      inline void MapExp(TRValue<R, cpu, dim, DType> *dst, const expr::Exp<E, DType, etype> &exp) {
      	...
      	MapExpCPUEngine<expr::PacketCheck<E, MSHADOW_DEFAULT_PACKET>::kPass,Saver, R, dim, DType, E, etype>
      	::Map(dst->ptrself(), exp);
      }
      
      template<bool pass_check, typename Saver, typename R, int dim, typename DType, typename E, int etype>
      struct MapExpCPUEngine {
      		inline static void Map(TRValue<R, cpu, dim, DType> *dst, const expr::Exp<E, DType, etype> &exp) {
      			MapPlan<Saver>(dst, MakePlan(exp.self()));
      	}
      };
      
      • MapPlan有两次调用,先是调用里面的MapPlan,函数在Expr_engine-inl.h中,并且得到Scalar<DType>对象的Plan对象。然后再调用Tensor_cpu-inl.hMapPlan
      template<typename DType>
      inline Plan<ScalarExp<DType>, DType> MakePlan(const ScalarExp<DType> &e) {
      	return Plan<ScalarExp<DType>, DType>(e.scalar_);
      }
      
      template<typename Saver, typename R, int dim, typename DType, typename E>
      inline void MapPlan(TRValue<R, cpu, dim, DType> *dst, const expr::Plan<E, DType> &plan) {
      	Shape<2> shape = expr::ShapeCheck<dim, R>::Check(dst->self()).FlatTo2D();
      	expr::Plan<R, DType> dplan = expr::MakePlan(dst->self());
      #if (MSHADOW_USE_CUDA == 0)
      	#pragma omp parallel for
      #endif
      // temp remove openmp, as default setting throttles CPU
      	for (openmp_index_t y = 0; y < shape[0]; ++y) {
      		for (index_t x = 0; x < shape[1]; ++x) {
        		// trust your compiler! -_- they will optimize it
        			Saver::template Save<DType>(dplan.REval(y, x), plan.Eval(y, x));
      		}
      	}
      }
      
      • 再调用plan.Eval(y, x)dplan.REval(y, x),两个函数都在Expr_engine-inl.h中:
      // scalar
      template<typename DType>
      class Plan<ScalarExp<DType>, DType> {
      	public:
      	explicit Plan(DType scalar) : scalar_(scalar) {}
      	MSHADOW_XINLINE DType Eval(index_t y, index_t x) const {
      		return scalar_;
      	}
      	private:
      	DType scalar_;
      };
      
      // tensor plan
      template <typename Device, int dim, typename DType>
      class Plan<Tensor<Device, dim, DType>, DType> {
      	public:
      	explicit Plan(const Tensor<Device, dim, DType> &t)
        		: dptr_(t.dptr_), stride_(t.stride_) {}
      	// for RValue, the return type should be reference
      	MSHADOW_XINLINE DType &REval(index_t y, index_t x) {
      		return dptr_[y * stride_ + x];
      	}
      	// const evaluation
      	MSHADOW_XINLINE const DType &Eval(index_t y, index_t x) const {
      		return dptr_[y * stride_ + x];
      	}
      
      	private:
      	DType  *dptr_;
      	index_t stride_;
      };
      
      • 最后调用的是Saver::template Save<DType>(dplan.REval(y, x), plan.Eval(y, x)),这个Save操作就是我们之前的saveto,调用在Base.h函数:
      /*! rief save to saver: = */
      struct saveto {
      	 /*! rief save b to a using save method */
      	template<typename DType>
      	MSHADOW_XINLINE static void Save(DType &a, DType b) { // NOLINT(*)
      		a = b;
      	}
          ...
      };
      

      到这里,基本的赋值操作就完成了,你会发现用表达式模板是十不直接的操作。

    • 第43行ret = implicit_dot(lhs, rhs.T())
      这个更加复杂,implicit_dot可以看成一个独立的操作符,如果想写一个新的操作符,可以参考Implicit_gemn.h。这个表达式复杂的原因是有三个运算符——.T()implicit_dotret,要用到递归运算了。无论表达式多么的复杂,我们只要记住,除了赋值运算(其实它也没有Plan类)外一个表达式的结果可以用Plan.Eval来获得的。下面是一些这行代码运算的基本过程:

      • rhs.T()这个操作在Expression.h中,返回一个TransposeExp表达式(注意的是inline的函数会在编译中展开,这里为了方便理解,只是说了调用)。这里要注意的是TransposeExp表达式的类型是kChainer
      inline const TransposeExp<Container, DType> T(void) const {
      	return TransposeExp<Container, DType>(this->self());
      }
      
      • implicit_dot(lhs, rhs.T())函数在Implicit_gemn.h中,返回一个ImplicitGEMMExp表达式。这里要注意的是implicit_dot表达式的类型是kChainer
      template<typename LhsExp, typename RhsExp, typename DType, int e1, int e2>
      inline ImplicitGEMMExp<LhsExp, RhsExp, DType>
      implicit_dot(const Exp<LhsExp, DType, e1> &lhs, const Exp<RhsExp, DType, e2> &rhs) {
      	TypeCheckPass<ExpInfo<LhsExp>::kDim == 2 && ExpInfo<RhsExp>::kDim == 2>
        		::Error_Expression_Does_Not_Meet_Dimension_Req();
      	return ImplicitGEMMExp<LhsExp, RhsExp, DType>(lhs.self(), rhs.self());
      }
      
      • 赋值运算符=与上面所以的步骤有相同之处,只是不同的类型,调用的重载函数是不一样的。上面的调用的第一个MapPlan是调用Implicit_gemn.h中的,返回的是一个ImplicitGEMMExpPlan类,第二个则是一样的。
      template<typename LhsExp, typename RhsExp, typename DType>
      inline Plan<ImplicitGEMMExp<LhsExp, RhsExp, DType>, DType>
      MakePlan(const ImplicitGEMMExp<LhsExp, RhsExp, DType> &exp) {
            return Plan<ImplicitGEMMExp<LhsExp, RhsExp, DType>, DType>(exp);
      }
      
      • 我们来看程序运行到Saver::template Save<DType>(dplan.REval(y, x), plan.Eval(y, x))时,对于dplan.REval(y, x)Save我们已经说过了,这次要说的是Plan.Eval(y, x),来看下是如果进行递归调用的。这个函数在Implicit_gemn.h中:
      template<typename LhsExp, typename RhsExp, typename DType>
      struct Plan<ImplicitGEMMExp<LhsExp, RhsExp, DType>, DType> {
       public:
        explicit Plan(const ImplicitGEMMExp<LhsExp, RhsExp, DType> &e)
            : lhs_(MakePlan(e.lhs_)),
              rhs_(MakePlan(e.rhs_)),
              prod_size_(e.prod_size_),
              prod_size_lower_align_(packet::LowerAlign<DType, MSHADOW_DEFAULT_PACKET>(e.prod_size_)) {
        }
      
        MSHADOW_XINLINE DType Eval(index_t y, index_t x) const {
          typedef packet::Packet<DType> Packet;
          Packet sum = Packet::Fill(0);
      
          const size_t packetSize = Packet::Size();
          DType lhs_temp[packetSize], rhs_temp[packetSize];
      
          for (index_t i = 0; i < prod_size_lower_align_; i += packetSize) {
            // unroll
            for (index_t j = 0; j < packetSize; ++j) {
              lhs_temp[j] = lhs_.Eval(y, i + j);
            }
            for (index_t j = 0; j < packetSize; ++j) {
              rhs_temp[j] = rhs_.Eval(i + j, x);
            }
            sum = sum + Packet::LoadUnAligned(lhs_temp) * Packet::LoadUnAligned(rhs_temp);
          }
          DType ret_result = sum.Sum();
      
          for (index_t i =  prod_size_lower_align_; i < prod_size_; ++i) {
            ret_result += lhs_.Eval(y, i) * rhs_.Eval(i, x);
          }
          return ret_result;
        }
      
       private:
        expr::Plan<LhsExp, DType> lhs_;
        expr::Plan<RhsExp, DType> rhs_;
        const index_t prod_size_;
        const index_t prod_size_lower_align_;
      };
      

      在进行计算中,要得到lhs_rhs_表达式的值,而这两个表达式也是Plan类,之前我们说过:只要记得Plan.Eval是得到表达式的值就行了。所以当调用rhs_.Eval一直递归到计算得到值为止,所以rhs_.Eval最后得到的ret = implicit_dot(lhs, rhs.T())rhs.T()的值。

      • 余下的过程和上述的过程差不多了。
    • 可以看到上述的操作并没有用临时内存。

    • 对于传统的一些操作符+-*/等,会被统一到TernaryMapExp(三目)、BinaryMapExp(双目)、UnaryMapExp(单目)中,参考加文件Expression.h,它的相关类MakePlanPlan则定义在Exp_engine-inl.h

    其它

    • 对于gpu的编程有它的一些限制,但设计的类和上而是差的多的。
    • logging.h是日志系统,打印记录一些系统的信息。
    • io.h是读写文件的操作,与mshadow相对来说是独立的,只是它适配了一些类型的格式(比如Tensor)读写。

    【防止爬虫转载而导致的格式问题——链接】:http://www.cnblogs.com/heguanyou/p/7545344.html

  • 相关阅读:
    【Unity与23种设计模式】命令模式(Command)
    【Unity3D与23种设计模式】组合模式(Composite)
    【Unity3D与23种设计模式】享元模式(Flyweight)
    【Unity3D与23种设计模式】建造者模式(Builder)
    【Unity3D与23种设计模式】工厂方法模式(Factory Method)
    【Unity3D与23种设计模式】模板方法模式(Template Method)
    【Unity3D与23种设计模式】策略模式(Strategy)
    【Unity3D与23种设计模式】桥接模式(Bridge)
    C# 清空数组Array.Clear
    Unity 修改windows窗口的标题
  • 原文地址:https://www.cnblogs.com/heguanyou/p/7545344.html
Copyright © 2011-2022 走看看