From 854a62101001a8e01b87cc2997af1e1db6c09880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20M?= Date: Wed, 23 Aug 2023 17:14:10 +0200 Subject: [PATCH] feat: add WASI-NN bindings to Wasm Workers Server (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add WASI-NN bindings to Wasm Workers Server * fix: remove unnecessary var initialization * fix: typo error when closing a conditional assignment * feat: add new documentation about ML inference and WASI-NN * docs: improve docs and remove typos * fix: remove support for tensor files and clarifies why we need to skip 1 --------- Co-authored-by: Rafael Fernández López --- Cargo.lock | 89 +- Cargo.toml | 8 +- crates/worker/Cargo.toml | 1 + crates/worker/src/config.rs | 4 + crates/worker/src/features/mod.rs | 1 + crates/worker/src/features/wasi_nn.rs | 13 + crates/worker/src/lib.rs | 27 + docs/docs/features/dynamic-routes.md | 7 +- docs/docs/features/http-requests.md | 7 +- docs/docs/features/machine-learning.md | 72 ++ docs/docs/features/mount-folders.md | 10 +- .../features/multiple-language-runtimes.md | 9 +- docs/docs/features/static-assets.md | 7 +- docs/static/img/docs/features/wasi-nn.webp | Bin 0 -> 25026 bytes examples/Makefile | 7 +- examples/rust-wasi-nn/.gitignore | 3 + examples/rust-wasi-nn/Cargo.lock | 544 +++++++++ examples/rust-wasi-nn/Cargo.toml | 15 + examples/rust-wasi-nn/README.md | 53 + examples/rust-wasi-nn/_images/.gitkeep | 0 examples/rust-wasi-nn/_models/dataset.json | 1004 +++++++++++++++++ examples/rust-wasi-nn/index.js | 68 ++ examples/rust-wasi-nn/inference.toml | 17 + examples/rust-wasi-nn/prepare.sh | 11 + examples/rust-wasi-nn/public/main.css | 84 ++ examples/rust-wasi-nn/public/main.js | 48 + examples/rust-wasi-nn/src/main.rs | 111 ++ 27 files changed, 2208 insertions(+), 12 deletions(-) create mode 100644 crates/worker/src/features/wasi_nn.rs create mode 100644 docs/docs/features/machine-learning.md create mode 100644 docs/static/img/docs/features/wasi-nn.webp create mode 100644 examples/rust-wasi-nn/.gitignore create mode 100644 examples/rust-wasi-nn/Cargo.lock create mode 100644 examples/rust-wasi-nn/Cargo.toml create mode 100644 examples/rust-wasi-nn/README.md create mode 100644 examples/rust-wasi-nn/_images/.gitkeep create mode 100644 examples/rust-wasi-nn/_models/dataset.json create mode 100644 examples/rust-wasi-nn/index.js create mode 100644 examples/rust-wasi-nn/inference.toml create mode 100755 examples/rust-wasi-nn/prepare.sh create mode 100644 examples/rust-wasi-nn/public/main.css create mode 100644 examples/rust-wasi-nn/public/main.js create mode 100644 examples/rust-wasi-nn/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 699fa89a..11a053de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1054,6 +1054,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.9.3" @@ -1061,7 +1074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", - "humantime", + "humantime 2.1.0", "log", "regex", "termcolor", @@ -1073,7 +1086,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ - "humantime", + "humantime 2.1.0", "is-terminal", "log", "regex", @@ -1430,6 +1443,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + [[package]] name = "humantime" version = "2.1.0" @@ -1959,6 +1981,39 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "openvino" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc731d9a7805dd533b69de3ee33062d5ea1dfa9fca1c19f8fd165b62e2cdde7" +dependencies = [ + "openvino-finder", + "openvino-sys", + "thiserror", +] + +[[package]] +name = "openvino-finder" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bbd80eea06c2b9ec3dce85900ff3ae596c01105b759b38a005af69bbeb4d07" +dependencies = [ + "cfg-if", + "log", +] + +[[package]] +name = "openvino-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "318ed662bdf05a3f86486408159e806d53363171621a8000b81366fab5158713" +dependencies = [ + "libloading", + "once_cell", + "openvino-finder", + "pretty_env_logger", +] + [[package]] name = "os_str_bytes" version = "6.5.1" @@ -2045,6 +2100,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger 0.7.1", + "log", +] + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -2112,6 +2177,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quickjs-wasm-rs" version = "1.0.0" @@ -3469,6 +3540,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wasmtime-wasi-nn" +version = "10.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74375e57624b24d790b8d1427bdde6c4ef9a76e4148b5b73998353dc973c24e" +dependencies = [ + "anyhow", + "openvino", + "thiserror", + "walkdir", + "wiggle", +] + [[package]] name = "wasmtime-winch" version = "10.0.1" @@ -4101,6 +4185,7 @@ dependencies = [ "wasi-common", "wasmtime", "wasmtime-wasi", + "wasmtime-wasi-nn", "wit-bindgen-wasmtime", "wws-config", "wws-data-kv", diff --git a/Cargo.toml b/Cargo.toml index 98bdc0ab..fbc71466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ repository = { workspace = true } [workspace.package] version = "1.4.0" edition = "2021" -authors = [ "Wasm Labs " ] +authors = ["Wasm Labs "] license = "Apache-2.0" repository = "https://github.com/vmware-labs/wasm-workers-server/" @@ -60,7 +60,7 @@ members = [ "crates/worker", "kits/rust", "kits/rust/worker", - "kits/javascript" + "kits/javascript", ] # Exclude examples exclude = [ @@ -68,7 +68,8 @@ exclude = [ "examples/rust-basic", "examples/rust-fetch", "examples/rust-kv", - "examples/rust-params" + "examples/rust-params", + "examples/rust-wasi-nn", ] [workspace.dependencies] @@ -93,6 +94,7 @@ wws-api-manage = { path = "./crates/api-manage" } wws-api-manage-openapi = { path = "./crates/api-manage-openapi" } wasmtime = "10.0.1" wasmtime-wasi = "10.0.1" +wasmtime-wasi-nn = "10.0.1" wasi-common = "10.0.1" path-slash = "0.2.1" openssl = { version = "=0.10.55" } diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 5f7495eb..6110b6bc 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -19,6 +19,7 @@ tokio = { workspace = true } toml = { workspace = true } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } +wasmtime-wasi-nn = { workspace = true } wasi-common = { workspace = true } wws-config = { workspace = true } wws-data-kv = { workspace = true } diff --git a/crates/worker/src/config.rs b/crates/worker/src/config.rs index f2a6763b..befa3c9c 100644 --- a/crates/worker/src/config.rs +++ b/crates/worker/src/config.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::features::http_requests::HttpRequestsConfig; +use crate::features::wasi_nn::WasiNnConfig; use crate::features::{data::ConfigData, folders::Folder}; use anyhow::{anyhow, Result}; use serde::{Deserialize, Deserializer}; @@ -13,9 +14,12 @@ use wws_data_kv::KVConfigData; /// List all available features for a worker #[derive(Deserialize, Clone, Default)] +#[serde(default)] pub struct Features { /// Allow to perform http requests from a worker pub http_requests: HttpRequestsConfig, + /// Enables WASI-NN bindings for Machine Learning inference + pub wasi_nn: WasiNnConfig, } /// Workers configuration. These files are optional when no configuration change is required. diff --git a/crates/worker/src/features/mod.rs b/crates/worker/src/features/mod.rs index b510ab48..93bc3d51 100644 --- a/crates/worker/src/features/mod.rs +++ b/crates/worker/src/features/mod.rs @@ -4,3 +4,4 @@ pub mod data; pub mod folders; pub mod http_requests; +pub mod wasi_nn; diff --git a/crates/worker/src/features/wasi_nn.rs b/crates/worker/src/features/wasi_nn.rs new file mode 100644 index 00000000..0d25cd7e --- /dev/null +++ b/crates/worker/src/features/wasi_nn.rs @@ -0,0 +1,13 @@ +// Copyright 2023 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; + +pub const WASI_NN_BACKEND_OPENVINO: &str = "openvino"; + +#[derive(Deserialize, Clone, Default)] +#[serde(default)] +pub struct WasiNnConfig { + /// List of Machine Learning backends. For now, only "openvino" option is supported + pub allowed_backends: Vec, +} diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index 0d897129..93f858b8 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -11,15 +11,18 @@ use actix_web::HttpRequest; use anyhow::{anyhow, Result}; use bindings::http::{add_to_linker as http_add_to_linker, HttpBindings}; use config::Config; +use features::wasi_nn::WASI_NN_BACKEND_OPENVINO; use io::{WasmInput, WasmOutput}; use sha256::digest as sha256_digest; use std::fs::{self, File}; use std::path::PathBuf; +use std::sync::Arc; use std::{collections::HashMap, path::Path}; use stdio::Stdio; use wasi_common::WasiCtx; use wasmtime::{Engine, Linker, Module, Store}; use wasmtime_wasi::{ambient_authority, Dir, WasiCtxBuilder}; +use wasmtime_wasi_nn::WasiNnCtx; use wws_config::Config as ProjectConfig; use wws_runtimes::{init_runtime, Runtime}; @@ -43,6 +46,7 @@ pub struct Worker { struct WorkerState { pub wasi: WasiCtx, + pub wasi_nn: Option>, pub http: HttpBindings, } @@ -134,12 +138,35 @@ impl Worker { } } + // WASI-NN + let allowed_backends = &self.config.features.wasi_nn.allowed_backends; + + let wasi_nn = if !allowed_backends.is_empty() { + // For now, we only support OpenVINO + if allowed_backends.len() != 1 + || !allowed_backends.contains(&WASI_NN_BACKEND_OPENVINO.to_string()) + { + eprintln!("❌ The only WASI-NN supported backend name is \"{WASI_NN_BACKEND_OPENVINO}\". Please, update your config."); + None + } else { + wasmtime_wasi_nn::add_to_linker(&mut linker, |s: &mut WorkerState| { + Arc::get_mut(s.wasi_nn.as_mut().unwrap()) + .expect("wasi-nn is not implemented with multi-threading support") + })?; + + Some(Arc::new(WasiNnCtx::new()?)) + } + } else { + None + }; + // Pass to the runtime to add any WASI specific requirement wasi_builder = self.runtime.prepare_wasi_ctx(wasi_builder)?; let wasi = wasi_builder.build(); let state = WorkerState { wasi, + wasi_nn, http: HttpBindings { http_config: self.config.features.http_requests.clone(), }, diff --git a/docs/docs/features/dynamic-routes.md b/docs/docs/features/dynamic-routes.md index db457578..41ea9872 100644 --- a/docs/docs/features/dynamic-routes.md +++ b/docs/docs/features/dynamic-routes.md @@ -1,8 +1,13 @@ --- +title: Dynamic Routes sidebar_position: 4 --- -# Dynamic routes +:::info + +[Available since v1.0](https://github.com/vmware-labs/wasm-workers-server/releases/tag/v1.0.0) + +::: Defining static routes may not be enough for some applications. You may need a worker to process URLs that includes identifiers. **To create a worker associated with a dynamic route, include the route parameter in brackets when setting the worker filename**. diff --git a/docs/docs/features/http-requests.md b/docs/docs/features/http-requests.md index 4c4b8895..30695083 100644 --- a/docs/docs/features/http-requests.md +++ b/docs/docs/features/http-requests.md @@ -1,8 +1,13 @@ --- +title: HTTP Requests (fetch) sidebar_position: 3 --- -# HTTP Requests (fetch) +:::info + +[Available since v1.4](https://github.com/vmware-labs/wasm-workers-server/releases/tag/v1.4.0) + +::: Often times, workers require to access data from an external resource like a website or an API. This feature allows workers to perform HTTP requests to external resources. It follows the capability-based model, so workers cannot perform any HTTP request until you configure the allowed hosts and HTTP methods. diff --git a/docs/docs/features/machine-learning.md b/docs/docs/features/machine-learning.md new file mode 100644 index 00000000..ac49ac45 --- /dev/null +++ b/docs/docs/features/machine-learning.md @@ -0,0 +1,72 @@ +--- +title: Machine Learning inference +--- + +:::caution + +This is a feature preview. It will be available in v1.5.0 + +::: + +Artificial Intelligence (AI) and Machine Learning (ML) are hot topics in the community. This feature enables you to expand the capabilities of your workers by running ML models in your models. For example, you can develop an application that uses image classification or text-to-speech. + +To provide this feature, Wasm Workers Server relies on the [WASI-NN proposal](https://github.com/WebAssembly/wasi-nn). This proposal defines a set of APIs to send and retrieve data, and run the ML inference at the host side. The main benefits of this approach are to reuse the existing ML ecosystem (like Tensorflow and OpenVINO) and use hardware acceleration when it's available (GPUs, TPUs, etc.). + +## Available backends + +A backend or ML engine is an application that parses the ML model, loads the inputs, runs them and returns the output. There are multiple backends like PyTorch, [Tensorflow](https://www.tensorflow.org/) (and [Lite version]((https://www.tensorflow.org/lite))), [ONNX](https://onnxruntime.ai/) and [OpenVINO™](https://docs.openvino.ai/). + +Currently, Wasm Workers Server only supports [OpenVINO™](https://docs.openvino.ai/) as ML inference engine or backend. The community is actively working on adding support for more backends, so you may expect new backends in the future. + +## Prerequisites + +### Install OpenVINO + +Install the [OpenVINO™ Runtime (2023.0.1)](https://docs.openvino.ai/2023.0): + + * [Windows](https://docs.openvino.ai/2023.0/openvino_docs_install_guides_installing_openvino_from_archive_windows.html) + * [Linux](https://docs.openvino.ai/2023.0/openvino_docs_install_guides_installing_openvino_from_archive_linux.html) + * [MacOS](https://docs.openvino.ai/2023.0/openvino_docs_install_guides_installing_openvino_from_archive_macos.html) + +Configure the OpenVINO™ environment: + + * [Windows](https://docs.openvino.ai/2023.0/openvino_docs_install_guides_installing_openvino_from_archive_windows.html#step-2-configure-the-environment) + * [Linux](https://docs.openvino.ai/2023.0/openvino_docs_install_guides_installing_openvino_from_archive_linux.html#step-2-configure-the-environment) + * [MacOS](https://docs.openvino.ai/2023.0/openvino_docs_install_guides_installing_openvino_from_archive_macos.html#step-2-configure-the-environment) + +## Run ML inference in a worker + +By default, workers cannot access the WASI-NN bindings. You need to configure it using the worker configuration file. For that, create a TOML file with the same name as the worker (like `index.wasm` and `index.toml`), and configure the WASI-NN feature: + +```toml +name = "wasi-nn" +version = "1" + +[features] +[features.wasi_nn] +allowed_backends = ["openvino"] + +[[folders]] +from = "./_models" +to = "/tmp/model" +``` + +In this specific configuration, we assume you are mounting a `_models` folder that contains your ML models. You need to adapt it to your specific case. + +### Example + +You can find a [full working example in the project repository](https://github.com/vmware-labs/wasm-workers-server/tree/main/examples/rust-wasi-nn). In this example, you have a worker that returns a website to upload an image. When you upload it, a second worker retrieves the image and runs a [MobileNet](https://arxiv.org/abs/1704.04861) ML model to classify the content of the image. + +We recommend to check this example to get started with ML and Wasm Workers Server. + +The sample application showing an image with a dog. The model predicts the image contains a 'Labrador retriever' with high confidence + +## Language compatibility + +| Language | Machine learning inference | +| --- | --- | +| JavaScript | ❌ | +| Rust | ✅ | +| Go | ❌ | +| Ruby | ❌ | +| Python | ❌ | diff --git a/docs/docs/features/mount-folders.md b/docs/docs/features/mount-folders.md index ba34c466..10b169be 100644 --- a/docs/docs/features/mount-folders.md +++ b/docs/docs/features/mount-folders.md @@ -1,4 +1,12 @@ -# Mount folders +--- +title: Mount folders +--- + +:::info + +[Available since v1.1](https://github.com/vmware-labs/wasm-workers-server/releases/tag/v1.1.0) + +::: Wasm Workers Server allows you to mount folders in the workers' execution context so they can access the files inside. This configuration is done through the `TOML` file associated to a worker (a `TOML` file with the same filename as the worker). **This means every worker has its own set of mount folders**. diff --git a/docs/docs/features/multiple-language-runtimes.md b/docs/docs/features/multiple-language-runtimes.md index 708ecd70..7af897cd 100644 --- a/docs/docs/features/multiple-language-runtimes.md +++ b/docs/docs/features/multiple-language-runtimes.md @@ -1,8 +1,13 @@ --- +title: Multiple language runtimes sidebar_position: 2 --- -# Multiple language runtimes +:::info + +[Available since v1.0](https://github.com/vmware-labs/wasm-workers-server/releases/tag/v1.0.0) + +::: Wasm Workers Server allows you to extend the supported languages by adding new language runtimes. In other words, you can run workers based on languages like Python or Ruby. @@ -163,4 +168,4 @@ wws runtimes list --repo-name=my-repo --repo-url=https://example.com/index.toml wws runtimes install ruby 3.2.0 --repo-name=my-repo --repo-url=https://example.com/index.toml ``` -After installing a language runtime, the repository information is also stored in the `.wws.toml` file. Developers that install the required language runtimes for an existing project will download them always from the right repository. \ No newline at end of file +After installing a language runtime, the repository information is also stored in the `.wws.toml` file. Developers that install the required language runtimes for an existing project will download them always from the right repository. diff --git a/docs/docs/features/static-assets.md b/docs/docs/features/static-assets.md index 5efd98f5..b7f83dfa 100644 --- a/docs/docs/features/static-assets.md +++ b/docs/docs/features/static-assets.md @@ -1,8 +1,13 @@ --- +title: Static assets sidebar_position: 5 --- -# Static assets +:::info + +[Available since v0.6](https://github.com/vmware-labs/wasm-workers-server/releases/tag/v0.6.0) + +::: Wasm Workers Server allows you to serve any static asset required by your workers. For that, place any static asset in a `public` folder. It must be present in the root of the directory you're serving with `wws`. diff --git a/docs/static/img/docs/features/wasi-nn.webp b/docs/static/img/docs/features/wasi-nn.webp new file mode 100644 index 0000000000000000000000000000000000000000..c20853f0d824d5d0134be373147fbf701e220dbc GIT binary patch literal 25026 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�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 { + if (request.method != "GET") { + // Don't allow other methods. + // Here you can see how to return a custom status + return new Response("Method not allowed", { + status: 405 + }); + } + + // Body response + const body = ` + + Wasm Workers Server + + + + + + +
+

Hello from Wasm Workers Server 👋

+

Upload an image to detect the content!

+

+
+ +
+
+
+
+
-
+
+
+
+
-
+
+
+
+
-
+
+
+
+
-
+
+
+
+
-
+
+
+
+ +`; + + // Build a new response + let response = new Response(body); + + // Add a new header + response.headers.set("x-generated-by", "wasm-workers-server"); + + return response; +} + +// Subscribe to the Fetch event +addEventListener("fetch", event => { + return event.respondWith(reply(event.request)); +}); diff --git a/examples/rust-wasi-nn/inference.toml b/examples/rust-wasi-nn/inference.toml new file mode 100644 index 00000000..a3e2765c --- /dev/null +++ b/examples/rust-wasi-nn/inference.toml @@ -0,0 +1,17 @@ +name = "wasi-nn-mobilenet" +version = "1" + +[vars] +MODEL = "model" + +[features] +[features.wasi_nn] +allowed_backends = ["openvino"] + +[[folders]] +from = "./_models" +to = "/tmp/model" + +[[folders]] +from = "./_images" +to = "/tmp/images" diff --git a/examples/rust-wasi-nn/prepare.sh b/examples/rust-wasi-nn/prepare.sh new file mode 100755 index 00000000..ba3e65b1 --- /dev/null +++ b/examples/rust-wasi-nn/prepare.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +MODEL_GITHUB=https://github.com/intel/openvino-rs/tree/main/crates/openvino/tests/fixtures/mobilenet +MODEL=https://github.com/intel/openvino-rs/raw/main/crates/openvino/tests/fixtures/mobilenet + +echo "Downloading the model from ${MODEL_GITHUB}" + +wget --no-clobber $MODEL/mobilenet.bin --output-document=_models/model.bin +wget --no-clobber $MODEL/mobilenet.xml --output-document=_models/model.xml + +echo "Finished!" diff --git a/examples/rust-wasi-nn/public/main.css b/examples/rust-wasi-nn/public/main.css new file mode 100644 index 00000000..b77873b9 --- /dev/null +++ b/examples/rust-wasi-nn/public/main.css @@ -0,0 +1,84 @@ +body { + max-width: 1000px; +} + +main { + margin: 5rem 0; +} + +h1, +p { + text-align: center; +} + +h1 { + margin-bottom: 2rem; +} + +pre { + font-size: .9rem; +} + +pre>code { + padding: 2rem; +} + +p { + margin-top: 2rem; +} + +input { + margin: 0 auto; + width: auto; +} + +.image { + width: 500px; + height: 224px; + border-radius: 5px; + background-color: #151f27; + margin: 2rem auto 1rem; + text-align: center; +} + +#image { + display: inline-block; + height: calc(224px - 2rem); + border-radius: 5px; + margin: 1rem; + width: auto; +} + +.results { + margin: 2rem auto; + width: 500px; +} + +.result { + margin-top: 1.25rem; +} + +.result_progress { + --progress: 0%; + position: relative; + height: 5px; + border-radius: 5px; + width: 100%; + background-color: #151f27; +} + +.result_progress::after { + background-color: blueviolet; + content: ""; + border-radius: 5px; + height: 5px; + width: var(--progress); + position: absolute; + top: 0; + left: 0; +} + +.result_label { + margin-top: .5rem; + font-style: italic; +} diff --git a/examples/rust-wasi-nn/public/main.js b/examples/rust-wasi-nn/public/main.js new file mode 100644 index 00000000..1ead93a5 --- /dev/null +++ b/examples/rust-wasi-nn/public/main.js @@ -0,0 +1,48 @@ +document.getElementById('file') + .addEventListener('change', getFile) + +function getFile(event) { + const input = event.target + if ('files' in input && input.files.length > 0) { + runInference(input.files[0]) + } +} + +function runInference(file) { + readFileContent(file).then(content => { + let result = btoa(content); + + // Set the image + document + .getElementById("image") + .src = `data:image/jpeg;base64,${result}`; + + fetch("/inference", { + method: "POST", + body: result + }).then(res => res.json()) + .then(json => setResults(json)); + + }).catch(error => console.log(error)) +} + +function setResults(json) { + for (let i = 0; i < 5; i++) { + let value = json.data[i]; + let res = document.getElementById(`result-${i}`); + let progress = res.querySelector(".result_progress"); + let label = res.querySelector(".result_label"); + + label.textContent = value[0]; + progress.style.setProperty("--progress", `${Math.max(value[1] * 100, 1)}%`); + } +} + +function readFileContent(file) { + const reader = new FileReader() + return new Promise((resolve, reject) => { + reader.onload = event => resolve(event.target.result) + reader.onerror = error => reject(error) + reader.readAsBinaryString(file) + }) +} diff --git a/examples/rust-wasi-nn/src/main.rs b/examples/rust-wasi-nn/src/main.rs new file mode 100644 index 00000000..7edf6ca1 --- /dev/null +++ b/examples/rust-wasi-nn/src/main.rs @@ -0,0 +1,111 @@ +use anyhow::Result; +use base64::{engine::general_purpose, Engine as _}; +use image2tensor::{ColorOrder, TensorType as ImageTensorType}; +use serde::{Deserialize, Serialize}; +use std::env; +use std::fs; +use std::fs::File; +use std::io::BufReader; +use wasi_nn::{ExecutionTarget, GraphBuilder, GraphEncoding, TensorType}; +use wasm_workers_rs::{ + http::{self, Request, Response}, + worker, Content, +}; + +/// Result labels +#[derive(Deserialize)] +pub struct Dataset { + pub names: Vec, +} + +// A wrapper for label and probability +#[derive(Debug, PartialEq, Serialize, Clone)] +pub struct InferenceResult(String, f32); + +pub fn inference(path: &str, labels: &Dataset) -> Result, wasi_nn::Error> { + let model = env::var("MODEL").unwrap(); + + let model_xml = format!("/tmp/model/{model}.xml"); + let model_bin = format!("/tmp/model/{model}.bin"); + + let input_dim = vec![1, 3, 224, 224]; + let mut output_buffer = vec![0f32; 1001]; + + let graph = GraphBuilder::new(GraphEncoding::Openvino, ExecutionTarget::CPU) + .build_from_files([&model_xml, &model_bin]) + .unwrap(); + + eprintln!("Load graph"); + let mut ctx = graph.init_execution_context()?; + eprintln!("Init execution context"); + + let width: u32 = 224; + let height: u32 = 224; + + let bytes = image2tensor::convert_image_to_bytes( + path, + width, + height, + ImageTensorType::F32, + ColorOrder::BGR, + ) + .unwrap(); + + ctx.set_input(0, TensorType::F32, &input_dim, &bytes)?; + + // Do the inference. + ctx.compute()?; + eprintln!("Run graph inference"); + + // Retrieve the output. + ctx.get_output(0, &mut output_buffer)?; + + Ok(sort_results(&output_buffer, labels)) +} + +// Sort the buffer of probabilities. +fn sort_results(buffer: &[f32], labels: &Dataset) -> Vec { + let mut results: Vec = buffer + .iter() + // In this specific case, the inference probabilities start at index 1. + // TODO: research more about where this issue may come from. + .skip(1) + .enumerate() + .map(|(c, p)| InferenceResult(labels.names.get(c).unwrap().to_string(), *p)) + .collect(); + results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + results +} + +#[derive(Serialize)] +struct Results { + data: Vec, +} + +#[worker] +fn handler(req: Request) -> Result> { + let body = req.body(); + + let image = general_purpose::STANDARD.decode(&body).unwrap(); + // Save the image for image2tensor + // TODO: Avoid this step by processing the bytes directly + fs::write("/tmp/images/image.jpg", &image).unwrap(); + + let labels_file = File::open("/tmp/model/dataset.json").unwrap(); + let reader = BufReader::new(labels_file); + let labels: Dataset = serde_json::from_reader(reader).unwrap(); + + let result = inference("/tmp/images/image.jpg", &labels).unwrap(); + + // Applied changes here to use the Response method. This requires changes + // on signature and how it returns the data. + let results = Results { + data: result[..10].to_vec().into(), + }; + let response = serde_json::to_string(&results).unwrap(); + + Ok(http::Response::builder() + .status(200) + .header("x-generated-by", "wasm-workers-server") + .body(response.into())?) +}