Skip to content

Metal kernels: 32-bit checked arithmetic, one thread per HP block, single-pass color output - #8

Merged
jcwal1516 merged 1 commit into
mainfrom
perf/metal-kernels
Sep 27, 2026
Merged

jcwal1516 merged 1 commit into
mainfrom
perf/metal-kernels

Conversation

@jcwal1516

Copy link
Copy Markdown
Member

Checked arithmetic. Every reconstruction kernel widened each add, subtract, and multiply to 64 bits, which Apple GPUs emulate, and returned through a branch after each operation. Arithmetic now uses exact 32-bit overflow predicates: sign-bit tests for add/sub, mulhi for quantizer products, and range tests for the ×3 steps. Each kernel accumulates them into a sticky flag and tests it once before storing. The transforms become straight-line code. Per-phase status codes (1, 2, 3, 4, 16) are unchanged, and a result derived from an overflowed intermediate is never stored.

HP transform. The kernel ran one threadgroup per macroblock, with thread 0 applying HP prediction serially between two barriers; that left half of each 32-wide SIMD group idle on 16-block luma macroblocks. It now runs one thread per 4×4 block in a flat grid, with no threadgroup memory or barriers. Each thread accumulates its own prediction chain in normative order, so every partial-sum overflow check matches the serial traversal. Block rows are stored as aligned int4 writes, and JxrPlaneAbi::from_plan now rejects sample planes that aren't four-sample aligned (they already are, since planes are 64-element aligned).

Output stores. Color conversion, including 4:2:0/4:2:2 chroma upsampling, ran once per output channel; each pixel is now loaded and converted once. Premultiplied stores scale alpha once per pixel. Chroma upsampling computes the weighted average exactly in 32 bits by splitting each operand into 8q + r; the weights always sum to eight, so the result always fits. Unsigned premultiplication uses 32-bit division, since 65535² + 32767 fits in u32. F32 output and signed premultiply still use 64-bit math; they're rare formats, and this keeps them exact.

One behavioral nuance: when two different overflows occur in the same macroblock, the HP kernel may report status 4 where it previously reported 3. The previous kernel could already do this across macroblocks, because first-failure-wins is racy between threadgroups.

Performance (M4 Pro, interleaved with main in the same session, better of two rounds). jxr-pathology-bench, 256×256 Boat tiles; "Metal-side" is submit + wait with CPU preparation excluded. Pipelined throughput also includes CPU entropy decoding, which this PR does not change:

Batch Metric main branch change
8 pipelined throughput 160.8 MP/s 204.5 MP/s +27%
8 resident, Metal-side 1.9 ms 1.5 ms -20%
8 dense, Metal-side 2.9 ms 2.6 ms -10%
8 host, Metal-side 1.8 ms 1.6 ms -12%
32 pipelined throughput 225.7 MP/s 233.3 MP/s +3%
32 resident, Metal-side 4.5 ms 3.7 ms -19%
32 dense, Metal-side 8.7 ms 6.3 ms -28%
32 host, Metal-side 4.4 ms 3.5 ms -20%
128 pipelined throughput 241.6 MP/s 266.3 MP/s +10%
128 resident, Metal-side 15.7 ms 14.2 ms -10%
128 dense, Metal-side 31.5 ms 16.2 ms -49%
128 host, Metal-side 17.0 ms 15.2 ms -10%

Single-image Metal decode (jxr-load-bench, lower of two rounds' medians): Seattle 23.4 → 21.2 ms (−10%), P19d 31.3 → 29.1 ms (−7%), Maui 128bpp 39.8 → 37.1 ms (−7%), Maui RGBE 36.9 → 35.2 ms (−5%). CPU decodes are unchanged. VeryWideLevel255 Metal timings were bimodal (19–30 ms within one run) and are not quoted.

Validation on an M4 Pro: cargo fmt --all -- --check; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo test --workspace --all-features (255 passed, covering every store entry point and the post-submission arithmetic-failure path); cargo test -p jxr-mpsgraph --test metal -- --ignored --test-threads=1 (7 passed); T.834/T.835 conformance Metal 517/517 byte-exact and CPU 517/517; jxr-pathology-bench checksum validation of resident and dense batches. The owner-only metal-hardware workflow has not been run on this revision.

Every reconstruction kernel widened each add, subtract, and multiply to
64-bit, which Apple GPUs emulate, and returned through a branch after
each operation. Arithmetic now uses exact 32-bit overflow predicates
(sign-bit tests for add/sub, mulhi for quantizer products, range tests
for the times-three steps) accumulated into a sticky flag that each
kernel tests once before storing. The transforms become straight-line
code; per-phase status codes are unchanged, and results derived from an
overflowed intermediate are never stored.

The HP kernel ran one threadgroup per macroblock with thread 0 applying
HP prediction serially between two barriers, leaving half of each SIMD
group idle for 16-block luma macroblocks. It now runs one thread per
4x4 block in a flat grid; each thread accumulates its own prediction
chain in the normative order, so every partial-sum overflow check
matches the serial traversal. Block rows are stored as aligned int4
writes, and plane ABI construction now rejects sample planes that are
not four-sample aligned.

The output kernels converted color, including chroma upsampling, once
per output channel. Each pixel is now loaded and converted once, and
premultiplied stores scale alpha once per pixel. Chroma upsampling
computes the weighted average exactly in 32 bits by splitting each
operand into 8q + r, and unsigned premultiplication uses 32-bit
division (65535^2 + 32767 fits in u32).
@jcwal1516
jcwal1516 merged commit f9ea7f0 into main Sep 27, 2026
2 checks passed
@jcwal1516
jcwal1516 deleted the perf/metal-kernels branch September 28, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant