From ad69c55f540ead6a999e4c90576dc1b1e1abf9a9 Mon Sep 17 00:00:00 2001 From: Mikhail Klimenko Date: Thu, 17 Sep 2026 12:04:00 +0200 Subject: [PATCH 1/6] Initial doc cleanup --- .github/pull_request_template.md | 5 +- .github/workflows/CLAUDE.md | 7 - .github/workflows/README.md | 192 +- .github/workflows/ci.yml | 4 + .github/workflows/docs-pr.yml | 34 +- .github/workflows/docs.yml | 41 +- .github/workflows/wpt-conformance-nightly.yml | 13 + .gitignore | 7 + AGENTS.md | 840 +-- CHANGELOG.md | 54 +- Cargo.lock | 28 + Cargo.toml | 10 +- MANIFEST.in | 29 - Makefile | 12 +- README.md | 284 +- TODO.txt | 577 +- docs/architecture/overview.md | 387 +- .../coreml-fixes-session-2025-12-14.md | 5 + .../coreml-weight-files.md | 5 + .../float16-investigation.md | 5 + .../ggml-integration-plan.md} | 5 + .../archive/minilm-demo-readme.md | 5 + .../operator-enum-refactor-execution.md | 5 + .../performance-benchmarks-2025-12.md} | 5 + docs/{ => archive}/plan_onnx_to_webnn_fix.md | 5 + docs/{ => archive}/quantized-support-plan.md | 5 + docs/archive/tensorrt-integration-plan.md | 855 +++ docs/archive/todo-2025-12.txt | 572 ++ .../trtx-development-guide-2026-01.md} | 5 + .../trtx-implementation-status-2026-01.md} | 5 + .../webnn_static_shapes_limitation.md | 5 + docs/development/CLAUDE.md | 14 - docs/development/backend-operator-support.md | 310 +- docs/development/code-coverage.md | 26 +- docs/development/documentation-policy.md | 79 + docs/development/flexible-input-shapes.md | 60 +- docs/development/implementation-status.md | 587 +- docs/development/setup.md | 521 +- docs/index.md | 186 +- docs/integration/tensorrt.md | 939 +-- docs/integration/windows-tensorrt-setup.md | 428 +- docs/reference/ipc-design.md | 44 +- docs/requirements.txt | 1 - docs/testing/wpt-test-guide.md | 31 +- docs/user-guide/advanced.md | 626 +- docs/user-guide/api-reference.md | 1350 +--- docs/user-guide/backends.md | 103 + docs/user-guide/examples.md | 648 +- docs/user-guide/getting-started.md | 349 +- examples/fast_style_transfer_builder_api.rs | 2 +- examples/images/CLAUDE.md | 7 - mkdocs.yml | 37 +- pytest.ini | 22 - rust-toolchain.toml | 3 + scripts/generate_backend_operator_report.py | 395 +- scripts/git-hooks/pre-commit | 13 +- scripts/install-git-hooks.sh | 4 +- .../test_generate_backend_operator_report.py | 85 +- site/404.html | 1299 ---- site/architecture/overview/index.html | 2120 ------ site/assets/_mkdocstrings.css | 237 - site/assets/images/favicon.png | Bin 1870 -> 0 bytes .../assets/javascripts/bundle.e71a0d61.min.js | 16 - .../javascripts/bundle.e71a0d61.min.js.map | 7 - .../javascripts/lunr/min/lunr.ar.min.js | 1 - .../javascripts/lunr/min/lunr.da.min.js | 18 - .../javascripts/lunr/min/lunr.de.min.js | 18 - .../javascripts/lunr/min/lunr.du.min.js | 18 - .../javascripts/lunr/min/lunr.el.min.js | 1 - .../javascripts/lunr/min/lunr.es.min.js | 18 - .../javascripts/lunr/min/lunr.fi.min.js | 18 - .../javascripts/lunr/min/lunr.fr.min.js | 18 - .../javascripts/lunr/min/lunr.he.min.js | 1 - .../javascripts/lunr/min/lunr.hi.min.js | 1 - .../javascripts/lunr/min/lunr.hu.min.js | 18 - .../javascripts/lunr/min/lunr.hy.min.js | 1 - .../javascripts/lunr/min/lunr.it.min.js | 18 - .../javascripts/lunr/min/lunr.ja.min.js | 1 - .../javascripts/lunr/min/lunr.jp.min.js | 1 - .../javascripts/lunr/min/lunr.kn.min.js | 1 - .../javascripts/lunr/min/lunr.ko.min.js | 1 - .../javascripts/lunr/min/lunr.multi.min.js | 1 - .../javascripts/lunr/min/lunr.nl.min.js | 18 - .../javascripts/lunr/min/lunr.no.min.js | 18 - .../javascripts/lunr/min/lunr.pt.min.js | 18 - .../javascripts/lunr/min/lunr.ro.min.js | 18 - .../javascripts/lunr/min/lunr.ru.min.js | 18 - .../javascripts/lunr/min/lunr.sa.min.js | 1 - .../lunr/min/lunr.stemmer.support.min.js | 1 - .../javascripts/lunr/min/lunr.sv.min.js | 18 - .../javascripts/lunr/min/lunr.ta.min.js | 1 - .../javascripts/lunr/min/lunr.te.min.js | 1 - .../javascripts/lunr/min/lunr.th.min.js | 1 - .../javascripts/lunr/min/lunr.tr.min.js | 18 - .../javascripts/lunr/min/lunr.vi.min.js | 1 - .../javascripts/lunr/min/lunr.zh.min.js | 1 - site/assets/javascripts/lunr/tinyseg.js | 206 - site/assets/javascripts/lunr/wordcut.js | 6708 ----------------- .../workers/search.7a47a382.min.js | 42 - .../workers/search.7a47a382.min.js.map | 7 - site/assets/stylesheets/main.618322db.min.css | 1 - .../stylesheets/main.618322db.min.css.map | 1 - .../stylesheets/palette.ab4e12ef.min.css | 1 - .../stylesheets/palette.ab4e12ef.min.css.map | 1 - .../implementation-status/index.html | 3034 -------- site/development/setup/index.html | 2598 ------- site/index.html | 1679 ----- site/integration/ggml/index.html | 2918 ------- site/integration/tensorrt/index.html | 3304 -------- .../windows-tensorrt-setup/index.html | 2325 ------ .../index.html | 1868 ----- .../coreml-weight-files/index.html | 1797 ----- .../float16-investigation/index.html | 1687 ----- site/objects.inv | Bin 138 -> 0 bytes site/reference/ipc-design/index.html | 2411 ------ site/reference/webnn-spec/index.html | 2135 ------ site/requirements.txt | 4 - site/search/search_index.json | 1 - site/sitemap.xml | 75 - site/sitemap.xml.gz | Bin 415 -> 0 bytes .../testing/performance-benchmarks/index.html | 1986 ----- site/testing/wpt-test-guide/index.html | 2562 ------- site/user-guide/advanced/index.html | 2358 ------ site/user-guide/api-reference/index.html | 4155 ---------- site/user-guide/examples/index.html | 2438 ------ site/user-guide/getting-started/index.html | 2074 ----- src/CLAUDE.md | 33 - src/backend_selection.rs | 31 +- src/backends/caching.rs | 8 + src/backends/cann.rs | 6 + src/backends/litert.rs | 7 + src/backends/mod.rs | 17 + src/backends/ort.rs | 6 + src/backends/trtx.rs | 9 + src/converters/CLAUDE.md | 11 - src/converters/mod.rs | 9 + src/converters/onnx.rs | 8 + src/error.rs | 8 + src/executors/mod.rs | 8 + src/executors/onnx.rs | 6 + src/executors/trtx.rs | 9 +- src/graph.rs | 9 + src/graphviz.rs | 3 + src/lib.rs | 102 + src/loader.rs | 6 + src/mlcontext.rs | 75 +- src/mlcontextoptions.rs | 12 + src/mlgraphbuilder.rs | 95 +- src/operator_enums.rs | 9 + src/protos.rs | 8 +- src/runtime_checks.rs | 7 + src/shape_inference.rs | 9 +- src/validator.rs | 9 + src/webnn_json.rs | 6 + tests/run_wpt_conformance.rs | 15 + tests/test_cann_execution.rs | 2 +- tests/wpt_conformance/wpt_execute_graph.rs | 2 +- 157 files changed, 4252 insertions(+), 59516 deletions(-) delete mode 100644 .github/workflows/CLAUDE.md delete mode 100644 MANIFEST.in rename docs/{investigations => archive}/coreml-fixes-session-2025-12-14.md (96%) rename docs/{investigations => archive}/coreml-weight-files.md (95%) rename docs/{investigations => archive}/float16-investigation.md (95%) rename docs/{integration/ggml.md => archive/ggml-integration-plan.md} (97%) rename examples/README-minilm.md => docs/archive/minilm-demo-readme.md (97%) rename docs/{development => archive}/operator-enum-refactor-execution.md (96%) rename docs/{testing/performance-benchmarks.md => archive/performance-benchmarks-2025-12.md} (93%) rename docs/{ => archive}/plan_onnx_to_webnn_fix.md (97%) rename docs/{ => archive}/quantized-support-plan.md (96%) create mode 100644 docs/archive/tensorrt-integration-plan.md create mode 100644 docs/archive/todo-2025-12.txt rename docs/{converters/TRTX_DEVELOPMENT_GUIDE.md => archive/trtx-development-guide-2026-01.md} (97%) rename docs/{converters/trtx-implementation-status.md => archive/trtx-implementation-status-2026-01.md} (99%) rename docs/{ => archive}/webnn_static_shapes_limitation.md (97%) delete mode 100644 docs/development/CLAUDE.md create mode 100644 docs/development/documentation-policy.md create mode 100644 docs/user-guide/backends.md delete mode 100644 examples/images/CLAUDE.md delete mode 100644 pytest.ini delete mode 100644 site/404.html delete mode 100644 site/architecture/overview/index.html delete mode 100644 site/assets/_mkdocstrings.css delete mode 100644 site/assets/images/favicon.png delete mode 100644 site/assets/javascripts/bundle.e71a0d61.min.js delete mode 100644 site/assets/javascripts/bundle.e71a0d61.min.js.map delete mode 100644 site/assets/javascripts/lunr/min/lunr.ar.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.da.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.de.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.du.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.el.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.es.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.fi.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.fr.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.he.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.hi.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.hu.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.hy.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.it.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.ja.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.jp.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.kn.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.ko.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.multi.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.nl.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.no.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.pt.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.ro.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.ru.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.sa.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.sv.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.ta.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.te.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.th.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.tr.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.vi.min.js delete mode 100644 site/assets/javascripts/lunr/min/lunr.zh.min.js delete mode 100644 site/assets/javascripts/lunr/tinyseg.js delete mode 100644 site/assets/javascripts/lunr/wordcut.js delete mode 100644 site/assets/javascripts/workers/search.7a47a382.min.js delete mode 100644 site/assets/javascripts/workers/search.7a47a382.min.js.map delete mode 100644 site/assets/stylesheets/main.618322db.min.css delete mode 100644 site/assets/stylesheets/main.618322db.min.css.map delete mode 100644 site/assets/stylesheets/palette.ab4e12ef.min.css delete mode 100644 site/assets/stylesheets/palette.ab4e12ef.min.css.map delete mode 100644 site/development/implementation-status/index.html delete mode 100644 site/development/setup/index.html delete mode 100644 site/index.html delete mode 100644 site/integration/ggml/index.html delete mode 100644 site/integration/tensorrt/index.html delete mode 100644 site/integration/windows-tensorrt-setup/index.html delete mode 100644 site/investigations/coreml-fixes-session-2025-12-14/index.html delete mode 100644 site/investigations/coreml-weight-files/index.html delete mode 100644 site/investigations/float16-investigation/index.html delete mode 100644 site/objects.inv delete mode 100644 site/reference/ipc-design/index.html delete mode 100644 site/reference/webnn-spec/index.html delete mode 100644 site/requirements.txt delete mode 100644 site/search/search_index.json delete mode 100644 site/sitemap.xml delete mode 100644 site/sitemap.xml.gz delete mode 100644 site/testing/performance-benchmarks/index.html delete mode 100644 site/testing/wpt-test-guide/index.html delete mode 100644 site/user-guide/advanced/index.html delete mode 100644 site/user-guide/api-reference/index.html delete mode 100644 site/user-guide/examples/index.html delete mode 100644 site/user-guide/getting-started/index.html delete mode 100644 src/CLAUDE.md delete mode 100644 src/converters/CLAUDE.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 79483888..7d62621e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,6 +9,9 @@ ## Documentation -- [ ] Updated docs if behavior changed +See `docs/development/documentation-policy.md` for which page belongs to which change. + +- [ ] Updated the rustdoc comments and docs pages that describe the changed behavior (or no documented behavior changed) - [ ] If backend converter/executor operator support changed, ran `make docs-backend-ops` and committed `docs/development/backend-operator-support.md` +- [ ] `make docs-api` and `make ci-docs` pass if rustdoc or docs pages changed diff --git a/.github/workflows/CLAUDE.md b/.github/workflows/CLAUDE.md deleted file mode 100644 index adfdcb11..00000000 --- a/.github/workflows/CLAUDE.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Recent Activity - - - -*No recent activity* - \ No newline at end of file diff --git a/.github/workflows/README.md b/.github/workflows/README.md index e5d4513a..2e98d1d7 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,167 +1,29 @@ # GitHub Actions Workflows -This directory contains GitHub Actions workflows for the project. - -## PyPI Publishing Workflow - -### `publish-pypi.yml` - Build and Publish to PyPI - -Builds wheels for multiple platforms and publishes the PyWebNN package to PyPI. - -**Triggers:** -- GitHub releases (automatic publish on new release) -- Manual trigger via workflow_dispatch (with publish flag) - -**What it does:** -1. **Builds wheels** for multiple platforms: - - Linux: x86_64 and aarch64 (manylinux) - - macOS: x86_64 (Intel) and aarch64 (Apple Silicon) - - Windows: x64 and x86 -2. **Builds source distribution** (sdist) -3. **Publishes to PyPI** (only on releases or manual trigger with publish=true) - -**Setup Requirements:** - -To enable PyPI publishing, configure trusted publishing: - -1. Go to [PyPI](https://pypi.org) and create an account -2. Create a new project named `pywebnn` -3. Go to project settings → Publishing → Add trusted publisher -4. Configure the trusted publisher: - - **Owner**: your-github-username - - **Repository**: rustnn - - **Workflow**: publish-pypi.yml - - **Environment**: pypi -5. In your GitHub repository: - - Go to Settings → Environments → New environment - - Name it `pypi` - - Add protection rules as needed - -**Publishing a New Release:** - -1. Update version in `pyproject.toml` -2. Create a new Git tag: - ```bash - git tag v0.1.0 - git push origin v0.1.0 - ``` -3. Create a GitHub release from the tag -4. The workflow will automatically build and publish to PyPI - -**Manual Publishing:** - -For testing or manual releases: - -1. Go to Actions → Publish to PyPI -2. Click "Run workflow" -3. Set `publish` to `true` to actually publish (or `false` for testing builds) -4. Click "Run workflow" - -**Testing Locally:** - -Before publishing, test the build locally: - -```bash -# Build wheels -maturin build --release --features python - -# Check the built wheel -pip install target/wheels/pywebnn-*.whl - -# Test the package -python -c "import webnn; print(webnn.__version__)" -``` - -## Documentation Workflows - -### `docs.yml` - Build and Deploy Documentation - -Builds and deploys the documentation to GitHub Pages. - -**Triggers:** -- Push to `main` branch (with changes to docs, mkdocs.yml, or Python code) -- Pull requests (build only, no deploy) -- Manual trigger via workflow_dispatch - -**What it does:** -1. Builds the MkDocs documentation -2. Uploads the built site as an artifact -3. Deploys to GitHub Pages (only on main branch) - -**Setup Requirements:** - -To enable GitHub Pages deployment: - -1. Go to your repository Settings → Pages -2. Under "Build and deployment", select: - - **Source**: GitHub Actions -3. The workflow will automatically deploy on the next push to main - -### `docs-pr.yml` - Documentation PR Check - -Validates documentation on pull requests without deploying. - -**Triggers:** -- Pull requests that modify documentation files - -**What it does:** -1. Builds the documentation to check for errors -2. Checks for broken internal links -3. Comments on the PR with build status - -## Using the Workflows - -### Local Testing - -Before pushing, test your documentation locally: - -```bash -# Install dependencies -pip install -r docs/requirements.txt - -# Serve documentation locally -mkdocs serve - -# Build documentation (as CI does) -mkdocs build --strict --verbose -``` - -### Viewing Deployed Docs - -After the first successful deployment: -- Your docs will be available at: `https://your-org.github.io/rustnn/` -- The URL will be shown in the workflow run - -### Manual Deployment - -You can manually trigger documentation deployment: - -1. Go to Actions → Build and Deploy Documentation -2. Click "Run workflow" -3. Select the branch and run - -## Troubleshooting - -### Deployment Fails - -If deployment fails with permissions error: -1. Go to Settings → Actions → General -2. Under "Workflow permissions", select: - - [OK] Read and write permissions -3. Save and re-run the workflow - -### Build Fails - -Common issues: -- **Broken links**: Check that all internal links use correct paths -- **Missing files**: Ensure all referenced files exist in the docs directory -- **Markdown errors**: Validate your Markdown syntax -- **MkDocs config**: Check mkdocs.yml for syntax errors - -### Pages Not Updating - -If GitHub Pages aren't updating: -1. Check that the workflow completed successfully -2. Verify GitHub Pages is configured (Settings → Pages) -3. Wait a few minutes for cache to clear -4. Hard refresh your browser (Ctrl+Shift+R / Cmd+Shift+R) +| Workflow | Trigger | What it does | +|---|---|---| +| `ci.yml` | push, pull request | Cargo.lock consistency, `cargo fmt --check`, `cargo check` per feature (ONNX Runtime, TensorRT, LiteRT, CANN, CoreML on macOS, wasm32 with `webnn-runtime`), `cargo test --lib` on Linux and macOS plus the CANN mock, rustdoc with warnings denied (`make docs-api`), operator report drift check (`make docs-backend-ops-check`) with the generator's unit tests, MkDocs strict build, version check on release tags | +| `wpt-conformance.yml` | push, pull request | WPT conformance suites: ONNX Runtime and LiteRT on Linux (LiteRT non-blocking), CoreML on macOS; uploads JSON and HTML reports | +| `wpt-conformance-nightly.yml` | schedule, manual | Full WPT run with reports, then builds the documentation site with rustdoc under `/api/` and the conformance dashboard under `/wpt-conformance/`, and deploys to GitHub Pages | +| `snapshot-sync.yml` | weekly (Monday 03:00 UTC), manual | Regenerates PASS snapshots and expected-failure lists for LiteRT, ONNX Runtime and CoreML against the pinned WPT revision and opens a pull request with the diff | +| `rustnnpt-gate.yml` | pull request | Runs the external rustnnpt conformance runner against the PR's rustnn revision and enforces a minimum pass rate | +| `docs.yml` | push to `main` (docs, `mkdocs.yml`, `src/`, `Cargo.toml`, `Makefile`), pull request, manual | MkDocs strict build, rustdoc embedded under `/api/`, cached WPT report embedded, deploy to GitHub Pages from `main` | +| `docs-pr.yml` | pull request touching docs | MkDocs strict build, rustdoc build, link check, status comment on the PR | +| `publish.yml` | GitHub release, manual | fmt, clippy, tests, `cargo publish` to crates.io | + +## Conventions + +- The Rust version is pinned in `rust-toolchain.toml`; every workflow that installs Rust pins + the same version. Bump them together (the toolchain file lists the workflows). +- `protoc` is installed in every job; `flatc` in jobs that build the `litert-runtime` feature. +- TensorRT-RTX has no GPU runner. CI compiles the backend (`cargo check -F trtx-runtime + --all-targets`); its WPT snapshots are regenerated locally with `make wpt-sync-trtx`. +- The documentation site combines three generated parts: MkDocs pages from `docs/`, rustdoc from + `make docs-api`, and the WPT dashboard cached by the nightly workflow. Test a docs change + locally with `make ci-docs` and `make docs-api`. + +## Pages deployment + +GitHub Pages is configured with "GitHub Actions" as the source. `docs.yml` deploys on pushes to +`main`; the nightly workflow redeploys with fresh conformance data. If a deployment fails with a +permission error, check Settings -> Actions -> General -> Workflow permissions (read and write). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cbc9192..84355c1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,6 +110,10 @@ jobs: - name: Run cargo test (library only, CANN mock) run: cargo test --lib --features cann-runtime-mock + - name: Build Rust API docs (rustdoc warnings are errors) + if: runner.os == 'Linux' + run: make docs-api + - name: Check backend operator report is up to date if: runner.os == 'Linux' run: | diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index ac063c4c..d3037c36 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -5,7 +5,6 @@ on: paths: - 'docs/**' - 'mkdocs.yml' - - 'python/**' - 'scripts/embed_wpt_conformance_pages.sh' - '.github/workflows/docs*.yml' @@ -26,6 +25,29 @@ jobs: python-version: '3.11' cache: 'pip' + # Keep the pinned Rust version in sync with rust-toolchain.toml. + - name: Install Rust + uses: dtolnay/rust-toolchain@1.97.0 + + - name: Install protobuf compiler + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler + protoc --version + + - name: Install flatc + run: | + curl -sSL "https://github.com/google/flatbuffers/releases/download/v25.12.19/Linux.flatc.binary.clang++-18.zip" -o /tmp/flatc.zip + sudo unzip -o /tmp/flatc.zip flatc -d /usr/local/bin + sudo chmod +x /usr/local/bin/flatc + flatc --version + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-docs-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -34,6 +56,12 @@ jobs: - name: Check documentation build run: mkdocs build --strict --verbose + - name: Build Rust API documentation + run: make docs-api + + - name: Embed Rust API documentation + run: cp -r target/doc site/api + - name: Restore published WPT conformance report uses: actions/cache/restore@v4 with: @@ -54,10 +82,10 @@ jobs: with: script: | const fs = require('fs'); - const output = `### Documentation Build ${process.env.BUILD_STATUS === 'success' ? '✅' : '❌'} + const output = `### Documentation Build: ${process.env.BUILD_STATUS === 'success' ? 'passed' : 'failed'} ${process.env.BUILD_STATUS === 'success' - ? 'Documentation builds successfully! 🎉' + ? 'MkDocs pages and Rust API docs build successfully.' : 'Documentation build failed. Please check the logs above.'} Preview will be available after merge.`; diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ea42a844..9cd035dc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,5 +1,9 @@ name: Build and Deploy Documentation +# The published site combines three generated parts: +# - MkDocs pages from docs/ (mkdocs.yml) +# - Rust API docs from rustdoc, embedded at /api/ (make docs-api) +# - the WPT conformance dashboard cached by the nightly workflow, embedded at /wpt-conformance/ on: push: branches: @@ -7,15 +11,19 @@ on: paths: - "docs/**" - "mkdocs.yml" + - "src/**" + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + - "Makefile" - ".github/workflows/docs.yml" - "scripts/embed_wpt_conformance_pages.sh" - - "python/**" pull_request: paths: - "docs/**" - "mkdocs.yml" - "scripts/embed_wpt_conformance_pages.sh" - - "python/**" + - ".github/workflows/docs.yml" workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages @@ -42,6 +50,29 @@ jobs: python-version: "3.11" cache: "pip" + # Keep the pinned Rust version in sync with rust-toolchain.toml. + - name: Install Rust + uses: dtolnay/rust-toolchain@1.97.0 + + - name: Install protobuf compiler + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler + protoc --version + + - name: Install flatc + run: | + curl -sSL "https://github.com/google/flatbuffers/releases/download/v25.12.19/Linux.flatc.binary.clang++-18.zip" -o /tmp/flatc.zip + sudo unzip -o /tmp/flatc.zip flatc -d /usr/local/bin + sudo chmod +x /usr/local/bin/flatc + flatc --version + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-docs-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Install dependencies run: | python -m pip install --upgrade pip @@ -50,6 +81,12 @@ jobs: - name: Build documentation run: mkdocs build --strict --verbose + - name: Build Rust API documentation + run: make docs-api + + - name: Embed Rust API documentation + run: cp -r target/doc site/api + - name: Restore published WPT conformance report uses: actions/cache/restore@v4 with: diff --git a/.github/workflows/wpt-conformance-nightly.yml b/.github/workflows/wpt-conformance-nightly.yml index bc56ed36..70246672 100644 --- a/.github/workflows/wpt-conformance-nightly.yml +++ b/.github/workflows/wpt-conformance-nightly.yml @@ -48,6 +48,13 @@ jobs: sudo apt-get update sudo apt-get install -y protobuf-compiler + - name: Install flatc + run: | + curl -sSL "https://github.com/google/flatbuffers/releases/download/v25.12.19/Linux.flatc.binary.clang++-18.zip" -o /tmp/flatc.zip + sudo unzip -o /tmp/flatc.zip flatc -d /usr/local/bin + sudo chmod +x /usr/local/bin/flatc + flatc --version + - name: Install MkDocs dependencies run: pip install -r docs/requirements.txt @@ -81,6 +88,12 @@ jobs: - name: Build documentation site run: mkdocs build --strict --verbose + - name: Build Rust API documentation + run: make docs-api + + - name: Embed Rust API documentation + run: cp -r target/doc site/api + - name: Embed WPT conformance report run: bash scripts/embed_wpt_conformance_pages.sh site diff --git a/.gitignore b/.gitignore index 63233cb2..dfe69f39 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,13 @@ /target /reports/ +# MkDocs output; built by `make docs-build` and in CI +/site/ + +# Agent memory plugins write per-directory CLAUDE.md activity logs; only the root file is tracked. +**/CLAUDE.md +!/CLAUDE.md + # Python .venv* __pycache__/ diff --git a/AGENTS.md b/AGENTS.md index 9d5f4d7c..4535de7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,541 +1,153 @@ -# rustnn (rustnn) - Project Guide - -## Project Overview - -**rustnn** is a cross-platform Rust crate that implements the W3C WebNN (Web Neural Network) specification, mirroring Chromium's graph handling while adding pluggable format converters and tooling for visualization, execution, and validation. - -**Core Capabilities:** -- Validates WebNN graph descriptions from JSON files -- Converts WebNN graphs to ONNX (cross-platform) and CoreML (macOS) formats -- Executes models on various backends: TensorRT (NVIDIA GPU), ONNX Runtime (CPU/GPU), and CoreML (macOS: GPU/Neural Engine) -- Visualizes graph structures using Graphviz DOT format -- Provides CLI tool and Rust library API -- **Python bindings** are available in the separate [pywebnn](https://github.com/rustnn/pywebnn) package - -## Architecture - -### Core Components +# rustnn - Project Guide for Contributors and Coding Agents + +rustnn is a Rust implementation of the W3C WebNN API: a WebNN-style graph builder, shape +inference and validation, and execution on pluggable backends (ONNX Runtime, NVIDIA +TensorRT-RTX, Apple CoreML, LiteRT, Huawei CANN). Python bindings live in the separate +[pywebnn](https://github.com/rustnn/pywebnn) repository; this repository contains no Python API. + +Documentation map: + +| Topic | Location | +|---|---| +| Site entry point | `docs/index.md` (built with MkDocs, `make docs-build`) | +| API overview, backends, examples, advanced topics | `docs/user-guide/` | +| Architecture | `docs/architecture/overview.md` | +| Workflow, adding operations and backends, CI | `docs/development/setup.md` | +| What to update together with a code change | `docs/development/documentation-policy.md` | +| Implementation status and known gaps | `docs/development/implementation-status.md` | +| Operator support per backend (generated) | `docs/development/backend-operator-support.md` | +| WPT conformance harness | `docs/testing/wpt-test-guide.md` | +| TensorRT-RTX backend | `docs/integration/tensorrt.md` | +| Rust API reference | `make docs-api` (rustdoc), published under https://rustnn.github.io/rustnn/api/rustnn/ | + +## Read this first + +1. The source is the reference. Documentation pages describe the state at their last commit; + when a page and the code disagree, the code wins and the page is fixed in the same change. +2. Removed APIs must not come back. `src/python/`, `PyMLContext`, `context.compute(...)`, + `maturin`, the string-typed `Operation` with JSON attributes and `HashMap` argument maps are + gone. `docs/development/documentation-policy.md` maps the old names to the current API. +3. Every change updates the documentation it affects, in the same pull request. The policy page + says which page belongs to which part of the code. +4. `docs/archive/` is frozen history. Do not cite it as current and do not edit it. +5. Numbers drift. Do not write operation counts or pass rates into pages; link to the generated + report or the WPT dashboard. + +## Architecture in brief +``` +MLContext::create(&MLContextOptions) -> selects Backend / BackendDevice (src/backend_selection.rs) +MLGraphBuilder::new(&mut context) -> records GraphInfo (operands, Operation enum, constants); + shape inference runs on every builder call +builder.build(&MLNamedOperands) -> converter (src/converters/) + backend compile -> MLGraph +context.create_tensor / write_tensor -> MLTensor owned by the backend (readable / writable flags) +context.dispatch(&mut graph, &inputs, &outputs) -> binding validation, then the backend runs +context.read_tensor -> results ``` - CLI (main.rs) / Library API (lib.rs) - - - - - -Loader Validator Backend -(JSON) (graph.rs) Selection +Key modules (`docs/architecture/overview.md` has the full table): + +| Path | Content | +|---|---| +| `src/mlcontext.rs`, `src/mlcontextoptions.rs`, `src/backend_selection.rs` | WebNN context, graph and tensor types; options and hints; the selection order | +| `src/mlgraphbuilder.rs` | Builder: inputs, constants, all operation methods (macro-generated `op` and `op_with_options`), `build`, `rustnn_save_webnn` | +| `src/operators.rs`, `src/operator_options.rs`, `src/operator_enums.rs` | `Operation` enum (one variant per operation, `op_type()`), `ML*Options` structs, spec enums | +| `src/shape_inference.rs`, `src/validator.rs`, `src/runtime_checks.rs` | Shape rules, structural validation, dispatch-time binding checks | +| `src/graph.rs` | `GraphInfo`, `Operand`, `Dimension` (static or dynamic), `DataType`, 4-bit packing | +| `src/loader.rs`, `src/webnn_json.rs`, `src/webnn_save.rs` | `.webnn` text and JSON import and export through the `webnn-graph` crate, `.safetensors` weights | +| `src/converters/{onnx,coreml_mlprogram,trtx,litert,cann,webnn}.rs` | Lowering of `GraphInfo` to each backend format; `ConverterRegistry` | +| `src/backends/{ort,trtx,coreml,litert,cann}.rs`, `src/backends/caching.rs` | Backend contexts implementing the crate-private `MLBackendContext` and `MLBackendBuilder` traits; on-disk caches | +| `src/executors/` | Legacy one-shot executors used by the CLI (`--run-onnx`, `--run-trtx`, `--run-coreml`) | +| `src/main.rs` | CLI: validate, `--export-dot`, `--convert`, `--run-*` | +| `tests/run_wpt_conformance.rs`, `tests/wpt_conformance/` | WPT conformance harness (`make test-wpt*`) | +| `scripts/generate_backend_operator_report.py` | Generates `docs/development/backend-operator-support.md`; CI fails on drift | + +Backend selection from the WebNN hints: accelerated with Default or HighPerformance -> +TensorRT-RTX, CoreML GPU, LiteRT GPU, ONNX Runtime GPU then NPU; accelerated with LowPower -> +CoreML NPU, LiteRT NPU, ONNX Runtime NPU; not accelerated -> CoreML CPU, LiteRT CPU, ONNX +Runtime CPU. ONNX Runtime CPU is the last resort. CANN is only selected through +`with_rustnn_backend_hint(Backend::Cann)`. + +## Conventions + +- Names: `snake_case` files and functions, `PascalCase` types. WebNN `camelCase` methods become + `snake_case` (`reduce_sum`, `where_`). rustnn-specific API carries the `rustnn_` prefix. +- Errors: typed with `thiserror`, `Send + Sync`, with context. `rustnn::error::Error` for the + WebNN API, `GraphBuilderError` and `ShapeInferenceError` for recording, `GraphError` for the + legacy pipeline. +- Comments: one line for non-obvious decisions only, ASCII only (`->`, `<=`). No emojis anywhere + in the repository (code, documentation, commit messages); use plain markers such as `[OK]` or + `[WARNING]` when a marker is needed. +- Formatting and lints: `cargo fmt` and `cargo clippy --all-targets -- -D warnings`. CI denies + warnings, including rustdoc warnings. +- Tests: unit tests in `#[cfg(test)]` modules at the end of the file; converter tests decode the + emitted model and assert on it; the WPT corpus is the conformance oracle. +- Serde: option structs use `#[serde(rename_all = "camelCase")]` to match WebNN JSON. +- No code copied from other projects (ONNX Runtime, Chromium, TensorFlow). Re-derive from the + specifications and cite them. + +## Development commands + +Use the Make targets; they set features and environment variables. `make help` lists them. +```bash +make build # cargo build +make test # fmt, clippy -D warnings, cargo test, operator report drift check +make test-wpt # WPT conformance on ONNX Runtime CPU (needs Node.js; fetches the corpus) +make test-wpt-op OP=relu # one operation; WPT_BACKEND=onnx|trtx|litert|coreml selects a backend +make test-wpt-trtx # also test-wpt-litert, test-wpt-coreml +make wpt-sync-onnx # also wpt-sync-litert, wpt-sync-coreml, wpt-sync-trtx: regenerate baselines +make docs-backend-ops # regenerate the operator support report +make docs-api # rustdoc with -D warnings +make ci-docs # MkDocs strict build +make onnxruntime-download # ONNX Runtime for the onnx-runtime feature; then export ORT_DYLIB_PATH +``` +Feature checks CI runs: `cargo check`, `cargo check --features onnx-runtime`, +`cargo check -F trtx-runtime --all-targets`, `cargo check --features litert-runtime`, +`cargo check --features cann-runtime`, `cargo check --features coreml-runtime` (macOS), wasm32 +with `webnn-runtime`; `cargo test --lib` and `cargo test --lib --features cann-runtime-mock`. +Environment variables: `ORT_DYLIB_PATH` (ONNX Runtime library), `RUST_LOG`, `RUSTNN_DEBUG=1|2` +with `RUSTNN_DEBUG_ONNX_DIR`, `RUSTNN_TRTX_LOG_VERBOSITY`, `TRTX_JSON_DUMP_PATH`, `WPT_BACKEND`, +`WPT_DIR`. The tables in `src/lib.rs` are authoritative. - Converter - (Runtime) +## Before proposing a commit +1. `make test` passes (formatting, clippy, tests, report drift). +2. WPT for the touched operations on every backend available locally; snapshots or expected + failures regenerated and reviewed (`make wpt-sync-`). +3. `make docs-backend-ops` when a converter's operator support changed. +4. Documentation updated per `docs/development/documentation-policy.md`; `make docs-api` and + `make ci-docs` pass when rustdoc or pages changed. +5. Line endings: files are LF in the index; do not introduce CRLF. +6. Commit messages: conventional prefix (`feat(trtx):`, `fix(coreml):`, `docs:`), imperative + subject, body with the problem, the change and the validation performed. +## Adding a WebNN operation +Check the Chromium reference implementation first +(https://chromium.googlesource.com/chromium/src/+/lkgr/services/webnn/, in particular +`ort/graph_builder_ort.cc`, `coreml/graph_builder_coreml.mm` and +`tflite/graph_builder_tflite.cc`) for the expected lowering and edge cases. Then follow the +checklist in `docs/development/setup.md`: options struct, `Operation` variant and `op_type`, +shape inference with tests, builder method, `webnn_json` mapping, each converter (or an explicit +unsupported entry), WPT per backend with snapshot sync, `make docs-backend-ops`, and the +operation table in `docs/user-guide/api-reference.md`. - ONNX / CoreML - Execution +## WebNN specification reference -``` +`docs/reference/webnn-index.bs` is a cached copy of the specification source (date in +`docs/reference/README.md`). The `search-bikeshed` tool indexes the live specification: -**Note:** Python bindings are now in [pywebnn](https://github.com/rustnn/pywebnn), which uses rustnn as its core library. - -### Key Architectural Principles - -**1. Backend-Agnostic Graph Representation (WebNN Spec-Compliant)** -- `builder.build()` creates an immutable `GraphInfo` structure -- Graph representation is **platform-independent** and **backend-agnostic** -- No backend-specific artifacts at graph build time -- Same graph can be executed on multiple backends - -**2. Runtime Backend Selection (WebNN Device Selection Explainer)** -- Follows [W3C WebNN Device Selection Explainer](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md) -- Backend selection happens at **context creation** using hints, not compile-time -- `MLContext::new()` takes `accelerated` (bool) and `power_preference` (str) hints: - - `accelerated=false` → `Backend::OnnxCpu` (CPU only) - - `accelerated=true` + `power="low-power"` → NPU > GPU > CPU - - `accelerated=true` + `power="high-performance"` → TensorRT > GPU > NPU > CPU - - `accelerated=true` + `power="default"` → TensorRT > GPU > NPU > CPU -- Platform autonomously selects actual device based on availability -- Selection logic in `PyMLContext::select_backend()` (src/python/context.rs:473) -- Feature flags control availability, not selection -- Per explainer: "implementations have a better grasp of the system...control should be relinquished to them" - -**3. Lazy Backend Conversion** -- Backend conversion happens during **`compute()`**, not `build()` -- `compute()` method routes to backend-specific execution: - - `compute_trtx()` → Converts to ONNX protobuf, executes with TensorRT - - `compute_onnx()` → Converts to ONNX protobuf, executes with ONNX Runtime - - `compute_coreml()` → Converts to CoreML protobuf, executes with CoreML - - `compute_fallback()` → Returns zeros when no backend available -- Conversion is transparent to the user - -**4. Rust-First Architecture** -- All core logic implemented in pure Rust -- Python bindings are thin PyO3 wrappers -- Zero Python code in critical path (validation, conversion, execution) -- Rust library usable independently without Python - -### Key Modules - -#### **graph.rs** - Core Data Model -- `DataType`: Float32, Float16, Int32, Uint32, Int8, Uint8 -- `OperandDescriptor`: Shape and type information -- `OperandKind`: Input, Constant, Output -- `Operand`: Graph nodes with descriptors and metadata -- `Operation`: Graph operations with inputs/outputs -- `ConstantData`: Weight/constant storage (base64 encoded) -- `GraphInfo`: Complete graph representation - -**Key Convention:** Operands are referenced by their array index (u32) within the graph's operands list. - -#### **validator.rs** - Validation Pipeline -- `ContextProperties`: Validation constraints and limits -- `GraphValidator`: Validates graph structure and dependencies -- `ValidationArtifacts`: Results including I/O descriptors and operation dependencies - -**Validation Checks:** -1. Operand count limits -2. Tensor byte length limits -3. Valid input/output names -4. Constant data integrity -5. Operation dependency ordering -6. Operand usage consistency - -#### **converters/** - Pluggable Format Conversion -- **Registry Pattern**: `ConverterRegistry` manages converters dynamically -- **Trait Interface**: `GraphConverter` defines conversion contract -- **Implementations**: - - `OnnxConverter` → ONNX protobuf format - - `CoremlMlProgramConverter` → CoreML MLProgram (MIL) protobuf format - -#### **executors/** - Runtime Execution -- **Platform-specific**: Conditional compilation for macOS -- **TensorRT Runtime**: `run_trtx_with_inputs()` - NVIDIA GPU execution (Linux/Windows, with mock mode for development) -- **ONNX Runtime**: `run_onnx_with_inputs()` - executes with actual tensor I/O (cross-platform) -- **CoreML Runtime**: `run_coreml_zeroed_cached()` - macOS only via Objective-C FFI - -#### **Backend Selection** -- Backend selection follows [W3C WebNN Device Selection Explainer](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md) -- Selection based on `accelerated` (bool) and `power_preference` (str) hints -- Platform autonomously selects optimal backend based on availability -- Supports: TensorRT (NVIDIA GPU), ONNX Runtime (CPU/GPU), CoreML (macOS) - -**For Python API documentation**, see [pywebnn](https://github.com/rustnn/pywebnn) - -#### **graphviz.rs** - Visualization -- Generates DOT format for graph visualization -- Color-coded nodes: inputs (green), outputs (blue), constants (yellow) - -## Development Conventions - -### Design Principles - -**Rust-First WebNN Implementation:** -- The Rust code is a fully valid, standalone WebNN implementation -- All core functionality, validation, and graph operations exist in pure Rust -- The Rust library is independently usable without any Python dependency -- Python bindings are a convenience layer to enable easy integration with Python projects -- Python code should be minimal wrappers that expose Rust functionality -- This ensures the library can be used in pure Rust projects, CLI tools, and Python projects alike - -### Code Style - -1. **Naming:** - - Files: `snake_case.rs` - - Types: `PascalCase` - - Functions: `snake_case` - - Enums: PascalCase variants, snake_case JSON serialization - -2. **Error Handling:** - - All fallible operations return `Result` - - Use `?` operator for error propagation - - `thiserror` for error type derivation - - Include contextual information in errors - -3. **Serde Integration:** - - `#[derive(Serialize, Deserialize)]` on all data types - - `#[serde(rename_all = "snake_case")]` for JSON compatibility - - `serde_with` for base64 encoding of binary data - - Optional fields use `Option` - -4. **Testing:** - - Unit tests in `#[cfg(test)]` modules at end of files - - Use realistic data structures matching actual usage - - Test examples exist in `graphviz.rs` and `converters/mod.rs` - -5. **Formatting:** - - No emojis in code, documentation, commit messages, or any project files - - Use plain text markers: [OK], [WARNING], [INFO], [TODO], etc. - - Keep all text professional and readable in all terminals and editors - - Prioritize clarity and accessibility over visual decoration - -### Architecture Patterns - -1. **Registry Pattern** (converters): - - Trait objects: `Box` - - Dynamic registration and lookup - - Extensible without modifying core code - -2. **Builder Pattern** (protobuf construction): - - Incremental construction of complex structures - - Used in ONNX and CoreML converters - -3. **Validation Pipeline**: - - Immutable graph input - - Stateful validator with progressive checks - - Comprehensive artifacts returned for downstream use - -4. **Conditional Compilation**: - - `#[cfg(target_os = "macos")]` for platform-specific code - - `#[cfg(feature = "...")]` for optional features - - Graceful degradation on unsupported platforms - -5. **Explicit Dependencies**: - - No singletons or global state - - Pass dependencies via function parameters - - Clear data flow through the system - -### WebNN Specification Reference - -The project uses the `search-bikeshed` tool for efficient browsing and searching of the W3C WebNN specification: - -**Installation:** ```bash -# Install search-bikeshed (if not already installed) pip install search-bikeshed -``` - -**Initial Setup:** -```bash -# Index the WebNN specification (run once per session or when spec updates) search-bs index https://github.com/webmachinelearning/webnn/blob/main/index.bs --name webnn +search-bs search --name webnn "MLTensor" --around 3 +search-bs get --name webnn --line 1234 --count 40 ``` -**Common Usage:** - -1. **Basic keyword search:** - ```bash - search-bs search --name webnn "MLTensor" - ``` - -2. **Phrase search:** - ```bash - search-bs search --name webnn "graph builder" - ``` - -3. **Search with context lines:** - ```bash - # Show 3 lines before and after each match - search-bs search --name webnn "MLContext" --around 3 - ``` - -4. **JSON output for scripting:** - ```bash - search-bs search --name webnn "MLContext" --json - ``` - -5. **Show URLs in results:** - ```bash - search-bs search --name webnn "operator" --show-url --max-results 10 - ``` - -6. **Get specific line ranges:** - ```bash - # Get 40 lines starting from line 1234 - search-bs get --name webnn --line 1234 --count 40 - ``` - -7. **JSON output for line ranges:** - ```bash - search-bs get --name webnn --line 1234 --count 40 --json - ``` - -**When to Use:** -- Verifying WebNN API signatures and behavior -- Understanding operation semantics and constraints -- Checking data type support for operations -- Finding spec language for documentation -- Resolving ambiguities in implementation - -**Advantages over web browsing:** -- Fast local search without network latency -- Context-aware results with surrounding lines -- Scriptable output formats (JSON) -- Works offline after initial indexing -- Integrated into development workflow - -### File Organization - -``` -src/ - lib.rs # Public API exports - main.rs # CLI entry point - graph.rs # Core data structures - error.rs # Error types - validator.rs # Graph validation - loader.rs # JSON loading - graphviz.rs # DOT export - protos.rs # Protobuf module setup - converters/ - mod.rs # Registry and trait - onnx.rs # ONNX converter - coreml_mlprogram.rs # CoreML MLProgram (MIL) converter - executors/ - mod.rs # Conditional compilation - trtx.rs # TensorRT runtime - onnx.rs # ONNX runtime - coreml.rs # CoreML runtime - -examples/ - sample_graph.json # Sample WebNN graph - # Python examples in pywebnn repository - -tests/ - # Python tests in pywebnn repository -``` - -**For Python bindings**, see the [pywebnn repository](https://github.com/rustnn/pywebnn). - -## Adding New Features - -### Adding a New Converter - -1. **Create converter file** in `src/converters/your_format.rs` -2. **Implement the trait:** - ```rust - pub struct YourFormatConverter; - - impl GraphConverter for YourFormatConverter { - fn name(&self) -> &str { "your-format" } - fn convert(&self, graph_info: &GraphInfo) -> Result { - // Implementation - } - } - ``` -3. **Register in** `converters/mod.rs` or `main.rs`: - ```rust - registry.register(Box::new(YourFormatConverter)); - ``` -4. **Add dependencies** to `Cargo.toml` if needed -5. **Add tests** in your converter file - -### Adding a New Executor - -1. **Create executor file** in `src/executors/your_runtime.rs` -2. **Add feature gate** in `Cargo.toml`: - ```toml - [features] - your-runtime = ["dep:your-runtime-crate"] - ``` -3. **Implement execution function:** - ```rust - #[cfg(feature = "your-runtime")] - pub fn run_your_runtime(model_data: &[u8]) -> Result<(), GraphError> { - // Implementation - } - ``` -4. **Add conditional compilation** in `executors/mod.rs` -5. **Wire up in CLI** (`main.rs`) if needed - -### Adding New WebNN Operations (Standard Workflow) - -**IMPORTANT: Before implementing any new operation, always check the Chromium reference implementation first:** -- **Chromium WebNN Source**: https://chromium.googlesource.com/chromium/src/+/lkgr/services/webnn/ -- **ONNX Runtime Backend**: https://chromium.googlesource.com/chromium/src/+/lkgr/services/webnn/ort/graph_builder_ort.cc -- **CoreML Backend**: https://chromium.googlesource.com/chromium/src/+/lkgr/services/webnn/coreml/graph_builder_coreml.mm -- **Why**: Chromium is the reference implementation of W3C WebNN spec. Their implementation shows: - - Correct WebNN API signatures and behavior - - How to handle type conversions (e.g., Cast nodes for ONNX bool types) - - Edge cases and validation requirements - - Backend-specific workarounds and best practices -- **How to use**: Search for the operation name in `graph_builder_ort.cc` or `graph_builder_coreml.mm` to see their implementation approach - -**Complete implementation checklist for adding operations:** - -1. **Shape Inference** (`src/shape_inference.rs`): - - Add `infer__shape()` function - - Add Rust unit tests for shape inference - - Handle all parameter variations (layouts, axes, etc.) - -2. **Python API** (`src/python/graph_builder.rs`): - - Add method following WebNN spec signature - - Use PyO3 `#[pyo3(signature = (...))]` for optional parameters - - Create `OperandDescriptor` with `pending_permutation: Vec::new()` - - Call shape inference function - - Store operation with all parameters in `attributes` JSON - -3. **ONNX Converter** (`src/converters/onnx.rs`): - - Add operation name mapping in `onnx_op_type()` - - Add attribute handling if needed (see `create_conv2d_attributes()` example) - -4. **CoreML Converter** (`src/converters/coreml_mlprogram.rs`): - - Add MIL operation name mapping in `get_mil_op_type()` - - Add operation input handling in `create_operation_inputs()` if needed - - CoreML MLProgram uses MIL operations (more flexible than old NeuralNetwork format) - -5. **Tests** (`tests/test_python_api.py`): - - Add 3-5 tests covering: - - Basic usage - - Optional parameters (scale, bias, etc.) - - Different layouts/shapes - - Edge cases and validation - - Run: `python -m pytest tests/test_python_api.py -v` - -6. **WPT Conformance Tests** (live upstream WPT via Rust harness): - - Tests are loaded from upstream WPT `.https.any.js` files (not checked-in JSON) - - Fetch corpus: `node scripts/fetch_wpt.mjs` - - Run: `make test-wpt` or `make test-wpt-op OP=` - - See: `docs/testing/wpt-test-guide.md` - -7. **Documentation** (`docs/api-reference.md`): - - Add operation to appropriate section - - Include parameters, shape inference, formula - - Add 2-3 practical examples - - Show common use cases - -8. **Update TODO.txt**: - - Mark operation as done with implementation summary - -9. **Rebuild Python module**: - ```bash - maturin develop --features python - ``` - -10. **Run all tests before committing**: - ```bash - cargo test --lib # Rust tests - pytest tests/ -v # Python + WPT tests - cargo fmt # Format code - ``` - -**Example PR titles:** -- "Add batch_normalization operation" -- "Add element-wise operations (abs, exp, log)" -- "Add reduction operations (reduceSum, reduceMean)" - -### Adding Protobuf Definitions - -1. **Add .proto files** to `protos/your_format/` -2. **Update** `build.rs` to compile them: - ```rust - prost_build::compile_protos(&["protos/your_format/schema.proto"], &["protos/"])?; - ``` -3. **Include generated code** in `src/protos.rs`: - ```rust - pub mod your_format { - include!(concat!(env!("OUT_DIR"), "/your.format.namespace.rs")); - } - ``` - -## Development - -**IMPORTANT: Always use Make commands instead of direct cargo/maturin/pytest commands.** - -The Makefile provides consistent build targets with proper feature flags, environment setup, and dependency management. Using Make ensures builds are reproducible and properly configured. - -**WebNN conformance tests** run in-repo via `tests/run_wpt_conformance.rs` (live WPT corpus → `MLGraphBuilder` → `MLContext`). CI runs `make test-wpt` on every push and PR. - -For detailed development instructions, build commands, and troubleshooting, see **[docs/development.md](docs/development.md)**. - -Common Make targets: -```bash -make build # Build Rust library with proper features -make python-dev # Install Python package in development mode -make test # Run Rust library tests -make test-wpt # WPT conformance (~2482 cases, requires Node.js + fetch_wpt) -make test-wpt-op OP=relu # Filter WPT trials by operation name -make python-test # Run all Python tests (pywebnn) -make fmt # Format Rust code -make help # Show all available targets -``` - -Direct cargo commands (AVOID - use Make instead): -```bash -cargo build --release # DON'T USE - use `make build` instead -maturin develop --features python # DON'T USE - use `make python-dev` instead -cargo test && python -m pytest tests/ # DON'T USE - use `make test && make python-test` instead -``` - -## Dependencies - -### Core Dependencies -- **clap 4.5** - CLI argument parsing -- **serde 1.0** + **serde_json 1.0** - JSON serialization -- **serde_with 3.8** - Base64 encoding -- **thiserror 1.0** - Error derivation -- **prost 0.12** + **prost-types 0.12** - Protobuf runtime -- **bytes 1.6** - Byte buffer utilities -- **bytemuck 1.15** - Type casting - -### Optional Runtime Dependencies -- **trtx 0.2.0** - TensorRT execution (NVIDIA GPU, with mock mode for development) -- **objc 0.2** - Objective-C FFI for CoreML (macOS) -- **ort 2.0.0-rc.10** - ONNX execution (successor to onnxruntime-rs) -- **pyo3 0.22** - Python bindings (optional, with `python` feature) - -### Build Dependencies -- **prost-build 0.12** - Protobuf code generation -- **maturin** - Python package build system (for Python bindings) - -## Platform Support - -- **Validation & Conversion**: Cross-platform (Linux, macOS, Windows) -- **TensorRT Execution**: Linux/Windows with NVIDIA GPU and `trtx-runtime` feature (mock mode via `trtx-runtime-mock` for development without GPU) -- **ONNX Execution**: Cross-platform with `onnx-runtime` feature -- **CoreML Execution**: macOS only with `coreml-runtime` feature -- **Neural Engine**: macOS with Apple Silicon (via CoreML) -- **Python Bindings**: Cross-platform with `python` feature (Python 3.8+) - -## Key Technical Decisions - -1. **WebNN Device Selection Explainer**: Follows [W3C WebNN Device Selection spec](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md) for platform-autonomous device selection using hints -2. **WebNN MLTensor Explainer**: Follows [W3C WebNN MLTensor spec](https://github.com/webmachinelearning/webnn/blob/main/mltensor-explainer.md) for explicit tensor management with descriptor flags (readable, writable, exportableToGPU), destroy() for resource cleanup, and dispatch() for async execution -3. **Protobuf for interop**: Native format for ONNX and CoreML -4. **Compile-time codegen**: Protobufs compiled at build time -5. **Feature flags**: Optional runtimes to minimize dependencies -6. **Objective-C FFI**: Direct CoreML access on macOS -7. **Zero-copy where possible**: `Bytes` type for efficiency -8. **Registry pattern**: Pluggable converters without core changes - -## Future Extension Points - -- **More converters**: TensorFlow Lite, TensorRT, OpenVINO -- **More executors**: Additional backend runtimes -- **Operation typing**: Strongly-typed operation variants -- **Graph optimization**: Pre-conversion graph transformations -- **Benchmarking**: Performance measurement tools -- **Graph diff**: Compare graphs for equivalence - -## Python Integration - -Python bindings for rustnn are available in the separate **[pywebnn](https://github.com/rustnn/pywebnn)** package. - -The pywebnn package provides: -- Full W3C WebNN API implementation -- PyO3-based bindings to rustnn's core functionality -- ONNX Runtime and CoreML backend support -- NumPy array integration -- Comprehensive test suite and examples - -**Installation:** -```bash -pip install pywebnn -``` - -**Usage:** -```python -import webnn -ml = webnn.ML() -context = ml.create_context(accelerated=False) -builder = context.create_graph_builder() -# ... build and execute graphs -``` - -For complete Python API documentation, examples, and development instructions, see the [pywebnn repository](https://github.com/rustnn/pywebnn). +Use it to verify operation signatures, option dictionaries and data type constraints. ## Claude Code - Approved Permissions @@ -582,191 +194,3 @@ The following operations have been approved for Claude Code to execute without r - `WebFetch(domain:www.w3.org)` - Fetch W3C WebNN specifications These permissions enable Claude Code to efficiently assist with development, testing, documentation, version control, and CI/CD monitoring tasks without interrupting the workflow. - -## Testing Validation Requirement - -**CRITICAL: All code changes MUST be validated by running tests before committing.** - -Before creating any git commit: - -1. **Run Rust Tests:** - ```bash - cargo test --lib - ``` - - All tests must pass - - No new warnings should be introduced - -2. **Run Python Tests (if Python code changed):** - ```bash - make python-test - ``` - - All tests must pass or be explicitly skipped (when dependencies unavailable) - - Skipped tests are acceptable if the skip reason is valid (e.g., ONNX runtime not available) - -3. **Format Rust Code (if Rust code changed):** - ```bash - cargo fmt - ``` - - MUST be run after any Rust code changes - - CI will fail if code is not formatted - - This ensures consistent code style across the project - -4. **Fix Any Failures:** - - Never commit code with failing tests - - If tests fail, fix the code or update the tests - - Document any intentional test skips with clear skip conditions - -**Rationale:** Running tests catches regressions early and ensures code quality. Tests are the safety net that allows confident refactoring and feature additions. - -**Example workflow:** -```bash -# Make changes to code -vim src/python/context.rs - -# Run tests to validate -cargo test --lib -make python-test - -# If tests pass, commit -git add src/python/context.rs -git commit -m "Update context implementation" -``` - -## Implemented Operations (as of 2025-12-20) - -### Implementation Summary - -**WebNN Specification Coverage:** -- **Total Operations in Spec:** 105 -- **Fully Implemented:** 88 (84%) -- **Not Yet Implemented:** 13 (12%) -- **Intentionally Deferred:** 4 (4%) - RNN operations (gru, gruCell, lstm, lstmCell) - -**Not Yet Implemented Operations:** -- `cumulativeSum` - Element-wise cumulative sum along axis -- `gatherElements` - Gather elements using index tensor -- `gatherND` - Gather N-dimensional slices -- `isInfinite` - Check for infinite values -- `isNaN` - Check for NaN values -- `l2Pool2d` - L2 pooling (L2 norm within window) -- `linear` - Linear transformation (alpha*x + beta) -- `max` - Element-wise maximum of two tensors -- `min` - Element-wise minimum of two tensors -- `notEqual` - Element-wise inequality comparison -- `resample2d` - Resize/resample 2D tensor -- `reverse` - Reverse elements along axes -- `roundEven` - Round to nearest even integer - -### Fully Implemented Operations by Category - -**Binary Element-wise Operations (11):** -- `add`, `sub`, `mul`, `div`, `pow`, `matmul` -- `equal`, `greater`, `greaterOrEqual`, `lesser`, `lesserOrEqual` -- Full NumPy-style broadcasting support - -**Unary Element-wise Operations (28):** -- **Arithmetic:** `abs`, `ceil`, `floor`, `neg`, `reciprocal`, `sign`, `sqrt` -- **Trigonometric:** `sin`, `cos`, `tan`, `asin`, `acos`, `atan` -- **Hyperbolic:** `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh` -- **Exponential/Log:** `exp`, `log`, `erf` -- **Rounding:** `round` - -**Activation Functions (11):** -- `relu`, `sigmoid`, `tanh`, `softmax`, `softplus`, `softsign` -- `elu`, `leakyRelu`, `prelu`, `gelu` -- `hardSigmoid`, `hardSwish` - -**Convolution Operations (2):** -- `conv2d` - 2D convolution with strides, dilations, padding, groups -- `convTranspose2d` - Transposed convolution with output padding/sizes -- Supports NCHW and NHWC layouts -- Depthwise convolution via groups parameter - -**Pooling Operations (4):** -- `averagePool2d`, `maxPool2d` - 2D pooling with window, stride, dilation, padding -- `global_average_pool`, `global_max_pool` - Global pooling (reduces spatial to 1x1) -- Supports NCHW and NHWC layouts - -**Normalization Operations (3):** -- `batchNormalization` - Batch norm with mean, variance, scale, bias, epsilon -- `instanceNormalization` - Instance norm with scale, bias, epsilon -- `layerNormalization` - Layer norm with scale, bias, epsilon, axes (for transformers) - -**Reduction Operations (10):** -- `reduceSum`, `reduceMean`, `reduceMax`, `reduceMin`, `reduceProduct` -- `reduceL1`, `reduceL2`, `reduceLogSum`, `reduceLogSumExp`, `reduceSumSquare` -- All support axes parameter and keepDimensions option - -**Shape Manipulation (9):** -- `reshape`, `transpose`, `expand`, `squeeze`, `unsqueeze` -- `concat`, `split`, `slice`, `tile` - -**Indexing/Gathering (4):** -- `gather` - Gather elements along axis -- `scatterElements` - Scatter elements using indices -- `scatterND` - Scatter N-dimensional updates -- `where` - Select elements based on condition - -**Matrix Operations (2):** -- `matmul` - Matrix multiplication with batched support -- `gemm` - General matrix multiplication (C = alpha*A*B + beta*C) - -**Quantization (2):** -- `quantizeLinear` - Quantize float to integer -- `dequantizeLinear` - Dequantize integer to float - -**Other Operations (2):** -- `cast` - Type conversion between data types -- `clamp` - Clamp values to range [min, max] -- `identity` - Identity operation -- `pad` - Pad tensor with constant/edge/reflection modes -- `argMax`, `argMin` - Find indices of max/min values -- `logical_and`, `logical_or`, `logical_xor`, `logical_not` - Logical operations -- `triangular` - Extract triangular part of matrices - -**Test Coverage:** -- 1350+ ONNX tests passing (100% of supported operations) -- WPT conformance tests for 44 operations -- Python API tests covering all 88 implemented operations - -For complete implementation status including WPT test coverage, see [docs/development/implementation-status.md](docs/development/implementation-status.md). - -## Documentation - -**User Documentation:** -- **[Getting Started](docs/user-guide/getting-started.md)** - Installation and first steps -- **[API Reference](docs/user-guide/api-reference.md)** - Complete Python API documentation -- **[Examples](docs/user-guide/examples.md)** - Working code samples -- **[Advanced Topics](docs/user-guide/advanced.md)** - Advanced usage patterns - -**Architecture & Design:** -- **[Architecture Overview](docs/architecture/overview.md)** - Core design principles and components -- **[Chromium Comparison](docs/architecture/chromium-comparison.md)** - How we compare to Chromium's WebNN - -**Development:** -- **[Development Setup](docs/development/setup.md)** - Build and test environment -- **[Implementation Status](docs/development/implementation-status.md)** - Comprehensive status across all backends with WPT test integration - -**Testing:** -- **[WPT Test Guide](docs/testing/wpt-test-guide.md)** - W3C WebNN conformance testing -- **[Performance Benchmarks](docs/testing/performance-benchmarks.md)** - Performance tracking - -**Integration Guides:** -- **[TensorRT Integration](docs/integration/tensorrt.md)** - NVIDIA GPU backend (Linux/Windows) -- **[Windows TensorRT Setup](docs/integration/windows-tensorrt-setup.md)** - Windows-specific TensorRT setup -- **[GGML Integration](docs/integration/ggml.md)** - GGML backend (future) - -**Reference:** -- **[WebNN Spec Reference](docs/reference/webnn-spec.md)** - W3C spec excerpts for offline use -- **[IPC Design](docs/reference/ipc-design.md)** - Inter-process communication design - -**Other Resources:** -- **README.md**: Crisp project overview and quickstart -- **examples/**: Sample WebNN graph JSON files and Python examples -- **tests/test_python_api.py**: Python API test suite (320+ tests passing) -- **TODO.txt**: Implementation roadmap and completed features -- **Makefile**: Common build and validation targets - ---- - -*This AGENTS.md evolves with the project. Update it as new patterns emerge or architecture changes.* diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fd7e72..4276587f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,59 @@ This changelog consolidates the previous `RELEASE_NOTES_*.md` files into a singl ## [Unreleased] +Changes on `main` since the `v0.5.12` publish branch (2026-05-02). + +### Added +- WebNN API in Rust: `MLContext`, `MLGraphBuilder`, `MLGraph`, `MLTensor`, `dispatch`, tensor read and write; backend and device hints on `MLContextOptions`; `RustNNOptions` with `TrtxOptions` (#111, #155, #181). +- Backends behind the unified API: TensorRT-RTX with native network lowering, refittable weights, zstd-compressed engine and runtime caches and CUDA graphs (#128, #162, #182, #189, #192, #193); CoreML (#140); LiteRT (#149, #179, #187, #196); Huawei CANN on OpenHarmony (#198, #203); browser WebNN bindings for wasm32 (#206). +- In-repo WPT conformance harness with per-backend PASS snapshots, expected-failure lists, JSON and HTML reports, audit mode and a weekly snapshot-sync workflow (#151, #166, #176, #197, #226). +- `MLGraphBuilder::rustnn_save_webnn`: save a graph as `.webnn` text plus `.safetensors` weights (#184). +- `int4` and `uint4` data types (#148); shape inference for the normalization operations (#125). +- Examples: builder-API fast style transfer, ResNet-50, SmolLM on `MLContext` (#124, #162). +- `TRTX_JSON_DUMP_PATH` engine dumps (#177). +- `MLOperand::rustnn_index()` and `From for OperandIndex` to fill operand fields of `ML*Options` such as `MLConv2dOptions::bias`. +- Rust API docs (rustdoc) built with warnings denied in CI and published under `/api/`; `docs/development/documentation-policy.md`. + ### Changed -- **WebNN spec alignment:** Replaced `round` with `roundEven` (spec name). Legacy JSON with `op_type: "round"` is still accepted and treated as `roundEven`. -- **Removed non-spec operators:** Dropped operators not in the current WebNN API: `asin`, `acos`, `atan`, `sinh`, `cosh`, `asinh`, `acosh`, `atanh`. Use only spec-defined operations for new graphs. +- **Breaking:** `MLContext::dispatch`, `MLGraphBuilder::build` and `rustnn_save_webnn` take `MLNamedTensors` and `MLNamedOperands` (`BTreeMap`) instead of `HashMap` (#202). +- `MLGraphBuilder::conv2_with_options` renamed to `conv2d_with_options`; the old name stays as a deprecated alias. `MLTensor::destoy` renamed to `destroy`. +- Errors are `Send + Sync` (#170); backend traits require `Send + Sync` (#145, #147). +- Dispatch validates tensor bindings: shapes and data types (#126, #132) and duplicate tensors (#220). +- Toolchain 1.97 (#211); `ort` 2.0.0-rc.13 (#208); `trtx` 0.8 for TensorRT-RTX 1.6 (#200); `thiserror` 2 (#190). +- CoreML: stable `reduceLogSumExp`, complete `resample2d`, gather normalization for dynamic and scalar indices, failing shims on Linux (#213, #215, #227, #209). +- Documentation rewritten for the Rust API; superseded plans moved to `docs/archive/`; Python-era files removed (`MANIFEST.in`, `pytest.ini`, the old `TODO.txt`); the built MkDocs site is no longer tracked. + +### Fixed +- TensorRT: CUDA graph replay, write synchronization, negative scatter indices, cache write races and many operator lowerings (#167, #168, #169, #182, #194, #231). +- ONNX Runtime: bool and uint8 handling (#180); bidirectional LSTM and GRU, an fp16 cast that broke optimized ONNX Runtime builds, `Squeeze` axes (#219). +- `cargo test` passes again: prost-build's `cleanup-markdown` feature turns the code blocks in the CoreML `.proto` comments into text fences instead of failing doctests, and the WPT harness exits successfully instead of failing when no backend feature is compiled in. + +## [0.5.12] - 2026-05-04 + +Published from a branch off `main` (2026-05-02) with the `web` feature dropped for crates.io. Contains everything merged since `v0.5.11`. + +### Added +- Strongly typed graph model: the `Operation` enum with one variant per WebNN operation and `ML*Options` structs replace string operation types and JSON attributes (#57, #78, #80, #90, #98, #99). +- Dynamic dimensions (`{ name, maxSize }`) behind the `dynamic-inputs` feature (#16, #74). +- Operations: `cumulativeSum`, `roundEven`, `reverse`, `resample2d`, `notEqual`, `linear`, `isNaN`, `isInfinite`, `gruCell`; `gatherElements` on CoreML (#34, #36, #37, #39, #41, #46, #48, #49, #50, #69). +- TensorRT-RTX engine building through the builder interface, `--run-trtx`, public `TrtxConverter::build_network`, re-export of `dynamically_load_tensorrt` (#9, #13, #79, #105). +- ONNX external weights for models above 2 GB (#104); quantization support (#10). +- Generated backend operator support report with a CI drift check (#38). +- WPT-based test suites and the rustnnpt gate in CI (#21, #52, #53). + +### Changed +- Python bindings split into the `pywebnn` repository; rustnn is a pure Rust crate (#11). +- **WebNN spec alignment:** `round` replaced by `roundEven`; legacy JSON with `op_type: "round"` is still accepted as `roundEven`. Non-spec operators `asin`, `acos`, `atan`, `sinh`, `cosh`, `asinh`, `acosh`, `atanh` removed. +- WebNN attributes use camelCase (#14). +- ONNX protos shared through `webnn-onnx-utils`. + +### Fixed +- ONNX conformance sweep: conv2d and convTranspose2d layout and padding, reductions, quantize and dequantize, pad, layerNormalization, the gather family, softmax, clamp, argMin and argMax, pooling (#23 to #45). +- CoreML MLProgram: required parameters for conv, slice and reshape, explicit gelu mode, 0-D scalars, boolean operations, pooling (#63, #68, #72, #102, #103, #106 to #109). + +## [0.5.5] to [0.5.11] - 2025-12-29 + +Patch releases for the Python wheel publishing pipeline of the time (bundled ONNX Runtime, manylinux builds, platform tarball names, `protoc` in CI) plus a backend info API. No graph or converter changes. ## [0.5.2] - 2025-12-28 diff --git a/Cargo.lock b/Cargo.lock index 60221aec..8df5e504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2513,6 +2513,8 @@ dependencies = [ "prettyplease", "prost", "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", "regex", "syn 2.0.119", "tempfile", @@ -2540,6 +2542,26 @@ dependencies = [ "prost", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.13.1", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "10.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0194e6e1966c23cc5fd988714f85b18d548d773e81965413555d96569931833d" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "pxfm" version = "0.1.30" @@ -3663,6 +3685,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 9f49015d..9c1e4793 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ rust-version = "1.97" description = "W3C WebNN implementation with ONNX, CoreML, and TensorRT backends [DO NOT USE IN PRODUCTION - Development Release]" license = "Apache-2.0" repository = "https://github.com/rustnn/rustnn" +homepage = "https://rustnn.github.io/rustnn/" +documentation = "https://rustnn.github.io/rustnn/api/rustnn/" keywords = ["webnn", "onnx", "coreml", "tensorrt", "neural-networks"] categories = ["science", "api-bindings"] exclude = [ @@ -19,6 +21,10 @@ exclude = [ "!README.md", ] +# docs.rs builds with the backends that compile on Linux so their APIs appear in the docs. +[package.metadata.docs.rs] +features = ["onnx-runtime", "trtx-runtime", "cann-runtime", "dynamic-inputs"] + # The `objc` 0.2 `msg_send!`/`sel_impl!` macros expand to `cfg(feature = "cargo-clippy")`, # which rustc flags as an unexpected cfg. A file-level `#![allow(unexpected_cfgs)]` does not # suppress it because the diagnostic originates inside the external macro; declaring the value @@ -178,7 +184,9 @@ pollster = { version = "1.0.1", optional = true } objc = { version = "0.2", optional = true, features = ["exception"] } [build-dependencies] -prost-build = "0.12" +# cleanup-markdown rewrites the .proto comments as Markdown and fences their code blocks as +# text, so the generated docs keep Apple's comments without producing failing doctests. +prost-build = { version = "0.12", features = ["cleanup-markdown"] } cc = "1.0" seahash = "4.1.0" diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 43e2fffb..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,29 +0,0 @@ -# Include essential files -include LICENSE -include README.md -include README_PYTHON.md -include CLAUDE.md -include TODO.txt -include pyproject.toml -include Cargo.toml -include Cargo.lock -include build.rs -include Makefile - -# Include Rust source files -recursive-include src *.rs -recursive-include protos *.proto - -# Include Python source and stubs -recursive-include python *.py *.pyi - -# Include examples and tests -recursive-include examples *.py *.json -recursive-include tests *.py - -# Exclude build artifacts and caches -global-exclude __pycache__ -global-exclude *.py[co] -global-exclude .DS_Store -global-exclude *.swp -global-exclude *~ diff --git a/Makefile b/Makefile index bd102bb0..80e0eb4d 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,6 @@ OHOS_SDK_NATIVE ?= ORT_VERSION ?= 1.29.0 ORT_BASE ?= https://github.com/microsoft/onnxruntime/releases/download/v$(ORT_VERSION) ORT_DIR ?= target/onnxruntime -MATURIN_ARGS ?= CHROMEDRIVER_CACHE ?= $(CURDIR)/.cache/chromedriver CHROMEDRIVER ?= $(CHROMEDRIVER_CACHE)/chromedriver @@ -84,7 +83,7 @@ CANN_CROSS_ENV = CC_aarch64_unknown_linux_ohos=$(OHOS_SDK_NATIVE)/llvm/bin/clang .PHONY: build test fmt fmt-check lint run viz clean clean-all help \ coverage coverage-html coverage-lcov coverage-open coverage-clean \ - docs-serve docs-build docs-clean ci-docs docs-backend-ops docs-backend-ops-check \ + docs-serve docs-build docs-clean ci-docs docs-api docs-backend-ops docs-backend-ops-check \ fetch-wpt require-wpt-cache test-wpt test-wpt-trtx test-wpt-litert test-wpt-coreml \ test-wpt-coreml-report test-wpt-op test-wpt-report \ wpt-sync-onnx wpt-sync-litert wpt-sync-coreml wpt-sync-trtx \ @@ -351,6 +350,14 @@ docs-clean: @echo "Cleaning documentation build artifacts..." rm -rf site/ +# Rust API documentation (rustdoc). Warnings are errors so broken doc links fail CI. +# The feature list matches what CI type-checks on Linux; coreml-runtime is macOS-only. +DOCS_API_FEATURES ?= onnx-runtime,trtx-runtime,litert-runtime,cann-runtime,dynamic-inputs +docs-api: + @echo "Building Rust API documentation..." + RUSTDOCFLAGS="-D warnings" $(CARGO) doc --no-deps --lib --features $(DOCS_API_FEATURES) + @echo "[OK] Rust API documentation generated in target/doc/rustnn/" + docs-backend-ops: @echo "Generating backend operator support report..." python3 scripts/generate_backend_operator_report.py @@ -426,6 +433,7 @@ help: @echo " docs-serve - Serve documentation with live reload" @echo " docs-build - Build static documentation site" @echo " ci-docs - Build documentation in strict mode (CI)" + @echo " docs-api - Build Rust API docs (rustdoc, warnings are errors)" @echo " docs-clean - Clean documentation artifacts" @echo " docs-backend-ops - Generate backend operator support report" @echo " docs-backend-ops-check - Verify backend operator report is up to date" diff --git a/README.md b/README.md index 2a39c270..0c4f458a 100644 --- a/README.md +++ b/README.md @@ -3,228 +3,144 @@ # rustnn - A Rust implementation of the W3C WebNN specification for neural network graph validation and backend conversion. + A Rust implementation of the W3C WebNN API with pluggable execution backends. --- ## [WARNING] EXPERIMENTAL - DO NOT USE IN PRODUCTION -This is an early-stage experimental implementation for research and exploration. Many features are incomplete, untested, or may change significantly. +rustnn is a development release (`0.5.x`). APIs change without notice. --- ## What is rustnn? -rustnn is a Rust library that provides: - -- **WebNN Graph Validation**: Validates WebNN graph structures against the W3C specification -- **Backend Conversion**: Converts WebNN graphs to ONNX and CoreML formats -- **Runtime Backends**: Executes graphs on CPU, GPU, or Neural Engine -- **Shape Inference**: Automatic tensor shape computation -- **Operation Support**: 88 WebNN operations (84% spec coverage) - -## Python Bindings - -Python users should use **[pywebnn](https://github.com/rustnn/pywebnn)** - a separate package that provides full W3C WebNN API Python bindings using rustnn as the core library. - -**Install Python package:** -```bash -pip install pywebnn -``` - -See the [pywebnn repository](https://github.com/rustnn/pywebnn) for Python documentation and examples. - -## Rust Library Installation - -Add rustnn to your `Cargo.toml`: +- **The WebNN API in Rust.** `MLContext`, `MLGraphBuilder`, `MLGraph`, `MLTensor` and + `dispatch` mirror the [W3C WebNN](https://www.w3.org/TR/webnn/) JavaScript API. Every + operation of the specification is available on the builder; rustnn-specific additions carry + a `rustnn_` prefix. +- **Backends selected at context creation.** ONNX Runtime, NVIDIA TensorRT-RTX, Apple CoreML, + LiteRT and Huawei CANN, chosen from the WebNN `accelerated` and power-preference hints or + forced with a backend hint. +- **Graph interchange.** Loads `.webnn` text and JSON graphs from + [webnn-graph](https://github.com/rustnn/webnn-graph) and + [onnx2webnn](https://github.com/rustnn/onnx2webnn), saves graphs with `.safetensors` weights, + exports ONNX and CoreML models. +- **Conformance.** The upstream WebNN Web Platform Tests run in-repo against the backends on + every pull request; the nightly [dashboard](https://rustnn.github.io/rustnn/wpt-conformance/) + shows per-operation results. + +Python users: the [pywebnn](https://github.com/rustnn/pywebnn) package wraps rustnn. This +repository contains no Python code. + +## Quick start ```toml [dependencies] -rustnn = { git = "https://github.com/rustnn/rustnn" } - -# Optional: Enable runtime backends -rustnn = { git = "https://github.com/rustnn/rustnn", features = ["onnx-runtime"] } +rustnn = { version = "0.5", features = ["onnx-runtime"] } ``` -**Features:** -- `onnx-runtime` - ONNX Runtime execution (CPU/GPU) -- `coreml-runtime` - CoreML execution (macOS only) -- `trtx-runtime-mock` - TensorRT mock (no GPU needed) -- `trtx-runtime` - TensorRT execution (Linux/Windows with NVIDIA GPU) - -## Quick Start (Rust) - ```rust -use rustnn::graph::GraphInfo; -use rustnn::converters::{GraphConverter, OnnxConverter}; -use rustnn::validator::GraphValidator; - -// Load a WebNN graph from JSON -let graph: GraphInfo = serde_json::from_str(&json_string)?; - -// Validate the graph -let validator = GraphValidator::new(); -let artifacts = validator.validate(&graph)?; - -// Convert to ONNX -let converter = OnnxConverter; -let onnx_model = converter.convert(&graph)?; - -// Save ONNX model -std::fs::write("model.onnx", onnx_model.data)?; +use rustnn::mlcontext::{ + MLContext, MLContextOptions, MLGraphBuilder, MLNamedOperands, MLNamedTensors, + MLOperandDescriptor, MLPowerPreference, MLTensorDescriptor, +}; +use rustnn::operator_enums::MLOperandDataType; + +fn main() -> rustnn::error::Result<()> { + let options = MLContextOptions::new(MLPowerPreference::Default, false); + let mut context = MLContext::create(&options)?; + + // y = relu(x + 1) + let mut builder = MLGraphBuilder::new(&mut context)?; + let descriptor = MLOperandDescriptor::new(MLOperandDataType::Float32, vec![2, 2]); + let x = builder.input("x", &descriptor)?; + let one = builder.constant_from_slice(&descriptor, &[1.0f32; 4])?; + let sum = builder.add(x, one)?; + let y = builder.relu(sum)?; + let mut outputs = MLNamedOperands::new(); + outputs.insert("y", y); + let mut graph = builder.build(&outputs)?; + + let tensor = MLTensorDescriptor::new(MLOperandDataType::Float32, vec![2, 2]); + let x_tensor = context.create_tensor(&tensor.to_writable())?; + let y_tensor = context.create_tensor(&tensor.to_readable())?; + context.write_tensor(&x_tensor, &[-2.0f32, -1.0, 0.0, 1.0])?; + + let mut inputs = MLNamedTensors::new(); + inputs.insert("x", &x_tensor); + let mut output_tensors = MLNamedTensors::new(); + output_tensors.insert("y", &y_tensor); + context.dispatch(&mut graph, &inputs, &output_tensors)?; + + let mut result = [0.0f32; 4]; + context.read_tensor(&y_tensor, &mut result)?; + assert_eq!(result, [0.0, 0.0, 1.0, 2.0]); + Ok(()) +} ``` -**For Python examples**, see the [pywebnn repository](https://github.com/rustnn/pywebnn). - -## Backend Selection - -Following the [W3C WebNN Device Selection spec](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md), backends are selected via hints: +The ONNX Runtime backend loads the shared library from `ORT_DYLIB_PATH`; in a clone, +`make onnxruntime-download` fetches a matching release. See +[Getting Started](docs/user-guide/getting-started.md). -```python -# CPU-only execution -context = ml.create_context(accelerated=False) +## Features -# Request GPU/NPU (platform selects best available) -context = ml.create_context(accelerated=True) - -# Request high-performance (prefers GPU) -context = ml.create_context(accelerated=True, power_preference="high-performance") - -# Request low-power (prefers NPU/Neural Engine) -context = ml.create_context(accelerated=True, power_preference="low-power") -``` +| Feature | Backend | +|---|---| +| `onnx-runtime` | ONNX Runtime (CPU, GPU, NPU execution providers), all platforms | +| `trtx-runtime` | NVIDIA TensorRT-RTX (Linux, Windows); `trtx-runtime-mock` builds without a GPU | +| `coreml-runtime` | Apple CoreML (macOS) | +| `litert-runtime` | LiteRT / TensorFlow Lite; needs `flatc` at build time | +| `cann-runtime` | Huawei CANN on OpenHarmony; `cann-runtime-mock` for validation | +| `dynamic-inputs` | Dynamic dimensions bounded by a maximum size | -**Platform-Specific Backends:** -- NPU: CoreML Neural Engine (Apple Silicon macOS only) -- GPU: ONNX Runtime GPU (cross-platform) or CoreML GPU (macOS) -- CPU: ONNX Runtime CPU (cross-platform) +Full list and environment variables: crate docs (`make docs-api`) or +[Backends](docs/user-guide/backends.md). -## Examples - -### Complete MobileNetV2 Image Classification +## Command line ```bash -# Download pretrained weights (first time only) -bash scripts/download_mobilenet_weights.sh - -# Run on different backends -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend cpu -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend gpu -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend coreml -``` - -**Output:** -``` -Top 5 Predictions (Real ImageNet Labels): - 1. lesser panda 99.60% - 2. polecat 0.20% - 3. weasel 0.09% - -Performance: 74.41ms (CPU) / 77.14ms (GPU) / 51.93ms (CoreML) +cargo run --features onnx-runtime -- examples/sample_graph.webnn # validate +cargo run --features onnx-runtime -- examples/sample_graph.webnn --export-dot graph.dot # Graphviz +cargo run --features onnx-runtime -- examples/sample_graph.webnn --convert onnx --convert-output model.onnx +cargo run --features onnx-runtime -- examples/sample_graph.webnn --convert onnx --run-onnx ``` -### Text Generation with Transformer Attention - -```bash -# Run generation with attention -make text-gen-demo - -# Train on custom text -make text-gen-train - -# Generate with trained weights -make text-gen-trained -``` - -See [examples/](examples/) for more samples. - ## Documentation -- **[Getting Started](docs/user-guide/getting-started.md)** - Installation and first steps -- **[API Reference](docs/user-guide/api-reference.md)** - Complete Python API documentation -- **[Examples](docs/user-guide/examples.md)** - Code examples and tutorials -- **[Architecture](docs/architecture/overview.md)** - Design principles and structure -- **[Development Guide](docs/development/setup.md)** - Building and contributing -- **[Changelog](CHANGELOG.md)** - Consolidated release history - -## Implementation Status - -- 85 of ~95 WebNN operations (89% spec coverage) -- Shape inference: 85/85 (100%) -- Python API: 85/85 (100%) -- ONNX Backend: 85/85 (100%) -- CoreML MLProgram: 85/85 (100%) -- 1350+ WPT conformance tests passing - -See [docs/development/implementation-status.md](docs/development/implementation-status.md) for complete details. - -## Rust CLI Usage - -```bash -# Validate a graph -cargo run -- examples/sample_graph.json - -# Visualize a graph (requires graphviz) -cargo run -- examples/sample_graph.json --export-dot graph.dot -dot -Tpng graph.dot -o graph.png - -# Convert to ONNX -cargo run -- examples/sample_graph.json --convert onnx --convert-output model.onnx - -# Execute with ONNX Runtime -cargo run --features onnx-runtime -- examples/sample_graph.json --convert onnx --run-onnx -``` - -See `make help` for all available targets. +- [Documentation site](https://rustnn.github.io/rustnn/) with the user guide, architecture and development pages +- [Rust API reference](https://rustnn.github.io/rustnn/api/rustnn/) (rustdoc) +- [Backend Operator Support](docs/development/backend-operator-support.md), generated from the converters +- [WPT conformance dashboard](https://rustnn.github.io/rustnn/wpt-conformance/) +- [Changelog](CHANGELOG.md) ## Contributing -Contributions welcome! Please see: - -- [AGENTS.md](AGENTS.md) - Project architecture and conventions -- [docs/development/contributing.md](docs/development/contributing.md) - How to add features -- [TODO.txt](TODO.txt) - Feature requests and known issues - -**Quick Contribution Guide:** - -1. Fork and create feature branch: `git checkout -b feature/my-feature` -2. Install hooks (optional): `./scripts/install-git-hooks.sh` -3. Make changes and test: `make test && make python-test` -4. If WPT snapshots or expected-failure lists need to be updated (indicated by - test failures), use one of: - - - Per-backend sync targets (regenerate PASS snapshots + `*_expected_failures.txt`): - - `make wpt-sync-onnx` - - `make wpt-sync-litert` - - `make wpt-sync-coreml` (macOS only) - - `make wpt-sync-trtx` (requires an NVIDIA GPU) - - Or update PASS snapshots manually via insta: - - `cargo insta review` (interactive; https://insta.rs/docs/cli/) - - `INSTA_UPDATE=always make test-wpt` (automatic) - - Review the diff before committing! -5. Format code: `make fmt` -6. Commit and push +1. Read [Setup and Workflow](docs/development/setup.md) and the + [Documentation Policy](docs/development/documentation-policy.md). Coding agents start with + [AGENTS.md](AGENTS.md). +2. Install the git hooks (optional): `./scripts/install-git-hooks.sh`. +3. Run `make test` (formatting, clippy, tests, operator report drift check). +4. For operation or converter changes, run the WPT cases and regenerate baselines: + `make test-wpt-op OP=`, then `make wpt-sync-onnx`, `make wpt-sync-litert`, + `make wpt-sync-coreml` (macOS) or `make wpt-sync-trtx` (NVIDIA GPU). Review the diff. +5. Update the documentation that describes the changed behaviour, then open a pull request. ## License -Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details. +Apache License, Version 2.0. See [LICENSE](LICENSE). ## Links -- **GitHub**: [https://github.com/rustnn/rustnn](https://github.com/rustnn/rustnn) -- **PyPI**: [https://pypi.org/project/pywebnn/](https://pypi.org/project/pywebnn/) -- **Documentation**: [https://rustnn.github.io/rustnn/](https://rustnn.github.io/rustnn/) -- **Changelog**: [CHANGELOG.md](CHANGELOG.md) -- **W3C WebNN Spec**: [https://www.w3.org/TR/webnn/](https://www.w3.org/TR/webnn/) +- GitHub: https://github.com/rustnn/rustnn +- crates.io: https://crates.io/crates/rustnn +- Python bindings: https://github.com/rustnn/pywebnn +- W3C WebNN specification: https://www.w3.org/TR/webnn/ ## Acknowledgments -- W3C WebNN Community Group for the specification -- Chromium WebNN implementation for reference -- PyO3 and Maturin projects for excellent Python-Rust integration - ---- - -**Made with Rust by [Tarek Ziade](https://github.com/tarekziade)** +- The W3C WebML Working Group for the specification +- The Chromium WebNN implementation, used as the reference for operator lowering +- Created by [Tarek Ziade](https://github.com/tarekziade) diff --git a/TODO.txt b/TODO.txt index 2f587e26..320fa70b 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,568 +1,17 @@ -# TODO - Future Implementation Tasks +# TODO -## Python API - Core Functionality +Current status and known gaps: docs/development/implementation-status.md +Operator support per backend: docs/development/backend-operator-support.md (generated) +Conformance per operation: https://rustnn.github.io/rustnn/wpt-conformance/ +Issues and feature requests: https://github.com/rustnn/rustnn/issues -### Execution Engine -[x] Implement actual tensor execution in MLContext.compute() - - Integrated with ONNX runtime - - Accepts numpy arrays as inputs - - Returns actual computed outputs as numpy arrays - - Includes fallback to zeros when ONNX runtime not available +Open items visible in the code (search for `todo!()`): -[x] Add MLTensor class for explicit tensor management - - createTensor() for pre-allocating tensors - - readTensor() for reading results - - writeTensor() for setting input data +- MLContext: create_from_gpu_device, lost, create_constant_tensor, destroy, op_support_limits +- MLGraphBuilder: constant_from_tensor, constant_from_value +- MLTensor: destroy, destroyed +- Async API: the methods marked //async in src/mlcontext.rs and src/mlgraphbuilder.rs are synchronous +- Browser WebNN backend (wasm32): generated bindings only, no MLContext backend +- Option structs hold operand indices (OperandIndex) instead of MLOperand values -[x] Implement async execution support - - WebNN spec uses async/await - - Python asyncio integration via AsyncMLContext wrapper - - Non-blocking compute operations with dispatch() - -### Operations - Missing Implementations - -[x] Convolution operations - - [x] conv2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) - - [x] convTranspose2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) - - [x] depthwiseConv2d (DONE: use conv2d with groups=in_channels parameter) - -[ ] Pooling operations - - [x] averagePool2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) - - [x] maxPool2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) - - l2Pool2d - - [x] globalAveragePool (DONE: shape inference, Python API, ONNX/CoreML converters, 6 tests) - - [x] globalMaxPool (DONE: shape inference, Python API, ONNX/CoreML converters, 6 tests) - -[ ] Normalization operations - - [x] batchNormalization (DONE: shape inference, Python API, ONNX converter, 3 tests) - - [x] instanceNormalization (DONE: shape inference, Python API, ONNX converter, 4 tests) - - [x] layerNormalization (DONE: shape inference, Python API, ONNX converter, 5 tests) - - [ ] localResponseNormalization (SKIPPED: Not in W3C WebNN spec as of 2025-12-07; W3C decision to use decomposition in higher layers due to rarity and backend inconsistencies) - -[x] Reduction operations (DONE: shape inference, Python API, ONNX/CoreML converters, 18 tests - all passing) - - [x] reduceSum (ONNX: ReduceSum, CoreML: ReduceSumLayerParams) - - [x] reduceMean (ONNX: ReduceMean, CoreML: ReduceMeanLayerParams) - - [x] reduceMax (ONNX: ReduceMax, CoreML: ReduceMaxLayerParams) - - [x] reduceMin (ONNX: ReduceMin, CoreML: ReduceMinLayerParams) - - [x] reduceProduct (ONNX: ReduceProd, CoreML: ReduceProdLayerParams) - - [x] reduceL1 (ONNX: ReduceL1, CoreML: ReduceL1LayerParams) - - [x] reduceL2 (ONNX: ReduceL2, CoreML: ReduceL2LayerParams) - - [x] reduceLogSum (ONNX: ReduceLogSum, CoreML: ReduceLogSumLayerParams) - - [x] reduceLogSumExp (ONNX: ReduceLogSumExp, CoreML: ReduceLogSumExpLayerParams) - - [x] reduceSumSquare (ONNX: ReduceSumSquare, CoreML: ReduceSumSquareLayerParams) - -[x] Element-wise operations (DONE: shape inference, Python API, ONNX/CoreML converters, 23 tests - all passing, 6 WPT test files) - - [x] Basic math: abs, ceil, floor, round, neg, sign (CoreML: dedicated layers + multiply workaround for neg) - - [x] Exponential/log: exp, log, sqrt, reciprocal (ONNX: capitalized names, CoreML: UnaryFunctionLayerParams) - - [x] Trigonometric: sin, cos, tan, asin, acos, atan (ONNX/CoreML: dedicated layer types) - - [x] Hyperbolic: sinh, cosh, asinh, acosh, atanh (ONNX/CoreML: dedicated layer types) - - [x] Special functions: erf, identity (CoreML: ErfLayerParams, multiply workaround for identity) - - [x] WPT conformance test data: abs, ceil, floor, exp, log, sqrt (14 test cases total) - -[x] Logic operations (DONE: shape inference, Python API, ONNX/CoreML converters with Cast node insertion, 9 tests - all passing) - - [x] Comparison operations: equal, greater, greaterOrEqual, lesser, lesserOrEqual (ONNX: Equal→Cast(bool→uint8), Greater→Cast, GreaterOrEqual→Cast, Less→Cast, LessOrEqual→Cast; CoreML: dedicated layer types with alpha=0.0) - - [x] Logical NOT: logicalNot (ONNX: Cast(input→bool)→Not→Cast(bool→uint8); CoreML: LogicalNotLayerParams) - unary operation - - [x] Logical operations: logicalAnd, logicalOr, logicalXor (ONNX: Cast(inputs→bool)→[And/Or/Xor]→Cast(bool→uint8); CoreML: dedicated layer types) - - [x] ONNX Cast node insertion: Automatically inserts Cast nodes to handle WebNN uint8 boolean type vs ONNX bool type - - Implementation details: create_cast_node() helper with AttributeType::Int, Cast nodes inserted in convert() for all logic operations - -[ ] Advanced operations - - concat (concatenate tensors) - - expand (broadcast dimensions) - - gather, scatter - - slice (extract sub-tensors) - - split (split tensor into parts) - - squeeze (remove dimensions of size 1) - - tile (repeat tensor) - - transpose - - where (conditional selection) - - pad (add padding) - - prelu, elu, leakyRelu, hardSigmoid, hardSwish, gelu - - softplus, softsign - -[ ] Recurrent operations (DEFERRED - See rationale below) - - gru, gruCell - - lstm, lstmCell - - **Deferral Rationale (2025-12-08):** - - These are complex composite operations (10-15 parameters each, ~2000-3000 LOC) - - WebNN spec debate about removing them in favor of lower-level primitives - - LSTM/GRU largely obsoleted by Transformers in modern ML - - WPT tests exist but implementation priority is low - - Focus on simpler, more widely-used operations first (concat, gather, slice, pad, etc.) - - Can revisit if/when spec stabilizes and user demand exists - -[x] Quantization operations (2025-12-08) - - dequantizeLinear: Converts quantized integers to float32 - - quantizeLinear: Converts float32 to quantized integers - - Shape inference: Preserves input shape - - ONNX support: ✅ Fully implemented, maps to DequantizeLinear/QuantizeLinear ops - - CoreML support: ✅ FULLY MIGRATED to MLProgram format (2025-12-08) - - CoreML Migration (2025-12-08): - - ✅ Migrated from NeuralNetwork (legacy) to MLProgram (modern) format - - ✅ Removed old src/converters/coreml.rs (NeuralNetwork-based) - - ✅ Implemented src/converters/coreml_mlprogram.rs (MIL-based) - - ✅ All 50+ WebNN operations now map to MIL operations - - ✅ Quantization supported via MIL "dequantize" and "quantize" ops - - ✅ Uses CoreML spec v7+ (iOS 15+, macOS 12+) - - ✅ Matches Chromium's MLProgram implementation - - ✅ Tested with simple operations (add) - - ⏸️ Complex operation parameters (conv padding, pool strides) deferred - - - Tests: 5 tests added (test_dequantize_linear, test_quantize_linear, uint8 variants, roundtrip) - -[x] Shape inference and broadcasting - - Automatic shape computation for operations - - Broadcasting rules for binary operations (NumPy-style) - - Shape validation at graph build time - - Proper matmul shape inference with batching support - -### CoreML Converter - MLProgram Format (Migrated 2025-12-08) - -[x] Migration to MLProgram (DONE: 2025-12-08) - - ✅ Replaced NeuralNetwork converter with MLProgram converter - - ✅ All operations now map to MIL operations - - ✅ Basic structure: Program → Function → Block → Operations - - ✅ Function inputs and block outputs implemented - - ⏸️ Operation-specific parameters (conv, pool, etc.) deferred - -[x] MIL Operation Mappings (50+ operations mapped) - - ✅ Binary: add, sub, mul, real_div, matmul - - ✅ Activations: relu, sigmoid, tanh, softmax - - ✅ Unary math: abs, ceil, floor, exp, log, sqrt, sign, sin, cos, tan, erf, reciprocal - - ✅ Logic: equal, greater, greater_equal, less, less_equal, logical_not, logical_and, logical_or, logical_xor - - ✅ Quantization: dequantize, quantize - - ✅ Convolution: conv, conv_transpose - - ✅ Pooling: avg_pool, max_pool - - ✅ Normalization: batch_norm, instance_norm, layer_norm - - ✅ Reduction: reduce_sum, reduce_mean, reduce_max, reduce_min, reduce_prod, reduce_l1, reduce_l2, etc. - - ✅ Shape: reshape - -[ ] Parameter Handling (Deferred) - - [ ] Conv2d parameters (strides, padding, dilations, groups) - - [ ] Pool2d parameters (window, strides, padding) - - [ ] Normalization parameters (epsilon, scale, bias) - - [ ] Need to implement MIL Value creation for immediate values - - Note: Basic tensor input/output works, complex parameters need MIL Value messages - -## Testing & Quality - -### Python Tests -[ ] Comprehensive operation tests - - Test each operation independently - - Test with different data types - - Test edge cases (empty tensors, scalars) - - Test shape broadcasting - -[ ] Integration tests - - End-to-end graph building and conversion - - Multi-layer network tests - - Complex graph patterns - -[ ] Property-based testing - - Use hypothesis for generative testing - - Random graph generation and validation - -[ ] Performance benchmarks - - Compilation time benchmarks - - Conversion speed benchmarks - - Memory usage profiling - -[ ] Test coverage - - Aim for >80% code coverage - - Add coverage reporting to CI - -### Type Checking & Linting -[ ] Add mypy for static type checking - - Type check all Python bindings - - Add mypy to CI pipeline - -[ ] Add ruff/flake8 for Python linting - - Enforce PEP 8 style - - Add to pre-commit hooks - -[ ] Add black for code formatting - - Auto-format Python code - - Check formatting in CI - -### Rust Code Quality -[ ] Fix Rust 2024 edition warnings - - Add unsafe blocks where needed - - Update to new edition idioms - -[ ] Add more Rust unit tests - - Test converters with various graphs - - Test validation edge cases - -[ ] Reduce compiler warnings - - Fix unused variable warnings - - Address clippy suggestions - -## Documentation - -### API Documentation -[ ] Auto-generate API docs from docstrings - - Add comprehensive docstrings to all Python classes - - Use mkdocstrings to auto-generate reference docs - - Add type hints throughout - -[ ] Add more code examples - - Real-world use cases (MNIST, ResNet, etc.) - - Transfer learning examples - - Model optimization examples - -[ ] Video tutorials - - Getting started video - - Building complex models - - Deployment guide - -[ ] Interactive examples - - Jupyter notebook examples - - Google Colab notebooks - - Try-it-live web interface - -### Performance Documentation -[ ] Benchmarking guide - - How to benchmark models - - Performance comparison ONNX vs CoreML - - Optimization tips - -[ ] Memory usage guide - - Understanding memory consumption - - Reducing memory footprint - - Float16 vs Float32 trade-offs - -### Platform-Specific Guides -[ ] macOS Neural Engine guide - - How to use ANE effectively - - Performance characteristics - - Supported operations - -[ ] Windows DirectML guide (future) - - DirectML integration - - GPU acceleration on Windows - -[ ] Linux GPU guide - - CUDA/ROCm integration - - CPU optimization flags - -## CI/CD & Packaging - -### PyPI Publishing -[ ] Create PyPI package publishing workflow - - Build wheels for multiple platforms - - manylinux wheels for Linux - - macOS universal2 wheels - - Windows wheels - -[ ] Automated version bumping - - Semantic versioning - - Changelog generation - - Git tag automation - -[ ] Release automation - - GitHub Releases on tag push - - Automated release notes - - Asset uploading (wheels, docs) - -### Multi-Platform Support -[ ] Test on multiple Python versions - - Python 3.8, 3.9, 3.10, 3.11, 3.12 - - Matrix testing in CI - -[ ] Test on multiple platforms - - Ubuntu (latest, 20.04, 22.04) - - macOS (Intel, Apple Silicon) - - Windows (latest) - -[ ] Platform-specific features - - Conditional compilation for platform features - - Feature detection at runtime - -### Docker Images -[ ] Create Docker images - - Python + Rust development image - - Runtime-only image - - GPU-enabled image - -[ ] Docker Hub publishing - - Automated image builds - - Multi-architecture images - - Version tagging - -## Features & Enhancements - -### Graph Optimization -[ ] Implement graph optimization passes - - Constant folding - - Dead code elimination - - Operation fusion - - Common subexpression elimination - -[ ] Graph analysis tools - - Visualize graphs (beyond Graphviz) - - Memory usage estimation - - Computational complexity analysis - -### Model Import/Export -[ ] ONNX model import - - Parse existing ONNX models - - Convert ONNX → WebNN graph - - Preserve metadata - -[ ] PyTorch integration - - Export PyTorch models to WebNN - - torch.fx graph conversion - - Maintain gradient information (future) - -[ ] TensorFlow integration - - Export TensorFlow models - - SavedModel → WebNN conversion - -[ ] Hugging Face integration - - Export transformers models - - Easy model hub integration - -### Developer Experience -[ ] Better error messages - - More descriptive validation errors - - Suggestions for fixes - - Error recovery hints - -[ ] Debugging tools - - Graph visualization in Jupyter - - Intermediate value inspection - - Step-by-step execution - -[ ] Profiling tools - - Operation-level timing - - Memory profiling - - Bottleneck identification - -### WebNN Spec Compliance -[ ] Full WebNN API compliance - - Implement all missing operations - - Match behavior exactly - - Pass WebNN conformance tests (if available) - -[ ] Context options - - Power preference enforcement - - Device preference handling - - Capability querying (opSupportLimits) - -[ ] Graph execution modes - - Sync vs async execution - - Streaming execution for large inputs - - Batch processing - -## Ecosystem Integration - -### NumPy Integration -[ ] Better NumPy interop - - Zero-copy where possible - - Support NumPy's __array_interface__ - - Proper dtype conversion - -[ ] NumPy-like API - - Operator overloading (+, -, *, /) - - Slicing support - - Pythonic indexing - -### ML Framework Integration -[ ] JAX integration - - Export JAX computations - - jax.tree_util support - -[ ] scikit-learn integration - - Convert simple sklearn models - - Pipeline integration - -### Visualization -[ ] Netron support - - Ensure exported models work in Netron - - Add metadata for better visualization - -[ ] TensorBoard integration - - Graph visualization - - Profiling data export - -## Infrastructure - -### Build System -[ ] Optimize build times - - Incremental compilation - - Build caching in CI - - Parallel builds - -[ ] Cross-compilation support - - Build for different targets - - Static linking options - -### Security -[ ] Security audit - - Dependency vulnerability scanning - - SAST (Static Application Security Testing) - - Regular security updates - -[ ] Sandboxing - - Restrict file system access - - Memory limits - - Timeout enforcement - -### Monitoring -[ ] Usage analytics (opt-in) - - Track which operations are used - - Performance telemetry - - Error reporting - -[ ] Crash reporting - - Automated crash reports (opt-in) - - Stack trace collection - - Issue auto-creation - -## Community - -### Examples & Templates -[ ] Example repository - - Real-world examples - - Template projects - - Starter kits - -[ ] Model zoo - - Pre-built models - - Optimized for WebNN - - Various domains (CV, NLP, etc.) - -### Documentation -[ ] Contributing guide - - How to contribute - - Development setup - - Code review process - -[ ] Architecture documentation - - High-level design - - Component interactions - - Extension points - -### Community Building -[ ] Discord/Slack channel - - Community discussions - - Support channel - - Show & tell - -[ ] Blog posts & tutorials - - Getting started blog post - - Technical deep dives - - Performance case studies - -## Priority Levels - -HIGH PRIORITY (Next Session): -- [x] Fix CoreML converter to support relu, sigmoid, tanh, softmax -- [x] Implement actual compute() with ONNX runtime integration -- [x] Add comprehensive Python tests -- [x] Fix Rust 2024 edition warnings (PyO3 internal warnings, will be fixed in PyO3 update) -- [x] Add basic shape inference/validation - -MEDIUM PRIORITY: -- [ ] Add more operations (conv2d, pooling, normalization) -- [ ] PyPI packaging and publishing -- [ ] Better error messages -- [ ] Performance benchmarks - -LOW PRIORITY: -- [ ] Full WebNN spec compliance -- [ ] Advanced graph optimizations -- [ ] Multi-framework integration -- [ ] Community infrastructure - -## Notes - -- Most missing functionality is in the Rust backend (converters, executors) -- Python bindings are complete for the architecture - just need more operations -- CoreML converter now supports basic activation functions (relu, sigmoid, tanh, softmax) -- ONNX runtime integration complete with actual tensor execution -- Documentation is comprehensive and ready for community use -- Testing infrastructure expanded with comprehensive compute tests -- CI/CD for packaging and publishing not yet set up - -Last Updated: 2025-12-08 - -## Recent Changes (2025-12-08) - -### Logic Operations with Cast Node Implementation (Latest) -- Implemented all 9 logic operations with full WebNN spec compliance -- Shape inference: Binary operations use broadcasting, unary logicalNot preserves shape -- Python API: Added 9 methods to MLGraphBuilder (src/python/graph_builder.rs) -- ONNX conversion: Automatic Cast node insertion for type conversions (src/converters/onnx.rs:446-580) - - **WORKAROUND**: Currently casts bool → float32 (should be bool → uint8) - - Migrated to ort v2.0.0-rc.10 (from onnxruntime-rs v0.0.14) - supports dynamic types via try_extract_tensor() - - Full uint8 support now technically possible but requires additional changes: - - Update OnnxOutputWithData struct to support multiple data types (not just Vec) - - Update executor to extract correct type based on model output - - Update Python bindings to handle uint8 → NumPy conversion - - Chromium correctly uses bool → uint8, we keep float32 workaround for simplicity - - **PROPER FIX** (future PR): Implement full uint8 output pipeline - - Change: Cast(bool → float32) to Cast(bool → uint8) - - Update output ValueInfo types from Float32 back to Uint8 - - Comparison ops: Execute op (outputs bool) → Cast(bool→float32) [TEMP] - - Logical ops: Cast(inputs→bool) → Execute op → Cast(bool→float32) [TEMP] - - Helper functions: create_cast_node() with AttributeType::Int, create_operation_attributes() -- CoreML conversion: Full support with dedicated layer types (alpha=0.0 for comparison ops) -- Python tests: All 9 tests PASSING with ONNX Runtime (141 passed total) -- All tests pass with Cast node structure (type field set to AttributeType::Int) -- Operations implemented: equal, greater, greaterOrEqual, lesser, lesserOrEqual, logicalNot, logicalAnd, logicalOr, logicalXor - -### Element-wise Operations Implementation -- Implemented all 23 unary element-wise operations with full WebNN spec compliance -- Shape inference: All operations preserve input shape (src/shape_inference.rs) -- Python API: Added 23 methods to MLGraphBuilder (src/python/graph_builder.rs) -- ONNX conversion: Operations map via capitalization (Abs, Ceil, etc.) -- CoreML conversion: Full support with dedicated layer types and workarounds - - UnaryFunctionLayerParams: abs, exp, log, sqrt, reciprocal - - Dedicated layers: ceil, floor, round, sign, trig/hyperbolic operations, erf - - Multiply workaround: neg (alpha=-1), identity (alpha=1) -- Python tests: 23 new tests, all passing with NumPy/SciPy validation (tests/test_python_api.py) -- WPT conformance data: 6 operations with 14 test cases (abs, ceil, floor, exp, log, sqrt) -- Updated CLAUDE.md: CoreML conversion now mandatory for all operations -- All 132 tests passing (109 regular + 23 element-wise) -- Commits: 7ff609d6 (implementation), af2e5a9d (WPT data), dde8208c (CoreML) - -## Recent Changes (2025-12-07) - -### Async Execution Support -- Implemented AsyncMLContext wrapper for async/await syntax -- Added dispatch() method for non-blocking graph execution -- Added read_tensor_async() and write_tensor_async() for async tensor I/O -- WebNN spec-compliant asynchronous execution model -- Uses Python's asyncio.run_in_executor() for thread pool execution -- 5 new async tests covering dispatch, tensor I/O, and concurrent operations -- All 45 tests passing (40 existing + 5 new async) -- Rust code remains synchronous (follows Rust-first principle) -- Zero Rust async dependencies - clean Python-layer solution - -### MLTensor Implementation -- Implemented MLTensor class for explicit tensor management -- Added createTensor(), readTensor(), writeTensor() methods to MLContext -- Thread-safe data storage using Arc>> -- Full NumPy interoperability with automatic type conversion -- Shape validation and data integrity checks in Rust -- 7 new Python tests covering tensor operations -- All 40 Python tests passing (33 existing + 7 new) -- Maintained Rust-first architecture: core logic in Rust, thin Python wrappers - -### Shape Inference and Validation -- Implemented NumPy-style broadcasting for binary operations -- Added proper matmul shape inference with batched matmul support -- Added reshape validation to ensure element count consistency -- Created comprehensive shape_inference module with full test coverage -- Added 11 new Python tests for shape inference functionality -- All shape errors now caught at graph build time with clear error messages - -### ONNX Runtime Integration -- Added CoreML support for relu, sigmoid, tanh, softmax activations -- Implemented run_onnx_with_inputs() for actual tensor execution -- Updated MLContext.compute() to use ONNX runtime with real inputs/outputs -- Added 8 new comprehensive Python tests for compute functionality -- Tests verify actual numerical results for all activation functions +The previous TODO list from the Python bindings era is archived in docs/archive/todo-2025-12.txt. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 027fdd05..6910a535 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -1,275 +1,124 @@ # Architecture -## Core Components +rustnn has three layers on top of one graph model. The WebNN API records graphs and executes +them on a backend; converters lower the graph model to a backend format; the legacy pipeline +loads, validates and converts stored graphs without a context. ``` -┌─────────────────────────────────────────────────────────────┐ -│ CLI (main.rs) / Library API (lib.rs) / Python API (PyO3) │ -└──────────────┬──────────────────────────────────────────────┘ - │ - ┌──────────┴──────────┬──────────────┬─────────────────┐ - ▼ ▼ ▼ ▼ -┌────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ -│Loader │────▶│ Validator │──▶│ Context │───▶│ Backend │ -│(JSON) │ │(graph.rs) │ │(selects) │ │ Selection │ -└────────┘ └──────────────┘ └────┬─────┘ └──────┬───────┘ - │ │ - ▼ ▼ - ┌──────────┐ ┌──────────────┐ - │ Builder │ │ Converter │ - │(backend- │ │ (Runtime) │ - │agnostic) │ │ │ - └────┬─────┘ └──────┬───────┘ - │ │ - ▼ ▼ - ┌─────────────┐ ┌────────────────┐ - │ MLGraph │ │ ONNX / CoreML │ - │(immutable) │ │ Execution │ - └─────────────┘ └────────────────┘ + WebNN API (rustnn::mlcontext, rustnn::mlgraphbuilder) + MLContext::create -> MLGraphBuilder -> build() -> MLGraph -> dispatch(MLTensor bindings) + | | + | selects | records + v v + backend_selection GraphInfo <---- loader (.webnn / JSON) <---- onnx2webnn, webnn-graph + (hints -> device) operands, Operation enum, constants + | | + v v + backends::{ort, trtx, coreml, litert, cann} uses converters::{onnx, coreml_mlprogram, trtx, litert, cann} + MLBackendContext / MLBackendBuilder -----> GraphConverter + | + v + ONNX Runtime | TensorRT-RTX | CoreML | LiteRT | HiAI ``` -## Key Principles - -### 1. Backend-Agnostic Graph Representation -- `builder.build()` creates an immutable, platform-independent `GraphInfo` structure -- Contains operands, operations, inputs, outputs, and constant data -- No backend-specific artifacts at this stage - -### 2. Runtime Backend Selection (WebNN Spec-Compliant) - -Following the [W3C WebNN Device Selection Explainer](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md): - -- Backend selection happens at **context creation** via `accelerated` and `power_preference` hints -- `accelerated=False` → ORT/LiteRT CPU -- `accelerated=True` + `power="high-performance"` → GPU preferred (TRTX, CoreML, LiteRT, ORT) -- `accelerated=True` + `power="low-power"` → NPU preferred (CoreML Neural Engine, LiteRT NPU) -- Platform autonomously selects actual device based on availability and runtime conditions -- Selection logic in `PyMLContext::select_backend()` - -### 3. MLTensor Management - -Following the [W3C WebNN MLTensor Explainer](https://github.com/webmachinelearning/webnn/blob/main/mltensor-explainer.md): - -- Explicit tensor management with descriptor flags (readable, writable, exportableToGPU) -- `destroy()` method for explicit resource cleanup -- `dispatch()` for async execution with MLTensor inputs/outputs -- Permission enforcement on read/write operations - -### 4. Lazy Backend Conversion -- Backend-specific conversion happens during `compute()`, not `build()` -- `compute()` routes to appropriate backend method: - - `compute_onnx()` for ONNX Runtime - - `compute_coreml()` for CoreML - - `compute_fallback()` when no backend available -- Same graph can be executed on different backends via different contexts - -### 5. Rust-First Architecture -- All core functionality in pure Rust (validation, conversion, execution) -- Python bindings are thin wrappers exposing Rust functionality -- Rust library usable independently without Python -- Design principle: "Rust is the implementation, Python is the interface" - -## Shape Inference - -**Shape inference** is the process of automatically computing output tensor shapes of neural network operations based on their input shapes and operation parameters, without executing the operation. - -### Why Shape Inference Matters - -Shape inference enables: - -1. **Early validation** - Catch shape mismatches at build time, not runtime -2. **Memory allocation** - Backend runtimes know output buffer sizes before execution -3. **Graph optimization** - Enables static analysis and optimization passes -4. **Self-describing graphs** - Graphs are fully annotated and backend-agnostic - -### How It Works - -Each WebNN operation has a shape inference function in `src/shape_inference.rs` that computes output shapes. Shape inference happens during **graph building**, before any backend selection or execution. - -**Binary Operations (add, mul, div, etc.):** -- Use NumPy-style broadcasting rules -- Two dimensions are compatible if equal or one is 1 -- Output dimension is the maximum of the two -```rust -// broadcast_shapes([3, 1, 5], [3, 4, 5]) → [3, 4, 5] -// The dimension 1 broadcasts to 4 -``` - -**Matrix Multiplication:** -```rust -// Simple 2D: [M, K] @ [K, N] → [M, N] -infer_matmul_shape([2, 3], [3, 4]) → [2, 4] - -// Batched: [batch, M, K] @ [batch, K, N] → [batch, M, N] -infer_matmul_shape([5, 2, 3], [5, 3, 4]) → [5, 2, 4] - -// Validates inner dimensions match (K must equal) -infer_matmul_shape([2, 3], [4, 5]) → Error: 3 != 4 -``` - -**Convolution (conv2d):** -- Takes input shape, filter shape, strides, padding, dilations -- Computes spatial output dimensions: - ``` - output_h = floor((input_h + pad_top + pad_bottom - dilation_h * (kernel_h - 1) - 1) / stride_h + 1) - output_w = floor((input_w + pad_left + pad_right - dilation_w * (kernel_w - 1) - 1) / stride_w + 1) - ``` -- Validates channel compatibility and group constraints -- Handles multiple layouts: NCHW, NHWC (inputs) and OIHW, HWIO, OHWI, IHWO (filters) - -**Reshape:** -```rust -// Validates element count is preserved -validate_reshape([2, 3, 4], [6, 4]) → OK (24 elements in both) -validate_reshape([2, 3, 4], [5, 5]) → Error (24 != 25 elements) -``` - -**Pooling Operations:** -- Similar to convolution but without filters -- Computes output spatial dimensions based on window size, strides, padding -- Handles both average and max pooling -- Global pooling reduces spatial dimensions to 1x1 - -### Integration with Graph Builder - -Shape inference is called automatically during graph construction: - -```python -# Python API example -x = builder.input("x", [2, 3], "float32") # Shape: [2, 3] -y = builder.input("y", [3, 4], "float32") # Shape: [3, 4] -z = builder.matmul(x, y) # Shape: [2, 4] (inferred) -output = builder.relu(z) # Shape: [2, 4] (preserved) -``` - -When you call `builder.matmul(x, y)`, the implementation: -1. Calls `infer_matmul_shape([2, 3], [3, 4])` from `src/shape_inference.rs` -2. Gets result `[2, 4]` -3. Creates operand descriptor with inferred shape -4. Stores operation in graph with validated inputs/outputs - -This creates a fully-annotated, backend-agnostic graph that can be: -- Validated for correctness -- Visualized with Graphviz -- Converted to ONNX, CoreML, or other formats -- Executed on different backends without re-inference - -### Implementation Status - -All 85 WebNN operations have shape inference implemented (100% coverage). Each operation includes: -- Shape inference function in `src/shape_inference.rs` -- Comprehensive validation (dimension compatibility, parameter constraints) -- Unit tests covering typical cases and edge cases -- Error messages with context for debugging - -## File Organization - -``` -src/ -├── lib.rs # Public Rust API exports -├── main.rs # CLI entry point -├── graph.rs # Core data structures (backend-agnostic) -├── error.rs # Error types -├── validator.rs # Graph validation -├── loader.rs # JSON loading -├── graphviz.rs # DOT export -├── protos.rs # Protobuf module setup -├── converters/ -│ ├── mod.rs # Registry and trait -│ ├── onnx.rs # ONNX converter -│ ├── coreml_mlprogram.rs # CoreML converter (MIL) -│ ├── cann.rs # CANN converter (HiAI IR, placeholder) -│ └── litert.rs # LiteRT converter (TFLite flatbuffer) -├── backends/ -│ ├── mod.rs # DisabledContext + module registry -│ ├── ort.rs # ONNX Runtime backend -│ ├── trtx.rs # TensorRT backend (CUDA) -│ ├── coreml.rs # CoreML backend (macOS) -│ ├── cann.rs # CANN backend (Ascend NPU, OHOS) -│ └── litert.rs # LiteRT backend (TFLite) -├── executors/ -│ ├── mod.rs # Conditional compilation -│ ├── onnx.rs # ONNX runtime -│ ├── coreml.rs # CoreML runtime -│ └── coreml_shim.mm # CoreML shim (ObjC++) -└── python/ # Python bindings (PyO3) - ├── mod.rs # Python module definition - ├── context.rs # ML and MLContext classes (backend selection) - ├── graph_builder.rs # MLGraphBuilder class - ├── graph.rs # MLGraph class - ├── operand.rs # MLOperand class - └── tensor.rs # MLTensor class - -python/webnn/ # Python package -├── __init__.py # Package exports (AsyncMLContext) -└── __init__.pyi # Type stubs - -tests/ -├── run_wpt_conformance.rs # WPT conformance (libtest_mimic + MLGraphBuilder) -├── wpt_conformance/ # WPT harness modules -├── test_python_api.py # Python API tests (pywebnn) -└── test_integration.py # Integration tests - -examples/ -├── python_simple.py # Basic Python example -├── python_matmul.py # Matrix multiplication -├── mobilenetv2_complete.py # Complete pretrained MobileNetV2 -├── text_generation_gpt.py # Transformer with attention -└── train_text_model.py # Model training script -``` - -## Design Patterns - -### Registry Pattern (Converters) -- `ConverterRegistry` manages converters dynamically -- Trait objects: `Box` -- Extensible without modifying core code - -### Builder Pattern (Graph Construction) -- `MLGraphBuilder` provides fluent API for graph construction -- Incremental construction of complex structures -- Used in ONNX and CoreML converters - -### Validation Pipeline -- Immutable graph input -- Stateful validator with progressive checks -- Comprehensive artifacts returned for downstream use - -### Conditional Compilation -- `#[cfg(target_os = "macos")]` for platform-specific code -- `#[cfg(feature = "...")]` for optional features -- Graceful degradation on unsupported platforms - -## Technical Decisions - -1. **WebNN Spec Compliance**: Follows W3C WebNN Device Selection and MLTensor explainers -2. **Protobuf for Interop**: Native format for ONNX and CoreML -3. **Compile-time Codegen**: Protobufs compiled at build time -4. **Feature Flags**: Optional runtimes to minimize dependencies -5. **Objective-C FFI**: Direct CoreML access on macOS -6. **Zero-copy where possible**: `Bytes` type for efficiency -7. **Registry Pattern**: Pluggable converters without core changes - -## Platform Support - -- **Validation & Conversion**: Cross-platform (Linux, macOS, Windows) -- **TRTX Execution**: Linux/Windows with `trtx-runtime` feature (NVIDIA GPU) -- **LiteRT Execution**: Cross-platform with `litert-runtime` feature (CPU/GPU/NPU) -- **ONNX Execution**: Cross-platform with `onnx-runtime` feature (CPU/GPU) -- **CoreML Execution**: macOS only with `coreml-runtime` feature (GPU/Neural Engine) -- **CANN Execution**: OHOS only with `cann-runtime` feature (Huawei Ascend NPU, Kirin) -- **Neural Engine**: macOS with Apple Silicon (via CoreML) -- **Python Bindings**: Cross-platform with `python` feature (Python 3.11+) - -## Implementation Status - -**85 WebNN operations fully implemented** across all backends: - -- Shape Inference: 85/85 (100%) -- Python API: 85/85 (100%) -- ONNX Backend: 85/85 (100%) -- CoreML MLProgram: 85/85 (100%) -- LiteRT Backend: 56/85 -- TRTX Backend: 85/85 (100%) - -See [implementation-status.md](../development/implementation-status.md) for complete details. +## Data flow + +1. **Context creation.** `MLContext::create` resolves the WebNN hints and the rustnn hints to a + `BackendDevice` (`src/backend_selection.rs`) and instantiates the backend context, which owns + the device handles and the tensors. +2. **Recording.** `MLGraphBuilder` appends operands and `Operation` variants to a `GraphInfo`. + Every method runs shape inference (`src/shape_inference.rs`) for its outputs, so the graph is + fully annotated at all times. Constants are stored as bytes in the graph. +3. **Build.** `build` marks the outputs and hands the `GraphInfo` to the backend builder, which + calls the backend's converter and compiles the result once: an ONNX Runtime session, a + TensorRT engine with refittable weights, a compiled CoreML model, a LiteRT interpreter or a + HiAI model. The `MLGraph` keeps the compiled artifact and the named I/O descriptors. +4. **Dispatch.** `dispatch` validates the tensor bindings (unique tensors, names, shapes, data + types) with `runtime_checks`, then the backend binds or copies the tensors and runs. + +## Modules + +| Path | Responsibility | +|---|---| +| `src/lib.rs` | Crate docs (features, environment variables), module list, re-exports of the legacy API | +| `src/mlcontext.rs` | `MLContext`, `MLGraph`, `MLTensor`, `MLOperand`, descriptors, the crate-private backend traits | +| `src/mlcontextoptions.rs` | `MLContextOptions`, `MLPowerPreference`, `RustNNOptions`, `TrtxOptions` | +| `src/backend_selection.rs` | `Backend`, `BackendDevice`, `DeviceType` and the selection order | +| `src/mlgraphbuilder.rs` | `MLGraphBuilder`: inputs, constants, all operation methods (mostly macro-generated), build, save | +| `src/operators.rs` | `Operation` enum with one variant per WebNN operation, `op_type()`, JSON attribute parsing | +| `src/operator_options.rs` | `ML*Options` structs mirroring the spec dictionaries, `MLDimension` | +| `src/operator_enums.rs` | `MLOperandDataType` and the other spec enums | +| `src/shape_inference.rs` | Output shape and data type rules per operation | +| `src/graph.rs` | `GraphInfo`, `Operand`, `OperandDescriptor`, `Dimension`, `DataType`, 4-bit packing, hashing for caches | +| `src/validator.rs` | `GraphValidator`: structural checks, I/O descriptor maps, dependency order | +| `src/runtime_checks.rs` | Shape checks of tensor bindings at dispatch, including dynamic dimensions | +| `src/loader.rs`, `src/webnn_json.rs`, `src/webnn_save.rs` | `.webnn` text and JSON import through the `webnn-graph` crate, export, `.safetensors` weights | +| `src/converters/` | `GraphConverter` trait, `ConverterRegistry`, one converter per format (`onnx.rs`, `coreml_mlprogram.rs`, `trtx.rs` with `trtx_gru.rs`, `trtx_lstm.rs` and `trtx_rnn.rs`, `litert.rs`, `cann.rs`, `webnn.rs` for the browser) and shared helpers (`pool2d_shared.rs`, `weight_file_builder.rs`) | +| `src/backends/` | One module per backend implementing the backend traits; `caching.rs` for on-disk caches; `webnn/` with generated browser bindings; `mod.rs` with `DisabledContext` aliases for backends that are compiled out | +| `src/executors/` | Legacy one-shot execution of converted bytes (ONNX Runtime, TensorRT, CoreML), used by the CLI | +| `src/protos.rs`, `build.rs`, `protos/` | Protobuf (ONNX, CoreML) and flatbuffer (TFLite) schemas compiled at build time | +| `src/graphviz.rs`, `src/debug.rs`, `src/tensor.rs` | DOT export, `RUSTNN_DEBUG` helpers, host tensor helpers | +| `src/main.rs` | The `rustnn` CLI | +| `tests/run_wpt_conformance.rs`, `tests/wpt_conformance/` | WPT conformance harness, see the [WPT Conformance Guide](../testing/wpt-test-guide.md) | +| `tests/test_*_execution.rs` | Backend integration tests (TensorRT, LiteRT, CANN on device) | +| `scripts/` | WPT corpus fetch and report tooling, the operator report generator, git hooks | + +## Backend contract + +A backend implements two crate-private traits from `src/mlcontext.rs`: + +- `MLBackendContext`: `create_tensor`, `read_tensor`, `write_tensor`, `dispatch`, tensor + capacity and resize, and `create_builder`. +- `MLBackendBuilder`: `build(GraphInfo) -> MLGraph`. + +It also implements `ListDevices::list_devices()` for the selection code. Compiled artifacts are +stored in the `MLBackendGraph` enum. When a backend's feature is off, `backends/mod.rs` aliases +its context type to `DisabledContext`, so `MLContext` and the selection code compile under every +feature combination. The traits are crate-private on purpose: external backends are not +supported yet. + +## Graph model + +`GraphInfo` is the single backend-agnostic representation. Operands are addressed by index; +operations are variants of `Operation` with named fields for their operand indices and an +`Option`. This replaced a string-typed `op_type` plus JSON attributes design: +attribute names are checked at compile time and every converter matches on the enum. The same +model round-trips through the `webnn-graph` crate's text and JSON formats, which is how +onnx2webnn hands models to rustnn. + +Dynamic dimensions (`Dimension::Dynamic { name, max_size }`) are part of the model but only +accepted when the `dynamic-inputs` feature is enabled. + +## Legacy pipeline + +`load_graph_from_path` -> `GraphValidator` -> `ConverterRegistry::convert` -> `executors::*` +runs a stored graph without an `MLContext`. It predates the WebNN API and reloads the converted +model on every call. The CLI and two examples still use it; the converters are shared with the +backends. + +## Design decisions + +| Decision | Reason | +|---|---| +| Backend selection at context creation from hints | Follows the WebNN device selection explainer; the same graph code runs on every backend | +| Strongly typed `Operation` and `ML*Options` | Compile-time checking of operand wiring and attribute names across five converters | +| `BTreeMap` for `MLNamedOperands` and `MLNamedTensors` | Deterministic iteration order for the spec's record types | +| `Send + Sync` contexts and errors | Embeddings such as Servo dispatch from several threads; errors compose with `anyhow` | +| Native lowering for TensorRT, CoreML, LiteRT and CANN instead of going through ONNX | Avoids a second lowering and exposes backend features such as weight refit and caching | +| Refittable weights and a topology-keyed engine cache for TensorRT | Engine builds are expensive; weights change more often than topology | +| Feature flags per backend, mock features for TensorRT and CANN | Keeps the default build dependency-free and lets CI type-check every backend | +| Protobuf and flatbuffer codegen at build time | No checked-in generated code | +| Live WPT corpus as the conformance oracle, snapshots and expected-failure lists per backend | Upstream tests define the semantics; regressions show up as snapshot diffs | +| Source-generated operator support report | Documentation that cannot drift from the converters | + +## Platform support + +| Capability | Platforms | +|---|---| +| Validation, shape inference, conversion to ONNX and CoreML | Linux, macOS, Windows, wasm32 | +| ONNX Runtime execution | Linux, macOS, Windows | +| TensorRT-RTX execution | Linux and Windows with NVIDIA RTX GPUs | +| CoreML execution | macOS | +| LiteRT execution | Linux, macOS | +| CANN execution | OpenHarmony (aarch64) | +| Browser WebNN | wasm32 (bindings only) | diff --git a/docs/investigations/coreml-fixes-session-2025-12-14.md b/docs/archive/coreml-fixes-session-2025-12-14.md similarity index 96% rename from docs/investigations/coreml-fixes-session-2025-12-14.md rename to docs/archive/coreml-fixes-session-2025-12-14.md index 0f4ab3ef..ac736833 100644 --- a/docs/investigations/coreml-fixes-session-2025-12-14.md +++ b/docs/archive/coreml-fixes-session-2025-12-14.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # CoreML Backend Fixes - Session 2025-12-14 ## Summary diff --git a/docs/investigations/coreml-weight-files.md b/docs/archive/coreml-weight-files.md similarity index 95% rename from docs/investigations/coreml-weight-files.md rename to docs/archive/coreml-weight-files.md index cb905bb0..c3d7bda4 100644 --- a/docs/investigations/coreml-weight-files.md +++ b/docs/archive/coreml-weight-files.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # CoreML Weight Files Implementation Plan ## Overview diff --git a/docs/investigations/float16-investigation.md b/docs/archive/float16-investigation.md similarity index 95% rename from docs/investigations/float16-investigation.md rename to docs/archive/float16-investigation.md index d4d46869..6c3b6408 100644 --- a/docs/investigations/float16-investigation.md +++ b/docs/archive/float16-investigation.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # Float16 Investigation - CoreML Backend ## Problem Summary diff --git a/docs/integration/ggml.md b/docs/archive/ggml-integration-plan.md similarity index 97% rename from docs/integration/ggml.md rename to docs/archive/ggml-integration-plan.md index 4db910e5..52b080a4 100644 --- a/docs/integration/ggml.md +++ b/docs/archive/ggml-integration-plan.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # GGML Integration Guide **Date:** December 8, 2024 diff --git a/examples/README-minilm.md b/docs/archive/minilm-demo-readme.md similarity index 97% rename from examples/README-minilm.md rename to docs/archive/minilm-demo-readme.md index d10aa656..df5d5831 100644 --- a/examples/README-minilm.md +++ b/docs/archive/minilm-demo-readme.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-17).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # all-MiniLM-L6-v2 Text Embeddings Demo This demo shows how to use the **all-MiniLM-L6-v2** BERT-based model for generating text embeddings using PyWebNN. diff --git a/docs/development/operator-enum-refactor-execution.md b/docs/archive/operator-enum-refactor-execution.md similarity index 96% rename from docs/development/operator-enum-refactor-execution.md rename to docs/archive/operator-enum-refactor-execution.md index dace14c7..18847a77 100644 --- a/docs/development/operator-enum-refactor-execution.md +++ b/docs/archive/operator-enum-refactor-execution.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # Operator Enum Refactor — Execution Document This document is the step-by-step execution guide for the Operator enum refactor. Use it to execute specific steps and track progress. diff --git a/docs/testing/performance-benchmarks.md b/docs/archive/performance-benchmarks-2025-12.md similarity index 93% rename from docs/testing/performance-benchmarks.md rename to docs/archive/performance-benchmarks-2025-12.md index 4fe6f018..6e3c83c7 100644 --- a/docs/testing/performance-benchmarks.md +++ b/docs/archive/performance-benchmarks-2025-12.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # Performance Benchmarks This document contains performance benchmark results for the rustnn WebNN implementation across different backends. diff --git a/docs/plan_onnx_to_webnn_fix.md b/docs/archive/plan_onnx_to_webnn_fix.md similarity index 97% rename from docs/plan_onnx_to_webnn_fix.md rename to docs/archive/plan_onnx_to_webnn_fix.md index e4b20d37..c3571dc5 100644 --- a/docs/plan_onnx_to_webnn_fix.md +++ b/docs/archive/plan_onnx_to_webnn_fix.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # Plan: Fix ONNX → WebNN Conversion Divergence Audience: developer working on the ONNX→WebNN converter in `rust-webnn-graph`. diff --git a/docs/quantized-support-plan.md b/docs/archive/quantized-support-plan.md similarity index 96% rename from docs/quantized-support-plan.md rename to docs/archive/quantized-support-plan.md index d4de3575..5bb542ae 100644 --- a/docs/quantized-support-plan.md +++ b/docs/archive/quantized-support-plan.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + Quantized support plan (int4, int8, fp16) ========================================== diff --git a/docs/archive/tensorrt-integration-plan.md b/docs/archive/tensorrt-integration-plan.md new file mode 100644 index 00000000..398227ab --- /dev/null +++ b/docs/archive/tensorrt-integration-plan.md @@ -0,0 +1,855 @@ +> **Archived (2026-09-17).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + +# TensorRT Integration Guide + +**Date:** December 8, 2024 +**Purpose:** Guide for adding NVIDIA TensorRT converter and executor to rustnn + +--- + +## [TARGET] Overview + +This document outlines the integration of [NVIDIA TensorRT](https://developer.nvidia.com/tensorrt) as a fourth execution backend for rustnn, optimized for NVIDIA GPU inference alongside ONNX Runtime, CoreML, and GGML. + +**Why TensorRT?** +- **GPU-optimized inference**: Best-in-class performance on NVIDIA GPUs (RTX, A100, H100) +- **Advanced quantization**: FP16, INT8, INT4, FP8, FP4 for maximum throughput +- **JIT optimization**: Just-In-Time compilation for specific GPU architectures +- **Production-ready**: Widely deployed in NVIDIA-accelerated inference (Triton, TensorRT-LLM) +- **ONNX-native**: Primary import via ONNX format (perfect match for rustnn) + +**TensorRT for RTX (New in 2025):** +- Lightweight library (<200 MB) optimized for Windows 11 + NVIDIA RTX GPUs +- 50%+ performance improvement vs baseline DirectML +- JIT compilation in <30 seconds +- Supports Turing through Blackwell GPU generations + +--- + +## TensorRT Background + +### What is TensorRT? + +TensorRT is NVIDIA's high-performance deep learning inference SDK. It optimizes trained models through: +- **Layer fusion**: Combines operations to reduce kernel launches +- **Precision calibration**: INT8/FP16 quantization with minimal accuracy loss +- **Kernel auto-tuning**: Selects fastest implementation for target GPU +- **Dynamic tensor memory**: Minimizes memory footprint + +**Key Resources:** +- [TensorRT Documentation](https://docs.nvidia.com/deeplearning/tensorrt/latest/index.html) +- [TensorRT SDK](https://developer.nvidia.com/tensorrt) +- [TensorRT for RTX (Windows 11)](https://docs.nvidia.com/deeplearning/tensorrt-rtx/latest/index.html) +- [ONNX-TensorRT GitHub](https://github.com/onnx/onnx-tensorrt) + +### TensorRT Architecture + +**Core Workflow:** +``` +ONNX Model → TensorRT Builder → Optimized Engine → Inference Runtime +``` + +**Key Concepts:** +1. **Builder (`IBuilder`)**: Configures optimization settings (precision, batch size, workspace) +2. **Network (`INetworkDefinition`)**: Graph of layers and tensors +3. **Engine (`ICudaEngine`)**: Optimized executable for specific GPU + precision +4. **Context (`IExecutionContext`)**: Runtime state for executing inference +5. **Parser (`IParser`)**: Imports ONNX models into TensorRT network + +**Optimization Pipeline:** +```rust +// 1. Create builder and network +let builder = create_infer_builder(); +let network = builder.create_network(); + +// 2. Parse ONNX model +let parser = create_onnx_parser(network); +parser.parse_from_file("model.onnx"); + +// 3. Build optimized engine +let config = builder.create_builder_config(); +config.set_flag(BuilderFlag::FP16); // Enable FP16 +let engine = builder.build_engine(network, config); + +// 4. Execute inference +let context = engine.create_execution_context(); +context.execute_v2(&bindings); // Run inference +``` + +### Supported Operations + +**300+ ONNX Operators** (opset 9-20) including: + +**Binary Operations:** +- Add, Sub, Mul, Div, MatMul, Pow +- Broadcasting support + +**Activations:** +- Relu, Sigmoid, Tanh, Softmax, Gelu, Elu, LeakyRelu, PRelu, Selu, HardSigmoid, HardSwish, Softplus, Softsign + +**Convolution & Pooling:** +- Conv, ConvTranspose (2D and 3D) +- MaxPool, AveragePool, GlobalAveragePool, GlobalMaxPool +- LpPool (with restrictions) + +**Normalization:** +- BatchNormalization, InstanceNormalization, LayerNormalization, GroupNormalization, LRN + +**Reduction:** +- ReduceSum, ReduceMean, ReduceMax, ReduceMin, ReduceProd +- ReduceL1, ReduceL2, ReduceLogSum, ReduceLogSumExp, ReduceSumSquare + +**Tensor Manipulation:** +- Reshape, Transpose, Concat, Split, Slice, Gather, Scatter, Squeeze, Unsqueeze, Expand, Pad, Tile + +**Comparison & Logic:** +- Equal, Greater, GreaterOrEqual, Less, LessOrEqual +- And, Or, Xor, Not + +**Math Functions:** +- Abs, Neg, Ceil, Floor, Round, Sqrt, Exp, Log, Sin, Cos, Tan, Asin, Acos, Atan, Sinh, Cosh, Tanh, Asinh, Acosh, Atanh, Erf, Sign, Reciprocal + +**Advanced:** +- LSTM, GRU (with restrictions) +- Attention mechanisms +- Einsum +- TopK, ArgMax, ArgMin +- Cast, Clip, Where + +**Quantization:** +- QuantizeLinear, DequantizeLinear + +**Data Types:** +DOUBLE, FLOAT32, FLOAT16, BFLOAT16, INT32, INT64, FP8, INT8, INT4, UINT8, BOOL + +**Important Limitations:** +- DOUBLE cast to FLOAT32 (with clamping) +- UINT8 only for input/output tensors +- INT8/INT4/FP8 require quantization from FP32/FP16 +- Some ops restricted to 2D/3D (e.g., pooling) + +--- + +## Integration Architecture + +### Following rustnn Patterns + +rustnn uses a **converter + executor** pattern: + +``` +WebNN GraphInfo → Converter → ONNX → TensorRT Engine → Executor → Results +``` + +**Existing Backends:** +1. **ONNX Runtime**: Cross-platform, protobuf → ONNX Runtime execution +2. **CoreML**: macOS-only, protobuf → CoreML execution +3. **GGML**: CPU-optimized, in-memory graph → GGML execution + +**New TensorRT Backend:** +4. **TensorRT**: NVIDIA GPU, ONNX → TensorRT Engine → GPU execution + +**Key Advantage:** We already have ONNX converter! TensorRT can consume ONNX directly. + +### File Structure + +``` +src/ + converters/ + mod.rs # Already has OnnxConverter (reuse!) + onnx.rs + coreml_mlprogram.rs + ggml.rs + tensorrt.rs # NEW: TensorRT-specific converter (optional) + executors/ + mod.rs # Add #[cfg(feature = "tensorrt-runtime")] + onnx.rs + coreml.rs + ggml.rs + tensorrt.rs # NEW: TensorRT executor + python/ + context.rs # Add Backend::TensorRT variant +``` + +--- + +## Implementation Plan + +### Phase 1: Executor (ONNX → TensorRT Engine) + +**File:** `src/executors/tensorrt.rs` + +**Feature Gate:** `#[cfg(feature = "tensorrt-runtime")]` + +**Strategy:** Reuse existing ONNX converter, build TensorRT engine from ONNX bytes + +**Implementation:** +```rust +#![cfg(feature = "tensorrt-runtime")] + +use crate::error::GraphError; +use crate::graph::{GraphInfo, OperandDescriptor}; +use std::collections::HashMap; + +pub struct TensorRTOutput { + pub name: String, + pub shape: Vec, + pub data: Vec, +} + +pub struct TensorRTInput { + pub name: String, + pub shape: Vec, + pub data: Vec, +} + +/// Execute TensorRT inference from ONNX model bytes +pub fn run_tensorrt_with_inputs( + onnx_model: &[u8], + inputs: HashMap, + precision: TensorRTPrecision, +) -> Result, GraphError> { + // 1. Create TensorRT builder + let logger = create_logger(); + let builder = create_infer_builder(&logger)?; + + // 2. Parse ONNX model + let network_flags = 1u32 << NetworkDefinitionCreationFlag::ExplicitBatchDimensions as u32; + let network = builder.create_network_v2(network_flags)?; + + let parser = create_onnx_parser(&network, &logger)?; + parser.parse(onnx_model)?; + + // 3. Configure builder + let config = builder.create_builder_config()?; + config.set_memory_pool_limit(MemoryPoolType::Workspace, 1 << 30)?; // 1GB + + // Set precision mode + match precision { + TensorRTPrecision::FP32 => {}, + TensorRTPrecision::FP16 => config.set_flag(BuilderFlag::FP16)?, + TensorRTPrecision::INT8 => config.set_flag(BuilderFlag::INT8)?, + } + + // 4. Build engine + let engine = builder.build_serialized_network(&network, &config)?; + let runtime = create_infer_runtime(&logger)?; + let engine = runtime.deserialize_cuda_engine(&engine)?; + + // 5. Create execution context + let context = engine.create_execution_context()?; + + // 6. Allocate GPU buffers and copy inputs + let bindings = allocate_and_copy_inputs(&engine, inputs)?; + + // 7. Execute inference + context.execute_v2(&bindings)?; + + // 8. Copy outputs back to CPU + let outputs = copy_outputs_from_gpu(&engine, &bindings)?; + + Ok(outputs) +} + +#[derive(Debug, Clone, Copy)] +pub enum TensorRTPrecision { + FP32, + FP16, + INT8, +} +``` + +**Key Challenges:** +1. **Rust bindings**: Use `tensorrt-rs` or `easy-tensorrt-sys` (FFI to C++ API) +2. **GPU memory management**: Allocate CUDA buffers for inputs/outputs +3. **Engine caching**: Serialized engines can be cached for faster startup +4. **Precision selection**: FP32/FP16/INT8 based on device hints +5. **Batch size**: Dynamic batch support vs fixed batch + +### Phase 2: Feature Flag & Dependencies + +**File:** `Cargo.toml` + +**Changes:** +```toml +[features] +default = [] +coreml-runtime = ["objc"] +onnx-runtime = ["onnxruntime"] +ggml-runtime = ["ggml"] +tensorrt-runtime = ["tensorrt-rs", "cuda-runtime"] # NEW + +[dependencies] +# ... existing dependencies ... +tensorrt-rs = { version = "0.8", optional = true } # NEW +cuda-runtime = { version = "0.7", optional = true } # NEW +# Alternative: easy-tensorrt-sys for more recent bindings +``` + +**Rust Bindings Options:** + +| Crate | Status | Notes | +|-------|--------|-------| +| `tensorrt-rs` | Older (2020) | Supports TensorRT 5-7, may need fork | +| `easy-tensorrt-sys` | Newer fork | Uses `cudarc` instead of old `cuda-rs` | +| Custom FFI | Most control | Bindgen to TensorRT C++ API | + +**Recommendation:** Start with `easy-tensorrt-sys` or custom FFI for TensorRT 10.x support + +### Phase 3: Registration + +**File:** `src/executors/mod.rs` + +**Changes:** +```rust +#[cfg(all(target_os = "macos", feature = "coreml-runtime"))] +pub mod coreml; +#[cfg(feature = "onnx-runtime")] +pub mod onnx; +#[cfg(feature = "ggml-runtime")] +pub mod ggml; +#[cfg(feature = "tensorrt-runtime")] // NEW +pub mod tensorrt; +``` + +**File:** `src/converters/mod.rs` + +**No changes needed!** Reuse existing `OnnxConverter` to generate ONNX bytes, then TensorRT executor parses ONNX directly. + +### Phase 4: Python API Integration + +**File:** `src/python/context.rs` + +**Changes:** +```rust +#[derive(Debug, Clone)] +enum Backend { + OnnxCpu, + OnnxGpu, + CoreML, + Ggml, + TensorRT, // NEW + None, +} + +impl PyMLContext { + fn select_backend(accelerated: bool, power: &str) -> (Backend, bool) { + // TensorRT selection logic + if accelerated { + #[cfg(feature = "tensorrt-runtime")] + if is_nvidia_gpu_available() { + // Prefer TensorRT on NVIDIA GPUs for high-performance + if power == "high-performance" { + return (Backend::TensorRT, true); + } + } + } + + // Existing logic for ONNX/CoreML/GGML... + } + + fn compute_tensorrt( + &self, + graph: &PyMLGraph, + inputs: HashMap>>>, + ) -> Result>>>, GraphError> { + #[cfg(feature = "tensorrt-runtime")] + { + use crate::converters::OnnxConverter; // Reuse ONNX converter! + use crate::executors::tensorrt::{run_tensorrt_with_inputs, TensorRTInput, TensorRTPrecision}; + + // 1. Convert GraphInfo to ONNX + let converter = OnnxConverter::default(); + let converted = converter.convert(&graph.graph)?; + + // 2. Convert inputs to TensorRTInput + let trt_inputs = convert_numpy_to_tensorrt(inputs)?; + + // 3. Execute with TensorRT + let precision = TensorRTPrecision::FP16; // Could be configurable + let outputs = run_tensorrt_with_inputs(&converted.data, trt_inputs, precision)?; + + // 4. Convert outputs back to NumPy + convert_tensorrt_to_numpy(outputs) + } + #[cfg(not(feature = "tensorrt-runtime"))] + Err(GraphError::BackendUnavailable { + backend: "TensorRT".to_string(), + }) + } +} + +#[cfg(feature = "tensorrt-runtime")] +fn is_nvidia_gpu_available() -> bool { + // Check for CUDA-capable NVIDIA GPU + // Could use cuda-runtime or parse nvidia-smi + std::process::Command::new("nvidia-smi") + .output() + .map(|output| output.status.success()) + .unwrap_or(false) +} +``` + +### Phase 5: Engine Caching (Performance Optimization) + +**Problem:** TensorRT engine building can take 10-60 seconds on first run. + +**Solution:** Cache serialized engines to disk, keyed by model hash + GPU architecture. + +**Implementation:** +```rust +use std::path::PathBuf; +use std::fs; +use sha2::{Sha256, Digest}; + +fn get_engine_cache_path(onnx_model: &[u8], gpu_arch: &str, precision: TensorRTPrecision) -> PathBuf { + let mut hasher = Sha256::new(); + hasher.update(onnx_model); + hasher.update(gpu_arch.as_bytes()); + hasher.update(format!("{:?}", precision).as_bytes()); + let hash = format!("{:x}", hasher.finalize()); + + PathBuf::from(format!(".tensorrt_cache/engine_{}.trt", hash)) +} + +pub fn run_tensorrt_with_caching( + onnx_model: &[u8], + inputs: HashMap, + precision: TensorRTPrecision, +) -> Result, GraphError> { + let gpu_arch = get_gpu_architecture()?; // e.g., "sm_89" for RTX 4090 + let cache_path = get_engine_cache_path(onnx_model, &gpu_arch, precision); + + let engine = if cache_path.exists() { + // Load cached engine + let serialized = fs::read(&cache_path)?; + let runtime = create_infer_runtime(&logger)?; + runtime.deserialize_cuda_engine(&serialized)? + } else { + // Build new engine + let engine = build_engine(onnx_model, precision)?; + + // Cache for future use + let serialized = engine.serialize()?; + fs::create_dir_all(cache_path.parent().unwrap())?; + fs::write(&cache_path, serialized)?; + + engine + }; + + // Execute with cached/new engine + execute_engine(engine, inputs) +} +``` + +--- + +## [STATS] Operation Coverage Analysis + +### WebNN Operations → TensorRT Support + +| WebNN Operation | TensorRT Support | Notes | +|----------------|------------------|-------| +| **Binary Ops** | | | +| `add`, `sub`, `mul`, `div` | [OK] Full | Via Add, Sub, Mul, Div | +| `matmul` | [OK] Full | Via MatMul | +| `pow` | [OK] Full | Via Pow | +| **Activations** | | | +| `relu`, `sigmoid`, `tanh`, `softmax` | [OK] Full | Native support | +| `gelu`, `elu`, `leakyRelu`, `prelu` | [OK] Full | Native support | +| `hardSigmoid`, `hardSwish`, `softplus`, `softsign` | [OK] Full | Native support | +| **Convolution** | | | +| `conv2d`, `convTranspose2d` | [OK] Full | 2D and 3D supported | +| **Pooling** | | | +| `averagePool2d`, `maxPool2d` | [OK] Full | 2D/3D, indices unsupported for MaxPool | +| `globalAveragePool`, `globalMaxPool` | [OK] Full | Native support | +| **Normalization** | | | +| `batchNormalization` | [OK] Full | Native support | +| `instanceNormalization` | [OK] Full | Native support | +| `layerNormalization` | [OK] Full | Native support | +| **Reduction** | | | +| All `reduce*` operations | [OK] Full | 10 reduction ops supported | +| **Tensor Ops** | | | +| `reshape`, `transpose`, `concat`, `split` | [OK] Full | Native support | +| `slice`, `gather`, `scatter`, `pad`, `tile` | [OK] Full | Native support | +| `squeeze`, `unsqueeze`, `expand` | [OK] Full | Native support | +| **Logic** | | | +| All comparison and logical ops | [OK] Full | 9 ops supported | +| **Math** | | | +| All element-wise math | [OK] Full | 23 ops supported | +| **Quantization** | | | +| `quantizeLinear`, `dequantizeLinear` | [OK] Full | Native support | +| **Advanced** | | | +| `argMax`, `argMin` | [OK] Full | Via ArgMax, ArgMin | +| `cast`, `clamp`, `where` | [OK] Full | Via Cast, Clip, Where | +| `gemm` | [OK] Full | Via Gemm | + +**Coverage:** ~95%+ of WebNN spec (TensorRT has 300+ ONNX ops, WebNN has 85-95 ops) + +**Not Supported:** +- Some RNN/LSTM restrictions (bidirectional requires matching activations) +- MaxPool indices output +- Certain dilation/padding combinations +- DOUBLE precision (cast to FLOAT32) + +--- + +## Challenges & Solutions + +### Challenge 1: Rust Bindings Maturity + +**Problem:** Existing Rust bindings (`tensorrt-rs`) are outdated (TensorRT 5-7, last update 2020). + +**Solutions:** +1. **Use `easy-tensorrt-sys`**: Newer fork with better CUDA integration via `cudarc` +2. **Create custom FFI**: Use `bindgen` to generate fresh bindings for TensorRT 10.x +3. **Fork and update `tensorrt-rs`**: Modernize existing crate for TensorRT 10.x +4. **Wait for official bindings**: NVIDIA may release official Rust support (unlikely short-term) + +**Recommendation:** Create custom FFI bindings for TensorRT 10.x C++ API using `bindgen`. Focus on core interfaces: IBuilder, INetworkDefinition, IExecutionContext, IParser. + +### Challenge 2: CUDA Dependency + +**Problem:** TensorRT requires CUDA toolkit and NVIDIA GPU runtime. + +**Solutions:** +- **Feature flag**: Only enable with `tensorrt-runtime` feature +- **Runtime detection**: Check for NVIDIA GPU before selecting backend +- **Clear errors**: Provide helpful error if CUDA unavailable +- **Documentation**: Document CUDA installation requirements + +### Challenge 3: Engine Build Time + +**Problem:** Building TensorRT engine can take 10-60 seconds on first run. + +**Solutions:** +- **Engine caching**: Serialize engines to disk, key by model hash + GPU arch +- **Ahead-of-time compilation**: Pre-build engines for target GPUs +- **JIT progress**: Show progress during engine building +- **TensorRT for RTX**: JIT compilation in <30 seconds (Windows 11) + +### Challenge 4: Precision Selection + +**Problem:** TensorRT supports FP32, FP16, INT8, FP8, FP4. How to select? + +**Solutions:** +- Follow WebNN device hints: + - `power="high-performance"` → FP16 (2x faster than FP32) + - `power="default"` → FP16 + - `power="low-power"` → INT8 (requires calibration) +- Add optional precision parameter to `compute()` +- Auto-detect GPU capability (e.g., FP8 only on Ada/Hopper) + +### Challenge 5: Platform Support + +**Problem:** TensorRT is NVIDIA GPU-only (Linux, Windows). No macOS/AMD support. + +**Solutions:** +- **Runtime detection**: Check for NVIDIA GPU at context creation +- **Graceful fallback**: Fall back to ONNX Runtime if TensorRT unavailable +- **Clear documentation**: Document platform requirements +- **Windows focus**: Leverage TensorRT for RTX (Windows 11 + RTX GPUs) + +### Challenge 6: Dynamic Shapes + +**Problem:** TensorRT engines can have fixed or dynamic input shapes. + +**Solutions:** +- **Use explicit batch**: Set `ExplicitBatchDimensions` flag +- **Optimization profiles**: Define min/opt/max shapes for dynamic inputs +- **Runtime binding**: Bind shapes at execution time +- **Future work**: Add dynamic shape support incrementally + +--- + +## [TARGET] Implementation Roadmap + +### Phase 1: Proof of Concept (2-3 days) +- [ ] Research TensorRT C++ API and identify core interfaces needed +- [ ] Create minimal FFI bindings using `bindgen` for TensorRT 10.x +- [ ] Implement basic executor for ONNX → TensorRT → inference +- [ ] Test with simple operation (add, matmul) on NVIDIA GPU +- [ ] Validate FP32 precision works correctly + +### Phase 2: Core Functionality (5-7 days) +- [ ] Expand FFI bindings for full IBuilder/INetworkDefinition API +- [ ] Implement ONNX parser integration +- [ ] Add FP16/INT8 precision support +- [ ] Implement GPU memory management (CUDA buffers) +- [ ] Add error handling and validation +- [ ] Test with 20+ WebNN operations + +### Phase 3: Performance Optimization (3-5 days) +- [ ] Implement engine caching to disk +- [ ] Add engine serialization/deserialization +- [ ] Optimize memory allocation/deallocation +- [ ] Add batch size optimization +- [ ] Profile and benchmark vs ONNX Runtime + +### Phase 4: Python Integration (2-3 days) +- [ ] Add Backend::TensorRT to context selection +- [ ] Implement `compute_tensorrt()` method +- [ ] Add NVIDIA GPU detection +- [ ] Add device selection logic (prefer TensorRT on NVIDIA) +- [ ] Test with Python API examples + +### Phase 5: Documentation & Testing (2-3 days) +- [ ] Update docs/implementation-status.md with TensorRT coverage +- [ ] Update docs/architecture.md with TensorRT backend +- [ ] Create example: `examples/tensorrt_inference.py` +- [ ] Add comprehensive unit tests (Rust + Python) +- [ ] Document CUDA installation requirements +- [ ] Update README.md with TensorRT backend section + +### Phase 6: Advanced Features (Future) +- [ ] TensorRT for RTX support (Windows 11) +- [ ] INT8 calibration for quantization +- [ ] Dynamic shape support +- [ ] Multi-stream execution +- [ ] DLA (Deep Learning Accelerator) support +- [ ] TensorRT-LLM integration for transformer models + +**Total Estimated Time:** 14-21 days for phases 1-5 + +--- + +## Testing Strategy + +### Unit Tests (Rust) + +**File:** `src/executors/tensorrt.rs` +```rust +#[cfg(all(test, feature = "tensorrt-runtime"))] +mod tests { + use super::*; + + #[test] + fn builds_engine_from_onnx() { + let onnx_model = create_simple_add_onnx(); + let logger = create_logger(); + let builder = create_infer_builder(&logger).unwrap(); + assert!(builder.is_valid()); + } + + #[test] + fn executes_add_operation() { + if !is_nvidia_gpu_available() { + eprintln!("Skipping test: No NVIDIA GPU available"); + return; + } + + let onnx_model = create_simple_add_onnx(); + let inputs = create_test_inputs(); + let outputs = run_tensorrt_with_inputs(&onnx_model, inputs, TensorRTPrecision::FP32).unwrap(); + + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].shape, vec![2, 3]); + // Verify output values + } + + #[test] + fn fp16_precision_works() { + // Test FP16 execution + } + + #[test] + fn engine_caching_works() { + // Test cache hit/miss + } +} +``` + +### Python Tests + +**File:** `tests/test_tensorrt_backend.py` +```python +import pytest +import webnn +import numpy as np +import subprocess + +def has_nvidia_gpu(): + """Check if NVIDIA GPU is available""" + try: + result = subprocess.run(["nvidia-smi"], capture_output=True) + return result.returncode == 0 + except FileNotFoundError: + return False + +def has_tensorrt_runtime(): + """Check if TensorRT runtime is available""" + try: + import webnn._rustnn as rustnn + return hasattr(rustnn, 'tensorrt_available') + except: + return False + +@pytest.mark.skipif(not has_nvidia_gpu(), reason="No NVIDIA GPU available") +@pytest.mark.skipif(not has_tensorrt_runtime(), reason="TensorRT runtime not available") +def test_tensorrt_add(): + ml = webnn.ML() + context = ml.create_context(accelerated=True, power_preference="high-performance") + + # Should select TensorRT on NVIDIA GPU + assert context.backend == "tensorrt" + + builder = context.create_graph_builder() + x = builder.input("x", [2, 3], "float32") + y = builder.input("y", [2, 3], "float32") + z = builder.add(x, y) + + graph = builder.build({"output": z}) + + inputs = { + "x": np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32), + "y": np.array([[1, 1, 1], [2, 2, 2]], dtype=np.float32), + } + + outputs = context.compute(graph, inputs) + expected = np.array([[2, 3, 4], [6, 7, 8]], dtype=np.float32) + np.testing.assert_allclose(outputs["output"], expected) + +@pytest.mark.skipif(not has_nvidia_gpu(), reason="No NVIDIA GPU available") +def test_tensorrt_fp16_precision(): + # Test FP16 execution + pass + +@pytest.mark.skipif(not has_nvidia_gpu(), reason="No NVIDIA GPU available") +def test_tensorrt_mobilenet(): + # Test full MobileNetV2 model on TensorRT + pass +``` + +### Performance Benchmarks + +**File:** `benchmarks/tensorrt_vs_onnx.py` +```python +import time +import webnn +import numpy as np + +def benchmark_backend(backend_name, accelerated, power_preference): + ml = webnn.ML() + context = ml.create_context(accelerated=accelerated, power_preference=power_preference) + + # Build MobileNetV2 graph + graph = build_mobilenetv2(context) + + # Warmup + for _ in range(5): + context.compute(graph, inputs) + + # Benchmark + times = [] + for _ in range(100): + start = time.perf_counter() + outputs = context.compute(graph, inputs) + times.append(time.perf_counter() - start) + + return { + "backend": backend_name, + "mean_ms": np.mean(times) * 1000, + "std_ms": np.std(times) * 1000, + "min_ms": np.min(times) * 1000, + "max_ms": np.max(times) * 1000, + } + +# Compare backends +onnx_gpu = benchmark_backend("ONNX GPU", True, "high-performance") +tensorrt = benchmark_backend("TensorRT", True, "high-performance") + +print(f"ONNX GPU: {onnx_gpu['mean_ms']:.2f}ms ± {onnx_gpu['std_ms']:.2f}ms") +print(f"TensorRT: {tensorrt['mean_ms']:.2f}ms ± {tensorrt['std_ms']:.2f}ms") +print(f"Speedup: {onnx_gpu['mean_ms'] / tensorrt['mean_ms']:.2f}x") +``` + +### Makefile Targets + +```makefile +# Add to Makefile +.PHONY: tensorrt-dev +tensorrt-dev: + maturin develop --features python,tensorrt-runtime + +.PHONY: test-tensorrt +test-tensorrt: + cargo test --features tensorrt-runtime + pytest tests/test_tensorrt_backend.py -v + +.PHONY: benchmark-tensorrt +benchmark-tensorrt: + python benchmarks/tensorrt_vs_onnx.py +``` + +--- + +## References + +### TensorRT Resources +- [TensorRT Documentation](https://docs.nvidia.com/deeplearning/tensorrt/latest/index.html) +- [TensorRT SDK](https://developer.nvidia.com/tensorrt) +- [TensorRT Architecture Overview](https://docs.nvidia.com/deeplearning/tensorrt/latest/architecture/architecture-overview.html) +- [TensorRT for RTX (Windows 11)](https://docs.nvidia.com/deeplearning/tensorrt-rtx/latest/index.html) +- [TensorRT for RTX Announcement](https://developer.nvidia.com/blog/nvidia-tensorrt-for-rtx-introduces-an-optimized-inference-ai-library-on-windows/) +- [Run High-Performance AI with TensorRT for RTX](https://developer.nvidia.com/blog/run-high-performance-ai-applications-with-nvidia-tensorrt-for-rtx/) + +### ONNX-TensorRT +- [ONNX-TensorRT GitHub](https://github.com/onnx/onnx-tensorrt) +- [Supported ONNX Operators](https://github.com/onnx/onnx-tensorrt/blob/main/docs/operators.md) +- [TensorRT Support Matrix](https://docs.nvidia.com/deeplearning/tensorrt/latest/getting-started/support-matrix.html) + +### Rust Bindings +- [tensorrt-rs (GitHub)](https://github.com/mstallmo/tensorrt-rs) +- [tensorrt-rs (crates.io)](https://crates.io/crates/tensorrt-rs) +- [easy-tensorrt-sys (crates.io)](https://crates.io/crates/easy-tensorrt-sys) +- [TensorRT-sys](https://lib.rs/crates/tensorrt-sys) + +### WebNN Spec +- [W3C WebNN API Specification](https://www.w3.org/TR/webnn/) +- [WebNN Device Selection Explainer](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md) + +### Related Projects +- [TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) +- [NVIDIA Triton Inference Server](https://github.com/triton-inference-server/server) +- [Torch-TensorRT](https://github.com/pytorch/TensorRT) + +--- + +## Summary + +**TensorRT Integration Value:** +- [OK] **Best GPU performance** on NVIDIA hardware (RTX, A100, H100) +- [OK] **Advanced quantization** (FP16, INT8, FP8, FP4) +- [OK] **Production-ready** (widely deployed in NVIDIA ecosystem) +- [OK] **ONNX-native** (reuse existing ONNX converter) +- [OK] **95%+ operation coverage** (300+ ONNX ops) +- [OK] **TensorRT for RTX** (optimized for Windows 11 + RTX GPUs) + +**Key Design Decisions:** +1. **Reuse ONNX converter** (no new converter needed!) +2. **Custom FFI bindings** for TensorRT 10.x C++ API +3. **Engine caching** to avoid rebuild overhead +4. **FP16 default** for 2x speedup over FP32 +5. **Prefer TensorRT** on NVIDIA GPUs with `accelerated=True` + `power="high-performance"` +6. **Graceful fallback** to ONNX Runtime if TensorRT unavailable + +**Platform Support:** +- **Primary**: Linux + NVIDIA GPU (CUDA) +- **Secondary**: Windows 11 + NVIDIA RTX GPU (TensorRT for RTX) +- **Not supported**: macOS (no NVIDIA GPU), AMD GPUs + +**Next Steps:** +1. Create FFI bindings for TensorRT 10.x +2. Implement basic executor with FP32 support +3. Add FP16/INT8 precision modes +4. Implement engine caching +5. Integrate with Python API +6. Benchmark vs ONNX Runtime GPU + +--- + +**Status:** Planning document (not yet implemented) + +**Estimated Effort:** 14-21 days for full integration with caching and FP16/INT8 support diff --git a/docs/archive/todo-2025-12.txt b/docs/archive/todo-2025-12.txt new file mode 100644 index 00000000..0c86d9da --- /dev/null +++ b/docs/archive/todo-2025-12.txt @@ -0,0 +1,572 @@ +ARCHIVED (2026-09-17). Historical TODO list kept for reference only. It tracks the removed +Python bindings and stale operation counts. The current status lives in +docs/development/implementation-status.md and the GitHub issue tracker. + +# TODO - Future Implementation Tasks + +## Python API - Core Functionality + +### Execution Engine +[x] Implement actual tensor execution in MLContext.compute() + - Integrated with ONNX runtime + - Accepts numpy arrays as inputs + - Returns actual computed outputs as numpy arrays + - Includes fallback to zeros when ONNX runtime not available + +[x] Add MLTensor class for explicit tensor management + - createTensor() for pre-allocating tensors + - readTensor() for reading results + - writeTensor() for setting input data + +[x] Implement async execution support + - WebNN spec uses async/await + - Python asyncio integration via AsyncMLContext wrapper + - Non-blocking compute operations with dispatch() + +### Operations - Missing Implementations + +[x] Convolution operations + - [x] conv2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) + - [x] convTranspose2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) + - [x] depthwiseConv2d (DONE: use conv2d with groups=in_channels parameter) + +[ ] Pooling operations + - [x] averagePool2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) + - [x] maxPool2d (DONE: shape inference, Python API, ONNX/CoreML converters, 8 tests) + - l2Pool2d + - [x] globalAveragePool (DONE: shape inference, Python API, ONNX/CoreML converters, 6 tests) + - [x] globalMaxPool (DONE: shape inference, Python API, ONNX/CoreML converters, 6 tests) + +[ ] Normalization operations + - [x] batchNormalization (DONE: shape inference, Python API, ONNX converter, 3 tests) + - [x] instanceNormalization (DONE: shape inference, Python API, ONNX converter, 4 tests) + - [x] layerNormalization (DONE: shape inference, Python API, ONNX converter, 5 tests) + - [ ] localResponseNormalization (SKIPPED: Not in W3C WebNN spec as of 2025-12-07; W3C decision to use decomposition in higher layers due to rarity and backend inconsistencies) + +[x] Reduction operations (DONE: shape inference, Python API, ONNX/CoreML converters, 18 tests - all passing) + - [x] reduceSum (ONNX: ReduceSum, CoreML: ReduceSumLayerParams) + - [x] reduceMean (ONNX: ReduceMean, CoreML: ReduceMeanLayerParams) + - [x] reduceMax (ONNX: ReduceMax, CoreML: ReduceMaxLayerParams) + - [x] reduceMin (ONNX: ReduceMin, CoreML: ReduceMinLayerParams) + - [x] reduceProduct (ONNX: ReduceProd, CoreML: ReduceProdLayerParams) + - [x] reduceL1 (ONNX: ReduceL1, CoreML: ReduceL1LayerParams) + - [x] reduceL2 (ONNX: ReduceL2, CoreML: ReduceL2LayerParams) + - [x] reduceLogSum (ONNX: ReduceLogSum, CoreML: ReduceLogSumLayerParams) + - [x] reduceLogSumExp (ONNX: ReduceLogSumExp, CoreML: ReduceLogSumExpLayerParams) + - [x] reduceSumSquare (ONNX: ReduceSumSquare, CoreML: ReduceSumSquareLayerParams) + +[x] Element-wise operations (DONE: shape inference, Python API, ONNX/CoreML converters, 23 tests - all passing, 6 WPT test files) + - [x] Basic math: abs, ceil, floor, round, neg, sign (CoreML: dedicated layers + multiply workaround for neg) + - [x] Exponential/log: exp, log, sqrt, reciprocal (ONNX: capitalized names, CoreML: UnaryFunctionLayerParams) + - [x] Trigonometric: sin, cos, tan, asin, acos, atan (ONNX/CoreML: dedicated layer types) + - [x] Hyperbolic: sinh, cosh, asinh, acosh, atanh (ONNX/CoreML: dedicated layer types) + - [x] Special functions: erf, identity (CoreML: ErfLayerParams, multiply workaround for identity) + - [x] WPT conformance test data: abs, ceil, floor, exp, log, sqrt (14 test cases total) + +[x] Logic operations (DONE: shape inference, Python API, ONNX/CoreML converters with Cast node insertion, 9 tests - all passing) + - [x] Comparison operations: equal, greater, greaterOrEqual, lesser, lesserOrEqual (ONNX: Equal→Cast(bool→uint8), Greater→Cast, GreaterOrEqual→Cast, Less→Cast, LessOrEqual→Cast; CoreML: dedicated layer types with alpha=0.0) + - [x] Logical NOT: logicalNot (ONNX: Cast(input→bool)→Not→Cast(bool→uint8); CoreML: LogicalNotLayerParams) - unary operation + - [x] Logical operations: logicalAnd, logicalOr, logicalXor (ONNX: Cast(inputs→bool)→[And/Or/Xor]→Cast(bool→uint8); CoreML: dedicated layer types) + - [x] ONNX Cast node insertion: Automatically inserts Cast nodes to handle WebNN uint8 boolean type vs ONNX bool type + - Implementation details: create_cast_node() helper with AttributeType::Int, Cast nodes inserted in convert() for all logic operations + +[ ] Advanced operations + - concat (concatenate tensors) + - expand (broadcast dimensions) + - gather, scatter + - slice (extract sub-tensors) + - split (split tensor into parts) + - squeeze (remove dimensions of size 1) + - tile (repeat tensor) + - transpose + - where (conditional selection) + - pad (add padding) + - prelu, elu, leakyRelu, hardSigmoid, hardSwish, gelu + - softplus, softsign + +[ ] Recurrent operations (DEFERRED - See rationale below) + - gru, gruCell + - lstm, lstmCell + + **Deferral Rationale (2025-12-08):** + - These are complex composite operations (10-15 parameters each, ~2000-3000 LOC) + - WebNN spec debate about removing them in favor of lower-level primitives + - LSTM/GRU largely obsoleted by Transformers in modern ML + - WPT tests exist but implementation priority is low + - Focus on simpler, more widely-used operations first (concat, gather, slice, pad, etc.) + - Can revisit if/when spec stabilizes and user demand exists + +[x] Quantization operations (2025-12-08) + - dequantizeLinear: Converts quantized integers to float32 + - quantizeLinear: Converts float32 to quantized integers + - Shape inference: Preserves input shape + - ONNX support: ✅ Fully implemented, maps to DequantizeLinear/QuantizeLinear ops + - CoreML support: ✅ FULLY MIGRATED to MLProgram format (2025-12-08) + + CoreML Migration (2025-12-08): + - ✅ Migrated from NeuralNetwork (legacy) to MLProgram (modern) format + - ✅ Removed old src/converters/coreml.rs (NeuralNetwork-based) + - ✅ Implemented src/converters/coreml_mlprogram.rs (MIL-based) + - ✅ All 50+ WebNN operations now map to MIL operations + - ✅ Quantization supported via MIL "dequantize" and "quantize" ops + - ✅ Uses CoreML spec v7+ (iOS 15+, macOS 12+) + - ✅ Matches Chromium's MLProgram implementation + - ✅ Tested with simple operations (add) + - ⏸️ Complex operation parameters (conv padding, pool strides) deferred + + - Tests: 5 tests added (test_dequantize_linear, test_quantize_linear, uint8 variants, roundtrip) + +[x] Shape inference and broadcasting + - Automatic shape computation for operations + - Broadcasting rules for binary operations (NumPy-style) + - Shape validation at graph build time + - Proper matmul shape inference with batching support + +### CoreML Converter - MLProgram Format (Migrated 2025-12-08) + +[x] Migration to MLProgram (DONE: 2025-12-08) + - ✅ Replaced NeuralNetwork converter with MLProgram converter + - ✅ All operations now map to MIL operations + - ✅ Basic structure: Program → Function → Block → Operations + - ✅ Function inputs and block outputs implemented + - ⏸️ Operation-specific parameters (conv, pool, etc.) deferred + +[x] MIL Operation Mappings (50+ operations mapped) + - ✅ Binary: add, sub, mul, real_div, matmul + - ✅ Activations: relu, sigmoid, tanh, softmax + - ✅ Unary math: abs, ceil, floor, exp, log, sqrt, sign, sin, cos, tan, erf, reciprocal + - ✅ Logic: equal, greater, greater_equal, less, less_equal, logical_not, logical_and, logical_or, logical_xor + - ✅ Quantization: dequantize, quantize + - ✅ Convolution: conv, conv_transpose + - ✅ Pooling: avg_pool, max_pool + - ✅ Normalization: batch_norm, instance_norm, layer_norm + - ✅ Reduction: reduce_sum, reduce_mean, reduce_max, reduce_min, reduce_prod, reduce_l1, reduce_l2, etc. + - ✅ Shape: reshape + +[ ] Parameter Handling (Deferred) + - [ ] Conv2d parameters (strides, padding, dilations, groups) + - [ ] Pool2d parameters (window, strides, padding) + - [ ] Normalization parameters (epsilon, scale, bias) + - [ ] Need to implement MIL Value creation for immediate values + - Note: Basic tensor input/output works, complex parameters need MIL Value messages + +## Testing & Quality + +### Python Tests +[ ] Comprehensive operation tests + - Test each operation independently + - Test with different data types + - Test edge cases (empty tensors, scalars) + - Test shape broadcasting + +[ ] Integration tests + - End-to-end graph building and conversion + - Multi-layer network tests + - Complex graph patterns + +[ ] Property-based testing + - Use hypothesis for generative testing + - Random graph generation and validation + +[ ] Performance benchmarks + - Compilation time benchmarks + - Conversion speed benchmarks + - Memory usage profiling + +[ ] Test coverage + - Aim for >80% code coverage + - Add coverage reporting to CI + +### Type Checking & Linting +[ ] Add mypy for static type checking + - Type check all Python bindings + - Add mypy to CI pipeline + +[ ] Add ruff/flake8 for Python linting + - Enforce PEP 8 style + - Add to pre-commit hooks + +[ ] Add black for code formatting + - Auto-format Python code + - Check formatting in CI + +### Rust Code Quality +[ ] Fix Rust 2024 edition warnings + - Add unsafe blocks where needed + - Update to new edition idioms + +[ ] Add more Rust unit tests + - Test converters with various graphs + - Test validation edge cases + +[ ] Reduce compiler warnings + - Fix unused variable warnings + - Address clippy suggestions + +## Documentation + +### API Documentation +[ ] Auto-generate API docs from docstrings + - Add comprehensive docstrings to all Python classes + - Use mkdocstrings to auto-generate reference docs + - Add type hints throughout + +[ ] Add more code examples + - Real-world use cases (MNIST, ResNet, etc.) + - Transfer learning examples + - Model optimization examples + +[ ] Video tutorials + - Getting started video + - Building complex models + - Deployment guide + +[ ] Interactive examples + - Jupyter notebook examples + - Google Colab notebooks + - Try-it-live web interface + +### Performance Documentation +[ ] Benchmarking guide + - How to benchmark models + - Performance comparison ONNX vs CoreML + - Optimization tips + +[ ] Memory usage guide + - Understanding memory consumption + - Reducing memory footprint + - Float16 vs Float32 trade-offs + +### Platform-Specific Guides +[ ] macOS Neural Engine guide + - How to use ANE effectively + - Performance characteristics + - Supported operations + +[ ] Windows DirectML guide (future) + - DirectML integration + - GPU acceleration on Windows + +[ ] Linux GPU guide + - CUDA/ROCm integration + - CPU optimization flags + +## CI/CD & Packaging + +### PyPI Publishing +[ ] Create PyPI package publishing workflow + - Build wheels for multiple platforms + - manylinux wheels for Linux + - macOS universal2 wheels + - Windows wheels + +[ ] Automated version bumping + - Semantic versioning + - Changelog generation + - Git tag automation + +[ ] Release automation + - GitHub Releases on tag push + - Automated release notes + - Asset uploading (wheels, docs) + +### Multi-Platform Support +[ ] Test on multiple Python versions + - Python 3.8, 3.9, 3.10, 3.11, 3.12 + - Matrix testing in CI + +[ ] Test on multiple platforms + - Ubuntu (latest, 20.04, 22.04) + - macOS (Intel, Apple Silicon) + - Windows (latest) + +[ ] Platform-specific features + - Conditional compilation for platform features + - Feature detection at runtime + +### Docker Images +[ ] Create Docker images + - Python + Rust development image + - Runtime-only image + - GPU-enabled image + +[ ] Docker Hub publishing + - Automated image builds + - Multi-architecture images + - Version tagging + +## Features & Enhancements + +### Graph Optimization +[ ] Implement graph optimization passes + - Constant folding + - Dead code elimination + - Operation fusion + - Common subexpression elimination + +[ ] Graph analysis tools + - Visualize graphs (beyond Graphviz) + - Memory usage estimation + - Computational complexity analysis + +### Model Import/Export +[ ] ONNX model import + - Parse existing ONNX models + - Convert ONNX → WebNN graph + - Preserve metadata + +[ ] PyTorch integration + - Export PyTorch models to WebNN + - torch.fx graph conversion + - Maintain gradient information (future) + +[ ] TensorFlow integration + - Export TensorFlow models + - SavedModel → WebNN conversion + +[ ] Hugging Face integration + - Export transformers models + - Easy model hub integration + +### Developer Experience +[ ] Better error messages + - More descriptive validation errors + - Suggestions for fixes + - Error recovery hints + +[ ] Debugging tools + - Graph visualization in Jupyter + - Intermediate value inspection + - Step-by-step execution + +[ ] Profiling tools + - Operation-level timing + - Memory profiling + - Bottleneck identification + +### WebNN Spec Compliance +[ ] Full WebNN API compliance + - Implement all missing operations + - Match behavior exactly + - Pass WebNN conformance tests (if available) + +[ ] Context options + - Power preference enforcement + - Device preference handling + - Capability querying (opSupportLimits) + +[ ] Graph execution modes + - Sync vs async execution + - Streaming execution for large inputs + - Batch processing + +## Ecosystem Integration + +### NumPy Integration +[ ] Better NumPy interop + - Zero-copy where possible + - Support NumPy's __array_interface__ + - Proper dtype conversion + +[ ] NumPy-like API + - Operator overloading (+, -, *, /) + - Slicing support + - Pythonic indexing + +### ML Framework Integration +[ ] JAX integration + - Export JAX computations + - jax.tree_util support + +[ ] scikit-learn integration + - Convert simple sklearn models + - Pipeline integration + +### Visualization +[ ] Netron support + - Ensure exported models work in Netron + - Add metadata for better visualization + +[ ] TensorBoard integration + - Graph visualization + - Profiling data export + +## Infrastructure + +### Build System +[ ] Optimize build times + - Incremental compilation + - Build caching in CI + - Parallel builds + +[ ] Cross-compilation support + - Build for different targets + - Static linking options + +### Security +[ ] Security audit + - Dependency vulnerability scanning + - SAST (Static Application Security Testing) + - Regular security updates + +[ ] Sandboxing + - Restrict file system access + - Memory limits + - Timeout enforcement + +### Monitoring +[ ] Usage analytics (opt-in) + - Track which operations are used + - Performance telemetry + - Error reporting + +[ ] Crash reporting + - Automated crash reports (opt-in) + - Stack trace collection + - Issue auto-creation + +## Community + +### Examples & Templates +[ ] Example repository + - Real-world examples + - Template projects + - Starter kits + +[ ] Model zoo + - Pre-built models + - Optimized for WebNN + - Various domains (CV, NLP, etc.) + +### Documentation +[ ] Contributing guide + - How to contribute + - Development setup + - Code review process + +[ ] Architecture documentation + - High-level design + - Component interactions + - Extension points + +### Community Building +[ ] Discord/Slack channel + - Community discussions + - Support channel + - Show & tell + +[ ] Blog posts & tutorials + - Getting started blog post + - Technical deep dives + - Performance case studies + +## Priority Levels + +HIGH PRIORITY (Next Session): +- [x] Fix CoreML converter to support relu, sigmoid, tanh, softmax +- [x] Implement actual compute() with ONNX runtime integration +- [x] Add comprehensive Python tests +- [x] Fix Rust 2024 edition warnings (PyO3 internal warnings, will be fixed in PyO3 update) +- [x] Add basic shape inference/validation + +MEDIUM PRIORITY: +- [ ] Add more operations (conv2d, pooling, normalization) +- [ ] PyPI packaging and publishing +- [ ] Better error messages +- [ ] Performance benchmarks + +LOW PRIORITY: +- [ ] Full WebNN spec compliance +- [ ] Advanced graph optimizations +- [ ] Multi-framework integration +- [ ] Community infrastructure + +## Notes + +- Most missing functionality is in the Rust backend (converters, executors) +- Python bindings are complete for the architecture - just need more operations +- CoreML converter now supports basic activation functions (relu, sigmoid, tanh, softmax) +- ONNX runtime integration complete with actual tensor execution +- Documentation is comprehensive and ready for community use +- Testing infrastructure expanded with comprehensive compute tests +- CI/CD for packaging and publishing not yet set up + +Last Updated: 2025-12-08 + +## Recent Changes (2025-12-08) + +### Logic Operations with Cast Node Implementation (Latest) +- Implemented all 9 logic operations with full WebNN spec compliance +- Shape inference: Binary operations use broadcasting, unary logicalNot preserves shape +- Python API: Added 9 methods to MLGraphBuilder (src/python/graph_builder.rs) +- ONNX conversion: Automatic Cast node insertion for type conversions (src/converters/onnx.rs:446-580) + - **WORKAROUND**: Currently casts bool → float32 (should be bool → uint8) + - Migrated to ort v2.0.0-rc.10 (from onnxruntime-rs v0.0.14) - supports dynamic types via try_extract_tensor() + - Full uint8 support now technically possible but requires additional changes: + - Update OnnxOutputWithData struct to support multiple data types (not just Vec) + - Update executor to extract correct type based on model output + - Update Python bindings to handle uint8 → NumPy conversion + - Chromium correctly uses bool → uint8, we keep float32 workaround for simplicity + - **PROPER FIX** (future PR): Implement full uint8 output pipeline + - Change: Cast(bool → float32) to Cast(bool → uint8) + - Update output ValueInfo types from Float32 back to Uint8 + - Comparison ops: Execute op (outputs bool) → Cast(bool→float32) [TEMP] + - Logical ops: Cast(inputs→bool) → Execute op → Cast(bool→float32) [TEMP] + - Helper functions: create_cast_node() with AttributeType::Int, create_operation_attributes() +- CoreML conversion: Full support with dedicated layer types (alpha=0.0 for comparison ops) +- Python tests: All 9 tests PASSING with ONNX Runtime (141 passed total) +- All tests pass with Cast node structure (type field set to AttributeType::Int) +- Operations implemented: equal, greater, greaterOrEqual, lesser, lesserOrEqual, logicalNot, logicalAnd, logicalOr, logicalXor + +### Element-wise Operations Implementation +- Implemented all 23 unary element-wise operations with full WebNN spec compliance +- Shape inference: All operations preserve input shape (src/shape_inference.rs) +- Python API: Added 23 methods to MLGraphBuilder (src/python/graph_builder.rs) +- ONNX conversion: Operations map via capitalization (Abs, Ceil, etc.) +- CoreML conversion: Full support with dedicated layer types and workarounds + - UnaryFunctionLayerParams: abs, exp, log, sqrt, reciprocal + - Dedicated layers: ceil, floor, round, sign, trig/hyperbolic operations, erf + - Multiply workaround: neg (alpha=-1), identity (alpha=1) +- Python tests: 23 new tests, all passing with NumPy/SciPy validation (tests/test_python_api.py) +- WPT conformance data: 6 operations with 14 test cases (abs, ceil, floor, exp, log, sqrt) +- Updated CLAUDE.md: CoreML conversion now mandatory for all operations +- All 132 tests passing (109 regular + 23 element-wise) +- Commits: 7ff609d6 (implementation), af2e5a9d (WPT data), dde8208c (CoreML) + +## Recent Changes (2025-12-07) + +### Async Execution Support +- Implemented AsyncMLContext wrapper for async/await syntax +- Added dispatch() method for non-blocking graph execution +- Added read_tensor_async() and write_tensor_async() for async tensor I/O +- WebNN spec-compliant asynchronous execution model +- Uses Python's asyncio.run_in_executor() for thread pool execution +- 5 new async tests covering dispatch, tensor I/O, and concurrent operations +- All 45 tests passing (40 existing + 5 new async) +- Rust code remains synchronous (follows Rust-first principle) +- Zero Rust async dependencies - clean Python-layer solution + +### MLTensor Implementation +- Implemented MLTensor class for explicit tensor management +- Added createTensor(), readTensor(), writeTensor() methods to MLContext +- Thread-safe data storage using Arc>> +- Full NumPy interoperability with automatic type conversion +- Shape validation and data integrity checks in Rust +- 7 new Python tests covering tensor operations +- All 40 Python tests passing (33 existing + 7 new) +- Maintained Rust-first architecture: core logic in Rust, thin Python wrappers + +### Shape Inference and Validation +- Implemented NumPy-style broadcasting for binary operations +- Added proper matmul shape inference with batched matmul support +- Added reshape validation to ensure element count consistency +- Created comprehensive shape_inference module with full test coverage +- Added 11 new Python tests for shape inference functionality +- All shape errors now caught at graph build time with clear error messages + +### ONNX Runtime Integration +- Added CoreML support for relu, sigmoid, tanh, softmax activations +- Implemented run_onnx_with_inputs() for actual tensor execution +- Updated MLContext.compute() to use ONNX runtime with real inputs/outputs +- Added 8 new comprehensive Python tests for compute functionality +- Tests verify actual numerical results for all activation functions diff --git a/docs/converters/TRTX_DEVELOPMENT_GUIDE.md b/docs/archive/trtx-development-guide-2026-01.md similarity index 97% rename from docs/converters/TRTX_DEVELOPMENT_GUIDE.md rename to docs/archive/trtx-development-guide-2026-01.md index e47c0ac8..ccde3f55 100644 --- a/docs/converters/TRTX_DEVELOPMENT_GUIDE.md +++ b/docs/archive/trtx-development-guide-2026-01.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # TensorRT Converter Development Guide **Last Updated:** 2026-01-29 diff --git a/docs/converters/trtx-implementation-status.md b/docs/archive/trtx-implementation-status-2026-01.md similarity index 99% rename from docs/converters/trtx-implementation-status.md rename to docs/archive/trtx-implementation-status-2026-01.md index cf700349..6497c95b 100644 --- a/docs/converters/trtx-implementation-status.md +++ b/docs/archive/trtx-implementation-status-2026-01.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # TensorRT (TrtxConverter) Implementation Status **Last Updated:** 2026-01-30 diff --git a/docs/webnn_static_shapes_limitation.md b/docs/archive/webnn_static_shapes_limitation.md similarity index 97% rename from docs/webnn_static_shapes_limitation.md rename to docs/archive/webnn_static_shapes_limitation.md index f85b9769..41afa416 100644 --- a/docs/webnn_static_shapes_limitation.md +++ b/docs/archive/webnn_static_shapes_limitation.md @@ -1,3 +1,8 @@ +> **Archived (2026-09-16).** Historical document kept for reference only. It describes +> plans, investigations or code states that no longer match the repository (removed Python +> bindings, the old executor API, superseded designs, stale status numbers). Do not use it as +> an API or workflow reference; the current documentation starts at `docs/index.md`. + # WebNN Static Shapes Limitation **Date:** 2026-01-09 diff --git a/docs/development/CLAUDE.md b/docs/development/CLAUDE.md deleted file mode 100644 index 91bc75bf..00000000 --- a/docs/development/CLAUDE.md +++ /dev/null @@ -1,14 +0,0 @@ - -# Recent Activity - - - -### Jan 15, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #5076 | 2:40 PM | 🟣 | Code Coverage Implementation Committed to Repository | ~362 | -| #5073 | 2:37 PM | ✅ | Documentation Step Numbering Corrected | ~228 | -| #5072 | " | ✅ | Setup Guide Updated with Coverage Workflow Step | ~350 | -| #5069 | 2:36 PM | 🟣 | Comprehensive Code Coverage Documentation Created | ~456 | - \ No newline at end of file diff --git a/docs/development/backend-operator-support.md b/docs/development/backend-operator-support.md index 338ec39f..7a5896af 100644 --- a/docs/development/backend-operator-support.md +++ b/docs/development/backend-operator-support.md @@ -1,180 +1,136 @@ # Backend Operator Support Report -This file is generated from converter sources by `scripts/generate_backend_operator_report.py`. -Do not edit this file manually. Run `make docs-backend-ops` after backend changes. - -## ONNX Runtime Backend - -- Converter source: `src/converters/onnx.rs` -- Executor source: `src/executors/onnx.rs` -- Converter operator count: **0** -- Executor operator count: **0** - -### Converter Operators - -- (none) - -### Executor Operators - -Executor-level operator coverage follows converter coverage for this backend. - -- (none) - -## CoreML MLProgram Backend - -- Converter source: `src/converters/coreml_mlprogram.rs` -- Executor source: `src/executors/coreml.rs` -- Converter operator count: **92** -- Executor operator count: **92** - -### Converter Operators - -- `abs`, `hardSigmoid`, `reducemax` -- `add`, `hardSwish`, `reducemean` -- `argMax`, `identity`, `reducemin` -- `argMin`, `instanceNormalization`, `reduceproduct` -- `averagePool2d`, `isInfinite`, `reducesum` -- `batchNormalization`, `isNaN`, `reducesumsquare` -- `cast`, `l2pool2d`, `relu` -- `ceil`, `layerNormalization`, `reshape` -- `clamp`, `leakyRelu`, `reverse` -- `concat`, `lesser`, `roundEven` -- `conv2d`, `lesserOrEqual`, `scatterElements` -- `convTranspose2d`, `log`, `scatterND` -- `cos`, `logicalAnd`, `sigmoid` -- `cumulativeSum`, `logicalNot`, `sign` -- `dequantizeLinear`, `logicalOr`, `sin` -- `div`, `logicalXor`, `slice` -- `elu`, `matmul`, `softmax` -- `equal`, `max`, `softplus` -- `erf`, `maxPool2d`, `softsign` -- `exp`, `min`, `split` -- `expand`, `mul`, `sqrt` -- `floor`, `neg`, `squeeze` -- `gather`, `pad`, `sub` -- `gatherElements`, `pow`, `tan` -- `gatherND`, `prelu`, `tanh` -- `gelu`, `quantizeLinear`, `tile` -- `gemm`, `reciprocal`, `transpose` -- `globalAveragePool`, `reducel1`, `triangular` -- `globalMaxPool`, `reducel2`, `unsqueeze` -- `greater`, `reducelogsum`, `where` -- `greaterOrEqual`, `reducelogsumexp` - -### Executor Operators - -Executor-level operator coverage follows converter coverage for this backend. - -- `abs`, `hardSigmoid`, `reducemax` -- `add`, `hardSwish`, `reducemean` -- `argMax`, `identity`, `reducemin` -- `argMin`, `instanceNormalization`, `reduceproduct` -- `averagePool2d`, `isInfinite`, `reducesum` -- `batchNormalization`, `isNaN`, `reducesumsquare` -- `cast`, `l2pool2d`, `relu` -- `ceil`, `layerNormalization`, `reshape` -- `clamp`, `leakyRelu`, `reverse` -- `concat`, `lesser`, `roundEven` -- `conv2d`, `lesserOrEqual`, `scatterElements` -- `convTranspose2d`, `log`, `scatterND` -- `cos`, `logicalAnd`, `sigmoid` -- `cumulativeSum`, `logicalNot`, `sign` -- `dequantizeLinear`, `logicalOr`, `sin` -- `div`, `logicalXor`, `slice` -- `elu`, `matmul`, `softmax` -- `equal`, `max`, `softplus` -- `erf`, `maxPool2d`, `softsign` -- `exp`, `min`, `split` -- `expand`, `mul`, `sqrt` -- `floor`, `neg`, `squeeze` -- `gather`, `pad`, `sub` -- `gatherElements`, `pow`, `tan` -- `gatherND`, `prelu`, `tanh` -- `gelu`, `quantizeLinear`, `tile` -- `gemm`, `reciprocal`, `transpose` -- `globalAveragePool`, `reducel1`, `triangular` -- `globalMaxPool`, `reducel2`, `unsqueeze` -- `greater`, `reducelogsum`, `where` -- `greaterOrEqual`, `reducelogsumexp` - -## TensorRT Backend - -- Converter source: `src/converters/trtx.rs` -- Executor source: `src/executors/trtx.rs` -- Converter operator count: **99** -- Executor operator count: **99** - -### Converter Operators - -- `abs`, `hardSigmoid`, `reduceLogSum` -- `add`, `hardSwish`, `reduceLogSumExp` -- `argMax`, `identity`, `reduceMax` -- `argMin`, `instanceNormalization`, `reduceMean` -- `averagePool2d`, `isInfinite`, `reduceMin` -- `batchNormalization`, `isNaN`, `reduceProduct` -- `cast`, `l2Pool2d`, `reduceSum` -- `ceil`, `layerNormalization`, `reduceSumSquare` -- `clamp`, `leakyRelu`, `relu` -- `concat`, `lesser`, `resample2d` -- `conv2d`, `lesserOrEqual`, `reshape` -- `convTranspose2d`, `linear`, `reverse` -- `cos`, `log`, `roundEven` -- `cumulativeSum`, `logicalAnd`, `scatterElements` -- `dequantizeLinear`, `logicalNot`, `scatterND` -- `div`, `logicalOr`, `sigmoid` -- `elu`, `logicalXor`, `sign` -- `equal`, `lstm`, `sin` -- `erf`, `lstmCell`, `slice` -- `exp`, `matmul`, `softmax` -- `expand`, `max`, `softplus` -- `floor`, `maxPool2d`, `softsign` -- `gather`, `min`, `split` -- `gatherElements`, `mul`, `sqrt` -- `gatherND`, `neg`, `squeeze` -- `gelu`, `notEqual`, `sub` -- `gemm`, `pad`, `tan` -- `globalAveragePool`, `pow`, `tanh` -- `globalMaxPool`, `prelu`, `tile` -- `greater`, `quantizeLinear`, `transpose` -- `greaterOrEqual`, `reciprocal`, `triangular` -- `gru`, `reduceL1`, `unsqueeze` -- `gruCell`, `reduceL2`, `where` - -### Executor Operators - -Executor-level operator coverage follows converter coverage for this backend. - -- `abs`, `hardSigmoid`, `reduceLogSum` -- `add`, `hardSwish`, `reduceLogSumExp` -- `argMax`, `identity`, `reduceMax` -- `argMin`, `instanceNormalization`, `reduceMean` -- `averagePool2d`, `isInfinite`, `reduceMin` -- `batchNormalization`, `isNaN`, `reduceProduct` -- `cast`, `l2Pool2d`, `reduceSum` -- `ceil`, `layerNormalization`, `reduceSumSquare` -- `clamp`, `leakyRelu`, `relu` -- `concat`, `lesser`, `resample2d` -- `conv2d`, `lesserOrEqual`, `reshape` -- `convTranspose2d`, `linear`, `reverse` -- `cos`, `log`, `roundEven` -- `cumulativeSum`, `logicalAnd`, `scatterElements` -- `dequantizeLinear`, `logicalNot`, `scatterND` -- `div`, `logicalOr`, `sigmoid` -- `elu`, `logicalXor`, `sign` -- `equal`, `lstm`, `sin` -- `erf`, `lstmCell`, `slice` -- `exp`, `matmul`, `softmax` -- `expand`, `max`, `softplus` -- `floor`, `maxPool2d`, `softsign` -- `gather`, `min`, `split` -- `gatherElements`, `mul`, `sqrt` -- `gatherND`, `neg`, `squeeze` -- `gelu`, `notEqual`, `sub` -- `gemm`, `pad`, `tan` -- `globalAveragePool`, `pow`, `tanh` -- `globalMaxPool`, `prelu`, `tile` -- `greater`, `quantizeLinear`, `transpose` -- `greaterOrEqual`, `reciprocal`, `triangular` -- `gru`, `reduceL1`, `unsqueeze` -- `gruCell`, `reduceL2`, `where` +This file is generated from the converter sources by `scripts/generate_backend_operator_report.py`. +Do not edit it manually. Run `make docs-backend-ops` after backend changes; CI fails on drift (`make docs-backend-ops-check`). + +Operation names are the WebNN builder names returned by `Operation::op_type()` in `src/operators.rs`. "Supported" means the converter emits a lowering for the operation. Data type restrictions, dynamic shape limits and known failing cases are tracked per backend in `tests/wpt_conformance/*_expected_failures.txt` and on the [WPT conformance dashboard](https://rustnn.github.io/rustnn/wpt-conformance/). + +## Summary + +| Backend | Converter source | Detection rule | Supported | +|---|---|---|---| +| ONNX Runtime | `src/converters/onnx.rs` | `Operation` variants referenced by the converter | 100 of 100 | +| CoreML | `src/converters/coreml_mlprogram.rs` | `Operation` variants referenced by the converter, plus names in its op-type dispatch | 99 of 100 | +| TensorRT | `src/converters/trtx.rs` | keys of the `match op_type` dispatch table | 99 of 100 | +| LiteRT | `src/converters/litert.rs` | `Operation` variants referenced by the converter | 92 of 100 | +| CANN | `src/converters/cann.rs` | variants accepted by `is_supported_op` | 17 of 100 | + +## Operation matrix + +| Operation | ONNX Runtime | CoreML | TensorRT | LiteRT | CANN | +|---|:-:|:-:|:-:|:-:|:-:| +| `abs` | yes | yes | yes | yes | - | +| `add` | yes | yes | yes | yes | yes | +| `argMax` | yes | yes | yes | yes | - | +| `argMin` | yes | yes | yes | yes | - | +| `averagePool2d` | yes | yes | yes | yes | - | +| `batchNormalization` | yes | yes | yes | yes | - | +| `cast` | yes | yes | yes | yes | yes | +| `ceil` | yes | yes | yes | yes | - | +| `clamp` | yes | yes | yes | yes | - | +| `concat` | yes | yes | yes | yes | yes | +| `conv2d` | yes | yes | yes | yes | yes | +| `convTranspose2d` | yes | yes | yes | yes | - | +| `cos` | yes | yes | yes | yes | - | +| `cumulativeSum` | yes | yes | yes | yes | - | +| `dequantizeLinear` | yes | yes | yes | yes | - | +| `div` | yes | yes | yes | yes | yes | +| `elu` | yes | yes | yes | yes | - | +| `equal` | yes | yes | yes | yes | - | +| `erf` | yes | yes | yes | yes | - | +| `exp` | yes | yes | yes | yes | - | +| `expand` | yes | yes | yes | yes | - | +| `floor` | yes | yes | yes | yes | - | +| `gather` | yes | yes | yes | yes | - | +| `gatherElements` | yes | yes | yes | yes | - | +| `gatherND` | yes | yes | yes | yes | - | +| `gelu` | yes | yes | yes | yes | - | +| `gemm` | yes | yes | yes | yes | - | +| `globalAveragePool` | yes | yes | yes | - | - | +| `globalMaxPool` | yes | yes | yes | - | - | +| `greater` | yes | yes | yes | yes | - | +| `greaterOrEqual` | yes | yes | yes | yes | - | +| `gru` | yes | yes | yes | - | - | +| `gruCell` | yes | yes | yes | - | - | +| `hardSigmoid` | yes | yes | yes | yes | - | +| `hardSwish` | yes | yes | yes | yes | - | +| `identity` | yes | yes | yes | yes | - | +| `instanceNormalization` | yes | yes | yes | yes | - | +| `isInfinite` | yes | yes | yes | yes | - | +| `isNaN` | yes | yes | yes | yes | - | +| `l2Pool2d` | yes | yes | yes | yes | - | +| `layerNormalization` | yes | yes | yes | yes | - | +| `leakyRelu` | yes | yes | yes | yes | - | +| `lesser` | yes | yes | yes | yes | - | +| `lesserOrEqual` | yes | yes | yes | yes | - | +| `linear` | yes | yes | yes | yes | - | +| `log` | yes | yes | yes | yes | - | +| `logicalAnd` | yes | yes | yes | yes | - | +| `logicalNot` | yes | yes | yes | yes | - | +| `logicalOr` | yes | yes | yes | yes | - | +| `logicalXor` | yes | yes | yes | yes | - | +| `lstm` | yes | yes | yes | - | - | +| `lstmCell` | yes | yes | yes | - | - | +| `matmul` | yes | yes | yes | yes | - | +| `max` | yes | yes | yes | yes | - | +| `maxPool2d` | yes | yes | yes | yes | yes | +| `min` | yes | yes | yes | yes | - | +| `mul` | yes | yes | yes | yes | yes | +| `neg` | yes | yes | yes | yes | - | +| `notEqual` | yes | yes | yes | yes | - | +| `pad` | yes | yes | yes | yes | - | +| `pow` | yes | yes | yes | yes | - | +| `prelu` | yes | yes | yes | yes | yes | +| `quantizeLinear` | yes | yes | yes | yes | - | +| `reciprocal` | yes | yes | yes | yes | - | +| `reduceL1` | yes | yes | yes | yes | - | +| `reduceL2` | yes | yes | yes | yes | - | +| `reduceLogSum` | yes | yes | yes | yes | - | +| `reduceLogSumExp` | yes | yes | yes | yes | - | +| `reduceMax` | yes | yes | yes | yes | - | +| `reduceMean` | yes | yes | yes | yes | - | +| `reduceMin` | yes | yes | yes | yes | - | +| `reduceProduct` | yes | yes | yes | yes | - | +| `reduceSum` | yes | yes | yes | yes | yes | +| `reduceSumSquare` | yes | yes | yes | yes | - | +| `relu` | yes | yes | yes | yes | - | +| `resample2d` | yes | yes | yes | yes | yes | +| `reshape` | yes | yes | yes | yes | yes | +| `reverse` | yes | yes | yes | yes | - | +| `roundEven` | yes | yes | yes | yes | - | +| `scatterElements` | yes | yes | yes | yes | - | +| `scatterND` | yes | yes | yes | yes | - | +| `shape` | yes | - | - | - | - | +| `sigmoid` | yes | yes | yes | yes | yes | +| `sign` | yes | yes | yes | yes | - | +| `sin` | yes | yes | yes | yes | - | +| `slice` | yes | yes | yes | yes | yes | +| `softmax` | yes | yes | yes | yes | yes | +| `softplus` | yes | yes | yes | yes | - | +| `softsign` | yes | yes | yes | yes | - | +| `split` | yes | yes | yes | yes | yes | +| `sqrt` | yes | yes | yes | yes | - | +| `squeeze` | yes | yes | yes | yes | - | +| `sub` | yes | yes | yes | yes | yes | +| `tan` | yes | yes | yes | yes | - | +| `tanh` | yes | yes | yes | yes | - | +| `tile` | yes | yes | yes | yes | - | +| `transpose` | yes | yes | yes | yes | yes | +| `triangular` | yes | yes | yes | yes | - | +| `unsqueeze` | yes | yes | yes | - | - | +| `where` | yes | yes | yes | yes | - | + +## Unsupported operations per backend + +- ONNX Runtime: none +- CoreML: `shape` +- TensorRT: `shape` +- LiteRT: `globalAveragePool`, `globalMaxPool`, `gru`, `gruCell`, `lstm`, `lstmCell`, `shape`, `unsqueeze` +- CANN: `abs`, `argMax`, `argMin`, `averagePool2d`, `batchNormalization`, `ceil`, `clamp`, `convTranspose2d`, `cos`, `cumulativeSum`, `dequantizeLinear`, `elu`, `equal`, `erf`, `exp`, `expand`, `floor`, `gather`, `gatherElements`, `gatherND`, `gelu`, `gemm`, `globalAveragePool`, `globalMaxPool`, `greater`, `greaterOrEqual`, `gru`, `gruCell`, `hardSigmoid`, `hardSwish`, `identity`, `instanceNormalization`, `isInfinite`, `isNaN`, `l2Pool2d`, `layerNormalization`, `leakyRelu`, `lesser`, `lesserOrEqual`, `linear`, `log`, `logicalAnd`, `logicalNot`, `logicalOr`, `logicalXor`, `lstm`, `lstmCell`, `matmul`, `max`, `min`, `neg`, `notEqual`, `pad`, `pow`, `quantizeLinear`, `reciprocal`, `reduceL1`, `reduceL2`, `reduceLogSum`, `reduceLogSumExp`, `reduceMax`, `reduceMean`, `reduceMin`, `reduceProduct`, `reduceSumSquare`, `relu`, `reverse`, `roundEven`, `scatterElements`, `scatterND`, `shape`, `sign`, `sin`, `softplus`, `softsign`, `sqrt`, `squeeze`, `tan`, `tanh`, `tile`, `triangular`, `unsqueeze`, `where` + +## Notes + +- `shape`: rustnn extension used by onnx2webnn exports. +- `squeeze`: removed from the WebNN spec (emulation appendix), kept for onnx2webnn. +- `unsqueeze`: removed from the WebNN spec (emulation appendix), kept for onnx2webnn. diff --git a/docs/development/code-coverage.md b/docs/development/code-coverage.md index ece5cb95..421e8663 100644 --- a/docs/development/code-coverage.md +++ b/docs/development/code-coverage.md @@ -8,28 +8,10 @@ rustnn uses [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) for code ## Current Coverage Metrics -As of the latest test run, the project has: - -- **Total Line Coverage:** 47.25% (9,937 / 21,030 lines) -- **Total Function Coverage:** 42.26% (661 / 1,564 functions) -- **Total Region Coverage:** 47.38% (15,767 / 33,278 regions) - -### Coverage by Module - -High coverage modules (>80%): -- `weight_file_builder.rs`: 99.13% lines -- `graph.rs`: 97.14% lines -- `shape_inference.rs`: 86.84% lines -- `graphviz.rs`: 92.70% lines -- `validator.rs`: 72.02% lines - -Modules needing attention (<30%): -- `coreml_mlprogram.rs`: 29.56% lines (converter) -- `onnx.rs`: 24.57% lines (converter) -- `webnn_json.rs`: 26.44% lines -- `coreml.rs`: 0% lines (platform-specific executor) -- `loader.rs`: 0% lines (needs tests) -- `error.rs`: 0% lines (error types, display-only) +Coverage figures are not recorded in this page because they go stale; run `make coverage` for +the current numbers. Coverage is measured for the library unit tests only (`--lib`), so the +converters are also exercised by the WPT conformance suites that the coverage run does not +include. ## Prerequisites diff --git a/docs/development/documentation-policy.md b/docs/development/documentation-policy.md new file mode 100644 index 00000000..f9841ad3 --- /dev/null +++ b/docs/development/documentation-policy.md @@ -0,0 +1,79 @@ +# Documentation Policy + +This page defines what "documented" means for a change to rustnn and what a contributor or a +coding agent must update together with the code. The documentation checklist in the pull +request template points here. + +## Where documentation lives + +| Location | Content | Built by | Published at | +|---|---|---|---| +| `//!` and `///` comments in `src/` | Rust API reference: public items, the crate overview with the feature and environment variable tables in `src/lib.rs` | `make docs-api` (rustdoc, warnings are errors) | https://rustnn.github.io/rustnn/api/rustnn/ | +| `docs/**/*.md`, `mkdocs.yml` | User guide, architecture, development, testing and integration pages | `make docs-build`; strict mode in CI with `make ci-docs` | https://rustnn.github.io/rustnn/ | +| `docs/development/backend-operator-support.md` | Operation-by-backend matrix generated from the converter sources | `make docs-backend-ops`; CI fails on drift | same site | +| WPT conformance dashboard | Per-operation pass and fail status per backend | nightly workflow | https://rustnn.github.io/rustnn/wpt-conformance/ | +| `README.md` | Front page on GitHub and crates.io | - | - | +| `AGENTS.md` (imported by `CLAUDE.md`) | Working guide for coding agents: architecture summary, conventions, workflows | - | - | +| `docs/archive/` | Superseded plans and investigation notes, excluded from the site, read-only | - | - | + +## The rule + +A change is complete when the documentation that describes the changed behaviour is updated in +the same pull request. Documentation follows the code; there is no later cleanup pass. + +| If the change touches | Update | +|---|---| +| A public type, method, trait or module | The rustdoc comment of the item; the module docs (`//!`) when the module's responsibility changes | +| A Cargo feature or an environment variable | The tables in `src/lib.rs` and in `docs/user-guide/backends.md`; for TensorRT also `docs/integration/tensorrt.md` | +| Backend selection, a backend's requirements or its execution model | The module docs of `src/backend_selection.rs` and `docs/user-guide/backends.md` | +| Operator support in a converter (`src/converters/*.rs`) | Run `make docs-backend-ops`, then refresh the WPT snapshots or expected-failure list of that backend (`make wpt-sync-`) | +| A new operation | The "Adding an operation" checklist in `docs/development/setup.md`, including the operation table in `docs/user-guide/api-reference.md` | +| The builder or context API (`src/mlgraphbuilder.rs`, `src/mlcontext.rs`) | `docs/user-guide/api-reference.md`; the code in `docs/user-guide/getting-started.md` and `docs/user-guide/examples.md` when it uses the changed call | +| Makefile targets, scripts or CI workflows | `docs/development/setup.md` and `.github/workflows/README.md` | +| The WPT harness (`tests/run_wpt_conformance.rs`, `tests/wpt_conformance/`) | `docs/testing/wpt-test-guide.md` | +| An example program | `docs/user-guide/examples.md` | +| Removing or renaming an API | Search for the old name in `docs/`, `README.md`, `AGENTS.md` and `src/` and update every hit | + +## Rules for coding agents + +1. The source is the reference. Read the current code before relying on a documentation page. + When a page contradicts the code, fix the page in the same change. +2. Do not reintroduce removed APIs. Old pull requests, archived pages and documents written + before 2026 describe code that no longer exists. The table below maps the names that keep + resurfacing to the current API. +3. Do not edit `docs/archive/`. Pages there are frozen. If some content is still useful, + rewrite it as a current page. +4. Do not write counts, percentages or dates that drift (operation totals, pass rates, + coverage figures). Link to the generated report or the dashboard instead. +5. Plain text: no emojis, ASCII in code comments and error strings, absolute dates. +6. Before handing over a change that touched rustdoc, pages or converters, run + `make docs-api`, `make ci-docs` and `make docs-backend-ops-check`. + +### Removed or replaced names + +| Seen in old documents or pull requests | Current state | +|---|---| +| `src/python/`, `PyMLContext`, `maturin develop --features python`, `tests/test_python_api.py` | Removed in January 2026 (PR #11). Python bindings live in the separate [pywebnn](https://github.com/rustnn/pywebnn) repository; rustnn contains no Python API. | +| `context.compute(graph, inputs)`, `compute_onnx()`, `compute_coreml()`, `compute_fallback()` | Replaced by `MLContext::dispatch` with `MLTensor` bindings. `compute()` is also gone from the WebNN specification. | +| `PyMLContext::select_backend()` | `select_backend` in `src/backend_selection.rs`, driven by `MLContextOptions` | +| `Operation { op_type: String, attributes: serde_json::Value, .. }` | `rustnn::operators::Operation` is an enum with one variant per operation and typed `ML*Options` structs from `src/operator_options.rs` | +| `HashMap` and `HashMap<&str, &MLTensor>` arguments | `MLNamedOperands` and `MLNamedTensors` (`BTreeMap` aliases) | +| `run_onnx_with_inputs`, `run_coreml_zeroed`, `run_trtx_with_inputs` as the execution API | Legacy one-shot executors kept for the CLI and two older examples. New code uses `MLContext`. | +| `ensure_trtx_loaded` | Removed; TensorRT-RTX is loaded on demand. `dynamically_load_tensorrt` exists for custom library paths. | +| `MLGraphBuilder::conv2_with_options`, `MLTensor::destoy` | Renamed to `conv2d_with_options` (the old name is a deprecated alias) and `destroy` | +| Operations `round`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `asinh`, `acosh`, `atanh` | Removed with the specification. JSON graphs with `round` are accepted as `roundEven`. | +| "88 of 105 operations", "84% coverage", "1350 ONNX tests passing" | Stale figures. Current coverage: `docs/development/backend-operator-support.md` and the WPT dashboard | +| `make python-dev`, `make python-test`, `make text-gen-demo`, `make mobilenet-demo`, `make minilm-demo-hub` | Targets no longer exist; `make help` lists the current ones | +| `docs/development/contributing.md`, `docs/api-reference.md`, `docs/development.md`, `docs/webnn-spec-reference.md` | Do not exist; the current pages are the ones listed in `mkdocs.yml` | +| `tarekziade/rustnn` | The repository is `rustnn/rustnn` | +| GGML backend | Never implemented; the plan is archived | + +## Writing guidelines + +- Lead with what the reader has to do. One idea per paragraph. +- Commands and code go in fenced blocks; prefer `make` targets where they exist. +- Link to rustdoc for signatures instead of copying them into pages. +- Name the exact file for anything the reader has to open. +- Archive a page (move it to `docs/archive/` and prepend the banner used there) when it + describes a design that was not adopted or a state that no longer exists. Keep history out + of the navigation, but do not delete it. diff --git a/docs/development/flexible-input-shapes.md b/docs/development/flexible-input-shapes.md index 165ec717..af9f647e 100644 --- a/docs/development/flexible-input-shapes.md +++ b/docs/development/flexible-input-shapes.md @@ -1,48 +1,56 @@ # Flexible Input Shapes -This document describes how dynamic dimensions are encoded and validated at runtime in `rustnn`. +This page describes how dynamic dimensions are encoded, validated and used at run time. +Dynamic dimensions require the `dynamic-inputs` Cargo feature; without it the validator rejects +such graphs with `GraphError::DynamicInputsFeatureDisabled`. -## JSON Encoding +## Encoding -Tensor dimensions use either: - -- Static dimension: a number -- Dynamic dimension: an object with `name` and `maxSize` - -Example: +A dimension is either a number (static) or an object with `name` and `maxSize` (dynamic): ```json { "inputs": { "x": { "dataType": "float32", - "shape": [ - { "name": "batch", "maxSize": 16 }, - 128 - ] + "shape": [ { "name": "batch", "maxSize": 16 }, 128 ] } } } ``` -Rules: +In Rust this is `Dimension::Dynamic(DynamicDimension { name, max_size })` in `src/graph.rs` +(`MLDimension` in the options structs). Dynamic dimensions are allowed on inputs, outputs and +intermediate operands; constants must be static. The `.webnn` text format and onnx2webnn +exports use the same model. Shape inference carries dynamic dimensions through the operations +whose rules are defined for them and falls back to the maximum size elsewhere +(`Dimension::get_static_or_max_size`). + +## Runtime validation + +`MLContext::dispatch` and the checked legacy executors enforce, through `src/runtime_checks.rs`: -- Dynamic dimensions are allowed on input/output operands. -- Constant operands must remain concrete (static shape only). +1. The bound tensor has the descriptor's rank and matches every static dimension. +2. The actual value of a dynamic dimension does not exceed `maxSize`. +3. Dynamic dimensions with the same `name` have the same value across all validated inputs and + outputs. +4. The tensor byte length matches the active shape. -## Runtime Validation +## MLContext API -Phase 5 runtime checks enforce: +Tensors bound to dynamic operands are allocated once with a capacity and resized per dispatch: -1. Actual tensor rank and static dimensions match descriptor. -2. Actual dynamic dimension values do not exceed `maxSize`. -3. Dynamic dimensions with the same `name` must match across validated inputs/outputs. -4. Tensor data length must match the runtime shape element count. +- `MLContext::rustnn_set_tensor_capacity(&mut tensor, &max_shape)` reserves storage for the + largest shape the tensor will take. +- `MLContext::rustnn_resize_tensor(&mut tensor, &shape)` sets the active shape without + reallocating; it must fit the capacity. -## Checked Execution APIs +Backends receive the active shape for each dispatch. `examples/smollm_mlcontext.rs` uses this +for a growing KV cache: the past key and value tensors start at length 0, get the maximum cache +length as capacity, and are resized every token. -Pass operand descriptor maps when runtime validation against the WebNN graph is required: +## Legacy executors -- ONNX: `run_onnx_with_inputs_checked(..., &input_descriptors, &output_descriptors)` passes the WebNN I/O descriptor maps from validation artifacts; ORT still validates feeds independently. Use `run_onnx_with_inputs` when you do not need descriptor checks. -- TensorRT: `run_trtx_with_inputs_checked(...)` -- CoreML: `run_coreml_with_inputs_checked(...)` +`run_onnx_with_inputs_checked` and `run_coreml_with_inputs_checked` take the input and output +descriptor maps from `ValidationArtifacts` and apply the same checks to a one-shot run; +`run_onnx_with_inputs` skips them. diff --git a/docs/development/implementation-status.md b/docs/development/implementation-status.md index 5bffc97b..e48f4bbb 100644 --- a/docs/development/implementation-status.md +++ b/docs/development/implementation-status.md @@ -1,502 +1,85 @@ -# WebNN Implementation Status & Testing Strategy - -**Last Updated:** 2025-12-20 - -## Executive Summary - -rustnn implements 88 of 105 WebNN operations (84% coverage) with full backend support across ONNX Runtime, CoreML MLProgram, and TensorRT. - -**Current Status:** -- ✓ 88 operations fully implemented (Shape Inference + Python API + ONNX + CoreML) -- ✗ 13 operations not yet implemented (cumulativeSum, gatherElements, gatherND, isInfinite, isNaN, l2Pool2d, linear, max, min, notEqual, resample2d, reverse, roundEven) -- ⏭ 4 operations intentionally deferred (gru, gruCell, lstm, lstmCell - RNN operations) -- ✓ WPT test infrastructure in place -- ✓ WPT test data converter working (44 operations with test data) -- ✓ 1350 ONNX tests passing (100% of ONNX-supported functionality) -- ✓ 129 architectural limitations properly marked as skipped -- ✓ 1479 CoreML tests temporarily disabled due to executor bugs -- ✓ Explicit backend selection implemented via device_type parameter - -For source-derived backend converter/executor operator coverage, see -[Backend Operator Support](backend-operator-support.md). - ---- - -## Implementation Status - -**Legend:** -- ✓ = Fully implemented -- ⚠ = Partially implemented -- ✗ = Not implemented -- ⏭ = Intentionally deferred - -### All Operations (Alphabetically Sorted) - -| Operation | Shape | Python | ONNX | CoreML | WPT | -|-----------|:-----:|:------:|:----:|:------:|:---:| -| `abs` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `acos` | ✓ | ✓ | ✓ | ✓ | - | -| `acosh` | ✓ | ✓ | ✓ | ✓ | - | -| `add` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `argMax` | ✓ | ✓ | ✓ | ✓ | - | -| `argMin` | ✓ | ✓ | ✓ | ✓ | - | -| `asin` | ✓ | ✓ | ✓ | ✓ | - | -| `asinh` | ✓ | ✓ | ✓ | ✓ | - | -| `atan` | ✓ | ✓ | ✓ | ✓ | - | -| `atanh` | ✓ | ✓ | ✓ | ✓ | - | -| `average_pool2d` | ✓ | ✓ | ✓ | ✓ | - | -| `batch_normalization` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `cast` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `ceil` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `clamp` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `concat` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `constant` | ✓ | ✓ | ✓ | ✓ | - | -| `conv2d` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `conv_transpose2d` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `cos` | ✓ | ✓ | ✓ | ✓ | - | -| `cosh` | ✓ | ✓ | ✓ | ✓ | - | -| `cumulativeSum` | ✗ | ✗ | ✗ | ✗ | - | -| `dequantize_linear` | ✓ | ✓ | ✓ | ✓ | - | -| `div` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `elu` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `equal` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `erf` | ✓ | ✓ | ✓ | ✓ | - | -| `exp` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `expand` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `floor` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `gather` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `gatherElements` | ✗ | ✗ | ✗ | ✗ | - | -| `gatherND` | ✗ | ✗ | ✗ | ✗ | - | -| `gelu` | ✓ | ✓ | ✓ | ✓ | - | -| `gemm` | ✓ | ✓ | ✓ | ✓ | - | -| `global_average_pool` | ✓ | ✓ | ✓ | ✓ | - | -| `global_max_pool` | ✓ | ✓ | ✓ | ✓ | - | -| `greater` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `greater_or_equal` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `gru` | ⏭ | ⏭ | ⏭ | ⏭ | - | -| `gruCell` | ⏭ | ⏭ | ⏭ | ⏭ | - | -| `hardSigmoid` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `hardSwish` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `identity` | ✓ | ✓ | ✓ | ✓ | - | -| `input` | ✓ | ✓ | ✓ | ✓ | - | -| `instance_normalization` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `isInfinite` | ✗ | ✗ | ✗ | ✗ | - | -| `isNaN` | ✗ | ✗ | ✗ | ✗ | - | -| `layer_normalization` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `leakyRelu` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `lesser` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `lesser_or_equal` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `l2Pool2d` | ✗ | ✗ | ✗ | ✗ | - | -| `linear` | ✗ | ✗ | ✗ | ✗ | - | -| `log` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `logical_and` | ✓ | ✓ | ✓ | ✓ | - | -| `logical_not` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `logical_or` | ✓ | ✓ | ✓ | ✓ | - | -| `logical_xor` | ✓ | ✓ | ✓ | ✓ | - | -| `lstm` | ⏭ | ⏭ | ⏭ | ⏭ | - | -| `lstmCell` | ⏭ | ⏭ | ⏭ | ⏭ | - | -| `matmul` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `max` | ✗ | ✗ | ✗ | ✗ | - | -| `max_pool2d` | ✓ | ✓ | ✓ | ✓ | - | -| `min` | ✗ | ✗ | ✗ | ✗ | - | -| `mul` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `neg` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `notEqual` | ✗ | ✗ | ✗ | ✗ | - | -| `pad` | ✓ | ✓ | ✓ | ✓ | - | -| `pow` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `prelu` | ✓ | ✓ | ✓ | ✓ | - | -| `quantize_linear` | ✓ | ✓ | ✓ | ✓ | - | -| `reciprocal` | ✓ | ✓ | ✓ | ✓ | - | -| `reduce_l1` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_l2` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_log_sum` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_log_sum_exp` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_max` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_mean` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_min` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_product` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_sum` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `reduce_sum_square` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `relu` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `resample2d` | ✗ | ✗ | ✗ | ✗ | - | -| `reshape` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `reverse` | ✗ | ✗ | ✗ | ✗ | - | -| `round` | ✓ | ✓ | ✓ | ✓ | - | -| `roundEven` | ✗ | ✗ | ✗ | ✗ | - | -| `scatterElements` | ✓ | ✓ | ✓ | ✓ | - | -| `scatterND` | ✓ | ✓ | ✓ | ✓ | - | -| `sigmoid` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `sign` | ✓ | ✓ | ✓ | ✓ | - | -| `sin` | ✓ | ✓ | ✓ | ✓ | - | -| `sinh` | ✓ | ✓ | ✓ | ✓ | - | -| `slice` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `softmax` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `softplus` | ✓ | ✓ | ✓ | ✓ | - | -| `softsign` | ✓ | ✓ | ✓ | ✓ | - | -| `split` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `sqrt` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `squeeze` | ✓ | ✓ | ✓ | ✓ | - | -| `sub` | ✓ | ✓ | ✓ | ✓ | ✓ | -| `tan` | ✓ | ✓ | ✓ | ✓ | - | -| `tanh` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `tile` | ✓ | ✓ | ✓ | ✓ | - | -| `transpose` | ✓ | ✓ | ✓ | ✓ | ⚠ | -| `triangular` | ✓ | ✓ | ✓ | ✓ | - | -| `unsqueeze` | ✓ | ✓ | ✓ | ✓ | - | -| `where` | ✓ | ✓ | ✓ | ✓ | - | - -**WPT Test Status:** -- ✓ = All tests passing (100% pass rate) -- ⚠ = Tests exist but some failing or incomplete -- `-` = No WPT test data available - -### Deferred Operations - -**Rationale:** Each RNN operation requires 10-15 parameters with complex shape inference (~2000-3000 LOC total). Active [W3C discussion](https://github.com/webmachinelearning/webnn/issues/453) about removing these in favor of lower-level primitives. Modern ML trends favor Transformer architectures over LSTM/GRU. - ---- - -## Summary Statistics - -``` -WebNN Specification Coverage: - Total Operations in Spec: 105 - Fully Implemented: 88 (84%) - Not Yet Implemented: 13 (12%) - Deferred (RNN): 4 (4%) (lstm, lstmCell, gru, gruCell) - -Not Yet Implemented Operations (13): - - cumulativeSum - Element-wise cumulative sum along axis - - gatherElements - Gather elements using index tensor - - gatherND - Gather N-dimensional slices - - isInfinite - Check for infinite values - - isNaN - Check for NaN values - - l2Pool2d - L2 pooling (L2 norm within window) - - linear - Linear transformation (alpha*x + beta) - - max - Element-wise maximum of two tensors - - min - Element-wise minimum of two tensors - - notEqual - Element-wise inequality comparison - - resample2d - Resize/resample 2D tensor - - reverse - Reverse elements along axes - - roundEven - Round to nearest even integer - -Implementation Status: - Shape Inference: 88/88 ✓ (100%) - Python API: 88/88 ✓ (100%) - ONNX Backend: 88/88 ✓ (100%) - CoreML MLProgram: 88/88 ✓ (100%) - -Test Coverage: - WPT Test Infrastructure: ✓ Complete (converter + runner + explicit backend selection) - WPT Conformance Files: 44 operations with test data - WPT Tests Collected: 2958 total tests (1479 per backend × 2 backends) - ONNX Tests Passing: 1350 tests (100% of ONNX-supported functionality) ✓ - ONNX Tests Skipped: 129 tests (architectural limitations) - CoreML Tests: 1479 tests (currently disabled due to executor bugs) - Overall Status: 100% pass rate for active backends ✓ - -Recent Test Fixes (2025-12-13): - - conv_transpose2d: 28/28 tests fixed (+32 overall) ✓ - Added missing bias parameter and fixed default filter_layout (oihw→iohw) - - batch_normalization: 84/96 tests fixed ✓ - Fixed input ordering (mean/variance positions) and axis-based shape calculation - - layer_normalization: +8 tests ✓ - Fixed epsilon/axis attributes and scale/bias shape calculation (X.shape[axis:]) - - reduce_l1: +2 tests ✓ - Added automatic float32 casting for uint32/uint8 types - - hardSwish: 28/28 passing (100%) ✓ - Added ONNX decomposition (Add + Clip + Div + Mul) - - logical_not: 14/14 passing (100%) ✓ - Fixed parameter name mapping ('a' → 'input') - - float16 normalization: +24 tests ✓ - Fixed default initializer data type handling - - reshape: 132/132 passing (100%) ✓ - Fixed parameter name mapping - - gather: 76/80 passing (95%) ✓ - Added uint32 index casting - - relu: All integer type tests passing ✓ - Added automatic float casting - - conv2d: 80/80 passing (100%) ✓ - Fixed layout transformations - - split: 40/40 passing (100%) ✓ - Fixed array splits - -Architectural Limitations (129 tests now skipped): - - batch_normalization: 12 tests (1D tensors and NHWC - semantic mismatches with ONNX) - - layer_normalization: 12 tests (non-consecutive axes require multi-operation emulation) - - instance_normalization: 8 tests (NHWC layout not supported - requires NCHW) - - Remaining: 97 tests (various unsupported type combinations and edge cases) - Note: All skipped tests marked with pytest.skip() - documented in Chromium comparison below -``` - -### Chromium Reference Implementation Comparison - -Analysis of remaining 32 failures against Chromium's WebNN implementation (the W3C reference): - -**instance_normalization NHWC (8 failures):** -- Status: Not supported in Chromium -- Chromium code: "ONNX InstanceNormalization expects NCHW layout, channel is at index 1" -- Chromium does NOT add transpose nodes for NHWC -- Conclusion: These tests validate error handling, not expected functionality - -**layer_normalization non-consecutive axes (12 failures):** -- Status: Requires complex emulation in Chromium -- Chromium code: "ONNX LayerNormalization only accepts the first normalization dimension" -- Chromium explicitly rejects non-consecutive axes like `[0,2]` -- Fallback: Manual emulation with 6+ primitive operations (ReduceMean, Sub, Pow, Sqrt, Div, Mul) -- Conclusion: Major architectural change required for both implementations - -**batch_normalization 1D/edge cases (12 failures):** -- Status: Partially supported in Chromium with limitations -- Chromium supports 1D operation (defaults channels=1) -- However, tests provide mean/variance with shapes incompatible with ONNX expectations -- Shape mismatch between WebNN test semantics and ONNX BatchNormalization requirements -- Conclusion: Edge case tests with semantic differences between WebNN and ONNX - -**Summary:** -- 8 tests: Unsupported in reference implementation (NHWC layout) -- 12 tests: Require complex multi-operation emulation (non-consecutive axes) -- 12 tests: Edge cases with spec/backend semantic mismatches (1D/NHWC batchnorm) -- **91.3% conformance matches or exceeds reference implementation capabilities** -- All 32 tests now properly skipped with architectural limitation markers - -**Backend Selection & Testing:** - -As of 2025-12-14, explicit backend selection has been implemented via the `device_type` parameter: -- `device_type="auto"` (default): Automatic backend selection based on availability -- `device_type="cpu"`: Force ONNX CPU backend -- `device_type="gpu"`: Force ONNX GPU backend -- `device_type="npu"`: Force CoreML backend (macOS only) - -**Current Test Configuration:** -- ONNX tests: Use `device_type="gpu"` to explicitly test ONNX GPU backend -- CoreML tests: Temporarily disabled due to executor bugs (see below) -- Test fixture parametrizes each test to run on both backends independently - -**Why CoreML Testing is Disabled:** -CoreML backend has critical executor bugs that cause process crashes: -1. Panics on multi-output operations (coreml_mlprogram.rs:632) -2. Data type mismatches causing crashes -3. Missing proper error handling (uses `.expect()` which panics) - -To re-enable CoreML testing: -1. Fix panic at coreml_mlprogram.rs:632 - handle multi-output ops -2. Fix data type conversion issues -3. Add proper error handling instead of panicking -4. Uncomment detection code in tests/conftest.py - -**Note:** CoreML graph conversion works correctly - only the executor has bugs - ---- - -## WPT Integration Status - -### What Exists - -✓ **Rust harness (in-repo):** -- `tests/run_wpt_conformance.rs` — libtest_mimic runner (~2482 conformance cases) -- `tests/wpt_conformance/` — corpus load, `MLGraphBuilder` replay, tolerance checking -- `scripts/fetch_wpt.mjs` — download WPT checkout into `.cache/wpt` -- `scripts/wpt_bridge/dump_corpus.mjs` — evaluate upstream `.https.any.js` → JSON corpus - -✓ **Backends:** ONNX CPU (default trials), `WPT_BACKEND=trtx` (when TensorRT is available) - -⚠ **Gaps:** CI wiring, TRTX smoke validation, `MLContext` reuse for performance - -**Baseline (2026-06-19):** 2482 trials, 2482 passed, 0 failed, 15.19 s (`onnx` CPU, `--test-threads 1`). - -### Running tests - -```bash -node scripts/fetch_wpt.mjs # once per machine / when updating WPT -make test-wpt # full ONNX CPU suite -make test-wpt-op OP=relu # filter by operation -make test-wpt-trtx # TensorRT path (mock or real) -``` - -Python WPT conformance lives in [pywebnn](https://github.com/rustnn/pywebnn). CI runs the in-repo Rust harness (`make test-wpt`). - ---- - -## Next Steps (Prioritized) - -### Priority 1: WPT harness merge readiness (IN PROGRESS) - -**Goal:** Stable in-repo Rust WPT suite in CI with recorded pass/fail baseline. - -**Remaining tasks:** CI Node.js + fetch, full 2482-case run, TRTX smoke, performance. - -**Estimated Effort:** 4-8 hours - ---- - -### Priority 2: Enable Python API Tests (MEDIUM IMPACT) - -**Goal:** Diagnose why 260 Python API tests are skipped and enable execution - -**Current Issue:** All Python API tests skipped, likely due to missing ONNX Runtime or other dependencies. - -**Action Items:** -1. **Investigate skip conditions** - ```bash - pytest tests/test_python_api.py -v --collect-only - ``` - - Identify why tests are marked as skipped - - Check for missing pytest markers (e.g., `pytest.mark.asyncio` warning) - -2. **Fix runtime dependencies** - - PyPI package (v0.4.0+): ONNX Runtime bundled automatically, no separate installation needed - - Building from source: Use `make python-dev` to install with ONNX Runtime support - - Verify `webnn` Python module built: `maturin develop --features python,onnx-runtime` - - Check for feature flags or environment variables required - -3. **Run tests and document results** - ```bash - pytest tests/test_python_api.py -v - cargo test --lib - ``` - -**Expected Outcome:** -- Python API tests passing (or failing with actionable errors) -- Clear documentation of which tests require specific backends -- Skipped tests only for unavailable backends (TensorRT on macOS, CoreML on Linux) - -**Estimated Effort:** 4-6 hours - ---- - -### Priority 3: Document Remaining Operations (LOW IMPACT) - -**Goal:** Complete WebNN specification coverage analysis - -**Action Items:** -1. **Identify remaining ~6 operations** from WebNN spec not yet implemented -2. **Assess priority** based on: - - Usage in popular models (BERT, ResNet, etc.) - - Complexity of implementation - - Backend support availability -3. **Update TODO.txt** with findings - -**Expected Outcome:** -- Clear roadmap for reaching 95/95 (100%) operation coverage -- Priority ranking for next implementation phase - -**Estimated Effort:** 2-3 hours - ---- - -### Priority 4: CI/CD Integration (MEDIUM IMPACT) - -**Goal:** Automate WPT tests in continuous integration pipeline - -**Prerequisites:** WPT harness stable (Priority 1) - -**Action Items:** -1. **Add WPT tests to CI workflow** (`.github/workflows/`) - - Node.js on PATH, `node scripts/fetch_wpt.mjs` - - `cargo test --test run_wpt_conformance --features onnx-runtime -- --test-threads 1` - - Fail build on test failures -2. **Create test matrix** - - Test on multiple platforms (Linux, macOS, Windows) - - Test with different backends (ONNX CPU, ONNX GPU, CoreML) -3. **Add status badges** to README.md - -**Expected Outcome:** -- Automated validation of every code change -- Visible test status for contributors -- Regression prevention - -**Estimated Effort:** 4-6 hours (after Priority 1 complete) - ---- - -## Testing Strategy Details - -### WPT harness - -Live upstream WPT conformance tests are evaluated via the Node bridge and executed through `MLGraphBuilder` + `MLContext`. See **Running tests** above and `tests/run_wpt_conformance.rs`. - -### Tolerance Checking - -`tests/wpt_conformance/tolerance.rs` implements WPT-compatible ULP and ATOL validation. Per-test overrides come from each WPT case; operation defaults are in `tolerance.rs`. - -### Running Tests - -```bash -# WPT conformance (Rust harness) -make test-wpt -make test-wpt-op OP=reduce_sum - -# Python API tests (pywebnn / when runtime available) -pytest tests/test_python_api.py -v - -# Rust library tests -cargo test --lib -make test -``` - ---- - -## References - -- **W3C WebNN Specification:** https://www.w3.org/TR/webnn/ -- **WPT WebNN Tests:** https://github.com/web-platform-tests/wpt/tree/master/webnn -- **Local WebNN Spec Reference:** `docs/webnn-spec-reference.md` -- **API Reference:** `docs/api-reference.md` -- **Development Guide:** `docs/development.md` - ---- - -## Revision History - -- **2025-12-14 (Skip Pattern Implementation):** - - Achieved 100% pass rate for supported functionality (2700 passing, 0 failing, 258 skipped) - - Fixed pytest skip patterns to properly match WPT test names: - - Test names use spaces not underscores (e.g., "1D tensor" not "1d_tensor") - - Added skip patterns for 32 architectural limitation tests matching Chromium reference implementation - - Validated against Chromium WebNN implementation: - - instance_normalization NHWC (8 tests): Not supported - requires NCHW layout - - layer_normalization non-consecutive axes (12 tests): Requires 6+ operation emulation - - batch_normalization 1D/NHWC (12 tests): Semantic mismatches with ONNX - - Added note: CoreML tests show ONNX errors because CoreML currently uses ONNX Runtime as intermediate format - - Total skipped: 258 tests (32 architectural limitations + 226 unsupported data types) - - Documentation: Updated executive summary and Chromium comparison section - - Commits: 1 (skip patterns + docs update) -- **2025-12-13 (Final Session):** - - Achieved 91.3% WPT conformance (2700 passing, 32 failing, 226 skipped) - - Major fix: - - **conv_transpose2d**: Added missing bias parameter to Python API and fixed default filter_layout from 'oihw' to 'iohw' (28/28 tests fixed, +32 tests overall due to side effects) - - Total session improvement: +32 tests (+1.1%) - - Commits: 1 (conv_transpose2d bias+filter_layout fix) - - Remaining 32 failures are architectural limitations and edge cases that require significant refactoring -- **2025-12-13 (Continued Session):** - - Achieved 90.2% WPT conformance (2668 passing, 64 failing, 226 skipped) - - Major fixes: - - **batch_normalization**: Fixed input ordering (Python API [input, mean, variance, scale, bias] → ONNX [input, scale, bias, mean, variance]) and axis-based channel dimension calculation (84/96 tests fixed) - - **layer_normalization**: Fixed ONNX attributes (epsilon, axis) and scale/bias shape calculation to match X.shape[axis:] specification (+8 tests) - - **reduce_l1**: Added automatic type casting (uint32→float32→operation→uint32) for ONNX Runtime compatibility (+2 tests) - - Documented architectural limitations: - - instance_normalization NHWC layout requires transpose nodes (8 failures deferred) - - layer_normalization non-consecutive axes requires operation emulation (12 failures deferred) - - Total session improvement: +42 tests (+1.5%) - - Commits: 4 (reduce_l1 casting, instance_norm TODO, layer_norm fixes, batch_norm fixes) -- **2025-12-13 (Late Evening - Session 2):** - - Achieved 88.7% WPT conformance (2626 passing, 106 failing, 226 skipped) - - Major fixes: - - **hardSwish**: Implemented ONNX opset 13 decomposition (28/28 passing) - `x * clip(x + 3, 0, 6) / 6` - - **logical_not**: Fixed parameter name mapping in test harness (14/14 passing) - - **layer_normalization**: Fixed 0D tensor and empty axes edge cases following Chromium implementation (6 tests fixed) - - **float16 normalization**: Fixed default initializer data type handling (24 tests fixed) - - Total session improvement: +72 tests (+2.8%) - - Marked hardSwish and logical_not as ✓ in implementation table - - Remaining work: batch_normalization (96 failures), conv_transpose2d (64 failures), custom axes support -- **2025-12-13 (Evening):** - - Major WPT test fixes completed: - - **expand**: Fixed ONNX converter to add shape as second input (88/88 passing) - - **clamp**: Fixed type matching for min/max initializers across all data types (96/102 passing) - - **concat**: Previously fixed (90/90 passing) - - Test harness improvements: - - Fixed parameter name mapping (camelCase → snake_case) - - Added None value filtering (None = use default) - - Added multi-output operation support - - Updated test statistics: 1128+ tests passing, 2958 total tests collected - - Marked clamp, concat, and expand as ✓ in implementation table -- **2025-12-13 (Morning):** - - Reorganized into single alphabetically sorted table with simple check icons (✓) - - Fixed WPT test data converter with Node.js-based extraction - - Successfully converted 44 operations with test data - - Updated status: converter working, test data populated -- **2025-12-08:** 85 operations fully implemented; CoreML end-to-end execution verified -- **2025-12-07:** WPT test infrastructure created; test data files initialized - ---- - -**Document Status:** Living Document - Update after major implementation milestones +# Implementation Status + +This page records which parts of the WebNN API rustnn implements and where the gaps are. +Counts and pass rates are not repeated here because they are generated: + +- Operation-by-backend matrix: [Backend Operator Support](backend-operator-support.md), from `make docs-backend-ops` +- Conformance per operation and backend: [WPT conformance dashboard](https://rustnn.github.io/rustnn/wpt-conformance/), nightly +- Test baselines: PASS snapshots in `tests/snapshots/` and `tests/wpt_conformance/*_expected_failures.txt` + +## WebNN API surface + +| Interface | Implemented | Not implemented or different | +|---|---|---| +| `ML`, context creation | `MLContext::create(&MLContextOptions)` with the `powerPreference` and `accelerated` hints | `createContext(GPUDevice)`: `create_from_gpu_device` is a stub | +| `MLContext` | `createTensor`, `writeTensor`, `readTensor`, `dispatch`, `accelerated`, all synchronous | `lost`, `destroy`, `createConstantTensor` and `opSupportLimits` are stubs (`todo!()`); resources are released by `Drop` | +| `MLGraphBuilder` | constructor, `input`, `constant` from buffers, every operation of the specification (see below), `build` (synchronous) | `constant` from an `MLTensor` and the scalar `constant(type, value)` form | +| `MLOperand` | `shape` and `dataType` through a `&GraphInfo` (`MLOperand::shape(graph)`) or the builder (`rustnn_operand_shape`) | The spec exposes them directly on the operand | +| `MLGraph` | Opaque compiled graph with `input_descriptors` and `output_descriptors` | - | +| `MLTensor` | `shape`, `dataType`, `readable`, `writable`, `constant` | `destroy` and `isDestroyed` are stubs | +| Data types | All spec types plus `int4` and `uint4` | - | +| Asynchrony | None; the JavaScript promises map to blocking calls | An async API is under consideration; the affected methods are marked `//async` in the source | + +rustnn extensions, all prefixed `rustnn_` or documented as such: `rustnn_backend`, +`rustnn_device`, `rustnn_device_type`, `rustnn_set_tensor_capacity`, `rustnn_resize_tensor`, +`rustnn_save_webnn`, `rustnn_webnn_text_for_outputs`, `rustnn_operand_shape`, +`rustnn_operand_data_type`, `MLGraphBuilder::new_uncompiled`, `build_graph_info`, +`finish_graph_info`, and the backend and device hints on `MLContextOptions`. + +## Operations + +The builder implements every operation method of the W3C WebNN specification as of the copy in +`docs/reference/webnn-index.bs` (its date is in `docs/reference/README.md`), including the +recurrent operations `gru`, `gruCell`, `lstm` and `lstmCell` and the 4-bit quantization paths. +Five additional operations are kept: `globalAveragePool` and `globalMaxPool` from earlier +drafts, `squeeze` and `unsqueeze` (moved to the specification's emulation appendix and still +emitted by onnx2webnn) and `shape`, a rustnn extension for onnx2webnn exports. + +Backend gaps, from the generated report: + +- ONNX Runtime lowers every operation. +- CoreML and TensorRT-RTX lower everything except the `shape` extension. +- LiteRT has no recurrent operations (`gru`, `gruCell`, `lstm`, `lstmCell`) and no + `globalAveragePool`, `globalMaxPool`, `unsqueeze` or `shape`; several data type combinations + are rejected up front (`dtype_unsupported_for_op` in `src/backends/litert.rs`). +- CANN supports a small subset (arithmetic, `conv2d`, `maxPool2d`, `reduceSum`, `softmax`, + `sigmoid`, `prelu`, `cast`, `concat`, `split`, `slice`, `reshape`, `transpose`, `resample2d`); + `is_supported_op` in `src/converters/cann.rs` is the authoritative list. + +"Supported" means the converter emits a lowering. Numerical conformance per case is what the +WPT runs report; the remaining CoreML failures, for example, come from negative scatter indices +and integer precision limits of the hardware rather than from missing lowerings. + +## Backends + +| Backend | State | +|---|---| +| ONNX Runtime | Reference backend; runs the full WPT suite in CI with PASS snapshots | +| TensorRT-RTX | Native lowering with refittable weights, engine and runtime caches, CUDA graphs. WPT snapshots are maintained by contributors with GPUs; CI only compiles the backend | +| CoreML | MLProgram lowering; runs the full WPT suite in CI on macOS with an expected-failure list | +| LiteRT | TFLite lowering; runs in CI (non-blocking) with snapshots and an expected-failure list | +| CANN | Runs on OpenHarmony devices; mock mode in CI | +| Browser WebNN (wasm32) | Generated bindings and a graph-compilation test in Chrome; no `MLContext` backend yet | + +## Known gaps + +- Operand fields of option structs (`MLConv2dOptions::bias`, `MLGemmOptions::c`, quantization + zero points) are operand indices rather than `MLOperand` values; `MLOperand::rustnn_index()` + provides them. +- Dynamic dimensions are only available for graphs loaded from files and require the + `dynamic-inputs` feature; builder descriptors are static. +- `Int4` and `Uint4` constants cannot be exported to `.safetensors`. +- The CLI needs a runtime feature at build time. +- The legacy executors deserialize the model on every call and have no device tensors. + +## Conformance tracking + +A WPT case is either passing, recorded as a PASS snapshot (`onnx`, `trtx`, `litert`), or listed +in the backend's expected-failure file (`coreml`, `litert`). The corpus is pinned by +`WPT_REVISION`; a weekly workflow refreshes snapshots and expectations and opens a pull +request. See the [WPT Conformance Guide](../testing/wpt-test-guide.md). + +## Versioning + +rustnn is published as a `0.5.x` development release. The API changes without notice; +`CHANGELOG.md` records notable changes per release. diff --git a/docs/development/setup.md b/docs/development/setup.md index 884aa961..3dac8b4d 100644 --- a/docs/development/setup.md +++ b/docs/development/setup.md @@ -1,371 +1,150 @@ -# Development Guide - -## Prerequisites - -- **Rust**: 1.70+ (install from [rustup.rs](https://rustup.rs/)) -- **Python**: 3.11+ with pip -- **Maturin**: `pip install maturin` -- **Optional**: Graphviz for visualization (`brew install graphviz` on macOS) - -## Building from Source - -```bash -# Clone repository -git clone https://github.com/tarekziade/rustnn.git -cd rustnn - -# See all available commands -make help - -# Build Rust library -make build - -# Build Python package (downloads ONNX Runtime automatically) -make python-dev - -# Run tests -make test # Rust tests -make python-test # Python tests (includes WPT conformance) - -# Build documentation -make docs-serve # Live preview at http://127.0.0.1:8000 -make docs-build # Build static site -``` - -## Running Examples - -### Python Examples - -```bash -# Install package first -make python-dev - -# Run examples -make python-example # Run all examples -make mobilenet-demo # MobileNetV2 on all 3 backends -make text-gen-demo # Text generation with attention -make text-gen-train # Train model on sample data -make text-gen-trained # Generate with trained weights - -# Or run individual examples -python examples/python_simple.py -python examples/python_matmul.py -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend cpu -``` - -### Rust Examples - -```bash -# Validate a graph -make run - -# Generate visualization -make viz - -# Convert to ONNX -make onnx - -# Convert to CoreML -make coreml -``` - -## Testing - -### Python Tests - -```bash -# All tests (includes WPT conformance tests) -make python-test - -# WPT conformance tests only -make python-test-wpt - -# Or use pytest directly -python -m pytest tests/ -v - -# Specific test -python -m pytest tests/test_python_api.py::test_context_creation -v - -# With coverage -python -m pytest tests/ --cov=webnn --cov-report=html -``` - -### Rust Tests - -```bash -# All Rust tests -make test - -# Or use cargo directly -cargo test - -# Specific module -cargo test validator - -# With output -cargo test -- --nocapture -``` - -## Feature Flags - -The project uses Cargo feature flags to control optional functionality. The Makefile handles these automatically: - -```bash -# Python bindings with ONNX Runtime (recommended) -make python-dev # Includes python,onnx-runtime features - -# Build Python wheel -make python-build # Production build with all features - -# Or use cargo/maturin directly if needed -cargo build --features python,onnx-runtime -maturin develop --features python,onnx-runtime,coreml-runtime -``` - -## Development Workflow - -### 1. Make Changes - -Edit Rust code in `src/` or Python code in `python/webnn/`. - -### 2. Format Code - -```bash -# Rust (automatically formats) -make fmt - -# Python -black python/ tests/ -``` - -### 3. Run Tests - -```bash -# Full test suite -make test # Rust tests -make python-test # Python tests - -# Or run comprehensive validation -make validate-all-env # Build, test, convert, validate -``` - -### 4. Check Code Coverage - -```bash -# Generate coverage report -make coverage # Text output -make coverage-html # HTML report -make coverage-open # HTML report + open in browser - -# For CI/CD -make coverage-lcov # LCOV format for upload to coverage services -``` - -See [Code Coverage Guide](code-coverage.md) for detailed coverage analysis and best practices. - -### 5. Build and Test Python Package - -```bash -make python-dev # Install in development mode -make python-test # Run all tests -``` - -### 6. Update Documentation - -Edit files in `docs/` and preview: - -```bash -make docs-serve # Live preview at http://127.0.0.1:8000 -make docs-build # Build static site -make ci-docs # Build in strict mode (CI) -``` - -## Debugging - -### Rust - -```bash -# Debug build -make build - -# Run with visualization -make viz - -# Run with backtrace -RUST_BACKTRACE=1 make run -``` - -### Python - -```bash -# Run specific example with verbose output -python examples/python_simple.py - -# Or enable debug logging in code -import webnn -import logging - -logging.basicConfig(level=logging.DEBUG) - -# Your code here -``` - -## Common Tasks - -### Add a New Operation - -1. Update `graph.rs` with new operation type -2. Add validation logic in `validator.rs` -3. Implement conversion in `converters/onnx.rs` and `converters/coreml.rs` -4. Add Python binding in `src/python/graph_builder.rs` -5. Add tests in `tests/test_python_api.py` - -### Add a New Backend - -1. Create new file in `src/executors/your_backend.rs` -2. Add feature flag in `Cargo.toml` -3. Implement executor trait/functions -4. Add conditional compilation in `src/executors/mod.rs` -5. Wire up in `src/python/context.rs` backend selection -6. Add tests - -### Update Documentation - -1. Edit markdown files in `docs/` -2. Preview with `make docs-serve` -3. Check links and formatting -4. Build with `make docs-build` -5. Test in strict mode with `make ci-docs` - -## Troubleshooting - -### Maturin Build Fails - -```bash -# Update Rust -rustup update - -# Clean all build artifacts -make clean-all - -# Rebuild from scratch -make python-dev -``` - -### Import Errors - -```bash -# Ensure you're in the right virtual environment -which python - -# Clean and reinstall -make python-clean -make python-dev - -# Verify installation -python -c "import webnn; print(webnn.__version__)" -``` - -### ONNX Runtime Issues - -The Makefile automatically downloads ONNX Runtime for you: - -```bash -# Download ONNX Runtime manually if needed -make onnxruntime-download - -# Or install system-wide (optional) -brew install onnxruntime - -# Build with system ONNX Runtime -export ORT_STRATEGY=system -export ORT_LIB_LOCATION=/opt/homebrew/lib -make python-dev -``` - -### Test Failures - -```bash -# Run tests with verbose output -make python-test - -# Run specific test -python -m pytest tests/test_python_api.py::test_name -xvs - -# Check if backend is available -python -c "import webnn; ctx = webnn.ML().create_context(); print(ctx.accelerated)" -``` - -## Code Style - -### Rust - -- Follow [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) -- Use `cargo fmt` for formatting -- Use `cargo clippy` for linting -- Write doc comments for public APIs - -### Python - -- Follow [PEP 8](https://pep8.org/) -- Use type hints -- Write docstrings for public APIs -- Use `black` for formatting - -## Git Workflow - -### Commits - -```bash -# Stage changes -git add . - -# Commit with descriptive message -git commit -m "Add feature X - -- Detail 1 -- Detail 2 - -[BOT] Generated with [Claude Code](https://claude.com/claude-code)" - -# Push -git push origin main -``` - -### Pre-commit Hooks - -The project uses pre-commit hooks to ensure code quality: - -- `cargo fmt --check` and `cargo clippy` automatically run when Rust files change -- `make python-ty-check` runs Ty against pywebnn when Python files change -- Tests run automatically in CI - -## CI/CD - -### GitHub Actions - -The project uses GitHub Actions for CI: - -- `.github/workflows/ci.yml` - Main CI pipeline - - Runs on push and pull requests - - Tests on Linux and macOS - - Builds Python wheels - - Runs all tests - -### Local CI Simulation - -```bash -# Run the same checks as CI -make fmt # Format code -cargo clippy -- -D warnings # Lint checks -make validate-all-env # Full validation pipeline -make ci-docs # Documentation build (strict mode) -``` - -## Resources - -- [Rust Book](https://doc.rust-lang.org/book/) -- [PyO3 Guide](https://pyo3.rs/) -- [W3C WebNN Spec](https://www.w3.org/TR/webnn/) -- [ONNX Documentation](https://onnx.ai/) -- [CoreML Documentation](https://developer.apple.com/documentation/coreml) +# Setup and Workflow + +## Toolchain + +| Tool | Notes | +|---|---| +| Rust | `rust-toolchain.toml` pins the channel (1.97.0 at the time of writing) with `rustfmt` and `clippy`; rustup installs it on first use. The CI workflows pin the same version, so bump them together | +| `protoc` | Required: `build.rs` compiles the ONNX and CoreML protobuf schemas. Linux `apt-get install protobuf-compiler`, macOS `brew install protobuf`, Windows `winget install Google.Protobuf` | +| `flatc` | Required for `litert-runtime` (TFLite schema). CI downloads the flatbuffers release binary; macOS `brew install flatbuffers` | +| Node.js | WPT corpus fetch and evaluation (`scripts/fetch_wpt.mjs`, `scripts/wpt_bridge/`) | +| Python 3 | Only for MkDocs (`pip install -r docs/requirements.txt`) and `scripts/generate_backend_operator_report.py` | +| libclang | `trtx-runtime`: the `trtx-sys` crate generates bindings with autocxx; set `LIBCLANG_PATH` if it is not found | +| Graphviz | Optional, for `make viz` | + +Windows: install the Visual Studio C++ build tools and run `git config --system core.longpaths true` +before cloning. Backend libraries (ONNX Runtime, TensorRT-RTX, LiteRT) are described in +[Backends](../user-guide/backends.md). + +## Build and test + +Use the Makefile targets; they set feature flags and environment variables consistently. +`make help` lists all of them. + +```bash +make build # cargo build, no backend features +make test # cargo fmt, clippy -D warnings, cargo test, operator report drift check +make lint # clippy only +make fmt # rustfmt + +make onnxruntime-download # fetch the pinned ONNX Runtime into target/onnxruntime +make run # validate examples/sample_graph.json with the CLI +make onnx # convert the sample graph to ONNX (GRAPH_FILE=... selects another graph) +make onnx-validate # convert and execute with ONNX Runtime +make coreml # CoreML conversion (macOS); also make litert, make cann + +make test-wpt # WPT conformance on ONNX Runtime CPU +make test-wpt-op OP=relu # one operation +make test-wpt-trtx # TensorRT-RTX; also make test-wpt-litert, make test-wpt-coreml + +make docs-api # rustdoc, warnings are errors +make docs-build # MkDocs site into site/ +make ci-docs # MkDocs strict mode, as CI runs it +make docs-serve # live preview +make docs-backend-ops # regenerate docs/development/backend-operator-support.md +make docs-backend-ops-check + +make coverage # cargo-llvm-cov; also coverage-html, coverage-lcov +``` + +CI type-checks every backend. Do the same before pushing when shared code changed: + +```bash +cargo check +cargo check --features onnx-runtime +cargo check -F trtx-runtime --all-targets +cargo check --features litert-runtime +cargo check --features cann-runtime +cargo check --features coreml-runtime # macOS +cargo test --lib +cargo test --lib --features cann-runtime-mock +``` + +## Workflow for a change + +1. Branch from `main`. +2. Make the change with its tests. Unit tests live in `#[cfg(test)]` modules at the end of each + file; converter tests decode the emitted model and assert on its structure. +3. Run `make test`. +4. If an operation or a converter changed: run the affected WPT cases on every backend you can + (`make test-wpt-op OP=`, `WPT_BACKEND=` to pick one), regenerate snapshots or + expected-failure lists with `make wpt-sync-`, review the diff, and run + `make docs-backend-ops`. +5. Update the documentation as described in the [Documentation Policy](documentation-policy.md). + Run `make docs-api` and `make ci-docs` when rustdoc or pages changed. +6. Open a pull request with the template filled in. CI runs formatting, clippy, tests, rustdoc, + the operator report drift check, the docs build and the WPT suites. + +`scripts/install-git-hooks.sh` installs a pre-commit hook that runs `cargo fmt --check` and +clippy when Rust files are staged. + +## Adding an operation + +Check Chromium's implementation first. It is the WebNN reference and shows the lowering each +backend needs, for example casts for boolean types or decompositions: + +- https://chromium.googlesource.com/chromium/src/+/lkgr/services/webnn/ort/graph_builder_ort.cc (ONNX Runtime) +- https://chromium.googlesource.com/chromium/src/+/lkgr/services/webnn/coreml/graph_builder_coreml.mm (CoreML) +- https://chromium.googlesource.com/chromium/src/+/lkgr/services/webnn/tflite/graph_builder_tflite.cc (LiteRT) + +Then: + +1. `src/operator_options.rs`: add the `ML*Options` struct when the spec defines a new dictionary. +2. `src/operators.rs`: add the `Operation` variant with named operand fields, its `op_type()` + name and the `from_json_attributes` parsing. +3. `src/shape_inference.rs`: output shape and data type rules, with unit tests. +4. `src/mlgraphbuilder.rs`: the builder method, through one of the `impl_*_op!` macros or + explicitly when the signature does not fit. +5. `src/webnn_json.rs`: import and export mapping for the text and JSON formats. +6. Converters: `src/converters/onnx.rs`, `coreml_mlprogram.rs`, `trtx.rs`, `litert.rs`, `cann.rs`. + A backend that cannot support the operation must reject it explicitly (LiteRT: + `unsupported_ops`; CANN: `is_supported_op`). +7. WPT: `make test-wpt-op OP=` on every backend you can run, then `make wpt-sync-`. +8. `make docs-backend-ops`, and add the method to the operation table in + `docs/user-guide/api-reference.md`. + +## Adding a backend + +1. Converter: implement `GraphConverter` in `src/converters/.rs` and register it in + `ConverterRegistry::with_defaults`. +2. Backend: implement `MLBackendContext`, `MLBackendBuilder` and `ListDevices` in + `src/backends/.rs`; add the `MLBackendGraph` variant and a `DisabledContext` alias in + `src/backends/mod.rs`. +3. Selection: add the `Backend` and `BackendDevice` variants and the position in the order in + `src/backend_selection.rs`; extend the fields of the `NoBackendAvailable` errors. +4. Cargo: feature flag, optional dependencies, a mock feature when the hardware is not available + in CI, and a `cargo check` step in `.github/workflows/ci.yml`. +5. Tests: a WPT backend entry in `tests/wpt_conformance/wpt_backend.rs`, an expected-failure + file or snapshots, a `test-wpt-` Make target, an integration test under `tests/`. +6. Report: a detection rule in `scripts/generate_backend_operator_report.py` and its tests. +7. Docs: `docs/user-guide/backends.md`, the feature table in `src/lib.rs`, an integration page + when setup is involved. + +## Continuous integration + +| Workflow | Trigger | Content | +|---|---|---| +| `ci.yml` | push, pull request | Cargo.lock consistency, `cargo fmt --check`, `cargo check` per feature (including wasm32, and CoreML on macOS), `cargo test --lib` on Linux and macOS plus the CANN mock, rustdoc with `-D warnings`, operator report drift check and generator tests, MkDocs strict build, version check on tags | +| `wpt-conformance.yml` | push, pull request | WPT suites for ONNX Runtime (Linux), LiteRT (Linux, non-blocking) and CoreML (macOS) | +| `wpt-conformance-nightly.yml` | schedule | Full WPT run with JSON and HTML reports; publishes the dashboard together with the docs site | +| `snapshot-sync.yml` | weekly, manual | Regenerates snapshots and expected failures against the pinned WPT revision and opens a pull request | +| `rustnnpt-gate.yml` | pull request | Runs the external rustnnpt conformance runner against the PR's rustnn and enforces a minimum pass rate | +| `docs.yml`, `docs-pr.yml` | push to `main`, pull request | MkDocs strict build, rustdoc embedded under `/api/`, WPT report embedded, link check on PRs, deploy to GitHub Pages from `main` | +| `publish.yml` | GitHub release | fmt, clippy, tests, `cargo publish` | + +TensorRT-RTX has no GPU runner. CI compiles it (`cargo check -F trtx-runtime --all-targets`); +contributors run `make test-wpt-trtx` locally before and after changing the converter. + +## Releasing + +Bump `version` in `Cargo.toml`, update `Cargo.lock`, tag `vX.Y.Z` and create a GitHub release. +`publish.yml` verifies that the version matches the tag and publishes to crates.io; docs.rs +builds the API docs with the features listed under `[package.metadata.docs.rs]`. + +## Code style + +- `cargo fmt` and `cargo clippy --all-targets -- -D warnings` must pass. +- Comments explain non-obvious decisions in one line and use ASCII only. No emojis anywhere in + the repository. +- Errors are typed with `thiserror`, carry context and are `Send + Sync`. +- Public items get rustdoc; a new module gets a `//!` header saying what the module owns. diff --git a/docs/index.md b/docs/index.md index 10a0ae6a..8c9ee22d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,98 +1,104 @@
- rustnn logo + rustnn logo
-# WebNN Python API Documentation - -Welcome to the WebNN Python API documentation. This library provides Python bindings for the [W3C WebNN (Web Neural Network) API](https://www.w3.org/TR/webnn/), enabling you to build, validate, and execute neural network graphs in Python. - -## Overview - -The WebNN Python API allows you to: - -- **Build neural network graphs** using a simple, intuitive Python API -- **Validate graphs** using the same validation logic as web browsers -- **Convert graphs** to ONNX and CoreML formats -- **Execute models** on CPU, GPU, or Neural Engine (macOS) -- **Integrate seamlessly** with NumPy for tensor operations - -## Key Features - -- ✓ **W3C Standard Compliant** - Implements the official WebNN specification -- ✓ **85 Operations** - 89% coverage of WebNN spec operations -- ✓ **Type-Safe** - Full type hints for IDE autocomplete -- ✓ **NumPy Integration** - Seamless conversion between NumPy arrays -- ✓ **Multiple Backends** - ONNX Runtime (CPU/GPU) and CoreML (macOS) -- ✓ **Actual Execution** - Run models with real tensor inputs/outputs -- ✓ **Async Support** - Non-blocking execution with Python asyncio -- ✓ **Fast** - Built with Rust and PyO3 for maximum performance -- ✓ **Cross-Platform** - Works on Linux, macOS, and Windows - -## Quick Example - -```python -import webnn -import numpy as np - -# Create ML context with device hints -ml = webnn.ML() -context = ml.create_context(accelerated=True) # Request GPU/NPU if available -builder = context.create_graph_builder() - -# Build a simple computation: z = relu(x + y) -x = builder.input("x", [2, 3], "float32") -y = builder.input("y", [2, 3], "float32") -z = builder.add(x, y) -output = builder.relu(z) - -# Compile the graph (backend-agnostic) -graph = builder.build({"output": output}) - -# Execute with actual data -x_data = np.array([[1, -2, 3], [4, -5, 6]], dtype=np.float32) -y_data = np.array([[-1, 2, -3], [-4, 5, -6]], dtype=np.float32) -results = context.compute(graph, {"x": x_data, "y": y_data}) - -print(results["output"]) # [[0. 0. 0.] [0. 0. 0.]] - -# Export to ONNX for deployment -context.convert_to_onnx(graph, "model.onnx") -``` - -## Installation - -### From Source - -```bash -# Clone the repository -git clone https://github.com/tarekziade/rustnn.git -cd rustnn - -# Install maturin -pip install maturin - -# Build and install -maturin develop --features python +# rustnn + +rustnn is a Rust implementation of the [W3C WebNN API](https://www.w3.org/TR/webnn/). It records +neural network graphs with a WebNN-style builder, validates and shape-infers them, and executes +them on a pluggable backend. + +**Experimental.** rustnn is published as a development release (`0.5.x`). APIs change without +notice and the crate is not meant for production use. + +## What rustnn provides + +- **The WebNN API in Rust.** `MLContext`, `MLGraphBuilder`, `MLGraph`, `MLTensor` and + `dispatch` mirror the JavaScript API. rustnn-specific additions carry a `rustnn_` prefix. +- **Backends selected at context creation.** ONNX Runtime, NVIDIA TensorRT-RTX, Apple CoreML, + LiteRT and Huawei CANN, chosen from the WebNN `accelerated` and power-preference hints or + forced with a backend hint. A browser WebNN backend for `wasm32` is in progress. +- **Graph interchange.** Loads `.webnn` text and JSON graphs written by the + [webnn-graph](https://github.com/rustnn/webnn-graph) crate and by + [onnx2webnn](https://github.com/rustnn/onnx2webnn), saves graphs with `.safetensors` + weights, and exports ONNX and CoreML models. +- **Conformance.** The upstream WebNN Web Platform Tests run in-repo against every backend on + each pull request; the nightly [WPT dashboard](https://rustnn.github.io/rustnn/wpt-conformance/) + shows per-operation results. + +## Quick example + +```rust +use rustnn::mlcontext::{ + MLContext, MLContextOptions, MLGraphBuilder, MLNamedOperands, MLNamedTensors, + MLOperandDescriptor, MLPowerPreference, MLTensorDescriptor, +}; +use rustnn::operator_enums::MLOperandDataType; + +fn main() -> rustnn::error::Result<()> { + // Pick a backend from the WebNN hints: not accelerated selects a CPU device. + let options = MLContextOptions::new(MLPowerPreference::Default, false); + let mut context = MLContext::create(&options)?; + + // Record y = relu(x + 1) and compile it for the selected backend. + let mut builder = MLGraphBuilder::new(&mut context)?; + let descriptor = MLOperandDescriptor::new(MLOperandDataType::Float32, vec![2, 2]); + let x = builder.input("x", &descriptor)?; + let one = builder.constant_from_slice(&descriptor, &[1.0f32; 4])?; + let sum = builder.add(x, one)?; + let y = builder.relu(sum)?; + let mut graph_outputs = MLNamedOperands::new(); + graph_outputs.insert("y", y); + let mut graph = builder.build(&graph_outputs)?; + + // Tensors live on the backend device; flags control host access. + let tensor = MLTensorDescriptor::new(MLOperandDataType::Float32, vec![2, 2]); + let x_tensor = context.create_tensor(&tensor.to_writable())?; + let y_tensor = context.create_tensor(&tensor.to_readable())?; + context.write_tensor(&x_tensor, &[-2.0f32, -1.0, 0.0, 1.0])?; + + let mut inputs = MLNamedTensors::new(); + inputs.insert("x", &x_tensor); + let mut outputs = MLNamedTensors::new(); + outputs.insert("y", &y_tensor); + context.dispatch(&mut graph, &inputs, &outputs)?; + + let mut result = [0.0f32; 4]; + context.read_tensor(&y_tensor, &mut result)?; + assert_eq!(result, [0.0, 0.0, 1.0, 2.0]); + Ok(()) +} ``` -### From PyPI - -```bash -pip install pywebnn -``` - -## Documentation Structure - -- **[Getting Started](user-guide/getting-started.md)** - Installation and first steps -- **[API Reference](user-guide/api-reference.md)** - Complete API documentation -- **[Examples](user-guide/examples.md)** - Code examples and tutorials -- **[Advanced Topics](user-guide/advanced.md)** - Advanced usage patterns +Build it with a runtime feature, for example `cargo run --features onnx-runtime`, with the ONNX +Runtime shared library reachable through `ORT_DYLIB_PATH` (see +[Getting Started](user-guide/getting-started.md)). + +## Documentation map + +| Section | Content | +|---|---| +| [Getting Started](user-guide/getting-started.md) | Requirements, adding the crate, first graph, loading `.webnn` files, the CLI | +| [API Overview](user-guide/api-reference.md) | The WebNN API types, builder conventions, options, data types, errors | +| [Backends](user-guide/backends.md) | Selection rules and per-backend requirements, execution model and test coverage | +| [Examples](user-guide/examples.md) | The example programs in `examples/` and short recipes | +| [Advanced Topics](user-guide/advanced.md) | Backend hints and options, dynamic shapes, saving and exporting graphs, caching, debugging | +| [Rust API Reference](https://rustnn.github.io/rustnn/api/rustnn/) | Generated rustdoc for every public item (`make docs-api` locally) | +| [Architecture](architecture/overview.md) | Layers, data flow, module map and design decisions | +| [Development](development/setup.md) | Toolchain, build and test commands, adding operations and backends, CI | +| [Documentation Policy](development/documentation-policy.md) | What to update when code changes; rules for contributors and coding agents | +| [Implementation Status](development/implementation-status.md) | API surface, backend status and known gaps | +| [Backend Operator Support](development/backend-operator-support.md) | Generated operation-by-backend matrix | +| [WPT Conformance Guide](testing/wpt-test-guide.md) | Running and triaging the Web Platform Tests | +| [TensorRT-RTX](integration/tensorrt.md) | The native TensorRT backend, caching and precision | + +## Python + +Python bindings live in the separate [pywebnn](https://github.com/rustnn/pywebnn) package, which +uses rustnn as its core library. This repository contains no Python API. ## Support -- **GitHub Issues**: [Report bugs or request features](https://github.com/tarekziade/rustnn/issues) -- **Specification**: [W3C WebNN Spec](https://www.w3.org/TR/webnn/) - -## License - -Apache-2.0 License - See [LICENSE](https://github.com/tarekziade/rustnn/blob/main/LICENSE) for details. +- Issues and discussions: [github.com/rustnn/rustnn](https://github.com/rustnn/rustnn/issues) +- Specification: [W3C WebNN](https://www.w3.org/TR/webnn/) +- License: Apache-2.0 ([LICENSE](https://github.com/rustnn/rustnn/blob/main/LICENSE)) diff --git a/docs/integration/tensorrt.md b/docs/integration/tensorrt.md index e1d9e58d..94f1dec9 100644 --- a/docs/integration/tensorrt.md +++ b/docs/integration/tensorrt.md @@ -1,850 +1,117 @@ -# TensorRT Integration Guide +# TensorRT-RTX Backend + +The `trtx` backend runs WebNN graphs on NVIDIA RTX GPUs through +[TensorRT for RTX](https://developer.nvidia.com/tensorrt-rtx) using the +[trtx](https://github.com/rustnn/trtx-rs) crate. Graphs are lowered directly into a TensorRT +network by `src/converters/trtx.rs` (with `trtx_gru.rs`, `trtx_lstm.rs` and `trtx_rnn.rs` for the +recurrent operations); there is no ONNX intermediate. The backend itself is +`src/backends/trtx.rs`. + +## Features + +| Feature | Use | +|---|---| +| `trtx-runtime` | The backend and converter; pulls in `trtx`, `cudarc` and `zstd-cache-compression` | +| `trtx-runtime-mock` | Builds the converter and the `--run-trtx` CLI path against the mock `trtx` API, without TensorRT | +| `trtx-enterprise` | `trtx-runtime` linked against the enterprise TensorRT library, for validation only | +| `zstd-cache-compression` | Compresses the on-disk caches; enabled by `trtx-runtime` | + +## Requirements + +- An NVIDIA RTX GPU with a current driver. The CUDA driver API is loaded at run time through + `cudarc`; the CUDA toolkit is not required. +- The TensorRT-RTX 1.6 library. The `trtx` crate loads it by name (`tensorrt_rtx_1_6`), so the + SDK's `lib` directory (Linux) or `bin` directory (Windows) must be on `LD_LIBRARY_PATH` or + `PATH`, or the application calls `rustnn::executors::trtx::dynamically_load_tensorrt(Some(path))` + before creating a context. When the library cannot be loaded, backend selection skips + TensorRT (on Windows the underlying error is `LoadLibraryExW` code 126). +- Build time: `trtx-sys` generates bindings from the TensorRT headers with autocxx and needs + libclang (`LIBCLANG_PATH` when it is not found automatically). `TENSORRT_INCLUDE_DIR`, + `TENSORRT_LIB_DIR` and `TENSORRT_SDK_DIR` override the header and library locations. + +The Windows steps are in [Windows TensorRT-RTX Setup](windows-tensorrt-setup.md). + +## Selection + +With `accelerated = true` and the `Default` or `HighPerformance` power preference, TensorRT-RTX +is the first choice when the feature is enabled and a CUDA device is present. Force it with +`MLContextOptions::with_rustnn_backend_hint(Backend::Trtx)` or pick a GPU with +`with_rustnn_device_hint(BackendDevice::Trtx { cuda_device_idx })`. `RUST_LOG=info` prints the +devices found and `Backend selected: Trtx { cuda_device_idx: 0 }`. + +## How a graph runs + +1. `TrtxConverter::build_network` translates the `GraphInfo` into a TensorRT network. Constants + become weights and are marked refittable, except constants that TensorRT bakes into the + engine (for example scalars cast to another type) and constants without a consumer. +2. The engine is built with `kREFIT_INDIVIDUAL` and `kSTRIP_PLAN`, and with TF32 disabled so + that float32 graphs compute in full float32 precision (the WPT references are strict IEEE + float32). Float16 graphs run in float16. +3. The stripped engine is stored in the engine cache and then refitted with the actual weights. + Loading a cached engine skips the build entirely. Engines are cached only when every constant + is refittable, so weight data never ends up in the cache. +4. Tensors are CUDA device buffers. `dispatch` binds them by name, captures a CUDA graph per + distinct binding set and replays it on later calls. `write_tensor` synchronizes the stream + before it returns. + +## Options + +Set through `RustNNOptions::trtx` (`TrtxOptions`): + +| Field | Default | Effect | +|---|---|---| +| `engine_caching` | `true` | Store and reuse stripped engines by topology hash | +| `runtime_cache` | `true` | Use the shared TensorRT runtime (JIT kernel) cache | +| `fail_on_cache_miss` | `false` | Never build; fail with `TrtxEngineCacheMiss` when no cached engine exists (ahead-of-time workflows) | +| `cuda_graphs` | `true` | Capture and replay CUDA graphs for dispatch | -**Date:** December 8, 2024 -**Purpose:** Guide for adding NVIDIA TensorRT converter and executor to rustnn - ---- - -## [TARGET] Overview - -This document outlines the integration of [NVIDIA TensorRT](https://developer.nvidia.com/tensorrt) as a fourth execution backend for rustnn, optimized for NVIDIA GPU inference alongside ONNX Runtime, CoreML, and GGML. - -**Why TensorRT?** -- **GPU-optimized inference**: Best-in-class performance on NVIDIA GPUs (RTX, A100, H100) -- **Advanced quantization**: FP16, INT8, INT4, FP8, FP4 for maximum throughput -- **JIT optimization**: Just-In-Time compilation for specific GPU architectures -- **Production-ready**: Widely deployed in NVIDIA-accelerated inference (Triton, TensorRT-LLM) -- **ONNX-native**: Primary import via ONNX format (perfect match for rustnn) - -**TensorRT for RTX (New in 2025):** -- Lightweight library (<200 MB) optimized for Windows 11 + NVIDIA RTX GPUs -- 50%+ performance improvement vs baseline DirectML -- JIT compilation in <30 seconds -- Supports Turing through Blackwell GPU generations - ---- - -## TensorRT Background - -### What is TensorRT? - -TensorRT is NVIDIA's high-performance deep learning inference SDK. It optimizes trained models through: -- **Layer fusion**: Combines operations to reduce kernel launches -- **Precision calibration**: INT8/FP16 quantization with minimal accuracy loss -- **Kernel auto-tuning**: Selects fastest implementation for target GPU -- **Dynamic tensor memory**: Minimizes memory footprint - -**Key Resources:** -- [TensorRT Documentation](https://docs.nvidia.com/deeplearning/tensorrt/latest/index.html) -- [TensorRT SDK](https://developer.nvidia.com/tensorrt) -- [TensorRT for RTX (Windows 11)](https://docs.nvidia.com/deeplearning/tensorrt-rtx/latest/index.html) -- [ONNX-TensorRT GitHub](https://github.com/onnx/onnx-tensorrt) - -### TensorRT Architecture - -**Core Workflow:** -``` -ONNX Model → TensorRT Builder → Optimized Engine → Inference Runtime -``` - -**Key Concepts:** -1. **Builder (`IBuilder`)**: Configures optimization settings (precision, batch size, workspace) -2. **Network (`INetworkDefinition`)**: Graph of layers and tensors -3. **Engine (`ICudaEngine`)**: Optimized executable for specific GPU + precision -4. **Context (`IExecutionContext`)**: Runtime state for executing inference -5. **Parser (`IParser`)**: Imports ONNX models into TensorRT network - -**Optimization Pipeline:** -```rust -// 1. Create builder and network -let builder = create_infer_builder(); -let network = builder.create_network(); - -// 2. Parse ONNX model -let parser = create_onnx_parser(network); -parser.parse_from_file("model.onnx"); - -// 3. Build optimized engine -let config = builder.create_builder_config(); -config.set_flag(BuilderFlag::FP16); // Enable FP16 -let engine = builder.build_engine(network, config); - -// 4. Execute inference -let context = engine.create_execution_context(); -context.execute_v2(&bindings); // Run inference -``` - -### Supported Operations - -**300+ ONNX Operators** (opset 9-20) including: - -**Binary Operations:** -- Add, Sub, Mul, Div, MatMul, Pow -- Broadcasting support - -**Activations:** -- Relu, Sigmoid, Tanh, Softmax, Gelu, Elu, LeakyRelu, PRelu, Selu, HardSigmoid, HardSwish, Softplus, Softsign - -**Convolution & Pooling:** -- Conv, ConvTranspose (2D and 3D) -- MaxPool, AveragePool, GlobalAveragePool, GlobalMaxPool -- LpPool (with restrictions) - -**Normalization:** -- BatchNormalization, InstanceNormalization, LayerNormalization, GroupNormalization, LRN - -**Reduction:** -- ReduceSum, ReduceMean, ReduceMax, ReduceMin, ReduceProd -- ReduceL1, ReduceL2, ReduceLogSum, ReduceLogSumExp, ReduceSumSquare - -**Tensor Manipulation:** -- Reshape, Transpose, Concat, Split, Slice, Gather, Scatter, Squeeze, Unsqueeze, Expand, Pad, Tile - -**Comparison & Logic:** -- Equal, Greater, GreaterOrEqual, Less, LessOrEqual -- And, Or, Xor, Not - -**Math Functions:** -- Abs, Neg, Ceil, Floor, Round, Sqrt, Exp, Log, Sin, Cos, Tan, Asin, Acos, Atan, Sinh, Cosh, Tanh, Asinh, Acosh, Atanh, Erf, Sign, Reciprocal - -**Advanced:** -- LSTM, GRU (with restrictions) -- Attention mechanisms -- Einsum -- TopK, ArgMax, ArgMin -- Cast, Clip, Where - -**Quantization:** -- QuantizeLinear, DequantizeLinear - -**Data Types:** -DOUBLE, FLOAT32, FLOAT16, BFLOAT16, INT32, INT64, FP8, INT8, INT4, UINT8, BOOL - -**Important Limitations:** -- DOUBLE cast to FLOAT32 (with clamping) -- UINT8 only for input/output tensors -- INT8/INT4/FP8 require quantization from FP32/FP16 -- Some ops restricted to 2D/3D (e.g., pooling) - ---- - -## Integration Architecture - -### Following rustnn Patterns - -rustnn uses a **converter + executor** pattern: - -``` -WebNN GraphInfo → Converter → ONNX → TensorRT Engine → Executor → Results -``` - -**Existing Backends:** -1. **ONNX Runtime**: Cross-platform, protobuf → ONNX Runtime execution -2. **CoreML**: macOS-only, protobuf → CoreML execution -3. **GGML**: CPU-optimized, in-memory graph → GGML execution - -**New TensorRT Backend:** -4. **TensorRT**: NVIDIA GPU, ONNX → TensorRT Engine → GPU execution - -**Key Advantage:** We already have ONNX converter! TensorRT can consume ONNX directly. - -### File Structure - -``` -src/ - converters/ - mod.rs # Already has OnnxConverter (reuse!) - onnx.rs - coreml_mlprogram.rs - ggml.rs - tensorrt.rs # NEW: TensorRT-specific converter (optional) - executors/ - mod.rs # Add #[cfg(feature = "tensorrt-runtime")] - onnx.rs - coreml.rs - ggml.rs - tensorrt.rs # NEW: TensorRT executor - python/ - context.rs # Add Backend::TensorRT variant -``` - ---- - -## Implementation Plan - -### Phase 1: Executor (ONNX → TensorRT Engine) - -**File:** `src/executors/tensorrt.rs` - -**Feature Gate:** `#[cfg(feature = "tensorrt-runtime")]` - -**Strategy:** Reuse existing ONNX converter, build TensorRT engine from ONNX bytes - -**Implementation:** -```rust -#![cfg(feature = "tensorrt-runtime")] - -use crate::error::GraphError; -use crate::graph::{GraphInfo, OperandDescriptor}; -use std::collections::HashMap; - -pub struct TensorRTOutput { - pub name: String, - pub shape: Vec, - pub data: Vec, -} - -pub struct TensorRTInput { - pub name: String, - pub shape: Vec, - pub data: Vec, -} - -/// Execute TensorRT inference from ONNX model bytes -pub fn run_tensorrt_with_inputs( - onnx_model: &[u8], - inputs: HashMap, - precision: TensorRTPrecision, -) -> Result, GraphError> { - // 1. Create TensorRT builder - let logger = create_logger(); - let builder = create_infer_builder(&logger)?; - - // 2. Parse ONNX model - let network_flags = 1u32 << NetworkDefinitionCreationFlag::ExplicitBatchDimensions as u32; - let network = builder.create_network_v2(network_flags)?; - - let parser = create_onnx_parser(&network, &logger)?; - parser.parse(onnx_model)?; - - // 3. Configure builder - let config = builder.create_builder_config()?; - config.set_memory_pool_limit(MemoryPoolType::Workspace, 1 << 30)?; // 1GB - - // Set precision mode - match precision { - TensorRTPrecision::FP32 => {}, - TensorRTPrecision::FP16 => config.set_flag(BuilderFlag::FP16)?, - TensorRTPrecision::INT8 => config.set_flag(BuilderFlag::INT8)?, - } - - // 4. Build engine - let engine = builder.build_serialized_network(&network, &config)?; - let runtime = create_infer_runtime(&logger)?; - let engine = runtime.deserialize_cuda_engine(&engine)?; - - // 5. Create execution context - let context = engine.create_execution_context()?; - - // 6. Allocate GPU buffers and copy inputs - let bindings = allocate_and_copy_inputs(&engine, inputs)?; - - // 7. Execute inference - context.execute_v2(&bindings)?; - - // 8. Copy outputs back to CPU - let outputs = copy_outputs_from_gpu(&engine, &bindings)?; - - Ok(outputs) -} - -#[derive(Debug, Clone, Copy)] -pub enum TensorRTPrecision { - FP32, - FP16, - INT8, -} -``` - -**Key Challenges:** -1. **Rust bindings**: Use `tensorrt-rs` or `easy-tensorrt-sys` (FFI to C++ API) -2. **GPU memory management**: Allocate CUDA buffers for inputs/outputs -3. **Engine caching**: Serialized engines can be cached for faster startup -4. **Precision selection**: FP32/FP16/INT8 based on device hints -5. **Batch size**: Dynamic batch support vs fixed batch - -### Phase 2: Feature Flag & Dependencies - -**File:** `Cargo.toml` - -**Changes:** -```toml -[features] -default = [] -coreml-runtime = ["objc"] -onnx-runtime = ["onnxruntime"] -ggml-runtime = ["ggml"] -tensorrt-runtime = ["tensorrt-rs", "cuda-runtime"] # NEW - -[dependencies] -# ... existing dependencies ... -tensorrt-rs = { version = "0.8", optional = true } # NEW -cuda-runtime = { version = "0.7", optional = true } # NEW -# Alternative: easy-tensorrt-sys for more recent bindings -``` - -**Rust Bindings Options:** - -| Crate | Status | Notes | -|-------|--------|-------| -| `tensorrt-rs` | Older (2020) | Supports TensorRT 5-7, may need fork | -| `easy-tensorrt-sys` | Newer fork | Uses `cudarc` instead of old `cuda-rs` | -| Custom FFI | Most control | Bindgen to TensorRT C++ API | - -**Recommendation:** Start with `easy-tensorrt-sys` or custom FFI for TensorRT 10.x support - -### Phase 3: Registration - -**File:** `src/executors/mod.rs` - -**Changes:** -```rust -#[cfg(all(target_os = "macos", feature = "coreml-runtime"))] -pub mod coreml; -#[cfg(feature = "onnx-runtime")] -pub mod onnx; -#[cfg(feature = "ggml-runtime")] -pub mod ggml; -#[cfg(feature = "tensorrt-runtime")] // NEW -pub mod tensorrt; -``` - -**File:** `src/converters/mod.rs` - -**No changes needed!** Reuse existing `OnnxConverter` to generate ONNX bytes, then TensorRT executor parses ONNX directly. - -### Phase 4: Python API Integration - -**File:** `src/python/context.rs` - -**Changes:** -```rust -#[derive(Debug, Clone)] -enum Backend { - OnnxCpu, - OnnxGpu, - CoreML, - Ggml, - TensorRT, // NEW - None, -} - -impl PyMLContext { - fn select_backend(accelerated: bool, power: &str) -> (Backend, bool) { - // TensorRT selection logic - if accelerated { - #[cfg(feature = "tensorrt-runtime")] - if is_nvidia_gpu_available() { - // Prefer TensorRT on NVIDIA GPUs for high-performance - if power == "high-performance" { - return (Backend::TensorRT, true); - } - } - } - - // Existing logic for ONNX/CoreML/GGML... - } - - fn compute_tensorrt( - &self, - graph: &PyMLGraph, - inputs: HashMap>>>, - ) -> Result>>>, GraphError> { - #[cfg(feature = "tensorrt-runtime")] - { - use crate::converters::OnnxConverter; // Reuse ONNX converter! - use crate::executors::tensorrt::{run_tensorrt_with_inputs, TensorRTInput, TensorRTPrecision}; - - // 1. Convert GraphInfo to ONNX - let converter = OnnxConverter::default(); - let converted = converter.convert(&graph.graph)?; - - // 2. Convert inputs to TensorRTInput - let trt_inputs = convert_numpy_to_tensorrt(inputs)?; - - // 3. Execute with TensorRT - let precision = TensorRTPrecision::FP16; // Could be configurable - let outputs = run_tensorrt_with_inputs(&converted.data, trt_inputs, precision)?; - - // 4. Convert outputs back to NumPy - convert_tensorrt_to_numpy(outputs) - } - #[cfg(not(feature = "tensorrt-runtime"))] - Err(GraphError::BackendUnavailable { - backend: "TensorRT".to_string(), - }) - } -} - -#[cfg(feature = "tensorrt-runtime")] -fn is_nvidia_gpu_available() -> bool { - // Check for CUDA-capable NVIDIA GPU - // Could use cuda-runtime or parse nvidia-smi - std::process::Command::new("nvidia-smi") - .output() - .map(|output| output.status.success()) - .unwrap_or(false) -} -``` - -### Phase 5: Engine Caching (Performance Optimization) - -**Problem:** TensorRT engine building can take 10-60 seconds on first run. - -**Solution:** Cache serialized engines to disk, keyed by model hash + GPU architecture. - -**Implementation:** ```rust -use std::path::PathBuf; -use std::fs; -use sha2::{Sha256, Digest}; - -fn get_engine_cache_path(onnx_model: &[u8], gpu_arch: &str, precision: TensorRTPrecision) -> PathBuf { - let mut hasher = Sha256::new(); - hasher.update(onnx_model); - hasher.update(gpu_arch.as_bytes()); - hasher.update(format!("{:?}", precision).as_bytes()); - let hash = format!("{:x}", hasher.finalize()); - - PathBuf::from(format!(".tensorrt_cache/engine_{}.trt", hash)) -} - -pub fn run_tensorrt_with_caching( - onnx_model: &[u8], - inputs: HashMap, - precision: TensorRTPrecision, -) -> Result, GraphError> { - let gpu_arch = get_gpu_architecture()?; // e.g., "sm_89" for RTX 4090 - let cache_path = get_engine_cache_path(onnx_model, &gpu_arch, precision); - - let engine = if cache_path.exists() { - // Load cached engine - let serialized = fs::read(&cache_path)?; - let runtime = create_infer_runtime(&logger)?; - runtime.deserialize_cuda_engine(&serialized)? - } else { - // Build new engine - let engine = build_engine(onnx_model, precision)?; - - // Cache for future use - let serialized = engine.serialize()?; - fs::create_dir_all(cache_path.parent().unwrap())?; - fs::write(&cache_path, serialized)?; - - engine - }; - - // Execute with cached/new engine - execute_engine(engine, inputs) -} +let mut options = RustNNOptions::default(); +options.trtx.cuda_graphs = false; +let context_options = MLContextOptions::new(MLPowerPreference::HighPerformance, true) + .with_rustnn_backend_hint(Backend::Trtx) + .with_rustnn_options(options); ``` ---- - -## [STATS] Operation Coverage Analysis - -### WebNN Operations → TensorRT Support - -| WebNN Operation | TensorRT Support | Notes | -|----------------|------------------|-------| -| **Binary Ops** | | | -| `add`, `sub`, `mul`, `div` | [OK] Full | Via Add, Sub, Mul, Div | -| `matmul` | [OK] Full | Via MatMul | -| `pow` | [OK] Full | Via Pow | -| **Activations** | | | -| `relu`, `sigmoid`, `tanh`, `softmax` | [OK] Full | Native support | -| `gelu`, `elu`, `leakyRelu`, `prelu` | [OK] Full | Native support | -| `hardSigmoid`, `hardSwish`, `softplus`, `softsign` | [OK] Full | Native support | -| **Convolution** | | | -| `conv2d`, `convTranspose2d` | [OK] Full | 2D and 3D supported | -| **Pooling** | | | -| `averagePool2d`, `maxPool2d` | [OK] Full | 2D/3D, indices unsupported for MaxPool | -| `globalAveragePool`, `globalMaxPool` | [OK] Full | Native support | -| **Normalization** | | | -| `batchNormalization` | [OK] Full | Native support | -| `instanceNormalization` | [OK] Full | Native support | -| `layerNormalization` | [OK] Full | Native support | -| **Reduction** | | | -| All `reduce*` operations | [OK] Full | 10 reduction ops supported | -| **Tensor Ops** | | | -| `reshape`, `transpose`, `concat`, `split` | [OK] Full | Native support | -| `slice`, `gather`, `scatter`, `pad`, `tile` | [OK] Full | Native support | -| `squeeze`, `unsqueeze`, `expand` | [OK] Full | Native support | -| **Logic** | | | -| All comparison and logical ops | [OK] Full | 9 ops supported | -| **Math** | | | -| All element-wise math | [OK] Full | 23 ops supported | -| **Quantization** | | | -| `quantizeLinear`, `dequantizeLinear` | [OK] Full | Native support | -| **Advanced** | | | -| `argMax`, `argMin` | [OK] Full | Via ArgMax, ArgMin | -| `cast`, `clamp`, `where` | [OK] Full | Via Cast, Clip, Where | -| `gemm` | [OK] Full | Via Gemm | - -**Coverage:** ~95%+ of WebNN spec (TensorRT has 300+ ONNX ops, WebNN has 85-95 ops) - -**Not Supported:** -- Some RNN/LSTM restrictions (bidirectional requires matching activations) -- MaxPool indices output -- Certain dilation/padding combinations -- DOUBLE precision (cast to FLOAT32) - ---- - -## Challenges & Solutions - -### Challenge 1: Rust Bindings Maturity - -**Problem:** Existing Rust bindings (`tensorrt-rs`) are outdated (TensorRT 5-7, last update 2020). - -**Solutions:** -1. **Use `easy-tensorrt-sys`**: Newer fork with better CUDA integration via `cudarc` -2. **Create custom FFI**: Use `bindgen` to generate fresh bindings for TensorRT 10.x -3. **Fork and update `tensorrt-rs`**: Modernize existing crate for TensorRT 10.x -4. **Wait for official bindings**: NVIDIA may release official Rust support (unlikely short-term) - -**Recommendation:** Create custom FFI bindings for TensorRT 10.x C++ API using `bindgen`. Focus on core interfaces: IBuilder, INetworkDefinition, IExecutionContext, IParser. - -### Challenge 2: CUDA Dependency - -**Problem:** TensorRT requires CUDA toolkit and NVIDIA GPU runtime. - -**Solutions:** -- **Feature flag**: Only enable with `tensorrt-runtime` feature -- **Runtime detection**: Check for NVIDIA GPU before selecting backend -- **Clear errors**: Provide helpful error if CUDA unavailable -- **Documentation**: Document CUDA installation requirements - -### Challenge 3: Engine Build Time - -**Problem:** Building TensorRT engine can take 10-60 seconds on first run. - -**Solutions:** -- **Engine caching**: Serialize engines to disk, key by model hash + GPU arch -- **Ahead-of-time compilation**: Pre-build engines for target GPUs -- **JIT progress**: Show progress during engine building -- **TensorRT for RTX**: JIT compilation in <30 seconds (Windows 11) - -### Challenge 4: Precision Selection - -**Problem:** TensorRT supports FP32, FP16, INT8, FP8, FP4. How to select? - -**Solutions:** -- Follow WebNN device hints: - - `power="high-performance"` → FP16 (2x faster than FP32) - - `power="default"` → FP16 - - `power="low-power"` → INT8 (requires calibration) -- Add optional precision parameter to `compute()` -- Auto-detect GPU capability (e.g., FP8 only on Ada/Hopper) - -### Challenge 5: Platform Support - -**Problem:** TensorRT is NVIDIA GPU-only (Linux, Windows). No macOS/AMD support. - -**Solutions:** -- **Runtime detection**: Check for NVIDIA GPU at context creation -- **Graceful fallback**: Fall back to ONNX Runtime if TensorRT unavailable -- **Clear documentation**: Document platform requirements -- **Windows focus**: Leverage TensorRT for RTX (Windows 11 + RTX GPUs) - -### Challenge 6: Dynamic Shapes - -**Problem:** TensorRT engines can have fixed or dynamic input shapes. - -**Solutions:** -- **Use explicit batch**: Set `ExplicitBatchDimensions` flag -- **Optimization profiles**: Define min/opt/max shapes for dynamic inputs -- **Runtime binding**: Bind shapes at execution time -- **Future work**: Add dynamic shape support incrementally - ---- - -## [TARGET] Implementation Roadmap - -### Phase 1: Proof of Concept (2-3 days) -- [ ] Research TensorRT C++ API and identify core interfaces needed -- [ ] Create minimal FFI bindings using `bindgen` for TensorRT 10.x -- [ ] Implement basic executor for ONNX → TensorRT → inference -- [ ] Test with simple operation (add, matmul) on NVIDIA GPU -- [ ] Validate FP32 precision works correctly - -### Phase 2: Core Functionality (5-7 days) -- [ ] Expand FFI bindings for full IBuilder/INetworkDefinition API -- [ ] Implement ONNX parser integration -- [ ] Add FP16/INT8 precision support -- [ ] Implement GPU memory management (CUDA buffers) -- [ ] Add error handling and validation -- [ ] Test with 20+ WebNN operations - -### Phase 3: Performance Optimization (3-5 days) -- [ ] Implement engine caching to disk -- [ ] Add engine serialization/deserialization -- [ ] Optimize memory allocation/deallocation -- [ ] Add batch size optimization -- [ ] Profile and benchmark vs ONNX Runtime - -### Phase 4: Python Integration (2-3 days) -- [ ] Add Backend::TensorRT to context selection -- [ ] Implement `compute_tensorrt()` method -- [ ] Add NVIDIA GPU detection -- [ ] Add device selection logic (prefer TensorRT on NVIDIA) -- [ ] Test with Python API examples - -### Phase 5: Documentation & Testing (2-3 days) -- [ ] Update docs/implementation-status.md with TensorRT coverage -- [ ] Update docs/architecture.md with TensorRT backend -- [ ] Create example: `examples/tensorrt_inference.py` -- [ ] Add comprehensive unit tests (Rust + Python) -- [ ] Document CUDA installation requirements -- [ ] Update README.md with TensorRT backend section +## Caches -### Phase 6: Advanced Features (Future) -- [ ] TensorRT for RTX support (Windows 11) -- [ ] INT8 calibration for quantization -- [ ] Dynamic shape support -- [ ] Multi-stream execution -- [ ] DLA (Deep Learning Accelerator) support -- [ ] TensorRT-LLM integration for transformer models +| Category | Location | Content | +|---|---|---| +| `trtx` | `/rustnn/trtx` | Stripped engines, keyed by the graph topology and a hash of the converter sources | +| `trtx-jit` | `/rustnn/trtx-jit` | The global TensorRT runtime cache, serialized after builds | -**Total Estimated Time:** 14-21 days for phases 1-5 +`` is `~/.cache` on Linux, `~/Library/Caches` on macOS and `%LOCALAPPDATA%` on +Windows. Files are zstd compressed and written through a temporary file plus rename. Deleting +either directory is safe; the next build recreates the entries. A stale engine cannot survive a +converter change because the converter source hash is part of the key. ---- +## Environment variables -## Testing Strategy +| Variable | Effect | +|---|---| +| `RUSTNN_TRTX_LOG_VERBOSITY` | TensorRT logger level: `internal_error`, `error`, `warning`, `info`, `verbose` | +| `TRTX_JSON_DUMP_PATH` | Directory that receives a layer JSON dump per built engine | +| `RUST_LOG` | rustnn logging; `debug` shows cache keys and refit details | -### Unit Tests (Rust) +## Testing -**File:** `src/executors/tensorrt.rs` -```rust -#[cfg(all(test, feature = "tensorrt-runtime"))] -mod tests { - use super::*; - - #[test] - fn builds_engine_from_onnx() { - let onnx_model = create_simple_add_onnx(); - let logger = create_logger(); - let builder = create_infer_builder(&logger).unwrap(); - assert!(builder.is_valid()); - } - - #[test] - fn executes_add_operation() { - if !is_nvidia_gpu_available() { - eprintln!("Skipping test: No NVIDIA GPU available"); - return; - } - - let onnx_model = create_simple_add_onnx(); - let inputs = create_test_inputs(); - let outputs = run_tensorrt_with_inputs(&onnx_model, inputs, TensorRTPrecision::FP32).unwrap(); - - assert_eq!(outputs.len(), 1); - assert_eq!(outputs[0].shape, vec![2, 3]); - // Verify output values - } - - #[test] - fn fp16_precision_works() { - // Test FP16 execution - } - - #[test] - fn engine_caching_works() { - // Test cache hit/miss - } -} -``` - -### Python Tests - -**File:** `tests/test_tensorrt_backend.py` -```python -import pytest -import webnn -import numpy as np -import subprocess - -def has_nvidia_gpu(): - """Check if NVIDIA GPU is available""" - try: - result = subprocess.run(["nvidia-smi"], capture_output=True) - return result.returncode == 0 - except FileNotFoundError: - return False - -def has_tensorrt_runtime(): - """Check if TensorRT runtime is available""" - try: - import webnn._rustnn as rustnn - return hasattr(rustnn, 'tensorrt_available') - except: - return False - -@pytest.mark.skipif(not has_nvidia_gpu(), reason="No NVIDIA GPU available") -@pytest.mark.skipif(not has_tensorrt_runtime(), reason="TensorRT runtime not available") -def test_tensorrt_add(): - ml = webnn.ML() - context = ml.create_context(accelerated=True, power_preference="high-performance") - - # Should select TensorRT on NVIDIA GPU - assert context.backend == "tensorrt" - - builder = context.create_graph_builder() - x = builder.input("x", [2, 3], "float32") - y = builder.input("y", [2, 3], "float32") - z = builder.add(x, y) - - graph = builder.build({"output": z}) - - inputs = { - "x": np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32), - "y": np.array([[1, 1, 1], [2, 2, 2]], dtype=np.float32), - } - - outputs = context.compute(graph, inputs) - expected = np.array([[2, 3, 4], [6, 7, 8]], dtype=np.float32) - np.testing.assert_allclose(outputs["output"], expected) - -@pytest.mark.skipif(not has_nvidia_gpu(), reason="No NVIDIA GPU available") -def test_tensorrt_fp16_precision(): - # Test FP16 execution - pass - -@pytest.mark.skipif(not has_nvidia_gpu(), reason="No NVIDIA GPU available") -def test_tensorrt_mobilenet(): - # Test full MobileNetV2 model on TensorRT - pass -``` - -### Performance Benchmarks - -**File:** `benchmarks/tensorrt_vs_onnx.py` -```python -import time -import webnn -import numpy as np - -def benchmark_backend(backend_name, accelerated, power_preference): - ml = webnn.ML() - context = ml.create_context(accelerated=accelerated, power_preference=power_preference) - - # Build MobileNetV2 graph - graph = build_mobilenetv2(context) - - # Warmup - for _ in range(5): - context.compute(graph, inputs) - - # Benchmark - times = [] - for _ in range(100): - start = time.perf_counter() - outputs = context.compute(graph, inputs) - times.append(time.perf_counter() - start) - - return { - "backend": backend_name, - "mean_ms": np.mean(times) * 1000, - "std_ms": np.std(times) * 1000, - "min_ms": np.min(times) * 1000, - "max_ms": np.max(times) * 1000, - } - -# Compare backends -onnx_gpu = benchmark_backend("ONNX GPU", True, "high-performance") -tensorrt = benchmark_backend("TensorRT", True, "high-performance") - -print(f"ONNX GPU: {onnx_gpu['mean_ms']:.2f}ms ± {onnx_gpu['std_ms']:.2f}ms") -print(f"TensorRT: {tensorrt['mean_ms']:.2f}ms ± {tensorrt['std_ms']:.2f}ms") -print(f"Speedup: {onnx_gpu['mean_ms'] / tensorrt['mean_ms']:.2f}x") +```bash +make test-wpt-trtx # WPT suite, requires a GPU +WPT_BACKEND=trtx make test-wpt-op OP=conv2d # one operation +cargo test --test test_trtx_execution --features trtx-runtime +make wpt-sync-trtx # regenerate PASS snapshots after converter changes +cargo check -F trtx-runtime --all-targets # what CI runs ``` -### Makefile Targets - -```makefile -# Add to Makefile -.PHONY: tensorrt-dev -tensorrt-dev: - maturin develop --features python,tensorrt-runtime - -.PHONY: test-tensorrt -test-tensorrt: - cargo test --features tensorrt-runtime - pytest tests/test_tensorrt_backend.py -v - -.PHONY: benchmark-tensorrt -benchmark-tensorrt: - python benchmarks/tensorrt_vs_onnx.py -``` - ---- - -## References - -### TensorRT Resources -- [TensorRT Documentation](https://docs.nvidia.com/deeplearning/tensorrt/latest/index.html) -- [TensorRT SDK](https://developer.nvidia.com/tensorrt) -- [TensorRT Architecture Overview](https://docs.nvidia.com/deeplearning/tensorrt/latest/architecture/architecture-overview.html) -- [TensorRT for RTX (Windows 11)](https://docs.nvidia.com/deeplearning/tensorrt-rtx/latest/index.html) -- [TensorRT for RTX Announcement](https://developer.nvidia.com/blog/nvidia-tensorrt-for-rtx-introduces-an-optimized-inference-ai-library-on-windows/) -- [Run High-Performance AI with TensorRT for RTX](https://developer.nvidia.com/blog/run-high-performance-ai-applications-with-nvidia-tensorrt-for-rtx/) - -### ONNX-TensorRT -- [ONNX-TensorRT GitHub](https://github.com/onnx/onnx-tensorrt) -- [Supported ONNX Operators](https://github.com/onnx/onnx-tensorrt/blob/main/docs/operators.md) -- [TensorRT Support Matrix](https://docs.nvidia.com/deeplearning/tensorrt/latest/getting-started/support-matrix.html) - -### Rust Bindings -- [tensorrt-rs (GitHub)](https://github.com/mstallmo/tensorrt-rs) -- [tensorrt-rs (crates.io)](https://crates.io/crates/tensorrt-rs) -- [easy-tensorrt-sys (crates.io)](https://crates.io/crates/easy-tensorrt-sys) -- [TensorRT-sys](https://lib.rs/crates/tensorrt-sys) - -### WebNN Spec -- [W3C WebNN API Specification](https://www.w3.org/TR/webnn/) -- [WebNN Device Selection Explainer](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md) - -### Related Projects -- [TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) -- [NVIDIA Triton Inference Server](https://github.com/triton-inference-server/server) -- [Torch-TensorRT](https://github.com/pytorch/TensorRT) - ---- - -## Summary - -**TensorRT Integration Value:** -- [OK] **Best GPU performance** on NVIDIA hardware (RTX, A100, H100) -- [OK] **Advanced quantization** (FP16, INT8, FP8, FP4) -- [OK] **Production-ready** (widely deployed in NVIDIA ecosystem) -- [OK] **ONNX-native** (reuse existing ONNX converter) -- [OK] **95%+ operation coverage** (300+ ONNX ops) -- [OK] **TensorRT for RTX** (optimized for Windows 11 + RTX GPUs) - -**Key Design Decisions:** -1. **Reuse ONNX converter** (no new converter needed!) -2. **Custom FFI bindings** for TensorRT 10.x C++ API -3. **Engine caching** to avoid rebuild overhead -4. **FP16 default** for 2x speedup over FP32 -5. **Prefer TensorRT** on NVIDIA GPUs with `accelerated=True` + `power="high-performance"` -6. **Graceful fallback** to ONNX Runtime if TensorRT unavailable - -**Platform Support:** -- **Primary**: Linux + NVIDIA GPU (CUDA) -- **Secondary**: Windows 11 + NVIDIA RTX GPU (TensorRT for RTX) -- **Not supported**: macOS (no NVIDIA GPU), AMD GPUs - -**Next Steps:** -1. Create FFI bindings for TensorRT 10.x -2. Implement basic executor with FP32 support -3. Add FP16/INT8 precision modes -4. Implement engine caching -5. Integrate with Python API -6. Benchmark vs ONNX Runtime GPU - ---- +There is no GPU runner in CI. The TensorRT PASS snapshots under `tests/snapshots/` are +regenerated by contributors with a GPU and reviewed in the pull request. Debug builds compile an +engine per WPT trial and take tens of minutes for the full suite; use `make test-wpt-op` while +iterating. -**Status:** Planning document (not yet implemented) +## Limitations -**Estimated Effort:** 14-21 days for full integration with caching and FP16/INT8 support +- The `shape` extension operation is not lowered. +- Data type restrictions of TensorRT (for example integer element-wise operations) are bridged + with casts in the converter; cases that still fail show up as missing PASS snapshots and on the + WPT dashboard. +- Engines are specific to the GPU architecture; the cache is per machine. diff --git a/docs/integration/windows-tensorrt-setup.md b/docs/integration/windows-tensorrt-setup.md index e1e05280..c7fce7ef 100644 --- a/docs/integration/windows-tensorrt-setup.md +++ b/docs/integration/windows-tensorrt-setup.md @@ -1,396 +1,72 @@ -# Windows Setup Guide: rustnn with TensorRT +# Windows TensorRT-RTX Setup -This guide provides step-by-step instructions for setting up rustnn with TensorRT support on Windows for high-performance GPU inference. +Steps to build and run rustnn with the TensorRT-RTX backend on Windows 10 or 11 (x64). -## Overview +## Install -When properly configured, rustnn will automatically use TensorRT as the highest-priority backend for accelerated execution on NVIDIA GPUs, providing significantly better performance than CPU or standard ONNX Runtime execution. +1. An NVIDIA driver for an RTX GPU. `nvidia-smi` must list the device. +2. The TensorRT-RTX 1.6 SDK from https://developer.nvidia.com/tensorrt-rtx (NVIDIA developer + account required). Extract it, for example to `C:\TensorRT-RTX-1.6`, and add its `bin` + directory to `PATH`: the `trtx` crate loads `tensorrt_rtx_1_6.dll` by name. Alternatively + call `rustnn::executors::trtx::dynamically_load_tensorrt(Some(path))` at startup. +3. Visual Studio Build Tools with the "Desktop development with C++" workload (linker and + Windows SDK). +4. LLVM for libclang, which autocxx uses while building `trtx-sys`: `winget install LLVM.LLVM`, + and `LIBCLANG_PATH=C:\Program Files\LLVM\bin` when cargo cannot find it. +5. `protoc`: `winget install Google.Protobuf`. +6. Rust from https://rustup.rs; `rust-toolchain.toml` selects the pinned toolchain. +7. `git config --system core.longpaths true` before cloning; paths exceed 260 characters. +8. Optional: Node.js for the WPT tests, and ONNX Runtime for the `onnx-runtime` feature + (`make onnxruntime-download`, then `ORT_DYLIB_PATH` as in [Getting Started](../user-guide/getting-started.md)). + Do not rely on the `onnxruntime.dll` in `System32`; it is too old for the `ort` crate. -## Prerequisites +The CUDA toolkit is not required for running: the CUDA driver API is loaded at run time. -### Hardware Requirements -- NVIDIA GPU with compute capability 7.0 or higher - - Recommended: T4, RTX 20/30/40 series, A10, A100 - - Minimum: GTX 1080, Quadro P4000 -- 8GB+ system RAM -- 20GB+ free disk space for dependencies - -### Software Requirements -- Windows 10 (64-bit) or Windows 11 -- Administrator access for installation - -## Installation Steps - -### Step 1: Install NVIDIA GPU Driver - -1. Check your current driver version: - ```powershell - nvidia-smi - ``` - If this command works, you already have drivers installed. - -2. Download the latest driver: - - Visit [NVIDIA Driver Downloads](https://www.nvidia.com/Download/index.aspx) - - Select your GPU model - - Download and run the installer - -3. Reboot your system after installation - -4. Verify installation: - ```powershell - nvidia-smi - ``` - You should see your GPU information displayed. - -### Step 2: Install CUDA Toolkit - -TensorRT requires the CUDA runtime libraries. - -1. Download CUDA Toolkit: - - Visit [NVIDIA CUDA Toolkit Downloads](https://developer.nvidia.com/cuda-downloads) - - Select Windows → x86_64 → your Windows version - - Download the installer (network or local installer) - - Recommended version: CUDA 12.x (check TensorRT-RTX compatibility) - -2. Run the installer: - - Choose "Custom Installation" - - At minimum, select: - - CUDA Toolkit - - CUDA Runtime Libraries - - CUDA Development Libraries (if you plan to build from source) - - Install to default location: `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.x` - -3. Verify installation: - ```powershell - nvcc --version - ``` - You should see CUDA compiler version information. - -4. Verify environment variable (automatically set by installer): - ```powershell - echo $env:CUDA_PATH - ``` - Should output: `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.x` - -### Step 3: Install TensorRT-RTX - -1. Download TensorRT-RTX: - - Visit [NVIDIA Developer TensorRT Downloads](https://developer.nvidia.com/tensorrt) - - You may need to create a free NVIDIA Developer account - - Download TensorRT-RTX for Windows (zip archive) - - Choose the version compatible with your CUDA installation - -2. Extract TensorRT-RTX: - - Extract the zip file to a permanent location - - Recommended: `C:\TensorRT-RTX` - - The directory structure should look like: - ``` - C:\TensorRT-RTX\ - ├── bin\ - ├── include\ - ├── lib\ - └── doc\ - ``` - -3. Set environment variable: - ```powershell - # Run PowerShell as Administrator - [System.Environment]::SetEnvironmentVariable('TENSORRT_RTX_DIR', 'C:\TensorRT-RTX', 'Machine') - ``` - -4. Add TensorRT to PATH: - ```powershell - # Run PowerShell as Administrator - $oldPath = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') - $newPath = "$oldPath;C:\TensorRT-RTX\lib" - [System.Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine') - ``` - -5. Restart your terminal or reboot for changes to take effect - -6. Verify installation: - ```powershell - dir $env:TENSORRT_RTX_DIR\include - dir $env:TENSORRT_RTX_DIR\lib - ``` - You should see TensorRT header files and library files. - -### Step 4: Install Rust Toolchain - -1. Download Rust: - - Visit [rustup.rs](https://rustup.rs/) - - Download and run `rustup-init.exe` - -2. Install with default settings: - - Choose option 1 (default installation) - - This installs: - - Rust compiler (rustc) - - Cargo package manager - - Standard library - -3. Verify installation: - ```powershell - rustc --version - cargo --version - ``` - -4. Install Visual Studio Build Tools (required for linking): - - Download [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/) - - Install "Desktop development with C++" - - Or use full Visual Studio 2019/2022 with C++ workload - -### Step 5: Install Python (for Python bindings) - -If you plan to use rustnn from Python: - -1. Download Python 3.8 or later: - - Visit [python.org](https://www.python.org/downloads/) - - Download Windows installer (64-bit) - -2. Install Python: - - Check "Add Python to PATH" during installation - - Choose "Install for all users" (recommended) - -3. Verify installation: - ```powershell - python --version - pip --version - ``` - -### Step 6: Build rustnn with TensorRT Support - -1. Clone the rustnn repository: - ```powershell - git clone https://github.com/tarekziade/rustnn.git - cd rustnn - ``` - -2. Build Rust library with TensorRT: - ```powershell - # Build with TensorRT support - cargo build --release --features trtx-runtime - ``` - - This will: - - Download and compile dependencies - - Link against TensorRT-RTX libraries - - Create optimized release build - - Take 5-15 minutes on first build - -3. Run tests to verify: - ```powershell - cargo test --lib --features trtx-runtime - ``` - -4. Build Python package (if using Python bindings): - ```powershell - # Install maturin - pip install maturin - - # Build Python wheel with TensorRT support - maturin build --release --features "python,trtx-runtime" - - # Install the wheel - pip install target/wheels/rustnn-*.whl - ``` - -### Step 7: Verify TensorRT Integration - -1. Create a test Python script (`test_trt.py`): - ```python - import webnn - import numpy as np - - # Create context - should select TensorRT backend - ml = webnn.ML() - context = ml.create_context( - power_preference="high-performance", - accelerated=True - ) - - print(f"Backend selected: {context.accelerated}") - print("TensorRT backend is active!" if context.accelerated else "Fallback backend") - - # Create a simple graph - builder = context.create_graph_builder() - x = builder.input("x", [2, 3], "float32") - y = builder.relu(x) - graph = builder.build({"output": y}) - - # Execute - inputs = {"x": np.array([[-1, 2, -3], [4, -5, 6]], dtype=np.float32)} - outputs = context.compute(graph, inputs) - - print("Output:", outputs["output"]) - print("Success! TensorRT is working.") - ``` - -2. Run the test: - ```powershell - python test_trt.py - ``` - -3. Expected output: - ``` - Backend selected: True - TensorRT backend is active! - Output: [[0. 2. 0.] - [4. 0. 6.]] - Success! TensorRT is working. - ``` - -## Troubleshooting - -### Build Errors - -**Error: "Cannot find TensorRT headers"** -``` -Solution: -1. Verify TENSORRT_RTX_DIR is set: echo $env:TENSORRT_RTX_DIR -2. Check the directory exists and contains include/ folder -3. Restart terminal after setting environment variables -``` - -**Error: "Linking error: cannot find -lnvinfer_10"** -``` -Solution: -1. Verify TensorRT lib directory is in PATH -2. Check lib files exist: dir $env:TENSORRT_RTX_DIR\lib -3. Ensure you downloaded the correct Windows version of TensorRT-RTX -4. Try adding to PATH manually: - $env:PATH += ";C:\TensorRT-RTX\lib" -``` - -**Error: "CUDA not found"** -``` -Solution: -1. Verify CUDA_PATH is set: echo $env:CUDA_PATH -2. Run: nvcc --version (should work) -3. Reinstall CUDA Toolkit if necessary -``` - -### Runtime Errors - -**Error: "TensorRT execution failed: CUDA initialization failed"** -``` -Solution: -1. Check GPU is accessible: nvidia-smi -2. Update GPU drivers to latest version -3. Ensure no other process is using the GPU exclusively -4. Restart your computer -``` - -**Error: "DLL not found" when running Python** -``` -Solution: -1. Ensure TensorRT lib directory is in PATH -2. Copy required DLLs to Python script directory: - - nvinfer_10.dll - - nvonnxparser_10.dll - - cudart64_12.dll (or your CUDA version) -3. Or add to PATH for current session: - $env:PATH += ";C:\TensorRT-RTX\lib;$env:CUDA_PATH\bin" -``` - -**Backend falls back to ONNX instead of TensorRT** -``` -Solution: -1. Verify you built with trtx-runtime feature: - cargo build --features trtx-runtime -2. Check Python package includes TensorRT: - pip show rustnn (should list trtx in dependencies) -3. Rebuild Python package with correct features: - maturin develop --features "python,trtx-runtime" -``` - -### Performance Issues - -**TensorRT is slower than expected** -``` -Tips: -1. TensorRT optimizes on first run (engine building) - - First inference may take 10-60 seconds - - Subsequent runs should be much faster -2. Use larger batch sizes when possible -3. Ensure GPU has adequate cooling (check temps with nvidia-smi) -4. Close other GPU-intensive applications -``` - -## Development Without TensorRT (Mock Mode) - -If you want to develop on a machine without an NVIDIA GPU, you can use mock mode: +## Build ```powershell -# Build with mock feature -cargo build --features trtx-runtime-mock - -# Run tests with mock -cargo test --lib --features trtx-runtime-mock - -# Build Python package with mock -maturin develop --features "python,trtx-runtime-mock" +cargo check -F trtx-runtime --all-targets +cargo build --release --features trtx-runtime ``` -Mock mode: -- Compiles and runs without GPU -- Useful for development and CI/CD -- Does NOT perform actual inference -- Returns dummy results - -## Environment Variable Summary - -For quick reference, here are all the environment variables you need: +## Verify ```powershell -# Run as Administrator -[System.Environment]::SetEnvironmentVariable('CUDA_PATH', 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.x', 'Machine') -[System.Environment]::SetEnvironmentVariable('TENSORRT_RTX_DIR', 'C:\TensorRT-RTX', 'Machine') - -# Add to PATH -$oldPath = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') -$newPath = "$oldPath;C:\TensorRT-RTX\lib;$env:CUDA_PATH\bin" -[System.Environment]::SetEnvironmentVariable('Path', $newPath, 'Machine') +$env:RUST_LOG = "info" +cargo run --release --features trtx-runtime,onnx-runtime -- examples\sample_graph.webnn --convert onnx --run-trtx ``` -After setting these, restart your terminal or reboot. - -## Performance Expectations - -With TensorRT properly configured, you should see: +The log lists the CUDA devices found and `Backend selected: Trtx { cuda_device_idx: 0 }`. In an +application, `MLContextOptions::new(MLPowerPreference::HighPerformance, true)` selects +TensorRT-RTX when the feature is enabled; `context.rustnn_backend()` confirms it. -| Operation | CPU (ONNX) | GPU (ONNX) | GPU (TensorRT) | -|-----------|------------|------------|----------------| -| Small models (<10 ops) | ~10ms | ~5ms | ~2ms | -| Medium models (10-100 ops) | ~100ms | ~20ms | ~5ms | -| Large models (>100 ops) | ~1000ms | ~100ms | ~20ms | +## WPT conformance -Note: First-run times include engine building overhead (10-60 seconds). - -## Next Steps - -Once TensorRT is working: - -1. Explore examples in `examples/` directory -2. Read the [API Reference](../user-guide/api-reference.md) for detailed usage -3. Check [Implementation Status](../development/implementation-status.md) for supported operations -4. See [Development Guide](../development/setup.md) for contributing +```bash +# Git Bash +export PATH="/c/TensorRT-RTX-1.6/bin:$PATH" +export ORT_DYLIB_PATH=/c/git/rustnn/target/onnxruntime/onnxruntime-win-x64-1.29.0/lib/onnxruntime.dll +make test-wpt-trtx +WPT_BACKEND=trtx make test-wpt-op OP=gemm +``` -## Additional Resources +Set `ORT_DYLIB_PATH` even for TensorRT runs: the harness probes the ONNX Runtime backend too, +and a wrong DLL poisons the ONNX Runtime initialization for the rest of the process. Test output +is fully buffered when redirected to a file; watch the console or wait for the process to exit. -- [TensorRT Documentation](https://docs.nvidia.com/deeplearning/tensorrt/) -- [CUDA Programming Guide](https://docs.nvidia.com/cuda/) -- [rustnn Python API Reference](../user-guide/api-reference.md) -- [trtx-rs GitHub](https://github.com/tarekziade/trtx-rs) +## Troubleshooting -## Support +| Symptom | Cause and fix | +|---|---| +| `LoadLibraryExW` error 126 when creating a context; TensorRT not selected | `tensorrt_rtx_1_6.dll` is not on `PATH`. Add the SDK `bin` directory or load the library explicitly | +| `Failed to load ONNX Runtime dylib: BadVersion { version_str: "1.17.1" }` | `ORT_DYLIB_PATH` is unset and the `System32` DLL was picked up. Point it at the downloaded release | +| libclang not found while compiling `trtx-sys` | Install LLVM and set `LIBCLANG_PATH` | +| Results look stale after a converter change | Delete the engine caches `%LOCALAPPDATA%\rustnn\trtx` and `%LOCALAPPDATA%\rustnn\trtx-jit` | +| `Access is denied` from `cargo fmt` or the linker | A test binary is still running; wait for it to finish | +| Path too long errors during clone or build | `git config --system core.longpaths true`, or clone to a short path such as `C:\git` | -If you encounter issues not covered in this guide: +## Without a GPU -1. Check existing [GitHub Issues](https://github.com/tarekziade/rustnn/issues) -2. Create a new issue with: - - Your Windows version - - GPU model (from nvidia-smi) - - CUDA version (from nvcc --version) - - TensorRT version - - Full error message and stack trace - - Steps to reproduce +`cargo check -F trtx-runtime --all-targets` compiles the backend and is what CI runs. The +`trtx-runtime-mock` feature builds the converter against the mock `trtx` API for API-level +checks; the execution tests are excluded in mock mode. diff --git a/docs/reference/ipc-design.md b/docs/reference/ipc-design.md index 0b114330..b96aa22f 100644 --- a/docs/reference/ipc-design.md +++ b/docs/reference/ipc-design.md @@ -8,29 +8,33 @@ This document outlines the design considerations for adding Inter-Process Commun ### Intermediate Representation -**Format:** Rust structs with JSON attributes +**Format:** a strongly typed Rust enum (`rustnn::operators::Operation`, `src/operators.rs`) +with one variant per WebNN operation, named operand indices and a typed options struct: + ```rust -pub struct Operation { - pub op_type: String, // e.g., "conv2d" - pub input_operands: Vec, // operand IDs - pub output_operand: Option, - pub attributes: serde_json::Value, // Flexible JSON - pub label: Option, +pub enum Operation { + Conv2d { + input: OperandIndex, + filter: OperandIndex, + options: Option, // bias is an Option inside the options + outputs: Vec, + }, + // one variant per operation } ``` +The enum replaced an earlier `op_type: String` plus `serde_json::Value` attributes design. It +serializes with serde (the `webnn-graph` JSON and `.webnn` text formats), so graphs can be saved +and loaded, and attribute names are checked at compile time. + **Benefits:** -- Simple: no code generation -- Flexible: easy to add operations -- Debuggable: human-readable JSON -- Serializable: can save/load graphs -- Cross-language: works with Python/Rust/CLI +- Compile-time checking of operand wiring and attribute names across all converters +- Serializable: graphs round-trip through the `.webnn` text and JSON formats +- Debuggable: `.webnn` text is human-readable **Limitations for IPC:** -- JSON parsing overhead on every access -- No structured validation at serialization boundaries -- String-based keys prone to typos -- Runtime-only validation +- Serde JSON is the only serialization; no schema shared with other languages +- No zero-copy access to constant data across a process boundary ## Chromium's Architecture (Multi-Process) @@ -289,7 +293,7 @@ impl GraphClient { ### Option A: Separate Service Process (Chromium-like) ``` -Client Process (Python/Rust) +Client Process (Rust, or bindings such as pywebnn) ↓ Cap'n Proto IPC Service Process (Rust WebNN) ↓ Direct FFI @@ -310,7 +314,7 @@ Backend (ONNX Runtime / CoreML / TensorRT) ### Option B: Worker Thread Pool (Simpler) ``` -Main Thread (Python/Rust) +Main Thread (Rust, or bindings such as pywebnn) ↓ Channel/Queue Worker Thread Pool ↓ Direct calls @@ -366,7 +370,7 @@ When adding IPC support: - [ ] Add authentication/security (if multi-user) - [ ] Add resource limits and quotas - [ ] Test serialization performance vs JSON -- [ ] Update Python bindings to support IPC mode +- [ ] Update downstream bindings (pywebnn) to support IPC mode - [ ] Add IPC mode examples - [ ] Document IPC setup and usage @@ -440,7 +444,7 @@ If implementing IPC for multi-user scenarios: 2. **Design Cap'n Proto schema** for WebNN operations 3. **Implement parallel format support** (keep JSON, add Cap'n Proto) 4. **Add IPC transport layer** (Unix sockets for POSIX, named pipes for Windows) -5. **Update Python bindings** to support IPC mode +5. **Update downstream bindings** (pywebnn) to support IPC mode 6. **Add service/client examples** 7. **Document migration path** for users 8. **Consider WebAssembly** integration (WASI sockets) diff --git a/docs/requirements.txt b/docs/requirements.txt index 6e91e85f..7ebb083f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,3 @@ mkdocs>=1.5.0 mkdocs-material>=9.0.0 -mkdocstrings[python]>=0.24.0 pymdown-extensions>=10.0 diff --git a/docs/testing/wpt-test-guide.md b/docs/testing/wpt-test-guide.md index 3a022c9d..88dcbe9f 100644 --- a/docs/testing/wpt-test-guide.md +++ b/docs/testing/wpt-test-guide.md @@ -9,8 +9,12 @@ The in-repo WPT harness runs upstream [WebNN conformance tests](https://github.c - **Node.js** on `PATH` (used by `scripts/wpt_bridge/dump_corpus.mjs`) - **WPT corpus** in `.cache/wpt` (fetched automatically on first run, or manually via `make fetch-wpt`) -- **ONNX Runtime** for the default `onnx` backend (`make onnxruntime-download` is a dependency of `make test-wpt`) -- **TensorRT** (optional) for the `trtx` backend — requires `trtx-runtime` feature and a working GPU; unavailable backends are skipped at startup +- **ONNX Runtime** for the default `onnx` backend (`make onnxruntime-download` is a dependency of `make test-wpt`; set `ORT_DYLIB_PATH` when running cargo directly) +- **TensorRT-RTX** (optional) for the `trtx` backend: `trtx-runtime` feature and an NVIDIA GPU +- **LiteRT** (optional) for the `litert` backend: `litert-runtime` feature and `flatc` at build time; `make test-wpt-litert` sets the library path +- **CoreML** (optional, macOS) for the `coreml` backend: `coreml-runtime` feature + +Unavailable backends are skipped at startup with a log message. ## Quick start @@ -26,6 +30,10 @@ make test-wpt-op OP=relu # Full suite on TensorRT (skips if GPU unavailable) make test-wpt-trtx + +# LiteRT, and CoreML on macOS +make test-wpt-litert +make test-wpt-coreml ``` Always use `--test-threads 1` for WPT runs. Parallel execution is not validated for `MLContext` thread safety. @@ -67,8 +75,11 @@ trtx::clamp::clamp_uint64_1D_tensor_with_bigint_max | `make fetch-wpt` | Download/update WPT corpus into `.cache/wpt` | | `make test-wpt` | Full suite, ONNX CPU backend | | `make test-wpt-trtx` | Full suite, TensorRT backend | -| `make test-wpt-op OP=` | Filter trials by operation (e.g. `OP=add`, `OP=dequantize`) | -| `make test-wpt-report` | Full ONNX run; writes JSON/HTML report even on failures | +| `make test-wpt-litert` | Full suite, LiteRT backend | +| `make test-wpt-coreml` | Full suite, CoreML backend (macOS); `make test-wpt-coreml-report` also writes the JSON report | +| `make test-wpt-op OP=` | Filter trials by operation (e.g. `OP=add`, `OP=dequantize`); `WPT_BACKEND=` selects the backend | +| `make test-wpt-report` | Full run with JSON/HTML reports even on failures; `WPT_BACKEND=onnx|trtx|litert|coreml` picks the backend | +| `make wpt-sync-onnx`, `wpt-sync-litert`, `wpt-sync-coreml`, `wpt-sync-trtx` | Regenerate PASS snapshots and expected-failure lists against the pinned corpus | Equivalent `cargo` invocations: @@ -90,10 +101,12 @@ Set `WPT_BACKEND` to limit which backends register trials: | Value | Backend | Notes | |-------|---------|-------| -| `onnx` (default when unset) | ONNX Runtime CPU | `MLPowerPreference::Default`, `accelerated=false` | -| `trtx` | TensorRT | `MLPowerPreference::HighPerformance`, `accelerated=true` | +| `onnx` | ONNX Runtime CPU | `MLPowerPreference::Default`, `accelerated=false` | +| `trtx` | TensorRT-RTX | `MLPowerPreference::HighPerformance`, `accelerated=true`; requires the `trtx-runtime` feature | +| `litert` | LiteRT | requires the `litert-runtime` feature | +| `coreml` | CoreML | macOS, requires the `coreml-runtime` feature | -Aliases: `ort`, `cpu`, `tensorrt`, `trt` are also accepted. +Aliases: `ort`, `cpu`, `onnx-cpu`, `ort-cpu` (onnx); `tensorrt`, `trt` (trtx); `tflite` (litert); `core-ml`, `mlprogram` (coreml). When `WPT_BACKEND` is unset, all **available** backends register trials. Unavailable backends (e.g. TRTX without a GPU) are skipped with a log message — they do not count as skips in the summary. @@ -102,7 +115,7 @@ When `WPT_BACKEND` is unset, all **available** backends register trials. Unavail | Variable | Default | Description | |----------|---------|-------------| | `WPT_DIR` | `.cache/wpt` | Path to WPT checkout | -| `WPT_BACKEND` | (all available) | Limit backend: `onnx` or `trtx` | +| `WPT_BACKEND` | (all available) | Limit backend: `onnx`, `trtx`, `litert` or `coreml` | | `WPT_REPORT_JSON` | (none; `reports/wpt-conformance.json` when `CI` is set) | Write structured pass/fail JSON report | | `WPT_REPORT_HTML` | (derived from JSON path) | HTML report path; set to empty string to disable | | `WPT_AUDIT` | (off) | Enable per-pass error metrics collection (see [Audit mode](#audit-mode)) | @@ -228,6 +241,8 @@ A scheduled workflow (`.github/workflows/snapshot-sync.yml`) runs these and open **TRTX backend skipped** — TensorRT is not available (no GPU, missing drivers, or `trtx-runtime` feature not enabled). Only `onnx` trials will run. +**`No WPT backends available`** (exit code 2) — a backend feature is compiled in but none of its runtimes could be created; check `ORT_DYLIB_PATH` or the TensorRT library. A build without any backend feature (plain `cargo test`) prints a notice and exits successfully instead, so the harness does not break the default test run. + **Parse warnings (`file_errors`)** — Some WPT files may fail to parse; warnings are logged but do not fail the run. **Slow TRTX runs** — TensorRT engine compilation per trial makes debug builds slow (~25–30 min for 2482 cases). Use `make test-wpt-op OP=` to iterate on a single operation. diff --git a/docs/user-guide/advanced.md b/docs/user-guide/advanced.md index 8c698204..adf536cf 100644 --- a/docs/user-guide/advanced.md +++ b/docs/user-guide/advanced.md @@ -1,517 +1,131 @@ # Advanced Topics -Advanced usage patterns and best practices for the WebNN Python API. +## Backend hints and options -## Performance Optimization +`MLContextOptions` carries the two WebNN hints and three rustnn extensions: -### Graph Compilation +```rust +use rustnn::mlcontext::{ + Backend, BackendDevice, MLContext, MLContextOptions, MLPowerPreference, RustNNOptions, +}; -Compile graphs once and reuse them: +// Restrict selection to one backend; fails if it cannot serve the hints. +let options = MLContextOptions::new(MLPowerPreference::HighPerformance, true) + .with_rustnn_backend_hint(Backend::Trtx); -```python -import webnn +// Use exactly this device: no availability check, no fallback. +let options = MLContextOptions::new(MLPowerPreference::Default, true) + .with_rustnn_device_hint(BackendDevice::Trtx { cuda_device_idx: 1 }); -class ModelCache: - def __init__(self): - self.ml = webnn.ML() - self.context = self.ml.create_context() - self.graphs = {} - - def get_or_build_graph(self, name, builder_fn): - """Cache compiled graphs for reuse.""" - if name not in self.graphs: - builder = self.context.create_graph_builder() - output = builder_fn(builder) - self.graphs[name] = builder.build({name: output}) - return self.graphs[name] - -# Usage -cache = ModelCache() - -def build_relu(builder): - x = builder.input("x", [100], "float32") - return builder.relu(x) - -# First call: compiles the graph -graph1 = cache.get_or_build_graph("relu", build_relu) - -# Second call: returns cached graph (fast!) -graph2 = cache.get_or_build_graph("relu", build_relu) -assert graph1 is graph2 -``` - -### Memory-Efficient Constants - -For large constant tensors, use the most memory-efficient data type: - -```python -import webnn -import numpy as np - -ml = webnn.ML() -context = ml.create_context() -builder = context.create_graph_builder() - -# Use float16 instead of float32 to halve memory usage -large_weights = np.random.randn(1000, 1000).astype('float16') -weights_op = builder.constant(large_weights) - -print(f"Memory saved: {large_weights.nbytes / 1024 / 1024:.2f} MB vs " - f"{(large_weights.nbytes * 2) / 1024 / 1024:.2f} MB for float32") -``` - -## Integration with Other Libraries - -### NumPy Integration with Execution - -Seamless conversion between NumPy and WebNN: - -```python -import webnn -import numpy as np - -ml = webnn.ML() -context = ml.create_context(accelerated=False) -builder = context.create_graph_builder() - -# Build a simple matmul with NumPy weights -x = builder.input("x", [1, 100], "float32") -weights = np.random.randn(100, 50).astype('float32') * 0.01 -bias = np.zeros(50, dtype='float32') - -w_op = builder.constant(weights) -b_op = builder.constant(bias) - -output = builder.add(builder.matmul(x, w_op), b_op) -graph = builder.build({"output": output}) - -# Execute with NumPy input -x_data = np.random.randn(1, 100).astype('float32') -results = context.compute(graph, {"x": x_data}) - -print(f"Input shape: {x_data.shape}") -print(f"Output shape: {results['output'].shape}") -print(f"Result is NumPy array: {isinstance(results['output'], np.ndarray)}") -``` - -### ONNX Integration - -Load existing ONNX models and convert them: - -```python -import webnn -import numpy as np -# Note: This is a conceptual example. Full ONNX loading -# would require parsing the ONNX protobuf format. - -def load_onnx_weights(onnx_path): - """ - Conceptual example of loading ONNX weights. - In practice, you'd use onnx.load() to parse the model. - """ - # This is a simplified example - weights = { - 'fc1': np.random.randn(784, 128).astype('float32'), - 'fc1_bias': np.zeros(128, dtype='float32'), - 'fc2': np.random.randn(128, 10).astype('float32'), - 'fc2_bias': np.zeros(10, dtype='float32'), - } - return weights - -def build_from_onnx_weights(weights): - ml = webnn.ML() - context = ml.create_context() - builder = context.create_graph_builder() - - # Build graph using ONNX weights - x = builder.input("input", [1, 784], "float32") - - w1 = builder.constant(weights['fc1']) - b1 = builder.constant(weights['fc1_bias']) - h1 = builder.matmul(x, w1) - h1 = builder.add(h1, b1) - h1 = builder.relu(h1) - - w2 = builder.constant(weights['fc2']) - b2 = builder.constant(weights['fc2_bias']) - output = builder.matmul(h1, w2) - output = builder.add(output, b2) - - return builder.build({"output": output}) - -weights = load_onnx_weights("model.onnx") -graph = build_from_onnx_weights(weights) -``` - -## Graph Introspection and Execution - -Inspect and analyze compiled graphs, then execute them: - -```python -import webnn -import numpy as np - -ml = webnn.ML() -context = ml.create_context(accelerated=False) -builder = context.create_graph_builder() - -# Build a complex graph -x = builder.input("x", [2, 3], "float32") -y = builder.input("y", [3, 4], "float32") -z = builder.matmul(x, y) -w = builder.relu(z) -output = builder.sigmoid(w) - -graph = builder.build({"final": output}) - -# Inspect the graph -print("Graph Analysis:") -print(f" Inputs: {graph.get_input_names()}") -print(f" Outputs: {graph.get_output_names()}") -print(f" Total operands: {graph.operand_count}") -print(f" Total operations: {graph.operation_count}") - -# Execute the graph -x_data = np.random.randn(2, 3).astype('float32') -y_data = np.random.randn(3, 4).astype('float32') -results = context.compute(graph, {"x": x_data, "y": y_data}) - -print(f"\nExecution:") -print(f" Output shape: {results['final'].shape}") -print(f" Output range: [{results['final'].min():.4f}, {results['final'].max():.4f}]") -``` - -## Custom Graph Patterns - -### Residual Connections - -```python -import webnn -import numpy as np - -def residual_block(builder, x, hidden_size): - """Create a residual block: output = relu(x + fc(x))""" - - # Linear transformation - w = builder.constant(np.random.randn(hidden_size, hidden_size).astype('float32') * 0.01) - transformed = builder.matmul(x, w) - - # Add residual connection - residual = builder.add(x, transformed) - - # Activation - output = builder.relu(residual) - - return output - -ml = webnn.ML() -context = ml.create_context() -builder = context.create_graph_builder() - -x = builder.input("x", [1, 128], "float32") -y = residual_block(builder, x, 128) -graph = builder.build({"output": y}) - -context.convert_to_onnx(graph, "residual.onnx") +// Backend tuning. The option structs are `#[non_exhaustive]`: start from `Default` and set fields. +let mut tuning = RustNNOptions::default(); +tuning.trtx.cuda_graphs = false; +tuning.trtx.fail_on_cache_miss = true; // ahead-of-time flows: never build, only load cached engines +let options = MLContextOptions::new(MLPowerPreference::Default, true).with_rustnn_options(tuning); +let context = MLContext::create(&options)?; ``` -### Attention Mechanism (Simplified) - -```python -import webnn -import numpy as np - -def scaled_dot_product_attention(builder, query, key, value, d_k): - """ - Simplified attention mechanism (without softmax for now). - attention = (query @ key.T) @ value - """ - # Transpose key (conceptually) - key_t = key # In practice, you'd need to handle transposition - - # Attention scores: query @ key.T - scores = builder.matmul(query, key_t) - - # Apply scaling factor (as a constant multiply) - scale = 1.0 / np.sqrt(d_k) - scale_tensor = builder.constant(np.full_like(scores, scale)) - scaled_scores = builder.mul(scores, scale_tensor) - - # Attention output: scores @ value - output = builder.matmul(scaled_scores, value) - - return output +`TrtxOptions` has `engine_caching`, `runtime_cache`, `fail_on_cache_miss` and `cuda_graphs`; +the ONNX Runtime, CoreML and LiteRT option structs exist but have no fields yet. + +## Dynamic shapes + +Dynamic dimensions are opt-in through the `dynamic-inputs` Cargo feature. Without it, loading +a graph with a dynamic dimension fails with `GraphError::DynamicInputsFeatureDisabled`. + +- In graph files a dynamic dimension is `{ "name": "seq", "maxSize": 4096 }` (JSON) or the + equivalent `.webnn` text; onnx2webnn exports produce them for symbolic ONNX dimensions. In the + graph model this is `Dimension::Dynamic(DynamicDimension { name, max_size })`. Constants must + stay static. +- Builder descriptors (`MLOperandDescriptor`) are static. Graphs with dynamic inputs come from + files and are compiled with `MLGraphBuilder::build_graph_info`. +- Tensors bound to dynamic inputs or outputs are created with a starting shape, given a + capacity for the largest shape they will take, and resized before each dispatch: + +```rust +let mut mask = context.create_tensor( + &MLTensorDescriptor::new(MLOperandDataType::Int64, vec![1, 1]).to_writable(), +)?; +context.rustnn_set_tensor_capacity(&mut mask, &[1, 4096])?; + +for step in 1..=steps { + context.rustnn_resize_tensor(&mut mask, &[1, step])?; // active shape, within capacity + context.write_tensor(&mask, &mask_values[..step as usize])?; + context.dispatch(&mut graph, &inputs, &outputs)?; +} ``` -## Error Handling Strategies - -### Comprehensive Error Handling - -```python -import webnn -import sys -import traceback - -def safe_graph_export(graph_fn, output_path): - """ - Safely build and export a graph with comprehensive error handling. - """ - try: - ml = webnn.ML() - context = ml.create_context() - builder = context.create_graph_builder() - - # Build the graph - try: - output = graph_fn(builder) - graph = builder.build({"output": output}) - except ValueError as e: - print(f" Graph validation failed: {e}", file=sys.stderr) - traceback.print_exc() - return False - - # Export to ONNX - try: - context.convert_to_onnx(graph, output_path) - print(f"[OK] Successfully exported to {output_path}") - return True - except IOError as e: - print(f" File I/O error: {e}", file=sys.stderr) - return False - except RuntimeError as e: - print(f" Conversion failed: {e}", file=sys.stderr) - return False - - except Exception as e: - print(f" Unexpected error: {e}", file=sys.stderr) - traceback.print_exc() - return False - -# Usage -def my_graph(builder): - x = builder.input("x", [10], "float32") - return builder.relu(x) - -success = safe_graph_export(my_graph, "model.onnx") -sys.exit(0 if success else 1) -``` - -## Testing Graphs - -### Unit Testing WebNN Graphs - -```python -import unittest -import webnn -import numpy as np -import os - -class TestWebNNGraphs(unittest.TestCase): - def setUp(self): - """Set up test fixtures.""" - self.ml = webnn.ML() - self.context = self.ml.create_context() - - def test_simple_relu(self): - """Test ReLU graph creation and export.""" - builder = self.context.create_graph_builder() - x = builder.input("x", [10], "float32") - y = builder.relu(x) - graph = builder.build({"y": y}) - - self.assertEqual(graph.operand_count, 2) - self.assertEqual(graph.operation_count, 1) - self.assertIn("x", graph.get_input_names()) - self.assertIn("y", graph.get_output_names()) - - def test_onnx_export(self): - """Test ONNX export functionality.""" - builder = self.context.create_graph_builder() - x = builder.input("x", [10], "float32") - y = builder.relu(x) - graph = builder.build({"y": y}) - - output_path = "test_model.onnx" - try: - self.context.convert_to_onnx(graph, output_path) - self.assertTrue(os.path.exists(output_path)) - self.assertGreater(os.path.getsize(output_path), 0) - finally: - if os.path.exists(output_path): - os.remove(output_path) - - def test_invalid_shape(self): - """Test that invalid shapes raise errors.""" - builder = self.context.create_graph_builder() - - # This should work - x = builder.input("x", [10, 20], "float32") - - # Empty shape is valid (scalar) - scalar = builder.input("scalar", [], "float32") - - def test_multiple_outputs(self): - """Test graphs with multiple outputs.""" - builder = self.context.create_graph_builder() - x = builder.input("x", [10], "float32") - - y1 = builder.relu(x) - y2 = builder.sigmoid(x) - - graph = builder.build({"relu": y1, "sigmoid": y2}) - - outputs = graph.get_output_names() - self.assertIn("relu", outputs) - self.assertIn("sigmoid", outputs) - -if __name__ == '__main__': - unittest.main() -``` - -## Debugging Tips - -### Verbose Graph Building - -```python -import webnn - -class VerboseBuilder: - """Wrapper that logs all operations.""" - - def __init__(self, context): - self.context = context - self.builder = context.create_graph_builder() - self.op_count = 0 - - def input(self, name, shape, dtype="float32"): - result = self.builder.input(name, shape, dtype) - print(f"[{self.op_count}] INPUT: {name} {shape} {dtype}") - self.op_count += 1 - return result - - def constant(self, value, **kwargs): - result = self.builder.constant(value, **kwargs) - print(f"[{self.op_count}] CONSTANT: shape={value.shape}") - self.op_count += 1 - return result - - def relu(self, x): - result = self.builder.relu(x) - print(f"[{self.op_count}] RELU") - self.op_count += 1 - return result - - def matmul(self, a, b): - result = self.builder.matmul(a, b) - print(f"[{self.op_count}] MATMUL") - self.op_count += 1 - return result - - # Add other operations as needed... - - def build(self, outputs): - print(f"\nBuilding graph with {len(outputs)} output(s)...") - return self.builder.build(outputs) - -# Usage -ml = webnn.ML() -context = ml.create_context() -builder = VerboseBuilder(context) - -x = builder.input("x", [10], "float32") -y = builder.relu(x) -graph = builder.build({"y": y}) -``` - -Output: -``` -[0] INPUT: x [10] float32 -[1] RELU - -Building graph with 1 output(s)... -``` - -## Platform-Specific Features - -### Backend Selection and Execution - -Choose the best backend for your platform and execute models: - -```python -import webnn -import numpy as np -import platform - -ml = webnn.ML() - -# Try GPU/NPU acceleration first -context = ml.create_context(accelerated=True, power_preference="high-performance") -print(f"Platform: {platform.system()}") -print(f"Accelerated: {context.accelerated}") - -# Build a simple graph -builder = context.create_graph_builder() -x = builder.input("x", [10], "float32") -y = builder.relu(x) -graph = builder.build({"y": y}) - -# Execute on selected backend -x_data = np.array([-5, -3, -1, 0, 1, 3, 5, 7, 9, 11], dtype=np.float32) -results = context.compute(graph, {"x": x_data}) - -print(f"Result: {results['y']}") - -# Export for different platforms -context.convert_to_onnx(graph, "model.onnx") -print("[OK] Exported ONNX (cross-platform)") - -if platform.system() == "Darwin": - try: - context.convert_to_coreml(graph, "model.mlmodel") - print("[OK] Exported CoreML (macOS GPU/Neural Engine)") - except Exception as e: - print(f" CoreML export: {e}") -``` - -## Best Practices Summary - -1. **Compile once, reuse**: Cache compiled graphs -2. **Use appropriate data types**: float16 for memory efficiency -3. **Handle errors gracefully**: Wrap operations in try-except blocks -4. **Test thoroughly**: Write unit tests for your graphs -5. **Validate shapes**: Check tensor dimensions before building -6. **Profile performance**: Measure compilation and export times -7. **Document graphs**: Add comments explaining graph structure -8. **Use type hints**: Leverage Python type hints for better IDE support - -```python -from typing import Dict -import webnn -import numpy as np - -def build_classifier( - input_size: int, - hidden_size: int, - num_classes: int -) -> webnn.MLGraph: - """ - Build a simple classifier graph. - - Args: - input_size: Size of input features - hidden_size: Size of hidden layer - num_classes: Number of output classes - - Returns: - Compiled MLGraph ready for export - """ - ml = webnn.ML() - context = ml.create_context() - builder = context.create_graph_builder() - - # Build model... - x = builder.input("input", [1, input_size], "float32") - # ... rest of the model - - return graph -``` +`dispatch` checks the active shapes against the graph's dimension bounds and requires equal +values for dynamic dimensions that share a name. `examples/smollm_mlcontext.rs` runs a KV +cache this way. The checked legacy executors (`run_onnx_with_inputs_checked` and friends) +apply the same rules to one-shot runs; see [Flexible Input Shapes](../development/flexible-input-shapes.md). + +## Saving and exporting graphs + +| Goal | Call | +|---|---| +| Save the graph under construction as `.webnn` text plus `.safetensors` weights | `builder.rustnn_save_webnn(&outputs, "model.webnn")` | +| Inspect the graph so far as `.webnn` text | `builder.rustnn_webnn_text_for_outputs(&outputs)` | +| Get the finished `GraphInfo` without compiling | `builder.finish_graph_info(&outputs)` | +| Convert a `GraphInfo` to ONNX, CoreML, TensorRT engine, TFLite or CANN bytes | `ConverterRegistry::with_defaults().convert("onnx", &graph_info)` | +| Write the ONNX sidecar for large models | `converted.weights_data` into `ONNX_EXTERNAL_WEIGHTS_FILENAME` next to the model | +| Graphviz | `rustnn::graph_to_dot(&graph_info)` or the CLI `--export-dot` | + +`MLGraphBuilder::new_uncompiled()` records without a backend, so conversion tools need no +runtime feature. `Int4` and `Uint4` constants cannot be written to `.safetensors`. + +## Loading external weights + +The loader resolves `@weights(...)` references through the `webnn-graph` crate. It looks next +to the graph file for a `manifest.json` plus `model.weights` pair (onnx2webnn layout) or for the +`.safetensors` file written by `rustnn_save_webnn`. Identifiers that contain `.` or `:` (ONNX +node names) are sanitized to `_` on import. Shape inference runs on import, so a loaded graph +carries complete descriptors. + +## Caching + +The TensorRT-RTX backend caches two things under the platform cache directory +(`~/.cache/rustnn/` on Linux, `~/Library/Caches/rustnn/` on macOS, `%LOCALAPPDATA%\rustnn\` on +Windows): + +| Category | Content | Key | +|---|---|---| +| `trtx` | Serialized engines with stripped, refittable weights | Hash of the graph topology, non-refittable constants and the converter sources | +| `trtx-jit` | The TensorRT runtime (JIT kernel) cache shared by all engines | Global | + +Because weights are refitted after loading, a cached engine serves every model with the same +topology. Entries are zstd compressed and written atomically. Disable caching with +`TrtxOptions::engine_caching` and `TrtxOptions::runtime_cache`; delete the directories to +start cold. Details are in [TensorRT-RTX](../integration/tensorrt.md). + +## Debugging + +| Setting | Effect | +|---|---| +| `RUST_LOG=info` (or `debug`, `trace`) | Library logging through the `log` crate: selected device, cache hits, per-dispatch shapes. Programs need a logger such as `pretty_env_logger` | +| `RUSTNN_DEBUG=1` | Enables the `debug_print!` output of converters | +| `RUSTNN_DEBUG=2` with `RUSTNN_DEBUG_ONNX_DIR=` | Also writes the converted ONNX model of every build for inspection in Netron or ONNX Runtime | +| `RUSTNN_TRTX_LOG_VERBOSITY=verbose` | TensorRT logger level (`internal_error`, `error`, `warning`, `info`, `verbose`) | +| `TRTX_JSON_DUMP_PATH=` | Writes TensorRT engine layer JSON per built engine | +| `builder.rustnn_operand_shape(op)` | Shape of an operand while recording | +| `cargo run --features onnx-runtime -- graph.webnn --export-dot graph.dot` | Graph structure as Graphviz | +| `make test-wpt-op OP=` | Runs the WPT conformance cases of one operation, printing expected and actual values on failure | + +## Threads + +`MLContext` is `Send + Sync`. `dispatch`, `write_tensor` and `read_tensor` take `&mut self`, +so concurrent use goes through a `Mutex`; the WPT harness reuses one context per +thread. A builder borrows the context mutably until `build`, so record graphs before sharing +the context. + +## Precision notes + +- TensorRT-RTX keeps float32 math at full precision (TF32 disabled) and runs float16 graphs in + float16. +- CoreML computes integer operations in float32; values near the int32 and int64 limits lose + precision. Rank is limited to 5. +- LiteRT rejects some data type and operation combinations up front; see + `dtype_unsupported_for_op` in `src/backends/litert.rs`. +- The WPT tolerances applied per operation are in `tests/wpt_conformance/tolerance.rs`; the + audit mode described in the [WPT Conformance Guide](../testing/wpt-test-guide.md) reports how + much of the tolerance each passing case uses. diff --git a/docs/user-guide/api-reference.md b/docs/user-guide/api-reference.md index 42816584..9132caf8 100644 --- a/docs/user-guide/api-reference.md +++ b/docs/user-guide/api-reference.md @@ -1,1206 +1,144 @@ -# API Reference - -Complete reference for the WebNN Python API. - -## Module: `webnn` - -The main module exports all public classes and types. - -```python -import webnn -``` - ---- - -## Class: `ML` - -Entry point for the WebNN API. Provides methods to create execution contexts. - -### Constructor - -```python -ml = webnn.ML() -``` - -Creates a new ML namespace instance. - -### Methods - -#### `create_context(accelerated=True, power_preference="default")` - -Creates a new execution context following the [W3C WebNN Device Selection spec](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md). - -**Parameters:** - -- `accelerated` (bool): Request GPU/NPU acceleration. Default: `True` - - `True`: Platform selects GPU or NPU if available - - `False`: CPU-only execution -- `power_preference` (str): Power/performance hint. Options: `"default"`, `"high-performance"`, `"low-power"`. Default: `"default"` - - `"low-power"`: Prefers NPU over GPU (Neural Engine on Apple Silicon) - - `"high-performance"`: Prefers GPU over NPU - - `"default"`: Platform decides (typically GPU > NPU > CPU) - -**Returns:** `MLContext` - -**Example:** - -```python -ml = webnn.ML() - -# Request acceleration (default) -context = ml.create_context(accelerated=True, power_preference="default") -print(f"Accelerated: {context.accelerated}") # Check actual capability - -# CPU-only execution -context = ml.create_context(accelerated=False) -``` - -**Note:** Per the WebNN Device Selection Explainer, `accelerated` is a hint. The platform autonomously selects the actual device based on availability and runtime conditions. - ---- - -## Class: `MLContext` - -Represents an execution context for neural network operations. - -### Properties - -#### `accelerated` (bool, read-only) - -Indicates if GPU/NPU acceleration is available for this context. - -- `True`: Platform can provide GPU or NPU resources -- `False`: Only CPU execution available - -This represents platform capability, not a guarantee of specific device allocation. - -#### `power_preference` (str, read-only) - -The power preference hint for this context. - -### Methods - -#### `create_graph_builder()` - -Creates a new graph builder for constructing computational graphs. - -**Returns:** `MLGraphBuilder` - -**Example:** - -```python -builder = context.create_graph_builder() -``` - -#### `compute(graph, inputs, outputs=None)` - -Executes the graph with given inputs (placeholder implementation). - -**Parameters:** - -- `graph` (MLGraph): The compiled graph to execute -- `inputs` (dict): Dictionary mapping input names to NumPy arrays -- `outputs` (dict, optional): Pre-allocated output arrays - -**Returns:** dict - Dictionary mapping output names to result NumPy arrays - -**Example:** - -```python -results = context.compute(graph, { - "input": np.array([[1, 2, 3]], dtype=np.float32) -}) -``` - -#### `convert_to_onnx(graph, output_path)` - -Converts the graph to ONNX format and saves it to a file. - -**Parameters:** - -- `graph` (MLGraph): The graph to convert -- `output_path` (str): Path where the ONNX model will be saved - -**Example:** - -```python -context.convert_to_onnx(graph, "model.onnx") -``` - -#### `convert_to_coreml(graph, output_path)` - -Converts the graph to CoreML format (macOS only). - -**Parameters:** - -- `graph` (MLGraph): The graph to convert -- `output_path` (str): Path where the CoreML model will be saved - -**Note:** Only available on macOS. Supports limited operations (add, matmul). - -**Example:** - -```python -context.convert_to_coreml(graph, "model.mlmodel") -``` - -#### `create_tensor(shape, data_type, readable=True, writable=True, exportable_to_gpu=False)` - -Creates an MLTensor for explicit tensor management. - -Following the [W3C WebNN MLTensor Explainer](https://github.com/webmachinelearning/webnn/blob/main/mltensor-explainer.md). - -**Parameters:** - -- `shape` (list[int]): Shape of the tensor -- `data_type` (str): Data type (e.g., "float32") -- `readable` (bool): If True, tensor data can be read back to CPU. Default: `True` -- `writable` (bool): If True, tensor data can be written from CPU. Default: `True` -- `exportable_to_gpu` (bool): If True, tensor can be exported for use as GPU texture. Default: `False` - -**Returns:** `MLTensor` - -**Example:** - -```python -# Create default tensor (readable and writable) -tensor = context.create_tensor([2, 3], "float32") - -# Create read-only tensor -ro_tensor = context.create_tensor([2, 3], "float32", readable=True, writable=False) - -# Create write-only tensor -wo_tensor = context.create_tensor([2, 3], "float32", readable=False, writable=True) - -# Create GPU-exportable tensor -gpu_tensor = context.create_tensor([2, 3], "float32", exportable_to_gpu=True) -``` - -#### `read_tensor(tensor)` - -Reads data from an MLTensor into a numpy array. - -**Parameters:** - -- `tensor` (MLTensor): The tensor to read from (must have `readable=True`) - -**Returns:** `numpy.ndarray` - -**Raises:** - -- `RuntimeError`: If tensor is not readable or has been destroyed - -**Example:** - -```python -tensor = context.create_tensor([2, 3], "float32") -result = context.read_tensor(tensor) -``` - -#### `write_tensor(tensor, data)` - -Writes data from a numpy array into an MLTensor. - -**Parameters:** - -- `tensor` (MLTensor): The tensor to write to (must have `writable=True`) -- `data` (numpy.ndarray): Data to write - -**Raises:** - -- `RuntimeError`: If tensor is not writable or has been destroyed -- `ValueError`: If data shape doesn't match tensor shape - -**Example:** - -```python -tensor = context.create_tensor([2, 3], "float32") -data = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) -context.write_tensor(tensor, data) -``` - -#### `dispatch(graph, inputs, outputs)` - -Dispatches graph execution asynchronously with MLTensor inputs/outputs. - -Following the [W3C WebNN MLTensor Explainer](https://github.com/webmachinelearning/webnn/blob/main/mltensor-explainer.md) timeline model. - -**Parameters:** - -- `graph` (MLGraph): The compiled graph to execute -- `inputs` (dict): Dictionary mapping input names to MLTensor objects -- `outputs` (dict): Dictionary mapping output names to MLTensor objects - -**Returns:** None (results are written to output tensors) - -**Example:** - -```python -# Create tensors -input_tensor = context.create_tensor([2, 3], "float32") -output_tensor = context.create_tensor([2, 3], "float32") - -# Write input data -input_data = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) -context.write_tensor(input_tensor, input_data) - -# Dispatch execution -context.dispatch(graph, {"x": input_tensor}, {"output": output_tensor}) - -# Read results -result = context.read_tensor(output_tensor) -``` - ---- - -## Class: `MLTensor` - -Represents an opaque typed tensor for explicit resource management. - -Following the [W3C WebNN MLTensor Explainer](https://github.com/webmachinelearning/webnn/blob/main/mltensor-explainer.md). - -### Properties - -#### `shape` (list[int], read-only) - -The shape of the tensor. - -#### `data_type` (str, read-only) - -The data type of the tensor. - -#### `size` (int, read-only) - -The total number of elements in the tensor. - -#### `readable` (bool, read-only) - -Whether tensor data can be read back to CPU. - -#### `writable` (bool, read-only) - -Whether tensor data can be written from CPU. - -#### `exportable_to_gpu` (bool, read-only) - -Whether tensor can be exported for use as GPU texture. - -### Methods - -#### `destroy()` - -Explicitly destroys the tensor and releases its resources. - -After calling `destroy()`, the tensor cannot be used for any operations. - -**Raises:** - -- `RuntimeError`: If tensor is already destroyed - -**Example:** - -```python -tensor = context.create_tensor([2, 3], "float32") -# ... use tensor ... -tensor.destroy() # Explicit cleanup -``` - ---- - -## Class: `MLGraphBuilder` - -Builder for constructing computational graphs using a declarative API. - -### Input/Constant Operations - -#### `input(name, shape, data_type="float32")` - -Creates an input operand. - -**Parameters:** - -- `name` (str): Name of the input -- `shape` (list[int]): Shape of the tensor -- `data_type` (str): Data type. Options: `"float32"`, `"float16"`, `"int32"`, `"uint32"`, `"int8"`, `"uint8"` - -**Returns:** `MLOperand` - -**Example:** - -```python -x = builder.input("x", [1, 3, 224, 224], "float32") -``` - -#### `constant(value, shape=None, data_type=None)` - -Creates a constant operand from a NumPy array or Python list. - -**Parameters:** - -- `value` (array-like): NumPy array or Python list -- `shape` (list[int], optional): Shape override -- `data_type` (str, optional): Data type override - -**Returns:** `MLOperand` - -**Example:** - -```python -import numpy as np - -weights = builder.constant(np.random.randn(784, 10).astype('float32')) -bias = builder.constant(np.zeros(10, dtype='float32')) -``` - -### Binary Operations - -All binary operations take two operands and return a new operand. - -#### `add(a, b)` - -Element-wise addition: `a + b` - -#### `sub(a, b)` - -Element-wise subtraction: `a - b` - -#### `mul(a, b)` - -Element-wise multiplication: `a * b` - -#### `div(a, b)` - -Element-wise division: `a / b` - -#### `matmul(a, b)` - -Matrix multiplication: `a @ b` - -**Example:** - -```python -x = builder.input("x", [2, 3], "float32") -y = builder.input("y", [2, 3], "float32") - -sum_result = builder.add(x, y) -product = builder.mul(x, y) -``` - -### Convolution Operations - -#### `conv2d(input, filter, strides=None, dilations=None, pads=None, groups=None, input_layout=None, filter_layout=None)` - -2D convolution operation for neural networks. - -**Parameters:** - -- `input` (MLOperand): Input tensor (4D: batch, channels, height, width or batch, height, width, channels) -- `filter` (MLOperand): Filter/kernel weights (4D constant tensor) -- `strides` (list[int], optional): Stride along each spatial axis. Default: `[1, 1]` -- `dilations` (list[int], optional): Dilation along each spatial axis. Default: `[1, 1]` -- `pads` (list[int], optional): Padding `[begin_height, begin_width, end_height, end_width]`. Default: `[0, 0, 0, 0]` -- `groups` (int, optional): Number of groups for grouped/depthwise convolution. Default: `1` -- `input_layout` (str, optional): Input tensor layout, either `"nchw"` (channels-first) or `"nhwc"` (channels-last). Default: `"nchw"` -- `filter_layout` (str, optional): Filter tensor layout: `"oihw"`, `"hwio"`, `"ohwi"`, or `"ihwo"`. Default: `"oihw"` - -**Returns:** MLOperand with output tensor - -**Shape Inference:** - -For NCHW input `[N, C_in, H_in, W_in]` and OIHW filter `[C_out, C_in/groups, K_h, K_w]`: - -``` -output_h = (H_in + pad_begin_h + pad_end_h - dilation_h * (K_h - 1) - 1) / stride_h + 1 -output_w = (W_in + pad_begin_w + pad_end_w - dilation_w * (K_w - 1) - 1) / stride_w + 1 -output_shape = [N, C_out, output_h, output_w] -``` - -**Example: Standard Convolution** - -```python -# Input: [batch=1, channels=3, height=32, width=32] (RGB image) -input_op = builder.input("input", [1, 3, 32, 32], "float32") - -# Filter: [out_channels=64, in_channels=3, height=3, width=3] -filter_weights = np.random.randn(64, 3, 3, 3).astype(np.float32) -filter_op = builder.constant(filter_weights) - -# Apply conv2d with stride=2 and padding=1 -output = builder.conv2d( - input_op, - filter_op, - strides=[2, 2], - pads=[1, 1, 1, 1] -) -# Output shape: [1, 64, 16, 16] -``` - -**Example: Depthwise Convolution** - -```python -# Depthwise convolution: each input channel is convolved separately -input_op = builder.input("input", [1, 32, 28, 28], "float32") - -# Filter: [out_channels=32, in_channels=1, height=3, width=3] -# groups=32 means 32 separate 1-channel convolutions -filter_weights = np.random.randn(32, 1, 3, 3).astype(np.float32) -filter_op = builder.constant(filter_weights) - -output = builder.conv2d( - input_op, - filter_op, - pads=[1, 1, 1, 1], - groups=32 # Depthwise: groups = input channels -) -# Output shape: [1, 32, 28, 28] -``` - -**Example: Dilated Convolution** - -```python -# Dilated (atrous) convolution increases receptive field -input_op = builder.input("input", [1, 3, 32, 32], "float32") -filter_weights = np.random.randn(64, 3, 3, 3).astype(np.float32) -filter_op = builder.constant(filter_weights) - -output = builder.conv2d( - input_op, - filter_op, - dilations=[2, 2], # Dilation factor of 2 - pads=[2, 2, 2, 2] # Larger padding for dilated kernels -) -# Effective kernel size: 3 + (3-1)*2 = 5x5 -``` - -**Example: NHWC Layout (Channels-Last)** - -```python -# Input in NHWC format: [batch, height, width, channels] -input_op = builder.input("input", [1, 32, 32, 3], "float32") -filter_weights = np.random.randn(64, 3, 3, 3).astype(np.float32) -filter_op = builder.constant(filter_weights) - -output = builder.conv2d( - input_op, - filter_op, - input_layout="nhwc", # Channels-last input - pads=[1, 1, 1, 1] -) -# Output shape: [1, 32, 32, 64] (also NHWC) -``` - -#### `conv_transpose2d(input, filter, strides=None, dilations=None, pads=None, output_padding=None, output_sizes=None, groups=None, input_layout=None, filter_layout=None)` - -2D transposed convolution (deconvolution) operation for upsampling. - -**Parameters:** - -- `input` (MLOperand): Input tensor (4D) -- `filter` (MLOperand): Filter weights (4D constant tensor) -- `strides` (list[int], optional): Stride along each spatial axis. Default: `[1, 1]` -- `dilations` (list[int], optional): Dilation along each spatial axis. Default: `[1, 1]` -- `pads` (list[int], optional): Padding. Default: `[0, 0, 0, 0]` -- `output_padding` (list[int], optional): Additional output padding. Default: `[0, 0]` -- `output_sizes` (list[int], optional): Explicit output spatial dimensions. Default: `None` (computed) -- `groups` (int, optional): Number of groups. Default: `1` -- `input_layout` (str, optional): `"nchw"` or `"nhwc"`. Default: `"nchw"` -- `filter_layout` (str, optional): Filter layout. Default: `"oihw"` - -**Returns:** MLOperand with upsampled output tensor - -**Shape Inference:** - -For NCHW input `[N, C_in, H_in, W_in]` and OIHW filter `[C_in, C_out/groups, K_h, K_w]`: - -``` -output_h = (H_in - 1) * stride_h + effective_kernel_h - pad_begin_h - pad_end_h + output_pad_h -output_w = (W_in - 1) * stride_w + effective_kernel_w - pad_begin_w - pad_end_w + output_pad_w -output_shape = [N, C_out, output_h, output_w] -``` - -**Example: Basic Upsampling** - -```python -# Upsample 14x14 to 29x29 with stride=2 -input_op = builder.input("input", [1, 64, 14, 14], "float32") -filter_weights = np.random.randn(64, 32, 3, 3).astype(np.float32) -filter_op = builder.constant(filter_weights) - -output = builder.conv_transpose2d(input_op, filter_op, strides=[2, 2]) -# Output shape: [1, 32, 29, 29] -``` - -**Example: With Output Padding** - -```python -# Use output_padding to control exact output size -input_op = builder.input("input", [1, 64, 14, 14], "float32") -filter_weights = np.random.randn(64, 32, 3, 3).astype(np.float32) -filter_op = builder.constant(filter_weights) - -output = builder.conv_transpose2d( - input_op, - filter_op, - strides=[2, 2], - output_padding=[1, 1] -) -# Output shape: [1, 32, 30, 30] -``` - -**Example: Explicit Output Sizes** - -```python -# Specify exact output dimensions -input_op = builder.input("input", [1, 64, 14, 14], "float32") -filter_weights = np.random.randn(64, 32, 3, 3).astype(np.float32) -filter_op = builder.constant(filter_weights) - -output = builder.conv_transpose2d( - input_op, - filter_op, - strides=[2, 2], - pads=[1, 1, 1, 1], - output_sizes=[28, 28] -) -# Output shape: [1, 32, 28, 28] -``` - -### Pooling Operations - -#### `average_pool2d(input, window_dimensions=None, strides=None, dilations=None, pads=None, layout=None)` - -2D average pooling operation for downsampling by computing the average of values in a pooling window. - -**Parameters:** - -- `input` (MLOperand): Input tensor (4D) -- `window_dimensions` (list[int], optional): Pooling window size `[height, width]`. Default: `[1, 1]` -- `strides` (list[int], optional): Stride along each spatial axis. Default: `[1, 1]` -- `dilations` (list[int], optional): Dilation along each spatial axis. Default: `[1, 1]` -- `pads` (list[int], optional): Padding `[begin_height, begin_width, end_height, end_width]`. Default: `[0, 0, 0, 0]` -- `layout` (str, optional): `"nchw"` or `"nhwc"`. Default: `"nchw"` - -**Returns:** `MLOperand` - Output tensor after pooling - -**Shape Inference:** - -For each spatial dimension: -``` -output_size = floor((input_size + pad_begin + pad_end - effective_window_size) / stride) + 1 -``` - -where `effective_window_size = (window_size - 1) * dilation + 1` - -**Example: Basic Average Pooling** - -```python -# Input: [1, 64, 28, 28] -input_op = builder.input("input", [1, 64, 28, 28], "float32") - -# Apply 2x2 average pooling with stride 2 -output = builder.average_pool2d( - input_op, - window_dimensions=[2, 2], - strides=[2, 2] -) -# Output shape: [1, 64, 14, 14] -``` - -**Example: Average Pooling with Padding** - -```python -input_op = builder.input("input", [1, 64, 28, 28], "float32") - -output = builder.average_pool2d( - input_op, - window_dimensions=[3, 3], - strides=[2, 2], - pads=[1, 1, 1, 1] # Padding on all sides -) -# Output shape: [1, 64, 14, 14] -``` - -**Example: NHWC Layout** - -```python -# Input in NHWC format: [batch, height, width, channels] -input_op = builder.input("input", [1, 28, 28, 64], "float32") - -output = builder.average_pool2d( - input_op, - window_dimensions=[2, 2], - strides=[2, 2], - layout="nhwc" -) -# Output shape: [1, 14, 14, 64] (also NHWC) -``` - -#### `max_pool2d(input, window_dimensions=None, strides=None, dilations=None, pads=None, layout=None)` - -2D max pooling operation for downsampling by taking the maximum value in a pooling window. - -**Parameters:** - -- `input` (MLOperand): Input tensor (4D) -- `window_dimensions` (list[int], optional): Pooling window size `[height, width]`. Default: `[1, 1]` -- `strides` (list[int], optional): Stride along each spatial axis. Default: `[1, 1]` -- `dilations` (list[int], optional): Dilation along each spatial axis. Default: `[1, 1]` -- `pads` (list[int], optional): Padding `[begin_height, begin_width, end_height, end_width]`. Default: `[0, 0, 0, 0]` -- `layout` (str, optional): `"nchw"` or `"nhwc"`. Default: `"nchw"` - -**Returns:** `MLOperand` - Output tensor after pooling - -**Shape Inference:** - -Same as `average_pool2d` - for each spatial dimension: -``` -output_size = floor((input_size + pad_begin + pad_end - effective_window_size) / stride) + 1 -``` - -**Example: Basic Max Pooling** - -```python -# Input: [1, 64, 28, 28] -input_op = builder.input("input", [1, 64, 28, 28], "float32") - -# Apply 2x2 max pooling with stride 2 -output = builder.max_pool2d( - input_op, - window_dimensions=[2, 2], - strides=[2, 2] -) -# Output shape: [1, 64, 14, 14] -``` - -**Example: Overlapping Max Pooling** - -```python -input_op = builder.input("input", [1, 32, 14, 14], "float32") - -# Window size 2x2, stride 1x1 (overlapping windows) -output = builder.max_pool2d( - input_op, - window_dimensions=[2, 2], - strides=[1, 1] -) -# Output shape: [1, 32, 13, 13] -``` - -**Example: Max Pooling with Padding** - -```python -input_op = builder.input("input", [1, 64, 28, 28], "float32") - -output = builder.max_pool2d( - input_op, - window_dimensions=[3, 3], - strides=[2, 2], - pads=[1, 1, 1, 1] -) -# Output shape: [1, 64, 14, 14] -``` - -#### `global_average_pool(input, layout=None)` - -Global average pooling operation that reduces spatial dimensions to 1x1 by averaging over all spatial locations. - -**Parameters:** - -- `input` (MLOperand): Input tensor (4D) -- `layout` (str, optional): `"nchw"` or `"nhwc"`. Default: `"nchw"` - -**Returns:** `MLOperand` - Output tensor with spatial dimensions 1x1 - -**Shape Inference:** - -- NCHW: `[N, C, H, W]` → `[N, C, 1, 1]` -- NHWC: `[N, H, W, C]` → `[N, 1, 1, C]` - -**Example: Basic Global Average Pooling** - -```python -# Input: [1, 64, 28, 28] -input_op = builder.input("input", [1, 64, 28, 28], "float32") - -# Global average pool reduces spatial dimensions to 1x1 -output = builder.global_average_pool(input_op) -# Output shape: [1, 64, 1, 1] -``` - -**Example: For Classification (Typical ResNet-style)** - -```python -# After last conv layer: [1, 2048, 7, 7] -features = builder.input("features", [1, 2048, 7, 7], "float32") - -# Global average pooling instead of flatten -pooled = builder.global_average_pool(features) -# Output shape: [1, 2048, 1, 1] - -# Reshape for fully connected layer -flattened = builder.reshape(pooled, [1, 2048]) -``` - -**Example: NHWC Layout** - -```python -# Input in NHWC: [1, 28, 28, 64] -input_op = builder.input("input", [1, 28, 28, 64], "float32") - -output = builder.global_average_pool(input_op, layout="nhwc") -# Output shape: [1, 1, 1, 64] -``` - -#### `global_max_pool(input, layout=None)` - -Global max pooling operation that reduces spatial dimensions to 1x1 by taking the maximum value over all spatial locations. - -**Parameters:** - -- `input` (MLOperand): Input tensor (4D) -- `layout` (str, optional): `"nchw"` or `"nhwc"`. Default: `"nchw"` - -**Returns:** `MLOperand` - Output tensor with spatial dimensions 1x1 - -**Shape Inference:** - -Same as `global_average_pool`: -- NCHW: `[N, C, H, W]` → `[N, C, 1, 1]` -- NHWC: `[N, H, W, C]` → `[N, 1, 1, C]` - -**Example: Basic Global Max Pooling** - -```python -# Input: [2, 128, 7, 7] -input_op = builder.input("input", [2, 128, 7, 7], "float32") - -# Global max pool reduces spatial dimensions to 1x1 -output = builder.global_max_pool(input_op) -# Output shape: [2, 128, 1, 1] -``` - -**Example: Multi-scale Feature Extraction** - -```python -# Extract features at different scales -input_op = builder.input("input", [1, 512, 14, 14], "float32") - -# Global max pooling captures strongest activations -max_pooled = builder.global_max_pool(input_op) -# Output shape: [1, 512, 1, 1] - -# Global average pooling captures average response -avg_pooled = builder.global_average_pool(input_op) -# Output shape: [1, 512, 1, 1] - -# Can concatenate both for richer representation -``` - -### Normalization Operations - -Normalization operations standardize activations to improve training stability and model performance. - -#### `batch_normalization(input, mean, variance, scale=None, bias=None, epsilon=1e-5, axis=1)` - -Batch normalization operation that normalizes the input across the batch dimension using pre-computed mean and variance statistics. - -**Parameters:** - -- `input` (MLOperand): Input tensor to normalize -- `mean` (MLOperand): Pre-computed mean values (1D tensor, size = channels) -- `variance` (MLOperand): Pre-computed variance values (1D tensor, size = channels) -- `scale` (MLOperand, optional): Learnable scale parameter (gamma) -- `bias` (MLOperand, optional): Learnable bias parameter (beta) -- `epsilon` (float, optional): Small constant for numerical stability. Default: `1e-5` -- `axis` (int, optional): Feature axis along which normalization occurs. Default: `1` - -**Returns:** `MLOperand` - Normalized output tensor (same shape as input) - -**Shape Inference:** -- Output shape = Input shape (preserves dimensions) - -**Formula:** -``` -y = scale * ((x - mean) / sqrt(variance + epsilon)) + bias -``` - -**Example: Basic Batch Normalization** - -```python -# Input: [2, 64, 28, 28] (batch=2, channels=64, height=28, width=28) -input_op = builder.input("input", [2, 64, 28, 28], "float32") -mean = builder.input("mean", [64], "float32") -variance = builder.input("variance", [64], "float32") - -# Apply batch normalization -output = builder.batch_normalization(input_op, mean, variance) -# Output shape: [2, 64, 28, 28] -``` - -**Example: With Learnable Parameters** - -```python -# Include scale and bias for training -input_op = builder.input("input", [4, 128, 14, 14], "float32") -mean = builder.input("mean", [128], "float32") -variance = builder.input("variance", [128], "float32") -scale = builder.input("scale", [128], "float32") # gamma -bias = builder.input("bias", [128], "float32") # beta - -output = builder.batch_normalization( - input_op, mean, variance, - scale=scale, bias=bias, - epsilon=1e-5 -) -``` - -**Example: Custom Epsilon for Numerical Stability** - -```python -# Use larger epsilon for very small variance values -input_op = builder.input("input", [1, 256, 7, 7], "float32") -mean = builder.input("mean", [256], "float32") -variance = builder.input("variance", [256], "float32") - -output = builder.batch_normalization( - input_op, mean, variance, - epsilon=1e-3 # Larger epsilon for stability -) -``` - -#### `instance_normalization(input, scale=None, bias=None, epsilon=1e-5, layout="nchw")` - -Instance normalization operation that normalizes each instance in a batch independently across spatial dimensions. Commonly used in style transfer and image generation tasks. - -**Parameters:** - -- `input` (MLOperand): Input tensor to normalize (typically 4D: [N, C, H, W]) -- `scale` (MLOperand, optional): Learnable scale parameter (1D tensor, size = channels) -- `bias` (MLOperand, optional): Learnable bias parameter (1D tensor, size = channels) -- `epsilon` (float, optional): Small constant for numerical stability. Default: `1e-5` -- `layout` (str, optional): `"nchw"` or `"nhwc"`. Default: `"nchw"` - -**Returns:** `MLOperand` - Normalized output tensor (same shape as input) - -**Shape Inference:** -- Output shape = Input shape (preserves dimensions) - -**Formula:** -``` -For each instance i and channel c: - y[i,c] = scale[c] * ((x[i,c] - mean[i,c]) / sqrt(variance[i,c] + epsilon)) + bias[c] -``` - -**Example: Basic Instance Normalization** - -```python -# Input: [2, 64, 28, 28] -input_op = builder.input("input", [2, 64, 28, 28], "float32") - -# Apply instance normalization (computes stats per instance) -output = builder.instance_normalization(input_op) -# Output shape: [2, 64, 28, 28] -``` - -**Example: With Scale and Bias (For Style Transfer)** - -```python -# Instance norm with learnable parameters -input_op = builder.input("input", [1, 32, 256, 256], "float32") -scale = builder.input("scale", [32], "float32") -bias = builder.input("bias", [32], "float32") - -output = builder.instance_normalization( - input_op, - scale=scale, - bias=bias, - epsilon=1e-5 -) -``` - -**Example: NHWC Layout** - -```python -# Use NHWC layout (channels-last) -input_op = builder.input("input", [2, 28, 28, 64], "float32") - -output = builder.instance_normalization(input_op, layout="nhwc") -# Output shape: [2, 28, 28, 64] -``` - -#### `layer_normalization(input, scale=None, bias=None, epsilon=1e-5, axes=None)` - -Layer normalization operation that normalizes across feature dimensions within each example. Fundamental for transformer architectures and modern language models. - -**Parameters:** - -- `input` (MLOperand): Input tensor to normalize -- `scale` (MLOperand, optional): Learnable scale parameter (gamma) -- `bias` (MLOperand, optional): Learnable bias parameter (beta) -- `epsilon` (float, optional): Small constant for numerical stability. Default: `1e-5` -- `axes` (list[int], optional): Dimensions over which to compute normalization statistics. Default: `[-1]` (last dimension) - -**Returns:** `MLOperand` - Normalized output tensor (same shape as input) - -**Shape Inference:** -- Output shape = Input shape (preserves dimensions) - -**Formula:** -``` -y = scale * ((x - mean(x, axes)) / sqrt(variance(x, axes) + epsilon)) + bias -``` - -**Example: Basic Layer Normalization (2D)** - -```python -# Input: [2, 512] (batch=2, features=512) - typical for transformers -input_op = builder.input("input", [2, 512], "float32") - -# Normalize over last dimension (features) -output = builder.layer_normalization(input_op) -# Output shape: [2, 512] -``` - -**Example: With Scale and Bias (Transformer Block)** - -```python -# Layer norm with learnable parameters -input_op = builder.input("input", [4, 768], "float32") -scale = builder.input("scale", [768], "float32") # gamma -bias = builder.input("bias", [768], "float32") # beta - -output = builder.layer_normalization( - input_op, - scale=scale, - bias=bias, - epsilon=1e-12 # Common in transformers -) -``` - -**Example: 3D Input (Sequence Data)** - -```python -# Input: [batch, sequence_length, features] -input_op = builder.input("input", [2, 10, 512], "float32") - -# Normalize over last dimension (feature dimension) -output = builder.layer_normalization(input_op, axes=[-1]) -# Output shape: [2, 10, 512] -``` - -**Example: Multiple Axes Normalization** - -```python -# Normalize over multiple dimensions -input_op = builder.input("input", [2, 8, 256], "float32") - -# Normalize over last two dimensions -output = builder.layer_normalization(input_op, axes=[-2, -1]) -# Output shape: [2, 8, 256] -``` - -**Example: Vision Transformer (ViT) Style** - -```python -# Typical ViT layer normalization setup -# Input: [batch, num_patches, embedding_dim] -input_op = builder.input("patches", [1, 196, 768], "float32") -scale = builder.input("ln_scale", [768], "float32") -bias = builder.input("ln_bias", [768], "float32") - -# Normalize over embedding dimension -normalized = builder.layer_normalization( - input_op, - scale=scale, - bias=bias, - axes=[-1], - epsilon=1e-6 -) -# Output shape: [1, 196, 768] -``` - -### Unary Operations - -All unary operations take one operand and return a new operand. - -#### `relu(x)` - -Rectified Linear Unit activation: `max(0, x)` - -#### `sigmoid(x)` - -Sigmoid activation: `1 / (1 + exp(-x))` - -#### `tanh(x)` - -Hyperbolic tangent activation - -#### `softmax(x)` - -Softmax activation (normalizes to probability distribution) - -**Example:** - -```python -x = builder.input("x", [1, 10], "float32") - -relu_out = builder.relu(x) -sigmoid_out = builder.sigmoid(x) -tanh_out = builder.tanh(x) -softmax_out = builder.softmax(x) -``` - -### Shape Operations - -#### `reshape(x, new_shape)` - -Reshapes a tensor to a new shape. - -**Parameters:** - -- `x` (MLOperand): Input operand -- `new_shape` (list[int]): New shape - -**Returns:** `MLOperand` - -**Example:** - -```python -x = builder.input("x", [1, 784], "float32") -reshaped = builder.reshape(x, [1, 28, 28, 1]) -``` - -### Graph Building - -#### `build(outputs)` - -Compiles the graph and returns an immutable MLGraph. - -**Parameters:** - -- `outputs` (dict): Dictionary mapping output names to MLOperand objects - -**Returns:** `MLGraph` - -**Example:** - -```python -x = builder.input("x", [2, 3], "float32") -y = builder.relu(x) - -graph = builder.build({"output": y}) -``` - ---- - -## Class: `MLOperand` - -Represents a tensor operand in the computational graph. - -### Properties - -#### `data_type` (str, read-only) - -The data type of the operand. - -#### `shape` (list[int], read-only) - -The shape of the operand. - -#### `name` (str | None, read-only) - -The name of the operand (if any). - -**Example:** - -```python -x = builder.input("x", [2, 3], "float32") - -print(x.data_type) # "float32" -print(x.shape) # [2, 3] -print(x.name) # "x" -``` - ---- - -## Class: `MLGraph` - -Represents a compiled, immutable computational graph. - -### Properties - -#### `operand_count` (int, read-only) - -The number of operands in the graph. - -#### `operation_count` (int, read-only) - -The number of operations in the graph. - -### Methods - -#### `get_input_names()` - -Returns the names of all input operands. - -**Returns:** list[str] - -#### `get_output_names()` - -Returns the names of all output operands. - -**Returns:** list[str] - -**Example:** - -```python -graph = builder.build({"output": y}) - -print(f"Operands: {graph.operand_count}") -print(f"Operations: {graph.operation_count}") -print(f"Inputs: {graph.get_input_names()}") -print(f"Outputs: {graph.get_output_names()}") -``` - ---- - -## Data Types - -Supported data types: - -| Type | Description | Bytes per element | -|------|-------------|-------------------| -| `"float32"` | 32-bit floating point | 4 | -| `"float16"` | 16-bit floating point | 2 | -| `"int32"` | 32-bit signed integer | 4 | -| `"uint32"` | 32-bit unsigned integer | 4 | -| `"int8"` | 8-bit signed integer | 1 | -| `"uint8"` | 8-bit unsigned integer | 1 | - ---- - -## Error Handling - -All operations can raise Python exceptions: - -```python -try: - graph = builder.build({"output": invalid_operand}) -except ValueError as e: - print(f"Graph validation failed: {e}") - -try: - context.convert_to_onnx(graph, "/invalid/path.onnx") -except IOError as e: - print(f"Failed to write file: {e}") - -try: - context.convert_to_coreml(graph, "model.mlmodel") -except RuntimeError as e: - print(f"Conversion failed: {e}") -``` - -Common exceptions: -- `ValueError`: Invalid graph structure or parameters -- `IOError`: File I/O errors -- `RuntimeError`: Conversion or execution failures +# API Overview + +This page explains how the WebNN API maps to Rust in rustnn and where to find each part. Exact +signatures live in the generated [Rust API reference](https://rustnn.github.io/rustnn/api/rustnn/) +(`make docs-api` builds it locally into `target/doc/rustnn/`). + +## Conventions + +- JavaScript `camelCase` names become Rust `snake_case`: `reduceSum` is `reduce_sum`, + `convTranspose2d` is `conv_transpose2d`, `where` is `where_`. +- Every operation has two methods: `op(...)` with the required operands and arguments, and + `op_with_options(..., options)` taking the matching options struct from + `rustnn::operator_options` (`MLConv2dOptions`, `MLReduceOptions`, ...). Operations without + spec options take `MLOperatorOptions`, which only carries the `label`. +- Operand fields inside option structs (`MLConv2dOptions::bias`, `MLGemmOptions::c`, the + quantization zero points) hold operand indices: pass `operand.rustnn_index()` or + `operand.into()`. +- Methods that are not part of the WebNN specification carry the `rustnn_` prefix. +- The API is synchronous. `dispatch`, `read_tensor` and `write_tensor` return when the work is + done. +- Results are `rustnn::error::Result`, an alias for `Result`. +- Lifetimes tie the objects together: `MLGraphBuilder<'context, 'builder>` borrows the + `MLContext<'context>` mutably while recording, and `MLGraph<'context>` cannot outlive its + context. + +## Types + +| Type | Module | Role | +|---|---|---| +| `MLContext` | `rustnn::mlcontext` | Owns a backend device, its tensors and compiled graphs | +| `MLContextOptions`, `MLPowerPreference`, `RustNNOptions`, `TrtxOptions` | `rustnn::mlcontext` (defined in `mlcontextoptions`) | WebNN hints plus rustnn backend, device and tuning hints | +| `Backend`, `BackendDevice`, `DeviceType` | `rustnn::mlcontext` (defined in `backend_selection`) | Selected backend and device | +| `MLGraphBuilder` | `rustnn::mlgraphbuilder`, re-exported from `mlcontext` | Records operations, compiles an `MLGraph` | +| `MLOperand` | `rustnn::mlcontext` | Copyable handle to an operand of one builder; `rustnn_index()` is the index that option structs take | +| `MLOperandDescriptor` | `rustnn::mlcontext` | Data type and shape (`Vec`) | +| `MLGraph` | `rustnn::mlcontext` | Compiled graph with `input_descriptors` and `output_descriptors` | +| `MLTensor`, `MLTensorDescriptor` | `rustnn::mlcontext` | Backend tensor and its shape, data type, `readable` and `writable` flags | +| `MLNamedOperands`, `MLNamedTensors` | `rustnn::mlcontext` | `BTreeMap<&str, MLOperand>` and `BTreeMap<&str, &MLTensor>` | +| `MLOperandDataType` and the other `ML*` enums | `rustnn::operator_enums` | Spec enums (`MLInputOperandLayout`, `MLPaddingMode`, ...) | +| `ML*Options` | `rustnn::operator_options` | One struct per spec options dictionary | +| `Operation` | `rustnn::operators` | The recorded operation enum, one variant per operation | +| `GraphInfo`, `Operand`, `OperandDescriptor`, `Dimension`, `DataType` | `rustnn::graph` | Backend-agnostic graph model | +| `Error`, `GraphBuilderError`, `ShapeInferenceError`, `GraphError` | `rustnn::error` | Error types, all `Send + Sync` | + +## MLContext + +| Method | Status | Notes | +|---|---|---| +| `MLContext::create(&options)` | implemented | Selects the backend; see [Backends](backends.md) | +| `accelerated()` | implemented | Whether the selected device is not a CPU | +| `create_tensor(&descriptor)` | implemented | Flags default to neither readable nor writable | +| `write_tensor(&tensor, &[T])`, `read_tensor(&tensor, &mut [T])` | implemented | `T: bytemuck::Pod`; the byte size must equal `tensor.rustnn_required_bytes()` | +| `dispatch(&mut graph, &inputs, &outputs)` | implemented | Validates bindings, then runs | +| `rustnn_backend()`, `rustnn_device()`, `rustnn_device_type()` | extension | Inspect the selection | +| `rustnn_set_tensor_capacity(&mut tensor, max_shape)`, `rustnn_resize_tensor(&mut tensor, shape)` | extension | Dynamic shapes; see [Advanced Topics](advanced.md) | +| `create_from_gpu_device`, `lost`, `create_constant_tensor`, `destroy`, `op_support_limits` | not implemented (`todo!()`) | Tensors and contexts are released by `Drop` | + +## MLGraphBuilder + +| Method | Status | Notes | +|---|---|---| +| `MLGraphBuilder::new(&mut context)` | implemented | One builder compiles one graph | +| `MLGraphBuilder::new_uncompiled()` | extension | Records a graph without a backend, for saving or converting | +| `input(name, &descriptor)` | implemented | The name is the dispatch key | +| `constant_from_slice(&descriptor, &[T])`, `constant_from_vec(&descriptor, Vec)`, `constant_from_bytes(&descriptor, Vec)` | implemented | Byte size must match the descriptor; `constant_from_bytes` avoids a copy for large weights | +| `constant_from_tensor`, `constant_from_value` | not implemented | Use `constant_from_slice` with an empty shape for scalars | +| `build(&outputs)` | implemented | Names the outputs and compiles; fails on an empty map, an input or constant used as output, or two names for one operand | +| `finish_graph_info(&outputs)` | extension | Returns the finished `GraphInfo` without compiling | +| `build_graph_info(graph_info)` | extension | Compiles a complete `GraphInfo`, for example one loaded from a file | +| `rustnn_save_webnn(&outputs, path)` | extension | Writes `.webnn` text plus a `.safetensors` weights file; the builder stays usable | +| `rustnn_webnn_text_for_outputs(&outputs)` | extension | The `.webnn` text of the graph so far, for debugging | +| `rustnn_operand_shape(operand)`, `rustnn_operand_data_type(operand)` | extension | Inspect operands while recording | + +Shape inference runs inside every operation method; a shape or data type conflict is returned +from that call as `Error::GraphBuilderError` wrapping a `ShapeInferenceError`. + +### Operations + +The Rust method for each WebNN operation, grouped as in the specification. Required arguments +follow the spec order; `a`, `b` stand for the two inputs of binary operations. + +| Group | Methods | +|---|---| +| Element-wise binary | `add`, `sub`, `mul`, `div`, `pow`, `max`, `min` | +| Comparison and logical (`Uint8` results) | `equal`, `greater`, `greater_or_equal`, `lesser`, `lesser_or_equal`, `not_equal`, `logical_and`, `logical_or`, `logical_xor`, `logical_not` | +| Element-wise unary | `abs`, `ceil`, `floor`, `round_even`, `neg`, `exp`, `log`, `sqrt`, `reciprocal`, `sin`, `cos`, `tan`, `erf`, `sign`, `identity`, `is_nan`, `is_infinite` | +| Activations | `relu`, `sigmoid`, `tanh`, `softmax(input, axis)`, `softplus`, `softsign`, `elu`, `leaky_relu`, `prelu(input, slope)`, `gelu`, `hard_sigmoid`, `hard_swish`, `linear`, `clamp` | +| Convolution and pooling | `conv2d(input, filter)`, `conv_transpose2d`, `average_pool2d`, `max_pool2d`, `l2_pool2d`, `global_average_pool`, `global_max_pool`, `resample2d` | +| Normalization | `batch_normalization(input, mean, variance)`, `instance_normalization`, `layer_normalization` | +| Reduction and indices | `reduce_sum`, `reduce_mean`, `reduce_max`, `reduce_min`, `reduce_product`, `reduce_l1`, `reduce_l2`, `reduce_log_sum`, `reduce_log_sum_exp`, `reduce_sum_square`, `arg_max`, `arg_min`, `cumulative_sum` | +| Shape | `reshape(input, new_shape)`, `transpose`, `expand(input, new_shape)`, `squeeze`, `unsqueeze`, `concat(&[operands], axis)`, `split(input, &splits)`, `split_equal_with_options(input, count, options)`, `slice(input, &starts, &sizes)`, `pad`, `tile`, `reverse`, `triangular`, `cast(input, data_type)`, `shape` | +| Gather and scatter | `gather(input, indices)`, `gather_elements`, `gather_nd`, `scatter_elements(input, indices, updates)`, `scatter_nd`, `where_(condition, true_value, false_value)` | +| Matrix | `matmul`, `gemm` | +| Quantization | `quantize_linear(input, scale)`, `quantize_linear_with_zeropoint(input, scale, zero_point)`, `dequantize_linear`, `dequantize_linear_with_zeropoint`, plus the `_with_options` forms | +| Recurrent | `gru_with_options`, `gru_cell_with_options`, `lstm_with_options`, `lstm_cell_with_options` (only the options forms exist). `gru_with_options` returns the hidden state plus the sequence when `return_sequence` is set; `lstm_with_options` returns hidden state, cell state and optionally the sequence; `lstm_cell_with_options` returns hidden and cell state, all as `Vec` | + +`globalAveragePool`, `globalMaxPool`, `squeeze` and `unsqueeze` are kept from earlier +specification drafts; `shape` is a rustnn extension emitted by onnx2webnn. The per-backend +support matrix is generated into +[Backend Operator Support](../development/backend-operator-support.md). + +## Tensors + +`MLTensorDescriptor` wraps an `MLOperandDescriptor` and two flags. `to_readable()` and +`to_writable()` return flagged copies; `set_readable` and `set_writable` change a descriptor in +place. `MLTensor` exposes `shape()`, `data_type()`, `readable()`, `writable()` and +`rustnn_required_bytes()`. The spec's `destroy()` is not implemented: dropping the tensor +releases it. + +## Data types + +`MLOperandDataType` has `Float32`, `Float16`, `Int32`, `Uint32`, `Int64`, `Uint64`, `Int8`, +`Uint8` and the rustnn extensions `Int4` and `Uint4`. Four-bit values are packed two per byte; +`rustnn_storage_byte_length(elements)` returns the storage size for a count. The graph model +uses `rustnn::graph::DataType` for the same set; conversions in both directions exist. + +## Errors + +| Type | Returned by | +|---|---| +| `Error` | `MLContext` methods, `MLGraphBuilder::build`, `dispatch`, saving. Variants name the failing stage: `NoBackendAvailable`, `GraphBuildError`, `GraphDispatchError`, `WrongWriteSize`, `DuplicateTensorBinding`, ... | +| `GraphBuilderError` | Recording: `GraphAlreadyBuilt`, `WrongConstantSize`, `RequestedInputAsOutput`, `DuplicateOutput`, shape inference failures | +| `ShapeInferenceError` | Per-operation shape and data type checks | +| `GraphError` | Loading, validation, conversion and the legacy executors | + +All error types are `Send + Sync` and compose with `anyhow` and similar crates. + +## Graph model and legacy pipeline + +`MLGraphBuilder` records into a `GraphInfo`: a `Vec`, a `Vec` and the +constant data. Operands are referenced by index (`u32`). `Operation` is an enum with named +operand fields and an `Option` per variant; `Operation::op_type()` returns the +WebNN name. + +The same `GraphInfo` drives the file formats and the legacy pipeline: + +| Item | Module | Purpose | +|---|---|---| +| `load_graph_from_path(path)` | `rustnn::loader` | Read `.webnn` text or JSON, resolve external weights, run shape inference | +| `to_graph_json`, `from_graph_json` | `rustnn::webnn_json` | Convert between `GraphInfo` and the `webnn-graph` JSON model | +| `GraphValidator::new(&graph, ContextProperties)` | `rustnn::validator` | Structural validation; returns `ValidationArtifacts` with named I/O descriptors | +| `ConverterRegistry::with_defaults()` | `rustnn::converters` | `convert("onnx" / "coreml" / "trtx" / "litert" / "cann", &graph)` to a `ConvertedGraph` | +| `run_onnx_with_inputs`, `run_trtx_with_inputs`, `run_coreml_with_inputs` | `rustnn::executors` | One-shot execution of converted bytes; used by the CLI, superseded by `MLContext` | +| `graph_to_dot(&graph)` | `rustnn::graphviz` | Graphviz DOT export | diff --git a/docs/user-guide/backends.md b/docs/user-guide/backends.md new file mode 100644 index 00000000..a9efad8e --- /dev/null +++ b/docs/user-guide/backends.md @@ -0,0 +1,103 @@ +# Backends + +rustnn executes a compiled graph on one of several backends. The backend is chosen when the +`MLContext` is created and stays fixed for that context; graphs built from the context are +compiled for that backend only. + +## Overview + +| Backend | Cargo feature | Platforms | Devices | Runtime dependency | Converter | +|---|---|---|---|---|---| +| ONNX Runtime | `onnx-runtime` | Linux, macOS, Windows | CPU, GPU and NPU devices reported by ONNX Runtime execution providers | ONNX Runtime shared library, loaded from `ORT_DYLIB_PATH`; the release must provide the API level the `ort` crate is built against (`make onnxruntime-download` fetches a matching build) | `OnnxConverter`: ONNX protobuf, with an external weights file for large models | +| TensorRT-RTX | `trtx-runtime`, `trtx-runtime-mock`, `trtx-enterprise` | Linux and Windows with an NVIDIA RTX GPU | GPU, one device per CUDA device | NVIDIA driver and the TensorRT-RTX 1.6 library, found by name on `PATH` or `LD_LIBRARY_PATH`, or loaded explicitly with `dynamically_load_tensorrt` | `TrtxConverter`: builds the TensorRT network directly | +| CoreML | `coreml-runtime` | macOS (compiles to failing shims elsewhere) | CPU, GPU, NPU (Neural Engine) via compute units | none beyond macOS | `CoremlMlProgramConverter`: MLProgram (MIL) | +| LiteRT | `litert-runtime` | Linux, macOS | CPU, GPU and NPU accelerators | LiteRT libraries downloaded by `litert-sys` into `~/.cache/litert-sys/`; `flatc` at build time | `LiteRtConverter`: TFLite flatbuffer, NCHW operands transposed to NHWC | +| CANN | `cann-runtime`, `cann-runtime-mock` | OpenHarmony (`aarch64-unknown-linux-ohos`) with a Kirin NPU | NPU | HiAI through the `hiai-rs` crate | `CannConverter` | +| Browser WebNN | `webnn-runtime` (`wasm32-unknown-unknown`) | browsers with WebNN | as provided by the browser | none | generated bindings only; not selectable through `MLContext::create` yet | + +Without a runtime feature the crate still validates graphs and converts them to ONNX and +CoreML, but `MLContext::create` fails with `Error::NoBackendAvailable`. + +## Selection rules + +`MLContext::create` resolves the WebNN hints in `MLContextOptions` (`accelerated` and the +power preference) in a fixed order, skipping backends that are not compiled in or report no +device. The order is implemented in `src/backend_selection.rs`: + +1. A device hint (`with_rustnn_device_hint`) is used as given, without an availability check. +2. `accelerated` with `Default` or `HighPerformance`: TensorRT-RTX (first CUDA device), then + CoreML (GPU), then LiteRT (GPU), then ONNX Runtime (GPU, then NPU). +3. `accelerated` with `LowPower`: CoreML (Neural Engine), then LiteRT (NPU), then ONNX Runtime + (NPU). +4. Not accelerated: CoreML (CPU), then LiteRT (CPU), then ONNX Runtime (CPU). +5. ONNX Runtime CPU is the last resort for every accelerated request. +6. CANN is only selected when requested with a backend hint. + +A backend hint restricts the search to one backend and fails with +`Error::NoBackendAvailableForBackendHint` when that backend cannot serve the hints: + +```rust +use rustnn::mlcontext::{Backend, MLContext, MLContextOptions, MLPowerPreference}; + +let options = MLContextOptions::new(MLPowerPreference::HighPerformance, true) + .with_rustnn_backend_hint(Backend::Trtx); +let context = MLContext::create(&options)?; +assert_eq!(context.rustnn_backend(), Backend::Trtx); +println!("{:?} {:?}", context.rustnn_device(), context.rustnn_device_type()); +``` + +Both error variants list the backends that were wanted and the backends that are compiled in, +so a build without the expected feature is visible in the error message. `RUST_LOG=info` logs +the selected device. + +## Execution model + +- `MLGraphBuilder::build` converts the recorded graph with the backend's converter and compiles + it once: an ONNX Runtime session, a TensorRT engine, a compiled CoreML model, a LiteRT + interpreter or a HiAI model. The `MLGraph` keeps the compiled artifact together with the + named input and output descriptors. +- `MLContext::create_tensor` allocates a tensor owned by the backend. Host access is controlled + by the `readable` and `writable` flags of the `MLTensorDescriptor`; both are off by default. +- `MLContext::dispatch` rejects a tensor bound under two names, checks every binding's name, + shape and data type against the compiled graph, then runs the backend. Results are written + into the bound output tensors. +- `read_tensor` and `write_tensor` copy whole tensors. The host buffer must hold exactly + `MLTensor::rustnn_required_bytes` bytes. The TensorRT backend synchronizes its stream in + `write_tensor`. +- All calls are synchronous. `MLContext` is `Send + Sync`, so one context can be shared behind + a mutex. + +## Data types + +| `MLOperandDataType` | Storage | Notes | +|---|---|---| +| `Float32`, `Float16` | 4 and 2 bytes | TensorRT keeps float32 math at full precision (TF32 disabled) | +| `Int32`, `Uint32`, `Int64`, `Uint64` | 4 and 8 bytes | CoreML computes integer operations in float32, which loses precision near the int32 and int64 limits | +| `Int8`, `Uint8` | 1 byte | Boolean results (comparisons, `logical*`, `isNaN`, `isInfinite`) are `Uint8` | +| `Int4`, `Uint4` | two values per byte | rustnn extension for quantized weights, used by the WPT corpus; cannot be saved to `.safetensors` | + +Per-backend data type restrictions and the cases that still fail are tracked in +`tests/wpt_conformance/_expected_failures.txt`, in the PASS snapshots under +`tests/snapshots/` and on the [WPT conformance dashboard](https://rustnn.github.io/rustnn/wpt-conformance/). + +## Testing a backend + +| Backend | WPT conformance | Integration tests | In CI | +|---|---|---|---| +| ONNX Runtime | `make test-wpt` (PASS snapshots) | unit tests in `src/backends/ort.rs` and `src/converters/onnx.rs` | yes (Linux) | +| TensorRT-RTX | `make test-wpt-trtx` (PASS snapshots, GPU required) | `cargo test --test test_trtx_execution --features trtx-runtime` | compile check only, no GPU runner | +| CoreML | `make test-wpt-coreml` (expected-failure list) | unit tests with `--features coreml-runtime` | yes (macOS) | +| LiteRT | `make test-wpt-litert` (PASS snapshots and expected-failure list) | `cargo test --test test_litert_execution --features litert-runtime` | yes (Linux, non-blocking) | +| CANN | not run | `make cann-device-test` on an OpenHarmony device; `cargo test --lib --features cann-runtime-mock` | mock build and tests | + +The [WPT Conformance Guide](../testing/wpt-test-guide.md) explains filtering by operation and +regenerating snapshots after a converter change. + +## Caches + +The TensorRT-RTX backend stores built engines (category `trtx`) and the TensorRT runtime cache +(category `trtx-jit`) under the platform cache directory: `~/.cache/rustnn/` on Linux, +`~/Library/Caches/rustnn/` on macOS and `%LOCALAPPDATA%\rustnn\` on +Windows. Entries are zstd compressed. Engine keys include a hash of the converter sources, so +converter edits never reuse a stale engine, and deleting the directories is always safe. +[TensorRT-RTX](../integration/tensorrt.md) lists the options that control caching. diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index fe29ee70..c613433a 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -1,562 +1,140 @@ # Examples -Practical examples demonstrating the WebNN Python API. - -## Basic Examples - -### Simple Addition with Execution - -```python -import webnn -import numpy as np - -# Create context and builder -ml = webnn.ML() -context = ml.create_context(accelerated=False) -builder = context.create_graph_builder() - -# Define computation: z = x + y -x = builder.input("x", [2, 3], "float32") -y = builder.input("y", [2, 3], "float32") -z = builder.add(x, y) - -# Compile graph -graph = builder.build({"z": z}) - -# Execute with real data -x_data = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) -y_data = np.array([[10, 20, 30], [40, 50, 60]], dtype=np.float32) -results = context.compute(graph, {"x": x_data, "y": y_data}) - -print("Result:") -print(results["z"]) -# [[11. 22. 33.] -# [44. 55. 66.]] - -# Optional: Export to ONNX -context.convert_to_onnx(graph, "add.onnx") -print("✓ Model exported to add.onnx") -``` - -### ReLU Activation with Execution - -```python -import webnn -import numpy as np - -ml = webnn.ML() -context = ml.create_context(accelerated=False) -builder = context.create_graph_builder() - -# Apply ReLU to input -x = builder.input("x", [10], "float32") -y = builder.relu(x) - -graph = builder.build({"y": y}) - -# Execute with negative values -x_data = np.array([-5, -3, -1, 0, 1, 3, 5, 7, 9, 11], dtype=np.float32) -results = context.compute(graph, {"x": x_data}) - -print("Input:", x_data) -print("ReLU output:", results["y"]) -# Input: [-5. -3. -1. 0. 1. 3. 5. 7. 9. 11.] -# ReLU output: [ 0. 0. 0. 0. 1. 3. 5. 7. 9. 11.] -``` - -## Intermediate Examples - -### Linear Layer - -A simple fully-connected layer: `output = input @ weights + bias` - -```python -import webnn -import numpy as np - -def create_linear_layer(builder, input_op, in_features, out_features): - """Creates a linear layer with small random weights.""" - weights = np.random.randn(in_features, out_features).astype('float32') * 0.01 - weights_op = builder.constant(weights) - - bias = np.zeros(out_features, dtype='float32') - bias_op = builder.constant(bias) - - matmul_result = builder.matmul(input_op, weights_op) - output = builder.add(matmul_result, bias_op) - return output, weights # Return weights for reference - -# Build and execute -ml = webnn.ML() -context = ml.create_context(accelerated=False) -builder = context.create_graph_builder() - -# Input: batch_size=1, features=4 (simplified example) -input_tensor = builder.input("input", [1, 4], "float32") - -# Linear layer: 4 -> 3 -output, weights = create_linear_layer(builder, input_tensor, 4, 3) - -# Compile -graph = builder.build({"output": output}) - -# Execute with sample input -input_data = np.array([[1.0, 2.0, 3.0, 4.0]], dtype=np.float32) -results = context.compute(graph, {"input": input_data}) - -print(f"Input shape: {input_data.shape}") -print(f"Output shape: {results['output'].shape}") -print(f"Output values: {results['output']}") -print(f"Graph: {graph.operand_count} operands, {graph.operation_count} operations") +## Example programs + +The `examples/` directory contains complete programs. Build them with the features they need; +the two largest are gated behind the `native-examples` feature so that `cargo build` stays +fast. + +| Program | What it shows | Run | +|---|---|---| +| `fast_style_transfer_builder_api.rs` | Builds the fast style transfer network with the builder API (`conv2d`, `conv_transpose2d`, instance normalization with a fallback composed from reductions, `pad`), downloads the weights from the WebNN test-data repository, and pipelines inferences over pre-allocated tensors | `cargo run --release --features native-examples,onnx-runtime --example fast_style_transfer_builder_api -- --input photo.jpg --output styled.png` | +| `smollm_mlcontext.rs` | Text generation with SmolLM-135M from an onnx2webnn export: loads `.webnn` plus weights, uses dynamic tensor shapes for the KV cache (`rustnn_set_tensor_capacity`, `rustnn_resize_tensor`) and a tokenizer | `cargo run --release --features native-examples,onnx-runtime,dynamic-inputs --example smollm_mlcontext -- --model model.webnn --tokenizer tokenizer.json --max-new-tokens 32` | +| `resnet50_webnn_rust.rs` | ResNet-50 classification and a latency benchmark from a `.webnn` export, with ImageNet preprocessing of a JPEG or PNG input | `cargo run --release --features onnx-runtime --example resnet50_webnn_rust -- --model resnet50_Opset16.webnn --input cat.jpg --labels examples/imagenet_classes.txt --bench` | +| `gpt2_webnn_rust.rs`, `smollm_webnn_rust.rs` | Older generation loops built on the legacy executor path (`ConverterRegistry` plus `run_onnx_with_inputs`) instead of `MLContext` | `cargo run --features onnx-runtime --example smollm_webnn_rust -- --help` | + +Replace `onnx-runtime` with `trtx-runtime` to run the same programs on TensorRT-RTX. All of +them accept `--help`. + +Other files in `examples/`: + +- `sample_graph.webnn`, `sample_graph.json`, `toy_transformer.webnn`, `toy_transformer.json`: + small graphs for the CLI and for `load_graph_from_path`. +- `mobilenetv2_manifest.json`, `mobilenetv2.weights`, `mobilenetv2_weights/`: a MobileNetV2 + weight set in the `manifest.json` plus `.weights` layout that the loader resolves. +- `imagenet_classes.txt`, `images/test.jpg`, `sample_text.txt`: inputs for the examples. +- `experimental/*.py`: Python scripts that run the exported ONNX models through ONNX Runtime + for parity checks. They do not use rustnn. + +## Recipes + +The snippets assume the imports from [Getting Started](getting-started.md) and a `context` +created there. `F32` abbreviates `MLOperandDataType::Float32`. + +### Linear layer + +```rust +let mut builder = MLGraphBuilder::new(&mut context)?; +let x = builder.input("x", &MLOperandDescriptor::new(F32, vec![1, 4]))?; +let weight = builder.constant_from_slice(&MLOperandDescriptor::new(F32, vec![4, 3]), &weights)?; +let bias = builder.constant_from_slice(&MLOperandDescriptor::new(F32, vec![3]), &biases)?; +let product = builder.matmul(x, weight)?; +let shifted = builder.add(product, bias)?; // bias broadcasts over the batch dimension +let y = builder.relu(shifted)?; +println!("y: {:?}", builder.rustnn_operand_shape(y)?); // [1, 3] ``` -### Multi-Layer Network with Execution - -```python -import webnn -import numpy as np - -ml = webnn.ML() -context = ml.create_context(accelerated=False) -builder = context.create_graph_builder() - -# Simplified example: 4 -> 8 -> 4 -> 2 -input_tensor = builder.input("input", [1, 4], "float32") - -# Hidden layer 1: 4 -> 8 -w1 = builder.constant(np.random.randn(4, 8).astype('float32') * 0.1) -b1 = builder.constant(np.zeros(8, dtype='float32')) -h1 = builder.add(builder.matmul(input_tensor, w1), b1) -h1 = builder.relu(h1) - -# Hidden layer 2: 8 -> 4 -w2 = builder.constant(np.random.randn(8, 4).astype('float32') * 0.1) -b2 = builder.constant(np.zeros(4, dtype='float32')) -h2 = builder.add(builder.matmul(h1, w2), b2) -h2 = builder.relu(h2) - -# Output layer: 4 -> 2 -w3 = builder.constant(np.random.randn(4, 2).astype('float32') * 0.1) -b3 = builder.constant(np.zeros(2, dtype='float32')) -output = builder.add(builder.matmul(h2, w3), b3) - -# Compile -graph = builder.build({"logits": output}) - -# Execute with sample input -input_data = np.array([[1.0, 0.5, -0.5, 2.0]], dtype=np.float32) -results = context.compute(graph, {"input": input_data}) - -print(f"Multi-layer network:") -print(f" Input shape: {input_data.shape}") -print(f" Output shape: {results['logits'].shape}") -print(f" Output values: {results['logits']}") -print(f" Graph: {graph.operand_count} operands, {graph.operation_count} operations") - -# Optional: Export to ONNX -context.convert_to_onnx(graph, "mlp.onnx") +Each call borrows the builder mutably, so keep intermediate operands in variables instead of +nesting calls. + +### Options + +```rust +use rustnn::operator_options::{MLConv2dOptions, MLReduceOptions}; + +let conv = MLConv2dOptions { + strides: vec![2, 2], + padding: vec![1, 1, 1, 1], // top, bottom, left, right + input_layout: "nchw".to_string(), + filter_layout: "oihw".to_string(), + bias: Some(bias.rustnn_index()), // operand fields hold operand indices + ..Default::default() +}; +let y = builder.conv2d_with_options(x, filter, conv)?; + +let reduce = MLReduceOptions { + axes: Some(vec![1]), + keep_dimensions: true, + ..Default::default() +}; +let mean = builder.reduce_mean_with_options(y, reduce)?; ``` -## Advanced Examples - -### Multiple Outputs - -Create a graph with multiple outputs: - -```python -import webnn +Option structs mirror the specification dictionaries; unset fields keep the spec defaults, and +layouts are the spec strings (`"nchw"`, `"oihw"`). The field lists are in the +[rustdoc of `operator_options`](https://rustnn.github.io/rustnn/api/rustnn/operator_options/). -ml = webnn.ML() -context = ml.create_context() -builder = context.create_graph_builder() +Operand fields of option structs (`MLConv2dOptions::bias`, `MLGemmOptions::c`, the quantization +zero points) hold operand indices; `MLOperand::rustnn_index()` or `.into()` provides them. -# Input -x = builder.input("x", [1, 10], "float32") +### Several outputs -# Multiple transformations -relu_out = builder.relu(x) -sigmoid_out = builder.sigmoid(x) -tanh_out = builder.tanh(x) - -# Build with multiple named outputs -graph = builder.build({ - "relu": relu_out, - "sigmoid": sigmoid_out, - "tanh": tanh_out -}) - -# Check outputs -print("Outputs:", graph.get_output_names()) -# Output: ['relu', 'sigmoid', 'tanh'] - -context.convert_to_onnx(graph, "multi_output.onnx") +```rust +let mut outputs = MLNamedOperands::new(); +outputs.insert("logits", logits); +outputs.insert("hidden", hidden); +let mut graph = builder.build(&outputs)?; ``` -### Working with Different Data Types - -```python -import webnn -import numpy as np - -ml = webnn.ML() -context = ml.create_context() -builder = context.create_graph_builder() - -# Float16 for reduced memory -x_fp16 = builder.input("x_fp16", [100, 100], "float16") -y_fp16 = builder.relu(x_fp16) +Bind one output tensor per name in `dispatch`. Each tensor may appear only once across the +input and output maps. -# Int8 for quantized models -x_int8 = builder.input("x_int8", [100, 100], "int8") -# Note: Quantized operations would need appropriate scaling +### Reuse a graph -# Float32 (default) -x_fp32 = builder.input("x_fp32", [100, 100], "float32") -y_fp32 = builder.relu(x_fp32) +Compile once, then loop over `write_tensor`, `dispatch` and `read_tensor`. Tensors are +allocated once as well; the backend keeps them on the device between dispatches. For pipelining +several inferences with separate tensor sets see `examples/fast_style_transfer_builder_api.rs`. -graph = builder.build({ - "out_fp16": y_fp16, - "out_fp32": y_fp32 -}) +### Save the graph you built -print(f"Graph with mixed precision: {graph.operand_count} operands") +```rust +builder.rustnn_save_webnn(&outputs, "model.webnn")?; // also writes model.safetensors +let reloaded = rustnn::load_graph_from_path("model.webnn")?; ``` -### Reshaping Tensors +`rustnn_save_webnn` borrows the builder read-only, so it can be called before `build`. The +`.webnn` file references the weights with `@weights(...)`, and the loader resolves them from +the sibling file. -```python -import webnn +### Convert to ONNX without a backend -ml = webnn.ML() -context = ml.create_context() -builder = context.create_graph_builder() +```rust +use rustnn::{ConverterRegistry, ONNX_EXTERNAL_WEIGHTS_FILENAME}; -# Flatten image: [1, 28, 28, 1] -> [1, 784] -image = builder.input("image", [1, 28, 28, 1], "float32") -flattened = builder.reshape(image, [1, 784]) - -# Unflatten back: [1, 784] -> [1, 28, 28, 1] -unflattened = builder.reshape(flattened, [1, 28, 28, 1]) - -graph = builder.build({"output": unflattened}) -context.convert_to_onnx(graph, "reshape.onnx") -``` - -### Converting Pre-trained NumPy Weights - -```python -import webnn -import numpy as np - -def convert_numpy_model_to_webnn(weights_dict): - """ - Convert a model with NumPy weights to WebNN graph. - - Args: - weights_dict: Dictionary with keys like 'fc1.weight', 'fc1.bias', etc. - """ - ml = webnn.ML() - context = ml.create_context() - builder = context.create_graph_builder() - - # Input - x = builder.input("input", [1, 784], "float32") - - # Layer 1 - w1 = builder.constant(weights_dict['fc1.weight'].astype('float32')) - b1 = builder.constant(weights_dict['fc1.bias'].astype('float32')) - h1 = builder.matmul(x, w1) - h1 = builder.add(h1, b1) - h1 = builder.relu(h1) - - # Layer 2 - w2 = builder.constant(weights_dict['fc2.weight'].astype('float32')) - b2 = builder.constant(weights_dict['fc2.bias'].astype('float32')) - output = builder.matmul(h1, w2) - output = builder.add(output, b2) - - # Build and export - graph = builder.build({"logits": output}) - context.convert_to_onnx(graph, "converted_model.onnx") - - return graph - -# Example usage -weights = { - 'fc1.weight': np.random.randn(784, 128), - 'fc1.bias': np.zeros(128), - 'fc2.weight': np.random.randn(128, 10), - 'fc2.bias': np.zeros(10), +let mut builder = MLGraphBuilder::new_uncompiled(); +// ... record the graph ... +let graph_info = builder.finish_graph_info(&outputs)?; +let converted = ConverterRegistry::with_defaults().convert("onnx", &graph_info)?; +std::fs::write("model.onnx", &converted.data)?; +if let Some(weights) = converted.weights_data { + // Large models keep their initializers in a sidecar file next to the model. + std::fs::write(ONNX_EXTERNAL_WEIGHTS_FILENAME, weights)?; } - -graph = convert_numpy_model_to_webnn(weights) -print(f"✓ Converted model: {graph.operation_count} operations") -``` - -## Error Handling Examples - -### Graceful Error Handling - -```python -import webnn -import numpy as np - -def build_and_export_safely(output_path): - """Build a graph with proper error handling.""" - try: - ml = webnn.ML() - context = ml.create_context() - builder = context.create_graph_builder() - - x = builder.input("x", [10], "float32") - y = builder.relu(x) - - graph = builder.build({"y": y}) - - # Try ONNX conversion - try: - context.convert_to_onnx(graph, output_path) - print(f"✓ ONNX model saved to {output_path}") - return True - except IOError as e: - print(f"✗ Failed to save ONNX: {e}") - return False - - except ValueError as e: - print(f"✗ Graph validation failed: {e}") - return False - except Exception as e: - print(f"✗ Unexpected error: {e}") - return False - -# Use it -success = build_and_export_safely("model.onnx") -``` - -### Validating Shapes - -```python -import webnn -import numpy as np - -def create_safe_matmul(builder, a_shape, b_shape): - """Create matmul with shape validation.""" - if len(a_shape) != 2 or len(b_shape) != 2: - raise ValueError("matmul requires 2D tensors") - - if a_shape[1] != b_shape[0]: - raise ValueError( - f"Incompatible shapes for matmul: " - f"{a_shape} and {b_shape}" - ) - - a = builder.input("a", a_shape, "float32") - b_data = np.random.randn(*b_shape).astype('float32') - b = builder.constant(b_data) - - result = builder.matmul(a, b) - return result - -ml = webnn.ML() -context = ml.create_context() -builder = context.create_graph_builder() - -try: - # Valid - output = create_safe_matmul(builder, [10, 20], [20, 30]) - print("✓ Valid matmul created") - - # Invalid - will raise error - output = create_safe_matmul(builder, [10, 20], [15, 30]) -except ValueError as e: - print(f"✗ Shape validation failed: {e}") ``` -## Complete Application Example - -### Image Classification Pipeline - -```python -import webnn -import numpy as np - -class SimpleClassifier: - """A simple image classifier using WebNN.""" - - def __init__(self, num_classes=10): - self.ml = webnn.ML() - self.context = self.ml.create_context() - self.graph = None - self.num_classes = num_classes - - def build_model(self): - """Build the classification model.""" - builder = self.context.create_graph_builder() - - # Input: 28x28 grayscale images - input_tensor = builder.input("image", [1, 28, 28, 1], "float32") - - # Flatten - x = builder.reshape(input_tensor, [1, 784]) - - # Hidden layer - w1 = builder.constant(np.random.randn(784, 128).astype('float32') * 0.01) - b1 = builder.constant(np.zeros(128, dtype='float32')) - x = builder.matmul(x, w1) - x = builder.add(x, b1) - x = builder.relu(x) - - # Output layer - w2 = builder.constant(np.random.randn(128, self.num_classes).astype('float32') * 0.01) - b2 = builder.constant(np.zeros(self.num_classes, dtype='float32')) - logits = builder.matmul(x, w2) - logits = builder.add(logits, b2) - - # Softmax - output = builder.softmax(logits) - - # Build - self.graph = builder.build({"probabilities": output}) - print(f"✓ Model built: {self.graph.operation_count} operations") - - def export(self, path="classifier.onnx"): - """Export the model to ONNX.""" - if self.graph is None: - raise RuntimeError("Build model first!") - - self.context.convert_to_onnx(self.graph, path) - print(f"✓ Model exported to {path}") - - def get_info(self): - """Get model information.""" - if self.graph is None: - return "Model not built yet" - - return { - "operands": self.graph.operand_count, - "operations": self.graph.operation_count, - "inputs": self.graph.get_input_names(), - "outputs": self.graph.get_output_names(), - } - -# Use the classifier -classifier = SimpleClassifier(num_classes=10) -classifier.build_model() -classifier.export("mnist_classifier.onnx") - -print("\nModel Info:") -for key, value in classifier.get_info().items(): - print(f" {key}: {value}") -``` - -This comprehensive set of examples should help you get started with various use cases! - ---- - -## Production-Ready Examples +`new_uncompiled` needs no runtime feature. The registry also knows `coreml` and, when their +features are enabled, `trtx`, `litert` and `cann`. -The `examples/` directory contains complete, production-ready examples demonstrating real-world use cases: +### Pick the backend -### Image Classification - -**[mobilenetv2_complete.py](https://github.com/tarekziade/rustnn/blob/main/examples/mobilenetv2_complete.py)** - Complete 106-layer pretrained MobileNetV2 -- Uses all 106 pretrained weight tensors from WebNN test-data -- Achieves 99.60% accuracy on real ImageNet classification -- Supports CPU, GPU, and CoreML (Neural Engine) backends -- Full implementation of inverted residual blocks and depthwise convolutions -- Run with: `make mobilenet-demo` - -```bash -# Run on different backends -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend cpu -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend gpu -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend coreml # macOS only -``` - -**[mobilenetv2_real.py](https://github.com/tarekziade/rustnn/blob/main/examples/mobilenetv2_real.py)** - Alternative MobileNetV2 implementation -- Similar architecture with different weight loading approach - -**[image_classification.py](https://github.com/tarekziade/rustnn/blob/main/examples/image_classification.py)** - Simplified image classification -- Demonstrates the classification pipeline with random weights -- Good starting point for understanding the architecture - -### Text Generation with Transformers - -**[text_generation_gpt.py](https://github.com/tarekziade/rustnn/blob/main/examples/text_generation_gpt.py)** - Next-token generation with attention -- Simplified transformer architecture with self-attention -- Autoregressive generation (one token at a time) -- Positional embeddings and temperature sampling -- Supports CPU, GPU, and CoreML backends -- Run with: `make text-gen-demo` - -```bash -python examples/text_generation_gpt.py --prompt "Hello world" --tokens 30 --backend cpu -``` - -**[text_generation_enhanced.py](https://github.com/tarekziade/rustnn/blob/main/examples/text_generation_enhanced.py)** - Enhanced version with KV cache -- Key-value caching for efficient generation -- HuggingFace tokenizer support -- Better performance for longer sequences -- Run with: `make text-gen-enhanced` - -### Model Training - -**[train_text_model.py](https://github.com/tarekziade/rustnn/blob/main/examples/train_text_model.py)** - Train a text generation model -- Simple gradient descent training loop -- Trains on sample text data -- Saves trained weights to JSON -- Run with: `make text-gen-train` - -```bash -# Train on custom data -python examples/train_text_model.py \ - --data examples/sample_text.txt \ - --epochs 15 \ - --batch-size 16 \ - --lr 0.05 \ - --save trained_model.json - -# Generate with trained weights -python examples/text_generation_gpt.py \ - --weights trained_model.json \ - --prompt "Hello" \ - --tokens 50 -``` - -**[train_simple_demo.py](https://github.com/tarekziade/rustnn/blob/main/examples/train_simple_demo.py)** - Simplified training demonstration -- Minimal example showing the training workflow -- Good starting point for understanding training - -### Basic Examples - -**[python_simple.py](https://github.com/tarekziade/rustnn/blob/main/examples/python_simple.py)** - Simple graph building -- Basic operations: add, relu -- Graph compilation and export -- Good first example - -**[python_matmul.py](https://github.com/tarekziade/rustnn/blob/main/examples/python_matmul.py)** - Matrix multiplication -- Demonstrates matmul operation -- Shows shape inference and broadcasting - ---- - -## Running the Examples - -All examples can be run using make targets or directly with Python: - -```bash -# Using make (recommended) -make python-example # Run all basic examples -make mobilenet-demo # MobileNetV2 on all 3 backends -make text-gen-demo # Text generation with attention -make text-gen-train # Train text model -make text-gen-trained # Generate with trained weights -make text-gen-enhanced # Enhanced version with KV cache - -# Or run directly -python examples/python_simple.py -python examples/mobilenetv2_complete.py examples/images/test.jpg --backend cpu -python examples/text_generation_gpt.py --prompt "Hello" --tokens 30 +```rust +let options = MLContextOptions::new(MLPowerPreference::Default, true) + .with_rustnn_backend_hint(Backend::Onnx); +let context = MLContext::create(&options)?; +match context.rustnn_device() { + BackendDevice::Onnx { device_type, .. } => println!("ONNX Runtime on {device_type:?}"), + other => println!("{other:?}"), +} ``` -For more information on running examples, see the [Development Guide](../development/setup.md). +The selection order and the per-backend requirements are described in [Backends](backends.md). diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index cc312544..5a0b7b48 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -1,251 +1,172 @@ # Getting Started -This guide will help you get started with the WebNN Python API. +rustnn is a Rust crate. This page goes from an empty project to a graph that runs on a backend, +then shows how to load graphs from files and how to use the command line tool. -## Installation +## Requirements -### From PyPI (Quick Start) +| Requirement | Needed for | +|---|---| +| Rust 1.97, pinned in `rust-toolchain.toml` (rustup installs it on the first build) | the crate (edition 2024) | +| `protoc`, the Protocol Buffers compiler, on `PATH` | `build.rs` compiles the ONNX and CoreML schemas | +| `flatc` | the `litert-runtime` feature only (TFLite schema) | +| Node.js | the WPT conformance tests only | +| A backend library | execution; see [Backends](backends.md). The ONNX Runtime shared library works on every platform and is the usual starting point | -Install PyWebNN with bundled ONNX Runtime (v0.4.0+): +On Windows run `git config --system core.longpaths true` before cloning; the repository +contains paths longer than 260 characters. -```bash -pip install pywebnn -``` - -Version 0.4.0+ includes bundled ONNX Runtime for immediate execution support. No additional dependencies needed! - -**Note:** Earlier versions (0.3.0 and below) required separate `onnxruntime` installation and had no execution backends. - -### Building from Source (Recommended for Full Features) - -#### Prerequisites - -- Python 3.11 or later -- Rust toolchain -- NumPy (automatically installed) -- ONNX Runtime 1.23+ (for execution support) - -#### Quick Setup with Makefile (Easiest) - -The Makefile handles everything automatically: - -```bash -# Clone the repository -git clone https://github.com/tarekziade/rustnn.git -cd rustnn - -# Install with ONNX Runtime support (downloads ONNX Runtime automatically) -make python-dev - -# Run tests to verify -make python-test -``` - -This creates a `.venv-webnn` virtual environment with everything configured. - -#### Manual Setup with Maturin - -1. **Install Rust** (if not already installed): - ```bash - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` - -2. **Clone and setup**: - ```bash - git clone https://github.com/tarekziade/rustnn.git - cd rustnn - pip install maturin - ``` - -3. **Build with features**: - ```bash - # With ONNX Runtime support (requires ONNX Runtime 1.23+) - maturin develop --features python,onnx-runtime - - # macOS: Add CoreML support - maturin develop --features python,onnx-runtime,coreml-runtime - - # Basic (validation/conversion only, no execution) - maturin develop --features python - ``` - -**Note:** When building with `onnx-runtime` feature, you need ONNX Runtime libraries available. The Makefile handles this automatically. For manual setup, see the [development guide](../development/setup.md). - -## Your First Graph - -Let's build a simple computational graph that adds two tensors and applies ReLU activation. - -### Step 1: Import and Setup - -```python -import webnn -import numpy as np +## Add the crate -# Create the ML namespace and context -ml = webnn.ML() -context = ml.create_context(accelerated=False, power_preference="default") +```toml +[dependencies] +rustnn = { version = "0.5", features = ["onnx-runtime"] } ``` -The `MLContext` represents the execution environment. Following the [W3C WebNN Device Selection spec](https://github.com/webmachinelearning/webnn/blob/main/device-selection-explainer.md), you provide hints: -- `accelerated`: `True` to request GPU/NPU, `False` for CPU-only -- `power_preference`: "default", "high-performance", or "low-power" +For unreleased changes use the git dependency: -The platform autonomously selects the actual device based on availability. - -### Step 2: Create a Graph Builder - -```python -# Create a graph builder -builder = context.create_graph_builder() +```toml +rustnn = { git = "https://github.com/rustnn/rustnn", features = ["onnx-runtime"] } ``` -The graph builder is used to construct computational graphs using a declarative API. +Features select backends. Without one the crate validates and converts graphs but cannot +execute them. The full feature list is in the crate documentation +([rustdoc](https://rustnn.github.io/rustnn/api/rustnn/)) and in [Backends](backends.md). -### Step 3: Define Inputs - -```python -# Define two input operands -x = builder.input("x", [2, 3], "float32") -y = builder.input("y", [2, 3], "float32") -``` +## Provide ONNX Runtime -Each input has: -- A **name** for identification -- A **shape** (list of dimensions) -- A **data type** ("float32", "float16", "int32", etc.) +The `onnx-runtime` feature loads the ONNX Runtime shared library at run time from the path in +`ORT_DYLIB_PATH`. In a clone of the repository, `make onnxruntime-download` fetches the release +pinned in the Makefile into `target/onnxruntime/`. Then point the variable at the library: -### Step 4: Build Operations - -```python -# Add the inputs -sum_result = builder.add(x, y) - -# Apply ReLU activation -output = builder.relu(sum_result) +```bash +# Linux +export ORT_DYLIB_PATH=$PWD/target/onnxruntime/onnxruntime-linux-x64-1.29.0/lib/libonnxruntime.so.1.29.0 +# macOS (Apple Silicon) +export ORT_DYLIB_PATH=$PWD/target/onnxruntime/onnxruntime-osx-arm64-1.29.0/lib/libonnxruntime.1.29.0.dylib +# Windows (Git Bash) +export ORT_DYLIB_PATH=$PWD/target/onnxruntime/onnxruntime-win-x64-1.29.0/lib/onnxruntime.dll ``` -Operations are chained to build the computational graph. - -### Step 5: Compile the Graph - -```python -# Compile the graph with named outputs -graph = builder.build({"output": output}) - -# Inspect the compiled graph -print(f"Graph has {graph.operand_count} operands") -print(f"Graph has {graph.operation_count} operations") -print(f"Inputs: {graph.get_input_names()}") -print(f"Outputs: {graph.get_output_names()}") +A library that is too old fails with a `BadVersion` error when the context is created. On +Windows the `onnxruntime.dll` in `System32` is such a library, so always set `ORT_DYLIB_PATH` +to a matching release. + +## First graph + +The program below computes `y = relu(x + 1)` for a 2x2 tensor. + +```rust +use rustnn::mlcontext::{ + MLContext, MLContextOptions, MLGraphBuilder, MLNamedOperands, MLNamedTensors, + MLOperandDescriptor, MLPowerPreference, MLTensorDescriptor, +}; +use rustnn::operator_enums::MLOperandDataType; + +fn main() -> rustnn::error::Result<()> { + // 1. Context: backend selection happens here. `accelerated = false` asks for a CPU device. + let options = MLContextOptions::new(MLPowerPreference::Default, false); + let mut context = MLContext::create(&options)?; + println!("backend: {:?}", context.rustnn_backend()); + + // 2. Builder: records operations for this context's backend. + let mut builder = MLGraphBuilder::new(&mut context)?; + let descriptor = MLOperandDescriptor::new(MLOperandDataType::Float32, vec![2, 2]); + let x = builder.input("x", &descriptor)?; + let one = builder.constant_from_slice(&descriptor, &[1.0f32; 4])?; + let sum = builder.add(x, one)?; + let y = builder.relu(sum)?; + + // 3. Build: names the outputs and compiles the graph. Output names are the dispatch keys. + let mut outputs = MLNamedOperands::new(); + outputs.insert("y", y); + let mut graph = builder.build(&outputs)?; + + // 4. Tensors: allocated by the context; the flags decide what the host may do with them. + let tensor = MLTensorDescriptor::new(MLOperandDataType::Float32, vec![2, 2]); + let x_tensor = context.create_tensor(&tensor.to_writable())?; + let y_tensor = context.create_tensor(&tensor.to_readable())?; + context.write_tensor(&x_tensor, &[-2.0f32, -1.0, 0.0, 1.0])?; + + // 5. Dispatch: bind tensors by name and run. + let mut inputs = MLNamedTensors::new(); + inputs.insert("x", &x_tensor); + let mut output_tensors = MLNamedTensors::new(); + output_tensors.insert("y", &y_tensor); + context.dispatch(&mut graph, &inputs, &output_tensors)?; + + // 6. Read back: the buffer must hold exactly the tensor's bytes. + let mut result = [0.0f32; 4]; + context.read_tensor(&y_tensor, &mut result)?; + assert_eq!(result, [0.0, 0.0, 1.0, 2.0]); + Ok(()) +} ``` -The `build()` method: -- Validates the graph structure -- Returns a compiled `MLGraph` object -- Takes a dictionary mapping output names to operands +Run it with a backend feature enabled, for example `cargo run --features onnx-runtime`. -### Step 6: Execute the Graph +Points worth knowing: -```python -import numpy as np +- Every builder call infers the output shape and data type immediately, so shape errors + surface at the call, not at build time. +- `build` consumes the recorded graph. A builder compiles exactly one graph; create a new + builder for the next one. +- The names passed to `input` and `build` are the keys that `dispatch` validates the tensor + bindings against. A missing input, a shape mismatch or a wrong data type is reported as + `Error::GraphDispatchError` before the backend runs. +- Graphs and tensors borrow the context; the borrow checker keeps them from outliving it. -# Prepare input data -x_data = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) -y_data = np.array([[1, 1, 1], [1, 1, 1]], dtype=np.float32) - -# Execute the graph with actual inputs -results = context.compute(graph, {"x": x_data, "y": y_data}) - -print("Input x:") -print(x_data) -print("\nInput y:") -print(y_data) -print("\nOutput (relu(x + y)):") -print(results["output"]) -# [[2. 3. 4.] -# [5. 6. 7.]] -``` +## Load a graph from a file -### Step 7: Export to Other Formats (Optional) +rustnn reads the `.webnn` text format and the JSON format of the +[webnn-graph](https://github.com/rustnn/webnn-graph) crate, including the exports of +[onnx2webnn](https://github.com/rustnn/onnx2webnn). Weights referenced with `@weights(...)` +are resolved from files next to the graph: `manifest.json` plus `model.weights`, or the +`.safetensors` file written by `rustnn_save_webnn`. -```python -# Export to ONNX for deployment -context.convert_to_onnx(graph, "my_model.onnx") -print("✓ ONNX model saved") +```rust +use rustnn::load_graph_from_path; +use rustnn::mlcontext::{MLContext, MLContextOptions, MLGraphBuilder, MLPowerPreference}; -# Export to CoreML (macOS only) -try: - context.convert_to_coreml(graph, "my_model.mlmodel") - print("✓ CoreML model saved") -except Exception as e: - print(f"CoreML conversion: {e}") +let graph_info = load_graph_from_path("model.webnn")?; +let mut context = MLContext::create(&MLContextOptions::new(MLPowerPreference::Default, true))?; +let mut builder = MLGraphBuilder::new(&mut context)?; +let mut graph = builder.build_graph_info(graph_info)?; +// graph.input_descriptors and graph.output_descriptors list the names and shapes to bind. ``` -## Complete Example +`build_graph_info` is a rustnn extension: it compiles an already complete graph, bypassing the +recording methods. The `examples/` directory contains `sample_graph.webnn`, `sample_graph.json` +and `toy_transformer.webnn` to try this with. -Here's the complete code with execution: +## Command line tool -```python -import webnn -import numpy as np +The `rustnn` binary validates a graph file, prints its inputs, outputs and dependency fan-out, +and optionally exports or executes it. It needs a runtime feature at build time. -def main(): - # Setup - ml = webnn.ML() - context = ml.create_context(accelerated=False) - builder = context.create_graph_builder() - - # Build graph: output = relu(x + y) - x = builder.input("x", [2, 3], "float32") - y = builder.input("y", [2, 3], "float32") - sum_result = builder.add(x, y) - output = builder.relu(sum_result) - - # Compile - graph = builder.build({"output": output}) - - print(f"✓ Graph compiled: {graph.operand_count} operands, " - f"{graph.operation_count} operations") - - # Execute with real data - x_data = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) - y_data = np.array([[1, 1, 1], [1, 1, 1]], dtype=np.float32) - results = context.compute(graph, {"x": x_data, "y": y_data}) +```bash +# Validate and describe +cargo run --features onnx-runtime -- examples/sample_graph.webnn - print(f"✓ Computed output:\n{results['output']}") +# Graphviz export +cargo run --features onnx-runtime -- examples/sample_graph.webnn --export-dot target/graph.dot +dot -Tpng target/graph.dot -o target/graph.png - # Optional: Export to ONNX - context.convert_to_onnx(graph, "model.onnx") - print(f"✓ Model exported to model.onnx") +# Convert; formats are onnx, coreml and, with their features, trtx, litert and cann +cargo run --features onnx-runtime -- examples/sample_graph.webnn --convert onnx --convert-output target/graph.onnx -if __name__ == "__main__": - main() +# Convert and execute once with zeroed inputs +cargo run --features onnx-runtime -- examples/sample_graph.webnn --convert onnx --run-onnx +cargo run --features onnx-runtime,trtx-runtime -- examples/sample_graph.webnn --convert onnx --run-trtx +cargo run --features coreml-runtime -- examples/sample_graph.webnn --convert coreml --run-coreml # macOS ``` -## Next Steps - -- Learn about all available operations in the [API Reference](api-reference.md) -- Explore more complex examples in [Examples](examples.md) -- Read about advanced topics in [Advanced Topics](advanced.md) - -## Common Issues +`--tensor-limit ` raises the validator's tensor byte limit for very large models. The +`make run`, `make viz`, `make onnx` and `make coreml` targets wrap these commands. -### Import Error +## Next steps -If you get `ModuleNotFoundError: No module named 'webnn'`: -- Make sure you ran `maturin develop` successfully -- Verify you're using the correct Python environment - -### Build Errors - -If maturin build fails: -- Ensure Rust is installed: `rustc --version` -- Update maturin: `pip install -U maturin` -- Check that you have the required features: `cargo check --features python` - -### NumPy Compatibility - -The library requires NumPy >= 1.20.0. Update if needed: -```bash -pip install -U numpy -``` +- [API Overview](api-reference.md): types, builder conventions and error types. +- [Backends](backends.md): selection rules and per-backend requirements. +- [Examples](examples.md): the example programs and short recipes. +- [Advanced Topics](advanced.md): backend options, dynamic shapes, saving graphs, caching and debugging. diff --git a/examples/fast_style_transfer_builder_api.rs b/examples/fast_style_transfer_builder_api.rs index d6308e26..a9ef2121 100644 --- a/examples/fast_style_transfer_builder_api.rs +++ b/examples/fast_style_transfer_builder_api.rs @@ -531,7 +531,7 @@ fn conv2d( ..Default::default() }; builder - .conv2_with_options(input, filter, options) + .conv2d_with_options(input, filter, options) .with_context(|| label.to_string()) } diff --git a/examples/images/CLAUDE.md b/examples/images/CLAUDE.md deleted file mode 100644 index adfdcb11..00000000 --- a/examples/images/CLAUDE.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Recent Activity - - - -*No recent activity* - \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index f3fc902a..86993e90 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: RustNN -site_description: Rust Implementation of the W3C WebNN API +site_description: Rust implementation of the W3C WebNN API site_author: Tarek Ziadé and contributors site_url: https://rustnn.github.io/rustnn/ @@ -44,16 +44,12 @@ theme: plugins: - search - - mkdocstrings: - handlers: - python: - options: - docstring_style: google - show_source: true -# Exclude files from the docs site +# Exclude files from the docs site. docs/archive holds superseded plans and +# investigation notes that reference removed APIs; they are kept for history only. exclude_docs: | README.md + archive/ markdown_extensions: - pymdownx.highlight: @@ -77,33 +73,36 @@ nav: - Home: index.md - User Guide: - Getting Started: user-guide/getting-started.md - - API Reference: user-guide/api-reference.md + - API Overview: user-guide/api-reference.md + - Backends: user-guide/backends.md - Examples: user-guide/examples.md - Advanced Topics: user-guide/advanced.md + - Rust API Reference (rustdoc): https://rustnn.github.io/rustnn/api/rustnn/ - Architecture: - Overview: architecture/overview.md - Development: - - Setup: development/setup.md + - Setup and Workflow: development/setup.md + - Documentation Policy: development/documentation-policy.md - Implementation Status: development/implementation-status.md - Backend Operator Support: development/backend-operator-support.md + - Flexible Input Shapes: development/flexible-input-shapes.md + - Code Coverage: development/code-coverage.md - Testing: - - Performance Benchmarks: testing/performance-benchmarks.md - - WPT Conformance: https://rustnn.github.io/rustnn/wpt-conformance/ + - WPT Conformance Guide: testing/wpt-test-guide.md + - WPT Conformance Dashboard: https://rustnn.github.io/rustnn/wpt-conformance/ - Integration: - - TensorRT: integration/tensorrt.md - - Windows TensorRT: integration/windows-tensorrt-setup.md - - GGML: integration/ggml.md + - TensorRT-RTX: integration/tensorrt.md + - Windows TensorRT-RTX Setup: integration/windows-tensorrt-setup.md - Reference: - - WebNN Spec: reference/webnn-spec.md - - IPC Design: reference/ipc-design.md + - WebNN Spec Notes: reference/webnn-spec.md + - WebNN Operators Explained: reference/operators-explanation.md + - IPC Design Notes: reference/ipc-design.md - Changelog: https://github.com/rustnn/rustnn/blob/main/CHANGELOG.md extra: social: - icon: fontawesome/brands/github link: https://github.com/rustnn/rustnn - - icon: fontawesome/brands/python - link: https://pypi.org/project/webnn/ version: provider: mike diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 009674cf..00000000 --- a/pytest.ini +++ /dev/null @@ -1,22 +0,0 @@ -[pytest] -markers = - wpt: WebNN W3C Web Platform Tests - requires_onnx_runtime: Test requires ONNX Runtime - requires_coreml_runtime: Test requires CoreML Runtime - benchmark: Performance benchmark tests - slow: Slow-running tests (excluded by default) - -# Test collection patterns -python_files = test_*.py -python_classes = Test* -python_functions = test_* - -# Display options -console_output_style = progress -addopts = - -v - --tb=short - --strict-markers - -# Ignore patterns -norecursedirs = .git .venv .venv-* target build dist *.egg-info diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 37194c3c..c9116135 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -5,6 +5,9 @@ # - .github/workflows/wpt-conformance.yml # - .github/workflows/wpt-conformance-nightly.yml # - .github/workflows/rustnnpt-gate.yml +# - .github/workflows/snapshot-sync.yml +# - .github/workflows/docs.yml +# - .github/workflows/docs-pr.yml # Note: .github/workflows/publish.yml intentionally uses `stable`. channel = "1.97.0" components = ["rustfmt", "clippy", "rust-src"] diff --git a/scripts/generate_backend_operator_report.py b/scripts/generate_backend_operator_report.py index dcef4f6f..a2f5ac56 100644 --- a/scripts/generate_backend_operator_report.py +++ b/scripts/generate_backend_operator_report.py @@ -1,8 +1,20 @@ #!/usr/bin/env python3 """ -Generate docs/development/backend-operator-support.md from converter sources. +Generate docs/development/backend-operator-support.md from the converter sources. -This script is intentionally source-driven to reduce documentation drift. +The report is source-driven so that it cannot drift from the code: CI runs this script +with --check and fails when the committed report differs from the generated one. + +Operation names come from `Operation::op_type()` in src/operators.rs. Per backend, an +operation counts as supported when: + +- ONNX Runtime, LiteRT: the converter source references the `Operation::` + (these converters dispatch on the enum and have no "unsupported operation" fallthrough + for referenced variants). +- CoreML: as above, plus operation names the converter compares against its lower-cased + op type (`get_mil_op_type` keys, `op_type_lower == "..."`, `matches!(op_type_lower...)`). +- TensorRT: the operation name is a key of the converter's `match op_type { ... }` table. +- CANN: the variant is listed in `is_supported_op` in src/converters/cann.rs. """ from __future__ import annotations @@ -13,244 +25,247 @@ import sys from dataclasses import dataclass - ROOT = pathlib.Path(__file__).resolve().parents[1] +OPERATORS_SRC = ROOT / "src/operators.rs" ONNX_SRC = ROOT / "src/converters/onnx.rs" COREML_SRC = ROOT / "src/converters/coreml_mlprogram.rs" TRTX_SRC = ROOT / "src/converters/trtx.rs" +LITERT_SRC = ROOT / "src/converters/litert.rs" +CANN_SRC = ROOT / "src/converters/cann.rs" OUTPUT = ROOT / "docs/development/backend-operator-support.md" +# Builder entry points that are not graph operations. +EXCLUDED_OPS = {"constant"} -EXCLUDED_OPS = { - # Internal/pseudo ops, not user-facing WebNN operators. - "constant", - "shape", +# Operations rustnn keeps beyond the current WebNN specification. +EXTENSION_OPS = { + "shape": "rustnn extension used by onnx2webnn exports", + "squeeze": "removed from the WebNN spec (emulation appendix), kept for onnx2webnn", + "unsqueeze": "removed from the WebNN spec (emulation appendix), kept for onnx2webnn", } - -DISPLAY_OVERRIDES = { - "convtranspose2d": "convTranspose2d", - "averagepool2d": "averagePool2d", - "maxpool2d": "maxPool2d", - "globalaveragepool": "globalAveragePool", - "globalmaxpool": "globalMaxPool", - "batchnormalization": "batchNormalization", - "instancenormalization": "instanceNormalization", - "layernormalization": "layerNormalization", - "hardsigmoid": "hardSigmoid", - "hardswish": "hardSwish", - "leakyrelu": "leakyRelu", - "logicaland": "logicalAnd", - "logicalor": "logicalOr", - "logicalxor": "logicalXor", - "logicalnot": "logicalNot", - "greaterorequal": "greaterOrEqual", - "lesserorequal": "lesserOrEqual", - "quantizelinear": "quantizeLinear", - "dequantizelinear": "dequantizeLinear", - "scatterelements": "scatterElements", - "scatternd": "scatterND", - "gatherelements": "gatherElements", - "gathernd": "gatherND", - "argmax": "argMax", - "argmin": "argMin", - "roundeven": "roundEven", - "resample2d": "resample2d", - "cumulativesum": "cumulativeSum", - "isnan": "isNaN", - "isinfinite": "isInfinite", - "notequal": "notEqual", -} +OP_TYPE_MARKER = "pub fn op_type(&self) -> &'static str {" +TRTX_MARKER = "match op_type {" +COREML_MARKER = "let mil_type = match webnn_op.to_lowercase().as_str() {" +CANN_MARKER = "pub(crate) fn is_supported_op(op: &Operation) -> bool {" @dataclass -class BackendOps: - backend: str - converter_ops: list[str] - executor_ops: list[str] - converter_source: str - executor_source: str - +class Backend: + name: str + source: pathlib.Path + rule: str + supported: set[str] # normalized operation names -def _normalize(op: str) -> str: - return re.sub(r"[^a-z0-9]", "", op.lower()) +def normalize(name: str) -> str: + return re.sub(r"[^a-z0-9]", "", name.lower()) -def _display(op: str) -> str: - n = _normalize(op) - return DISPLAY_OVERRIDES.get(n, op) +def extract_brace_block_after_marker(text: str, marker: str) -> str: + start = text.find(marker) + if start < 0: + raise RuntimeError(f"Could not find marker: {marker}") + brace_open = text.find("{", start) + if brace_open < 0: + raise RuntimeError(f"Could not find opening brace after marker: {marker}") -def _extract_quoted_ops(text: str, pattern: str) -> list[str]: - values: list[str] = [] - for match in re.finditer(pattern, text, flags=re.MULTILINE): - values.append(match.group(1)) - return values + depth = 0 + for i in range(brace_open, len(text)): + ch = text[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return text[brace_open + 1 : i] + raise RuntimeError(f"Unbalanced braces while parsing marker: {marker}") -def _collect_from_matches_macro(text: str) -> list[str]: - values: list[str] = [] - pattern = re.compile(r"matches!\(\s*op\.op_type\.as_str\(\),([\s\S]*?)\)") - for match in pattern.finditer(text): - block = match.group(1) - for q in re.finditer(r'"([A-Za-z0-9_]+)"', block): - values.append(q.group(1)) - return values +def parse_operation_names(operators_text: str) -> dict[str, str]: + """Map `Operation` variant name -> WebNN operation name from `op_type()`.""" + block = extract_brace_block_after_marker(operators_text, OP_TYPE_MARKER) + names: dict[str, str] = {} + for match in re.finditer( + r'Operation::([A-Za-z0-9]+)\s*\{\s*\.\.\s*\}\s*=>\s*"([A-Za-z0-9]+)"', block + ): + names[match.group(1)] = match.group(2) + if not names: + raise RuntimeError("No Operation variants found in op_type()") + return names -def parse_onnx_ops(text: str) -> list[str]: - ops: list[str] = [] - ops.extend( - _extract_quoted_ops(text, r'op\.op_type\s*==\s*"([A-Za-z0-9_]+)"') - ) - ops.extend( - _extract_quoted_ops( - text, r'op\.op_type\.eq_ignore_ascii_case\("([A-Za-z0-9_]+)"\)' - ) - ) - ops.extend(_collect_from_matches_macro(text)) - return canonicalize_ops(ops) +def parse_variant_references(text: str, variants: dict[str, str]) -> set[str]: + """Operations whose `Operation::` is referenced anywhere in `text`.""" + found: set[str] = set() + for match in re.finditer(r"Operation::([A-Z][A-Za-z0-9]*)", text): + variant = match.group(1) + if variant in variants: + found.add(normalize(variants[variant])) + return found -def parse_coreml_ops(text: str) -> list[str]: - marker = "let mil_type = match webnn_op.to_lowercase().as_str() {" +def parse_dispatch_table(text: str, marker: str) -> set[str]: + """String keys of the match arms in the block after `marker` (e.g. `"add" =>`).""" block = extract_brace_block_after_marker(text, marker) - ops = [q.group(1) for q in re.finditer(r'"([A-Za-z0-9_]+)"\s*=>', block)] - return canonicalize_ops(ops) + keys = re.findall(r'"([A-Za-z0-9_]+)"\s*(?=\||=>)', block) + return {normalize(key) for key in keys} -def parse_trtx_ops(text: str) -> list[str]: - marker = "match op_type {" +def parse_variant_list(text: str, marker: str, variants: dict[str, str]) -> set[str]: + """Variants listed inside the block after `marker` (e.g. a `matches!` gate).""" block = extract_brace_block_after_marker(text, marker) - ops = [q.group(1) for q in re.finditer(r'"([A-Za-z0-9_]+)"\s*=>', block)] - return canonicalize_ops(ops) + return parse_variant_references(block, variants) -def extract_brace_block_after_marker(text: str, marker: str) -> str: - start = text.find(marker) - if start < 0: - raise RuntimeError(f"Could not find marker: {marker}") - brace_open = text.find("{", start) - if brace_open < 0: - raise RuntimeError(f"Could not find opening brace after marker: {marker}") - +def extract_paren_block(text: str, start: int) -> str: + """Text inside the parentheses that open at or after `start`.""" + paren_open = text.find("(", start) + if paren_open < 0: + raise RuntimeError("Could not find opening parenthesis") depth = 0 - i = brace_open - while i < len(text): + for i in range(paren_open, len(text)): ch = text[i] - if ch == "{": + if ch == "(": depth += 1 - elif ch == "}": + elif ch == ")": depth -= 1 if depth == 0: - return text[brace_open + 1 : i] - i += 1 - raise RuntimeError(f"Unbalanced braces while parsing marker: {marker}") + return text[paren_open + 1 : i] + raise RuntimeError("Unbalanced parentheses") + + +def parse_op_name_comparisons(text: str, ident: str, known: set[str]) -> set[str]: + """Operation names compared against the lower-cased op type variable `ident`. + + Covers `ident == "name"` and `matches!(ident.as_str(), "a" | "b")`. Only names that + are known operations are returned, so unrelated string literals are ignored. + """ + found: set[str] = set() + for match in re.finditer(re.escape(ident) + r'\s*==\s*"([a-z0-9_]+)"', text): + found.add(normalize(match.group(1))) + for match in re.finditer(r"matches!\(\s*" + re.escape(ident) + r"(?:\.as_str\(\))?\s*,", text): + block = extract_paren_block(text, match.start()) + for quoted in re.finditer(r'"([a-z0-9_]+)"', block): + found.add(normalize(quoted.group(1))) + return found & known + + +def parse_coreml_ops(text: str, variants: dict[str, str]) -> set[str]: + """CoreML dispatches on the enum in some passes and on the lower-cased op name in others.""" + known = {normalize(name) for name in variants.values()} + supported = parse_variant_references(text, variants) + supported |= parse_dispatch_table(text, COREML_MARKER) & known + supported |= parse_op_name_comparisons(text, "op_type_lower", known) + return supported + + +def build_backends(variants: dict[str, str]) -> list[Backend]: + return [ + Backend( + name="ONNX Runtime", + source=ONNX_SRC, + rule="`Operation` variants referenced by the converter", + supported=parse_variant_references(ONNX_SRC.read_text(encoding="utf-8"), variants), + ), + Backend( + name="CoreML", + source=COREML_SRC, + rule="`Operation` variants referenced by the converter, plus names in its op-type dispatch", + supported=parse_coreml_ops(COREML_SRC.read_text(encoding="utf-8"), variants), + ), + Backend( + name="TensorRT", + source=TRTX_SRC, + rule="keys of the `match op_type` dispatch table", + supported=parse_dispatch_table(TRTX_SRC.read_text(encoding="utf-8"), TRTX_MARKER), + ), + Backend( + name="LiteRT", + source=LITERT_SRC, + rule="`Operation` variants referenced by the converter", + supported=parse_variant_references( + LITERT_SRC.read_text(encoding="utf-8"), variants + ), + ), + Backend( + name="CANN", + source=CANN_SRC, + rule="variants accepted by `is_supported_op`", + supported=parse_variant_list( + CANN_SRC.read_text(encoding="utf-8"), CANN_MARKER, variants + ), + ), + ] -def canonicalize_ops(ops: list[str]) -> list[str]: - by_norm: dict[str, str] = {} - for raw in ops: - n = _normalize(raw) - if not n or n in EXCLUDED_OPS: - continue - current = by_norm.get(n) - if current is None: - by_norm[n] = raw - continue - # Prefer camelCase-ish forms over all-lower where available. - if any(c.isupper() for c in raw) and not any(c.isupper() for c in current): - by_norm[n] = raw - - display_ops = [_display(v) for v in by_norm.values()] - return sorted(display_ops, key=lambda s: s.lower()) - - -def bullet_columns(items: list[str], cols: int = 3) -> str: - if not items: - return "- (none)\n" - rows = (len(items) + cols - 1) // cols - table: list[list[str]] = [["" for _ in range(cols)] for _ in range(rows)] - for idx, item in enumerate(items): - r = idx % rows - c = idx // rows - table[r][c] = f"`{item}`" - lines = [] - for row in table: - vals = [v for v in row if v] - lines.append("- " + ", ".join(vals)) - return "\n".join(lines) + "\n" - - -def render(backends: list[BackendOps]) -> str: +def render(variants: dict[str, str], backends: list[Backend]) -> str: + operations = sorted( + (name for name in variants.values() if name.lower() not in EXCLUDED_OPS), + key=lambda s: s.lower(), + ) + keys = {op: normalize(op) for op in operations} + out: list[str] = [] out.append("# Backend Operator Support Report") out.append("") out.append( - "This file is generated from converter sources by " + "This file is generated from the converter sources by " "`scripts/generate_backend_operator_report.py`." ) out.append( - "Do not edit this file manually. Run `make docs-backend-ops` after backend changes." + "Do not edit it manually. Run `make docs-backend-ops` after backend changes; " + "CI fails on drift (`make docs-backend-ops-check`)." + ) + out.append("") + out.append( + "Operation names are the WebNN builder names returned by `Operation::op_type()` " + "in `src/operators.rs`. \"Supported\" means the converter emits a lowering for the " + "operation. Data type restrictions, dynamic shape limits and known failing cases are " + "tracked per backend in `tests/wpt_conformance/*_expected_failures.txt` and on the " + "[WPT conformance dashboard](https://rustnn.github.io/rustnn/wpt-conformance/)." ) out.append("") + out.append("## Summary") + out.append("") + out.append("| Backend | Converter source | Detection rule | Supported |") + out.append("|---|---|---|---|") for b in backends: - out.append(f"## {b.backend}") - out.append("") - out.append(f"- Converter source: `{b.converter_source}`") - out.append(f"- Executor source: `{b.executor_source}`") - out.append(f"- Converter operator count: **{len(b.converter_ops)}**") - out.append(f"- Executor operator count: **{len(b.executor_ops)}**") - out.append("") - out.append("### Converter Operators") - out.append("") - out.append(bullet_columns(b.converter_ops).rstrip()) - out.append("") - out.append("### Executor Operators") - out.append("") - if b.executor_ops == b.converter_ops: - out.append( - "Executor-level operator coverage follows converter coverage for this backend." - ) - out.append("") - out.append(bullet_columns(b.executor_ops).rstrip()) - out.append("") + count = sum(1 for op in operations if keys[op] in b.supported) + rel = b.source.relative_to(ROOT).as_posix() + out.append(f"| {b.name} | `{rel}` | {b.rule} | {count} of {len(operations)} |") + out.append("") + out.append("## Operation matrix") + out.append("") + header = "| Operation | " + " | ".join(b.name for b in backends) + " |" + out.append(header) + out.append("|---|" + "|".join(":-:" for _ in backends) + "|") + for op in operations: + cells = ["yes" if keys[op] in b.supported else "-" for b in backends] + out.append(f"| `{op}` | " + " | ".join(cells) + " |") + out.append("") + out.append("## Unsupported operations per backend") + out.append("") + for b in backends: + missing = [op for op in operations if keys[op] not in b.supported] + if missing: + out.append(f"- {b.name}: " + ", ".join(f"`{op}`" for op in missing)) + else: + out.append(f"- {b.name}: none") + out.append("") + out.append("## Notes") + out.append("") + for op in operations: + note = EXTENSION_OPS.get(op.lower()) + if note: + out.append(f"- `{op}`: {note}.") + out.append("") return "\n".join(out) + "\n" def build_report() -> str: - onnx_text = ONNX_SRC.read_text(encoding="utf-8") - coreml_text = COREML_SRC.read_text(encoding="utf-8") - trtx_text = TRTX_SRC.read_text(encoding="utf-8") - - onnx_ops = parse_onnx_ops(onnx_text) - coreml_ops = parse_coreml_ops(coreml_text) - trtx_ops = parse_trtx_ops(trtx_text) - - backends = [ - BackendOps( - backend="ONNX Runtime Backend", - converter_ops=onnx_ops, - executor_ops=onnx_ops, - converter_source="src/converters/onnx.rs", - executor_source="src/executors/onnx.rs", - ), - BackendOps( - backend="CoreML MLProgram Backend", - converter_ops=coreml_ops, - executor_ops=coreml_ops, - converter_source="src/converters/coreml_mlprogram.rs", - executor_source="src/executors/coreml.rs", - ), - BackendOps( - backend="TensorRT Backend", - converter_ops=trtx_ops, - executor_ops=trtx_ops, - converter_source="src/converters/trtx.rs", - executor_source="src/executors/trtx.rs", - ), - ] - return render(backends) + variants = parse_operation_names(OPERATORS_SRC.read_text(encoding="utf-8")) + return render(variants, build_backends(variants)) def main() -> int: diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit index ac7cb44f..64f703b1 100755 --- a/scripts/git-hooks/pre-commit +++ b/scripts/git-hooks/pre-commit @@ -1,5 +1,5 @@ #!/bin/bash -# Pre-commit hook to ensure Rust and Python quality checks run only when needed +# Pre-commit hook: run the Rust quality checks when Rust files are staged. # # To install this hook, run: # cp scripts/git-hooks/pre-commit .git/hooks/pre-commit @@ -19,10 +19,6 @@ has_rust_changes() { echo "$STAGED_FILES" | grep -E '\.rs$|Cargo\.toml|build\.rs' >/dev/null } -has_python_changes() { - echo "$STAGED_FILES" | grep -E '\.py$|\.pyi$' >/dev/null -} - if [ -z "$STAGED_FILES" ]; then echo "No staged files; skipping checks." exit 0 @@ -36,10 +32,5 @@ if has_rust_changes; then cargo clippy --all-targets -- -D warnings fi -if has_python_changes; then - echo "Running ty type check (python-ty-check)..." - make python-ty-check -fi - -echo "✅ Pre-commit checks passed (skipped sections if no relevant changes)." +echo "[OK] Pre-commit checks passed (sections skipped when no relevant files are staged)." exit 0 diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh index 927b36e6..62b14692 100755 --- a/scripts/install-git-hooks.sh +++ b/scripts/install-git-hooks.sh @@ -12,9 +12,9 @@ echo "Installing git hooks..." cp "$SCRIPT_DIR/git-hooks/pre-commit" "$PROJECT_ROOT/.git/hooks/pre-commit" chmod +x "$PROJECT_ROOT/.git/hooks/pre-commit" -echo "✅ Git hooks installed successfully!" +echo "[OK] Git hooks installed." echo "" echo "The following hooks are now active:" -echo " - pre-commit: Runs 'cargo fmt --check' + 'cargo clippy' for Rust changes, and 'make python-ty-check' for Python changes" +echo " - pre-commit: runs 'cargo fmt --check' and 'cargo clippy' when Rust files are staged" echo "" echo "To bypass the hook (not recommended), use: git commit --no-verify" diff --git a/scripts/test_generate_backend_operator_report.py b/scripts/test_generate_backend_operator_report.py index 1a9dc2a8..5bcdb656 100644 --- a/scripts/test_generate_backend_operator_report.py +++ b/scripts/test_generate_backend_operator_report.py @@ -6,48 +6,83 @@ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) from scripts.generate_backend_operator_report import ( # noqa: E402 - canonicalize_ops, - parse_coreml_ops, - parse_trtx_ops, + normalize, + parse_dispatch_table, + parse_op_name_comparisons, + parse_operation_names, + parse_variant_list, + parse_variant_references, ) +OPERATORS_SNIPPET = """ +impl Operation { + pub fn op_type(&self) -> &'static str { + match self { + Operation::Add { .. } => "add", + Operation::ConvTranspose2d { .. } => "convTranspose2d", + Operation::IsNaN { .. } => "isNaN", + Operation::Constant { .. } => "constant", + } + } +} +""" + class BackendOperatorReportTests(unittest.TestCase): - def test_canonicalize_prefers_camel_case(self) -> None: - ops = canonicalize_ops(["convtranspose2d", "convTranspose2d"]) - self.assertIn("convTranspose2d", ops) - self.assertNotIn("convtranspose2d", ops) + def setUp(self) -> None: + self.variants = parse_operation_names(OPERATORS_SNIPPET) + + def test_parse_operation_names(self) -> None: + self.assertEqual(self.variants["ConvTranspose2d"], "convTranspose2d") + self.assertEqual(self.variants["IsNaN"], "isNaN") - def test_parse_coreml_extracts_known_ops(self) -> None: + def test_variant_references_ignore_associated_functions(self) -> None: text = """ - fn get_mil_op_type(&self, webnn_op: &str) -> Result<&'static str, GraphError> { - let mil_type = match webnn_op.to_lowercase().as_str() { - "add" => mil_ops::ADD, - "linear" => mil_ops::MUL, - _ => { return Err(GraphError::ConversionFailed { format: String::new(), reason: String::new()}); } - }; - Ok(mil_type) + let op = Operation::from_legacy(a, b); + match op { + Operation::Add { a, b, .. } => add(a, b), + Operation::IsNaN { input, .. } => is_nan(input), } """ - parsed = parse_coreml_ops(text) - self.assertIn("add", parsed) - self.assertIn("linear", parsed) + self.assertEqual( + parse_variant_references(text, self.variants), {"add", "isnan"} + ) - def test_parse_trtx_extracts_known_ops(self) -> None: + def test_dispatch_table_handles_or_patterns(self) -> None: text = """ fn add_single_operation(&self) { match op_type { "add" => foo()?, - "isNaN" => bar()?, - _ => { return Err(GraphError::ConversionFailed { format: String::new(), reason: String::new()}); } + "isNaN" | "isInfinite" => bar()?, + _ => { return Err(GraphError::ConversionFailed { format: "x".into(), reason: "y".into() }); } } } """ - parsed = parse_trtx_ops(text) - self.assertIn("add", parsed) - self.assertIn("isNaN", parsed) + parsed = parse_dispatch_table(text, "match op_type {") + self.assertEqual(parsed, {"add", "isnan", "isinfinite"}) + + def test_op_name_comparisons_filter_unknown_strings(self) -> None: + text = """ + if matches!(op_type_lower.as_str(), "equal" | "notequal" | "isnan") { cast_bool(); } + if op_type_lower == "convtranspose2d" { fix_layout(); } + let reason = "not supported"; + """ + known = {normalize(name) for name in self.variants.values()} + parsed = parse_op_name_comparisons(text, "op_type_lower", known) + self.assertEqual(parsed, {"convtranspose2d", "isnan"}) + + def test_variant_list_reads_only_the_gate(self) -> None: + text = """ + pub(crate) fn is_supported_op(op: &Operation) -> bool { + matches!(op, Operation::Add { .. } | Operation::ConvTranspose2d { .. }) + } + fn elsewhere(op: &Operation) { let _ = Operation::IsNaN { input: 0, options: None, outputs: vec![] }; } + """ + parsed = parse_variant_list( + text, "pub(crate) fn is_supported_op(op: &Operation) -> bool {", self.variants + ) + self.assertEqual(parsed, {"add", "convtranspose2d"}) if __name__ == "__main__": unittest.main() - diff --git a/site/404.html b/site/404.html deleted file mode 100644 index 8dd1f7e5..00000000 --- a/site/404.html +++ /dev/null @@ -1,1299 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - WebNN Python API - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- -
- - - - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
-
- - - -
-
-
- - - -
- -
- -

404 - Not found

- -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/site/architecture/overview/index.html b/site/architecture/overview/index.html deleted file mode 100644 index 52168a60..00000000 --- a/site/architecture/overview/index.html +++ /dev/null @@ -1,2120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Overview - WebNN Python API - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - - - -

Architecture

-

Core Components

-
┌─────────────────────────────────────────────────────────────┐
-│ CLI (main.rs) / Library API (lib.rs) / Python API (PyO3)    │
-└──────────────┬──────────────────────────────────────────────┘
-               │
-    ┌──────────┴──────────┬──────────────┬─────────────────┐
-    ▼                     ▼              ▼                 ▼
-┌────────┐     ┌──────────────┐   ┌──────────┐    ┌──────────────┐
-│Loader  │────▶│  Validator   │──▶│ Context  │───▶│  Backend     │
-│(JSON)  │     │(graph.rs)    │   │(selects) │    │  Selection   │
-└────────┘     └──────────────┘   └────┬─────┘    └──────┬───────┘
-                                        │                 │
-                                        ▼                 ▼
-                                  ┌──────────┐    ┌──────────────┐
-                                  │ Builder  │    │  Converter   │
-                                  │(backend- │    │  (Runtime)   │
-                                  │agnostic) │    │              │
-                                  └────┬─────┘    └──────┬───────┘
-                                       │                 │
-                                       ▼                 ▼
-                              ┌─────────────┐   ┌────────────────┐
-                              │  MLGraph    │   │ ONNX / CoreML  │
-                              │(immutable)  │   │   Execution    │
-                              └─────────────┘   └────────────────┘
-
-

Key Principles

-

1. Backend-Agnostic Graph Representation

-
    -
  • builder.build() creates an immutable, platform-independent GraphInfo structure
  • -
  • Contains operands, operations, inputs, outputs, and constant data
  • -
  • No backend-specific artifacts at this stage
  • -
-

2. Runtime Backend Selection (WebNN Spec-Compliant)

-

Following the W3C WebNN Device Selection Explainer:

-
    -
  • Backend selection happens at context creation via accelerated and power_preference hints
  • -
  • accelerated=False → ONNX Runtime CPU
  • -
  • accelerated=True + power="high-performance" → GPU preferred (ONNX or CoreML)
  • -
  • accelerated=True + power="low-power" → NPU preferred (CoreML Neural Engine on Apple Silicon)
  • -
  • Platform autonomously selects actual device based on availability and runtime conditions
  • -
  • Selection logic in PyMLContext::select_backend()
  • -
-

3. MLTensor Management

-

Following the W3C WebNN MLTensor Explainer:

-
    -
  • Explicit tensor management with descriptor flags (readable, writable, exportableToGPU)
  • -
  • destroy() method for explicit resource cleanup
  • -
  • dispatch() for async execution with MLTensor inputs/outputs
  • -
  • Permission enforcement on read/write operations
  • -
-

4. Lazy Backend Conversion

-
    -
  • Backend-specific conversion happens during compute(), not build()
  • -
  • compute() routes to appropriate backend method:
  • -
  • compute_onnx() for ONNX Runtime
  • -
  • compute_coreml() for CoreML
  • -
  • compute_fallback() when no backend available
  • -
  • Same graph can be executed on different backends via different contexts
  • -
-

5. Rust-First Architecture

-
    -
  • All core functionality in pure Rust (validation, conversion, execution)
  • -
  • Python bindings are thin wrappers exposing Rust functionality
  • -
  • Rust library usable independently without Python
  • -
  • Design principle: "Rust is the implementation, Python is the interface"
  • -
-

Shape Inference

-

Shape inference is the process of automatically computing output tensor shapes of neural network operations based on their input shapes and operation parameters, without executing the operation.

-

Why Shape Inference Matters

-

Shape inference enables:

-
    -
  1. Early validation - Catch shape mismatches at build time, not runtime
  2. -
  3. Memory allocation - Backend runtimes know output buffer sizes before execution
  4. -
  5. Graph optimization - Enables static analysis and optimization passes
  6. -
  7. Self-describing graphs - Graphs are fully annotated and backend-agnostic
  8. -
-

How It Works

-

Each WebNN operation has a shape inference function in src/shape_inference.rs that computes output shapes. Shape inference happens during graph building, before any backend selection or execution.

-

Binary Operations (add, mul, div, etc.): -- Use NumPy-style broadcasting rules -- Two dimensions are compatible if equal or one is 1 -- Output dimension is the maximum of the two -

// broadcast_shapes([3, 1, 5], [3, 4, 5]) → [3, 4, 5]
-// The dimension 1 broadcasts to 4
-

-

Matrix Multiplication: -

// Simple 2D: [M, K] @ [K, N] → [M, N]
-infer_matmul_shape([2, 3], [3, 4]) → [2, 4]
-
-// Batched: [batch, M, K] @ [batch, K, N] → [batch, M, N]
-infer_matmul_shape([5, 2, 3], [5, 3, 4]) → [5, 2, 4]
-
-// Validates inner dimensions match (K must equal)
-infer_matmul_shape([2, 3], [4, 5]) → Error: 3 != 4
-

-

Convolution (conv2d): -- Takes input shape, filter shape, strides, padding, dilations -- Computes spatial output dimensions: -

output_h = floor((input_h + pad_top + pad_bottom - dilation_h * (kernel_h - 1) - 1) / stride_h + 1)
-output_w = floor((input_w + pad_left + pad_right - dilation_w * (kernel_w - 1) - 1) / stride_w + 1)
-
-- Validates channel compatibility and group constraints -- Handles multiple layouts: NCHW, NHWC (inputs) and OIHW, HWIO, OHWI, IHWO (filters)

-

Reshape: -

// Validates element count is preserved
-validate_reshape([2, 3, 4], [6, 4]) → OK (24 elements in both)
-validate_reshape([2, 3, 4], [5, 5]) → Error (24 != 25 elements)
-

-

Pooling Operations: -- Similar to convolution but without filters -- Computes output spatial dimensions based on window size, strides, padding -- Handles both average and max pooling -- Global pooling reduces spatial dimensions to 1x1

-

Integration with Graph Builder

-

Shape inference is called automatically during graph construction:

-
# Python API example
-x = builder.input("x", [2, 3], "float32")    # Shape: [2, 3]
-y = builder.input("y", [3, 4], "float32")    # Shape: [3, 4]
-z = builder.matmul(x, y)                     # Shape: [2, 4] (inferred)
-output = builder.relu(z)                     # Shape: [2, 4] (preserved)
-
-

When you call builder.matmul(x, y), the implementation: -1. Calls infer_matmul_shape([2, 3], [3, 4]) from src/shape_inference.rs -2. Gets result [2, 4] -3. Creates operand descriptor with inferred shape -4. Stores operation in graph with validated inputs/outputs

-

This creates a fully-annotated, backend-agnostic graph that can be: -- Validated for correctness -- Visualized with Graphviz -- Converted to ONNX, CoreML, or other formats -- Executed on different backends without re-inference

-

Implementation Status

-

All 85 WebNN operations have shape inference implemented (100% coverage). Each operation includes: -- Shape inference function in src/shape_inference.rs -- Comprehensive validation (dimension compatibility, parameter constraints) -- Unit tests covering typical cases and edge cases -- Error messages with context for debugging

-

File Organization

-
src/
-├── lib.rs              # Public Rust API exports
-├── main.rs             # CLI entry point
-├── graph.rs            # Core data structures (backend-agnostic)
-├── error.rs            # Error types
-├── validator.rs        # Graph validation
-├── loader.rs           # JSON loading
-├── graphviz.rs         # DOT export
-├── protos.rs           # Protobuf module setup
-├── converters/
-│   ├── mod.rs          # Registry and trait
-│   ├── onnx.rs         # ONNX converter
-│   └── coreml.rs       # CoreML converter
-├── executors/
-│   ├── mod.rs          # Conditional compilation
-│   ├── onnx.rs         # ONNX runtime
-│   └── coreml.rs       # CoreML runtime
-└── python/             # Python bindings (PyO3)
-    ├── mod.rs          # Python module definition
-    ├── context.rs      # ML and MLContext classes (backend selection)
-    ├── graph_builder.rs # MLGraphBuilder class
-    ├── graph.rs        # MLGraph class
-    ├── operand.rs      # MLOperand class
-    └── tensor.rs       # MLTensor class
-
-python/webnn/           # Python package
-├── __init__.py         # Package exports (AsyncMLContext)
-└── __init__.pyi        # Type stubs
-
-tests/
-├── test_python_api.py  # Python API tests (320+ tests)
-├── test_wpt_conformance.py # WPT spec compliance tests
-└── test_integration.py # Integration tests
-
-examples/
-├── python_simple.py          # Basic Python example
-├── python_matmul.py          # Matrix multiplication
-├── mobilenetv2_complete.py   # Complete pretrained MobileNetV2
-├── text_generation_gpt.py    # Transformer with attention
-└── train_text_model.py       # Model training script
-
-

Design Patterns

-

Registry Pattern (Converters)

-
    -
  • ConverterRegistry manages converters dynamically
  • -
  • Trait objects: Box<dyn GraphConverter + Send + Sync>
  • -
  • Extensible without modifying core code
  • -
-

Builder Pattern (Graph Construction)

-
    -
  • MLGraphBuilder provides fluent API for graph construction
  • -
  • Incremental construction of complex structures
  • -
  • Used in ONNX and CoreML converters
  • -
-

Validation Pipeline

-
    -
  • Immutable graph input
  • -
  • Stateful validator with progressive checks
  • -
  • Comprehensive artifacts returned for downstream use
  • -
-

Conditional Compilation

-
    -
  • #[cfg(target_os = "macos")] for platform-specific code
  • -
  • #[cfg(feature = "...")] for optional features
  • -
  • Graceful degradation on unsupported platforms
  • -
-

Technical Decisions

-
    -
  1. WebNN Spec Compliance: Follows W3C WebNN Device Selection and MLTensor explainers
  2. -
  3. Protobuf for Interop: Native format for ONNX and CoreML
  4. -
  5. Compile-time Codegen: Protobufs compiled at build time
  6. -
  7. Feature Flags: Optional runtimes to minimize dependencies
  8. -
  9. Objective-C FFI: Direct CoreML access on macOS
  10. -
  11. Zero-copy where possible: Bytes type for efficiency
  12. -
  13. Registry Pattern: Pluggable converters without core changes
  14. -
-

Platform Support

-
    -
  • Validation & Conversion: Cross-platform (Linux, macOS, Windows)
  • -
  • ONNX Execution: Cross-platform with onnx-runtime feature (CPU/GPU)
  • -
  • CoreML Execution: macOS only with coreml-runtime feature (GPU/Neural Engine)
  • -
  • Neural Engine: macOS with Apple Silicon (via CoreML)
  • -
  • Python Bindings: Cross-platform with python feature (Python 3.11+)
  • -
-

Implementation Status

-

85 WebNN operations fully implemented across all backends:

-
    -
  • Shape Inference: 85/85 (100%)
  • -
  • Python API: 85/85 (100%)
  • -
  • ONNX Backend: 85/85 (100%)
  • -
  • CoreML MLProgram: 85/85 (100%)
  • -
-

See implementation-status.md for complete details.

- - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/site/assets/_mkdocstrings.css b/site/assets/_mkdocstrings.css deleted file mode 100644 index 854048ca..00000000 --- a/site/assets/_mkdocstrings.css +++ /dev/null @@ -1,237 +0,0 @@ - -/* Avoid breaking parameter names, etc. in table cells. */ -.doc-contents td code { - word-break: normal !important; -} - -/* No line break before first paragraph of descriptions. */ -.doc-md-description, -.doc-md-description>p:first-child { - display: inline; -} - -/* No text transformation from Material for MkDocs for H5 headings. */ -.md-typeset h5 .doc-object-name { - text-transform: none; -} - -/* Max width for docstring sections tables. */ -.doc .md-typeset__table, -.doc .md-typeset__table table { - display: table !important; - width: 100%; -} - -.doc .md-typeset__table tr { - display: table-row; -} - -/* Defaults in Spacy table style. */ -.doc-param-default, -.doc-type_param-default { - float: right; -} - -/* Parameter headings must be inline, not blocks. */ -.doc-heading-parameter, -.doc-heading-type_parameter { - display: inline; -} - -/* Default font size for parameter headings. */ -.md-typeset .doc-heading-parameter { - font-size: inherit; -} - -/* Prefer space on the right, not the left of parameter permalinks. */ -.doc-heading-parameter .headerlink, -.doc-heading-type_parameter .headerlink { - margin-left: 0 !important; - margin-right: 0.2rem; -} - -/* Backward-compatibility: docstring section titles in bold. */ -.doc-section-title { - font-weight: bold; -} - -/* Backlinks crumb separator. */ -.doc-backlink-crumb { - display: inline-flex; - gap: .2rem; - white-space: nowrap; - align-items: center; - vertical-align: middle; -} -.doc-backlink-crumb:not(:first-child)::before { - background-color: var(--md-default-fg-color--lighter); - content: ""; - display: inline; - height: 1rem; - --md-path-icon: url('data:image/svg+xml;charset=utf-8,'); - -webkit-mask-image: var(--md-path-icon); - mask-image: var(--md-path-icon); - width: 1rem; -} -.doc-backlink-crumb.last { - font-weight: bold; -} - -/* Symbols in Navigation and ToC. */ -:root, :host, -[data-md-color-scheme="default"] { - --doc-symbol-parameter-fg-color: #df50af; - --doc-symbol-type_parameter-fg-color: #df50af; - --doc-symbol-attribute-fg-color: #953800; - --doc-symbol-function-fg-color: #8250df; - --doc-symbol-method-fg-color: #8250df; - --doc-symbol-class-fg-color: #0550ae; - --doc-symbol-type_alias-fg-color: #0550ae; - --doc-symbol-module-fg-color: #5cad0f; - - --doc-symbol-parameter-bg-color: #df50af1a; - --doc-symbol-type_parameter-bg-color: #df50af1a; - --doc-symbol-attribute-bg-color: #9538001a; - --doc-symbol-function-bg-color: #8250df1a; - --doc-symbol-method-bg-color: #8250df1a; - --doc-symbol-class-bg-color: #0550ae1a; - --doc-symbol-type_alias-bg-color: #0550ae1a; - --doc-symbol-module-bg-color: #5cad0f1a; -} - -[data-md-color-scheme="slate"] { - --doc-symbol-parameter-fg-color: #ffa8cc; - --doc-symbol-type_parameter-fg-color: #ffa8cc; - --doc-symbol-attribute-fg-color: #ffa657; - --doc-symbol-function-fg-color: #d2a8ff; - --doc-symbol-method-fg-color: #d2a8ff; - --doc-symbol-class-fg-color: #79c0ff; - --doc-symbol-type_alias-fg-color: #79c0ff; - --doc-symbol-module-fg-color: #baff79; - - --doc-symbol-parameter-bg-color: #ffa8cc1a; - --doc-symbol-type_parameter-bg-color: #ffa8cc1a; - --doc-symbol-attribute-bg-color: #ffa6571a; - --doc-symbol-function-bg-color: #d2a8ff1a; - --doc-symbol-method-bg-color: #d2a8ff1a; - --doc-symbol-class-bg-color: #79c0ff1a; - --doc-symbol-type_alias-bg-color: #79c0ff1a; - --doc-symbol-module-bg-color: #baff791a; -} - -code.doc-symbol { - border-radius: .1rem; - font-size: .85em; - padding: 0 .3em; - font-weight: bold; -} - -code.doc-symbol-parameter, -a code.doc-symbol-parameter { - color: var(--doc-symbol-parameter-fg-color); - background-color: var(--doc-symbol-parameter-bg-color); -} - -code.doc-symbol-parameter::after { - content: "param"; -} - -code.doc-symbol-type_parameter, -a code.doc-symbol-type_parameter { - color: var(--doc-symbol-type_parameter-fg-color); - background-color: var(--doc-symbol-type_parameter-bg-color); -} - -code.doc-symbol-type_parameter::after { - content: "type-param"; -} - -code.doc-symbol-attribute, -a code.doc-symbol-attribute { - color: var(--doc-symbol-attribute-fg-color); - background-color: var(--doc-symbol-attribute-bg-color); -} - -code.doc-symbol-attribute::after { - content: "attr"; -} - -code.doc-symbol-function, -a code.doc-symbol-function { - color: var(--doc-symbol-function-fg-color); - background-color: var(--doc-symbol-function-bg-color); -} - -code.doc-symbol-function::after { - content: "func"; -} - -code.doc-symbol-method, -a code.doc-symbol-method { - color: var(--doc-symbol-method-fg-color); - background-color: var(--doc-symbol-method-bg-color); -} - -code.doc-symbol-method::after { - content: "meth"; -} - -code.doc-symbol-class, -a code.doc-symbol-class { - color: var(--doc-symbol-class-fg-color); - background-color: var(--doc-symbol-class-bg-color); -} - -code.doc-symbol-class::after { - content: "class"; -} - - -code.doc-symbol-type_alias, -a code.doc-symbol-type_alias { - color: var(--doc-symbol-type_alias-fg-color); - background-color: var(--doc-symbol-type_alias-bg-color); -} - -code.doc-symbol-type_alias::after { - content: "type"; -} - -code.doc-symbol-module, -a code.doc-symbol-module { - color: var(--doc-symbol-module-fg-color); - background-color: var(--doc-symbol-module-bg-color); -} - -code.doc-symbol-module::after { - content: "mod"; -} - -.doc-signature .autorefs { - color: inherit; - border-bottom: 1px dotted currentcolor; -} - -/* Source code blocks (admonitions). */ -:root { - --md-admonition-icon--mkdocstrings-source: url('data:image/svg+xml;charset=utf-8,') -} -.md-typeset .admonition.mkdocstrings-source, -.md-typeset details.mkdocstrings-source { - border: none; - padding: 0; -} -.md-typeset .admonition.mkdocstrings-source:focus-within, -.md-typeset details.mkdocstrings-source:focus-within { - box-shadow: none; -} -.md-typeset .mkdocstrings-source > .admonition-title, -.md-typeset .mkdocstrings-source > summary { - background-color: inherit; -} -.md-typeset .mkdocstrings-source > .admonition-title::before, -.md-typeset .mkdocstrings-source > summary::before { - background-color: var(--md-default-fg-color); - -webkit-mask-image: var(--md-admonition-icon--mkdocstrings-source); - mask-image: var(--md-admonition-icon--mkdocstrings-source); -} diff --git a/site/assets/images/favicon.png b/site/assets/images/favicon.png deleted file mode 100644 index 1cf13b9f9d978896599290a74f77d5dbe7d1655c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ diff --git a/site/assets/javascripts/bundle.e71a0d61.min.js b/site/assets/javascripts/bundle.e71a0d61.min.js deleted file mode 100644 index c76b3b2b..00000000 --- a/site/assets/javascripts/bundle.e71a0d61.min.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict";(()=>{var Zi=Object.create;var _r=Object.defineProperty;var ea=Object.getOwnPropertyDescriptor;var ta=Object.getOwnPropertyNames,Bt=Object.getOwnPropertySymbols,ra=Object.getPrototypeOf,Ar=Object.prototype.hasOwnProperty,bo=Object.prototype.propertyIsEnumerable;var ho=(e,t,r)=>t in e?_r(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Ar.call(t,r)&&ho(e,r,t[r]);if(Bt)for(var r of Bt(t))bo.call(t,r)&&ho(e,r,t[r]);return e};var vo=(e,t)=>{var r={};for(var o in e)Ar.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Bt)for(var o of Bt(e))t.indexOf(o)<0&&bo.call(e,o)&&(r[o]=e[o]);return r};var Cr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var oa=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of ta(t))!Ar.call(e,n)&&n!==r&&_r(e,n,{get:()=>t[n],enumerable:!(o=ea(t,n))||o.enumerable});return e};var $t=(e,t,r)=>(r=e!=null?Zi(ra(e)):{},oa(t||!e||!e.__esModule?_r(r,"default",{value:e,enumerable:!0}):r,e));var go=(e,t,r)=>new Promise((o,n)=>{var i=c=>{try{a(r.next(c))}catch(p){n(p)}},s=c=>{try{a(r.throw(c))}catch(p){n(p)}},a=c=>c.done?o(c.value):Promise.resolve(c.value).then(i,s);a((r=r.apply(e,t)).next())});var xo=Cr((kr,yo)=>{(function(e,t){typeof kr=="object"&&typeof yo!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(kr,(function(){"use strict";function e(r){var o=!0,n=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function c(k){var ut=k.type,je=k.tagName;return!!(je==="INPUT"&&s[ut]&&!k.readOnly||je==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function p(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(a(r.activeElement)&&p(r.activeElement),o=!0)}function u(k){o=!1}function d(k){a(k.target)&&(o||c(k.target))&&p(k.target)}function v(k){a(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function S(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",ee),document.addEventListener("mousedown",ee),document.addEventListener("mouseup",ee),document.addEventListener("pointermove",ee),document.addEventListener("pointerdown",ee),document.addEventListener("pointerup",ee),document.addEventListener("touchmove",ee),document.addEventListener("touchstart",ee),document.addEventListener("touchend",ee)}function re(){document.removeEventListener("mousemove",ee),document.removeEventListener("mousedown",ee),document.removeEventListener("mouseup",ee),document.removeEventListener("pointermove",ee),document.removeEventListener("pointerdown",ee),document.removeEventListener("pointerup",ee),document.removeEventListener("touchmove",ee),document.removeEventListener("touchstart",ee),document.removeEventListener("touchend",ee)}function ee(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,re())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",S,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",v,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var ro=Cr((jy,Rn)=>{"use strict";/*! - * escape-html - * Copyright(c) 2012-2013 TJ Holowaychuk - * Copyright(c) 2015 Andreas Lubbe - * Copyright(c) 2015 Tiancheng "Timothy" Gu - * MIT Licensed - */var qa=/["'&<>]/;Rn.exports=Ka;function Ka(e){var t=""+e,r=qa.exec(t);if(!r)return t;var o,n="",i=0,s=0;for(i=r.index;i{/*! - * clipboard.js v2.0.11 - * https://clipboardjs.com/ - * - * Licensed MIT © Zeno Rocha - */(function(t,r){typeof Nt=="object"&&typeof io=="object"?io.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Nt=="object"?Nt.ClipboardJS=r():t.ClipboardJS=r()})(Nt,function(){return(function(){var e={686:(function(o,n,i){"use strict";i.d(n,{default:function(){return Xi}});var s=i(279),a=i.n(s),c=i(370),p=i.n(c),l=i(817),f=i.n(l);function u(q){try{return document.execCommand(q)}catch(C){return!1}}var d=function(C){var _=f()(C);return u("cut"),_},v=d;function S(q){var C=document.documentElement.getAttribute("dir")==="rtl",_=document.createElement("textarea");_.style.fontSize="12pt",_.style.border="0",_.style.padding="0",_.style.margin="0",_.style.position="absolute",_.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return _.style.top="".concat(D,"px"),_.setAttribute("readonly",""),_.value=q,_}var X=function(C,_){var D=S(C);_.container.appendChild(D);var N=f()(D);return u("copy"),D.remove(),N},re=function(C){var _=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=X(C,_):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=X(C.value,_):(D=f()(C),u("copy")),D},ee=re;function k(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(_){return typeof _}:k=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},k(q)}var ut=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},_=C.action,D=_===void 0?"copy":_,N=C.container,G=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(G!==void 0)if(G&&k(G)==="object"&&G.nodeType===1){if(D==="copy"&&G.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(G.hasAttribute("readonly")||G.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return ee(We,{container:N});if(G)return D==="cut"?v(G):ee(G,{container:N})},je=ut;function R(q){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?R=function(_){return typeof _}:R=function(_){return _&&typeof Symbol=="function"&&_.constructor===Symbol&&_!==Symbol.prototype?"symbol":typeof _},R(q)}function se(q,C){if(!(q instanceof C))throw new TypeError("Cannot call a class as a function")}function ce(q,C){for(var _=0;_0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof N.action=="function"?N.action:this.defaultAction,this.target=typeof N.target=="function"?N.target:this.defaultTarget,this.text=typeof N.text=="function"?N.text:this.defaultText,this.container=R(N.container)==="object"?N.container:document.body}},{key:"listenClick",value:function(N){var G=this;this.listener=p()(N,"click",function(We){return G.onClick(We)})}},{key:"onClick",value:function(N){var G=N.delegateTarget||N.currentTarget,We=this.action(G)||"copy",Yt=je({action:We,container:this.container,target:this.target(G),text:this.text(G)});this.emit(Yt?"success":"error",{action:We,text:Yt,trigger:G,clearSelection:function(){G&&G.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(N){return Mr("action",N)}},{key:"defaultTarget",value:function(N){var G=Mr("target",N);if(G)return document.querySelector(G)}},{key:"defaultText",value:function(N){return Mr("text",N)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(N){var G=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return ee(N,G)}},{key:"cut",value:function(N){return v(N)}},{key:"isSupported",value:function(){var N=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],G=typeof N=="string"?[N]:N,We=!!document.queryCommandSupported;return G.forEach(function(Yt){We=We&&!!document.queryCommandSupported(Yt)}),We}}]),_})(a()),Xi=Ji}),828:(function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,c){for(;a&&a.nodeType!==n;){if(typeof a.matches=="function"&&a.matches(c))return a;a=a.parentNode}}o.exports=s}),438:(function(o,n,i){var s=i(828);function a(l,f,u,d,v){var S=p.apply(this,arguments);return l.addEventListener(u,S,v),{destroy:function(){l.removeEventListener(u,S,v)}}}function c(l,f,u,d,v){return typeof l.addEventListener=="function"?a.apply(null,arguments):typeof u=="function"?a.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(S){return a(S,f,u,d,v)}))}function p(l,f,u,d){return function(v){v.delegateTarget=s(v.target,f),v.delegateTarget&&d.call(l,v)}}o.exports=c}),879:(function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}}),370:(function(o,n,i){var s=i(879),a=i(438);function c(u,d,v){if(!u&&!d&&!v)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(v))throw new TypeError("Third argument must be a Function");if(s.node(u))return p(u,d,v);if(s.nodeList(u))return l(u,d,v);if(s.string(u))return f(u,d,v);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function p(u,d,v){return u.addEventListener(d,v),{destroy:function(){u.removeEventListener(d,v)}}}function l(u,d,v){return Array.prototype.forEach.call(u,function(S){S.addEventListener(d,v)}),{destroy:function(){Array.prototype.forEach.call(u,function(S){S.removeEventListener(d,v)})}}}function f(u,d,v){return a(document.body,u,d,v)}o.exports=c}),817:(function(o){function n(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),p=document.createRange();p.selectNodeContents(i),c.removeAllRanges(),c.addRange(p),s=c.toString()}return s}o.exports=n}),279:(function(o){function n(){}n.prototype={on:function(i,s,a){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var c=this;function p(){c.off(i,p),s.apply(a,arguments)}return p._=s,this.on(i,p,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),c=0,p=a.length;for(c;c0&&i[i.length-1])&&(p[0]===6||p[0]===2)){r=0;continue}if(p[0]===3&&(!i||p[1]>i[0]&&p[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function K(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],s;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(a){s={error:a}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(s)throw s.error}}return i}function B(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||c(d,S)})},v&&(n[d]=v(n[d])))}function c(d,v){try{p(o[d](v))}catch(S){u(i[0][3],S)}}function p(d){d.value instanceof dt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){c("next",d)}function f(d){c("throw",d)}function u(d,v){d(v),i.shift(),i.length&&c(i[0][0],i[0][1])}}function To(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Oe=="function"?Oe(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(s){return new Promise(function(a,c){s=e[i](s),n(a,c,s.done,s.value)})}}function n(i,s,a,c){Promise.resolve(c).then(function(p){i({value:p,done:a})},s)}}function I(e){return typeof e=="function"}function yt(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Jt=yt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: -`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` - `):"",this.name="UnsubscriptionError",this.errors=r}});function Ze(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var qe=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Oe(s),c=a.next();!c.done;c=a.next()){var p=c.value;p.remove(this)}}catch(S){t={error:S}}finally{try{c&&!c.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var l=this.initialTeardown;if(I(l))try{l()}catch(S){i=S instanceof Jt?S.errors:[S]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=Oe(f),d=u.next();!d.done;d=u.next()){var v=d.value;try{So(v)}catch(S){i=i!=null?i:[],S instanceof Jt?i=B(B([],K(i)),K(S.errors)):i.push(S)}}}catch(S){o={error:S}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new Jt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)So(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ze(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ze(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var $r=qe.EMPTY;function Xt(e){return e instanceof qe||e&&"closed"in e&&I(e.remove)&&I(e.add)&&I(e.unsubscribe)}function So(e){I(e)?e():e.unsubscribe()}var De={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var xt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,s=n.isStopped,a=n.observers;return i||s?$r:(this.currentObservers=null,a.push(r),new qe(function(){o.currentObservers=null,Ze(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,s=o.isStopped;n?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Ho(r,o)},t})(F);var Ho=(function(e){ie(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:$r},t})(T);var jr=(function(e){ie(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(T);var Rt={now:function(){return(Rt.delegate||Date).now()},delegate:void 0};var It=(function(e){ie(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Rt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,s=o._infiniteTimeWindow,a=o._timestampProvider,c=o._windowTime;n||(i.push(r),!s&&i.push(a.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,s=n._buffer,a=s.slice(),c=0;c0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t})(St);var Ro=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(Ot);var Dr=new Ro(Po);var Io=(function(e){ie(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=Tt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var s=r.actions;o!=null&&o===r._scheduled&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==o&&(Tt.cancelAnimationFrame(o),r._scheduled=void 0)},t})(St);var Fo=(function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o;r?o=r.id:(o=this._scheduled,this._scheduled=void 0);var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t})(Ot);var ye=new Fo(Io);var y=new F(function(e){return e.complete()});function tr(e){return e&&I(e.schedule)}function Vr(e){return e[e.length-1]}function pt(e){return I(Vr(e))?e.pop():void 0}function Fe(e){return tr(Vr(e))?e.pop():void 0}function rr(e,t){return typeof Vr(e)=="number"?e.pop():t}var Lt=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function or(e){return I(e==null?void 0:e.then)}function nr(e){return I(e[wt])}function ir(e){return Symbol.asyncIterator&&I(e==null?void 0:e[Symbol.asyncIterator])}function ar(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function fa(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var sr=fa();function cr(e){return I(e==null?void 0:e[sr])}function pr(e){return wo(this,arguments,function(){var r,o,n,i;return Gt(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,dt(r.read())];case 3:return o=s.sent(),n=o.value,i=o.done,i?[4,dt(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,dt(n)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function lr(e){return I(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(nr(e))return ua(e);if(Lt(e))return da(e);if(or(e))return ha(e);if(ir(e))return jo(e);if(cr(e))return ba(e);if(lr(e))return va(e)}throw ar(e)}function ua(e){return new F(function(t){var r=e[wt]();if(I(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function da(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?g(function(n,i){return e(n,i,o)}):be,Ee(1),r?Qe(t):tn(function(){return new fr}))}}function Yr(e){return e<=0?function(){return y}:E(function(t,r){var o=[];t.subscribe(w(r,function(n){o.push(n),e=2,!0))}function le(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new T}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,c=a===void 0?!0:a;return function(p){var l,f,u,d=0,v=!1,S=!1,X=function(){f==null||f.unsubscribe(),f=void 0},re=function(){X(),l=u=void 0,v=S=!1},ee=function(){var k=l;re(),k==null||k.unsubscribe()};return E(function(k,ut){d++,!S&&!v&&X();var je=u=u!=null?u:r();ut.add(function(){d--,d===0&&!S&&!v&&(f=Br(ee,c))}),je.subscribe(ut),!l&&d>0&&(l=new bt({next:function(R){return je.next(R)},error:function(R){S=!0,X(),f=Br(re,n,R),je.error(R)},complete:function(){v=!0,X(),f=Br(re,s),je.complete()}}),U(k).subscribe(l))})(p)}}function Br(e,t){for(var r=[],o=2;oe.next(document)),e}function M(e,t=document){return Array.from(t.querySelectorAll(e))}function j(e,t=document){let r=ue(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ue(e,t=document){return t.querySelector(e)||void 0}function Ne(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var Ra=L(h(document.body,"focusin"),h(document.body,"focusout")).pipe(Ae(1),Q(void 0),m(()=>Ne()||document.body),Z(1));function Ye(e){return Ra.pipe(m(t=>e.contains(t)),Y())}function it(e,t){return H(()=>L(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?jt(r=>He(+!r*t)):be,Q(e.matches(":hover"))))}function sn(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)sn(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)sn(o,n);return o}function br(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function _t(e){let t=x("script",{src:e});return H(()=>(document.head.appendChild(t),L(h(t,"load"),h(t,"error").pipe(b(()=>Nr(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),A(()=>document.head.removeChild(t)),Ee(1))))}var cn=new T,Ia=H(()=>typeof ResizeObserver=="undefined"?_t("https://unpkg.com/resize-observer-polyfill"):$(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>cn.next(t)))),b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Le(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ia.pipe(O(r=>r.observe(t)),b(r=>cn.pipe(g(o=>o.target===t),A(()=>r.unobserve(t)))),m(()=>de(e)),Q(de(e)))}function At(e){return{width:e.scrollWidth,height:e.scrollHeight}}function vr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function pn(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function ln(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function mn(e){return L(h(window,"load"),h(window,"resize")).pipe($e(0,ye),m(()=>Be(e)),Q(Be(e)))}function gr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ge(e){return L(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe($e(0,ye),m(()=>gr(e)),Q(gr(e)))}var fn=new T,Fa=H(()=>$(new IntersectionObserver(e=>{for(let t of e)fn.next(t)},{threshold:0}))).pipe(b(e=>L(tt,$(e)).pipe(A(()=>e.disconnect()))),Z(1));function mt(e){return Fa.pipe(O(t=>t.observe(e)),b(t=>fn.pipe(g(({target:r})=>r===e),A(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function un(e,t=16){return Ge(e).pipe(m(({y:r})=>{let o=de(e),n=At(e);return r>=n.height-o.height-t}),Y())}var yr={drawer:j("[data-md-toggle=drawer]"),search:j("[data-md-toggle=search]")};function dn(e){return yr[e].checked}function at(e,t){yr[e].checked!==t&&yr[e].click()}function Je(e){let t=yr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function ja(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ua(){return L(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function hn(){let e=h(window,"keydown").pipe(g(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:dn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),g(({mode:t,type:r})=>{if(t==="global"){let o=Ne();if(typeof o!="undefined")return!ja(o,r)}return!0}),le());return Ua().pipe(b(t=>t?y:e))}function we(){return new URL(location.href)}function st(e,t=!1){if(V("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function bn(){return new T}function vn(){return location.hash.slice(1)}function gn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Zr(e){return L(h(window,"hashchange"),e).pipe(m(vn),Q(vn()),g(t=>t.length>0),Z(1))}function yn(e){return Zr(e).pipe(m(t=>ue(`[id="${t}"]`)),g(t=>typeof t!="undefined"))}function Wt(e){let t=matchMedia(e);return ur(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function xn(){let e=matchMedia("print");return L(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function eo(e,t){return e.pipe(b(r=>r?t():y))}function to(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let s=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+s*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function ze(e,t){return to(e,t).pipe(b(r=>r.text()),m(r=>JSON.parse(r)),Z(1))}function xr(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),Z(1))}function En(e,t){let r=new DOMParser;return to(e,t).pipe(b(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),Z(1))}function wn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Tn(){return L(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(wn),Q(wn()))}function Sn(){return{width:innerWidth,height:innerHeight}}function On(){return h(window,"resize",{passive:!0}).pipe(m(Sn),Q(Sn()))}function Ln(){return z([Tn(),On()]).pipe(m(([e,t])=>({offset:e,size:t})),Z(1))}function Er(e,{viewport$:t,header$:r}){let o=t.pipe(ne("size")),n=z([o,r]).pipe(m(()=>Be(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:s,size:a},{x:c,y:p}])=>({offset:{x:s.x-c,y:s.y-p+i},size:a})))}function Wa(e){return h(e,"message",t=>t.data)}function Da(e){let t=new T;return t.subscribe(r=>e.postMessage(r)),t}function Mn(e,t=new Worker(e)){let r=Wa(t),o=Da(t),n=new T;n.subscribe(o);let i=o.pipe(oe(),ae(!0));return n.pipe(oe(),Ve(r.pipe(W(i))),le())}var Va=j("#__config"),Ct=JSON.parse(Va.textContent);Ct.base=`${new URL(Ct.base,we())}`;function Te(){return Ct}function V(e){return Ct.features.includes(e)}function Me(e,t){return typeof t!="undefined"?Ct.translations[e].replace("#",t.toString()):Ct.translations[e]}function Ce(e,t=document){return j(`[data-md-component=${e}]`,t)}function me(e,t=document){return M(`[data-md-component=${e}]`,t)}function Na(e){let t=j(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>j(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function _n(e){if(!V("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=j(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return H(()=>{let t=new T;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Na(e).pipe(O(r=>t.next(r)),A(()=>t.complete()),m(r=>P({ref:e},r)))})}function za(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function An(e,t){let r=new T;return r.subscribe(({hidden:o})=>{e.hidden=o}),za(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))}function Dt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wr(...e){return x("div",{class:"md-tooltip2",role:"dialog"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Cn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function kn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Dt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Hn(e){return x("button",{class:"md-code__button",title:Me("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function $n(){return x("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function Pn(){return x("nav",{class:"md-code__nav"})}var In=$t(ro());function oo(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(c=>!e.terms[c]).reduce((c,p)=>[...c,x("del",null,(0,In.default)(p))," "],[]).slice(0,-1),i=Te(),s=new URL(e.location,i.base);V("search.highlight")&&s.searchParams.set("h",Object.entries(e.terms).filter(([,c])=>c).reduce((c,[p])=>`${c} ${p}`.trim(),""));let{tags:a}=Te();return x("a",{href:`${s}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(c=>{let p=a?c in a?`md-tag-icon md-tag--${a[c]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${p}`},c)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Me("search.result.term.missing"),": ",...n)))}function Fn(e){let t=e[0].score,r=[...e],o=Te(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),s=r.findIndex(l=>l.scoreoo(l,1)),...c.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,c.length>0&&c.length===1?Me("search.result.more.one"):Me("search.result.more.other",c.length))),...c.map(l=>oo(l,1)))]:[]];return x("li",{class:"md-search-result__item"},p)}function jn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?br(r):r)))}function no(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function Un(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Qa(e){var o;let t=Te(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Wn(e,t){var o;let r=Te();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Me("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Qa)))}var Ya=0;function Ba(e,t=250){let r=z([Ye(e),it(e,t)]).pipe(m(([n,i])=>n||i),Y()),o=H(()=>pn(e)).pipe(J(Ge),gt(1),Pe(r),m(()=>ln(e)));return r.pipe(Re(n=>n),b(()=>z([r,o])),m(([n,i])=>({active:n,offset:i})),le())}function Vt(e,t,r=250){let{content$:o,viewport$:n}=t,i=`__tooltip2_${Ya++}`;return H(()=>{let s=new T,a=new jr(!1);s.pipe(oe(),ae(!1)).subscribe(a);let c=a.pipe(jt(l=>He(+!l*250,Dr)),Y(),b(l=>l?o:y),O(l=>l.id=i),le());z([s.pipe(m(({active:l})=>l)),c.pipe(b(l=>it(l,250)),Q(!1))]).pipe(m(l=>l.some(f=>f))).subscribe(a);let p=a.pipe(g(l=>l),te(c,n),m(([l,f,{size:u}])=>{let d=e.getBoundingClientRect(),v=d.width/2;if(f.role==="tooltip")return{x:v,y:8+d.height};if(d.y>=u.height/2){let{height:S}=de(f);return{x:v,y:-16-S}}else return{x:v,y:16+d.height}}));return z([c,s,p]).subscribe(([l,{offset:f},u])=>{l.style.setProperty("--md-tooltip-host-x",`${f.x}px`),l.style.setProperty("--md-tooltip-host-y",`${f.y}px`),l.style.setProperty("--md-tooltip-x",`${u.x}px`),l.style.setProperty("--md-tooltip-y",`${u.y}px`),l.classList.toggle("md-tooltip2--top",u.y<0),l.classList.toggle("md-tooltip2--bottom",u.y>=0)}),a.pipe(g(l=>l),te(c,(l,f)=>f),g(l=>l.role==="tooltip")).subscribe(l=>{let f=de(j(":scope > *",l));l.style.setProperty("--md-tooltip-width",`${f.width}px`),l.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(Y(),xe(ye),te(c)).subscribe(([l,f])=>{f.classList.toggle("md-tooltip2--active",l)}),z([a.pipe(g(l=>l)),c]).subscribe(([l,f])=>{f.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),a.pipe(g(l=>!l)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),Ba(e,r).pipe(O(l=>s.next(l)),A(()=>s.complete()),m(l=>P({ref:e},l)))})}function Xe(e,{viewport$:t},r=document.body){return Vt(e,{content$:new F(o=>{let n=e.title,i=Cn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t},0)}function Ga(e,t){let r=H(()=>z([mn(e),Ge(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:s,height:a}=de(e);return{x:o-i.x+s/2,y:n-i.y+a/2}}));return Ye(e).pipe(b(o=>r.pipe(m(n=>({active:o,offset:n})),Ee(+!o||1/0))))}function Dn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return H(()=>{let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({offset:a}){e.style.setProperty("--md-tooltip-x",`${a.x}px`),e.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),mt(e).pipe(W(s)).subscribe(a=>{e.toggleAttribute("data-md-visible",a)}),L(i.pipe(g(({active:a})=>a)),i.pipe(Ae(250),g(({active:a})=>!a))).subscribe({next({active:a}){a?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe($e(16,ye)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?e.style.setProperty("--md-tooltip-0",`${-a}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(s),g(a=>!(a.metaKey||a.ctrlKey))).subscribe(a=>{a.stopPropagation(),a.preventDefault()}),h(n,"mousedown").pipe(W(s),te(i)).subscribe(([a,{active:c}])=>{var p;if(a.button!==0||a.metaKey||a.ctrlKey)a.preventDefault();else if(c){a.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(p=Ne())==null||p.blur()}}),r.pipe(W(s),g(a=>a===o),nt(125)).subscribe(()=>e.focus()),Ga(e,t).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function Ja(e){let t=Te();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate&&typeof t.annotate=="object"){let o=e.closest("[class|=language]");if(o)for(let n of Array.from(o.classList)){if(!n.startsWith("language-"))continue;let[,i]=n.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return M(r.join(", "),e)}function Xa(e){let t=[];for(let r of Ja(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let s;for(;s=/(\(\d+\))(!)?/.exec(i.textContent);){let[,a,c]=s;if(typeof c=="undefined"){let p=i.splitText(s.index);i=p.splitText(a.length),t.push(p)}else{i.textContent=a,t.push(i);break}}}}return t}function Vn(e,t){t.append(...Array.from(e.childNodes))}function Tr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,s=new Map;for(let a of Xa(t)){let[,c]=a.textContent.match(/\((\d+)\)/);ue(`:scope > li:nth-child(${c})`,e)&&(s.set(c,kn(c,i)),a.replaceWith(s.get(c)))}return s.size===0?y:H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=[];for(let[l,f]of s)p.push([j(".md-typeset",f),j(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(c)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of p)l?Vn(f,u):Vn(u,f)}),L(...[...s].map(([,l])=>Dn(l,t,{target$:r}))).pipe(A(()=>a.complete()),le())})}function Nn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Nn(t)}}function zn(e,t){return H(()=>{let r=Nn(e);return typeof r!="undefined"?Tr(r,e,t):y})}var Kn=$t(ao());var Za=0,qn=L(h(window,"keydown").pipe(m(()=>!0)),L(h(window,"keyup"),h(window,"contextmenu")).pipe(m(()=>!1))).pipe(Q(!1),Z(1));function Qn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Qn(t)}}function es(e){return Le(e).pipe(m(({width:t})=>({scrollable:At(e).width>t})),ne("scrollable"))}function Yn(e,t){let{matches:r}=matchMedia("(hover)"),o=H(()=>{let n=new T,i=n.pipe(Yr(1));n.subscribe(({scrollable:d})=>{d&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let s=[],a=e.closest("pre"),c=a.closest("[id]"),p=c?c.id:Za++;a.id=`__code_${p}`;let l=[],f=e.closest(".highlight");if(f instanceof HTMLElement){let d=Qn(f);if(typeof d!="undefined"&&(f.classList.contains("annotate")||V("content.code.annotate"))){let v=Tr(d,e,t);l.push(Le(f).pipe(W(i),m(({width:S,height:X})=>S&&X),Y(),b(S=>S?v:y)))}}let u=M(":scope > span[id]",e);if(u.length&&(e.classList.add("md-code__content"),e.closest(".select")||V("content.code.select")&&!e.closest(".no-select"))){let d=+u[0].id.split("-").pop(),v=$n();s.push(v),V("content.tooltips")&&l.push(Xe(v,{viewport$}));let S=h(v,"click").pipe(Ut(R=>!R,!1),O(()=>v.blur()),le());S.subscribe(R=>{v.classList.toggle("md-code__button--active",R)});let X=fe(u).pipe(J(R=>it(R).pipe(m(se=>[R,se]))));S.pipe(b(R=>R?X:y)).subscribe(([R,se])=>{let ce=ue(".hll.select",R);if(ce&&!se)ce.replaceWith(...Array.from(ce.childNodes));else if(!ce&&se){let he=document.createElement("span");he.className="hll select",he.append(...Array.from(R.childNodes).slice(1)),R.append(he)}});let re=fe(u).pipe(J(R=>h(R,"mousedown").pipe(O(se=>se.preventDefault()),m(()=>R)))),ee=S.pipe(b(R=>R?re:y),te(qn),m(([R,se])=>{var he;let ce=u.indexOf(R)+d;if(se===!1)return[ce,ce];{let Se=M(".hll",e).map(Ue=>u.indexOf(Ue.parentElement)+d);return(he=window.getSelection())==null||he.removeAllRanges(),[Math.min(ce,...Se),Math.max(ce,...Se)]}})),k=Zr(y).pipe(g(R=>R.startsWith(`__codelineno-${p}-`)));k.subscribe(R=>{let[,,se]=R.split("-"),ce=se.split(":").map(Se=>+Se-d+1);ce.length===1&&ce.push(ce[0]);for(let Se of M(".hll:not(.select)",e))Se.replaceWith(...Array.from(Se.childNodes));let he=u.slice(ce[0]-1,ce[1]);for(let Se of he){let Ue=document.createElement("span");Ue.className="hll",Ue.append(...Array.from(Se.childNodes).slice(1)),Se.append(Ue)}}),k.pipe(Ee(1),xe(pe)).subscribe(R=>{if(R.includes(":")){let se=document.getElementById(R.split(":")[0]);se&&setTimeout(()=>{let ce=se,he=-64;for(;ce!==document.body;)he+=ce.offsetTop,ce=ce.offsetParent;window.scrollTo({top:he})},1)}});let je=fe(M('a[href^="#__codelineno"]',f)).pipe(J(R=>h(R,"click").pipe(O(se=>se.preventDefault()),m(()=>R)))).pipe(W(i),te(qn),m(([R,se])=>{let he=+j(`[id="${R.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(se===!1)return[he,he];{let Se=M(".hll",e).map(Ue=>+Ue.parentElement.id.split("-").pop());return[Math.min(he,...Se),Math.max(he,...Se)]}}));L(ee,je).subscribe(R=>{let se=`#__codelineno-${p}-`;R[0]===R[1]?se+=R[0]:se+=`${R[0]}:${R[1]}`,history.replaceState({},"",se),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+se,oldURL:window.location.href}))})}if(Kn.default.isSupported()&&(e.closest(".copy")||V("content.code.copy")&&!e.closest(".no-copy"))){let d=Hn(a.id);s.push(d),V("content.tooltips")&&l.push(Xe(d,{viewport$}))}if(s.length){let d=Pn();d.append(...s),a.insertBefore(d,e)}return es(e).pipe(O(d=>n.next(d)),A(()=>n.complete()),m(d=>P({ref:e},d)),Ve(L(...l).pipe(W(i))))});return V("content.lazy")?mt(e).pipe(g(n=>n),Ee(1),b(()=>o)):o}function ts(e,{target$:t,print$:r}){let o=!0;return L(t.pipe(m(n=>n.closest("details:not([open])")),g(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(g(n=>n||!o),O(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Bn(e,t){return H(()=>{let r=new T;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),ts(e,t).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}var Gn=0;function rs(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],o=e.nextElementSibling;for(;o&&!(o instanceof HTMLHeadingElement);)r.push(o),o=o.nextElementSibling;return r}function os(e,t){for(let r of M("[href], [src]",e))for(let o of["href","src"]){let n=r.getAttribute(o);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){r[o]=new URL(r.getAttribute(o),t).toString();break}}for(let r of M("[name^=__], [for]",e))for(let o of["id","for","name"]){let n=r.getAttribute(o);n&&r.setAttribute(o,`${n}$preview_${Gn}`)}return Gn++,$(e)}function Jn(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(V("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let o=z([Ye(e),it(e)]).pipe(m(([i,s])=>i||s),Y(),g(i=>i));return rt([r,o]).pipe(b(([i])=>{let s=new URL(e.href);return s.search=s.hash="",i.has(`${s}`)?$(s):y}),b(i=>xr(i).pipe(b(s=>os(s,i)))),b(i=>{let s=e.hash?`article [id="${e.hash.slice(1)}"]`:"article h1",a=ue(s,i);return typeof a=="undefined"?y:$(rs(a))})).pipe(b(i=>{let s=new F(a=>{let c=wr(...i);return a.next(c),document.body.append(c),()=>c.remove()});return Vt(e,P({content$:s},t))}))}var Xn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var so,is=0;function as(){return typeof mermaid=="undefined"||mermaid instanceof Element?_t("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):$(void 0)}function Zn(e){return e.classList.remove("mermaid"),so||(so=as().pipe(O(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Xn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),Z(1))),so.subscribe(()=>go(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${is++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),s=r.attachShadow({mode:"closed"});s.innerHTML=n,e.replaceWith(r),i==null||i(s)})),so.pipe(m(()=>({ref:e})))}var ei=x("table");function ti(e){return e.replaceWith(ei),ei.replaceWith(Un(e)),$({ref:e})}function ss(e){let t=e.find(r=>r.checked)||e[0];return L(...e.map(r=>h(r,"change").pipe(m(()=>j(`label[for="${r.id}"]`))))).pipe(Q(j(`label[for="${t.id}"]`)),m(r=>({active:r})))}function ri(e,{viewport$:t,target$:r}){let o=j(".tabbed-labels",e),n=M(":scope > input",e),i=no("prev");e.append(i);let s=no("next");return e.append(s),H(()=>{let a=new T,c=a.pipe(oe(),ae(!0));z([a,Le(e),mt(e)]).pipe(W(c),$e(1,ye)).subscribe({next([{active:p},l]){let f=Be(p),{width:u}=de(p);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=gr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ge(o),Le(o)]).pipe(W(c)).subscribe(([p,l])=>{let f=At(o);i.hidden=p.x<16,s.hidden=p.x>f.width-l.width-16}),L(h(i,"click").pipe(m(()=>-1)),h(s,"click").pipe(m(()=>1))).pipe(W(c)).subscribe(p=>{let{width:l}=de(o);o.scrollBy({left:l*p,behavior:"smooth"})}),r.pipe(W(c),g(p=>n.includes(p))).subscribe(p=>p.click()),o.classList.add("tabbed-labels--linked");for(let p of n){let l=j(`label[for="${p.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(c),g(f=>!(f.metaKey||f.ctrlKey)),O(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return V("content.tabs.link")&&a.pipe(Ie(1),te(t)).subscribe(([{active:p},{offset:l}])=>{let f=p.innerText.trim();if(p.hasAttribute("data-md-switching"))p.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let v of M("[data-tabs]"))for(let S of M(":scope > input",v)){let X=j(`label[for="${S.id}"]`);if(X!==p&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),S.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),a.pipe(W(c)).subscribe(()=>{for(let p of M("audio, video",e))p.offsetWidth&&p.autoplay?p.play().catch(()=>{}):p.pause()}),ss(n).pipe(O(p=>a.next(p)),A(()=>a.complete()),m(p=>P({ref:e},p)))}).pipe(et(pe))}function oi(e,t){let{viewport$:r,target$:o,print$:n}=t;return L(...M(".annotate:not(.highlight)",e).map(i=>zn(i,{target$:o,print$:n})),...M("pre:not(.mermaid) > code",e).map(i=>Yn(i,{target$:o,print$:n})),...M("a",e).map(i=>Jn(i,t)),...M("pre.mermaid",e).map(i=>Zn(i)),...M("table:not([class])",e).map(i=>ti(i)),...M("details",e).map(i=>Bn(i,{target$:o,print$:n})),...M("[data-tabs]",e).map(i=>ri(i,{viewport$:r,target$:o})),...M("[title]:not([data-preview])",e).filter(()=>V("content.tooltips")).map(i=>Xe(i,{viewport$:r})),...M(".footnote-ref",e).filter(()=>V("content.footnote.tooltips")).map(i=>Vt(i,{content$:new F(s=>{let a=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(a).cloneNode(!0).children),p=wr(...c);return s.next(p),document.body.append(p),()=>p.remove()}),viewport$:r})))}function cs(e,{alert$:t}){return t.pipe(b(r=>L($(!0),$(!1).pipe(nt(2e3))).pipe(m(o=>({message:r,active:o})))))}function ni(e,t){let r=j(".md-typeset",e);return H(()=>{let o=new T;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),cs(e,t).pipe(O(n=>o.next(n)),A(()=>o.complete()),m(n=>P({ref:e},n)))})}var ps=0;function ls(e,t){document.body.append(e);let{width:r}=de(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=vr(t),n=typeof o!="undefined"?Ge(o):$({x:0,y:0}),i=L(Ye(t),it(t)).pipe(Y());return z([i,n]).pipe(m(([s,a])=>{let{x:c,y:p}=Be(t),l=de(t),f=t.closest("table");return f&&t.parentElement&&(c+=f.offsetLeft+t.parentElement.offsetLeft,p+=f.offsetTop+t.parentElement.offsetTop),{active:s,offset:{x:c-a.x+l.width/2-r/2,y:p-a.y+l.height+8}}}))}function ii(e){let t=e.title;if(!t.length)return y;let r=`__tooltip_${ps++}`,o=Dt(r,"inline"),n=j(".md-typeset",o);return n.innerHTML=t,H(()=>{let i=new T;return i.subscribe({next({offset:s}){o.style.setProperty("--md-tooltip-x",`${s.x}px`),o.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),L(i.pipe(g(({active:s})=>s)),i.pipe(Ae(250),g(({active:s})=>!s))).subscribe({next({active:s}){s?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe($e(16,ye)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(gt(125,ye),g(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?o.style.setProperty("--md-tooltip-0",`${-s}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),ls(o,e).pipe(O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))}).pipe(et(pe))}function ms({viewport$:e}){if(!V("header.autohide"))return $(!1);let t=e.pipe(m(({offset:{y:n}})=>n),ot(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),Y()),o=Je("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),Y(),b(n=>n?r:$(!1)),Q(!1))}function ai(e,t){return H(()=>z([Le(e),ms(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),Y((r,o)=>r.height===o.height&&r.hidden===o.hidden),Z(1))}function si(e,{header$:t,main$:r}){return H(()=>{let o=new T,n=o.pipe(oe(),ae(!0));o.pipe(ne("active"),Pe(t)).subscribe(([{active:s},{hidden:a}])=>{e.classList.toggle("md-header--shadow",s&&!a),e.hidden=a});let i=fe(M("[title]",e)).pipe(g(()=>V("content.tooltips")),J(s=>ii(s)));return r.subscribe(o),t.pipe(W(n),m(s=>P({ref:e},s)),Ve(i.pipe(W(n))))})}function fs(e,{viewport$:t,header$:r}){return Er(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=de(e);return{active:n>0&&o>=n}}),ne("active"))}function ci(e,t){return H(()=>{let r=new T;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=ue(".md-content h1");return typeof o=="undefined"?y:fs(o,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))})}function pi(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),Y()),n=o.pipe(b(()=>Le(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ne("bottom"))));return z([o,n,t]).pipe(m(([i,{top:s,bottom:a},{offset:{y:c},size:{height:p}}])=>(p=Math.max(0,p-Math.max(0,s-c,i)-Math.max(0,p+c-a)),{offset:s-i,height:p,active:s-i<=c})),Y((i,s)=>i.offset===s.offset&&i.height===s.height&&i.active===s.active))}function us(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return $(...e).pipe(J(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),Z(1))}function li(e){let t=M("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Wt("(prefers-color-scheme: light)");return H(()=>{let i=new T;return i.subscribe(s=>{if(document.body.setAttribute("data-md-color-switching",""),s.color.media==="(prefers-color-scheme)"){let a=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(a.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");s.color.scheme=c.getAttribute("data-md-color-scheme"),s.color.primary=c.getAttribute("data-md-color-primary"),s.color.accent=c.getAttribute("data-md-color-accent")}for(let[a,c]of Object.entries(s.color))document.body.setAttribute(`data-md-color-${a}`,c);for(let a=0;as.key==="Enter"),te(i,(s,a)=>a)).subscribe(({index:s})=>{s=(s+1)%t.length,t[s].click(),t[s].focus()}),i.pipe(m(()=>{let s=Ce("header"),a=window.getComputedStyle(s);return o.content=a.colorScheme,a.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(s=>r.content=`#${s}`),i.pipe(xe(pe)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),us(t).pipe(W(n.pipe(Ie(1))),vt(),O(s=>i.next(s)),A(()=>i.complete()),m(s=>P({ref:e},s)))})}function mi(e,{progress$:t}){return H(()=>{let r=new T;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(O(o=>r.next({value:o})),A(()=>r.complete()),m(o=>({ref:e,value:o})))})}function fi(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function ds(e,t){let r=new Map;for(let o of M("url",e)){let n=j("loc",o),i=[fi(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let s of M("[rel=alternate]",o)){let a=s.getAttribute("href");a!=null&&i.push(fi(new URL(a),t))}}return r}function kt(e){return En(new URL("sitemap.xml",e)).pipe(m(t=>ds(t,new URL(e))),ve(()=>$(new Map)),le())}function ui({document$:e}){let t=new Map;e.pipe(b(()=>M("link[rel=alternate]")),m(r=>new URL(r.href)),g(r=>!t.has(r.toString())),J(r=>kt(r).pipe(m(o=>[r,o]),ve(()=>y)))).subscribe(([r,o])=>{t.set(r.toString().replace(/\/$/,""),o)}),h(document.body,"click").pipe(g(r=>!r.metaKey&&!r.ctrlKey),b(r=>{if(r.target instanceof Element){let o=r.target.closest("a");if(o&&!o.target){let n=[...t].find(([f])=>o.href.startsWith(`${f}/`));if(typeof n=="undefined")return y;let[i,s]=n,a=we();if(a.href.startsWith(i))return y;let c=Te(),p=a.href.replace(c.base,"");p=`${i}/${p}`;let l=s.has(p.split("#")[0])?new URL(p,c.base):new URL(i);return r.preventDefault(),$(l)}}return y})).subscribe(r=>st(r,!0))}var co=$t(ao());function hs(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function di({alert$:e}){co.default.isSupported()&&new F(t=>{new co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||hs(j(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(O(t=>{t.trigger.focus()}),m(()=>Me("clipboard.copied"))).subscribe(e)}function hi(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),$(r)):y}function bi(e){let t=new Map;for(let r of M(":scope > *",e.head))t.set(r.outerHTML,r);return t}function vi(e){for(let t of M("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return $(e)}function bs(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...V("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=ue(o),i=ue(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=bi(document);for(let[o,n]of bi(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Ce("container");return Ke(M("script",r)).pipe(b(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),y}),oe(),ae(document))}function gi({sitemap$:e,location$:t,viewport$:r,progress$:o}){if(location.protocol==="file:")return y;$(document).subscribe(vi);let n=h(document.body,"click").pipe(Pe(e),b(([a,c])=>hi(a,c)),m(({href:a})=>new URL(a)),le()),i=h(window,"popstate").pipe(m(we),le());n.pipe(te(r)).subscribe(([a,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",a)}),L(n,i).subscribe(t);let s=t.pipe(ne("pathname"),b(a=>xr(a,{progress$:o}).pipe(ve(()=>(st(a,!0),y)))),b(vi),b(bs),le());return L(s.pipe(te(t,(a,c)=>c)),s.pipe(b(()=>t),ne("hash")),t.pipe(Y((a,c)=>a.pathname===c.pathname&&a.hash===c.hash),b(()=>n),O(()=>history.back()))).subscribe(a=>{var c,p;history.state!==null||!a.hash?window.scrollTo(0,(p=(c=history.state)==null?void 0:c.y)!=null?p:0):(history.scrollRestoration="auto",gn(a.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(ne("offset"),Ae(100)).subscribe(({offset:a})=>{history.replaceState(a,"")}),V("navigation.instant.prefetch")&&L(h(document.body,"mousemove"),h(document.body,"focusin")).pipe(Pe(e),b(([a,c])=>hi(a,c)),Ae(25),Qr(({href:a})=>a),hr(a=>{let c=document.createElement("link");return c.rel="prefetch",c.href=a.toString(),document.head.appendChild(c),h(c,"load").pipe(m(()=>c),Ee(1))})).subscribe(a=>a.remove()),s}var yi=$t(ro());function xi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,s)=>`${i}${s}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return s=>(0,yi.default)(s).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function zt(e){return e.type===1}function Sr(e){return e.type===3}function Ei(e,t){let r=Mn(e);return L($(location.protocol!=="file:"),Je("search")).pipe(Re(o=>o),b(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:V("search.suggest")}}})),r}function wi(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=po(n))==null?void 0:l.pathname;if(i===void 0)return;let s=ys(o.pathname,i);if(s===void 0)return;let a=Es(t.keys());if(!t.has(a))return;let c=po(s,a);if(!c||!t.has(c.href))return;let p=po(s,r);if(p)return p.hash=o.hash,p.search=o.search,p}function po(e,t){try{return new URL(e,t)}catch(r){return}}function ys(e,t){if(e.startsWith(t))return e.slice(t.length)}function xs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oy)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:s,aliases:a})=>s===i||a.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),b(n=>h(document.body,"click").pipe(g(i=>!i.metaKey&&!i.ctrlKey),te(o),b(([i,s])=>{if(i.target instanceof Element){let a=i.target.closest("a");if(a&&!a.target&&n.has(a.href)){let c=a.href;return!i.target.closest(".md-version")&&n.get(c)===s?y:(i.preventDefault(),$(new URL(c)))}}return y}),b(i=>kt(i).pipe(m(s=>{var a;return(a=wi({selectedVersionSitemap:s,selectedVersionBaseURL:i,currentLocation:we(),currentBaseURL:t.base}))!=null?a:i})))))).subscribe(n=>st(n,!0)),z([r,o]).subscribe(([n,i])=>{j(".md-header__topic").appendChild(Wn(n,i))}),e.pipe(b(()=>o)).subscribe(n=>{var a;let i=new URL(t.base),s=__md_get("__outdated",sessionStorage,i);if(s===null){s=!0;let c=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let p of c)for(let l of n.aliases.concat(n.version))if(new RegExp(p,"i").test(l)){s=!1;break e}__md_set("__outdated",s,sessionStorage,i)}if(s)for(let c of me("outdated"))c.hidden=!1})}function ws(e,{worker$:t}){let{searchParams:r}=we();r.has("q")&&(at("search",!0),e.value=r.get("q"),e.focus(),Je("search").pipe(Re(i=>!i)).subscribe(()=>{let i=we();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=Ye(e),n=L(t.pipe(Re(zt)),h(e,"keyup"),o).pipe(m(()=>e.value),Y());return z([n,o]).pipe(m(([i,s])=>({value:i,focus:s})),Z(1))}function Si(e,{worker$:t}){let r=new T,o=r.pipe(oe(),ae(!0));z([t.pipe(Re(zt)),r],(i,s)=>s).pipe(ne("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ne("focus")).subscribe(({focus:i})=>{i&&at("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=j("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ws(e,{worker$:t}).pipe(O(i=>r.next(i)),A(()=>r.complete()),m(i=>P({ref:e},i)),Z(1))}function Oi(e,{worker$:t,query$:r}){let o=new T,n=un(e.parentElement).pipe(g(Boolean)),i=e.parentElement,s=j(":scope > :first-child",e),a=j(":scope > :last-child",e);Je("search").subscribe(l=>{a.setAttribute("role",l?"list":"presentation"),a.hidden=!l}),o.pipe(te(r),Gr(t.pipe(Re(zt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:s.textContent=f.length?Me("search.result.none"):Me("search.result.placeholder");break;case 1:s.textContent=Me("search.result.one");break;default:let u=br(l.length);s.textContent=Me("search.result.other",u)}});let c=o.pipe(O(()=>a.innerHTML=""),b(({items:l})=>L($(...l.slice(0,10)),$(...l.slice(10)).pipe(ot(4),Xr(n),b(([f])=>f)))),m(Fn),le());return c.subscribe(l=>a.appendChild(l)),c.pipe(J(l=>{let f=ue("details",l);return typeof f=="undefined"?y:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(g(Sr),m(({data:l})=>l)).pipe(O(l=>o.next(l)),A(()=>o.complete()),m(l=>P({ref:e},l)))}function Ts(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=we();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function Li(e,t){let r=new T,o=r.pipe(oe(),ae(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),Ts(e,t).pipe(O(n=>r.next(n)),A(()=>r.complete()),m(n=>P({ref:e},n)))}function Mi(e,{worker$:t,keyboard$:r}){let o=new T,n=Ce("search-query"),i=L(h(n,"keydown"),h(n,"focus")).pipe(xe(pe),m(()=>n.value),Y());return o.pipe(Pe(i),m(([{suggest:a},c])=>{let p=c.split(/([\s-]+)/);if(a!=null&&a.length&&p[p.length-1]){let l=a[a.length-1];l.startsWith(p[p.length-1])&&(p[p.length-1]=l)}else p.length=0;return p})).subscribe(a=>e.innerHTML=a.join("").replace(/\s/g," ")),r.pipe(g(({mode:a})=>a==="search")).subscribe(a=>{switch(a.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(g(Sr),m(({data:a})=>a)).pipe(O(a=>o.next(a)),A(()=>o.complete()),m(()=>({ref:e})))}function _i(e,{index$:t,keyboard$:r}){let o=Te();try{let n=Ei(o.search,t),i=Ce("search-query",e),s=Ce("search-result",e);h(e,"click").pipe(g(({target:c})=>c instanceof Element&&!!c.closest("a"))).subscribe(()=>at("search",!1)),r.pipe(g(({mode:c})=>c==="search")).subscribe(c=>{let p=Ne();switch(c.type){case"Enter":if(p===i){let l=new Map;for(let f of M(":first-child [href]",s)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}c.claim()}break;case"Escape":case"Tab":at("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof p=="undefined")i.focus();else{let l=[i,...M(":not(details) > [href], summary, details[open] [href]",s)],f=Math.max(0,(Math.max(0,l.indexOf(p))+l.length+(c.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}c.claim();break;default:i!==Ne()&&i.focus()}}),r.pipe(g(({mode:c})=>c==="global")).subscribe(c=>{switch(c.type){case"f":case"s":case"/":i.focus(),i.select(),c.claim();break}});let a=Si(i,{worker$:n});return L(a,Oi(s,{worker$:n,query$:a})).pipe(Ve(...me("search-share",e).map(c=>Li(c,{query$:a})),...me("search-suggest",e).map(c=>Mi(c,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,tt}}function Ai(e,{index$:t,location$:r}){return z([t,r.pipe(Q(we()),g(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>xi(o.config)(n.searchParams.get("h"))),m(o=>{var s;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let a=i.nextNode();a;a=i.nextNode())if((s=a.parentElement)!=null&&s.offsetHeight){let c=a.textContent,p=o(c);p.length>c.length&&n.set(a,p)}for(let[a,c]of n){let{childNodes:p}=x("span",null,c);a.replaceWith(...Array.from(p))}return{ref:e,nodes:n}}))}function Ss(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:s},{offset:{y:a}}])=>(s=s+Math.min(n,Math.max(0,a-i))-n,{height:s,locked:a>=i+n})),Y((i,s)=>i.height===s.height&&i.locked===s.locked))}function lo(e,o){var n=o,{header$:t}=n,r=vo(n,["header$"]);let i=j(".md-sidebar__scrollwrap",e),{y:s}=Be(i);return H(()=>{let a=new T,c=a.pipe(oe(),ae(!0)),p=a.pipe($e(0,ye));return p.pipe(te(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*s}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),p.pipe(Re()).subscribe(()=>{for(let l of M(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2})}}}),fe(M("label[tabindex]",e)).pipe(J(l=>h(l,"click").pipe(xe(pe),m(()=>l),W(c)))).subscribe(l=>{let f=j(`[id="${l.htmlFor}"]`);j(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),V("content.tooltips")&&fe(M("abbr[title]",e)).pipe(J(l=>Xe(l,{viewport$})),W(c)).subscribe(),Ss(e,r).pipe(O(l=>a.next(l)),A(()=>a.complete()),m(l=>P({ref:e},l)))})}function Ci(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return rt(ze(`${r}/releases/latest`).pipe(ve(()=>y),m(o=>({version:o.tag_name})),Qe({})),ze(r).pipe(ve(()=>y),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return ze(r).pipe(m(o=>({repositories:o.public_repos})),Qe({}))}}function ki(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return rt(ze(`${r}/releases/permalink/latest`).pipe(ve(()=>y),m(({tag_name:o})=>({version:o})),Qe({})),ze(r).pipe(ve(()=>y),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Qe({}))).pipe(m(([o,n])=>P(P({},o),n)))}function Hi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return Ci(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ki(r,o)}return y}var Os;function Ls(e){return Os||(Os=H(()=>{let t=__md_get("__source",sessionStorage);if(t)return $(t);if(me("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return y}return Hi(e.href).pipe(O(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>y),g(t=>Object.keys(t).length>0),m(t=>({facts:t})),Z(1)))}function $i(e){let t=j(":scope > :last-child",e);return H(()=>{let r=new T;return r.subscribe(({facts:o})=>{t.appendChild(jn(o)),t.classList.add("md-source__repository--active")}),Ls(e).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function Ms(e,{viewport$:t,header$:r}){return Le(document.body).pipe(b(()=>Er(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ne("hidden"))}function Pi(e,t){return H(()=>{let r=new T;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(V("navigation.tabs.sticky")?$({hidden:!1}):Ms(e,t)).pipe(O(o=>r.next(o)),A(()=>r.complete()),m(o=>P({ref:e},o)))})}function _s(e,{viewport$:t,header$:r}){let o=new Map,n=M(".md-nav__link",e);for(let a of n){let c=decodeURIComponent(a.hash.substring(1)),p=ue(`[id="${c}"]`);typeof p!="undefined"&&o.set(a,p)}let i=r.pipe(ne("height"),m(({height:a})=>{let c=Ce("main"),p=j(":scope > :first-child",c);return a+.8*(p.offsetTop-c.offsetTop)}),le());return Le(document.body).pipe(ne("height"),b(a=>H(()=>{let c=[];return $([...o].reduce((p,[l,f])=>{for(;c.length&&o.get(c[c.length-1]).tagName>=f.tagName;)c.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return p.set([...c=[...c,l]].reverse(),u)},new Map))}).pipe(m(c=>new Map([...c].sort(([,p],[,l])=>p-l))),Pe(i),b(([c,p])=>t.pipe(Ut(([l,f],{offset:{y:u},size:d})=>{let v=u+d.height>=Math.floor(a.height);for(;f.length;){let[,S]=f[0];if(S-p=u&&!v)f=[l.pop(),...f];else break}return[l,f]},[[],[...c]]),Y((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([a,c])=>({prev:a.map(([p])=>p),next:c.map(([p])=>p)})),Q({prev:[],next:[]}),ot(2,1),m(([a,c])=>a.prev.length{let i=new T,s=i.pipe(oe(),ae(!0));if(i.subscribe(({prev:a,next:c})=>{for(let[p]of c)p.classList.remove("md-nav__link--passed"),p.classList.remove("md-nav__link--active");for(let[p,[l]]of a.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",p===a.length-1)}),V("toc.follow")){let a=L(t.pipe(Ae(1),m(()=>{})),t.pipe(Ae(250),m(()=>"smooth")));i.pipe(g(({prev:c})=>c.length>0),Pe(o.pipe(xe(pe))),te(a)).subscribe(([[{prev:c}],p])=>{let[l]=c[c.length-1];if(l.offsetHeight){let f=vr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=de(f);f.scrollTo({top:u-d/2,behavior:p})}}})}return V("navigation.tracking")&&t.pipe(W(s),ne("offset"),Ae(250),Ie(1),W(n.pipe(Ie(1))),vt({delay:250}),te(i)).subscribe(([,{prev:a}])=>{let c=we(),p=a[a.length-1];if(p&&p.length){let[l]=p,{hash:f}=new URL(l.href);c.hash!==f&&(c.hash=f,history.replaceState({},"",`${c}`))}else c.hash="",history.replaceState({},"",`${c}`)}),_s(e,{viewport$:t,header$:r}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))})}function As(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:s}})=>s),ot(2,1),m(([s,a])=>s>a&&a>0),Y()),i=r.pipe(m(({active:s})=>s));return z([i,n]).pipe(m(([s,a])=>!(s&&a)),Y(),W(o.pipe(Ie(1))),ae(!0),vt({delay:250}),m(s=>({hidden:s})))}function Ii(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new T,s=i.pipe(oe(),ae(!0));return i.subscribe({next({hidden:a}){e.hidden=a,a?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(s),ne("height")).subscribe(({height:a})=>{e.style.top=`${a+16}px`}),h(e,"click").subscribe(a=>{a.preventDefault(),window.scrollTo({top:0})}),As(e,{viewport$:t,main$:o,target$:n}).pipe(O(a=>i.next(a)),A(()=>i.complete()),m(a=>P({ref:e},a)))}function Fi({document$:e,viewport$:t}){e.pipe(b(()=>M(".md-ellipsis")),J(r=>mt(r).pipe(W(e.pipe(Ie(1))),g(o=>o),m(()=>r),Ee(1))),g(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,V("content.tooltips")?Xe(n,{viewport$:t}).pipe(W(e.pipe(Ie(1))),A(()=>n.removeAttribute("title"))):y})).subscribe(),V("content.tooltips")&&e.pipe(b(()=>M(".md-status")),J(r=>Xe(r,{viewport$:t}))).subscribe()}function ji({document$:e,tablet$:t}){e.pipe(b(()=>M(".md-toggle--indeterminate")),O(r=>{r.indeterminate=!0,r.checked=!1}),J(r=>h(r,"change").pipe(Jr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),te(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function Cs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ui({document$:e}){e.pipe(b(()=>M("[data-md-scrollfix]")),O(t=>t.removeAttribute("data-md-scrollfix")),g(Cs),J(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Wi({viewport$:e,tablet$:t}){z([Je("search"),t]).pipe(m(([r,o])=>r&&!o),b(r=>$(r).pipe(nt(r?400:100))),te(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ks(){return location.protocol==="file:"?_t(`${new URL("search/search_index.js",Or.base)}`).pipe(m(()=>__index),Z(1)):ze(new URL("search/search_index.json",Or.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ct=an(),Kt=bn(),Ht=yn(Kt),mo=hn(),ke=Ln(),Lr=Wt("(min-width: 60em)"),Vi=Wt("(min-width: 76.25em)"),Ni=xn(),Or=Te(),zi=document.forms.namedItem("search")?ks():tt,fo=new T;di({alert$:fo});ui({document$:ct});var uo=new T,qi=kt(Or.base);V("navigation.instant")&&gi({sitemap$:qi,location$:Kt,viewport$:ke,progress$:uo}).subscribe(ct);var Di;((Di=Or.version)==null?void 0:Di.provider)==="mike"&&Ti({document$:ct});L(Kt,Ht).pipe(nt(125)).subscribe(()=>{at("drawer",!1),at("search",!1)});mo.pipe(g(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ue("link[rel=prev]");typeof t!="undefined"&&st(t);break;case"n":case".":let r=ue("link[rel=next]");typeof r!="undefined"&&st(r);break;case"Enter":let o=Ne();o instanceof HTMLLabelElement&&o.click()}});Fi({viewport$:ke,document$:ct});ji({document$:ct,tablet$:Lr});Ui({document$:ct});Wi({viewport$:ke,tablet$:Lr});var ft=ai(Ce("header"),{viewport$:ke}),qt=ct.pipe(m(()=>Ce("main")),b(e=>pi(e,{viewport$:ke,header$:ft})),Z(1)),Hs=L(...me("consent").map(e=>An(e,{target$:Ht})),...me("dialog").map(e=>ni(e,{alert$:fo})),...me("palette").map(e=>li(e)),...me("progress").map(e=>mi(e,{progress$:uo})),...me("search").map(e=>_i(e,{index$:zi,keyboard$:mo})),...me("source").map(e=>$i(e))),$s=H(()=>L(...me("announce").map(e=>_n(e)),...me("content").map(e=>oi(e,{sitemap$:qi,viewport$:ke,target$:Ht,print$:Ni})),...me("content").map(e=>V("search.highlight")?Ai(e,{index$:zi,location$:Kt}):y),...me("header").map(e=>si(e,{viewport$:ke,header$:ft,main$:qt})),...me("header-title").map(e=>ci(e,{viewport$:ke,header$:ft})),...me("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?eo(Vi,()=>lo(e,{viewport$:ke,header$:ft,main$:qt})):eo(Lr,()=>lo(e,{viewport$:ke,header$:ft,main$:qt}))),...me("tabs").map(e=>Pi(e,{viewport$:ke,header$:ft})),...me("toc").map(e=>Ri(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})),...me("top").map(e=>Ii(e,{viewport$:ke,header$:ft,main$:qt,target$:Ht})))),Ki=ct.pipe(b(()=>$s),Ve(Hs),Z(1));Ki.subscribe();window.document$=ct;window.location$=Kt;window.target$=Ht;window.keyboard$=mo;window.viewport$=ke;window.tablet$=Lr;window.screen$=Vi;window.print$=Ni;window.alert$=fo;window.progress$=uo;window.component$=Ki;})(); -//# sourceMappingURL=bundle.e71a0d61.min.js.map - diff --git a/site/assets/javascripts/bundle.e71a0d61.min.js.map b/site/assets/javascripts/bundle.e71a0d61.min.js.map deleted file mode 100644 index 23451b54..00000000 --- a/site/assets/javascripts/bundle.e71a0d61.min.js.map +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 3, - "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinct.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/exhaustMap.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/link/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/alternate/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], - "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2025 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n fetchSitemap,\n setupAlternate,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 60em)\")\nconst screen$ = watchMedia(\"(min-width: 76.25em)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up language selector */\nsetupAlternate({ document$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up sitemap for instant navigation and previews */\nconst sitemap$ = fetchSitemap(config.base)\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ sitemap$, location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { sitemap$, viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n */\nexport class Subscription implements SubscriptionLike {\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param value The `next` value.\n */\n next(value: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param err The `error` exception.\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as ((value: T) => void) | undefined,\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent.\n * @param subscriber The stopped subscriber.\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @param subscribe The function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @param subscribe the subscriber function to be passed to the Observable constructor\n * @return A new observable.\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @param operator the operator defining the operation to take on the observable\n * @return A new observable with the Operator applied.\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param observerOrNext Either an {@link Observer} with some or all callback methods,\n * or the `next` handler that is called for each value emitted from the subscribed Observable.\n * @param error A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param complete A handler for a terminal event resulting from successful completion.\n * @return A subscription reference to the registered handlers.\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next A handler for each value emitted by the observable.\n * @return A promise that either resolves on observable completion or\n * rejects with the handled error.\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @return This instance of the observable.\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n *\n * @return The Observable result of all the operators having been called\n * in the order they were passed in.\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return Observable that this Subject casts to.\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param _bufferSize The size of the buffer to replay on subscription\n * @param _windowTime The amount of time the buffered items will stay buffered\n * @param _timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param state Some contextual data that the `work` function uses when called by the\n * Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is implicit\n * and defined by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param work A function representing a task, or some unit of work to be\n * executed by the Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is\n * implicit and defined by the Scheduler itself.\n * @param state Some contextual data that the `work` function uses when called\n * by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && id === scheduler._scheduled && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n let flushId;\n if (action) {\n flushId = action.id;\n } else {\n flushId = this._scheduled;\n this._scheduled = undefined;\n }\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an