llama.cpp 是不带 Python 的本地推理引擎,C++ 写的,解压出来 60 MB 一堆 exe 加 dll,跑起来不需要装 CUDA、不需要 Python 环境、不需要联网。它的模型格式是 GGUF,一份量化文件就是一个模型。

拿 Qwen2.5 的 0.5B 指令模型(Q4_K_M 量化,462.96 MiB)在 12 核 CPU 上做实测,两段负载的最优线程数不一样:提示词处理一路涨到 12 线程,生成在 8 线程就到顶了。

60 MB 的目录,双击就能跑

llama-bench --help
llama-cli --help
llama-completion --help

目录里每个 exe 只有 9216 字节,真正的代码在旁边的 ggml.dll 和 llama.dll 里,整个目录 60 MB。这样拆的好处是 llama-bench / llama-cli / llama-server 共用同一套推理核心,换模型不用换程序。

模型文件是 491400032 字节的 GGUF,放在哪儿都行,命令行里指路径就好。它跟程序完全解耦,同一个模型能被这里的所有工具读。

四个线程档位,跑同一段基准

llama-bench 的 pp512 测的是处理 512 个 token 提示词的速度,tg128 测的是连续生成 128 个 token 的速度。前者吃并行,后者吃内存带宽:

线程数pp512tg128pp 相对 8 线程tg 相对 8 线程
2163.85 t/s54.10 t/s47%68%
4257.16 t/s72.04 t/s74%91%
8346.79 t/s79.34 t/s100%100%
12398.37 t/s72.99 t/s115%92%
2 到 8 线程的倍数2.12 倍1.47 倍——
2 到 12 线程的倍数2.43 倍1.35 倍——
12 线程比 8 线程高 14.9%低 8.0%——
8 线程那档的波动±2.44±0.81——
12 线程那档的波动±15.60±3.17——
模型体积462.96 MiB491400032 字节——
单个 exe 体积9216 字节逻辑在 dll 里——
解压后目录体积60 MB含全部 dll——

从 2 线程加到 8 线程,提示词处理快 2.12 倍,生成快 1.47 倍。继续加到 12 线程,提示词处理还能再涨到 398.37 t/s,生成却从 79.34 跌回 72.99。

绝对值会随机器负载浮动。同一台机器上跑了三轮,8 线程的 pp512 落在 346.79 到 404.39 之间,tg128 落在 79.34 到 79.88 之间。生成这一列很稳,提示词那一列的波动接近 15%,做横向对比时只看趋势,别拿单次数字当基准。

两段负载的最优线程数不一样

提示词处理是批量的矩阵乘法,512 个 token 一起算,多开线程就有更多活可以分,所以 12 线程的 398.37 t/s 比 8 线程的 346.79 高 14.9%。

生成阶段完全相反:每步只算一个 token,矩阵乘法的并行度很低,真正的瓶颈是内存带宽。12 个线程同时在等同一份权重从内存读进来,调度和缓存同步的开销反而成了负担,于是 79.34 掉到 72.99,少了 8.0%。

误差列也能看出来:8 线程那档 pp512 是 346.79 ± 2.44,很稳;12 线程是 398.37 ± 15.60,波动大了六倍,说明满核调度不稳。

实用的做法按用途分。-t 8 适合长文生成和交互对话,这时候生成速度决定体验;-t 12 适合一次性吞长文档做摘要,提示词处理占大头,能多吃 14.9% 的吞吐。

一次问答的完整时间线

llama-completion -m model.gguf -t 8 -n 128 -p "Explain what a disk cache is in one sentence." --no-warmup -no-cnv
load time =     79.91 ms
prompt eval time =     79.78 ms /    11 tokens (   7.25 ms per token,   137.89 tokens per second)
       eval time =   1638.27 ms /   127 runs   (  12.90 ms per token,    77.52 tokens per second)
      total time =   1748.81 ms /   138 tokens
阶段耗时处理量速度
模型加载79.91 ms462.96 MiB—
提示词处理79.78 ms11 个 token137.89 t/s
生成1638.27 ms127 个 token77.52 t/s
合计1748.81 ms138 个 token—
整条命令墙钟(三次取最小)1.19 秒——
启动与初始化开销0.61 秒——

这条命令跑了三次,墙钟分别是 2.56、2.34、1.19 秒。差异来自生成的 token 数不固定:模型自己决定在哪里停下来,同样 -n 128 上限,有时输出 41 个 token 就收尾,有时写到 127 个才停。

所以判断速度要看内部计时的 77.52 t/s,别用墙钟除 token 数。这个数字和 llama-bench 那档 8 线程的 79.34 t/s 对得上,两套工具测的是同一个东西。

墙钟和 total time 的差值约 0.61 秒,是进程启动、加载 dll、初始化后端的固定成本。要连着问很多次,用 llama-server 把模型常驻内存,这 0.61 秒就省下来了。

0.5B 能干什么,不能干什么

71.46 t/s 的生成速度比人阅读快好几倍,输出来是一串串往外冒的。这个速度来自模型足够小:630.17M 参数、462.96 MiB 的量化文件。

换成 7B 模型(Q4 大约 4.4 GB),同规格的 CPU 上生成速度会掉到个位数 t/s,提示词处理也一起变慢。本地跑模型的现实是:速度基本和参数量成反比,想要更好的回答质量就得接受更长的等待。

0.5B 这类尺寸适合做格式转换、简单分类、关键词提取这些任务,写长文和推理链条它撑不住。选型的时候先想清楚要它干什么,再决定参数规模。

两种会直接退出

模型文件不完整的时候,llama.cpp 不会给一句「文件损坏」,而是把内部张量名打出来:

llama_model_load: error loading model: tensor 'token_embd.weight' data is not within the file bounds, model is corrupted or incomplete
llama_model_load_from_file_impl: failed to load model
llama_bench: error: failed to load model 'model.gguf'

模型文件字节数不对会报错退出,一个结果都不打印。这个报错在下载中断、磁盘写满时都会出现,判断依据是文件字节数对不对,不是看有没有报错文字。benchmark 表头照样打印,底下是空的。

写错参数名会报错退出,一个 token 都不生成。旧版本上的 -no-cnv 拿到新版上用就是这样:

error: invalid argument: -no-cnv

退出码 1,提示只有这一行。命令行行为在版本之间会变,脚本里写死参数名的话,升级前先跑一次 --help 核对。

不用 GPU 也能用

这次全程没有显卡参与,走的是 CPU 后端(版本日志里的 ggml-cpu-alderlake.dll 就是针对这代 CPU 指令集编的)。同一套 exe 换到有显卡的机器上,加 -ngl 参数把层卸载到 GPU 能再快一个量级,但那是另一套测法。

没有显卡的机器上,它的价值是把「本地跑模型」的门槛降到解压即用:60 MB 的目录里 12 个 exe 共用一套 dll,不用装 Python 依赖,不用配环境变量,换模型只换那个 gguf 文件。