🧠 第六章 语言模型训练:高效赋能业务场景
📄 一句话概括

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 秒实验

  1. 把批大小从 64 加到 1024,看 GFLOPS 还涨不涨、内存占用变成多少。收益有上限,内存没有。
  2. 把 768 维改成 1024 维(换更大的模型),再跑一遍。 计算量按维度平方涨,所以耗时也涨得比参数量快。
  3. 把总条数从 4096 改成 40960,看批大小 1 和 256 的差距会不会变化。 总条数越大,调用开销被摊得越薄,但 1 那档的固定损耗还在。

常见误区

  1. 「上传完显示完成就行」。扫描件和乱码也会显示完成,只是检不到东西。建完库问一句验证。
  2. 「一条一条传比较稳」。批处理快近十倍,而且失败重试的成本也更低。一次传几十份。
  3. 「向量要量化省空间」。1000 段才 3.9 MB,量化省几 MB 却要付精度代价。百万级才值得。
自测三问(点开对答案) 1. 为什么向量化要批处理? —— 因为矩阵乘很快,慢的是每次调用的固定开销。批处理把开销摊到更多样本上,吞吐能差近十倍。 2. 「显示完成但检索不到」通常是什么原因? —— 扫描件没有文字层,或者编码不对读出乱码。两者都会被正常「向量化」,只是检不到东西。 3. 为什么个人知识库不需要量化向量? —— 因为向量本身就很小(1000 段 1024 维才 3.9 MB),量化省的是几 MB, 却要付精度损失和额外的转换开销。

文件进去了,接下来是查询那一侧:「相似度多少才算命中」这个阈值,为什么抄不得。

相关笔记