从 7B 到 Baichuan2,我算是把百川大模型系列的部署和微调都摸了一遍。最早拿到 Baichuan-7B 的时候,我还是拿单卡 A100 在跑推理,后来 Baichuan2 系列出来,又在 vLLM 和 LoRA 微调上折腾了好几个星期。如果你现在正盯着百川的模型列表犹豫该下哪个权重,或者已经下载完却在加载时报了一堆错,那这篇文章应该能帮你省下不少时间。我尽量把选型逻辑、部署步骤、微调原理和踩坑过程串起来讲,不讲空话。
1. 先搞清楚百川各个版本之间的真实差距
1.1 参数、词表与上下文:7B 和 13B 不是简单翻倍
百川系列最常被拿出来对比的两组尺寸就是 7B 和 13B。很多人以为 13B 就是 7B 的“加大杯”,部署时把显存预算乘个两倍就行。但真实情况是,参数量的变化只影响权重体积,真正决定你能不能跑得动,还得看词表大小、上下文长度、激活值占用这些容易被忽略的指标。
模型加载时的权重占用大概可以按“参数数量 x 字节数”来估算。BF16 精度下每个参数占 2 字节,所以 7B 的权重大约是 14GB,13B 大约 26GB。这里说的还是纯权重,没算 KV Cache 和激活值。百川系列的词表做到 64K 以上,这比很多英文界模型要大,好处是中文压缩率高、编码碎片少,代价是 embedding 矩阵本身吃掉的显存比同参数量英文模型更多。上下文方面,初代 Baichuan 默认是 4096,Baichuan2 相同。如果你要处理长文档,只看权重估算是不行的,序列长度一旦拉长,生成阶段的 KV Cache 会指数级往上走。
这时你会发现,7B 和 13B 的区别不仅仅是“13B 更强”这么简单。同样的任务,13B 在复杂推理和指令遵循上确实有可感知的提升,但推理速度会下降,微调显存也会紧很多。我的建议很简单:如果你的任务以中文问答、信息抽取、摘要为主,7B 微调之后完全够用;如果要做复杂多轮推理或长文本分析,再考虑 13B。
1.2 代际差异:Baichuan2 比初代强在哪
Baichuan2-7B 和 Baichuan-7B 虽然参数规模差不多,但两代模型在能力分布上差别不小。官方发布时给过一组评测对比,Baichuan2 在数学、代码、逻辑推理这些维度上普遍领先初代,尤其在中文场景下的指令跟随能力提升明显。从使用角度说,Baichuan2 的生成文本更稳,同样的提示词下不太容易出现答非所问或上下文漂移。
我实测下来的感觉是:初代 Baichuan-7B 更适合做文本表征类任务,比如向量化、信息抽取,它的基座属性更强。Baichuan2-7B 则在对话类场景表现更好,你直接拿去接客服问答、写文案、做知识库回答都比初代省事。Baichuan2-Chat 系列在加载时用官方指定的对话模板,不需要你自己拼复杂的历史消息,这也在一定程度上减少了工程上的坑。
另外要注意,Baichuan2 的对齐程度更高,但也意味着它更“听话”,输出风格更收敛。如果你需要多样化表达,比如生成营销文案或创意故事,反倒可以试着把温度调到 0.8 以上。而如果只是做结构化输出,温度保持在 0.3 以下会更稳。这些参数细节后面部署时候专门讲。
1.3 按你的显存和任务来选型
我把自己用下来的选型方法整理成了一张表,你可以直接对着参考。这里说的显存都是指推理时至少需要预留的量,如果还要做 LoRA 微调,显存需求另算。
| 你的硬件 | 推荐模型 | 说明 |
|---|---|---|
| 8GB 显存 | Baichuan-7B / Baichuan2-7B + 4bit 量化 | 只做推理,不要指望微调 |
| 16GB 显存 | Baichuan2-7B BF16 推理 | 可以跑 LoRA 微调 |
| 24GB 显存 | Baichuan2-13B 4bit 推理 / Baichuan2-7B BF16 微调 | 13B 量化推理更实际 |
| 40GB 以上 | Baichuan2-13B BF16 推理 + LoRA 微调 | 14B 全参微调仍然紧张 |
| 多卡(每张 24GB+) | Baichuan2-13B 全参微调 | 需要 DeepSpeed ZeRO 或 FSDP |
这张表不是绝对的,但至少能帮你避开“下载了 13B 才发现 8GB 显存根本塞不下”的尴尬。选型时优先看任务复杂度,再看显存,最后才是参数尺寸。很多人一上来就追求最大模型,结果卡在部署环节,反而耽误时间。
2. 部署前的准备工作:显存估算与环境安装
2.1 权重、KV Cache、峰值显存怎么算
部署之前把显存算清楚,比报错了再查日志高效得多。推理时的显存占用大致是三块之和:模型权重、激活值、KV Cache。
模型权重最简单,BF16 下就是参数量(10^9) x 2 字节。激活值和 KV Cache 则跟序列长度强相关。KV Cache 的计算公式大约是2 x 层数 x 多头数 x head_dim x 序列长度 x batch_size x 字节数,百川的层数和头数不同版本有差异,你跑起来之后可以通过nvidia-smi实测。我习惯直接用「权重 + 2GB 保底」来粗估,比如 7B BF16 我就按至少 16GB 去准备,13B 按至少 32GB。这不是精确计算,但在选机器阶段够用了。
如果你只是做一次性的离线推理,一次性加载权重后马上释放就行,峰值显存和持续显存差距不大。但如果要做 API 服务,多个并发请求会成倍放大 KV Cache 需求。我遇到过单请求没超显存、并发 4 个请求直接 OOM 的情况,所以生产部署我一般会在权重预算上再预留 30% 给并发。不要贪,宁可把 batch_size 调小一点。
2.2 CUDA、PyTorch 与 Python 版本搭配
百川模型加载用到的依赖其实不复杂,简单说就是 PyTorch + transformers + 相关工具库。但版本搭配如果不当,你会在from_pretrained那一步遇到各种莫名其妙的报错,比如找不到BaichuanForCausalLM或FlashAttention编译失败。
我目前验证过比较稳的组合是:Python 3.10、PyTorch 2.1.x、CUDA 12.1、cuDNN 8.9、transformers 4.35 以上、accelerate 0.25 以上。如果你已经有 PyTorch 2.0 环境也能跑,但部分新版模型代码会对trust_remote_code的处理方式有要求,老版本容易踩坑。注意,baichuan模型是走自定义代码加载的,所以你还需要把trust_remote_code=True打开。这个参数后面单独说,这里先记住它是必须的。
如果你要用 vLLM 做服务化,vLLM 的版本也要和 CUDA、PyTorch 对齐。我第一次装 vLLM 时直接用最新版,结果和已有的 CUDA 11.8 不兼容,折腾了一下午。后来我学乖了,先在干净环境里建一个虚拟环境,把 CUDA 和 PyTorch 装好,之后再装 vLLM,出错概率低很多。
2.3 国内镜像与依赖安装顺序
环境安装的顺序我建议是:先确认驱动和 CUDA 工具包,再装 PyTorch,之后装 transformers 和加速库,最后才装 vLLM 这类推理框架。依赖顺序如果乱掉,比如先装 vLLM 再装 PyTorch,很可能会把 PyTorch 版本改掉,到时候新旧版本冲突更麻烦。
国内环境下,直接用官方源下载大模型依赖可能比较慢,用镜像源会是顺手的选择。我通常在 pip 里配置清华源,pip install torch会快很多,尤其是一些大体积的 CUDA 版本 wheel 包。另外,模型的权重文件建议用huggingface-cli download或者直接从镜像站拉取,断点续传的工具我习惯用hfd,比一次次跑git clone省心。整包下载后放到本地目录,加载时直接填本地路径,不要每次都走在线加载。
3. 第一次推理:用 transformers 把百川跑起来
3.1 基础模型和对话模型的加载差异
百川系列的加载方式分两类:一类是基础模型,比如 Baichuan-7B、Baichuan2-7B;另一类是对话模型,比如 Baichuan2-7B-Chat。如果你是做文本生成,建议直接用 Chat 版本,因为它在基座上做了指令对齐,输出更完整。如果你只是做特征提取或微调底座,那再考虑基础版本。
代码上,两类模型在加载时都要加trust_remote_code=True,但对话模板的拼接方式不同。基础模型直接输入纯文本就行,Chat 模型则要按官方定义的格式组织用户消息和助手消息。这是最容易出错的地方:你直接丢一句"帮我写个文案"给 Chat 模型,它也能回答,但多轮对话时,因为历史消息没按模板拼接,模型会越聊越糊涂。所以我建议把模板封装好,而不是零散地拼字符串。
3.2 最小推理代码与 chat 模板
下面这段代码是我验证过的,能用最少步骤跑通 Baichuan2-7B-Chat:
import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_id = "./baichuan2-7b-chat" tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True ) messages = [ {"role": "user", "content": "介绍一下长征二号F火箭的技术特点"} ] prompt = tokenizer.apply_chat_template(messages, tokenize=False) inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate( inputs.input_ids, max_new_tokens=256, do_sample=True, temperature=0.6, top_p=0.85 ) response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True) print(response)这里有几个细节值得注意。apply_chat_template是新的 transformers 推荐做法,百川的 tokenizer 代码里定义了对应模板,不需要你手动拼<human>之类的符号。max_new_tokens要按任务调:摘要生成 200 就够,开放式写作可以给 512 甚至更多。skip_special_tokens=True建议开着,否则输出里会漏出 BOS/EOS 等特殊标记,干扰后面的文本处理。
3.3 trust_remote_code 为什么必须开
百川和不少中文大模型在 HuggingFace 上打包时,并不是用标准的LlamaForCausalLM结构,而是带了一份自定义建模文件,只有加载时开启trust_remote_code=True,transformers 才会临时加载模型目录里的modeling_baichuan.py和tokenization_baichuan.py。如果你不开,它会直接报错,提示你要不要用远程代码。这个设计听起来吓人,但本质上是给模型开发者预留更灵活的定制空间。
我个人的建议是,不要因为安全问题就绕开这个机制。你只要确认是从可信渠道下的模型,那trust_remote_code=True就是必要步骤。如果你真的很谨慎,可以先下载模型文件,打开modeling_baichuan.py扫一眼,再决定要不要加载。绝大部分情况下,这个文件就是常规的 transformer 层堆叠和采样逻辑,没有隐藏的高风险行为。
4. 生产环境部署:vLLM 与量化方案
4.1 vLLM 带来的吞吐量提升
如果只是自己在笔记本上跑一跑,transformers 的generate足够。但一旦要把模型做成接口给多个用户调用,直接用 transformers 的 naive 方式会有明显问题:生成请求串行排队、GPU 利用率低、显存被 KL Cache 占光。这时候就该换 vLLM。
vLLM 的核心思路是 PagedAttention,把 KV Cache 划分成固定大小的块,按需分配,不像 transformers 那样一次性预留完整空间。实测下来,我在单张 40GB 显存的卡上用 Baichuan2-7B-Chat 部署 vLLM,并发 8 个请求依然能保持每请求几百 token 的输出,整体吞吐比原生 transformers 部署高了不少。对于这种体量的模型,vLLM 是最省事的生产方案。
启动时用 OpenAI 兼容的接口,客户端完全可以用openai库来对接,代码层面没有任何学习成本。下面就是一个典型的启动命令:
python -m vllm.entrypoints.openai.api_server \ --model ./baichuan2-7b-chat \ --tokenizer ./baichuan2-7b-chat \ --tensor-parallel-size 1 \ --dtype bfloat16 \ --max-model-len 4096 \ --gpu-memory-utilization 0.9 \ --trust-remote-code--gpu-memory-utilization可以按显存实际情况调整,0.9 意味着让 vLLM 尽量用满 90% 的显存,剩下的留给其他进程。如果同时跑多个服务,可以调低到 0.7。另外一个需要注意的参数是--max-model-len,它直接决定了最大序列长度上限,设得太大反而会限制并发的 batch 数。我建议按你的业务需求来,不要盲目追求长文本。
4.2 GPTQ 量化与 bitsandbytes 量化的取舍
低显存机器上跑百川,量化几乎是必经之路。目前用的比较多的两条路线:一是离线用 GPTQ 把模型压成 4bit,二是加载时用 bitsandbytes 做 4bit/8bit 推理。
GPTQ 需要先在 A100 这类高显存设备上对数据集做校准,产出一份量化后的权重,之后加载就变得很轻。7B 模型 4bit 量化后权重约 4GB,13B 约 7-8GB,8GB 显存卡也能勉强跑 7B 量化版。bitsandbytes 则更方便,直接在from_pretrained里传load_in_4bit=True就行,适合快速验证。如果要做生产服务,我建议优先用 GPTQ:它推理时无需动态量化,速度和稳定性更好。但要注意,GPTQ 量化后的模型在复杂数学和代码任务上可能比 BF16 版本有明显下降,生产前一定要对齐测试集做回归评估。
4.3 部署成 OpenAI 兼容 API
vLLM 启动成功后,可以用一行 Python 代码验证:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY") resp = client.chat.completions.create( model="baichuan2-7b-chat", messages=[{"role": "user", "content": "用一句话解释什么是大模型"}], temperature=0.6, max_tokens=128 ) print(resp.choices[0].message.content)接口路径、返回结构都兼容 OpenAI 标准格式。这样做的好处是,你后续想换成其他模型,只要改一下base_url和model,业务代码几乎不用动。我在多个项目里都用这一套,省掉了重复写前端适配层的成本。
5. 微调之前必须掌握的机制
5.1 全参微调的成本计算
很多人一听说“微调”,第一反应就是整个模型重新训练一轮。但 7B 模型全参微调对显存的需求远超你想象:除了权重和梯度,你还得保存优化器状态。以 AdamW 为例,每个参数需要额外保存一阶动量、二阶动量,以及对应浮点状态,加起来大概占参数的 12 字节。这意味着 7B 模型仅优化器状态就可能超过 80GB 显存,单卡根本塞不下。
即便你用 DeepSpeed ZeRO-3 或 FSDP 做多卡分片,团队也要有足够工程能力去处理稳定性、断点保存和梯度同步问题。对于大多数个人开发者或中小企业,全参微调不是性价比最高的方案。反而是 LoRA 这类参数高效微调方法,能把可训练参数量压缩到全部参数的 0.1%-1%,单卡就能跑起来。
5.2 LoRA 和 QLoRA 为什么省显存
LoRA 的核心是冻结原始权重,仅在注意力层旁边插入低秩矩阵。这么做的好处是梯度和优化器状态只跟低秩矩阵有关,显存占用大幅下降。QLoRA 更进一步,把原始权重先用 NF4 量化成 4bit,再叠加 LoRA 训练。这样一来,原本 7B 模型需要 80GB 才能做的全参微调,被压缩到 16GB 甚至更低的显存就能训练。
我用 QLoRA 微调过 Baichuan2-7B,在单张 24GB 卡上,设置lora_rank=64、lora_alpha=16、max_length=1024、batch_size取 4,再用梯度累积到 16,整个训练过程的显存峰值大约在 22GB 上下。这个参数组合不是随便拍的:rank 越高,拟合能力越强,但显存和训练时间同步上升;batch_size 受显存限制不能设太大,改成梯度累积来虚拟放大 batch 是常见的折中。
5.3 微调样本怎么组织
微调百川这类模型时,数据组织方式比模型选型更影响效果。我不建议直接丢一堆纯文本让模型练生成,而是按指令格式组织。以 Alpaca 格式为例,每条样本包含 instruction、input、output 三个字段。Chat 模型微调则偏好更完整的对话历史。下面是一个日常任务微调的样本示例:
{ "instruction": "请根据商品信息生成一段吸引人的商品描述", "input": "品牌:某户外用品;货名:轻量折叠椅;重量:0.9kg;承重:120kg;特点:收纳体积小,适合露营钓鱼。", "output": "这款轻量折叠椅仅重0.9kg,收纳后体积不及一瓶水,却能承重120kg。无论是露营还是钓鱼,带上它都能让你随时坐下来享受户外。" }数量上,我自己的经验是 2 千到 1 万条高质量样本就能看到明显效果,关键在多样性:把不同长度、不同表达风格、不同行业的内容混合进去,避免模型对某个句式过拟合。另外,无监督语料和指令数据按比例混合也很重要,训练时别把上万条纯文本语料全扔掉,保留 20%-30% 让模型保持原有语言能力,效果会稳定很多。
6. 手把手微调一个 Baichuan2-7B-Chat
6.1 用 QLoRA 做中文指令微调
我以一个具体的“商品文案生成助手”为例子。目标是把一个通用 Chat 模型调成更懂电商文案口吻的模型。数据是几千条“商品信息 + 描述文案”的配对。准备阶段要做三件事:清洗文本、删除过长样本、切分训练集和验证集。这里有个容易被忽略的点:如果原始样本超过配置的max_length,训练时会被截断,模型学到不完整指令,输出往往也会戛然而止。所以我建议把max_length=1024但只保留 900 token 以内的样本,留一点余量给生成目标。
6.2 训练脚本与关键参数
我基于 HuggingFace 的 Trainer 写了下面的训练脚本,这也是目前最通用的方式。LoRA 层主要加在注意力层的q_proj、k_proj、v_proj、o_proj上,部分实现会把gate_proj、up_proj也加上。以我的经验,加上 MLP 层能提升一点性能,但显存消耗也会上涨,按需取舍。
import torch from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForSeq2Seq ) from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training model_id = "./baichuan2-7b-chat" tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True, load_in_4bit=True ) model = prepare_model_for_kbit_training(model) lora_config = LoraConfig( r=64, lora_alpha=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config) training_args = TrainingArguments( output_dir="./lora-baichuan2-7b", per_device_train_batch_size=4, gradient_accumulation_steps=4, learning_rate=2e-4, num_train_epochs=3, lr_scheduler_type="cosine", warmup_ratio=0.03, logging_steps=20, save_steps=500, fp16=True, grad_checkpointing=True, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, data_collator=DataCollatorForSeq2Seq(tokenizer), ) trainer.train()挑几个参数说明一下:lora_alpha=16对应r=64时,缩放因子是 16/64=0.25,效果相当于让低秩矩阵对原始权重的影响不至于过猛。学习率 2e-4 是 LoRA 微调常用的起点,如果数据量小可以降到 1e-4,数据量大且噪声多可以提到 3e-4,但再高就容易震荡。fp16=True能在部分硬件上减少显存,同时提一点速度;如果生成质量波动,改成bf16=True也可以。
6.3 合并权重、转格式与部署
训练完成后,模型权重还保存在低秩分支里,推理时可以用 PeftModel 动态加载,但如果你要在 vLLM 等服务上使用,更推荐把 LoRA 权重合并回主模型,导出一份完整的权重文件。合并代码也比较短:
from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained( "./baichuan2-7b-chat", trust_remote_code=True, torch_dtype=torch.bfloat16, device_map="auto" ) merged_model = PeftModel.from_pretrained(base_model, "./lora-baichuan2-7b") merged_model = merged_model.merge_and_unload() merged_model.save_pretrained("./baichuan2-7b-chat-merged", safe_serialization=True) tokenizer.save_pretrained("./baichuan2-7b-chat-merged")合并后的模型可以直接被 vLLM 加载,也能继续用from_pretrained读取。这个过程最怕的是 LoRA 适配器和 base 模型版本不匹配。我的建议是微调前记录好基础模型的 commit id 或文件名,合并时用同一个目录,避免出现维度对不上的情况。另外,merge_and_unload()之后要检查一下 PEFT 的配置文件是否还残留,如果加载时报错说找不到 adapter,可以先删除未知键再重试。
7. 那些文档里没写的坑
7.1 OOM:不要只调 batch_size
用 QLoRA 微调 Baichuan2 时,最常见的错误是CUDA out of memory。很多人第一反应是把per_device_train_batch_size调成 1,但 OOM 不一定只由 batch_size 引起。最有效的三板斧是:开gradient_checkpointing=True,减少激活值缓存;把max_length从 2048 降到 1024;再配合梯度累积保持有效 batch 不变。三者组合之后,24GB 卡跑 7B QLoRA 是完全可行的。
还要注意一个隐蔽问题:load_in_4bit=True在部分驱动下会触发慢速且高显存的kbit算子。如果训练特别慢并且显存异常,可以检查是否安装了最新的bitsandbytes版本,或者尝试切换到 8bit。实测下来,不同版本之间显存占用差距能达到几个 GB。
7.2 中文乱码与分词效果异常
百川系列对中文的支持总体不错,但仍有特殊情况。当训练数据里混入大量特殊符号、emoji、生僻字时,AutoTokenizer的默认处理方式可能把文本切成大量 unk 标记,导致生成内容变烂。我的经验是:清洗数据时不要只做“去 HTML 标签”这种基础处理,还要跑一遍 tokenizer,统计文本里的 unk token 数,超过一定比例就单独清理。
另外,如果你在微调时给不同角色加了<im_start>之类的自定义标记,需要确保 tokenizer 和模型训练时的 special token 列表一致。否则推理时会碰到tokenizer.decode输出一堆奇怪符号的问题,这个问题排查起来非常折磨,但通常就是模板字符串和 tokenizer 的 special token 映射没对上。
7.3 生成效果总是重复或跑题
生成阶段的问题往往不是模型不够好,而是采样参数没调对。我在部署商品文案生成服务时,遇到的典型问题是句子重复。这时我会先用no_repeat_ngram_size=3做硬性拦截,再把温度从默认值降到 0.6-0.8,top_p调到 0.85 左右。如果效果仍然跑题,问题大概率在提示词模板:提示词里给的信息必须精确。你要让模型生成“卖点描述”,就要在指令里明确指定“控制在50字以内,突出轻便和承重能力”之类约束,而不是笼统说“写一段广告”。
还有一个小建议:把微调数据和生成评估放在一起看。你训练时的输出格式是商品信息 + 商品描述,推理时也要保持同样的字段次序和提示格式。模型对格式的记忆比我们想象中强得多,你只要在推理时换个模板,输出质量就会掉一截。
从我自己的经历来看,百川系列的部署和微调并没有想象中那么高门槛,但每一步都藏着小陷阱。如果你只是做推理,先按选型表把模型定下来,然后把 vLLM 服务跑通;如果你需要更贴合业务的效果,再考虑 QLoRA 微调那套流程。吴恩达那句“先用最简方案跑通,再考虑优化”放在这里非常适用。希望这篇攻略能让你少走几个弯路。最后提醒一句,模型文件比较大,下载时记得校验 SHA256 人哈希值,免得文件损坏之后排查半天找不到原因。