diff --git a/.github/workflows/image-classification.yml b/.github/workflows/image-classification.yml new file mode 100644 index 0000000000..4ddf8abc8c --- /dev/null +++ b/.github/workflows/image-classification.yml @@ -0,0 +1,44 @@ +name: image-classification + +on: + push: + branches: + - master + pull_request: + paths: + - rust/image-classification/** + - .github/workflows/image-classification.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust-image-classification: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.0 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Install wasm32-wasi target + run: rustup target add wasm32-wasi + - name: Install wasi2ic + run: | + git clone https://github.com/wasm-forge/wasi2ic /tmp/wasi2ic + cargo install --path /tmp/wasi2ic --locked + - name: Install wasm-opt + run: | + version=117 + curl -fsSLO "https://github.com/WebAssembly/binaryen/releases/download/version_${version}/binaryen-version_${version}-x86_64-linux.tar.gz" + tar -xzf "binaryen-version_${version}-x86_64-linux.tar.gz" --strip-components 1 -C /usr/local + rm "binaryen-version_${version}-x86_64-linux.tar.gz" + - name: Download model + working-directory: rust/image-classification + run: ./download_model.sh + - name: Deploy and test + working-directory: rust/image-classification + run: | + icp network start -d + icp deploy + make test diff --git a/.github/workflows/rust-image-classification-example.yaml b/.github/workflows/rust-image-classification-example.yaml deleted file mode 100644 index a3730e3e6c..0000000000 --- a/.github/workflows/rust-image-classification-example.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: rust-image-classification -on: - push: - branches: - - master - pull_request: - paths: - - rust/image-classification/** - - .github/workflows/provision-darwin.sh - - .github/workflows/provision-linux.sh - - .github/workflows/rust-image-classification-example.yaml - - .ic-commit -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - rust-image-classification-example-darwin: - runs-on: macos-15 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Darwin - run: bash .github/workflows/provision-darwin.sh - - name: Remove networks.json - run: rm -f ~/.config/dfx/networks.json - - name: Rust Image Classification Darwin - run: | - dfx start --background - pushd rust/image-classification - make test - popd - rust-image-classification-example-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Linux - run: bash .github/workflows/provision-linux.sh - - name: Remove networks.json - run: rm -f ~/.config/dfx/networks.json - - name: Rust Image Classification Linux - run: | - dfx start --background - pushd rust/image-classification - make test - popd diff --git a/rust/image-classification/.gitignore b/rust/image-classification/.gitignore index 6273b8366a..b56729fef1 100644 --- a/rust/image-classification/.gitignore +++ b/rust/image-classification/.gitignore @@ -1,5 +1,3 @@ -.dfx/ -build/ node_modules/ dist/ .DS_Store @@ -8,5 +6,5 @@ _MACOSX target/ *.old.did .idea -src/backend/assets/mobilenetv2-7.onnx -.env +backend/assets/mobilenetv2-7.onnx +frontend/src/bindings/ diff --git a/rust/image-classification/Cargo.toml b/rust/image-classification/Cargo.toml index 4c16df46ee..d1e49e317a 100644 --- a/rust/image-classification/Cargo.toml +++ b/rust/image-classification/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["src/backend"] +members = ["backend"] resolver = "2" diff --git a/rust/image-classification/Makefile b/rust/image-classification/Makefile index d953551236..8a533f3e34 100644 --- a/rust/image-classification/Makefile +++ b/rust/image-classification/Makefile @@ -1,38 +1,8 @@ -.PHONY: all -all: build - -.PHONY: download_model -.SILENT: download_model -download_model: - bash ./download_model.sh - -.PHONY: node_modules -.SILENT: node_modules -node_modules: - npm install - -.PHONY: build -.SILENT: build -build: node_modules download_model - dfx canister create --all - dfx build - -.PHONY: install -.SILENT: install -install: build - dfx deploy --yes - -.PHONY: upgrade -.SILENT: upgrade -upgrade: build - dfx canister install --all --mode=upgrade - .PHONY: test -.SILENT: test -test: install - dfx canister call backend run | grep -w 'tractor' && echo 'PASS' -.PHONY: clean -.SILENT: clean -clean: - rm -fr .dfx +test: + @echo "=== Test 1: run() classifies the built-in test image ===" + @result=$$(icp canister call --query backend run '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'tractor' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/image-classification/README.md b/rust/image-classification/README.md index 49432d7d63..4872a90288 100644 --- a/rust/image-classification/README.md +++ b/rust/image-classification/README.md @@ -1,33 +1,64 @@ # ICP image classification -This is an ICP smart contract that accepts an image from the user and runs image classification inference. +This example demonstrates running an ONNX machine-learning model inside an ICP canister. +The smart contract accepts an image from the user and runs image classification inference using the [Tract ONNX inference engine](https://github.com/sonos/tract) with the [MobileNet v2-7 model](https://github.com/onnx/models/tree/main/validated/vision/classification/mobilenet). + +The example uses the WASI polyfill to run Tract (which relies on POSIX file I/O) inside the deterministic ICP runtime, and Wasm SIMD instructions for faster inference. + The smart contract consists of two canisters: -- the backend canister embeds the [the Tract ONNX inference engine](https://github.com/sonos/tract) with [the MobileNet v2-7 model](https://github.com/onnx/models/tree/main/validated/vision/classification/mobilenet). - It provides `classify()` and `classify_query()` endpoints for the frontend code to call. - The former endpoint is used for replicated execution (running on all nodes) whereas the latter runs only on a single node. -- the frontend canister contains the Web assets such as HTML, JS, CSS that are served to the browser. +- **backend** — embeds the Tract ONNX inference engine with the MobileNet v2-7 model. + It provides `classify()` and `classify_query()` endpoints: + the former runs under replicated execution (all nodes), the latter runs on a single node as a query call. +- **frontend** — serves the web UI (HTML/JS/CSS) from which users upload images and view results. + +## Build and deploy from the command line + +### Prerequisites -This example uses Wasm SIMD instructions that are available in `dfx` version `0.20.2-beta.0` or newer. +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- wasi2ic: follow https://github.com/wasm-forge/wasi2ic and ensure `wasi2ic` is in your `$PATH` +- wasm-opt: `cargo install wasm-opt` +- Rust target `wasm32-wasi`: `rustup target add wasm32-wasi` -## Prerequisites +### Install -- [x] Install the [IC - SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install). For local testing, `dfx >= 0.22.0` is required. -- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples` -- [x] Install WASI SDK 21: - - [x] Install `wasi-skd-21.0` from https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-21 - - [x] Export `CC_wasm32_wasi` in your shell such that it points to WASI clang and sysroot. Example: `export CC_wasm32_wasi="/path/to/wasi-sdk-21.0/bin/clang --sysroot=/path/to/wasi-sdk-21.0/share/wasi-sysroot` -- [x] Install `wasi2ic`: Follow the steps in https://github.com/wasm-forge/wasi2ic and make sure that `wasi2ic` binary is in your `$PATH`. -- [x] Download MobileNet v2-7 to `src/backend/assets/mobilenetv2-7.onnx`: `./downdload_model.sh` -- [x] Install `wasm-opt`: `cargo install wasm-opt` +```bash +git clone https://github.com/dfinity/examples +cd examples/rust/image-classification +``` -## Build the application +Download the MobileNet v2-7 model: +```bash +./download_model.sh ``` -dfx start --background -dfx deploy + +### Deploy and test + +```bash +icp network start -d +icp deploy +make test +icp network stop ``` -If the deployment is successful, the it will show the `frontend` URL. -Open that URL in browser to interact with the smart contract. +If the deployment is successful, the CLI will print the frontend URL. +Open that URL in a browser to interact with the smart contract. + +For frontend development with hot reload: + +```bash +npm run dev --prefix frontend +``` + +## Updating the Candid interface + +```bash +icp build backend && candid-extractor target/wasm32-wasi/release/backend.wasm > backend/backend.did +``` + +## Security considerations and best practices + +Refer to the [ICP security best practices](https://docs.internetcomputer.org/guides/security/overview) for guidance on securing your canister. diff --git a/rust/image-classification/src/backend/Cargo.toml b/rust/image-classification/backend/Cargo.toml similarity index 100% rename from rust/image-classification/src/backend/Cargo.toml rename to rust/image-classification/backend/Cargo.toml diff --git a/rust/image-classification/src/backend/assets/logo_transparent.png b/rust/image-classification/backend/assets/logo_transparent.png similarity index 100% rename from rust/image-classification/src/backend/assets/logo_transparent.png rename to rust/image-classification/backend/assets/logo_transparent.png diff --git a/rust/image-classification/src/backend/assets/man_on_ferrari_1975.png b/rust/image-classification/backend/assets/man_on_ferrari_1975.png similarity index 100% rename from rust/image-classification/src/backend/assets/man_on_ferrari_1975.png rename to rust/image-classification/backend/assets/man_on_ferrari_1975.png diff --git a/rust/image-classification/src/backend/backend.did b/rust/image-classification/backend/backend.did similarity index 100% rename from rust/image-classification/src/backend/backend.did rename to rust/image-classification/backend/backend.did diff --git a/rust/image-classification/src/backend/src/lib.rs b/rust/image-classification/backend/src/lib.rs similarity index 100% rename from rust/image-classification/src/backend/src/lib.rs rename to rust/image-classification/backend/src/lib.rs diff --git a/rust/image-classification/src/backend/src/onnx.rs b/rust/image-classification/backend/src/onnx.rs similarity index 100% rename from rust/image-classification/src/backend/src/onnx.rs rename to rust/image-classification/backend/src/onnx.rs diff --git a/rust/image-classification/build.sh b/rust/image-classification/build.sh deleted file mode 100755 index adb25ebfa0..0000000000 --- a/rust/image-classification/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -ex - -export RUSTFLAGS=$RUSTFLAGS' -C target-feature=+simd128' -cargo build --release --target=wasm32-wasi -wasi2ic ./target/wasm32-wasi/release/backend.wasm ./target/wasm32-wasi/release/backend-ic.wasm -wasm-opt -Os -o ./target/wasm32-wasi/release/backend-ic.wasm \ - ./target/wasm32-wasi/release/backend-ic.wasm diff --git a/rust/image-classification/dfx.json b/rust/image-classification/dfx.json deleted file mode 100644 index e5ee40ef0d..0000000000 --- a/rust/image-classification/dfx.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "canisters": { - "backend": { - "metadata": [ - { - "name": "candid:service" - } - ], - "candid": "src/backend/backend.did", - "package": "backend", - "type": "custom", - "wasm": "target/wasm32-wasi/release/backend-ic.wasm", - "build": [ "bash build.sh" ] - }, - "frontend": { - "dependencies": [ - "backend" - ], - "frontend": { - "entrypoint": "src/frontend/src/index.html" - }, - "source": [ - "src/frontend/assets", - "dist/frontend/" - ], - "type": "assets" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} \ No newline at end of file diff --git a/rust/image-classification/src/frontend/assets/.ic-assets.json5 b/rust/image-classification/frontend/assets/.ic-assets.json5 similarity index 100% rename from rust/image-classification/src/frontend/assets/.ic-assets.json5 rename to rust/image-classification/frontend/assets/.ic-assets.json5 diff --git a/rust/image-classification/src/frontend/assets/default.png b/rust/image-classification/frontend/assets/default.png similarity index 100% rename from rust/image-classification/src/frontend/assets/default.png rename to rust/image-classification/frontend/assets/default.png diff --git a/rust/image-classification/src/frontend/assets/favicon.ico b/rust/image-classification/frontend/assets/favicon.ico similarity index 100% rename from rust/image-classification/src/frontend/assets/favicon.ico rename to rust/image-classification/frontend/assets/favicon.ico diff --git a/rust/image-classification/src/frontend/assets/loader.svg b/rust/image-classification/frontend/assets/loader.svg similarity index 100% rename from rust/image-classification/src/frontend/assets/loader.svg rename to rust/image-classification/frontend/assets/loader.svg diff --git a/rust/image-classification/src/frontend/assets/logo_transparent.png b/rust/image-classification/frontend/assets/logo_transparent.png similarity index 100% rename from rust/image-classification/src/frontend/assets/logo_transparent.png rename to rust/image-classification/frontend/assets/logo_transparent.png diff --git a/rust/image-classification/src/frontend/assets/main.css b/rust/image-classification/frontend/assets/main.css similarity index 100% rename from rust/image-classification/src/frontend/assets/main.css rename to rust/image-classification/frontend/assets/main.css diff --git a/rust/image-classification/src/frontend/src/index.html b/rust/image-classification/frontend/index.html similarity index 73% rename from rust/image-classification/src/frontend/src/index.html rename to rust/image-classification/frontend/index.html index c3f25f9ba6..8258f6a02b 100644 --- a/rust/image-classification/src/frontend/src/index.html +++ b/rust/image-classification/frontend/index.html @@ -6,8 +6,9 @@