本地部署大模型第一步:按显存选对模型和推理引擎并跑起来
本地部署大模型第一步:按显存选对模型和推理引擎并跑起来
站内搜索
直接问 AI

本地部署大模型第一步:按显存选对模型和推理引擎并跑起来

这是「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 个问题按现象整理了一遍,可以对照排查。

发表回复

向下探索