wai-ops: int_synth decodes through the integer operator set, goldens unchanged (D2c, M1) #242
31 changed files with 1579 additions and 307 deletions
8
.github/workflows/wai-byte-equality.yml
vendored
8
.github/workflows/wai-byte-equality.yml
vendored
|
|
@ -69,6 +69,10 @@ on:
|
|||
# The fast kernels int_synth, int_transform and int_hyper_synth dispatch to
|
||||
# by default: the goldens here decode through them.
|
||||
- 'wai/wai-rs/src/int_kernels.rs'
|
||||
# The integer operator set and the bridge to it: the goldens decode int_synth
|
||||
# through its lowering too.
|
||||
- 'wai/wai-rs/src/int_ops.rs'
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-rs/tests/vectors/int_video/**'
|
||||
- 'wai/wai-rs/tests/vectors/int_hyper/**'
|
||||
- 'wai/wai-rs/tests/vectors/int_mlicpp/**'
|
||||
|
|
@ -98,6 +102,10 @@ on:
|
|||
# The fast kernels int_synth, int_transform and int_hyper_synth dispatch to
|
||||
# by default: the goldens here decode through them.
|
||||
- 'wai/wai-rs/src/int_kernels.rs'
|
||||
# The integer operator set and the bridge to it: the goldens decode int_synth
|
||||
# through its lowering too.
|
||||
- 'wai/wai-rs/src/int_ops.rs'
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-rs/tests/vectors/int_video/**'
|
||||
- 'wai/wai-rs/tests/vectors/int_hyper/**'
|
||||
- 'wai/wai-rs/tests/vectors/int_mlicpp/**'
|
||||
|
|
|
|||
17
.github/workflows/wai-conformance.yml
vendored
17
.github/workflows/wai-conformance.yml
vendored
|
|
@ -39,6 +39,8 @@ on:
|
|||
# wai-rs path-depends on wai-quantum, and the quantum corpora below are
|
||||
# verified through it: a change there changes what this workflow checks.
|
||||
- 'wai/wai-quantum/**'
|
||||
# The integer decoders that decode through the integer operator set depend on it.
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/*-conformance/**'
|
||||
- 'wai/byte-exact-conformance/**'
|
||||
# The registry export and the two library tests that read files outside
|
||||
|
|
@ -74,6 +76,8 @@ on:
|
|||
# wai-rs path-depends on wai-quantum, and the quantum corpora below are
|
||||
# verified through it: a change there changes what this workflow checks.
|
||||
- 'wai/wai-quantum/**'
|
||||
# The integer decoders that decode through the integer operator set depend on it.
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/*-conformance/**'
|
||||
- 'wai/byte-exact-conformance/**'
|
||||
# The registry export and the two library tests that read files outside
|
||||
|
|
@ -204,6 +208,15 @@ jobs:
|
|||
# different things in each: with overflow checks on (dev) that the fast path panics
|
||||
# where the reference panics, with them off (release) that it returns the
|
||||
# same integers. All pure Rust, so every leg runs them.
|
||||
# tests/int_synth_ops.rs holds wai.neural.int_synth to its lowering through the
|
||||
# integer operator set (extensions/integer-operators.md §13.1): every committed set
|
||||
# lowers to its committed graph and loads with its certificate's verdict, decodes
|
||||
# through it to every committed golden, and refuses whatever the direct decode
|
||||
# refuses. tests/int_ops_differential.rs (feature legacy_int: the direct decode,
|
||||
# test-only) decodes random sets and payloads both ways: the same output or both
|
||||
# refused, except on the planned breaks of integer-operators §15.3, where the decode
|
||||
# through the operator set refuses. wai-ops-differential.yml runs it for an hour in
|
||||
# each profile, nightly.
|
||||
# tests/int_mlicpp.rs decodes the committed wai.neural.int_mlicpp vector against
|
||||
# its pinned hashes (image, every scale bucket, every symbol) under every kernel
|
||||
# policy and thread count, and checks that damaged payloads are refused rather than
|
||||
|
|
@ -261,10 +274,10 @@ jobs:
|
|||
# codestream. No step named any of them until scripts/check-tests-in-ci.py asked.
|
||||
- name: Fast integer kernels ≡ reference kernels (overflow checks on)
|
||||
working-directory: wai/wai-rs
|
||||
run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration
|
||||
run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration
|
||||
- name: Fast integer kernels ≡ reference kernels (release, overflow checks off)
|
||||
working-directory: wai/wai-rs
|
||||
run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test manifest_parse_memory --test manifest_reader_equivalence --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration
|
||||
run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,score,staged,stage_measure,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test cli_generate_score --test int_payload --test int_payload_preflight --test int_payload_cost --test int_video_enc_memory --test cli_stage --test staged_resources --test stage_measure --test stage_measure_bounds --test manifest_parse_memory --test manifest_reader_equivalence --test int_video --test int_video_halfpel --test int_video_keyframe --test int_world --test int_world2 --test jpegai_bitstream_calibration
|
||||
# The integer decode path builds under each of its features alone, so a sink can take
|
||||
# one medium without the others, and for wasm32 (a browser sink); `int_payload` arms a
|
||||
# medium only under its feature, and none needs `neural`. So does the staged-measurement
|
||||
|
|
|
|||
6
.github/workflows/wai-neural-gates.yml
vendored
6
.github/workflows/wai-neural-gates.yml
vendored
|
|
@ -68,6 +68,9 @@ on:
|
|||
- 'wai/wai-rs/tests/ffi_neural_pin.rs'
|
||||
- 'wai/wai-rs/tests/neural_cost_limit.rs'
|
||||
- 'wai/wai-rs/src/int_synth.rs'
|
||||
# The integer operator set the integer decoders load their sets through.
|
||||
- 'wai/wai-rs/src/int_ops.rs'
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-rs/src/ort_session.rs'
|
||||
- 'wai/neural-conformance/vectors/**'
|
||||
- 'wai/pin-conformance/**'
|
||||
|
|
@ -105,6 +108,9 @@ on:
|
|||
- 'wai/wai-rs/tests/ffi_neural_pin.rs'
|
||||
- 'wai/wai-rs/tests/neural_cost_limit.rs'
|
||||
- 'wai/wai-rs/src/int_synth.rs'
|
||||
# The integer operator set the integer decoders load their sets through.
|
||||
- 'wai/wai-rs/src/int_ops.rs'
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-rs/src/ort_session.rs'
|
||||
- 'wai/neural-conformance/vectors/**'
|
||||
- 'wai/pin-conformance/**'
|
||||
|
|
|
|||
63
.github/workflows/wai-ops-differential.yml
vendored
Normal file
63
.github/workflows/wai-ops-differential.yml
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
name: wai-ops differential (nightly)
|
||||
|
||||
# Each integer decoder that decodes through the integer operator set
|
||||
# (wai/extensions/integer-operators.md) against its direct decode (the test-only
|
||||
# `legacy_int` feature), on random parameter sets and payloads, for an hour under each
|
||||
# build profile: with overflow checks on and with them off. Outside the planned breaks
|
||||
# of integer-operators §15.3 the two decode to the same bytes or both refuse, and the
|
||||
# direct decode never panics; under a planned break the decode through the operator set
|
||||
# refuses (wai/wai-rs/tests/int_ops_differential.rs). Today: wai.neural.int_synth.
|
||||
#
|
||||
# Each night starts from its own seed, the date, which the log prints; a failing case
|
||||
# names its seed, and `WAI_DIFF_SEED=<seed>` re-runs it. The same tests run for a fixed
|
||||
# count on every push, in wai-conformance.yml's fast-kernel steps.
|
||||
|
||||
# One run per branch at a time. A push supersedes the run before it.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '41 2 * * *'
|
||||
# A change to this file runs it for two minutes per profile, so a broken job is seen
|
||||
# before the night.
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/wai-ops-differential.yml'
|
||||
|
||||
jobs:
|
||||
differential:
|
||||
name: differential · ${{ matrix.profile }}
|
||||
runs-on: linux
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
profile: [debug, release]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# The docker image ships no Rust.
|
||||
- name: Install Rust 1.98.0
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
command -v rustup >/dev/null 2>&1 || \
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
|
||||
rustup toolchain install 1.98.0 --profile minimal
|
||||
rustup default 1.98.0
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Through the operator set against the direct decode, random sets and payloads
|
||||
working-directory: wai/wai-rs
|
||||
shell: bash
|
||||
env:
|
||||
WAI_DIFF_SECS: ${{ github.event_name == 'schedule' && '3600' || '120' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
P=""
|
||||
if [ "${{ matrix.profile }}" = release ]; then P="--release"; fi
|
||||
export WAI_DIFF_SEED="$(date -u +%Y%m%d)"
|
||||
echo "WAI_DIFF_SECS=$WAI_DIFF_SECS WAI_DIFF_SEED=$WAI_DIFF_SEED"
|
||||
cargo test $P --locked --no-default-features --features legacy_int --test int_ops_differential -- --nocapture
|
||||
4
.github/workflows/wai-web-wasm.yml
vendored
4
.github/workflows/wai-web-wasm.yml
vendored
|
|
@ -59,6 +59,8 @@ on:
|
|||
paths:
|
||||
- 'wai/wai-web/**'
|
||||
- 'wai/wai-rs/**'
|
||||
# wai-rs's integer decoders depend on the integer operator set.
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-quantum/**'
|
||||
# quantum::tests and quantum_toolchain::tests read the quantum corpora.
|
||||
- 'wai/quantum-conformance/**'
|
||||
|
|
@ -74,6 +76,8 @@ on:
|
|||
paths:
|
||||
- 'wai/wai-web/**'
|
||||
- 'wai/wai-rs/**'
|
||||
# wai-rs's integer decoders depend on the integer operator set.
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-quantum/**'
|
||||
# quantum::tests and quantum_toolchain::tests read the quantum corpora.
|
||||
- 'wai/quantum-conformance/**'
|
||||
|
|
|
|||
|
|
@ -6,6 +6,40 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### `wai.neural.int_synth` through the integer operator set
|
||||
|
||||
The first capability whose reference decoder decodes through `wai-ops/1`.
|
||||
|
||||
- **The decode.** `int_synth::decode_bytes` checks the payload
|
||||
([integer-payloads](extensions/integer-payloads.md) §13), lowers the set by
|
||||
`int_synth/1` ([integer-operators](extensions/integer-operators.md) §13.1), loads the
|
||||
graph under `acc64` (load steps 1–8, the last being the bounds certificate) and runs it
|
||||
through the operator set's interpreter, with the fast transposed-convolution kernel
|
||||
behind it (`wai-rs/src/int_ops.rs`). Every committed output is unchanged, byte for
|
||||
byte, on every platform the byte-equality matrix covers and in the browser crate, and
|
||||
every committed refusal keeps its code (`payload_malformed`). The direct decode it
|
||||
replaced is kept, test-only (feature `legacy_int`): `tests/int_ops_differential.rs`
|
||||
decodes random sets and payloads both ways and requires the same output or both
|
||||
refusals outside the planned breaks, and `wai-ops-differential.yml` runs it for an
|
||||
hour in each build profile every night.
|
||||
- **Planned breaks** (SPEC §8, integer-operators §15.3): new refusals of input no
|
||||
committed corpus contains. `wai.neural.int_synth` refuses a payload whose reserved byte
|
||||
is not 0, whose `C` is 0 or whose latents exceed `2^26` values; a set with no layers,
|
||||
with a layer whose `out_ch`, `k` or `stride` is 0, with a field of `2^32` or more, or
|
||||
with a shift of 128 or more; a set or payload a cap refuses (a tensor, or a transposed
|
||||
convolution's uncropped output, past `2^26` elements); and a set whose certificate does
|
||||
not meet `acc64`. A shift from 64 to 127, which a build used to mask or panic on, now
|
||||
has its exact meaning. A scan of every committed `int_synth` set and payload finds
|
||||
none affected. The text now states the two rules the lowering already made and the
|
||||
payload text did not: at least one layer, and `out_ch ≥ 1`.
|
||||
- **Its `ops` member in the registry** ([integer-operators](extensions/integer-operators.md)
|
||||
§11). `registry/capabilities.json` gives `wai.neural.int_synth` the member
|
||||
`{"set": "wai-ops/1", "profile": "acc64", "subset": "full", "lowering":
|
||||
"int_synth/1"}`, and the schema describes the member. SPEC §5's row cites the payload
|
||||
and set ([integer-payloads](extensions/integer-payloads.md) §13) and the lowering
|
||||
(integer-operators §13.1), and SPEC §7 says that a `NeuralIntegerExact` capability
|
||||
that decodes through the operator set names it in its `ops` member.
|
||||
|
||||
### Vector graphics (Draft): the sink as a WebAssembly 1.0 module, in three browser engines
|
||||
|
||||
Additive. No payload, outline set, vector or digest changes.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ together with the class functions of [`wai-rs/src/codecs/mod.rs`](wai-rs/src/cod
|
|||
|
||||
| file | what |
|
||||
|---|---|
|
||||
| [`registry/capabilities.json`](registry/capabilities.json) | every registered string with its kind, status, media class, role, fidelity tier, licence class, replicate class, set membership, registering section, edition and registration date; the named identifiers that are not capabilities; the unregistered labels; the wire identifiers |
|
||||
| [`registry/capabilities.json`](registry/capabilities.json) | every registered string with its kind, status, media class, role, fidelity tier, licence class, replicate class, set membership, registering section, edition and registration date, and, for a capability that decodes through the integer operator set, its `ops` member ([integer-operators](extensions/integer-operators.md) §11); the named identifiers that are not capabilities; the unregistered labels; the wire identifiers |
|
||||
| [`registry/capabilities.schema.json`](registry/capabilities.schema.json) | JSON Schema (2020-12) for that file; its enums are the label sets below |
|
||||
| [`vocab/v2.ttl`](vocab/v2.ttl), [`vocab/v2.jsonld`](vocab/v2.jsonld), [`vocab/v2.shapes.ttl`](vocab/v2.shapes.ttl) | the same registry as RDF, a JSON-LD context for manifests, and a SHACL shape that checks a manifest's capability and fallback against it, only the form of its media, and its parameter-set pins (`vocab/README.md`); a violation for a string the registry does not hold means the string is not registered in this revision, not that the envelope fails to conform or cannot be dispatched (SPEC §4, §8) |
|
||||
| the table at the end of this file | the same registry, for reading |
|
||||
|
|
|
|||
20
wai/SPEC.md
20
wai/SPEC.md
|
|
@ -650,7 +650,7 @@ registration and makes no conforming envelope non-conforming (§8).
|
|||
|---|---|---|---|
|
||||
| `wai.neural.bmshj2018` | bmshj2018-factorized + rANS (byte-exact CompressAI port) | about 0.26 bpp at 29 dB PSNR (quality 3) | CompressAI |
|
||||
| `wai.neural.mbt2018_mean` | mean-scale hyperprior (Gaussian-conditional entropy) | the conditional-entropy family (arXiv 1809.02736) | `neural/hyperprior.rs` |
|
||||
| `wai.neural.int_synth` | integer fixed-point synthesis (all-i64 transposed-conv, no float/ONNX) | bit-identical on every machine — soundly backs `intent = replicate` | `int_synth.rs` (integer weights sink-supplied; the reference tools make them by training in float, then quantising after training with per-layer shifts calibrated on measured activations, `tools/wai_int_synth_train.py`. Of the committed vectors only `photo` comes from a trained model, `tools/wai_int_synth_real.py`; `gradient`, `bloom`, `facade` and the cross-architecture golden use seeded integer weights) |
|
||||
| `wai.neural.int_synth` | integer fixed-point synthesis (all-i64 transposed-conv, no float/ONNX) | bit-identical on every machine — soundly backs `intent = replicate` | payload and set: [integer-payloads](extensions/integer-payloads.md) §13; decoded through its lowering, [integer-operators](extensions/integer-operators.md) §13.1 (`int_synth/1`); `int_synth.rs` (integer weights sink-supplied; the reference tools make them by training in float, then quantising after training with per-layer shifts calibrated on measured activations, `tools/wai_int_synth_train.py`. Of the committed vectors only `photo` comes from a trained model, `tools/wai_int_synth_real.py`; `gradient`, `bloom`, `facade` and the cross-architecture golden use seeded integer weights) |
|
||||
| `wai.neural.int_hyper` | integer fixed-point **conditional** codec — whole bitstream→image path integer (integer hyper-decoder σ → integer-σ entropy → integer synthesis) | the integer-entropy contract fully assembled: a PTQ of mbt2018_mean, bit-identical on every machine, whose integer decode CI holds within 1 dB of the same model's float decode on a pinned photograph | payload `WIH1`; `int_hyper_synth.rs` (tables sink-supplied) |
|
||||
| `wai.neural.int_mlicpp` | integer fixed-point **MLIC++ entropy topology** — `K` channel slices of the latent, each decoded as a checkerboard anchor pass then a non-anchor pass, with an entropy network over concat[latent decoded so far, hyper] re-run before every pass to give σ‖μ; hyper-decoder, entropy network and synthesis all integer | the channel-autoregressive × checkerboard entropy model at **decode-equivalence**: the encoder and every decoder compute the same σ, so the same distribution for every element, at any thread count; bit-identical on every machine. The committed conformance vector has synthetic weights, so it demonstrates decode-equivalence and states no quality figure | `int_mlicpp.rs` (one-file model bundle sink-supplied; `tools/wai_int_mlicpp_export.py`) |
|
||||
| `wai.image.jpegai` | JPEG AI (ISO/IEC 6048-1:2025) codestream → RGB — integer entropy front end (mANS, with an integer scale-index derivation) + 16-bit-PTQ integer hyper-decoder, context-model and synthesis networks + dequantisation, latent refinement, post-filters (the eICCI CNN among them) and colour conversion in IEEE-754 float with a fixed evaluation order. Scope: one configuration — 256×256 4:4:4, `model_id` 0 (tools_0), `beta_displacement_log` 0, the high-operation-point synthesis, the eICCI model pair the weight bundle carries, and the other header conditions in [determinism-tiers](extensions/determinism-tiers.md) §5; both decode paths refuse any other codestream. Within it, a sink without the integer weight bundle, or in a non-default floating-point environment, takes a float ONNX decode instead. Only checkpoint constants are sink-supplied | **entropy-consistency** (registry class `NeuralEntropyExact`): the entropy decode, including the scale-index derivation, is integer on both decode paths, so the decoded symbols reproduce on every machine. The exact decoder also pins one reconstruction to RGB, reproducible across machines under the build and floating-point conditions in [determinism-tiers](extensions/determinism-tiers.md) §5–§6 (it checks the floating-point environment before decoding) — a property of that decoder, not of the capability, so `wai.image.jpegai` does not back `replicate` (§7). On one reference codestream the exact decoder stays within max\|Δ\| ≤ 2 of the reference decoder (recorded max\|Δ\| = 1 on ~4.4% of samples); evidence in [determinism-tiers](extensions/determinism-tiers.md) §6. **Downgraded in v0.4.0:** this row previously claimed decode-equivalence, and the registry classed the capability `StandardDefined` | `jpegai_decode.rs` (+ `jpegai_{bitstream,mans,pipeline,synthesis,icci}.rs`) |
|
||||
|
|
@ -1465,6 +1465,12 @@ registry as `codecs::capability_replicate_class(cap) -> ReplicateClass`:
|
|||
checkerboard entropy model on integer networks, so the σ it re-derives before
|
||||
each of its `2·K` passes is integer too; `wai.video.int_hyper`,
|
||||
`wai.audio.int_codec` and `wai.splat.int_codec` are integer throughout as well.)
|
||||
A capability of this class whose networks decode through the integer operator set
|
||||
names it in the registry, in its `ops` member
|
||||
([integer-operators](extensions/integer-operators.md) §11): the lowering its sets
|
||||
are lowered by, the profile every conforming set's certificate meets, and the modes
|
||||
of its stage operators. A set whose certificate does not meet that profile is
|
||||
refused. In this revision: `wai.neural.int_synth`.
|
||||
- **`NeuralEntropyExact`** — the entropy decode is integer by construction, so
|
||||
the decoded symbols are byte-identical on every machine; later stages run float.
|
||||
Registered at `entropy-consistency`. An encoder MUST NOT emit `intent =
|
||||
|
|
@ -2339,6 +2345,18 @@ envelope that uses any string a later revision registers.
|
|||
envelope that carried it as a `WAI1` `capability` without `refines`, as a
|
||||
`fallback`, or as a selectable rendition or a companion, and one that bound a
|
||||
companion to it. Such envelopes still parse.
|
||||
- **Planned breaks.** A capability that comes to decode through the integer operator
|
||||
set ([integer-operators](extensions/integer-operators.md) §13) refuses some inputs it
|
||||
decoded before, or decoded differently by build. Each is a new refusal of input no
|
||||
committed corpus contains, and no committed output changes; integer-operators §15.3
|
||||
states them, and the changelog records each when it lands. Landed in this revision:
|
||||
- `wai.neural.int_synth`: a payload whose reserved byte is not 0, or whose `C` is 0,
|
||||
or whose latents exceed `2^26` values; a set with no layers, with a layer whose
|
||||
`out_ch`, `k` or `stride` is 0, with a field of `2^32` or more, or with a shift of
|
||||
128 or more; a set or payload that a cap refuses (a tensor, or a transposed
|
||||
convolution's uncropped output, past `2^26` elements); and a set whose certificate
|
||||
does not meet `acc64`. A shift from 64 to 127, which a build used to mask or panic
|
||||
on, now has its exact meaning.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
5
wai/byte-exact-conformance/Cargo.lock
generated
5
wai/byte-exact-conformance/Cargo.lock
generated
|
|
@ -182,8 +182,13 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"wai-ops",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wai-ops"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
|
|
|||
|
|
@ -22,3 +22,13 @@ unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
# The integer operator set the included decoders run their networks through
|
||||
# (`wai-rs/src/int_ops.rs`, included at the crate root). no_std, no dependencies.
|
||||
wai-ops = { path = "../wai-ops" }
|
||||
|
||||
[features]
|
||||
# wai-rs's test-only switch for the integer decoders' direct decodes (`int_synth::legacy`),
|
||||
# declared so the included source's `cfg` names a known feature. The goldens here decode
|
||||
# as a sink does, through the integer operator set; the crate's own tests compile the
|
||||
# direct decode's unit tests under `cfg(test)`.
|
||||
legacy_int = []
|
||||
|
|
|
|||
|
|
@ -84,16 +84,14 @@ impl Lcg {
|
|||
}
|
||||
}
|
||||
|
||||
/// Integer fixed-point synthesis end-to-end: a constructed 2-layer integer
|
||||
/// model + an 8×8 int8 latent → 32×32 RGB whose SHA-256 must equal the golden
|
||||
/// constant on EVERY architecture (the integer-exact `replicate` guarantee).
|
||||
#[test]
|
||||
fn int_synth_output_hash_is_golden() {
|
||||
// Produced by the reference run (macOS arm64); every other platform must
|
||||
// reproduce it bit-for-bit.
|
||||
const GOLDEN_SHA256: &str =
|
||||
"84e76529e360a98c3867d13af0a156fe31e8e8773a3f688b2ca4f63a91860cad";
|
||||
/// The int_synth golden's SHA-256: produced by the reference run (macOS arm64); every
|
||||
/// other platform must reproduce it bit-for-bit.
|
||||
const INT_SYNTH_GOLDEN_SHA256: &str =
|
||||
"84e76529e360a98c3867d13af0a156fe31e8e8773a3f688b2ca4f63a91860cad";
|
||||
|
||||
/// The int_synth golden's input: a constructed 2-layer integer model (`int_synth.json`)
|
||||
/// and a payload of 6×8×8 int8 latents for a 32×32 output.
|
||||
fn int_synth_golden_case() -> (serde_json::Value, Vec<u8>) {
|
||||
let (c_in, s) = (6usize, 8usize);
|
||||
let mut rng = Lcg(0x5EED_CAFE);
|
||||
let layer = |rng: &mut Lcg, in_ch: usize, out_ch: usize, shift: u32, relu: bool| {
|
||||
|
|
@ -109,10 +107,6 @@ fn int_synth_output_hash_is_golden() {
|
|||
layer(&mut rng, 8, 3, 3, false),
|
||||
]});
|
||||
|
||||
let dir = std::env::temp_dir().join("wai_byte_exact_golden");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("int_synth.json"), serde_json::to_vec(&spec).unwrap()).unwrap();
|
||||
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&32u32.to_le_bytes()); // H
|
||||
payload.extend_from_slice(&32u32.to_le_bytes()); // W
|
||||
|
|
@ -122,13 +116,40 @@ fn int_synth_output_hash_is_golden() {
|
|||
for _ in 0..c_in * s * s {
|
||||
payload.push((rng.next_i32(-128, 127) as i8) as u8);
|
||||
}
|
||||
(spec, payload)
|
||||
}
|
||||
|
||||
/// Integer fixed-point synthesis end-to-end: a constructed 2-layer integer
|
||||
/// model + an 8×8 int8 latent → 32×32 RGB whose SHA-256 must equal the golden
|
||||
/// constant on EVERY architecture (the integer-exact `replicate` guarantee).
|
||||
#[test]
|
||||
fn int_synth_output_hash_is_golden() {
|
||||
let (spec, payload) = int_synth_golden_case();
|
||||
let dir = std::env::temp_dir().join("wai_byte_exact_golden");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("int_synth.json"), serde_json::to_vec(&spec).unwrap()).unwrap();
|
||||
|
||||
let img = int_synth::decode(&payload, &dir.join("model")).expect("decode");
|
||||
assert_eq!((img.width, img.height), (32, 32));
|
||||
let hash = format!("{:x}", Sha256::digest(&img.rgb));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert_eq!(
|
||||
hash, GOLDEN_SHA256,
|
||||
hash, INT_SYNTH_GOLDEN_SHA256,
|
||||
"int_synth output diverged on this platform — the integer-exact contract is broken"
|
||||
);
|
||||
}
|
||||
|
||||
/// The same golden through the byte entry a sink without a filesystem calls
|
||||
/// (`decode_from_spec`): the set's lowering `int_synth/1` (integer-operators §13.1),
|
||||
/// loaded under `acc64` with its bounds certificate and run by the operator set's
|
||||
/// interpreter, must give the same bytes on every architecture.
|
||||
#[test]
|
||||
fn int_synth_through_its_lowering_is_golden() {
|
||||
let (spec, payload) = int_synth_golden_case();
|
||||
let img = int_synth::decode_from_spec(&payload, &serde_json::to_vec(&spec).unwrap()).expect("decode");
|
||||
assert_eq!((img.width, img.height), (32, 32));
|
||||
assert_eq!(
|
||||
format!("{:x}", Sha256::digest(&img.rgb)), INT_SYNTH_GOLDEN_SHA256,
|
||||
"int_synth through its lowering diverged on this platform"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ pub mod int_entropy;
|
|||
#[path = "../../wai-rs/src/int_kernels.rs"]
|
||||
pub mod int_kernels;
|
||||
|
||||
// The bridge the integer decoders load their lowered graphs through and run them by
|
||||
// (`wai-ops/1`, the integer operator set): int_synth decodes through its lowering, so
|
||||
// the matrix checks the decode a sink runs.
|
||||
#[path = "../../wai-rs/src/int_ops.rs"]
|
||||
pub mod int_ops;
|
||||
|
||||
// The merged byte-exact JPEG-AI decoder's entropy stage (crate-root, self-contained —
|
||||
// no shims needed). Included so the cross-arch byte-equality matrix covers the decoder
|
||||
// that `wai.image.jpegai` actually reconstructs with, not only the older `neural/` port.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
>
|
||||
> **What a reader can build from this text.** With the payload extensions it cites, an implementation of this text decodes `wai.video.int_motion`, `wai.audio.int_codec`, `wai.splat.int_codec`, `wai.neural.int_synth`, `wai.neural.int_hyper`, `wai.video.int_hyper`, `wai.neural.int_mlicpp` and `wai.{image,video,audio,splat}.int_refine`, and reproduces the symbols and scale indices that `wai.image.jpegai` entropy-decodes (§13.6.1). The payload text four of these depend on, `entropy.json`, the `int_synth` payload and the `WIM1` payload with its `WMB1` bundle, is in [integer-payloads](integer-payloads.md), and §15.2 says where. The integer renderer of `wai.world.model` and the other integer networks of `wai.image.jpegai` are informative (§13.5, §13.6.2).
|
||||
>
|
||||
> Reference implementation: the `wai-ops` crate (`wai/wai-ops`): parse, lowering, interpreter, stage operators and bounds. Tool: `wai-verify bounds`. Conformance: `wai/ops-conformance/`. This Draft precedes all three. Until they land, each capability's reference implementation is the decoder its payload extension names, and §15.3 lists where this text and that decoder differ.
|
||||
> Reference implementation: the `wai-ops` crate (`wai/wai-ops`): parse, lowering, interpreter, stage operators and bounds. Tool: `wai-verify bounds`. Conformance: `wai/ops-conformance/`. A capability's reference decoder decodes through this operator set from the revision that registers the capability's `ops` member (§11); in this revision, `wai.neural.int_synth`. Until then, a capability's reference implementation is the decoder its payload extension names, and §15.3 lists where this text and that decoder differ.
|
||||
>
|
||||
> The keywords **MUST**, **MUST NOT**, **SHOULD** and **MAY** are as defined in RFC 2119 and RFC 8174.
|
||||
|
||||
|
|
@ -1203,9 +1203,11 @@ Sizes of 0 mean that the input's size is set per decode (§10.2). A driver plans
|
|||
Then `clamp(0, 255)`. The graph's one output is the clamp's.
|
||||
|
||||
**Load checks.** These are in addition to the payload's own checks, and every refusal is `payload_malformed`:
|
||||
- there is at least one layer;
|
||||
- the layers chain: the first `in_ch` is `C`, and each later `in_ch` is the previous `out_ch`;
|
||||
- the parameter lengths are exact;
|
||||
- `k ≥ 1`, `stride ≥ 1` and `shift ≤ 127`;
|
||||
- `in_ch`, `out_ch`, `k`, `stride`, `pad` and `out_pad` are below `2^32`, the width of their `WHG1` fields (§10.4);
|
||||
- `out_ch ≥ 1`, `k ≥ 1`, `stride ≥ 1` and `shift ≤ 127`;
|
||||
- the caps (§3);
|
||||
- the certificate meets `acc64`.
|
||||
|
||||
|
|
@ -1521,7 +1523,8 @@ No committed output changes, and no committed refusal fixture changes its code.
|
|||
|
||||
1. **`wai.neural.int_synth`.**
|
||||
- A set whose shift is 128 or more, whose `k` or `stride` is 0, or whose certificate fails `acc64`, is refused.
|
||||
- A payload whose reserved byte is not 0, or whose `C` is 0, is refused. The reference decoder does not check either today.
|
||||
- A set with no layers, or with a layer whose `out_ch` is 0, is refused, and so is one with a field of 2^32 or more. The direct decoder decoded each of these where the shapes allowed: no layers presented the latents themselves as the image, and a layer of no channels left the next with only its bias.
|
||||
- A payload whose reserved byte is not 0, or whose `C` is 0, is refused. The direct decoder checked neither.
|
||||
- A shift from 64 to 127 now has its exact meaning.
|
||||
- The caps apply.
|
||||
2. **`wai.world.model`'s integer renderer** refuses values outside its lowered `check` ranges, and a set whose `afrac` is outside `1..=30` or differs between its two networks.
|
||||
|
|
|
|||
|
|
@ -600,9 +600,10 @@ SPEC §5 keeps this capability's payload format; this section writes it down.
|
|||
The format has no magic and no version. The payload is exactly `12 + C·S·S` bytes, and
|
||||
`C·S·S ≤ 2^26`.
|
||||
|
||||
The set is one file, `int_synth.json`: a JSON object whose member `layers` is an array
|
||||
of layer objects. A layer has `in_ch`, `out_ch`, `k`, `stride` and `pad`, each a
|
||||
non-negative integer; `out_pad`, a non-negative integer, 0 when absent; `weight`,
|
||||
The set is one file, `int_synth.json`: a JSON object whose member `layers` is a
|
||||
non-empty array of layer objects. A layer has `in_ch`, `out_ch`, `k`, `stride` and
|
||||
`pad`, each a non-negative integer below `2^32`, with `out_ch`, `k` and `stride` at
|
||||
least 1; `out_pad`, a non-negative integer below `2^32`, 0 when absent; `weight`,
|
||||
`in_ch·out_ch·k·k` integers each fitting `i32`, in the layout `[in_ch][out_ch][k][k]`;
|
||||
`bias`, `out_ch` integers each fitting `i32`; `shift`, in `0..=127`, 0 when absent; and
|
||||
`relu`, a boolean, false when absent. Other members are ignored.
|
||||
|
|
@ -611,13 +612,15 @@ The decode is [integer-operators](integer-operators.md) §13.1's graph on the la
|
|||
Its output must be `[3, H, W]`, and it is presented as row-major HWC bytes. Every
|
||||
refusal of the payload, the set or the decode is `payload_malformed` (§8).
|
||||
|
||||
**Checks that wait for the lowering.** The reference decoder
|
||||
(`int_synth::decode_bytes`) makes every check of this section except these, which it
|
||||
makes once the capability decodes through its lowering
|
||||
([integer-operators](integer-operators.md) §15.3): a reserved byte of 0; `C ≥ 1`; the
|
||||
caps; `k ≥ 1`, `stride ≥ 1` and a shift of at most 127; and the certificate. Until
|
||||
then it decodes a payload or set that fails one of them where it can, and with a shift
|
||||
from 64 to 127 its result depends on how it was built.
|
||||
The reference decoder (`int_synth::decode_bytes`) decodes through the lowering. It
|
||||
checks the payload first: its header, a reserved byte of 0, `C ≥ 1`, `C·S·S ≤ 2^26` and
|
||||
its length. It then lowers the set and loads the graph under `acc64`, which makes the
|
||||
set's checks ([integer-operators](integer-operators.md) §13.1: at least one layer, the
|
||||
layers chain, the lengths, fields below `2^32`, `out_ch`, `k` and `stride` at least 1,
|
||||
a shift of at most 127, the caps and the certificate), and runs it. A shift from 64 to
|
||||
127 has its exact meaning. Several of these checks are new refusals of input no
|
||||
committed corpus contains ([integer-operators](integer-operators.md) §15.3, SPEC §8
|
||||
*Planned breaks*).
|
||||
|
||||
## 14. `WIM1` and `WMB1` — `wai.neural.int_mlicpp`
|
||||
|
||||
|
|
|
|||
|
|
@ -739,7 +739,13 @@
|
|||
"sets": [],
|
||||
"spec_section": "§5 Neural capabilities",
|
||||
"edition": null,
|
||||
"registered": "2026-06-13"
|
||||
"registered": "2026-06-13",
|
||||
"ops": {
|
||||
"set": "wai-ops/1",
|
||||
"profile": "acc64",
|
||||
"subset": "full",
|
||||
"lowering": "int_synth/1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "wai.neural.mbt2018_mean",
|
||||
|
|
|
|||
|
|
@ -132,6 +132,78 @@
|
|||
"type": "string",
|
||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
|
||||
"description": "The date of the first commit of this repository that registers the string; the specification entered the repository on 2026-06-12."
|
||||
},
|
||||
"ops": {
|
||||
"type": "object",
|
||||
"description": "The integer operator set the capability decodes through (extensions/integer-operators.md §11). Absent for a capability that does not.",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"set",
|
||||
"profile",
|
||||
"subset",
|
||||
"lowering"
|
||||
],
|
||||
"properties": {
|
||||
"set": {
|
||||
"const": "wai-ops/1"
|
||||
},
|
||||
"profile": {
|
||||
"enum": [
|
||||
"acc32",
|
||||
"acc64"
|
||||
]
|
||||
},
|
||||
"subset": {
|
||||
"enum": [
|
||||
"full",
|
||||
"npu"
|
||||
]
|
||||
},
|
||||
"lowering": {
|
||||
"type": "string"
|
||||
},
|
||||
"stage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"rans64": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"table",
|
||||
"escape",
|
||||
"end"
|
||||
],
|
||||
"properties": {
|
||||
"table": {
|
||||
"enum": [
|
||||
"t4097",
|
||||
"t2"
|
||||
]
|
||||
},
|
||||
"escape": {
|
||||
"enum": [
|
||||
"lenient",
|
||||
"strict"
|
||||
]
|
||||
},
|
||||
"end": {
|
||||
"enum": [
|
||||
"canonical",
|
||||
"final_only"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bucket": {
|
||||
"enum": [
|
||||
"ceil",
|
||||
"floor"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,17 +151,60 @@ fn to64(v: i128, at: u32) -> Res<i64> {
|
|||
i64::try_from(v).map_err(|_| wide(at))
|
||||
}
|
||||
|
||||
/// A host's own kernels for some operators (§6). A sink may compute an operator's
|
||||
/// output by other means (another order of the same sums, narrower types where the
|
||||
/// values fit, threads, vector lanes) provided it gives the same integers; [`run_with`]
|
||||
/// asks the host first and computes an operator itself only where the host declines.
|
||||
/// Everything else stays the interpreter's: the plan, every scalar and input check,
|
||||
/// every tensor's range policy, the state of a sequence and every refusal.
|
||||
pub trait Kernels {
|
||||
/// The outputs of `op` on its inputs `ins` (in the operator's input order), each
|
||||
/// output `k` of the shape `out[k]` (`(C, H, W)`, from the run-time plan); or `None`
|
||||
/// for the interpreter to compute them. An implementation that returns `Some`
|
||||
/// returns exactly the integers the interpreter would, so it returns `None` for
|
||||
/// anything it does not hold to that: an operator whose arithmetic would leave
|
||||
/// its narrower types, or one the interpreter would refuse.
|
||||
fn eval(&self, op: &crate::graph::Op, ins: &[&Tensor], out: &[(u32, u32, u32)]) -> Option<Vec<Tensor>>;
|
||||
}
|
||||
|
||||
/// The interpreter's own kernels for every operator: a host that has none.
|
||||
pub struct Reference;
|
||||
|
||||
impl Kernels for Reference {
|
||||
#[inline(always)]
|
||||
fn eval(&self, _: &crate::graph::Op, _: &[&Tensor], _: &[(u32, u32, u32)]) -> Option<Vec<Tensor>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// `rq_o(v)` (§5.1) for output channel `o`: the requantisation a host kernel applies to
|
||||
/// the exact sum `v` it formed, so that its outputs are the interpreter's. `None`
|
||||
/// where a value leaves `i128`, which no certified graph forms.
|
||||
pub fn rq(rq: &Rq, o: usize, v: i128) -> Option<i128> {
|
||||
rq_apply(rq, o, v, 0, &mut Quiet)
|
||||
}
|
||||
|
||||
/// Run one decode of `g`, a graph that passed every load step ([`crate::graph::load`]).
|
||||
/// `inputs` holds the data inputs, in input-table order; each state input is taken
|
||||
/// from `seq` (§10.3). `scalars` holds every scalar's value. Returns the graph's
|
||||
/// outputs, in output-table order. A refused decode leaves `seq` as it was.
|
||||
pub fn run(g: &Loaded, inputs: &[Tensor], scalars: &[i64], seq: &mut Sequence) -> Res<Vec<Tensor>> {
|
||||
run_observed(g, inputs, scalars, seq, &mut Quiet)
|
||||
run_inner(g, inputs, scalars, seq, &mut Quiet, &Reference)
|
||||
}
|
||||
|
||||
/// [`run`], with the host's kernels `k` asked first for each operator ([`Kernels`]).
|
||||
/// The outputs and refusals are [`run`]'s, provided `k` keeps its contract.
|
||||
pub fn run_with<K: Kernels + ?Sized>(g: &Loaded, inputs: &[Tensor], scalars: &[i64], seq: &mut Sequence, k: &K) -> Res<Vec<Tensor>> {
|
||||
run_inner(g, inputs, scalars, seq, &mut Quiet, k)
|
||||
}
|
||||
|
||||
/// [`run`], reporting every named intermediate and producer result to `obs`
|
||||
/// ([`Observe`]). The outputs and refusals are [`run`]'s.
|
||||
pub fn run_observed<O: Observe>(g: &Loaded, inputs: &[Tensor], scalars: &[i64], seq: &mut Sequence, obs: &mut O) -> Res<Vec<Tensor>> {
|
||||
run_inner(g, inputs, scalars, seq, obs, &Reference)
|
||||
}
|
||||
|
||||
fn run_inner<O: Observe, K: Kernels + ?Sized>(g: &Loaded, inputs: &[Tensor], scalars: &[i64], seq: &mut Sequence, obs: &mut O, k: &K) -> Res<Vec<Tensor>> {
|
||||
let g: &Graph = g.graph();
|
||||
// The inputs, data and state, in input-table order.
|
||||
let n_data = g.inputs.iter().filter(|i| i.kind == 0).count();
|
||||
|
|
@ -204,7 +247,16 @@ pub fn run_observed<O: Observe>(g: &Loaded, inputs: &[Tensor], scalars: &[i64],
|
|||
vals[rec.tensor as usize] = Some(t);
|
||||
}
|
||||
for (i, op) in g.ops.iter().enumerate() {
|
||||
let outs = eval(g, op, i as u32, &vals, &hw, scalars, obs)?;
|
||||
let host = {
|
||||
let ins: Vec<&Tensor> = op.ins.iter().map(|&t| get(&vals, t)).collect();
|
||||
let shapes: Vec<(u32, u32, u32)> =
|
||||
op.outs.iter().map(|&t| (g.tensors[t as usize].channels, hw[t as usize].0 as u32, hw[t as usize].1 as u32)).collect();
|
||||
k.eval(op, &ins, &shapes)
|
||||
};
|
||||
let outs = match host {
|
||||
Some(o) => o,
|
||||
None => eval(g, op, i as u32, &vals, &hw, scalars, obs)?,
|
||||
};
|
||||
for (k, ten) in outs.iter().enumerate() {
|
||||
for &v in &ten.data {
|
||||
obs.see(i as u32, OUT[k], v as i128);
|
||||
|
|
|
|||
|
|
@ -49,4 +49,4 @@ pub mod xint;
|
|||
pub use bounds::Profile;
|
||||
pub use code::{Code, Refusal, Site};
|
||||
pub use graph::{Graph, Loaded};
|
||||
pub use interp::{Sequence, Tensor, run};
|
||||
pub use interp::{Kernels, Sequence, Tensor, run, run_with};
|
||||
|
|
|
|||
5
wai/wai-pack/Cargo.lock
generated
5
wai/wai-pack/Cargo.lock
generated
|
|
@ -680,9 +680,14 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"wai-ops",
|
||||
"wai-quantum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wai-ops"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "wai-pack"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
5
wai/wai-rs/Cargo.lock
generated
5
wai/wai-rs/Cargo.lock
generated
|
|
@ -4451,12 +4451,17 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"wai-ops",
|
||||
"wai-quantum",
|
||||
"xz2",
|
||||
"zstd",
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wai-ops"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.3"
|
||||
|
|
|
|||
|
|
@ -102,6 +102,13 @@ curve25519-dalek = { version = "4", optional = true }
|
|||
# without the media/codec chain. `wai` re-exports it unchanged.
|
||||
wai-quantum = { version = "0.4.1", path = "../wai-quantum", optional = true, default-features = false }
|
||||
|
||||
# ---- the integer operator set (behind `neural_int`) ----
|
||||
# `wai-ops/1` (extensions/integer-operators.md): an integer decoder that decodes
|
||||
# through it lowers its parameter set to WHG1 graphs and loads them under its
|
||||
# capability's profile, the bounds certificate included (src/int_ops.rs). no_std, no
|
||||
# dependencies of its own.
|
||||
wai-ops = { path = "../wai-ops", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["codecs"]
|
||||
# The zeroth-condition codec wrappers (image/audio/video/text) + the C
|
||||
|
|
@ -127,7 +134,12 @@ ffi = []
|
|||
# construction — so it sits behind its own light feature, checkable in a CI
|
||||
# matrix with no ONNX Runtime present. `neural` pulls it in. blake3 is for the
|
||||
# conformance corpus's content hash (the same hash the other classes freeze).
|
||||
neural_int = ["dep:blake3"]
|
||||
neural_int = ["dep:blake3", "dep:wai-ops"]
|
||||
# Test-only: the integer decoders' direct decodes, as they were before they decoded
|
||||
# through the integer operator set (`int_synth::legacy`). The differential tests decode
|
||||
# random sets and payloads both ways and require the same output or the same refusal
|
||||
# outside the planned breaks (tests/int_ops_differential.rs). No sink enables it.
|
||||
legacy_int = ["neural_int"]
|
||||
# Deterministic integer generative synthesis (`wai.generate.*`): byte-exact
|
||||
# generation pinned to (model, prompt, seed, steps) — the create-side of the
|
||||
# integer contract. Pure i64, no ONNX/float (serde_json is already a base dep),
|
||||
|
|
@ -734,6 +746,16 @@ name = "int_kernels_geometry"
|
|||
path = "tests/int_kernels_geometry.rs"
|
||||
required-features = ["neural_int"]
|
||||
|
||||
[[test]]
|
||||
name = "int_ops_differential"
|
||||
path = "tests/int_ops_differential.rs"
|
||||
required-features = ["legacy_int"]
|
||||
|
||||
[[test]]
|
||||
name = "int_synth_ops"
|
||||
path = "tests/int_synth_ops.rs"
|
||||
required-features = ["legacy_int"]
|
||||
|
||||
[[test]]
|
||||
name = "int_mlicpp"
|
||||
path = "tests/int_mlicpp.rs"
|
||||
|
|
|
|||
|
|
@ -1330,6 +1330,35 @@ pub fn capability_replicate_class(cap: &str) -> Option<ReplicateClass> {
|
|||
})
|
||||
}
|
||||
|
||||
/// The integer operator set a capability's decode runs through
|
||||
/// (`extensions/integer-operators.md` §11): the registry's `ops` member. `profile` is the
|
||||
/// profile every conforming parameter set of the capability meets, and a set whose
|
||||
/// certificate does not meet it is refused; `subset` is `full` or `npu`; `lowering`
|
||||
/// names the §13 lowering the capability's sets are lowered by; `rans64` and `bucket`
|
||||
/// fix the modes of the stage operators its driver uses (§5E). Strengthening a profile
|
||||
/// or a subset narrows the sets a capability accepts, a breaking change (SPEC §8).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OpsMember {
|
||||
pub profile: &'static str,
|
||||
pub subset: &'static str,
|
||||
pub lowering: &'static str,
|
||||
/// `rans64_decode`'s `(table, escape, end)` (E1), where the driver decodes one.
|
||||
pub rans64: Option<(&'static str, &'static str, &'static str)>,
|
||||
/// `bucket`'s rule (E3), where the driver buckets a scale.
|
||||
pub bucket: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// The `ops` member of `cap`, for a capability decoded through the integer operator
|
||||
/// set; `None` for any other. A capability gains one when its decoder runs its
|
||||
/// networks through their lowering (`extensions/integer-operators.md` §15.1).
|
||||
pub fn capability_ops(cap: &str) -> Option<OpsMember> {
|
||||
let ops = |lowering, rans64, bucket| Some(OpsMember { profile: "acc64", subset: "full", lowering, rans64, bucket });
|
||||
match cap {
|
||||
CAP_NEURAL_INT_SYNTH => ops("int_synth/1", None, None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a capability's **neural** decode is integer-exact by construction —
|
||||
/// the integer-entropy contract. Isolates exactly the neural codecs WAI itself
|
||||
/// guarantees byte-identical on every machine, so a deployer can require
|
||||
|
|
|
|||
|
|
@ -564,6 +564,9 @@ pub struct Registration {
|
|||
/// See [`ROWS`]: a designation, or `None` for the edition current at `registered`.
|
||||
pub edition: Option<&'static str>,
|
||||
pub registered: &'static str,
|
||||
/// The integer operator set the capability decodes through, if it does
|
||||
/// (`extensions/integer-operators.md` §11).
|
||||
pub ops: Option<super::OpsMember>,
|
||||
}
|
||||
|
||||
/// Every registered string — payload, companion, derivation and record — sorted
|
||||
|
|
@ -585,6 +588,7 @@ pub fn registrations() -> Vec<Registration> {
|
|||
spec_section: section,
|
||||
edition,
|
||||
registered,
|
||||
ops: super::capability_ops(cap),
|
||||
})
|
||||
.collect();
|
||||
for op in derivation::derivation_operations() {
|
||||
|
|
@ -602,6 +606,7 @@ pub fn registrations() -> Vec<Registration> {
|
|||
spec_section: S_PRIOR,
|
||||
edition: Some("ISO/IEC 23001-16:2021"),
|
||||
registered: DERIVATIONS_REGISTERED,
|
||||
ops: None,
|
||||
});
|
||||
}
|
||||
v.sort_by(|a, b| a.capability.cmp(b.capability));
|
||||
|
|
@ -746,7 +751,7 @@ pub fn capabilities_json() -> String {
|
|||
let caps: Vec<Value> = registrations()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
json!({
|
||||
let mut v = json!({
|
||||
"capability": r.capability,
|
||||
"kind": r.kind.label(),
|
||||
"status": r.status.label(),
|
||||
|
|
@ -759,7 +764,11 @@ pub fn capabilities_json() -> String {
|
|||
"spec_section": r.spec_section,
|
||||
"edition": r.edition,
|
||||
"registered": r.registered,
|
||||
})
|
||||
});
|
||||
if let Some(o) = r.ops {
|
||||
v["ops"] = ops_json(&o);
|
||||
}
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
let mut ids: Vec<&Identifier> = IDENTIFIERS.iter().collect();
|
||||
|
|
@ -790,12 +799,63 @@ pub fn capabilities_json() -> String {
|
|||
}))
|
||||
}
|
||||
|
||||
/// The registry's `ops` member (`extensions/integer-operators.md` §11), its members in
|
||||
/// that section's order.
|
||||
fn ops_json(o: &super::OpsMember) -> serde_json::Value {
|
||||
use serde_json::{json, Map, Value};
|
||||
let mut v = json!({ "set": "wai-ops/1", "profile": o.profile, "subset": o.subset, "lowering": o.lowering });
|
||||
let mut stage = Map::new();
|
||||
if let Some((table, escape, end)) = o.rans64 {
|
||||
stage.insert("rans64".into(), json!({ "table": table, "escape": escape, "end": end }));
|
||||
}
|
||||
if let Some(b) = o.bucket {
|
||||
stage.insert("bucket".into(), Value::from(b));
|
||||
}
|
||||
if !stage.is_empty() {
|
||||
v["stage"] = Value::Object(stage);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn nullable_enum(labels: &[&str]) -> serde_json::Value {
|
||||
let mut e: Vec<serde_json::Value> = labels.iter().map(|l| serde_json::Value::from(*l)).collect();
|
||||
e.push(serde_json::Value::Null);
|
||||
serde_json::json!({ "enum": e })
|
||||
}
|
||||
|
||||
/// The schema of a capability's `ops` member (`extensions/integer-operators.md` §11).
|
||||
fn ops_schema() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"description": "The integer operator set the capability decodes through (extensions/integer-operators.md §11). Absent for a capability that does not.",
|
||||
"additionalProperties": false,
|
||||
"required": ["set", "profile", "subset", "lowering"],
|
||||
"properties": {
|
||||
"set": { "const": "wai-ops/1" },
|
||||
"profile": { "enum": ["acc32", "acc64"] },
|
||||
"subset": { "enum": ["full", "npu"] },
|
||||
"lowering": { "type": "string" },
|
||||
"stage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"rans64": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["table", "escape", "end"],
|
||||
"properties": {
|
||||
"table": { "enum": ["t4097", "t2"] },
|
||||
"escape": { "enum": ["lenient", "strict"] },
|
||||
"end": { "enum": ["canonical", "final_only"] }
|
||||
}
|
||||
},
|
||||
"bucket": { "enum": ["ceil", "floor"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `wai/registry/capabilities.schema.json` (JSON Schema 2020-12). Its enums are
|
||||
/// the registry's label sets.
|
||||
pub fn capabilities_schema() -> String {
|
||||
|
|
@ -845,7 +905,8 @@ pub fn capabilities_schema() -> String {
|
|||
"type": "string",
|
||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
|
||||
"description": "The date of the first commit of this repository that registers the string; the specification entered the repository on 2026-06-12."
|
||||
}
|
||||
},
|
||||
"ops": ops_schema()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -1356,6 +1417,32 @@ mod tests {
|
|||
assert!(md == want, "the generated table in wai/REGISTRY.md differs from the registry: run `wai registry export`");
|
||||
}
|
||||
|
||||
/// The `ops` members (`extensions/integer-operators.md` §11). Only a
|
||||
/// `NeuralIntegerExact` capability carries one, every one is at `acc64` with the
|
||||
/// full subset and names a lowering of §13, and the export writes exactly these.
|
||||
/// In this revision `wai.neural.int_synth` is the one capability that decodes
|
||||
/// through the operator set.
|
||||
#[test]
|
||||
fn ops_members_name_the_operator_set_a_capability_decodes_through() {
|
||||
let regs = registrations();
|
||||
let with: Vec<&str> = regs.iter().filter(|r| r.ops.is_some()).map(|r| r.capability).collect();
|
||||
assert_eq!(with, [CAP_NEURAL_INT_SYNTH]);
|
||||
for r in regs.iter().filter(|r| r.ops.is_some()) {
|
||||
let o = r.ops.unwrap();
|
||||
assert_eq!(r.replicate, Some(ReplicateClass::NeuralIntegerExact), "{}", r.capability);
|
||||
assert_eq!((o.profile, o.subset), ("acc64", "full"), "{}", r.capability);
|
||||
assert!(["int_synth/1", "whm1/1", "wam1/1", "wmb1/1", "world/1", "jpegai-int/1"].contains(&o.lowering), "{}", r.capability);
|
||||
}
|
||||
let export: serde_json::Value = serde_json::from_str(&read("registry/capabilities.json")).unwrap();
|
||||
for c in export["capabilities"].as_array().unwrap() {
|
||||
let want = match c["capability"].as_str().unwrap() {
|
||||
CAP_NEURAL_INT_SYNTH => serde_json::json!({ "set": "wai-ops/1", "profile": "acc64", "subset": "full", "lowering": "int_synth/1" }),
|
||||
_ => serde_json::Value::Null,
|
||||
};
|
||||
assert_eq!(c["ops"], want, "{}", c["capability"]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The schema's enums are the label sets the registry uses.
|
||||
#[test]
|
||||
fn registry_schema_enums_equal_labels() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@
|
|||
//! them. The scalar convolution, transposed-convolution, IGDN, channel-attention
|
||||
//! (MDTA) and channel-LayerNorm kernels they replace are kept verbatim beside each
|
||||
//! caller (`reference` modules) and stay selectable ([`Policy::reference`]); they are
|
||||
//! the executable specification. The JPEG-AI element-wise steps (the GDFN gate and
|
||||
//! the executable specification. `int_synth` reaches the convolution kernels through
|
||||
//! the integer operator set (`int_ops`): the interpreter lends them its convolutions
|
||||
//! under a fast policy and computes every operator itself under the reference policy,
|
||||
//! so for it the operator set's interpreter is the reference. The JPEG-AI element-wise steps (the GDFN gate and
|
||||
//! residuals in `int_transformer`, the activations and gates in `jpegai_synthesis`)
|
||||
//! were instead rewritten in place onto `par_map` / `par_zip`: each output element is
|
||||
//! the same pure function of its own inputs as before, and under the reference
|
||||
|
|
@ -57,9 +60,10 @@
|
|||
//! proportion to what the reference touches (below), and a transposed geometry
|
||||
//! whose sizes the reference's own arithmetic cannot form: an uncropped plane
|
||||
//! `cout·(ho + 2·ph)·(wo + 2·pw)` (the buffer the int_hyper, int_audio and
|
||||
//! JPEG-AI references accumulate into before cropping) beyond the allocation cap,
|
||||
//! or an int_synth output size that its reference's `i64` arithmetic overflows —
|
||||
//! so such input gets the reference's own behaviour, in either profile.
|
||||
//! JPEG-AI references accumulate into before cropping) beyond the allocation cap —
|
||||
//! so such input gets the reference's own behaviour, in either profile. (Behind
|
||||
//! the integer operator set's interpreter, `int_ops`, a declined operator is the
|
||||
//! interpreter's, whose plan has refused every size past its caps already.)
|
||||
//! * **Work the fast path adds of its own stays in proportion to the reference's**
|
||||
//! and cannot overflow where the reference performs no such step. Its buffer
|
||||
//! sizes and indices are bounded by checked geometry and its worker-count
|
||||
|
|
@ -113,8 +117,10 @@
|
|||
//! kept output position (the int_hyper transposed and regular convolutions,
|
||||
//! including rows wide enough for the SIMD lanes), overflow only at a cropped border
|
||||
//! position (the int_hyper and int_audio transposed convolutions, `i64`, and the
|
||||
//! JPEG-AI one, `i128`) and operands beyond the bound (int_synth); they assert
|
||||
//! matching panics with overflow checks on and matching integers with them off.
|
||||
//! JPEG-AI one, `i128`); they assert matching panics with overflow checks on and
|
||||
//! matching integers with them off. Behind the interpreter (`int_ops`), the fast
|
||||
//! convolutions are held to the interpreter instead, operator by operator on random
|
||||
//! graphs and decoder by decoder on every committed vector.
|
||||
//! Geometry at the edges runs through the public decoders under every policy
|
||||
//! (`tests/int_kernels_geometry.rs`): a 1×1 input upsampled by strides up to
|
||||
//! `usize::MAX`, a regular convolution whose stride exceeds its padded input,
|
||||
|
|
@ -714,9 +720,7 @@ impl ConvGeom {
|
|||
// wrapped size and then indexes out of it; where it is merely huge, the
|
||||
// reference's allocation is huge. The fast kernel never allocates the plane,
|
||||
// so it runs only while the plane is within the cap every other buffer is
|
||||
// held to; beyond it the reference runs and fails (or not) its own way. The
|
||||
// bound also keeps every term of int_synth's `i64` output-size expression
|
||||
// (at most `full_h` or `full_w`) far from overflowing `i64`.
|
||||
// held to; beyond it the reference runs and fails (or not) its own way.
|
||||
if cout.checked_mul(full_h)?.checked_mul(full_w)? > MAX_ELEMS {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -1611,8 +1615,9 @@ mod tests {
|
|||
// that can refuse it. At `padded == PAD_FACTOR · touched` the fast kernel
|
||||
// runs; beyond it the geometry is refused. That each caller then runs its
|
||||
// reference, with the same integers, is pinned beside each dispatch (the
|
||||
// `padding_beyond_the_pad_factor_runs_the_reference` tests: int_hyper_synth,
|
||||
// int_synth and int_transformer on these shapes, int_audio on 1-D ones).
|
||||
// `padding_beyond_the_pad_factor_runs_the_reference` tests: int_hyper_synth
|
||||
// and int_transformer on these shapes, int_audio on 1-D ones; int_synth's
|
||||
// operators, declined, are the interpreter's).
|
||||
use testkit::{fast_conv_runs, FAST};
|
||||
let measure = |g: &ConvGeom| {
|
||||
let lay = Layout::new(g).unwrap();
|
||||
|
|
|
|||
197
wai/wai-rs/src/int_ops.rs
Normal file
197
wai/wai-rs/src/int_ops.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
//! The integer decoders' networks through `wai-ops/1`, the integer operator set
|
||||
//! (`extensions/integer-operators.md`).
|
||||
//!
|
||||
//! A decoder lowers its parameter set to `WHG1` graphs (§13) and loads each graph as a
|
||||
//! sink does, from its canonical bytes, under its capability's profile: load steps 1–8
|
||||
//! of §10.5, the last of which is the bounds pass, whose certificate must meet that
|
||||
//! profile (§8.6). A set whose certificate does not meet it is refused. It then runs
|
||||
//! each decode through the operator set's interpreter ([`wai_ops::run_with`]), which
|
||||
//! owns the run-time plan, every range check and every refusal (§4), and lends it this
|
||||
//! crate's fast kernels ([`crate::int_kernels`]) for the operators they cover (§6),
|
||||
//! under the current [`crate::int_kernels::Policy`]:
|
||||
//!
|
||||
//! - under [`KernelPath::Reference`] the interpreter computes every operator itself, so
|
||||
//! a decode is the operator set's reference;
|
||||
//! - under [`KernelPath::Fast`] a convolution and a transposed convolution run on the
|
||||
//! fast kernels, which form the same exact sums (`int_kernels`' equivalence rule) and
|
||||
//! finish each output with the operator set's own requantisation
|
||||
//! ([`wai_ops::interp::rq`]). Where a fast kernel declines (operands past its narrowed
|
||||
//! types, geometry it does not cover, a value that would leave `i64`), the interpreter
|
||||
//! computes the operator, and refuses it where it refuses.
|
||||
//!
|
||||
//! Either way the integers are the interpreter's: the tests below check it operator by
|
||||
//! operator on random graphs, and `tests/int_synth_ops.rs` decoder by decoder on every
|
||||
//! committed vector under every policy.
|
||||
|
||||
use wai_ops::graph::{Graph, Loaded, Op, Params};
|
||||
use wai_ops::interp::{Kernels, Sequence, Tensor};
|
||||
use wai_ops::{Profile, Refusal};
|
||||
|
||||
use crate::int_kernels::{policy, ConvGeom, KernelPath};
|
||||
|
||||
/// The profile of every capability that decodes through the operator set
|
||||
/// (`extensions/integer-operators.md` §11): `acc64`, which every lowering of §13
|
||||
/// declares. A set may meet `acc32` as well; its certificate shows that.
|
||||
const PROFILE: Profile = Profile::Acc64;
|
||||
|
||||
/// Load a lowered graph as a sink does: from its canonical bytes, under [`PROFILE`].
|
||||
/// A refusal is `model_mismatch`, or `too_large` for a size (§10.5).
|
||||
pub fn load(g: &Graph) -> Result<Loaded, Refusal> {
|
||||
wai_ops::graph::load(&g.to_bytes(), PROFILE)
|
||||
}
|
||||
|
||||
/// One decode of a loaded graph with one data input and no state, through the
|
||||
/// interpreter with this crate's kernels behind it ([`Host`]): its first output.
|
||||
pub fn run1(g: &Loaded, x: Tensor, scalars: &[i64]) -> Result<Tensor, Refusal> {
|
||||
let mut out = wai_ops::run_with(g, &[x], scalars, &mut Sequence::new(), &Host)?;
|
||||
Ok(out.swap_remove(0))
|
||||
}
|
||||
|
||||
/// This crate's fast kernels, lent to the interpreter (see the module docs).
|
||||
pub struct Host;
|
||||
|
||||
impl Kernels for Host {
|
||||
fn eval(&self, op: &Op, ins: &[&Tensor], out: &[(u32, u32, u32)]) -> Option<Vec<Tensor>> {
|
||||
if policy().path != KernelPath::Fast {
|
||||
return None;
|
||||
}
|
||||
let x = ins.first()?;
|
||||
let &(oc, oh, ow) = out.first()?;
|
||||
let (cin, h, w) = (x.c as usize, x.h as usize, x.w as usize);
|
||||
let u = |v: &u32| *v as usize;
|
||||
let g = match &op.p {
|
||||
Params::Conv { cout, kh, kw, sh, sw, ph, pw, groups, w: wt, .. } => {
|
||||
ConvGeom::conv(cin, h, w, u(cout), u(groups), u(kh), u(kw), u(sh), u(sw), u(ph), u(pw), x.data.len(), wt.len())?
|
||||
}
|
||||
Params::ConvT { cout, kh, kw, sh, sw, ph, pw, oph, opw, w: wt, .. } => {
|
||||
ConvGeom::transposed(cin, h, w, u(cout), u(kh), u(kw), u(sh), u(sw), u(ph), u(pw), u(oph), u(opw), x.data.len(), wt.len())?
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let (Params::Conv { rq, w: wt, .. } | Params::ConvT { rq, w: wt, .. }) = &op.p else { return None };
|
||||
if (g.cout, g.ho, g.wo) != (oc as usize, oh as usize, ow as usize) {
|
||||
return None;
|
||||
}
|
||||
Some(vec![Tensor::new(oc, oh, ow, conv(&g, &x.data, wt, rq)?)])
|
||||
}
|
||||
}
|
||||
|
||||
/// A (transposed) convolution on the fast kernel, each output finished with the
|
||||
/// operator set's requantisation. `None` where the kernel declines, or where a
|
||||
/// requantised value would leave `i64`, which no certified graph forms: the interpreter
|
||||
/// then computes the operator, and refuses it.
|
||||
fn conv(g: &ConvGeom, x: &[i64], wt: &[i64], rq: &wai_ops::graph::Rq) -> Option<Vec<i64>> {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
let wide = AtomicBool::new(false);
|
||||
let out = crate::int_kernels::conv_acc_i128(g, x, wt, |oc, acc| match wai_ops::interp::rq(rq, oc, acc).map(i64::try_from) {
|
||||
Some(Ok(v)) => v,
|
||||
_ => {
|
||||
wide.store(true, Ordering::Relaxed);
|
||||
0
|
||||
}
|
||||
})?;
|
||||
(!wide.load(Ordering::Relaxed)).then_some(out)
|
||||
}
|
||||
|
||||
/// Gate 5 of each decoder's migration, the per-operator half: the fast kernels behind
|
||||
/// the interpreter against the interpreter alone, on random graphs of one convolution or
|
||||
/// transposed convolution. Every output element must be the same integer under every
|
||||
/// fast policy, and a refusal the same refusal. The whole-decoder half is
|
||||
/// `tests/int_synth_ops.rs`.
|
||||
#[cfg(test)]
|
||||
mod equivalence {
|
||||
use super::*;
|
||||
use crate::int_kernels::testkit::{fast_conv_runs, Rng, FAST};
|
||||
use crate::int_kernels::{with_policy, Policy};
|
||||
use wai_ops::graph::{opcode, BiasKind, InputRec, Round, Rq, TensorRec, CAP};
|
||||
|
||||
fn graph(x: TensorRec, code: u8, cout: u32, p: Params) -> Graph {
|
||||
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![x, TensorRec::derived(cout)],
|
||||
ops: vec![Op { code, ins: vec![0], outs: vec![1], p }],
|
||||
}
|
||||
}
|
||||
|
||||
fn rq(g: &mut Rng, cout: u32, round: Round, bias_kind: BiasKind, mult: bool) -> Rq {
|
||||
let n = cout as usize;
|
||||
Rq {
|
||||
round,
|
||||
bias_kind,
|
||||
m_kind: if mult { 2 } else { 0 },
|
||||
s_kind: 1,
|
||||
m_bytes: if mult { 8 } else { 0 },
|
||||
b_bytes: if bias_kind == BiasKind::None { 0 } else { 8 },
|
||||
mult: if mult { g.vec(n, 1 << 20, 0) } else { vec![] },
|
||||
shift: vec![g.below(40) as u8],
|
||||
bias: if bias_kind == BiasKind::None { vec![] } else { g.vec(n, 1 << 24, 10) },
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `g` on `x` under the interpreter alone and under every fast policy; the
|
||||
/// results must agree. Returns whether the graph loaded and how many decodes the
|
||||
/// fast kernel computed.
|
||||
fn agree(g: &Graph, x: &Tensor) -> (bool, u64) {
|
||||
let Ok(l) = load(g) else { return (false, 0) };
|
||||
let want = with_policy(Policy::reference(), || run1(&l, x.clone(), &[]));
|
||||
let mut fast = 0;
|
||||
for p in FAST {
|
||||
let (got, runs) = fast_conv_runs(|| with_policy(p, || run1(&l, x.clone(), &[])));
|
||||
assert_eq!(got, want, "{p:?} on operator {}", g.ops[0].code);
|
||||
fast += runs;
|
||||
}
|
||||
(true, fast)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convolutions_equal_the_interpreter() {
|
||||
let mut r = Rng(0x0D2C_0001);
|
||||
let (mut loaded, mut fast) = (0, 0);
|
||||
for case in 0..400 {
|
||||
let (k, s) = (1 + r.below(5), 1 + r.below(3));
|
||||
let (cin, cout) = (1 + r.below(5) as u32, 1 + r.below(6) as u32);
|
||||
let (h, w) = (1 + r.below(6) as u32, 1 + r.below(8) as u32);
|
||||
let round = [Round::Floor, Round::HalfUp, Round::HalfEven][r.below(3)];
|
||||
let bias_kind = [BiasKind::None, BiasKind::Pre, BiasKind::Post][r.below(3)];
|
||||
let with_mult = r.below(2) == 1;
|
||||
let q = rq(&mut r, cout, round, bias_kind, with_mult);
|
||||
let xmax = [127i64, 1 << 12, 1 << 20][r.below(3)];
|
||||
let wt = r.vec(cin as usize * cout as usize * k * k, 1 << 14, 10);
|
||||
let pad = r.below(k) as u32;
|
||||
let (k, s) = (k as u32, s as u32);
|
||||
let (code, p) = if case % 2 == 1 {
|
||||
let op = if s > 1 { r.below(s as usize) as u32 } else { 0 };
|
||||
(opcode::CONV_TRANSPOSE, Params::ConvT { cout, kh: k, kw: k, sh: s, sw: s, ph: pad, pw: pad, oph: op, opw: op, w_bytes: 8, rq: q, w: wt })
|
||||
} else {
|
||||
(opcode::CONV, Params::Conv { cout, kh: k, kw: k, sh: s, sw: s, ph: pad, pw: pad, groups: 1, w_bytes: 8, rq: q, w: wt })
|
||||
};
|
||||
let g = graph(TensorRec::check(cin, -xmax, xmax), code, cout, p);
|
||||
let x = Tensor::new(cin, h, w, r.vec((cin * h * w) as usize, xmax, 30));
|
||||
let (l, f) = agree(&g, &x);
|
||||
loaded += l as u32;
|
||||
fast += f;
|
||||
}
|
||||
assert!(loaded > 300, "only {loaded} of 400 random graphs loaded");
|
||||
assert!(fast > 600, "the fast kernel ran only {fast} times");
|
||||
}
|
||||
|
||||
/// A value outside an input's `check` range is refused by the interpreter before any
|
||||
/// kernel runs, under every policy alike.
|
||||
#[test]
|
||||
fn a_check_range_refuses_before_the_kernel() {
|
||||
let p = Params::Conv {
|
||||
cout: 1, kh: 1, kw: 1, sh: 1, sw: 1, ph: 0, pw: 0, groups: 1, w_bytes: 1,
|
||||
rq: Rq::plain(Round::Floor, 0), w: vec![3],
|
||||
};
|
||||
let g = graph(TensorRec::check(1, -10, 10), opcode::CONV, 1, p);
|
||||
let l = load(&g).unwrap();
|
||||
for pol in [Policy::reference(), Policy::fast(1), Policy::fast(0)] {
|
||||
assert_eq!(with_policy(pol, || run1(&l, Tensor::new(1, 1, 2, vec![10, -10]), &[])).unwrap().data, vec![30, -30]);
|
||||
let r = with_policy(pol, || run1(&l, Tensor::new(1, 1, 2, vec![10, 11]), &[])).unwrap_err();
|
||||
assert_eq!(r.code, wai_ops::Code::FieldRange, "{pol:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
//! thing that makes cross-hardware bit-exactness real — lives here and is exact
|
||||
//! whatever the weights.
|
||||
//!
|
||||
//! Wire format (little-endian):
|
||||
//! Wire format (little-endian), `extensions/integer-payloads.md` §13:
|
||||
//! u32 H, u32 W, u16 C, u8 S_log2, u8 _reserved (12 B)
|
||||
//! C*S*S bytes int8 latents, channel-major
|
||||
//!
|
||||
|
|
@ -34,9 +34,19 @@
|
|||
//! weight:[i32; in*out*k*k] (layout ic,oc,ky,kx),
|
||||
//! bias:[i32; out_ch], shift:u32, relu:bool }, … ] }
|
||||
//! The final layer MUST output 3 channels at H×W; values are clamped to [0,255].
|
||||
//!
|
||||
//! The decode ([`decode_from_spec`]) is the graph `synth` of the set's lowering
|
||||
//! `int_synth/1` (`extensions/integer-operators.md` §13.1), loaded under `acc64` with its
|
||||
//! bounds certificate and run through the integer operator set ([`crate::int_ops`]),
|
||||
//! whose interpreter lends the transposed convolutions to the fast kernels. The direct
|
||||
//! decode it replaced is kept, test-only (feature `legacy_int`), as `legacy`: each
|
||||
//! layer's transposed convolution, bias, shift and ReLU computed here by the scalar
|
||||
//! kernel. The differential tests decode random sets and payloads both ways
|
||||
//! (`tests/int_ops_differential.rs`).
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(any(test, feature = "legacy_int"))]
|
||||
use serde::Deserialize;
|
||||
|
||||
const HEADER: usize = 12;
|
||||
|
|
@ -66,6 +76,7 @@ pub struct IntImage {
|
|||
pub height: u32,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "legacy_int"))]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct LayerSpec {
|
||||
in_ch: usize,
|
||||
|
|
@ -87,67 +98,17 @@ struct LayerSpec {
|
|||
relu: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "legacy_int"))]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct SynthSpec {
|
||||
layers: Vec<LayerSpec>,
|
||||
}
|
||||
|
||||
/// One integer transposed convolution + bias + requantize + optional ReLU — see
|
||||
/// [`reference::conv_transpose2d`] for the definition. Runs the fast kernel
|
||||
/// ([`crate::int_kernels`]) unless the current [`crate::int_kernels::Policy`] selects
|
||||
/// the reference; both give the same integer for every output element, and every
|
||||
/// error case is left to the reference so errors are reported exactly as before.
|
||||
fn conv_transpose2d(
|
||||
input: &[i64],
|
||||
cin: usize,
|
||||
hin: usize,
|
||||
win: usize,
|
||||
l: &LayerSpec,
|
||||
) -> Result<(Vec<i64>, usize, usize), IntSynthError> {
|
||||
use crate::int_kernels::{conv_acc_i64, policy, ConvGeom, KernelPath};
|
||||
let (k, s, p, op) = (l.k, l.stride, l.pad, l.out_pad);
|
||||
if policy().path == KernelPath::Fast
|
||||
&& Some(l.weight.len()) == cin.checked_mul(l.out_ch).and_then(|v| v.checked_mul(k)).and_then(|v| v.checked_mul(k))
|
||||
&& l.bias.len() == l.out_ch
|
||||
&& let Some(g) = ConvGeom::transposed(cin, hin, win, l.out_ch, k, k, s, s, p, p, op, op, input.len(), l.weight.len())
|
||||
&& reference_output_side(hin, l) == Some(g.ho)
|
||||
&& reference_output_side(win, l) == Some(g.wo)
|
||||
{
|
||||
let wt: Vec<i64> = l.weight.iter().map(|&v| v as i64).collect();
|
||||
let out = conv_acc_i64(&g, input, &wt, |oc, v| {
|
||||
let mut x = (v + l.bias[oc] as i64) >> l.shift;
|
||||
if l.relu && x < 0 {
|
||||
x = 0;
|
||||
}
|
||||
x
|
||||
});
|
||||
// `None`: operands outside the kernel's no-overflow bound — the reference runs.
|
||||
if let Some(out) = out {
|
||||
return Ok((out, g.ho, g.wo));
|
||||
}
|
||||
}
|
||||
reference::conv_transpose2d(input, cin, hin, win, l)
|
||||
}
|
||||
|
||||
/// One output side exactly as [`reference::conv_transpose2d`] computes it — in
|
||||
/// `i64`, from the same `as` casts, in the same order — or `None` where that
|
||||
/// expression overflows `i64` (the reference then panics with overflow checks on
|
||||
/// and wraps with them off) or is negative. The fast path runs only where this
|
||||
/// equals the side it computed in checked `usize` arithmetic, so it never returns
|
||||
/// where the reference's size arithmetic fails. `ConvGeom::transposed`'s bound on
|
||||
/// the uncropped plane already keeps every term far below `i64::MAX`; this check
|
||||
/// states the rule where the reference's arithmetic is, independent of that cap.
|
||||
fn reference_output_side(n: usize, l: &LayerSpec) -> Option<usize> {
|
||||
let v = (n as i64)
|
||||
.checked_sub(1)?
|
||||
.checked_mul(l.stride as i64)?
|
||||
.checked_add(l.k as i64)?
|
||||
.checked_add(l.out_pad as i64)?
|
||||
.checked_sub(2i64.checked_mul(l.pad as i64)?)?;
|
||||
usize::try_from(v).ok()
|
||||
}
|
||||
|
||||
/// The scalar reference kernel the fast path is checked against, kept verbatim.
|
||||
/// The direct decode's transposed convolution, kept verbatim: the scalar kernel its fast
|
||||
/// dispatch was checked against. That dispatch, and the guard that kept it to sizes this
|
||||
/// kernel's own arithmetic forms, are gone: the decode runs through the integer operator
|
||||
/// set, whose plan forms every size in checked arithmetic under its caps.
|
||||
#[cfg(any(test, feature = "legacy_int"))]
|
||||
mod reference {
|
||||
use super::{IntSynthError, LayerSpec};
|
||||
|
||||
|
|
@ -259,7 +220,40 @@ pub fn decode_bytes(payload: &[u8], spec_json: &[u8]) -> Result<IntImage, IntSyn
|
|||
/// `int_synth.json` contents). No filesystem, no float, no ONNX — the
|
||||
/// integer-exact decode a browser / WASM sink runs directly, host-supplied
|
||||
/// weights in, RGB out. Byte-identical on every machine.
|
||||
///
|
||||
/// It decodes through the integer operator set. The payload is checked first
|
||||
/// (`extensions/integer-payloads.md` §13): its header, a reserved byte of 0, `C ≥ 1`,
|
||||
/// `C·S·S ≤ 2^26` and its exact length. Then the set is lowered (integer-operators
|
||||
/// §13.1, whose checks include at least one layer, fields below `2^32`, `out_ch`, `k`
|
||||
/// and `stride` at least 1 and a shift of at most 127) and loaded under `acc64`: load
|
||||
/// steps 1–8, the last being the bounds certificate ([`crate::int_ops`]). Then the graph
|
||||
/// runs on the latents, through the operator set's run-time plan with its caps, and its
|
||||
/// output must be `[3, H, W]`. Every refusal is [`IntSynthError::InvalidPayload`]
|
||||
/// (`payload_malformed`).
|
||||
pub fn decode_from_spec(payload: &[u8], spec_json: &[u8]) -> Result<IntImage, IntSynthError> {
|
||||
let (h, w, c, s) = header(payload)?;
|
||||
let refused = |what: &str, r: wai_ops::Refusal| IntSynthError::InvalidPayload(format!("int_synth {what}: {} at {:?}", r.code.as_str(), r.at));
|
||||
let set = wai_ops::lower::int_synth(spec_json).map_err(|r| refused("int_synth.json does not lower", r))?;
|
||||
let g = crate::int_ops::load(set.get("synth").expect("int_synth/1 lowers to the graph synth")).map_err(|r| refused("set does not load", r))?;
|
||||
let latents: Vec<i64> = payload[HEADER..].iter().map(|&b| b as i8 as i64).collect();
|
||||
let out = crate::int_ops::run1(&g, wai_ops::Tensor::new(c, s, s, latents), &[]).map_err(|r| refused("decode", r))?;
|
||||
if (out.c, out.h as usize, out.w as usize) != (3, h, w) {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth output [{},{},{}] != [3,{h},{w}]", out.c, out.h, out.w)));
|
||||
}
|
||||
// `clamp(0, 255)` ends the graph, so every value is a byte.
|
||||
let hw = h * w;
|
||||
let mut rgb = vec![0u8; hw * 3];
|
||||
for (k, &v) in out.data.iter().enumerate() {
|
||||
rgb[(k % hw) * 3 + k / hw] = v as u8;
|
||||
}
|
||||
Ok(IntImage { rgb, width: w as u32, height: h as u32 })
|
||||
}
|
||||
|
||||
/// The payload's header (`extensions/integer-payloads.md` §13): `(H, W, C, S)`, after
|
||||
/// its checks. The side is `2^S_log2`; `C·S·S` is formed without overflow and is at
|
||||
/// most `2^26`, and the payload is exactly `12 + C·S·S` bytes.
|
||||
fn header(payload: &[u8]) -> Result<(usize, usize, u32, u32), IntSynthError> {
|
||||
if payload.len() < HEADER {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth payload {} B < {HEADER} B header", payload.len())));
|
||||
|
|
@ -267,55 +261,93 @@ pub fn decode_from_spec(payload: &[u8], spec_json: &[u8]) -> Result<IntImage, In
|
|||
let h = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
|
||||
let w = u32::from_le_bytes(payload[4..8].try_into().unwrap()) as usize;
|
||||
let c = u16::from_le_bytes(payload[8..10].try_into().unwrap()) as usize;
|
||||
// The side is 2^payload[10]. A shift of the word's width or more panics with
|
||||
// overflow checks on and masks without them, so the same bytes would decode two
|
||||
// ways; and C·S·S must be formed without overflow. Neither can match a payload's
|
||||
// length when it overflows, so both are refused here as a length mismatch.
|
||||
if payload[11] != 0 {
|
||||
return Err(IntSynthError::InvalidPayload("int_synth payload's reserved byte is not 0".into()));
|
||||
}
|
||||
if c == 0 {
|
||||
return Err(IntSynthError::InvalidPayload("int_synth payload has no latent channels".into()));
|
||||
}
|
||||
const CAP: usize = 1 << 26;
|
||||
let n = 1usize.checked_shl(payload[10] as u32)
|
||||
.and_then(|s| c.checked_mul(s)?.checked_mul(s).map(|n| (s, n)));
|
||||
.and_then(|s| c.checked_mul(s)?.checked_mul(s).map(|n| (s, n)))
|
||||
.filter(|&(_, n)| n <= CAP);
|
||||
let Some((s, _)) = n.filter(|&(_, n)| HEADER.checked_add(n) == Some(payload.len())) else {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth payload {} B is not {HEADER} + C·S·S with C = {c}, S = 2^{}", payload.len(), payload[10])));
|
||||
"int_synth payload {} B is not {HEADER} + C·S·S with C = {c}, S = 2^{}, C·S·S ≤ 2^26", payload.len(), payload[10])));
|
||||
};
|
||||
Ok((h, w, c as u32, s as u32))
|
||||
}
|
||||
|
||||
let spec: SynthSpec = serde_json::from_slice(spec_json)
|
||||
.map_err(|e| IntSynthError::InvalidPayload(format!("int_synth.json parse: {e}")))?;
|
||||
/// The direct decode, as the decoder ran before it decoded through the integer operator
|
||||
/// set: the payload and set read here, each layer's transposed convolution, bias, shift
|
||||
/// and ReLU computed by the scalar reference kernel. Test-only (`legacy_int`): the
|
||||
/// differential tests decode both ways, and the two differ only on the planned breaks
|
||||
/// of `extensions/integer-operators.md` §15.3.
|
||||
#[cfg(any(test, feature = "legacy_int"))]
|
||||
pub mod legacy {
|
||||
use super::*;
|
||||
|
||||
// int8 latents → i64.
|
||||
let mut cur: Vec<i64> = payload[HEADER..].iter().map(|&b| b as i8 as i64).collect();
|
||||
let (mut cin, mut ch, mut cw) = (c, s, s);
|
||||
for (i, l) in spec.layers.iter().enumerate() {
|
||||
if l.in_ch != cin {
|
||||
/// The direct decode of [`super::decode_from_spec`]'s arguments.
|
||||
pub fn decode_from_spec(payload: &[u8], spec_json: &[u8]) -> Result<IntImage, IntSynthError> {
|
||||
if payload.len() < HEADER {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth layer {i} in_ch {} != current {cin}", l.in_ch)));
|
||||
"int_synth payload {} B < {HEADER} B header", payload.len())));
|
||||
}
|
||||
let (out, ho, wo) = conv_transpose2d(&cur, cin, ch, cw, l)?;
|
||||
cur = out;
|
||||
cin = l.out_ch;
|
||||
ch = ho;
|
||||
cw = wo;
|
||||
}
|
||||
if cin != 3 || ch != h || cw != w {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth output [{cin},{ch},{cw}] != [3,{h},{w}]")));
|
||||
}
|
||||
let h = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
|
||||
let w = u32::from_le_bytes(payload[4..8].try_into().unwrap()) as usize;
|
||||
let c = u16::from_le_bytes(payload[8..10].try_into().unwrap()) as usize;
|
||||
// The side is 2^payload[10]. A shift of the word's width or more panics with
|
||||
// overflow checks on and masks without them, so the same bytes would decode two
|
||||
// ways; and C·S·S must be formed without overflow. Neither can match a payload's
|
||||
// length when it overflows, so both are refused here as a length mismatch.
|
||||
let n = 1usize.checked_shl(payload[10] as u32)
|
||||
.and_then(|s| c.checked_mul(s)?.checked_mul(s).map(|n| (s, n)));
|
||||
let Some((s, _)) = n.filter(|&(_, n)| HEADER.checked_add(n) == Some(payload.len())) else {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth payload {} B is not {HEADER} + C·S·S with C = {c}, S = 2^{}", payload.len(), payload[10])));
|
||||
};
|
||||
|
||||
let mut rgb = vec![0u8; h * w * 3];
|
||||
let hw = h * w;
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let pi = y * w + x;
|
||||
for ch_i in 0..3 {
|
||||
rgb[pi * 3 + ch_i] = cur[ch_i * hw + pi].clamp(0, 255) as u8;
|
||||
let spec: SynthSpec = serde_json::from_slice(spec_json)
|
||||
.map_err(|e| IntSynthError::InvalidPayload(format!("int_synth.json parse: {e}")))?;
|
||||
|
||||
// int8 latents → i64.
|
||||
let mut cur: Vec<i64> = payload[HEADER..].iter().map(|&b| b as i8 as i64).collect();
|
||||
let (mut cin, mut ch, mut cw) = (c, s, s);
|
||||
for (i, l) in spec.layers.iter().enumerate() {
|
||||
if l.in_ch != cin {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth layer {i} in_ch {} != current {cin}", l.in_ch)));
|
||||
}
|
||||
let (out, ho, wo) = reference::conv_transpose2d(&cur, cin, ch, cw, l)?;
|
||||
cur = out;
|
||||
cin = l.out_ch;
|
||||
ch = ho;
|
||||
cw = wo;
|
||||
}
|
||||
if cin != 3 || ch != h || cw != w {
|
||||
return Err(IntSynthError::InvalidPayload(format!(
|
||||
"int_synth output [{cin},{ch},{cw}] != [3,{h},{w}]")));
|
||||
}
|
||||
|
||||
let mut rgb = vec![0u8; h * w * 3];
|
||||
let hw = h * w;
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let pi = y * w + x;
|
||||
for ch_i in 0..3 {
|
||||
rgb[pi * 3 + ch_i] = cur[ch_i * hw + pi].clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(IntImage { rgb, width: w as u32, height: h as u32 })
|
||||
}
|
||||
Ok(IntImage { rgb, width: w as u32, height: h as u32 })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
// The direct decode's scalar kernel, against hand computation.
|
||||
use super::reference::conv_transpose2d;
|
||||
|
||||
fn layer(out_ch: usize, in_ch: usize, k: usize, stride: usize, weight: Vec<i32>) -> LayerSpec {
|
||||
LayerSpec {
|
||||
|
|
@ -396,139 +428,3 @@ mod tests {
|
|||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
/// The fast transposed conv against the scalar reference, element by element —
|
||||
/// results AND errors.
|
||||
#[cfg(test)]
|
||||
mod kernel_equivalence {
|
||||
use super::*;
|
||||
use crate::int_kernels::testkit::{Rng, FAST};
|
||||
use crate::int_kernels::with_policy;
|
||||
|
||||
#[test]
|
||||
fn conv_transpose2d_equals_reference() {
|
||||
let mut g = Rng(0x5E7_0001);
|
||||
for case in 0..500 {
|
||||
let k = 1 + g.below(5);
|
||||
let stride = 1 + g.below(3);
|
||||
let pad = g.below(k + 1);
|
||||
let out_pad = if stride > 1 { g.below(stride) } else { 0 };
|
||||
let (h, w) = (1 + g.below(6), 1 + g.below(8));
|
||||
let (cin, out_ch) = (1 + g.below(5), 1 + g.below(7));
|
||||
let mut weight: Vec<i32> = g.vec(cin * out_ch * k * k, 1 << 14, 10).into_iter().map(|v| v as i32).collect();
|
||||
let mut bias: Vec<i32> = g.vec(out_ch, 1 << 18, 0).into_iter().map(|v| v as i32).collect();
|
||||
// exercise the error paths too: wrong weight / bias length
|
||||
match case % 25 {
|
||||
0 => { weight.pop(); }
|
||||
1 => { bias.push(0); }
|
||||
_ => {}
|
||||
}
|
||||
let l = LayerSpec {
|
||||
in_ch: cin, out_ch, k, stride, pad, out_pad, weight, bias,
|
||||
shift: g.below(12) as u32, relu: g.below(2) == 1,
|
||||
};
|
||||
let x = g.vec(cin * h * w, 127, 40); // int8 latents, sparse
|
||||
let want = reference::conv_transpose2d(&x, cin, h, w, &l);
|
||||
for p in FAST {
|
||||
let got = with_policy(p, || conv_transpose2d(&x, cin, h, w, &l));
|
||||
assert_eq!(got, want, "int_synth {p:?} case {case}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Operands beyond the fast kernel's no-overflow bound, laid out so the only
|
||||
/// overflowing sums would be at border positions (a 1×1 input, 3×3 kernel,
|
||||
/// pad 1, corner taps `2^22` on inputs `2^40`). This reference skips positions
|
||||
/// outside the output rather than accumulating and cropping them, so it forms
|
||||
/// no such sum and returns the kept centre `2·2^40 = 2^41` in every build. The
|
||||
/// fast path hands these operands to the reference and must agree.
|
||||
#[test]
|
||||
fn operands_beyond_the_bound_follow_the_reference() {
|
||||
let (cin, out_ch, k) = (2usize, 1usize, 3usize);
|
||||
let mut weight = vec![0i32; cin * out_ch * k * k];
|
||||
for tap in weight.chunks_mut(k * k) {
|
||||
for c in [0, 2, 6, 8] {
|
||||
tap[c] = 1 << 22;
|
||||
}
|
||||
tap[4] = 1;
|
||||
}
|
||||
let l = LayerSpec { in_ch: cin, out_ch, k, stride: 1, pad: 1, out_pad: 0, weight, bias: vec![0], shift: 0, relu: false };
|
||||
let x = vec![1i64 << 40; cin];
|
||||
let want = reference::conv_transpose2d(&x, cin, 1, 1, &l);
|
||||
assert_eq!(want, Ok((vec![1i64 << 41], 1, 1)), "the reference forms no border sum");
|
||||
for p in FAST {
|
||||
assert_eq!(with_policy(p, || conv_transpose2d(&x, cin, 1, 1, &l)), want, "{p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Geometry whose padded input would be more than `PAD_FACTOR` times what the
|
||||
/// reference reads and writes is refused by the fast path, and the reference
|
||||
/// runs: the fast kernel is not entered and the result is the reference's. At
|
||||
/// exactly the factor the fast kernel runs. The shapes and their sizes are worked
|
||||
/// in `int_kernels`' `a_padded_input_beyond_pad_factor_is_left_to_the_reference`.
|
||||
#[test]
|
||||
fn padding_beyond_the_pad_factor_runs_the_reference() {
|
||||
use crate::int_kernels::testkit::fast_conv_runs;
|
||||
// 1×1 input and kernel, stride 1: (channels, pad, output pad, expected
|
||||
// fast-kernel runs, output side). No tap reaches the kept output, so each
|
||||
// output is the bias, shifted.
|
||||
for (cin, pad, out_pad, runs, side) in [(2usize, 6usize, 13usize, 1u64, 2usize), (2, 7, 15, 0, 2), (1, 6, 12, 0, 1)] {
|
||||
let l = LayerSpec {
|
||||
in_ch: cin, out_ch: 1, k: 1, stride: 1, pad, out_pad, weight: vec![3; cin], bias: vec![14], shift: 1, relu: false,
|
||||
};
|
||||
let x = vec![5i64; cin];
|
||||
let want = reference::conv_transpose2d(&x, cin, 1, 1, &l);
|
||||
assert_eq!(want, Ok((vec![7; side * side], side, side)), "reference, pad {pad} out_pad {out_pad}");
|
||||
for p in FAST {
|
||||
let got = fast_conv_runs(|| with_policy(p, || conv_transpose2d(&x, cin, 1, 1, &l)));
|
||||
assert_eq!(got, (want.clone(), runs), "pad {pad} out_pad {out_pad} {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The reference sizes its output in `i64`; the fast path runs only where that
|
||||
/// arithmetic neither overflows nor disagrees with its own `usize` geometry.
|
||||
/// `reference_output_side` evaluates the reference's expression step by step,
|
||||
/// and here refuses every input on which the reference's arithmetic overflows.
|
||||
/// Through the dispatch, the fast kernel is not entered for them, and every
|
||||
/// policy has the reference's outcome: a panic with overflow checks on, and the
|
||||
/// wrapped (here still correct) size with them off.
|
||||
#[test]
|
||||
fn the_reference_output_size_is_checked_in_its_own_arithmetic() {
|
||||
use crate::int_kernels::testkit::{fast_conv_runs, overflow_checks};
|
||||
use crate::int_kernels::Policy;
|
||||
let spec = |stride, pad, out_pad| LayerSpec {
|
||||
in_ch: 1, out_ch: 1, k: 3, stride, pad, out_pad, weight: (1..=9).collect(), bias: vec![0], shift: 0, relu: false,
|
||||
};
|
||||
// Ordinary: (4 − 1)·2 + 3 + 1 − 2·1 = 8.
|
||||
assert_eq!(reference_output_side(4, &spec(2, 1, 1)), Some(8));
|
||||
// Not positive: 0·1 + 3 + 0 − 2·2 = −1.
|
||||
assert_eq!(reference_output_side(1, &spec(1, 2, 0)), None);
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
{
|
||||
let s = 1usize << 62;
|
||||
// (3 − 1)·2^62 = 2^63: the product overflows i64.
|
||||
assert_eq!(reference_output_side(3, &spec(s, s, 0)), None);
|
||||
// 2^62 + 3 + 2^62: the second addition overflows i64, although the
|
||||
// wrapped result would be the true size, 3.
|
||||
assert_eq!(reference_output_side(2, &spec(s, s, s)), None);
|
||||
// 2·2^62 overflows i64 (0 + 3 + 0 − 2^63 would be negative anyway).
|
||||
assert_eq!(reference_output_side(1, &spec(1, s, 0)), None);
|
||||
|
||||
// Through the dispatch: a 2×2 input, stride = pad = output pad = 2^62,
|
||||
// a 3×3 output that only input (1, 1) reaches, through every tap.
|
||||
let l = spec(s, s, s);
|
||||
let x = [1i64, 2, 3, 4];
|
||||
let run = |p: Policy| fast_conv_runs(|| std::panic::catch_unwind(|| with_policy(p, || conv_transpose2d(&x, 1, 2, 2, &l))).ok());
|
||||
let want = run(Policy::reference());
|
||||
if overflow_checks() {
|
||||
assert_eq!(want, (None, 0), "the reference overflows i64 sizing its output");
|
||||
} else {
|
||||
assert_eq!(want, (Some(Ok(((1..=9).map(|v| 4 * v).collect(), 3, 3))), 0));
|
||||
}
|
||||
for p in FAST {
|
||||
assert_eq!(run(p), want, "{p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ pub mod int_synth;
|
|||
// on wasm.
|
||||
#[cfg(feature = "neural_int")]
|
||||
pub mod int_kernels;
|
||||
// The integer decoders' networks through the integer operator set `wai-ops/1`
|
||||
// (extensions/integer-operators.md): each parameter set lowered to WHG1 graphs and
|
||||
// loaded under its capability's profile, the bounds certificate included.
|
||||
#[cfg(feature = "neural_int")]
|
||||
pub mod int_ops;
|
||||
|
||||
// Integer Gaussian-conditional entropy — the reusable scale→bucket primitive
|
||||
// that makes a conditional codec's entropy decode hardware-deterministic (the
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@
|
|||
//! The reference computes each of the first three in a handful of operations. Every
|
||||
//! policy must return the same integers (checked against a hand computation), and
|
||||
//! the fast path must not overflow, or allocate or loop in proportion to the stride
|
||||
//! or the padding, where the reference does not. In the last, the reference panics
|
||||
//! (or, for int_synth with overflow checks off, returns), and every policy must do
|
||||
//! the same. The int_audio 1-D transposed convolution has the
|
||||
//! or the padding, where the reference does not. In the last, the reference panics,
|
||||
//! and every policy must do the same. int_synth decodes through its lowering
|
||||
//! (integer-operators §13.1), whose fields are 32 bits and whose run-time plan caps
|
||||
//! every size it forms at 2^26: where a case is past either, every policy refuses it,
|
||||
//! one of the planned breaks of integer-operators §15.3. The int_audio 1-D transposed
|
||||
//! convolution has the
|
||||
//! same stride check beside its kernel (`int_audio` unit tests). CI runs this file
|
||||
//! beside `int_kernels_vectors` on every `wai-conformance` leg, with overflow checks
|
||||
//! on and off.
|
||||
|
|
@ -40,13 +43,6 @@ fn same_under_every_policy<T: PartialEq + std::fmt::Debug>(what: &str, f: impl F
|
|||
want
|
||||
}
|
||||
|
||||
/// Whether this build traps on integer overflow. Detected by performing one, since
|
||||
/// `cfg!(debug_assertions)` only usually coincides with `overflow-checks`.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn overflow_checks() -> bool {
|
||||
std::panic::catch_unwind(|| std::hint::black_box(i64::MAX) + std::hint::black_box(1)).is_err()
|
||||
}
|
||||
|
||||
/// Run under every policy, catching a panic. The outcome (the value, or that the
|
||||
/// call panicked) must be the same under every policy. Returns the reference
|
||||
/// kernels' outcome, `None` for a panic. The panic message is not compared: a fast
|
||||
|
|
@ -129,8 +125,14 @@ fn a_1x1_input_upsampled_by_any_stride() {
|
|||
let got = same_under_every_policy(&format!("int_synth, stride {s}"), || {
|
||||
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
|
||||
});
|
||||
// centre taps 5, 14, 23 of the three output channels, times 5
|
||||
assert_eq!(got, Ok(vec![25, 70, 115]), "int_synth, stride {s}");
|
||||
if s <= u32::MAX as usize {
|
||||
// centre taps 5, 14, 23 of the three output channels, times 5
|
||||
assert_eq!(got, Ok(vec![25, 70, 115]), "int_synth, stride {s}");
|
||||
} else {
|
||||
// int_synth decodes through its lowering, whose `WHG1` fields are 32 bits: a
|
||||
// stride past them is refused, a planned break.
|
||||
assert!(got.is_err(), "int_synth, stride {s}: a stride past 32 bits is refused");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,11 +171,22 @@ fn a_regular_conv_whose_stride_exceeds_its_padded_input() {
|
|||
fn padding_far_beyond_the_kernel() {
|
||||
let p = 1usize << 12;
|
||||
// Transposed: pad 2^12 with output padding 2^13 − 2 restores a 1×1 output that
|
||||
// no tap reaches (it sits 2^12 rows into the uncropped plane), so it is the bias.
|
||||
// no tap reaches (it sits 2^12 rows into the uncropped plane). int_synth decodes
|
||||
// through its lowering, whose run-time plan caps the uncropped output a transposed
|
||||
// convolution forms at 2^26 elements (integer-operators §5.3); this one is
|
||||
// 3·8193·8193, so every policy refuses it before anything is allocated, a planned
|
||||
// break. With pad 2^9 and output padding 2^10 − 2 the uncropped output, 3·1025·1025,
|
||||
// is within the cap, and the decode gives the bias.
|
||||
let (payload, spec) = (synth_payload(5), synth_spec(1, p, 2 * p - 2, [10, 20, 30]));
|
||||
let got = same_under_every_policy("int_synth, pad 2^12", || {
|
||||
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
|
||||
});
|
||||
assert!(got.is_err(), "int_synth, pad 2^12: the uncropped output is past the cap");
|
||||
let q = 1usize << 9;
|
||||
let (payload, spec) = (synth_payload(5), synth_spec(1, q, 2 * q - 2, [10, 20, 30]));
|
||||
let got = same_under_every_policy("int_synth, pad 2^9", || {
|
||||
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
|
||||
});
|
||||
assert_eq!(got, Ok(vec![10, 20, 30]));
|
||||
// Regular: a 1×1 input, 1×1 kernel, pad 2^12, stride 2^13 — a 2×2 output whose
|
||||
// samples all fall in the padding, so each is the bias.
|
||||
|
|
@ -204,10 +217,11 @@ fn synth_payload_2x2(h: u32, w: u32, latents: [i8; 4]) -> Vec<u8> {
|
|||
// Transposed convolutions of an input with two or more rows and columns, whose
|
||||
// stride and padding make the reference's own size arithmetic overflow. The
|
||||
// int_hyper, int_audio and JPEG-AI references size an uncropped plane
|
||||
// `cout·(ho + 2·pad)·(wo + 2·pad)` in unchecked `usize` arithmetic; the int_synth
|
||||
// reference computes the output size in unchecked `i64`. With overflow checks on,
|
||||
// the reference panics on that arithmetic. With them off it wraps and, for the
|
||||
// uncropped plane, then indexes out of the wrapped buffer. A fast policy must have
|
||||
// `cout·(ho + 2·pad)·(wo + 2·pad)` in unchecked `usize` arithmetic. With overflow
|
||||
// checks on, the reference panics on that arithmetic. With them off it wraps and then
|
||||
// indexes out of the wrapped buffer. (int_synth decodes through its lowering, whose
|
||||
// plan forms every size in checked 64-bit arithmetic and caps it: it refuses these,
|
||||
// below.) A fast policy must have
|
||||
// the same outcome: it may not return a value where the reference panics. A 1×1
|
||||
// input cannot show this, since `(h − 1)·stride` is 0. The int_audio 1-D form is
|
||||
// tested beside its kernel.
|
||||
|
|
@ -265,29 +279,25 @@ fn int_hyper_convt_where_the_reference_plane_size_overflows() {
|
|||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn int_synth_where_the_reference_output_size_overflows_i64() {
|
||||
// 2×2 latent, k = 3, stride = pad = out_pad = 2^62: a 3×3 output. The reference
|
||||
// computes (2 − 1)·2^62 + 3 + 2^62 − 2·2^62 in `i64`, which overflows (2^63 + 3)
|
||||
// although every `usize` step fits. With overflow checks on it panics; with them
|
||||
// off the wrapped sum is still 3 and the decode returns. Only latent (1, 1)
|
||||
// reaches the output, through every tap.
|
||||
// 2×2 latent, k = 3, stride = pad = out_pad = 2^62: a 3×3 output. The direct
|
||||
// decode computed (2 − 1)·2^62 + 3 + 2^62 − 2·2^62 in `i64`, which overflows
|
||||
// (2^63 + 3) although every `usize` step fits: it panicked with overflow checks on
|
||||
// and, with them off, returned the wrapped size's output. int_synth decodes through
|
||||
// its lowering (integer-operators §13.1), whose `WHG1` fields are 32 bits: in every
|
||||
// build and under every policy the decode is refused, not panicked. A planned break.
|
||||
let s = 1usize << 62;
|
||||
let (payload, spec) = (synth_payload_2x2(3, 3, [1, 2, 3, 4]), synth_spec(s, s, s, [0, 0, 0]));
|
||||
let got = same_outcome_under_every_policy("int_synth, 2×2 latent, stride = pad = out_pad = 2^62", || {
|
||||
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
|
||||
});
|
||||
if overflow_checks() {
|
||||
assert_eq!(got, None, "the reference panics on its i64 output size");
|
||||
} else {
|
||||
// rgb[(3y + x)·3 + c] = 4 · weight[c][y][x] = 4 · (9c + 3y + x + 1)
|
||||
let want: Vec<u8> = (0..9).flat_map(|px| (0..3).map(move |c| (4 * (9 * c + px + 1)) as u8)).collect();
|
||||
assert_eq!(got, Some(Ok(want)), "the wrapped output size is 3");
|
||||
}
|
||||
assert!(matches!(got, Some(Err(_))), "refused, not panicked: {got:?}");
|
||||
|
||||
// The u32 fields: no `i64` step overflows, and int_synth's reference allocates
|
||||
// no uncropped plane, so it returns. A 4×4 output that no tap reaches: the bias.
|
||||
// The u32 fields fit `WHG1`, but the uncropped side (2 − 1)·(2^32 − 1) + 3 is past
|
||||
// the run-time plan's cap of 2^26: refused before anything is allocated (the direct
|
||||
// decode returned the bias), a planned break.
|
||||
let (payload, spec) = (synth_payload_2x2(4, 4, [1, 2, 3, 4]), synth_spec(S32, P32, 0, [10, 20, 30]));
|
||||
let got = same_outcome_under_every_policy("int_synth, 2×2 latent, u32 stride and pad", || {
|
||||
wai::int_synth::decode_from_spec(&payload, &spec).map(|img| img.rgb)
|
||||
});
|
||||
assert_eq!(got, Some(Ok([10u8, 20, 30].repeat(16))), "u32 fields: the bias alone");
|
||||
assert!(matches!(got, Some(Err(_))), "u32 fields: refused by the cap: {got:?}");
|
||||
}
|
||||
|
|
|
|||
298
wai/wai-rs/tests/int_ops_differential.rs
Normal file
298
wai/wai-rs/tests/int_ops_differential.rs
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
//! Each integer decoder through the integer operator set against its direct decode
|
||||
//! (`legacy_int`, test-only), on random parameter sets and payloads. The two must give
|
||||
//! the same output, or both refuse with the same code. The only inputs on which they may
|
||||
//! differ are the planned breaks the operator set makes (`extensions/integer-operators.md`
|
||||
//! §15.3), which are new refusals: each case is classified from the payload and the set
|
||||
//! alone, and a case that falls under a planned break must be refused by the decode
|
||||
//! through the operator set, whatever the direct decode does there.
|
||||
//!
|
||||
//! The random cases run for a fixed count by default. `WAI_DIFF_SECS` runs each random
|
||||
//! test for that many seconds instead (the nightly run, `wai-ops-differential.yml`), and
|
||||
//! `WAI_DIFF_SEED` sets the first seed; a failing case names its seed.
|
||||
#![cfg(feature = "legacy_int")]
|
||||
|
||||
use std::panic::catch_unwind;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use wai::int_synth::{decode_from_spec, legacy, IntImage, IntSynthError};
|
||||
|
||||
fn wai() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
||||
}
|
||||
|
||||
/// A splitmix64 sequence.
|
||||
struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = self.0;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
fn below(&mut self, n: u64) -> u64 {
|
||||
self.next() % n
|
||||
}
|
||||
/// A value in `[−r, r]`.
|
||||
fn signed(&mut self, r: i64) -> i64 {
|
||||
(self.below(2 * r as u64 + 1) as i64) - r
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `case` on successive seeds: `n` of them, or for `WAI_DIFF_SECS` seconds. Each
|
||||
/// seed is `WAI_DIFF_SEED` (or a fixed default) mixed with the case's index. Returns how
|
||||
/// many ran.
|
||||
fn cases(name: &str, n: u64, mut case: impl FnMut(&mut Rng, u64)) -> u64 {
|
||||
let secs: Option<u64> = std::env::var("WAI_DIFF_SECS").ok().and_then(|s| s.parse().ok());
|
||||
let seed0: u64 = std::env::var("WAI_DIFF_SEED").ok().and_then(|s| s.parse().ok()).unwrap_or(0x0D2C);
|
||||
let until = secs.map(|s| Instant::now() + Duration::from_secs(s));
|
||||
let mut k = 0u64;
|
||||
loop {
|
||||
match until {
|
||||
Some(t) if Instant::now() >= t => break,
|
||||
None if k >= n => break,
|
||||
_ => {}
|
||||
}
|
||||
let seed = seed0 ^ k.wrapping_mul(0xD1B5_4A32_D192_ED03);
|
||||
case(&mut Rng(seed), seed);
|
||||
k += 1;
|
||||
}
|
||||
eprintln!("{name}: {k} cases from WAI_DIFF_SEED={seed0}");
|
||||
k
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ int_synth (M1)
|
||||
|
||||
const CAP: u128 = 1 << 26;
|
||||
|
||||
/// The planned break of integer-operators §15.3 item 1 that a payload and set fall
|
||||
/// under, if any, read from the bytes alone. `"shift 64 to 127"` is the one that is not a
|
||||
/// refusal: such a shift now has its exact meaning, where the direct decode's result
|
||||
/// depended on the build.
|
||||
fn int_synth_planned_break(payload: &[u8], set: &[u8]) -> Option<&'static str> {
|
||||
if payload.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
if payload[11] != 0 {
|
||||
return Some("a reserved byte that is not 0");
|
||||
}
|
||||
let c = u16::from_le_bytes([payload[8], payload[9]]) as u128;
|
||||
if c == 0 {
|
||||
return Some("C of 0");
|
||||
}
|
||||
let s_log2 = payload[10] as u32;
|
||||
if s_log2 >= 64 {
|
||||
return None;
|
||||
}
|
||||
if (1u128 << (2 * s_log2)).checked_mul(c).is_none_or(|n| n > CAP) {
|
||||
return Some("latents past 2^26");
|
||||
}
|
||||
let Ok(root) = serde_json::from_slice::<Value>(set) else { return None };
|
||||
let Some(layers) = root.get("layers").and_then(Value::as_array) else { return None };
|
||||
if layers.is_empty() {
|
||||
return Some("no layers");
|
||||
}
|
||||
let field = |l: &Value, k: &str, default: Option<u64>| l.get(k).map_or(default, Value::as_u64);
|
||||
let mut shift_64 = false;
|
||||
let mut side = 1u128 << s_log2;
|
||||
for l in layers {
|
||||
let (Some(in_ch), Some(out_ch), Some(k), Some(stride), Some(pad), Some(out_pad), Some(shift)) = (
|
||||
field(l, "in_ch", None), field(l, "out_ch", None), field(l, "k", None), field(l, "stride", None),
|
||||
field(l, "pad", None), field(l, "out_pad", Some(0)), field(l, "shift", Some(0)),
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
if [in_ch, out_ch, k, stride, pad, out_pad].iter().any(|&v| v > u32::MAX as u64) {
|
||||
return Some("a field past 32 bits");
|
||||
}
|
||||
if shift >= 128 {
|
||||
return Some("a shift of 128 or more");
|
||||
}
|
||||
if k == 0 || stride == 0 {
|
||||
return Some("k or stride of 0");
|
||||
}
|
||||
if out_ch == 0 {
|
||||
return Some("out_ch of 0");
|
||||
}
|
||||
shift_64 |= shift >= 64;
|
||||
// The run-time plan's caps (integer-operators §3, §5.3): the uncropped sides and
|
||||
// output, then the output.
|
||||
let full = (side - 1) * stride as u128 + k as u128 + out_pad as u128;
|
||||
if full > CAP || out_ch as u128 * full * full > CAP {
|
||||
return Some("an uncropped output past 2^26");
|
||||
}
|
||||
if full <= 2 * pad as u128 {
|
||||
return None;
|
||||
}
|
||||
side = full - 2 * pad as u128;
|
||||
if out_ch as u128 * side * side > CAP {
|
||||
return Some("a tensor past 2^26");
|
||||
}
|
||||
}
|
||||
let lowered = wai_ops::lower::int_synth(set).ok()?;
|
||||
if wai::int_ops::load(lowered.get("synth")?).is_err() {
|
||||
return Some("a certificate that does not meet acc64");
|
||||
}
|
||||
shift_64.then_some("shift 64 to 127")
|
||||
}
|
||||
|
||||
/// A random `int_synth.json` of 1 to 3 layers and a payload for it: kernels 1 to 4,
|
||||
/// strides 1 to 3, paddings mostly below the kernel and output paddings mostly below the
|
||||
/// stride, shifts 0 to 24, weights of up to 2^4, 2^10 or 2^14 (one set in eight up to
|
||||
/// 2^20, where certificates start to fail), a quarter of them 0, and biases of up to
|
||||
/// 2^18; latent channels 1 to 3, sides 1 to 4, a quarter of the latents at the ends of
|
||||
/// their range. The payload's `H` and `W` are the output's sides, or (one case in eight)
|
||||
/// not; one payload in thirty-two has a reserved byte that is not 0.
|
||||
fn int_synth_case(r: &mut Rng) -> (Vec<u8>, Vec<u8>) {
|
||||
let c0 = 1 + r.below(3) as usize;
|
||||
let s_log2 = r.below(3) as u8;
|
||||
let (mut c, mut side) = (c0, 1i64 << s_log2);
|
||||
let n_layers = 1 + r.below(3) as usize;
|
||||
let big = r.below(8) == 0;
|
||||
let mut layers = Vec::new();
|
||||
for i in 0..n_layers {
|
||||
let out = if i + 1 == n_layers && r.below(16) != 0 { 3 } else { 1 + r.below(4) as usize };
|
||||
let k = 1 + r.below(4) as usize;
|
||||
let stride = 1 + r.below(3) as usize;
|
||||
let pad = if r.below(16) == 0 { r.below(k as u64 + 2) } else { r.below(k as u64) } as usize;
|
||||
let out_pad = if r.below(16) == 0 { r.below(stride as u64 + 2) } else { r.below(stride as u64) } as usize;
|
||||
let shift = r.below(25);
|
||||
let wmax = if big { 1 << 20 } else { [1i64 << 4, 1 << 10, 1 << 14][r.below(3) as usize] };
|
||||
let weight: Vec<i64> = (0..c * out * k * k).map(|_| if r.below(4) == 0 { 0 } else { r.signed(wmax) }).collect();
|
||||
let bias: Vec<i64> = (0..out).map(|_| r.signed(1 << 18)).collect();
|
||||
layers.push(json!({
|
||||
"in_ch": c, "out_ch": out, "k": k, "stride": stride, "pad": pad, "out_pad": out_pad,
|
||||
"weight": weight, "bias": bias, "shift": shift, "relu": r.below(2) == 1,
|
||||
}));
|
||||
side = ((side - 1) * stride as i64 + k as i64 + out_pad as i64 - 2 * pad as i64).max(1);
|
||||
c = out;
|
||||
}
|
||||
let (mut h, mut w) = (side as u32, side as u32);
|
||||
if r.below(8) == 0 {
|
||||
h += 1 + r.below(2) as u32;
|
||||
w = w.saturating_sub(r.below(2) as u32);
|
||||
}
|
||||
let mut p = Vec::new();
|
||||
p.extend(h.to_le_bytes());
|
||||
p.extend(w.to_le_bytes());
|
||||
p.extend((c0 as u16).to_le_bytes());
|
||||
p.extend([s_log2, if r.below(32) == 0 { 1 + r.below(255) as u8 } else { 0 }]);
|
||||
let n = c0 << (2 * s_log2);
|
||||
p.extend((0..n).map(|_| match r.below(8) {
|
||||
0 => 0x80,
|
||||
1 => 0x7F,
|
||||
_ => r.signed(127) as i8 as u8,
|
||||
}));
|
||||
(serde_json::to_vec(&json!({ "layers": layers })).unwrap(), p)
|
||||
}
|
||||
|
||||
/// The direct decode, a panic (an overflow-checked build's arithmetic overflow) caught.
|
||||
fn direct(payload: &[u8], set: &[u8]) -> Option<Result<IntImage, IntSynthError>> {
|
||||
catch_unwind(|| legacy::decode_from_spec(payload, set)).ok()
|
||||
}
|
||||
|
||||
/// Gate 4 of the migration: the decode through the operator set against the direct
|
||||
/// decode on random sets and payloads. Outside the planned breaks both decode to the
|
||||
/// same bytes or both refuse (one code, `payload_malformed`), and the direct decode never
|
||||
/// panics; under a planned break the decode through the operator set refuses.
|
||||
#[test]
|
||||
fn int_synth_random_sets_and_payloads_decode_alike() {
|
||||
let (mut alike, mut refused, mut breaks) = (0u64, 0u64, std::collections::BTreeMap::<&str, u64>::new());
|
||||
let n = cases("int_synth", 3000, |r, seed| {
|
||||
let (set, pay) = int_synth_case(r);
|
||||
let new = decode_from_spec(&pay, &set);
|
||||
let why = || format!("seed {seed}\nset {}\npayload {:02x?}", String::from_utf8_lossy(&set), pay);
|
||||
match int_synth_planned_break(&pay, &set) {
|
||||
Some("shift 64 to 127") => *breaks.entry("shift 64 to 127").or_default() += 1,
|
||||
Some(b) => {
|
||||
assert!(new.is_err(), "{b}: the decode through the operator set must refuse it\n{}", why());
|
||||
*breaks.entry(b).or_default() += 1;
|
||||
}
|
||||
None => match (&new, direct(&pay, &set)) {
|
||||
(Ok(a), Some(Ok(b))) => {
|
||||
assert!(*a == b, "the two decodes differ\n{}", why());
|
||||
alike += 1;
|
||||
}
|
||||
(Err(_), Some(Err(_))) => refused += 1,
|
||||
(_, old) => panic!("new {new:?}, direct {old:?}\n{}", why()),
|
||||
},
|
||||
}
|
||||
});
|
||||
eprintln!("int_synth: {alike} decoded alike, {refused} refused by both, planned breaks {breaks:?}");
|
||||
assert!(alike * 2 > n, "the generator must mostly produce decodable cases: {alike} of {n}");
|
||||
assert!(n < 1000 || breaks.get("a certificate that does not meet acc64").is_some_and(|&b| b > 0), "no case reached a failing certificate");
|
||||
}
|
||||
|
||||
/// One layer of `int_synth.json`: every weight 1, the biases 10, 20 and 30 (or, for
|
||||
/// fewer channels, their first `out_ch`).
|
||||
fn layer(in_ch: u64, out_ch: u64, k: u64, stride: u64, pad: u64, out_pad: u64, shift: u64) -> Value {
|
||||
let bias: Vec<i64> = [10, 20, 30].into_iter().chain(std::iter::repeat(0)).take(out_ch as usize).collect();
|
||||
json!({
|
||||
"in_ch": in_ch, "out_ch": out_ch, "k": k, "stride": stride, "pad": pad, "out_pad": out_pad,
|
||||
"weight": vec![1; (in_ch * out_ch * k * k) as usize], "bias": bias, "shift": shift,
|
||||
})
|
||||
}
|
||||
|
||||
fn set(layers: Vec<Value>) -> Vec<u8> {
|
||||
serde_json::to_vec(&json!({ "layers": layers })).unwrap()
|
||||
}
|
||||
|
||||
/// A payload for an `h`×`h` output: `C` channels of side 1, reserved byte `reserved`, the
|
||||
/// latents `lat`.
|
||||
fn payload(h: u32, c: u16, reserved: u8, lat: &[i8]) -> Vec<u8> {
|
||||
let mut p = Vec::new();
|
||||
p.extend(h.to_le_bytes());
|
||||
p.extend(h.to_le_bytes());
|
||||
p.extend(c.to_le_bytes());
|
||||
p.extend([0u8, reserved]);
|
||||
p.extend(lat.iter().map(|&v| v as u8));
|
||||
p
|
||||
}
|
||||
|
||||
/// The planned breaks of integer-operators §15.3 item 1, each on a payload and set the
|
||||
/// direct decode decodes (or, for a shift of 64 or more, decodes by build): the
|
||||
/// classifier names it, and the decode through the operator set refuses it, or, for a
|
||||
/// shift from 64 to 127, gives the shift its exact meaning.
|
||||
#[test]
|
||||
fn int_synth_planned_breaks_are_new_refusals() {
|
||||
let one = set(vec![layer(1, 3, 1, 1, 0, 0, 0)]);
|
||||
let five = payload(1, 1, 0, &[5]);
|
||||
let expect = |b: &str, pay: &[u8], s: &[u8], direct_decodes: bool| {
|
||||
assert_eq!(int_synth_planned_break(pay, s), Some(b), "{b}: the classifier");
|
||||
if direct_decodes {
|
||||
assert!(matches!(direct(pay, s), Some(Ok(_))), "{b}: the direct decode decodes it");
|
||||
}
|
||||
assert!(decode_from_spec(pay, s).is_err(), "{b}: the decode through the operator set refuses it");
|
||||
};
|
||||
// The base decodes alike: 5·1 + the bias.
|
||||
assert_eq!(int_synth_planned_break(&five, &one), None);
|
||||
assert_eq!(decode_from_spec(&five, &one).unwrap().rgb, [15, 25, 35]);
|
||||
assert_eq!(direct(&five, &one), Some(decode_from_spec(&five, &one)));
|
||||
expect("a reserved byte that is not 0", &payload(1, 1, 7, &[5]), &one, true);
|
||||
// C of 0: no latents; the set's one layer reads no channel, so the direct decode's
|
||||
// output is the bias.
|
||||
expect("C of 0", &payload(1, 0, 0, &[]), &set(vec![layer(0, 3, 1, 1, 0, 0, 0)]), true);
|
||||
// No layers: the direct decode presents the latents themselves, clamped, as the image.
|
||||
expect("no layers", &payload(1, 3, 0, &[5, -3, 100]), &set(vec![]), true);
|
||||
// A layer of no channels: the next reads none, so the output is its bias.
|
||||
expect("out_ch of 0", &five, &set(vec![layer(1, 0, 1, 1, 0, 0, 0), layer(0, 3, 1, 1, 0, 0, 0)]), true);
|
||||
expect("k or stride of 0", &five, &set(vec![layer(1, 3, 0, 1, 0, 1, 0)]), true);
|
||||
expect("k or stride of 0", &five, &set(vec![layer(1, 3, 1, 0, 0, 0, 0)]), true);
|
||||
expect("a field past 32 bits", &five, &set(vec![layer(1, 3, 1, 1 << 32, 0, 0, 0)]), true);
|
||||
// An uncropped output of 3·8193·8193 cropped to 1×1 that no tap reaches: the direct
|
||||
// decode forms no uncropped plane, and its output is the bias.
|
||||
expect("an uncropped output past 2^26", &five, &set(vec![layer(1, 3, 3, 1, 1 << 12, (1 << 13) - 2, 0)]), true);
|
||||
// The certificate: the falsification set that passes i64 by one unit decodes a
|
||||
// small latent directly.
|
||||
let overflow = std::fs::read(wai().join("ops-conformance/sets/overflow_acc64/int_synth.json")).unwrap();
|
||||
expect("a certificate that does not meet acc64", &payload(1, 1, 0, &[1]), &overflow, true);
|
||||
// A shift of 128 or more; the direct decode panics or masks, by build.
|
||||
expect("a shift of 128 or more", &five, &set(vec![layer(1, 3, 1, 1, 0, 0, 128)]), false);
|
||||
// A shift from 64 to 127 has its exact meaning: every sum here is below 2^64.
|
||||
let s64 = set(vec![layer(1, 3, 1, 1, 0, 0, 64)]);
|
||||
assert_eq!(int_synth_planned_break(&five, &s64), Some("shift 64 to 127"));
|
||||
assert_eq!(decode_from_spec(&five, &s64).unwrap().rgb, [0, 0, 0]);
|
||||
}
|
||||
384
wai/wai-rs/tests/int_synth_ops.rs
Normal file
384
wai/wai-rs/tests/int_synth_ops.rs
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
//! `wai.neural.int_synth` through the integer operator set: its lowering `int_synth/1`
|
||||
//! (`extensions/integer-operators.md` §13.1) and the certificate of every committed
|
||||
//! set (§8.5, §14.3), as this crate loads them (`int_ops`).
|
||||
//!
|
||||
//! The lowered graphs and certificates are committed under `ops-conformance/certificates/`,
|
||||
//! where `wai-verify` and the independent checker (`tools/wai_ops_check.py`) each
|
||||
//! reproduce them byte for byte (`wai-ops.yml`). Here the lowering this crate runs must
|
||||
//! write those same bytes, and the load a sink makes must give the verdict the
|
||||
//! certificate records.
|
||||
//!
|
||||
//! The decode through the lowering (`int_synth::decode_from_spec`, the one a sink runs) must give, on
|
||||
//! every committed vector, the bytes of the direct decode it replaced
|
||||
//! (`int_synth::legacy`, behind the test-only feature `legacy_int`, which this file
|
||||
//! needs) and the vector's committed digests. Every refusal of the direct decode is a
|
||||
//! refusal of the decode through the lowering, with the same code: integer-payloads §13
|
||||
//! gives one, `payload_malformed`.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::Value;
|
||||
use wai_ops::interp::Tensor;
|
||||
use wai_ops::{Code, Profile, Site};
|
||||
|
||||
const CAP: &str = "wai.neural.int_synth";
|
||||
|
||||
fn wai() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
||||
}
|
||||
|
||||
fn read(rel: &str) -> Vec<u8> {
|
||||
std::fs::read(wai().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}"))
|
||||
}
|
||||
|
||||
fn json(rel: &str) -> Value {
|
||||
serde_json::from_slice(&read(rel)).unwrap_or_else(|e| panic!("{rel}: {e}"))
|
||||
}
|
||||
|
||||
fn sha256(b: &[u8]) -> String {
|
||||
wai_ops::sha256::sha256(b).iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
/// The `int_synth` sets an index of `ops-conformance/certificates/` lists:
|
||||
/// `(name, set file, set digest, lowered graph file)`, each relative to `wai/`.
|
||||
fn indexed(index: &str) -> Vec<(String, String, String, String)> {
|
||||
json(index)["sets"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|s| s["capability"] == CAP)
|
||||
.map(|s| {
|
||||
assert_eq!(s["lowering"], "int_synth/1");
|
||||
let roles = s["roles"].as_array().unwrap();
|
||||
assert_eq!(roles.len(), 1, "int_synth/1 lowers to one graph, synth");
|
||||
(
|
||||
s["name"].as_str().unwrap().to_string(),
|
||||
s["members"]["int_synth.json"].as_str().unwrap().to_string(),
|
||||
s["set_sha256"].as_str().unwrap().to_string(),
|
||||
roles[0].as_str().unwrap().to_string(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The certificate of the set with digest `d`.
|
||||
fn certificate(d: &str) -> Value {
|
||||
json(&format!("ops-conformance/certificates/{CAP}/{d}.json"))
|
||||
}
|
||||
|
||||
/// Gate 1 of the migration: every committed `int_synth` set lowers to its committed
|
||||
/// graph, the certificate beside it is that graph's, and loading the graph under the
|
||||
/// capability's profile gives the certificate's verdict. The neural-conformance corpus
|
||||
/// meets `acc64`; facade, gradient and bloom meet `acc32` too, and photo does not.
|
||||
#[test]
|
||||
fn every_committed_set_lowers_to_its_committed_graph_and_verdict() {
|
||||
let sets = indexed("ops-conformance/certificates/index.json");
|
||||
let mut names: Vec<&str> = sets.iter().map(|s| s.0.as_str()).collect();
|
||||
names.sort();
|
||||
assert_eq!(names, ["bloom", "facade", "gradient", "photo", "synth"], "the committed int_synth sets");
|
||||
for (name, set, digest, role) in &sets {
|
||||
let bytes = read(set);
|
||||
let lowered = wai_ops::lower::int_synth(&bytes).unwrap_or_else(|r| panic!("{name}: does not lower: {r:?}"));
|
||||
let roles: Vec<&str> = lowered.roles.iter().map(|(r, _)| *r).collect();
|
||||
assert_eq!(roles, ["synth"], "{name}");
|
||||
let g = lowered.get("synth").unwrap();
|
||||
// The verdict first: the load a sink makes, then the profile each set meets.
|
||||
let loaded = wai::int_ops::load(g).unwrap_or_else(|r| panic!("{name}: refused under acc64: {r:?}"));
|
||||
let acc32 = matches!(name.as_str(), "facade" | "gradient" | "bloom" | "synth");
|
||||
assert_eq!(loaded.meets(Profile::Acc32), acc32, "{name}: whether it meets acc32");
|
||||
// Then the committed bytes: the set, its lowered graph and its certificate.
|
||||
assert_eq!(&sha256(&bytes), digest, "{name}: the set's digest");
|
||||
let graph = g.to_bytes();
|
||||
assert!(graph == read(role), "{name}: the lowering differs from the committed {role}");
|
||||
let cert = certificate(digest);
|
||||
assert_eq!(cert["set_sha256"].as_str(), Some(digest.as_str()), "{name}");
|
||||
assert_eq!(cert["graphs"][0]["name"], "synth", "{name}");
|
||||
assert_eq!(cert["graphs"][0]["graph_sha256"].as_str(), Some(sha256(&graph).as_str()), "{name}: the certificate is the graph's");
|
||||
assert_eq!(cert["verdict"]["acc64"], true, "{name}");
|
||||
assert_eq!(Value::Bool(loaded.meets(Profile::Acc32)), cert["verdict"]["acc32"], "{name}: the certificate's verdict on acc32");
|
||||
}
|
||||
// The browser demo serves byte copies of the corpus's sets.
|
||||
for name in ["bloom", "facade", "gradient", "photo"] {
|
||||
let copy = sha256(&read(&format!("wai-web/demo/int_synth/{name}/int_synth.json")));
|
||||
assert!(sets.iter().any(|s| s.2 == copy), "wai-web/demo/int_synth/{name}: not a committed set");
|
||||
}
|
||||
}
|
||||
|
||||
/// The two falsification sets of §14.3. `overflow_acc64`'s accumulator passes i64 by
|
||||
/// one unit, so the capability's load refuses it at load step 8; `claim_acc32`'s
|
||||
/// passes i32 by one unit, so it loads under `acc64` and does not meet `acc32`.
|
||||
#[test]
|
||||
fn the_falsification_sets_fail_the_profile_they_claim() {
|
||||
let sets = indexed("ops-conformance/certificates/falsification.json");
|
||||
let mut names: Vec<&str> = sets.iter().map(|s| s.0.as_str()).collect();
|
||||
names.sort();
|
||||
assert_eq!(names, ["claim_acc32", "overflow_acc64"]);
|
||||
for (name, set, digest, role) in &sets {
|
||||
let bytes = read(set);
|
||||
assert_eq!(&sha256(&bytes), digest, "{name}");
|
||||
let lowered = wai_ops::lower::int_synth(&bytes).unwrap();
|
||||
let g = lowered.get("synth").unwrap();
|
||||
assert!(g.to_bytes() == read(role), "{name}: the lowering differs from the committed {role}");
|
||||
let cert = certificate(digest);
|
||||
match name.as_str() {
|
||||
"overflow_acc64" => {
|
||||
assert_eq!(cert["verdict"]["acc64"], false);
|
||||
let r = wai::int_ops::load(g).expect_err("the overflow set is refused");
|
||||
assert_eq!((r.code, r.at), (Code::ModelMismatch, Site::Load(8)));
|
||||
}
|
||||
_ => {
|
||||
assert_eq!((cert["verdict"]["acc64"].as_bool(), cert["verdict"]["acc32"].as_bool()), (Some(true), Some(false)));
|
||||
assert!(!wai::int_ops::load(g).expect("acc64").meets(Profile::Acc32));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gate 2 of the migration: golden equality. Every committed `int_synth` vector decodes
|
||||
/// through the lowering to the bytes the direct decode gives, and to every digest the
|
||||
/// corpus commits for it: the SHA-256 sidecar, the BLAKE3 `rgb_hash` with its sizes,
|
||||
/// and photo's reference RGB from the Python integer forward. The browser demo serves
|
||||
/// byte copies of the corpus's payloads and digests.
|
||||
#[test]
|
||||
fn every_committed_vector_decodes_through_the_lowering_to_its_golden() {
|
||||
let hashes = json("neural-conformance/expected_hashes.json");
|
||||
let sidecar = json("neural-conformance/sha256.json");
|
||||
assert!(read("wai-web/demo/int_synth/expected_hashes.json") == read("neural-conformance/expected_hashes.json"));
|
||||
let mut n = 0;
|
||||
for (name, want) in hashes.as_object().unwrap() {
|
||||
let dir = format!("neural-conformance/vectors/{name}");
|
||||
let (payload, set) = (read(&format!("{dir}/latent.bin")), read(&format!("{dir}/int_synth.json")));
|
||||
assert!(payload == read(&format!("wai-web/demo/int_synth/{name}/latent.bin")), "{name}: the demo's copy");
|
||||
let ops = wai::int_synth::decode_from_spec(&payload, &set).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let direct = wai::int_synth::legacy::decode_from_spec(&payload, &set).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert!(ops == direct, "{name}: the decode through the lowering differs from the direct decode");
|
||||
assert_eq!((ops.width as u64, ops.height as u64), (want["width"].as_u64().unwrap(), want["height"].as_u64().unwrap()), "{name}");
|
||||
assert_eq!(sha256(&ops.rgb), sidecar[name]["sha256"].as_str().unwrap(), "{name}: the SHA-256 sidecar");
|
||||
let mut h = blake3::Hasher::new();
|
||||
h.update(b"wai:int-synth-rgb\x01");
|
||||
h.update(&ops.rgb);
|
||||
assert_eq!(h.finalize().to_hex().as_str(), want["rgb_hash"].as_str().unwrap(), "{name}: rgb_hash");
|
||||
if let Ok(r) = std::fs::read(wai().join(format!("{dir}/ref_rgb.bin"))) {
|
||||
assert!(ops.rgb == r, "{name}: the Python integer-forward reference");
|
||||
}
|
||||
n += 1;
|
||||
}
|
||||
assert_eq!(n, 4, "the four committed int_synth vectors");
|
||||
}
|
||||
|
||||
/// A payload of `C` channels at side `2^s_log2` for an `H`×`W` output: the latents are
|
||||
/// `fill`, and `extra` bytes are added (negative: removed) at the end.
|
||||
fn payload(h: u32, w: u32, c: u16, s_log2: u8, fill: u8, extra: i64) -> Vec<u8> {
|
||||
let mut p = Vec::new();
|
||||
p.extend(h.to_le_bytes());
|
||||
p.extend(w.to_le_bytes());
|
||||
p.extend(c.to_le_bytes());
|
||||
p.extend([s_log2, 0]);
|
||||
let n = (c as usize) << (2 * s_log2 as usize);
|
||||
p.resize((12 + n as i64 + extra) as usize, fill);
|
||||
p
|
||||
}
|
||||
|
||||
/// One layer of `int_synth.json`, every weight `w`.
|
||||
fn layer(in_ch: u64, out_ch: u64, k: u64, stride: u64, pad: u64, w: i64) -> Value {
|
||||
serde_json::json!({
|
||||
"in_ch": in_ch, "out_ch": out_ch, "k": k, "stride": stride, "pad": pad,
|
||||
"weight": vec![w; (in_ch * out_ch * k * k) as usize], "bias": vec![1; out_ch as usize], "shift": 1, "relu": true,
|
||||
})
|
||||
}
|
||||
|
||||
fn set(layers: Vec<Value>) -> Vec<u8> {
|
||||
serde_json::to_vec(&serde_json::json!({ "layers": layers })).unwrap()
|
||||
}
|
||||
|
||||
/// The fixtures: one for each refusal the direct decode makes (integer-payloads §13),
|
||||
/// each beside a payload and set it decodes. One layer, 1 → 3 channels, k 2, stride 2,
|
||||
/// on a 1×1 latent: a 2×2 output.
|
||||
fn refusal_fixtures() -> Vec<(&'static str, Vec<u8>, Vec<u8>)> {
|
||||
let good_set = set(vec![layer(1, 3, 2, 2, 0, 3)]);
|
||||
let good = payload(2, 2, 1, 0, 9, 0);
|
||||
let edit = |f: &dyn Fn(&mut Value)| {
|
||||
let mut v: Value = serde_json::from_slice(&good_set).unwrap();
|
||||
f(&mut v);
|
||||
serde_json::to_vec(&v).unwrap()
|
||||
};
|
||||
let p = |what, pay: Vec<u8>| (what, pay, good_set.clone());
|
||||
let q = |what, s: Vec<u8>| (what, good.clone(), s);
|
||||
vec![
|
||||
p("no payload", vec![]),
|
||||
p("a payload shorter than its header", good[..11].to_vec()),
|
||||
p("a latent short", payload(2, 2, 1, 0, 9, -1)),
|
||||
p("a byte past the latents", payload(2, 2, 1, 0, 9, 1)),
|
||||
p("a side of 2^64", { let mut v = good.clone(); v[10] = 64; v }),
|
||||
p("a side of 2^255", { let mut v = good.clone(); v[10] = 255; v }),
|
||||
p("C·S·S past the word", { let mut v = payload(2, 2, 1, 0, 9, 0); v[8..10].copy_from_slice(&u16::MAX.to_le_bytes()); v[10] = 31; v }),
|
||||
p("an output taller than H", payload(1, 2, 1, 0, 9, 0)),
|
||||
p("an output narrower than W", payload(2, 3, 1, 0, 9, 0)),
|
||||
p("H of 0", payload(0, 2, 1, 0, 9, 0)),
|
||||
p("first in_ch is not C", payload(2, 2, 2, 0, 9, 0)),
|
||||
q("a set that is not JSON", b"{".to_vec()),
|
||||
q("a set with no layers member", b"{}".to_vec()),
|
||||
q("layers that is not a list", br#"{"layers":{}}"#.to_vec()),
|
||||
q("a layer that is not an object", br#"{"layers":[1]}"#.to_vec()),
|
||||
q("a layer without k", edit(&|v| { v["layers"][0].as_object_mut().unwrap().remove("k"); })),
|
||||
q("a negative stride", edit(&|v| v["layers"][0]["stride"] = (-2).into())),
|
||||
q("a k with a fraction", edit(&|v| v["layers"][0]["k"] = 2.5.into())),
|
||||
q("a negative shift", edit(&|v| v["layers"][0]["shift"] = (-1).into())),
|
||||
q("a relu that is not a boolean", edit(&|v| v["layers"][0]["relu"] = 1.into())),
|
||||
q("a weight short", edit(&|v| { v["layers"][0]["weight"].as_array_mut().unwrap().pop(); })),
|
||||
q("a bias past out_ch", edit(&|v| v["layers"][0]["bias"].as_array_mut().unwrap().push(0.into()))),
|
||||
q("a weight past i32", edit(&|v| v["layers"][0]["weight"][0] = (1i64 << 31).into())),
|
||||
q("a bias below i32", edit(&|v| v["layers"][0]["bias"][0] = (-(1i64 << 31) - 1).into())),
|
||||
q("layers that do not chain", set(vec![layer(1, 2, 1, 1, 0, 1), layer(3, 3, 2, 2, 0, 1)])),
|
||||
q("four output channels", set(vec![layer(1, 4, 2, 2, 0, 3)])),
|
||||
q("an output side of 0", set(vec![layer(1, 3, 2, 2, 1, 3)])),
|
||||
]
|
||||
}
|
||||
|
||||
/// The code a sink reports for a decode of `wai.neural.int_synth` (`int_payload`, the
|
||||
/// integer decode path the dispatch takes).
|
||||
fn code(payload: &[u8], set: &[u8]) -> Result<(), &'static str> {
|
||||
let params = wai::int_payload::resolve(CAP, None, &[wai::pin::HeldBytes::File(set)], None).expect("an unpinned set resolves");
|
||||
wai::int_payload::decode_int(CAP, payload, ¶ms).map(|_| ()).map_err(|e| e.code())
|
||||
}
|
||||
|
||||
/// Gate 3 of the migration: refusal parity. Each fixture, refused by the direct decode,
|
||||
/// is refused by the decode through the lowering, and a sink reports it as
|
||||
/// `payload_malformed`, the one code integer-payloads §13 gives. Beside them, the
|
||||
/// payload and set they are edited from decode both ways.
|
||||
#[test]
|
||||
fn every_refusal_of_the_direct_decode_is_refused_through_the_lowering() {
|
||||
let good = (payload(2, 2, 1, 0, 9, 0), set(vec![layer(1, 3, 2, 2, 0, 3)]));
|
||||
let a = wai::int_synth::decode_from_spec(&good.0, &good.1).expect("the fixtures' base decodes");
|
||||
assert_eq!(Ok(a.clone()), wai::int_synth::legacy::decode_from_spec(&good.0, &good.1));
|
||||
assert_eq!(a.rgb, [14u8; 12], "(9·3 + 1) >> 1 at every sample");
|
||||
assert_eq!(code(&good.0, &good.1), Ok(()));
|
||||
let fixtures = refusal_fixtures();
|
||||
for (what, pay, s) in &fixtures {
|
||||
let direct = wai::int_synth::legacy::decode_from_spec(pay, s);
|
||||
let ops = wai::int_synth::decode_from_spec(pay, s);
|
||||
assert!(direct.is_err(), "{what}: the direct decode refuses it: {direct:?}");
|
||||
assert!(ops.is_err(), "{what}: the decode through the lowering refuses it: {ops:?}");
|
||||
assert_eq!(code(pay, s), Err("payload_malformed"), "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The lowered graph's one `check` range, its input's `[−128, 127]` (§13.0's driver
|
||||
/// contract), at its ends and one past them: a payload's latents are signed bytes, so
|
||||
/// only a graph-level decode reaches past them, and the graph refuses it (`field_range`,
|
||||
/// at input 0).
|
||||
#[test]
|
||||
fn the_latent_range_refuses_one_past_each_end() {
|
||||
let lowered = wai_ops::lower::int_synth(&read("neural-conformance/vectors/gradient/int_synth.json")).unwrap();
|
||||
let g = wai::int_ops::load(lowered.get("synth").unwrap()).unwrap();
|
||||
let (c, s) = (4u32, 8u32);
|
||||
let x = |v: i64| {
|
||||
let mut d = vec![0i64; (c * s * s) as usize];
|
||||
d[37] = v;
|
||||
Tensor::new(c, s, s, d)
|
||||
};
|
||||
for v in [-128, 127] {
|
||||
assert!(wai::int_ops::run1(&g, x(v), &[]).is_ok(), "{v} is a latent");
|
||||
}
|
||||
for v in [-129, 128] {
|
||||
let r = wai::int_ops::run1(&g, x(v), &[]).expect_err("one past the latent range");
|
||||
assert_eq!((r.code, r.at), (Code::FieldRange, Site::Input(0)), "{v}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Gate 5 of the migration, the whole-decoder half: the decode through the lowering
|
||||
/// under the reference kernels (the interpreter computes every operator) and under each
|
||||
/// fast policy (the transposed convolutions on this crate's fast kernels, serial and
|
||||
/// threaded) gives the same bytes on every committed vector, and on the fixtures' base
|
||||
/// case. The per-operator half is `int_ops`' equivalence tests.
|
||||
#[test]
|
||||
fn every_kernel_policy_decodes_through_the_lowering_alike() {
|
||||
use wai::int_kernels::{with_policy, Policy};
|
||||
const POLICIES: [Policy; 5] = [Policy::reference(), Policy::fast(1), Policy::fast(2), Policy::fast(5), Policy::fast(0)];
|
||||
let mut cases: Vec<(String, Vec<u8>, Vec<u8>)> = json("neural-conformance/expected_hashes.json")
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.map(|name| {
|
||||
let dir = format!("neural-conformance/vectors/{name}");
|
||||
(name.clone(), read(&format!("{dir}/latent.bin")), read(&format!("{dir}/int_synth.json")))
|
||||
})
|
||||
.collect();
|
||||
cases.push(("the fixtures' base".into(), payload(2, 2, 1, 0, 9, 0), set(vec![layer(1, 3, 2, 2, 0, 3)])));
|
||||
for (name, p, s) in &cases {
|
||||
let want = with_policy(POLICIES[0], || wai::int_synth::decode_from_spec(p, s)).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
for pol in &POLICIES[1..] {
|
||||
let got = with_policy(*pol, || wai::int_synth::decode_from_spec(p, s));
|
||||
assert!(got.as_ref() == Ok(&want), "{name}: {pol:?} differs from the interpreter alone");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The committed `int_synth` sets and payloads: every directory under
|
||||
/// `neural-conformance/vectors/`, the browser demo's `wai-web/demo/int_synth/` and
|
||||
/// `ops-conformance/sets/` that holds an `int_synth.json`, with its `latent.bin` where
|
||||
/// it has one. Relative to `wai/`.
|
||||
fn committed_sets_and_payloads() -> Vec<(String, Option<String>)> {
|
||||
let mut v = Vec::new();
|
||||
for root in ["neural-conformance/vectors", "wai-web/demo/int_synth", "ops-conformance/sets"] {
|
||||
let mut dirs: Vec<String> = std::fs::read_dir(wai().join(root))
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().join("int_synth.json").exists())
|
||||
.map(|e| format!("{root}/{}", e.file_name().to_string_lossy()))
|
||||
.collect();
|
||||
dirs.sort();
|
||||
for d in dirs {
|
||||
let p = format!("{d}/latent.bin");
|
||||
let payload = wai().join(&p).exists().then_some(p);
|
||||
v.push((format!("{d}/int_synth.json"), payload));
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// O12: the load rules the switch brings (integer-operators §15.3 item 1, and the two
|
||||
/// the lowering makes that its text now states) affect no committed set or payload.
|
||||
/// Every committed set has layers, each with `out_ch`, `k` and `stride` at least 1,
|
||||
/// fields that fit 32 bits and a shift of at most 63 (so none from 64 to 127 changes
|
||||
/// meaning either), and lowers and loads under `acc64`; the one exception is
|
||||
/// `overflow_acc64`, the falsification set made to fail its certificate. Every
|
||||
/// committed payload has a reserved byte of 0, `C ≥ 1` and `C·S·S ≤ 2^26`, is exactly
|
||||
/// its length, and decodes through the lowering against its set.
|
||||
#[test]
|
||||
fn the_new_load_rules_affect_no_committed_set_or_payload() {
|
||||
let all = committed_sets_and_payloads();
|
||||
let (mut sets, mut payloads) = (0, 0);
|
||||
for (set_path, payload_path) in &all {
|
||||
let bytes = read(set_path);
|
||||
let v: Value = serde_json::from_slice(&bytes).unwrap();
|
||||
let layers = v["layers"].as_array().unwrap();
|
||||
assert!(!layers.is_empty(), "{set_path}: no layers");
|
||||
for (i, l) in layers.iter().enumerate() {
|
||||
let f = |k: &str, d: u64| l.get(k).map_or(Some(d), Value::as_u64).unwrap_or_else(|| panic!("{set_path} layer {i}: {k}"));
|
||||
for k in ["in_ch", "out_ch", "k", "stride", "pad"] {
|
||||
assert!(f(k, u64::MAX) <= u32::MAX as u64, "{set_path} layer {i}: {k} past 32 bits");
|
||||
}
|
||||
assert!(f("out_pad", 0) <= u32::MAX as u64, "{set_path} layer {i}: out_pad");
|
||||
assert!(f("out_ch", 0) >= 1 && f("k", 0) >= 1 && f("stride", 0) >= 1, "{set_path} layer {i}: out_ch, k or stride of 0");
|
||||
assert!(f("shift", 0) <= 63, "{set_path} layer {i}: a shift of 64 or more");
|
||||
}
|
||||
let lowered = wai_ops::lower::int_synth(&bytes).unwrap_or_else(|r| panic!("{set_path}: does not lower: {r:?}"));
|
||||
let loaded = wai::int_ops::load(lowered.get("synth").unwrap());
|
||||
if set_path.contains("/overflow_acc64/") {
|
||||
assert!(loaded.is_err(), "{set_path}: the falsification set fails acc64");
|
||||
} else {
|
||||
assert!(loaded.is_ok(), "{set_path}: does not load under acc64: {loaded:?}");
|
||||
}
|
||||
sets += 1;
|
||||
let Some(p) = payload_path else { continue };
|
||||
let b = read(p);
|
||||
let c = u16::from_le_bytes([b[8], b[9]]) as u64;
|
||||
let n = c << (2 * b[10] as u64);
|
||||
assert_eq!(b[11], 0, "{p}: the reserved byte");
|
||||
assert!(c >= 1 && n <= 1 << 26 && b.len() as u64 == 12 + n, "{p}: C, the cap and the length");
|
||||
assert!(wai::int_synth::decode_from_spec(&b, &bytes).is_ok(), "{p}: decodes through the lowering");
|
||||
payloads += 1;
|
||||
}
|
||||
assert!(sets >= 11 && payloads >= 8, "the committed int_synth sets and payloads, at least those of this revision: {all:?}");
|
||||
}
|
||||
5
wai/wai-web/Cargo.lock
generated
5
wai/wai-web/Cargo.lock
generated
|
|
@ -1221,9 +1221,14 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"wai-ops",
|
||||
"wai-quantum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wai-ops"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.4.3"
|
||||
|
|
|
|||
Loading…
Reference in a new issue