AI

深入理解大模型训练推理过程(1):从本地运行开始

2026年8月17日 阅读(9)

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. 模型训练与微调
(LoRA, SFT, RLHF)

Transformers (Safetensors)

唯一源头。 所有其他格式都从它转换而来。生态最全(Trainer, TRL, LoRA),必须用这种格式才能“修改模型”。

2. Mac本地极速推理
(追求原生性能)

MLX (Safetensors)

苹果专属引擎。 文件结构与 Transformers 一样,但内部权重已针对 Apple GPU 重新排列。调用 mlx-lm

即可无缝调用 Metal 加速。

3. Mac/端侧“开箱即用”
(无需代码环境)

GGUF

单文件分发王。 一个 .gguf

文件打包了权重、配置、分词器。配合 Ollama/LM Studio 即开即用,最爱消费级硬件。

4. CPU服务器/跨平台部署
(追求通用性)

ONNX

跨框架万能胶。 针对非 NVIDIA 硬件或纯 CPU 环境优化最好,onnxruntime

生态成熟,适合 Embedding 或轻量级模型。

5. 向量检索/语义匹配
(RAG, 搜索)

Sentence-Transformers
(基于 Transformers)

专用流水线。 这是一种特殊文件结构(含 1_Pooling

等),直接输出语义向量而非文本,是 RAG 系统的基石。

6. 手机/浏览器/IoT
(追求极致端侧)

MediaPipe / TFLite / ExecuTorch

AOT编译产物。 需将模型预编译为针对特定硬件(如苹果神经引擎ANE)的二进制文件,体积和功耗极低,但灵活性最差,不通用。

7. 纯NVIDIA生产环境
(低延迟、高吞吐)

TensorRT-LLM Engine

特化编译王。 针对特定 GPU 型号和 batch size 深度优化。性能天花板极高,但必须“换一张卡就重编一次”,不适合个人实验。

8. 模型安全分发

Safetensors

通用容器标准。 它不是独立格式,而是安全的权重容器。无论你搞 TF、PyTorch 还是 MLX,只要看到 .safetensors

,就知道加载又快又防病毒。

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

文件名

大小

类别

用途详细说明

model-00001-of-00002.safetensors

5.35 GB

权重文件

模型的主权重文件(第1片)。包含绝大部分 Transformer 层和 Embedding 的参数。由于经过了 4bit 量化,体积显著缩小。专门针对 Apple GPU 内存对齐排布。

model-00002-of-00002.safetensors

600 MB

权重文件

模型的辅权重文件(第2片)。包含剩余的参数(如尾部网络层或 lm_head 输出层)。因为单文件超 2GB 会触发限制,所以被切分成两个文件。

model.safetensors.index.json

123 KB

权重索引

权重分布地图。记录了“哪一个张量在第几个 safetensors 文件里”,让 MLX 框架知道该去哪个文件读取特定的模型参数。

config.json

3.3 KB

核心配置

模型架构图纸。定义了模型有几层、维度多大、注意力头数、上下文长度,以及标明了是 4bit 量化格式。

chat_template.jinja

7.7 KB

对话模板

对话格式渲染脚本。一段 Jinja2 代码,定义了如何将 {"role": "user", "content": "..."}

格式转换为模型认识的 `<

tokenizer.json

19.9 MB

分词器

分词器核心引擎。Rust 编写的高速分词状态机,负责将文本转换为模型能吃进去的数字 ID。

tokenizer_config.json

1.1 KB

分词器配置

分词器行为规则。定义了特殊符号(如结束符 `<

vocab.json

6.7 MB

分词器字典

基础词表。记录了“字/词”与“数字 ID”的对应关系。tokenizer.json的原始数据源。

preprocessor_config.json

390 B

多模态配置

图像预处理规则。告诉模型收到图片时,要裁剪/缩放到多大尺寸(如 448×448)、归一化均值和方差是多少。

video_preprocessor_config.json

385 B

多模态配置

视频预处理规则。定义处理视频时的抽帧频率、帧尺寸等。这是证明该模型支持视频输入的关键文件。

processor_config.json

1.3 KB

多模态配置

多模态融合处理器配置。定义如何将文本、图像、视频的特征拼接在一起喂给大模型。

README.md

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 格式在转换时,做了一件看似微小但极其重要的事:

  1. config.json 里的 torch_dtype 改成了 "float16" "bfloat16"(MLX 默认用 float16,而 PyTorch 习惯用 bfloat16 或 float32)。
  2. 把 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 首高质量、体裁标准的唐诗。

按优先级依次过滤:

  1. 完整性:title 非空,author 非空,paragraphs 非空且长度 > 0
  2. 体裁合规:通过算法检测为标准体裁(五言/七言-绝句/律诗,排除古体诗、杂言诗、乐府等(格律不固定,对微调不利)
  3. 字数严格匹配:每句字数完全符合体裁要求(允许少量容差,如 ±0)
  4. 内容有效:去除含乱码、非汉字字符(日文假名、特殊符号)的诗歌
  5. 去重:内容完全相同的诗歌仅保留一条(取作者知名度最高者)
  6. 作者覆盖度:对同一作者最多保留 N 首(如 50 首),避免风格单一化;优先保留知名诗人作品(李白、杜甫、王维、白居易、李商隐、杜牧等)
  7. 体裁均衡:确保 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