# Blake2b256 Streaming Linux Performance Plan

Started: 2026-06-30

Goal: turn `blake2b256 / streaming` on the Linux CI matrix from 8/16/3 to
27/0/0 against the fastest external implementation, without vendoring external
assembly and without changing standard Blake2b output semantics.

This is a performance ownership project. The first target is the three s390x
losses. The second target is converting the remaining ties into wins while
preserving the existing x86 wins.

## Scope

In scope:

- `Blake2b256::new()` + `update()` + `finalize()` on 1 MiB streaming benchmark
  input.
- Chunk sizes `64B`, `4096B`, and `65536B`.
- Linux CI runners:
  - `amd-zen4`
  - `amd-zen5`
  - `intel-icl`
  - `intel-spr`
  - `graviton3`
  - `graviton4`
  - `ibm-power10`
  - `ibm-s390x`
  - `rise-riscv`
- Rust-owned scalar, intrinsics, inline `asm!`, and rscrypto-owned assembly.

Out of scope:

- Vendoring BLAKE2 assembly from BLAKE2, libsodium, OpenSSL, AWS-LC,
  RustCrypto, or any other implementation.
- Changing `Blake2b256` digest semantics.
- Using BLAKE2bp/BLAKE2sp for this API. Those are parallel tree variants and
  do not produce standard sequential Blake2b digests.
- Adding dependencies for Blake2b.
- Masking losses by changing benchmark classification thresholds.

## Baseline

Source artifact: `benchmark_results/2026-06-30/linux/*/results.txt`

Commit:

```text
1bcc2d19cf7b2df93ad3e814a1763ab9ce7e84b4
```

Benchmark source:

```text
benches/blake2.rs
```

Benchmark shape:

```text
1 MiB random input
streamed in chunks of 64, 4096, and 65536 bytes
competitors: rscrypto, RustCrypto blake2, dryoc/libsodium
ratio: fastest external time / rscrypto time
win:  > 1.05x
tie:  0.95x..1.05x
loss: < 0.95x
```

Current public matrix:

| Platform | Chunk | rscrypto | Fastest external | Ratio | Class | Reduction to win |
| --- | ---: | ---: | ---: | ---: | --- | ---: |
| amd-zen4 | 64B | 1.2273 ms | rustcrypto 1.2636 ms | 1.030x | tie | 1.9% |
| amd-zen4 | 4096B | 1.1427 ms | rustcrypto 1.2014 ms | 1.051x | win | 0.0% |
| amd-zen4 | 65536B | 1.1414 ms | rustcrypto 1.2006 ms | 1.052x | win | 0.0% |
| amd-zen5 | 64B | 1.3683 ms | rustcrypto 1.3363 ms | 0.977x | tie | 7.0% |
| amd-zen5 | 4096B | 1.3258 ms | rustcrypto 1.3029 ms | 0.983x | tie | 6.4% |
| amd-zen5 | 65536B | 1.3248 ms | rustcrypto 1.3022 ms | 0.983x | tie | 6.4% |
| intel-icl | 64B | 1.0013 ms | rustcrypto 1.1546 ms | 1.153x | win | 0.0% |
| intel-icl | 4096B | 975.24 us | rustcrypto 1.0820 ms | 1.109x | win | 0.0% |
| intel-icl | 65536B | 973.81 us | rustcrypto 1.0814 ms | 1.110x | win | 0.0% |
| intel-spr | 64B | 967.91 us | rustcrypto 1.0246 ms | 1.059x | win | 0.0% |
| intel-spr | 4096B | 962.01 us | rustcrypto 1.0467 ms | 1.088x | win | 0.0% |
| intel-spr | 65536B | 970.12 us | rustcrypto 1.0396 ms | 1.072x | win | 0.0% |
| graviton3 | 64B | 1.3903 ms | rustcrypto 1.3950 ms | 1.003x | tie | 4.4% |
| graviton3 | 4096B | 1.3010 ms | rustcrypto 1.3178 ms | 1.013x | tie | 3.5% |
| graviton3 | 65536B | 1.2989 ms | rustcrypto 1.3155 ms | 1.013x | tie | 3.5% |
| graviton4 | 64B | 1.2650 ms | rustcrypto 1.2481 ms | 0.987x | tie | 6.0% |
| graviton4 | 4096B | 1.1695 ms | rustcrypto 1.1697 ms | 1.000x | tie | 4.7% |
| graviton4 | 65536B | 1.1647 ms | rustcrypto 1.1660 ms | 1.001x | tie | 4.7% |
| ibm-power10 | 64B | 1.5516 ms | rustcrypto 1.4944 ms | 0.963x | tie | 8.3% |
| ibm-power10 | 4096B | 1.4275 ms | rustcrypto 1.4161 ms | 0.992x | tie | 5.5% |
| ibm-power10 | 65536B | 1.4210 ms | rustcrypto 1.4147 ms | 0.996x | tie | 5.2% |
| ibm-s390x | 64B | 2.9357 ms | rustcrypto 2.0328 ms | 0.692x | loss | 34.1% |
| ibm-s390x | 4096B | 2.3788 ms | rustcrypto 1.8960 ms | 0.797x | loss | 24.1% |
| ibm-s390x | 65536B | 2.5897 ms | rustcrypto 1.8863 ms | 0.728x | loss | 30.6% |
| rise-riscv | 64B | 7.9284 ms | rustcrypto 7.7375 ms | 0.976x | tie | 7.1% |
| rise-riscv | 4096B | 6.7983 ms | rustcrypto 6.7354 ms | 0.991x | tie | 5.6% |
| rise-riscv | 65536B | 6.7401 ms | rustcrypto 6.6775 ms | 0.991x | tie | 5.6% |

## Evidence From Initial Investigation

RustCrypto is the fastest external implementation for every row in this
cluster. dryoc/libsodium is not the pressure point.

The s390x gap is streaming-specific:

- s390x one-shot large inputs are only moderately behind RustCrypto.
- s390x streaming rows are 20% to 45% behind.
- `Blake2s256 / streaming` on s390x does not show the same failure shape, so
  this is not a generic benchmark harness problem.

s390x assembly emitted locally with:

```bash
cargo rustc --lib --release --target s390x-unknown-linux-gnu \
  --no-default-features --features blake2b,diag -- --emit=asm
```

Findings:

- `Core::update` is emitted as a standalone function.
- The update hot path calls `self.compress` and `self.compress_blocks` through
  stored function pointers.
- `compress_blocks_portable` calls `compress@PLT` inside the per-block loop.
- The raw s390x compression body is close to RustCrypto's compression body in
  instruction mix, so compression rounds alone do not explain the streaming
  gap.

Current code locations:

- Streaming benchmark: `benches/blake2.rs`
- Blake2b state: `src/hashes/crypto/blake2b/mod.rs`
- Blake2b dispatch: `src/hashes/crypto/blake2b/dispatch.rs`
- Blake2b kernels: `src/hashes/crypto/blake2b/kernels.rs`
- s390x vector backend: `src/hashes/crypto/blake2b/s390x.rs`

## Research Inputs

Use these as constraints and comparison material, not as sources to copy from:

- RFC 7693: `https://datatracker.ietf.org/doc/html/rfc7693`
- BLAKE2 paper: `https://www.blake2.net/blake2.pdf`
- Official BLAKE2 implementations: `https://github.com/BLAKE2/BLAKE2`
- RustCrypto Blake2 implementation:
  `https://github.com/RustCrypto/hashes/tree/master/blake2`
- Local RustCrypto source used by benchmarks:
  `${CARGO_HOME:-~/.cargo}/registry/src/*/blake2-*`
- Existing rscrypto assembly policy and ledger: `docs/tasks/asm.md`

Required conclusions from the research pass:

- Standard sequential Blake2b has a chaining-value dependency between blocks.
  Consecutive blocks cannot be compressed in parallel without switching to a
  tree mode such as BLAKE2bp.
- BLAKE2b's public semantics are defined by the parameter block, counter, final
  block flag, and little-endian message-word interpretation. Optimizations must
  preserve those exactly.
- The useful competitor comparison is RustCrypto's generated scalar code shape,
  especially on s390x. Official and C-library implementations are useful for
  schedule ideas and validation, but their assembly must not be vendored.
- The best outcome is not "more SIMD"; it is the fastest measured backend per
  architecture with the smallest owned maintenance surface.

## Success Criteria

Primary performance target:

- `blake2b256 / streaming`: 27/0/0 on the Linux fastest-external matrix.
- All three s390x rows must move from loss to win.
- Existing x86 wins must stay wins.

Secondary performance target:

- `blake2b512 / streaming`, keyed Blake2b, and one-shot Blake2b must not lose
  more than 2% on any platform unless the public fastest-external matrix still
  improves and the regression is explicitly accepted.

Correctness target:

- Official Blake2b vectors pass.
- Differential tests against RustCrypto pass.
- Per-kernel forced tests pass for every selected kernel.
- Streaming split tests match one-shot for all chunk boundaries around 0, 1,
  63, 64, 65, 127, 128, 129, 4096, 65536, and 1 MiB.

Security and ownership target:

- No external-derived assembly added.
- Any new assembly is rscrypto-owned and recorded in the assembly ledger.
- Keyed Blake2b remains key-byte-independent in branch control and memory
  access.
- No secret-dependent table lookup, branch, or early return is introduced.
- No public API break.
- No new dependency.

## Shared Baseline Work

1. Add or extend a Blake2 gap-report parser -> verify: it reproduces
   `8/16/3`, the exact s390x losses, and the table above from
   `benchmark_results/2026-06-30/linux`.

2. Add diagnostic Blake2 streaming rows under `diag` -> verify: the bench can
   isolate constructor cost, update cost, block-loop cost, finalization cost,
   selected kernel, forced portable kernel, and forced architecture kernel.

3. Add forced s390x diagnostic entrypoints -> verify: s390x can benchmark
   portable versus z/Vector without changing production dispatch.

4. Add assembly inspection notes or script snippets -> verify: local
   `cargo rustc --target s390x-unknown-linux-gnu --emit=asm` can confirm
   whether the block loop still contains a call to `compress@PLT`.

5. Freeze the baseline artifact path in this document -> verify: every later
   retained/rejected result names the benchmark artifact, platform, commit, and
   command.

Do not start architecture tuning until this shared baseline is in place. Without
that split, it is too easy to attribute update overhead to compression kernels.

## Step 1: Devirtualize The Streaming Block Loop

Purpose: remove avoidable indirect calls and ABI boundaries from the hot block
processing path before touching architecture kernels.

### Implementation Work

1. Split portable compression into a public wrapper and an inline inner
   primitive -> verify: `compress()` remains available for tests and diagnostics
   while block loops can call the inner primitive directly.

2. Add a non-final portable block primitive -> verify: the regular block loop
   avoids the `last` branch and does not construct `u128` when only `(lo, hi)`
   counter words are needed.

3. Rewrite `compress_blocks_portable` as a direct block loop -> verify: emitted
   s390x assembly for `compress_blocks_portable` has no call to
   `blake2b::kernels::compress@PLT` in the per-block loop.

4. Rewrite target-specific `compress_blocks_*` functions so each one calls its
   raw target kernel directly instead of passing a function pointer through
   `compress_blocks_with` -> verify: x86_64 AVX2/AVX-512VL, RISC-V V, WASM
   SIMD128, AArch64 NEON, POWER VSX, and s390x z/Vector diagnostic block loops
   contain no per-block function-pointer call.

5. Add an update fast path for `buf_len == BLOCK_SIZE` when new data arrives
   -> verify: 64B streaming avoids zero-length fill-copy work before compressing
   the previously retained block.

6. Keep the lazy-final-block rule intact -> verify: an update ending exactly on
   a full block still retains that block until the next update or finalize, so
   the finalization flag remains correct.

7. Remove cold panic branches from the hot path where the block-size invariant
   already proves safety -> verify: debug assertions remain, release assembly
   does not contain new slice panic calls on the hot path.

8. Keep one-shot and streaming behavior unified only where it helps codegen
   -> verify: any helper shared by one-shot and streaming is measured neutral or
   faster on both surfaces.

### Local Verification

```bash
cargo fmt --all --check
cargo check --features 'blake2b,blake2s,diag' --bench blake2
cargo test --features 'blake2b,blake2s,diag' blake2
cargo test --test blake2_official_vectors --features 'blake2b,blake2s'
cargo test --test blake2_differential --features 'blake2b,blake2s'
cargo rustc --lib --release --target s390x-unknown-linux-gnu \
  --no-default-features --features blake2b,diag -- --emit=asm
```

Assembly checks:

- `compress_blocks_portable` has no hot call to `compress@PLT`.
- `Core::update` has at most one indirect call per large update.
- The 64B update path does not add extra copy or panic branches.

### Benchmark Verification

First CI run:

```text
PRIMITIVES/SELECTORS: blake2
RUNNERS: ibm-s390x
FILTERS: blake2/streaming/(rscrypto|rustcrypto|dryoc)/blake2b256
FEATURES: blake2b,blake2s,diag
```

Retain this step only if one of these is true:

- s390x streaming improves by at least 10% on at least two of three chunk
  sizes and no other Blake2b public row regresses by more than 2%.
- s390x reaches at least tie status on all three streaming rows.
- assembly proves the direct-loop cleanup worked and later steps need it as a
  prerequisite.

Reject or revise this step if:

- s390x does not move and assembly still shows a call boundary inside the block
  loop.
- x86 wins regress below 1.05x.
- correctness tests require special-case behavior for final blocks.

## Step 2: Split Fixed Unkeyed Streaming State

Purpose: stop forcing `Blake2b256::new()` through the full keyed/parameterized
state shape when the benchmark uses fixed-output unkeyed hashing.

### Design Constraints

`Blake2b256` currently supports both unkeyed streaming and keyed construction
through the same public type. Do not break that API.

The implementation must choose one of these shapes after measurement:

| Shape | Benefit | Cost | Accept If |
| --- | --- | --- | --- |
| `enum Blake2b256Inner { Unkeyed(FixedCore<32>), Keyed(Core) }` | true lean unkeyed state | branch in `update` and `finalize` | branch cost is below saved state cost |
| Keep one `Core`, but split fixed unkeyed fields internally | smaller API churn | less reduction in state size | enough to close ties |
| Add a new private fixed-core and route only `Digest::new` through it | minimal public effect | keyed path still carries old core | unkeyed streaming wins without API change |

Do not remove unkeyed zeroization silently. Benchmark a no-wipe variant only as
a diagnostic. Retain it only if the project explicitly accepts that unkeyed hash
state is not key material and the security documentation is updated. The default
plan is to preserve wiping or replace it with an equivalent lower-overhead wipe.

### Implementation Work

1. Add a private fixed-output unkeyed core for Blake2b-256 and Blake2b-512 ->
   verify: it stores only chaining state, buffer, buffer length, counter, and
   the minimum dispatch data required for block compression.

2. Remove key, salt, personalization, output-length, and variable-output
   branches from the unkeyed fixed path -> verify: `core::mem::size_of` for the
   unkeyed fixed state is materially smaller than the current `Core`.

3. Add fixed `write_output_32` and `write_output_64` helpers -> verify:
   finalization does not loop over dynamic `nn` for fixed-output hashers.

4. Add fixed `reset` paths using `BLAKE2B256_H0` and `BLAKE2B512_H0` -> verify:
   reset does not rebuild parameter blocks for unkeyed fixed output.

5. Keep keyed construction on the existing parameterized `Core` until measured
   otherwise -> verify: `Blake2b256::new_keyed`, `keyed_digest`, and
   `Blake2bParams` tests are unchanged.

6. Add targeted host-overhead diagnostics for constructor, update-only,
   finalize-only, and drop-only cost -> verify: 64B streaming improvement can be
   attributed to state shape rather than noise.

7. Audit drop behavior -> verify: unkeyed, keyed, and parameterized paths have
   explicit wipe policy and no accidental key retention.

### Local Verification

```bash
cargo fmt --all --check
cargo check --features 'blake2b,blake2s,diag' --bench blake2
cargo test --features 'blake2b,blake2s,diag' blake2b
cargo test --test blake2_official_vectors --features 'blake2b,blake2s'
cargo test --test blake2_differential --features 'blake2b,blake2s'
cargo test --test portable_fallback --features 'blake2b,blake2s'
```

### Benchmark Verification

Targeted CI order:

```text
1. ibm-s390x: blake2/streaming/blake2b256
2. amd-zen5: blake2/streaming/blake2b256
3. graviton4: blake2/streaming/blake2b256
4. ibm-power10: blake2/streaming/blake2b256
5. rise-riscv: blake2/streaming/blake2b256
```

Retain this step if:

- s390x 64B improves by at least 15% from the original baseline or becomes a
  tie after Step 1.
- at least half of non-s390x ties improve.
- keyed and parameterized Blake2b do not regress beyond the 2% secondary
  target.

Reject or revise this step if:

- enum branching erases the fixed-core benefit.
- public API behavior changes.
- the implementation becomes more complex than the measured gain justifies.

## Step 3: Fix s390x After Rust-Level Cleanup

Purpose: close the actual losses after the generic streaming overhead has been
removed. Do not write architecture-specific code until Steps 1 and 2 identify
the remaining s390x gap.

### Measurement Work

1. Add forced s390x z/Vector compression diagnostics -> verify:
   `portable`, `s390x/vector`, and selected production rows are visible in
   `bench=blake2` output.

2. Measure raw compression throughput for portable versus z/Vector -> verify:
   rows exist for 1 block, 8 blocks, 32 blocks, 512 blocks, and 8192 blocks.

3. Measure streaming with forced portable and forced z/Vector -> verify:
   `64B`, `4096B`, and `65536B` show whether update overhead or compression is
   still dominant.

4. Inspect s390x assembly for both candidates -> verify:
   message loads, byte reversal, rotates, spills, and vector-register use are
   documented before writing new code.

### Implementation Options

Try in this order:

1. Repair the existing z/Vector Rust/inline-asm backend -> verify: direct
   vector loads, `verllg` rotates, and lane permutations beat portable on raw
   compression.

2. Replace scalar message loading in the s390x vector backend -> verify:
   little-endian block words load through vector byte-reverse operations instead
   of sixteen scalar reversed loads plus pair construction.

3. Write a fully owned s390x single-block assembly kernel -> verify:
   provenance header, ABI contract, capability gate, and per-kernel tests are
   present before production dispatch can select it.

4. Add a selected s390x block-loop wrapper -> verify:
   production dispatch selects s390x only when the measured kernel beats
   portable on all three streaming chunk sizes or on the full public matrix.

Do not attempt parallel compression of consecutive standard Blake2b blocks.
The chaining value dependency makes that a different algorithm unless the API
explicitly switches to BLAKE2bp/BLAKE2sp.

### Safety Requirements For New s390x Assembly

Any new `.s` or inline `asm!` must include:

- SPDX license header.
- `rscrypto-owned` provenance note.
- ABI documentation in the Rust wrapper.
- CPU feature requirement and runtime dispatch guard.
- `// SAFETY:` comments for each call boundary.
- Constant-time statement covering keyed Blake2b.
- Differential tests against portable and RustCrypto.
- Entry in `docs/tasks/asm.md`.

### Local Verification

```bash
cargo fmt --all --check
cargo check --features 'blake2b,blake2s,diag' --bench blake2
cargo test --features 'blake2b,blake2s,diag' blake2b_forced_kernels_match_oracle
cargo test --test blake2_official_vectors --features 'blake2b,blake2s'
cargo test --test blake2_differential --features 'blake2b,blake2s'
scripts/check/asm-ledger.sh
```

### Benchmark Verification

Required targeted run:

```text
PRIMITIVES/SELECTORS: blake2
RUNNERS: ibm-s390x
FILTERS: blake2/streaming/blake2b256
FEATURES: blake2b,blake2s,diag
```

Retain s390x production dispatch only if:

- all three s390x streaming rows are wins, or
- s390x geomean improves by at least 25% and no s390x Blake2b one-shot/keyed
  row regresses beyond 2%, with a follow-up task left for the remaining row.

Reject s390x production dispatch if:

- the vector or assembly kernel wins only raw compression but loses public
  streaming rows.
- 64B improves while 4096B or 65536B regresses below the current baseline.
- correctness requires special casing that is not in RFC 7693 sequential mode.

## Step 4: Tune Remaining Architecture Ties

Purpose: convert the 16 ties into wins after the generic and s390x work lands.
This step must be data-driven by architecture. Do not tune a platform that is
already safely above 1.05x unless a shared change regresses it.

### x86_64

Targets:

- `amd-zen4 / 64B`: needs about 1.9%.
- `amd-zen5 / 64B`: needs about 7.0%.
- `amd-zen5 / 4096B`: needs about 6.4%.
- `amd-zen5 / 65536B`: needs about 6.4%.

Work:

1. Add forced AVX2 and AVX-512VL streaming diagnostic rows -> verify:
   Zen4, Zen5, Ice Lake, and Sapphire Rapids show selected versus forced
   backend timings for all three chunk sizes.

2. Check Zen5 AVX-512VL policy against AVX2 -> verify: if AVX-512VL downclocks
   or schedules worse on Zen5, dispatch can prefer AVX2 for Blake2b streaming
   on that CPU class.

3. Inline target-feature block loops -> verify: forced AVX2/AVX-512VL rows do
   not call a scalar wrapper per block.

4. Retune x86 only if ICL and SPR stay wins -> verify: no x86 platform drops
   below 1.05x.

### AArch64

Targets:

- `graviton3`: 3.5% to 4.4%.
- `graviton4`: 4.7% to 6.0%.

Work:

1. Re-test portable versus NEON after Steps 1 and 2 -> verify: the old result
   that single-block NEON loses is still true before keeping it disabled.

2. Try scalar schedule cleanup before new NEON -> verify: portable codegen
   improvements move both Graviton generations without selecting a slower NEON
   path.

3. If NEON is still worth testing, write an owned direct-load NEON candidate
   without external source lifting -> verify: forced NEON beats portable on
   both Graviton3 and Graviton4 public streaming rows.

### POWER10

Targets:

- `64B`: needs about 8.3%.
- `4096B`: needs about 5.5%.
- `65536B`: needs about 5.2%.

Work:

1. Re-test portable versus VSX after Steps 1 and 2 -> verify: forced VSX either
   beats portable or remains rejected with current evidence.

2. Inspect POWER portable codegen -> verify: rotations, byte loads, and spills
   are documented before writing VSX-specific code.

3. Try a direct VSX load/rotate schedule only if forced rows justify it ->
   verify: all three POWER10 streaming rows become wins.

### RISC-V

Targets:

- `64B`: needs about 7.1%.
- `4096B`: needs about 5.6%.
- `65536B`: needs about 5.6%.

Work:

1. Confirm whether production selected RVV or portable -> verify:
   diag output records kernel name on `rise-riscv`.

2. Compare forced RVV versus forced portable -> verify: the selected backend is
   actually the fastest backend for this runner.

3. If RVV is selected but only ties RustCrypto, inspect vector-length handling
   and scalar fallback overhead -> verify: no per-block dynamic vector setup
   dominates the loop.

4. If portable wins, disable RVV for Blake2b streaming on this profile -> verify:
   one-shot and keyed rows do not regress.

### Full Matrix Verification

Run order:

```text
1. Targeted s390x run
2. Targeted non-x86 tie run: graviton3,graviton4,ibm-power10,rise-riscv
3. Targeted x86 tie run: amd-zen4,amd-zen5
4. Full nine-runner Linux Blake2 run
5. Full public benchmark extraction and overview refresh
```

Retain Step 4 changes only if:

- every changed architecture improves or stays flat on all three public
  streaming rows.
- no existing win drops below 1.05x.
- the full matrix is at least 24/3/0 after the first pass and has a clear path
  to 27/0/0.

Reject architecture-specific tuning if:

- the win is below 2% and adds target-specific complexity.
- the change helps one chunk size but hurts another enough to remain a tie.
- the change requires external-derived assembly.

## Correctness And Regression Suite

Minimum local suite before any targeted CI run:

```bash
cargo fmt --all --check
cargo check --features 'blake2b,blake2s,diag' --bench blake2
cargo test --features 'blake2b,blake2s,diag' blake2
cargo test --test blake2_official_vectors --features 'blake2b,blake2s'
cargo test --test blake2_differential --features 'blake2b,blake2s'
cargo test --test portable_fallback --features 'blake2b,blake2s'
git diff --check
```

Required before closing the task:

```bash
just check
just test --all
just test-miri
```

Run fuzzing before release if any block-boundary code changes:

```bash
just test-fuzz
```

## Benchmark Reporting

Every retained or rejected candidate must record:

- commit hash;
- artifact path or GitHub Actions run ID;
- runner names;
- exact filters;
- feature flags;
- public row deltas;
- relevant diagnostic row deltas;
- reason retained or rejected.

Use this format:

```text
Run:
Commit:
Runners:
Filters:
Candidate:
Result:
Decision:
Follow-up:
```

Do not summarize a candidate as "faster" without row-level numbers.

## Closeout Checklist

The task is complete only when all items are true:

- `blake2b256 / streaming` has no Linux losses.
- s390x has no remaining Blake2b256 streaming loss.
- existing x86 wins are preserved.
- all new architecture-specific code is owned by rscrypto.
- rejected candidates are removed.
- diagnostic-only code is behind `diag`.
- assembly ledger is updated if any assembly is added, removed, or reclassified.
- benchmark overview is refreshed from the final full Linux run.
- this file has a closeout section with final W/T/L, geomean, median, artifact
  path, commit, and retained/rejected changes.
