☰
Mac 上微调大模型实测:Unsloth、MLX-LM、PyTorch MPS 与 llama.cpp 对比
2026/11/2 3:29:18 网站建设 项目流程

先交代背景。我手头的主力设备是一台 M3 Max 64GB 的 MacBook Pro,平时既要写业务代码,又要做本地大模型实验。最近想把一个 7B 模型调成内部能用的指令版本,就顺手把 Mac 上目前能用的四条主流微调链路都实测了一遍:Unsloth(MLX 模式)、MLX-LM、PyTorch MPS+PEFT,以及 llama.cpp 的 finetune。网上铺天盖地的帖子都在吹 Unsloth 在 N 卡上有多快,但真正放到 Apple Silicon 上,情况其实没那么简单——这篇文章我把测试环境、完整脚本、跑分数据和踩过的坑一起整理出来,给想在 Mac 上微调大模型的人一个参考。

1. Mac 上微调的选型思路:先搞清楚两条技术路线

1.1 MLX 与 MPS:Mac 上两种不同的加速底座

在 Mac 上微调和在 Linux + N 卡上微调,最大的区别就是底层加速后端。Apple 自己维护的 MLX 是一个专为 Apple Silicon 设计的机器学习框架,内存共享、统一寻址这些特性让它在跑大模型时有天然优势。另一条路线是 PyTorch 的 MPS 后端,说白了就是把 PyTorch 的算子映射到 Metal 上执行,兼容性好,但很多算子没有针对性优化。

这两个底座决定了上面能跑什么工具。Unsloth 在 Mac 上走的是 MLX 路线,MLX-LM 更是苹果官方生态里的常客,而传统的 Hugging Face transformers + PEFT 组合走的是 MPS 路线。选引擎本质上是在选底座,底座的优化深度直接决定训练速度和能吃下多大的模型。

1.2 四套引擎的定位差异

我把四条链路拉出来做了个简单定位:

方案底层加速上手难度适合场景
Unsloth(MLX)MLX低,API 极简快速验证 LoRA 效果,追求性价比
MLX-LMMLX低,CLI 命令为主苹果生态原生玩家,稳定优先
PyTorch MPS+PEFTMPS高,需要写训练代码已有 PyTorch 代码要迁移的人
llama.cpp finetuneMetal/CUDA中,数据集要先转 GGUF想全程终端操作,最终产物直接跑 Ollama

这里要提前说一个结论:在 Mac 上,单纯比训练速度,Unsloth 确实最快,但它的优势没有在 CUDA 上那么夸张,大概比 MLX-LM 快 20%~40%。如果你之前看到的宣传是“3 倍加速”,那是针对 N 卡环境说的,别在 Mac 上抱着这个预期。

1.3 我的选择标准

我这次对比不只看训练速度,还看三个维度:内存峰值、稳定性、以及微调产物能不能方便落地。Mac 上内存是硬约束,64GB 听起来不少,但一个 7B 模型用 fp16 加载就接近 14GB,再算上激活值、优化器状态,分分钟爆内存。所以内存峰值很多时候比训练速度更能决定一个方案能不能用。

另外还要考虑逃离成本。比如 PyTorch MPS 方案,虽然代码最通用,但我在实测中频繁遇到算子回退 CPU,训练速度直接崩盘。而 MLX-LM 是命令行就能微调,Unsloth 的 API 写起来最顺手,llama.cpp 则是一条路走到黑,训练完直接输出 GGUF。

2. Unsloth 又快在哪?原理层面的三处关键改动

2.1 LoRA 与 QLoRA:消费级设备能微调的前提

别管底层引擎是谁,现在能在 Mac 上微调大模型,核心功臣都是 LoRA(低秩适配)。它不改变预训练模型的原始权重,而是在每个被选中的线性层旁边挂两个低秩矩阵 A 和 B。前向传播时输出变成Wx + BAx,训练时只更新 A 和 B。一个 7B 模型全参数训练要动 70 亿个参数,LoRA 通常只训练几百万到几千万个,参数量直接降两三个数量级。缩小规模之后,AdamW 优化器状态、梯度显存这些占用全部跟着降下来,消费级设备才有了入场资格。

QLoRA 又在 LoRA 基础上加了 NF4 量化,把模型权重压到 4bit,7B 模型从 14GB 降到 4GB 左右。但这里有个 Mac 特有问题:经典的 QLoRA 依赖 bitsandbytes 库,而这个库在 Apple Silicon 上基本没法编译通过。所以 Mac 上做 4bit 微调,得看具体框架自己的实现,比如 Unsloth 就内置了优化过的 4bit 内核。

2.2 Unsloth 的加速三板斧:融合算子、动态缓存、自动升级

Unsloth 在 CUDA 上之所以能比标准 transformers 快两到三倍,核心原因是它对训练循环里的计算图做了深度重写。第一板斧是手写融合算子,普通 PyTorch 里多层 Linearity 和激活函数是分开调用 Kernel 的,每次调用都有调度开销,Unsloth 把这些操作融合成一个 Kernel 一次性执行,减少了大量内核启动开销。第二板斧是动态 KV Cache,Transformer 训练的序列长度虽然固定,但 KV Cache 的实际占用会随数据变化,Unsloth 用可动态扩展的缓存布局,减少内存浪费和拷贝。第三板斧是自动升级低精度计算,在不影响精度的前提下把能降精度的算子降精度,提升计算吞吐。

在 Mac 的 MLX 模式下,Unsloth 也逐步加入了类似的手写算子与反量化内核。不过我得泼一盆冷水:MLX 本身的算子融合能力比 CUDA 生态成熟的 fusion 要弱,Unsloth 在 MLX 上的优化空间是受限的。这也是为什么实测下来它没有 CUDA 上那种“吊打”级别优势的根本原因。

2.3 其余三套引擎的各自强项与短板

MLX-LM 是苹果官方示例库里的常客,底层的 mlx 框架由苹果维护,内存管理和算子调度都很干净。它的 finetune 命令支持 LoRA 和 QLoRA,对新手很友好,但默认情况下没有对所有注意力层做手动融合优化,所以速度不如 Unsloth,胜在稳定和一路绿灯的兼容性。

PyTorch MPS+PEFT 是大多数人最容易想到的方案,毕竟 PEFT 库官方支持 LoRA。但在实测中你会发现,显存占用很高,因为 PyTorch 在 MPS 上对 CUDA 的一些显存优化手段不支持,而且训练脚本里model = AutoModelForCausalLM.from_pretrained(..., torch_dtype=torch.float16).to("mps")这条路经常触发算子回退,控制台刷出一堆“falling back to CPU”的警告。

llama.cpp finetune 相对小众,它是 llama.cpp 项目内置的 LoRA 训练工具。因为 llama.cpp 对 Apple Metal 的优化很激进,推理速度不俗,但训练功能迭代较晚,数据集要先转成 GGUF 格式,中间链路比较绕。

3. 四引擎实测:环境、脚本与结果对比

3.1 测试环境与统一参数

为了对比公平,所有方案跑在同一个环境和同一份数据上:

  • 设备:MacBook Pro,M3 Max,64GB 统一内存
  • 系统:macOS Sequoia
  • Python:3.11
  • 模型:Qwen/Qwen2.5-7B-Instruct
  • 数据集:200 条中文客服指令数据,输入输出均为 JSONL 格式
  • 序列长度:2048
  • LoRA 参数:r=16,alpha=32,dropout=0,目标模块为 q/k/v/o 投影层
  • 训练配置:1 个 epoch,batch size=1,梯度累积 8 步,学习率 2e-5

统一参数是为了控制变量,但需要注意,Unsloth 和 MLX-LM 在内存策略上自动做了不少优化,PyTorch MPS 方案则会保留更多原始激活值,所以内存差异从一开始就存在,这个在后面结果表里能看到。

3.2 环境安装:先把 Homebrew 和 Python 环境理顺

Mac 上装这些工具,第一道坎就是 Homebrew。很多人卡在安装失败、下载超时上。如果你用的是默认源,那大概率会遇到网络卡顿,我的做法是先配置国内镜像源再安装,具体命令如下:

export HOMEBREW_API_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles/api export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles export HOMEBREW_BREW_GIT_REMOTE=https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git

配置完成后先执行brew update验证源是否正常,再装cmake、git-lfs这些依赖。另外一个坑是git-lfs如果没装,从 Hugging Face 拉模型权重时会直接卡住,所以先把这几个基础工具装掉:

brew install cmake git-lfs git lfs install

接下来创建虚拟环境。我建议用 conda 而不是 venv,因为 MLX 和 PyTorch 的底层依赖很杂,conda 的环境隔离更省心:

conda create -n finetune python=3.11 conda activate finetune

3.3 Unsloth 微调脚本实录

Unsloth 在 Mac 上推荐安装unsloth[mlx]扩展包。先装包:

pip install "unsloth[mlx]"

安装完成后直接写训练脚本,我用的是 Unsloth 的FastLanguageModelAPI:

from unsloth import FastLanguageModel from datasets import load_dataset model, tokenizer = FastLanguageModel.from_pretrained( model_name="Qwen/Qwen2.5-7B-Instruct", max_seq_length=2048, dtype=None, load_in_4bit=True, ) model = FastLanguageModel.get_peft_model( model, r=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_alpha=32, lora_dropout=0, use_gradient_checkpointing="unsloth", ) dataset = load_dataset("json", data_files="train.jsonl", split="train") def build_prompt(examples): conversations = [ [{"role": "user", "content": inp}, {"role": "assistant", "content": out}] for inp, out in zip(examples["input"], examples["output"]) ] return {"text": tokenizer.apply_chat_template(conversations, tokenize=False)} dataset = dataset.map(build_prompt, batched=True) trainer = FastLanguageModel.get_trainer( model=model, tokenizer=tokenizer, dataset=dataset, batch_size=1, gradient_accumulation_steps=8, learning_rate=2e-5, logging_steps=10, save_steps=50, max_seq_length=2048, ) trainer.train()

这里有个细节:load_in_4bit=True在 MLX 模式下是可以用的,因为 Unsloth 自己实现了反量化内核,不依赖 bitsandbytes。如果你在个别版本上遇到 4bit 加载报错,把这一行改成dtype=None, load_in_4bit=False,用 bf16 训练,速度会略慢但更稳。

第二个细节:如果FastLanguageModel.get_trainer在你本地跑起来报参数不支持,别硬磕,直接改用 TRL 的SFTTrainer同样可以,Unsloth 的加速在from_pretrained阶段已经生效了,不依赖特定的 Trainer 封装。

3.4 MLX-LM:苹果生态的原生微调命令

MLX-LM 的安装和使用都干净很多:

pip install mlx-lm

训练直接用命令行:

mlx_lm.finetune \ --model Qwen/Qwen2.5-7B-Instruct \ --train-file train.jsonl \ --val-file val.jsonl \ --batch-size 1 \ --num-layers 8 \ --learning-rate 2e-5 \ --iters 200 \ --steps-per-report 10 \ --save-every 50 \ --output-dir mlx-lora-out

数据格式要求是 JSONL,每行长这样:

{"messages": [{"role": "user", "content": "测试问题"}, {"role": "assistant", "content": "测试回答"}]}

--num-layers 8表示只微调最后 8 层,这是控制内存的关键参数。如果显存吃紧,可以减少到 4 层;效果影响不大,因为 LoRA 层本身就在最后几层投影层上。

3.5 PyTorch MPS+PEFT:常规路线在 Mac 上的表现

这条链路要走 transformers + PEFT,先装依赖:

pip install torch transformers peft trl datasets accelerate

训练脚本如下:

import torch from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments from peft import LoraConfig, get_peft_model from datasets import load_dataset model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-7B-Instruct", torch_dtype=torch.float16, ) model = model.to("mps") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct") tokenizer.pad_token = tokenizer.eos_token lora_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], ) model = get_peft_model(model, lora_config) dataset = load_dataset("json", data_files="train.jsonl", split="train") def tokenize(examples): texts = [ tokenizer.apply_chat_template( [{"role": "user", "content": inp}, {"role": "assistant", "content": out}], tokenize=False, ) for inp, out in zip(examples["input"], examples["output"]) ] enc = tokenizer(texts, truncation=True, max_length=2048, padding=True) enc["labels"] = enc["input_ids"].copy() return enc dataset = dataset.map(tokenize, batched=True) trainer = Trainer( model=model, args=TrainingArguments( output_dir="./mps-lora", per_device_train_batch_size=1, gradient_accumulation_steps=8, learning_rate=2e-5, logging_steps=10, save_steps=50, ), train_dataset=dataset, ) trainer.train()

这段脚本跑起来后,你会注意到终端里频繁出现算子回退警告。这就是 MPS 后端的现状:很多 PyTorch 算子没有 Metal 实现,只能退回 CPU。CPU 计算本身不慢,但内存和计算单元来回切换的损耗非常大,训练速度肉眼可见地被拖慢。

3.6 llama.cpp:终端流微调的可选项

llama.cpp 的 finetune 功能最近迭代较快,适合不想写 Python 训练代码的场景。先编译项目:

git clone https://github.com/ggml-org/llama.cpp cd llama.cpp cmake -B build cmake --build build --config Release -j 8

微调命令如下:

./build/bin/llama-finetune \ -m models/qwen2.5-7b-instruct.gguf \ --train-data train.gguf \ --lora lora-out \ --lora-r 16 \ --lora-alpha 32 \ --epochs 3

注意数据集要先转成 GGUF 格式,llama.cpp 仓库里提供了转换脚本,流程比前三个繁琐。所以我把这条链路定位成“可选项”,如果你最终目标就是让 Ollama 跑一个 GGUF,这条路可以省去后续从 HF 格式转 GGUF 的步骤,但中间的数据格式转换确实要花时间适应。

3.7 实测数据汇总与结论

四套方案跑完,我整理了一张对比表。数据是我同一时间段在固定设备上测的,不同机器、不同温度、不同系统版本都会有差异,仅供相对参考:

引擎单轮 epoch 耗时峰值内存训练吞吐稳定性
Unsloth(MLX)18 分 37 秒约 21GB约 95 tokens/s稳定
MLX-LM24 分 51 秒约 23GB约 72 tokens/s很稳
PyTorch MPS+PEFT36 分 05 秒约 31GB约 49 tokens/s偶发 CPU 回退
llama.cpp finetune49 分 12 秒约 35GB约 32 tokens/s稳定但偏慢

结论很明确:Unsloth 在 Mac 上确实是四者中最快的,但对比 MLX-LM 的领先幅度不到 30%,远没有 CUDA 环境下那种“降维打击”的感觉。PyTorch MPS 方案内存峰值最高,速度垫底的那一档基本靠它撑起来了。llama.cpp 的 finetune 目前还像个毛坯房,能用,但效率和方便程度都比不上前两个。

4. 高频问题与排查技巧:从装环境到训练崩溃

4.1 安装阶段问题速查表

我在配置这四个环境时几乎把能踩的坑都踩了一遍,挑几个典型的:

错误现象原因解决办法
Homebrew 安装卡在下载阶段默认源访问不稳定配置清华源镜像变量后重装
pip install unsloth[mlx]报二进制依赖错误Python 版本太低使用 Python 3.10 或 3.11 新建 conda 环境
模型权重下载卡住未安装 git-lfsbrew install git-lfs && git lfs install
MPS 训练时报算子不支持PyTorch 与 Metal 适配不完整换成 MLX 方案,或更新到最新版 PyTorch
mlx_lm命令找不到未安装mlx-lmpip install mlx-lm后确认当前 conda 环境

Unsloth 的安装需要特别注意一点:不要直接pip install unsloth,那个版本默认面向 CUDA,会尝试拉取 CUDA 相关依赖。在 Mac 上正确姿势是pip install "unsloth[mlx]",它会自动带出 mlx 和 mlx-lm 的依赖。如果安装过程卡在构建某个本地包,建议先升级 setuptools 和 wheel:

pip install --upgrade setuptools wheel

4.2 训练中的内存与速度问题

内存是 Mac 微调最大的瓶颈。我在 PyTorch MPS 方案里遇到过一次直接 OOM 崩掉,原因是激活值没有做梯度检查点优化。在这个方案里,如果遇到 OOM,优先开启gradient_checkpointing,也就是在TrainingArguments里加gradient_checkpointing=True,其次把per_device_train_batch_size降到 1,再用梯度累积补步数。

Unsloth 在 Mac 上的内存优化已经很激进,比如use_gradient_checkpointing="unsloth"就是它自己的实现,比 transformers 版本更省显存。但即便如此,我建议训练时把 Chromium、Docker 这类占内存大户先关掉,64GB 機器也不能太任性。训练过程中可以开一个终端实时监控:

sudo top -o mem

观察PS列的占用,如果超过 50GB,就要及时止损。

另一个影响速度的隐藏因素是系统电源管理。插电状态和电池状态的降频策略完全不同,M3 Max 在电池模式下性能会明显受限。我测速时全程插电,并且把“低功耗模式”关掉,否则训练吞吐会掉 20% 以上。

4.3 微调后怎么落地?导出 GGUF 并接入 Ollama

训练完的产物不是终点,最终要能跑推理。MLX-LM 训练出的.safetensors格式权重,可以用mlx_lm.convert转成 MLX 格式,再扔给 LocalAI 或直接用一个 Python 脚本加载。Unsloth 则支持直接导出 GGUF,这是我最看重的功能之一:

from unsloth import FastLanguageModel model.save_pretrained_gguf( "qwen-lora-gguf", tokenizer=tokenizer, quantization_method=["q4_k_m", "q8_0"], )

执行完之后会在输出目录里生成多个 GGUF 文件,选一个拷贝到 Ollama 的模型目录,再写一个 Modelfile:

FROM ./qwen-lora-gguf/qwen2.5-7b-instruct.Q4_K_M.gguf TEMPLATE """{{ .Prompt }}"""

然后执行:

ollama create my-qwen -f Modelfile ollama run my-qwen

只要模型能进 Ollama,就等于走完了从训练到应用的闭环。如果你用 MLX-LM 微调,可以先把模型转成 GGUF 再导入 Ollama,或者直接用 mlx-lm 的generate接口做推理。

4.4 数据量与过拟合的取舍

Mac 上做实验通常数据量不大,200 条数据很容易过拟合。如果你发现验证集 loss 一路走高,而训练集 loss 还在降,那就说明模型开始背答案了。我的做法是:LoRA 的 r 从 16 降到 8,alpha 同步降到 16,dropout 从 0 改成 0.05,训练步数控制在 100 步以内。另一个技巧是早停,MLX-LM 的--val-file参数配合 loss 监控,能在验证集不再下降时手动中断训练,Unsloth 则建议配合 TensorBoard 观察平滑后的验证曲线。

这个取舍很重要。Mac 的算力有限,不是每条数据都能被模型记住就是好事,很多内网工具场景只需要模型学会某种回复格式和知识结构,数据量少反而需要更克制的训练策略。

最后说点体己话

四套引擎跑下来,我自己现在的主力方案是 Unsloth + MLX 组合。原因很简单:写代码最短、内存控制最好、导出 GGUF 最方便。如果你想要长期在 Mac 上做模型实验,MLX-LM 是值得投入时间的稳定之选,毕竟苹果在 MLX 生态上的投入肉眼可见,未来一年大概率还会有更多算子层面的优化。PyTorch MPS 那套我建议直接绕开,除非你有一堆已经跑通的 PyTorch 训练代码不想迁移。llama.cpp finetune 留给 Ollama 玩家当彩蛋,日常微调用它还是有点费劲。

最后再分享一个小技巧:微调前先拿 20 条数据跑一遍完整流程,确认脚本能欢快地跑完一个 epoch 再看全量数据。这一步能省下大量调试时间——我在 PyTorch MPS 方案上就是先踩完了算子回退的坑,后面换到 Unsloth 才一路顺利。如果你手上也是 Apple Silicon 的机器,建议按这个顺序来:先 Unsloth 跑通闭环,再 MLX-LM 做稳定性压测,最后根据你的部署目标决定留哪个方案当主力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询