@Vengineerの戯言 : Twitter SystemVerilogの世界へようこそ、すべては、SystemC v0.9公開から始まった
はじめに
GraphcoreのTensorflowのコード眺めました。2020年7月15日なので、11カ月ぶり?
このブログに書いた内容さっぱり覚えていない。。。とりあえず、記録として。
Compiler
HloRunner::CreateExecute メソッドから呼び出されるのが、下記の2つのメソッド。
- backend().compiler()->Compile メソッド
- backend().compiler()->Backend メソッド
Compilerは、PoplarCompilerクラスで Compile メソッドは、ここ にあります。 RunHloPasses メソッドの後に、RunBackendメソッドを実行して、executable を生成しています。
StatusOr<std::vector<std::unique_ptr<Executable>>> PoplarCompiler::Compile(
std::unique_ptr<HloModuleGroup> module_group,
std::vector<std::vector<se::StreamExecutor*>> stream_exec,
se::DeviceMemoryAllocator* device_allocator) {
TENSORFLOW_TRACEPOINT();
if (module_group->empty()) {
return std::vector<std::unique_ptr<Executable>>();
}
if (module_group->size() > 1) {
return tensorflow::errors::Unimplemented(
"Compilation of multiple HLO modules is not supported on Poplar.");
}
if (stream_exec.size() != 1 || stream_exec[0].size() != 1) {
return tensorflow::errors::Unimplemented(
"Unexpected number of StreamExecutor's.");
}
auto hlo_modules = module_group->ConsumeModules();
TF_ASSIGN_OR_RETURN(auto module,
RunHloPasses(std::move(hlo_modules[0]), stream_exec[0][0],
device_allocator));
TF_ASSIGN_OR_RETURN(
auto executable,
RunBackend(std::move(module), stream_exec[0][0], device_allocator));
std::vector<std::unique_ptr<Executable>> ret;
ret.push_back(std::move(executable));
return std::move(ret);
}
RunHloPasses メソッドは何もしていません。
StatusOr<std::unique_ptr<HloModule>> PoplarCompiler::RunHloPasses(
std::unique_ptr<HloModule> module,
perftools::gputools::StreamExecutor* executor,
se::DeviceMemoryAllocator* device_allocator) {
TENSORFLOW_TRACEPOINT();
return std::move(module);
}
代わりに、RunBackend メソッドは巨大です。744行もあります。
ここでプログラムをコンパイルして、poplar::Executableに変換しています。
poplar::Executable exec =
poplar::compileGraph(main_graph, progs, opt_flags, progress_logging);
プログラムは、progs です。下記のように、
- visitor.GetHostToDevice()
- main_program
- visitor.GetDeviceToHost()
を登録しています。
// ======================================================================= // DO NOT CHANGE THE ORDER OF THESE WITHOUT UPDATING PoplarProgramType IN // poplar_executor.h // ======================================================================= progs.push_back(visitor.GetHostToDevice()); progs.push_back(main_program); progs.push_back(visitor.GetDeviceToHost()); // For verified transfers fuse all 3 programs in a single one. if (poplar_executor->UseVerifiedTransfers()) { poplar::program::Sequence fused_program({}, "FusedProgram"); for (auto& prog : progs) { fused_program.add(prog); } progs.clear(); progs.push_back(fused_program); }
Executable
Executabe::ExecuteOnStream メソッド
StatusOr<ExecutionOutput> Executable::ExecuteOnStream(
const ServiceExecutableRunOptions* run_options,
std::vector<ShapeTree<xla::MaybeOwningDeviceMemory>> arguments,
HloExecutionProfile* hlo_execution_profile) {
StatusOr<ExecutionOutput> result = ExecuteAsyncOnStream(
run_options, std::move(arguments), hlo_execution_profile);
Status blocking_status = run_options->stream()->BlockHostUntilDone();
TF_RETURN_IF_ERROR(result.status());
TF_RETURN_IF_ERROR(blocking_status);
return result;
}
PoplarExecutable::ExecuteAsyncOnStream メソッドのAsyncExecuteTask構造体の operator() メソッドにて、ExecuteComputeFunction メソッドが呼ばれています。
struct AsyncExecuteTask {
PoplarExecutable* executable;
ServiceExecutableRunOptions run_options;
se::DeviceMemoryBase output_buffer;
HloExecutionProfile* hlo_execution_profile;
std::vector<se::DeviceMemoryBase> argument_buffers;
std::vector<Shape> argument_shapes;
PoplarExecutor::ArgsHandleMap args_map;
uint64 start_time_us;
void operator()() {
TF_CHECK_OK(executable->ExecuteComputeFunction(
&run_options.run_options(), &output_buffer, hlo_execution_profile,
argument_buffers, argument_shapes, args_map, start_time_us));
}
};
PoplarExecutable::ExecuteComputeFunction メソッドで PoplarExecuteクラスの ExecuteEngine メソッドを呼んでいます。
Status PoplarExecutable::ExecuteComputeFunction(
const ExecutableRunOptions* run_options,
se::DeviceMemoryBase* result_buffer,
HloExecutionProfile* hlo_execution_profile,
const std::vector<se::DeviceMemoryBase>& argument_buffers,
const std::vector<Shape>& argument_shapes,
const PoplarExecutor::ArgsHandleMap& args_map, uint64 start_time_us) {
TENSORFLOW_TRACEPOINT();
VLOG(2) << "Begin asynchronous engine execution " << module().name();
se::Stream* stream = run_options->stream();
se::StreamExecutor* executor = stream->parent();
PoplarExecutor* poplar_executor =
static_cast<PoplarExecutor*>(executor->implementation());
se::DeviceMemoryAllocator* memory_allocator = run_options->allocator();
poplar_executor->ExecuteEngine(result_buffer, executor, *this, args_map,
memory_allocator, argument_buffers);
Executor
Executor は、PoplarExxecutor で、ExecuteEngine メソッドは下記のようにExecuteEngineImpメソッドを呼んでいます。
void PoplarExecutor::ExecuteEngine(se::DeviceMemoryBase* result_buffer, se::StreamExecutor* executor, PoplarExecutable& executable, const ArgsHandleMap& args_map, se::DeviceMemoryAllocator* allocator, const Args& args) { TENSORFLOW_TRACEPOINT(); std::lock_guard<std::recursive_mutex> g(ipu_.Mutex()); if (!current_status_.ok()) { LOG(FATAL) << current_status_.ToString(); } current_status_ = ExecuteEngineImpl(result_buffer, executor, executable, args_map, allocator, args); }
ExecuteEngineImpl メソッドも長いです。実際に実行しているところが下記の部分です。current_engine_ は、poplar::Engineのポインタです。poplar::Engineクラスの run メソッドを実行して、IPU上でプログラムを実行しています。
// Before executing the main program, prepare the random seeds for each // replica. seed_generator_.PrepareSeedsForReplicas(current_replication_factor_); // Run the main engine current_engine_->enableExecutionProfiling(); current_engine_->run(PoplarProgramType::MAIN_SEQUENCE); StopIOThreads();
run メソッドは、
void run(unsigned prog = 0, const std::string &debugName = "");
のようになっています。上記では、PoplarProgramType::MAIN_SEQUENCE を渡しています。この PoplarProgramType::MAIN_SEQUENCE は、poplar_executor.h にて次のように定義されています。MAIN_SEQUENCEはenum ですが、数値だと1になります。
enum PoplarProgramType {
HOST_TO_DEVICE,
MAIN_SEQUENCE,
DEVICE_TO_HOST,
};
これはprogsの1番目、main_program を実行することを意味しています。
progs.push_back(visitor.GetHostToDevice());
progs.push_back(main_program);
progs.push_back(visitor.GetDeviceToHost());
おわりに
Graphcore の Tensorflowのコードを眺め直しました。
TensorFlow XLA から Poplar API に置き換えてる感じです。Poplar APIを直接たたくのに比べて、かなりオーバーヘッドがありますね。