tiny-gpt2Qwen:你改了哪些代码

这份说明文档的目标,不是重复介绍 LoRA 的原理,而是专门回答一个更实际的问题:

course_lora-tinygpt2-src 的基础上,把模型换成 Qwen/Qwen2.5-0.5B-Instruct 时,你到底改了哪些地方?这些改动为什么必要?

整体上看,这次修改不是简单把模型名替换一下,而是做了 6 类同步调整:

  1. 把基础模型从 sshleifer/tiny-gpt2 换成了 Qwen/Qwen2.5-0.5B-Instruct
  2. 把训练数据从 instruction / input / output 三段式改成了 messages 对话格式
  3. 把文本拼接方式从手写字符串改成了 tokenizer.apply_chat_template(...)
  4. 把 LoRA 的挂载目标层改成了 Qwen 结构对应的投影层
  5. 把训练参数改成更适合 0.5B 指令模型的小显存设置
  6. 把推理 prompt 也改成了 chat 模型常用的 system + user 形式

可以把这理解成一句话:

你不是“把 tiny 模型换成大一点的模型”,而是把整套实验从“普通 causal LM 演示”升级成了“面向指令模型的 LoRA 最小实验”。


一、目录层面的变化

course_lora-tinygpt2-src 相比,course_lora_qwen_src 有几个明显变化:

为什么会这样?

所以这次修改,其实也改变了整个实验包的使用方式。


二、00_check_env.ipynb 改了什么

这个 notebook 的目标,从“检查 tiny-gpt2 课程目录是否完整”改成了“检查 Qwen 能不能被下载和加载”。

Cell 1:标题和目标

# 00_check_env
检查 Python、Torch、CUDA、GPU、代理和模型下载环境。

这里最关键的变化是:你把“环境检查”的重点从本地课程包转向了 联网下载 Qwen 模型

Cell 2:更简洁的系统信息输出

import os, sys, platform
print(sys.version)
print(platform.platform())
print('PWD:', os.getcwd())

和原版相比:

为什么这样改:

Cell 3:直接调用 nvidia-smi

!nvidia-smi || true

原版是用 subprocess.run() 手动调用。你这里改成了 notebook 里更直接的 shell 命令。

逐项解释:

为什么这样改:

Cell 4:保留 torch / CUDA 检查,但更紧凑

import torch
print('torch:', torch.__version__)
print('cuda available:', torch.cuda.is_available())
print('device count:', torch.cuda.device_count())
if torch.cuda.is_available():
    print('device:', torch.cuda.get_device_name(0))

和原版相比,这一段逻辑没有本质变化,但表达更简短。

这说明你的目标不是改功能,而是降低学生阅读负担。

Cell 5:重点检查代理和 Hugging Face 相关环境变量

import os
for k in ['http_proxy','https_proxy','HTTP_PROXY','HTTPS_PROXY','HF_ENDPOINT','REQUESTS_CA_BUNDLE']:
    print(k, os.environ.get(k))

这是一个很关键的升级。

原版里: - 先设置证书 - 再 source /etc/network_turbo - 再把代理变量灌回 Python 环境 - 再设置 HF_ENDPOINT

Qwen 版里: - 不再替学生自动改环境 - 只检查这些变量有没有已经准备好

这体现了一个重要变化:

tiny-gpt2 版更像“全包办式演示”,Qwen 版更像“真实环境诊断”。

Cell 6:从“保存 tiny-gpt2”变成“测试 Qwen tokenizer 能不能加载”

from transformers import AutoTokenizer
model_name='Qwen/Qwen2.5-0.5B-Instruct'
print('Testing tokenizer load for', model_name)
try:
    tok=AutoTokenizer.from_pretrained(model_name, use_fast=False)
    print('Tokenizer loaded OK')
except Exception as e:
    print('Tokenizer load failed:', repr(e))

这是整个 00_check_env 里最重要的变化。

逐行解释:

为什么不直接在这里加载模型?


三、01_lora_demo.ipynb 改了什么

这是改动最大的一份 notebook。它从“tiny-gpt2 的最小 LoRA 演示”升级成了“Qwen 指令模型的最小 LoRA 演示”。

Cell 1:标题变化

# 01_lora_demo
Qwen2.5-0.5B-Instruct 的最小 LoRA 演示。建议先跑通,再改参数。

这里已经明确提示学生:

Cell 2:把实验核心路径写成变量

model_name = 'Qwen/Qwen2.5-0.5B-Instruct'
train_file = '/root/course_lora/data/sample_train.json'
val_file = '/root/course_lora/data/sample_val.json'
output_dir = '/root/course_lora/outputs/qwen_lora_demo'

和原版相比,这里有 4 个关键变化:

为什么改成绝对路径:

Cell 3:导入库并创建输出目录

import os, json, torch
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForLanguageModeling
from peft import LoraConfig, get_peft_model

os.makedirs(output_dir, exist_ok=True)

这里把原来分散在后面的逻辑提到前面。

逐项理解:

Cell 4:加载 tokenizer 和模型

tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=dtype, trust_remote_code=True)

这是从 tiny-gpt2 切换到 Qwen 的核心之一。

use_fast=False

你明确要求加载非 fast tokenizer。这样做通常是为了兼容性更稳,尤其是一些 chat 模型和模板处理逻辑。

pad_token = eos_token

很多 causal LM 没有单独定义 pad_token。但训练时做 padding='max_length' 又需要 pad token,所以你把它补成 eos_token

dtype = ...

torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16

这说明你不再像 tiny-gpt2 版那样用默认精度,而是主动根据硬件能力选:

这是一个非常典型的“从教学演示走向真实工程”的变化。

trust_remote_code=True

这一项也很重要。它表示允许 transformers 信任模型仓库中的自定义代码实现。

为什么加它:

Cell 5:LoRA 配置改成针对 Qwen 的目标层

lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
    bias='none',
    task_type='CAUSAL_LM',
    target_modules=['q_proj','k_proj','v_proj','o_proj','gate_proj','up_proj','down_proj'],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

这部分是 结构适配 的关键。

tiny-gpt2 版里没有显式指定这些 QKV / FFN 投影层,因为 tiny-gpt2 的演示更简化。Qwen 版必须明确说清 LoRA 要挂在哪里。

逐项解释:

这说明你做的不只是“把模型换成 Qwen”,而是:

明确把 LoRA 挂载点调整成了适合 Qwen 架构的模块名。

Cell 6:数据预处理从“三段式文本”改成“聊天模板”

def load_json(path):
    with open(path, 'r', encoding='utf-8') as f:
        return json.load(f)

def preprocess(example, max_length=384):
    text = tokenizer.apply_chat_template(example['messages'], tokenize=False, add_generation_prompt=False)
    tokenized = tokenizer(text, truncation=True, max_length=max_length, padding='max_length')
    tokenized['labels'] = tokenized['input_ids'].copy()
    return tokenized

train_ds = Dataset.from_list(load_json(train_file)).map(preprocess)
val_ds = Dataset.from_list(load_json(val_file)).map(preprocess)
cols = ['input_ids','attention_mask','labels']
train_ds.set_format(type='torch', columns=cols)
val_ds.set_format(type='torch', columns=cols)
len(train_ds), len(val_ds)

这段改动非常关键。

原版 tiny-gpt2: - 自己拼 Instruction: ... / Input: ... / Response: ...

Qwen 版: - 直接要求数据是 messages - 用 tokenizer.apply_chat_template(...) 去拼 prompt

这背后的思想变化是:

对 chat 模型,最稳的做法不是自己随手拼字符串,而是使用模型自己的 chat template。

max_length=384

原版只到 128,现在增大到 384,是因为聊天模板加上 system/user/assistant 后文本更长。

labels = input_ids.copy()

这保留了 causal LM 训练的标准做法:

Cell 7:训练参数为 Qwen 调整

training_args = TrainingArguments(
    output_dir=output_dir,
    overwrite_output_dir=True,
    num_train_epochs=1,
    per_device_train_batch_size=1,
    per_device_eval_batch_size=1,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    logging_steps=1,
    eval_strategy='epoch',
    save_strategy='epoch',
    save_total_limit=1,
    report_to='none',
    fp16=torch.cuda.is_available() and not (torch.cuda.is_available() and torch.cuda.is_bf16_supported()),
    bf16=torch.cuda.is_available() and torch.cuda.is_bf16_supported(),
    remove_unused_columns=False,
)

这部分几乎每一项都反映了“模型更大了,所以策略也要变”。

per_device_train_batch_size=1

原版是 2,现在改成 1,目的是减轻显存压力。

gradient_accumulation_steps=4

虽然每步只放 1 条样本,但累积 4 步梯度,再更新一次参数。这样可以在小显存下模拟更大的有效 batch。

learning_rate=2e-4

原版是 5e-4,现在调低,通常是因为:

eval_strategy='epoch' / save_strategy='epoch'

原版按 step 存和评估。Qwen 版按 epoch。

为什么:

save_total_limit=1

只保留 1 个 checkpoint,节省磁盘空间。

fp16 / bf16

这里写得很讲究:

这样可以避免两种半精度模式冲突。

Cell 8 和 Cell 9:训练与保存

trainer.train()
trainer.save_model(output_dir)
tokenizer.save_pretrained(output_dir)
print('saved to', output_dir)

这部分逻辑没变,但输出目录已经换成 Qwen 对应的目录。

Cell 10:训练后立刻做一次对话推理

from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=dtype, trust_remote_code=True)
merged = PeftModel.from_pretrained(base_model, output_dir)
messages = [
    {'role':'system','content':'你是一位清晰、耐心的大模型课程助教。'},
    {'role':'user','content':'请简要解释什么是 LoRA。'}
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors='pt').to(merged.device)
out = merged.generate(**inputs, max_new_tokens=80)
print(tokenizer.decode(out[0], skip_special_tokens=True))

这里的变化包括:

这是在向学生传递一个很重要的习惯:

对 instruct/chat 模型,训练和推理都要尊重它的对话格式。


四、02_eval_and_infer.ipynb 改了什么

这份 notebook 的改动比 01_lora_demo 少,但方向完全一致:改成面向 Qwen chat 模型的推理流程。

Cell 1:标题改成 Qwen 版本

# 02_eval_and_infer
加载 Qwen 基座模型与训练好的 LoRA 适配器,做简单推理。

Cell 2:直接指定模型名和 adapter 目录

model_name = 'Qwen/Qwen2.5-0.5B-Instruct'
adapter_dir = '/root/course_lora/outputs/qwen_lora'

Cell 3:更稳的 tokenizer / model 加载方式

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel

dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
tokenizer = AutoTokenizer.from_pretrained(adapter_dir if __import__('os').path.exists(adapter_dir) else model_name, use_fast=False)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token
base_model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=dtype, trust_remote_code=True)

这里最值得学生学的是这一行:

tokenizer = AutoTokenizer.from_pretrained(adapter_dir if __import__('os').path.exists(adapter_dir) else model_name, use_fast=False)

它的意思是:

这是一种很实用的“兜底写法”。

Cell 4:加载 LoRA adapter

model = PeftModel.from_pretrained(base_model, adapter_dir)
print('LoRA adapter loaded from', adapter_dir)

这一段和 tiny-gpt2 版本的核心逻辑一样,但对象已经换成了 Qwen 基座模型。

Cell 5:推理 prompt 改成聊天格式

messages = [
    {'role':'system','content':'你是一位清晰、耐心的大模型课程助教。'},
    {'role':'user','content':'请比较全参数微调和 LoRA 微调。'}
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors='pt').to(model.device)
outputs = model.generate(**inputs, max_new_tokens=120)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

这里和 01_lora_demo 的最终测试逻辑一致,说明你在训练和推理两边都统一采用了 chat 格式。

这是对的,因为:

这样前后一致,模型行为更稳定。


五、train_lora.py 改了什么

这份脚本最能体现你的迁移思路,因为它把 notebook 里的改动正式工程化了。

第 1 行

#!/usr/bin/env python

你加了 shebang。这样脚本更像一个正式可执行脚本。

第 2-16 行:导入顺序调整

import os
import json
import argparse
from typing import Dict, List

import torch
from datasets import Dataset
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    TrainingArguments,
    Trainer,
    DataCollatorForLanguageModeling,
)
from peft import LoraConfig, get_peft_model

这里主要有两个变化:

第 19-34 行:参数系统重写

def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("--model_name", type=str, default="Qwen/Qwen2.5-0.5B-Instruct")
    parser.add_argument("--train_file", type=str, default="/root/course_lora/data/sample_train.json")
    parser.add_argument("--val_file", type=str, default="/root/course_lora/data/sample_val.json")
    parser.add_argument("--output_dir", type=str, default="/root/course_lora/outputs/qwen_lora")
    parser.add_argument("--num_train_epochs", type=float, default=1.0)
    parser.add_argument("--per_device_train_batch_size", type=int, default=1)
    parser.add_argument("--per_device_eval_batch_size", type=int, default=1)
    parser.add_argument("--gradient_accumulation_steps", type=int, default=4)
    parser.add_argument("--learning_rate", type=float, default=2e-4)
    parser.add_argument("--max_length", type=int, default=384)
    parser.add_argument("--lora_r", type=int, default=8)
    parser.add_argument("--lora_alpha", type=int, default=16)
    parser.add_argument("--lora_dropout", type=float, default=0.05)
    return parser.parse_args()

和原版相比,这里有几个重要变化:

这说明你想让这个脚本既能课堂跑通,也能更方便地试参数。

第 37-39 行:简化 JSON 读取

def load_json(path: str) -> List[Dict]:
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)

原版支持 JSON 和 JSONL 两种格式。你这里改成只读 JSON。

为什么合理:

第 42-47 行:用 chat template 取代手写字符串

def build_text(example: Dict, tokenizer):
    return tokenizer.apply_chat_template(
        example["messages"],
        tokenize=False,
        add_generation_prompt=False,
    )

这是整个脚本最本质的变化之一。

原版自己拼: - Instruction: ... - Input: ... - Response: ...

新版: - 要求数据中有 messages - 让 tokenizer 自己负责拼接

这保证了: - prompt 格式和模型匹配 - system / user / assistant 的角色结构不丢

第 50-59 行:新的 tokenization 逻辑

def tokenize_function(example: Dict, tokenizer, max_length: int):
    text = build_text(example, tokenizer)
    tokenized = tokenizer(
        text,
        truncation=True,
        max_length=max_length,
        padding="max_length",
    )
    tokenized["labels"] = tokenized["input_ids"].copy()
    return tokenized

这一段和 notebook 版完全对齐,说明你已经把 notebook 逻辑成功搬进脚本。

第 66-77 行:Qwen tokenizer / model 加载

print("Loading tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(args.model_name, use_fast=False)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

print("Loading model...")
dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
model = AutoModelForCausalLM.from_pretrained(
    args.model_name,
    torch_dtype=dtype,
    trust_remote_code=True,
)

这部分和 notebook 一致,体现的是“先在 notebook 验证,再写进脚本”。

第 79-86 行:LoRA 模块改成 Qwen 对应层

target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]

这是脚本版里最重要的结构性修改。

原版 tiny-gpt2 只是一个课堂极简演示;现在你已经明确写出了:

第 91-102 行:数据集构建方式变化

train_data = load_json(args.train_file)
val_data = load_json(args.val_file)

train_ds = Dataset.from_list(train_data)
val_ds = Dataset.from_list(val_data)

train_ds = train_ds.map(lambda x: tokenize_function(x, tokenizer, args.max_length))
val_ds = val_ds.map(lambda x: tokenize_function(x, tokenizer, args.max_length))

cols = ["input_ids", "attention_mask", "labels"]
train_ds.set_format(type="torch", columns=cols)
val_ds.set_format(type="torch", columns=cols)

这说明数据已经不再先被改造成 {"text": ...} 形式,而是直接保留原始 messages 结构,等到 map() 时再变成 token。

这是更符合 chat 数据处理逻辑的写法。

第 106-122 行:训练参数工程化升级

这里和 notebook 一致,但在脚本里更重要,因为它是批处理训练的正式入口。

尤其值得注意的是:

这几项都反映出你已经在考虑真实显存限制和训练稳定性。

第 124-136 行:保存说明更准确

最后这句:

print(f"Saved LoRA adapter and tokenizer to: {args.output_dir}")

比原版更准确。因为这里保存的不是“整个模型”,而是:


六、run_train.sh 改了什么

#!/usr/bin/env bash
set -e

cd /root/course_lora

MODEL_NAME="Qwen/Qwen2.5-0.5B-Instruct"
TRAIN_FILE="/root/course_lora/data/sample_train.json"
VAL_FILE="/root/course_lora/data/sample_val.json"
OUTPUT_DIR="/root/course_lora/outputs/qwen_lora"

mkdir -p "${OUTPUT_DIR}"

python train_lora.py --model_name "${MODEL_NAME}" --train_file "${TRAIN_FILE}" --val_file "${VAL_FILE}" --output_dir "${OUTPUT_DIR}" --num_train_epochs 1 --per_device_train_batch_size 1 --gradient_accumulation_steps 4 --learning_rate 2e-4 --max_length 384 --lora_r 8 --lora_alpha 16 --lora_dropout 0.05

和原版相比,最关键的变化有:

这说明你已经不把 run_train.sh 当成“随便跑一下”的脚本,而是当成了:

一份固定、稳妥、适合学生直接执行的最小训练配方。


七、数据文件改了什么

这部分其实和模型切换同样重要。

sample_train.json

原版是:

{
  "instruction": "...",
  "input": "...",
  "output": "..."
}

现在改成:

{
  "messages": [
    {"role": "system", "content": "..."},
    {"role": "user", "content": "..."},
    {"role": "assistant", "content": "..."}
  ]
}

这意味着:

每一条样本都保留了统一的 system 角色:

你是一位清晰、耐心的大模型课程助教。

这很好,因为它把模型角色固定住了,有助于课堂演示时输出风格更稳定。

sample_val.json

验证集也改成了同样的 messages 格式。

这保证了训练和验证处理逻辑完全一致,不会出现“训练时是 chat 格式,验证时是普通字符串”的混乱情况。


八、requirements.txt 改了什么

原版多是“裸包名”,新版改成了“带最低版本约束”的形式:

torch>=2.6
transformers>=4.51
datasets>=3.0
peft>=0.14
accelerate>=1.0
trl>=0.15
jupyterlab>=4.0
...
safetensors>=0.4.5

这里透露出两个重要判断:

  1. 你希望环境更稳定、更可复现
  2. 你在主动绕开新版本 PyTorch / transformers / safetensors 兼容性问题

尤其是把 bitsandbytes 去掉,换成 safetensors,说明这份最小 Qwen 实验不再强依赖量化训练,而更强调基础稳定性。


九、README_学生版.md 改了什么

这份 README 从“完整课程包说明”改成了“Qwen 课堂版最小说明”。

也就是说:

这种改法是合理的,因为这份目录现在更聚焦:


十、你这次修改最值得学生学到什么

如果让我把这次修改总结成 5 个最重要的工程经验,那就是:

  1. 换模型时,不能只换模型名,数据格式和 prompt 格式也要一起换。
  2. 对 chat 模型,优先使用 apply_chat_template(),不要手写 prompt 拼接。
  3. LoRA 的 target_modules 必须跟模型结构匹配。
  4. 模型稍大后,batch size、gradient accumulation、precision 都要重新设计。
  5. notebook 跑通之后,要把同样的逻辑落实到脚本里,才能形成可复现实验。

最后一条提醒

这份 course_lora_qwen_src 已经不是单纯的“tiny-gpt2 替换版”,而是一份更接近真实大模型课堂实验的脚手架。

它的特点是:

如果后面你还要继续升级,这一版最自然的下一步会是:

这三步做完,这套 Qwen 实验包就会和上面 course_lora-tinygpt2 的说明体系完全对齐。