reranking
— GELO

Two architecture-typed rerank services share one trait, both ride the same GELO mask + TwinShield primitives the embedder validated. Scores never leave the TEE — the only wire emission is a fixed-shape, shuffled, AES-GCM-encrypted bundle whose rank is sealed inside each ciphertext.

protocol  GELO + TwinShield + HKDF re-encrypt
TEE  AMD SEV-SNP CVM
GPU  commodity Vulkan via VFIO
models  bge-reranker-v2-m3 · Qwen3-Reranker-0.6B
scores stay sealed inside the boundary
bge rerank · k′=20
3.02s batch
bge-reranker-v2-m3 · rayon-parallel candidates · 1.19 s single-pair
Qwen3 rerank · k′=20
6.48s batch
Qwen3-Reranker-0.6B · rayon-parallel candidates · 1.83 s single-pair
bundle k_max
k_max items
always padded · shuffled · rank sealed inside each ciphertext
HKDF labels
v1
gelo-rerank.session.v1 · gelo-rerank.query.v1
§01

Role in the project

The reranker is the post-retrieval refinement step in private RAG. After CAPRISE-decrypted top-k_prime candidates land back inside the CVM (see Embedding + Storage), the reranker re-scores each (query, chunk) pair with a more precise relevance model and selects the final k_final. Two facts make this step its own privacy primitive rather than a thin extension of retrieval.

The score function changes, but more importantly the score itself becomes a new exit channel. A bi-encoder cosine score on the storage server is already encrypted by CAPRISE — the server learns rank order, not absolute scores. A cross-encoder or causal-LM discriminator score is a plaintext scalar that lives wherever the reranker runs. If exported in plaintext, accumulating scores across queries inverts back to query content. The mitigation here — score never exits the TEE, only the encrypted ordered set — is structural rather than statistical: there's nothing to bound because there's nothing to leak.

The model is openweight and runs the same GELO mask + TwinShield primitives the embedder validated. No new cryptography, no new protocol layer; the rerank service is an architecture-typed wrapper over the existing gelo-embedder BERT and decoder forward paths, plus a small head module and a per-query HKDF re-encryption pass.

The design pulls the reranker fully inside the trust boundary — ingest, retrieve, rerank, re-encrypt all happen in the CVM. The only wire emission per query is k_max AES-GCM ciphertexts of constant per-item size. A host observer can correlate count, timing, and ciphertext shape but learns nothing about scores, score distribution, rank order, or chunk identity.

§02

Definitions & glossary

Protocol primitives (mask, shield, U-Verify, OutAttnMult) are defined in embedding §02. Only rerank-specific additions are listed here.

Term Definition
Cross-encoder Reranker family that concatenates (query, candidate) as one sequence and runs a single forward pass to produce a relevance score. Higher accuracy than dual-encoder cosine; higher cost. Today's example: BAAI/bge-reranker-v2-m3 (XLM-RoBERTa-large, 24 layers).
Causal-LM discriminator Reranker family that uses a causal decoder to score whether the candidate answers the query. Example: Qwen3-Reranker-0.6B — Qwen3 backbone, 28 layers, sigmoid-of-yes-token at the last position.
k_max Fixed bundle size emitted on every rerank request. The TEE returns exactly k_max shuffled, AES-GCM-sealed score items regardless of true top_k — the wire pattern reveals nothing about how many candidates actually scored above threshold.
Bundle ciphertext Per-candidate AES-GCM seal of (rank, score, candidate_id) under QueryKey = HKDF(SessionKey, "rerank.score.v1", query_id). Rank is sealed inside each ciphertext — order on the wire is shuffled, so an observer cannot read rank from position.
QueryKey Per-query symmetric key derived in-TEE from the session secret. Disposed at request end so a future TEE break cannot retroactively open old bundles.
Stage-1 / Stage-2 Stage-1 = the embedder's ANN candidate set (top k′ by ciphertext cosine). Stage-2 = the reranker re-scores those candidates with a heavier model.
§03

Threat model

Same trust posture as the embedder, extended to cover the rerank-specific exit channel.

Component Trust What it sees What it does NOT see
User-side text confidential
TEE (SEV-SNP CVM) trusted query · candidate chunk plaintext · scores · rank order · mask state · model weights
GPU + driver + PCIe untrusted public model weights · per-batch masked activations U=A·H · integrity-probed matmul results clean activations · mask A · scores · query · doc text
TEE host (CVM operator) untrusted encrypted CVM memory · masked PCIe traffic · per-query AES-GCM ciphertexts · fixed k_max count · per-item size scores · rank order · chunk identity · query content · model output before encryption
Network operator untrusted TLS-wrapped requests · attestation evidence RATLS contents
External generator (if used) untrusted the prompt content the client sends after decrypting the bundle rerank protocol state

Four exit-channel leaks are closed structurally rather than statistically:

Leak How it's closed
Score-export leakage Scores never exit the CVM. The rerank output that crosses the trust boundary is the encrypted ordered set, not the scores.
Rank-order via emission order The integer rank is encoded inside the AES-GCM payload and the wire list is shuffled. A host observer cannot read the ranking out of list position.
Cardinality (k_final) leak Bundles always emit exactly k_max items, with k_max - k_final decoys. Decoys are AES-GCM-padded to the longest real candidate's length so per-item size doesn't fingerprint them.
Ciphertext-to-storage linkability Top-k_final chunks are re-encrypted under a per-query QueryKey = HKDF(SessionKey, "rerank-output", query_id). The output ciphertexts share no bytes with the storage-time AES-GCM ciphertexts the host may have observed at ingest.

Not covered. Prompt content the client forwards to an external generator. Query frequency and timing. Side channels in the TEE itself. Replay defense at the protocol layer (re-using (SessionKey, QueryId) re-derives the same QueryKey and breaks AES-GCM — caller must guarantee unique QueryId per session).

§04

Supported model architectures

Names track architecture, not model family. Each loads as Arc<…Weights> from safetensors via the HuggingFace Hub; the SHA-256 of backbone bytes + head bytes + (for the discriminator) template + pinned token IDs rides as model_identity through every attestation report.

Family Crate path Reference model Layers · Hidden · Inter Distinguishing ops Score function
Cross-encoder gelo_reranker::cross_encoder BAAI/bge-reranker-v2-m3 24 · 1024 · 4096 post-LN BERT · GELU FFN · full bidirectional attention out_proj(tanh(dense(cls_row))) · 2-layer classifier
Causal-LM discriminator gelo_reranker::causal_discriminator Qwen/Qwen3-Reranker-0.6B 28 · 1024 · 3072 pre-LN RMSNorm · SwiGLU FFN · GQA(16/8) · RoPE · causal mask softmax([no_logit, yes_logit])[1] · tied LM head · two dot products

Why Qwen3-Reranker is the primary

The backbone is byte-identical to Qwen3-Embedding-0.6B already in gelo_embedder::decoder. Every GELO primitive (mask, shield rows, U-Verify, OutAttnMult, permuted attention, length auto-switch, sensitive-layer exclusion) applies without modification. Adding it took a head loader + a chat template + a yes/no logit gather. ~98% code reuse with the embedder.

Why bge-reranker-v2-m3 is the fallback

Validates the protocol on a structurally different architecture (XLM-RoBERTa post-LN BERT vs Qwen3 pre-LN decoder), exercises the BERT path the embedder uses for BGE-base + BGE-small, and acts as a parity bench against Qwen3-Reranker. Apache-2.0; well-trodden in the IR literature.

jina-reranker-v3 is deferred. Its listwise packed-context architecture pushes n ≈ 16k+ per forward, which sits in the fused-permuted-attention regime documented in gelo-llm.md §3 — a 5–7 week prerequisite. CC-BY-NC-4.0 license is also a blocker for commercial deployments. Revisit alongside the LLM-serving stack.

§05

Components

Each card pairs [component ↦ source ↦ what it secures ↦ threat]. Ordered by request path: trait → heads → session keys → bundle → in-TEE sort → HTTP route.

trait

RerankServicegelo_reranker :: service

Common surface for both architectures. Three methods: model_identity() (SHA-256 over backbone+head+template), family() (cross-encoder / causal-discriminator), rerank() (the only entry point that crosses the trust boundary).

impls
CrossEncoderRerankService<X> · CausalDiscriminatorRerankService<X>
generic
X: TrustedExecutor — typically InProcessTrustedExecutor<WgpuVulkanEngine>
identity
folded into REPORT_DATA[0..32] — relying party pins backbone + head + template together
head

ClassifierHeadgelo_reranker :: head

2-layer XLMRobertaForSequenceClassification head: out_proj(tanh(dense(cls))). Loads classifier.dense.{weight,bias} + classifier.out_proj.{weight,bias} from safetensors, hashes the four tensor blobs into the head identity.

shape
dense (hidden, hidden) · out_proj (hidden, 1)
used by
CrossEncoderRerankService — applied to CLS row of the joint [CLS] q [SEP] d [SEP] forward
head

YesNoHeadgelo_reranker :: head

Pinned vocab IDs for the causal-LM discriminator. YesNoHead { yes_token_id, no_token_id } resolved from the tokenizer at load and folded into model_identity — a tokenizer-config drift that re-numbers yes/no trips the attestation.

storage
two u32s + the tied token_embedding from DecoderWeights doubles as the output projection (Qwen3-Reranker sets tie_word_embeddings = true)
cost
two dot products against token_embedding.row(yes_id) and .row(no_id) per (q, d) pair · ~µs
crypto

SessionKey / QueryKeygelo_reranker :: session

HKDF-SHA256 hierarchy mirroring rag_core::keying::HkdfPolicy::V1. Session root derived from a per-session shared secret; per-query AES-256-GCM key derived from session root + query_id. Both Zeroizing-wrapped.

policy
SessionKeyPolicy::V1 · salt "gelo-rerank.session.v1" · info "gelo-rerank.query.v1"
derivation
QueryKey = HKDF(salt=SessionKey, ikm=query_id, info="gelo-rerank.query.v1")
uniqueness
caller must guarantee unique query_id per session — AES-GCM does not survive key+nonce reuse
future
attestation-bound ECDH KEX will replace the manual session secret; API surface unchanged
crypto

EncryptedRerankBundlegelo_reranker :: output

Fixed-shape AES-GCM-256 wire format. Always emits exactly k_max shuffled (nonce, ciphertext) items. Real items carry (rank, chunk_id, chunk_text); decoys carry a tag the client drops after decryption.

seal
top-k_final → encode each → AES-GCM with fresh per-item nonce → pad with k_max−k_final decoys → shuffle → emit
open
decrypt every item → filter Decoy → sort real items by embedded rank
padding
decoy plaintext padded to the longest real candidate's text length so per-item size doesn't reveal which are decoys
ranking

top_k_with_tie_shufflegelo_reranker :: score

In-TEE sort. Sort by score descending, then within each equal-score bucket randomise with the same QueryKey-seeded ChaCha20 RNG. Stops the host from learning a stable secondary order when scores tie.

determinism
given the same QueryKey, same RNG seed, same ordering — useful for debugging without breaking AES-GCM nonce safety
edge case
NaN scores sort to the back · empty top_k returns empty list
§06

Compute flow & trust boundaries

The rerank stage adds no new privacy primitive at the PCIe boundary — it reuses the same GELO mask + TwinShield round-trip the embedder validated, applied to a different model and a different scoring head. Two figures below trace one layer-block per architecture and add the family-specific scoring head at the bottom. FIG. 02a covers the cross-encoder family (bge-reranker-v2-m3 · XLM-RoBERTa-large · 24 layers); FIG. 02b covers the causal-LM-discriminator family (Qwen3-Reranker-0.6B · Qwen3 backbone · 28 layers). Same 4-offload-group rhythm per block as the embedder; what differs is the input packing and the head.

FIG. 02a — CrossEncoderRerankService · one XLM-RoBERTa-large block · post-LN · GELU · bidirectional · 24 layers · 2-layer classifier head on CLS blue arc = residual · solid red = TEE flow · dashed amber = masked PCIe transit (mix → / unmix ←)
Trusted side · in CVM cross-encoder + protocol kernel Untrusted GPU burn-cubecl · Vulkan PCIe · masked H_in (n, 1024) [CLS] query [SEP] document [SEP] residual ① QKV (masked offload) mix → matmul → unmix · +bias matmul_many [Q, K, V] 3 weights · 1 dispatch · 1 sync mix: U = A·H unmix: Aᵀ·(U·W) ② Bidirectional MHA 16 heads · q ↔ d token interaction · TEE ③ O (masked offload) mix → matmul → unmix · +bias matmul O single GEMM + ④ LayerNorm₁ (post-LN) LayerNorm(H + attn_proj) residual ⑤ FfnUp (masked offload) mix → matmul → unmix · 1024 → 4096 matmul FfnUp single GEMM · 1024 → 4096 ⑥ GELU ⑦ FfnDown (masked offload) mix → matmul → unmix · 4096 → 1024 matmul FfnDown single GEMM · 4096 → 1024 + ⑧ LayerNorm₂ (post-LN) → next block (×23) after the final block ⑨ Gather CLS row · H_final[0] ⑩ classifier.dense (masked) mix → matmul → unmix · 1024 → 1024 matmul classifier.dense single GEMM · 1024 → 1024 ⑪ tanh → out_proj (1024 → 1) · TEE · scalar score
FIG. 02b — CausalDiscriminatorRerankService · one Qwen3 decoder block · pre-LN · SwiGLU · causal GQA + RoPE · 28 layers · tied LM-head yes/no gather same pre-LN block as the Qwen3 embedder; only the input packing and the scoring head differ
Trusted side · in CVM decoder + protocol kernel Untrusted GPU burn-cubecl · Vulkan PCIe · masked H_in (n, 1024) <|im_start|>system Judge…<|im_end|> user <Query>:… <Document>:… <|im_end|> assistant <think> residual (around Norm + sub-block) ① RMSNorm₁ (pre-LN) ② QKV (masked offload) mix → matmul → unmix · no bias matmul_many [Q, K, V] 3 weights · 1 dispatch · 1 sync mix: U = A·H unmix: Aᵀ·(U·W) ③ RoPE on Q, K ④ Causal GQA attention 16 Q-heads · 8 KV-heads · causal mask · TEE + residual (around Norm + sub-block) ⑤ RMSNorm₂ (pre-LN) ⑥a Gate (masked) mix → matmul → unmix ⑥b Up (masked) mix → matmul → unmix matmul_many [gate, up] 2 weights · 1 dispatch · 1 sync ⑦ SiLU(gate) silu(gate) ⊙ up ⑨ FfnDown (masked offload) mix → matmul → unmix · 3072 → 1024 matmul FfnDown single GEMM · 3072 → 1024 + → next block (×27) after the final block + RMSNorm ⑩ Gather last-token H_final[n−1] · tied LM-head yes/no dot products H[n−1] · token_embedding.row(yes_id) | .row(no_id) ⑪ softmax([no, yes])[1] · TEE · scalar score

What to look for between the two figures.input packing — the cross-encoder packs [CLS] query [SEP] document [SEP] into one bidirectional sequence; the causal-discriminator wraps (query, document) in Qwen3's chat template and runs causal attention left-to-right. ② scoring head — cross-encoder gathers row 0 (CLS) and runs a 2-layer classifier (dense → tanh → out_proj); causal-discriminator gathers the last-token hidden and computes two dot products against token_embedding.row(yes_id) / .row(no_id) (tied LM head — no separate weight, no GPU offload for the scoring step). ③ per-block topology identical to the embedder family it inherits from — see embedding.html §05 for the same 8-step (BERT) / 9-step (Qwen3) rhythm. The reranker reuses the embedder's gelo_embedder::bert::forward::run / gelo_embedder::decoder::forward::run unchanged; the head is the only new mask boundary.

Boundary What crosses What does not
PCIe (TEE ↔ GPU), Stage B embed U=A·H_query · public weights clean H_query · mask A · query tokens
PCIe (TEE ↔ GPU), Stage C rerank U=A·H_pair per layer · public weights clean activations · mask · scores · chunk text
CVM ↔ Host RAM encrypted CVM pages · SWIOTLB DMA bounce buffers decrypted activations · scores · plaintext chunks
Network (TEE → client) k_max AES-GCM (nonce, ciphertext) pairs of fixed per-item size scores · rank order · chunk identity · k_final
Client → external generator prompt content the client decides to build rerank protocol state

Mask sampling cadence — three modes, one default

A Haar-uniform orthogonal mask A protects each masked PCIe round-trip. How often A is resampled is a separate dial from where it gets used. Three cadences are coherent at the protocol level; the executor in gelo-protocol ships the first one as the default and exposes the third as an explicit opt-in for the BSS-recovery / parity tests.

Cadence Haar samples / forward Security knob Wall-time cost When to pick
Per forward pass (default) 1 Requires ShieldConfig (8 Gaussian rows at energy 4·mean‖h‖) — the appended noise rows break the cross-offload Gram-matrix correlation an ICA / BSS attacker would otherwise exploit when the same A protects QKV, O, gate/up, and down within a forward. 1 Haar QR / forward. At n+k=520 that's ~18 ms once, then amortised across ~96 mask-apply / mask-unapply GEMMs per forward. ~<1% of rerank wall time on Vulkan iGPU. Production. Paper §3.2 protocol. Best wall-clock with shield's correlation defence intact.
Per transformer block num_hidden_layers (24 for bge-rerank, 28 for Qwen3-rerank) Same shield requirement intra-block. Cross-block isolation only helps if the threat model includes a per-block correlation attacker — which the current ICA / Game-of-Arrows attacks do not. No empirical attack motivates this cadence today. ~N× the per-forward cost; at our QR rate ~0.4–0.5 s / forward extra purely for sampling. Niche. Useful if a future attack ever localises within a block: drop in begin_block/end_block brackets. Not implemented; would be a thin extension of the current session-mask scaffolding.
Per offload 4–7 × num_hidden_layers (96 / forward for bge-rerank, 112–140 for Qwen3-rerank) Strictly the safest in isolation — every masked GEMM has independent A, so no cross-offload ICA is possible even without shield. This is what the bare_orthogonal_mask_leaks_gram_matrix test exercises, demonstrating that without shield and without per-offload, Gram matrix leakage is recoverable. Dominant. Measured at 46–48% of rerank wall time pre-flip (33 s of mask sampling per 71 s bge-rerank, 51 s per 108 s Qwen3-rerank). Safety / attack baselines only. Opt in via InProcessTrustedExecutor::with_seed(…).with_per_offload_mask(). The BSS / U-Verify regression tests use this mode.

Why per-forward + shield is the default. Per-offload's correlation safety is real but the cost is the Haar-QR over an (n+k)×(n+k) matrix, which is O((n+k)³). Reranker inputs at n≈256–512 push the QR cost to 17–23 ms per call — at ~100 calls per forward that is most of the wall-clock. Per-forward sampling does the QR once, then the same A participates in every offload's mix/unmix GEMM (which is GEMM-fast and CBLAS-tractable). Shield rows are sampled fresh per offload regardless (cheap — k Gaussians), and that per-offload freshness is what stops cross-offload ICA without paying the per-offload QR tax. The paper's §3.2 + §4.2 pairing is exactly this trade.

§08

Performance & correctness

Hardware: AMD Ryzen AI Max+ 395 (Strix Halo). Vulkan adapter: AMD Radeon Graphics (RADV GFX1151) (IntegratedGpu). All numbers under InProcessTrustedExecutor with GELO+mask enabled.

Rerank latency — two regimes, two architectures

"Per-pair" alone is ambiguous: it depends on whether the candidate batch fans out across CPU workers (k′ > 1) or runs sequentially (k′ = 1). Both regimes reported for both architectures. Same lever stack (L1+L2+L3+L4+L5+L6) applies to both — same patterns, ported to bert::* and decoder::* separately.

Workload Single-pair (k′=1) Batch (k′=20)
bge-reranker-v2-m3, NFCorpus n≈256, Vulkan 1.19 s/pair 3.02 s · 151 ms/pair
Qwen3-Reranker-0.6B, NFCorpus n≈400, Vulkan 1.83 s/pair 6.48 s · 324 ms/pair

Why Qwen3 is slower at both regimes: 28 layers vs BGE's 24, chat-templated prompt inflates n from ~256 to ~400, and the mask GEMMs scale as (n+k)² · d. The shape of the breakdown is the same on both paths — mask GEMMs are at the AOCL-BLIS AVX-512 floor, GPU is dispatch-bound, attention is 6× smaller after rayon-parallel heads.

Per-bucket breakdown (single-pair k′=1)

Traced with E2E_TRACE=1 on rerank_e2e_bench.rs. Batch trace is empty by design — profile::time is thread-local and rayon workers don't roll up to the main thread.

Bucket bge (1.19 s) Qwen3 (1.64 s traced) Notes
gelo:mask_unapply 28% 35% AOCL-BLIS AVX-512 SGEMM; floor for this hardware
gelo:mask_apply 21% 20% same — floor
engine:matmul + matmul_many 27% 24% Vulkan iGPU dispatch — bandwidth-bound
In-TEE attention 4.7% 5.9% 16 heads parallelised via rayon (was ~24% / ~21% pre-L3)
Last-block in-TEE projections 6.7% 7.1% skip-last-layer keeps these TEE-resident (GELO §3.2)
Element-wise (GELU / SwiGLU / LN / RMSNorm / residual) 5% 3% rayon-parallel at rerank shape (L4/L5)
gelo:mask_sample 2.5% 1.6% one Haar QR per forward — see §05
Other (tokenize, head, lookup) <1% <1%

End-to-end stages (R7 GPU, 100 docs, 1 query, k'=20, k=10)

Stage Wall Per-unit
A · Ingest (BGE-base GELO+mask+Vulkan + CAPRISE) 7.7 s 13.0 docs/s
B · Retrieve (BGE-base query embed + CAPRISE cosine, k'=20) 178 ms 178 ms/query
C · Rerank bge (20 pairs) 3.02 s 151 ms/pair
C · Rerank Qwen3 (20 pairs) 6.48 s 324 ms/pair

Optimization headroom landed

All numbers below ride the paper-parity executor default (per-forward Haar mask + shield k=8) and AOCL-BLIS via the blas feature (now default-on for gelo-rag / gelo-reranker / gelo-snp-runner).

Stack bge k′=1 bge k′=20 Qwen3 k′=1 Qwen3 k′=20
T0 baseline — per-offload mask, no BLAS 2.29 s 45.7 s 5.39 s 107.8 s
+ L1 (AOCL-BLIS, thread-local pin) 1.74 s 27.1 s 2.37 s 40.7 s
+ L2 (rayon-parallel candidates) 1.74 s 3.23 s 2.37 s ~9 s
+ L3/L4/L5/L6 (parallel heads, GELU/SwiGLU, LN/RMSNorm, skip-last) 1.19 s 3.02 s 1.83 s 6.48 s
Speedup vs T0 1.92× 15.1× 2.94× 16.6×

Qwen3's batch speedup is slightly larger than BGE's (16.6× vs 15.1×) because L1 alone gives more headroom on the longer-n Qwen3 path (mask GEMMs are (n+k)² · d; n≈400 vs 256 → 2.4× more flops per offload, so the BLIS speedup matters more). BGE's single-pair speedup is smaller because more of its work is in the GPU dispatch column, which doesn't accelerate with CPU-side rayon.

Two non-default options tried and judged not worth turning on:

Two non-default options tried and judged not worth turning on:

Ranking metrics on the same run

Baseline = BGE-base GELO+mask+Vulkan cosine over CAPRISE index. Subset of 100 NFCorpus docs constructed to retain qrel-relevant docs (subset_corpus in the bench). Single-digit query counts make these numbers high-variance per-stage; the relative deltas matter more than the absolute values at this scale.

Stage nDCG@10 Recall@k MRR@10 Δ(nDCG@10 vs baseline)
B · retrieve (baseline) 0.629 0.717 (k=20) 0.800
C · rerank bge 0.597 0.490 (k=10) 0.753 −0.032
C · rerank Qwen3 (1-query slice) 0.571 0.375 (k=10) 1.000 −0.247

Two distinct stories. The bge Δ = −0.032 on 10 queries is within sample noise on a 100-doc subset where the baseline is already strong (subset_corpus deliberately keeps relevant docs). Not evidence of a pipeline bug. The Qwen3 Δ = −0.247 across multiple runs is structural — the QWEN3_RERANKER_TEMPLATE constant omits the <Instruct>: line from the official HF model card example. Without it, the discriminator falls back to a weaker signal. Tracked in §08 as the first follow-up.

M1.11 R2 — Batched prefill

The L2 path above runs N rerank candidates through Rayon as N independent single-stream forwards. That extracts CPU parallelism, but each forward pays its own GELO mask sample and its own per-layer GPU dispatch, and N forwards contend for the iGPU's serial work queue. M1.11 R2 restructures rerank so all N (query, doc) pairs ride one batched forward: one prefill pass with N per-sequence masks A_b derived from a shared batched-forward seed, one GPU dispatch per layer covering the whole batch. The per-row security argument is unchanged from the Rayon-per-worker model — identical A_b structure, just dispatched as one batched call — so no new AloePri derivation is required (the c4 spot-check at B=16 is paranoia rather than protocol revision).

Result — Qwen3-Reranker-0.6B, B = 8

A/B against serial single-stream rerank × N (no concurrency, no Rayon). The acceptance baseline is deliberately *not* the existing Rayon path: Rayon is orthogonal CPU parallelism that the batched design composes with, not against. Measuring against serial lets the M1.11 mechanism (GPU dispatch amortisation + per-sequence mask vectorisation) appear in isolation. Two subsequent CPU-side optimisations stack on top of the substrate.

Stack Wall (B = 8) Per-pair vs serial
Serial × B (baseline) 6.08 s 760 ms (base)
+ batched substrate 2.54 s 317 ms 2.07×
+ rayon-parallel per-block mask apply/unapply ~2.00 s ~250 ms ~3×
+ batched matmul_many for QKV + SwiGLU gate/up (final) 1.86 s 233 ms 3.27×

Hardware: Strix Halo iGPU (RADV GFX1151, Vulkan). Speedup ratio is the load-bearing number; absolute wall fluctuates ±10 % across runs because the iGPU and CPU share unified memory.

Where the wall goes (post-optimisation)

Bucket Serial Batched Batched share
GPU matmul (single + many) 3 901 ms 989 ms 60.0 %
GELO mask apply + unapply 1 822 ms 392 ms 23.8 %
Shield-row fill 68 ms 116 ms 7.0 %
In-TEE causal attention (per-sequence loop) 50 ms 56 ms 3.4 %
Other CPU (norms, RoPE, SwiGLU, head, …) ≈ 230 ms ≈ 95 ms 5.8 %

GPU dispatch count crashes 8× (896 → 112) and total CPU mask work shrinks ≈ 4.7×. GPU now dominates batched wall at 60 % — the characteristic shape of a GPU-bandwidth-bound private-rerank workload on this iGPU. Shield-fill share grows in relative terms even though the bucket size doesn't change much; the D1.7 sub-stream RNG optimisation lands in the decode bench and carries over for decode-shaped reranker workloads, but rerank at B = 8 doesn't yet show it as a top-tier bucket.

Client interface unchanged: the rerank service still emits a fixed-shape AES-GCM bundle exactly as before. The batched path is chosen automatically when more than one candidate is submitted.

Protocol fidelity

Nine reranker-specific tests across four files validate masked-vs-plain agreement and the wire-format round trip:

§09

Status & gaps

What's landed

Highest-impact next levers

# Lever Status Measured impact
L1 AOCL-BLIS for mask GEMMs (blas default feature) + thread-local single-thread pin in sgemm_blis so rayon workers don't oversubscribe. landed −24% single-pair · −41% batch
L2 Rayon-parallel candidate loop in CrossEncoderRerankService::rerank and CausalDiscriminatorRerankService::rerank; one cloned executor per worker, single-candidate fast path. landed −88% batch · per-pair unchanged
L3 Rayon-parallel attention heads in bert::attention::multi_head_attention and decoder::attention::causal_gqa_attention (plus the per-head softmax+V loop of causal_gqa_attention_with_offload) when n ≥ 64; embedder shape stays serial. landed −27% single-pair (bucket 7.1× shrink)
L3a Route every ndarray::dot() through BLIS (blas-ndarray opt-in feature). opt-in −3% single-pair · −1% batch — kept opt-in for hardware-specific tuning
L3b OutAttnMult for the BERT path (BertConfig::use_out_attn_mult); plumbed end-to-end. plumbed, default off regresses on this iGPU at n ≈ 256; useful at n ≥ 512 or on a dGPU
L4 / L5 Rayon-parallel elementwise + norm: GELU/LayerNorm/add_bias in bert::forward, SwiGLU in decoder::swiglu, RMSNorm in decoder::rms_norm. Shape-conditional threshold (n × d ≥ 32 768). landed ~−7% single-pair (combined)
L6 with_skip_last_layer(true) builder on both reranker services (wraps BertConfig::skip_last_layer / DecoderConfig::skip_last_layer) — GELO §3.2 sensitive-layer exclusion. landed −1.5% wall · paper-aligned default
Qwen3 template Fix QWEN3_RERANKER_TEMPLATE — add the missing <Instruct>: line per the HF model card. Tracked separately because Qwen3 was descoped from the BGE perf sweep. open most likely cause of the Qwen3 −0.247 nDCG regression
Bucket-pad seq Bucket-pad input tokens to {128, 256, 512} to keep cubecl's autotune cache hot. open p99 stabilisation, mean wall unchanged

Deferred / out of scope