在 Linux 上部署本地大模型:llama.cpp + GGUF
编译、下载、算显存、调参,以及两个最容易误判的"故障"。
为什么自己编译
ollama 装起来简单,但它会自己管理模型、自己占端口、自己决定显存策略。 如果这台机器上还要跑 ComfyUI 这类吃显存的程序,两者很容易打架。
自己编译 llama.cpp 的好处:
- 装在用户目录下,不污染系统
- 参数完全可控(量化档、上下文、批大小、显存分配)
- 能和别的 GPU 程序共存,随时停掉腾显存
- 暴露 OpenAI 兼容接口,各种客户端直接接
一、先探环境,别假设
# OS / 内存 / 核数
cat /etc/os-release | grep -E "^(NAME|VERSION)="; uname -r; free -h | head -2; nproc
# 编译工具
for c in gcc g++ cmake git make curl; do
printf "%-8s " "$c"; command -v $c >/dev/null && $c --version 2>/dev/null | head -1 || echo "缺失"
done
# ★ CUDA:不要只看 nvcc 在不在 PATH,要看装了什么包
ls -d /usr/local/cuda* 2>/dev/null
dpkg -l | grep -iE "cuda-toolkit|cuda-nvcc" | awk '{print $2, $3}'
# 显卡
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
# 磁盘(模型动辄 10~30GB)
df -h / $HOME
常见误判:Ubuntu 上经常出现「nvcc不在 PATH,但cuda-toolkit其实已经装全」的情况。/usr/local/cuda-13.0/bin/nvcc就在那儿,不需要重装 CUDA, 编译时 export 一下 PATH 就行。先dpkg -l | grep cuda确认,能省 3GB 下载。
二、编译
关键是 CMAKE_CUDA_ARCHITECTURES 要对上显卡:
| 显卡 | 算力代号 |
|---|---|
| RTX 20 系(Turing) | 75 |
| RTX 30 系(Ampere) | 86 |
| RTX 40 系(Ada) | 89 |
| RTX 50 系(Blackwell) | 120 |
| 不确定 | native(编译机必须就是运行机) |
git clone --depth 1 https://github.com/ggml-org/llama.cpp ~/llama.cpp
cd ~/llama.cpp
export PATH=/usr/local/cuda-13.0/bin:$PATH # 按实际版本改
export CUDACXX=/usr/local/cuda-13.0/bin/nvcc
cmake -B build \
-DGGML_CUDA=ON \
-DCMAKE_CUDA_ARCHITECTURES=89 \
-DCMAKE_BUILD_TYPE=Release \
-DLLAMA_CURL=ON
cmake --build build --config Release -j$(nproc)
16 核机器约 3~4 分钟。CUDA 目标文件很多,放后台跑,别卡在前台等超时。
验证:
export LD_LIBRARY_PATH=~/llama.cpp/build/bin:$LD_LIBRARY_PATH
~/llama.cpp/build/bin/llama-cli --list-devices
# 必须看到:Available devices: CUDA0: <显卡名>
看不到 CUDA0 就说明编译时没找到 CUDA,回头查 CUDACXX。
三、下载模型:国内网络怎么搞
这是最花时间的一步。实测了几种方案:
| 方案 | 结果 |
|---|---|
curl 直连 huggingface.co | 约 1.0 MB/s,45 秒就断流 |
hf download 直连 | 能跑但慢,还会报 LocalEntryNotFoundError |
hf download + hf-mirror 镜像 | 第一次下 2.8G 后断流,之后 HEAD 请求稳定失败 |
开启 HF_HUB_ENABLE_HF_TRANSFER | 救不了,同样报错 |
| aria2 直连官方 resolve 链接 | ✅ 11 MiB/s,全程无错 |
aria2c -x 16 -s 16 -k 1M -c --retry-wait=5 -m 0 \
--console-log-level=warn --summary-interval=10 \
-d ~/models/<模型名> -o <文件名>.gguf \
"https://huggingface.co/<repo>/resolve/main/<文件名>.gguf"
-c 断点续传,-m 0 无限重试。放后台跑,断了自己接上。
镜像为什么不行:它对 HEAD 请求返回 308 跳回 huggingface.co,
huggingface_hub 拿到这个跨域跳转后判定
「资源不在 huggingface.co 上」直接报错。而用 curl 单独测又是正常的,
很容易误判成网络问题。
校验的陷阱
新仓库走 HuggingFace 的 Xet 存储(下载会重定向到 *.cdn.hf.co),
此时 HTTP etag 是 Xet 内容哈希,不是 sha256。
拿 etag 当 sha256 去比对,会误判文件损坏。正确的做法:
stat -c %s比对 HEAD 返回的content-length- 再独立下一次,两次哈希一致即可
- 最终以「llama.cpp 能加载并推理」为功能验收
四、算显存:先看模型结构,别拍脑袋
一定要去读模型的 config.json。两个坑:
- 多模态模型的 LLM 参数嵌在
text_config里,顶层字段全是 None - 混合架构(线性注意力 + 全注意力)的 KV cache 只算全注意力层, 按总层数算会高估 3~4 倍,白白砍掉上下文
curl -s "https://huggingface.co/<base_repo>/raw/main/config.json" \
| python3 -c '
import sys, json
c = json.load(sys.stdin)
t = c.get("text_config", c)
for k in ["num_hidden_layers","hidden_size","num_attention_heads",
"num_key_value_heads","head_dim","max_position_embeddings",
"full_attention_interval","layer_types"]:
v = t.get(k)
print(k, "=", f"list(len={len(v)})" if isinstance(v, list) else v)'
KV cache 的计算:
全注意力层数 L = 有 layer_types 就数 "full_attention" 的个数
否则 L = num_hidden_layers
每 token KV 字节 = L × 2(K,V) × num_key_value_heads × head_dim × 2(f16)
KV 总量 = 每 token × 上下文长度
显存总账 = 模型权重 + mmproj(多模态视觉塔) + KV cache + 缓冲(1~1.5GB)
留 1.5~2GB 余量,别把 16GB 卡算到 15.5GB。长输入时会 OOM。
五、量化档与上下文怎么配
16GB 卡的实测组合:
| 组合 | 可达上下文 | 余量 |
|---|---|---|
| IQ3_XXS 9.40GB + 64K | 64K | 约 1.8 GiB 宽松 |
| IQ2_S 8.62GB + 120K | 120K | 约 468 MiB 偏紧 |
两者质量差异很小(同仓库评测:IQ2_S 零样本均分反而高 0.89, 但代码能力低 2.28)。长会话、Agent 场景选 IQ2_S + 大上下文更划算。
腾显存的三个手段(实测效果)
| 手段 | 省下 | 代价 |
|---|---|---|
| 降上下文 128K→120K | 约 250 MiB | 窗口小 6% |
| 调小批大小 | 约 190 MiB | prefill 慢约 5% |
| 视觉塔放 CPU | 888 MiB | 图片编码 2.8s → 50s(18 倍) |
第三条看着诱人,但一旦发图就卡死 50 秒,除非完全不发图,否则别用。
上下文没生效?
不同客户端读的地方不一样,这几个都要对:
# 服务端实际设置
curl -s http://127.0.0.1:8080/props | \
python3 -c 'import sys,json;print(json.load(sys.stdin)["default_generation_settings"]["n_ctx"])'
# OpenAI 兼容客户端读的
curl -s http://127.0.0.1:8080/v1/models | \
python3 -c 'import sys,json;m=json.load(sys.stdin)["data"][0]["meta"];print(m["n_ctx"], m["n_ctx_train"])'
# 日志里启动时的 slot 配置
grep "n_ctx_slot" ~/<模型名>.log
注意 n_ctx 是你设的值,n_ctx_train 是模型原生窗口。
客户端报「窗口小于真实值」时,通常是读了前者。
六、写启停脚本,别裸跑
直接 llama-server 裸跑,重启机器就没了,参数也容易记错。写个脚本:
#!/bin/sh
# start-llm.sh
export LD_LIBRARY_PATH=~/llama.cpp/build/bin:$LD_LIBRARY_PATH
exec ~/llama.cpp/build/bin/llama-server \
-m ~/models/<模型>.gguf \
--mmproj ~/models/<mmproj>.gguf \
--host 0.0.0.0 --port 8080 \
-c 120000 \
-ngl 99 \
-b 1024 -ub 256 \
--cache-type-k q8_0 --cache-type-v q8_0 \
-fa \
--jinja
| 参数 | 作用 |
|---|---|
-c | 上下文长度 |
-ngl 99 | 全部层放 GPU |
-b / -ub | 批大小 / 微批大小,影响显存和 prefill 速度 |
--cache-type-k/v q8_0 | KV cache 量化,省一半显存 |
-fa | Flash Attention,省显存提速 |
--jinja | 用模型的对话模板(思考模型必须) |
七、两个极易误判的"故障"
首次请求要等几十秒——这是预热,不是性能差
服务刚起来,第一次请求会卡 20~60 秒。这不是模型太慢, 而是 llama.cpp 在自动预热(跑一遍计算图、分配缓存)。
预热完之后就正常了。判断方法:连发两次同样的请求,第二次如果秒回,就是预热。
压测"崩溃"可能是别的进程在重启服务
有次压测到一半服务挂了,日志看着像 OOM。查了半天发现是机器上另一个程序 (定时任务)在重启这个服务。
排查顺序:
# 1. 看服务是不是被重启了(启动时间对不上)
systemctl status <服务名> | head -5
# 2. 看是不是 OOM
dmesg | grep -i "killed process"
# 3. 看显存是不是被别人占了
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv
八、小结
- 环境检查别假设:nvcc 不在 PATH ≠ 没装 CUDA
- 下载用 aria2 直连官方,别折腾镜像
- 算显存先读 config.json,混合架构只算全注意力层
- 要更长上下文,优先降量化档,别降 KV 精度
- 首次请求慢是预热,不是性能问题
- 装在家目录、写启停脚本,别污染系统、别裸跑