Cross-Modal Data Co-Locality for Heterogeneous Storage Systems

Abdullah Al-Mamun · arXiv 2610.34595 · 发表 2026-10-25
12 分钟阅读时长 4580字 4.40综合分 第 2 名10-28 Infra 日报
目录 · 10 节1它到底在解决什么问题?2整体流程3访问图:为什么要混合三种信号?4放置:把一组向量整体放到一层5迁移:只搬划算的部分6预取:藏掉躲不开的慢层访问7实验结果8我认为它的局限9一个推荐/搜索团队可以怎么试10一句话结论
相关 5新颖 4实用 4证据 4时效 5

一条多模态查询往往要先查文本向量,再取对应的图片向量。两次 ANN 检索各自都很快,但查出来的两份向量可能一份在 GPU 显存里、一份在远端对象存储上,真正慢的是把它们凑到一起的那次搬运。CoLoc 不改 embedding、不改 ANN 索引,只调整向量放在哪一层存储:经常被同一条查询一起用到的向量尽量放在同一层。在一个由 GPU、内存、本地 NVMe 盘和 Amazon S3 组成的四层测试环境里,需要跨层取数据的查询从 73.04% 降到 0,端到端延迟降低 6.8%,多模态查询占一半时降低 24.8%,检索结果的 Recall@10 一点没变。

做多模态检索、向量库或特征存储的工程师可以读一读,原因是它把一个常被忽略的成本单独拎了出来:索引优化管的是在哪里搜,它管的是同一条查询要用的数据在哪里碰头。核心思想一句话:统计哪些向量经常被一起读,把它们打包放到同一层,访问模式变了只搬划算的那部分,剩下躲不开的慢层访问用预取藏起来。不过这是一篇单作者的短论文,原型用 Python 写,实验规模只有 5 万个对象,结论更像一个方向验证,离生产级证据还有距离。

它到底在解决什么问题?

用一个医疗检索的例子说明(示意)。系统里每个病例有两种表示:一份病历文本向量、一份胸片图像向量,分别建了两个 ANN 索引。医生输入一段描述,系统先在文本索引里找到相似病例,再去取这些病例的胸片向量做比对。

病例(示意)文本向量放在胸片向量放在这条查询要做的搬运
AGPU 显存GPU 显存无
B内存本地 NVMe一次跨层读
C内存Amazon S3一次远端读,约 24 毫秒

两个索引是各自独立决定数据放在哪里的。文本索引按自己的访问热度把病例 C 的文本留在内存,图像索引却可能因为胸片向量整体偏冷,把它放到了 S3。单看任何一个索引,布局都合理;合在一条查询里,就多出一次远端往返。论文测的 S3 往返是 24.2 毫秒,比其他层慢一到两个数量级,所以只要一条查询碰到一次,延迟就被它决定了。

作者把这件事叫跨模态共置:同一条查询会一起用到的表示,应该放在物理上靠近的地方。只靠语义相似不够,因为语义相近的两份向量不一定被一起读;只靠访问记录也不够,新出现的访问模式记录太少。

整体流程

CoLoc 是挂在现有向量索引之上的一层存储管理,全部在后台运行,不进入查询路径。

部件做什么产出
跨模态访问图记录哪些向量经常被同一条查询一起读一张带权重的图,边越重越该放一起
放置优化器把图切成若干组,每组整体放到一层上每个向量该放在哪一层
迁移管理器访问模式变了以后,判断搬哪些划算少量迁移
预取器查询读到 A 时,提前把 A 的邻居从慢层拉进缓存藏掉剩下的慢层延迟

关键分工是:图负责回答谁和谁该在一起,优化器负责回答放在哪一层,迁移管理器负责回答现在值不值得搬。下面拆开讲。

访问图:为什么要混合三种信号?

图里每个节点是一个能独立搬动的存储单元,两个节点之间的边权由三部分相加:

其中 是两者被同一条查询一起读到的次数(归一化后), 是两个 embedding 的语义相似度, 看两者最近一次被访问的时间差:两个向量在相近的时刻都被读过,它就高;最近一次访问相隔很久,它就低。三个系数可调,实验里都取 1。共同访问是主要证据;语义相似用来解决冷启动,新入库的向量还没有访问记录时也能有一个初始权重;时间接近用来捕捉短期的关联,比如一批病例这几天正被反复调阅。

举个能手算的例子(数值都是示意):病例 C 的文本和胸片在过去被一起读了很多次,归一化后 ;两者语义相似度 ;最近访问时间很近,。权重是 。另一对向量语义很像()但从没被一起读过、最近也没被访问,权重只有 0.9。优化器会优先把前一对放在一起。

这张图是增量维护的:有新访问时只重算涉及这个对象的边。语义相似度不会变,算一次就缓存下来。但共同访问次数的归一化用的是全局统计量,一旦这个统计量变了,所有边都得重算。所以增量维护只在“频繁的小变动”下省时间,一次大的访问模式切换仍会触发全量重建。作者把这一点写得很清楚,实验里也分开测了。

放置:把一组向量整体放到一层

论文算法 1:放置优化器的流程。先用标签传播把访问图聚成若干组,按组内连边总权重(有查询频率时先按频率)从高到低排队;依次给每组找一个容量够、且和已放好的邻居之间搬运成本最低的层。整组放不下时,把度数最高的节点剥出来,剩下的重新排队。
论文算法 1:放置优化器的流程。先用标签传播把访问图聚成若干组,按组内连边总权重(有查询频率时先按频率)从高到低排队;依次给每组找一个容量够、且和已放好的邻居之间搬运成本最低的层。整组放不下时,把度数最高的节点剥出来,剩下的重新排队。

带容量约束的图划分是 NP 难问题,优化器用的是一个贪心做法。它先用标签传播把图聚成组:每个节点起初带一个自己的标签,然后反复改成邻居里连边权重最大的那个标签,几轮之后连得紧的一群节点会收敛到同一个标签,就成了一组。然后按热度排队:如果知道每个向量的查询频率,就先按组的总查询量排,高流量的组先占快层,被挤到慢层的就是最冷的数据。

每一组放哪一层,看的是把它放在某层后,和已经放好的邻居之间的搬运成本。第一组还没有放好的邻居,各层这项成本都是 0,这时选到其他各层平均搬运成本最低的那一层,通常是快层,所以排在前面的高流量组先占到快层。层与层之间的搬运成本写成一张表,每一格是从一层取数据到另一层大约要花多少秒,综合了延迟、带宽等几项因素。这样同一张访问图,在不同硬件拓扑上会得到不同的放法,比如能区分 NUMA(非一致内存访问)架构下读本插槽内存和读另一个插槽内存的差别。整组放不下时,优化器把组里在访问图上连边最多的那个节点单独剥出来,剩下的节点组成一个小组,两者都放回队列重新找位置。

这一步只移动存储单元,不改 ANN 图的边、不改向量编码、不改索引结构,所以每个索引原有的优化都保留。

迁移:只搬划算的部分

论文算法 2:迁移管理器的流程。只看上次检查后被标记变动的单元和它们在图上的邻居,在这个小范围里重跑一次放置;比较搬之前和搬之后的预期搬运成本,节省超过迁移本身的成本时才搬。搬的时候先复制、校验成功后再原子地切换并释放旧副本,失败就保留原位置。
论文算法 2:迁移管理器的流程。只看上次检查后被标记变动的单元和它们在图上的邻居,在这个小范围里重跑一次放置;比较搬之前和搬之后的预期搬运成本,节省超过迁移本身的成本时才搬。搬的时候先复制、校验成功后再原子地切换并释放旧副本,失败就保留原位置。

访问模式会随时间变化,静态放置会越来越过时,定期全局重排又可能搬得比省下的还多。迁移管理器的判断规则很直接:

是搬过去之后每条查询能省下的搬运时间, 是预计未来还会有多少条查询用到它,按这个单元过去的访问速率推算(示意:过去一小时被查了 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 毫秒。在真实的多模态检索链路里,后面往往还有重排和生成,几毫秒的差别可能被淹没。

第三,对照组偏弱。最主要的对照是两个索引完全各自放置,没有和“按对象把所有模态打包存放”这种最朴素的做法比。在这个实验里每个对象恰好只有一份文本和一份图像,按对象打包很可能就能拿到大部分收益。

一个推荐/搜索团队可以怎么试

  1. 先统计现状:在多模态检索或多路特征拉取的日志里,看一条请求需要的数据分散在几层存储上,跨层读占端到端延迟多少。比例低就不必做。
  2. 先试最朴素的版本:按对象或按物品把各模态的向量放在一起,测跨层访问比例和延迟的变化。
  3. 朴素版本不够时,再引入共同访问统计,只对跨对象的强关联(比如经常一起出现的商品组合)做共置。
  4. 迁移和预取放在最后,用“省下的搬运时间乘以预计访问次数大于迁移成本”这条规则控制搬迁量,避免全局重排。

一句话结论

当一条查询要从多个独立索引里取数据时,延迟可能不取决于哪次检索最慢,而取决于这些数据之间离得多远。

维度评价
问题重要性中。多模态检索和分层存储越来越常见,但跨层搬运在多数链路里不是第一瓶颈
方法创新性中。各部件都是熟悉的技术,新意在把查询级的共同访问当作放置目标
实验证据偏弱。单数据集、5 万对象、对照组只有各自独立放置
工程成本中。不改索引,但要维护访问图、迁移协议和预取缓存
是否值得尝试先做现状统计;跨层读占比高的团队可以从按对象打包开始

读完这篇,你觉得

点一下即可,无需登录;再点一次撤回。只统计匿名次数,不记录你是谁。

讨论

欢迎补充实验细节、复现经验或不同看法。需 GitHub 登录。

正在加载讨论…