MacBook Air M5:零依赖运行多模态大模型!
一个 8 MB 级原生可执行文件,加上一份 Gemma 4 E4B 模型,就能在 Mac 上对话、理解图片,也能作为 Rust 库直接嵌入现有程序。
不安装 Python、PyTorch、Conda、Docker,也不启动常驻模型服务:一个 Rust 对象、一份 GGUF 权重,就能把原生多模态推理放进自己的进程。
这篇文章在一台 24 GB 内存、10 核 Apple M5 的 MacBook Air 上完成。当前 release 版 zllm-metal 是 8.1 MB;配合 Gemma 4 E4B 的 Q4_K_M 主模型和视觉投影,它可以直接通过 Metal 完成多轮对话和图像理解。
先说明标题中的“零依赖”:它指的是交付和运行时不依赖第三方推理框架。最终用户不需要 Python 环境、PyTorch 动态库、CUDA 工具包或单独管理的 C++ runtime;模型权重仍然是推理所需的数据,macOS 自带的 Metal 与系统框架也仍然存在。从源码构建时,Cargo 会正常编译 zLLM 使用的 Rust crates,并把它们链接进原生程序。
库模式是怎样工作的
zLLM 不是对另一个本地服务的 HTTP 包装。zllm::embedded::Engine 直接持有模型、Metal backend、KV Cache 和生成状态,在调用方进程内走完整推理路径:
Rust 应用
└─ Engine::from_config(装载一次)
├─ GGUF 权重与 chat template
├─ Metal backend / kernels
└─ terminal KV Cache
↓
Engine::generate(重复调用)
├─ OpenAI Chat JSON → template → tokenize
├─ 可选 image_url → mmproj → 视觉 embedding
├─ prefill / append prefill / decode
└─ 每个 token 通过 Rust 回调返回
这条路径与 zLLM 的 Metal node 共用模型 runtime、权重装配和生成状态机,只是没有启动 HTTP、Scheduler 或 iroh。
| 接口 | 用途 |
|---|---|
Engine::from_config(path) | 从 kind: standalone YAML 装载模型和 backend,但不启动服务 |
Engine::load(...) | 已经解析配置时直接装载 |
Engine::generate(...) | 接受结构化 Chat JSON,通过回调流式返回 token |
Engine::cancellation() | 创建可复制到 UI 或控制线程的取消句柄 |
Engine::kv_resources() | 查看 KV capacity、active、resident、available 和单会话占用 |
GenerationResult | 返回结束原因、输入/输出 token 数和后续轮次可复用的 cache_id |
回调返回 false,或从另一线程调用 Cancellation::cancel(),生成就会结束。引擎释放时还会执行优雅关闭;调用方不需要管理 Metal command buffer,也不会接触平台 KV 对象。
在任意 Rust 程序中嵌入
zLLM 源码已在 GitHub 公开。 可以直接使用 Git 依赖,也可以 clone 后改成本地
path。
在现有项目的 Cargo.toml 中加入:
[dependencies]
serde_json = "1"
zllm = { git = "https://github.com/zllm-lab/zllm" }
把路径换成 zLLM 源码在本机的实际位置。下面是一个完整的最小程序:引擎只加载一次,之后可以反复调用 generate。
use std::io::{self, Write};
use serde_json::json;
use zllm::embedded::Engine;
fn main() -> Result<(), String> {
let mut engine = Engine::from_config("gemma4-metal.yaml")?;
let cancellation = engine.cancellation();
let result = engine.generate(
&json!({
"model": "gemma4",
"messages": [{
"role": "user",
"content": "用一句话解释为什么推理引擎适合嵌入应用"
}],
"max_completion_tokens": 128
}),
&cancellation,
|_token, text| {
print!("{text}");
io::stdout().flush().is_ok()
},
)?;
println!(
"\nfinish={} prompt={} completion={} cache={:?}",
result.finish_reason,
result.prompt_tokens,
result.completion_tokens,
result.cache_id
);
Ok(())
}
后续轮次继续传完整的 messages。Runtime 会校验 cache identity 和真实 token 前缀;匹配时恢复 terminal session,只为新增后缀执行 append prefill,不匹配时安全回退到最长公共前缀或完整 prefill。
图像输入仍然使用同一个接口,只需把 user content 改成有序的图文 part:
let cancellation = engine.cancellation();
let result = engine.generate(
&json!({
"model": "gemma4",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "请描述这张图片"},
{"type": "image_url", "image_url": {
"url": "/absolute/path/to/photo.png"
}}
]
}],
"max_completion_tokens": 256
}),
&cancellation,
|_token, text| {
print!("{text}");
io::stdout().flush().is_ok()
},
)?;
本地路径、HTTP(S) URL 和 data: URL 会进入同一套图像物化与视觉编码路径。Gemma 4 E4B 的 mmproj 在第一次图像请求时懒加载;只做文字对话时不必先装载视觉塔。
准备 Gemma 4 E4B
本文使用 instruction-tuned 的 Gemma 4 E4B GGUF:
下载下面两个文件,并放在同一个目录:
models/gemma-4-E4B-it-GGUF/
├── gemma-4-E4B-it-Q4_K_M.gguf # 主模型,4.98 GB
└── mmproj-F16.gguf # 视觉编码器/投影,990 MB
可以在上面的文件页直接下载,也可以临时使用 Hugging Face CLI;这个下载工具不是 zLLM 的运行时依赖:
hf download unsloth/gemma-4-E4B-it-GGUF \
gemma-4-E4B-it-Q4_K_M.gguf mmproj-F16.gguf \
--local-dir ./models/gemma-4-E4B-it-GGUF
国内网络可以从魔搭页面选择同名文件。Q4_K_M 在 24 GB MacBook Air 上兼顾体积和质量;视觉投影约 1 GB,保留 F16 可以避免再引入一层视觉量化误差。
库模式使用的最小 gemma4-metal.yaml 如下:
version: 1
kind: standalone
http:
listen: 127.0.0.1:8000
public_base_url: http://127.0.0.1:8000
artifacts:
directory: ./artifacts
node:
cache_directory: ./cache/gemma4
persist_kv_cache: false
max_concurrency: 1
model:
architecture: gemma4
weights_directory: ./models/gemma-4-E4B-it-GGUF/gemma-4-E4B-it-Q4_K_M.gguf
lm_head_quantization: native
max_sequence_length: 49152
execution:
prefill_chunk_size: 2048
backend:
kind: metal
device: default
配置里保留 http 和 artifacts 是为了与 standalone 配置结构兼容;Engine::from_config 不会监听端口。
一个 8 MB 的 Metal 命令行程序
只想在终端里马上对话时,不需要写 YAML。直接从 GitHub 获取源码并构建:
git clone https://github.com/zllm-lab/zllm.git
cd zllm
cargo build --release --bin zllm-metal
然后把主模型路径直接传给 8.1 MB 的 release 程序:
./target/release/zllm-metal \
./models/gemma-4-E4B-it-GGUF/gemma-4-E4B-it-Q4_K_M.gguf
zllm-metal 会自动完成这些工作:
- 从 GGUF metadata 识别 Gemma 4 架构;
- 发现同目录的
mmproj-*.gguf; - 如果存在匹配的
mtp-*.gguf,自动启用 MTP 投机解码; - 根据统一内存、权重和安全余量推导 KV 预算与上下文;
- 装载 Metal runtime,进入多轮终端对话并复用 terminal KV Cache。
这台机器的一次实际启动输出是:
[zllm-metal] gemma4 (hybrid GQA, KV f16) | 权重 4.64 GiB |
KV 预算 7.96 GiB | ≈172032 B/token | 上下文 49152 (模型上限 131072)
[zllm-metal] 模型加载完成(1.4s),直接输入消息开始对话
直接输入文字即可对话:
> 请用一句话介绍你自己,并说明你正在本地 Mac 上运行。
识别本地图片时,把路径和问题放在同一条消息里即可;裸路径、引号、反引号和带空格路径都支持,路径本身不会进入模型提示词:
> 请用两句话描述这张图片 `/Users/me/Pictures/demo.png`
也可以先复制图片,再输入:
> /paste 请描述剪贴板中的图片
常用命令如下:
/stats 查看上下文与 KV 内存
/reset 清空当前对话
/compact 压缩较早的对话历史
/paste 读取 macOS 剪贴板中的 PNG 图片
/exit 退出
真机结果与视频演示
下面的数据均来自这台 MacBook Air M5(24 GB)和同一份 Q4_K_M 权重,不是理论估算:
| 项目 | 实测结果 |
|---|---|
zllm-metal release 文件 | 8.1 MB |
| CLI 自动上下文 | 49,152 token |
| 模型加载(本地热文件缓存,多次启动) | 0.9–1.6 秒 |
文本 tg50,三次冷启动均值 | 40.47 token/s |
| 同一 115-token 回复的全段 decode | 38.1–38.5 token/s |
| 323-token 图文请求 prefill | 6.469 秒 |
| 同一图文请求 TTFT | 7.312 秒 |
下面的 23 秒短片把同一次终端流程排成更易阅读的画面:启动 zllm-metal、进行文字对话、把刚才那张本地图片路径交给模型、查看 KV 状态并退出。命令、启动数据、token 数和回答文字均取自实机终端输出;画面只做了换行和节奏编排。
这段视频验证的是从模型装载、文字对话到视觉输入的完整调用链,不是模型精度评测。速度会随模型版本、上下文、采样参数、文件缓存和机器散热状态变化。
最后的边界
zLLM 的目标不是把 Python 环境换成另一套庞大 runtime,而是让模型执行真正成为应用的一部分:一个 Rust 对象、一份模型、一个原生 backend。应用拥有自己的线程、取消、缓存和生命周期,不必把核心能力委托给另一个进程。
对 Rust 开发者来说,“给现有程序增加本地 AI”因此不再是一次服务部署,而是一次普通的库调用;对终端用户来说,它就是一个 8 MB 级原生程序,在 MacBook Air 上完成文字与图像推理。
