skip to content

In FAISS IndexIVFPQ, what do the m and nbits parameters control and cost?

level: middleimportance: should knowfreq 52%

answer

  1. sub-vectors and bits per sub-code
  2. stored bytes = m times nbits over 8
  3. the dimension must divide evenly
  4. 256 centroids per slice by default
  5. a compression ceiling nprobe cannot lift

basics

~20 s

m is how many sub-vectors each vector is split into and nbits the bits per sub-code, so a stored vector shrinks to about m*nbits/8 bytes. Raising either improves accuracy but costs memory, distance-computation time and training data.

solid answer

~40 s

`IndexIVFPQ(quantizer, d, nlist, m, nbits)` compresses each d-dimensional float32 vector into `m` codes of `nbits` bits each. With the usual nbits=8, each sub-vector is replaced by one byte pointing into a 256-entry codebook, so the stored size is `m` bytes instead of `4*d` — a 128-dim vector at m=16 goes from 512 bytes to 16, a 32x reduction. `d` must be divisible by `m`, since each sub-quantizer owns d/m components. Raising `m` gives finer-grained approximation and proportionally more memory and more table lookups per distance; raising `nbits` gives each sub-quantizer 2^nbits centroids instead of 256, which improves accuracy but explodes both codebook size and the training data needed, since each of those centroids must be fitted. nbits=8 is the well-optimised default; deviating is a deliberate tradeoff.

code

python · 14 lines
python
import faiss
import numpy as np

d, nlist, m, nbits = 128, 1024, 16, 8
assert d % m == 0                      # FAISS requires this

xt = np.random.random((100_000, d)).astype('float32')
quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits)
index.train(xt)
index.add(xt)

print(index.pq.code_size)   # 16 bytes per vector, vs 512 as float32
print(index.pq.ksub)        # 256 centroids per sub-quantizer at nbits=8

go deeper

for a junior

Know that PQ compresses each vector into a few bytes, that m sets how many pieces the vector is cut into, and that this trades accuracy for a large memory saving.

for a middle

Compute the memory yourself: m * nbits / 8 bytes per vector, plus codebooks. Explain the d % m == 0 constraint and why nbits=8 is the default, and describe how each parameter affects training data needs.

for a senior

Show that you treat the code size as a recall ceiling: diagnose a plateau as quantization loss and reach for a refine stage or a larger code rather than more nprobe. Size the index against a real RAM budget before choosing m.

for a principal

Own the footprint-versus-accuracy curve across the fleet: how many bytes per vector the corpus can afford at projected growth, whether a two-stage retrieve-then-rerank architecture beats a fatter index, and what a change of embedding dimension does to all of it.

## The two parameters `IndexIVFPQ(quantizer, d, nlist, m, nbits)` takes four numbers beyond the coarse quantizer. `nlist` belongs to the IVF layer. `m` and `nbits` define the product quantizer that compresses the residual vectors stored in each cell. - **m** — the number of slices a vector is cut into. Each slice covers `d/m` consecutive components and gets its own independent codebook. - **nbits** — the number of bits used to identify one entry in that codebook, so a sub-quantizer has 2^nbits centroids. The default and by far the most common value is 8, giving 256 centroids per slice and one byte per code. ## The memory math Stored size per vector is `m * nbits / 8` bytes, rounded up. That is the entire point of PQ: | d | representation | bytes per vector | |---|---|---| | 128 | raw float32 | 512 | | 128 | m=16, nbits=8 | 16 | | 128 | m=32, nbits=8 | 32 | | 768 | raw float32 | 3072 | | 768 | m=96, nbits=8 | 96 | On top of the codes there is a fixed cost for the codebooks themselves: `m * 2^nbits * (d/m)` floats, which is negligible at nbits=8 and becomes significant at 12 or 16. `index.pq.code_size` reports the per-vector byte count directly, which is the number to multiply by `ntotal` when you are sizing a machine. ## The d % m == 0 constraint FAISS requires the dimension to divide evenly by `m` and throws at construction otherwise. This bites with awkward embedding dimensions: 768 divides nicely (96, 64, 48, 32, 16...), 384 does too, but a hand-rolled 100-dim embedding leaves you few choices. The workarounds are to pad or project the vectors to a friendlier dimension first — a PCA or OPQ pre-transform can output a chosen dimension — rather than forcing an odd `m`. ## What each parameter buys Raising **m** narrows each slice, so the sub-quantizer's 256 centroids cover fewer dimensions and approximate them more tightly. Accuracy improves smoothly; memory grows linearly; the asymmetric distance computation does one table lookup and add per sub-vector, so query CPU also grows roughly linearly in `m`. Going from m=8 to m=64 on a 768-dim embedding is the difference between an aggressive 8-byte code that loses a lot of signal and a 64-byte code that preserves most rankings. Raising **nbits** gives each slice a richer codebook. It helps accuracy, but the costs are steeper than they look: the codebook grows as 2^nbits, the distance lookup tables built per query grow with it, and the training requirement grows with it too — you need enough vectors to fit 2^nbits centroids per slice, so nbits=12 wants tens of times more training data than nbits=8. FAISS's fast paths are also most optimised for the 8-bit case. In practice, tune `m` and leave `nbits` at 8 unless you have measured a specific reason. ## Training implications PQ codebooks are learned in `train()` alongside the IVF centroids. The rule of thumb mirrors the coarse quantizer's: you want on the order of tens of points per centroid, so at nbits=8 (256 centroids per slice) a training set of a few tens of thousands of vectors is a sensible floor, and FAISS will warn when you are below it. Under-trained codebooks produce codes that cluster badly and cap recall in a way no search parameter can recover. ## The recall ceiling This is the operational consequence worth carrying into an interview. PQ distances are approximations computed from codes, so an IVFPQ index has a recall ceiling set by `m` and `nbits`, independent of how many cells you scan. If recall plateaus below target with nprobe already high, the code is too short — enlarge it, or add a re-ranking stage that recomputes exact distances on a shortlist against full-precision vectors (`faiss.IndexRefineFlat` with a `k_factor` above 1, which retrieves more candidates then re-orders them). Re-ranking is usually the better trade: it keeps memory low for the bulk index and pays exact-distance cost only on a handful of candidates. ## Choosing values in practice Start from the memory budget. Divide the RAM you can spend by the number of vectors to get bytes per vector, which fixes `m` at nbits=8. Then measure recall against exact ground truth; if it misses, decide between a larger `m` (more memory everywhere) and a refine stage (more query CPU on a shortlist). Common working points are 8–16 bytes per vector for very large web-scale corpora where a refine stage is expected, and 32–96 bytes when the corpus is smaller and accuracy matters more than footprint.

  • Your embeddings are 100-dimensional and FAISS refuses to build the PQ index. Why, and what do you do?
    PQ requires d to be divisible by m, and 100 has awkward factors, so most sensible m values fail at construction. The practical fixes are to reduce or reshape the dimension first with a pre-transform that outputs a friendly size, or to pad the vectors to 128. Forcing an odd m such as 25 technically satisfies the constraint but gives sub-vectors of an unhelpful width.
  • Recall is short of target and you cannot afford more memory per vector. What is the lever?
    Re-ranking. Keep the compact PQ index for candidate generation, retrieve more candidates than you need, and recompute exact distances for that shortlist against full-precision vectors — faiss.IndexRefineFlat with a k_factor above 1 does exactly this. You pay CPU on a few hundred candidates instead of memory on every vector, which is usually the cheaper trade.
  • Why is nbits=8 the near-universal choice?
    It puts one code in one byte, which keeps the stored codes and the per-query distance lookup tables aligned and cache-friendly, and it is the case FAISS's fast implementations target. Larger nbits multiplies codebook size and training-data requirements by 2^nbits while improving accuracy less than simply raising m usually does.

Product quantization is like describing a photo by naming the closest paint-chip colour for each tile of a grid: more tiles (m) and a bigger paint catalogue (nbits) reproduce it better, but the description gets longer and the catalogue costs more to print.

saying these in an interview costs you the question

  • Thinking m is the number of neighbours or the number of clusters
  • Assuming PQ compression is lossless because distances still look sensible
  • Raising nprobe to fix recall that is actually capped by short codes
  • Ignoring the d % m == 0 constraint until construction throws
  • Setting nbits high without increasing the training set

context