目录
LMDeploy 的量化和部署
本节课的笔记,建议大家直接使用InternStudio官方开发机进行复现。由于lmdeploy的TurboMind当前只支持FP16和INIT4,P40无法转换模型;然后尝试用lmdeploy的torch版进行8bit模型转换,成功转换为了8bit模型,但是推理仍然失败了,老师给出的原因还是triton对P40的支持问题。因此P40进行不下去了,我会把详细报错信息放在最后供大家参考。
1 环境配置
创建新的conda环境
conda create -n lmdeply
然后激活环境。
conda activate lmdeploy
注意,环境激活后,左边会显示当前(也就是 lmdeploy
)的环境名称,如下图所示。
可以进入Python检查一下 PyTorch 和 lmdeploy 的版本。由于 PyTorch 在官方提供的环境里,我们应该可以看到版本显示,而 lmdeploy 需要我们自己安装,此处应该会提示“没有这个包”,如下图所示。
lmdeploy 没有安装,我们接下来手动安装一下,建议安装最新的稳定版。 如果是在 InternStudio 开发环境,需要先运行下面的命令,否则会报错。
# 解决 ModuleNotFoundError: No module named 'packaging' 问题
pip install packaging
# 使用 flash_attn 的预编译包解决安装过慢问题
pip install /root/share/wheels/flash_attn-2.4.2+cu118torch2.0cxx11abiTRUE-cp310-cp310-linux_x86_64.whl
安装lmdeploy[all],新的conda环境如果报有依赖不存在,则安装即可
pip install 'lmdeploy[all]==v0.1.0'
由于默认安装的是 runtime 依赖包,但是我们这里还需要部署和量化,所以,这里选择 [all]
。然后可以再检查一下 lmdeploy 包,如下图所示。
基础环境到这里就配置好了。
如果遇到lmdeploy: command not found,
一般情况下是下到了'/root/.local/bin',增加一下环境变量:
export PATH=$PATH:'/root/.local/bin'
2 服务部署
这一部分主要涉及本地推理和部署。我们先看一张图。
我们把从架构上把整个服务流程分成下面几个模块。
- 模型推理/服务。主要提供模型本身的推理,一般来说可以和具体业务解耦,专注模型推理本身性能的优化。可以以模块、API等多种方式提供。
- Client。可以理解为前端,与用户交互的地方。
- API Server。一般作为前端的后端,提供与产品和服务相关的数据和功能支持。
值得说明的是,以上的划分是一个相对完整的模型,但在实际中这并不是绝对的。比如可以把“模型推理”和“API Server”合并,有的甚至是三个流程打包在一起提供服务。
接下来,我们看一下lmdeploy提供的部署功能。
2.1 模型转换
使用 TurboMind 推理模型需要先将模型转化为 TurboMind 的格式,目前支持在线转换和离线转换两种形式。在线转换可以直接加载 Huggingface 模型,离线转换需需要先保存模型再加载。
TurboMind 是一款关于 LLM 推理的高效推理引擎,基于英伟达的 FasterTransformer 研发而成。它的主要功能包括:LLaMa 结构模型的支持,persistent batch 推理模式和可扩展的 KV 缓存管理器。
2.1.1 在线转换
mdeploy 支持直接读取 Huggingface 模型权重,目前共支持三种类型:
- 在 huggingface.co 上面通过 lmdeploy 量化的模型,如 llama2-70b-4bit, internlm-chat-20b-4bit
- huggingface.co 上面其他 LM 模型,如 Qwen/Qwen-7B-Chat
示例如下:
# 需要能访问 Huggingface 的网络环境
lmdeploy chat turbomind internlm/internlm-chat-20b-4bit --model-name internlm-chat-20b
lmdeploy chat turbomind Qwen/Qwen-7B-Chat --model-name qwen-7b
上面两行命令分别展示了如何直接加载 Huggingface 的模型,第一条命令是加载使用 lmdeploy 量化的版本,第二条命令是加载其他 LLM 模型。
我们也可以直接启动本地的 Huggingface 模型,如下所示。
lmdeploy chat turbomind /share/temp/model_repos/internlm-chat-7b/ --model-name internlm-chat-7b
以上命令都会启动一个本地对话界面,通过 Bash 可以与 LLM 进行对话。
2.1.2 离线转换
离线转换需要在启动服务之前,将模型转为 lmdeploy TurboMind 的格式,如下所示。
# 转换模型(FastTransformer格式) TurboMind
lmdeploy convert internlm-chat-7b /path/to/internlm-chat-7b
这里我们使用官方提供的模型文件,就在用户根目录执行,如下所示。
lmdeploy convert internlm-chat-7b /root/share/temp/model_repos/internlm-chat-7b/
执行完成后将会在当前目录生成一个 workspace
的文件夹。这里面包含的就是 TurboMind 和 Triton “模型推理”需要到的文件。
目录如下图所示:
weights
和 tokenizer
目录分别放的是拆分后的参数和 Tokenizer。如果我们进一步查看 weights
的目录,就会发现参数是按层和模块拆开的,如下图所示:
每一份参数第一个 0 表示“层”的索引,后面的那个0表示 Tensor 并行的索引,因为我们只有一张卡,所以被拆分成 1 份。如果有两张卡可以用来推理,则会生成0和1两份,也就是说,会把同一个参数拆成两份。比如 layers.0.attention.w_qkv.0.weight
会变成 layers.0.attention.w_qkv.0.weight
和 layers.0.attention.w_qkv.1.weight
。执行 lmdeploy convert
命令时,可以通过 --tp
指定(tp 表示 tensor parallel),该参数默认值为1(也就是一张卡)。
关于Tensor并行
Tensor并行一般分为行并行或列并行,原理如下图所示:
简单来说,就是把一个大的张量(参数)分到多张卡上,分别计算各部分的结果,然后再同步汇总。
2.2 TurboMind 推理+命令行本地对话
模型转换完成后,我们就具备了使用模型推理的条件,接下来就可以进行真正的模型推理环节。
我们先尝试本地对话(Bash Local Chat
),下面用(Local Chat 表示)在这里其实是跳过 API Server 直接调用 TurboMind。简单来说,就是命令行代码直接执行 TurboMind。所以说,实际和前面的架构图是有区别的。
这里支持多种方式运行,比如Turbomind、PyTorch、DeepSpeed。但 PyTorch 和 DeepSpeed 调用的其实都是 Huggingface 的 Transformers 包,PyTorch表示原生的 Transformer 包,DeepSpeed 表示使用了 DeepSpeed 作为推理框架。Pytorch/DeepSpeed 目前功能都比较弱,不具备生产能力,不推荐使用。
执行命令如下:
# Turbomind + Bash Local Chat
lmdeploy chat turbomind ./workspace
启动后就可以和它进行对话了,如下图所示:
输入后两次回车,退出时输入exit
回车两次即可。此时,Server 就是本地跑起来的模型(TurboMind),命令行可以看作是前端。
3 模型量化
本部分内容主要介绍如何对模型进行量化。主要包括 KV Cache 量化和模型参数量化。总的来说,量化是一种以参数或计算中间结果精度下降换空间节省(以及同时带来的性能提升)的策略。
正式介绍 LMDeploy 量化方案前,需要先介绍两个概念:
- 计算密集(compute-bound): 指推理过程中,绝大部分时间消耗在数值计算上;针对计算密集型场景,可以通过使用更快的硬件计算单元来提升计算速。
- 访存密集(memory-bound): 指推理过程中,绝大部分时间消耗在数据读取上;针对访存密集型场景,一般通过减少访存次数、提高计算访存比或降低访存量来优化。
常见的 LLM 模型由于 Decoder Only 架构的特性,实际推理时大多数的时间都消耗在了逐 Token 生成阶段(Decoding 阶段),是典型的访存密集型场景。
那么,如何优化 LLM 模型推理中的访存密集问题呢? 我们可以使用 KV Cache 量化和 4bit Weight Only 量化(W4A16)。KV Cache 量化是指将逐 Token(Decoding)生成过程中的上下文 K 和 V 中间结果进行 INT8 量化(计算时再反量化),以降低生成过程中的显存占用。4bit Weight 量化,将 FP16 的模型权重量化为 INT4,Kernel 计算时,访存量直接降为 FP16 模型的 1/4,大幅降低了访存成本。Weight Only 是指仅量化权重,数值计算依然采用 FP16(需要将 INT4 权重反量化)。
3.1 KV Cache 量化
3.1.1 量化步骤
KV Cache 量化是将已经生成序列的 KV 变成 Int8,使用过程一共包括三步:
第一步:计算 minmax。主要思路是通过计算给定输入样本在每一层不同位置处计算结果的统计情况。
- 对于 Attention 的 K 和 V:取每个 Head 各自维度在所有Token的最大、最小和绝对值最大值。对每一层来说,上面三组值都是
(num_heads, head_dim)
的矩阵。这里的统计结果将用于本小节的 KV Cache。 - 对于模型每层的输入:取对应维度的最大、最小、均值、绝对值最大和绝对值均值。每一层每个位置的输入都有对应的统计值,它们大多是
(hidden_dim, )
的一维向量,当然在 FFN 层由于结构是先变宽后恢复,因此恢复的位置维度并不相同。这里的统计结果用于下个小节的模型参数量化,主要用在缩放环节(回顾PPT内容)。
第一步执行命令如下:
# 计算 minmax lmdeploy lite calibrate \ --model /root/share/temp/model_repos/internlm-chat-7b/ \ --calib_dataset "c4" \ --calib_samples 128 \ --calib_seqlen 2048 \ --work_dir ./quant_output
在这个命令行中,会选择 128 条输入样本,每条样本长度为 2048,数据集选择 C4,输入模型后就会得到上面的各种统计值。值得说明的是,如果显存不足,可以适当调小 samples 的数量或 sample 的长度。
这一步由于默认需要从 Huggingface 下载数据集,国内经常不成功。所以我们导出了需要的数据,大家需要对读取数据集的代码文件做一下替换。共包括两步:
- 第一步:复制
calib_dataloader.py
到安装目录替换该文件:cp /root/share/temp/datasets/c4/calib_dataloader.py /root/.conda/envs/lmdeploy/lib/python3.10/site-packages/lmdeploy/lite/utils/
- 第二步:将用到的数据集(c4)复制到下面的目录:
cp -r /root/share/temp/datasets/c4/ /root/.cache/huggingface/datasets/
第二步:通过 minmax 获取量化参数。主要就是利用下面这个公式,获取每一层的 K V 中心值(zp)和缩放值(scale)。
zp = (min+max) / 2 scale = (max-min) / 255 quant: q = round( (f-zp) / scale) dequant: f = q * scale + zp
有这两个值就可以进行量化和解量化操作了。具体来说,就是对历史的 K 和 V 存储 quant 后的值,使用时在 dequant。
第二步的执行命令如下:
# 通过 minmax 获取量化参数 lmdeploy lite kv_qparams \ --work_dir ./quant_output \ --turbomind_dir workspace/triton_models/weights/ \ --kv_sym False \ --num_tp 1
在这个命令中,num_tp
的含义前面介绍过,表示 Tensor 的并行数。每一层的中心值和缩放值会存储到 workspace
的参数目录中以便后续使用。kv_sym
为 True
时会使用另一种(对称)量化方法,它用到了第一步存储的绝对值最大值,而不是最大值和最小值。
第三步:修改配置。也就是修改 weights/config.ini
文件,这个我们在《2.6.2 模型配置实践》中已经提到过了(KV int8 开关),只需要把 quant_policy
改为 4 即可。
这一步需要额外说明的是,如果用的是 TurboMind1.0,还需要修改参数 use_context_fmha
,将其改为 0。
接下来就可以正常运行前面的各种服务了,只不过咱们现在可是用上了 KV Cache 量化,能更省(运行时)显存了。
3.1.2 量化效果
官方给出了 internlm-chat-7b 模型在 KV Cache 量化前后的显存对比情况,如下表所示。
batch_size | fp16 memory(MiB) | int8 memory(MiB) | diff(MiB) |
---|---|---|---|
8 | 22337 | 18241 | -4096 |
16 | 30593 | 22369 | -8224 |
32 | 47073 | 30625 | -16448 |
48 | 63553 | 38881 | -24672 |
可以看出,KV Cache 可以节约大约 20% 的显存。
同时,还在 opencompass 平台上测试了量化前后的精准度(Accuracy)对比情况,如下表所示。
task | dataset | metric | int8 | fp16 | diff |
---|---|---|---|---|---|
Language | winogrande | accuracy | 60.77 | 61.48 | -0.71 |
Knowledge | nq | score | 2.69 | 2.60 | +0.09 |
Reasoning | gsm8k | accuracy | 33.28 | 34.72 | -1.44 |
Reasoning | bbh | naive_average | 20.12 | 20.51 | -0.39 |
Understanding | openbookqa_fact | accuracy | 82.40 | 82.20 | +0.20 |
Understanding | eprstmt-dev | accuracy | 90.62 | 88.75 | +1.87 |
Safety | crows_pairs | accuracy | 32.56 | 31.43 | +1.13 |
可以看出,精度不仅没有明显下降,相反在不少任务上还有一定的提升。可能得原因是,量化会导致一定的误差,有时候这种误差可能会减少模型对训练数据的拟合,从而提高泛化性能。量化可以被视为引入轻微噪声的正则化方法。或者,也有可能量化后的模型正好对某些数据集具有更好的性能。
总结一下,KV Cache 量化既能明显降低显存占用,还有可能同时带来精准度(Accuracy)的提升。
3.2 W4A16 量化
3.2.1 量化步骤
W4A16中的A是指Activation,保持FP16,只对参数进行 4bit 量化。使用过程也可以看作是三步。
第一步:同 1.3.1,不再赘述。
第二步:量化权重模型。利用第一步得到的统计值对参数进行量化,具体又包括两小步:
- 缩放参数。主要是性能上的考虑(回顾 PPT)。
- 整体量化。
第二步的执行命令如下:
# 量化权重模型 lmdeploy lite auto_awq \ --model /root/share/temp/model_repos/internlm-chat-7b/ \ --w_bits 4 \ --w_group_size 128 \ --work_dir ./quant_output
命令中 w_bits
表示量化的位数,w_group_size
表示量化分组统计的尺寸,work_dir
是量化后模型输出的位置。这里需要特别说明的是,因为没有 torch.int4
,所以实际存储时,8个 4bit 权重会被打包到一个 int32 值中。所以,如果你把这部分量化后的参数加载进来就会发现它们是 int32 类型的。
最后一步:转换成 TurboMind 格式。
# 转换模型的layout,存放在默认路径 ./workspace 下 lmdeploy convert internlm-chat-7b ./quant_output \ --model-format awq \ --group-size 128
这个 group-size
就是上一步的那个 w_group_size
。如果不想和之前的 workspace
重复,可以指定输出目录:--dst_path
,比如:
lmdeploy convert internlm-chat-7b ./quant_output \ --model-format awq \ --group-size 128 \ --dst_path ./workspace_quant
接下来和上一节一样,可以正常运行前面的各种服务了,不过咱们现在用的是量化后的模型。
最后再补充一点,量化模型和 KV Cache 量化也可以一起使用,以达到最大限度节省显存。
3.2.2 量化效果
官方在 NVIDIA GeForce RTX 4090 上测试了 4-bit 的 Llama-2-7B-chat 和 Llama-2-13B-chat 模型的 token 生成速度。测试配置为 BatchSize = 1,prompt_tokens=1,completion_tokens=512,结果如下表所示。
model | llm-awq | mlc-llm | turbomind |
---|---|---|---|
Llama-2-7B-chat | 112.9 | 159.4 | 206.4 |
Llama-2-13B-chat | N/A | 90.7 | 115.8 |
可以看出,TurboMind 相比其他框架速度优势非常显著,比 mlc-llm 快了将近 30%。
另外,也测试了 TurboMind 在不同精度和上下文长度下的显存占用情况,如下表所示。
model(context length) | 16bit(2048) | 4bit(2048) | 16bit(4096) | 4bit(4096) |
---|---|---|---|---|
Llama-2-7B-chat | 15.1 | 6.3 | 16.2 | 7.5 |
Llama-2-13B-chat | OOM | 10.3 | OOM | 12.0 |
可以看出,4bit 模型可以降低 50-60% 的显存占用,效果非常明显。
总而言之,W4A16 参数量化后能极大地降低显存,同时相比其他框架推理速度具有明显优势。
3.3 最佳实践
本节是针对《模型量化》部分的最佳实践。
首先我们需要明白一点,服务部署和量化是没有直接关联的,量化的最主要目的是降低显存占用,主要包括两方面的显存:模型参数和中间过程计算结果。前者对应《3.2 W4A16 量化》,后者对应《3.1 KV Cache 量化》。
量化在降低显存的同时,一般还能带来性能的提升,因为更小精度的浮点数要比高精度的浮点数计算效率高,而整型要比浮点数高很多。
所以我们的建议是:在各种配置下尝试,看效果能否满足需要。这一般需要在自己的数据集上进行测试。具体步骤如下。
- Step1:优先尝试正常(非量化)版本,评估效果。
- 如果效果不行,需要尝试更大参数模型或者微调。
- 如果效果可以,跳到下一步。
- Step2:尝试正常版本+KV Cache 量化,评估效果。
- 如果效果不行,回到上一步。
- 如果效果可以,跳到下一步。
- Step3:尝试量化版本,评估效果。
- 如果效果不行,回到上一步。
- 如果效果可以,跳到下一步。
- Step4:尝试量化版本+ KV Cache 量化,评估效果。
- 如果效果不行,回到上一步。
- 如果效果可以,使用方案。
简单流程如下图所示。
另外需要补充说明的是,使用哪种量化版本、开启哪些功能,除了上述流程外,还需要考虑框架、显卡的支持情况,比如有些框架可能不支持 W4A16 的推理,那即便转换好了也用不了。
根据实践经验,一般情况下:
- 精度越高,显存占用越多,推理效率越低,但一般效果较好。
- Server 端推理一般用非量化版本或半精度、BF16、Int8 等精度的量化版本,比较少使用更低精度的量化版本。
- 端侧推理一般都使用量化版本,且大多是低精度的量化版本。这主要是因为计算资源所限。
以上是针对项目开发情况,如果是自己尝试(玩儿)的话:
- 如果资源足够(有GPU卡很重要),那就用非量化的正常版本。
- 如果没有 GPU 卡,只有 CPU(不管什么芯片),那还是尝试量化版本。
- 如果生成文本长度很长,显存不够,就开启 KV Cache。
建议大家根据实际情况灵活选择方案。
Tesla P40 8bit模型推理过程
手动将原 16bit 权重量化为 8bit,并保存至 internlm-chat-7b-w8
目录下,操作命令如下:
注意:这里我换回了internlm-chat-7b模型,怕再引入其他兼容性问题。
lmdeploy lite smooth_quant internlm/internlm-chat-7b --work-dir ./internlm-chat-7b-w8
成功得到internlm-chat-7b-w8:
然后开始推理:
lmdeploy chat torch ./internlm-chat-7b-w8
输入第一个问题后,推理时报错了:
经过本地debug最后定位到这行代码:
本来怀疑是不是转8bit过程中有问题,但是官方还没有放出w8模型,所以也无法尝试。
由于该问题短时间内未解决,因此在P40上的本次复现就停在了这里。
课程链接
https://github.com/InternLM/tutorial/blob/main/lmdeploy/lmdeploy.md