Mac 基于 mlx-lm 开发大模型(调用及训练)以 Qwen3 为例

1,995 阅读4分钟

为什么选择用 Mac 来微调大模型?因为相比起来内存真的便宜啊,要是买一张 A100(80G),那预算得上六位数,而在 Mac 上,96GB 内存的 Mac Studio 价格不到 A100 的零头,这样可以用更低的价格跑上更大规格的模型了。

image.png 博主使用的配置为 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)

运行结果:

image.png

可以看到跑 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、full
  • optimizer: 选择优化器
  • data: 训练数据目录,需包含 train.jsonl 和 valid.jsonl
  • seed: 随机数种子
  • 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 有效地下降了,由于训练数据比较少,同时训练的模型比较小,训练速度比较快,大概三秒就完成训练了:

image.png

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)

输出结果如下:

image.png

接下来测试一下通用能力,把提示词改为“头痛应该吃什么药?”,结果如下:

image.png

可以看到,训练没有造成过拟合,只改变了我们想改变的部分,同时充分发挥了 Qwen 系列模型混合推理的优势。