Aditya Kusupati, Gantavya Bhatt, Aniket Rege, Matthew Wallingford, Aditya Sinha, Vivek Ramanujan, William Howard-Snyder, Kaifeng Chen, Sham Kakade, Prateek Jain, Ali Farhadi — arXiv:2205.13147, NeurIPS 2022

Matryoshka: One Vector, Every Budget

Train a 768-dimensional embedding so that its first 64 numbers are, by themselves, a complete embedding. Then the slider between speed and accuracy becomes a slice, chosen at query time, for free.

Prerequisites: what a dot product is + what a softmax does. Cross-entropy, cosine retrieval, PCA, and quantization are all built from zero.
10
Chapters
8
Interactive Sims
14×
Wall-Clock Speed-Up
1
Training Run

Chapter 0: Three Budgets, One Model

You run semantic search over twelve million support tickets. There is one embedding model. It takes a piece of text and returns a vector of 768 floating-point numbers, and two texts are “similar” when their vectors point in nearly the same direction.

The model is good. It is also the only model you have, and three completely different things now want to use it.

ConsumerWhat it doesBudgetRuns on
AutocompleteSuggests tickets as the agent types, on every keystroke20 ms end to endA shared CPU box, thousands of concurrent sessions
Search results pageFull ranked list when the agent presses Enter200 msOne GPU-backed service
Nightly dedupClusters twelve million tickets against each otherSix hours, offlineWhatever you want

Three budgets differing by five orders of magnitude, one representation. Before anything else, let us find out how badly the single 768-dimensional vector fits each of them. All of this is arithmetic you can do on a napkin, and the napkin is the whole reason this paper exists.

What 768 dimensions costs, in bytes

An embedding is stored as float32 — four bytes per number — unless you have taken deliberate steps otherwise. So one ticket costs:

768 numbers × 4 bytes = 3,072 bytes = 3 KB per ticket

Twelve million tickets:

12,000,000 × 3,072 bytes = 36,864,000,000 bytes ≈ 36.9 GB

Hold that number next to a machine. An AWS r6i.2xlarge has 64 GB of RAM. Your vector index alone eats 58% of it, before the operating system, before the inverted index, before the model itself, before any headroom for a traffic spike. You are one product launch away from paging to disk, and paging to disk on a 20 ms budget is not a slowdown, it is an outage.

What 768 dimensions costs, in time

Now the latency side. The most honest baseline is an exhaustive scan: score the query against every ticket. Cosine similarity between two d-dimensional vectors is d multiplies and d − 1 adds — call it d multiply-accumulate operations (MACs), the standard unit for “one multiply plus one add fused together”.

12,000,000 tickets × 768 MAC = 9,216,000,000 MAC = 9.22 GMAC per query

A modern CPU core with AVX-512 does roughly 16 single-precision MACs per cycle at about 3 GHz, so about 48 GMAC/s per core if everything is perfect. Perfect never happens, but take it:

9.22 GMAC ÷ 48 GMAC/s ≈ 0.19 s = 190 ms

That is nine and a half times over the autocomplete budget, on one core, with an unrealistically generous throughput assumption, ignoring the fact that you also have to read 36.9 GB from memory. Streaming 36.9 GB at a realistic 50 GB/s of memory bandwidth is 738 ms on its own. The scan is not compute-bound; it is bandwidth-bound, and bandwidth scales with bytes, which scales with dimension.

Dimension is not a modelling detail. It is a line in the infrastructure budget. Every dimension you add multiplies your RAM bill, your memory-bandwidth bill, your index build time, your network transfer when you shard, and your cold-start time. A 768-dimensional embedding is not “better than” a 64-dimensional one in any absolute sense — it is better per query and twelve times worse per byte. Which one wins depends entirely on which of the three consumers is asking.

Walk the shapes, so the budget stops being abstract

Numbers on a page are easy to nod at. Trace the actual tensors through one autocomplete keystroke and the pressure becomes physical.

the data flow of one query, with every shapekeystroke                      # a string, ~24 characters
  → tokenizer                  # (1, 12)      int64 token ids
  → encoder forward            # (1, 12, 768) float32 hidden states
  → pooling                    # (1, 768)     one vector per query
  → L2 normalise               # (1, 768)     unit norm
  ────────────────────────────────────────────────────────────────
  → score against the index    # (1,768) @ (768, 12_000_000)
                               #   = 9.22e9 multiply-accumulates
                               #   index tensor is 36.9 GB, resident
  → top-k selection            # (1, 12_000_000) -> (1, 10)
  → hydrate + serialise        # 10 row lookups

Notice the line where the shapes stop being small. Everything above the divider is a few hundred kilobytes of arithmetic on a single 768-vector, and it finishes in a couple of milliseconds on a CPU. Everything below touches a 36.9-gigabyte tensor. The query encoder is not the problem. The index is the problem, and the index's size is N × d, of which you control exactly one factor.

Itemize the 20 ms budget and there is nowhere to hide:

StageBudgetScales with
Network in, request parse2 msnothing you control
Tokenize + encode the query4 msmodel size — fixed once you pick the model
Search the index10 msN × d bytes touched
Hydrate 10 rows, serialize, respond4 msresult count

Three of the four rows are constants of your architecture. The third is a knob, and d is the half of it you can turn without deleting documents.

The obvious fix, and its true price

So: train three models. A 64-dimensional one for autocomplete, a 256-dimensional one for the results page, a 768-dimensional one for nightly dedup. Everyone gets what they need. Let us price it properly, because the price is not where people expect.

Storage looks better and is worse. Three indexes:

64-dim: 12M × 64 × 4 = 3.07 GB
256-dim: 12M × 256 × 4 = 12.29 GB
768-dim: 12M × 768 × 4 = 36.86 GB
total = 52.2 GB, which is more than the single 768-dim index you started with

You wanted to save memory and you spent 42% more, because the small indexes did not replace the big one — the nightly job still needs it.

Training triples, and keeps tripling. Three runs is three times the GPU-hours, three sets of hyperparameters, three learning-rate schedules that each want their own tuning because a 64-dimensional bottleneck does not train like a 768-dimensional one. Worse, this cost recurs. Every time the base model improves — new data, new architecture, a bug fix in the tokenizer — you pay for three runs again, forever.

Evaluation triples, and the regressions do not correlate. Three quality dashboards. A change that helps the 768-dimensional model can hurt the 64-dimensional one, because a low-dimensional bottleneck has different failure modes: it must discard something, and which something it discards is a different optimization problem. You will spend real weeks on “the small model regressed and the big one did not.”

Re-embedding triples. Twelve million documents through an encoder at, say, 1,000 documents per second per GPU is 12,000 seconds — 3.3 GPU-hours per pass. Three passes is ten GPU-hours per model revision. That is affordable. What is not affordable is the coordination: three backfills, three cutover windows, three chances for a partially-migrated index to serve garbage.

And now the one that actually kills it. Three independently trained models produce three unrelated geometries. The 64-dimensional vector for ticket #4,201,882 and its 768-dimensional vector are not two views of one thing; they are two arbitrary points in two arbitrary spaces. A cosine of 0.83 in the small space means nothing in the big space. So you cannot use the cheap model to shortlist and the expensive model to re-rank — the shortlist's notion of “close” has no defined relationship to the re-ranker's. The three systems cannot cooperate. They can only be three systems.

That last point deserves to be stated as a design principle, because it is the one that will motivate everything from Chapter 6 onward: the whole reason to want a cheap representation is so that it can filter work for an expensive one. A cheap representation that cannot hand off to an expensive one is worth much less than it looks.

“But we use an ANN index” — why that does not rescue you

Everything above priced an exhaustive scan, and the immediate objection is that nobody scans exhaustively. True. It is also beside the point, and seeing exactly why is worth the derivation, because it isolates which resource dimension actually controls.

Take IVF, the standard inverted-file index: cluster the corpus into √N cells with k-means, and at query time compare the query to the cell centroids and search only the nearest nprobe cells.

cells = √12,000,000 = 3,464   documents per cell = 12,000,000/3,464 = 3,464
with nprobe = 32:  comparisons = 3,464 × 32 = 110,848  (plus 3,464 centroid comparisons)
versus 12,000,000 — a 108× reduction in comparisons

So the compute problem is solved: 110,848 × 768 = 85.1 MMAC, which runs in about two milliseconds. Latency, fixed. Now ask the question that matters.

How many vectors are still resident? All twelve million. Any cell might be probed by the next query, so every vector must be reachable at memory speed — you cannot page in a cell on demand and stay inside 20 ms. The index is still 36.9 GB. ANN reduces the number of comparisons you make; it does not reduce the number of vectors you must keep. And the bill, the sharding, the replication factor, the rebuild time, and the cold-start time are all functions of what you keep.

The same holds for HNSW, and slightly worse: the graph adds 32 neighbour ids per node at four bytes each, which is another 164 MB on top of the vectors it needs to reach.

What an ANN index fixesWhat it leaves untouched
Comparisons per query — 100× or moreResident bytes — unchanged, plus graph overhead
Query CPU timeThe RAM bill
Shard count, and therefore fan-out tail latency
Index build and rebuild time, which is dominated by distance computations at d dimensions
Cold-start time after a deploy: 36.9 GB has to come from somewhere

Read the right-hand column. Every line is proportional to d, and none of them is helped by a smarter search structure. That is why dimension keeps being the lever: it is the only knob that moves the resident footprint, and the resident footprint is what determines how many machines you buy.

The second obvious fix: just cut the vector short

Train one 768-dimensional model, and when you need 64 dimensions, take the first 64 numbers and throw the rest away. Storage problem solved, geometry problem solved — the 64 numbers are literally inside the 768.

This does not work. Not “works a bit worse” — on an ordinary embedding model, truncating 768 dimensions down to 64 typically drops retrieval quality toward the floor. Chapter 3 derives exactly why, with numbers, but the one-line version is worth having now: an ordinary training objective has no reason to put anything in particular in the first 64 coordinates. Information is spread democratically across all 768 because nothing in the loss ever asked otherwise. Cutting at 64 is like reading the first 8% of every word in a book.

The third obvious fix: PCA it afterwards

If the problem is that the coordinates are in a bad order, rotate them into a good order. Principal component analysis (PCA) finds the directions along which your embeddings vary the most, and lets you keep the top few. That is a real technique, it is cheap, and it does help — but it has three properties that make it an incomplete answer, and one that makes it sometimes catastrophic.

Property of post-hoc PCAConsequence
Fitted after training, on a sample of embeddingsThe encoder never learned to be truncatable. You are rearranging a fixed pile, not building a better pile
Requires a d×m projection matrix at query time768×64 = 49,152 extra MACs per query and per document. Small, but it is a new artifact to version, ship, and keep in sync with the index
Optimizes variance retainedVariance is not accuracy. The directions your data varies along are not necessarily the directions your task cares about
One projection per (model, corpus) pairChange the corpus, refit. The projection is a property of your data distribution, not of the model

The third row is the dangerous one, and Chapter 3 will make it concrete with a four-line worked example where PCA keeps exactly the wrong dimension and lands you at chance accuracy while a single well-chosen coordinate would have given you 99.9%.

What we actually want — write the wish down

Before meeting the method, state the requirement precisely enough that we could check whether it was met. We want a single trained encoder F producing z = F(x) ∈ Rd such that:

1. Prefixes are complete
For a chosen set of sizes M ⊆ {1,…,d}, the first m coordinates z1:m are, on their own, about as good as an embedding from a model whose native output size is m.
2. Slicing is free
Getting z1:64 from z is a pointer and a length. No matrix multiply, no decompression, no second artifact to ship.
3. One geometry
All the prefixes live in nested subspaces of one space, so a shortlist computed at 64 dimensions can be re-ranked at 768 dimensions and the two agree about what “close” means.
4. The big one is not sacrificed
Quality at d = 768 stays essentially where it was. We are adding an option, not paying a tax on the default.

Requirement 1 is the hard one. Requirements 2 and 3 come free once 1 holds, because a prefix is a slice and prefixes are nested. Requirement 4 is the empirical question the paper has to answer with experiments, and Chapter 7 reports how that went.

The three-budget ledger

The same three consumers, priced two ways. Flip between the three-independent-models plan and the one-matryoshka-model plan, and drag the corpus size. Watch total memory, per-query scan time, the number of encoders you have to keep alive, and — the row that matters most — whether the cheap tier can legally hand work to the expensive tier.

Corpus size 12M

Two things to notice while you drag. First, the three-model plan's total memory is always larger than the single big index, because the small indexes are additions, not replacements. Second, the “can shortlist hand off?” row is the only one that is not a number, and it is the one that changes the achievable design, not just the bill.

Why this problem got urgent in 2022, and unavoidable by 2024

Nothing above is new physics. Dimensionality reduction is older than deep learning. What changed is the ratio between two costs.

EraTypical embeddingTypical corpusWhere the cost lived
Word vectors (2013–2016)300-dim word2vec / GloVeA vocabulary — hundreds of thousands of itemsTraining. Storage was a rounding error: 400k × 300 × 4 = 480 MB
Sentence encoders (2018–2020)768-dim BERT-familyMillions of documentsEncoding throughput. Indexes were single-digit gigabytes
Retrieval-augmented systems (2021–)768 to 3072 dimsHundreds of millions to billions of chunksServing. The index is the largest, hottest, most replicated object in the system

Run the last row's arithmetic once and the urgency is obvious. One billion chunks at 3,072 dimensions in float32:

1,000,000,000 × 3,072 × 4 bytes = 12,288,000,000,000 bytes ≈ 12.3 TB

Twelve terabytes of hot vectors, replicated for availability, sharded across machines, rebuilt whenever the model changes. At this scale the dimension is not a hyperparameter that a research team picks and a serving team absorbs. It is a joint decision with a seven-figure annual consequence, and the two teams want different answers.

The claim to hold this paper to. Matryoshka Representation Learning says you can have all the answers at once. One encoder, one training run whose extra cost the authors describe as negligible, one index, and a dimension that becomes a runtime argument. The paper reports embeddings up to 14× smaller at matched accuracy on ImageNet-1K classification, and up to 14× real-world speed-ups on large-scale retrieval. By Chapter 6 you will be able to derive where a number like that comes from, and by Chapter 7 you will know which parts of it are free and which are marketing.

The units, defined once

Four quantities recur in every calculation in this lesson. Fixing them now means never having to re-derive a bill later.

QuantityFormulaAt N = 12M, d = 768
Index bytesN × d × (bytes per number)36.9 GB
Exhaustive scan MACs per queryN × d9.22 GMAC
Scan time, bandwidth-boundindex bytes ÷ memory bandwidth36.9 GB ÷ 50 GB/s = 738 ms
Scan time, compute-boundMACs ÷ MAC throughput9.22 ÷ 48 GMAC/s = 192 ms

Compare the last two rows: 738 ms against 192 ms. The scan is bandwidth-bound by a factor of nearly four, which is the single most important fact about vector search on a CPU and the reason this lesson keeps talking about bytes rather than operations. You are not waiting for arithmetic; you are waiting for memory. Every optimization that reduces operations without reducing bytes is aimed at the wrong resource — and every one that reduces bytes gets the operation reduction thrown in free.

It also explains why Chapter 8's quantization axis works as well as it does. Halving the bytes per number halves the bandwidth requirement exactly, whether or not the arithmetic gets faster, and on a bandwidth-bound workload the bandwidth is the clock.

Where 768 came from, and why that should bother you

One more observation before we start building, because it reframes the whole problem. Ask where your embedding dimension actually came from and the answer is almost always: it was inherited.

DimensionWhere it originatesWhat it was optimized for
300word2vec and GloVeAn empirical sweep on word-analogy tasks, in 2013
768BERT-base: 12 attention heads × 64 dimensions per headMasked language modelling. The 64 came from wanting a sensible per-head dimension for scaled dot-product attention
1024BERT-large: 16 heads × 64The same, one size up
1536 / 3072Frontier text embeddersInherited scaling of the same conventions

Read the 768 row twice. The most common embedding dimension in production retrieval is a consequence of an attention-head arithmetic decision made in 2018 for a model that was not a retrieval model, using an objective that was not a retrieval objective. It then propagated into essentially every vector database on earth because the pooled hidden state was the convenient thing to store.

Nobody ever measured that 768 was the right number to serve. It was the right number to train with, and it leaked. The serving question — how many numbers do I need to keep in order to rank documents well enough for this product — has a completely different answer, a different objective, and a different constraint set, and until 2022 there was no way to ask it separately. That is what makes “dimension as a runtime parameter” a genuine reframing rather than a convenience: it is the first time the training decision and the serving decision were allowed to be different decisions.

One more escape people try: store fewer documents

Completeness demands mentioning the fourth fix, because in a design review someone always proposes it. If N × d is too large, shrink N: deduplicate aggressively, drop old tickets, index only a summary of each document.

This is often good engineering and it is not a substitute, for one reason. Every reduction in N deletes information from the product — a ticket you dropped cannot be retrieved, ever, by anyone. A reduction in d deletes precision from the ranking, which degrades gracefully and can be recovered by a rerank stage. One is lossy about what exists; the other is lossy about how finely you can order it. Those are not the same kind of loss and they should not compete for the same budget.

Four stakeholders, one number, no agreement

It is worth naming why this is an organizational problem and not only a technical one, because that explains why the fix had to be a runtime parameter rather than a better default.

WhoWants d to beBecauseWhat they will do if overruled
The research teamAs large as trains stablyEvery benchmark improves monotonically with capacity, and benchmarks are how the work is judgedShip the big model and let someone else worry
The serving teamAs small as quality allowsThey own the memory bill, the p99, and the pagerQuietly add a PCA step nobody versions
The mobile teamSmaller stillThe index has to fit on a device with 4 GB of RAM shared with everything elseTrain their own tiny model, forking the geometry
The analytics teamMaximumThey run offline and want every drop of signal for clustering and dedupKeep a second full-fidelity copy of everything

Every one of those escape hatches creates a second geometry, and a second geometry is a permanent tax: two evaluation suites, two sets of thresholds, two backfills, and a guarantee that a similarity score computed in one place cannot be compared to one computed in another.

The reframing that makes the paper obvious in hindsight. The disagreement above is not resolvable by picking a better d, because the stakeholders are optimizing different objectives under different constraints and all of them are right. The only resolution is to stop making it one decision. Turn d from a property of the model into an argument of the query. Everything in the next nine chapters is the machinery for doing that without paying for it four times.

The same problem, three other systems

Support-ticket search is a convenient story, but the shape recurs everywhere embeddings are stored at scale, and it recurs with the same three-budget structure. Recognizing it in your own system is most of the work.

SystemWhat is embeddedThe cheap tierThe expensive tier
RecommenderEvery item and every userCandidate generation over the whole catalogue, tens of millisecondsRanking a few hundred candidates, with the full model
Face recognitionEvery enrolled identity in the galleryGallery pre-filter on device, where RAM is measured in megabytesVerification against the shortlist, where a false match is expensive
Code searchEvery function in a monorepoEditor autocomplete, sub-100 ms, must run locallyRepository-wide semantic search, server-side
DeduplicationEvery document in a training corpusBlocking — find plausible pairs among billionsExact comparison of the surviving pairs

Every row has the same skeleton: a cheap stage that must touch everything and an expensive stage that touches almost nothing. That is the structural reason a nested representation is worth more than a merely small one — the two stages have to agree about what “similar” means, or the first cannot be trusted to feed the second.

The deduplication row is the extreme case and the clearest illustration. Blocking a billion documents against each other is a quadratic problem you make tractable by hashing or clustering in a very low-dimensional space, and then confirming survivors at full fidelity. If the low-dimensional space and the full-fidelity space are unrelated, your blocking recall is unmeasurable and your training corpus quietly retains duplicates you thought you removed.

A note on what this lesson will and will not claim

MRL is a small idea. The core equation fits on one line, the implementation is roughly twenty lines of PyTorch, and it introduces no new layer type, no new loss family, no new optimizer. It is worth being clear up front that this is a feature, not a criticism — the interesting scientific content is not “is this technique clever” but “is this constraint free.”

Because that is the real question. You are about to ask a single representation to be simultaneously good at nine different sizes. Information theory says constraints cost something. The entire empirical contribution of the paper is measuring how much, and finding that the answer is close to zero for the largest size while being enormous for the smallest sizes. Chapter 1 explains why that asymmetry is plausible before Chapter 7 shows that it is real.

Where we are going

Chapters 1–2 — build the idea
What a nested representation is and why a prefix is the right structure → the objective in one line, walked symbol by symbol, with the gradient argument that makes it work
Chapters 3–5 — earn every claim
Derive from a noise model why ordinary embeddings collapse under truncation → price the training overhead exactly → compute an eight-dimensional example entirely by hand, gradients included
Chapters 6–8 — ship it
Derive the adaptive-retrieval cost model and why 128× in FLOPs is 14× on a clock → the results and the production APIs, with the caveats → compose with quantization into the modern serving stack
Chapter 9 — where it went
Nesting over depth and over network width → what is still genuinely open → the cheat sheet
You train three independent models — 64, 256, and 768 dimensions — on the same corpus. Why can you not use the 64-dim model to fetch 200 candidates and the 768-dim model to re-rank them?

Chapter 1: The Nested Doll

A matryoshka doll is not a container. That distinction is the whole idea, so let us be pedantic about it.

A container holds a thing. Open a lunchbox and you find a sandwich; the lunchbox itself is not food. A matryoshka is different: open the biggest doll and inside is a smaller doll that is itself a complete doll — painted, symmetric, standing upright on a shelf without apology. Open that one and there is another. Every level is a finished object. Nothing is packaging.

Now transfer that to a vector. An ordinary compressed embedding is a container: you have a small code, and to use it you decompress it back to the big one. A matryoshka embedding is the doll: the first 64 numbers of the 768-number vector are, by themselves, a finished 64-dimensional embedding. You do not decode them. You do not project them. You use them.

The property, stated once, precisely. Let z = F(x) ∈ Rd and write z1:m for its first m coordinates. For a chosen nesting set M ⊆ {1,…,d}, we want: for every m ∈ M, the vector z1:m performs the downstream task about as well as the best m-dimensional representation you could have trained on its own. Not “degrades gracefully”. Not “retains most of the variance”. As well as a dedicated model of that size.

What nesting is not — four things it gets confused with

Because the word “smaller” is doing a lot of work, it is worth ruling out four neighbours explicitly. Each is a real technique that produces a smaller thing, and each differs from nesting in a way that matters operationally.

Not thisWhat it actually doesThe distinguishing test
CompressionProduces a code you must decode before use“Can I compute a dot product directly on the small thing?” For a compressed code: no. For a prefix: yes
DistillationTrains a separate small model to imitate a big one“Is the small output a subset of the big one's bytes?” For distillation: no, it is a different model's output
Early exitStops the network partway and reads an intermediate layer“Did I save encoder compute?” For early exit: yes. For MRL: no — you ran the whole network and stored less
PruningRemoves weights or channels the model was not using“Did the model get structurally smaller?” For pruning: yes. For MRL: the model is byte-identical in size

The early-exit row is the one worth internalizing, because it names the boundary of what this paper can do for you. MRL does not make inference cheaper. You run the full encoder, produce the full 768 numbers, and then choose how many to keep. If your bottleneck is the encoder, this technique is aimed at the wrong resource — and Chapter 9 will point you at the one aimed at the right one.

Why a prefix, and not some other subset

You could imagine asking for a different structure — “any 64 of the 768 coordinates work”, or “here is a learned mask per budget”. Prefixes win for three specific engineering reasons, and it is worth being able to say all three.

One: slicing a prefix is genuinely free. A vector in memory is a base address and a length. The first m coordinates share the base address; you change the length. In NumPy z[:m] returns a view — no copy, no allocation. In C it is the same pointer. On disk you can mmap a 768-dimensional index and read only the first 64 floats of every row if you lay it out row-major, though in practice you store the truncated index separately so the reads are contiguous. An arbitrary subset, by contrast, requires a gather: 64 scattered loads instead of one 256-byte streaming read, which on modern hardware is roughly an order of magnitude worse.

Two: prefixes nest transitively. The first 8 coordinates are a prefix of the first 16, which are a prefix of the first 32. So the granularities form a chain: z1:8z1:16 ⊂ … ⊂ z1:768. That chain is exactly what lets you build the cascade in Chapter 6, where each stage refines the previous stage's shortlist using strictly more information than the stage before. An arbitrary-subset scheme has no chain; the 64-dim mask and the 256-dim mask might not even overlap.

Three: a prefix induces an ordering, and an ordering is exploitable. If the first coordinates matter most, then coordinate index becomes a proxy for importance. You get, for free, an answer to “which parts of this embedding should I keep if I can only keep some?” — and, as Chapter 8 shows, that same ordering tells a quantizer where to spend its bits.

SchemeCost to extract a small versionNested?Extra artifact to ship
Prefix (matryoshka)Zero — pointer + lengthYes, a full chainNone
Learned mask per budgetA gather — scattered memory readsNot guaranteedOne mask per budget
PCA / SVD projectiond × m MACs per vectorYes, if you keep the top-m chainA d×d rotation matrix, refit per corpus
Learned autoencoder headA forward pass through an MLPNoOne decoder per budget
Separate small modelA full second encoder passNo — unrelated geometryA whole model

Read the “Nested?” column. Only two rows say yes, and one of them costs a matrix multiply and a corpus-specific artifact. That is the design space, and it is small.

What “free” means at the byte level

“Slicing is free” is the sort of claim that deserves one paragraph of literalism, because the whole deployment story rests on it.

A batch of embeddings in memory is one contiguous block. For B documents at d = 768 float32, the block is B × 3,072 bytes, laid out row-major: document 0's 768 numbers, then document 1's, and so on. Taking a prefix means reading the first 256 float32s of each row and skipping the rest.

what a prefix costs, three ways# 1. In-memory view - no copy at all
Z = np.load("index.npy", mmap_mode="r")   # (12_000_000, 768) float32
P = Z[:, :256]                              # a VIEW. zero bytes allocated.
# but note: P is strided - each row skips 512 floats - so a scan over P
# still touches every cache line of Z. Views are free; SCANS are not.

# 2. Materialised prefix index - what you actually ship
np.ascontiguousarray(Z[:, :256]).tofile("index256.bin")
# 12.3 GB one-off write, then every scan reads 12.3 GB instead of 36.9 GB

# 3. Truncating a single query vector at request time
q256 = q[:256]; q256 /= np.linalg.norm(q256)   # ~1 microsecond
The honest version of “free”. The arithmetic of taking a prefix is free — no matrix multiply, no model, no fitted artifact. But if you want the memory-bandwidth saving, you must store the prefix index contiguously, because a strided read still drags the whole cache line through. So the real deployment is: one full-precision archive, plus one small contiguous shortlist index derived from it by a copy. That copy is a memcpy with a stride, not a model, and it can be regenerated from the archive at any time with no retraining. Compare that to PCA, where regenerating the small index requires the projection matrix you fitted last year on a corpus that has since changed.

Where does an ordinary embedding put its information?

Everywhere, in roughly equal amounts. And this is not an accident or a flaw — it is the correct answer to the question an ordinary loss asks.

Consider what a standard training objective sees. It gets one representation zR768 and one head WRL×768, computes Wz, and pays a loss. Now ask: does that loss have any preference about which coordinates carry the signal? Take any invertible matrix R and replace z with Rz and W with WR−1. The logits are identical:

(WR−1)(Rz) = W(R−1R)z = Wz

The loss cannot tell these apart. Every rotation of the representation space is exactly as good, so the training run wanders into whichever one the initialization and the optimizer happen to hand it. In the language of optimization, the objective has a large symmetry group — a set of transformations under which it is invariant — and any property you want that is not invariant under that group must be put into the objective explicitly, because the optimizer will not choose it for you.

The one-sentence diagnosis. Truncatability is not rotation-invariant — rotating a vector completely changes what its first 64 coordinates contain — but the standard loss is rotation-invariant. Therefore the standard loss cannot produce truncatable embeddings except by luck, and with 768 dimensions there is not enough luck in the universe. If you want a property the loss is blind to, you have to write it into the loss. That is the entire content of Chapter 2.

This also explains why the failure is so severe rather than merely mild. With information spread uniformly over 768 coordinates, keeping 64 of them keeps 64/768 = 8.3% of the coordinates. Chapter 3 shows that in the presence of noise this translates to keeping only √(64/768) = 28.9% of the signal-to-noise ratio, which for a hard task is the difference between working and not working.

What the nesting constraint costs

Nothing is free. If z1:8 has to be a good 8-dimensional embedding, then the full z is no longer allowed to be an arbitrary 768-dimensional embedding — you have removed a lot of the rotational freedom described above. So there ought to be a price at d = 768. Why might it be small?

Reason one: the constraint is a chain, not a set of conflicts. The nesting requirements are compatible with each other in a strong sense. Suppose you have found a solution good at m = 8. Now extend to m = 16 with a head whose extra columns are all zero. The logits at 16 are then identical to the logits at 8:

logits(16) = W:,1:16 z1:16 = W:,1:8 z1:8 + W:,9:16 z9:16
set the second block of columns to zero and the second term vanishes

So a solution that is good at every smaller granularity can always be extended to be at least as good at the next one. The family of constraints admits a monotone solution by construction. This is why we should expect quality to be non-decreasing in m rather than trading off between granularities, and it is why the objective in Chapter 2 is a plain sum rather than something adversarial.

Reason two: deep representations are enormously redundant. A ResNet50 penultimate layer has 2048 coordinates, but the set of images it actually sees does not fill 2048 dimensions. Empirically the intrinsic dimension — the smallest number of coordinates needed to parameterize the data manifold without loss — of such representations is far lower than the ambient dimension. If most of the 2048 numbers are describing directions the data never explores, then reorganizing so that the used directions come first costs almost nothing.

Reason three: the price falls where you can afford it. The constraint binds hardest on the small prefixes, because they are the ones being asked to do something they could not otherwise do. At m = 768 the constraint is nearly vacuous — you already have all the coordinates. So the tax lands on the granularity that was previously impossible, which is a strange kind of tax: you are being charged for a capability you did not have.

Where the information sits

A 64-coordinate embedding, drawn two ways. Each bar is one coordinate's contribution to the downstream decision. The curve underneath is the cumulative fraction of task-relevant signal captured by the first m coordinates. Drag the cut point and flip between an ordinary encoder and a matryoshka one. The bracket marks the prefix you would keep.

Cut at m = 8

Set the cut to 8 and compare. The ordinary encoder's cumulative curve is a straight line — every coordinate contributes the same, so keeping 8 of 64 captures 12.5% of the signal. The matryoshka curve is concave and steep at the left: the first 8 coordinates already capture most of what a linear decision needs. The bars are not sorted by a post-processing step. That shape is what the training objective produced.

The extendability argument, on actual numbers

The claim that a good solution at granularity m can always be extended to be at least as good at the next one deserves a concrete instance, because it is the reason to expect the quality curve to be monotone rather than a trade-off frontier.

Suppose a two-class problem with a representation whose first two coordinates already separate perfectly, and a head W whose first two columns are (1.0, 0.0) for class A and (−1.0, 0.0) for class B. On an example with z = [0.8, 0.3, anything, anything], granularity 2 gives logits (0.8, −0.8), a margin of 1.6.

Now extend to granularity 4 by appending two columns of zeros to each row:

u(4)A = 1.0(0.8) + 0.0(0.3) + 0.0·z3 + 0.0·z4 = 0.8  — identical
u(4)B = −0.8  — identical  ⇒  L4 = L2 exactly

So the granularity-4 term can always be made no worse than granularity 2 by zeroing the new columns, and optimization will only leave that solution if the extra columns strictly help. The constraint set therefore contains a monotone solution and the objective has no reason to prefer a non-monotone one.

Two useful consequences. First, if your trained model shows quality that decreases with m anywhere, that is a bug or undertraining, not a fundamental trade-off — a monotone solution provably exists. Second, this is why MRL-E's tied head is a mild constraint rather than a crippling one: the zero-extension argument works verbatim for column slices of a single W, since “set the new columns to zero” is available to a sliced head too.

What the constraint costs, in bits

A rough information-theoretic sanity check, because “the tax is small” should have some quantitative grounding beyond “the experiments say so.”

An unconstrained d-dimensional representation may be rotated freely. The set of rotations of Rd — the orthogonal group — has d(d − 1)/2 degrees of freedom. For d = 2048 that is 2,096,128 free parameters of orientation, every one of which the ordinary loss is blind to.

MRL spends some of that orientation freedom to satisfy the nesting constraints. How much? Roughly, forcing the first m coordinates to span the best m-dimensional task subspace fixes the orientation of that subspace — m(d − m) parameters — while leaving rotations within the prefix and within the tail untouched.

for m = 8, d = 2048:  8 × 2040 = 16,320 of 2,096,128 ≈ 0.78% of the orientation freedom

Nine nested constraints together fix a nested flag of subspaces, still a small fraction of the whole. And crucially none of this touches the representation's information content: rotations are invertible, so a rotated representation contains exactly what the original did. The tax is paid in orientation freedom, not in bits — which is a strong hint about why the cost at full width is close to zero, and it is the closest thing to an explanation the literature currently offers.

Choosing M — why exponential spacing

The nesting set M is a design choice. The paper uses exponentially spaced granularities: for a 2048-dimensional ResNet50 representation,

M = { 8, 16, 32, 64, 128, 256, 512, 1024, 2048 }  — nine granularities

Why exponential? Two independent arguments land on the same answer.

The quality argument. Representation quality is roughly logarithmic in dimension over the useful range: doubling from 8 to 16 buys about as much as doubling from 512 to 1024. If quality is roughly linear in log m, then sampling m on a log grid samples the quality axis uniformly. A linear grid would waste almost all of its granularities in the flat high-dimensional region where they are indistinguishable, and would have nothing to say in the steep low-dimensional region where every doubling matters.

The cost argument. Every granularity is one more classifier head and one more loss term. On a linear grid with stride 8 you would have 2048/8 = 256 granularities: 256 heads, 256 softmaxes, 256 backward passes through heads. On a log grid you have log2(2048/8) + 1 = 9. Chapter 4 prices this exactly, and the difference between 9 and 256 is the difference between “negligible” and “this doubles training cost.”

ModeldA natural M|M|
ResNet50 (vision)20488, 16, 32, 64, 128, 256, 512, 1024, 20489
ViT-B/16, BERT-base (768-dim)76812, 24, 48, 96, 192, 384, 7687
A 1024-dim sentence encoder102432, 64, 128, 256, 512, 10246
A 3072-dim frontier text embedder3072256, 512, 1024, 1536, 30725

Note the 768-dimensional row. 768 is not a power of two, so the chain is built by repeatedly halving from the top: 768, 384, 192, 96, 48, 24, 12. Everything still nests, because nesting only requires that the sizes be a chain of increasing integers — nothing about the arithmetic requires powers of two.

The metaphor, pushed until it breaks

Analogies teach by being right in one place, and they teach twice as much when you find where they go wrong. Four ways a matryoshka doll is not a matryoshka embedding:

The dollThe embeddingWhat the difference teaches
The inner dolls are separate physical objectsThe prefix is the same bytes as the start of the wholeThere is no extraction step at all. Not even a copy, if you accept a strided read
The outer doll is hollow — a shell around the nextThe tail coordinates are not empty; they carry the fine distinctionsThe big vector is not “the small one plus packaging.” The tail is doing real work, just less broadly useful work
Each inner doll is a scaled copy of the outerThe prefix is a coarser representation, not a smaller one of the same thingThe prefix answers different questions — broad ones. It is a change of resolution, not of size
The number of dolls is fixed at manufactureYou can cut between granularities, with a caveatM sets where the guarantee is; it does not set where the knife can go

What survives the demolition is exactly one claim, and it is the only one we need: every level is a complete, finished object. That is the whole metaphor, and it is worth keeping because the alternative mental models — compression, packaging, downsampling — all carry an implied decode step that does not exist here.

How many dolls? The |M| question

Choosing M has two independent parts: how far down it reaches, and how many granularities it contains. The first is a product decision — the smallest size any consumer will ask for. The second is a modelling one, and it has a clean answer.

The number of granularities is the number of distinguishable importance tiers your coordinates get. With |M| = 9 the multiplicity takes nine values, so the objective can distinguish nine levels of “how broadly useful should this coordinate be” across 2048 coordinates. Within a tier — coordinates 513 through 1024, say — the objective is completely indifferent to ordering, and the model will not produce one.

|M|Head cost (ResNet50)Importance tiersVerdict
1 (ordinary training)2.05M params1 — no orderingThe baseline
3, e.g. {64, 512, 2048}2.62M3 coarse tiersWorks, but you can only cut at three places
9, exponential4.09M9The paper's choice. Cheap, fine-grained
256, linear stride 8262M — ten times the backbone256Absurd. The head now dominates the model

The last row is the one that shows why exponential spacing is not a stylistic choice. Linear spacing over 2048 dimensions makes the classifier heads ten times larger than the network they sit on, which turns a 0.2% training overhead into a 10× one. Exponential spacing buys nine tiers for the price of two heads, because — as Chapter 2 will compute exactly — the sum of a doubling chain is less than twice its largest term.

What about dimensions that are not in M?

A fair worry: you trained with M = {12, 24, 48, 96, 192, 384, 768}, and now a service asks for 100 dimensions. Is z1:100 garbage?

It is not, and the reason is structural rather than lucky. The pressure the objective puts on coordinate j is a function of how many granularities include it, and that function is a staircase that decreases smoothly as j grows (Chapter 2 derives the staircase exactly). Coordinate 97 is included in every granularity from 192 upward, coordinate 100 in the same ones. Neighbouring coordinates get near-identical treatment, so the resulting importance ordering is smooth rather than blocky. Truncating between the trained granularities interpolates between their qualities; the paper reports that MRL remains accurate at intermediate dimensions it never explicitly optimized.

The practical rule. Trained granularities are guaranteed by the objective; intermediate ones are supported by the smoothness of the pressure staircase. If a specific size matters to your product — because it is exactly what fits in a cache line, or exactly what your ANN library wants — put it in M. It costs one more head. If it merely needs to be reasonable, slice it and measure.

Prior art, and the exact gap MRL fills

The idea of an information ordering is old. It is worth seeing the neighbours, because knowing what already existed sharpens what is new.

Prior ideaWhat it ordersChosen byWhy it is not enough here
Progressive JPEG / JPEG 2000Bytes of an encoded imageA hand-designed transform (DCT, wavelets)The ordering is fixed by the codec, not learned from a task. Nothing analogous exists for a learned representation
PCA / SVDDirections of a fixed datasetVariance, fitted post-hocChapter 3: variance is not discriminability, and post-hoc methods cannot change what the encoder made linearly available
Knowledge distillationNothing — it produces a separate small modelA teacher's outputsGives you a second, geometrically unrelated model. Exactly Chapter 0's problem
Slimmable / once-for-all networksChannels inside the networkMulti-width trainingClosest relative, and genuinely the same family of idea — but it targets inference FLOPs, not the shipped embedding, and does not give you nested vectors in one geometry
Ordered / nested dropoutUnits of an autoencoder codeRandomly truncating during trainingThe same instinct, applied to reconstruction rather than to a downstream task at supervised scale
MRLCoordinates of the shipped representationThe downstream task loss, at training time

Read the last column of the slimmable-networks row carefully, because it is the sharpest distinction. Slimmable networks make the model elastic; MRL makes the output elastic. In a retrieval system those are different bills: the model runs once per query while the output is stored once per document, and there are a billion documents and one query. Chapter 9 shows that the two ideas eventually merged.

The property, as a test you could actually write

A definition you can assert in code is a definition you understand. Here is the nesting property expressed as a check, which is also the shape of the acceptance test you should run before believing any claim of matryoshka-ness.

python — the nesting contract, as an assertiondef assert_matryoshka(model, dims, eval_fn, reference, tol=0.02):
    """
    model      : produces (N, d) embeddings
    dims       : the granularities claimed to be supported
    eval_fn    : (embeddings) -> a scalar quality metric
    reference  : {m: quality of a model TRAINED natively at m}
    """
    E = model.encode(eval_set)                  # (N, d) - ONE pass
    prev = -1.0
    for m in sorted(dims):
        q = eval_fn(l2(E[:, :m]))               # slice, THEN normalise

        # (1) each prefix is about as good as a dedicated m-dim model
        assert q >= reference[m] - tol, \
            f"prefix {m} scores {q:.4f}, native model gets {reference[m]:.4f}"

        # (2) quality never decreases with m - Chapter 1's extendability
        assert q >= prev - 1e-3, \
            f"quality fell from {prev:.4f} to {q:.4f} going up to m={m}"
        prev = q

        # (3) the prefix is literally a slice - no transform anywhere
        assert np.shares_memory(E[:, :m], E)

Three assertions, and each one is a different chapter of this lesson. The first is the definition. The second is the monotonicity that the extendability argument guarantees, and its failure means undertraining rather than a fundamental limit. The third is the one people forget to check, and it is the one that separates matryoshka from every neighbour in the table above: if a transform is being applied, you do not have a nested representation, you have a compression scheme wearing its clothes.

What a matryoshka embedding looks like, numerically

Abstractions are easier to hold once you have seen the numbers they describe. Here is the shape of the coordinate-magnitude profile for two 768-dimensional text embedders — illustrative values of the kind you get from the twenty-minute diagnostic in Chapter 3, not measurements from any specific model.

Coordinate blockOrdinary encoder: mean zj2Matryoshka encoder: mean zj2Ratio
1 – 640.001300.008106.2×
65 – 1280.001300.003402.6×
129 – 2560.001300.001501.2×
257 – 5120.001300.000620.5×
513 – 7680.001310.000310.2×
Sum over all 7681.0001.000

Both rows of totals are 1.000 because both vectors are unit-norm — the same amount of “stuff”, distributed completely differently. The ordinary column is flat to three decimal places, which is exactly what Chapter 1's rotation-invariance argument predicts: with nothing in the loss expressing a preference, 1/768 = 0.00130 per coordinate is what you get.

The matryoshka column falls by a factor of 26 from the first block to the last. And the first 64 coordinates — 8.3% of the vector — hold 64 × 0.0081 = 0.518, or 52% of the total squared magnitude. Check the second row too: coordinates 65 through 128 add 64 × 0.0034 = 0.218, so the first 128 hold 74%.

One honest caveat about this diagnostic. Magnitude is a proxy, not the thing. A coordinate could be large and useless, or small and decisive — nothing forces informativeness to track amplitude. What makes the proxy usable is that MRL's mechanism happens to produce both together: the early coordinates receive more gradient, which moves them further, which makes them both larger and more useful. So the profile is strong evidence of front-loading and weak proof of it. Use it as a thirty-second screen and settle the question with the recall test.

Two names for two things, so Chapter 2 reads cleanly

TermMeaningSymbol
GranularityOne of the sizes at which the representation must be usablem ∈ M
Nesting setThe chosen collection of granularitiesM
Prefix / sliceThe first m coordinates of z, as a vector in Rmz1:m
Granularity headThe linear map from an m-dimensional prefix to logitsW(m)RL×m
Relative importanceThe scalar weight on granularity m's loss termcm > 0
BackboneEverything that produces z — the part you actually care aboutF(·; θF)

One thing to fix in your head now, because it is the most common misreading of this paper: MRL is a training objective, not an architecture. There is no matryoshka layer. The network is whatever network you already had. What changes is the sum you minimize.

Why does an ordinary training objective have no incentive to make the first 64 coordinates of a 768-dim embedding especially informative?

Chapter 2: The Objective, Term by Term

Here is the entire method. One line. Read it once, do not try to understand it yet, and then we will take it apart symbol by symbol until nothing in it is mysterious.

min{W(m)}m∈M, θF   (1/N) ∑i=1N  ∑m∈M  cm · L ( W(m) · F(xi; θF)1:m ,  yi )

That is Matryoshka Representation Learning. There is nothing else. If you have written a training loop with a cross-entropy loss, you have written 90% of this already; the remaining 10% is a for loop and a slice.

Walking the equation, left to right

min over {W(m)}m∈M and θF. Two groups of parameters are being learned jointly: the backbone weights θF, and one linear head W(m) for each granularity. Both groups see gradients from the same loss. This matters: the backbone is not frozen and then fitted with nine heads afterwards. The nesting requirement reaches all the way back through the encoder, which is why the encoder learns to produce front-loaded vectors rather than merely being read front-loadedly.

(1/N) ∑i=1N. The ordinary average over a minibatch of N examples. Nothing special. Note that unlike a contrastive loss, there is no interaction between examples here — each example's loss is computed independently, so batch size is a throughput knob, not a modelling knob.

m∈M. This is the new sum, and it is the whole paper. Instead of one loss per example there are |M| losses per example, one for each granularity. Nine terms for the ResNet50 setup. Every one of them is a complete, ordinary classification loss.

F(xi; θF). The backbone applied to the input. Concretely for the paper's ImageNet setup: xi is an image tensor of shape (3, 224, 224); F is a ResNet50 up to and including global average pooling; the output zi = F(xi) has shape (2048,). For a text setup: xi is a sequence of token ids of shape (L,), F is BERT-base with mean or [CLS] pooling, and zi has shape (768,).

1:m. The slice. Take the first m entries of a (2048,) tensor and you have an (m,) tensor. In PyTorch this is z[:, :m] on a batched (N, 2048) tensor, giving (N, m). It allocates nothing and it is differentiable: the backward pass of a slice is “route the incoming gradient to the first m positions and put zeros in the rest.” No parameters. No cost worth measuring.

W(m) · (…). The granularity head. W(m)RL×m where L is the number of classes — 1000 for ImageNet-1K. Multiplying an (m,) prefix by an (L, m) matrix gives an (L,) vector of logits, the unnormalized scores that a softmax will turn into probabilities.

L( · , yi ). Multi-class softmax cross-entropy, exactly as you already know it: exponentiate the logits, normalize to a distribution, take the negative log of the probability assigned to the true class yi.

cm. A positive scalar setting how much granularity m matters relative to the others. The paper sets cm = 1 for all m — every granularity weighted equally. Chapter 4 examines whether that is a lazy choice or a considered one.

The one structural fact to carry forward. There are |M| loss terms but exactly one backbone forward pass. The nine granularities all read the same z; they differ only in how much of it they read. So the expensive part of training — the convolutions, the attention, the backward pass through 25 million parameters — happens once, not nine times. Everything that is duplicated is a matrix of size L×m, which is nothing. Chapter 4 puts real numbers on “nothing”.

MRL and MRL-E: independent heads versus a sliced head

There are two variants, and the difference is one line of code with a genuinely different parameter story.

MRL keeps |M| separate matrices: W(8)R1000×8, W(16)R1000×16, and so on up to W(2048)R1000×2048. Each granularity gets its own freedom to decide how to read its prefix.

MRL-E (“efficient”) ties them all to slices of one matrix. Keep a single WRL×d and define

W(m) := W:, 1:m  — the first m columns of the one big head

Now the head is matryoshka too. And this is more elegant than it first looks. Because column j of W multiplies coordinate j of z, tying the heads means every granularity must agree on what coordinate j means. Under untied MRL, granularity 8 could interpret coordinate 3 one way and granularity 2048 another way; under MRL-E they are forced into a single shared reading.

Count the parameters for ImageNet-1K, L = 1000, d = 2048:

MRL heads:  1000 × (8+16+32+64+128+256+512+1024+2048) = 1000 × 4088 = 4,088,000
MRL-E head:  1000 × 2048 = 2,048,000
ordinary head:  1000 × 2048 = 2,048,000

Verify the inner sum by hand, because it is a pleasant one: 8+16 = 24, +32 = 56, +64 = 120, +128 = 248, +256 = 504, +512 = 1016, +1024 = 2040, +2048 = 4088. Notice the pattern — each partial sum is one less than double the next term, so the total is just under twice the largest granularity. The full geometric chain costs less than two of its largest element. That single fact is why nine granularities is cheap and why 256 linearly-spaced ones would not be.

MRLMRL-E
Head parameters (ImageNet-1K)4.09M2.05M — identical to a plain model
Freedom per granularityEach reads its prefix its own wayAll share one reading of each coordinate
Typical accuracySlightly higherSlightly lower, and the gap widens at the smallest granularities
Use it whenThe head is small relative to the backbone — almost alwaysL is huge (extreme classification, millions of classes) so the head dominates

The paper reports MRL-E as slightly weaker than MRL, which is the expected direction: tying weights is a constraint, and constraints cost accuracy. The reason MRL-E exists at all is the extreme-classification regime, where L can be in the millions and 4.09M × (L/1000) stops being a rounding error.

Now the gradient — where the magic actually is

Everything so far has been bookkeeping. This section is the mechanism, and it is a genuinely beautiful one because it is not a mechanism at all in the usual sense: nobody wrote a sorting step, an orthogonality penalty, or an importance regularizer. The ordering falls out of a counting argument.

Ask: which loss terms does coordinate j of z appear in?

Coordinate j appears in the prefix z1:m if and only if mj. So the total gradient arriving at coordinate j is

∂Ltotal / ∂zj  =  ∑m ∈ M,  m ≥ j   cm · ∂Lm / ∂zj

Read that sum's index set. For j = 1, every granularity in M satisfies m ≥ 1, so coordinate 1 receives gradient from all nine loss terms. For j = 2000, only m = 2048 qualifies, so coordinate 2000 receives gradient from one.

Define the multiplicity of coordinate j as the number of granularities that see it: μ(j) = |{ m ∈ M : mj }|. For the ResNet50 nesting set:

Coordinate rangeWidthMultiplicity μSees granularities
1 – 889all of them
9 – 168816 and above
17 – 3216732 and above
33 – 6432664 and above
65 – 128645128 and above
129 – 2561284256 and above
257 – 5122563512, 1024, 2048
513 – 102451221024, 2048
1025 – 2048102412048 only

Check the widths sum correctly: 8 + 8 + 16 + 32 + 64 + 128 + 256 + 512 + 1024 = 2048. Good.

This staircase is the whole mechanism. Coordinate 1 is under nine simultaneous, independent demands to carry useful signal. Coordinate 2000 is under one. Gradient descent is an averaging machine: a coordinate pulled by nine gradients toward “be broadly discriminative” will end up broadly discriminative, while a coordinate pulled by one gradient is free to specialize on whatever residual the others left. Nobody instructed the network to sort its coordinates by importance. The sort is an emergent consequence of an index-set inequality: mj.

There is a second, subtler effect hiding in the same sum, and it is worth spelling out because it explains why the small prefixes get good rather than merely getting attention. The nine terms are not nine copies of the same demand. The m = 8 term demands that eight numbers alone separate 1000 classes — a brutal requirement that can only be satisfied by extremely coarse, high-level structure. The m = 2048 term permits fine, specialized distinctions. So the early coordinates are being asked for coarse information by construction, not merely for more information. The representation ends up ordered coarse-to-fine, which is exactly the ordering you want for a cascade.

The per-coordinate gradient, written out

Let us make it fully concrete for softmax cross-entropy, because the shape of the expression explains the previous paragraph.

For a single granularity m, write the logits u(m) = W(m)z1:mRL and the softmax probabilities p(m) = softmax(u(m)). The classic result for cross-entropy with true class y:

∂Lm / ∂uk(m) = pk(m) − [k = y]

where [k = y] is 1 for the true class and 0 otherwise. Chain that through the head:

∂Lm / ∂zj = ∑k=1L ( pk(m) − [k = y] ) · W(m)k,j    (valid only when j ≤ m)

And the total, summing over the granularities that contain j:

∂Ltotal / ∂zj = ∑m ≥ j cmk ( pk(m) − [k = y] ) W(m)k,j

Now the important observation. The error signal (p(m) − one-hot) is different for every m, because each granularity has its own predictions and therefore its own residual. Granularity 8, working with almost no capacity, will still be badly wrong about fine-grained classes; its residual is dominated by coarse confusions. Granularity 2048 has nearly solved the coarse problem and its residual is dominated by fine confusions. So coordinate 1 receives a gradient that is mostly the coarse residual repeated nine times, while coordinate 2000 receives only the fine residual once.

That is the coarse-to-fine ordering, derived. Not asserted, not hoped for — it is a direct consequence of summing residuals from classifiers of different capacity.

SHOWCASE — the objective, walked

Left: the embedding z with the active prefix highlighted. Middle: the head matrix, with the slice that granularity m uses. Right: the |M| loss terms. Step through the granularities to watch which coordinates each term touches, then switch to the multiplicity view to see the staircase that results. Toggle granularities on and off in the nesting set and watch the staircase reshape.

View:
In M:

Turn off the 8 and 16 granularities and watch the staircase's tall left step disappear: with M starting at 32, coordinates 1 through 32 all get identical multiplicity, and the model has no reason to order within that block. That is the design knob — M determines the resolution of the importance ordering, and below the smallest granularity there is no ordering at all.

Why a sum? The combiners that were not chosen

The |M| losses have to be combined into one scalar, and “add them” is such a natural move that it slips past unexamined. It should not, because the alternatives all have advocates and each fails in an instructive way.

CombinerWhat it optimizesWhy not
m cm Lm (chosen)Average performance across granularities
maxm LmThe worst granularity — a minimax objectiveThe worst is always the smallest, so the gradient sees only m = 8 for most of training. The tail coordinates get no supervision at all and the full-dimension quality collapses
Ld + λ · penalty on prefix errorFull dimension first, nesting as a constraintEquivalent to the sum with a particular c profile, but framed so that λ needs tuning. The sum is the same thing with fewer knobs
Sample one m per step at randomThe same expectation, one term at a timeA valid stochastic estimator of the sum, and cheaper if the heads were expensive. They are not (0.2% of a forward pass), so you would be adding gradient variance to save nothing
Curriculum: large m first, then smallNesting as a fine-tuning stagePlausible, and largely untested. The risk is that a representation optimized for 2048 dimensions is in a basin far from any good nested solution, so the second phase has to undo the first

The random-sampling row is worth a second look because it clarifies what the sum is doing. Sampling one granularity per step gives the same objective in expectation but a noisier gradient. Summing all of them is variance reduction by exhaustive enumeration — affordable precisely because |M| is small and the heads are cheap. If someone proposed matryoshka over 256 linearly spaced granularities, sampling would suddenly be the right call.

How the gradient actually gets there: the slice's backward pass

One more piece of concreteness, because “autograd handles it” hides the exact thing we care about. A slice is a linear operation, so it has a transpose, and the transpose is what the backward pass applies.

forward:  s = Sm z   where Sm = [ Im   0 ] ∈ Rm×d
backward:  ∂L/∂z += SmT (∂L/∂s) = [ ∂L/∂s ; 0d−m ]

In words: the incoming gradient of length m is written into the first m slots and zeros are written everywhere else. Do that once per granularity and accumulate, and the buffer at z ends up holding exactly the multiplicity sum:

the gradient buffer at z, filled granularity by granularityd = 16, M = [2, 4, 8, 16]

after L2  backward:  [ g g . . . . . . . . . . . . . . ]
after L4  backward:  [ g g g g . . . . . . . . . . . . ]
after L8  backward:  [ g g g g g g g g . . . . . . . . ]
after L16 backward:  [ g g g g g g g g g g g g g g g g ]
                       ─────────────────────── accumulated ───────
number of writes:      4 4 3 3 2 2 2 2 1 1 1 1 1 1 1 1
                       ^                             ^
                    coordinate 1                 coordinate 16
                    written 4 times              written once

That bottom row is the multiplicity staircase again, now visible as literal accumulation counts in a gradient buffer. There is no cleverness anywhere in the implementation — there is a += that happens more often on the left than on the right.

Real shapes, three real models

SettingBackbone output zMHeadsLoss
ResNet50 / ImageNet-1K(N, 2048) after global average pool8 … 2048, 9 sizes9 × Linear(m, 1000)Softmax cross-entropy × 9
BERT-base sentence embedder(N, 768) after mean pooling12 … 768, 7 sizesnone — normalize each prefixMultiple-negatives ranking × 7
CLIP-style dual encoder(N, 1024) image and (N, 1024) text32 … 1024, 6 sizesnone — normalize each prefix, both towersSymmetric InfoNCE × 6

Rows two and three have no heads at all, which is worth pausing on. In a contrastive setting the “head” that reads the prefix is the dot product itself, and a dot product has no parameters. So the entire matryoshka modification collapses to: slice, normalize, compute the similarity matrix, sum the losses. There is nothing to add to the model — only to the loop.

The implementation, in full

Twenty lines. Every shape annotated. This is not pseudocode; it is the whole thing.

python — pytorch, the entire methodimport torch, torch.nn as nn, torch.nn.functional as F

class MatryoshkaHead(nn.Module):
    """One linear head per granularity. MRL (untied)."""
    def __init__(self, dims, n_classes):
        super().__init__()
        self.dims  = dims                       # e.g. [8,16,32,...,2048]
        self.heads = nn.ModuleList([nn.Linear(m, n_classes, bias=False)
                                    for m in dims])

    def forward(self, z):                    # z: (N, d)
        return [h(z[:, :m]) for h, m in zip(self.heads, self.dims)]
        #      ^ z[:, :m] is a VIEW - no copy, no allocation
        # returns |M| tensors, each (N, n_classes)

class MatryoshkaHeadE(nn.Module):
    """MRL-E: one weight matrix, sliced by columns."""
    def __init__(self, dims, n_classes, d):
        super().__init__()
        self.dims = dims
        self.W    = nn.Parameter(torch.empty(n_classes, d))   # (L, d)
        nn.init.trunc_normal_(self.W, std=0.02)

    def forward(self, z):
        return [z[:, :m] @ self.W[:, :m].T for m in self.dims]
        #       (N,m) @ (m,L) -> (N,L)   the SAME columns, every time

def mrl_loss(logits_list, y, c=None):
    """logits_list: |M| tensors of shape (N, L). y: (N,) int64."""
    if c is None: c = [1.0] * len(logits_list)      # the paper's choice
    return sum(cm * F.cross_entropy(u, y)
               for cm, u in zip(c, logits_list))

# --- the training step is otherwise completely unchanged ---
z      = backbone(x)                 # (N, 2048)  ONE forward pass
logits = head(z)                     # list of 9 tensors, each (N, 1000)
loss   = mrl_loss(logits, y)
loss.backward()                     # gradients merge at z automatically
opt.step()

Two lines deserve a second look. z[:, :m] is a view, so the nine “different inputs” are nine windows onto one tensor — autograd handles the merge at z without you writing anything, and the merge is the multiplicity sum from three sections ago. And sum(...) over the nine cross-entropies builds a single scalar, so there is exactly one backward(): nine loss terms, one backward pass, one optimizer step.

Three sanity checks on the equation itself

Before trusting any objective, poke it at its boundaries. Three degenerate cases, each of which should give an answer you can predict.

What if M = {d} only? The sum has one term and the equation collapses to ordinary training. Every coordinate has multiplicity 1, no ordering pressure exists, and you have exactly the model you had before. Good — the objective contains its own baseline as a special case, which is the first thing any generalization should do.

What if every cm = 0 except the smallest? Then only z1:8 is supervised. The remaining 2040 coordinates receive no gradient at all through the heads, so they drift under weight decay toward zero and the model is effectively an 8-dimensional encoder padded with noise. Predictable, and a useful reminder that the large granularity is not a free rider — it is the term keeping the tail meaningful.

What if M = {1, 2, 3, …, d}, every integer? Multiplicity becomes exactly d − j + 1 — a perfectly linear ramp, and the strongest possible ordering pressure. Also 2048 heads totalling L × 2,098,176 parameters, which for L = 1000 is 2.1 billion — eighty times the backbone. The objective does not break; the budget does. This is the cleanest way to see that exponential spacing is a compute decision rather than a modelling one.

MMultiplicity of coordinate 1Head parameters (L = 1000)Behaviour
{2048}12.05MOrdinary training
{8, 16, …, 2048}94.09MThe paper
{1, 2, …, 2048}20482,098MCorrect and unaffordable

Check the last row's arithmetic: the sum of 1 through 2048 is 2048 × 2049 / 2 = 2,098,176, times 1000 classes. The geometric chain summed to 4,088; the arithmetic chain sums to 2,098,176 — five hundred times more. That single comparison is the entire justification for exponential spacing, and it takes one line of Gauss to see.

Adapting the objective to a non-classification task

The equation above uses classification cross-entropy because that is what the paper trains on, but nothing about the structure depends on it. Replace L with any per-example loss and you have a matryoshka version of that method.

Original objectiveMatryoshka versionWhat changes
Softmax cross-entropy over L classesm cm CE(W(m)z1:m, y)|M| heads. The paper's setting
InfoNCE / contrastive (CLIP-style)m cm InfoNCE(z1:m, z'1:m)No heads at all — normalize each prefix separately and compute the similarity matrix at each m
Triplet / multiple-negatives rankingm cm Triplet(a1:m, p1:m, n1:m)Same, per granularity. This is how modern sentence embedders do it
Regression / metric learningm cm ‖g(m)(z1:m) − t‖2|M| small regression heads

The contrastive row is the practically important one, because every production text embedder in Chapter 7 is trained contrastively, not with a classification head. Note the crucial detail hidden in it: each prefix must be L2-normalized on its own. If you normalize the full 768-dimensional vector and then truncate, the resulting 64-dimensional vector does not have unit norm — and worse, its norm varies from example to example depending on how much mass sat in the tail. Cosine similarity computed on non-unit vectors silently becomes a length contest. Normalize after slicing, always.

Reading the objective as a constrained problem

One more angle on the same equation, because it clarifies what cm is rather than merely what it does.

What we actually want is: minimize the full-dimension loss, subject to every prefix also being good. Write that as a constrained program — minimize Ld subject to Lm ≤ τm for each m < d — and form the Lagrangian:

Ld + ∑m < d λm ( Lm − τm )

The τ terms are constants and vanish from the gradient, so this is exactly the MRL objective with cd = 1 and cm = λm. The relative importance weights are Lagrange multipliers on the nesting constraints.

Two things follow. First, setting every cm = 1 is a claim that all the constraints are equally binding — defensible, but a modelling assumption rather than a neutral default. Second, there is a principled procedure available when you need one: run the constrained problem properly, raising λm while constraint m is violated and lowering it while it has slack. That is dual ascent, and it is what you would reach for given a hard requirement like “quality at 64 dimensions must be within one point of a native 64-dimensional model.”

You wantSet cmInterpretation
Best average across granularitiesAll 1 — the paper's choiceEvery constraint weighted equally
Full-width quality protected absolutelycd large, the rest smallNesting as a soft preference, not a constraint
One served granularity to hit a targetDual ascent on that λA real constraint, solved as one
Terms on very different scales (multi-task)Divide each by its running meanRemove the unit mismatch before weighting

The last row matters the moment you apply MRL to a mixed objective — a retrieval loss plus a classification loss, say. Cross-entropies over the same L classes are commensurable and need nothing; a cross-entropy and a mean-squared error are not, and summing them raw quietly lets one term own the gradient.

The gotcha that bites everyone once. normalize(z)[:m] is not normalize(z[:m]). The first gives you a vector whose length depends on how much of the original norm happened to live in the first m coordinates — and since MRL deliberately front-loads magnitude, that length is systematically high and example-dependent. Documents whose signal concentrates early will score higher against every query, for no reason connected to relevance. Every production API that exposes a dimensions parameter tells you to re-normalize after truncation, and this is why.
In the MRL objective, why does coordinate 1 of z receive gradient from every loss term while coordinate 2000 receives gradient from only one?

Chapter 3: Why Plain Embeddings Do Not Truncate

Chapter 1 asserted that cutting an ordinary embedding short destroys it. That was an assertion. This chapter earns it, with a model simple enough to compute in your head and general enough that the conclusion transfers.

We will build the argument in four steps. First a noise model that makes “how much information survives truncation” a number. Then the exact scaling law, derived. Then two worked examples that turn the scaling law into accuracies. Then the post-hoc fixes — PCA, LDA, random projection — and precisely where each one runs out.

A model of what an embedding contains

Every useful embedding is a mixture of two things: signal, the part that varies with what you care about, and nuisance, everything else — sensor noise, phrasing accidents, lighting, the particular way this document happened to be worded.

Take the simplest possible instance. Two classes. An embedding in Rd. Class “+” has mean μ+ = +su and class “−” has mean μ = −su, where u is a unit vector giving the direction that separates the classes and s is how far apart they are along it. Around each mean, add independent Gaussian noise with standard deviation σ in every coordinate.

z = ± s u + ε,   ε ~ N(0, σ2 Id)

Two things about this model deserve a defence, because a model you do not believe teaches nothing.

Why isotropic noise? Because that is the neutral assumption — it says the nuisance directions have no preferred orientation, which is what you get when the encoder has no reason to prefer one. If the noise were anisotropic and you knew its shape, you would whiten it away first, and after whitening the noise is isotropic by construction. So this is the post-whitening picture.

Why a single signal direction? For two classes there genuinely is only one direction that matters — the difference of means — and everything orthogonal to it is nuisance by definition. With L classes the signal occupies a subspace of dimension at most L − 1, and the argument below repeats independently along each of its directions. Two classes is not a simplification that hides anything; it is one factor of the general case.

How much signal survives a cut

The best you can do with a full d-dimensional vector is to project it onto u and threshold. That projection gives a scalar: the signal contributes ±s, and the noise contributes a Gaussian of standard deviation σ (projecting isotropic noise onto any unit vector gives variance σ2). Define the standard measure of separability, the discriminability index:

d′ = (distance between class means, in noise units) = 2s / σ

And for two equally likely Gaussian classes with equal covariance, the optimal error rate is

error = Φ(−d′/2) = Φ(−s/σ)
Φ is the standard normal cumulative distribution — Φ(−1) = 0.1587, Φ(−2) = 0.0228, Φ(−3) = 0.00135

Now truncate to the first m coordinates. What happens to each piece?

The noise is unchanged per coordinate. Each retained coordinate still carries noise of standard deviation σ. You did not remove noise; you removed coordinates, and the ones you kept are as noisy as before.

The signal shrinks by however much of u lived in the prefix. The class means in the truncated space are ±su1:m, so the separation is 2s ‖u1:m‖. Therefore:

d′m = d′ · ‖u1:m
There is the entire chapter, in one factor. Truncation multiplies your discriminability by ‖u1:m‖ — the length of the signal direction's shadow on the prefix. Nothing else in the problem changes. So the only question that has ever mattered is: how much of the signal direction lives in the first m coordinates? And that is a property of the encoder's chosen basis, which Chapter 1 showed the ordinary loss does not care about.

Case A: the democratic embedding — what ordinary training gives you

An ordinary encoder has no reason to align u with any particular axis, so the signal direction is effectively a random unit vector — and a random unit vector in high dimensions spreads its mass almost perfectly evenly. Take the exactly-even case:

u = (1/√d) (1, 1, …, 1)  ⇒   ‖u1:m‖ = √(m · (1/d)) = √(m/d)

So for the democratic case:

d′m = d′ · √(m/d)

A square root, not a linear factor. That is worse than the naive intuition in one direction and better in another, so let us be careful: keeping 8.3% of the coordinates keeps 28.9% of the discriminability, not 8.3%. The square root is generous. It is also nowhere near generous enough.

Worked example 1. Take d = 768 and a task where the full embedding is comfortably good: s/σ = 3, so d′ = 6 and the error is Φ(−3) = 0.00135. That is 99.87% accuracy — a solved problem. Now cut.

Keep m of 768√(m/768)d′m = 6 · √(m/768)error = Φ(−d′m/2)accuracy
7681.0006.000Φ(−3.000) = 0.0013599.87%
3840.7074.243Φ(−2.121) = 0.017098.30%
1920.5003.000Φ(−1.500) = 0.066893.32%
960.3542.121Φ(−1.061) = 0.144485.56%
640.2891.732Φ(−0.866) = 0.193280.68%
240.1771.061Φ(−0.530) = 0.298070.20%
80.1020.612Φ(−0.306) = 0.379762.03%

Check one row by hand to be sure the table is not decoration. At m = 64: √(64/768) = √(1/12). Since 12 = 4 × 3, √(1/12) = 1/(2√3) = 1/3.4641 = 0.28868. Multiply by 6: d64 = 1.7321. Half of that is 0.8660, and Φ(−0.866) = 1 − 0.8068 = 0.1932. So 80.68% accuracy. ✓

Read the first and last rows together. A task that was 99.87% solved is 62.03% solved — barely better than a coin — when you keep 8 of 768 coordinates. And remember this is two classes. On a 1000-class problem the same √(m/d) shrinkage applies simultaneously to every one of the 999 signal directions, and the accuracy collapse is far steeper because you now need many separations to survive at once.

A sanity check: is the √ law believable?

A derived law you cannot check independently is a law you should not trust, so verify it from a completely different direction — energy.

The signal vector is su with total squared length s2, spread evenly over d coordinates, so each coordinate holds s2/d of signal energy and σ2 of noise energy. Keep m coordinates:

signal energy kept = m · s2/d = s2(m/d)  — a linear fraction
signal amplitude = √(signal energy) = s√(m/d)  — the √ appears here

Two things fall out, and both are worth holding separately. Energy is linear in m; amplitude is the square root of energy. Discriminability is an amplitude quantity — a distance in units of a standard deviation — so it inherits the square root. Anyone who tells you that keeping 8% of the coordinates keeps 8% of the quality is quoting an energy fraction where an amplitude one belongs.

Second check, from the noise side. The noise in the retained subspace has total energy 2, so noise amplitude grows as √m while signal amplitude also grows as √m — and their ratio would be constant if the signal were confined to the kept coordinates. It is not: the signal amplitude in the prefix grows as √m only because the signal is spread, while the best detector projects noise down to a single direction regardless. That asymmetry is where the whole effect lives, and it is why concentrating the signal (Case B) beats spreading it.

A number to remember instead of a formula. Keeping a quarter of the coordinates of a democratic embedding keeps half the discriminability. That one fact — quarter to half — regenerates the law any time you need it, and it makes clear why the loss feels so severe: 768 to 192 is already a halving, and nobody stops at 192.

Case B: the concentrated embedding — what we wish we had

Now suppose the encoder had put the entire signal direction into coordinate 1: u = e1 = (1, 0, 0, …, 0). Then for every m ≥ 1:

‖u1:m‖ = ‖(1, 0, …)‖ = 1  ⇒   d′m = d′

Truncation is free. Cut to one dimension and lose nothing, because everything you were going to use is already there. The other 767 coordinates were carrying nuisance, and throwing away nuisance costs nothing.

Two encoders. Identical information content — you can rotate one into the other and back, losing nothing. Wildly different truncation behaviour. The difference is not what the encoder knows. It is where the encoder put it.

And now you can state what MRL is, in one sentence. Real representations are not one signal direction; they are hundreds. Matryoshka Representation Learning is a training objective that pushes the most broadly useful of those directions toward the early coordinates and the most specialized ones toward the late coordinates, so that ‖u1:m‖ is large for the directions a small budget most needs. It is a rotation of the representation, chosen by the task loss instead of by chance.

Scaling the argument to many classes — why the collapse is steeper than the table

Two classes gave us one signal direction. With L classes there are up to L − 1 independent directions the representation must keep apart, and the √(m/d) shrinkage applies to each of them at once.

Think about what that does to accuracy. For a single pair of classes to be confused, one separation has to fail. For a 1000-way prediction to be correct, essentially all the separations involving the true class must hold simultaneously. Approximate the classifier as needing all 999 pairwise comparisons against the true class to come out right, each independently succeeding with probability 1 − e(m) where e is the pairwise error at that dimension:

accuracy ≈ (1 − e(m))999

The independence assumption is crude — real confusions are correlated, which makes the true accuracy higher than this — but the qualitative behaviour it predicts is right and dramatic. Take three pairwise error rates and see:

Pairwise error eTwo-class accuracy(1 − e)999
0.00135  (m = 768)99.87%0.9987999 = 0.2597 → 26.0%
0.010099.00%0.99999 = 0.0000434 → 0.004%
0.1932  (m = 64)80.68%essentially zero

Check the first: ln(0.9987) = −0.0013009; times 999 = −1.2996; e−1.2996 = 0.2726. (Close to the 0.2597 above; the difference is rounding in the logarithm — either way, the point stands.) A pairwise error rate that looks superb in a two-class problem is already fatal at 1000 classes under this crude model, which is exactly why real classifiers do far better than it predicts: confusions are highly correlated, most class pairs are trivially separable, and only a handful compete.

The transferable lesson, even though the model is crude. Accuracy on a many-way task is roughly exponential in the pairwise error, so a linear degradation in discriminability produces a super-linear collapse in accuracy. That is why truncation feels like falling off a cliff rather than sliding down a ramp: √(m/d) shrinks discriminability smoothly, and the exponent turns smooth into sudden. It is also why the numbers in the naive-truncation curve of the coming simulation drop so much faster than the √ law alone would suggest.

Then why not just rotate afterwards? Enter PCA

If the problem is orientation, rotate. Principal component analysis takes your embeddings, computes their covariance matrix, and finds the orthogonal directions along which they vary most, largest first. Keep the top m and you have the m-dimensional subspace that best reconstructs your data in a least-squares sense.

PCA genuinely helps. It is cheap, it is standard, and against naive truncation it is a large improvement. But it optimizes variance retained, and variance is not accuracy. Here is the counterexample, small enough to verify by inspection.

Worked example 2 — the two-coordinate trap. An embedding with d = 2. Coordinate 1 encodes a nuisance factor — say document length, or image brightness — that varies wildly and has nothing to do with the label. Coordinate 2 encodes the label cleanly but with small amplitude.

Coordinate 1 (“brightness”)Coordinate 2 (“is it a cat”)
Class + mean0+0.6
Class − mean0−0.6
Within-class std3.00.2
Total variance3.02 = 9.000.22 + 0.62 = 0.04 + 0.36 = 0.40

The total variance of coordinate 2 is the within-class variance plus the between-class variance, because the two class means sit at ±0.6 around zero: the mean-square of the means is 0.62 = 0.36. So 0.04 + 0.36 = 0.40.

PCA keeps the direction of largest variance. 9.00 versus 0.40 — a ratio of 22.5 to 1 — so PCA at m = 1 keeps coordinate 1 and discards coordinate 2. Now compute both outcomes:

PCA keeps coordinate 1:  both class means are 0 ⇒ d′ = 0 ⇒ error = Φ(0) = 0.5 ⇒ 50% accuracy — chance
Keeping coordinate 2:  d′ = 1.2/0.2 = 6 ⇒ error = Φ(−3) = 0.00135 ⇒ 99.87% accuracy

PCA did not underperform. PCA achieved exactly chance while a one-dimensional representation existed that achieved 99.87%. It made the worst possible choice available to it, and it did so while correctly maximizing the thing it was designed to maximize. The objective was wrong, not the algorithm.

This is not a contrived pathology. Nuisance factors with large amplitude are the normal condition of learned representations: document length, speaker identity, image exposure, JPEG artifacts, template boilerplate. They vary enormously and predict nothing.

Then why not use a supervised rotation? Enter LDA

Fair. Linear discriminant analysis finds the directions maximizing between-class scatter relative to within-class scatter — exactly the right objective for the example above, and it would pick coordinate 2 instantly. So why is post-hoc LDA not the answer?

ObstacleWhy it bites
Rank capThe between-class scatter matrix has rank at most L − 1. Two classes gives you exactly one useful direction; you cannot ask LDA for a 64-dimensional subspace on a binary task
Needs labels at reduction timeRetrieval corpora do not have class labels. “Which of my twelve million tickets is relevant to this query” has no label set to compute scatter over
Task-specific and corpus-specificThe projection is fitted for one task on one distribution. A general-purpose embedding serving fifteen downstream consumers would need fifteen projections, and then you are back to Chapter 0's incompatible geometries
It cannot create linear structureThe one that actually matters — see below

The deep limit of every post-hoc method. A rotation is invertible: it moves information around without adding or removing any. So if you ask “what is the best m-dimensional linear subspace of this fixed encoder's output for this specific linear task”, a supervised rotation gets you the optimum and MRL cannot beat it. That sounds like a knockout. It is not, and here is why.

The downstream reader is a linear probe, or a cosine similarity, or a nearest-neighbour lookup — all of them linear or near-linear operations. An ordinary encoder, trained with capacity to spare, has no reason to make coarse structure linearly available in few dimensions. It might encode “is this an animal” in a way that requires combining eighty coordinates nonlinearly, because at 768 dimensions with a linear head that is fine — the head has 768 coefficients to play with. No rotation can turn that into a property readable from 8 coordinates, because no rotation changes what is linearly readable in a subspace of that size.

MRL's m = 8 loss term is a linear head on eight coordinates. During training, that term is only satisfiable if the encoder makes coarse structure linearly separable in eight dimensions. So the encoder changes. It produces different vectors than it otherwise would have.

Post-hoc methods rearrange. MRL creates. PCA, LDA, SVD, and random projection all operate on a frozen encoder's output and are therefore bounded by what that output already makes linearly available in low dimensions. MRL puts the low-dimensional requirement inside the training objective, so the encoder is optimized to make it available. That is the difference between reorganizing a bookshelf and rewriting the books.

And random projection? The honest bound

One more contender, because it is the theoretically respectable one. The Johnson–Lindenstrauss lemma says a random linear map into m dimensions preserves all pairwise distances among n points to within a factor (1 ± ε), provided

m ≥ 8 ln(n) / ε2

Delightful theorem, and it has the enormous virtue of requiring no assumptions and no fitting. Now put your numbers in. Twelve million documents, 10% distortion tolerance:

m ≥ 8 · ln(12,000,000) / 0.12 = 8 · 16.30 / 0.01 = 13,040 dimensions

Verify the logarithm: ln(1.2 × 107) = ln(1.2) + 7 ln(10) = 0.182 + 16.118 = 16.30. ✓ So the guarantee demands seventeen times more dimensions than the 768 you started with.

This is not a defect of the lemma, it is a lesson about what guarantees cost. JL is distribution-free: it works for adversarially chosen points, so it must pay for the worst case. Real embeddings are not adversarial — they live near a low-dimensional manifold — and a method that exploits that structure will beat a method that refuses to assume it. Random projection is the price of assuming nothing, and it is a high price.

It also reveals a subtlety worth keeping. JL preserves distances. Retrieval only needs the ranking of distances, which is weaker. And a classification task only needs the decision boundary, which is weaker still. Every step down that ladder buys you dimensions, which is exactly why 64 well-chosen coordinates can work when the distance-preservation bound says 13,000.

SHOWCASE — the truncation-quality curve

Accuracy against retained dimensions, on a log axis, for five ways of getting a small embedding. Drag the cut point to read every curve at once, and toggle curves on and off. The dashed line is the reference: a separate model trained natively at that size, which is the bar MRL is trying to reach. Curves come from this chapter's noise model, calibrated so the full-dimension point matches a ResNet50-scale baseline — they reproduce the shape the paper reports rather than its exact table.

Cut at m = 64

Three features of that picture are the ones to carry away. The naive-truncation curve falls off a cliff below 128 dimensions, because √(m/d) is brutal down there. The SVD curve tracks the reference well at large m — where there is plenty of room and the top components really do carry the task — and separates below about 64, where the difference between “high variance” and “discriminative” stops being negligible. And the MRL curve hugs the reference across the whole range, which is the paper's central empirical claim.

One more contender: train a small autoencoder on top

A neural compressor. Take the frozen 768-dimensional embeddings, train a small MLP down to 64 dimensions and another back up, minimize reconstruction error. Nonlinear, learned, and it genuinely beats PCA at reconstruction. Four reasons it does not solve this problem:

It is a second model to ship. Encoder weights, decoder weights, versioned and kept in sync with the base model and with the index. When the base model updates, the autoencoder must be refit and every vector re-encoded — the same backfill treadmill as three separate models.

Its output is not a prefix. There is no chain. A 64-dimensional autoencoder code and a 256-dimensional one from a different autoencoder are unrelated, so no cascade is possible — back to Chapter 0's incompatible geometries.

It optimizes reconstruction, which is PCA's mistake with more parameters. A nonlinear reconstruction objective is still a reconstruction objective. It will happily spend capacity representing the brightness nuisance from Worked Example 2, because brightness is a large part of what you have to reconstruct.

It costs a forward pass per query and per document. Small, but nonzero, and it sits directly on the latency path that motivated all of this.

Nonlinear autoencoderMRL
Changes the base encoderNoYes — the operative difference
Output nestsNoYes
Extra artifacts2 per budget0
Query-time costOne MLP forwardNone
ObjectiveReconstructionThe downstream task

The one way truncation can genuinely help

This chapter has been an argument that truncation destroys information, so intellectual honesty requires the counter-current: there is a regime where fewer dimensions makes retrieval better, and it is worth understanding because it explains occasional surprising measurements.

In high dimensions, distances concentrate. For points drawn from a distribution in Rd, the spread of pairwise distances shrinks relative to their mean as d grows — the classic statement is that

(max distance − min distance) / min distance → 0  as d → ∞

which means the very notion of a “nearest” neighbour loses contrast: everything is roughly equally far from everything. Real embeddings are not drawn from a full-dimensional distribution — they live near a low-dimensional manifold, which is why nearest-neighbour search works at all — but a residue of the effect survives. Coordinates that carry no task signal contribute noise to every distance, flattening the gap between a genuine match and a near-miss.

So cutting a representation back toward its intrinsic dimension does two opposing things at once: it removes signal (the √(m/d) law) and it removes nuisance that was diluting contrast. For an ordinary embedding the first dominates overwhelmingly. For a matryoshka embedding, where the removed coordinates were specifically the least broadly useful ones, the two can be closer — and the practical consequence is that you will sometimes measure a truncated index retrieving slightly better than the full one on some query sets.

What to do when you see it. Do not celebrate and do not dismiss it. A small gain from truncation usually means the tail coordinates were carrying corpus-specific nuisance that your queries do not care about — which is real, and worth exploiting, and also fragile: it is a property of this corpus and these queries, not of the model. Measure it on a held-out query set before you make it the default, and re-measure after any corpus shift. Free lunches that come from noise cancellation have a habit of expiring.

The four baselines, and what each one is actually testing

BaselineNeedsCost at query timeWhat it is really measuring
Trained natively at m (“FF-m”)One full training run per mZero — the encoder outputs m dimsThe reference. What an m-dim representation can do when nothing constrains it
Naive truncationNothingZeroHow much the ordinary loss happened to front-load by accident. Answer: nothing
Post-hoc SVD / PCAA sample of embeddings to fit ond×m MACs, plus a matrix to shipWhether variance ordering is a good proxy for task importance
Random projectionA random seedd×m MACsThe distribution-free floor. Anything that loses to this has a bug
MRL prefixThe nesting set M at training timeZeroWhether the nesting constraint is cheap — the whole question

Notice which two rows have zero query cost and no shipped artifact: naive truncation and MRL. That column, not the accuracy column, is why MRL got adopted so fast. A technique whose deployment story is “use fewer bytes of the vector you already have” needs no infrastructure at all.

How to find out whether a model you did not train front-loads

You are handed an embedding API. The docs say nothing about matryoshka. Twenty minutes and a thousand documents will tell you whether truncation is safe, and the procedure is worth having in your fingers.

python — is this model truncatable? three testsimport numpy as np
E = embed(docs)                       # (10000, d) float32, unit norm
d = E.shape[1]

# TEST 1 - the magnitude profile. 30 seconds, no ground truth needed.
mag = (E ** 2).mean(0)                # (d,) mean squared magnitude per coordinate
print("front/back ratio:", mag[:d//8].mean() / mag[d//2:].mean())
# ~1.0  -> democratic, do not truncate
# >5    -> something front-loaded this deliberately

# TEST 2 - the operational one. Needs queries; ground truth is the FULL model.
def l2(X): return X / np.linalg.norm(X, axis=-1, keepdims=True)
truth = np.argsort(-(Q @ E.T), axis=1)[:, :10]      # (n_q, 10)
for m in [64, 128, 256, 512, d]:
    approx = np.argsort(-(l2(Q[:, :m]) @ l2(E[:, :m]).T), axis=1)[:, :10]
    rec = np.mean([len(set(a) & set(t)) / 10 for a, t in zip(approx, truth)])
    print(m, "recall@10 vs full:", round(rec, 3))

# TEST 3 - the control. Is truncation beating a post-hoc rotation?
U, S, Vt = np.linalg.svd(E - E.mean(0), full_matrices=False)
for m in [64, 256]:
    Esvd = l2((E - E.mean(0)) @ Vt[:m].T)
    # ... same recall computation. If SVD wins, the model is NOT matryoshka
    # and you should ship the projection matrix rather than a slice.

Test 1 is a screen, not a proof — magnitude is not the same as usefulness, and a model could in principle front-load information without front-loading magnitude. Test 2 is the real answer, and it is the number to put in a decision document. Test 3 is the control that stops you from congratulating yourself: if a plain SVD at 64 dimensions beats the prefix at 64 dimensions, the model was never matryoshka and you have learned something valuable in twenty minutes.

Front/back ratiorecall@10 at d/12Verdict
~1< 0.4Ordinary model. Do not truncate; fit an SVD or change models
> 5> 0.9Matryoshka-trained. Truncate freely inside the documented sizes
~1> 0.9Unusual and interesting — the task is easy enough that even a poor prefix suffices. Verify on harder queries before trusting it
> 5< 0.4Almost certainly a normalization bug in your test. Re-read Chapter 2
Under this chapter's noise model with the signal direction spread evenly across all d coordinates, truncating to m coordinates multiplies the discriminability d′ by which factor?

Chapter 4: Training Mechanics

The objective is one line and the code is twenty. What remains is the set of decisions you have to make to actually run it, and the arithmetic that justifies the paper's claim that the extra cost is negligible. “Negligible” is a word people use when they have not measured. Let us measure.

What the nine granularities actually cost

Take the paper's headline setting: ResNet50 backbone, ImageNet-1K, d = 2048, L = 1000, M = {8, 16, …, 2048}.

Parameters. A standard ResNet50 has 25,557,032 parameters, of which the final fully-connected layer contributes 2048 × 1000 + 1000 = 2,049,000 — about 8% of the model. Replace that single head with nine bias-free heads:

MRL heads = 1000 × (8 + 16 + 32 + 64 + 128 + 256 + 512 + 1024 + 2048) = 1000 × 4088 = 4,088,000
total = 25,557,032 − 2,049,000 + 4,088,000 = 27,596,032   (+7.98%)

Under MRL-E the single sliced head is 1000 × 2048 = 2,048,000, so the total is 25,556,032 — within a thousand parameters of the baseline. Zero, for practical purposes.

Compute. This is where the real answer lives, because parameters sit in memory while FLOPs cost time. A ResNet50 forward pass on a 224×224 image is about 4.1 GFLOP. The heads:

head FLOPs = 2 × 4,088,000 MAC = 8,176,000 FLOP = 0.00818 GFLOP
fraction of the backbone = 0.00818 / 4.1 = 0.20%

Two-tenths of one percent. And that is the total for all nine heads — the single baseline head was already 0.10% of the same figure, so the marginal cost is 0.10%. The backward pass roughly triples the forward cost for both, so the ratio holds.

Why it is this cheap, in one sentence. There is one backbone forward pass, not |M| of them. The convolutions — which are 99.9% of the arithmetic — run once and produce one z. The nine granularities differ only in how much of that one z they read, and reading is free. If MRL required nine encoder passes it would be a 9× training cost and nobody would use it.

Activation memory. The one thing that genuinely grows is the stored logits, since autograd must keep each granularity's output for the backward pass. At batch size 256:

9 granularities × 256 × 1000 classes × 4 bytes = 9,216,000 bytes = 9.22 MB

ResNet50 activations at batch 256 run to several gigabytes. Nine megabytes is a rounding error, and it is the only line item that scales with |M|. Even a hundred granularities would add only 102 MB — the reason to keep |M| small is the head parameters and the loop overhead, not activation memory.

ResourceBaselineMRL (9 granularities)MRL-EOverhead
Parameters25.56M27.60M25.56M+8.0% / 0%
Forward FLOPs4.1004 GFLOP4.1082 GFLOP4.1082 GFLOP+0.19%
Logit activations (N=256)1.02 MB9.22 MB9.22 MB+8.2 MB
Backbone forward passes111none
Optimizer states (Adam, 2× params)51.1M floats55.2M floats51.1M floats+8.0% / 0%

The same arithmetic for a text embedder, where it is even cheaper. BERT-base has about 110 million parameters and a forward pass on a 128-token sequence costs roughly 22 GFLOP. Suppose you train it contrastively with M = {12, 24, 48, 96, 192, 384, 768}. There are no heads at all — the similarity is a dot product. The only extra work is computing six additional similarity matrices:

extra cost = ∑m ∈ M, m < 768 2 · N2 · m  FLOP per batch
at N = 256:  2 × 65,536 × (12+24+48+96+192+384) = 131,072 × 756 = 99.1 MFLOP
backbone at N = 256:  256 × 22 GFLOP = 5,632 GFLOP
ratio = 0.0991 / 5,632 = 0.0018%

Under two thousandths of one percent. In the contrastive setting matryoshka training is, to measurement precision, free — which is exactly why every production text embedder now does it.

What does grow: the similarity matrices in memory. Each is (N, N) float32. At N = 256 that is 256 KB each, so seven of them is 1.8 MB. At the batch sizes contrastive training actually wants — 8,192 or more — each matrix is 268 MB and seven is 1.9 GB, which is a real number on a real GPU. The mitigation is the same one large-batch contrastive training already uses: compute the loss in chunks, or free each granularity's matrix before building the next.

The relative importance weights cm

The objective has a free scalar per granularity, and the paper sets them all to 1. Is that principled or lazy? Three arguments say it is principled, and one caveat says know what you are getting.

Argument one: the terms are already commensurable. All nine terms are softmax cross-entropies over the same 1000 classes. At initialization each sits near ln(1000) = 6.908, and each is bounded below by 0. There is no unit mismatch to correct — unlike, say, mixing a cross-entropy with a mean-squared reconstruction term, where the relative scale is arbitrary and must be tuned.

Argument two: there is already an implicit weighting, and it points the right way. This is the subtle one. Uniform cm does not mean uniform emphasis on the coordinates, because of the multiplicity staircase from Chapter 2. Coordinate 1 accumulates nine gradient contributions; coordinate 2000 accumulates one. So even with equal term weights, the early coordinates are under 9× the pressure. Uniform cm is not neutral — it is already strongly biased toward making small prefixes work, which is exactly the bias you want.

Argument three: it is one fewer hyperparameter, and hyperparameters are the enemy of adoption. A method whose recipe is “change your loss to a sum, keep everything else” will be tried by a hundred times more people than one requiring a nine-dimensional weight sweep.

The caveat. If you know at training time which granularity you will actually serve at, weighting it up is free and sensible. Concretely, in Chapter 5's toy the total gradient pressure on coordinate 1 is −0.901 with uniform weights; raising c2 from 1 to 2 takes it to −1.222, a 36% increase in how fast that coordinate moves. Whether that improves the served metric is an empirical question, but the knob is real and it is one line of code.

Gradient pressure per coordinate

The staircase from Chapter 2, now with the weights turned on. Each bar is the total weight ∑m ≥ j cm arriving at coordinate j. Drag the profile selector to see uniform weights, small-granularity-emphasis, and large-granularity-emphasis, and watch the shape of the ordering pressure change. The line on top is the same thing expressed as the fraction of total pressure that lands in the first m coordinates.

Tilt toward small m uniform

Push the tilt all the way toward small m and watch the cumulative curve become almost vertical at the left: nearly all the ordering pressure lands in the first sixteen coordinates and the tail is essentially unsupervised. Push it the other way and the staircase flattens toward the ordinary, order-blind objective. Uniform sits in a sensible middle, which is a reasonable defence of the paper's choice.

Five decisions that are not in the equation

1. Where in the network do you apply it? To the final representation — the vector you would actually ship. For a CNN that is the post-global-average-pool feature; for a Transformer it is the pooled sequence output; for a model with a projection head it is whatever the serving system stores. Applying MRL to an internal projection you throw away at inference accomplishes nothing, and this is the single most common way to waste a training run.

2. What kind of head? Linear. This is not a stylistic preference. The head is a stand-in for whatever will read the embedding downstream, and downstream readers are linear probes and cosine similarities. If you use a two-layer MLP head, the prefix becomes good only when read through that MLP — which nobody at serving time has. The MLP absorbs the ordering into its own weights and the shipped vector learns nothing.

3. Normalization. If your pipeline L2-normalizes embeddings — and every retrieval pipeline does — normalize each prefix separately, inside the loss:

python — the contrastive variant, correctlydef mrl_contrastive(z_a, z_b, dims, temp=0.05, c=None):
    """z_a, z_b: (N, d) paired encodings. Returns a scalar."""
    if c is None: c = [1.0] * len(dims)
    total = 0.0
    labels = torch.arange(z_a.shape[0], device=z_a.device)
    for cm, m in zip(c, dims):
        a = F.normalize(z_a[:, :m], dim=-1)     # slice FIRST
        b = F.normalize(z_b[:, :m], dim=-1)     # then normalize
        sim = a @ b.T / temp                    # (N, N)
        total = total + cm * 0.5 * (F.cross_entropy(sim, labels) +
                                       F.cross_entropy(sim.T, labels))
    return total

# WRONG, and it will look like it works for a while:
#   a = F.normalize(z_a, dim=-1)[:, :m]
# now |a| varies per example and long-tailed docs win every comparison

4. The nesting set. Exponential spacing down to the smallest size any consumer will plausibly ask for. Include sizes your infrastructure actually wants — if your ANN library is happiest at 128 or your cache line holds 64 float32s, put those numbers in M. Every extra granularity costs one head, which Chapter 4's arithmetic says is nothing.

5. Everything else stays. Same epochs, same learning-rate schedule, same augmentation, same optimizer, same weight decay. That is a genuine claim and a large part of the appeal: MRL is not a training recipe, it is a modification to one line of the loss.

Numerics, distribution, and the things that bite at scale

Four practical concerns that do not appear in the equation but appear in every real run.

Mixed precision. The nine cross-entropies are summed before the backward pass, so the loss scalar is roughly |M| times larger than a baseline loss — about 62 at initialization instead of 6.9. With float16 gradient scaling that is harmless (the scaler adapts), but if you have a hand-tuned static loss scale, divide it by |M| or you will start clipping. The safer habit is to log the mean of the per-granularity losses for monitoring while backpropagating the sum, so your dashboards remain comparable with the baseline run.

Gradient norm and clipping. The gradient at z is up to |M| times larger on the early coordinates, so the global gradient norm rises. If your recipe clips at a fixed global norm, that clip now binds more often and it binds asymmetrically — it scales down the whole gradient, which disproportionately throttles the tail coordinates that were already receiving the least. Either raise the clip threshold or clip per parameter group.

Weight decay on the heads. Under untied MRL you now have nine head matrices instead of one, so the total weight-decay penalty on the head grows about twofold. This is usually invisible against a 25-million-parameter backbone, but in a small model where the head is a large fraction of the parameters it will silently shrink your logit scale. Exclude the heads from decay, or scale their decay by 1/|M|.

Data-parallel training. Nothing special: the loss is a sum of per-example terms, so gradients all-reduce exactly as before. The one wrinkle is contrastive training, where negatives come from the batch — if you gather across ranks to build a large effective batch, you must gather once and reuse the gathered tensors for all |M| similarity matrices, not gather |M| times. Gathering per granularity multiplies your communication volume by |M| and turns a 0.002% compute overhead into a real slowdown.

KnobBaseline valueWith |M| = 9Why
Static loss scale (fp16)SS / 9The summed loss is ~9× larger
Gradient clip normC~2–3C, or per-groupEarly coordinates carry up to 9× the gradient
Head weight decayλλ/2, or exclude headsTwice the head parameters under untied MRL
All-gather in contrastive DDP1 per stepstill 1Gather once, reuse for every granularity
Learning rate, schedule, epochs, augmentationunchangedThe paper's whole point

Retrofitting an existing model

You already have a trained 768-dimensional encoder with an index built from it, and you would rather not retrain from scratch. Two options, in increasing order of cost and quality.

ApproachWhat you trainCostWhat you get
Frozen backbone + matryoshka headsOnly the heads; the encoder never movesMinutes to hoursNothing useful. A frozen z is a fixed pile — this is exactly the post-hoc regime of Chapter 3, and heads cannot rotate the representation
Short fine-tune of the whole encoder with the MRL lossBackbone + heads, a small fraction of the original scheduleHours to a dayMost of the benefit. The encoder is allowed to rotate, which is the operative degree of freedom
Full retrain with MRL from the startEverythingSame as your original runThe paper's numbers

The first row is worth dwelling on, because it is the intuitive thing to try and it does not work. Attaching a nine-headed classifier to a frozen encoder trains nine linear probes. Linear probes on a frozen representation are precisely the post-hoc rotation family from Chapter 3, subject to exactly the same ceiling. The encoder has to be allowed to move, because moving the encoder is the mechanism.

The second row is what most teams actually do, and there is a growing literature on doing it well — adaptor-style approaches that learn a small transformation plus a short fine-tune, trading a little quality for a lot of compute.

The training loop, with the logging that saves you a week

python — the whole loop, instrumentedDIMS = [8, 16, 32, 64, 128, 256, 512, 1024, 2048]
head  = MatryoshkaHead(DIMS, n_classes=1000).to(dev)
opt   = torch.optim.SGD(list(backbone.parameters()) + list(head.parameters()),
                        lr=0.1, momentum=0.9, weight_decay=1e-4)   # UNCHANGED

for x, y in loader:
    z          = backbone(x)                      # (N, 2048)  ONE pass
    logits     = head(z)                          # list of 9 x (N, 1000)
    per_m      = [F.cross_entropy(u, y) for u in logits]
    loss       = sum(per_m)                       # c_m = 1
    opt.zero_grad(); loss.backward(); opt.step()

    # --- the logging that matters ---
    for m, l in zip(DIMS, per_m):
        log(f"loss/m={m}", l.item())          # NINE scalars, never one
    # the front-loading smoke test, cheap enough to run every 500 steps
    with torch.no_grad():
        mag = (z ** 2).mean(0)                  # (2048,) mean squared magnitude
        log("mag/first8",   mag[:8].mean().item())
        log("mag/last1024", mag[1024:].mean().item())
        # the RATIO is the signal. Ordinary training holds it near 1.0;
        # a working MRL run drives it into the tens within a few epochs.

The last three lines are the highest-value diagnostic in the whole recipe, because they distinguish “my loss is going down” from “my representation is reorganizing.” Those are different claims and only the second one is what you are paying for. A run whose nine losses all fall beautifully while the magnitude ratio sits at 1.0 has a bug — almost always a slice applied to the wrong axis, or MRL applied to a projection that is discarded before z is stored.

Three recipe cards

Image classifierText embedderDual-encoder (CLIP-style)
Where z comes fromGlobal average poolMean-pooled tokensEach tower's projection output
M8 … 2048 (9)64, 128, 256, 512, 768 (5)32 … 1024 (6)
Head9 × Linear(m, L)NoneNone
Loss per granularitySoftmax cross-entropyMultiple-negatives rankingSymmetric InfoNCE
Normalize?NoYes — per prefix, after slicingYes — per prefix, both towers
Marginal training cost~0.2%~0.002%~0.01%
What to evaluate at each mTop-1, and 1-NN on the featuresMTEB retrieval subset, nDCG@10Zero-shot accuracy and recall@k, both directions
The mistake to avoidApplying MRL before the poolNormalizing before slicingNesting one tower and not the other

That last cell is a real failure mode. In a dual encoder the two towers must be nested at the same granularities, because a 64-dimensional image prefix will be compared against a 64-dimensional text prefix. Nest only the image side and the text side has no reason to put anything useful in its first 64 coordinates, so the comparison at m = 64 is a good vector against a random one.

When full-width quality actually drops: a decision procedure

The most alarming outcome is a run where the nine losses all fall but your production metric at m = d is half a point below the old model. The nesting constraint is binding. Work through this in order, because the causes are ranked by how often they turn out to be the real one.

1. Is it real?
Retrain the baseline with the same seed and the same schedule. Run-to-run variance on a large model is often two to four tenths of a point. Half a point may be noise, and chasing noise costs weeks.
↓ it reproduces
2. Is M too greedy?
Drop the smallest granularity and retrain. If the gap closes, you were asking eight dimensions to separate a class structure that genuinely needs more, and the resulting residual was distorting the whole representation.
↓ still there
3. Is it undertrained?
The nested objective has more constraints to satisfy, so it can need more steps to reach the same full-width quality even though the final solution is no worse. Extend the schedule by 20% and compare at matched final loss, not matched steps.
↓ still there
4. Reweight rather than remove
Set cd = 2 and leave the rest at 1. You keep every granularity available while telling the optimizer which one is the product. Chapter 2's Lagrangian view says this is the principled knob.
↓ the gap is genuinely irreducible
5. Price it honestly
You are being asked to trade a small full-width loss for a large low-dimension gain. That is a product decision, not a bug. Put both numbers in front of whoever owns the serving budget and let them choose.

Step 3 deserves emphasis because it is counter-intuitive. More constraints does not mean a worse optimum — Chapter 1's extendability argument says a monotone solution exists — but it can mean a harder optimization landscape, and “harder to reach” looks identical to “worse” if you stop at a fixed step count.

A natural extension: distilling across granularities

One modification appears repeatedly in follow-up work and is worth knowing about, because it is the first thing you would invent yourself. Right now the nine granularities are trained independently against the ground-truth label; they never talk to each other. But granularity 2048 is a better model than granularity 8 — so let the small one learn from the big one:

L = ∑m cm CE( u(m), y )  +  β ∑m < d KL( softmax(u(d)/T) ‖ softmax(u(m)/T) )

The second term is ordinary knowledge distillation, with the full-width granularity as the teacher and each prefix as a student, and the teacher is free because it is already being computed. The argument for it: a hard label says only “cat”, while the teacher's distribution says “cat, but somewhat lynx, definitely not trombone” — and that similarity structure is exactly the coarse information a small prefix has room to represent. The argument against: you must stop the gradient through the teacher, or the big granularity will learn to make itself easy to imitate rather than accurate.

Plain MRL+ cross-granularity distillation
What the small prefix imitatesThe one-hot labelThe full-width model's full distribution
Extra cost|M| − 1 KL terms on tensors you already have — near zero
New hyperparametersM, cmβ and the temperature T
The trapForgetting detach() on the teacher logits

Diagnostics: what to log, and what each signal means

Log every granularity's loss separately. One scalar is nine scalars in disguise and the disguise hides every interesting failure.

SymptomAlmost certainlyFix
L8 pinned near ln(L) = 6.91 while L2048 falls normallyThe smallest granularity is not learning at all — usually a slicing bug, or the head is reading the wrong axisAssert z[:, :8].shape == (N, 8) and print it once
All losses fall together, but the m=8 eval is at chanceYou are evaluating the wrong vector — probably normalizing before slicingSlice, then normalize. Check by asserting each prefix has unit norm
L2048 is clearly worse than your old baselineThe nesting constraint is binding harder than it should. Often M reaches too far down for the taskDrop the smallest granularity, or lower its cm
Losses look great, retrieval at m=64 does not improveTrained with a classification head but serving cosine similarity — the head absorbed the structureTrain with the objective you serve: contrastive in, contrastive out
Quality is non-monotone in m (128 beats 256)Undertrained, or too few granularities so the ordering inside a block is unconstrainedTrain longer; add granularities in the flat region
The one-line smoke test. After training, embed a thousand held-out examples and compute the average squared magnitude of each coordinate, then plot it against coordinate index. An MRL model gives a visibly decreasing curve; an ordinary model gives a flat one. It is not a proof — magnitude is not the same as usefulness — but it takes thirty seconds and it catches the “the loss was fine but the slicing was wrong” class of bug immediately.

What this does to your evaluation cost

One overhead nobody warns you about, and it is entirely self-inflicted. You now have nine granularities to evaluate instead of one. Done naively — re-embed the eval set once per granularity — your evaluation cost goes up ninefold, and evaluation is often already the slowest part of a research loop.

Done correctly it costs about twice one evaluation, and the reason is the same geometric-sum fact that made the heads cheap.

encode the eval set: once, at full width → E ∈ RN×2048
scoring at every granularity: ∑m∈M N · m = N × 4088
scoring at full width alone: N × 2048
ratio = 4088 / 2048 = 2.0×
python — evaluate every granularity for the price of twoE = encode(eval_set)             # (N, 2048)  ONCE. This is the expensive part.
Q = encode(queries)              # (n_q, 2048) ONCE.

results = {}
for m in DIMS:
    e = l2(E[:, :m])              # a view + one normalise. No encoder.
    q = l2(Q[:, :m])
    results[m] = ndcg_at_10(q @ e.T, qrels)

# The mistake that costs 9x:
#   for m in DIMS: E = encode(eval_set, dimensions=m)   # re-runs the model
# The prefix is a slice. There is nothing to re-encode. Ever.

The same reasoning applies to your production monitoring: instrument one embedding pass and derive every granularity's metric from it. If a dashboard is re-encoding per dimension, someone has misunderstood what a prefix is, and the fix is a two-line change that reclaims 89% of the job's runtime.

Reading the training curves

Nine loss curves on one chart is a lot of ink. Here is what the shapes mean, so you can glance rather than squint.

ShapeReading
Nine curves fanned out, ordered by m, all descendingHealthy. Smaller granularities plateau higher because they have less capacity. The vertical spread is the truncation cost, visible live
The fan narrows over trainingGood — the representation is reorganizing and the small granularities are catching up. Usually the slowest part of the run
Curves crossSuspicious. Under MRL-E a smaller granularity should never beat a larger one at convergence. Early crossing is noise; late crossing is a bug
The smallest curve is flat at ln(L)A slicing bug. It is learning nothing at all
All nine collapse onto one lineYou are somehow feeding all heads the same input. Check that z[:, :m] uses m and not a captured loop variable
The largest curve tracks your old baseline exactlyThe nesting tax at full width is near zero — the result the paper is claiming, reproduced on your own data

The last row is the one to watch for, because it is the whole bet. If L2048 overlays your baseline curve step for step, you are getting eight extra operating points for free, and you can stop worrying about whether the constraint is costing you something.

What you actually ship

A final piece of concreteness: the artifacts that leave the training job, because the short list is the argument for the whole method.

ArtifactBaseline modelMRL model
Encoder weightsYesYes — same file, same size
Classifier headsUsually discarded at servingAlso discarded — they were scaffolding for the objective
Projection matricesNoneNone
CodebooksNoneNone
Calibration statisticsNoneOnly if you quantize — and that is Chapter 8's artifact, not this one
DocumentationThe list of supported m, and “re-normalize after truncating”
Read the third and fourth rows again, because they are the deployment story in two words: nothing extra. The serving system receives the same encoder it received before. Everything MRL did happened during training and is now baked into which numbers come out in which order. There is no inference-time component to version, no artifact to keep in sync, no second thing that can be stale. That is why this technique crossed from paper to production faster than nearly anything else in representation learning, and it is worth noticing that the reason is not about quality at all.
Why does attaching matryoshka heads to a frozen pretrained encoder fail to produce truncatable embeddings?

Chapter 5: The 8-Dimensional Toy, By Hand

Every equation in Chapter 2 now gets computed with a pencil. Eight dimensions, three classes, three granularities. By the end of this chapter you will have produced a single scalar loss the way the optimizer does, and computed the gradient arriving at two different coordinates so you can see the multiplicity effect as a number rather than a claim.

The setup

Three classes: cat, dog, bird. Representation dimension d = 8. Nesting set M = {2, 4, 8}. We use MRL-E, so there is one head matrix WR3×8 and granularity m uses its first m columns.

One training example, an image of a cat, produces the representation:

z = [ 0.90, 0.40, 0.30, 0.20, 0.15, 0.10, 0.05, 0.05 ]  ∈ R8

Front-loaded, as an MRL-trained encoder should be. Coordinate 1 has magnitude 0.90; coordinate 8 has 0.05, eighteen times smaller.

The head, written as a table because you are about to read its columns as well as its rows:

j=1j=2j=3j=4j=5j=6j=7j=8
cat1.00.50.40.20.10.10.00.1
dog0.6−0.40.20.5−0.20.00.10.0
bird−0.50.3−0.20.10.3−0.10.2−0.1

True label y = cat. Relative importance cm = 1 for all three granularities, as in the paper.

Granularity 1: m = 2

The prefix is z1:2 = [0.90, 0.40], and the head slice is the first two columns. Three dot products:

ucat = (1.0)(0.90) + (0.5)(0.40) = 0.90 + 0.20 = 1.10
udog = (0.6)(0.90) + (−0.4)(0.40) = 0.54 − 0.16 = 0.38
ubird = (−0.5)(0.90) + (0.3)(0.40) = −0.45 + 0.12 = −0.33

Exponentiate:

e1.10 = 3.004166   e0.38 = 1.462285   e−0.33 = 0.718922
sum = 3.004166 + 1.462285 + 0.718922 = 5.185373

Normalize:

p(2) = [ 3.004166/5.185373 , 1.462285/5.185373 , 0.718922/5.185373 ]
     = [ 0.5794 , 0.2820 , 0.1386 ]

The loss is the negative log of the true class's probability:

L2 = −ln(0.5794) = 0.5458

Sanity check the magnitude before moving on. Guessing uniformly over three classes gives −ln(1/3) = ln 3 = 1.0986. We are at 0.5458, comfortably below the guessing baseline, so two coordinates already carry real information. Good — that is the whole promise of the small granularity, verified on the first computation.

Granularity 2: m = 4

Two more coordinates, z3 = 0.30 and z4 = 0.20, and two more columns. Because the head is sliced rather than retrained, the first two terms are identical to before — we can just add:

ucat = 1.10 + (0.4)(0.30) + (0.2)(0.20) = 1.10 + 0.12 + 0.04 = 1.26
udog = 0.38 + (0.2)(0.30) + (0.5)(0.20) = 0.38 + 0.06 + 0.10 = 0.54
ubird = −0.33 + (−0.2)(0.30) + (0.1)(0.20) = −0.33 − 0.06 + 0.02 = −0.37

That incremental structure is worth pausing on: under MRL-E, going from granularity m to the next one adds terms to the logits and never rewrites them. That is the mechanical reason the “can always be extended” argument from Chapter 1 works — and it is why a well-trained MRL-E model has quality monotone in m.

e1.26 = 3.525422   e0.54 = 1.716007   e−0.37 = 0.690734
sum = 5.932163
p(4) = [ 0.5943 , 0.2893 , 0.1164 ]
L4 = −ln(0.5943) = 0.5204

Granularity 3: m = 8

The last four coordinates are [0.15, 0.10, 0.05, 0.05] — small, by construction. Add their contributions:

ucat = 1.26 + (0.1)(0.15) + (0.1)(0.10) + (0.0)(0.05) + (0.1)(0.05)
     = 1.26 + 0.015 + 0.010 + 0 + 0.005 = 1.290
udog = 0.54 + (−0.2)(0.15) + (0.0)(0.10) + (0.1)(0.05) + (0.0)(0.05)
     = 0.54 − 0.030 + 0 + 0.005 + 0 = 0.515
ubird = −0.37 + (0.3)(0.15) + (−0.1)(0.10) + (0.2)(0.05) + (−0.1)(0.05)
     = −0.37 + 0.045 − 0.010 + 0.010 − 0.005 = −0.330
e1.290 = 3.632783   e0.515 = 1.673639   e−0.330 = 0.718922
sum = 6.025344
p(8) = [ 0.6029 , 0.2778 , 0.1193 ]
L8 = −ln(0.6029) = 0.5060

Notice how little the last four coordinates bought: 0.5204 → 0.5060, a gain of 0.0144, versus the 0.0254 that coordinates 3 and 4 bought and the enormous jump from the 1.0986 guessing baseline that coordinates 1 and 2 bought on their own. That decreasing-returns shape is what a front-loaded representation looks like from the inside.

An aside on how you would actually compute this

We exponentiated raw logits above because the numbers were small and it kept the arithmetic legible. Real code does not do that, and the reason is worth one paragraph because it is a bug you will otherwise write.

Logits in a trained network routinely reach 20 or 30. Then e30 is about 1013, and with a few more it overflows float32 entirely, producing inf, then inf/inf = NaN, then a silently poisoned training run. The fix is to subtract the maximum first, which changes nothing because softmax is shift-invariant:

softmax(u)k = euk / ∑i eui = euk−c / ∑i eui−c  for any c — take c = maxi ui

Verify on our own m = 8 numbers. The logits were (1.290, 0.515, −0.330) with maximum 1.290. Shift them to (0, −0.775, −1.620):

e0 = 1, e−0.775 = 0.460704, e−1.620 = 0.197898,  sum = 1.658602
pcat = 1/1.658602 = 0.602918  — the same 0.6029 we computed the naive way ✓

And the loss falls out even more directly, without ever forming a probability:

L = −ln py = logsumexp(u) − uy = [ 1.290 + ln(1.658602) ] − 1.290 = ln(1.658602) = 0.505987

Matching the 0.5060 from the long way round. The largest exponent is always exactly 1, so nothing can overflow, and F.cross_entropy in PyTorch does precisely this — which is why you should hand it logits rather than probabilities. Passing it a softmax output is the second most common bug in this territory, after normalizing before slicing.

The total objective

L = c2L2 + c4L4 + c8L8 = 0.5458 + 0.5204 + 0.5060 = 1.5722

That single number is what loss.backward() is called on. One scalar, three classifiers of three different capacities, one shared representation.

And immediately: what would the weighted variants give? With c = (0.5, 1, 1) — de-emphasizing the tiny granularity:

L = (0.5)(0.5458) + 0.5204 + 0.5060 = 0.2729 + 0.5204 + 0.5060 = 1.2993

With c = (2, 1, 1) — leaning hard on the smallest, because that is what you plan to serve:

L = (2)(0.5458) + 0.5204 + 0.5060 = 1.0916 + 0.5204 + 0.5060 = 2.1180

The absolute value is meaningless — scaling a loss scales nothing that matters. What matters is the ratio of the terms, because that sets which granularity's residual dominates the gradient. Which is where we go next.

The gradient, and the multiplicity effect as a number

Recall the cross-entropy gradient with respect to logits: ∂L/∂uk = pk − [k = y]. With y = cat (index 1), the three residual vectors are:

r(2) = (0.5794 − 1, 0.2820, 0.1386) = (−0.4206, 0.2820, 0.1386)
r(4) = (−0.4057, 0.2893, 0.1164)
r(8) = (−0.3971, 0.2778, 0.1193)

Every residual has the same structure — push the true class's logit up, push the others down — and its magnitude shrinks as the classifier gets more capacity and gets more of the answer right. Now chain into a coordinate. For coordinate j:

∂Lm/∂zj = ∑k r(m)k · Wk,j

Coordinate 1, whose column of W is (1.0, 0.6, −0.5). It appears in all three granularities, so all three residuals reach it:

from m=2: (−0.4206)(1.0) + (0.2820)(0.6) + (0.1386)(−0.5) = −0.4206 + 0.1692 − 0.0693 = −0.3207
from m=4: (−0.4057)(1.0) + (0.2893)(0.6) + (0.1164)(−0.5) = −0.4057 + 0.1736 − 0.0582 = −0.2903
from m=8: (−0.3971)(1.0) + (0.2778)(0.6) + (0.1193)(−0.5) = −0.3971 + 0.1667 − 0.0597 = −0.2901
total ∂L/∂z1 = −0.3207 − 0.2903 − 0.2901 = −0.9011

Coordinate 5, whose column is (0.1, −0.2, 0.3). It appears only in the m = 8 prefix, so only one residual reaches it:

from m=8: (−0.3971)(0.1) + (0.2778)(−0.2) + (0.1193)(0.3) = −0.0397 − 0.0556 + 0.0358 = −0.0595
total ∂L/∂z5 = −0.0595

Now the comparison that makes the mechanism visible. Coordinate 1 receives −0.9011. If MRL had not been used — if there were only the ordinary m = 8 loss — coordinate 1 would receive only its m = 8 contribution, −0.2901. So:

multiplicity amplification on coordinate 1 = 0.9011 / 0.2901 = 3.11×

Three granularities, roughly 3× the update speed on the first coordinates. Under SGD with learning rate η, coordinate 1 moves 3.11 times as far per step as it would under the ordinary objective, while coordinate 5 moves exactly as far as it always would. Repeat that asymmetry for tens of thousands of steps and the representation reorganizes: whatever is most broadly useful migrates to where the pressure is highest.

Nobody sorted anything. There is no ranking step, no importance score, no orthogonality penalty, no auxiliary network. There is a sum whose index set is mj, and the consequence of that inequality is a 3.11× pressure gradient across the coordinate index. Scale M from three granularities to nine and the amplification at the front becomes 9×. The ordering is an artifact of arithmetic.

One SGD step, so the reorganization is not hypothetical

Treat z as if it were directly updatable — it stands in for whatever the backbone would do — and take one step with learning rate η = 0.1. We have the two gradients we need plus coordinate 2's, computed the same way from column (0.5, −0.4, 0.3):

∂L/∂z2 = (−0.2815) + (−0.2837) + (−0.2739) = −0.8391
z1 ← 0.90 − 0.1(−0.9011) = 0.9901    z2 ← 0.40 − 0.1(−0.8391) = 0.4839
z5 ← 0.15 − 0.1(−0.0595) = 0.1560

Coordinates 1 and 2 each moved by about 0.09; coordinate 5 moved by 0.006 — fifteen times less. Now recompute the small granularity with the updated prefix:

ucat = 1.0(0.9901) + 0.5(0.4839) = 0.9901 + 0.2420 = 1.2321
udog = 0.6(0.9901) − 0.4(0.4839) = 0.5941 − 0.1936 = 0.4005
ubird = −0.5(0.9901) + 0.3(0.4839) = −0.4951 + 0.1452 = −0.3499
e1.2321 = 3.4283, e0.4005 = 1.4926, e−0.3499 = 0.7048, sum = 5.6256
pcat = 3.4283/5.6256 = 0.6094  ⇒   L2 = −ln(0.6094) = 0.4953

Down from 0.5458 in a single step — a 9.3% improvement, driven overwhelmingly by the two coordinates that were under triple pressure. Run that ten thousand times and the front-loading is not a tendency, it is the shape of the representation.

The head is matryoshka too

One more gradient, because it reveals that the ordering pressure applies to both sides of the product. For MRL-E the head is a single W, and column j of W is used by every granularity mj — the same index set. So:

∂Lm/∂Wk,j = r(m)k · zj  (for j ≤ m)  ⇒   ∂L/∂Wk,j = zj · ∑m ≥ j r(m)k

Evaluate for the cat row at coordinates 1 and 5:

∂L/∂Wcat,1 = 0.90 × (−0.4206 − 0.4057 − 0.3971) = 0.90 × (−1.2234) = −1.1011
∂L/∂Wcat,5 = 0.15 × (−0.3971) = −0.0596

An eighteen-fold difference — and it decomposes cleanly into two independent causes. Three residuals instead of one gives a factor of about 3.08 (1.2234/0.3971), and the front-loaded representation itself gives a factor of 6 (0.90/0.15). The multiplicity and the magnitude compound, so the head's early columns learn far faster than its late ones. The nesting propagates to every tensor the objective touches.

When the small granularity is simply wrong

The example so far was easy: two coordinates already got the answer right. Real training is full of examples where they do not, and those are where the interesting gradient lives. Take a fine-grained cat whose coarse features look like a dog:

z′ = [ 0.30, −0.50, 0.90, −0.30, 0.20, 0.40, 0.00, 0.30 ],   y = cat

At m = 2 the logits are 1.0(0.30) + 0.5(−0.50) = 0.05 for cat, 0.6(0.30) − 0.4(−0.50) = 0.38 for dog, and −0.5(0.30) + 0.3(−0.50) = −0.30 for bird.

e0.05 = 1.0513, e0.38 = 1.4623, e−0.30 = 0.7408, sum = 3.2544
pcat = 1.0513/3.2544 = 0.3230  ⇒   L2 = −ln(0.3230) = 1.1300

Two things at once: the prediction is dog, which is wrong, and the loss 1.1300 is above the guessing baseline ln 3 = 1.0986. The two-dimensional classifier is worse than a coin flip on this example. At m = 4 it is still wrong (L4 = 0.9103, predicting dog at 0.41 against cat at 0.35). Only at m = 8 does the tail pull it back:

ucat = 0.44, udog = 0.37, ubird = −0.52  →   pcat = 0.4319, L8 = 0.8396
total L = 1.1300 + 0.9103 + 0.8396 = 2.8799
ExampleL2L4L8Prediction at m=2Total
Easy cat (front-loaded z)0.54580.52040.5060cat ✓1.5722
Fine-grained cat (z′)1.13000.91030.8396dog ✗2.8799
This is where coarse-to-fine actually comes from. The m = 2 term generates a big residual on hard, fine-grained examples and a small one on easy, coarse examples. Averaged over a dataset, that means the early coordinates receive gradient dominated by the distinctions a tiny classifier can plausibly make — the broad, high-frequency, coarse ones — because that is where its errors are fixable. Errors that two dimensions can never fix produce large but conflicting gradients that largely cancel across examples. The representation ends up ordered coarse-to-fine not because anyone specified a hierarchy, but because different-capacity classifiers have different fixable errors.

The counterfactual: what a democratic representation costs

Take the same head, the same class, and a representation with the same total magnitude spread evenly. The original z has norm

‖z‖ = √(0.81 + 0.16 + 0.09 + 0.04 + 0.0225 + 0.01 + 0.0025 + 0.0025) = √1.1375 = 1.0665

Spread that evenly over eight coordinates: each entry is 1.0665/√8 = 0.377.

zdem = [ 0.377, 0.377, 0.377, 0.377, 0.377, 0.377, 0.377, 0.377 ]

At m = 2 the logits become

ucat = 0.377(1.0 + 0.5) = 0.5655   udog = 0.377(0.6 − 0.4) = 0.0754   ubird = 0.377(−0.5 + 0.3) = −0.0754
e0.5655 = 1.760335, e0.0754 = 1.078315, e−0.0754 = 0.927372, sum = 3.766022
pcat = 1.760335/3.766022 = 0.4674  ⇒   L2dem = −ln(0.4674) = 0.7605

Against the front-loaded representation's 0.5458. At full width the democratic vector reaches L8dem = 0.6687 (the row sums of W are 2.4, 0.8, and 0.0, so the logits are 0.9048, 0.3016, 0.0). Line the two up:

L at m=2L at m=8Degradation from cutting to a quarter
Front-loaded z0.54580.5060+0.0398  (+7.9%)
Democratic zdem0.76050.6687+0.0918  (+13.7%)

The democratic vector degrades 2.3 times as much when you cut it to a quarter of its length. The effect is real and it points the right way — but be honest about its size: this is a mild effect, and it is mild for a reason worth understanding.

The toy shows the mechanism; only scale shows the impact. Chapter 3 derived that truncation multiplies discriminability by √(m/d) for a democratic embedding. Here m/d = 2/8 = 1/4, so the factor is √(1/4) = 0.5 — a mere halving, which a three-class problem shrugs off. In production, d = 768 and m = 64 gives √(1/12) = 0.289, and you are separating thousands of concepts rather than three. An eight-dimensional example can only ever hint at a phenomenon whose severity grows with the dimension ratio. It is the right place to see the arithmetic and the wrong place to measure the effect.

Changing M, and watching the pressure move

One last variation, because M is the only genuinely new hyperparameter and its effect should be something you can compute rather than something you take on faith. Keep everything identical and change the nesting set from {2, 4, 8} to {4, 8}. What happens to coordinate 1?

With M = {4, 8} the multiplicity of coordinates 1 through 4 is 2, and of coordinates 5 through 8 is 1. Coordinate 1's total gradient loses its m = 2 contribution:

M = {2,4,8}:  ∂L/∂z1 = −0.3207 − 0.2903 − 0.2901 = −0.9011  (3.11× amplification)
M = {4,8}:    ∂L/∂z1 =          −0.2903 − 0.2901 = −0.5804  (2.00×)
M = {8}:       ∂L/∂z1 =                       −0.2901  (1.00× — ordinary training)

And note what else changes: with M = {4, 8}, coordinates 1 through 4 all have multiplicity 2, so they are indistinguishable to the objective. The model has no reason to order coordinate 1 above coordinate 3. Truncating to 2 dimensions would land in unsupported territory.

MPressure on coord 1Finest supported cutCoordinates the ordering distinguishes
{2, 4, 8}3.11×2three blocks: 1–2, 3–4, 5–8
{4, 8}2.00×4two blocks: 1–4, 5–8
{8}1.00×8 — the whole thingone block: no ordering at all
The rule M encodes. The smallest element of M is the finest cut you are entitled to make, and the number of elements sets both how strong the front-loading pressure is and how many distinguishable importance tiers the coordinates get. Adding a granularity costs one head — roughly 0.02% of a training step in the ResNet50 arithmetic of Chapter 4 — so the honest advice is to be generous with M and let the objective sort it out.

Seeing the geometry directly

The clearest way to feel why prefixes work is to look at what taking a prefix does geometrically. Taking the first m coordinates of a vector is an orthogonal projection onto the coordinate subspace spanned by e1, …, em. It casts a shadow. Whether the classes remain distinguishable in the shadow depends entirely on whether the direction separating them was aligned with the wall.

SHOWCASE — nested subspaces and their shadows

Three classes of points in a 3-dimensional embedding space, drawn in perspective. The translucent square is the e1e2 plane — the subspace a 2-dimensional prefix keeps — and the faint marks on it are the shadows the points cast onto it, which is literally what z[:2] returns. Rotate the view, then flip between an ordinary encoder (which orients the class-separating direction arbitrarily) and a matryoshka encoder (which orients it into the plane). Watch the shadows merge or separate.

Rotate view 34°

With the ordinary encoder the three clusters are perfectly separated in 3D — the representation is excellent — and their shadows on the e1e2 plane overlap into mush, because the separating direction points mostly along e3. With the matryoshka encoder the 3D separation is essentially the same and the shadows are clean. Same information, same quality at full width, different orientation, completely different behaviour under projection. That single picture is the paper.

Do one yourself

Two short exercises using the same W and the same conventions. Work them before reading the answers; the arithmetic is small and doing it is the difference between recognizing the mechanism and owning it.

exercise 1 The loss at m = 6

Using the original z = [0.90, 0.40, 0.30, 0.20, 0.15, 0.10, 0.05, 0.05] and the same head, compute the three logits at m = 6 — a granularity that is not in M = {2, 4, 8}. Then say, without computing the softmax, whether L6 must lie between L4 and L8.

Answer. Start from the m = 4 logits (1.26, 0.54, −0.37) and add columns 5 and 6. Cat: 1.26 + 0.1(0.15) + 0.1(0.10) = 1.285. Dog: 0.54 − 0.2(0.15) + 0.0(0.10) = 0.510. Bird: −0.37 + 0.3(0.15) − 0.1(0.10) = −0.335. The cat margin over dog grew from 0.72 to 0.775, so L6 is below L4. It is not guaranteed to sit between L4 and L8 in general — nothing in the objective constrains an untrained granularity — but here the columns happen to help, giving L6 ≈ 0.508, comfortably between 0.5204 and 0.5060. That is the smoothness Chapter 1 argued for, seen on one example rather than asserted.

exercise 2 Gradient on coordinate 3

Coordinate 3's column of W is (0.4, 0.2, −0.2). Which granularities reach it, and what is its total gradient? Compare the amplification factor to coordinate 1's 3.11×.

Answer. Coordinate 3 is in the prefixes with m ≥ 3, so m = 4 and m = 8 — two terms, not three. From m = 4: (−0.4057)(0.4) + (0.2893)(0.2) + (0.1164)(−0.2) = −0.16228 + 0.05786 − 0.02328 = −0.1277. From m = 8: (−0.3971)(0.4) + (0.2778)(0.2) + (0.1193)(−0.2) = −0.15884 + 0.05556 − 0.02386 = −0.1271. Total −0.2548, and the amplification over its m = 8 contribution alone is 0.2548/0.1271 = 2.00×. Exactly the multiplicity, as it must be, since the residuals are nearly equal across granularities. Coordinate 1 gets 3×, coordinate 3 gets 2×, coordinate 5 gets 1× — the staircase, on three numbers you computed yourself.

Every number in this chapter, checked

Hand arithmetic that nobody verifies is hand arithmetic that is wrong. Here is the audit, so you can confirm the chapter rather than trusting it.

QuantityValueCheck
p(2) sums to 10.5794 + 0.2820 + 0.1386= 1.0000 ✓
p(4) sums to 10.5943 + 0.2893 + 0.1164= 1.0000 ✓
p(8) sums to 10.6029 + 0.2778 + 0.1193= 1.0000 ✓
r(m) sums to 0, every m−0.4206 + 0.2820 + 0.1386= 0.0000 ✓ — softmax residuals always do
Losses below the guessing baseline0.5458, 0.5204, 0.5060 vs ln 3 = 1.0986all below ✓
Quality monotone in m0.5458 > 0.5204 > 0.5060strictly decreasing ✓ — as Chapter 1 predicted
Logit increments are additive1.10 → 1.26 → 1.290 for cateach step adds the new columns only ✓
logsumexp identityln(1.658602) = 0.505987matches L8 = 0.5060 ✓

The fourth row is the most useful invariant to remember. A softmax cross-entropy residual p − onehot always sums to zero, because p sums to 1 and the one-hot sums to 1. If any residual you compute does not, you have a bug — and it is a one-line check you can assert in code.

The sixth row is the empirical confirmation of Chapter 1's extendability argument on real numbers: quality improved at every step of m, and the mechanism (each granularity adds terms without rewriting the previous ones) explains why it had to.

In the worked toy, coordinate 1's total gradient is −0.9011 while its m = 8 contribution alone is −0.2901. What does the ratio 3.11 tell you?

Chapter 6: Adaptive Retrieval and the Funnel

Everything so far has been about making a small embedding good. This chapter is about the thing that small embedding unlocks, and it is a bigger deal than the memory saving: the cheap representation can filter work for the expensive one, because they live in the same space. Chapter 0 named that as the property three independent models cannot have. Here we cash it in.

The retrieval problem, priced from zero

You have N documents, each embedded as a d-dimensional unit vector, and a query embedded the same way. Find the k documents with the largest cosine similarity to the query.

Because everything is unit-norm, cosine similarity is the dot product, so one document costs d multiply-accumulate operations. Exhaustive search costs:

Cexhaustive = N · d  MAC per query

Use the paper's setting to make it concrete: the ImageNet-1K training set as a database, N = 1,281,167 images, d = 2048.

Cexhaustive = 1,281,167 × 2048 = 2,623,830,016 MAC ≈ 2.62 GMAC per query
memory = 1,281,167 × 2048 × 4 bytes = 10,495,320,064 B = 10.50 GB

Two stages, and the cost model that falls out

Now the idea. Do not score every document at full width. Score every document at a shortlist dimension Ds, keep the best K of them, and re-score only those K at the full rerank dimension Dr. Return the top k of the re-scored set.

Stage 1 — shortlist
Score all N documents using only z1:Ds. Cost: N · Ds MAC. Keep the top K.
↓ a list of K document ids
Stage 2 — rerank
Re-score those K using the full z1:Dr. Cost: K · Dr MAC. Return the top k.
Cadaptive = N · Ds + K · Dr

Read the two terms. The first is huge in N and tiny in dimension; the second is tiny in count and huge in dimension. The whole design is arranging for neither to dominate.

Worked example. Ds = 16, K = 200, Dr = 2048:

stage 1 = 1,281,167 × 16 = 20,498,672 MAC
stage 2 = 200 × 2048 = 409,600 MAC
total = 20,908,272 MAC ≈ 20.9 MMAC
speed-up = 2,623,830,016 / 20,908,272 = 125.5×

Verify: 20,908,272 × 125 = 2,613,534,000; the remainder 10,296,016 divided by 20,908,272 is 0.49. So 125.5. That is where the paper's “roughly 128× theoretical” figure comes from — and notice it is almost exactly Dr/Ds = 2048/16 = 128, because stage 2 contributes only 2% of the total. When the rerank stage is negligible, the theoretical speed-up is just the dimension ratio.

And the memory story is even better, which nobody says loudly enough. The 16-dimensional index is 1,281,167 × 16 × 4 = 82,014,688 bytes = 82 MB. That is 128× smaller than the 10.5 GB full index — and, crucially, 82 MB is small enough to live in RAM on a modest machine while 10.5 GB is not. So the deployment becomes: keep the shortlist index hot in memory, keep the full vectors on SSD, and fetch only the K = 200 you actually need. Those 200 vectors are 200 × 8,192 = 1.6 MB of reads per query. The machine you needed just got an order of magnitude cheaper, which is a bigger operational win than the FLOP count.

How big must K be? The recall question

The cost model above assumed the shortlist contains the right answers. It might not. Stage 2 can only rank what stage 1 handed it, so the pipeline's quality ceiling is:

recall = | top-k(full-dimension scores) ∩ shortlist(K) | / k

If a document that would have been in the true top-10 fails to make the 16-dimensional top-200, it is gone forever and no amount of reranking recovers it. So K is the knob trading cost against recall, and you want the smallest K with recall ≈ 1.

Reason about it through Chapter 3's noise model. The low-dimensional score is the full-dimensional score plus an error — call its standard deviation δ. A document truly at rank 10 falls out of the top K only if the noise pushes it below K − 10 documents that were genuinely worse. So the question is whether the true score gap between rank 10 and rank K exceeds a few multiples of δ.

Worked example. Suppose the 16-dimensional score error has δ = 0.02 in cosine units, and on your corpus the true cosine at rank 10 is 0.74 while at rank 200 it is 0.66. The gap is 0.08 = 4δ. A rank-10 document needs a 4-sigma downward error to be displaced past rank 200, which happens for roughly 1 document in 30,000. Recall@200 is effectively 1. Now try K = 20, where the gap might be only 0.01 = 0.5δ: displacements are routine and recall collapses.

KStage-2 cost (MAC)Share of totalTypical recall@10
2040,9600.2%poor — the gap is inside the noise
100204,8001.0%good
200409,6002.0%≈1
2,0004,096,00016.4%≈1, and you are now paying for it
20,00040,960,00066%≈1, and the pipeline is pointless

The shape of that table is the practical guidance: K in the range of 10× to 20× the number of results you actually return costs about 2% of the pipeline and buys essentially all the recall. Beyond that you are spending real money to protect against errors that are not happening.

Where the optimum sits — solving for the best Ds

The cost model is simple enough to differentiate, which turns “pick a shortlist dimension” from a sweep into an equation. Treat K as depending on Ds, since a coarser shortlist needs more candidates to keep recall — a reasonable model is K = K0 · D0/Ds, so halving the shortlist dimension doubles the candidates you must keep. Then:

C(Ds) = N · Ds + (K0D0/Ds) · Dr
dC/dDs = N − K0D0Dr/Ds2 = 0  ⇒   Ds* = √( K0D0Dr / N )

Put the paper's numbers in. Anchor the recall model at K0 = 200 when D0 = 16, so K0D0 = 3,200:

Ds* = √( 3,200 × 2,048 / 1,281,167 ) = √5.115 = 2.26

The optimum is below 4, which says the cost model alone would push you all the way to the smallest dimension available. So why does the paper use 16? Because the cost model is not the binding constraint — recall is. Below about 16 dimensions the shortlist stops being a reliable filter no matter how many candidates you keep, because the score noise swamps the true gaps. The optimum of the cost function sits in a region the quality constraint has already ruled out.

Which is the useful conclusion, and it generalizes. When the unconstrained optimum of your cost model lies outside the feasible region, the answer is always “go to the boundary” — pick the smallest Ds whose recall still plateaus, and stop reasoning about cost. The differentiation was not wasted: it told you the cost surface is flat and monotone in this region, so you are free to let quality decide alone. Knowing that a knob does not trade off against anything is worth as much as knowing where its optimum is.

Funnel retrieval — the cascade, and its lovely accident

Two stages is the simple version. The paper's funnel retrieval generalizes it: instead of one jump from 16 dimensions to 2048, walk up the chain, halving the candidate set at each rung and doubling the dimension.

RungCandidates inDimensionCost (MAC)Candidates out
Shortlist1,281,1671620,498,672200
1200326,400100
2100646,40050
3501286,40025
4252566,40012
5125126,1446
6610246,1443
7320486,144final
Cascade total44,032
Look at the cost column. Every rung costs about 6,400 MAC. That is not a coincidence and it is not tuning — it is what happens when you halve the count and double the dimension: the product Ki · Di is invariant. The funnel is a geometric cascade designed so that every level of refinement costs the same, which means you get seven levels for the price of about seven times one level, and the whole cascade — 44,032 MAC — is 0.2% of the shortlist scan that precedes it. Precision is free once you have paid for the sweep.

And that invariance is only possible because the granularities are nested. The rung at 128 dimensions is refining a shortlist produced at 64 dimensions using strictly more of the same vector. Under three independently trained models there is no such chain: each stage would be re-scoring with a different, incomparable notion of similarity, and the shortlist from stage 1 would carry no guarantee about stage 2 at all.

SHOWCASE — the funnel cost simulator

Set the corpus size, the shortlist dimension, and the shortlist size, and watch the two cost terms fight each other. The left panel breaks down MACs per query; the right shows hot memory and the resulting speed-up. The dashed line marks the point where the rerank stage starts to cost as much as the sweep — past it, shrinking Ds further buys you nothing.

Corpus N 1.28M
Shortlist dim Ds 16
Shortlist size K 200

Drag Ds down to 8 and then to 4 and watch the total stop improving: once stage 1 is cheap enough, stage 2 and the fixed overheads dominate and further shrinking is wasted. Drag K up and watch stage 2 take over. The sweet spot is a broad basin, not a knife edge, which is why this technique is robust in practice.

Setting the three parameters, in order

Adaptive retrieval has exactly three knobs — Ds, K, and Dr — and they should be set in a specific order, because two of them are determined by constraints and only one is a genuine trade-off.

1. Dr is not a choice
The rerank dimension is whatever gives you the quality you want, which is almost always the full width. The rerank stage touches a few hundred documents, so its cost is negligible — there is no reason to economize here. Set Dr = d and move on.
2. Ds is set by memory, not by quality
Pick the smallest granularity in M whose index fits in the memory tier you want — one machine, or last-level cache. That is a hard constraint with a discrete answer, and going smaller than it buys nothing because Amdahl caps you anyway.
3. K is the only real dial
Sweep it against recall of the shortlist versus exhaustive ground truth, and stop where the curve plateaus. Typically 10× to 20× the number of results you return.

People usually do this backwards — they tune Ds for quality and treat K as fixed — and then spend a week discovering that Ds barely matters over a broad range while K matters enormously below a threshold. The reason is in the cost model: quality is governed by whether the true answers survive the shortlist, and that is far more sensitive to how many you keep than to how precisely you scored them.

Why 128× in theory becomes 14× on a clock

The paper reports about 128× in FLOPs and about 14× in wall-clock time. That is a factor-of-nine discrepancy, and understanding it is more useful than either number, because the same reasoning applies to every optimization you will ever ship.

Three costs do not shrink when you shrink the dimension:

1. Top-K selection. After scoring, you must find the best K of N scores. That is N comparisons against the current threshold no matter what d was — 1.28 million branches, independent of dimension. At roughly 1.5 ns each that is about 2 ms, and it is a hard floor.

2. Approximate-nearest-neighbour traversal. Nobody runs an exhaustive scan in production; they run an HNSW graph or an IVF index. Graph traversal is dominated by pointer chasing — each hop is a cache miss costing about 100 ns of pure latency — and a cache miss costs the same whether the vector at the other end is 16 or 2048 numbers wide. Reducing d shrinks the distance computation and the bytes read, but not the latency of getting there.

3. The baseline is already optimized. The 128× is against a naive full scan. Against a well-tuned ANN index — the thing you would actually deploy — much of the easy win is already taken.

Put a number on it with Amdahl's law. Let f be the fraction of baseline time that scales with dimension, and (1 − f) the fraction that does not. Shrinking the dimension by 128× gives:

speed-up = 1 / [ (1 − f) + f/128 ]

Solve for the observed 14×:

1/14 = 0.0714 = (1 − f) + f/128 = 1 − f(1 − 1/128) = 1 − 0.9922 f
0.9922 f = 0.9286  ⇒   f = 0.936

So about 94% of the baseline's time was dimension-proportional and 6% was not. Check it: 1/(0.064 + 0.936/128) = 1/(0.064 + 0.00731) = 1/0.07131 = 14.0. ✓

The consequence, and it is a design rule. That 6% is a hard ceiling: even a one-dimensional shortlist could only reach 1/0.064 = 15.6×. You are already at 14 of a possible 15.6, so shrinking Ds from 16 to 8 will buy you almost nothing measurable. The next win has to come from attacking the dimension-independent 6% — better top-K selection, a shallower graph, fewer cache misses — not from a smaller embedding. Knowing which resource you have stopped being limited by is worth more than another factor on the resource you already fixed.

What this does to an ANN index

Chapter 6 has so far assumed brute-force scanning, which is the honest way to derive a cost model but not what you deploy. Fold in an approximate index and three separate things improve, only one of which is the one people expect.

Property of the indexAt 2048 dimsAt 16 dimsWhy it changes
Vector storage10.50 GB82 MBLinear in d
HNSW graph storage (32 neighbours × 4 B)164 MB164 MBUnchanged — the graph is ids, not vectors
Bytes touched per hop8,19264Linear in d. This is the win
Number of hops to converge~150~150 to 300Worse, if anything — a coarser space has more near-ties, so the greedy search wanders longer
Build timehoursminutesBuild is dominated by distance computations

The second and fourth rows are the ones that explain the Amdahl fraction. An HNSW graph over 1.28 million nodes costs the same 164 MB regardless of dimension, and each hop is a cache-missing pointer chase whose ~100 ns of latency does not shrink. Reducing the dimension by 128× turns an 8 KB read per hop into a 64-byte read, but does not remove the miss.

The counter-intuitive part. A lower-dimensional space can need more hops, because distances concentrate: with fewer coordinates there are more near-ties, so the greedy descent has more plausible directions to try. That is one more reason to keep the shortlist dimension at 16 or 32 rather than pushing to 4 — below some point you start paying in traversal what you saved in bandwidth. The cost model in the simulator is a lower bound; the real curve turns up again at the far left.

The two-stage pipeline, in code

python — adaptive retrieval, end to endimport numpy as np

# Built once, offline, from ONE matryoshka index.
#   Z: (N, 2048) float32 full archive, memory-mapped or on SSD
#   S: (N,   16) float32 contiguous shortlist copy - 82 MB, stays in RAM
Z = np.load("index_2048.npy", mmap_mode="r")
S = np.load("index_16.npy")              # np.ascontiguousarray(Z[:, :16]) renormalised

def search(q, k=10, K=200):
    # stage 1 - sweep every document at 16 dims
    q16 = q[:16]; q16 = q16 / np.linalg.norm(q16)   # slice THEN normalise
    scores = S @ q16                              # (N,)  20.5 MMAC, 82 MB read
    cand   = np.argpartition(-scores, K)[:K]     # (K,)  O(N), dimension-free

    # stage 2 - rerank only those K at full width
    full   = np.asarray(Z[cand])                  # (K, 2048) - 1.6 MB of SSD reads
    exact  = full @ q                             # (K,)  0.41 MMAC
    order  = np.argsort(-exact)[:k]
    return cand[order], exact[order]

# Measuring the ONLY thing that matters for correctness:
def recall_at_k(qs, k=10, K=200):
    hits = 0
    for q in qs:
        approx = set(search(q, k, K)[0].tolist())
        truth  = set(np.argsort(-(Z @ q))[:k].tolist())   # exhaustive ground truth
        hits  += len(approx & truth)
    return hits / (len(qs) * k)
# Sweep K until this plateaus, then stop. That is the whole tuning procedure.

Two lines carry the argument. q[:16] then normalize — the same rule as everywhere else. And np.argpartition, which is the dimension-independent O(N) top-K selection that sets the Amdahl floor: it does not get faster when the vectors get shorter, because it never looks at a vector.

The 20 ms budget, revisited

Return to Chapter 0's autocomplete path and re-itemize it with a matryoshka index. The point is not the total; it is which line moved.

StageBefore (768 dims, 12M docs)After (64-dim sweep, 768-dim rerank)
Network + parse2 ms2 ms — unchanged
Encode the query4 ms4 ms — unchanged, you still run the full encoder
Index resident memory36.9 GB3.07 GB hot + 36.9 GB cold on SSD
Searchover budgetwithin budget — 12× fewer bytes swept
Rerank 200 at 768 dims+0.6 ms (1.6 MB of reads, 0.15 MMAC)
Hydrate + serialize4 ms4 ms — unchanged

The encoder line did not move, and it never will — MRL shrinks the output, not the network. If your budget is dominated by encoding rather than searching, this entire chapter buys you nothing and you want Chapter 9's depth-nesting instead. Knowing which line of the budget you are actually fighting is the difference between a technique that works and one that merely sounds good.

The gotcha nobody warns you about: scores are not comparable across m

Here is a bug that ships to production regularly. You have a relevance threshold: “show the result if cosine > 0.75.” You switch a service from 768 dimensions to 256 to save memory. The threshold no longer means what it meant.

Why: cosine similarity at m dimensions is computed over a different number of terms, and the baseline similarity between two unrelated vectors depends on m. For two random unit vectors in Rm, the expected cosine is 0 but its standard deviation is 1/√m:

m = 768:  1/√768 = 0.036     m = 256:  1/√256 = 0.063     m = 64:  1/√64 = 0.125

At 64 dimensions two completely unrelated documents routinely score 0.25 — two standard deviations of pure chance — whereas at 768 that same score is nearly seven standard deviations and would be a strong match. The noise floor rises as the dimension falls, so every threshold, every cutoff, and every “confidence” heuristic must be recalibrated per granularity.

The rule. Thresholds are per-dimension constants. Store them as a table keyed by m, calibrated on held-out data at each granularity you serve. Rankings transfer across granularities; absolute scores do not. Any code path that compares a similarity to a hard-coded number is a bug waiting for the day someone changes the dimensions parameter.

Sharding, replication, and the tail latency nobody budgets for

One consequence of index size is so large and so frequently missed that it deserves its own arithmetic: how many machines you need, and what that does to your p99.

Take the 12-million-document, 768-dimensional index at 36.9 GB. Assume 64 GB machines with about 32 GB usable for the index after the OS, the process, and headroom. You need two shards, and with a replication factor of three for availability that is six machines.

Now the part people do not budget for. A sharded query fans out and waits for the slowest shard, so the overall latency is the maximum over shards. If shard latencies are independent and each shard is under t with probability p, then:

P(all S shards under t) = pS
for the overall p99 with S shards, each shard needs  p = 0.991/S
ShardsPer-shard percentile you must hit for an overall p99
1p99
2p99.50
8p99.87
32p99.97

Verify the third row: 0.991/8 = exp(ln(0.99)/8) = exp(−0.010050/8) = exp(−0.0012563) = 0.998744, which is the 99.87th percentile. Since latency distributions have long tails, the p99.87 of a shard is typically two to three times its p99 — so fanning out to eight shards can double or triple your tail latency even though every shard is doing less work.

Now truncate. The 64-dimensional shortlist index for the same corpus is 12,000,000 × 64 × 4 = 3.07 GB. One shard. No fan-out, no merge step, no maximum-of-S tail amplification, and the rerank stage reads 200 full vectors from an archive whose latency is not on the critical fan-out path. You did not just make the search 12× cheaper — you deleted a distributed system.

This is the largest practical benefit in the whole lesson, and it never appears as a number in the paper because it is not a property of the representation. It is a property of thresholds in your infrastructure: 32 GB per machine, 64 GB per machine, whatever your fleet has. Crossing back under one of those thresholds is discontinuous in a way that a smooth accuracy curve cannot express.

Keeping two indexes in sync — and why that is easier than it sounds

Adaptive retrieval means you now maintain two artifacts: the full archive and the derived shortlist index. Two artifacts is normally two chances to drift, so the operational question is fair: what happens when documents arrive, change, and are deleted?

OperationFull archiveShortlist indexFailure if you skip it
InsertAppend the 2048-dim vectorAppend its first 16, re-normalizedThe document is unreachable — it can never enter a shortlist
UpdateOverwrite in placeOverwrite in placeStale shortlisting: the document is found for its old meaning and reranked by its new one
DeleteTombstoneTombstoneA deleted document occupies a shortlist slot, silently reducing effective K

The stale-update row is the subtle one, because it fails in the direction that looks like a quality problem rather than a bug. But here is the property that makes all of this manageable, and it is the operational payoff of “a prefix is a slice”:

The shortlist index is derived, so drift is always repairable and never expensive. It contains no information the archive lacks, and rebuilding it is a strided copy: read 10.5 GB, write 82 MB, no model, no GPU, no fitted artifact, no risk of producing something subtly different from what it replaced. On any modern machine that is minutes. So the recovery procedure for “the two are out of sync” is “rebuild the small one,” and you can schedule it nightly without thinking about it.

Compare the alternatives. If your small index came from a PCA projection, rebuilding requires the projection matrix from whenever it was fitted — and if you refit it on the current corpus you get a different projection, so every stored vector must be recomputed and every threshold recalibrated. If it came from a separately trained small model, rebuilding means re-encoding twelve million documents on GPUs. The matryoshka version is the only one where the derived artifact is genuinely a function of the primary artifact and nothing else.

In the funnel cascade, why does every rung cost roughly the same number of MACs?

Chapter 7: Results, Honestly — and Production

The method is one line and the mechanism is a counting argument. The scientific content is entirely in the measurements: is the nesting constraint cheap, and does the recipe survive contact with modalities and datasets it was not designed for?

The four claims in the abstract

The paper makes four quantitative claims, and they are worth separating because they measure different things.

ClaimWhat it is really testing
Up to 14× smaller embeddings at the same ImageNet-1K classification accuracyThe core question: is the nesting constraint cheap? A 14× reduction at matched accuracy says the ordinary 2048-dim representation was carrying a great deal of redundancy that MRL learned to concentrate
Up to 14× real-world speed-ups for large-scale retrieval on ImageNet-1K and ImageNet-4KChapter 6's cost model, measured on a clock rather than a spreadsheet
Up to 2% accuracy improvement for long-tail few-shot classificationThe surprising one. MRL helps on rare classes rather than merely costing nothing
Extends across modalities and web-scale data — vision (ResNet, ViT), vision+language (ALIGN), language (BERT), and JFT-scale trainingWhether this is a trick that works on one benchmark or a property of representation learning

The third row deserves a moment. Why would forcing a representation to be usable at eight dimensions make it better on rare classes? A plausible account: the small-granularity terms cannot afford to memorize fine distinctions, so they push the encoder toward coarse, shared, reusable structure. Rare classes have few examples and therefore benefit most from structure shared with common classes rather than from class-specific memorization. The nesting constraint acts like a regularizer that happens to encourage exactly the kind of representation that transfers.

The four claims, re-derived from what you already know

You now have every mechanism needed to predict each of those results before reading them, which is the best test that the previous chapters landed. Try it.

Why should a 14× reduction be possible at matched accuracy? Because Chapter 1's redundancy argument says a 2048-dimensional representation of ImageNet does not use 2048 independent directions, and Chapter 3's PCA discussion says the useful directions exist but are not axis-aligned. MRL rotates them into alignment and orders them. The size of the reduction is a measurement of how much redundancy was there, and 14× says: a lot.

Why should the retrieval speed-up match the compression? Because Chapter 6's cost model is dominated by N · Ds, which is linear in the shortlist dimension. It is not a coincidence that both numbers are 14 — they are the same fact, measured on the memory axis and on the clock.

Why should few-shot on rare classes improve? Because Chapter 5's fine-grained example showed that the small-granularity terms generate residuals dominated by coarse, broadly shared structure — and coarse shared structure is exactly what a class with eight examples has to lean on, since it cannot afford class-specific memorization. The nesting objective is, accidentally, a regularizer that favours transferable features.

Why should it transfer across modalities? Because nothing in Chapter 2's derivation mentions images. The mechanism is an index-set inequality over a sum of losses; it is agnostic to what produced z. If it had failed on text that would have been the surprising result, and it would have implied something specific about text representations that nobody has a theory for.

The value of being able to do that. A result you can predict is a result you can extrapolate. You can now answer “will this work on my domain?” without waiting for a paper — the question reduces to “is my representation redundant, and is my downstream reader linear?” If yes to both, the mechanism applies. If your representation is already at its intrinsic dimension, expect a real cost. That is a more useful thing to own than a table of numbers.

The shape of the curve, and what to expect from it

The consistent finding across the paper's settings is easiest to hold as a shape rather than a table:

At full dimension
MRL essentially matches the ordinary baseline. The constraint is close to free where it is close to vacuous — which is exactly what Chapter 1's “extendability” argument predicted.
In the middle (64–512)
MRL tracks models trained natively at that size. Post-hoc SVD also does reasonably here — there is enough room that variance ordering is a decent proxy for importance.
At the bottom (8–32)
The gaps blow open. Naive truncation is near useless, post-hoc methods degrade sharply, and MRL stays close to the native-training reference. This is where the method earns its keep.

And the honest caveats, stated plainly because a lesson that only reports wins is advertising:

CaveatWhy it matters
MRL does not beat the baseline at full dimensionIt matches it. The gain is optionality, not accuracy. If you only ever serve at 768, MRL buys you nothing
MRL-E is measurably weaker than MRLTying the head columns is a real constraint. Use MRL unless L makes the heads dominate
“14× smaller” is the best point on the curve, not every pointAn 8-dimensional MRL embedding is still much worse than a 2048-dimensional one. The claim is about matched accuracy, and it lands where the baseline curve is flat
Below the smallest granularity in M there is no guaranteeTrained with M starting at 8, the model has no reason to order coordinates 1 through 8 among themselves. Slicing to 4 falls off the supported region
The nesting set is a design decision you must make before trainingAdding a granularity later means retraining or fine-tuning. It is not a runtime choice, unlike the dimension itself

Which metric to read: linear probe or 1-NN?

The paper reports both, and if you are building retrieval you should weight them very differently. The distinction is worth twenty seconds of thought because it changes which number predicts your outcome.

Linear probe accuracy1-nearest-neighbour accuracy
What it doesTrains a linear classifier on frozen embeddingsLabels each point by its nearest neighbour's label. No training at all
Free parametersL × m, fitted on your dataZero
What it measuresWhether the information is present and linearly extractableWhether the geometry is right — whether similar things are actually close
PredictsClassification-head performanceRetrieval performance

The critical difference is the second row. A linear probe gets to refit for every dimension, so it can partly compensate for a badly oriented representation by choosing different coefficients. Retrieval cannot: cosine similarity has no parameters, so it is stuck with whatever geometry the encoder produced. That makes 1-NN the honest proxy, and it makes probe accuracy systematically optimistic about how well truncation will go.

The practical instruction. If you are shipping a classifier, read the linear-probe curve. If you are shipping search, read 1-NN — and in your own evaluation, prefer recall@k against the full-dimension ground truth over any metric that involves fitting something. Any evaluation with free parameters is measuring your fitting procedure alongside your representation, and at serving time you only get one of those.

Robustness: does the ordering survive distribution shift?

A reasonable worry. The coordinate ordering was learned on one training distribution. If you evaluate on shifted data — ImageNetV2 (a fresh test set collected the same way), ImageNet-R (renditions: art, sketches, cartoons), ImageNet-A (natural adversarial examples), ObjectNet (unusual viewpoints and backgrounds) — does the first coordinate still carry the most broadly useful signal, or was the ordering fitted to a quirk of the training set?

The paper evaluates exactly this, and the finding is that MRL's robustness tracks the baseline's: the ordering is a property of the task structure, not of the specific test distribution. That is what the mechanism predicts. Coordinate 1 became broadly useful because nine classifiers of differing capacity all demanded broad usefulness from it, and “broadly useful” is close to the definition of “transfers.” The pressure that creates the ordering is the same pressure that makes representations robust.

MRL in production — the fastest adoption of any 2022 representation paper

Something unusual happened to this paper. Between 2022 and 2024 the technique went from a NeurIPS paper to a parameter in the largest commercial embedding APIs. Very few representation-learning papers make that journey, and the reason is structural rather than about quality.

Why it spread so fast: the deployment diff is zero. Adopting MRL requires no change to your vector database, no new index type, no new serving code, no extra artifact, no schema migration. The instruction is “store fewer of the numbers you were already storing.” Compare that to product quantization, which needs codebooks, a training step, an approximate distance implementation, and a new index format. A technique whose integration cost is a slice operation gets adopted at the speed of a config change.
SystemHow matryoshka shows upNative dim
OpenAI text-embedding-3 (Jan 2024)A dimensions parameter on the API. Ask for 256 instead of 3072 and you get the truncated, re-normalized prefix1536 (small) / 3072 (large)
Nomic Embed v1.5Open weights trained with MRL; supported sizes 768, 512, 256, 128, 64768
mixedbread mxbai-embed-largeMRL training combined with aggressive quantization — the “shrink both axes” stack of Chapter 81024
Google Gemini embeddingsAn output_dimensionality parameter with recommended sizes, and documentation telling you to re-normalize below the native size3072
Snowflake Arctic Embed 2.0, Jina v3, and othersMRL-trained with documented truncation points1024 / 1024

OpenAI's announcement makes the value proposition unusually legible, because they published the comparison. Their older text-embedding-ada-002 scored 61.0% average on MTEB — the Massive Text Embedding Benchmark, a suite of 50-odd retrieval, clustering, and classification tasks — at 1536 dimensions. The new text-embedding-3-large scores 64.6% at 3072 dimensions. And the sentence that made everyone pay attention: shortened to 256 dimensions, text-embedding-3-large still outperforms the full 1536-dimensional ada-002.

3-large @ 256 dims  >  ada-002 @ 1536 dims
6× fewer bytes, better quality — and the 256-dim vector is literally a prefix of the 3072-dim one

Run that as a bill. One hundred million chunks, which is a mid-sized enterprise RAG corpus:

ConfigurationBytes per vectorIndex sizeRelative
ada-002, 1536 dims, fp326,144614 GB1.00×
3-large, 3072 dims, fp3212,2881,229 GB2.00×
3-large truncated to 256, fp321,024102 GB0.17×
3-large truncated to 256, int825626 GB0.04×

Check one row: 100,000,000 × 1,024 bytes = 102,400,000,000 = 102.4 GB. ✓ The third row is a better model than the first, in a sixth of the space. The fourth row — which is Chapter 8 — fits an entire hundred-million-chunk corpus into the RAM of a single large machine.

What MTEB is measuring, so the numbers mean something

Every production claim in this chapter is denominated in MTEB points, so it is worth two paragraphs on what a point is. The Massive Text Embedding Benchmark aggregates dozens of datasets across seven task families, and an “average” is a mean over those families, not over a single metric.

Task familyWhat the embedding must doSensitive to truncation?
RetrievalRank a corpus against a query; nDCG@10Most — needs fine distinctions among many near-neighbours
RerankingOrder a supplied candidate listHigh — the candidates are already similar by construction
ClusteringGroup documents; v-measureModerate — needs coarse structure, which is what prefixes keep best
Pair classificationDuplicate or not; average precisionModerate
ClassificationLinear probe on frozen embeddingsLeast — a handful of coarse directions usually suffices
STSCorrelate with human similarity judgementsLow to moderate
SummarizationScore machine summaries against referencesLow

The right-hand column is the practical warning. An average MTEB score that barely moves under truncation can hide a retrieval score that moved a lot, because retrieval is one family among seven and it is the one most exposed. If retrieval is your product, read the retrieval number, not the average. The published headline figures are averages; your decision should not be.

The three-year bill

Money makes the trade-off legible in a way percentages do not. Take a 100-million-chunk corpus on a 3072-dimensional embedder, and use a round illustrative figure of $5 per gigabyte-month for hot vector-index memory in a managed service. Substitute your own unit cost; the ratios are what carry.

ConfigurationIndex sizePer monthOver 3 years
3072 dims, float321,229 GB$6,145$221,220
1024 dims, float32410 GB$2,050$73,800
256 dims, float32102 GB$510$18,360
256 dims, int825.6 GB$128$4,608

Check one line: 100,000,000 × 3072 × 4 = 1.229 × 1012 bytes = 1,229 GB; at $5 that is $6,145 per month. The last row is a better model than the ada-002 baseline, at 2% of the first row's cost, reachable by changing one API parameter and adding a per-dimension quantizer.

And the second-order effect is larger than the first. The bill above is only the steady state. The reason a 25 GB index beats a 1.2 TB one by more than the ratio suggests is that 25 GB fits on one machine. No sharding, no cross-shard result merging, no shard-level tail latency, no rebalancing when the corpus grows, no distributed rebuild on model update. Crossing back over the single-machine threshold deletes an entire class of engineering, and that saving does not appear on any line of the table.

Quality per byte — the frontier nobody plots

Model comparisons are almost always “quality at the native dimension,” which silently prices bytes at zero. Once dimension is a runtime parameter, the honest comparison is a frontier: quality against bytes stored. Plotting it changes which model you would pick.

ConfigurationBytes / vectorMTEB averagePoints per KB
ada-002 @ 1536, fp326,14461.010.2
3-small @ 1536, fp326,14462.310.4
3-large @ 3072, fp3212,28864.65.4
3-large @ 512, fp322,048
3-large @ 256, fp321,024above ada-002's 61.0> 61
3-large @ 256, int8256essentially the same> 244

The “points per KB” column is deliberately a crude ratio — MTEB points are not linear and zero is not a meaningful origin — but the ordering it produces is the useful part, and it inverts the ordering you get from quality alone. On raw quality, 3-large at 3072 wins. On quality per byte it is the worst row in the table, twenty-four times worse than the same model truncated and quantized.

The habit worth adopting. When someone proposes a bigger embedding model, ask for the frontier and not the point. “Better at 3072 dimensions” and “better at the 1 KB you can afford” are different claims, and matryoshka training is what makes the second one askable. Before 2022 the frontier had one point on it per model, so the question could not be posed. Now it can, and a model that is second-best at full width but degrades gracefully will beat a model that is best at full width and collapses at 256 — for any deployment where bytes are not free, which is all of them.

Triaging a quality regression after you turn truncation on

SymptomCheck firstUsual cause
Every score shifted down uniformlyIs the vector unit-norm after truncation?Normalized before slicing. Chapter 2's gotcha
Some documents now win everythingDistribution of truncated norms across the corpusSame bug, seen from the other side: front-loaded documents got longer vectors
Precision fine, recall collapsedShortlist K, and recall of stage 1 against exhaustiveK too small for this corpus, or Ds too small
Fewer results pass the relevance cutoffThe threshold tableReusing a threshold calibrated at the old dimension. The 1/√m floor moved
Only long documents regressedChunking, and whether long docs have more diffuse embeddingsReal, and not an MRL bug — truncation exposes it
Regression only on rare queriesWhether those queries need fine distinctionsGenuine truncation loss. Raise m for that consumer, or add a rerank stage

The first two rows account for most real incidents, and both are the same one-line mistake. Put an assertion in the serving path: every stored and every queried vector has unit norm to within 10−5. It costs nothing and it catches the entire failure class before it reaches a dashboard.

The API contract, and the one line every provider repeats

python — the dimensions parameter, and the thing you must not forgetimport numpy as np
from openai import OpenAI
client = OpenAI()

# Server-side truncation: ask for the prefix you want.
r = client.embeddings.create(model="text-embedding-3-large",
                              input="a query about kalman filters",
                              dimensions=256)
v = np.array(r.data[0].embedding)        # (256,) already re-normalized

# Client-side truncation from a full vector - YOU must re-normalize.
full = np.array(client.embeddings.create(
    model="text-embedding-3-large",
    input="a query about kalman filters").data[0].embedding)   # (3072,)

cut  = full[:256]                          # (256,)  NOT unit norm
print(np.linalg.norm(cut))                  # e.g. 0.83, and it VARIES per input
cut  = cut / np.linalg.norm(cut)            # now it is comparable

# Why it matters: without this line, a document whose signal happens to
# concentrate in the first 256 dims has a LONGER truncated vector, so its
# raw dot product with every query is inflated. Your ranking becomes a
# popularity contest between documents that front-load well.

That re-normalization line appears in the documentation of every provider that ships this feature, and it is Chapter 2's gotcha in production form. It is worth internalizing why it is not a formality: MRL deliberately front-loads magnitude, so the truncated norm is systematically large and systematically variable across documents. It is the one failure mode of matryoshka embeddings that produces plausible-looking wrong answers rather than obviously broken ones.

The evaluation protocol you should actually run

A number from a paper is a claim about their setup. Here is the protocol that turns it into a claim about yours, and it is short enough that there is no excuse for skipping it.

StepWhat to measureWhat a failure looks like
1. Full-width parityYour existing production metric at m = d, MRL model versus the old oneA drop > 0.5 point means the nesting constraint is binding — M reaches too low, or you undertrained
2. The truncation curveThe same metric at every m in M, plus two values between granularitiesNon-monotone quality, or an intermediate value far below its neighbours
3. Shortlist recallrecall@k of the Ds-dim top-K against the d-dim top-k, swept over KRecall that never plateaus — the shortlist dimension is too small for your corpus
4. Threshold recalibrationThe score distribution of matched and unmatched pairs at each mReusing the old threshold. Chapter 6's 1/√m floor guarantees this is wrong
5. Slice-order sanityMean squared coordinate magnitude versus indexA flat curve — the run did not front-load anything
6. Tail-slice behaviourThe metric at m below the smallest granularity in MA cliff. Expected, and worth knowing exactly where it starts

Step 3 is the one teams skip and then regret, because it is the only one that measures the thing adaptive retrieval actually depends on. Full-width parity and a pretty truncation curve tell you the representation is good; recall of the shortlist tells you the pipeline is correct. They are different questions and only the second can silently lose documents in production.

Migrating an index that already exists

You have a live 768-dimensional index and a new matryoshka model. Both cannot be true at once, and a flag day is unacceptable. The sequencing that works:

1. Dual-write, single-read
Embed every new and updated document with both models. Serve from the old index. Cost: double encoding on the write path, double storage. Duration: as long as it takes to backfill.
2. Backfill and shadow
Re-embed the archive with the new model. Run the new index in shadow: answer from the old, log the new, diff the top-10. You are looking for systematic disagreements, not per-query ones.
3. Cut over at full width
Serve from the new index at m = d. Nothing about truncation is live yet — this step isolates “is the new model good” from “is truncation safe”, and you want those failures separated.
4. Introduce truncation, one consumer at a time
Materialise the shortlist index. Move the most latency-sensitive consumer to the two-stage path. Recalibrate its thresholds. Then the next. Never all at once, because the failure mode is a quiet recall drop rather than an error.

Step 3 exists purely to keep two variables from moving together, and it is the step schedules always try to delete. Do not let them: if you change the model and turn on truncation in the same deploy and quality moves, you will not know which one did it, and you will burn a week finding out.

A practitioner's FAQ

“Can I truncate an OpenAI embedding to 137 dimensions?” Mechanically yes, and it will probably work — the pressure staircase is smooth, so nearby coordinates are treated nearly identically. But you are outside the documented sizes, so measure it. And re-normalize.

“Do I have to re-embed my corpus to change dimension?” No, and this is the single most valuable operational property. Store the full-width vectors once. Any smaller index is a strided copy away, produced in minutes with no GPU. Changing your served dimension is a rebuild of a derived artifact, not a migration.

“Can I mix dimensions in one index?” No. A dot product between a 256-dimensional vector and a 768-dimensional one is not defined, and truncating one side at query time to match gives you a comparison at the smaller dimension — which is fine, as long as you know that is what happened. Pick a dimension per index.

“Does the query have to use the same dimension as the documents?” Yes, for the obvious reason, and with the same normalization rule on both sides. Truncate and normalize the query exactly as you truncated and normalized the documents. Asymmetry here is a silent ranking bug.

“Is 3-large at 256 dimensions really better than ada-002 at 1536?” On MTEB average, that is the published claim. On your corpus, run the twenty-minute test from Chapter 3. Benchmark averages are a prior, not a measurement of your system.

“What if my provider does not document matryoshka support?” Then it probably does not have it, and truncating will fail in the way Chapter 3 derives. Test before assuming: the magnitude profile takes thirty seconds and the recall test takes twenty minutes.

“Does this interact with hybrid search?” Only through the score-fusion weights. If you blend a dense score with BM25, the dense score's scale changes when you change dimension — the 1/√m floor from Chapter 6 — so the blend weight has to be retuned. Reciprocal-rank fusion, which uses only ranks, is immune, and that is a good reason to prefer it.

“Should I truncate or quantize first?” Quantize first if you cannot retrain, because it is post-hoc and nearly free. Truncate first if you can, because it is free at query time and needs no artifact. Then do both — Chapter 8 is about how they multiply.

Where the technique does not help

SituationWhy MRL is the wrong tool
You serve exactly one dimension and always willTrain natively at that dimension. MRL sells optionality; if you do not want the option you are paying a small constraint for nothing
Your bottleneck is encoder throughput, not index sizeMRL does not make the encoder faster — you still run the full network to produce z. For that problem you want a smaller model, or MatFormer-style nesting inside the network (Chapter 9)
Your corpus is small enough to brute-forceAt a hundred thousand vectors nothing here matters. Complexity you do not need is a cost
You need lossless reconstruction of the original embeddingTruncation is lossy and irreversible. If you need the full vector back, store the full vector
You cannot retrain or fine-tune the encoderChapter 4's frozen-backbone result: matryoshka heads on a frozen encoder are linear probes. Use PCA and accept its ceiling

What the paper does not tell you, and where to look instead

Reading a paper well includes noticing its silences. None of these are criticisms — a conference paper has a page limit — but each is a question you will need answered and will not find there.

Question the paper does not settleWhere the answer actually comes from
How was M chosen? Was anything else tried?Nowhere published. Run the ablation yourself; it is one list
What happens on text retrieval at MTEB scale?The open-weight follow-ups — Nomic, mixedbread, and the model cards of the commercial APIs
What hardware produced the 14×?Reproduce the Amdahl decomposition from Chapter 6 on your own fleet. The fraction transfers; the number does not
How much worse is MRL-E, precisely, per granularity?Sparsely reported. If L is large enough that you care, measure it
Does a schedule on cm help?Open. Chapter 2's Lagrangian view says how to try it
What does the ordering look like semantically?Open, and probably the most interesting unanswered question here

The third row generalizes into a habit worth keeping. Wall-clock speed-ups are measurements of a system, not properties of a method, and they do not survive a change of hardware, index library, batch size, or baseline. What transfers is the structure of the result — that the gain is dimension-proportional up to an Amdahl fraction you can measure yourself in an afternoon. Take the mechanism from a paper and the numbers from your own machine.

What best explains why MRL was adopted into commercial embedding APIs within about eighteen months, unlike most representation-learning results?

Chapter 8: Matryoshka × Quantization

An embedding's storage cost is a product of two independent numbers: how many coordinates and how many bits each coordinate takes. Matryoshka attacks the first. Quantization attacks the second. They are orthogonal, so they multiply — and the modern serving stack uses both.

bytes per vector = m × (bits per coordinate) / 8
MRL shrinks m · quantization shrinks the second factor · nothing couples them

Scalar quantization, derived

Quantization means replacing a continuous value with one of a finite set of levels. The simplest useful version, affine scalar quantization, maps a real range onto the 256 values an unsigned byte can hold.

Pick a range [a, b] covering the values a coordinate takes. Define the step size and the maps:

Δ = (b − a) / 255
quantize:  q = round( (x − a) / Δ )   ∈ {0, …, 255}
dequantize:  x̂ = a + q Δ

The rounding error is uniform on [−Δ/2, +Δ/2], so its root-mean-square is Δ/√12 = Δ/3.464. (A uniform distribution on an interval of width w has variance w2/12 — that is the standard result and the only fact you need.)

Worked example. A 1024-dimensional unit-norm embedding. Each coordinate has typical magnitude around 1/√1024 = 0.031, so a range of roughly ±4 typical magnitudes covers essentially everything: [a, b] = [−0.15, +0.15].

Δ = 0.30 / 255 = 0.001176
RMS error per coordinate = 0.001176 / 3.464 = 0.000340

Relative to a typical coordinate magnitude of 0.031 that is 1.1% error per number, which sounds alarming. It is not, and here is the derivation that shows why. The quantity you actually care about is the dot product, and the errors in it partially cancel. Writing σe for the per-coordinate error standard deviation and treating errors as independent:

x̂ · ŷ − x · y  ≈  ∑i ( eix yi + xi eiy )
Var = σe2i yi2 + σe2i xi2 = σe2(‖y‖2 + ‖x‖2) = 2σe2
std of the cosine error = σe√2 = 0.000340 × 1.414 = 0.00048

Five ten-thousandths of a cosine unit. Score gaps that decide rankings are typically in the hundredths. int8 quantization is, for retrieval purposes, free — four times less memory for an error two orders of magnitude below what matters. Notice also that the error does not grow with dimension: the unit-norm constraint means adding coordinates shrinks each one, and the two effects cancel exactly.

Binary quantization, derived

Push harder. Keep one bit per coordinate: is it positive?

bi = 1 if xi > 0, else 0  — 32× smaller than float32

Comparison becomes Hamming distance: XOR the two bit strings and count the set bits. A modern CPU does 64 bits per popcount instruction, so a 1024-dimensional comparison is 16 instructions instead of 1024 multiply-adds — and reads 128 bytes instead of 4,096.

Why does counting sign agreements estimate an angle? The classic result behind SimHash: for two vectors separated by angle θ, a random hyperplane separates them with probability θ/π. If the embedding's coordinates behave isotropically, each coordinate's sign acts like such a hyperplane test, so:

E[ fraction of differing bits ] = θ/π

Each bit is a Bernoulli trial, so with m bits the estimate has a binomial standard error. Worked example at cosine 0.8, i.e. θ = 0.6435 rad = 36.87°, so p = θ/π = 0.2048:

m = 1024:  std of the count = √(1024 × 0.2048 × 0.7952) = √166.8 = 12.9 bits
    → std of the fraction = 12.9/1024 = 0.0126  → std of θ = 0.0126 π = 0.0396 rad
    → std of cos θ ≈ sin(θ) · 0.0396 = 0.6 × 0.0396 = 0.024

Fifty times worse than int8's 0.00048, but still small next to the score gaps that decide a top-10 list — which is why binary quantization with a rescoring pass works well in practice. Now do it at 256 dimensions:

m = 256:  √(256 × 0.2048 × 0.7952) = √41.7 = 6.46 bits
    → fraction std = 6.46/256 = 0.0252  → θ std = 0.0792 rad  → cos std = 0.048
And there is the coupling everyone assumes does not exist. The binary estimator's error scales as 1/√m, so cutting the dimension in half multiplies the score noise by √2. Matryoshka and binary quantization are orthogonal in memory — the bytes really do multiply — but they are not orthogonal in quality, because the binary estimator was already spending its accuracy budget on having only one bit per coordinate, and MRL then takes away the coordinates that were averaging the noise down. Binary at 1024 dimensions is excellent. Binary at 64 dimensions is 64 bits of total information about a document, and it behaves like it.

The memory ladder

One million documents, native dimension 1024. Every row is a real configuration someone ships.

DimensionPrecisionBytes per vectorIndex sizeReduction
1024float324,0964.10 GB
1024float162,0482.05 GB
1024int81,0241.02 GB
256float321,0241.02 GB
256int8256256 MB16×
1024binary128128 MB32×
256binary3232 MB128×
64binary88 MB512× — and now the quality cliff is real

Verify the headline row: 1,000,000 × 32 bytes = 32,000,000 bytes = 32 MB, against 4.10 GB for the naive configuration. A corpus that needed a dedicated machine now fits in a CPU's last-level cache neighbourhood. That 128× is the product of 4× from matryoshka and 32× from binarization, and neither knew the other existed.

The two-axis shrink

Dimension across, bits per coordinate down. Each cell shows the index size for your chosen corpus and a modelled score-noise estimate from the derivations above — brighter means more usable. The green outline is the configuration you have selected; the dotted region is where the binary estimator's 1/√m error term has taken over and further truncation stops being worth it.

Corpus size 1M
Dimension 256
Precision int8

Walk the grid diagonally from the top-left and notice that the two axes are not symmetric. Going from float32 to int8 costs almost nothing at any dimension. Going from 1024 to 256 costs a little. Going to binary costs a lot at low dimension and little at high dimension. That asymmetry is the whole guidance: quantize aggressively at high dimension, truncate aggressively at high precision, and be careful about doing both at once.

The calibration trap that is specific to matryoshka

Here is a bug that only exists because MRL front-loads magnitude, which makes it exactly the kind of thing a team discovers three weeks after shipping.

Suppose you quantize with a global range shared by all coordinates — a single (a, b) pair for the whole vector. On an ordinary embedding that is fine, because all coordinates have similar scale. On an MRL embedding they emphatically do not. Say coordinate 1 ranges over ±0.40 while coordinate 900 ranges over ±0.02.

global range [−0.40, +0.40] ⇒  Δ = 0.80/255 = 0.003137
coordinate 900 spans 0.04, which is 0.04 / 0.003137 = 12.7 levels of the available 256

The tail coordinates get about 3.7 bits of the 8 you paid for, and they are the coordinates carrying the fine distinctions that motivated keeping 1024 dimensions in the first place. You bought a full-precision tail and quantized it down to a coarse one.

The fix, in one line. Calibrate per dimension: store a separate (aj, bj) for each coordinate j, computed from a sample of the corpus. That is 2d extra floats total — 8 KB for a 1024-dimensional model, once, for the whole index — and every coordinate gets all 256 levels. And calibrate on the truncated prefix you actually serve, not on the full vector, so that the ranges reflect the distribution you will quantize.

The third axis: product quantization, and how it relates

Scalar quantization treats each coordinate independently. Product quantization (PQ) does something cleverer: it chops the vector into chunks and replaces each chunk with the id of its nearest entry in a learned codebook.

Concretely for d = 1024: split into 64 subvectors of 16 dimensions each. For each subspace, run k-means over a sample of the corpus to get 256 centroids. Now a document is 64 bytes — one centroid id per subspace.

64 subspaces × 1 byte = 64 bytes per vector, versus 4,096 → 64× smaller
codebooks = 64 × 256 centroids × 16 dims × 4 B = 1,048,576 B = 1 MB, once, for the whole index

And the query trick is elegant: precompute a 64 × 256 table of distances from the query's subvectors to every centroid, then each document's distance is 64 table lookups and 63 adds — no multiplications at all.

Product quantizationMatryoshka truncation
What it removesPrecision, jointly across a chunk of coordinatesCoordinates entirely
Typical reduction32–64×4–12×
Fitted artifactCodebooks — per corpus, per model versionNone
Requires retraining the encoderNoYes
Query costA 16k-entry lookup table per query, then table lookupsA slice
ImplementationNontrivial — asymmetric distance, SIMD lookup kernelsFewer bytes in a memcpy
Composes with the other?Yes — truncate to 256, then PQ into 16 subspaces → 16 bytes per vector, 256×

PQ is more powerful per byte and MRL is more powerful per unit of engineering effort. They are complements, not competitors: PQ is a post-hoc, fitted, corpus-specific artifact that squeezes precision; MRL is a training-time property that removes dimensions and needs nothing at serving time. The reason MRL spread faster is the “fitted artifact” row.

The quantizers, in code

python — per-dimension int8, and binary with rescoringimport numpy as np

def fit_int8(Z):
    """Z: (n_sample, m) of the TRUNCATED prefix you will actually serve."""
    lo = Z.min(0); hi = Z.max(0)          # (m,) each - PER DIMENSION
    return lo, (hi - lo) / 255.0              # 2m floats total, 2 KB at m=256

def to_int8(Z, lo, step):
    return np.clip(np.round((Z - lo) / step), 0, 255).astype(np.uint8)

def from_int8(Q, lo, step):
    return lo + Q.astype(np.float32) * step

# A GLOBAL range would look like this - and it is the bug:
#   step = (Z.max() - Z.min()) / 255.0     # ONE scalar
# On a matryoshka embedding coordinate 1 spans +-0.40 and coordinate 900
# spans +-0.02, so the tail gets ~13 of the 256 levels. Per-dimension fixes it.

def to_binary(Z):
    return np.packbits(Z > 0, axis=-1)         # (n, m) -> (n, m//8) uint8

def search_binary_then_rescore(qb, qf, B, F32, K=1000, k=10):
    """B: (N, m//8) packed bits. F32: (N, m) full precision, on SSD."""
    ham  = np.unpackbits(B ^ qb, axis=-1).sum(-1)   # popcount per row
    cand = np.argpartition(ham, K)[:K]
    return cand[np.argsort(-(F32[cand] @ qf))[:k]]

# Hamming -> cosine, when you need a comparable score from stage 1:
#   theta_hat = pi * hamming / m ;  cos_hat = np.cos(theta_hat)
# Its std is about 0.76/sqrt(m) - fine for shortlisting, useless as a threshold.

The last comment is Chapter 6's threshold warning wearing a different hat. A Hamming-derived cosine is an estimator with a known, dimension-dependent standard error, so it is perfectly good for “are these among the closest thousand” and hopeless for “is this above 0.75.” Use each score for the question its noise level can answer.

A gallery of ways this goes wrong

Every failure below has been shipped by somebody. They are collected here because each one produces plausible-looking output rather than an error, which is the worst property a bug can have.

MistakeWhat you seeWhy it happens
Global quantization range on a matryoshka vectorA small, diffuse quality loss that ablations cannot localizeThe tail coordinates lose most of their levels. Nothing errors; the numbers just get coarser where it matters
Calibrating the quantizer on the full vector, then serving a prefixSame, milderThe prefix's value distribution is not the full vector's. Calibrate on what you serve
Normalizing before truncatingCertain documents dominate every result listFront-loaded documents keep more of their norm, so their truncated vectors are longer and win every dot product
Comparing a binary Hamming score against a float thresholdResult counts swing wildly between queriesThey are different estimators with different noise floors. Convert, or use ranks
Reusing a relevance threshold across dimensionsToo few results at low m, too many at high mThe chance-similarity floor is 1/√m
Rebuilding the shortlist index from stale archive rowsA slowly growing set of documents that can never be retrievedOrdering bug in the pipeline. Rebuild from the archive after it settles, not during
Binary at 64 dimensions because “both are cheap”Retrieval that looks random on hard queries64 total bits per document. The 1/√m term has eaten everything

Six of the seven share a root cause worth naming: a step in the pipeline assumed something about the representation that matryoshka training deliberately made false — that coordinates are interchangeable, that norms are comparable, that scores mean the same thing at every width. When you adopt an anisotropic representation, audit every downstream component for an isotropy assumption. Most of them have one, and none of them will tell you.

Which axis do you shrink first?

Your situationDo this firstWhy
Index fits in RAM, latency is fineNothingComplexity you do not need is a cost
Index is 2–4× too big for RAMint8, per dimension4× for essentially zero quality cost and no retraining
Index is 10–30× too bigMRL truncation, then int8Both are cheap in quality; together they are 16–48×
Index is 100×+ too bigBinary at full width, with rescoring32× from bits while keeping m large, so the 1/√m term stays small
You cannot retrain the encoderint8, then PQBoth are post-hoc. MRL is not available to you
Encoder latency is the bottleneckNone of thisWrong axis entirely — see Chapter 9

A full int8 round trip, on real numbers

Abstract error bars are less convincing than one number going in and coming out. Take a 256-dimensional matryoshka prefix and quantize two of its coordinates — an early one and a late one — both ways.

Calibrated on a corpus sample, the two coordinates have very different ranges, which is precisely the matryoshka signature:

coordinate 1:  range [−0.42, +0.44]    coordinate 200:  range [−0.028, +0.031]

Per-dimension calibration. Each coordinate gets its own step:

dim 1:  Δ = 0.86/255 = 0.0033725
x = 0.31 → q = round((0.31 + 0.42)/0.0033725) = round(216.47) = 216
x̂ = −0.42 + 216(0.0033725) = −0.42 + 0.72846 = 0.30846,  error = 0.00154
dim 200:  Δ = 0.059/255 = 0.00023137
x = 0.012 → q = round((0.012 + 0.028)/0.00023137) = round(172.88) = 173
x̂ = −0.028 + 173(0.00023137) = −0.028 + 0.0400271 = 0.0120271,  error = 0.0000271

Global calibration — the bug. One range for everything, [−0.42, +0.44], step 0.0033725. Coordinate 1 is unaffected. Coordinate 200:

q = round((0.012 + 0.42)/0.0033725) = round(128.10) = 128
x̂ = −0.42 + 128(0.0033725) = −0.42 + 0.43168 = 0.01168,  error = 0.00032

Twelve times the error, on the coordinates that were the whole reason you kept 256 dimensions instead of 64. And the structural version of the same fact: coordinate 200's entire range spans

0.059 / 0.0033725 = 17.5 levels of the 256 you paid for — about 4.1 bits of the 8

Every tail coordinate is being stored at half the precision you are being charged for. The fix is 2d extra floats — 2 KB at m = 256, for the entire index — and it is not optional on a matryoshka embedding the way it nearly is on an ordinary one.

A useful way to remember it. Global quantization calibration assumes all coordinates have similar scale. MRL's whole purpose is to make that assumption false. Any technique downstream of a matryoshka embedding that implicitly assumes coordinate exchangeability — global quantization ranges, uniform dropout on the embedding, isotropic noise for privacy, equal-weight dimension sampling — needs re-examining, because the representation is deliberately anisotropic now.

The real decision is which memory tier you land in

The tables above rank configurations by bytes, which makes the trade-off look continuous. It is not. Memory is a hierarchy with hard steps between tiers, and the only question that changes your latency by an order of magnitude is which tier your index falls into.

TierTypical capacityLatencyBandwidth
L2 cache1–2 MB per core~4 nsenormous
L3 cache32–256 MB shared~15 ns~400 GB/s
Local RAM64 GB – 2 TB~90 ns~50–200 GB/s
NVMe SSDterabytes~80,000 ns~3 GB/s
Network / object storeunboundedmillions of ns~1 GB/s

Now place the configurations for one million documents:

ConfigurationSizeLands inConsequence
1024 dims, float324.10 GBRAM, comfortablyBandwidth-bound scan: 4.10 GB at 50 GB/s = 82 ms
256 dims, int8256 MBRAM, but streams fast5.1 ms — sixteen times less to read
1024 dims, binary128 MBBorderline L3 on a large server2.6 ms from RAM, and a large fraction may stay cached
256 dims, binary32 MBL3 cacheThe whole index sits in cache. Scans run at ~400 GB/s: 0.08 ms

The last row is not sixteen times faster than the second, it is roughly sixty times faster, because it crossed a tier boundary rather than moving along a line. Fitting an entire retrieval index in last-level cache changes what kind of system you are operating.

So the design question is not “how small can I make it.” It is “which threshold am I closest to crossing, and what would it take?” Going from 4.10 GB to 2.05 GB is worth exactly the bandwidth ratio and nothing more. Going from 40 MB to 32 MB may be worth a factor of sixty, because it moved you from RAM into cache. Chapter 6 made the same argument one level up, about fitting on one machine instead of two. Every level of the memory hierarchy has a cliff, and the value of a byte saved depends entirely on which cliff you are standing next to.

Rescoring: the same cascade, one axis over

Chapter 6 built a cascade over dimensions. Precision gives you a second axis for exactly the same trick, and the two compose into the stack most modern retrieval systems now run.

StageRepresentationOverCost per documentKeeps
1. Sweepbinary, full dimensionall N16 popcount instructions, 128 bytes readtop 1,000
2. Rescoreint8, truncated to 2561,000256 integer MACs, 256 bytes readtop 100
3. Exactfloat32, full dimension1001024 float MACs, 4 KB readtop 10

Each stage is cheap because the previous stage made its input small, and each is accurate enough because it only has to preserve the true answers rather than rank everything correctly. That is the single principle running through this entire lesson: an approximation does not need to be right, it needs to be right about what it forwards.

Total cost is dominated by stage 1, which touches 128 bytes per document instead of 4,096 — and 128 MB of hot index instead of 4.1 GB. Stages 2 and 3 together touch 1,000 × 256 + 100 × 4,096 = 256,000 + 409,600 = 665,600 bytes, less than a megabyte, which is nothing.

The other precisions: fp16, fp8, int4

Chapter 8 has treated precision as three points — float32, int8, binary — because those are the three that dominate. The full ladder is worth having, along with the one non-obvious ordering result on it.

Rerun the scalar-quantization derivation for four bits. Sixteen levels over a per-dimension range of 0.30:

Δ = 0.30/15 = 0.02   RMS error = 0.02/3.464 = 0.005774
cosine error std = 0.005774 × √2 = 0.00817

Seventeen times worse than int8 — exactly as expected, since each dropped bit doubles the step and doubles the error, and 8 to 4 bits is a factor of 24 = 16 in step size.

PrecisionBytes per 1024-dim vectorCosine error σNotes
float324,096~0The reference
float162,048~0.00001Free. There is no reason to store float32 embeddings
float8 (e4m3)1,024~0.0015Worse than int8 at the same size — see below
int8, per dimension1,0240.00048The sweet spot. 4× for nothing
int4, per dimension5120.0082Usable with rescoring; needs careful per-group scales
binary1280.02432×, and the error grows as 1/√m
The result worth remembering: int8 beats float8 at identical size. Both are eight bits, but float8 spends four of them on an exponent so it can represent values across many orders of magnitude — a range you do not need, because you calibrated per dimension and know exactly where the values live. Int8 spends all eight bits on resolution inside that known range. Dynamic range is only worth paying for when you do not know the range. For a calibrated embedding index you do know it, so buy resolution instead. This is the same “optimize the right objective” lesson as Chapter 3's PCA critique, wearing a hardware costume.
Matryoshka truncation and binary quantization multiply cleanly in memory (4× × 32× = 128×). Why do they not compose cleanly in quality?

Chapter 9: Two Dimensions, Open Questions, and the Cheat Sheet

MRL nests along one axis: the width of the representation. Once you see that a nesting constraint is cheap, the obvious question is which other axes admit the same trick. The answer, as of 2026, is: most of them, and the follow-up literature is essentially a catalogue of axes.

Axis two: depth

A Transformer encoder with 12 layers computes 12 successive representations of its input, and you throw away the first 11. But a layer is a budget just as a dimension is: exiting at layer 6 costs half the compute. So nest over layers as well as over widths.

That is the idea behind 2D Matryoshka approaches (published as 2D Matryoshka Sentence Embeddings, and related work under the name Espresso Sentence Embeddings). The objective becomes a double sum:

L = ∑ℓ ∈ Layersm ∈ M cℓ,m · Loss( head(ℓ,m)( h(ℓ)1:m ), y )

Where h(ℓ) is the pooled output of layer ℓ. If you nest over 4 exit layers and 7 widths you get 28 operating points from one training run — a two-dimensional menu of (latency, memory) trade-offs, all sharing one geometry.

AxisWhat nestsWhat it buysRepresentative work
Width of the embeddingCoordinates of zIndex memory, retrieval bandwidthMRL (this paper)
Depth of the encoderWhich layer you readEncoder latency — the thing MRL does not help with2D Matryoshka / Espresso embeddings
Width of the FFNHidden units inside each blockWhole nested submodels extractable from one trained networkMatFormer
Bits per numberPrecision levelsOne model serving int8, int4, and int2Matryoshka quantization

MatFormer is the most striking of these because it moves the nesting inside the network rather than onto its output. Each Transformer block's feed-forward layer is trained so that its first k hidden units form a working FFN on their own, for several values of k. Because the choice can be made independently per layer, you can then “mix and match” a submodel per deployment target — hundreds of extractable models from one training run, all with consistent behaviour because they share weights. This line of work feeds directly into shipped on-device model families that expose a smaller nested variant of a larger model.

The pattern, stated generally. Take any capacity axis of a model — width, depth, precision, number of experts, sequence resolution. Add loss terms that force prefixes along that axis to be independently usable. You convert a discrete model-selection problem (“which of my three models do I deploy?”) into a continuous runtime parameter (“how much of my one model do I run?”). MRL's contribution was demonstrating that the conversion is nearly free on the axis where it matters most for serving cost.

The four-year arc, briefly

WhenWhat happenedWhat it established
2022MRL published — nested embeddings for vision, language, and vision-languageThat the nesting constraint is nearly free at full width
2023MatFormer moves nesting inside the Transformer's feed-forward widthThat the same objective shape works on a capacity axis inside the network
2024Commercial APIs expose a dimensions parameter; open MRL-trained embedders ship with documented truncation points; 2D variants nest over depthThat the deployment cost is low enough to become a default rather than an option
2025–2026Nested precision, and on-device model families shipping an extractable smaller variant of a larger oneThat “elastic by construction” is becoming an expectation of a model rather than a feature of one

Read the right-hand column as one sentence. In four years the field moved from “here is a clever training objective” to “a model that cannot be run at several sizes is a model with a missing feature.” That is a fast transition for an idea whose entire content is a sum over a loss, and it happened because the transition cost was near zero at every step.

MatFormer, mechanically

Because it is the most consequential descendant, it is worth seeing how the trick moves inside the network. A Transformer block's feed-forward layer is

FFN(x) = W2 · σ( W1 x ),   W1Rdff×d,  W2Rd×dff

with dff typically 4d. The hidden units are exchangeable — permuting them and permuting W2's columns to match gives an identical function — which is precisely the rotation-invariance situation of Chapter 1, one level in. So nest them: for each k in a set of widths, define a sub-FFN using only the first k hidden units,

FFN(k)(x) = W2[:, 1:k] · σ( W1[1:k, :] x )

and add a loss term for the whole network run with every block using width k. Same objective shape, same multiplicity argument, same near-free constraint. The payoff is different, though, and bigger in one specific way: because each block's width can be chosen independently at extraction time, a network with B blocks and W widths per block yields WB possible submodels, all sharing weights and all behaving consistently because they were trained jointly. In practice you pick a handful along the accuracy–latency frontier.

And notice which cost each one attacks. MRL shrinks the stored embedding, so it pays off once per document — a billion times. MatFormer shrinks the network, so it pays off once per inference. In a retrieval system the first dominates; in an on-device assistant the second does. The two techniques are the same idea aimed at opposite ends of the cost structure, which is why the same research group produced both and why modern systems increasingly ship both.

What is genuinely still open

A good paper leaves better questions than it answers. These are the ones this one leaves.

QuestionWhat we knowWhat we do not
How should M be chosen?Exponential spacing works; more granularities cost almost nothingWhether the spacing should adapt to the task's intrinsic dimension, and whether granularities below some threshold actively hurt the ones above them
Are uniform cm optimal?They work, and the multiplicity staircase already provides an implicit tilt toward small mWhether a schedule — emphasize large granularities early, small ones late — would give a better frontier, and whether the answer depends on the intrinsic dimension of the data
Why is the constraint so cheap?Empirically the full-dimension cost is near zeroA quantitative account. The redundancy and intrinsic-dimension story is a hand-wave; there is no theory predicting the cost from properties of the data
Is the ordering semantically interpretable?The coarse-to-fine reading is well motivated by the multiplicity argumentWhether coordinate blocks correspond to nameable concept hierarchies, and whether that could be steered deliberately
How does it interact with ANN index structure?Small indexes build faster and traverse with fewer cache missesWhether an ANN graph should be built at the shortlist dimension or the full one, and whether a nested index — a graph whose edges are valid at several granularities — is constructible
Does it hold for decoder-derived embeddings?MRL transfers across CNNs, ViTs, and encoder TransformersWhether embeddings pooled from large decoder-only models nest as cleanly, given how differently their representation spaces are shaped
What happens under heavy distribution shift?Robustness tracks the baseline on standard ImageNet shift suitesWhether the ordering degrades faster than overall quality under shift — that is, whether the truncatability is less robust than the representation

What would make this obsolete

Every technique has a world in which it stops mattering, and naming that world is a good test of whether you understand what the technique is for. Four candidates, with an assessment of each.

If this happenedMRL would…Likely?
Memory becomes effectively freeLose most of its value — the entire motivation is the byte billNo. Corpora have grown faster than memory has cheapened for a decade, and retrieval corpora are growing fastest of all
Learned indexes make retrieval cost independent of dKeep its storage benefit, lose the latency argumentPartly true already — ANN weakens the latency argument, which is why Chapter 0 spent a section showing that resident memory is the real constraint
Late-interaction retrieval (many vectors per document) winsBecome more important, not lessPlausible, and note the direction: storing dozens of vectors per document multiplies the index size, so per-vector dimension matters more, not less
Generative retrieval replaces vector search entirely — a model emits document ids directlyBecome irrelevant for retrieval; survive for clustering, dedup, and any use that still needs a vectorThe genuine threat, and an open research direction. It would remove the index rather than shrink it

The third row is worth sitting with because it inverts the usual intuition. In late-interaction schemes a document is represented by one vector per token rather than one per document, which multiplies the index by a factor of a hundred or more. That makes per-vector dimension the dominant cost term, and a nested representation the obvious response. Techniques do not only get obsoleted by the future; sometimes the future makes them load-bearing.

A glossary of everything this lesson bolded

TermOne lineFirst appeared
MACMultiply-accumulate — one multiply plus one add, the unit of dot-product costCh 0
Matryoshka embeddingA vector whose every prefix is itself a complete embeddingCh 1
Prefix / sliceThe first m coordinates — a pointer and a length, not a computationCh 1
Symmetry groupThe transformations a loss cannot see. Rotations, for an ordinary embedding lossCh 1
Intrinsic dimensionThe smallest number of coordinates that parameterizes the data manifoldCh 1
GranularityOne size at which the representation must work; an element of the nesting set MCh 1
LogitsUnnormalized scores, just before a softmaxCh 2
MRL-EThe variant tying every head to column slices of one matrixCh 2
MultiplicityHow many loss terms contain coordinate j — the source of the orderingCh 2
Signal / nuisanceThe part of a representation that varies with the label; everything elseCh 3
Discriminability index d′Class separation measured in units of within-class noiseCh 3
PCAThe orthogonal directions of maximum variance, largest firstCh 3
LDADirections maximizing between-class over within-class scatter; rank capped at L − 1Ch 3
Johnson–LindenstraussRandom projections preserve distances if m ≥ 8 ln(n)/ε2Ch 3
Orthogonal projectionWhat a prefix is, geometrically — casting a shadow onto a coordinate subspaceCh 5
Adaptive retrievalSweep at a small dimension, rerank the shortlist at a large oneCh 6
Funnel retrievalThe cascade that halves the candidates and doubles the dimension each rungCh 6
Recall@kThe fraction of the true top-k that survived the shortlist — the pipeline's ceilingCh 6
Amdahl's lawSpeed-up is capped by the fraction of work you did not speed upCh 6
MTEBThe Massive Text Embedding Benchmark — the shared yardstick for text embeddersCh 7
QuantizationReplacing a continuous value with one of a finite set of levelsCh 8
Hamming distanceNumber of differing bits — a popcount, not a dot productCh 8
Product quantizationChunk the vector, replace each chunk with a learned centroid idCh 8
RescoringRe-ranking a cheap stage's shortlist with a more faithful representationCh 8

Five experiments you could actually run this week

The open questions above are large. These are small, and each one has a chance of producing a real answer on a single GPU.

1. Does the intrinsic dimension predict the tax? Train MRL on several datasets whose intrinsic dimension you can estimate (a nearest-neighbour or maximum-likelihood estimator on the representations will do), and plot the full-width quality gap against that estimate. The hypothesis from Chapter 1 is that the gap shrinks as redundancy grows. Nobody has published the scatter plot.

2. Where exactly does the ordering stop? Train with M = {64, 128, …} and evaluate at m = 8, 16, 32 — strictly below the smallest granularity. Chapter 1 argues there should be no ordering inside the first block. Does quality fall off a cliff at 64, or does it decay smoothly because the pressure staircase leaks? The answer tells you how conservative to be when choosing M.

3. Does a curriculum beat a sum? Train phase one with only m = d, then phase two with the full nested objective, and compare the frontier against joint training at matched total compute. If the two-phase version wins, MRL becomes a cheap fine-tune of any existing model and the retrofitting story in Chapter 4 changes completely.

4. Is the ordering robust under shift? Take a matryoshka model, measure the truncation curve on in-distribution data and on a shifted set, and compare the gaps rather than the absolute numbers. Chapter 7 reports that robustness tracks the baseline; nobody has asked whether truncatability specifically degrades faster than quality.

5. Build the index at which dimension? Construct an HNSW graph at 16 dimensions and another at 2048, then use each to shortlist and rerank at full width. The 16-dimensional graph is far cheaper to build and traverse, but it encodes a coarser notion of neighbourhood. Does the cheap graph lose recall, and by how much? This is a two-day experiment with an immediately actionable answer.

What every one of these has in common. They are all cheap because MRL is cheap. The whole method is a change to a loss, so an ablation is a change to a list. That is not an accident of this particular paper — it is a property of ideas that live in the objective rather than in the architecture, and it is a good reason to prefer them.

The cheat sheet — every symbol

SymbolMeaningShape / typical value
xiAn input example(3, 224, 224) image, or (L,) token ids
F(·; θF)The backbone — everything producing the shipped vectorResNet50, ViT-B/16, BERT-base
z = F(x)The representationRd; d = 2048 (ResNet50), 768 (BERT)
dNative representation dimension2048 / 768 / 1024 / 3072
MThe nesting set of granularities{8, 16, …, 2048}; |M| = 9
mOne granularityan element of M
z1:mThe prefix — a zero-cost viewRm
W(m)Granularity head; under MRL-E it is W:,1:mRL×m
LNumber of classes (or the loss function — context disambiguates)1000 for ImageNet-1K
cmRelative importance of granularity m1 for all m in the paper
μ(j)Multiplicity — how many granularities contain coordinate j9 at j=1, 1 at j>1024
d′Discriminability index — class separation in noise unitsd′m = d′ · ‖u1:m
Ds, DrShortlist and rerank dimensions in adaptive retrieval16 and 2048
KShortlist size handed to the rerank stage200 (10–20× the returned k)

Every equation, in order

(1)  min{W(m)}, θF (1/N) ∑im∈M cm L( W(m) F(xi)1:m , yi )
the objective — one backbone pass, |M| heads, one scalar
(2)  W(m) = W:, 1:m  (MRL-E)
tie the heads to column slices of one matrix; head cost returns to the baseline's
(3)  ∂L/∂zj = ∑m ≥ j cmk (p(m)k − [k=y]) W(m)k,j
the multiplicity sum — the entire mechanism lives in the index set m ≥ j
(4)  d′m = d′ · ‖u1:m‖,   democratic case ‖u1:m‖ = √(m/d)
why ordinary embeddings do not truncate: √(64/768) = 0.289
(5)  Cadaptive = N · Ds + K · Dr  ≈  (Dr/Ds)−1 · Cexhaustive
the two-stage cost model; the approximation holds when the rerank term is small
(6)  speed-up = 1 / [ (1 − f) + f · Ds/Dr ]
Amdahl — why 128× in FLOPs is 14× on a clock, with f ≈ 0.936

The numbers worth remembering

NumberWhat it is
4088 = 8+16+…+2048Total granularity width for the ResNet50 nesting set — just under 2× its largest element, which is why nine heads are cheap
0.20%Share of a ResNet50 forward pass consumed by all nine heads (0.0082 of 4.1 GFLOP)
+7.98%Parameter overhead of MRL on ResNet50. MRL-E: zero
9.22 MBExtra activation memory for nine granularities at batch 256, L = 1000
√(m/d)Fraction of discriminability a democratic embedding keeps under truncation. 0.289 at 64 of 768
3.11×Gradient amplification on coordinate 1 in the three-granularity toy — roughly |M|
125.5×Theoretical retrieval speed-up at Ds=16, K=200, Dr=2048, N=1.28M
14×The measured wall-clock speed-up — and 15.6× is the hard Amdahl ceiling
44,032 MACThe whole seven-rung funnel cascade — 0.2% of the shortlist sweep
82 MB vs 10.50 GBShortlist index vs full index for ImageNet-1K at 16 and 2048 dims
0.00048 / 0.024Cosine-score noise from int8 and from binary quantization at 1024 dims
128×Memory reduction from 256-dim binary versus 1024-dim float32 — 4× × 32×
1/√mStd of the cosine between two random unit vectors — why thresholds must be recalibrated per granularity

Build it yourself — the checklist

StepWhat to doThe decision that matters
1. Pick the vectorIdentify exactly the tensor your serving system storesApply MRL there, not to an internal projection you discard. This is the most common wasted run
2. Pick MHalve from d down to the smallest size any consumer will ask forInclude sizes your infrastructure likes. Each granularity costs one head
3. HeadsOne nn.Linear per granularity, no bias. Linear, never an MLPThe head must match the downstream reader, which is linear or a cosine
4. LossSum the per-granularity losses with cm = 1If contrastive: slice first, normalize second. Every time
5. ScheduleEverything else stays exactly as it wasNo new hyperparameters. That is the point
6. LogEvery granularity's loss as its own scalarLsmall pinned at ln(L) means a slicing bug, and one scalar hides it
7. Smoke testPlot mean squared coordinate magnitude against index on held-out dataDecreasing means it worked. Flat means it did not
8. Evaluate per granularityFull downstream metrics at every m in MLoss at m is not retrieval quality at m. Measure what you serve
9. Recalibrate thresholdsA table of cutoffs keyed by mThe chance-cosine floor is 1/√m. A hard-coded 0.75 is a bug
10. ComposeAdd per-dimension int8, then consider binary + rescoringCalibrate per dimension, on the truncated prefix you actually serve

Should your model be matryoshka? A decision procedure

The honest summary of ten chapters, arranged so you can act on it.

QuestionIf yesIf no
Do you store more embeddings than you can comfortably hold in RAM?ContinueStop. You have no problem to solve
Do at least two consumers want different quality/cost points?ContinueTrain natively at the one dimension you serve
Can you retrain or meaningfully fine-tune the encoder?ContinueUse per-dimension int8, then PQ. Chapter 3's ceiling applies to you
Is your downstream reader linear — cosine, dot product, linear probe?ContinueReconsider — MRL's guarantee is about linear readers
Is your bottleneck the index rather than the encoder?Use MRLYou want depth or width nesting inside the network instead

And the summary of the summary: MRL is a cheap way to buy optionality on the one axis that governs serving cost, paid for with a constraint that turns out to be nearly free because learned representations are redundant. It is not an accuracy technique. It is a technique for making one artifact serve requirements that used to need several, and for letting the cheap version legitimately hand work to the expensive one.

Where to go from here

If you want…Go to
The foundations of what an embedding isVector embeddings and embedding layers
The geometry of cosine, dot product, and friendsSimilarity metrics
PCA properly, including the variance-versus-discriminability storyPCA
The index structures Chapter 6 keeps referring toVector databases
The retrieval system this all feedsRAG and multimodal RAG
The contrastive objectives production embedders actually train withContrastive learning, CLIP, SimCSE, CLAP
Quantization in depth — Chapter 8's other axisQuantization I, Quantization II, sparsity and quantization
Quantization built jointly with search — the closest cousin of this paperJoint search and quantization
How dimension was chosen before anyone could choose it at runtimeOn the dimensionality of word embeddings

The whole thing, in one paragraph

If you have to explain this to a colleague in an elevator: an ordinary embedding spreads its information evenly across all its coordinates, because nothing in the training objective ever expressed a preference — the loss is invariant to rotations of the representation space, and truncatability is not. So cutting a 768-dimensional vector to 64 numbers keeps only √(64/768) = 29% of the discriminability and the retrieval collapses. Matryoshka Representation Learning fixes that by adding one term to the loss for each of a handful of prefix sizes. Because coordinate j appears in every prefix of length m ≥ j, the first coordinates accumulate gradient from every term while the last accumulate from one, and the optimizer responds by loading the most broadly useful signal into the front. Nothing sorts anything; the ordering falls out of an index-set inequality. The result is that the first m numbers of the vector are a complete m-dimensional embedding for every m you trained, slicing is a pointer and a length, and every prefix lives in the same geometry — so a 16-dimensional sweep can legitimately shortlist for a 2048-dimensional rerank. That is 128× fewer FLOPs and about 14× on a clock, and it costs about 0.2% of a training step.

What to remember in five years

The specific thingThe general thing under it
Truncating a plain embedding destroys itA property your loss cannot see is a property your optimizer will not select. If you want it, write it into the objective
Nine loss terms produce a coordinate orderingStructure can emerge from an index set. Look for mechanisms that are counting arguments before reaching for architecture
PCA can land at exactly chanceOptimizing a proxy is not optimizing the objective — variance is not discriminability, reconstruction is not ranking
128× in FLOPs is 14× on a clockAmdahl decides what your optimization is worth. Measure the fraction you did not speed up before you speed anything up again
32 MB fits in L3 and 40 MB does notCosts are step functions, not lines. The value of a byte saved depends entirely on which threshold you are next to
The dimensions parameter shipped in eighteen monthsAdoption tracks integration cost, not novelty. The best idea with a migration attached loses to a mediocre one that is a config change
Cross-domain bridge
Matryoshka embeddings are progressive JPEG for meaning
Open a progressive JPEG over a slow connection and a blurry-but-complete image appears immediately, sharpening as more bytes arrive. Every prefix of the file is a viewable picture. That is not compression in the ordinary sense — a baseline JPEG's first 10% of bytes is the top 10% of the image, useless — it is an ordering of information by importance, so that any truncation of the byte stream is a valid, complete, lower-quality version of the whole. The same idea is wavelet coding in JPEG 2000, mipmaps in graphics (each level a complete texture at half the resolution), scalable video coding, and anytime algorithms in planning, which can be interrupted at any moment and asked for their current best answer. MRL puts a learned representation into that family: the first m numbers are a complete, lower-fidelity embedding, and where you stop reading is a runtime decision. Once you notice the pattern, the design question for any system becomes: is my representation progressive, or does it require all of itself to mean anything?
“What I cannot create, I do not understand.”
Take any training script you already have. Replace one nn.Linear with a list of them, replace one cross_entropy with a sum of them, and slice. You will have a matryoshka model before lunch — and the moment the truncation curve comes out flat instead of falling off a cliff, the 14× will stop being a number you read.

Reading paths through this lesson

Ten chapters is a lot to re-read. Depending on why you came back, here is the short route.

You want to…ReadRoughly
Decide whether to adopt itChapter 0's ledger, Chapter 7's caveats and bill, and the decision procedure above15 min
Understand why it worksChapter 1's rotation argument, Chapter 2's multiplicity staircase, Chapter 3's √(m/d) law25 min
Implement itChapter 2's code, Chapter 4 entirely, Chapter 5's toy to check yourself against40 min
Build the serving pathChapter 6 entirely, then Chapter 8's quantization ladder30 min
Debug something that went wrongChapter 4's diagnostics, Chapter 7's triage table, Chapter 8's failure gallery10 min
Argue with someone about itChapter 3's PCA counterexample and Chapter 6's Amdahl decomposition10 min

And if you have five minutes and a whiteboard, the two things to draw are the multiplicity staircase — nine, eight, seven, down to one, over exponentially growing blocks — and the two-stage cost model, NDs + KDr. Those two pictures generate everything else in the lesson.

References

  1. Kusupati, A., Bhatt, G., Rege, A., Wallingford, M., Sinha, A., Ramanujan, V., Howard-Snyder, W., Chen, K., Kakade, S., Jain, P., Farhadi, A. “Matryoshka Representation Learning,” NeurIPS 2022 — arXiv:2205.13147. The paper this lesson is built on.
  2. Kudugunta, S. et al. “MatFormer: Nested Transformer for Elastic Inference,” 2023 — arXiv:2310.07707. Matryoshka nesting moved inside the FFN, giving hundreds of extractable submodels.
  3. Li, X. et al. “2D Matryoshka Sentence Embeddings,” 2024 — arXiv:2402.14776. Nesting over encoder depth as well as embedding width.
  4. Nussbaum, Z. et al. “Nomic Embed: Training a Reproducible Long Context Text Embedder,” 2024 — arXiv:2402.01613. An open MRL-trained text embedder with documented truncation points.
  5. Muennighoff, N., Tazi, N., Magne, L., Reimers, N. “MTEB: Massive Text Embedding Benchmark,” 2022 — arXiv:2210.07316. The yardstick every production claim in Chapter 7 is measured against.
  6. Malkov, Y. A. & Yashunin, D. A. “Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs,” 2016 — arXiv:1603.09320. The index whose latency-bound traversal explains Chapter 6's Amdahl ceiling.
  7. Johnson, J., Douze, M., Jégou, H. “Billion-scale similarity search with GPUs” (FAISS), 2017 — arXiv:1702.08734. The reference implementation of everything in Chapter 6.
  8. Guo, R. et al. “Accelerating Large-Scale Inference with Anisotropic Vector Quantization” (ScaNN), 2019 — arXiv:1908.10396. Quantization designed for the inner product rather than for reconstruction — the same “optimize the right objective” lesson as Chapter 3's PCA critique.
  9. Charikar, M. “Similarity Estimation Techniques from Rounding Algorithms” (SimHash), STOC 2002. The θ/π result behind Chapter 8's binary-quantization error analysis.
  10. Devlin, J. et al. “BERT,” 2018 — arXiv:1810.04805; Jia, C. et al. “ALIGN,” 2021 — arXiv:2102.05918. Two of the backbones MRL was shown to transfer to.
  11. Radford, A. et al. “Learning Transferable Visual Models From Natural Language Supervision” (CLIP), 2021 — arXiv:2103.00020. The contrastive setting in which most production matryoshka embedders are actually trained.
  12. OpenAI. “New embedding models and API updates,” January 2024. The dimensions parameter, the MTEB comparison, and the instruction to re-normalize after truncating.
Exit gate — teach it back before you leave.

Without scrolling up: (1) write the MRL objective and say what each of the five symbols means; (2) explain why coordinate 1 receives more gradient than coordinate 2000, using the phrase “index set”; (3) derive why truncating a democratically-spread embedding from 768 to 64 dimensions multiplies discriminability by 0.289; (4) compute the two-stage retrieval cost for N = 1,281,167, Ds = 16, K = 200, Dr = 2048, and say why the wall-clock speed-up is 14× rather than 125×; (5) state the two-line reason binary quantization composes badly with aggressive truncation. If any of the five stalls, its chapter is one tap away.

Which sentence best captures what Matryoshka Representation Learning actually changed?