目录 · 10 节
1它到底在解决什么问题?2整体流程3访问图:为什么要混合三种信号?4放置:把一组向量整体放到一层5迁移:只搬划算的部分6预取:藏掉躲不开的慢层访问7实验结果8我认为它的局限9一个推荐/搜索团队可以怎么试10一句话结论一条多模态查询往往要先查文本向量,再取对应的图片向量。两次 ANN 检索各自都很快,但查出来的两份向量可能一份在 GPU 显存里、一份在远端对象存储上,真正慢的是把它们凑到一起的那次搬运。CoLoc 不改 embedding、不改 ANN 索引,只调整向量放在哪一层存储:经常被同一条查询一起用到的向量尽量放在同一层。在一个由 GPU、内存、本地 NVMe 盘和 Amazon S3 组成的四层测试环境里,需要跨层取数据的查询从 73.04% 降到 0,端到端延迟降低 6.8%,多模态查询占一半时降低 24.8%,检索结果的 Recall@10 一点没变。
做多模态检索、向量库或特征存储的工程师可以读一读,原因是它把一个常被忽略的成本单独拎了出来:索引优化管的是在哪里搜,它管的是同一条查询要用的数据在哪里碰头。核心思想一句话:统计哪些向量经常被一起读,把它们打包放到同一层,访问模式变了只搬划算的那部分,剩下躲不开的慢层访问用预取藏起来。不过这是一篇单作者的短论文,原型用 Python 写,实验规模只有 5 万个对象,结论更像一个方向验证,离生产级证据还有距离。
它到底在解决什么问题?
用一个医疗检索的例子说明(示意)。系统里每个病例有两种表示:一份病历文本向量、一份胸片图像向量,分别建了两个 ANN 索引。医生输入一段描述,系统先在文本索引里找到相似病例,再去取这些病例的胸片向量做比对。
| 病例(示意) | 文本向量放在 | 胸片向量放在 | 这条查询要做的搬运 |
|---|---|---|---|
| A | GPU 显存 | GPU 显存 | 无 |
| B | 内存 | 本地 NVMe | 一次跨层读 |
| C | 内存 | Amazon S3 | 一次远端读,约 24 毫秒 |
两个索引是各自独立决定数据放在哪里的。文本索引按自己的访问热度把病例 C 的文本留在内存,图像索引却可能因为胸片向量整体偏冷,把它放到了 S3。单看任何一个索引,布局都合理;合在一条查询里,就多出一次远端往返。论文测的 S3 往返是 24.2 毫秒,比其他层慢一到两个数量级,所以只要一条查询碰到一次,延迟就被它决定了。
作者把这件事叫跨模态共置:同一条查询会一起用到的表示,应该放在物理上靠近的地方。只靠语义相似不够,因为语义相近的两份向量不一定被一起读;只靠访问记录也不够,新出现的访问模式记录太少。
整体流程
CoLoc 是挂在现有向量索引之上的一层存储管理,全部在后台运行,不进入查询路径。
| 部件 | 做什么 | 产出 |
|---|---|---|
| 跨模态访问图 | 记录哪些向量经常被同一条查询一起读 | 一张带权重的图,边越重越该放一起 |
| 放置优化器 | 把图切成若干组,每组整体放到一层上 | 每个向量该放在哪一层 |
| 迁移管理器 | 访问模式变了以后,判断搬哪些划算 | 少量迁移 |
| 预取器 | 查询读到 A 时,提前把 A 的邻居从慢层拉进缓存 | 藏掉剩下的慢层延迟 |
关键分工是:图负责回答谁和谁该在一起,优化器负责回答放在哪一层,迁移管理器负责回答现在值不值得搬。下面拆开讲。
访问图:为什么要混合三种信号?
图里每个节点是一个能独立搬动的存储单元,两个节点之间的边权由三部分相加:
其中 是两者被同一条查询一起读到的次数(归一化后), 是两个 embedding 的语义相似度, 看两者最近一次被访问的时间差:两个向量在相近的时刻都被读过,它就高;最近一次访问相隔很久,它就低。三个系数可调,实验里都取 1。共同访问是主要证据;语义相似用来解决冷启动,新入库的向量还没有访问记录时也能有一个初始权重;时间接近用来捕捉短期的关联,比如一批病例这几天正被反复调阅。
举个能手算的例子(数值都是示意):病例 C 的文本和胸片在过去被一起读了很多次,归一化后 ;两者语义相似度 ;最近访问时间很近,。权重是 。另一对向量语义很像()但从没被一起读过、最近也没被访问,权重只有 0.9。优化器会优先把前一对放在一起。
这张图是增量维护的:有新访问时只重算涉及这个对象的边。语义相似度不会变,算一次就缓存下来。但共同访问次数的归一化用的是全局统计量,一旦这个统计量变了,所有边都得重算。所以增量维护只在“频繁的小变动”下省时间,一次大的访问模式切换仍会触发全量重建。作者把这一点写得很清楚,实验里也分开测了。
放置:把一组向量整体放到一层

带容量约束的图划分是 NP 难问题,优化器用的是一个贪心做法。它先用标签传播把图聚成组:每个节点起初带一个自己的标签,然后反复改成邻居里连边权重最大的那个标签,几轮之后连得紧的一群节点会收敛到同一个标签,就成了一组。然后按热度排队:如果知道每个向量的查询频率,就先按组的总查询量排,高流量的组先占快层,被挤到慢层的就是最冷的数据。
每一组放哪一层,看的是把它放在某层后,和已经放好的邻居之间的搬运成本。第一组还没有放好的邻居,各层这项成本都是 0,这时选到其他各层平均搬运成本最低的那一层,通常是快层,所以排在前面的高流量组先占到快层。层与层之间的搬运成本写成一张表,每一格是从一层取数据到另一层大约要花多少秒,综合了延迟、带宽等几项因素。这样同一张访问图,在不同硬件拓扑上会得到不同的放法,比如能区分 NUMA(非一致内存访问)架构下读本插槽内存和读另一个插槽内存的差别。整组放不下时,优化器把组里在访问图上连边最多的那个节点单独剥出来,剩下的节点组成一个小组,两者都放回队列重新找位置。
这一步只移动存储单元,不改 ANN 图的边、不改向量编码、不改索引结构,所以每个索引原有的优化都保留。
迁移:只搬划算的部分

访问模式会随时间变化,静态放置会越来越过时,定期全局重排又可能搬得比省下的还多。迁移管理器的判断规则很直接:
是搬过去之后每条查询能省下的搬运时间, 是预计未来还会有多少条查询用到它,按这个单元过去的访问速率推算(示意:过去一小时被查了 100 次,就估计下一小时还有 100 次), 是这次搬迁本身要花的时间,等于数据大小除以两层之间的带宽。左边大于右边才搬。
手算一下(示意):一组向量 50MB,从 S3 搬到内存,S3 带宽按论文测的 86.4MB/s 算,搬一次约 0.58 秒。搬过去后每条查询省 20 毫秒,那么只要未来有 29 条以上的查询会用到它,就值得搬。一组冷数据如果预计只会被查几次,就留在原地。
搬迁按“复制、校验、切换”三步走:先把数据复制到目标层并校验,校验通过后原子地更新位置表,最后才释放旧副本。中途失败时旧位置仍然有效,不会出现数据丢失。
预取:藏掉躲不开的慢层访问
容量有限时,总有一部分数据只能放在最慢的层。预取器做的是:查询读到 u 时,把 u 在访问图上的邻居提前从慢层拉进一个有上限的缓存。
光看共同访问次数不够,因为“2 次里一起读了 2 次”和“200 次里一起读了 200 次”的可信度完全不同。预取器用一个 Beta 先验来平滑:样本少的时候估计值向先验靠拢,样本多了才接近实际比例。只有估计的条件概率超过阈值、且邻居确实在慢层又不在缓存里时才预取。工作线程不够用时,按置信度从高到低出队,而不是先来先服务。
实验结果
实验在亚马逊云上搭了四层:一台带 GPU 的机器、一台大内存机器、一台带 NVMe 盘的机器,加上 Amazon S3。每层容量都按数据集大小的一定比例设置,GPU 只放得下 15%,所以任何策略都必须用到多层。数据是 MIMIC-CXR 公开胸片数据集里的 5 万个对象,每个对象一份报告向量、一份图像向量。对照组是两个索引各自独立放置。
| 配置 | 跨层访问比例 | 端到端延迟 | 占用最满那一层的占用率 | Recall@10 |
|---|---|---|---|---|
| 各自独立放置 | 73.04% | 12.52 毫秒 | 166.7%(超容量) | 1.0000 |
| 只用共同访问 | 未报告 | 20.67 毫秒 | 未报告 | 1.0000 |
| 访问图 + 放置优化器 | 0.00% | 11.67 毫秒 | 100% | 1.0000 |
| 完整系统 + 预取 | 未报告 | 10.75 毫秒 | 未报告 | 1.0000 |
跨层访问比例指的是需要从不同层凑数据的多模态查询占多少;占用率超过 100% 表示那一层放的数据超出了它的容量。它降到 0,是因为一起被读的文本和图像落在了同一层,取的时候不用在层之间来回搬。与此同时放在远端的数据反而略多了(38.8% 对 34.9%),这说明它不是靠把更多数据挪到快层取胜,而是让一起被读的数据待在一起。只用共同访问信号的版本延迟反而更高,这说明单靠访问记录做放置可能更差,语义和时间信号确实起作用。
多模态查询越多,收益越大:多模态查询占 10% 时延迟降低 8.3%,占 50% 时降低 24.8%。这说明共置只帮得上需要跨模态取数据的查询,单模态查询为主的系统收益有限。
迁移方面,作者让访问模式在中途切换一次,对比三种做法:不搬、定期全局重排、迁移管理器。迁移管理器只搬了 5.10MB、用时 14.03 秒,全局重排搬了 30.19MB、用时 54.65 秒,跨层访问比例和全局重排几乎一样(0.11% 对 0),切换后延迟最低。这说明只搬变化的部分,就能拿到全局重排的大部分好处。增量维护访问图在 19 批小更新里只触发了 1 次全量重建,构建时间从 5.235 秒降到 0.756 秒。
我认为它的局限
第一,规模太小。5 万个对象、每层一台机器、10 万个向量,全部数据只有几百 MB 量级,迁移最多也就 30MB。生产环境的向量库动辄上亿条,访问图本身的存储和标签传播的开销在这个实验里看不出来。
第二,延迟收益的绝对值很小。基线 12.52 毫秒、优化后 11.67 毫秒,ANN 检索本身就占了 8.72 毫秒。在真实的多模态检索链路里,后面往往还有重排和生成,几毫秒的差别可能被淹没。
第三,对照组偏弱。最主要的对照是两个索引完全各自放置,没有和“按对象把所有模态打包存放”这种最朴素的做法比。在这个实验里每个对象恰好只有一份文本和一份图像,按对象打包很可能就能拿到大部分收益。
一个推荐/搜索团队可以怎么试
- 先统计现状:在多模态检索或多路特征拉取的日志里,看一条请求需要的数据分散在几层存储上,跨层读占端到端延迟多少。比例低就不必做。
- 先试最朴素的版本:按对象或按物品把各模态的向量放在一起,测跨层访问比例和延迟的变化。
- 朴素版本不够时,再引入共同访问统计,只对跨对象的强关联(比如经常一起出现的商品组合)做共置。
- 迁移和预取放在最后,用“省下的搬运时间乘以预计访问次数大于迁移成本”这条规则控制搬迁量,避免全局重排。
一句话结论
当一条查询要从多个独立索引里取数据时,延迟可能不取决于哪次检索最慢,而取决于这些数据之间离得多远。
| 维度 | 评价 |
|---|---|
| 问题重要性 | 中。多模态检索和分层存储越来越常见,但跨层搬运在多数链路里不是第一瓶颈 |
| 方法创新性 | 中。各部件都是熟悉的技术,新意在把查询级的共同访问当作放置目标 |
| 实验证据 | 偏弱。单数据集、5 万对象、对照组只有各自独立放置 |
| 工程成本 | 中。不改索引,但要维护访问图、迁移协议和预取缓存 |
| 是否值得尝试 | 先做现状统计;跨层读占比高的团队可以从按对象打包开始 |
读完这篇,你觉得
夜读鸮记下了,谢谢你的反馈。
点一下即可,无需登录;再点一次撤回。只统计匿名次数,不记录你是谁。
讨论
欢迎补充实验细节、复现经验或不同看法。需 GitHub 登录。
正在加载讨论…