ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

静态嵌入实测:5194 段中文编码 1.69 秒,比 ONNX 版快 127 倍

静态嵌入实测:5194 段中文编码 1.69 秒,比 ONNX 版快 127 倍 静态句向量这个概念不新但它最近换了个做法把一整个句向量模型蒸馏成一张 token 向量表推理时只做查表加平均池化。GitHub 上的 model2vec 仓库就是这条路线代表权重是 potion-multilingual-128M。我在这台 2 核 CPU 的机器上拿 5194 段中文笔记实测了一轮全量编码 1.69 秒同一批数据用 ONNX 版 bge-small-zh-v1.5 是 215.8 秒差 127 倍而检索指标没掉。静态嵌入在算什么它的推理里没有注意力、没有层、没有位置编码。每个 token 对应表里的一行固定向量一句话的向量就是这句话所有 token 向量的平均值最后做 L2 归一化。同一个词出现在任何句子里拿到的都是同一行——所以上下文这件事在这里不存在。我用的这份权重模型卡上写的是用 Tokenlearn 从 BAAI/bge-m3 蒸馏出来的训练数据取自 C4 的 101 种语言、200 万句输出 256 维词表也是 bge-m3 的。模型卡给的 MTEB 任务均值是 47.31同一页里 LaBSE 是 52.07也就是官方口径下约九成水平。看它自己的 config.jsonnormalize 为 trueapply_pca 和 sif_coefficient 都是 null。翻译成代码就是三步——查表、平均、归一化没有加权项没有投影矩阵。把那张表从 safetensors 里读出来safetensors 的格式比 pickle 干净前 8 字节是头部长度接着是描述每个张量 dtype、shape、字节偏移的 JSON再往后全是裸数据。读它的代码一共二十来行importjson,mmap,structimportnumpyasnpdefread_safetensors(path):fopen(path,rb)mmmmap.mmap(f.fileno(),0,accessmmap.ACCESS_READ)# 488 MB 的表别整块读进内存(header_len,)struct.unpack(Q,mm[:8])headerjson.loads(mm[8:8header_len])base8header_len tensors{}forname,infoinheader.items():ifname__metadata__:continuedt{F32:np.float32,F16:np.float16,I64:np.int64}[info[dtype]]start,endinfo[data_offsets]countint(np.prod(info[shape]))tensors[name]np.frombuffer(mm,dtypedt,countcount,offsetbasestart).reshape(info[shape])returntensors,header_len,len(mm)-base跑出来是这样tensor key: embeddings | dtype: float32 | shape: (500353, 256) vocab: 500353 dim: 256 params: 128090368 文件头字节: 80 数据字节: 512361472 归一化: True | 词表来自: BAAI/bge-m3 | PCA: None unk id: 1 | vocab 实际条数: 500353 字数 41 - token 28 前 8 个 id [4, 25084, 67826, 226612, 2281, 2344, 28673, 255486] 输出矩阵: (3, 256) float32 | 每行 L2 范数: [1. 1. 1.]500353 × 256 128090368名字里的 128M 就是这么来的全都是这张表。头部 JSON 只占 80 字节剩下 512 MB 全是浮点数据。编码口径我按仓库里 model.py 的实现抄分词时 add_special_tokensFalse把 unk 丢掉池化取平均归一化的分母加 1e-32。实验口径项目口径语料本机中文 Markdown 与笔记切出的 5194 段段落级、去重后分词样本语料前 300 段共 40895 字符已知项查询106 条取自语料内的原句正解是它所在的那一段改写查询15 条人工改写换说法不复制原句机器2 核 CPU、无 GPUONNX Runtime CPUExecutionProviderintra_op 线程 2静态模型手动实现查表 平均池化 L2 归一化对照模型bge-small-zh-v1.5 ONNX fp32 与 int8查询侧加官方指令前缀CLS 池化快 127 倍权重反而大 5.4 倍模型权重体积5194 段全量编码单条查询延迟加载后常驻内存编码峰值内存potion-multilingual-128M静态488.6 MiB1.69 s3068 段/秒0.12 ms578 MB820 MBbge-small-zh-v1.5 ONNX fp3290.5 MiB215.8 s24.1 段/秒14.6 ms187 MB1880 MBbge-small-zh-v1.5 ONNX int822.9 MiB129.5 s40.1 段/秒5.14 ms100 MB1731 MB反直觉的地方在这里静态模型的参数量是 1.28 亿比 bge-small-zhfp32 权重 90.5 MiB反推约 2400 万参数重五倍多文件也大 5.4 倍但快 127 倍。参数多不等于算得多——那 1.28 亿个数字只被查一次没有矩阵乘法把它们叠起来。内存那一列也要分开看。刚加载完静态模型常驻 578 MBbge 只有 187 MB跑完全量编码bge fp32 的峰值到 1.88 GB静态模型 820 MB。ONNX 的内存池按最大 batch 预留同一份数据上它把峰值堆得比查表方案高这台 3 GB 内存的机器上已经不算宽裕。常驻大和峰值大是两件事选型时最好自己各量一遍。int8 量化把 bge 提速 1.66 倍仍然比静态模型慢 76 倍。它的收益主要在权重体积22.9 MiB能塞进很小的镜像。检索质量已知项没输改写查询三家打平模型已知项 R1已知项 R10改写查询 R1改写查询 R10potion-multilingual-128M静态0.7450.9628/150.80bge-small-zh-v1.5 ONNX fp320.7080.9538/150.80bge-small-zh-v1.5 ONNX int80.7080.9628/150.867静态模型在已知项 R1 上还高 3.8 个百分点。我不觉得这说明它更强查询本身就是原文里的句子大词表切分又粗撞上的概率天然高而且 bge-small-zh 是 2023 年的小模型在这份语料上本来就不是强基线。能下的结论只有一条——这个规模上静态嵌入没有任何一项指标掉到不能用。15 条改写查询三家都只中 8 条但漏的不是同一批静态模型漏 555、2035、4255 三段fp32 漏 555、1480、2035int8 漏 1480、2035。想再榨召回把两路向量拼起来加权比换更大的模型便宜。顺手量了一下分数分布同一批 40 对查询正解段落模型正解对余弦均值正解对中位随机错配对均值potion-multilingual-128M静态0.8360.8560.457bge-small-zh-v1.5 ONNX fp320.7960.7870.490两个模型的正解分数都能拉到 0.8 附近但静态模型有 10% 的对子直接顶到 0.9997。换模型时把旧的相似度阈值抄过去一定会出错。我怎么用它分词这道坎绕不过去。静态模型省下的是神经网络前向不是分词同一批中文文本m2v 的词表bge-m3 那套切出 0.721 token/字符bge-small-zh 是 0.833。词表 500353 行对中文实际只用掉一小部分我下一步会把语料里没出现的行裁掉再落盘省的是磁盘和冷启动时间单条延迟不会变。实现上有三处得较真。一是特殊符号实测这份词表的 encode 加不加特殊符号输出完全相同add_special_tokensTrue 和 False 都是 13 个 token所以不用额外处理但 unk 要显式丢掉前 500 段里它占 4/48493中文下约万分之八可以忽略多语言混排时就会把池化结果拉偏。二是空文本实测归一化后范数 0.0入库前必须过滤否则这一行跟所有查询的余弦都是 0还会把阈值标定带歪。三是别指望它处理同义替换一个词在表里只有一行向量多义项挤在同一个点位上。要落地的话按这个顺序做先量你现在管线的嵌入耗时每千条文档多少秒再决定要不要换成查表方案。检索瓶颈常常在别处比如分词和 IO。拿自己语料切 5000 段左右把静态查表和现有模型的 int8 版放同一口径下比一次看召回而不是看感觉。报告数字时把口径写清楚——查询侧加不加指令、切块多长、用的是 R1 还是 R10。同一份语料换个切块粒度指标能差好几个点。
返回列表