![](https://oscimg.oschina.net/oscnet/237170ff-7df3-48aa-b0d8-70a968be3a0e.gif)
在大语言模型(LLM)端侧部署上,基于 MNN 实现的 mnn-llm 项目已经展现出业界领先的性能,特别是在 ARM 架构的 CPU 上。目前利用 mnn-llm 的推理能力,qwen-1.8b在mnn-llm的驱动下能够在移动端达到端侧实时会话的能力,能够在较低内存(<2G)的情况下,做到快速响应。
![](https://oscimg.oschina.net/oscnet/d467474a-4481-45cc-8602-d7bc0d58fbf3.png)
在大型语言模型(LLM)领域的迅猛发展背景下,开源社区已经孵化了众多优异的 LLM 模型。这些模型在自然语言处理的各个领域展现出了强大的能力,但同时也带来了一个挑战,即如何有效地将这些模型部署到端侧设备上。为此,原来只支持特定模型的 ChatGLM-MNN 项目已升级并更名为 mnn-llm,并集成到了MNN项目中;该项目支持多个目前主流的开源LLM模型的部署。与此同时,为了简化不同 LLM 模型向 ONNX 格式导出的流程,我们推出了 llm-export 项目。该项目为多种 LLM 模型提供了统一的导出方案,大大地降低了从预训练模型导出的复杂度。在本文中,我们将介绍LLM模型的导出与部署支持与MNN针对LLM端侧CPU推理的性能优化方案。
mnn-llm地址:https://github.com/wangzhaode/mnn-llm llm-export地址:https://github.com/wangzhaode/llm-export
![](https://oscimg.oschina.net/oscnet/8bbf9c73-a28e-4ff3-aa45-0d03ffaad597.png)
![](https://oscimg.oschina.net/oscnet/33902c1e-6dbe-47f3-84a2-6fa4bbeebb1f.jpg)
class LLM(torch.nn.Module):
def __init__(self, args):
super().__init__()
# load tokenizer, embed, blocks, lm
self.load_model(args.path)
def forward(self, input_ids, attention_mask, position_ids, past_key_values):
hidden_states = self.embed(input_ids)
presents = []
for i in range(self.block_nums):
hidden_states, kv = self.blocks[i](hidden_states, attention_mask,
position_ids, past_key_values[i])
presents.append(kv)
token_id = self.lm(hidden_states).view(1)
presents = torch.stack(presents)
return token_id, presents
def export(self):
# export llm to onnx and mnn
...
class Chatglm2_6b(LLM):
def load_model(self, model_path: str):
# chatglm2 load impl
...
class Qwen_7b(LLM):
def load_model(self, model_path: str):
# qwen load impl
...
![](https://oscimg.oschina.net/oscnet/8adf0ec8-a3ed-46b3-9d71-ffd8c6fb859c.png)
在部署大型语言模型(LLM)时,兼容性和易用性是关键因素。为了解决这一挑战,我们开发了一个名为 mnn-llm 的项目。考虑到MNN在跨平台上支持上的优秀表现,该项目基于 MNN 构建,旨在为各种平台提供一个统一的 LLM 模型部署解决方案。mnn-llm 项目使得从 llm-export 导出的模型能够无缝进行端到端的推理,并为开发者提供了一个简易的文本到文本(txt2txt)调用接口。
在mnn-llm中我们移植实现了目前主流的tokenizer工具:Sentencepiece 和 Tiktoken。这些 tokenizer 组件是处理自然语言输入的关键部分,它们能够将原始文本转换为模型能理解的格式。同时为了轻量化,两种模型都使用文本的方式存储,移除了Sentencepiece中迪对protobuf的依赖。此外,考虑到内存占用在移动设备上尤为宝贵,我们还在 mnn-llm 中引入了 disk embedding 功能。这意味着用户可以根据需要选择:在模型推理过程中使用 embedding 模型在内存计算,或者直接从磁盘加载 embedding 值。这种灵活性不仅降低了运行时的内存需求,也为用户提供了更多的选择来平衡推理性能和资源使用。为了确保 mnn-llm 的通用性和扩展性,我们设计了一种易于扩展的架构。开发者可以通过继承基类并实现特定的配置来支持不同的 LLM 模型。这种架构设计使得整合新的 LLM 模型变得简单快捷,大大降低了将最新的研究成果应用到实际产品中的门槛。
![](https://oscimg.oschina.net/oscnet/8661bf72-ddbe-4f58-b9a6-8d1df4c875b9.png)
![](https://oscimg.oschina.net/oscnet/ac61d605-f614-44be-a008-25fc1f7b8f05.png)
▐ 性能分析
-
在prefill阶段,Linear算子的耗时占比相对稳定,超过了93%,而MatMul和Memory算子的耗时占比分别约为3%和2%; -
在decode阶段,随着m+n的增长,Linear算子的时间占比有所下降,而MatMul和Memory算子的占比有所上升。尽管如此,在多数情况下,耗时主要集中在Linear算子上。
▐ 优化策略
-
对于计算密集型的优化:我们关注于使用更强大的计算指令,选择计算峰值更高的SIMD指令集来完成核心的计算,同时使用汇编实现多种规模的kernel,以此来加速矩阵乘法操作。 -
对于访存密集型的优化:我们的策略是通过降低数据的位宽来减少内存访问量(量化技术)和数据重排来实现。我们选择了 W4A8 的量化方案,即将模型中的权重(W)量化到 4 位,而将激活值(A,即模型的输入和输出)量化到 8 位。这样做可以大幅减少模型的内存占用,并提升性能,因为较低位宽的数据需要更少的内存带宽来读写;同时针对W4A8的量化方案按照设备支持的最优SIMD计算指令对数据进行特定的重排以提升内存局部性,提升访存效率。
![](https://oscimg.oschina.net/oscnet/238512df-cfd0-462d-a460-0e8e8863c7af.png)
采用这种量化方法的一个巨大优势在于,计算过程中模型的权重访存量可以减少四倍,从而有效提高访存性能。
在量化权重的同时,我们也审视了模型输入的处理方式。过去,我们利用了混合精度计算,即结合了4位量化的权重与16位或32位浮点的输入。这要求我们在计算前将量化后的权重解量化为浮点数,同时从内存中加载浮点型的输入数据,接着进行浮点乘加运算。这种方法在处理输入时需要较多的内存访问,并且由于权重是以行优先的方式存储的,这进一步增加了内存访问量。另外,由于涉及浮点计算,它使用浮点型的SIMD指令,其峰值性能低于整数型指令。
为了应对这些问题,我们采取了针对输入的动态量化方案,即W4A8的量化方案。首先我们需要在运行时统计输入的分布情况,使用absmax-quant的方案将输入两位8bit整形作为线性性层的如数。然后我们将4bit量化的权重加载并转换为8位整数值,使用8位整数来完成矩阵乘的乘累加操作,使用32位整数来保存累加结果。最终,在整个计算过程结束后,我们会将这些累加结果反量化回浮点数。采用W4A8技术不仅显著降低了内存访问量,还使我们能够利用更高算力的整数计算指令,大幅提升了模型的计算性能。
![](https://oscimg.oschina.net/oscnet/35375158-3e65-433b-96fe-4b1ea2b9bfc3.jpg)
vfmadd132ps
这类SIMD指令。相对地,当转向W4A8方案,即使用8位整数(int8)来执行计算,我们可以利用vpmaddubsw
这类更高效的整数指令。在ARM架构的系统中,浮点计算使用fmla
指令,而在采用8位整数计算时,可以使用如sdot
或smmla
的指令。性能测试显示,在x86平台上,vpmaddubsw
的峰值性能几乎是vfmadd132ps
的三倍。而在ARM平台上,smmla
指令的峰值性能则是fmla
的四倍。这一显著提升反映了通过优化指令选择可实现的算力增益。除了提升计算性能外,内存访问效率也是模型优化中不可或缺的一环。以一个具有4096x4096大小权重矩阵的广义矩阵向量乘法(GEMV)为例,我们将以传统的32位浮点(fp32)权重和输入数据的内存访问量作为基准。与此基准相比,W4A8方案明显减少了内存访问量,降幅超过五倍。即使与W4A16和W4A32方案相比,W4A8方案仍实现了两到三倍的访存量减少。
![](https://oscimg.oschina.net/oscnet/1dd0291c-9287-49cb-8c5e-9b26d44444c3.jpg)
同时为了提升内存局部性,减少访存开销,我们结合W4A8的计算模式与设备支持的计算指令对输入和权重做了特定的数据重排。
具体来说,考虑到输入数据的形状通常为 [batch, ic],权重的形状为 [oc, ic],我们将这些数据重新排列为 [ic/pack, batch, pack] 和 [oc/pack, ic/pack, pack, pack]。这里的 "pack" 是一种按照硬件支持的计算指令精心选取的分块大小。例如,当系统支持 smmla
指令时,我们选择 pack = 8;而当系统支持 sdot
指令时,我们选择 pack = 4;当系统支持AVX2
时选择pack = 8;而当系统仅支持SSE
时,选择使pack = 4。这种重排策略使得计算核心(kernel)能够执行更为紧凑的矩阵乘法操作:[batch, pack] 与 [pack, pack] 的矩阵相乘,生成 [batch, pack] 的结果矩阵。我们进一步针对可用的寄存器数量,实现了针对 batch 维度的不同的计算Kernel,可以充分利用计算器来提升访存效率。在大型语言模型(LLM)的线性层中,输出通道的数量(oc)往往较大。这为我们提供了在 [oc/pack] 维度上执行多线程并行计算的机会,能够充分利用现代多核能力,提升多核性能。
以下是ARM上针对smmla
指令的矩阵乘kernel核心代码:
LoopSz_TILE_2:
// src : 1 x [2 x 8] : v4
// weight : 4 x [2 x 8] : v0-3
// dst : 1 x 4 x [4] : v16-19
ld1 {v0.16b, v1.16b}, [x25], #32 // weight
// int4 to int8: v0, v1, v2, v3
ushr v8.16b, v0.16b, #4
and v9.16b, v0.16b, v14.16b
sub v8.16b, v8.16b, v15.16b
sub v9.16b, v9.16b, v15.16b
ushr v10.16b, v1.16b, #4
and v11.16b, v1.16b, v14.16b
sub v10.16b, v10.16b, v15.16b
sub v11.16b, v11.16b, v15.16b
zip1 v0.16b, v8.16b, v9.16b
zip2 v1.16b, v8.16b, v9.16b
zip1 v2.16b, v10.16b, v11.16b
zip2 v3.16b, v10.16b, v11.16b
ld1 {v4.16b}, [x24], x15 // src
.inst 0x4e80a490 // smmla v16.4s, v4.16b, v0.16b
.inst 0x4e81a491 // smmla v17.4s, v4.16b, v1.16b
.inst 0x4e82a492 // smmla v18.4s, v4.16b, v2.16b
.inst 0x4e83a493 // smmla v19.4s, v4.16b, v3.16b
subs x26, x26, #1
bne LoopSz_TILE_2
![](https://oscimg.oschina.net/oscnet/039da724-6349-4c4c-ad84-817555dd74d8.png)
测试模型主要选取1.8b, 6b与7b的模型,其中6b, 7b模型均使用4bit量化;1.8b模型分别测试了4bit和8bit量化。均使用CPU-4线程测试,android使用fp16, 其他平台使用fp32,具体测试环境如下:
![](https://oscimg.oschina.net/oscnet/af965613-a0d9-4289-ac9c-779b0925d43a.png)
分别测试llm的prefill速度与decode速度,
prefill速度计算:输入token数目为m的prompt, 计算得到第一个输出token的时间为t1, 则prefill_speed = m / t1
decode速度计算:在输出第二个token开始直到结束输出token数为n, 耗时为t2, 则decode_speed = n / t2
各模型的速度如下图所示:
![](https://oscimg.oschina.net/oscnet/1f8c263f-1f49-4252-9740-40945d9847cf.jpg)
在CPU上选择了与llama.cpp和fastllm进行性能对比,模型选取3个框架都支持的llama2-7b,均采用4bit量化;输入采用fp32。测试结果表明在ARM CPU上mnn-llm的性能大幅领先;在x86上decode速度略慢,prefill速度有较大领先。同时我们选择了与编译方法实现的llm框架mlc-llm作对比,但是mlc-llm官方不支持CPU部署,因此我们对比了mlc-llm在相同的Android设备上的GPU性能。
![](https://oscimg.oschina.net/oscnet/4424ba19-c314-4954-82bf-b2639e5d84ec.png)
qwen-1.8b-apk地址:https://github.com/wangzhaode/mnn-llm/releases/tag/qwen-1.8b-apk
![](https://oscimg.oschina.net/oscnet/a7b5b956-22e8-4612-a852-8afbfdbb72a6.png)
-
https://github.com/alibaba/MNN/tree/master/llm
-
https://github.com/wangzhaode/llm-export -
https://github.com/wangzhaode/mnn-llm
![](https://oscimg.oschina.net/oscnet/2110a19f-411b-4a07-abdb-d3e3a8e09235.png)
本篇内容作者:雁行
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。