4096 段文本做向量化:批大小 1 时每秒 10055 段, 批大小 256 时每秒 95380 段——差 9.5 倍,而矩阵乘本身一点没变快。 存储那边: 1000 段 1024 维 float32 是 3.9 MB、int8 是 1.0 MB,量化省的是几 MB,不值得折腾。
上一节把库分好了。 这一节把文件塞进去——重点是「向量化」这一步的三件事:能不能读、跑多快、占多大。
一、先确认「读得出来」,再谈向量化
上传前先看文件是文字层还是图片层。 扫描版 PDF 没有文字层, 向量化出来是空的——工具不会报错,只会显示「完成」,然后你什么都检索不到。 三种常见事故:扫描件(要 OCR)、表格(转成文本后列对齐丢失)、 图片里的文字(完全读不到)。 验证方法很简单:随便问一个只有那份文件里才有答案的问题,看能不能检索到。 建完库立刻问一句,两秒钟就能发现空库。 还有一种隐蔽情况:文件读出来了,但全是乱码(编码不对)。 乱码也会被正常向量化,只是检不到东西——判断方法是看片段预览里是不是一堆问号或方块。
二、向量化要批处理,别一条一条喂
向量化本质上是大量矩阵乘。矩阵乘本身很快,慢的是每次调用的开销。 Demo 把 4096 段按 1/8/64/256 四种批大小各跑一遍: 批大小 1 时每秒 10055 段,256 时每秒 95380 段。 差 9.5 倍的原因不是算子变快了,是每次调用 numpy 都要过一遍 Python 和 BLAS 的边界。 批大小 1 时,这段开销和真正的计算一样长。 这条结论跟你用什么模型无关——只要是一次算一批,就一定有这个效应。所以批量上传永远比一个个传快, 而且快的不止一点。 实操上:一次传 50 到 200 份文件是舒服的区间。传太多会占满内存,传太少等于白付调用开销。
三、存储:三种精度,差不了多少
1000 段、1024 维的向量,三种精度的占用:
| 精度 | 每维字节 | 1000 段占用 |
|---|---|---|
| float32 | 4 | 3.9 MB |
| float16 | 2 | 2.0 MB |
| int8 | 1 | 1.0 MB |
| 这些数都太小了。 一万段也只有 39 MB。 | ||
| 所以「量化向量省空间」这件事,在个人知识库里基本没有意义。 | ||
| 更关键的是:量化不等于加速。检索时算相似度还得把 int8 反量化回浮点, | ||
| 省了带宽,多了一步转换。 | ||
| 什么时候才值得量化?百万级向量的时候。那时候 1024 维 float32 是 3.9 GB,确实吃不消; | ||
| 而几万段以下的库,把精力花在切块和 k 上收益大得多。 |
四、向量化失败的三种表现
① 一直停在「处理中」。 通常是文件太大或格式不支持。拆成小文件重传,比等它跑完快。 ② 显示完成但检索不到。 八成是扫描件没文字层,或者编码乱码。去片段预览里看一眼就知道。 ③ 检索得到但内容不全。 通常是切块把关键段落切断了——这就是「重叠」参数存在的理由。 第三种最难查,因为它是「有答案但不完整」。表现为模型答得很含糊,或者只答了一半。 排查方法:把正确答案所在的那一段原文调出来,和检索到的片段比。如果片段里缺了那半句, 就是切块的问题。
五、动手 Demo:向量化:一条条喂和批处理,吞吐差近十倍
用 768×768 的矩阵乘模拟嵌入模型的一层计算,把 4096 条「样本」按 1/8/64/256 四种批大小各跑一遍, 量总耗时、GFLOPS 和每秒处理条数; 再算 1000 段 1024 维向量在 float32/float16/int8 三种精度下的占用。
W = rg.normal(0, 0.02, (768, 768)) # 模拟一层权重
X = rg.normal(0, 1, (4096, 768))
for bs in (1, 8, 64, 256):
t0 = time.perf_counter()
for i in range(0, 4096, bs):
X[i:i + bs] @ W # 一批一起乘
dt = time.perf_counter() - t0
print(bs, dt, 4096 / dt, 2.0 * 4096 * 768 * 768 / dt / 1e9)
实跑输出:
--------------------------------------------------------------------
向量化:一条条喂和批处理,吞吐差近十倍
--------------------------------------------------------------------
批大小 总耗时 吞吐 每秒段数
----------------------------------------------
1 380.3 ms 12.7 GFLOPS 10771 段/秒
8 139.6 ms 34.6 GFLOPS 29351 段/秒
64 131.1 ms 36.9 GFLOPS 31247 段/秒
256 47.1 ms 102.5 GFLOPS 86929 段/秒
**批大小 1 时 10771 段每秒,256 时 86929 段,差 8.1 倍。**
差这么多不是矩阵乘变快了,是**调用开销被摊薄了**:
每调一次 numpy 都要过一遍 Python 和 BLAS 的边界。
所以向量化要批处理,别一条一条喂。**这一条对所有嵌入模型都成立,跟模型大小无关。**
再算存储:1000 段 × 1024 维,三种精度各占多少:
float32 3.9 MB
float16 2.0 MB
int8 1.0 MB
**量化到 int8 只省存储,不省算力**——检索时还得反量化回来算。所以小知识库没必要折腾。
四行数据: 批大小 1 时 407 毫秒跑完 4096 条,256 时只要 43 毫秒。 吞吐从 1 万涨到 9.5 万条每秒。 GFLOPS 那一列更直观:11.9 涨到 112.5。同一台机器、同一个矩阵乘,只是不再一条一条地喂。 注意 64 和 256 差不多(81.2 对 112.5)——收益在 64 就基本吃满了。再往上加批大小, 主要是在吃内存。 存储那段:3.9 MB、2.0 MB、1.0 MB。 这个量级说明「量化省空间」在个人库上是伪需求。一万段才 39 MB,一个模型动辄几百 MB。 诚实说明:这里用一层矩阵乘模拟嵌入模型,真实模型有几十层,还有分词和归一化。 但「批处理提升吞吐」这个效应的方向和量级是真的,换真模型一样成立。
30 秒实验
- 把批大小从 64 加到 1024,看 GFLOPS 还涨不涨、内存占用变成多少。收益有上限,内存没有。
- 把 768 维改成 1024 维(换更大的模型),再跑一遍。 计算量按维度平方涨,所以耗时也涨得比参数量快。
- 把总条数从 4096 改成 40960,看批大小 1 和 256 的差距会不会变化。 总条数越大,调用开销被摊得越薄,但 1 那档的固定损耗还在。
常见误区
- 「上传完显示完成就行」。扫描件和乱码也会显示完成,只是检不到东西。建完库问一句验证。
- 「一条一条传比较稳」。批处理快近十倍,而且失败重试的成本也更低。一次传几十份。
- 「向量要量化省空间」。1000 段才 3.9 MB,量化省几 MB 却要付精度代价。百万级才值得。
自测三问(点开对答案)
1. 为什么向量化要批处理? —— 因为矩阵乘很快,慢的是每次调用的固定开销。批处理把开销摊到更多样本上,吞吐能差近十倍。 2. 「显示完成但检索不到」通常是什么原因? —— 扫描件没有文字层,或者编码不对读出乱码。两者都会被正常「向量化」,只是检不到东西。 3. 为什么个人知识库不需要量化向量? —— 因为向量本身就很小(1000 段 1024 维才 3.9 MB),量化省的是几 MB, 却要付精度损失和额外的转换开销。文件进去了,接下来是查询那一侧:「相似度多少才算命中」这个阈值,为什么抄不得。
- ☐ (本节完,下一节 6.2.4 搜索知识库)