D2c M6: wai.world.model's integer renderer renders through wai-ops/1 (world/1), goldens unchanged; run_net and the convolution dispatch leave production #274
30 changed files with 2116 additions and 714 deletions
10
.github/workflows/wai-conformance.yml
vendored
10
.github/workflows/wai-conformance.yml
vendored
|
|
@ -256,7 +256,9 @@ jobs:
|
|||
# wai.audio.int_codec and its lowering wam1/1 (§13.3), and tests/int_hyper_ops.rs
|
||||
# for wai.neural.int_hyper and the keyframe of wai.video.int_hyper (whm1/1: h_s,
|
||||
# then g_s with the RGB8 step), and tests/int_mlicpp_ops.rs for wai.neural.int_mlicpp
|
||||
# (wmb1/1: h_s, entropy, then g_s with the RGB8 step).
|
||||
# (wmb1/1: h_s, entropy, then g_s with the RGB8 step), and tests/int_world_ops.rs for
|
||||
# wai.world.model's integer renderer (world/1, informative: T with its action planes,
|
||||
# then g_s with the RGB8 step).
|
||||
# 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
|
||||
|
|
@ -281,6 +283,8 @@ jobs:
|
|||
# the decodes: a second decode through a set is a hit with the same bytes on every
|
||||
# committed vector, sets one byte apart never share an entry, a refused set stays
|
||||
# refused, refusals keep their codes, and the least recently used set goes first;
|
||||
# tests/int_world_memory.rs (its own binary, the same allocator) checks that the world
|
||||
# renderer, with its set loaded, holds no more than the direct renderer it replaced.
|
||||
# tests/int_video_enc_memory.rs
|
||||
# (its own binary, the same allocator) checks that int_video_enc::working_bytes bounds
|
||||
# what the encoder holds. tests/staged_resources.rs (its
|
||||
|
|
@ -320,10 +324,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,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_mlicpp_ops --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_set_cache --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_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_mlicpp_ops --test int_world_ops --test int_world_memory --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_set_cache --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,legacy_int --lib --test int_kernels_vectors --test int_kernels_geometry --test int_synth_ops --test int_ops_differential --test int_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_mlicpp_ops --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_set_cache --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_splat_ops --test int_audio_ops --test int_hyper_ops --test int_mlicpp --test int_mlicpp_preflight --test int_mlicpp_ops --test int_world_ops --test int_world_memory --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_set_cache --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
|
||||
|
|
|
|||
5
.github/workflows/wai-ops-differential.yml
vendored
5
.github/workflows/wai-ops-differential.yml
vendored
|
|
@ -8,7 +8,8 @@ name: wai-ops differential (nightly)
|
|||
# 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,
|
||||
# wai.splat.int_codec, wai.audio.int_codec, wai.neural.int_hyper (whose decode is
|
||||
# also the keyframe of wai.video.int_hyper) and wai.neural.int_mlicpp.
|
||||
# also the keyframe of wai.video.int_hyper), wai.neural.int_mlicpp, and wai.world.model's
|
||||
# integer renderer, against its direct renderer with the direct guard.
|
||||
#
|
||||
# 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
|
||||
|
|
@ -62,4 +63,4 @@ jobs:
|
|||
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,splat_int,audio_int --test int_ops_differential -- --nocapture
|
||||
cargo test $P --locked --no-default-features --features legacy_int,splat_int,audio_int,world_int --test int_ops_differential -- --nocapture
|
||||
|
|
|
|||
10
.github/workflows/wai-web-wasm.yml
vendored
10
.github/workflows/wai-web-wasm.yml
vendored
|
|
@ -18,6 +18,9 @@ name: wai-web wasm32
|
|||
# - demo/verify_pack.mjs: the browser writers `pack` and `packMulti` refuse
|
||||
# every candidate the export lists, in every place an envelope can name one,
|
||||
# and a manifest that names a member twice in one object;
|
||||
# - demo/verify_int_world_wasm.mjs against wai/wai-rs/tests/vectors/int_world{,2}/:
|
||||
# the WorldModel binding, wai.world.model's integer renderer, must render every
|
||||
# frame of both committed world vectors to the SHA-256 their sidecars state;
|
||||
# - demo/verify_staged_wasm.mjs against the staged-delivery corpus
|
||||
# wai/staged-conformance/: every case replayed through the wasm32 StageSink
|
||||
# must give the reports, final units and stage hashes the native sink wrote;
|
||||
|
|
@ -196,6 +199,13 @@ jobs:
|
|||
shell: bash
|
||||
working-directory: wai/wai-web
|
||||
run: node demo/verify_int_payload_wasm.mjs ./pkg-node/wai_web.js ../wai-rs/tests/vectors
|
||||
# WorldModel, wai.world.model's integer renderer (integer-operators §13.5,
|
||||
# informative), renders each committed world vector, one agent and two, frame by
|
||||
# frame, to the SHA-256 its sidecar states for every frame and for the clip.
|
||||
- name: wasm32 integer world renderer equals the native sidecars
|
||||
shell: bash
|
||||
working-directory: wai/wai-web
|
||||
run: node demo/verify_int_world_wasm.mjs ./pkg-node/wai_web.js ../wai-rs/tests/vectors
|
||||
# The generator and the score synth in wasm32 against the native pins: every size
|
||||
# sizes.json and instr-v0.json pin (the object's four listed sizes, and the first
|
||||
# listed size of each object at a bound of form v0), and every score vector's
|
||||
|
|
|
|||
|
|
@ -6,6 +6,42 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### `wai.world.model`'s integer renderer through the integer operator set
|
||||
|
||||
- **The lowering, from the models the renderer holds.** `wai-ops` gains
|
||||
`lower::world_net` and `lower::world_net_loaded`: `world/1`
|
||||
([integer-operators](extensions/integer-operators.md) §13.5, informative) of two
|
||||
networks already parsed, the second loading `T` before it lowers `g_s`. Both write
|
||||
and refuse what `lower::world` does from the bytes; the committed sets' graphs and
|
||||
certificates are unchanged.
|
||||
- **No `ops` member.** The renderer runs through the operator set, but its lowering is
|
||||
informative: the capability's registered payload is its signed action trace, and no
|
||||
payload text yet binds a trace to a renderer's set. SPEC §5's row cites §13.5, and the
|
||||
registry's test holds that `wai.world.model` has no `ops` member.
|
||||
- **The direct guard, test-only.** `int_world::legacy::checked` runs the direct renderer
|
||||
with the check every other integer decoder made before each layer
|
||||
(`run_net_checked`); the tests hold the `check` ranges of `world/1` to it, layer by
|
||||
layer, at the edge of every range and through the whole renderer.
|
||||
- **The switch.** `int_world::{decode, decode_deltas, decode_planes}`, and the browser's
|
||||
`WorldModel`, now render through `world/1`: `T` and `g_s` lowered and loaded under
|
||||
`acc64` once (`int_world::load`, an `int_world::World`), then planned on the latent's
|
||||
shape, then run frame by frame. Every committed frame and clip digest is unchanged, on
|
||||
every leg and in wasm32. Each now returns a `Result`; `WorldModel`'s methods throw on a
|
||||
refusal. The per-row `step`, `step_delta` and `step_planes` of a parsed model are
|
||||
replaced by `World::step`, `World::step_delta`, `World::step_action` and
|
||||
`World::render`. The direct renderer is `int_world::legacy`, test-only (`legacy_int`).
|
||||
- **Planned breaks** (integer-operators §15.3 item 2, SPEC §8). Before the first frame,
|
||||
where the direct renderer rendered from the input or panicked: a set whose `afrac` is
|
||||
outside `1..=30` or unequal, with no action plane, or that breaks a `WHM1` network's
|
||||
rules (`model_mismatch`); a row without one value per action plane, and `T` or `g_s`
|
||||
that does not fit the latent (`model_mismatch`); a latent that is not `C·lat·lat`
|
||||
values (`geometry_mismatch`), of side 0 or past `2^26` values, and a move whose plane
|
||||
value leaves `i64`. A value outside a `check` range is refused (`field_range`). No
|
||||
committed set or clip is affected.
|
||||
- **Memory.** With its set loaded, a clip through the operator set holds no more than the
|
||||
direct renderer held for it, and so do one step and one frame
|
||||
(`tests/int_world_memory.rs`).
|
||||
|
||||
### `wai.neural.int_mlicpp` through the integer operator set
|
||||
|
||||
- **Its `ops` member in the registry** ([integer-operators](extensions/integer-operators.md)
|
||||
|
|
|
|||
11
wai/SPEC.md
11
wai/SPEC.md
|
|
@ -806,7 +806,7 @@ standard. See the linked extension for each.
|
|||
| capability | payload | conformance · extension |
|
||||
|---|---|---|
|
||||
| `wai.world.scene` / `wai.world.replay` / `wai.world.live` | WWLD container (op log) | replay-equivalence · [interactive-worlds](extensions/interactive-worlds.md) |
|
||||
| `wai.world.model` | generative-world transport: a signed **action trace** (the world CausalOp log) + a declared `model_id` (`world_model.rs`) | the wire carries the *action trace*, not the pixels — it replays under canonical total order to one **session hash** on every machine (replay-equivalence), the generated frames vouched for by a signed, joules-accounted provenance receipt. The neural sibling of `.replay`; with an `int_*` renderer the frames hash-converge too (a generative interactive world you can put in a receipt) |
|
||||
| `wai.world.model` | generative-world transport: a signed **action trace** (the world CausalOp log) + a declared `model_id` (`world_model.rs`) | the wire carries the *action trace*, not the pixels — it replays under canonical total order to one **session hash** on every machine (replay-equivalence), the generated frames vouched for by a signed, joules-accounted provenance receipt. The neural sibling of `.replay`; with an `int_*` renderer the frames hash-converge too (a generative interactive world you can put in a receipt). The integer renderer (`int_world.rs`) renders through its lowering, [integer-operators](extensions/integer-operators.md) §13.5 (`world/1`, informative: no payload text yet binds a trace to a renderer's set, so the capability carries no `ops` member) |
|
||||
| `wai.mesh.glb` | glTF 2.0 Binary (Khronos) | asset, content-addressed (decoder sink-supplied) |
|
||||
| `wai.splat.spz` | SPZ (an open-source Gaussian-splat format) | asset, content-addressed; presentation only |
|
||||
| `wai.audio.scene` | WSCN scene (objects + trajectories) | mixdown-equivalence · [spatial-audio](extensions/spatial-audio.md) |
|
||||
|
|
@ -2421,6 +2421,15 @@ envelope that uses any string a later revision registers.
|
|||
more that a layer uses, and a leaky ReLU's shift of 64 or more; where a stream or a
|
||||
value was refused first, the bundle now is. A bundle with a convolution whose padded
|
||||
side exceeds `2^26` is refused.
|
||||
- `wai.world.model`'s integer renderer (informative): what the direct renderer, which
|
||||
checked nothing, rendered from or panicked on is refused before the first frame: a
|
||||
set whose `afrac` is outside `1..=30` or differs between its two networks, with no
|
||||
action plane, or that breaks a `WHM1` network's rules above (`model_mismatch`); a row
|
||||
that does not carry one value per action plane; `T` that does not give the latent's
|
||||
shape back, or `g_s` that does not plan on it (`model_mismatch`); a latent that is
|
||||
not `C·lat·lat` values (`geometry_mismatch`), of side 0, or past `2^26` values; a move
|
||||
whose plane value leaves `i64`. A value outside a `check` range is refused
|
||||
(`field_range`) where the renderer computed past its exact range.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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/`. 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`, `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and `wai.neural.int_mlicpp`. 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.
|
||||
> 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`, `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and `wai.neural.int_mlicpp`. The integer renderer of `wai.world.model` renders through it too, by `world/1` (§13.5), and stays informative. 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.
|
||||
|
||||
|
|
@ -1309,6 +1309,7 @@ Each capability's driver is defined by its payload extension. This table names t
|
|||
- `clamp(−2^afrac, 2^afrac)`, with `T`'s `afrac`. Its output is the graph's.
|
||||
- **The graph `g_s`** is `whm1/1` with the RGB8 step, which uses `g_s`'s `afrac`. The load check makes the two equal.
|
||||
- **The driver.** Frame 0 is `g_s(z_0)`. For each row in order, `z` becomes `T(z)` with the row's entries as the scalars, and the next frame is `g_s(z)`. A single-agent set (`A = 3`) whose rows are moves `(dx, dy)` has the plane values `dequantize(dx, afrac, 0)`, `dequantize(dy, afrac, 0)` and 0. A two-agent set (`A = 4`) has `(dxA, dyA, dxB, dyB)`, each scaled the same way.
|
||||
- **Before the first frame,** after the load checks: `z_0` is `[C, lat, lat]`, and both graphs are planned on that shape (§4). `T` must give the shape back, so that each transition reads what the one before it wrote, and `g_s` must plan on it; a network that does not is refused (`model_mismatch`), as a set that does not fit a payload's shapes is. The reference renderer reports a latent that is not `C·lat·lat` values as `geometry_mismatch`, a side of 0 or a move whose plane value leaves `i64` as `field_range`, and a latent past `2^26` values as `too_large`. These codes are informative until the renderer has payload text.
|
||||
|
||||
### 13.6 The integer stages of `wai.image.jpegai` (`jpegai-int/1`)
|
||||
|
||||
|
|
@ -1541,8 +1542,8 @@ No committed output changes, and no committed refusal fixture changes its code.
|
|||
- 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.
|
||||
3. **`WHM1` and `WAM1` layers.** A layer whose predicate fails at 0 (a bias of magnitude `2^63 − 1` or more) is refused at load, not at decode. So is a set in which a `check` range cannot hold any value its producer can give (§8.1), which today refuses every decode with `field_range`. Each is refused with `model_mismatch`, before any stream is decoded, where the direct decoder refused with `field_range`, or with a stream's code when the stream failed first. A network with no layer of tag 0, 1 or 2, and one whose header `shift` or `gf` is 128 or more where no layer uses it, are refused too (§13.2); the direct decoder decoded each where the shapes allowed. These take effect for `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and `wai.neural.int_mlicpp` in this revision; for `wai.audio.int_codec` the `check` ranges include the PCM step's (§13.3), so a set whose last layer cannot give a sample inside it is refused at load, where every decode was refused at that step. For the conditional codec either network can carry the change, and the set is refused after its plan and its shape checks and before either stream (integer-payloads §7), where the direct decoder refused with the code of whichever stream or network failed first. For `wai.neural.int_mlicpp` any of its three networks can, and the bundle is refused after the header and the networks' geometry are checked and before either stream (integer-payloads §14). Its direct decoder made four more of these checks only when a network ran, after the streams and networks before it: a layer that does not fit the channels the layer before it gives; a network whose input is not the payload's channel count; a header `shift` or `gf` of 128 or more that a layer uses; and a leaky ReLU's shift of 64 or more. It refused each with `model_mismatch` too, so only the order changes: where a stream or a value was refused first, the bundle now is. Its graphs' run-time plan also caps a convolution's padded side at `2^26` (§4, §5.2), which its direct decoder did not; such a bundle is refused (`model_mismatch`).
|
||||
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. Its direct renderer checked nothing: it computed past its exact range, rendered frames from a set or a row that does not fit, or panicked. The renderer through `world/1` also refuses, before the first frame, a set with no action plane (`A ≥ 1`), a row that does not carry `A` values, `T` that does not give the latent's shape back or `g_s` that does not plan on it, and a set that breaks item 3's rules in either network.
|
||||
3. **`WHM1` and `WAM1` layers.** A layer whose predicate fails at 0 (a bias of magnitude `2^63 − 1` or more) is refused at load, not at decode. So is a set in which a `check` range cannot hold any value its producer can give (§8.1), which today refuses every decode with `field_range`. Each is refused with `model_mismatch`, before any stream is decoded, where the direct decoder refused with `field_range`, or with a stream's code when the stream failed first. A network with no layer of tag 0, 1 or 2, and one whose header `shift` or `gf` is 128 or more where no layer uses it, are refused too (§13.2); the direct decoder decoded each where the shapes allowed. These take effect for `wai.splat.int_codec`, `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper`, `wai.neural.int_mlicpp` and the integer renderer of `wai.world.model` in this revision; for `wai.audio.int_codec` the `check` ranges include the PCM step's (§13.3), so a set whose last layer cannot give a sample inside it is refused at load, where every decode was refused at that step. For the conditional codec either network can carry the change, and the set is refused after its plan and its shape checks and before either stream (integer-payloads §7), where the direct decoder refused with the code of whichever stream or network failed first. For `wai.neural.int_mlicpp` any of its three networks can, and the bundle is refused after the header and the networks' geometry are checked and before either stream (integer-payloads §14). Its direct decoder made four more of these checks only when a network ran, after the streams and networks before it: a layer that does not fit the channels the layer before it gives; a network whose input is not the payload's channel count; a header `shift` or `gf` of 128 or more that a layer uses; and a leaky ReLU's shift of 64 or more. It refused each with `model_mismatch` too, so only the order changes: where a stream or a value was refused first, the bundle now is. Its graphs' run-time plan also caps a convolution's padded side at `2^26` (§4, §5.2), which its direct decoder did not; such a bundle is refused (`model_mismatch`).
|
||||
4. **`wai.image.jpegai`.**
|
||||
- Its exact decoder refuses a malformed latent or hyper-latent stream (`stream_exhausted`) where it panics today.
|
||||
- It refuses a bundle that fails §13.6.1's load checks (`model_mismatch`), including a bound above 255 or a shift outside `0..=127`, which today's loader wraps.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ mod whm;
|
|||
|
||||
pub use jpegai::jpegai;
|
||||
pub use synth::int_synth;
|
||||
pub use whm::{Net, NetLayer, int_hyper, parse_wam1, parse_whm1, splat, wam1, wam1_net, wam1_net_loaded, whm1, whm1_net, whm1_net_loaded, wmb1, world};
|
||||
pub use whm::{Net, NetLayer, int_hyper, parse_wam1, parse_whm1, splat, wam1, wam1_net, wam1_net_loaded, whm1, whm1_net, whm1_net_loaded, wmb1, world, world_net, world_net_loaded};
|
||||
#[cfg(kani)]
|
||||
pub(crate) use whm::{p_conv, p_igdn, p_leaky};
|
||||
|
||||
|
|
|
|||
|
|
@ -431,10 +431,35 @@ pub fn wmb1(bytes: &[u8]) -> Res<Lowered> {
|
|||
/// `world/1` (§13.5, informative): the transition `T` with `A` action planes, then
|
||||
/// the synthesis `g_s` with the RGB8 step. `c` is the latent's channel count.
|
||||
pub fn world(t: &[u8], g_s: &[u8], c: u32) -> Res<Lowered> {
|
||||
let mut tn = parse_whm1(t)?;
|
||||
let gn = parse_whm1(g_s)?;
|
||||
let (tn, gn) = (parse_whm1(t)?, parse_whm1(g_s)?);
|
||||
let tg = world_t(tn, &gn, c)?;
|
||||
Ok(Lowered { roles: vec![("T", tg), ("g_s", net_graph(gn, true)?)] })
|
||||
}
|
||||
|
||||
/// [`world`] of two networks already parsed: a decoder that holds its networks parsed
|
||||
/// lowers them from that form, which is [`parse_whm1`] of their bytes. The graphs are
|
||||
/// the ones [`world`] writes from those bytes, and it refuses what [`world`] refuses.
|
||||
pub fn world_net(t: &Net, g_s: &Net, c: u32) -> Res<Lowered> {
|
||||
let tg = world_t(t.clone(), g_s, c)?;
|
||||
Ok(Lowered { roles: vec![("T", tg), ("g_s", net_graph(g_s.clone(), true)?)] })
|
||||
}
|
||||
|
||||
/// [`world_net`], then each graph loaded under `p`, as [`whm1_net_loaded`] loads one:
|
||||
/// `T`, then `g_s`. `T` is lowered and loaded before `g_s` is lowered, so no more than
|
||||
/// one network's lowering is held at once. The networks' lists move into the graphs.
|
||||
pub fn world_net_loaded(t: Net, g_s: Net, c: u32, p: Profile) -> Res<(Loaded, Loaded)> {
|
||||
let tl = crate::graph::load_lowered(world_t(t, &g_s, c)?, p)?;
|
||||
Ok((tl, crate::graph::load_lowered(net_graph(g_s, true)?, p)?))
|
||||
}
|
||||
|
||||
/// `world/1`'s load checks (§13.5), on both networks, then the graph `T` (its lists move
|
||||
/// into the graph).
|
||||
fn world_t(mut tn: Net, gn: &Net, c: u32) -> Res<Graph> {
|
||||
if tn.audio || gn.audio {
|
||||
return refuse();
|
||||
}
|
||||
header_ok(&tn)?;
|
||||
header_ok(&gn)?;
|
||||
header_ok(gn)?;
|
||||
if tn.afrac != gn.afrac {
|
||||
return refuse();
|
||||
}
|
||||
|
|
@ -456,6 +481,5 @@ pub fn world(t: &[u8], g_s: &[u8], c: u32) -> Res<Lowered> {
|
|||
let one = 1i64 << tn.afrac;
|
||||
let cc = b.channels(x);
|
||||
let out = b.op(opcode::CLAMP, vec![x], TensorRec::derived(cc), Params::Clamp { lo: -one, hi: one });
|
||||
let tg = b.finish(vec![out])?;
|
||||
Ok(Lowered { roles: vec![("T", tg), ("g_s", net_graph(gn, true)?)] })
|
||||
b.finish(vec![out])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -749,7 +749,7 @@ required-features = ["neural_int"]
|
|||
[[test]]
|
||||
name = "int_ops_differential"
|
||||
path = "tests/int_ops_differential.rs"
|
||||
required-features = ["legacy_int", "splat_int", "audio_int"]
|
||||
required-features = ["legacy_int", "splat_int", "audio_int", "world_int"]
|
||||
|
||||
[[test]]
|
||||
name = "int_synth_ops"
|
||||
|
|
@ -771,6 +771,18 @@ name = "int_hyper_ops"
|
|||
path = "tests/int_hyper_ops.rs"
|
||||
required-features = ["video_int", "legacy_int"]
|
||||
|
||||
# wai.world.model's integer renderer through its lowering world/1 (integer-operators §13.5).
|
||||
[[test]]
|
||||
name = "int_world_ops"
|
||||
path = "tests/int_world_ops.rs"
|
||||
required-features = ["world_int", "legacy_int"]
|
||||
|
||||
# What that renderer holds against the direct renderer (its own binary: a live-bytes allocator).
|
||||
[[test]]
|
||||
name = "int_world_memory"
|
||||
path = "tests/int_world_memory.rs"
|
||||
required-features = ["world_int", "legacy_int"]
|
||||
|
||||
[[test]]
|
||||
name = "int_mlicpp"
|
||||
path = "tests/int_mlicpp.rs"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ fn main() {
|
|||
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|v| v.as_i64().unwrap()).collect();
|
||||
let actions: Vec<usize> = j["actions"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap() as usize).collect();
|
||||
|
||||
let clip = decode(&t, &gs, &z0, &actions, c, lat);
|
||||
let clip = decode(&t, &gs, &z0, &actions, c, lat).unwrap_or_else(|r| panic!("the world renderer refused the clip: {r}"));
|
||||
println!("frames {} {}x{} frame0 {:016x} clip {:016x}",
|
||||
clip.frames.len(), clip.frame_px, clip.frame_px, clip.frame_hash(0), clip.clip_hash());
|
||||
if let Some(out) = std::env::args().nth(2) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
//! Decode throughput of WAI's integer decoders: the scalar reference kernels
|
||||
//! against the fast kernels, measured in one process on the same inputs.
|
||||
//! against the fast kernels, measured in one process on the same inputs. A `WHM1`
|
||||
//! network (the int_hyper, int_splat and int_world networks) runs as its decoders run
|
||||
//! it, through its lowering in the integer operator set (`whm1/1`), so its reference is
|
||||
//! the operator set's interpreter, and its fast kernels are the ones the interpreter is
|
||||
//! lent.
|
||||
//!
|
||||
//! ```text
|
||||
//! wai_decode_bench [--rounds N] [--filter SUBSTRING] [--quick]
|
||||
|
|
@ -205,34 +209,46 @@ fn jpegai_weights(luma: bool) -> wai::jpegai_synthesis::Weights {
|
|||
wai::jpegai_synthesis::Weights::from_json(&Value::Object(m))
|
||||
}
|
||||
|
||||
/// `model` lowered by `whm1/1` (with the RGB8 step where `rgb8` is set) and loaded under
|
||||
/// `acc64`: the network as the integer decoders run it.
|
||||
fn lowered(model: &IntModel, rgb8: bool) -> wai_ops::Loaded {
|
||||
let net = wai::int_ops::whm1_net(model).expect("the synthetic model converts");
|
||||
wai_ops::lower::whm1_net_loaded(net, rgb8, wai_ops::Profile::Acc64).expect("the synthetic model loads under acc64")
|
||||
}
|
||||
|
||||
/// One run of a loaded network on `x` (`[c, h, w]`) through the integer operator set, its
|
||||
/// fast kernels behind it under the current policy.
|
||||
fn run_lowered(g: &wai_ops::Loaded, x: &[i64], c: usize, h: usize, w: usize) -> wai_ops::Tensor {
|
||||
wai::int_ops::run1(g, wai_ops::Tensor::new(c as u32, h as u32, w as u32, x.to_vec()), &[]).expect("the input is within its check ranges")
|
||||
}
|
||||
|
||||
// ─────────────────────────────── cases ───────────────────────────────
|
||||
|
||||
fn kernel_cases(cases: &mut Vec<Case>) {
|
||||
use wai::int_hyper_synth::run_net;
|
||||
use wai::int_transformer::{conv_grouped, conv_transpose};
|
||||
|
||||
// int_hyper g_s middle layer: convT 192→192, 5×5, stride 2, 64² → 128².
|
||||
{
|
||||
let mut g = Rng(0x4B_0001);
|
||||
let model = IntModel { afrac: 12, gf: 16, shift: 24, layers: vec![convt_layer(&mut g, 192, 192, 5, 2, 24)] };
|
||||
let model = lowered(&IntModel { afrac: 12, gf: 16, shift: 24, layers: vec![convt_layer(&mut g, 192, 192, 5, 2, 24)] }, false);
|
||||
let x = g.vec(192 * 64 * 64, 3 << 12);
|
||||
cases.push(Case {
|
||||
name: "kernel · int_hyper convT 192→192 k5 s2, 64²→128²".into(),
|
||||
per_run: (128.0 * 128.0 / 1e6, "MPix"),
|
||||
macs: Some(192 * 192 * 25 * 64 * 64),
|
||||
run: Box::new(move || Out::I(run_net(&model, &x, 192, 64, 64).0)),
|
||||
run: Box::new(move || Out::I(run_lowered(&model, &x, 192, 64, 64).data)),
|
||||
});
|
||||
}
|
||||
// int_hyper h_s-style regular conv: 3×3, 192→192 at 64².
|
||||
{
|
||||
let mut g = Rng(0x4B_0002);
|
||||
let model = IntModel { afrac: 12, gf: 16, shift: 24, layers: vec![conv_layer(&mut g, 192, 192, 3, 24)] };
|
||||
let model = lowered(&IntModel { afrac: 12, gf: 16, shift: 24, layers: vec![conv_layer(&mut g, 192, 192, 3, 24)] }, false);
|
||||
let x = g.vec(192 * 64 * 64, 3 << 12);
|
||||
cases.push(Case {
|
||||
name: "kernel · int_hyper conv 192→192 k3 s1, 64²".into(),
|
||||
per_run: (64.0 * 64.0 / 1e6, "MPix"),
|
||||
macs: Some(192 * 192 * 9 * 64 * 64),
|
||||
run: Box::new(move || Out::I(run_net(&model, &x, 192, 64, 64).0)),
|
||||
run: Box::new(move || Out::I(run_lowered(&model, &x, 192, 64, 64).data)),
|
||||
});
|
||||
}
|
||||
// IGDN c = 192 at 128²: Q12 activations (32-bit term path) and wide ones (64-bit terms).
|
||||
|
|
@ -286,7 +302,7 @@ fn kernel_cases(cases: &mut Vec<Case>) {
|
|||
fn decoder_cases(cases: &mut Vec<Case>, quick: bool) {
|
||||
// Synthetic mbt2018-mean-class g_s (quality 6 topology): 16² latent → 256² RGB.
|
||||
{
|
||||
let model = sota_gs();
|
||||
let model = lowered(&sota_gs(), true);
|
||||
let mut g = Rng(0x5074_0002);
|
||||
let lat = if quick { 8 } else { 16 };
|
||||
let y = g.vec(320 * lat * lat, 3 << 12);
|
||||
|
|
@ -295,7 +311,7 @@ fn decoder_cases(cases: &mut Vec<Case>, quick: bool) {
|
|||
name: format!("decoder · int_hyper g_s, mbt2018-mean q6 topology (synthetic weights), {}²", 16 * lat),
|
||||
per_run: (px as f64 / 1e6, "MPix"),
|
||||
macs: None,
|
||||
run: Box::new(move || Out::B(wai::int_hyper_synth::decode(&model, &y, 320, lat, lat).0)),
|
||||
run: Box::new(move || Out::B(run_lowered(&model, &y, 320, lat, lat).data.iter().map(|&v| v as u8).collect())),
|
||||
});
|
||||
}
|
||||
// JPEG-AI synthesis (luma + chroma nets), 256² (the supported geometry).
|
||||
|
|
@ -366,7 +382,7 @@ fn vector_cases(cases: &mut Vec<Case>) {
|
|||
name: format!("vector · int_world trained interactive clip ({frames} frames, committed)"),
|
||||
per_run: (frames as f64, "frames"),
|
||||
macs: None,
|
||||
run: Box::new(move || Out::H(wai::int_world::decode(&t, &gs, &z0, &actions, c, lat).clip_hash())),
|
||||
run: Box::new(move || Out::H(wai::int_world::decode(&t, &gs, &z0, &actions, c, lat).unwrap().clip_hash())),
|
||||
});
|
||||
}
|
||||
// int_audio: trained 1-D codec (entropy + transposed convs + IGDN).
|
||||
|
|
@ -413,7 +429,6 @@ fn vector_cases(cases: &mut Vec<Case>) {
|
|||
// ─────────────────────────────── layer mode ───────────────────────────────
|
||||
|
||||
fn layer_profile(policies: &[(&str, Policy); 3], rounds: usize) {
|
||||
use wai::int_hyper_synth::run_net;
|
||||
let model = sota_gs();
|
||||
let mut g = Rng(0x5074_0002);
|
||||
let (mut c, mut h, mut w) = (320usize, 16usize, 16usize);
|
||||
|
|
@ -433,14 +448,15 @@ fn layer_profile(policies: &[(&str, Policy); 3], rounds: usize) {
|
|||
}
|
||||
_ => ("other".into(), String::new()),
|
||||
};
|
||||
let one = IntModel { afrac: model.afrac, gf: model.gf, shift: model.shift, layers: vec![clone_layer(layer)] };
|
||||
let one = lowered(&IntModel { afrac: model.afrac, gf: model.gf, shift: model.shift, layers: vec![clone_layer(layer)] }, false);
|
||||
let mut best = [Duration::MAX; 3];
|
||||
let mut outs: Vec<Vec<i64>> = Vec::new();
|
||||
for r in 0..rounds {
|
||||
for k in 0..3 {
|
||||
let idx = (k + r) % 3;
|
||||
let t0 = Instant::now();
|
||||
let (o, oc, oh, ow) = with_policy(policies[idx].1, || black_box(run_net(&one, &x, c, h, w)));
|
||||
let t = with_policy(policies[idx].1, || black_box(run_lowered(&one, &x, c, h, w)));
|
||||
let (o, oc, oh, ow) = (t.data, t.c as usize, t.h as usize, t.w as usize);
|
||||
best[idx] = best[idx].min(t0.elapsed());
|
||||
if r == 0 && k == 0 {
|
||||
outs.push(o.clone());
|
||||
|
|
|
|||
|
|
@ -1351,6 +1351,8 @@ pub struct OpsMember {
|
|||
/// 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).
|
||||
/// `wai.world.model` has none: its integer renderer runs through `world/1`, which is
|
||||
/// informative (§13.5), and the capability's decode is the replay of its action trace.
|
||||
pub fn capability_ops(cap: &str) -> Option<OpsMember> {
|
||||
let ops = |lowering, rans64, bucket| Some(OpsMember { profile: "acc64", subset: "full", lowering, rans64, bucket });
|
||||
// integer-payloads §2's stream: `rans64_decode` with `t4097`, `lenient` and `canonical`.
|
||||
|
|
|
|||
|
|
@ -1423,12 +1423,19 @@ mod tests {
|
|||
/// In this revision `wai.neural.int_synth`, `wai.splat.int_codec`,
|
||||
/// `wai.audio.int_codec`, `wai.neural.int_hyper`, `wai.video.int_hyper` and
|
||||
/// `wai.neural.int_mlicpp` decode through the operator set.
|
||||
///
|
||||
/// `wai.world.model` carries none. Its integer renderer renders through the operator
|
||||
/// set too, lowered by `world/1`, but that lowering is informative (§13.5): the
|
||||
/// capability's registered payload is its signed action trace, whose decode is the
|
||||
/// replay, and no payload text yet binds a trace to a renderer's set.
|
||||
#[test]
|
||||
fn ops_members_name_the_operator_set_a_capability_decodes_through() {
|
||||
let regs = registrations();
|
||||
let mut with: Vec<&str> = regs.iter().filter(|r| r.ops.is_some()).map(|r| r.capability).collect();
|
||||
with.sort_unstable();
|
||||
assert_eq!(with, [CAP_AUDIO_INT_CODEC, CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_MLICPP, CAP_NEURAL_INT_SYNTH, CAP_SPLAT_INT_CODEC, CAP_VIDEO_INT_HYPER]);
|
||||
let world = regs.iter().find(|r| r.capability == CAP_WORLD_MODEL).expect("wai.world.model is registered");
|
||||
assert!(world.ops.is_none() && world.replicate != Some(ReplicateClass::NeuralIntegerExact), "wai.world.model: its decode is the trace's replay; its renderer's lowering is informative");
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
use serde_json::Value;
|
||||
|
||||
use crate::int_entropy::det_build_index;
|
||||
use crate::int_transform::igdn;
|
||||
use crate::rans::RansDecoder;
|
||||
|
||||
fn u(v: &Value, k: &str) -> usize {
|
||||
|
|
@ -35,175 +34,8 @@ fn i64s(v: &Value) -> Vec<i64> {
|
|||
v.as_array().map(|a| a.iter().filter_map(|n| n.as_i64()).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// One integer transposed conv + per-output-channel requant
|
||||
/// `((raw·mult)>>shift) + bias`. `x` is `[cin, h·w]`; `weight` is `[cin][cout][k][k]`.
|
||||
/// Returns `([cout, ho·wo], ho, wo)`. Runs [`reference::conv_t`] or the fast kernel
|
||||
/// ([`crate::int_kernels`]) per the current [`crate::int_kernels::Policy`]; both give
|
||||
/// the same integer for every output element.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn conv_t(
|
||||
x: &[i64], cin: usize, h: usize, w: usize, weight: &[i64], cout: usize, k: usize,
|
||||
stride: usize, pad: usize, opad: usize, mult: &[i64], bias: &[i64], shift: u32,
|
||||
) -> (Vec<i64>, usize, usize) {
|
||||
use crate::int_kernels::{conv_acc_i64, policy, ConvGeom, KernelPath};
|
||||
if policy().path == KernelPath::Fast && mult.len() >= cout && bias.len() >= cout
|
||||
&& let Some(g) = ConvGeom::transposed(cin, h, w, cout, k, k, stride, stride, pad, pad, opad, opad, x.len(), weight.len())
|
||||
&& let Some(out) = conv_acc_i64(&g, x, weight, |oc, raw| ((raw as i128 * mult[oc] as i128) >> shift) as i64 + bias[oc])
|
||||
{
|
||||
return (out, g.ho, g.wo);
|
||||
}
|
||||
reference::conv_t(x, cin, h, w, weight, cout, k, stride, pad, opad, mult, bias, shift)
|
||||
}
|
||||
|
||||
/// One integer **regular** conv (stride/pad, no output-pad) + per-output-channel
|
||||
/// requant — see [`reference::conv`]. Dispatches like [`conv_t`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn conv(
|
||||
x: &[i64], cin: usize, h: usize, w: usize, weight: &[i64], cout: usize, k: usize,
|
||||
stride: usize, pad: usize, mult: &[i64], bias: &[i64], shift: u32,
|
||||
) -> (Vec<i64>, usize, usize) {
|
||||
use crate::int_kernels::{conv_acc_i64, policy, ConvGeom, KernelPath};
|
||||
if policy().path == KernelPath::Fast && mult.len() >= cout && bias.len() >= cout
|
||||
&& let Some(g) = ConvGeom::conv(cin, h, w, cout, 1, k, k, stride, stride, pad, pad, x.len(), weight.len())
|
||||
&& let Some(out) = conv_acc_i64(&g, x, weight, |oc, raw| ((raw as i128 * mult[oc] as i128) >> shift) as i64 + bias[oc])
|
||||
{
|
||||
return (out, g.ho, g.wo);
|
||||
}
|
||||
reference::conv(x, cin, h, w, weight, cout, k, stride, pad, mult, bias, shift)
|
||||
}
|
||||
|
||||
/// The scalar reference kernels — the executable specification the fast kernels
|
||||
/// in [`crate::int_kernels`] are checked against, kept verbatim. Selected for the
|
||||
/// whole decode by `int_kernels::with_policy(Policy::reference(), ..)`.
|
||||
#[doc(hidden)]
|
||||
pub mod reference {
|
||||
/// One integer transposed conv + per-output-channel requant `((raw·mult)>>shift)
|
||||
/// + bias`. `x` is `[cin, h·w]`; `weight` is `[cin][cout][k][k]`. Returns
|
||||
/// `([cout, ho·wo], ho, wo)`. Pure i64 (the accumulation stays well inside i64).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn conv_t(
|
||||
x: &[i64], cin: usize, h: usize, w: usize, weight: &[i64], cout: usize, k: usize,
|
||||
stride: usize, pad: usize, opad: usize, mult: &[i64], bias: &[i64], shift: u32,
|
||||
) -> (Vec<i64>, usize, usize) {
|
||||
let ho = (h - 1) * stride + k + opad - 2 * pad;
|
||||
let wo = (w - 1) * stride + k + opad - 2 * pad;
|
||||
let (hp, wp) = (ho + 2 * pad, wo + 2 * pad);
|
||||
let kk = k * k;
|
||||
let mut raw = vec![0i64; cout * hp * wp];
|
||||
// Loop order ic → oc → spatial → kernel: for a fixed (ic, oc) the k×k weight
|
||||
// block is loaded once and every scatter lands in the single `oc` output plane,
|
||||
// so that plane (and the input row) stays hot in cache. Integer accumulation is
|
||||
// order-independent, so this is byte-identical to the naive oc-inner order.
|
||||
for ic in 0..cin {
|
||||
let xc = &x[ic * h * w..ic * h * w + h * w];
|
||||
for oc in 0..cout {
|
||||
let wb = (ic * cout + oc) * kk;
|
||||
let wk = &weight[wb..wb + kk];
|
||||
let plane = oc * hp * wp;
|
||||
for iy in 0..h {
|
||||
let ry0 = iy * stride;
|
||||
let xrow = &xc[iy * w..iy * w + w];
|
||||
for (ix, &v) in xrow.iter().enumerate() {
|
||||
if v == 0 {
|
||||
continue;
|
||||
}
|
||||
let base = plane + ry0 * wp + ix * stride;
|
||||
for ky in 0..k {
|
||||
let dst = base + ky * wp;
|
||||
let wky = ky * k;
|
||||
for kx in 0..k {
|
||||
raw[dst + kx] += wk[wky + kx] * v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![0i64; cout * ho * wo];
|
||||
for oc in 0..cout {
|
||||
let (m, b) = (mult[oc] as i128, bias[oc]);
|
||||
for oy in 0..ho {
|
||||
for ox in 0..wo {
|
||||
let r = raw[(oc * hp + oy + pad) * wp + ox + pad] as i128;
|
||||
out[(oc * ho + oy) * wo + ox] = ((r * m) >> shift) as i64 + b;
|
||||
}
|
||||
}
|
||||
}
|
||||
(out, ho, wo)
|
||||
}
|
||||
|
||||
/// One integer **regular** conv (stride/pad, no output-pad) + per-output-channel
|
||||
/// requant — the stride-1 `Conv2d` that closes the hyper-decoder (`M·3/2 → M·2`,
|
||||
/// emitting σ‖μ). `weight` is `[cout][cin][k][k]`. Mirrors the export's
|
||||
/// `conv_int` byte-for-byte. Pure i64 (i128 only for the requant multiply).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn conv(
|
||||
x: &[i64], cin: usize, h: usize, w: usize, weight: &[i64], cout: usize, k: usize,
|
||||
stride: usize, pad: usize, mult: &[i64], bias: &[i64], shift: u32,
|
||||
) -> (Vec<i64>, usize, usize) {
|
||||
let ho = (h + 2 * pad - k) / stride + 1;
|
||||
let wo = (w + 2 * pad - k) / stride + 1;
|
||||
let kk = k * k;
|
||||
let mut out = vec![0i64; cout * ho * wo]; // raw sums first; requantized below
|
||||
// Loop order oc → ic → kernel → spatial: the weight tap is a hoisted scalar
|
||||
// and the inner `ox` loop streams one output row against one input row (great
|
||||
// locality, vectorizable). For stride 1 the valid `ox` span is precomputed so
|
||||
// the inner loop is branch-free. Integer accumulation is order-independent, so
|
||||
// this is byte-identical to the naive output-stationary order.
|
||||
for oc in 0..cout {
|
||||
let plane = oc * ho * wo;
|
||||
for ic in 0..cin {
|
||||
let xb = ic * h * w;
|
||||
let wb = (oc * cin + ic) * kk;
|
||||
for ky in 0..k {
|
||||
for kx in 0..k {
|
||||
let wv = weight[wb + ky * k + kx];
|
||||
if wv == 0 {
|
||||
continue;
|
||||
}
|
||||
// valid output columns: input ix = ox·stride + kx − pad ∈ [0, w)
|
||||
let (ox_lo, ox_hi) = if stride == 1 {
|
||||
(pad.saturating_sub(kx), (w + pad).saturating_sub(kx).min(wo))
|
||||
} else {
|
||||
(0, wo)
|
||||
};
|
||||
for oy in 0..ho {
|
||||
let iy = oy * stride + ky;
|
||||
if iy < pad || iy >= pad + h {
|
||||
continue;
|
||||
}
|
||||
let xrow = xb + (iy - pad) * w;
|
||||
let orow = plane + oy * wo;
|
||||
if stride == 1 {
|
||||
// over [ox_lo, ox_hi): ox + kx ≥ pad, so ox+kx−pad ≥ 0.
|
||||
for ox in ox_lo..ox_hi {
|
||||
out[orow + ox] += wv * x[xrow + ox + kx - pad];
|
||||
}
|
||||
} else {
|
||||
for ox in 0..wo {
|
||||
let ix = ox * stride + kx;
|
||||
if ix < pad || ix >= pad + w {
|
||||
continue;
|
||||
}
|
||||
out[orow + ox] += wv * x[xrow + ix - pad];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for oc in 0..cout {
|
||||
let (m, b) = (mult[oc] as i128, bias[oc]);
|
||||
for v in &mut out[oc * ho * wo..(oc + 1) * ho * wo] {
|
||||
*v = ((*v as i128 * m) >> shift) as i64 + b;
|
||||
}
|
||||
}
|
||||
(out, ho, wo)
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed integer conv net — the typed form [`run_net`] consumes, built from
|
||||
/// A parsed integer conv net — the typed form the lowerings convert
|
||||
/// ([`crate::int_ops::whm1_net`]), built from
|
||||
/// either the export's JSON ([`IntModel::from_json`]) or its **compact binary**
|
||||
/// ([`IntModel::from_bytes`]): the binary stores the int12 weights as `i16` and
|
||||
/// IGDN `gamma` as `i32`, roughly halving the model and making the in-browser
|
||||
|
|
@ -216,8 +48,9 @@ pub struct IntModel {
|
|||
}
|
||||
|
||||
/// One layer of an [`IntModel`]. Weights/params are widened to `i64` for the
|
||||
/// decode (the arithmetic stays well inside i64); `in_ch` is not stored — the
|
||||
/// running channel count threads through [`run_net`].
|
||||
/// decode (the arithmetic stays well inside i64); `in_ch` is not stored: a layer's input
|
||||
/// channels are the running channel count, or its weights' count over its output channels
|
||||
/// and kernel.
|
||||
pub enum IntLayer {
|
||||
ConvT { out_ch: usize, k: usize, stride: usize, pad: usize, opad: usize, weight: Vec<i64>, mult: Vec<i64>, bias: Vec<i64> },
|
||||
Conv { out_ch: usize, k: usize, stride: usize, pad: usize, weight: Vec<i64>, mult: Vec<i64>, bias: Vec<i64> },
|
||||
|
|
@ -383,53 +216,6 @@ impl IntModel {
|
|||
}
|
||||
}
|
||||
|
||||
/// Run an integer conv net (`model`: `afrac`/`gf`/`shift` + a `layers` stack of
|
||||
/// `convT` / `conv` / `igdn` / `relu`) on `input` (`[c, h, w]` at `2^afrac`). Returns the
|
||||
/// raw integer feature map `([c·h·w], c, h, w)` at `2^afrac`. Shared by both the
|
||||
/// synthesis `g_s` (→ pixels via [`decode`]) and the hyper-decoder `h_s`
|
||||
/// (→ σ‖μ). Pure i64; byte-identical on every machine.
|
||||
pub fn run_net(model: &IntModel, input: &[i64], mut c: usize, mut h: usize, mut w: usize) -> (Vec<i64>, usize, usize, usize) {
|
||||
let mut x = input.to_vec();
|
||||
for layer in &model.layers {
|
||||
(x, c, h, w) = apply_layer(model, layer, x, c, h, w);
|
||||
}
|
||||
(x, c, h, w)
|
||||
}
|
||||
|
||||
/// One layer of [`run_net`] on `x` (`[c, h, w]`): the new activations and shape. The
|
||||
/// test-only direct guard (`legacy::run_net_checked`) runs layers through this same
|
||||
/// function, so the two cannot compute different integers.
|
||||
fn apply_layer(model: &IntModel, layer: &IntLayer, mut x: Vec<i64>, c: usize, h: usize, w: usize) -> (Vec<i64>, usize, usize, usize) {
|
||||
let (afrac, gf, shift) = (model.afrac, model.gf, model.shift);
|
||||
match layer {
|
||||
IntLayer::ConvT { out_ch, k, stride, pad, opad, weight, mult, bias } => {
|
||||
let (nx, ho, wo) = conv_t(&x, c, h, w, weight, *out_ch, *k, *stride, *pad, *opad, mult, bias, shift);
|
||||
(nx, *out_ch, ho, wo)
|
||||
}
|
||||
IntLayer::Conv { out_ch, k, stride, pad, weight, mult, bias } => {
|
||||
let (nx, ho, wo) = conv(&x, c, h, w, weight, *out_ch, *k, *stride, *pad, mult, bias, shift);
|
||||
(nx, *out_ch, ho, wo)
|
||||
}
|
||||
IntLayer::Igdn { gamma, beta } => (igdn(&x, c, h * w, afrac, gamma, beta, gf), c, h, w),
|
||||
IntLayer::Relu => {
|
||||
for v in &mut x {
|
||||
if *v < 0 {
|
||||
*v = 0;
|
||||
}
|
||||
}
|
||||
(x, c, h, w)
|
||||
}
|
||||
IntLayer::Leaky { slope_q, slope_frac } => {
|
||||
for v in &mut x {
|
||||
if *v < 0 {
|
||||
*v = (*v * *slope_q) >> *slope_frac;
|
||||
}
|
||||
}
|
||||
(x, c, h, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a network was refused before it ran ([`net_shape`]), or by the test-only direct
|
||||
/// guard (`legacy::run_net_checked`): the index of the first layer refused, the check
|
||||
/// that failed, and whether that check was about the values presented (`out_of_range`:
|
||||
|
|
@ -540,28 +326,6 @@ pub fn net_shape(model: &IntModel, mut c: usize, mut h: usize, mut w: usize) ->
|
|||
Ok(NetShape { out: (c, h, w), peak })
|
||||
}
|
||||
|
||||
/// Decode the integer synthesis `model` on the latent `input` (`[c, h, w]` at
|
||||
/// `2^afrac`) → RGB, row-major **HWC** `u8`. Byte-identical on every machine.
|
||||
pub fn decode(model: &IntModel, input: &[i64], c: usize, h: usize, w: usize) -> (Vec<u8>, usize, usize) {
|
||||
let (x, c, h, w) = run_net(model, input, c, h, w);
|
||||
(to_rgb8(&x, c, h, w, model.afrac), h, w)
|
||||
}
|
||||
|
||||
/// The integer pixel quantisation that ends a synthesis: each value clamped to
|
||||
/// `[0, 2^afrac]`, scaled to 8 bits rounding half up, CHW → row-major HWC `u8`.
|
||||
/// `afrac` must be in `1..=54`, so that `2^afrac · 255 + 2^(afrac-1)` fits `i64`.
|
||||
pub fn to_rgb8(x: &[i64], c: usize, h: usize, w: usize, afrac: u32) -> Vec<u8> {
|
||||
let (one, half) = (1i64 << afrac, 1i64 << (afrac - 1));
|
||||
let mut rgb = vec![0u8; h * w * c];
|
||||
for cc in 0..c {
|
||||
for p in 0..h * w {
|
||||
let v = x[cc * h * w + p].clamp(0, one);
|
||||
rgb[p * c + cc] = ((v * 255 + half) >> afrac).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
rgb
|
||||
}
|
||||
|
||||
/// FNV-1a 64-bit of `bytes` — the dependency-free checksum the export tool and
|
||||
/// this decoder share to prove byte-for-byte agreement across languages.
|
||||
pub fn fnv1a(bytes: &[u8]) -> u64 {
|
||||
|
|
@ -1013,12 +777,215 @@ fn latent(ent: &CondEntropy, bs: &CondStream<'_>, afrac: u32, sigma: &[i64], mu:
|
|||
/// operator set: `h_s` and `g_s` run layer by layer by [`legacy::run_net_checked`], which
|
||||
/// checks each layer's arithmetic on the values it is given before it runs, then the RGB8
|
||||
/// bytes formed here ([`to_rgb8`]). The guard itself is here too: the direct decodes of the
|
||||
/// splat, audio and MLIC++-topology codecs call it (or its IGDN check), and no production
|
||||
/// decode does. Test-only (`legacy_int`): the differential tests decode both ways, and the
|
||||
/// splat, audio and MLIC++-topology codecs call it (or its IGDN check). So is the unchecked
|
||||
/// [`run_net`], with the scalar kernels it runs and [`decode`]: the world renderer's direct
|
||||
/// renderer runs them, and the tests hold the guard to them. No production decode calls
|
||||
/// any of these. 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::*;
|
||||
use crate::int_transform::igdn;
|
||||
|
||||
/// The scalar kernels of the direct decode, kept verbatim: the convolutions
|
||||
/// [`run_net`] runs.
|
||||
pub mod reference {
|
||||
/// One integer transposed conv + per-output-channel requant `((raw·mult)>>shift)
|
||||
/// + bias`. `x` is `[cin, h·w]`; `weight` is `[cin][cout][k][k]`. Returns
|
||||
/// `([cout, ho·wo], ho, wo)`. Pure i64 (the accumulation stays well inside i64).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn conv_t(
|
||||
x: &[i64], cin: usize, h: usize, w: usize, weight: &[i64], cout: usize, k: usize,
|
||||
stride: usize, pad: usize, opad: usize, mult: &[i64], bias: &[i64], shift: u32,
|
||||
) -> (Vec<i64>, usize, usize) {
|
||||
let ho = (h - 1) * stride + k + opad - 2 * pad;
|
||||
let wo = (w - 1) * stride + k + opad - 2 * pad;
|
||||
let (hp, wp) = (ho + 2 * pad, wo + 2 * pad);
|
||||
let kk = k * k;
|
||||
let mut raw = vec![0i64; cout * hp * wp];
|
||||
// Loop order ic → oc → spatial → kernel: for a fixed (ic, oc) the k×k weight
|
||||
// block is loaded once and every scatter lands in the single `oc` output plane,
|
||||
// so that plane (and the input row) stays hot in cache. Integer accumulation is
|
||||
// order-independent, so this is byte-identical to the naive oc-inner order.
|
||||
for ic in 0..cin {
|
||||
let xc = &x[ic * h * w..ic * h * w + h * w];
|
||||
for oc in 0..cout {
|
||||
let wb = (ic * cout + oc) * kk;
|
||||
let wk = &weight[wb..wb + kk];
|
||||
let plane = oc * hp * wp;
|
||||
for iy in 0..h {
|
||||
let ry0 = iy * stride;
|
||||
let xrow = &xc[iy * w..iy * w + w];
|
||||
for (ix, &v) in xrow.iter().enumerate() {
|
||||
if v == 0 {
|
||||
continue;
|
||||
}
|
||||
let base = plane + ry0 * wp + ix * stride;
|
||||
for ky in 0..k {
|
||||
let dst = base + ky * wp;
|
||||
let wky = ky * k;
|
||||
for kx in 0..k {
|
||||
raw[dst + kx] += wk[wky + kx] * v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![0i64; cout * ho * wo];
|
||||
for oc in 0..cout {
|
||||
let (m, b) = (mult[oc] as i128, bias[oc]);
|
||||
for oy in 0..ho {
|
||||
for ox in 0..wo {
|
||||
let r = raw[(oc * hp + oy + pad) * wp + ox + pad] as i128;
|
||||
out[(oc * ho + oy) * wo + ox] = ((r * m) >> shift) as i64 + b;
|
||||
}
|
||||
}
|
||||
}
|
||||
(out, ho, wo)
|
||||
}
|
||||
|
||||
/// One integer **regular** conv (stride/pad, no output-pad) + per-output-channel
|
||||
/// requant — the stride-1 `Conv2d` that closes the hyper-decoder (`M·3/2 → M·2`,
|
||||
/// emitting σ‖μ). `weight` is `[cout][cin][k][k]`. Mirrors the export's
|
||||
/// `conv_int` byte-for-byte. Pure i64 (i128 only for the requant multiply).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn conv(
|
||||
x: &[i64], cin: usize, h: usize, w: usize, weight: &[i64], cout: usize, k: usize,
|
||||
stride: usize, pad: usize, mult: &[i64], bias: &[i64], shift: u32,
|
||||
) -> (Vec<i64>, usize, usize) {
|
||||
let ho = (h + 2 * pad - k) / stride + 1;
|
||||
let wo = (w + 2 * pad - k) / stride + 1;
|
||||
let kk = k * k;
|
||||
let mut out = vec![0i64; cout * ho * wo]; // raw sums first; requantized below
|
||||
// Loop order oc → ic → kernel → spatial: the weight tap is a hoisted scalar
|
||||
// and the inner `ox` loop streams one output row against one input row (great
|
||||
// locality, vectorizable). For stride 1 the valid `ox` span is precomputed so
|
||||
// the inner loop is branch-free. Integer accumulation is order-independent, so
|
||||
// this is byte-identical to the naive output-stationary order.
|
||||
for oc in 0..cout {
|
||||
let plane = oc * ho * wo;
|
||||
for ic in 0..cin {
|
||||
let xb = ic * h * w;
|
||||
let wb = (oc * cin + ic) * kk;
|
||||
for ky in 0..k {
|
||||
for kx in 0..k {
|
||||
let wv = weight[wb + ky * k + kx];
|
||||
if wv == 0 {
|
||||
continue;
|
||||
}
|
||||
// valid output columns: input ix = ox·stride + kx − pad ∈ [0, w)
|
||||
let (ox_lo, ox_hi) = if stride == 1 {
|
||||
(pad.saturating_sub(kx), (w + pad).saturating_sub(kx).min(wo))
|
||||
} else {
|
||||
(0, wo)
|
||||
};
|
||||
for oy in 0..ho {
|
||||
let iy = oy * stride + ky;
|
||||
if iy < pad || iy >= pad + h {
|
||||
continue;
|
||||
}
|
||||
let xrow = xb + (iy - pad) * w;
|
||||
let orow = plane + oy * wo;
|
||||
if stride == 1 {
|
||||
// over [ox_lo, ox_hi): ox + kx ≥ pad, so ox+kx−pad ≥ 0.
|
||||
for ox in ox_lo..ox_hi {
|
||||
out[orow + ox] += wv * x[xrow + ox + kx - pad];
|
||||
}
|
||||
} else {
|
||||
for ox in 0..wo {
|
||||
let ix = ox * stride + kx;
|
||||
if ix < pad || ix >= pad + w {
|
||||
continue;
|
||||
}
|
||||
out[orow + ox] += wv * x[xrow + ix - pad];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for oc in 0..cout {
|
||||
let (m, b) = (mult[oc] as i128, bias[oc]);
|
||||
for v in &mut out[oc * ho * wo..(oc + 1) * ho * wo] {
|
||||
*v = ((*v as i128 * m) >> shift) as i64 + b;
|
||||
}
|
||||
}
|
||||
(out, ho, wo)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run an integer conv net (`model`: `afrac`/`gf`/`shift` + a `layers` stack of
|
||||
/// `convT` / `conv` / `igdn` / `relu`) on `input` (`[c, h, w]` at `2^afrac`), as the
|
||||
/// direct decodes did, with no check of the values: a value that leaves `i64` panics
|
||||
/// in an overflow-checked build and wraps without the checks. Returns the raw integer
|
||||
/// feature map `([c·h·w], c, h, w)` at `2^afrac`. The decodes run their networks through
|
||||
/// their lowerings (`extensions/integer-operators.md` §13) instead.
|
||||
pub fn run_net(model: &IntModel, input: &[i64], mut c: usize, mut h: usize, mut w: usize) -> (Vec<i64>, usize, usize, usize) {
|
||||
let mut x = input.to_vec();
|
||||
for layer in &model.layers {
|
||||
(x, c, h, w) = apply_layer(model, layer, x, c, h, w);
|
||||
}
|
||||
(x, c, h, w)
|
||||
}
|
||||
|
||||
/// One layer of [`run_net`] on `x` (`[c, h, w]`): the new activations and shape, on the
|
||||
/// scalar kernels ([`reference`]). The direct guard ([`run_net_checked`]) runs layers
|
||||
/// through this same function, so the two cannot compute different integers.
|
||||
pub(crate) fn apply_layer(model: &IntModel, layer: &IntLayer, mut x: Vec<i64>, c: usize, h: usize, w: usize) -> (Vec<i64>, usize, usize, usize) {
|
||||
let (afrac, gf, shift) = (model.afrac, model.gf, model.shift);
|
||||
match layer {
|
||||
IntLayer::ConvT { out_ch, k, stride, pad, opad, weight, mult, bias } => {
|
||||
let (nx, ho, wo) = reference::conv_t(&x, c, h, w, weight, *out_ch, *k, *stride, *pad, *opad, mult, bias, shift);
|
||||
(nx, *out_ch, ho, wo)
|
||||
}
|
||||
IntLayer::Conv { out_ch, k, stride, pad, weight, mult, bias } => {
|
||||
let (nx, ho, wo) = reference::conv(&x, c, h, w, weight, *out_ch, *k, *stride, *pad, mult, bias, shift);
|
||||
(nx, *out_ch, ho, wo)
|
||||
}
|
||||
IntLayer::Igdn { gamma, beta } => (igdn(&x, c, h * w, afrac, gamma, beta, gf), c, h, w),
|
||||
IntLayer::Relu => {
|
||||
for v in &mut x {
|
||||
if *v < 0 {
|
||||
*v = 0;
|
||||
}
|
||||
}
|
||||
(x, c, h, w)
|
||||
}
|
||||
IntLayer::Leaky { slope_q, slope_frac } => {
|
||||
for v in &mut x {
|
||||
if *v < 0 {
|
||||
*v = (*v * *slope_q) >> *slope_frac;
|
||||
}
|
||||
}
|
||||
(x, c, h, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The direct decode of an integer synthesis `model` on the latent `input` (`[c, h, w]`
|
||||
/// at `2^afrac`) → RGB, row-major **HWC** `u8`: [`run_net`], then [`to_rgb8`].
|
||||
pub fn decode(model: &IntModel, input: &[i64], c: usize, h: usize, w: usize) -> (Vec<u8>, usize, usize) {
|
||||
let (x, c, h, w) = run_net(model, input, c, h, w);
|
||||
(to_rgb8(&x, c, h, w, model.afrac), h, w)
|
||||
}
|
||||
|
||||
/// The integer pixel quantisation that ends a synthesis: each value clamped to
|
||||
/// `[0, 2^afrac]`, scaled to 8 bits rounding half up, CHW → row-major HWC `u8`.
|
||||
/// `afrac` must be in `1..=54`, so that `2^afrac · 255 + 2^(afrac-1)` fits `i64`.
|
||||
pub fn to_rgb8(x: &[i64], c: usize, h: usize, w: usize, afrac: u32) -> Vec<u8> {
|
||||
let (one, half) = (1i64 << afrac, 1i64 << (afrac - 1));
|
||||
let mut rgb = vec![0u8; h * w * c];
|
||||
for cc in 0..c {
|
||||
for p in 0..h * w {
|
||||
let v = x[cc * h * w + p].clamp(0, one);
|
||||
rgb[p * c + cc] = ((v * 255 + half) >> afrac).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
rgb
|
||||
}
|
||||
|
||||
|
||||
/// [`run_net`], run only while every step stays exact.
|
||||
///
|
||||
|
|
@ -1041,11 +1008,9 @@ pub mod legacy {
|
|||
/// plus one must fit `i64`. For the leaky ReLU, `max|x| · |slope|` must fit `i64`.
|
||||
///
|
||||
/// A layer that fails a check is not run, and the refusal says which and why. On
|
||||
/// `Ok` the output is [`run_net`]'s, computed by the same code ([`apply_layer`], which
|
||||
/// dispatches to the fast kernels or the reference as `run_net` does), and no step
|
||||
/// overflowed, so a build with overflow checks and one without compute the same
|
||||
/// integers and neither panics. The fast kernels, which narrow only under their own
|
||||
/// bounds, form no value the reference does not (see [`crate::int_kernels`]).
|
||||
/// `Ok` the output is [`run_net`]'s, computed by the same code ([`apply_layer`], on the
|
||||
/// scalar kernels), and no step overflowed, so a build with overflow checks and one
|
||||
/// without compute the same integers and neither panics.
|
||||
///
|
||||
/// The checks are one layer deep, on real values, so they refuse only activations
|
||||
/// that come within a factor `terms` (or, for IGDN, `c`) of the type's range, not
|
||||
|
|
@ -1056,9 +1021,9 @@ pub mod legacy {
|
|||
/// Every network that decodes in production does so through its lowering in the integer
|
||||
/// operator set (`extensions/integer-operators.md` §13.2), which makes these checks as the
|
||||
/// `check` ranges of its graph, refusing exactly what these refuse, at the same layer: the
|
||||
/// splat, audio, conditional and MLIC++-topology decodes. This guard, and the direct
|
||||
/// decodes that call it or [`igdn_check`], are test-only, for the tests that hold the two
|
||||
/// to each other.
|
||||
/// splat, audio, conditional and MLIC++-topology decodes, and the world renderer. This
|
||||
/// guard, and the direct decodes that call it or [`igdn_check`], are test-only, for the
|
||||
/// tests that hold the two to each other.
|
||||
pub fn run_net_checked(
|
||||
model: &IntModel, input: &[i64], mut c: usize, mut h: usize, mut w: usize,
|
||||
) -> Result<(Vec<i64>, usize, usize, usize), NetRefusal> {
|
||||
|
|
@ -1397,261 +1362,13 @@ pub mod ops {
|
|||
}
|
||||
}
|
||||
|
||||
/// The fast kernels against the scalar reference, element by element. Operand
|
||||
/// ranges cover the `i32` fast path, the `i64` path, and sparse (zero-heavy)
|
||||
/// inputs; geometries cover strides, paddings, output paddings and kernels smaller
|
||||
/// than the stride.
|
||||
#[cfg(test)]
|
||||
mod kernel_equivalence {
|
||||
use super::*;
|
||||
use crate::int_kernels::testkit::{Rng, FAST};
|
||||
use crate::int_kernels::{with_policy, Policy};
|
||||
|
||||
/// (input range, weight range) per case: fits-i32, beyond-i32, mixed.
|
||||
const RANGES: [(i64, i64); 3] = [(1 << 14, 2047), (1 << 40, 1 << 10), (1 << 20, 1 << 20)];
|
||||
|
||||
#[test]
|
||||
fn transposed_conv_equals_reference() {
|
||||
let mut g = Rng(0xC0FF_EE01);
|
||||
let mut ran = 0;
|
||||
for case in 0..600 {
|
||||
let k = 1 + g.below(5);
|
||||
let stride = 1 + g.below(3);
|
||||
let pad = g.below(k);
|
||||
let opad = if stride > 1 { g.below(stride) } else { 0 };
|
||||
let (h, w) = (1 + g.below(7), 1 + g.below(9));
|
||||
let (cin, cout) = (1 + g.below(6), 1 + g.below(9));
|
||||
if (h - 1) * stride + k + opad <= 2 * pad || (w - 1) * stride + k + opad <= 2 * pad {
|
||||
continue;
|
||||
}
|
||||
let (xr, wr) = RANGES[case % 3];
|
||||
let x = g.vec(cin * h * w, xr, [0, 50, 90][case % 3]);
|
||||
let wt = g.vec(cin * cout * k * k, wr, 10);
|
||||
let mult: Vec<i64> = (0..cout).map(|_| 1 + g.signed(1 << 20).abs()).collect();
|
||||
let bias = g.vec(cout, 1 << 20, 0);
|
||||
let shift = 16 + g.below(8) as u32;
|
||||
let want = reference::conv_t(&x, cin, h, w, &wt, cout, k, stride, pad, opad, &mult, &bias, shift);
|
||||
for p in FAST {
|
||||
let got = with_policy(p, || conv_t(&x, cin, h, w, &wt, cout, k, stride, pad, opad, &mult, &bias, shift));
|
||||
assert_eq!(got, want, "conv_t {p:?} k={k} s={stride} p={pad} op={opad} {cin}x{h}x{w}->{cout}");
|
||||
}
|
||||
ran += 1;
|
||||
}
|
||||
assert!(ran > 300, "too few valid cases ({ran})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_conv_equals_reference() {
|
||||
let mut g = Rng(0xC0FF_EE02);
|
||||
let mut ran = 0;
|
||||
for case in 0..600 {
|
||||
let k = 1 + g.below(5);
|
||||
let stride = 1 + g.below(3);
|
||||
let pad = g.below(k);
|
||||
let (h, w) = (1 + g.below(9), 1 + g.below(11));
|
||||
let (cin, cout) = (1 + g.below(6), 1 + g.below(9));
|
||||
if h + 2 * pad < k || w + 2 * pad < k {
|
||||
continue;
|
||||
}
|
||||
let (xr, wr) = RANGES[case % 3];
|
||||
let x = g.vec(cin * h * w, xr, [0, 50, 90][case % 3]);
|
||||
let wt = g.vec(cin * cout * k * k, wr, 10);
|
||||
let mult: Vec<i64> = (0..cout).map(|_| 1 + g.signed(1 << 20).abs()).collect();
|
||||
let bias = g.vec(cout, 1 << 20, 0);
|
||||
let shift = 16 + g.below(8) as u32;
|
||||
let want = reference::conv(&x, cin, h, w, &wt, cout, k, stride, pad, &mult, &bias, shift);
|
||||
for p in FAST {
|
||||
let got = with_policy(p, || conv(&x, cin, h, w, &wt, cout, k, stride, pad, &mult, &bias, shift));
|
||||
assert_eq!(got, want, "conv {p:?} k={k} s={stride} p={pad} {cin}x{h}x{w}->{cout}");
|
||||
}
|
||||
ran += 1;
|
||||
}
|
||||
assert!(ran > 300, "too few valid cases ({ran})");
|
||||
}
|
||||
|
||||
/// Strides up to 16, beyond the kernel and the input. The transposed kernel's
|
||||
/// row bands then hold fewer rows than the stride (a tap table only for each row
|
||||
/// phase present) as well as more (rows wrapping onto the same phases), with
|
||||
/// bands starting anywhere in a phase; many phases receive no tap at all. The
|
||||
/// regular kernel's stride often exceeds its padded row.
|
||||
#[test]
|
||||
fn wide_strides_equal_reference() {
|
||||
let mut g = Rng(0xC0FF_EE03);
|
||||
let (mut ran_t, mut ran_c) = (0, 0);
|
||||
for case in 0..300 {
|
||||
let k = 1 + g.below(5);
|
||||
let stride = 1 + g.below(16);
|
||||
let pad = g.below(k);
|
||||
let opad = g.below(stride);
|
||||
let (h, w) = (1 + g.below(4), 1 + g.below(5));
|
||||
let (cin, cout) = (1 + g.below(4), 1 + g.below(6));
|
||||
let (xr, wr) = RANGES[case % 3];
|
||||
let x = g.vec(cin * h * w, xr, [0, 50, 90][case % 3]);
|
||||
let wt = g.vec(cin * cout * k * k, wr, 10);
|
||||
let mult: Vec<i64> = (0..cout).map(|_| 1 + g.signed(1 << 20).abs()).collect();
|
||||
let bias = g.vec(cout, 1 << 20, 0);
|
||||
let shift = 16 + g.below(8) as u32;
|
||||
if (h - 1) * stride + k + opad > 2 * pad && (w - 1) * stride + k + opad > 2 * pad {
|
||||
let want = reference::conv_t(&x, cin, h, w, &wt, cout, k, stride, pad, opad, &mult, &bias, shift);
|
||||
for p in FAST {
|
||||
let got = with_policy(p, || conv_t(&x, cin, h, w, &wt, cout, k, stride, pad, opad, &mult, &bias, shift));
|
||||
assert_eq!(got, want, "conv_t {p:?} k={k} s={stride} p={pad} op={opad} {cin}x{h}x{w}->{cout}");
|
||||
}
|
||||
ran_t += 1;
|
||||
}
|
||||
if h + 2 * pad >= k && w + 2 * pad >= k {
|
||||
let want = reference::conv(&x, cin, h, w, &wt, cout, k, stride, pad, &mult, &bias, shift);
|
||||
for p in FAST {
|
||||
let got = with_policy(p, || conv(&x, cin, h, w, &wt, cout, k, stride, pad, &mult, &bias, shift));
|
||||
assert_eq!(got, want, "conv {p:?} k={k} s={stride} p={pad} {cin}x{h}x{w}->{cout}");
|
||||
}
|
||||
ran_c += 1;
|
||||
}
|
||||
}
|
||||
assert!(ran_t > 150 && ran_c > 150, "too few valid cases ({ran_t} transposed, {ran_c} regular)");
|
||||
}
|
||||
|
||||
/// Operands whose `i64` accumulation overflows: the reference panics in an
|
||||
/// overflow-checked build and wraps otherwise, and the fast kernel must do the
|
||||
/// same — a panic where it panics, the identical wrapped integers where it wraps.
|
||||
#[test]
|
||||
fn overflowing_accumulation_behaves_like_reference() {
|
||||
let (cin, h, w, cout, k) = (4usize, 3usize, 3usize, 2usize, 3usize);
|
||||
let x = vec![i32::MAX as i64; cin * h * w];
|
||||
let wt = vec![i32::MAX as i64; cin * cout * k * k];
|
||||
let (mult, bias) = (vec![1i64; cout], vec![0i64; cout]);
|
||||
let run = |p: Policy| {
|
||||
std::panic::catch_unwind(|| {
|
||||
with_policy(p, || conv_t(&x, cin, h, w, &wt, cout, k, 2, 1, 1, &mult, &bias, 0))
|
||||
})
|
||||
};
|
||||
let want = run(Policy::reference()).ok();
|
||||
for p in FAST {
|
||||
let got = run(p).ok();
|
||||
if crate::int_kernels::testkit::overflow_checks() {
|
||||
assert!(want.is_none() && got.is_none(), "both must panic on overflow in a checked build ({p:?})");
|
||||
} else {
|
||||
assert!(want.is_some(), "an unchecked build wraps rather than panics");
|
||||
assert_eq!(got, want, "both must wrap identically ({p:?})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same with rows wide enough (16 outputs) for the SIMD lanes to engage: the
|
||||
/// lanes wrap instead of trapping, so they must not run where an accumulator can
|
||||
/// overflow — in a checked build the fast path must still panic like the reference.
|
||||
#[test]
|
||||
fn overflowing_accumulation_in_simd_width_rows_behaves_like_reference() {
|
||||
let (cin, h, w, cout, k) = (4usize, 3usize, 16usize, 4usize, 3usize);
|
||||
let x = vec![i32::MAX as i64; cin * h * w];
|
||||
let wt = vec![i32::MAX as i64; cin * cout * k * k];
|
||||
let (mult, bias) = (vec![1i64; cout], vec![0i64; cout]);
|
||||
let run = |p: Policy| {
|
||||
std::panic::catch_unwind(|| with_policy(p, || conv(&x, cin, h, w, &wt, cout, k, 1, 1, &mult, &bias, 0))).ok()
|
||||
};
|
||||
let want = run(Policy::reference());
|
||||
for p in FAST {
|
||||
let got = run(p);
|
||||
if crate::int_kernels::testkit::overflow_checks() {
|
||||
assert!(want.is_none() && got.is_none(), "both must panic on overflow in a checked build ({p:?})");
|
||||
} else {
|
||||
assert_eq!(got, want, "both must wrap identically ({p:?})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overflow only at a position the reference accumulates into and then crops.
|
||||
/// A 1×1 input with a 3×3 kernel, stride 1 and pad 1 keeps only the centre of
|
||||
/// the reference's 3×3 raw buffer. The centre is `2·2^40·1 = 2^41`. Each corner
|
||||
/// is `2·2^40·2^22 = 2^63` and overflows `i64` on its second term, which the
|
||||
/// fast kernel never computes. So the reference panics in an overflow-checked
|
||||
/// build and the fast path must too (these operands are outside its no-overflow
|
||||
/// bound, so it hands them to the reference). Without the checks both give 2^41.
|
||||
#[test]
|
||||
fn overflow_only_at_a_cropped_border_behaves_like_reference() {
|
||||
use crate::int_kernels::testkit::overflow_checks;
|
||||
let (cin, cout, k) = (2usize, 1usize, 3usize);
|
||||
let x = vec![1i64 << 40; cin];
|
||||
let mut wt = vec![0i64; cin * cout * k * k];
|
||||
for tap in wt.chunks_mut(k * k) {
|
||||
for c in [0, 2, 6, 8] {
|
||||
tap[c] = 1 << 22;
|
||||
}
|
||||
tap[4] = 1;
|
||||
}
|
||||
let (mult, bias) = (vec![1i64; cout], vec![0i64; cout]);
|
||||
let run = |p: Policy| {
|
||||
std::panic::catch_unwind(|| with_policy(p, || conv_t(&x, cin, 1, 1, &wt, cout, k, 1, 1, 0, &mult, &bias, 0))).ok()
|
||||
};
|
||||
let want = run(Policy::reference());
|
||||
if overflow_checks() {
|
||||
assert!(want.is_none(), "the reference overflows at a cropped corner in a checked build");
|
||||
} else {
|
||||
assert_eq!(want, Some((vec![1i64 << 41], 1, 1)), "the kept centre is unaffected by the wrapped corners");
|
||||
}
|
||||
for p in FAST {
|
||||
assert_eq!(run(p), want, "fast must match the reference, panic included ({p:?})");
|
||||
}
|
||||
}
|
||||
|
||||
/// Malformed geometry goes to the reference, so it fails exactly as before.
|
||||
#[test]
|
||||
fn malformed_geometry_is_left_to_the_reference() {
|
||||
let x = vec![1i64; 4];
|
||||
let short_w = vec![1i64; 3]; // needs 1·1·3·3
|
||||
let r = std::panic::catch_unwind(|| conv_t(&x, 1, 2, 2, &short_w, 1, 3, 1, 0, 0, &[1], &[0], 0));
|
||||
let q = std::panic::catch_unwind(|| {
|
||||
with_policy(Policy::reference(), || conv_t(&x, 1, 2, 2, &short_w, 1, 3, 1, 0, 0, &[1], &[0], 0))
|
||||
});
|
||||
assert_eq!(r.is_err(), q.is_err());
|
||||
}
|
||||
|
||||
/// 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 integers are 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;
|
||||
let (mult, bias, shift) = ([3i64], [7i64], 1u32);
|
||||
// Regular, 1×1 kernel, pad 5: (channels, input side, stride, expected
|
||||
// fast-kernel runs, output). Only output (1, 1) can read the input.
|
||||
for (cin, h, stride, runs, out) in [
|
||||
(1usize, 2usize, 6usize, 1u64, [7i64, 7, 7, ((10 * 4 * 3) >> 1) + 7]), // at the factor
|
||||
(1, 2, 7, 0, [7; 4]), // beyond it
|
||||
(3, 3, 7, 0, [7, 7, 7, (((10 * 9 + 20 * 18 + 30 * 27) * 3) >> 1) + 7]), // beyond it
|
||||
] {
|
||||
let x: Vec<i64> = (1..=(cin * h * h) as i64).collect();
|
||||
let wt: Vec<i64> = (1..=cin as i64).map(|c| 10 * c).collect();
|
||||
let want = reference::conv(&x, cin, h, h, &wt, 1, 1, stride, 5, &mult, &bias, shift);
|
||||
assert_eq!(want, (out.to_vec(), 2, 2), "reference, {cin}×{h}² stride {stride}");
|
||||
for p in FAST {
|
||||
let got = fast_conv_runs(|| with_policy(p, || conv(&x, cin, h, h, &wt, 1, 1, stride, 5, &mult, &bias, shift)));
|
||||
assert_eq!(got, (want.clone(), runs), "{cin}×{h}² stride {stride} {p:?}");
|
||||
}
|
||||
}
|
||||
// Transposed, 1×1 input and kernel, stride 1: (channels, pad, output pad,
|
||||
// expected fast-kernel runs, output side). No tap reaches the kept output.
|
||||
for (cin, pad, opad, runs, side) in [(2usize, 6usize, 13usize, 1u64, 2usize), (2, 7, 15, 0, 2), (1, 6, 12, 0, 1)] {
|
||||
let (x, wt) = (vec![5i64; cin], vec![3i64; cin]);
|
||||
let want = reference::conv_t(&x, cin, 1, 1, &wt, 1, 1, 1, pad, opad, &mult, &bias, shift);
|
||||
assert_eq!(want, (vec![7; side * side], side, side), "reference, pad {pad} opad {opad}");
|
||||
for p in FAST {
|
||||
let got = fast_conv_runs(|| with_policy(p, || conv_t(&x, cin, 1, 1, &wt, 1, 1, 1, pad, opad, &mult, &bias, shift)));
|
||||
assert_eq!(got, (want.clone(), runs), "pad {pad} opad {opad} {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`run_net_checked`]: it computes [`run_net`]'s integers wherever it runs, runs
|
||||
/// right up to each bound, and refuses — without panicking — past it and on
|
||||
/// malformed layers, including an input on which `run_net` itself overflows.
|
||||
#[cfg(test)]
|
||||
mod checked {
|
||||
use super::*;
|
||||
use super::legacy::{igdn_check, run_net_checked};
|
||||
use super::legacy::{apply_layer, igdn_check, run_net, run_net_checked};
|
||||
use crate::int_kernels::testkit::Rng;
|
||||
|
||||
fn conv1(weight: Vec<i64>, mult: i64, bias: i64) -> IntLayer {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@
|
|||
//! 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. `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
|
||||
//! the executable specification. `int_synth`, and the `WHM1` and `WAM1` networks
|
||||
//! (`int_hyper_synth`, `int_splat`, `int_audio`, `int_mlicpp`, `int_world`), reach the
|
||||
//! convolution and IGDN kernels through the integer operator set (`int_ops`): the
|
||||
//! interpreter lends them its convolutions and inverse GDNs under a fast policy and
|
||||
//! computes every operator itself under the reference policy, so for them 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
|
||||
|
|
@ -1627,10 +1629,10 @@ mod tests {
|
|||
// and far inside the allocation cap, so the proportion is the only check
|
||||
// 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
|
||||
// and int_transformer on these shapes, int_audio on 1-D ones; int_synth's
|
||||
// operators, declined, are the interpreter's).
|
||||
// reference, with the same integers, is pinned beside each dispatch:
|
||||
// int_transformer's `padding_beyond_the_pad_factor_runs_the_reference` on these
|
||||
// shapes, and for the operators the integer operator set's interpreter computes
|
||||
// where the kernel declines, `int_ops`' `padding_beyond_the_pad_factor_runs_on_the_interpreter`.
|
||||
use testkit::{fast_conv_runs, FAST};
|
||||
let measure = |g: &ConvGeom| {
|
||||
let lay = Layout::new(g).unwrap();
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ fn latent(
|
|||
pub mod legacy {
|
||||
use super::*;
|
||||
use crate::int_hyper_synth::legacy::run_net_checked;
|
||||
use crate::int_hyper_synth::to_rgb8;
|
||||
use crate::int_hyper_synth::legacy::to_rgb8;
|
||||
|
||||
/// Run `net` on `x` (`[c, h, w]`) through [`run_net_checked`], after [`preflight`]:
|
||||
/// the payload is refused if its values could take a layer out of the exact integer
|
||||
|
|
|
|||
|
|
@ -524,6 +524,96 @@ mod equivalence {
|
|||
assert!(fast > 800, "the fast kernel ran only {fast} times");
|
||||
}
|
||||
|
||||
/// The committed `wai.world.model` renderer's sets (one agent, two agents) through their
|
||||
/// lowering `world/1`: under every fast policy each convolution of `T`, every time it
|
||||
/// runs (once per row), and each transposed convolution and inverse GDN of `g_s`, every
|
||||
/// time it runs (once per frame), run on the fast kernels, and every frame's bytes are
|
||||
/// the interpreter's. `T`'s planes, its leaky ReLU and clamp, and the RGB8 step run on
|
||||
/// the interpreter under every policy.
|
||||
#[cfg(feature = "world_int")]
|
||||
#[test]
|
||||
fn the_world_renderer_runs_its_networks_on_the_fast_kernels() {
|
||||
use crate::int_kernels::testkit::fast_igdn_runs;
|
||||
use wai_ops::graph::opcode;
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../wai-rs/tests/vectors");
|
||||
for (d, js, t, g) in [("int_world", "world.json", "world_T.bin", "world_gs.bin"), ("int_world2", "world2.json", "world2_T.bin", "world2_gs.bin")] {
|
||||
let read = |f: &str| std::fs::read(dir.join(d).join(f)).unwrap();
|
||||
let j: serde_json::Value = serde_json::from_slice(&read(js)).unwrap();
|
||||
let (c, lat, afrac) = (j["c"].as_u64().unwrap() as usize, j["lat"].as_u64().unwrap() as usize, j["afrac"].as_u64().unwrap() as u32);
|
||||
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|v| v.as_i64().unwrap()).collect();
|
||||
let mv = |a: &serde_json::Value| crate::int_world::ACTIONS[a.as_u64().unwrap() as usize];
|
||||
let rows: Vec<Vec<i64>> = match j.get("pairs") {
|
||||
Some(p) => p.as_array().unwrap().iter().map(|p| {
|
||||
let ((xa, ya), (xb, yb)) = (mv(&p[0]), mv(&p[1]));
|
||||
[xa, ya, xb, yb].iter().map(|v| v << afrac).collect()
|
||||
}).collect(),
|
||||
None => j["actions"].as_array().unwrap().iter().map(|a| {
|
||||
let (x, y) = mv(a);
|
||||
vec![x << afrac, y << afrac, 0]
|
||||
}).collect(),
|
||||
};
|
||||
let w = crate::int_world::ops::load(&IntModel::from_bytes(&read(t)).unwrap(), &IntModel::from_bytes(&read(g)).unwrap(), c).unwrap();
|
||||
let count = |l: &wai_ops::Loaded, codes: &[u8]| l.graph().ops.iter().filter(|o| codes.contains(&o.code)).count() as u64;
|
||||
let (steps, frames) = (rows.len() as u64, rows.len() as u64 + 1);
|
||||
let convs = [opcode::CONV, opcode::CONV_TRANSPOSE];
|
||||
let want_convs = steps * count(w.t(), &convs) + frames * count(w.g_s(), &convs);
|
||||
let want_igdns = steps * count(w.t(), &[opcode::IGDN]) + frames * count(w.g_s(), &[opcode::IGDN]);
|
||||
assert!(want_convs > 0 && want_igdns > 0, "{d}: the committed set has convolutions and inverse GDNs");
|
||||
let render = |pol: Policy| fast_igdn_runs(|| fast_conv_runs(|| with_policy(pol, || w.decode_planes(&z0, &rows, lat).unwrap().frames)));
|
||||
let ((want, none), no_igdn) = render(Policy::reference());
|
||||
assert_eq!((none, no_igdn), (0, 0), "{d}: the reference policy runs no fast kernel");
|
||||
for pol in FAST {
|
||||
let ((got, runs), igdns) = render(pol);
|
||||
assert!(got == want, "{d} {pol:?}: the frames differ from the interpreter alone");
|
||||
assert_eq!((runs, igdns), (want_convs, want_igdns), "{d} {pol:?}: every convolution and inverse GDN runs on the fast kernels");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `int_hyper_synth`'s check that geometry past the fast kernels' pad factor is left to
|
||||
/// the scalar kernels, held to the interpreter instead: a convolution or transposed
|
||||
/// convolution whose padded input would be more than `PAD_FACTOR` times what the
|
||||
/// operator reads and writes is declined by the fast kernel, and the interpreter computes
|
||||
/// it, to the same integers. 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_on_the_interpreter() {
|
||||
let rq = || Rq { round: Round::Floor, bias_kind: BiasKind::Post, m_kind: 2, s_kind: 1, m_bytes: 8, b_bytes: 8, mult: vec![3], shift: vec![1], bias: vec![7] };
|
||||
let run = |g: Graph, x: Tensor| {
|
||||
let l = load(&g).expect("the graph loads");
|
||||
let want = with_policy(Policy::reference(), || run1(&l, x.clone(), &[])).unwrap();
|
||||
let mut runs = Vec::new();
|
||||
for p in FAST {
|
||||
let (got, n) = fast_conv_runs(|| with_policy(p, || run1(&l, x.clone(), &[])));
|
||||
assert_eq!(got.as_ref(), Ok(&want), "{p:?}");
|
||||
runs.push(n);
|
||||
}
|
||||
(want, runs)
|
||||
};
|
||||
// Regular, 1×1 kernel, pad 5: (channels, input side, stride, fast-kernel runs per
|
||||
// policy, output). Only output (1, 1) can read the input.
|
||||
for (cin, h, stride, fast, out) in [
|
||||
(1u32, 2u32, 6u32, 1u64, [7i64, 7, 7, ((10 * 4 * 3) >> 1) + 7]), // at the factor
|
||||
(1, 2, 7, 0, [7; 4]), // beyond it
|
||||
(3, 3, 7, 0, [7, 7, 7, (((10 * 9 + 20 * 18 + 30 * 27) * 3) >> 1) + 7]), // beyond it
|
||||
] {
|
||||
let x: Vec<i64> = (1..=(cin * h * h) as i64).collect();
|
||||
let wt: Vec<i64> = (1..=cin as i64).map(|c| 10 * c).collect();
|
||||
let p = Params::Conv { cout: 1, kh: 1, kw: 1, sh: stride, sw: stride, ph: 5, pw: 5, groups: 1, w_bytes: 8, rq: rq(), w: wt };
|
||||
let (got, runs) = run(graph(TensorRec::check(cin, -1000, 1000), opcode::CONV, 1, p), Tensor::new(cin, h, h, x));
|
||||
assert_eq!((got.data, got.h, got.w), (out.to_vec(), 2, 2), "{cin}×{h}² stride {stride}");
|
||||
assert_eq!(runs, vec![fast; FAST.len()], "{cin}×{h}² stride {stride}: the fast kernel's runs");
|
||||
}
|
||||
// Transposed, 1×1 input and kernel, stride 1: (channels, pad, output pad, fast-kernel
|
||||
// runs per policy, output side). No tap reaches the kept output.
|
||||
for (cin, pad, opad, fast, side) in [(2u32, 6u32, 13u32, 1u64, 2u32), (2, 7, 15, 0, 2), (1, 6, 12, 0, 1)] {
|
||||
let p = Params::ConvT { cout: 1, kh: 1, kw: 1, sh: 1, sw: 1, ph: pad, pw: pad, oph: opad, opw: opad, w_bytes: 8, rq: rq(), w: vec![3; cin as usize] };
|
||||
let (got, runs) = run(graph(TensorRec::check(cin, -1000, 1000), opcode::CONV_TRANSPOSE, 1, p), Tensor::new(cin, 1, 1, vec![5; cin as usize]));
|
||||
assert_eq!((got.data, got.h, got.w), (vec![7; (side * side) as usize], side, side), "pad {pad} opad {opad}");
|
||||
assert_eq!(runs, vec![fast; FAST.len()], "pad {pad} opad {opad}: the fast kernel's runs");
|
||||
}
|
||||
}
|
||||
|
||||
/// A value outside an input's `check` range is refused by the interpreter before any
|
||||
/// kernel runs, under every policy alike.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -236,8 +236,8 @@ pub mod reference {
|
|||
out
|
||||
}
|
||||
|
||||
/// Grouped / depthwise integer convolution — the one op the dense conv in
|
||||
/// [`crate::int_hyper_synth::run_net`] lacks (5 DWConv modules in the Restormer
|
||||
/// Grouped / depthwise integer convolution — the one op the dense convolutions of the
|
||||
/// `WHM1` networks lack (5 DWConv modules in the Restormer
|
||||
/// synthesis need it). `groups` divides both `cin` and `cout`; output channel
|
||||
/// `oc` (in group `g = oc/(cout/groups)`) sums only over its group's input
|
||||
/// channels. Depthwise ⇔ `groups == cin == cout` (weight `[cout, 1, k, k]`).
|
||||
|
|
|
|||
|
|
@ -7,14 +7,27 @@
|
|||
//! frame_0 = g_s(z_0); z_{t+1} = clip( T( z_t ‖ action_t ) ); frame_{t+1} = g_s(z_{t+1})
|
||||
//! ```
|
||||
//! Both the transition `T` and the synthesis `g_s` are [`IntModel`]s (the int_hyper
|
||||
//! WHM1 format); the action is folded in as `A` constant input planes. The whole
|
||||
//! decode is i64 — integer convs, integer IGDN, an arithmetic-shift requant — so
|
||||
//! given `(T, g_s, z_0, action trace)` every frame is byte-identical on every
|
||||
//! machine. Paired with [`crate::world_model`]'s signed, replay-equivalent action
|
||||
//! trace, both the trace AND the generated frames hash-converge: a generative
|
||||
//! interactive world you can put in a receipt.
|
||||
//! WHM1 format); the action is folded in as `A` constant input planes, `A` being `T`'s
|
||||
//! input channels less the latent's. The renderer runs through the integer operator set
|
||||
//! (`extensions/integer-operators.md`): `T` and `g_s` lowered by `world/1` (§13.5,
|
||||
//! informative) and loaded under `acc64` with their bounds certificates ([`ops`]), so
|
||||
//! given `(T, g_s, z_0, action trace)` every frame is byte-identical on every machine,
|
||||
//! and a value that could take a layer outside its exact integer range is refused
|
||||
//! (`check` ranges) rather than computed. Paired with [`crate::world_model`]'s signed,
|
||||
//! replay-equivalent action trace, both the trace AND the generated frames
|
||||
//! hash-converge: a generative interactive world you can put in a receipt.
|
||||
//!
|
||||
//! The renderer is informative: `wai.world.model`'s payload is its action trace, and no
|
||||
//! payload text yet binds a trace to a renderer's set. Its refusals ([`Refused`]) use the
|
||||
//! integer payloads' codes: `model_mismatch` for a set that breaks `world/1`'s rules, a row
|
||||
//! without one value per action plane, or a network that does not fit the latent;
|
||||
//! `geometry_mismatch` for a latent that is not `C·lat·lat` values; `field_range` for a side
|
||||
//! of 0, a move whose plane value leaves `i64`, or a value outside a `check` range;
|
||||
//! `too_large` for a latent past `2^26` values.
|
||||
|
||||
use crate::int_hyper_synth::{decode as synth_decode, fnv1a, run_net, IntModel};
|
||||
use crate::int_hyper_synth::{fnv1a, IntModel, Refused};
|
||||
|
||||
pub use ops::{load, World};
|
||||
|
||||
/// The reference action set (must match the export tool): stay, +x, −x, +y, −y.
|
||||
/// `(dx, dy)` are folded into the transition as constant planes (scaled to `2^afrac`).
|
||||
|
|
@ -44,79 +57,418 @@ impl WorldClip {
|
|||
}
|
||||
}
|
||||
|
||||
/// One integer transition step from an explicit set of action **planes**:
|
||||
/// `z_{t+1} = clip( T( z_t ‖ plane₀ ‖ … ‖ plane_{A-1} ) )`. Each `planes_q[k]` is a
|
||||
/// constant value (already at `2^afrac`) broadcast to one `lat·lat` input plane, so
|
||||
/// the number of planes (`A`) is whatever the model expects — `A=3` for a single
|
||||
/// agent (dx, dy, btn), `A=4` for two agents (dxA, dyA, dxB, dyB), etc. The next
|
||||
/// latent is clamped to `[-2^afrac, 2^afrac]` (the bound the model trained under).
|
||||
/// The generalized core every higher-level step path calls.
|
||||
pub fn step_planes(t_model: &IntModel, z: &[i64], planes_q: &[i64], c: usize, lat: usize) -> Vec<i64> {
|
||||
let one = 1i64 << t_model.afrac;
|
||||
let plane = lat * lat;
|
||||
let na = planes_q.len();
|
||||
let mut inp = Vec::with_capacity((c + na) * plane);
|
||||
inp.extend_from_slice(z);
|
||||
for &v in planes_q {
|
||||
inp.extend(std::iter::repeat_n(v, plane));
|
||||
/// Render a clip from rows of action plane values at `2^afrac`, one value per plane of
|
||||
/// `T` (§13.5's driver): the set loaded ([`load`]), then [`World::decode_planes`]. A
|
||||
/// merged multi-actor log maps to one row per op (the acting agent's planes filled,
|
||||
/// the others 0), so participants co-driving one world render the same frames.
|
||||
pub fn decode_planes(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], rows: &[Vec<i64>], c: usize, lat: usize) -> Result<WorldClip, Refused> {
|
||||
ops::decode_planes(t_model, gs_model, z0, rows, c, lat)
|
||||
}
|
||||
|
||||
/// Render a clip from a single agent's moves `(dx, dy)` ([`World::decode_deltas`]): the
|
||||
/// path of a merged, canonically ordered multi-actor log from
|
||||
/// [`crate::world_model::replay`], which renders to one byte-exact frame sequence on every
|
||||
/// machine, whatever order the ops arrived in.
|
||||
pub fn decode_deltas(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], deltas: &[(i64, i64)], c: usize, lat: usize) -> Result<WorldClip, Refused> {
|
||||
ops::decode_deltas(t_model, gs_model, z0, deltas, c, lat)
|
||||
}
|
||||
|
||||
/// Render a clip from indices into [`ACTIONS`] (past its end, the last).
|
||||
pub fn decode(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], actions: &[usize], c: usize, lat: usize) -> Result<WorldClip, Refused> {
|
||||
ops::decode(t_model, gs_model, z0, actions, c, lat)
|
||||
}
|
||||
|
||||
/// The direct renderer, as `wai.world.model`'s integer renderer ran before it rendered
|
||||
/// through the integer operator set: `T` and `g_s` run layer by layer by
|
||||
/// [`crate::int_hyper_synth::legacy::run_net`], with no check of the values they are given.
|
||||
/// Test-only (feature `legacy_int`): the tests hold the renderer to it, 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::*;
|
||||
use crate::int_hyper_synth::legacy::{decode as synth_decode, run_net};
|
||||
|
||||
/// One integer transition step from an explicit set of action **planes**:
|
||||
/// `z_{t+1} = clip( T( z_t ‖ plane₀ ‖ … ‖ plane_{A-1} ) )`. Each `planes_q[k]` is a
|
||||
/// constant value (already at `2^afrac`) broadcast to one `lat·lat` input plane, so
|
||||
/// the number of planes (`A`) is whatever the model expects — `A=3` for a single
|
||||
/// agent (dx, dy, btn), `A=4` for two agents (dxA, dyA, dxB, dyB), etc. The next
|
||||
/// latent is clamped to `[-2^afrac, 2^afrac]` (the bound the model trained under).
|
||||
/// The generalized core every higher-level step path calls.
|
||||
pub fn step_planes(t_model: &IntModel, z: &[i64], planes_q: &[i64], c: usize, lat: usize) -> Vec<i64> {
|
||||
let one = 1i64 << t_model.afrac;
|
||||
let plane = lat * lat;
|
||||
let na = planes_q.len();
|
||||
let mut inp = Vec::with_capacity((c + na) * plane);
|
||||
inp.extend_from_slice(z);
|
||||
for &v in planes_q {
|
||||
inp.extend(std::iter::repeat_n(v, plane));
|
||||
}
|
||||
let (mut zn, _, _, _) = run_net(t_model, &inp, c + na, lat, lat);
|
||||
for v in &mut zn {
|
||||
*v = (*v).clamp(-one, one);
|
||||
}
|
||||
zn
|
||||
}
|
||||
let (mut zn, _, _, _) = run_net(t_model, &inp, c + na, lat, lat);
|
||||
for v in &mut zn {
|
||||
*v = (*v).clamp(-one, one);
|
||||
|
||||
/// One integer transition step from raw `(dx, dy)` deltas (single agent, `A=3`).
|
||||
pub fn step_delta(t_model: &IntModel, z: &[i64], dx: i64, dy: i64, c: usize, lat: usize) -> Vec<i64> {
|
||||
let one = 1i64 << t_model.afrac;
|
||||
step_planes(t_model, z, &[dx * one, dy * one, 0], c, lat)
|
||||
}
|
||||
zn
|
||||
}
|
||||
|
||||
/// One integer transition step from raw `(dx, dy)` deltas (single agent, `A=3`).
|
||||
pub fn step_delta(t_model: &IntModel, z: &[i64], dx: i64, dy: i64, c: usize, lat: usize) -> Vec<i64> {
|
||||
let one = 1i64 << t_model.afrac;
|
||||
step_planes(t_model, z, &[dx * one, dy * one, 0], c, lat)
|
||||
}
|
||||
|
||||
/// One integer transition step from an action index into [`ACTIONS`].
|
||||
pub fn step(t_model: &IntModel, z: &[i64], action: usize, c: usize, lat: usize) -> Vec<i64> {
|
||||
let (dx, dy) = ACTIONS[action.min(ACTIONS.len() - 1)];
|
||||
step_delta(t_model, z, dx, dy, c, lat)
|
||||
}
|
||||
|
||||
/// Decode an interactive world clip from a list of `(dx, dy)` deltas: from `z0`,
|
||||
/// render `g_s(z0)`, then for each delta advance the integer transition and render.
|
||||
/// The convergence path — a **merged, canonically-ordered multi-actor** action log
|
||||
/// (from [`crate::world_model::replay`]) decodes to one byte-exact frame sequence on
|
||||
/// every machine, regardless of network arrival order. Pure i64.
|
||||
pub fn decode_deltas(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], deltas: &[(i64, i64)], c: usize, lat: usize) -> WorldClip {
|
||||
let mut z = z0.to_vec();
|
||||
let (f0, frame_px, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
let mut frames = Vec::with_capacity(deltas.len() + 1);
|
||||
frames.push(f0);
|
||||
for &(dx, dy) in deltas {
|
||||
z = step_delta(t_model, &z, dx, dy, c, lat);
|
||||
let (f, _, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
frames.push(f);
|
||||
/// One integer transition step from an action index into [`ACTIONS`].
|
||||
pub fn step(t_model: &IntModel, z: &[i64], action: usize, c: usize, lat: usize) -> Vec<i64> {
|
||||
let (dx, dy) = ACTIONS[action.min(ACTIONS.len() - 1)];
|
||||
step_delta(t_model, z, dx, dy, c, lat)
|
||||
}
|
||||
WorldClip { frames, frame_px }
|
||||
}
|
||||
|
||||
/// Decode an interactive world clip from an explicit sequence of action **plane**
|
||||
/// rows (each row `planes_q` at `2^afrac`, length = the model's `A`). The general
|
||||
/// path — single- or multi-agent. A merged multi-actor log maps to one plane row per
|
||||
/// op (the acting agent's slot filled, others zero), so two participants co-drive one
|
||||
/// world and every peer renders the IDENTICAL frames. Pure i64.
|
||||
pub fn decode_planes(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], rows: &[Vec<i64>], c: usize, lat: usize) -> WorldClip {
|
||||
let mut z = z0.to_vec();
|
||||
let (f0, frame_px, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
let mut frames = Vec::with_capacity(rows.len() + 1);
|
||||
frames.push(f0);
|
||||
for row in rows {
|
||||
z = step_planes(t_model, &z, row, c, lat);
|
||||
let (f, _, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
frames.push(f);
|
||||
/// Decode an interactive world clip from a list of `(dx, dy)` deltas: from `z0`,
|
||||
/// render `g_s(z0)`, then for each delta advance the integer transition and render.
|
||||
/// The convergence path — a **merged, canonically-ordered multi-actor** action log
|
||||
/// (from [`crate::world_model::replay`]) decodes to one byte-exact frame sequence on
|
||||
/// every machine, regardless of network arrival order. Pure i64.
|
||||
pub fn decode_deltas(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], deltas: &[(i64, i64)], c: usize, lat: usize) -> WorldClip {
|
||||
let mut z = z0.to_vec();
|
||||
let (f0, frame_px, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
let mut frames = Vec::with_capacity(deltas.len() + 1);
|
||||
frames.push(f0);
|
||||
for &(dx, dy) in deltas {
|
||||
z = step_delta(t_model, &z, dx, dy, c, lat);
|
||||
let (f, _, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
frames.push(f);
|
||||
}
|
||||
WorldClip { frames, frame_px }
|
||||
}
|
||||
|
||||
/// Decode an interactive world clip from an explicit sequence of action **plane**
|
||||
/// rows (each row `planes_q` at `2^afrac`, length = the model's `A`). The general
|
||||
/// path — single- or multi-agent. A merged multi-actor log maps to one plane row per
|
||||
/// op (the acting agent's slot filled, others zero), so two participants co-drive one
|
||||
/// world and every peer renders the IDENTICAL frames. Pure i64.
|
||||
pub fn decode_planes(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], rows: &[Vec<i64>], c: usize, lat: usize) -> WorldClip {
|
||||
let mut z = z0.to_vec();
|
||||
let (f0, frame_px, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
let mut frames = Vec::with_capacity(rows.len() + 1);
|
||||
frames.push(f0);
|
||||
for row in rows {
|
||||
z = step_planes(t_model, &z, row, c, lat);
|
||||
let (f, _, _) = synth_decode(gs_model, &z, c, lat, lat);
|
||||
frames.push(f);
|
||||
}
|
||||
WorldClip { frames, frame_px }
|
||||
}
|
||||
|
||||
/// Decode an interactive world clip from action indices (single-actor convenience).
|
||||
pub fn decode(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], actions: &[usize], c: usize, lat: usize) -> WorldClip {
|
||||
let deltas: Vec<(i64, i64)> = actions.iter().map(|&a| ACTIONS[a.min(ACTIONS.len() - 1)]).collect();
|
||||
decode_deltas(t_model, gs_model, z0, &deltas, c, lat)
|
||||
}
|
||||
|
||||
/// The direct renderer with the direct guard the other integer decoders ran
|
||||
/// ([`crate::int_hyper_synth::legacy::run_net_checked`]): before each layer of `T` and
|
||||
/// of `g_s`, the values present are checked against the bounds of that layer's
|
||||
/// arithmetic, and a layer that could leave the exact integer range is not run. The
|
||||
/// direct renderer itself never checked; this is what the `check` ranges of `world/1`
|
||||
/// restate (`extensions/integer-operators.md` §13.2), and what the tests hold the
|
||||
/// renderer through the operator set to. Test-only (`legacy_int`).
|
||||
#[cfg(any(test, feature = "legacy_int"))]
|
||||
pub mod checked {
|
||||
use super::*;
|
||||
use crate::int_hyper_synth::legacy::run_net_checked;
|
||||
use crate::int_hyper_synth::legacy::to_rgb8;
|
||||
|
||||
/// Where the guard refused: the network (`"T"` or `"g_s"`), the step (0 for frame
|
||||
/// 0, `k` for the `k`-th row's transition and the frame after it), the layer whose
|
||||
/// check failed, and whether that check was about the values (`out_of_range`) or
|
||||
/// about the layer itself.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Refusal {
|
||||
pub net: &'static str,
|
||||
pub step: usize,
|
||||
pub layer: usize,
|
||||
pub out_of_range: bool,
|
||||
}
|
||||
|
||||
/// [`super::decode_planes`], with each network run by `run_net_checked`.
|
||||
pub fn decode_planes(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], rows: &[Vec<i64>], c: usize, lat: usize) -> Result<WorldClip, Refusal> {
|
||||
let at = |net, step| move |r: crate::int_hyper_synth::NetRefusal| Refusal { net, step, layer: r.layer, out_of_range: r.out_of_range };
|
||||
let render = |z: &[i64], step: usize| -> Result<(Vec<u8>, usize), Refusal> {
|
||||
let (x, oc, oh, ow) = run_net_checked(gs_model, z, c, lat, lat).map_err(at("g_s", step))?;
|
||||
Ok((to_rgb8(&x, oc, oh, ow, gs_model.afrac), oh))
|
||||
};
|
||||
let one = 1i64 << t_model.afrac;
|
||||
let plane = lat * lat;
|
||||
let mut z = z0.to_vec();
|
||||
let (f0, frame_px) = render(&z, 0)?;
|
||||
let mut frames = vec![f0];
|
||||
for (k, row) in rows.iter().enumerate() {
|
||||
let mut inp = z;
|
||||
for &v in row {
|
||||
inp.extend(std::iter::repeat_n(v, plane));
|
||||
}
|
||||
let (mut zn, ..) = run_net_checked(t_model, &inp, c + row.len(), lat, lat).map_err(at("T", k + 1))?;
|
||||
for v in &mut zn {
|
||||
*v = (*v).clamp(-one, one);
|
||||
}
|
||||
z = zn;
|
||||
frames.push(render(&z, k + 1)?.0);
|
||||
}
|
||||
Ok(WorldClip { frames, frame_px })
|
||||
}
|
||||
}
|
||||
WorldClip { frames, frame_px }
|
||||
}
|
||||
|
||||
/// Decode an interactive world clip from action indices (single-actor convenience).
|
||||
pub fn decode(t_model: &IntModel, gs_model: &IntModel, z0: &[i64], actions: &[usize], c: usize, lat: usize) -> WorldClip {
|
||||
let deltas: Vec<(i64, i64)> = actions.iter().map(|&a| ACTIONS[a.min(ACTIONS.len() - 1)]).collect();
|
||||
decode_deltas(t_model, gs_model, z0, &deltas, c, lat)
|
||||
/// The renderer through the integer operator set (`extensions/integer-operators.md`):
|
||||
/// the transition `T` and the synthesis `g_s` lowered by `world/1` (§13.5, informative;
|
||||
/// §13.2 for each network's layers), each loaded under `acc64` with its bounds
|
||||
/// certificate ([`crate::int_ops`]).
|
||||
pub mod ops {
|
||||
use super::*;
|
||||
use crate::int_hyper_synth::{refused, RefusalKind, Refused, MAX_TENSOR};
|
||||
use wai_ops::{Code, Graph, Loaded, Refusal, Tensor};
|
||||
|
||||
/// A refusal of the operator set as the renderer reports it: a value outside a `check`
|
||||
/// range is `field_range`; the other codes keep their names (integer-payloads §8).
|
||||
fn kind(r: Refusal, what: &'static str) -> Refused {
|
||||
let k = match r.code {
|
||||
Code::FieldRange => RefusalKind::FieldRange,
|
||||
Code::TooLarge => RefusalKind::TooLarge,
|
||||
Code::GeometryMismatch => RefusalKind::GeometryMismatch,
|
||||
Code::StreamExhausted => RefusalKind::StreamExhausted,
|
||||
Code::StreamNoncanonical => RefusalKind::StreamNoncanonical,
|
||||
Code::ModelMismatch => RefusalKind::ModelMismatch,
|
||||
};
|
||||
refused(k, what)
|
||||
}
|
||||
|
||||
const SET: &str = "T and g_s do not lower by world/1 to integer-operator graphs that load under acc64";
|
||||
|
||||
/// Both networks in the form `wai-ops` lowers them from ([`crate::int_ops::whm1_net`]),
|
||||
/// and `C` as the lowering takes it.
|
||||
fn nets(t: &IntModel, g_s: &IntModel, c: usize) -> Result<(wai_ops::lower::Net, wai_ops::lower::Net, u32), Refused> {
|
||||
let c = u32::try_from(c).map_err(|_| refused(RefusalKind::ModelMismatch, SET))?;
|
||||
let tn = crate::int_ops::whm1_net(t).map_err(|r| kind(r, SET))?;
|
||||
Ok((tn, crate::int_ops::whm1_net(g_s).map_err(|r| kind(r, SET))?, c))
|
||||
}
|
||||
|
||||
/// The set's two graphs, `T` then `g_s` (with the RGB8 step), as `world/1` writes them
|
||||
/// from the models this crate parsed, for a latent of `c` channels
|
||||
/// ([`wai_ops::lower::world_net`]).
|
||||
pub fn lower(t: &IntModel, g_s: &IntModel, c: usize) -> Result<(Graph, Graph), Refused> {
|
||||
let (tn, gn, c) = nets(t, g_s, c)?;
|
||||
let mut roles = wai_ops::lower::world_net(&tn, &gn, c).map_err(|r| kind(r, SET))?.roles.into_iter().map(|(_, g)| g);
|
||||
match (roles.next(), roles.next()) {
|
||||
(Some(tg), Some(gg)) => Ok((tg, gg)),
|
||||
_ => Err(refused(RefusalKind::ModelMismatch, SET)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The set as the renderer holds it: `T` and `g_s` lowered by `world/1` and loaded
|
||||
/// under `acc64`, with the latent's channel count `C` and the number of action planes
|
||||
/// `A`, which is `T`'s input channel count less `C` (§13.5).
|
||||
pub struct World {
|
||||
pub(super) t: Loaded,
|
||||
pub(super) g_s: Loaded,
|
||||
pub(super) c: usize,
|
||||
pub(super) a: usize,
|
||||
pub(super) afrac: u32,
|
||||
}
|
||||
|
||||
impl World {
|
||||
/// `T`'s graph, loaded.
|
||||
pub fn t(&self) -> &Loaded {
|
||||
&self.t
|
||||
}
|
||||
/// `g_s`'s graph (ending with the RGB8 step), loaded.
|
||||
pub fn g_s(&self) -> &Loaded {
|
||||
&self.g_s
|
||||
}
|
||||
/// `C`, the latent's channel count.
|
||||
pub fn channels(&self) -> usize {
|
||||
self.c
|
||||
}
|
||||
/// `A`, the action planes a row carries: `T`'s input channels less `C`.
|
||||
pub fn planes(&self) -> usize {
|
||||
self.a
|
||||
}
|
||||
/// The fixed-point scale both networks share.
|
||||
pub fn afrac(&self) -> u32 {
|
||||
self.afrac
|
||||
}
|
||||
|
||||
/// A row carries one value per action plane (§13.5's load checks, `model_mismatch`).
|
||||
fn row_ok(&self, row: &[i64]) -> Result<(), Refused> {
|
||||
if row.len() != self.a {
|
||||
return Err(refused(RefusalKind::ModelMismatch, "a row does not carry one value per action plane of T"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The elements of a latent `[C, lat, lat]`: a side of 0 is `field_range`, and a
|
||||
/// latent past [`MAX_TENSOR`] elements `too_large`.
|
||||
fn latent_len(&self, lat: usize) -> Result<usize, Refused> {
|
||||
if lat == 0 {
|
||||
return Err(refused(RefusalKind::FieldRange, "the latent's side is zero"));
|
||||
}
|
||||
let n = self.c.checked_mul(lat).and_then(|v| v.checked_mul(lat)).filter(|&n| n <= MAX_TENSOR);
|
||||
n.ok_or(refused(RefusalKind::TooLarge, "the latent exceeds 2^26 elements"))
|
||||
}
|
||||
|
||||
/// The latent `z` as a tensor `[C, lat, lat]` ([`World::latent_len`]); `z` of
|
||||
/// another length is `geometry_mismatch`.
|
||||
fn latent(&self, z: &[i64], lat: usize) -> Result<Tensor, Refused> {
|
||||
if z.len() != self.latent_len(lat)? {
|
||||
return Err(refused(RefusalKind::GeometryMismatch, "the latent is not C·lat·lat values"));
|
||||
}
|
||||
// C and lat are within 2^26, so each fits u32.
|
||||
Ok(Tensor::new(self.c as u32, lat as u32, lat as u32, z.to_vec()))
|
||||
}
|
||||
|
||||
/// The frame a latent `[C, lat, lat]` gives, `(channels, h, w)`, planned before
|
||||
/// any value is read (`extensions/integer-operators.md` §4): `T` planned on that
|
||||
/// shape must give it back, so that every transition reads what the one before it
|
||||
/// wrote; then `g_s` must plan on it. A network that does not (it reads another
|
||||
/// channel count, a size passes a cap, or a layer leaves no output) does not fit the
|
||||
/// latent: `model_mismatch`, as a set that does not fit a payload's shapes is.
|
||||
pub fn plan(&self, lat: usize) -> Result<(usize, usize, usize), Refused> {
|
||||
use wai_ops::plan::run_plan;
|
||||
self.latent_len(lat)?;
|
||||
let shape = [(self.c as u64, lat as u64, lat as u64)];
|
||||
let t = self.t.graph();
|
||||
let misfit = |what| move |_| refused(RefusalKind::ModelMismatch, what);
|
||||
let hw = run_plan(t, &shape, &vec![0; self.a]).map_err(misfit("T does not plan on the latent's shape"))?;
|
||||
let o = t.outputs[0] as usize;
|
||||
if (t.tensors[o].channels as u64, hw[o].0, hw[o].1) != shape[0] {
|
||||
return Err(refused(RefusalKind::ModelMismatch, "T does not give the latent's shape back"));
|
||||
}
|
||||
let g = self.g_s.graph();
|
||||
let hw = run_plan(g, &shape, &[]).map_err(misfit("g_s does not plan on the latent's shape"))?;
|
||||
let o = g.outputs[0] as usize;
|
||||
Ok((g.tensors[o].channels as usize, hw[o].0 as usize, hw[o].1 as usize))
|
||||
}
|
||||
|
||||
/// `T(z)` with `row`'s values as its scalars, then the clamp to `[−2^afrac,
|
||||
/// 2^afrac]` that ends `T`'s graph. A value outside a `check` range is `field_range`.
|
||||
fn run_t(&self, z: Tensor, row: &[i64]) -> Result<Tensor, Refused> {
|
||||
let shape = (z.c, z.h, z.w);
|
||||
let out = crate::int_ops::run1(&self.t, z, row).map_err(|r| kind(r, "a latent or plane value is outside a check range of T"))?;
|
||||
if (out.c, out.h, out.w) != shape {
|
||||
return Err(refused(RefusalKind::ModelMismatch, "T does not give the latent's shape back"));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `g_s(z)`, which ends with the RGB8 step, as row-major HWC bytes, with the
|
||||
/// frame's height and width. A value outside a `check` range is `field_range`.
|
||||
fn run_g(&self, z: Tensor) -> Result<(Vec<u8>, usize, usize), Refused> {
|
||||
let out = crate::int_ops::run1(&self.g_s, z, &[]).map_err(|r| kind(r, "a latent value is outside a check range of g_s"))?;
|
||||
let (c, h, w) = (out.c as usize, out.h as usize, out.w as usize);
|
||||
let plane = h * w;
|
||||
let mut hwc = vec![0u8; c * plane];
|
||||
for (k, &v) in out.data.iter().enumerate() {
|
||||
hwc[(k % plane) * c + k / plane] = v as u8;
|
||||
}
|
||||
Ok((hwc, h, w))
|
||||
}
|
||||
|
||||
/// One transition: `z` (`[C, lat, lat]`) becomes `T(z)`, with `row`'s values, one per
|
||||
/// action plane at `2^afrac`, as `T`'s scalars (§13.5).
|
||||
pub fn step(&self, z: &[i64], row: &[i64], lat: usize) -> Result<Vec<i64>, Refused> {
|
||||
self.row_ok(row)?;
|
||||
let x = self.latent(z, lat)?;
|
||||
Ok(self.run_t(x, row)?.data)
|
||||
}
|
||||
|
||||
/// [`World::step`] for a single agent's move `(dx, dy)` ([`World::move_row`]).
|
||||
pub fn step_delta(&self, z: &[i64], dx: i64, dy: i64, lat: usize) -> Result<Vec<i64>, Refused> {
|
||||
let row = self.move_row(dx, dy)?;
|
||||
self.step(z, &row, lat)
|
||||
}
|
||||
|
||||
/// [`World::step_delta`] for an index into [`ACTIONS`] (past its end, the last).
|
||||
pub fn step_action(&self, z: &[i64], action: usize, lat: usize) -> Result<Vec<i64>, Refused> {
|
||||
let (dx, dy) = ACTIONS[action.min(ACTIONS.len() - 1)];
|
||||
self.step_delta(z, dx, dy, lat)
|
||||
}
|
||||
|
||||
/// One frame: `g_s(z)` as row-major HWC bytes, with its height and width.
|
||||
pub fn render(&self, z: &[i64], lat: usize) -> Result<(Vec<u8>, usize, usize), Refused> {
|
||||
let x = self.latent(z, lat)?;
|
||||
self.run_g(x)
|
||||
}
|
||||
|
||||
/// The row of a single agent's move `(dx, dy)` (§13.5): `dequantize(dx, afrac, 0)`,
|
||||
/// `dequantize(dy, afrac, 0)` and 0. A set whose `A` is not 3 takes no such row
|
||||
/// (`model_mismatch`), and a move whose value leaves `i64` is `field_range`.
|
||||
pub fn move_row(&self, dx: i64, dy: i64) -> Result<Vec<i64>, Refused> {
|
||||
if self.a != 3 {
|
||||
return Err(refused(RefusalKind::ModelMismatch, "a single agent's move is a row of three planes, and T takes another count"));
|
||||
}
|
||||
let q = |d: i64| wai_ops::stage::dequantize(d, self.afrac, 0).map_err(|_| refused(RefusalKind::FieldRange, "a move's plane value leaves i64"));
|
||||
Ok(vec![q(dx)?, q(dy)?, 0])
|
||||
}
|
||||
|
||||
/// A clip (§13.5's driver): frame 0 is `g_s(z_0)`; for each row in order, `z`
|
||||
/// becomes `T(z)` with the row's values as the scalars, and the next frame is
|
||||
/// `g_s(z)`. Before the first frame: every row carries `A` values
|
||||
/// (`model_mismatch`), `z_0` is `[C, lat, lat]`, and both graphs plan on it
|
||||
/// ([`World::plan`]).
|
||||
pub fn decode_planes(&self, z0: &[i64], rows: &[Vec<i64>], lat: usize) -> Result<WorldClip, Refused> {
|
||||
for row in rows {
|
||||
self.row_ok(row)?;
|
||||
}
|
||||
let mut z = self.latent(z0, lat)?;
|
||||
self.plan(lat)?;
|
||||
let (mut frames, mut frame_px) = (Vec::with_capacity(rows.len() + 1), 0);
|
||||
for k in 0..=rows.len() {
|
||||
if k > 0 {
|
||||
z = self.run_t(z, &rows[k - 1])?;
|
||||
}
|
||||
// The last frame takes the latent, which no transition reads after it.
|
||||
let x = if k == rows.len() { std::mem::replace(&mut z, Tensor::new(0, 0, 0, Vec::new())) } else { z.clone() };
|
||||
let (f, h, _) = self.run_g(x)?;
|
||||
if k == 0 {
|
||||
frame_px = h;
|
||||
}
|
||||
frames.push(f);
|
||||
}
|
||||
Ok(WorldClip { frames, frame_px })
|
||||
}
|
||||
|
||||
/// [`World::decode_planes`] for a single agent's moves, each a row
|
||||
/// ([`World::move_row`]).
|
||||
pub fn decode_deltas(&self, z0: &[i64], deltas: &[(i64, i64)], lat: usize) -> Result<WorldClip, Refused> {
|
||||
let rows = deltas.iter().map(|&(dx, dy)| self.move_row(dx, dy)).collect::<Result<Vec<_>, _>>()?;
|
||||
self.decode_planes(z0, &rows, lat)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`World::decode_planes`] of the set `T`, `g_s` for a latent of `c` channels, loaded
|
||||
/// first ([`load`]).
|
||||
pub fn decode_planes(t: &IntModel, g_s: &IntModel, z0: &[i64], rows: &[Vec<i64>], c: usize, lat: usize) -> Result<WorldClip, Refused> {
|
||||
load(t, g_s, c)?.decode_planes(z0, rows, lat)
|
||||
}
|
||||
|
||||
/// [`World::decode_deltas`] of the set, loaded first ([`load`]).
|
||||
pub fn decode_deltas(t: &IntModel, g_s: &IntModel, z0: &[i64], deltas: &[(i64, i64)], c: usize, lat: usize) -> Result<WorldClip, Refused> {
|
||||
load(t, g_s, c)?.decode_deltas(z0, deltas, lat)
|
||||
}
|
||||
|
||||
/// [`decode_deltas`] for indices into [`ACTIONS`] (past its end, the last).
|
||||
pub fn decode(t: &IntModel, g_s: &IntModel, z0: &[i64], actions: &[usize], c: usize, lat: usize) -> Result<WorldClip, Refused> {
|
||||
let deltas: Vec<(i64, i64)> = actions.iter().map(|&a| ACTIONS[a.min(ACTIONS.len() - 1)]).collect();
|
||||
decode_deltas(t, g_s, z0, &deltas, c, lat)
|
||||
}
|
||||
|
||||
/// The load checks of `world/1` (§13.5, `model_mismatch`): both networks' `afrac` in
|
||||
/// `1..=30` and equal, `A ≥ 1`, and each network's rules (§13.2); then `T`, then `g_s`,
|
||||
/// lowered and loaded under `acc64` ([`wai_ops::lower::world_net_loaded`]: the result of
|
||||
/// loading the lowered graphs' bytes, without writing and parsing them again).
|
||||
pub fn load(t: &IntModel, g_s: &IntModel, c: usize) -> Result<World, Refused> {
|
||||
let (tn, gn, c32) = nets(t, g_s, c)?;
|
||||
let (tl, gl) = wai_ops::lower::world_net_loaded(tn, gn, c32, crate::int_ops::PROFILE).map_err(|r| kind(r, SET))?;
|
||||
let a = tl.graph().scalars.len();
|
||||
Ok(World { t: tl, g_s: gl, c, a, afrac: t.afrac })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
mod require_vectors;
|
||||
|
||||
use require_vectors::{load_all, load_json, InputDir};
|
||||
use wai::int_hyper_synth::{decode, fnv1a, IntModel};
|
||||
use wai::int_hyper_synth::{fnv1a, IntModel};
|
||||
|
||||
const PRODUCE: &str = "tools/wai_sota_vectors.sh <dir> (then WAI_SOTA_INT_DIR=<dir>, default /tmp/sota_int)";
|
||||
|
||||
|
|
@ -71,23 +71,21 @@ fn rust_integer_synthesis_reproduces_the_export_hash() {
|
|||
input["data"].as_array().unwrap().iter().map(|n| n.as_i64().unwrap()).collect();
|
||||
|
||||
let model = IntModel::from_json(&model);
|
||||
let (rgb, oh, ow) = decode(&model, &latent, c, h, w);
|
||||
// The synthesis through its lowering (whm1/1 with the RGB8 step), as the decode a sink
|
||||
// runs computes it, row-major HWC.
|
||||
let t = through_the_lowering(&model, true, latent, c, h, w);
|
||||
let (oh, ow) = (t.h as usize, t.w as usize);
|
||||
assert_eq!((oh, ow), (conf["out_h"].as_u64().unwrap() as usize, conf["out_w"].as_u64().unwrap() as usize));
|
||||
assert_eq!(rgb.len(), oh * ow * conf["out_c"].as_u64().unwrap() as usize);
|
||||
|
||||
assert_eq!(t.c as u64, conf["out_c"].as_u64().unwrap());
|
||||
let plane = oh * ow;
|
||||
let mut rgb = vec![0u8; t.data.len()];
|
||||
for (k, &v) in t.data.iter().enumerate() {
|
||||
rgb[(k % plane) * t.c as usize + k / plane] = v as u8;
|
||||
}
|
||||
let got = format!("{:016x}", fnv1a(&rgb));
|
||||
let want = conf["rgb_hash"].as_str().unwrap();
|
||||
println!("int_hyper integer synthesis: {oh}x{ow}, fnv1a {got} (psnr {:.2} dB)", conf["psnr_int"].as_f64().unwrap_or(0.0));
|
||||
assert_eq!(got, want, "Rust integer synthesis must reproduce the export's reconstruction byte-for-byte");
|
||||
// The same synthesis through its lowering (whm1/1 with the RGB8 step), as the
|
||||
// decode a sink runs computes it: the same bytes, row-major HWC.
|
||||
let t = through_the_lowering(&model, true, latent, c, h, w);
|
||||
let plane = t.h as usize * t.w as usize;
|
||||
let mut hwc = vec![0u8; t.data.len()];
|
||||
for (k, &v) in t.data.iter().enumerate() {
|
||||
hwc[(k % plane) * t.c as usize + k / plane] = v as u8;
|
||||
}
|
||||
assert_eq!(format!("{:016x}", fnv1a(&hwc)), want, "the synthesis through the integer operator set must reproduce the export's reconstruction");
|
||||
assert_eq!(got, want, "the integer synthesis through the integer operator set must reproduce the export's reconstruction byte-for-byte");
|
||||
}
|
||||
|
||||
/// One network of the pretrained set through its lowering `whm1/1` (with the RGB8 step
|
||||
|
|
@ -116,20 +114,17 @@ fn rust_integer_hyper_decoder_reproduces_the_export_hash() {
|
|||
);
|
||||
let z: Vec<i64> = input["data"].as_array().unwrap().iter().map(|n| n.as_i64().unwrap()).collect();
|
||||
let model = IntModel::from_json(&model);
|
||||
let (out, oc, oh, ow) = wai::int_hyper_synth::run_net(&model, &z, c, h, w);
|
||||
// The hyper-decoder through its lowering (whm1/1), as the decode a sink runs.
|
||||
let t = through_the_lowering(&model, false, z, c, h, w);
|
||||
let (oc, oh, ow) = (t.c as usize, t.h as usize, t.w as usize);
|
||||
assert_eq!((oc, oh, ow), (
|
||||
conf["out_c"].as_u64().unwrap() as usize,
|
||||
conf["out_h"].as_u64().unwrap() as usize,
|
||||
conf["out_w"].as_u64().unwrap() as usize));
|
||||
let bytes: Vec<u8> = out.iter().flat_map(|v| v.to_le_bytes()).collect();
|
||||
let bytes: Vec<u8> = t.data.iter().flat_map(|v| v.to_le_bytes()).collect();
|
||||
let got = format!("{:016x}", fnv1a(&bytes));
|
||||
println!("integer h_s: z_hat {c}x{h}x{w} -> sigma||mu {oc}x{oh}x{ow}, fnv1a {got}");
|
||||
assert_eq!(got, conf["params_hash"].as_str().unwrap(), "integer h_s must reproduce the export byte-for-byte");
|
||||
// The same hyper-decoder through its lowering (whm1/1), as the decode a sink runs.
|
||||
let t = through_the_lowering(&model, false, z, c, h, w);
|
||||
assert_eq!((t.c as usize, t.h as usize, t.w as usize), (oc, oh, ow));
|
||||
let bytes: Vec<u8> = t.data.iter().flat_map(|v| v.to_le_bytes()).collect();
|
||||
assert_eq!(format!("{:016x}", fnv1a(&bytes)), conf["params_hash"].as_str().unwrap(), "h_s through the integer operator set must reproduce the export");
|
||||
assert_eq!(got, conf["params_hash"].as_str().unwrap(), "h_s through the integer operator set must reproduce the export byte-for-byte");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
//! (serial, explicit multi-threaded tilings, automatic):
|
||||
//!
|
||||
//! * a 1×1 input upsampled by strides from 1 up to `usize::MAX`, through the
|
||||
//! transposed convolutions of int_hyper (`run_net`), int_synth
|
||||
//! transposed convolutions of int_hyper (a `WHM1` layer through its lowering), int_synth
|
||||
//! (`decode_from_spec`) and the JPEG-AI synthesis (`conv_transpose`);
|
||||
//! * a regular convolution whose stride reaches or exceeds its padded input
|
||||
//! (`run_net`, `conv_grouped`);
|
||||
//! (a `WHM1` layer through its lowering, `conv_grouped`);
|
||||
//! * padding far beyond the kernel, with output padding restoring a small output;
|
||||
//! * an input of two or more rows and columns whose stride and padding make the
|
||||
//! reference's own size arithmetic overflow (int_hyper, int_synth, JPEG-AI).
|
||||
|
|
@ -16,9 +16,10 @@
|
|||
//! 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,
|
||||
//! 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. int_audio decodes through its
|
||||
//! (integer-operators §13.1), and the int_hyper networks through theirs (`whm1/1`,
|
||||
//! §13.2): their fields are 32 bits and their run-time plan caps every size it forms at
|
||||
//! 2^26, so where a case is past either, every policy refuses it, where the scalar kernel
|
||||
//! the int_hyper decodes once ran panicked or computed. int_audio decodes through its
|
||||
//! lowering `wam1/1` too; its 1-D transposed convolution's edge geometry is tested
|
||||
//! through that graph under every policy (`int_audio` unit tests). CI runs this file
|
||||
//! beside `int_kernels_vectors` on every `wai-conformance` leg, with overflow checks
|
||||
|
|
@ -77,7 +78,18 @@ fn strides() -> Vec<usize> {
|
|||
|
||||
/// A one-layer int_hyper model.
|
||||
fn hyper_model(layer: serde_json::Value) -> wai::int_hyper_synth::IntModel {
|
||||
wai::int_hyper_synth::IntModel::from_json(&json!({ "afrac": 0, "gf": 0, "shift": 0, "layers": [layer] }))
|
||||
wai::int_hyper_synth::IntModel::from_json(&json!({ "afrac": 12, "gf": 16, "shift": 0, "layers": [layer] }))
|
||||
}
|
||||
|
||||
/// A one-layer int_hyper model run as the int_hyper decodes run their networks: lowered by
|
||||
/// `whm1/1`, loaded under `acc64`, and run by the integer operator set with this crate's
|
||||
/// fast kernels behind it (`int_ops`). The output and its shape `(c, h, w)`, or the
|
||||
/// refusal's code: a field past 32 bits, or a size the run-time plan caps.
|
||||
fn hyper_run(model: &wai::int_hyper_synth::IntModel, x: &[i64], c: usize, h: usize, w: usize) -> Result<(Vec<i64>, usize, usize, usize), wai_ops::Code> {
|
||||
let net = wai::int_ops::whm1_net(model).map_err(|r| r.code)?;
|
||||
let l = wai_ops::lower::whm1_net_loaded(net, false, wai_ops::Profile::Acc64).map_err(|r| r.code)?;
|
||||
let t = wai::int_ops::run1(&l, wai_ops::Tensor::new(c as u32, h as u32, w as u32, x.to_vec()), &[]).map_err(|r| r.code)?;
|
||||
Ok((t.data, t.c as usize, t.h as usize, t.w as usize))
|
||||
}
|
||||
|
||||
/// An int_synth payload: a 1×1 output image from one int8 latent (C = 1, S = 1).
|
||||
|
|
@ -116,10 +128,13 @@ fn a_1x1_input_upsampled_by_any_stride() {
|
|||
"type": "convT", "out_ch": 1, "k": 3, "stride": s, "pad": 1, "opad": 0,
|
||||
"weight": KERNEL, "mult": [1], "bias": [0]
|
||||
}));
|
||||
let got = same_under_every_policy(&format!("int_hyper convT, stride {s}"), || {
|
||||
wai::int_hyper_synth::run_net(&model, &[5], 1, 1, 1)
|
||||
});
|
||||
assert_eq!(got, (vec![25], 1, 1, 1), "int_hyper convT, stride {s}");
|
||||
let got = same_under_every_policy(&format!("int_hyper convT, stride {s}"), || hyper_run(&model, &[5], 1, 1, 1));
|
||||
if s <= u32::MAX as usize {
|
||||
assert_eq!(got, Ok((vec![25], 1, 1, 1)), "int_hyper convT, stride {s}");
|
||||
} else {
|
||||
// A WHM1 field is 32 bits: a model past it is not a WHM1 network.
|
||||
assert_eq!(got, Err(wai_ops::Code::ModelMismatch), "int_hyper convT, stride {s}");
|
||||
}
|
||||
|
||||
let (payload, spec) = (synth_payload(5), synth_spec(s, 1, 0, [0, 0, 0]));
|
||||
let got = same_under_every_policy(&format!("int_synth, stride {s}"), || {
|
||||
|
|
@ -159,10 +174,12 @@ fn a_regular_conv_whose_stride_exceeds_its_padded_input() {
|
|||
"type": "conv", "out_ch": 1, "k": 3, "stride": s, "pad": pad,
|
||||
"weight": KERNEL, "mult": [1], "bias": [0]
|
||||
}));
|
||||
let got = same_under_every_policy(&format!("int_hyper conv, pad {pad}, stride {s}"), || {
|
||||
wai::int_hyper_synth::run_net(&model, &x, 1, 5, 5)
|
||||
});
|
||||
assert_eq!(got, (vec![want], 1, 1, 1), "int_hyper conv, pad {pad}, stride {s}");
|
||||
let got = same_under_every_policy(&format!("int_hyper conv, pad {pad}, stride {s}"), || hyper_run(&model, &x, 1, 5, 5));
|
||||
if s <= u32::MAX as usize {
|
||||
assert_eq!(got, Ok((vec![want], 1, 1, 1)), "int_hyper conv, pad {pad}, stride {s}");
|
||||
} else {
|
||||
assert_eq!(got, Err(wai_ops::Code::ModelMismatch), "int_hyper conv, pad {pad}, stride {s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,8 +214,8 @@ fn padding_far_beyond_the_kernel() {
|
|||
let model = hyper_model(json!({
|
||||
"type": "conv", "out_ch": 1, "k": 1, "stride": 2 * p, "pad": p, "weight": [3], "mult": [1], "bias": [7]
|
||||
}));
|
||||
let got = same_under_every_policy("int_hyper conv, pad 2^12", || wai::int_hyper_synth::run_net(&model, &[5], 1, 1, 1));
|
||||
assert_eq!(got, (vec![7; 4], 1, 2, 2));
|
||||
let got = same_under_every_policy("int_hyper conv, pad 2^12", || hyper_run(&model, &[5], 1, 1, 1));
|
||||
assert_eq!(got, Ok((vec![7; 4], 1, 2, 2)));
|
||||
}
|
||||
|
||||
/// An int_synth payload: an `h`×`w` output image from a 2×2 int8 latent (C = 1,
|
||||
|
|
@ -254,7 +271,10 @@ fn jpegai_conv_transpose_where_the_reference_plane_size_overflows() {
|
|||
|
||||
#[test]
|
||||
fn int_hyper_convt_where_the_reference_plane_size_overflows() {
|
||||
// As above, through the int_hyper decoder.
|
||||
// As above, through a WHM1 layer's lowering, as the int_hyper decodes run it. The
|
||||
// scalar kernel they once ran panicked on both; through the operator set, every policy
|
||||
// refuses both before anything is allocated: 2^62 is past a WHM1 field's 32 bits, and
|
||||
// the u32 stride and pad give an uncropped side, (2 − 1)·(2^32 − 1) + 1, past the cap.
|
||||
let convt = |s: usize, p: usize| {
|
||||
hyper_model(json!({
|
||||
"type": "convT", "out_ch": 1, "k": 1, "stride": s, "pad": p, "opad": 0,
|
||||
|
|
@ -264,16 +284,12 @@ fn int_hyper_convt_where_the_reference_plane_size_overflows() {
|
|||
#[cfg(target_pointer_width = "64")]
|
||||
{
|
||||
let model = convt(1 << 62, 1 << 62);
|
||||
let got = same_outcome_under_every_policy("int_hyper convT, 3×3, stride = pad = 2^62", || {
|
||||
wai::int_hyper_synth::run_net(&model, &X, 1, 3, 3)
|
||||
});
|
||||
assert_eq!(got, None, "the reference panics");
|
||||
let got = same_outcome_under_every_policy("int_hyper convT, 3×3, stride = pad = 2^62", || hyper_run(&model, &X, 1, 3, 3));
|
||||
assert_eq!(got, Some(Err(wai_ops::Code::ModelMismatch)), "refused, not panicked");
|
||||
}
|
||||
let model = convt(S32, P32);
|
||||
let got = same_outcome_under_every_policy("int_hyper convT, 2×2, u32 stride and pad", || {
|
||||
wai::int_hyper_synth::run_net(&model, &X[..4], 1, 2, 2)
|
||||
});
|
||||
assert_eq!(got, None, "u32 fields: the reference panics");
|
||||
let got = same_outcome_under_every_policy("int_hyper convT, 2×2, u32 stride and pad", || hyper_run(&model, &X[..4], 1, 2, 2));
|
||||
assert!(matches!(got, Some(Err(_))), "u32 fields: refused by the cap, not panicked: {got:?}");
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ fn int_world_clips() {
|
|||
let (c, lat) = (j["c"].as_u64().unwrap() as usize, j["lat"].as_u64().unwrap() as usize);
|
||||
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|v| v.as_i64().unwrap()).collect();
|
||||
let actions: Vec<usize> = j["actions"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap() as usize).collect();
|
||||
let frames = same_under_every_policy("int_world", || decode(&t, &gs, &z0, &actions, c, lat).frames);
|
||||
let frames = same_under_every_policy("int_world", || decode(&t, &gs, &z0, &actions, c, lat).map(|c| c.frames).map_err(|r| r.kind)).expect("the committed clip renders");
|
||||
let clip = wai::int_world::WorldClip { frames, frame_px: 0 };
|
||||
assert_eq!(format!("{:016x}", clip.clip_hash()), j["clip_hash"].as_str().unwrap());
|
||||
sha256_sidecar::frames(&d, "world", RGB8, &clip.frames);
|
||||
|
|
@ -127,7 +127,7 @@ fn int_world_clips() {
|
|||
vec![a.0 * one, a.1 * one, b.0 * one, b.1 * one]
|
||||
})
|
||||
.collect();
|
||||
let frames = same_under_every_policy("int_world2", || decode_planes(&t, &gs, &z0, &rows, c, lat).frames);
|
||||
let frames = same_under_every_policy("int_world2", || decode_planes(&t, &gs, &z0, &rows, c, lat).map(|c| c.frames).map_err(|r| r.kind)).expect("the committed clip renders");
|
||||
let want = j["frame_hashes"].as_array().unwrap();
|
||||
assert_eq!(frames.len(), want.len());
|
||||
for (f, w) in frames.iter().zip(want) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
//! The decoders: `wai.neural.int_synth` (`int_synth/1`), `wai.splat.int_codec`
|
||||
//! (`whm1/1`), `wai.audio.int_codec` (`wam1/1`), `wai.neural.int_hyper` (`whm1/1`, its
|
||||
//! two networks; the keyframe of `wai.video.int_hyper` is the same decode) and
|
||||
//! `wai.neural.int_mlicpp` (`wmb1/1`, its three networks).
|
||||
//! `wai.neural.int_mlicpp` (`wmb1/1`, its three networks); and `wai.world.model`'s integer
|
||||
//! renderer (`world/1`, informative: `T` and `g_s`), against its direct renderer with the
|
||||
//! direct guard every other decoder ran.
|
||||
//!
|
||||
//! The same generators hold load step 8 to its oracle: the bounds pass, whose
|
||||
//! convolutions and inverse GDNs are in closed form, against the pass as it stood before
|
||||
|
|
@ -18,7 +20,7 @@
|
|||
//! 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(all(feature = "legacy_int", feature = "splat_int", feature = "audio_int"))]
|
||||
#![cfg(all(feature = "legacy_int", feature = "splat_int", feature = "audio_int", feature = "world_int"))]
|
||||
|
||||
use std::panic::catch_unwind;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -1865,6 +1867,326 @@ fn int_mlicpp_planned_breaks_are_new_refusals() {
|
|||
assert!(matches!((&new, &old), (Err(a), Some(Err(b))) if a == b && a.starts_with("model_mismatch")), "{new:?} {old:?}");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the world renderer (M6)
|
||||
|
||||
/// A random set of `wai.world.model`'s integer renderer, and a clip for it.
|
||||
#[derive(Clone, Debug)]
|
||||
struct WorldCase {
|
||||
t: Vec<u8>,
|
||||
g_s: Vec<u8>,
|
||||
c: usize,
|
||||
lat: usize,
|
||||
z0: Vec<i64>,
|
||||
rows: Vec<Vec<i64>>,
|
||||
}
|
||||
|
||||
/// The input channel count of a network's first layer of tag 0, 1 or 2.
|
||||
fn in_channels(n: &wai_ops::lower::Net) -> Option<u32> {
|
||||
use wai_ops::lower::NetLayer as N;
|
||||
n.layers.iter().find_map(|l| match l {
|
||||
N::ConvT { inc, .. } | N::Conv { inc, .. } => Some(*inc),
|
||||
N::Igdn { c, .. } => Some(*c),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The planned break of integer-operators §15.3 item 2 a world case falls under, if any,
|
||||
/// read from the set and the clip alone. The direct renderer checked nothing before it ran,
|
||||
/// so each check the renderer makes before its first frame is new: the set refused at load
|
||||
/// (`world/1`, loaded under `acc64`) for the `afrac` rule, for `A < 1`, for one of a
|
||||
/// `WHM1` network's rules (§13.2, named as [`load_break`] names them) or for layers that do
|
||||
/// not chain; a row that does not carry `A` values; and `T` that does not give the latent's
|
||||
/// shape back or `g_s` that does not plan on it. A value outside a `check` range is not a
|
||||
/// break: the direct guard refuses it alike.
|
||||
fn world_planned_break(cs: &WorldCase) -> Option<&'static str> {
|
||||
use wai_ops::plan::run_plan;
|
||||
let (tn, gn) = (wai_ops::lower::parse_whm1(&cs.t).ok()?, wai_ops::lower::parse_whm1(&cs.g_s).ok()?);
|
||||
let loaded = wai_ops::lower::world(&cs.t, &cs.g_s, cs.c as u32).and_then(|l| {
|
||||
let (t, g) = (wai::int_ops::load(l.get("T").unwrap())?, wai::int_ops::load(l.get("g_s").unwrap())?);
|
||||
Ok((t, g))
|
||||
});
|
||||
let (t, g) = match loaded {
|
||||
Ok(tg) => tg,
|
||||
Err(_) => {
|
||||
if [tn.afrac, gn.afrac].iter().any(|a| !(1..=30).contains(a)) || tn.afrac != gn.afrac {
|
||||
return Some("afrac outside 1..=30, or unequal");
|
||||
}
|
||||
if let Some(b) = load_break(&tn, wai_ops::lower::whm1(&cs.t, false)).or_else(|| load_break(&gn, wai_ops::lower::whm1(&cs.g_s, true))) {
|
||||
return Some(b);
|
||||
}
|
||||
if in_channels(&tn).is_some_and(|cin| cin <= cs.c as u32) {
|
||||
return Some("A < 1");
|
||||
}
|
||||
return Some("layers that do not chain, or a shift a layer cannot take");
|
||||
}
|
||||
};
|
||||
let a = t.graph().scalars.len();
|
||||
if cs.rows.iter().any(|r| r.len() != a) {
|
||||
return Some("a row that does not carry A values");
|
||||
}
|
||||
let shape = [(cs.c as u64, cs.lat as u64, cs.lat as u64)];
|
||||
let fits = run_plan(t.graph(), &shape, &vec![0; a]).is_ok_and(|hw| {
|
||||
let o = t.graph().outputs[0] as usize;
|
||||
(t.graph().tensors[o].channels as u64, hw[o].0, hw[o].1) == shape[0]
|
||||
});
|
||||
if !fits || run_plan(g.graph(), &shape, &[]).is_err() {
|
||||
return Some("T or g_s that does not fit the latent");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// [`same_size_layer`], its multipliers (when `unit` is set) about `2^shift` over the
|
||||
/// layer's terms and largest weight, so that its output keeps about its input's
|
||||
/// magnitude, of either sign. A random transition of such layers leaves the latent mostly
|
||||
/// inside `T`'s clamp, where a difference in `T`'s arithmetic shows.
|
||||
fn world_layer_conv(r: &mut Rng, c: u32, out: u32, shift: u32, unit: bool) -> Whm {
|
||||
let mut l = same_size_layer(r, c, out);
|
||||
if unit && let Whm::Conv { k, w, m, .. } = &mut l {
|
||||
let wmax = w.iter().map(|v| v.unsigned_abs() as i64).max().unwrap_or(1).max(1);
|
||||
let terms = (c * *k * *k) as i64;
|
||||
let g = ((1i64 << shift.min(62)) / (terms * wmax)).max(1);
|
||||
for v in m.iter_mut() {
|
||||
*v = if r.below(2) == 0 { g } else { -g };
|
||||
}
|
||||
}
|
||||
l
|
||||
}
|
||||
|
||||
/// A random world set and clip. `T`: `afrac` 12 mostly (one in sixteen anywhere in
|
||||
/// 1..=30, one in 32 at 0 or 31, and `g_s`'s unequal one in 32), a header `shift` of 8 to
|
||||
/// 23 (128 or more one in 64) and `gf` 16 (128 or more one in 64); `C` of 1 to 3 and `A`
|
||||
/// of 1 to 4 (0 one in 32); a first layer from `C + A` channels ([`world_layer_conv`],
|
||||
/// near unit gain three cases in four), up to one leaky ReLU (its slope below 1 three
|
||||
/// cases in four), ReLU or inverse GDN, and a last layer back to `C` (one in sixteen to
|
||||
/// `C + 1`, and one in sixteen strided by 2). `g_s`: 1 to 3 of [`splat_layers`]' layers
|
||||
/// from `C` channels (none one in 32, ReLUs only one in 32). The clip: a latent of side 1
|
||||
/// to 4; `z_0` within `±2^(afrac+1)` mostly, one case in four with a value at `g_s`'s
|
||||
/// input range or one past it and one in sixteen at an end of `i64`; 0 to 4 rows of moves
|
||||
/// `−1, 0, 1` at `2^afrac`, one row in eight with a value at `T`'s `x_0` range or one past
|
||||
/// it, one in 32 with a value up to `2^40`, and one in sixteen without `A` values.
|
||||
fn world_case(r: &mut Rng) -> WorldCase {
|
||||
let afrac = match r.below(32) {
|
||||
0 | 1 => 1 + r.below(30) as u32,
|
||||
2 => [0, 31][r.below(2) as usize],
|
||||
_ => 12,
|
||||
};
|
||||
let g_afrac = if r.below(32) == 0 { afrac ^ 1 } else { afrac };
|
||||
let shift = if r.below(64) == 0 { 128 + r.below(64) as u32 } else { 8 + r.below(16) as u32 };
|
||||
let gf = if r.below(64) == 0 { 128 + r.below(64) as u32 } else { 16 };
|
||||
let c = 1 + r.below(3) as u32;
|
||||
let a = if r.below(32) == 0 { 0 } else { 1 + r.below(4) as u32 };
|
||||
let h1 = 1 + r.below(4) as u32;
|
||||
let unit = r.below(4) != 0;
|
||||
let mut tl = vec![world_layer_conv(r, c + a, h1, shift, unit)];
|
||||
match r.below(4) {
|
||||
0 => {
|
||||
let f = r.below(16) as u32;
|
||||
tl.push(Whm::Leaky { q: if unit { r.below(1 << f) as i64 } else { r.signed(1 << 10) }, f });
|
||||
}
|
||||
1 => tl.push(Whm::Relu),
|
||||
2 => tl.push(Whm::Igdn {
|
||||
c: h1,
|
||||
g: (0..h1 * h1).map(|_| r.below(1 << 12) as i32).collect(),
|
||||
b: (0..h1).map(|_| (1 << 24) + r.below(1 << 20) as i64).collect(),
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
let out = if r.below(16) == 0 { c + 1 } else { c };
|
||||
let mut last = world_layer_conv(r, h1, out, shift, unit);
|
||||
if r.below(16) == 0 && let Whm::Conv { s, .. } = &mut last {
|
||||
*s = 2;
|
||||
}
|
||||
tl.push(last);
|
||||
let t = whm1_bytes(afrac, gf, shift, &tl);
|
||||
let n = if r.below(32) == 0 { 0 } else { 1 + r.below(3) as usize };
|
||||
let only_relu = r.below(32) == 0;
|
||||
let (gl, _) = splat_layers(r, c, n, only_relu);
|
||||
let g_s = whm1_bytes(g_afrac, 16, 8 + r.below(16) as u32, &gl);
|
||||
|
||||
let (c, lat) = (c as usize, 1 + r.below(4) as usize);
|
||||
let one = 1i64 << afrac.min(30);
|
||||
let ranges = wai_ops::lower::world(&t, &g_s, c as u32).ok().map(|l| {
|
||||
let (tg, gg) = (l.get("T").unwrap(), l.get("g_s").unwrap());
|
||||
(tg.tensors[1 + tg.scalars.len()].hi, gg.tensors[0].hi)
|
||||
});
|
||||
let edge = |r: &mut Rng, x: i64| [x, x.saturating_add(1), -x, -x.saturating_add(1)][r.below(4) as usize];
|
||||
let mut z0: Vec<i64> = (0..c * lat * lat).map(|_| r.signed(2 * one)).collect();
|
||||
let k = r.below(z0.len() as u64) as usize;
|
||||
match (r.below(16), ranges) {
|
||||
(0..=3, Some((_, xg))) => z0[k] = edge(r, xg),
|
||||
(4, _) => z0[k] = [i64::MIN, i64::MAX][r.below(2) as usize],
|
||||
_ => {}
|
||||
}
|
||||
let mut rows = Vec::new();
|
||||
for _ in 0..r.below(5) {
|
||||
let n = match r.below(16) {
|
||||
0 if a > 0 => a as usize - 1,
|
||||
1 => a as usize + 1,
|
||||
_ => a as usize,
|
||||
};
|
||||
let mut row: Vec<i64> = (0..n).map(|_| r.signed(1) * one).collect();
|
||||
if !row.is_empty() {
|
||||
let p = r.below(row.len() as u64) as usize;
|
||||
match (r.below(32), ranges) {
|
||||
(0..=3, Some((xt, _))) => row[p] = edge(r, xt),
|
||||
(4, _) => row[p] = r.signed(1 << 40),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
WorldCase { t, g_s, c, lat, z0, rows }
|
||||
}
|
||||
|
||||
/// Where a world graph refused, as the layer whose input check it is: for `T` (`A` fills,
|
||||
/// the concatenation, then the layers), the output of operator `A + ℓ`; for `g_s`, its
|
||||
/// input for layer 0 and the output of operator `ℓ − 1` for layer `ℓ`. `None` for a
|
||||
/// refusal that is no layer's check.
|
||||
fn world_layer(net: &str, a: usize, at: wai_ops::Site) -> Option<usize> {
|
||||
match (net, at) {
|
||||
("T", wai_ops::Site::Op(i)) if i as usize >= a => Some(i as usize - a),
|
||||
("g_s", wai_ops::Site::Input(0)) => Some(0),
|
||||
("g_s", wai_ops::Site::Op(i)) => Some(i as usize + 1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer through the operator set, as `World::decode_planes` runs it after its
|
||||
/// checks, one graph at a time, so that a refusal names its network, step and layer.
|
||||
fn world_traced(w: &wai::int_world::ops::World, cs: &WorldCase) -> Result<Vec<Vec<u8>>, (&'static str, usize, Option<usize>, wai_ops::Code)> {
|
||||
use wai_ops::Tensor;
|
||||
let a = w.planes();
|
||||
let g = |z: &Tensor, step: usize| {
|
||||
let out = wai::int_ops::run1(w.g_s(), z.clone(), &[]).map_err(|r| ("g_s", step, world_layer("g_s", a, r.at), r.code))?;
|
||||
let plane = (out.h * out.w) as usize;
|
||||
let mut hwc = vec![0u8; out.data.len()];
|
||||
for (k, &v) in out.data.iter().enumerate() {
|
||||
hwc[(k % plane) * out.c as usize + k / plane] = v as u8;
|
||||
}
|
||||
Ok(hwc)
|
||||
};
|
||||
let mut z = Tensor::new(cs.c as u32, cs.lat as u32, cs.lat as u32, cs.z0.clone());
|
||||
let mut frames = vec![g(&z, 0)?];
|
||||
for (k, row) in cs.rows.iter().enumerate() {
|
||||
z = wai::int_ops::run1(w.t(), z, row).map_err(|r| ("T", k + 1, world_layer("T", a, r.at), r.code))?;
|
||||
frames.push(g(&z, k + 1)?);
|
||||
}
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
/// Gate 4 of the migration, for `wai.world.model`'s integer renderer: the renderer through
|
||||
/// the operator set against the direct renderer with the direct guard
|
||||
/// (`int_world::legacy::checked`) on random sets and clips. Outside the planned breaks
|
||||
/// both render the same frames, which are also the unguarded direct renderer's, through
|
||||
/// the same latents after every transition, or both
|
||||
/// refuse a value (`field_range`), in the same network, at the same step and layer, and the
|
||||
/// guard never panics; under a planned break the renderer through the operator set refuses
|
||||
/// before its first frame.
|
||||
#[test]
|
||||
fn int_world_random_sets_and_clips_render_alike() {
|
||||
use std::collections::BTreeMap;
|
||||
use wai::int_world::{legacy, ops};
|
||||
let (mut alike, mut refused, mut breaks) = (0u64, BTreeMap::<String, u64>::new(), BTreeMap::<&str, u64>::new());
|
||||
let n = cases("int_world", 3000, |r, seed| {
|
||||
let cs = world_case(r);
|
||||
let why = || format!("seed {seed}\ncase {cs:?}");
|
||||
let (t, g) = (IntModel::from_bytes(&cs.t).expect("the generator writes WHM1 that parses"), IntModel::from_bytes(&cs.g_s).unwrap());
|
||||
let new = ops::decode_planes(&t, &g, &cs.z0, &cs.rows, cs.c, cs.lat).map(|c| c.frames);
|
||||
if let Some(b) = world_planned_break(&cs) {
|
||||
assert!(new.is_err(), "{b}: the renderer through the operator set must refuse it\n{}", why());
|
||||
*breaks.entry(b).or_default() += 1;
|
||||
return;
|
||||
}
|
||||
let world = ops::load(&t, &g, cs.c).unwrap_or_else(|e| panic!("no planned break, yet the set is refused: {e}\n{}", why()));
|
||||
let traced = world_traced(&world, &cs);
|
||||
let direct = catch_unwind(AssertUnwindSafe(|| legacy::checked::decode_planes(&t, &g, &cs.z0, &cs.rows, cs.c, cs.lat).map(|c| c.frames)))
|
||||
.unwrap_or_else(|_| panic!("the direct guard panicked\n{}", why()));
|
||||
match (&new, &traced, &direct) {
|
||||
(Ok(a), Ok(b), Ok(d)) => {
|
||||
assert!(a == d && b == d, "the two renderers' frames differ\n{}", why());
|
||||
let plain = legacy::decode_planes(&t, &g, &cs.z0, &cs.rows, cs.c, cs.lat).frames;
|
||||
assert!(plain == *d, "the guard changed the direct renderer's frames\n{}", why());
|
||||
// The latents too: a random g_s mostly saturates the RGB8 step, which can
|
||||
// hide a difference in T.
|
||||
let (mut zo, mut zd) = (cs.z0.clone(), cs.z0.clone());
|
||||
for (k, row) in cs.rows.iter().enumerate() {
|
||||
zo = world.step(&zo, row, cs.lat).unwrap_or_else(|e| panic!("transition {}: {e}\n{}", k + 1, why()));
|
||||
zd = legacy::step_planes(&t, &zd, row, cs.c, cs.lat);
|
||||
assert!(zo == zd, "the latents after transition {} differ\n{}", k + 1, why());
|
||||
}
|
||||
alike += 1;
|
||||
}
|
||||
(Err(a), Err((net, step, layer, code)), Err(d)) => {
|
||||
assert_eq!((a.kind, *code), (RefusalKind::FieldRange, wai_ops::Code::FieldRange), "the renderer refused other than a value\n{}", why());
|
||||
assert!(d.out_of_range, "the guard refused other than a value: {d:?}\n{}", why());
|
||||
assert_eq!((*net, *step, *layer), (d.net, d.step, Some(d.layer)), "the refusals' network, step and layer\n{}", why());
|
||||
*refused.entry(format!("{net} layer {}", d.layer)).or_default() += 1;
|
||||
}
|
||||
_ => panic!("through the operator set {:?}, traced {:?}, directly {:?}\n{}", new.as_ref().err(), traced.as_ref().err(), direct.as_ref().err(), why()),
|
||||
}
|
||||
});
|
||||
eprintln!("int_world: {alike} rendered alike, refused by both {refused:?}, planned breaks {breaks:?}");
|
||||
assert!(alike * 8 > n, "the generator must produce renderable cases: {alike} of {n}");
|
||||
assert!(n < 1000 || (refused.keys().any(|k| k.starts_with("T")) && refused.keys().any(|k| k.starts_with("g_s"))), "a network's check range was not reached: {refused:?}");
|
||||
assert!(n < 1000 || breaks.len() >= 5, "too few kinds of planned break reached: {breaks:?}");
|
||||
}
|
||||
|
||||
/// The planned breaks of integer-operators §15.3 item 2 for the world renderer, each on a
|
||||
/// set and clip the direct renderer rendered from (or panicked on, or whose guard refused a
|
||||
/// value): the classifier names it, and the renderer through the operator set refuses it
|
||||
/// before its first frame (`model_mismatch`).
|
||||
#[test]
|
||||
fn int_world_planned_breaks_are_new_refusals() {
|
||||
use wai::int_world::{legacy, ops};
|
||||
// C = 1, A = 1, a latent of side 2: T passes z through (weights 1 for z, 0 for the plane),
|
||||
// g_s gives three channels of the latent, the RGB8 step then maps 2^12 to 255.
|
||||
let pass = |b: i64| Whm::Conv { inc: 2, out: 1, k: 1, s: 1, p: 0, w: vec![1, 0], m: vec![1], b: vec![b] };
|
||||
let to3 = |w: i16, b: i64| Whm::ConvT { inc: 1, out: 3, k: 1, s: 1, p: 0, op: 0, w: vec![w; 3], m: vec![1; 3], b: vec![b; 3] };
|
||||
let net = |afrac: u32, shift: u32, gf: u32, l: Vec<Whm>| whm1_bytes(afrac, gf, shift, &l);
|
||||
let case = |t: Vec<u8>, g_s: Vec<u8>, c: usize, rows: Vec<Vec<i64>>| WorldCase { t, g_s, c, lat: 2, z0: vec![1 << 12; c * 4], rows };
|
||||
let base = case(net(12, 0, 16, vec![pass(0)]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0], vec![1 << 12]]);
|
||||
let render = |cs: &WorldCase| {
|
||||
let (t, g) = (IntModel::from_bytes(&cs.t).unwrap(), IntModel::from_bytes(&cs.g_s).unwrap());
|
||||
let new = ops::decode_planes(&t, &g, &cs.z0, &cs.rows, cs.c, cs.lat).map(|c| c.frames).map_err(|e| e.kind);
|
||||
let old = catch_unwind(AssertUnwindSafe(|| legacy::decode_planes(&t, &g, &cs.z0, &cs.rows, cs.c, cs.lat).frames)).ok();
|
||||
let guard = catch_unwind(AssertUnwindSafe(|| legacy::checked::decode_planes(&t, &g, &cs.z0, &cs.rows, cs.c, cs.lat).map(|c| c.frames.len()))).ok();
|
||||
(new, old, guard)
|
||||
};
|
||||
assert_eq!(world_planned_break(&base), None);
|
||||
let (new, old, _) = render(&base);
|
||||
assert_eq!(new, Ok(vec![vec![255; 12]; 3]), "the base renders through the operator set");
|
||||
assert_eq!(old, Some(vec![vec![255; 12]; 3]), "the base renders directly");
|
||||
let expect = |kind: &str, cs: WorldCase| {
|
||||
assert_eq!(world_planned_break(&cs), Some(kind), "{kind}: the classifier");
|
||||
let (new, old, guard) = render(&cs);
|
||||
assert_eq!(new.map(|_| ()), Err(RefusalKind::ModelMismatch), "{kind}: the renderer through the operator set refuses it");
|
||||
let d = match (old, guard) {
|
||||
(Some(f), Some(Ok(_))) => format!("rendered {} frames, and so did its guard", f.len()),
|
||||
(Some(f), Some(Err(g))) => format!("rendered {} frames; its guard refused at {} layer {} (step {})", f.len(), g.net, g.layer, g.step),
|
||||
(Some(f), None) => format!("rendered {} frames; its guard panicked", f.len()),
|
||||
(None, _) => "panicked".to_string(),
|
||||
};
|
||||
println!("{kind}: refused model_mismatch; the direct renderer {d}");
|
||||
};
|
||||
expect("afrac outside 1..=30, or unequal", case(net(13, 0, 16, vec![pass(0)]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
expect("afrac outside 1..=30, or unequal", case(net(31, 0, 16, vec![pass(0)]), net(31, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
expect("A < 1", case(net(12, 0, 16, vec![pass(0)]), net(12, 0, 16, vec![to3(1, 0)]), 2, vec![]));
|
||||
expect("no layer of tag 0, 1 or 2", case(net(12, 0, 16, vec![pass(0)]), net(12, 0, 16, vec![Whm::Relu]), 1, vec![vec![0]]));
|
||||
expect("a shift or gf of 128 or more that no layer uses", case(net(12, 0, 16, vec![pass(0)]), net(12, 0, 200, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
expect("a bias of magnitude 2^63 - 1 or more", case(net(12, 0, 16, vec![pass(i64::MAX)]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
let first = Whm::Conv { inc: 2, out: 1, k: 1, s: 1, p: 0, w: vec![0, 0], m: vec![1], b: vec![1 << 30] };
|
||||
let tight = Whm::Conv { inc: 1, out: 1, k: 1, s: 1, p: 0, w: vec![1 << 11], m: vec![1 << 40], b: vec![0] };
|
||||
expect("a check range its producer cannot meet", case(net(12, 0, 16, vec![first, tight]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
expect("layers that do not chain, or a shift a layer cannot take", case(net(12, 200, 16, vec![pass(0)]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
expect("a row that does not carry A values", case(net(12, 0, 16, vec![pass(0)]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0, 0]]));
|
||||
let halve = Whm::Conv { inc: 2, out: 1, k: 1, s: 2, p: 0, w: vec![1, 0], m: vec![1], b: vec![0] };
|
||||
expect("T or g_s that does not fit the latent", case(net(12, 0, 16, vec![halve]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
let wide = Whm::Conv { inc: 2, out: 2, k: 1, s: 1, p: 0, w: vec![1, 0, 1, 0], m: vec![1; 2], b: vec![0; 2] };
|
||||
expect("T or g_s that does not fit the latent", case(net(12, 0, 16, vec![wide]), net(12, 0, 16, vec![to3(1, 0)]), 1, vec![vec![0]]));
|
||||
let big = Whm::Conv { inc: 1, out: 1, k: 3, s: 1, p: 0, w: vec![1; 9], m: vec![1], b: vec![0] };
|
||||
expect("T or g_s that does not fit the latent", case(net(12, 0, 16, vec![pass(0)]), net(12, 0, 16, vec![big, to3(1, 0)]), 1, vec![vec![0]]));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ load step 8
|
||||
|
||||
/// What the bounds pass made of the graphs compared: refused, or the profiles met.
|
||||
|
|
@ -1902,9 +2224,10 @@ fn same_pass(g: &wai_ops::Graph, v: &mut Verdicts, why: &dyn Fn() -> String) {
|
|||
}
|
||||
}
|
||||
|
||||
/// The bounds pass against its oracle on every graph the four generators above lower:
|
||||
/// `int_synth/1`'s graph, the splat set's `g_s`, the audio synthesis, and the
|
||||
/// conditional codec's `h_s` and `g_s`. Every verdict is reached: a refusal by the pass
|
||||
/// The bounds pass against its oracle on every graph the generators above lower:
|
||||
/// `int_synth/1`'s graph, the splat set's `g_s`, the audio synthesis, the conditional
|
||||
/// codec's `h_s` and `g_s`, the MLIC++ bundle's three networks, and the world
|
||||
/// renderer's `T` and `g_s`. Every verdict is reached: a refusal by the pass
|
||||
/// itself (a `check` range its producer cannot meet), `acc32`, `acc64` only, and neither.
|
||||
#[test]
|
||||
fn the_bounds_pass_equals_its_oracle_on_random_sets() {
|
||||
|
|
@ -1939,6 +2262,12 @@ fn the_bounds_pass_equals_its_oracle_on_random_sets() {
|
|||
}
|
||||
}
|
||||
}
|
||||
let cs = world_case(r);
|
||||
if let Ok(l) = wai_ops::lower::world(&cs.t, &cs.g_s, cs.c as u32) {
|
||||
for (_, g) in &l.roles {
|
||||
same_pass(g, &mut v, &why);
|
||||
}
|
||||
}
|
||||
});
|
||||
eprintln!("bounds pass: {n} cases, {v:?}");
|
||||
assert!(v.graphs > n * 2, "the generators must mostly lower: {} graphs from {n} cases", v.graphs);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ fn interactive_world_regenerates_byte_for_byte() {
|
|||
// From z0 + the action walk, the integer transition + synthesis regenerate the
|
||||
// frames in pure i64 — no float, no ML runtime. Each must reproduce its committed
|
||||
// hash (a 1-ULP disagreement anywhere in the conv/IGDN/transition would diverge).
|
||||
let clip = decode(&t, &gs, &z0, &actions, c, lat);
|
||||
let clip = decode(&t, &gs, &z0, &actions, c, lat).expect("the committed clip renders");
|
||||
let want = j["frame_hashes"].as_array().unwrap();
|
||||
assert_eq!(clip.frames.len(), want.len(), "frame count = initial + one per action");
|
||||
assert_eq!(clip.frames.len(), actions.len() + 1);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ fn two_agent_world_regenerates_byte_for_byte() {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let clip = decode_planes(&t, &gs, &z0, &rows, c, lat);
|
||||
let clip = decode_planes(&t, &gs, &z0, &rows, c, lat).expect("the committed clip renders");
|
||||
let want = j["frame_hashes"].as_array().unwrap();
|
||||
assert_eq!(clip.frames.len(), rows.len() + 1, "initial + one frame per combined action");
|
||||
for i in 0..clip.frames.len() {
|
||||
|
|
|
|||
108
wai/wai-rs/tests/int_world_memory.rs
Normal file
108
wai/wai-rs/tests/int_world_memory.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
//! What `wai.world.model`'s integer renderer holds, through the integer operator set,
|
||||
//! against the direct renderer it replaced (`int_world::legacy`, feature `legacy_int`),
|
||||
//! measured with a global allocator that tracks the bytes live and their peak.
|
||||
//!
|
||||
//! The renderer declares no cost: it has no payload (`extensions/integer-operators.md`
|
||||
//! §13.5, informative). So what it holds is held to the direct renderer: with the set
|
||||
//! loaded beforehand (`int_world::load`, once per clip or per session, as the browser's
|
||||
//! `WorldModel` holds it), a clip through the operator set holds at least its frames and
|
||||
//! no more than the direct renderer holds for the same clip, both on the reference
|
||||
//! kernels; and so does each step and each frame of the live path.
|
||||
//!
|
||||
//! Its own test binary with a single test, so that no other test's allocations land in
|
||||
//! the window it measures.
|
||||
#![cfg(all(feature = "world_int", feature = "legacy_int"))]
|
||||
|
||||
use std::alloc::{GlobalAlloc, Layout, System};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
|
||||
|
||||
use serde_json::Value;
|
||||
use wai::int_hyper_synth::IntModel;
|
||||
use wai::int_kernels::{with_policy, Policy};
|
||||
use wai::int_world::{self, legacy, ACTIONS};
|
||||
|
||||
/// The system allocator, tracking the bytes live and the most live at once.
|
||||
struct Live;
|
||||
static CUR: AtomicUsize = AtomicUsize::new(0);
|
||||
static PEAK: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
unsafe impl GlobalAlloc for Live {
|
||||
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
|
||||
PEAK.fetch_max(CUR.fetch_add(l.size(), Relaxed) + l.size(), Relaxed);
|
||||
unsafe { System.alloc(l) }
|
||||
}
|
||||
unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 {
|
||||
PEAK.fetch_max(CUR.fetch_add(l.size(), Relaxed) + l.size(), Relaxed);
|
||||
unsafe { System.alloc_zeroed(l) }
|
||||
}
|
||||
unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 {
|
||||
if new >= l.size() {
|
||||
PEAK.fetch_max(CUR.fetch_add(new - l.size(), Relaxed) + (new - l.size()), Relaxed);
|
||||
} else {
|
||||
CUR.fetch_sub(l.size() - new, Relaxed);
|
||||
}
|
||||
unsafe { System.realloc(p, l, new) }
|
||||
}
|
||||
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
|
||||
CUR.fetch_sub(l.size(), Relaxed);
|
||||
unsafe { System.dealloc(p, l) }
|
||||
}
|
||||
}
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: Live = Live;
|
||||
|
||||
/// The most bytes `f` holds at once beyond what was live when it started, and its result
|
||||
/// (still held when the measure is taken).
|
||||
fn peak_during<R>(f: impl FnOnce() -> R) -> (R, u64) {
|
||||
let base = CUR.load(Relaxed);
|
||||
PEAK.store(base, Relaxed);
|
||||
let r = f();
|
||||
(r, (PEAK.load(Relaxed) - base) as u64)
|
||||
}
|
||||
|
||||
fn read(rel: &str) -> Vec<u8> {
|
||||
let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors").join(rel);
|
||||
std::fs::read(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_renderer_holds_no_more_than_the_direct_renderer() {
|
||||
for (d, js, t, g) in [("int_world", "world.json", "world_T.bin", "world_gs.bin"), ("int_world2", "world2.json", "world2_T.bin", "world2_gs.bin")] {
|
||||
let j: Value = serde_json::from_slice(&read(&format!("{d}/{js}"))).unwrap();
|
||||
let (c, lat, afrac) = (j["c"].as_u64().unwrap() as usize, j["lat"].as_u64().unwrap() as usize, j["afrac"].as_u64().unwrap() as u32);
|
||||
let z0: Vec<i64> = j["z0"].as_array().unwrap().iter().map(|v| v.as_i64().unwrap()).collect();
|
||||
let mv = |a: &Value| ACTIONS[a.as_u64().unwrap() as usize];
|
||||
let rows: Vec<Vec<i64>> = match j.get("pairs") {
|
||||
Some(p) => p.as_array().unwrap().iter().map(|p| {
|
||||
let ((xa, ya), (xb, yb)) = (mv(&p[0]), mv(&p[1]));
|
||||
[xa, ya, xb, yb].iter().map(|v| v << afrac).collect()
|
||||
}).collect(),
|
||||
None => j["actions"].as_array().unwrap().iter().map(|a| {
|
||||
let (x, y) = mv(a);
|
||||
vec![x << afrac, y << afrac, 0]
|
||||
}).collect(),
|
||||
};
|
||||
let (tm, gm) = (IntModel::from_bytes(&read(&format!("{d}/{t}"))).unwrap(), IntModel::from_bytes(&read(&format!("{d}/{g}"))).unwrap());
|
||||
// A clip: the direct renderer, then the renderer with its set loaded beforehand.
|
||||
let (want, direct) = with_policy(Policy::reference(), || peak_during(|| legacy::decode_planes(&tm, &gm, &z0, &rows, c, lat)));
|
||||
let (world, set) = peak_during(|| int_world::load(&tm, &gm, c).unwrap());
|
||||
let (got, held) = with_policy(Policy::reference(), || peak_during(|| world.decode_planes(&z0, &rows, lat).unwrap()));
|
||||
assert!(got.frames == want.frames, "{d}: the frames through the lowering are the direct renderer's");
|
||||
let frames: u64 = got.frames.iter().map(|f| f.len() as u64).sum();
|
||||
assert!(held >= frames && held <= direct, "{d}, set loaded: held {held}; the direct renderer held {direct}; the frames are {frames}");
|
||||
// The whole call, which loads the set first.
|
||||
let (r, whole) = with_policy(Policy::reference(), || peak_during(|| int_world::decode_planes(&tm, &gm, &z0, &rows, c, lat)));
|
||||
drop(r.unwrap());
|
||||
assert!(whole <= set + direct, "{d}: the call that loads the set held {whole}, over the set loaded ({set}) and the direct renderer's {direct}");
|
||||
// The live path: one transition, then one frame, against the direct renderer's.
|
||||
let (zd, step_direct) = with_policy(Policy::reference(), || peak_during(|| legacy::step_planes(&tm, &z0, &rows[0], c, lat)));
|
||||
let (zo, step_held) = with_policy(Policy::reference(), || peak_during(|| world.step(&z0, &rows[0], lat).unwrap()));
|
||||
assert!(zo == zd && step_held <= step_direct, "{d}: one step held {step_held}; the direct step held {step_direct}");
|
||||
let (fd, frame_direct) = with_policy(Policy::reference(), || peak_during(|| legacy::decode_planes(&tm, &gm, &zd, &[], c, lat).frames));
|
||||
let (fo, frame_held) = with_policy(Policy::reference(), || peak_during(|| world.render(&zo, lat).unwrap().0));
|
||||
assert!(fo == fd[0] && frame_held <= frame_direct, "{d}: one frame held {frame_held}; the direct frame held {frame_direct}");
|
||||
println!("{d}: a clip of {} frames, set loaded beforehand, held {held}; the direct renderer held {direct}; loading the set held {set}; the call that loads it held {whole}; one step held {step_held} (direct {step_direct}); one frame {frame_held} (direct {frame_direct})", got.frames.len());
|
||||
}
|
||||
}
|
||||
587
wai/wai-rs/tests/int_world_ops.rs
Normal file
587
wai/wai-rs/tests/int_world_ops.rs
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
//! `wai.world.model`'s integer renderer through the integer operator set: its lowering
|
||||
//! `world/1` (`extensions/integer-operators.md` §13.5, informative: the transition `T`
|
||||
//! with its `A` action planes, then the synthesis `g_s` with the RGB8 step of §13.0) and
|
||||
//! the certificate of every committed set (§8.5), as this crate loads them (`int_ops`,
|
||||
//! `int_world::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, from
|
||||
//! the models it parses (`int_ops::whm1_net`), must write those same bytes, and the load a
|
||||
//! sink makes must give the verdict the certificate records.
|
||||
#![cfg(all(feature = "world_int", feature = "legacy_int"))]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::Value;
|
||||
use wai::int_hyper_synth::IntModel;
|
||||
use wai::int_world;
|
||||
use wai_ops::Profile;
|
||||
|
||||
const CAP: &str = "wai.world.model";
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// A committed `wai.world.model` set, as `ops-conformance/certificates/index.json` lists
|
||||
/// it: its name, whether it is synthetic, the latent's channel count `C`, its members
|
||||
/// (name, path below `wai/`), its digest and its lowered graphs (`T`, then `g_s`).
|
||||
struct Indexed {
|
||||
name: String,
|
||||
synthetic: bool,
|
||||
c: u32,
|
||||
members: Vec<(String, String)>,
|
||||
digest: String,
|
||||
roles: [String; 2],
|
||||
}
|
||||
|
||||
impl Indexed {
|
||||
fn member(&self, name: &str) -> Vec<u8> {
|
||||
read(&self.members.iter().find(|(k, _)| k == name).unwrap_or_else(|| panic!("{}: no {name}", self.name)).1)
|
||||
}
|
||||
}
|
||||
|
||||
fn indexed() -> Vec<Indexed> {
|
||||
json("ops-conformance/certificates/index.json")["sets"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|s| s["capability"] == CAP)
|
||||
.map(|s| {
|
||||
assert_eq!(s["lowering"], "world/1");
|
||||
let roles = s["roles"].as_array().unwrap();
|
||||
assert_eq!(roles.len(), 2, "world/1 lowers a set to two graphs, T and g_s");
|
||||
Indexed {
|
||||
name: s["name"].as_str().unwrap().to_string(),
|
||||
synthetic: s["synthetic"].as_bool().unwrap(),
|
||||
c: s["c"].as_str().unwrap().parse().unwrap(),
|
||||
members: s["members"].as_object().unwrap().iter().map(|(k, v)| (k.clone(), v.as_str().unwrap().to_string())).collect(),
|
||||
digest: s["set_sha256"].as_str().unwrap().to_string(),
|
||||
roles: [roles[0].as_str().unwrap().to_string(), roles[1].as_str().unwrap().to_string()],
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Gate 1 of the migration: every committed `wai.world.model` set lowers to its committed
|
||||
/// graphs, from its members' bytes (`lower::world`) and from the models this crate parses
|
||||
/// (`int_world::ops::lower`); the certificate beside them is those graphs'; and loading
|
||||
/// each graph under `acc64` gives the certificate's verdict, as `int_world::ops::load`
|
||||
/// holds them.
|
||||
///
|
||||
/// - `A`, the action planes, is `T`'s input channel count less `C` (§13.5): 3 for the one
|
||||
/// agent set, 4 for the two agent set. `T` has one scalar per plane, each over all of
|
||||
/// `i64`, and its input `z` is `check` over all of `i64` (§13.0's driver contract).
|
||||
/// - `g_s`'s input range is the lowering's `X_0` for its first layer (§13.2).
|
||||
/// - The sets meet `acc64` and not `acc32`: the first `acc32` failure is `T`'s input range,
|
||||
/// at 64 bits, as Appendix C of the design predicted.
|
||||
#[test]
|
||||
fn every_committed_set_lowers_to_its_committed_graphs_and_verdict() {
|
||||
let sets = indexed();
|
||||
let names: Vec<(&str, bool, u32)> = sets.iter().map(|s| (s.name.as_str(), s.synthetic, s.c)).collect();
|
||||
assert_eq!(names, [("world", false, 24), ("world2", false, 24), ("world", true, 2)], "the committed world sets");
|
||||
for s in &sets {
|
||||
let name = format!("{}{}", s.name, if s.synthetic { " (synthetic)" } else { "" });
|
||||
let (t, g) = (s.member("T.whm"), s.member("g_s.whm"));
|
||||
assert_eq!(wai_ops::generate::sets::set_digest(&[("T.whm", t.clone()), ("g_s.whm", g.clone())]), s.digest, "{name}: the set's digest");
|
||||
let lowered = wai_ops::lower::world(&t, &g, s.c).unwrap_or_else(|r| panic!("{name}: does not lower: {r:?}"));
|
||||
let roles: Vec<&str> = lowered.roles.iter().map(|(r, _)| *r).collect();
|
||||
assert_eq!(roles, ["T", "g_s"], "{name}");
|
||||
// The models this crate parses convert to the networks wai-ops parses from the bytes.
|
||||
let (tm, gm) = (IntModel::from_bytes(&t).unwrap(), IntModel::from_bytes(&g).unwrap());
|
||||
for (m, b) in [(&tm, &t), (&gm, &g)] {
|
||||
let net = wai::int_ops::whm1_net(m).expect("a model parsed from bytes converts");
|
||||
assert!(net == wai_ops::lower::parse_whm1(b).unwrap(), "{name}: the converted model is the one wai-ops parses from the bytes");
|
||||
}
|
||||
let (ot, og) = int_world::ops::lower(&tm, &gm, s.c as usize).unwrap_or_else(|r| panic!("{name}: {r}"));
|
||||
let world = int_world::ops::load(&tm, &gm, s.c as usize).unwrap_or_else(|r| panic!("{name}: {r}"));
|
||||
let cert = json(&format!("ops-conformance/certificates/{CAP}/{}.json", s.digest));
|
||||
assert_eq!(cert["set_sha256"].as_str(), Some(s.digest.as_str()), "{name}");
|
||||
assert_eq!(cert["capability"].as_str(), Some(CAP), "{name}");
|
||||
assert_eq!(cert["lowering"], "world/1", "{name}");
|
||||
for (k, (role, ours, held)) in [("T", &ot, world.t()), ("g_s", &og, world.g_s())].into_iter().enumerate() {
|
||||
let from_bytes = lowered.get(role).unwrap();
|
||||
// The verdict first: the load a sink makes.
|
||||
let loaded = wai::int_ops::load(from_bytes).unwrap_or_else(|r| panic!("{name} {role}: refused under acc64: {r:?}"));
|
||||
assert!(!loaded.meets(Profile::Acc32), "{name} {role}: a graph with check ranges inside it does not meet acc32");
|
||||
// Then the committed bytes: the graph, both ways, as the renderer holds it, and the
|
||||
// certificate's record of it.
|
||||
let graph = from_bytes.to_bytes();
|
||||
assert!(graph == read(&s.roles[k]), "{name}: the lowering of the bytes differs from the committed {}", s.roles[k]);
|
||||
assert!(ours.to_bytes() == graph, "{name}: the lowering of the parsed models differs from the committed {role}");
|
||||
assert!(held.graph().to_bytes() == graph, "{name}: the renderer holds another {role} than the committed one");
|
||||
assert!(!held.meets(Profile::Acc32), "{name} {role}");
|
||||
let c = &cert["graphs"][k];
|
||||
assert_eq!(c["name"].as_str(), Some(role), "{name}");
|
||||
assert_eq!(c["graph_sha256"].as_str(), Some(sha256(&graph).as_str()), "{name} {role}: the certificate is the graph's");
|
||||
let x0 = &c["inputs"][0];
|
||||
let t0 = &from_bytes.tensors[0];
|
||||
assert_eq!((x0["policy"].as_str(), x0["lo"].as_str(), x0["hi"].as_str()), (Some("check"), Some(t0.lo.to_string().as_str()), Some(t0.hi.to_string().as_str())), "{name} {role}");
|
||||
}
|
||||
// T: z over all of i64 with C channels, and A scalars over all of i64, A being T's
|
||||
// input channels less C.
|
||||
let tg = lowered.get("T").unwrap();
|
||||
let cin = match &wai_ops::lower::parse_whm1(&t).unwrap().layers[0] {
|
||||
wai_ops::lower::NetLayer::Conv { inc, .. } | wai_ops::lower::NetLayer::ConvT { inc, .. } => *inc,
|
||||
l => panic!("{name}: T's first layer {l:?}"),
|
||||
};
|
||||
let a = (cin - s.c) as usize;
|
||||
assert_eq!((tg.tensors[0].channels, tg.tensors[0].lo, tg.tensors[0].hi), (s.c, i64::MIN, i64::MAX), "{name}: T's input z");
|
||||
assert_eq!(tg.scalars.len(), a, "{name}: one scalar per action plane");
|
||||
assert!(tg.scalars.iter().all(|r| (r.lo, r.hi) == (i64::MIN, i64::MAX)), "{name}: every plane value over all of i64");
|
||||
assert_eq!((world.channels(), world.planes()), (s.c as usize, a), "{name}: the renderer's C and A");
|
||||
if !s.synthetic {
|
||||
assert_eq!(a, if s.name == "world2" { 4 } else { 3 }, "{name}: A");
|
||||
}
|
||||
// g_s: a symmetric check range on z.
|
||||
let g0 = &lowered.get("g_s").unwrap().tensors[0];
|
||||
assert!(g0.channels == s.c && g0.hi > 0 && g0.lo == -g0.hi, "{name}: g_s reads z of C channels within a symmetric check range");
|
||||
let v = &cert["verdict"];
|
||||
assert_eq!((v["acc64"].as_bool(), v["acc32"].as_bool()), (Some(true), Some(false)), "{name}: the certificate's verdict");
|
||||
let first = &v["acc32_first_failure"];
|
||||
assert_eq!(
|
||||
(first["graph"].as_str(), first["where"].as_str(), first["index"].as_str(), first["key"].as_str(), first["bits"].as_str()),
|
||||
(Some("T"), Some("input"), Some("0"), Some("range"), Some("64")),
|
||||
"{name}: the first acc32 failure Appendix C predicts"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A committed world vector: its two networks, `C`, the latent's side, `z_0`, its JSON
|
||||
/// (the export's FNV-1a frame and clip hashes) and its SHA-256 sidecar entry.
|
||||
struct Vector {
|
||||
name: &'static str,
|
||||
t: IntModel,
|
||||
g_s: IntModel,
|
||||
c: usize,
|
||||
lat: usize,
|
||||
z0: Vec<i64>,
|
||||
j: Value,
|
||||
sha: Value,
|
||||
}
|
||||
|
||||
/// The two committed vectors: one agent (`A = 3`) and two agents (`A = 4`).
|
||||
fn vectors() -> [Vector; 2] {
|
||||
let v = |name: &'static str, dir: &str, js: &str, t: &str, g: &str| {
|
||||
let j = json(&format!("wai-rs/tests/vectors/{dir}/{js}"));
|
||||
let sha = json(&format!("wai-rs/tests/vectors/{dir}/sha256.json"))[name].clone();
|
||||
Vector {
|
||||
name,
|
||||
t: IntModel::from_bytes(&read(&format!("wai-rs/tests/vectors/{dir}/{t}"))).unwrap(),
|
||||
g_s: IntModel::from_bytes(&read(&format!("wai-rs/tests/vectors/{dir}/{g}"))).unwrap(),
|
||||
c: j["c"].as_u64().unwrap() as usize,
|
||||
lat: j["lat"].as_u64().unwrap() as usize,
|
||||
z0: j["z0"].as_array().unwrap().iter().map(|x| x.as_i64().unwrap()).collect(),
|
||||
j,
|
||||
sha,
|
||||
}
|
||||
};
|
||||
[v("world", "int_world", "world.json", "world_T.bin", "world_gs.bin"), v("world2", "int_world2", "world2.json", "world2_T.bin", "world2_gs.bin")]
|
||||
}
|
||||
|
||||
impl Vector {
|
||||
/// The vector's rows at `2^afrac` (§13.5): one agent, each action's move `(dx, dy)` as
|
||||
/// `dequantize(dx, afrac, 0)`, `dequantize(dy, afrac, 0)` and 0; two agents, each pair's
|
||||
/// two moves.
|
||||
fn rows(&self) -> Vec<Vec<i64>> {
|
||||
let afrac = self.j["afrac"].as_u64().unwrap() as u32;
|
||||
let q = |d: i64| wai_ops::stage::dequantize(d, afrac, 0).unwrap();
|
||||
let mv = |a: &Value| int_world::ACTIONS[a.as_u64().unwrap() as usize];
|
||||
match self.j.get("pairs") {
|
||||
Some(p) => p.as_array().unwrap().iter().map(|p| {
|
||||
let ((xa, ya), (xb, yb)) = (mv(&p[0]), mv(&p[1]));
|
||||
vec![q(xa), q(ya), q(xb), q(yb)]
|
||||
}).collect(),
|
||||
None => self.j["actions"].as_array().unwrap().iter().map(|a| {
|
||||
let (dx, dy) = mv(a);
|
||||
vec![q(dx), q(dy), 0]
|
||||
}).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The direct renderer's clip (`int_world::legacy`).
|
||||
fn direct(&self) -> int_world::WorldClip {
|
||||
int_world::legacy::decode_planes(&self.t, &self.g_s, &self.z0, &self.rows(), self.c, self.lat)
|
||||
}
|
||||
|
||||
/// The vector's goldens against `frames`: each frame's FNV-1a (the export's) and
|
||||
/// SHA-256 (the sidecar's), then the clip's. Every frame that differs is named.
|
||||
fn check_goldens(&self, what: &str, frames: &[Vec<u8>]) {
|
||||
let fnv = self.j["frame_hashes"].as_array().unwrap();
|
||||
let sha = self.sha["frames_sha256"].as_array().unwrap();
|
||||
assert_eq!((frames.len(), fnv.len(), sha.len()), (fnv.len(), sha.len(), self.rows().len() + 1), "{} {what}: the frame count", self.name);
|
||||
let off: Vec<usize> = (0..frames.len())
|
||||
.filter(|&i| format!("{:016x}", wai::int_hyper_synth::fnv1a(&frames[i])) != fnv[i].as_str().unwrap() || sha256(&frames[i]) != sha[i].as_str().unwrap())
|
||||
.collect();
|
||||
assert!(off.is_empty(), "{} {what}: frames {off:?} of 0..{} differ from their committed FNV-1a and SHA-256", self.name, frames.len());
|
||||
let clip = int_world::WorldClip { frames: frames.to_vec(), frame_px: 0 };
|
||||
assert_eq!(Some(format!("{:016x}", clip.clip_hash()).as_str()), self.j["clip_hash"].as_str(), "{} {what}: the clip's FNV-1a", self.name);
|
||||
let all: Vec<u8> = frames.iter().flat_map(|f| wai_ops::sha256::sha256(f)).collect();
|
||||
assert_eq!(Some(sha256(&all).as_str()), self.sha["clip_sha256"].as_str(), "{} {what}: the clip's SHA-256", self.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gate 2 of the migration: golden equality. Each committed vector renders through the
|
||||
/// lowering (`int_world::ops`) to the frames the direct renderer gives
|
||||
/// (`int_world::legacy`), every byte, and to every golden the corpus commits for it: the
|
||||
/// nine frame SHA-256s and the clip SHA-256 of `sha256.json`, and the export's FNV-1a
|
||||
/// frame and clip hashes. The two-agent vector (`A = 4`) renders from its rows through
|
||||
/// `decode_planes`; the one-agent vector (`A = 3`) from its rows, its moves
|
||||
/// (`decode_deltas`) and its action indices (`decode`) alike.
|
||||
#[test]
|
||||
fn every_committed_vector_renders_through_the_lowering_to_its_goldens() {
|
||||
for v in vectors() {
|
||||
let direct = v.direct();
|
||||
let world = int_world::ops::load(&v.t, &v.g_s, v.c).unwrap_or_else(|r| panic!("{}: {r}", v.name));
|
||||
assert_eq!(world.planes(), if v.name == "world2" { 4 } else { 3 }, "{}: A", v.name);
|
||||
let ops = world.decode_planes(&v.z0, &v.rows(), v.lat).unwrap_or_else(|r| panic!("{}: {r}", v.name));
|
||||
let off: Vec<usize> = (0..ops.frames.len().max(direct.frames.len())).filter(|&i| ops.frames.get(i) != direct.frames.get(i)).collect();
|
||||
assert!(off.is_empty(), "{}: frames {off:?} through the lowering differ from the direct renderer's", v.name);
|
||||
assert_eq!((ops.frame_px, direct.frame_px), (v.j["frame"].as_u64().unwrap() as usize, v.j["frame"].as_u64().unwrap() as usize), "{}", v.name);
|
||||
v.check_goldens("through the lowering", &ops.frames);
|
||||
v.check_goldens("direct", &direct.frames);
|
||||
let free = int_world::ops::decode_planes(&v.t, &v.g_s, &v.z0, &v.rows(), v.c, v.lat).unwrap();
|
||||
assert!(free.frames == ops.frames, "{}: the set loaded per call renders alike", v.name);
|
||||
if v.name == "world" {
|
||||
let actions: Vec<usize> = v.j["actions"].as_array().unwrap().iter().map(|a| a.as_u64().unwrap() as usize).collect();
|
||||
let deltas: Vec<(i64, i64)> = actions.iter().map(|&a| int_world::ACTIONS[a]).collect();
|
||||
assert!(world.decode_deltas(&v.z0, &deltas, v.lat).unwrap().frames == ops.frames, "world: decode_deltas");
|
||||
assert!(int_world::ops::decode(&v.t, &v.g_s, &v.z0, &actions, v.c, v.lat).unwrap().frames == ops.frames, "world: decode");
|
||||
assert!(int_world::legacy::decode(&v.t, &v.g_s, &v.z0, &actions, v.c, v.lat).frames == direct.frames, "world: the direct decode of the actions");
|
||||
}
|
||||
// The live path: one transition and one frame at a time.
|
||||
let rows = v.rows();
|
||||
let (f0, h, w) = world.render(&v.z0, v.lat).unwrap();
|
||||
assert_eq!((h, w), (direct.frame_px, direct.frame_px));
|
||||
let mut frames = vec![f0];
|
||||
let mut z = v.z0.clone();
|
||||
for r in &rows {
|
||||
z = world.step(&z, r, v.lat).unwrap();
|
||||
frames.push(world.render(&z, v.lat).unwrap().0);
|
||||
}
|
||||
assert!(frames == ops.frames, "{}: step and render, one at a time, give the clip", v.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// The layer whose input check a refusal of a world graph is (§13.2's `x_ℓ`), from where
|
||||
/// the graph refused: for `T` (`A` fills, then the concatenation that writes `x_0`, then
|
||||
/// the layers), the output of operator `A + ℓ`; for `g_s`, its input for layer 0 and the
|
||||
/// output of operator `ℓ − 1` for layer `ℓ`.
|
||||
fn layer_of(net: &str, a: usize, at: wai_ops::Site) -> usize {
|
||||
match (net, at) {
|
||||
("T", wai_ops::Site::Op(i)) if i as usize >= a => i as usize - a,
|
||||
("g_s", wai_ops::Site::Input(0)) => 0,
|
||||
("g_s", wai_ops::Site::Op(i)) => i as usize + 1,
|
||||
_ => panic!("{net} refused at {at:?}, which is no layer's check"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer through the operator set, as `World::decode_planes` runs it, one graph at
|
||||
/// a time, so that a refusal names its network, its step and its layer
|
||||
/// ([`layer_of`]). It must agree with `World::decode_planes` (checked by the caller).
|
||||
fn traced(w: &int_world::ops::World, z0: &[i64], rows: &[Vec<i64>], lat: usize) -> Result<Vec<Vec<u8>>, (&'static str, usize, usize, wai_ops::Code)> {
|
||||
use wai_ops::Tensor;
|
||||
let (c, a) = (w.channels() as u32, w.planes());
|
||||
let g = |z: &Tensor, step: usize| {
|
||||
let out = wai::int_ops::run1(w.g_s(), z.clone(), &[]).map_err(|r| ("g_s", step, layer_of("g_s", a, r.at), r.code))?;
|
||||
let plane = (out.h * out.w) as usize;
|
||||
let mut hwc = vec![0u8; out.data.len()];
|
||||
for (k, &v) in out.data.iter().enumerate() {
|
||||
hwc[(k % plane) * out.c as usize + k / plane] = v as u8;
|
||||
}
|
||||
Ok(hwc)
|
||||
};
|
||||
let mut z = Tensor::new(c, lat as u32, lat as u32, z0.to_vec());
|
||||
let mut frames = vec![g(&z, 0)?];
|
||||
for (k, row) in rows.iter().enumerate() {
|
||||
z = wai::int_ops::run1(w.t(), z, row).map_err(|r| ("T", k + 1, layer_of("T", a, r.at), r.code))?;
|
||||
frames.push(g(&z, k + 1)?);
|
||||
}
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
/// Gate 3, at the edge of every `check` range: for each layer of `T` and of `g_s` in every
|
||||
/// committed set, the layer's operator in the full `world/1` graph is run alone, from a
|
||||
/// graph of that one operator whose input has the record the full graph gives the layer's
|
||||
/// input (for `T`, layer 0 reads `x_0`, the concatenation of `z` and the action planes).
|
||||
/// That range is §13.2's `[−X_ℓ, X_ℓ]`: the range of the layer lowered alone by `whm1/1`
|
||||
/// for a convolution or an inverse GDN, and `⌊(2^63 − 1) / |slope|⌋` for a leaky ReLU. An
|
||||
/// input whose largest magnitude is `X_ℓ` runs, through the operator set and through the
|
||||
/// direct guard (`run_net_checked`), to the same output; one at `X_ℓ + 1` is refused by
|
||||
/// both: by the graph (`field_range`, at its input) and by the guard (a value out of range
|
||||
/// at that layer). So the ranges refuse exactly what the guard refuses, at the same layer.
|
||||
#[test]
|
||||
fn every_check_range_refuses_exactly_what_the_direct_guard_refuses() {
|
||||
use wai::int_hyper_synth::legacy::run_net_checked;
|
||||
use wai::int_hyper_synth::IntLayer;
|
||||
use wai_ops::graph::{Graph, InputRec, Op, Policy, TensorRec, CAP};
|
||||
use wai_ops::{Code, Site, Tensor};
|
||||
let mut checked = 0;
|
||||
for s in indexed() {
|
||||
let (tb, gb) = (s.member("T.whm"), s.member("g_s.whm"));
|
||||
let full = wai_ops::lower::world(&tb, &gb, s.c).unwrap();
|
||||
for (net, bytes) in [("T", &tb), ("g_s", &gb)] {
|
||||
let g = full.get(net).unwrap();
|
||||
// T's layers follow z, its A fills and the concatenation; g_s's follow its input.
|
||||
let (first_t, first_op) = if net == "T" { (1 + g.scalars.len(), g.scalars.len() + 1) } else { (0, 0) };
|
||||
let m = IntModel::from_bytes(bytes).unwrap();
|
||||
for l in 0..m.layers.len() {
|
||||
let what = format!("{} {net} layer {l}", s.name);
|
||||
let rec = g.tensors[first_t + l];
|
||||
let op = &g.ops[first_op + l];
|
||||
if rec.policy != Policy::Check {
|
||||
assert!(matches!(m.layers[l], IntLayer::Relu), "{what}: only a ReLU's input is derived");
|
||||
continue;
|
||||
}
|
||||
let mut one = IntModel::from_bytes(bytes).unwrap();
|
||||
let layer = one.layers.swap_remove(l);
|
||||
one.layers = vec![layer];
|
||||
match &one.layers[0] {
|
||||
IntLayer::Leaky { slope_q, .. } => {
|
||||
let x = i64::MAX as u64 / slope_q.unsigned_abs();
|
||||
assert_eq!((rec.lo, rec.hi), (-(x as i64), x as i64), "{what}: X·|slope| <= 2^63 - 1");
|
||||
}
|
||||
_ => {
|
||||
let alone = wai_ops::lower::whm1_net(&wai::int_ops::whm1_net(&one).unwrap(), false).unwrap();
|
||||
assert_eq!(alone.tensors[0], rec, "{what}: the range of the layer lowered alone");
|
||||
}
|
||||
}
|
||||
let (c, hi) = (rec.channels, rec.hi);
|
||||
assert!(hi > 0 && rec.lo == -hi, "{what}: a symmetric check range");
|
||||
let out = TensorRec { policy: Policy::Derived, lo: 0, hi: 0, ..g.tensors[op.outs[0] as usize] };
|
||||
let alone = 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![rec, out],
|
||||
ops: vec![Op { code: op.code, ins: vec![0], outs: vec![1], p: op.p.clone() }],
|
||||
};
|
||||
let loaded = wai::int_ops::load(&alone).unwrap_or_else(|r| panic!("{what}: the operator alone loads: {r:?}"));
|
||||
let x = |v: i64| {
|
||||
let mut d = vec![0i64; c as usize * 4];
|
||||
d[c as usize * 2 + 1] = v;
|
||||
d
|
||||
};
|
||||
for v in [hi, -hi] {
|
||||
let ops = wai::int_ops::run1(&loaded, Tensor::new(c, 2, 2, x(v)), &[]).unwrap_or_else(|r| panic!("{what}: {v} runs: {r:?}"));
|
||||
let (direct, ..) = run_net_checked(&one, &x(v), c as usize, 2, 2).unwrap_or_else(|r| panic!("{what}: {v} runs directly: {r}"));
|
||||
assert!(ops.data == direct, "{what}: at {v} the two differ");
|
||||
}
|
||||
for v in [hi + 1, -hi - 1] {
|
||||
let r = wai::int_ops::run1(&loaded, Tensor::new(c, 2, 2, x(v)), &[]).expect_err("one past the range");
|
||||
assert_eq!((r.code, r.at), (Code::FieldRange, Site::Input(0)), "{what}: {v}");
|
||||
let d = run_net_checked(&one, &x(v), c as usize, 2, 2).expect_err("one past the range, directly");
|
||||
assert!(d.out_of_range && d.layer == 0, "{what}: {v}: {d}");
|
||||
}
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// world and world2: T's 3 layers less none, g_s's 5; the synthetic set: 3 and 2.
|
||||
assert_eq!(checked, 8 + 8 + 5, "every layer's range was checked");
|
||||
}
|
||||
|
||||
/// Gate 3, through the whole renderer: on each committed vector, with a latent value or an
|
||||
/// action plane value placed at the edge of a `check` range or past it (`g_s`'s input
|
||||
/// range for `z_0`, `T`'s `x_0` range for a row's value, the ends of `i64`), the renderer
|
||||
/// through the operator set renders the frames the direct renderer gives, or refuses where
|
||||
/// the direct guard refuses: `field_range`, in the same network, at the same step, at the
|
||||
/// same layer.
|
||||
#[test]
|
||||
fn the_renderer_refuses_where_the_direct_guard_refuses_at_the_same_layer() {
|
||||
use int_world::legacy::checked;
|
||||
use wai::int_hyper_synth::RefusalKind;
|
||||
for v in vectors() {
|
||||
let world = int_world::ops::load(&v.t, &v.g_s, v.c).unwrap();
|
||||
let (tg, gg) = int_world::ops::lower(&v.t, &v.g_s, v.c).unwrap();
|
||||
let (xt, xg) = (tg.tensors[1 + world.planes()].hi, gg.tensors[0].hi);
|
||||
let k = 5 * v.lat * v.lat + 10;
|
||||
let mut cases: Vec<(String, Vec<i64>, Vec<Vec<i64>>)> = vec![("committed".into(), v.z0.clone(), v.rows())];
|
||||
for z in [xg, xg + 1, -xg, -xg - 1, i64::MIN, i64::MAX] {
|
||||
let mut z0 = v.z0.clone();
|
||||
z0[k] = z;
|
||||
cases.push((format!("z_0[{k}] = {z}"), z0, v.rows()));
|
||||
}
|
||||
for (row, p, x) in [(0usize, 0usize, xt), (0, 0, xt + 1), (0, 1, -xt), (0, 1, -xt - 1), (3, 2, i64::MAX), (7, 0, i64::MIN)] {
|
||||
let mut rows = v.rows();
|
||||
rows[row][p] = x;
|
||||
cases.push((format!("row {row} plane {p} = {x}"), v.z0.clone(), rows));
|
||||
}
|
||||
let (mut ok, mut refused) = (0, 0);
|
||||
for (what, z0, rows) in &cases {
|
||||
let ops = world.decode_planes(z0, rows, v.lat).map(|c| c.frames);
|
||||
let tr = traced(&world, z0, rows, v.lat);
|
||||
let direct = checked::decode_planes(&v.t, &v.g_s, z0, rows, v.c, v.lat).map(|c| c.frames);
|
||||
match (&ops, &tr, &direct) {
|
||||
(Ok(a), Ok(b), Ok(d)) => {
|
||||
assert!(a == d && b == d, "{} {what}: the frames differ from the direct renderer's", v.name);
|
||||
let plain = int_world::legacy::decode_planes(&v.t, &v.g_s, z0, rows, v.c, v.lat).frames;
|
||||
assert!(plain == *d, "{} {what}: the guard changed the direct renderer's frames", v.name);
|
||||
ok += 1;
|
||||
}
|
||||
(Err(a), Err((net, step, layer, code)), Err(d)) => {
|
||||
assert_eq!((a.kind, *code), (RefusalKind::FieldRange, wai_ops::Code::FieldRange), "{} {what}: {a}", v.name);
|
||||
assert!(d.out_of_range, "{} {what}: the guard refused a value, {d:?}", v.name);
|
||||
assert_eq!((*net, *step, *layer), (d.net, d.step, d.layer), "{} {what}: the network, step and layer of the refusal", v.name);
|
||||
refused += 1;
|
||||
}
|
||||
_ => panic!("{} {what}: through the operator set {ops:?}, traced {tr:?}, directly {direct:?}", v.name, ops = ops.as_ref().map(|f| f.len()), tr = tr.as_ref().map(|f| f.len()), direct = direct.as_ref().map(|f| f.len())),
|
||||
}
|
||||
}
|
||||
assert!(ok >= 1 && refused >= 8, "{}: {ok} rendered and {refused} refused alike", v.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer's own checks (`extensions/integer-operators.md` §13.5, informative; §15.3
|
||||
/// item 2), each refused through the operator set with its code before any frame is
|
||||
/// rendered, where the direct renderer, which checked nothing, rendered a clip from them or
|
||||
/// panicked: a network's `afrac` outside `1..=30` or unequal to the other's, `A` of 0, a row
|
||||
/// that does not carry `A` values (a single agent's moves for the two agent set among them),
|
||||
/// `T` that does not give the latent's shape back, `g_s` that does not read `C` channels
|
||||
/// (`model_mismatch`); a latent that is not `C·lat·lat` values (`geometry_mismatch`), a side
|
||||
/// of 0 or a move whose plane value leaves `i64` (`field_range`), and a latent past the cap
|
||||
/// (`too_large`).
|
||||
#[test]
|
||||
fn the_renderers_own_checks_refuse_before_any_frame() {
|
||||
use wai::int_hyper_synth::{IntLayer, RefusalKind::*};
|
||||
let [w1, w2] = vectors();
|
||||
let direct = |t: &IntModel, g: &IntModel, z0: &[i64], rows: &[Vec<i64>], c: usize, lat: usize| {
|
||||
Some(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| int_world::legacy::decode_planes(t, g, z0, rows, c, lat).frames.len())).ok())
|
||||
};
|
||||
// `d`: what the direct renderer did with the same input (`Some(None)`: it panicked),
|
||||
// or `None` where it takes no such input.
|
||||
let refuse = |what: &str, want, got: Result<int_world::WorldClip, wai::int_hyper_synth::Refused>, d: Option<Option<usize>>| {
|
||||
assert_eq!(got.map(|c| c.frames.len()).map_err(|r| r.kind), Err(want), "{what}");
|
||||
let d = match d {
|
||||
None => "takes no such input".to_string(),
|
||||
Some(None) => "panicked".to_string(),
|
||||
Some(Some(n)) => format!("rendered {n} frames"),
|
||||
};
|
||||
println!("{what}: refused {want:?}; the direct renderer {d}");
|
||||
};
|
||||
let model = |m: &IntModel, f: &dyn Fn(&mut IntModel)| {
|
||||
let mut m = IntModel { afrac: m.afrac, gf: m.gf, shift: m.shift, layers: m.layers.iter().map(clone_layer).collect() };
|
||||
f(&mut m);
|
||||
m
|
||||
};
|
||||
let (t, g, z0, rows, c, lat) = (&w1.t, &w1.g_s, &w1.z0, w1.rows(), w1.c, w1.lat);
|
||||
for (what, tm, gm) in [
|
||||
("T's afrac 0", model(t, &|m| m.afrac = 0), model(g, &|_| {})),
|
||||
("g_s's afrac 31", model(t, &|_| {}), model(g, &|m| m.afrac = 31)),
|
||||
("both afrac 31", model(t, &|m| m.afrac = 31), model(g, &|m| m.afrac = 31)),
|
||||
("T's afrac 13, g_s's 12", model(t, &|m| m.afrac = 13), model(g, &|_| {})),
|
||||
] {
|
||||
let d = direct(&tm, &gm, z0, &rows, c, lat);
|
||||
refuse(what, ModelMismatch, int_world::ops::decode_planes(&tm, &gm, z0, &rows, c, lat), d);
|
||||
}
|
||||
// A = 0: C is all of T's input channels.
|
||||
refuse("A = 0", ModelMismatch, int_world::ops::decode_planes(t, g, &vec![0; 27 * lat * lat], &[], 27, lat), direct(t, g, &vec![0; 27 * lat * lat], &[], 27, lat));
|
||||
// Rows without A values: short, long, and a single agent's moves for the two agent set.
|
||||
let mut short = rows.clone();
|
||||
short[4].pop();
|
||||
refuse("a row of A - 1 values", ModelMismatch, int_world::ops::decode_planes(t, g, z0, &short, c, lat), direct(t, g, z0, &short, c, lat));
|
||||
let mut long = rows.clone();
|
||||
long[0].push(0);
|
||||
refuse("a row of A + 1 values", ModelMismatch, int_world::ops::decode_planes(t, g, z0, &long, c, lat), direct(t, g, z0, &long, c, lat));
|
||||
let moves: Vec<Vec<i64>> = w2.rows().iter().map(|r| vec![r[0], r[1], 0]).collect();
|
||||
refuse("three planes for the two agent T", ModelMismatch, int_world::ops::decode_planes(&w2.t, &w2.g_s, &w2.z0, &moves, w2.c, w2.lat), direct(&w2.t, &w2.g_s, &w2.z0, &moves, w2.c, w2.lat));
|
||||
refuse("a single agent's moves for the two agent T", ModelMismatch, int_world::ops::decode_deltas(&w2.t, &w2.g_s, &w2.z0, &[(1, 0)], w2.c, w2.lat), None);
|
||||
// T that halves the latent's side; g_s that reads 23 channels.
|
||||
let halving = model(t, &|m| if let IntLayer::Conv { stride, .. } = &mut m.layers[2] { *stride = 2 });
|
||||
refuse("T that does not give the latent's shape back", ModelMismatch, int_world::ops::decode_planes(&halving, g, z0, &rows, c, lat), direct(&halving, g, z0, &rows, c, lat));
|
||||
let narrow = model(g, &|m| if let IntLayer::ConvT { weight, .. } = &mut m.layers[0] { weight.truncate(23 * 24 * 16) });
|
||||
refuse("g_s that reads 23 channels", ModelMismatch, int_world::ops::decode_planes(t, &narrow, z0, &rows, c, lat), direct(t, &narrow, z0, &rows, c, lat));
|
||||
// The latent's shape and the moves' values.
|
||||
refuse("z_0 one value short", GeometryMismatch, int_world::ops::decode_planes(t, g, &z0[1..], &rows, c, lat), direct(t, g, &z0[1..], &rows, c, lat));
|
||||
refuse("a side of 0", FieldRange, int_world::ops::decode_planes(t, g, &[], &rows, c, 0), direct(t, g, &[], &rows, c, 0));
|
||||
refuse("a latent past 2^26 elements", TooLarge, int_world::ops::decode_planes(t, g, &[], &rows, c, 1 << 11), None);
|
||||
refuse("a move past i64 at 2^afrac", FieldRange, int_world::ops::decode_deltas(t, g, z0, &[(1, 0), (i64::MAX >> 11, 0)], c, lat), None);
|
||||
// A refused set or row renders no frame: the checks precede frame 0.
|
||||
let world = int_world::ops::load(t, g, c).unwrap();
|
||||
assert_eq!(world.step(z0, &[0, 0], lat).map(|_| ()).map_err(|r| r.kind), Err(ModelMismatch), "step: a row of two values");
|
||||
assert_eq!(world.render(&z0[1..], lat).map(|_| ()).map_err(|r| r.kind), Err(GeometryMismatch), "render: z_0 one value short");
|
||||
assert_eq!(int_world::ops::load(&w2.t, &w2.g_s, w2.c).unwrap().step_action(&w2.z0, 1, w2.lat).map(|_| ()).map_err(|r| r.kind), Err(ModelMismatch), "an action index for the two agent T");
|
||||
}
|
||||
|
||||
fn clone_layer(l: &wai::int_hyper_synth::IntLayer) -> wai::int_hyper_synth::IntLayer {
|
||||
use wai::int_hyper_synth::IntLayer::*;
|
||||
match l {
|
||||
ConvT { out_ch, k, stride, pad, opad, weight, mult, bias } => ConvT { out_ch: *out_ch, k: *k, stride: *stride, pad: *pad, opad: *opad, weight: weight.clone(), mult: mult.clone(), bias: bias.clone() },
|
||||
Conv { out_ch, k, stride, pad, weight, mult, bias } => Conv { out_ch: *out_ch, k: *k, stride: *stride, pad: *pad, weight: weight.clone(), mult: mult.clone(), bias: bias.clone() },
|
||||
Igdn { gamma, beta } => Igdn { gamma: gamma.clone(), beta: beta.clone() },
|
||||
Relu => Relu,
|
||||
Leaky { slope_q, slope_frac } => Leaky { slope_q: *slope_q, slope_frac: *slope_frac },
|
||||
}
|
||||
}
|
||||
|
||||
/// The new rules affect no committed set or vector: every `wai.world.model` set the
|
||||
/// corpus indexes loads (one agent, two agents, the synthetic set), and the committed
|
||||
/// vectors render (gate 2 holds them to their goldens). `git grep -l WHM1` names no other
|
||||
/// world set: the renderer's sets are the two under `wai-rs/tests/vectors/int_world{,2}`
|
||||
/// and the synthetic one under `ops-conformance/sets/world`.
|
||||
#[test]
|
||||
fn the_new_rules_affect_no_committed_set() {
|
||||
for s in indexed() {
|
||||
let (t, g) = (IntModel::from_bytes(&s.member("T.whm")).unwrap(), IntModel::from_bytes(&s.member("g_s.whm")).unwrap());
|
||||
let w = int_world::ops::load(&t, &g, s.c as usize).unwrap_or_else(|r| panic!("{}: {r}", s.name));
|
||||
// The synthetic set's T keeps a latent of any side; it renders a frame from zeros.
|
||||
let lat = 3;
|
||||
let z = vec![0i64; s.c as usize * lat * lat];
|
||||
let (f, h, wd) = w.render(&z, lat).unwrap_or_else(|r| panic!("{}: {r}", s.name));
|
||||
assert_eq!(f.len(), w.plan(lat).map(|(c, h, w)| c * h * w).unwrap(), "{}", s.name);
|
||||
assert!(h > 0 && wd > 0);
|
||||
let row = vec![0i64; w.planes()];
|
||||
assert_eq!(w.step(&z, &row, lat).unwrap().len(), z.len(), "{}: T gives the latent's shape back", s.name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gate 5 of the migration, the whole-renderer half: the renderer through the lowering
|
||||
/// under the reference kernels (the interpreter computes every operator) and under each
|
||||
/// fast policy (the convolutions and inverse GDNs on this crate's fast kernels, serial and
|
||||
/// threaded) renders the same frames, or refuses alike, on each committed vector, and with
|
||||
/// a latent value at `g_s`'s input range, one past it, and a row value at `T`'s `x_0` range
|
||||
/// and one past it. The per-operator half is `int_ops`' equivalence tests, one of which
|
||||
/// counts the fast kernels' runs on these sets.
|
||||
#[test]
|
||||
fn every_kernel_policy_renders_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)];
|
||||
for v in vectors() {
|
||||
let world = int_world::ops::load(&v.t, &v.g_s, v.c).unwrap();
|
||||
let (tg, gg) = int_world::ops::lower(&v.t, &v.g_s, v.c).unwrap();
|
||||
let (xt, xg) = (tg.tensors[1 + world.planes()].hi, gg.tensors[0].hi);
|
||||
let mut cases: Vec<(String, Vec<i64>, Vec<Vec<i64>>)> = vec![("committed".into(), v.z0.clone(), v.rows())];
|
||||
for z in [xg, xg + 1] {
|
||||
let mut z0 = v.z0.clone();
|
||||
z0[7] = z;
|
||||
cases.push((format!("z_0[7] = {z}"), z0, v.rows()));
|
||||
}
|
||||
for x in [xt, -xt - 1] {
|
||||
let mut rows = v.rows();
|
||||
rows[2][1] = x;
|
||||
cases.push((format!("row 2 plane 1 = {x}"), v.z0.clone(), rows));
|
||||
}
|
||||
for (what, z0, rows) in &cases {
|
||||
let render = |p: Policy| with_policy(p, || world.decode_planes(z0, rows, v.lat).map(|c| c.frames).map_err(|r| r.kind));
|
||||
let want = render(POLICIES[0]);
|
||||
assert!(what != "committed" || want.is_ok(), "{}: the committed clip renders", v.name);
|
||||
for p in &POLICIES[1..] {
|
||||
assert!(render(*p) == want, "{} {what}: {p:?} renders other than the interpreter alone", v.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ fn header() -> ModelHeader {
|
|||
fn run(ops: &[CausalOp], t: &IntModel, gs: &IntModel, z0: &[i64], c: usize, lat: usize) -> (String, String, Vec<Vec<u8>>) {
|
||||
let s = replay(&header(), ops, true).expect("replay verifies + orders");
|
||||
let deltas: Vec<(i64, i64)> = s.actions.iter().map(|a| (a.dx as i64, a.dy as i64)).collect();
|
||||
let clip = decode_deltas(t, gs, z0, &deltas, c, lat);
|
||||
let clip = decode_deltas(t, gs, z0, &deltas, c, lat).expect("the merged log renders");
|
||||
(hex32(&s.session_hash), format!("{:016x}", clip.clip_hash()), clip.frames)
|
||||
}
|
||||
|
||||
|
|
|
|||
78
wai/wai-web/demo/verify_int_world_wasm.mjs
Normal file
78
wai/wai-web/demo/verify_int_world_wasm.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// The wasm32 integer renderer of wai.world.model (extensions/integer-operators.md §13.5,
|
||||
// informative) equals the native goldens: the browser crate's WorldModel, built for
|
||||
// wasm32, renders each committed world vector, frame by frame, to the SHA-256 its
|
||||
// sha256.json sidecar states for every frame and for the clip. The one-agent vector is
|
||||
// driven by action index (step) and by move (stepDelta) alike; the two-agent vector
|
||||
// (A = 4) by its rows of action planes (stepPlanes).
|
||||
//
|
||||
// node verify_int_world_wasm.mjs [path/to/wai_web.js] [path/to/wai-rs/tests/vectors]
|
||||
//
|
||||
// Exit 0 iff every check passes.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const pkg = resolve(here, process.argv[2] ?? './pkg-node/wai_web.js');
|
||||
const vectors = resolve(process.argv[3] ?? resolve(here, '../../wai-rs/tests/vectors'));
|
||||
const wai = await import(pathToFileURL(pkg).href);
|
||||
|
||||
const rd = (rel) => new Uint8Array(readFileSync(`${vectors}/${rel}`));
|
||||
const js = (rel) => JSON.parse(readFileSync(`${vectors}/${rel}`, 'utf8'));
|
||||
const sha = (b) => createHash('sha256').update(b).digest();
|
||||
const hex = (b) => Buffer.from(b).toString('hex');
|
||||
// The reference action set: stay, +x, −x, +y, −y.
|
||||
const D = [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]];
|
||||
|
||||
let failed = 0;
|
||||
const check = (what, frames, conf, side) => {
|
||||
const want = side.frames_sha256;
|
||||
const got = frames.map((f) => hex(sha(f)));
|
||||
const off = got.map((g, i) => (g === want[i] ? -1 : i)).filter((i) => i >= 0);
|
||||
const clip = hex(sha(Buffer.concat(frames.map((f) => sha(f)))));
|
||||
const n = conf.frame * conf.frame * 3;
|
||||
const sizes = frames.every((f) => f.length === n);
|
||||
if (got.length !== want.length || off.length || clip !== side.clip_sha256 || !sizes) {
|
||||
failed++;
|
||||
console.log(`FAIL ${what}: ${got.length} frames for ${want.length}; frames ${JSON.stringify(off)} differ; clip ${clip}${sizes ? '' : '; a frame is not frame² RGB'}`);
|
||||
} else {
|
||||
console.log(`ok ${what}: ${got.length} frames, clip ${clip}`);
|
||||
}
|
||||
};
|
||||
|
||||
// One agent (A = 3), by action index and by move.
|
||||
{
|
||||
const conf = js('int_world/world.json');
|
||||
const side = js('int_world/sha256.json').world;
|
||||
const one = 1 << conf.afrac;
|
||||
for (const by of ['step', 'stepDelta']) {
|
||||
const wm = new wai.WorldModel(rd('int_world/world_T.bin'), rd('int_world/world_gs.bin'), conf.c, conf.lat);
|
||||
const frames = [wm.reset(Int32Array.from(conf.z0))];
|
||||
if (wm.framePx !== conf.frame) { failed++; console.log(`FAIL world ${by}: framePx ${wm.framePx}`); }
|
||||
for (const a of conf.actions) frames.push(by === 'step' ? wm.step(a) : wm.stepDelta(D[a][0], D[a][1]));
|
||||
check(`world (one agent), ${by}`, frames, conf, side);
|
||||
// The same moves as rows of three planes.
|
||||
if (by === 'step') {
|
||||
const wp = new wai.WorldModel(rd('int_world/world_T.bin'), rd('int_world/world_gs.bin'), conf.c, conf.lat);
|
||||
const fp = [wp.reset(Int32Array.from(conf.z0))];
|
||||
for (const a of conf.actions) fp.push(wp.stepPlanes(Int32Array.from([D[a][0] * one, D[a][1] * one, 0])));
|
||||
check('world (one agent), stepPlanes', fp, conf, side);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two agents (A = 4), by rows of four planes.
|
||||
{
|
||||
const conf = js('int_world2/world2.json');
|
||||
const side = js('int_world2/sha256.json').world2;
|
||||
const one = 1 << conf.afrac;
|
||||
const wm = new wai.WorldModel(rd('int_world2/world2_T.bin'), rd('int_world2/world2_gs.bin'), conf.c, conf.lat);
|
||||
const frames = [wm.reset(Int32Array.from(conf.z0))];
|
||||
for (const [a, b] of conf.pairs) frames.push(wm.stepPlanes(Int32Array.from([...D[a], ...D[b]].map((x) => x * one))));
|
||||
check('world2 (two agents), stepPlanes', frames, conf, side);
|
||||
}
|
||||
|
||||
console.log(failed ? `${failed} check(s) failed` : 'every world vector renders to its native goldens in wasm32');
|
||||
process.exit(failed ? 1 : 0);
|
||||
|
|
@ -1,82 +1,88 @@
|
|||
//! In-browser **byte-exact interactive world model** — zero JS ML runtime.
|
||||
//!
|
||||
//! The same pure-i64 Rust that renders `wai.world.model`'s integer reference world
|
||||
//! The same Rust that renders `wai.world.model`'s integer reference world
|
||||
//! (`wai::int_world`) compiles to wasm32 and produces the IDENTICAL frames here: an
|
||||
//! action-conditioned next-frame model whose transition + synthesis are integer
|
||||
//! convs + integer IGDN. So the page can drive the world with the arrow keys, render
|
||||
//! every frame in pure i64, and re-check each frame's hash against the native
|
||||
//! reference. And every keystroke is a signed action op whose trace replays to one
|
||||
//! portable **session hash** ([`world_session_hash`]) — replay-equivalence, a
|
||||
//! generative interactive world you can put in a receipt.
|
||||
//! action-conditioned next-frame model whose transition and synthesis run through the
|
||||
//! integer operator set (`extensions/integer-operators.md` §13.5, `world/1`), loaded once
|
||||
//! when the model is made. So the page can drive the world with the arrow keys, render
|
||||
//! every frame in integers, and re-check each frame's hash against the native reference.
|
||||
//! And every keystroke is a signed action op whose trace replays to one portable
|
||||
//! **session hash** ([`world_session_hash`]) — replay-equivalence, a generative
|
||||
//! interactive world you can put in a receipt.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use wai::int_hyper_synth::{decode as synth_decode, IntModel};
|
||||
use wai::int_hyper_synth::{IntModel, Refused};
|
||||
use wai::int_world::World;
|
||||
|
||||
/// A stateful integer world model: hold the transition `T` + synthesis `g_s`, carry
|
||||
/// the latent state `z`, and advance it one action at a time — the live-play path.
|
||||
fn refused(r: Refused) -> JsError {
|
||||
JsError::new(&r.to_string())
|
||||
}
|
||||
|
||||
/// A stateful integer world model: hold the transition `T` + synthesis `g_s`, lowered and
|
||||
/// loaded once, carry the latent state `z`, and advance it one action at a time — the
|
||||
/// live-play path.
|
||||
#[wasm_bindgen]
|
||||
pub struct WorldModel {
|
||||
t: IntModel,
|
||||
gs: IntModel,
|
||||
c: usize,
|
||||
world: World,
|
||||
lat: usize,
|
||||
z: Vec<i64>,
|
||||
frame_px: usize,
|
||||
}
|
||||
|
||||
impl WorldModel {
|
||||
/// Take `z` as the latent state and render it.
|
||||
fn advance(&mut self, z: Vec<i64>) -> Result<Box<[u8]>, JsError> {
|
||||
let (f, h, _) = self.world.render(&z, self.lat).map_err(refused)?;
|
||||
self.z = z;
|
||||
self.frame_px = h;
|
||||
Ok(f.into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WorldModel {
|
||||
/// Load the two WHM1 models (`world_T.bin`, `world_gs.bin`); `c`/`lat` from
|
||||
/// `world.json`.
|
||||
/// `world.json`. A set `world/1` refuses (`afrac` outside `1..=30` or unequal, no
|
||||
/// action plane, a network's rules) is refused here.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(t_bin: &[u8], gs_bin: &[u8], c: u32, lat: u32) -> Result<WorldModel, JsError> {
|
||||
Ok(WorldModel {
|
||||
t: IntModel::from_bytes(t_bin).map_err(JsError::new)?,
|
||||
gs: IntModel::from_bytes(gs_bin).map_err(JsError::new)?,
|
||||
c: c as usize,
|
||||
lat: lat as usize,
|
||||
z: Vec::new(),
|
||||
frame_px: 0,
|
||||
})
|
||||
let t = IntModel::from_bytes(t_bin).map_err(JsError::new)?;
|
||||
let gs = IntModel::from_bytes(gs_bin).map_err(JsError::new)?;
|
||||
let world = wai::int_world::load(&t, &gs, c as usize).map_err(refused)?;
|
||||
Ok(WorldModel { world, lat: lat as usize, z: Vec::new(), frame_px: 0 })
|
||||
}
|
||||
|
||||
/// Reset the world to an initial latent `z0` (`c·lat·lat` ints at `2^afrac`);
|
||||
/// returns the rendered frame 0 (HWC RGB `u8`).
|
||||
pub fn reset(&mut self, z0: &[i32]) -> Box<[u8]> {
|
||||
self.z = z0.iter().map(|&v| v as i64).collect();
|
||||
let (f, h, _) = synth_decode(&self.gs, &self.z, self.c, self.lat, self.lat);
|
||||
self.frame_px = h;
|
||||
f.into_boxed_slice()
|
||||
pub fn reset(&mut self, z0: &[i32]) -> Result<Box<[u8]>, JsError> {
|
||||
self.advance(z0.iter().map(|&v| v as i64).collect())
|
||||
}
|
||||
|
||||
/// Advance the integer transition by one `action` (index into the reference
|
||||
/// action set: 0 stay, 1 +x, 2 −x, 3 +y, 4 −y) and render — pure i64, byte-exact.
|
||||
pub fn step(&mut self, action: u32) -> Box<[u8]> {
|
||||
self.z = wai::int_world::step(&self.t, &self.z, action as usize, self.c, self.lat);
|
||||
let (f, _, _) = synth_decode(&self.gs, &self.z, self.c, self.lat, self.lat);
|
||||
f.into_boxed_slice()
|
||||
/// Advance the transition by one `action` (index into the reference action set:
|
||||
/// 0 stay, 1 +x, 2 −x, 3 +y, 4 −y) and render, byte-exact.
|
||||
pub fn step(&mut self, action: u32) -> Result<Box<[u8]>, JsError> {
|
||||
let z = self.world.step_action(&self.z, action as usize, self.lat).map_err(refused)?;
|
||||
self.advance(z)
|
||||
}
|
||||
|
||||
/// Advance the transition by raw `(dx, dy)` deltas and render — the shared-world
|
||||
/// path (a merged multi-actor log decodes to a delta sequence via
|
||||
/// [`replay_world_ops`]).
|
||||
#[wasm_bindgen(js_name = stepDelta)]
|
||||
pub fn step_delta(&mut self, dx: i32, dy: i32) -> Box<[u8]> {
|
||||
self.z = wai::int_world::step_delta(&self.t, &self.z, dx as i64, dy as i64, self.c, self.lat);
|
||||
let (f, _, _) = synth_decode(&self.gs, &self.z, self.c, self.lat, self.lat);
|
||||
f.into_boxed_slice()
|
||||
pub fn step_delta(&mut self, dx: i32, dy: i32) -> Result<Box<[u8]>, JsError> {
|
||||
let z = self.world.step_delta(&self.z, dx as i64, dy as i64, self.lat).map_err(refused)?;
|
||||
self.advance(z)
|
||||
}
|
||||
|
||||
/// Advance the transition by an explicit set of action **planes** (length = the
|
||||
/// model's `A`; values at `2^afrac`) and render — the multi-agent path, where a
|
||||
/// step carries every agent's action (e.g. `[dxA,dyA,dxB,dyB]` for two agents).
|
||||
/// Advance the transition by an explicit set of action **planes** (one value per plane
|
||||
/// of `T`, at `2^afrac`) and render — the multi-agent path, where a step carries every
|
||||
/// agent's action (e.g. `[dxA,dyA,dxB,dyB]` for two agents).
|
||||
#[wasm_bindgen(js_name = stepPlanes)]
|
||||
pub fn step_planes(&mut self, planes: &[i32]) -> Box<[u8]> {
|
||||
pub fn step_planes(&mut self, planes: &[i32]) -> Result<Box<[u8]>, JsError> {
|
||||
let q: Vec<i64> = planes.iter().map(|&v| v as i64).collect();
|
||||
self.z = wai::int_world::step_planes(&self.t, &self.z, &q, self.c, self.lat);
|
||||
let (f, _, _) = synth_decode(&self.gs, &self.z, self.c, self.lat, self.lat);
|
||||
f.into_boxed_slice()
|
||||
let z = self.world.step(&self.z, &q, self.lat).map_err(refused)?;
|
||||
self.advance(z)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter, js_name = framePx)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue