From adbe87720093a39dd1508d7e2e9674ca3403beb9 Mon Sep 17 00:00:00 2001 From: Stephen Jia Date: Mon, 24 Aug 2026 11:42:09 -0700 Subject: [PATCH] Update [ghstack-poisoned] --- backends/native/runtime/BUCK | 11 + backends/native/runtime/Program.cpp | 54 +++ backends/native/runtime/Program.h | 65 +++ backends/native/runtime/graph/BUCK | 11 + backends/native/runtime/graph/Format.h | 34 ++ backends/native/runtime/graph/targets.bzl | 11 + backends/native/runtime/targets.bzl | 59 +++ backends/native/runtime/utils/ToDot.cpp | 559 ++++++++++++++++++++++ backends/native/tools/native_executor.cpp | 27 ++ backends/native/tools/targets.bzl | 3 + 10 files changed, 834 insertions(+) create mode 100644 backends/native/runtime/BUCK create mode 100644 backends/native/runtime/Program.cpp create mode 100644 backends/native/runtime/Program.h create mode 100644 backends/native/runtime/graph/BUCK create mode 100644 backends/native/runtime/graph/Format.h create mode 100644 backends/native/runtime/graph/targets.bzl create mode 100644 backends/native/runtime/targets.bzl create mode 100644 backends/native/runtime/utils/ToDot.cpp diff --git a/backends/native/runtime/BUCK b/backends/native/runtime/BUCK new file mode 100644 index 00000000000..0ab35888218 --- /dev/null +++ b/backends/native/runtime/BUCK @@ -0,0 +1,11 @@ +load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target", "non_fbcode_target") +load(":targets.bzl", "define_common_targets") + +oncall("executorch") + +# Any targets that should be shared between fbcode and xplat must be defined in +# targets.bzl. This file can contain cell-only targets. + +non_fbcode_target(_kind = define_common_targets) + +fbcode_target(_kind = define_common_targets) diff --git a/backends/native/runtime/Program.cpp b/backends/native/runtime/Program.cpp new file mode 100644 index 00000000000..e15589ad04a --- /dev/null +++ b/backends/native/runtime/Program.cpp @@ -0,0 +1,54 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +#include + +#include +#include +#include +#include + +#include + +#include + +namespace ptn { + +namespace { +// Minimum bytes for a FlatBuffer carrying a file identifier: a 4-byte root +// offset plus the 4-byte identifier. +constexpr size_t kMinBufferSize = 8; +} // namespace + +Program Program::load(const void* data, size_t size) { + if (data == nullptr || size < kMinBufferSize) { + throw std::runtime_error("native program: buffer is null or too small"); + } + + const uint8_t* begin = static_cast(data); + std::vector bytes(begin, begin + size); + + if (!::native_backend::ProgramBufferHasIdentifier(bytes.data())) { + throw std::runtime_error( + "native program: bad FlatBuffer file identifier (expected 'NPTG')"); + } + + flatbuffers::Verifier verifier(bytes.data(), bytes.size()); + if (!::native_backend::VerifyProgramBuffer(verifier)) { + throw std::runtime_error("native program: FlatBuffer verification failed"); + } + + const ::native_backend::Program* program_fb = + ::native_backend::GetProgram(bytes.data()); + return Program(std::move(bytes), program_fb); +} + +size_t Program::num_methods() const { + const auto* methods = program_fb_->methods(); + return methods == nullptr ? 0 : methods->size(); +} + +} // namespace ptn diff --git a/backends/native/runtime/Program.h b/backends/native/runtime/Program.h new file mode 100644 index 00000000000..acaa4198b4a --- /dev/null +++ b/backends/native/runtime/Program.h @@ -0,0 +1,65 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +#pragma once + +#include +#include +#include +#include + +// Forward-declaration of the generated FlatBuffer root type. The generated +// header is included only in Program.cpp / utils/ToGraphViz.cpp, so flatbuffers +// stays an implementation detail of the reader. +namespace native_backend { +struct Program; +} // namespace native_backend + +namespace ptn { + +// A loaded native-graph program: owns the serialized FlatBuffer bytes and +// exposes a zero-copy view of the root. Reader-only for now (no in-memory +// mutable graph, constants, or execution). +class Program { + private: + // Owns the bytes; the program_fb_ pointer aliases into this buffer. + // std::vector's move preserves the buffer address, so program_fb_ stays valid + // across a move. Never null on a live Program: the constructor is private and + // load(), the only caller, throws rather than hand back a null root, so the + // accessors below dereference it unchecked. + std::vector bytes_; + const ::native_backend::Program* program_fb_ = nullptr; + + Program( + std::vector bytes, + const ::native_backend::Program* program_fb) + : bytes_(std::move(bytes)), program_fb_(program_fb) {} + + public: + ~Program() = default; + Program(Program&&) noexcept = default; + Program& operator=(Program&&) noexcept = default; + Program(const Program&) = delete; + Program& operator=(const Program&) = delete; + + // Parse and verify serialized native-graph bytes (a *.nptg buffer). Throws + // std::runtime_error on failure. The returned Program owns a copy of the + // bytes; the zero-copy accessors are valid for its lifetime. + static Program load(const void* data, size_t size); + + // Zero-copy FlatBuffer root, pointing into this Program's owned bytes. + const ::native_backend::Program* flatbuffer() const { + return program_fb_; + } + + size_t num_methods() const; + + // Render this program to Graphviz DOT text (impl in utils/ToDot.cpp). Pure + // string builder; the caller writes/renders it (e.g. `dot -Tpng`). + std::string to_dot() const; +}; + +} // namespace ptn diff --git a/backends/native/runtime/graph/BUCK b/backends/native/runtime/graph/BUCK new file mode 100644 index 00000000000..0ab35888218 --- /dev/null +++ b/backends/native/runtime/graph/BUCK @@ -0,0 +1,11 @@ +load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target", "non_fbcode_target") +load(":targets.bzl", "define_common_targets") + +oncall("executorch") + +# Any targets that should be shared between fbcode and xplat must be defined in +# targets.bzl. This file can contain cell-only targets. + +non_fbcode_target(_kind = define_common_targets) + +fbcode_target(_kind = define_common_targets) diff --git a/backends/native/runtime/graph/Format.h b/backends/native/runtime/graph/Format.h new file mode 100644 index 00000000000..d139dfc722b --- /dev/null +++ b/backends/native/runtime/graph/Format.h @@ -0,0 +1,34 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +#pragma once + +#include +#include +#include + +namespace ptn { + +// Render a double for a debug dump. Not std::to_string: its fixed six-decimal +// format prints 1e-8 as "0.000000" and 1e300 as 312 digits. to_chars emits the +// shortest form that round-trips. The longest such form is 24 characters +// ("-1.7976931348623157e+308"), so the buffer cannot overflow and the result +// needs no error check. +inline std::string format_double(double value) { + std::array buf{}; + const std::to_chars_result out = + std::to_chars(buf.data(), buf.data() + buf.size(), value); + std::string text(buf.data(), out.ptr); + // to_chars renders 6.0 as "6", which in a dump reads as an int argument. + // Put the point back; exponent, "inf" and "nan" forms are already + // unambiguous, and each carries one of these characters. + if (text.find_first_of(".eni") == std::string::npos) { + text += ".0"; + } + return text; +} + +} // namespace ptn diff --git a/backends/native/runtime/graph/targets.bzl b/backends/native/runtime/graph/targets.bzl new file mode 100644 index 00000000000..306231ec0b4 --- /dev/null +++ b/backends/native/runtime/graph/targets.bzl @@ -0,0 +1,11 @@ +load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime") + +def define_common_targets(): + # Debug-dump formatting shared by every renderer of the IR (header-only). + runtime.cxx_library( + name = "format", + exported_headers = [ + "Format.h", + ], + visibility = ["//executorch/backends/native/..."], + ) diff --git a/backends/native/runtime/targets.bzl b/backends/native/runtime/targets.bzl new file mode 100644 index 00000000000..e6b810f845f --- /dev/null +++ b/backends/native/runtime/targets.bzl @@ -0,0 +1,59 @@ +load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "is_xplat", "runtime") + +def define_common_targets(): + # The wrapper rewrites "//executorch/..." references for xplat in deps / + # exported_deps / visibility only, not in srcs, so spell the schema target + # out per cell. is_xplat() reads the package context, so it can only be + # called from inside this function, not at module scope. + native_graph_fbs = ( + "//xplat/executorch/backends/native:native_graph.fbs" if is_xplat() else "//executorch/backends/native:native_graph.fbs" + ) + + # Compile the native graph FlatBuffer schema to a C++ header. flatc takes an + # output directory (not a file), so use `outs` to expand ${OUT} to the dir. + runtime.genrule( + name = "generate_native_graph", + srcs = [native_graph_fbs], + outs = {"native_graph_generated.h": ["native_graph_generated.h"]}, + default_outs = ["native_graph_generated.h"], + cmd = " ".join([ + "$(exe {})".format(runtime.external_dep_location("flatc")), + "--cpp", + "--cpp-std c++11", + "--gen-mutable", + "--scoped-enums", + "-o ${OUT}", + "${SRCS}", + ]), + ) + + # Header-only library exposing the generated FlatBuffer accessors. Kept internal + # so flatbuffers stays an implementation detail of the reader. + runtime.cxx_library( + name = "native_graph_schema", + srcs = [], + exported_headers = { + "native_graph_generated.h": ":generate_native_graph[native_graph_generated.h]", + }, + exported_external_deps = ["flatbuffers-api"], + visibility = ["//executorch/backends/native/..."], + ) + + # The native runtime program reader (standalone; no ExecuTorch dependency). + # utils/ToDot.cpp implements Program::to_dot() (DOT rendering); it is part of + # this package (no BUCK under utils/). + runtime.cxx_library( + name = "runtime", + srcs = [ + "Program.cpp", + "utils/ToDot.cpp", + ], + exported_headers = [ + "Program.h", + ], + deps = [ + ":native_graph_schema", + "//executorch/backends/native/runtime/graph:format", + ], + visibility = ["PUBLIC"], + ) diff --git a/backends/native/runtime/utils/ToDot.cpp b/backends/native/runtime/utils/ToDot.cpp new file mode 100644 index 00000000000..a24a51c4913 --- /dev/null +++ b/backends/native/runtime/utils/ToDot.cpp @@ -0,0 +1,559 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +#include + +#include +#include +#include + +#include +#include + +// Everything below runs on a buffer that Program::load() has already put +// through flatbuffers::Verifier, so accessors return non-null wherever the +// schema declares the field required and wherever a union discriminator +// matches; the helpers here dereference those results directly. Fields the +// schema leaves optional are still checked, because verification says nothing +// about whether they are present. + +namespace ptn { +namespace { + +namespace nb = ::native_backend; + +// ---- small string helpers --------------------------------------------------- + +// Escape a dynamic string for embedding inside a DOT double-quoted label: +// backslash and double-quote get escaped; real newlines collapse to spaces so +// they cannot break the label (we insert line breaks ourselves as the literal +// two-character sequence "\n"). +std::string esc(const std::string& s) { + std::string out; + out.reserve(s.size()); + for (const char c : s) { + switch (c) { + case '\\': + out += "\\\\"; + break; + case '"': + out += "\\\""; + break; + case '\n': + case '\r': + out += ' '; + break; + default: + out += c; + } + } + return out; +} + +std::string str_of(const flatbuffers::String* s) { + return s != nullptr ? s->str() : std::string(); +} + +bool nonempty(const flatbuffers::String* s) { + return s != nullptr && s->size() > 0; +} + +// ---- schema-node -> label fragments ----------------------------------------- + +std::string dim_str(const nb::Dim* d) { + if (d->min() == d->max()) { + return std::to_string(d->min()); + } + return std::to_string(d->min()) + ".." + + (d->max() < 0 ? std::string("inf") : std::to_string(d->max())); +} + +std::string quant_suffix(const nb::QuantSpec* q) { + if (q == nullptr) { + return ""; + } + switch (q->scheme_type()) { + case nb::QuantScheme::AffineGroup: { + const auto* a = q->scheme_as_AffineGroup(); + const int gs = a != nullptr ? a->group_size() : 0; + return std::string(" q:affine g=") + + (gs == 0 ? "perchan" : std::to_string(gs)); + } + case nb::QuantScheme::PackedQuant: { + const auto* p = q->scheme_as_PackedQuant(); + return std::string(" q:") + (p != nullptr ? str_of(p->codec()) : ""); + } + default: + return ""; + } +} + +// e.g. "FLOAT[16,16]" or "BYTE[8,16] q:affine g=32" +std::string meta_label(const nb::TensorMeta* m) { + if (m == nullptr) { + return ""; + } + std::string s = nb::EnumNameScalarType(m->dtype()); + s += "["; + const auto* sizes = m->sizes(); + if (sizes != nullptr) { + for (flatbuffers::uoffset_t i = 0; i < sizes->size(); ++i) { + if (i != 0) { + s += ","; + } + s += dim_str(sizes->Get(i)); + } + } + s += "]"; + s += quant_suffix(m->quant()); + return s; +} + +// Compact rendering of a non-tensor argument for a call node's label. Returns +// raw text (the caller esc()s it). Tensor/tensor-list args are drawn as edges, +// not here. +std::string arg_str(const nb::Argument* a) { + using AV = nb::ArgumentValue; + switch (a->value_type()) { + case AV::NoneArg: + return "None"; + case AV::TensorArg: + return "%" + str_of(a->value_as_TensorArg()->name()); + case AV::IntArg: { + const auto* x = a->value_as_IntArg(); + return nonempty(x->ref()) ? "%" + str_of(x->ref()) + : std::to_string(x->value()); + } + case AV::FloatArg: { + const auto* x = a->value_as_FloatArg(); + return nonempty(x->ref()) ? "%" + str_of(x->ref()) + : format_double(x->value()); + } + case AV::BoolArg: { + const auto* x = a->value_as_BoolArg(); + return nonempty(x->ref()) ? "%" + str_of(x->ref()) + : (x->value() ? "true" : "false"); + } + case AV::StringArg: + return "\"" + str_of(a->value_as_StringArg()->value()) + "\""; + case AV::ScalarTypeArg: + return nb::EnumNameScalarType(a->value_as_ScalarTypeArg()->value()); + case AV::IntListArg: { + const auto* x = a->value_as_IntListArg(); + const auto* vals = x->values(); + const auto* refs = x->refs(); + std::string s = "["; + if (vals != nullptr) { + for (flatbuffers::uoffset_t i = 0; i < vals->size(); ++i) { + if (i != 0) { + s += ","; + } + if (refs != nullptr && i < refs->size() && nonempty(refs->Get(i))) { + s += "%" + refs->Get(i)->str(); + } else { + s += std::to_string(vals->Get(i)); + } + } + } + return s + "]"; + } + case AV::FloatListArg: { + const auto* vals = a->value_as_FloatListArg()->values(); + std::string s = "["; + if (vals != nullptr) { + for (flatbuffers::uoffset_t i = 0; i < vals->size(); ++i) { + if (i != 0) { + s += ","; + } + s += format_double(vals->Get(i)); + } + } + return s + "]"; + } + case AV::BoolListArg: { + const auto* vals = a->value_as_BoolListArg()->values(); + std::string s = "["; + if (vals != nullptr) { + for (flatbuffers::uoffset_t i = 0; i < vals->size(); ++i) { + if (i != 0) { + s += ","; + } + s += vals->Get(i) ? "true" : "false"; + } + } + return s + "]"; + } + case AV::TensorListArg: { + const auto* names = a->value_as_TensorListArg()->names(); + std::string s = "["; + if (names != nullptr) { + for (flatbuffers::uoffset_t i = 0; i < names->size(); ++i) { + if (i != 0) { + s += ","; + } + s += "%" + names->Get(i)->str(); + } + } + return s + "]"; + } + case AV::GraphArg: + return "graph(" + str_of(a->value_as_GraphArg()->name()) + ")"; + default: + return nb::EnumNameArgumentValue(a->value_type()); + } +} + +bool is_tensor_like(nb::ArgumentValue t) { + return t == nb::ArgumentValue::TensorArg || + t == nb::ArgumentValue::TensorListArg || + t == nb::ArgumentValue::OptionalTensorListArg || + t == nb::ArgumentValue::GraphArg; +} + +// Method-level side tables (empty for HOP subgraphs, which carry none). +struct Ctx { + std::unordered_map consts; + std::unordered_map muts; + std::unordered_map ospecs; +}; + +// Draws the dataflow edges of one graph. Bundles what every edge needs: the +// output buffer, the graph's node-id prefix, its metadata and producer +// (name -> defining node id) side tables, and the method context. `dangling` +// numbers the synthesized sources for values with no producer in this graph, so +// it persists across draw() calls. One emitter per graph body. +struct EdgeEmitter { + std::string& out; + const std::string& prefix; + const std::unordered_map& tm; + const std::unordered_map& def; + const Ctx& ctx; + int dangling = 0; + + void draw( + const std::string& vn, + const std::string& to_id, + const std::string& suffix, + bool mutated, + bool to_output); +}; + +void EdgeEmitter::draw( + const std::string& vn, + const std::string& to_id, + const std::string& suffix, + bool mutated, + bool to_output) { + std::string label = esc(vn); + const auto mt = tm.find(vn); + if (mt != tm.end()) { + label += " " + meta_label(mt->second); + } + label += suffix; + std::string attrs; + if (mutated) { + label += " (a!)"; + attrs += ", color=red, style=bold"; + } + if (to_output) { + const auto os = ctx.ospecs.find(vn); + if (os != ctx.ospecs.end() && + os->second->kind() != nb::OutputKind::USER_OUTPUT) { + label += " [" + std::string(nb::EnumNameOutputKind(os->second->kind())) + + "->" + esc(str_of(os->second->target())) + "]"; + } + } + const auto it = def.find(vn); + std::string from; + if (it != def.end()) { + from = it->second; + } else { + // No producer in this graph (malformed or lifted operand): synthesize a + // visible source so the edge is not silently dropped. + from = prefix + "_ext" + std::to_string(dangling++); + out += " " + from + " [shape=point, color=red];\n"; + } + out += " " + from + " -> " + to_id + " [label=\"" + label + "\"" + attrs + + "];\n"; +} + +// Forward decl: graph emission recurses through HOP subgraphs. +void emit_graph( + std::string& out, + const std::string& prefix, + const std::string& title, + const nb::Graph* g, + const Ctx& ctx); + +// A placeholder node's label + fill style, from the method side tables. +void placeholder_label( + const std::string& name, + const nb::TensorMeta* meta, + const Ctx& ctx, + std::string& label, + std::string& extra) { + const auto ci = ctx.consts.find(name); + if (ci != ctx.consts.end()) { + const nb::NamedTensorRef* c = ci->second; + label = esc(name) + "\\n" + nb::EnumNameInputKind(c->kind()) + + (c->mutated() ? " (mut)" : "") + "\\n" + esc(str_of(c->data_key())); + if (c->meta() != nullptr) { + label += "\\n" + meta_label(c->meta()); + } + extra = ", style=filled, fillcolor=\"#e6e6e6\""; + return; + } + const auto mi = ctx.muts.find(name); + if (mi != ctx.muts.end()) { + label = esc(name) + "\\nMUTABLE_BUFFER\\n" + esc(str_of(mi->second->fqn())); + if (meta != nullptr) { + label += "\\n" + meta_label(meta); + } + extra = ", style=filled, fillcolor=\"#fff2cc\""; + return; + } + label = esc(name) + "\\nUSER_INPUT"; + if (meta != nullptr) { + label += "\\n" + meta_label(meta); + } +} + +void emit_node( + std::string& out, + const std::string& id, + const nb::Node* nd, + const std::unordered_map& tm, + const Ctx& ctx) { + const std::string name = str_of(nd->name()); + std::string shape; + std::string label; + std::string extra; + + switch (nd->op_kind()) { + case nb::OpKind::PLACEHOLDER: { + shape = "oval"; + const auto it = tm.find(name); + placeholder_label( + name, it != tm.end() ? it->second : nullptr, ctx, label, extra); + break; + } + case nb::OpKind::OUTPUT: + shape = "doubleoctagon"; + label = name.empty() ? "output" : esc(name); + break; + default: { + shape = "box"; + label = esc(name) + "\\n" + esc(str_of(nd->target())); + const auto* ins = nd->inputs(); + if (ins != nullptr) { + for (const nb::NamedArgument* na : *ins) { + if (is_tensor_like(na->arg()->value_type())) { + continue; // drawn as an edge + } + label += + "\\n" + esc(str_of(na->name())) + "=" + esc(arg_str(na->arg())); + } + } + } + } + out += " " + id + " [shape=" + shape + ", label=\"" + label + "\"" + + extra + "];\n"; +} + +void emit_graph( + std::string& out, + const std::string& prefix, + const std::string& title, + const nb::Graph* g, + const Ctx& ctx) { + if (g == nullptr) { + return; + } + + // value name -> tensor metadata + std::unordered_map tm; + if (const auto* tvs = g->tensor_values()) { + for (const nb::TensorValue* tv : *tvs) { + tm[str_of(tv->name())] = tv->meta(); + } + } + + // per-node ids + value name -> producing node id (def-use inversion) + const auto* nodes = g->nodes(); + const flatbuffers::uoffset_t n = nodes != nullptr ? nodes->size() : 0; + std::vector ids(n); + std::unordered_map def; + for (flatbuffers::uoffset_t j = 0; j < n; ++j) { + ids[j] = prefix + "_" + std::to_string(j); + const nb::Node* nd = nodes->Get(j); + if (const auto* outs = nd->outputs()) { + for (const nb::Output* o : *outs) { + if (nonempty(o->name())) { + def[o->name()->str()] = ids[j]; + } + if (o->kind() == nb::OutputValueKind::TENSOR_LIST) { + if (const auto* nm = o->names()) { + for (const flatbuffers::String* s : *nm) { + def[s->str()] = ids[j]; + } + } + } + } + } + } + + out += " subgraph cluster_" + prefix + " {\n"; + out += " label=\"" + esc(title) + "\";\n"; + out += " style=rounded; color=gray;\n"; + + for (flatbuffers::uoffset_t j = 0; j < n; ++j) { + emit_node(out, ids[j], nodes->Get(j), tm, ctx); + } + + EdgeEmitter edges{out, prefix, tm, def, ctx}; + + for (flatbuffers::uoffset_t j = 0; j < n; ++j) { + const nb::Node* nd = nodes->Get(j); + const bool to_output = nd->op_kind() == nb::OpKind::OUTPUT; + if (const auto* ins = nd->inputs()) { + // Distinct cluster per GraphArg. Must stay outside the argument loop: a + // node may carry several GraphArgs and each needs its own suffix. + int sg_index = 0; + for (const nb::NamedArgument* na : *ins) { + const nb::Argument* a = na->arg(); + switch (a->value_type()) { + case nb::ArgumentValue::TensorArg: + edges.draw( + str_of(a->value_as_TensorArg()->name()), + ids[j], + "", + na->mutated(), + to_output); + break; + case nb::ArgumentValue::TensorListArg: { + const auto* nm = a->value_as_TensorListArg()->names(); + if (nm != nullptr) { + for (flatbuffers::uoffset_t x = 0; x < nm->size(); ++x) { + edges.draw( + nm->Get(x)->str(), + ids[j], + "[" + std::to_string(x) + "]", + na->mutated(), + to_output); + } + } + break; + } + case nb::ArgumentValue::OptionalTensorListArg: { + const auto* oa = a->value_as_OptionalTensorListArg(); + const auto* nm = oa->names(); + const auto* hv = oa->has_value(); + if (nm != nullptr) { + for (flatbuffers::uoffset_t x = 0; x < nm->size(); ++x) { + if (hv != nullptr && x < hv->size() && hv->Get(x)) { + edges.draw( + nm->Get(x)->str(), + ids[j], + "[" + std::to_string(x) + "]?", + na->mutated(), + to_output); + } + } + } + break; + } + case nb::ArgumentValue::GraphArg: { + const nb::GraphArg* ga = a->value_as_GraphArg(); + const std::string name = str_of(ga->name()); + const std::string sg = ids[j] + "_sg" + std::to_string(sg_index++); + emit_graph(out, sg, "subgraph: " + name, ga->graph(), Ctx{}); + const auto* sgn = + ga->graph() != nullptr ? ga->graph()->nodes() : nullptr; + if (sgn != nullptr && sgn->size() > 0) { + out += " "; + out += ids[j]; + out += " -> "; + out += sg; + out += "_0 [style=dashed, color=blue, label=\""; + out += esc(name); + out += "\", lhead=cluster_"; + out += sg; + out += "];\n"; + } + break; + } + default: + break; // scalars shown in the node label + } + } + } + if (const auto* outs = nd->outputs()) { + for (const nb::Output* o : *outs) { + if (nonempty(o->alias_of())) { + const auto it = def.find(o->alias_of()->str()); + if (it != def.end()) { + out += " " + it->second + " -> " + ids[j] + + " [style=dashed, color=\"#888888\", label=\"alias\"];\n"; + } + } + } + } + } + + out += " }\n"; +} + +std::string render_program(const nb::Program& program_fb) { + std::string out; + out += "digraph program {\n"; + out += " compound=true;\n"; + out += " rankdir=TB;\n"; + out += " labelloc=\"t\";\n"; + out += " node [fontname=\"monospace\", fontsize=10];\n"; + out += " edge [fontname=\"monospace\", fontsize=9];\n"; + const std::string ver = str_of(program_fb.version()); + out += " label=\"native_backend::Program" + + (ver.empty() ? std::string() : " version=" + esc(ver)) + "\";\n"; + + if (const auto* methods = program_fb.methods()) { + for (flatbuffers::uoffset_t i = 0; i < methods->size(); ++i) { + const nb::Method* m = methods->Get(i); + Ctx ctx; + if (const auto* cs = m->constants()) { + for (const nb::NamedTensorRef* c : *cs) { + ctx.consts[str_of(c->name())] = c; + } + } + if (const auto* mbs = m->mutable_buffers()) { + for (const nb::MutableBufferSpec* mb : *mbs) { + ctx.muts[str_of(mb->name())] = mb; + } + } + if (const auto* os = m->output_specs()) { + for (const nb::OutputSpec* o : *os) { + ctx.ospecs[str_of(o->name())] = o; + } + } + emit_graph( + out, + "m" + std::to_string(i), + "method: " + str_of(m->name()), + m->graph(), + ctx); + } + } + + out += "}\n"; + return out; +} + +} // namespace + +std::string Program::to_dot() const { + return render_program(*program_fb_); +} + +} // namespace ptn diff --git a/backends/native/tools/native_executor.cpp b/backends/native/tools/native_executor.cpp index 1188d616b94..3f52e8f594d 100644 --- a/backends/native/tools/native_executor.cpp +++ b/backends/native/tools/native_executor.cpp @@ -15,15 +15,18 @@ #include #include #include +#include #include #include #include #include +#include #include DEFINE_string(program, "", "Path to the *.nptg to load. Required."); DEFINE_string(constants, "", "Path to the out-of-line constant file."); +DEFINE_bool(dot, false, "Emit Graphviz DOT to stdout, then exit."); namespace { @@ -62,6 +65,12 @@ bool has_program_magic(const std::vector& buf) { 0; } +// Emit `text` on stdout. False on a short write, so a full disk truncating a +// redirected dump is reported rather than exiting 0 on partial output. +bool write_stdout(const std::string& text) { + return std::fwrite(text.data(), 1, text.size(), stdout) == text.size(); +} + } // namespace int main(int argc, char** argv) { @@ -77,6 +86,24 @@ int main(int argc, char** argv) { if (!program) { return 1; } + + // --dot: parse + verify via the program reader, emit Graphviz DOT to stdout + // (nothing else), then exit. Pipe to a file and render with `dot -Tpng`. + if (FLAGS_dot) { + try { + const ptn::Program prog = + ptn::Program::load(program->data(), program->size()); + if (!write_stdout(prog.to_dot())) { + std::fprintf(stderr, "error: short write emitting DOT\n"); + return 1; + } + return 0; + } catch (const std::exception& e) { + std::fprintf(stderr, "error: %s\n", e.what()); + return 2; + } + } + const bool magic_ok = has_program_magic(*program); std::printf("program: %s\n", FLAGS_program.c_str()); std::printf(" bytes: %zu\n", program->size()); diff --git a/backends/native/tools/targets.bzl b/backends/native/tools/targets.bzl index 211e474f9b4..f2d2251f71d 100644 --- a/backends/native/tools/targets.bzl +++ b/backends/native/tools/targets.bzl @@ -4,6 +4,9 @@ def define_common_targets(): runtime.cxx_binary( name = "native_executor", srcs = ["native_executor.cpp"], + deps = [ + "//executorch/backends/native/runtime:runtime", + ], external_deps = ["gflags"], visibility = ["PUBLIC"], )