上一课(L25)我们认识了 include/llama.h 那套 C API——它确实能驱动整台机器,可真用起来挺"啰嗦":填一个 llama_batch 要逐字段设 token/pos/seq_id;搭一条采样链要一节节 llama_sampler_chain_add;把命令行解析成参数、再把模型从网上拉下来,每个工具都得从头写一遍。这些反复出现的杂活,正是 common/ 这一层要替你包掉的。
common/ 是 llama.cpp 自带的"公共胶水层":它把上面那些零碎的 C 调用,封装成一组顺手的 C++ helper,让 llama-cli、llama-server 这些自带工具都站在它的肩膀上,不必各写各的样板。但有件事要先说清楚:common 不是公共 API——它没有 llama.h 那样的 ABI 稳定承诺,只是项目"内部"给自家工具用的便利库;第三方语言绑定该直接对着 llama.h 写,而不是依赖 common。
这一层到底替你包了哪些杂活?大致可分四类:把命令行解析成一个大配置,按配置把模型与上下文一次性初始化好,把采样链和语法约束包成一个顺手的采样器,以及在你只给出一个仓库名时自动下载并缓存模型文件。本课就顺着这四件事走一遍,最后再花点篇幅讲清"为什么 common 不算公共 API",以及两个天天在用、却很少被单独提起的小帮手——日志与终端着色。
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() 取用)。之所以拆成两步,是因为全局初始化必须先于任何日志输出发生,否则早期的报错可能就被悄悄吞掉。
命令行解析后的结果:-m 模型、-p 提示词、-n 长度,还有整块采样设置,全在这一个结构体里。
内部跑的正是 L25 那条手写序列:加载模型、建上下文、搭采样链,一口气全办了。
开箱即用的三件套,经 .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_CLI、LLAMA_EXAMPLE_SERVER、LLAMA_EXAMPLE_COMMON),它决定这次解析认哪些选项——同一套机制,因此能给不同工具暴露不同的参数子集:cli 有 cli 的选项、server 有 server 的,互不打架,而标成 COMMON 的那批则是大家共享的基本选项。若环境变量与命令行同时给了值,命令行优先——这样在 CI 里用环境变量设默认、临时在命令行覆盖,就显得很自然。
光说不够直观。下面顺着一条最小命令行 -m model.gguf -p "Hi" -n 16 --temp 0.7,看它怎么一步步变成 common_params 字段、再被交去产出三件套:
采样这件事,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)把链一节节搭起来。
用起来更省事:common_sampler_sample(gsmpl, ctx, idx, grammar_first) 一次调用,就把"取 logits、过语法掩码、走完整条链、挑出 token"全办了;选完再用 common_sampler_accept(...) 把这个 token 喂回去,更新重复惩罚和语法状态。llama-cli、llama-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/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 下载"那一格,其实只有首次运行才真的会走,之后都被缓存短路掉了。
缓存是关键:模型只在第一次用到时才下载,落进本地缓存目录后,以后每次启动都直接命中、秒开。缓存默认在 ~/.cache/llama.cpp/(可用环境变量 LLAMA_CACHE 覆盖);common_list_cached_models() 能列出已经缓存的模型;下载过程则由 common_download_callback(带 on_start / on_update / on_done 三个回调,配合 common_download_progress)驱动那条进度条。这份缓存还是各工具共享的:cli、server、bench 指向同一个仓库时,命中的都是同一份本地副本,不会各下一遍。
最后用两个折叠,补两个常被问到的点:为什么反复强调"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 一起编进来。这条线也解释了为什么本课开头一再强调它——把"对外稳定"和"对内便利"分清楚,是用好整个项目的前提。
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"把杂活包圆"的一部分。调试交互式会话时,颜色能正常显示、退格与中文输入都不串位,靠的正是这层不起眼的封装。
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.
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.
The result of parsing the command line: -m model, -p prompt, -n length, plus the whole sampling block, all in this one struct.
Inside, it runs exactly L25's hand-written sequence: load the model, build the context, build the sampler chain - all at once.
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.
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:
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).
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.
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.
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.
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.
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.
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.