1.下载模型文件
https://hf-mirror.com/mlx-community/models?search=qwen3.5
https://modelscope.cn/models?name=qwen
找到需要下载的模型,模型数据默认缓存在:/Users/peile.duan/.cache/huggingface/hub/,可通过–local-dir指定下载路径。具体命令如下:
uv pip install huggingface_hub
export HF_ENDPOINT=https://hf-mirror.com
hf download Qwen/Qwen2.5-0.5B-Instruct --local-dir ./models/Qwen2.5-0.5B-Instruct
uv pip install modelscope
modelscope download --model Qwen/Qwen3.5-9B --local-dir ./models/Qwen3.5-9B
2.模型格式说明
2.1 格式对比
2.1.1 信息组成
一个”能用的大模型”至少包含 4 部分信息:
|
组成 |
说明 |
|
① 权重 |
几十亿个浮点数,占体积 99%+ |
|
② 计算图/架构 |
这些权重怎么组织成 Transformer,RoPE 怎么算,Attention 怎么连 |
|
③ 超参数 |
层数、隐藏维度、head 数、词表大小、RoPE base…… |
|
④ Tokenizer |
词表、merges、特殊 token、chat template |
各种格式的本质区别,就是这 4 样东西”放在哪里”。
|
格式 |
本质 |
计算图在哪 |
权重存储 |
典型体积(7B) |
|
Transformers (safetensors) |
权重字典 + Python 代码 |
Python 库 |
safetensors |
~14 GB |
|
GGUF |
自描述单文件 |
C++ 代码 (llama.cpp) |
内嵌 |
~14 GB (F16) / ~4 GB (Q4) |
|
ONNX |
Protobuf 计算图 |
文件内 |
内嵌/外置 |
~14 GB |
|
MLX |
HF 结构 + Apple 重排 |
MLX 库 |
safetensors |
~14 GB / ~4 GB (4bit) |
|
TensorRT-LLM (.engine) |
AOT 编译产物 |
编译进二进制 |
内嵌 |
~7 GB (FP8) |
|
OpenVINO IR |
XML 图 + bin 权重 |
文件内 |
.bin |
~14 GB |
|
CoreML (.mlpackage) |
Apple 编译产物 |
内嵌 |
内嵌 |
~7 GB |
|
GPTQ / AWQ |
HF 格式 + 量化打包 |
Python 库 |
safetensors (int4) |
~4 GB |
|
EXL2 / EXL3 |
可变比特率量化 |
ExLlamaV2 代码 |
safetensors |
~3.5 GB |
|
TFLite / ExecuTorch |
移动端编译图 |
内嵌 |
内嵌 |
~4 GB (int8) |
2.1.2 应用场景
HF/Transformers 格式是唯一的”母版”(能训练、生态最全);GGUF 是为消费级本地推理做的单文件自包含分发格式;ONNX 是为跨框架跨硬件设计的自描述计算图;TensorRT/CoreML 之类是针对特定硬件的预编译产物。
|
核心场景 |
最佳格式 |
推荐理由与关键原则 |
|
1. 模型训练与微调 |
Transformers (Safetensors) |
唯一源头。 所有其他格式都从它转换而来。生态最全(Trainer, TRL, LoRA),必须用这种格式才能“修改模型”。 |
|
2. Mac本地极速推理 |
MLX (Safetensors) |
苹果专属引擎。 文件结构与 Transformers 一样,但内部权重已针对 Apple GPU 重新排列。调用 即可无缝调用 Metal 加速。 |
|
3. Mac/端侧“开箱即用” |
GGUF |
单文件分发王。 一个 文件打包了权重、配置、分词器。配合 Ollama/LM Studio 即开即用,最爱消费级硬件。 |
|
4. CPU服务器/跨平台部署 |
ONNX |
跨框架万能胶。 针对非 NVIDIA 硬件或纯 CPU 环境优化最好, 生态成熟,适合 Embedding 或轻量级模型。 |
|
5. 向量检索/语义匹配 |
Sentence-Transformers |
专用流水线。 这是一种特殊文件结构(含 等),直接输出语义向量而非文本,是 RAG 系统的基石。 |
|
6. 手机/浏览器/IoT |
MediaPipe / TFLite / ExecuTorch |
AOT编译产物。 需将模型预编译为针对特定硬件(如苹果神经引擎ANE)的二进制文件,体积和功耗极低,但灵活性最差,不通用。 |
|
7. 纯NVIDIA生产环境 |
TensorRT-LLM Engine |
特化编译王。 针对特定 GPU 型号和 batch size 深度优化。性能天花板极高,但必须“换一张卡就重编一次”,不适合个人实验。 |
|
8. 模型安全分发 |
Safetensors |
通用容器标准。 它不是独立格式,而是安全的权重容器。无论你搞 TF、PyTorch 还是 MLX,只要看到 ,就知道加载又快又防病毒。 |
2.2 标准Transformers模型文件说明
(AI) peile.duan@U-7K6V5HCV-0153 AI % ls -rlt models/Qwen3.5-9B-MLX-4bit/
total 11674032
-rw-r--r-- 1 peile.duan staff 600449850 8 6 22:38 model-00002-of-00002.safetensors
-rw-r--r-- 1 peile.duan staff 1954 8 6 22:38 README.md
-rw-r--r-- 1 peile.duan staff 19989343 8 6 22:38 tokenizer.json
-rw-r--r-- 1 peile.duan staff 3331 8 6 22:38 config.json
-rw-r--r-- 1 peile.duan staff 1300 8 6 22:38 processor_config.json
-rw-r--r-- 1 peile.duan staff 5349771222 8 6 22:38 model-00001-of-00002.safetensors
-rw-r--r-- 1 peile.duan staff 1139 8 6 22:38 tokenizer_config.json
-rw-r--r-- 1 peile.duan staff 6722759 8 6 22:38 vocab.json
-rw-r--r-- 1 peile.duan staff 385 8 6 22:38 video_preprocessor_config.json
-rw-r--r-- 1 peile.duan staff 390 8 6 22:38 preprocessor_config.json
-rw-r--r-- 1 peile.duan staff 123592 8 6 22:38 model.safetensors.index.json
-rw-r--r-- 1 peile.duan staff 7756 8 6 22:38 chat_template.jinja
|
文件名 |
大小 |
类别 |
用途详细说明 |
|
|
5.35 GB |
权重文件 |
模型的主权重文件(第1片)。包含绝大部分 Transformer 层和 Embedding 的参数。由于经过了 4bit 量化,体积显著缩小。专门针对 Apple GPU 内存对齐排布。 |
|
|
600 MB |
权重文件 |
模型的辅权重文件(第2片)。包含剩余的参数(如尾部网络层或 lm_head 输出层)。因为单文件超 2GB 会触发限制,所以被切分成两个文件。 |
|
|
123 KB |
权重索引 |
权重分布地图。记录了“哪一个张量在第几个 safetensors 文件里”,让 MLX 框架知道该去哪个文件读取特定的模型参数。 |
|
|
3.3 KB |
核心配置 |
模型架构图纸。定义了模型有几层、维度多大、注意力头数、上下文长度,以及标明了是 4bit 量化格式。 |
|
|
7.7 KB |
对话模板 |
对话格式渲染脚本。一段 Jinja2 代码,定义了如何将 格式转换为模型认识的 `< |
|
|
19.9 MB |
分词器 |
分词器核心引擎。Rust 编写的高速分词状态机,负责将文本转换为模型能吃进去的数字 ID。 |
|
|
1.1 KB |
分词器配置 |
分词器行为规则。定义了特殊符号(如结束符 `< |
|
|
6.7 MB |
分词器字典 |
基础词表。记录了“字/词”与“数字 ID”的对应关系。 |
|
|
390 B |
多模态配置 |
图像预处理规则。告诉模型收到图片时,要裁剪/缩放到多大尺寸(如 448×448)、归一化均值和方差是多少。 |
|
|
385 B |
多模态配置 |
视频预处理规则。定义处理视频时的抽帧频率、帧尺寸等。这是证明该模型支持视频输入的关键文件。 |
|
|
1.3 KB |
多模态配置 |
多模态融合处理器配置。定义如何将文本、图像、视频的特征拼接在一起喂给大模型。 |
|
|
1.9 KB |
说明文档 |
模型的说明书,通常包含如何使用该模型的示例代码和版本声明。 |
3.Mac本地运行-推理
Mac 本地运行大模型的核心优势在于其统一内存架构,CPU 与 GPU 共享海量系统内存,打破了传统 PC 独立显存的容量瓶颈,让轻薄本也能流畅跑起几十亿甚至百亿参数的大模型;同时兼具功耗极低、静音运行与数据绝对隐私的开发友好特性。
3.1 使用transformers库
3.1.1 库说明
Transformers 是 Hugging Face 推出的模型标准库,它的核心优势在于”通用性”和”生态垄断地位”——它是目前唯一一个既能加载几乎所有主流开源模型(Qwen、LLaMA、Mistral、BERT、T5……)、又能同时支持训练/微调和推理的 Python 库,from_pretrained() 一行代码就能加载 Hugging Face Hub 上 50 万+ 模型,Trainer API 可以直接微调,pipeline 接口让零基础用户也能跑通情感分类、命名实体识别、文本生成等任务,并且所有其他推理框架(vLLM、SGLang、MLX、llama.cpp)的模型格式几乎都从它转换而来,可以说它定义了整个开源大模型生态的”通用语言”。
作为一个追求通用性的库,它在任何一个具体场景上都不是最优的——推理吞吐量远不如 vLLM/SGLang(没有 PagedAttention、没有连续批处理、KV Cache 管理粗糙,多用户并发时性能可能只有 vLLM 的 1/5),在 Apple Silicon 上的速度不如原生的 MLX(因为底层走的是 PyTorch 的 MPS 后端,未针对 Metal 深度优化),在消费级 CPU 上不如 llama.cpp(缺乏 GGUF 量化和 SIMD 指令级优化),量化生态碎片化严重(bitsandbytes/GPTQ/AWQ 各一套接口),而且必须依赖 Python + PyTorch 的重型环境。
它最适合的场景是:模型训练和微调(LoRA/QLoRA/全量微调,这是其他框架做不了的)、快速原型验证和实验(几行代码跑通一个新模型看效果)、教学和学习(API 最标准、文档最全、社区最大)、以及作为其他框架的模型格式转换源头;而它不适合的场景是:生产环境的高并发 API 服务(应该用 vLLM/SGLang)、Mac 上追求极致推理速度(应该用 MLX 或 Ollama)、消费级设备上跑大模型(应该用 GGUF + llama.cpp)、以及对延迟极度敏感的实时应用(应该用 TensorRT-LLM)。
3.1.2 模型运行
安装依赖库
uv pip install transformers torch accelerate
运行如下example代码
python example.py
from transformers import AutoTokenizer, AutoModelForCausalLM
path = "./models/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(path)
model = AutoModelForCausalLM.from_pretrained(path)
messages = [{"role": "user", "content": "你好,介绍一下你自己"}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=128)
# 只解码新生成的部分
reply = tokenizer.decode(
outputs[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True
)
print(reply)
3.2 使用mlx库
MPS 的优势在于统一内存:CPU 和 GPU 共享同一块内存,不存在”显存不够”的问题(只要系统内存够大)。所以 M3 Max 128GB 可以跑 70B 模型,这在消费级 NVIDIA 卡上几乎不可能。
MLX 的设计哲学就是为 Apple Silicon 统一内存架构优化的,K-quants 是其原生支持的推理格式。而 Transformers 的 MPS 后端对量化支持极差(几乎不可用),FP16 就是它在 Mac 上的”最佳实践”。大量评测已证明 Q4_K_M 与 FP16 的 perplexity 差距 < 0.5%。
除了推理之后,mlx-lm 支持在 Mac 上做 LoRA 微调和全量微调。
3.2.1 MLX格式下载与说明
可以通过如下命令直接下载MLX格式的模型文体
hf download mlx-community/Qwen3.5-9B-MLX-4bit --local-dir ./models/Qwen3.5-9B-MLX-4bit
或者通过使用如下命令,将标准格式转换为MLX量化格式:
# 标准 HF 格式 → MLX 格式 (包括量化和重排)
mlx_lm.convert \
--hf-path ./models/Qwen3.5-9B \
--mlx-path ./models/Qwen3.5-9B-MLX-q4km \
-q \
--q-bits 4 \
--q-group-size 64
从目录结构来看,MLX 格式和标准 Transformers 格式看起来几乎一模一样,因为它们用的都是同一套文件规范(config.json + safetensors + tokenizer.json)。
但它们之间有一个最核心、最本质的区别:
标准 Transformers 格式的权重,是给 PyTorch 用的(CPU/GPU 通用);而 MLX 格式的权重,是给 Apple Silicon 的 MLX 框架专门重新排列过的(M 系列芯片专用)。
Apple Silicon(M1/M2/M3/M4)的统一内存架构有一个特点:GPU 访问内存时,要求数据必须是 16 字节对齐的。标准的 PyTorch safetensors 文件里,权重的排布是按照 NVIDIA GPU 或 CPU 的习惯来的。如果直接用 MLX 框架读取标准的 safetensors,在把权重交给 Apple GPU 计算时,会频繁触发内存重排,导致速度极慢。所以,MLX 格式在转换时,做了一件看似微小但极其重要的事:
- 把
config.json里的torch_dtype改成了"float16"或"bfloat16"(MLX 默认用 float16,而 PyTorch 习惯用 bfloat16 或 float32)。 - 把 safetensors 里的张量数据,按照 Apple GPU 的对齐要求进行了“预重排”和打包。
3.2 模型运行
安装依赖库
uv pip install mlx-lm mlx-vlm mlx
运行如下example代码
python example.py
from mlx_lm import load, generate
path = "./models/Qwen2.5-0.5B-Instruct"
model, tokenizer = load(path)
messages = [{"role": "user", "content": "你好,介绍一下你自己"}]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
# 最简版本,只传必要参数
reply = generate(
model,
tokenizer,
prompt=text,
max_tokens=128,
verbose=False
)
print(reply)
3.3 性能对比
cpu vs PyTorch MPS vs Mlx
|
模型 |
格式 |
Tokens/s |
相对速度 |
|
Qwen2.5-0.5B-Instruct |
Transformers MPS |
~45 |
1.0x |
|
Qwen2.5-0.5B-Instruct |
MLX FP16 |
~55 |
1.2x |
|
Qwen2.5-7B-Instruct |
Transformers MPS |
~12 |
1.0x |
|
Qwen2.5-7B-Instruct |
MLX FP16 |
~28 |
2.3x |
|
Qwen2.5-7B-Instruct |
MLX 4bit |
~45 |
3.8x |
|
Qwen2.5-14B-Instruct |
Transformers MPS |
OOM |
— |
|
Qwen2.5-14B-Instruct |
MLX 4bit |
~22 |
∞ |
实测结果如下,将原始模型转换为4bit量化版本之后,tps可以提高到原来的3倍。
mlx_lm.generate \
--model ./models/Qwen3.5-9B-MLX-q4km \
--prompt "Test prompt" \
--max-tokens 512 \
--verbose True
==========
Prompt: 12 tokens, 55.031 tokens-per-sec
Generation: 512 tokens, 25.975 tokens-per-sec
Peak memory: 5.165 GB
mlx_lm.generate \
--model ./models/Qwen3.5-9B-MLX \
--prompt "Test prompt" \
--max-tokens 512 \
--verbose True
==========
Prompt: 12 tokens, 5.307 tokens-per-sec
Generation: 214 tokens, 8.077 tokens-per-sec
Peak memory: 17.998 GB
4.Mac本地运行-微调
Mac 的显存(统一内存)虽然大(最高 128GB),但 GPU 算力远不如 NVIDIA,所以全量微调基本不可行,LoRA/QLoRA 是唯一的现实选择。
|
内存 |
适合做什么 |
|
8GB |
0.5B LoRA,短文本,batch=1 |
|
16GB |
0.5B~1.5B LoRA 比较合适 |
|
32GB |
3B LoRA 可试,7B 量化 LoRA 谨慎 |
|
64GB+ |
7B 量化 LoRA 有机会 |
|
128GB+ |
更大模型可实验,但效率仍不如云 GPU |
4.1 模型选择&数据准备
4.1.1 模型选择
做中文微调实验,要选”中文能力弱、但词表里有中文 token”的模型,因此首选国外开源模型。
这里选择以google/gemma-3-270m-it为示例,对它进行微调以增强古诗词能力。
uv pip install modelscope
modelscope download --model google/gemma-3-270m-it --local-dir ./models/gemma-3-270m-it
4.1.2 基于大模型的指令合成(Self-Instruct / 数据蒸馏)
目标:对 gemma-3-270m-it进行LoRA 微调,使其能根据用户指定的体裁(五言绝句/七言绝句/五言律诗/七言律诗)和主题,生成风格合规、格律正确的唐诗。基线模型当前输出质量极差(乱码、格律混乱、混入非中文字符),微调后需显著改善。
首先下载诗歌集,从 31 万首中筛选 ≤10,000 首高质量、体裁标准的唐诗。
按优先级依次过滤:
- 完整性:title 非空,author 非空,paragraphs 非空且长度 > 0
- 体裁合规:通过算法检测为标准体裁(五言/七言-绝句/律诗,排除古体诗、杂言诗、乐府等(格律不固定,对微调不利)
- 字数严格匹配:每句字数完全符合体裁要求(允许少量容差,如 ±0)
- 内容有效:去除含乱码、非汉字字符(日文假名、特殊符号)的诗歌
- 去重:内容完全相同的诗歌仅保留一条(取作者知名度最高者)
- 作者覆盖度:对同一作者最多保留 N 首(如 50 首),避免风格单一化;优先保留知名诗人作品(李白、杜甫、王维、白居易、李商隐、杜牧等)
- 体裁均衡:确保 4 种体裁在最终数据集中比例合理(建议:五绝 30%、七绝 30%、五律 20%、七律 20%)
最终保留 8,000-10,000 首,4 种体裁覆盖均衡,作者覆盖 ≥200 位(含主要诗人)。之后为每首诗构造多样化的 instruction-response 训练样本。
Gemma-3-270M 的参数上限决定了它永远无法达到 7B+ 模型的古诗创作水平。它的最佳定位是:唐诗补全/默写助手。因此首先将唐诗300首中100首的默写和补全做完训练目标,使用如下命令产生训练数据:
python tang_300.py --num-poems 100
样本示例:
{"messages": [{"role": "user", "content": "补全上句:____,凤城寒尽怕春宵。"}, {"role": "assistant", "content": "为有云屏无限娇。"}]}
{"messages": [{"role": "user", "content": "补全上句:____,翠华摇摇行复止。"}, {"role": "assistant", "content": "千乘万骑西南行。"}]}
{"messages": [{"role": "user", "content": "补全下句:黄鹤一去不复返,____"}, {"role": "assistant", "content": "白云千载空悠悠。"}]}
{"messages": [{"role": "user", "content": "补全下句:势拔五岳掩赤城,____"}, {"role": "assistant", "content": "天台四万八千丈。"}]}
{"messages": [{"role": "user", "content": "补全上句:____,役夫敢申恨。"}, {"role": "assistant", "content": "长者虽有问。"}]}
{"messages": [{"role": "user", "content": "补全上句:____,便下襄阳向洛阳。"}, {"role": "assistant", "content": "即从巴峡穿巫峡。"}]}
{"messages": [{"role": "user", "content": "补全上句:____,列缺霹雳。"}, {"role": "assistant", "content": "水澹澹兮生烟。"}]}
{"messages": [{"role": "user", "content": "补全上句:____,又送王孙去。"}, {"role": "assistant", "content": "晴翠接荒城。"}]}
{"messages": [{"role": "user", "content": "默写李白《清平调 二》全文"}, {"role": "assistant", "content": "一枝红艳露凝香,云雨巫山枉断肠。\n借问汉宫谁得似,可怜飞燕倚新妆。"}]}
{"messages": [{"role": "user", "content": "默写李商隐《落花》全文"}, {"role": "assistant", "content": "高阁客竟去,小园花乱飞。\n参差连曲陌,迢遰送斜晖。\n肠断未忍扫,眼穿仍欲归。\n芳心向春尽,所得是沾衣。"}]}
4.2 Mlx
MLX 是 Apple 官方推出的机器学习框架,专为 M 系列芯片优化,微调速度比 PyTorch MPS 快 2-5 倍。
先将模型转换为Mlx格式
mlx_lm.convert \
--hf-path models/gemma-3-270m-it \
--mlx-path models/gemma-3-270m-it-MLX
4.2.1 执行微调
mlx_lm.lora \
--model ./models/gemma-3-270m-it-MLX \
--train \
--data ./debug_data \
--iters 200 \
--batch-size 2 \
--learning-rate 3e-5 \
--adapter-path ./adapters \
--val-batches 2 \
--steps-per-eval 50 \
--max-seq-length 256
mlx_lm.lora \
--model ./models/gemma-3-270m-it-MLX \
--train \
--data ./llm-lib/experiments/lora/data/ \
--config lora_config.yaml \
--iters 3000 \
--batch-size 4 \
--learning-rate 4e-5 \
--adapter-path ./adapters \
--val-batches 25 \
--steps-per-eval 100 \
--max-seq-length 512 \
--save-every 500
cat lora_config.yaml
lora_layers: 18 # 全层开启
rank: 16 # 100首仅需 ~1000 个映射,rank=16 提供充足冗余
alpha: 32 # rank × 2
dropout: 0.0 # 精确记忆任务不加 dropout
target_modules:
- q_proj
- v_proj
- k_proj
- o_proj
- gate_proj
- up_proj
- down_proj
简单测试
mlx_lm.generate \
--model ./models/gemma-3-270m-it-MLX \
--adapter-path ./adapters \
--prompt "补全下句:古木无人径,____" \
--max-tokens 64 \
--temp 0
mlx_lm.generate \
--model ./models/gemma-3-270m-it-MLX \
--adapter-path ./adapters \
--prompt "默写《春晓》全文" \
--max-tokens 64 \
--temp 0
mlx_lm.generate --model ./models/gemma-3-270m-it-MLX \
--adapter-path ./adapters \
--prompt "补全下句:渭城朝雨浥轻尘,____"
--max-tokens 64 \
--temp 0
mlx_lm.generate --model ./models/gemma-3-270m-it-MLX \
--adapter-path ./adapters \
--prompt "默写王维《过香积寺》全文" \
--max-tokens 64 \
--temp 0
4.2.2 合并 LoRA 权重(可选)
mlx_lm.fuse \
--model ./models/gemma-3-270m-it-MLX \
--adapter-path ./adapters \
--save-path ./models/gemma-3-270m-it-MLX-finetuned
4.2.3 使用微调后的模型
mlx_lm.generate \
--model ./models/gemma-3-270m-it-MLX-finetuned \
--prompt "默写《春晓》全文" \
--max-tokens 64 \
--temp 0
4.2.4 结果评测
(AI) peile.duan@U-7K6V5HCV-0153 AI % mlx_lm.generate \
--model ./models/gemma-3-270m-it \
--prompt "默写《春晓》全文" \
--max-tokens 64 \
--temp 0
==========
《春晓》是一部充满诗意的中国古典小说,讲述了主人公在春天的到来与春晓的可爱与美丽之间的故事。
==========
Prompt: 16 tokens, 454.953 tokens-per-sec
Generation: 32 tokens, 187.434 tokens-per-sec
Peak memory: 0.554 GB
==========
(AI) peile.duan@U-7K6V5HCV-0153 AI % mlx_lm.generate \
--model ./models/gemma-3-270m-it-MLX-finetuned \
--prompt "默写《春晓》全文" \
--max-tokens 64 \
--temp 0
==========
春眠不觉晓,处处闻啼鸟。
夜来风雨声,花落知多少。
==========
Prompt: 16 tokens, 502.232 tokens-per-sec
Generation: 25 tokens, 185.186 tokens-per-sec
Peak memory: 0.554 GB
(AI) peile.duan@U-7K6V5HCV-0153 AI % mlx_lm.generate \
--model ./models/gemma-3-270m-it-MLX \
--prompt "补全下句:渭城朝雨浥轻尘,____" \
--max-tokens 64 \
--temp 0
==========
渭城朝雨浥輕塵。
==========
Prompt: 23 tokens, 691.148 tokens-per-sec
Generation: 10 tokens, 205.328 tokens-per-sec
Peak memory: 0.561 GB
(AI) peile.duan@U-7K6V5HCV-0153 AI % mlx_lm.generate \
--model ./models/gemma-3-270m-it-MLX-finetuned \
--prompt "补全下句:渭城朝雨浥轻尘,____" \
--max-tokens 64 \
--temp 0
==========
客舍青青杨柳春。
==========
Prompt: 23 tokens, 672.014 tokens-per-sec
Generation: 9 tokens, 204.445 tokens-per-sec
Peak memory: 0.561 GB
mlx_lm.generate \
--model ./models/gemma-3-270m-it \
--prompt "写一首关于春天的五言律诗" \
--max-tokens 64 \
--temp 0
mlx_lm.generate \
--model ./models/gemma-3-270m-it-MLX-finetuned \
--prompt "写一首关于春天的五言律诗" \
--max-tokens 64 \
--temp 0
4.3 Transformers + PyTorch MPS + PEFT
pip install torch transformers accelerate peft datasets
4.2.1 执行微调
import torch
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
DataCollatorForSeq2Seq,
)
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
# ==================== 1. 加载模型和分词器 ====================
# ⚠️ 必须使用 HF 格式的 Gemma-3-270M-it,不能用 MLX 格式
model_path = "./models/gemma-3-270m-it"
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.bfloat16, # Gemma3 推荐 bf16(MPS 用 float16)
device_map="auto",
trust_remote_code=True,
)
# ==================== 2. 配置 LoRA(严格对齐 lora_config.yaml)====================
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # rank: 16
lora_alpha=32, # alpha: 32 (rank × 2)
lora_dropout=0.0, # dropout: 0.0(精确记忆任务不加)
target_modules=[ # 7 个模块,顺序无关
"q_proj", "v_proj", "k_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
bias="none",
layers_to_transform=list(range(18)), # lora_layers: 18(全层)
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 预期: trainable params: ~500K || all params: ~270M || trainable%: ~0.18%
# ==================== 3. 准备数据(对齐 MLX chat template 处理)====================
MAX_SEQ_LENGTH = 512 # 对齐 --max-seq-length 512
def format_and_tokenize(example):
"""
应用 chat template → tokenize → 生成 labels
对齐 mlx_lm 内部的 apply_chat_template + loss masking 逻辑
"""
text = tokenizer.apply_chat_template(
example["messages"],
tokenize=False,
add_generation_prompt=False,
)
tokenized = tokenizer(
text,
truncation=True,
max_length=MAX_SEQ_LENGTH,
padding=False, # 动态 padding,由 DataCollator 处理
)
# labels = input_ids,DataCollatorForSeq2Seq 会自动将 user tokens mask 为 -100
tokenized["labels"] = tokenized["input_ids"].copy()
return tokenized
dataset = load_dataset(
"json",
data_files={
"train": "./llm-lib/experiments/lora/data/train.jsonl",
"validation": "./llm-lib/experiments/lora/data/valid.jsonl",
},
)
tokenized_dataset = dataset.map(
format_and_tokenize,
remove_columns=["messages"],
desc="Tokenizing",
)
# ==================== 4. 训练配置(逐参数对齐 mlx_lm.lora)====================
training_args = TrainingArguments(
output_dir="./adapters", # 对齐 --adapter-path ./adapters
num_train_epochs=5, # 3000 iters ÷ (train_samples/batch) ≈ 4-5 epochs
per_device_train_batch_size=4, # 对齐 --batch-size 4
per_device_eval_batch_size=4, # 对齐 --val-batches 25 中的 batch size
learning_rate=4e-5, # 对齐 --learning-rate 4e-5
lr_scheduler_type="cosine", # mlx_lm 默认 cosine
warmup_ratio=0.05, # mlx_lm 默认 5% warmup
bf16=True, # Gemma3 推荐;MPS 改为 fp16=True
eval_strategy="steps", # 对齐 --steps-per-eval
eval_steps=100, # 对齐 --steps-per-eval 100
save_strategy="steps", # 对齐 --save-every
save_steps=500, # 对齐 --save-every 500
load_best_model_at_end=True, # 自动选 Val Loss 最低的 checkpoint
metric_for_best_model="eval_loss",
greater_is_better=False,
logging_steps=50,
report_to="none",
optim="adamw_torch",
remove_unused_columns=False, # ⚠️ 必须!chat template 已自行处理字段
dataloader_pin_memory=False, # MPS 兼容性
)
# ==================== 5. 开始训练 ====================
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["validation"],
# ⚠️ 关键:用 Seq2Seq collator 而非 LanguageModeling collator
# 它会自动将 prompt (user) 部分的 labels 设为 -100,仅计算 assistant 回复的 loss
# 这与 mlx_lm.lora 的 loss masking 行为完全一致
data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True),
)
trainer.train()
# ==================== 6. 保存 LoRA 权重 ====================
model.save_pretrained("./adapters")
tokenizer.save_pretrained("./adapters")
训练花了15分钟。
4.2.2 合并权重
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# === 路径配置 ===
BASE_MODEL_PATH = "./models/gemma-3-270m-it" # HF 格式基座模型
ADAPTER_PATH = "./adapters" # LoRA adapter 目录
MERGED_OUTPUT_PATH = "./models/gemma-3-270m-it-tang100-merged"
# === 1. 加载基座模型 ===
print("Loading base model...")
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL_PATH,
torch_dtype=torch.bfloat16, # MPS 用户改为 torch.float16
device_map="cpu", # 合并在 CPU 上进行,避免显存溢出
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_PATH)
# === 2. 加载并合并 LoRA 权重 ===
print("Merging LoRA adapter...")
model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)
model = model.merge_and_unload() # ⚠️ 核心操作:合并 + 卸载 adapter
# === 3. 保存合并后的完整模型 ===
print(f"Saving merged model to {MERGED_OUTPUT_PATH}...")
model.save_pretrained(MERGED_OUTPUT_PATH)
tokenizer.save_pretrained(MERGED_OUTPUT_PATH)
print("✅ Merge complete!")
print(f" Base model: {BASE_MODEL_PATH}")
print(f" Adapter: {ADAPTER_PATH}")
print(f" Merged: {MERGED_OUTPUT_PATH}")
4.2.3 使用微调后的模型
mlx_lm.generate \
--model ./models/gemma-3-270m-it-tang100-merged \
--prompt "默写《春晓》全文" \
--max-tokens 64 \
--temp 0
4.4 Swift
python -m swift.cli.sft \
--model ./models/gemma-3-270m-it \
--dataset ./llm-lib/experiments/lora/data/train.jsonl \
--val_dataset ./llm-lib/experiments/lora/data/valid.jsonl \
--tuner_type lora \
--lora_rank 16 \
--lora_alpha 32 \
--lora_dropout 0.0 \
--target_modules q_proj k_proj v_proj o_proj gate_proj up_proj down_proj \
--max_length 512 \
--per_device_train_batch_size 4 \
--learning_rate 4e-5 \
--num_train_epochs 5 \
--lr_scheduler_type cosine \
--warmup_ratio 0.05 \
--eval_steps 100 \
--save_steps 500 \
--output_dir ./saves/tang100-swift \
--bf16 true \
--load_best_model_at_end true \
--metric_for_best_model eval_loss \
--greater_is_better false \
--logging_steps 50 \
--log_level debug \
--report_to none
# 合并 LoRA 权重
swift export \
--model ./models/gemma-3-270m-it \
--adapters ./saves/tang100-swift/checkpoint-best \
--merge_lora true \
--output_dir ./models/gemma-3-270m-it-tang100-merged
# 导出 GGUF(用于 llama.cpp / Ollama)
swift export \
--model ./models/gemma-3-270m-it-tang100-merged \
--to_gguf true \
--gguf_quant q4_k_m \
--output_dir ./models/gemma-3-270m-it-tang100-q4km