本篇文章聊聊如何使用 HuggingFace 的 Transformers 来量化 Meta AI 出品的 LLaMA2 大模型,让模型能够只使用 5GB 左右显存就能够运行。

写在前面

在前两篇文章《使用 Docker 快速上手官方版 LLaMA2 开源大模型》和《使用 Docker 快速上手中文版 LLaMA2 开源大模型》中,我们聊过了如何快速上手和使用新鲜出炉的 Meta AI LLaMA2 大模型。

经过实际测试,不论是原版模型(英文),还是中文版模型(双语),我们都需要 13~14 GB 显存,才能够将其运行起来。

为了能够让更多同学能够玩起来 LLaMA2 模型,我尝试使用HuggingFace 的 Transformers 对模型进行了量化,量化后的模型只需要 5GB 左右显存即可运行。

LLaMA2 Chat 项目

完整的代码和模型,我已经上传到了 GitHubHuggingFace,感兴趣的同学可以自取。

准备工作

本文中所有的方法,你都可以参考并在非 Docker 容器中使用。

为了简单省事,可以参考前两篇文章,可以快速的搞定原版或者中文版的 LLaMA2 模型运行环境和 Docker 镜像。如果你本地环境完备,那么忽略 Docker 相关的命令,直接在 Bash 中执行各种具体的程序命令即可。

接下来,我们以使用 LLaMA2 中文模型镜像为例,进行模型的量化操作。

在前文中,我们使用下面的命令快速启动一个 LLaMA2 中文模型应用:

docker run --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --rm -it -v `pwd`/LinkSoul:/app/LinkSoul -p 7860:7860 soulteary/llama2:7b-cn bash

因为要对量化的模型进行保存,我们首先对上面的命令进行简单调整,添加一个参数:

docker run --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --rm -it -v `pwd`/LinkSoul:/app/LinkSoul -v `pwd`/soulteary:/app/soulteary -p 7860:7860 soulteary/llama2:7b-cn bash

这里我们多添加一个参数 -v `pwd`/soulteary:/app/soulteary ,来将当前执行命令的目录下的 soulteary 目录映射到容器内的 /app/soulteary,用来保存未来的量化后的模型文件。

执行命令后,我们将进入环境完备的 Docker 容器内部的交互式命令行环境中。

使用 Transformers 对 LLaMA2 进行量化

这里,我们只使用 HuggingFace 出品的 Transformers 就能够完成一切所需的工作,不需要引入其他的开源项目。

Transformers 量化模型的核心配置

Transformers 的量化功能实现是调用了bitsandbytes。想正确调用这个函数库进行量化,则需要在 AutoModelForCausalLM.from_pretrained 方法中完成 quantization_config 的参数配置。

在 Transformers 的 utils/quantization_config.py#L37 源代码中,我们能够直观的看到函数的运行方式和参数定义,最简单的 4BIT 量化的配置如下:

model = AutoModelForCausalLM.from_pretrained(
	# 要载入的模型名称
    model_id,
	# 仅使用本地模型,不通过网络下载模型
    local_files_only=True,
	# 指定模型精度,保持和之前文章中的模型程序相同 `model.py`
    torch_dtype=torch.float16,
	# 量化配置
    quantization_config = BitsAndBytesConfig(
		# 量化数据类型设置
        bnb_4bit_quant_type="nf4",
		# 量化数据的数据格式
        bnb_4bit_compute_dtype=torch.bfloat16
    ),
	# 自动分配设备资源
    device_map='auto'
)

这里的 bnb_4bit_quant_type 之所以设置为 nf4,是因为在 HuggingFace 的 QLoRA 大模型量化实践中,使用 nf4 (NormalFloat)这种新的数据类型,能够在不牺牲性能的前提下,尽可能节省内存消耗。

bnb_4bit_compute_dtype 之所以设置为 torch.bfloat16 则是因为 HuggingFace 的另外一篇说明,我们可以使用这种新的数据格式,来减少传统 FP32 的“空间浪费” 和避免 FP32 转换 FP16 存在的潜在的溢出问题。

编写模型量化程序

综上所述,不难写出一段简单的不到三十行的程序,来完成对于 LLaMA2 模型的量化(相关程序,我上传到了 soulteary/docker-llama2-chat/llama2-7b-cn-4bit/quantization_4bit.py):

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

# 使用中文版
model_id = 'LinkSoul/Chinese-Llama-2-7b'
# 或者,使用原版
# model_id = 'meta-llama/Llama-2-7b-chat-hf'

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    local_files_only=True,
    torch_dtype=torch.float16,
    quantization_config = BitsAndBytesConfig(
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16
    ),
    device_map='auto'
)

import os
output = "soulteary/Chinese-Llama-2-7b-4bit"
if not os.path.exists(output):
    os.mkdir(output)

model.save_pretrained(output)
print("done")

对模型执行量化操作

我们将上面的内容保存为 quantization_4bit.py,放置于和 LLaMA2 模型目录 meta-llamaLinkSoul 同级的目录中,然后使用 python quantization_4bit.py 执行程序,即可开始模型的量化工作:

# python quantization_4bit.py

Loading checkpoint shards: 100%|██████████████████████████████████████████████████████████████████| 3/3 [00:15<00:00,  5.01s/it]
done

稍等片刻,就能够在当前程序目录中找到自动创建,并保存来新的模型对目录 soulteary/Chinese-Llama-2-7b-4bit/ 啦:

# du -hs soulteary/Chinese-Llama-2-7b-4bit/

13G	soulteary/Chinese-Llama-2-7b-4bit/

# ls -al soulteary/Chinese-Llama-2-7b-4bit/

total 13161144
drwxr-xr-x 2 root root       4096 Jul 21 18:12 .
drwxr-xr-x 3 root root       4096 Jul 21 18:11 ..
-rw-r--r-- 1 root root        629 Jul 21 18:11 config.json
-rw-r--r-- 1 root root        132 Jul 21 18:11 generation_config.json
-rw-r--r-- 1 root root 9976638098 Jul 21 18:12 pytorch_model-00001-of-00002.bin
-rw-r--r-- 1 root root 3500316839 Jul 21 18:12 pytorch_model-00002-of-00002.bin
-rw-r--r-- 1 root root      26788 Jul 21 18:12 pytorch_model.bin.index.json

补齐模型运行文件

模型量化计算是结束了,但是此时的模型还不能使用,因为缺少了 tokenizer 相关的程序文件。类似 LLaMA2 官方版本和中文版本全部兼容,这里的量化版的模型和量化前的模型,也是全部兼容的。

解决这个问题非常简单,我们只需要将量化前的模型中的文件复制到新模型目录即可:

cp LinkSoul/Chinese-Llama-2-7b/tokenizer.model soulteary/Chinese-Llama-2-7b-4bit/

cp LinkSoul/Chinese-Llama-2-7b/special_tokens_map.json soulteary/Chinese-Llama-2-7b-4bit/

cp LinkSoul/Chinese-Llama-2-7b/tokenizer_config.json soulteary/Chinese-Llama-2-7b-4bit/

调整模型程序

前文中提到,这里量化的程序和原版程序没有使用上的区别,所以多数程序都可以保持原样。不过因为是新的模型文件,还是要进行几处简单的调整的。

更新模型运行程序

前文中提到,这里量化的程序和原版程序没有使用上的区别,所以多数程序都可以保持原样。为了能够让模型正确的通过 4BIT 方式加载和运行,我们需要调整两处内容:

我们需要调整前两篇文章中相关项目使用的 model.py 中的 model_id 变量,以及在 AutoModelForCausalLM.from_pretrained 调用中加上 load_in_4bit=True

model_id = 'soulteary/Chinese-Llama-2-7b-4bit'

if torch.cuda.is_available():
    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        load_in_4bit=True,
        local_files_only=True,
        torch_dtype=torch.float16,
        device_map='auto'
    )
else:
    model = None
tokenizer = AutoTokenizer.from_pretrained(model_id)

这部分完整的代码在 soulteary/docker-llama2-chat/llama2-7b-cn-4bit/model.py 可以找到。

运行模型应用

模型应用程序,我上传到了soulteary/docker-llama2-chat/llama2-7b-cn-4bit,因为和前两篇文章中没有什么区别,就不展开了。

如果你选择不在容器内运行,直接使用 python app.py ,模型程序就会快速的运行起来。

构建新的容器镜像

构建 4BIT 的镜像,和之前的文章中一样,执行脚本,等待镜像构建完成即可:

bash scripts/make-7b-cn-4bit.sh

如果你之前跟着前两篇文章走过一遍,那么这个应该操作能够在 1~2 秒内完成。

使用容器启动模型应用

使用容器启动应用和之前的文章也并没有什么区别,执行命令,调用下面的脚本即可:

bash scripts/run-7b-cn-4bit.sh

等待日志中出现 Running on local URL: http://0.0.0.0:7860,我们就能够正常进行使用和测试啦。

运行起来的量化后的中文 LLaMA2 项目

显存资源使用

显存资源一直是大家都比较关注的部分,模型启动大概需要 5G 出头的显存资源。

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.125.06   Driver Version: 525.125.06   CUDA Version: 12.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  Off  | 00000000:01:00.0 Off |                  Off |
| 31%   61C    P2   366W / 450W |   5199MiB / 24564MiB |     99%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1396      G   /usr/lib/xorg/Xorg                167MiB |
|    0   N/A  N/A      1572      G   /usr/bin/gnome-shell               16MiB |
|    0   N/A  N/A      8595      C   python                           5012MiB |
+-----------------------------------------------------------------------------+

使用一段时间后,依旧还在 6GB 以内,是不是感觉还凑合?

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.125.06   Driver Version: 525.125.06   CUDA Version: 12.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  Off  | 00000000:01:00.0 Off |                  Off |
| 32%   50C    P8    35W / 450W |   5725MiB / 24564MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1402      G   /usr/lib/xorg/Xorg                167MiB |
|    0   N/A  N/A      1608      G   /usr/bin/gnome-shell               16MiB |
|    0   N/A  N/A     24950      C   python                           5538MiB |
+-----------------------------------------------------------------------------+

最后

并不是所有的同学都是人手一张或几张 4090 或者 A100,所以即使量化会带来一些效果的下降,但总归比因为显存不足无法跑起来模型,不能一起玩要好呀。

况且,即使效果下降,依旧是适合做非常多场景下的使用的。后面的文章里,我们再做展开。

工程的艺术就在于 “trade-off”,前一阵线下偶然听到一位新朋友提起,有一种将心底藏了很久的东西唤醒的感觉。

–EOF