从 Ollama 到 llama.cpp:一次“降一层“的本地推理探索

副标题: 同一台电脑、同一个模型------换掉推理引擎之后,性能差了多少?


一、引子:Ollama 很好,但它是个"黑盒"

三天前,我们用 Ollama 完成了 Qwen3-8B 的本地部署。过程并不轻松------CUDA 版本兼容问题、Vulkan 回退 segfault、显存调优------但最终跑起来了。

Ollama 做了它该做的事:

复制代码
我:ollama pull qwen3:8b
Ollama:好的,拉取完成,创建模型,启动 HTTP 服务
我:curl localhost:11434/api/generate
Ollama:好的,返回推理结果

一切都很简单。但它同时也藏了很多细节:

  • 模型在 GPU 的哪几层跑? ------Ollama 决定,你不知道
  • KV Cache 用了多大? ------Ollama 决定,你不知道
  • Flash Attention 开了没有? ------Ollama 决定,你不知道

对于"开箱即用"来说,这没问题。但对于"我想知道我到底在用什么东西跑推理"来说,这就有点难受了。

这就是 llama.cpp 的切入点。


二、llama.cpp 是什么

Ollama 和 llama.cpp 的关系:

复制代码
你写的代码
    │
    ▼
┌──────────────┐
│   Ollama     │  ← Go 写的进程管理 + HTTP 路由
│  (约 50MB)   │     帮你拉模型、切模型、保活
├──────────────┤
│  llama.cpp   │  ← C++ 写的推理引擎(真正干活的部分)
│  (约 200KB)  │     加载 GGUF、跑 CUDA kernel、输出 token
└──────────────┘
    │
    ▼
    GPU

Ollama = llama.cpp + 封装。 封装带来了便利,也带来了额外开销。

llama.cpp 本身是一个 C++ 推理库,直接加载 GGUF 文件、直接调用 CUDA。没有中间层。它提供 llama-cli(命令行推理)和 llama-server(HTTP API 服务)两个入口。

本文的目标: 用同一台电脑(GTX 1660 Ti · 6GB VRAM)、同一个模型(Qwen3-8B Q4_K_M)、在 llama.cpp 上跑一遍,然后跟 Ollama 的结果做对比------看看到底差了多少。


三、编译踩坑:GCC 8 的 filesystem 之痛

如果你想跳过编译部分直接看性能对比,可以跳到第四节。但这个故事我觉得值得记下来。

llama.cpp 的编译本身很简单:

bash 复制代码
git clone https://github.com/ggerganov/llama.cpp.git
cmake -B build -DGGML_CUDA=ON
cmake --build build -j12

但第一次编译完跑 llama-cli,立刻 segfault。

GDB 看一下:

复制代码
#0  std::filesystem::path::~path()
#1  ggml_backend_load_best()

问题出在 std::filesystem 上。我的系统 GCC 是 8.4------C++17 的 filesystem 在 GCC 8 里还在"实验性"阶段,需要额外链接 -lstdc++fs。而 llama.cpp 的 CMake 没有处理这个情况。

换 GCC 9 重编,问题解决。

复制代码
build: 7152 (d414db02) with gcc-9 (Ubuntu 9.4.0) 9.4.0 for x86_64-linux-gnu

小小的教训: 玩 C++ 项目,GCC 版本别太老。llama.cpp 这种紧跟标准的项目,GCC 9+ 是底线。


四、控制变量法:同一模型,两种引擎

实验设计:

控制变量 取值
模型 Qwen3-8B Q4_K_M(4.7GB)
硬件 GTX 1660 Ti 6GB + 15GB RAM
Prompt "What is machine learning?"(生成 128 tokens)
上下文 4096
GPU offload 全部层(-ngl 36)

为了保证公平,Ollama 也用同一个本地 GGUF 文件创建模型(而非从 Ollama 仓库拉取)。

复制代码
# Ollama 侧:
ollama create benchmark-qwen3 -f Modelfile
# Modelfile 内容就一行:
FROM /home/.../qwen3-8b-q4_k_m.gguf

结果

复制代码
                    llama.cpp           Ollama          差距
                    ────────────        ────────────    ─────────
Prompt eval 速度    144.1 tok/s         56.3 tok/s      ×2.6
生成速度 (128 tok)   22.1 tok/s         16.6 tok/s      +33%
VRAM 占用           5,336 MiB           ~5,000 MiB     接近
Flash Attention     自动开启            默认关闭         ---

Prompt eval 差了 2.6 倍------这是两引擎最本质的区别。Prompt eval 阶段(把用户输入变成模型能理解的内部表示)计算密集但不需要逐 token 输出,这时候 llama.cpp 的零开销架构优势明显。

生成速度差 33%------生成阶段算力瓶颈在 GPU 本身,引擎开销占比小,所以差距缩小了。

VRAM 占用几乎一样------模型本身 4.7GB 占大头,引擎的额外开销可以忽略。


五、实验一:-ngl 分档调优------GPU 给多少,性能长多少?

llama.cpp 最有用的参数之一就是 -ngl(number of GPU layers)。在 Ollama 里你没法控制它,但在 llama.cpp 里你可以精确到"只 offload 9 层到 GPU"。

我们测试了 Qwen3-8B(36 层)在 5 个档位的表现:

复制代码
-ngl 分档性能曲线(Qwen3-8B Q4_K_M · GTX 1660 Ti)
═══════════════════════════════════════════════════

 ngl  │  GPU 层数 │ Prompt 512    │ 生成 128 tok  │ VRAM 占用
──────┼───────────┼───────────────┼───────────────┼──────────
   0  │     0%    │  116.9 tok/s  │    3.9 tok/s  │    ~0
   9  │    25%    │  122.1 tok/s  │    5.0 tok/s  │  ~1.3GB
  18  │    50%    │  129.1 tok/s  │    6.4 tok/s  │  ~2.6GB
  27  │    75%    │  136.6 tok/s  │   10.1 tok/s  │  ~3.9GB
  36  │   100%    │  144.7 tok/s  │   21.5 tok/s  │  5.3GB

这条曲线说明了什么?

Prompt eval(处理用户输入) 随 GPU 层数增加稳步提升:116 → 145,+24%。这是因为 prompt eval 可以并行计算,每多一层在 GPU 上都直接加速。

生成速度 则是一根"拐棍曲线":从 0 到 18 层,只从 3.9 涨到 6.4 tok/s(慢如蜗牛);但到 27 层突然跳到 10.1,再到 36 层的 21.5------翻了 5.5 倍

为什么会有这个拐点?因为生成是串行的------每生成一个 token,都要经过所有层。当大部分层在 CPU 上时,每步都要等 CPU-GPU 数据传输,这个开销非常大。只有足够多的层在 GPU 上时(大概 75%+),带宽瓶颈才被突破。

实际意义: 如果你的显卡显存不够装下整个模型,用 -ngl 放 75% 的层到 GPU 就能获得接近全 GPU 的体验。比如 Q5_K_M(5.5GB)装不进 6GB 显存,但 -ngl 27 就能跑,速度还有 10 tok/s。


六、实验二:Flash Attention------长上下文的"免费午餐"

Flash Attention 是一种不用存储完整注意力矩阵的算法改进。llama.cpp 支持 --flash-attn 开关。

我们对比了开启和关闭 Flash Attention 时,不同上下文长度下的性能:

复制代码
Flash Attention on/off 对比(Qwen3-8B · -ngl 36)
═══════════════════════════════════════════════════════

                 Flash Attn OFF    Flash Attn ON    加速比
                 ─────────────    ─────────────    ─────
pp512  (短输入)    144.1 tok/s     151.5 tok/s      +5%
pp4096 (长输入)    112.1 tok/s     131.9 tok/s     +18%
tg128  (生成)       21.5 tok/s      21.8 tok/s      +1%

有意思的两个发现:

① 短输入时 Flash Attention 几乎没区别(+5%)。 因为 512 token 的注意力矩阵只有 512×512 ≈ 0.25M 个元素,计算量太小,优化的空间有限。

② 长输入时效果显著(+18%)。 4096 token 的注意力矩阵是 4096×4096 ≈ 16M 个元素------64 倍于短输入。Flash Attention 避免了存储这个巨大矩阵,内存带宽压力骤降。

③ 生成速度不变(+1%)。 生成阶段每步只处理 1 个新 token,注意力计算是 O(n) 的,Flash Attention 的优势发挥不出来。

实际意义: 如果你的应用需要长上下文(比如分析整份文档、多轮对话),务必开启 Flash Attention 。它完全免费------不影响输出质量,只改变计算方式。在 llama.cpp 里默认开启,Ollama 则需要设置 OLLAMA_FLASH_ATTENTION=1


七、升级模型:DeepSeek-R1-Distill-Qwen-7B

既然 llama.cpp 已经跑通了,我们换一个更强一点的模型试试。

DeepSeek-R1-Distill-Qwen-7B是 DeepSeek-R1(671B MoE)蒸馏到 Qwen-7B 架构的版本。参数少但推理能力强。

llama.cpp 上两个模型的对比

复制代码
                     Qwen3-8B         DeepSeek-R1-Distill-7B
                     8.19B params     7.62B params
                     36 layers        28 layers
                     ─────────────    ─────────────────────
Prompt 512           144.1 tok/s      160.0 tok/s
Prompt 2048          128.7 tok/s      144.5 tok/s
生成 128 tok         22.1 tok/s       42.7 tok/s         ← ×1.9
VRAM                 5,336 MiB        4,696 MiB

DeepSeek 的生成速度是 Qwen3 的 1.9 倍。原因简单:层数少(28 vs 36),每步计算量小。而且 VRAM 只用了 4.7GB,比 Qwen3 的 5.3GB 多留出了 600MB 给 KV cache。

说明一件事: 72B 的模型"质感"可以通过蒸馏压缩到 7B 的"速度"里。虽然是 7B 级的参数,推理能力确实比同体量的通用模型强一截。


八、API 延迟对比:不只是快,是可控

除了命令行,llama.cpp 也提供 HTTP API 服务(llama-server),暴露 OpenAI 兼容的 /v1/chat/completions 接口。

复制代码
llama-server 启动:
  ./build/bin/llama-server \
    -m qwen3-8b-q4_k_m.gguf \
    -ngl 36 \
    --port 8080 \
    --flash-attn 1

访问:
  curl http://localhost:8080/v1/chat/completions \
    -d '{"messages":[{"role":"user","content":"你好"}]}'

API 延迟对比(同一 prompt,生成 128 tok):

复制代码
┌──────────────────────┬──────────────┬──────────────┬──────┐
│                      │  llama-server │    Ollama    │ 差距 │
├──────────────────────┼──────────────┼──────────────┼──────┤
│ Generation 速度       │   20.5 tok/s │   16.6 tok/s │+23% │
│ 总耗时               │      6.50s   │      8.07s   │+24% │
│ API 协议             │  OpenAI 兼容  │    自定义    │     │
└──────────────────────┴──────────────┴──────────────┘──────┘

多出 23% 的吞吐。但我觉得 llama-server 更大的优势不是快,而是可控

  • 可以用 -ngl N 精确控制多少层在 GPU
  • 可以用 -c 32768 设置上下文长度(Ollama 默认 2048)
  • 可以用 --flash-attn 1 开关 Flash Attention
  • 可以实时看每层的显存占用、KV cache 大小

九、什么时候该用哪个?

在跑完这组测试后,我的个人结论是:

选 Ollama:

  • 你需要多模型切换ollama run model1 然后 ollama run model2
  • 你不想碰编译
  • 你只需要一个快速的 API 服务

选 llama.cpp:

  • 你想压榨性能(多 23% 的生成速度,2.6 倍的 prompt eval)
  • 你需要精细控制(每层 offload、KV cache 大小、量化策略)
  • 你想做调优实验(不同 -ngl 的对比、Flash Attention 开关的影响)
  • 你是开发者,想把推理引擎嵌入自己的应用

选哪个模型:

  • 日常聊天、工具调用 → Qwen3-8B(生态成熟、Chat Template 完整)
  • 推理、编程、数学 → DeepSeek-R1-Distill-Qwen-7B(R1 蒸馏增益明显)

十、总结

从 Ollama "降一层"到 llama.cpp,我们得到了:

  1. 性能提升 20-30%------对于 6GB 显存的入门级显卡,这相当于"刚好能用"和"比较流畅"的区别
  2. 全栈可控------每一层在哪跑、KV cache 多大、Flash Attention 开不开,自己说了算
  3. 对引擎的理解------拆掉 Ollama 的封装后,原来推理引擎本身不复杂,复杂的是工程化封装

Ollama 和 llama.cpp 不是"谁替代谁"的关系。它们本质上是一个东西的不同抽象层级:

复制代码
硬件 → llama.cpp → Ollama → 用户
        裸引擎      开箱即用

选哪个,取决于你想站在这条链的哪一端。


附:本文所有数据均在同一硬件上实测获得

硬件 参数
GPU NVIDIA GeForce GTX 1660 Ti 6GB
驱动 570.133.07 / CUDA 12.8
CPU 12 核
内存 15GB
llama.cpp commit d414db02, build 7152
Ollama v0.30.11

测试命令:

bash 复制代码
# llama-bench(自动跑多次取平均)
./build/bin/llama-bench \
  -m /models/qwen3-8b-q4_k_m.gguf \
  -m /models/DeepSeek-R1-Distill-Qwen-7B-Q4_K_M.gguf \
  -ngl 0,36 -p 512,2048 -n 128 -r 2

# Ollama API 测试
curl -X POST http://localhost:11434/api/generate \
  -d '{"model":"benchmark-qwen3","prompt":"...","stream":false,...

系列全篇:

  1. 从零到一:用 AI Agent 辅助在 6GB 显卡上本地部署大模型实战--- 部署全流程
  2. 只有 B 级模型,怎么干出 A 级的活? --- 任务拆解方法论
  3. Agent 不是更聪明的模型,而是长了手脚的模型 --- Agent 能力框架