Why every production RAG system is a cascade. The bi-encoder / cross-encoder asymmetry, the indexability theorem, ANN mathematics, fusion derivations, latency composition, and the (k, quality, latency) Pareto frontier.
This deck covers the architecture and cost-quality mathematics of the two-stage cascade. Its sibling, RAG 09 — Reranker Mathematics, takes the second stage and derives the actual scoring functions and training losses: cross-encoder attention, ColBERT MaxSim, RankNet / LambdaRank / LambdaMART, InfoNCE, MarginMSE distillation, and the metric mathematics (DCG / NDCG / MRR / MAP).
Every serious RAG system in production is a cascade: a cheap, high-recall stage that scans the entire corpus, followed by an expensive, high-precision stage that re-scores a short-list of survivors. The structure is not an implementation detail — it is the only architecture that makes the cost arithmetic work.
A modern cross-encoder reranker scores one (query, document) pair in 1–5 ms on a single GPU. A modest production corpus has 107 documents. Scoring every pair: 107 × 2 ms = 5.5 hours per query. Per query. With one user. The cascade exists because we cannot run the precise model on the whole corpus.
The cascade is justified by two empirical observations that hold across almost every retrieval benchmark:
The cascade composes the strengths: cheap recall × expensive precision = both. Everything in this deck is a quantitative consequence of that composition.
The two stages exist because there are two structurally different ways to score a (query, document) pair. They differ in where the query and document interact.
Query and document are encoded independently into a single vector each: f(q), g(d) ∈ ℝD. Score is a single inner product. Time per pair: O(D). Document vectors can be precomputed once and stored in an index.
Concatenate query and document into one transformer input. Every query token attends to every document token in every layer. Time per pair: O((|q|+|d|)2) in the attention layers. Document representations depend on the query and cannot be cached.
In a bi-encoder, the only path from query token qi to document token dj is through two pooled scalars. Token-level evidence (a single matching named entity, a specific operator, a date) has to fight its way into a global pooled vector. In a cross-encoder, query token qi can attend directly to document token dj in layer 1 and refine that attention in every subsequent layer. The fine-grained signal is preserved end-to-end. The cost of preserving it: you must re-run the model on every (q,d) pair you want to score.
| Property | Bi-encoder | Cross-encoder |
|---|---|---|
| Score formula | 〈f(q), g(d)〉 | h([CLS] q [SEP] d) |
| Doc encoding depends on query? | No — precompute once | Yes — recompute every query |
| Cost of one score (BERT-base) | 1 dot product ≈ 1–3 μs | 1 full forward pass ≈ 1–5 ms (GPU) |
| Cost over corpus of N docs | 1 query encode + N dot products = O(D·N) | N full forward passes = O(L·N) |
| Indexable by MIPS / ANN? | Yes (see slide 03) | No — no factorisation exists |
| BEIR avg NDCG@10 (2024 SOTA) | ≈ 48–52 (BGE-M3, E5-mistral) | ≈ 55–60 (BGE-Reranker-v2-m3, Cohere) |
An index speeds up retrieval by exploiting structure in the scoring function. Whether structure exists is a property of the function, not the data. This is where the bi-encoder / cross-encoder split becomes a hard mathematical wall.
For a bi-encoder, retrieving the top-k documents is the problem:
d∗ = arg maxd ∈ D 〈 f(q), g(d) 〉Crucially, f(q) is fixed once the query is encoded; only g(d) varies as we sweep d. The problem reduces to finding the nearest neighbour of a fixed point in a precomputed point cloud {g(d) : d ∈ D}. This admits sub-linear indexing — tree-, graph-, and quantisation-based structures all work.
For a cross-encoder, the score is h([CLS] q [SEP] d). There is no decomposition
that holds for general inputs. Self-attention layers contain softmax(QKT/√d) V where Q, K, V are computed from all tokens jointly. There is no way to precompute a per-document representation that supports cheap query-time scoring under arbitrary cross-attention. This is the indexability barrier.
Suppose, for contradiction, that a cross-encoder admits a factorisation h(q,d) ≡ 〈φ(q), ψ(d)〉 with φ, ψ mapping to ℝD. Then the rank of the matrix Mq,d = h(q, d), over any finite set of queries and documents, is at most D. But the matrix of cross-encoder scores has empirically been shown to be approximately full rank on real corpora — if it were not, a bi-encoder of dimension D would match cross-encoder quality, which is false. Hence no exact factorisation exists.
ColBERT keeps token-level embeddings on both sides and replaces full cross-attention with a per-query-token max:
s(q,d) = Σi ∈ q maxj ∈ d 〈 Eqi, Edj 〉This is factorisable per document (each document's tokens are independent of the query at encode time) and admits a token-level inverted index. It is a strict relaxation of full cross-attention — each query token chooses its match in isolation, with no joint reasoning across query tokens — but it preserves enough token-level signal to close most of the gap to a cross-encoder. Covered in detail in Deck 09.
Bi-encoder retrieval over millions of vectors is fast only because we use an approximate nearest-neighbour structure. The three production-grade families — HNSW, IVF, and PQ — each trade exactness for speed in a different way.
Multi-layer proximity graph. Top layer is sparse; bottom contains every vector. Search descends greedily, beam width ef_search controls candidate set per layer.
Expected query complexity: O(log N). Recall is monotone in ef_search; bigger ef = more recall, more latency. Memory: ~1.2–1.5× the raw vectors (graph overhead).
K-means into n_list centroids at build time. Query: pick the n_probe nearest centroids, search only those cells.
Cost: O(n_probe · N/n_list) distance evaluations. Typical: n_list = √N, n_probe = 8–32. Recall scales sharply with n_probe; latency scales linearly.
Split each D-dim vector into m sub-vectors of dim D/m. K-means each sub-space to 2b codes. Store one byte (b=8) per sub-vector.
Memory: m bytes per vector instead of 4D. Distance via lookup table — ~8× faster than float dot product but lossy. Usually layered under HNSW or IVF for compression.
Every ANN structure exposes a knob (ef_search, n_probe, refinement depth) whose monotone increase produces a smooth recall–latency curve:
Two empirical facts shape all production tuning:
ef_search doubles distance evaluations. Doubling n_probe doubles cell scans. There are no free lunches inside one method — the lunches come from changing methods (IVF→HNSW, fp16→int8, refine-with-fp32).A high-leverage 2024 pattern: index 1-bit-per-dimension binary vectors with Hamming distance (32× memory reduction, 4–10× speed-up), then rescore the top ~200 candidates with the original fp16/fp32 vectors. Recall@10 typically drops by <1% versus float indexing. Supported in Qdrant, Vespa, Weaviate, Milvus, and Postgres pgvector 0.7+.
Each stage of the cascade has a different optimisation target. Confusing them is the most common architectural mistake in RAG pipelines.
| Stage | Optimise for | Why | Typical k |
|---|---|---|---|
| Stage 1 (retriever) | Recall | Anything missed here is lost forever — no downstream stage can recover it. | 100–1000 |
| Stage 2 (reranker) | Precision & rank quality (NDCG, MRR) | The LLM sees only the top-k. Bad ranking here is bad context. | 5–20 |
| Stage 3 (LLM) | Faithfulness | Given the top-k, does the generation cite them correctly? | 3–10 in prompt |
Let R1(k) = Recall@k of stage 1 over a query distribution. For any stage-2 reranker 𝒻, including an oracle that ranks the stage-1 short-list perfectly:
Recall@kfinal(𝒻) ≤ R1(k1) for any k ≤ k2 ≤ k1Stage-1 recall is the global ceiling. If your stage-1 Recall@100 is 92%, then 8% of user queries have no answer reachable from stage 2 — no matter how good your reranker, no matter how big your LLM. The only fix is at stage 1: better embeddings, hybrid retrieval, larger short-list, query rewriting.
Before tuning anything downstream, measure Recall@k1 on a labelled set. If it is below ~90% at the short-list size you can afford to rerank, the cascade is fundamentally capped — investing in a better reranker buys nothing. Fix stage 1 first.
When stage 1 fans out into multiple retrievers (BM25 + dense, plus perhaps domain-specific lexical or graph retrievers), their result lists must be fused. The fusion methods differ in what they assume about score scales.
BM25 returns unbounded positive scores whose magnitudes depend on document length, query term IDF, and corpus statistics. Dense bi-encoder cosine similarity returns values in [-1, 1] with most relevant pairs in [0.3, 0.9]. Adding them directly is meaningless — one retriever's noise floor can be louder than the other's signal.
Requires per-retriever score normalisation — usually min-max to [0,1] or Z-score:
Sensitive to outliers and to query-to-query score distribution drift — one query with a huge top score skews the entire normalisation.
Uses only ranks, never raw scores. Three properties make it the modern default:
A variant: CombMNZ(d) = (number of retrievers that returned d) × CombSUM(d). Documents found by all retrievers are boosted. Rarely used in modern pipelines because RRF achieves a similar agreement-boosting effect implicitly — a document in the top-10 of every retriever accumulates many reciprocals, while a document found only once accumulates one.
If you have labelled query–document pairs (clicks, manual judgments, LLM-as-judge), a small LambdaMART or shallow MLP over per-retriever rank-features (rankr(d), scorer(d), 1/rankr(d), retriever indicator) typically beats RRF by 1–3 NDCG@10 on the training distribution. The training cost is rarely worth it for a single-domain RAG system — use RRF until measured otherwise.
Latency budgets are not additive in the obvious way. The mean of a sum is the sum of means, but tail percentiles compose much worse, and parallelism reshapes the budget entirely.
For independent stages with latencies X, Y:
E[X + Y] = E[X] + E[Y] means add P99(X + Y) ≠ P99(X) + P99(Y) in generalIf X and Y are independent and roughly Gaussian, P99(X+Y) ≈ √(P99(X)2 + P99(Y)2) — squared addition. But on real systems stage latencies are correlated (GC pauses, GPU contention, network blips), so the worst case is closer to P99(X) + P99(Y). Plan against the worst case until you have measurements.
Typical 1M-doc corpus: 15 + 8 + 80 = 103 ms mean. P99 closer to 50 + 25 + 130 = 205 ms. Wastes the obvious independence of stage 1.
Same numbers: max(15, 8) + 1 + 80 = 96 ms mean — modest mean win, but a larger tail win because the slow retriever drives the max. Doable with asyncio.gather or any thread pool.
A batched cross-encoder reranker scoring k candidates has latency roughly:
Lrerank(k) ≈ tload + ⌈ k / B ⌉ · tfwd(B, Lseq)where B is GPU batch size, Lseq is sequence length, tfwd is per-batch forward time. Two practical levers:
Tuning a cascade is a constrained optimisation. There is one free knob — the short-list size k — that simultaneously trades against quality and latency. Plotting one against the other yields the Pareto frontier and tells you exactly where to operate.
Three behaviours hold across every cross-encoder reranker on every modern benchmark:
NDCG@10 vs k typically follows a concave saturating curve; latency follows a near-linear ramp. Their composition is a classic logarithmic-in-quality, linear-in-cost frontier. The Pareto-optimal operating point is the knee — usually k=50–100 on text reranking. Below the knee you leave quality on the table; above it, you pay linear latency for vanishing returns.
Reranker scores are not probabilities. They are logits or unnormalised real numbers that have been trained for ranking, not for absolute probability. If you want to use a score as a confidence threshold (“drop any document with relevance < 0.3”) or compare scores across queries, you must calibrate.
Fit a one-parameter (or two-parameter) sigmoid to map reranker score s to a calibrated probability:
P(rel | s) = σ(a · s + b) = 1 / (1 + exp(−(a s + b)))Fit a, b by maximum-likelihood on a held-out set of (s, binary-rel) labels. Lightweight, monotone, and reversible. Works well when the score-to-probability map is roughly sigmoidal — usually true for cross-encoder logits.
A one-parameter restriction of Platt (a=1/T, b=0). Often used for neural classifiers; rarely the best fit for reranker outputs, but easy and hard to overfit.
Fit a monotone step function from s to P(rel | s) by pool-adjacent-violators. No functional form — learns whatever monotone map fits. Pros: handles any shape, including S-curves, hockey-sticks, or plateaus. Cons: needs more calibration data (~1000+ pairs); not differentiable; harder to extrapolate beyond the training range.
The two places calibration is load-bearing: (1) early exit — if the top reranker score is above threshold τhigh, skip the LLM and return the document directly; (2) abstention — if every score is below τlow, return “I don't know” instead of hallucinating from poor context. Both need cross-query-comparable probabilities, not raw scores.
Concrete numbers ground the abstractions. Consider a typical mid-scale enterprise RAG: 107 chunks, a 200 ms retrieval budget (300 ms left for LLM), single-GPU reranker.
| Stage | Choice | Mean | P99 | Notes |
|---|---|---|---|---|
| Query encode (bi-encoder) | BGE-base (110 M), fp16, GPU | 4 ms | 9 ms | Single query, no batch |
| BM25 retrieve | OpenSearch, 4 shards, top-200 | 22 ms | 70 ms | Parallel with dense |
| Dense ANN retrieve | Qdrant HNSW, ef=128, top-200, binary+rescore | 12 ms | 30 ms | Parallel with BM25 |
| RRF fusion | k=60, in-process Python | <1 ms | <1 ms | Reduces to top-100 |
| Cross-encoder rerank | BGE-reranker-base (278 M), Lseq=384, batch=64, A10 | 85 ms | 140 ms | One batch covers k=100 |
| End-to-end retrieval | Parallel stage 1 + sequential rerank | ≈ 110 ms | ≈ 220 ms | P99 just over budget |
In rough order of leverage: (1) drop k from 100 to 50 — saves ~40 ms reranker P99 for typically 1–2 NDCG@10 points; (2) use a smaller reranker (ms-marco-MiniLM-L-6, ~20 ms for k=100, ~4 NDCG@10 worse); (3) move the reranker to a dedicated GPU and serve via gRPC (cuts variance, not mean); (4) cache rerank scores on (query-hash, doc-id) for repeat queries; (5) use ColBERT-MaxSim instead — comparable quality, lower latency at k>200 (see Deck 09).
Recall slide 01: scoring all 107 docs with the cross-encoder would take 5.5 hours. The cascade compresses that into ~85 ms by:
Net speed-up: ~2×105×. That is the entire point of the architecture.
The companion deck, RAG 09 — Reranker Mathematics, dives into stage 2 itself: the cross-encoder architecture, ColBERT's late-interaction MaxSim, the learning-to-rank loss family (pointwise, pairwise RankNet, listwise LambdaRank / LambdaMART), the InfoNCE contrastive loss used to train modern bi-encoders, MarginMSE distillation from teachers to students, and formal derivations of the evaluation metrics (DCG with log2 discount, NDCG, MRR, MAP) and their relationship to the loss functions.