← Back to skills
extension
Category: Development & EngineeringAPI key requirement unconfirmed

HNSW 构建性能基准与 GPU 加速真伪鉴别

向量索引(HNSW)构建性能基准测试,以及"GPU 加速"真伪鉴别。当需要测量向量数据库建索引耗时、判断 GPU 是否真的参与构建、拆分距离计算与图组装的耗时占比、估算端到端加速上限,或排查"接了 GPU 却没有加速"时使用。

personAuthor: LiSiyu0001hubOpenAPI

HNSW 构建性能基准与 GPU 加速真伪鉴别

这个 Skill 解决什么问题

"给索引构建加上 GPU,快了几十倍"这句话,绝大多数情况下是不成立的,而且不成立的方式有规律可循。

典型场景:有人给出一个 70 倍、100 倍的加速比,但那是单段微基准(只算距离矩阵),而整条构建链路里这一段只占 74%——端到端上限其实只有 3.7 倍。再往下查,往往会发现所谓"GPU 后端"根本没被调用,只是换了个语法标签。

这个 Skill 提供一套可复现的流程,把三件事分开:

  1. 端到端构建耗时——用户真正感知的部分
  2. 分段耗时占比——换硬件到底能撬动多少
  3. GPU 是否真的参与——不看宣传,看信号

核心原则一句话:先分段,再端到端;先证伪,再报数。

什么时候用

  • 需要给向量库的建索引耗时出一份可信的基准报告
  • 有人声称某个后端/硬件带来了加速,需要验证真伪
  • 已经接了 GPU,但端到端没变化,需要定位瓶颈在哪一段
  • 要在两种索引实现(如 CPU 后端 vs GPU 后端)之间做公平对比

不要用这个 Skill 做什么

  • 不要用它测查询延迟。构建和查询是两条完全不同的链路,并行结构不同,加速比不可互推。用查询数据去支撑构建结论,是最常见的答辩翻车点。
  • 不要用它给"某产品比某产品快"下通用结论。这里的每个数字都绑定具体的数据规模、维度、参数(M / ef_construction)和机器规格。

步骤 1:先测端到端构建基线

在动手改任何代码之前,先拿到"现状"。

要测的是 DDL 完成到索引可用 的墙钟时间,不是插入数据的时间。很多实现把索引构建和 INSERT 混在一起(在建表语句里直接带索引),那样测出来的数字无法拆分,必须改成"先灌满数据,再单独 CREATE INDEX 并计时"。

至少测 4 个规模,跨越两个数量级(例如 5k / 10k / 20k / 50k)。原因:

  • 小规模时固定开销(进程启动、内存分配)占比高,加速比会虚高
  • 大规模时复杂度项才显现,O(N²) 段会拉开差距
  • 只测一个点几乎总能得到误导性结论

记录表至少包含:n、dim、参数、耗时(秒)、返回码、数据来源(真实数据集还是合成)。

步骤 2:用参数探针拆出距离计算占比

这是整个流程里最关键、也最容易被跳过的一步。

HNSW 构建的总耗时由两部分构成:

T(ef) ≈ T_固定 + c · ef

ef_construction(或同类参数)决定了每个插入点做多少次距离计算和图遍历,而图结构组装、内存分配这些开销基本不随它变化。所以:

  1. 固定 N,让 ef_construction 取 4~5 个不同值(如 50 / 100 / 200 / 400 / 800)
  2. 每个值测一次构建耗时
  3. 对 (ef, T) 做线性拟合,得到截距 T_固定 和斜率 c
  4. 距离计算占比 = (T(ef_实测) - T_固定) / T(ef_实测)

拿到占比就可以立刻算出端到端加速上限(Amdahl 定律):

上限 = 1 / ((1 - p) + p / s)

其中 p 是距离计算占比,s 是该段能拿到的加速比。若 s → ∞,上限收敛到 1 / (1 - p)。

用 scripts/fit_ef_probe.py 可以直接算:

python scripts/fit_ef_probe.py --data ef_probe.csv --ef 200 --seg-speedup 70

输入 CSV 两列 ef,seconds。脚本会输出拟合结果、占比、以及若干段加速比对应的端到端上限。

如果这一步算出来的上限只有 2~4 倍,那么任何"几十倍"的端到端数字都必然是错的,不用再看后面。

步骤 3:判断 GPU 是否真的参与

这一步要用多个独立信号交叉验证,不能只看一个指标。

可信的信号

| 信号 | 怎么看 | 为什么可信 | |---|---|---| | 后端库真实存在 | find / -iname '*cuvs*' -o -iname '*cagra*' 等,能找到 .so 再用 ldd 确认 | 库不存在就不可能跑在 GPU 上 | | 进程映射了 GPU 运行时 | lsof -p <pid> 或 /proc/<pid>/maps 里出现 libamdhip64 / libcudart | 静态链接也会留痕,但比日志可靠 | | 显存占用变化 | rocm-smi --showmemuse / nvidia-smi --query-gpu=memory.used 在构建期间上升 | 真正的 GPU 计算必然要传数据上去 | | 端到端耗时随规模呈 GPU 特征 | 大规模时 CPU 超线性增长、GPU 近似平线 | 这是复杂度结构决定的,难以伪造 |

常见的假信号(会被误当成证据)

| 假信号 | 为什么会骗人 | |---|---| | GPU use / 利用率百分比 | 容器或虚拟化环境下可能恒为固定值(实测遇到空闲时也报 100%)。用之前必须先采一段空闲基线 | | 后端参数被接受 | WITH (lib=xxx) 这类语法被解析通过,只说明枚举存在,不代表实现了 | | 日志里出现 "gpu" 字样 | 初始化日志、回退日志、探测日志都会打印,需看它后面是否真的走了该路径 | | 加速比"看起来像 GPU" | 0.97×~1.05× 这种比值也能被讲成"接近 1 倍",本质是噪声 |

判定流程

  1. 空闲状态下连续采样 6~10 次利用率/显存,记录下来
  2. 若空闲读数就非零(例如恒为 100),该指标直接判废,在报告里写明"此环境下不可用"
  3. 改用"库是否存在"+"端到端耗时是否随规模分离"这两个信号下结论
  4. 若后端声明存在但加速比落在 0.95~1.05 之间,应判定为回退到 CPU,而不是"轻微加速"

步骤 4:报数口径

报告里必须把三类数字分栏写,永远不要合并成一个"加速比":

| 类别 | 含义 | 能不能当成绩 | |---|---|---| | 分段加速比 | 只算距离计算/top-k 那一段 | 能,但要标明占比 | | 端到端加速比 | 完整建索引链路 | 能,这是用户感知的 | | Amdahl 上限 | 理论上限,不是实测 | 不能,只能作为"为什么端到端只有这么多"的解释 |

同时必须显式记录:

  • CPU 基线规格(核数、型号)。用 128 核 EPYC 和用 8 核笔记本当对照,结论完全不同。
  • 数据来源(真实数据集 / 固定种子合成向量),合成数据要给出种子。
  • 回退情况。若某个后端发生 CPU 回退,写清楚"声明支持但实际未启用",不要隐去。

常见误判清单

详细版见 references/pitfalls.md,最常踩的四条:

  1. 拿查询加速当构建成绩。batch_knn、search 类接口测的是检索吞吐,与建索引无关。
  2. 只测一个规模。小规模下 GPU 常因启动开销反而更慢(实测 100 条时 0.71×),规模上去才反超(2000 条 7.4×)。给一个点等于没给。
  3. 把 Amdahl 上限当实测结果。写成"预期可达 3.7 倍",被读成"已经达到 3.7 倍"。
  4. 忘了重复性。同一配置连跑两次,比值可能在 0.97 和 1.05 之间来回跳。任何小于 5% 的差异,先按噪声处理,用重复实验确认。

一个完整的实测样例

以下数字来自一台 128 逻辑核服务器 + AMD Radeon(RDNA3)的组合,维度 128,M=16、ef_construction=200:

端到端建索引(同一数据、同一参数,只换后端):

| N | CPU 后端 | 标称 GPU 后端 | 比值 | |---|---|---|---| | 10,000 | 2.66 s | 2.53 s | 1.05× | | 10,000(复测) | 2.65 s | 2.74 s | 0.97× | | 50,000 | 17.09 s | 16.79 s | 1.02× | | 100,000 | 49.96 s | 49.54 s | 1.01× |

分段微基准(只算 top-64 近邻):

| N | CPU | GPU | 加速比 | |---|---|---|---| | 5,000 | 0.987 s | 0.007 s | 137.29× | | 10,000 | 1.985 s | 0.022 s | 90.46× | | 20,000 | 6.633 s | 0.078 s | 85.39× | | 50,000 | 33.699 s | 0.457 s | 73.77× |

结论的正确写法是:分段能到 70~137 倍,但端到端是 1.0 倍,说明瓶颈段从未被接进构建链路;其中 1.0× 不是 GPU 成绩,而是回退证据。

另有一个反直觉观察值得记:这台 128 逻辑核机器建 10k 索引要 2.65 s,而 8 核笔记本只要 2.52 s——基本持平。说明该实现在这个规模区间没用满多核,瓶颈不在 CPU 并行度上。这类"对手比想象中弱/强"的发现,往往比加速比本身更有信息量。

脚本与参考

  • scripts/fit_ef_probe.py —— 对 (ef, 耗时) 做线性拟合,输出距离计算占比与端到端上限
  • references/pitfalls.md —— 误判清单详解,含每条的识别方法与反例
<!-- OB-S:4Bu9Egu4KWYr3hBiZNUMFA -->