[原始碼解析] 機器學習引數伺服器Paracel (3)------資料處理

羅西的思考發表於2021-08-21

[原始碼解析] 機器學習引數伺服器Paracel (3)------資料處理

0x00 摘要

Paracel是豆瓣開發的一個分散式計算框架,它基於引數伺服器正規化,用於解決機器學習的問題:邏輯迴歸、SVD、矩陣分解(BFGS,sgd,als,cg),LDA,Lasso...。

Paracel支援資料和模型的並行,為使用者提供簡單易用的通訊介面,比mapreduce式的系統要更加靈活。Paracel同時支援非同步的訓練模式,使迭代問題收斂地更快。此外,Paracel程式的結構與序列程式十分相似,使用者可以更加專注於演算法本身,不需將精力過多放在分散式邏輯上。

前文介紹了PyTorch 的資料處理部分,本文接著介紹Paracel的資料處理部分,正好可以與PyTorch做一下印證。

為了行文完整,本文部分基礎知識與前文重複,另外在解析時候會刪除部分非主體程式碼。

引數伺服器系列其他文章如下:

[原始碼解析] 機器學習引數伺服器ps-lite 之(1) ----- PostOffice

[原始碼解析] 機器學習引數伺服器ps-lite(2) ----- 通訊模組Van

[原始碼解析] 機器學習引數伺服器ps-lite 之(3) ----- 代理人Customer

[原始碼解析]機器學習引數伺服器ps-lite(4) ----- 應用節點實現

[原始碼解析] 機器學習引數伺服器 Paracel (1)-----總體架構

[原始碼解析] PyTorch 分散式(1) --- 資料載入之DistributedSampler

[原始碼解析] PyTorch 分散式(2) --- 資料載入之DataLoader

0x01 切分需要

1.1 切分的好處

深度學習領域的特點是:海量資料 + 海量運算。因為會出現運算時間過長或者模型過大的情況,所以會對資料或者模型進行切分,從而並行的分散式的解決問題,就是我們常常聽到的資料並行或者模型並行。

切分問題包括對訓練資料和訓練模型的切分。即:切分模型以便處理大模型,切分資料以加速訓練。

1.2 資料並行

比如下圖中,每一個節點都擁有一個模型的完整拷貝,但是每個節點的訓練資料不同。每個節點上執行一個訓練程式,我們稱之為 worker。這些worker讀取一個批次資料,各自完成前向計算和後向傳播,得到梯度,然後把各自的梯度提交到引數伺服器上,由引數伺服器進行歸併/更新引數操作,然後引數伺服器把更新後的模型回傳給各個節點,然後每個計算節點負責對本地模型的引數進行更新。進行新一輪迭代訓練。

img

1.3 模型並行

如果可以對模型進行有意義的分割,然後分段載入並且傳送到引數伺服器上,演算法也支援分段並行處理,那麼理論上就可以進行模型並行。我們首先可以把模型分為線性可分模型和非線性模型(神經網路)。

1.3.1 線性模型

針對線性模型,我們可以把模型和資料按照特徵維度進行劃分,分配到不同的計算節點上,每個節點的區域性模型引數計算不依賴於其他維度的特徵,彼此相對獨立,不需要與其他節點進行引數交換。這樣就可以在每個計算節點上採用梯度下降優化演算法進行優化,進行模型並行處理。

某些機器學習問題,如矩陣因子化、主題建模和線性迴歸,由於使用的小批量大小不是非常大,從而提高了統計效率,因此模型並行通常可以實現比資料並行更快的訓練時間。

1.3.2 非線性模型(神經網路)

神經網路的模型與傳統機器學習模型不同,具有如下特點:

  • 神經網路具有很強的非線性,引數之間有較強的關聯依賴。
  • 深度學習的計算本質上是矩陣運算,這些矩陣儲存在GPU視訊記憶體之中。
  • 因為過於複雜,所以神經網路需要較高的網路頻寬來完成節點之間的通訊。

根據這些特徵,神經網路可以分為 層間分割 和 層內分割:

  • 層間分割:橫向按層劃分或縱向跨層劃分進行網路劃分。每個計算節點計算然後通過RPC將引數傳遞到其他節點上進行引數的合併。從網路角度來看,就是把神經網路結構拆分。
  • 層內分割:如果矩陣過大,則一張顯示卡無法載入整個矩陣,這就需要把一張巨大矩陣拆分開來放到不同GPU上去計算,每個GPU只負責模型的一部分。從計算角度看就是把矩陣做分塊拆分處理。

具體可以參見下圖:

img

1.4 混合使用

有的時候資料並行和模型並行會被同時用上。

  • 對於與資料相關的這類模型(如矩陣分解,pagerank,svd等,換句話說就是model是key-value的樣子,key對應一個物體或人),我們可以通過對資料的切分來控制切分模型的方式。
  • 有些模型不直接和資料相關(如LR、神經網路等),這時只要分別對資料和模型做各自的切分即可。

比如:

  • 卷積神經網路中卷積層計算量大,但所需引數係數 W 少,適合使用資料並行。
  • 全連線層計算量小,所需引數係數 W 多。適合使用模型並行。

就像這樣:
img

0x02 切分機制與資料格式

2.1 切分原則

切分資料意味著減少計算量,切分模型的方式則決定了計算和通訊的拓撲。不同的劃分方式可能導致計算效能上的差異。所以我們要試圖尋找切分的一些原則:

  • 切分資料的同時儘量保證切開的模型大小均衡以及通訊較優。

  • 要能保證引數伺服器負載均衡,降低引數伺服器單點效能瓶頸,降低網路傳輸成本(比如在網路中傳輸Embedding模型引數,整個時延和成本將是不可接受的),因此原則如下:

    • 關聯的資料/模型在同一個引數伺服器上。
    • 儘量將一個模型平均分配到所有引數伺服器節點上。
    • 對於非常小的模型,將它們儘量放在一個引數伺服器節點上。
    • 對於多行的模型,儘量將同一行放在一個引數伺服器節點上。
  • 提供定製化的需求,因為各個演算法,或者一個演算法的各種實現,對劃分方式要求都不一樣。

2.2 模型和資料格式

因為分散式實際上不僅包括計算分散式,也涉及到儲存分散式。這就要求模型檔案和資料檔案的格式必須天生支援切分。

對於與資料相關的模型(如矩陣分解,pagerank,svd等,即模型表示成key-value格式),可以通過對資料的切分來控制切分模型的方式。另一些情況是模型不直接和資料相關(如LR、神經網路等),只要分別對資料和模型做各自的切分即可。

在這個方面,各個公司也做了自己的努力。比如騰訊的Angel的模型是以矩陣為單位來儲存的。預設情況下, Angel將模型(矩陣)切分成大小相等的矩形區域,每一個矩陣在模型儲存路徑下對應一個以矩陣名命名的資料夾,裡面包含矩陣的後設資料檔案和資料檔案。一個矩陣只有一個後設資料檔案(後設資料主要由矩陣特徵,分割槽索引和行相關索引組成),但是一般有多個資料檔案。

2.3 Paracel 資料機制

Paracel 提供了豐富的資料切分方式,我們需要從幾個方面一一說明。

2.3.1 資料表示

Paracel用圖和矩陣來表示訓練資料。

有四種型別的圖:

  • bigraph
  • bigraph_continuous
  • digraph
  • undirected_graph

Paracel使用 Eigen3庫來支撐矩陣/向量的操作,因此支援兩種矩陣:

  • SparseMatrix
  • MatrixXd

我們以bigraph為例看看。

在圖論的數學領域中,bigraph的頂點可以劃分為兩個不相交的集合U和V(即U和V是各自獨立的集合),使得U中的一個頂點與V中的一個頂點相連。

定義如下:

template <class T = paracel::default_id_type>
class bigraph {
 private:
  size_t v_sz = 0; 
  size_t e_sz = 0;
  paracel::dict_type<T, paracel::dict_type<T, double> > adj;
 
 public:
  MSGPACK_DEFINE(v_sz, e_sz, adj);    
 public:
  bigraph();
  bigraph(std::unordered_map<T, std::unordered_map<T, double> > edge_info);
  bigraph(std::vector<std::tuple<T, T> > tpls);
  bigraph(std::vector<std::tuple<T, T, double> > tpls);
  void add_edge(const T & v, const T & w);
  void add_edge(const T & v, const T & w, double wgt);
  // return bigraph data
  std::unordered_map<T, std::unordered_map<T, double> > get_data();
  // traverse bigraph edge using functor func
  template <class F>
  void traverse(F & func);
  // traverse vertex v’s related edges using functor func
  template <class F>
  void traverse(const T & v, F & func);
  // return U bag
  std::vector<T> left_vertex_bag();
  // return U set
  std::unordered_set<T> left_vertex_set();
  // out: tpls
  void dump2triples(std::vector<std::tuple<T, T, double> > & tpls);
  // out: dict
  void dump2dict(std::unordered_map > & dict);
  // return number of vertexes in U
  inline size_t v();
  // return number of edges in bigraph
  inline size_t e();
  // return adjacent info of vertex v
  std::unordered_map<T, double> adjacent(const T & v);
  // return outdegree of vertex u in U
  inline size_t outdegree(const T & u);
  // return indegree of vertex v in V
  inline size_t indegree(const T & v);
};

2.3.2 資料載入

Paracel為載入輸入檔案提供了各種介面。在最新版本中,所有與載入相關的介面都只支援文字格式的檔案,這樣會佔用多一些記憶體。

使用者可以並行讀取資料的一個分割槽對應的行,然後構造自定義的資料結構,也可以直接將輸入資料載入為Paracel的“graph”或“matrix”型別。在後一種情況下,必須使用'pattern'和'mix_flag'變數來描述輸入檔案的結構。Pattern還決定輸入資料的分割槽方法。

Paracel用變數“pattern”定義了幾個模式:

pattern structure line example
linesplit(default) 用行來確定分割槽 all structures
fmap first-second case(value set to 1.0)
first-second-value case
依據第一個欄位進行分割槽
a,b
a,b,0.2
smap second-first case(value set to 1.0)
second-first-value case
依據第二個欄位進行分割槽
a,b
a,b,0.2
fsmap support the same structure as fmap and smap
用兩個欄位一起分割槽
a,b or a,b,0.2
fvec id,feature1,…,feature k,
依據id分割槽
1001 0.1|0.2|0.3|0.4
fset attr1,attr2,attr3,… attr1,attr2|value2,attr3|value3,…
依據第一個欄位進行分割槽
a,b,c or a,b|0.2,c|0.4

變數mix_flag 表示圖形/矩陣的連結關係是否在一行中定義。如下面的示例所示,當mix_flag 設定為false時,節點 “a” 的所有連結關係都展開為三行。如果“pattern”等於“fvec”和“fset”,則mix_flag 始終為“true”。

mix_flag example
true a,b,c,d
b,c,d …
true a,b
a,c,d
b,c
b,d …
false(default) a,b
a,c
a,d
b,c
b,d

如上所述,pattern不僅決定資料格式,還決定分割槽策略,而mix_flag告訴Paracel連結關係是否在一行中混合。

0x03 資料載入

3.1 並行處理

AI框架的資料處理主要如下並行處理:

  • 資料載入/處理使用CPU。
  • 訓練使用GPU。

在理想狀態下,應該是每輪迭代訓練之前,CPU就完成載入,準備好訓練資料,這樣訓練就可以持續無縫迭代。

然而,GPU算力每年會提升一倍,CPU的提升速度遠遠落後於GPU,所以CPU會是拖後腿的那個角色。這裡不僅僅是CPU算力不足的問題,也包括從儲存中讀取資料速度不足的問題。

因此,機器學習對於資料載入和前期預處理的要求越來越高,必須在GPU計算時間內,完成下一迭代資料的準備工作,不能讓GPU因為等待訓練資料而空閒。

3.2 流水線

對於機器學習訓練,載入資料可以分為三個步驟:

  • 將資料從磁碟或者分散式儲存載入到主機(CPU)。
  • 將資料從主機可分頁記憶體傳輸到主機固定記憶體。
  • 將資料從主機固定記憶體轉移到主機GPU。

因此,流行的深度學習框架會依據載入步驟的特點和異構硬體的特點來進行流水線處理,從而提高資料處理過程的吞吐量。

流水線一般包括多個運算元,每個運算元內部由資料佇列組成一個緩衝區,上游運算元完成處理之後會傳給給下游運算元進行處理。這樣每個運算元任務會彼此獨立,運算元內部可以使用細粒度的多執行緒/多程式來並行加速,每個運算元可以獨立控制處理速度和記憶體以適配不同網路對於處理速度的需求。

如果運算元內部資料佇列不為空,模型就會一直源源不斷獲得資料,就不會因為等待訓練資料而產生瓶頸。

下面是序列處理邏輯:

+------+            +-----------+           +---------------------------+
|      |            |           |           |                           |
| Data +----------> | Load Data +---------> | Transfer to Pinned Memory |
|      |            |           |           |                           |
+------+            +-----------+           +---------------------------+

下面是並行流水線邏輯:

                    +------------+
+--------+          |            |
|        |          | Process 1  |
| Data 1 +--------> |            +------+
|        |          | Load Data  |      |
+--------+          |            |      |
                    +------------+      |
                                        |
                                        |
                                        |
                    +------------+      |
+--------+          |            |      |        +-----------------------------+
|        |          | Process 2  |      +------> | Pin-memory process          |
| Data 2 +--------> |            |               |                             |
|        |          | Load Data  +-------------> |                             |
+--------+          |            |               |  Transfer to Pinned Memory  |
                    +------------+       +-----> +-----------------------------+
                                         |
                                         |
                                         |
+--------+          +------------+       |
|        |          |            |       |
| Data 3 +--------> | Process 3  +-------+
|        |          |            |
+--------+          | Load Data  |
                    |            |
                    +------------+

3.3 GPU

本文到現在是解決CPU側的資料傳輸問題,即:從磁碟載入資料,從可分頁到固定記憶體。

但是,從固定記憶體到GPU的資料傳輸(tensor.cuda())也可以使用CUDA流進行流水線處理。

另外,深度學習應用程式需要複雜的多階段資料處理管道,包括載入、解碼、裁剪、調整大小和許多其他增強功能。這些目前在 CPU 上執行的資料處理管道已經成為瓶頸,限制了訓練和推理的效能和可擴充套件性。

Nvidia DALI 通過將資料預處理放到 GPU 處理來解決 CPU 瓶頸問題,使用者可以依據自己模型的特點,構建基於 GPU 的 pipeline,或者基於CPU的pipeline。

0x04 Paracel資料載入

前面提到,Paracel使用圖,矩陣等進行資料載入,接下來我們就看看具體如何實現。

4.1 樣例程式碼

我們從原始碼中選取樣例,恰好裡面有model partition 和 data partition 字樣。

其實,這裡的意思是:並行載入模型和並行載入資料

class adjust_ktop_s : public paracel::paralg {

 public:
  adjust_ktop_s(paracel::Comm comm,
                std::string hosts_dct_str,
                std::string _rating_input,
                std::string _fmt,
                std::string _sim_input,
                int _low_limit,
                std::string _output) : 
      paracel::paralg(hosts_dct_str, comm, _output),
      rating_input(_rating_input),
      fmt(_fmt),
      sim_input(_sim_input),
      low_limit(_low_limit) {}

  virtual void solve() {

    // load sim_G, model partition 並行載入模型
    auto local_parser = [] (const std::string & line) {
      auto tmp = paracel::str_split(line, '\t');
      auto adj = paracel::str_split(tmp[1], '|');
      std::vector<std::string> stuff = {tmp[0]};
      stuff.insert(stuff.end(), adj.begin(), adj.end());
      return stuff;
    };
    auto parser_func = paracel::gen_parser(local_parser);
    paracel_load_as_graph(sim_G,
                          sim_input,
                          parser_func,
                          "fset");

    // load rating_G, data partition 並行載入資料
    auto local_parser_rating = [] (const std::string & line) {
      return paracel::str_split(line, ',');
    };
    auto local_parser_rating_sfv = [] (const std::string & line) {
      std::vector<std::string> tmp = paracel::str_split(line, ',');
      std::vector<std::string> r({tmp[1], tmp[0], tmp[2]});
      return r;
    };
    auto rating_parser_func = paracel::gen_parser(local_parser_rating);
    if(fmt == "sfv") {
      rating_parser_func = paracel::gen_parser(local_parser_rating_sfv);
    }
    paracel_load_as_graph(rating_G,
                          rating_input,
                          rating_parser_func,
                          fmt);

    // init rating_G 
    paracel::dict_type<std::string, double> tmp_msg;
    auto init_lambda = [&] (const node_t & uid,
                            const node_t & iid,
                            double v) {
      std::string key = std::to_string(uid) + "_" + std::to_string(iid);
      tmp_msg[key] = v;
    };
    rating_G.traverse(init_lambda);
    paracel_write_multi(tmp_msg);
    paracel_sync();

    // learning
    cal_low_peak();
  }

 private:
  std::string rating_input, fmt;
  std::string sim_input;
  int low_limit = 1;
  paracel::bigraph<node_t> sim_G;
  paracel::bigraph<node_t> rating_G;
  paracel::dict_type<node_t, int> ktop_result;
  double training_rmse = 0., original_rmse = 0.;
}; // class adjust_ktop_s

4.2 載入圖

對於圖結構的資料或者模型,首先每個 worker 會通過fixload並行載入檔案,然後會通過paracel_sync進行同步。

注意:每個worker都會執行以下函式,內部會通過MPI進行協調和統一。

  template <class T, class G>
  void paracel_load_as_graph(paracel::bigraph<G> & grp,
                             const T & fn,
                             parser_type & parser,
                             const paracel::str_type & pattern = "fmap",
                             bool mix_flag = false) {
    if(pattern == "fset") {
      mix_flag = true;
    }
    // TODO: check pattern 
    // load lines
    paracel::loader<T> ld(fn, worker_comm, parser, pattern, mix_flag);
    // 並行載入,fixload 裡面有一個all2all交換
    paracel::list_type<paracel::str_type> lines = ld.fixload();
    paracel_sync(); //這裡進行同步,確保所有worker都完成載入
    // create graph 
    ld.create_graph(lines, grp); // 此時才開始建立圖
    set_decomp_info(pattern);
    lines.resize(0); lines.shrink_to_fit(); paracel::cheat_to_os();
  }

4.3 載入檔案

Paracel 的資料(模型),可能由多個資料檔案構成,因此可以進行並行載入:

  • 首先,依據檔案列表和world size來分割槽,確保多個載入的worker之間不會出現workload不均衡的情況
  • 其次,呼叫 structure_load 來做並行載入,和pytorch 暗合
  paracel::list_type<paracel::str_type> fixload() {
    
    paracel::scheduler scheduler(m_comm, pattern, mix);
    auto fname_lst = paracel::expand(filenames); //檔名字列表
    
    // 依據檔案列表和world size來分割槽,確保多個載入的worker之間不會出現workload不均衡的情況
    paracel::partition partition_obj(fname_lst,
                                     m_comm.get_size(),
                                     pattern);
    partition_obj.files_partition();
    // parallel loading lines 此時才並行載入
    auto linelst = scheduler.structure_load(partition_obj);
    m_comm.synchronize();
    if(m_comm.get_rank() == 0) std::cout << "lines got" << std::endl;
    
    return linelst;
  }

4.3.1 分割槽

於是就涉及到了一個問題,假如有6個worker,12個檔案,那麼每個worker怎麼做到並行載入呢?

可能有同學會說:每個worker 載入兩個檔案。但是這種情況只適用於檔案大小基本一致的情況,如果檔案大小不一致,比如一個檔案15000行,一個檔案50行,一個檔案20000行.....,那麼就會造成worker的 load 不均衡,導致無法達到並行載入的效果。

所以需要按照所有檔案的總行數進行分配。比如12個檔案一共120000行,則每個worker負責載入10000行。

第一個worker可能負責載入第一個檔案的10000行,第二個worker負責載入第一個檔案的後5000行 和第二個檔案的50行,第三個檔案的 xxx 行.....

4.3.2 分割槽定義

我們看看分割槽是如何定義的:

  • namelst :是模型檔案或者資料檔名字列表。

  • slst 其中第 i 個元素是第 i 個分割槽的起始行數。

  • elst : 第 i 個元素是第 i 個分割槽的終止行數。

  • np : 是所有worker個數。

  • displs :第 i 個元素是第 i 個檔案在所有檔案行數的起始行數。比如:第一個檔案5行,第二個檔案6行,第三個檔案6行,則displs[0] = 0,displs[1] = 5,displs[2] = 11 ...

具體如下:

class partition {

 public:
  partition(paracel::list_type<paracel::str_type> namelst_in,
            int np_in, paracel::str_type pattern_in)
      : namelst(namelst_in), np(np_in), pattern(pattern_in) {}

 private:
  paracel::list_type<paracel::str_type> namelst;
  int np;  // world size
  paracel::str_type pattern;
  paracel::list_type<long> slst, elst, displs;

}; // class partition

4.3.3 均衡分割槽

目的就是計算所有檔案的總行數,然後在各個worker之中進行均衡分配。

const int BLK_SZ = 32;

void files_partition(int blk_sz = paracel::BLK_SZ) {
    if(pattern == "linesplit" || pattern == "fvec") {
      blk_sz = 1;
    }
    slst.resize(0);
    elst.resize(0);
    displs.resize(0);
    displs.resize(namelst.size() + 1, 0); // 擴充套件為檔案個數
    for(size_t i = 0; i < displs.size() - 1; ++i) {
      std::ifstream f(namelst[i], std::ios::ate); // ate作用是寫入的資料被加入到檔案末尾
      long tmp = f.tellg(); // 得到某個檔案的行數
      f.close();
      displs[i + 1] = displs[i] + tmp; // 計算每個檔案在總行數中的位置
    }
    long sz = displs.back(); //得到所有檔案的總行數
    int nbk = np * blk_sz; // 每個worker負責的範圍
    long bk_sz = sz / static_cast<long>(nbk); //每個partition的大小
    long s, e;
    for(int i = 0; i < nbk; ++i) { //  nbk是每個worker負責的範圍,其中每個範圍是s, e,s和e之間大小是BLK_SZ。
      s = static_cast<long>(i) * bk_sz; // 載入起始行
      if(i == nbk - 1) {
        e = sz;
      } else {
        e = (i + 1) * bk_sz; // 載入終止行
      }
      assert(s < e);
      slst.push_back(s); //插入起始行
      elst.push_back(e); //插入終止行
    }
  }

4.4 並行載入

回憶一下前面的程式碼,當我們用分割槽做負載均衡之後,就可以用scheduler實施並行載入:

    partition_obj.files_partition();
    // parallel loading lines 此時才並行載入
    auto linelst = scheduler.structure_load(partition_obj);

scheduler可以認為是排程器,負責排程多個程式並行載入

比如某worker,rank = 2, 則依據自己的rank來計算,得到本worker載入的起始,終止位置是:st = 64, en = 96。然後使用 files_load_lines_impl 具體載入。

paracel::list_type<paracel::str_type>
scheduler::structure_load(partition & partition_obj) {
  paracel::list_type<paracel::str_type> result;
  int blk_sz = paracel::BLK_SZ;
  if(pattern == "fvec" || pattern == "linesplit") {
    blk_sz = 1;
  }
  int st = m_comm.get_rank() * blk_sz; // 依據自己的rank來計算,看看自己這個程式從哪裡載入。
  int en = (m_comm.get_rank() + 1) * blk_sz; // 載入到哪裡結束
  auto slst = partition_obj.get_start_list();
  auto elst = partition_obj.get_end_list();
  for(int i = st; i < en; ++i) { // 遍歷 64 ~ 96
    // 去找 slst[64 ~ 96], elst[64 ~ 96]的來逐一載入
    auto lines = partition_obj.files_load_lines_impl(slst[i], elst[i]); // 自己應該載入什麼
    result.insert(result.end(), lines.begin(), lines.end());
  }
  return result;
}

files_load_lines_impl完成了對具體檔案的載入功能。

  template <class F>
  void files_load_lines_impl(long st, long en, F & func) {
    // to locate files index to load from
    int fst = 0;
    int fen = 0;
    long offset;
    // 找到st, en分別屬於哪個檔案,即在 displs 的位置,找到哪些files
    for(size_t i = 0; i < namelst.size(); ++i) {
      if(st >= displs[i]) {
        fst = i; // st所在檔案的idx
      }
      if(en > displs[i + 1]) {
        fen = i + 1; // en所在檔案的idx
      }
    }
    assert(fst <= fen);
    bool flag = false;
    // load from files
    for(auto fi = fst; fi < fen + 1; ++fi) { // 遍歷載入 fst, fen之間的檔案
      if(flag) { 
        offset = 0;
      } else {
        offset = st - displs[fi];
      }
      assert(offset >= 0);
      
      std::ifstream f(namelst[fi]); // 載入某個file
      // 依據檔案行數,找到對應在哪個檔案之中,然後載入
      if(offset) {
        f.seekg(offset - 1);
        paracel::str_type l;
        std::getline(f, l);
        offset += l.size();
      }
      if(fi == fen) {
        while(offset + displs[fi] < en) {
          paracel::str_type l;
	        std::getline(f, l);
	        offset += l.size() + 1;
          func(l);
        }
      } else {
        flag = true;
        while(1) {
          paracel::str_type l;
	        std::getline(f, l);
	        if(l.size() == 0) {
            break;
          }
          func(l);
        }
      }
      f.close();
    } // end of for
  }

4.5 建立圖

載入完成之後,會呼叫create_graph完成對圖的構建。

  void create_graph(paracel::list_type<paracel::str_type> & linelst,
                    paracel::bigraph<paracel::default_id_type> & grp) {
    
    paracel::scheduler scheduler(m_comm, pattern, mix); 
    
    // hash lines into slotslst,每個worker構建自己負責的部分
    paracel::list_type<paracel::list_type<paracel::compact_triple_type> > result;
    scheduler.lines_organize(linelst, 
                             parserfunc, 
                             result);
    linelst.resize(0); linelst.shrink_to_fit(); paracel::cheat_to_os();
    m_comm.synchronize();
    
    // alltoall exchange,讓每個worker都擁有全部的資料
    paracel::list_type<paracel::compact_triple_type> stf;
    scheduler.exchange(result, stf);
    result.resize(0); result.shrink_to_fit(); paracel::cheat_to_os();
    m_comm.synchronize();

    for(auto & tpl : stf) {
      grp.add_edge(std::get<0>(tpl), 
                   std::get<1>(tpl), 
                   std::get<2>(tpl));
    }
    stf.resize(0); stf.shrink_to_fit(); paracel::cheat_to_os();
  }

在構建過程中,使用lines_organize完成了對具體資料行的處理,具體就是依據檔案中行的格式來進行解析,比如檔案型別是fset?還是 fsv?還是 bfs 等,針對每種格式進行不同的處理。

  template <class F = std::function< paracel::list_type<paracel::str_type>(paracel::str_type) > >
  listlistriple_type 
  lines_organize(const paracel::list_type<paracel::str_type> & lines,
                 F && parser_func = default_parser) {

    listlistriple_type line_slot_lst(m_comm.get_size());
    paracel::str_type delimiter("[:| ]*");
    for(auto & line : lines) { 
      auto stf = parser_func(line);
      if(stf.size() == 2) {
        // bfs or part of fset case
	      // ['a', 'b'] or ['a', 'b:0.2']
	      auto tmp = paracel::str_split(stf[1], delimiter);
	      if(tmp.size() == 1) {
	        paracel::triple_type tpl(stf[0], stf[1], 1.);
	        line_slot_lst[h(stf[0], stf[1], npx, npy)].push_back(tpl);
	      } else {
	        paracel::triple_type tpl(stf[0], tmp[0], std::stod(tmp[1]));
	        line_slot_lst[h(stf[0], tmp[0], npx, npy)].push_back(tpl);
	      }
      } else if(mix) {
        // fset case
	      // ['a', 'b', 'c'] or ['a', 'b|0.2', 'c|0.4']
        // but ['a', '0.2', '0.4'] is not supported here
        for(paracel::default_id_type i = 1; i < stf.size(); ++i) {
	        auto item = stf[i];
	        auto tmp = paracel::str_split(item, delimiter);
	        if(tmp.size() == 1) {
	          paracel::triple_type tpl(stf[0], item, 1.);
	          line_slot_lst[h(stf[0], item, npx, npy)].push_back(tpl);
	        } else {
	          paracel::triple_type tpl(stf[0], tmp[0], std::stod(tmp[1]));
	          line_slot_lst[h(stf[0], tmp[0], npx, npy)].push_back(tpl);
	        }
	      } // end of for
      } else {
        // fsv case
        paracel::triple_type tpl(stf[0], stf[1], std::stod(stf[2]));
	      line_slot_lst[h(stf[0], stf[1], npx, npy)].push_back(tpl);
      } // end of if
    } // end of for
    return line_slot_lst;
  }

0x05 總結

現在歸納下總體邏輯如下,我們假設有兩個workers對若干檔案進行並行載入。最終每個worker都把資料和模型載入進入自己的程式。

+------------------------------------------------------------------------------------------------------------------------------------------------------+
| worker 1          +------------------+                                                                                                               |
|                   | partition        |                             +-----------------+                                                               |
|                   |                  |      3 structure_load       | scheduler       |                                                               |
|                   |         slst     +---------------------------> |                 |         5                       6                 8           |
|   1 fixload       |                  |                             |                 +----> paracel_sync +-----> create_graph +----> lines_organize  |
|  +------------->  |         elst     | <---------------------------+                 |                                                               |
|                   |                  |   4 files_load_lines_impl   +-----------------+          ^                        ^                           |
|                   |         displs   |                                                          |                        |                           |
|                   |                  |                                                          |                        |                           |
|                   +------------------+                                                          |                        |                           |
|                                                                                                 |                        |                           |
|                             ^                                                                   |                        |                           |
|                             |                                                                   |                        |                           |
|                             |2 files_partition                                                  |                        |                           |
+------------------------------------------------------------------------------------------------------------------------------------------------------+
                              |                                                                   |                        |
               +------------------------------+                                                   |                        |
               |              |               |                                                   |                        |
               |              |               |                                                   |                        |
          +----+----+     +---+----+     +----+---+                                               |                    7   +
          | File 1  |     | File 2 |     | File n |                                               |            scheduler.exchange
          +----+----+     +---+----+     +----+---+                                               |                        +
               |              |               |                                                   |                        |
               |              |               |                                                   |                        |
               +------------------------------+                                                   |                        |
                              |                                                                   |                        |
+------------------------------------------------------------------------------------------------------------------------------------------------------+
| worker 2                    |2 files_partition                                                  |                        |                           |
|                             v                                                                   |                        |                           |
|                                                                                                 |                        |                           |
|                   +-------------------+                                                         |                        |                           |
|                   | partition         |                                                         |                        |                           |
|                   |                   |                           +------------------+          |                        |                           |
|    1 fixload      |          slst     |   3  structure_load       | scheduler        |          v                        v                           |
|   +-------------> |                   +------------------------>  |                  |                                                    8          |
|                   |          elst     |                           |                  +----> paracel_sync +-----> create_graph +-----> lines_organize |
|                   |                   | <-------------------------+                  |          5                       6                            |
|                   |          displs   |  4 files_load_lines_impl  +------------------+                                                               |
|                   |                   |                                                                                                              |
|                   +-------------------+                                                                                                              |
+------------------------------------------------------------------------------------------------------------------------------------------------------+

手機如下:

img

至此,Paracel分析完畢,我們下一篇開始介紹 GPipe,敬請期待。

0xFF 參考

卷積神經網路的並行化模型--One weird trick for parallelizing convolutional neural networks

AI框架中資料處理的挑戰與解決思路

PyTorch 原始碼解讀之 torch.utils.data:解析資料處理全流程

談談你對大規模機器學習這個領域的理解和認識?

Nvidia-DALI 從放棄到入門

pytorch(分散式)資料並行個人實踐總結——DataParallel/DistributedDataParallel

相關文章