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\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+C
,dst
并没有包括赋值前所分配的空间。 - 如果我们想保留所有的变量预先分配内存的语法,并且表达式执行时没有内存分配(这是我们在mshadow中所做的),我们仍然需要表达式模板。
- 这只是删除了表达式模板中表达式所需的内存,比如
Mshadow源码解析
Mshadow采用了表达式模板增强了c++矩阵库的性能,四个主要的数据类型的继承关系如下:
Tensor --> TRValue --> RValueExp --> Exp
基类Exp
可以看到基类是Exp,除了一些基本的数据类型(如int
、float
等),其它的数据类型都是继承于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()
指的是lhs
,scalar<DType>(s)
则是1
。
inline Container &__assign(DType s) {
ExpEngine<sv::saveto, Container, DType>::Eval(this->ptrself(), scalar<DType>(s));
return *(this->ptrself());
}
- 调用以下函数(Expr_engine-inl.h),SV是操作符
saveto
:
template<typename E>
inline static void Eval(RV *dst, const Exp<E, DType, type::kMapper> &exp) {
MapExp<SV>(dst, exp);
}
- 调用Tensor_cpu-inl.h的
MapExp
、Map
函数:
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.h的MapPlan
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_dot
和ret
,要用到递归运算了。无论表达式多么的复杂,我们只要记住,除了赋值运算(其实它也没有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中的,返回的是一个ImplicitGEMMExp
的Plan
类,第二个则是一样的。
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,它的相关类MakePlan
与Plan
则定义在Exp_engine-inl.h。
其它
- 对于
gpu
的编程有它的一些限制,但设计的类和上而是差的多的。 - logging.h是日志系统,打印记录一些系统的信息。
- io.h是读写文件的操作,与mshadow相对来说是独立的,只是它适配了一些类型的格式(比如
Tensor
)读写。
【防止爬虫转载而导致的格式问题——链接】:http://www.cnblogs.com/heguanyou/p/7545344.html
mshadow的原理--MXNet的更多相关文章
- mxnet的训练过程——从python到C++
mxnet的训练过程--从python到C++ mxnet(github-mxnet)的python接口相当完善,我们可以完全不看C++的代码就能直接训练模型,如果我们要学习它的C++的代码,从pyt ...
- MXNet之ps-lite及parameter server原理
MXNet之ps-lite及parameter server原理 ps-lite框架是DMLC组自行实现的parameter server通信框架,是DMLC其他项目的核心,例如其深度学习框架MXNE ...
- MShadow中的表达式模板
表达式模板是Eigen.GSL和boost.uBLAS等高性能C++矩阵库的核心技术.本文基于MXNet给出的教程文档来阐述MXNet所依赖的高性能矩阵库MShadow背后的原理. 编写高效的机器学习 ...
- ubantu16.04+mxnet +opencv+cuda8.0 环境搭建
ubantu16.04+mxnet +opencv+cuda8.0 环境搭建 建议:环境搭建完成之后,不要更新系统(内核) 转载请注明出处: 微微苏荷 一 我的安装环境 系统:ubuntu16.04 ...
- MXNet设计和实现简介
原文:https://github.com/dmlc/mxnet/issues/797 神经网络本质上是一种语言,我们通过它来表达对应用问题的理解.例如我们用卷积层来表达空间相关性,RNN来表达时间连 ...
- Windows安装mxnet
code { white-space: pre } div.sourceCode { } table.sourceCode,tr.sourceCode,td.lineNumbers,td.source ...
- mxnet目录结构
普通目录 R-package, R语言API, 因为用的python, 所以对R暂时不感兴趣 amalgamation, 将整个mxnet库打包成一个文件, 以方便直接在客户端调用, 如Android ...
- Win10 + Python + GPU版MXNet + VS2015 + RTools + R配置
最近入手一台GTX 1070的笔记本,手痒想在win10上试下GPU跑模型,所以就有了接下来的安装GPU版mxnet的坎坷历程,经过多重试验终于搞定了python和R安装mxnet,现将主要点记录如下 ...
- 用MXNet实现mnist的生成对抗网络(GAN)
用MXNet实现mnist的生成对抗网络(GAN) 生成式对抗网络(Generative Adversarial Network,简称GAN)由一个生成网络与一个判别网络组成.生成网络从潜在空间(la ...
随机推荐
- NSURLConnection从入门到放弃
尽管NSURLConnection已经不建议使用了,iOS 7取而代之的是NSURLSession,但是本文依然打算剖析一下,其中一些思想还是值得学习一下,如果您不用NSURLConnection,并 ...
- JMeter安装和简介
1.Apache jmeter 是一个100%的纯java桌面应用,用于压力测试和性能测量.它最初被设计用于Web应用测试但后来扩展到其他测试领域,可以用于对静态的和动态的资源(文件,Servlet, ...
- HttpServlet源码分析
1.HttpServlet的用法 提供了创建Http Servlet的抽象类,通过实现此类定义自己的Servlet 2.HttpServlet是否是线程安全 先说结论:HttpServlet不是线程安 ...
- git命令中带有特殊符号如@
使用带用户密码clone的方式:git clone https://username:password@remote 当username和password中含有特殊符号会导致出错,因为为http的请求 ...
- 【可视化】div背景半透明
css实现元素半透明使用 opacity:0.x 实现背景色半透明:rgba(a,b,c,x); x为透明度0,到1
- MAVEN 编译打包时报“.....找不到符号” 的处理方法总结
这个错都是在MAVEN插件在编译的时候报的,所以问题一定是出在编译的环节上. 这个时候就要好好检查MAVEN的编译配置, 1.看看配置里的编译版本和本机环境上配置的java版本是否一致,有时候报错的类 ...
- 8.23.4 IO-输入输出16个流
字节流: FileInputStream FileInputStream fIn = new FileInputStream("1.avi"); FileOutputStrea ...
- go web 第三天 学习笔记 --mysql
CREATE TABLE `userinfo` ( `uid` INT() NOT NULL AUTO_INCREMENT, `username` VARCHAR() NULL DEFAULT NUL ...
- Struts2流程分析与工具配置
1. 运行流程 请求 -- StrutsPrepareAndExecuteFilter 核心控制器 -– Interceptors 拦截器(实现代码功能 ) -– Action 的execuute - ...
- mongodb菜鸟整理 2 C#Driver使用
一下载 从官网上下载... 二 引用 下载完了将其解压到某个文件夹内,打开vs建立一个工程 右键引用,找到刚才解压的目录,把里面说有的dll文件全部添加就好 三 命名空间 我都是先打然后alt+shi ...