资讯动态

端侧多模态检索落地全复盘:双塔结构、模型压缩与向量检索实战

发布时间:2026/10/20 17:29:08 来源:尧图企业网站定制
“端侧”这个词干过移动端AI的人一听就懂——意味着模型从服务器下来住进你的手机、摄像头、甚至门锁里。这两年我业余时间一直在折腾一个端侧多模态检索的模拟项目起因很简单想做一个完全不联网的相册能对着手机说“找海边拍的那张”或者拿另一张图去搜相似照片。说实话云端随便调用成熟接口就能跑通但真要在某款中端手机上做离线推理加检索问题接踵而至——模型怎么压、向量怎么存、检索怎么做才能流畅每一步都是坑。这篇文章不是科普是我把一整套“适用于端侧的多模态检索模型”从训练到部署的完整实操复盘。会讲清楚为什么端侧多模态检索必须用双塔结构、怎么把大模型压成小模型还能保住检索精度、10万条向量塞进手机内存要怎么做、以及实测中那些最容易让人崩溃的翻车现场。适合想上手端侧AI、或者已有后端检索经验但第一次接触移动端部署的开发者。1. 端侧多模态检索在解决什么问题1.1 典型场景和需求特征我做的这个模拟项目核心需求一句话概括设备本地保存一批图片、文本片段用户输入任意模态的查询比如一段描述文字、一张照片系统要能在几十毫秒内从库里找回最匹配的内容。拆一下场景主要三类。离线相册检索算一类用户不靠网络、不上传隐私数据在本地完成以文搜图和以图搜图端侧安防算一类门锁或者摄像头设备上把抓拍到的画面跟本地底库比对避免把图像传云端造成隐私风险还有一类是商品识别类比如智能货柜终端本地识别商品减少网络决策带来的延迟和断网风险。这些场景有个共同点对延迟敏感、网络不一定可靠、隐私要求高、设备性能有限。这不是故意的苛刻这就是物理限制。某产品的体验基线是本地单次检索不超过100ms内存增量控制在几十MB以内基础模型文件不超过10MB实测下来这些数字直接影响用户愿不愿意用功能。1.2 和云端检索方案的本质差异如果走云端整个事情会简单很多。上传query到服务器服务器用强大的多模态模型做编码和检索返回top-k结果客户端的活儿很轻。但端侧检索把“编码”“索引”“打分”三个环节全都搬到了本地设备上瓶颈立刻从“算法好不好”变成了“内存够不够、CPU撑不撑得住、发热可不可接受”。我梳理了一下差别主要体现在四个维度延迟构成不同。云端延迟网络往返加服务器推理端侧延迟本地推理加本地检索没有网络抖动但计算慢的地方很实在。隐私边界不同。端侧所有数据不离开设备隐私问题天然回避这是很多场景选择端侧的决定性原因。算力预算不同。服务器可以用几十GB显存、多卡并行端侧可能只有一个中低端CPU核心和几百MB可用内存。模型更新周期不同。云端可以随时热更新端侧模型一旦下发短期内很难迭代训练时就要充分预估泛化问题。正是这些差异决定了端侧多模态检索不能照搬云端的成熟方案从模型结构到工程实现都得重新设计。2. 为什么双塔结构是绕不开的主线2.1 检索模型和分类/生成模型的根本区别很多第一次接触多模态检索的同学会习惯性想这不就是用一个大模型把图片和文本一起输入进去输出匹配程度吗从原理上讲没错但一旦带上“检索”两个字事情就变了。分类模型和生成模型处理的是单条输入一条数据进来算一次输出结果。检索模型面对的是一个规模和事先不知道的候选集合用户query来的时候系统要在整个库里找出最匹配的若干条。如果每条候选都走一遍完整模型推理计算量就是候选数量乘以单次推理开销——假设库里有1万条单次模型推理50ms光打分就500秒完全不可用。检索场景的真正解法是把“理解”和“搜索”分离。模型不再直接输出匹配度而是把各种模态的输入都编码成固定长度的向量语义相似的输入在向量空间里距离相近。这样用户query只需要编码一次得到查询向量然后到预先构建好的向量索引里做相似度搜索。候选条目在用户查询之前就可以全部提前编码好、存起来检索变成了纯粹的向量计算不再涉及模型推理。2.2 双塔与单塔的取舍业内把这条路分成两类主流结构。单塔结构让query和候选在模型内部做深度交互比如把图片和文字拼起来过一遍大模型。双塔结构则是两个独立的编码器各处理各的输入只在最后的向量空间里对齐。双塔结构之所以成为端侧检索的主流选择原因很直接。正因为query侧和doc侧的编码相互独立doc侧可以离线批量编码、建索引query侧只需要一个轻量编码器实时算一次。单塔结构在效果上通常更好因为交互发生在模型内部信息融合更充分但它无法预计算索引每条候选都要和query实时交互这在端侧根本跑不起来。我之前看到一个对比实验数据单塔在图文匹配的准确率上平均高4到8个百分点但单次打分耗时是双塔的几十倍在10万候选集上检索延迟差距达到三个数量级。所以做检索系统双塔不是“更好”的选择而是“必须”的选择。对比维度双塔结构单塔结构编码方式query和doc各自独立编码query和doc在模型内部交互候选预计算支持离线批量建索引不支持每个候选实时计算检索延迟极低耗时基本固定随候选数量线性增长模型效果相对较弱相对更强端侧适用性适合不适合2.3 端侧模型选型时的资源约束确定了双塔框架之后才算开始面对端侧真正的考验。我当时的想法是图像编码器用一个移动端友好的轻量网络文本编码器用一个轻量Transformer输出向量统一映射到128维。先说图像编码器。标准的视觉模型动辄几十MB、几亿次浮点运算对端侧来说太奢侈。我采用的知识蒸馏方案是拿一个开源的强视觉大模型做老师让轻量学生网络去学它的输出特征。学生网络参数量控制在5M以内单帧推理耗时目标在20ms左右。这里关键不是选哪个网络结构而是怎么让压缩后的模型保留语义对齐能力。直接训练一个小的图像编码器然后去跟文本塔对齐效果往往很差因为小模型学不到老师那种丰富的语义抽象。再说文本编码器。中文和英文我都测过轻量Transformer在短文本检索上表现比想象中好。我定了最大序列长度64超过就截断。因为检索场景下用户输入往往是一句话甚至几个词做长序列建模意义不大反而浪费计算。最后是维度选择。输出向量维度从512降到128之后检索精度只掉了大约2%但向量存储量直接降到原来的四分之一相似度计算的乘加运算量也同步下降。对端侧来说这交易太划算了。3. 从训练到压缩让模型适应端侧环境3.1 多模态对齐训练的关键细节做图文双塔训练最常用的目标函数是InfoNCE这类对比损失。它做的事很简单让匹配的图文对在向量空间中靠近不匹配的对互相推开。严格来说训练好一个双塔有两个关键因素一个是难负样本挖掘一个是温度系数调节。难负样本这块我踩过不少坑。随机采的负样本太简单模型很快就学不到东西了。后来我改成把batch内其他样本作为负样本也就是in-batch负样本配合部分难负样本混合效果才上来。难负样本可以用“最容易混淆”的策略比如文字描述相似但图像不同的对这让模型不得不去关注真正细粒度的语义差异。温度系数是另一个容易被忽略的参数。对比学习里的温度系数控制相似度分布的锐利程度温度太小会让模型过度自信温度太大会让所有样本的相似度趋同、失去区分度。我先是沿用公开方案里的0.07后来在自己的数据上重新调参发现0.1左右对本项目更合适。调这个参数不能用准确率要看验证集上的召回率是否稳定。训练还有一个容易吃亏的细节两个塔的学习率如果不分开设置很容易出现一个塔收敛过快、另一个塔拖着后腿的情况。我最后是图像塔和文本塔各自设置不同的学习率配合warmup训练曲线才稳定下来。3.2 蒸馏方案怎么设计蒸馏是端侧模型的核心压缩手段。我用的方法分三步。第一步老师模型在完整大规模图文数据上做预训练得到高质量的图文联合表示。第二步学生模型分别从图像和文本两个方向去模仿老师的输出损失函数由两部分加和一是学生输出向量和老师输出向量的一致性损失二是学生自身的对比学习损失。第三步再把学生模型做量化压缩和端侧部署。这套方案做的关键决策是并不直接拿老师模型的输出去做硬标签。检索模型的蒸馏和分类蒸馏不太一样。分类蒸馏可以拿老师的概率分布当软标签检索模型如果只对齐老师的最终向量学生可能学到一些无关紧要的表面模式反而忽略语义结构。我加了额外的排序关系约束要求老师和学生对同一组样本的相似度排序尽可能一致。这比单纯向量距离对齐更接近检索的本质。蒸馏时的batch size要尽量大。对比学习的有效性和batch大小强相关大batch能提供更丰富的负样本学生模型学习效果显著更好。但显存有限我通过梯度累积把等效batch size从256拉到1024效果提升明显。训练时图像塔和文本塔的batch结构要保持一致否则蒸馏的目标都漂移了。3.3 量化压缩的实操与参数模型压到几MB之后还要面对推理速度和内存的大山。量化是必须迈过的一道坎把FP32的权重和激活从4字节降到1字节甚至更低。我对模型做了INT8量化精度损失控制在1到2个点以内。量化有两条路线训练后量化和量化感知训练。训练后量化实现简单拿一批校准数据跑一遍统计每层激活的数值范围算出缩放系数就行。但校准集的质量直接影响量化效果。我一开始偷懒用了开源数据集的图片做校准结果模型部署到端侧后召回率直接掉了快10个点。后来改成从实际业务数据里均匀采样几百张有代表性的图片精度一下回来了。这事的教训是校准集必须贴近真实部署时的输入分布。量化感知训练效果更稳但训练成本高。如果项目节奏允许我建议至少对关键层做量化感知训练尤其是embedding输出层和最后几层。实验发现输出向量层对量化最敏感稍微有点误差检索排序就会乱。我的方案是这层保留FP16其他层用INT8最终精度几乎无损速度也没慢多少。embedding的存储也别忽略。端侧向量检索的embedding往往以float32保存一个库10万条、每条128维就是51.2MB纯裸数据加上检索结构很容易超过内存预算。我把embedding从float32量化成uint8内存直接砍半用余弦相似度时需要预先做L2归一化把归一化因子单独保存量化后按整数点积计算再反归一化。这里有个细节值得记下来int8点积累加时中间结果可能超出int16范围需要转int32累加否则数值溢出会导致相似度错乱。实测用SIMD指令加速uint8向量点积10万条128维逐条扫描一次只需要约3ms完全够实时检索。4. 检索系统落地从向量索引到端侧推理引擎4.1 索引结构怎么选模型训练完之后最实际的问题就是向量怎么存、怎么搜。候选集合在端侧通常是静态的预先知道数量级。我把策略分成两档来描述。如果向量规模在10万条以内暴力扫描完全可行。很多人一提到“检索”就觉得一定要上倒排索引、图索引。实际上端侧最大的限制是内存不是计算且计算规模并没有大到必须建立复杂索引的程度。按上面的计算uint8的128维向量10万条全部扫描一次内存读取大约13MB读一个13MB数据在当前手机的内存带宽下就是几毫秒的事配合top-k排序也基本不超过10ms。暴力扫描实现简单、无索引额外内存开销、精度无损这是端侧小规模检索的首选方案。如果规模到百万级情况不同。这时候全量扫描的内存带宽和计算量会线性放大到不可接受。我实测百万级uint8向量一次全扫描接近百毫秒体验下滑。这种情况只能引入近似最近邻搜索主要思路是倒排加量化。先聚类把向量空间划分成若干区域查询时只在最相关的几个区域里搜索配合乘积量化进一步压缩向量存储。代价是召回率会有损失调参的时候需要在召回率和速度之间做取舍。HNSW这类图索引我测过一轮查询速度确实快但建图额外内存开销太大而且端侧追加新向量时动态更新图的代价比较高。如果候选库是静态一次构建的HNSW可以考虑如果有可能增量更新我劝你谨慎。索引方案10万条耗时内存开销召回率端侧推荐度暴力扫描约3ms无额外精确10万级推荐倒排量化小于1ms中等有损百万级推荐图索引小于1ms较高有损静态库可考虑4.2 预计算与打分链路最终线上检索链路由两条路径构成。模型更新或库更新时离线任务把全部候选内容跑一遍图像塔编码得到n条128维归一化向量再量化成uint8落盘。在线请求时query进来先跑对应塔的前向推理得到查询向量做同样归一化和量化然后和库里的向量批量计算余弦相似度取top-k返回。这里容易出错的是两条路径的预处理不一致。图像塔在线推理时如果缩放和归一化参数跟离线建库时不一致检索精度会明显变差。我一开始在线侧用了系统相册的默认EXIF旋转离线侧没有处理结果整个库的相似度分数全部失真排查了很久才发现是图片方向的问题。打分方式上训练时用的是余弦相似度端侧为了计算效率要在量化前先把向量L2归一化这样余弦相似度就变成点积。归一化因子的精度很重要我当时用FP32保存没有引入额外量化误差。4.3 端侧推理引擎与前后处理优化模型要在端侧跑起来得选一个推理引擎转成端侧格式再做图优化、算子融合、内存复用。这一步没有银弹我在几个开源引擎之间反复对比过有的在CPU算子支持上好有的模型转换工具省心有的内存控制更激进。最终选定之前我用同一份量化后的模型跑了基准测试重点看三个指标首次加载时间、单次推理耗时、峰值内存。这里有个经验别一上来就看推理速度先把模型的输入输出节点确认清楚。多模态检索模型往往有两个输入image和text共用同一个模型或两个子模型端侧引擎对动态shape支持不如静态shape好。我在转模型时把输入分辨率固定为模型训练时的值序列长度也固定为64这样端侧引擎能做更激进的静态优化实测推理速度比动态shape快了近一倍。前后处理的算子有时比模型本身还慢。图像解码、缩放、归一化这些操作如果全在CPU里串行跑每一步都占用几十毫秒。我把图像预处理放到端侧图像处理管线里复用系统硬件加速归一化用定点计算替代浮点除法。文本侧则做词表的int映射和padding直接在内存中完成杜绝不必要的拷贝。4.4 内存与启动的几招优化端侧APP的内存是稀缺资源加载一个10MB模型文件再加几十MB的embedding索引如果不好好管理很容易被系统杀掉。我实际用了几个很有效的手段。模型文件用内存映射方式加载不用一次性读进内存用多少加载多少能明显降低启动峰值内存。embedding索引也会定期访问我同样做了内存映射。查询时把索引文件映射进地址空间核心扫描走线性内存访问对端侧存储介质很友好。启动速度方面模型初始化放到子线程首帧返回时再触发检索初始化保证界面不卡顿。实测从点击到看到第一次检索结果约1.2秒后续单次检索稳定在50ms内。“懒加载”也是个实用原则。我一开始把所有模块都初始化好再进界面白屏时间长得吓人。后来改成页面渲染完再初始化检索模块首帧速度提升明显代价只是第一次检索前有一个短暂的加载等待。5. 实测中遇到的问题与排查思路5.1 量化后检索精度剧烈下滑这是我第一次做量化时几乎崩溃的经历。INT8转换完单测的图文匹配准确率看着还行一上真实检索数据top-1命中率掉了快十几个百分点。排查思路要一层层剥。我先对比了量化前后的embedding输出差异发现误差集中在最后输出层。继续下钻又发现校准集覆盖不够真实场景的照片大多是室内暗光、复杂物体而我用的校准数据是干净的自然风景图。重新采集贴近业务分布的校准集后砸下去十几个点的精度回来了七八个。剩下几个点是最后一层量化敏感导致的将那层保留FP16后精度基本无损。经验总结校准集真的决定了量化的成败别用网上随便找的数据集凑数。5.2 端侧检索结果与服务器结果不一致同样的模型在服务器上测试top-1是对的部署到手机上后结果变了。第一次遇到我还以为是模型转换出了问题反复重新导出好几遍问题依旧。最后发现是端侧图像预处理管线的差异。服务器测试走的是传统RGB三通道输入端侧硬件的图像格式可能是RGBA四通道直接喂进模型通道顺序不一致导致模型输出向量偏移。我在端侧代码里补了显式的RGB转换和通道格式指定对齐服务器测试的预处理流程后结果完全一致。这个问题的隐蔽性在于模型往往没有任何报错推理可以正常跑只是输出向量在空间里漂移了一点导致排序变化。手头有FP32版本的端侧模型时可以和INT8版做逐层输出对比很快就能定位是预处理问题还是量化问题。5.3 检索速度不快反慢有一次我优化相似度计算改成SIMD加速后速度反而更慢了。查看性能剖析才发现向量已经量化成uint8但打分时依然走了昂贵的内存拷贝路径每一条候选向量都先拷贝到临时buffer再做点积拷贝开销远超计算本身。改成直接基于内存映射的原始地址做只读点积速度立刻提了上来。从这里我学到一个通用排查原则端侧性能瓶颈内存访问模式往往比计算更关键。打开调试工具记录函数耗时、内存带宽、缓存命中率先解决内存拷贝和数据布局问题再考虑指令集优化。5.4 轻量模型在复杂语义下难以对齐蒸馏压缩后的轻量模型在日常短语上检索效果不错但一遇到复杂语义描述就吃力比如“一辆红色的车停在黄昏的湖边”这种包含多个语义要素的查询轻量模型经常找错。我的解法是调整训练数据的配比。人为构造更多长句和组合语义的图文对让模型必须同时捕获多个维度的特征。同时在蒸馏时增加一个辅助任务让轻量模型单独重建老师模型的局部语义结构而不是只对齐整句的输出向量。效果提升之后我还发现这类复杂查询在最终排序时容易出现不同类型的候选项分数接近于是给最终打分加了一个小阈值处理优先保证top-1的稳定性。6. 几点实操心得说到最后分享一些我在反复折腾中沉淀的体会。一是“端侧”这两个字意味着所有方案都要假定资源有限所以设计的第一原则是砍需求。输出维度能不能降、nms里的候选能不能少、索引能不能无状态每一项决策都要拿评估数据说话。二是模型训练和端侧部署不能割裂。很多工放在训练完模型后才开始考虑部署结果发现蒸馏目标没有兼顾量化误差又回头改训练方案。我在项目中期把量化模拟直接加进蒸馏训练里让学生在训练阶段就适应量化噪声最终部署精度明显更稳。三是扎实的日志和对比工具是端侧调试的生命线。我建了一套离线和端侧的回归对比脚本每次改模型、改预处理、改检索逻辑都自动跑一遍同一组测试集输出top-1命中率、单次耗时、峰值内存三个核心指标。任何指标出现异常波动马上就能定位到改动来源省了大量排查时间。四是项目的迭代节奏应该是小步快跑。每次只改一个变量量化、蒸馏、索引结构调整都单独做一次完整验证。看似多花时间实际是最快的路径因为我体验过一次性改一堆东西后精度崩掉、根本不知道是哪个环节出问题的痛苦。如果你也在做端侧多模态检索希望这份复盘能帮你少踩几个坑。技术方案永远要服务于真实设备和真实场景跑通只是起点跑得好才是真正要磨的功夫。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑