From 0aef52570c3e871084516d6e481f90134591219d Mon Sep 17 00:00:00 2001 From: Lei <45155667+leifu1128@users.noreply.github.com> Date: Wed, 11 Oct 2023 21:11:28 -0400 Subject: [PATCH] Initial Wasm runner implementation (#173) This PR adds a WASM runner, which can run WASM models compiled using the interface (subject to change #175) defined in ```../carton-runner-wasm/wit/lib.wit```. The existing implementation is still unoptimized, requiring 2 copies per Tensor moved to/from WASM. An example of compiling a compatible model can be found in ```carton-runner-wasm/tests/test_model```. ## Limitations - Only the ```wasm32-unknown-unknown``` target has been tested to be working. - Only ```infer``` is supported for now. - Packing only supports a single ```.wasm``` file and no other artifacts. - No WebGPU, and probably not for a while. ## Test Coverage All type conversions from Carton to WASM and vice versa and fully covered. Pack, Load, Infer are covered in pack.rs. ## TODOs Track in #164 --- .gitignore | 2 + Cargo.toml | 1 + ci/build.py | 1 + source/carton-runner-wasm/Cargo.toml | 31 +++ source/carton-runner-wasm/README.md | 5 + .../src/bin/build_wasm_releases.rs | 76 ++++++ source/carton-runner-wasm/src/component.rs | 11 + source/carton-runner-wasm/src/lib.rs | 58 +++++ source/carton-runner-wasm/src/main.rs | 75 ++++++ source/carton-runner-wasm/src/types.rs | 238 ++++++++++++++++++ source/carton-runner-wasm/tests/pack.rs | 93 +++++++ .../tests/test_model/Cargo.toml | 16 ++ .../tests/test_model/README.md | 8 + .../tests/test_model/model.wasm | Bin 0 -> 115150 bytes .../tests/test_model/src/lib.rs | 54 ++++ source/carton-runner-wasm/wit/lib.wit | 34 +++ 16 files changed, 703 insertions(+) create mode 100644 source/carton-runner-wasm/Cargo.toml create mode 100644 source/carton-runner-wasm/README.md create mode 100644 source/carton-runner-wasm/src/bin/build_wasm_releases.rs create mode 100644 source/carton-runner-wasm/src/component.rs create mode 100644 source/carton-runner-wasm/src/lib.rs create mode 100644 source/carton-runner-wasm/src/main.rs create mode 100644 source/carton-runner-wasm/src/types.rs create mode 100644 source/carton-runner-wasm/tests/pack.rs create mode 100644 source/carton-runner-wasm/tests/test_model/Cargo.toml create mode 100644 source/carton-runner-wasm/tests/test_model/README.md create mode 100644 source/carton-runner-wasm/tests/test_model/model.wasm create mode 100644 source/carton-runner-wasm/tests/test_model/src/lib.rs create mode 100644 source/carton-runner-wasm/wit/lib.wit diff --git a/.gitignore b/.gitignore index b147ea8..20e66b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ target/ .vscode/ libtorch/ +.idea/ +*.DS_Store \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 254716e..490afd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "source/carton-runner-py", "source/carton-runner-rust-bert", "source/carton-runner-torch", + "source/carton-runner-wasm", "source/carton-utils-py", "source/anywhere", "source/fetch-deps", diff --git a/ci/build.py b/ci/build.py index 39adec3..ed30711 100644 --- a/ci/build.py +++ b/ci/build.py @@ -116,6 +116,7 @@ def update_version_numbers(): run_command(["cargo", "run", RELEASE_FLAG, "--timings", "--target", TARGET, "-p", "carton-runner-py", "--bin", "build_releases", "--", "--output-path", args.runner_release_dir]) run_command(["cargo", "run", RELEASE_FLAG, "--timings", "--target", TARGET, "-p", "carton-runner-rust-bert", "--bin", "build_rust_bert_releases", "--", "--output-path", args.runner_release_dir]) run_command(["cargo", "run", RELEASE_FLAG, "--timings", "--target", TARGET, "-p", "carton-runner-torch", "--bin", "build_torch_releases", "--", "--output-path", args.runner_release_dir]) + run_command(["cargo", "run", RELEASE_FLAG, "--timings", "--target", TARGET, "-p", "carton-runner-wasm", "--bin", "build_wasm_releases", "--", "--output-path", args.runner_release_dir]) # Show sccache stats RUSTC_WRAPPER = os.getenv("RUSTC_WRAPPER", "") diff --git a/source/carton-runner-wasm/Cargo.toml b/source/carton-runner-wasm/Cargo.toml new file mode 100644 index 0000000..6183a72 --- /dev/null +++ b/source/carton-runner-wasm/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "carton-runner-wasm" +version = "0.0.1" +edition = "2021" +publish = false + +exclude = ["tests/test_model", "carton-wasm-interface"] + +[dependencies] +carton = { path = "../carton" } +carton-runner-interface = { path = "../carton-runner-interface" } +color-eyre = "0.6.2" +lunchbox = { version = "0.1", default-features = false } +wasmtime = { version = "13.0.0", features = ["component-model"] } +tokio = "1.32.0" +ndarray = "0.15.6" + +# Used by the `build_releases` binary +escargot = "0.5.8" +carton-runner-packager = { path = "../carton-runner-packager" } +clap = { version = "4.4.6", features = ["derive"] } +env_logger = "0.10.0" +log = "0.4.20" +target-lexicon = "0.12.11" +serde_json = "1.0.107" +semver = "1.0.20" + +[dev-dependencies] +escargot = "0.5.8" +paste = "1.0.14" +tempfile = "3.8.0" \ No newline at end of file diff --git a/source/carton-runner-wasm/README.md b/source/carton-runner-wasm/README.md new file mode 100644 index 0000000..8fe2265 --- /dev/null +++ b/source/carton-runner-wasm/README.md @@ -0,0 +1,5 @@ +# WASM Runner (Experimental) +This runner is capable of running models that implement the interface defined in 'carton-runner-wasm/wit/lib.wit'. + +## Disclaimer +The defined interface is subject to change, and backwards compatability is not guaranteed, while experimental. \ No newline at end of file diff --git a/source/carton-runner-wasm/src/bin/build_wasm_releases.rs b/source/carton-runner-wasm/src/bin/build_wasm_releases.rs new file mode 100644 index 0000000..8b2a196 --- /dev/null +++ b/source/carton-runner-wasm/src/bin/build_wasm_releases.rs @@ -0,0 +1,76 @@ +//! Same as torch runner + +use std::{path::PathBuf, time::SystemTime}; + +use clap::Parser; + +use carton_runner_interface::slowlog::slowlog; +use carton_runner_packager::discovery::RunnerInfo; + +// TODO: This should be the version of carton-interface-wasm, but it's not done yet. +const INTERFACE_VERSION: semver::Version = semver::Version::new(0, 0, 1); + +#[derive(Parser, Debug)] +struct Args { + #[arg(long)] + output_path: PathBuf, +} + +#[tokio::main] +async fn main() { + // Logging (for long running downloads) + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + // Parse args + let args = Args::parse(); + + log::info!("Starting runner build..."); + let mut sl = slowlog("Building runner", 5).await.without_progress(); + // Build the runner + let runner_path = escargot::CargoBuild::new() + .package("carton-runner-wasm") + .bin("carton-runner-wasm") + .current_release() + .current_target() + .arg("--timings") + .run() + .unwrap() + .path() + .display() + .to_string(); + + sl.done(); + log::info!("Runner Path: {}", runner_path); + + let package = carton_runner_packager::package( + RunnerInfo { + runner_name: "wasm".to_string(), + framework_version: INTERFACE_VERSION, + runner_compat_version: 1, + runner_interface_version: 1, + runner_release_date: SystemTime::now().into(), + runner_path, + platform: target_lexicon::HOST.to_string(), + }, + vec![], + ) + .await; + + // Write the zip file to our output dir + tokio::fs::write( + &args + .output_path + .join(format!("{}.zip", package.get_data_sha256())), + package.get_data(), + ) + .await + .unwrap(); + + // Write the package config so it can be loaded when the runner zip files will be uploaded + tokio::fs::write( + &args.output_path.join(format!("{}.json", package.get_id())), + serde_json::to_string_pretty(&package).unwrap(), + ) + .await + .unwrap(); +} diff --git a/source/carton-runner-wasm/src/component.rs b/source/carton-runner-wasm/src/component.rs new file mode 100644 index 0000000..1c52d47 --- /dev/null +++ b/source/carton-runner-wasm/src/component.rs @@ -0,0 +1,11 @@ +wasmtime::component::bindgen!({ + world: "model", + path: "./wit", +}); + +use crate::component::carton_wasm::lib::types::Host; +pub(crate) use carton_wasm::lib::types::{Dtype, TensorNumeric, TensorString}; + +pub(crate) struct HostImpl; + +impl Host for HostImpl {} diff --git a/source/carton-runner-wasm/src/lib.rs b/source/carton-runner-wasm/src/lib.rs new file mode 100644 index 0000000..b001fd9 --- /dev/null +++ b/source/carton-runner-wasm/src/lib.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use color_eyre::eyre::{eyre, Result}; +use wasmtime::component::{Component, Linker}; +use wasmtime::{Engine, Store}; + +use carton_runner_interface::types::Tensor as CartonTensor; + +use crate::component::{HostImpl, Model, Tensor}; + +mod component; +mod types; + +pub struct WASMModelInstance { + store: Store, + model: Model, +} + +impl WASMModelInstance { + pub fn from_bytes(engine: &Engine, bytes: &[u8]) -> Result { + /* + see https://docs.wasmtime.dev/api/wasmtime/component/macro.bindgen.html + Some of the names may be confusing, here is the general idea from my + understanding: + - HostImpl is the host side implementation of what a interface imports + since our current interface does not import anything, this is an empty + struct + - Model is the loaded and linked interface, i.e. the API we expect the + user to implement. (Non stateful) + TODO: rename to ModelInterface + */ + let comp = Component::from_binary(&engine, bytes).unwrap(); + let mut linker = Linker::::new(&engine); + Model::add_to_linker(&mut linker, |state: &mut HostImpl| state).unwrap(); + let mut store = Store::new(&engine, HostImpl); + let (model, _) = Model::instantiate(&mut store, &comp, &linker).unwrap(); + Ok(Self { store, model }) + } + + pub fn infer( + &mut self, + inputs: HashMap, + ) -> Result> { + let inputs = inputs + .into_iter() + .map(|(k, v)| Ok((k, v.try_into()?))) + .collect::>>()?; + let outputs = self + .model + .call_infer(&mut self.store, inputs.as_ref()) + .map_err(|e| eyre!(e))?; + let mut ret = HashMap::new(); + for (k, v) in outputs.into_iter() { + ret.insert(k, v.into()); + } + Ok(ret) + } +} diff --git a/source/carton-runner-wasm/src/main.rs b/source/carton-runner-wasm/src/main.rs new file mode 100644 index 0000000..0729f28 --- /dev/null +++ b/source/carton-runner-wasm/src/main.rs @@ -0,0 +1,75 @@ +use color_eyre::eyre::{eyre, Result}; +use lunchbox::{path::Path, types::WritableFileSystem, ReadableFileSystem}; +use wasmtime::{Config, Engine}; + +use carton_runner_interface::server::{init_runner, RequestData, ResponseData}; +use carton_runner_wasm::WASMModelInstance; + +fn new_engine() -> Result { + let mut config = Config::new(); + config.wasm_component_model(true); + Engine::new(&config).map_err(|e| eyre!(e)) +} + +#[tokio::main] +async fn main() { + color_eyre::install().unwrap(); + let mut server = init_runner().await; + let engine = new_engine().unwrap(); + let mut model: Option = None; + + while let Some(req) = server.get_next_request().await { + let req_id = req.id; + match req.data { + RequestData::Load { fs, .. } => { + let fs = server.get_readonly_filesystem(fs).await.unwrap(); + let bin = &fs.read("model.wasm").await.unwrap(); + model = Some( + WASMModelInstance::from_bytes(&engine, bin) + .expect("Failed to initialize WASM model"), + ); + server + .send_response_for_request(req_id, ResponseData::Load) + .await + .unwrap(); + } + RequestData::Pack { + input_path, + temp_folder, + fs, + } => { + let fs = server.get_writable_filesystem(fs).await.unwrap(); + fs.symlink(input_path, Path::new(&temp_folder).join("model.wasm")) + .await + .unwrap(); + server + .send_response_for_request( + req_id, + ResponseData::Pack { + output_path: temp_folder, + }, + ) + .await + .unwrap(); + } + RequestData::Seal { .. } => { + todo!() + } + RequestData::InferWithTensors { tensors, .. } => { + let result = model.as_mut().map(|m| m.infer(tensors)).unwrap(); + server + .send_response_for_request( + req_id, + ResponseData::Infer { + tensors: result.unwrap(), + }, + ) + .await + .unwrap(); + } + RequestData::InferWithHandle { .. } => { + todo!() + } + } + } +} diff --git a/source/carton-runner-wasm/src/types.rs b/source/carton-runner-wasm/src/types.rs new file mode 100644 index 0000000..47d6ed1 --- /dev/null +++ b/source/carton-runner-wasm/src/types.rs @@ -0,0 +1,238 @@ +use color_eyre::eyre::{ensure, eyre}; +use color_eyre::{Report, Result}; + +use carton::types::for_each_numeric_carton_type; +use carton_runner_interface::types::{Tensor as CartonTensor, TensorStorage as CartonStorage}; + +use crate::component::{Dtype, Tensor as WasmTensor, TensorNumeric, TensorString}; + +impl Into for WasmTensor { + fn into(self) -> CartonTensor { + match self { + WasmTensor::Numeric(t) => t.into(), + WasmTensor::String(t) => t.into(), + } + } +} + +fn bytes_to_slice(b: &[u8]) -> Result<&[T]> { + ensure!( + b.len() % std::mem::size_of::() == 0, + "Invalid byte length" + ); + Ok(unsafe { + std::slice::from_raw_parts(b.as_ptr() as *const T, b.len() / std::mem::size_of::()) + }) +} + +fn copy_to_storage(mut s: CartonStorage, b: &[u8]) -> CartonStorage { + s.view_mut() + .as_slice_mut() + .unwrap() + .clone_from_slice(bytes_to_slice(b).unwrap()); + s +} + +impl Into for TensorNumeric { + fn into(self) -> CartonTensor { + match self.dtype { + Dtype::Float => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + Dtype::Double => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + Dtype::I8 => copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into(), + Dtype::I16 => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + Dtype::I32 => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + Dtype::I64 => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + Dtype::U8 => copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into(), + Dtype::U16 => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + Dtype::U32 => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + Dtype::U64 => { + copy_to_storage(CartonStorage::::new(self.shape), &self.buffer).into() + } + } + } +} + +impl Into for TensorString { + fn into(self) -> CartonTensor { + let mut t = CartonStorage::new(self.shape); + t.view_mut() + .as_slice_mut() + .unwrap() + .clone_from_slice(&self.buffer); + t.into() + } +} + +impl TryFrom for WasmTensor { + type Error = Report; + + fn try_from(value: CartonTensor) -> Result { + Ok(match value { + CartonTensor::Float(t) => WasmTensor::Numeric(t.into()), + CartonTensor::Double(t) => WasmTensor::Numeric(t.into()), + CartonTensor::I8(t) => WasmTensor::Numeric(t.into()), + CartonTensor::I16(t) => WasmTensor::Numeric(t.into()), + CartonTensor::I32(t) => WasmTensor::Numeric(t.into()), + CartonTensor::I64(t) => WasmTensor::Numeric(t.into()), + CartonTensor::U8(t) => WasmTensor::Numeric(t.into()), + CartonTensor::U16(t) => WasmTensor::Numeric(t.into()), + CartonTensor::U32(t) => WasmTensor::Numeric(t.into()), + CartonTensor::U64(t) => WasmTensor::Numeric(t.into()), + CartonTensor::String(t) => WasmTensor::String(t.into()), + CartonTensor::NestedTensor(_) => return Err(eyre!("Nested tensors are not supported")), + }) + } +} + +fn slice_to_bytes(s: &[T]) -> &[u8] { + unsafe { + std::slice::from_raw_parts(s.as_ptr() as *const u8, s.len() * std::mem::size_of::()) + } +} + +impl From> for TensorNumeric { + fn from(value: CartonStorage) -> Self { + let view = value.view(); + let shape = view.shape().iter().map(|&x| x as u64).collect(); + let buffer = view.as_slice().unwrap(); + TensorNumeric { + buffer: slice_to_bytes(buffer).to_vec(), + dtype: T::dtype(), + shape, + } + } +} + +impl From> for TensorString { + fn from(value: CartonStorage) -> Self { + let view = value.view(); + let shape = view.shape().iter().map(|&x| x as u64).collect(); + let buffer = view.as_slice().unwrap().to_vec(); + TensorString { buffer, shape } + } +} + +trait DTypeOf { + fn dtype() -> Dtype; +} + +for_each_numeric_carton_type! { + $( + impl DTypeOf for $RustType { + fn dtype() -> Dtype { + Dtype::$CartonType + } + } + )* +} + +#[cfg(test)] +mod tests { + use super::*; + + for_each_numeric_carton_type! { + $( + paste::item! { + #[test] + fn [< $TypeStr "_tensor_carton_to_wasm" >]() { + let storage = CartonStorage::<$RustType>::new(vec![3]); + let carton_tensor = CartonTensor::$CartonType( + copy_to_storage( + storage, + slice_to_bytes( + &[1.0 as $RustType, 2.0 as $RustType, 3.0 as $RustType] + ) + ) + ); + let wasm_tensor = WasmTensor::try_from(carton_tensor).unwrap(); + match wasm_tensor { + WasmTensor::Numeric(tensor_numeric) => { + assert_eq!( + tensor_numeric.buffer, + slice_to_bytes(&[1.0 as $RustType, 2.0 as $RustType, 3.0 as $RustType]) + ); + } + _ => { + panic!(concat!("Expected WasmTensor::Numeric variant")); + } + } + } + + #[test] + fn [< $TypeStr "_tensor_wasm_to_carton" >]() { + let buffer = slice_to_bytes(&[1.0 as $RustType, 2.0 as $RustType, 3.0 as $RustType]); + let tensor = WasmTensor::Numeric(TensorNumeric { + buffer: buffer.to_vec(), + dtype: Dtype::$CartonType, + shape: vec![3], + }); + let carton_tensor: CartonTensor = tensor.into(); + match carton_tensor { + CartonTensor::$CartonType(storage) => { + assert_eq!( + storage.view().as_slice().unwrap(), + &[1.0 as $RustType, 2.0 as $RustType, 3.0 as $RustType] + ); + } + _ => { + panic!(concat!("Expected CartonTensor::", stringify!($CartonType), " variant")); + } + } + } + } + )* + } + + #[test] + fn string_tensor_carton_to_wasm() { + let buffer = vec!["hello".to_string(), "world".to_string()]; + let mut storage = CartonStorage::::new(vec![2]); + storage + .view_mut() + .as_slice_mut() + .unwrap() + .clone_from_slice(&buffer); + let carton_tensor = CartonTensor::String(storage); + let wasm_tensor = WasmTensor::try_from(carton_tensor).unwrap(); + match wasm_tensor { + WasmTensor::String(tensor_string) => { + assert_eq!(tensor_string.buffer, buffer); + } + _ => { + panic!("Expected WasmTensor::String variant"); + } + } + } + + #[test] + fn string_tensor_wasm_to_carton() { + let buffer = vec!["hello".to_string(), "world".to_string()]; + let tensor = WasmTensor::String(TensorString { + buffer: buffer.clone(), + shape: vec![2], + }); + let carton_tensor: CartonTensor = tensor.into(); + match carton_tensor { + CartonTensor::String(storage) => { + assert_eq!(storage.view().as_slice().unwrap(), &buffer); + } + _ => { + panic!("Expected CartonTensor::String variant"); + } + } + } +} diff --git a/source/carton-runner-wasm/tests/pack.rs b/source/carton-runner-wasm/tests/pack.rs new file mode 100644 index 0000000..7523910 --- /dev/null +++ b/source/carton-runner-wasm/tests/pack.rs @@ -0,0 +1,93 @@ +use std::path::PathBuf; + +use carton::{ + info::RunnerInfo, + types::{LoadOpts, Tensor}, +}; +use carton_runner_packager::RunnerPackage; +use tokio::process::Command; + +#[tokio::test] +async fn test_pack() { + // Logging (for long running downloads) + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .is_test(true) + .init(); + + // Get the path of the builder + let builder_path = PathBuf::from(env!("CARGO_BIN_EXE_build_wasm_releases")); + + // Create a tempdir to store packaging artifacts + let tempdir = tempfile::tempdir().unwrap(); + let tempdir_path = tempdir.path(); + + // Run the builder + let status = Command::new(builder_path) + .args(&["--output-path", tempdir_path.to_str().unwrap()]) + .status() + .await + .unwrap(); + assert!(status.success()); + + // Get a package + let package_config = std::fs::read_dir(&tempdir_path) + .unwrap() + .find_map(|item| { + if let Ok(item) = item { + if item.file_name().to_str().unwrap().ends_with(".json") { + return Some(item); + } + } + + None + }) + .unwrap(); + + let package: RunnerPackage = + serde_json::from_slice(&std::fs::read(package_config.path()).unwrap()).unwrap(); + + // Get the zipfile path + let path = tempdir_path.join(format!("{}.zip", package.get_data_sha256())); + let download_info = package.get_download_info(path.to_str().unwrap().to_owned()); + + // Now install the runner we just packaged into a tempdir + let runner_dir = tempfile::tempdir().unwrap(); + std::env::set_var("CARTON_RUNNER_DIR", runner_dir.path()); + log::info!("About to install runner"); + carton_runner_packager::install(download_info, true).await; + log::info!("Installed runner"); + + // Pack a model + let model_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/test_model/model.wasm"); + + let packed_model = carton::Carton::pack( + model_path.to_str().unwrap(), + RunnerInfo { + runner_name: "wasm".into(), + required_framework_version: semver::VersionReq::parse("=0.0.1").unwrap(), + runner_compat_version: None, + opts: None, + }, + ) + .await + .unwrap(); + + // Load the model + let model = carton::Carton::load(packed_model.to_str().unwrap(), LoadOpts::default()) + .await + .unwrap(); + + let tensor_in1 = ndarray::ArrayD::from_shape_vec(vec![20], vec![1.5f32; 20]).unwrap(); + + let out = model + .infer([("in1", Tensor::new(tensor_in1))]) + .await + .unwrap(); + + let s = match out.get("out1").unwrap() { + Tensor::Float(s) => s, + _ => panic!("Invalid tensor type"), + }; + + assert_eq!(s.view().as_slice().unwrap(), &vec![2.5f32; 20]); +} diff --git a/source/carton-runner-wasm/tests/test_model/Cargo.toml b/source/carton-runner-wasm/tests/test_model/Cargo.toml new file mode 100644 index 0000000..d532042 --- /dev/null +++ b/source/carton-runner-wasm/tests/test_model/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "basic_model" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = "0.12.0" + +[workspace] + +[profile.release] +lto = true +opt-level = 'z' diff --git a/source/carton-runner-wasm/tests/test_model/README.md b/source/carton-runner-wasm/tests/test_model/README.md new file mode 100644 index 0000000..6e30f7b --- /dev/null +++ b/source/carton-runner-wasm/tests/test_model/README.md @@ -0,0 +1,8 @@ +To build the test model, run: +```shell +cargo build --target wasm32-unknown-unknown --release +``` +```shell +wasm-tools component new ./target/wasm32-unknown-unknown/release/basic_model.wasm \ + -o model.wasm +``` \ No newline at end of file diff --git a/source/carton-runner-wasm/tests/test_model/model.wasm b/source/carton-runner-wasm/tests/test_model/model.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d5acd0cca112a2d4b91219b4fee9c97a45993703 GIT binary patch literal 115150 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 Dv-2pi literal 0 HcmV?d00001 diff --git a/source/carton-runner-wasm/tests/test_model/src/lib.rs b/source/carton-runner-wasm/tests/test_model/src/lib.rs new file mode 100644 index 0000000..93434b0 --- /dev/null +++ b/source/carton-runner-wasm/tests/test_model/src/lib.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; +use std::ops::Add; +use std::panic::panic_any; +use std::slice::from_raw_parts; + +use carton_wasm::lib::types::{Dtype, TensorNumeric}; + +wit_bindgen::generate!({ + world: "model", + path: "../../wit", + exports: { + world: Model + } +}); + +struct Model; + +fn bytes_to_vec(b: &mut [u8]) -> Vec { + assert_eq!(b.len() % std::mem::size_of::(), 0, "Invalid byte length"); + let len = b.len() / std::mem::size_of::(); + let ptr = b.as_mut_ptr() as *mut T; + unsafe { from_raw_parts(ptr, len).to_vec() } +} + +fn vec_to_bytes(t: &mut [T]) -> Vec { + let len = t.len() * std::mem::size_of::(); + let ptr = t.as_mut_ptr() as *mut u8; + unsafe { from_raw_parts(ptr, len).to_vec() } +} + +fn to_f32(t: &mut Tensor) -> Vec { + let n = match t { + Tensor::Numeric(t) => t, + Tensor::String(_) => panic_any("Invalid tensor type") + }; + bytes_to_vec::(&mut n.buffer) +} + +impl Guest for Model { + fn infer(in_: Vec<(String, Tensor)>) -> Vec<(String, Tensor)> { + let mut inputs: HashMap = in_.into_iter().collect(); + let out = to_f32(inputs.get_mut("in1").unwrap()); + let len = out.len().clone() as u64; + let mut out = out.into_iter().map(|x| x.add(1f32)).collect::>(); + let t = Tensor::Numeric( + TensorNumeric { + buffer: vec_to_bytes::(&mut out), + dtype: Dtype::Float, + shape: vec![len], + } + ); + vec![("out1".parse().unwrap(), t)] + } +} diff --git a/source/carton-runner-wasm/wit/lib.wit b/source/carton-runner-wasm/wit/lib.wit new file mode 100644 index 0000000..ed8ce7f --- /dev/null +++ b/source/carton-runner-wasm/wit/lib.wit @@ -0,0 +1,34 @@ +package carton-wasm:lib + +interface types { + enum dtype { + float, + double, + I8, + I16, + I32, + I64, + U8, + U16, + U32, + U64, + } + record tensor-numeric { + buffer: list, + dtype: dtype, + shape: list, + } + record tensor-string { + buffer: list, + shape: list, + } + variant tensor { + numeric(tensor-numeric), + %string(tensor-string), + } +} + +world model { + use types.{tensor}; + export infer: func(in: list>) -> list>; +} \ No newline at end of file