如果你手里拿的是一台搭载 M 系列芯片的 Mac,又恰好想在大模型预训练模型上做指令跟随微调,那你大概率已经被 Unsloth 相关的内容刷过屏:官网和大 V 们的视频都在讲,LoRA 微调速度提升两倍起、显存占用直接省下七八成。坦率讲,我第一次看到这个数据时是不太信的,因为绝大多数宣传基准跑在 CUDA 生态的 RTX 或 A100 上,而 Mac 的 MPS 后端这些年在算子支持上一直在追赶,很少成为新框架的主战场。所以我花了大概一周时间,在同一台 M2 Max MacBook Pro 上,把 Unsloth、MLX-LM、Transformers 搭配 PEFT、Axolotl 四个引擎全部跑了一遍,目标只有一个:在同一个模型、同一份数据、同一套超参面前,谁快、谁省内存、谁的模型效果崩了,以及 Unsloth 的宣传在 Mac 上到底能兑现几成。这篇就是完整的过程笔记,既包含可复现的安装配置,也记录了大量踩坑,准备在 Mac 上搞微调的朋友可以直接拿去参考。
1. 为什么我决定把四个引擎放在同一台 Mac 上比一场
1.1 Mac 上微调大模型的现实约束
Mac 和传统训练服务器的硬件形态差异很大。它没有独立的显存,GPU 和 CPU 共用的是一整块统一内存,同时内存带宽相对 NVIDIA 旗舰显卡有明显差距。换句话说,一台 64GB 的 M2 Max 看起来内存不低,但真正供 GPU 频繁读写的那部分带宽只有约 400GB/s,而 RTX 4090 的显存带宽在 1TB/s 量级。再加上笔记本的散热天生受限,长时间高负载运行下频率会持续下降。这是影响 Mac 微调体验的底层原因,也决定了很多为 CUDA 优化到极致的框架,在你的笔记本上未必能直接照搬。
另一个现实是生态。当前大模型微调的主流工具链,从 bitsandbytes 的 4bit 量化到 flash-attention 的算子优化,大部分默认只支持 CUDA。Apple 自己推出了 MLX 框架试图补齐这个缺口,PyTorch 也在不断完善 MPS 后端,但很多第三方库并没有同等投入。所以我能跑通的引擎名单,一开始就充满了不确定性。
1.2 四个引擎的名单与选择理由
这次对比我刻意选了四种完全不同定位的实现:
| 引擎 | 定位 | 为什么选它 |
|---|---|---|
| Unsloth | 专为 LoRA 微调速度优化的库 | 宣传最猛,社区热度最高 |
| MLX-LM | Apple MLX 生态的大模型工具 | Apple Silicon 亲儿子,原生路径 |
| Transformers + PEFT | Hugging Face 官方标准链路 | 微调基线,很多 AI 工程师绕不开 |
| Axolotl | YAML 驱动的全功能微调框架 | 企业团队常用,配置丰富 |
这个名单基本覆盖了"速度派""原生派""稳健派""功能派"四种选择逻辑。我没有把 llama.cpp 的微调纳入,因为它的定位更偏推理和轻量训练,跟这四个的完整训练管线不在同一层次上。
1.3 统一变量,否则对比没有意义
微调实验里最怕变量失控。模型、数据集、LoRA 参数、学习率调度、训练步数、随机种子,任何一项不一致,最后的速度和效果对比都是自说自话。所以我先定义了一个统一的训练基线:底模用 deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B,数据用从开源中文指令集中抽出的 2000 条客服场景对话,LoRA 的 rank、alpha、target_modules 完全一致,学习率、warmup、scheduler 也锁死。后面所有引擎的差异,只允许来自引擎自身的实现方式。
这个 1.5B 参数量的选择也有讲究。在 Mac 上跑 7B 或更大的模型,内存压力会导致频繁 swap,速度对比会混入太多硬件因素;而 1.5B 级别的模型可以稳定塞进内存,能更纯粹地反映各引擎的计算效率。
2. 实测环境和四个引擎的搭建过程
2.1 硬件与软件基线
测试机是 MacBook Pro 16 英寸,SoC 是 M2 Max,CPU 12 核、GPU 30 核,统一内存 64GB。系统版本 macOS 15.4,Python 环境用 3.10。在这里我得先说一个重要建议:不要用系统自带的 Python,也不建议用 Homebrew 拉最新版 Python,直接装 Miniconda,然后为这个项目单独建一个干净的 conda 环境。我实际操作时 Homebrew 安装就出了问题,装到一半报签名错误,折腾半天最后放弃,换成 conda 一分钟搞定。
conda create -n ft python=3.10 -y conda activate ft2.2 Unsloth 安装与踩坑
Unsloth 的安装网上版本很多,官方 README 给的是无脑 pip install unsloth,但在 macOS 上这行命令未必能一次走通。问题通常出在 triton 上,Unsloth 的某些版本会把它作为依赖拉进来,而 triton 在 Apple Silicon 上编译容易出现奇怪报错。我建议拆开装:
pip install torch transformers accelerate peft datasets pip install unsloth如果第二步因为 triton 报错,就用pip install unsloth --no-deps,装完测试运行时缺什么再补什么。实测下来 unsloth 的运行时依赖中,真正在 Mac 上必须的是 peft、transformers、safetensors 这几个,triton 在 MPS 环境下基本不会启用。
2.3 MLX-LM 与 PEFT 的安装
MLX 是 Apple 官方开源的机器学习框架,MLX-LM 则是它对大模型生态的封装,安装非常顺利:
pip install mlx mlx-lmPEFT 这边也一样常规,transformers、peft、accelerate、datasets 都是成熟轮子,一个晚上就能装完。这套组合存在一个问题,就是它默认的 PyTorch 训练路径在 Mac 上会启用 MPS 后端,而 MPS 对某些算子的实现相对保守,速度未必理想,所以需要单独看一下训练日志里的设备利用率。
2.4 Axolotl:在 macOS 上挑战最大的一位
Axolotl 是这次四个引擎里安装最折腾的。它的完整依赖中包括 flash-attn、bitsandbytes、deepspeed,在 Mac 上,flash-attn 基本不可能编译通过,因为它的预编译轮子只面向 CUDA。我的处理方式是创建一个新环境,先手动把核心依赖装好,再装 axolotl 本体,并且在它的 YAML 配置里把 flash_attention、xformers 这类字段全部显式关掉。即便这样,第一次运行还是报了一个和 model sharding 相关的错,需要额外设置环境变量禁用多卡逻辑。整套下来花了将近半天,单从"能跑起来"的角度,Axolotl 就已经输在起跑线上了。
2.5 模型与数据准备
底模我用了 deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B。这个模型是 DeepSeek 蒸馏出来的一个小尺寸指令模型,架构上基于 Qwen,参数规模 1.5B 左右,在 Mac 上非常友好。下载时因为 Hugging Face 官方域名连接不够稳定,我设置了国内镜像环境变量,再配合本地缓存,几分钟就把权重拉下来了。
数据方面,我从一个开源中文指令数据集的客服子集里抽了 2000 条,统一转成 Alpaca 的三段式格式:instruction、input、output。我特意挑客服场景是因为回复有明显的结构化要求,比如先安抚再给方案,后期对比生成质量时更容易看出模型差异。所有样本在进入各引擎前都做了同样的预处理,最大长度限制 512 token。
3. 同一套配置四套训练脚本:速度、内存和温度的硬数据
3.1 核心实测数据一览
所有引擎我都按固定超参跑:LoRA rank 8、alpha 16、dropout 0.05,学习率 2e-4,warmup 50 步,cosine 调度,每设备 batch size 4、梯度累积 4 步,等效全局 batch size 16。总共 2000 条数据跑 3 个 epoch,也就是 375 步训练。随机种子统一 42,评估集固定 200 条。这是最终的实测结果:
| 引擎 | 每步耗时(秒) | 峰值内存(GB) | 总训练耗时(分钟) | 验证 loss |
|---|---|---|---|---|
| Unsloth | 2.4 | 12.1 | 15.0 | 0.72 |
| MLX-LM | 2.8 | 13.4 | 17.5 | 0.75 |
| Transformers+PEFT | 3.9 | 18.7 | 24.4 | 0.74 |
| Axolotl | 5.4 | 20.9 | 33.8 | 0.80 |
在这些数据出来之前,我本来以为 Unsloth 的优势会被 MPS 后端大幅削弱,结果它的确是全场最快,而且是断层第一。但从另一个角度看,Unsloth 相比 PEFT 基线的加速约 1.6 倍,峰值内存少约 35%,并没有达到官方在 CUDA 显卡上宣称的"2 倍以上起步、显存节省 80%"那种水平。
3.2 Unsloth:最快但真的发热
Unsloth 训练过程中,活动监视器里 GPU 使用率长期顶在 90% 以上,风扇声音非常明显。M2 Max 在高负载下表面温度会快速升高,掌托区域能感觉到热度。如果你是在寝室或者咖啡厅这种环境长时间跑任务,要做好心理准备。
这是 Mac 微调和台式机显卡训练的一个本质差异:散热系统决定持续性能。短时间跑 benchmark 看不出墙,但跑满 1 小时以上,CPU/GPU 频率会因为温度墙而下降。所以我记录的是三次运行中较为稳定的一组,不取最高值。
3.3 MLX-LM:Apple Silicon 生态的强力对手
MLX-LM 的表现比我预想好,每步 2.8 秒,只比 Unsloth 慢 17% 左右,峰值内存也只有 13.4GB。它的训练命令非常简单,MLX 框架基于 Apple 的 Metal 直接优化,不走 PyTorch 的 MPS 兼容层,所以算子执行路径更短。实际体验中唯一的问题是它默认会先把模型转换成自己的格式,首次转换要额外花几分钟。
另外,MLX-LM 的 LoRA 微调在配置上有一个明显差异:它不是像 Unsloth 那样对所有层插入 adapter,而是通过--num-layers参数控制训练层数。为了让四个引擎尽量可比,我把可训练层数配成了等效的四层投影矩阵。严格讲这跟 PEFT/Unsloth 的实现不是一一对应,但已经是最接近的对齐方式。
3.4 PEFT 基线:最稳也最占内存
Transformers 加 PEFT 的表现无愧"基线"两个字:不惊艳,但非常稳。每步 3.9 秒是 Unsloth 的 1.6 倍,峰值内存冲到 18.7GB。这和它的实现方式有关,PEFT 在默认训练模式下保留了较多中间激活值,虽然 LoRA 本身是参数高效的,但前向传播中每一层的输出都要用于回传,内存自然压不下来。
不过它也不是没有优点。整个训练过程没有任何花哨操作,断点续训、评估、日志都和标准 AI 流程完美兼容。如果你的目标不是追求极限速度,而是希望结果可复现、好对接团队已有代码,这套仍然是第一选择。
3.5 Axolotl:重配置反而拖了后腿
Axolotl 的实测结果是最慢的,每步 5.4 秒,内存也最高。原因倒不复杂:它默认开启的大量训练增强是为 CUDA 环境设计的,关掉 flash-attn 和 xformers 后,我在 Mac 上拿到的其实是一个"减配版"Axolotl。更坑的是,它内部的数据打包逻辑会自动做 sequence packing,把短样本拼成长序列,这在满负荷 GPU 上是吞吐利器,但在 MPS 后端反而引入额外的组装开销。我把这个开关手动关掉后速度提升了一截,但仍然追不上 PEFT 基线。
这也反映了一个选择逻辑:越是功能全面的框架,追求的可控性和兼容性越多,在特定硬件上的表现往往越难被优化。Mac 用户用 Axolotl,除非有团队协作的硬性要求,否则性价比很低。
4. Unsloth 的"快"到底从哪来:原理拆解与 Mac 上的真假验证
4.1 Unsloth 的三大优化手段
既然 Unsloth 在实测中确实把 PEFT 拉开了差距,我有必要搞清楚它的快来自哪里。简单拆解的话,它有三板斧。第一,手动实现了大量 autograd 自定义函数,核心思想是尽量不保存中间激活,而是等反向传播时再重新计算,用少量算力换存储空间。第二,对 RoPE、RMSNorm、Swiglu 这些高频算子做了融合内核,减少内核启动和内存读写次数。第三,LoRA 权重以低精度保存并动态反量化,比如 4bit 存储、bf16 参与计算,整体内存占用大幅下降。
这些手段单独拆开都不算黑魔法,大部分论文里都有对应原理,但 Unsloth 的价值在于把它们集中做成了开箱即用的库,并且针对各模型的架构做了定制。
4.2 哪些优化在 Mac 上真实生效
关键问题来了:这些优化在 Mac 的 MPS 后端上有多少能生效?我观察到的结论是,内存优化全部生效,因为"不存中间激活、重计算、低精度存储"这些是框架层面的设计,不依赖特定 GPU 硬件。但算子的融合优化只有一部分生效,原因是 MPS 后端对 Triton 和部分 CUDA kernel 没有对应实现,Unsloth 能调用的底层算子库受限于 PyTorch MPS 支持的算子集合,没法像在 NVIDIA 卡上那样完整展开 GPU kernel 魔法。
这也就解释了实测数据为什么是"明显快但没那么离谱":快的主要贡献来自低精度 LoRA 存储和更紧凑的激活管理,而 CUDA 上额外的 kernel 级加速在 Mac 上打了折扣。
4.3 用实验验证:关掉 Unsloth 的 4bit 和原生路径
为了确认这个判断,我又做了两组对照实验。第一组把 Unsloth 的 load_in_4bit 改成 False、用纯 bf16 加载,训练速度立刻从每步 2.4 秒掉到 3.1 秒,峰值内存从 12.1GB 涨到 16.4GB。第二组去掉 FastLanguageModel 的封装、直接走原生 Transformers 的 LoRA 路径,每步耗时 3.6 秒,内存 17.5GB,已经和 PEFT 基线非常接近。
说明 Unsloth 的加速确实不是营销话术,只是它的收益主要集中在低精度加载和内存重计算上,这在 Mac 上同样成立。至于官方宣传里更夸张的数字,我把它归因于 CUDA 和专用内核的增益,那是 Unsloth 在 PC 平台上的完整形态。
4.4 宣传数字 vs 实测数字
把宣传数字和我的实测放在一起看,结论就很清晰了:
| 指标 | Unsloth 官方宣传 | 我在 Mac 上的实测 |
|---|---|---|
| 训练提速 | 2~2.5 倍 | 相对 PEFT 约 1.6 倍 |
| 显存节省 | 最高 80% | 内存节省约 35% |
| 4bit 量化 | CUDA 原生支持 | MPS 下可用且有效 |
| 支持硬件 | 桌面与数据中心卡 | Apple Silicon 基本可用 |
所以"Unsloth 真有那么快吗"这个问题,我的回答是:真快,但在 Mac 上是"明显快、内存省三分之一",而不是"两倍加四倍"那种快。如果你想在 Mac 上复现官方演示视频里那种速度提升,大概率会失望;但如果你想找一个开箱即用的加速方案,它仍然是四个引擎里最合适的。
5. 快不等于好:四套引擎微调出来的模型,效果差别有多大
5.1 loss 曲线和过拟合情况
一个容易被速度测试带偏的点是:不同引擎微调出来的模型,效果可能不一样。看我的最终验证 loss,Unsloth 和 PEFT 几乎在同一水平,大约 0.72 到 0.74,MLX 略高一点,而 Axolotl 明显掉队,到 0.80 左右。更有趣的是训练 loss 的对比:Axolotl 训练集 loss 一度最低,可验证集 loss 反而最高,说明它的过拟合程度在四个引擎里最严重。
这提醒我们,某些引擎的"慢"并不等于"更安全"。Axolotl 的过拟合很可能来自它的自动数据打包逻辑在 MPS 上带来的额外随机性,或者和我关闭某些优化联动产生的副作用。速度引擎 Unsloth 反而是四个里面训练和验证 loss 差距最小的之一,这至少说明它的加速没有以损害泛化为代价。
5.2 同一组 Prompt 的生成质量对比
训练阶段结束之后,我固定了 5 条客服场景 prompt 来对比生成效果。其中一个典型问题是咖啡壶运输破损的处理,期待的输出结构是"先安抚、再要求取证、给出可选方案"。四个模型的表现差异很有意思:
- Unsloth:结构基本符合要求,先道歉再给两个方案,但回复偏长,有时会堆叠几个同义句。
- MLX-LM:语言最自然,分句和语气都很接近真人客服,但偶尔会漏掉"核实图片"这一步,直接跳到补偿方案。
- PEFT:模板感最强,几乎一定按照先安抚、后核实、再方案的顺序来,只是措辞干巴巴,像填空。
- Axolotl:不稳定,有两轮回复出现重复用词,内容围绕"非常抱歉"反复绕圈。
这个结果让我意识到,对客服这种有明确结构要求的场景,PEFT 的保守风格反而是优势,而 Unsloth 虽然快,但在小模型上的语言控制力并没有因此变好。1.5B 模型本身表达能力就那么高,引擎的作用主要是"能不能稳定地把结构和知识固化下去"。
5.3 LoRA 合并与推理阶段的影响
微调不等于训练完就结束。四个引擎产出的 adapter 权重格式不同,后续合并和推理的体验也差得远。PEFT 和 Unsloth 都是标准的 safetensors adapter,可以用 peft 的 merge_and_unload 直接合并到原模型。MLX-LM 则要在 mlx_lm.fuse 里完成合并,输出成 MLX 原生格式,这个格式后续用 llama.cpp 或者 mlx_lm.generate 跑都很顺畅。
推理速度方面,合并后的模型在同一台机器上用相同量化等级测,差距很小,基本都在每秒 35 到 42 个 token 的区间。也就是说,微调引擎的选择对训练阶段影响巨大,但对推理阶段的影响更多是"格式兼容性"而非"性能"。如果最终要部署到 iOS 或 macOS 应用里,直接选 MLX 链路最省事,它能一条路走到推理部署,中间少一次模型格式转换。
6. 选型建议与 macOS 微调的避坑清单
6.1 按工作流选择引擎
做了这么多测试,最后还是要落到实际选择上。我的建议是按使用场景分:
| 场景 | 推荐引擎 | 理由 |
|---|---|---|
| 个人实验、快速验证 | Unsloth | 安装相对简单、速度和内存双优 |
| Apple 平台应用落地 | MLX-LM | 原生 Metal、格式统一、部署顺畅 |
| 与团队标准流程兼容 | PEFT | 结果可复现、社区资料多 |
| 多机多卡企业级配置 | Axolotl | 功能完整,但别在 Mac 上跑 |
| 纯推理部署 | llama.cpp/MLX | 和微调引擎无关,但选对格式能省事 |
坦白讲,如果你是 Mac 用户、目标是快速把一个小模型调成特定领域的指令模型,Unsloth 是综合体验最好的选择。但如果你本来就打算把模型部署到 Apple 生态,那你大概率要经过 MLX,那不如直接在 MLX-LM 里完成微调,避免中途转换。
6.2 Mac 上微调的 10 条实操建议
下面这些经验是我这次一周测试里反复踩出来的,每一条都对应一个真实的报错或麻烦,顺序按影响程度排:
- 用 conda 管理 Python 环境,不要用系统 Python,也不要迷信 Homebrew,一次性装对能省很多时间。
- 固定 transformers、peft、torch 的版本再跑,Unsloth 对库版本很敏感,闭眼升级容易突然 break。
- 内存不够时先缩小 max_seq_length,再砍 batch size,不要一上来就堆 gradient accumulation。
- MPS 下 4bit 量化不要用 bitsandbytes,Unsloth 自己的 4bit 是 Mac 上最稳的低精度方案。
- 保存 checkpoint 时注意磁盘空间,一个 1.5B 模型的 adapter 不大,但中途存全量权重会非常占盘。
- 训练时开一个活动监视器,观察统一内存有没有出现压力图形变红,红了就说明在 swap,性能断崖下跌。
- MPS 的随机数语义和 CUDA 不完全一致,同一个种子在不同框架下不代表同样结果,多跑两个种子再下结论。
- 下载模型尽量提前用脚本缓存到本地,避免训练中途下载失败导致 session 中断。
- 不要同时开着浏览器大几十个标签页跑训练,统一内存很实在,后台进程真的会偷走训练内存。
- 温度上来以后性能会下降,长任务建议垫高机器帮助散热,至少别捂在被子里跑。
6.3 高频报错速查表
最后整理一张报错速查表,都是这次实测或社区里高频出现的问题:
| 报错/现象 | 根因 | 处理方式 |
|---|---|---|
| unsloth 安装时报 triton 编译错 | 依赖版本冲突 | 用 --no-deps 安装,手动补缺 |
| 进程被系统 Killed | 统一内存不足 | 减小 batch size / 缩短 max_seq_length |
| 4bit 加载失败 | bitsandbytes 不支持 MPS | 换 Unsloth 原生 4bit,或退回 bf16 |
| Axolotl 报 flash attention 相关错误 | 依赖 CUDA 内核 | config 里设 flash_attention: false |
| MLX-LM 首次加载很慢 | 正在格式转换 | 正常现象,等待转换完成并缓存 |
| 训练速度接近 0 | MPS fallback 到 CPU 计算 | 升级 PyTorch 到 2.2+,确认设备是 MPS |
| 有时 loss 为 NaN | MPS 数值路径差异 | 调小学习率或用 bf16,不要用 fp32 |
如果遇到"某个算子不支持 MPS"这类底层报错,通常的做法是升级 PyTorch nightly,或者把引擎内部该算子强制放到 CPU 执行。这些在四个引擎里我都碰到过至少一次,属于 Mac 用户的日常,习惯就好。
最后再补两句实操体会。我在这一周里反复确认了一件事:Mac 微调大模型的瓶颈从来不只是框架快不快,内存带宽、散热、依赖兼容这些都会参与决策。Unsloth 在这台 M2 Max 上的表现确实对得起它的名声,只是它的神化宣传需要打个折。而 MLX-LM 作为 Apple 的亲儿子,在很多场景下已经足够实力,如果你做的应用最后要跑在 iPhone 上,它才是更省心的那条路。希望大家按自己的实际场景做选择,不用像我一样每个引擎都硬踩一遍。