Repository: webonnx/wonnx (language: python)

Issue:
Support data types other than f32
This PR (based on my earlier PR #45 that implemented the DAG) adds support for data types other than `f32`. Right now I have implemented support for `i32` inputs, but others (such as `f16`) should be fairly easy to add. The current version will always cast outputs to `Vec<f32>` but of course the output type should depend on the actual output type in the model.

Notably, BERT and some other models appear to use `int64` inputs - working with i64 in WGSL however is not (yet?) possible. We should therefore probably re-interpret these as `int32`. 

Some implementation notes:

* `Shape` now has a `data_type` field of type `ScalarType`. The latter holds some information about the type's size and striding.
* In `struct.wgsl`, I define a type `Scalar` to be the 'default' scalar type (all ops except those that do conversions work in a single type) based on template variable `scalar_type`. It also defines `Vec3`, `Vec4`, `Mat4x4` etc. which are the appropriate vector/matrix types. All shaders use these type names instead of the raw ones.
* The compiler sets `scalar_type` variable by looking at the types of inputs/outputs. They should all agree (except in cases where there may be inputs/outputs of different type, which happens e.g. for `Reshape` where the first input is the data and the second input is a shape, with integer data type) or else an error is thrown.
* Input tensors are now wrapped in an enum, e.g. `InputTensor::F32(my_input.as_slice())`. Output tensors are currently still all of type `Vec<f32>` (when the actual output data type is different the current version casts the elements to `f32` first).

See `tests/arithmetic.rs` for an example of integer addition.
Make comparison between CPU and GPU less strict
**Is your feature request related to a problem? Please describe.**
Experimentation shows that results on NVIDIA GPUs is a bit further from CPU results, for some reason, than it is on e.g. Apple M1. An example on an 1080 Ti:

````sh
cargo run --features=cpu --release -- infer ./data/models/opt-squeeze.onnx -i data=./data/images/pelican.jpeg --labels ./data/models/squeeze-labels.txt --top 3 --compare --benchmark
Error: Comparison("output element 285 differs too much: GPU says 8.999586 vs CPU says 8.999575 (difference is 0.000011444092)")
````

**Describe the solution you'd like**

Allow slightly more difference to exist between CPU and GPU before showing a warning (or make this configurable). 



Discussion / hints:
@haixuanTao I rebased this on top of your workspace PR, this one should be ready to go now. 


Fix patch:
diff --git a/wonnx/.gitattributes b/.gitattributes
similarity index 100%
rename from wonnx/.gitattributes
rename to .gitattributes
diff --git a/.github/deploy_manylinux.sh b/.github/deploy_manylinux.sh
index cc93f0de..df70c052 100644
--- a/.github/deploy_manylinux.sh
+++ b/.github/deploy_manylinux.sh
@@ -1,4 +1,4 @@
-cd py-wonnx
+cd wonnx-py
 rustup override set nightly-2022-01-01
 export RUSTFLAGS='-C target-feature=+fxsr,+sse,+sse2,+sse3,+ssse3,+sse4.1,+popcnt'
 maturin publish \
diff --git a/.github/workflows/create-py-mac.yaml b/.github/workflows/create-py-mac.yaml
index f2270eee..95a4fc13 100644
--- a/.github/workflows/create-py-mac.yaml
+++ b/.github/workflows/create-py-mac.yaml
@@ -4,7 +4,7 @@ on:
   push:
     # Sequence of patterns matched against refs/tags
     tags:
-      - 'py-wonnx-v*' # Push events to matching py-wonnx-v*
+      - "wonnx-py-v*" # Push events to matching wonnx-py-v*
 
 jobs:
   build:
@@ -32,7 +32,7 @@ jobs:
       - name: Prepare maturin publish
         shell: bash
         run: |
-          cd py-wonnx
+          cd wonnx-py
           rustup override set nightly-2022-01-01
       - name: maturin publish
         uses: messense/maturin-action@v1
@@ -41,4 +41,4 @@ jobs:
         with:
           maturin-version: 0.12.1
           command: publish
-          args: -m py-wonnx/Cargo.toml --no-sdist --universal2 -o wheels -i python -u __token__
\ No newline at end of file
+          args: -m wonnx-py/Cargo.toml --no-sdist --universal2 -o wheels -i python -u __token__
diff --git a/.github/workflows/create-py-manylinux.yaml b/.github/workflows/create-py-manylinux.yaml
index 6a8f59a6..241107c1 100644
--- a/.github/workflows/create-py-manylinux.yaml
+++ b/.github/workflows/create-py-manylinux.yaml
@@ -4,7 +4,7 @@ on:
   push:
     # Sequence of patterns matched against refs/tags
     tags:
-      - 'py-wonnx-v*' # Push events to matching py-wonnx-v*
+      - "wonnx-py-v*" # Push events to matching wonnx-py-v*
 jobs:
   build_manylinux:
     name: Create Release manylinux
@@ -17,4 +17,4 @@ jobs:
           MATURIN_PASSWORD: ${{ secrets.PYPI_PASS }}
         with:
           entrypoint: /bin/bash
-          args: .github/deploy_manylinux.sh
\ No newline at end of file
+          args: .github/deploy_manylinux.sh
diff --git a/.github/workflows/create-py-windows.yaml b/.github/workflows/create-py-windows.yaml
index b42c3d15..8bc7668d 100644
--- a/.github/workflows/create-py-windows.yaml
+++ b/.github/workflows/create-py-windows.yaml
@@ -2,9 +2,9 @@ name: Create Python release windows macos
 
 on:
   push:
-     # Sequence of patterns matched against refs/tags
+    # Sequence of patterns matched against refs/tags
     tags:
-      - 'py-wonnx-v*' # Push events to matching py-wonnx-v*
+      - "wonnx-py-v*" # Push events to matching wonnx-py-v*
 jobs:
   build:
     name: Create Release
@@ -14,32 +14,32 @@ jobs:
         os: ["macos-latest", "windows-latest"]
         python-version: ["3.7"]
     steps:
-        - uses: actions/checkout@v2
-        - name: Install latest Rust nightly
-          uses: actions-rs/toolchain@v1
-          with:
-            toolchain: nightly-2022-01-01
-            override: true
-            components: rustfmt, clippy
-        - name: Set up Python
-          uses: actions/setup-python@v2
-          with:
-            python-version: ${{ matrix.python-version }}
-        - name: Install dependencies
-          run: |
-            python -m pip install --upgrade pip
-            pip install maturin==0.12.1
-        - name: Publish wheel
-          shell: bash
-          env:
-            MATURIN_PASSWORD: ${{ secrets.PYPI_PASS }}
-          run: |
-            cd py-wonnx
-            rustup override set nightly-2022-01-01
-            export RUSTFLAGS='-C target-feature=+fxsr,+sse,+sse2,+sse3'
-            maturin publish \
-            --no-sdist \
-            --skip-existing \
-            -o wheels \
-            -i python \
-            --username __token__ \
\ No newline at end of file
+      - uses: actions/checkout@v2
+      - name: Install latest Rust nightly
+        uses: actions-rs/toolchain@v1
+        with:
+          toolchain: nightly-2022-01-01
+          override: true
+          components: rustfmt, clippy
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install maturin==0.12.1
+      - name: Publish wheel
+        shell: bash
+        env:
+          MATURIN_PASSWORD: ${{ secrets.PYPI_PASS }}
+        run: |
+          cd wonnx-py
+          rustup override set nightly-2022-01-01
+          export RUSTFLAGS='-C target-feature=+fxsr,+sse,+sse2,+sse3'
+          maturin publish \
+          --no-sdist \
+          --skip-existing \
+          -o wheels \
+          -i python \
+          --username __token__ \
diff --git a/.gitignore b/.gitignore
index cf1ca767..0a9164e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 /target
-**/*.onnx
 /coverage
 *.profraw
 .DS_Store
-/examples/data/
-synset.txt
+.env
+/wonnx-py/target
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..42061c01
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,1 @@
+README.md
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 46a054d1..3dad6baa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -20,7 +20,7 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.4",
  "once_cell",
  "version_check",
 ]
@@ -34,6 +34,27 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
+
+[[package]]
+name = "anymap2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
+
 [[package]]
 name = "approx"
 version = "0.5.1"
@@ -43,6 +64,18 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
 [[package]]
 name = "arrayvec"
 version = "0.7.2"
@@ -58,6 +91,31 @@ dependencies = [
  "libloading",
 ]
 
+[[package]]
+name = "assert_cmd"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e"
+dependencies = [
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -75,6 +133,12 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
+[[package]]
+name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
 [[package]]
 name = "base64"
 version = "0.13.0"
@@ -102,6 +166,17 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "blake2b_simd"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
+dependencies = [
+ "arrayref",
+ "arrayvec 0.5.2",
+ "constant_time_eq",
+]
+
 [[package]]
 name = "block"
 version = "0.1.6"
@@ -117,7 +192,16 @@ dependencies = [
  "block-padding",
  "byte-tools",
  "byteorder",
- "generic-array",
+ "generic-array 0.12.4",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array 0.14.5",
 ]
 
 [[package]]
@@ -135,7 +219,10 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
 dependencies = [
+ "lazy_static",
  "memchr",
+ "regex-automata",
+ "serde",
 ]
 
 [[package]]
@@ -168,6 +255,50 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
 
+[[package]]
+name = "bzip2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.11+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "cached-path"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2e84e6a7366ce5a2d7ce7fb466610d4d906d89981bc93215e215937a2c51ce"
+dependencies = [
+ "flate2",
+ "fs2",
+ "glob",
+ "indicatif",
+ "log",
+ "rand 0.8.4",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tar",
+ "tempfile",
+ "thiserror",
+ "zip",
+ "zip-extensions",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.72"
@@ -195,6 +326,7 @@ dependencies = [
  "libc",
  "num-integer",
  "num-traits",
+ "time",
  "winapi",
 ]
 
@@ -221,10 +353,19 @@ dependencies = [
 ]
 
 [[package]]
-name = "chunked_transfer"
-version = "1.4.0"
+name = "clap"
+version = "2.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim 0.8.0",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
 
 [[package]]
 name = "codespan-reporting"
@@ -242,6 +383,21 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
+[[package]]
+name = "console"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "regex",
+ "terminal_size",
+ "unicode-width",
+ "winapi",
+]
+
 [[package]]
 name = "console_error_panic_hook"
 version = "0.1.7"
@@ -262,6 +418,12 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
 [[package]]
 name = "copyless"
 version = "0.1.5"
@@ -296,6 +458,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "crc32fast"
 version = "1.3.1"
@@ -349,6 +520,28 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "csv"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
+dependencies = [
+ "bstr",
+ "csv-core",
+ "itoa 0.4.8",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "cty"
 version = "0.2.2"
@@ -366,6 +559,41 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "darling"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim 0.9.3",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "deflate"
 version = "0.8.6"
@@ -376,132 +604,393 @@ dependencies = [
  "byteorder",
 ]
 
+[[package]]
+name = "derive-new"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
+dependencies = [
+ "darling",
+ "derive_builder_core",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "deunicode"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
 
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
 [[package]]
 name = "digest"
 version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
 dependencies = [
- "generic-array",
+ "generic-array 0.12.4",
 ]
 
 [[package]]
-name = "either"
-version = "1.6.1"
+name = "digest"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array 0.14.5",
+]
 
 [[package]]
-name = "env_logger"
-version = "0.9.0"
+name = "dirs"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
 dependencies = [
- "atty",
- "humantime",
- "log",
- "regex",
- "termcolor",
+ "libc",
+ "redox_users 0.3.5",
+ "winapi",
 ]
 
 [[package]]
-name = "fake-simd"
-version = "0.1.2"
+name = "dirs"
+version = "3.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
+dependencies = [
+ "dirs-sys",
+]
 
 [[package]]
-name = "flate2"
-version = "1.0.22"
+name = "dirs-sys"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
+checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
 dependencies = [
- "cfg-if",
- "crc32fast",
  "libc",
- "miniz_oxide 0.4.4",
+ "redox_users 0.4.0",
+ "winapi",
 ]
 
 [[package]]
-name = "fnv"
-version = "1.0.7"
+name = "doc-comment"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
 [[package]]
-name = "foreign-types"
-version = "0.3.2"
+name = "downcast-rs"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
+checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
 
 [[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
+name = "dyn-clone"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
 
 [[package]]
-name = "form_urlencoded"
-version = "1.0.1"
+name = "educe"
+version = "0.4.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+checksum = "f86b50932a01e7ec5c06160492ab660fb19b6bb2a7878030dd6cd68d21df9d4d"
 dependencies = [
- "matches",
- "percent-encoding",
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
-name = "fxhash"
-version = "0.2.1"
+name = "either"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
-dependencies = [
- "byteorder",
-]
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
 
 [[package]]
-name = "generic-array"
-version = "0.12.4"
+name = "encode_unicode"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
-dependencies = [
- "typenum",
-]
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
 [[package]]
-name = "getrandom"
-version = "0.2.4"
+name = "encoding_rs"
+version = "0.8.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
+checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
 dependencies = [
  "cfg-if",
- "js-sys",
- "libc",
- "wasi",
- "wasm-bindgen",
 ]
 
 [[package]]
-name = "gif"
-version = "0.11.3"
+name = "enum-ordinalize"
+version = "3.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b"
+checksum = "0b166c9e378360dd5a6666a9604bb4f54ae0cac39023ffbac425e917a2a04fef"
 dependencies = [
- "color_quant",
- "weezl",
-]
-
-[[package]]
-name = "globset"
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "esaxx-rs"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f4617b351b734e97a3dd32022a721471349aa3038d4132beee8568cdfa7e716"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
+[[package]]
+name = "fastrand"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.2.10",
+ "winapi",
+]
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e"
+
+[[package]]
+name = "flate2"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
+dependencies = [
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide 0.4.4",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
+
+[[package]]
+name = "futures-io"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
+
+[[package]]
+name = "futures-task"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
+
+[[package]]
+name = "futures-util"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "globset"
 version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
@@ -575,6 +1064,31 @@ dependencies = [
  "bitflags",
 ]
 
+[[package]]
+name = "h2"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+
 [[package]]
 name = "hashbrown"
 version = "0.11.2"
@@ -584,6 +1098,15 @@ dependencies = [
  "ahash",
 ]
 
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "hermit-abi"
 version = "0.1.19"
@@ -599,6 +1122,40 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
 
+[[package]]
+name = "http"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 1.0.1",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
 [[package]]
 name = "humansize"
 version = "1.1.1"
@@ -611,6 +1168,49 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
+[[package]]
+name = "hyper"
+version = "0.14.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa 0.4.8",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "0.2.3"
@@ -669,6 +1269,18 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "indicatif"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4"
+dependencies = [
+ "console",
+ "lazy_static",
+ "number_prefix",
+ "regex",
+]
+
 [[package]]
 name = "indoc"
 version = "0.3.6"
@@ -711,18 +1323,57 @@ dependencies = [
 ]
 
 [[package]]
-name = "itoa"
-version = "1.0.1"
+name = "ipnet"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
 
 [[package]]
-name = "jpeg-decoder"
-version = "0.1.22"
+name = "itertools"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
 dependencies = [
- "rayon",
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+
+[[package]]
+name = "jpeg-decoder"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+dependencies = [
+ "rayon",
 ]
 
 [[package]]
@@ -744,12 +1395,34 @@ dependencies = [
  "libloading",
 ]
 
+[[package]]
+name = "kstring"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b310ccceade8121d7d77fee406160e457c2f4e7c7982d589da3499bc7ea4526"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "lexical-core"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+dependencies = [
+ "arrayvec 0.5.2",
+ "bitflags",
+ "cfg-if",
+ "ryu",
+ "static_assertions",
+]
+
 [[package]]
 name = "libc"
 version = "0.2.117"
@@ -766,6 +1439,64 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "liquid"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26e930310cf4334c4936ae18737500a57739c69442b5c42bae114d619af54b82"
+dependencies = [
+ "doc-comment",
+ "kstring",
+ "liquid-core",
+ "liquid-derive",
+ "liquid-lib",
+ "serde",
+]
+
+[[package]]
+name = "liquid-core"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eae470f061bfc53607283906de925ab67ed57a341e827146e3b241699a1dcde"
+dependencies = [
+ "anymap2",
+ "chrono",
+ "itertools 0.10.3",
+ "kstring",
+ "liquid-derive",
+ "num-traits",
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "liquid-derive"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6510e456700da1afe07603913b0da5a2595f2482656ade07abf719aae7501f0a"
+dependencies = [
+ "proc-macro2",
+ "proc-quote",
+ "syn",
+]
+
+[[package]]
+name = "liquid-lib"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6341259f779ff663bdf1fc478bddb2ca51fda25414006dc69395eddfac07e0a4"
+dependencies = [
+ "chrono",
+ "itertools 0.10.3",
+ "kstring",
+ "liquid-core",
+ "once_cell",
+ "percent-encoding",
+ "regex",
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "lock_api"
 version = "0.4.6"
@@ -799,6 +1530,16 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
 
+[[package]]
+name = "mapr"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a28a55dbc005b2f6f123c4058933d57add373d362f6fd3a76aab4fe6973500"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "matches"
 version = "0.1.9"
@@ -843,6 +1584,18 @@ dependencies = [
  "objc",
 ]
 
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
 [[package]]
 name = "miniz_oxide"
 version = "0.3.7"
@@ -862,6 +1615,34 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "mio"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "multimap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
+
 [[package]]
 name = "naga"
 version = "0.8.5"
@@ -880,6 +1661,24 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "ndarray"
 version = "0.15.4"
@@ -893,6 +1692,37 @@ dependencies = [
  "rawpointer",
 ]
 
+[[package]]
+name = "nom"
+version = "5.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
+dependencies = [
+ "lexical-core",
+ "memchr",
+ "version_check",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+ "version_check",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "num"
 version = "0.4.0"
@@ -990,6 +1820,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "number_prefix"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
+
 [[package]]
 name = "objc"
 version = "0.2.7"
@@ -1015,12 +1851,73 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
 
+[[package]]
+name = "onig"
+version = "6.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ddfe2c93bb389eea6e6d713306880c7f6dcc99a75b659ce145d962c861b225"
+dependencies = [
+ "bitflags",
+ "lazy_static",
+ "libc",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dd3eee045c84695b53b20255bb7317063df090b68e18bfac0abb6c39cf7f33e"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
 [[package]]
 name = "opaque-debug"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
 
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openssl"
+version = "0.10.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.11.2"
@@ -1041,7 +1938,7 @@ dependencies = [
  "cfg-if",
  "instant",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.2.10",
  "smallvec",
  "winapi",
 ]
@@ -1065,6 +1962,12 @@ dependencies = [
  "proc-macro-hack",
 ]
 
+[[package]]
+name = "paste"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5"
+
 [[package]]
 name = "paste-impl"
 version = "0.1.18"
@@ -1074,6 +1977,33 @@ dependencies = [
  "proc-macro-hack",
 ]
 
+[[package]]
+name = "paw"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09c0fc9b564dbc3dc2ed7c92c0c144f4de340aa94514ce2b446065417c4084e9"
+dependencies = [
+ "paw-attributes",
+ "paw-raw",
+]
+
+[[package]]
+name = "paw-attributes"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f35583365be5d148e959284f42526841917b7bfa09e2d1a7ad5dde2cf0eaa39"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "paw-raw"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f0b59668fe80c5afe998f0c0bf93322bf2cd66cafeeb80581f291716f3467f2"
+
 [[package]]
 name = "percent-encoding"
 version = "2.1.0"
@@ -1123,6 +2053,16 @@ dependencies = [
  "sha-1",
 ]
 
+[[package]]
+name = "petgraph"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
 [[package]]
 name = "phf"
 version = "0.10.1"
@@ -1149,7 +2089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
 dependencies = [
  "phf_shared",
- "rand",
+ "rand 0.8.4",
 ]
 
 [[package]]
@@ -1162,6 +2102,24 @@ dependencies = [
  "uncased",
 ]
 
+[[package]]
+name = "pin-project-lite"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
+
 [[package]]
 name = "png"
 version = "0.16.8"
@@ -1187,43 +2145,175 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
 
 [[package]]
-name = "proc-macro-hack"
-version = "0.5.19"
+name = "predicates"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
+dependencies = [
+ "difflib",
+ "itertools 0.10.3",
+ "predicates-core",
+]
 
 [[package]]
-name = "proc-macro2"
-version = "1.0.36"
+name = "predicates-core"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
-dependencies = [
- "unicode-xid",
-]
+checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
 
 [[package]]
-name = "profiling"
+name = "predicates-tree"
 version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9145ac0af1d93c638c98c40cf7d25665f427b2a44ad0a99b1dccf3e2f25bb987"
+checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
 
 [[package]]
-name = "protobuf"
-version = "2.27.1"
+name = "prettytable-rs"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96"
+checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e"
 dependencies = [
- "bytes",
+ "atty",
+ "csv",
+ "encode_unicode",
+ "lazy_static",
+ "term",
+ "unicode-width",
 ]
 
 [[package]]
-name = "py-wonnx"
-version = "0.1.1"
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
 dependencies = [
- "pollster",
- "protobuf",
- "pyo3",
- "wonnx",
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "proc-quote"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e84ab161de78c915302ca325a19bee6df272800e2ae1a43fe3ef430bab2a100"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "proc-quote-impl",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-quote-impl"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb3ec628b063cdbcf316e06a8b8c1a541d28fa6c0a8eacd2bfb2b7f49e88aa0"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9145ac0af1d93c638c98c40cf7d25665f427b2a44ad0a99b1dccf3e2f25bb987"
+
+[[package]]
+name = "prost"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools 0.10.3",
+ "lazy_static",
+ "log",
+ "multimap",
+ "petgraph",
+ "prost",
+ "prost-types",
+ "regex",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe"
+dependencies = [
+ "anyhow",
+ "itertools 0.10.3",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a"
+dependencies = [
+ "bytes",
+ "prost",
+]
+
+[[package]]
+name = "protobuf"
+version = "2.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96"
+dependencies = [
+ "bytes",
 ]
 
 [[package]]
@@ -1236,7 +2326,7 @@ dependencies = [
  "indoc",
  "libc",
  "parking_lot",
- "paste",
+ "paste 0.1.18",
  "pyo3-build-config",
  "pyo3-macros",
  "unindent",
@@ -1283,6 +2373,19 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
+]
+
 [[package]]
 name = "rand"
 version = "0.8.4"
@@ -1290,9 +2393,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
- "rand_hc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.3",
+ "rand_hc 0.3.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
 ]
 
 [[package]]
@@ -1302,7 +2415,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
 ]
 
 [[package]]
@@ -1311,7 +2433,16 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.4",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
 ]
 
 [[package]]
@@ -1320,7 +2451,7 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
 dependencies = [
- "rand_core",
+ "rand_core 0.6.3",
 ]
 
 [[package]]
@@ -1356,6 +2487,17 @@ dependencies = [
  "rayon-core",
 ]
 
+[[package]]
+name = "rayon-cond"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd1259362c9065e5ea39a789ef40b1e3fd934c94beb7b5ab3ac6629d3b5e7cb7"
+dependencies = [
+ "either",
+ "itertools 0.8.2",
+ "rayon",
+]
+
 [[package]]
 name = "rayon-core"
 version = "1.9.1"
@@ -1369,6 +2511,12 @@ dependencies = [
  "num_cpus",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.10"
@@ -1378,6 +2526,27 @@ dependencies = [
  "bitflags",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
+dependencies = [
+ "getrandom 0.1.16",
+ "redox_syscall 0.1.57",
+ "rust-argon2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom 0.2.4",
+ "redox_syscall 0.2.10",
+]
+
 [[package]]
 name = "regex"
 version = "1.5.4"
@@ -1389,12 +2558,27 @@ dependencies = [
  "regex-syntax",
 ]
 
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
 [[package]]
 name = "regex-syntax"
 version = "0.6.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
 
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "renderdoc-sys"
 version = "0.7.1"
@@ -1402,37 +2586,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157"
 
 [[package]]
-name = "ring"
-version = "0.16.20"
+name = "reqwest"
+version = "0.11.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525"
 dependencies = [
- "cc",
- "libc",
- "once_cell",
- "spin",
- "untrusted",
+ "base64 0.13.0",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
  "web-sys",
- "winapi",
+ "winreg",
 ]
 
 [[package]]
-name = "rustc-hash"
-version = "1.1.0"
+name = "rust-argon2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
+dependencies = [
+ "base64 0.13.0",
+ "blake2b_simd",
+ "constant_time_eq",
+ "crossbeam-utils",
+]
 
 [[package]]
-name = "rustls"
-version = "0.20.2"
+name = "rustc-hash"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84"
-dependencies = [
- "log",
- "ring",
- "sct",
- "webpki",
-]
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "ryu"
@@ -1449,6 +2654,16 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
 [[package]]
 name = "scoped-tls"
 version = "1.0.0"
@@ -1468,13 +2683,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
-name = "sct"
-version = "0.7.0"
+name = "security-framework"
+version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
 dependencies = [
- "ring",
- "untrusted",
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
 ]
 
 [[package]]
@@ -1482,6 +2710,9 @@ name = "serde"
 version = "1.0.136"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+dependencies = [
+ "serde_derive",
+]
 
 [[package]]
 name = "serde_derive"
@@ -1500,7 +2731,19 @@ version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
 dependencies = [
- "itoa",
+ "itoa 1.0.1",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.1",
  "ryu",
  "serde",
 ]
@@ -1511,154 +2754,528 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
 dependencies = [
- "block-buffer",
- "digest",
+ "block-buffer 0.7.3",
+ "digest 0.8.1",
  "fake-simd",
- "opaque-debug",
+ "opaque-debug 0.2.3",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug 0.3.0",
+]
+
+[[package]]
+name = "siphasher"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e"
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "slotmap"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "slug"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+dependencies = [
+ "deunicode",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "socket2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "spirv"
+version = "0.2.0+1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830"
+dependencies = [
+ "bitflags",
+ "num-traits",
+]
+
+[[package]]
+name = "spm_precompiled"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46e3ef0f38d59930678abc6404a61c98831a413111bbb3f6fca47683043ea715"
+dependencies = [
+ "base64 0.12.3",
+ "nom 5.1.2",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "strsim"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
+
+[[package]]
+name = "structopt"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
+dependencies = [
+ "clap",
+ "lazy_static",
+ "paw",
+ "structopt-derive",
+]
+
+[[package]]
+name = "structopt-derive"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall 0.2.10",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "tera"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3cac831b615c25bcef632d1cabf864fa05813baad3d526829db18eb70e8b58d"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding",
+ "pest",
+ "pest_derive",
+ "rand 0.8.4",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "unic-segment",
+]
+
+[[package]]
+name = "term"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
+dependencies = [
+ "byteorder",
+ "dirs 1.0.5",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "termtree"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tiff"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+dependencies = [
+ "jpeg-decoder",
+ "miniz_oxide 0.4.4",
+ "weezl",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokenizers"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f5c159c4c2efb7d3fbed139b726cb5c0135b12d68e2c09afc1a851f8f2534e8"
+dependencies = [
+ "cached-path",
+ "clap",
+ "derive_builder",
+ "dirs 3.0.2",
+ "esaxx-rs",
+ "indicatif",
+ "itertools 0.9.0",
+ "lazy_static",
+ "log",
+ "onig",
+ "rand 0.7.3",
+ "rayon",
+ "rayon-cond",
+ "regex",
+ "regex-syntax",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "spm_precompiled",
+ "unicode-normalization-alignments",
+ "unicode-segmentation",
+ "unicode_categories",
 ]
 
 [[package]]
-name = "siphasher"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e"
-
-[[package]]
-name = "slotmap"
-version = "1.0.6"
+name = "tokio"
+version = "1.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
+checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
 dependencies = [
- "version_check",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "winapi",
 ]
 
 [[package]]
-name = "slug"
-version = "0.1.4"
+name = "tokio-native-tls"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
 dependencies = [
- "deunicode",
+ "native-tls",
+ "tokio",
 ]
 
 [[package]]
-name = "smallvec"
-version = "1.8.0"
+name = "tokio-util"
+version = "0.6.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
 
 [[package]]
-name = "spin"
-version = "0.5.2"
+name = "tower-service"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
 
 [[package]]
-name = "spirv"
-version = "0.2.0+1.5.4"
+name = "tracing"
+version = "0.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830"
+checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9"
 dependencies = [
- "bitflags",
- "num-traits",
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
 ]
 
 [[package]]
-name = "syn"
-version = "1.0.86"
+name = "tracing-core"
+version = "0.1.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
+checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
 dependencies = [
- "proc-macro2",
- "quote",
- "unicode-xid",
+ "lazy_static",
 ]
 
 [[package]]
-name = "tera"
-version = "1.15.0"
+name = "tract-core"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3cac831b615c25bcef632d1cabf864fa05813baad3d526829db18eb70e8b58d"
+checksum = "0ec224e77668b0857a0b93b21994323672e53479057cfa8fb4a6987086c9c879"
 dependencies = [
- "chrono",
- "chrono-tz",
- "globwalk",
- "humansize",
+ "anyhow",
+ "bit-set",
+ "derive-new",
+ "downcast-rs",
+ "dyn-clone",
+ "educe",
+ "half",
  "lazy_static",
- "percent-encoding",
- "pest",
- "pest_derive",
- "rand",
- "regex",
- "serde",
- "serde_json",
- "slug",
- "unic-segment",
+ "log",
+ "maplit",
+ "ndarray",
+ "num-integer",
+ "num-traits",
+ "smallvec",
+ "tract-data",
+ "tract-linalg",
 ]
 
 [[package]]
-name = "termcolor"
-version = "1.1.2"
+name = "tract-data"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+checksum = "1609c2b286a347d3040c7234e204de170920ec3709b15521defe098c0f8a5a67"
 dependencies = [
- "winapi-util",
+ "anyhow",
+ "educe",
+ "half",
+ "itertools 0.10.3",
+ "lazy_static",
+ "maplit",
+ "ndarray",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "smallvec",
 ]
 
 [[package]]
-name = "thiserror"
-version = "1.0.30"
+name = "tract-hir"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+checksum = "b579830105a172704676ca4da670931010577070e869724be6740ec43d4cc3eb"
 dependencies = [
- "thiserror-impl",
+ "derive-new",
+ "educe",
+ "log",
+ "tract-core",
 ]
 
 [[package]]
-name = "thiserror-impl"
-version = "1.0.30"
+name = "tract-linalg"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+checksum = "7f51d85987d90f5e6479dbe203cbf259eb9208ff4e1c94870ab0ca61492171bb"
 dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "cc",
+ "derive-new",
+ "downcast-rs",
+ "dyn-clone",
+ "educe",
+ "half",
+ "lazy_static",
+ "libc",
+ "liquid",
+ "log",
+ "num-traits",
+ "paste 1.0.6",
+ "smallvec",
+ "tract-data",
+ "unicode-normalization",
+ "walkdir",
 ]
 
 [[package]]
-name = "thread_local"
-version = "1.1.4"
+name = "tract-nnef"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+checksum = "d6b15a11fea45a9f69c595cf386c87e4c3cd45d1323e7b5f430004a8cd11d2c4"
 dependencies = [
- "once_cell",
+ "byteorder",
+ "flate2",
+ "log",
+ "nom 7.1.0",
+ "tar",
+ "tract-core",
+ "walkdir",
 ]
 
 [[package]]
-name = "tiff"
-version = "0.6.1"
+name = "tract-onnx"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
+checksum = "7a9a6eb2116cdab535b5b9f1771e82acebea419f2571bd688e267451de3506ce"
 dependencies = [
- "jpeg-decoder",
- "miniz_oxide 0.4.4",
- "weezl",
+ "bytes",
+ "derive-new",
+ "educe",
+ "log",
+ "mapr",
+ "num-integer",
+ "prost",
+ "prost-build",
+ "smallvec",
+ "tract-hir",
+ "tract-nnef",
+ "tract-onnx-opl",
 ]
 
 [[package]]
-name = "tinyvec"
-version = "1.5.1"
+name = "tract-onnx-opl"
+version = "0.15.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+checksum = "a45ed1d95f1deef2a6240d296fa0eaf97dc4b15df32be7a5220ee825e0a3fc4d"
 dependencies = [
- "tinyvec_macros",
+ "educe",
+ "tract-nnef",
 ]
 
 [[package]]
-name = "tinyvec_macros"
-version = "0.1.0"
+name = "try-lock"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
 [[package]]
 name = "typenum"
@@ -1746,6 +3363,21 @@ dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "unicode-normalization-alignments"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
+
 [[package]]
 name = "unicode-width"
 version = "0.1.9"
@@ -1759,33 +3391,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
 
 [[package]]
-name = "unindent"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
-
-[[package]]
-name = "untrusted"
-version = "0.7.1"
+name = "unicode_categories"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
 [[package]]
-name = "ureq"
-version = "2.4.0"
+name = "unindent"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5"
-dependencies = [
- "base64",
- "chunked_transfer",
- "flate2",
- "log",
- "once_cell",
- "rustls",
- "url",
- "webpki",
- "webpki-roots",
-]
+checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
 
 [[package]]
 name = "url"
@@ -1799,12 +3414,33 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "walkdir"
 version = "2.3.2"
@@ -1816,6 +3452,22 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
 [[package]]
 name = "wasi"
 version = "0.10.2+wasi-snapshot-preview1"
@@ -1922,25 +3574,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "webpki"
-version = "0.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "0.22.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449"
-dependencies = [
- "webpki",
-]
-
 [[package]]
 name = "weezl"
 version = "0.1.5"
@@ -1953,7 +3586,7 @@ version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b97cd781ff044d6d697b632a2e212032c2e957d1afaa21dbf58069cbb8f78567"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.7.2",
  "js-sys",
  "log",
  "naga",
@@ -1974,7 +3607,7 @@ version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4688c000eb841ca55f7b35db659b78d6e1cd77d7caf8fb929f4e181f754047d"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.7.2",
  "bitflags",
  "cfg_aliases",
  "codespan-reporting",
@@ -1997,7 +3630,7 @@ version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "93b1a9400e8d7f32dd4dd909bb9a391015d70633d639775ddd3f14d1104bc970"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.7.2",
  "ash",
  "bit-set",
  "bitflags",
@@ -2038,6 +3671,17 @@ dependencies = [
  "bitflags",
 ]
 
+[[package]]
+name = "which"
+version = "4.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2"
+dependencies = [
+ "either",
+ "lazy_static",
+ "libc",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -2069,6 +3713,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "winreg"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "wonnx"
 version = "0.2.4"
@@ -2078,7 +3731,7 @@ dependencies = [
  "console_error_panic_hook",
  "console_log",
  "env_logger",
- "getrandom",
+ "getrandom 0.2.4",
  "image",
  "js-sys",
  "lazy_static",
@@ -2092,10 +3745,86 @@ dependencies = [
  "serde_derive",
  "tera",
  "thiserror",
- "ureq",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "wasm-bindgen-test",
  "web-sys",
  "wgpu",
 ]
+
+[[package]]
+name = "wonnx-cli"
+version = "0.1.0"
+dependencies = [
+ "assert_cmd",
+ "async-trait",
+ "env_logger",
+ "log",
+ "ndarray",
+ "pollster",
+ "prettytable-rs",
+ "protobuf",
+ "structopt",
+ "thiserror",
+ "tract-onnx",
+ "wgpu",
+ "wonnx",
+ "wonnx-preprocessing",
+]
+
+[[package]]
+name = "wonnx-preprocessing"
+version = "0.1.0"
+dependencies = [
+ "image",
+ "log",
+ "ndarray",
+ "protobuf",
+ "thiserror",
+ "tokenizers",
+ "tract-onnx",
+ "wgpu",
+ "wonnx",
+]
+
+[[package]]
+name = "wonnx-py"
+version = "0.1.1"
+dependencies = [
+ "pollster",
+ "protobuf",
+ "pyo3",
+ "wonnx",
+]
+
+[[package]]
+name = "xattr"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "zip"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
+dependencies = [
+ "byteorder",
+ "bzip2",
+ "crc32fast",
+ "flate2",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "zip-extensions"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a64c3c977bc3434ce2d4bcea8ad3c644672de0f2c402b72b9171ca80a8885d14"
+dependencies = [
+ "zip",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 2cae8ba7..5f6f4aad 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,10 @@
 [workspace]
 members = [
 	"wonnx",
-	"py-wonnx"
+	"wonnx-py",
+	"wonnx-cli",
+	"wonnx-preprocessing"
 ]
+
+default-members = ["wonnx", "wonnx-cli"]
 resolver = "2"
diff --git a/README.md b/README.md
index 44c06057..9336fc6d 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 ![Crates.io](https://img.shields.io/crates/l/wonnx)
 
 
-Wonnx aims for running blazing Fast AI on any device.
+Wonnx is a GPU-accelerated ONNX inference run-time written 100% in Rust, ready for the web.
 
 ## Supported Platforms (enabled by `wgpu`)
 
@@ -19,25 +19,35 @@ Wonnx aims for running blazing Fast AI on any device.
 
 :white_check_mark: = First Class Support — :ok: = Best Effort Support — :construction: = Unsupported, but support in progress
 
-## Getting Started
+## Getting started
 
 - Install Rust
 - Install Vulkan, Metal, or DX12 for the GPU API.
+- Ensure Git LFS is installed
 - git clone this repo.
 
 ```bash
 git clone https://github.com/webonnx/wonnx.git
+git lfs install
 ```
 
-### From Rust
+### From the command line
 
-Ensure Git LFS is initialized and has downloaded the model files (in `wonnx/examples/data/models`). Then to run an example:
+Ensure Git LFS is initialized and has downloaded the model files (in `wonnx/examples/data/models`). Then, you're all set!
+You can run an example:
 
 ```bash
 cargo run --example squeeze --release
 ```
 
-### From Python 
+Or you can try the CLI (see the [README](./wonnx-cli/README.md) for more information):
+
+````bash
+cargo run --release -- info ./data/models/opt-squeeze.mnist
+cargo run --release -- infer ./data/models/opt-squeeze.onnx -i data=./data/images/pelican.jpeg --labels ./data/models/squeeze-labels.txt --top 3
+````
+
+### From Python
 
 ```bash
 pip install wonnx
@@ -46,8 +56,9 @@ pip install wonnx
 And then:
 
 ```python
-session = wonnx.PySession.from_path(
-    "../examples/data/models/single_relu.onnx"
+from wonnx import PySession
+session = PySession.from_path(
+    "../data/models/single_relu.onnx"
 )
 inputs = {"x": [-1.0, 2.0]}
 assert session.run(inputs) == {"y": [0.0, 2.0]}
@@ -56,12 +67,15 @@ assert session.run(inputs) == {"y": [0.0, 2.0]}
 To build the Python module for development:
 
 ````sh
+cd wonnx-py
 python3 -m venv .env
 source .env/bin/activate
 pip install maturin
 maturin develop
 ````
 
+Then run `python3` with the above Python code!
+
 ## Running a model from scratch
 
 - To run an onnx model, first simplify it with [onnx-simplifier](https://github.com/daquexian/onnx-simplifier), with the command:
@@ -77,8 +91,6 @@ python -m onnxsim mnist-8.onnx opt-mnist.onnx
 cargo run --example mnist --release
 ```
 
-## Usage in Rust
-
 ```rust
 fn main() -> HashMap<String, Vec<f32>> {
     let mut input_data = HashMap::new();
@@ -102,7 +114,7 @@ fn main() -> HashMap<String, Vec<f32>> {
 > Examples are available in the `examples` folder
 
 
-## Tested Models
+## Tested models
 
 - Squeezenet
 - MNIST
diff --git a/wonnx/examples/data/images/0.jpg b/data/images/0.jpg
similarity index 100%
rename from wonnx/examples/data/images/0.jpg
rename to data/images/0.jpg
diff --git a/wonnx/examples/data/images/3.jpg b/data/images/3.jpg
similarity index 100%
rename from wonnx/examples/data/images/3.jpg
rename to data/images/3.jpg
diff --git a/wonnx/examples/data/images/5.jpg b/data/images/5.jpg
similarity index 100%
rename from wonnx/examples/data/images/5.jpg
rename to data/images/5.jpg
diff --git a/wonnx/examples/data/images/7.jpg b/data/images/7.jpg
similarity index 100%
rename from wonnx/examples/data/images/7.jpg
rename to data/images/7.jpg
diff --git a/wonnx/examples/data/images/bald_eagle.jpeg b/data/images/bald_eagle.jpeg
similarity index 100%
rename from wonnx/examples/data/images/bald_eagle.jpeg
rename to data/images/bald_eagle.jpeg
diff --git a/wonnx/examples/data/images/italian_greyhound.jpeg b/data/images/italian_greyhound.jpeg
similarity index 100%
rename from wonnx/examples/data/images/italian_greyhound.jpeg
rename to data/images/italian_greyhound.jpeg
diff --git a/wonnx/examples/data/images/pelican.jpeg b/data/images/pelican.jpeg
similarity index 100%
rename from wonnx/examples/data/images/pelican.jpeg
rename to data/images/pelican.jpeg
diff --git a/data/models/mnist-labels.txt b/data/models/mnist-labels.txt
new file mode 100644
index 00000000..c6dfb408
--- /dev/null
+++ b/data/models/mnist-labels.txt
@@ -0,0 +1,10 @@
+Zero
+One
+Two
+Three
+Four
+Five
+Six
+Seven
+Eight
+Nine
\ No newline at end of file
diff --git a/data/models/opt-mnist.onnx b/data/models/opt-mnist.onnx
new file mode 100644
index 00000000..89f04749
--- /dev/null
+++ b/data/models/opt-mnist.onnx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:486cf6b7d7cdb5fdce37e65537326fac02f9c9866efe1f09e3f9779e495e8624
+size 25905
diff --git a/data/models/opt-squeeze.onnx b/data/models/opt-squeeze.onnx
new file mode 100644
index 00000000..fe92128b
--- /dev/null
+++ b/data/models/opt-squeeze.onnx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:235b3dc20192f1bb64785679a45d044f9e214a03f2b925eb497ac2be4a79d476
+size 4959452
diff --git a/data/models/single_relu.onnx b/data/models/single_relu.onnx
new file mode 100644
index 00000000..b991da56
--- /dev/null
+++ b/data/models/single_relu.onnx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c2efaa294b40f4e0f0b70e4811dc12dedd5bb35216f17639b50a79dac97c985a
+size 98
diff --git a/wonnx/examples/data/models/synset.txt b/data/models/squeeze-labels.txt
similarity index 100%
rename from wonnx/examples/data/models/synset.txt
rename to data/models/squeeze-labels.txt
diff --git a/wonnx-cli/Cargo.toml b/wonnx-cli/Cargo.toml
new file mode 100644
index 00000000..a526d4a4
--- /dev/null
+++ b/wonnx-cli/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "wonnx-cli"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+cpu = ["tract-onnx"]
+default = []
+
+[[bin]]
+name = "nnx"
+path = "src/main.rs"
+
+[dependencies]
+async-trait = "0.1.52"
+env_logger = "0.9.0"
+
+log = "0.4.14"
+ndarray = "0.15.4"
+pollster = "0.2.4"
+prettytable-rs = "0.8.0"
+protobuf = { version = "2.25.2", features = ["with-bytes"] }
+structopt = { version = "0.3.25", features = [ "paw" ] }
+thiserror = "1.0.30"
+tract-onnx = { version = "0.15.8", optional = true }
+wgpu = "*"
+wonnx = { path = "../wonnx" }
+wonnx-preprocessing = { path = "../wonnx-preprocessing" }
+
+[dev-dependencies]
+assert_cmd = "2.0"
\ No newline at end of file
diff --git a/wonnx-cli/README.md b/wonnx-cli/README.md
new file mode 100644
index 00000000..df0e8d9a
--- /dev/null
+++ b/wonnx-cli/README.md
@@ -0,0 +1,173 @@
+# Wonnx command-line interface (`nnx`)
+
+Command-line interface for inference using wonnx
+
+ONNX defines a standardized format to exchange machine learning models. However, up to this point there is no easy way to
+perform one-off inference using such a model without resorting to Python. Installation of Python and the required libraries
+(e.g. TensorFlow and underlying GPU setup) can be cumbersome. Additionally specific code is always needed to transfer
+inputs (images, text, etc.) in and out of the formats required by the model (i.e. image classification models want their
+images as fixed-size tensors with the pixel values normalized to specific values, et cetera).
+
+This project provides a very simple all-in-one binary command line tool that can be used to perform inference using ONNX
+models on the GPU. Thanks to wonnx,  inference is performed on the GPU. 
+
+NNX tries to make educated guesses about how to transform input and output for a model. These guesses are a default - i.e.
+it should always be possible to override them. The goal is to reduce the amount of configuration required to be able to
+run a model. Currently the following heuristics are applied:
+
+- The first input and first output specified in the ONNX file are used by default.
+- Models taking inputs of shape (1,3,w,h) and (3,w,h) will be fed images resized to w\*h with pixel values normalized to
+  0...1 (currently we also apply the SqueezeNet normalization)
+- Similarly, models taking inputs of shape (1,1,w,h) and (1,w,h) will be fed black-and-white images with pixel values
+  normalized to 0...1.
+- When a label file is supplied, an output vector of shape (n,) will be interpreted as providing the probabilities for each
+  class. The label for each class is taken from the n'th line in the label file.
+
+## Usage
+
+```sh
+$ nnx infer ./data/models/opt-squeeze.onnx -i data=./data/images/pelican.jpeg --labels ./data/models/squeeze-labels.txt --probabilities
+n01608432 kite: 21.820244
+n02051845 pelican: 21.112095
+n02018795 bustard: 20.359694
+n01622779 great grey owl, great gray owl, Strix nebulosa: 20.176003
+n04417672 thatch, thatched roof: 19.638676
+n02028035 redshank, Tringa totanus: 19.606218
+n02011460 bittern: 18.90648
+n02033041 dowitcher: 18.708323
+n01829413 hornbill: 18.595457
+n01616318 vulture: 17.508785
+
+$ nnx infer ./data/models/opt-mnist.onnx -i Input3=./data/images/7.jpg
+[-1.2942507, 0.5192305, 8.655695, 9.474595, -13.768464, -5.8907413, -23.467274, 28.252314, -6.7598896, 3.9513395]
+
+$ nnx infer ./data/models/opt-mnist.onnx -i Input3=./data/images/7.jpg --labels ./data/models/mnist-labels.txt --top=1
+Seven
+
+$ nnx info ./data/models/opt-mnist.onnx
++------------------+-----------------------------------------------------------------+
+| Model version    | 1                                                               |
++------------------+-----------------------------------------------------------------+
+| IR version       | 3                                                               |
++------------------+-----------------------------------------------------------------+
+| Producer name    | CNTK                                                            |
++------------------+-----------------------------------------------------------------+
+| Producer version | 2.5.1                                                           |
++------------------+-----------------------------------------------------------------+
+| Opsets           | 8                                                               |
++------------------+-----------------------------------------------------------------+
+| Inputs           | +--------+-------------+-----------+                            |
+|                  | | Name   | Description | Shape     |                            |
+|                  | +--------+-------------+-----------+                            |
+|                  | | Input3 |             | 1x1x28x28 |                            |
+|                  | +--------+-------------+-----------+                            |
++------------------+-----------------------------------------------------------------+
+| Outputs          | +------------------+-------------+-------+                      |
+|                  | | Name             | Description | Shape |                      |
+|                  | +------------------+-------------+-------+                      |
+|                  | | Plus214_Output_0 |             | 1x10  |                      |
+|                  | +------------------+-------------+-------+                      |
++------------------+-----------------------------------------------------------------+
+| Ops used         | +---------+---------------------------------------------------+ |
+|                  | | Op      | Attributes                                        | |
+|                  | +---------+---------------------------------------------------+ |
+|                  | | Relu    |                                                   | |
+|                  | +---------+---------------------------------------------------+ |
+|                  | | Conv    | dilations, kernel_shape, group, strides, auto_pad | |
+|                  | +---------+---------------------------------------------------+ |
+|                  | | MaxPool | pads, kernel_shape, auto_pad, strides             | |
+|                  | +---------+---------------------------------------------------+ |
+|                  | | Gemm    | transB, alpha, transA, beta                       | |
+|                  | +---------+---------------------------------------------------+ |
+|                  | | Reshape |                                                   | |
+|                  | +---------+---------------------------------------------------+ |
++------------------+-----------------------------------------------------------------+
+```
+
+- Replace `nnx` with `cargo run --release --` to run development version
+- Prepend `RUST_LOG=wonnx-cli=info` to see useful logging from the CLI tool, `RUST_LOG=wonnx=info` to see logging from WONNX.
+
+## CPU inference using `tract`
+
+The nnx utility can use [tract](https://github.com/sonos/tract) as CPU-based backend for ONNX inference. In order to use
+this, nnx needs to be compiled with the `cpu` feature enabled. You can then specify one of the following arguments:
+*  `--backend cpu` to select the cpu backend
+* `--fallback` to select the cpu backend when the gpu backend cannot be used (e.g. because of an unsupported operation type)
+* `--compare` to run inference on both CPU and GPU backends and compare the output
+* `--benchmark` to run the specified inference a hundred times, then report the performance
+* `--compare --benchmark` to run inference on both CPU and GPU a hundred times each, and compare the performance
+
+A benchmarking example (the below result was obtained on an Apple M1 Max system):
+
+```sh
+# Run from workspace root
+$ cargo run --release --features=cpu -- infer ./data/models/opt-squeeze.onnx -i data=./data/images/pelican.jpeg --compare --benchmark
+OK (gpu=572ms, cpu=1384ms, 2.42x)
+```
+
+## End-to-end example with Keras
+
+1. `pip install tensorflow onnx tf2onnx`
+
+2. Create a very simple model for the MNIST digits:
+
+```python
+from tensorflow.keras.datasets import mnist
+(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
+
+# train_images will be (60000,28,28) i.e. 60k black-and-white images of 28x28 pixels (which are ints between 0..255)
+# train_labels will be (60000,) i.e. 60k integers ranging 0...9
+# test_images/test_labels are similar but only have 10k items
+
+# Build model
+from tensorflow import keras
+from tensorflow.keras import layers
+
+# Convert images to have pixel values as floats between 0...1
+train_images_input = train_images.astype("float32") / 255
+
+model = keras.Sequential([
+    layers.Reshape((28*28,), input_shape=(28,28)),
+    layers.Dense(512, activation = 'relu'),
+    layers.Dropout(rate=0.01),
+    layers.Dense(10,  activation = 'softmax')
+])
+
+model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy", metrics=["accuracy"])
+
+# Train the model
+model.fit(train_images_input, train_labels, epochs=20, batch_size=1024)
+```
+
+3. Save Keras model to ONNX with inferred dimensions:
+
+```python
+import tf2onnx
+import tensorflow as tf
+import onnx
+input_signature = [tf.TensorSpec([1,28,28], tf.float32, name='input')]
+onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature, opset=13)
+
+from onnx import helper, shape_inference
+inferred_model = shape_inference.infer_shapes(onnx_model)
+
+onnx.save(onnx_model, "tymnist.onnx")
+onnx.save(inferred_model, "tymnist-inferred.onnx")
+```
+
+4. Infer with NNX:
+
+```sh
+nnx  ./tymnist-inferred.onnx infer -i input=./data/mnist-7.png --labels ./data/models/mnist-labels.txt
+```
+
+5. compare inference result with what Keras would generate (`pip install numpy pillow matplotlib`):
+
+```python
+import PIL
+import numpy
+import matplotlib.pyplot as plt
+m5 = PIL.Image.open("data/mnist-7.png").resize((28,28), PIL.Image.ANTIALIAS)
+nm5 = numpy.array(m5).reshape((1,28,28))
+model.predict(nm5)
+```
\ No newline at end of file
diff --git a/wonnx-cli/src/cpu.rs b/wonnx-cli/src/cpu.rs
new file mode 100644
index 00000000..6818aefa
--- /dev/null
+++ b/wonnx-cli/src/cpu.rs
@@ -0,0 +1,124 @@
+use std::collections::HashMap;
+
+use crate::{InferOptions, Inferer, NNXError};
+use async_trait::async_trait;
+use tract_onnx::prelude::*;
+use wonnx::{onnx::ModelProto, utils::Shape};
+
+type RunnableOnnxModel =
+    SimplePlan<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>;
+
+pub struct CPUInferer {
+    model: RunnableOnnxModel,
+    input_shapes: HashMap<String, Shape>,
+}
+
+impl CPUInferer {
+    pub async fn new(
+        model_path: &str,
+        input_shapes: &HashMap<String, Shape>,
+    ) -> Result<CPUInferer, NNXError> {
+        let mut cpu_model = tract_onnx::onnx().model_for_path(&model_path)?;
+
+        for (input_name, input_shape) in input_shapes {
+            let input_node = cpu_model.node_by_name(&input_name)?.id;
+            let fact = InferenceFact::dt_shape(f32::datum_type(), &input_shape.dims);
+            log::info!(
+                "set input '{}' (id {}) to shape {:?}",
+                input_name,
+                input_node,
+                input_shape
+            );
+            cpu_model.set_input_fact(input_node, fact)?;
+        }
+
+        let cpu_model = cpu_model.into_optimized()?.into_runnable()?;
+        Ok(CPUInferer {
+            model: cpu_model,
+            input_shapes: input_shapes.clone(),
+        })
+    }
+}
+
+trait ToTract {
+    fn to_tract_tensor(&self, dims: &[usize]) -> Result<Tensor, NNXError>;
+}
+
+impl ToTract for wonnx_preprocessing::Tensor {
+    fn to_tract_tensor(&self, dims: &[usize]) -> Result<Tensor, NNXError> {
+        match self {
+            wonnx_preprocessing::Tensor::F32(d) => Ok(tract_onnx::prelude::Tensor::from_shape(
+                dims,
+                d.as_slice().unwrap(),
+            )?),
+        }
+    }
+}
+
+#[async_trait]
+impl Inferer for CPUInferer {
+    async fn infer(
+        &self,
+        infer_opt: &InferOptions,
+        inputs: &HashMap<String, crate::Tensor>,
+        model: &ModelProto,
+    ) -> Result<Vec<f32>, NNXError> {
+        let mut cpu_inputs: HashMap<usize, tract_onnx::prelude::Tensor> = HashMap::new();
+
+        for (input_name, input_tensor) in inputs {
+            let input_index = model
+                .get_graph()
+                .get_input()
+                .iter()
+                .enumerate()
+                .find(|x| x.1.get_name() == input_name)
+                .unwrap_or_else(|| panic!("input not found with name {}", input_name));
+            log::info!("set input fact {} for cpu model", input_index.0,);
+
+            let dims: Vec<usize> = self.input_shapes[input_name]
+                .dims
+                .iter()
+                .map(|x| (*x) as usize)
+                .collect();
+
+            cpu_inputs.insert(input_index.0, input_tensor.to_tract_tensor(&dims)?);
+        }
+
+        let mut cpu_inputs_ordered = TVec::new();
+        for i in 0..inputs.len() {
+            cpu_inputs_ordered.push(cpu_inputs.get(&i).unwrap().clone());
+        }
+
+        let result = self.model.run(cpu_inputs_ordered)?;
+        log::info!("cpu result: {:?}", result);
+
+        let result_vector = match &infer_opt.output_name {
+            Some(output_name) => {
+                /* Find position of the node with the specified name in the output set. Note that Tract will suffix the
+                names of the nodes in the ONNX graph with the name of the output, i.e. "Plus214_Output_0.ab.matmatmul"
+                where the original name is called "Plus214_Output_0.ab.", hence the 'starts_with' hack below. */
+                if let Some(idx) = self.model.outputs.iter().enumerate().find(|x| {
+                    self.model.model.nodes[x.1.node]
+                        .name
+                        .starts_with(&format!("{}.", output_name))
+                }) {
+                    log::info!(
+						"output node with name '{}' has idx {:?} (and tract id {}, slot {}, name '{}')",
+						output_name,
+						idx.0,
+						idx.1.node,
+						idx.1.slot,
+						self.model.model.nodes[idx.1.node].name
+					);
+                    result[idx.0].clone()
+                } else {
+                    return Err(NNXError::OutputNotFound(output_name.to_string()));
+                }
+            }
+            None => result[0].clone(),
+        };
+
+        let av = result_vector.to_array_view()?;
+        Ok(av.as_slice().unwrap().to_vec())
+    }
+}
diff --git a/wonnx-cli/src/gpu.rs b/wonnx-cli/src/gpu.rs
new file mode 100644
index 00000000..5922f67d
--- /dev/null
+++ b/wonnx-cli/src/gpu.rs
@@ -0,0 +1,48 @@
+use std::collections::HashMap;
+
+use wonnx::onnx::ModelProto;
+
+use async_trait::async_trait;
+
+use crate::types::InferOptions;
+use crate::types::Inferer;
+use crate::types::NNXError;
+
+pub struct GPUInferer {
+    session: wonnx::Session,
+}
+
+impl GPUInferer {
+    pub async fn new(model_path: &str) -> Result<GPUInferer, NNXError> {
+        Ok(GPUInferer {
+            session: wonnx::Session::from_path(model_path).await?,
+        })
+    }
+}
+
+#[async_trait]
+impl Inferer for GPUInferer {
+    async fn infer(
+        &self,
+        infer_opt: &InferOptions,
+        inputs: &HashMap<String, crate::Tensor>,
+        _model: &ModelProto,
+    ) -> Result<Vec<f32>, NNXError> {
+        let input_refs = inputs
+            .iter()
+            .map(|(k, v)| (k.clone(), v.input_tensor()))
+            .collect();
+        let mut result = self.session.run(&input_refs).await.expect("run failed");
+
+        let result = match &infer_opt.output_name {
+            Some(output_name) => match result.remove(output_name) {
+                Some(out) => out,
+                None => return Err(NNXError::OutputNotFound(output_name.to_string())),
+            },
+            None => result.values().next().unwrap().clone(),
+        };
+
+        log::info!("gpu result: {:?}", result);
+        Ok(result)
+    }
+}
diff --git a/wonnx-cli/src/info.rs b/wonnx-cli/src/info.rs
new file mode 100644
index 00000000..0dd4e770
--- /dev/null
+++ b/wonnx-cli/src/info.rs
@@ -0,0 +1,199 @@
+use std::collections::{HashMap, HashSet};
+
+use prettytable::{cell, row, table, Table};
+use wonnx::{
+    onnx::{GraphProto, ModelProto, NodeProto},
+    utils::{ScalarType, Shape},
+    WonnxError,
+};
+
+use crate::utils::ValueInfoProtoUtil;
+
+fn dimensions_infos(graph_proto: &GraphProto) -> Result<HashMap<String, Shape>, wonnx::WonnxError> {
+    let mut shapes_info = HashMap::new();
+
+    for info in graph_proto.get_input() {
+        let shape = info.get_shape()?;
+        shapes_info.insert(info.get_name().to_string(), shape);
+    }
+
+    for info in graph_proto.get_output() {
+        let shape = info.get_shape()?;
+        shapes_info.insert(info.get_name().to_string(), shape);
+    }
+
+    for info in graph_proto.get_value_info() {
+        let shape = info.get_shape()?;
+        shapes_info.insert(info.get_name().to_string(), shape);
+    }
+
+    for info in graph_proto.get_initializer() {
+        let shape = Shape::from(ScalarType::from_i32(info.get_data_type())?, info.get_dims());
+        shapes_info.insert(info.get_name().to_string(), shape);
+    }
+
+    Ok(shapes_info)
+}
+
+fn node_identifier(index: usize, node: &NodeProto) -> String {
+    format!("\"{} {}\"", index, node.get_op_type())
+}
+
+pub fn print_graph(model: &ModelProto) {
+    println!("strict digraph {{");
+    let graph = model.get_graph();
+
+    let mut outputs: HashMap<String, String> = HashMap::new();
+    for (index, node) in graph.get_node().iter().enumerate() {
+        for output in node.get_output() {
+            outputs.insert(output.clone(), node_identifier(index, node));
+        }
+    }
+
+    // Special values
+    let model_inputs: HashSet<&str> = graph.get_input().iter().map(|v| v.get_name()).collect();
+    let initializers: HashSet<&str> = graph
+        .get_initializer()
+        .iter()
+        .map(|x| x.get_name())
+        .collect();
+
+    for (index, node) in graph.get_node().iter().enumerate() {
+        for input in node.get_input() {
+            if initializers.contains(input.as_str()) {
+                continue;
+            }
+
+            if model_inputs.contains(input.as_str()) {
+                println!("\t\"Input {}\" -> {}", input, node_identifier(index, node));
+            }
+            // Find input node
+            else if let Some(out_from_node) = outputs.get(input) {
+                println!("\t{} -> {}", out_from_node, node_identifier(index, node));
+            } else {
+                println!(
+                    "\t\"Unknown: {}\" -> {}",
+                    input,
+                    node_identifier(index, node)
+                );
+            }
+        }
+    }
+    println!("}}");
+}
+
+pub fn info_table(model: &ModelProto) -> Result<Table, WonnxError> {
+    // List initializers
+    let initializer_names: HashSet<String> = model
+        .get_graph()
+        .get_initializer()
+        .iter()
+        .map(|it| it.get_name().to_string())
+        .collect();
+
+    let mut inputs_table = Table::new();
+    inputs_table.add_row(row![b->"Name", b->"Description", b->"Shape", b->"Type"]);
+    let inputs = model.get_graph().get_input();
+    for i in inputs {
+        if !initializer_names.contains(i.get_name()) {
+            inputs_table.add_row(row![
+                i.get_name(),
+                i.get_doc_string(),
+                i.dimensions()
+                    .iter()
+                    .map(|x| x.to_string())
+                    .collect::<Vec<String>>()
+                    .join("x"),
+                i.data_type()?
+            ]);
+        }
+    }
+
+    let mut outputs_table = Table::new();
+    outputs_table.add_row(row![b->"Name", b->"Description", b->"Shape", b->"Type"]);
+
+    let outputs = model.get_graph().get_output();
+    for i in outputs {
+        outputs_table.add_row(row![
+            i.get_name(),
+            i.get_doc_string(),
+            i.dimensions()
+                .iter()
+                .map(|x| x.to_string())
+                .collect::<Vec<String>>()
+                .join("x"),
+            i.data_type()?
+        ]);
+    }
+
+    let opset_string = model
+        .get_opset_import()
+        .iter()
+        .map(|os| format!("{} {}", os.get_version(), os.get_domain()))
+        .collect::<Vec<String>>()
+        .join(";");
+
+    // List ops used
+    struct Usage {
+        attributes: HashSet<String>,
+    }
+
+    let mut usage: HashMap<&str, Usage> = HashMap::new();
+
+    let graph = model.get_graph();
+    for node in graph.get_node() {
+        let usage = if let Some(usage) = usage.get_mut(node.get_op_type()) {
+            usage
+        } else {
+            usage.insert(
+                node.get_op_type(),
+                Usage {
+                    attributes: HashSet::new(),
+                },
+            );
+            usage.get_mut(node.get_op_type()).unwrap()
+        };
+
+        node.get_attribute().iter().for_each(|a| {
+            usage.attributes.insert(a.get_name().to_string());
+        });
+    }
+
+    let mut usage_table = Table::new();
+    usage_table.add_row(row![b->"Op", b->"Attributes"]);
+    for (op, usage) in usage.iter() {
+        let attrs = usage
+            .attributes
+            .iter()
+            .cloned()
+            .collect::<Vec<String>>()
+            .join(", ");
+        usage_table.add_row(row![op, attrs]);
+    }
+
+    // List node inputs/outputs that don't have dimensions
+    let shapes = dimensions_infos(graph)?;
+    for node in graph.get_node() {
+        for input in node.get_input() {
+            match shapes.get(input) {
+                None => println!(
+                    "Node '{}' input '{}' has unknown shape",
+                    node.get_name(),
+                    input
+                ),
+                Some(_) => {}
+            }
+        }
+    }
+
+    Ok(table![
+        [b->"Model version", model.get_model_version()],
+        [b->"IR version", model.get_ir_version()],
+        [b->"Producer name", model.get_producer_name()],
+        [b->"Producer version", model.get_producer_version()],
+        [b->"Opsets", opset_string],
+        [b->"Inputs", inputs_table],
+        [b->"Outputs", outputs_table],
+        [b->"Ops used", usage_table]
+    ])
+}
diff --git a/wonnx-cli/src/main.rs b/wonnx-cli/src/main.rs
new file mode 100644
index 00000000..496da083
--- /dev/null
+++ b/wonnx-cli/src/main.rs
@@ -0,0 +1,325 @@
+use crate::info::info_table;
+use info::print_graph;
+use ndarray::Array;
+use prettytable::{cell, row, Table};
+use protobuf::{self, Message};
+use std::{collections::HashMap, path::Path};
+use structopt::StructOpt;
+use wonnx::onnx::ModelProto;
+use wonnx::utils::Shape;
+use wonnx_preprocessing::text;
+use wonnx_preprocessing::text::get_lines;
+use wonnx_preprocessing::Tensor;
+
+mod gpu;
+mod info;
+mod types;
+mod utils;
+
+use crate::types::*;
+use crate::utils::*;
+
+async fn run() -> Result<(), NNXError> {
+    env_logger::init();
+    let opt = Opt::from_args();
+
+    match opt.cmd {
+        Command::Devices => {
+            let instance = wgpu::Instance::new(wgpu::Backends::all());
+            let adapters = instance.enumerate_adapters(wgpu::Backends::all());
+            let mut adapters_table = Table::new();
+            adapters_table.add_row(row![b->"Adapter", b->"Vendor", b->"Backend"]);
+            for adapter in adapters {
+                let info = adapter.get_info();
+                adapters_table.add_row(row![info.name, info.vendor, format!("{:?}", info.backend)]);
+            }
+            adapters_table.printstd();
+            Ok(())
+        }
+
+        Command::Info(info_opt) => {
+            // Load the model
+            let model_path = info_opt
+                .model
+                .into_os_string()
+                .into_string()
+                .expect("invalid path");
+            let model = ModelProto::parse_from_bytes(
+                &std::fs::read(&model_path).expect("ONNX Model path not found."),
+            )
+            .expect("Could not deserialize the model");
+            let table = info_table(&model)?;
+            table.printstd();
+            Ok(())
+        }
+
+        Command::Graph(info_opt) => {
+            // Load the model
+            let model_path = info_opt
+                .model
+                .into_os_string()
+                .into_string()
+                .expect("invalid path");
+            let model = ModelProto::parse_from_bytes(
+                &std::fs::read(&model_path).expect("ONNX Model path not found."),
+            )
+            .expect("Could not deserialize the model");
+            print_graph(&model);
+            Ok(())
+        }
+
+        Command::Infer(infer_opt) => {
+            // Load the model
+            let model_path = infer_opt
+                .model
+                .clone()
+                .into_os_string()
+                .into_string()
+                .expect("invalid path");
+            let model = ModelProto::parse_from_bytes(
+                &std::fs::read(&model_path).expect("ONNX Model path not found."),
+            )
+            .expect("Could not deserialize the model");
+
+            let mut inputs: HashMap<String, Tensor> = HashMap::new();
+            let mut input_shapes = HashMap::with_capacity(inputs.len());
+
+            // Process text inputs
+            if !infer_opt.text.is_empty() || !infer_opt.text_mask.is_empty() {
+                let tok = text::BertTokenizer::new(Path::new(&infer_opt.vocab));
+
+                // Tokenized text input
+                for (text_input_name, text) in &infer_opt.text {
+                    let text_input_shape = model
+                        .get_input_shape(text_input_name)?
+                        .ok_or_else(|| NNXError::InputNotFound(text_input_name.clone()))?;
+                    let input = tok.get_input_for(text, &text_input_shape)?;
+                    inputs.insert(text_input_name.clone(), input);
+                    input_shapes.insert(text_input_name.clone(), text_input_shape);
+                }
+
+                // Tokenized text input
+                for (text_input_name, text) in &infer_opt.text_mask {
+                    let text_input_shape = model
+                        .get_input_shape(text_input_name)?
+                        .ok_or_else(|| NNXError::InputNotFound(text_input_name.clone()))?;
+                    let input = tok.get_mask_input_for(text, &text_input_shape)?;
+                    inputs.insert(text_input_name.clone(), input);
+                    input_shapes.insert(text_input_name.clone(), text_input_shape);
+                }
+            }
+
+            // Process raw inputs
+            for (raw_input_name, text) in &infer_opt.raw {
+                let raw_input_shape = model
+                    .get_input_shape(raw_input_name)?
+                    .ok_or_else(|| NNXError::InputNotFound(raw_input_name.clone()))?;
+
+                let values: Result<Vec<f32>, _> =
+                    text.split(',').map(|v| v.parse::<f32>()).collect();
+                let mut values = values.map_err(NNXError::InvalidNumber)?;
+                values.resize(raw_input_shape.element_count() as usize, 0.0);
+                inputs.insert(
+                    raw_input_name.clone(),
+                    Tensor::F32(Array::from_vec(values).into_dyn()),
+                );
+                input_shapes.insert(raw_input_name.clone(), raw_input_shape);
+            }
+
+            // Load input image if it was supplied
+            for (input_name, image_path) in &infer_opt.input_images {
+                let mut input_shape = model
+                    .get_input_shape(input_name)?
+                    .ok_or_else(|| NNXError::InputNotFound(input_name.clone()))?;
+
+                let data = load_image_input(image_path, &input_shape)?;
+
+                // Some models allow us to set the number of items we are throwing at them.
+                if input_shape.dim(0) == 0 {
+                    input_shape.dims[0] = 1;
+                    log::info!(
+                        "changing first dimension for input {} to {:?}",
+                        input_name,
+                        input_shape
+                    );
+                }
+
+                inputs.insert(input_name.clone(), Tensor::F32(data));
+                input_shapes.insert(input_name.clone(), input_shape.clone());
+            }
+
+            #[cfg(feature = "cpu")]
+            if infer_opt.compare {
+                let gpu_backend = Backend::Gpu
+                    .inferer_for_model(&model_path, &input_shapes)
+                    .await?;
+                let gpu_start = std::time::Instant::now();
+                if infer_opt.benchmark {
+                    for _ in 0..100 {
+                        let _ = gpu_backend.infer(&infer_opt, &inputs, &model).await?;
+                    }
+                }
+                let gpu_output = gpu_backend.infer(&infer_opt, &inputs, &model).await?;
+                let gpu_time = gpu_start.elapsed();
+                log::info!("gpu time: {}ms", gpu_time.as_millis());
+                drop(gpu_backend);
+
+                let cpu_backend = Backend::Cpu
+                    .inferer_for_model(&model_path, &input_shapes)
+                    .await?;
+                let cpu_start = std::time::Instant::now();
+                if infer_opt.benchmark {
+                    for _ in 0..100 {
+                        let _ = cpu_backend.infer(&infer_opt, &inputs, &model).await?;
+                    }
+                }
+                let cpu_output = cpu_backend.infer(&infer_opt, &inputs, &model).await?;
+                let cpu_time = cpu_start.elapsed();
+                log::info!(
+                    "cpu time: {}ms ({:.2}x gpu time)",
+                    cpu_time.as_millis(),
+                    cpu_time.as_secs_f64() / gpu_time.as_secs_f64()
+                );
+                if gpu_output.len() != cpu_output.len() {
+                    return Err(NNXError::Comparison(format!(
+                        "length of GPU result ({}) mismatches CPU result ({})",
+                        gpu_output.len(),
+                        cpu_output.len()
+                    )));
+                }
+
+                for i in 0..gpu_output.len() {
+                    let diff = (gpu_output[i] - cpu_output[i]).abs();
+                    if diff > 0.001 {
+                        return Err(NNXError::Comparison(format!(
+							"output element {} differs too much: GPU says {} vs CPU says {} (difference is {})",
+							i, gpu_output[i], cpu_output[i], diff
+						)));
+                    }
+                }
+                if infer_opt.benchmark {
+                    println!(
+                        "OK (gpu={}ms, cpu={}ms, {:.2}x)",
+                        gpu_time.as_millis(),
+                        cpu_time.as_millis(),
+                        cpu_time.as_secs_f64() / gpu_time.as_secs_f64()
+                    );
+                } else {
+                    println!("OK")
+                }
+                return Ok(());
+            }
+
+            let first_result = async {
+                let backend = infer_opt
+                    .backend
+                    .inferer_for_model(&model_path, &input_shapes)
+                    .await?;
+
+                if infer_opt.benchmark {
+                    let benchmark_start = std::time::Instant::now();
+                    for _ in 0..100 {
+                        let _ = backend.infer(&infer_opt, &inputs, &model).await?;
+                    }
+                    let benchmark_time = benchmark_start.elapsed();
+                    println!(
+                        "time for 100 inferences: {}ms ({}/s)",
+                        benchmark_time.as_millis(),
+                        1000 / benchmark_time.as_millis()
+                    );
+                }
+                backend.infer(&infer_opt, &inputs, &model).await
+            };
+
+            let output = match first_result.await {
+                Ok(x) => x,
+                Err(e) => {
+                    #[cfg(feature = "cpu")]
+                    if infer_opt.fallback {
+                        match infer_opt.backend.fallback() {
+                            Some(fallback_backend) => {
+                                log::warn!(
+                                    "inference with {:?} backend failed: {}",
+                                    infer_opt.backend,
+                                    e,
+                                );
+                                log::warn!("trying {:?} backend instead", fallback_backend);
+                                let fallback_inferer = fallback_backend
+                                    .inferer_for_model(&model_path, &input_shapes)
+                                    .await?;
+                                fallback_inferer.infer(&infer_opt, &inputs, &model).await?
+                            }
+                            None => return Err(e),
+                        }
+                    } else {
+                        return Err(e);
+                    }
+
+                    #[cfg(not(feature = "cpu"))]
+                    return Err(e);
+                }
+            };
+
+            // Look up label
+            match infer_opt.labels {
+                Some(labels_path) => {
+                    let labels = get_lines(&labels_path);
+
+                    let mut probabilities = output.iter().enumerate().collect::<Vec<_>>();
+                    probabilities.sort_unstable_by(|a, b| b.1.partial_cmp(a.1).unwrap());
+
+                    let top = infer_opt.top.unwrap_or(10);
+                    for i in 0..top.min(labels.len()) {
+                        if infer_opt.probabilities {
+                            println!("{}: {}", labels[probabilities[i].0], probabilities[i].1);
+                        } else {
+                            println!("{}", labels[probabilities[i].0]);
+                        }
+                    }
+                }
+                None => {
+                    println!("{:?}", output);
+                }
+            }
+
+            Ok(())
+        }
+    }
+}
+
+#[cfg(feature = "cpu")]
+mod cpu;
+
+impl Backend {
+    #[cfg(feature = "cpu")]
+    fn fallback(&self) -> Option<Backend> {
+        match self {
+            #[cfg(feature = "cpu")]
+            Backend::Cpu => None,
+
+            Backend::Gpu => {
+                #[cfg(feature = "cpu")]
+                return Some(Backend::Cpu);
+
+                #[cfg(not(feature = "cpu"))]
+                return None;
+            }
+        }
+    }
+
+    async fn inferer_for_model(
+        &self,
+        model_path: &str,
+        #[allow(unused_variables)] input_shapes: &HashMap<String, Shape>,
+    ) -> Result<Box<dyn Inferer>, NNXError> {
+        Ok(match self {
+            Backend::Gpu => Box::new(gpu::GPUInferer::new(model_path).await?),
+            #[cfg(feature = "cpu")]
+            Backend::Cpu => Box::new(cpu::CPUInferer::new(model_path, input_shapes).await?),
+        })
+    }
+}
+
+fn main() -> Result<(), NNXError> {
+    pollster::block_on(run())
+}
diff --git a/wonnx-cli/src/types.rs b/wonnx-cli/src/types.rs
new file mode 100644
index 00000000..a30c5bb5
--- /dev/null
+++ b/wonnx-cli/src/types.rs
@@ -0,0 +1,185 @@
+use std::{collections::HashMap, num::ParseFloatError, path::PathBuf, str::FromStr};
+use structopt::StructOpt;
+use thiserror::Error;
+use wonnx::{onnx::ModelProto, SessionError, WonnxError};
+use wonnx_preprocessing::{text::PreprocessingError, Tensor};
+
+#[cfg(feature = "cpu")]
+use tract_onnx::prelude::*;
+
+#[derive(Debug, StructOpt)]
+pub struct InfoOptions {
+    /// Model file (.onnx)
+    #[structopt(parse(from_os_str))]
+    pub model: PathBuf,
+}
+
+#[derive(Debug, StructOpt)]
+pub enum Backend {
+    Gpu,
+    #[cfg(feature = "cpu")]
+    Cpu,
+}
+
+#[derive(Error, Debug)]
+pub enum NNXError {
+    #[error("invalid backend selected")]
+    InvalidBackend(String),
+
+    #[error("input shape is invalid")]
+    InvalidInputShape,
+
+    #[error("output not found")]
+    OutputNotFound(String),
+
+    #[error("input not found")]
+    InputNotFound(String),
+
+    #[error("backend error: {0}")]
+    BackendFailed(#[from] WonnxError),
+
+    #[error("backend execution error: {0}")]
+    BackendExecutionFailed(#[from] SessionError),
+
+    #[cfg(feature = "cpu")]
+    #[error("cpu backend error: {0}")]
+    CPUBackendFailed(#[from] TractError),
+
+    #[cfg(feature = "cpu")]
+    #[error("comparison failed")]
+    Comparison(String),
+
+    #[error("preprocessing failed: {0}")]
+    PreprocessingFailed(#[from] PreprocessingError),
+
+    #[error("invalid number: {0}")]
+    InvalidNumber(ParseFloatError),
+}
+
+impl FromStr for Backend {
+    type Err = NNXError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "gpu" => Ok(Backend::Gpu),
+            #[cfg(feature = "cpu")]
+            "cpu" => Ok(Backend::Cpu),
+            _ => Err(NNXError::InvalidBackend(s.to_string())),
+        }
+    }
+}
+
+/// Parse a single key-value pair
+fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn std::error::Error>>
+where
+    T: std::str::FromStr,
+    T::Err: std::error::Error + 'static,
+    U: std::str::FromStr,
+    U::Err: std::error::Error + 'static,
+{
+    let pos = s
+        .find('=')
+        .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?;
+    Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
+}
+
+#[derive(Debug, StructOpt)]
+pub struct InferOptions {
+    /// Model file (.onnx)
+    #[structopt(parse(from_os_str))]
+    pub model: PathBuf,
+
+    #[structopt(long, default_value = "gpu")]
+    pub backend: Backend,
+
+    /// Input image
+    #[structopt(short = "i", parse(try_from_str = parse_key_val), number_of_values = 1)]
+    pub input_images: Vec<(String, PathBuf)>,
+
+    // Number of labels to print (default: 10)
+    #[structopt(long)]
+    pub top: Option<usize>,
+
+    /// Whether to print probabilities
+    #[structopt(long)]
+    pub probabilities: bool,
+
+    /// Node to take output from (defaults to the first output when not specified)
+    #[structopt(long)]
+    pub output_name: Option<String>,
+
+    /// Vocab file for text encoding
+    #[structopt(
+        long = "vocab",
+        parse(from_os_str),
+        default_value = "./data/models/bertsquad-vocab.txt"
+    )]
+    pub vocab: PathBuf,
+
+    /// Set an input to tokenized text (-t input_name="some text")
+    #[structopt(short = "t", parse(try_from_str = parse_key_val), number_of_values = 1)]
+    pub text: Vec<(String, String)>,
+
+    /// Set an input to the mask after tokenizing (-m input_name="some text")
+    #[structopt(short = "m", parse(try_from_str = parse_key_val), number_of_values = 1)]
+    pub text_mask: Vec<(String, String)>,
+
+    /// Provide raw input (-r input_name=1,2,3,4)
+    #[structopt(short = "r", parse(try_from_str = parse_key_val), number_of_values = 1)]
+    pub raw: Vec<(String, String)>,
+
+    /// Path to a labels file (each line containing a single label)
+    #[structopt(short, long, parse(from_os_str))]
+    pub labels: Option<PathBuf>,
+
+    #[cfg(feature = "cpu")]
+    #[structopt(long)]
+    /// Whether to fall back to the CPU backend type if GPU inference fails
+    pub fallback: bool,
+
+    #[cfg(feature = "cpu")]
+    #[structopt(long, conflicts_with = "backend")]
+    /// Compare results of CPU and GPU inference (100 iterations to measure time)
+    pub compare: bool,
+
+    #[structopt(long)]
+    /// Perform 100 inferences to measure time
+    pub benchmark: bool,
+}
+
+#[derive(Debug, StructOpt)]
+pub enum Command {
+    /// List available GPU devices
+    Devices,
+
+    /// Perform inference using a model and inputs
+    Infer(InferOptions),
+
+    /// Show information about a model, such as its inputs, outputs and the ops it uses
+    Info(InfoOptions),
+
+    /// Return a GraphViz direct graph of the nodes in the model
+    Graph(InfoOptions),
+}
+
+#[derive(Debug, StructOpt)]
+#[structopt(
+    name = "nnx: Neural Network Execute",
+    about = "GPU-accelerated ONNX inference through wonnx from the command line"
+)]
+pub struct Opt {
+    #[structopt(subcommand)]
+    pub cmd: Command,
+}
+
+use async_trait::async_trait;
+
+#[async_trait]
+pub trait Inferer {
+    async fn infer(
+        &self,
+        infer_opt: &InferOptions,
+        inputs: &HashMap<String, Tensor>,
+        model: &ModelProto,
+    ) -> Result<Vec<f32>, NNXError>;
+}
diff --git a/wonnx-cli/src/utils.rs b/wonnx-cli/src/utils.rs
new file mode 100644
index 00000000..bb73c10d
--- /dev/null
+++ b/wonnx-cli/src/utils.rs
@@ -0,0 +1,128 @@
+use ndarray::ArrayBase;
+use std::path::Path;
+use wonnx::utils::{DataTypeError, ScalarType, Shape};
+use wonnx::WonnxError;
+use wonnx_preprocessing::image::{load_bw_image, load_rgb_image};
+
+use wonnx::onnx::{ModelProto, TensorShapeProto, ValueInfoProto};
+
+use crate::types::NNXError;
+pub trait ValueInfoProtoUtil {
+    fn dimensions(&self) -> Vec<usize>;
+    fn data_type(&self) -> Result<ScalarType, DataTypeError>;
+}
+
+pub trait TensorShapeProtoUtil {
+    fn shape_dimensions(&self) -> Vec<usize>;
+}
+
+impl ValueInfoProtoUtil for ValueInfoProto {
+    fn dimensions(&self) -> Vec<usize> {
+        match &self.get_field_type().value {
+            Some(x) => match x {
+                wonnx::onnx::TypeProto_oneof_value::tensor_type(t) => {
+                    t.get_shape().shape_dimensions()
+                }
+                wonnx::onnx::TypeProto_oneof_value::sequence_type(_) => todo!(),
+                wonnx::onnx::TypeProto_oneof_value::map_type(_) => todo!(),
+                wonnx::onnx::TypeProto_oneof_value::optional_type(_) => todo!(),
+                wonnx::onnx::TypeProto_oneof_value::sparse_tensor_type(_) => todo!(),
+            },
+            None => vec![],
+        }
+    }
+
+    fn data_type(&self) -> Result<ScalarType, DataTypeError> {
+        Ok(match &self.get_field_type().value {
+            Some(x) => match x {
+                wonnx::onnx::TypeProto_oneof_value::tensor_type(t) => {
+                    ScalarType::from_i32(t.get_elem_type())?
+                }
+                wonnx::onnx::TypeProto_oneof_value::sequence_type(_) => todo!(),
+                wonnx::onnx::TypeProto_oneof_value::map_type(_) => todo!(),
+                wonnx::onnx::TypeProto_oneof_value::optional_type(_) => todo!(),
+                wonnx::onnx::TypeProto_oneof_value::sparse_tensor_type(_) => todo!(),
+            },
+            None => return Err(DataTypeError::Undefined),
+        })
+    }
+}
+
+impl TensorShapeProtoUtil for TensorShapeProto {
+    fn shape_dimensions(&self) -> Vec<usize> {
+        self.get_dim()
+            .iter()
+            .map(|d| match d.value {
+                Some(wonnx::onnx::TensorShapeProto_Dimension_oneof_value::dim_value(i)) => {
+                    i as usize
+                }
+                _ => 0,
+            })
+            .collect()
+    }
+}
+
+pub trait ModelProtoUtil {
+    fn get_input_shape(&self, input_name: &str) -> Result<Option<Shape>, WonnxError>;
+}
+
+impl ModelProtoUtil for ModelProto {
+    fn get_input_shape(&self, input_name: &str) -> Result<Option<Shape>, WonnxError> {
+        let value_info = self
+            .get_graph()
+            .get_input()
+            .iter()
+            .find(|x| x.get_name() == input_name);
+        match value_info {
+            Some(vi) => Ok(Some(vi.get_shape()?)),
+            None => Ok(None),
+        }
+    }
+}
+
+pub fn load_image_input(
+    input_image: &Path,
+    input_shape: &Shape,
+) -> Result<ArrayBase<ndarray::OwnedRepr<f32>, ndarray::IxDyn>, NNXError> {
+    if input_shape.rank() == 3 {
+        let mut w = input_shape.dim(1) as usize;
+        let mut h = input_shape.dim(2) as usize;
+        if w == 0 {
+            w = 224;
+        }
+        if h == 0 {
+            h = 224;
+        }
+
+        if input_shape.dim(0) == 3 {
+            log::info!("input is (3,?,?), loading as RGB image");
+            Ok(load_rgb_image(input_image, w, h).into_dyn())
+        } else if input_shape.dim(0) == 1 {
+            log::info!("input is (1,?,?), loading as BW image");
+            Ok(load_bw_image(input_image, w, h).into_dyn())
+        } else {
+            Err(NNXError::InvalidInputShape)
+        }
+    } else if input_shape.rank() == 4 {
+        let mut w = input_shape.dim(2) as usize;
+        let mut h = input_shape.dim(3) as usize;
+        if w == 0 {
+            w = 224;
+        }
+        if h == 0 {
+            h = 224;
+        }
+
+        if input_shape.dim(1) == 3 {
+            log::info!("input is (?,3,?,?), loading as RGB image");
+            Ok(load_rgb_image(input_image, w, h).into_dyn())
+        } else if input_shape.dim(1) == 1 {
+            log::info!("input is (?,1,?,?), loading as BW image");
+            Ok(load_bw_image(input_image, w, h).into_dyn())
+        } else {
+            Err(NNXError::InvalidInputShape)
+        }
+    } else {
+        Err(NNXError::InvalidInputShape)
+    }
+}
diff --git a/wonnx-preprocessing/Cargo.toml b/wonnx-preprocessing/Cargo.toml
new file mode 100644
index 00000000..65b25552
--- /dev/null
+++ b/wonnx-preprocessing/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "wonnx-preprocessing"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+image = "0.23.14"
+log = "0.4.14"
+ndarray = "0.15.4"
+protobuf = { version = "2.25.2", features = ["with-bytes"] }
+thiserror = "1.0.30"
+tokenizers = "0.11.0"
+tract-onnx = { version = "0.15.8", optional = true }
+wgpu = "*"
+wonnx = { path = "../wonnx" }
\ No newline at end of file
diff --git a/wonnx-preprocessing/src/image.rs b/wonnx-preprocessing/src/image.rs
new file mode 100644
index 00000000..9b98656b
--- /dev/null
+++ b/wonnx-preprocessing/src/image.rs
@@ -0,0 +1,73 @@
+use std::path::Path;
+
+use image::{imageops::FilterType, ImageBuffer, Pixel, Rgb};
+use ndarray::s;
+
+// Loads an image as (1,1,w,h) with pixels ranging 0...1 for 0..255 pixel values
+pub fn load_bw_image(
+    image_path: &Path,
+    width: usize,
+    height: usize,
+) -> ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<[usize; 4]>> {
+    let image_buffer: ImageBuffer<Rgb<u8>, Vec<u8>> = image::open(image_path)
+        .unwrap()
+        .resize_exact(width as u32, height as u32, FilterType::Nearest)
+        .to_rgb8();
+
+    // Python:
+    // # image[y, x, RGB]
+    // # x==0 --> left
+    // # y==0 --> top
+
+    // See https://github.com/onnx/models/blob/master/vision/classification/imagenet_inference.ipynb
+    // for pre-processing image.
+    // WARNING: Note order of declaration of arguments: (_,c,j,i)
+    ndarray::Array::from_shape_fn((1, 1, width, height), |(_, c, j, i)| {
+        let pixel = image_buffer.get_pixel(i as u32, j as u32);
+        let channels = pixel.channels();
+
+        // range [0, 255] -> range [0, 1]
+        (channels[c] as f32) / 255.0
+    })
+}
+
+// Loads an image as (1, w, h, 3)
+pub fn load_rgb_image(
+    image_path: &Path,
+    width: usize,
+    height: usize,
+) -> ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<[usize; 4]>> {
+    log::info!("load_rgb_image {:?} {}x{}", image_path, width, height);
+    let image_buffer: ImageBuffer<Rgb<u8>, Vec<u8>> = image::open(image_path)
+        .unwrap()
+        .resize_to_fill(width as u32, height as u32, FilterType::Nearest)
+        .to_rgb8();
+
+    // Python:
+    // # image[y, x, RGB]
+    // # x==0 --> left
+    // # y==0 --> top
+
+    // See https://github.com/onnx/models/blob/master/vision/classification/imagenet_inference.ipynb
+    // for pre-processing image.
+    // WARNING: Note order of declaration of arguments: (_,c,j,i)
+    let mut array = ndarray::Array::from_shape_fn((1, 3, 224, 224), |(_, c, j, i)| {
+        let pixel = image_buffer.get_pixel(i as u32, j as u32);
+        let channels = pixel.channels();
+
+        // range [0, 255] -> range [0, 1]
+        (channels[c] as f32) / 255.0
+    });
+
+    // Normalize channels to mean=[0.485, 0.456, 0.406] and std=[0.229, 0.224, 0.225]
+    let mean = [0.485, 0.456, 0.406];
+    let std = [0.229, 0.224, 0.225];
+    for c in 0..3 {
+        let mut channel_array = array.slice_mut(s![0, c, .., ..]);
+        channel_array -= mean[c];
+        channel_array /= std[c];
+    }
+
+    // Batch of 1
+    array
+}
diff --git a/wonnx-preprocessing/src/lib.rs b/wonnx-preprocessing/src/lib.rs
new file mode 100644
index 00000000..88778728
--- /dev/null
+++ b/wonnx-preprocessing/src/lib.rs
@@ -0,0 +1,27 @@
+use ndarray::ArrayBase;
+use wonnx::utils::InputTensor;
+
+pub mod image;
+pub mod text;
+
+pub enum Tensor {
+    F32(ArrayBase<ndarray::OwnedRepr<f32>, ndarray::IxDyn>),
+}
+
+impl Tensor {
+    // pub fn shape(&self) -> Shape {
+    //     match self {
+    //         Tensor::F32(a) => {
+    //             let shape = a.shape();
+    //             let shape_int: Vec<i64> = shape.iter().map(|x| *x as i64).collect();
+    //             Shape::from(ScalarType::F32, &shape_int[..])
+    //         }
+    //     }
+    // }
+
+    pub fn input_tensor(&self) -> InputTensor {
+        match self {
+            Tensor::F32(a) => InputTensor::F32(a.as_slice().unwrap()),
+        }
+    }
+}
diff --git a/wonnx-preprocessing/src/text.rs b/wonnx-preprocessing/src/text.rs
new file mode 100644
index 00000000..94917777
--- /dev/null
+++ b/wonnx-preprocessing/src/text.rs
@@ -0,0 +1,147 @@
+use std::borrow::Cow;
+use std::fs::File;
+use std::io::{BufRead, BufReader};
+use std::path::Path;
+use thiserror::Error;
+use tokenizers::models::wordpiece::WordPieceBuilder;
+use tokenizers::normalizers::BertNormalizer;
+use tokenizers::pre_tokenizers::bert::BertPreTokenizer;
+use tokenizers::processors::bert::BertProcessing;
+use tokenizers::utils::padding::{PaddingDirection::Right, PaddingParams, PaddingStrategy::Fixed};
+use tokenizers::utils::truncation::TruncationParams;
+use tokenizers::utils::truncation::TruncationStrategy::LongestFirst;
+use tokenizers::{AddedToken, EncodeInput, Encoding, InputSequence, Tokenizer};
+use wonnx::utils::Shape;
+
+use crate::Tensor;
+
+#[derive(Error, Debug)]
+pub enum PreprocessingError {
+    #[error("text tokenization error: {0}")]
+    TextTokenizationError(#[from] Box<dyn std::error::Error + Sync + Send>),
+}
+
+pub struct BertTokenizer {
+    pub tokenizer: Tokenizer,
+}
+
+pub struct BertEncodedText {
+    pub encoding: Encoding,
+}
+
+impl BertTokenizer {
+    pub fn new(vocab_path: &Path) -> BertTokenizer {
+        let wp_builder = WordPieceBuilder::new()
+            .files(vocab_path.as_os_str().to_string_lossy().to_string())
+            .continuing_subword_prefix("##".into())
+            .max_input_chars_per_word(100)
+            .unk_token("[UNK]".into())
+            .build()
+            .unwrap();
+
+        let mut tokenizer = Tokenizer::new(wp_builder);
+        tokenizer.with_padding(Some(PaddingParams {
+            strategy: Fixed(60),
+            direction: Right,
+            pad_id: 0,
+            pad_type_id: 0,
+            pad_token: "[PAD]".into(),
+            pad_to_multiple_of: None,
+        }));
+        tokenizer.with_truncation(Some(TruncationParams {
+            max_length: 60,
+            strategy: LongestFirst,
+            stride: 0,
+        }));
+        tokenizer.with_pre_tokenizer(BertPreTokenizer);
+        tokenizer.with_post_processor(BertProcessing::new(
+            ("[SEP]".into(), 102),
+            ("[CLS]".into(), 101),
+        ));
+        tokenizer.with_normalizer(BertNormalizer::new(true, true, Some(false), false));
+        tokenizer.add_special_tokens(&[
+            AddedToken {
+                content: "[PAD]".into(),
+                single_word: false,
+                lstrip: false,
+                rstrip: false,
+                normalized: false, //?
+            },
+            AddedToken {
+                content: "[CLS]".into(),
+                single_word: false,
+                lstrip: false,
+                rstrip: false,
+                normalized: false, //?
+            },
+            AddedToken {
+                content: "[SEP]".into(),
+                single_word: false,
+                lstrip: false,
+                rstrip: false,
+                normalized: false, //?
+            },
+            AddedToken {
+                content: "[MASK]".into(),
+                single_word: false,
+                lstrip: false,
+                rstrip: false,
+                normalized: false, //?
+            },
+        ]);
+
+        BertTokenizer { tokenizer }
+    }
+
+    fn tokenize(&self, text: &str) -> Result<BertEncodedText, PreprocessingError> {
+        let encoding = self
+            .tokenizer
+            .encode(
+                EncodeInput::Single(InputSequence::Raw(Cow::from(text))),
+                true,
+            )
+            .map_err(PreprocessingError::TextTokenizationError)?;
+        Ok(BertEncodedText { encoding })
+    }
+
+    pub fn get_mask_input_for(
+        &self,
+        text: &str,
+        shape: &Shape,
+    ) -> Result<Tensor, PreprocessingError> {
+        let segment_length = shape.dim(shape.rank() - 1) as usize;
+        let tokenized = self.tokenize(text)?;
+        let mut tokens = tokenized.get_mask();
+        tokens.resize(segment_length, 0);
+        let data = ndarray::Array::from_iter(tokens.iter().map(|x| (*x) as f32)).into_dyn();
+        Ok(Tensor::F32(data))
+    }
+
+    pub fn get_input_for(&self, text: &str, shape: &Shape) -> Result<Tensor, PreprocessingError> {
+        let segment_length = shape.dim(shape.rank() - 1) as usize;
+        let tokenized = self.tokenize(text)?;
+        let mut tokens = tokenized.get_tokens();
+        tokens.resize(segment_length, 0);
+        let data = ndarray::Array::from_iter(tokens.iter().map(|x| (*x) as f32)).into_dyn();
+        Ok(Tensor::F32(data))
+    }
+}
+
+impl BertEncodedText {
+    pub fn get_mask(&self) -> Vec<i64> {
+        self.encoding
+            .get_attention_mask()
+            .iter()
+            .map(|x| *x as i64)
+            .collect()
+    }
+
+    pub fn get_tokens(&self) -> Vec<i64> {
+        self.encoding.get_ids().iter().map(|x| *x as i64).collect()
+    }
+}
+
+pub fn get_lines(path: &Path) -> Vec<String> {
+    let file = BufReader::new(File::open(path).unwrap());
+    file.lines().map(|line| line.unwrap()).collect()
+}
diff --git a/py-wonnx/.gitignore b/wonnx-py/.gitignore
similarity index 100%
rename from py-wonnx/.gitignore
rename to wonnx-py/.gitignore
diff --git a/py-wonnx/Cargo.lock b/wonnx-py/Cargo.lock
similarity index 99%
rename from py-wonnx/Cargo.lock
rename to wonnx-py/Cargo.lock
index 51e249df..7a311271 100644
--- a/py-wonnx/Cargo.lock
+++ b/wonnx-py/Cargo.lock
@@ -943,7 +943,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "py-wonnx"
+name = "wonnx-py"
 version = "0.1.1"
 dependencies = [
  "pollster",
diff --git a/py-wonnx/Cargo.toml b/wonnx-py/Cargo.toml
similarity index 97%
rename from py-wonnx/Cargo.toml
rename to wonnx-py/Cargo.toml
index c7e796ff..3fd3ef93 100644
--- a/py-wonnx/Cargo.toml
+++ b/wonnx-py/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "py-wonnx"
+name = "wonnx-py"
 version = "0.1.1"
 edition = "2018"
 authors = ["haixuanTao <tao.xavier@outlook.com>", "Tommy van der Vorst <tommy@pixelspark.nl>"]
diff --git a/py-wonnx/pyproject.toml b/wonnx-py/pyproject.toml
similarity index 100%
rename from py-wonnx/pyproject.toml
rename to wonnx-py/pyproject.toml
diff --git a/py-wonnx/requirements.txt b/wonnx-py/requirements.txt
similarity index 100%
rename from py-wonnx/requirements.txt
rename to wonnx-py/requirements.txt
diff --git a/py-wonnx/src/lib.rs b/wonnx-py/src/lib.rs
similarity index 100%
rename from py-wonnx/src/lib.rs
rename to wonnx-py/src/lib.rs
diff --git a/wonnx/.gitignore b/wonnx/.gitignore
index f590ff4e..d68f6c26 100644
--- a/wonnx/.gitignore
+++ b/wonnx/.gitignore
@@ -1,7 +1,4 @@
 target
-**/*.onnx
 coverage
 *.profraw
 .DS_Store
-examples/data/
-synset.txt
diff --git a/wonnx/Cargo.toml b/wonnx/Cargo.toml
index 46fd9d98..983a1264 100644
--- a/wonnx/Cargo.toml
+++ b/wonnx/Cargo.toml
@@ -23,8 +23,6 @@ exclude = [
 [dependencies]
 wgpu = "0.12.0"
 bytemuck = "1.7.2"
-env_logger = "0.9.0"
-pollster = "0.2.4"
 log = "0.4.14" 
 protobuf = { version = "2", features = ["with-bytes"] }
 tera = "1.15.0"
@@ -37,9 +35,9 @@ num = "0.4.0"
 [dev-dependencies]
 image = "0.23.14"
 ndarray = "0.15.4"
-ureq = "2.3.1"
 approx = "0.5.0"
-
+pollster = "0.2.4"
+env_logger = "0.9.0"
 
 [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
 console_error_panic_hook = "0.1.6"
diff --git a/wonnx/examples/mnist.rs b/wonnx/examples/mnist.rs
index 9a5f387f..1c9164bb 100644
--- a/wonnx/examples/mnist.rs
+++ b/wonnx/examples/mnist.rs
@@ -32,8 +32,8 @@ async fn execute_gpu() -> Option<HashMap<String, Vec<f32>>> {
     );
 
     let model_path = Path::new(env!("CARGO_MANIFEST_DIR"))
-        .join("examples/data/models")
-        .join("opt-squeeze.onnx");
+        .join("../data/models")
+        .join("opt-mnist.onnx");
     let session = wonnx::Session::from_path(model_path).await.unwrap();
 
     let time_pre_compute = Instant::now();
@@ -65,7 +65,7 @@ fn main() {
 pub fn load_image() -> ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<[usize; 4]>> {
     let image_buffer: ImageBuffer<Rgb<u8>, Vec<u8>> = image::open(
         Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("examples/data/images")
+            .join("../data/images")
             .join("7.jpg"),
     )
     .unwrap()
diff --git a/wonnx/examples/squeeze.rs b/wonnx/examples/squeeze.rs
index eccfcb5c..7786b9b2 100644
--- a/wonnx/examples/squeeze.rs
+++ b/wonnx/examples/squeeze.rs
@@ -7,9 +7,8 @@ use ndarray::s;
 use std::time::Instant;
 use std::{
     fs,
-    io::{self, BufRead, BufReader},
+    io::{BufRead, BufReader},
     path::Path,
-    time::Duration,
 };
 use wonnx::utils::InputTensor;
 use wonnx::WonnxError;
@@ -41,7 +40,7 @@ async fn execute_gpu() -> Result<HashMap<String, Vec<f32>>, WonnxError> {
     );
 
     let model_path = Path::new(env!("CARGO_MANIFEST_DIR"))
-        .join("examples/data/models")
+        .join("../data/models")
         .join("opt-squeeze.onnx");
     let session = wonnx::Session::from_path(model_path).await?;
     let time_pre_compute = Instant::now();
@@ -79,7 +78,7 @@ pub fn load_image() -> ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<
         Path::new(&args[1]).to_path_buf()
     } else {
         Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("examples/data/images")
+            .join("../data/images")
             .join("pelican.jpeg")
     };
 
@@ -120,32 +119,8 @@ pub fn load_image() -> ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<
 fn get_imagenet_labels() -> Vec<String> {
     // Download the ImageNet class labels, matching SqueezeNet's classes.
     let labels_path = Path::new(env!("CARGO_MANIFEST_DIR"))
-        .join("examples/data/models")
-        .join("synset.txt");
-    if !labels_path.exists() {
-        let url = "https://s3.amazonaws.com/onnx-model-zoo/synset.txt";
-        println!("Downloading {:?} to {:?}...", url, labels_path);
-        let resp = ureq::get(url)
-            .timeout(Duration::from_secs(180)) // 3 minutes
-            .call()
-            .map_err(Box::new)
-            .unwrap();
-
-        let len = resp
-            .header("Content-Length")
-            .and_then(|s| s.parse::<usize>().ok())
-            .unwrap();
-        println!("Downloading {} bytes...", len);
-
-        let mut reader = resp.into_reader();
-
-        let f = fs::File::create(&labels_path).unwrap();
-        let mut writer = io::BufWriter::new(f);
-
-        let bytes_io_count = io::copy(&mut reader, &mut writer).unwrap();
-
-        assert_eq!(bytes_io_count, len as u64);
-    }
+        .join("../data/models")
+        .join("squeeze-labels.txt");
     let file = BufReader::new(fs::File::open(labels_path).unwrap());
 
     file.lines().map(|line| line.unwrap()).collect()


Test patch:
diff --git a/wonnx-cli/tests/cli.rs b/wonnx-cli/tests/cli.rs
new file mode 100644
index 00000000..a22e8393
--- /dev/null
+++ b/wonnx-cli/tests/cli.rs
@@ -0,0 +1,17 @@
+use assert_cmd::prelude::*;
+use std::{path::Path, process::Command};
+
+#[test]
+fn simple_inference() -> Result<(), Box<dyn std::error::Error>> {
+    let mut cmd = Command::cargo_bin("nnx")?;
+
+    let model_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../data/models/single_relu.onnx");
+
+    cmd.arg("infer")
+        .arg(model_path)
+        .arg("-r")
+        .arg("x=99,-99")
+        .assert()
+        .success();
+    Ok(())
+}
diff --git a/py-wonnx/tests/test_onnx_backend.py b/wonnx-py/tests/test_onnx_backend.py
similarity index 100%
rename from py-wonnx/tests/test_onnx_backend.py
rename to wonnx-py/tests/test_onnx_backend.py
diff --git a/py-wonnx/tests/test_wonnx.py b/wonnx-py/tests/test_wonnx.py
similarity index 100%
rename from py-wonnx/tests/test_wonnx.py
rename to wonnx-py/tests/test_wonnx.py
diff --git a/wonnx/tests/arithmetic.rs b/wonnx/tests/arithmetic.rs
index d0c666f1..b235fed4 100644
--- a/wonnx/tests/arithmetic.rs
+++ b/wonnx/tests/arithmetic.rs
@@ -4,6 +4,8 @@ use wonnx::{
     utils::{graph, model, node, tensor, tensor_of_type, InputTensor},
 };
 
+mod common;
+
 #[test]
 fn test_cos() {
     let n: usize = 16;
@@ -52,7 +54,7 @@ fn test_reciprocal() {
         pollster::block_on(wonnx::Session::from_model(model)).expect("Session did not create");
 
     let result = pollster::block_on(session.run(&input_data)).unwrap();
-    assert_eq!(result["Y"], reciprocal_data);
+    common::assert_eq_vector(result["Y"].as_slice(), &reciprocal_data);
 }
 
 #[test]
diff --git a/wonnx/tests/batchnormalization.rs b/wonnx/tests/batchnormalization.rs
index 15546b29..fca3bdef 100644
--- a/wonnx/tests/batchnormalization.rs
+++ b/wonnx/tests/batchnormalization.rs
@@ -1,13 +1,6 @@
-use approx::assert_ulps_eq;
 use std::collections::HashMap;
 use wonnx::utils::{attribute, graph, initializer, model, node, tensor, InputTensor};
-
-fn abs_eq_vector(xs: &[f32], ys: &[f32]) {
-    assert_eq!(xs.len(), ys.len());
-    for i in 0..xs.len() {
-        assert_ulps_eq!(xs[i], ys[i], max_ulps = 2);
-    }
-}
+mod common;
 
 #[test]
 fn batch_normalization() {
@@ -68,7 +61,7 @@ fn batch_normalization() {
     let result = pollster::block_on(session.run(&input_data)).unwrap();
     let out_y = &result["Y"];
 
-    abs_eq_vector(
+    common::assert_eq_vector(
         out_y.as_slice(),
         &[
             // Y = (X - input_mean) / sqrt(input_var + epsilon) * scale + B
diff --git a/wonnx/tests/common/mod.rs b/wonnx/tests/common/mod.rs
new file mode 100644
index 00000000..698780f6
--- /dev/null
+++ b/wonnx/tests/common/mod.rs
@@ -0,0 +1,9 @@
+use approx::assert_ulps_eq;
+
+/// Assert two vectors are equal up to a specific number of units in last place (ULPS)
+pub fn assert_eq_vector(xs: &[f32], ys: &[f32]) {
+    assert_eq!(xs.len(), ys.len());
+    for i in 0..xs.len() {
+        assert_ulps_eq!(xs[i], ys[i], max_ulps = 2);
+    }
+}
diff --git a/wonnx/tests/globalaveragepool.rs b/wonnx/tests/globalaveragepool.rs
index 9db3ebfd..58c97b50 100644
--- a/wonnx/tests/globalaveragepool.rs
+++ b/wonnx/tests/globalaveragepool.rs
@@ -1,13 +1,6 @@
-use approx::assert_ulps_eq;
 use std::collections::HashMap;
 use wonnx::utils::{graph, model, node, tensor, InputTensor};
-
-fn abs_eq_vector(xs: &[f32], ys: &[f32]) {
-    assert_eq!(xs.len(), ys.len());
-    for i in 0..xs.len() {
-        assert_ulps_eq!(xs[i], ys[i], max_ulps = 2);
-    }
-}
+mod common;
 
 #[test]
 fn global_average_pool() {
@@ -56,5 +49,5 @@ fn global_average_pool() {
     // Channel 2: [[4,5], [6,7]] => average is 5,5
     // Channel 3: [[8,9], [10, 11]] => average is 9,5
     // Channel 4: [[12,13], [14, 15]] => average is 13,5
-    abs_eq_vector(out_y.as_slice(), &[1.5, 5.5, 9.5, 13.5]);
+    common::assert_eq_vector(out_y.as_slice(), &[1.5, 5.5, 9.5, 13.5]);
 }
diff --git a/wonnx/tests/pretrained_models.rs b/wonnx/tests/pretrained_models.rs
index 187a8f9b..6530b89f 100644
--- a/wonnx/tests/pretrained_models.rs
+++ b/wonnx/tests/pretrained_models.rs
@@ -12,10 +12,8 @@ fn test_relu() {
     let data = vec![-1.0f32, 1.0];
     input_data.insert("x".to_string(), InputTensor::F32(data.as_slice()));
 
-    let session = pollster::block_on(wonnx::Session::from_path(
-        "examples/data/models/single_relu.onnx",
-    ))
-    .expect("session did not create");
+    let session = pollster::block_on(wonnx::Session::from_path("../data/models/single_relu.onnx"))
+        .expect("session did not create");
     let result = pollster::block_on(session.run(&input_data)).unwrap();
 
     assert_eq!(result["y"], &[0.0, 1.0]);
@@ -30,10 +28,8 @@ fn test_mnist() {
         "Input3".to_string(),
         InputTensor::F32(image.as_slice().unwrap()),
     );
-    let session = pollster::block_on(wonnx::Session::from_path(
-        "examples/data/models/opt-mnist.onnx",
-    ))
-    .expect("Session did not create");
+    let session = pollster::block_on(wonnx::Session::from_path("../data/models/opt-mnist.onnx"))
+        .expect("Session did not create");
 
     let result = pollster::block_on(session.run(&input_data)).unwrap()["Plus214_Output_0"]
         .iter()
@@ -118,10 +114,8 @@ fn test_squeeze() {
         InputTensor::F32(image.as_slice().unwrap()),
     );
 
-    let session = pollster::block_on(wonnx::Session::from_path(
-        "examples/data/models/opt-squeeze.onnx",
-    ))
-    .expect("session did not create");
+    let session = pollster::block_on(wonnx::Session::from_path("../data/models/opt-squeeze.onnx"))
+        .expect("session did not create");
     let result =
         &pollster::block_on(session.run(&input_data)).unwrap()["squeezenet0_flatten0_reshape0"];
     let mut probabilities = result.iter().enumerate().collect::<Vec<_>>();
@@ -136,7 +130,7 @@ pub fn load_image(
 ) -> ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<[usize; 4]>> {
     let image_buffer: ImageBuffer<Rgb<u8>, Vec<u8>> = image::open(
         Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("examples/data/images")
+            .join("../data/images")
             .join(image_path),
     )
     .unwrap()
@@ -164,7 +158,7 @@ pub fn load_squeezenet_image(
 ) -> ndarray::ArrayBase<ndarray::OwnedRepr<f32>, ndarray::Dim<[usize; 4]>> {
     let image_buffer: ImageBuffer<Rgb<u8>, Vec<u8>> = image::open(
         Path::new(env!("CARGO_MANIFEST_DIR"))
-            .join("examples/data/images")
+            .join("../data/images")
             .join("bald_eagle.jpeg"),
     )
     .unwrap()