ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

单卡微调大模型实战:基于MindSpore的LoRA微调与推理部署全流程

单卡微调大模型实战:基于MindSpore的LoRA微调与推理部署全流程 1. 为什么单卡微调值得认真对待1.1 从“跑不起来”到“跑得动”的认知转变很多人第一次接触大模型微调脑子里浮现的画面是机房里的八卡A100集群觉得这事跟自己手头的设备没什么关系。我一开始也是这么想的直到有一次手头只有一个任务让一个几B参数的小模型学会按照特定格式输出结构化的设备巡检报告。租云卡按小时计费跑一轮实验调参加上数据清洗账单很快就上去了。后来我换了个思路把模型规模压下来把数据质量提上去用一张消费级显卡硬是把这件事做成了。单卡微调的核心逻辑不是“用更少的卡做同样的事”而是“用更少的卡做更聚焦的事”。你不需要让模型学会所有知识只需要让它在某个垂直场景里表现稳定。这个认知转变很关键它决定了你后面所有的技术选型模型选多大、数据怎么构造、训练参数怎么设、推理怎么部署。昇思 MindSpore 在这条路径上有它自己的优势。它的图算融合和自动并行机制在单卡场景下能把显存利用得比较充分而且 MindSpore 的mindformers套件对大模型微调做了比较完整的封装很多底层细节不用自己从头写。这篇文章就是把我自己从零搭建一套“单卡微调加推理”流程的完整过程拆开来讲包括环境怎么配、数据怎么处理、参数怎么调、推理怎么接以及中间踩过的那些坑。适合读这篇内容的人我大致分三类一是手头只有一张卡但想跑通全流程的开发者二是想用 MindSpore 做垂直场景模型定制的算法工程师三是对大模型微调感兴趣、想找一个能实际动手跑起来的最小闭环的学生或爱好者。不管你是哪一类下面的内容都可以直接照着操作我会把每一步的意图和参数依据都讲清楚。1.2 单卡微调的现实边界在哪里先把预期管理做好。单卡微调不是万能的它有明确的边界。以目前主流的消费级显卡为例24GB 显存是一个比较常见的档位。在这个显存条件下全量微调一个 7B 参数的模型基本不现实因为光是模型权重加优化器状态就远超显存容量。但如果你用 LoRA 或者 QLoRA 这类参数高效微调方法把可训练参数降到原来的百分之一甚至更低再配合梯度检查点和混合精度训练7B 级别的模型是可以跑起来的。这里有一个简单的估算方法。假设模型有 N 个参数用 FP16 存储权重需要 2N 字节用 FP32 存储优化器状态以 Adam 为例需要 8N 字节再加上梯度 2N 字节全量微调的总显存需求大约是 12N 字节。7B 参数就是 84GB 左右单卡显然放不下。换成 LoRA 之后可训练参数可能只有几十M优化器状态和梯度都只针对这部分参数显存占用就降到了模型权重加少量额外开销的水平。7B 模型 FP16 权重约 14GB加上激活值和中间计算24GB 显存刚好够用但 batch size 只能设得很小通常就是 1 到 2。所以单卡微调的关键词是“参数高效”和“显存精打细算”。你需要在模型规模、序列长度、batch size、梯度累积步数这几个变量之间找到一个平衡点。这个平衡点没有标准答案取决于你的具体任务和硬件条件但后面的章节我会给出一个经过实测的参考配置你可以在此基础上调整。2. 环境搭建与工具链选型2.1 昇思 MindSpore 安装的版本选择MindSpore 的安装方式有好几种pip 安装、conda 安装、源码编译。对于单卡微调这个场景我建议直接用 pip 安装官方发布的版本省去编译的麻烦。版本选择上要和你显卡的 CUDA 版本匹配。MindSpore 目前对 CUDA 11.6 和 11.8 的支持比较成熟如果你用的是较新的显卡驱动建议选 CUDA 11.8 对应的版本。安装命令大致是这样的pip install mindspore2.3.0 -i https://pypi.tuna.tsinghua.edu.cn/simple这里指定版本号是有必要的因为mindformers套件对 MindSpore 的版本有依赖关系版本不匹配会出现一些奇怪的报错。我试过用最新版的 MindSpore 配旧版的 mindformers结果在加载模型权重的时候一直报 shape 不匹配排查了半天才发现是版本问题。安装完之后用下面这段代码验证一下import mindspore as ms print(ms.__version__) print(ms.context.get_context(device_target))如果输出是GPU并且版本号和你安装的一致说明基础环境没问题。如果输出是CPU那说明 CUDA 没配好需要检查显卡驱动和 CUDA 环境变量。注意MindSpore 的 GPU 版本对驱动版本有最低要求安装前先确认你的驱动版本是否满足。驱动太旧的话即使 CUDA 装对了MindSpore 也可能识别不到显卡。2.2 mindformers 套件的获取与配置mindformers是 MindSpore 生态里专门做大模型训练和推理的套件里面已经封装好了常见模型的网络结构、数据预处理、训练流程和推理接口。单卡微调用这个套件可以省掉大量重复代码。获取方式是从代码仓库克隆对应版本的源码git clone https://gitee.com/mindspore/mindformers.git cd mindformers git checkout r1.3 pip install -e .这里git checkout到对应的发布分支很重要主分支的代码可能还在开发中接口不稳定。pip install -e .是开发模式安装这样你修改套件里的配置文件后不用重新安装就能生效。安装完成后进入research或者examples目录找到对应模型的微调脚本。以常见的几个模型为例目录结构大概是这样的mindformers/ research/ llama2/ run_llama2.py llama2_config.yaml qwen/ run_qwen.py qwen_config.yaml每个模型目录下都有一个训练入口脚本和一个配置文件。配置文件里定义了模型结构、训练超参、数据路径、并行策略等所有关键信息。单卡微调的核心工作就是改这个配置文件。2.3 依赖库的版本对齐除了 MindSpore 和 mindformers 本身还有一些依赖库需要注意版本。比如numpy的版本不能太高某些版本和 MindSpore 的算子有兼容性问题。sentencepiece是处理 tokenizer 用的版本太旧会导致分词结果异常。protobuf的版本也要注意太新的版本可能导致模型权重加载失败。我整理了一个经过实测的依赖版本组合可以作为参考依赖库推荐版本说明mindspore2.3.0GPU 版本CUDA 11.8mindformersr1.3对应 MindSpore 2.3numpy1.24.4避免 2.x 版本的兼容问题sentencepiece0.1.99分词器依赖protobuf3.20.3权重加载依赖transformers4.35.0部分模型配置参考这些版本不是绝对的但如果你遇到一些莫名其妙的报错先检查一下这几个库的版本是否在这个范围内。3. 数据准备与格式转换3.1 微调数据的构造原则单卡微调的数据量不需要很大但质量要求很高。我见过很多人拿几万条脏数据去微调结果模型学了一堆噪声输出变得乱七八糟。对于垂直场景的微调几百到几千条高质量数据往往就能看到明显效果。数据构造的核心原则是“输入输出对齐”。你要让模型学会的是“给定这样的输入输出那样的结果”所以每一条数据都必须是一个完整的输入输出对。以设备巡检报告为例输入是设备运行参数和巡检记录输出是结构化的报告文本。数据格式可以是 JSONL每行一个样本{instruction: 根据以下设备参数生成巡检报告, input: 设备编号A-102温度78℃振动4.2mm/s电流12.5A, output: 巡检报告设备A-102运行状态异常温度偏高建议检查散热系统。}这种 instruction-input-output 的三段式结构是微调数据最常见的格式。instruction 是任务描述input 是具体输入output 是期望输出。在训练时通常只对 output 部分计算损失instruction 和 input 作为条件输入。数据质量比数量重要得多。我自己的经验是500 条精心构造的数据效果往往好过 5000 条随手收集的数据。构造数据时要注意几点输出格式要统一不要一会儿用中文标点一会儿用英文标点输出内容要准确不要有事实性错误输入要有一定的多样性覆盖不同的参数组合和场景。3.2 数据格式转换与 tokenizer 适配原始数据准备好之后需要转换成 MindSpore 能读取的格式。mindformers 支持多种数据格式最常用的是 MindRecord 和 TFRecord。对于单卡微调我建议先用一个简单的文本格式然后在训练脚本里做实时 tokenize这样调试起来比较方便。具体做法是准备一个 JSONL 文件然后在配置文件里指定数据路径和预处理方式。mindformers 的配置文件里有一个data_loader部分可以这样配置data_loader: type: CommonDataLoader dataset_dir: ./data dataset_name: finetune_data batch_size: 1 max_seq_length: 1024 tokenizer: type: LlamaTokenizer vocab_file: ./tokenizer.model这里max_seq_length设成 1024 是一个比较稳妥的选择。设得太长会显著增加显存占用设得太短又会截断重要信息。你可以先统计一下自己数据里输入输出对的长度分布取一个覆盖 95% 样本的值。tokenizer 的选择要和模型匹配。如果你用的是 Llama 架构的模型就用 LlamaTokenizer如果是 Qwen 架构就用对应的 QwenTokenizer。用错 tokenizer 会导致分词结果完全错误模型训练出来的效果会非常差。提示在正式训练之前先写一个小脚本把几条数据过一遍 tokenizer看看分词结果是否符合预期。特别是中文数据有些 tokenizer 对中文的分词粒度很粗一个词可能被拆成好几个 token这会显著增加序列长度。3.3 数据集的划分与验证集的作用单卡微调的数据量不大但仍然需要划分训练集和验证集。我通常按 9:1 的比例划分如果数据量特别少比如只有两三百条可以按 8:2 划分。验证集的作用不是评估模型泛化能力而是监控训练过程是否过拟合。在配置文件里可以这样指定train_dataset: ./data/train.jsonl eval_dataset: ./data/eval.jsonl训练过程中每隔一定步数跑一次验证集观察 loss 的变化。如果训练 loss 持续下降但验证 loss 开始上升说明模型开始过拟合了这时候应该停止训练或者减小学习率。我自己的经验是单卡微调很容易过拟合因为数据量少、模型参数量大。所以验证集的监控非常重要不要等到训练完了才发现模型已经过拟合了。通常训练 3 到 5 个 epoch 就能看到比较明显的效果再多就容易过拟合。4. 微调配置与参数调优4.1 LoRA 微调的原理与参数设置LoRA 的全称是 Low-Rank Adaptation中文叫低秩适配。它的核心思想是在模型的某些权重矩阵旁边加一个低秩的旁路训练时只更新这个旁路原始权重保持冻结。这样做的好处是可训练参数量大幅减少显存占用和训练时间都显著降低。在 mindformers 里配置 LoRA 微调需要在配置文件里加上pet_config部分pet_config: pet_type: lora lora_rank: 8 lora_alpha: 16 lora_dropout: 0.05 target_modules: [q_proj, v_proj]这里几个参数的含义需要解释一下。lora_rank是低秩矩阵的秩决定了旁路的容量。秩越大可训练参数越多拟合能力越强但过拟合风险也越高。对于单卡微调我建议从 8 开始试如果效果不够再增加到 16 或 32。lora_alpha是缩放因子通常设为 rank 的两倍。lora_dropout是 dropout 概率用来防止过拟合数据量少的时候可以设大一点比如 0.1。target_modules指定在哪些权重矩阵上加 LoRA。最常见的做法是在 attention 的 query 和 value 投影矩阵上加也就是q_proj和v_proj。有些实践也会在 key 和 output 投影上加但那样可训练参数会增多。对于单卡场景我建议先只加 q 和 v效果不够再扩展。LoRA 的一个关键优势是推理时可以合并回原始权重。训练完成后把 LoRA 旁路的权重加到原始权重上推理时就完全看不出和全量微调的区别也不会增加推理延迟。mindformers 提供了合并权重的脚本后面推理部分会讲到。4.2 训练超参的选取与显存优化训练超参里最关键的几个是学习率、batch size、梯度累积步数和训练轮数。单卡微调的学习率通常比全量微调大一些因为可训练参数少需要更大的步长才能有效更新。我常用的配置是学习率 1e-4 到 5e-4配合 cosine 衰减策略。batch size 受显存限制24GB 显存跑 7B 模型 LoRA 微调batch size 通常只能设 1 或 2。为了保持训练稳定性需要用梯度累积来模拟更大的 batch。比如设gradient_accumulation_steps: 8实际等效 batch size 就是 8。learning_rate: 2e-4 lr_schedule: cosine warmup_steps: 100 batch_size: 1 gradient_accumulation_steps: 8 epochs: 5 optimizer: adamw weight_decay: 0.01显存优化方面有几个开关可以打开。梯度检查点gradient checkpointing用计算换显存可以把激活值的显存占用降低很多代价是训练速度慢一些。混合精度训练AMP用 FP16 或 BF16 存储激活值和梯度也能显著降低显存。这两个开关在配置文件里通常是这样的gradient_checkpointing: true amp_level: O2 compute_dtype: float16amp_level设为 O2 表示使用混合精度compute_dtype设为 float16 表示计算时用半精度。如果你的显卡支持 BF16也可以设成 bfloat16数值稳定性更好。我实测下来7B 模型在 24GB 显存上开启梯度检查点和混合精度batch size 设为 1序列长度 1024显存占用大约在 20GB 左右还有一定余量。如果序列长度增加到 2048显存就会比较紧张需要进一步降低 batch size 或者缩短序列长度。4.3 训练过程的监控与中断恢复训练启动之后需要监控 loss 曲线和显存占用。mindformers 默认会输出训练日志包括每步的 loss、学习率、耗时等信息。我习惯把日志重定向到文件方便后续分析python run_llama2.py --config llama2_config.yaml train.log 21 然后可以用tail -f train.log实时查看训练进度。重点关注几个指标loss 是否在下降、下降速度是否合理、有没有出现 NaN、显存占用是否稳定。如果 loss 突然变成 NaN通常是学习率太大或者数据里有异常样本需要检查数据和调整学习率。训练中断恢复也是单卡微调需要考虑的问题。因为训练时间可能比较长中途可能因为各种原因中断。mindformers 支持 checkpoint 保存和恢复在配置文件里设置保存间隔save_checkpoint_steps: 500 keep_checkpoint_max: 3这样每 500 步保存一次最多保留 3 个最新的 checkpoint。如果训练中断了可以从最新的 checkpoint 恢复python run_llama2.py --config llama2_config.yaml --load_checkpoint ./checkpoints/llama2_500.ckpt注意恢复训练时优化器状态和训练步数也需要恢复否则学习率调度会从头开始。mindformers 的 checkpoint 里包含了这些信息但需要确认配置文件里的load_checkpoint路径指向的是完整的 checkpoint 文件。5. 推理部署与效果验证5.1 LoRA 权重合并与模型导出训练完成后得到的是 LoRA 旁路的权重文件。这个文件不能直接用于推理需要先和原始模型权重合并。mindformers 提供了合并脚本通常在tools目录下python tools/merge_lora.py \ --base_model ./base_model.ckpt \ --lora_model ./lora_model.ckpt \ --output ./merged_model.ckpt合并的原理很简单就是把 LoRA 的旁路权重按缩放因子加到原始权重上。合并后的模型和全量微调的模型在结构上完全一致推理时不需要任何额外处理。合并完成后可以把模型导出成 MindIR 格式这样推理时加载更快也不依赖训练时的代码环境python tools/export.py \ --config ./config.yaml \ --checkpoint ./merged_model.ckpt \ --output ./exported_model导出的 MindIR 文件可以在推理引擎里直接加载适合部署到生产环境。5.2 推理脚本的编写与流式输出推理脚本的核心是加载模型、构造输入、生成输出。mindformers 提供了MindFormerPipeline接口封装了常见的推理流程from mindformers import pipeline pipe pipeline( tasktext_generation, modelllama2_7b, checkpoint./merged_model.ckpt, max_length512, do_sampleTrue, temperature0.7, top_k50, top_p0.9 ) result pipe(根据以下设备参数生成巡检报告设备编号A-102温度78℃) print(result)这里几个生成参数需要解释一下。temperature控制输出的随机性值越大输出越多样值越小输出越确定。对于结构化输出任务建议设小一点比如 0.3 到 0.7。top_k和top_p是采样策略top_k表示只从概率最高的 k 个 token 里采样top_p表示从累积概率达到 p 的 token 集合里采样。这两个参数通常设一个就行我习惯用top_p。流式输出对于交互式应用很重要可以让用户看到逐字生成的效果。mindformers 的 pipeline 支持流式输出for token in pipe.stream(根据以下设备参数生成巡检报告设备编号A-102温度78℃): print(token, end, flushTrue)流式输出的实现原理是每次生成一个 token 就返回而不是等整个序列生成完再返回。这对于长文本生成场景可以显著提升用户体验。5.3 推理效果的评估与迭代推理效果评估不能只看一两个例子需要构造一个测试集覆盖不同的输入场景。我通常从验证集里抽 20 到 30 条数据人工检查输出是否符合预期。评估的维度包括格式是否正确、内容是否准确、有没有重复或遗漏、语言是否流畅。如果发现效果不理想可以从几个方向排查。首先是数据质量检查训练数据里有没有错误的标注或者格式不一致的情况。其次是训练轮数可能训练不够或者过拟合了。再次是 LoRA 的秩可能秩太小导致拟合能力不足。最后是推理参数调整 temperature 和 top_p 可能会改善输出质量。我自己的经验是单卡微调的效果很大程度上取决于数据质量。同样的模型和参数用精心构造的 500 条数据训练效果明显好过用随手收集的 2000 条数据。所以在数据构造上多花时间是值得的。6. 常见问题与排查技巧6.1 显存不足的排查与解决显存不足是单卡微调最常见的问题。报错信息通常是Out of memory或者CUDA out of memory。排查思路是从大到小逐个检查显存占用项。首先检查模型权重的显存占用。7B 模型 FP16 权重约 14GB如果加载模型后就占用了超过 20GB说明可能加载了 FP32 权重或者有额外的缓存。可以在配置文件里确认compute_dtype和param_init_type是否设成了 float16。其次检查激活值的显存占用。激活值和序列长度、batch size 成正比。如果序列长度设成了 2048可以降到 1024 试试。如果 batch size 设成了 2可以降到 1。然后检查优化器状态的显存占用。LoRA 微调时优化器状态只针对可训练参数占用很小。但如果误开了全量微调优化器状态会占用大量显存。最后检查有没有内存泄漏。MindSpore 在反复加载模型时可能会有显存碎片可以在脚本开头设置ms.set_context(mempool_block_size1GB)来优化显存分配。我整理了一个显存不足的排查清单排查项检查方法解决方法模型权重精度查看配置文件 compute_dtype改为 float16序列长度查看 max_seq_length降低到 1024 或 512batch size查看 batch_size降低到 1梯度检查点查看 gradient_checkpointing设为 true混合精度查看 amp_level设为 O2显存碎片观察多次运行后的显存设置 mempool_block_size6.2 训练 loss 异常的排查Loss 异常通常表现为 loss 不下降、loss 震荡、loss 变成 NaN。每种情况的原因不同排查方法也不同。Loss 不下降可能是学习率太小、数据有问题、或者模型没有正确加载预训练权重。先检查学习率是否在合理范围然后检查数据里有没有大量重复样本或者错误标注。最后确认预训练权重是否加载成功可以打印模型的部分权重看看是否和预训练权重一致。Loss 震荡通常是学习率太大或者 batch size 太小。可以降低学习率或者增大梯度累积步数来等效增大 batch size。如果数据里有长度差异很大的样本也可能导致 loss 震荡可以考虑按长度分桶。Loss 变成 NaN通常是数值溢出。检查学习率是否太大检查数据里有没有异常值检查混合精度的设置是否合适。如果用的是 FP16可以换成 BF16 试试BF16 的数值范围更大不容易溢出。提示训练初期 loss 在 2 到 3 左右是正常的随着训练进行会逐渐下降到 1 以下。如果 loss 一直在 5 以上不下降说明模型可能没有学到东西需要检查数据和配置。6.3 推理输出不符合预期的排查推理输出不符合预期可能表现为格式错误、内容重复、答非所问。排查思路是从训练和推理两个环节分别检查。格式错误通常是训练数据里格式不统一导致的。检查训练数据的输出部分确保所有样本的格式一致。如果训练数据里有的用中文标点有的用英文标点模型就会学混。内容重复通常是推理参数设置不当。降低 temperature减小 top_k 或 top_p可以减少重复。如果重复很严重可能是模型过拟合了需要减少训练轮数或者增加数据量。答非所问通常是训练数据里输入输出不对齐。检查数据里有没有输入和输出不匹配的样本。另外如果 instruction 部分写得太模糊模型可能无法理解任务意图需要把 instruction 写得更明确。我自己的经验是推理效果不好时先回头检查训练数据十有八九是数据的问题。数据质量上去了推理效果自然就好。7. 单卡微调的扩展思路7.1 从单卡到多卡的平滑过渡单卡微调跑通之后如果任务需要更大的模型或者更多的数据可以考虑扩展到多卡。MindSpore 的自动并行机制让单卡到多卡的过渡比较平滑主要改动在配置文件的并行策略部分。单卡配置里通常是parallel_config: data_parallel: 1, model_parallel: 1。扩展到多卡时可以增加数据并行度或者模型并行度。数据并行是把 batch 切分到多张卡上每张卡持有完整的模型副本。模型并行是把模型切分到多张卡上每张卡持有部分模型。对于单卡微调的场景如果只是数据量增大优先考虑数据并行。如果模型太大单卡放不下才考虑模型并行。MindSpore 的自动并行可以根据配置自动推导切分策略不需要手动指定每一层的切分方式。7.2 推理性能的进一步优化单卡推理的性能优化有几个方向。一是模型量化把 FP16 权重转成 INT8推理速度可以提升一倍左右精度损失通常在可接受范围内。MindSpore 提供了量化工具可以对导出的 MindIR 模型做量化。二是算子融合MindSpore 的图算融合机制会自动把一些相邻算子合并减少 kernel 启动开销。可以在推理时开启ms.set_context(graph_kernel_flags--enable_graph_kernel1)来启用。三是批处理推理如果有多个请求同时到达可以把它们拼成一个 batch 一起推理提高显卡利用率。但要注意 batch 太大会增加延迟需要根据实际场景权衡。7.3 持续迭代与数据飞轮模型上线之后不是终点而是新的起点。用户的真实输入会暴露出训练数据没有覆盖的场景这些场景可以作为新的训练数据来源。把线上推理的输入输出记录下来人工筛选出高质量的部分加入训练集重新微调模型效果会逐步提升。这个循环就是数据飞轮。单卡微调的好处是迭代成本低一次微调可能只需要几个小时可以快速验证新数据的效果。我自己的做法是每周收集一批线上数据筛选后加入训练集每月做一次重新微调。这样模型能持续适应新的场景保持较好的效果。最后再分享一个小技巧在构造训练数据时可以故意加入一些边界情况的样本比如输入为空、输入特别长、输入包含特殊字符等。这样模型在面对异常输入时表现会更稳定不会轻易输出乱七八糟的内容。这个技巧在实际部署中非常有用能减少很多线上问题。
返回列表