極簡入門TensorFlow C++源碼

前一段時間,一直在忙框架方面的工作,偶爾也會幫業務同學去優化優化使用TensorFlow的代碼,也加上之前看了dmlc/relay,nnvm的代碼,覺得蠻有意思,也想分別看下TensorFlow的Graph IR、PaddlePaddle的Graph IR,上週五,看代碼看的正津津有味的時候,看到某個數據競賽群裡面討論東西,不記得具體內容,大概說的是框架的代碼實現, 有幾位算法大佬說看底層源碼比較麻煩,因為比較早從框架,這塊代碼通常都還能看,問題都不大,和群裡小夥伴吹水了半天之後,感覺是可以寫篇如何看TensorFlow或者其他框架底層源碼的勸退文了。

利其器

首先,一定是要找個好工作來看源碼,很多人推薦vs code、sublime,我試過vs code+bazel的,好像也不錯,但是後面做c++適應了clion之後,除了資源要求比較多,還是蠻不錯的,使用c++一般推薦使用cmake來看編譯項目,但是TensorFlow是bazel的,無法直接支持,最開始,這邊是自己寫簡單的cmake,能夠實現簡單的代碼跳轉,但是涉及到比如protobuf之類的編譯過後產生的文件無法跳轉,比較麻煩,不夠純粹,很早之前知道clion有bazel的組件,但是不知道為啥一直搞不通,上週找時間再試了試,發現竟然通了,使用之後,這才是看tf源碼的真正方式:

首先,選擇合適版本的bazel,千萬不能太高,也不能太低,這裡我拉的是TF2.0的代碼,使用bazel 0.24.0剛剛好,切記千萬別太高也比太低, 千萬別太高也比太低,千萬別太高也比太低

極簡入門TensorFlow C++源碼

其次,clion上選擇bazel的插件

極簡入門TensorFlow C++源碼

第三步,./configure,然後按你的意圖選擇合適的編譯配置

極簡入門TensorFlow C++源碼

第四步,導入bazel項目:File=>Import Bazel Project

極簡入門TensorFlow C++源碼

極簡入門TensorFlow C++源碼

極簡入門TensorFlow C++源碼

經過上面幾步之後,接下來就要經過比較長時間的等待,clion會導入bazel項目,然後編譯整個項目,這個耗時視你機器和網絡而定(順便提一句,最好保證比較暢通的訪問github的網絡,另外由於上面targets:all,會編譯TensorFlow所有的項目,如果你知道是什麼意思,可以自己修改,如果不知道的話我先不提了,默認就好,期間會有很多Error出現,放心,問題不大,因為會默認編譯所有的模塊)經過上面之後,我們就可以愉快的看代碼啦,連protobuf生成的文件都很開心的跳轉啦

極簡入門TensorFlow C++源碼

極簡入門TensorFlow C++源碼

極簡版c++入門

TensorFlow大部分人都知道,底層是c++寫的,然後外面包了一層python的api,既然底層是c++寫的,那麼用c++也是可以用來訓練模型的,大部分人應該都用過c++或者java去載入frozen的模型,然後做serving應用在業務系統上,應該很少人去使用c++來訓練模型,既然我們這裡要讀代碼,我們先嚐試看看用c++寫模型,文件路徑如下圖:

極簡入門TensorFlow C++源碼

主要函數就那麼幾個:CreateGraphDef, ConcurrentSteps, ConcurrentSessions:

CreateGraphDef 構造計算圖

<code>GraphDef CreateGraphDef() {
// TODO(jeff,opensource): This should really be a more interesting
// computation. Maybe turn this into an mnist model instead?
Scope root = Scope::NewRootScope();
using namespace ::tensorflow::ops; // NOLINT(build/namespaces)

// A = [3 2; -1 0]. Using Const<float> means the result will be a
// float tensor even though the initializer has integers.
auto a = Const<float>(root, {{3, 2}, {-1, 0}});

// x = [1.0; 1.0]
auto x = Const(root.WithOpName("x"), {{1.f}, {1.f}});


// y = A * x
auto y = MatMul(root.WithOpName("y"), a, x);

// y2 = y.^2
auto y2 = Square(root, y);

// y2_sum = sum(y2). Note that you can pass constants directly as
// inputs. Sum() will automatically create a Const node to hold the
// 0 value.
auto y2_sum = Sum(root, y2, 0);

// y_norm = sqrt(y2_sum)
auto y_norm = Sqrt(root, y2_sum);

// y_normalized = y ./ y_norm
Div(root.WithOpName("y_normalized"), y, y_norm);

GraphDef def;
TF_CHECK_OK(root.ToGraphDef(&def));

return def;
}/<float>/<float>/<code>

定義graph 節點 root, 然後定義常數變量a (shape為2*2), x (shape為2* 1),然後 y = A * x, y2 = y.2, y2_sum = sum(y2), y_norm = sqrt(y2_sum), y_normlized = y ./ y_norm。代碼很簡潔, 看起來一目瞭然,然後是ConcurrentSteps

<code>void ConcurrentSteps(const Options* opts, int session_index) {
// Creates a session.
SessionOptions options;
std::unique_ptr<session> session(NewSession(options));
GraphDef def = CreateGraphDef();
if (options.target.empty()) {
graph::SetDefaultDevice(opts->use_gpu ? "/device:GPU:0" : "/cpu:0", &def);
}

TF_CHECK_OK(session->Create(def));

// Spawn M threads for M concurrent steps.
const int M = opts->num_concurrent_steps;
std::unique_ptr<:threadpool> step_threads(
new thread::ThreadPool(Env::Default(), "trainer", M));

for (int step = 0; step < M; ++step) {
step_threads->Schedule([&session, opts, session_index, step]() {
// Randomly initialize the input.
Tensor x(DT_FLOAT, TensorShape({2, 1}));

auto x_flat = x.flat<float>();
x_flat.setRandom();
std::cout << "x_flat: " << x_flat << std::endl;
Eigen::Tensor<float> inv_norm =
x_flat.square().sum().sqrt().inverse();
x_flat = x_flat * inv_norm();

// Iterations.
std::vector<tensor> outputs;
for (int iter = 0; iter < opts->num_iterations; ++iter) {
outputs.clear();
TF_CHECK_OK(
session->Run({{"x", x}}, {"y:0", "y_normalized:0"}, {}, &outputs));
CHECK_EQ(size_t{2}, outputs.size());

const Tensor& y = outputs[0];
const Tensor& y_norm = outputs[1];
// Print out lambda, x, and y.
std::printf("%06d/%06d %s\\n", session_index, step,
DebugString(x, y).c_str());
// Copies y_normalized to x.
x = y_norm;
}
});
}

// Delete the threadpool, thus waiting for all threads to complete.
step_threads.reset(nullptr);
TF_CHECK_OK(session->Close());
}/<tensor>/<float>/<float>/<session>/<code>

新建一個session,然後設置10個線程來計算,來執行:

<code>std::vector<tensor> outputs;
for (int iter = 0; iter < opts->num_iterations; ++iter) {
outputs.clear();
TF_CHECK_OK(
session->Run({{"x", x}}, {"y:0", "y_normalized:0"}, {}, &outputs));
CHECK_EQ(size_t{2}, outputs.size());

const Tensor& y = outputs[0];
const Tensor& y_norm = outputs[1];
// Print out lambda, x, and y.
std::printf("%06d/%06d %s\\n", session_index, step,
DebugString(x, y).c_str());
// Copies y_normalized to x.
x = y_norm;

}/<tensor>/<code>

每次計算之後,x=y_norm,這裡的邏輯其實就是為了計算矩陣A的最大eigenvalue, 重複執行x = y/y_norm; y= A*x;編譯:

<code>bazel build //tensorflow/cc:tutorials_example_trainer/<code>

執行結果,前面不用太care是我打印的一些調試輸出:

極簡入門TensorFlow C++源碼

簡單的分析

上面簡單的c++入門實例之後,可以抽象出TensorFlow的邏輯:

  1. 構造graphdef,使用TensorFlow本身的Graph API,利用算子去構造一個邏輯計算的graph,可以試上述簡單地計算eigenvalue,也可以是複雜的卷積網絡,這裡是涉及到Graph IR的東西,想要了解的話,我建議先看下nnvm和relay,才會有初步的概念;
  2. 用於構造graphdef的各種操作,比如上述將達到的Square、MatMul,這些操作可以是自己寫的一些數學操作也可以是TensorFlow本身封裝一些數學計算操作,可以是MKL的封裝,也可以是cudnn的封裝,當然也可以是非數學庫,如TFRecord的讀取;
  3. Session的構造,新建一個session,然後用於graph外與graph內部的數據交互:session->Run({{"x", x}}, {"y:0", "y_normalized:0"}, {}, &outputs));這裡不停地把更新的x王graph裡喂來計算y與y_normalized,然後將x更新為y_normalized;

GraphDef這一套,太過複雜,不適合演示如何看TF源碼,建議大家先有一定的基礎知識之後,再看,這裡我們摘出一些算法同學感興趣的,比如Square這個怎麼在TF當中實現以及綁定到對應操作

  1. 代碼中直接跳轉到Square類,如下圖;
極簡入門TensorFlow C++源碼

2.很明顯看到Square類的定義,其構造函數,接收一個scope還有一個input, 然後我們找下具體實現,如下圖:

極簡入門TensorFlow C++源碼

3.同目錄下, http://math_ops.cc,看實現邏輯,我們是構造一個名為Square的op,然後往scope裡更新,既然如此,肯定是預先有保存名為Square的op,接下來我們看下圖:

極簡入門TensorFlow C++源碼

4.這裡講functor::square註冊到"Square"下,且為UnaryOp,這個我不知道怎麼解釋,相信用過eigen的人都知道,不知道的話去google下,很容易理解,且支持各種數據類型;

極簡入門TensorFlow C++源碼

5.那麼看起來,square的實現就在functor::square,我們再進去看看,集成base模板類,且看起來第二個模板參數為其實現的op,再跳轉看看:

極簡入門TensorFlow C++源碼

 6.最後,我們到達了最終的實現邏輯:operator()和packetOp,也看到了最終的實現,是不是沒有想象的那麼難。

極簡入門TensorFlow C++源碼

更重要一點

看完了上面那些,基本上會知道怎麼去看TensorFlow的一些基礎的代碼,如果你瞭解graph ir這套,可以更深入去理解下,這個過程中,如果對TensorFlow各個文件邏輯感興趣,不妨去寫寫測試用例,TensorFlow很多源碼文件都有對應的test用例,我們可以通過Build文件來查看,比如我想跑下http://client_session_test.cc這裡的測試用例

極簡入門TensorFlow C++源碼

我們看一下Build文件中

極簡入門TensorFlow C++源碼

這裡表明了對應的編譯規則,然後我們只需要

<code>bazel build //tensorflow/cc:client_client_session_test/<code>
極簡入門TensorFlow C++源碼

然後運行相應的測試程序即可

極簡入門TensorFlow C++源碼

更更重要的一點

上面把如何看TensorFlow代碼的小經驗教給各位,但是其實這個只是真正的開始,無論TensorFlow、MXNet、PaddlePaddle異或是TVM這些,單純去看代碼,很難理解深刻其中原理,需要去找相關行業的paper,以及找到行業的精英去請教,去學習。目前網上ml system的資料還是蠻多的,有點『亂花迷人眼』的感覺,也沒有太多的課程來分享這塊的工作,十分期望這些框架的官方分享這些框架的乾貨,之後我也會在學習中總結一些資料,有機會的話分享給大家。最後,這些東西確實是很複雜,作者在這塊也是還是懵懵懂懂,希望能花時間把這些內在的東西搞清楚,真的還蠻有意思的。


也歡迎大家關注我的同名微信公眾號 小石頭的碼瘋窩(xiaoshitou_ml_tech),或者通過公眾號加我的個人微信進行討論


分享到:


相關文章: