Skip to content

perf(j2k-native): packed-column Tier-1 passes for BYPASS code-blocks #311

Closed

Description

@jcoyne

Now that Persimmon is on 0.13.1, I compared it with Kakadu 8.6.2 again (Persimmon's adapter built natively for macOS arm64, every 1024 px tile at every scale, JPEG output). On the 18000×12000 images j2k is now within 3–12% of Kakadu, at about the same memory, which is great. The remaining gap is on the 6000×4000 archival image. That's the kind of untiled, Kakadu-encoded JP2 our repository holds, so it's what Persimmon mostly serves. There Kakadu was 1.5–1.6 times faster.

Why. That image is classic, with 7 layers, 64×64 code-blocks, RPCL and Cmodes=BYPASS. On a serial 1024² region, Tier-1 is about 84% of the decode (66–68 ms of about 79 ms). A sample profile shows the whole of Tier-1 on the per-coefficient passes, arithmetic.rs and bypass.rs, arithmetic passes included. reset_for_job only uses the packed columns when every segment is arithmetic and the style is single_codeword_segment(), because packed and per-coefficient state can't be mixed within a block. So any selective bypass, or termination on each pass, keeps a block off the passes #303 sped up.

Prototype. I tried giving those blocks packed passes, on a branch off main:

  • Eligibility. packed_columns = !vertically_causal_context. Bypass, termination on each pass, context resets and segmentation symbols all take the packed columns.
  • Raw passes. flag_passes.rs gains significance_propagation_pass_raw, magnitude_refinement_pass_raw and cleanup_pass_raw. They make the decisions of the packed arithmetic passes in the same order, reading raw bits: no contexts, and the raw sign bit is the sign, with no XOR. The arithmetic passes are untouched.
  • Raw reader. A RawBits reader buffers up to 64 de-stuffed bits. It skips the top bit of the byte after a 0xFF and returns ones once the segment is exhausted, as BypassDecoder does. Before a 0xFF whose stuffing it can't vouch for (the stuffed bit is one, or the data ends), it stops buffering and hands the rest of the segment to a BypassDecoder. So strict and lenient decodes of malformed data come out exactly as now.
  • Dispatch. Both segment loops in schedule.rs run raw segments on the packed raw passes when the block uses packed columns, and reset_for_next_bitplane skips its per-coefficient bypass bookkeeping for packed blocks.
  • Tests. A test-only switch, FORCE_PER_COEFFICIENT, keeps the per-coefficient passes testable, and the four state tests that check their internals set it. A new parity test encodes random code-blocks in all 16 non-causal style combinations, 8 sizes, 4 sub-band types and 1 to 16 bitplanes, about a quarter of them with raw segments. It decodes each with both sets of passes, strict and lenient, and also 24 corrupt copies of each (bit flips, bytes set to 0xFF, truncation). Samples and errors match exactly. With a deliberately flipped raw sign bit, it fails at once.

Results. Serial component-plane region decodes, against main, medians of 11 runs. Output hashes and peak heap are identical.

Region main Prototype
archival 1024², middle 87.2 ms 74.6 ms (−14%)
archival 1024², top-left 53.7 ms 46.8 ms (−13%)
archival 1024² at ½ 137.1 ms 111.6 ms (−19%)
archival 512² 27.4 ms 22.9 ms (−16%)
18000×12000 PLT 1024² (no bypass) 118.0 ms 119.7 ms
18000×12000 no PLT 1024² (no bypass) 141.4 ms 140.4 ms

In Persimmon's tile benchmark on the archival image, with Kakadu, 0.13.1 and the prototype run alternately:

Kakadu 8.6.2 j2k 0.13.1 Prototype
Concurrency 1, mean tile time 52.7 ms 80.0 ms 68.9 ms
Concurrency 8, tiles/s 124.5 77.3 94.2

That takes the gap from about 1.5× to about 1.3×. The gain is larger than the raw passes alone would explain, because moving the block onto packed columns speeds up its arithmetic passes too. After the change, the profile is about a third cleanup, a fifth arithmetic significance propagation and a sixth raw significance propagation, with the IDWT and output about 15%. The raw significance pass now costs about the same per decision as the arithmetic one: the cost is the column scanning and significance updates, not the bit reads.

Questions.

  1. This is right next to the code you just changed in perf(j2k-native): faster CPU classic Tier-1 decoding #303. Would you like a PR from me, or would you rather take it from here? I'm happy either way, and can push the branch for you to look at.
  2. Generalizing the arithmetic Scan over a symbol source might avoid the duplicated raw passes, but I kept your arithmetic passes byte for byte so as not to disturb what perf(j2k-native): faster CPU classic Tier-1 decoding #303 tuned. Do you prefer one or the other?
  3. Termination on each pass, context resets and segmentation symbols now take the packed columns too. The parity test covers all those combinations, but tell me if there's a reason they were kept off.
  4. all_segments_arithmetic on reset_for_job is now unused. I'd remove it in a PR.

Activity

  1. jcwal1516 commented on Oct 30, 2026

    @jcwal1516
    Member

    Awesome, thanks for finding this!

    Since you offered either way, would you mind pushing your prototype and letting me take it from there? I have some CPU decode work that hasn't been pushed yet, including changes to flag_passes.rs and the x86 dispatch in schedule.rs. From what you've described, that work sounds complementary to yours. It still keeps BYPASS blocks on the per-coefficient path, so this would address a gap it doesn't cover.

    Some numbers from the combined changes so far: on the M5 Ultra at six threads, a middle 1024² region of the 18000×12000 PLT image went from about 22 to 14.5 ms, and the HT image from 17.6 to 6.8 ms. Those are RGB8 region decodes, medians of five interleaved rounds against your #310 branch, with identical pixels. So there are some useful gains on top of your work already. I'd be interested to see what your BYPASS changes add to the combination, especially on the archival image.

    With the overlap in those files, it probably makes sense for me to handle combining them. I'll preserve your commits where possible and credit your work and tests in the PR, then run the combination on ARM and x86 and share the results here.

    Thanks again for finding this and offering to share the code!

  2. jcoyne commented on Oct 30, 2026

    @jcoyne
    ContributorAuthor

    Sounds good, and those numbers are impressive. Please take it from here.

    It's pushed as one commit on jcoyne:perf/bypass-passes, 4ecd3c42, on top of 629dc030. Beyond what I described above, it also drops the now-unused all_segments_arithmetic parameter from reset_for_job, and it passes clippy, clippy-strict and the j2k-native tests. Two things I didn't get to:

    • a whole-image differential fuzz on BYPASS images. Our encoder can't write them, so the existing fuzz never reaches this path. The code-block parity test in tests/classic_passes.rs is the coverage so far.
    • a CHANGELOG entry.

    Some context on how much this matters for us. Our repository's encoding settings changed recently: new images are encoded without Cmodes=BYPASS (for Photoshop compatibility) and with -rate -. So BYPASS mostly matters for images already in the collection, which is most of what we serve today. On a real image with the new settings (5661×8287, 8 layers, 3.46 bpp, gg891fv3841_0002), j2k 0.13.1 is already level with Kakadu in Persimmon's tile benchmark: 124.6 against 124.5 ms for full-resolution tiles, 144.9 against 152.2 ms across all scales, and 38.0 against 36.5 tiles/s at concurrency 8, with about the same peak footprint. That image would make a good benchmark for this kind of content.

  3. jcwal1516 commented on Nov 2, 2026

    @jcwal1516
    Member

    Thanks again for sharing the prototype and parity tests. I’ve integrated them into #314, now merged into main, with your co-author credit preserved.

    In the isolated ARM64 comparison, the BYPASS re-encode of the 5661×8287 RGB image took 22% less time for a one-thread full decode. The 1024×1024 region tests took 19% less time at one thread and 12% less at six threads, with identical output. ARM64 and x86-64 checks, plus CUDA and Metal validation, passed.

    We also ran the archival workload suite on both the Mac mini and Studio with your BYPASS implementation included, covering full images, reduced decodes and IIIF tile/region requests. The pixel comparisons were clean. We used the gg891 file you suggested and several other public JP2s from the same site:

    The original gg891 is non-BYPASS; its BYPASS control was generated locally. Those broader runs also included additional streaming improvements, so I’m keeping their combined results separate from the isolated BYPASS comparison. The follow-up series is now open in #316, #317 and #318.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions