用 Unsloth 在 Colab 上微调 Qwen-3:一份能跑通的实战指南

0 阅读

为什么选 Qwen-3 来微调

DeepSeek 热度正高,但不少开发者已经悄悄转向 Qwen-3 做微调。原因很实在:Qwen-3 在代码、数学、通用能力等基准测试里分数漂亮,DeepSeek-R1、o1、o3-mini、Grok-3、Gemini-2.5-Pro 都被它压了一头。更关键的是,Qwen-3 的 MoE 架构让小模型也能打——Qwen3-30B-A3B 的激活参数量是 Qwen-32B 的 10 倍,而 Qwen3-4B 这种小不点居然能追上 Qwen2.5-72B-Instruct 的表现。

微调这事,说贵确实贵,但用对工具就能降下来。这篇指南全程在 Google Colab 免费 T4 GPU(15GB 显存)上跑通,目标是 Qwen-3-8B 的 4bit 量化版本。

准备工作:库、硬件和数据

要装的包

Unsloth 是核心,它能让 Llama、Mistral、Gemma、Qwen 这些模型的微调速度翻倍,内存省 70%,精度还不掉。配合 PyTorch、Transformers、TRL 就能开工。

!pip install --no-deps bitsandbytes accelerate xformers==0.0.29.post3 peft trl==0.15.2 triton cut_cross_entropy unsloth_zoo
!pip install sentencepiece protobuf datasets huggingface_hub hf_transfer
!pip install --no-deps unsloth

本地有强卡的话直接 pip install unsloth 就行。

显存怎么算

Qwen-3-8B 的 4bit 量化版大概要吃 8-12GB 显存。Colab T4 的 15GB 刚好够,还能留点余量给训练时的激活值和优化器状态。

数据从哪来

微调需要任务相关的结构化数据。这里混了两个数据集:

  • unsloth/OpenMathReasoning-mini:增强推理和解题能力,只用它的 COT(思维链)部分
  • mlabonne/FineTome-100k:提升通用对话能力

后面会按 7:3 的比例混合,让模型既能聊天又能算题。

加载模型和分词器

用 Unsloth 的 FastLanguageModel 加载预训练模型,比原生 Transformers 快一截:

from unsloth import FastLanguageModel
import torch

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-8B-unsloth-bnb-4bit",
    max_seq_length = 2048,
    load_in_4bit = True,
    load_in_8bit = False,
    full_finetuning = False,
)

max_seq_length=2048 控制上下文长度,影响内存和精度。load_in_4bit=True 把模型压到 4bit,省显存。full_finetuning=False 表示不做全参数训练,后面用 LoRA 做参数高效微调。

加上 LoRA 适配器

LoRA 的核心思想很简单:冻结原模型,只在关键层旁边插两个小矩阵(低秩分解),训练时只更新这两个小矩阵。这样可训练参数从几十亿降到几百万,显存压力骤减。

model = FastLanguageModel.get_peft_model(
    model,
    r = 32,
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj"],
    lora_alpha = 64,
    lora_dropout = 0,
    bias = "none",
    use_gradient_checkpointing = "unsloth",
    random_state = 3433,
)

r=32 是 LoRA 的秩,控制矩阵大小。lora_alpha=64 是缩放因子,一般设成 r 的 1-2 倍。target_modules 列出了要加 LoRA 的层,Qwen-3 用的是标准 Transformer 结构,这些层覆盖了注意力机制和 FFN。

处理数据:从原始格式到对话结构

推理数据格式化

OpenMathReasoning-mini 里的数据是问题和解答分离的,需要转成对话格式:

def generate_conversation(examples):
    problems = examples["problem"]
    solutions = examples["generated_solution"]
    conversations = []
    for problem, solution in zip(problems, solutions):
        conversations.append([
            {"role": "user", "content": problem},
            {"role": "assistant", "content": solution},
        ])
    return {"conversations": conversations}

reasoning_conversations = tokenizer.apply_chat_template(
    reasoning_dataset.map(generate_conversation, batched=True)["conversations"],
    tokenize = False,
)

apply_chat_template 把 Python 列表转成模型认识的字符串格式,带特殊 token 区分角色。

通用对话数据标准化

FineTome-100k 的格式不太统一,用 Unsloth 的 standardize_sharegpt 洗一遍:

from unsloth.chat_templates import standardize_sharegpt
dataset = standardize_sharegpt(non_reasoning_dataset)

non_reasoning_conversations = tokenizer.apply_chat_template(
    dataset["conversations"],
    tokenize = False,
)

按 7:3 混合

聊天和推理的比例直接影响模型性格。这里想要一个偏聊天的模型,所以通用对话占 70%,数学题占 30%:

import pandas as pd

chat_percentage = 0.7
non_reasoning_subset = pd.Series(non_reasoning_conversations).sample(
    int(len(reasoning_conversations) * (1.0 - chat_percentage)),
    random_state = 2407,
)

data = pd.concat([
    pd.Series(reasoning_conversations),
    pd.Series(non_reasoning_subset)
])
data.name = "text"

from datasets import Dataset
combined_dataset = Dataset.from_pandas(pd.DataFrame(data))
combined_dataset = combined_dataset.shuffle(seed = 3407)

训练配置和启动

用 TRL 库的 SFTTrainer 做监督微调,关键参数解释一下:

from trl import SFTTrainer, SFTConfig

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = combined_dataset,
    eval_dataset = None,
    args = SFTConfig(
        dataset_text_field = "text",
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        max_steps = 30,
        learning_rate = 2e-4,
        logging_steps = 1,
        optim = "adamw_8bit",
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 3407,
        report_to = "none",
    ),
)

trainer_stats = trainer.train()
  • per_device_train_batch_size=2:单步喂 2 条样本
  • gradient_accumulation_steps=4:攒 4 步再反向传播,等效 batch size 是 8
  • max_steps=30:演示用的,真训练要设 num_train_epochs 或更大的 max_steps
  • optim="adamw_8bit":8bit AdamW 优化器,省显存
  • learning_rate=2e-4:LoRA 微调常用这个量级

跑起来后每步打印 loss,30 步在 T4 上大概几分钟。

推理测试:开不开思考模式

Qwen-3 有个特点:可以强制开启或关闭思考。测同一道题 (x+2)^2=0 看看区别。

关闭思考

messages = [{"role": "user", "content": "Solve (x + 2)^2 = 0."}]
text = tokenizer.apply_chat_template(
    messages,
    tokenize = False,
    add_generation_prompt = True,
    enable_thinking = False,  # 关掉
)

_ = model.generate(
    **tokenizer(text, return_tensors = "pt").to("cuda"),
    max_new_tokens = 256,
    temperature = 0.7, top_p = 0.8, top_k = 20,
    streamer = TextStreamer(tokenizer, skip_prompt = True),
)

输出很直接:

To solve the equation (x + 2)² = 0, we can take the square root of both sides. This gives us x + 2 = 0. Then, we can subtract 2 from both sides to get x = -2.

没废话,一步出答案。

开启思考

把 enable_thinking=True,max_new_tokens 提到 1024:

text = tokenizer.apply_chat_template(
    messages,
    tokenize = False,
    add_generation_prompt = True,
    enable_thinking = True,  # 打开
)

_ = model.generate(
    **tokenizer(text, return_tensors = "pt").to("cuda"),
    max_new_tokens = 1024,
    temperature = 0.6, top_p = 0.95, top_k = 20,
    streamer = TextStreamer(tokenizer, skip_prompt = True),
)

输出里会夹带 <thinking>...</thinking> 标签,模型先把思考过程写一遍,再给答案。简单题看着啰嗦,复杂题这功能就值回票价了。

保存模型

训练完把 LoRA 权重和原模型合并,存成标准格式:

model.save_pretrained_merged(
    "Qwen3-16bit-OpenMathReasoning-Finetuned-Merged",
    tokenizer,
    save_method = "merged_16bit"
)

merged_16bit 表示合并后用 16bit 精度存,比 4bit 精度高,比 32bit 省空间。

几个实操要点

显存不够怎么办

  • 把 max_seq_length 降到 1024 或 512
  • r 从 32 降到 16 或 8
  • per_device_train_batch_size 降到 1,同时提高 gradient_accumulation_steps
  • 开 use_gradient_checkpointing="unsloth",用时间换空间

数据比例怎么调

  • 要代码助手:多放代码数据集,少放闲聊
  • 要数学能力:OpenMathReasoning 比例提到 50% 以上
  • 要角色扮演:找对应风格的对话数据

学习率别乱动

LoRA 微调 2e-4 到 5e-4 是安全区。全参数微调才需要 1e-5 这种小学习率。

思考模式什么时候开

  • 数学题、逻辑题、需要步骤解释的:开
  • 闲聊、翻译、简单问答:关,省 token 也省时间

写在最后

这套流程走下来,用免费资源也能得到一个定制版的 Qwen-3。Unsloth 把显存优化和训练加速做了封装,LoRA 把可训练参数压到可接受范围,剩下的就是数据工程和调参的脏活累活。

微调不是炼丹,但确实需要迭代。先跑通小步数验证流程,再放大训练规模,比一上来就烧几个小时 GPU 更靠谱。