前两天,和大家分享了阿里最新开源的推理模型:
比肩满血DS,阿里新王 QwQ-32B 本地部署,Ollma/vLLM 实测对比
有朋友反馈:模型还是太大了。。。
其实,很多情况下,我们只需解决一些特定场景的问题,完全没必要搞这么大的模型。
指令微调了解下?
最近,DeepSeek算命大师很火,今日分享,以算命为例,用 Unsloth 来微调一个垂直领域的推理模型。
项目地址: https://github.com/unslothai/unsloth
关于大模型指令微调,笔者之前有过分享:
【大模型指令微调实战】小说创作,一键直达天池挑战赛Top50
当时微调用的 peft 框架。
不得不感叹,这个领域技术更新太快,最新出的 Unsloth 极大加速了模型微调的速度,同时降低显存占用。
开源四个月,GitHub 已斩获 34k star。

老规矩,简短介绍下 Unsloth 亮点:
- 所有内核均基于 OpenAI 的 Triton 重写,大幅提升模型训练速度,降低显存占用。
- 实现中不存在近似计算,模型训练的精度损失为零。
- 支持绝大多数主流的 NVIDIA GPU 设备,CUDA 计算能力 7.0+。
- 支持 4bit 和 16bit QLoRA / LoRA 微调(基于 bitsandbytes)
来看看惊人的加速比和显存节省:

pip 一键安装:
pip install unsloth
这里查看所有支持的模型列表: https://docs.unsloth.ai/get-started/all-our-models
最新的 QwQ-32B 也已支持,包括量化版和原始模型:

为兼顾到绝大部分同学,本次微调选用 DeepSeek 蒸馏版 Qwen2.5 0.5B:

首先,从 huggingface 下载模型:(国内伙伴可引入镜像)
cd /home/luyanjie21/Desktop/microtrain-qwen3/ export HF_ENDPOINT=https://hf-mirror.com hf download unsloth/Qwen2.5-0.5B --local-dir ./ckpts/qwen-0.5b
数据是燃料,是模型微调成功的关键。
就像是给孩子补课的教材,这些数据往往需要审核(标注),以便模型有样学样。
比如,如果要让模型学会算命,就得准备一些标注好的命理学知识。
开源社区已有这样的数据集: https://huggingface.co/datasets/Conard/fortune-telling
不妨先下载来试试:
export HF_ENDPOINT=https://hf-mirror.com hf download --repo-type dataset Conard/fortune-telling --local-dir data/fortune-telling
注:采用 huggingface-cli 下载数据集时,加上 --repo-type dataset
数据集格式如下:

OK,一切准备就绪,下面开始炼丹!
模型微调,只需按照以下 8 步走~
step 1:引入依赖
from unsloth import FastLanguageModel, is_bfloat16_supported from transformers import TrainingArguments from trl import SFTTrainer from datasets import load_dataset
step 2:加载模型
max_seq_length = 8192 # 模型处理文本的最大长度# 加载模型model, tokenizer = FastLanguageModel.from_pretrained( model_name = "ckpts/ qwen-1.5b" , max_seq_length = max_seq_length, dtype=None, # 自动检测合适的类型 load_in_4bit = True, # device_map="balanced" # 多卡训练时均衡分布模型权重,默认为sequential)
step 3:加载数据集
# 定义训练数据格式化字符串模板train_prompt_style="""请遵循指令回答用户问题。在回答之前,请仔细思考问题,并创建一个逻辑连贯的思考过程,以确保回答准确无误。 指令:你是一位精通八字算命、紫微斗数、风水、易经卦象、塔罗牌占卜、星象、面相手相和运势预测等方面的算命大师。请回答以下算命问题。 问题:{} 回答:
{}
{}"""# 加载数据集dataset = load_dataset("data/fortune-telling", split="train")def formatting_data(examples): questions = examples["Question"] cots = examples["Complex_CoT"] responses = examples["Response"] texts = [] for q, c, r in zip(questions, cots, responses): text = train_prompt_style.format(q, c, r) + tokenizer.eos_token texts.append(text) return {"text": texts}dataset = dataset.map(formatting_data, batched=True)
可以打印一行数据,输出看看:
Generating train split: 207 examples [00:00, 1319.25 examples/s] Dataset size: 200 ['Question', 'Response', 'Complex_CoT']
step 5:定义 LoRA
# 添加 LoRA 权重model = FastLanguageModel.get_peft_model( model, r = 16, # Rank of the LoRA matrix target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj",], # Layers to apply LoRA to lora_alpha = 16, # LoRA alpha value lora_dropout = 0, # Supports any, but = 0 is optimized,防止过拟合,0 表示不drop任何参数 bias = "none", # Supports any, but = "none" is optimized use_gradient_checkpointing = "unsloth", # True or "unsloth" for very long context random_state = 3407, use_rslora = False, # We support rank stabilized LoRA loftq_config = None, # And LoftQ)
step 6:定义 trainer
trainer = SFTTrainer( model = model, tokenizer = tokenizer, train_dataset = dataset, dataset_text_field = "text", max_seq_length = max_seq_length, dataset_num_proc = 2, packing = False, # Can make training 5x faster for short sequences. args = TrainingArguments( per_device_train_batch_size = 2, # 每个GPU上的batch size gradient_accumulation_steps = 4, # 梯度累积步数 warmup_steps = 10, # max_steps = 200, # 最大训练步数 num_train_epochs=3, # 训练轮数 和 max_steps 二选一 learning_rate = 2e-4, # 学习率,默认值是 2.0e-5 fp16 = not is_bfloat16_supported(), bf16 = is_bfloat16_supported(), logging_steps = 2, output_dir = "outputs", optim = "adamw_8bit", seed = 3407, ), )
step 7:开始训练
train_stats = trainer.train()
Unsloth 会根据加载的模型和设备情况自动选择 GPU 数量。
如果默认 device_map="sequential",只有当单卡显存不够时,才占用其他卡。

如果设定 device_map="balanced",会占用所有卡,并均衡分布模型权重:

看到损失下降,说明成功开始训练:

step 8:模型保存
最后,别忘了保存训练好的模型权重:
model.save_pretrained( "ckpts/lora_model") tokenizer.save_pretrained( "ckpts/lora_model")
注:这里只会保存 LoRA 权重,在adapter_config.json会指定原始模型位置:

至此,我们成功走完了模型微调之旅,完整训练代码如下:
from unsloth import FastLanguageModel, is_bfloat16_supported from transformers import TrainingArguments from trl import SFTTrainer from datasets import load_dataset max_seq_length = 8192 # 模型处理文本的最大长度 # 加载模型 model, tokenizer = FastLanguageModel.from_pretrained( model_name = "ckpts/qwen-0.5b", max_seq_length = max_seq_length, dtype=None, # 自动检测合适的类型 load_in_4bit = True, # device_map="balanced" # 多卡训练时均衡分布模型权重,默认为sequential ) # 定义训练数据格式化字符串模板 train_prompt_style="""请遵循指令回答用户问题。 在回答之前,请仔细思考问题,并创建一个逻辑连贯的思考过程,以确保回答准确无误。 指令: 你是一位精通八字算命、紫微斗数、风水、易经卦象、塔罗牌占卜、星象、面相手相和运势预测等方面的算命大师。 请回答以下算命问题。 问题: {} 回答:
{}
{} """ # 加载数据集 dataset = load_dataset("data/fortune-telling", split="train") # train[0:200]可以指定前200条数据 def formatting_data(examples): questions = examples["Question"] cots = examples["Complex_CoT"] responses = examples["Response"] texts = [] for q, c, r in zip(questions, cots, responses): text = train_prompt_style.format(q, c, r) + tokenizer.eos_token texts.append(text) return {"text": texts} dataset = dataset.map(formatting_data, batched=True) # 添加 LoRA 权重 model = FastLanguageModel.get_peft_model( model, r = 16, # Rank of the LoRA matrix target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj",], # Layers to apply LoRA to lora_alpha = 16, # LoRA alpha value lora_dropout = 0, # Supports any, but = 0 is optimized,防止过拟合,0 表示不drop任何参数 bias = "none", # Supports any, but = "none" is optimized use_gradient_checkpointing = "unsloth", # True or "unsloth" for very long context random_state = 3407, use_rslora = False, # We support rank stabilized LoRA loftq_config = None, # And LoftQ ) trainer = SFTTrainer( model = model, tokenizer = tokenizer, train_dataset = dataset, dataset_text_field = "text", max_seq_length = max_seq_length, dataset_num_proc = 2, packing = False, # Can make training 5x faster for short sequences. args = TrainingArguments( per_device_train_batch_size = 2, # 每个GPU上的batch size gradient_accumulation_steps = 4, # 梯度累积步数 warmup_steps = 10, # max_steps = 200, # 最大训练步数 num_train_epochs=3, # 训练轮数 和 max_steps 二选一 learning_rate = 2e-4, # 学习率,默认值是 2.0e-5 fp16 = not is_bfloat16_supported(), bf16 = is_bfloat16_supported(), logging_steps = 2, output_dir = "outputs", optim = "adamw_8bit", seed = 3407, ), ) train_stats = trainer.train() # 保存模型权重 model.save_pretrained("ckpts/lora_model") tokenizer.save_pretrained("ckpts/lora_model")
对于 qwen2.5 1.5B而言,4G 显存即可开启微调:

模型测试代码如下:
from unsloth import FastLanguageModel import torch import re max_seq_length = 8192 # 加载模型 model, tokenizer = FastLanguageModel.from_pretrained( model_name="ckpts/lora_model", max_seq_length=max_seq_length, dtype=None, load_in_4bit=True, ) FastLanguageModel.for_inference(model) # 设置pad token if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token # 更简洁的提示模板 prompt_style = """你是一个算命大师。请直接回答用户的问题,不要输出任何思考过程、推理步骤或
标签。 用户问题:{} 你的回答:""" # 五个问题 questions = [ '1995年七月初十出生的人,2026年的整体运势如何?', '1995年七月初十出生的人,2026年的财运如何?', '1995年七月初十出生的人,2026年的感情运势如何?', '1995年七月初十出生的人,2026年的事业运势如何?', '1995年七月初十出生的人,2026年的健康运势如何?' ] for i, question in enumerate(questions, 1): print(f" {'='*50}") print(f"问题 {i}: {question}") print(f"{'='*50}") # 构造输入 text = prompt_style.format(question) inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512).to("cuda") # 生成参数 with torch.no_grad(): outputs = model.generate( inputs, max_new_tokens=300, temperature=0.7, do_sample=True, top_p=0.9, repetition_penalty=1.1, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id, ) # 解码 full_response = tokenizer.decode(outputs[0], skip_special_tokens=True) # 提取回答部分(去掉问题) if "你的回答:" in full_response: answer = full_response.split("你的回答:")[-1].strip() else: answer = full_response[len(text):].strip() # 清理可能残留的think标签 answer = re.sub(r'
.*?
', '', answer, flags=re.DOTALL) answer = re.sub(r'<|.*?|>', '', answer) # 移除特殊token # 如果答案为空,尝试另一种方式 if not answer or len(answer) < 10: print("正在重试...") # 更直接的提示 direct_prompt = f"问题:{question} 答案:" inputs2 = tokenizer(direct_prompt, return_tensors="pt", truncation=True, max_length=256).to("cuda") with torch.no_grad(): outputs2 = model.generate( inputs2, max_new_tokens=200, temperature=0.5, do_sample=True, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id, ) answer = tokenizer.decode(outputs2[0], skip_special_tokens=True) answer = answer.replace(direct_prompt, "").strip() print(answer if answer else "无法生成答案") print() # 可选:保存模型 # model.save_pretrained_gguf("ckpts/merged", tokenizer, quantization_method="q4_k_m") # 定义训练数据格式化字符串模板 prompt_style="""请遵循指令回答用户问题。 在回答之前,请仔细思考问题,并创建一个逻辑连贯的思考过程,以确保回答准确无误。 指令: 你是一位精通八字算命、紫微斗数、风水、易经卦象、塔罗牌占卜、星象、面相手相和运势预测等方面的算命大师。 请回答以下算命问题。 问题: {} 回答: """ question = '1995年七月初十生,今年是2025年,了解未来五年的运势' inputs = tokenizer([prompt_style.format(question)], return_tensors='pt', max_length=max_seq_length).to("cuda") outputs = model.generate(inputs['input_ids'], attention_mask=inputs['attention_mask'], max_length=max_seq_length, use_cache=True) answer = tokenizer.batch_decode(outputs, skip_special_tokens=True)[0] print(answer) # model.save_pretrained_gguf("ckpts/merged", tokenizer, quantization_method="q4_k_m")
注:如果单卡放不下模型权重,会报错,因为模型权重切分了,但 inputs 并没有切分,因此可考虑采用
vLLM 推理。
不了解 vLLM 推理 的小伙伴,可翻看上篇:比肩满血DS,阿里新王 QwQ-32B 本地部署,Ollma/vLLM 实测对比
首先,我们要将微调后的模型保存为 GGUF 格式:
model.save_pretrained_gguf(
"ckpts/merged", tokenizer, quantization_method="q4_k_m")
Unsloth 会自动下载编译 llama.cpp 进行格式转换:

过程中先转成 BF16,然后再进行 4bit 量化,权重大小分别为 3G 和 1G:

转换成功后,一键开启 vLLM 推理:
vllm serve ckpts/merged/
unsloth.Q4_K_M.gguf --api-key 123 --port 3002
本文分享了开源大模型微调工具 Unsloth,并通过一个简单例子,带大家走完了微调全流程。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/272188.html