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