🦙 llama.cpp 图解教程llama.cpp Visual Guide 第五部分 · 公共 API 与工具Part 5 · Public API & tools 26 / 40
第五部分 · 公共 API 与工具Part 5 · Public API & tools

common 工具层The common layer

上一课(L25)我们认识了 include/llama.h 那套 C API——它确实能驱动整台机器,可真用起来挺"啰嗦":填一个 llama_batch 要逐字段设 token/pos/seq_id;搭一条采样链要一节节 llama_sampler_chain_add;把命令行解析成参数、再把模型从网上拉下来,每个工具都得从头写一遍。这些反复出现的杂活,正是 common/ 这一层要替你包掉的。

common/ 是 llama.cpp 自带的"公共胶水层":它把上面那些零碎的 C 调用,封装成一组顺手的 C++ helper,让 llama-clillama-server 这些自带工具都站在它的肩膀上,不必各写各的样板。但有件事要先说清楚:common 不是公共 API——它没有 llama.h 那样的 ABI 稳定承诺,只是项目"内部"给自家工具用的便利库;第三方语言绑定该直接对着 llama.h 写,而不是依赖 common。

这一层到底替你包了哪些杂活?大致可分四类:把命令行解析成一个大配置,按配置把模型与上下文一次性初始化好,把采样链和语法约束包成一个顺手的采样器,以及在你只给出一个仓库名时自动下载并缓存模型文件。本课就顺着这四件事走一遍,最后再花点篇幅讲清"为什么 common 不算公共 API",以及两个天天在用、却很少被单独提起的小帮手——日志与终端着色。

🌍 宏观理解
common 夹在 llama.h(稳定地基)和各个 tool(楼上的房间)之间,是一层"只对内、不对外"的脚手架。它的全部价值就两个字:复用。把每个工具都要做的重复动作——解析参数、配采样器、下载模型、打日志——统统收拢到一处,工具的 main() 因此能瘦到几乎只剩业务逻辑。读懂 common,你就读懂了 cli/server 这些工具"短小"的秘密:不是它们做得少,而是杂活早被 common 提前包圆了。换个角度看,common 的存在让"地基"得以保持精简:凡是为了顺手、却不值得写进稳定 C API 的东西,都可以安心搁在 common 这层,将来要改也不会惊动外部用户。
🔌 生活类比
把这套分层想成一家连锁餐饮:llama.h水电管网(稳定、对所有人一致),common 是中央厨房(把洗菜、切配、调酱这些每家店都要做的预处理一次做好),各个 tool 则是门店(只管按订单出菜)。门店不必各自洗菜,中央厨房也从不直接面对顾客——它只服务自家门店。这正是 common"对内不对外"的含义:它是给自家工具用的后厨,不是开给外人的接口。厨房改了刀工、换了酱料配方,受影响的只有自家门店,不会波及马路上的顾客;可水电管网若要改规格,全城接它的人都得跟着动——这正是"对内可随时调整、对外必须稳定"的分别。

一站式配置:common_params

common 的核心,是一个大配置结构体 common_params(见 common/common.h)。模型路径、提示词、要生成多少 token、上下文开多大,还有一整块嵌套的采样设置,几乎所有"旋钮"都塞进这一个结构体里。它还自带一套合理的默认值,于是你只需覆盖真正关心的那几项,其余的留空也能跑起来。这样一来,工具不再到处传一大把零散参数,而是只围着这一个 common_params 转:解析命令行时按引用(common_params &)往里填,初始化时再把它整个取出来用——同一个对象从头贯穿到尾,省去层层透传的麻烦。

// 一个大结构体装下所有旋钮 (简化自 common/common.h)
struct common_params {
    std::string  prompt;              // -p  提示词
    int32_t      n_predict;           // -n  最多生成多少 token
    int32_t      n_ctx;               // -c  上下文窗口大小
    common_params_sampling sampling;  // 嵌套: samplers / temp / grammar
    common_params_model    model;     // model.path / model.hf_repo ...
    // ... 还有几十个字段
};

留意那两个嵌套子结构sampling(即 common_params_sampling,装着采样链顺序、温度、语法等)和 model(即 common_params_model,装着本地路径 model.path 与 HF 仓库 model.hf_repo)。把相关的旋钮收进各自的小结构体,既让 common_params 不至于变成一锅乱炖,也方便采样、下载这些模块各取所需。

有了配置,怎么把它变成跑得起来的对象?分两步。先调一次 common_init()全局初始化(起日志系统、打印 build 信息)——这步和具体模型无关,整个程序只做一次;真正干活的是 common_init_from_params(params),它接过填好的 common_params,一次性产出一个 common_init_result,里面装着加载好的 model、建好的 context、配好的 sampler(分别经 .model() / .context() / .sampler() 取用)。之所以拆成两步,是因为全局初始化必须先于任何日志输出发生,否则早期的报错可能就被悄悄吞掉。

1

common_params(填好的配置)

命令行解析后的结果:-m 模型、-p 提示词、-n 长度,还有整块采样设置,全在这一个结构体里。

2

common_init_from_params(params)

内部跑的正是 L25 那条手写序列:加载模型、建上下文、搭采样链,一口气全办了。

3

common_init_result -> {model, context, sampler}

开箱即用的三件套,经 .model() / .context() / .sampler() 取出,工具直接拿去推理。

这一步的意义,是把 L25 那条容易写错的长序列——llama_model_load_from_file 加载、llama_init_from_model 建上下文、再一节节搭采样链——整个收进了 common_init_from_params 内部。它还顺手照看了出错与释放:加载失败时返回一个空结果让你及早发现,成功时这个 common_init_result 则"持有"那几个对象、析构时一并释放,你不必再手动逐个 free。工具的 main() 因此干净许多:填好 common_params,调一次初始化,就拿到了三件套,省掉一大串样板。这也呼应了 L17 的 cparams、L14 的模型加载——那些底层步骤一个没少,只是被收进了这层胶水里。

命令行如何变成配置

common_params 里的字段是从哪来的?大多来自命令行common/arg.{h,cpp} 把每一个命令行选项都描述成一个 common_arg 对象:它记着选项的名字(可同时给 -m 短写和 --model 长写)、一段帮助文本,以及一个"拿到值就往 common_params 里写"的回调。所有 common_arg 的帮助文本还会被自动汇总,拼成你敲 --help 时看到的那张说明表,省得另写一份文档、还总忘了同步。声明时更能用链式 builder 微调行为——.set_examples() 限定它属于哪些工具、.set_env() 允许从环境变量取值、.set_excludes() 把某些工具排除在外。

// 声明 "-m / --model" 这个选项 (简化自 common/arg.cpp)
add_opt(common_arg(
    {"-m", "--model"}, "FNAME", "model path to load",
    [](common_params & params, const std::string & value) {
        params.model.path = value;          // 拿到值就写进 common_params
    }
).set_examples({LLAMA_EXAMPLE_COMMON}).set_env("LLAMA_ARG_MODEL"));

// 把整条 argv 解析进 params; 第 4 个参数选哪套工具的选项集
common_params_parse(argc, argv, params, LLAMA_EXAMPLE_CLI);

真正把命令行"灌"进结构体的,是 common_params_parse(argc, argv, params, ex):它遍历 argv,按名字找到对应的 common_arg,挨个调用回调写进 params。第 4 个参数 ex 是一个 enum llama_example(如 LLAMA_EXAMPLE_CLILLAMA_EXAMPLE_SERVERLLAMA_EXAMPLE_COMMON),它决定这次解析认哪些选项——同一套机制,因此能给不同工具暴露不同的参数子集:cli 有 cli 的选项、server 有 server 的,互不打架,而标成 COMMON 的那批则是大家共享的基本选项。若环境变量与命令行同时给了值,命令行优先——这样在 CI 里用环境变量设默认、临时在命令行覆盖,就显得很自然。

光说不够直观。下面顺着一条最小命令行 -m model.gguf -p "Hi" -n 16 --temp 0.7,看它怎么一步步变成 common_params 字段、再被交去产出三件套:

追踪一次参数解析:最小命令行如何被 common_params_parse 填进结构体、再交给 common_init_from_params 产出三件套(数值为示意)。
① argv
-m model.gguf-p "Hi"-n 16--temp 0.7
原始命令行
common_
params_parse
② common_params 字段
model.path=model.ggufprompt="Hi"n_predict=16sampling.temp=0.70
每个选项写进对应字段
common_init
from_params
③ 就绪
{model, ctx, sampler}
开箱即用的三件套

采样包装:common_sampler

采样这件事,L21 讲过它的底层是一条 llama_sampler 链,L23 又讲了 GBNF 语法约束。这两样本是分开的:链负责按概率挑词,语法负责"只准挑合法的词"。common 把它们裹成一个对象 common_sampler(见 common/sampling.{h,cpp}):采样链 + 语法捆在一起,对外只露出一个句柄,省得每个工具自己去操心"先过语法还是先采样"这类细节。common_sampler_init(model, params.sampling) 会照着 params.sampling.samplers 里列出的顺序(一串 common_sampler_type,如 COMMON_SAMPLER_TYPE_TOP_K / TOP_P / TEMPERATURE)把链一节节搭起来。

top_k
留前 k 个
->
top_p
核采样
->
temp
温度缩放
->
dist
按分布抽样
->
token
选出下一个

用起来更省事:common_sampler_sample(gsmpl, ctx, idx, grammar_first) 一次调用,就把"取 logits、过语法掩码、走完整条链、挑出 token"全办了;选完再用 common_sampler_accept(...) 把这个 token 喂回去,更新重复惩罚和语法状态。llama-clillama-server 用的都是这一层,而不是直接碰 L21 那套裸 llama_sampler_*。上图只画了默认链里的几节(真实默认还含 penalties、dry、min_p 等),但顺序的道理一样:先一层层筛掉不要的候选,最后按分布抽一个。

// cli/server 都用这层, 不直接碰裸 llama_sampler_* (简化自 common/sampling.h)
common_sampler * smpl = common_sampler_init(model, params.sampling);
// ... 每步 ...
llama_token id = common_sampler_sample(smpl, ctx, -1);   // 取分->过语法->采样, 一步到位
common_sampler_accept(smpl, id, /*is_generated=*/ true); // 反馈: 更新惩罚与语法状态

注意这是个每步都要走一遍的循环:每生成一个 token,就 common_sampler_sample 选一个、再 common_sampler_accept 反馈一次,如此往复,直到遇上结束符或写满预定长度。accept 这一步至关重要——重复惩罚要靠它记住"已经出过哪些词",语法状态也要靠它推进到下一个合法位置。参数里的 grammar_first 则控制"语法掩码"先于还是后于其它采样器生效:多数情况下用默认即可,只有当语法很严、又想让温度等先发挥作用时才需要调整。

🔬 细节 / 源码对应
common_sampler 不是另起炉灶,而是薄薄一层壳:内部仍是 L21 那条 llama_sampler 链,只是额外捆上语法、候选缓冲和一点状态管理。需要时还能用 common_sampler_get(gsmpl) 把底层那条裸 llama_sampler 链取回来。换句话说,它和 llama_sampler_* 不是二选一,而是"上层便利 + 下层内核"的关系——和 common 整体之于 llama.h 的关系如出一辙。日常使用几乎不必直接碰那条裸链,但知道它"随时能拆开"这一点,会在你要做特殊采样时留出一条后路。

下载与缓存

还有一桩重复的杂活:弄到模型文件本身。common/download.{h,cpp} 让你能直接写 -hf user/repo:tag 从 Hugging Face(一个公开的模型托管站)拉模型,省去"先手动下载、再用 -m 指路"那两步。第一步是 common_download_split_repo_tag("repo:tag"),把 repo:tag 这种写法拆成仓库名和标签两段——标签常用来挑某一种量化(如 Q4_K_M);接着按这个信息去 HF 上把文件下载下来,并在本地建一份缓存。遇到被切成多片的大模型(多个 .gguf 分卷),它也会把各片一并取齐。

-hf user/repo:tag
命令行写法
->
split_repo_tag
拆出 repo 与 tag
->
HF 下载
首次才联网
->
本地缓存
~/.cache/llama.cpp
->
model.path
当成本地文件用

这条流水线最关键的一步是先查缓存:真正联网下载前,它会先看本地是否已有这个文件、且与远端版本一致,若有就直接跳过下载。所以上图里"HF 下载"那一格,其实只有首次运行才真的会走,之后都被缓存短路掉了。

缓存是关键:模型只在第一次用到时才下载,落进本地缓存目录后,以后每次启动都直接命中、秒开。缓存默认在 ~/.cache/llama.cpp/(可用环境变量 LLAMA_CACHE 覆盖);common_list_cached_models() 能列出已经缓存的模型;下载过程则由 common_download_callback(带 on_start / on_update / on_done 三个回调,配合 common_download_progress)驱动那条进度条。这份缓存还是各工具共享的:cli、server、bench 指向同一个仓库时,命中的都是同一份本地副本,不会各下一遍。

💡 动手试试
动手试一下:第一次跑 llama-cli -hf <user>/<repo> 会看到一段下载进度,之后再跑同一个就瞬间启动——因为文件已经躺在 ~/.cache/llama.cpp/ 里了。想把缓存搬到大硬盘?设一个 LLAMA_CACHE=/path/to/dir 即可;想知道缓存了哪些,去那个目录翻一翻、或在代码里调 common_list_cached_models()。这套"下一次就免下载"的机制,正是 common 把"弄到模型"这桩杂活也一并包圆的体现:你只管写 -hf,下载、命名、缓存、复用,它都替你想好了。当然,第一次仍要联网、也得留足磁盘空间;跑通一次之后,就再不必为"模型在哪"操心了。

深入:边界与两个小帮手

最后用两个折叠,补两个常被问到的点:为什么反复强调"common 不是公共 API",以及那些不起眼、却天天在用的小工具(日志与终端)。这两点,一个关乎"边界"——你到底该依赖哪一层,一个关乎"手感"——调试时是谁在默默帮你,放在一起正好给本课收个尾。

1 为什么说 common 不算"公共 API"? 点击展开

include/llama.h 是有 ABI 稳定承诺的对外契约(L25),各语言绑定都照着它写;而 common/ 没有这种承诺——它的结构体布局、函数签名会随项目需要随时改动,因为它本就是给自家工具用的内部便利库,不在"对外保证"的范围里。所以一条实用的分界是:写第三方绑定(Python/Go/Rust)请直接对着 llama.h,把 common 当成"可以参考、但别依赖"的示例;只有当你给 llama.cpp 贡献自带工具(往 tools/examples/ 里加东西)时,才该站上 common 的肩膀。分清这条线,能帮你躲开"绑定依赖了 common,下次一更新就编不过"的坑。

一个简单的判断法:如果你的代码只要 #include "llama.h" 就够用,就别去 include common 里的头文件;只有当你在写自带工具、确实想复用参数解析或采样封装时,才把 common 一起编进来。这条线也解释了为什么本课开头一再强调它——把"对外稳定"和"对内便利"分清楚,是用好整个项目的前提。

2 log 与 console:两个不起眼的帮手 点击展开

common/log.{h,cpp} 提供分级日志LOG_INF / LOG_WRN / LOG_ERR / LOG_DBG 分别对应信息、警告、错误、调试四档,底层由一个带后台线程的 common_log 统一输出(异步打印,不拖慢主线程)。调试时把日志级别调高、多打几行 LOG_DBG,就能看清模型加载、批次、采样每一步到底发生了什么,比到处插 printf 干净得多。日志级别可用 -v 或环境变量调节,无需重新编译。

common/console.{h,cpp} 则管终端交互与着色console::set_display(...) 切换不同的显示类别(提示、用户输入、错误等各有颜色),console::readline(...) 处理一行输入(含多行与 UTF-8)。llama-cli 里那个带颜色、能正常敲中文的交互界面,就是靠它撑起来的。这两个小工具谈不上"核心",却实实在在让工具用着顺手、调着省心——也是 common"把杂活包圆"的一部分。调试交互式会话时,颜色能正常显示、退格与中文输入都不串位,靠的正是这层不起眼的封装。

✅ 关键要点
  • common/ 是 llama.cpp 自带工具的共享胶水层,把裸 C API 包成顺手的 C++;它不是公共 API(无 ABI 稳定承诺),第三方绑定应直接用 llama.h
  • common_params 一个大结构体装下所有旋钮;common_init() 做全局初始化,common_init_from_params(params) 产出 common_init_result(model + context + sampler)。
  • 命令行:每个选项是一个 common_arg(链式 .set_examples/.set_env/.set_excludes);common_params_parse(argc, argv, params, ex)enum llama_example 把 argv 填进 common_params
  • 采样:common_sampler 把 L21 的 llama_sampler 链 + L23 的 GBNF 语法裹成一个对象;common_sampler_initsamplers 顺序建链,common_sampler_sample 一步采样。cli/server 用这层、不用裸采样器。
  • 下载:-hf user/repo:tagcommon_download_split_repo_tag + HF 缓存变成本地文件;首次下载、之后命中 ~/.cache/llama.cpp/
  • 小帮手:common/log 给分级日志(LOG_INF/WRN/ERR/DBG,异步输出),common/console 管终端着色与交互输入——调试时省心不少。
💡 设计洞察
common 的设计哲学,一句话:复用胜过重写。它没有发明任何新机制,只是把每个工具都要做的重复动作——填参数、配采样、下模型、打日志——抽到一处,让 cli/server 的 main() 短到一眼能读完。但它刻意把自己伪装成对外接口:稳定契约留给 llama.h,common 只做"对内顺手"。这条"对外稳定、对内便利"的分工,和你在 L25 见到的"稳定的表面 + 自由的内部"是同一套思路——只不过这次,common 站在了"便利"的那一端。看懂这层,第五部分接下来的 cli 与 server,就只是 common 之上各搭各的楼了。把这一层吃透,你会发现后面的工具课大多是在看"如何组合 common 的零件",而不是又一套全新机制。说到底,common 教给我们的,是一种"把重复的杂活抽出来、把稳定的承诺留在边界上"的工程素养——这份在"对外稳定"与"对内便利"之间拿捏分寸的眼光,比记住任何单个函数名都更值得带走。

🧪 自测 · 想一想为什么这么设计

1. common 是 llama.cpp 公共 API 的一部分吗?
  1. 是:第三方语言绑定都应该直接依赖 common
  2. 是:它和 include/llama.h 一样是对外稳定契约
  3. 不是:它是给自家工具用的内部共享库,没有 ABI 稳定承诺
  4. 算半个:取决于编译时是否开启某个开关
看答案与解析 点击展开
答案:C。common 是工具共享的内部便利库,结构体布局与签名会随项目需要改动,没有 ABI 稳定承诺;对外稳定契约是 include/llama.h,第三方绑定应直接对着它写。
2. 谁负责把命令行 argv 变成填好的 common_params?
  1. 没有谁,main() 里手写一大堆 if-else 分支
  2. common_init():它在全局初始化时顺便解析命令行
  3. common_sampler_init:它解析所有采样相关的参数
  4. common_params_parse:按 enum llama_example 选定选项集,逐个调用 common_arg 的回调写入字段
看答案与解析 点击展开
答案:D。common_params_parse(argc, argv, params, ex) 遍历 argv、按名字匹配 common_arg、调用回调写进 common_params;第 4 个参数 ex(enum llama_example)决定这次认哪套选项。common_init() 只做全局初始化(日志、build 信息)。
3. llama-cli / llama-server 做采样时,用的是哪一层?
  1. Python 绑定里实现的采样器
  2. common_sampler:它把 L21 的 llama_sampler 链和 L23 的 GBNF 语法裹成一个对象
  3. 直接用裸 llama_sampler_*,完全不经过 common
  4. 每个工具各自手写一条独立的采样链
看答案与解析 点击展开
答案:B。cli/server 用的是 common_sampler 这层:common_sampler_init 按 params.sampling.samplers 的顺序建链,common_sampler_sample 一步完成取分、过语法、采样。需要时还能用 common_sampler_get 取回底层裸 llama_sampler 链。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 假设你要为 llama.cpp 写一个 Rust 绑定:你会依赖 common,还是只用 include/llama.h?结合 common“对内不对外”、没有 ABI 稳定承诺这两点,说说你的理由。

Last lesson (L25) we met the C API in include/llama.h - it really can drive the whole machine, but using it is rather "verbose": filling one llama_batch means setting token/pos/seq_id field by field; building a sampler chain means adding it node by node with llama_sampler_chain_add; parsing the command line into params and pulling a model off the network - every tool writes all of this from scratch. Those repeated chores are exactly what the common/ layer packages away for you.

common/ is llama.cpp's built-in "shared glue layer": it wraps those scattered C calls into a set of handy C++ helpers, so bundled tools like llama-cli and llama-server all stand on its shoulders instead of each writing their own boilerplate. But one thing must be clear up front: common is not the public API - it carries no ABI-stability promise like llama.h does, it is just an internal convenience library for the project's own tools; third-party language bindings should write against llama.h directly rather than depend on common.

So what chores does this layer actually package? Roughly four kinds: parse the command line into one big config, initialize the model and context from that config in one shot, wrap the sampler chain and grammar constraint into one handy sampler, and download and cache the model file when you hand it only a repo name. This lesson walks those four things in turn, then spends a little time making clear "why common is not the public API", plus two small helpers used every day yet rarely mentioned on their own - logging and terminal coloring.

🌍 Big picture
common sits between llama.h (the stable foundation) and each tool (the rooms upstairs), a layer of scaffolding that faces inward only, never outward. Its whole value is one word: reuse. Gather the repeated actions every tool needs - parse args, configure samplers, download models, print logs - into one place, and a tool's main() shrinks to almost nothing but business logic. Understand common and you understand the secret behind how "small" cli/server look: not that they do less, but that the chores were packaged up ahead of time by common. Seen another way, common lets the "foundation" stay lean: anything handy to have but not worth freezing into the stable C API can sit safely up here in common, free to change later without disturbing outside users.
🔌 Analogy
Picture the layering as a restaurant chain: llama.h is the plumbing and wiring (stable, identical for everyone), common is the central kitchen (it does the washing, chopping, and sauce-prep every branch would otherwise repeat), and each tool is a storefront (just plate up to order). The storefronts never wash vegetables themselves, and the central kitchen never faces customers directly - it only serves the chain's own branches. That is what common's "inward, not outward" means: it is the back kitchen for the project's own tools, not an interface opened to outsiders. If the kitchen changes its knife work or swaps a sauce recipe, only the chain's own branches feel it, never the customers on the street; but if the plumbing changes spec, everyone in the city wired to it must follow - exactly the split between "freely adjustable inward, necessarily stable outward".

One-stop config: common_params

At common's core is one big config struct, common_params (see common/common.h). The model path, the prompt, how many tokens to generate, how large the context is, plus a whole nested block of sampling settings - nearly every "knob" is packed into this one struct. It also carries a set of sane defaults, so you only override the few things you actually care about and leave the rest to run as-is. So a tool no longer passes a fistful of scattered parameters around; it just revolves around this single common_params: fill it by reference (common_params &) while parsing the command line, then pull the whole thing out at init time - one object threaded end to end, sparing you layers of pass-through.

// one big struct holds every knob (simplified from common/common.h)
struct common_params {
    std::string  prompt;              // -p  the prompt
    int32_t      n_predict;           // -n  how many tokens to generate
    int32_t      n_ctx;               // -c  context window size
    common_params_sampling sampling;  // nested: samplers / temp / grammar
    common_params_model    model;     // model.path / model.hf_repo ...
    // ... dozens more fields
};

Note the two nested sub-structs: sampling (that is common_params_sampling, holding the sampler-chain order, temperature, grammar, and so on) and model (that is common_params_model, holding the local model.path and the HF model.hf_repo). Tucking related knobs into their own little structs keeps common_params from turning into one big stew, and lets modules like sampling and download each take just what they need.

With the config in hand, how does it become runnable objects? In two steps. First call common_init() once for global init (start the logging system, print build info) - this step is model-independent and runs once per program; the real work is done by common_init_from_params(params), which takes the filled common_params and produces, in one shot, a common_init_result holding the loaded model, the built context, and the configured sampler (taken via .model() / .context() / .sampler()). The two steps are split because global init must happen before any logging, or early errors could be swallowed silently.

1

common_params (the filled config)

The result of parsing the command line: -m model, -p prompt, -n length, plus the whole sampling block, all in this one struct.

2

common_init_from_params(params)

Inside, it runs exactly L25's hand-written sequence: load the model, build the context, build the sampler chain - all at once.

3

common_init_result -> {model, context, sampler}

The ready-to-use trio, taken via .model() / .context() / .sampler(), handed straight to inference.

The point of this step is that L25's long, easy-to-misfire sequence - llama_model_load_from_file to load, llama_init_from_model to build the context, then the sampler chain node by node - is folded entirely inside common_init_from_params. It also looks after errors and cleanup along the way: on a failed load it returns an empty result so you notice early, and on success the common_init_result "owns" those objects and frees them together on destruction, so you need not free each one by hand. A tool's main() is much cleaner for it: fill common_params, call init once, and the trio is ready, sparing you a long stretch of boilerplate. This echoes L17's cparams and L14's model loading - none of those low-level steps are gone, they are just tucked into this glue layer.

How the command line becomes config

Where do the fields in common_params come from? Mostly from the command line. common/arg.{h,cpp} describes each command-line option as a common_arg object: it records the option's names (a short -m and a long --model at once), a line of help text, and a callback that "writes the value into common_params". The help text of every common_arg is also gathered automatically into the table you see when you type --help, sparing a separate document that always drifts out of sync. At declaration you can further tune behavior with chainable builders - .set_examples() limits which tools it belongs to, .set_env() lets it read from an environment variable, .set_excludes() rules certain tools out.

// declare the "-m / --model" option (simplified from common/arg.cpp)
add_opt(common_arg(
    {"-m", "--model"}, "FNAME", "model path to load",
    [](common_params & params, const std::string & value) {
        params.model.path = value;          // the value goes into common_params
    }
).set_examples({LLAMA_EXAMPLE_COMMON}).set_env("LLAMA_ARG_MODEL"));

// parse the whole argv into params; the 4th arg picks which tool's option set
common_params_parse(argc, argv, params, LLAMA_EXAMPLE_CLI);

What actually "pours" the command line into the struct is common_params_parse(argc, argv, params, ex): it walks argv, finds the matching common_arg by name, and calls each callback to write into params. The 4th argument ex is an enum llama_example (like LLAMA_EXAMPLE_CLI, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMMON) that decides which options this parse recognizes - one mechanism, so it can expose different parameter subsets to different tools: cli has cli's options, server has server's, with no clashes, while the ones marked COMMON are the shared basics everyone gets. When an environment variable and the command line both supply a value, the command line wins - so setting defaults via env in CI and overriding on the command line ad hoc feels natural.

Words alone are not concrete enough. Below we follow one minimal command line, -m model.gguf -p "Hi" -n 16 --temp 0.7, and watch it turn step by step into common_params fields, then get handed off to produce the trio:

Tracing one arg parse: how a minimal command line is filled into the struct by common_params_parse, then handed to common_init_from_params to produce the trio (values are illustrative).
(1) argv
-m model.gguf-p "Hi"-n 16--temp 0.7
raw command line
common_
params_parse
(2) common_params fields
model.path=model.ggufprompt="Hi"n_predict=16sampling.temp=0.70
each option written to its field
common_init
from_params
(3) ready
{model, ctx, sampler}
the ready-to-use trio

The sampler wrapper: common_sampler

As for sampling, L21 covered how its underlying form is a llama_sampler chain, and L23 covered GBNF grammar constraints. These two were separate by nature: the chain picks words by probability, the grammar enforces "only legal words allowed". common wraps them into one object, common_sampler (see common/sampling.{h,cpp}): the sampler chain plus the grammar bundled together, exposing only one handle outward, so each tool need not fuss over details like "apply the grammar before or after sampling". common_sampler_init(model, params.sampling) builds the chain node by node, following the order listed in params.sampling.samplers (a list of common_sampler_type, like COMMON_SAMPLER_TYPE_TOP_K / TOP_P / TEMPERATURE).

top_k
keep top k
->
top_p
nucleus
->
temp
temperature scale
->
dist
sample by dist
->
token
pick the next

Using it is tidier: common_sampler_sample(gsmpl, ctx, idx, grammar_first) does "take logits, apply the grammar mask, run the whole chain, pick a token" in one call; afterward common_sampler_accept(...) feeds the token back to update repetition penalties and grammar state. llama-cli and llama-server both use this layer, not the raw llama_sampler_* from L21. The diagram draws only a few nodes of the default chain (the real default also includes penalties, dry, min_p, and more), but the logic of the order is the same: sieve out unwanted candidates layer by layer, then draw one by distribution at the end.

// cli/server use this layer, not raw llama_sampler_* (simplified from common/sampling.h)
common_sampler * smpl = common_sampler_init(model, params.sampling);
// ... each step ...
llama_token id = common_sampler_sample(smpl, ctx, -1);   // logits->grammar->sample in one call
common_sampler_accept(smpl, id, /*is_generated=*/ true); // feedback: update penalties and grammar state

Note this is a loop you run every step: for each generated token you common_sampler_sample to pick one, then common_sampler_accept to feed it back, over and over until an end token or the planned length is reached. That accept step matters - the repetition penalty relies on it to remember "which words already appeared", and the grammar state relies on it to advance to the next legal position. The grammar_first argument controls whether the "grammar mask" applies before or after the other samplers: the default is fine most of the time, and you only adjust it when the grammar is strict yet you still want temperature and friends to act first.

🔬 Details / source
common_sampler does not start from scratch; it is a thin shell: inside is still L21's llama_sampler chain, just bundled with the grammar, a candidate buffer, and a little state management. When needed you can even call common_sampler_get(gsmpl) to get the underlying raw llama_sampler chain back. In other words, it and llama_sampler_* are not an either-or but an "upper-level convenience plus lower-level core" relationship - exactly like common as a whole relates to llama.h. In everyday use you hardly ever touch that raw chain directly, but knowing it "can be opened up any time" leaves you an escape hatch when you need some special sampling.

Download and cache

There is one more repeated chore: getting the model file itself. common/download.{h,cpp} lets you write -hf user/repo:tag to pull a model straight from Hugging Face (a public model-hosting site), sparing the two-step dance of "download by hand, then point -m at it". The first step is common_download_split_repo_tag("repo:tag"), which splits the repo:tag form into a repo name and a tag - the tag often picks a particular quantization (like Q4_K_M); then it downloads the file from HF using that info and builds a local cache. For a big model split into several shards (multiple .gguf parts) it fetches all the parts together too.

-hf user/repo:tag
command-line form
->
split_repo_tag
split repo and tag
->
HF download
network on first run
->
local cache
~/.cache/llama.cpp
->
model.path
use as a local file

The key step on this pipeline is to check the cache first: before any real network download, it looks at whether the file already exists locally and matches the remote version, and if so it skips the download entirely. So the "HF download" box above really only runs on the very first run; later runs are short-circuited by the cache.

The cache is the key: a model downloads only the first time it is used, and once it lands in the local cache directory, every later start hits it directly and opens instantly. The cache defaults to ~/.cache/llama.cpp/ (overridable via the LLAMA_CACHE environment variable); common_list_cached_models() lists the already-cached models; and the download itself is driven by common_download_callback (with three callbacks on_start / on_update / on_done, paired with common_download_progress) that powers the progress bar. This cache is also shared across tools: when cli, server, and bench point at the same repo, they all hit the same local copy rather than each downloading their own.

💡 Hands-on
Try it: the first run of llama-cli -hf <user>/<repo> shows a download progress bar, and running the same one again starts instantly - because the file already sits in ~/.cache/llama.cpp/. Want to move the cache to a bigger disk? Set LLAMA_CACHE=/path/to/dir. Want to see what is cached? Browse that directory, or call common_list_cached_models() in code. This "no download next time" mechanism is common packaging up the "get the model" chore too: you just write -hf, and downloading, naming, caching, and reuse are all handled for you. Of course the first time still needs the network and enough disk space; once it has run through once, you never worry about "where the model is" again.

Deep dive: the boundary and two small helpers

Finally, two folds for two often-asked points: why we keep stressing that "common is not the public API", and those unglamorous little utilities used every day (logging and the terminal). One is about the "boundary" - which layer you should actually depend on - and the other about the "feel" - who quietly helps you while debugging - and together they round off the lesson.

1 Why does common not count as a "public API"? click to expand

include/llama.h is the outward contract with an ABI-stability promise (L25), and every language binding writes against it; common/ carries no such promise - its struct layouts and function signatures change whenever the project needs, because it is an internal convenience library for the project's own tools, outside the "outward guarantee". So a practical dividing line is: for third-party bindings (Python/Go/Rust) write against llama.h directly and treat common as "fine to read, but do not depend on" sample code; only when you contribute a bundled tool to llama.cpp (adding something under tools/ or examples/) should you stand on common's shoulders. Drawing this line keeps you clear of the trap where "a binding depended on common, and the next update no longer compiles".

A simple test: if your code only needs #include "llama.h", do not reach for common's headers; pull common in only when you are writing a bundled tool and genuinely want to reuse the arg parsing or the sampler wrapper. This line also explains why the lesson stresses it from the start - keeping "stable outward" and "convenient inward" apart is the prerequisite for using the whole project well.

2 log and console: two unglamorous helpers click to expand

common/log.{h,cpp} provides leveled logging: LOG_INF / LOG_WRN / LOG_ERR / LOG_DBG map to info, warning, error, and debug, all emitted through a common_log backed by a worker thread (asynchronous printing, so the main thread is not slowed). When debugging, raise the log level and print a few more LOG_DBG lines, and you can see exactly what happens at each step of model loading, batching, and sampling - far cleaner than scattering printf everywhere. The log level can be tuned with -v or an environment variable, no recompile needed.

common/console.{h,cpp} handles terminal interaction and coloring: console::set_display(...) switches display categories (prompt, user input, error, each with its own color), and console::readline(...) handles a line of input (including multi-line and UTF-8). The colored, input-capable interactive interface in llama-cli is held up by exactly this. Neither tool is "core", yet both genuinely make the tools pleasant to use and easy to debug - also part of common "packaging the chores". When you debug an interactive session, the colors showing correctly and backspace and UTF-8 input not garbling all come from this unglamorous wrapper.

✅ Key points
  • common/ is the shared glue layer for llama.cpp's bundled tools, wrapping the raw C API into handy C++; it is not the public API (no ABI-stability promise), and third-party bindings should use llama.h directly.
  • common_params is one big struct holding every knob; common_init() does global init, and common_init_from_params(params) produces a common_init_result (model + context + sampler).
  • Command line: each option is a common_arg (chainable .set_examples/.set_env/.set_excludes); common_params_parse(argc, argv, params, ex) fills argv into common_params per enum llama_example.
  • Sampling: common_sampler wraps L21's llama_sampler chain plus L23's GBNF grammar into one object; common_sampler_init builds the chain in samplers order, and common_sampler_sample samples in one step. cli/server use this layer, not the raw sampler.
  • Download: -hf user/repo:tag becomes a local file via common_download_split_repo_tag + the HF cache; first run downloads, later runs hit ~/.cache/llama.cpp/.
  • Small helpers: common/log gives leveled logging (LOG_INF/WRN/ERR/DBG, async), and common/console handles terminal coloring and interactive input - a real relief when debugging.
💡 Design insight
common's design philosophy in a phrase: reuse over rewrite. It invents no new mechanism; it just lifts the actions every tool repeats - fill params, configure sampling, download models, print logs - into one place, so cli/server's main() shrinks to something you can read at a glance. But it deliberately does not disguise itself as an outward interface: the stable contract is left to llama.h, and common only does "handy on the inside". This split of "stable outward, convenient inward" is the same thinking as L25's "stable surface plus free interior" - only this time, common stands on the "convenient" end. Grasp this layer and Part 5's coming cli and server are just buildings each raised on top of common. Master it and you will find the coming tool lessons are mostly about "how to assemble common's parts", not yet another brand-new mechanism. In the end, what common teaches is a kind of engineering taste - "lift the repeated chores out, keep the stable promise at the boundary" - and that sense of where to draw the line between "stable outward" and "convenient inward" is worth more than memorizing any single function name.

🧪 Self-test - think about the design

1. Is common part of llama.cpp's public API?
  1. Yes: third-party language bindings should all depend on common directly
  2. Yes: like include/llama.h, it is a stable outward contract
  3. No: it is an internal shared library for the project's own tools, with no ABI-stability promise
  4. Half: it depends on a compile-time switch
Show answer & explanation click to expand
Answer: C. common is a tool-shared internal convenience library; its layouts and signatures change as the project needs, with no ABI-stability promise. The stable outward contract is include/llama.h, which third-party bindings should target directly.
2. Who turns the command-line argv into a filled common_params?
  1. Nobody; main() hand-writes a big pile of if-else branches
  2. common_init(): it parses the command line as part of global init
  3. common_sampler_init: it parses all sampling-related arguments
  4. common_params_parse: it picks the option set by enum llama_example and calls each common_arg callback to write fields
Show answer & explanation click to expand
Answer: D. common_params_parse(argc, argv, params, ex) walks argv, matches a common_arg by name, and calls callbacks to write into common_params; the 4th arg ex (enum llama_example) decides which option set applies. common_init() only does global init (logging, build info).
3. When llama-cli / llama-server sample, which layer do they use?
  1. the sampler implemented in the Python bindings
  2. common_sampler: it wraps L21's llama_sampler chain and L23's GBNF grammar into one object
  3. the raw llama_sampler_* directly, bypassing common entirely
  4. each tool hand-writes its own separate sampler chain
Show answer & explanation click to expand
Answer: B. cli/server use the common_sampler layer: common_sampler_init builds the chain in params.sampling.samplers order, and common_sampler_sample does logits, grammar, and sampling in one step. You can still recover the raw llama_sampler chain via common_sampler_get when needed.
💭 Open questions (no single right answer - just think or try)
  • Say you are writing a Rust binding for llama.cpp: would you depend on common, or use include/llama.h only? Argue from common being inward-facing and carrying no ABI-stability promise.