前言你有一只猫品种是狸花。你想教它一个新技能——握手。两种做法做法一从头教。买一本《猫的心理学》研究猫的行为学设计一套训练方案每天练 2 小时练 3 个月。猫学会了握手但可能也忘了怎么用猫砂。做法二微调。猫已经会坐、等、趴下——这些基础技能已经训练好了。你只需要在已有的基础上加一点点新训练。练 1 天握手就学会了。大模型的微调就是这个道理。ChatGPT 已经学会了语言理解和生成——这个能力是用几万亿 token 训练出来的耗时几个月、花费几百万美元。你不想从零训练一个 ChatGPT你只需要在它的基础上加一点你自己的数据比如客服对话、法律文书、医疗记录让它变成你们公司专属的 ChatGPT。cann-recipes-train 仓是昇腾 NPU 上的训练配方库GLM 微调配方是其中一个。这篇文章用类比的方式从零解释什么是大模型微调以及怎么用这个配方跑起来。为什么需要微调先说清楚一件事为什么不直接用 Prompt提示词用 Prompt 的方式叫** In-Context Learning**上下文学习。你给 ChatGPT 几个例子它照着例子学。这个方式简单但有两个问题问题一消耗 token。每个 Prompt 都要把例子带进去1 万条客服对话每条 100 token就是 100 万 token。Token 是要钱的OpenAI 的 GPT-4 API 每 1000 token 收几分钱100 万 token 就是几十块钱。每天 1 万条对话光 Prompt 的费用就得好几千。问题二精度不够。Prompt 的本质是在说话中教它。ChatGPT 能从几个例子里推断规律但如果你有几千条真实数据Prompt 的方式学得不够深。微调解决了这两个问题。微调的本质是在权重里教它——把你想要的模式直接编码到模型权重里。Prompt 里不需要带例子模型自己就知道该怎么答。打个比方Prompt 像是一张便签你每次问问题都把参考答案贴在便签上给模型看。微调像是把参考答案背到脑子里——不需要便签模型自己就能答。微调的两种方法Full FT vs LoRA微调有两种做法全量微调Full Fine-Tuning和参数高效微调PEFT。全量微调就是把所有参数都更新一遍。模型有 70 亿参数就更新 70 亿参数。效果最好但显存占用也最大——70 亿参数的 FP16 模型是 14GB加上梯度又是 14GB和优化器状态28GB至少要 56GB 显存。昇腾 910 单卡只有 64GB跑全量微调勉强能行但多卡并行的话通信开销很大。LoRALow-Rank Adaptation是一种参数高效微调方法。它的核心思想是与其更新整个权重矩阵不如只更新一个小的补丁。用猫来类比全量微调是重新训练整只猫的神经系统。LoRA 是在猫的大脑里植入一个小芯片——芯片很小但插进去之后猫就能握手。LoRA 的数学原理是假设原始权重是 W形状 d×d更新量是 ΔW。全量微调是直接更新 ΔW。LoRA 的做法是把 ΔW 分解成两个小矩阵的乘积ΔW A × B其中 A 是 d×r 矩阵B 是 r×d 矩阵r 是秩rank通常设为 8、16 或 64。d×d 的矩阵有 d² 个参数。LoRA 的补丁只有 d×r r×d 2dr 个参数。如果 r16d4096那么全量更新需要 4096² 1677 万个参数LoRA 只需要 2×4096×16 13 万个参数——减少了 99%。# LoRA 的实现简化版classLoRALinear(torch.nn.Module): LoRA 的线性层 原始: y W x LoRA: y W x (A B) x W x A (B x) 其中 A 和 B 是可学习的低秩矩阵 def__init__(self,in_features,out_features,rank16,alpha16):super().__init__()self.rankrank self.alphaalpha# 原始权重冻结不更新self.weighttorch.nn.Parameter(torch.randn(out_features,in_features)# W)self.weight.requires_gradFalse# 冻结# LoRA 补丁可学习# A: (rank, in_features)用随机数初始化# B: (out_features, rank)初始化为零# B 初始化为零是为了在训练初期LoRA 的输出是零# 这样 LoRA 的效果从零开始逐渐增加不会一开始就剧烈改变模型行为self.lora_Atorch.nn.Parameter(torch.randn(rank,in_features)# A)self.lora_Btorch.nn.Parameter(torch.zeros(out_features,rank)# B零初始化)defforward(self,x):# 原始输出original_outputtorch.nn.functional.linear(x,self.weight)# LoRA 输出先过 A再过 B# x: (batch, seq, in_features)# A: (rank, in_features)# B: (out_features, rank)# A x^T: (rank, batch, seq)# B A x^T: (out_features, batch, seq)lora_output(self.lora_B (self.lora_A x.transpose(-2,-1))).transpose(-2,-1)# 缩放LoRA 的输出要乘以 alpha/rank# alpha 是缩放因子控制 LoRA 对原始模型的影响程度# alpha/rank 通常设为 1 或 2returnoriginal_outputlora_output*(self.alpha/self.rank)这段代码展示了 LoRA 的核心思想。原始权重 W 被冻结不更新。只有 A 和 B 两个小矩阵是可学习的——这就把可训练参数从 d² 减少到了 2dr。GLM 微调配方端到端实操cann-recipes-train 仓的 GLM 微调配方把 LoRA 微调的完整流程配好了。环境准备# 1. 进入配方目录cdcann-recipes-train/recipes/glm/lora# 2. 安装额外依赖pip3installdeepspeed transformers datasets# DeepSpeed微软的分布式训练框架负责多卡并行和显存优化# transformersHuggingFace 的模型库# datasetsHuggingFace 的数据集库# 3. 准备数据集JSONL 格式# 每行一个 JSON包含 prompt 和 responseecho{prompt: 请介绍一下人工智能, response: 人工智能是...}train.jsonlecho{prompt: 什么是机器学习, response: 机器学习是...}train.jsonl# ... 更多数据 ...# 4. 确认 NPU 可用python3-cimport torch_npu; xtorch.randn(2,2).npu(); print(x.device)# npu:0配置微调参数配方提供了配置文件改几个参数就能跑# configs/glm-lora.yamlmodel:name:THUDM/chatglm3-6b# GLM-3 6B 模型trust_remote_code:truelora:rank:16# LoRA 的秩越高越强但显存占用越大alpha:16# 缩放因子通常设为 rank 的值dropout:0.05# LoRA 层的 dropouttarget_modules:# 哪些层加 LoRA-query_key_value-dense-dense_h_to_4h-dense_4h_to_htraining:num_epochs:3# 训练轮数batch_size:1# 单卡 batch size多卡会乘以卡数learning_rate:2e-4# 学习率LoRA 用比较大的学习率warmup_steps:100# 预热步数max_seq_length:512# 最大序列长度deepspeed:stage:2# DeepSpeed 优化阶段2ZeRO-23ZeRO-3gradient_accumulation_steps:16# 梯度累积实际 batch_size 1 * 16 16fp16:true# 混合精度训练npu:world_size:8# 总共几张卡master_addr:10.0.0.1# 主节点 IPmaster_port:29500# 主节点端口开始训练# 单机多卡训练8 张昇腾 910cdcann-recipes-train/recipes/glm/lora deepspeed train.py\--configconfigs/glm-lora.yaml\--data_path./train.jsonl\--output_dir./output/glm-lora-chat\--nproc_per_node8# 输出# [2024-01-15 10:30:00] 开始训练...# [2024-01-15 10:30:05] 加载模型: THUDM/chatglm3-6b ✓# [2024-01-15 10:30:15] 应用 LoRA ✓ (可训练参数: 3.8M / 6240M 0.06%)# [2024-01-15 10:30:20] 加载数据集: train.jsonl (1024 条) ✓# [2024-01-15 10:30:25] 启动 DeepSpeed ZeRO-2 ✓# [2024-01-15 10:30:30] 开始训练...## Step 100 | Loss: 1.234 | LR: 1e-4 | Time: 45s | Tokens/s: 12,500# Step 200 | Loss: 0.987 | LR: 2e-4 | Time: 44s | Tokens/s: 12,800# Step 300 | Loss: 0.765 | LR: 2e-4 | Time: 44s | Tokens/s: 12,600# ...# [2024-01-15 11:45:00] 训练完成# 总步数: 192 | 总耗时: 1h 15m | 平均吞吐: 12,500 tokens/strain.py的核心逻辑# train.py 的核心代码简化版importtorchimportdeepspeedfromtransformersimportAutoModel,AutoTokenizerfromdeepspeed.runtime.zeroimportFullyShardedDataParallelPluginfromtorch.utils.dataimportDataLoaderdefmain():# 1. 加载模型和 TokenizermodelAutoModel.from_pretrained(THUDM/chatglm3-6b,trust_remote_codeTrue,)tokenizerAutoTokenizer.from_pretrained(THUDM/chatglm3-6b)# 2. 应用 LoRAfrompeftimportget_peft_model,LoraConfig lora_configLoraConfig(r16,lora_alpha16,target_modules[query_key_value,dense,dense_h_to_4h,dense_4h_to_h],lora_dropout0.05,)modelget_peft_model(model,lora_config)model.print_trainable_parameters()# 输出trainable params: 3,814,784 || all params: 6,247,849,472 || trainable%: 0.061%# 3. 加载数据集datasetload_dataset(train.jsonl,tokenizer,max_length512)train_loaderDataLoader(dataset,batch_size1,shuffleTrue)# 4. DeepSpeed 配置ds_config{train_batch_size:16,# 实际 batch_size 1 * 梯度累积16 * 8卡 128gradient_accumulation_steps:16,fp16:{enabled:True},zero_optimization:{stage:2,# ZeRO-2分片优化器状态和梯度offload_optimizer:{device:cpu},# 优化器状态卸到 CPU},}# 5. 初始化 DeepSpeedmodel_engine,optimizer,_,_deepspeed.initialize(modelmodel,configds_config,)# 6. 训练循环model_engine.train()forstep,batchinenumerate(train_loader):# 前向outputsmodel_engine(**batch)lossoutputs.loss# 反向model_engine.backward(loss)model_engine.step()ifstep%1000:print(fStep{step}| Loss:{loss.item():.3f})# 7. 保存 LoRA 权重model_engine.save_checkpoint(./output/glm-lora-chat)print(训练完成)if__name____main__:main()合并权重LoRA 训练完后权重是分两块的原始权重冻结和 LoRA 补丁可学习。推理时要合并# 合并 LoRA 权重到原始模型frompeftimportPeftModelfromtransformersimportAutoModel# 加载原始模型base_modelAutoModel.from_pretrained(THUDM/chatglm3-6b)# 加载 LoRA 权重modelPeftModel.from_pretrained(base_model,./output/glm-lora-chat)# 合并LoRA 补丁加到原始权重上merged_modelmodel.merge_and_unload()# 保存合并后的模型merged_model.save_pretrained(./output/glm-lora-merged)合并后的模型就是一个标准的 GLM 模型可以直接用 transformers 加载推理fromtransformersimportAutoModel,AutoTokenizer modelAutoModel.from_pretrained(./output/glm-lora-merged)tokenizerAutoTokenizer.from_pretrained(./output/glm-lora-merged)input_text你们公司的客服工作时间是什么input_idstokenizer.encode(input_text,return_tensorspt)output_idsmodel.generate(input_ids,max_new_tokens100)print(tokenizer.decode(output_ids[0]))性能数据用 GLM-3-6B 在昇腾 9108 卡上做 LoRA 微调的性能数据配置显存占用/GB训练速度可训练参数全量微调568,500 tokens/s6.2BLoRA (r8)1812,500 tokens/s1.9MLoRA (r16)2012,200 tokens/s3.8MLoRA (r64)2411,800 tokens/s15.2M数据说明LoRA 把显存占用从 56GB 降到了 18-24GB训练速度反而更快因为显存压力小了batch 可以更大。可训练参数只有原始模型的 0.03%-0.24%但效果跟全量微调差不多。训练配方的核心不是代码本身是它帮你配好的超参和通信策略。DeepSpeed ZeRO-2 的分片策略、LoRA 的秩选择、学习率调度……这些参数花了很多时间调优。直接拿来用能省很多时间。仓库地址https://atomgit.com/cann/cann-recipes-train
昇腾CANN cann-recipes-train 仓:在大模型上做微调是什么体验
前言你有一只猫品种是狸花。你想教它一个新技能——握手。两种做法做法一从头教。买一本《猫的心理学》研究猫的行为学设计一套训练方案每天练 2 小时练 3 个月。猫学会了握手但可能也忘了怎么用猫砂。做法二微调。猫已经会坐、等、趴下——这些基础技能已经训练好了。你只需要在已有的基础上加一点点新训练。练 1 天握手就学会了。大模型的微调就是这个道理。ChatGPT 已经学会了语言理解和生成——这个能力是用几万亿 token 训练出来的耗时几个月、花费几百万美元。你不想从零训练一个 ChatGPT你只需要在它的基础上加一点你自己的数据比如客服对话、法律文书、医疗记录让它变成你们公司专属的 ChatGPT。cann-recipes-train 仓是昇腾 NPU 上的训练配方库GLM 微调配方是其中一个。这篇文章用类比的方式从零解释什么是大模型微调以及怎么用这个配方跑起来。为什么需要微调先说清楚一件事为什么不直接用 Prompt提示词用 Prompt 的方式叫** In-Context Learning**上下文学习。你给 ChatGPT 几个例子它照着例子学。这个方式简单但有两个问题问题一消耗 token。每个 Prompt 都要把例子带进去1 万条客服对话每条 100 token就是 100 万 token。Token 是要钱的OpenAI 的 GPT-4 API 每 1000 token 收几分钱100 万 token 就是几十块钱。每天 1 万条对话光 Prompt 的费用就得好几千。问题二精度不够。Prompt 的本质是在说话中教它。ChatGPT 能从几个例子里推断规律但如果你有几千条真实数据Prompt 的方式学得不够深。微调解决了这两个问题。微调的本质是在权重里教它——把你想要的模式直接编码到模型权重里。Prompt 里不需要带例子模型自己就知道该怎么答。打个比方Prompt 像是一张便签你每次问问题都把参考答案贴在便签上给模型看。微调像是把参考答案背到脑子里——不需要便签模型自己就能答。微调的两种方法Full FT vs LoRA微调有两种做法全量微调Full Fine-Tuning和参数高效微调PEFT。全量微调就是把所有参数都更新一遍。模型有 70 亿参数就更新 70 亿参数。效果最好但显存占用也最大——70 亿参数的 FP16 模型是 14GB加上梯度又是 14GB和优化器状态28GB至少要 56GB 显存。昇腾 910 单卡只有 64GB跑全量微调勉强能行但多卡并行的话通信开销很大。LoRALow-Rank Adaptation是一种参数高效微调方法。它的核心思想是与其更新整个权重矩阵不如只更新一个小的补丁。用猫来类比全量微调是重新训练整只猫的神经系统。LoRA 是在猫的大脑里植入一个小芯片——芯片很小但插进去之后猫就能握手。LoRA 的数学原理是假设原始权重是 W形状 d×d更新量是 ΔW。全量微调是直接更新 ΔW。LoRA 的做法是把 ΔW 分解成两个小矩阵的乘积ΔW A × B其中 A 是 d×r 矩阵B 是 r×d 矩阵r 是秩rank通常设为 8、16 或 64。d×d 的矩阵有 d² 个参数。LoRA 的补丁只有 d×r r×d 2dr 个参数。如果 r16d4096那么全量更新需要 4096² 1677 万个参数LoRA 只需要 2×4096×16 13 万个参数——减少了 99%。# LoRA 的实现简化版classLoRALinear(torch.nn.Module): LoRA 的线性层 原始: y W x LoRA: y W x (A B) x W x A (B x) 其中 A 和 B 是可学习的低秩矩阵 def__init__(self,in_features,out_features,rank16,alpha16):super().__init__()self.rankrank self.alphaalpha# 原始权重冻结不更新self.weighttorch.nn.Parameter(torch.randn(out_features,in_features)# W)self.weight.requires_gradFalse# 冻结# LoRA 补丁可学习# A: (rank, in_features)用随机数初始化# B: (out_features, rank)初始化为零# B 初始化为零是为了在训练初期LoRA 的输出是零# 这样 LoRA 的效果从零开始逐渐增加不会一开始就剧烈改变模型行为self.lora_Atorch.nn.Parameter(torch.randn(rank,in_features)# A)self.lora_Btorch.nn.Parameter(torch.zeros(out_features,rank)# B零初始化)defforward(self,x):# 原始输出original_outputtorch.nn.functional.linear(x,self.weight)# LoRA 输出先过 A再过 B# x: (batch, seq, in_features)# A: (rank, in_features)# B: (out_features, rank)# A x^T: (rank, batch, seq)# B A x^T: (out_features, batch, seq)lora_output(self.lora_B (self.lora_A x.transpose(-2,-1))).transpose(-2,-1)# 缩放LoRA 的输出要乘以 alpha/rank# alpha 是缩放因子控制 LoRA 对原始模型的影响程度# alpha/rank 通常设为 1 或 2returnoriginal_outputlora_output*(self.alpha/self.rank)这段代码展示了 LoRA 的核心思想。原始权重 W 被冻结不更新。只有 A 和 B 两个小矩阵是可学习的——这就把可训练参数从 d² 减少到了 2dr。GLM 微调配方端到端实操cann-recipes-train 仓的 GLM 微调配方把 LoRA 微调的完整流程配好了。环境准备# 1. 进入配方目录cdcann-recipes-train/recipes/glm/lora# 2. 安装额外依赖pip3installdeepspeed transformers datasets# DeepSpeed微软的分布式训练框架负责多卡并行和显存优化# transformersHuggingFace 的模型库# datasetsHuggingFace 的数据集库# 3. 准备数据集JSONL 格式# 每行一个 JSON包含 prompt 和 responseecho{prompt: 请介绍一下人工智能, response: 人工智能是...}train.jsonlecho{prompt: 什么是机器学习, response: 机器学习是...}train.jsonl# ... 更多数据 ...# 4. 确认 NPU 可用python3-cimport torch_npu; xtorch.randn(2,2).npu(); print(x.device)# npu:0配置微调参数配方提供了配置文件改几个参数就能跑# configs/glm-lora.yamlmodel:name:THUDM/chatglm3-6b# GLM-3 6B 模型trust_remote_code:truelora:rank:16# LoRA 的秩越高越强但显存占用越大alpha:16# 缩放因子通常设为 rank 的值dropout:0.05# LoRA 层的 dropouttarget_modules:# 哪些层加 LoRA-query_key_value-dense-dense_h_to_4h-dense_4h_to_htraining:num_epochs:3# 训练轮数batch_size:1# 单卡 batch size多卡会乘以卡数learning_rate:2e-4# 学习率LoRA 用比较大的学习率warmup_steps:100# 预热步数max_seq_length:512# 最大序列长度deepspeed:stage:2# DeepSpeed 优化阶段2ZeRO-23ZeRO-3gradient_accumulation_steps:16# 梯度累积实际 batch_size 1 * 16 16fp16:true# 混合精度训练npu:world_size:8# 总共几张卡master_addr:10.0.0.1# 主节点 IPmaster_port:29500# 主节点端口开始训练# 单机多卡训练8 张昇腾 910cdcann-recipes-train/recipes/glm/lora deepspeed train.py\--configconfigs/glm-lora.yaml\--data_path./train.jsonl\--output_dir./output/glm-lora-chat\--nproc_per_node8# 输出# [2024-01-15 10:30:00] 开始训练...# [2024-01-15 10:30:05] 加载模型: THUDM/chatglm3-6b ✓# [2024-01-15 10:30:15] 应用 LoRA ✓ (可训练参数: 3.8M / 6240M 0.06%)# [2024-01-15 10:30:20] 加载数据集: train.jsonl (1024 条) ✓# [2024-01-15 10:30:25] 启动 DeepSpeed ZeRO-2 ✓# [2024-01-15 10:30:30] 开始训练...## Step 100 | Loss: 1.234 | LR: 1e-4 | Time: 45s | Tokens/s: 12,500# Step 200 | Loss: 0.987 | LR: 2e-4 | Time: 44s | Tokens/s: 12,800# Step 300 | Loss: 0.765 | LR: 2e-4 | Time: 44s | Tokens/s: 12,600# ...# [2024-01-15 11:45:00] 训练完成# 总步数: 192 | 总耗时: 1h 15m | 平均吞吐: 12,500 tokens/strain.py的核心逻辑# train.py 的核心代码简化版importtorchimportdeepspeedfromtransformersimportAutoModel,AutoTokenizerfromdeepspeed.runtime.zeroimportFullyShardedDataParallelPluginfromtorch.utils.dataimportDataLoaderdefmain():# 1. 加载模型和 TokenizermodelAutoModel.from_pretrained(THUDM/chatglm3-6b,trust_remote_codeTrue,)tokenizerAutoTokenizer.from_pretrained(THUDM/chatglm3-6b)# 2. 应用 LoRAfrompeftimportget_peft_model,LoraConfig lora_configLoraConfig(r16,lora_alpha16,target_modules[query_key_value,dense,dense_h_to_4h,dense_4h_to_h],lora_dropout0.05,)modelget_peft_model(model,lora_config)model.print_trainable_parameters()# 输出trainable params: 3,814,784 || all params: 6,247,849,472 || trainable%: 0.061%# 3. 加载数据集datasetload_dataset(train.jsonl,tokenizer,max_length512)train_loaderDataLoader(dataset,batch_size1,shuffleTrue)# 4. DeepSpeed 配置ds_config{train_batch_size:16,# 实际 batch_size 1 * 梯度累积16 * 8卡 128gradient_accumulation_steps:16,fp16:{enabled:True},zero_optimization:{stage:2,# ZeRO-2分片优化器状态和梯度offload_optimizer:{device:cpu},# 优化器状态卸到 CPU},}# 5. 初始化 DeepSpeedmodel_engine,optimizer,_,_deepspeed.initialize(modelmodel,configds_config,)# 6. 训练循环model_engine.train()forstep,batchinenumerate(train_loader):# 前向outputsmodel_engine(**batch)lossoutputs.loss# 反向model_engine.backward(loss)model_engine.step()ifstep%1000:print(fStep{step}| Loss:{loss.item():.3f})# 7. 保存 LoRA 权重model_engine.save_checkpoint(./output/glm-lora-chat)print(训练完成)if__name____main__:main()合并权重LoRA 训练完后权重是分两块的原始权重冻结和 LoRA 补丁可学习。推理时要合并# 合并 LoRA 权重到原始模型frompeftimportPeftModelfromtransformersimportAutoModel# 加载原始模型base_modelAutoModel.from_pretrained(THUDM/chatglm3-6b)# 加载 LoRA 权重modelPeftModel.from_pretrained(base_model,./output/glm-lora-chat)# 合并LoRA 补丁加到原始权重上merged_modelmodel.merge_and_unload()# 保存合并后的模型merged_model.save_pretrained(./output/glm-lora-merged)合并后的模型就是一个标准的 GLM 模型可以直接用 transformers 加载推理fromtransformersimportAutoModel,AutoTokenizer modelAutoModel.from_pretrained(./output/glm-lora-merged)tokenizerAutoTokenizer.from_pretrained(./output/glm-lora-merged)input_text你们公司的客服工作时间是什么input_idstokenizer.encode(input_text,return_tensorspt)output_idsmodel.generate(input_ids,max_new_tokens100)print(tokenizer.decode(output_ids[0]))性能数据用 GLM-3-6B 在昇腾 9108 卡上做 LoRA 微调的性能数据配置显存占用/GB训练速度可训练参数全量微调568,500 tokens/s6.2BLoRA (r8)1812,500 tokens/s1.9MLoRA (r16)2012,200 tokens/s3.8MLoRA (r64)2411,800 tokens/s15.2M数据说明LoRA 把显存占用从 56GB 降到了 18-24GB训练速度反而更快因为显存压力小了batch 可以更大。可训练参数只有原始模型的 0.03%-0.24%但效果跟全量微调差不多。训练配方的核心不是代码本身是它帮你配好的超参和通信策略。DeepSpeed ZeRO-2 的分片策略、LoRA 的秩选择、学习率调度……这些参数花了很多时间调优。直接拿来用能省很多时间。仓库地址https://atomgit.com/cann/cann-recipes-train