LangChain 系列 2:在 M1 Max 上本地部署 Qwen3-14B MLX 大模型


在开始写 LangChain 代码之前,先要准备一个可以稳定调用的大模型。

如果模型本身还不能正常加载,后面遇到问题时,就很难判断究竟是 LangChain 代码写错了,还是模型、运行环境、网络接口出了问题。因此,本系列先从最底层开始:把模型下载到本地,并确认它可以独立完成一次推理。

本文使用一台配备 64GB 统一内存的 M1 Max Mac,通过 Python 3.12、ModelScope 和 MLX-LM 部署 Qwen3-14B-AWQ-4bit-MLX。这里不讨论训练、微调和企业生产服务,而是完成模型下载、资源检查、直接推理验证,并把模型启动为只允许本机访问的 HTTP 服务。

如果还不清楚 LangChain、LangGraph 和 LangSmith 分别解决什么问题,可以先阅读 LangChain 系列 1《先搞懂 LangChain、LangGraph、LangSmith 是什么》。

整个部署过程可以拆成四个阶段:

环境准备
  -> 模型下载与文件检查
  -> MLX-LM 直接加载与推理验证
  -> 启动本机 HTTP 服务并验证聊天接口

这四个阶段不是只追求“命令没有报错”。最终还要确认模型文件完整、MLX-LM 能够加载、模型可以生成中文、HTTP 聊天接口能够返回真实回答,并且资源占用在机器可接受的范围内。

1. 先认识模型社区

1.1 Hugging Face

Hugging Face 是常见的开放模型社区。开发者会把模型权重、配置、Tokenizer、数据集、示例代码和模型说明发布到 Hugging Face Hub。

下载一个模型时,拿到的通常不只是一个权重文件,而是一整个模型仓库。后面会看到,模型目录中除了 .safetensors 权重,还包括 config.json、Tokenizer、聊天模板和生成参数等文件。

1.2 魔塔社区 ModelScope

魔塔社区 ModelScope 是国内常用的模型社区和工具链,也提供模型浏览、下载、推理、训练和评测等能力。

模型地址:

mlx-community/Qwen3-14B-AWQ-4bit-MLX

选择 ModelScope 不是因为 Hugging Face 不能下载这个模型,而是因为本文的下载代码使用 ModelScope,并且在国内网络环境下通常更方便。

2. 为什么选择 Qwen3-14B-AWQ-4bit-MLX

模型 ID 是:

mlx-community/Qwen3-14B-AWQ-4bit-MLX

这个名称包含了本次选型的几个关键信息。

名称 含义
Qwen3 模型系列
14B 参数规模约为 140 亿
AWQ 一种权重量化方式
4bit 模型权重主要按 4bit 保存
MLX 已转换为适合 Apple Silicon 使用的 MLX 格式

选择这个模型主要有三个原因。

第一,MLX 是面向 Apple Silicon 的数组计算框架。对于 M1、M2、M3、M4 系列 Mac,使用 MLX-LM 加载已经转换好的 MLX 模型,安装和推理过程都比较直接。

第二,4bit 量化可以显著降低模型权重占用。14B 模型如果使用 FP16 或 BF16,单是权重的理论体积就接近 28GB;使用 4bit 后,理论体积约为 7GB,更适合本地实验和单用户推理。

第三,14B 在模型能力和本地资源之间比较均衡。更小的模型占用更低,但中文问答、代码解释和后续 Agent 实验能力也会下降;更大的模型可能有更强的能力,但加载、上下文和并发会占用更多统一内存。

3. 部署前估算资源

下载模型之前,需要先判断机器能不能运行它。

部署前可以使用 ApX VRAM Calculator 估算显存需求。不过在线计算器只能给出参考值,最终仍然要看本机运行日志和活动监视器。

3.1 模型权重的粗略计算

最基础的计算公式是:

模型权重大小 ≈ 参数量 × 每个参数占用的字节数

常见精度可以这样估算:

精度 每个参数约占用
FP32 4 字节
FP16 / BF16 2 字节
INT8 1 字节
4bit 0.5 字节

因此,Qwen3-14B 4bit 的理论权重大小约为:

14B × 0.5 byte ≈ 7GB

3.2 运行内存不等于模型文件大小

7GB 只是权重的粗略下限。模型运行时还需要考虑:

资源 作用
量化元数据 保存 scale、bias 等量化信息
KV Cache 保存上下文计算结果,上下文越长,占用越高
Tokenizer 负责文本编码和解码
MLX 与 Python 运行时 加载代码、张量和中间计算结果
batch 与并发 同时处理的请求越多,占用越高
macOS 系统占用 系统进程与模型共享统一内存

Apple Silicon 使用统一内存,因此不能简单地把 64GB 全部当成模型可用“显存”。操作系统、应用程序、模型权重和运行时缓存都使用同一块内存。

统一内存的组成可以这样理解:

本地推理的统一内存组成

图中各项不是固定比例。上下文越长、batch 越大、并发请求越多,KV Cache 和运行时开销通常越高,因此目录大小只能用于部署前粗估,不能替代运行时观察。

本次模型目录实测为 7.8G,MLX-LM 生成日志中的 Peak memory 为 8.510 GB。这个指标反映 MLX 运行过程中的峰值内存,不等于整台 Mac 的总统一内存占用;判断系统压力时还要结合活动监视器和 vm_stat。对 M1 Max 64GB 来说,本次单用户推理有较充足的余量,但这不代表可以无限增加上下文和并发。

4. 项目目录

项目代码统一放在:

/path/to/llm-learning

第一阶段目录结构如下:

llm-learning/
├── .venv_langchain/
├── Qwen3-14B-AWQ-4bit-MLX/
├── requirements.txt
└── langchain/
    └── p02_local_models/
        ├── 01_download_model.py
        ├── 02_test_generate.py
        ├── README.md
        └── requirements.txt

代码位于 langchain/p02_local_models/,模型入口放在项目根目录的 Qwen3-14B-AWQ-4bit-MLX/。目录名直接展示了模型名称、量化方式和运行格式,后续阅读启动命令与客户端代码时,不需要再猜测 model/ 代表哪一个模型。首次部署时,下载脚本会创建这个普通目录并把模型文件保存在其中。后续 LangChain、LangGraph 和 LangSmith 的代码都可以复用同一份模型,不需要每个阶段复制一份 7.8GB 的权重。

为了复用此前已经下载的权重,本机将这个描述性目录设置成了符号链接:

llm-learning/Qwen3-14B-AWQ-4bit-MLX -> /Users/bianhn/Documents/git/llm/qwen3/model

这只是作者本机的目录安排,不是运行本文的前置条件。代码始终访问项目根目录的 Qwen3-14B-AWQ-4bit-MLX/;第一次部署时不需要创建符号链接,下载脚本会直接创建同名普通目录并保存模型文件。

5. 使用 Python 3.12 创建虚拟环境

进入项目目录:

cd /path/to/llm-learning

明确使用 Python 3.12 创建同名虚拟环境:

python3.12 \
  -m venv --prompt llm-learning .venv_langchain

激活虚拟环境:

source .venv_langchain/bin/activate

检查 Python 版本和解释器位置:

python -V
which python

本机输出:

Python 3.12.11
/Users/bianhn/Documents/git/llm-learning/.venv_langchain/bin/python

必须检查 which python。只看终端前面的环境名称并不可靠,尤其是在 Conda 的 (base) 环境中再次激活 venv 时,要以解释器实际路径为准。

6. 安装并锁定依赖

先升级 pip:

python -m pip install -U pip

第一阶段的 requirements.txt 内容如下:

mlx-lm==0.28.3
mlx==0.29.3
modelscope==1.31.0
transformers==4.57.1

安装依赖:

python -m pip install \
  -r langchain/p02_local_models/requirements.txt

安装完成后,使用包元数据检查真实版本:

python - <<'PY'
from importlib.metadata import version
import platform
import sys

# 检查当前 Python,避免依赖被装到其他环境。
print("python", platform.python_version())
print("executable", sys.executable)
# mlx 没有稳定的 __version__ 属性,使用包元数据读取版本。
for package in ["mlx", "mlx-lm", "modelscope", "transformers"]:
    print(package, version(package))
PY

本机输出:

python 3.12.11
executable /Users/bianhn/Documents/git/llm-learning/.venv_langchain/bin/python
mlx 0.29.3
mlx-lm 0.28.3
modelscope 1.31.0
transformers 4.57.1

本篇只安装本地推理需要的组件。LangChain、LangGraph 等依赖放到后续阶段安装,这样出现问题时更容易判断是模型环境还是应用框架造成的。

7. 使用 ModelScope 下载模型

下载脚本是 langchain/p02_local_models/01_download_model.py:

from pathlib import Path
import shutil
import sys
import time

from modelscope import snapshot_download


# 固定模型 ID,避免后续脚本误下载到原始 FP16/BF16 或其他量化版本。
MODEL_ID = "mlx-community/Qwen3-14B-AWQ-4bit-MLX"
MODEL_REVISION = "67a0161a28eb07263a862fa6b3a63fcd84b55c14"
# 当前脚本位于 llm-learning/langchain/p02_local_models/ 下。
# parents[2] 就是 llm-learning 项目根目录。
ROOT_DIR = Path(__file__).resolve().parents[2]
MODEL_DIR = ROOT_DIR / "Qwen3-14B-AWQ-4bit-MLX"

# 4bit 权重约 7-8GB,但下载临时文件、缓存和日志也要占空间。
# 这里预留 25GB,磁盘不足时提前失败,比下载到一半报错更容易排查。
MIN_FREE_GB = 25


def free_gb(path: Path) -> float:
    """返回指定目录所在磁盘的剩余空间,单位为 GiB。"""
    usage = shutil.disk_usage(path)
    return usage.free / 1024**3


def main() -> int:
    # 模型放在项目根目录,后续 LangChain、LangGraph 等代码都可以共用这一份模型。
    ROOT_DIR.mkdir(parents=True, exist_ok=True)
    MODEL_DIR.mkdir(parents=True, exist_ok=True)

    available = free_gb(ROOT_DIR)
    print(f"Target directory: {ROOT_DIR}")
    print(f"Model directory:  {MODEL_DIR}")
    print(f"Model ID:         {MODEL_ID}")
    print(f"Model revision:   {MODEL_REVISION}")
    print(f"Free disk:        {available:.2f} GiB")

    if available < MIN_FREE_GB:
        print(
            f"ERROR: free disk is lower than {MIN_FREE_GB} GiB. "
            "Clean disk space before downloading the model.",
            file=sys.stderr,
        )
        return 2

    started = time.time()

    # snapshot_download 会把模型仓库文件下载到本地目录。
    # 如果中途网络中断,保留同一个 MODEL_DIR 后重新运行,通常可以复用已下载内容。
    model_dir = snapshot_download(
        model_id=MODEL_ID,
        revision=MODEL_REVISION,
        local_dir=str(MODEL_DIR),
    )
    elapsed = time.time() - started

    print(f"Downloaded to:    {model_dir}")
    print(f"Elapsed seconds:  {elapsed:.1f}")
    print("Top-level files:")

    # 下载后列出顶层文件,便于确认 safetensors、tokenizer、config 是否齐全。
    for item in sorted(MODEL_DIR.iterdir()):
        print(f"  {item.name}")
    return 0


if __name__ == "__main__":
    # 用退出码表达下载结果,方便 shell、日志和后续自动化判断是否成功。
    raise SystemExit(main())

运行脚本:

cd /path/to/llm-learning
source .venv_langchain/bin/activate
python langchain/p02_local_models/01_download_model.py

初次下载时,ModelScope SDK 共下载 15 个顶层文件,其中最耗时的是两个 .safetensors 权重分片。本次初次下载日志为:

Downloading: 100%|██████████| 15/15 [49:12<00:00, 196.86s/file]
Downloaded to:    /Users/bianhn/Documents/git/llm-learning/Qwen3-14B-AWQ-4bit-MLX
Elapsed seconds:  2956.4

初次下载约 49 分钟。实际时间会受到网络速度、ModelScope 服务状态和磁盘写入速度影响,不能把这个数据当成固定值。

8. 模型目录中的 15 个文件

下载完成后,可以先确认顶层文件数量:

find -L Qwen3-14B-AWQ-4bit-MLX -maxdepth 1 -type f | wc -l

本机输出:

15

这 15 个文件的内容和作用如下。

文件 实测大小 内容与作用
.gitattributes 2.2K Git LFS 和仓库文件属性配置。它不参与推理,但记录了模型仓库中的大文件应如何管理。
README.md 830B 模型仓库说明,通常记录模型来源、转换方式和基础用法。它不是推理必需文件。
added_tokens.json 707B 额外 token 与 token id 的映射,包括聊天、thinking 和工具调用可能使用的特殊 token。
chat_template.jinja 4.6K 聊天模板。它负责把 system、user、assistant 消息转换成模型真正接收的 prompt。
config.json 80K 模型主体配置,包括网络结构、层数、注意力头、上下文长度和量化参数等。
configuration.json 73B ModelScope 平台使用的任务和框架元信息。
generation_config.json 237B 默认生成参数,例如 temperature、top_p、top_k 和结束 token。
merges.txt 1.6M BPE Tokenizer 的子词合并规则,与 vocab.json 一起决定文本怎样被切成 token。
model-00001-of-00002.safetensors 5.0G 第一个模型权重分片,保存量化后的模型张量。
model-00002-of-00002.safetensors 2.8G 第二个模型权重分片。两个分片合在一起才是完整权重。
model.safetensors.index.json 84K 权重索引,记录每个张量位于哪个 .safetensors 分片中。
special_tokens_map.json 614B 定义结束符、填充符、聊天边界等特殊 token 的语义。
tokenizer.json 11M Tokenizer 的完整序列化文件,负责把文本编码为 token,并把输出 token 解码为文本。
tokenizer_config.json 5.3K Tokenizer 配置,记录特殊 token、解码规则和聊天模板相关设置。
vocab.json 2.6M Tokenizer 词表,记录 token 字符串与 token id 的对应关系。

可以把它们简单分成四类:

类型 主要文件 作用
模型权重 model-*.safetensors 保存模型参数
模型结构 config.json、权重索引 告诉 MLX-LM 怎样构建并加载模型
文本处理 tokenizer*、vocab.json、merges.txt 在文本和 token 之间转换
对话与生成 chat_template.jinja、generation_config.json 组织聊天格式和默认生成参数

只看到两个大权重文件并不代表模型目录完整。缺少配置、索引或 Tokenizer 文件时,模型仍然可能无法加载。

9. 使用 MLX-LM 测试模型

测试脚本是 langchain/p02_local_models/02_test_generate.py:

from pathlib import Path
import subprocess
import time

from mlx_lm import generate, load


# 当前脚本位于 llm-learning/langchain/p02_local_models/ 下。
# 直接写出模型目录名称,让读者一眼看出当前加载的模型。
ROOT_DIR = Path(__file__).resolve().parents[2]
MODEL_DIR = ROOT_DIR / "Qwen3-14B-AWQ-4bit-MLX"


def run(command: list[str]) -> str:
    """执行系统命令并返回输出,这里只用于记录 du/vm_stat 等资源信息。"""
    result = subprocess.run(command, check=False, text=True, capture_output=True)
    return (result.stdout or result.stderr).strip()


def main() -> None:
    if not MODEL_DIR.exists():
        raise FileNotFoundError(f"Model directory does not exist: {MODEL_DIR}")

    model_size_path = MODEL_DIR.resolve() if MODEL_DIR.is_symlink() else MODEL_DIR

    # 先记录模型目录大小和加载前内存状态,方便和生成后的资源占用对比。
    print(f"Model directory: {MODEL_DIR}")
    if MODEL_DIR.is_symlink():
        print(f"Model target: {model_size_path}")
    print(f"Model size: {run(['du', '-sh', str(model_size_path)])}")
    print("vm_stat before loading:")
    print(run(["vm_stat"]))

    started = time.time()

    # load 会读取 MLX 权重和 tokenizer;如果模型文件不完整,通常会在这里暴露问题。
    model, tokenizer = load(str(MODEL_DIR))
    load_elapsed = time.time() - started
    print(f"Model load elapsed seconds: {load_elapsed:.1f}")

    messages = [
        {"role": "system", "content": "你是一个老师。"},
        {"role": "user", "content": "请用一句话说明唐诗和宋词的区别。"},
    ]

    prompt = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,

        # Qwen3 默认可能先输出 <think> 思考过程。
        # 连通性测试更关心模型能否快速返回答案,所以这里关闭 thinking。
        enable_thinking=False,
    )

    generate_started = time.time()
    response = generate(
        model,
        tokenizer,
        prompt=prompt,

        # 这里限制输出长度,避免测试脚本因为长回答占用过多时间和内存。
        max_tokens=256,

        # verbose=True 会打印 token 数、生成速度和 peak memory,是本次验证的关键指标。
        verbose=True,
    )
    generate_elapsed = time.time() - generate_started

    print("\nGenerated response:")
    print(response)
    print(f"\nGenerate elapsed seconds: {generate_elapsed:.1f}")
    print("vm_stat after generation:")
    print(run(["vm_stat"]))


if __name__ == "__main__":
    main()

运行测试:

cd /path/to/llm-learning
source .venv_langchain/bin/activate
python langchain/p02_local_models/02_test_generate.py

本次重新运行后的关键输出:

Model directory: /Users/bianhn/Documents/git/llm-learning/Qwen3-14B-AWQ-4bit-MLX
Model target: /Users/bianhn/Documents/git/llm/qwen3/model
Model size: 7.8G  /Users/bianhn/Documents/git/llm/qwen3/model
Model load elapsed seconds: 1.2
Prompt: 32 tokens, 36.757 tokens-per-sec
Generation: 38 tokens, 15.371 tokens-per-sec
Peak memory: 8.510 GB
Generate elapsed seconds: 3.7

模型实际回答:

唐诗以格律严谨、意境宏大见长,注重对仗与声律,而宋词则更重情感表达与音乐性,形式灵活,题材广泛。

这次测试的目的不是系统评估回答质量,而是确认模型文件完整、MLX-LM 可以加载权重、聊天模板能够正确组织消息,并且模型可以生成中文内容。脚本执行结束后进程会退出,模型也不再提供服务,因此还需要继续把它启动成常驻 HTTP 服务。

到这里可以确认:模型目录可访问、15 个文件可以被正确读取、MLX-LM 能够加载权重,Tokenizer 和聊天模板也能正常工作。

加载耗时和生成速度会随着机器负载、系统缓存、提示词长度和输出长度变化,因此文章记录的是本次实测值,不是性能保证。

10. 选择哪一种本地推理服务

下载模型只是把权重和配置保存到磁盘。要让 OpenAI SDK、LangChain 或其他程序通过统一地址调用模型,还需要启动一个持续运行的推理服务。

对比维度 MLX-LM Server Ollama vLLM
主要定位 Apple Silicon 上的 MLX 模型推理与本地 HTTP 接口 简化本地模型下载、管理和运行 面向吞吐量、批处理和服务化的推理引擎
常见硬件与系统 Apple Silicon Mac macOS、Linux、Windows,可使用 CPU 或受支持的 GPU 常见于 Linux 与服务器加速器;其他平台的支持取决于当前版本和硬件插件
模型产物 直接加载 MLX 格式权重 使用 Ollama 模型或其支持导入的模型格式 使用 vLLM 当前版本支持的模型架构与权重格式
接口 提供与 OpenAI Chat API 相近的 HTTP 接口 提供原生 API,也兼容部分 OpenAI API 提供 OpenAI 兼容服务接口
并发与吞吐 适合本地开发和基础调用,本文不做高并发承诺 支持并行请求和队列,实际能力取决于内存、上下文和并发配置 连续批处理等能力更适合高吞吐场景,但仍需容量测试
部署复杂度 已有 MLX 模型时最直接 日常本地使用和模型管理较方便 参数和运行环境较多,生产部署还需要外围基础设施
安全边界 官方说明只提供基础安全检查,不建议直接用于生产 默认本机使用;对外暴露时仍需认证和网络防护 推理引擎不等于完整安全网关,生产环境仍需认证、TLS、限流和审计

对比依据可以查看 MLX-LM Server 官方说明Ollama 模型导入文档Ollama 并发说明vLLM 安装说明。这些工具仍在持续更新,因此表格说明的是选型方向,不是永久不变的兼容矩阵。

还需要注意,GGUF 是一种模型文件格式,不等于量化方法。GGUF 文件既可以保存不同精度的权重,也可以使用 Q4、Q8 等量化版本。类似地,本文目录中虽然也有 .safetensors 文件,但其中保存的是已经转换过的 MLX 量化张量,不能只根据扩展名判断它可以被 Ollama 或 vLLM 直接加载。

本文已经下载的是 MLX 格式模型,并且运行机器是 M1 Max,所以继续使用 MLX-LM Server 可以避免重新下载或转换另一份权重。

11. 启动本地私有化服务

这里的“私有化服务”是指模型文件和推理过程都位于本机,HTTP 服务也只监听本机回环地址。它可以供本机的 OpenAI SDK 和 LangChain 调用,但不等同于已经完成企业生产部署。

11.1 启动 MLX-LM Server

在第一个终端进入项目并激活虚拟环境:

cd /path/to/llm-learning
source .venv_langchain/bin/activate

启动服务:

"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
  --model Qwen3-14B-AWQ-4bit-MLX \
  --host 127.0.0.1 \
  --port 18080 \
  --chat-template-args '{"enable_thinking": false}'

这里显式使用虚拟环境中的 Python,避免终端误调用 Conda Base 或其他环境中的 MLX-LM。参数含义如下。

参数 含义
--model Qwen3-14B-AWQ-4bit-MLX 从项目根目录的同名模型目录读取权重、配置和 Tokenizer
--host 127.0.0.1 只监听本机回环地址,局域网和公网不能直接访问
--port 18080 将 HTTP 服务端口固定为 18080
enable_thinking: false 基础连通性测试关闭 Qwen3 thinking 输出

启动后会看到类似日志:

UserWarning: mlx_lm.server is not recommended for production as it only implements basic security checks.
INFO - Starting httpd at 127.0.0.1 on port 18080...

出现启动日志后,这个终端仍需保持运行。它表示 HTTP 服务已经监听端口,但还不能单独证明模型能够完成生成。

11.2 检查 HTTP 服务

在第二个终端执行:

curl --fail-with-body --silent --show-error \
  http://127.0.0.1:18080/v1/models | python -m json.tool

该接口在当前环境返回:

{
    "object": "list",
    "data": []
}

HTTP 200 说明端口和路由可以访问。这个版本返回空的 data,不代表没有指定模型,也不能证明模型已经成功生成内容,因此还要继续请求聊天接口。

11.3 验证聊天接口

执行一次最小聊天请求:

curl --fail-with-body --silent --show-error \
  http://127.0.0.1:18080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "Qwen3-14B-AWQ-4bit-MLX",
    "messages": [
      {"role": "system", "content": "你是一个老师。"},
      {"role": "user", "content": "请用一句话说明唐诗和宋词的区别。"}
    ],
    "temperature": 0,
    "max_tokens": 96
  }' | python -c \
  'import json, sys; data = json.load(sys.stdin); print(data["choices"][0]["message"]["content"]); print(data["usage"])'

复测输出:

唐诗以格律严谨、意境宏大见长,注重对仗与声律,而宋词则更重情感表达与音乐性,形式灵活,题材广泛。
{'prompt_tokens': 32, 'completion_tokens': 38, 'total_tokens': 70}

聊天接口返回 HTTP 200、非空中文回答和 token 用量,才能确认 HTTP 服务、模型加载、聊天模板与生成流程都已经连通。请求中的 Qwen3-14B-AWQ-4bit-MLX 与启动命令的 --model Qwen3-14B-AWQ-4bit-MLX 保持一致;它既是当前项目中的模型目录名,也是客户端明确声明的模型名称。

11.4 停止服务与安全边界

测试完成后,在运行 MLX-LM Server 的第一个终端按 Ctrl+C 停止服务。停止后再次访问 18080 端口应该连接失败。

本文特意使用 127.0.0.1,而不是 0.0.0.0。MLX-LM 官方也明确提示这个服务只实现了基础安全检查,不建议直接用于生产环境。不要把 18080 端口直接暴露到局域网或公网。

企业环境除了推理引擎,还需要认证、TLS、反向代理、访问控制、限流、日志审计、监控告警和容量评测。本文只验证单机本地开发链路,不实现这些生产设施。

12. 怎样观察资源占用

12.1 检查模型文件大小

当前环境使用符号链接,所以检查它指向的真实模型目录:

du -sh /Users/bianhn/Documents/git/llm/qwen3/model

输出:

7.8G    /Users/bianhn/Documents/git/llm/qwen3/model

12.2 查看系统内存

可以在另一个终端执行:

vm_stat
top -o mem
ps aux | grep '[p]ython'

也可以直接打开 macOS 的“活动监视器”,观察:

  1. 内存压力是否变成黄色或红色。
  2. Python 进程占用了多少内存。
  3. Swap 使用量是否持续增加。
  4. 生成结束后内存是否能够回收。

vm_stat 中的 Swapins、Swapouts 是系统启动以来的累计计数,不能只看到非零值就判断本次推理正在大量使用 Swap。判断当前状态时,应结合活动监视器中的内存压力和 Swap 使用量。

13. 本次遇到的问题

13.1 系统 python3 不是 Python 3.12

第一次直接执行:

python3 -m venv .venv_langchain

创建出来的是 Python 3.9 环境,并出现了 urllib3 与 LibreSSL 相关警告。

解决方法是删除错误环境,然后明确使用 Python 3.12:

rm -rf .venv_langchain
python3.12 \
  -m venv --prompt llm-learning .venv_langchain
source .venv_langchain/bin/activate
python -V
which python

13.2 未固定版本导致依赖组合发生变化

直接执行不带版本约束的安装命令,会随着软件仓库更新而得到不同依赖组合。需要重建环境时,应优先使用项目的 requirements.txt;下面的命令用于强制重新安装其中记录的依赖:

python -m pip install --force-reinstall \
  'mlx-lm==0.28.3' \
  'mlx==0.29.3' \
  'modelscope==1.31.0' \
  'transformers==4.57.1'

因此,后续复现应优先使用仓库中的 requirements.txt,不要无条件升级所有依赖。

13.3 ModelScope 大文件下载中断

下载两个权重分片时出现过网络中断和 SSL 抖动:

IncompleteRead(...), will retry
SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number'), will retry

处理时不要删除已经下载的 Qwen3-14B-AWQ-4bit-MLX/,也不要临时更换下载目录。先让 ModelScope SDK 自动重试;如果脚本最终退出,再使用相同模型 ID 和相同目录重新运行,避免重新下载已有文件。

13.4 Qwen3 的 thinking 占满输出长度

第一次测试没有关闭 thinking,模型先生成较长的 。当 max_tokens=256 时,思考内容可能占满输出长度,真正回答反而被截断。

本文只是做连通性测试,因此在聊天模板中设置:

enable_thinking=False

这不会改变模型文件,只影响本次 prompt 的聊天模板。后续确实需要观察推理过程时,可以再开启 thinking,并相应提高 max_tokens。

13.5 移动项目后找不到模型

模型文件很大,不适合随着每个示例目录复制。作者本机通过项目根目录的 Qwen3-14B-AWQ-4bit-MLX 符号链接复用原来的权重:

ls -ld Qwen3-14B-AWQ-4bit-MLX

如果链接失效,可以重新创建:

rm Qwen3-14B-AWQ-4bit-MLX
ln -s /Users/bianhn/Documents/git/llm/qwen3/model Qwen3-14B-AWQ-4bit-MLX

如果它不是符号链接,而是正常的模型目录,不要执行 rm Qwen3-14B-AWQ-4bit-MLX。先用 ls -ld Qwen3-14B-AWQ-4bit-MLX 确认第一个字符是 l,再处理链接。

13.6 mx.metal.device_info 弃用警告

运行时可能看到:

mx.metal.device_info is deprecated and will be removed in a future version.
Use mx.device_info instead.

这是 MLX-LM 内部调用产生的弃用警告,不会阻止当前模型加载和生成。只要后面仍然有正常回答、生成速度和峰值内存日志,就不需要为了这个警告修改业务代码。

13.7 /v1/models 可访问,但聊天请求返回 502

本次实际遇到过 /v1/models 返回 HTTP 200,而聊天请求返回 502 的情况。服务端日志中同时出现:

RuntimeError: There is no Stream(gpu, 0) in current thread.

原因不是模型列表接口,而是客户端使用项目 .venv_langchain,服务端命令却调用了 Miniforge Base 环境中的旧版 MLX-LM。HTTP 路由可以先启动,真正进入模型生成线程后才暴露版本不匹配。

排查时应先查看运行 MLX-LM Server 的终端日志,然后检查解释器和依赖版本:

python -c 'import sys; print(sys.executable)'
python -c \
  'from importlib.metadata import version; print(version("mlx-lm"), version("mlx"), version("transformers"))'

确认无误后,使用项目虚拟环境中的解释器重新启动服务:

"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
  --model Qwen3-14B-AWQ-4bit-MLX \
  --host 127.0.0.1 \
  --port 18080 \
  --chat-template-args '{"enable_thinking": false}'

14. 小结

这次部署完成了五件事:

  1. 使用 Python 3.12 创建独立的 .venv_langchain 虚拟环境。
  2. 使用 ModelScope 下载 Qwen3-14B-AWQ-4bit-MLX 的 15 个模型文件。
  3. 使用 MLX-LM 直接加载本地模型并生成中文回答。
  4. 使用 MLX-LM Server 启动只监听本机的 HTTP 服务。
  5. 分别验证模型列表路由和真实聊天生成接口,并记录资源占用与排错过程。

对于 M1 Max 64GB,14B MLX 4bit 模型能够在保留较多系统余量的情况下完成本地单用户推理,并通过 OpenAI 风格的聊天接口为本机程序提供服务。下一篇不再重复部署服务,而是分别使用 OpenAI Python SDK 和 LangChain 调用这个地址。

15. 最终部署结果

完成环境安装、模型下载、直接推理和 HTTP 服务验证后,将本次实际部署数据整理如下。

项目 实测结果 含义
机器 Apple M1 Max,64GB 统一内存 本次部署使用的硬件及可供 CPU、GPU 共同使用的内存容量
系统 macOS 15,arm64 操作系统版本及 Apple Silicon 使用的处理器架构
Python 3.12.11 运行下载和推理脚本的 Python 版本
模型 mlx-community/Qwen3-14B-AWQ-4bit-MLX ModelScope 上的完整模型仓库 ID,指向本次下载的具体模型版本
模型 revision 67a0161a28eb07263a862fa6b3a63fcd84b55c14 下载脚本固定的模型仓库提交版本
模型格式 MLX,AWQ 4bit 量化 权重适用于 MLX 框架,并使用 AWQ 方法压缩为 4bit
模型目录大小 7.8G 模型全部文件在磁盘上的实际占用空间
模型文件数量 15 个顶层文件 模型目录第一层包含的权重、配置和分词器等文件数量
初次下载耗时 约 49 分钟 首次从 ModelScope 下载完整模型所需时间
MLX-LM 0.28.3 用于加载和运行大语言模型的 MLX-LM 工具版本
MLX 0.29.3 Apple Silicon 上执行张量计算的 MLX 框架版本
ModelScope 1.31.0 用于从魔塔社区下载模型的 Python 包版本
transformers 4.57.1 用于读取模型配置和分词器等内容的 Transformers 版本
本次复测加载耗时 1.2 秒 从本地磁盘加载模型并完成推理准备所需时间
本次复测生成速度 15.371 tokens/s 模型平均每秒生成的 token 数量
本次复测 MLX-LM Peak memory 8.510 GB 本次推理过程中 MLX-LM 记录的峰值内存占用
本地服务地址 http://127.0.0.1:18080 只允许本机访问的 MLX-LM Server 地址
/v1/models HTTP 200,data 为空数组 证明 HTTP 路由可访问,不单独代表推理成功
/v1/chat/completions HTTP 200,返回中文回答与 token 用量 证明服务端已经完成真实模型生成
服务适用范围 本机开发与接口联调 MLX-LM Server 只提供基础安全检查,不直接作为生产公网服务

模型的 bit 数表示权重保存时使用的精度。16bit 精度较高,但模型体积和内存占用最大;8bit 在精度与资源占用之间较均衡;4bit 占用最小,更适合本地部署,但可能带来少量效果损失。以 14B 模型为例,仅权重的理论大小在 16bit、8bit 和 4bit 下分别约为 28GB、14GB 和 7GB。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录