简介:这份GLM-4代码仓库源码压缩包面向希望研读并二次开发GLM-4的开发者、学生与算法工程师,提供从基础调用到微调、多模态交互及服务化部署的参考实现,可用于课程实验、原型验证与工程迁移。包内共78个文件,以Python脚本、Markdown说明、YAML/JSON配置、PNG/JPEG图示和少量TypeScript前端资源为主,覆盖推理示例、微调配置、OpenAI接口服务、vLLM命令行演示、OpenVINO/ITREX设备示例及Web交互界面等模块,压缩包约7.57MB,目录分层清晰,便于按功能检索。包内还含项目说明、许可证与协作模板类文本,方便快速了解仓库约定与目录组织,也适合作为团队内部技术分享和代码走查的样本。已有307人学习下载。读者可据此了解GLM-4仓库的组织方式,对照基础、综合与视觉演示理解模型调用链路,并从微调脚本、配置样例和接口服务代码中获得可复用的工程参考与排错线索。
1. 拿到 GLM-4 源码仓库 zip 包,先别急着 pip install
同事凌晨把一个解压好的目录放到我旁边,说“这不是 pip 包,是一整套工程”。确实,这个 GLM-4 源码仓库 zip 包解开之后,里面有能直接对话的命令行脚本、有 OpenAI 兼容的服务端、有 LoRA 微调的配置和脚本,还有一套 Gradio 界面。它适合两类人:一是在自己机器上把 9B 级别模型跑通、并且准备改代码而不是只调 API 的开发者;二是要给内部做私有化部署、需要看懂推理和微调链路的同学。我头一天就踩了个顺序上的错——先装依赖再看目录,结果装了三遍环境。正确顺序是先看目录,再决定装哪一份 requirements。
2. 仓库全景:先认清 basic_demo、finetune_demo 这几层
2.1 解压后的目录体检:每个目录管一段链路
zip 解开后通常还套一层壳目录(比如GLM-4-main/),真正的业务代码在里面。先别管壳目录叫什么,进去看顶层条目,职责划分其实很清楚:
| 目录或文件 | 作用 | 上手优先级 |
|---|---|---|
basic_demo/ | 命令行推理、int4 量化推理、vLLM 版脚本、OpenAI 兼容服务端 | 第一个跑 |
composite_demo/ | Gradio 多轮界面,带工具调用、角色预设、多模态入口 | 第二个看 |
finetune_demo/ | LoRA / SFT / DPO 的训练配置、finetune.py、合并脚本 | 要微调再看 |
resources/ | 截图、示例图,纯静态资源 | 不用管 |
顶层README.md、LICENSE | 模型规格、显存要求、许可证范围 | 扫一眼 |
basic_demo是最值得先读的,因为它是整个仓库里“最短的一条完整链路”:加载 tokenizer、套 chat template、generate、流式打印。composite_demo目录里那套 Gradio 代码体积大一些,但它是拿来改的——你要做自己的对话产品,基本就是从这儿删页面开始的。finetune_demo独立成体系,configs/放 yaml,scripts/放 shell,finetune.py是唯一入口。
这里有个容易忽略的点:basic_demo里往往同时存在 transformers 版和 vLLM 版两个推理脚本,文件名不一样,依赖也不一样。transformers 版装完requirements.txt就能跑,vLLM 版要额外装 vLLM,而且对 torch 和 CUDA 版本挑得厉害。新手别一上来就动 vLLM 版,先让 transformers 版出字,心里有底了再去折腾吞吐。
2.2 技术栈组合:transformers、peft、vLLM 各管一段
这套代码的技术栈是分层的,不是随便堆的:
- transformers负责权重加载、chat template 渲染、
generate采样。整个推理链路的地基。 - peft负责 LoRA 注入,
finetune_demo里训练完得到的是一个几百 MB 的 adapter,不是完整权重。 - vLLM负责高并发推理,PagedAttention 加连续批处理,单卡吞吐能比裸
generate高一个量级。 - FastAPI + uvicorn负责把模型包装成 OpenAI 兼容接口,让现成的客户端、IDE 插件直接接进来。
- Gradio负责
composite_demo的界面层,工具调用、文件上传这些交互都在这儿。
版本约束是这套代码最容易出问题的地方。GLM-4 属于自定义架构,权重加载要靠trust_remote_code=True走仓库里的建模文件,所以 transformers 版本太低会直接报“不认识这个 model type”,太高又可能撞上 API 改名。我的习惯是:先按requirements.txt里给的区间装,跑通之后再考虑升级,别反过来。另外peft和accelerate这两个包的版本要一起看,accelerate太旧会导致device_map="auto"在多卡上分不匀。
这里解释一下为什么要单独强调trust_remote_code。它不是一个可有可无的开关,而是告诉 transformers “别用你内置的建模类,用权重目录里那份modeling_*.py”。GLM-4 的注意力实现、位置编码、RoPE 缩放都在那份文件里,关掉它等于换了半个模型。第一次加载时 transformers 会提示你确认,脚本里显式写上trust_remote_code=True就不会卡在那一步。
2.3 硬件与模型规格对照:先算显存再选路径
不看显存直接跑,是新手最常见的浪费时间的做法。9B 参数的模型,bf16 下光权重就接近 18GB,加上 KV cache 和中间激活,24GB 卡跑单轮短对话是够的,长上下文就悬了。大致可以按这张表选路径:
| 场景 | 加载方式 | 显存大致占用 | 说明 |
|---|---|---|---|
| 单卡 24GB,短对话 | bf16 / fp16 | 20GB 上下 | max_new_tokens控制在 1k 内 |
| 单卡 16GB | int4 量化 | 10GB 上下 | 用仓库里的 int4 推理脚本 |
| 单卡 24GB,长文本 | int4 + 限制输入长度 | 12GB 上下 | 输入切块,别一次塞满 |
| 多卡 | device_map="auto"或 vLLM 张量并行 | 按卡数摊 | vLLM 要显式设tensor_parallel_size |
| 多模态分支 | 视觉编码器额外占显存 | 比纯文本多 3~6GB | 图片分辨率越高越吃 |
这张表是估算,不是承诺。实际能不能跑起来,还取决于你的 batch size、输入长度、以及有没有开 flash attention。真正的判断方法是先跑一次nvidia-smi盯着峰值,而不是靠算。我一般会在推理脚本外面套一层torch.cuda.max_memory_allocated()打印,跑几条真实长度的请求,心里就有数了。
3. 跑通推理:从 trans_infer_cli.py 到 OpenAI 兼容接口
3.1 环境准备:先把 torch 装对,再装依赖清单
环境这一步最容易出问题,因为pip install torch默认可能给你拉一个 CPU 轮子,装完torch.cuda.is_available()返回 False,后面全白搭。
# 1) 独立环境,Python 3.10 是这套依赖比较省心的版本 conda create -n glm4 python=3.10 -y conda activate glm4 # 2) 先按本机 CUDA 装 torch,别让 pip 自己挑 pip install torch==2.4.0 torchvision==0.19.0 --index-url https://download.pytorch.org/whl/cu121 # 3) 再装仓库自带的依赖清单(在 basic_demo 目录下) cd GLM-4-main/basic_demo pip install -r requirements.txt # 4) 自检:两个都输出 True / 卡名,才算环境通了 python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"第 2 步的cu121要和nvidia-smi右上角那个 CUDA Version 对得上,不是和驱动版本对。对不上就先升驱动,别硬装。第 3 步如果requirements.txt里又写了一遍 torch,pip 会自己再解析一次,这时候用pip install -r requirements.txt --no-deps配pip check排查,比反复重装省时间。
权重有两种拿法:一是在线让from_pretrained直接拉仓库名,二是提前下好放到本地目录。生产环境我一律用第二种,把路径写死成常量,脚本里所有地方引用同一个变量。原因是缓存目录被清、多用户共享机器、离线机器这几种情况太常见,路径写死之后排查问题至少少一个变量。
3.2 最小推理脚本:trans_infer_cli.py 的核心逻辑
这个脚本的作用就是“一条 prompt 进去,一段文字流式出来”。剥掉参数解析和交互循环,骨架是这样的:
import torch from transformers import AutoModelForCausalLM, AutoTokenizer, TextStreamer MODEL_PATH = "/data/models/glm-4-9b-chat" # 本地权重目录,换成你的路径 tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtype=torch.bfloat16, # 20 系卡没有 bf16,改成 torch.float16 device_map="auto", # 多卡自动切,单卡等价于全放 cuda:0 trust_remote_code=True, ).eval() messages = [{"role": "user", "content": "用三句话说明 LoRA 和全参微调的区别"}] # 关键:chat 模型必须走 apply_chat_template,它会补上 <|user|>、<|assistant|> 这些特殊 token inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, # 末尾补上“该助手说话了”的标记 return_tensors="pt", return_dict=True, ).to(model.device) streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) with torch.inference_mode(): model.generate( **inputs, max_new_tokens=512, # 生成长度上限,不是总长度 do_sample=True, # 关掉就是贪心解码,输出会很死板 temperature=0.6, # 0.1 严谨、0.9 发散,业务问答一般 0.6~0.8 top_p=0.9, # 和 temperature 二选一调,别一起猛调 streamer=streamer, )逐段说清楚。torch_dtype决定权重精度,bf16 显存占用和 fp16 一样但数值范围大,不容易溢出,前提是卡支持;device_map="auto"底层由 accelerate 计算每层放哪,单卡时它会把全部层塞进 cuda:0,多卡时按显存比例切。apply_chat_template是这段代码里最不能省的一行——它把messages列表渲染成模型训练时见过的格式,漏掉它模型会把你当纯文本续写,表现就是自问自答或者输出一串特殊符号。
max_new_tokens和max_length是两回事,前者是“最多再生成多少”,后者是“输入加输出总共多少”,混用会导致明明设了 512 却只吐出几十个字。temperature和top_p建议只调一个,两个一起调的时候输出质量会变得很难复现——这是玄学感的来源,其实是采样空间被双重收窄了。
另外注意torch.inference_mode(),它比no_grad()更狠,会关掉版本计数,显存能省一点。跑长对话的时候这点显存就是能不能多撑两轮的区别。
3.3 起一个 OpenAI 兼容服务并验证
basic_demo里那个服务端脚本的价值在于:它把模型包装成了/v1/chat/completions,任何支持 OpenAI 协议的客户端、编辑器插件、内部工具都能直接接。骨架大概长这样:
# openai_api_server.py 精简骨架 from fastapi import FastAPI import uvicorn from transformers import AutoModelForCausalLM, AutoTokenizer app = FastAPI() MODEL_PATH = "/data/models/glm-4-9b-chat" model, tokenizer = None, None @app.on_event("startup") def load_model(): global model, tokenizer tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtype="auto", device_map="auto", trust_remote_code=True ).eval() @app.get("/v1/models") def list_models(): # 客户端启动时会先探这个接口,返回的 id 要和请求体里的 model 字段一致 return {"object": "list", "data": [{"id": "glm-4-9b-chat", "object": "model"}]} if __name__ == "__main__": # workers 必须是 1:模型只在主进程加载了一份,多 worker 会撑爆显存 uvicorn.run(app, host="0.0.0.0", port=8000, workers=1)启动之后先用curl探一下,别急着接客户端:
curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "glm-4-9b-chat", "messages": [{"role": "user", "content": "写一个十行的 Python 快速排序"}], "temperature": 0.6, "stream": true }'workers=1这一行是血泪经验。uvicorn 多 worker 是进程级并行,每个进程都会完整加载一遍模型,四张卡也不够这么造。想要并发就上 vLLM,别在这儿堆 worker。stream: true时返回的是 SSE 格式的data: {...}分块,最后一块是data: [DONE],客户端解析要按行切,不要一次性json.loads整个响应体。
接口通了以后,把这个地址填到你常用的编辑器插件或者内部工具里,把 model 名字对上,基本就完成了私有化接入,不用改客户端代码。
4. 微调链路:LoRA SFT 从 jsonl 数据到权重合并
4.1 数据格式:一份能直接喂进去的 jsonl 长什么样
finetune_demo吃的是 jsonl,一行一条样本,每条是一个messages数组。格式对不上是训练启动失败的第一大原因:
{"messages": [{"role": "system", "content": "你是一名严谨的工单分类助手,只输出分类结果"}, {"role": "user", "content": "登录之后点导出按钮就白屏"}, {"role": "assistant", "content": "分类:前端渲染异常"}]} {"messages": [{"role": "user", "content": "发票抬头填错了能改吗"}, {"role": "assistant", "content": "分类:账务咨询"}]}三个细节要盯住。第一,system不是必须的,但一旦你有,就要保证训练集和线上推理用的是同一个 system prompt,否则模型学到的行为在线上不触发。第二,assistant 内容要短、要规整,SFT 阶段模型是在模仿这段文本的分布,你给它一段两百字的解释,它以后就很难只输出八个字。第三,整个文件必须是 UTF-8 无 BOM,Windows 上编辑过的 jsonl 经常带 BOM,读第一行就抛异常。
数据量上,几百条能看出方向,几千条才有实用价值。我一般会切 9:1 出训练和验证,验证集不用大,两百条足够观察过拟合。max_input_length这个参数是硬截断,超过就砍尾巴,如果你的关键信息在 prompt 末尾,会被砍掉,所以要么改大这个值,要么把关键信息放前面。
4.2 配置文件与 finetune.py 的核心训练片段
finetune_demo用 yaml 管参数,训练脚本读配置再组装 Trainer。配置长这样(字段名不同版本略有差异,以仓库里的样例为准):
# finetune_demo/configs/lora.yaml data_config: train_file: /data/finetune/train.jsonl val_file: /data/finetune/dev.jsonl num_proc: 8 # 预处理进程数,CPU 核多就调大 max_input_length: 1024 max_output_length: 512 training_args: output_dir: /data/finetune/out-lora per_device_train_batch_size: 1 # 9B 模型单卡基本只能 1,靠累积顶 gradient_accumulation_steps: 8 # 等效 batch = 8 learning_rate: 1.0e-4 # LoRA 的 lr 比全参大一个量级 num_train_epochs: 3 logging_steps: 5 save_steps: 200真正注入 LoRA 的那几行在finetune.py里:
from peft import LoraConfig, get_peft_model, TaskType lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, # rank,8 或 16 起步,数据少于一千条别上 64 lora_alpha=32, # 一般取 4*r,缩放系数 lora_dropout=0.05, target_modules=["query_key_value"], # 示例值,实际以仓库配置为准,别照抄 LLaMA 的 q_proj bias="none", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 正常落在 0.1%~1%,超了说明 target_modules 挂多了target_modules是这套代码里最容易抄错的地方。LLaMA 系的教程都写q_proj、v_proj,GLM 系的注意力层命名不一样,直接抄会命中零个模块——peft 有时不报错,只是什么都没训,你看着 loss 在动,其实动的是别的东西。稳妥做法是打开仓库自带的 configs 目录,照着里面的名字写;或者先print([n for n, _ in model.named_modules()])看一眼真实层名。
r=8配alpha=32是个保守组合,对风格迁移、分类、固定格式输出这类任务够用。想让模型学新知识,rank 要往上加,但加 rank 不如加数据。learning_rate=1e-4是 LoRA 的常见起点,全参微调一般用 1e-5 到 2e-5,这两个别混。
跑起来就是一行:
cd GLM-4-main/finetune_demo bash scripts/train.sh configs/lora.yamltrain.sh里通常就是torchrun或python finetune.py --config。多卡时它会带上--nproc_per_node,但注意 LoRA 的多卡是数据并行,每张卡上都有一份完整基座,显存并不会因为多卡而变宽,只是 batch 变大。
4.3 合并权重与推理验证
训练完output_dir里是一堆 adapter 权重,几十到几百 MB,不能直接当模型用,得和基座合并:
import torch from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base_dir = "/data/models/glm-4-9b-chat" lora_dir = "/data/finetune/out-lora" out_dir = "/data/models/glm-4-9b-chat-sft" tokenizer = AutoTokenizer.from_pretrained(base_dir, trust_remote_code=True) base = AutoModelForCausalLM.from_pretrained( base_dir, torch_dtype=torch.bfloat16, trust_remote_code=True, device_map="cpu", # 合并放 CPU,省显存,慢一点但不容易 OOM ) merged = PeftModel.from_pretrained(base, lora_dir).merge_and_unload() merged.save_pretrained(out_dir, safe_serialization=True) tokenizer.save_pretrained(out_dir) # 少这行,推理端会自己造 chat_templatemerge_and_unload()做的是把B @ A * scale加回原权重,然后把 LoRA 层摘掉,产出一个标准模型目录。device_map="cpu"是刻意的——合并过程不需要前向计算,放 CPU 上慢几十秒,但绝对不会因为显存不足中断。safe_serialization=True输出 safetensors,比 bin 加载快,也不会被杀毒软件锁文件。
验证环节别只看 loss 曲线。把验证集里十条样本拿出来,分别用合并前和合并后的模型跑一遍,逐条对比输出。分类任务看标签对不对,生成任务看格式有没有跑偏。合并后效果突然消失,八成是 tokenizer 没一起保存,或者合并时用错了基座——用 base 版基座去合 chat 版的 adapter,输出会非常怪。
5. 踩坑排查:五处让人卡半天的报错现场
5.1 环境与加载阶段的两个翻车点
现象一:from_pretrained直接抛ValueError: ... model type 'glm' not recognized,或者 tokenizer 加载完chat_template是 None。
原因:transformers 版本低于权重要求的版本,内置注册表里没有这个架构;或者脚本里漏了trust_remote_code=True,回退到了默认建模类。
解决:先升 transformers 到requirements.txt里给的区间,再检查每一处from_pretrained有没有带这个参数。tokenizer 的 chat_template 为 None,通常是权重目录里缺tokenizer_config.json或者被手改过,从原始包里重新拷一份,别自己拼。
现象二:模型能加载,但第一次调用卡住不动,或者报连接超时。
原因:脚本里写的是在线仓库名,正在往外网拉权重,而机器访问不通或很慢。
解决:提前把权重下到本地目录,路径写成绝对路径常量;离线机器上加local_files_only=True,让它直接失败而不是干等。缓存目录换到数据盘也很重要,默认的~/.cache经常是根分区,几十 GB 下去系统就满了。
5.2 显存与推理阶段的两个坑
现象三:int4 量化脚本站起来就报CUDA error或者bitsandbytes相关的 so 加载失败;或者不报错,但第一次推理就 OOM。
原因:bitsandbytes的版本和 CUDA、torch 对不上,这个包的二进制是编译好的,装错版本不会在 import 阶段暴露;OOM 则是device_map把全部层塞进了一张卡,或者max_new_tokens设得太大。
解决:按官方对照表重装bitsandbytes,装完立刻跑一次 4bit 的小模型做冒烟测试。显存问题用device_map="auto"配max_memory={0: "18GiB"},给系统留出余量;max_new_tokens从 512 起,别一上来就 4096。
现象四:输出自问自答、复读、或者夹杂一堆<|endoftext|>之类的特殊 token。
原因:chat 模型没用apply_chat_template,或者反过来了——拿 Base 版模型套了 Chat 模板。还有一种是把eos_token_id覆盖错了,模型不知道什么时候该停。
解决:Chat 版走apply_chat_template(..., add_generation_prompt=True),Base 版老老实实做文本续写。TextStreamer记得加skip_special_tokens=True。如果换了 tokenizer,eos_token_id要从 tokenizer 里读,不要硬编码。
5.3 微调与合并阶段的坑
现象五:训练日志里 loss 一路下降,看着挺正常,合并之后效果几乎为零,甚至比基座还差。
原因:target_modules写的名字在模型里一个都没匹配上,peft 静默地训了个空 adapter;或者合并时基座精度用的是 fp16 而训练时是 bf16,数值上有偏移;还有一种常见情况是训练数据里 system prompt 和线上不一致,模型学到的触发条件在线上没出现。
解决:训练开始前打印一次print_trainable_parameters(),可训练参数占比接近 0 就是没挂上;合并时基座精度和训练保持一致;把线上实际用的 system prompt 原样写进训练集。这三条检查完,八成问题就定位了。
6. 二次开发:改哪几个文件、怎么验证没改坏
这份源码最实际的用法不是跑一遍 demo 就完事,而是当成一个底座去改。我一般按三个方向动手,改动面从小到大。
第一层,改服务端。basic_demo里的 OpenAI 兼容服务端是最值得加东西的地方:加一层 API Key 校验、加请求日志、加 token 计数、加超时熔断。这些都是纯 FastAPI 的活,不动模型代码,风险最低。加鉴权就是在路由上加依赖注入,把校验逻辑塞进Depends,别去改推理函数。
第二层,改推理参数与会话管理。多轮对话要自己维护messages数组,并且控制总长度——超过max_input_length就要从最早的轮次开始丢,丢的时候注意别把 system prompt 丢了。工具调用那块,模型返回的调用结构要自己解析再回填结果,composite_demo里有现成的解析函数可以参照,但它的写法偏演示,线上要把解析失败、参数缺失这些分支补齐。
第三层,改微调配置。想让模型学会固定格式输出,先把target_modules扩到注意力层之外的所有线性层,再调r,最后才考虑换数据。顺序反了会浪费很多 GPU 时间。
改完怎么验证?我用一个最笨但最管用的办法:固定一组 prompt 存成 txt,写个脚本分别打基座和改后的服务,把两边输出并排写进一个文件,人眼扫一遍。
import json, urllib.request PROMPTS = [l.strip() for l in open("regression_prompts.txt", encoding="utf-8") if l.strip()] def ask(url, prompt): body = json.dumps({"model": "glm-4-9b-chat", "messages": [{"role": "user", "content": prompt}], "temperature": 0.1}).encode() # 温度压到 0.1,减少随机性 req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}) return json.loads(urllib.request.urlopen(req, timeout=120).read())["choices"][0]["message"]["content"] with open("diff_report.md", "w", encoding="utf-8") as f: for p in PROMPTS: f.write(f"## {p}\n\n**base**\n\n{ask('http://127.0.0.1:8000/v1/chat/completions', p)}\n\n" f"**new**\n\n{ask('http://127.0.0.1:8001/v1/chat/completions', p)}\n\n")这组 prompt 要覆盖三类:正常业务问题、边界输入(空串、超长、纯符号)、以及你最在意的那类输出格式。温度压到 0.1 是为了让对比可复现,不然两次跑出来不一样,你根本分不清是改动生效了还是采样随机。我一般会攒到二十条左右,太少覆盖不住,太多看不完。
还有一个技巧值得说:改 chat template 的时候,把渲染后的实际字符串打印出来存文件。很多人调不通是因为脑子里想的格式和 tokenizer 实际渲染出来的完全不是一回事,看到真实字符串,问题一目了然。
从那以后我每次动推理脚本或者模板,都强制走一遍这组固定 prompt 的对比,哪怕只改了一个参数。肉眼看输出可能觉得都差不多,并排放在一起才发现模型开始漏掉格式里的某个字段。这套流程花不了十分钟,比线上出问题再回滚省事得多,希望帮到你。
本文还有配套的精品资源,点击获取