ChatGLM3-6B 本地部署与调用

ChatGLM3-6B 是一个清华开源双语对话语言模型,这里记录一下该模型在本地电脑上部署的过程和参考官方示例代码实现基本的对话功能调用。

环境和依赖配置

首先,无论是 Windows 还是 Linux 系统,确保系统中已经配置好 CUDA 相关环境。创建一个项目文件夹,在该项目文件夹下创建一个虚拟运行环境并安装 PyTorch 相关依赖(从官网下载 GPU 版本),例如:

1
2
3
python -m venv venv
source ./venv/bin/activate
pip install torch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu118

因为我计划从零开始一点点参考官方 Demo 来完成 ChatGLM3 的调用,因此这里先不下拉官方仓库,而是先安装项目的一些最小依赖:

1
pip install -r requirements.txt

这里的 requirements.txt 中依赖要少于官方,内容如下:

1
2
3
4
5
6
7
8
accelerate
cpm_kernels
modelscope
protobuf
sentencepiece
tokenizers
transformers
gradio

模型下载

考虑到国内的网络情况,这里选择从国内的 ModelScope 中下载 ChatGLM3-6B。上一部分已经在虚拟环境中安装好了 modelscope 包,只需要使用如下代码:

1
2
3
from modelscope import snapshot_download

model_dir = snapshot_download("ZhipuAI/chatglm3-6b", revision = "master")

同时也可以手动指定模型下载路径,例如在当前项目文件夹下创建 models 文件夹,然后使用如下代码:

1
2
3
model_path: str = os.path.join(Path().resolve(), "models")
# 使用 local_files_only=True 前要先确保模型已经下载到本地
model_dir: str = snapshot_download("ZhipuAI/chatglm3-6b", revision="master", cache_dir=model_path, local_files_only=True)

模型调用

完成模型下载后,可以使用如下代码进入模型的评估模式:

1
2
3
4
5
from modelscope import AutoModel, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
model = AutoModel.from_pretrained(model_dir, trust_remote_code=True).cuda()
model.eval()

模型运行上述代码官方说需要占用大概 13GB 显存,但实际上我在 RTX4070 12GB 上也能运行。

同时依据官方建议,使用如下模型量化代码可以在 6GB 显存的显卡上运行:

1
model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True).quantize(4).cuda()

对话

ChatGLM3 支持 chat()stream_chat() 两种代码调用方式来实现模型对话。其中使用 chat() 方法模型会一次性返回当前回复的所有内容,而使用 stream_chat() 方法模型会流式输出当前回复的内容,也就是像使用 ChatGPT 那样一个字一个字的返回。

chat()

chat() 的使用比较简单,参考官方的代码示例:

1
2
3
4
response, history = model.chat(tokenizer, "你好!", history=[])
print(response)
response, history = model.chat(tokenizer, "圣诞节是什么时候?", history=history)
print(response)

其中,response 即为模型的回复结果,为 str 类型;而 historylist 类型,可以理解为被用来保存用户与 ChatGLM3-6B 模型的聊天记录内容,大致的格式为 [{"role": "user", "content": "user questions"}, {"role": "assistant", "content": "llm answers"}, ...]

stream_chat()

使用 chat() 来获得模型的完整回复内容通常需要用户等待一小段时间,为了拥有一个更好的使用体验,使用 stream_chat() 方法会是一个不错的方式。参考官方的命令行对话代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ...
while True:
# ...
current_length: int = 0
for response, history, past_key_values in model.stream_chat(
tokenizer,
user_question,
history=history,
top_p=1,
temperature=0.01,
past_key_values=past_key_values,
return_past_key_values=True,
):
print(response[current_length:], end="", flush=True)
current_length = len(response)
# ...

top_ptemperature

上述示例代码中 stream_chat() 方法用到了 top_ptemperature 参数,chat() 方法也同样支持这两个参数。

参考 《Prompt Engineering Guide》模型设置 这一章的内容:

temperature 参数用来控制模型生成文本的随机性和创造性的参数,较高的值会使输出更具随机性,而较低的值则会使输出更具确定性。如果调高该参数值,大语言模型可能会返回更随机的结果,也就是说这可能会带来更多样化或更具创造性的产出。

top_p 参数用来控制模型返回结果的真实性。如果你需要准确和事实的答案,就把参数值调低。如果你想要更多样化的答案,就把参数值调高一些。

一般建议是改变其中一个参数就行,不用两个都调整。

使用 Gradio 搭建 Web UI

Gradio 是一个用于快速构建交互式 Web 演示界面的框架,你不需要了解前端技术,只需要写几行 Python 代码即可。

这里还是用官方示例中的代码来快速构建一个聊天界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Gradio UI
with gr.Blocks(title="ChatGLM3-6B Gradio Simple Demo") as demo:
gr.HTML(value="""<h1 align="center">ChatGLM3-6B Gradio Simple Demo</h1>""")
chatbot = gr.Chatbot()

with gr.Row():
with gr.Column(scale=4):
user_input = gr.Textbox(placeholder="Input...", lines=5, container=False)
submit_btn = gr.Button(value="Submit", variant="primary")
with gr.Column(scale=1):
empty_btn = gr.ClearButton(value="Clear History", size="sm")

top_p_input = gr.Slider(
minimum=0,
maximum=1,
value=0.8,
step=0.01,
label="Top P",
interactive=True,
)
temperature_input = gr.Slider(
minimum=0.01,
maximum=1,
value=0.6,
step=0.01,
label="Temperature",
interactive=True,
)

empty_btn.add(components=[user_input, chatbot])

submit_btn.click(
fn=query_user_input,
inputs=[user_input, chatbot],
outputs=[user_input, chatbot],
queue=False,
).then(
fn=llm_reply,
inputs=[chatbot, top_p_input, temperature_input],
outputs=chatbot,
)

demo.queue().launch(
inbrowser=True,
share=False,
show_api=False,
)

模型的回复还是使用 stream_chat() 方法,只是需要稍作修改,不再是直接打印内容而是使用 yield 来返回一个生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def llm_reply(chat_history: list[Any], top_p: float, temperature: float):
messages: list[Any] = []
for idx, (user_msg, model_msg) in enumerate(chat_history):
if idx == len(chat_history) - 1 and not model_msg:
user_question: str = user_msg # 用户最新的提问
break
if user_msg:
messages.append({"role": "user", "content": user_msg})
if model_msg:
messages.append({"role": "assistant", "content": model_msg})

past_key_values = None
for reply, messages, past_key_values in model.stream_chat(
tokenizer,
user_question,
history=messages,
top_p=top_p,
temperature=temperature,
past_key_values=past_key_values,
return_past_key_values=True,
):
chat_history[-1][1] = reply
yield chat_history

另外需要留意的一点是 Gradio 和 ChatGLM3-6B 用于保存对话聊天历史的类型格式是不一样的,相互之间传递数据时需要人为转换。ChatGLM3-6B 的格式上面已经提过,类似 [{"role": "user", "content": "user questions"}, {"role": "assistant", "content": "llm answers"}, ...] 这样;而 Gradio 的格式更简单一些,类似 [["user question1", "llm answer1"], ["user question2", "llm answer2"], ...] 这样。