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()  # 去除多余空格