unsloth笔记:运行&微调 gemma

  • Google 发布了 Gemma 3 ,包括一个新的 270M 模型 ,以及之前的 1B、4B、12B 和 27B 模型。

  • 其中 270M 和 1B 是仅文本模型,更大的模型支持文本 + 视觉。

  • 注:GGUF无法微调,专为高效、本地推理设计

1 官方推荐推理设置

根据 Gemma 团队的官方推荐:

  • Temperature = 1.0

  • Top_K = 64

  • Top_P = 0.95

  • Min_P = 0.0(可选,0.01 效果也好;llama.cpp 默认是 0.1)

  • Repetition Penalty = 1.0(在 llama.cpp/transformers 中,1.0 表示禁用)


2 聊天模板

原始格式(带换行符 \n

复制代码
<bos><start_of_turn>user\nHello!<end_of_turn>\n
<start_of_turn>model\nHey there!<end_of_turn>\n
<start_of_turn>user\nWhat is 1+1?<end_of_turn>\n<start_of_turn>model\n

渲染换行后的格式

复制代码
<bos><start_of_turn>user 
Hello!<end_of_turn> 
<start_of_turn>model 
Hey there!<end_of_turn> 
<start_of_turn>user 
What is 1+1?<end_of_turn> 
<start_of_turn>model \n

3 在手机上运行 Gemma 3

  • 推荐使用能运行 GGUF 的本地应用,例如:

    • AnythingLLM 移动端(Android 可下载)

    • ChatterUI

  • 建议使用 Gemma 3 270MGemma 3n(轻量版本),因为手机 RAM 有限且可能过热。

  • 流程:微调 → 导出 GGUF → 移动端加载。

4 用unsloth微调gemma3

4.1 Unsloth 微调修复方案

包含三部分:

  1. 将所有中间激活保持在 bfloat16 格式(也可以是 float32,但这会多用 2 倍显存/内存,Unsloth 通过异步梯度检查点解决)。

  2. 所有矩阵乘法都在 float16 上用 Tensor Cores 执行,但手动进行上下转型,不依赖 PyTorch 的混合精度自动转换。

  3. 将其他不需要矩阵乘法的操作(如 LayerNorm )上转为 float32

4.1.1 Gemma 3 修复分析

  • 对 Gemma 3(1B--27B)进行微调或运行之前,发现如果使用 float16 混合精度梯度和激活会变成无穷大
    • 这在 T4 GPU、RTX 20 系列、V100 上都会出现,因为它们只有 float16 Tensor Cores。
    • 而在 RTX 30 系列或更高、A100、H100 等新 GPU 上,就没有这个问题,因为它们支持 bfloat16 Tensor Cores

原因分析

  • float16 最大值是 65504 ,而 bfloat16 最大值可表示到 10^38

  • 两者都只有 16 位!

    • float16:更多位数给小数 → 精度高,但范围小。

    • bfloat16:更多位数给指数 → 精度差,但范围大。

所以 大模型在 float16 下容易溢出 ,但 bfloat16 可以避免

为什么不用 float32?

float32 确实能解决溢出问题,但在 GPU 上做矩阵乘法时会慢 4--10 倍,因此不可行。

5 unsloth 微调Gemma

5.1 导入库

python 复制代码
from unsloth import FastModel
import torch

'''
🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.
🦥 Unsloth Zoo will now patch everything to make training faster!
'''

5.2 加载模型

python 复制代码
model, tokenizer = FastModel.from_pretrained(
    model_name = "unsloth/gemma-3-4b-it",
    max_seq_length = 2048, # 设置 模型能处理的最大序列长度(上下文长度)
    load_in_4bit = True,  # 打开 4-bit 量化:把模型参数从 16/32-bit 压缩成 4-bit 存储。
    load_in_8bit = False, #不启用 8-bit 量化
    full_finetuning = False, #是否允许 全参数微调;默认 False:只会加载成 LoRA 或 QLoRA 这种参数高效微调方式
    # token = "hf_...", # 某些需要的huggingface的api
)

5.3 给模型套上PEFT

python 复制代码
model = FastModel.get_peft_model(
    model,
    finetune_vision_layers     = False, 
    # 如果模型是多模态(text+vision),这里可以决定要不要微调视觉部分

    finetune_language_layers   = True,  
    # 是否在语言层(Transformer encoder/decoder block 里的语言部分)插 LoRA。
    #一般必须开着,否则 LoRA 只挂在注意力/MLP,微调效果会差。

    finetune_attention_modules = True,  
    #LoRA 是否插到 注意力模块 (Q/K/V projection)。
    #打开会增强模型对任务的快速适应(尤其对 RLHF/GRPO、指令跟随类任务有帮助)。


    finetune_mlp_modules       = True,  
    #LoRA 是否插到 前馈 MLP 层。
    #官方推荐一直开,因为只在 attention 上加 LoRA 效果有限;MLP 对生成质量和泛化也很关键。


    '''
    LoRA 参数
    '''
    r = 8,           
    #LoRA 的秩 (rank),也就是低秩分解的维度。
    #越大,模型能学到的表达能力越强,但参数量也会上升,容易过拟合。


    lora_alpha = 8, 
    # 缩放因子,控制 LoRA 矩阵更新的强度。
    #一般推荐 lora_alpha ≥ r
    #如果设得太小,LoRA 的更新效果会被稀释;太大则可能训练不稳定。

    lora_dropout = 0,
    #在 LoRA 层里加不加 dropout。
    #设成 0 表示完全不用 dropout(适合数据量较大或模型稳定的情况)。
    #如果数据特别少,适当加一点(如 0.05~0.1)能防过拟合。


    bias = "none",
    #LoRA 是否也学习 bias 项。
    "none" 表示只改权重矩阵,不动 bias。
    #一般保持 "none",能减少不必要的开销。

    random_state = 3407,
    #随机种子,保证可复现性。
)

5.4 给tokenizer套上聊天格式模板

  • 不同的开源大模型(LLaMA、Mistral、Gemma...)各自对「聊天 prompt」的格式有不同要求
  • 如果直接喂模型「用户问题」,可能效果差甚至出错;加上正确的 template 才能保证训练/推理一致性。
python 复制代码
from unsloth.chat_templates import get_chat_template
tokenizer = get_chat_template(
    tokenizer,
    chat_template = "gemma-3",
)
'''
把原始的 tokenizer 包了一层,得到一个支持对话模板的新 tokenizer
chat_template="gemma-3":指定使用 Gemma-3 官方的对话格式

这样设置后,你就可以直接给 tokenizer 一个「列表格式的对话」,而不用手动拼 prompt。
'''

目前支持的模板包括:zephyr、chatml、mistral、llama、alpaca、vicuna、vicuna_old、phi3、llama3、phi4、qwen2.5、gemma3 等。

5.5 导入数据

5.5.1 从datasets导入数据

python 复制代码
from datasets import load_dataset
dataset = load_dataset("mlabonne/FineTome-100k", split = "train")
'''
数据集可能包含多个分片(train/test/validation)。

这里直接指定只要 train 部分
'''


dataset[0]

5.5.2 整理成标准化聊天数据集

好把你前面拿到的 FineTome-100k 原始格式 (带有 conversations 列、text 列、sourcescore 等杂项)整理成 标准化的聊天数据集

python 复制代码
from unsloth.chat_templates import standardize_data_formats
dataset = standardize_data_formats(dataset)
dataset[0]

5.5.3 把标准化的对话转换成 纯文本 prompt+response,以便后续送进模型做微调。

python 复制代码
def formatting_prompts_func(examples):
   convos = examples["conversations"]
   #从批量样本中取出对话字段。
   texts = [
       tokenizer.apply_chat_template(
           convo,
           tokenize = False,              # 直接生成字符串,不做 token 化
           add_generation_prompt = False  # 不在最后额外加 Assistant 的起始符
       ).removeprefix('<bos>')            # 去掉开头的 <bos>
       for convo in convos
   ]
   '''
    每条 convo 都是一个 list:
    [
      {"role": "user", "content": "Explain boolean operators."},
      {"role": "assistant", "content": "Boolean operators are ..."}
    ]

   tokenizer.apply_chat_template(...)把这轮对话按照 gemma-3 的 chat 格式拼接成文本
    【前面已经get_chat_template(tokenizer, chat_template="gemma-3") 设置好了】
   输出会是类似这样的字符串
        <bos><start_of_turn>user
        Explain boolean operators.<end_of_turn>
        <start_of_turn>assistant
        Boolean operators are ...

   .removeprefix('<bos>')
        训练时 只需要一个 <bos> 开头 token。
        Hugging Face 的 Trainer / Unsloth Processor 会在真正送进模型前自己加 <bos>。
        所以这里提前把拼出来的 <bos> 删掉,避免重复。训练时 只需要一个 <bos> 开头 token。

   '''
   return { "text" : texts, }
   '''
    给每条样本生成一个新的 "text" 字段,存放清理好的对话字符串。

    格式化后的 dataset 就能直接用于微调。
    '''
python 复制代码
dataset = dataset.map(formatting_prompts_func, batched=True)
  • Hugging Face Datasets 的 map 会对整个数据集执行上面的函数,生成一个新的 "text" 列。

  • 原始的 "conversations" 仍然在,只是又多了 "text"

5.6 创建Trainer

python 复制代码
from trl import SFTTrainer, SFTConfig
#SFTTrainer = 用于 指令微调 (Instruction Tuning) 的 Trainer。
trainer = SFTTrainer(
    model = model,
    #前面用LoRA包裹的Gemma-3
    
    tokenizer = tokenizer, 
    #用gemma-3的chat_temple包装过的tokenizer
    
    train_dataset = dataset,
    #处理过的 dataset(带 "text" 字段)。
    #训练时,Trainer 会取 example["text"] 来作为训练样本。
    
    eval_dataset = None, 
    # 没有设置验证集
    
    args = SFTConfig(
        dataset_text_field = "text", 
        #告诉 Trainer 用哪个字段作为输入
        
        per_device_train_batch_size = 2,
        #每个 GPU 上一次 forward 的 batch size。
        
        gradient_accumulation_steps = 4, 
        # 梯度累积,等价于 有效 batch size = 2 × 4 = 8。
        
        warmup_steps = 5,
        #前 5 步采用线性升高学习率,从 0 到设定的 learning_rate

        output_dir = "training_checkpoints", # 存放检查点的目录(early stopping 需要)
        save_strategy = "steps",             # 每 N 步保存模型(或者"epoch")
        save_steps = 10,                     # 间隔多少步保存一次
        save_total_limit = 3,                # 只保留 3 个最新检查点,节省磁盘

        
        # num_train_epochs = 1
        # 如果设定,会跑完整个数据集 1 遍。
        # 现在没启用,而是用 max_steps 控制。
        
        max_steps = 30,
        #最多训练 30 步,然后停下来
        
        learning_rate = 2e-4, 
        # 学习率
        
        logging_steps = 1,
        # 每 1 步输出一次 loss。
        
        optim = "adamw_8bit",
        # 使用 8-bit AdamW 优化器(来自 bitsandbytes),显存更省。
        # 如果你的环境不支持 8bit,可以改 "adamw_torch"。
        
        weight_decay = 0.01,
        #L2 正则项,防止过拟合。
        
        lr_scheduler_type = "linear",
        #学习率调度器,用线性下降
        seed = 3407,
        # 随机种子
        
        report_to = "none"
        #不上传日志。
        #如果想用 WandB 或 TensorBoard,可以改 "wandb" 或 "tensorboard"。
    ),
)

5.6.1 Trainer的dataset

相比于送入trainer之前多'input_ids', 'attention_mask', 'labels'

python 复制代码
tokenizer.decode(trainer.train_dataset[0]['input_ids'])

进入trainer之后的text相当于多了一个<bos>

5.6.2 给 Trainer 套上一个"只训练在助手回复部分"的过滤逻辑

在指令微调 (SFT) 时,训练数据通常是 用户指令 + 助手回答 拼在一起:

python 复制代码
<start_of_turn>user
Explain what boolean operators are ...<end_of_turn>
<start_of_turn>model
Boolean operators are ...

如果直接算 loss,模型会被迫"学习"用户输入的 token

这其实没意义,反而会浪费算力,还可能让模型生成"用户风格的输入"而不是回答。

所以unsloth可以多这一步

python 复制代码
from unsloth.chat_templates import train_on_responses_only
trainer = train_on_responses_only(
    trainer,
    instruction_part = "<start_of_turn>user\n",
    response_part = "<start_of_turn>model\n",
)
'''
会对数据里的每个训练样本做 label masking:
    用户部分 → label 设为 -100(在 CE loss 里被忽略)
    助手部分 → 保留 label,参与 loss 计算

instruction_part:用来标记用户输入开始的分隔符(这里是 <start_of_turn>user\n)

response_part:用来标记助手输出开始的分隔符(这里是 <start_of_turn>model\n)
(FineTome-100k 用 model 代替 assistant 角色)

这样 Trainer 就能精确识别"从哪里开始计算 loss"
'''

labeldecode的结果:可以看到前面一堆pad

5.7 真正开始训练

python 复制代码
trainer_stats = trainer.train()

trainer.train()

  • Hugging Face Trainer / trl.SFTTrainer 的标准方法。

  • 会遍历你传进去的 train_dataset,执行前向传播 + 反向传播 + 参数更新。

  • 根据你之前配置的 max_steps=30,这里只会训练 30 个 step,就会停下来。

  • 如果换成 num_train_epochs=1,则会把整个数据集完整训练一遍。

resume_from_checkpoint=True

  • 如果你上次训练被中断,可以从保存的 checkpoint 目录继续训练。

  • 默认会在 output_dir(比如 ./outputs/)下找最近的 checkpoint。

返回值 trainer_stats

  • 一个包含训练过程信息的字典,里面有:

    • train_loss:最终训练损失

    • metrics:训练时的统计信息

    • train_runtime:总运行时间

    • train_samples_per_second:吞吐量

5.8 查看训练过程的显存和耗时

python 复制代码
gpu_stats = torch.cuda.get_device_properties(1)
#gpu_stats = torch.cuda.get_device_properties(1)

start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
#返回 当前已被 PyTorch allocator 保留的显存最大值(字节数)
#也即训练前的内存占用

max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)
#显卡的总显存
print(f"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.")
#GPU = NVIDIA RTX A5000. Max memory = 23.673 GB.
print(f"{start_gpu_memory} GB of memory reserved.")
#4.359 GB of memory reserved.


# @title Show final memory and time stats
used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
#整个训练过程 GPU 上最大显存占用量。

used_memory_for_lora = round(used_memory - start_gpu_memory, 3)
#训练本身额外消耗的显存

used_percentage = round(used_memory / max_memory * 100, 3)
#训练期间显存最高占用 / 总显存,转百分比

lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)
#LoRA 训练本身新增的显存 / 总显存,转百分比。

print(f"{trainer_stats.metrics['train_runtime']} seconds used for training.")
#2140.7552 seconds used for training.
print(
    f"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training."
)
#35.68 minutes used for training.
print(f"Peak reserved memory = {used_memory} GB.")
#Peak reserved memory = 19.129 GB.
print(f"Peak reserved memory for training = {used_memory_for_lora} GB.")
#Peak reserved memory for training = 14.77 GB.
print(f"Peak reserved memory % of max memory = {used_percentage} %.")
#Peak reserved memory % of max memory = 80.805 %.
print(f"Peak reserved memory for training % of max memory = {lora_percentage} %.")
#Peak reserved memory for training % of max memory = 62.392 %.

5.9 推理

python 复制代码
from unsloth.chat_templates import get_chat_template
tokenizer = get_chat_template(
    tokenizer,
    chat_template = "gemma-3",
)
#和前面一样,给tokenizer套上gemma的模板
#这样你可以用类似 {"role": "user", "content": ...} 的结构输入,而不用自己手动拼 prompt。

messages = [{
    "role": "user",
    "content": "Continue the sequence: 1, 1, 2, 3, 5, 8,",
}]

#需要推理生成的message
python 复制代码
text = tokenizer.apply_chat_template(
    messages,
    add_generation_prompt = True, # 推理时必须加,告诉模型"现在轮到助手说话了"
)
text
#'<bos><start_of_turn>user\nContinue the sequence: 1, 1, 2, 3, 5, 8,<end_of_turn>\n<start_of_turn>model\n'


tokenizer.apply_chat_template(
    messages
)
#'<bos><start_of_turn>user\nContinue the sequence: 1, 1, 2, 3, 5, 8,<end_of_turn>\n'
#不加就是这个样子
python 复制代码
outputs = model.generate(
    **tokenizer(text, return_tensors = "pt").to("cuda"),
    max_new_tokens = 64, # Increase for longer outputs!
    # Recommended Gemma-3 settings!
    temperature = 1.0, top_p = 0.95, top_k = 64,
)
# 解码输出
print(tokenizer.batch_decode(outputs)[0])
'''
<bos><start_of_turn>user
Continue the sequence: 1, 1, 2, 3, 5, 8,<end_of_turn>
<start_of_turn>model
2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2

'''

5.9.1 流式推理

这样就不用等到所有token都生成完才有输出了,直接一个一个token蹦出来

python 复制代码
from transformers import TextStreamer
outputs = model.generate(
    **tokenizer([text], return_tensors = "pt").to("cuda"),
    max_new_tokens = 64, # Increase for longer outputs!
    # Recommended Gemma-3 settings!
    temperature = 1.0, top_p = 0.95, top_k = 64,
    streamer = TextStreamer(tokenizer, skip_prompt = True), #多了这一行
)
print(tokenizer.batch_decode(outputs)[0])
'''
13, 21, 34...
This is the Fibonacci sequence, where each number is the sum of the two numbers before it.
<end_of_turn>
'''

5.10 保存lora权重

python 复制代码
model.save_pretrained("gemma-3")      # 本地保存 LoRA adapters
tokenizer.save_pretrained("gemma-3")  # 保存 tokenizer 相关文件
  • 注意 :这里保存的并不是完整的 gemma-3-4b 模型,而是你训练得到的 LoRA adapter 权重

  • 本地目录 "gemma-3" 会包含:

    • adapter_model.bin (LoRA 权重)

    • adapter_config.json (LoRA 配置)

    • tokenizer.json 等文件

5.11 加载Lora权重

python 复制代码
from unsloth import FastModel
model, tokenizer = FastModel.from_pretrained(
        model_name = "lora_model",  # 你保存的目录名,例如 "gemma-3"
        max_seq_length = 2048,
        load_in_4bit = True,
    )

'''
model_name = "lora_model" 要改成你保存的目录,比如 "gemma-3"。

这样加载后,Unsloth 会自动:

    下载基础模型 unsloth/gemma-3-4b-it

    再把你保存的 LoRA 权重 合并进来 → 恢复到训练后的模型。
'''
  • Unsloth.FastModel.from_pretrained 里,传的 model_name 如果是你本地保存的 LoRA 目录 (例如 "gemma-3"),Unsloth 会做两件事:
    • 读取本地目录
    • 自动识别 & 下载基础模型
      • FastModel.from_pretrained("gemma-3") 会先加载目录里的 adapter 配置。

      • 看到 base_model_name_or_path 后,就会自动去 Hugging Face Hub 拉取对应的 基础模型 (比如 unsloth/gemma-3-4b-it)。

      • 加载完基础模型后,再把 adapter_model.bin 合并进去 → 得到你微调后的完整模型

相关推荐
googleccsdn6 小时前
ESNP LAB 笔记:配置MPLS(Part4)
网络·笔记·网络协议
楚肽生物小敏6 小时前
Cy5-Tyramide, Cyanine 5 Tyramide;1431148-26-3
笔记
THMAIL6 小时前
深度学习从入门到精通 - 生成对抗网络(GAN)实战:创造逼真图像的魔法艺术
人工智能·python·深度学习·神经网络·机器学习·生成对抗网络·cnn
却道天凉_好个秋6 小时前
计算机视觉(八):开运算和闭运算
人工智能·计算机视觉·开运算与闭运算
无风听海6 小时前
神经网络之深入理解偏置
人工智能·神经网络·机器学习·偏置
JoinApper6 小时前
目标检测系列-Yolov5下载及运行
人工智能·yolo·目标检测
speop6 小时前
llm的一点学习笔记
笔记·学习
北京地铁1号线6 小时前
GPT(Generative Pre-trained Transformer)模型架构与损失函数介绍
gpt·深度学习·transformer
运维小雅6 小时前
哪些因素会直观地影响到产品销量?
经验分享·笔记·媒体