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+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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM