GLM4源码包部署实战:从解压配置到OpenAI兼容服务

发布时间:2026/11/6 2:53:09
GLM4源码包部署实战:从解压配置到OpenAI兼容服务

简介:这是一份围绕 GLM-4 代码仓库整理的源码压缩包,面向具备一定 Python 基础、希望了解大模型工程实现、复现实验流程或开展二次开发的研究者与开发者。压缩包共 78 个文件,约 7.57MB,内含 28 个 Python 脚本、14 个 Markdown 文档、9 张图片,以及 yaml、json、ts、txt 等配置与说明材料;目录按基础演示、微调、OpenAI API 服务、vLLM、Web 与批处理演示等模块组织,文档补充环境配置、使用方式和目录说明,便于快速定位关键实现。已有 307 人学习下载,在 GLM-4 相关实践中具备一定参考价值。读者可获得较完整的仓库源码、示例脚本和配置线索,用于梳理模型调用、微调与演示程序的实现思路,也可作为搭建本地实验环境、排查依赖问题与开展二次开发的参考资料,对理解大模型应用从脚本到服务的组织方式也有帮助。

1. 解压完 glm4 源码包,先弄清它替我省掉了哪三件事

上周有人把 glm4 代码仓库源码 zip 包 丢过来,第一句话是「解压完打开一看几十个 py 文件,从哪下手」。这是拿到大模型源码包最典型的状态:权重得另外下,压缩包里只有代码,第一眼看上去像一堆没头没尾的脚本。其实这套 glm4 源码干的是三件脏活:对话模板拼接、KV cache 与位置编码处理、推理服务的接口层。没有它,你得自己对着 tokenizer 配置拼<|user|>这类特殊 token,自己处理 batch padding 和长文本外推,自己造一套兼容 OpenAI 协议的转发服务。适合谁:想在本机跑通多轮对话、要做私有化接口、打算改模型结构或挂 LoRA 微调的人。纯调云端接口的,下这个包没意义。

2. 解压后先读骨架:四个目录决定这套代码能不能直接跑

很多人解压之后直接去找 README 里的python demo.py,跑不起来就开始怀疑包有问题。顺序反了。先看清楚它的目录骨架,判断这个 zip 是「纯代码 + 少量示例」还是「代码连同小权重一起打包」,后面所有环境准备的动作都取决于这个判断。我一般会花十分钟做三件事:量体积、列目录、读配置文件。这三步做完,这套代码大概什么水平、能不能直接进生产、缺哪些外部依赖,心里就有数了。

2.1 先量体积再列目录,区分纯代码包和带权重的包

压缩包解压到独立目录,别和已有工程混在一起,否则后面import冲突排查起来非常痛苦。

# 解压到独立工作目录,避免与已有工程混在一起 mkdir -p ~/workspace/glm4-src && cd ~/workspace/glm4-src unzip ~/Downloads/glm4-code.zip -d . # 先看总体积:几十 MB 是纯代码,几个 GB 大概率带了权重或示例数据 du -sh . # 只看两层目录,层级再深就不是骨架问题了 find . -maxdepth 2 -type d | sort # 按体积排前 20 的文件,能立刻发现是不是塞了 .safetensors 或 .bin du -ah . | sort -rh | head -20

du -sh那一步是分水岭。如果总体积只有几十兆,说明权重得单独去模型托管仓库下载,代码包里通常只留一个config.json之类的占位;如果体积到了几个 GB,包内可能带了量化权重或者示例数据集,这时候要注意解压后的磁盘占用会翻倍,先确认剩余空间。find -maxdepth 2看的是顶层组织方式,常见的有两种:一种是把推理、微调、量化、服务端各放一个目录,另一种是所有 py 文件平铺在根目录,靠文件名区分。前者好办,后者要额外读一遍__init__.py和入口脚本的 import 关系,才知道哪些文件是核心、哪些是临时脚本。

2.2 config.json 里七个字段,读完就知道能不能上长文本

config.json是这套代码的说明书。结构文件长得再花,最终都是照着这个配置实例化的。下面这几个字段建议逐个对一遍:

{ "architectures": ["Glm4ForCausalLM"], "hidden_size": 4096, "num_hidden_layers": 40, "num_attention_heads": 32, "num_key_value_heads": 2, "max_position_embeddings": 8192, "rope_theta": 10000.0, "torch_dtype": "bfloat16", "eos_token_id": [151329, 151336, 151338] }
字段读它干什么踩错会怎样
architectures决定AutoModel走哪个类类和权重不匹配,加载直接报 KeyError
num_hidden_layers算 KV cache 显存、定 LoRA 挂载层数层数算错,长上下文必 OOM
num_attention_heads/num_key_value_heads判断是不是 GQA、几个 query 共享一个 KV 头自己改推理代码时 head 数对不上,输出全是乱码
max_position_embeddings原生支持的上下文长度上限超长输入不报错但效果断崖式下跌
rope_theta/rope_scaling长文本外推的依据缺缩放配置硬拉长文本,句子重复
torch_dtype决定加载时用 bf16 还是 fp16老卡上 bf16 起不来,fp16 又容易溢出
eos_token_id可能是数组,不止一个停止符只认一个 eos,模型停不下来一直生成

重点说num_key_value_heads。这个值和num_attention_heads不相等就说明用了分组查询注意力,KV cache 的显存只跟前者相关。很多人按全量 head 数去估算显存,算出来吓自己一跳,实际部署时发现余量很大,就是这里读错了。eos_token_id是数组这点也值得单独记一笔,GLM-4 系列的对话结束符通常不止一个,推理时如果自己写停止条件只判断单个 id,模型会一直续写下去,表现出来的现象是「回答完了还在自言自语」。

还有一个隐藏位置:对话模板不一定在config.json里,可能在tokenizer_config.json的chat_template字段,也可能是一个独立的.jinja文件。找到它,后面跑推理能省掉一半调试时间。

2.3 依赖清单核对:torch 和 transformers 的版本是第一个雷

源码包里的requirements.txt通常只锁了宽松的上下界,直接pip install -r装出来不一定能跑。我的习惯是先确认三件事再装:

# 1. 确认显卡驱动和 CUDA 运行时版本,决定装哪个 torch 轮子 nvidia-smi | head -15 # 2. 看依赖清单里有没有写死版本,尤其是 transformers 和 torch cat requirements.txt # 3. 看代码里实际用到的 API,反推最低版本要求 grep -rn "from transformers import\|from peft import\|import flash_attn" --include=*.py . | head -20

第 3 步是关键。如果代码里出现了较新的AutoModelForCausalLM参数或者attn_implementation这类开关,而你的 transformers 版本偏老,报错信息会非常含糊,常见的是TypeError: __init__() got an unexpected keyword argument,看着像源码有 bug,实际是版本不匹配。反过来,版本太新也可能翻车:一些自定义建模文件用了内部 API,新版本一升级就失效。经验做法是,先用requirements.txt给定的范围装一遍,跑通之后再考虑升级,别一上来就装最新版。

trust_remote_code=True这件事要提前想清楚。如果模型结构是用仓库里的modeling_*.py自定义实现的,加载时必须开这个开关,而且那个 py 文件必须和权重放在同一个目录,或者用--model-path指到包含它的路径。这一步没做对,报错通常是「找不到类」或者「权重里有 unexpected key」,后面第五章会细说。

3. 跑通第一条推理:从 config.json 到 chat template 对齐

骨架读完了,接着是最有成就感也最容易卡住的一步:让它吐出一句人话。卡点基本不在模型本身,而在两个地方——精度和模板。精度决定能不能加载,模板决定输出像不像对话。这一章给的是能直接抄的流程,跑通之后再谈参数调优。

3.1 建环境:三步把依赖装到不打架

独立环境是底线,别往系统 Python 里装。CUDA 版本和 torch 轮子的对应关系搞错,会得到一堆看起来毫不相关的报错。

# 1. 建独立环境,Python 版本按 requirements.txt 的要求来 conda create -n glm4 python=3.10 -y conda activate glm4 # 2. 先装 torch,要和自己驱动支持的 CUDA 版本对齐 # cu121 只是示例,按 nvidia-smi 右上角的版本号改 pip install torch --index-url https://download.pytorch.org/whl/cu121 # 3. 再装其余依赖,避免 pip 顺手把 torch 降级 pip install -r requirements.txt pip install accelerate peft # 多卡切分和 LoRA 常用,缺了会很别扭 # 4. 自检:四行输出里任何一行不对,先别往下走 python -c "import torch, transformers; \ print('torch', torch.__version__); \ print('transformers', transformers.__version__); \ print('cuda', torch.version.cuda); \ print('device', torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU only')"

第 2 步和第 3 步的顺序不能换。先装requirements.txt再装 torch,pip 的依赖解析很可能给你换一个不带 CUDA 的 CPU 版 torch,症状是torch.cuda.is_available()返回 False,但驱动明明正常,这个坑非常浪费时间。另外accelerate不装的话,device_map="auto"会直接抛异常,提示你缺包;peft不装,后面想做 LoRA 还得回头补。

3.2 加载模型并跑通第一条输出

权重目录和源码包是两个东西,路径别写混。下面这段是最小可运行版本:

import torch from transformers import AutoModelForCausalLM, AutoTokenizer MODEL_PATH = "./glm4-model" # 权重目录,必须包含 config.json 和自定义建模文件 # trust_remote_code=True:结构是仓库自带 py 文件实现的,必须开 tok = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( MODEL_PATH, torch_dtype=torch.bfloat16, # 老卡不支持 bf16 时改成 torch.float16 device_map="auto", # 多卡自动切分,单卡也行 trust_remote_code=True, ).eval() messages = [ {"role": "system", "content": "你是一个严谨的代码助手。"}, {"role": "user", "content": "把一个左连接的 SQL 改写成子查询,说明为什么这么改。"}, ] # 关键:用模型自带的模板拼 prompt,别手写特殊 token inputs = tok.apply_chat_template( messages, add_generation_prompt=True, # 补上让模型开始回答的前缀 return_tensors="pt", return_dict=True, # 同时拿到 attention_mask ).to(model.device) with torch.no_grad(): out = model.generate(**inputs, max_new_tokens=512, do_sample=False) # 只解码新生成的部分,输入部分要切掉 new_tokens = out[0][inputs["input_ids"].shape[1]:] print(tok.decode(new_tokens, skip_special_tokens=True))

这段代码有三个易错点。第一个是apply_chat_template,它按模型自带的模板把messages拼成带特殊分隔符的字符串。自己手拼<|user|>这类标记,格式差一个空格,模型表现就会明显变差,而且不会报错,只会让你怀疑模型能力不行。第二个是return_dict=True,它同时返回input_ids和attention_mask,generate接收字典参数,注意力掩码传不进去的话,有 padding 的场景会算错。第三个是解码切片,out里包含完整输入,只 decode 全部内容会把你的问题也打出来一遍,看起来像模型在复读。

torch_dtype这里也给个判断依据:安培架构之后的卡用 bf16,更老的卡只能用 fp16,但 fp16 容易在长序列上数值溢出,表现为输出突然变成重复字符或者nan。不确定就先试 bf16,加载报错再退回 fp16,不要一开始就为了兼容性牺牲稳定性。

3.3 生成参数与对话模板:输出像不像人话全在这

跑通之后,输出质量就由生成参数和模板对齐决定了。下面这几个参数是我每次都要调的:

参数常用值作用与调法
do_sample代码任务 False,创作任务 True关闭采样就是贪婪解码,同一输入输出稳定
temperature0.1 到 0.7越低越保守,超过 1 容易跑题
top_p0.8 到 0.95和 temperature 二选一为主,别同时拧到极端
repetition_penalty1.05 到 1.15高了会前言不搭后语,低了容易绕圈子
max_new_tokens按业务给是新生成长度,不是总长度,别和max_length混用
eos_token_id从 config 取需要传列表时不能只传单个 id

最容易被误解的是max_new_tokens和max_length。前者是「最多新生成多少 token」,后者是「输入加输出的总长度上限」。给一个很长的 prompt 又设了较小的max_length,模型还没开始回答就停了,输出是空字符串,第一次遇到会以为是加载失败。我在代码里一律用max_new_tokens,避免这个歧义。

多轮对话的模板处理也要说清楚。正确做法是每轮把完整历史重新喂进messages,让apply_chat_template统一处理,而不是自己把上一轮的输出拼成一个字符串再塞进新问题。后者会把角色标记搞乱,聊到第三四轮模型开始忘记系统提示词,或者把用户的话当成自己说的,这类问题排查起来非常费劲,因为它不报错。

4. 起一个兼容 OpenAI 协议的服务:启动参数与并发边界

本地脚本跑通只解决了一半问题,真正落地要把它变成服务。好消息是这类 glm4 源码包通常自带一个 OpenAI 兼容的 API 入口,前端和现有 SDK 几乎不用改就能接。麻烦的是启动参数和并发边界,配错了要么起不来,要么一上量就报错。

4.1 找到真正的服务入口,别照着 README 瞎猜

不同仓库里服务脚本名字差异很大,openai_api_server.py、api_server.py、serving/openai_api.py都有可能。与其一个个试,不如直接从 argparse 定义反查:

# 找出所有定义了命令行参数的 py 文件,服务入口一定有 grep -rln "add_argument" --include=*.py . | head -20 # 再从候选里筛出带 uvicorn / FastAPI / 端口参数的 grep -rn "uvicorn.run\|FastAPI\|--port\|--host" --include=*.py . | head -20

筛出入口之后,先跑--help看它接受哪些参数,比读 README 可靠:

python openai_api_server.py --help # 常见的一组启动参数 python openai_api_server.py \ --model-path ./glm4-model \ --host 0.0.0.0 \ --port 8000 \ --device cuda:0 \ --dtype bfloat16 \ --max-model-len 8192

--host 0.0.0.0是给容器或内网其它机器访问用的,只在本机自测写127.0.0.1更安全。--device在多卡机器上要显式指定,不指定有时会默认吃 0 号卡,和别人的任务抢显存。--max-model-len直接决定 KV cache 能开多大,这个值设得比实际需要大,显存会被提前占掉,设小了长输入会被截断且不报错,属于典型的静默失败。

4.2 用 curl 和 SDK 各打一次,确认协议兼容

服务起来之后,先用最原始的方式确认它能返回,再用 SDK 确认协议兼容:

# 最简请求:先确认能返回 JSON,不纠结内容质量 curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "glm4", "messages": [{"role": "user", "content": "用一句话解释什么是 KV cache"}], "temperature": 0.2, "max_tokens": 256, "stream": false }'
from openai import OpenAI # base_url 指到服务的 /v1,api_key 随便填,本地服务通常不校验 client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY") resp = client.chat.completions.create( model="glm4", # 必须和服务端注册的模型名一致 messages=[{"role": "user", "content": "写一个 Python 的单例装饰器"}], temperature=0, max_tokens=512, ) print(resp.choices[0].message.content) print(resp.usage) # 重点看 prompt_tokens 和 completion_tokens 是否符合预期

SDK 这一层要是能通,基本说明字段映射没问题。resp.usage值得单独看一眼:如果prompt_tokens明显偏小,说明模板没被正确应用,服务端可能走了裸文本通道;如果completion_tokens每次都贴着max_tokens,说明停止符没生效,模型在硬撑长度,长期跑会明显拖慢吞吐。

流式和同步两种模式的返回结构不同,前端接入前要确认服务端支持哪一种。很多自带的实现只做了非流式,前端传stream=true会直接 500,这个在联调阶段很常见。

4.3 显存与并发的边界:先把 max_model_len 算清楚

服务能不能扛住并发,取决于 KV cache 还剩多少显存。这个可以粗算:

KV cache 字节数 ≈ 2 × 层数 × kv_heads × head_dim × 序列长度 × 并发数 × 精度字节 其中 head_dim ≈ hidden_size / num_attention_heads

按num_hidden_layers=40、num_key_value_heads=2、head_dim=128、序列 8192、并发 8、bf16(2 字节)来算,KV cache 大约 2.7 GB。权重按 9B 量级、bf16 算约 18 GB,加起来二十出头,一张 24 GB 的卡刚好卡在边缘。这也是为什么max_model_len不要一上来就拉到模型支持的上限,先把并发和长度乘一遍再填。

参数影响调法
dtype权重显存优先 bf16,老卡退 fp16
max_model_lenKV cache 上限按真实业务长度设,加大 20% 余量
gpu_memory_utilization允许占用的显存比例和其它任务共卡时降到 0.8 以下
最大并发数队列长度与排队延迟从 4 起压,观察首 token 延迟
张量并行度多卡切分单卡放不下再开,切分有通信开销

并发这块有个血泪经验:单进程单模型的推理服务,请求进来是排队串行处理的,四五个并发还能忍,十几个一起打首 token 延迟会爆炸。要真正提吞吐,要么上框架自带的批处理调度,要么起多个实例在前面做负载均衡,别指望一个实例撑住几十个并发。压测时重点看两个指标:首 token 延迟和整体吞吐,前者决定体验,后者决定成本,只看平均响应时间没有意义。

5. 排查清单:六个把新手卡在下班前的坑

下面这六条是我在几套不同的大模型源码包上反复遇到的,按出现频率排。每条按现象、原因、解决来写,对着改就行。

5.1 加载与环境类的三个坑

坑一:开不开trust_remote_code都能加载,但报错完全不同。

现象:AutoModel.from_pretrained报ValueError: ... requires you to execute the configuration file,或者加载成功但输出全是乱码。原因:这套 glm4 的模型结构是用仓库自带的modeling_*.py定义的,不在 transformers 内置映射表里,不开开关会走错分支;开了开关但自定义文件没和权重放同一目录,也会拿到不完整的结构。解决:确认权重目录里同时存在config.json、modeling_*.py,加载时统一加trust_remote_code=True;如果用--model-path传路径,确保路径指向的是这个目录而不是它的父目录。

坑二:显存够,但加载阶段就 OOM。

现象:nvidia-smi看着还有十几 G 空闲,from_pretrained却报CUDA out of memory。原因多数是 dtype 没指定,默认按 fp32 加载,权重显存直接翻倍;另一部分是没有device_map,所有层先往一张卡上堆。解决:显式写torch_dtype=torch.bfloat16和device_map="auto",后者需要accelerate。如果还是不够,先用device_map="balanced"或者手动分层的max_memory参数把负载摊开。

坑三:tokenizer 找不到 chat template。

现象:调用apply_chat_template报TemplateError,或者拼出来的字符串里一个特殊 token 都没有。原因:模板可能放在独立的.jinja文件里,也可能是老版本 tokenizer 配置里没有这个字段。解决:先在目录里搜*.jinja和tokenizer_config.json里的chat_template键,找到模板文件后用tokenizer.chat_template = open(...).read()手动挂上去;实在没有,就照仓库里的示例脚本硬拼一遍,但要把每个特殊 token 的 id 打印出来核对。

5.2 输出质量与接口类的三个坑

坑四:回答完了还在自言自语。

现象:内容已经结束了,后面还跟着一串对话标记或者重复内容。原因:停止符判断不全,eos_token_id在配置里是数组,自己写推理逻辑时只判了一个 id;或者解码时没加skip_special_tokens=True,把角色标记也当正文打出来了。解决:从config.json或generation_config.json读出完整的eos_token_id列表传进去,解码统一加skip_special_tokens=True。

坑五:多轮对话聊到第三轮就失忆。

现象:前两轮正常,第三轮开始忘记系统提示,或者把用户的问题当成自己的回答。原因:手工拼接历史,角色标记顺序错乱,或者把模型上一轮的输出错误地追加成了assistant之外的角色。解决:每轮都重传完整的messages数组,由apply_chat_template统一生成,不要维护一个不断增长的字符串;调试时把模板拼出来的原始字符串打印一次,角色顺序一眼就能看出来。

坑六:本地脚本正常,起服务一波并发就 500。

现象:curl单条请求正常,压测几十个并发开始零星报错,日志里是显存不足或者请求超时。原因:单实例串行推理撑不住并发,或者max_model_len按最长可能的输入设了,KV cache 预留过大导致可用余量不足。解决:先把max_model_len降到业务真实长度的 1.2 倍,把gpu_memory_utilization留出 10% 余量;并发需求确实高就起多实例做前面一层分发,或者换用带连续批处理的服务框架。别在单实例上硬扛。

6. 微调与量化落地:用一份自测集验证改完的权重

源码包真正的价值在后面:它把训练侧脚本也放进来了。挂 LoRA 时我一般只改三处——数据格式、目标模块、输出目录。数据用 jsonl,一行一条{"messages":[...]},和推理时的模板保持一致,这一步不一致,训出来的模型在推理时表现会莫名其妙地差。目标模块常见的选择是注意力里的四个投影加 MLP 的三个投影:

# 典型的 LoRA 启动参数,按仓库脚本的参数名微调 python finetune.py \ --model_path ./glm4-model \ --data_path ./data/train.jsonl \ --output_dir ./out-lora \ --lora_rank 16 \ --lora_alpha 32 \ --lora_dropout 0.05 \ --target_modules q_proj k_proj v_proj o_proj gate_proj up_proj down_proj \ --per_device_batch_size 2 \ --gradient_accumulation_steps 8 \ --learning_rate 1e-4 \ --num_train_epochs 2

lora_rank和lora_alpha的比例大致保持 1:2,dropout别超过 0.1,否则小数据集上学不动。gradient_accumulation_steps是拿来凑等效批大小的,显存吃紧时优先加大它而不是减小学习率。训练结束后千万别只看 loss 曲线就上线,准备一份 30 到 50 条的固定自测集,改前改后各跑一遍,用do_sample=False保证可比,逐条对比。

量化环节同理。4bit 加载之后,最该验证的不是「能不能跑」,而是「行为有没有漂移」:

from transformers import BitsAndBytesConfig import torch bnb = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16, # 计算精度别跟着降到 4bit bnb_4bit_quant_type="nf4", )

bnb_4bit_compute_dtype保持 bf16 是个关键细节,把它也降下去,生成质量会掉得很难看。量化后跑同一份自测集,重点看两类指标:格式化输出(JSON、SQL、代码块)是否还合法,以及长回答有没有提前截断。前者对精度最敏感,后者能暴露停止符判断的问题。

从那以后我每次改完权重或者换量化配置,都强制走一遍「固定自测集 + 贪婪解码 + 逐条 diff」这套流程,宁可多花二十分钟,也不在上线后被业务方拿着一条退化样本找上门。这套 glm4 源码包本身不复杂,难的是把每一步的验证做扎实。希望帮到你。

本文还有配套的精品资源,点击获取