首页
/ textgen 训练指南:在 Training 标签页中完成 LoRA 微调、指令模板配置与 Loss 调优

textgen 训练指南:在 Training 标签页中完成 LoRA 微调、指令模板配置与 Loss 调优

2026-09-05 13:31:36作者:申梦珏Efrain

本篇基于 textgen(开源本地 LLM 桌面应用,支持文本、视觉、工具调用与 OpenAI/Anthropic 兼容 API)的 Training 标签页文档与源码实现,系统讲解如何在本应用中训练自己的 LoRA 适配器:从数据集格式(OpenAI messages / ShareGPT / 纯文本)、指令模板(Jinja2 apply_chat_template())的两种来源,到 Rank、学习率、批大小等关键参数的取值依据、断点续训机制与 Loss 曲线的判读方法。读完本文,你可以独立完成一次 LoRA 训练任务,理解每个参数在源码中的实际作用,并能诊断训练异常。

前提与核心约束:LoRA 与模型架构绑定

一条最重要的规则:LoRA 是绑定特定模型架构的——在 Llama 3 8B 上训练的 LoRA 无法用在 Mistral 7B 上,必须在你计划使用的确切模型上训练。

从源码看,这个约束还有两个前置条件,modules/training.py 中的校验逻辑可以直接看到:

  1. 必须使用 Transformers 加载器。训练入口 do_train() 首先检查加载器类型,若当前模型由 llama.cpp(GGUF)加载,会直接报错退出:"LoRA training requires a model loaded with the Transformers loader. GGUF models are not supported for training."
  2. 训练前不应已挂载其他 LoRA。若模型已是 PeftModelForCausalLM,源码会给出警告("Training LoRA over top of another LoRA. May have unexpected effects.")并等待 5 秒让你按 Interrupt 中止(见 modules/training.py)。

此外,训练开始前源码会做一些隐式处理(modules/training.py):

  • 梯度累积步数由 gradient_accumulation_steps = batch_size // micro_batch_size 计算;
  • 若 tokenizer 没有 pad_token,会回退使用 eos_token_id 充当 pad token,并统一设置为右侧填充(padding_side = "right")。

Quick Start:五分钟完成一次 LoRA 训练

文档给出的快速上手流程如下,共五步:

  1. Transformers 加载器加载基础模型(不加载任何 LoRA)。
  2. 打开 Training 标签页 > Train LoRA
  3. 选择数据集并配置参数(见下文 Parameters 一节)。
  4. 点击 Start LoRA Training,监控 Loss
  5. 训练完成后,在 Models 标签页加载该 LoRA 并测试。

点击开始按钮后,UI 事件会绑定到 do_train(),其完整签名接收 34 个参数(modules/training.py),覆盖模块选择、批参数、优化器、数据集与模板等全部配置。训练在独立线程中运行(trainer.train(resume_from_checkpoint=...)),主线程负责向 WebUI 推送进度(当前步数/总步数、it/s 速率、剩余时间估算),并响应 Interrupt。

训练完成后的注意事项

训练结束时源码会提示:在测试新 LoRA 之前必须先重新加载模型,因为基础模型已被本次训练修改(shared.model_dirty_from_training 标志会被置位,见 modules/training.py)。如果上一次训练(哪怕是失败的)改脏了模型,下一次训练前源码会自动先执行 reload_model() 重载基础模型(modules/training.py)。

Resuming Training:断点续训机制

文档说明:要从中断点恢复训练,使用相同的 LoRA 名称并取消勾选 Override Existing Files。若存在检查点(来自 Save every n steps),训练会自动从最新检查点恢复,完整保留优化器与调度器状态。注意:已创建 LoRA 的 Rank 不可更改

源码实现印证了这一流程(modules/training.py):

  • 当未勾选覆盖且目标目录已存在时,代码会按修改时间排序扫描 checkpoint-* 目录,取最新者作为 resume_checkpoint,传给 HuggingFace Trainer 的 trainer.train(resume_from_checkpoint=resume_checkpoint),这就是"完整恢复优化器/调度器状态"的来源;
  • 若没有检查点目录,则走旧版回退路径:只加载裸适配器权重(adapter_model.safetensorsadapter_model.bin),不恢复优化器状态。

文档还建议同时使用 UI 顶部的 Copy parameters from 下拉框,从上次运行复制 UI 设置(学习率、epochs 等)。其实现是读取该 LoRA 目录下自动保存的 training_parameters.json(由 do_copy_params() 完成),逐参数回填到界面——因此每次训练开始时,当前 34 个参数都会写入该文件备查(modules/training.py)。

另一个容易忽略的保护机制:取消勾选 Override Existing Files 且目录下已存在旧适配器时,backup_adapter() 会按适配器文件的创建日期建立 Backup-YYYY-MM-DD 子文件夹并整目录备份,避免旧成果被覆盖。

Instruction Templates:两种模板来源,同一条分词路径

所有指令/聊天训练都使用 apply_chat_template() 与 Jinja2 模板。Instruction Template 下拉框有两类选项(get_instruction_templates()):

  • Chat Template:使用模型 tokenizer 自带的 chat template。适用于出厂即带聊天模板的 instruct/chat 模型(Llama 3、Qwen、Mistral 等)。若模型 tokenizer 没有 chat_template,源码会直接报错,提示改选命名模板或加载 instruct 模型(modules/training.py)。
  • Named template(ChatML、Alpaca、Llama-v3 等):从 user_data/instruction-templates/ 加载 Jinja2 模板文件。适合没有内置模板的基础模型,或需要覆盖模型默认模板的场景。

两种方式在功能上完全等价——唯一区别是 Jinja2 模板字符串的来源。源码上,命名模板会被赋给 shared.tokenizer.chat_templatemodules/training.py),随后两种情况走同一条分词路径。在两种情况下:

  • 数据集经 apply_chat_template() 分词;
  • labels 自动掩码,只对 assistant 回复部分计算损失
  • 原生支持多轮对话;
  • 特殊 token 由模板正确处理。

assistant-only 掩码的具体实现

tokenize_conversation()modules/training.py)的做法值得细看:先把完整消息序列分词得到 full_ids,labels 初始化为全 -100(即全部忽略);然后对每个 role == "assistant" 的消息,分别计算 apply_chat_template(messages[:i], add_generation_prompt=True)(该回复开始前的 token 数,即 start)与 apply_chat_template(messages[:i+1])(到该回复结束,即 end),把 full_ids[start:end] 原样写回 labels。这样 loss 只落在 assistant 生成的 token 上。注释指出该前缀假设对所有标准聊天模板(Llama、ChatML、Mistral 等)成立。

超长会话由 Excess length 参数控制,默认 drop(整条丢弃,推荐),可选 truncate(从右侧截断,可能产生不完整回复)。被丢弃的条数会以警告形式打印:Dropped {dropped}/{total} conversations exceeding cutoff length of {cutoff_len} tokens.modules/training.py)。

模板文件从哪里来

user_data/instruction-templates/ 目录中预置了一批命名模板,例如 ChatML.yamlAlpaca.yamlLlama-v3.yamlMistral.yaml、Open Assistant.yaml、Vicuna-v1.1.yaml。以 ChatML 为例,其内容是一个标准 Jinja2 模板:

{%- for message in messages %}
    {%- if message['role'] == 'system' -%}
        {{- '系统\n' + message['content'].rstrip() + '\n' -}}
    {%- elif message['role'] == 'user' -%}
        {{-'用户\n' + message['content'].rstrip() + '\n'-}}
    {%- else -%}
        {{-'助手\n' + message['content'] + '\n' -}}
    {%- endif -%}
{%- endfor %}
{%- if add_generation_prompt -%}
    {{-'助手\n'-}}
{%- endif %}

(以上为示意;实际文件使用 `` / `

登录后查看全文
热门项目推荐
相关项目推荐