Stream: wasmtime

Topic: recommendations for proper usage of `wasmtime` (Rust)


view this post on Zulip yonil (Mar 17 2023 at 18:31):

Hi,

The following are perhaps 'basic' questions, but I wasn't able to get clear guidance for all of them from the wasmtime crate's documentation. I would appreciate your recommendations.

Scenario:

Goal:

Questions:

  1. should i have a single cached instance of Linker or one per invocation of a function in the guest? or, what are the considerations for each option?
  2. should i have a single instance of Module or one per invocation of a function in the guest? (i've tried caching the module after calling Module::from_file(...) once, and this has significantly reduced runtime duration. but i want to make sure i fully understand the downsides of doing this, if there are any)
  3. what does wasmtime_wasi::add_to_linker(...) actually do, and if I do cache the Module, is it ok to call it just once after i first call Module::from_file(...)?
  4. do I need a separate Store for each invocation of a function in the guest, or can i reuse Stores under certain circumstances (i understand there's no built-in GC for Stores)?
  5. are there any other tips for improving the performance of the aforementioned scenario?

Thanks in advance!

view this post on Zulip Alex Crichton (Mar 17 2023 at 18:42):

Good questions, and thanks for asking!

One important thing you may want to consider is the distinction between a module and an instance. A module is a compiled version of a wasm module, and an instance is a "runnable version" of that module. Modules have no state other than their compilation artifact and can hence be reused amongst many instances. Instances have state, but by default do not share state. The Linker type is the means by which you go from a Module to an Instance by satisfying all the imports of the module with actual runtime values.

I'm explaining these bits because the answer to some of your questions is sort of how the state works out. Do you want one wasm instance for the entire lifetime of the host for example? One instance per function call? The impact of this is what state the wasm instance can share between function calls (e.g. with an instance-per-function-call you'll share no state). With only one instance then you only need one Module and one Linker. With instance-per-function (or some other granularity), you only need one Module and you probably only need one Linker for the whole lifetime unless you're instantiating with different runtime values for each import.

Given some of that background, here's an attempt to answer some specific questions:

  1. The fastest option is going to be one Linker per lifetime of the process. This may require some refactoring and/or adjusting of the data model of your component, and it may not, depends on the specifics.
  2. You should only ever have one Module per wasm blob. If you've only got one wasm blob you should have only one Module. This is expected and there's no downside to Module reuse, it's designed to be reused.
  3. The wasmtime_wasi::add_to_linker function will populate a Linker with an implementation of WASI functions that the instance can get access to. This is required if the Module imports from the wasi_snapshot_preview1 name, for example. The Linker does not store the Module so it doesn't matter whether this happens in relation to the creation of the Module or not.
  4. Whether or not you want a separate Store depends on your data sharing module. If you want the wasm instance to persist its linear memory between function invocations you'll want to use the same Store. If you want no data persisted you should create a second Store with a second instance. A good rule-of-thumb is one instance per store. You should not repeatedly instantiate into the same Store for the lifetime of your process since, as you've noted, nothing in a Store is dropped until the whole Store is dropped which means that your program would otherwise have a memory leak.

view this post on Zulip Lann Martin (Mar 17 2023 at 19:44):

If you do end up wanting a single Linker and multiple Instances, take a look at https://docs.rs/wasmtime/6.0.1/wasmtime/struct.InstancePre.html, which is intended to be the fastest way to get multiple instances from the same module/linker config

view this post on Zulip yonil (Mar 17 2023 at 19:54):

thanks @Alex Crichton ! this is very helpful information.

one follow up question:

  1. what does calling linker.module() actually do?
    and if I go with the option of 'single linker, single module per lifetime of the process', but separate Store per Instance - is it possible to call linker.module() once (with some Store), or must i call it foreach Store that I create? (and is overhead in calling linker.module() each time i create a new Store?)

view this post on Zulip Alex Crichton (Mar 17 2023 at 19:57):

The linker.module() method is intended for what is largely now legacy WASI integration, I'd recommend using .instantiate_pre as @Lann Martin mentioned and working with that for creating multiple instances (or using .instantiate if you're only creating one instance)

If you create a new instance-per-function (or similar), I'd go with @Lann Martin's suggestion which is to call linker.instantiate_pre() and then repeatedly instantiate that instance (since that does type and import checking once and not on each instantiation)

view this post on Zulip yonil (Mar 17 2023 at 19:59):

thanks very much @Alex Crichton and @Lann Martin !

view this post on Zulip yonil (Mar 17 2023 at 20:15):

i'm not out of the woods just yet...

i define host functions (using linker.func_wrap()) whose implementation depends on the contents of the current store (i write the input to the guest into them, and have the guest write its output using them)

something like:

            let input: &[u8] = ...;
            linker
                .func_wrap("my_host", "get_input_size", move || -> i32 {
                    input.len() as i32
                })
                .unwrap();

            linker
                .func_wrap(
                    "my_host",
                    "get_input",
                    move |mut caller: Caller<'_, WasiState>, ptr: i32| {
                        let mem = caller.get_export("memory").unwrap().into_memory().unwrap();
                        let offset = ptr as u32 as usize;
                        mem.write(&mut caller, offset, input).unwrap();
                    },
                )
                .unwrap();

            let output: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![]));
            let output_ = Arc::clone(&output);

            linker
                .func_wrap(
                    "my_host",
                    "set_output",
                    move |mut caller: Caller<'_, WasiState>, ptr: i32, capacity: i32| {
                        let output = output_.clone();
                        let mem = caller.get_export("memory").unwrap().into_memory().unwrap();
                        let offset = ptr as u32 as usize;
                        let mut buffer: Vec<u8> = vec![0; capacity as usize];
                        mem.read(&caller, offset, &mut buffer).unwrap();
                        let mut output = output.lock().unwrap();
                        *output = buffer;
                    },
                )
                .unwrap();

if i call linker.instantiate_pre() when i instantiate the module (once, at the beginning of the host's lifetime, and before i call the aforementioned linker.func_wrap(), which differ per function invocation as the input and output are different), then the guest doesn't recognize the host's functions and panics.

is there a simple way around this you can think of?

view this post on Zulip Alex Crichton (Mar 17 2023 at 20:19):

You'll want to place store-specific data in the T of Store<T>

view this post on Zulip Alex Crichton (Mar 17 2023 at 20:19):

right now it's WasiState in your example, but you'd instead want to do something like:

struct MyState {
    wasi: WasiState,
    input: Vec<u8>,
    output: Option<Vec<u8>>,
}

view this post on Zulip Alex Crichton (Mar 17 2023 at 20:20):

then before you invoke wasm you'd do:

store.data_mut().input = ...;

and after invoking wasm you'd do:

let output = store.data().output.as_ref().ok_or_else(|| ...)?;

view this post on Zulip yonil (Mar 17 2023 at 20:59):

great point. thanks again!

view this post on Zulip Alex Crichton (Mar 17 2023 at 21:03):

If you've got suggestions about how to improve the docs here that'd also be much appreciated as well

view this post on Zulip Alex Crichton (Mar 17 2023 at 21:03):

be it more examples or more words or less words!

view this post on Zulip yonil (Mar 17 2023 at 22:24):

  1. i think an end-to-end example of how to write a guest and a host which pass arguments and returns values through shared memory would be very helpful for people with scenario's like mine

  2. the relationships between Module/Instance/Linker/Store, and best practices in creating/caching them, as mentioned in this thread, can also be helpful if documented in a central place.

view this post on Zulip Víctor García (Mar 18 2023 at 07:03):

@Alex Crichton, as @yonil mentioned, at least for my way of learning, I miss examples and best practice recommendations

view this post on Zulip Alex Crichton (Mar 20 2023 at 15:22):

Makes sense! I don't know how best to put all that into a document today, but it's good to be aware of nonetheless!


Last updated: Dec 23 2024 at 14:03 UTC