为什么选择用 Mac 来微调大模型?因为相比起来内存真的便宜啊,要是买一张 A100(80G),那预算得上六位数,而在 Mac 上,96GB 内存的 Mac Studio 价格不到 A100 的零头,这样可以用更低的价格跑上更大规格的模型了。
博主使用的配置为 M3 Ultra,256GB 内存的 Mac Studio。本文使用框架为苹果开发的 mlx-lm 框架,模型使用 Qwen3 举例,为快速测试,本文选用 Qwen3-4B 模型进行演示,不同规模的模型在操作方法上无区别。
准备工作
1.创建虚拟环境
conda create -n mlx python=3.12
conda activate mlx
2.安装依赖
modelscope 用于下载模型权重。
pip install mlx-lm
pip install modelscope
3.下载模型权重
cache_dir 为模型保存的路径,模型可以选择其他尺寸,注意不能下载量化模型。
from modelscope import snapshot_download
model_dir = snapshot_download('Qwen/Qwen3-4B', cache_dir="./model")
4.将模型转化为 mlx 格式
from mlx_lm import convert
hf_path = "./model/Qwen/Qwen3-4B"
mlx_path = "./model/Qwen/Qwen3-4B-MLX"
convert(hf_path, mlx_path, dtype="float16")
可选参数:
hf-path: Hugging Face 格式模型路径mlx-path: 转换后的 MLX 模型保存路径dtype: 模型精度,使用 float16 可以减少内存占用quantize: 布尔类型,是否量化,默认关闭q_bits: int类型,量化精度,默认为 4
模型推理
from mlx_lm import load, generate
model, tokenizer = load("./model/Qwen/Qwen3-4B-MLX")
prompt = "写一段简短的大模型介绍。"
messages = [{"role": "user", "content": prompt}]
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True
)
text = generate(model, tokenizer, prompt=prompt, max_tokens=2048, verbose=True)
运行结果:
可以看到跑 qwen3-4B 可以达到 73 tokens/s,速度还是不错的。
模型训练
1.准备数据
创建存放数据的文件夹
mkdir data
创建训练数据文件(本文快速构建简单的数据用于测试)
cat > data/train.jsonl << 'EOL'
{"text": "<|im_start|>user\n你是谁?<|im_end|>\n<|im_start|>assistant\n我叫小小安,一个擅长解决医学问题的AI助手,很高兴能帮助你。<|im_end|>"}
{"text": "<|im_start|>user\n请介绍一下你自己<|im_end|>\n<|im_start|>assistant\n我的名字是小小安,一个具有丰富医学知识的AI助手,很高兴能帮助你。<|im_end|>"}
{"text": "<|im_start|>user\n你叫什么名字?<|im_end|>\n<|im_start|>assistant\n我叫小小安,一个医学垂直领域的AI助手,请问有什么可以帮助你的?<|im_end|>"}
EOL
创建验证数据
cat > data/valid.jsonl << 'EOL'
{"text": "<|im_start|>user\n你叫什么名字?<|im_end|>\n<|im_start|>assistant\n 我的名字叫小小安,你有什么想咨询我的医疗问题吗。<|im_end|>"}
{"text": "<|im_start|>user\n你是谁?<|im_end|>\n<|im_start|>assistant\n我是小小安,一个医学的AI助手,很高兴能帮助你。<|im_end|>"}
{"text": "<|im_start|>user\n 中国首都在哪里<|im_end|>\n<|im_start|>assistant\n 中国的首都是北京。<|im_end|>"}
EOL
数据格式要求如下:
- 文件格式需要是 JSONL
- 每行一个完整的 JSON 字符串
- 必须包含 text 字段
- 使用 Qwen 的格式:
用户输入:<|im_start|>user\n问题<|im_end|>
助手回答:<|im_start|>assistant\n回答<|im_end|> - 必须有 train.jsonl 和 valid.jsonl
2.创建训练配置文件
创建文件 lora_config.yaml,填入以下内容:
model: "./model/Qwen/Qwen3-4B-MLX"
train: true
fine_tune_type: lora
optimizer: adamw
data: "./data"
seed: 0
num_layers: 16
batch_size: 1
iters: 5
learning_rate: 1e-5
steps_per_report: 1
steps_per_eval: 5
adapter_path: "adapters"
save_every: 5
lora_parameters:
keys: ["self_attn.q_proj", "self_attn.v_proj"]
rank: 8
scale: 20.0
dropout: 0.0
参数说明:
model: MLX 格式的模型路径train: 启用训练模式fine_tune_type: 训练方法,lora、dora、fulloptimizer: 选择优化器data: 训练数据目录,需包含 train.jsonl 和 valid.jsonlseed: 随机数种子num-layers: Lora 层数,越小内存占用越少batch-size: 批次大小,Mac 上建议保持为 1-2。 测试了 mac 大 batch 会导致内存访问模式不够优化。iters: 训练轮次,这里由于数据量比较少设置为 5 步,防止过拟合。learning-rate: 学习率steps-per-report: 每 1 步报告一次训练状态steps_per_eval: 每 5 步调用一次验证集adapter_path: 训练权重保存路径save_every: 每 5 步保存一次lora_parameters: LoRA 参数设置
随后运行以下命令开始训练:
mlx_lm.lora --config ./lora_config.yaml
训练过程如下,可以看到 loss 有效地下降了,由于训练数据比较少,同时训练的模型比较小,训练速度比较快,大概三秒就完成训练了:
3.测试模型
测试时仅需要在 load 函数中增加一个参数 adapter_path:
from mlx_lm import load, generate
model, tokenizer = load('./model/Qwen/Qwen3-4B-MLX', adapter_path="./adapters", tokenizer_config={"eos_token": "<|im_end|>"})
prompt = "你是谁?"
messages = [{"role": "user", "content": prompt}]
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True
)
response = generate(model, tokenizer, prompt=prompt, max_tokens=2048, verbose=True)
输出结果如下:
接下来测试一下通用能力,把提示词改为“头痛应该吃什么药?”,结果如下:
可以看到,训练没有造成过拟合,只改变了我们想改变的部分,同时充分发挥了 Qwen 系列模型混合推理的优势。