Skip to content

[1/N] Add BackendOptions class #11389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: gh/cccclai/21/base
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 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
217 changes: 217 additions & 0 deletions runtime/backend/options.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* 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 <executorch/runtime/core/error.h>
#include <executorch/runtime/core/span.h>
#include <array>
#include <cstddef>
#include <cstring>
#include <variant>

namespace executorch {
namespace runtime {

static constexpr size_t kMaxOptionKeyLength = 64;
static constexpr size_t kMaxOptionValueLength = 256;

// All string keys and values must have static storage duration (string
// literals, static const char arrays, or global constants). The BackendOptions
// class does NOT take ownership of strings.
using OptionValue =
std::variant<bool, int, std::array<char, kMaxOptionValueLength>>;

struct BackendOption {
// key is the name of the backend option, like num_threads, enable_profiling,
// etc
char key[kMaxOptionKeyLength];
// value is the value of the backend option, like 4, true, etc
OptionValue value;
};

/**
* A template class for storing and managing backend-specific configuration
* options.
*
* This class provides a type-safe way to store key-value pairs for backend
* configuration, with compile-time capacity limits and runtime type checking.
* It supports bool, int, and const char* value types.
*
* @tparam MaxCapacity The maximum number of options that can be stored
*/
template <size_t MaxCapacity>
class BackendOptions {
public:
/**
* Copy constructor
*/
BackendOptions(const BackendOptions& other) : size_(other.size_) {
for (size_t i = 0; i < size_; ++i) {
options_[i] = other.options_[i];
}
}

/**
* Copy assignment operator
*/
BackendOptions& operator=(const BackendOptions& other) {
if (this != &other) {
size_ = other.size_;
for (size_t i = 0; i < size_; ++i) {
options_[i] = other.options_[i];
}
}
return *this;
}

/**
* Default constructor - initializes with zero options.
*/
BackendOptions() : size_(0) {}

/**
* Returns a const view of all stored options as a Span.
*
* @return A const Span containing all BackendOption entries
*/
executorch::runtime::Span<const BackendOption> view() const {
return executorch::runtime::Span<const BackendOption>(options_, size_);
}

/**
* Returns a mutable view of all stored options as a Span.
*
* @return A mutable Span containing all BackendOption entries
*/
executorch::runtime::Span<BackendOption> mutable_view() {
return executorch::runtime::Span<BackendOption>(options_, size_);
}

/**
* Sets a boolean option value for the given key.
* If the key already exists, updates its value. Otherwise, adds a new option.
*
* @tparam N The length of the key string (automatically deduced)
* @param key The option key (must be a string literal or array)
* @param value The boolean value to set
* @return Error::Ok on success, Error::InvalidArgument if storage is full
*/
template <size_t N>
Error set_option(const char (&key)[N], bool value) noexcept {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need N? It will just create N different methods for many possible lengths. My suggestion is to remove N as template param and expect callsites to be something like

char my_option_name[kMaxOptionKeyLength] = "use_gpu";
...set_option(my_option_name, True)

static_assert(N <= kMaxOptionKeyLength, "Option key is too long");
return set_option_impl(key, value);
}

/**
* Sets an integer option value for the given key.
* If the key already exists, updates its value. Otherwise, adds a new option.
*
* @tparam N The length of the key string (automatically deduced)
* @param key The option key (must be a string literal or array)
* @param value The integer value to set
* @return Error::Ok on success, Error::InvalidArgument if storage is full
*/
template <size_t N>
Error set_option(const char (&key)[N], int value) noexcept {
static_assert(N <= kMaxOptionKeyLength, "Option key is too long");
return set_option_impl(key, value);
}

/**
* Sets a string option value for the given key.
* If the key already exists, updates its value. Otherwise, adds a new option.
*
* Note: The string value must have static storage duration. This class does
* NOT take ownership of the string - it only stores the pointer.
*
* @tparam N The length of the key string (automatically deduced)
* @param key The option key (must be a string literal or array)
* @param value The string value to set (must have static storage duration)
* @return Error::Ok on success, Error::InvalidArgument if storage is full
*/
template <size_t N>
Error set_option(const char (&key)[N], const char* value) noexcept {
static_assert(N <= kMaxOptionKeyLength, "Option key is too long");
// Create a fixed-size array and copy the string
std::array<char, kMaxOptionValueLength> arr;
strncpy(arr.data(), value, kMaxOptionValueLength - 1);
arr[kMaxOptionValueLength - 1] = '\0'; // Ensure null termination
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the suggestion I mentioned above will also not require you to do this. ALthough its not a bit deal

return set_option_impl(key, arr);
}
/**
* Retrieves an option value by key and type.
*
* @tparam T The expected type of the option value (bool, int, or const char*)
* @tparam KeyLen The length of the key string (automatically deduced)
* @param key The option key to look up
* @param out Reference to store the retrieved value
* @return Error::Ok if found and type matches, Error::NotFound if key doesn't
* exist, Error::InvalidArgument if type doesn't match
*/
template <typename T, size_t KeyLen>
Error get_option(const char (&key)[KeyLen], T& out) const {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as before on keylen

static_assert(KeyLen <= kMaxOptionKeyLength, "Option key is too long");
for (size_t i = 0; i < size_; ++i) {
if (std::strcmp(options_[i].key, key) == 0) {
// Special handling for string (convert array to const char*)
if constexpr (std::is_same_v<T, const char*>) {
if (auto* arr = std::get_if<std::array<char, kMaxOptionValueLength>>(
&options_[i].value)) {
out = arr->data(); // Return pointer to stored array
return Error::Ok;
}
}
// Default handling for bool/int
else if (auto* val = std::get_if<T>(&options_[i].value)) {
out = *val;
return Error::Ok;
}
return Error::InvalidArgument;
}
}
return Error::NotFound;
}

private:
BackendOption options_[MaxCapacity]{}; // Storage for backend options
size_t size_; // Current number of options

/**
* Internal implementation for setting option values.
* Handles both updating existing options and adding new ones.
*
* @tparam T The type of the value (bool, int, or const char*)
* @param key The option key
* @param value The value to set
* @return Error::Ok on success, Error::InvalidArgument if storage is full
*/
template <typename T>
Error set_option_impl(const char* key, T value) {
// Update existing if found
for (size_t i = 0; i < size_; ++i) {
if (strcmp(options_[i].key, key) == 0) {
options_[i].value = value;
return Error::Ok;
}
}
if (size_ < MaxCapacity) {
BackendOption new_option;
const size_t key_len = std::strlen(key);
const size_t copy_len = std::min(key_len, kMaxOptionKeyLength - 1);
std::memcpy(new_option.key, key, copy_len);
new_option.key[copy_len] = '\0';
new_option.value = value; // Restored value assignment
options_[size_++] = new_option; // Store option and increment size
return Error::Ok;
}
return Error::InvalidArgument;
}
};

} // namespace runtime
} // namespace executorch
1 change: 1 addition & 0 deletions runtime/backend/targets.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def define_common_targets():
exported_headers = [
"backend_execution_context.h",
"backend_init_context.h",
"options.h",
"interface.h",
],
preprocessor_flags = ["-DUSE_ATEN_LIB"] if aten_mode else [],
Expand Down
164 changes: 164 additions & 0 deletions runtime/backend/test/backend_options_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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/runtime/backend/options.h>
#include <executorch/runtime/platform/runtime.h>
#include <executorch/test/utils/DeathTest.h>

#include <gtest/gtest.h>

using namespace ::testing;
using executorch::runtime::BackendOptions;
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("backend_type", "GPU");
const char* result = nullptr;
EXPECT_EQ(options.get_option("backend_type", result), Error::Ok);
EXPECT_STREQ(result, "GPU");

// Update existing key
options.set_option("backend_type", "CPU");
EXPECT_EQ(options.get_option("backend_type", result), Error::Ok);
EXPECT_STREQ(result, "CPU");
}

// Test boolean options
TEST_F(BackendOptionsTest, HandlesBoolOptions) {
options.set_option("debug", true);
bool debug = false;
EXPECT_EQ(options.get_option("debug", debug), Error::Ok);
EXPECT_TRUE(debug);

// Test false value
options.set_option("verbose", false);
EXPECT_EQ(options.get_option("verbose", debug), Error::Ok);
EXPECT_FALSE(debug);
}

// Test integer options
TEST_F(BackendOptionsTest, HandlesIntOptions) {
options.set_option("num_threads", 256);
int num_threads = 0;
EXPECT_EQ(options.get_option("num_threads", num_threads), Error::Ok);
EXPECT_EQ(num_threads, 256);
}

// Test error conditions
TEST_F(BackendOptionsTest, HandlesErrors) {
// Non-existent key
bool dummy_bool;
EXPECT_EQ(options.get_option("missing", dummy_bool), Error::NotFound);

// Type mismatch
options.set_option("threshold", 100);
const char* dummy_str = nullptr;
EXPECT_EQ(options.get_option("threshold", dummy_str), Error::InvalidArgument);

// Null value handling, should expect failure
ET_EXPECT_DEATH(
options.set_option("nullable", static_cast<const char*>(nullptr)), "");
}

// Test type-specific keys
TEST_F(BackendOptionsTest, EnforcesKeyTypes) {
// Same key name - later set operations overwrite earlier ones
options.set_option("flag", true);
options.set_option("flag", 123); // Overwrites the boolean entry

bool bval;
int ival;

// Boolean get should fail - type was overwritten to INT
EXPECT_EQ(options.get_option("flag", bval), Error::InvalidArgument);

// Integer get should succeed with correct value
EXPECT_EQ(options.get_option("flag", ival), Error::Ok);
EXPECT_EQ(ival, 123);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests for mutability of the options now that you have mutable view

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added


TEST_F(BackendOptionsTest, MutableOption) {
int ival;
options.set_option("flag", 0);
// Integer get should succeed with correct value
EXPECT_EQ(options.get_option("flag", ival), Error::Ok);
EXPECT_EQ(ival, 0);

options.mutable_view()[0].value = 123; // Overwrites the entry

// Integer get should succeed with the updated value
EXPECT_EQ(options.get_option("flag", ival), Error::Ok);
EXPECT_EQ(ival, 123);
}

// Test copy constructor
TEST_F(BackendOptionsTest, CopyConstructor) {
// Set up original option
options.set_option("debug", true);

// Create copy using copy constructor
BackendOptions<5> copied_options(options);

// Verify option was copied correctly
bool debug_val;
EXPECT_EQ(copied_options.get_option("debug", debug_val), Error::Ok);
EXPECT_TRUE(debug_val);

// Verify independence - modifying original doesn't affect copy
options.set_option("debug", false);
EXPECT_EQ(copied_options.get_option("debug", debug_val), Error::Ok);
EXPECT_TRUE(debug_val); // Should still be true in copy

// Verify independence - modifying copy doesn't affect original
copied_options.set_option("debug", false);
EXPECT_EQ(options.get_option("debug", debug_val), Error::Ok);
EXPECT_FALSE(debug_val); // Should be false in original
}

// Test copy assignment operator
TEST_F(BackendOptionsTest, CopyAssignmentOperator) {
// Set up original option
options.set_option("enable_profiling", true);

// Create another options object and assign to it
BackendOptions<5> assigned_options;
assigned_options.set_option("temp_option", false); // Add something first

assigned_options = options;

// Verify option was copied correctly
bool profiling_val;
EXPECT_EQ(
assigned_options.get_option("enable_profiling", profiling_val),
Error::Ok);
EXPECT_TRUE(profiling_val);

// Verify the temp_option was overwritten (not present in assigned object)
bool temp_val;
EXPECT_EQ(
assigned_options.get_option("temp_option", temp_val), Error::NotFound);

// Verify independence - modifying original doesn't affect assigned copy
options.set_option("enable_profiling", false);
EXPECT_EQ(
assigned_options.get_option("enable_profiling", profiling_val),
Error::Ok);
EXPECT_TRUE(profiling_val); // Should still be true in assigned copy
}
Loading
Loading