# Integer decoder throughput — fast kernels, same bytes > **Status.** Implementation note for the reference implementation (`wai-rs`), > informative, not normative. Nothing here changes a bitstream or a conformance > hash, and decoded outputs keep the same integers by the argument and tests of > §2. Every number below was measured on **one machine** under the method stated > in §3. Other machines, other loads and other models will give other numbers. ## 1. What changed The integer decoders (`int_synth`, `int_hyper_synth`, `int_audio`, `int_splat`, `int_world`, the neural video keyframe, and the JPEG-AI synthesis in `jpegai_synthesis` / `int_transformer`) now run their hot kernels through `wai-rs/src/int_kernels.rs`: - **Convolution and transposed convolution** — register-blocked tiles (4 output channels × 8 adjacent outputs held in registers across every tap), zero-padded inputs, and a polyphase formulation of the transposed convolution so every tap reads a contiguous run of inputs. On aarch64 the 32×32→64-bit multiply-accumulate uses NEON widening instructions (`std::arch`, one small submodule); every other target runs the same kernel as portable Rust. - **IGDN** — the same register blocking, with NEON on aarch64 when every `x²` fits in 32 bits, and a portable 64-bit path otherwise. - **JPEG-AI channel attention, LayerNorm and element-wise gates** — rows, positions and element chunks spread over threads; the attention sums narrowed from `i128` to `i64` where a bound proves the result identical. - **Threads** — `std::thread::scope` workers taking tiles from a shared queue. No new dependency. None on wasm (the browser sink runs the same kernels single-threaded) or under the model checker. The worker count is automatic (all cores, capped so each worker has enough work), explicit through `int_kernels::Policy`, or set process-wide with `WAI_INT_THREADS`. The scalar convolution, transposed-convolution, IGDN, channel-attention (MDTA) and channel-LayerNorm kernels they replace are kept verbatim in `reference` modules beside each caller and stay selectable at run time: `int_kernels::with_policy(Policy::reference(), || decode(..))`. They are the executable specification the fast kernels are checked against. The JPEG-AI element-wise steps (the GDFN gate and the residual additions in `int_transformer`, the activations and gates in `jpegai_synthesis`) have no separate reference copy: they were 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 policy they run on one worker. ## 2. Why the output cannot change Integer arithmetic is exact, so re-partitioning the work cannot change a result as long as every output element is computed from the same terms, computed the same way, in the same accumulator type, and no step's overflow behaviour changes. The kernels are written to that rule (stated in full in the `int_kernels` module documentation): 1. **Work is split only across output elements.** A thread owns whole tiles; a SIMD lane owns one output element. No sum is split between threads or lanes. 2. **Each element adds the same terms in the reference's order**, from the same starting value, apart from terms that are zero. The fast kernel may add terms the reference skips because they are zero (a zero input, a zero-padding sample) and may omit terms the reference adds although they are zero (a tap whose weight is zero for every channel of a block). Adding or omitting a zero changes no partial sum, and a zero product cannot overflow. 3. **A narrower type only where a bound over the actual operands proves the same integer.** 32×32→64-bit products require both operands to have `|v| ≤ i32::MAX`. An `i64` accumulator replaces an `i128` one only when `terms · max|w| · max|x| ≤ i64::MAX`. IGDN and channel attention use the analogous bounds. 4. **A convolution takes the fast path only when no step of the reference can overflow.** The bound `terms · max|w| · max|x|` must fit the reference's accumulator (`i64`, or `i128` where the reference accumulates in `i128`). Otherwise the reference itself runs. This matters because the reference transposed convolutions accumulate into border positions and then crop them. The fast kernel never computes those positions, so only the reference can reproduce an overflow there. Under the bound nothing can overflow, so the SIMD lanes, which wrap where overflow-checked scalar code traps, are exact too. IGDN, channel attention, LayerNorm and the element-wise gates compute every value the reference computes, narrowing only under their own bounds. The per-output requantisation is the same expression in both, evaluated at the same kept positions. 5. **Anything the fast path does not handle goes to the reference**: malformed geometry, short buffers, shift amounts outside the narrowed range, padding whose padded input would exceed the fast path's allocation cap or be out of proportion to what the reference reads and writes (rule 6), and a transposed geometry whose sizes the reference's own arithmetic cannot form. That is an uncropped plane `cout·(ho + 2·pad)·(wo + 2·pad)` beyond the allocation cap (the int_hyper, int_audio and JPEG-AI references allocate it before cropping, sized in unchecked `usize`), or an int_synth output size that overflows its reference's `i64` arithmetic. Such input gets the reference's own behaviour, in either profile. 6. **The fast path's own work cannot overflow and stays in proportion to the reference's.** Its buffer sizes and indices are bounded by the checks of rule 5, and its worker-count estimates are widened or saturating. The reference performs none of these steps, so none of them may be the step at which a fast decode panics. What it allocates and loops over is bounded the same way. The zero-padded input copy may hold at most 16 times the elements the reference reads and writes (input, weights and output). The largest ratio is 0.9 over the committed vectors and 1.0 over the benchmark shapes (both measured with the check logged), and 7.2 over every shape the randomised convolution tests can draw (enumerated from their parameter ranges with the layout arithmetic mirrored), so none of those changes path. Nothing grows with the stride. A transposed convolution builds tap tables only for the (row, column) phases that hold output in a tile, so there are at most as many tables as the tile has output elements per channel, and their taps together number at most `cin · kh · kw`. A regular convolution lays out at most as many column phases as its padded row has columns. What these rules give, precisely: - **Integers.** For every input the reference returns from, every policy returns the same integer for every output element. In a release build (overflow checks off) that includes inputs whose accumulation wraps, at a kept output position or only at a border position a reference transposed convolution computes and then crops. - **Panics in an overflow-checked (debug) build.** A fast policy panics on the inputs where the reference panics, including an overflow confined to a cropped border position and one in the reference's own size arithmetic (rule 5). Only the panic itself can differ: another element may fail first, and a panic raised on a spawned worker reaches the caller as the thread scope's own panic ("a scoped thread panicked"), not the reference's arithmetic-overflow message. - **Outside the claim: an allocation that fails.** The two paths allocate different buffers. Besides the output both allocate, the fast path allocates the padded input copy, per-tile tap tables and per-row accumulators (each bounded by rule 6), and the int_hyper, int_audio and JPEG-AI references allocate a transposed convolution's uncropped plane. While the fast path runs, the padded copy, the output and that plane each hold at most the allocation cap (2³² elements on 64-bit targets, 2²⁸ on 32-bit ones). Still, where memory runs out, or on a 32-bit target where one buffer would exceed `isize::MAX` bytes (which Rust reports as a "capacity overflow" panic), one path can fail where the other does not. What was checked (the claim rests on these, not on the argument alone): - **Element-by-element comparison with the reference** over randomised shapes (strides, paddings, output paddings, groups, kernels smaller than the stride, inputs narrower than the stride) and operand ranges. The ranges cover the 32-bit path, the 64-bit path, the `i128` path, both sides of every narrowing bound, and zero-heavy inputs. Every comparison runs under one worker, two explicit multi-threaded tilings, and the automatic count. Files: the `kernel_equivalence` and `igdn_equivalence` test modules in each kernel's file. - **Overflow parity.** Three kinds of operand are tested: ones that overflow the reference's accumulator at a kept output position (the `int_hyper` transposed and regular convolutions, including rows wide enough for the SIMD lanes to engage), ones that overflow it only at a border position a transposed convolution computes and then crops (the `int_hyper` and `int_audio` transposed convolutions, `i64` accumulator, and the JPEG-AI one, `i128`), and operands beyond the bound (`int_synth`). With overflow checks on, the tests require every policy to panic where the reference panics; with them off, the same integers. All of these run in both profiles. The border case was missed in the kernels' first form, whose tests covered overflow at kept positions only. Review found it with a probe through the public `int_hyper_synth::run_net` and `int_transformer::conv_transpose`, and with the `int_audio` cropped-border test run against that form. With overflow checks on, the reference panicked in all three and every fast policy returned a value. In release, all policies returned the same integers. Rule 4's routing and the border tests were added then. With rule 4's routing deleted in a scratch copy of the sources, the three border tests fail in an overflow-checked build, as does the SIMD-width kept-position test. - **Rule 6.** A transposed geometry with padding and output padding far beyond the kernel (a 1×1 output whose padded input would have about 2⁴⁰ elements) is now left to the reference, and an explicit worker count of any size, up to `usize::MAX`, gives the same integers as one worker. Both tests fail on the kernels as they stood before rule 6: the geometry was accepted, and the worker count overflowed `4 · threads` in an overflow-checked build. The channel attention's worker-count estimate `heads · c² · hw` is now widened before it is multiplied, because on a 32-bit target it can exceed `usize` (for example 128 channels per head over 2¹⁸ positions). That last change is argued, not tested: the inputs that reach it are too large for a unit test. - **Rule 6, stride and padding.** Before this rule's final form, the fast transposed kernel built one tap table for every (row, column) phase, `sh · sw` tables per tile, whether or not a phase held output. A 1×1 input upsampled by a huge stride is a 1×1 output that the reference computes in a handful of operations. Review found, with a probe through the public API, that the fast path panicked with "capacity overflow" at stride 2²⁹ (which fits a WHM1 model's `u32` stride field and the JSON specs), and on the multiplication `sh · sw` at stride 2³² in an overflow-checked build. At 2¹⁶ the kernel committed memory for 2³² tables: the geometry test, run against that earlier kernel, was killed after more than a minute, and swap grew by 17 GiB while it ran. Until the tables were limited to phases holding output, the "Integers" statement above did not hold for such geometry under the default policy. Decoded bytes of valid streams were never affected. The regular convolution's padded row grew with the stride too, bounded only by the allocation cap. Now `tests/int_kernels_geometry.rs` runs these cases through the public decoders (`int_hyper_synth::run_net`, `int_synth::decode_from_spec` and the JPEG-AI `conv_transpose` / `conv_grouped`) under every policy, and checks each against a hand computation: - a 1×1 input upsampled by strides from 1 to `usize::MAX`; - a 5×5 input whose stride reaches or exceeds its padded width; - padding of 2¹², far beyond a 3×3 or 1×1 kernel. `int_audio` has the same stride test beside its kernel. A randomised test compares both convolutions element by element at strides up to 16, beyond kernel and input. There, row bands hold fewer rows than the stride as well as more, and many phases receive no tap. Unit tests check that geometry whose padded copy is out of proportion (about 2²⁴ and 2²⁷ elements where the reference touches 11 and 6) goes to the reference. - **Rule 6, at the factor.** The committed vectors, the benchmark shapes and the randomised tests all stay below half the factor (the ratios above), so dedicated tests pin both sides of it. Geometry whose padded copy is exactly 16 times what the reference touches is accepted, and the fast kernel runs. Geometry one element, two elements, one padding step or one stride step beyond it is refused, and the reference runs. Each refused shape is small and well-formed and is measured with the kernels' own layout arithmetic, and a sweep over about 7,400 small regular and transposed shapes checks that acceptance is exactly "padded ≤ 16 · touched", equality included. So nothing but the proportion refuses these shapes. Both paths give the same integers, so the output cannot show which one ran. Test builds therefore count fast-kernel entries per thread. The count shows the fast kernel running once at the factor and never beyond it, through each of the six dispatching functions: `int_hyper_synth` regular and transposed, `int_synth`, `int_audio`, and the JPEG-AI `conv_grouped` and `conv_transpose`. In each case the output equals the reference's and a hand computation. Three mutations were each run against these tests, and each made all five fail: a factor of 17, the proportion check removed, and `>` changed to `≥`. - **Rule 5, the reference's own size arithmetic.** Review found, again with a probe through the public API, that the fast geometry check bounded only what the fast path lays out, not two sizes the references compute in unchecked arithmetic. One is the uncropped plane of the int_hyper, int_audio and JPEG-AI transposed convolutions, in `usize`. The other is int_synth's output size, in `i64`. An input of two or more rows with a stride and padding of 2⁶² overflows them. So does, within a WHM1 model's `u32` fields, a stride of 2³² − 1 with padding of 2³¹ − 1. The reference then panics with overflow checks on. With them off, the uncropped-plane references index out of a wrapped buffer. Every fast policy returned a value, the mathematically correct one, so under the default policy a model the reference cannot decode decoded. No decoded byte changed for any input the reference returns from. The earlier geometry tests missed this because their huge-stride transposed cases all used a 1×1 input, where `(h − 1)·stride` is 0. Rule 5 now covers both sizes. Beyond the cap the reference runs and fails, or not, in its own way. A plane that is merely huge also goes to the reference, so the fast path does not decode what the reference could not allocate. int_synth's dispatch evaluates its reference's `i64` expression step by step, and runs the fast kernel only where that expression neither overflows nor disagrees with the fast geometry. `tests/int_kernels_geometry.rs` runs these inputs through `int_hyper_synth::run_net`, the JPEG-AI `conv_transpose` and `int_synth::decode_from_spec`, and requires the reference's outcome under every policy. That outcome is a panic, except for int_synth with overflow checks off, where the wrapped arithmetic still yields the correct image. `int_audio` has the 1-D form beside its kernel: a buffer size that overflows `usize`, and one no allocation can hold. A unit test pins the bound at the cap and one row past it, where nothing else refuses the geometry. Mutations: - With the plane bound removed, the int_hyper, JPEG-AI, int_audio and bound tests fail in both profiles. The int_synth tests still pass on the `i64` check alone. - With both removed, the int_synth geometry test fails with overflow checks on. With them off it passes, because every policy then returns the same image. The int_synth unit test, which also counts fast-kernel entries, fails in both profiles. - With the `i64` check removed and the bound kept, every test passes. At the current cap the bound alone keeps that expression from overflowing, and the check keeps int_synth's rule from depending on the cap's value. The JPEG-AI and int_hyper u32-width cases were also run alone against the unbounded geometry, and failed there in both profiles. - **Every committed vector that reaches the kernels, under every policy** (`tests/int_kernels_vectors.rs`): int_hyper, int_audio, int_splat, int_world (one and two agents), the neural-keyframe video and the four `neural-conformance` int_synth vectors, plus the real-footage video. Each decodes byte-identically under the reference and each fast policy, and reproduces its frozen hash; the video checks also compare the frame count. The plain int_video vector (block motion compensation and rANS only) never reaches the kernels and is not in this test. The int_synth photo vector also still equals its independent integer-forward reference `ref_rgb.bin`. - **CI.** The `wai-conformance` workflow runs the lib tests with every learned integer codec's feature (so every `kernel_equivalence` and `igdn_equivalence` module, the overflow-parity tests and the rule-6 tests), `tests/int_kernels_vectors.rs` and `tests/int_kernels_geometry.rs`, once with overflow checks on and once in release, on each leg of its matrix (Linux x86_64 and arm64, macOS arm64, Windows x86_64). `wai-byte-equality` and `wai-formal-verification` also fire on a change to `int_kernels.rs`, which their crates `#[path]`-include. - **The tests can fail.** Deliberate defects were injected one at a time: an off-by-one tap offset, a wrong row band, a skipped SIMD tail, a swapped NEON lane, a bypassed narrowing or overflow gate, a missing IGDN clamp, a wrong phase in the strided pad, a mis-indexed attention row. The tests caught each one. Two initially survived, an IGDN sum-bound bypass and a bypassed SIMD overflow gate, and each led to a new test case that catches it. Separately, review found a real defect in the new 64-bit IGDN path: with an all-zero `gamma`, the square of a huge input was formed in `i64`, where the reference squares in `i128`. It was fixed, with a test that fails without the fix. - **wasm.** `wai-web` builds for `wasm32-unknown-unknown`. That build caught one real 32-bit defect in the new code (an allocation cap that overflowed a 32-bit `usize`), since fixed. The equivalence tests and every committed vector also pass on `wasm32-wasip1` under wasmtime 49, exercising the portable, single-threaded, 32-bit-`usize` path. Skipped there: tests that need unwinding or a temp directory, and one pre-existing rANS test whose data generator overflows a 32-bit `usize`. - **Formal verification unchanged.** `cargo kani` in `wai-verify` (Kani 0.67): every harness still verifies. The harnesses do not cover the convolution or IGDN kernels, fast or reference (SPEC §7, *machine-checked totality*). The fast kernels' equivalence is **tested, not proved**. ## 3. Method - **Machine:** an arm64 macOS laptop [model removed 2026-09-30], 18 logical cores, macOS 27.0; rustc 1.98.0. macOS reports the cores as two performance levels (`hw.perflevelN.name`): 6 "Super" and 12 "Performance". It reports no efficiency cores. The harness prints the levels as the OS names them. - **Revision:** measured once rule 6 had its final form, on the kernels as they stand apart from one later routing check. That check, rule 5's bound on the reference's size arithmetic, adds a few checked multiplications per transposed convolution and sends no benchmarked shape down a different path: every benchmarked uncropped plane has fewer than 2²⁴ elements, against a cap of 2³², and every int_synth size is far below `i64::MAX`. That check was not itself timed. The other later changes are a test-build counter (which compiles to nothing outside tests), tests, comments and lint declarations. - **Load and runs:** the machine was shared with other workloads throughout, and the load average read between 6.2 and 10.5 at the start and end of the runs. Four complete runs of the benchmark and four of the per-layer profile were made back to back. From run to run, the best time for one case under one policy varied by 2% to 67%, mostly by 10% to 30%. The largest variations were on all cores, up to 67% for the all-core IGDN kernel with Q12 activations. The tables give, for each case and policy, the best over all rounds of the four runs. They are best-case figures for this machine under this load, not typical ones. Each speed-up is the ratio of those best times, the same ratio the harness reports within one run. The range of speed-ups within single runs is given under the tables. Nothing here compares revisions. - **Build:** `wai-rs` release profile (`opt-level = 3`, LTO, `codegen-units = 1`), `--no-default-features --features neural_int,video_int,audio_int,world_int,splat_int`. - **Harness:** `src/bin/wai_decode_bench.rs`. For each case, three policies run **interleaved in one process on the same inputs**, rotating the order each round: the reference kernels, the fast kernels on one thread (`1T`), and the fast kernels with the automatic worker count (`auto`, up to 18 here). The harness reports the best time over the rounds (medians are in its output). Every run's output is hashed, and the harness aborts rather than report a time for an output that differs from the reference's. In all eight runs, every output under every policy matched the reference's. - **Inputs:** self-contained. Kernel and whole-decoder cases use deterministic pseudo-random weights at the real layer shapes. The int_hyper `g_s` is the mbt2018-mean quality-6 topology the export tool emits (M = 320, N = 192; int12 weights, `afrac` 12 / `gf` 16 / `shift` 24, like the committed models). The JPEG-AI synthesis uses the exact weight shapes of the reference model_y and model_uv nets, with 16-bit weights and Q16 activations. The `vector` cases are the committed conformance vectors, and not all of them are trained models: - **Trained:** int_synth photo, int_world, int_splat and int_audio. Their export tools train the float model, then quantize it. int_synth and int_audio were trained on captured content (a photograph, an audio clip); int_world and int_splat on procedurally generated content. - **Random weights:** the int_hyper tiny conditional codec. Its generator, `tools/wai_int_hyper_tiny_vector.py`, draws the integer weights from a seeded generator. - **No learned model:** int_video real footage, which is integer block motion compensation plus rANS. So no trained int_hyper model was timed: the `g_s` row uses synthetic weights and the int_hyper vector uses random ones. Time depends on shape and on operand *width*, not on trained values, with one exception that is visible below: activation magnitude selects the IGDN arithmetic path. > **Correction (2026-10-07).** The int_audio vector timed here was trained on, > and coded, a third-party recording, not content made for the repository. It > has been replaced: the int_audio model and vector are now trained and coded on > a signal `tools/wai_int_audio_export.py` generates from a seed. The model has > the same shape (16 channels, 512 latent samples, 8192 output samples); the > row's figures are for the earlier vector, and the current one's time can > differ through its activations (the exception above) and the length of its > rANS stream. ## 4. Results ### Whole decoders | decode | reference | fast, 1 thread | fast, auto | speed-up 1T / auto | |---|---:|---:|---:|---:| | int_hyper `g_s`, mbt2018-mean q6 topology (synthetic weights), 16² latent → 256² RGB | 2961 ms (0.022 MPix/s) | 299 ms (0.22 MPix/s) | 35.3 ms (1.86 MPix/s) | 9.9× / 84.0× | | JPEG-AI synthesis, luma + chroma nets (reference topology, synthetic weights), 256² | 5556 ms (0.012 MPix/s) | 452 ms (0.14 MPix/s) | 79.7 ms (0.82 MPix/s) | 12.3× / 69.7× | | int_synth trained photo vector, 128² | 14.3 ms | 5.25 ms | 3.69 ms | 2.7× / 3.9× | | int_world trained interactive clip vector, 9 frames | 28.5 ms (316 frames/s) | 3.82 ms (2356 frames/s) | 3.76 ms (2396 frames/s) | 7.5× / 7.6× | | int_splat trained attribute vector, 1024 Gaussians | 1.99 ms | 0.29 ms | 0.27 ms | 6.9× / 7.5× | | int_audio trained clip vector, 8192 samples | 3.94 ms | 0.93 ms | 0.89 ms | 4.2× / 4.4× | | int_hyper tiny conditional vector (random weights), 256 px | 14.7 µs | 7.9 µs | 7.8 µs | 1.9× / 1.9× | | int_video half-pel real-footage vector, 8 frames 96×96 (no learned model: motion compensation + rANS only) | 2.38 ms | 2.32 ms | 2.36 ms | 1.0× / 1.0× | Within single runs, the `g_s` speed-ups ranged from 8.5× to 10.5× on one thread and from 71.5× to 84.0× on all cores. For the JPEG-AI synthesis the ranges were 11.7× to 13.0× and 57.0× to 69.7×. The automatic worker count follows each layer's work: one worker per 2²⁰ multiply-accumulates, up to the core count. Logged during the vector tests on this machine (18 cores), it stayed at one to three workers per call for the int_hyper tiny, int_audio, int_splat, int_world and neural-keyframe video decodes, so `auto` adds little over one thread there. The int_synth photo layers are larger: `auto` ran 10 to 18 workers on its four transposed convolutions, which is why that row gains more from all cores. The int_video decode has no learned kernel. Its time is the rANS entropy decode, which is sequential by construction and was not changed. ### Kernels (layer shapes from the two decoders above, and one regular convolution) | kernel | reference | fast, 1 thread | fast, auto | |---|---:|---:|---:| | convT 192→192, 5×5, stride 2, 64²→128² (int_hyper `g_s`) | 2.5 GMAC/s | 31.7 GMAC/s | 289 GMAC/s | | conv 192→192, 3×3, 64² (a regular convolution at the `g_s` width; not a layer of either decoder) | 4.0 | 31.7 | 273 | | IGDN c = 192, 128², Q12 activations (x² fits 32 bits) | 1.1 | 9.0 | 106 | | IGDN c = 192, 128², x² > 2³¹ (64-bit path) | 1.1 | 6.5 | 58 | | conv 128→128, 3×3, 64² (JPEG-AI CAB) | 1.9 | 32.0 | 264 | | conv 1×1 128→1024, 64² (JPEG-AI GDFN in) | 1.9 | 22.7 | 207 | | depthwise 3×3 ×1024, 64² (JPEG-AI GDFN) | 1.7 | 6.6 | 17.1 | | conv 128→128, 3×3, stride 2, 128²→64² | 1.8 | 27.8 | 197 | | convT 128→128, 3×3, stride 2, 64²→128² (reference accumulates in `i128`) | 1.4 | 29.6 | 257 | ### Where the time goes (profile) `wai_decode_bench --layers` times each layer of the synthetic `g_s` on its real intermediate activations under each policy, so the time is attributed to layers exactly. The total is the sum of the layers' best times: | `g_s` layer, 256² decode | work | reference | fast 1T | fast auto | |---|---|---:|---:|---:| | convT 320→192, 16²→32² | 0.39 GMAC | 159 ms | 14.1 ms | 2.02 ms | | IGDN, 32² (max \|x\| 36562) | 0.04 GMAC | 29.1 | 4.1 | 0.66 | | convT 192→192, 32²→64² | 0.94 GMAC | 380 | 30.7 | 3.5 | | IGDN, 64² (max \|x\| 42256) | 0.15 GMAC | 118 | 16.2 | 1.8 | | convT 192→192, 64²→128² | 3.77 GMAC | 1504 | 119 | 12.7 | | IGDN, 128² (max \|x\| 57793: x² > 2³¹) | 0.60 GMAC | 475 | 92.0 | 9.5 | | convT 192→3, 128²→256² | 0.24 GMAC | 93 | 10.0 | 2.65 | | **total** | | **2759** | **287** | **32.8** | With the reference kernels, transposed convolution takes 77% of the decode and IGDN 23%. With the fast kernels the split is about 61/39 on one thread and 63/37 on all cores. The transposed convolution 192→192 at 64²→128² is still the largest layer, on one thread and on all cores. The last IGDN is second. On one thread it is the slowest layer per MAC, at about 6.6 GMAC/s against about 31.6 for that convolution. Its activations exceed the 32-bit-square range, so it takes the 64-bit path. For the JPEG-AI synthesis, sampling the all-core decode showed the channel attention, the GDFN element-wise gate, the channel LayerNorm and the element-wise gates of the synthesis running serially behind the parallel convolutions. Those were parallelised. Sampled symbol attribution in an LTO build is approximate, so the kernel table is the better guide. ## 5. Limits - **One shared machine, best of four runs.** The load average was 6.2–10.5 throughout, and a case's best time moved by up to 67% between runs (§3). The tables are the best observed, not the typical, and a quieter machine or another one will give other numbers. - **One machine, one architecture measured.** The NEON paths are aarch64-only. x86_64 runs the portable kernels: it compiles, but it was **not executed** here, because this machine cannot run x86_64 binaries. wasm runs the portable kernels single-threaded. It was tested for identical output, not timed. - **No trained int_hyper model was timed.** The `g_s` row uses synthetic weights, and the int_hyper tiny vector uses random ones. Synthetic weights reproduce shapes and operand widths, not trained activations, and activation size decides the IGDN path (§4). So a trained int_hyper model, which was not measured here, may run faster or slower. The JPEG-AI row also uses synthetic weights (see below). The trained models that were timed are the small committed vectors int_synth photo, int_world, int_splat and int_audio. - **JPEG-AI:** the synthesis nets only. The entropy stage and the float post-filter chain are unchanged and were not timed. The real exported JPEG-AI weights were not available in this environment, so the real-weight end-to-end tests skipped. The JPEG-AI kernels' equivalence rests on the randomised kernel and whole-TAB tests plus the synthetic-weight decode being identical under every policy on every run. - **Not proved.** The equivalence is argued by construction and tested (including by injected defects), not machine-proved. ## Reproduce ```sh cd wai/wai-rs F=neural_int,video_int,audio_int,world_int,splat_int cargo run --release --no-default-features --features $F --bin wai_decode_bench # all cases cargo run --release --no-default-features --features $F --bin wai_decode_bench -- --layers # per-layer profile cargo test --no-default-features --features $F --lib --test int_kernels_vectors --test int_kernels_geometry # equivalence, overflow checks on cargo test --release --no-default-features --features $F --lib --test int_kernels_vectors --test int_kernels_geometry # equivalence, release ``` The tables in §4 come from running the first two commands four times each and taking, per case and policy, the smallest of the four reported best times.