技术笔记

系统调优 · 工具使用 · 踩坑记录

← 返回首页
大模型

在 Linux 上部署本地大模型:llama.cpp + GGUF

编译、下载、算显存、调参,以及两个最容易误判的"故障"。

2026-11 · 约 10 分钟

为什么自己编译

ollama 装起来简单,但它会自己管理模型、自己占端口、自己决定显存策略。 如果这台机器上还要跑 ComfyUI 这类吃显存的程序,两者很容易打架。

自己编译 llama.cpp 的好处:

一、先探环境,别假设

# 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 去比对,会误判文件损坏。正确的做法:

  1. stat -c %s 比对 HEAD 返回的 content-length
  2. 再独立下一次,两次哈希一致即可
  3. 最终以「llama.cpp 能加载并推理」为功能验收

四、算显存:先看模型结构,别拍脑袋

一定要去读模型的 config.json。两个坑:

  1. 多模态模型的 LLM 参数嵌在 text_config 里,顶层字段全是 None
  2. 混合架构(线性注意力 + 全注意力)的 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 + 64K64K约 1.8 GiB 宽松
IQ2_S 8.62GB + 120K120K约 468 MiB 偏紧

两者质量差异很小(同仓库评测:IQ2_S 零样本均分反而高 0.89, 但代码能力低 2.28)。长会话、Agent 场景选 IQ2_S + 大上下文更划算。

腾显存的三个手段(实测效果)

手段省下代价
降上下文 128K→120K约 250 MiB窗口小 6%
调小批大小约 190 MiBprefill 慢约 5%
视觉塔放 CPU888 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_0KV cache 量化,省一半显存
-faFlash 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

八、小结