简介:这份 GLM-4 代码仓库源码压缩包面向希望快速上手智谱 GLM-4 的开发者与学习者,提供从基础对话、视觉理解到微调与部署验证的代码参考,适合具备 Python 基础、想研究大模型推理服务和二次开发的中高级读者。包内共 78 个文件,整体约 7.57MB,以 Python 脚本、Markdown 说明、YAML/JSON 配置和少量图片、TypeScript 前端资源为主,分别用于示例运行、参数配置、文档阅读与界面演示。目录围绕基础演示、微调示例、批处理与命令行工具、OpenAI API 服务、vLLM 调用、网页与视觉交互以及 Intel OpenVINO、ITREX 设备示例展开,便于理解 GLM-4 的调用链路和工程组织。已有 307 人学习,可作为研究源码结构、搭建本地推理服务与探索微调流程的实践素材。
1. 拿到 glm4 代码仓库源码 zip 包,先别急着 pip install
下载完 glm4 代码仓库源码 zip 包,很多人的第一反应是解压、进目录、pip install -r requirements.txt三连,然后在 CUDA 版本冲突和 flash-attn 编译错误里耗掉半天,最后连模型长什么样都没见到。这个包真正的价值不在“装上”,而在于它把权重加载、tokenizer、chat template、推理脚本这几件事串成了一条能本地跑通的最小链路,是一份可以改、可以嵌进自己业务、也可以当作微调起点的工程骨架。它适合三类人:想在单卡上把 GLM-4 跑起来做效果验证的算法同学,需要把它包成内部 API 的后端同学,以及准备在它上面挂 LoRA 做领域适配的训练同学。下面的顺序就是我实际拆包的顺序,从目录结构一路讲到服务化和回归验证。
2. 解压与目录结构:仓库里真正干活的只有这几个文件
zip 包解开之后第一件事不是装依赖,是把目录摸清楚。同一个仓库里通常混着三类东西:能直接python xxx.py跑起来的示例脚本、被别的脚本 import 的库代码、以及一堆 CI 配置和文档。分不清这三类,后面看报错就会完全失去方向——你以为是环境问题,其实是跑错了入口文件。
2.1 解压后的分层:先找入口,再看依赖
我一般不解压到当前目录,而是单独开一个工作目录,把源码和权重分开放。原因是权重动辄十几 GB,和源码混在一起之后,du -sh、打包、传服务器都会变得很难受。
# 源码和权重分两个目录,后面所有脚本都靠这两个路径 mkdir -p ~/work/glm4 && cd ~/work/glm4 unzip ~/Downloads/glm4-repo.zip -d ./src cd ./src # 只展开两层,避免一屏刷出几千行 find . -maxdepth 2 -type d | sort # 找可以直接跑的入口:带 __main__ 的脚本通常就是 demo grep -rl "__main__" --include="*.py" . | head -20 # 依赖清单先看一眼,别急着装 ls -la requirements*.txt setup.py pyproject.toml 2>/dev/null这三条命令解决的是不同问题。find -maxdepth 2让你在一屏内看清仓库的顶层组织,是一层src/包着所有代码,还是examples/、training/、serving/平铺。grep -rl "__main__"是在找“可执行入口”,这类文件才值得你花时间读,其他的多半是被 import 的组件。最后那条ls是确认依赖声明到底在哪:有的仓库把依赖拆成requirements.txt和requirements-dev.txt,训练依赖单独放,装错文件会出现“推理能跑、微调缺包”的怪现象。
提示:
head -20不要省。有些仓库的examples/下有几十个脚本,全部刷出来反而看不出重点。
2.2 环境怎么定:Python、torch、CUDA 的对齐顺序
依赖装崩,九成是顺序错了。正确的顺序是:先看显卡驱动支持到什么程度,再定 CUDA runtime,再定 torch wheel,最后才装仓库的 requirements。反过来做,就会陷入“装了 torch 发现 CUDA 不匹配、卸了重装、结果 requirements 又把 torch 升回去”的死循环。
# 1. 看驱动支持的上限,右上角那个 CUDA Version 是上限不是当前版本 nvidia-smi # 2. 用独立虚拟环境,别污染系统 Python python3 -m venv .venv && source .venv/bin/activate python -V pip install --upgrade pip setuptools wheel # 3. 先单独装 torch,装完立刻验证,别等 requirements 一起装 pip install torch --index-url https://download.pytorch.org/whl/cu121 python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.device_count())"torch.cuda.is_available()返回False是第一个必须当场解决的信号,不要带着这个状态往下装。常见原因是虚拟环境里装的是 CPU 版 wheel,或者宿主机驱动版本低于 wheel 要求的 CUDA runtime。确认能打印出True和卡数之后,再执行仓库的pip install -r requirements.txt。如果 requirements 里再次出现 torch,用pip install -r requirements.txt --no-deps之外更稳的做法是先看它锁的 torch 版本和你装的是否一致,不一致就以能跑通的那份为准,把 requirements 里那行注释掉。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
torch.cuda.is_available()为 False | 装了 CPU 版 wheel / 驱动过低 | 按驱动上限重装对应 cu 版本 |
| import torch 报 undefined symbol | torch 与系统 CUDA 库混装 | 卸载 conda 里的 cudatoolkit,只用 pip wheel |
| 装 flash-attn 卡在编译半小时 | 无预编译 wheel,本机编译 | 用对应版本预编译包,或推理阶段先关掉 |
2.3 权重与代码的对接:代码只认目录,不认 zip
这一步是新手最容易翻车的地方。模型权重通常也是压缩包形式分发,但加载代码需要的不是那个压缩包,而是解压后的目录,目录里必须有config.json、tokenizer_config.json、model.safetensors.index.json和若干分片文件。如果你把 zip 路径直接传进去,报错信息往往是一句含糊的“找不到 config”,看不出是路径问题。
# 权重单独解压,确认关键文件齐全 mkdir -p ~/work/glm4/weights unzip ~/Downloads/glm4-weights.zip -d ~/work/glm4/weights ls ~/work/glm4/weights | head # 分片文件应该和 index.json 里登记的列表对得上 python -c " import json,os d='$HOME/work/glm4/weights' idx=json.load(open(os.path.join(d,'model.safetensors.index.json'))) missing=[f for f in set(idx['weight_map'].values()) if not os.path.exists(os.path.join(d,f))] print('缺失分片:', missing or '无') "最后那段小脚本值得留着。分片下载不完整是极常见的静默故障:文件在,名字对,但大小是 0 或者只下了一半,加载时报的是“unexpected end of file”之类的底层错误,很难往“文件没下全”上想。先用 index.json 里的weight_map反查一遍,能把这类问题挡在推理之前。权重目录和源码目录分开之后,后面换模型、换量化版本都只是改一个路径常量的事。
3. 把仓库跑起来:单卡推理脚本的三段式改造
仓库自带的 demo 通常只覆盖最理想的一条路径,输入一句话、打印一段输出。要把它变成自己能用东西,需要做三件事:把加载和生成拆开、把 chat 模板这一层显式化、把批量与显存这两件事算清楚。这三件事做完,脚本才具备被复用和调参的基础。
3.1 最小可跑脚本:tokenizer 与 chat template 的两处硬坑
对话模型的输入不是纯文本,是经过 chat template 拼装后的 token 序列。直接tokenizer("你好")喂进去,模型看到的格式和训练时完全不一样,输出会变得非常随意,甚至自问自答。正确写法是把消息列表交给apply_chat_template,并且一定要带add_generation_prompt=True。
from transformers import AutoModelForCausalLM, AutoTokenizer import torch MODEL_PATH = "/home/user/work/glm4/weights" # 解压后的目录,不是 zip tok = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtype=torch.bfloat16, # 40 系/Ampere 及以上走 bf16,老卡退 fp16 device_map="auto", # 单卡也建议写,多卡时自动切分 trust_remote_code=True, ).eval() messages = [{"role": "user", "content": "用三句话说明 KV Cache 为什么能省算力"}] inputs = tok.apply_chat_template( messages, add_generation_prompt=True, # 少这一句,模型不知道轮到自己说话了 return_tensors="pt", return_dict=True, ).to(model.device) with torch.no_grad(): out = model.generate(**inputs, max_new_tokens=256, do_sample=False) # 一定要切掉 prompt 部分,否则会把你的问题一起打印出来 ans = tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) print(ans)几个参数各自的作用值得说清楚。torch_dtype决定权重加载精度,bf16 在支持的卡上数值稳定性和速度都比 fp16 好,老架构卡(比如部分 V100 之前的卡)不支持 bf16,会直接报错或者退化成奇怪的数值。device_map="auto"在单卡场景下等价于把整模型放在 0 号卡,但写上之后换多卡不用改代码。add_generation_prompt=True是对话模板的关键开关,它负责在序列末尾补上“助手开始回答”的标记。最后那个切片out[0][inputs["input_ids"].shape[1]:]是硬要求,generate返回的是 prompt 加新生成内容的完整序列。
trust_remote_code=True这个参数要单独提一句。仓库里的模型实现代码会被动态加载执行,路径正确时它读的是你解压出来的那份源码,路径写错时它会去别的地方找,于是出现“我明明改了仓库代码但效果没变”的玄学现象。排查这类问题,在加载后打印一下model.__class__.__module__,能立刻确认到底加载的是哪份实现。
3.2 批量与流式:把 generate 包一层
单条推理验证完,下一步就是把接口做成能接受列表输入。这里有个非常隐蔽的坑:生成任务的 padding 方向必须是左边。因果语言模型是从左往右预测的,右侧 padding 会让模型的“当前位置”错位,批量结果互相污染,表现为第一个问题答得还行,后面几条答非所问或者直接复读第一条。
# 生成任务必须左侧 padding,这一行写在加载 tokenizer 之后 tok.padding_side = "left" if tok.pad_token is None: tok.pad_token = tok.eos_token texts = ["解释一下 LoRA 的低秩假设", "讲清楚量化里的 group size 是什么"] prompts = [ tok.apply_chat_template([{"role": "user", "content": t}], add_generation_prompt=True, tokenize=False) for t in texts ] batch = tok(prompts, return_tensors="pt", padding=True).to(model.device) with torch.no_grad(): outs = model.generate(**batch, max_new_tokens=256, do_sample=False, pad_token_id=tok.pad_token_id) for i, o in enumerate(outs): print(i, tok.decode(o[batch["input_ids"].shape[1]:], skip_special_tokens=True)[:120])pad_token缺失是另一个高频故障。很多对话模型的 tokenizer 默认没有 pad token,批量编码时直接抛异常,或者悄悄用了 eos 导致每行尾部多一个结束符。显式把pad_token设成eos_token是最省事的做法,代价是极少数情况下模型会在无关位置停止生成,可以接受。
流式的做法是把generate丢到子线程,主线程消费TextIteratorStreamer。这个模式的好处是不用改模型代码,坏处是流式下没法做 beam search,num_beams必须为 1。做内部工具时我一般默认开流式,首字延迟从几秒降到几百毫秒,体验差别很明显。
from transformers import TextIteratorStreamer from threading import Thread streamer = TextIteratorStreamer(tok, skip_prompt=True, skip_special_tokens=True) kwargs = dict(**inputs, max_new_tokens=512, streamer=streamer, do_sample=False) thread = Thread(target=model.generate, kwargs=kwargs) thread.start() for chunk in streamer: print(chunk, end="", flush=True) thread.join()3.3 精度与显存:bf16、fp16、int4 的取舍
显存不够是这类仓库落地时最实在的阻力。权重精度直接决定占用,一个几十亿参数量的模型,bf16 下光权重就要十几 GB,加上 KV Cache 和中间激活,单卡 24 GB 是很紧的。降精度的顺序我一般按“先降 KV Cache、再降权重、最后才上 4bit 量化”来走,因为 4bit 对输出质量的损伤在小模型上比较明显。
| 方案 | 权重占用(相对) | 质量影响 | 适用场景 |
|---|---|---|---|
| bf16 | 1.0 | 无 | 有 A100/4090 级别卡,追求稳定 |
| fp16 | 1.0 | 轻微,长文本偶有溢出 | 不支持 bf16 的老卡 |
| int8 | 约 0.5 | 小 | 单卡 24 GB 跑长上下文 |
| int4 | 约 0.25 | 明显,指令跟随变弱 | 仅做演示或边缘设备 |
实际调的时候,先看max_new_tokens和上下文长度。KV Cache 的占用和batch_size × 序列长度 × 层数 × 隐藏维度成正比,把并发从 8 降到 2,往往比换量化版本更能解决问题,而且不损失质量。这也解释了为什么很多人在压测时发现“单条跑得好好的,一上并发就 OOM”——问题不在权重,在缓存。
4. 服务化与微调:把 glm4 仓库改成一个能对外提供接口的工程
脚本能跑通和能对外提供服务之间,还差一个推理引擎和一层接口协议。自己用 FastAPI 包一层generate也能用,但并发上来之后显存和吞吐会很难看,原因是 HF 的原生generate没有做 continuous batching。这一步换引擎的收益远大于调参。
4.1 用 vLLM 起一个 OpenAI 兼容服务
换成 vLLM 之后,仓库里的模型实现依然能被复用,前提是权重目录结构是标准格式。启动命令里几个参数决定成败,尤其是显存水位和最大长度,这两个设大了直接起不来。
python -m vllm.entrypoints.openai.api_server \ --model /home/user/work/glm4/weights \ --served-model-name glm4-local \ --dtype bfloat16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --trust-remote-code \ --port 8000--max-model-len是预分配的上下文长度上限,设成 32768 会直接吃满显存,启动阶段就报 KV Cache 不足;按业务实际需要设,宁可先小后大。--gpu-memory-utilization 0.85留 15% 给激活值和碎片,设到 0.95 时看起来能塞下,实际一有长请求就 OOM。--trust-remote-code在模型需要自定义实现时必须加,否则加载阶段就失败。启动成功的标志是日志里打印出 “Uvicorn running” 并且模型加载耗时正常结束,卡在这一步的话,先把--max-model-len砍半再试。
4.2 客户端调用与并发下的显存水位
服务起来之后,客户端用标准 OpenAI SDK 就能接,这一点让它的迁移成本极低——原来调云端接口的代码只改base_url和model两个字段。
from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY") resp = client.chat.completions.create( model="glm4-local", messages=[{"role": "user", "content": "把这条日志归类:ERROR timeout after 3 retries"}], temperature=0.2, # 分类任务压低随机性 max_tokens=128, ) print(resp.choices[0].message.content)压测的时候要盯两个数:显存占用和 P99 延迟。显存随并发上升是正常的,但如果在某个并发值上突然台阶式跳变,通常说明有请求触发了长度上限,把 KV Cache 撑开了。这时候的处理不是加卡,是把客户端侧的超长输入截断,或者给服务端加一层长度校验。temperature这类采样参数在服务端不生效,由客户端传,所以线上一致性要靠调用方约束,这一点很容易在联调时被忽略。
4.3 LoRA 微调:数据格式与显存账
仓库里如果带了微调脚本,它的入口一般是接受一个 jsonl。数据格式的细节决定训练能不能收敛,最常见的问题是只写了问答对、没写角色标记,训练出来的模型不会按对话格式输出。
{"messages": [{"role": "user", "content": "把这句话改写得正式一些:这玩意儿不太好使"}, {"role": "assistant", "content": "该功能目前存在可用性问题。"}]}字段名要和仓库脚本里解析的那套对上,有的是conversations、有的是messages,写错不报错,只是 loss 一直不变。训练启动参数里,per_device_train_batch_size设 1、靠gradient_accumulation_steps凑等效批量,是小显存下最稳的组合。
python finetune.py \ --model_name_or_path /home/user/work/glm4/weights \ --train_file ./data/train.jsonl \ --max_seq_length 2048 \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 8 \ --learning_rate 1e-4 \ --num_train_epochs 2 \ --lora_rank 16 \ --lora_alpha 32 \ --output_dir ./out-lora \ --bf16lora_rank和lora_alpha的比值大致决定适配强度,16/32 是一个比较温和的起点;max_seq_length直接乘进显存,2048 和 8192 的差距是数量级的。跑之前先抽 20 条数据确认模板拼出来的字符串长什么样,比训练两小时后再去看 loss 曲线高效得多。
5. 避坑与排查:六个翻车现场的现象、原因和解法
下面这几条都是我在不同机器、不同仓库分支上真实遇到过的,按“现象 → 原因 → 解决”整理,遇到问题可以按这个顺序对着排查。
5.1 环境与依赖类:装不上、装上了跑不动
坑一:import torch 报 undefined symbol。现象是pip install全部成功,python -c "import torch"直接抛undefined symbol: c10::...。原因通常是机器上同时存在 conda 安装的 cudatoolkit 和 pip 装的 torch wheel,动态库加载到了错误的那一份。解决办法是彻底清掉环境重建,只用 pip 装 torch,不要在同一个环境里混用 conda 的 CUDA 包。
坑二:flash-attn 编译半小时后失败。现象是安装过程长时间卡在 building wheel,最后报编译器版本或内存不足。原因是这个包默认从源码编译,对 nvcc 版本和内存都有要求。可行的做法是优先找与 torch、CUDA、Python 三者版本都匹配的预编译包;实在找不到,就在推理脚本里把 attention 实现切回默认,功能不受影响,只是长序列慢一些。
坑三:权重加载报 shape mismatch 或意外的 key。现象是加载到 90% 突然报某个张量维度不对。原因基本是权重版本和代码分支不匹配,比如拿量化版的权重配了非量化的加载代码,或者反过来。解决办法是对着权重目录里的config.json看量化相关字段,确认加载代码走的是同一条分支,不要凭目录名猜。
5.2 推理与输出类:能跑但答非所问
坑四:输出重复、复读、或者上来就自问自答。现象是不报错,但回答质量明显不像这个量级的模型。原因多半是 chat template 没生效,或者漏了add_generation_prompt。解决方式是先把模板拼出来的原始字符串打印出来看,正常情况下应该能看到明确的角色标记和结尾的助手起始标记;看不到就说明模板压根没应用上。
坑五:批量推理时第二条开始串味。现象是单条测都好,一批量就互相污染。原因是padding_side用了默认的 right,生成任务必须 left。修法是在加载 tokenizer 之后立刻设tok.padding_side = "left",并显式设置pad_token。
坑六:nvidia-smi显示还有空余却 OOM。现象是显存占用显示 20/24 GB,但请求一进来就报 out of memory。原因是显存碎片加上 KV Cache 预分配,可用连续块远小于表面数字。处理方式是下调max_model_len、把gpu_memory_utilization从 0.95 降到 0.85,或者直接减少并发。加卡之前先试这三条,多数情况能省下一笔预算。
注意:六个坑里有四个的根因都是“路径或版本对不上”,而不是代码有 bug。遇到报错先确认加载的到底是哪份代码、哪个权重目录,比读源码快得多。
6. 进阶验证:用一段固定 prompt 集给仓库做回归
改完 chat template、换过量化版本、调过生成参数之后,最怕的是“感觉好像变差了但说不清哪里变差”。我的做法是准备一份固定的 prompt 集,二十条左右,覆盖四类任务:指令跟随、信息抽取、多轮追问、长文本摘要。每条只保留输入和期望要点,不写标准答案,因为生成式输出没法逐字比对。
import json, hashlib from difflib import SequenceMatcher from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY") cases = [json.loads(l) for l in open("regress.jsonl", encoding="utf-8")] def run(tag): rows = [] for c in cases: r = client.chat.completions.create( model="glm4-local", messages=[{"role": "user", "content": c["q"]}], temperature=0, max_tokens=256, ) txt = r.choices[0].message.content.strip() rows.append({"id": c["id"], "tag": tag, "out": txt}) return rows base = run("base") new = run("new") # 逐条算相似度,低于 0.6 的人工看一眼 for b, n in zip(base, new): s = SequenceMatcher(None, b["out"], n["out"]).ratio() if s < 0.6: print(b["id"], round(s, 2), "|", n["out"][:100])脚本里temperature=0是为了让同一版本两次调用结果尽量稳定,SequenceMatcher只是给一个粗粒度信号,真正要看的是“改动前后哪几条回答变化最大”。如果变化集中在信息抽取任务上,说明你的改动影响了指令跟随;如果集中在长文本上,多半是上下文截断。这套东西跑一遍不到十分钟,但能挡掉大部分“上线后才发现效果回退”的事故。
| 回归维度 | 观察指标 | 异常信号 |
|---|---|---|
| 指令跟随 | 是否按格式输出 | 出现多余解释、格式跑偏 |
| 信息抽取 | 字段完整性 | 漏字段、字段名改成中文 |
| 多轮追问 | 上下文引用是否正确 | 忘记上一轮内容 |
| 长文本 | 是否被截断 | 结尾突兀、重复开头 |
我现在的习惯是:任何一次动到 chat template、量化配置或者推理引擎版本的改动,都先把这份 20 条的基线跑出来存一份,改完再跑一份 diff,两份输出摆在同一个窗口里看。这个习惯是被一次“只改了一个采样参数结果抽取准确率掉了十几个点”的事故逼出来的,那之后我才明白,模型工程的确定性不是靠感觉,是靠可重复的对照。希望帮到你。
本文还有配套的精品资源,点击获取