wai-ops: the bounds pass, certificates from two certifiers, and the operator set's Kani harnesses (D2b) #221

Merged
dcharlot merged 48 commits from wai/d2b-bounds into main 2026-10-13 00:53:47 -04:00
85 changed files with 7654 additions and 1488 deletions

View file

@ -22,6 +22,15 @@ name: wai formal verification (integer core)
# The proofs are bounded (small symbolic CDFs, fixed-length symbolic streams,
# explicit unwind limits). Bounded is not universal, and the job claims only
# what it checks.
#
# It also checks the integer operator set (wai/extensions/integer-operators.md):
# the harnesses of its test plan, in wai/wai-ops/src/proofs/, which the host
# includes beside the operator set's sources. They prove the roundings, the
# requantisation, a 32-bit fabric's limb product and epilogue, the soundness of
# the bound rules against the interpreter, the stage operators' totality and
# refusal order, the plans, and the graph form's parse. Each harness states its
# window. The harnesses run in shards; the first job holds the shards to
# covering every harness exactly once.
# One run per branch at a time. A push supersedes the run before it.
#
@ -55,10 +64,13 @@ on:
- 'wai/wai-rs/src/wai_video_policy.rs'
- 'wai/wai-rs/src/staged/predict.rs'
- 'wai/wai-rs/src/staged/wir1_arith.rs'
# Not verified by any harness, but #[path]-included because int_transform's
# IGDN dispatches to it, so it must still compile under the model checker
# (its cfg(kani) branches are compiled nowhere else).
# Verified by K10 as the fast path of the reference IGDN, and #[path]-included
# because int_transform's IGDN dispatches to it, so it must also compile under
# the model checker (its cfg(kani) branches are compiled nowhere else).
- 'wai/wai-rs/src/int_kernels.rs'
# The integer operator set: its sources, which the host includes module by
# module, and its harnesses (src/proofs/).
- 'wai/wai-ops/src/**'
- '.github/workflows/wai-formal-verification.yml'
push:
branches:
@ -72,19 +84,66 @@ on:
- 'wai/wai-rs/src/staged/predict.rs'
- 'wai/wai-rs/src/staged/wir1_arith.rs'
- 'wai/wai-rs/src/int_kernels.rs'
- 'wai/wai-ops/src/**'
- '.github/workflows/wai-formal-verification.yml'
jobs:
kani:
# Every harness the host compiles runs in exactly one shard of the matrix below:
# one no shard runs would be compiled and never checked. No toolchain is needed;
# the script reads the included sources and this file.
shards:
name: every harness in one shard
runs-on: linux
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Every harness runs in exactly one shard
run: python3 -I wai/wai-verify/check_shards.py .
kani:
name: kani (${{ matrix.shard }})
runs-on: ${{ matrix.os }}
timeout-minutes: 120
strategy:
fail-fast: false
# Each shard is a list of `--harness` filters: a harness runs when its full name
# contains one. The integer operator set's harnesses (K1–K33 of its test plan)
# are in proofs::, split so that no shard runs much past an hour; the two 512-bit
# isqrt harnesses and the longest stage harnesses have shards of their own.
# Harnesses gated by cfg(kani_open) are not compiled here.
#
# Each shard runs where its memory fits. The rans64 totality proof (k26) peaks at
# about 28 GiB of resident memory and takes about 690 s of verification (measured
# with /usr/bin/time -l on a 256 GB host); the linux runner has 15 GiB, shared by
# two jobs, and CBMC ran out of memory there. It has a shard of its own on the
# macos runner. Every other shard has run to completion on the linux runner.
matrix:
include:
- { shard: integer-core, os: linux, harness: "rans:: int_entropy:: int_transform:: wai_video_policy:: staged_predict:: staged_wir1_arith::" }
- { shard: arith, os: linux, harness: "proofs::arith::k01 proofs::arith::k03 proofs::arith::k13_isqrt_u128 proofs::arith::k17 proofs::arith::k32 proofs::limb::" }
- { shard: isqrt512, os: linux, harness: "proofs::arith::k13_isqrt_i512 proofs::arith::k13_isqrt_ceil_i512" }
- { shard: stage, os: linux, harness: "proofs::stage::k23 proofs::stage::k27 proofs::stage::k28 proofs::stage::k29_residual_step_32bit_identity_registered proofs::stage::k30" }
- { shard: stage-long, os: linux, harness: "proofs::stage::k29_residual_step_32bit_identity_every_range" }
- { shard: rans64-t2, os: macos, harness: "proofs::stage::k26" }
- { shard: sound, os: linux, harness: "proofs::sound::" }
- { shard: plan, os: linux, harness: "proofs::plan::" }
steps:
- uses: actions/checkout@v4
# dtolnay/rust-toolchain cannot help — it calls `rustup`, which is absent
# on this runner image.
# on the linux runner's image. The macOS runner executes on the host, which
# has rustup in ~/.cargo/bin, but launchd's PATH omits that directory: it
# is put first, so the host's install is found and left as it is.
- name: Install rustup + toolchain
shell: bash
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
if ! command -v rustup >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain 1.98.0
@ -92,8 +151,11 @@ jobs:
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Kani ships its own pinned nightly and a CBMC build; both are slow to
# fetch, so they are cached on the verifier version.
# fetch, so they are cached on the verifier version. Not on macOS: that
# runner's home is the host's own, where Kani is installed, and a restore
# would write over a live install.
- name: Cache Kani toolchain
if: matrix.os != 'macos'
uses: actions/cache@v4
with:
path: |
@ -102,20 +164,41 @@ jobs:
~/.kani
key: kani-0.67.0-${{ runner.os }}
# The same steps on every runner: Kani 0.67.0 unless that version is installed,
# then its setup unless it is set up. Setup fetches the release bundle (CBMC and
# the driver) and installs the pinned nightly, which the bundle's `toolchain`
# links to in ~/.rustup. Run again, it fetches and installs both again, so it is
# skipped when the bundle and its toolchain are both in place: on the macOS
# host, which is set up, it would rewrite a live install. The cache holds the
# bundle but not ~/.rustup, so a container sets up every time.
- name: Install Kani
shell: bash
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
command -v cargo-kani >/dev/null || cargo install --locked kani-verifier --version 0.67.0
# Idempotent; fetches the pinned nightly + CBMC on a cold cache.
cargo kani setup
if [ "$(cargo kani --version 2>/dev/null || true)" != "cargo-kani 0.67.0" ]; then
cargo install --locked kani-verifier --version 0.67.0
fi
k="$HOME/.kani/kani-0.67.0"
if [ ! -x "$k/bin/kani-driver" ] || [ ! -x "$k/bin/cbmc" ] || [ ! -e "$k/toolchain/bin/rustc" ]; then
cargo kani setup
fi
cargo kani --version
- name: Verify the integer core
- name: Verify the shard's harnesses
working-directory: wai/wai-verify
shell: bash
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
# Every harness must verify. A failing harness is a real finding
# about the decoder, so this job is allowed to be the thing that
# blocks the merge.
cargo kani
#
# The harnesses are in the library. The crate's one binary, the
# `wai-verify` tool, needs the `cli` feature, and a target whose
# required features are off stops `cargo kani` from building any;
# `--lib` selects the library alone.
args=()
for f in ${{ matrix.harness }}; do args+=(--harness "$f"); done
cargo kani --lib "${args[@]}"

View file

@ -23,6 +23,11 @@ name: WAI JPEG-AI whole decode (pinned inputs, required)
# of samples differ from it at all (the test's bar);
# - two decodes in one process are byte-identical and the reconstruction binding
# verifies the second and rejects a one-byte change;
# - the bundle's bounds certificate (wai/extensions/integer-operators.md §8.5,
# §13.6.1: the two scale graphs) is byte for byte the one committed under the
# set's digest in wai/ops-conformance/certificates/, which is the listing digest
# the set-form pin below covers, from both certifiers (wai-verify and the
# independent tools/wai_ops_check.py), and its verdict meets acc32;
# - the decoded pixels hash to WAI_JPEGAI_EXPECT_SHA256 below: the value measured
# from these inputs on an Apple Silicon Mac (aarch64 macOS, native) and
# reproduced by an x86_64 Linux build (glibc, run under QEMU user-mode
@ -108,6 +113,16 @@ on:
- 'wai/wai-rs/tests/support/**'
- 'wai/tools/wai_jpegai_fetch_e2e_inputs.sh'
- 'wai/tools/jpegai-e2e-v1.sha256'
# The bounds certificate of the bundle's scale path (the step after the fetch):
# the certifier, and the certificate committed by digest.
- 'wai/wai-ops/src/**'
- 'wai/wai-ops/tests/goldens.rs'
- 'wai/wai-verify/Cargo.toml'
- 'wai/wai-verify/Cargo.lock'
- 'wai/wai-verify/src/bin/**'
- 'wai/ops-conformance/certificates/external.json'
- 'wai/ops-conformance/certificates/wai.image.jpegai/3e243dc11566922f5cb7f4c628afd440b18d56a08b99b07cc33d5c8522aeec37.json'
- 'wai/tools/wai_ops_check.py'
- '.github/workflows/wai-jpegai-e2e.yml'
pull_request:
paths:
@ -133,6 +148,16 @@ on:
- 'wai/wai-rs/tests/support/**'
- 'wai/tools/wai_jpegai_fetch_e2e_inputs.sh'
- 'wai/tools/jpegai-e2e-v1.sha256'
# The bounds certificate of the bundle's scale path (the step after the fetch):
# the certifier, and the certificate committed by digest.
- 'wai/wai-ops/src/**'
- 'wai/wai-ops/tests/goldens.rs'
- 'wai/wai-verify/Cargo.toml'
- 'wai/wai-verify/Cargo.lock'
- 'wai/wai-verify/src/bin/**'
- 'wai/ops-conformance/certificates/external.json'
- 'wai/ops-conformance/certificates/wai.image.jpegai/3e243dc11566922f5cb7f4c628afd440b18d56a08b99b07cc33d5c8522aeec37.json'
- 'wai/tools/wai_ops_check.py'
- '.github/workflows/wai-jpegai-e2e.yml'
# Weekly, independent of changes: the inputs live outside the repository, and
# their disappearing should surface here, not at the next unrelated PR.
@ -165,6 +190,53 @@ jobs:
shell: bash
working-directory: wai
run: tools/wai_jpegai_fetch_e2e_inputs.sh "$WAI_JPEGAI_DIR"
# The scale path's bounds certificate (integer-operators §8.5, §13.6.1). The
# bundle is not committed, so its certificate is, under the set's digest (the
# listing of norm.json and weights/*, as the set-form pin covers it), and
# listed in wai/ops-conformance/certificates/external.json.
- name: The bundle's bounds certificate is the committed one, and meets acc32
shell: bash
working-directory: wai
run: |
set -euo pipefail
(cd wai-verify && cargo build --locked --release --features cli --bin wai-verify)
D="$WAI_JPEGAI_DIR"
M=(--member "norm.json=$D/norm.json")
for n in hs_y hs_uv hyp_y hyp_uv synth_y synth_uv mcm0_y mcm1_y mcm2_y mcm3_y icci; do
M+=(--member "weights/$n.json=$D/$n.json")
done
wai-verify/target/release/wai-verify bounds --capability wai.image.jpegai "${M[@]}" --profile acc32 \
--out "${RUNNER_TEMP:-/tmp}/jpegai-bounds.json" \
--check ops-conformance/certificates/wai.image.jpegai/3e243dc11566922f5cb7f4c628afd440b18d56a08b99b07cc33d5c8522aeec37.json
# The same certificate from the independent certifier (Python standard library
# only, sharing no code with wai-ops).
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: The independent certifier writes the same certificate, and it meets acc32
shell: bash
working-directory: wai
run: |
set -euo pipefail
D="$WAI_JPEGAI_DIR"
M=(--member "norm.json=$D/norm.json")
for n in hs_y hs_uv hyp_y hyp_uv synth_y synth_uv mcm0_y mcm1_y mcm2_y mcm3_y icci; do
M+=(--member "weights/$n.json=$D/$n.json")
done
python3 -I tools/wai_ops_check.py certify --capability wai.image.jpegai --lowering jpegai-int/1 "${M[@]}" \
--profile acc32 --out "${RUNNER_TEMP:-/tmp}/jpegai-bounds-py.json" \
--check ops-conformance/certificates/wai.image.jpegai/3e243dc11566922f5cb7f4c628afd440b18d56a08b99b07cc33d5c8522aeec37.json
# The operator set's lowering of the bundle's scale path (integer-operators
# §13.6.1), run through the interpreter with every value held to its certified
# interval, against the legacy decoder's arithmetic on the same bundle.
- name: The scale path's lowering decodes as the legacy arithmetic does
shell: bash
working-directory: wai/wai-ops
run: |
set -euo pipefail
WAI_OPS_JPEGAI_BUNDLE="$WAI_JPEGAI_DIR" \
cargo test --locked --release --test goldens -- --ignored --exact jpegai_scale_path_on_a_supplied_bundle
- name: Whole decode vs the reference, determinism, pinned pixel hash (missing input = failure)
shell: bash
working-directory: wai/wai-rs

View file

@ -5,17 +5,23 @@ name: wai-ops integer operators
# wai/ops-conformance/ from its inputs and requires the committed bytes:
#
# - Linux x86_64, Linux arm64 and macOS arm64, natively, debug and release: the
# unit tests, the goldens (every committed parameter set lowered, loaded and
# unit tests (the bound rules against hand-computed values among them), the
# goldens (every committed parameter set lowered, loaded through load step 8 and
# decoded through the interpreter to its committed output digest), the step-8
# test and the doc tests, each in the default build and with the `uncertified`
# switch, and `wai-ops vectors verify`, which also re-lowers every set and
# regenerates the whole corpus;
# - WASI (wasm32-wasip2) under wasmtime, a 32-bit target: the same unit tests,
# goldens and step-8 test in both builds, and the same `vectors verify`;
# and verdict tests and the doc tests; `wai-verify vectors verify`, which also
# re-lowers every set, recomputes its bounds certificate from the committed
# lowered graphs and regenerates the whole corpus, so every certificate is
# byte-identical on every leg; and `wai-verify bounds` on the falsification sets
# of integer-operators §14.3, which must exit 1, and on committed sets, which
# must exit 0 under their profile;
# - WASI (wasm32-wasip2) under wasmtime, a 32-bit target: the same tests, and the
# same `vectors verify`;
# - a browser's engine (wasm32-unknown-unknown) under Node: the recomputation of
# every vector file;
# - the independent checker, tools/wai_ops_check.py: Python standard library only,
# sharing no code with wai-ops, rewrites every vector file and every lowered graph.
# sharing no code with wai-ops, rewrites every vector file and every lowered graph,
# and, as a second certifier, every certificate, the falsification sets' included
# (each must fail the profile it claims).
#
# There is no Windows leg: the Windows runner has been offline since 2026-08-28.
@ -33,6 +39,8 @@ on:
pull_request:
paths:
- 'wai/wai-ops/**'
# The certifier, the lowerings and the corpus tool, and the Kani host they share.
- 'wai/wai-verify/**'
- 'wai/ops-conformance/**'
- 'wai/tools/wai_ops_*.py'
# The text the checker is held to.
@ -52,6 +60,8 @@ on:
- main
paths:
- 'wai/wai-ops/**'
# The certifier, the lowerings and the corpus tool, and the Kani host they share.
- 'wai/wai-verify/**'
- 'wai/ops-conformance/**'
- 'wai/tools/wai_ops_*.py'
- 'wai/extensions/integer-operators.md'
@ -91,28 +101,66 @@ jobs:
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Without the `uncertified` switch, load step 8 refuses every graph: the default
# build is what a decoder would link before the certifier exists. Its tests show
# that, and the doc tests that a graph runs only once loaded.
- name: Unit, step-8 and doc tests, default build
- name: Unit, goldens, step-8, verdict and doc tests, debug
working-directory: wai/wai-ops
shell: bash
run: cargo test --locked
- name: Unit tests and goldens, debug
- name: Unit, goldens, step-8 and verdict tests, release
working-directory: wai/wai-ops
shell: bash
run: cargo test --locked --features uncertified
run: cargo test --locked --release
- name: Unit tests and goldens, release
working-directory: wai/wai-ops
- name: Build wai-verify, and its own tests (the witness through the interpreter; --check's path)
working-directory: wai/wai-verify
shell: bash
run: cargo test --locked --release --features uncertified
run: |
set -euo pipefail
cargo build --locked --release --features cli --bin wai-verify
cargo test --locked --release --features cli --bin wai-verify
- name: Every vector file recomputed, every set re-lowered, the corpus regenerated
working-directory: wai/wai-ops
- name: Every vector file recomputed, every set re-lowered and certified, the corpus regenerated
working-directory: wai
shell: bash
run: cargo run --locked --release --features uncertified --bin wai-ops -- vectors verify ..
run: wai-verify/target/release/wai-verify vectors verify .
# The exit status of `wai-verify bounds` (0: the verdict meets the profile, 1: it
# fails it, 3: a difference from the committed certificate), on the sets whose
# verdict integer-operators §14.3 and the design's predictions fix.
- name: The falsification sets fail their profile; committed sets meet theirs
working-directory: wai
shell: bash
run: |
set -euo pipefail
V=wai-verify/target/release/wai-verify
C=ops-conformance/certificates/wai.neural.int_synth
T="${RUNNER_TEMP:-/tmp}"
bounds() {
local want=$1; shift
set +e; "$V" bounds "$@" --out "$T/cert.json"; local got=$?; set -e
[ "$got" = "$want" ] || { echo "::error::wai-verify bounds $* exited $got, not $want"; exit 1; }
}
# One unit past i64 under acc64, and a claim of acc32 one weight past it.
bounds 1 ops-conformance/sets/overflow_acc64/int_synth.json --capability wai.neural.int_synth --profile acc64 \
--check $C/b35460f39f31a559e97e66b8dde4597a309cbc44199c40097f935dd8e6e7305a.json
bounds 1 ops-conformance/sets/claim_acc32/int_synth.json --capability wai.neural.int_synth --profile acc32 \
--check $C/76916c8f85da7663c09fba8cb091d628ed38328b680563285b8056cec82fd1ac.json
# facade meets acc32; photo meets acc64 and fails acc32 (at node 6, convT.acc).
bounds 0 neural-conformance/vectors/facade/int_synth.json --capability wai.neural.int_synth --profile acc32 \
--check $C/c6a362696d61605931ef4da98ae630362dcd73ea6be7864db6e61d6c5abbcd24.json
bounds 0 neural-conformance/vectors/photo/int_synth.json --capability wai.neural.int_synth --profile acc64 \
--check $C/50868e72f5f1599569c413ed1f67ab8610fa0b3c17919162cbf370f49275125c.json
# With --witness, a node-local graph for each failing node: photo's node 6, whose
# input attains convT.acc's upper end. With --advise, the clamps that would let
# the overflow set meet acc64.
bounds 1 neural-conformance/vectors/photo/int_synth.json --capability wai.neural.int_synth --profile acc32 \
--witness "$T/witness"
test -s "$T/witness/synth-6.json"
grep -q '"acc":"26172239136"' "$T/witness/synth-6.json"
bounds 1 ops-conformance/sets/overflow_acc64/int_synth.json --capability wai.neural.int_synth --advise acc64
# A certificate that is not the committed one is exit 3.
bounds 3 neural-conformance/vectors/photo/int_synth.json --capability wai.neural.int_synth \
--check $C/c6a362696d61605931ef4da98ae630362dcd73ea6be7864db6e61d6c5abbcd24.json
wasip2:
name: wasm32-wasip2 (wasmtime)
@ -144,7 +192,7 @@ jobs:
# The tests read committed files through CARGO_MANIFEST_DIR/.., so the wai
# directory is preopened at its own absolute path.
- name: Unit and step-8 tests, default build, under WASI
- name: Unit, goldens, step-8 and verdict tests, under WASI
working-directory: wai/wai-ops
shell: bash
run: |
@ -153,27 +201,18 @@ jobs:
CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run --dir $W::$W" \
cargo test --locked --release --target wasm32-wasip2
- name: Unit tests, goldens and step-8 test, under WASI
working-directory: wai/wai-ops
- name: Build wai-verify for wasm32-wasip2
working-directory: wai/wai-verify
shell: bash
run: |
set -euo pipefail
W=$(cd .. && pwd)
CARGO_TARGET_WASM32_WASIP2_RUNNER="wasmtime run --dir $W::$W" \
cargo test --locked --release --features uncertified --target wasm32-wasip2
run: cargo build --locked --release --features cli --target wasm32-wasip2 --bin wai-verify
- name: Build the tool for wasm32-wasip2
working-directory: wai/wai-ops
shell: bash
run: cargo build --locked --release --features uncertified --target wasm32-wasip2 --bin wai-ops
- name: Every vector file recomputed, every set re-lowered, the corpus regenerated, under WASI
- name: Every vector file recomputed, every set re-lowered and certified, the corpus regenerated, under WASI
working-directory: wai
shell: bash
run: |
set -euo pipefail
wasmtime --version
wasmtime run --dir .::/wai wai-ops/target/wasm32-wasip2/release/wai-ops.wasm vectors verify /wai
wasmtime run --dir .::/wai wai-verify/target/wasm32-wasip2/release/wai-verify.wasm vectors verify /wai
node:
name: wasm32 under Node
@ -219,7 +258,7 @@ jobs:
python-version: '3.11'
# Standard library only, so it needs nothing installed.
- name: wai_ops_check.py rewrites every vector file and every lowered graph
- name: wai_ops_check.py rewrites every vector file, every lowered graph and every certificate
working-directory: wai
shell: bash
run: python3 -I tools/wai_ops_check.py ops-conformance

View file

@ -22,6 +22,12 @@ name: WAI mbt2018_mean integer decode (regenerated vectors)
# - the Rust integer decode reproduces the export tool's integer reference
# byte-for-byte (synthesis, hyper-decoder, the whole conditional bitstream, the
# compact binary model, the eight-frame clip);
# - the regenerated set's bounds certificate (wai/extensions/integer-operators.md
# §8.5) is byte for byte the one committed under its set digest in
# wai/ops-conformance/certificates/, from both certifiers (wai-verify and the
# independent tools/wai_ops_check.py), and its verdict meets acc64. The set is
# not committed, so this job is where its certificate is checked; the set's
# three files have had the same digests on every run and machine recorded;
# - on the pinned photograph, the integer decode costs at most 1 dB against the
# same model's float decode (the bar CI enforces), measured by the Rust decoder
# on its own output. Measured on an Apple Silicon Mac: 35.985 dB for the integer
@ -129,6 +135,15 @@ on:
- 'wai/tools/requirements-sota-int.txt'
# The wasm32 decode of the same vectors (the last steps below).
- 'wai/wai-web/**'
# The bounds certificate of the regenerated set (the step after the
# regeneration): the certifier, and the certificate committed by digest.
- 'wai/wai-ops/src/**'
- 'wai/wai-verify/Cargo.toml'
- 'wai/wai-verify/Cargo.lock'
- 'wai/wai-verify/src/bin/**'
- 'wai/ops-conformance/certificates/external.json'
- 'wai/ops-conformance/certificates/wai.neural.int_hyper/f16fd09cd79b831d7d821e030c05e99e9fb5ea216574e699944ea4e55ac0a404.json'
- 'wai/tools/wai_ops_check.py'
- '.github/workflows/wai-sota-vectors.yml'
pull_request:
paths:
@ -151,6 +166,15 @@ on:
- 'wai/tools/requirements-sota-int.txt'
# The wasm32 decode of the same vectors (the last steps below).
- 'wai/wai-web/**'
# The bounds certificate of the regenerated set (the step after the
# regeneration): the certifier, and the certificate committed by digest.
- 'wai/wai-ops/src/**'
- 'wai/wai-verify/Cargo.toml'
- 'wai/wai-verify/Cargo.lock'
- 'wai/wai-verify/src/bin/**'
- 'wai/ops-conformance/certificates/external.json'
- 'wai/ops-conformance/certificates/wai.neural.int_hyper/f16fd09cd79b831d7d821e030c05e99e9fb5ea216574e699944ea4e55ac0a404.json'
- 'wai/tools/wai_ops_check.py'
- '.github/workflows/wai-sota-vectors.yml'
# Weekly, independent of changes: the job depends on artifacts fetched from
# outside the repository (the checkpoint, the pinned wheels), and their
@ -228,6 +252,35 @@ jobs:
# files differ between machines (see the header). It is not a list any
# other run must reproduce, here or elsewhere.
(cd "$WAI_SOTA_INT_DIR" && sha256sum *)
# The set's bounds certificate (integer-operators §8.5). The set is not
# committed, so its certificate is, under the set's digest, and listed in
# wai/ops-conformance/certificates/external.json. A regenerated set with other
# bytes has another digest, and --check fails on set_sha256 first.
- name: The regenerated set's bounds certificate is the committed one, and meets acc64
shell: bash
working-directory: wai
run: |
set -euo pipefail
(cd wai-verify && cargo build --locked --release --features cli --bin wai-verify)
D="$WAI_SOTA_INT_DIR"
wai-verify/target/release/wai-verify bounds --capability wai.neural.int_hyper \
--member h_s.whm="$D/int_hs_model.bin" --member g_s.whm="$D/int_synth_sota_model.bin" \
--member entropy.json="$D/cond_entropy.json" --profile acc64 \
--out "${RUNNER_TEMP:-/tmp}/int-hyper-bounds.json" \
--check ops-conformance/certificates/wai.neural.int_hyper/f16fd09cd79b831d7d821e030c05e99e9fb5ea216574e699944ea4e55ac0a404.json
# The same certificate from the independent certifier (Python standard library
# only, sharing no code with wai-ops), which needs only the Python set up above.
- name: The independent certifier writes the same certificate, and it meets acc64
shell: bash
working-directory: wai
run: |
set -euo pipefail
D="$WAI_SOTA_INT_DIR"
python3 -I tools/wai_ops_check.py certify --capability wai.neural.int_hyper --lowering whm1/1 \
--member h_s.whm="$D/int_hs_model.bin" --member g_s.whm="$D/int_synth_sota_model.bin" \
--member entropy.json="$D/cond_entropy.json" --profile acc64 \
--out "${RUNNER_TEMP:-/tmp}/int-hyper-bounds-py.json" \
--check ops-conformance/certificates/wai.neural.int_hyper/f16fd09cd79b831d7d821e030c05e99e9fb5ea216574e699944ea4e55ac0a404.json
- name: Rust decodes every int_hyper vector byte-for-byte (missing input = failure), and the missing-input rule holds
shell: bash
working-directory: wai/wai-rs

View file

@ -5,7 +5,7 @@ Vectors for `wai-ops/1`, the integer operator set
operators (§5), its stage operators (§5E), the `WHG1` graph form (§10), and the
canonical lowerings of the registered integer network forms (§13).
**Provenance.** Generated here by `wai-ops vectors gen` from fixed SplitMix64
**Provenance.** Generated here by `wai-verify vectors gen` from fixed SplitMix64
sequences: every graph, parameter, input and stream under `vectors/`, `stage/`,
`graphs/` and `sets/` is generated integers, with no media. The lowered graphs under
`certificates/` are those of the sets this corpus generates and of the parameter sets
@ -33,10 +33,26 @@ lowering conforms when it writes every lowered graph byte for byte.
- `lower_<form>` — each lowering of §13 on a small set from `sets/`.
- `sets/<name>/` — the small parameter sets the `lower_*` vectors lower, one per
lowering, in their registered forms.
- `certificates/<capability>/<set digest>/<role>.whg1` — each lowered graph (§14.3).
- `certificates/<capability>/<set digest>.json` — each set's bounds certificate
(§8.5, §14.3), and beside it `certificates/<capability>/<set digest>/<role>.whg1`,
each lowered graph.
- `certificates/index.json` — every set lowered here: its capability, its lowering,
its members (the name in the set and the file, relative to `wai/`, that holds it),
its digest, and the path of each lowered graph, in §13.0's role order.
- `certificates/falsification.json` — the two sets of §14.3 that a certifier must
reject, in the same form, each with `claim`, the profile it is checked against:
`overflow_acc64`, whose accumulator passes i64 by one unit, and `claim_acc32`,
whose pin claims `acc32` and whose accumulator passes i32 by one unit, one weight
past a set that meets it. Their members are under `sets/`. A sink refuses both
under the profile they are checked against (§8.6).
- `certificates/external.json` — the sets whose certificates are here although the
sets are not: the CI-regenerated `wai.neural.int_hyper` set and the `jpegai/v1`
bundle of the models root. Each entry gives the set's members (the name in the set,
then the file the job that holds it reads), its digest, the profile its verdict
meets, and that job's workflow, which certifies the set and requires these bytes.
Here, `vectors verify` and the independent checker check only what they can
without the set: that each such certificate is canonical, names its set, and meets
its profile. The job that holds the set certifies it with both certifiers.
Files are compared as bytes; `.gitattributes` turns off line-ending conversion.
@ -57,9 +73,9 @@ In short:
line ending with one line feed.
- A graph vector's decodes list the graph's **data** inputs only; the decodes of one
vector are one sequence, and a refused decode changes no state (§10.3).
- `load` is the outcome of load steps 1–8 (§10.5); every vector here loads or is
refused at steps 1–7 (see below). A vector whose load is refused has no decodes and
lists both profiles; one that loads lists exactly the profiles its verdict meets.
- `load` is the outcome of load steps 1–8 (§10.5), under every profile the vector
lists. A vector whose load is refused, at any step, has no decodes and lists both
profiles; one that loads lists exactly the profiles its verdict meets.
- `at` is −1 for a refusal before any symbol or element, the symbol's index for
`rans64_decode` (the number of symbols for a refusal after the last one), and, for
`mans_decode`, the position whose step refuses, or, for a flush that belongs to no
@ -67,24 +83,26 @@ In short:
- A vector of `graphs/lower_<form>.jsonl` has the id `<file>/<set>/<role>/…`: its
graph is that role of the set `sets/<set>/` as lowered.
## What this corpus does not cover yet
## The vectors load step 8 decides
Load step 8 has no implementation in `wai-ops` yet: its bounds pass and verdict land
with the certifier. Until then the `uncertified` switch skips it, and the generator
writes no vector whose outcome depends on it; a generated case whose graph meets no
profile is dropped, and `vectors gen` reports the count. So these coverage classes
of §14.1 item 6, each a load refusal at step 8, are not here yet:
Whether a graph loads, and under which profiles, is the verdict of its bounds pass
(§8). Besides every other vector's `profiles`, these depend on it alone (§14.1 item
6), each in its operator's file, with an id that names the class:
- `sigmoid` and `elu` on an input range that reaches `−2^63`, refused at load (the
corpus has the decoding side, ranges that stop at `−(2^63 − 1)`);
- an `rq` whose `q` passes 64 bits while `y` fits, refused at load under `acc64`
(`conv`, `conv_transpose`, `rescale`, `chscale`, `mul`, `gram`, `apply`);
- a `lut16` whose input range passes `[−32767, 32767]`, and a `check` range that no
value of its producer can meet (§8.1);
- a graph refused at step 8 in `graphs/load.jsonl`.
The independent checker already runs step 8 and recomputes every vector's
`profiles` from its own bounds pass, so these vectors are checked the day they land.
- `sigmoid/step8/…`, `elu/step8/…`: an input range that reaches `−2^63`, refused at
load (`exp.negx` passes i64); the ranges that stop at `−(2^63 − 1)` decode;
- `<op>/step8_q/…`: an `rq` whose `q` passes 64 bits, refused under `acc64`; with a
`post` bias (`conv`, `conv_transpose`, `rescale`) `y` fits while `q` does not, and
beside each, the same graph one unit inside, which decodes;
- `l2norm/eps/…` and `layernorm_c`'s cases with `ε = 2^64 − 1`, which load under
`acc64` only;
- `<op>/acc32_param/…`: a graph that fails `acc32` by one parameter condition of
§7.2 alone (`rq.mult`, `rq.shift`, `rq.post_bias`, `weights`, `chscale.m`,
`chscale.s`, `igdn.gamma`, `igdn.shift`, `leaky.a`), every interval within i32;
- `lut16/step8/…`: an input range that reaches `±32768` (§5.6);
- `graphs/load.jsonl`, `load/step8/…`: a `check` range its producer cannot meet, one
that its producer meets at one value (loads, under `acc64` only), and an
accumulator past i64.
## The lowered sets
@ -96,18 +114,25 @@ lowering's latent channel count, which the initial latent fixes, is the entry's
## Generation and verification
cd wai/wai-ops
cargo run --release --features uncertified --bin wai-ops -- vectors gen ..
cd wai/wai-verify
cargo run --release --features cli --bin wai-verify -- vectors gen ..
writes every file here but this README and `.gitattributes`.
writes every file here but this README, `.gitattributes`, and `external.json` with
the certificates it lists.
cargo run --release --features uncertified --bin wai-ops -- vectors verify ..
cargo run --release --features cli --bin wai-verify -- vectors verify ..
recomputes every vector file from its inputs, re-lowers every set, and regenerates
the corpus, and requires every byte to equal the committed one. The same check runs
built for `wasm32-wasip2` under a WASI runtime, and, for the vector files,
`wai-ops/node/verify.mjs` runs the recomputation as a `wasm32-unknown-unknown`
module under a browser's engine.
recomputes every vector file from its inputs, re-lowers every set and recomputes its
certificate from the committed lowered graphs, checks that each falsification set
fails the profile it claims, and regenerates the corpus, and requires every byte to
equal the committed one. The same check runs built for `wasm32-wasip2` under a WASI
runtime, and, for the vector files, `wai-ops/node/verify.mjs` runs the recomputation
as a `wasm32-unknown-unknown` module under a browser's engine.
wai-verify bounds <set> --capability <name> [--profile acc32|acc64] [--check <cert>]
certifies one set (or `WHG1` graphs on their own), and exits 0 when its verdict meets
the profile, 1 when it does not, and 3 when `--check` finds a difference.
python3 -I tools/wai_ops_check.py ops-conformance
@ -117,4 +142,16 @@ decides, and the disagreement is a bug in one of them. It rewrites every vector
from its inputs, `load` (steps 1–8) and `profiles` from its own bounds pass and
verdict (§8.1–§8.5) included; re-lowers every set and requires each lowered graph,
and that each loads; and requires each lowering vector's graph to be the role its id
names. Every byte must equal the committed one.
names. It is also a second certifier (§1): from each set's lowered graphs it writes
the set's certificate (§8.5) and requires the committed bytes, for every set of
`index.json` and both falsification sets, each of which must fail the profile it
claims. Every byte must equal the committed one.
python3 -I tools/wai_ops_check.py ops-conformance --external <name>=<dir>
python3 -I tools/wai_ops_check.py certify --capability <name> --lowering <id> \
--member <name>=<path>... [--profile acc32|acc64] [--out <cert>] [--check <cert>]
certify an external set from the directory that holds its files, and one set (or
`WHG1` graphs on their own, with `--whg1 <role>=<path>`), with the exit statuses of
`wai-verify bounds`: 0, 1, 3 as above; 2 when a graph is malformed or refused at load;
4 when the lowering refuses the set; 5 for usage.

View file

@ -0,0 +1 @@
{"format":"wai-ops-external/1","sets":[{"capability":"wai.neural.int_hyper","lowering":"whm1/1","members":{"entropy.json":"cond_entropy.json","g_s.whm":"int_synth_sota_model.bin","h_s.whm":"int_hs_model.bin"},"name":"sota","note":"regenerated from pinned inputs by the job that certifies it; not committed","profile":"acc64","set_sha256":"f16fd09cd79b831d7d821e030c05e99e9fb5ea216574e699944ea4e55ac0a404","workflow":".github/workflows/wai-sota-vectors.yml"},{"capability":"wai.image.jpegai","lowering":"jpegai-int/1","members":{"norm.json":"norm.json","weights/hs_uv.json":"hs_uv.json","weights/hs_y.json":"hs_y.json","weights/hyp_uv.json":"hyp_uv.json","weights/hyp_y.json":"hyp_y.json","weights/icci.json":"icci.json","weights/mcm0_y.json":"mcm0_y.json","weights/mcm1_y.json":"mcm1_y.json","weights/mcm2_y.json":"mcm2_y.json","weights/mcm3_y.json":"mcm3_y.json","weights/synth_uv.json":"synth_uv.json","weights/synth_y.json":"synth_y.json"},"name":"jpegai/v1","note":"the models root's jpegai/v1, the files tools/jpegai-e2e-v1.sha256 pins; not committed","profile":"acc32","set_sha256":"3e243dc11566922f5cb7f4c628afd440b18d56a08b99b07cc33d5c8522aeec37","workflow":".github/workflows/wai-jpegai-e2e.yml"}]}

View file

@ -0,0 +1 @@
{"format":"wai-ops-falsification/1","sets":[{"capability":"wai.neural.int_synth","claim":"acc64","lowering":"int_synth/1","members":{"int_synth.json":"ops-conformance/sets/overflow_acc64/int_synth.json"},"name":"overflow_acc64","note":"fails acc64 by one unit (section 14.3)","roles":["ops-conformance/certificates/wai.neural.int_synth/b35460f39f31a559e97e66b8dde4597a309cbc44199c40097f935dd8e6e7305a/synth.whg1"],"set_sha256":"b35460f39f31a559e97e66b8dde4597a309cbc44199c40097f935dd8e6e7305a","synthetic":true},{"capability":"wai.neural.int_synth","claim":"acc32","lowering":"int_synth/1","members":{"int_synth.json":"ops-conformance/sets/claim_acc32/int_synth.json"},"name":"claim_acc32","note":"its pin claims acc32, which it fails by one weight (section 14.3)","roles":["ops-conformance/certificates/wai.neural.int_synth/76916c8f85da7663c09fba8cb091d628ed38328b680563285b8056cec82fd1ac/synth.whg1"],"set_sha256":"76916c8f85da7663c09fba8cb091d628ed38328b680563285b8056cec82fd1ac","synthetic":true}]}

View file

@ -0,0 +1 @@
{"capability":"wai.audio.int_codec","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"8f68d90b0e57279d06062705c1f46e3fffefa44c90f829f0219e9eba646a4c4e","inputs":[{"hi":"2882303761517117","kind":"data","lo":"-2882303761517117","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"g_s","nodes":[{"acc32_params":[],"fits":["acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"63","hi":"3963167672086035875","lo":"-3963167672086035875"},"convT.term":{"bits":"61","hi":"1152921504606846800","lo":"-1152921504606846800"},"rq.p":{"bits":"75","hi":"12198630094680818423250","lo":"-12198630094680818423250"},"rq.q":{"bits":"63","hi":"2978181175459184185","lo":"-2978181175459184186"},"rq.t":{"bits":"63","hi":"3963167672086035875","lo":"-3963167672086035875"},"rq.y":{"bits":"63","hi":"2978181175459184134","lo":"-2978181175459184237"}},"k_c":"0","mac":{"a_bits":["53"],"w_bits":"10"},"npu_flags":["rq_floor_mult","rq_post_bias"],"op":"conv_transpose","out":[{"bits":"38","hi":"121438811524","lo":"-121438811524","policy":"check","result":{"bits":"63","hi":"2978181175459184134","lo":"-2978181175459184237"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"1","intermediates":{"igdn.N":{"bits":"73","hi":"3027966488820337357051","lo":"0"},"igdn.gx2":{"bits":"85","hi":"12387803353263739970163840","lo":"0"},"igdn.r":{"bits":"37","hi":"55026961471","lo":"189"},"igdn.term":{"bits":"73","hi":"3024366053042905266153","lo":"0"},"igdn.x2":{"bits":"75","hi":"14747384944361595202576","lo":"0"},"igdn.xr":{"bits":"74","hi":"6682408802815178791804","lo":"-6682408802815178791804"}},"k_c":"0","npu_flags":[],"op":"igdn","out":[{"bits":"53","hi":"4013651887230102","lo":"-4013651887230102","policy":"check","result":{"bits":"64","hi":"6525789846499198038","lo":"-6525789846499198039"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"2","intermediates":{"convT.acc":{"bits":"64","hi":"5209720149624672396","lo":"-5209720149624672396"},"convT.term":{"bits":"62","hi":"1537228672809129066","lo":"-1537228672809129066"},"rq.p":{"bits":"72","hi":"1604593806084399097968","lo":"-1604593806084399097968"},"rq.q":{"bits":"60","hi":"391746534688573998","lo":"-391746534688573999"},"rq.t":{"bits":"64","hi":"5209720149624672396","lo":"-5209720149624672396"},"rq.y":{"bits":"60","hi":"391746534688574460","lo":"-391746534688573537"}},"k_c":"0","mac":{"a_bits":["53"],"w_bits":"10"},"npu_flags":["rq_floor_mult","rq_post_bias"],"op":"conv_transpose","out":[{"bits":"60","hi":"391746534688574460","lo":"-391746534688573537","policy":"derived","result":{"bits":"60","hi":"391746534688574460","lo":"-391746534688573537"},"tensor":"3"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"3","intermediates":{},"npu_flags":[],"op":"crop","out":[{"bits":"50","hi":"281483566907399","lo":"-281483566907400","policy":"check","result":{"bits":"60","hi":"391746534688574460","lo":"-391746534688573537"},"tensor":"4"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"4","intermediates":{"rq.p":{"bits":"64","hi":"9223372036854743545","lo":"-9223372036854775800"},"rq.q":{"bits":"54","hi":"9007199254740960","lo":"-9007199254740992"},"rq.t":{"bits":"50","hi":"281483566907399","lo":"-281483566907400"},"rq.y":{"bits":"54","hi":"9007199254740960","lo":"-9007199254740992"}},"npu_flags":[],"op":"rescale","out":[{"bits":"54","hi":"9007199254740960","lo":"-9007199254740992","policy":"derived","result":{"bits":"54","hi":"9007199254740960","lo":"-9007199254740992"},"tensor":"5"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"5","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"16","hi":"32767","lo":"-32768","policy":"derived","result":{"bits":"16","hi":"32767","lo":"-32768"},"tensor":"6"}],"saturated":false}],"scalars":[{"hi":"67108864","index":"0","lo":"1"}]}],"lowering":"wam1/1","set_sha256":"8eca5bfdc1d8098ccdcfb8e3a7280c0550c4120ea87f224f0d4269e8fd4d1896","verdict":{"acc32":false,"acc32_first_failure":{"bits":"53","graph":"g_s","index":"0","key":"range","where":"input"},"acc64":true,"npu":false,"npu_flags":["rq_floor_mult","rq_post_bias"],"npu_reasons":["acc32","crop_scalar","op:igdn"]}}

View file

@ -0,0 +1 @@
{"capability":"wai.neural.int_synth","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"e965a537999df89fd1d9aa3ca5a554a814d7c0aebc31d432af619355fa1d7d97","inputs":[{"hi":"127","kind":"data","lo":"-128","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"synth","nodes":[{"acc32_params":[],"fits":["acc32","acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"16","hi":"20902","lo":"-20730"},"convT.term":{"bits":"14","hi":"5120","lo":"-5120"},"rq.p":{"bits":"16","hi":"20902","lo":"-20730"},"rq.q":{"bits":"13","hi":"2612","lo":"-2592"},"rq.t":{"bits":"16","hi":"20902","lo":"-20730"},"rq.y":{"bits":"13","hi":"2612","lo":"-2592"}},"k_c":"419430","mac":{"a_bits":["8"],"w_bits":"7"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"13","hi":"2612","lo":"-2592","policy":"derived","result":{"bits":"13","hi":"2612","lo":"-2592"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"1","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"13","hi":"2612","lo":"0","policy":"derived","result":{"bits":"13","hi":"2612","lo":"0"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"2","intermediates":{"convT.acc":{"bits":"21","hi":"762704","lo":"-877680"},"convT.term":{"bits":"18","hi":"104480","lo":"-104480"},"rq.p":{"bits":"21","hi":"762704","lo":"-877680"},"rq.q":{"bits":"17","hi":"47669","lo":"-54855"},"rq.t":{"bits":"21","hi":"762704","lo":"-877680"},"rq.y":{"bits":"17","hi":"47669","lo":"-54855"}},"k_c":"20554","mac":{"a_bits":["13"],"w_bits":"7"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"17","hi":"47669","lo":"-54855","policy":"derived","result":{"bits":"17","hi":"47669","lo":"-54855"},"tensor":"3"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"3","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"4"}],"saturated":false}],"scalars":[]}],"lowering":"int_synth/1","set_sha256":"00c2274be6b49039365a16a357f41162095db65ed4166a4c70f5ddb97e16b4d1","verdict":{"acc32":true,"acc64":true,"npu":true,"npu_flags":[],"npu_reasons":[]}}

View file

@ -0,0 +1 @@
{"capability":"wai.neural.int_synth","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"04147b8ba5e5544c15466d9bfc66a813e6abdc58bd67314c7cbbb6cd38e9e324","inputs":[{"hi":"127","kind":"data","lo":"-128","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"synth","nodes":[{"acc32_params":[],"fits":["acc32","acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"16","hi":"22516","lo":"-22539"},"convT.term":{"bits":"13","hi":"2304","lo":"-2286"},"rq.p":{"bits":"16","hi":"22516","lo":"-22539"},"rq.q":{"bits":"12","hi":"1407","lo":"-1409"},"rq.t":{"bits":"16","hi":"22516","lo":"-22539"},"rq.y":{"bits":"12","hi":"1407","lo":"-1409"}},"k_c":"932067","mac":{"a_bits":["8"],"w_bits":"6"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"12","hi":"1407","lo":"-1409","policy":"derived","result":{"bits":"12","hi":"1407","lo":"-1409"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"1","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"12","hi":"1407","lo":"0","policy":"derived","result":{"bits":"12","hi":"1407","lo":"0"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"2","intermediates":{"convT.acc":{"bits":"20","hi":"40803","lo":"-304023"},"convT.term":{"bits":"16","hi":"8442","lo":"-25326"},"rq.p":{"bits":"20","hi":"40803","lo":"-304023"},"rq.q":{"bits":"15","hi":"1275","lo":"-9501"},"rq.t":{"bits":"20","hi":"40803","lo":"-304023"},"rq.y":{"bits":"15","hi":"1275","lo":"-9501"}},"k_c":"84793","mac":{"a_bits":["12"],"w_bits":"6"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"15","hi":"1275","lo":"-9501","policy":"derived","result":{"bits":"15","hi":"1275","lo":"-9501"},"tensor":"3"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"3","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"4"}],"saturated":false}],"scalars":[]}],"lowering":"int_synth/1","set_sha256":"1c4705d17c01f865b20d785191c7f63b5c18dcc8fce77e15852cf8240b7368be","verdict":{"acc32":true,"acc64":true,"npu":true,"npu_flags":[],"npu_reasons":[]}}

View file

@ -0,0 +1 @@
{"capability":"wai.neural.int_synth","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"0be18812f9593d58117b14b6882e6fa4038b2af4cd85e586292292dddf224ebd","inputs":[{"hi":"127","kind":"data","lo":"-128","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"synth","nodes":[{"acc32_params":[],"fits":["acc32","acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"17","hi":"35947","lo":"-35878"},"convT.term":{"bits":"13","hi":"2304","lo":"-2286"},"rq.p":{"bits":"17","hi":"35947","lo":"-35878"},"rq.q":{"bits":"11","hi":"561","lo":"-561"},"rq.t":{"bits":"17","hi":"35947","lo":"-35878"},"rq.y":{"bits":"11","hi":"561","lo":"-561"}},"k_c":"932067","mac":{"a_bits":["8"],"w_bits":"6"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"11","hi":"561","lo":"-561","policy":"derived","result":{"bits":"11","hi":"561","lo":"-561"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"1","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"2"}],"saturated":false}],"scalars":[]}],"lowering":"int_synth/1","set_sha256":"21f092964e1319eca7e494ba3f6794e3166886d50e9dd74d00935c5b317055a9","verdict":{"acc32":true,"acc64":true,"npu":true,"npu_flags":[],"npu_reasons":[]}}

View file

@ -0,0 +1 @@
{"capability":"wai.neural.int_synth","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"0424c3e9fc68733885cd528542317494c1debba38ce507b31d23590fcc70b102","inputs":[{"hi":"127","kind":"data","lo":"-128","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"synth","nodes":[{"acc32_params":[],"fits":["acc32","acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"22","hi":"1486634","lo":"-1485967"},"convT.term":{"bits":"15","hi":"16256","lo":"-16129"},"rq.p":{"bits":"22","hi":"1486634","lo":"-1485967"},"rq.q":{"bits":"13","hi":"2903","lo":"-2903"},"rq.t":{"bits":"22","hi":"1486634","lo":"-1485967"},"rq.y":{"bits":"13","hi":"2903","lo":"-2903"}},"k_c":"132104","mac":{"a_bits":["8"],"w_bits":"8"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"13","hi":"2903","lo":"-2903","policy":"derived","result":{"bits":"13","hi":"2903","lo":"-2903"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"1","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"13","hi":"2903","lo":"0","policy":"derived","result":{"bits":"13","hi":"2903","lo":"0"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"2","intermediates":{"convT.acc":{"bits":"25","hi":"6369182","lo":"-16550003"},"convT.term":{"bits":"20","hi":"223531","lo":"-368681"},"rq.p":{"bits":"25","hi":"6369182","lo":"-16550003"},"rq.q":{"bits":"20","hi":"199036","lo":"-517188"},"rq.t":{"bits":"25","hi":"6369182","lo":"-16550003"},"rq.y":{"bits":"20","hi":"199036","lo":"-517188"}},"k_c":"5824","mac":{"a_bits":["13"],"w_bits":"8"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"20","hi":"199036","lo":"-517188","policy":"derived","result":{"bits":"20","hi":"199036","lo":"-517188"},"tensor":"3"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"3","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"19","hi":"199036","lo":"0","policy":"derived","result":{"bits":"19","hi":"199036","lo":"0"},"tensor":"4"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"4","intermediates":{"convT.acc":{"bits":"30","hi":"189482281","lo":"-488832473"},"convT.term":{"bits":"26","hi":"6369152","lo":"-25277572"},"rq.p":{"bits":"30","hi":"189482281","lo":"-488832473"},"rq.q":{"bits":"26","hi":"11842642","lo":"-30552030"},"rq.t":{"bits":"30","hi":"189482281","lo":"-488832473"},"rq.y":{"bits":"26","hi":"11842642","lo":"-30552030"}},"k_c":"84","mac":{"a_bits":["19"],"w_bits":"8"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"26","hi":"11842642","lo":"-30552030","policy":"derived","result":{"bits":"26","hi":"11842642","lo":"-30552030"},"tensor":"5"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"5","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"25","hi":"11842642","lo":"0","policy":"derived","result":{"bits":"25","hi":"11842642","lo":"0"},"tensor":"6"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"6","intermediates":{"convT.acc":{"bits":"36","hi":"26172239136","lo":"-25165614250"},"convT.term":{"bits":"32","hi":"1373746472","lo":"-1504015534"},"rq.p":{"bits":"36","hi":"26172239136","lo":"-25165614250"},"rq.q":{"bits":"32","hi":"1635764946","lo":"-1572850891"},"rq.t":{"bits":"36","hi":"26172239136","lo":"-25165614250"},"rq.y":{"bits":"32","hi":"1635764946","lo":"-1572850891"}},"k_c":"1","mac":{"a_bits":["25"],"w_bits":"8"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"32","hi":"1635764946","lo":"-1572850891","policy":"derived","result":{"bits":"32","hi":"1635764946","lo":"-1572850891"},"tensor":"7"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"7","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"8"}],"saturated":false}],"scalars":[]}],"lowering":"int_synth/1","set_sha256":"50868e72f5f1599569c413ed1f67ab8610fa0b3c17919162cbf370f49275125c","verdict":{"acc32":false,"acc32_first_failure":{"bits":"36","graph":"synth","index":"6","key":"convT.acc","where":"node"},"acc64":true,"npu":false,"npu_flags":[],"npu_reasons":["acc32"]}}

View file

@ -0,0 +1 @@
{"capability":"wai.neural.int_synth","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"633c99de690fc890904ad9a73f3324eef8a197d7f98e6e9b6b37dc1d9f48f8d1","inputs":[{"hi":"127","kind":"data","lo":"-128","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"synth","nodes":[{"acc32_params":[],"fits":["acc32","acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"10","hi":"256","lo":"-128"},"convT.term":{"bits":"8","hi":"127","lo":"-128"},"rq.p":{"bits":"10","hi":"256","lo":"-128"},"rq.q":{"bits":"2","hi":"1","lo":"-1"},"rq.t":{"bits":"10","hi":"256","lo":"-128"},"rq.y":{"bits":"2","hi":"1","lo":"-1"}},"k_c":"16777215","mac":{"a_bits":["8"],"w_bits":"2"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"2","hi":"1","lo":"-1","policy":"derived","result":{"bits":"2","hi":"1","lo":"-1"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"1","intermediates":{"convT.acc":{"bits":"33","hi":"2147483648","lo":"-2147483648"},"convT.term":{"bits":"32","hi":"1073741824","lo":"-1073741824"},"rq.p":{"bits":"33","hi":"2147483648","lo":"-2147483648"},"rq.q":{"bits":"33","hi":"2147483648","lo":"-2147483648"},"rq.t":{"bits":"33","hi":"2147483648","lo":"-2147483648"},"rq.y":{"bits":"33","hi":"2147483648","lo":"-2147483648"}},"k_c":"1","mac":{"a_bits":["2"],"w_bits":"32"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"33","hi":"2147483648","lo":"-2147483648","policy":"derived","result":{"bits":"33","hi":"2147483648","lo":"-2147483648"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"2","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"3"}],"saturated":false}],"scalars":[]}],"lowering":"int_synth/1","set_sha256":"76916c8f85da7663c09fba8cb091d628ed38328b680563285b8056cec82fd1ac","verdict":{"acc32":false,"acc32_first_failure":{"bits":"33","graph":"synth","index":"1","key":"convT.acc","where":"node"},"acc64":true,"npu":false,"npu_flags":[],"npu_reasons":["acc32"]}}

View file

@ -0,0 +1 @@
{"capability":"wai.neural.int_synth","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"98d845410a6af26ba1fbaa4b4c8b9cf1416e3036442fc8cc1f8499f765a4d0ea","inputs":[{"hi":"127","kind":"data","lo":"-128","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"synth","nodes":[{"acc32_params":[],"fits":["acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"37","hi":"42128471623","lo":"-42460191872"},"convT.term":{"bits":"37","hi":"42128471623","lo":"-42460191872"},"rq.p":{"bits":"37","hi":"42128471623","lo":"-42460191872"},"rq.q":{"bits":"37","hi":"42128471623","lo":"-42460191872"},"rq.t":{"bits":"37","hi":"42128471623","lo":"-42460191872"},"rq.y":{"bits":"37","hi":"42128471623","lo":"-42460191872"}},"k_c":"0","mac":{"a_bits":["8"],"w_bits":"30"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"37","hi":"42128471623","lo":"-42460191872","policy":"derived","result":{"bits":"37","hi":"42128471623","lo":"-42460191872"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"1","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"37","hi":"42128471623","lo":"0","policy":"derived","result":{"bits":"37","hi":"42128471623","lo":"0"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":[],"index":"2","intermediates":{"convT.acc":{"bits":"65","hi":"9223372036854775808","lo":"0"},"convT.term":{"bits":"64","hi":"9223372036854775807","lo":"0"},"rq.p":{"bits":"65","hi":"9223372036854775808","lo":"0"},"rq.q":{"bits":"65","hi":"9223372036854775808","lo":"0"},"rq.t":{"bits":"65","hi":"9223372036854775808","lo":"0"},"rq.y":{"bits":"65","hi":"9223372036854775808","lo":"0"}},"k_c":"0","mac":{"a_bits":["37"],"w_bits":"29"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"65","hi":"9223372036854775808","lo":"0","policy":"derived","result":{"bits":"65","hi":"9223372036854775808","lo":"0"},"tensor":"3"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"3","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"4"}],"saturated":false}],"scalars":[]}],"lowering":"int_synth/1","set_sha256":"b35460f39f31a559e97e66b8dde4597a309cbc44199c40097f935dd8e6e7305a","verdict":{"acc32":false,"acc32_first_failure":{"bits":"37","graph":"synth","index":"0","key":"convT.acc","where":"node"},"acc64":false,"acc64_first_failure":{"bits":"65","graph":"synth","index":"2","key":"convT.acc","where":"node"},"npu":false,"npu_flags":[],"npu_reasons":["acc32"]}}

View file

@ -0,0 +1 @@
{"capability":"wai.neural.int_synth","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"293aee268c980f56cb13fd77816693aa8fdef0e3c5fc4955a4f81e433a02e105","inputs":[{"hi":"127","kind":"data","lo":"-128","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"synth","nodes":[{"acc32_params":[],"fits":["acc32","acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"16","hi":"16895","lo":"-16956"},"convT.term":{"bits":"13","hi":"2304","lo":"-2286"},"rq.p":{"bits":"16","hi":"16895","lo":"-16956"},"rq.q":{"bits":"12","hi":"1055","lo":"-1060"},"rq.t":{"bits":"16","hi":"16895","lo":"-16956"},"rq.y":{"bits":"12","hi":"1055","lo":"-1060"}},"k_c":"932067","mac":{"a_bits":["8"],"w_bits":"6"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"12","hi":"1055","lo":"-1060","policy":"derived","result":{"bits":"12","hi":"1055","lo":"-1060"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"1","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"12","hi":"1055","lo":"0","policy":"derived","result":{"bits":"12","hi":"1055","lo":"0"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"2","intermediates":{"convT.acc":{"bits":"20","hi":"41145","lo":"-310318"},"convT.term":{"bits":"16","hi":"6330","lo":"-18990"},"rq.p":{"bits":"20","hi":"41145","lo":"-310318"},"rq.q":{"bits":"15","hi":"1285","lo":"-9698"},"rq.t":{"bits":"20","hi":"41145","lo":"-310318"},"rq.y":{"bits":"15","hi":"1285","lo":"-9698"}},"k_c":"113084","mac":{"a_bits":["12"],"w_bits":"6"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"15","hi":"1285","lo":"-9698","policy":"derived","result":{"bits":"15","hi":"1285","lo":"-9698"},"tensor":"3"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"3","intermediates":{},"npu_flags":[],"op":"relu","out":[{"bits":"12","hi":"1285","lo":"0","policy":"derived","result":{"bits":"12","hi":"1285","lo":"0"},"tensor":"4"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"4","intermediates":{"convT.acc":{"bits":"20","hi":"51400","lo":"-514100"},"convT.term":{"bits":"16","hi":"7710","lo":"-23130"},"rq.p":{"bits":"20","hi":"51400","lo":"-514100"},"rq.q":{"bits":"16","hi":"3212","lo":"-32132"},"rq.t":{"bits":"20","hi":"51400","lo":"-514100"},"rq.y":{"bits":"16","hi":"3212","lo":"-32132"}},"k_c":"92844","mac":{"a_bits":["12"],"w_bits":"6"},"npu_flags":[],"op":"conv_transpose","out":[{"bits":"16","hi":"3212","lo":"-32132","policy":"derived","result":{"bits":"16","hi":"3212","lo":"-32132"},"tensor":"5"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"5","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"6"}],"saturated":false}],"scalars":[]}],"lowering":"int_synth/1","set_sha256":"c6a362696d61605931ef4da98ae630362dcd73ea6be7864db6e61d6c5abbcd24","verdict":{"acc32":true,"acc64":true,"npu":true,"npu_flags":[],"npu_reasons":[]}}

View file

@ -0,0 +1 @@
{"capability":"wai.splat.int_codec","certificate":"wai-ops/1 bounds","graphs":[{"graph_sha256":"7082d056e453e9f814e0a7a62b317815364e911c358abbae96ef6d7dc648f070","inputs":[{"hi":"5632249656115","kind":"data","lo":"-5632249656115","max_h":"0","max_w":"0","policy":"check","tensor":"0"}],"name":"g_s","nodes":[{"acc32_params":[],"fits":["acc64"],"index":"0","intermediates":{"convT.acc":{"bits":"61","hi":"884032273774154285","lo":"-884032273774154285"},"convT.term":{"bits":"55","hi":"11529215046067405","lo":"-11529215046067405"},"rq.p":{"bits":"70","hi":"305677959136398172000","lo":"-305677959136398172000"},"rq.q":{"bits":"46","hi":"18219826169991","lo":"-18219826169992"},"rq.t":{"bits":"61","hi":"884032273774154285","lo":"-884032273774154285"},"rq.y":{"bits":"46","hi":"18219826170500","lo":"-18219826169483"}},"k_c":"0","mac":{"a_bits":["44"],"w_bits":"12"},"npu_flags":["rq_floor_mult","rq_post_bias"],"op":"conv_transpose","out":[{"bits":"38","hi":"89166253876","lo":"-89166253876","policy":"check","result":{"bits":"46","hi":"18219826170500","lo":"-18219826169483"},"tensor":"1"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"1","intermediates":{"igdn.N":{"bits":"79","hi":"164859282006515137208244","lo":"0"},"igdn.gx2":{"bits":"90","hi":"367644657812944418765929616","lo":"0"},"igdn.r":{"bits":"40","hi":"406028671409","lo":"4470"},"igdn.term":{"bits":"74","hi":"5609812283522711467985","lo":"0"},"igdn.x2":{"bits":"74","hi":"7950620830279285023376","lo":"0"},"igdn.xr":{"bits":"76","hi":"36204055595789876631284","lo":"-36204055595789876631284"}},"k_c":"0","npu_flags":[],"op":"igdn","out":[{"bits":"44","hi":"5632249656115","lo":"-5632249656115","policy":"check","result":{"bits":"64","hi":"8838880760690887849","lo":"-8838880760690887850"},"tensor":"2"}],"saturated":false},{"acc32_params":[],"fits":["acc64"],"index":"2","intermediates":{"convT.acc":{"bits":"61","hi":"805805958300373050","lo":"-805805958300373050"},"convT.term":{"bits":"55","hi":"11529215046067405","lo":"-11529215046067405"},"rq.p":{"bits":"70","hi":"404028584243961107750","lo":"-404028584243961107750"},"rq.q":{"bits":"46","hi":"24081980242965","lo":"-24081980242966"},"rq.t":{"bits":"61","hi":"805805958300373050","lo":"-805805958300373050"},"rq.y":{"bits":"46","hi":"24081980243505","lo":"-24081980242426"}},"k_c":"0","mac":{"a_bits":["44"],"w_bits":"12"},"npu_flags":["rq_floor_mult","rq_post_bias"],"op":"conv_transpose","out":[{"bits":"46","hi":"24081980243505","lo":"-24081980242426","policy":"derived","result":{"bits":"46","hi":"24081980243505","lo":"-24081980242426"},"tensor":"3"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"3","intermediates":{},"npu_flags":[],"op":"clamp","out":[{"bits":"14","hi":"4096","lo":"0","policy":"derived","result":{"bits":"14","hi":"4096","lo":"0"},"tensor":"4"}],"saturated":false},{"acc32_params":[],"fits":["acc32","acc64"],"index":"4","intermediates":{"rq.p":{"bits":"21","hi":"1046528","lo":"0"},"rq.q":{"bits":"9","hi":"255","lo":"0"},"rq.t":{"bits":"14","hi":"4096","lo":"0"},"rq.y":{"bits":"9","hi":"255","lo":"0"}},"npu_flags":[],"op":"rescale","out":[{"bits":"9","hi":"255","lo":"0","policy":"derived","result":{"bits":"9","hi":"255","lo":"0"},"tensor":"5"}],"saturated":false}],"scalars":[]}],"lowering":"whm1/1","set_sha256":"033a37c9d29d7ea9675a0b4a3e269d74f2ab874b5a3b1425eec69bf3bad8b26f","verdict":{"acc32":false,"acc32_first_failure":{"bits":"44","graph":"g_s","index":"0","key":"range","where":"input"},"acc64":true,"npu":false,"npu_flags":["rq_floor_mult","rq_post_bias"],"npu_reasons":["acc32","op:igdn"]}}

View file

@ -58,3 +58,6 @@
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000f7ffffffffffffff090000000000000002000000800000000000000000000000000000000000000018010100000000000100000024000000010000000000000002000000000000000100ffff00000000000000000000000000000000","id":"load/step5/57","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-1","0","-1","1"],"shape":["2","1","2"]}]},"inputs":[{"data":["-9","9","3","-4"],"shape":["2","1","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000f7ffffffffffffff0900000000000000020000008000000000000000000000000000000000000000180101000000000001000000280000000100000000000000040000000000000001000000ffffffff00000000000000000000000000000000","id":"load/step5/58","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"refuse":"geometry_mismatch"},"inputs":[{"data":["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"],"shape":["1","4","4"]}],"scalars":["1"]},{"expect":{"refuse":"geometry_mismatch"},"inputs":[{"data":["2","2","2","2","2","2","2","2","2","2","2","2","2","2","2","2"],"shape":["1","4","4"]}],"scalars":["3"]}],"graph":"5748473101010000020000000100000001000100010000000000000400000000010000000000000003000000000000000000000004000000040000000000000000000000000000000000000000000000010000000100000080020000f7ffffffffffffff0900000000000000010000008000000000000000000000000000000000000000140101000000000001000000200000000100000000000000000000000000000000000000040000000000000001000000","id":"load/crop_plan/59","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"5748473101010000030000000200000001000100010000000000000400000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000020000000200000080020000f7ffffffffffffff09000000000000000300000080000000000000000000000000000000000000000300000080020000f7ffffffffffffffffffffffffffffff010101000000000001000000330000000300000001000000010000000100000001000000000000000000000001000000010000000000000100000000010102030405060c010100010000000200000000000000","id":"load/step8/60","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"refuse":"field_range"},"inputs":[{"data":["-9","9","3","-3"],"shape":["2","1","2"]}],"scalars":["0"]},{"expect":{"outputs":[{"data":["0","0","0","0","0","0"],"shape":["3","1","2"]}]},"inputs":[{"data":["0","-1","0","0"],"shape":["2","1","2"]}],"scalars":["0"]}],"graph":"5748473101010000030000000200000001000100010000000000000400000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000020000000200000080020000f7ffffffffffffff09000000000000000300000080000000000000000000000000000000000000000300000080020000f7ffffffffffffff0000000000000000010101000000000001000000330000000300000001000000010000000100000001000000000000000000000001000000010000000000000100000000010102030405060c010100010000000200000000000000","id":"load/step8/61","load":"ok","profiles":["acc64"]}
{"decodes":[],"graph":"57484731010100000300000002000000010001000100000000000004000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000200000002000000800200000000000000000080ffffffffffffff7f030000008000000000000000000000000000000000000000030000008000000000000000000000000000000000000000010101000000000001000000330000000300000001000000010000000100000001000000000000000000000001000000010000000000000100000000010101010101010c010100010000000200000000000000","id":"load/step8/62","load":"model_mismatch","profiles":["acc32","acc64"]}

View file

@ -0,0 +1 @@
{"layers":[{"in_ch":1,"out_ch":2,"k":1,"stride":1,"pad":0,"weight":[1,1],"bias":[129,129],"shift":8},{"in_ch":2,"out_ch":3,"k":1,"stride":1,"pad":0,"weight":[1073741824,0,0,1073741824,0,0],"bias":[0,0,0],"shift":0}]}

View file

@ -0,0 +1 @@
{"layers":[{"in_ch":1,"out_ch":1,"k":1,"stride":1,"pad":0,"weight":[331720249],"bias":[0],"shift":0,"relu":true},{"in_ch":1,"out_ch":3,"k":1,"stride":1,"pad":0,"weight":[218934409,0,0],"bias":[1,0,0],"shift":0}]}

View file

@ -62,3 +62,6 @@
{"decodes":[{"expect":{"outputs":[{"data":["-22","0","23","9","-9","15"],"shape":["2","1","3"]}]},"inputs":[{"data":["-9","0","9","3","-3","5"],"shape":["2","1","3"]}],"scalars":["2"]}],"graph":"5748473101010000020000000100000001000100010000000000000400000000010000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000f7ffffffffffffff09000000000000000200000080000000000000000000000000000000000000000301010000000000010000002c0000000000000004000000010401000100000002000000030000000400000005000000060000000700000008000000","id":"chscale/scalar/61","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"refuse":"field_range"},"inputs":[{"data":["-9","0","9","3","-3","5"],"shape":["2","1","3"]}],"scalars":["0"]}],"graph":"5748473101010000020000000100000001000100010000000000000400000000010000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000f7ffffffffffffff09000000000000000200000080000000000000000000000000000000000000000301010000000000010000002c0000000000000004000000010401000100000002000000030000000400000005000000060000000700000008000000","id":"chscale/scalar/62","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"refuse":"field_range"},"inputs":[{"data":["-9","0","9","3","-3","5"],"shape":["2","1","3"]}],"scalars":["3"]}],"graph":"5748473101010000020000000100000001000100010000000000000400000000010000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000f7ffffffffffffff09000000000000000200000080000000000000000000000000000000000000000301010000000000010000002c0000000000000004000000010401000100000002000000030000000400000005000000060000000700000008000000","id":"chscale/scalar/63","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"5748473101010000020000000100000001000100010000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000004000000000000000400100000080000000000000000000000000000000000000000301010000000000010000001000000000000000010000000004000002000000","id":"chscale/step8_q/64","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-1","0"],"shape":["1","1","2"]}]},"inputs":[{"data":["-1","0"],"shape":["1","1","2"]}],"scalars":["0"]}],"graph":"5748473101010000020000000100000001000100010000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000ffffffffffffffff000000000000000001000000800000000000000000000000000000000000000003010100000000000100000014000000000000000100000000081f000000008000000000","id":"chscale/acc32_param/65","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","0"],"shape":["1","1","2"]}]},"inputs":[{"data":["0","5"],"shape":["1","1","2"]}],"scalars":["0"]}],"graph":"5748473101010000020000000100000001000100010000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000000005000000000000000100000080000000000000000000000000000000000000000301010000000000010000001000000000000000010000000004400001000000","id":"chscale/acc32_param/66","load":"ok","profiles":["acc64"]}

View file

@ -120,3 +120,6 @@
{"decodes":[{"expect":{"refuse":"field_range"},"inputs":[{"data":["-4","-4"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000f6ffffffffffffff14000000000000000100000080020000faffffffffffffff1e000000000000000101010000000000010000002e00000001000000010000000100000001000000010000000000000000000000010000000100000000000001000000000002","id":"conv/check/119","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"refuse":"field_range"},"inputs":[{"data":["16","16"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000f6ffffffffffffff14000000000000000100000080020000faffffffffffffff1e000000000000000101010000000000010000002e00000001000000010000000100000001000000010000000000000000000000010000000100000000000001000000000002","id":"conv/check/120","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-6","-6","27","30"],"shape":["1","1","4"]}]},"inputs":[{"data":["-100","-3","9","100"],"shape":["1","1","4"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080010000f6ffffffffffffff14000000000000000100000080010000faffffffffffffff1e000000000000000101010000000000010000002e00000001000000010000000100000001000000010000000000000000000000010000000100000000000001000000000003","id":"conv/check/121","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000002000000000000000200100000080000000000000000000000000000000000000000101010000000000010000003600000001000000010000000100000001000000010000000000000000000000010000000100000000020101040400000400000000ffffffff01","id":"conv/step8_q/122","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-1","9223372036854775803"],"shape":["1","1","2"]}]},"inputs":[{"data":["0","2305843009213693951"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000000000000000000ffffffffffffff1f0100000080000000000000000000000000000000000000000101010000000000010000003600000001000000010000000100000001000000010000000000000000000000010000000100000000020101040400000400000000ffffffff01","id":"conv/step8_q/123","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-2147483648","0"],"shape":["1","1","2"]}]},"inputs":[{"data":["-1","0"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000ffffffffffffffff0000000000000000010000008000000000000000000000000000000000000000010101000000000001000000350000000100000001000000010000000100000001000000000000000000000001000000080000000000000100000000000000008000000000","id":"conv/acc32_param/124","load":"ok","profiles":["acc64"]}

View file

@ -111,3 +111,6 @@
{"decodes":[{"expect":{"outputs":[{"data":["-49","128","-194","-110","67","1","-121","47","122","137","-170","269","-26","31","13","-230","98","70","229","-62","-5","7","-140","-20","-125","-73","19","98","1","-7","-2","-14","5","73","152","-95","-34","7","104","-107","125","-142","-73","-152","20","-98","-95","146","-121","-13","-173","280","-158","43","-14","49","-23","71","227","2","-11","-26","37","41","-40","-10","-2","-47","46","-53","28","-2","-22","-17","-29","62","-37","55","-90","70","-188","90","-4","-110","59","-67","110","-154","136","2","-58","82","-96","4","-127","86","81","-56","10","0","-58","-5","-73","-103","-13","14","98","-37","108","-37","81","94","2","-15","-185","62","5","79","114","-13","-200","4","-67","-201","-1","-43","35","-153","139","6","-40","-26","-46","42","-129","86","86","-37","81","-16","7","13","-51","18","-51","23","12","16","21","-43","6","-63","12","1"],"shape":["2","11","7"]}]},"inputs":[{"data":["-20","-15","-10","-5","0","5","10","15","20","-16","-11","-6","-1","4","9","14","19","-17","-12","-7","-2","3","8","13","18","-18","-13","-8","-3","2"],"shape":["2","5","3"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000ecffffffffffffff14000000000000000200000080000000000000000000000000000000000000001101010000000000010000008100000002000000040000000400000002000000020000000100000001000000010000000100000001000000010202010404000003000000feffffff0105000000fbfffffffbfe0104fcff0205fd0003fbfe0104fcff0205fd0003fbfe0104fcff0205fd0003fbfe0104fcff0205fd0003fbfe0104fcff0205fd0003fbfe0104fcff0205fd","id":"conv_transpose/geometry/110","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"refuse":"geometry_mismatch"},"inputs":[{"data":["-20","-15"],"shape":["2","1","1"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000ecffffffffffffff14000000000000000200000080000000000000000000000000000000000000001101010000000000010000006500000002000000030000000300000001000000010000000200000002000000000000000000000001000000010202010404000003000000feffffff0105000000fbfffffffbfe0104fcff0205fd0003fbfe0104fcff0205fd0003fbfe0104fcff0205fd0003fbfe01","id":"conv_transpose/geometry/111","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-25","20","133","-32","43","80","-40","65","28","-47","-2","95","-60","30","60","-45","40","25","-30","50","-10","-95"],"shape":["2","1","11"]}]},"inputs":[{"data":["-20","-15","-10","-5","0","5","10","15"],"shape":["2","1","4"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000200000080020000ecffffffffffffff14000000000000000200000080000000000000000000000000000000000000001101010000000000010000005500000002000000010000000500000001000000030000000000000002000000000000000100000001000000010202010404000003000000feffffff0105000000fbfffffffbfe0104fcff0205fd0003fbfe0104fcff0205fd","id":"conv_transpose/geometry/112","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000002000000000000000200100000080000000000000000000000000000000000000001101010000000000010000003a0000000100000001000000010000000100000001000000000000000000000000000000000000000100000000020101040400000400000000ffffffff01","id":"conv_transpose/step8_q/113","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-1","9223372036854775803"],"shape":["1","1","2"]}]},"inputs":[{"data":["0","2305843009213693951"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000000000000000000ffffffffffffff1f0100000080000000000000000000000000000000000000001101010000000000010000003a0000000100000001000000010000000100000001000000000000000000000000000000000000000100000000020101040400000400000000ffffffff01","id":"conv_transpose/step8_q/114","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-2147483648","0"],"shape":["1","1","2"]}]},"inputs":[{"data":["-1","0"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000ffffffffffffffff000000000000000001000000800000000000000000000000000000000000000011010100000000000100000039000000010000000100000001000000010000000100000000000000000000000000000000000000080000000000000100000000000000008000000000","id":"conv_transpose/acc32_param/115","load":"ok","profiles":["acc64"]}

View file

@ -33,3 +33,5 @@
{"decodes":[{"expect":{"outputs":[{"data":["-1","-1","1","1","1","9223372036854775807"],"shape":["1","1","6"]}]},"inputs":[{"data":["0","-1","1","-2861838","-9223372036854775807","9223372036854775807"],"shape":["1","1","6"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000100000000000080ffffffffffffff7f0100000080000000000000000000000000000000000000001c010100000000000100000008000000ffffffffffffffff","id":"elu/zero/32","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","0","1","-3855","-3855","9223372036854775807"],"shape":["1","1","6"]}]},"inputs":[{"data":["0","-1","1","-2861838","-9223372036854775807","9223372036854775807"],"shape":["1","1","6"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000100000000000080ffffffffffffff7f0100000080000000000000000000000000000000000000001c0101000000000001000000080000000f0f000000000000","id":"elu/zero/33","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-2","-1","1","3856","3856","9223372036854775807"],"shape":["1","1","6"]}]},"inputs":[{"data":["0","-1","1","-2861838","-9223372036854775807","9223372036854775807"],"shape":["1","1","6"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000100000000000080ffffffffffffff7f0100000080000000000000000000000000000000000000001c010100000000000100000008000000f0f0ffffffffffff","id":"elu/zero/34","load":"ok","profiles":["acc64"]}
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000008000000000000000000100000080000000000000000000000000000000000000001c0101000000000001000000080000000000010000000000","id":"elu/step8/35","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000008000000000000000000100000080000000000000000000000000000000000000001c0101000000000001000000080000000100000000000000","id":"elu/step8/36","load":"model_mismatch","profiles":["acc32","acc64"]}

View file

@ -64,3 +64,5 @@
{"decodes":[{"expect":{"outputs":[{"data":["-32766","0","32765"],"shape":["1","1","3"]}]},"inputs":[{"data":["-46340","0","46340"],"shape":["1","1","3"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000fc4affffffffffff04b50000000000000100000080000000000000000000000000000000000000001201010000000000010000001c0000001f000000100000000400000000000000ffffff7f0000000000000000","id":"igdn/edge/63","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-70368744144896","0","70368744144896"],"shape":["1","1","3"]}]},"inputs":[{"data":["-2147483648","0","2147483648"],"shape":["1","1","3"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000000000080ffffffff00000080000000000100000080000000000000000000000000000000000000001201010000000000010000001c0000001f000000100000000400000000000000ffffff7f0000000000000000","id":"igdn/edge/64","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-70368744079361","0","70368744079360"],"shape":["1","1","3"]}]},"inputs":[{"data":["-2147483647","0","2147483647"],"shape":["1","1","3"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000001000080ffffffffffffff7f000000000100000080000000000000000000000000000000000000001201010000000000010000001c0000001f000000100000000400000000000000ffffff7f0000000000000000","id":"igdn/edge/65","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","1"],"shape":["1","1","2"]}]},"inputs":[{"data":["0","1"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000000000000000000000100000000000000010000008000000000000000000000000000000000000000120101000000000001000000200000001f00000000000000080000000000000000000080000000000100000000000000","id":"igdn/acc32_param/66","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","1"],"shape":["1","1","2"]}]},"inputs":[{"data":["0","1"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000000001000000000000000100000080000000000000000000000000000000000000001201010000000000010000001c00000040000000000000000400000000000000010000000100000000000000","id":"igdn/acc32_param/67","load":"ok","profiles":["acc64"]}

View file

@ -39,3 +39,4 @@
{"decodes":[{"expect":{"outputs":[{"data":["37632","-37632","-37632","45824","-45824","7288","35894","-43477","32796"],"shape":["3","1","3"]}]},"inputs":[{"data":["16777216","-16777216","-16777216","16777216","-16777216","2668493","12188653","-14763103","11136542"],"shape":["3","1","3"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000004000000040000000000000000000000000000000000000000000000010000000300000080020000000000ffffffffff0000000100000000030000008000000000000000000000000000000000000000190101000000000001000000080000000000004000000000","id":"l2norm/random/38","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-600","-182"],"shape":["1","1","2"]}]},"inputs":[{"data":["-300","-91"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000002000000030000000000000000000000000000000000000000000000010000000100000080020000d4feffffffffffff2c01000000000000010000008000000000000000000000000000000000000000190101000000000001000000080000000000004000000000","id":"l2norm/random/39","load":"ok","profiles":["acc64"]}
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000fbffffffffffffff0500000000000000010000008000000000000000000000000000000000000000190101000000000001000000080000000100000000000000","id":"l2norm/sizes/40","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-1","0","0","0"],"shape":["1","2","2"]}]},"inputs":[{"data":["-5","5","1","0"],"shape":["1","2","2"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000002000000020000000000000000000000000000000000000000000000010000000100000080020000fbffffffffffffff050000000000000001000000800000000000000000000000000000000000000019010100000000000100000008000000ffffffffffffffff","id":"l2norm/eps/41","load":"ok","profiles":["acc64"]}

View file

@ -53,3 +53,4 @@
{"decodes":[{"expect":{"outputs":[{"data":["-1","-1","-1","-1","0","1000"],"shape":["1","1","6"]}]},"inputs":[{"data":["-1000","-7","-5","-1","0","1000"],"shape":["1","1","6"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000018fcffffffffffffe8030000000000000100000080000000000000000000000000000000000000000d01010000000000010000000c00000001000000000000003f000000","id":"leaky/floor/52","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-160","-2","-1","-1","0","1000"],"shape":["1","1","6"]}]},"inputs":[{"data":["-1000","-7","-5","-1","0","1000"],"shape":["1","1","6"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000018fcffffffffffffe8030000000000000100000080000000000000000000000000000000000000000d01010000000000010000000c0000008f020000000000000c000000","id":"leaky/floor/53","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000018fcffffffffffffe8030000000000000100000080000000000000000000000000000000000000000d01010000000000010000000c000000010000000000000040000000","id":"leaky/floor/54","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-1","0","5"],"shape":["1","1","3"]}]},"inputs":[{"data":["-1","0","5"],"shape":["1","1","3"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000ffffffffffffffff05000000000000000100000080000000000000000000000000000000000000000d01010000000000010000000c00000000000080000000001f000000","id":"leaky/acc32_param/55","load":"ok","profiles":["acc64"]}

File diff suppressed because one or more lines are too long

View file

@ -73,3 +73,4 @@
{"decodes":[{"expect":{"outputs":[{"data":["-2147441940","-2147441940"],"shape":["1","1","2"]}]},"inputs":[{"data":["-46340","46340"],"shape":["1","1","2"]},{"data":["46341","-46341"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"574847310101000003000000010000000200010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000100000080020000fc4affffffffffff04b50000000000000100000080020000fb4affffffffffff05b50000000000000100000080000000000000000000000000000000000000001002010000000000010000000200000009000000000000010000000000","id":"mul/edge32/72","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"refuse":"geometry_mismatch"},"inputs":[{"data":["3","3","3","3","3","3","3","3","3","3","3","3"],"shape":["2","2","3"]},{"data":["-3","-3","-3","-3","-3","-3"],"shape":["1","3","2"]}],"scalars":[]}],"graph":"574847310101000003000000010000000200010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000200000080020000fbffffffffffffff05000000000000000100000080020000fbffffffffffffff05000000000000000200000080000000000000000000000000000000000000001002010000000000010000000200000009000000010000010000000001","id":"mul/geometry/73","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-7","-4","-1","2","5","8","-7","-4","-1","2","5","8"],"shape":["2","2","3"]}]},"inputs":[{"data":["3","3","3","3","3","3","3","3","3","3","3","3"],"shape":["2","2","3"]},{"data":["-5","-3","-1","1","3","5"],"shape":["1","2","3"]}],"scalars":[]}],"graph":"574847310101000003000000010000000200010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000200000080020000fbffffffffffffff05000000000000000100000080020000fbffffffffffffff05000000000000000200000080000000000000000000000000000000000000001002010000000000010000000200000009000000010000010000000001","id":"mul/geometry/74","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"57484731010100000300000001000000020001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000010000008002000000000080000000000000008000000000010000008002000000000080000000000000008000000000010000008000000000000000000000000000000000000000100201000000000001000000020000000d00000000000101040000000200000000","id":"mul/step8_q/75","load":"model_mismatch","profiles":["acc32","acc64"]}

View file

@ -160,3 +160,8 @@
{"decodes":[{"expect":{"outputs":[{"data":["-2147483647","2147483646","-1","0","12345"],"shape":["1","1","5"]}]},"inputs":[{"data":["-2147483648","2147483647","-1","0","12345"],"shape":["1","1","5"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000000000080ffffffffffffff7f00000000010000008000000000000000000000000000000000000000020101000000000001000000110000000102010104040000ffffff7f1f00000000","id":"rescale/product32/159","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-2147483647","2147483646","-1","0","12345"],"shape":["1","1","5"]}]},"inputs":[{"data":["-2147483648","2147483647","-1","0","12345"],"shape":["1","1","5"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000000000080ffffffffffffff7f00000000010000008000000000000000000000000000000000000000020101000000000001000000110000000202010104040000ffffff7f1f00000000","id":"rescale/product32/160","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["-1","1","1","3","3","5","5","7"],"shape":["1","1","8"]}]},"inputs":[{"data":["-9","-5","-3","-1","1","3","5","9"],"shape":["1","1","8"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000f7ffffffffffffff09000000000000000100000080000000000000000000000000000000000000000201010000000000010000000d00000002020001000400000103000000","id":"rescale/rounding/161","load":"ok","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000002000000000000000200100000080000000000000000000000000000000000000000201010000000000010000001100000000020101040400000400000000ffffffff","id":"rescale/step8_q/162","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["9223372036854775803"],"shape":["1","1","1"]}]},"inputs":[{"data":["2305843009213693951"],"shape":["1","1","1"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000ffffffffffffff1fffffffffffffff1f0100000080000000000000000000000000000000000000000201010000000000010000001100000000020101040400000400000000ffffffff","id":"rescale/step8_q/163","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","1","2","3"],"shape":["1","1","4"]}]},"inputs":[{"data":["0","1","2","3"],"shape":["1","1","4"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000000000000000000030000000000000001000000800000000000000000000000000000000000000002010100000000000100000011000000000001010800000000000080000000001f","id":"rescale/acc32_param/164","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","0"],"shape":["1","1","2"]}]},"inputs":[{"data":["0","3"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000000000000000000030000000000000001000000800000000000000000000000000000000000000002010100000000000100000009000000000000010000000040","id":"rescale/acc32_param/165","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","2147483647"],"shape":["1","1","2"]}]},"inputs":[{"data":["-2147483648","-1"],"shape":["1","1","2"]}],"scalars":[]}],"graph":"5748473101010000020000000100000001000100000000000000000400000000000000000000000000000000000000000000000000000000000000000000000001000000010000008002000000000080ffffffffffffffffffffffff010000008000000000000000000000000000000000000000020101000000000001000000110000000002000100080000000000008000000000","id":"rescale/acc32_param/166","load":"ok","profiles":["acc64"]}

View file

@ -29,3 +29,5 @@
{"decodes":[{"expect":{"outputs":[{"data":["0","0","65536","0","65536","0","0","65536","65536","65430","32763","38651","65536","65536","0","65536","65536","65536","0","65536","60015","0","65536","0"],"shape":["2","3","4"]}]},"inputs":[{"data":["-792729","-1048576","826445","-1048576","1048576","-1048576","-1048576","1048576","1048576","421031","0","23772","1048576","1048576","-1048576","1048576","981941","1048576","-1048576","1048576","156422","-1048576","1048576","-1048576"],"shape":["2","3","4"]}],"scalars":[]},{"expect":{"outputs":[{"data":["46483","32763","65529","65536","36","65536","32763","65536","64892","58536","10","0"],"shape":["2","3","2"]}]},"inputs":[{"data":["58597","0","598851","1048576","-489302","1048576","0","1048576","302116","139273","-566856","-788917"],"shape":["2","3","2"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000070000000400000000000000000000000000000000000000000000000100000002000000800200000000f0ffffffffff00001000000000000200000080000000000000000000000000000000000000001b010100000000000100000000000000","id":"sigmoid/random/28","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["32699","32717","32763","32763","32825","32763","32836","32836","32836","32763","32699","32724","32836","32798","32836","32836","32780","32808","32740","32699","32763","32699","32828","32836"],"shape":["3","2","4"]}]},"inputs":[{"data":["-300","-225","0","0","256","-37","300","300","300","0","-300","-197","300","141","300","300","69","183","-130","-300","0","-300","268","300"],"shape":["3","2","4"]}],"scalars":[]}],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000300000080020000d4feffffffffffff2c010000000000000300000080000000000000000000000000000000000000001b010100000000000100000000000000","id":"sigmoid/random/29","load":"ok","profiles":["acc64"]}
{"decodes":[{"expect":{"outputs":[{"data":["0","0","0","32771","32763","32764","65536","65536","65536"],"shape":["1","1","9"]}]},"inputs":[{"data":["-9223372036854775807","-2861838","-2816412","-1","0","1","2816412","2861838","9223372036854775807"],"shape":["1","1","9"]}],"scalars":[]}],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000100000000000080ffffffffffffff7f0100000080000000000000000000000000000000000000001b010100000000000100000000000000","id":"sigmoid/edge/30","load":"ok","profiles":["acc64"]}
{"decodes":[],"graph":"57484731010100000200000001000000010001000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000010000000100000080020000000000000000008000000000000000000100000080000000000000000000000000000000000000001b010100000000000100000000000000","id":"sigmoid/step8/31","load":"model_mismatch","profiles":["acc32","acc64"]}
{"decodes":[],"graph":"574847310101000002000000010000000100010000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000100000001000000800200000000000000000080ffffffffffffff7f0100000080000000000000000000000000000000000000001b010100000000000100000000000000","id":"sigmoid/step8/32","load":"model_mismatch","profiles":["acc32","acc64"]}

View file

@ -15,15 +15,31 @@ extension's text (and, for the source forms it lowers, integer-payloads), it:
lowerings of section 13, and requires each lowered graph's bytes, each set's
digest (SPEC 3.1), and that every lowered graph loads; and requires the graph
of each graphs/lower_*.jsonl vector to be the role of the set its id names, as
re-lowered.
re-lowered;
- is a certifier (section 1): from each set's lowered graphs it computes the
bounds certificate (the pass of sections 8.1-8.3, the classes and parameter
conditions of section 7, the flags and reasons of section 9.2, and the canonical
writer of section 8.5), and requires the bytes of the committed certificate,
for every set of index.json and the two falsification sets of section 14.3,
each of which must fail the profile it claims. The sets of external.json are
certified too when their files are given (--external NAME=DIR); otherwise their
certificates are checked for what needs no set.
The bounds pass decides the verdict only. It does not write the certificate of
section 8.5, which the certifier half of this checker will.
python3 -I tools/wai_ops_check.py ops-conformance # check
python3 -I tools/wai_ops_check.py ops-conformance --write DIR # also write the files
python3 -I tools/wai_ops_check.py ops-conformance # check
python3 -I tools/wai_ops_check.py ops-conformance --write DIR # also write the files
python3 -I tools/wai_ops_check.py ops-conformance --external jpegai/v1=DIR ...
Exit status 1 on any difference or failure.
python3 -I tools/wai_ops_check.py certify --capability CAP --lowering ID \\
[--c N] --member NAME=PATH ... [--profile P] [--out FILE] [--check FILE]
python3 -I tools/wai_ops_check.py certify --whg1 ROLE=PATH ... [--profile P] ...
certifies one set, or WHG1 graphs on their own, writing the certificate to FILE or
to stdout. Exit status: 0 the verdict meets the profile (acc64 by default); 1 it
does not, and the certificate is still written; 2 a graph is malformed or refused
at load; 3 --check finds a difference, and the first differing path is printed with
both values; 4 the lowering refuses the set; 5 usage.
"""
import hashlib
import json
@ -566,6 +582,16 @@ def load(b):
"""Load steps 1-8 of section 10.5. Step 8 is the bounds pass and the verdict
(section 8): a graph whose verdict meets neither profile is refused, so a graph
that loads meets acc64 at least, and g["verdict"] says whether it meets acc32."""
g = load_graph(b)
if not g["verdict"]["acc64"]:
bad()
return g
def load_graph(b):
"""Load steps 1-7, then step 8's bounds pass with the refusals the pass makes
(lut16's range, an empty `check` interval), but not the profile against the
verdict: what a certifier needs to write a certificate whatever the verdict."""
g = parse_whg1(b)
if g["inputs"][0]["max_h"] == 0:
if any(op["name"] in ("gram", "l2norm", "softmax") for op in g["ops"]):
@ -580,9 +606,8 @@ def load(b):
hw[i["tensor"]] = (i["max_h"], i["max_w"])
walk(g, hw, None, True)
g["sizes"] = hw
g["verdict"] = bounds(g)
if not g["verdict"]["acc64"]:
bad()
g["pass"] = bounds_pass(g)
g["verdict"] = verdict_of([("", g)])
return g
@ -610,10 +635,6 @@ CLASS = {
}
def fits_bits(iv, n):
return -(1 << (n - 1)) <= iv[0] and iv[1] <= (1 << (n - 1)) - 1
def hull(ivs):
return (min(v[0] for v in ivs), max(v[1] for v in ivs))
@ -907,63 +928,219 @@ def node_bounds(nb, g, op, iv):
return [x]
def params32(op):
"""The parameter conditions of section 7.2: whether the node meets every one."""
PROFILES = ("acc32", "acc64")
POLICY = ("derived", "clamp", "check")
_RQ = ["rq.t", "rq.p", "rq.q", "rq.y"]
# Table 8.5-A: the intermediates each operator records, in definition order. Every
# other operator records none.
RECORDS = {
"conv": ["conv.term", "conv.acc"] + _RQ,
"conv_transpose": ["convT.term", "convT.acc"] + _RQ,
"rescale": _RQ,
"chscale": _RQ,
"mul": ["mul.p"] + _RQ,
"gram": ["gram.term", "gram.acc"] + _RQ,
"apply": ["apply.term", "apply.acc"] + _RQ,
"leaky": ["leaky.p"],
"igdn": ["igdn.x2", "igdn.gx2", "igdn.term", "igdn.N", "igdn.r", "igdn.xr"],
"layernorm_c": ["ln.S", "ln.m", "ln.d", "ln.T", "ln.v", "rsq.num", "ln.k", "ln.dk", "ln.n", "ln.ng"],
"l2norm": ["l2.T", "rsq.num", "l2.k", "l2.vk"],
"softmax": ["sm.diff", "exp.negx", "exp.nl", "exp.t2", "exp.c", "sm.sum", "sm.num"],
"sigmoid": ["exp.negx", "exp.nl", "exp.t2", "exp.c", "sig.num"],
"elu": ["exp.negx", "exp.nl", "exp.t2", "exp.c", "elu.p"],
}
# Section 8.5, k_c: the operators that record it, and the term interval of section 6
# it is taken over.
KC_TERM = {"conv": "conv.term", "conv_transpose": "convT.term", "igdn": "igdn.term", "gram": "gram.term", "apply": "apply.term"}
# Section 8.5, mac: the operators that record it; w_bits for the first three only.
MAC = ("conv", "conv_transpose", "chscale", "mul", "gram", "apply")
# Section 5's table: the operators marked "no" in the wai-ops/1-npu column.
NPU_NO = ("igdn", "layernorm_c", "l2norm", "softmax", "sigmoid", "elu", "gram", "apply")
def bits(iv):
"""bits([lo, hi]) of section 2: the smallest N >= 1 such that both fit iN."""
return max((v if v >= 0 else ~v).bit_length() + 1 for v in iv)
def acc32_params(op):
"""The parameter conditions of section 7.2 the node fails, as their keys, in that
table's order."""
n, P = op["name"], op["p"]
f32 = lambda v: I32_MIN <= v <= I32_MAX
ok = True
keys = []
rq = P.get("rq")
if rq is not None:
ok = all(map(f32, rq["mult"])) and all(s <= 63 for s in rq["shift"]) and (rq["bias"] != 2 or all(map(f32, rq["biasv"])))
if n in ("conv", "conv_transpose"):
ok = ok and all(map(f32, P["w"]))
elif n == "chscale":
ok = all(map(f32, P["M"])) and P["shift"] <= 63
elif n == "igdn":
ok = all(map(f32, P["gamma"])) and P["gf"] <= 63 and P["frac"] <= 63
elif n == "leaky":
ok = f32(P["a"])
return ok
if not all(map(f32, rq["mult"])):
keys.append("rq.mult")
if not all(s <= 63 for s in rq["shift"]):
keys.append("rq.shift")
if rq["bias"] == 2 and not all(map(f32, rq["biasv"])):
keys.append("rq.post_bias")
if n in ("conv", "conv_transpose") and not all(map(f32, P["w"])):
keys.append("weights")
if n == "chscale":
if not all(map(f32, P["M"])):
keys.append("chscale.m")
if P["shift"] > 63:
keys.append("chscale.s")
if n == "igdn":
if not all(map(f32, P["gamma"])):
keys.append("igdn.gamma")
if P["gf"] > 63 or P["frac"] > 63:
keys.append("igdn.shift")
if n == "leaky" and not f32(P["a"]):
keys.append("leaky.a")
return keys
def bounds(g):
"""The bounds pass (sections 8.1-8.4) and the verdict (section 8.5) of a graph
that passed load steps 1-7: {"acc32": bool, "acc64": bool}. Refuses
(model_mismatch) where the pass itself refuses the graph at load: a `check`
interval that is empty (section 8.1), or lut16's input range (section 5.6)."""
def node_npu(g, op):
"""The node's flags of section 9.2, in ascending order, and its reasons of section
8.5 other than "acc32"."""
n, P = op["name"], op["p"]
flags, reasons = set(), set()
if n == "lut16":
flags.add("lut16")
if n in ("add", "chunk_add"):
state = {i["tensor"] for i in g["inputs"] if i["kind"] == 1}
if any(t in state for t in op["ins"]):
flags.add("state_add")
rq = None
if n == "chscale":
flags.add("chscale")
# A chscale always has a multiplier, M[r][c], and no bias.
rq = (P["round"], True, P["M"], [P["shift"]], False)
elif "rq" in P:
q = P["rq"]
rq = (q["round"], q["m_kind"] != 0, q["mult"], q["shift"], q["bias"] == 2)
if rq is not None:
rnd, has_mult, mults, shifts, post = rq
if rnd == 2:
flags.add("rq_half_even")
if rnd == 0 and has_mult:
flags.add("rq_floor_mult")
if post:
flags.add("rq_post_bias")
if any(m < 0 for m in mults):
flags.add("rq_neg_mult")
if any(not 0 <= s <= 31 for s in shifts):
flags.add("rq_shift_range")
if n in NPU_NO:
reasons.add("op:" + n)
if n == "conv":
cin, G = g["tensors"][op["ins"][0]]["c"], P["groups"]
if (G != 1 and G != cin) or (G == cin and G > 1 and P["cout"] != cin):
reasons.add("conv_groups")
if n == "crop" and any(o[0] == "scalar" for o in P["ops"]):
reasons.add("crop_scalar")
return sorted(flags), reasons
def width_failures(nd, p):
"""The node's recorded intermediates and producer results (`out.<k>`) that do not
meet profile p's width for their class (section 7.1), as (bits, key)."""
W = WIDTH[p]
f = [(bits(r), k) for k, r in nd["rec"].items() if bits(r) > W[CLASS[k]]]
f += [(bits(o["result"]), "out.%d" % j) for j, o in enumerate(nd["outs"]) if bits(o["result"]) > W["tensor"]]
return f
def bounds_pass(g):
"""The bounds pass of sections 8.1-8.3 over a graph that passed load steps 1-7,
in graph order. Refuses (model_mismatch) where the pass refuses the graph at
load: a `check` interval that is empty (section 8.1), or lut16's input range
(section 5.6). Returns one record per node: its recorded intermediates, whether
it saturated, its input tensors' intervals, each output's producer result and
interval after its policy, its acc32_params, its npu flags and reasons, and the
profiles it fits (section 8.5)."""
T = g["tensors"]
ok = {"acc32": True, "acc64": True}
iv = {}
declared = [(T[i["tensor"]]["lo"], T[i["tensor"]]["hi"]) for i in g["inputs"]] + list(g["scalars"])
for r in declared:
for p in ok:
ok[p] = ok[p] and fits_bits(r, WIDTH[p]["tensor"])
for i in g["inputs"]:
iv[i["tensor"]] = (T[i["tensor"]]["lo"], T[i["tensor"]]["hi"])
for op in g["ops"]:
iv = {i["tensor"]: (T[i["tensor"]]["lo"], T[i["tensor"]]["hi"]) for i in g["inputs"]}
nodes = []
for k, op in enumerate(g["ops"]):
nb = Node()
ins = [iv[t] for t in op["ins"]]
results = node_bounds(nb, g, op, iv)
fit = {p: not nb.saturated for p in ok}
for p in ok:
for k, r in nb.rec.items():
fit[p] = fit[p] and fits_bits(r, WIDTH[p][CLASS[k]])
for r in results:
fit[p] = fit[p] and fits_bits(r, WIDTH[p]["tensor"])
fit["acc32"] = fit["acc32"] and params32(op)
for t, (a, b) in zip(op["outs"], results):
if sorted(nb.rec) != sorted(RECORDS.get(op["name"], [])):
raise AssertionError("%s records %s" % (op["name"], sorted(nb.rec)))
outs = []
for t, r in zip(op["outs"], results):
rec = T[t]
if rec["policy"] == 0:
iv[t] = (a, b)
ti = r
elif rec["policy"] == 1:
iv[t] = (min(max(a, rec["lo"]), rec["hi"]), min(max(b, rec["lo"]), rec["hi"]))
ti = (min(max(r[0], rec["lo"]), rec["hi"]), min(max(r[1], rec["lo"]), rec["hi"]))
else:
fit["acc32"] = False
iv[t] = (max(a, rec["lo"]), min(b, rec["hi"]))
if iv[t][0] > iv[t][1]:
ti = (max(r[0], rec["lo"]), min(r[1], rec["hi"]))
if ti[0] > ti[1]:
bad()
for p in ok:
ok[p] = ok[p] and fit[p]
return ok
iv[t] = ti
outs.append({"tensor": t, "iv": ti, "result": r, "policy": rec["policy"]})
flags, reasons = node_npu(g, op)
nd = {"index": k, "op": op, "rec": nb.rec, "saturated": nb.saturated, "ins": ins, "outs": outs,
"acc32_params": acc32_params(op), "flags": flags, "reasons": reasons}
nd["fits"] = {}
for p in PROFILES:
fit = not nd["saturated"] and not width_failures(nd, p)
if p == "acc32":
fit = fit and not nd["acc32_params"] and all(o["policy"] != 2 for o in outs)
nd["fits"][p] = fit
nodes.append(nd)
return nodes
def node_failure(nd, p, name):
"""The failure of section 8.5 for a node that does not fit profile p."""
f = {"graph": name, "index": nd["index"], "where": "node"}
if nd["saturated"]:
f["key"] = "saturated"
return f
w = width_failures(nd, p)
if w:
f["bits"], f["key"] = min(w, key=lambda e: (-e[0], e[1].encode()))
else:
f["key"] = nd["acc32_params"][0] if nd["acc32_params"] else "policy.check"
return f
def verdict_of(graphs):
"""The verdict of section 8.5 over [(name, g)], each g carrying its pass."""
v = {}
for p in PROFILES:
tw = WIDTH[p]["tensor"]
fail = None
for name, g in graphs:
T = g["tensors"]
for k, i in enumerate(g["inputs"]):
r = (T[i["tensor"]]["lo"], T[i["tensor"]]["hi"])
if bits(r) > tw:
fail = {"bits": bits(r), "graph": name, "index": k, "key": "range", "where": "input"}
break
if fail is None:
for k, r in enumerate(g["scalars"]):
if bits(r) > tw:
fail = {"bits": bits(r), "graph": name, "index": k, "key": "range", "where": "scalar"}
break
if fail is None:
for nd in g["pass"]:
if not nd["fits"][p]:
fail = node_failure(nd, p, name)
break
if fail is not None:
break
v[p] = fail is None
if fail is not None:
v[p + "_first_failure"] = fail
reasons, flags = set(), set()
if not v["acc32"]:
reasons.add("acc32")
for _, g in graphs:
for nd in g["pass"]:
reasons |= nd["reasons"]
flags |= set(nd["flags"])
v["npu_reasons"] = sorted(reasons, key=lambda s: s.encode())
v["npu"] = not reasons
v["npu_flags"] = sorted(flags, key=lambda s: s.encode())
return v
# --------------------------------------------------------------- the operators (section 5)
@ -1262,8 +1439,8 @@ def run(g, data_inputs, scalars, state):
for op in g["ops"]:
outs = ev(g, op, vals, hw, scalars)
for t, ten in zip(op["outs"], outs):
# A value outside i64 is one load step 8 excludes; before the certifier
# exists, such a decode is refused.
# A value outside i64 is one load step 8 excludes; should one be formed
# all the same, the decode is refused rather than carried on.
if any(v is None or not I64_MIN <= v <= I64_MAX for v in ten.d):
raise Refuse("model_mismatch")
if not apply_policy(g["tensors"][t], ten):
@ -1347,6 +1524,136 @@ def graph_line(v):
return out
# --------------------------------------------------------------- the certificate (8.5)
def ivj(iv):
return {"bits": bits(iv), "hi": iv[1], "lo": iv[0]}
def node_json(nd):
op = nd["op"]
n, P = op["name"], op["p"]
d = {
"acc32_params": nd["acc32_params"],
"fits": [p for p in PROFILES if nd["fits"][p]],
"index": nd["index"],
"intermediates": {k: ivj(nd["rec"][k]) for k in RECORDS.get(n, [])},
"npu_flags": nd["flags"],
"op": n,
"out": [{"bits": bits(o["iv"]), "hi": o["iv"][1], "lo": o["iv"][0], "policy": POLICY[o["policy"]],
"result": ivj(o["result"]), "tensor": o["tensor"]} for o in nd["outs"]],
"saturated": nd["saturated"],
}
if n in KC_TERM:
t = nd["rec"][KC_TERM[n]]
m = max(abs(t[0]), abs(t[1]))
d["k_c"] = "unbounded" if m == 0 else I32_MAX // m
if n in MAC:
d["mac"] = {"a_bits": [bits(x) for x in nd["ins"]]}
if n in ("conv", "conv_transpose", "chscale"):
w = P["M"] if n == "chscale" else P["w"]
d["mac"]["w_bits"] = bits((min(w), max(w)))
return d
def graph_json(name, b, g):
T = g["tensors"]
inputs = []
for i in g["inputs"]:
rec = T[i["tensor"]]
e = {"hi": rec["hi"], "kind": "state" if i["kind"] else "data", "lo": rec["lo"], "max_h": i["max_h"],
"max_w": i["max_w"], "policy": POLICY[rec["policy"]], "tensor": i["tensor"]}
if i["kind"]:
e["init"], e["src"] = i["init"], i["src"]
inputs.append(e)
return {
"graph_sha256": hashlib.sha256(b).hexdigest(),
"inputs": inputs,
"name": name,
"nodes": [node_json(nd) for nd in g["pass"]],
"scalars": [{"hi": hi, "index": k, "lo": lo} for k, (lo, hi) in enumerate(g["scalars"])],
}
def canonical(v):
"""The canonical JSON of section 8.5, as bytes. Every string is printable ASCII;
a role or capability name outside that alphabet cannot be certified."""
def strings(x):
if isinstance(x, str):
yield x
elif isinstance(x, list):
for y in x:
yield from strings(y)
elif isinstance(x, dict):
for k, y in x.items():
yield k
yield from strings(y)
for s in strings(v):
if any(not 0x20 <= ord(ch) <= 0x7E for ch in s):
raise ValueError("a string outside printable ASCII cannot be certified: %r" % s)
return cw(v).encode("ascii")
def certificate(capability, lowering, set_sha256, roles):
"""The bounds certificate of section 8.5 for the graphs [(role, WHG1 bytes)], in
the order given: (its canonical bytes, its verdict). Each graph passes load steps
1-7 and the bounds pass, or the Refuse of the step that fails propagates."""
gs = [(name, b, load_graph(b)) for name, b in roles]
v = verdict_of([(name, g) for name, _, g in gs])
cert = {
"capability": capability,
"certificate": "wai-ops/1 bounds",
"graphs": [graph_json(name, b, g) for name, b, g in gs],
"lowering": lowering,
"set_sha256": set_sha256,
"verdict": v,
}
return canonical(cert), v
ABSENT = "(absent)"
def json_diffs(a, b, path="", out=None, limit=None):
"""The JSON paths at which two parsed values differ, in document order, each
with both values: [(path, a's value, b's value)]."""
out = [] if out is None else out
if limit is not None and len(out) >= limit:
return out
if isinstance(a, dict) and isinstance(b, dict):
for k in sorted(set(a) | set(b), key=lambda s: s.encode()):
p = path + ("." + k if k.isidentifier() else '["%s"]' % k)
if k not in a or k not in b:
out.append((p, a.get(k, ABSENT), b.get(k, ABSENT)))
else:
json_diffs(a[k], b[k], p, out, limit)
elif isinstance(a, list) and isinstance(b, list):
for k in range(max(len(a), len(b))):
p = "%s[%d]" % (path, k)
if k >= len(a) or k >= len(b):
out.append((p, a[k] if k < len(a) else ABSENT, b[k] if k < len(b) else ABSENT))
else:
json_diffs(a[k], b[k], p, out, limit)
elif a != b:
out.append((path or "$", a, b))
return out[:limit] if limit is not None else out
def first_difference(got, want):
"""Part of what a `--check` mismatch prints: the first differing JSON path, with
both values."""
try:
a, b = json.loads(got), json.loads(want)
except ValueError:
return "the committed file is not JSON"
d = json_diffs(a, b, limit=1)
if not d:
return "the same JSON, written differently"
p, x, y = d[0]
return "first difference at %s: computed %s, committed %s" % (p, cw(x) if x is not ABSENT else x, cw(y) if y is not ABSENT else y)
# --------------------------------------------------------------- stage operators (5E)
@ -2231,30 +2538,44 @@ def jpegai_lower(norm_b, hy_b, huv_b):
def lower_set(entry, files):
"""The set's lowered graphs, [(role, WHG1 bytes)] in section 13.0's role order.
Raises LowerRefuse when the lowering refuses the set."""
low, cap = entry["lowering"], entry["capability"]
try:
if low == "int_synth/1":
roles = int_synth_lower(files["int_synth.json"])
elif low == "whm1/1" and cap == "wai.splat.int_codec":
roles = [("g_s", whm1_lower(files["g_s.whm"], True))]
elif low == "whm1/1":
roles = [("h_s", whm1_lower(files["h_s.whm"], False)), ("g_s", whm1_lower(files["g_s.whm"], True))]
elif low == "wam1/1":
roles = [("g_s", wam1_lower(files["model.wam"]))]
elif low == "wmb1/1":
roles = wmb1_lower(files["model.wmb"])
elif low == "world/1":
roles = world_lower(files["T.whm"], files["g_s.whm"], dec(entry["c"]))
elif low == "jpegai-int/1":
roles = jpegai_lower(files["norm.json"], files["weights/hs_y.json"], files["weights/hs_uv.json"])
else:
raise ValueError("no lowering %r" % low)
except LowerRefuse:
raise ValueError("the lowering refused the set")
# Every lowered graph loads.
for _, b in roles:
load(b)
return roles
if low == "int_synth/1":
return int_synth_lower(files["int_synth.json"])
if low == "whm1/1" and cap == "wai.splat.int_codec":
return [("g_s", whm1_lower(files["g_s.whm"], True))]
if low == "whm1/1":
return [("h_s", whm1_lower(files["h_s.whm"], False)), ("g_s", whm1_lower(files["g_s.whm"], True))]
if low == "wam1/1":
return [("g_s", wam1_lower(files["model.wam"]))]
if low == "wmb1/1":
return wmb1_lower(files["model.wmb"])
if low == "world/1":
return world_lower(files["T.whm"], files["g_s.whm"], dec(entry["c"]))
if low == "jpegai-int/1":
return jpegai_lower(files["norm.json"], files["weights/hs_y.json"], files["weights/hs_uv.json"])
raise ValueError("no lowering %r" % low)
def cert_path(root, entry):
"""Section 14.3: certificates/<capability>/<set digest>.json."""
return os.path.join(root, "certificates", entry["capability"], entry["set_sha256"] + ".json")
def check_set_certificate(root, entry, roles, write):
"""Recomputes a set's certificate from its lowered graphs and requires the bytes
of the committed one. Returns the verdict."""
got, v = certificate(entry["capability"], entry["lowering"], entry["set_sha256"], roles)
path = cert_path(root, entry)
if write:
p = os.path.join(write, os.path.relpath(path, root))
os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, "wb").write(got)
want = open(path, "rb").read()
if got != want:
raise ValueError("the certificate differs: %s" % first_difference(got, want))
return v
def set_digest(files):
@ -2267,7 +2588,7 @@ def set_digest(files):
# --------------------------------------------------------------- main
def check(root, write):
def check(root, write, external=None):
wai = os.path.dirname(os.path.abspath(root))
failures = 0
nfiles = nvec = 0
@ -2297,15 +2618,24 @@ def check(root, write):
k = next((k for k in range(min(len(a), len(b))) if a[k] != b[k]), min(len(a), len(b)))
print("DIFF %s/%s: line %d differs" % (sub, name, k + 1))
print("rewrote %d vector files, %d vectors" % (nfiles, nvec))
idx = json.load(open(os.path.join(root, "certificates", "index.json")))
nsets = ngraphs = 0
nsets = ngraphs = ncerts = 0
lowered = {}
for e in idx["sets"]:
# The sets index.json lists, then the falsification sets of section 14.3. Each is
# re-lowered, its lowered graphs compared, and its certificate recomputed from
# them and compared. An index set's graphs each load (steps 1-8), so its verdict
# meets acc64; a falsification set's verdict fails the profile it claims.
listed = [(e, None) for e in json.load(open(os.path.join(root, "certificates", "index.json")))["sets"]]
listed += [(e, e["claim"]) for e in json.load(open(os.path.join(root, "certificates", "falsification.json")))["sets"]]
for e, claim in listed:
what = "falsification set" if claim else "set"
try:
files = {m: open(os.path.join(wai, p), "rb").read() for m, p in e["members"].items()}
if set_digest(files) != e["set_sha256"]:
raise ValueError("the set's digest differs")
roles = lower_set(e, files)
try:
roles = lower_set(e, files)
except LowerRefuse:
raise ValueError("the lowering refused the set")
if len(roles) != len(e["roles"]):
raise ValueError("the roles differ")
for (role, b), path in zip(roles, e["roles"]):
@ -2317,15 +2647,66 @@ def check(root, write):
open(p, "wb").write(b)
if open(os.path.join(wai, path), "rb").read() != b:
raise ValueError("%s: the lowered graph differs" % role)
if e.get("synthetic"):
if e.get("synthetic") and not claim:
lowered[(FORM_FILE[e["lowering"]], e["name"], role)] = b
ngraphs += 1
nsets += 1
v = check_set_certificate(root, e, roles, write)
ncerts += 1
if claim is None and not v["acc64"]:
raise ValueError("a lowered graph does not load: the verdict fails acc64")
if claim is not None and v[claim]:
raise ValueError("its verdict meets %s, the profile it must fail" % claim)
except Refuse as ex:
failures += 1
print("FAIL %s %s (%s): a lowered graph is refused at load: %s" % (what, e.get("name"), e.get("capability"), ex.code))
except Exception as ex:
failures += 1
print("FAIL lowering %s (%s): %s" % (e.get("name"), e.get("capability"), ex))
print("re-lowered %d sets, %d graphs" % (nsets, ngraphs))
print("FAIL %s %s (%s): %s" % (what, e.get("name"), e.get("capability"), ex))
print("re-lowered %d sets, %d graphs; recomputed %d certificates byte for byte" % (nsets, ngraphs, ncerts))
failures += check_lower_vectors(root, lowered)
failures += check_external(root, external or {}, write)
return failures
def check_external(root, external, write):
"""The sets of external.json, whose certificates are committed although the sets
are not. With the set's files (`external` maps an entry's name to the directory
that holds them), the certificate is recomputed and compared. Without them, only
what needs no set: the certificate is canonical, names its set, its capability
and its lowering, and its verdict meets the profile the entry gives."""
failures = n_full = n_part = 0
for e in json.load(open(os.path.join(root, "certificates", "external.json")))["sets"]:
try:
want = open(cert_path(root, e), "rb").read()
d = e["name"] in external
if d:
files = {m: open(os.path.join(external[e["name"]], f), "rb").read() for m, f in e["members"].items()}
if set_digest(files) != e["set_sha256"]:
raise ValueError("the set's digest differs")
try:
roles = lower_set(e, files)
except LowerRefuse:
raise ValueError("the lowering refused the set")
check_set_certificate(root, e, roles, write)
c = json.loads(want)
if canonical(c) != want:
raise ValueError("the certificate is not canonical")
if (c["set_sha256"], c["capability"], c["lowering"]) != (e["set_sha256"], e["capability"], e["lowering"]):
raise ValueError("the certificate names another set")
if c["verdict"][e["profile"]] is not True:
raise ValueError("its verdict does not meet %s" % e["profile"])
if d:
n_full += 1
else:
n_part += 1
except Refuse as ex:
failures += 1
print("FAIL external set %s: a lowered graph is refused at load: %s" % (e["name"], ex.code))
except Exception as ex:
failures += 1
print("FAIL external set %s: %s" % (e["name"], ex))
print("external sets: %d certificates recomputed byte for byte, %d checked without their set" % (n_full, n_part))
return failures
@ -2364,16 +2745,99 @@ def check_lower_vectors(root, lowered):
return failures
def certify_main(args):
"""`certify`: one set, or WHG1 graphs on their own, to a certificate (section 8.5),
with the exit status of the bounds tool: 0 the verdict meets the profile; 1 it
does not (the certificate is still written); 2 a graph is malformed or refused at
load; 3 `--check` finds a difference; 4 the lowering refuses the set; 5 usage."""
opts = {"--capability": None, "--lowering": None, "--c": None, "--profile": "acc64", "--out": None, "--check": None}
members, graphs = [], []
k = 0
while k < len(args):
a = args[k]
if k + 1 >= len(args) or (a not in opts and a not in ("--member", "--whg1")):
print(CERTIFY_USAGE, file=sys.stderr)
return 5
v = args[k + 1]
if a in ("--member", "--whg1"):
if "=" not in v:
print(CERTIFY_USAGE, file=sys.stderr)
return 5
(members if a == "--member" else graphs).append(tuple(v.split("=", 1)))
else:
opts[a] = v
k += 2
if bool(members) == bool(graphs) or opts["--profile"] not in PROFILES or (members and not (opts["--capability"] and opts["--lowering"])):
print(CERTIFY_USAGE, file=sys.stderr)
return 5
try:
if graphs:
roles = [(name, open(path, "rb").read()) for name, path in graphs]
capability, lowering, digest = opts["--capability"], "whg1", None
else:
files = {name: open(path, "rb").read() for name, path in members}
capability, lowering, digest = opts["--capability"], opts["--lowering"], set_digest(files)
entry = {"capability": capability, "lowering": lowering, "c": opts["--c"], "set_sha256": digest}
try:
roles = lower_set(entry, files)
except (LowerRefuse, KeyError, ValueError) as ex:
print("the lowering refused the set%s" % (": %s" % ex if str(ex) else ""), file=sys.stderr)
return 4
except OSError as ex:
print(ex, file=sys.stderr)
return 5
try:
got, v = certificate(capability, lowering, digest, roles)
except Refuse as ex:
print("a graph is refused at load: %s" % ex.code, file=sys.stderr)
return 2
if opts["--out"]:
open(opts["--out"], "wb").write(got)
else:
sys.stdout.write(got.decode("ascii"))
sys.stdout.flush()
p = opts["--profile"]
if opts["--check"]:
want = open(opts["--check"], "rb").read()
if got != want:
print("--check: %s" % first_difference(got, want), file=sys.stderr)
return 3
if not v[p]:
print("the verdict does not meet %s: %s" % (p, cw(v[p + "_first_failure"])), file=sys.stderr)
return 1
print("the verdict meets %s: %s" % (p, cw(v)), file=sys.stderr)
return 0
CERTIFY_USAGE = """usage:
wai_ops_check.py certify --capability CAP --lowering ID [--c N] --member NAME=PATH ...
[--profile acc32|acc64] [--out FILE] [--check FILE]
wai_ops_check.py certify --whg1 ROLE=PATH ... [--capability CAP]
[--profile acc32|acc64] [--out FILE] [--check FILE]"""
def main(argv):
if len(argv) not in (2, 4) or (len(argv) == 4 and argv[2] != "--write"):
if len(argv) >= 2 and argv[1] == "certify":
return certify_main(argv[2:])
if len(argv) < 2 or len(argv) % 2:
print(__doc__)
return 2
write = argv[3] if len(argv) == 4 else None
failures = check(argv[1], write)
write, external = None, {}
for k in range(2, len(argv), 2):
a, v = argv[k], argv[k + 1]
if a == "--write":
write = v
elif a == "--external" and "=" in v:
name, d = v.split("=", 1)
external[name] = d
else:
print(__doc__)
return 2
failures = check(argv[1], write, external)
if failures:
print("%d failures" % failures)
return 1
print("ops-conformance: every vector file and every lowered graph rewritten byte for byte")
print("ops-conformance: every vector file, every lowered graph and every certificate rewritten byte for byte")
return 0

View file

@ -1,8 +1,8 @@
# The reference implementation of `wai-ops/1`, the integer operator set
# (`extensions/integer-operators.md`): the `WHG1` graph form, the load and run-time
# plans, the interpreter, the stage operators, the canonical lowerings of the
# registered integer network forms, and the conformance vectors of
# `../ops-conformance/`.
# registered integer network forms, the bounds pass with its certificate (load step
# 8), and the conformance vectors of `../ops-conformance/`.
#
# `no_std` with `alloc`, and no dependencies, so that a model checker can include
# its sources by `#[path]` the way `wai-verify` includes the other integer sources.
@ -13,28 +13,14 @@ edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
publish = false
description = "WAI integer operator set wai-ops/1: WHG1 graphs, plans, interpreter, stage operators, lowerings and conformance vectors"
description = "WAI integer operator set wai-ops/1: WHG1 graphs, plans, interpreter, stage operators, lowerings, bounds certificates and conformance vectors"
[lib]
name = "wai_ops"
path = "src/lib.rs"
# The vector and lowering tool. It reads and writes files, so it is `std`; the
# library it drives is not.
[[bin]]
name = "wai-ops"
path = "src/bin/wai-ops.rs"
required-features = ["uncertified"]
[features]
# Load step 8 (the bounds pass and the profile verdict, §10.5) is not built yet.
# Without this switch a graph cannot complete its load, and since the interpreter
# runs only a graph `graph::load` returned, the default build decodes nothing; with
# it, step 8 is skipped, so the vectors and the lowerings run before the certifier
# exists. The tests, the conformance tool and its browser-engine build (`node/`)
# enable it, and no decoder may. wai-ops is a workspace of its own, so no other
# crate's features unify with it. The switch goes when the certifier lands.
uncertified = []
# The tool that drives this library (the certifier, the lowerings and the corpus) is
# `wai-verify`, a binary of `../wai-verify` behind its `cli` feature.
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }

View file

@ -14,7 +14,7 @@ crate-type = ["cdylib"]
path = "src/lib.rs"
[dependencies]
wai-ops = { path = "..", features = ["uncertified"] }
wai-ops = { path = ".." }
[profile.release]
debug = false

View file

@ -1,225 +0,0 @@
//! `wai-ops`: the vector and lowering tool of `wai/ops-conformance/`.
//!
//! ```text
//! wai-ops vectors gen <wai-dir> write every file of <wai-dir>/ops-conformance
//! wai-ops vectors verify <wai-dir> recompute every vector file and every lowered
//! graph, and regenerate the corpus; every byte
//! must equal the committed one
//! wai-ops vectors recompute <file> recompute one vector file to stdout
//! ```
//!
//! Exit status: 0 when everything verifies, 1 on a difference, 2 on a usage or I/O
//! error.
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use wai_ops::generate;
use wai_ops::json;
use wai_ops::vectors::{self, Kind};
fn usage() -> ExitCode {
eprintln!("usage: wai-ops vectors gen|verify <wai-dir> | wai-ops vectors recompute <file.jsonl>");
ExitCode::from(2)
}
fn kind_of(rel: &str) -> Option<Kind> {
if !rel.ends_with(".jsonl") {
return None;
}
if rel.starts_with("stage/") { Some(Kind::Stage) } else { Some(Kind::Graph) }
}
fn reader(wai: &Path) -> impl Fn(&str) -> Option<Vec<u8>> + '_ {
move |p: &str| std::fs::read(wai.join(p)).ok()
}
fn write_corpus(wai: &Path) -> ExitCode {
let read = reader(wai);
let (files, dropped) = match generate::all(&read) {
Ok(x) => x,
Err(e) => {
eprintln!("gen: {e}");
return ExitCode::from(2);
}
};
let root = wai.join("ops-conformance");
for (rel, bytes) in &files {
let p = root.join(rel);
if let Some(d) = p.parent() {
std::fs::create_dir_all(d).expect("create directory");
}
std::fs::write(&p, bytes).expect("write");
}
let lines: usize = files.iter().filter(|(r, _)| r.ends_with(".jsonl")).map(|(_, b)| b.iter().filter(|&&c| c == b'\n').count()).sum();
eprintln!("wrote {} files, {} vectors", files.len(), lines);
for (f, d) in dropped.iter().filter(|(_, d)| *d > 0) {
eprintln!(" {f}: {d} cases met no profile and were not written");
}
ExitCode::SUCCESS
}
fn list(dir: &Path, base: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
let mut v: Vec<PathBuf> = rd.filter_map(|e| e.ok().map(|e| e.path())).collect();
v.sort();
for p in v {
if p.is_dir() {
list(&p, base, out);
} else {
out.push(p.strip_prefix(base).expect("below base").to_path_buf());
}
}
}
fn verify(wai: &Path) -> ExitCode {
let root = wai.join("ops-conformance");
let mut bad = 0usize;
// 1. Every committed vector file, recomputed from its inputs.
let mut committed = Vec::new();
list(&root, &root, &mut committed);
let (mut nfiles, mut nlines) = (0usize, 0usize);
for rel in &committed {
let r = rel.to_string_lossy().replace('\\', "/");
let Some(kind) = kind_of(&r) else { continue };
let text = std::fs::read_to_string(root.join(rel)).expect("read");
nfiles += 1;
nlines += text.lines().count();
match vectors::recompute_file(kind, &text) {
Ok(out) if out == text => {}
Ok(out) => {
bad += 1;
let n = out.lines().zip(text.lines()).position(|(a, b)| a != b).map_or(out.lines().count().min(text.lines().count()), |n| n);
eprintln!("DIFF {r}: line {} differs when recomputed", n + 1);
}
Err(e) => {
bad += 1;
eprintln!("FAIL {r}: {e}");
}
}
}
eprintln!("recomputed {nfiles} vector files, {nlines} vectors");
// 2. Every lowered graph, from its set, as the index lists it.
match std::fs::read(root.join("certificates/index.json")) {
Ok(b) => match json::parse(&b) {
Ok(idx) => {
let read = reader(wai);
let mut n = 0;
for s in idx.get("sets").and_then(|v| v.as_arr()).unwrap_or(&[]) {
n += 1;
if let Err(e) = check_set(s, &read, wai) {
bad += 1;
eprintln!("FAIL lowering {}: {e}", s.get("name").and_then(|v| v.as_str()).unwrap_or("?"));
}
}
eprintln!("re-lowered {n} sets");
}
Err(_) => {
bad += 1;
eprintln!("FAIL certificates/index.json does not parse");
}
},
Err(e) => {
bad += 1;
eprintln!("FAIL certificates/index.json: {e}");
}
}
// 3. The generator writes the committed corpus again, byte for byte, and nothing
// is committed that it does not write (the README aside).
let read = reader(wai);
match generate::all(&read) {
Ok((files, _)) => {
let mut want: Vec<String> = Vec::new();
for (rel, bytes) in &files {
want.push(rel.clone());
match std::fs::read(root.join(rel)) {
Ok(c) if &c == bytes => {}
Ok(_) => {
bad += 1;
eprintln!("DIFF {rel}: the generator writes other bytes");
}
Err(_) => {
bad += 1;
eprintln!("MISSING {rel}");
}
}
}
for rel in &committed {
let r = rel.to_string_lossy().replace('\\', "/");
if r != "README.md" && r != ".gitattributes" && !want.contains(&r) {
bad += 1;
eprintln!("EXTRA {r}: committed, but the generator does not write it");
}
}
eprintln!("regenerated {} files", files.len());
}
Err(e) => {
bad += 1;
eprintln!("FAIL gen: {e}");
}
}
if bad == 0 {
eprintln!("ops-conformance: every file verifies");
ExitCode::SUCCESS
} else {
eprintln!("ops-conformance: {bad} failures");
ExitCode::from(1)
}
}
fn check_set(s: &json::Value, read: &dyn Fn(&str) -> Option<Vec<u8>>, wai: &Path) -> Result<(), String> {
use wai_ops::generate::sets;
let name = s.get("name").and_then(|v| v.as_str()).ok_or("name")?;
let cap = s.get("capability").and_then(|v| v.as_str()).ok_or("capability")?;
let lowering = s.get("lowering").and_then(|v| v.as_str()).ok_or("lowering")?;
let c = s.get("c").and_then(|v| v.as_dec()).and_then(|v| v.to_i64()).map(|v| v as u32);
let members: Vec<(String, String)> = s.get("members").and_then(|v| v.as_obj()).ok_or("members")?
.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect();
let mut bytes = Vec::new();
for (k, p) in &members {
bytes.push((k.clone(), read(p).ok_or_else(|| format!("cannot read {p}"))?));
}
let m: Vec<(&str, Vec<u8>)> = bytes.iter().map(|(k, b)| (k.as_str(), b.clone())).collect();
let digest = sets::set_digest(&m);
if Some(digest.as_str()) != s.get("set_sha256").and_then(|v| v.as_str()) {
return Err(format!("set digest {digest} differs"));
}
let leak = |x: &str| -> &'static str { Box::leak(x.to_string().into_boxed_str()) };
let set = sets::Set { name: leak(name), capability: leak(cap), lowering: leak(lowering), members: vec![], c, note: "" };
let l = sets::lower_set(&set, &m)?;
let paths: Vec<&str> = s.get("roles").and_then(|v| v.as_arr()).ok_or("roles")?.iter().filter_map(|v| v.as_str()).collect();
if paths.len() != l.roles.len() {
return Err("the roles differ".into());
}
for ((role, g), p) in l.roles.iter().zip(paths) {
let want = std::fs::read(wai.join(p)).map_err(|e| format!("{p}: {e}"))?;
if want != g.to_bytes() {
return Err(format!("{role}: the lowered graph differs from {p}"));
}
}
Ok(())
}
fn main() -> ExitCode {
let a: Vec<String> = std::env::args().collect();
match a.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
[_, "vectors", "gen", dir] => write_corpus(Path::new(dir)),
[_, "vectors", "verify", dir] => verify(Path::new(dir)),
[_, "vectors", "recompute", file] => {
let p = Path::new(file);
let rel = p.to_string_lossy();
let kind = if rel.contains("/stage/") || rel.starts_with("stage/") { Kind::Stage } else { Kind::Graph };
match std::fs::read_to_string(p).map_err(|e| e.to_string()).and_then(|t| vectors::recompute_file(kind, &t)) {
Ok(out) => {
print!("{out}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{e}");
ExitCode::from(2)
}
}
}
_ => usage(),
}
}

1665
wai/wai-ops/src/bounds.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use super::tag;
use crate::bounds::Profile;
use crate::graph::{self, CAP, Graph, InputRec, Op, Params, ScalarRec, TensorRec};
use crate::interp::Tensor;
use crate::json::{self, C, hex};
@ -45,43 +45,27 @@ pub fn one_op(code: u8, ins: Vec<In>, outs: Vec<TensorRec>, p: Params, scalars:
Graph { max_elems: CAP as u32, scalars, inputs, outputs: (n..n + no).collect(), tensors, ops: vec![op] }
}
/// The profiles a graph's vector is tagged with: both when its load refuses (steps
/// 1–7 do not depend on a profile); otherwise those its verdict meets, or `None` when
/// it meets neither (a load-refusal vector of D2b, not written here).
pub fn profiles(bytes: &[u8]) -> Option<Vec<&'static str>> {
let g = match graph::Graph::parse(bytes) {
Ok(g) => g,
Err(_) => return Some(vec!["acc32", "acc64"]),
};
let sizes = match crate::plan::load_plan(&g) {
Ok(s) => s,
Err(_) => return Some(vec!["acc32", "acc64"]),
};
let v = tag::verdict(&g, sizes.as_deref());
match (v.acc32, v.acc64) {
(true, true) => Some(vec!["acc32", "acc64"]),
(false, true) => Some(vec!["acc64"]),
_ => None,
}
}
/// The vector line for a graph and its decodes, with `load` and every `expect`
/// computed by [`vectors::graph_line`]; `None` when the graph meets no profile.
pub fn line(id: &str, bytes: &[u8], decodes: &[Decode]) -> Option<String> {
let prof = profiles(bytes)?;
/// The vector line for a graph and its decodes, with `load`, `profiles` and every
/// `expect` computed by [`vectors::graph_line`]: a graph whose load is refused, at
/// any step of §10.5 including step 8, keeps no decodes and lists both profiles.
pub fn line(id: &str, bytes: &[u8], decodes: &[Decode]) -> String {
let ds: Vec<C> = decodes
.iter()
.map(|d| C::obj(vec![("inputs", C::Arr(d.inputs.iter().map(vectors::tensor_c).collect())), ("scalars", C::ints(d.scalars.iter().map(|&s| s as i128)))]))
.collect();
let c = C::obj(vec![
("decodes", C::Arr(ds)),
("graph", C::Str(hex(bytes))),
("id", C::s(id)),
("profiles", C::Arr(prof.iter().map(|p| C::s(p)).collect())),
]);
let v = json::parse(json::to_string(&c).as_bytes()).ok()?;
let out = vectors::graph_line(&v).ok()?;
Some(json::to_string(&out))
let c = C::obj(vec![("decodes", C::Arr(ds)), ("graph", C::Str(hex(bytes))), ("id", C::s(id)), ("profiles", C::Arr(vec![]))]);
let v = json::parse(json::to_string(&c).as_bytes()).expect("a canonical line parses");
json::to_string(&vectors::graph_line(&v).expect("a generated line recomputes"))
}
/// Whether a random case is kept: unless load step 8 is what refuses its graph. A
/// graph refused at steps 1–7 is a load-refusal vector like any other; one that only
/// step 8 refuses meets no profile, and a random case is drawn again in its place.
pub fn random_keeps(bytes: &[u8]) -> bool {
match graph::load(bytes, Profile::Acc64) {
Ok(_) => true,
Err(r) => r.at != crate::code::Site::Load(8),
}
}
/// A file of lines.

View file

@ -39,9 +39,7 @@ impl G {
fn emit(&mut self, class: &str, b: &[u8], d: Vec<Decode>) {
let id = format!("{}/{}/{}", self.name, class, self.n);
self.n += 1;
let l = line(&id, b, &d);
assert!(l.is_some(), "{id}: a graph vector meets no profile");
self.file.push(l);
self.file.push(Some(line(&id, b, &d)));
}
}
@ -109,7 +107,7 @@ fn phase() -> File {
let d = vec![Decode::new(vec![x.clone()], vec![ho as i64, wo as i64])];
// The two outputs agree, or the phase form is not what §9.3 says.
let b = gr.to_bytes();
let gl = crate::graph::load(&b).expect("phase graph loads");
let gl = crate::graph::load(&b, crate::bounds::Profile::Acc64).expect("phase graph loads");
match run(&gl, &[x], &[ho as i64, wo as i64], &mut Sequence::new()) {
Ok(outs) => assert_eq!(outs[0], outs[1], "the phase form differs from conv_transpose"),
Err(e) => panic!("phase graph refused: {e:?}"),
@ -382,6 +380,24 @@ fn load_refusals() -> File {
ops: vec![op(CROP, vec![0], vec![1], Params::Crop { y0: Operand::Scalar(0), x0: Operand::Const(0), h: Operand::Const(4), w: Operand::Const(1) })],
};
g.emit("crop_plan", &gr.to_bytes(), vec![Decode::new(vec![Tensor::filled(1, 4, 4, 2)], vec![1]), Decode::new(vec![Tensor::filled(1, 4, 4, 2)], vec![3])]);
// Step 8 (§8.1, §8.6): the base graph with its relu's output checked against a
// range its producer cannot give, refused under both profiles; against one that
// meets the producer's interval at a single value, which loads under acc64 only
// (a check inside the graph); and with its convolution's weight raised until
// `conv.acc` passes i64, which no profile meets.
let mut gr = load_base();
gr.tensors[2] = chk(3, -9, -1);
g.emit("step8", &gr.to_bytes(), vec![]);
let mut gr = load_base();
gr.tensors[2] = chk(3, -9, 0);
g.emit("step8", &gr.to_bytes(), vec![Decode::new(vec![Tensor::new(2, 1, 2, vec![-9, 9, 3, -3])], vec![0]), Decode::new(vec![Tensor::new(2, 1, 2, vec![0, -1, 0, 0])], vec![0])]);
let mut gr = load_base();
gr.tensors[0] = chk(2, i64::MIN, i64::MAX);
if let Params::Conv { w, w_bytes, .. } = &mut gr.ops[0].p {
*w = vec![1, 1, 1, 1, 1, 1];
*w_bytes = 1;
}
g.emit("step8", &gr.to_bytes(), vec![]);
g.file
}

View file

@ -7,8 +7,8 @@
//! the graph bytes and the inputs, and every lowered graph from its set, and must
//! write the same bytes.
//!
//! It runs the interpreter, so it needs the `uncertified` feature, as every decode
//! does before the certifier exists.
//! Every graph vector's `load` and `profiles` come from load step 8, the certifier
//! of [`crate::bounds`], and every expectation from the interpreter.
use alloc::format;
use alloc::string::String;
@ -20,7 +20,6 @@ pub mod ops;
pub mod rng;
pub mod sets;
pub mod stages;
pub mod tag;
/// Every file of `ops-conformance/` the generator writes, by its path below
/// `ops-conformance/`, with the count of cases dropped because they met no profile.
@ -54,6 +53,8 @@ pub fn all(read: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<(Vec<(String, Vec<u
for (r, g) in &l.roles {
files.push((format!("certificates/{}/{digest}/{r}.whg1", s.capability), g.to_bytes()));
}
let cert = sets::certificate(&s, &digest, &l)?;
files.push((format!("certificates/{}/{digest}.json", s.capability), cert.to_json().into_bytes()));
index.push(sets::index_entry(&s, &digest, &roles, false));
}
for (s, m) in sets::synthetic() {
@ -66,6 +67,8 @@ pub fn all(read: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<(Vec<(String, Vec<u
for (r, g) in &l.roles {
files.push((format!("certificates/{}/{digest}/{r}.whg1", s.capability), g.to_bytes()));
}
let cert = sets::certificate(&s, &digest, &l)?;
files.push((format!("certificates/{}/{digest}.json", s.capability), cert.to_json().into_bytes()));
index.push(sets::index_entry(&s, &digest, &roles, true));
let vname: &'static str = match s.lowering {
"int_synth/1" => "lower_int_synth",
@ -80,5 +83,27 @@ pub fn all(read: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<(Vec<(String, Vec<u
dropped.push((format!("graphs/{vname}"), f.dropped));
}
files.push((String::from("certificates/index.json"), sets::index_json(index)));
// The falsification sets of §14.3, listed apart from the index: each is lowered and
// certified like the others, and its verdict fails the profile it is checked
// against, which a lowering's driver refuses.
let mut fals = Vec::new();
for (s, m, claim) in sets::falsification() {
for (n, b) in &m {
files.push((format!("sets/{}/{n}", s.name), b.clone()));
}
let l = sets::lower_set(&s, &m)?;
let digest = sets::set_digest(&m);
let roles: Vec<&str> = l.roles.iter().map(|(r, _)| *r).collect();
for (r, g) in &l.roles {
files.push((format!("certificates/{}/{digest}/{r}.whg1", s.capability), g.to_bytes()));
}
let cert = sets::certificate(&s, &digest, &l)?;
if cert.verdict().meets(claim) {
return Err(format!("{}: the falsification set meets {}", s.name, claim.as_str()));
}
files.push((format!("certificates/{}/{digest}.json", s.capability), cert.to_json().into_bytes()));
fals.push(sets::falsification_entry(&s, &digest, &roles, claim));
}
files.push((String::from("certificates/falsification.json"), sets::falsification_json(fals)));
Ok((files, dropped))
}

View file

@ -5,7 +5,7 @@ use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use super::build::{Decode, File, In, line, one_op};
use super::build::{Decode, File, In, line, one_op, random_keeps};
use super::rng::Rng;
use crate::graph::{BiasKind, Graph, Operand, Params, Round, Rq, ScalarRec, TensorRec, opcode::*, smallest_width};
use crate::interp::Tensor;
@ -114,16 +114,16 @@ impl Gen {
fn new(name: &'static str) -> Gen {
Gen { rng: Rng::named(name), file: File::default(), name, n: 0 }
}
/// Write one vector; returns whether it was kept.
fn emit(&mut self, class: &str, g: &Graph, d: Vec<Decode>) -> bool {
/// Write one vector: a graph that loads, with its decodes, or one whose load is
/// refused (load step 8 included), with none.
fn emit(&mut self, class: &str, g: &Graph, d: Vec<Decode>) {
let id = format!("{}/{}/{}", self.name, class, self.n);
self.n += 1;
let l = line(&id, &g.to_bytes(), &d);
let kept = l.is_some();
self.file.push(l);
kept
self.file.push(Some(l));
}
/// Write `count` random vectors from `f`, retrying cases that meet no profile.
/// Write `count` random vectors from `f`, retrying cases whose graph meets no
/// profile.
fn random(&mut self, count: usize, mut f: impl FnMut(&mut Rng) -> (Graph, Vec<Decode>)) {
let mut made = 0;
let mut tries = 0;
@ -131,7 +131,9 @@ impl Gen {
tries += 1;
let (g, d) = f(&mut self.rng);
let id = format!("{}/random/{}", self.name, self.n);
if let Some(l) = line(&id, &g.to_bytes(), &d) {
let b = g.to_bytes();
if random_keeps(&b) {
let l = line(&id, &b, &d);
self.n += 1;
self.file.lines.push(l);
made += 1;
@ -209,9 +211,105 @@ fn file(code: u8, name: &'static str) -> File {
APPLY => apply(&mut g),
_ => {}
}
step8(&mut g, code);
g.file
}
// ------------------------------------------------------------------ load step 8
/// An `RQ` with one multiplier, one shift and one bias of the given placement.
fn rq1(round: Round, m: Option<i64>, s: u8, bias: BiasKind, b: i64) -> Rq {
let (m_kind, m_bytes, mult) = match m {
None => (0, 0, vec![]),
Some(v) => (1, smallest_width(&[v], &[4, 8]), vec![v]),
};
let (b_bytes, bv) = if bias == BiasKind::None { (0, vec![]) } else { (smallest_width(&[b], &[4, 8]), vec![b]) };
Rq { round, bias_kind: bias, m_kind, s_kind: 1, m_bytes, b_bytes, mult, shift: vec![s], bias: bv }
}
/// The vectors whose outcome only the verdict decides (§14.1 item 6), appended to each
/// operator's file after its other cases:
/// - `sigmoid` and `elu` on an input range that reaches −2^63, which fails `acc64`
/// through `exp.negx` and is refused at load;
/// - an `rq` whose `q` passes 64 bits while `y` fits (with a `post` bias), refused at
/// load under `acc64`; for the operators without a bias, `q` and `y` pass together;
/// - `l2norm` with `ε = 2^64 − 1` (`layernorm_c`'s is among its own cases);
/// - for each key of §7.2's table, a graph that fails `acc32` by that parameter
/// condition alone: every interval fits i32, so it loads under `acc64` only;
/// - `lut16` on an input range that reaches ±32768, refused at load (§5.6).
fn step8(g: &mut Gen, code: u8) {
let x1 = |v: Vec<i64>| Tensor::new(1, 1, v.len() as u32, v);
let unary = |g: &mut Gen, class: &str, lo: i64, hi: i64, p: Params, d: Vec<Vec<i64>>| {
let gr = one_op(code, vec![inp(chk(1, lo, hi), 0, 0)], vec![der(1)], p, vec![]);
g.emit(class, &gr, d.into_iter().map(|v| Decode::new(vec![x1(v)], vec![])).collect());
};
const P61: i64 = 1 << 61;
let conv1 = |w: i64, rq: Rq| Params::Conv { cout: 1, kh: 1, kw: 1, sh: 1, sw: 1, ph: 0, pw: 0, groups: 1, w_bytes: smallest_width(&[w], &[1, 2, 4, 8]), rq, w: vec![w] };
let convt1 = |w: i64, rq: Rq| Params::ConvT { cout: 1, kh: 1, kw: 1, sh: 1, sw: 1, ph: 0, pw: 0, oph: 0, opw: 0, w_bytes: smallest_width(&[w], &[1, 2, 4, 8]), rq, w: vec![w] };
match code {
SIGMOID => {
unary(g, "step8", i64::MIN, 0, Params::None, vec![]);
unary(g, "step8", i64::MIN, i64::MAX, Params::None, vec![]);
}
ELU => {
for alpha in [1i64 << 16, 1] {
unary(g, "step8", i64::MIN, 0, Params::Elu { alpha }, vec![]);
}
}
RESCALE => {
// t = 2^61, m = 4: q = 2^63 passes i64, y = 2^63 − 1 fits; one less fits.
let q = |v| rq1(Round::Floor, Some(4), 0, BiasKind::Post, v);
unary(g, "step8_q", P61, P61, Params::Rescale(q(-1)), vec![]);
unary(g, "step8_q", P61 - 1, P61 - 1, Params::Rescale(q(-1)), vec![vec![P61 - 1]]);
// One per key of the rq rows of §7.2.
unary(g, "acc32_param", 0, 3, Params::Rescale(rq1(Round::Floor, Some(1 << 31), 31, BiasKind::None, 0)), vec![vec![0, 1, 2, 3]]);
unary(g, "acc32_param", 0, 3, Params::Rescale(rq1(Round::Floor, None, 64, BiasKind::None, 0)), vec![vec![0, 3]]);
unary(g, "acc32_param", -(1 << 31), -1, Params::Rescale(rq1(Round::Floor, None, 0, BiasKind::Post, 1 << 31)), vec![vec![-(1 << 31), -1]]);
}
CONV | CONV_TRANSPOSE => {
let mk = |w, rq| if code == CONV { conv1(w, rq) } else { convt1(w, rq) };
let q = rq1(Round::Floor, Some(4), 0, BiasKind::Post, -1);
unary(g, "step8_q", P61, P61, mk(1, q.clone()), vec![]);
unary(g, "step8_q", 0, P61 - 1, mk(1, q), vec![vec![0, P61 - 1]]);
unary(g, "acc32_param", -1, 0, mk(1 << 31, Rq::plain(Round::Floor, 0)), vec![vec![-1, 0]]);
}
MUL => {
let rq = rq1(Round::Floor, Some(2), 0, BiasKind::None, 0);
let gr = one_op(MUL, vec![inp(chk(1, 1 << 31, 1 << 31), 0, 0), inp(chk(1, 1 << 31, 1 << 31), 0, 0)], vec![der(1)], Params::Mul(rq), vec![]);
g.emit("step8_q", &gr, vec![]);
}
CHSCALE => {
let sc = vec![ScalarRec { lo: 0, hi: 0 }];
let ch = |m: i64, shift: u8| Params::Chscale { scalar: 0, n_rows: 1, round: Round::Floor, m_bytes: smallest_width(&[m], &[4, 8]), shift, m: vec![m] };
let gr = one_op(CHSCALE, vec![inp(chk(1, 1 << 62, 1 << 62), 0, 0)], vec![der(1)], ch(2, 0), sc.clone());
g.emit("step8_q", &gr, vec![]);
let gr = one_op(CHSCALE, vec![inp(chk(1, -1, 0), 0, 0)], vec![der(1)], ch(1 << 31, 31), sc.clone());
g.emit("acc32_param", &gr, vec![Decode::new(vec![x1(vec![-1, 0])], vec![0])]);
let gr = one_op(CHSCALE, vec![inp(chk(1, 0, 5), 0, 0)], vec![der(1)], ch(1, 64), sc);
g.emit("acc32_param", &gr, vec![Decode::new(vec![x1(vec![0, 5])], vec![0])]);
}
IGDN => {
let ig = |gf: u32, gamma: i64| Params::Igdn { gf, frac: 0, g_bytes: smallest_width(&[gamma], &[4, 8]), gamma: vec![gamma], beta: vec![1] };
unary(g, "acc32_param", 0, 1, ig(31, 1 << 31), vec![vec![0, 1]]);
unary(g, "acc32_param", 0, 1, ig(64, 1), vec![vec![0, 1]]);
}
LEAKY => unary(g, "acc32_param", -1, 5, Params::Leaky { a: 1 << 31, f: 31 }, vec![vec![-1, 0, 5]]),
L2NORM => {
let gr = one_op(L2NORM, vec![inp(chk(1, -5, 5), 2, 2)], vec![der(1)], Params::L2norm { eps: u64::MAX }, vec![]);
g.emit("eps", &gr, vec![Decode::new(vec![Tensor::new(1, 2, 2, vec![-5, 5, 1, 0])], vec![])]);
}
LUT16 => {
let mut t = vec![1i16; 65536];
t[0] = 0;
for (lo, hi) in [(-32768, 0), (0, 32768)] {
let gr = one_op(LUT16, vec![inp(chk(1, lo, hi), 0, 0)], vec![der(1)], Params::Lut16 { e_in: -128, e_out: -128, t: t.clone() }, vec![]);
g.emit("step8", &gr, vec![]);
}
}
_ => {}
}
}
// ------------------------------------------------------------------ conv
fn conv_params(r: &mut Rng, cin: u32) -> (Params, u32, u32, u32, u32, u32) {

View file

@ -8,6 +8,7 @@ use alloc::vec::Vec;
use super::rng::Rng;
use crate::json::{self, C, hex};
use crate::bounds::{self, Certificate, Profile};
use crate::lower::{self, Lowered};
use crate::sha256::sha256;
@ -122,6 +123,56 @@ pub fn synthetic() -> Vec<(Set, Vec<(&'static str, Vec<u8>)>)> {
v
}
/// The two sets of §14.3 that a certificate must reject, each with the profile it is
/// checked against: an `int_synth` set whose accumulator passes i64 by one unit, and
/// one whose pin claims `acc32` and whose accumulator passes i32 by one unit. Each is
/// one unit past a set that meets the profile, which is the set with the bias or the
/// weight named below one smaller.
pub fn falsification() -> Vec<(Set, Vec<(&'static str, Vec<u8>)>, Profile)> {
let base = |n: &str| format!("ops-conformance/sets/{n}/int_synth.json");
let layer = |i: usize, o: usize, w: &[i64], b: &[i64], shift: u32, relu: bool| -> String {
let mut s = format!("{{\"in_ch\":{i},\"out_ch\":{o},\"k\":1,\"stride\":1,\"pad\":0,\"weight\":{},\"bias\":{},\"shift\":{shift}", ints(w), ints(b));
if relu {
s.push_str(",\"relu\":true");
}
s.push('}');
s
};
let doc = |layers: &[String]| -> Vec<u8> {
let mut s = String::from("{\"layers\":[");
s.push_str(&layers.join(","));
s.push_str("]}\n");
s.into_bytes()
};
// 127 · 331720249 · 218934409 = 2^63 − 1. Layer 1 maps [−128, 127] onto
// [0, 127·331720249] (ReLU); layer 2's channel 0 then sums to 2^63 − 1 plus its
// bias of 1: `convT.acc` reaches 2^63, one past i64. With a bias of 0 it fits.
let overflow = doc(&[layer(1, 1, &[331_720_249], &[0], 0, true), layer(1, 3, &[218_934_409, 0, 0], &[1, 0, 0], 0, false)]);
// Layer 1 maps [−128, 127] onto [−1, 1] in each of two channels (shift 8, bias
// 129); layer 2's channel 0 sums both with weight 2^30: `convT.acc` reaches 2^31,
// one past i32. With one of those weights 2^30 − 1 it fits.
let claim = doc(&[layer(1, 2, &[1, 1], &[129, 129], 8, false), layer(2, 3, &[1 << 30, 0, 0, 1 << 30, 0, 0], &[0, 0, 0], 0, false)]);
vec![
(
Set { name: "overflow_acc64", capability: "wai.neural.int_synth", lowering: "int_synth/1", members: vec![("int_synth.json", base("overflow_acc64"))], c: None, note: "fails acc64 by one unit (section 14.3)" },
vec![("int_synth.json", overflow)],
Profile::Acc64,
),
(
Set { name: "claim_acc32", capability: "wai.neural.int_synth", lowering: "int_synth/1", members: vec![("int_synth.json", base("claim_acc32"))], c: None, note: "its pin claims acc32, which it fails by one weight (section 14.3)" },
vec![("int_synth.json", claim)],
Profile::Acc32,
),
]
}
/// The bounds certificate (§8.5) of a set's lowered graphs.
pub fn certificate(s: &Set, digest: &str, l: &Lowered) -> Result<Certificate, String> {
let bytes: Vec<(&str, Vec<u8>)> = l.roles.iter().map(|(r, g)| (*r, g.to_bytes())).collect();
let refs: Vec<(&str, &[u8])> = bytes.iter().map(|(r, b)| (*r, b.as_slice())).collect();
bounds::certify(Some(s.capability), s.lowering, Some(digest), &refs).map_err(|e| format!("{}: {} cannot be certified ({:?})", s.name, e.name, e.refusal))
}
/// The set's digest (SPEC §3.1): one file's SHA-256, or its listing's.
pub fn set_digest(members: &[(&str, Vec<u8>)]) -> String {
if members.len() == 1 {
@ -448,6 +499,26 @@ fn jpegai_bundle(r: &mut Rng) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
/// Write the index (`certificates/index.json`): pretty enough to read, and
/// canonical in its member order.
/// The entry of a falsification set: an index entry with the profile it is checked
/// against, `claim`, whose verdict it fails.
pub fn falsification_entry(s: &Set, digest: &str, roles: &[&str], claim: Profile) -> C {
match index_entry(s, digest, roles, true) {
C::Obj(mut m) => {
m.push((String::from("claim"), C::s(claim.as_str())));
C::Obj(m)
}
c => c,
}
}
/// `certificates/falsification.json`.
pub fn falsification_json(entries: Vec<C>) -> Vec<u8> {
let c = C::obj(vec![("format", C::s("wai-ops-falsification/1")), ("sets", C::Arr(entries))]);
let mut s = json::to_string(&c);
s.push('\n');
s.into_bytes()
}
pub fn index_json(entries: Vec<C>) -> Vec<u8> {
let c = C::obj(vec![("format", C::s("wai-ops-lowered/1")), ("sets", C::Arr(entries))]);
let mut s = json::to_string(&c);

View file

@ -1,511 +0,0 @@
//! The profiles a generated graph meets (§7, §8.3), for the `profiles` member of its
//! vector.
//!
//! This is the generator's own tagger, not the certifier: it applies the bound rules
//! of §8.3 and the classes and parameter conditions of §7 to the graphs the generator
//! writes, in `i128` with every step checked. A value that leaves `i128` lies outside
//! every class width of both profiles, so treating it as a failure gives the same
//! verdict as saturating it (§8.1). The certifier (D2b) computes the same verdicts
//! from the text, and must agree with every tag written here.
//!
//! What a node fits follows §8.5: its intermediates and its producer results. A
//! tensor's interval after a `clamp` policy is not classed by that rule, although
//! §7.1 calls every tensor element class `tensor`; no vector here depends on the
//! difference.
use alloc::vec;
use alloc::vec::Vec;
use crate::graph::{BiasKind, Graph, Operand, Params, Policy, Round, Rq, opcode::*};
use crate::interp::round;
/// An interval.
type Iv = (i128, i128);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Verdict {
pub acc32: bool,
pub acc64: bool,
}
/// Widths of a class under each profile: (acc32, acc64), in bits.
#[derive(Clone, Copy)]
enum Class {
Tensor,
Acc,
Wide,
Product,
}
fn widths(c: Class) -> (u32, u32) {
match c {
Class::Tensor | Class::Acc => (32, 64),
Class::Wide => (32, 128),
Class::Product => (64, 128),
}
}
fn fits(v: i128, n: u32) -> bool {
if n >= 128 {
return true;
}
let lim = 1i128 << (n - 1);
-lim <= v && v < lim
}
struct Acc {
a32: bool,
a64: bool,
dead: bool,
}
impl Acc {
fn class(&mut self, iv: Option<Iv>, c: Class) {
match iv {
None => self.dead = true,
Some((lo, hi)) => {
let (w32, w64) = widths(c);
if !(fits(lo, w32) && fits(hi, w32)) {
self.a32 = false;
}
if !(fits(lo, w64) && fits(hi, w64)) {
self.a64 = false;
}
}
}
}
}
fn hull(a: Iv, b: Iv) -> Iv {
(a.0.min(b.0), a.1.max(b.1))
}
fn hull_opt(a: Option<Iv>, b: Iv) -> Option<Iv> {
Some(match a {
None => b,
Some(a) => hull(a, b),
})
}
fn mul(a: i128, b: i128) -> Option<i128> {
a.checked_mul(b)
}
fn add(a: i128, b: i128) -> Option<i128> {
a.checked_add(b)
}
fn term(w: i128, l: i128, h: i128) -> Option<Iv> {
let (x, y) = (mul(w, l)?, mul(w, h)?);
Some((x.min(y), x.max(y)))
}
fn corner(a: Iv, b: Iv) -> Option<Iv> {
let c = [mul(a.0, b.0)?, mul(a.0, b.1)?, mul(a.1, b.0)?, mul(a.1, b.1)?];
Some((*c.iter().min()?, *c.iter().max()?))
}
/// `A(K, b)` (§8.2).
fn psum(terms: &[Iv], b: i128) -> Option<Iv> {
let mut lo = b.min(0);
let mut hi = b.max(0);
for t in terms {
lo = add(lo, t.0.min(0))?;
hi = add(hi, t.1.max(0))?;
}
Some((lo, hi))
}
fn floor_sh(v: i128, s: u32) -> i128 {
if s >= 127 { if v < 0 { -1 } else { 0 } } else { v >> s }
}
/// The `rq` rules for one channel: `rq.p`, `rq.q`, `rq.y` from `rq.t`.
fn rq_rules(t: Iv, m: Option<i64>, s: u32, r: Round, post: i64) -> Option<(Iv, Iv, Iv)> {
let m = m.unwrap_or(1) as i128;
let p = term(m, t.0, t.1)?;
let a = if r != Round::Floor && s >= 1 { 1i128.checked_shl(s - 1).filter(|_| s - 1 < 127)? } else { 0 };
let rp = (p.0, add(p.1, a)?);
let q = (round(p.0, s, r)?, round(p.1, s, r)?);
let y = (add(q.0, post as i128)?, add(q.1, post as i128)?);
Some((rp, q, y))
}
/// The intermediates and the producer result of a whole `rq`, hulled over channels,
/// given each channel's `rq.t`.
fn rq_all(acc: &mut Acc, ts: &[Option<Iv>], rq: &Rq) -> Option<Iv> {
let (mut t, mut p, mut q, mut y) = (None, None, None, None);
for (o, ti) in ts.iter().enumerate() {
let ti = (*ti)?;
let (rp, rq_, ry) = rq_rules(ti, rq.mult(o), rq.shift(o), rq.round, rq.post(o))?;
t = hull_opt(t, ti);
p = hull_opt(p, rp);
q = hull_opt(q, rq_);
y = hull_opt(y, ry);
}
acc.class(t, Class::Acc);
acc.class(p, Class::Product);
acc.class(q, Class::Acc);
acc.class(y, Class::Acc);
y
}
fn exp_rule(acc: &mut Acc, u: Iv) {
acc.class(Some(u), Class::Acc);
acc.class(Some((45426 * (u.0 / 45426), 45426 * (u.1 / 45426))), Class::Acc);
acc.class(Some((43245 * 43245, 88670 * 88670)), Class::Acc);
acc.class(Some((0, 23495 * ((88670i128 * 88670) >> 16))), Class::Acc);
}
fn fits64(v: i128) -> bool {
fits(v, 64)
}
/// The profiles `g` meets, given the sizes the load plan gave (`None` when the
/// inputs declare none).
pub fn verdict(g: &Graph, sizes: Option<&[(u64, u64)]>) -> Verdict {
let mut a = Acc { a32: true, a64: true, dead: false };
// Inputs and scalars: their declared ranges.
for i in &g.inputs {
let t = &g.tensors[i.tensor as usize];
a.class(Some((t.lo as i128, t.hi as i128)), Class::Tensor);
}
for s in &g.scalars {
a.class(Some((s.lo as i128, s.hi as i128)), Class::Tensor);
}
let mut iv: Vec<Option<Iv>> = vec![None; g.tensors.len()];
for i in &g.inputs {
let t = &g.tensors[i.tensor as usize];
iv[i.tensor as usize] = Some((t.lo as i128, t.hi as i128));
}
for op in &g.ops {
let outs = node(&mut a, g, op, &iv, sizes);
for (k, &t) in op.outs.iter().enumerate() {
let rec = &g.tensors[t as usize];
let r = outs.as_ref().and_then(|o| o.get(k).copied().flatten());
a.class(r, Class::Tensor);
let after = r.map(|(lo, hi)| match rec.policy {
Policy::Derived => (lo, hi),
Policy::Clamp => (lo.clamp(rec.lo as i128, rec.hi as i128), hi.clamp(rec.lo as i128, rec.hi as i128)),
Policy::Check => (lo.max(rec.lo as i128), hi.min(rec.hi as i128)),
});
if let Some((lo, hi)) = after
&& lo > hi
{
a.dead = true;
}
if rec.policy == Policy::Check {
a.a32 = false;
}
iv[t as usize] = after;
}
params32(&mut a, op);
}
Verdict { acc32: a.a32 && a.a64 && !a.dead, acc64: a.a64 && !a.dead }
}
/// The parameter conditions of §7.2.
fn params32(a: &mut Acc, op: &crate::graph::Op) {
let f32_ = |v: i64| fits(v as i128, 32);
let rq_ok = |rq: &Rq| rq.mult.iter().all(|&m| f32_(m)) && rq.shift.iter().all(|&s| s <= 63) && (rq.bias_kind != BiasKind::Post || rq.bias.iter().all(|&b| f32_(b)));
let ok = match &op.p {
Params::Conv { rq, w, .. } | Params::ConvT { rq, w, .. } => rq_ok(rq) && w.iter().all(|&x| f32_(x)),
Params::Rescale(rq) | Params::Mul(rq) | Params::Gram { rq, .. } | Params::Apply { rq, .. } => rq_ok(rq),
Params::Chscale { shift, m, .. } => *shift <= 63 && m.iter().all(|&x| f32_(x)),
Params::Igdn { gf, frac, gamma, .. } => *gf <= 63 && *frac <= 63 && gamma.iter().all(|&x| f32_(x)),
Params::Leaky { a: s, .. } => f32_(*s),
_ => true,
};
if !ok {
a.a32 = false;
}
}
/// One node: classes its intermediates into `a` and returns its producer results.
fn node(a: &mut Acc, g: &Graph, op: &crate::graph::Op, iv: &[Option<Iv>], sizes: Option<&[(u64, u64)]>) -> Option<Vec<Option<Iv>>> {
let x = iv[op.ins[0] as usize];
let Some((l, h)) = x else {
a.dead = true;
return None;
};
let one = |r: Option<Iv>| Some(vec![r]);
let cin = g.tensors[op.ins[0] as usize].channels as usize;
match (&op.p, op.code) {
(Params::Conv { cout, kh, kw, groups, rq, w, .. }, _) => {
let (cout, kk, cg) = (*cout as usize, (*kh * *kw) as usize, cin / *groups as usize);
let mut tm: Option<Iv> = None;
let mut accs: Option<Iv> = None;
let mut ts = Vec::with_capacity(cout);
for o in 0..cout {
let mut terms = Vec::with_capacity(cg * kk);
for k in 0..cg * kk {
let t = term(w[o * cg * kk + k] as i128, l, h);
match t {
Some(t) => {
terms.push(t);
tm = hull_opt(tm, t);
}
None => a.dead = true,
}
}
let ac = psum(&terms, rq.pre(o) as i128);
if let Some(ac) = ac {
accs = hull_opt(accs, ac);
} else {
a.dead = true;
}
ts.push(ac);
}
a.class(tm.or(Some((0, 0))), Class::Acc);
a.class(accs, Class::Acc);
one(rq_all(a, &ts, rq))
}
(Params::ConvT { cout, kh, kw, sh, sw, rq, w, .. }, _) => {
let (cout, kh, kw, sh, sw) = (*cout as usize, *kh as usize, *kw as usize, *sh as usize, *sw as usize);
let mut tm: Option<Iv> = None;
let mut accs: Option<Iv> = None;
let mut ts = Vec::with_capacity(cout);
for o in 0..cout {
let mut all: Option<Iv> = None;
for ry in 0..sh {
for rx in 0..sw {
let mut terms = Vec::new();
for ci in 0..cin {
for i in (ry..kh).step_by(sh) {
for j in (rx..kw).step_by(sw) {
match term(w[((ci * cout + o) * kh + i) * kw + j] as i128, l, h) {
Some(t) => {
terms.push(t);
tm = hull_opt(tm, t);
}
None => a.dead = true,
}
}
}
}
match psum(&terms, rq.pre(o) as i128) {
Some(p) => all = hull_opt(all, p),
None => a.dead = true,
}
}
}
accs = match (accs, all) {
(Some(x), Some(y)) => Some(hull(x, y)),
(None, y) => y,
(x, None) => x,
};
ts.push(all);
}
a.class(tm.or(Some((0, 0))), Class::Acc);
a.class(accs, Class::Acc);
one(rq_all(a, &ts, rq))
}
(Params::Rescale(rq), _) => {
let ts: Vec<Option<Iv>> = (0..cin).map(|c| Some((add(l, rq.pre(c) as i128)?, add(h, rq.pre(c) as i128)?))).collect();
one(rq_all(a, &ts, rq))
}
(Params::Chscale { scalar, round: r, shift, m, .. }, _) => {
let s = g.scalars[*scalar as usize];
let (mut p, mut q) = (None, None);
for c in 0..cin {
for row in s.lo..=s.hi {
let (rp, rq_, _) = rq_rules((l, h), Some(m[row as usize * cin + c]), *shift as u32, *r, 0)?;
p = hull_opt(p, rp);
q = hull_opt(q, rq_);
}
}
a.class(Some((l, h)), Class::Acc);
a.class(p, Class::Product);
a.class(q, Class::Acc);
a.class(q, Class::Acc);
one(q)
}
(Params::Mul(rq), _) => {
let b = iv[op.ins[1] as usize]?;
let mp = corner((l, h), b);
a.class(mp, Class::Acc);
let ts = vec![mp; cin];
one(rq_all(a, &ts, rq))
}
(Params::Gram { rq, .. }, _) | (Params::Apply { rq, .. }, _) => {
let b = iv[op.ins[1] as usize]?;
let t = corner((l, h), b);
a.class(t, Class::Acc);
let n = match op.code {
GRAM => {
let s = sizes?;
let (h0, w0) = s[op.ins[0] as usize];
let (h1, w1) = s[op.ins[1] as usize];
(h0 * w0).max(h1 * w1) as i128
}
_ => (cin / match op.p { Params::Apply { groups, .. } => groups as usize, _ => 1 }) as i128,
};
let acc = t.and_then(|t| Some((mul(n, t.0.min(0))?, mul(n, t.1.max(0))?)));
a.class(acc, Class::Acc);
let ts = vec![acc; cin];
one(rq_all(a, &ts, rq))
}
(_, ADD) => {
let b = iv[op.ins[1] as usize]?;
one(Some((add(l, b.0)?, add(h, b.1)?)))
}
(_, CHUNK_ADD) => one(Some((mul(2, l)?, mul(2, h)?))),
(Params::Clamp { lo, hi }, _) => one(Some((l.clamp(*lo as i128, *hi as i128), h.clamp(*lo as i128, *hi as i128)))),
(_, RELU) => one(Some((l.max(0), h.max(0)))),
(_, ABS) => one(Some(if l >= 0 { (l, h) } else if h <= 0 { (-h, -l) } else { (0, (-l).max(h)) })),
(Params::Leaky { a: s, f }, _) => {
let mut r: Option<Iv> = None;
if l < 0 {
let p = corner((l, h.min(-1)), (*s as i128, *s as i128));
a.class(p, Class::Acc);
let p = p?;
r = Some((floor_sh(p.0, *f), floor_sh(p.1, *f)));
} else {
a.class(Some((0, 0)), Class::Acc);
}
if h >= 0 {
r = hull_opt(r, (l.max(0), h));
}
one(r)
}
(Params::Lut16 { t, .. }, _) => {
if l < -32767 || h > 32767 {
a.dead = true;
return None;
}
let vals = (l..=h).map(|u| t[(u + 32768) as usize] as i128);
let (mn, mx) = vals.fold((i128::MAX, i128::MIN), |(a, b), v| (a.min(v), b.max(v)));
one(Some((mn, mx)))
}
(Params::Igdn { gf, frac, gamma, beta, .. }, _) => {
let x2 = if l <= 0 && h >= 0 { (0, mul(l, l)?.max(mul(h, h)?)) } else { (mul(l, l)?.min(mul(h, h)?), mul(l, l)?.max(mul(h, h)?)) };
a.class(Some(x2), Class::Wide);
let (mut gx, mut tm, mut nn, mut rr) = (None, None, None, None);
for i in 0..cin {
let mut terms = Vec::with_capacity(cin);
let (mut flo, mut fhi) = (beta[i] as i128, beta[i] as i128);
for j in 0..cin {
let g2 = corner((gamma[i * cin + j] as i128, gamma[i * cin + j] as i128), x2)?;
gx = hull_opt(gx, g2);
let t = (floor_sh(g2.0, *gf), floor_sh(g2.1, *gf));
tm = hull_opt(tm, t);
terms.push(t);
flo = add(flo, t.0)?;
fhi = add(fhi, t.1)?;
}
nn = hull_opt(nn, psum(&terms, beta[i] as i128)?);
let r = ((flo.max(0) as u128).isqrt() as i128, (fhi.max(0) as u128).isqrt() as i128);
rr = hull_opt(rr, r);
}
a.class(gx, Class::Product);
a.class(tm, Class::Wide);
a.class(nn, Class::Wide);
a.class(rr, Class::Acc);
let xr = corner((l, h), rr?);
a.class(xr, Class::Wide);
let xr = xr?;
one(Some((floor_sh(xr.0, *frac), floor_sh(xr.1, *frac))))
}
(Params::LayerNorm { eps, gamma, beta, .. }, _) => {
let c = cin as i128;
let e = *eps as i128;
let r0 = (e as u128).isqrt() as i128;
let k = (1i128 << 32) / r0;
let d = h - l;
a.class(Some((mul(c, l.min(0))?, mul(c, h.max(0))?)), Class::Wide);
a.class(Some((mul(c, l)? - c / 2, add(mul(c, h)?, c / 2)?)), Class::Wide);
a.class(Some((-d, d)), Class::Acc);
a.class(mul(d, d).and_then(|dd| Some((0, mul(c, dd)?))), Class::Wide);
a.class(mul(d, d).and_then(|dd| Some((e, add(dd, e)?))), Class::Wide);
a.class(Some((1 << 32, 1 << 32)), Class::Acc);
a.class(Some((0, k)), Class::Acc);
let ic = (c as u128).isqrt() as i128;
let icc = if ic * ic == c { ic } else { ic + 1 };
let dmax_l = mul(d, k);
let dmax_r = mul(icc, 1i128 << 32).and_then(|v| mul(v, r0 + 1)).map(|v| (v + r0 - 1) / r0);
let dd = match (dmax_l, dmax_r) {
(Some(x), Some(y)) => Some(x.min(y)),
(x, y) => x.or(y),
};
a.class(dd.map(|v| (-v, v)), Class::Wide);
let nmax = mul(icc, 1i128 << 16).and_then(|v| mul(v, r0 + 1)).map(|v| v / r0 + 1);
let n1 = mul(d, k).map(|v| (v >> 16) + 1);
let n = match (n1, nmax) {
(Some(x), Some(y)) => x.min(y),
(x, y) => x.or(y)?,
};
a.class(Some((-n, n)), Class::Acc);
let (mut ng, mut pr) = (None, None);
for cc in 0..cin {
let gv = gamma[cc] as i128;
let t = corner((-n, n), (gv, gv))?;
ng = hull_opt(ng, t);
pr = hull_opt(pr, (add(t.0 >> 16, beta[cc] as i128)?, add(t.1 >> 16, beta[cc] as i128)?));
}
a.class(ng, Class::Wide);
one(pr)
}
(Params::L2norm { eps }, _) => {
let s = sizes?;
let (hh, ww) = s[op.ins[0] as usize];
let xm = l.abs().max(h.abs());
let e = *eps as i128;
a.class(mul(xm, xm).and_then(|x2| mul((hh * ww) as i128, x2)).and_then(|v| add(v, e)).map(|v| (0, v)), Class::Wide);
a.class(Some((1 << 32, 1 << 32)), Class::Acc);
a.class(Some((0, (1i128 << 32) / (e as u128).isqrt() as i128)), Class::Acc);
a.class(Some((-(1 << 32), 1 << 32)), Class::Wide);
one(Some((-(1 << 16), 1 << 16)))
}
(_, SOFTMAX) => {
let s = sizes?;
let wm = s[op.ins[0] as usize].1 as i128;
a.class(Some((l - h, 0)), Class::Acc);
exp_rule(a, (0, h - l));
a.class(Some((0, wm * 65553)), Class::Acc);
a.class(Some((0, 65553 * 65536 + (wm * 65553) / 2)), Class::Acc);
one(Some((0, 1 << 16)))
}
(_, SIGMOID) => {
let u = if l >= 0 { (l, h) } else if h <= 0 { (-h, -l) } else { (0, (-l).max(h)) };
exp_rule(a, u);
a.class(Some((0, 65553 * 65536)), Class::Acc);
one(Some((0, 1 << 16)))
}
(Params::Elu { alpha }, _) => {
exp_rule(a, ((-h).max(0), (-l).max(0)));
let p = corner((*alpha as i128, *alpha as i128), (-(1 << 16), 17));
a.class(p, Class::Wide);
let mut r: Option<Iv> = None;
if h > 0 {
r = Some((l.max(1), h));
}
if l <= 0 {
let p = p?;
r = hull_opt(r, (p.0 >> 16, p.1 >> 16));
}
one(r)
}
(_, CONCAT) | (_, SUBPOS_MERGE) => {
let mut r = None;
for &t in &op.ins {
r = hull_opt(r, iv[t as usize]?);
}
one(r)
}
(Params::Fill { kind, value }, _) => {
if *kind == 0 {
one(Some((*value as i128, *value as i128)))
} else {
let s = g.scalars[*value as usize];
one(Some((s.lo as i128, s.hi as i128)))
}
}
(_, SUBPOS_SPLIT) => Some(vec![Some((l, h)); 4]),
(Params::Crop { .. }, _) => one(Some((l, h))),
_ => one(Some((l, h))),
}
}
/// Whether an `Operand` is a scalar; used by the generator to know whether a crop
/// needs a scalar.
pub fn is_scalar(o: Operand) -> bool {
matches!(o, Operand::Scalar(_))
}
/// Whether `v` fits `i64`.
pub fn fits_i64(v: i128) -> bool {
fits64(v)
}

View file

@ -4,6 +4,7 @@
use alloc::vec;
use alloc::vec::Vec;
use crate::bounds::Profile;
use crate::code::{Code, Refusal, at_load};
pub const MAGIC: &[u8; 4] = b"WHG1";
@ -308,7 +309,13 @@ impl<'a> R<'a> {
.collect())
}
fn zero(&mut self, n: usize, st: u8) -> Res<()> {
if self.take(n, st)?.iter().all(|&b| b == 0) { Ok(()) } else { Err(at_load(Code::ModelMismatch, st)) }
let s = self.take(n, st)?;
for k in 0..n {
if s[k] != 0 {
return Err(at_load(Code::ModelMismatch, st));
}
}
Ok(())
}
}
@ -979,19 +986,18 @@ pub fn smallest_width(v: &[i64], allowed: &[u8]) -> u8 {
8
}
/// A graph that passed every load step of §10.5. Only [`load`] makes one, and
/// [`crate::interp::run`] takes nothing else, so no graph decodes without passing
/// load step 8: a [`Graph`] that was parsed, lowered or built in place runs only
/// after `load` of its bytes.
/// A graph that passed every load step of §10.5, step 8 included, under a profile.
/// Only [`load`] makes one, and [`crate::interp::run`] takes nothing else, so no graph
/// decodes without passing load step 8: a [`Graph`] that was parsed, lowered or built
/// in place runs only after `load` of its bytes.
///
/// ```
/// # #[cfg(feature = "uncertified")] {
/// use wai_ops::{Sequence, Tensor, graph, lower, run};
/// use wai_ops::{Profile, Sequence, Tensor, graph, lower, run};
/// let set = lower::int_synth(br#"{"layers":[{"in_ch":1,"out_ch":3,"k":1,"stride":1,"pad":0,"weight":[1,2,3],"bias":[0,0,0]}]}"#).unwrap();
/// let g = graph::load(&set.get("synth").unwrap().to_bytes()).unwrap();
/// let g = graph::load(&set.get("synth").unwrap().to_bytes(), Profile::Acc64).unwrap();
/// assert!(g.meets(Profile::Acc32));
/// let out = run(&g, &[Tensor::new(1, 1, 1, vec![5])], &[], &mut Sequence::new()).unwrap();
/// assert_eq!(out[0].data, vec![5, 10, 15]);
/// # }
/// ```
///
/// A lowered graph that has not been loaded does not compile as an argument to `run`:
@ -1005,6 +1011,7 @@ pub fn smallest_width(v: &[i64], allowed: &[u8]) -> u8 {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Loaded {
g: Graph,
acc32: bool,
}
impl Loaded {
@ -1012,6 +1019,15 @@ impl Loaded {
pub fn graph(&self) -> &Graph {
&self.g
}
/// Whether the graph's verdict meets `p` (§8.5). A loaded graph meets the profile
/// it was loaded under, and every graph that meets `acc32` also meets `acc64`.
pub fn meets(&self, p: Profile) -> bool {
match p {
Profile::Acc64 => true,
Profile::Acc32 => self.acc32,
}
}
}
impl core::ops::Deref for Loaded {
@ -1021,28 +1037,20 @@ impl core::ops::Deref for Loaded {
}
}
/// Load a graph: steps 1–7 of §10.5, then step 8.
/// Load a graph under the profile `p` of the operator implementation that is to run
/// it: steps 1–7 of §10.5, then step 8, the bounds pass (§8), `lut16`'s range check
/// (§5.6) and `p` against the graph's verdict (§8.6). Every refusal is
/// `model_mismatch`, or `too_large` for a size.
///
/// Step 8 (the bounds pass, `lut16`'s range check, and the capability's profile
/// against the verdict) is not built yet. Without the `uncertified` feature it
/// refuses every graph (`model_mismatch` at step 8), so no [`Loaded`] exists and
/// nothing decodes; with the feature, which only the tests, the conformance tool and
/// its browser-engine build enable, it is skipped.
pub fn load(b: &[u8]) -> Res<Loaded> {
/// A set of several graphs meets `p` exactly when each of its graphs does, so a
/// driver that loads every graph of a set under its capability's declared profile
/// refuses exactly the sets whose certificate does not meet it.
pub fn load(b: &[u8], p: Profile) -> Res<Loaded> {
let g = Graph::parse(b)?;
crate::plan::load_plan(&g)?;
step8(&g)?;
Ok(Loaded { g })
}
#[cfg(feature = "uncertified")]
fn step8(_g: &Graph) -> Res<()> {
Ok(())
}
#[cfg(not(feature = "uncertified"))]
fn step8(_g: &Graph) -> Res<()> {
Err(Refusal::new(Code::ModelMismatch, crate::code::Site::Load(8)))
let sizes = crate::plan::load_plan(&g)?;
let bd = crate::bounds::step8(&g, sizes.as_deref(), p)?;
let acc32 = bd.meets(&g, Profile::Acc32);
Ok(Loaded { g, acc32 })
}
#[cfg(test)]
@ -1067,18 +1075,22 @@ mod tests {
assert_eq!(Graph::parse(&b).unwrap().to_bytes(), b);
}
/// Without the `uncertified` switch, load step 8 refuses every graph: nothing
/// decodes through an uncertified graph outside the tests and the tool.
#[cfg(not(feature = "uncertified"))]
/// Load step 8: the graph loads under both profiles; with an input range past
/// i32 it loads under `acc64` only; with a `check` range that its producer cannot
/// meet, under neither.
#[test]
fn step_8_refuses_every_graph_until_the_certifier_exists() {
let r = load(&tiny().to_bytes()).unwrap_err();
assert_eq!(r, Refusal::new(Code::ModelMismatch, crate::code::Site::Load(8)));
}
#[cfg(feature = "uncertified")]
#[test]
fn the_switch_skips_step_8() {
assert!(load(&tiny().to_bytes()).is_ok());
fn step_8_applies_the_profile() {
let g = tiny();
let l = load(&g.to_bytes(), Profile::Acc32).unwrap();
assert!(l.meets(Profile::Acc32) && l.meets(Profile::Acc64));
let mut wide = tiny();
wide.tensors[0] = TensorRec::check(1, -5, 1 << 40);
assert_eq!(load(&wide.to_bytes(), Profile::Acc32).unwrap_err(), Refusal::new(Code::ModelMismatch, crate::code::Site::Load(8)));
assert!(!load(&wide.to_bytes(), Profile::Acc64).unwrap().meets(Profile::Acc32));
let mut empty = tiny();
empty.tensors[1] = TensorRec::check(1, -9, -1);
for p in Profile::ALL {
assert_eq!(load(&empty.to_bytes(), p).unwrap_err(), Refusal::new(Code::ModelMismatch, crate::code::Site::Load(8)));
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -8,27 +8,32 @@
//! - [`interp`]: the reference interpreter of the graph operators (§5).
//! - [`stage`]: the stage operators E1–E13 (§5E).
//! - [`lower`]: the canonical lowerings of §13.
//! - [`limb32`]: a reference model of a 32-bit fabric's `acc32` products and epilogue.
//! - [`bounds`]: load step 8, the bounds pass (§8.1–§8.3), the verdict and the bounds
//! certificate (§8.5), and admissible ranges (§8.4).
//! - [`vectors`]: the conformance vector files of §14.1, recomputed from their inputs.
//! - [`generate`]: the generator of `../ops-conformance/`, which writes those files.
//!
//! Every value is an exact integer. Nothing here uses floating point, wraps, or
//! depends on the target's word size.
//! Every value is an exact integer. Nothing here uses floating point or depends on the
//! target's word size, and nothing wraps except in [`limb32`], whose wrapping 32-bit
//! adds are the fabric's it models, and are proved to give the exact values.
//!
//! **Load step 8**, the bounds pass and the profile verdict (§8, §10.5 step 8), is
//! not built yet. Without the `uncertified` feature a graph never completes its load;
//! with it, step 8 is skipped. See [`graph::load`]. The interpreter runs only a
//! [`Loaded`] graph, which only `load` makes, so no entry point decodes past step 8.
//! A graph loads under a profile ([`graph::load`]): load step 8 runs the bounds pass
//! and refuses a graph whose verdict does not meet that profile (§8.6). The
//! interpreter runs only a [`Loaded`] graph, which only `load` makes, so no entry
//! point decodes a graph its certificate does not admit.
#![no_std]
#![forbid(unsafe_code)]
extern crate alloc;
pub mod bounds;
pub mod code;
#[cfg(feature = "uncertified")]
pub mod generate;
pub mod graph;
pub mod interp;
pub mod json;
pub mod limb32;
pub mod lower;
pub mod plan;
pub mod sha256;
@ -36,6 +41,12 @@ pub mod stage;
pub mod vectors;
pub mod xint;
// The model checker's harnesses (K1–K33 of the test plan) are in `src/proofs/`, behind
// `#[cfg(kani)]`. `wai-verify` hosts them: it includes this crate's modules by
// `#[path]`, beside the `wai-rs` sources one of them is checked against, so they are
// compiled there and not here.
pub use bounds::Profile;
pub use code::{Code, Refusal, Site};
pub use graph::{Graph, Loaded};
pub use interp::{Sequence, Tensor, run};

201
wai/wai-ops/src/limb32.rs Normal file
View file

@ -0,0 +1,201 @@
//! The arithmetic of a 32-bit integer fabric for `acc32`'s two `product`
//! intermediates (§7.1, §7.3): a signed 32 × 32-bit product formed from 16-bit halves
//! in unsigned 32-bit arithmetic that wraps, and the requantisation epilogue on that
//! two-word value — the rounding shift, the `post` bias, and the narrowing to `i32`.
//!
//! This is a reference model for a shader. It uses only what a 32-bit integer shading
//! language has: `u32` addition, subtraction and multiplication that wrap, bitwise
//! operations, comparisons, and shifts whose amount is taken modulo 32. So every shift
//! by 32 or more is handled explicitly (§6), and a signed value is a `u32`'s bits read
//! as `i32`. Kani's K2 and K2b check it against the definitions; the unit tests below
//! check it at the edges.
use crate::graph::Round;
/// A 64-bit two's-complement value as two 32-bit words.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct W64 {
pub hi: u32,
pub lo: u32,
}
impl W64 {
pub const fn from_i64(v: i64) -> W64 {
W64 { hi: (v >> 32) as u32, lo: v as u32 }
}
pub const fn to_i64(self) -> i64 {
(((self.hi as u64) << 32) | self.lo as u64) as i64
}
}
// Shifts as a 32-bit shading language performs them: the amount is taken mod 32.
fn shl(x: u32, k: u32) -> u32 {
x.wrapping_shl(k)
}
fn shr(x: u32, k: u32) -> u32 {
x.wrapping_shr(k)
}
/// An arithmetic shift right of a word read as `i32`.
fn sar(x: u32, k: u32) -> u32 {
(x as i32).wrapping_shr(k) as u32
}
/// `a + b`, modulo 2^64.
pub fn add(a: W64, b: W64) -> W64 {
let lo = a.lo.wrapping_add(b.lo);
let carry = (lo < a.lo) as u32;
W64 { hi: a.hi.wrapping_add(b.hi).wrapping_add(carry), lo }
}
/// `−a`, modulo 2^64.
pub fn neg(a: W64) -> W64 {
add(W64 { hi: !a.hi, lo: !a.lo }, W64 { hi: 0, lo: 1 })
}
/// The products of the 16-bit halves of `a = a1·2^16 + a0` and `b = b1·2^16 + b0`:
/// `[a0·b0, a0·b1, a1·b0, a1·b1]`. Each is at most `(2^16 − 1)² < 2^32`, so none wraps.
pub fn partials(a: u32, b: u32) -> [u32; 4] {
let (a0, a1, b0, b1) = (a & 0xFFFF, a >> 16, b & 0xFFFF, b >> 16);
[a0 * b0, a0 * b1, a1 * b0, a1 * b1]
}
/// `p00 + (p01 + p10)·2^16 + p11·2^32` as two words, with the carries of 32-bit adds.
pub fn combine(p: [u32; 4]) -> W64 {
let [p00, p01, p10, p11] = p;
let mid = p01.wrapping_add(p10);
let c1 = (mid < p01) as u32;
let mid2 = mid.wrapping_add(p00 >> 16);
let c2 = (mid2 < mid) as u32;
let lo = shl(mid2, 16) | (p00 & 0xFFFF);
let hi = p11.wrapping_add(shr(mid2, 16)).wrapping_add(shl(c1 + c2, 16));
W64 { hi, lo }
}
/// The unsigned product of two 32-bit values.
pub fn mul_u32(a: u32, b: u32) -> W64 {
combine(partials(a, b))
}
/// `|a|` as a `u32`: `2^31` for `a = −2^31`.
pub fn magnitude(a: i32) -> u32 {
if a < 0 { (a as u32).wrapping_neg() } else { a as u32 }
}
/// The signed product of two `i32`, `rq.p` under `acc32`: the product of the
/// magnitudes, negated when exactly one operand is negative.
pub fn mul_i32(a: i32, b: i32) -> W64 {
let m = mul_u32(magnitude(a), magnitude(b));
if (a < 0) != (b < 0) { neg(m) } else { m }
}
/// `2^k` as two words, for `k` in `0..=63` (`2^63` is the bit pattern of `−2^63`).
fn pow2(k: u32) -> W64 {
if k < 32 { W64 { hi: 0, lo: shl(1, k) } } else { W64 { hi: shl(1, k - 32), lo: 0 } }
}
/// `⌊p / 2^s⌋`, for `s` in `0..=63`.
pub fn floor_shift(p: W64, s: u32) -> W64 {
if s == 0 {
p
} else if s < 32 {
W64 { hi: sar(p.hi, s), lo: shr(p.lo, s) | shl(p.hi, 32 - s) }
} else {
W64 { hi: sar(p.hi, 31), lo: sar(p.hi, s - 32) }
}
}
/// `R_round(p, s)` (§2) on two words, for `s` in `0..=63`. `half_up` forms
/// `p + 2^(s−1)`, which `rq.p`'s interval holds, so under `acc32` it fits i64.
pub fn round(p: W64, s: u32, r: Round) -> W64 {
if s == 0 {
return p;
}
match r {
Round::Floor => floor_shift(p, s),
Round::HalfUp => floor_shift(add(p, pow2(s - 1)), s),
Round::HalfEven => {
let q = floor_shift(p, s);
// The remainder p − q·2^s is p's low s bits; it and h = 2^(s−1) are
// non-negative and below 2^63, so they compare as unsigned pairs.
let mask = add(pow2(s), W64 { hi: u32::MAX, lo: u32::MAX });
let rem = W64 { hi: p.hi & mask.hi, lo: p.lo & mask.lo };
let h = pow2(s - 1);
let above = rem.hi > h.hi || (rem.hi == h.hi && rem.lo > h.lo);
if above || (rem == h && q.lo & 1 == 1) { add(q, W64 { hi: 0, lo: 1 }) } else { q }
}
}
}
/// The requantisation epilogue on a product `p`, for `s` in `0..=63`:
/// `R_round(p, s) + post`, narrowed to `i32`. The bias is added on two words, so the
/// result is exact whenever `rq.y` fits i32, which `acc32` certifies.
pub fn epilogue(p: W64, s: u32, r: Round, post: i32) -> i32 {
let y = add(round(p, s, r), W64::from_i64(post as i64));
y.lo as i32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interp;
const EDGES: [i32; 12] = [0, 1, -1, 2, -2, 0xFFFF, 0x10000, -0x10000, i32::MAX, i32::MIN, i32::MAX - 1, i32::MIN + 1];
/// Whether `rq.p`'s second value, `p + 2^(s−1)` under `half_up`, fits i64, as `acc32`
/// requires of every product it certifies.
fn addend_fits(p: i64, s: u32, r: Round) -> bool {
r != Round::HalfUp || s == 0 || p as i128 + (1i128 << (s - 1)) <= i64::MAX as i128
}
fn lcg(s: &mut u64) -> u64 {
*s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*s >> 16
}
#[test]
fn product_matches_i64() {
for &a in &EDGES {
for &b in &EDGES {
assert_eq!(mul_i32(a, b).to_i64(), a as i64 * b as i64, "{a} × {b}");
}
}
let mut s = 1u64;
for _ in 0..200_000 {
let (a, b) = (lcg(&mut s) as i32, lcg(&mut s) as i32);
assert_eq!(mul_i32(a, b).to_i64(), a as i64 * b as i64, "{a} × {b}");
}
}
#[test]
fn epilogue_matches_the_definitions() {
let mut s = 7u64;
let rounds = [Round::Floor, Round::HalfUp, Round::HalfEven];
for k in 0..400_000u64 {
let (a, b) = (lcg(&mut s) as i32, lcg(&mut s) as i32);
let p = a as i64 * b as i64;
let sh = (lcg(&mut s) % 64) as u32;
let r = rounds[(k % 3) as usize];
let post = (lcg(&mut s) as i32) >> (lcg(&mut s) % 32);
if !addend_fits(p, sh, r) {
continue;
}
let q = interp::round(p as i128, sh, r).unwrap();
let y = q + post as i128;
if i32::try_from(y).is_ok() {
assert_eq!(epilogue(mul_i32(a, b), sh, r, post) as i128, y, "{a} × {b}, s {sh}, {r:?}, post {post}");
}
}
// The ties of each rounding, at every shift.
for sh in 1..64u32 {
for &r in &rounds {
for &p in &[3i64.wrapping_shl(sh - 1), -(3i64.wrapping_shl(sh - 1)), 1i64 << (sh - 1), -(1i64 << (sh - 1))] {
if !addend_fits(p, sh, r) {
continue;
}
let q = interp::round(p as i128, sh, r).unwrap();
assert_eq!(round(W64::from_i64(p), sh, r).to_i64() as i128, q, "p {p}, s {sh}, {r:?}");
}
}
}
}
}

View file

@ -17,6 +17,8 @@ mod whm;
pub use jpegai::jpegai;
pub use synth::int_synth;
pub use whm::{Net, NetLayer, int_hyper, parse_wam1, parse_whm1, splat, wam1, whm1, wmb1, world};
#[cfg(kani)]
pub(crate) use whm::{p_conv, p_igdn, p_leaky};
/// A lowered set: its graphs by role, in §13.0's order.
#[derive(Clone, Debug, PartialEq, Eq)]

View file

@ -138,41 +138,52 @@ fn maxabs(v: &[i64]) -> I512 {
const I64: I512 = I512::from_i128(i64::MAX as i128);
const I128: I512 = I512::from_i128(i128::MAX);
/// `P_ℓ(X)` for a layer of tag 0 or 1 (§13.2): `c·k·k·Wm·X ≤ I64` and
/// `⌊c·k·k·Wm·X·Mm / 2^shift⌋ + 1 + Bm ≤ I64`, with `terms = c·k·k` (`c·k` for `WAM1`).
pub(crate) fn p_conv(terms: I512, wm: I512, mm: I512, bm: I512, shift: u32, x: u128) -> bool {
let a = terms.mul(wm).mul(I512::from_u128(x));
a <= I64 && a.mul(mm).shr_floor(shift).add(I512::ONE).add(bm) <= I64
}
/// `P_ℓ(X)` for a layer of tag 2 (§13.2): `G·X² ≤ I128`;
/// `N = Bb + c·(⌊G·X²/2^gf⌋ + 1) ≤ I128`; `isqrt(N) ≤ I64`;
/// `⌊X·isqrt(N) / 2^afrac⌋ + 1 ≤ I64`.
pub(crate) fn p_igdn(g: I512, bb: I512, cc: I512, gf: u32, af: u32, x: u128) -> bool {
let x = I512::from_u128(x);
let gx2 = g.mul(x).mul(x);
if gx2 > I128 {
return false;
}
let n = bb.add(cc.mul(gx2.shr_floor(gf).add(I512::ONE)));
if n > I128 {
return false;
}
let r = n.isqrt();
r <= I64 && x.mul(r).shr_floor(af).add(I512::ONE) <= I64
}
/// `P_ℓ(X)` for a layer of tag 4 (§13.2): `X·|slope_q| ≤ I64`.
pub(crate) fn p_leaky(a: I512, x: u128) -> bool {
I512::from_u128(x).mul(a) <= I64
}
/// `X_ℓ` for a layer reading `c` channels (§13.2), or `None` for a layer no `check`
/// range precedes (a ReLU). Refuses when `P_ℓ(0)` fails.
fn x_of(net: &Net, l: &NetLayer, c: u32) -> Res<Option<u128>> {
let le = |a: I512, b: I512| a <= b;
let x = match l {
NetLayer::ConvT { k, w, mult, bias, .. } | NetLayer::Conv { k, w, mult, bias, .. } => {
let kk = if net.audio { *k as u64 } else { *k as u64 * *k as u64 };
let terms = I512::from_u64(c as u64).mul(I512::from_u64(kk));
let (wm, mm, bm) = (maxabs(w), maxabs(mult), maxabs(bias));
let s = net.shift;
largest(|x| {
let a = terms.mul(wm).mul(I512::from_u128(x));
le(a, I64) && le(a.mul(mm).shr_floor(s).add(I512::ONE).add(bm), I64)
})
largest(|x| p_conv(terms, wm, mm, bm, net.shift, x))
}
NetLayer::Igdn { gamma, beta, .. } => {
let (g, bb, cc) = (maxabs(gamma), maxabs(beta), I512::from_u64(c as u64));
let (gf, af) = (net.gf, net.afrac);
largest(|x| {
let x = I512::from_u128(x);
let gx2 = g.mul(x).mul(x);
if !le(gx2, I128) {
return false;
}
let n = bb.add(cc.mul(gx2.shr_floor(gf).add(I512::ONE)));
if !le(n, I128) {
return false;
}
let r = n.isqrt();
le(r, I64) && le(x.mul(r).shr_floor(af).add(I512::ONE), I64)
})
largest(|x| p_igdn(g, bb, cc, net.gf, net.afrac, x))
}
NetLayer::Leaky { slope_q, .. } => {
let a = I512::from_i128((*slope_q as i128).abs());
largest(|x| le(I512::from_u128(x).mul(a), I64))
largest(|x| p_leaky(a, x))
}
NetLayer::Relu => return Ok(None),
};

View file

@ -30,8 +30,8 @@ enum Mode<'a> {
/// The sizes `(H, W)` of every tensor, indexed by tensor.
pub type Sizes = Vec<(u64, u64)>;
#[derive(Clone, Copy)]
enum Fail {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Fail {
Big,
Shape,
}
@ -46,6 +46,58 @@ fn capped(v: u64) -> Result<u64, Fail> {
if v > CAP { Err(Fail::Big) } else { Ok(v) }
}
/// `conv`'s output sides (§5.2): the padded sides are capped first.
#[allow(clippy::too_many_arguments)]
pub(crate) fn conv_sides(h: u64, w: u64, kh: u64, kw: u64, sh: u64, sw: u64, ph: u64, pw: u64) -> Result<(u64, u64), Fail> {
let hp = capped(add(h, mul(2, ph)?)?)?;
let wp = capped(add(w, mul(2, pw)?)?)?;
if hp < kh || wp < kw {
return Err(Fail::Shape);
}
Ok(((hp - kh) / sh + 1, (wp - kw) / sw + 1))
}
/// `conv_transpose`'s output sides (§5.3): the uncropped sides and the uncropped
/// output of `cout` planes are capped first.
#[allow(clippy::too_many_arguments)]
pub(crate) fn conv_t_sides(h: u64, w: u64, cout: u64, kh: u64, kw: u64, sh: u64, sw: u64, ph: u64, pw: u64, oph: u64, opw: u64) -> Result<(u64, u64), Fail> {
let uh = capped(add(add(mul(h - 1, sh)?, kh)?, oph)?)?;
let uw = capped(add(add(mul(w - 1, sw)?, kw)?, opw)?)?;
capped(mul(mul(cout, uh)?, uw)?)?;
let (ch2, cw2) = (2 * ph, 2 * pw);
if uh <= ch2 || uw <= cw2 {
return Err(Fail::Shape);
}
Ok((uh - ch2, uw - cw2))
}
/// `pixel_shuffle`'s output sides (§5.10).
pub(crate) fn shuffle_sides(h: u64, w: u64, r: u64) -> Result<(u64, u64), Fail> {
Ok((capped(mul(h, r)?)?, capped(mul(w, r)?)?))
}
/// `pixel_unshuffle`'s output sides (§5.10).
pub(crate) fn unshuffle_sides(h: u64, w: u64, r: u64) -> Result<(u64, u64), Fail> {
if h % r != 0 || w % r != 0 {
return Err(Fail::Shape);
}
Ok((h / r, w / r))
}
/// `pad_rep`'s output sides (§5.10).
pub(crate) fn pad_rep_sides(h: u64, w: u64, m: u64) -> Result<(u64, u64), Fail> {
Ok((capped(mul(h.div_ceil(m), m)?)?, capped(mul(w.div_ceil(m), m)?)?))
}
/// `crop`'s output side along one axis at run time (§5.10): the start `s0` and the
/// length `l` against the input's side `n`.
pub(crate) fn crop_side(s0: i128, l: i128, n: u64) -> Result<u64, Fail> {
if s0 < 0 || l < 1 || s0 + l > n as i128 {
return Err(Fail::Shape);
}
Ok(l as u64)
}
/// One operator's output sizes from its inputs' sizes.
fn op_sizes(g: &Graph, op: &crate::graph::Op, hw: &[(u64, u64)], mode: Mode) -> Result<Vec<(u64, u64)>, Fail> {
let (h, w) = hw[op.ins[0] as usize];
@ -54,22 +106,12 @@ fn op_sizes(g: &Graph, op: &crate::graph::Op, hw: &[(u64, u64)], mode: Mode) ->
let shape = Err(Fail::Shape);
Ok(match (&op.p, op.code) {
(Params::Conv { kh, kw, sh, sw, ph, pw, .. }, _) => {
let hp = capped(add(h, mul(2, *ph as u64)?)?)?;
let wp = capped(add(w, mul(2, *pw as u64)?)?)?;
if hp < *kh as u64 || wp < *kw as u64 {
return shape;
}
vec![((hp - *kh as u64) / *sh as u64 + 1, (wp - *kw as u64) / *sw as u64 + 1)]
let u = |v: &u32| *v as u64;
vec![conv_sides(h, w, u(kh), u(kw), u(sh), u(sw), u(ph), u(pw))?]
}
(Params::ConvT { cout, kh, kw, sh, sw, ph, pw, oph, opw, .. }, _) => {
let uh = capped(add(add(mul(h - 1, *sh as u64)?, *kh as u64)?, *oph as u64)?)?;
let uw = capped(add(add(mul(w - 1, *sw as u64)?, *kw as u64)?, *opw as u64)?)?;
capped(mul(mul(*cout as u64, uh)?, uw)?)?;
let (ch2, cw2) = (2 * *ph as u64, 2 * *pw as u64);
if uh <= ch2 || uw <= cw2 {
return shape;
}
vec![(uh - ch2, uw - cw2)]
let u = |v: &u32| *v as u64;
vec![conv_t_sides(h, w, u(cout), u(kh), u(kw), u(sh), u(sw), u(ph), u(pw), u(oph), u(opw))?]
}
(_, ADD) | (_, MUL) => {
if !same(hw[op.ins[1] as usize]) {
@ -83,21 +125,9 @@ fn op_sizes(g: &Graph, op: &crate::graph::Op, hw: &[(u64, u64)], mode: Mode) ->
}
vec![(h, w)]
}
(Params::Shuffle { r }, PIXEL_SHUFFLE) => {
let r = *r as u64;
vec![(capped(mul(h, r)?)?, capped(mul(w, r)?)?)]
}
(Params::Shuffle { r }, _) => {
let r = *r as u64;
if h % r != 0 || w % r != 0 {
return shape;
}
vec![(h / r, w / r)]
}
(Params::PadRep { m }, _) => {
let m = *m as u64;
vec![(capped(mul(h.div_ceil(m), m)?)?, capped(mul(w.div_ceil(m), m)?)?)]
}
(Params::Shuffle { r }, PIXEL_SHUFFLE) => vec![shuffle_sides(h, w, *r as u64)?],
(Params::Shuffle { r }, _) => vec![unshuffle_sides(h, w, *r as u64)?],
(Params::PadRep { m }, _) => vec![pad_rep_sides(h, w, *m as u64)?],
(_, CROP_TO) => {
let (rh, rw) = hw[op.ins[1] as usize];
if rh > h || rw > w {
@ -115,11 +145,7 @@ fn op_sizes(g: &Graph, op: &crate::graph::Op, hw: &[(u64, u64)], mode: Mode) ->
Operand::Scalar(s) => sv[s as usize] as i128,
}
};
let (s0, l) = (v(start), v(len));
if s0 < 0 || l < 1 || s0 + l > n as i128 {
return Err(Fail::Shape);
}
Ok(l as u64)
crop_side(v(start), v(len), n)
}
Mode::Load => {
// The output is planned on the largest length the scalar's range

View file

@ -0,0 +1,371 @@
//! The integer functions of §2 and the requantisation of §5.1: K1, K3, K13, K17 and
//! K32 (the 512-bit arithmetic of the bounds pass).
use alloc::vec;
use super::{Rec, any_round};
use crate::graph::{BiasKind, Round, Rq};
use crate::interp::{Observe, exp_observed, round, rq_apply};
use crate::xint::I512;
/// Whether `q = ⌊t / 2^s⌋` for `s ≤ 127`: `q·2^s` is formed with no bit lost, and
/// `0 ≤ t − q·2^s < 2^s`.
fn is_floor(q: i128, t: i128, s: u32) -> bool {
let qs = q << s;
if qs >> s != q {
return false;
}
match t.checked_sub(qs) {
Some(rem) => rem >= 0 && (rem as u128) < (1u128 << s),
None => false,
}
}
/// K3. `R_floor`, `R_half_up` and `R_half_even` (§2) each equal their definition, for
/// every `p` in `i128` and every shift `s` in `0..=127` (the range `rq.shift`,
/// `chscale`'s `s`, `scale_index` and `block_stat` allow). The reference is the
/// defining inequality of the floor division, `q·2^s ≤ t < (q + 1)·2^s`, checked with
/// no value wrapping. `R_half_up` forms `p + 2^(s−1)`; it returns `None` exactly when
/// that value leaves `i128`, which a certified graph never forms (§8.3, `rq.p`).
#[kani::proof]
fn k03_rounding_is_its_definition() {
let p: i128 = kani::any();
let s: u32 = kani::any();
kani::assume(s <= 127);
let r = any_round();
let got = round(p, s, r);
if s == 0 {
assert_eq!(got, Some(p), "a shift of 0 leaves p as it is");
return;
}
let h = 1i128 << (s - 1);
match r {
Round::Floor => {
let q = got.expect("R_floor is formed for every p");
assert!(is_floor(q, p, s), "R_floor(p, s) = ⌊p / 2^s⌋");
}
Round::HalfUp => match p.checked_add(h) {
Some(t) => {
let q = got.expect("formed whenever p + 2^(s−1) fits i128");
assert!(is_floor(q, t, s), "R_half_up(p, s) = ⌊(p + 2^(s−1)) / 2^s⌋");
}
None => assert!(got.is_none(), "no result only when p + 2^(s−1) leaves i128"),
},
Round::HalfEven => {
let q = got.expect("R_half_even is formed for every p");
// q0 = ⌊p / 2^s⌋, r = p − q0·2^s, h = 2^(s−1): q0 + 1 if r > h, or r = h and
// q0 odd; else q0.
let q0 = p >> s;
assert!(is_floor(q0, p, s));
let rem = (p - (q0 << s)) as u128;
let h = h as u128;
let want = if rem > h || (rem == h && q0 & 1 == 1) { q0 + 1 } else { q0 };
assert_eq!(q, want, "R_half_even(p, s) as §2 defines it");
}
}
}
/// K3. Each rounding is non-decreasing in `p` (§2; §8.3's `rq.q` rule relies on it),
/// for every `p₁ ≤ p₂` in `i128` and every shift in `0..=127`. `R_half_up`'s `None`
/// (its addend leaves `i128`) is monotone too: it never follows a larger `p` that had a
/// result.
#[kani::proof]
fn k03_rounding_is_monotone() {
let a: i128 = kani::any();
let b: i128 = kani::any();
kani::assume(a <= b);
let s: u32 = kani::any();
kani::assume(s <= 127);
let r = any_round();
let (ra, rb) = (round(a, s, r), round(b, s, r));
if let Some(qb) = rb {
let qa = ra.expect("a smaller p has a result whenever a larger one does");
assert!(qa <= qb, "non-decreasing in p");
}
}
// ------------------------------------------------------------------------------- K1
/// K1. `rq` (§5.1), in every rounding mode and every bias placement, equals its
/// definition: `t = v + pre`, `p = t·m`, `q = R(p, s)`, `y = q + post`, each formed
/// exactly. Window: `v`, `m` and the bias are any `i64`, `m` may be absent, and `s` is
/// any shift in `0..=127`.
///
/// The harness reads the values the implementation forms ([`Rec`]): `rq.t`, then
/// `rq.p` (and `p + 2^(s−1)` under `half_up` and `half_even`), `rq.q` and `rq.y`. It
/// shows that `t` is `v + pre`; that `q` is `R(p, s)` by the defining inequality; that
/// `y` is `q + post` with `q` at full width (the cover reaches a `q` outside `i64` with a
/// `y` inside it); that the result is `y`; and that the implementation refuses, and
/// never wraps, exactly where a value leaves `i128`: the product (Rust's checked
/// multiplication, exact or `None` by the language's definition), the addend, or the
/// bias. Nothing panics.
#[kani::proof]
fn k01_rq_is_its_definition() {
let v: i64 = kani::any();
let mult: Option<i64> = if kani::any() { Some(kani::any()) } else { None };
let s: u8 = kani::any();
kani::assume(s <= 127);
let r = any_round();
let b: i64 = kani::any();
let bk: u8 = kani::any();
kani::assume(bk < 3);
let bias_kind = [BiasKind::None, BiasKind::Pre, BiasKind::Post][bk as usize];
let rq = Rq {
round: r,
bias_kind,
m_kind: if mult.is_some() { 1 } else { 0 },
s_kind: 1,
m_bytes: if mult.is_some() { 8 } else { 0 },
b_bytes: if bias_kind == BiasKind::None { 0 } else { 8 },
mult: mult.into_iter().collect(),
shift: vec![s],
bias: if bias_kind == BiasKind::None { vec![] } else { vec![b] },
};
let mut rec = Rec::new();
let got = rq_apply(&rq, 0, v as i128, 0, &mut rec);
let s = s as u32;
let pre = if bias_kind == BiasKind::Pre { b as i128 } else { 0 };
let post = if bias_kind == BiasKind::Post { b as i128 } else { 0 };
assert!(rec.n >= 1 && rec.v[0] == v as i128 + pre, "rq.t = v + pre, exactly");
if rec.n == 1 {
assert!(mult.is_some() && got.is_none(), "only a product past i128 stops here, refused");
return;
}
let p = rec.v[1];
if mult.is_none() {
assert_eq!(p, rec.v[0], "with no multiplier, p = t");
}
let half = r != Round::Floor && s >= 1;
let addend = if half { p.checked_add(1i128 << (s - 1)) } else { None };
let k = if addend.is_some() { 3 } else { 2 };
if let Some(a) = addend {
assert_eq!(rec.v[2], a, "rq.p's second value is p + 2^(s−1)");
}
if rec.n == k {
assert!(got.is_none(), "no rq.q only when it is refused");
assert!(half && addend.is_none() && r == Round::HalfUp, "only half_up's addend past i128 stops here");
return;
}
let q = rec.v[k];
match r {
_ if s == 0 => assert_eq!(q, p),
Round::Floor => assert!(is_floor(q, p, s), "R_floor"),
Round::HalfUp => assert!(is_floor(q, addend.expect("formed"), s), "R_half_up"),
Round::HalfEven => {
let q0 = p >> s;
assert!(is_floor(q0, p, s));
let rem = (p - (q0 << s)) as u128;
let h = 1u128 << (s - 1);
assert_eq!(q, if rem > h || (rem == h && q0 & 1 == 1) { q0 + 1 } else { q0 }, "R_half_even");
}
}
match q.checked_add(post) {
None => assert!(got.is_none() && rec.n == k + 1, "a y past i128 is refused, never wrapped"),
Some(y) => {
assert!(rec.n == k + 2 && rec.v[k + 1] == y, "rq.y = q + post");
assert_eq!(got, Some(y));
kani::cover!(i64::try_from(q).is_err() && i64::try_from(y).is_ok(), "q outside i64 with y inside it: q is held at full width");
}
}
}
// ------------------------------------------------------------------------------ K13
/// K13. `isqrt` (§2) as the interpreter forms it, the standard library's `u128::isqrt`
/// (`igdn`'s `r`, `rsqrt_q16`'s `r`), is `⌊√n⌋`: `r² ≤ n < (r + 1)²`. Window: every
/// `n` below 2^32, which reaches the `u32` stages through the `u128` and `u64` entry
/// points; the stages for wider values divide by symbolic values, and at 2^64 the
/// check had not closed in 20 minutes.
#[kani::proof]
fn k13_isqrt_u128_is_floor_sqrt() {
let n: u32 = kani::any();
let n = n as u128;
let r = n.isqrt();
assert!(r * r <= n, "r² ≤ n");
assert!(n < (r + 1) * (r + 1), "n < (r + 1)²");
}
/// K13. `isqrt` (§2) as the bounds pass forms it, in 512 bits (`igdn.r`, `r0` of
/// `layernorm_c` and `l2norm`): `r² ≤ n < (r + 1)²`. The digit-by-digit loop runs
/// the same code at every width, one iteration per two bits. Window: every `n` below
/// 2^32 (the loop's 16 iterations; at 2^64 every 512-bit operation of its 32
/// iterations had not finished symbolic execution in 21 minutes).
#[kani::proof]
#[kani::unwind(20)]
fn k13_isqrt_i512_is_floor_sqrt() {
let n: u32 = kani::any();
let r = I512::from_u64(n as u64).isqrt().to_i128().expect("isqrt(n) < 2^16") as u128;
let n = n as u128;
assert!(r * r <= n, "r² ≤ n");
assert!(n < (r + 1) * (r + 1), "n < (r + 1)²");
}
/// K13. `isqrt_ceil` (§2) in 512 bits (lemma L1's `isqrt_ceil(C)`) is the smallest `r`
/// with `r² ≥ n`. Window: every `n` below 2^32.
#[kani::proof]
#[kani::unwind(20)]
fn k13_isqrt_ceil_i512_is_ceil_sqrt() {
let n: u32 = kani::any();
let c = I512::from_u64(n as u64).isqrt_ceil().to_i128().expect("isqrt_ceil(n) ≤ 2^16") as u128;
let n = n as u128;
assert!(c * c >= n, "isqrt_ceil(n)² ≥ n");
assert!(c == 0 || (c - 1) * (c - 1) < n, "and it is the smallest such r");
}
// ------------------------------------------------------------------------------ K17
/// The values an observed run reports, in the order it reports them.
struct Seen {
v: [i128; 4],
n: usize,
}
impl Observe for Seen {
fn see(&mut self, _op: u32, _key: &'static str, v: i128) {
if self.n < 4 {
self.v[self.n] = v;
}
self.n += 1;
}
}
/// K17. `exp_neg_q16(x)` (§2) for every `x ∈ [−(2^63 − 1), 0]`: the result lies in
/// `[0, 65553]`, and the four values §2 names, as the interpreter forms them, lie in
/// the intervals §8.3's `exp_neg_q16` rule records — `exp.negx = −x`,
/// `exp.nl = 45426·⌊u/45426⌋` (so `0 ≤ u − exp.nl < 45426`),
/// `exp.t2 ∈ [43245², 88670²]` and `exp.c ∈ [0, 23495·⌊88670²/2^16⌋]` — each of which
/// fits `i64` (class `acc`), so nothing overflows a 64-bit implementation. At
/// `x = −2^63`, `exp.negx = 2^63` does not fit `i64`, so no certified graph forms it.
fn exp_ranges(x: i64) {
let mut seen = Seen { v: [0; 4], n: 0 };
let e = exp_observed(x as i128, 0, &mut seen);
assert_eq!(seen.n, 4, "exp.negx, exp.nl, exp.t2 and exp.c, once each");
let [u, nl, t2, c] = seen.v;
assert_eq!(u, -(x as i128), "exp.negx = −x");
assert!(u <= i64::MAX as i128, "exp.negx fits i64");
assert!(0 <= nl && nl <= u && u - nl < 45426, "exp.nl = 45426·⌊u/45426⌋");
assert!(43245 * 43245 <= t2 && t2 <= 88670 * 88670, "exp.t2 ∈ [43245², 88670²]");
assert!(0 <= c && c <= 23495 * ((88670 * 88670) >> 16), "exp.c ∈ [0, 23495·⌊88670²/2^16⌋]");
assert!((0..=65553).contains(&e), "the result lies in [0, 65553]");
kani::cover!(x == 0 && e == 65553, "exp_neg_q16(0) = 65553");
}
/// K17 on its window: every `x ∈ [−(2^63 − 1), 0]`.
#[cfg(kani_open)]
#[kani::proof]
fn k17_exp_neg_q16_ranges() {
let x: i64 = kani::any();
kani::assume(x <= 0 && x > i64::MIN);
exp_ranges(x);
}
/// K17 on `x ∈ [−(2^31 − 1), 0]`: the same statement. Past `x ≤ −45426·63` the
/// result is 0 whatever `x`, and every intermediate depends on `x` only through
/// `n = ⌊−x/45426⌋` and the remainder, which this window takes through every value.
#[kani::proof]
fn k17_exp_neg_q16_ranges_i32() {
let x: i32 = kani::any();
kani::assume(x <= 0 && x > i32::MIN);
exp_ranges(x as i64);
}
// ------------------------------------------------------------------------------ K32
const CAP: u32 = 255;
/// An operand at a saturation edge of §8.1: `±(2^255 − d)` for `d < 2^16`, or a small
/// value `w`.
#[derive(Clone, Copy)]
enum Edge {
Near { neg: bool, d: u16 },
Small(i64),
}
fn any_edge() -> Edge {
if kani::any() { Edge::Near { neg: kani::any(), d: kani::any() } } else { Edge::Small(kani::any()) }
}
fn value(e: Edge) -> I512 {
match e {
Edge::Near { neg, d } => {
let v = I512::pow2(CAP).sub(I512::from_u64(d as u64));
if neg { v.neg() } else { v }
}
Edge::Small(w) => I512::from_i64(w),
}
}
/// The product of two edge operands, built from shifts and additions of small values:
/// `(2^255 − d₁)(2^255 − d₂) = 2^510 − (d₁ + d₂)·2^255 + d₁·d₂`, and
/// `(2^255 − d)·w = w·2^255 − w·d`.
fn product(a: Edge, b: Edge) -> I512 {
match (a, b) {
(Edge::Near { neg: n1, d: d1 }, Edge::Near { neg: n2, d: d2 }) => {
let (d1, d2) = (d1 as u64, d2 as u64);
let m = I512::pow2(2 * CAP).sub(I512::from_u64(d1 + d2).shl(CAP)).add(I512::from_u64(d1 * d2));
if n1 != n2 { m.neg() } else { m }
}
(Edge::Near { neg, d }, Edge::Small(w)) | (Edge::Small(w), Edge::Near { neg, d }) => {
let m = I512::from_i64(w).shl(CAP).sub(I512::from_i128(w as i128 * d as i128));
if neg { m.neg() } else { m }
}
(Edge::Small(x), Edge::Small(y)) => I512::from_i128(x as i128 * y as i128),
}
}
fn le(a: I512, b: I512) -> bool {
a.cmp(&b) != core::cmp::Ordering::Greater
}
/// K32. The bounds pass's 512-bit arithmetic does not wrap on saturated operands
/// (§8.1, *Width*): for operands at the saturation edges, `add`, `sub`, `neg` and `abs`
/// give the exact values. Window: `d` below 2^16, and the small operand any `i64`.
#[kani::proof]
#[kani::unwind(10)]
fn k32_xint_add_sub_neg_exact_at_the_saturation_edges() {
let (ea, eb) = (any_edge(), any_edge());
let (a, b) = (value(ea), value(eb));
let c = I512::pow2(CAP);
// Every edge operand lies in [−2^255, 2^255], which `sat` leaves unchanged.
assert!(!a.sat().1 && !b.sat().1);
// add, sub, neg and abs: exact, so they invert, and the signs are those of the
// exact values (a wrap would flip one).
let s = a.add(b);
assert!(le(s.abs(), I512::pow2(CAP + 1)), "|a + b| ≤ 2^256");
assert!(s.sub(b) == a && s.sub(a) == b);
assert!(a.is_neg() || b.is_neg() || !s.is_neg(), "a sum of non-negatives is non-negative");
assert!(!a.is_neg() || !b.is_neg() || s.is_neg(), "a sum of negatives is negative");
assert!(a.neg().neg() == a);
assert!(le(a.abs(), c) && !a.abs().is_neg(), "|a| ≤ 2^255");
}
/// K32. The bounds pass's 512-bit product does not wrap on saturated operands: at the
/// saturation edges, `mul` gives `(2^255 − d₁)(2^255 − d₂) = 2^510 − (d₁ + d₂)·2^255 +
/// d₁·d₂` with the sign of the operands, and `±(2^255 − d)·w = ±(w·2^255 − w·d)`, and
/// `|a·b| ≤ 2^510`. Window: `d`, `d₁`, `d₂` below 2^16, `w` any `i64`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k32_xint_mul_exact_at_the_saturation_edges() {
let (ea, eb) = (any_edge(), any_edge());
let p = value(ea).mul(value(eb));
assert!(p == product(ea, eb), "the product is exact");
assert!(le(p.abs(), I512::pow2(2 * CAP)), "|a·b| ≤ 2^510");
}
/// K32. `sat` (§8.1) clamps to `[−2^255, 2^255]`, reports exactly when it changed a
/// value, and is non-decreasing. Window: every pair of 512-bit values.
#[kani::proof]
#[kani::unwind(10)]
fn k32_sat_is_a_monotone_clamp() {
let a = I512::from_limbs(kani::any());
let b = I512::from_limbs(kani::any());
let c = I512::pow2(CAP);
let (sa, ha) = a.sat();
assert!(sa.cmp(&c) != core::cmp::Ordering::Greater && sa.cmp(&c.neg()) != core::cmp::Ordering::Less);
assert!(ha == (sa != a), "the flag is set exactly when the value changed");
assert!(!ha || a.cmp(&c) == core::cmp::Ordering::Greater || a.cmp(&c.neg()) == core::cmp::Ordering::Less);
if a.cmp(&b) != core::cmp::Ordering::Greater {
assert!(sa.cmp(&b.sat().0) != core::cmp::Ordering::Greater, "sat is non-decreasing");
}
}

View file

@ -0,0 +1,138 @@
//! `conv_transpose` (§5.3): the interpreter's scatter form against the text's gather
//! form (K7) and the phase form of §9.3 (K8).
//!
//! Each harness runs the kernel at a set of concrete shapes, one call site per shape,
//! so the checker unrolls every loop exactly; weights, inputs, the bias, the rounding
//! and the shift are symbolic. The shapes span the window — kernels up to 3×3, strides
//! up to 3, padding up to 2, output padding up to 3, planes up to 3×3, one channel each
//! way — and include a stride larger than the kernel, where some positions no tap
//! reaches, and a non-square kernel and stride.
use alloc::vec;
use alloc::vec::Vec;
use super::any_round;
use crate::graph::{BiasKind, Rq};
use crate::interp::{self, Quiet, Tensor, round};
/// The kernel's output, and the operands, at one shape.
struct Run {
out: Tensor,
w: Vec<i64>,
x: Vec<i64>,
rq: Rq,
}
#[allow(clippy::too_many_arguments)]
fn run(kh: usize, kw: usize, sh: usize, sw: usize, ph: usize, pw: usize, oph: usize, opw: usize, hh: usize, ww: usize) -> Run {
let wraw: [i8; 9] = kani::any();
let w: Vec<i64> = wraw[..kh * kw].iter().map(|&v| v as i64).collect();
let xraw: [i8; 9] = kani::any();
let x: Vec<i64> = xraw[..hh * ww].iter().map(|&v| v as i64).collect();
let b: i8 = kani::any();
let s: u8 = kani::any();
kani::assume(s <= 4);
let rq = Rq { round: any_round(), bias_kind: BiasKind::Pre, m_kind: 0, s_kind: 1, m_bytes: 0, b_bytes: 4, mult: vec![], shift: vec![s], bias: vec![b as i64] };
let (oh, ow) = ((hh - 1) * sh + kh + oph - 2 * ph, (ww - 1) * sw + kw + opw - 2 * pw);
let out = interp::k_conv_t(&Tensor::new(1, hh as u32, ww as u32, x.clone()), 1, kh, kw, sh, sw, ph, pw, oph, opw, &rq, &w, oh, ow, 0, &mut Quiet).expect("i8 operands");
Run { out, w, x, rq }
}
/// `rq_0` of an accumulator, as §5.1 writes it for this block (no multiplier).
fn rq0(rq: &Rq, acc: i128) -> i128 {
round(acc + rq.bias[0] as i128, rq.shift[0] as u32, rq.round).expect("small values")
}
/// §5.3's gather form at a symbolic output position (so at every one), against the
/// kernel.
#[allow(clippy::too_many_arguments)]
fn gather(kh: usize, kw: usize, sh: usize, sw: usize, ph: usize, pw: usize, oph: usize, opw: usize, hh: usize, ww: usize) {
let r = run(kh, kw, sh, sw, ph, pw, oph, opw, hh, ww);
let (oh, ow) = (r.out.h as usize, r.out.w as usize);
let (y, xo): (usize, usize) = (kani::any(), kani::any());
kani::assume(y < oh && xo < ow);
{
{
let mut acc: i128 = 0;
for i in 0..kh {
for j in 0..kw {
let (ty, tx) = ((y + ph) as i64 - i as i64, (xo + pw) as i64 - j as i64);
if ty < 0 || tx < 0 || ty % sh as i64 != 0 || tx % sw as i64 != 0 {
continue;
}
let (u, v) = ((ty / sh as i64) as usize, (tx / sw as i64) as usize);
if u >= hh || v >= ww {
continue;
}
acc += r.w[i * kw + j] as i128 * r.x[u * ww + v] as i128;
}
}
assert_eq!(r.out.data[y * ow + xo] as i128, rq0(&r.rq, acc), "gather = scatter, then crop");
}
}
}
/// §9.3's phase form at a symbolic output position (so at every one), against the
/// kernel.
#[allow(clippy::too_many_arguments)]
fn phase(kh: usize, kw: usize, sh: usize, sw: usize, ph: usize, pw: usize, oph: usize, opw: usize, hh: usize, ww: usize) {
let r = run(kh, kw, sh, sw, ph, pw, oph, opw, hh, ww);
let (oh, ow) = (r.out.h as usize, r.out.w as usize);
let (y, xo): (usize, usize) = (kani::any(), kani::any());
kani::assume(y < oh && xo < ow);
{
{
let (ry, rx) = ((y + ph) % sh, (xo + pw) % sw);
let (u, v) = (((y + ph) / sh) as i64, ((xo + pw) / sw) as i64);
let mut z: i128 = 0;
let mut t = 0;
while ry + sh * t < kh {
let mut t2 = 0;
while rx + sw * t2 < kw {
let (iy, ix) = (u - t as i64, v - t2 as i64);
if iy >= 0 && ix >= 0 && iy < hh as i64 && ix < ww as i64 {
z += r.w[(ry + sh * t) * kw + rx + sw * t2] as i128 * r.x[iy as usize * ww + ix as usize] as i128;
}
t2 += 1;
}
t += 1;
}
assert_eq!(r.out.data[y * ow + xo] as i128, rq0(&r.rq, z), "the phase form is the transposed convolution");
}
}
}
/// K7. The interpreter's `conv_transpose` (scatter into an uncropped plane, then crop)
/// equals §5.3's gather form at every output position — the sum over `i < kh, j < kw`
/// with `ty = y + ph − i ≥ 0`, `ty mod sh = 0`, `ty / sh < H`, and likewise for `x`, of
/// `Wt[i][j]·X[ty / sh][tx / sw]`, then `rq` — and a position no tap reaches holds
/// `rq(0)`. Window: the shapes below, with every weight, input, bias, rounding and shift
/// up to 4 symbolic.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(12)]
fn k07_conv_transpose_gather_equals_scatter() {
match kani::any::<u8>() % 4 {
0 => gather(3, 3, 2, 2, 1, 1, 1, 1, 2, 2),
1 => gather(3, 3, 3, 3, 2, 2, 3, 3, 2, 2),
2 => gather(2, 2, 3, 3, 0, 0, 0, 0, 2, 2),
_ => gather(3, 1, 2, 1, 1, 0, 1, 0, 2, 3),
}
}
/// K8. The phase form of §9.3 equals the transposed convolution: for
/// `ρ = ((y + ph) mod sh, (x + pw) mod sw)` and `(u, v) = (⌊(y + ph)/sh⌋, ⌊(x + pw)/sw⌋)`,
/// `Z_ρ[u][v] = Σ_{t, t′} Wt[ρy + sh·t][ρx + sw·t′]·X̃[u − t][v − t′]` over the phase's
/// sub-kernel, with `X̃` zero outside the input, and the output is `rq(Z_ρ[u][v])`.
/// Window: K7's shapes.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(12)]
fn k08_conv_transpose_phase_form() {
match kani::any::<u8>() % 4 {
0 => phase(3, 3, 2, 2, 1, 1, 1, 1, 2, 2),
1 => phase(3, 3, 3, 3, 2, 2, 3, 3, 2, 2),
2 => phase(2, 2, 3, 3, 0, 0, 0, 0, 2, 2),
_ => phase(3, 1, 2, 1, 1, 0, 1, 0, 2, 3),
}
}

View file

@ -0,0 +1,44 @@
//! `igdn` (§5.7) against the scalar reference every `WHM1` and `WAM1` decoder uses
//! today, and its fast paths (K10). Only the host compiles this: the reference and the
//! fast kernels are `wai-rs` sources it includes beside this crate's.
use alloc::vec::Vec;
use crate::interp::{self, Quiet, Tensor};
/// K10. `igdn` as the operator set defines it (the interpreter: each term floored, then
/// summed; `isqrt` of the clamped norm; the product shifted) equals
/// `int_transform::reference::igdn`, and the fast kernel's narrow paths (32-bit terms,
/// 64-bit sums and products) equal both. Window: `C ≤ 2`, two positions, `x` and `γ`
/// in `i8`, `β` in `i16`, `gf` and `frac` up to 16; on it every narrowing condition
/// holds, so the narrowest path runs.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k10_igdn_equals_the_reference() {
igdn_equal(if kani::any() { 1 } else { 2 }, 2);
}
/// K10 at one channel and one position: the same statement on a narrower window.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k10_igdn_equals_the_reference_c1() {
igdn_equal(1, 1);
}
fn igdn_equal(c: usize, hw: usize) {
let xraw: [i8; 4] = kani::any();
let graw: [i8; 4] = kani::any();
let braw: [i16; 2] = kani::any();
let (gf, frac): (u32, u32) = (kani::any(), kani::any());
kani::assume(gf <= 16 && frac <= 16);
let x: Vec<i64> = xraw[..c * hw].iter().map(|&v| v as i64).collect();
let gamma: Vec<i64> = graw[..c * c].iter().map(|&v| v as i64).collect();
let beta: Vec<i64> = braw[..c].iter().map(|&v| v as i64).collect();
let reference = crate::int_transform::reference::igdn(&x, c, hw, frac, &gamma, &beta, gf);
let ops = interp::k_igdn(&Tensor::new(c as u32, 1, hw as u32, x.clone()), gf, frac, &gamma, &beta, 0, &mut Quiet).expect("igdn on i8 inputs is formed");
assert_eq!(ops.data, reference, "the operator set's igdn is the reference");
let fast = crate::int_kernels::igdn(&x, c, hw, frac, &gamma, &beta, gf).expect("the fast path applies");
assert_eq!(fast, reference, "the narrow fast path is the reference");
}

View file

@ -0,0 +1,155 @@
//! A 32-bit fabric's arithmetic under `acc32` (§6, §7.3): K2 and K2b on the model in
//! [`crate::limb32`], and K5 and K6 on the chunked sums `K_c` allows.
use super::any_round;
use crate::graph::Round;
use crate::interp::round;
use crate::limb32::{self, W64};
// ------------------------------------------------------------------------------- K2
/// K2. The 16-bit-limb product: for every value of the four partial products a
/// 16 × 16-bit multiplication can give (each in `[0, (2^16 − 1)²]`, taken independently,
/// which includes every combination real halves produce), the 32-bit adds with their
/// carries give `p00 + (p01 + p10)·2^16 + p11·2^32` exactly, as two words. Kani's
/// overflow checks show the partial products themselves never wrap a `u32`.
///
/// With `a = a1·2^16 + a0` and `b = b1·2^16 + b0`, the integer identity
/// `a·b = a0·b0 + (a0·b1 + a1·b0)·2^16 + a1·b1·2^32` then makes the result the `u64`
/// product for every `u32 × u32`. The harnesses below show that the partial products
/// do not wrap and that the magnitudes and the negation are exact; `mul_i32` is those
/// steps in sequence, so the limb product is the `i64` product for every `i32 × i32`.
/// The identity is arithmetic, not re-derived by a second multiplier circuit, for the
/// reason the module documentation gives; the unit tests of `limb32` compare the whole
/// product with `i64` multiplication at the edges and on samples.
#[kani::proof]
fn k02_limb_combination_is_exact() {
let p: [u32; 4] = kani::any();
let max = 0xFFFF * 0xFFFF;
kani::assume(p.iter().all(|&v| v <= max));
let got = limb32::combine(p);
let want = p[0] as u128 + ((p[1] as u128 + p[2] as u128) << 16) + ((p[3] as u128) << 32);
assert!(want < 1 << 64);
assert_eq!(((got.hi as u128) << 32) | got.lo as u128, want, "the carries are exact");
}
/// K2. The partial products are formed without wrapping: `partials` multiplies the
/// 16-bit halves in `u32` with Rust's unchecked operator, and Kani's overflow check on
/// each multiplication proves that none overflows, for every `a` and `b`. The halves
/// recompose the operands.
#[kani::proof]
fn k02_limb_partials_do_not_wrap() {
let (a, b): (u32, u32) = (kani::any(), kani::any());
let _ = limb32::partials(a, b);
let (a0, a1, b0, b1) = (a & 0xFFFF, a >> 16, b & 0xFFFF, b >> 16);
assert_eq!((a1 << 16) | a0, a);
assert_eq!((b1 << 16) | b0, b);
}
/// K2. The signs: `|a|` is the magnitude for every `i32` (`2^31` for `−2^31`), the
/// two-word negation is negation modulo 2^64 for every 64-bit value, and the product
/// takes the negation exactly when one operand is negative.
#[kani::proof]
fn k02_limb_signs() {
let a: i32 = kani::any();
assert_eq!(limb32::magnitude(a) as i64, (a as i64).abs(), "|a|");
let v: i64 = kani::any();
assert_eq!(limb32::neg(W64::from_i64(v)).to_i64(), v.wrapping_neg(), "−v, modulo 2^64");
assert_eq!(W64::from_i64(v).to_i64(), v);
}
// ------------------------------------------------------------------------------ K2b
/// K2b. The epilogue on two words (§7.3): for every product `acc32` allows — any `p`
/// in `i64` whose `rq.p` interval, with `half_up`'s addend `2^(s−1)`, fits `i64` — every
/// shift `s ≤ 63` and every rounding, the two-word rounding shift, written with 32-bit
/// shifts that take their amount mod 32, equals `R_floor`, `R_half_up` and
/// `R_half_even` (§2, through [`round`], which K3 proves equal to them). Then the
/// `post` bias is added and the value narrowed to `i32`: exact whenever `rq.y` fits
/// `i32` (and so whenever `rq.q` and `rq.y` both do).
#[kani::proof]
fn k02b_limb_epilogue() {
let p: i64 = kani::any();
let s: u32 = kani::any();
kani::assume(s <= 63);
let r = any_round();
kani::assume(r != Round::HalfUp || s == 0 || p as i128 + (1i128 << (s - 1)) <= i64::MAX as i128);
let q = round(p as i128, s, r).expect("formed for every product acc32 allows");
let got = limb32::round(W64::from_i64(p), s, r);
assert_eq!(got.to_i64() as i128, q, "the two-word rounding shift is R(p, s)");
let post: i32 = kani::any();
let y = q + post as i128;
if i32::try_from(y).is_ok() {
assert_eq!(limb32::epilogue(W64::from_i64(p), s, r, post) as i128, y, "exact whenever rq.y fits i32");
}
}
// ------------------------------------------------------------------------- K5 and K6
/// A term interval and its `P = max(|lo|, |hi|)`, `1 ≤ P ≤ 2^31 − 1` (with `P ≥ 2^31`,
/// `K_c` is 0 and a fabric forms no 32-bit partial sum), with
/// `K_c = ⌊(2^31 − 1) / P⌋` (§6), which the certificate records as `k_c` (the
/// independent certifier recomputes every certificate's `k_c`).
fn term_interval() -> (i64, i64, usize) {
let (lo, hi): (i32, i32) = (kani::any(), kani::any());
kani::assume(lo <= hi && lo > i32::MIN);
let p = (lo as i64).abs().max((hi as i64).abs());
kani::assume(p >= 1);
(lo as i64, hi as i64, ((i32::MAX as i64) / p) as usize)
}
/// K5. Chunked 32-bit partial sums (§6): for any `n ≤ 8` terms in a term interval, and
/// any partition of them into consecutive chunks of at most `K_c` terms, each chunk's
/// running sum fits `i32` at every step, and the chunk sums, widened and added, equal
/// the exact sum. Window: every term interval within `i32`, every partition.
#[kani::proof]
#[kani::unwind(10)]
fn k05_chunked_i32_sums() {
let (lo, hi, kc) = term_interval();
let n: usize = kani::any();
kani::assume(n <= 8);
let t: [i32; 8] = kani::any();
kani::assume(t[..n].iter().all(|&v| lo <= v as i64 && v as i64 <= hi));
// A partition: a cut after term i where cut[i] is set; no chunk longer than K_c.
let cut: [bool; 8] = kani::any();
let (mut total, mut chunk, mut len, mut exact) = (0i64, 0i32, 0usize, 0i64);
for i in 0..n {
len += 1;
kani::assume(len <= kc);
chunk = chunk.checked_add(t[i]).expect("a chunk of at most K_c terms fits i32");
exact += t[i] as i64;
if cut[i] || i + 1 == n {
total += chunk as i64;
chunk = 0;
len = 0;
}
}
assert_eq!(total, exact, "the chunked sum is the exact sum");
}
/// K6. An `n`-product dot instruction into one 32-bit lane (§6), for `n = 2` and `4`:
/// after `j` instructions the lane holds `n·j` products, and while `n·j ≤ K_c` it never
/// wraps, at every instruction and inside it. Window: every term interval within `i32`,
/// up to 8 products.
#[kani::proof]
#[kani::unwind(10)]
fn k06_dot_lane_never_wraps() {
let (lo, hi, kc) = term_interval();
let n: usize = if kani::any() { 2 } else { 4 };
let j: usize = kani::any();
kani::assume((1..=4).contains(&j));
kani::assume(n * j <= 8 && n * j <= kc);
let t: [i32; 8] = kani::any();
kani::assume(t[..n * j].iter().all(|&v| lo <= v as i64 && v as i64 <= hi));
let mut lane = 0i32;
let mut exact = 0i64;
for ins in 0..j {
let mut dot = 0i32;
for k in 0..n {
dot = dot.checked_add(t[ins * n + k]).expect("inside the instruction");
exact += t[ins * n + k] as i64;
}
lane = lane.checked_add(dot).expect("the lane does not wrap");
}
assert_eq!(lane as i64, exact);
}

View file

@ -0,0 +1,103 @@
//! The model checker's harnesses for `wai-ops/1`: K1–K33 of the operator set's test
//! plan, each named `kNN_…` after its row.
//!
//! They are compiled only under `cfg(kani)`, and `wai-verify` hosts them: it includes
//! this crate's modules at its own root by `#[path]`, so `crate::` resolves the same
//! way there. Each harness states its window (the sizes and widths it is bounded to)
//! in its documentation. Bounded is not universal, and a harness claims only what it
//! checks.
//!
//! References are written independently of the code they check: a definition from
//! the text is checked as its defining inequalities, or recomputed in 512-bit
//! arithmetic ([`crate::xint::I512`]), never by calling the function under test. One
//! exception is stated where it applies: where both sides would multiply the same two
//! symbolic operands, the product is formed once and shared, because a SAT-based
//! checker does not decide the equivalence of two multiplier circuits in bounded time.
//! Rust's checked multiplication is exact by the language's definition, which the
//! checker models bit for bit.
// Harnesses gated by `cfg(kani_open)` state a property of the test plan at a window
// on which the model checker did not finish within the budget its CI shard allows
// (each says which, and the pull request that added them records how long it ran).
// They are compiled and run only on request:
//
// RUSTFLAGS="--cfg kani_open" cargo kani --lib --exact --harness <name>
//
// The helpers and imports only they use are then unused in an ordinary run.
#![allow(dead_code, unused_imports)]
use crate::graph::Round;
use crate::interp::Observe;
mod arith;
mod conv_t;
mod igdn;
mod limb;
mod plan;
mod rules;
mod sound;
mod stage;
/// A symbolic rounding mode.
pub(crate) fn any_round() -> Round {
let k: u8 = kani::any();
kani::assume(k < 3);
match k {
0 => Round::Floor,
1 => Round::HalfUp,
_ => Round::HalfEven,
}
}
/// The values an observed run reports (§8.5's intermediates), by position: an
/// operator reports them in a fixed order, which each harness states. Only the first
/// `40` are kept.
pub(crate) struct Rec {
pub v: [i128; 40],
pub n: usize,
}
impl Rec {
pub fn new() -> Rec {
Rec { v: [0; 40], n: 0 }
}
}
impl Observe for Rec {
fn see(&mut self, _op: u32, _key: &'static str, v: i128) {
if self.n < 40 {
self.v[self.n] = v;
}
self.n += 1;
}
}
/// An observer that holds every value an operator forms to the interval given for its
/// key (§8.5, Table 8.5-A), and counts what it sees. A key not listed fails.
pub(crate) struct Keyed<const N: usize> {
pub rules: [(&'static str, rules::R); N],
pub seen: u32,
}
/// A key's tag: its length, its last byte and its second byte. The keys one operator
/// reports differ in their tags (`conv.term`, `conv.acc`, `rq.t`, `rq.p`, …), and a tag
/// is three loads, where comparing strings is a loop the checker unrolls at every call.
fn tag(k: &str) -> (usize, u8, u8) {
let b = k.as_bytes();
(b.len(), b[b.len() - 1], b[1])
}
impl<const N: usize> Observe for Keyed<N> {
fn see(&mut self, _op: u32, key: &'static str, v: i128) {
self.seen += 1;
let t = tag(key);
let mut found = false;
for (k, r) in self.rules.iter() {
if tag(k) == t {
assert!(rules::within(*r, v), "a value lies in its key's interval");
found = true;
}
}
assert!(found, "every key the operator reports has an interval");
}
}

View file

@ -0,0 +1,373 @@
//! The plans, the graph form and the data-movement operators: K12 (the lowerings'
//! predicates and their search), K15 (`lut16`'s index), K16 (movement), K31 (`WHG1`
//! parse) and K33 (the run-time plan).
use alloc::vec;
use alloc::vec::Vec;
use crate::bounds::{self, Iv};
use crate::graph::{BiasKind, Graph, Op, Params, Round, Rq, TensorRec, opcode::*};
use crate::interp::{self, Tensor};
use crate::plan;
use crate::lower::{largest, p_conv, p_igdn, p_leaky};
use crate::xint::I512;
/// A symbolic `u32` in `[lo, hi]`.
fn range(lo: u32, hi: u32) -> u32 {
let v: u32 = kani::any();
kani::assume(lo <= v && v <= hi);
v
}
/// `n` symbolic `i8` values, as `i64`.
fn data(n: usize) -> Vec<i64> {
let raw: [i8; 72] = kani::any();
raw[..n].iter().map(|&v| v as i64).collect()
}
/// `0, 1, …, n − 1`: every element names its own position.
fn ids(n: usize) -> Vec<i64> {
(0..n as i64).collect()
}
/// Every element of `out` is an input position, and no two are the same: with as many
/// elements as the input, a bijection.
fn is_permutation_of_ids(out: &[i64], n: usize) {
assert_eq!(out.len(), n);
let (k1, k2): (usize, usize) = (kani::any(), kani::any());
kani::assume(k1 < n && k2 < n && k1 != k2);
assert!(0 <= out[k1] && (out[k1] as usize) < n);
assert!(out[k1] != out[k2], "no input element is used twice");
}
// ------------------------------------------------------------------------------ K12
/// K12. The search of §13.2 returns the maximum: for a predicate that holds at `X`
/// whenever it holds at a larger value, `largest` gives the largest `X ∈ [0, 2^63]` at
/// which it holds, or `None` when it fails at 0. A predicate on `[0, 2^63]` that holds
/// below wherever it holds is `X ≤ T` for some `T`, or nowhere; the harness covers
/// every `T`.
#[kani::proof]
#[kani::unwind(66)]
fn k12_largest_returns_the_maximum() {
let t: u128 = kani::any();
let nowhere: bool = kani::any();
let got = largest(|x| !nowhere && x <= t);
if nowhere {
assert_eq!(got, None);
} else {
assert_eq!(got, Some(t.min(1 << 63)));
}
}
/// K12. `P_ℓ` of a convolution layer (tags 0 and 1, §13.2) holds at `X` whenever it
/// holds at a larger `X`. Window: the term count, `Wm`, `Mm` and `Bm` in `u8`, any
/// shift in `0..=127`, and every `X′ ≤ X` in `u32`.
#[kani::proof]
#[kani::unwind(10)]
fn k12_p_conv_is_monotone() {
let (terms, wm, mm, bm): (u8, u8, u8, u8) = (kani::any(), kani::any(), kani::any(), kani::any());
let s: u32 = range(0, 127);
let (a, b): (u32, u32) = (kani::any(), kani::any());
kani::assume(a <= b);
let u = |v: u8| I512::from_u64(v as u64);
if p_conv(u(terms), u(wm), u(mm), u(bm), s, b as u128) {
assert!(p_conv(u(terms), u(wm), u(mm), u(bm), s, a as u128), "P holds below wherever it holds");
}
}
/// K12. `P_ℓ` of an `igdn` layer (tag 2, §13.2) holds at `X` whenever it holds at a
/// larger `X`. Window: `G` and `Bb` in `u8`, `c ≤ 4`, `gf` and `afrac` up to 16, and
/// every `X′ ≤ X` in `u8`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(16)]
fn k12_p_igdn_is_monotone() {
let (g, bb): (u8, u8) = (kani::any(), kani::any());
let c = range(1, 4);
let (gf, af) = (range(0, 16), range(1, 16));
let (a, b): (u8, u8) = (kani::any(), kani::any());
kani::assume(a <= b);
let u = |v: u64| I512::from_u64(v);
if p_igdn(u(g as u64), u(bb as u64), u(c as u64), gf, af, b as u128) {
assert!(p_igdn(u(g as u64), u(bb as u64), u(c as u64), gf, af, a as u128), "P holds below wherever it holds");
}
}
/// K12. `P_ℓ` of a `leaky` layer (tag 4, §13.2) holds at `X` whenever it holds at a
/// larger `X`. Window: `|slope_q|` in `u16`, every `X′ ≤ X` in `u32`.
#[kani::proof]
#[kani::unwind(10)]
fn k12_p_leaky_is_monotone() {
let q: u16 = kani::any();
let (a, b): (u32, u32) = (kani::any(), kani::any());
kani::assume(a <= b);
let s = I512::from_u64(q as u64);
if p_leaky(s, b as u128) {
assert!(p_leaky(s, a as u128), "P holds below wherever it holds");
}
}
// ------------------------------------------------------------------------------ K15
/// K15. `lut16` (§5.6): for every input range within `[−32767, 32767]` — which load
/// step 8 requires — and every value in it, the index `v + 32768` addresses the table;
/// and the rule refuses, at load, a range that passes either end. Window: every such
/// range and value; the table's entries do not affect the index.
#[kani::proof]
#[kani::unwind(10)]
fn k15_lut16_index_is_total() {
let (l, h): (i64, i64) = (kani::any(), kani::any());
kani::assume(-32767 <= l && l <= h && h <= 32767);
let v: i64 = kani::any();
kani::assume(l <= v && v <= h);
let t = vec![0i16; 65536];
assert!(interp::k_lut16(&Tensor::new(1, 1, 1, vec![v]), &t, 0).is_ok(), "the index addresses the table");
// One past either end: the rule's range check refuses the graph at load.
for (lo, hi) in [(-32768i64, 0i64), (0, 32768)] {
let op = Op { code: LUT16, ins: vec![0], outs: vec![1], p: Params::Lut16 { e_in: -128, e_out: -128, t: vec![0i16; 65536] } };
let g = Graph { max_elems: 1 << 26, scalars: vec![], inputs: vec![], outputs: vec![1], tensors: vec![TensorRec::check(1, lo, hi), TensorRec::derived(1)], ops: vec![] };
assert!(bounds::eval(&g, &op, &[Iv::of_i64(lo, hi)], None).lut_range, "a range past [−32767, 32767] is refused at load");
}
}
// ------------------------------------------------------------------------------ K16
/// `pixel_shuffle` at one shape: a bijection, and the element the text names.
fn shuffle_case(r: usize, co: usize, h: usize, w: usize) {
let c = co * r * r;
let n = c * h * w;
let (oh, ow) = plan::shuffle_sides(h as u64, w as u64, r as u64).expect("small sides");
let out = interp::k_pixel_shuffle(&Tensor::new(c as u32, h as u32, w as u32, ids(n)), r, oh as usize, ow as usize);
is_permutation_of_ids(&out.data, n);
let (cc, y, x, i, j) = (range(0, co as u32 - 1) as usize, range(0, h as u32 - 1) as usize, range(0, w as u32 - 1) as usize, range(0, r as u32 - 1) as usize, range(0, r as u32 - 1) as usize);
let (oh, ow) = (h * r, w * r);
assert_eq!(out.data[(cc * oh + y * r + i) * ow + x * r + j], (((cc * r * r + i * r + j) * h + y) * w + x) as i64);
}
/// K16. `pixel_shuffle` (§5.10) is a bijection, and `out[c][h·r + i][w·r + j]` is
/// `in[c·r² + i·r + j][h][w]`. Window: the shapes below, every output plane up to 4×4.
#[kani::proof]
#[kani::unwind(18)]
fn k16_pixel_shuffle_is_a_bijection() {
match kani::any::<u8>() % 4 {
0 => shuffle_case(2, 1, 2, 2),
1 => shuffle_case(2, 2, 1, 2),
2 => shuffle_case(1, 2, 3, 2),
_ => shuffle_case(2, 1, 1, 1),
}
}
/// `pixel_unshuffle` at one shape: the plan refuses a side `r` does not divide;
/// otherwise a bijection, and the element the text names.
fn unshuffle_case(r: usize, c: usize, h: usize, w: usize) {
let n = c * h * w;
let Ok((oh, ow)) = plan::unshuffle_sides(h as u64, w as u64, r as u64) else {
assert!(h % r != 0 || w % r != 0, "only a side r does not divide is refused");
return;
};
let (oh, ow) = (oh as usize, ow as usize);
let out = interp::k_pixel_unshuffle(&Tensor::new(c as u32, h as u32, w as u32, ids(n)), r, oh, ow);
is_permutation_of_ids(&out.data, n);
let (cc, y, x, i, j) = (range(0, c as u32 - 1) as usize, range(0, oh as u32 - 1) as usize, range(0, ow as u32 - 1) as usize, range(0, r as u32 - 1) as usize, range(0, r as u32 - 1) as usize);
assert_eq!(out.data[((cc * r * r + i * r + j) * oh + y) * ow + x], ((cc * h + y * r + i) * w + x * r + j) as i64);
}
/// K16. `pixel_unshuffle` (§5.10) is a bijection, the inverse layout of
/// `pixel_shuffle`, and the run-time plan refuses a side `r` does not divide. Window:
/// the shapes below, planes up to 4×4.
#[kani::proof]
#[kani::unwind(18)]
fn k16_pixel_unshuffle_is_a_bijection() {
match kani::any::<u8>() % 4 {
0 => unshuffle_case(2, 1, 4, 4),
1 => unshuffle_case(2, 2, 2, 4),
2 => unshuffle_case(1, 2, 3, 2),
_ => unshuffle_case(2, 1, 3, 4),
}
}
fn any_order() -> [u8; 4] {
let o: [u8; 4] = kani::any();
kani::assume(o.iter().all(|&v| v < 4) && o[0] != o[1] && o[0] != o[2] && o[0] != o[3] && o[1] != o[2] && o[1] != o[3] && o[2] != o[3]);
o
}
/// `subpos_split` and `subpos_merge` at one shape and a symbolic order.
fn subpos_case(c: usize, h: usize, w: usize) {
let n = c * h * w;
let order = any_order();
let (oh, ow) = (h / 2, w / 2);
let outs = interp::k_subpos_split(&Tensor::new(c as u32, h as u32, w as u32, ids(n)), &order, oh, ow);
let all: Vec<i64> = outs.iter().flat_map(|t| t.data.iter().copied()).collect();
is_permutation_of_ids(&all, n);
let k = range(0, 3) as usize;
let (a, b) = ((order[k] >> 1) as usize, (order[k] & 1) as usize);
let (cc, y, x) = (range(0, c as u32 - 1) as usize, range(0, oh as u32 - 1) as usize, range(0, ow as u32 - 1) as usize);
assert_eq!(outs[k].data[(cc * oh + y) * ow + x], ((cc * h + 2 * y + a) * w + 2 * x + b) as i64);
let ins: Vec<&Tensor> = outs.iter().collect();
let back = interp::k_subpos_merge(&ins, &order, h, w);
let e = range(0, n as u32 - 1) as usize;
assert!(back.data.len() == n && back.data[e] == e as i64, "merge undoes split");
}
/// K16. `subpos_split` (§5.10) is a bijection from its input onto its four outputs,
/// with `out_k[c][y][x] = in[c][2y + a_k][2x + b_k]`, and `subpos_merge` with the same
/// order undoes it. Window: one channel, a 2×4 plane, every order.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(18)]
fn k16_subpos_split_and_merge() {
subpos_case(1, 2, 4);
}
/// K16. `pad_rep`, `crop_to` and `crop` (§5.10), once the run-time plan has passed:
/// every index they read is in range (the checker's bounds checks), and each output
/// element is the one the text names. `crop`'s operands are any values a scalar can
/// take; the plan refuses one that does not fit. Window: planes up to 4×4, one
/// channel.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(18)]
fn k16_pad_and_crop_read_in_range() {
let (h, w) = (range(1, 4) as usize, range(1, 4) as usize);
let n = h * w;
let x = Tensor::new(1, h as u32, w as u32, ids(n));
// pad_rep
let m = range(1, 4) as u64;
let (ph, pw) = plan::pad_rep_sides(h as u64, w as u64, m).expect("small sides");
let (ph, pw) = (ph as usize, pw as usize);
let p = interp::k_pad_rep(&x, ph, pw);
let (y, xx) = (range(0, ph as u32 - 1) as usize, range(0, pw as u32 - 1) as usize);
assert_eq!(p.data[y * pw + xx], (y.min(h - 1) * w + xx.min(w - 1)) as i64);
// crop: any start and length a scalar can give, through the run-time plan's rule.
let (y0, x0, ch, cw): (i64, i64, i64, i64) = (kani::any(), kani::any(), kani::any(), kani::any());
if let (Ok(oh), Ok(ow)) = (plan::crop_side(y0 as i128, ch as i128, h as u64), plan::crop_side(x0 as i128, cw as i128, w as u64)) {
let cr = interp::crop(&x, y0 as usize, x0 as usize, oh as usize, ow as usize);
let (y, xx) = (range(0, oh as u32 - 1) as usize, range(0, ow as u32 - 1) as usize);
assert_eq!(cr.data[y * ow as usize + xx], ((y0 as usize + y) * w + x0 as usize + xx) as i64);
} else {
assert!(y0 < 0 || ch < 1 || y0 as i128 + ch as i128 > h as i128 || x0 < 0 || cw < 1 || x0 as i128 + cw as i128 > w as i128);
}
// crop_to: the plan accepts a ref no larger than the input.
let (rh, rw) = (range(1, h as u32) as usize, range(1, w as u32) as usize);
let ct = interp::crop(&x, 0, 0, rh, rw);
let (y, xx) = (range(0, rh as u32 - 1) as usize, range(0, rw as u32 - 1) as usize);
assert_eq!(ct.data[y * rw + xx], (y * w + xx) as i64);
}
// ------------------------------------------------------------------------------ K31
/// K31. `WHG1` parsing (§10, load steps 1–6) is total: every file of up to 96 bytes is
/// parsed or refused (`model_mismatch` at a load step), and nothing panics. Window:
/// every byte string of length 0 to 96.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(100)]
fn k31_whg1_parse_is_total() {
let b: [u8; 96] = kani::any();
let n: usize = kani::any();
kani::assume(n <= 96);
match Graph::parse(&b[..n]) {
Ok(g) => assert!(!g.tensors.is_empty() && !g.ops.is_empty()),
Err(r) => assert_eq!(r.code, crate::code::Code::ModelMismatch),
}
}
// ------------------------------------------------------------------------------ K33
/// An `rq` with no multiplier, one shift of 0 and no bias: valid over any channel count.
fn rq0() -> Rq {
Rq { round: Round::Floor, bias_kind: BiasKind::None, m_kind: 0, s_kind: 1, m_bytes: 0, b_bytes: 0, mult: vec![], shift: vec![0], bias: vec![] }
}
/// K33. The run-time plan (§4, run step 3) for `conv` is total, and no shape it accepts
/// lets the operator index out of range: for every input size, kernel, stride and
/// padding in the window, `conv`'s shape rule refuses, or the kernel computes every
/// output at the sizes the rule gives, reading only inside its input and weights.
/// Window: one channel each way, planes up to 3×3, kernels up to 3×3, strides up to 2,
/// padding up to 1.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k33_plan_conv() {
let (h, w) = (range(1, 3) as u64, range(1, 3) as u64);
let (kh, kw, sh, sw, ph, pw) = (range(1, 3) as u64, range(1, 3) as u64, range(1, 2) as u64, range(1, 2) as u64, range(0, 1) as u64, range(0, 1) as u64);
match plan::conv_sides(h, w, kh, kw, sh, sw, ph, pw) {
Ok((oh, ow)) => {
let x = Tensor::new(1, h as u32, w as u32, data((h * w) as usize));
let u = |v: u64| v as usize;
let o = interp::k_conv(&x, 1, u(kh), u(kw), u(sh), u(sw), u(ph), u(pw), 1, &rq0(), &data(u(kh * kw)), u(oh), u(ow), 0, &mut interp::Quiet).expect("i8 values");
assert_eq!(o.data.len() as u64, oh * ow);
}
Err(f) => assert!(f == plan::Fail::Shape && (h + 2 * ph < kh || w + 2 * pw < kw), "only a kernel larger than the padded input"),
}
}
/// K33. The same for `conv_transpose`: its shape rule refuses only an empty crop, and
/// otherwise the kernel computes every output, reading only inside its input, its
/// weights and its uncropped plane. Window: one channel each way, planes up to 2×2,
/// kernels up to 3×3, strides up to 2, padding and output padding up to 1.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k33_plan_conv_transpose() {
let (h, w) = (range(1, 2) as u64, range(1, 2) as u64);
let (kh, kw, sh, sw) = (range(1, 3) as u64, range(1, 3) as u64, range(1, 2) as u64, range(1, 2) as u64);
let (ph, pw, oph, opw) = (range(0, 1) as u64, range(0, 1) as u64, range(0, 1) as u64, range(0, 1) as u64);
let (uh, uw) = ((h - 1) * sh + kh + oph, (w - 1) * sw + kw + opw);
match plan::conv_t_sides(h, w, 1, kh, kw, sh, sw, ph, pw, oph, opw) {
Ok((oh, ow)) => {
let x = Tensor::new(1, h as u32, w as u32, data((h * w) as usize));
let u = |v: u64| v as usize;
let o = interp::k_conv_t(&x, 1, u(kh), u(kw), u(sh), u(sw), u(ph), u(pw), u(oph), u(opw), &rq0(), &data(u(kh * kw)), u(oh), u(ow), 0, &mut interp::Quiet).expect("i8 values");
assert!(oh == uh - 2 * ph && ow == uw - 2 * pw && o.data.len() as u64 == oh * ow);
}
Err(f) => assert!(f == plan::Fail::Shape && (uh <= 2 * ph || uw <= 2 * pw), "only an empty crop"),
}
}
/// K33. The operators with a second input, a scalar or a reshape: `mul` (a `b` of `C`
/// or 1 channels, at the sizes the plan's equal-size rule accepts), `chunk_add`,
/// `split` (`start + count ≤ C`, a load rule), `chscale` (a row the scalar's range
/// allows), `gram` and `apply` (the shapes their rules accept): each kernel computes
/// every output, reading only inside its inputs. Window: up to 2 channels, planes up to
/// 2×2.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k33_plan_multi_input() {
let c = 2usize;
let (h, w) = (range(1, 2) as usize, range(1, 2) as usize);
let a = Tensor::new(c as u32, h as u32, w as u32, data(c * h * w));
match range(0, 5) {
0 => {
let cb = if kani::any() { c } else { 1 };
let b = Tensor::new(cb as u32, h as u32, w as u32, data(cb * h * w));
assert!(interp::k_mul(&a, &b, &rq0(), 0, &mut interp::Quiet).is_ok());
}
1 => assert_eq!(interp::k_chunk_add(&a, 0).expect("i8").data.len(), h * w),
2 => {
let (start, count) = (range(0, 1) as usize, range(1, 2));
kani::assume(start + count as usize <= c);
assert_eq!(interp::k_split(&a, start, count).data.len(), count as usize * h * w);
}
3 => {
let rows = range(1, 3) as usize;
let row = range(0, rows as u32 - 1) as usize;
let m = data(rows * c);
assert!(interp::k_chscale(&a, row, Round::HalfEven, 3, &m, 0, &mut interp::Quiet).is_ok());
}
4 => {
let groups = range(1, 2) as usize;
let b = Tensor::new(c as u32, h as u32, w as u32, data(c * h * w));
assert_eq!(interp::k_gram(&a, &b, groups, &rq0(), 0, &mut interp::Quiet).expect("i8").data.len(), c * (c / groups));
}
_ => {
let groups = range(1, 2) as usize;
let d = c / groups;
let att = Tensor::new(c as u32, 1, d as u32, data(c * d));
assert_eq!(interp::k_apply(&att, &a, groups, &rq0(), 0, &mut interp::Quiet).expect("i8").data.len(), c * h * w);
}
}
}

View file

@ -0,0 +1,61 @@
//! The bound rules of §8.2–§8.3, restated from the text in `i128` for the soundness
//! harnesses.
//!
//! The bounds pass ([`crate::bounds`]) forms these intervals in 512-bit arithmetic
//! with saturation, which a model checker cannot execute on symbolic operands in
//! bounded time: every 512-bit product is 36 limb multiplications, and every
//! operation a loop over eight limbs. The soundness harnesses therefore take each
//! rule as §8.3 writes it, on operands small enough that no value here leaves `i128`
//! (each harness's window), and prove that every value the interpreter forms lies in
//! it. That the bounds pass computes these same rules is shown separately, and on
//! every graph the corpus holds: the independent certifier (`tools/wai_ops_check.py`,
//! written from the text alone) recomputes every certificate byte for byte.
use crate::graph::Round;
use crate::interp::round;
/// An interval `[lo, hi]`.
pub(crate) type R = (i128, i128);
pub(crate) fn within(r: R, v: i128) -> bool {
r.0 <= v && v <= r.1
}
pub(crate) fn hull(a: R, b: R) -> R {
(a.0.min(b.0), a.1.max(b.1))
}
/// The term interval of a weight `w` over `[l, h]` (§8.2).
pub(crate) fn term(w: i128, x: R) -> R {
let (a, b) = (w * x.0, w * x.1);
(a.min(b), a.max(b))
}
/// The corner hull of two run-time intervals (§8.2).
pub(crate) fn corner(a: R, b: R) -> R {
let c = [a.0 * b.0, a.0 * b.1, a.1 * b.0, a.1 * b.1];
(c[0].min(c[1]).min(c[2]).min(c[3]), c[0].max(c[1]).max(c[2]).max(c[3]))
}
/// The partial-sum interval `A(K, b)` (§8.2), for the terms `ks` and the bias `b`.
pub(crate) fn psum(ks: &[R], b: i128) -> R {
let mut r = (b.min(0), b.max(0));
for k in ks {
r = (r.0 + k.0.min(0), r.1 + k.1.max(0));
}
r
}
/// `⌊v / 2^s⌋` of both ends.
pub(crate) fn shr(r: R, s: u32) -> R {
(r.0 >> s, r.1 >> s)
}
/// The `rq` rules (§8.3) from `rq.t`: `rq.p` (with `half_up`'s and `half_even`'s
/// addend), `rq.q` and `rq.y`. `R` is [`round`], which K3 proves equal to §2.
pub(crate) fn rq(t: R, m: i128, s: u32, r: Round, post: i128) -> (R, R, R) {
let p = term(m, t);
let a = if r != Round::Floor && s >= 1 { 1i128 << (s - 1) } else { 0 };
let q = (round(p.0, s, r).expect("small values"), round(p.1, s, r).expect("small values"));
((p.0, p.1 + a), q, (q.0 + post, q.1 + post))
}

View file

@ -0,0 +1,584 @@
//! Soundness of the bound rules of §8.3: every value the interpreter forms lies in the
//! interval the rule gives it. K4, K9, K11, K14, K18–K22.
//!
//! Each harness runs one operator's kernel ([`crate::interp`]'s `k_*`) on symbolic
//! inputs inside a symbolic input interval, and holds every value the kernel reports
//! (its named intermediates, §8.5's Table 8.5-A) and every output to the interval §8.3's
//! rule gives it, restated in [`super::rules`]. Where a rule bounds partial sums
//! (`A(K, b)`, §8.2), a second harness takes every set of terms one output position can
//! hold, with or without the bias: every partial sum, in every order and grouping, and
//! the sum at a position where padding removes taps, is such a subset sum.
//!
//! The kernels take their shapes as arguments, so the convolution harnesses run them at
//! concrete shapes, which the checker unrolls exactly; the shape-free harnesses cover
//! the rest of each window.
use alloc::vec;
use alloc::vec::Vec;
use super::rules::{R, corner, hull, psum, rq, shr, term, within};
use super::{Keyed, Rec, any_round};
use crate::graph::{BiasKind, Round, Rq};
use crate::interp::{self, Tensor};
/// A symbolic value in `[lo, hi]`.
fn any_in(lo: i64, hi: i64) -> i64 {
let v: i64 = kani::any();
kani::assume(lo <= v && v <= hi);
v
}
/// `n` symbolic `i8` values in `[l, h]`, as `i64`.
fn i8s_in(n: usize, l: i8, h: i8) -> Vec<i64> {
let raw: [i8; 18] = kani::any();
kani::assume(raw[..n].iter().all(|&v| l <= v && v <= h));
raw[..n].iter().map(|&v| v as i64).collect()
}
/// An `rq` that leaves its input as it is apart from a `pre` bias `b`: no multiplier,
/// shift 0. Its output is then the accumulator plus `b`.
fn identity_rq(b: i64, c: usize) -> Rq {
Rq { round: Round::Floor, bias_kind: BiasKind::Pre, m_kind: 0, s_kind: 1, m_bytes: 0, b_bytes: 8, mult: vec![], shift: vec![0], bias: vec![b; c] }
}
/// `rq` with no multiplier, no bias, one shift: the contractions' (§5.9).
fn plain_rq(r: Round, s: u8) -> Rq {
Rq { round: r, bias_kind: BiasKind::None, m_kind: 0, s_kind: 1, m_bytes: 0, b_bytes: 0, mult: vec![], shift: vec![s], bias: vec![] }
}
// ------------------------------------------------------------------------------ K14
/// K14. `leaky` (§5.5, §8.3): a negative input's product `v·a` lies in `leaky.p`, and is
/// floored (`⌊v·a / 2^f⌋`, toward −∞); every output lies in the producer result.
/// Window: every interval `[l, h]` and value in it, and every `a`, within `i32`; every
/// `f` in `0..=63`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(4)]
fn k14_leaky_bounds_sound() {
let (l, h, a): (i32, i32, i32) = (kani::any(), kani::any(), kani::any());
leaky_case(l, h, a);
}
/// K14 with the interval, the value and `a` within `i16`: the same statement on a
/// narrower window.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(4)]
fn k14_leaky_bounds_sound_i16() {
let (l, h, a): (i16, i16, i16) = (kani::any(), kani::any(), kani::any());
leaky_case(l as i32, h as i32, a as i32);
}
/// K14 with the interval, the value and `a` within `i8`.
#[kani::proof]
#[kani::unwind(4)]
fn k14_leaky_bounds_sound_i8() {
let (l, h, a): (i8, i8, i8) = (kani::any(), kani::any(), kani::any());
leaky_case(l as i32, h as i32, a as i32);
}
fn leaky_case(l: i32, h: i32, a: i32) {
kani::assume(l <= h);
let v = any_in(l as i64, h as i64);
let f: u32 = kani::any();
kani::assume(f <= 63);
// §8.3's rule, in i64: every product of two i32 values fits.
let (li, hi, ai) = (l as i64, h as i64, a as i64);
let (c1, c2) = (li * ai, hi.min(-1) * ai);
let p = (c1.min(c2) as i128, c1.max(c2) as i128);
let mut res: Option<R> = if li < 0 { Some(shr(p, f)) } else { None };
if hi >= 0 {
let nn = (li.max(0) as i128, hi as i128);
res = Some(res.map_or(nn, |r| hull(r, nn)));
}
let res = res.expect("one part is present");
// leaky reports leaky.p, for a negative input only.
let mut rec = Rec::new();
let out = interp::k_leaky(&Tensor::new(1, 1, 1, vec![v]), a as i64, f, 0, &mut rec).expect("leaky is total");
let y = out.data[0] as i128;
assert!(within(res, y), "the output lies in the producer result");
if v < 0 {
assert_eq!(rec.n, 1);
let pv = rec.v[0];
assert!(within(p, pv), "v·a lies in leaky.p");
assert!(y * (1i128 << f) <= pv && pv < (y + 1) * (1i128 << f), "the product is floored");
} else {
assert_eq!(rec.n, 0);
assert_eq!(y, v as i128);
}
}
// ------------------------------------------------------------------------------- K4
/// The term interval of an `i8` weight over an `i8` interval, and `A(K, b)`, in `i32`
/// (every value here is at most 18·2^14 in magnitude): §8.2's formulas.
fn term32(w: i32, l: i32, h: i32) -> (i32, i32) {
let (a, b) = (w * l, w * h);
(a.min(b), a.max(b))
}
/// K4 and K9, first lemma: a term lies in its term interval (§8.2). For every weight,
/// interval and value in `i8`, `w·x` lies in `[min(w·l, w·h), max(w·l, w·h)]`.
#[kani::proof]
fn k04_term_lies_in_its_interval() {
let (w, l, h, x): (i8, i8, i8, i8) = (kani::any(), kani::any(), kani::any(), kani::any());
kani::assume(l <= x && x <= h);
let (lo, hi) = term32(w as i32, l as i32, h as i32);
let t = w as i32 * x as i32;
assert!(lo <= t && t <= hi, "w·x lies in its term interval");
}
/// K4 and K9, second lemma: `A(K, b)` (§8.2) holds every partial sum, by induction on
/// the terms. Base: with no term, a partial sum is 0 or the bias `b`, and both lie in
/// `[min(0, b), max(0, b)]`. Step: if a partial sum `s` lies in `[lo, hi]` and a term
/// `t` lies in its interval `[l, h]`, then `s` and `s + t` both lie in
/// `[lo + min(0, l), hi + max(0, h)]`. So every subset sum of any number of terms, with
/// or without the bias, lies in `A(K, b)`: the taps one position reads, in every order
/// and with padding removing any of them, and for `conv_transpose` the taps of one
/// residue class (`k09_conv_transpose_residues`). With the first lemma, every partial
/// sum of every output position lies in `conv.acc` and `convT.acc`. Window: every
/// value in `i32` (the step's sums are formed in `i64`).
#[kani::proof]
fn k04_partial_sums_lie_in_a() {
let b: i32 = kani::any();
let s0: i64 = if kani::any() { b as i64 } else { 0 };
assert!((b as i64).min(0) <= s0 && s0 <= (b as i64).max(0), "base");
let (s, lo, hi, t, l, h): (i32, i32, i32, i32, i32, i32) = (kani::any(), kani::any(), kani::any(), kani::any(), kani::any(), kani::any());
kani::assume(lo <= s && s <= hi && l <= t && t <= h);
let (lo2, hi2) = (lo as i64 + (l as i64).min(0), hi as i64 + (h as i64).max(0));
let (s, t) = (s as i64, t as i64);
assert!(lo2 <= s && s <= hi2, "a sum without the term stays in the wider interval");
assert!(lo2 <= s + t && s + t <= hi2, "a sum with the term lies in the wider interval");
}
/// K4. The interpreter's `conv` at a concrete shape — two input channels, one output
/// channel, a 3×3 kernel with padding 1 on a 3×3 plane, so positions on the edge lose
/// taps — with symbolic `i8` weights, bias, interval and inputs: every term it forms
/// lies in `conv.term`, and every partial sum, the accumulator with its bias, and every
/// output, in `conv.acc`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(20)]
fn k04_conv_interpreter_stays_in_the_rule() {
let (l, h): (i8, i8) = (kani::any(), kani::any());
kani::assume(l <= h);
let w = i8s_in(18, i8::MIN, i8::MAX);
let x = i8s_in(18, l, h);
let b: i8 = kani::any();
let terms: Vec<R> = w.iter().map(|&wv| term(wv as i128, (l as i128, h as i128))).collect();
let conv_term = terms.iter().copied().reduce(hull).expect("taps");
let acc = psum(&terms, b as i128);
let mut obs = Keyed { rules: [("conv.term", conv_term), ("conv.acc", acc), ("rq.t", acc), ("rq.p", acc), ("rq.q", acc), ("rq.y", acc)], seen: 0 };
let out = interp::k_conv(&Tensor::new(2, 3, 3, x), 1, 3, 3, 1, 1, 1, 1, 1, &identity_rq(b as i64, 1), &w, 3, 3, 0, &mut obs).expect("conv on i8 values is formed");
assert!(obs.seen > 0);
for &v in &out.data {
assert!(within(acc, v as i128), "every output lies in conv.acc");
}
}
// ------------------------------------------------------------------------------- K9
/// K9. The residue-class rule of `conv_transpose` (§8.3): one output position reads the
/// taps of one residue only. For every output row `y`, padding `ph ≤ 2`, stride
/// `sh ≤ 3` and kernel row `i < 3`, a tap that §5.3's gather form gives position `y`
/// (`ty = y + ph − i ≥ 0`, `ty mod sh = 0`) has `i mod sh = (y + ph) mod sh`. Columns
/// are the same statement. A position's partial sums are therefore partial sums of one
/// `K_ρ`, which `k04_partial_sums_lie_in_a` puts in `A(K_ρ, b)`, and so in `convT.acc`,
/// the hull over residues.
#[kani::proof]
fn k09_conv_transpose_residues() {
let (y, ph, sh, i): (u32, u32, u32, u32) = (kani::any(), kani::any(), kani::any(), kani::any());
kani::assume(y <= 64 && ph <= 2 && (1..=3).contains(&sh) && i < 3);
let ty = (y + ph) as i64 - i as i64;
if ty >= 0 && ty % sh as i64 == 0 {
assert_eq!(i % sh, (y + ph) % sh, "a position reads the taps of one residue");
}
}
/// `convT.acc` for one channel each way: the hull over residues `ρ` of `A(K_ρ, b)`.
fn convt_acc(w: &[i64], kh: usize, kw: usize, sh: usize, sw: usize, x: R, b: i128) -> (R, R) {
let terms: Vec<R> = w.iter().map(|&wv| term(wv as i128, x)).collect();
let all = terms.iter().copied().reduce(hull).expect("taps");
let mut acc: Option<R> = None;
for ry in 0..sh {
for rx in 0..sw {
let mut ks: Vec<R> = Vec::new();
for i in 0..kh {
for j in 0..kw {
if i % sh == ry && j % sw == rx {
ks.push(terms[i * kw + j]);
}
}
}
let a = psum(&ks, b);
acc = Some(acc.map_or(a, |r| hull(r, a)));
}
}
(all, acc.expect("a residue"))
}
/// K9. The interpreter's `conv_transpose` at two concrete shapes — 3×3 kernels, stride
/// 2 with padding 1 and output padding 1, and stride 3 with no padding, on 2×2 planes —
/// with symbolic `i8` weights, bias, interval and inputs: every product it forms lies
/// in `convT.term`, and every partial sum of every cell, the accumulator with its bias,
/// and every output, in `convT.acc`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(12)]
fn k09_conv_transpose_interpreter_stays_in_the_rule() {
let (l, h): (i8, i8) = (kani::any(), kani::any());
kani::assume(l <= h);
let w = i8s_in(9, i8::MIN, i8::MAX);
let x = i8s_in(4, l, h);
let b: i8 = kani::any();
let (s, p, op) = if kani::any() { (2usize, 1usize, 1usize) } else { (3, 0, 0) };
let (all, acc) = convt_acc(&w, 3, 3, s, s, (l as i128, h as i128), b as i128);
let o = (2 - 1) * s + 3 + op - 2 * p;
let mut obs = Keyed { rules: [("convT.term", all), ("convT.acc", acc), ("rq.t", acc), ("rq.p", acc), ("rq.q", acc), ("rq.y", acc)], seen: 0 };
let out = interp::k_conv_t(&Tensor::new(1, 2, 2, x), 1, 3, 3, s, s, p, p, op, op, &identity_rq(b as i64, 1), &w, o, o, 0, &mut obs).expect("formed");
for &v in &out.data {
assert!(within(acc, v as i128), "every output lies in convT.acc");
}
}
// ------------------------------------------------------------------------------ K11
/// K11. `igdn` (§5.7, §8.3), first part: the values the interpreter forms up to the
/// final norm lie in the rule's intervals — `x_j²` in `igdn.x2`, each `γ·x_j²` in
/// `igdn.gx2`, each floored term in `igdn.term`, every partial sum of `N_i` (with or
/// without `β_i`) in `igdn.N` — and the final norm `N_i` in the rule's final-norm
/// interval `Nf_i`, from which the rule takes `igdn.r`. The rest of the rule follows
/// from the three lemmas below and K3: `r_i = isqrt(max(0, N_i))` lies in
/// `[isqrt(max(0, Nf_lo)), isqrt(max(0, Nf_hi))]` because `isqrt` is non-decreasing;
/// `x_i·r_i` lies in the corner hull `igdn.xr`; and the output, its floor shift, in the
/// producer result, because `R_floor` is non-decreasing. Window: `C ≤ 2`, `x` and `γ`
/// in `i8`, `β` in `i16`, `gf` up to 8.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k11_igdn_bounds_sound() {
if kani::any() { igdn_case(1, 0) } else if kani::any() { igdn_case(2, 0) } else { igdn_case(2, 1) }
}
/// K11's first part at `c` channels, for output channel `i`.
fn igdn_case(c: usize, i: usize) {
let (l, h): (i8, i8) = (kani::any(), kani::any());
kani::assume(l <= h);
let data = i8s_in(c, l, h);
let gamma = i8s_in(c * c, i8::MIN, i8::MAX);
let braw: [i16; 2] = kani::any();
let beta: Vec<i64> = braw[..c].iter().map(|&v| v as i64).collect();
let gf: u32 = kani::any();
kani::assume(gf <= 8);
// §8.3's rule, for output channel i.
let (li, hi) = (l as i128, h as i128);
let x2 = if li <= 0 && 0 <= hi { (0, (li * li).max(hi * hi)) } else { ((li * li).min(hi * hi), (li * li).max(hi * hi)) };
let terms: Vec<R> = (0..c).map(|j| corner((gamma[i * c + j] as i128, gamma[i * c + j] as i128), x2)).collect();
let floored: Vec<R> = terms.iter().map(|&t| shr(t, gf)).collect();
let n_rule = psum(&floored, beta[i] as i128);
let nf = floored.iter().fold((beta[i] as i128, beta[i] as i128), |a, t| (a.0 + t.0, a.1 + t.1));
// igdn reports, at one position: x_j² for each j; then for each output channel:
// N = β, and for each j γ·x_j², the floored term and N; then r and x·r.
let mut rec = Rec::new();
let _ = interp::k_igdn(&Tensor::new(c as u32, 1, 1, data), gf, 0, &gamma, &beta, 0, &mut rec).expect("igdn on i8 inputs is formed");
assert_eq!(rec.n, c + c * (3 + 3 * c));
for j in 0..c {
assert!(within(x2, rec.v[j]), "x_j² lies in igdn.x2");
}
let base = c + i * (3 + 3 * c);
assert!(within(n_rule, rec.v[base]), "β alone lies in igdn.N");
let pick: [bool; 2] = kani::any();
let mut partial: i128 = if kani::any() { beta[i] as i128 } else { 0 };
for j in 0..c {
let (gx2, t, n) = (rec.v[base + 1 + 3 * j], rec.v[base + 2 + 3 * j], rec.v[base + 3 + 3 * j]);
assert!(within(terms[j], gx2), "γ·x_j² lies in igdn.gx2");
assert!(within(floored[j], t), "the floored term lies in igdn.term");
assert!(within(n_rule, n), "the running norm lies in igdn.N");
if pick[j] {
partial += t;
}
}
assert!(within(n_rule, partial), "every partial sum of N_i lies in igdn.N");
assert!(within(nf, rec.v[base + 3 * c]), "the final norm lies in Nf_i");
}
/// K11, second lemma: `isqrt` (the interpreter's) is non-decreasing. Window: every
/// `a ≤ b` below 2^32 (the norms of K11's window are below 2^23).
#[kani::proof]
fn k11_isqrt_is_monotone() {
let (a, b): (u32, u32) = (kani::any(), kani::any());
kani::assume(a <= b);
assert!((a as u128).isqrt() <= (b as u128).isqrt());
}
/// K11 (and every rule that takes a corner hull, §8.2), third lemma: for `x ∈ [l, h]`
/// and `r ∈ [a, b]`, `x·r` lies in the corner hull of the two intervals. Window: every
/// value in `i8`.
#[kani::proof]
fn k11_corner_hull_holds_products() {
let (l, h, x, a, b, r): (i8, i8, i8, i8, i8, i8) = (kani::any(), kani::any(), kani::any(), kani::any(), kani::any(), kani::any());
kani::assume(l <= x && x <= h && a <= r && r <= b);
let (l, h, x, a, b, r) = (l as i32, h as i32, x as i32, a as i32, b as i32, r as i32);
let c = [l * a, l * b, h * a, h * b];
let (lo, hi) = (c[0].min(c[1]).min(c[2]).min(c[3]), c[0].max(c[1]).max(c[2]).max(c[3]));
assert!(lo <= x * r && x * r <= hi, "x·r lies in the corner hull");
}
// ------------------------------------------------------------------------------ K19
/// K19. `sigmoid` (§5.8): for every `v` in `i64` but `−2^63` (which `exp.negx` excludes),
/// the output lies in `[0, 2^16]` — the producer result — and the dividend `sig.num`
/// (`2^32`, or `e·2^16`) lies in `[0, 65553·2^16]`.
#[kani::proof]
#[kani::unwind(4)]
fn k19_sigmoid_ranges() {
let v: i64 = kani::any();
kani::assume(v > i64::MIN);
// sigmoid reports exp.negx, exp.nl, exp.t2, exp.c, then sig.num.
let mut rec = Rec::new();
let out = interp::k_sigmoid(&Tensor::new(1, 1, 1, vec![v]), 0, &mut rec).expect("sigmoid is total");
assert_eq!(rec.n, 5);
let y = out.data[0] as i128;
assert!((0..=1 << 16).contains(&y), "the output lies in [0, 2^16]");
assert!((0..=65553 << 16).contains(&rec.v[4]), "sig.num lies in [0, 65553·2^16]");
}
/// K19. `elu(α)` (§5.8): for every interval `[l, h]` and `v` in it within `i64` (with
/// `exp.negx = −v` fitting `i64`), and every `α` in `i64` whose producer result fits
/// `i64` (every certified graph's): `α·(e − 2^16)` lies in `elu.p`, the corner hull of
/// `[α, α]` and `[−2^16, 17]`, and the output in the producer result.
#[kani::proof]
#[kani::unwind(4)]
fn k19_elu_ranges() {
let (l, h): (i64, i64) = (kani::any(), kani::any());
kani::assume(l > i64::MIN && l <= h);
let v = any_in(l, h);
let alpha: i64 = kani::any();
let ai = alpha as i128;
let elu_p = corner((ai, ai), (-(1 << 16), 17));
let mut res: Option<R> = if h > 0 { Some(((l as i128).max(1), h as i128)) } else { None };
if l <= 0 {
let q = shr(elu_p, 16);
res = Some(res.map_or(q, |r| hull(r, q)));
}
let res = res.expect("one part is present");
// A certified graph: its producer result fits i64 (acc64's `tensor` class). With
// α = −2^63 the result reaches 2^63, and load step 8 refuses the graph.
kani::assume(i64::try_from(res.0).is_ok() && i64::try_from(res.1).is_ok());
// elu reports, for v ≤ 0, exp.negx, exp.nl, exp.t2, exp.c, then elu.p.
let mut rec = Rec::new();
let out = interp::k_elu(&Tensor::new(1, 1, 1, vec![v]), alpha, 0, &mut rec).expect("elu is formed for every certified input");
let y = out.data[0] as i128;
assert!(within(res, y), "the output lies in the producer result");
if v <= 0 {
assert_eq!(rec.n, 5);
assert!(within(elu_p, rec.v[4]), "α·(e − 2^16) lies in elu.p");
} else {
assert_eq!(rec.n, 0);
}
}
// ------------------------------------------------------------------------------ K22
/// K22. `gram` (§5.9, §8.3): every term `A·B` lies in `gram.term`, every partial sum in
/// `gram.acc`, `rq.p`, `rq.q` and `rq.y` in their intervals, and the output in the
/// producer result. Window: `C = 2` in one or two groups, `H·W = 2` (the largest the
/// declared sizes allow), `i8` values, any rounding, shifts up to 8.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(12)]
fn k22_gram_bounds_sound() {
let groups: usize = if kani::any() { 1 } else { 2 };
let (la, ha, lb, hb): (i8, i8, i8, i8) = (kani::any(), kani::any(), kani::any(), kani::any());
kani::assume(la <= ha && lb <= hb);
let a = i8s_in(4, la, ha);
let b = i8s_in(4, lb, hb);
let s: u8 = kani::any();
kani::assume(s <= 8);
let r = any_round();
// §8.3's rule, with L = 2.
let gt = corner((la as i128, ha as i128), (lb as i128, hb as i128));
let gacc = (2 * gt.0.min(0), 2 * gt.1.max(0));
let (p_rule, q_rule, y_rule) = rq(gacc, 1, s as u32, r, 0);
let mut obs = Keyed { rules: [("gram.term", gt), ("gram.acc", gacc), ("rq.t", gacc), ("rq.p", p_rule), ("rq.q", q_rule), ("rq.y", y_rule)], seen: 0 };
let out = interp::k_gram(&Tensor::new(2, 1, 2, a), &Tensor::new(2, 1, 2, b), groups, &plain_rq(r, s), 0, &mut obs).expect("gram is formed");
assert!(obs.seen > 0);
for &v in &out.data {
assert!(within(y_rule, v as i128), "the output lies in the producer result");
}
}
/// K22. `apply` (§5.9, §8.3): every term `A·V` lies in `apply.term`, every partial sum
/// in `apply.acc`, `rq.p`, `rq.q` and `rq.y` in their intervals, and the output in the
/// producer result. Window: `C = 2` in one or two groups, `V` of `H·W = 2`, `i8` values,
/// any rounding, shifts up to 8.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(12)]
fn k22_apply_bounds_sound() {
let groups: usize = if kani::any() { 1 } else { 2 };
let d = 2 / groups;
let (la, ha, lv, hv): (i8, i8, i8, i8) = (kani::any(), kani::any(), kani::any(), kani::any());
kani::assume(la <= ha && lv <= hv);
let a = i8s_in(2 * d, la, ha);
let v = i8s_in(4, lv, hv);
let s: u8 = kani::any();
kani::assume(s <= 8);
let r = any_round();
let at = corner((la as i128, ha as i128), (lv as i128, hv as i128));
let aacc = (d as i128 * at.0.min(0), d as i128 * at.1.max(0));
let (p_rule, q_rule, y_rule) = rq(aacc, 1, s as u32, r, 0);
let mut obs = Keyed { rules: [("apply.term", at), ("apply.acc", aacc), ("rq.t", aacc), ("rq.p", p_rule), ("rq.q", q_rule), ("rq.y", y_rule)], seen: 0 };
let out = interp::k_apply(&Tensor::new(2, 1, d as u32, a), &Tensor::new(2, 1, 2, v), groups, &plain_rq(r, s), 0, &mut obs).expect("apply is formed");
assert!(obs.seen > 0);
for &v in &out.data {
assert!(within(y_rule, v as i128), "the output lies in the producer result");
}
}
// ------------------------------------------------------------------------------ K18
/// K18. `softmax` (§5.7): over a row of `W ≤ 4` values whose spread `max − min` fits
/// `i64` (`sm.diff`'s class, which `acc64` certifies), every output lies in
/// `[0, 2^16]`, and the denominator `Σ = Σ_x exp_neg_q16(v_x − m)` is at least 65553
/// (so the division is defined) and at most `W·65553` (`sm.sum`); each numerator lies
/// in `sm.num`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k18_softmax_ranges() {
softmax_case(4);
}
/// K18 on rows of up to 2 values: the same statement on a narrower window.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k18_softmax_ranges_w2() {
softmax_case(2);
}
fn softmax_case(wmax: u32) {
let w: u32 = kani::any();
kani::assume((1..=wmax).contains(&w));
let raw: [i64; 4] = kani::any();
let row = &raw[..w as usize];
let m = *row.iter().max().unwrap();
let mn = *row.iter().min().unwrap();
kani::assume(i64::try_from(m as i128 - mn as i128).is_ok());
// softmax reports, for each element of the row: sm.diff, exp.negx, exp.nl, exp.t2,
// exp.c and the running sum sm.sum; then each numerator sm.num.
let mut rec = Rec::new();
let out = interp::k_softmax(&Tensor::new(1, 1, w, row.to_vec()), 0, &mut rec).expect("softmax is formed");
let w = w as usize;
assert_eq!(rec.n, 7 * w);
let sum = rec.v[6 * w - 1];
assert!(sum >= 65553, "Σ ≥ 65553: the largest element contributes exp_neg_q16(0)");
assert!(sum <= w as i128 * 65553, "Σ lies in sm.sum");
for k in 0..w {
assert!((0..=65553 * 65536 + (w as i128 * 65553) / 2).contains(&rec.v[6 * w + k]), "sm.num lies in its interval");
assert!((0..=1 << 16).contains(&out.data[k]), "every output lies in [0, 2^16]");
}
}
// ------------------------------------------------------------------------------ K20
/// K20. Lemma L1 (`layernorm_c`, §8.3): `|n_c| ≤ N`, the bound the rule records as
/// `ln.n` — `N = min(⌊(h − l)·⌊2^32/r0⌋ / 2^16⌋ + 1, N_max)` with
/// `N_max = ⌊isqrt_ceil(C)·2^16·(r0 + 1) / r0⌋ + 1` and `r0 = isqrt(ε)` — and
/// `U = V + ε` is held in 128 bits. Window: `C ≤ 3`, inputs within `±2^10` (the
/// declared range), any `ε` in `1..=2^64 − 1`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k20_layernorm_lemma_l1() {
let eps: u64 = kani::any();
kani::assume(eps >= 1);
lemma_l1(eps);
}
/// K20 with `ε` in `1..=2^31 − 1`: the same statement on a narrower `ε`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k20_layernorm_lemma_l1_eps_u31() {
let eps: u32 = kani::any();
kani::assume(eps >= 1 && eps < 1 << 31);
lemma_l1(eps as u64);
}
fn lemma_l1(eps: u64) {
let c: u32 = kani::any();
kani::assume((1..=3).contains(&c));
let raw: [i16; 3] = kani::any();
kani::assume(raw.iter().all(|&v| (-1024..=1024).contains(&v)));
let xs: Vec<i64> = raw[..c as usize].iter().map(|&v| v as i64).collect();
let (graw, braw): ([i8; 3], [i8; 3]) = (kani::any(), kani::any());
let gamma: Vec<i64> = graw[..c as usize].iter().map(|&v| v as i64).collect();
let beta: Vec<i64> = braw[..c as usize].iter().map(|&v| v as i64).collect();
// §8.3's ln.n, on the declared range [−2^10, 2^10].
let r0 = (eps as u128).isqrt() as i128;
let ic = match c {
1 => 1,
_ => 2,
}; // isqrt_ceil(C) for C ≤ 3
let nmax = ic * (1 << 16) * (r0 + 1) / r0 + 1;
let k32 = (1i128 << 32) / r0;
let n_rule = (((2048 * k32) >> 16) + 1).min(nmax);
// layernorm_c reports, at one position: ln.S for each channel, ln.m, then ln.d and
// ln.T for each channel, ln.v (U = V + ε, formed in i128), rsq.num and ln.k; then
// ln.dk, ln.n and ln.ng for each channel.
let mut rec = Rec::new();
let _ = interp::k_layernorm(&Tensor::new(c, 1, 1, xs), eps, &gamma, &beta, 0, &mut rec).expect("layernorm_c is formed: U = V + ε is held in 128 bits");
let c = c as usize;
assert_eq!(rec.n, 6 * c + 4);
let j: usize = kani::any();
kani::assume(j < c);
let n = rec.v[3 * c + 4 + 3 * j + 1];
assert!(-n_rule <= n && n <= n_rule, "|n_c| ≤ N (lemma L1)");
}
// ------------------------------------------------------------------------------ K21
/// K21. Lemma L2 (`l2norm`, §8.3): `|v·k| ≤ 2^32`, so every output lies in
/// `[−2^16, 2^16]`, the producer result. Window: `L ≤ 4` positions, values within
/// `±2^20`, any `ε` in `1..=2^64 − 1`.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k21_l2norm_lemma_l2() {
let eps: u64 = kani::any();
kani::assume(eps >= 1);
lemma_l2(eps, 1 << 20);
}
/// K21 with `ε` in `1..=2^20` and values within `±2^10`: the same statement on a
/// narrower window.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(6)]
fn k21_l2norm_lemma_l2_small() {
let eps: u32 = kani::any();
kani::assume(eps >= 1 && eps <= 1 << 20);
lemma_l2(eps as u64, 1 << 10);
}
fn lemma_l2(eps: u64, lim: i32) {
let len: u32 = kani::any();
kani::assume((1..=4).contains(&len));
let raw: [i32; 4] = kani::any();
kani::assume(raw.iter().all(|&v| (-lim..=lim).contains(&v)));
let xs: Vec<i64> = raw[..len as usize].iter().map(|&v| v as i64).collect();
// l2norm reports, for its one channel: l2.T for each position, then U, rsq.num and
// l2.k; then l2.vk for each position.
let mut rec = Rec::new();
let out = interp::k_l2norm(&Tensor::new(1, 1, len, xs), eps, 0, &mut rec).expect("l2norm is formed");
let len = len as usize;
assert_eq!(rec.n, 2 * len + 3);
for j in 0..len {
let vk = rec.v[len + 3 + j];
assert!(-(1i128 << 32) <= vk && vk <= 1i128 << 32, "|v·k| ≤ 2^32 (lemma L2)");
assert!((-(1 << 16)..=1 << 16).contains(&out.data[j]), "the output lies in [−2^16, 2^16]");
}
}

View file

@ -0,0 +1,467 @@
//! The stage operators of §5E: K23–K30.
use alloc::vec;
use alloc::vec::Vec;
use crate::code::Code;
use crate::interp::Tensor;
use crate::stage::mans::{self, Span, Win};
use crate::stage::rans64::{self, End, Escape, Modes, StageRefusal, Table, TableRule};
use crate::stage::{band_lookup, block_stat, bucket_ceil, bucket_floor, i32_form_fits, residual_step, residual_step_i32, scale_index};
// ------------------------------------------------------------------------------ K23
/// K23. `bucket` (E3), rule `ceil`: for any `N ≤ 4` edges and any `σ`, the index lies in
/// `[0, N − 1]`, and it is non-decreasing in `σ`. Window: every edge table of up to 4
/// `i64` edges, ordered or not, and every pair `σ₁ ≤ σ₂`.
#[kani::proof]
#[kani::unwind(6)]
fn k23_bucket_ceil_in_range_and_monotone() {
let e: [i64; 4] = kani::any();
let n: usize = kani::any();
kani::assume((1..=4).contains(&n));
let (a, b): (i64, i64) = (kani::any(), kani::any());
kani::assume(a <= b);
let ia = bucket_ceil(a, &e[..n]).expect("N ≥ 1 edges give an index");
let ib = bucket_ceil(b, &e[..n]).expect("N ≥ 1 edges give an index");
assert!(ia < n && ib < n, "the index lies in [0, N − 1]");
assert!(ia <= ib, "non-decreasing in σ");
}
/// K23. `bucket`, rule `floor`: for `N − 1 ≤ 3` non-decreasing edges (the load check)
/// and any `σ`, the index `#{i : e_i ≤ σ}` lies in `[0, N − 1]` and is non-decreasing in
/// `σ`; edges that decrease are refused. Window: every table of up to 3 `i64` edges.
#[kani::proof]
#[kani::unwind(6)]
fn k23_bucket_floor_in_range_and_monotone() {
let e: [i64; 3] = kani::any();
let m: usize = kani::any();
kani::assume((1..=3).contains(&m));
let (a, b): (i64, i64) = (kani::any(), kani::any());
kani::assume(a <= b);
let ordered = e[..m].windows(2).all(|p| p[0] <= p[1]);
match (bucket_floor(a, &e[..m]), bucket_floor(b, &e[..m])) {
(Some(ia), Some(ib)) => {
assert!(ordered, "only a non-decreasing table gives an index");
assert!(ia <= m && ib <= m, "the index lies in [0, N − 1]");
assert!(ia <= ib, "non-decreasing in σ");
}
(None, None) => assert!(!ordered, "a non-decreasing table is never refused"),
_ => panic!("the load check does not depend on σ"),
}
}
// --------------------------------------------------------------- rANS, from the text
/// integer-payloads §2's decoder, written from the text for K24–K26: the state, the
/// renormalisation, `bits` and the table step, over a stream of `L` bytes.
struct Model<'a> {
s: &'a [u8],
idx: usize,
x: u64,
out: bool,
}
impl<'a> Model<'a> {
fn word(&self, k: usize) -> u64 {
u32::from_le_bytes([self.s[4 * k], self.s[4 * k + 1], self.s[4 * k + 2], self.s[4 * k + 3]]) as u64
}
fn new(s: &'a [u8]) -> Model<'a> {
let mut m = Model { s, idx: 2, x: 0, out: false };
m.x = (m.word(1) << 32) | m.word(0);
m
}
/// If `x < 2^31`: `x = (x << 32) | w` with the next unread word, or, with none left,
/// the stream has run out and `x` stays.
fn renorm(&mut self) {
if self.x < 1 << 31 {
if 4 * self.idx + 4 <= self.s.len() {
self.x = (self.x << 32) | self.word(self.idx);
self.idx += 1;
} else {
self.out = true;
}
}
}
fn bits(&mut self, n: u32) -> u64 {
let v = self.x % (1 << n);
self.x >>= n;
self.renorm();
v
}
/// The table step against `cdf = [0, 2^15, 2^16]`: both symbols have frequency
/// `2^15`. Returns `s`.
fn half_table(&mut self) -> usize {
let cum = self.x % 65536;
let s = if cum >= 32768 { 1 } else { 0 };
self.x = 32768 * (self.x >> 16) + cum - 32768 * s as u64;
self.renorm();
s
}
/// The canonical end: every word read, and the state `2^31`.
fn canonical(&self) -> bool {
!self.out && self.x == 1 << 31 && 4 * self.idx == self.s.len()
}
}
/// E1's `strict(lo, hi)` decode of one symbol after its table step, from the text: the
/// escape's reads, then the checks of one symbol in their order. `n = 3`.
fn model_strict(m: &mut Model, s: usize, offset: i64, lo: i64, hi: i64, canonical: bool) -> Result<i64, Code> {
let n2 = 1i64;
let (sym, count, last) = if (s as i64) < n2 {
(s as i64 + offset, 0u64, 1u64)
} else {
let count = m.bits(4);
let (mut raw, mut last) = (0u64, 0u64);
if count <= 8 {
for j in 0..count {
last = m.bits(4);
raw += last << (4 * j);
}
}
let value = if raw % 2 == 1 { -((raw / 2) as i64) - 1 } else { (raw / 2) as i64 + n2 };
(value + offset, count, last)
};
if canonical && m.out {
return Err(Code::StreamExhausted);
}
if (s as i64) == n2 && count > 8 {
return Err(Code::StreamNoncanonical);
}
if (s as i64) == n2 && count >= 1 && last == 0 {
return Err(Code::StreamNoncanonical);
}
if sym < lo || sym > hi {
return Err(Code::FieldRange);
}
Ok(sym)
}
fn any_end() -> End {
if kani::any() { End::Canonical } else { End::FinalOnly }
}
/// The expected outcome of a strict decode of `k` symbols against `[0, 2^15, 2^16]`.
fn model_strict_decode(stream: &[u8], k: usize, offset: i64, lo: i64, hi: i64, end: End) -> Result<Vec<i64>, StageRefusal> {
let mut m = Model::new(stream);
let mut out = Vec::new();
for i in 0..k {
let s = m.half_table();
match model_strict(&mut m, s, offset, lo, hi, end == End::Canonical) {
Ok(v) => out.push(v),
Err(code) => return Err(StageRefusal { code, at: i as i64 }),
}
}
match end {
End::Canonical if !m.canonical() => Err(StageRefusal { code: Code::StreamNoncanonical, at: k as i64 }),
End::FinalOnly if m.out => Err(StageRefusal { code: Code::StreamExhausted, at: k as i64 }),
_ => Ok(out),
}
}
/// Whether two outcomes of a decode are the same, compared element by element (a
/// comparison of the vectors is a byte loop the checker would unroll).
fn same(a: &Result<Vec<i64>, StageRefusal>, b: &Result<Vec<i64>, StageRefusal>) -> bool {
match (a, b) {
(Ok(x), Ok(y)) => x.len() == y.len() && (0..x.len()).all(|i| x[i] == y[i]),
(Err(x), Err(y)) => x.code == y.code && x.at == y.at,
_ => false,
}
}
// ------------------------------------------------------------------------------ K24
/// K24. `rans64_decode` under `strict(lo, hi)` (E1): it refuses `count > 8` and a
/// non-minimal encoding (`stream_noncanonical`), accepts minimal ones, and when several
/// refusals apply to one symbol it reports the first in E1's order — the run-out under
/// `canonical`, then `count > 8`, then the zero last group, then the range. The decode
/// equals the text's, written out above, symbol for symbol and refusal for refusal.
/// Window: every 64-byte stream, a 3-entry table (`[0, 2^15, 2^16]`), one or two
/// symbols, any offset and range within `i32`, both end rules.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k24_rans64_strict_escape() {
let stream: [u8; 64] = kani::any();
let k: usize = if kani::any() { 1 } else { 2 };
let (offset, lo, hi): (i32, i32, i32) = (kani::any(), kani::any(), kani::any());
kani::assume(lo <= hi);
let end = any_end();
let t = Table { cdf: vec![0, 32768, 65536], offset: offset as i64 };
let modes = Modes { table: TableRule::T4097, escape: Escape::Strict { lo, hi }, end };
let got = rans64::decode(&stream, &[t], &vec![0; k], modes);
let want = model_strict_decode(&stream, k, offset as i64, lo as i64, hi as i64, end);
assert!(same(&got, &want), "the decode is the text's");
kani::cover!(got.as_ref().is_err_and(|r| r.code == Code::StreamNoncanonical && r.at == 0), "a refused escape");
kani::cover!(got.as_ref().is_ok_and(|v| v[0] != offset as i64), "an accepted escape");
}
// ------------------------------------------------------------------------------ K25
/// K25. The `canonical` end rule (E1, integer-payloads §2): a decode is accepted only
/// when, after its last symbol, the state is `2^31` and every word has been read, with
/// no run-out; otherwise it is refused, `stream_noncanonical` at the end. Window: every
/// 32-byte stream, the table `[0, 2^15, 2^16]`, one or two symbols, `strict` over all
/// of `i32`, so no symbol is refused for its range.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k25_rans64_canonical_end() {
let stream: [u8; 32] = kani::any();
let k: usize = if kani::any() { 1 } else { 2 };
let t = Table { cdf: vec![0, 32768, 65536], offset: 0 };
let modes = Modes { table: TableRule::T4097, escape: Escape::Strict { lo: i32::MIN, hi: i32::MAX }, end: End::Canonical };
let got = rans64::decode(&stream, &[t], &vec![0; k], modes);
assert!(same(&got, &model_strict_decode(&stream, k, 0, i32::MIN as i64, i32::MAX as i64, End::Canonical)));
if got.is_ok() {
let mut m = Model::new(&stream);
for _ in 0..k {
let s = m.half_table();
let _ = model_strict(&mut m, s, 0, i32::MIN as i64, i32::MAX as i64, true);
}
assert!(m.x == 1 << 31 && 4 * m.idx == stream.len() && !m.out, "accepted only at state 2^31 with every word read");
}
kani::cover!(got.is_ok(), "a canonical stream");
}
// ------------------------------------------------------------------------------ K26
/// K26. `rans64_decode` under the `t2` table rule with `n = 2`, where every symbol is
/// the escape: total under both escape rules and both end rules — it returns its
/// symbols or a refusal, and never panics or overflows. Window: every 32-byte stream,
/// up to three symbols, any offset, any `strict` range.
#[kani::proof]
#[kani::unwind(12)]
fn k26_rans64_t2_is_total() {
let stream: [u8; 32] = kani::any();
let k: usize = kani::any();
kani::assume(k <= 3);
let offset: i32 = kani::any();
let escape = if kani::any() {
Escape::Lenient
} else {
let (lo, hi): (i32, i32) = (kani::any(), kani::any());
Escape::Strict { lo, hi }
};
let empty = matches!(escape, Escape::Strict { lo, hi } if lo > hi);
let t = Table { cdf: vec![0, 65536], offset: offset as i64 };
let modes = Modes { table: TableRule::T2, escape, end: any_end() };
match rans64::decode(&stream, &[t], &vec![0; k], modes) {
Ok(v) => assert_eq!(v.len(), k),
Err(r) if r.at == -1 => assert!(empty && r.code == Code::ModelMismatch, "only an empty range is refused before any symbol"),
Err(r) => assert!(r.at >= 0 && r.at <= k as i64, "refused at a symbol or at the end"),
}
}
// ------------------------------------------------------------------------------ K27
/// A fixed table of 256 entries (`nb·2^24 + base·2^16 + low`) whose fields take every
/// `nb` from 0 to 8, every base, and low bits `u·257`: the low byte is `u`, so `fac`'s
/// escape symbol 63 occurs, and as a 16-bit value it reaches 0 and −1, so `sgm`'s
/// escape (`sym + β = 0`) occurs for `β` of 0 and 1. Evaluated at compile time.
const fn fixed_table(seed: u32) -> [u32; 256] {
let mut t = [0u32; 256];
let mut k = 0;
while k < 256 {
let u = k as u32;
t[k] = ((u + seed) % 9) << 24 | ((u * 37 + seed) % 256) << 16 | (u * 257);
k += 1;
}
t
}
/// K27. `mans_decode` (E8) is total: for every stream, every bound `B_y` in
/// `[0, 255]`, every mask and every distribution index, a span of the latent decode
/// returns its symbols or refuses, and never panics; each new state is taken mod 2^8
/// (the state is a byte). Window: 16-byte streams, two fixed tables (`fixed_table`),
/// spans of up to 5 positions.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k27_mans_sgm_is_total() {
let stream: [u8; 16] = kani::any();
let Ok(mut w) = Win::init(&stream) else { return };
const TABLES: [[u32; 256]; 2] = [fixed_table(1), fixed_table(7)];
let tables = TABLES;
let bounds: [i64; 2] = kani::any();
kani::assume(bounds.iter().all(|b| (0..=255).contains(b)));
let n: usize = kani::any();
kani::assume(n <= 5);
let mask: [bool; 5] = kani::any();
let sigma: [usize; 5] = kani::any();
kani::assume(sigma.iter().all(|&g| g < 2));
let span = Span { mask: mask[..n].to_vec(), sigma: sigma[..n].to_vec() };
if let Ok(v) = mans::sgm_span(&mut w, &tables, &bounds, &span, 0) {
assert_eq!(v.len(), n);
for (i, &x) in v.iter().enumerate() {
assert!((-32768..=32767).contains(&x), "every symbol is a wrap16 value");
assert!(mask[i] || x == 0, "an unmasked position is 0");
}
}
}
/// K27. The hyper-latent decode (`fac`) of E8 is total: for every 16-byte stream, a
/// row of up to 5 symbols returns or refuses, and never panics. Window: a fixed row
/// table (`fixed_table`).
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(10)]
fn k27_mans_fac_is_total() {
let stream: [u8; 16] = kani::any();
let Ok(mut w) = Win::init(&stream) else { return };
const TZ: [u32; 256] = fixed_table(3);
let tz = TZ;
let n: usize = kani::any();
kani::assume(n <= 5);
if let Ok(v) = mans::fac_row(&mut w, &tz, n, 0) {
assert_eq!(v.len(), n);
assert!(v.iter().all(|&x| (0..=255).contains(&x)), "every symbol is a byte");
}
}
/// K27. `decode_sgm` refuses a bound outside `[0, 255]` before any symbol: `B_y` is
/// unsigned (E8), so a bound read as signed can never reach a decode.
#[kani::proof]
#[kani::unwind(4)]
fn k27_mans_bounds_are_unsigned() {
let b: i64 = kani::any();
kani::assume(!(0..=255).contains(&b));
let pmf = vec![vec![256i64]];
let r = mans::decode_sgm(&[0x80, 0, 0, 0, 0, 0, 0, 1], &pmf, &[b], &[]);
assert_eq!(r, Err(StageRefusal { code: Code::ModelMismatch, at: -1 }), "a bound outside [0, 255] is refused before any symbol");
}
// ------------------------------------------------------------------------------ K28
/// K28. The latent table build of E8, for any row that passes the load checks
/// (non-negative, summing to exactly 256): every slot is written, and `next ∈ [1, 511]`.
/// A slot no column writes would keep `next = 0`. Window: every row of width 8, every
/// entry symbolic; a narrower row builds the table its zero-extension to width 8 does
/// (the columns past its sum write nothing), so this covers every width up to 8.
#[cfg(kani_open)]
#[kani::proof]
#[kani::unwind(258)]
fn k28_mans_every_slot_written() {
let raw: [u16; 8] = kani::any();
let row: [i64; 8] = [raw[0] as i64, raw[1] as i64, raw[2] as i64, raw[3] as i64, raw[4] as i64, raw[5] as i64, raw[6] as i64, raw[7] as i64];
kani::assume(row.iter().all(|&p| p <= 256) && row.iter().sum::<i64>() == 256);
let (_, next) = mans::latent_slots(&row);
for &n in next.iter() {
assert!((1..=511).contains(&n), "every slot is written, with next ∈ [1, 511]");
}
}
/// K28. Each entry of a latent table, for every `next ∈ [1, 511]` (K28 above) and every
/// symbol: `nb ≤ 8`, `base = (next·2^nb) XOR 256 < 256`, and the three fields do not
/// overlap, so `nb`, `base` and `sym mod 2^16` read back from the entry.
#[kani::proof]
#[kani::unwind(11)]
fn k28_mans_entry_fields() {
let (sym, next): (i16, i64) = (kani::any(), kani::any());
kani::assume((1..=511).contains(&next));
let e = mans::latent_entry(sym as i64, next);
let nb = e >> 24;
assert!(nb <= 8, "nb ≤ 8");
assert!(next << nb >= 256 && (nb == 0 || next << (nb - 1) < 256), "nb doublings bring next to 256 or more, and no fewer");
assert_eq!((e >> 16) & 0xFF, ((next << nb) ^ 256) as u32, "base = (next·2^nb) XOR 256");
assert!((((next << nb) ^ 256) as u32) < 256, "base < 256");
assert_eq!(e & 0xFFFF, sym as u16 as u32, "sym mod 2^16");
}
// ------------------------------------------------------------------------------ K29
/// K29. `residual_step` (E9): its 32-bit form equals it, and every value that form
/// computes fits `i32` (each operation is checked), for every `r` in `i32`, every
/// `q ∈ [1, 65535]`, every `pred ∈ [lo, hi]`, and every range on which the form is
/// stated (`hi − lo ≤ 65535`, with `lo − 131070` and `hi + 131070` in `i32`): the
/// registered `U8` and `I16` ranges among them.
#[kani::proof]
fn k29_residual_step_32bit_identity_every_range() {
let (lo, hi): (i32, i32) = (kani::any(), kani::any());
kani::assume(i32_form_fits(lo, hi));
let (pred, r, q): (i32, i32, i32) = (kani::any(), kani::any(), kani::any());
kani::assume(lo <= pred && pred <= hi && (1..=65535).contains(&q));
let exact = residual_step(pred as i64, r as i64, q as i64, lo as i64, hi as i64).expect("in E9's domain");
assert_eq!(residual_step_i32(pred, r, q, lo, hi) as i64, exact, "the 32-bit form is E9");
kani::cover!(lo == 0 && hi == 255, "U8");
kani::cover!(lo == -32768 && hi == 32767, "I16");
}
fn residual_case(lo: i32, hi: i32) {
let (pred, r, q): (i32, i32, i32) = (kani::any(), kani::any(), kani::any());
kani::assume(lo <= pred && pred <= hi && (1..=65535).contains(&q));
let exact = residual_step(pred as i64, r as i64, q as i64, lo as i64, hi as i64).expect("in E9's domain");
assert_eq!(residual_step_i32(pred, r, q, lo, hi) as i64, exact, "the 32-bit form is E9");
}
/// K29 on the registered ranges, `U8 = [0, 255]` and `I16 = [−32768, 32767]`: the
/// 32-bit form equals E9 and every value it computes fits `i32`, for every `r` in
/// `i32`, every `q ∈ [1, 65535]` and every `pred` in the range.
#[kani::proof]
fn k29_residual_step_32bit_identity_registered_ranges() {
if kani::any() { residual_case(0, 255) } else { residual_case(-32768, 32767) }
}
// ------------------------------------------------------------------------------ K30
/// K30. E5 `scale_index` lies in `[0, n − 1]` for every `v` in `i64`, every shift in
/// `0..=127` and every `n ≥ 1`.
#[kani::proof]
fn k30_scale_index_in_range() {
let (v, s, n): (i64, u32, i64) = (kani::any(), kani::any(), kani::any());
kani::assume(s <= 127 && n >= 1);
let i = scale_index(v, s, n).expect("defined for n ≥ 1 and s ≤ 127");
assert!(0 <= i && i < n);
}
fn block_case(h: u32, w: u32, b: u32) {
let raw: [i32; 9] = kani::any();
let data: Vec<i64> = raw[..(h * w) as usize].iter().map(|&v| v as i64).collect();
let (s, lo, hi): (u32, i64, i64) = (kani::any(), kani::any(), kani::any());
kani::assume(s <= 127 && lo <= hi);
let out = block_stat(&Tensor::new(1, h, w, data), b, s, lo, hi).expect("b ≥ 1, s ≤ 127, lo ≤ hi");
let (y, x): (u32, u32) = (kani::any(), kani::any());
kani::assume(y < h && x < w);
let v = out.data[(y * w + x) as usize];
assert!(lo <= v && v <= hi);
let (y0, x0) = ((y / b) * b, (x / b) * b);
assert_eq!(v, out.data[(y0 * w + x0) as usize], "a block shares one value");
}
/// K30. E6 `block_stat`: every output lies in `[lo, hi]`, and every position of a block
/// takes its block's value. Window: the shapes below — full blocks, partial last
/// blocks in either direction, and blocks of 1 — with one channel, any shift in
/// `0..=127`, any range and any `i32` inputs.
#[kani::proof]
#[kani::unwind(11)]
fn k30_block_stat_in_range() {
match kani::any::<u8>() % 4 {
0 => block_case(3, 3, 2),
1 => block_case(3, 2, 3),
2 => block_case(2, 3, 1),
_ => block_case(1, 1, 1),
}
}
/// K30. E7 `band_lookup`: for up to 3 edges and any `v`, the band is `#{k : v ≥ e_k}`
/// and the result is that entry of the row; a row of the wrong length, or a row index
/// past the table, is refused rather than read out of range.
#[kani::proof]
#[kani::unwind(6)]
fn k30_band_lookup_in_range() {
let m: usize = kani::any();
kani::assume(m <= 3);
let e: [i64; 3] = kani::any();
let v: i64 = kani::any();
let row_len: usize = kani::any();
kani::assume(row_len <= 4);
let cells: [i64; 4] = kani::any();
let table = vec![cells[..row_len].to_vec()];
let row: usize = kani::any();
kani::assume(row <= 1);
let band = e[..m].iter().filter(|&&x| v >= x).count();
match band_lookup(v, &e[..m], &table, row) {
Some(r) => assert!(row == 0 && row_len == m + 1 && r == cells[band]),
None => assert!(row == 1 || row_len != m + 1),
}
}

View file

@ -9,7 +9,7 @@ use crate::code::Code;
use crate::stage::rans64::StageRefusal;
/// The window and the two states.
struct Win {
pub(crate) struct Win {
m: Vec<u8>,
p: usize,
h: u64,
@ -22,7 +22,7 @@ struct Win {
pos: i64,
}
type R<T> = Result<T, ()>;
pub(crate) type R<T> = Result<T, ()>;
impl Win {
fn le64(&self, i: usize) -> u64 {
@ -30,7 +30,7 @@ impl Win {
b.copy_from_slice(&self.m[i..i + 8]);
u64::from_le_bytes(b)
}
fn init(stream: &[u8]) -> R<Win> {
pub(crate) fn init(stream: &[u8]) -> R<Win> {
let mut m = vec![0u8; 8];
m.extend_from_slice(stream);
let mut w = Win { m, p: stream.len(), h: 0, b: 0, s: [0, 0], pos: 0 };
@ -90,43 +90,61 @@ pub fn build_latent_tables(pmf: &[Vec<i64>]) -> Option<Vec<[u32; 256]>> {
if row.len() != w || row.iter().any(|&p| p < 0) || row.iter().sum::<i64>() != 256 {
return None;
}
let mut c = vec![0i64; w + 1];
for k in 0..w {
c[k + 1] = c[k] + row[k];
}
let f = |k: usize| c[k] - c[k] / 2;
let g = |k: usize| c[k] / 2 + 128;
let sym = |k: usize| -> i64 { if k % 2 == 0 { (k / 2) as i64 } else { -(((k + 1) / 2) as i64) } };
let mut symbols = [0i64; 256];
let mut next = [0i64; 256];
let mut k = 0;
while c[k] < 256 {
for u in f(k)..f(k + 1) {
symbols[u as usize] = sym(k);
next[u as usize] = row[k] + (u - f(k));
}
for u in g(k)..g(k + 1) {
symbols[u as usize] = sym(k);
next[u as usize] = row[k] + (f(k + 1) - f(k)) + (u - g(k));
}
k += 1;
}
let (symbols, next) = latent_slots(row);
let mut t = [0u32; 256];
for u in 0..256 {
// Every slot is written and next ∈ [1, 511], so nb ≤ 8; the bound only keeps
// the loop total.
let mut nb = 0u32;
while nb < 9 && next[u] << nb < 256 {
nb += 1;
}
let base = ((next[u] << nb) ^ 256) as u32;
t[u] = (nb << 24) | (base << 16) | (symbols[u].rem_euclid(65536) as u32);
t[u] = latent_entry(symbols[u], next[u]);
}
out.push(t);
}
Some(out)
}
/// Each slot's symbol and `next` for one row that passed the load checks (§5E E8,
/// *Building the latent tables*). A slot no column writes keeps `next = 0`.
pub(crate) fn latent_slots(row: &[i64]) -> ([i64; 256], [i64; 256]) {
let w = row.len();
let mut c = vec![0i64; w + 1];
for k in 0..w {
c[k + 1] = c[k] + row[k];
}
let f = |k: usize| c[k] - c[k] / 2;
let g = |k: usize| c[k] / 2 + 128;
let sym = |k: usize| -> i64 { if k % 2 == 0 { (k / 2) as i64 } else { -(((k + 1) / 2) as i64) } };
let mut symbols = [0i64; 256];
let mut next = [0i64; 256];
// Each column k, while C[k] < 256, writes the slots of [F[k], F[k+1]) and
// [G[k], G[k+1]). Every slot is visited by its index, so each loop has a fixed
// bound.
for k in 0..w {
if c[k] >= 256 {
break;
}
for u in 0..256i64 {
if f(k) <= u && u < f(k + 1) {
symbols[u as usize] = sym(k);
next[u as usize] = row[k] + (u - f(k));
}
if g(k) <= u && u < g(k + 1) {
symbols[u as usize] = sym(k);
next[u as usize] = row[k] + (f(k + 1) - f(k)) + (u - g(k));
}
}
}
(symbols, next)
}
/// A slot's entry: `nb·2^24 + base·2^16 + (sym mod 2^16)`. Every slot is written and
/// `next ∈ [1, 511]`, so `nb ≤ 8`; the bound on the loop only keeps it total.
pub(crate) fn latent_entry(sym: i64, next: i64) -> u32 {
let mut nb = 0u32;
while nb < 9 && next << nb < 256 {
nb += 1;
}
let base = ((next << nb) ^ 256) as u32;
(nb << 24) | (base << 16) | (sym.rem_euclid(65536) as u32)
}
/// One `sgm` span: the masked positions and their distributions.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Span {
@ -138,10 +156,10 @@ pub struct Span {
/// and the spans, decoded in order from one window. Returns one array per span.
pub fn decode_sgm(stream: &[u8], pmf: &[Vec<i64>], bounds: &[i64], spans: &[Span]) -> Result<Vec<Vec<i64>>, StageRefusal> {
let before = StageRefusal { code: Code::ModelMismatch, at: -1 };
let tables = build_latent_tables(pmf).ok_or(before)?;
if bounds.iter().any(|&b| !(0..=255).contains(&b)) {
return Err(before);
}
let tables = build_latent_tables(pmf).ok_or(before)?;
let nd = tables.len().min(bounds.len());
for s in spans {
if s.mask.len() != s.sigma.len() || s.sigma.iter().any(|&g| g >= nd) {
@ -162,7 +180,7 @@ pub fn decode_sgm(stream: &[u8], pmf: &[Vec<i64>], bounds: &[i64], spans: &[Span
Ok(out)
}
fn sgm_span(w: &mut Win, t: &[[u32; 256]], bounds: &[i64], s: &Span, base: i64) -> R<Vec<i64>> {
pub(crate) fn sgm_span(w: &mut Win, t: &[[u32; 256]], bounds: &[i64], s: &Span, base: i64) -> R<Vec<i64>> {
let n = s.mask.len();
let mut sym = vec![0i64; n];
let inb = |w: &mut Win, which: usize, i: usize, sym: &mut [i64]| -> R<()> {
@ -266,7 +284,7 @@ pub fn decode_fac(stream: &[u8], rows: &[Vec<i64>], lengths: &[usize]) -> Result
Ok(out)
}
fn fac_row(w: &mut Win, tz: &[u32; 256], n: usize, base: i64) -> R<Vec<i64>> {
pub(crate) fn fac_row(w: &mut Win, tz: &[u32; 256], n: usize, base: i64) -> R<Vec<i64>> {
let mut sym = vec![0i64; n];
let inb = |w: &mut Win, which: usize, i: usize, sym: &mut [i64]| -> R<()> {
w.pos = base + i as i64;

View file

@ -9,6 +9,7 @@ use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use crate::bounds::Profile;
use crate::code::Code;
use crate::graph;
use crate::interp::{self, Sequence, Tensor};
@ -66,17 +67,21 @@ pub fn tensor_c(t: &Tensor) -> C {
])
}
/// Recompute one graph vector (§14.1): `load`, and every decode's `expect`.
/// Recompute one graph vector (§14.1): `load` (steps 1–8), `profiles` and every
/// decode's `expect`. A graph that loads lists exactly the profiles its verdict
/// meets, and its decodes run; one whose load is refused lists both profiles, since
/// an implementation of either refuses it, and has no decodes. The `profiles` a line
/// carries are not read: they are recomputed.
pub fn graph_line(v: &Value) -> Result<C, Bad> {
let hexg = get(v, "graph")?.as_str().ok_or("graph is not a string")?;
let bytes = unhex(hexg).ok_or("graph is not lowercase hexadecimal")?;
let id = get(v, "id")?.as_str().ok_or("id is not a string")?;
let profiles = arr(v, "profiles")?;
arr(v, "profiles")?;
let decodes = arr(v, "decodes")?;
let loaded = graph::load(&bytes);
let load = match &loaded {
Ok(_) => C::s("ok"),
Err(r) => C::s(r.code.as_str()),
let loaded = graph::load(&bytes, Profile::Acc64);
let (load, profiles) = match &loaded {
Ok(g) => (C::s("ok"), Profile::ALL.iter().filter(|p| g.meets(**p)).map(|p| C::s(p.as_str())).collect()),
Err(r) => (C::s(r.code.as_str()), Profile::ALL.iter().map(|p| C::s(p.as_str())).collect()),
};
let mut out = Vec::new();
if let Ok(g) = &loaded {
@ -100,7 +105,7 @@ pub fn graph_line(v: &Value) -> Result<C, Bad> {
("graph", C::Str(hex(&bytes))),
("id", C::s(id)),
("load", load),
("profiles", C::Arr(profiles.iter().map(|p| C::s(p.as_str().unwrap_or("?"))).collect())),
("profiles", C::Arr(profiles)),
]))
}

View file

@ -17,9 +17,18 @@ use core::cmp::Ordering;
const N: usize = 8;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[derive(Clone, Copy, Eq, Hash, Debug)]
pub struct I512([u64; N]);
/// Limb by limb, written out: the derived comparison of the array is a byte loop,
/// which a model checker must unwind 64 times at every use.
impl PartialEq for I512 {
fn eq(&self, o: &I512) -> bool {
let (a, b) = (&self.0, &o.0);
a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3] && a[4] == b[4] && a[5] == b[5] && a[6] == b[6] && a[7] == b[7]
}
}
impl Default for I512 {
fn default() -> Self {
I512::ZERO
@ -44,6 +53,12 @@ impl I512 {
pub const fn from_u64(v: u64) -> I512 {
I512::from_u128(v as u128)
}
/// The value whose eight little-endian 64-bit limbs are `l`: a symbolic value for
/// the model checker.
#[cfg(kani)]
pub(crate) const fn from_limbs(l: [u64; N]) -> I512 {
I512(l)
}
pub fn is_neg(&self) -> bool {
self.0[N - 1] >> 63 == 1
@ -51,6 +66,10 @@ impl I512 {
pub fn is_zero(&self) -> bool {
self.0.iter().all(|&l| l == 0)
}
/// Whether the value is odd (`v mod 2 = 1`), negative values included.
pub fn is_odd(&self) -> bool {
self.0[0] & 1 == 1
}
fn not(self) -> I512 {
let mut r = self.0;

View file

@ -5,15 +5,14 @@
//! what a capability's driver does around its graphs (§13.5). They check the lowerings
//! and the operators against outputs that were fixed before either existed.
//!
//! Every graph a driver runs goes through `graph::load` of its bytes, as a sink's
//! does. That is why these tests need the `uncertified` switch: until the certifier
//! exists, load step 8 refuses every graph without it.
#![cfg(feature = "uncertified")]
//! Every graph a driver runs goes through `graph::load` of its bytes under `acc64`,
//! the profile every lowering of §13 declares, as a sink's does: load step 8 runs the
//! bounds pass and refuses a set whose certificate does not meet it.
use std::path::{Path, PathBuf};
use wai_ops::graph;
use wai_ops::interp::{Sequence, Tensor, run};
use wai_ops::{Profile, bounds, graph, plan};
use wai_ops::interp::{Sequence, Tensor, run_observed};
use wai_ops::json::{self, Value, hex};
use wai_ops::lower;
use wai_ops::sha256::sha256;
@ -42,10 +41,19 @@ fn hwc(t: &Tensor) -> Vec<u8> {
o
}
/// One decode of a lowered graph with one data input: loaded from its bytes, as a sink
/// loads it, then run.
/// loads it, then run, observed: every value each operator forms under a named
/// intermediate (§8.5, Table 8.5-A), and every producer result, must lie in the interval
/// the graph's bounds pass records for it.
fn one(g: &wai_ops::Graph, x: Tensor, sc: &[i64]) -> Tensor {
let g = graph::load(&g.to_bytes()).expect("a lowered graph loads");
run(&g, &[x], sc, &mut Sequence::new()).unwrap().remove(0)
let g = graph::load(&g.to_bytes(), Profile::Acc64).expect("a lowered graph loads");
let sizes = plan::load_plan(&g).unwrap();
let b = bounds::pass(&g, sizes.as_deref()).unwrap();
let mut w = bounds::Within::new(&b);
let out = run_observed(&g, &[x], sc, &mut Sequence::new(), &mut w).unwrap().remove(0);
assert!(w.outside.is_empty(), "values outside their certified intervals: {:?}", w.outside);
assert!(w.unrecorded.is_empty(), "values under keys the certificate does not record: {:?}", w.unrecorded);
assert!(w.seen > 0);
out
}
fn u32le(b: &[u8], o: usize) -> u32 {
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())

View file

@ -1,33 +1,61 @@
//! Without the `uncertified` switch, which only the tests, the conformance tool and
//! its browser-engine build enable, load step 8 refuses every graph (§10.5): until the
//! certifier exists, the default build decodes nothing. The interpreter takes only a
//! `Loaded` graph, which only `graph::load` makes, so a lowered set's graphs cannot be
//! run around that refusal. (That `run` rejects a `Graph` at compile time is the
//! `compile_fail` example on `Loaded`.)
//! Load step 8 (§10.5): every graph loads under a profile, and loads only when its
//! verdict meets that profile (§8.6). The interpreter takes only a `Loaded` graph,
//! which only `graph::load` makes, so a lowered set's graphs cannot be run around
//! the verdict. (That `run` rejects a `Graph` at compile time is the `compile_fail`
//! example on `Loaded`.)
use std::path::PathBuf;
use wai_ops::code::{Code, Refusal, Site};
use wai_ops::{graph, lower};
use wai_ops::{Profile, graph, lower};
fn read(p: &str) -> Vec<u8> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join(p);
std::fs::read(&path).unwrap_or_else(|e| panic!("{p}: {e}"))
}
/// Every lowered graph of the committed int_synth sets loads through steps 1–7, and
/// then step 8 refuses it in the default build and passes it with the switch.
fn step8() -> Refusal {
Refusal::new(Code::ModelMismatch, Site::Load(8))
}
/// Every lowered graph of the committed int_synth sets loads under `acc64`. The
/// facade, gradient and bloom sets also load under `acc32`; photo does not, and its
/// `acc32` load is refused at step 8.
#[test]
fn a_lowered_set_decodes_only_through_load_step_8() {
for name in ["gradient", "bloom", "facade", "photo"] {
fn a_lowered_set_loads_only_under_a_profile_its_verdict_meets() {
for (name, acc32) in [("gradient", true), ("bloom", true), ("facade", true), ("photo", false)] {
let set = lower::int_synth(&read(&format!("neural-conformance/vectors/{name}/int_synth.json"))).unwrap();
for (role, g) in &set.roles {
let r = graph::load(&g.to_bytes());
if cfg!(feature = "uncertified") {
assert!(r.is_ok(), "{name}/{role}");
} else {
assert_eq!(r.unwrap_err(), Refusal::new(Code::ModelMismatch, Site::Load(8)), "{name}/{role}");
let b = g.to_bytes();
let l = graph::load(&b, Profile::Acc64).unwrap_or_else(|e| panic!("{name}/{role}: {e:?}"));
assert_eq!(l.meets(Profile::Acc32), acc32, "{name}/{role}");
match graph::load(&b, Profile::Acc32) {
Ok(_) => assert!(acc32, "{name}/{role} loads under acc32"),
Err(e) => {
assert!(!acc32, "{name}/{role} is refused under acc32");
assert_eq!(e, step8(), "{name}/{role}");
}
}
}
}
}
/// The overflow set of §14.3, whose accumulator passes i64 by one unit, is refused at
/// step 8 under both profiles, and so is the same set one unit inside.
#[test]
fn the_overflow_set_is_refused_at_step_8() {
let set = lower::int_synth(&read("ops-conformance/sets/overflow_acc64/int_synth.json")).unwrap();
let b = set.get("synth").unwrap().to_bytes();
for p in Profile::ALL {
assert_eq!(graph::load(&b, p).unwrap_err(), step8(), "{p:?}");
}
}
/// The set of §14.3 whose pin claims `acc32` meets `acc64` and fails `acc32`.
#[test]
fn the_acc32_claim_set_loads_under_acc64_only() {
let set = lower::int_synth(&read("ops-conformance/sets/claim_acc32/int_synth.json")).unwrap();
let b = set.get("synth").unwrap().to_bytes();
assert!(!graph::load(&b, Profile::Acc64).unwrap().meets(Profile::Acc32));
assert_eq!(graph::load(&b, Profile::Acc32).unwrap_err(), step8());
}

151
wai/wai-ops/tests/trace.rs Normal file
View file

@ -0,0 +1,151 @@
//! Hand-built graphs at the edge of what `acc64` certifies, run observed: every value
//! each operator forms under a named intermediate (§8.5, Table 8.5-A), and every
//! producer result, lies in the interval the bounds pass records for it.
//!
//! Each graph loads from its bytes under `acc64` (load step 8 included), and is run on
//! inputs at the ends of their declared ranges — all at one end, all at the other, the
//! two ends alternating in both phases — and on pseudo-random inputs within them.
use wai_ops::bounds::{self, Within};
use wai_ops::graph::{self, BiasKind, CAP, Graph, InputRec, Op, Params, Round, Rq, ScalarRec, TensorRec, opcode::*};
use wai_ops::interp::{Sequence, Tensor, run_observed};
use wai_ops::{Profile, plan};
fn rng(s: &mut u64) -> u64 {
*s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*s >> 11
}
/// A one-operator graph: inputs with records `ins` (declared sizes `hw`, or none), one
/// output.
fn graph1(ins: Vec<TensorRec>, out: TensorRec, code: u8, p: Params, scalars: Vec<ScalarRec>, hw: (u32, u32)) -> Graph {
let n = ins.len() as u32;
let mut tensors = ins;
tensors.push(out);
Graph {
max_elems: CAP as u32,
scalars,
inputs: (0..n).map(|t| InputRec { tensor: t, max_h: hw.0, max_w: hw.1, kind: 0, src: 0, init: 0 }).collect(),
outputs: vec![n],
tensors,
ops: vec![Op { code, ins: (0..n).collect(), outs: vec![n], p }],
}
}
/// Inputs for one run: pattern 0 all at `lo`, 1 all at `hi`, 2 and 3 alternating, and
/// above 3 pseudo-random within `[lo, hi]`.
fn fill(c: u32, h: u32, w: u32, lo: i64, hi: i64, pattern: u32, s: &mut u64) -> Tensor {
let n = (c * h * w) as usize;
let data = (0..n)
.map(|k| match pattern {
0 => lo,
1 => hi,
2 => if k % 2 == 0 { lo } else { hi },
3 => if k % 2 == 0 { hi } else { lo },
_ => {
let span = (hi as i128 - lo as i128) as u128 + 1;
(lo as i128 + (rng(s) as u128 % span) as i128) as i64
}
})
.collect();
Tensor::new(c, h, w, data)
}
/// Load `g` under `acc64`, then run it on every pattern for its inputs' records and
/// `shapes`, observed; every value must lie in its interval.
fn check(name: &str, g: Graph, shapes: &[(u32, u32)], scalars: &[i64]) {
let l = graph::load(&g.to_bytes(), Profile::Acc64).unwrap_or_else(|r| panic!("{name}: refused at load: {r:?}"));
let sizes = plan::load_plan(&l).unwrap();
let b = bounds::pass(&l, sizes.as_deref()).unwrap();
let mut s = 0x9E3779B97F4A7C15u64;
let mut seen = 0u64;
for pattern in 0..12 {
let inputs: Vec<Tensor> = l.inputs.iter().zip(shapes).map(|(i, &(h, w))| {
let t = &l.tensors[i.tensor as usize];
fill(t.channels, h, w, t.lo, t.hi, pattern, &mut s)
}).collect();
let mut obs = Within::new(&b);
run_observed(&l, &inputs, scalars, &mut Sequence::new(), &mut obs).unwrap_or_else(|r| panic!("{name}: pattern {pattern} refused: {r:?}"));
assert!(obs.outside.is_empty(), "{name}: pattern {pattern}: outside: {:?}", obs.outside);
assert!(obs.unrecorded.is_empty(), "{name}: pattern {pattern}: unrecorded: {:?}", obs.unrecorded);
seen += obs.seen;
}
assert!(seen > 0, "{name}");
}
fn rq(round: Round, bias: BiasKind, b: Vec<i64>, m: Option<Vec<i64>>, shift: u8) -> Rq {
let (m_kind, m_bytes, mult) = match m {
None => (0, 0, vec![]),
Some(v) => (2, 8, v),
};
let b_bytes = if bias == BiasKind::None { 0 } else { 8 };
Rq { round, bias_kind: bias, m_kind, s_kind: 1, m_bytes, b_bytes, mult, shift: vec![shift], bias: if bias == BiasKind::None { vec![] } else { b } }
}
#[test]
fn convolutions_at_the_edge() {
// 18 terms of 2^20 × 2^38: 2^62.2, under i64; products by ±2^30, under i128.
let w: Vec<i64> = (0..36).map(|k| if k % 3 == 0 { -(1 << 20) } else { 1 << 20 }).collect();
for round in [Round::Floor, Round::HalfUp, Round::HalfEven] {
let r = rq(round, BiasKind::Post, vec![i32::MAX as i64, i32::MIN as i64], Some(vec![1 << 30, -(1 << 30)]), 40);
let p = Params::Conv { cout: 2, kh: 3, kw: 3, sh: 1, sw: 1, ph: 1, pw: 1, groups: 1, w_bytes: 4, rq: r, w: w.clone() };
check("conv", graph1(vec![TensorRec::check(2, -(1 << 38), 1 << 38)], TensorRec::derived(2), CONV, p, vec![], (0, 0)), &[(4, 5)], &[]);
let r = rq(round, BiasKind::Pre, vec![-(1 << 61), 1 << 61], None, 63);
let p = Params::ConvT { cout: 2, kh: 3, kw: 3, sh: 2, sw: 2, ph: 1, pw: 1, oph: 1, opw: 1, w_bytes: 4, rq: r, w: w.clone() };
check("conv_transpose", graph1(vec![TensorRec::check(2, -(1 << 38), 1 << 38)], TensorRec::derived(2), CONV_TRANSPOSE, p, vec![], (0, 0)), &[(3, 4)], &[]);
}
}
#[test]
fn requantisations_and_products_at_the_edge() {
let r = rq(Round::HalfUp, BiasKind::Pre, vec![i64::MIN / 2, i64::MAX / 2], Some(vec![i64::MAX, i64::MIN]), 127);
check("rescale", graph1(vec![TensorRec::check(2, i64::MIN / 2, i64::MAX / 2)], TensorRec::derived(2), RESCALE, Params::Rescale(r), vec![], (0, 0)), &[(2, 3)], &[]);
let p = Params::Chscale { scalar: 0, n_rows: 2, round: Round::HalfEven, m_bytes: 8, shift: 64, m: vec![i64::MAX, i64::MIN, -1, 1] };
check("chscale", graph1(vec![TensorRec::check(2, i64::MIN, i64::MAX)], TensorRec::derived(2), CHSCALE, p.clone(), vec![ScalarRec { lo: 0, hi: 1 }], (0, 0)), &[(2, 2)], &[0]);
check("chscale", graph1(vec![TensorRec::check(2, i64::MIN, i64::MAX)], TensorRec::derived(2), CHSCALE, p, vec![ScalarRec { lo: 0, hi: 1 }], (0, 0)), &[(2, 2)], &[1]);
let r = rq(Round::Floor, BiasKind::None, vec![], Some(vec![1 << 40, -(1 << 40)]), 70);
check("mul", graph1(vec![TensorRec::check(2, -(1 << 31), 1 << 31), TensorRec::check(1, i32::MIN as i64, i32::MAX as i64)], TensorRec::derived(2), MUL, Params::Mul(r), vec![], (0, 0)), &[(2, 2), (2, 2)], &[]);
let gr = Rq { round: Round::HalfUp, bias_kind: BiasKind::None, m_kind: 0, s_kind: 1, m_bytes: 0, b_bytes: 0, mult: vec![], shift: vec![16], bias: vec![] };
check("gram", graph1(vec![TensorRec::check(4, -(1 << 28), 1 << 28), TensorRec::check(4, -(1 << 28), 1 << 28)], TensorRec::derived(4), GRAM, Params::Gram { groups: 2, rq: gr.clone() }, vec![], (3, 3)), &[(3, 3), (3, 3)], &[]);
check("apply", graph1(vec![TensorRec::check(4, -(1 << 30), 1 << 30), TensorRec::check(4, -(1 << 31), 1 << 31)], TensorRec::derived(4), APPLY, Params::Apply { groups: 2, rq: gr }, vec![], (0, 0)), &[(1, 2), (3, 3)], &[]);
}
#[test]
fn elementwise_at_the_edge() {
check("leaky", graph1(vec![TensorRec::check(1, -(1 << 31), i64::MAX)], TensorRec::derived(1), LEAKY, Params::Leaky { a: 1 << 31, f: 63 }, vec![], (0, 0)), &[(3, 3)], &[]);
check("leaky", graph1(vec![TensorRec::check(1, i32::MIN as i64, 5)], TensorRec::derived(1), LEAKY, Params::Leaky { a: i32::MIN as i64, f: 0 }, vec![], (0, 0)), &[(3, 3)], &[]);
let mut t = vec![0i16; 65536];
for (k, v) in t.iter_mut().enumerate().skip(1) {
*v = ((k as i64 * 7919) % 65536 - 32768) as i16;
}
check("lut16", graph1(vec![TensorRec::check(1, -32767, 32767)], TensorRec::derived(1), LUT16, Params::Lut16 { e_in: -128, e_out: -128, t }, vec![], (0, 0)), &[(4, 4)], &[]);
check("abs", graph1(vec![TensorRec::check(1, i64::MIN + 1, i64::MAX)], TensorRec::derived(1), ABS, Params::None, vec![], (0, 0)), &[(2, 2)], &[]);
check("chunk_add", graph1(vec![TensorRec::check(2, i64::MIN / 2, i64::MAX / 2)], TensorRec::derived(1), CHUNK_ADD, Params::None, vec![], (0, 0)), &[(2, 2)], &[]);
}
#[test]
fn normalisations_and_scalar_functions_at_the_edge() {
// igdn: γ·x² of 2^31 × 2^62 = 2^93, under i128; r of the final norm under 2^47.
let p = Params::Igdn { gf: 0, frac: 30, g_bytes: 4, gamma: vec![i32::MAX as i64, -1, 7, i32::MIN as i64], beta: vec![i64::MAX, i64::MIN] };
check("igdn", graph1(vec![TensorRec::check(2, -(1 << 31), 1 << 31)], TensorRec::derived(2), IGDN, p, vec![], (0, 0)), &[(2, 3)], &[]);
for eps in [1u64, 1 << 32, u64::MAX] {
let p = Params::LayerNorm { eps, g_bytes: 8, gamma: vec![i32::MAX as i64, i32::MIN as i64, 1], beta: vec![i64::MAX / 4, i64::MIN / 4, 0] };
check("layernorm_c", graph1(vec![TensorRec::check(3, -(1 << 40), 1 << 40)], TensorRec::derived(3), LAYERNORM_C, p, vec![], (0, 0)), &[(2, 2)], &[]);
check("l2norm", graph1(vec![TensorRec::check(2, -(1 << 40), 1 << 40)], TensorRec::derived(2), L2NORM, Params::L2norm { eps }, vec![], (3, 3)), &[(3, 3)], &[]);
}
// The widest spread acc64 allows: exp.negx = h − l = 2^63 − 1. One more, and the
// set is refused at load (below).
check("softmax", graph1(vec![TensorRec::check(2, -(1 << 62), (1 << 62) - 1)], TensorRec::derived(2), SOFTMAX, Params::None, vec![], (2, 5)), &[(2, 5)], &[]);
let wide = graph1(vec![TensorRec::check(2, -(1 << 62), 1 << 62)], TensorRec::derived(2), SOFTMAX, Params::None, vec![], (2, 5));
assert!(graph::load(&wide.to_bytes(), Profile::Acc64).is_err(), "exp.negx = 2^63 does not fit i64");
check("softmax", graph1(vec![TensorRec::check(1, -5, 7)], TensorRec::derived(1), SOFTMAX, Params::None, vec![], (1, 4)), &[(1, 4)], &[]);
check("sigmoid", graph1(vec![TensorRec::check(1, i64::MIN + 1, i64::MAX)], TensorRec::derived(1), SIGMOID, Params::None, vec![], (0, 0)), &[(3, 3)], &[]);
check("sigmoid", graph1(vec![TensorRec::check(1, -200000, 200000)], TensorRec::derived(1), SIGMOID, Params::None, vec![], (0, 0)), &[(3, 3)], &[]);
// α = −2^63 gives ⌊α·(−2^16) / 2^16⌋ = 2^63, past i64: refused at load.
let wide = graph1(vec![TensorRec::check(1, -1, 0)], TensorRec::derived(1), ELU, Params::Elu { alpha: i64::MIN }, vec![], (0, 0));
assert!(graph::load(&wide.to_bytes(), Profile::Acc64).is_err(), "elu's producer result reaches 2^63");
for alpha in [i64::MAX, i64::MIN + 1, 65536, -3] {
check("elu", graph1(vec![TensorRec::check(1, i64::MIN + 1, i64::MAX)], TensorRec::derived(1), ELU, Params::Elu { alpha }, vec![], (0, 0)), &[(3, 3)], &[]);
check("elu", graph1(vec![TensorRec::check(1, -300000, 10)], TensorRec::derived(1), ELU, Params::Elu { alpha }, vec![], (0, 0)), &[(3, 3)], &[]);
}
}

View file

@ -0,0 +1,78 @@
//! The vectors whose outcome only load step 8 decides (§14.1 item 6), checked against
//! the reason the text gives for each: the bounds pass must refuse or tag them for
//! that reason, not another.
use std::path::PathBuf;
use wai_ops::bounds::{self, Profile};
use wai_ops::json::{self, unhex};
use wai_ops::{graph::Graph, plan};
fn lines(file: &str) -> Vec<json::Value> {
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../ops-conformance").join(file);
std::fs::read_to_string(&p).unwrap().lines().map(|l| json::parse(l.as_bytes()).unwrap()).collect()
}
fn graph_of(v: &json::Value) -> Graph {
Graph::parse(&unhex(v.get("graph").unwrap().as_str().unwrap()).unwrap()).unwrap()
}
/// Every key of §7.2's table has a vector that fails `acc32` by that parameter
/// condition alone: its graph meets `acc64`, every interval fits `acc32`, and its one
/// node's only failure is that key.
#[test]
fn each_acc32_parameter_key_fails_alone() {
let want = ["rq.mult", "rq.shift", "rq.post_bias", "weights", "chscale.m", "chscale.s", "igdn.gamma", "igdn.shift", "leaky.a"];
let mut seen: Vec<String> = Vec::new();
for op in ["rescale", "conv", "conv_transpose", "chscale", "igdn", "leaky"] {
for v in lines(&format!("vectors/{op}.jsonl")) {
if !v.get("id").unwrap().as_str().unwrap().contains("/acc32_param/") {
continue;
}
let g = graph_of(&v);
let sizes = plan::load_plan(&g).unwrap();
let b = bounds::pass(&g, sizes.as_deref()).unwrap();
assert!(b.meets(&g, Profile::Acc64));
let n = &b.nodes[0];
assert!(n.inter.iter().all(|i| i.iv.fits(i.class.width(Profile::Acc32))), "{op}: an interval fails acc32");
assert!(n.results.iter().all(|r| r.fits(32)));
assert_eq!(n.params32.len(), 1, "{op}: {:?}", n.params32);
let f = b.first_failure(&g, Profile::Acc32).unwrap();
assert_eq!(f.miss.key, n.params32[0]);
seen.push(f.miss.key.clone());
}
}
// `weights` has one for each of `conv` and `conv_transpose`.
seen.sort();
seen.dedup();
let mut w: Vec<String> = want.iter().map(|s| s.to_string()).collect();
w.sort();
assert_eq!(seen, w);
}
/// `sigmoid` and `elu` at −2^63 fail `acc64` through `exp.negx`, at 65 bits; an `rq`
/// whose `q` passes 64 bits fails at `rq.q` while `rq.y` fits where a `post` bias
/// brings it back. Without a bias (`chscale`, `mul`) `y` and the producer result pass
/// with `q`, at the same bits, and `out.0` is the smallest key of the three.
#[test]
fn step8_refusals_fail_where_the_text_says() {
for (file, key) in [("sigmoid", "exp.negx"), ("elu", "exp.negx"), ("rescale", "rq.q"), ("conv", "rq.q"), ("conv_transpose", "rq.q"), ("chscale", "out.0"), ("mul", "out.0")] {
let mut n = 0;
for v in lines(&format!("vectors/{file}.jsonl")) {
let id = v.get("id").unwrap().as_str().unwrap();
if !(id.contains("/step8/") || id.contains("/step8_q/")) || v.get("load").unwrap().as_str() == Some("ok") {
continue;
}
let g = graph_of(&v);
let b = bounds::pass(&g, None).unwrap();
let f = b.first_failure(&g, Profile::Acc64).unwrap();
assert_eq!((f.miss.key.as_str(), f.miss.bits), (key, Some(65)), "{id}");
if matches!(file, "rescale" | "conv" | "conv_transpose") {
let y = b.nodes[0].inter.iter().find(|i| i.key == "rq.y").unwrap();
assert!(y.iv.fits(64), "{id}: y fits");
}
n += 1;
}
assert!(n > 0, "{file}: no step-8 refusal");
}
}

View file

@ -2,6 +2,13 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "wai-ops"
version = "0.1.0"
[[package]]
name = "wai-verify"
version = "0.1.0"
dependencies = [
"wai-ops",
]

View file

@ -17,7 +17,20 @@ publish = false
# `cfg(kani)` is set by the model checker on the included sources. Declared so an
# ordinary build or clippy run of this crate does not warn on it.
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)", "cfg(kani_open)"] }
[dependencies]
# none — the integer core this crate verifies is pure std.
# None for the library: the integer core this crate verifies is pure std. The
# `wai-verify` tool (the bounds certifier, the lowerings and the conformance corpus
# of `wai-ops/1`) drives the `wai-ops` crate, behind the `cli` feature, so that the
# model checker's compilation environment stays the included sources and nothing
# else.
wai-ops = { path = "../wai-ops", optional = true }
[features]
cli = ["dep:wai-ops"]
[[bin]]
name = "wai-verify"
path = "src/bin/wai-verify/main.rs"
required-features = ["cli"]

View file

@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Every model-checker harness this crate hosts runs in exactly one shard.
`.github/workflows/wai-formal-verification.yml` splits the harnesses into shards, each
a list of `--harness` filters (a harness runs when its full name contains a filter).
This finds every `#[kani::proof]` function in the sources `src/lib.rs` includes by
`#[path]` (with the modules those files declare in turn), gives each its full name,
and fails unless each name is matched by the filters of exactly one shard, and each
filter matches at least one harness. A harness no shard runs would be compiled and
never checked; one in two shards would be checked twice. A harness gated by
`cfg(kani_open)` (one whose window the checker did not finish within a shard's
budget) is compiled only on request, so no shard runs it; it is counted apart.
Usage: python3 -I check_shards.py <repository root>
Standard library only.
"""
import os
import re
import sys
PATH_MOD = re.compile(r'#\[path = "([^"]+)"\]\s*(?:pub(?:\(crate\))? )?mod (\w+);')
PLAIN_MOD = re.compile(r'^(?:pub(?:\(crate\))? )?mod (\w+);', re.M)
PROOF = re.compile(r'(#\[cfg\(kani_open\)\]\s*)?#\[kani::proof\]\s*(?:#\[[^\]]*\]\s*)*(?:pub(?:\(crate\))? )?fn (\w+)')
# A matrix line names its shard first and its filters last; between them it may give
# other plain fields, such as the runner (`os: macos`).
SHARD = re.compile(r'^\s*- \{ shard: ([\w-]+),(?: *[\w-]+: [\w-]+,)* *harness: "([^"]*)"', re.M)
MATRIX_LINE = re.compile(r'^\s*- \{ shard: ', re.M)
OPEN = []
def harnesses(path, prefix, out):
"""The harnesses of the module in `path` and of the modules it declares. A harness
gated by `cfg(kani_open)` is compiled only on request and runs in no shard; it is
listed apart."""
src = open(path, encoding="utf-8").read()
for gated, name in PROOF.findall(src):
(OPEN if gated else out).append(prefix + name)
base = os.path.dirname(path)
stem = os.path.splitext(os.path.basename(path))[0]
# A `mod x;` in a.rs is a/x.rs or a/x/mod.rs; in mod.rs or lib.rs, x.rs beside it.
here = base if stem in ("mod", "lib") else os.path.join(base, stem)
for name in PLAIN_MOD.findall(src):
for cand in (os.path.join(here, name + ".rs"), os.path.join(here, name, "mod.rs")):
if os.path.exists(cand):
harnesses(cand, prefix + name + "::", out)
break
def main(root):
lib = os.path.join(root, "wai", "wai-verify", "src", "lib.rs")
src = open(lib, encoding="utf-8").read()
names = []
for rel, name in PATH_MOD.findall(src):
harnesses(os.path.normpath(os.path.join(os.path.dirname(lib), rel)), name + "::", names)
wf = open(os.path.join(root, ".github", "workflows", "wai-formal-verification.yml"), encoding="utf-8").read()
shards = [(s, f.split()) for s, f in SHARD.findall(wf)]
failures = 0
if not names or not shards:
print("FAIL no harnesses or no shards found")
return 1
# A shard line this cannot read would drop its filters unseen.
if len(shards) != len(MATRIX_LINE.findall(wf)):
print("FAIL %d shard lines, %d read" % (len(MATRIX_LINE.findall(wf)), len(shards)))
return 1
for n in names:
hit = [s for s, fs in shards if any(f in n for f in fs)]
if len(hit) != 1:
print("FAIL harness %s is in %d shards: %s" % (n, len(hit), ", ".join(hit) or "none"))
failures += 1
for s, fs in shards:
for f in fs:
if not any(f in n for n in names):
print("FAIL shard %s: the filter %s matches no harness" % (s, f))
failures += 1
print("%d harnesses in %d shards; %d gated by cfg(kani_open), in none" % (len(names), len(shards), len(OPEN)))
for s, fs in shards:
print(" %-14s %d" % (s, sum(1 for n in names if any(f in n for f in fs))))
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))

View file

@ -0,0 +1,285 @@
//! `wai-verify vectors`: the conformance corpus of `wai/ops-conformance/` (§14).
//!
//! - `gen <wai-dir>` writes every file of `<wai-dir>/ops-conformance` but its README
//! and `.gitattributes`: the vector files, the lowered graphs and the certificates.
//! - `verify <wai-dir>` recomputes every vector file from its inputs; re-lowers every
//! set that `certificates/index.json` and `certificates/falsification.json` list,
//! recomputes its certificate from the committed lowered graphs, and checks that a
//! falsification set's verdict fails the profile it is checked against; checks the
//! certificates of the sets that are not committed, which `external.json` lists, as
//! far as they can be checked without their sets; then regenerates the corpus.
//! Every byte must equal the committed one.
//! - `recompute <file>` recomputes one vector file to stdout.
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use wai_ops::bounds::{self, Profile};
use wai_ops::generate::{self, sets};
use wai_ops::json;
use wai_ops::vectors::{self, Kind};
fn kind_of(rel: &str) -> Option<Kind> {
if !rel.ends_with(".jsonl") {
return None;
}
if rel.starts_with("stage/") { Some(Kind::Stage) } else { Some(Kind::Graph) }
}
fn reader(wai: &Path) -> impl Fn(&str) -> Option<Vec<u8>> + '_ {
move |p: &str| std::fs::read(wai.join(p)).ok()
}
pub fn write(wai: &Path) -> ExitCode {
let read = reader(wai);
let (files, dropped) = match generate::all(&read) {
Ok(x) => x,
Err(e) => {
eprintln!("gen: {e}");
return ExitCode::from(2);
}
};
let root = wai.join("ops-conformance");
for (rel, bytes) in &files {
let p = root.join(rel);
if let Some(d) = p.parent() {
std::fs::create_dir_all(d).expect("create directory");
}
std::fs::write(&p, bytes).expect("write");
}
let lines: usize = files.iter().filter(|(r, _)| r.ends_with(".jsonl")).map(|(_, b)| b.iter().filter(|&&c| c == b'\n').count()).sum();
let certs = files.iter().filter(|(r, _)| r.starts_with("certificates/") && r.ends_with(".json") && r.matches('/').count() == 2).count();
eprintln!("wrote {} files, {} vectors, {} certificates", files.len(), lines, certs);
for (f, d) in dropped.iter().filter(|(_, d)| *d > 0) {
eprintln!(" {f}: {d} cases were not written");
}
ExitCode::SUCCESS
}
fn list(dir: &Path, base: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
let mut v: Vec<PathBuf> = rd.filter_map(|e| e.ok().map(|e| e.path())).collect();
v.sort();
for p in v {
if p.is_dir() {
list(&p, base, out);
} else {
out.push(p.strip_prefix(base).expect("below base").to_path_buf());
}
}
}
/// The sets a listing names, or the error that it cannot be read.
fn listing(root: &Path, name: &str) -> Result<Vec<json::Value>, String> {
let b = std::fs::read(root.join("certificates").join(name)).map_err(|e| format!("certificates/{name}: {e}"))?;
let v = json::parse(&b).map_err(|_| format!("certificates/{name} does not parse"))?;
Ok(v.get("sets").and_then(|s| s.as_arr()).map(|a| a.to_vec()).unwrap_or_default())
}
pub fn verify(wai: &Path) -> ExitCode {
let root = wai.join("ops-conformance");
let mut bad = 0usize;
// 1. Every committed vector file, recomputed from its inputs.
let mut committed = Vec::new();
list(&root, &root, &mut committed);
let (mut nfiles, mut nlines) = (0usize, 0usize);
for rel in &committed {
let r = rel.to_string_lossy().replace('\\', "/");
let Some(kind) = kind_of(&r) else { continue };
let text = std::fs::read_to_string(root.join(rel)).expect("read");
nfiles += 1;
nlines += text.lines().count();
match vectors::recompute_file(kind, &text) {
Ok(out) if out == text => {}
Ok(out) => {
bad += 1;
let n = out.lines().zip(text.lines()).position(|(a, b)| a != b).map_or(out.lines().count().min(text.lines().count()), |n| n);
eprintln!("DIFF {r}: line {} differs when recomputed", n + 1);
}
Err(e) => {
bad += 1;
eprintln!("FAIL {r}: {e}");
}
}
}
eprintln!("recomputed {nfiles} vector files, {nlines} vectors");
// 2. Every set, re-lowered and certified from its committed lowered graphs.
let read = reader(wai);
let mut n = 0;
for (list_name, falsify) in [("index.json", false), ("falsification.json", true)] {
match listing(&root, list_name) {
Ok(sets) => {
for s in &sets {
n += 1;
if let Err(e) = check_set(s, &read, wai, falsify) {
bad += 1;
eprintln!("FAIL {} {}: {e}", list_name, s.get("name").and_then(|v| v.as_str()).unwrap_or("?"));
}
}
}
Err(e) => {
bad += 1;
eprintln!("FAIL {e}");
}
}
}
eprintln!("re-lowered and certified {n} sets");
// 3. The certificates of the sets that are not committed, which the jobs that
// regenerate or fetch them check by digest: each is canonical, names its set,
// and its verdict meets the profile the listing gives.
match listing(&root, "external.json") {
Ok(sets) => {
for s in &sets {
if let Err(e) = check_external(s, &root) {
bad += 1;
eprintln!("FAIL external.json {}: {e}", s.get("name").and_then(|v| v.as_str()).unwrap_or("?"));
}
}
eprintln!("checked {} external certificates", sets.len());
}
Err(e) => {
bad += 1;
eprintln!("FAIL {e}");
}
}
// 4. The generator writes the committed corpus again, byte for byte, and nothing
// is committed that it does not write (the README aside).
match generate::all(&read) {
Ok((files, _)) => {
let mut want: Vec<String> = Vec::new();
for (rel, bytes) in &files {
want.push(rel.clone());
match std::fs::read(root.join(rel)) {
Ok(c) if &c == bytes => {}
Ok(_) => {
bad += 1;
eprintln!("DIFF {rel}: the generator writes other bytes");
}
Err(_) => {
bad += 1;
eprintln!("MISSING {rel}");
}
}
}
// The external sets' certificates, and their listing, are written by the
// jobs that hold those sets.
let external: Vec<String> = match listing(&root, "external.json") {
Ok(sets) => sets
.iter()
.filter_map(|s| Some(format!("certificates/{}/{}.json", s.get("capability")?.as_str()?, s.get("set_sha256")?.as_str()?)))
.chain([String::from("certificates/external.json")])
.collect(),
Err(_) => vec![],
};
for rel in &committed {
let r = rel.to_string_lossy().replace('\\', "/");
if r != "README.md" && r != ".gitattributes" && !want.contains(&r) && !external.contains(&r) {
bad += 1;
eprintln!("EXTRA {r}: committed, but the generator does not write it");
}
}
eprintln!("regenerated {} files", files.len());
}
Err(e) => {
bad += 1;
eprintln!("FAIL gen: {e}");
}
}
if bad == 0 {
eprintln!("ops-conformance: every file verifies");
ExitCode::SUCCESS
} else {
eprintln!("ops-conformance: {bad} failures");
ExitCode::from(1)
}
}
/// Re-lower one listed set and compare its graphs; certify the committed graphs and
/// compare the certificate; for a falsification set, require that its verdict fails
/// the profile it claims.
fn check_set(s: &json::Value, read: &dyn Fn(&str) -> Option<Vec<u8>>, wai: &Path, falsify: bool) -> Result<(), String> {
let name = s.get("name").and_then(|v| v.as_str()).ok_or("name")?;
let cap = s.get("capability").and_then(|v| v.as_str()).ok_or("capability")?;
let lowering = s.get("lowering").and_then(|v| v.as_str()).ok_or("lowering")?;
let digest_listed = s.get("set_sha256").and_then(|v| v.as_str()).ok_or("set_sha256")?;
let c = s.get("c").and_then(|v| v.as_dec()).and_then(|v| v.to_i64()).map(|v| v as u32);
let members: Vec<(String, String)> =
s.get("members").and_then(|v| v.as_obj()).ok_or("members")?.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect();
let mut bytes = Vec::new();
for (k, p) in &members {
bytes.push((k.clone(), read(p).ok_or_else(|| format!("cannot read {p}"))?));
}
let m: Vec<(&str, Vec<u8>)> = bytes.iter().map(|(k, b)| (k.as_str(), b.clone())).collect();
let digest = sets::set_digest(&m);
if digest != digest_listed {
return Err(format!("set digest {digest} differs"));
}
let leak = |x: &str| -> &'static str { Box::leak(x.to_string().into_boxed_str()) };
let set = sets::Set { name: leak(name), capability: leak(cap), lowering: leak(lowering), members: vec![], c, note: "" };
let l = sets::lower_set(&set, &m)?;
let paths: Vec<&str> = s.get("roles").and_then(|v| v.as_arr()).ok_or("roles")?.iter().filter_map(|v| v.as_str()).collect();
if paths.len() != l.roles.len() {
return Err("the roles differ".into());
}
let mut graphs: Vec<(&str, Vec<u8>)> = Vec::new();
for ((role, g), p) in l.roles.iter().zip(paths) {
let want = std::fs::read(wai.join(p)).map_err(|e| format!("{p}: {e}"))?;
if want != g.to_bytes() {
return Err(format!("{role}: the lowered graph differs from {p}"));
}
graphs.push((role, want));
}
let refs: Vec<(&str, &[u8])> = graphs.iter().map(|(r, b)| (*r, b.as_slice())).collect();
let cert = bounds::certify(Some(cap), lowering, Some(&digest), &refs).map_err(|e| format!("{} cannot be certified: {:?}", e.name, e.refusal))?;
let path = format!("ops-conformance/certificates/{cap}/{digest}.json");
let committed = std::fs::read(wai.join(&path)).map_err(|e| format!("{path}: {e}"))?;
if committed != cert.to_json().into_bytes() {
return Err(format!("{path}: the certificate differs"));
}
if falsify {
let claim = s.get("claim").and_then(|v| v.as_str()).and_then(Profile::parse).ok_or("claim")?;
if cert.verdict().meets(claim) {
return Err(format!("its verdict meets {}, which it is there to fail", claim.as_str()));
}
}
Ok(())
}
fn check_external(s: &json::Value, root: &Path) -> Result<(), String> {
let cap = s.get("capability").and_then(|v| v.as_str()).ok_or("capability")?;
let digest = s.get("set_sha256").and_then(|v| v.as_str()).ok_or("set_sha256")?;
let profile = s.get("profile").and_then(|v| v.as_str()).and_then(Profile::parse).ok_or("profile")?;
let lowering = s.get("lowering").and_then(|v| v.as_str()).ok_or("lowering")?;
let path = format!("certificates/{cap}/{digest}.json");
let b = std::fs::read(root.join(&path)).map_err(|e| format!("{path}: {e}"))?;
let v = json::parse(&b).map_err(|_| format!("{path} does not parse"))?;
let c = vectors::value_to_c(&v)?;
if json::to_string(&c).into_bytes() != b {
return Err(format!("{path} is not canonical"));
}
let field = |k: &str| v.get(k).and_then(|x| x.as_str());
if field("capability") != Some(cap) || field("set_sha256") != Some(digest) || field("lowering") != Some(lowering) || field("certificate") != Some("wai-ops/1 bounds") {
return Err(format!("{path} names another set"));
}
let verdict = v.get("verdict").ok_or("verdict")?;
if verdict.get(profile.as_str()).and_then(|x| x.as_bool()) != Some(true) {
return Err(format!("{path}: its verdict does not meet {}", profile.as_str()));
}
Ok(())
}
pub fn recompute(file: &str) -> ExitCode {
let p = Path::new(file);
let rel = p.to_string_lossy();
let kind = if rel.contains("/stage/") || rel.starts_with("stage/") { Kind::Stage } else { Kind::Graph };
match std::fs::read_to_string(p).map_err(|e| e.to_string()).and_then(|t| vectors::recompute_file(kind, &t)) {
Ok(out) => {
print!("{out}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{e}");
ExitCode::from(2)
}
}
}

View file

@ -0,0 +1,459 @@
//! `wai-verify`: the bounds certifier, the lowerings and the conformance corpus of
//! `wai-ops/1` (`extensions/integer-operators.md` §8, §13, §14).
//!
//! ```text
//! wai-verify bounds <graph.whg1>... | <set-path> [--capability <name>] [--lowering <id>]
//! [--member <name>=<path>]... [--c <n>] [--profile acc32|acc64]
//! [--out <cert.json>] [--check <committed.json>]
//! [--advise acc32|acc64] [--witness <dir>]
//! wai-verify lower <set-path> --capability <name> [--member <name>=<path>]... [--c <n>]
//! --out <dir>
//! wai-verify vectors gen|verify <wai-dir>
//! wai-verify vectors recompute <file.jsonl>
//! ```
//!
//! A legacy set is a file (a one-file set) or a directory, whose every file is a
//! member named by its path below the directory, as a `wai.prior.set` listing names
//! it; `--member` adds a member, or names one whose file is named otherwise. The
//! capability selects the lowering (§13.0) and its declared profile, `acc64` for
//! every lowering of §13; `--c` is `world/1`'s latent channel count. `WHG1` graphs
//! are certified on their own, in the order given, with the lowering `"whg1"`.
//!
//! The certificate (§8.5) goes to `--out`, or to stdout. The verdict, the first
//! failure, the witnesses' paths and the advice go to stderr.
//!
//! Exit status:
//! - 0: the certificate was written and its verdict meets the profile;
//! - 1: the verdict fails the profile (the certificate is still written);
//! - 2: malformed input: a graph refused at load steps 1–7, an empty `check`
//! intersection or a `lut16` range past `[−32767, 32767]`, or an unreadable file;
//! - 3: `--check` found a difference, printed as the first differing JSON path;
//! - 4: the lowering refused the set;
//! - 5: usage.
//!
//! For `vectors`, 0 when every file verifies, 1 on a difference, 2 on an error.
mod corpus;
mod witness;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use wai_ops::bounds::{self, Certificate, Profile};
use wai_ops::generate::sets;
use wai_ops::json::{self, C, Value};
const USAGE: &str = "usage:
wai-verify bounds <graph.whg1>... | <set-path> [--capability <name>] [--lowering <id>]
[--member <name>=<path>]... [--c <n>] [--profile acc32|acc64]
[--out <cert.json>] [--check <committed.json>]
[--advise acc32|acc64] [--witness <dir>]
wai-verify lower <set-path> --capability <name> [--member <name>=<path>]... [--c <n>] --out <dir>
wai-verify vectors gen|verify <wai-dir>
wai-verify vectors recompute <file.jsonl>";
fn usage(why: &str) -> ExitCode {
if !why.is_empty() {
eprintln!("wai-verify: {why}");
}
eprintln!("{USAGE}");
ExitCode::from(5)
}
/// The lowering of §13.0's table for a capability.
fn lowering_of(cap: &str) -> Option<&'static str> {
Some(match cap {
"wai.neural.int_synth" => "int_synth/1",
"wai.neural.int_hyper" | "wai.video.int_hyper" | "wai.splat.int_codec" => "whm1/1",
"wai.audio.int_codec" => "wam1/1",
"wai.neural.int_mlicpp" => "wmb1/1",
"wai.world.model" => "world/1",
"wai.image.jpegai" => "jpegai-int/1",
_ => return None,
})
}
/// The member name of a one-file set of the capability.
fn one_file_member(cap: &str) -> Option<&'static str> {
Some(match cap {
"wai.neural.int_synth" => "int_synth.json",
"wai.audio.int_codec" => "model.wam",
"wai.splat.int_codec" => "g_s.whm",
"wai.neural.int_mlicpp" => "model.wmb",
_ => return None,
})
}
#[derive(Default)]
struct Args {
paths: Vec<String>,
capability: Option<String>,
lowering: Option<String>,
members: Vec<(String, String)>,
c: Option<u32>,
profile: Option<Profile>,
out: Option<String>,
check: Option<String>,
advise: Option<Profile>,
witness: Option<String>,
}
fn parse_args(a: &[String]) -> Result<Args, String> {
let mut r = Args::default();
let mut k = 0;
while k < a.len() {
let flag = a[k].as_str();
let val = |k: usize| a.get(k + 1).cloned().ok_or_else(|| format!("{flag} takes a value"));
match flag {
"--capability" => r.capability = Some(val(k)?),
"--lowering" => r.lowering = Some(val(k)?),
"--member" => {
let v = val(k)?;
let (n, p) = v.split_once('=').ok_or("--member takes <name>=<path>")?;
r.members.push((n.to_string(), p.to_string()));
}
"--c" => r.c = Some(val(k)?.parse().map_err(|_| "--c takes a channel count")?),
"--profile" => r.profile = Some(Profile::parse(&val(k)?).ok_or("--profile is acc32 or acc64")?),
"--advise" => r.advise = Some(Profile::parse(&val(k)?).ok_or("--advise is acc32 or acc64")?),
"--out" => r.out = Some(val(k)?),
"--check" => r.check = Some(val(k)?),
"--witness" => r.witness = Some(val(k)?),
f if f.starts_with("--") => return Err(format!("unknown option {f}")),
_ => {
r.paths.push(a[k].clone());
k += 1;
continue;
}
}
k += 2;
}
Ok(r)
}
fn files_below(dir: &Path, base: &Path, out: &mut Vec<(String, PathBuf)>) -> std::io::Result<()> {
let mut v: Vec<PathBuf> = std::fs::read_dir(dir)?.filter_map(|e| e.ok().map(|e| e.path())).collect();
v.sort();
for p in v {
if p.is_dir() {
files_below(&p, base, out)?;
} else {
let rel = p.strip_prefix(base).expect("below base").to_string_lossy().replace('\\', "/");
out.push((rel, p));
}
}
Ok(())
}
/// A set's members, by name, from its path and the `--member` options.
fn members(args: &Args, cap: &str) -> Result<Vec<(String, Vec<u8>)>, String> {
let mut m: Vec<(String, PathBuf)> = Vec::new();
for p in &args.paths {
let pb = PathBuf::from(p);
if pb.is_dir() {
files_below(&pb, &pb, &mut m).map_err(|e| format!("{p}: {e}"))?;
} else {
let n = one_file_member(cap).ok_or_else(|| format!("{cap} is a set of several files: give a directory or --member"))?;
m.push((n.to_string(), pb));
}
}
for (n, p) in &args.members {
m.retain(|(k, _)| k != n);
m.push((n.clone(), PathBuf::from(p)));
}
let mut out = Vec::new();
for (n, p) in m {
out.push((n, std::fs::read(&p).map_err(|e| format!("{}: {e}", p.display()))?));
}
out.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
Ok(out)
}
/// Either the certificate, or the exit code and message of why there is none.
type Certified = Result<Certificate, (u8, String)>;
fn certify(args: &Args) -> Certified {
let whg1 = args.capability.is_none() || args.lowering.as_deref() == Some("whg1") || (!args.paths.is_empty() && args.paths.iter().all(|p| p.ends_with(".whg1")));
let refused = |e: bounds::Uncertifiable| (2, format!("{}: refused at load ({:?} at {:?})", e.name, e.refusal.code, e.refusal.at));
if whg1 {
if args.paths.is_empty() {
return Err((5, "no graph given".into()));
}
let mut gs: Vec<(String, Vec<u8>)> = Vec::new();
for p in &args.paths {
let b = std::fs::read(p).map_err(|e| (2, format!("{p}: {e}")))?;
let name = Path::new(p).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
gs.push((name, b));
}
let refs: Vec<(&str, &[u8])> = gs.iter().map(|(n, b)| (n.as_str(), b.as_slice())).collect();
return bounds::certify(args.capability.as_deref(), "whg1", None, &refs).map_err(refused);
}
let cap = args.capability.clone().unwrap();
let lowering = lowering_of(&cap).ok_or_else(|| (4, format!("{cap}: no lowering of §13 lowers it")))?;
if let Some(l) = &args.lowering
&& l != lowering
{
return Err((4, format!("{cap} lowers by {lowering}, not {l}")));
}
let m = members(args, &cap).map_err(|e| (2, e))?;
if m.is_empty() {
return Err((5, "no set given".into()));
}
let mv: Vec<(&str, Vec<u8>)> = m.iter().map(|(n, b)| (n.as_str(), b.clone())).collect();
let digest = sets::set_digest(&mv);
let leak = |x: &str| -> &'static str { Box::leak(x.to_string().into_boxed_str()) };
let set = sets::Set { name: "set", capability: leak(&cap), lowering, members: vec![], c: args.c, note: "" };
let l = sets::lower_set(&set, &mv).map_err(|e| (4, e))?;
let bytes: Vec<(&str, Vec<u8>)> = l.roles.iter().map(|(r, g)| (*r, g.to_bytes())).collect();
let refs: Vec<(&str, &[u8])> = bytes.iter().map(|(r, b)| (*r, b.as_slice())).collect();
bounds::certify(Some(&cap), lowering, Some(&digest), &refs).map_err(refused)
}
/// A JSON value shown in a message.
fn show(v: &Value) -> String {
match v {
Value::Null => "null".into(),
Value::Bool(b) => b.to_string(),
Value::Str(s) => format!("\"{s}\""),
Value::Num(n) => n.to_string(),
Value::Arr(a) => format!("[{} items]", a.len()),
Value::Obj(m) => format!("{{{} members}}", m.len()),
}
}
fn step(path: &str, k: &str) -> String {
if !k.is_empty() && k.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_') {
if path.is_empty() { k.to_string() } else { format!("{path}.{k}") }
} else {
format!("{path}[\"{k}\"]")
}
}
/// The first path at which two JSON values differ, with both values there.
fn first_diff(a: &Value, b: &Value, path: &str) -> Option<(String, String, String)> {
match (a, b) {
(Value::Arr(x), Value::Arr(y)) => {
for (k, (p, q)) in x.iter().zip(y).enumerate() {
if let Some(d) = first_diff(p, q, &format!("{path}[{k}]")) {
return Some(d);
}
}
if x.len() != y.len() {
return Some((format!("{path}.length"), x.len().to_string(), y.len().to_string()));
}
None
}
(Value::Obj(x), Value::Obj(y)) => {
let mut keys: Vec<&String> = x.iter().map(|(k, _)| k).chain(y.iter().map(|(k, _)| k)).collect();
keys.sort_by(|p, q| p.as_bytes().cmp(q.as_bytes()));
keys.dedup();
for k in keys {
let (p, q) = (x.iter().find(|(n, _)| n == k).map(|(_, v)| v), y.iter().find(|(n, _)| n == k).map(|(_, v)| v));
match (p, q) {
(Some(p), Some(q)) => {
if let Some(d) = first_diff(p, q, &step(path, k)) {
return Some(d);
}
}
(p, q) => return Some((step(path, k), p.map_or("absent".into(), show), q.map_or("absent".into(), show))),
}
}
None
}
_ if a == b => None,
_ => Some((if path.is_empty() { "$".into() } else { path.to_string() }, show(a), show(b))),
}
}
fn describe(f: &bounds::SetFailure) -> String {
let bits = f.failure.miss.bits.map_or(String::new(), |b| format!(", {b} bits"));
format!("graph {} {} {}: {}{bits}", f.graph, f.failure.at.as_str(), f.failure.index, f.failure.miss.key)
}
fn iv_c(v: &bounds::Iv) -> C {
C::obj(vec![("hi", C::big(v.hi)), ("lo", C::big(v.lo))])
}
fn advise(cert: &Certificate, p: Profile) -> C {
let graphs = cert
.graphs
.iter()
.map(|cg| {
let rows = match bounds::advise(&cg.graph, cg.sizes.as_deref(), p) {
Ok(v) => C::Arr(
v.iter()
.map(|a| {
C::obj(vec![
("admissible", a.admissible.as_ref().map_or(C::Null, iv_c)),
("clamp", a.clamp.as_ref().map_or(C::Null, iv_c)),
("interval", iv_c(&a.interval)),
("tensor", C::int(a.tensor as i128)),
])
})
.collect(),
),
Err(r) => C::s(&format!("refused: {:?}", r.code)),
};
C::obj(vec![("name", C::s(&cg.name)), ("tensors", rows)])
})
.collect();
C::obj(vec![("advise", C::s(p.as_str())), ("graphs", C::Arr(graphs))])
}
fn cmd_bounds(args: &Args) -> ExitCode {
let cert = match certify(args) {
Ok(c) => c,
Err((code, why)) => {
eprintln!("wai-verify bounds: {why}");
return ExitCode::from(code);
}
};
let profile = args.profile.unwrap_or(Profile::Acc64);
let text = cert.to_json();
match &args.out {
Some(p) => {
if let Err(e) = std::fs::write(p, &text) {
eprintln!("wai-verify bounds: {p}: {e}");
return ExitCode::from(2);
}
}
None => print!("{text}"),
}
let v = cert.verdict();
eprintln!("set {}", cert.set_sha256.as_deref().unwrap_or("(graphs on their own)"));
for p in Profile::ALL {
match v.failure(p) {
None => eprintln!("{}: meets", p.as_str()),
Some(f) => eprintln!("{}: fails, first at {}", p.as_str(), describe(f)),
}
}
eprintln!("npu: {} (reasons {:?}, flags {:?})", v.npu(), v.npu_reasons, v.npu_flags);
let mut code = if v.meets(profile) { 0 } else { 1 };
if let Some(dir) = &args.witness
&& !v.meets(profile)
{
if let Err(e) = std::fs::create_dir_all(dir) {
eprintln!("wai-verify bounds: {dir}: {e}");
return ExitCode::from(2);
}
for cg in &cert.graphs {
for n in cg.bounds.nodes.iter().filter(|n| !n.fits(profile)) {
let p = Path::new(dir).join(format!("{}-{}.json", cg.name, n.index));
let w = json::to_string(&witness::witness(cg, n, profile));
if let Err(e) = std::fs::write(&p, w) {
eprintln!("wai-verify bounds: {}: {e}", p.display());
return ExitCode::from(2);
}
eprintln!("witness {}", p.display());
}
}
}
if let Some(p) = args.advise {
eprintln!("{}", json::to_string(&advise(&cert, p)));
}
if let Some(c) = &args.check {
let committed = match std::fs::read(c) {
Ok(b) => b,
Err(e) => {
eprintln!("wai-verify bounds: {c}: {e}");
return ExitCode::from(2);
}
};
if committed != text.as_bytes() {
let d = match (json::parse(text.as_bytes()), json::parse(&committed)) {
(Ok(a), Ok(b)) => first_diff(&a, &b, ""),
_ => None,
};
match d {
Some((path, got, want)) => eprintln!("--check: differs at {path}: computed {got}, committed {want}"),
None => eprintln!("--check: the bytes differ ({c} is not canonical)"),
}
code = 3;
} else {
eprintln!("--check: identical to {c}");
}
}
ExitCode::from(code)
}
fn cmd_lower(args: &Args) -> ExitCode {
let (Some(cap), Some(out)) = (&args.capability, &args.out) else { return usage("lower takes --capability and --out") };
let Some(lowering) = lowering_of(cap) else {
eprintln!("wai-verify lower: {cap}: no lowering of §13 lowers it");
return ExitCode::from(4);
};
let m = match members(args, cap) {
Ok(m) if !m.is_empty() => m,
Ok(_) => return usage("no set given"),
Err(e) => {
eprintln!("wai-verify lower: {e}");
return ExitCode::from(2);
}
};
let mv: Vec<(&str, Vec<u8>)> = m.iter().map(|(n, b)| (n.as_str(), b.clone())).collect();
let leak = |x: &str| -> &'static str { Box::leak(x.to_string().into_boxed_str()) };
let set = sets::Set { name: "set", capability: leak(cap), lowering, members: vec![], c: args.c, note: "" };
let l = match sets::lower_set(&set, &mv) {
Ok(l) => l,
Err(e) => {
eprintln!("wai-verify lower: {e}");
return ExitCode::from(4);
}
};
if let Err(e) = std::fs::create_dir_all(out) {
eprintln!("wai-verify lower: {out}: {e}");
return ExitCode::from(2);
}
for (role, g) in &l.roles {
let p = Path::new(out).join(format!("{role}.whg1"));
if let Err(e) = std::fs::write(&p, g.to_bytes()) {
eprintln!("wai-verify lower: {}: {e}", p.display());
return ExitCode::from(2);
}
eprintln!("{}", p.display());
}
eprintln!("set {}", sets::set_digest(&mv));
ExitCode::SUCCESS
}
fn main() -> ExitCode {
let a: Vec<String> = std::env::args().skip(1).collect();
let s: Vec<&str> = a.iter().map(String::as_str).collect();
match s.as_slice() {
["vectors", "gen", dir] => corpus::write(Path::new(dir)),
["vectors", "verify", dir] => corpus::verify(Path::new(dir)),
["vectors", "recompute", file] => corpus::recompute(file),
["bounds", rest @ ..] | ["lower", rest @ ..] => {
let args = match parse_args(&a[1..1 + rest.len()]) {
Ok(x) => x,
Err(e) => return usage(&e),
};
if s[0] == "bounds" { cmd_bounds(&args) } else { cmd_lower(&args) }
}
_ => usage(""),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `--check` reports the first differing path, with both values.
#[test]
fn first_diff_names_the_path() {
let a = json::parse(br#"{"graphs":[{"nodes":[{"intermediates":{"convT.acc":{"hi":"5","lo":"0"}}}]}],"verdict":{"acc32":true}}"#).unwrap();
let b = json::parse(br#"{"graphs":[{"nodes":[{"intermediates":{"convT.acc":{"hi":"6","lo":"0"}}}]}],"verdict":{"acc32":false}}"#).unwrap();
assert_eq!(first_diff(&a, &b, ""), Some(("graphs[0].nodes[0].intermediates[\"convT.acc\"].hi".into(), "\"5\"".into(), "\"6\"".into())));
assert_eq!(first_diff(&a, &a, ""), None);
let c = json::parse(br#"{"verdict":{"acc32":true,"x":"1"}}"#).unwrap();
let d = json::parse(br#"{"verdict":{"acc32":true}}"#).unwrap();
assert_eq!(first_diff(&c, &d, ""), Some(("verdict.x".into(), "\"1\"".into(), "absent".into())));
}
#[test]
fn every_lowered_capability_has_its_lowering() {
for (cap, low) in [("wai.neural.int_synth", "int_synth/1"), ("wai.video.int_hyper", "whm1/1"), ("wai.audio.int_codec", "wam1/1"), ("wai.splat.int_codec", "whm1/1"), ("wai.neural.int_mlicpp", "wmb1/1"), ("wai.world.model", "world/1"), ("wai.image.jpegai", "jpegai-int/1")] {
assert_eq!(lowering_of(cap), Some(low));
}
assert_eq!(lowering_of("wai.video.int_motion"), None);
}
}

View file

@ -0,0 +1,271 @@
//! `--witness`: a node-local graph for each node that fails the profile (Part 2.5 of
//! the design).
//!
//! A witness is a one-operator `WHG1` graph whose inputs carry the failing node's
//! input intervals as `check` ranges. For `conv` and `conv_transpose` it also holds an
//! input that attains the failing end of the accumulator's interval on the channel
//! (and, for `conv_transpose`, the residue) that attains it, `x = h` where the weight
//! is positive and `x = l` where it is negative for the upper end, and the exact
//! accumulator at that position. It shows that the rule is tight for the node in
//! isolation; it says nothing about whether a payload can reach that input.
use wai_ops::bounds::{CertGraph, Class, Iv, Node, Profile};
use wai_ops::graph::{CAP, Graph, InputRec, Op, Params, TensorRec, op_name, opcode};
use wai_ops::json::{C, hex};
use wai_ops::xint::I512;
fn i(v: i64) -> I512 {
I512::from_i64(v)
}
/// The value a tap's input takes to push the sum toward `up` (the upper end) or
/// away from it: the end of `[l, h]` that maximises (or minimises) `w·x`, or the
/// value nearest 0 for a zero weight.
fn pick(w: i64, l: i64, h: i64, up: bool) -> i64 {
match (w.signum(), up) {
(1, true) | (-1, false) => h,
(-1, true) | (1, false) => l,
_ => 0i64.clamp(l, h),
}
}
/// The input tensor, the exact accumulator, the channel and the end, for a
/// convolution witness; `None` when the node is not a convolution.
/// The witness of a convolution: the operator, the input `(C, H, W, data)`, the exact
/// accumulator, the output channel and position `(y, x)` it lies at, and whether it
/// attains the upper end.
type ConvWitness = (Op, (u32, u32, u32, Vec<i64>), I512, u32, (u32, u32), bool);
fn conv_witness(op: &Op, cin: u32, x: (i64, i64), acc: &Iv, width: u32) -> Option<ConvWitness> {
// The end that fails the width, or else the larger in magnitude.
let up = I512::bits(acc.hi, acc.hi) > width || (I512::bits(acc.lo, acc.lo) <= width && acc.hi.abs() >= acc.lo.abs());
let (l, h) = x;
match &op.p {
Params::Conv { cout, kh, kw, groups, w_bytes, rq, w, .. } => {
let (cg, og) = (cin / groups, cout / groups);
let n = (cg * kh * kw) as usize;
// The channel whose partial-sum end is extreme.
let mut best: Option<(I512, u32)> = None;
for o in 0..*cout {
let ws = &w[o as usize * n..(o as usize + 1) * n];
let b = i(rq.pre(o as usize));
let mut s = if up { b.max(I512::ZERO) } else { b.min(I512::ZERO) };
for &wv in ws {
let t = i(wv).mul(i(pick(wv, l, h, up)));
s = if up { s.add(t.max(I512::ZERO)) } else { s.add(t.min(I512::ZERO)) };
}
let better = match best {
None => true,
Some((v, _)) => (up && s > v) || (!up && s < v),
};
if better {
best = Some((s, o));
}
}
let (_, o) = best?;
let g0 = (o / og) * cg;
let mut data = vec![0i64.clamp(l, h); (cin * kh * kw) as usize];
let mut value = i(rq.pre(o as usize));
for ci in 0..cg {
for k in 0..(kh * kw) {
let wv = w[o as usize * n + (ci * kh * kw + k) as usize];
let xv = pick(wv, l, h, up);
data[((g0 + ci) * kh * kw + k) as usize] = xv;
value = value.add(i(wv).mul(i(xv)));
}
}
let p = Params::Conv { cout: *cout, kh: *kh, kw: *kw, sh: 1, sw: 1, ph: 0, pw: 0, groups: *groups, w_bytes: *w_bytes, rq: rq.clone(), w: w.clone() };
let o2 = Op { code: opcode::CONV, ins: vec![0], outs: vec![1], p };
Some((o2, (cin, *kh, *kw, data), value, o, (0, 0), up))
}
Params::ConvT { cout, kh, kw, sh, sw, w_bytes, rq, w, .. } => {
let (rh, rw) = ((*sh).min(*kh), (*sw).min(*kw));
let at = |ci: u32, o: u32, ii: u32, jj: u32| w[(((ci * cout + o) * kh + ii) * kw + jj) as usize];
let mut best: Option<(I512, u32, u32, u32)> = None;
for o in 0..*cout {
let b = i(rq.pre(o as usize));
for ry in 0..rh {
for rx in 0..rw {
let mut s = if up { b.max(I512::ZERO) } else { b.min(I512::ZERO) };
for ci in 0..cin {
for ii in (ry..*kh).step_by(*sh as usize) {
for jj in (rx..*kw).step_by(*sw as usize) {
let wv = at(ci, o, ii, jj);
let t = i(wv).mul(i(pick(wv, l, h, up)));
s = if up { s.add(t.max(I512::ZERO)) } else { s.add(t.min(I512::ZERO)) };
}
}
}
let better = match best {
None => true,
Some((v, ..)) => (up && s > v) || (!up && s < v),
};
if better {
best = Some((s, o, ry, rx));
}
}
}
}
let (_, o, ry, rx) = best?;
// With no padding, output (ρy + sh·(H − 1), ρx + sw·(W − 1)) reads tap
// (i, j) of residue ρ at input row H − 1 − (i − ρy)/sh, column likewise.
let (hh, ww) = (kh.div_ceil(*sh), kw.div_ceil(*sw));
let mut data = vec![0i64.clamp(l, h); (cin * hh * ww) as usize];
let mut value = i(rq.pre(o as usize));
for ci in 0..cin {
for ii in (ry..*kh).step_by(*sh as usize) {
for jj in (rx..*kw).step_by(*sw as usize) {
let wv = at(ci, o, ii, jj);
let xv = pick(wv, l, h, up);
let (u, v) = (hh - 1 - (ii - ry) / sh, ww - 1 - (jj - rx) / sw);
data[((ci * hh + u) * ww + v) as usize] = xv;
value = value.add(i(wv).mul(i(xv)));
}
}
}
let p = Params::ConvT { cout: *cout, kh: *kh, kw: *kw, sh: *sh, sw: *sw, ph: 0, pw: 0, oph: 0, opw: 0, w_bytes: *w_bytes, rq: rq.clone(), w: w.clone() };
let o2 = Op { code: opcode::CONV_TRANSPOSE, ins: vec![0], outs: vec![1], p };
Some((o2, (cin, hh, ww, data), value, o, (ry + sh * (hh - 1), rx + sw * (ww - 1)), up))
}
_ => None,
}
}
/// The witness of node `n` of `cg`, failing `p`, as canonical JSON.
pub fn witness(cg: &CertGraph, n: &Node, p: Profile) -> C {
let g = &cg.graph;
let op = &g.ops[n.index as usize];
let miss = n.miss(p).expect("a failing node");
let ins: Vec<Iv> = op.ins.iter().map(|&t| cg.bounds.tensors[t as usize]).collect();
let fits = ins.iter().all(|v| v.fits(64));
let mut m = vec![
("index", C::int(n.index as i128)),
("key", C::s(&miss.key)),
("op", C::s(op_name(op.code).unwrap_or("?"))),
("profile", C::s(p.as_str())),
("role", C::s(&cg.name)),
];
let acc = n.inter.iter().find(|x| x.key == "conv.acc" || x.key == "convT.acc");
let cin = g.tensors[op.ins[0] as usize].channels;
if fits && !n.saturated {
let x = (ins[0].lo.to_i64().unwrap(), ins[0].hi.to_i64().unwrap());
if let Some(a) = acc
&& let Some((o2, (c, hh, ww, data), value, ch, (py, px), up)) = conv_witness(op, cin, x, &a.iv, Class::Acc.width(p))
{
let out_c = g.tensors[op.outs[0] as usize].channels;
let gr = Graph {
max_elems: CAP as u32,
scalars: vec![],
inputs: vec![InputRec { tensor: 0, max_h: 0, max_w: 0, kind: 0, src: 0, init: 0 }],
outputs: vec![1],
tensors: vec![TensorRec::check(c, x.0, x.1), TensorRec::derived(out_c)],
ops: vec![o2],
};
m.push(("graph", C::Str(hex(&gr.to_bytes()))));
m.push(("input", C::obj(vec![("data", C::ints(data.iter().map(|&v| v as i128))), ("shape", C::ints([c as i128, hh as i128, ww as i128]))])));
m.push(("channel", C::int(ch as i128)));
m.push(("at", C::ints([py as i128, px as i128])));
m.push(("end", C::s(if up { "hi" } else { "lo" })));
m.push(("acc", C::big(value)));
return C::obj(m);
}
// Any other operator: the node alone, its inputs checked against its input
// intervals, with no input chosen.
let mut tensors = Vec::new();
let mut inputs = Vec::new();
let mut map: Vec<(u32, u32)> = Vec::new();
for (&t, v) in op.ins.iter().zip(&ins) {
if map.iter().any(|(a, _)| *a == t) {
continue;
}
let k = tensors.len() as u32;
let (mh, mw) = cg.sizes.as_ref().map_or((0, 0), |s| (s[t as usize].0 as u32, s[t as usize].1 as u32));
tensors.push(TensorRec::check(g.tensors[t as usize].channels, v.lo.to_i64().unwrap(), v.hi.to_i64().unwrap()));
inputs.push(InputRec { tensor: k, max_h: mh, max_w: mw, kind: 0, src: 0, init: 0 });
map.push((t, k));
}
let first_out = tensors.len() as u32;
for &t in &op.outs {
tensors.push(TensorRec::derived(g.tensors[t as usize].channels));
}
let o2 = Op {
code: op.code,
ins: op.ins.iter().map(|t| map.iter().find(|(a, _)| a == t).unwrap().1).collect(),
outs: (first_out..first_out + op.outs.len() as u32).collect(),
p: op.p.clone(),
};
let gr = Graph { max_elems: CAP as u32, scalars: g.scalars.clone(), inputs, outputs: o2.outs.clone(), tensors, ops: vec![o2] };
m.push(("graph", C::Str(hex(&gr.to_bytes()))));
} else {
m.push(("graph", C::Null));
}
m.push(("input", C::Null));
C::obj(m)
}
#[cfg(test)]
mod tests {
use super::*;
use wai_ops::bounds::{self, Profile};
use wai_ops::interp::{Sequence, Tensor, run, run_observed};
use wai_ops::json::{self, unhex};
use wai_ops::{graph, lower};
/// The witness of the photo set's node 6 under `acc32`, run through the
/// interpreter: at the position it names, its output is the rounded accumulator
/// it records, and that accumulator is `convT.acc`'s upper end.
#[test]
fn the_photo_witness_attains_its_bound() {
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../neural-conformance/vectors/photo/int_synth.json");
let set = lower::int_synth(&std::fs::read(p).unwrap()).unwrap();
let b = set.get("synth").unwrap().to_bytes();
let cert = bounds::certify(None, "int_synth/1", None, &[("synth", &b)]).unwrap();
let cg = &cert.graphs[0];
let n = &cg.bounds.nodes[6];
let w = json::parse(json::to_string(&witness(cg, n, Profile::Acc32)).as_bytes()).unwrap();
let acc = w.get("acc").unwrap().as_dec().unwrap();
assert_eq!(acc, n.inter.iter().find(|i| i.key == "convT.acc").unwrap().iv.hi);
let g = graph::load(&unhex(w.get("graph").unwrap().as_str().unwrap()).unwrap(), Profile::Acc64).unwrap();
let inp = w.get("input").unwrap();
let shape: Vec<u32> = inp.get("shape").unwrap().as_arr().unwrap().iter().map(|v| v.as_dec().unwrap().to_i64().unwrap() as u32).collect();
let data: Vec<i64> = inp.get("data").unwrap().as_arr().unwrap().iter().map(|v| v.as_dec().unwrap().to_i64().unwrap()).collect();
let out = run(&g, &[Tensor::new(shape[0], shape[1], shape[2], data)], &[], &mut Sequence::new()).unwrap().remove(0);
let ch = w.get("channel").unwrap().as_dec().unwrap().to_i64().unwrap() as usize;
let at: Vec<usize> = w.get("at").unwrap().as_arr().unwrap().iter().map(|v| v.as_dec().unwrap().to_i64().unwrap() as usize).collect();
let shift = match &g.ops[0].p {
wai_ops::graph::Params::ConvT { rq, .. } => rq.shift(ch),
_ => unreachable!(),
};
let got = out.data[(ch * out.h as usize + at[0]) * out.w as usize + at[1]];
assert_eq!(I512::from_i64(got), acc.shr_floor(shift));
}
/// The same witness, run observed: every value the operator forms lies in the
/// interval the photo set's certificate records for node 6, and the accumulator
/// with its `pre` bias (`rq.t`, whose interval is `convT.acc`) reaches that
/// interval's upper end, so the end is attained, not only a bound.
#[test]
fn the_photo_witness_stays_in_its_certified_intervals() {
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../neural-conformance/vectors/photo/int_synth.json");
let set = lower::int_synth(&std::fs::read(p).unwrap()).unwrap();
let b = set.get("synth").unwrap().to_bytes();
let cert = bounds::certify(None, "int_synth/1", None, &[("synth", &b)]).unwrap();
let n = &cert.graphs[0].bounds.nodes[6];
let w = json::parse(json::to_string(&witness(&cert.graphs[0], n, Profile::Acc32)).as_bytes()).unwrap();
let g = graph::load(&unhex(w.get("graph").unwrap().as_str().unwrap()).unwrap(), Profile::Acc64).unwrap();
let inp = w.get("input").unwrap();
let shape: Vec<u32> = inp.get("shape").unwrap().as_arr().unwrap().iter().map(|v| v.as_dec().unwrap().to_i64().unwrap() as u32).collect();
let data: Vec<i64> = inp.get("data").unwrap().as_arr().unwrap().iter().map(|v| v.as_dec().unwrap().to_i64().unwrap()).collect();
// The witness graph's one node is node 6 on node 6's input interval, so its pass
// gives node 6's intervals.
let wb = bounds::pass(&g, None).unwrap();
assert_eq!(wb.nodes[0].inter, n.inter);
let mut obs = bounds::Within::new(&wb);
run_observed(&g, &[Tensor::new(shape[0], shape[1], shape[2], data)], &[], &mut Sequence::new(), &mut obs).unwrap();
assert!(obs.outside.is_empty(), "{:?}", obs.outside);
assert!(obs.unrecorded.is_empty(), "{:?}", obs.unrecorded);
let hi = n.inter.iter().find(|i| i.key == "convT.acc").unwrap().iv.hi;
let (_, seen_hi) = obs.extreme(0, "rq.t").unwrap();
assert_eq!(I512::from_i128(seen_hi), hi, "the accumulator with its bias attains convT.acc's upper end");
}
}

View file

@ -7,10 +7,11 @@
//! arithmetic is checked for all inputs in a bounded window rather than for the
//! vectors someone thought to write.
//!
//! The harnesses live beside the code they verify, in `wai-rs`, behind
//! `#[cfg(kani)]`; this crate only supplies a minimal compilation environment
//! (no codec libraries, no ONNX, no MSRV floor above the verifier's toolchain).
//! One source of truth, exactly as the conformance crate does it.
//! The harnesses live beside the code they verify, in `wai-rs` and in `wai-ops` (the
//! integer operator set), behind `#[cfg(kani)]`; this crate only supplies a minimal
//! compilation environment (no codec libraries, no ONNX, no MSRV floor above the
//! verifier's toolchain). One source of truth, exactly as the conformance crate does
//! it.
/// The integer rANS coder — the arithmetic every byte-exact decode flows
/// through, and therefore the place the determinism claim is won or lost.
@ -30,9 +31,9 @@ pub mod int_entropy;
#[path = "../../wai-rs/src/int_transform.rs"]
pub mod int_transform;
/// The fast kernels `int_transform::igdn` dispatches to. Present so the included
/// sources compile (only IGDN uses it here, hence the dead-code allowance); no
/// harness reaches it, and it spawns no threads under the model checker.
/// The fast kernels `int_transform::igdn` dispatches to. Only IGDN is used here, hence
/// the dead-code allowance; the operator set's K10 checks its narrow paths against the
/// reference IGDN. It spawns no threads under the model checker.
#[allow(dead_code)]
#[path = "../../wai-rs/src/int_kernels.rs"]
pub mod int_kernels;
@ -50,3 +51,53 @@ pub mod wai_video_policy;
pub mod staged_predict;
#[path = "../../wai-rs/src/staged/wir1_arith.rs"]
pub mod staged_wir1_arith;
// ---------------------------------------------------------------- wai-ops/1
//
// The reference implementation of the integer operator set
// (`extensions/integer-operators.md`), included module by module at this crate's
// root, so that its `crate::` paths resolve here exactly as they do in `wai-ops`.
// Its harnesses (K1–K33 of the operator set's test plan) live in `wai-ops` behind
// `#[cfg(kani)]`, in `src/proofs/`, and run here. Only the model checker compiles
// these: the `wai-verify` tool reaches the same sources through the `wai-ops`
// dependency, so an ordinary build does not compile them twice.
#[cfg(kani)]
extern crate alloc;
#[cfg(kani)]
#[path = "../../wai-ops/src/code.rs"]
pub mod code;
#[cfg(kani)]
#[path = "../../wai-ops/src/xint.rs"]
pub mod xint;
#[cfg(kani)]
#[path = "../../wai-ops/src/json.rs"]
pub mod json;
#[cfg(kani)]
#[path = "../../wai-ops/src/sha256.rs"]
pub mod sha256;
#[cfg(kani)]
#[path = "../../wai-ops/src/graph.rs"]
pub mod graph;
#[cfg(kani)]
#[path = "../../wai-ops/src/plan.rs"]
pub mod plan;
#[cfg(kani)]
#[path = "../../wai-ops/src/interp.rs"]
pub mod interp;
#[cfg(kani)]
#[path = "../../wai-ops/src/bounds.rs"]
pub mod bounds;
#[cfg(kani)]
#[path = "../../wai-ops/src/limb32.rs"]
pub mod limb32;
#[cfg(kani)]
#[path = "../../wai-ops/src/lower/mod.rs"]
pub mod lower;
#[cfg(kani)]
#[path = "../../wai-ops/src/stage/mod.rs"]
pub mod stage;
#[cfg(kani)]
#[path = "../../wai-ops/src/proofs/mod.rs"]
mod proofs;