Vengineerの妄想

人生を妄想しています。

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

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

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

vengineer.hatenablog.com

今回は、下記のGopARTではなく、Poplarの例題

vengineer.hatenablog.com

Poplar prefetching callbacks

Copy from input stream to tensor.
Modify tensor value
Copy tensor to output stream.

ホストからのテンソルデータを input stream として、IPU に渡して、

IPU 内でそのテンソルデータを修正して、

修正したテンソルデータを output stream にコピーして、ホストに返す。

というもの。

まずは、グラフを生成します。デバイスマネージャをDeviceManager::createDeviceManager()で生成し、そのデバイスマネージャーからIPUを1個、getDevicesにて見つけます。見つからないときは、例外を投げます。見つけた device に attach() して、attach できないときも例外を投げます。

attach できたデバイス(device)に対して、Graphを作り、このGraphに対していろいろと設定していきます。ここまでは、いろいろなフレームワークとほぼ同じ。

const unsigned numIpus = 1;

auto manager = DeviceManager::createDeviceManager();
auto devices = manager.getDevices(poplar::TargetType::IPU, numIpus);
if (devices.empty()) {
throw poplar::poplar_error("No devices found");
}
auto device = std::move(devices[0]);
if (!device.attach()) {
throw poplar::poplar_error("Failed to attach");
}

// Graph elements
Graph graph(device);

 

graphに対して、Variableを定義します。Variableは、タイプと数、名前を指定します。

戻り値のタイプは、Tensorになります。このVariableをsetTileMappingを使って、特定のTile(ここでは、0)にマッピングします。Tileって、Core + Local Memory(256KB)が入った もの。これがIPUには、1216個入っていて、各コアは7スレッドで動くようです。

つまり、この Variable (t) は、Tile番号0にマッピングするように指示しているということ。

Tensor t = graph.addVariable(FLOAT, {elements}, "data");
graph.setTileMapping(t, 0);

今後は計算する部分、Poplarで用意されているものではなく、カスタムコードもサポートしているようです。addCodelets にてファイルからコードを読みこんでコンパイルします。addComputeSetが計算する部分のコードに対応し、そのコードをaddVertexにてノード(Vertex)に割当、そのVertexをVariableと同じように、setTileMappingにて、Tile(ここでも、Variable t と同じ0にマッピング)します。Graphのconnectにて、Vertex(v1)の 入力ポート(x)と出力ポート(y)にVariable(t)を接続します。

graph.addCodelets(__FILE__);

ComputeSet cs = graph.addComputeSet();
auto v1 = graph.addVertex(cs, "Increment");
graph.setTileMapping(v1, 0);

graph.connect(v1["x"], t);
graph.connect(v1["y"], t);

 カスタムコードは、Vertexクラスを継承した Increment 。入力ポート(Input)のタイプは、Vector<float>、出力ポート(Output)のタイプは、Vector<float>。実際に計算する部分は、computeメソッドで、入力ポートと出力ポートのサイズをチェック後、入力ポートからデータを読んで、データをインクリメントして、出力ポートに書き込む。計算が正しく出来た時の戻り値は、trueで、エラーの時は false にする。

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

   bool compute() {
     if (x->size() != y->size())
     return false;

    for (unsigned i = 0; i < x->size(); i++) {
      y[i] = x[i] + 1;
   }
   return true;
  }
};

 ここまでで、IPU で動くグラフ生成は終わり、次はホスト側とのデータのやり取りのためのコード。

// Streams
auto inStream = graph.addHostToDeviceFIFO("in", FLOAT, t.numElements());
auto outStream = graph.addDeviceToHostFIFO("out", FLOAT, t.numElements());

 やりとりは、Streamというもので行う模様。ホストからIPUへは、addHostToDeviceFIFOにて、名前、タイプ、数を指定する。IPUからホストへは、addDeviceToHostFIFOにて、名前、タイプ、数を指定する。

これで、ホストからデータを渡して、IPUで計算し、結果をホストに戻せます。

一連の処理は、Sequence(Copy(inStream,t), Execute(cs), Copy(t,outStream) にて実行し、Repear(repeat, ...) にて、その処理を5回繰り返します。

const unsigned repeat = 5;

// Program
auto prog = Repeat(
repeat, Sequence(Copy(inStream, t), Execute(cs), Copy(t, outStream)));

 この時点では処理を決めただけど、この処理をコンパイルし、IPUにコンパイル済みのプログラムをロードします。

// Compile program
OptionFlags options{{"exchange.streamBufferOverlap", "none"},
                                 {"exchange.enablePrefetch", "true"}};
Engine eng(graph, prog, options);
eng.load(device);

 Engine に、グラフ(graph)、プログラム(prog)、オプション(options) を指定し、load メソッドでデバイスコンパイル済みのロードします。

// Connect output
std::vector<float> hOut(t.numElements());
eng.connectStream(outStream, hOut.data(),
std::next(hOut.data(), hOut.size()));

 ホスト側のデータを Engine の connectStream にて出力側のStream に接続します。入力側への接続はこの後で行います。

// Run the program multiple times.
// Replace input stream callback when value changes.
boost::optional<float> previous;
for (float in : {1.0f, 3.0f, 3.0f, 9.0f, 9.0f}) {
  if (previous != in) {
    std::cout << "Set input callback\n";
    std::unique_ptr<FillCallback> cb{new FillCallback(in, t.numElements())};
    eng.connectStreamToCallback(inStream, std::move(cb));
  }

入力データを inStream に connectStreamToCallback にて接続し、

  std::cout << "Running\n";
  eng.run(0);

run メソッドでプログラムを実行する。実行後、出力データのチェックをしている。


  // Values in output result should be 'in' plus 1
  float expected = in + 1;
  auto match_expected =
  std::bind(std::equal_to<float>(), std::placeholders::_1, expected);
  bool success = std::all_of(hOut.begin(), hOut.end(), match_expected);
  if (!success) {
    std::stringstream ss("Unexpected result. ");
    ss << "Expected: " << expected << ";\n"
    << "Actual : " << hOut << "\n";
    throw std::runtime_error(ss.str());
  }
  previous = in;
}

 

 このコードのポイントは、setTileMapping にて、VariableとVertexをTileにマッピングすること。