向量库容量测算台

HNSW 索引内存怎么估:把 M、efConstruction 和邻居表分开算

HNSW 索引内存怎么估:把 M、efConstruction 和邻居表分开算

估算 HNSW 索引的内存占用时,最容易出错的做法是拿一个「每条向量占多少字节」的系数去乘总条数。这个系数把三件性质完全不同的事混在一起:原始向量本身、图的邻接表结构、以及构建期为了搜索而存在的临时数据。三者的增长规律不一样,只有第一项能算出接近精确的值。

第一层:原始向量,公式确定

这一层是纯算术。存储精度决定每个分量占几个字节:fp32 每分量 4 字节,fp16 每分量 2 字节,int8 每分量 1 字节,二值量化每个分量只占 1 bit(因此每 8 个分量凑成 1 字节)。向量总字节数 = 条数 × 维度 × 每分量字节数。维度 768、条数 100 万、fp32,结果是 768 × 1,000,000 × 4 = 3,072,000,000 字节,约 2.86 GiB。这一层不需要任何关于索引实现的假设。

第二层:图的邻接表,按公开算法结构可推

HNSW 为每个节点维护一张邻居表。节点数 N,每层的出边上限 M,每条边要存一个邻居 ID。图结构的空间取决于邻居 ID 的存储宽度和层数分布:

  • 层数分布是按指数衰减的随机过程,绝大多数节点只在最底一层,第 0 层承载几乎全部节点。经验上第 0 层邻居表通常按 M0 = 2M 计,上层按 M 计。
  • 若邻居 ID 用 32 位整数存储,第 0 层的邻接表约为 N × 2M × 4 字节。维度 768、M = 16、条数 100 万,即 1,000,000 × 32 × 4 = 128,000,000 字节,约 119 MiB。
  • 实现常用紧凑位宽(例如按 N 上限取 ceil(log2 N) 位)来压这张表,位宽越窄越省,但多数库默认仍用 32 位或 64 位 ID。算的时候要按实现实际的 ID 类型取值,否则会高估或低估。

第三层:efConstruction 与构建期峰值,算不准

构建期的搜索需要为每个新插入的节点维护一个候选队列,队列长度由 efConstruction 决定。此外多线程插入会产生临时向量副本,扩缩容、compaction 期间还可能有双份索引并存。这一层的占用取决于实现细节、线程数、内存分配器的行为和 compaction 策略,没有一个能普遍成立的公式。

本站的做法是把它单列成一段「算不准」,而不是给一个系数了事。一个经验性的量级参考:构建期峰值通常是索引常驻部分的数倍,具体倍数跟 efConstruction 和并发度走。

加在一起仍然只是下限

三段相加得到的仍偏向下限,原因至少有三个:内存分配器对齐与碎片、写入放大带来的临时副本、以及不同实现对量化与压缩的额外开销。真实进程的常驻内存一定更高。

要得到可用的数字,请把这三层分别填进本站的测算台(维度、条数、精度、索引类型,M 与 efConstruction 按实现实际配置填),而不是把三层乘进一个系数。填完只出结果、不做推荐,也不保存任何输入。