Vengineerの戯言

人生は短いけど、長いです。人生を楽しみましょう!

GraphcoreのPoplarの例題を深堀してみる。(その2)

@Vengineerの戯言 : Twitter
SystemVerilogの世界へようこそすべては、SystemC v0.9公開から始まった 

Graphcoreのgithubにアップされている例題をちょっと深堀してみます。(その2)

Advanced Poplar Example

この例題では、複数のTileにマッピングするもの。

// Create some tensor variables. In this case they are just vectors:
Tensor v1 = g.addVariable(FLOAT, {4}, "a");
Tensor v2 = g.addVariable(FLOAT, {4}, "b");
Tensor v3 = g.addVariable(FLOAT, {4}, "c");

// Variables need to be explcitly mapped to tiles. Put each variable on
// a different tile (regardless of whether it is sensible in this case):
g.setTileMapping(v1, 0);
g.setTileMapping(v2, 1);
g.setTileMapping(v3, 2);

 3つのVariable (v1, v2, v3) に対して、setTileMapping にて、TILEを(0, 1, 2)にマッピングする。 

// In order to do any computation we need a compute set and a compute
// vertex that is placed in that compute set:
ComputeSet cs1 = g.addComputeSet("cs1");

// Before we can add a custom vertex to the graph we need to load its
// code. NOTE: .gp files are precompiled codelets but we could also
// have loaded and compiled source directly here:
g.addCodelets("codelets.gp"); //g.addCodelets("codelets.cpp");
auto v = g.addVertex(cs1, "VectorAdd");

// Vertices must also be mapped to tiles. This computation will
// run on tile 0. Exchanges will automatically be generated to
// get the inputs and results to the correct locations:
g.setTileMapping(v, 0);

 計算する部分とVertex。コードは、別ファイルの codelets.cpp 。Vertex(v)は、Tile(0)にマッピング

class VectorAdd : public Vertex {
public:
  Input<Vector<float>> x;
  Input<Vector<float>> y;
  Output<Vector<float>> z;

  bool compute() {
    for (auto i = 0u; i < x.size(); ++i) {
      z[i] = x[i] + y[i];
    }
    return true;
  }
};

 入力ポートは二つ(x と y)、出力ポートはひとつ(z)。計算部の compute メソッドでは、入力 x と y を加えて、出力 z に書き出すもの。戻り値は true 。

// Next connect the variables to the vertex's fields by name:
g.connect(v["x"], v1);
g.connect(v["y"], v2);
g.connect(v["z"], v3);

Variableの v1 を Vertex v の xポートに、v2 を yポートに、v3 を zポートに接続します。

if (options.useIpuModel) {
  // For generating IPUmodel profiles a cycle estimate must
  // be set for each custom vertex:
  g.setCycleEstimate(v, 20);
}

オプションで、useIpuModel を指定した場合は、カスタムコードの実行サイクル数の概算値を設定します。Poplarで提供されているものを使うときはこの実行サイクル数は事前に入っているんでしょうね。

// Create streams that allow reading and writing of the variables:
auto stream1 = g.addHostToDeviceFIFO("write_x", FLOAT, v1.numElements());
auto stream2 = g.addHostToDeviceFIFO("write_y", FLOAT, v2.numElements());
auto stream3 = g.addHostToDeviceFIFO("write_z", FLOAT, v3.numElements());
auto stream4 = g.addDeviceToHostFIFO("read_z", FLOAT, v3.numElements());

 Stream に対して、Variable (v1, v2, v3) を接続しています。v3 は、HostToDeviceとDeviceToHostの両方に接続している。

次に2つ目の計算部を追加してます。入力の v1 と v2 の Reduction (ADD)を v3 に出力する。

// Add a second compute set that will perform the same calculation using
// Poplib's reduction API:
popops::addCodelets(g);
std::vector<ComputeSet> reductionSets;
// Concatenate the vectors into a matrix so we can reduce one axis:
auto tc = poplar::concat(v1.reshape({v1.numElements(), 1}),
                                      v2.reshape({v2.numElements(), 1}),
{1});
auto reduce = popops::ReduceParams(popops::Operation::ADD);
// Create compute sets that perform the reduction, storing th eresult in v3:
popops::reduceWithOutput(g, tc, v3, {1}, reduce, reductionSets, "reduction");

 この後は、4種類のプログラム片(小さなプログラム)を定義しています。

最初のプログラム片は、Stream に 入力データ(Variable) をコピーするもの。

// Now can start constructing the programs. Construct a vector of
// separate programs that can be called individually:
std::vector<program::Program> progs(Progs::NUM_PROGRAMS);

// Add program which initialises the inputs. Poplar is able to merge these
// copies for efficiency:
progs[WRITE_INPUTS] =
                                       program::Sequence(
                                          program::Copy(stream1, v1),
                                          program::Copy(stream2, v2),
                                          program::Copy(stream3, v3)
                                       );

2番目のプログラム片は、計算部(Compute Set:cs1)を実行するもの。

// Program that executes custom vertex in compute set 1:

progs[CUSTOM_PROG] = program::Execute(cs1);

3番目のプログラム片は、Reductionを実行するもの。

// Program that executes all the reduction compute sets:
auto seq = program::Sequence();
for (auto &cs : reductionSets) {
    seq.add(program::Execute(cs));
}
progs[REDUCTION_PROG] = seq;

最後(4番目)のプログラム片は、Stream(stream4)から出力データを Variable(v3)にコピーするもの。

// Add a program to read back the result:
progs[READ_RESULTS] = program::Copy(v3, stream4);

return progs;
}

 ここまでが、プログラムのコンパイルおよび組み立て。executeGraphProgramメソッドにて、入力データを設定し、プログラム片の実行しています。

入力データ (x, y, zinit)をStreamに接続している。

void executeGraphProgram(poplar::Device &device,

                                             poplar::Executable &exe,
                                             const utils::Options &options) {
poplar::Engine engine(std::move(exe));
engine.load(device);

std::vector<float> x = {1, 2, 3, 4};
std::vector<float> y = {4, 3, 2, 1};
std::vector<float> zInit = {-1, -1, -1, -1};
std::vector<float> zResult1 = {0, 0, 0, 0};
std::vector<float> zResult2 = {0, 0, 0, 0};
engine.connectStream("write_x", x.data());
engine.connectStream("write_y", y.data());
engine.connectStream("write_z", zInit.data());

最初のプログラムを実行。

WRITE_INPUTS => CUSTOM_PROG => READ_RESULTS

// Run using custom vertex:
engine.connectStream("read_z", zResult1.data());
engine.run(WRITE_INPUTS);
engine.run(CUSTOM_PROG);
engine.run(READ_RESULTS);

二番目のプログラムの実行。

WRITE_INPUTS => REDUCTION_PROG => READ_RESULTS

// Run program using Poplibs reduction:
engine.connectStream("read_z", zResult2.data());
engine.run(WRITE_INPUTS);
engine.run(REDUCTION_PROG);
engine.run(READ_RESULTS);

最初のプログラムの結果と二番目のプログラムの結果を比較

// Check both methods give same result:
for (auto i = 0u; i< zResult1.size(); ++i) {
if (zResult1[i] != zResult2[i]) {
throw std::runtime_error("Results do not match");
}
}
std::cerr << "Results match.\n";

プロファイルのオプションが指定されているときは、プロファイルファイルを生成する。

if (options.profile) {
  // Retrieve and save profiling information from the engine:
  std::ofstream of(options.profileName);
  auto graphProfile = engine.getGraphProfile();
  auto executionProfile = engine.getExecutionProfile();
  poplar::printProfileSummary(of, graphProfile, executionProfile,

                                               {{"showExecutionSteps", "true"}});
}

 

この例題で分かったことは、プログラムはプログラム片を組み合わせて作れる。