[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-1022":3,"consumer-news-interaction-1022":41,"consumer-news-related-1022":44},{"detail":4,"item":36},{"card":5,"schemaVersion":23,"fields":24,"content":30},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":15,"href":16,"sourceName":12,"meta":17,"metrics":20,"tags":21,"resolved":22},"NEWS_ARTICLE:1022","news","NEWS_ARTICLE",1022,"资讯","千问大模型二次LoRA‑SFT指令微调指南","博客园","大模型微调是实现模型领域定制的核心方案。本文基于 Ubuntu 22.04 服务器环境，依托 NVIDIA RTX 家用高性能显卡，选用千问 Qwen3.5‑0.8B‑Instruct 指令模型，完整演示二次 LoRA‑SFT 微调全流程，包含环境搭建、模型选型、ChatML 数据集制作校验、LoR","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1379525\u002F202609\u002F1379525-20260906165616374-885820108.jpg","","\u002Fnews\u002F1022",[18,19],"2026","人工智能",{},[19],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":25,"categoryName":19,"summary":13,"description":13,"publishTime":26,"updateTime":27,"sourceUrl":28,"language":29},"lyshark","2026-09-06T18:30","2026-09-11T15:22:03","https:\u002F\u002Fwww.cnblogs.com\u002FLyShark\u002Fp\u002F22864081","中文",{"format":31,"policy":32,"normalized":22,"html":33,"text":34,"wordCount":35,"hasBody":22},"HTML","NEWS_CONTENT_V1","\u003Cp>大模型微调是实现模型领域定制的核心方案。本文基于 Ubuntu 22.04 服务器环境，依托 NVIDIA RTX 家用高性能显卡，选用千问 Qwen3.5‑0.8B‑Instruct 指令模型，完整演示二次 LoRA‑SFT 微调全流程，包含环境搭建、模型选型、ChatML 数据集制作校验、LoRA 参数配置、SFT 训练、效果验证、权重合并，附带可直接运行的工程脚本，便于开发者快速实现千问模型轻量化定制与上线部署。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F1379525\u002F202609\u002F1379525-20260906165616374-885820108.jpg?w=720&amp;quality=65&amp;strip=all\" alt=\"logo_qwen\">\u003C\u002Fp>\n\u003Cp>在开展Qwen模型微调实操前，需提前配置好服务器软硬件运行环境，本文所有实操流程均基于以下稳定运行的本机环境，读者可直接参考对齐配置，适配复现：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>操作系统：Ubuntu 22.04.5 LTS (GNU\u002FLinux 5.15.0-187-generic x86_64)\u003C\u002Fli>\n \u003Cli>框架版本：torch 2.13.0 + CUDA 12.8.1 + Python 3.10.12\u003C\u002Fli>\n \u003Cli>显卡驱动：NVIDIA ≥ 570.133.07\u003C\u002Fli>\n \u003Cli>显卡类型：NVIDIA RTX 4090 24GB GPU\u003C\u002Fli>\n \u003Cli>CPU核心：INTEL(R) XEON(R) GOLD 6530\u003C\u002Fli>\n \u003Cli>内存：64GB DDR5\u003C\u002Fli>\n \u003Cli>存储：50GB(系统盘)+100GB(数据盘)\u003C\u002Fli>\n\u003C\u002Ful>\n基础知识\n\u003Ch3>两种训练\u003C\u002Fh3>\n\u003Cp>大模型行业落地场景中，通用预训练模型难以适配垂直领域专属知识、定制化对话风格与专属业务指令需求，模型微调成为轻量化、低成本实现模型能力定制的核心手段。\u003C\u002Fp>\n\u003Cp>模型的微调训练包含两种主流模式：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cp>\u003Cstrong>第一种：\u003C\u002Fstrong>基于Base预训练基座模型开展从零SFT微调，该类模型仅完成通用预训练，无官方指令对齐能力，无法原生理解对话指令，需人工构建对话格式、手动拼接对话文本及监督数据集，从零学习指令跟随与对话能力，多用于全新模型定制与二次预训练任务，该模式消耗算力大，一般不会使用此方式微调。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>第二种：\u003C\u002Fstrong>指令模型二次 LoRA‑SFT 微调，也是本文的核心内容，千问 Instruct 指令模型由 Base 基座经过官方 SFT 监督微调、DPO\u002FRLHF 偏好对齐得到，原生具备指令理解与多轮对话能力。在此成熟对齐模型基础上开展二次 LoRA‑SFT 微调，可在保留模型通用知识与基础对话能力的前提下，低成本注入领域专属能力或知识，规避基座从零训练的数据门槛与对齐成本，是工程落地与轻量化模型定制的最优方案。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>进行微调前需要判别模型底座，模型名称带有\u003Ccode>‑Base\u003C\u002Fcode>后缀的为预训练裸基座；模型目录中包含\u003Ccode>chat_template.jinja\u003C\u002Fcode>文件，则说明是 Instruct 指令模型，该类模型已经完成 SFT、DPO、RLHF 等指令对齐流程。现实中原始预训练基模较少对外开源，公开可获取的大多是经过完整后训练的成品权重。\u003C\u002Fp>\n\u003Ch3>LoRA\u002FQLoRA\u003C\u002Fh3>\n\u003Cp>LoRA 与 QLoRA 是目前主流的参数高效微调方法。LoRA 通过冻结主干模型，仅训练注意力层的低秩适配器，在几乎不损失模型性能的前提下降低训练参数量，适合显存条件较好的硬件环境。QLoRA 在 LoRA 基础上引入 4\u002F8 比特权重量化，将主干模型以量化形式加载，以此来压缩显存占用，使大模型微调可以在消费级显卡上完成，但会引入少量量化噪声，可能对最终效果带来轻微影响。\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>项目\u003C\u002Fth>\n   \u003Cth>LoRA\u003C\u002Fth>\n   \u003Cth>QLoRA\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>核心机制\u003C\u002Ftd>\n   \u003Ctd>低秩适配器\u003C\u002Ftd>\n   \u003Ctd>低秩适配器 + INT4\u002FINT8 权重量化\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>模型主干精度\u003C\u002Ftd>\n   \u003Ctd>FP16\u002FBF16\u003C\u002Ftd>\n   \u003Ctd>INT4\u002FINT8 量化冻结\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>显存消耗\u003C\u002Ftd>\n   \u003Ctd>较低\u003C\u002Ftd>\n   \u003Ctd>更低\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>精度损失\u003C\u002Ftd>\n   \u003Ctd>几乎无\u003C\u002Ftd>\n   \u003Ctd>存在轻微量化损失\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>工程复杂度\u003C\u002Ftd>\n   \u003Ctd>低，稳定通用\u003C\u002Ftd>\n   \u003Ctd>较高，存在量化适配问题\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>适用场景\u003C\u002Ftd>\n   \u003Ctd>显存条件较好，8B‑14B，追求微调效果\u003C\u002Ftd>\n   \u003Ctd>显存受限，超大模型 (27B+)，消费级显卡\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>LoRA 与 QLoRA 的选择主要依据硬件设备显存条件确定。对于 8B 规模模型，当显卡显存大于 24GB 时，优先采用 LoRA 微调；针对 14B、27B 等更大规模模型或显存资源受限场景，则选用 QLoRA 并开启 4 比特量化加载以降低显存压力。本文选用体量较小的 Qwen3.5‑0.8B‑Instruct 作为实验对象，模型参数量小，便于流程演示，因此直接采用 LoRA 开展二次 SFT 指令微调。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>完整实验链路为：环境准备 → 数据集准备(train.json) → 加载基座模型(Qwen3.5‑0.8B‑Instruct) → LoRA 参数配置 → SFTTrainer 执行微调训练 → 本地推理效果验证 → LoRA 适配器与主模型权重合并导出\u003C\u002Fli>\n\u003C\u002Ful>\n微调实验\n\u003Ch3>环境安装\u003C\u002Fh3>\n\u003Cp>1、创建独立 Python 虚拟环境，隔离项目依赖，避免和系统 Python 包冲突。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>root@localhost:~\u002F# sudo apt install -y python3-full python3-venv tmux git tree\nroot@localhost:~\u002F# sudo python3 -m venv ~\u002Fmyvenv\nroot@localhost:~\u002F# source ~\u002Fmyvenv\u002Fbin\u002Factivate\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>2、激活进入虚拟环境，使用腾讯云镜像源加速深度学习、微调相关全套依赖包。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>root@localhost:~\u002F# pip3 install -i https:\u002F\u002Fmirrors.cloud.tencent.com\u002Fpypi\u002Fsimple\u002F torch torchvision trl datasets peft accelerate bitsandbytes wandb transformers sentencepiece huggingface_hub protobuf modelscope\nroot@localhost:~\u002F# pip3 list\nPackage                Version\n---------------------- ------------\naccelerate             1.14.0\naiohappyeyeballs       2.7.1\naiohttp                3.14.3\naiosignal              1.4.0\nannotated-doc          0.0.5\nannotated-types        0.8.0\nanyio                  4.15.1\nasync-timeout          5.0.1\nattrs                  26.1.0\nbitsandbytes           0.50.2\ncertifi                2026.7.22\ncharset-normalizer     3.5.1\nclick                  8.5.0\ncuda-bindings          13.3.1\ncuda-pathfinder        1.8.1\ncuda-toolkit           13.0.3.0\ndatasets               5.0.1\ndill                   0.4.1\nexceptiongroup         1.3.1\nfilelock               3.32.5\nfrozenlist             1.8.0\nfsspec                 2026.6.0\nh11                    0.16.0\nhf-xet                 1.6.0\nhttpcore               1.0.9\nhttpx                  0.28.1\nhuggingface_hub        1.30.0\nidna                   3.19\nJinja2                 3.1.6\nmarkdown-it-py         4.2.0\nMarkupSafe             3.0.3\nmdurl                  0.1.2\nmpmath                 1.3.0\nmultidict              6.7.1\nmultiprocess           0.70.19\nnetworkx               3.4.2\nnumpy                  2.2.6\nnvidia-cublas          13.1.1.3\nnvidia-cuda-cupti      13.0.85\nnvidia-cuda-nvrtc      13.0.88\nnvidia-cuda-runtime    13.0.96\nnvidia-cudnn-cu13      9.24.0.43\nnvidia-cufft           12.0.0.61\nnvidia-cufile          1.15.1.6\nnvidia-curand          10.4.0.35\nnvidia-cusolver        12.0.4.66\nnvidia-cusparse        12.6.3.3\nnvidia-cusparselt-cu13 0.8.1\nnvidia-nccl-cu13       2.30.7\nnvidia-nvjitlink       13.3.33\nnvidia-nvshmem-cu13    3.4.5\nnvidia-nvtx            13.0.85\nopentelemetry-api      1.44.0\npackaging              26.3\npandas                 2.3.3\npeft                   0.20.0\npillow                 12.3.0\npip                    22.0.2\nplatformdirs           4.11.7\npropcache              0.5.2\nprotobuf               7.36.1\npsutil                 7.2.2\npyarrow                25.0.1\npydantic               2.13.5\npydantic_core          2.46.5\nPygments               2.21.0\npython-dateutil        2.9.0.post0\npytz                   2026.3.post1\nPyYAML                 6.0.3\nregex                  2026.9.3\nrequests               2.34.2\nrich                   15.0.0\nsafetensors            0.8.0\nsentry-sdk             2.68.1\nsetuptools             84.0.0\nshellingham            1.5.4\nsix                    1.17.0\nsympy                  1.14.0\ntokenizers             0.23.2\ntorch                  2.14.0\ntorchaudio             2.11.0\ntorchvision            0.29.0\ntqdm                   4.70.0\ntransformers           5.16.1\ntriton                 3.8.0\ntrl                    1.12.0\ntyper                  0.27.2\ntyping_extensions      4.16.0\ntyping-inspection      0.4.4\ntzdata                 2026.3\nurllib3                2.7.0\nwandb                  0.29.0\nxxhash                 4.0.1\nyarl                   1.24.5\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>下载底座模型\u003C\u002Fh3>\n\u003Cp>使用 modelscope 下载 Qwen\u002FQwen3.5‑0.8B‑Instruct 已经预 SFT 完成的底座模型，该模型原生支持 ChatML 对话模板，不需要自己修改 tokenizer，适合直接二次 LoRA 微调；不要手动复制网页，使用 modelscope 下载保证文件完整，避免文件缺失导致训练报错。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fwww.modelscope.cn\u002Fmodels\u002Ficyfenix\u002FQwen3.5-0.8B-Instruct\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">https:\u002F\u002Fwww.modelscope.cn\u002Fmodels\u002Ficyfenix\u002FQwen3.5-0.8B-Instruct\u003C\u002Fa>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>root@localhost:~\u002F# mkdir data\nroot@localhost:~\u002F# cd data\nroot@localhost:~\u002F# modelscope download --model icyfenix\u002FQwen3.5-0.8B-Instruct --local_dir .\u002Fqwen3.5‑0.8B‑Instruct\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>准备 SFT 微调数据集\u003C\u002Fh3>\n\u003Cp>采用 Qwen 标准 ChatML 格式，json 数组格式，每一条样本包含\u003Ccode>conversation\u003C\u002Fcode>数组，数组内是多轮对话，角色仅允许\u003Ccode>system\u002Fuser\u002Fassistant\u003C\u002Fcode>，文件保存到\u003Ccode>\u002Froot\u002Fdata\u002Ftrain.json\u003C\u002Fcode>中。\u003C\u002Fp>\n\u003Cp>此处为小样本演示数据集，适合跑通流程；真实业务建议扩充到几百～几千条，太少容易过拟合。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>[\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"什么是大模型LoRA微调？\"},\n            {\"role\":\"assistant\",\"content\":\"LoRA是一种轻量微调技术，冻结大模型主干权重，只训练少量低秩矩阵，不需要全量更新模型参数，显存消耗低，训练速度快，产出的LoRA适配器体积很小，可以挂载在基础模型上使用。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"LoRA和QLoRA有什么区别？\"},\n            {\"role\":\"assistant\",\"content\":\"LoRA主干模型使用FP16\u002FBF16精度加载，主干不量化；QLoRA把主干模型4bit量化，进一步降低显存开销，会带来微小的精度损失。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"SFT监督微调是什么？\"},\n            {\"role\":\"assistant\",\"content\":\"SFT即监督微调，使用高质量问答对话数据集，让大模型学习期望的输出格式、回答风格、领域知识，是对齐大模型的基础步骤。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"微调为什么会出现灾难性遗忘？\"},\n            {\"role\":\"assistant\",\"content\":\"当训练数据集单一，模型过度学习新数据，丢失原本基础能力，就会发生灾难性遗忘。解决办法是混入通用对话数据，控制训练步数，避免过拟合。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"PEFT是什么？\"},\n            {\"role\":\"assistant\",\"content\":\"PEFT是HuggingFace的参数高效微调库，包含LoRA、IA3等多种算法，只训练一小部分参数，不用完整保存整个大模型。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"如何判断微调过拟合？\"},\n            {\"role\":\"assistant\",\"content\":\"观察eval验证loss，loss不再下降甚至上升；模型在训练集问题回答很好，陌生问题表现很差；输出重复、背诵训练集内容，就是过拟合。可以减少训练步数，增大数据集多样性。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"vLLM可以直接加载LoRA适配器吗？\"},\n            {\"role\":\"assistant\",\"content\":\"原生PEFT标准LoRA适配器，新版vLLM支持直接加载，不需要手动合并权重；Unsloth的bnb‑4bit的LoRA不能直接加载，必须合并导出完整权重。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"system\",\"content\":\"你是大模型微调助手，回答简洁专业\"},\n            {\"role\":\"user\",\"content\":\"微调需要多少样本？\"},\n            {\"role\":\"assistant\",\"content\":\"风格迁移：200‑1000条；垂直领域知识增强：2000‑10000条；样本过少极易过拟合。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"训练的时候max_steps和epoch选哪个？\"},\n            {\"role\":\"assistant\",\"content\":\"小数据集优先max_steps，防止epoch重复训练造成过拟合；大数据集可以使用num_train_epochs，一般1‑3轮。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"LoRA的r参数代表什么？\"},\n            {\"role\":\"assistant\",\"content\":\"r是LoRA秩，r越大可训练参数量越大；常用8、16、32；r太小表达能力不足，r太大容易过拟合。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"梯度累积的作用是什么？\"},\n            {\"role\":\"assistant\",\"content\":\"显存不足调小batch_size，通过gradient_accumulation_steps做梯度累积，模拟更大的batch，不增加显存占用。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"bf16和fp16区别？\"},\n            {\"role\":\"assistant\",\"content\":\"bf16指数范围更大，适合大模型训练；30系及以上显卡支持bf16；20系及更早显卡只能用fp16。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"微调数据集一定要用apply_chat_template吗？\"},\n            {\"role\":\"assistant\",\"content\":\"强烈建议使用，不要手动拼接&lt;|im_start|&gt;标签，手动拼接极易格式错误，直接导致训练失效。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"训练完怎么推理？\"},\n            {\"role\":\"assistant\",\"content\":\"两种方式：1、peft.PeftModel加载基础模型+LoRA适配器；2、合并LoRA权重得到完整模型；也可以vLLM直接加载LoRA做API服务。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"为什么要划分验证集？\"},\n            {\"role\":\"assistant\",\"content\":\"验证集没有参与训练，用来监控过拟合，脚本中load_best_model_at_end会自动保存验证loss最优的LoRA权重。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"什么是packing在SFTTrainer？\"},\n            {\"role\":\"assistant\",\"content\":\"packing=True会把多个短样本拼接在一起提升效率；样本长度参差不齐时建议关闭packing=False。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"学习率LoRA一般设置多大？\"},\n            {\"role\":\"assistant\",\"content\":\"标准LoRA常用1e‑4 ~3e‑4，不要设置过大，容易震荡发散。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"system\",\"content\":\"你是大模型微调助手\"},\n            {\"role\":\"user\",\"content\":\"训练loss下降就代表微调成功吗？\"},\n            {\"role\":\"assistant\",\"content\":\"不是。loss下降只代表拟合训练数据，必须在真实问题上做人工评测，同时看验证集loss，防止过拟合。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"可以直接拿网上开源数据集直接训练吗？\"},\n            {\"role\":\"assistant\",\"content\":\"可以，但要检查字段格式，转换为Qwen ChatML，过滤脏数据，最好混入通用对话数据，避免遗忘基础能力。\"}\n        ]\n    },\n    {\n        \"conversation\": [\n            {\"role\":\"user\",\"content\":\"微调之后模型会永久改变吗？\"},\n            {\"role\":\"assistant\",\"content\":\"LoRA只是附加适配器，基础模型文件不会改动；推理时加载LoRA才生效，移除LoRA就恢复原始基础模型。\"}\n        ]\n    }\n]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>数据集校验\u003C\u002Fh3>\n\u003Cp>数据集校验脚本，提前拦截 JSON 语法错误、字段缺失、非法 role 角色；同时调用分词器\u003Ccode>apply_chat_template\u003C\u002Fcode>渲染对话，检查 ChatML 标签\u003Ccode>&lt;|im_start|&gt;\u003C\u002Fcode>\u002F\u003Ccode>&lt;|im_end|&gt;\u003C\u002Fcode>输出是否正常。\u003C\u002Fp>\n\u003Cp>很多训练失败根源是数据集格式错误，训练前必须运行该脚本，全部校验通过再进入训练。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import json\nfrom transformers import AutoTokenizer\n\nJSON_PATH = r\"\u002Froot\u002Fdata\u002Ftrain.json\"\nMODEL_NAME = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\"\n\nif __name__ == \"__main__\":\n    try:\n        with open(JSON_PATH, \"r\", encoding=\"utf‑8\") as f:\n            data = json.load(f)\n    except Exception as e:\n        print(f\"[-] JSON解析失败：{e}\")\n        exit(1)\n\n    if not isinstance(data, list):\n        print(\"[-] 数据集最外层必须是数组\")\n        exit(1)\n\n    print(f\"[+] JSON读取成功，总样本数：{len(data)}\")\n\n    # 加载分词器\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)\n    print(f\"[+] 成功加载分词器 {MODEL_NAME}\")\n    valid_roles = {\"user\", \"assistant\", \"system\"}\n    error_count = 0\n\n    for idx, item in enumerate(data):\n        if \"conversation\" not in item:\n            print(f\"\\n[-] 第{idx}条：缺少 conversation 字段\")\n            error_count += 1\n            continue\n\n        conv = item[\"conversation\"]\n        if not isinstance(conv, list):\n            print(f\"\\n[-] 第{idx}条：conversation必须是数组\")\n            error_count += 1\n            continue\n\n        for turn_idx, turn in enumerate(conv):\n            if \"role\" not in turn or \"content\" not in turn:\n                print(f\"\\n[-] 第{idx}条‑第{turn_idx}轮：缺少role或content\")\n                error_count += 1\n                continue\n            r = turn[\"role\"]\n            if r not in valid_roles:\n                print(f\"\\n[-] 第{idx}条‑第{turn_idx}轮：非法role={r}，允许：{valid_roles}\")\n                error_count += 1\n\n    print(f\"\\n---------------------------\")\n    if error_count &gt; 0:\n        print(f\"[!] 检测到 {error_count} 处错误，请修改后再训练！\")\n        exit(1)\n    else:\n        print(\"[+] 所有样本字段格式校验通过！\")\n\n    # 渲染ChatML看标签是否正确\n    print(\"\\n===== 抽样渲染前2条看ChatML输出 =====\")\n    for i in range(min(2, len(data))):\n        conv = data[i][\"conversation\"]\n        text = tokenizer.apply_chat_template(\n            conv, tokenize=False, add_generation_prompt=False\n        )\n        print(f\"\\n--------样本{i}--------\")\n        print(text[:600])\n\n        if \"&lt;|im_start|&gt;\" not in text:\n            print(\"[!] 警告：输出没有 &lt;|im_start|&gt;，模板异常！\")\n\n    print(\"\\n[+] 校验完成，可以送入训练脚本。\")\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>检查通过后可看到如下所示输出内容：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>root@localhost:~\u002Fdata# python check.py \n[+] JSON读取成功，总样本数：20\n[+] 成功加载分词器 .\u002Fqwen3.5-0.8B-Instruct\n---------------------------\n[+] 所有样本字段格式校验通过！\n\n===== 抽样渲染前2条看ChatML输出 =====\n\n--------样本0--------\n&lt;|im_start|&gt;user\n什么是大模型LoRA微调？&lt;|im_end|&gt;\n&lt;|im_start|&gt;assistant\n&lt;think&gt;\n&lt;\u002Fthink&gt;\n\nLoRA是一种轻量微调技术，冻结大模型主干权重，只训练少量低秩矩阵，不需要全量更新模型参数，显存消耗低，训练速度快，产出的LoRA适配器体积很小，可以挂载在基础模型上使用。&lt;|im_end|&gt;\n\n--------样本1--------\n&lt;|im_start|&gt;user\nLoRA和QLoRA有什么区别？&lt;|im_end|&gt;\n&lt;|im_start|&gt;assistant\n&lt;think&gt;\n&lt;\u002Fthink&gt;\n\nLoRA主干模型使用FP16\u002FBF16精度加载，主干不量化；QLoRA把主干模型4bit量化，进一步降低显存开销，会带来微小的精度损失。&lt;|im_end|&gt;\n\n[+] 校验完成，可以送入训练脚本。\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>分词器加载与验证脚本\u003C\u002Fh3>\n\u003Cp>验证底座模型分词器加载是否正常，查看特殊 token（\u003Ccode>&lt;|im_start|&gt;\u003C\u002Fcode>、\u003Ccode>&lt;|im_end|&gt;\u003C\u002Fcode>、eos\u002Fpad token）ID，验证编码解码、对话模板输出。很多训练 loss 爆炸、生成乱码来自分词器配置错误；Qwen 系列需要把\u003Ccode>pad_token\u003C\u002Fcode>设置等于\u003Ccode>eos_token\u003C\u002Fcode>，\u003Ccode>padding_side=right\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>from datasets import load_dataset, concatenate_datasets\nfrom transformers import AutoTokenizer,set_seed\n\nSEED = 42\nset_seed(SEED)\nMODEL_NAME = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\"\n\nif __name__ == \"__main__\":\n    # 加载分词器\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)\n    tokenizer.pad_token = tokenizer.eos_token\n    tokenizer.padding_side = \"right\"\n\n    print(\"[+] 成功加载分词器\", MODEL_NAME)\n    print(f\"分词器类名: {type(tokenizer).__name__}\")\n    print(f\"vocab_size: {tokenizer.vocab_size}\")\n    print(f\"model_max_length: {tokenizer.model_max_length}\")\n    print(f\"padding_side: {tokenizer.padding_side}\")\n    print(f\"truncation_side: {tokenizer.truncation_side}\")\n\n    print(\"\\n---------- 全部特殊token及ID ----------\")\n    special_tokens = [\n        (\"bos_token\", tokenizer.bos_token, tokenizer.bos_token_id),\n        (\"eos_token\", tokenizer.eos_token, tokenizer.eos_token_id),\n        (\"pad_token\", tokenizer.pad_token, tokenizer.pad_token_id),\n        (\"unk_token\", tokenizer.unk_token, tokenizer.unk_token_id),\n        (\"im_start token\", \"&lt;|im_start|&gt;\", tokenizer.convert_tokens_to_ids(\"&lt;|im_start|&gt;\")),\n        (\"im_end token\", \"&lt;|im_end|&gt;\", tokenizer.convert_tokens_to_ids(\"&lt;|im_end|&gt;\")),\n    ]\n    for name, tok, tid in special_tokens:\n        print(f\"{name:&lt;15} token={repr(tok):&lt;22} id={tid}\")\n\n    print(\"\\n---------- special_tokens_map ----------\")\n    print(tokenizer.special_tokens_map)\n\n    #print(\"\\n---------- init_kwargs 初始化参数 ----------\")\n    #print(tokenizer.init_kwargs)\n\n    print(\"\\n---------- 词表前30个样例 ----------\")\n    vocab = list(tokenizer.get_vocab().items())[:30]\n    for token_str, token_id in vocab:\n        print(f\"{token_id:6d} | {repr(token_str)}\")\n\n    print(\"\\n---------- 简单编码解码测试 ----------\")\n    test_sentence = \"你好，Qwen大模型！\"\n    encode_out = tokenizer(test_sentence)\n    print(f\"原文：{test_sentence}\")\n    print(f\"input_ids：{encode_out['input_ids']}\")\n    print(f\"还原：{tokenizer.decode(encode_out['input_ids'])}\")\n\n    print(\"\\n---------- apply_chat_template对话模板测试 ----------\")\n    messages = [\n        {\"role\":\"system\",\"content\":\"你是助手\"},\n        {\"role\":\"user\",\"content\":\"你好\"}\n    ]\n    chat_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n    print(\"chat template输出文本：\")\n    print(repr(chat_text))\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>检查通过后可看到如下所示输出内容：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>重点检查：\u003Ccode>pad_token\u003C\u002Fcode>不为 None，\u003Ccode>padding_side=right\u003C\u002Fcode>，\u003Ccode>apply_chat_template\u003C\u002Fcode>输出包含正确 im 标签。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cpre>\u003Ccode>root@localhost:~\u002Fdata# python check.py \n[+] 成功加载分词器\n\n分词器类名: Qwen2Tokenizer\nvocab_size: 248044\nmodel_max_length: 262144\npadding_side: right\ntruncation_side: right\n\n---------- 全部特殊token及ID ----------\nbos_token       token=None                   id=None\neos_token       token='&lt;|im_end|&gt;'           id=248046\npad_token       token='&lt;|im_end|&gt;'           id=248046\nunk_token       token=None                   id=None\nim_start token  token='&lt;|im_start|&gt;'         id=248045\nim_end token    token='&lt;|im_end|&gt;'           id=248046\n\n---------- special_tokens_map ----------\n{'eos_token': '&lt;|im_end|&gt;', 'pad_token': '&lt;|im_end|&gt;', 'audio_bos_token': '&lt;|audio_start|&gt;', 'audio_eos_token': '&lt;|audio_end|&gt;', 'audio_token': '&lt;|audio_pad|&gt;', 'image_token': '&lt;|image_pad|&gt;', 'video_token': '&lt;|video_pad|&gt;', 'visi\non_bos_token': '&lt;|vision_start|&gt;', 'vision_eos_token': '&lt;|vision_end|&gt;'}\n\n---------- 词表前30个样例 ----------\n222609 | 'ãģ¨æĢĿãģ£ãģŁãĤī'\n150945 | 'ĠÐ´Ñĥ'\n101644 | 'ä¹Łå°±æĺ¯'\n 38671 | 'Ġcurse'\n173593 | 'Ð½Ð°Ð·Ð½Ð°'\n126060 | 'åīįä¸įä¹ħ'\n164288 | 'hÃ£o'\n136874 | 'éĢĢä¼ĳäººåĳĺ'\n125605 | 'ä¸ĢäºĽå°ı'\n148886 | 'à®³'\n228510 | 'Ġentregue'\n184351 | 'Ġinklus'\n167525 | 'ĠØ§ÙĦÙħØµÙĨ'\n176754 | 'Ġtratamento'\n 57152 | 'calar'\n187517 | 'ÑģÐ½Ñĥ'\n  3978 | 'ĠOff'\n 42853 | 'Ġrecruited'\n170936 | 'Ġbiá»ĥn'\n  6770 | 'ometry'\n201897 | 'Ġmaintenir'\n 21855 | 'ĠSure'\n144957 | 'åĩĨèĢĥè¯ģæīĵåį°'\n 66583 | '(seg'\n 81516 | 'ulado'\n125493 | 'ç©ºç©º'\n   832 | 'arg'\n146557 | 'èı²èı²'\n173079 | 'ĠTÆ°'\n 83695 | 'ĉJson'\n\n---------- 简单编码解码测试 ----------\n原文：你好，Qwen大模型！\ninput_ids：[109266, 3709, 48, 16451, 95779, 103725, 6115]\n还原：你好，Qwen大模型！\n\n---------- apply_chat_template对话模板测试 ----------\nchat template输出文本：\n'&lt;|im_start|&gt;system\\n你是助手&lt;|im_end|&gt;\\n&lt;|im_start|&gt;user\\n你好&lt;|im_end|&gt;\\n&lt;|im_start|&gt;assistant\\n&lt;think&gt;\\n\\n&lt;\u002Fthink&gt;\\n\\n'\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>LoRA‑SFT 训练脚本\u003C\u002Fh3>\n\u003Cp>加载本地 Qwen3.5‑0.8B‑Instruct 基座模型，冻结主干，使用 LoRA 做轻量化微调；读取本地 json 对话数据集，经过 chat 模板格式化、分词、过滤、划分训练验证集；使用 Trainer 执行 step 式训练，保存 LoRA 适配器，最后加载 LoRA 做简单推理验证。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import os\nimport torch\nfrom datasets import load_dataset\nfrom transformers import (\n    AutoModelForCausalLM,\n    AutoTokenizer,\n    TrainingArguments,\n    set_seed,\n    DataCollatorForLanguageModeling,\n    Trainer\n)\nfrom peft import PeftModel, LoraConfig, get_peft_model\nimport warnings\n\nwarnings.filterwarnings(\"ignore\")\n\nSEED = 42\nset_seed(SEED)\n\n# 配置路径\nLOCAL_JSON_PATH = r\"\u002Froot\u002Fdata\u002Ftrain.json\"\nMODEL_NAME = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\"\nOUTPUT_DIR = r\"\u002Froot\u002Fdata\u002Fqwen_lora\"\n\nMAX_SEQ_LENGTH = 8192\n\n# LoRA超参\nLORA_R = 16\nLORA_ALPHA = 16\nLORA_DROPOUT = 0.0\n\n# RTX4090‑24G 8192上下文 调参\nBATCH_SIZE_PER_DEVICE = 2\nGRADIENT_ACCUMULATION_STEPS = 4\nMAX_STEPS = 40\nLEARNING_RATE = 1e-4\nWARMUP_STEPS = 10\n\n# 显存优化\nGRADIENT_CHECKPOINTING = True\n\n# 4090+torch2.13优先bfloat16 比fp16更稳\nUSE_BF16 = True\n\ndef format_conversation(sample):\n    conv = sample[\"conversation\"]\n    text = tokenizer.apply_chat_template(\n        conv,\n        tokenize=False,\n        add_generation_prompt=False\n    )\n    return {\"text\": text}\n\ndef tokenize_fn(sample):\n    out = tokenizer(\n        sample[\"text\"],\n        truncation=True,\n        max_length=MAX_SEQ_LENGTH,\n    )\n    return out\n\ndef filter_long_sample(sample):\n    return len(sample[\"input_ids\"]) &lt; MAX_SEQ_LENGTH\n\nif __name__ == \"__main__\":\n    print(f\"PyTorch version: {torch.__version__}\")\n    print(f\"CUDA available: {torch.cuda.is_available()}\")\n    print(f\"CUDA version: {torch.version.cuda}\")\n    print(f\"GPU count: {torch.cuda.device_count()}\")\n    if torch.cuda.is_available():\n        print(f\"GPU Name: {torch.cuda.get_device_name(0)}\")\n    print(f\"Use bf16: {USE_BF16}\")\n\n    # 路径校验\n    print(f\"\\n[CHECK] MODEL_NAME={MODEL_NAME}\")\n    if not os.path.isdir(MODEL_NAME):\n        raise FileNotFoundError(f\"模型文件夹不存在：{MODEL_NAME}，检查真实路径！\")\n    if not os.path.exists(os.path.join(MODEL_NAME, \"config.json\")):\n        raise FileNotFoundError(f\"{MODEL_NAME} 缺少config.json，不是完整模型目录\")\n    print(\"[CHECK] 模型目录校验通过\")\n\n    if not os.path.exists(LOCAL_JSON_PATH):\n        raise FileNotFoundError(f\"数据集文件不存在：{LOCAL_JSON_PATH}\")\n    print(\"[CHECK] 数据集文件校验通过\\n\")\n\n    # 加载分词器\n    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)\n    if tokenizer.pad_token is None:\n        tokenizer.pad_token = tokenizer.eos_token\n    tokenizer.padding_side = \"right\"\n    print(\"[+] 成功加载分词器\", MODEL_NAME)\n\n    # 加载模型 单卡强制0号卡\n    model = AutoModelForCausalLM.from_pretrained(\n        MODEL_NAME,\n        dtype=torch.bfloat16 if USE_BF16 else torch.float16,\n        device_map={\"\": 0},\n        trust_remote_code=True,\n    )\n    for param in model.parameters():\n        param.requires_grad = False\n    print(\"[+] 主干模型加载完成 已冻结\")\n\n    if GRADIENT_CHECKPOINTING:\n        model.gradient_checkpointing_enable()\n        model.enable_input_require_grads()\n        model.config.use_cache = False\n        print(\"[+] 开启梯度检查点 显存优化\")\n\n    # LoRA配置\n    lora_config = LoraConfig(\n        r=LORA_R,\n        lora_alpha=LORA_ALPHA,\n        target_modules=[\n            \"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\",\n            \"gate_proj\", \"up_proj\", \"down_proj\"\n        ],\n        lora_dropout=LORA_DROPOUT,\n        bias=\"none\",\n        task_type=\"CAUSAL_LM\",\n    )\n    model = get_peft_model(model, lora_config)\n    model.print_trainable_parameters()\n\n    # 数据集加载\n    raw_ds = load_dataset(\"json\", data_files=LOCAL_JSON_PATH, split=\"train\")\n    print(f\"原始数据集样本数量: {len(raw_ds)}\")\n\n    raw_ds = raw_ds.map(format_conversation, num_proc=8)\n    raw_ds = raw_ds.map(tokenize_fn, num_proc=8)\n    raw_ds = raw_ds.filter(filter_long_sample)\n    raw_ds = raw_ds.select_columns([\"input_ids\", \"attention_mask\"])\n    raw_ds = raw_ds.shuffle(seed=SEED)\n\n    split_ds = raw_ds.train_test_split(test_size=0.05, seed=SEED)\n    train_ds = split_ds[\"train\"]\n    eval_ds = split_ds[\"test\"]\n    print(f\"train:{len(train_ds)}, eval:{len(eval_ds)}\")\n    print(\"数据集字段：\", train_ds.column_names)\n\n    data_collator = DataCollatorForLanguageModeling(\n        tokenizer=tokenizer,\n        mlm=False\n    )\n\n    training_args = TrainingArguments(\n        output_dir=OUTPUT_DIR,\n        per_device_train_batch_size=BATCH_SIZE_PER_DEVICE,\n        per_device_eval_batch_size=1,\n        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,\n        warmup_steps=WARMUP_STEPS,\n        max_steps=MAX_STEPS,\n        learning_rate=LEARNING_RATE,\n        bf16=USE_BF16,\n        fp16=not USE_BF16,\n        logging_steps=5,\n        optim=\"adamw_torch\",\n        weight_decay=0.01,\n        lr_scheduler_type=\"linear\",\n        seed=SEED,\n        report_to=\"none\",\n        eval_strategy=\"steps\",\n        eval_steps=20,\n        save_strategy=\"steps\",\n        save_steps=20,\n        save_total_limit=3,\n        load_best_model_at_end=True,\n        prediction_loss_only=True,\n        push_to_hub=False,\n        save_only_model=True,\n    )\n\n    trainer = Trainer(\n        model=model,\n        args=training_args,\n        train_dataset=train_ds,\n        eval_dataset=eval_ds,\n        data_collator=data_collator,\n    )\n\n    print(\"[+] 开始训练......\")\n    trainer.train()\n\n    trainer.save_model(OUTPUT_DIR)\n    tokenizer.save_pretrained(OUTPUT_DIR)\n    print(f\"训练完成，LoRA适配器输出路径：{OUTPUT_DIR}\")\n\n    # 推理验证测试\n    base_model = AutoModelForCausalLM.from_pretrained(\n        MODEL_NAME,\n        dtype=torch.bfloat16,\n        device_map={\"\": 0},\n        trust_remote_code=True\n    )\n    lora_model = PeftModel.from_pretrained(base_model, OUTPUT_DIR)\n    messages = [{\"role\":\"user\", \"content\":\"你好\"}]\n    prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)\n    inputs = tokenizer(prompt, return_tensors=\"pt\").to(\"cuda\")\n    outputs = lora_model.generate(**inputs, max_new_tokens=100)\n    print(tokenizer.decode(outputs[0], skip_special_tokens=True))\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>训练结束输出产物：\u003Ccode>\u002Froot\u002Fdata\u002Fqwen_lora\u003C\u002Fcode>，里面是 LoRA 适配器，体积很小（几十 MB）不是完整模型；包含\u003Ccode>adapter_model.safetensors\u003C\u002Fcode>、\u003Ccode>adapter_config.json\u003C\u002Fcode>以及 checkpoint 中间快照。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>root@localhost:~\u002Fdata# python qwen_lora.py\nPyTorch version: 2.6.0+cu124\nCUDA available: True\nCUDA version: 12.4\nGPU count: 1\nGPU Name: NVIDIA GeForce RTX 4090\nUse bf16: True\n\n[CHECK] MODEL_NAME=\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\n[CHECK] 模型目录校验通过\n[CHECK] 数据集文件校验通过\n\n[+] 成功加载分词器 \u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\nLoading weights: 100%|█████████████████████████████████████████| 320\u002F320 [00:00&lt;00:00, 1021.22it\u002Fs]\n[+] 主干模型加载完成，已冻结\n[+] 开启梯度检查点，显存优化\ntrainable params: 6,389,760 || all params: 758,782,784 || trainable%: 0.8421\n原始数据集样本数量: 20\ntrain:19, eval:1\n数据集字段： ['input_ids', 'attention_mask']\n[+] 开始训练......\n{'loss': '3.747', 'grad_norm': '4.377', 'learning_rate': '4e-05', 'epoch': '1.8'}                                                                                                    \n{'loss': '3.275', 'grad_norm': '2.973', 'learning_rate': '9e-05', 'epoch': '3.4'}                                                                                                    \n{'loss': '2.823', 'grad_norm': '2.982', 'learning_rate': '8.667e-05', 'epoch': '5'}                                                                                                  \n{'loss': '2.292', 'grad_norm': '2.099', 'learning_rate': '7e-05', 'epoch': '6.8'}                                                                                                    \n{'eval_loss': '2.52', 'eval_runtime': '0.1862', 'eval_samples_per_second': '5.371', 'eval_steps_per_second': '5.371', 'epoch': '6.8'}                                                \n{'loss': '2.01', 'grad_norm': '2.022', 'learning_rate': '5.333e-05', 'epoch': '8.4'}                                                                                                 \n{'loss': '1.762', 'grad_norm': '3.386', 'learning_rate': '3.667e-05', 'epoch': '10'}                                                                                                 \n{'loss': '1.535', 'grad_norm': '2.145', 'learning_rate': '2e-05', 'epoch': '11.8'}                                                                                                   \n{'loss': '1.426', 'grad_norm': '2.224', 'learning_rate': '3.333e-06', 'epoch': '13.4'}                                                                                               \n{'eval_loss': '2.717', 'eval_runtime': '0.1921', 'eval_samples_per_second': '5.204', 'eval_steps_per_second': '5.204', 'epoch': '13.4'}                                              \n{'train_runtime': '144.7', 'train_samples_per_second': '2.211', 'train_steps_per_second': '0.276', 'train_loss': '2.359', 'epoch': '13.4'}                                           \n100%|███████████████████████████████████████████████████| 40\u002F40 [02:24&lt;00:00,  3.62s\u002Fit]\n训练完成，LoRA适配器输出路径：\u002Froot\u002Fdata\u002Fqwen_lora\n=====推理验证=====\nLoading weights: 100%|██████████████████████████████████| 320\u002F320 [00:00&lt;00:00, 1011.21it\u002Fs]\nuser\n你好\nassistant\n&lt;think&gt;\n&lt;\u002Fthink&gt;\n\n你好！有什么我可以帮你的吗？\n\nroot@localhost:~\u002Fdata# cd qwen_lora\u002F\nroot@localhost:~\u002Fdata\u002Fqwen_lora# ll\ntotal 44556\ndrwxr-xr-x 4 root root     4096 Sep  5 23:33 .\u002F\ndrwxr-xr-x 5 root root      189 Sep  5 23:25 ..\u002F\n-rw-r--r-- 1 root root     5218 Sep  5 23:33 README.md\n-rw-r--r-- 1 root root     1164 Sep  5 23:33 adapter_config.json\n-rw------- 1 root root 25584224 Sep  5 23:33 adapter_model.safetensors\n-rw-r--r-- 1 root root     7755 Sep  5 23:33 chat_template.jinja\ndrwxr-xr-x 2 root root     4096 Sep  5 23:31 checkpoint-20\u002F\ndrwxr-xr-x 2 root root     4096 Sep  5 23:33 checkpoint-40\u002F\n-rw-r--r-- 1 root root 19989325 Sep  5 23:33 tokenizer.json\n-rw-r--r-- 1 root root     1124 Sep  5 23:33 tokenizer_config.json\n-rw-r--r-- 1 root root     4792 Sep  5 23:33 training_args.bin\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>适配器合并完整权重\u003C\u002Fh3>\n\u003Cp>LoRA 适配器不能直接用于 llama.cpp、GGUF 导出；需要把 LoRA 权重合并进底座模型权重，输出完整 HF 格式模型。\u003C\u002Fp>\n\u003Cp>脚本\u003Ccode>merge_lora.py\u003C\u002Fcode>将 LoRA 低秩矩阵和基础模型权重矩阵做矩阵相加，输出完整独立模型，不再依赖 peft 库；合并后可以直接 vLLM 推理，也可以转 GGUF。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import torch\nfrom peft import PeftModel\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nBASE_MODEL_PATH = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\"\nLORA_PATH = r\"\u002Froot\u002Fdata\u002Fqwen_lora\"\nMERGED_OUT = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged\"\n\nprint(\"加载基础模型...\")\nbase = AutoModelForCausalLM.from_pretrained(\n    BASE_MODEL_PATH,\n    dtype=torch.bfloat16,\n    device_map={\"\": 0},\n    trust_remote_code=True\n)\n\nprint(\"加载LoRA适配器并合并...\")\nmodel = PeftModel.from_pretrained(base, LORA_PATH)\nmerged_model = model.merge_and_unload()\n\nprint(\"保存合并后的完整模型\")\nmerged_model.save_pretrained(MERGED_OUT, safe_serialization=True)\n\ntokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_PATH, trust_remote_code=True)\ntokenizer.save_pretrained(MERGED_OUT)\n\nprint(f\"合并完成，输出目录：{MERGED_OUT}\")\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>输出目录\u003Ccode>\u002Froot\u002Fdata\u002Fqwen3.5‑0.8B‑lora‑merged\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>root@localhost:~\u002Fdata# python lora‑merged.py \n加载基础模型...\nLoading weights: 100%|█████████████████████████████████████████████████| 320\u002F320 [00:00&lt;00:00, 918.63it\u002Fs]\n加载LoRA适配器并合并...\n保存合并后的完整模型\nWriting model shards: 100%|██████████████████████████████████████████| 1\u002F1 [00:01&lt;00:00,  1.98s\u002Fit]\n合并完成，输出目录：\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged\n\nroot@localhost:~\u002Fdata# cd qwen3.5-0.8B-Instruct\u002F\nroot@localhost:~\u002Fdata\u002Fqwen3.5-0.8B-Instruct# ls -lh\ntotal 1.7G\n-rw-r--r-- 1 root root 1.4K Sep  5 23:12 README.md\n-rw-r--r-- 1 root root 2.9K Sep  5 23:12 config.json\n-rw-r--r-- 1 root root   48 Sep  5 23:12 configuration.json\n-rw-r--r-- 1 root root 1.7G Sep  5 23:17 model.safetensors\n-rw-r--r-- 1 root root  336 Sep  5 23:12 preprocessor_config.json\n-rw-r--r-- 1 root root  13M Sep  5 23:12 tokenizer.json\n-rw-r--r-- 1 root root  17K Sep  5 23:12 tokenizer_config.json\n\nroot@localhost:~\u002Fdata\u002Fqwen_lora# ls -lh\ntotal 44M\n-rw-r--r-- 1 root root 5.1K Sep  5 23:33 README.md\n-rw-r--r-- 1 root root 1.2K Sep  5 23:33 adapter_config.json\n-rw------- 1 root root  25M Sep  5 23:33 adapter_model.safetensors\n-rw-r--r-- 1 root root 7.6K Sep  5 23:33 chat_template.jinja\ndrwxr-xr-x 2 root root 4.0K Sep  5 23:31 checkpoint-20\ndrwxr-xr-x 2 root root 4.0K Sep  5 23:33 checkpoint-40\n-rw-r--r-- 1 root root  20M Sep  5 23:33 tokenizer.json\n-rw-r--r-- 1 root root 1.1K Sep  5 23:33 tokenizer_config.json\n-rw-r--r-- 1 root root 4.7K Sep  5 23:33 training_args.bin\n\nroot@localhost:~\u002Fdata\u002Fqwen3.5-0.8B-lora-merged# ls -lh\ntotal 1.5G\n-rw-r--r-- 1 root root 7.6K Sep  5 23:37 chat_template.jinja\n-rw-r--r-- 1 root root 1.8K Sep  5 23:37 config.json\n-rw-r--r-- 1 root root  116 Sep  5 23:37 generation_config.json\n-rw------- 1 root root 1.5G Sep  5 23:37 model.safetensors\n-rw-r--r-- 1 root root  20M Sep  5 23:37 tokenizer.json\n-rw-r--r-- 1 root root 1.1K Sep  5 23:37 tokenizer_config.json\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>完整模型推理测试\u003C\u002Fh3>\n\u003Cp>不依赖 peft 库，直接加载合并完成完整 HF 模型，验证对话生成效果；调用\u003Ccode>apply_chat_template\u003C\u002Fcode>构建 prompt，设置 temperature、top_p 采样参数做生成。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nMODEL_PATH = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged\"\n\n# 加载完整合并模型，不需要peft\nmodel = AutoModelForCausalLM.from_pretrained(\n    MODEL_PATH,\n    dtype=torch.bfloat16,\n    device_map={\"\": 0},\n    trust_remote_code=True\n)\ntokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)\n\ndef chat(prompt_text):\n    messages = [\n        {\"role\": \"user\", \"content\": prompt_text}\n    ]\n    text = tokenizer.apply_chat_template(\n        messages,\n        tokenize=False,\n        add_generation_prompt=True\n    )\n    inputs = tokenizer(text, return_tensors=\"pt\").to(\"cuda\")\n    outputs = model.generate(\n        **inputs,\n        max_new_tokens=200,\n        temperature=0.7,\n        top_p=0.8,\n        do_sample=True\n    )\n    return tokenizer.decode(outputs[0], skip_special_tokens=True)\n\nif __name__ == \"__main__\":\n    print(\"====测试1====\")\n    res1 = chat(\"你好\")\n    print(res1)\n\n    print(\"\\n====测试2====\")\n    res2 = chat(\"简单介绍一下人工智能\")\n    print(res2)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行后输出测试效果如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>root@localhost:~\u002Fdata# python test.py  \nLoading weights: 100%|██████████████████████████████████████████████| 320\u002F320 [00:00&lt;00:00, 1111.48it\u002Fs]\n====测试1====\nuser\n你好\nassistant\n&lt;think&gt;\n\n&lt;\u002Fthink&gt;\n\n你好！有什么可以帮你的吗？\n\n\n====测试2====\nuser\n简单介绍一下人工智能\nassistant\n&lt;think&gt;\n\n&lt;\u002Fthink&gt;\n\n人工智能是让计算机模仿人类智能，实现自主决策、学习和适应的能力。\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>底座和LoRA合并模型对比测试，同时加载原始底座模型、微调合并后的模型，输入同一个问题对比输出，直观确认微调是否生效。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nBASE = \"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\"\nMERGED = \"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged\"\n\ntokenizer = AutoTokenizer.from_pretrained(BASE, trust_remote_code=True)\n\ndef get_answer(model_path, question):\n    model = AutoModelForCausalLM.from_pretrained(\n        model_path,\n        dtype=torch.bfloat16,\n        device_map={\"\":0},\n        trust_remote_code=True\n    )\n    messages = [{\"role\":\"user\",\"content\":question}]\n    prompt = tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=True)\n    inputs = tokenizer(prompt,return_tensors=\"pt\").to(\"cuda\")\n    out = model.generate(**inputs,max_new_tokens=150,temperature=0.7)\n    ans = tokenizer.decode(out[0],skip_special_tokens=True)\n    del model\n    torch.cuda.empty_cache()\n    return ans\n\nq = \"你好，什么是大模型LoRA微调？\"\nprint(\"【原始底座回答】\")\nprint(get_answer(BASE,q))\nprint(\"\\n【LoRA合并后回答】\")\nprint(get_answer(MERGED,q))\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行后输出测试效果如下：\u003C\u002Fp>\n\u003Cblockquote>\n \u003Cp>现象：底座回答偏向通用百科；微调后的输出风格、话术会贴近训练集的回答内容。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cpre>\u003Ccode>root@localhost:~\u002Fdata# python test.py \n【原始底座回答】\nLoading weights: 100%|████████████████████████████████████████████████| 320\u002F320 [00:00&lt;00:00, 867.20it\u002Fs]\n[transformers] The following generation flags are not valid and may be ignored: ['temperature']. Set `TRANSFORMERS_VERBOSITY=info` for more details.\nuser\n你好，什么是大模型LoRA微调？\nassistant\n&lt;think&gt;\n\n&lt;\u002Fthink&gt;\n\n你好！大模型（LLM）的 **LoRA（Low-Rank Adaptation）** 微调是一种高效、轻量级的模型微调技术，旨在在不显著影响模型整体性能的前提下，大幅降低计算成本。\n\n简单来说，LoRA 的核心思想是：**只修改模型中“低秩”的部分，而保留“高秩”（即模型的核心能力）不变。**\n\n以下是关于 LoRA 微调的详细解析：\n\n### 1. 核心原理：低秩分解\nLoRA 的精髓在于**低秩分解（Low-Rank Decomposition, LRD）**。\n\n*   **高秩矩阵**：代表模型中已经学习好的核心能力（如语言理解、逻辑推理、代码生成等\n\n【LoRA合并后回答】\nLoading weights: 100%|███████████████████████████████████████████████████| 320\u002F320 [00:00&lt;00:00, 1027.28it\u002Fs]\nuser\n你好，什么是大模型LoRA微调？\nassistant\n&lt;think&gt;\n\n&lt;\u002Fthink&gt;\n\nLoRA是LoRA适配器，只加载少量权重，训练时只加载少量权重，训练速度更快，推理速度更快，适合微调大模型。\n\u003C\u002Fcode>\u003C\u002Fpre>","大模型微调是实现模型领域定制的核心方案。本文基于 Ubuntu 22.04 服务器环境，依托 NVIDIA RTX 家用高性能显卡，选用千问 Qwen3.5‑0.8B‑Instruct 指令模型，完整演示二次 LoRA‑SFT 微调全流程，包含环境搭建、模型选型、ChatML 数据集制作校验、LoRA 参数配置、SFT 训练、效果验证、权重合并，附带可直接运行的工程脚本，便于开发者快速实现千问模型轻量化定制与上线部署。 在开展Qwen模型微调实操前，需提前配置好服务器软硬件运行环境，本文所有实操流程均基于以下稳定运行的本机环境，读者可直接参考对齐配置，适配复现： 操作系统：Ubuntu 22.04.5 LTS (GNU\u002FLinux 5.15.0-187-generic x86_64) 框架版本：torch 2.13.0 + CUDA 12.8.1 + Python 3.10.12 显卡驱动：NVIDIA ≥ 570.133.07 显卡类型：NVIDIA RTX 4090 24GB GPU CPU核心：INTEL(R) XEON(R) GOLD 6530 内存：64GB DDR5 存储：50GB(系统盘)+100GB(数据盘) 基础知识 两种训练 大模型行业落地场景中，通用预训练模型难以适配垂直领域专属知识、定制化对话风格与专属业务指令需求，模型微调成为轻量化、低成本实现模型能力定制的核心手段。 模型的微调训练包含两种主流模式： 第一种：基于Base预训练基座模型开展从零SFT微调，该类模型仅完成通用预训练，无官方指令对齐能力，无法原生理解对话指令，需人工构建对话格式、手动拼接对话文本及监督数据集，从零学习指令跟随与对话能力，多用于全新模型定制与二次预训练任务，该模式消耗算力大，一般不会使用此方式微调。 第二种：指令模型二次 LoRA‑SFT 微调，也是本文的核心内容，千问 Instruct 指令模型由 Base 基座经过官方 SFT 监督微调、DPO\u002FRLHF 偏好对齐得到，原生具备指令理解与多轮对话能力。在此成熟对齐模型基础上开展二次 LoRA‑SFT 微调，可在保留模型通用知识与基础对话能力的前提下，低成本注入领域专属能力或知识，规避基座从零训练的数据门槛与对齐成本，是工程落地与轻量化模型定制的最优方案。 进行微调前需要判别模型底座，模型名称带有‑Base后缀的为预训练裸基座；模型目录中包含chat_template.jinja文件，则说明是 Instruct 指令模型，该类模型已经完成 SFT、DPO、RLHF 等指令对齐流程。现实中原始预训练基模较少对外开源，公开可获取的大多是经过完整后训练的成品权重。 LoRA\u002FQLoRA LoRA 与 QLoRA 是目前主流的参数高效微调方法。LoRA 通过冻结主干模型，仅训练注意力层的低秩适配器，在几乎不损失模型性能的前提下降低训练参数量，适合显存条件较好的硬件环境。QLoRA 在 LoRA 基础上引入 4\u002F8 比特权重量化，将主干模型以量化形式加载，以此来压缩显存占用，使大模型微调可以在消费级显卡上完成，但会引入少量量化噪声，可能对最终效果带来轻微影响。 项目 LoRA QLoRA 核心机制 低秩适配器 低秩适配器 + INT4\u002FINT8 权重量化 模型主干精度 FP16\u002FBF16 INT4\u002FINT8 量化冻结 显存消耗 较低 更低 精度损失 几乎无 存在轻微量化损失 工程复杂度 低，稳定通用 较高，存在量化适配问题 适用场景 显存条件较好，8B‑14B，追求微调效果 显存受限，超大模型 (27B+)，消费级显卡 LoRA 与 QLoRA 的选择主要依据硬件设备显存条件确定。对于 8B 规模模型，当显卡显存大于 24GB 时，优先采用 LoRA 微调；针对 14B、27B 等更大规模模型或显存资源受限场景，则选用 QLoRA 并开启 4 比特量化加载以降低显存压力。本文选用体量较小的 Qwen3.5‑0.8B‑Instruct 作为实验对象，模型参数量小，便于流程演示，因此直接采用 LoRA 开展二次 SFT 指令微调。 完整实验链路为：环境准备 → 数据集准备(train.json) → 加载基座模型(Qwen3.5‑0.8B‑Instruct) → LoRA 参数配置 → SFTTrainer 执行微调训练 → 本地推理效果验证 → LoRA 适配器与主模型权重合并导出 微调实验 环境安装 1、创建独立 Python 虚拟环境，隔离项目依赖，避免和系统 Python 包冲突。 root@localhost:~\u002F# sudo apt install -y python3-full python3-venv tmux git tree root@localhost:~\u002F# sudo python3 -m venv ~\u002Fmyvenv root@localhost:~\u002F# source ~\u002Fmyvenv\u002Fbin\u002Factivate 2、激活进入虚拟环境，使用腾讯云镜像源加速深度学习、微调相关全套依赖包。 root@localhost:~\u002F# pip3 install -i https:\u002F\u002Fmirrors.cloud.tencent.com\u002Fpypi\u002Fsimple\u002F torch torchvision trl datasets peft accelerate bitsandbytes wandb transformers sentencepiece huggingface_hub protobuf modelscope root@localhost:~\u002F# pip3 list Package Version ---------------------- ------------ accelerate 1.14.0 aiohappyeyeballs 2.7.1 aiohttp 3.14.3 aiosignal 1.4.0 annotated-doc 0.0.5 annotated-types 0.8.0 anyio 4.15.1 async-timeout 5.0.1 attrs 26.1.0 bitsandbytes 0.50.2 certifi 2026.7.22 charset-normalizer 3.5.1 click 8.5.0 cuda-bindings 13.3.1 cuda-pathfinder 1.8.1 cuda-toolkit 13.0.3.0 datasets 5.0.1 dill 0.4.1 exceptiongroup 1.3.1 filelock 3.32.5 frozenlist 1.8.0 fsspec 2026.6.0 h11 0.16.0 hf-xet 1.6.0 httpcore 1.0.9 httpx 0.28.1 huggingface_hub 1.30.0 idna 3.19 Jinja2 3.1.6 markdown-it-py 4.2.0 MarkupSafe 3.0.3 mdurl 0.1.2 mpmath 1.3.0 multidict 6.7.1 multiprocess 0.70.19 networkx 3.4.2 numpy 2.2.6 nvidia-cublas 13.1.1.3 nvidia-cuda-cupti 13.0.85 nvidia-cuda-nvrtc 13.0.88 nvidia-cuda-runtime 13.0.96 nvidia-cudnn-cu13 9.24.0.43 nvidia-cufft 12.0.0.61 nvidia-cufile 1.15.1.6 nvidia-curand 10.4.0.35 nvidia-cusolver 12.0.4.66 nvidia-cusparse 12.6.3.3 nvidia-cusparselt-cu13 0.8.1 nvidia-nccl-cu13 2.30.7 nvidia-nvjitlink 13.3.33 nvidia-nvshmem-cu13 3.4.5 nvidia-nvtx 13.0.85 opentelemetry-api 1.44.0 packaging 26.3 pandas 2.3.3 peft 0.20.0 pillow 12.3.0 pip 22.0.2 platformdirs 4.11.7 propcache 0.5.2 protobuf 7.36.1 psutil 7.2.2 pyarrow 25.0.1 pydantic 2.13.5 pydantic_core 2.46.5 Pygments 2.21.0 python-dateutil 2.9.0.post0 pytz 2026.3.post1 PyYAML 6.0.3 regex 2026.9.3 requests 2.34.2 rich 15.0.0 safetensors 0.8.0 sentry-sdk 2.68.1 setuptools 84.0.0 shellingham 1.5.4 six 1.17.0 sympy 1.14.0 tokenizers 0.23.2 torch 2.14.0 torchaudio 2.11.0 torchvision 0.29.0 tqdm 4.70.0 transformers 5.16.1 triton 3.8.0 trl 1.12.0 typer 0.27.2 typing_extensions 4.16.0 typing-inspection 0.4.4 tzdata 2026.3 urllib3 2.7.0 wandb 0.29.0 xxhash 4.0.1 yarl 1.24.5 下载底座模型 使用 modelscope 下载 Qwen\u002FQwen3.5‑0.8B‑Instruct 已经预 SFT 完成的底座模型，该模型原生支持 ChatML 对话模板，不需要自己修改 tokenizer，适合直接二次 LoRA 微调；不要手动复制网页，使用 modelscope 下载保证文件完整，避免文件缺失导致训练报错。 https:\u002F\u002Fwww.modelscope.cn\u002Fmodels\u002Ficyfenix\u002FQwen3.5-0.8B-Instruct root@localhost:~\u002F# mkdir data root@localhost:~\u002F# cd data root@localhost:~\u002F# modelscope download --model icyfenix\u002FQwen3.5-0.8B-Instruct --local_dir .\u002Fqwen3.5‑0.8B‑Instruct 准备 SFT 微调数据集 采用 Qwen 标准 ChatML 格式，json 数组格式，每一条样本包含conversation数组，数组内是多轮对话，角色仅允许system\u002Fuser\u002Fassistant，文件保存到\u002Froot\u002Fdata\u002Ftrain.json中。 此处为小样本演示数据集，适合跑通流程；真实业务建议扩充到几百～几千条，太少容易过拟合。 [ { \"conversation\": [ {\"role\":\"user\",\"content\":\"什么是大模型LoRA微调？\"}, {\"role\":\"assistant\",\"content\":\"LoRA是一种轻量微调技术，冻结大模型主干权重，只训练少量低秩矩阵，不需要全量更新模型参数，显存消耗低，训练速度快，产出的LoRA适配器体积很小，可以挂载在基础模型上使用。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"LoRA和QLoRA有什么区别？\"}, {\"role\":\"assistant\",\"content\":\"LoRA主干模型使用FP16\u002FBF16精度加载，主干不量化；QLoRA把主干模型4bit量化，进一步降低显存开销，会带来微小的精度损失。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"SFT监督微调是什么？\"}, {\"role\":\"assistant\",\"content\":\"SFT即监督微调，使用高质量问答对话数据集，让大模型学习期望的输出格式、回答风格、领域知识，是对齐大模型的基础步骤。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"微调为什么会出现灾难性遗忘？\"}, {\"role\":\"assistant\",\"content\":\"当训练数据集单一，模型过度学习新数据，丢失原本基础能力，就会发生灾难性遗忘。解决办法是混入通用对话数据，控制训练步数，避免过拟合。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"PEFT是什么？\"}, {\"role\":\"assistant\",\"content\":\"PEFT是HuggingFace的参数高效微调库，包含LoRA、IA3等多种算法，只训练一小部分参数，不用完整保存整个大模型。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"如何判断微调过拟合？\"}, {\"role\":\"assistant\",\"content\":\"观察eval验证loss，loss不再下降甚至上升；模型在训练集问题回答很好，陌生问题表现很差；输出重复、背诵训练集内容，就是过拟合。可以减少训练步数，增大数据集多样性。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"vLLM可以直接加载LoRA适配器吗？\"}, {\"role\":\"assistant\",\"content\":\"原生PEFT标准LoRA适配器，新版vLLM支持直接加载，不需要手动合并权重；Unsloth的bnb‑4bit的LoRA不能直接加载，必须合并导出完整权重。\"} ] }, { \"conversation\": [ {\"role\":\"system\",\"content\":\"你是大模型微调助手，回答简洁专业\"}, {\"role\":\"user\",\"content\":\"微调需要多少样本？\"}, {\"role\":\"assistant\",\"content\":\"风格迁移：200‑1000条；垂直领域知识增强：2000‑10000条；样本过少极易过拟合。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"训练的时候max_steps和epoch选哪个？\"}, {\"role\":\"assistant\",\"content\":\"小数据集优先max_steps，防止epoch重复训练造成过拟合；大数据集可以使用num_train_epochs，一般1‑3轮。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"LoRA的r参数代表什么？\"}, {\"role\":\"assistant\",\"content\":\"r是LoRA秩，r越大可训练参数量越大；常用8、16、32；r太小表达能力不足，r太大容易过拟合。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"梯度累积的作用是什么？\"}, {\"role\":\"assistant\",\"content\":\"显存不足调小batch_size，通过gradient_accumulation_steps做梯度累积，模拟更大的batch，不增加显存占用。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"bf16和fp16区别？\"}, {\"role\":\"assistant\",\"content\":\"bf16指数范围更大，适合大模型训练；30系及以上显卡支持bf16；20系及更早显卡只能用fp16。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"微调数据集一定要用apply_chat_template吗？\"}, {\"role\":\"assistant\",\"content\":\"强烈建议使用，不要手动拼接\u003C|im_start|>标签，手动拼接极易格式错误，直接导致训练失效。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"训练完怎么推理？\"}, {\"role\":\"assistant\",\"content\":\"两种方式：1、peft.PeftModel加载基础模型+LoRA适配器；2、合并LoRA权重得到完整模型；也可以vLLM直接加载LoRA做API服务。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"为什么要划分验证集？\"}, {\"role\":\"assistant\",\"content\":\"验证集没有参与训练，用来监控过拟合，脚本中load_best_model_at_end会自动保存验证loss最优的LoRA权重。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"什么是packing在SFTTrainer？\"}, {\"role\":\"assistant\",\"content\":\"packing=True会把多个短样本拼接在一起提升效率；样本长度参差不齐时建议关闭packing=False。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"学习率LoRA一般设置多大？\"}, {\"role\":\"assistant\",\"content\":\"标准LoRA常用1e‑4 ~3e‑4，不要设置过大，容易震荡发散。\"} ] }, { \"conversation\": [ {\"role\":\"system\",\"content\":\"你是大模型微调助手\"}, {\"role\":\"user\",\"content\":\"训练loss下降就代表微调成功吗？\"}, {\"role\":\"assistant\",\"content\":\"不是。loss下降只代表拟合训练数据，必须在真实问题上做人工评测，同时看验证集loss，防止过拟合。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"可以直接拿网上开源数据集直接训练吗？\"}, {\"role\":\"assistant\",\"content\":\"可以，但要检查字段格式，转换为Qwen ChatML，过滤脏数据，最好混入通用对话数据，避免遗忘基础能力。\"} ] }, { \"conversation\": [ {\"role\":\"user\",\"content\":\"微调之后模型会永久改变吗？\"}, {\"role\":\"assistant\",\"content\":\"LoRA只是附加适配器，基础模型文件不会改动；推理时加载LoRA才生效，移除LoRA就恢复原始基础模型。\"} ] } ] 数据集校验 数据集校验脚本，提前拦截 JSON 语法错误、字段缺失、非法 role 角色；同时调用分词器apply_chat_template渲染对话，检查 ChatML 标签\u003C|im_start|>\u002F\u003C|im_end|>输出是否正常。 很多训练失败根源是数据集格式错误，训练前必须运行该脚本，全部校验通过再进入训练。 import json from transformers import AutoTokenizer JSON_PATH = r\"\u002Froot\u002Fdata\u002Ftrain.json\" MODEL_NAME = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\" if __name__ == \"__main__\": try: with open(JSON_PATH, \"r\", encoding=\"utf‑8\") as f: data = json.load(f) except Exception as e: print(f\"[-] JSON解析失败：{e}\") exit(1) if not isinstance(data, list): print(\"[-] 数据集最外层必须是数组\") exit(1) print(f\"[+] JSON读取成功，总样本数：{len(data)}\") # 加载分词器 tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) print(f\"[+] 成功加载分词器 {MODEL_NAME}\") valid_roles = {\"user\", \"assistant\", \"system\"} error_count = 0 for idx, item in enumerate(data): if \"conversation\" not in item: print(f\"\\n[-] 第{idx}条：缺少 conversation 字段\") error_count += 1 continue conv = item[\"conversation\"] if not isinstance(conv, list): print(f\"\\n[-] 第{idx}条：conversation必须是数组\") error_count += 1 continue for turn_idx, turn in enumerate(conv): if \"role\" not in turn or \"content\" not in turn: print(f\"\\n[-] 第{idx}条‑第{turn_idx}轮：缺少role或content\") error_count += 1 continue r = turn[\"role\"] if r not in valid_roles: print(f\"\\n[-] 第{idx}条‑第{turn_idx}轮：非法role={r}，允许：{valid_roles}\") error_count += 1 print(f\"\\n---------------------------\") if error_count > 0: print(f\"[!] 检测到 {error_count} 处错误，请修改后再训练！\") exit(1) else: print(\"[+] 所有样本字段格式校验通过！\") # 渲染ChatML看标签是否正确 print(\"\\n===== 抽样渲染前2条看ChatML输出 =====\") for i in range(min(2, len(data))): conv = data[i][\"conversation\"] text = tokenizer.apply_chat_template( conv, tokenize=False, add_generation_prompt=False ) print(f\"\\n--------样本{i}--------\") print(text[:600]) if \"\u003C|im_start|>\" not in text: print(\"[!] 警告：输出没有 \u003C|im_start|>，模板异常！\") print(\"\\n[+] 校验完成，可以送入训练脚本。\") 检查通过后可看到如下所示输出内容： root@localhost:~\u002Fdata# python check.py [+] JSON读取成功，总样本数：20 [+] 成功加载分词器 .\u002Fqwen3.5-0.8B-Instruct --------------------------- [+] 所有样本字段格式校验通过！ ===== 抽样渲染前2条看ChatML输出 ===== --------样本0-------- \u003C|im_start|>user 什么是大模型LoRA微调？\u003C|im_end|> \u003C|im_start|>assistant \u003Cthink> \u003C\u002Fthink> LoRA是一种轻量微调技术，冻结大模型主干权重，只训练少量低秩矩阵，不需要全量更新模型参数，显存消耗低，训练速度快，产出的LoRA适配器体积很小，可以挂载在基础模型上使用。\u003C|im_end|> --------样本1-------- \u003C|im_start|>user LoRA和QLoRA有什么区别？\u003C|im_end|> \u003C|im_start|>assistant \u003Cthink> \u003C\u002Fthink> LoRA主干模型使用FP16\u002FBF16精度加载，主干不量化；QLoRA把主干模型4bit量化，进一步降低显存开销，会带来微小的精度损失。\u003C|im_end|> [+] 校验完成，可以送入训练脚本。 分词器加载与验证脚本 验证底座模型分词器加载是否正常，查看特殊 token（\u003C|im_start|>、\u003C|im_end|>、eos\u002Fpad token）ID，验证编码解码、对话模板输出。很多训练 loss 爆炸、生成乱码来自分词器配置错误；Qwen 系列需要把pad_token设置等于eos_token，padding_side=right。 from datasets import load_dataset, concatenate_datasets from transformers import AutoTokenizer,set_seed SEED = 42 set_seed(SEED) MODEL_NAME = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\" if __name__ == \"__main__\": # 加载分词器 tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token tokenizer.padding_side = \"right\" print(\"[+] 成功加载分词器\", MODEL_NAME) print(f\"分词器类名: {type(tokenizer).__name__}\") print(f\"vocab_size: {tokenizer.vocab_size}\") print(f\"model_max_length: {tokenizer.model_max_length}\") print(f\"padding_side: {tokenizer.padding_side}\") print(f\"truncation_side: {tokenizer.truncation_side}\") print(\"\\n---------- 全部特殊token及ID ----------\") special_tokens = [ (\"bos_token\", tokenizer.bos_token, tokenizer.bos_token_id), (\"eos_token\", tokenizer.eos_token, tokenizer.eos_token_id), (\"pad_token\", tokenizer.pad_token, tokenizer.pad_token_id), (\"unk_token\", tokenizer.unk_token, tokenizer.unk_token_id), (\"im_start token\", \"\u003C|im_start|>\", tokenizer.convert_tokens_to_ids(\"\u003C|im_start|>\")), (\"im_end token\", \"\u003C|im_end|>\", tokenizer.convert_tokens_to_ids(\"\u003C|im_end|>\")), ] for name, tok, tid in special_tokens: print(f\"{name:\u003C15} token={repr(tok):\u003C22} id={tid}\") print(\"\\n---------- special_tokens_map ----------\") print(tokenizer.special_tokens_map) #print(\"\\n---------- init_kwargs 初始化参数 ----------\") #print(tokenizer.init_kwargs) print(\"\\n---------- 词表前30个样例 ----------\") vocab = list(tokenizer.get_vocab().items())[:30] for token_str, token_id in vocab: print(f\"{token_id:6d} | {repr(token_str)}\") print(\"\\n---------- 简单编码解码测试 ----------\") test_sentence = \"你好，Qwen大模型！\" encode_out = tokenizer(test_sentence) print(f\"原文：{test_sentence}\") print(f\"input_ids：{encode_out['input_ids']}\") print(f\"还原：{tokenizer.decode(encode_out['input_ids'])}\") print(\"\\n---------- apply_chat_template对话模板测试 ----------\") messages = [ {\"role\":\"system\",\"content\":\"你是助手\"}, {\"role\":\"user\",\"content\":\"你好\"} ] chat_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) print(\"chat template输出文本：\") print(repr(chat_text)) 检查通过后可看到如下所示输出内容： 重点检查：pad_token不为 None，padding_side=right，apply_chat_template输出包含正确 im 标签。 root@localhost:~\u002Fdata# python check.py [+] 成功加载分词器 分词器类名: Qwen2Tokenizer vocab_size: 248044 model_max_length: 262144 padding_side: right truncation_side: right ---------- 全部特殊token及ID ---------- bos_token token=None id=None eos_token token='\u003C|im_end|>' id=248046 pad_token token='\u003C|im_end|>' id=248046 unk_token token=None id=None im_start token token='\u003C|im_start|>' id=248045 im_end token token='\u003C|im_end|>' id=248046 ---------- special_tokens_map ---------- {'eos_token': '\u003C|im_end|>', 'pad_token': '\u003C|im_end|>', 'audio_bos_token': '\u003C|audio_start|>', 'audio_eos_token': '\u003C|audio_end|>', 'audio_token': '\u003C|audio_pad|>', 'image_token': '\u003C|image_pad|>', 'video_token': '\u003C|video_pad|>', 'visi on_bos_token': '\u003C|vision_start|>', 'vision_eos_token': '\u003C|vision_end|>'} ---------- 词表前30个样例 ---------- 222609 | 'ãģ¨æĢĿãģ£ãģŁãĤī' 150945 | 'ĠÐ´Ñĥ' 101644 | 'ä¹Łå°±æĺ¯' 38671 | 'Ġcurse' 173593 | 'Ð½Ð°Ð·Ð½Ð°' 126060 | 'åīįä¸įä¹ħ' 164288 | 'hÃ£o' 136874 | 'éĢĢä¼ĳäººåĳĺ' 125605 | 'ä¸ĢäºĽå°ı' 148886 | 'à®³' 228510 | 'Ġentregue' 184351 | 'Ġinklus' 167525 | 'ĠØ§ÙĦÙħØµÙĨ' 176754 | 'Ġtratamento' 57152 | 'calar' 187517 | 'ÑģÐ½Ñĥ' 3978 | 'ĠOff' 42853 | 'Ġrecruited' 170936 | 'Ġbiá»ĥn' 6770 | 'ometry' 201897 | 'Ġmaintenir' 21855 | 'ĠSure' 144957 | 'åĩĨèĢĥè¯ģæīĵåį°' 66583 | '(seg' 81516 | 'ulado' 125493 | 'ç©ºç©º' 832 | 'arg' 146557 | 'èı²èı²' 173079 | 'ĠTÆ°' 83695 | 'ĉJson' ---------- 简单编码解码测试 ---------- 原文：你好，Qwen大模型！ input_ids：[109266, 3709, 48, 16451, 95779, 103725, 6115] 还原：你好，Qwen大模型！ ---------- apply_chat_template对话模板测试 ---------- chat template输出文本： '\u003C|im_start|>system\\n你是助手\u003C|im_end|>\\n\u003C|im_start|>user\\n你好\u003C|im_end|>\\n\u003C|im_start|>assistant\\n\u003Cthink>\\n\\n\u003C\u002Fthink>\\n\\n' LoRA‑SFT 训练脚本 加载本地 Qwen3.5‑0.8B‑Instruct 基座模型，冻结主干，使用 LoRA 做轻量化微调；读取本地 json 对话数据集，经过 chat 模板格式化、分词、过滤、划分训练验证集；使用 Trainer 执行 step 式训练，保存 LoRA 适配器，最后加载 LoRA 做简单推理验证。 import os import torch from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, set_seed, DataCollatorForLanguageModeling, Trainer ) from peft import PeftModel, LoraConfig, get_peft_model import warnings warnings.filterwarnings(\"ignore\") SEED = 42 set_seed(SEED) # 配置路径 LOCAL_JSON_PATH = r\"\u002Froot\u002Fdata\u002Ftrain.json\" MODEL_NAME = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\" OUTPUT_DIR = r\"\u002Froot\u002Fdata\u002Fqwen_lora\" MAX_SEQ_LENGTH = 8192 # LoRA超参 LORA_R = 16 LORA_ALPHA = 16 LORA_DROPOUT = 0.0 # RTX4090‑24G 8192上下文 调参 BATCH_SIZE_PER_DEVICE = 2 GRADIENT_ACCUMULATION_STEPS = 4 MAX_STEPS = 40 LEARNING_RATE = 1e-4 WARMUP_STEPS = 10 # 显存优化 GRADIENT_CHECKPOINTING = True # 4090+torch2.13优先bfloat16 比fp16更稳 USE_BF16 = True def format_conversation(sample): conv = sample[\"conversation\"] text = tokenizer.apply_chat_template( conv, tokenize=False, add_generation_prompt=False ) return {\"text\": text} def tokenize_fn(sample): out = tokenizer( sample[\"text\"], truncation=True, max_length=MAX_SEQ_LENGTH, ) return out def filter_long_sample(sample): return len(sample[\"input_ids\"]) \u003C MAX_SEQ_LENGTH if __name__ == \"__main__\": print(f\"PyTorch version: {torch.__version__}\") print(f\"CUDA available: {torch.cuda.is_available()}\") print(f\"CUDA version: {torch.version.cuda}\") print(f\"GPU count: {torch.cuda.device_count()}\") if torch.cuda.is_available(): print(f\"GPU Name: {torch.cuda.get_device_name(0)}\") print(f\"Use bf16: {USE_BF16}\") # 路径校验 print(f\"\\n[CHECK] MODEL_NAME={MODEL_NAME}\") if not os.path.isdir(MODEL_NAME): raise FileNotFoundError(f\"模型文件夹不存在：{MODEL_NAME}，检查真实路径！\") if not os.path.exists(os.path.join(MODEL_NAME, \"config.json\")): raise FileNotFoundError(f\"{MODEL_NAME} 缺少config.json，不是完整模型目录\") print(\"[CHECK] 模型目录校验通过\") if not os.path.exists(LOCAL_JSON_PATH): raise FileNotFoundError(f\"数据集文件不存在：{LOCAL_JSON_PATH}\") print(\"[CHECK] 数据集文件校验通过\\n\") # 加载分词器 tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token tokenizer.padding_side = \"right\" print(\"[+] 成功加载分词器\", MODEL_NAME) # 加载模型 单卡强制0号卡 model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, dtype=torch.bfloat16 if USE_BF16 else torch.float16, device_map={\"\": 0}, trust_remote_code=True, ) for param in model.parameters(): param.requires_grad = False print(\"[+] 主干模型加载完成 已冻结\") if GRADIENT_CHECKPOINTING: model.gradient_checkpointing_enable() model.enable_input_require_grads() model.config.use_cache = False print(\"[+] 开启梯度检查点 显存优化\") # LoRA配置 lora_config = LoraConfig( r=LORA_R, lora_alpha=LORA_ALPHA, target_modules=[ \"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\" ], lora_dropout=LORA_DROPOUT, bias=\"none\", task_type=\"CAUSAL_LM\", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # 数据集加载 raw_ds = load_dataset(\"json\", data_files=LOCAL_JSON_PATH, split=\"train\") print(f\"原始数据集样本数量: {len(raw_ds)}\") raw_ds = raw_ds.map(format_conversation, num_proc=8) raw_ds = raw_ds.map(tokenize_fn, num_proc=8) raw_ds = raw_ds.filter(filter_long_sample) raw_ds = raw_ds.select_columns([\"input_ids\", \"attention_mask\"]) raw_ds = raw_ds.shuffle(seed=SEED) split_ds = raw_ds.train_test_split(test_size=0.05, seed=SEED) train_ds = split_ds[\"train\"] eval_ds = split_ds[\"test\"] print(f\"train:{len(train_ds)}, eval:{len(eval_ds)}\") print(\"数据集字段：\", train_ds.column_names) data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=False ) training_args = TrainingArguments( output_dir=OUTPUT_DIR, per_device_train_batch_size=BATCH_SIZE_PER_DEVICE, per_device_eval_batch_size=1, gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS, warmup_steps=WARMUP_STEPS, max_steps=MAX_STEPS, learning_rate=LEARNING_RATE, bf16=USE_BF16, fp16=not USE_BF16, logging_steps=5, optim=\"adamw_torch\", weight_decay=0.01, lr_scheduler_type=\"linear\", seed=SEED, report_to=\"none\", eval_strategy=\"steps\", eval_steps=20, save_strategy=\"steps\", save_steps=20, save_total_limit=3, load_best_model_at_end=True, prediction_loss_only=True, push_to_hub=False, save_only_model=True, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_ds, eval_dataset=eval_ds, data_collator=data_collator, ) print(\"[+] 开始训练......\") trainer.train() trainer.save_model(OUTPUT_DIR) tokenizer.save_pretrained(OUTPUT_DIR) print(f\"训练完成，LoRA适配器输出路径：{OUTPUT_DIR}\") # 推理验证测试 base_model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, dtype=torch.bfloat16, device_map={\"\": 0}, trust_remote_code=True ) lora_model = PeftModel.from_pretrained(base_model, OUTPUT_DIR) messages = [{\"role\":\"user\", \"content\":\"你好\"}] prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(prompt, return_tensors=\"pt\").to(\"cuda\") outputs = lora_model.generate(**inputs, max_new_tokens=100) print(tokenizer.decode(outputs[0], skip_special_tokens=True)) 训练结束输出产物：\u002Froot\u002Fdata\u002Fqwen_lora，里面是 LoRA 适配器，体积很小（几十 MB）不是完整模型；包含adapter_model.safetensors、adapter_config.json以及 checkpoint 中间快照。 root@localhost:~\u002Fdata# python qwen_lora.py PyTorch version: 2.6.0+cu124 CUDA available: True CUDA version: 12.4 GPU count: 1 GPU Name: NVIDIA GeForce RTX 4090 Use bf16: True [CHECK] MODEL_NAME=\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct [CHECK] 模型目录校验通过 [CHECK] 数据集文件校验通过 [+] 成功加载分词器 \u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct Loading weights: 100%|█████████████████████████████████████████| 320\u002F320 [00:00\u003C00:00, 1021.22it\u002Fs] [+] 主干模型加载完成，已冻结 [+] 开启梯度检查点，显存优化 trainable params: 6,389,760 || all params: 758,782,784 || trainable%: 0.8421 原始数据集样本数量: 20 train:19, eval:1 数据集字段： ['input_ids', 'attention_mask'] [+] 开始训练...... {'loss': '3.747', 'grad_norm': '4.377', 'learning_rate': '4e-05', 'epoch': '1.8'} {'loss': '3.275', 'grad_norm': '2.973', 'learning_rate': '9e-05', 'epoch': '3.4'} {'loss': '2.823', 'grad_norm': '2.982', 'learning_rate': '8.667e-05', 'epoch': '5'} {'loss': '2.292', 'grad_norm': '2.099', 'learning_rate': '7e-05', 'epoch': '6.8'} {'eval_loss': '2.52', 'eval_runtime': '0.1862', 'eval_samples_per_second': '5.371', 'eval_steps_per_second': '5.371', 'epoch': '6.8'} {'loss': '2.01', 'grad_norm': '2.022', 'learning_rate': '5.333e-05', 'epoch': '8.4'} {'loss': '1.762', 'grad_norm': '3.386', 'learning_rate': '3.667e-05', 'epoch': '10'} {'loss': '1.535', 'grad_norm': '2.145', 'learning_rate': '2e-05', 'epoch': '11.8'} {'loss': '1.426', 'grad_norm': '2.224', 'learning_rate': '3.333e-06', 'epoch': '13.4'} {'eval_loss': '2.717', 'eval_runtime': '0.1921', 'eval_samples_per_second': '5.204', 'eval_steps_per_second': '5.204', 'epoch': '13.4'} {'train_runtime': '144.7', 'train_samples_per_second': '2.211', 'train_steps_per_second': '0.276', 'train_loss': '2.359', 'epoch': '13.4'} 100%|███████████████████████████████████████████████████| 40\u002F40 [02:24\u003C00:00, 3.62s\u002Fit] 训练完成，LoRA适配器输出路径：\u002Froot\u002Fdata\u002Fqwen_lora =====推理验证===== Loading weights: 100%|██████████████████████████████████| 320\u002F320 [00:00\u003C00:00, 1011.21it\u002Fs] user 你好 assistant \u003Cthink> \u003C\u002Fthink> 你好！有什么我可以帮你的吗？ root@localhost:~\u002Fdata# cd qwen_lora\u002F root@localhost:~\u002Fdata\u002Fqwen_lora# ll total 44556 drwxr-xr-x 4 root root 4096 Sep 5 23:33 .\u002F drwxr-xr-x 5 root root 189 Sep 5 23:25 ..\u002F -rw-r--r-- 1 root root 5218 Sep 5 23:33 README.md -rw-r--r-- 1 root root 1164 Sep 5 23:33 adapter_config.json -rw------- 1 root root 25584224 Sep 5 23:33 adapter_model.safetensors -rw-r--r-- 1 root root 7755 Sep 5 23:33 chat_template.jinja drwxr-xr-x 2 root root 4096 Sep 5 23:31 checkpoint-20\u002F drwxr-xr-x 2 root root 4096 Sep 5 23:33 checkpoint-40\u002F -rw-r--r-- 1 root root 19989325 Sep 5 23:33 tokenizer.json -rw-r--r-- 1 root root 1124 Sep 5 23:33 tokenizer_config.json -rw-r--r-- 1 root root 4792 Sep 5 23:33 training_args.bin 适配器合并完整权重 LoRA 适配器不能直接用于 llama.cpp、GGUF 导出；需要把 LoRA 权重合并进底座模型权重，输出完整 HF 格式模型。 脚本merge_lora.py将 LoRA 低秩矩阵和基础模型权重矩阵做矩阵相加，输出完整独立模型，不再依赖 peft 库；合并后可以直接 vLLM 推理，也可以转 GGUF。 import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer BASE_MODEL_PATH = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\" LORA_PATH = r\"\u002Froot\u002Fdata\u002Fqwen_lora\" MERGED_OUT = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged\" print(\"加载基础模型...\") base = AutoModelForCausalLM.from_pretrained( BASE_MODEL_PATH, dtype=torch.bfloat16, device_map={\"\": 0}, trust_remote_code=True ) print(\"加载LoRA适配器并合并...\") model = PeftModel.from_pretrained(base, LORA_PATH) merged_model = model.merge_and_unload() print(\"保存合并后的完整模型\") merged_model.save_pretrained(MERGED_OUT, safe_serialization=True) tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_PATH, trust_remote_code=True) tokenizer.save_pretrained(MERGED_OUT) print(f\"合并完成，输出目录：{MERGED_OUT}\") 输出目录\u002Froot\u002Fdata\u002Fqwen3.5‑0.8B‑lora‑merged root@localhost:~\u002Fdata# python lora‑merged.py 加载基础模型... Loading weights: 100%|█████████████████████████████████████████████████| 320\u002F320 [00:00\u003C00:00, 918.63it\u002Fs] 加载LoRA适配器并合并... 保存合并后的完整模型 Writing model shards: 100%|██████████████████████████████████████████| 1\u002F1 [00:01\u003C00:00, 1.98s\u002Fit] 合并完成，输出目录：\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged root@localhost:~\u002Fdata# cd qwen3.5-0.8B-Instruct\u002F root@localhost:~\u002Fdata\u002Fqwen3.5-0.8B-Instruct# ls -lh total 1.7G -rw-r--r-- 1 root root 1.4K Sep 5 23:12 README.md -rw-r--r-- 1 root root 2.9K Sep 5 23:12 config.json -rw-r--r-- 1 root root 48 Sep 5 23:12 configuration.json -rw-r--r-- 1 root root 1.7G Sep 5 23:17 model.safetensors -rw-r--r-- 1 root root 336 Sep 5 23:12 preprocessor_config.json -rw-r--r-- 1 root root 13M Sep 5 23:12 tokenizer.json -rw-r--r-- 1 root root 17K Sep 5 23:12 tokenizer_config.json root@localhost:~\u002Fdata\u002Fqwen_lora# ls -lh total 44M -rw-r--r-- 1 root root 5.1K Sep 5 23:33 README.md -rw-r--r-- 1 root root 1.2K Sep 5 23:33 adapter_config.json -rw------- 1 root root 25M Sep 5 23:33 adapter_model.safetensors -rw-r--r-- 1 root root 7.6K Sep 5 23:33 chat_template.jinja drwxr-xr-x 2 root root 4.0K Sep 5 23:31 checkpoint-20 drwxr-xr-x 2 root root 4.0K Sep 5 23:33 checkpoint-40 -rw-r--r-- 1 root root 20M Sep 5 23:33 tokenizer.json -rw-r--r-- 1 root root 1.1K Sep 5 23:33 tokenizer_config.json -rw-r--r-- 1 root root 4.7K Sep 5 23:33 training_args.bin root@localhost:~\u002Fdata\u002Fqwen3.5-0.8B-lora-merged# ls -lh total 1.5G -rw-r--r-- 1 root root 7.6K Sep 5 23:37 chat_template.jinja -rw-r--r-- 1 root root 1.8K Sep 5 23:37 config.json -rw-r--r-- 1 root root 116 Sep 5 23:37 generation_config.json -rw------- 1 root root 1.5G Sep 5 23:37 model.safetensors -rw-r--r-- 1 root root 20M Sep 5 23:37 tokenizer.json -rw-r--r-- 1 root root 1.1K Sep 5 23:37 tokenizer_config.json 完整模型推理测试 不依赖 peft 库，直接加载合并完成完整 HF 模型，验证对话生成效果；调用apply_chat_template构建 prompt，设置 temperature、top_p 采样参数做生成。 import torch from transformers import AutoModelForCausalLM, AutoTokenizer MODEL_PATH = r\"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged\" # 加载完整合并模型，不需要peft model = AutoModelForCausalLM.from_pretrained( MODEL_PATH, dtype=torch.bfloat16, device_map={\"\": 0}, trust_remote_code=True ) tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True) def chat(prompt_text): messages = [ {\"role\": \"user\", \"content\": prompt_text} ] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = tokenizer(text, return_tensors=\"pt\").to(\"cuda\") outputs = model.generate( **inputs, max_new_tokens=200, temperature=0.7, top_p=0.8, do_sample=True ) return tokenizer.decode(outputs[0], skip_special_tokens=True) if __name__ == \"__main__\": print(\"====测试1====\") res1 = chat(\"你好\") print(res1) print(\"\\n====测试2====\") res2 = chat(\"简单介绍一下人工智能\") print(res2) 运行后输出测试效果如下： root@localhost:~\u002Fdata# python test.py Loading weights: 100%|██████████████████████████████████████████████| 320\u002F320 [00:00\u003C00:00, 1111.48it\u002Fs] ====测试1==== user 你好 assistant \u003Cthink> \u003C\u002Fthink> 你好！有什么可以帮你的吗？ ====测试2==== user 简单介绍一下人工智能 assistant \u003Cthink> \u003C\u002Fthink> 人工智能是让计算机模仿人类智能，实现自主决策、学习和适应的能力。 底座和LoRA合并模型对比测试，同时加载原始底座模型、微调合并后的模型，输入同一个问题对比输出，直观确认微调是否生效。 import torch from transformers import AutoModelForCausalLM, AutoTokenizer BASE = \"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-Instruct\" MERGED = \"\u002Froot\u002Fdata\u002Fqwen3.5-0.8B-lora-merged\" tokenizer = AutoTokenizer.from_pretrained(BASE, trust_remote_code=True) def get_answer(model_path, question): model = AutoModelForCausalLM.from_pretrained( model_path, dtype=torch.bfloat16, device_map={\"\":0}, trust_remote_code=True ) messages = [{\"role\":\"user\",\"content\":question}] prompt = tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=True) inputs = tokenizer(prompt,return_tensors=\"pt\").to(\"cuda\") out = model.generate(**inputs,max_new_tokens=150,temperature=0.7) ans = tokenizer.decode(out[0],skip_special_tokens=True) del model torch.cuda.empty_cache() return ans q = \"你好，什么是大模型LoRA微调？\" print(\"【原始底座回答】\") print(get_answer(BASE,q)) print(\"\\n【LoRA合并后回答】\") print(get_answer(MERGED,q)) 运行后输出测试效果如下： 现象：底座回答偏向通用百科；微调后的输出风格、话术会贴近训练集的回答内容。 root@localhost:~\u002Fdata# python test.py 【原始底座回答】 Loading weights: 100%|████████████████████████████████████████████████| 320\u002F320 [00:00\u003C00:00, 867.20it\u002Fs] [transformers] The following generation flags are not valid and may be ignored: ['temperature']. Set `TRANSFORMERS_VERBOSITY=info` for more details. user 你好，什么是大模型LoRA微调？ assistant \u003Cthink> \u003C\u002Fthink> 你好！大模型（LLM）的 **LoRA（Low-Rank Adaptation）** 微调是一种高效、轻量级的模型微调技术，旨在在不显著影响模型整体性能的前提下，大幅降低计算成本。 简单来说，LoRA 的核心思想是：**只修改模型中“低秩”的部分，而保留“高秩”（即模型的核心能力）不变。** 以下是关于 LoRA 微调的详细解析： ### 1. 核心原理：低秩分解 LoRA 的精髓在于**低秩分解（Low-Rank Decomposition, LRD）**。 * **高秩矩阵**：代表模型中已经学习好的核心能力（如语言理解、逻辑推理、代码生成等 【LoRA合并后回答】 Loading weights: 100%|███████████████████████████████████████████████████| 320\u002F320 [00:00\u003C00:00, 1027.28it\u002Fs] user 你好，什么是大模型LoRA微调？ assistant \u003Cthink> \u003C\u002Fthink> LoRA是LoRA适配器，只加载少量权重，训练时只加载少量权重，训练速度更快，推理速度更快，适合微调大模型。",25906,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":16,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":40},"2026 · 人工智能","#2563eb","16 \u002F 10",[19],{"targetType":8,"targetId":9,"likedByMe":42,"likeCount":43,"commentCount":43,"contentLikeCount":43,"contentCommentCount":43,"sourceLikeCount":43,"sourceCommentCount":43},false,0,[45,52,59,66,73,80,86,93],{"id":46,"kind":7,"title":47,"summary":48,"image":49,"href":50,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":51},"NEWS_ARTICLE:930","基于 vLLM+Nginx 构建负载均衡推理集群","企业内部私有环境部署大模型推理集群时，很容易遇到流量调度混乱、节点负载失衡、会话上下文丢失、接口缺少鉴权防护等一系列问题，单 vLLM 推理节点难以支撑并发请求。本文基于 Ubuntu 22.04 系统环境，搭建 Nginx + vLLM-Semantic-Router + vLLM-Router","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1379525\u002F202609\u002F1379525-20260910165534385-2022408098.png","\u002Fnews\u002F930",[19],{"id":53,"kind":7,"title":54,"summary":55,"image":56,"href":57,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":58},"NEWS_ARTICLE:932","2026年AI编程工具大全，33个主流工具一次看懂","事情是这样的，前两天看到一张图，是某个社区官网的「支持的工具」清单，我数了数，整整31个AI编程工具。 两年前这份清单撑死5个，现在直接31个，而且我居然每一个都认识。。。 干脆整理成一篇，顺手把最近字节的TraeWork和豆包工作也补了进来，凑成33个。 今天给大家推荐一遍，每个工具说说它是干什么","https:\u002F\u002Fimage.kjdaohang.com\u002Fimg\u002F20260909210838518.png","\u002Fnews\u002F932",[19],{"id":60,"kind":7,"title":61,"summary":62,"image":63,"href":64,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":65},"NEWS_ARTICLE:935","[Agent Memory \u002F 强化学习] MemPO源码学习笔记 ---（1）--- 总体","[Agent Memory \u002F 强化学习] MemPO源码学习笔记 （1） 总体 目录[Agent Memory \u002F 强化学习] MemPO源码学习笔记 （1） 总体0x00 概要0x01 基础 &amp; 背景1.1 用RL训练记忆系统的要点1.2 主要难点1.3 主要思路1.4 RL训练方案1.","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1850883\u002F202609\u002F1850883-20260906190854637-986949885.jpg","\u002Fnews\u002F935",[19],{"id":67,"kind":7,"title":68,"summary":69,"image":70,"href":71,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":72},"NEWS_ARTICLE:940","LLama-Factory 实现大模型LoRA-SFT微调指南","LLaMA Factory 是一款开源、低代码、一站式大语言与多模态模型微调框架，用于降低大模型的微调落地门槛。框架兼容 Qwen、LLaMA、ChatGLM、LLaVA 等上百款主流开源模型，支持增量预训练、SFT 监督微调、DPO、KTO、ORPO 等多种训练对齐方案，原生集成 LoRA、QLo","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1379525\u002F202609\u002F1379525-20260909140111199-832484524.png","\u002Fnews\u002F940",[19],{"id":74,"kind":7,"title":75,"summary":76,"image":77,"href":78,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":79},"NEWS_ARTICLE:950","每天白嫖 WorkBuddy 100 积分，我让WorkBuddy自己领","有没有小伙伴跟我一样，每天都去白嫖 WorkBuddy 的 100 积分，每天都怕忘记领。 其实我早就开了会员，但还是会有积分焦虑，天天惦记着今天的积分领没领。后来我发现 WorkBuddy 可以自己领积分，今天把这个技能分享给小伙伴们。 Buddy 加油站每天签到领 100 积分，连续签满 7 天","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F2381533\u002F202609\u002F2381533-20260910080820041-1607850292.png","\u002Fnews\u002F950",[19],{"id":81,"kind":7,"title":82,"summary":83,"image":15,"href":84,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":85},"NEWS_ARTICLE:949","AI知识库，是捷径吗？","从信息化到数字化，再到当下的智能化，技术进步带来的效率提升，如果往好处想应该是：节省更多的时间和成本，用来做更多的事情。但从现实的角度看，节流比开源来得容易。","\u002Fnews\u002F949",[19],{"id":87,"kind":7,"title":88,"summary":89,"image":90,"href":91,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":92},"NEWS_ARTICLE:977","【OpenClaw具身硬件】ZeroClaw 源码阅读笔记（4）--- 代码执行","【OpenClaw具身硬件】ZeroClaw 源码阅读笔记（4） 代码执行 目录【OpenClaw具身硬件】ZeroClaw 源码阅读笔记（4） 代码执行0x00 概要0x01 代码合成1.1 核心思想1.2 业务逻辑具体应用场景能力体现1.3 实现细节生成种类Rust 代码Arduino CLI的","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F1850883\u002F202608\u002F1850883-20260823212850242-1723558087.png","\u002Fnews\u002F977",[19],{"id":94,"kind":7,"title":95,"summary":96,"image":97,"href":98,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":99},"NEWS_ARTICLE:985","机器学习项目：客户分群——K-Means 聚类从选参到业务画像的完整实战","商场用户分群： 一、前言 在商场运营中，面对成千上万的顾客，如果用同一套营销策略对待所有人，效果往往事倍功半。高收入的中年人和月光族的年轻人等等群体，消费习惯和偏好截然不同——这就需要用户分群（Customer Segmentation）：根据用户的年龄、收入、消费行为等特征，将相似的用户归为一类，","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F3775135\u002F202609\u002F3775135-20260908154332888-1132576367.png","\u002Fnews\u002F985",[19]]