这是「AI 模型本地部署」系列的第一篇。读完你会有一个跑在自己机器上的推理服务:按显存选对模型和引擎,装好、起成系统服务、开机自启,并用一条 curl 验证它真的在工作。
全程用 Gemma 4 举例,但选型方法和排错思路对任何开源模型都适用。前置条件只有一个:一块 NVIDIA 显卡和能用的驱动(nvidia-smi 有输出即可)。
本地大模型部署系列(共 4 篇):① 选型与环境 → ② 对外暴露 → ③ 客户端接入 → ④ 知识库 RAG。本文是第 ① 篇。
第一步:先搞清楚你有多少显存
nvidia-smi --query-gpu=name,memory.total,compute_cap --format=csv
输出类似:
name, memory.total [MiB], compute_cap
NVIDIA GeForce RTX 4070 Laptop GPU, 8188 MiB, 8.9
三个数都有用:
- 显存决定能跑多大的模型。注意 8188 MiB 是 8 GB 卡,别按
8188/1024=7.99向下取整当成 7 GB——差一档就会选错模型。 - 算力等级(compute capability)决定能不能用 FP8。需要 ≥ 8.9(Ada / Hopper / Blackwell);A100 是 8.0,用不了。
- 还要减去已占用的显存。接了显示器的机器通常已经被占几百 MB:
nvidia-smi --query-gpu=memory.used,memory.free --format=csv
第二步:查模型的真实体积,不要估算
这一步最容易被跳过,也最容易出错。参数量算不出显存占用——量化检查点可能只量化了一部分层,实际体积比”参数量 × 位宽”大得多。
直接问 HuggingFace API 要字节数:
check_size() {
curl -s "https://huggingface.co/api/models/$1?blobs=true" | python3 -c "
import json,sys
d=json.load(sys.stdin)
w=[(s['rfilename'], s.get('size') or 0) for s in d.get('siblings',[])
if s['rfilename'].endswith(('.safetensors','.gguf'))]
main=sum(s for n,s in w if 'mmproj' not in n)
mm=sum(s for n,s in w if 'mmproj' in n)
print(f' 主体 {main/1e9:.2f} GB' + (f' + 多模态投影 {mm/1e9:.2f} GB' if mm else ''))
"
}
check_size google/gemma-4-E4B-it-qat-w4a16-ct
check_size google/gemma-4-E4B-it-qat-q4_0-gguf
结果会让你意外:
gemma-4-E4B-it-qat-w4a16-ct 主体 11.51 GB
gemma-4-E4B-it-qat-q4_0-gguf 主体 5.15 GB + 多模态投影 0.99 GB
同一个模型,两种”4bit 量化”格式差了一倍多。原因是 w4a16-ct 只量化线性层,embedding 保持 bf16;GGUF 的 q4_0 把 embedding 也量化了。踩坑篇里有完整的分析。
第三步:按显存选模型和引擎
下面这张表基于实测体积,留出了 KV cache 和计算缓冲的余量:
| 可用显存 | 模型 | 引擎 |
|---|---|---|
| ≥ 72 GB | 31B bf16 | vLLM |
| ≥ 40 GB 且算力 ≥ 8.9 | 31B FP8 | vLLM |
| ≥ 28 GB | 31B W4A16 | vLLM |
| ≥ 22 GB | 31B q4_0 GGUF | llama.cpp |
| ≥ 18 GB | 26B-A4B q4_0 GGUF | llama.cpp |
| ≥ 13 GB | 12B W4A16 | vLLM |
| ≥ 9 GB | 12B q4_0 GGUF | llama.cpp |
| ≥ 7 GB | E4B q4_0 GGUF | llama.cpp |
| ≥ 5 GB | E2B q4_0 GGUF | llama.cpp |
为什么引擎会换
这不是偏好问题,是装不装得下的问题:
- 显存宽裕用 vLLM。连续批处理和 PagedAttention 让它在并发下吞吐明显更好,适合多人共用或高频调用。
- 显存吃紧用 llama.cpp + GGUF。GGUF 把 embedding 也量化了,是小显存下唯一装得进去的格式。
好消息是两者都提供 OpenAI 兼容接口,所以上层的反向代理、客户端、文档都不用改。后面几篇讲的接入方式对两个引擎通用。
第四步:装引擎
路线 A:llama.cpp(小显存)
官方发布页对 Linux 没有提供 CUDA 预编译包,但有 Vulkan 版——在 N 卡上一样跑,还省掉几 GB 的 CUDA toolkit(驱动自带 Vulkan ICD 就够):
# 确认驱动带 Vulkan
ls /usr/share/vulkan/icd.d/nvidia_icd.json
# 取最新版本号并下载 Vulkan 构建
TAG=$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])')
curl -fL -o /tmp/llama.tar.gz \
"https://github.com/ggml-org/llama.cpp/releases/download/${TAG}/llama-${TAG}-bin-ubuntu-vulkan-x64.tar.gz"
# 解压前先验完整性——下载被截断时解压会报错,但更早发现更省事
tar tzf /tmp/llama.tar.gz >/dev/null || { echo "压缩包损坏"; exit 1; }
sudo mkdir -p /opt/llama.cpp
sudo tar xzf /tmp/llama.tar.gz -C /opt/llama.cpp --strip-components=1
确认它认得到显卡:
LD_LIBRARY_PATH=/opt/llama.cpp /opt/llama.cpp/llama-server --list-devices
Available devices:
Vulkan0: NVIDIA GeForce RTX 4070 Laptop GPU (8188 MiB, 7756 MiB free)
路线 B:vLLM(大显存)
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv /opt/gemma4/venv --python 3.12
uv pip install --python /opt/gemma4/venv/bin/python 'vllm>=0.19.0' huggingface_hub
提前说一句:vLLM 会连带装整套 CUDA 运行库,依赖树十几 GB,第一次装要有耐心。
第五步:下载权重
国内网络下这一步经常是最慢的环节。实测几个源的差距很大:
ModelScope 16.7 MB/s
hf-mirror 2.4 MB/s
HuggingFace 2.1 MB/s
优先用 ModelScope,注意路径里是 master 不是 main:
mkdir -p /opt/gemma4/models
curl -L --retry 20 --retry-delay 5 --retry-all-errors -C - \
-o /opt/gemma4/models/gemma-4-E4B_q4_0-it.gguf \
"https://modelscope.cn/models/gpustack/gemma-4-E4B-it-qat-q4_0-gguf/resolve/master/gemma-4-E4B_q4_0-it.gguf"
-C - 是断点续传,网络中断后重跑这条命令会接着下。
下完一定要核对体积——下载工具”成功”但没下全是很常见的:
ls -l /opt/gemma4/models/*.gguf | awk '{printf "%.0f MB %s\n", $5/1048576, $9}'
多模态的模型还要额外下一个投影文件(文件名通常带 mmproj),没有它就只能处理文字。
第六步:写成 systemd 服务
直接 nohup 起进程的问题是重启就没了、崩了不会拉起、日志到处飞。写成 systemd 单元一次解决:
[Unit]
Description=本地大模型推理服务
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=youruser
Environment=LD_LIBRARY_PATH=/opt/llama.cpp
ExecStart=/opt/llama.cpp/llama-server \
--model /opt/gemma4/models/gemma-4-E4B_q4_0-it.gguf \
--mmproj /opt/gemma4/models/gemma-4-E4B-it-mmproj.gguf \
--alias gemma4 \
--host 127.0.0.1 --port 8000 \
--ctx-size 131072 \
--parallel 4 \
--n-gpu-layers 99 \
--flash-attn on \
--cache-type-k q8_0 --cache-type-v q8_0 \
--jinja \
--metrics \
--no-webui \
--api-key sk-换成你自己的
Restart=on-failure
RestartSec=10
# 首次加载大模型可能要几分钟,别让 systemd 提前判死
TimeoutStartSec=1800
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/gemma4
[Install]
WantedBy=multi-user.target
逐个参数说明
这几个默认值不符合直觉,值得单独讲:
--ctx-size是总量,不是单请求上下文。它会被--parallel平分。上面写 131072 配 4 槽,每个请求实际能用 32K。如果你按”单请求”理解填了 16384 而并发是默认的 4,实际会分配 65536 的 KV cache——显存吃光后模型层被挤到 CPU,速度掉一个数量级。--parallel是并发上限。翻译类客户端会同时发几十个请求,槽位多比上下文长更重要;写代码相反。--flash-attn on对某些模型是必需的,不是可选优化。用滑动窗口注意力的模型(Gemma 4 的 E 系列就是)在 Vulkan 后端不开它,prompt 处理会慢 40 倍。--cache-type-k/v q8_0把 KV cache 压到 8bit,小显存下能让可用上下文翻倍,质量影响可忽略。--api-key必填。哪怕只监听 127.0.0.1,也别留空——后面要往外暴露时你会感谢现在的自己。
启动并设为开机自启:
sudo systemctl daemon-reload
sudo systemctl enable --now gemma4-server
第七步:验证
7.1 服务起来了吗
systemctl is-active gemma4-server
curl -s 127.0.0.1:8000/health
7.2 看它实际算出来的参数
这一步比看你写了什么更重要。日志里有引擎真正采用的值:
sudo journalctl -u gemma4-server --no-pager | grep -iE "n_slots|n_ctx|loaded"
srv load_model: initializing, n_slots = 4, n_ctx_slot = 32768
srv load_model: loaded multimodal model, '.../gemma-4-E4B-it-mmproj.gguf'
n_slots × n_ctx_slot 才是真正的 KV cache 总量。多模态模型这里会显示投影文件已加载。
7.3 显存占用符合预期吗
nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader
参考值:E4B q4_0 + mmproj + 128K 的 q8_0 KV cache,实测占 5.2 GB。如果比你估算的多很多,多半是 --ctx-size 和 --parallel 的关系没算对。
7.4 发一个真实请求
KEY=sk-换成你自己的
curl -s 127.0.0.1:8000/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"gemma4","max_tokens":2048,
"messages":[{"role":"user","content":"用一句话解释 KV cache"}]}' \
| python3 -m json.tool
如果 content 是空字符串,先别怀疑模型坏了——看看有没有 reasoning_content 字段。带思考模式的模型(Gemma 4 就是)会先输出推理再给答案,max_tokens 给小了会在思考阶段就被截断。把它调到 2048 以上再试。
7.5 测一下真实速度
第一次测不作数:Vulkan 后端的着色器是懒编译的,冷启动后第一个大 prompt 会慢一个数量级。用几百 token 的输入预热两次再测:
python3 -c "
import json
p = '请阅读以下内容并总结要点。' + '人工智能的发展经历了多个阶段。' * 20
print(json.dumps({'model':'gemma4','max_tokens':200,
'messages':[{'role':'user','content':p}]}))" > /tmp/warm.json
# 预热两次
for i in 1 2; do
curl -s -o /dev/null 127.0.0.1:8000/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' --data @/tmp/warm.json
done
# 正式测
curl -s 127.0.0.1:8000/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' --data @/tmp/warm.json \
| python3 -c "
import json,sys
t = json.load(sys.stdin).get('timings', {})
print(f\"prompt {t.get('prompt_per_second',0):.0f} t/s 生成 {t.get('predicted_per_second',0):.1f} t/s\")"
8 GB 笔记本显卡跑 E4B 的参考值:prompt 900–1700 t/s,生成 63–66 t/s。如果 prompt 处理只有几十 t/s,检查 --flash-attn 是不是没开。
常见问题
服务起不来,怎么定位
sudo journalctl -u gemma4-server -n 50 --no-pager
最常见的三类:
unrecognized arguments——引擎版本变了,某个参数被删或改名。去掉它重试。failed to load model——权重没下全,回到第五步核对体积。- 显存不足直接退出——把
--ctx-size调小,或换更小的模型。
能不能同时跑两个模型
可以,起两个 systemd 单元、监听不同端口即可。但要注意显存是共享的:两个模型的权重加 KV cache 必须都装得下。如果第二个模型只是做 embedding,让它跑 CPU 更划算——这个做法在系列第四篇讲知识库时会用到。
为什么只监听 127.0.0.1
这是有意的。现在这个服务只有 API Key 一层保护,直接监听 0.0.0.0 等于把它裸露在网络上。下一篇会讲怎么在前面加一层反向代理再暴露出去,包括限流、路径分流和 TLS。
下一步
到这里你有了一个稳定运行、开机自启的本地推理服务,但它只能从本机访问。系列第二篇会讲两种把它安全暴露到外网的方式——零端口的隧道方案和直连端口映射方案——以及它们各自适合什么场景。
如果过程中撞到了奇怪的问题,这篇踩坑记录把我实际遇到的 11 个问题按现象整理了一遍,可以对照排查。