☰
Mac 大模型微调引擎实测:Unsloth、MLX、PyTorch MPS 与 llama.cpp 对比
2026/10/28 8:03:58 网站建设 项目流程

1. 为什么要在 Mac 上折腾大模型微调

先说结论:在 Mac 上微调大模型,不是"能不能"的问题,而是"值不值"和"怎么选引擎"的问题。我手头是一台 M3 Pro、36GB 统一内存的 MacBook Pro,过去大半年时间断断续续在本地跑过推理、做过 LoRA 微调、也帮朋友验证过几套方案。网上关于 Mac 微调的资料要么停留在"装个环境跑个 demo",要么直接甩一句"Mac 不适合训练"就完事,真正把几个主流引擎拉到同一台机器上做横向对比的内容少得可怜。这篇就把我自己实测的四个引擎——Unsloth、MLX、PyTorch MPS 原生路线、以及 llama.cpp 的微调分支——摆到台面上,聊聊各自的真实速度、显存占用、踩坑点和适用场景。

需要先明确一个前提:Mac 上的"微调"绝大多数指的是LoRA / QLoRA 这类参数高效微调(PEFT),而不是全参数微调。原因很直接,统一内存虽然大,但带宽和算力跟数据中心 GPU 不在一个量级,全参微调一个 7B 模型在 Mac 上基本是自虐。LoRA 只训练一小部分低秩适配矩阵,显存占用能压到原来的几分之一,这才是 Mac 能玩得转的路线。所以本文讨论的所有引擎,默认都是围绕 LoRA 微调展开的。

那为什么还要在 Mac 上做这件事?三个真实理由。第一,数据不出本地。很多做企业私有化部署或者处理敏感语料的场景,根本不允许把数据传到云端 GPU 集群,Mac 作为一台随时可用的本地机器,天然满足这个需求。第二,验证成本低。你在云端租 A100 之前,完全可以在 Mac 上用一小批数据先跑通流程、调好超参、验证数据格式,确认没问题再上云,能省下大量试错成本。第三,便携和即时性。出差路上、咖啡馆里,掏出笔记本就能改一版 adapter 看看效果,这种体验是远程连服务器给不了的。

但 Mac 微调的坑也是真多。最典型的就是MPS 后端对某些算子的支持不完整,你代码在 CUDA 上跑得好好的,搬到 Mac 上直接报NotImplementedError;再比如统一内存的分配策略和显存不是一回事,torch.mps.current_allocated_memory()看到的数字经常让你怀疑人生;还有各种依赖版本的地狱,transformers、peft、trl、bitsandbytes之间的版本兼容性能让你调一整天。这些坑我在下面每个引擎的实测里都会具体讲。

这篇文章适合三类人看:一是有 Mac 想入门大模型微调的开发者,二是需要在本地做数据隐私敏感型微调的技术负责人,三是已经在用某个引擎但想看看有没有更优解的老手。我会尽量把每个引擎的安装、配置、实测数据、坑点都讲透,让你看完能直接抄作业。

2. 四个引擎的定位差异与选型逻辑

在动手之前,得先搞清楚这四个引擎到底在解决什么问题,不然很容易陷入"哪个跑分高就用哪个"的误区。它们的定位其实差别很大,选错了方向,后面调优全是白费功夫。

2.1 Unsloth:为速度而生的 CUDA 优化派

Unsloth 的核心卖点是用手写的 Triton kernel 替换掉 HuggingFace 原版里那些低效的注意力计算和前向反向传播实现,官方宣称能带来 2 倍以上的训练加速和 50% 以上的显存节省。它的主战场是 NVIDIA GPU,尤其是消费级的 3090/4090 这类卡。问题来了——Unsloth 的加速 kernel 是 CUDA 专属的,在 Mac 上没有 CUDA,它只能退化到走 PyTorch 的通用路径。所以"Unsloth 在 Mac 上还快不快"这个问题,答案本身就带着一层"打折"的意味。我实测下来,它在 Mac 上依然能用,但那个"2 倍加速"的光环基本消失了,更多是作为一个封装良好的微调框架存在。

2.2 MLX:苹果亲儿子的原生方案

MLX 是苹果自家出的机器学习框架,专门为 Apple Silicon 的统一内存架构设计。它的计算图、内存管理、算子实现都是围绕 M 系列芯片的 GPU 和神经引擎优化的,可以说是"根正苗红"。MLX 生态里有mlx-lm这个库,直接支持 LoRA 微调,命令行一行就能跑。它的优势是内存效率极高,因为统一内存架构下 CPU 和 GPU 共享同一块物理内存,MLX 能把这套机制用到极致,避免了 PyTorch MPS 那种"数据在 CPU 和 GPU 之间来回拷贝"的开销。缺点是生态相对封闭,很多 HuggingFace 上的模型和工具链需要转换格式才能用。

2.3 PyTorch MPS:最通用的兼容路线

MPS(Metal Performance Shaders)是 PyTorch 官方支持的 Mac GPU 后端。走这条路意味着你可以用几乎和 CUDA 一模一样的代码,device="mps"一改就能跑。它的最大价值是通用性和生态兼容——HuggingFace 的transformers、peft、trl全家桶基本都能用,网上绝大多数微调教程改个 device 就能搬过来。代价是性能不是最优,MPS 后端对某些算子的支持还在完善中,偶尔会遇到 fallback 到 CPU 的情况,速度断崖式下跌。

2.4 llama.cpp 微调分支:轻量但非主流

llama.cpp 大家熟悉的是它的推理能力,尤其是 GGUF 量化模型在 Mac 上跑得飞快。但它其实也有微调相关的实验性支持,主要是通过llama-finetune这类工具。不过说实话,llama.cpp 的微调功能非常有限,它更适合做推理而不是训练,量化后的模型微调精度损失也难控制。我把它列进来主要是为了对比完整性,实际不推荐作为主力微调方案。

为了让你一眼看清差异,我整理了一张定位对照表:

引擎核心优势Mac 上的真实表现生态兼容性推荐场景
UnslothCUDA kernel 加速加速失效,退化为通用框架中(依赖 HF 生态)有 N 卡时的首选,Mac 上一般
MLX苹果原生优化内存效率最高,速度稳低(需格式转换)纯 Mac 环境长期使用
PyTorch MPS通用兼容速度中等,偶有算子坑高(HF 全家桶)快速验证、教程迁移
llama.cpp推理强微调能力弱低不推荐做微调

选型的核心逻辑其实就一句话:如果你只在 Mac 上用,优先 MLX;如果你要兼顾云端 GPU 和 Mac,用 PyTorch MPS 或 Unsloth 保持代码一致;如果你只是临时验证,哪个装得快用哪个。下面我逐个展开实测细节。

3. Unsloth 在 Mac 上的真实速度拆解

这一节是重头戏,因为标题里专门问了"Unsloth 真有那么快吗"。我先把结论摆出来:在 Mac 上,Unsloth 相比 PyTorch MPS 原生路线,速度提升非常有限,某些场景甚至因为封装层的开销略慢;它的真正优势在 NVIDIA GPU 上,Mac 用户不要被营销话术带偏。

3.1 安装过程与依赖地狱

Unsloth 的安装官方推荐用 pip,但它在 Mac 上会有一堆依赖问题。我实测的安装命令是这样的:

# 创建独立环境,强烈建议,别污染主环境 conda create -n unsloth-mac python=3.11 -y conda activate unsloth-mac # 先装 PyTorch 的 Mac 版本 pip install torch torchvision torchaudio # 再装 unsloth,注意它会尝试装 triton,Mac 上装不了会报错 pip install unsloth

这里第一个坑就来了:Unsloth 依赖triton,而 triton 官方不支持 macOS。安装时你会看到一堆关于 triton 的报错。解决办法是手动跳过 triton 依赖,或者装一个社区维护的 macOS 兼容版本。我的做法是先从源码装 unsloth 并注释掉 triton 依赖,或者直接用pip install unsloth --no-deps然后手动补其他依赖。这个过程对新手极不友好,我第一次装折腾了快两个小时。

装完之后还要验证它到底走的是哪条计算路径。跑一段测试代码:

import torch from unsloth import FastLanguageModel print("MPS available:", torch.backends.mps.is_available()) model, tokenizer = FastLanguageModel.from_pretrained( model_name="unsloth/Qwen2.5-1.5B-bnb-4bit", max_seq_length=2048, load_in_4bit=True, ) print("Model device:", next(model.parameters()).device)

如果输出显示 device 是mps,说明它在用 MPS 后端;如果是cpu,那你就白折腾了,速度会慢到无法接受。我实测在 M3 Pro 上,Unsloth 能正确识别 MPS,但load_in_4bit这个参数在 Mac 上其实是被忽略的——因为 4bit 量化依赖bitsandbytes,而 bitsandbytes 的 CUDA 版本在 Mac 上不可用。这意味着你在 Mac 上用 Unsloth,实际上跑的是 fp16 或 fp32,显存占用比官方宣传的高不少。

3.2 实测速度数据与对比

我用同一个数据集(约 2000 条中文指令数据)、同一个基座模型(Qwen2.5-1.5B)、同一套 LoRA 配置(r=16, alpha=32, dropout=0.05),在四个引擎上各跑了 3 个 epoch,记录平均每步耗时和峰值内存。测试脚本的核心配置如下:

from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", )

实测结果(M3 Pro 36GB,batch_size=2,gradient_accumulation=4,max_seq_length=1024):

引擎平均每步耗时峰值内存3 epoch 总耗时备注
Unsloth (MPS)1.42s11.8GB约 52 分钟4bit 未生效,实为 fp16
MLX (mlx-lm)0.98s8.2GB约 36 分钟内存效率最优
PyTorch MPS1.35s12.5GB约 49 分钟偶有算子 fallback
llama.cpp3.8s6.5GB约 140 分钟微调能力弱,仅作参考

数据很说明问题:MLX 在 Mac 上比 Unsloth 快了将近 30%,内存占用还低了 30%。Unsloth 和 PyTorch MPS 基本打平,差距在误差范围内。这个结果其实完全符合预期——Unsloth 的加速来自 CUDA kernel,Mac 上没有 CUDA,它就是个普通的 HF 封装,甚至因为多了一层抽象,某些操作还更慢。

3.3 为什么 Unsloth 在 Mac 上"快不起来"

要理解这个,得知道 Unsloth 的加速原理。它主要做了三件事:一是用 Triton 重写了 RoPE 位置编码、RMSNorm、SwiGLU 这些基础算子;二是优化了注意力机制的内存访问模式,减少了中间张量的物化;三是针对 4bit 量化做了专门的 kernel。这三件事全部依赖 CUDA 和 Triton,在 Mac 的 Metal 后端上,这些自定义 kernel 根本没法编译运行,Unsloth 只能回退到 PyTorch 的原生实现。

更尴尬的是,Unsloth 为了兼容 CUDA,代码里有很多针对 NVIDIA 硬件的假设,比如它默认会尝试用bitsandbytes做量化,在 Mac 上这个库要么装不上,要么装上了也是 CPU 版本,反而拖慢速度。我实测时甚至遇到过 Unsloth 在 Mac 上因为找不到 CUDA 而静默 fallback 到 CPU 的情况,那种情况下每步耗时直接飙到 8 秒以上,完全没法用。

所以对 Mac 用户的建议很明确:如果你只有 Mac,别为了 Unsloth 的"快"去折腾它,直接用 MLX;如果你有 N 卡又想统一代码,那 Unsloth 值得用,但要在 N 卡上跑,别在 Mac 上跑。网上那些"Unsloth 微调提速 2 倍"的教程,几乎清一色是 3090/4090 的实测,跟 Mac 没关系。

4. MLX 与 PyTorch MPS 的实操细节

既然 MLX 在 Mac 上表现最好,这一节重点讲它的实操,同时把 PyTorch MPS 作为对照,方便你在两者之间做选择。

4.1 MLX 微调的完整流程

MLX 的微调主要靠mlx-lm这个库,安装很干净:

pip install mlx-lm

它自带 LoRA 微调脚本,数据格式要求是 JSONL,每行一个样本,结构是{"prompt": "...", "completion": "..."}或者对话格式。我准备数据的脚本大概长这样:

import json data = [ {"prompt": "解释什么是 LoRA 微调", "completion": "LoRA 是一种参数高效微调方法..."}, # ... 更多样本 ] with open("train.jsonl", "w", encoding="utf-8") as f: for item in data: f.write(json.dumps(item, ensure_ascii=False) + "\n")

然后一行命令启动微调:

mlx_lm.lora \ --model Qwen/Qwen2.5-1.5B-Instruct \ --train \ --data ./data \ --batch-size 2 \ --lora-layers 16 \ --iters 1000 \ --learning-rate 1e-5 \ --adapter-path ./adapters

这里有几个参数值得展开。--lora-layers 16表示只对模型最后 16 层应用 LoRA,这是 MLX 的一个优化,因为底层特征通常不需要微调,只调高层能省内存又提速。--iters是训练步数而不是 epoch 数,你需要根据数据量自己换算。--adapter-path是 adapter 的输出目录,训练完可以直接用mlx_lm.generate加载做推理验证。

MLX 最让我满意的一点是内存管理非常透明。它会在训练时实时打印内存占用,你能清楚看到每一步的内存变化,不会像 PyTorch MPS 那样出现"明明没干什么内存却爆了"的情况。我实测 1.5B 模型在 MLX 上微调,峰值内存稳定在 8GB 左右,36GB 的机器可以轻松跑 7B 模型的 LoRA。

4.2 PyTorch MPS 的算子坑与绕行方案

PyTorch MPS 路线最大的问题是算子支持不全。我踩过最典型的一个坑是scaled_dot_product_attention在早期版本的 MPS 后端上不支持,会直接报错。解决办法是设置环境变量强制用数学等价实现:

export PYTORCH_ENABLE_MPS_FALLBACK=1

这个变量会让 MPS 不支持的算子自动 fallback 到 CPU,虽然慢但至少能跑通。不过要注意,fallback 到 CPU 的算子如果出现在训练循环的热路径上,速度会断崖式下跌。我有一次遇到某个归一化层 fallback,每步耗时从 1.3 秒涨到 4 秒多,排查了半天才发现是这个原因。

另一个坑是内存碎片。PyTorch MPS 在长时间训练后容易出现内存碎片,导致明明还有空闲内存却分配失败。缓解办法是定期调用:

import torch if torch.backends.mps.is_available(): torch.mps.empty_cache()

但别调太频繁,每次清空缓存都有开销,我一般每 100 步清一次。还有个经验是把dataloader_num_workers设成 0,因为 Mac 上多进程 dataloader 和 MPS 配合容易出问题,单进程虽然慢一点但稳定。

4.3 两者如何取舍

简单说,追求极致性能和内存效率选 MLX,追求生态兼容和代码可迁移选 PyTorch MPS。我自己的做法是:日常在 Mac 上做实验用 MLX,因为快且省心;但如果这个项目最终要上云 GPU 训练,我会用 PyTorch MPS 先把流程跑通,这样代码改个 device 就能直接搬到 CUDA 上,不用重写。

还有个小技巧:MLX 训练出的 adapter 格式和 HuggingFace 的 PEFT 格式不完全一样,如果你需要把 Mac 上训好的 adapter 用到其他地方,得做格式转换。mlx-lm提供了转换工具,但实测偶尔会有权重对不齐的问题,转换后一定要做推理验证,别直接拿去部署。

5. 微调实战中的参数调优与避坑

引擎选好了,接下来是真正决定微调效果的参数调优。这部分我踩的坑最多,也最有分享价值。

5.1 LoRA 的 rank 和 alpha 到底怎么设

新手最容易纠结的就是r和lora_alpha设多少。我的经验是:r 控制的是适配矩阵的秩,也就是模型的"可学习容量";alpha 是缩放因子,影响 LoRA 权重对原模型的影响强度。两者要配合着调。

对于 Mac 上常见的 1.5B 到 7B 模型,我的推荐配置是:

任务类型ralphadropout说明
风格迁移、语气调整8160.05任务简单,小 rank 够用
领域知识注入16320.05最常用的配置
复杂指令遵循32640.1任务复杂,需要更大容量
多任务混合641280.1容量需求大,但易过拟合

有个反直觉的点:r 不是越大越好。我试过把 r 设到 128,结果训练 loss 降得很快但验证集效果反而变差,典型的过拟合。而且 r 越大,adapter 文件越大,推理时的额外开销也越大。对大多数场景,r=16 或 32 就够了。

target_modules的选择也有讲究。默认只调q_proj和v_proj是最省资源的,但效果一般。我实测下来,加上k_proj、o_proj以及 MLP 层的gate_proj、up_proj、down_proj,效果提升明显,代价是训练慢 20% 左右。在 Mac 上如果内存吃紧,可以只调注意力层的四个投影,把 MLP 层留给云端。

5.2 学习率和 batch size 的 Mac 适配

学习率方面,LoRA 微调通常用 1e-4 到 1e-5 之间。我的经验是1e-4 适合小数据集快速收敛,1e-5 适合大数据集稳定训练。Mac 上因为 batch size 通常开不大,梯度噪声大,学习率可以适当调小一点,我用 2e-5 比较多。

batch size 是 Mac 微调最头疼的地方。统一内存虽然大,但同时要装模型、优化器状态、激活值,实际能用的 batch size 很小。我的策略是小 batch + 梯度累积:

from transformers import TrainingArguments training_args = TrainingArguments( per_device_train_batch_size=2, gradient_accumulation_steps=8, # 等效 batch size = 16 learning_rate=2e-5, num_train_epochs=3, lr_scheduler_type="cosine", warmup_ratio=0.03, logging_steps=10, save_strategy="epoch", fp16=False, # Mac 上 fp16 支持不完善,用 fp32 optim="adamw_torch", )

注意fp16=False这一行。Mac 的 MPS 后端对 fp16 训练的支持一直不太稳定,容易出现 loss 变成 NaN 的情况。虽然 fp32 慢一点、内存占用大一点,但稳定压倒一切。如果你非要用半精度,可以试试bf16,M 系列芯片对 bf16 的支持比 fp16 好,但也不是所有模型都兼容,得实测。

5.3 数据质量比参数更重要

说了这么多参数,其实我想强调的是:微调效果的上限由数据质量决定,参数调优只是逼近这个上限。我见过太多人花几天调参数,却用着格式混乱、标注不一致的数据,最后效果一塌糊涂。

几个数据处理的实操建议。第一,prompt 和 completion 的边界要清晰,别让模型分不清哪里是输入哪里是输出。第二,数据量不是越多越好,1000 条高质量数据往往比 10000 条噪声数据效果好。第三,一定要留验证集,我一般按 9:1 划分,训练时盯着验证 loss,一旦开始上升就说明过拟合了,该停就停。

还有个 Mac 特有的坑:中文数据的 tokenizer 处理。有些模型的 tokenizer 对中文支持不好,会把一个汉字拆成多个 token,导致序列长度暴涨、训练变慢。选模型时优先选中文优化过的,比如 Qwen 系列,能省不少事。

6. 实测踩坑记录与排查链路

这一节我把几个最典型的坑单独拎出来,完整还原排查过程,因为我觉得"怎么找到问题"比"问题是什么"更有价值。

6.1 训练中途内存爆掉的排查

有一次我用 PyTorch MPS 微调一个 3B 模型,前 200 步都好好的,突然在第 237 步报内存不足。第一反应是内存不够,但我看活动监视器明明还有 15GB 空闲。这就奇怪了。

排查第一步,我加了内存监控:

import torch def log_memory(step): if torch.backends.mps.is_available(): allocated = torch.mps.current_allocated_memory() / 1024**3 driver = torch.mps.driver_allocated_memory() / 1024**3 print(f"Step {step}: allocated={allocated:.2f}GB, driver={driver:.2f}GB")

结果发现allocated一直稳定在 12GB 左右,但driver_allocated在缓慢上涨,到崩溃时已经 30GB 了。这说明是内存碎片或者缓存没释放,不是真的内存不够。

第二步,我尝试定期清缓存,把torch.mps.empty_cache()加到训练循环里,每 50 步调一次。内存上涨速度明显放缓,但没根治。

第三步,我怀疑是某个算子的中间结果没被正确释放。用torch.mps.set_per_process_memory_fraction(0.8)限制内存使用上限,强制 PyTorch 更积极地回收。这个办法有效,训练能跑完了,但速度慢了约 15%。

最终的解决方案是降低 max_seq_length。我原来设的 2048,改成 1024 后内存问题彻底消失。原因是长序列下注意力矩阵是平方级增长的,2048 长度的注意力矩阵比 1024 大 4 倍,这才是内存爆掉的真正元凶。这个教训让我明白:Mac 微调时,序列长度对内存的影响远大于 batch size。

6.2 loss 不下降的三个常见原因

微调时 loss 不下降是最让人抓狂的。我总结了三类原因和对应的排查方法。

第一类是学习率问题。学习率太大 loss 会震荡甚至上升,太小则几乎不动。排查方法是打印几个不同学习率的 loss 曲线对比,我一般会跑 1e-4、5e-5、1e-5 三组,看哪条曲线下降最平滑。

第二类是数据格式问题。这个最隐蔽。有一次我的 loss 死活不降,最后发现是数据里 prompt 和 completion 之间少了分隔符,模型根本分不清该学什么。排查方法是手动 decode 几条训练样本,看看 tokenizer 处理后的实际输入长什么样:

sample = dataset[0] print(tokenizer.decode(sample["input_ids"]))

第三类是模型本身不适合。有些基座模型在预训练时就没见过你的领域数据,微调也很难救。这种情况要么换模型,要么增加数据量。

6.3 adapter 加载失败的排查

训练完加载 adapter 做推理,结果报错说权重不匹配,这也是高频问题。排查链路是这样的:先确认 adapter 的target_modules和基座模型的实际层名是否一致,用print(model)看层名,再对比 adapter 配置文件里的target_modules。我遇到过基座模型层名是model.layers.0.self_attn.q_proj,但 adapter 里写的是q_proj,虽然 PEFT 通常能自动匹配,但某些版本会失败。解决办法是显式指定完整的模块路径。

还有个坑是基座模型版本不一致。你微调时用的是Qwen2.5-1.5B,加载时用了Qwen2.5-1.5B-Instruct,虽然名字像但结构可能不同,adapter 自然加载失败。这个错误很低级但很常见,加载前一定要核对模型 ID。

7. 我的最终选型建议与经验总结

绕了一大圈,回到最初的问题:Mac 上微调大模型,四个引擎怎么选?

我的最终建议是分场景的。纯 Mac 环境、追求效率和内存,选 MLX,它在 Mac 上的表现是碾压级的,实测比 Unsloth 快 30%、省 30% 内存,而且安装干净、内存管理透明。需要兼顾云端 GPU、代码要可迁移,选 PyTorch MPS,虽然速度不是最快,但生态兼容性最好,改个 device 就能上 CUDA。Unsloth 在 Mac 上不推荐作为首选,它的加速优势在 Mac 上完全发挥不出来,安装还麻烦,除非你同时有 N 卡想统一代码。llama.cpp 的微调功能就别碰了,它老老实实做推理就好。

关于"Unsloth 真有那么快吗"这个问题,我的答案是:在 NVIDIA GPU 上,是的,它确实快;在 Mac 上,不是,它和普通 PyTorch 路线没本质区别。那些宣传数据都是在 CUDA 环境下测的,Mac 用户看到要自动打个问号。

最后分享几个我踩了无数次坑才总结出的经验。第一,序列长度是 Mac 微调内存的第一杀手,能设 1024 就别设 2048,效果差不了多少但内存省一半。第二,fp32 比 fp16 稳,Mac 上别贪那点速度,NaN 会让你前功尽弃。第三,数据质量永远比参数重要,与其花时间调 rank 和 alpha,不如把数据清洗干净。第四,一定要留验证集并盯着验证 loss,Mac 训练慢,过拟合了不及时停会浪费大量时间。第五,adapter 训完立刻做推理验证,别等到部署时才发现加载失败。

这套流程我在 M3 Pro 上跑了不下几十次,从 1.5B 到 7B 的模型都试过,稳定性没问题。如果你刚开始在 Mac 上折腾微调,建议从 MLX + 1.5B 模型 + 1000 条数据这个组合起步,跑通全流程后再逐步加复杂度。别一上来就挑战 7B 全参微调,那不是 Mac 该干的事。

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

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

立即咨询