From a28899de590a47fd6f19380a3b5430aaeebafd44 Mon Sep 17 00:00:00 2001 From: Chen Lai Date: Wed, 4 Jun 2025 21:09:21 -0700 Subject: [PATCH] [1/N] Add BackendOptions class Introduce backend option as discussed in https://github.com/pytorch/executorch/discussions/10216 Step 1: Introducd Backend Option class In later stage, it will be plugged in with the rest of the stack. Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712/) [ghstack-poisoned] --- runtime/backend/backend_options.h | 177 ++++++++++++++++++ runtime/backend/targets.bzl | 1 + runtime/backend/test/backend_options_test.cpp | 130 +++++++++++++ runtime/backend/test/targets.bzl | 11 +- 4 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 runtime/backend/backend_options.h create mode 100644 runtime/backend/test/backend_options_test.cpp diff --git a/runtime/backend/backend_options.h b/runtime/backend/backend_options.h new file mode 100644 index 00000000000..682c441e3af --- /dev/null +++ b/runtime/backend/backend_options.h @@ -0,0 +1,177 @@ +/* + * 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 + +namespace executorch { +namespace runtime { + +// Strongly-typed option key template +// Wraps a string key with type information for type-safe option access +template +struct OptionKey { + const char* key; // String identifier for the option + constexpr explicit OptionKey(const char* k) : key(k) {} +}; + +// Supported option data types +enum class OptionType { BOOL, INT, STRING }; + +// Union-like container for option values (only one member is valid per option) +struct OptionValue { + bool bool_value; // Storage for boolean values + int int_value; // Storage for integer values + const char* string_value; // Storage for string values +}; + +// Represents a single backend configuration option +struct BackendOption { + const char* key; // Name of the option + OptionType type; // Data type of the option + OptionValue value; // Current value of the option +}; + +// Fixed-capacity container for backend options with type-safe access +// MaxCapacity: Maximum number of options this container can hold +template +class BackendOptions { + public: + // Initialize with zero options + BackendOptions() : size(0) {} + + // Type-safe setters --------------------------------------------------- + + /// Sets or updates a boolean option + /// @param key: Typed option key + /// @param value: Boolean value to set + void set_option(OptionKey key, bool value) { + set_option_internal(key.key, OptionType::BOOL, {.bool_value = value}); + } + + /// Sets or updates an integer option + /// @param key: Typed option key + /// @param value: Integer value to set + void set_option(OptionKey key, int value) { + set_option_internal(key.key, OptionType::INT, {.int_value = value}); + } + + /// Sets or updates a string option + /// @param key: Typed option key + /// @param value: Null-terminated string value to set + void set_option(OptionKey key, const char* value) { + set_option_internal(key.key, OptionType::STRING, {.string_value = value}); + } + + // Type-safe getters --------------------------------------------------- + + /// Retrieves a boolean option value + /// @param key: Typed option key + /// @param out_value: Reference to store retrieved value + /// @return: Error code (Ok on success) + executorch::runtime::Error get_option(OptionKey key, bool& out_value) + const { + OptionValue val{}; + executorch::runtime::Error err = + get_option_internal(key.key, OptionType::BOOL, val); + if (err == executorch::runtime::Error::Ok) { + out_value = val.bool_value; + } + return err; + } + + /// Retrieves an integer option value + /// @param key: Typed option key + /// @param out_value: Reference to store retrieved value + /// @return: Error code (Ok on success) + executorch::runtime::Error get_option(OptionKey key, int& out_value) + const { + OptionValue val{}; + executorch::runtime::Error err = + get_option_internal(key.key, OptionType::INT, val); + if (err == executorch::runtime::Error::Ok) { + out_value = val.int_value; + } + return err; + } + + /// Retrieves a string option value + /// @param key: Typed option key + /// @param out_value: Reference to store retrieved pointer + /// @return: Error code (Ok on success) + executorch::runtime::Error get_option( + OptionKey key, + const char*& out_value) const { + OptionValue val{}; + executorch::runtime::Error err = + get_option_internal(key.key, OptionType::STRING, val); + if (err == executorch::runtime::Error::Ok) { + out_value = val.string_value; + } + return err; + } + + private: + BackendOption options[MaxCapacity]{}; // Storage for options + size_t size; // Current number of options + + // Internal helper to set/update an option + void + set_option_internal(const char* key, OptionType type, OptionValue value) { + // Update existing key if found + for (size_t i = 0; i < size; ++i) { + if (strcmp(options[i].key, key) == 0) { + options[i].type = type; + options[i].value = value; + return; + } + } + // Add new option if capacity allows + if (size < MaxCapacity) { + options[size++] = {key, type, value}; + } + } + + // Internal helper to get an option value with type checking + executorch::runtime::Error get_option_internal( + const char* key, + OptionType expected_type, + OptionValue& out) const { + for (size_t i = 0; i < size; ++i) { + if (strcmp(options[i].key, key) == 0) { + // Verify type matches expectation + if (options[i].type != expected_type) { + return executorch::runtime::Error::InvalidArgument; + } + out = options[i].value; + return executorch::runtime::Error::Ok; + } + } + return executorch::runtime::Error::NotFound; // Key not found + } +}; + +// Helper functions for creating typed option keys -------------------------- + +/// Creates a boolean option key +constexpr OptionKey BoolKey(const char* k) { + return OptionKey(k); +} + +/// Creates an integer option key +constexpr OptionKey IntKey(const char* k) { + return OptionKey(k); +} + +/// Creates a string option key +constexpr OptionKey StrKey(const char* k) { + return OptionKey(k); +} +} // namespace runtime +} // namespace executorch diff --git a/runtime/backend/targets.bzl b/runtime/backend/targets.bzl index d2187afb5fc..49a14d4d0d6 100644 --- a/runtime/backend/targets.bzl +++ b/runtime/backend/targets.bzl @@ -17,6 +17,7 @@ def define_common_targets(): exported_headers = [ "backend_execution_context.h", "backend_init_context.h", + "backend_options.h", "interface.h", ], preprocessor_flags = ["-DUSE_ATEN_LIB"] if aten_mode else [], diff --git a/runtime/backend/test/backend_options_test.cpp b/runtime/backend/test/backend_options_test.cpp new file mode 100644 index 00000000000..1ada86d4690 --- /dev/null +++ b/runtime/backend/test/backend_options_test.cpp @@ -0,0 +1,130 @@ +/* + * 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 + +using namespace ::testing; +using executorch::runtime::BackendOptions; +using executorch::runtime::BoolKey; +using executorch::runtime::IntKey; +using executorch::runtime::OptionKey; +using executorch::runtime::StrKey; +using executorch::runtime::Error; + +class BackendOptionsTest : public ::testing::Test { + protected: + void SetUp() override { + // Since these tests cause ET_LOG to be called, the PAL must be initialized + // first. + executorch::runtime::runtime_init(); + } + BackendOptions<5> options; // Capacity of 5 for testing limits +}; + +// Test basic string functionality +TEST_F(BackendOptionsTest, HandlesStringOptions) { + // Set and retrieve valid string + options.set_option(StrKey("backend_type"), "GPU"); + const char* result = nullptr; + EXPECT_EQ(options.get_option(StrKey("backend_type"), result), Error::Ok); + EXPECT_STREQ(result, "GPU"); + + // Update existing key + options.set_option(StrKey("backend_type"), "CPU"); + EXPECT_EQ(options.get_option(StrKey("backend_type"), result), Error::Ok); + EXPECT_STREQ(result, "CPU"); +} + +// Test boolean options +TEST_F(BackendOptionsTest, HandlesBoolOptions) { + options.set_option(BoolKey("debug"), true); + bool debug = false; + EXPECT_EQ(options.get_option(BoolKey("debug"), debug), Error::Ok); + EXPECT_TRUE(debug); + + // Test false value + options.set_option(BoolKey("verbose"), false); + EXPECT_EQ(options.get_option(BoolKey("verbose"), debug), Error::Ok); + EXPECT_FALSE(debug); +} + +// Test integer options +TEST_F(BackendOptionsTest, HandlesIntOptions) { + options.set_option(IntKey("num_threads"), 256); + int size = 0; + EXPECT_EQ(options.get_option(IntKey("num_threads"), size), Error::Ok); + EXPECT_EQ(size, 256); +} + +// Test error conditions +TEST_F(BackendOptionsTest, HandlesErrors) { + // Non-existent key + bool dummy_bool; + EXPECT_EQ( + options.get_option(BoolKey("missing"), dummy_bool), Error::NotFound); + + // Type mismatch + options.set_option(IntKey("threshold"), 100); + const char* dummy_str = nullptr; + EXPECT_EQ( + options.get_option(StrKey("threshold"), dummy_str), + Error::InvalidArgument); + + // Null value handling + options.set_option(StrKey("nullable"), nullptr); + EXPECT_EQ(options.get_option(StrKey("nullable"), dummy_str), Error::Ok); + EXPECT_EQ(dummy_str, nullptr); +} + +// Test capacity limits +TEST_F(BackendOptionsTest, HandlesCapacity) { + // Use persistent storage for keys + std::vector keys = {"key0", "key1", "key2", "key3", "key4"}; + + // Fill to capacity with persistent keys + for (int i = 0; i < 5; i++) { + options.set_option(IntKey(keys[i].c_str()), i); + } + + // Verify all exist + int value; + for (int i = 0; i < 5; i++) { + EXPECT_EQ(options.get_option(IntKey(keys[i].c_str()), value), Error::Ok); + EXPECT_EQ(value, i); + } + + // Add beyond capacity - should fail + const char* overflow_key = "overflow"; + options.set_option(IntKey(overflow_key), 99); + EXPECT_EQ(options.get_option(IntKey(overflow_key), value), Error::NotFound); + + // Update existing within capacity + options.set_option(IntKey(keys[2].c_str()), 222); + EXPECT_EQ(options.get_option(IntKey(keys[2].c_str()), value), Error::Ok); + EXPECT_EQ(value, 222); +} + +// Test type-specific keys +TEST_F(BackendOptionsTest, EnforcesKeyTypes) { + // Same key name - later set operations overwrite earlier ones + options.set_option(BoolKey("flag"), true); + options.set_option(IntKey("flag"), 123); // Overwrites the boolean entry + + bool bval; + int ival; + + // Boolean get should fail - type was overwritten to INT + EXPECT_EQ(options.get_option(BoolKey("flag"), bval), Error::InvalidArgument); + + // Integer get should succeed with correct value + EXPECT_EQ(options.get_option(IntKey("flag"), ival), Error::Ok); + EXPECT_EQ(ival, 123); +} diff --git a/runtime/backend/test/targets.bzl b/runtime/backend/test/targets.bzl index 9ea585f650c..97299bbcb35 100644 --- a/runtime/backend/test/targets.bzl +++ b/runtime/backend/test/targets.bzl @@ -1,7 +1,16 @@ +load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime") + def define_common_targets(): """Defines targets that should be shared between fbcode and xplat. The directory containing this targets.bzl file should also contain both TARGETS and BUCK files that call this function. """ - pass + runtime.cxx_test( + name = "backend_options_test", + srcs = ["backend_options_test.cpp"], + deps = [ + "//executorch/runtime/core:core", + "//executorch/runtime/backend:interface", + ], + )