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\n", 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\n", 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)\n",
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:
/*! \return subtype instance of current class */
inline const SubType& self(void) const {
return *static_cast<const SubType*>(this);
}
/*! \return 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
/*!
* \brief this expression directly correspnds to a data class,
* can be used to assign data
*/
const int kRValue = 0;
/*!
* \brief expression contains element-wise tensor operations,
* map a expression to same shape
*/
const int kMapper = 1;
/*!
* \brief 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;
/*! \brief 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
//--------------------------------
/*! \brief whether current type lies in cpu */
static const bool kDevCPU = Device::kDevCPU;
/*! \brief dimension of subtype */
static const int kSubdim = dimension - 1;
//--------------------------------
// struct memembers
//--------------------------------
/*! \brief pointer to the data */
DType *dptr_;
/*! \brief shape of the tensor */
Shape<dimension> shape_;
/*!
* \brief 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_;
/*!
* \brief 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:
/*! \brief pointer to the data */
void *dptr_;
/*! \brief shape of the tensor */
TShape shape_;
/*!
* \brief storing the stride information in x dimension
*/
index_t stride_;
/*! \brief device mask of the corresponding device */
int dev_mask_;
/*! \brief 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函数:
    /*! \brief save to saver: = */
    struct saveto {
    /*! \brief 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

最新文章

  1. socket(一)
  2. oledb 写入 office2010 以及发布到iis 遇到的奇怪问题总结
  3. Java 正则表达式学习总结和一些小例子
  4. SQL Server性能优化(1)使用SET函数
  5. MVC 增加手机站
  6. Web App 讲义教程
  7. ArcGIS中文件共享锁定数据溢出 这个方法不行,建议用gdb,不要用mdb
  8. properties文件的继承(套用)关系
  9. POJ 1190 生日蛋糕(DFS)
  10. haproxy nginx 多路径
  11. 与众不同 windows phone (30) - Communication(通信)之基于 Socket TCP 开发一个多人聊天室
  12. Codeforce 143B - Help Kingdom of Far Far Away 2
  13. 典型的NIO代码
  14. SDL 在指定窗口中绘图
  15. unity一个按钮实现开和关
  16. Python 提案
  17. python练习题-day15
  18. Springboot 允许跨域访问
  19. AndroidStudio、gradle、buildToolsVersion关系
  20. 解决javah生成c头文件时找不到android类库的问题

热门文章

  1. php 单引号,双引号,反引号区别
  2. github+hexo搭建自己的博客网站(三)主题之外的一些基本配置(图片位置,文章目录功能)
  3. C#基础 课堂笔记 下
  4. vue.js移动端app实战3:从一个购物车入门vuex
  5. spring的Java配置入门(Spring Boot学习笔记之一)
  6. 谈谈个人网站的建立(二)—— lucene的使用
  7. [2013-06-05]bat脚本设置DNS
  8. html复选框
  9. 多线程编程学习一(Java多线程的基础).
  10. Tornado模板