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
where this fits
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.
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
what each component sees · what it cannot
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
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
two architecture-typed services under one RerankService trait
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
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
crate-side · 6 implementation units
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).
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"
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
per-request, external-generator deployment
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 CLSblue arc = residual · solid red = TEE flow · dashed amber =
masked PCIe transit (mix → / unmix ←)
FIG. 02b — CausalDiscriminatorRerankService · one Qwen3 decoder
block · pre-LN · SwiGLU · causal GQA + RoPE · 28 layers · tied
LM-head yes/no gathersame pre-LN block as the Qwen3 embedder; only the input packing
and the scoring head differ
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 oftenA 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
measured on AMD Strix Halo iGPU
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)
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).
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:
L3a · ndarray/blas globally — routes
every workspace .dot() through cblas. Marginal on BGE:
1.62 → 1.57 s single-pair, 3.22 → 3.18 s batch (~3% / ~1%). Per-head
attention shape (256, 64) · (64, 256) sits on the
BLIS-vs-matrixmultiply crossover. Kept as an opt-in
blas-ndarray feature for users to try on their own
hardware; the global blast radius doesn't justify default-on for a
3% gain.
L3b · OutAttnMult for the BERT path — plumbed (new
BertConfig::use_out_attn_mult +
multi_head_attention_with_offload mirroring the decoder
path), but on this iGPU at our shapes it regressed both regimes
(single-pair 1.74 → 4.33 s; batch 3.23 → 17.83 s — the iGPU
serialises per-layer Q·Kᵀ dispatches across rayon workers). Code
stays — earns its keep at n ≥ 512 or on a dGPU — but
the bench default keeps it off.
Two non-default options tried and judged not worth turning on:
L3a · ndarray/blas globally — routes
every workspace .dot() through cblas. Marginal: 1.62 →
1.57 s single-pair, 3.22 → 3.18 s batch (~3% / ~1%). Per-head
attention shape (256, 64) · (64, 256) sits on the
BLIS-vs-matrixmultiply crossover. Kept as an opt-in
blas-ndarray feature for users to try on their own
hardware; the global blast radius doesn't justify default-on for a
3% gain.
L3b · OutAttnMult for the BERT path — plumbed (new
BertConfig::use_out_attn_mult +
multi_head_attention_with_offload mirroring the decoder
path), but on this iGPU at our shapes it regressed both regimes
(single-pair 1.74 → 4.33 s; batch 3.23 → 17.83 s — the iGPU
serialises per-layer Q·Kᵀ dispatches across rayon workers). Code
stays — earns its keep at n ≥ 512 or on a dGPU — but
the bench default keeps it off.
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:
cross_encoder_parity.rs — masked and plaintext
executors agree on score within 1e-3 on synthetic
2-layer BERT weights; top-1 rank preserved.
causal_discriminator_parity.rs — same shape;
softmax([no, yes])[1] agrees within
1e-3 and [0, 1] bounds hold under both
executors.
bundle_round_trip.rs — full rerank() →
wire-shape EncryptedRerankBundle (always
k_max items) → client decrypts with matching
QueryKey → recovers exactly top_k real
items in the in-TEE rank order. Wrong session key fails to open.
comparative_bench.rs::real_models_bge_vs_qwen3 —
#[ignore] real-weight A/B; both rerankers select a
RAG-grounded doc at rank 0.
gelo-snp-runner integration tests —
/rerank returns 501 unconfigured; returns a valid
bundle when configured; client reconstructs and opens it via the
session-derived key.
§09
Status & gaps
what's landed · what's next · what's deferred
What's landed
crates/gelo-reranker — full crate, 9 source files, 4
test files.
gelo-snp-runner/rerank HTTP route with
mock-issuer integration test.
E2E bench
crates/gelo-rag/tests/rerank_e2e_bench.rs with
E2E_DOCS · E2E_QUERIES · E2E_KPRIME · E2E_KFINAL · E2E_SKIP_BGE ·
E2E_SKIP_QWEN3
knobs and configurable corpus subsetting.
Real-weight bench
crates/gelo-reranker/tests/comparative_bench.rs::real_models_bge_vs_qwen3
running on AMD Vulkan iGPU.
This documentation page.
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).
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
jina-reranker-v3 — listwise n≈16k forwards need
gelo-llm.md §3's fused permuted attention +
FlashAttention to land first. License is CC-BY-NC-4.0. Revisit
alongside the LLM-serving stack.
Shredder at the pooled activation — round-2 §6
follow-up. Only relevant if a deployment needs to
export scores for downstream use (calibrated cutoffs,
hybrid fusion). Default TEE-internal architecture already keeps
scores sealed.
Score-DP accountant — formal
(ε, δ) budget over rerank score exports. Same trigger
as Shredder.
ECDH-bound session-key handshake — currently
session_secret is a 32-byte token the client supplies
per request. An attestation-bound ECDH KEX will replace it; the
SessionKey::derive API surface stays identical.
Game-of-Arrows attack bench in reranker mode —
round-2 §4.3. The construction is safe by the same argument that
protects the embedder; the empirical confirmation is gated on
lifting the attack reference into a workspace test.