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 10 commits into
base: gh/cccclai/21/base
Choose a base branch
from
Open

Conversation

cccclai
Copy link
Contributor

@cccclai cccclai commented Jun 5, 2025

Stack from ghstack (oldest at bottom):

Introduce backend option as discussed in #10216

Step 1: Introducd Backend Option class

In later stage, it will be plugged in with the rest of the stack. BackendOptions is pretty much a list of BackendOption, and backend option is a key value pair. The key is a string, and the value can be 3 different types, including bool, string and int.

Differential Revision: D75993712

Differential Revision: D75993712

Introduce backend option as discussed in #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]
Copy link

pytorch-bot bot commented Jun 5, 2025

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/11389

Note: Links to docs will display an error until the docs builds have been completed.

✅ You can merge normally! (3 Unrelated Failures)

As of commit 943fe96 with merge base 608a745 (image):

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

cccclai added a commit that referenced this pull request Jun 5, 2025
Introduce backend option as discussed in #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-source-id: 288273758
Pull Request resolved: #11389
@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 5, 2025
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Copy link

github-actions bot commented Jun 5, 2025

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@cccclai
Copy link
Contributor Author

cccclai commented Jun 5, 2025

Previous comment in the old PR: #11288

From @JacobSzwejbka :

use int64_t for int type.

@cccclai
Copy link
Contributor Author

cccclai commented Jun 5, 2025

From @mergennachin:

I don't think we need to define a generic BackendOption that takes arbitrary key value. Each backend owners could define their options instead. Vulkan will define its own config structure, Core ML will define its own config structure. Wdyt?

@JacobSzwejbka, @mcr229, @kimishpatel

Something like this:

BackendInterface { // parent interface
  virtual Error setOption(const BackendOptions& options) = 0;
}

struct BackendOptions {  // Empty parent struct
  virtual ~BackendOptions() = default;
};

Each individual backends define their own configs

struct VulkanBackendOptions : public BackendOptions {
  const char* metadata[20] = "";
  int thread_count = 1;
  // ... other Vulkan-specific options
};

VulkanBackend extends BackendInterface {
  Error setOption(const BackendOptions& options) override {
    auto* vulkan_opts = dynamic_cast<const VulkanBackendOptions*>(&options);
  }
}

// User code

VulkanBackendOptions opts;
opts.thread_count = 5;
auto backend = std::make_unique<VulkanBackend>();
backend->setOption(opts);  // 

@cccclai
Copy link
Contributor Author

cccclai commented Jun 5, 2025

My response:

I thought about this idea at the beginning, but it means that users code needs to link to a specific backend, otherwise the runner code will fail to build. Currently the same user code can be backend agnostic, meaning the same code can still run with different backends with/without linking the backend. As the example, the portable runner can work with/without xnnpack https://github.com/pytorch/executorch/blob/main/examples/xnnpack/executor_runner/targets.bzl.

It also exposes the backend specific symbol to users, and the contract interface is a bit looser. Previously, The contract is like backend <-> ET <-> backend, and if we change it to this, it can be user <-> backend. And users can pass arbitrary things to backend. Users can run backend->init, backend->execute in their own code too, and it will be out of our control.

With the proposed solution (#10216), the user code can still be backend agnostic, and the backend option is still optional. We are still on the hook of the contract.

@kimishpatel
Copy link
Contributor

I think what @mergennachin suggested makes sense. If user code is setting backend specific option they already have to know what backend they are talking about, unless some options are backend agnostic.

Other piece of the puzzle is how the user will update this. And from #10216 it seems that we will pipe update (I would call set_backend_options) method that will set all the options. But part that is not clear to me is how would user indicate which backend they setting this option for. So somehow we need a string identifer for the backends that can be used to figure out which backend's update method be called. @mergennachin in this case we cannot quite have what you proposed. Imagine you have a pointer to an instance of VulkanBackendOptions. You pass this to runtime's update method which accepts BackendOptions*. Eventually you want to call VulkanBackend's update options method so you need to cast BackendOptions* to VulkanBackendOptions*. You have two choices either do static_cast or dynamic_cast. The later requires rtti which we dont allow. For static_cast, it has to be safe and you need to know that BackendOption* is actually pointing to VulkanBackendOption. To do that you need to have BackendOptions store tag containing what is the type of the derived class. If we do that, then what you suggest can work.

Introduce backend option as discussed in #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]
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Introduce backend option as discussed in #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]
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Introduce backend option as discussed in #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]
Introduce backend option as discussed in #10216

Step 1: Introducd Backend Option class

In later stage, it will be plugged in with the rest of the stack. BackendOptions is pretty much a list of BackendOption, and backend option is a key value pair. The key is a string, and the value can be 3 different types, including bool, string and int.


Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712/)

Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712)

[ghstack-poisoned]
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Introduce backend option as discussed in #10216

Step 1: Introducd Backend Option class

In later stage, it will be plugged in with the rest of the stack. BackendOptions is pretty much a list of BackendOption, and backend option is a key value pair. The key is a string, and the value can be 3 different types, including bool, string and int.


Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712/)

Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712)

[ghstack-poisoned]
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Introduce backend option as discussed in #10216

Step 1: Introducd Backend Option class

In later stage, it will be plugged in with the rest of the stack. BackendOptions is pretty much a list of BackendOption, and backend option is a key value pair. The key is a string, and the value can be 3 different types, including bool, string and int.


Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712/)

Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712)

[ghstack-poisoned]
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Copy link
Contributor

@mergennachin mergennachin left a comment

Choose a reason for hiding this comment

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

See inline


// Union replaced with std::variant
using OptionValue = std::variant<bool, int, const char*>;

Copy link
Contributor

Choose a reason for hiding this comment

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

Document the safety and lifecycle of string types

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the actual value itself, I think we should do this where std::variant itself holds array of chars rather than pointer to char. Like this https://godbolt.org/z/odTc9ex96

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated

Comment on lines 98 to 100
constexpr OptionKey<const char*> StrKey(const char* k) {
return OptionKey<const char*>(k);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you force to accept only string literals with this?

template <size_t N>
constexpr OptionKey<const char*> StrKey(const char (&literal)[N]) {
  return OptionKey<const char*>(literal);
}


// Type-safe setters
template <typename T>
void set_option(OptionKey<T> key, T value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

rename this to set_option_impl and make it a private function. And add type safe public APIs to enforce key is always safe string... And value is also safe in the case of string.

  // String options - only accepts string literals/arrays
  template <size_t KeyN, size_t ValueN>
  Error set_option(
    const char (&key)[KeyN], 
    const char (&value)[ValueN]
  ) noexcept {
    return set_option_impl(key, static_cast<const char*>(value));
  }
  
  // Non-string options
  template <size_t N>
  Error set_option(const char (&key)[N], bool value) noexcept {
    return set_option_impl(key, value);
  }
  
  template <size_t N>
  Error set_option(const char (&key)[N], int value) noexcept {
    return set_option_impl(key, value);
  }


// Strongly-typed option key template
template <typename T>
struct OptionKey {
Copy link
Contributor

Choose a reason for hiding this comment

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

I dont think we need OptionKey as a separate class. Just use const char * in the BackendOption class.

However, the thing that worries me the most is by owning const char* we are giving users a footgun. They can allocate pointer to the backend option name on stack, pass the pointer around and it will go out-of-scope. If this results in segfault that might be your best option. Worst can lead to undefined behaviors.

I would suggest that we just store const char[kMaxLegthOfBackendOptionName].

Copy link
Contributor

Choose a reason for hiding this comment

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

You resolved this comment but I dont see it being addressed

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like you addressed but kept OptionKey. please remove

Introduce backend option as discussed in #10216

Step 1: Introducd Backend Option class

In later stage, it will be plugged in with the rest of the stack. BackendOptions is pretty much a list of BackendOption, and backend option is a key value pair. The key is a string, and the value can be 3 different types, including bool, string and int.


Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712/)

Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712)

[ghstack-poisoned]
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Introduce backend option as discussed in #10216

Step 1: Introducd Backend Option class

In later stage, it will be plugged in with the rest of the stack. BackendOptions is pretty much a list of BackendOption, and backend option is a key value pair. The key is a string, and the value can be 3 different types, including bool, string and int.


Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712/)

Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712)

[ghstack-poisoned]
@cccclai
Copy link
Contributor Author

cccclai commented Jun 23, 2025

@kimishpatel @mergennachin thanks for the feedback! I've addressed most of them. If there is anything I miss, let me know

@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

Copy link
Contributor

@mergennachin mergennachin left a comment

Choose a reason for hiding this comment

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

See inline, approving but inline comments should be addressed

*/
template <size_t N>
Error set_option(const char (&key)[N], bool value) noexcept {
ET_CHECK_MSG(N <= kMaxOptionKeyLength, "Option key is too long");
Copy link
Contributor

Choose a reason for hiding this comment

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

static_assert(N <= kMaxOptionKeyLength, "Option key is too long");

for compile time check, as opposed to runtime check

Copy link
Contributor

Choose a reason for hiding this comment

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

why is there KeyLen here? Can it not be just max length? Like why are we templating this on keylength? If it is to allow for users to construct const char[10] vs. const char [5], I would say just make recommended use via const char[kMaxOptionKeyLength]? Feels like that is simler

*/
template <size_t N>
Error set_option(const char (&key)[N], int value) noexcept {
ET_CHECK_MSG(N <= kMaxOptionKeyLength, "Option key is too long");
Copy link
Contributor

Choose a reason for hiding this comment

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

.

*/
template <size_t N>
Error set_option(const char (&key)[N], const char* value) noexcept {
ET_CHECK_MSG(N <= kMaxOptionKeyLength, "Option key is too long");
Copy link
Contributor

Choose a reason for hiding this comment

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

.

Comment on lines 176 to 177
strncpy(new_option.key, key, kMaxOptionKeyLength - 1);
new_option.key[kMaxOptionKeyLength - 1] = '\0';
Copy link
Contributor

Choose a reason for hiding this comment

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

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';

*
* @return A const Span containing all BackendOption entries
*/
executorch::runtime::Span<BackendOption> view() const {
Copy link
Contributor

Choose a reason for hiding this comment

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

executorch::runtime::Span<const BackendOption> view() const

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Potentially have this

  /**
   * 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;
  }

@kimishpatel
Copy link
Contributor

@mergennachin I am concerned about lifecycle and potential footgun it introduces when setting string values. So I would like to resolve that before we land

@mergennachin
Copy link
Contributor

mergennachin commented Jun 23, 2025

@kimishpatel

Can we just always require string value to be a string literals, static const char arrays, or global constants in the API like this?

#11389 (comment)

*
* @return A mutable Span containing all BackendOption entries
*/
executorch::runtime::Span<BackendOption> view() {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe make it explicit that this is a mutable view.

* 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

// 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

Copy link
Contributor

@kimishpatel kimishpatel left a comment

Choose a reason for hiding this comment

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

I think we should make option itself have char array instead of char*

@kimishpatel
Copy link
Contributor

@kimishpatel

Can we just always require string value to be a string literals, static const char arrays, or global constants in the API like this?

#11389 (comment)

Yeah those are fine options. I just want API to make it clear, like what you suggested in the comment (although I think length specifier, for option and value, being template param seems a bit weird. Just have a max longest length as some const)

Introduce backend option as discussed in #10216

Step 1: Introducd Backend Option class

In later stage, it will be plugged in with the rest of the stack. BackendOptions is pretty much a list of BackendOption, and backend option is a key value pair. The key is a string, and the value can be 3 different types, including bool, string and int.


Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712/)

Differential Revision: [D75993712](https://our.internmc.facebook.com/intern/diff/D75993712)

[ghstack-poisoned]
@facebook-github-bot
Copy link
Contributor

This pull request was exported from Phabricator. Differential Revision: D75993712

@cccclai
Copy link
Contributor Author

cccclai commented Jun 23, 2025

New comments are addressed, let me know your thoughts

* @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)

// 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

Copy link
Contributor

@kimishpatel kimishpatel left a comment

Choose a reason for hiding this comment

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

Looks good. I still think we should not need N or KeyLen template params. So I would suggest to fix that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ciflow/trunk CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. fb-exported
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants