结论:M4 与 16GB 统一内存的 Mac 上,用 mlx-lm 给 Qwen3-0.6B 挂一层 LoRA,训 300 轮花 105 秒、进程峰值 2.6GB,把一条「告警文本转固定 JSON 编码」任务的 120 条测试集从全错做到全对。固定格式、固定枚举、内部编码这类活值得训,指望它记住新知识就别训。
这台机器没有独显,显存就是内存。很多人默认 Mac 只能推理不能训练,实测并非如此:0.6B 与 1.7B 都在舒适区,8B 的 4bit 量化版也能跑完。真正劝退人的是数据格式和几个默认参数,硬件反倒不是瓶颈。
这轮做的是什么任务
场景取自运维值班。一条中文告警进来,模型要吐出 {"team":"存储组","code":"STO-02","level":"P2"}。编码一共 12 个,分属存储组、网关组、账号组、计费组,级别 P1 到 P3 由编码类型决定。这套映射规则人能背下来,模型不训就是不会。
数据由脚本合成,480 条训练、60 条验证、120 条测试,12 个编码严格均衡。合成数据会高估效果,这句话得写在前面:模板化样本的同义表达太集中,真实业务请用人工标注的 200 到 500 条替换,再用同一套判据复核。
环境与模型下载
本轮版本组合:mlx-lm 0.32.2、mlx 0.32.4、Python 3.12.12、macOS 27.3、M4 芯片 16GB。安装只有两条命令,走清华源十几秒装完。
uv venv -p 3.12 .venv
uv pip install --python .venv/bin/python mlx-lm
模型别走 Hugging Face 镜像。本轮同时段实测,hf download 经 hf-mirror 拉 Qwen3-0.6B,十分钟只落了 4.4MB 就不再增长;改走魔搭的 HTTP 接口,Qwen3-1.7B 的两个分片共 4.06GB 用 127 秒下完,折合 32MB/s。镜像速度随时间波动,任何把速度写死的教程都别信。
for f in config.json tokenizer.json tokenizer_config.json vocab.json merges.txt model.safetensors; do
curl -sL -o "$f" "https://modelscope.cn/api/v1/models/Qwen/Qwen3-0.6B/repo?Revision=master&FilePath=$f"
done
1.7B 这类仓库是分片的,文件名带 model-00001-of-00002.safetensors,并配一份 model.safetensors.index.json。按 0.6B 的单文件写法去拉,model.safetensors 会返回一段几百字节的 JSON 错误体,训练时报缺少配置文件。
数据目录的硬规则
mlx-lm 只认一个目录,目录里文件名必须正好是 train.jsonl 与 valid.jsonl。你叫成 mydata.jsonl 或者塞进子目录,它就当 Hugging Face 数据集 ID 去联网找,最后抛 Training set not found or empty。
每行一条样本,聊天格式最省事:
{"messages":[{"role":"user","content":"监控发现 注册验证码 失败 83条(持续 3 分钟),请判定归属团队"},
{"role":"assistant","content":"{\"team\": \"账号组\", \"code\": \"AUTH-03\", \"level\": \"P3\"}"}]}
assistant 段里的 JSON 要转义,别用单引号,也别把三个字段写成嵌套对象。缺少 valid.jsonl 不会报错,只打一行 Warning 就继续训,代价是你再也看不到 val loss,判断不了过拟合,也不知道该在第几轮停。
样本要按标签均衡。本轮 480 条是 12 类各 40 条。直接拿原始分布去训,模型会把高频编码背得很熟,低频编码一条都对不上。
训练命令与三档对照
命令就一条,四个参数决定成败:--num-layers 决定挂哪几层,--batch-size 决定显存,--learning-rate 决定收敛快慢,--mask-prompt 让损失只算在回答段上。
python -m mlx_lm lora --train \
--model models/qwen3-0.6b \
--data data \
--batch-size 4 --iters 300 --learning-rate 1e-4 \
--num-layers 16 --max-seq-length 768 \
--adapter-path adapters06 --mask-prompt
入口是新写法 python -m mlx_lm lora。老教程里的 python -m mlx_lm.lora 还能跑,但会先打一行弃用提示,在日志里容易和真正的报错混在一起。
同一份数据、同一套超参,三个规模的实测如下。耗时是 300 轮的墙钟时间,内存取进程峰值,准确率是测试集的整条命中,8B 那档跑 60 条,另两档跑 120 条。
| 模型 | 可训参数 | 300 轮耗时 | 单轮 | 进程峰值 | adapter 体积 | 测试集 |
|---|---|---|---|---|---|---|
| Qwen3-0.6B(fp16) | 2.884M / 596.05M(0.484%) | 105.1 秒 | 0.35 秒 | 2.6GB | 11.6MB | 120 / 120 |
| Qwen3-1.7B(fp16) | 4.981M / 1.72B(0.289%) | 218.0 秒 | 0.73 秒 | 5.4GB | 19.0MB | 120 / 120 |
| Qwen3-8B(4bit) | 9.699M / 8.19B(0.118%) | 933.1 秒 | 3.11 秒 | 7.7GB | 37MB | 60 / 60 |
反直觉的一处:参数从 0.6B 涨到 8B 是 13.7 倍,训练时间涨不到 10 倍。原因是 LoRA 只让 adapter 参与反向传播,主干权重只做前向。显存才是随规模线性走的那一项。
数据量到多少才够
把训练集切成每类 4 条、10 条、40 条三档,同样训 300 轮,测同一个 120 条测试集,验证集的损失一起记下来。
| 训练样本 | 每类条数 | val loss | 编码命中 | 整条命中 |
|---|---|---|---|---|
| 48 条 | 4 | 0.033 | 83.3% | 80.8% |
| 120 条 | 10 | 0.000 | 100% | 100% |
| 480 条 | 40 | 0.000 | 100% | 100% |
48 条那档的 23 个错例很有信息量。格式错 0 个,全部错在相邻编码:短信验证码失败(AUTH-03)被判成登录 P99(AUTH-02)出现 4 次,限流拒绝率(GWY-03)被判成 AUTH-03 出现 2 次,计费消息堆积(BILL-01)被判成发票开具(BILL-03)出现 1 次。格式在 48 条时就学会了,缺的是语义边界。
落到一句可操作的:每个编码至少 10 条,一百多条就能解决。少于每类 4 条别急着调学习率,先把标签体系砍到能分清。
怎么确认 adapter 真的生效
训练跑完不等于加载上了。三个判据逐个过。
第一,训练日志里 Trainable parameters 必须非 0。本轮三档分别是 0.484%、0.289%、0.118%。看到 0,就是 --num-layers 或数据路径写错。
第二,拿同一份测试集,带 adapter 与不带 adapter 各跑一遍。0.6B 不带是 120 条全错,带上就是 120 条全对。这个对照比任何 val loss 都硬。
第三,看 adapter 目录里的 adapter_config.json,rank 与 scale 要与命令里写的一致。
prompt = tok.apply_chat_template(msgs, add_generation_prompt=True,
tokenize=False, enable_thinking=False)
out = generate(model, tok, prompt=prompt, max_tokens=64)
# load(model_path, adapter_path="adapters06")
三个静默失败
第一个,默认只训最后 16 层。Qwen3-0.6B 和 1.7B 都是 28 层,--num-layers 16 意味着前 12 层完全冻结。改输出格式够用,想让模型吸收新的领域表达就得写 --num-layers -1,显存和时间跟着涨。日志里只打印 16 layers,不会提醒你这不是全模型。
第二个,每个存档点都留一份权重。默认 --save-every 100,训 300 轮就得到 100、200、300 三份加一份最终 adapters.safetensors。0.6B 单份 11.6MB,目录 44MB;1.7B 目录 76MB。上线只读最终那份,其余的删掉,或者把 --save-every 设成与 --iters 相同。
第三个,推理端的思考段会吃掉 token 预算。Qwen3 的聊天模板默认不注入思考块,但模型自己会写。给 64 token 预算时,0.6B 的输出整段都是「好的,我现在需要处理用户提供的关于月结对账差异的告警信息」,一个左花括号都没有,JSON 命中率 0%。加 enable_thinking=False 才直接答。这条只解释输出为什么是空的或被截断,抬预算解决不了能力问题:把预算放到 512 token,准确率仍是 0%,它改为写一段分派建议。上下文预算怎么换算成可用输入,见 Ollama 上下文设多少才够 里那组 4096 只吃 2050 token 的实测。
合并权重与接到本地服务
adapter 是外挂的,部署前要和主干合并:
python -m mlx_lm fuse --model models/qwen3-0.6b \
--adapter-path adapters06 --save-path models/qwen3-0.6b-json
合并后的目录约 1.1GB,可以直接喂给 mlx_lm.server,不用再传 adapter 路径。要进 Ollama 或 llama.cpp 还得转 GGUF,那一步的坑在脚本本身:转换报错时退出码仍然是 0,批量脚本只能靠产物文件是否存在来判定,别信 $?。
显存怎么估、不同量化档位差多少,可以对着 本地大模型显存的三笔账 一起看。训完之后想比推理引擎的速度,MLX 与 Ollama 的同机实测 里有长上下文反转的那一段。
常见问题
16GB 的 Mac 能训多大的模型?
0.6B 峰值 2.6GB,1.7B 峰值 5.4GB,8B 的 4bit 版峰值 7.7GB。留 3GB 给系统,16GB 机器的上限在 8B 4bit 附近,再往上要 32GB。fp16 的 8B 光权重就 16GB,不要试。
微调能让模型学会新知识吗?
不能。LoRA 只动 0.1% 到 0.5% 的参数,学的是输出格式、枚举集合、术语映射。要它知道产品参数或内部文档,走检索,微调帮不上。
需要多少条数据?
每个标签 10 条起。本轮每类 4 条只有 80.8%,每类 10 条就到 100%。数据少于 50 条时先怀疑标签体系有歧义,别急着调学习率。
延伸阅读
- Cherry Studio 知识库配嵌入:Ollama 实测与四个坑3 天前
- 本地大模型显存怎么算:8B 每 1K 上下文 144MB5 天前
- MinerU 4.2 装完仍报错:五步排查与三档速度实测9 小时前
- Ollama 显存不释放怎么修:keep_alive 四写法实测4 天前
继续读
往下这几篇值得接着看:
- MinerU 4.2 装完仍报错:五步排查与三档速度实测2026-11-13
- n8n 本地安装实测:Node 24 与 5679 端口两个坑2026-11-13
- AWS Emulator 开源:438 个 API 模型本地跑与 59.71% 保真度2026-11-13
