chatGLM微调和部署
常用版本
https://github.com/THUDM/ChatGLM-6B
https://modelscope.cn/models/ZhipuAI/chatglm3-6b
https://modelscope.cn/models/ZhipuAI/chatglm2-6b-int4
https://modelscope.cn/models/ZhipuAI/chatglm-6b-int4-qe
安装ChatGLM3-6B
先创建虚拟环境
下载项目到本地git clone https://github.com/THUDM/ChatGLM3
cd ChatGLM3
此时只是包括一些chatGLM3的工具,并不包括模型的
ChatGLM3 的主要目录:
basic_demo 运行demo
finetune_demo 微调
langchain_demo 接入langchain的demo
openai_api_demo 启动API服务的demo
安装依赖pip install -r requirements.txt
安装pytorch 这个要根据cuda版本
下载6b模型权重文件,我是先取消git代理才使用这个包下载的,如果是科学上网可用国外的git clone https://www.modelscope.cn/ZhipuAI/chatglm3-6b.git
chatglm3-6b不能直接转换为量化保存,只能启动时添加参数以量化启动
注意修改MODEL_PATH 路径为实际模型路径
根据chatglm3的官方demo修改
cli_demo.py:
import os
import platform
from transformers import AutoTokenizer, AutoModel
# MODEL_PATH = os.environ.get('MODEL_PATH', 'THUDM/chatglm3-6b') # 使用预设远程模型
MODEL_PATH = os.environ.get('MODEL_PATH', './ChatGLM3/chatglm3-6b') # 使用本地模型
TOKENIZER_PATH = os.environ.get("TOKENIZER_PATH", MODEL_PATH) # 分词器路径
tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH, trust_remote_code=True) # 加载分词器
# model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True, device_map="auto").eval() #自动选择cpu或gpu,会提示缺少一个库
# model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True).to("cuda").eval() # 指定使用cuda运行,如果内存
model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True).quantize(4).cuda() #使用量化模式运行, 4bit量化,量化必须在cuda上运行
# 量化模式运行需要额外安装 cpm_kernels库
os_name = platform.system() # 获取系统名称
clear_command = 'cls' if os_name == 'Windows' else 'clear' # 清空命令
stop_stream = False # 是否停止流式对话
welcome_prompt = "欢迎使用 ChatGLM3-6B 模型,输入内容即可进行对话,clear 清空对话历史,stop 终止程序" # 欢迎语
# 返回完整提示函数
# def build_prompt(history):
# prompt = welcome_prompt # 初始化提示语
# for query, response in history: # 遍历历史对话记录
# prompt += f"\n\n用户:{query}" # 添加用户的对话内容
# prompt += f"\n\nChatGLM3-6B:{response}" # 添加模型的对话内容
# return prompt # 返回完整的提示语
def main():
past_key_values, history = None, [] # 初始化历史对话记录和过去的键值对,history是上下文
global stop_stream # 声明全局变量stop_stream
print(welcome_prompt) # 打印欢迎提示语
while True:
query = input("\n用户:") # 获取用户输入
if query.strip() == "stop": # 如果用户输入"stop",则退出循环
break
if query.strip() == "clear": # 如果用户输入"clear",则清空对话历史
past_key_values, history = None, [] # 重置历史对话记录和过去的键值对
os.system(clear_command) # 清空命令行
print(welcome_prompt) # 打印欢迎提示语
continue
print("\nChatGLM:", end="") # 打印模型名称,不换行
current_length = 0 # 初始化当前长度
# 生成对话
for response, history, past_key_values in model.stream_chat(tokenizer, query, history=history, top_p=1,
temperature=0.01,
past_key_values=past_key_values,
return_past_key_values=True):
if stop_stream: # 如果停止流式对话
stop_stream = False # 重置停止标志
break
else:
print(response[current_length:], end="", flush=True) # 打印生成的响应内容,不换行
current_length = len(response) # 更新当前长度
print("") # 换行
if __name__ == "__main__":
main()
基于streamlit的网络对话demo
web_demo_streamlit.py:
"""
这个脚本是一个基于Streamlit的简单网络演示,展示了ChatGLM3-6B模型的使用。对于更全面的网络演示,建议使用'composite_demo'。
使用方法:
- 使用Streamlit运行脚本:`streamlit run web_demo_streamlit.py`
- 从侧边栏调整模型参数。
- 在聊天输入框中输入问题,与ChatGLM3-6B模型互动。
注意:确保已安装'streamlit'和'transformers'库,并且所需的模型检查点可用。
pip install streamlit
启动命令: streamlit run ./ChatGLM3/basic_demo/web_demo_streamlit.py
运行后可能会提示一个 __path__....torch::class_的警告,但实际上不影响使用
"""
import os
import streamlit as st
import torch
from transformers import AutoModel, AutoTokenizer
MODEL_PATH = os.environ.get('MODEL_PATH', './ChatGLM3/chatglm3-6b') # 使用本地模型,os是读取环境变量,如果没有则使用指定的路径
TOKENIZER_PATH = os.environ.get("TOKENIZER_PATH", MODEL_PATH) # 分词器路径
# 设置Streamlit页面的配置
st.set_page_config(
page_title="ChatGLM3-6B 测试Demo", # 页面标题
page_icon=":robot:", # 页面图标
layout="wide" # 页面布局
)
# 缓存模型加载函数,避免每次重新加载模型
@st.cache_resource
def get_model():
tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH, trust_remote_code=True) # 加载分词器
# model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True, device_map="auto").eval() #默认运行方式
model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True).quantize(4).cuda() # 使用4bit量化模式运行
return tokenizer, model
# 加载ChatGLM3的模型和分词器
tokenizer, model = get_model()
# 初始化会话状态中的历史记录和过去的键值对
if "history" not in st.session_state:
st.session_state.history = []
if "past_key_values" not in st.session_state:
st.session_state.past_key_values = None
# 在侧边栏中添加滑块,用于调整模型参数,三个值参数依次是最小,最大,初始值
max_length = st.sidebar.slider("max_length", 0, 32768, 8192, step=1) # 最大生成长度,过小可能导致截断,过大消耗更多显存和资源
top_p = st.sidebar.slider("top_p", 0.0, 1.0, 0.8, step=0.01) # Top-P 采样,限制候选词范围,控制生成文本的多样性,小值更严谨,大值更开放和相关性越差
temperature = st.sidebar.slider("temperature", 0.0, 1.0, 0.6, step=0.01) # 温度系数,低值更保守,适用于调整创造性或严谨性
# 推荐的严谨模式,如技术问答、代码生成:
# temperature = 0.3
# top_p = 0.7
# 对话模式(如客服、知识问答):
# temperature = 0.6
# top_p = 0.8
# 创造模式(如故事、诗歌):
# temperature = 0.9
# top_p = 0.95
# 在侧边栏中添加按钮,用于清理会话历史
buttonClean = st.sidebar.button("清理会话历史", key="clean")
if buttonClean:
st.session_state.history = [] # 清空历史记录
st.session_state.past_key_values = None # 重置过去的键值对
if torch.cuda.is_available(): # 如果CUDA可用
torch.cuda.empty_cache() # 清空CUDA缓存
st.rerun() # 重新运行脚本
# 显示历史对话记录
for i, message in enumerate(st.session_state.history):
if message["role"] == "user":
with st.chat_message(name="user", avatar="user"): # 显示用户消息
st.markdown(message["content"])
else:
with st.chat_message(name="assistant", avatar="assistant"): # 显示助手消息
st.markdown(message["content"])
# 创建用户和助手的消息占位符
with st.chat_message(name="user", avatar="user"):
input_placeholder = st.empty() # 用户输入占位符
with st.chat_message(name="assistant", avatar="assistant"):
message_placeholder = st.empty() # 助手回复占位符
# 获取用户输入
prompt_text = st.chat_input("请输入您的问题")
if prompt_text:
input_placeholder.markdown(prompt_text) # 显示用户输入
history = st.session_state.history # 获取历史记录
past_key_values = st.session_state.past_key_values # 获取过去的键值对
# 生成对话
for response, history, past_key_values in model.stream_chat(
tokenizer,
prompt_text,
history,
past_key_values=past_key_values,
max_length=max_length,
top_p=top_p,
temperature=temperature,
return_past_key_values=True,
):
message_placeholder.markdown(response) # 显示助手回复
st.session_state.history = history # 更新历史记录
st.session_state.past_key_values = past_key_values # 更新过去的键值对
ChatGLM3多卡部署
如果你有多张 GPU,但是每张 GPU 的显存大小都不足以容纳完整的模型,那么可以将模型切分在多张GPU上。首先安装 accelerate: pip install accelerate
,然后通过如下方法加载模型:
from utils import load_model_on_gpus
model = load_model_on_gpus("THUDM/chatglm3-6b", num_gpus=2)
即可将模型部署到两张 GPU 上进行推理。你可以将 num_gpus
改为你希望使用的 GPU 数。默认是均匀切分的,你也可以传入 device_map
参数来自己指定。
使用TensorRT-LLM加速推理
https://github.com/THUDM/ChatGLM3/blob/main/tensorrt_llm_demo/README.md
chatGLM-4 9B 量化和运行
https://github.com/THUDM/GLM-4
下载 :https://modelscope.cn/models/ZhipuAI/glm-4-9b-chat-hf/summary
转换为int4精度
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tokenizer = AutoTokenizer.from_pretrained("./glm-4-9b-chat-hf", trust_remote_code=True)
query = "你好"
inputs = tokenizer.apply_chat_template([{"role": "user", "content": query}],
add_generation_prompt=True,
tokenize=True,
return_tensors="pt",
return_dict=True
)
inputs = inputs.to(device)
model = AutoModelForCausalLM.from_pretrained(
"./glm-4-9b-chat-hf",
low_cpu_mem_usage=True,
trust_remote_code=True,
load_in_4bit=True
).eval()
model.save_pretrained("glm-4-9b-chat-int4") #另存为
tokenizer.save_pretrained("glm-4-9b-chat-int4")
运行int4后的模型:
"""
该脚本创建一个基于 transformers 的 CLI 示例,用于 glm-4-9b-chat 模型,
允许用户通过命令行界面与模型进行交互。
使用说明:
- 运行脚本以启动 CLI 示例。
- 通过输入问题与模型交互,并接收模型的回复。
注意:脚本包括了将 markdown 转换为纯文本的处理,
以确保 CLI 界面能正确显示格式化后的文本。
如果使用 flash attention,请安装 flash-attn 并在加载模型时添加 attn_implementation="flash_attention_2"。
"""
import torch
from threading import Thread
from transformers import AutoTokenizer, AutoModelForCausalLM, StoppingCriteria, StoppingCriteriaList, TextIteratorStreamer
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
MODEL_PATH = "./glm-4-9b-chat-int4" # 修改为本地模型路径
# trust_remote_code=True 是 glm-4-9b-chat 所需的选项
# 如果使用 glm-4-9b-chat-hf 则无需设置该选项
# tokenizer 和 model 的加载需保持一致
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
# 加载模型
model = AutoModelForCausalLM.from_pretrained(
MODEL_PATH,
torch_dtype=torch.bfloat16, # 使用 flash-attn 时必须使用 bfloat16 或 float16
trust_remote_code=True).to(device).eval() # 将模型设置为评估模式
# 自定义停止条件类,用于检测结束标志
class StopOnTokens(StoppingCriteria):
def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool:
stop_ids = model.config.eos_token_id
for stop_id in stop_ids:
if input_ids[0][-1] == stop_id:
return True
return False
if __name__ == "__main__":
history = [] # 用于存储聊天记录
max_length = 8192 # 最大生成长度
top_p = 0.8 # 生成文本的随机性控制
temperature = 0.6 # 温度系数,影响生成的多样性
stop = StopOnTokens()
print("欢迎使用 GLM-4-9B CLI 聊天。请在下方输入你的消息。")
while True:
user_input = input("\n你: ") # 接收用户输入
if user_input.lower() in ["exit", "quit"]: # 输入 exit 或 quit 退出
break
history.append([user_input, ""]) # 将用户输入添加到聊天记录中
# 将历史记录转换为对话格式
messages = []
for idx, (user_msg, model_msg) in enumerate(history):
if idx == len(history) - 1 and not model_msg: # 当前用户的输入尚无模型回复
messages.append({"role": "user", "content": user_msg})
break
if user_msg:
messages.append({"role": "user", "content": user_msg})
if model_msg:
messages.append({"role": "assistant", "content": model_msg})
# 使用模板生成模型输入
model_inputs = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt"
).to(model.device)
# 创建流式生成器
streamer = TextIteratorStreamer(
tokenizer=tokenizer,
timeout=60, # 超时时间
skip_prompt=True, # 跳过提示符
skip_special_tokens=True # 跳过特殊标记
)
# 配置生成参数
generate_kwargs = {
"input_ids": model_inputs["input_ids"],
"attention_mask": model_inputs["attention_mask"],
"streamer": streamer,
"max_new_tokens": max_length,
"do_sample": True,
"top_p": top_p,
"temperature": temperature,
"stopping_criteria": StoppingCriteriaList([stop]),
"repetition_penalty": 1.2, # 防止重复生成
"eos_token_id": model.config.eos_token_id, # 结束标记 ID
}
# 使用子线程执行生成任务
t = Thread(target=model.generate, kwargs=generate_kwargs)
t.start()
print("GLM-4:", end="", flush=True)
# 流式打印生成内容
for new_token in streamer:
if new_token:
print(new_token, end="", flush=True)
history[-1][1] += new_token # 将生成内容存入历史记录
history[-1][1] = history[-1][1].strip() # 去除多余空格