运行 LLM 嵌入模型在 CPU 上速度很慢,在 GPU 上成本很高。我们将使用ONNX 模型量化将其速度提高 3 倍,看看不同的int8 格式如何影响新旧硬件上的性能,并在量化模型的基础上进一步进行ONNX 变压器优化。
无人谈论的向量搜索问题
要使用 LLM 嵌入执行语义搜索,您必须先计算这些嵌入。不幸的是,在市场上有许多向量搜索数据库的情况下,计算嵌入被认为是事后才考虑的问题,并且超出了范围。
嵌入推理是任何语义搜索系统的首要步骤。图片由作者提供。
一种新的开源搜索引擎Nixiesearch,它可以根据您的数据微调嵌入。由于我们在服务器端处理嵌入,因此我们并不惊讶地看到在 CPU 上运行嵌入对性能的巨大影响。
使用 e5-small-v2 嵌入模型的 Nixiesearch 索引过程的火焰图。图片由作者提供。
上面的火焰图显示,95% 的 CPU时间都花在了计算嵌入上。当然,您可以通过将索引切换到 GPU 来加快速度,但对于想要尝试 Nixiesearch 的人来说,这仍然是一个很大的遗憾。我们能否在仍使用 CPU 的情况下加快速度?
模型量化
目前所有的深度神经网络都只是矩阵运算的华丽组合。
注意层矩阵操作。图片来自 Vaswani 等人的《Attention Is All You Need》。
如上图所示,Transformer 网络的注意层只是矩阵之上的简单代数变换的组合。在经典实现中,这些矩阵包含 32 位浮点值。如果我们准备牺牲一点精度来获得更好的性能,将浮点大小从 32 字节减少到 8 字节,会怎么样?
32 位浮点矩阵乘法。图片由作者提供。
这种方法称为量化:降低用于存储模型权重和神经元激活值的矩阵的数值精度。
精度从 32 位提高到 8 位将使存储模型权重所需的 RAM 大小减少 4 倍,并有望使其在现代 CPU 上运行得更快。
但是,您不能将 32 位浮点数恰好放入 8 位整数而不会造成任何损失:每个数字只有 8 位存储空间,因此您只能编码 256 个不同的值!
训练好的 TinyBERT 中的权重分布。图片来自 J. Jin 等人的《KDLSQ-BERT:结合知识蒸馏和学习步长量化的量化 Bert》。
但神经网络中的权重和激活值并不是随机数!从上面的直方图可以看出,它们通常接近于零,并且呈正态分布。通过这一观察,我们可以将所有网络操作替换为量化感知操作——同时跟踪底层数值分布的零点和尺度。
ONNX 中基于运算符的量化。图片由作者提供。
这种方法称为运算符量化,被认为是解决问题的最直接方法。另一种选择是 QDQ 量化,您仍然使用相同的 32 位运算符,但在常规运算符之前注入量化-反量化运算符对 – 这通常要慢得多。
运算符与 QDQ 量化。来自 ONNX 文档 — 量化 ONNX 模型的图片。
在上图中,您可以看到在 QDQ 模式下向图中注入了额外的节点,这通常会导致更糟糕的性能:
- 额外的量化-反量化运算符不是免费的。
- 主要运算符像以前一样在 Float32数据上运行,因此您只能节省 RAM/VRAM 的使用,而不会节省性能。
- QDQ 仅支持ONNX 运行时的静态量化– 有关详细信息,请参阅下一章。
为了简单起见,本文我们只针对运算符量化。
请注意,模型量化与所有主流向量搜索引擎(如 Elasticsearch、Qdrant、Vespa 和 Weaviate)支持的嵌入量化不同。量化模型仍像以前一样发出 Float32 嵌入– 它只是对权重和激活使用更紧凑的布局。
动态量化与静态量化
对于图中每个操作,计算零点和尺度量化参数有两个选项:
- 动态:每个操作符在运行时为每个批次重新计算这些参数。它更耗费资源,但也更精确——这些参数可能会在批次之间漂移。
- 静态:我们不会每次都为每个批次重新计算这些参数,而是进行离线校准——使用一个很小的数据集前馈网络,并根据观察到的分布静态记录量化参数。这没有额外的开销,但由于量化参数是静态的,因此在批次之间出现漂移的情况下,它们可能不完美。
静态量化的主要缺点是需要进行校准,这是一个额外的手动步骤。由于静态量化通常会导致精度下降,因此我们将在本文中重点介绍动态量化方法。
LLM 推理运行时
您在 HuggingFace Hub 上找到的模型文件仅包含您需要在模型权重之上执行的矩阵运算的定义(执行图)。
执行运行时是执行实际矩阵乘法的部分。图片由作者提供。
执行运行时会在您的硬件上解释并执行此图:
- PyTorch 和 TensorFlow – 用于模型开发,以 Python 为中心,但也有其他语言(如 C/C++)的低级绑定。实际上,两者都针对 GPU 上的训练和批处理进行了更优化。
- OpenVINO — 英特尔推出的以 CPU 为中心的运行时,仅限 Python/C++。
- ONNX — 一个开放的多语言(Java/JS/WASM/C++/Python)和多后端(CPU/GPU/TPU)运行时。
- TensorRT ——Nvidia 推出的 ONNX 兼容运行时,专门用于 GPU 执行。
Nixiesearch 与Apache Lucene绑定,作为 JVM 应用程序实现——并且其他开源搜索引擎选择使用 ONNX 作为神经网络的主要执行运行时。
将模型转换为 ONNX
您需要先进行转换才能在 ONNX 运行时内执行模型。
ONNX 模型转换流程。图片由作者提供。
为了实现这样的转换,我们开发了一个工具nixiesearch/onnx-convert,它是原始xenova/transformers.js项目中转换脚本的扩展版本。转换后的模型文件与任何 ONNX 风格的搜索引擎兼容,并且可以在Elasticsearch Inference Processor、Vespa Embedder和直接在Nixiesearch中使用。
nixiesearch/onnx-convert转换工具。图片由作者提供。
ONNX 转换+量化过程有多个重要的可调参数,影响性能和质量:
- 底层量化格式:可以是有符号/无符号的 8 位整数和 16 位浮点数。这会对性能和精度产生什么影响?
- ONNX Transformer 优化器:ONNX 可以将多个典型运算符融合到单个优化核心中,用于注意力块等。这真的很重要吗?它的级别如何影响最终推理延迟?
除了这些可变参数之外,我们将其他不太重要的参数固定为推荐的常量值:
- ONNX opset :ONNX 有多个版本,对数据类型和运算符的支持不同。我们选择了Python onnx 包支持的最新opset=17 。
- 每通道量化:图表是否应跟踪单个张量尺度和零点值,还是应按通道(张量中的切片)进行?关闭它可能会略微提高性能并降低精度 — 但由于需要使用额外的内存,我们选择默认启用它。
- 7 位比例:作为对数字溢出的额外保护,8 位值是否应该缩小一点?ONNX 文档指出,它可能会提高没有 AVX-VNNI 的旧硬件的精度。
我们将采用E5-v2系列嵌入模型,采用小型、基础和大型变体来查看每个变化对不同大小模型的影响。
QUint8/QInt8/Float16 和推理延迟
我们将e5-small-v2、e5-base-v2和e5–large-v2转换为QUint8、QInt8和Float16数值类型,仍然没有进行任何优化:
ONNX 转换工具。图片由作者提供。
并运行嵌入基准测试套件:
- 对于每种尺寸的每个模型,使用 JVM 中的 onnxruntime 计算嵌入延迟。
- 该套件运行在最新一代 AWS M7I.2xlarge实例上,该实例具有 8 个支持 AVX-VNNI 的 VCPU。
Float32 与其他格式之间的相对推理时间改进。值越高越好。图片来自作者。
上表显示,“对于 e5-small-v2 模型,4 个 token 的 QUInt8 格式比 Float32快 1.38 倍”。我们得到的结果相当令人惊讶:
- 好消息是,我们对基础模型和大型模型的推理速度提高了 3 倍以上!
- 在 VNNI 硬件上,QUInt8 和 QInt8 差别不大。
- 混合精度Float16 格式意外地比基线慢了 2 到 7 倍。这一令人惊讶的事实源于现代 CPU 缺乏对 FP16/BF16 的支持:只有支持 AMX 的最新 Intel Sapphire Rapids Xeon CPU才能原生处理这些数据类型。没有 AMX 的 CPU 每次发现 Float16 类型时都会执行向下转换-向上转换操作。
因此,你显然应该量化你的模型,但是如果你的硬件不支持 VNNI 怎么办?
AVX-VNNI 的影响
我们明确提到 CPU 中的 AVX-VNNI 支持是有原因的。在撰写本文时,作者无法复制文章“使用Vespa 加速基于 Transformer 的嵌入检索”中关于模型量化的性能数字。根本原因是 AMD Zen2 CPU 不支持 VNNI 指令集。
VNNI 是矢量神经网络指令的 AVX 扩展,受 2019 年以后生产的 Intel CPU 和 AMD Zen 4+ 支持:
VNNI CPU 支持。图片来自https://en.wikichip.org/wiki/x86/avx512_vnni。
在非 VNNI AMD Zen2 CPU 上,结果截然不同:
非 VNNI CPU 与 Float32 基线的相对推理差异。值越低越好。图片由作者提供。
有两个主要的重要观察结果:
- 对于QInt8/QUInt8,1.2x-1.6x 的加速不如VNNI CPU 显著。
- 与 VNNI CPU 相比,QInt8 和 QUInt8 之间存在差异:有符号的QInt8 几乎总是比无符号的 QUInt8 快 15% 左右。
因此,即使没有 VNNI 支持,量化的延迟改进仍然是值得的。
优化模型
以上所有结果都是使用 1–1 PyTorch 到 ONNX 转换模型得出的。但 ONNX 有一组专门用于 Transformer LLM 的融合内核,例如 QAttention。融合内核是一组一起执行的矩阵运算,通常以高度优化的方式执行。
矩阵操作链被替换为 QAttention 块,并作为单个 BLAS GEMM 方法实现。图片由作者提供。
因此,您无需使用许多中间张量执行多个单独的操作(例如 MatMul-Scale-Mask 等),只需用单个 QAttention 块替换它们即可,从技术上讲,这是一个BLAS GEMM 函数调用。
ONNX 有 4 个优化器级别:
- 0:执行 1–1 翻译,不进行任何优化。
- 1:像上面描述的 QAttention 那样进行仅图形融合。
- 2:与 1 相同,但也将融合应用于仅限 Python 的块。
- 99:与 2 相同,但采用 RELU 近似。
在每个级别之上堆叠相对改进。图片由作者提供。
上表可以理解为“对于具有 64 个 token 输入的 QInt8 格式,使用级别 1 的优化可使推理速度提高 33%,而级别 2 在级别 1 的基础上额外提高了 4%”。
主要观察结果如下:
- ONNX 优化产生了影响:QInt8/QUInt8 速度提高了约 30%!即使对于非量化模型,仍有约 10% 的改进。
- 级别 1 显著改善了延迟,而级别 2 几乎总是在此基础上额外提供 3–4% 的延迟。
- 与级别 2 相比,级别 99 的性能略有降低,降低幅度为 1-2% 。
因此,您还应该执行 ONNX 优化,因为它可以使您的量化模型更快,而且无需额外成本。
BEIR/MTEB 的嵌入质量
既然量化过程并非无损,那么它会如何影响嵌入质量?我们将使用BEIR-MTEB 参考基准的子集来查看量化模型的质量如何下降。
NGCG@10 在 3 个 BEIR 数据集上。Float32 是没有任何量化的基线。由于量化模型在 CPU 上运行时间过长,因此只有小模型和三个数据集。图片由作者提供。
我们可以得出几个令人惊讶的结论:
- Float16量化在这三个数据集上几乎没有性能下降。但考虑到它在 CPU 上运行时对性能有严重影响,这不是一个好选择。
- QUint8和QInt8 的结果稍差一些,但考虑到它们的速度,这种下降仍然可以被认为是可以容忍的。
- 每通道量化和 7 位缩放是两个重要参数:量化期间不使用它们通常会导致精度显著下降。
因此,在选择量化参数时,最好是格外准确:做错可能会导致严重的质量损失。我们建议使用onnx-convert 工具,以免弄巧成拙。
结论
因此,如果您正在寻找一种方法来改善嵌入模型的推理延迟,那么您绝对应该考虑 ONNX 量化和优化组合。以下是我们针对e5–base-v2模型获得的绝对前后数字:
以毫秒为单位的绝对推理数字,Float32 优化=0,QInt8 优化=2。图片由作者提供。
通过这种方法,我们可以将e5-base-v2 模型上长文档的延迟从 50 毫秒缩短到 15 毫秒,也就是延迟提高了 3.5 倍!
模型量化是改善嵌入模型推理延迟的绝佳方法:
- 推理能力可以提高 2 至 4 倍,但代价是质量会略有下降。
- 由于在非 VNNI CPU 上性能更好,因此更喜欢 QInt8 格式。
- 坚持ONNX 优化级别 1 和 2 — 它们是合理的选择,且无需额外成本。
- 优先使用 7 位缩放进行每通道量化:这会导致模型质量下降最小。
本文测试的所有 E5 模型均在https://huggingface.co/nixiesearch上发布,有三种版本:原始 ONNX 非优化版本、优化版本和优化+量化版本。
huggingface.co/nixiesearch 命名空间。图片由作者提供。
但使用 onnx-convert 工具,您可以将开源和专有模型转换为优化和量化的 ONNX 格式/
RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/4260