Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions backends/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,29 +252,44 @@ prefix (or the corresponding `CMAKE_INSTALL_DATADIR` location).
CPU engine tests, including quantized linear execution, graph caching, and
mutable buffers.

### Vision classification runner
### Vision classification runners

Enable `EXECUTORCH_BUILD_NATIVE_VISION_RUNNER=ON` together with
`EXECUTORCH_BUILD_NATIVE_VULKAN=ON` to build `run_classification` from
`examples/vision`. The runner additionally requires gflags; an OSS build uses
the vendored copy, and other builds resolve an installed gflags package.
Running the runner requires a Vulkan device.
`examples/vision` shares model loading, FP32 input/output handling, top-5 display,
and eager-reference comparison between the CPU and Vulkan engines.
`run_classification_cpu` uses the CPU engine and `run_classification` the Vulkan
engine; `run_classification_vulkan` is a Buck alias for `run_classification`.

For a CPU-only CMake build:

```sh
cmake -S . -B cmake-out -DCMAKE_BUILD_TYPE=Release \
-DEXECUTORCH_BUILD_NATIVE=ON -DEXECUTORCH_BUILD_NATIVE_VULKAN=ON \
-DEXECUTORCH_BUILD_NATIVE=ON -DEXECUTORCH_BUILD_NATIVE_CPU=ON \
-DEXECUTORCH_BUILD_XNNPACK=ON \
-DEXECUTORCH_BUILD_KERNELS_OPTIMIZED=ON \
-DEXECUTORCH_BUILD_NATIVE_VISION_RUNNER=ON
cmake --build cmake-out --target run_classification
cmake --build cmake-out --target run_classification_cpu
cmake --install cmake-out --prefix install
install/bin/run_classification \
install/bin/run_classification_cpu \
--package mv2.ptn --input mv2_input.bin \
--expected mv2_expected.bin --categories categories.txt
```

`EXECUTORCH_BUILD_NATIVE_CPU` provides `native_cpu_engine`, using
XNNPACK and the Reference provider with optimized/portable operator registrations.
The CPU build does not need a Vulkan SDK or device. Enable
`EXECUTORCH_BUILD_NATIVE_VULKAN=ON` to also build `run_classification`, which
runs on the host's Vulkan device; both engines can be enabled together. The
runners require gflags.

Export the `.ptn`, input, reference logits, and labels with
[`export.py`](../../examples/native/vision/export.py). The runner prints the
top-5 logits and, when `--expected` is given, exits nonzero if the logits
differ from the reference beyond `--rtol`/`--atol` (both default to `1e-3`).
[`export.py`](../../examples/native/vision/export.py). The same package and FP32
artifacts are accepted by either runner, subject to each engine's supported
operators. Each runner expects a model with one FP32 input and one FP32 output,
and prints the selected engine and device and the top-5 logits. When
`--expected` is given, it compares the logits with the reference using
`--rtol`/`--atol` (both default to `1e-3`). Exit codes are 0 on success, 1 for
invalid arguments, 2 for a loading or execution error, and 3 for a reference
mismatch.

## Native LLM runner

Expand Down
41 changes: 34 additions & 7 deletions backends/native/examples/vision/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,47 @@ load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")

oncall("executorch")

# Run an exported .ptn on the Vulkan engine and check the logits against the
# eager reference. Runs on the host's system Vulkan device.
fbcode_target(
_kind = runtime.cxx_binary,
name = "run_classification",
srcs = ["run_classification.cpp"],
runtime.cxx_library(
name = "classification_runner",
srcs = ["ClassificationRunner.cpp"],
exported_headers = ["ClassificationRunner.h"],
exported_deps = ["//executorch/backends/native/runtime/engine:engine"],
deps = [
"//executorch/backends/native/examples/runner_utils:runner_utils",
"//executorch/backends/native/runtime:runtime",
"//executorch/backends/native/runtime/deserialize:package",
"//executorch/backends/native/runtime/engine:engine",
"//executorch/backends/native/runtime/graph:scalar_type",
],
external_deps = ["gflags"],
visibility = ["PUBLIC"],
)

runtime.cxx_binary(
name = "run_classification_cpu",
srcs = ["run_classification_cpu.cpp"],
deps = [
":classification_runner",
"//executorch/backends/native/runtime/cpu:cpu_engine",
],
external_deps = ["gflags"],
visibility = ["PUBLIC"],
)

fbcode_target(
_kind = runtime.cxx_binary,
name = "run_classification",
srcs = ["run_classification.cpp"],
deps = [
":classification_runner",
"//executorch/backends/native/runtime/vulkan:vulkan_engine",
],
external_deps = ["gflags"],
visibility = ["PUBLIC"],
)

fbcode_target(
_kind = runtime.command_alias,
name = "run_classification_vulkan",
exe = ":run_classification",
visibility = ["PUBLIC"],
)
44 changes: 35 additions & 9 deletions backends/native/examples/vision/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(EXECUTORCH_ROOT "${executorch_native_PREFIX}/include/executorch")
add_subdirectory(../runner_utils runner_utils)
set(_native_vision_runtime executorch_native::runtime)
set(_native_vision_vulkan_engine executorch_native::runtime)
set(EXECUTORCH_BUILD_NATIVE_VULKAN ON)

if(NOT TARGET gflags AND NOT TARGET gflags::gflags)
find_package(gflags REQUIRED)
Expand All @@ -23,19 +25,43 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(_native_runner_gflags gflags)
endif()
else()
set(_native_vision_runtime native_engine native_package native_runtime
native_vulkan_engine
)
set(_native_vision_runtime native_engine)
set(_native_vision_vulkan_engine native_vulkan_engine)
endif()

add_executable(run_classification run_classification.cpp)
add_library(native_classification_runner STATIC ClassificationRunner.cpp)
target_link_libraries(
run_classification PRIVATE ${_native_runner_gflags} native_runner_utils
${_native_vision_runtime}
native_classification_runner
PUBLIC ${_native_vision_runtime}
PRIVATE ${_native_runner_gflags} native_runner_utils
)
target_compile_options(
run_classification PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-fexceptions>
native_classification_runner
PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-fexceptions>
)

install(TARGETS run_classification RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
function(add_native_classification_runner target source engine)
add_executable(${target} ${source})
target_link_libraries(
${target} PRIVATE native_classification_runner ${_native_runner_gflags}
${engine}
)
target_compile_options(
${target} PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-fexceptions>
)
install(TARGETS ${target} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endfunction()

if(EXECUTORCH_BUILD_NATIVE_VULKAN)
add_native_classification_runner(
run_classification run_classification.cpp ${_native_vision_vulkan_engine}
)
endif()

if(EXECUTORCH_BUILD_NATIVE_CPU)
add_native_classification_runner(
run_classification_cpu run_classification_cpu.cpp native_cpu_engine
)
endif()
178 changes: 178 additions & 0 deletions backends/native/examples/vision/ClassificationRunner.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 <executorch/backends/native/examples/vision/ClassificationRunner.h>

#include <cstdint>
#include <cstdio>
#include <exception>
#include <memory>
#include <string>
#include <vector>

#include <executorch/backends/native/examples/runner_utils/runner_utils.h>
#include <executorch/backends/native/runtime/Program.h>
#include <executorch/backends/native/runtime/deserialize/Package.h>
#include <executorch/backends/native/runtime/engine/Engine.h>
#include <executorch/backends/native/runtime/graph/ScalarType.h>
#include <gflags/gflags.h>

DEFINE_string(package, "", "Path to the *.ptn to run. Required.");
DEFINE_string(input, "", "Path to the input tensor, raw float32. Required.");
DEFINE_string(
expected,
"",
"Reference logits to compare against, raw float32.");
DEFINE_string(categories, "", "Newline-separated class labels, for display.");
DEFINE_string(method, "", "Method to run. Defaults to the first one.");
DEFINE_double(rtol, 1e-3, "Relative tolerance for the logits comparison.");
DEFINE_double(atol, 1e-3, "Absolute tolerance for the logits comparison.");
namespace {

std::string label_of(const std::vector<std::string>& labels, int index) {
if (index >= 0 && index < static_cast<int>(labels.size())) {
return labels[index];
}
return "class_" + std::to_string(index);
}

void print_top5(
const std::vector<float>& logits,
const std::vector<std::string>& labels) {
const std::vector<int> order = ptn::runner_utils::top_k(logits, 5);
std::printf("top-5 (class : logit):\n");
for (const int index : order) {
std::printf(
" %4d %-28s %10.4f\n",
index,
label_of(labels, index).c_str(),
logits[index]);
}
}

} // namespace

namespace ptn {

int run_classification(
const std::function<std::unique_ptr<EngineHost>()>& make_host,
const std::function<ConstantBytes(const EngineContext&)>& constant_bytes) {
if (FLAGS_package.empty() || FLAGS_input.empty()) {
std::fprintf(stderr, "error: --package and --input are required\n");
return 1;
}

try {
const auto package =
std::make_shared<const ptn::Package>(ptn::Package::load(FLAGS_package));
const ptn::ByteSpan program_bytes = package->program_bytes();
const auto program = std::make_shared<const ptn::Program>(
ptn::Program::load(program_bytes.data(), program_bytes.size()));

const std::vector<std::string> names = program->method_names();
if (names.empty()) {
std::fprintf(stderr, "error: package holds no methods\n");
return 2;
}
const std::string method_name =
FLAGS_method.empty() ? names[0] : FLAGS_method;
std::printf(
"package: %s (program %zu bytes, %zu constants / %zu bytes)\n",
FLAGS_package.c_str(),
program_bytes.size(),
package->keys().size(),
package->constant_bytes());

const auto host = make_host();
const auto context = host->create_context(program, package);
std::printf(
"engine: %s on %s\n",
host->name().c_str(),
host->device_name().c_str());

const std::unique_ptr<ptn::EngineExecutable> executable =
context->compile(method_name);
std::printf(
"method: %s (%zu inputs, %zu outputs)\n",
method_name.c_str(),
executable->num_inputs(),
executable->num_outputs());
const ConstantBytes constants = constant_bytes(*context);
std::printf(
"constants: %zu unique source bytes, %zu materialized\n",
constants.unique,
constants.materialized);

if (executable->num_inputs() != 1 || executable->num_outputs() != 1) {
std::fprintf(
stderr,
"error: expected 1 input and 1 output, got %zu and %zu\n",
executable->num_inputs(),
executable->num_outputs());
return 2;
}
if (executable->input_dtype(0) != ptn::kFloat ||
executable->output_dtype(0) != ptn::kFloat) {
std::fprintf(stderr, "error: this runner handles fp32 models only\n");
return 2;
}

const std::vector<int64_t> in_sizes = executable->input_sizes(0);
const std::vector<int64_t> out_sizes = executable->output_sizes(0);
std::printf(
" input: %s\n output: %s\n",
ptn::runner_utils::shape_str(in_sizes).c_str(),
ptn::runner_utils::shape_str(out_sizes).c_str());

const std::vector<float> input =
ptn::runner_utils::read_floats(FLAGS_input);
if (static_cast<int64_t>(input.size()) !=
ptn::runner_utils::numel_of(in_sizes)) {
std::fprintf(
stderr,
"error: input has %zu floats, the model wants %lld\n",
input.size(),
static_cast<long long>(ptn::runner_utils::numel_of(in_sizes)));
return 2;
}

executable->set_input(0, input.data(), input.size(), ptn::kFloat);
executable->execute();

std::vector<float> logits(
static_cast<size_t>(ptn::runner_utils::numel_of(out_sizes)));
executable->get_output(0, logits.data(), logits.size(), ptn::kFloat);
if (logits.empty()) {
std::fprintf(stderr, "error: model produced no logits\n");
return 2;
}

std::vector<std::string> labels;
if (!FLAGS_categories.empty()) {
labels = ptn::runner_utils::read_lines(FLAGS_categories);
}
print_top5(logits, labels);
const int argmax = ptn::runner_utils::top_k(logits, 1).front();
std::printf(
"argmax: %d (%s)\n", argmax, label_of(labels, argmax).c_str());

if (FLAGS_expected.empty()) {
std::printf("result: ran (no reference given)\n");
return 0;
}
const std::vector<float> reference =
ptn::runner_utils::read_floats(FLAGS_expected);
const bool pass = ptn::runner_utils::compare_floats(
logits, reference, FLAGS_rtol, FLAGS_atol);
std::printf("result: %s\n", pass ? "PASS" : "FAIL");
return pass ? 0 : 3;
} catch (const std::exception& e) {
std::fprintf(stderr, "error: %s\n", e.what());
return 2;
}
}

} // namespace ptn
32 changes: 32 additions & 0 deletions backends/native/examples/vision/ClassificationRunner.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 <cstddef>
#include <functional>
#include <memory>

#include <executorch/backends/native/runtime/engine/Engine.h>

namespace ptn {

struct ConstantBytes {
size_t unique = 0;
size_t materialized = 0;
};

template <typename Context>
ConstantBytes constant_bytes(const EngineContext& context) {
const auto& typed = dynamic_cast<const Context&>(context);
return {typed.unique_constant_bytes(), typed.materialized_constant_bytes()};
}

int run_classification(
const std::function<std::unique_ptr<EngineHost>()>& make_host,
const std::function<ConstantBytes(const EngineContext&)>& constant_bytes);

} // namespace ptn
Loading
Loading