-
Notifications
You must be signed in to change notification settings - Fork 78
Embedded Rust API RFC #96
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,333 @@ | ||
| - Feature Name: embedded_rust_interface | ||
| - Start Date: 2022-10-04 | ||
| - RFC PR: [apache/tvm-rfcs#0000](https://github.com/apache/tvm-rfcs/pull/96) | ||
| - GitHub Issue: [apache/tvm#0000](https://github.com/apache/tvm/issues/0000) | ||
|
|
||
| # Summary | ||
| [summary]: #summary | ||
|
|
||
| This RFC outlines a set of additional APIs for the C Runtime to enable direct calling of an [AOT micro entrypoint](https://discuss.tvm.apache.org/t/rfc-utvm-aot-optimisations-for-embedded-targets/9849) using Embedded Rust, aiming to provide parity with the [Embedded C APIs](https://discuss.tvm.apache.org/t/rfc-utvm-embedded-c-runtime-interface/9951). | ||
|
|
||
| # Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| Embedded Rust is an emerging field with a eco-system based around a standard [embedded hardware abstraction layer](https://github.com/rust-embedded/embedded-hal) and Rust's inherent memory safety. In order to run ML models on Embedded Devices written in Rust using TVM, we wanted to build an interface which could be used with pure Rust and without `unsafe` code wherever possible. It is believed this interface moves TVM to the forefront of embedded development by embracing this new technology. | ||
|
|
||
| # Guide-level explanation | ||
| [guide-level-explanation]: #guide-level-explanation | ||
|
|
||
| As much as possible, we aim to provide an idiomatic and `safe` Rust experience for users, this is possible for: | ||
| * Running a simple model | ||
| * Running a model with workspace pools | ||
| * Running a model with constant pools | ||
| * Running a model with I/O pools | ||
| * Using Rust drivers with the Device API | ||
|
|
||
| ## Running a model | ||
| Users will be able to import a generated crate from within the [Model Library Format archive](https://discuss.tvm.apache.org/t/rfc-tvm-model-library-format/9121) which includes the dependencies that are required for running that model, this can be added to a user application in `Cargo.toml`: | ||
|
|
||
| ```rust | ||
| [dependencies] | ||
| tvmgen_ultimate_cat_spotter = { path = "./tvm_archive/crates/ultimate_cat_spotter" } | ||
| ``` | ||
|
|
||
| The generated crate provides types for the Model and Workspace implementing any necessary TVM Runtime traits which allow | ||
| to write applications generic over the model used: | ||
|
|
||
| ```rust | ||
| mod my_app_logic; | ||
| extern crate tvmgen_ultimate_cat_spotter as ultimate_cat_spotter; | ||
|
|
||
| fn main() { | ||
| let mut input_data: [i8; 25600] = my_app_logic::create_input(); | ||
| let mut output_data: [f32; 12] = my_app_logic::create_output(); | ||
|
|
||
| let mut workspace = ultimate_cat_spotter::Workspace::new( | ||
| &mut input_data, | ||
| &mut output_data, | ||
| ); | ||
|
|
||
| let mut model = ultimate_cat_spotter::Model::new(); | ||
| model.infer(&mut workspace, TVMDevice::CPU); | ||
|
|
||
| assert_eq!(output_data, my_app_logic::expected_output()); | ||
| } | ||
| ``` | ||
|
|
||
| ## Running a model with workspace pools | ||
| This extends the above premise and provides an additional memory pool argument: | ||
|
|
||
| ```rust | ||
| mod my_app_logic; | ||
| extern crate tvmgen_ultimate_cat_spotter as ultimate_cat_spotter; | ||
|
|
||
| fn main() { | ||
| let mut input_data: [i8; 25600] = my_app_logic::create_input(); | ||
| let mut output_data: [f32; 12] = my_app_logic::create_output(); | ||
| let mut memory_pool: [u8; 20000] = my_app_logic::create_memory_pool(); | ||
|
|
||
| let mut workspace = ultimate_cat_spotter::Workspace::new( | ||
| &mut input_data, | ||
| &mut output_data, | ||
| &mut memory_pool, | ||
| ); | ||
|
|
||
| let mut model = ultimate_cat_spotter::Model::new(); | ||
| model.infer(&mut workspace, TVMDevice::CPU); | ||
| assert_eq!(output_data, my_app_logic::expected_output()); | ||
| } | ||
| ``` | ||
|
|
||
| ## Running a model using constant pools | ||
| By utilising macros, users can generate the appropriate constant pools at compilation time: | ||
|
|
||
| ```rust | ||
| mod my_app_logic; | ||
| extern crate tvmgen_ultimate_cat_spotter as ultimate_cat_spotter; | ||
|
|
||
| fn main() { | ||
| let mut input_data: [i8; 25600] = my_app_logic::create_input(); | ||
| let mut output_data: [f32; 12] = my_app_logic::create_output(); | ||
| let mut memory_pool: [u8; 20000] = my_app_logic::create_memory_pool(tvmgen_default::constant_pool_data!()); | ||
|
|
||
| let mut workspace = ultimate_cat_spotter::Workspace::new( | ||
| &mut input_data, | ||
| &mut output_data, | ||
| &mut memory_pool, | ||
| ); | ||
|
|
||
| let mut model = ultimate_cat_spotter::Model::new(); | ||
| model.infer(&mut workspace, TVMDevice::CPU); | ||
| assert_eq!(output_data, my_app_logic::expected_output()); | ||
| } | ||
| ``` | ||
|
|
||
| ## Running a model with I/O memory pools | ||
| This utilises Rust slices to take an input array of bytes and provide Rust access to different sections within the memory pools: | ||
|
|
||
| ```rust | ||
| mod my_app_logic; | ||
| extern crate tvmgen_ultimate_cat_spotter as ultimate_cat_spotter; | ||
|
|
||
| fn main() { | ||
| let mut memory_pool: [u8; 20000] = my_app_logic::create_memory_pool(); | ||
|
|
||
| let mut workspace = ultimate_cat_spotter::Workspace::new( | ||
| &mut memory_pool, | ||
| ); | ||
|
|
||
| let mut model_input = workspace.input_data(); | ||
| my_app_logic::copy_input_data(model_input); | ||
|
|
||
| let mut model = ultimate_cat_spotter::Model::new(); | ||
| model.infer(&mut workspace, TVMDevice::CPU); | ||
| assert_eq!(workspace.output_data(), my_app_logic::expected_output()); | ||
| } | ||
| ``` | ||
|
|
||
| ## Rust Device API | ||
| In the Rust interface, we provide a similar interface as the C Device API, providing a trait for driver authors to implement: | ||
|
|
||
| ```rust | ||
| trait TVMDevice { | ||
| fn activate(); | ||
| fn open(); | ||
| fn close(); | ||
| fn deactivate(); | ||
| } | ||
| ``` | ||
|
|
||
| This can then be used by a driver author as simply: | ||
|
|
||
| ```rust | ||
| impl TVMDevice for MyDriver { ... } | ||
| ``` | ||
|
|
||
| Which can be used as an alternative to `TVMDevice::CPU` in the `run` function: | ||
|
|
||
| ```rust | ||
| mod my_app_logic; | ||
| mod woofles_accelerator; | ||
| extern crate tvmgen_ultimate_cat_spotter as ultimate_cat_spotter; | ||
|
|
||
| fn main() { | ||
| let mut memory_pool: [u8; 20000] = my_app_logic::create_memory_pool(); | ||
|
|
||
| let mut workspace = ultimate_cat_spotter::Workspace::new( | ||
| &mut memory_pool, | ||
| ); | ||
|
|
||
| let mut model_input = workspace.input_data(); | ||
| my_app_logic::copy_input_data(model_input); | ||
|
|
||
| let mut model = ultimate_cat_spotter::Model::new(); | ||
| model.infer(&mut workspace, TVMDevice::CPU); | ||
| assert_eq!(workspace.output_data(), my_app_logic::expected_output()); | ||
| } | ||
| ``` | ||
|
|
||
| # Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| In order to create a Rust interface, we must first compile the C artefacts and then create `safe` wrappers around the resultant code. | ||
|
|
||
| ## C Backend Compilation | ||
| As the AOT LLVM backend is limited to the C++ runtime, we re-use the C backend with the Rust FFI module to compile the C code at build time - this allows us to re-use existing [Embedded C APIs](https://discuss.tvm.apache.org/t/rfc-utvm-embedded-c-runtime-interface/9951) with Rust wrappers, such that the call to the C function `tvmgen_<model>_run` is replaced by a `safe` Rust variant: | ||
|
|
||
| This requires an additional build step in `build.rs` to reference the existing C files using `cc-rs`: | ||
|
|
||
| ```rust | ||
| use cc; | ||
| fn main() { | ||
| cc::Build::new() | ||
| .include("../codegen/host/include") | ||
| .include("../runtime/include") | ||
| .file("../codegen/host/src/default_lib0.c") | ||
| .file("../codegen/host/src/default_lib1.c") | ||
| .compile("mlf"); | ||
| } | ||
| ``` | ||
|
|
||
| This is contained within the generated crate so the user is not required to manage this. | ||
|
|
||
| ## C Wrapper Structs | ||
| By wrapping the C types in Rust structs, and exposing a `new` constructor it allows us to take a defined Rust array and translate it into a `void*`. Users of this interface only need to deal with pure Rust: | ||
|
|
||
| ```rust | ||
| /// Input tensors for TVM module "rusty_coffee" | ||
| #[repr(C)] | ||
| pub struct Workspace { | ||
| input: *mut ::std::os::raw::c_void, | ||
| output: *mut ::std::os::raw::c_void, | ||
| } | ||
|
|
||
| impl Inputs { | ||
| pub fn new <'a>( | ||
| input: &mut [u8; 100], | ||
| output: &mut [u8; 10], | ||
| ) -> Self { | ||
| Self { | ||
| input: input.as_ptr() as *mut ::std::os::raw::c_void, | ||
| output: output.as_ptr() as *mut ::std::os::raw::c_void, | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## C Wrapper Entrypoint | ||
| Similar to the above, using the Rust FFI we can create an entrypoint function which passes the void pointers directly into C FFI from the Rust structs with the appropriate `unsafe` block to prevent user facing code from having to be `unsafe`. The `run` wrapper also checks the return code of TVM and converts it into a [standard Rust `Result` object](https://doc.rust-lang.org/rust-by-example/error/result.html). Distinct from the C interface API, the Rust interface only has a concept of `Workspace` which is more consistent across the invocations. The device also always specified, and implementations can be written to use either CPU or another Device as necessary within application code. | ||
|
|
||
| ```rust | ||
| /// Entrypoint function for TVM module "rusty_coffee" | ||
| /// # Arguments | ||
| /// * `workspace` - Workspace for model to operate on | ||
| pub struct Model { | ||
| pub fn new() { | ||
| return Self; | ||
| } | ||
|
|
||
| pub fn infer( | ||
| self, | ||
| workspace: &mut Workspace, | ||
| device: &mut TVMDevice | ||
| ) -> Result<(), ()> { | ||
| unsafe { | ||
| let ret = tvmgen_rusty_coffee_run( | ||
| { | ||
| .input = workspace.input | ||
| }, | ||
| { | ||
| .output = workspace.output | ||
| }, | ||
| ); | ||
| if ret == 0 { | ||
| Ok(()) | ||
| } else { | ||
| Err(()) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[repr(C)] | ||
| struct Inputs { | ||
| input: *mut ::std::os::raw::c_void, | ||
| } | ||
|
|
||
| #[repr(C)] | ||
| struct Outputs { | ||
| output: *mut ::std::os::raw::c_void, | ||
| } | ||
|
|
||
| extern "C" { | ||
| pub fn tvmgen_rusty_coffee_run( | ||
| inputs: *mut Inputs, | ||
| outputs: *mut Outputs, | ||
| ) -> i32; | ||
| } | ||
| ``` | ||
|
|
||
| ## Rust Device API | ||
| Additional interfaces can be added to TVM's rust crate to provide `TVMDevice` with an implementation for CPU named `TVMDevice::CPU`: | ||
|
|
||
| ```rust | ||
| pub struct CPU {} | ||
| impl TVMDevice for CPU { | ||
| fn activate() {} | ||
| fn open() {} | ||
| fn close() {} | ||
| fn deactivate() {} | ||
| } | ||
| ``` | ||
|
|
||
| We also define a `TVMDevice` shim to convert the C pointers in the executor to Rust, such as: | ||
| ```rust | ||
| #[no_mangle] | ||
| pub extern TVMDeviceWooflesActivate(device: *TVMDevice) { | ||
| *device.activate(); | ||
| } | ||
| #[no_mangle] | ||
| pub extern TVMDeviceWooflesOpen(device: *TVMDevice) { | ||
| *device.open(); | ||
| } | ||
| #[no_mangle] | ||
| pub extern TVMDeviceWooflesClose(device: *TVMDevice) { | ||
| *device.close(); | ||
| } | ||
| #[no_mangle] | ||
| pub extern TVMDeviceWooflesDeactivate(device: *TVMDevice) { | ||
| *device.deactivate(); | ||
| } | ||
| ``` | ||
|
|
||
| # Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| This introduces a second embedded API alongside the C API, but fundamentally we want users to be able to create applications in the language that best suits their needs. | ||
|
|
||
| Whilst it is an innovation project, this will be supported with best efforts but may be lag in features behind the C interface API. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we clarify the scope of the support (e.g. what models can be supported and what are the restrictions)?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe we do this currently for the C Embedded API and if we want to, we should define it as part of the over-arching microTVM project?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My question is mainly about scope of support of the current proposal just to bring some clarity, that is all.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we can provide that at the interface level, it is the same as the Embedded C interface which relies on the various functions of the AOT executor and C runtime to function - it's not specific to the Embedded Rust Interface?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This particular comment do not have things to do with interface, but instead the models that can be supported and possible restrictions. In this case stating that "this particular module will support all the models that can be supported through embedded C API, because there is a gap in lacking behind C interface API A, B, C , we might miss models X, Y, Z" would be sufficient.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not to block this RFC but just curious, who will be on the hook to maintain and ensure that the embedded rust API is properly tested? Right now for example the Rust TVM bindings aren't really owned by anyone and the Go bindings aren't even run in CI and are potentially bit rotting away There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to add a section to the RFC that outlines the testing and regression prevention strategy?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The distinction here is features not robustness, as this lands in TVM I would expect it to be both initially tested and regression tested in line with https://github.com/apache/tvm/blob/main/docs/contribute/code_review.rst#id11 ? |
||
|
|
||
| # Rationale and alternatives | ||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| - Add LLVM support for Embedded AOT, this would take longer and the user interface would only be marginally improved compared to the time taken to achieve it | ||
| - Provide sample applications for Embedded Rust, by using the knowledge TVM has about a model it can generate proper types and guide users when building an application rather than them having to recreate it from [Model Library Format archive](https://discuss.tvm.apache.org/t/rfc-tvm-model-library-format/9121). | ||
|
|
||
| # Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| This largely aims to follow the APIs defined for `--interface-api=c` (see: [Embedded C APIs](https://discuss.tvm.apache.org/t/rfc-utvm-embedded-c-runtime-interface/9951)), but wraps each call to allow idiomatic user facing Rust code. | ||
|
|
||
| It builds upon the [Model Library Format archive](https://discuss.tvm.apache.org/t/rfc-tvm-model-library-format/9121) to provide a complete package for Rust appication developers with all relevant files. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: appication -> application |
||
|
|
||
| # Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| - Are we as idiomatic as we can be? | ||
| - Is there a better abstraction for devices? | ||
|
|
||
| # Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
|
||
| * Move away from the C codegen and provide an LLVM based solution without the C interface API | ||
| * Rust based [Project API](https://github.com/apache/tvm-rfcs/blob/main/rfcs/0008-microtvm-project-api.md) templates. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thakns for the RFC, after reading the proposed code, I think the main thing that would be worth clarifying is the overall scope, and scope limitations in relation with existing namespace. I will put my input in a followup comment section.