
版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。1000 万条 768 维向量按 FP32 摆进内存光向量本体就是 30.7 GB。这个数字是所有「要不要量化」讨论的真实起点也是绝大多数误判发生的地方——因为一旦你把它除以 4、除以 32得到的 7.7 GB 和 0.96 GB 看起来像是同一件事的两个档位。我把同一批向量按四档压了一遍结论不是这么回事int8 把单条从 3072 字节压到 768 字节出头recall10 从 1.000 掉到 0.975二值化把单条压到 96 字节recall10 掉到 0.296。前者叫「换了个存储格式」后者叫「换了个检索任务」。这篇文章只做一件事把「省了几倍内存」和「掉了多少召回」放进同一张账里并给出每一档真正值得上的条件。一、先分清两笔账压权重改生成压索引只改召回权重量化和向量量化只共用了一个词代价却完全不可互换前者改的是模型吐出来的字后者改的是索引捞出来的条目。权重量化与向量量化只共用了一个词模型权重量化GGUF、AWQ 那一类压的是模型参数本身。一个 16 位权重变成 4 位影响的是每一次前向计算的每一个 token掉下去的质量是不可逆的——想补回来只能重训、蒸馏或者换更大的模型。它的账算在显存上验收标准是生成质量。索引里的向量量化压的是另一个东西向量条目。768 个 float32 变成 768 个 int8影响的是「哪几条文档被捞出来」。捞出来之后模型读到的原文一个字都没变——文本没被压缩被压缩的只是用来找文本的那串坐标。这两件事的容错差得很远。权重掉点会均匀地摊在每一句生成上用户立刻能感觉到索引掉召回只在检索边界上表现出来而且可以用过采样加重打分补回一部分。所以判断你遇到的是哪一类问题一句话就够怀疑「模型变笨了」不是本文的范围怀疑「该找到的没找到、答非所问」才是。单条字节数是一条公式不是一句「省 4 倍」单条向量的字节数只有一个算法维度 × 位宽 ÷ 8再加上整库共享的参数摊到每条。FAISS 的 Vector codecs 页把这个口径写得很直白SQ84x是每个分量压到 8 位PCARx,SQ4code size x/2是 4 位标量量化页面上那个例子是 1000 条 20 维向量压到每分量 4 位得到 1000 个各 10 字节的编码——10 就是 20×4÷8。Qdrant 的量化文档用的是同一条账标量量化把 float32 逐个分量转成 uint8单条向量的内存降到 1/4二值化把每个分量压成 1 bit降到 1/32。我把这四档按 768 维算平得到下面这张表。共享参数那一列容易被忽略int8 和 4-bit 我按维度各存一个 float32 的 scale整库 768 个、共 3 KB5000 条摊下来每条 0.61 字节如果库里只有 1000 条同样的 3 KB 就要摊 3.07 字节——条目少的时候scale 不能当零头抹掉。二值化没有 scale只有符号位。档位每维位数单条公式d768单条字节共享参数压缩倍数FP3232768 × 43072.0001.00×int88768 × 1 3 KB / 5000768.61每维一个 scale共 3 KB4.00×4-bit4384 × 1 3 KB / 5000384.61每维一个 scale共 3 KB7.99×binary1768 ÷ 896.00无32.00×这张表只算向量本体。HNSW 的图结构、每条向量的 ID、payload 都不在里面——不同实现的图开销差得很远混进来会让压缩倍数失真所以本文全程只算向量本体这一点在后面每张表里都成立。二、同一批向量压四遍int8 掉 2.5 个点二值化掉 70 个点先说方法上最容易翻车的地方数据必须是聚簇的纯随机高斯会把量化损伤整个藏起来。为什么数据必须带簇随机高斯会把损伤藏起来真实语料的 embedding 是有结构的同一个主题、同一类问题的句子挤在一起一个簇内部相邻两条的差别很小。检索真正难的地方就在这些簇内的小差别上——top-1 和 top-10 的距离差不了多少谁能排前面取决于很细的分量差异。纯随机高斯的 5000 条向量里没有这种结构。768 维下两两近似正交最近邻和第十名的相似度几乎打平排序本来就带着随机性量化噪声落进去也翻不动名次。用这种数据测四档量化都会给出接近满分的 recall然后你在真实语料上第一次上线就发现对不上。所以我的构造是60 个中心每个向量 所属中心 标准差 0.55 的高斯噪声再 L2 归一化余弦检索100 条查询从同一分布里取真值是 FP32 全库精确算出来的 top-10。60 个簇只算中等强度。簇数对量化损伤的影响我没有扫这里只给一句方向上的提醒簇越紧、簇越多二值化掉得越狠别拿本文的数字当你的下限。四档实测0.975 / 0.668 / 0.296四档都跑完了FP32 是基准int8 掉 2.5 个点4-bit 掉 33 个点二值化掉 70 个点。 实测环境Python 3.13.12 / macOS / numpy 2.5.405 篇 实验一四档向量编码的 recall10 与单条字节数。 数据带聚簇结构60 个中心 高斯噪声因为纯随机高斯会低估量化损伤。 importnumpyasnp rngnp.random.default_rng(20261024)N,D,NQ,K,C5000,768,100,10,60# 聚簇每簇一个中心噪声 0.55再 L2 归一化余弦检索centersrng.normal(0.0,1.0,(C,D)).astype(np.float32)Xcenters[rng.integers(0,C,N)]rng.normal(0.0,0.55,(N,D)).astype(np.float32)X/np.linalg.norm(X,axis1,keepdimsTrue)Qvcenters[rng.integers(0,C,NQ)]rng.normal(0.0,0.55,(NQ,D)).astype(np.float32)Qv/np.linalg.norm(Qv,axis1,keepdimsTrue)# 真值FP32 精确 top-Kgtnp.argsort(-(Qv X.T),axis1)[:,:K]defrecall(scores):topnp.argsort(-scores,axis1)[:,:K]returnfloat(np.mean([len(set(a)set(b))/Kfora,binzip(top,gt)]))defreport(name,scores,nbytes,shared):pernbytesshared/Nprint(f{name:8}{per:12.2f}{3072/per:9.2f}x{recall(scores):12.3f})print(fvectors{N}dim{D}queries{NQ}K{K}clusters{C})print(f{codec:8}{bytes/vec:12}{ratio:9}{recall10:12})report(FP32,Qv X.T,D*4,0)# int8按维度对称量化每维一个 scale整库共享s8np.abs(X).max(axis0)/127.0q8np.clip(np.rint(X/s8),-127,127).astype(np.int8)report(int8,Qv (q8.astype(np.float32)*s8).T,D,D*4)# 4-bit同样按维度对称量化到 -7..7两个维度压进一个字节s4np.abs(X).max(axis0)/7.0q4(np.clip(np.rint(X/s4),-7,7)8).astype(np.uint8)packed(q4[:,0::2]4)|q4[:,1::2]hipacked4lopacked0x0Fq4_rtnp.empty_like(q4)q4_rt[:,0::2],q4_rt[:,1::2]hi,lo deq4(q4_rt.astype(np.float32)-8.0)*s4 report(4bit,Qv deq4.T,packed.shape[1],D*4)# binary符号位 汉明距离相似度 D - 2 * 汉明距离xbnp.packbits(X0,axis1)qbnp.packbits(Qv0,axis1)popnp.array([bin(i).count(1)foriinrange(256)],dtypenp.uint8)hampop[xb[None,:,:]^qb[:,None,:]].sum(axis2,dtypenp.int32)report(binary,D-2.0*ham,xb.shape[1],0)vectors5000 dim768 queries100 K10 clusters60 codec bytes/vec ratio recall10 FP32 3072.00 1.00x 1.000 int8 768.61 4.00x 0.975 4bit 384.61 7.99x 0.668 binary 96.00 32.00x 0.296这里的 recall10 是 100 条查询 × 10 个位置 1000 个位置上对上的比例。0.975 意味着 1000 个位置里错了 25 个0.296 意味着错了 704 个——不是「差一点」是把大部分正确答案换成了同簇的邻居。有个细节值得说4-bit 那一档我在第一版脚本里踩了个坑8 位整数左移 4 位溢出解包回来的码全是错的recall 直接跑成 0.000。这类错误在真实项目里非常容易发生而且症状是「召回突然归零」而不是「召回略降」——看到 0 先怀疑解包不要怀疑模型。上面贴的代码里已经改成用无符号 8 位存、再按位解包。Qdrant 官方文档里有一组相近方向的数3072 维模型在 1 bit 二值化下不开重打分的准确度是 76% 到 77%开了是 97% 到 99%512 维模型则分别是 53% 到 55% 和 71% 到 91%。他们那组用了过采样和重打分、口径和本文不完全一样但方向很一致维度一低1 bit 就撑不住。我的 768 维落在他们两行之间量出来的 0.296 比 512 维那一行还低一截差在数据更集中、查询只有 100 条这两个因素上绝对值不能直接比方向可以拿来对照。性价比的拐点在 int8不在最省的那一档FAISS 那页给的选择标准原话是 Choosing a codec means striking a tradeoff between … accuracy of the codec——编码后每条占多少字节和编码本身的精度两头只能选一头。把这两个值放在一起换算是这样的从 int8 走到 4-bit单条从 768 字节降到 384 字节1000 万条省的 3.84 GB 内存代价是 30.7 个召回点从 4-bit 走到二值化再省 2.89 GB又掉 37.2 个点。换个说法前 4 倍压缩每省 1 GB 只花不到 1 个召回点后两段每省 1 GB 要花 8 到 13 个召回点。这就是为什么 int8 几乎是默认选项而 4-bit 只在一种很具体的条件下值得上——那 3.8 GB 的差额正好是「两台机器」和「一台机器」的分界或者正好卡在你的内存上限上。没有这个具体条件8 倍压缩换来的是一场召回事故。三、把候选放大到 100二值化的差距收到 0直觉是「多取点候选就补回来了」这个直觉在候选层面成立在排序层面不成立——而生产要的是排序。候选深度从 10 扫到 100缺口从 0.679 收到 0我把候选深度 k 从 10 扫到 100真值固定为 FP32 的精确 top-10看这 10 条里有几条进了量化索引的前 k 名。下面这段脚本一次跑完两件事上半段扫候选深度下半段接着算二值化加重打分能补回多少两段用的是同一批数据和同一套编码。 实测环境Python 3.13.12 / macOS / numpy 2.5.405 篇 实验二候选取多深k 扫描 二值化重打分能补回多少。 上半段真值固定为 FP32 精确 top-10看这 10 条里几条被量化索引排进前 k 名。 下半段二值化先取 10*os 条候选再用 FP32 精确重排看最终 recall10。 importnumpyasnp rngnp.random.default_rng(20261024)N,D,NQ,C,K5000,768,100,60,10centersrng.normal(0.0,1.0,(C,D)).astype(np.float32)Xcenters[rng.integers(0,C,N)]rng.normal(0.0,0.55,(N,D)).astype(np.float32)X/np.linalg.norm(X,axis1,keepdimsTrue)Qvcenters[rng.integers(0,C,NQ)]rng.normal(0.0,0.55,(NQ,D)).astype(np.float32)Qv/np.linalg.norm(Qv,axis1,keepdimsTrue)exactQv X.T s8np.abs(X).max(axis0)/127.0deq8np.clip(np.rint(X/s8),-127,127).astype(np.int8).astype(np.float32)*s8 s4np.abs(X).max(axis0)/7.0deq4np.clip(np.rint(X/s4),-7,7).astype(np.float32)*s4 xbnp.packbits(X0,axis1)qbnp.packbits(Qv0,axis1)popnp.array([bin(i).count(1)foriinrange(256)],dtypenp.uint8)hampop[xb[None,:,:]^qb[:,None,:]].sum(axis2,dtypenp.int32)scores{FP32:exact,int8:Qv deq8.T,4bit:Qv deq4.T,binary:D-2.0*ham}ranks{n:np.argsort(-v,axis1)forn,vinscores.items()}gt10ranks[FP32][:,:K]print(候选深度扫描真值固定 FP32 top-10分母 10)print(k .join(f{n:9}forninscores) binary-int8)forkin[10,20,30,50,80,100]:row{n:float(np.mean([len(set(ranks[n][i,:k])set(gt10[i]))/Kforiinrange(NQ)]))forninscores}print(f{k:7}.join(f{row[n]:9.3f}forninscores)f{row[int8]-row[binary]:14.3f})print()print(fbinary 每条{xb.shape[1]}字节原向量每条{D*4}字节)print(oversample candidates recall10 RAM 需存原向量)forosin[1,2,4,8,16]:hits0.0foriinrange(NQ):candranks[binary][i,:K*os]hitslen(set(cand[np.argsort(-exact[i,cand])][:K])set(gt10[i]))/Kprint(f{os:10}{K*os:10}{hits/NQ:11.3f}f{否ifos1else是:12})候选深度扫描真值固定 FP32 top-10分母 10 k FP32 int8 4bit binary binary-int8 10 1.000 0.975 0.668 0.296 0.679 20 1.000 1.000 0.912 0.488 0.512 30 1.000 1.000 0.976 0.666 0.334 50 1.000 1.000 0.999 0.868 0.132 80 1.000 1.000 1.000 0.986 0.014 100 1.000 1.000 1.000 1.000 0.000 binary 每条 96 字节原向量每条 3072 字节 oversample candidates recall10 RAM 需存原向量 1 10 0.296 否 2 20 0.488 是 4 40 0.796 是 8 80 0.986 是 16 160 1.000 是缺口确实一路收到 0二值化在 k80 时达到 0.986已经追平 int8 在 k10 时的水平k100 时 1.000完全追平。输出上半段最后那一列只是把「差多少」摊开了真正要问的是「差在哪」。但看清楚这个数字在说什么它只说「真 top-10 都进了前 100」没说「前 100 里谁排第一」。生产要返回 10 条你就得在这 100 条里重新排一次——这一刀切下去二值化的收益和它的代价才同时显形。还有成本上的连带效应k100 等于 10 倍过采样每次查询要多算 90 条候选。二值化比较本身很便宜96 字节异或再数位但候选一多重打分就成了主项延迟的大头从「比较」挪到了「回读」。重打分把召回买回来也把内存账买回来补排序的办法就是输出下半段那件事拿二值化筛出的候选用 FP32 精确重算一遍8 倍过采样时 recall10 到 0.986基本追平 int816 倍到 1.000。曲线很干净过采样每翻一倍召回往上抬一截从 0.296 一路到 1.000中间没有平台期。单看这一列二值化加重打分像是白捡了 32 倍。问题在最后一列「RAM 需存原向量」。重打分要用原始 FP32 向量算精确相似度这些向量必须还在。三种放法各有各的账留在内存里32 倍立刻变成 0 倍放磁盘每次查询回读 80 条 × 3072 字节 ≈ 240 KB放远端存储延迟直接由网络决定。Qdrant 的文档把这条写成了明确的警告重打分只能在二值化已经取到的候选里重排过采样低或维度小时补不回来而原向量存在磁盘上时重打分会让搜索速度显著下降。所以二值化的真相不是「省 32 倍」是「把内存换成 I/O」。这笔置换在下面这张表里能看清。资源项纯二值化不重打分二值化 8 倍过采样重打分int8RAM 里的向量96 B/条96 B/条原向量另存磁盘768.6 B/条实测 recall100.2960.9860.975每次查询额外读盘080 条 × 3072 B ≈ 240 KB01000 万条的内存占用0.96 GB0.96 GB 磁盘 30.7 GB7.69 GB主要瓶颈召回不可用磁盘随机读内存容量四、按现象排障内存没降、召回掉了、延迟反而涨这四档方案在线上出问题的方式高度集中六种现象能覆盖绝大部分而且每一种的处置都不一样。六种现象对应六种原因现象最可能的原因处置换成 int8 后内存几乎没降原 FP32 向量没从内存里移除或 scale 按每条向量各存了一份scale 按维度存、整库共享确认原向量已释放只留编码压完召回掉一大截离线脚本却显示只掉 1 个点离线用的是随机高斯数据没有簇结构抽 5000 条真实 embedding 重测按本文的口径二值化后召回极低模型是 384 维维度低于 1000 时 1 bit 丢得太多且分量未中心化先减均值或改用 int8、2 bit不要对低维模型上 1 bit开了重打分召回回来了P99 延迟翻倍原向量在磁盘每次查询回读 80 条原向量换本地 SSD 常驻或把过采样降到 4接受 0.80 左右的召回recall100 很漂亮线上答非所问口径错位返回条数不是 100却用 100 当 k 验收用生产实际返回条数当 k 复测一遍4-bit 实测 0.67别人却说接近无损对方开了重打分、或维度更大、或数据分布不同按本文口径自己复测别直接采信结论第三行和第五行是最贵的两个坑。前者是把「低维模型不适合 1 bit」当成实现问题去调参调一天也调不出来后者是验收口径和线上口径不是同一个 k报表全绿、用户全在骂。三条阈值与一个停手条件三条可以直接抄的阈值int8 默认上前提是基线 recall10 不低于 0.94-bit 只在「768 字节放不下、384 字节能放下」这个具体缺口上用并且必须配重打分二值化要同时满足三条——维度不低于 1024、向量已减均值中心化、过采样不低于 8 且开重打分缺一条就别上。4-bit 配重打分的曲线本文没有单独跑方法在第三章那段脚本里——把重打分的候选换成 4-bit 的排序改两行就能得到你自己的数字在拿到那个数字之前不要把 4-bit 当成「介于 int8 和二值化之间的安全档」它是独立的一档得单独验。停手条件只有一条而且就是上面那条 0.9 的线基线 recall10 低于 0.9 时先回去修 embedding 模型或切块策略别动量化的参数。量化的作用是让一个已经能用的检索变便宜不是让一个不准的检索变准——它只会把你原有的错误按同样的比例放大到用户眼前。这份资料是什么本文用到的四档量化对照表和排障清单我整理成了一页速查版含 recall 与字节数的换算口径。放在资料包里扫码即可获取五、把这套账做成你项目里的表再决定压到几 bit改参数之前先跑完五步任何一步不过就不要往下走——这五步的顺序不能换因为后面的判断依赖前面的真值。上线前跑完这五步第一步抽 5000 条真实向量不要用随机数造。有簇结构的数据才能暴露损伤随机高斯只会给你一个漂亮的假结论。第二步准备 100 条真实查询用 FP32 全库扫出 top-10 当真值。别拿 ANN 的结果当真值那测的是「和 ANN 的分歧」不是量化损伤。第三步四档各跑一遍同时记两个数recall10和单条字节数含共享参数摊薄。只记召回不记字节等于只算了一半的账。第四步对准备上的那一档再扫一遍 k10/20/50/100 和过采样 1/2/4/8看候选召回和重打分后的召回分别落在哪。这两条曲线的差就是你为「能排对」要付的钱。第五步把「每省 1 GB 花多少召回点」算出来和你的机器数对齐。如果省下的内存不足以让你少一台机器这笔交易在账面上就是亏的。1000 万条30.7 GB 与 0.96 GB 之间隔着什么按 768 维、1000 万条算FP32 是 30.72 GBint8 是 7.69 GB4-bit 是 3.85 GB二值化是 0.96 GB。图结构和 ID 另算实际占用都会比这高。三个可以直接用的判断单机内存 64 GB 以内、QPS 中等上 int8不要犹豫内存卡在 8 GB 上下、又必须全量常驻考虑 4-bit 加重打分但要先接受召回从 0.97 段掉到 0.67 段这个事实只做粗筛、后面还接精排模型二值化可以用但这时候你该盯的指标是「候选覆盖率」而不是 recall10——因为精排会替你修排序你只需要它别漏。回到标题那两个数字1/4 内存换 2.5 个召回点是工程上划算的交易32 倍内存换 70 个召回点不是交易是把检索换成了别的东西。二值化真正能同时拿到的是「RAM 里 96 字节的码 磁盘上 3072 字节的原向量」这套分层省的是内存付的是每次查询 240 KB 的读。账能算平但它从来不是免费的。这份资料是什么五步验证清单的可执行版含每步该记哪两个数、判据怎么设以及本文两段脚本的完整跑法。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表事实官方原文或转述出处文档名 发布方 链接本文位置SQ8(4x): compresses to 8 bit per component.Vector codecsfacebookresearch/faiss Wikihttps://github.com/facebookresearch/faiss/wiki/Vector-codecs第一章第二节PCARx,SQ4(code size x/2): same, with a 4-bit scalar quantizer同上第一章第二节1000 条 20 维向量压到每分量 4 位得到 1000 个各 10 字节的编码即 d×b÷8同上第一章第二节Choosing a codec means striking a tradeoff between: bytes per encoded vector … accuracy of the codec同上第二章第三节float32 逐分量转 uint8单条向量的内存降到 1/4QuantizationQdranthttps://qdrant.tech/documentation/guides/quantization/第一章第二节每个分量 1 bit内存降到 1/32仅在向量维度高且分量中心化时有效同上第二章第二节1 bit 压缩在维度低于 1000 时精度显著下降、常需重打分3072 维不开重打分 76%~77%、开了 97%~99%512 维 53%~55% 与 71%~91%同上第二章第二节重打分只能在二值化已取到的候选里重排过采样低或维度小时补不回原向量在磁盘时重打分显著降速同上第三章第二节写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。