4. TF – Kernels模組
TF中包含大量Op運算元,這些運算元組成Graph的節點集合。這些運算元對Tensor實現相應的運算操作。圖 4.1列出了TF中的Op運算元的分類和舉例。
圖 4.1 TensorFlow核心庫中的部分運算
4.1 OpKernels 簡介
OpKernel類(core/framework/op_kernel.h)是所有Op類的基類。繼承OpKernel還可以自定義新的Op類。用的較多的Op如(MatMul, Conv2D, SoftMax, AvgPooling, Argmax等)。
所有Op包含註冊(Register Op)和實現(正向計算、梯度定義)兩部分。
所有Op類的實現需要overide抽象基函式 void Compute(OpKernelContext* context),實現自身Op功能。使用者可以根據需要自定義新的Op操作,參考[12]。
TF中所有Op操作的屬性定義和描述都在 ops/ops.pbtxt。如下Add操作,定義了輸入引數x、y,輸出引數z。
4.2 UnaryOp & BinaryOp
UnaryOp和BinaryOp定義了簡單的一元操作和二元操作,類定義在/core/kernels/ cwise_ops.h檔案,類實現在/core/kernels/cwise_op_*.cc型別的檔案中,如cwise_op_sin.cc檔案。
一元操作全稱為Coefficient-wise unary operations,一元運算有abs, sqrt, exp, sin, cos,conj(共軛)等。如abs的基本定義:
二元操作全稱為Coefficient-wise binary operations,二元運算有add,sub, div, mul,mod等。如sum的基本定義:
4.3 MatMul
4.3.1 Python相關部分
在Python指令碼中定義matmul運算:
根據Ops名稱MatMul從Ops庫中找出對應Ops型別
建立ops節點
建立ops節點並指定相關屬性和裝置分配
4.3.2 C++相關部分
Python指令碼通過swig呼叫進入C介面API檔案core/client/tensor_c_api.cc,呼叫TF_NewNode函式生成節點,同時還需要指定輸入變數,TF_AddInput函式設定first輸入變數,TF_AddInputList函式設定other輸入變數。這裡op_type為MatMul,first輸入變數為a,other輸入變數為b。
建立節點根據節點型別從註冊的Ops工廠中生成,即TF通過工廠模式把一系列Ops註冊到Ops工廠中。其中MatMul的註冊函式為如下
4.3.3 MatMul正向計算
MatMul的實現部分在core/kernels/matmul_op.cc檔案中,類MatMulOp繼承於OpKernel,成員函式Compute完成計算操作。
MatMul的測試用例core/kernels/matmul_op_test.cc檔案,要除錯這個測試用例,可通過如下方式:
在TF中MatMul實現了CPU和GPU兩個版本,其中CPU版本使用Eigen庫,GPU版本使用cuBLAS庫。
CPU版的MatMul使用Eigen庫,呼叫方式如下:
簡而言之就是呼叫eigen的constract函式。
GPU版的MatMul使用cuBLAS庫,準確而言是基於cuBLAS的stream_executor庫。Stream executor是google開發的開源平行計算庫,呼叫方式如下:
其中stream類似於裝置控制程式碼,可以呼叫stream executor中的cuda模組完成運算。
4.3.4 MatMul梯度計算
MatMul的梯度計算本質上也是一種kernel ops,描述為MatMulGrad。MatMulgrad操作是定義在grad_ops工廠中,類似於ops工廠。定義方式如下:
MatmulGrad由FDH(Function Define Helper)完成定義,
其中attr_adj_x="transpose_a" ax0=false, ax1=true, attr_adj_y= "transpose_b", ay0=true, ay1=false, *g屬於FunctionDef類,包含MatMul的梯度定義。
從FDH定義中可以看出MatMulGrad本質上還是MatMul操作。在矩陣求導運算中:
MatMulGrad的測試用例core/ops/math_grad_test.cc檔案,要除錯這個測試用例,可通過如下方式:
4.4 Conv2d
關於conv2d的python呼叫部分和C++建立部分可參考MatMul中的描述。
4.4.1 Conv2d正向計算部分
TF中conv2d介面如下所示,簡單易用:
實現部分在core/kernels/conv_ops.cc檔案中,類Conv2DOp繼承於抽象基類OpKernel。
Conv2DOp的測試用例core/kernels/eigen_spatial_convolutions_test.cc檔案,要除錯這個測試用例,可通過如下方式:
Conv2DOp的成員函式Compute完成計算操作。
為方便描述,假設tf.nn.conv2d中input引數的shape為[batch, in_rows, in_cols, in_depth],filter引數的shape為[filter_rows, filter_cols, in_depth, out_depth]。
首先,計算卷積運算後輸出tensor的shape。
Ø 若padding=VALID,output_size = (input_size - filter_size + stride) / stride;
Ø 若padding=SAME,output_size = (input_size + stride - 1) / stride;
其次,根據計算結果給輸出tensor分配記憶體。
然後,開始卷積計算。Conv2DOp實現了CPU和GPU兩種模式下的卷積運算。同時,還需要注意input tensor的輸入格式,通常有NHWC和NCHW兩種格式。在TF中,Conv2d-CPU模式下目前僅支援NHWC格式,即[Number, Height, Weight, Channel]格式。Conv2d-GPU模式下以NCHW為主,但支援將NHWC轉換為NCHW求解。C++中多維陣列是row-major順序儲存的,而Eigen預設是col-major順序的,則C++中[N, H, W, C]相當於Eigen中的[C, W, H, N],即dimention order是相反的,需要特別注意。
Conv2d-CPU模式下呼叫Eigen庫函式。
Eigen庫中卷積函式的詳細程式碼參見圖 4.2。
圖 4.2 Eigen卷積運算的定義
Ø Tensor::extract_image_patches() 為卷積或池化操作抽取與kernel size一致的image patches。該函式的定義在eigen3/unsupported/Eigen/CXX11/src/Tensor/ TensorBase.h中,參考該目錄下ReadME.md。
Ø Tensor::extract_image_patches() 的輸出與input tensor的data layout有關。設input tensor為ColMajor格式[NHWC],則image patches輸出為[batch, filter_index, filter_rows, filter_cols, in_depth],並reshape為[batch * filter_index, filter_rows * filter_cols * in_depth],而kernels維度為[filter_rows * filter_cols * in_depth, out_depth],然後kernels矩陣乘image patches得到輸出矩陣[batch * filter_index, out_depth],並reshape為[batch, out_rows, out_cols, out_depth]。
Conv2d-GPU模式下呼叫基於cuDNN的stream_executor庫。若input tensor為NHWC格式的,則先轉換為NCHW格式
呼叫cudnn庫實現卷積運算:
計算完成後再轉換成HHWC格式的
4.4.2 Conv2d梯度計算部分
Conv2D梯度計算公式,假設output=Conv2d(input, filter),則
Conv2D梯度計算的測試用例core/kernels/eigen_backward_spatial_convolutions_test.cc檔案,要除錯這個測試用例,可通過如下方式:
Conv2d的梯度計算函式描述為Conv2DGrad。Conv2DGrad操作定義在grad_ops工廠中。註冊方式如下:
Conv2DGrad由FDH(Function Define Helper)完成定義,參見圖 4.3。
圖 4.3 Conv2DGrad的函式定義
Conv2DGrad梯度函式定義中依賴Conv2DBackpropInput和Conv2DBackpropFilter兩種Ops,二者均定義在kernels/conv_grad_ops.cc檔案中。
Conv2DBackpropInputOp和Conv2DBackpropFilterOp的實現分為GPU和CPU版本。
Conv2D運算的GPU版實現定義在類Conv2DSlowBackpropInputOp<GPUDevice, T>和類Conv2DSlowBackprop FilterOp <GPUDevice, T>中。
Conv2D運算的CPU版有兩種實現形式,分別為custom模式和fast模式。Custom模式基於賈揚清在caffe中的思路實現,相關類是Conv2DCustomBackpropInputOp<CPUDevice, T>和Conv2DCustomBackpropFilterOp<CPUDevice, T>。Fast模式基於Eigen計算庫,由於在GPU下會出現nvcc編譯超時,目前僅適用於CPU環境,相關類是Conv2DFastBackpropInputOp<CPUDevice, T>和Conv2DFastBackpropFilterOp<CPUDevice, T>。
根據Conv2DGrad的函式定義,從程式碼分析Conv2D-GPU版的實現程式碼,即分析Conv2DBackpropInput和Conv2DBackpropFilter的實現方式。
Conv2DSlowBackpropInputOp的成員函式Compute完成計算操作。
Compute實現部分呼叫stream executor的相關函式,需要先獲取庫的stream控制程式碼,再呼叫卷積梯度函式。
stream executor在卷積梯度運算部分仍然是藉助cudnn庫實現的。
4.4.3 MaxPooling計算部分
在很多影象分類和識別問題中都用到了池化運算,池化操作主要有最大池化(max pooling)和均值池化(avg pooling),本章節主要介紹最大池化的實現方法。呼叫TF介面可以很容易實現池化操作。
類MaxPoolingOp繼承於類OpKernel,成員函式Compute實現了最大池化運算。
最大池化運算呼叫Eigen庫實現。
Eigen庫中最大池化的詳細描述如下:
其中最大池化運算主要分為兩步,第一步中extract_image_patch為池化操作抽取與kernel size一致的image patches,第二步計算每個image patch的最大值。
4.5 SendOp & RecvOp
TF所有操作都是節點形式表示的,包括計算節點和非計算節點。在跨裝置通訊中,傳送節點(SendOp)和接收節點(RecvOp)為不同裝置的兩個相鄰節點完成完成資料通訊操作。Send和Recv通過TCP或RDMA來傳輸資料。
TF採用Rendezvous(回合)通訊機制,Rendezvous類似生產者/消費者的訊息信箱。引用TF描述如下:
TF的訊息傳遞屬於採用“傳送不阻塞/接收阻塞”機制,實現場景有LocalRendezvous
(本地訊息傳遞)、RpcRemoteRendezvous (分散式訊息傳遞)。除此之外還有IntraProcessRendezvous用於本地不同裝置間通訊。
TF會在不同裝置的兩個相鄰節點之間新增Send和Recv節點,通過Send和Recv之間進行通訊來達到op之間通訊的效果,如圖 4.4右子圖所示。圖中還涉及到一個優化問題,即a->b和a->c需要建立兩組send/recv連線的,但兩組連線是可以共用的,所以合併成一組連線。
圖 4.4 Graph跨裝置通訊
Send和Recv分別對應OpKernel中的SendOp和RecvOp兩個類(kernels/sendrecv_ops.h)。
SendOp的計算函式。
SendOp作為傳送方需要先獲取封裝ctx訊息,然後藉助Rendezvous模組傳送給接收方。
RecvOp的計算函式如下。
RecvOp作為接收方藉助Rendezvous模組獲取ctx訊息。
其中parsed變數是類ParsedKey的例項。圖 5‑5是Rendezvous封裝的ParsedKey訊息實體示例。
4.6 ReaderOp & QueueOp
4.6.1 TF資料讀取
TF系統定義了三種資料讀取方式[13]:
Ø 供給資料(Feeding): 在TensorFlow程式執行的每一步, 通過feed_dict來供給資料。
Ø 從檔案讀取資料: 在TensorFlow圖的起始, 讓一個輸入管線(piplines)從檔案中讀取資料放入佇列,通過QueueRunner供給資料,其中佇列可以實現多執行緒非同步計算。
Ø 預載入資料: 在TensorFlow圖中定義常量或變數來儲存所有資料,如Mnist資料集(僅適用於資料量比較小的情況)。
除了以上三種資料讀取方式外,TF還支援使用者自定義資料讀取方式,即繼承ReaderOpKernel類建立新的輸入讀取類[14]。本章節主要講述通過piplines方式讀取資料的方法。
Piplines利用佇列實現非同步計算
從piplines讀取資料也有兩種方式:一種是讀取所有樣本檔案路徑名轉換成string tensor,使用input_producer將tensor亂序(shuffle)或slice(切片)處理放入佇列中;另一種是將資料轉化為TF標準輸入格式,即使用TFRecordWriter將樣本資料寫入tfrecords檔案中,再使用TFRecordReader將tfrecords檔案讀取到佇列中。
圖 4.6描述了piplines讀取資料的第一種方式,這些流程通過節點和邊串聯起來,成為graph資料流的一部分。
從左向右,第一步是載入檔案列表,使用convert_to_tensor函式將檔案列表轉化為tensor,如cifar10資料集中的image_files_tensor和label_tensor。
第二步是使用input_producer將image_files_tensor和label_tensor放入圖中的檔案佇列中,這裡的input_producer作用就是將樣本放入佇列節點中,有string_input_producer、range_input_producer和slice_input_producer三種,其中slice_input_producer的切片功能支援亂序,其他兩種需要藉助tf.train.shuffle_batch函式作亂序處理,有關三種方式的具體描述可參考tensorflow/python/training/input.py註釋說明。
第三步是使用tf.read_file()讀取佇列中的檔案資料到記憶體中,使用解碼器如tf.image.decode_jpeg()解碼成[height, width, channels]格式的資料。
最後就是使用batch函式將樣本資料處理成一批批的樣本,然後使用session執行訓練。
圖 4.6 使用piplines讀取資料
4.6.2 TFRecords使用
TFRecords是TF支援的標準檔案格式,這種格式允許將任意的資料轉換為TFRecords支援的檔案格式。TFRecords方法需要兩步:第一步是使用TFRecordWriter將樣本資料寫入tfrecords檔案中,第二步是使用TFRecordReader將tfrecords檔案讀取到佇列中。
圖 4.7是TFRecords檔案寫入的簡單示例。tf.train.Example將資料填入到Example協議記憶體塊(protocol buffer),將協議記憶體塊序列化為一個字串,通過TFRecordWriter寫入到TFRecords檔案,圖中定義了label和image_raw兩個feature。Example協議記憶體塊的定義請參考檔案core/example/example.proto。
圖 4.7 TFRecordWriter寫入資料示例
圖 4.8是TFRecords檔案讀取的簡單示例。tf.parse_single_example解析器將Example協議記憶體塊解析為張量,放入example佇列中,其中features命名和型別要與Example寫入的一致。
圖 4.8 TFRecrodReader讀取資料示例
4.6.3 ReaderOps分析
ReaderOpsKernel類封裝了資料讀取的入口函式Compute,通過繼承ReaderOpsKernel類可實現各種自定義的資料讀取方法。圖 4.9是ReaderOp相關的UML檢視。
圖 4.9 ReaderOp相關的UML檢視
ReaderOpKernel子類必須重新定義成員函式SetReaderFactory實現對應的資料讀取邏輯。TFRecordReaderOp的讀取方法定義在TFRecordReader類中。
TFRecordReader呼叫RecordReader::ReadRecord()函式逐步讀取.tfrecord檔案中的資料,每讀取一次,offset向後移動一定長度。
其中offset的計算方式。