🦙 llama.cpp 图解教程llama.cpp Visual Guide 第八部分 · 实战与贡献Part 8 · Practice & contributing 39 / 40
第八部分 · 实战与贡献Part 8 · Practice & contributing

编译·调试·贡献Build & contribute

读懂了源码,下一步自然是动手:改一行、修个 bug、加个特性,然后把它提成一个能被合并的 PR。可"动手"这件事本身也有一套流程——怎么把这个上百万行的 C++ 大项目编译出来、改完怎么确认没把别的地方弄坏、提 PR 又要守哪些规矩。这一课不讲新的内部原理,而是带你把这条真实的开发回路完整走一遍,让前面三十多课学到的"读",真正落到"能改、能贡献"上。

回路其实就三段:编译(用 CMake 一套命令把源码变成 build/bin/ 里的二进制,换个开关就切到 CUDA / Metal / Vulkan 等后端)、测试(用 ctest 跑自动化用例,其中 test-backend-ops 专门校验各后端算子结果一致)、贡献(按 CONTRIBUTING.md 提 PR:一个 PR 一个功能、CPU 支持优先、用 clang-format 对齐风格)。把这三段串起来,你就有了一条从"本地改代码"到"被上游接纳"的完整路径。这条路你走通一次,以后面对任何一个陌生的大型 C++ 开源项目,套路其实都八九不离十。

有一件事必须先讲明白,尤其因为你可能正借助 AI 读这门课:llama.cpp 的 CONTRIBUTING.md 有一条明确的 AI 政策——不接受完全或主要由 AI 生成的 PR,且要求贡献者能独立理解、调试、维护自己提交的代码。这一课会把这条政策原样讲清楚,因为它直接决定了"怎样的贡献才会被这个项目接受"。路线图:编译(配一张"一次贡献的生命周期"追踪图)-> 测试 -> 调试 -> 贡献规范 -> 两个折叠深挖。一句话定个调:这一课的每个工具,最终都服务于同一个目标——让你的改动既改对了、又能被这个项目长期接住。

🌍 宏观理解
这一课的主线是一条回路:编译 -> 改 -> 测试 -> 提 PR -> CI 验证。它和前面所有"讲原理"的课不同——这里讲的是怎么真正参与进来。核心工具就三样:CMake(把源码编译成二进制,一套命令靠不同开关切换 CPU / CUDA / Metal / Vulkan 等后端)、ctest(跑自动化测试,其中 test-backend-ops 最关键,它逐算子比对不同后端的结果是否一致)、CONTRIBUTING.md(贡献规范,尤其是对 AI 生成 PR 的明确政策)。理解这条回路,你就从"读代码的人"变成了"能往里写、且写法能被项目接纳的人"。而且这条回路是自洽的:CONTRIBUTING 要求"改了 ggml 算子就要跑 test-backend-ops"、"新功能先只做 CPU",正是因为 CPU 实现是所有后端的参考答案(呼应 L31/L33)——规范不是凭空定的规矩,而是从"怎么保证几十种后端都算对"这个工程现实里长出来的。还有一点值得先记在心里:这条回路是有方向的——总是"先能编、再改、再测、最后才提",跳过任何一步(比如没跑测试就提 PR)都会在后面加倍还回来。把这个顺序刻进肌肉记忆,比记住任何一条具体命令都重要,因为命令可以现查,流程错了却要整段返工。
🔌 生活类比
把参与 llama.cpp 想象成加入一个超大型开源厨房。你不能直接把菜端上桌:先得照菜谱(build.md)把灶台架起来——煤气灶、电磁炉、烤箱(CPU / CUDA / Metal)各有各的点火方式(不同 CMake 开关)。改好一道菜,不能自己觉得行就行,要先过标准化试吃ctest / test-backend-ops:同一道菜在每个灶上做出来味道必须一致)。最后上桌前还得守餐厅规矩CONTRIBUTING):一次只上一道菜(一个 PR 一个功能)、新菜先出基础版(CPU 优先)、摆盘要符合统一格式(clang-format)。还有一条贴在墙上的硬规定:不收纯靠 AI 代做、你自己却讲不清怎么做的菜——因为出了问题,得是你、而不是 AI 来回锅。这套"标准化试吃 + 餐厅规矩"看着繁琐,其实全是在替你兜底:它让你改的东西在上桌前就被挡下明显的错,也让维护者敢放心收你的菜——没有这层保障,一个上百贡献者的厨房早就乱成一锅了。

编译:一套 CMake 命令,切换各种后端

一切从编译开始。llama.cpp 用 CMake 构建,最基本的就两条命令:先配置(探测编译器和依赖、生成构建系统),再构建(真正把源码编译成二进制)。编译产物都落在 build/bin/ 下——你熟悉的 llama-clillama-server 都在那。想换个后端(比如用上 NVIDIA GPU),不用改源码,只在配置那一步加一个开关:

# 最基本的编译: 配置 + 构建 (来自 docs/build.md)
cmake -B build                              # 配置: 探测编译器/依赖, 生成构建系统
cmake --build build --config Release        # 构建: 真正编译, 产物落 build/bin/

# 换后端只改"配置"那步, 比如启用 CUDA:
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release

每个后端对应一个 -DGGML_* 开关,背后是 L31-L33 讲过的那套后端机制——同一份计算图,换一套算子实现。下面这张表把常用后端和它们的开关、适用平台列在一起,编译前对着选就行:

后端CMake 开关适用平台
CPU(默认)无需开关所有平台,参考实现
CUDA-DGGML_CUDA=ONNVIDIA GPU
Metal默认开(-DGGML_METAL=OFF 可关)Apple 芯片
Vulkan-DGGML_VULKAN=ON跨厂商 GPU
HIP / SYCL / MUSA-DGGML_HIP / SYCL / MUSA=ONAMD / Intel / 摩尔线程

编译只是开发回路的起点。把"改一个东西、最后被合并"的全过程定格成一条流水,你会看到编译、测试、规范是怎么串成一条线的:

追踪一次贡献的生命周期:从 clone 到合并,编译出二进制、改代码、跑测试确认没坏、格式化、提 PR、过 CI 矩阵,最后 squash 合并(示意)。
① clone
本地拿到源码
fork + clone
cmake -B build
--build
② 编译
build/bin/ 二进制
确认能跑
改代码
③ 改动
你的修改
一个功能
ctest /
test-backend-ops
④ 测试
用例全过
没弄坏别处
clang-format
提 PR
⑤ PR
一功能 · CPU 优先
守 CONTRIBUTING
CI 矩阵
review
⑥ 合并
squash merge
进入 master

测试:ctest 与 test-backend-ops

改完代码,怎么确认没把别处弄坏?跑测试。llama.cpp 的测试用 CTest 组织:tests/ 下一堆 test-*.cpp,由 tests/CMakeLists.txt 里的几个宏注册成用例。最常用的是 llama_build_and_test(编译一个测试源文件并注册)和 llama_test(把同一个测试程序配上不同参数,注册成多个用例——比如 test-tokenizer-0 就对每个词表各跑一遍)。注册好之后,在 build/ 里一条 ctest 就能把它们全跑起来:

# tests/CMakeLists.txt: 用宏把一个 test-*.cpp 注册成 ctest 用例
llama_build_and_test(test-backend-ops.cpp)         # 编译并注册
llama_test(test-tokenizer-0 NAME ... ARGS ...)     # 参数化: 同一程序跑多个词表

# 在 build/ 里跑测试:
ctest --test-dir build                 # 跑全部用例
ctest --test-dir build -R backend-ops  # 只跑名字含 backend-ops 的

测试分几类,各管一摊:test-tokenizer-* 验证分词结果和参考一致(呼应 L20)、test-quantize-* 验证量化/反量化的误差在范围内(呼应 L06/L12)、test-sampling 验证采样逻辑(呼应 L21)。但其中分量最重的是 test-backend-ops:它把 ggml 的每一个算子在不同后端上各算一遍,再逐元素比对结果是否一致。这就是 llama.cpp 敢同时维护十几种后端的底气——任何一个后端的任何一个算子写错了,这个测试都会当场抓出来。所以它不只是"一个测试",而是整个多后端体系的正确性地基。顺带说一句它的运行成本:因为要在多个后端上各算一遍、还要逐元素比对,test-backend-ops 跑起来并不算快;但这恰恰是它的价值所在——它把"某个后端会不会悄悄算错"这种最难靠肉眼抓的 bug,提前压进了一次可重复的自动化比对里,用机器的耐心换人的安心。

调试:Debug 构建、sanitizer 与算子核对

测试告诉你"坏了",调试帮你找到"哪坏了"。第一招是换 Debug 构建(-DCMAKE_BUILD_TYPE=Debug):带上调试符号、关掉激进优化,崩溃时能看清调用栈、能单步跟。第二招是 sanitizer——CI 里专门有一条 build-sanitize,用 ASan / UBSan 跑测试,能逮住越界访问、未定义行为这类"平时不报、偶尔才炸"的 bug。在本地复现某个 CI 失败时,照着同样的 sanitizer 开关编一份,问题往往一下就现形。一条很实用的经验:Release 构建下那种偶发、难复现的崩溃,十有八九是内存越界或未定义行为,换成 Debug + sanitizer 重编一遍再跑,往往比盯着代码干想快得多——让工具替你把错误现场抓出来,是调试的第一性原则。

如果你动的是 ggml 算子,调试有个专属利器,还是它:test-backend-ops。它能把你改的那个算子在 CPU 和目标后端上各算一遍、逐元素比对,第一时间告诉你"结果对不对、从哪个元素开始偏"。CONTRIBUTING 也正因此点名:改了或新增 ggml 算子,必须跑(并补充)test-backend-ops。再配上各 example / 工具的 --verbose 日志,绝大多数推理层面的问题都能定位。这一节只点到为止——真正的调试功力得自己练,但你至少要知道这几件趁手的工具都摆在哪。还有一类问题不在 C++ 里、而在 Python 转换脚本或构建配置上——这时候 python-lint / python-type-check 这些 CI 检查就是你的第一道提示,本地照着跑一遍,能省掉一轮"提了才发现格式不过"的来回。

贡献规范:怎样的 PR 会被接受

工具会用了,最后这关是规矩。CONTRIBUTING.md 列的要求不多,但条条都为"让维护者能长期接住你的代码":一个 PR 一个功能(改动聚焦才好审)、CPU 支持优先(新东西先做 CPU、其它后端放后续 PR)、改了 ggml 算子就跑并补 test-backend-ops、用 clang-format(clang-tools v15+)对齐风格。维护者合并时用 squash,提交标题写成 <module> : <title> (#NNNN) 这样的格式。把这些要件摆成一张过关清单:

范围一个 PR 一个功能
改动聚焦、好审;别把无关的重构和修复塞进同一个 PR
后端CPU 支持优先
新功能 / 新模型先只做 CPU,CUDA 等放后续 PR(CPU 是参考实现)
测试改 ggml 就跑 test-backend-ops
动了算子要跑、必要时补测例,保证各后端结果一致
风格clang-format + 标题格式
用 clang-format(v15+) 对齐;squash 标题写成 <module> : <title> (#NNNN)

最后是最该认真对待的一条——AI 使用政策CONTRIBUTING.md 在很靠前的位置就写明:不接受完全或主要由 AI 生成的 PR;用 AI 先写、人再改,仍算 AI 生成。它要求贡献者能独立理解、调试、维护自己提交的代码,如实披露 AI 的使用方式,并明确禁止用 AI 代写 PR 描述、issue、评论这类与人沟通的内容。这条政策的用意不难理解:一个 PR 是一份长期承诺——维护者要审它、集成它、长期支持它;项目要的从来不是"代码从哪来",而是背后有没有一个能为它负责的人。所以如果你正用 AI 学这门课,最稳的姿势是:让它帮你读懂代码,而把"设计、决策、能讲清楚为什么"牢牢留在自己手里。说白了,这条政策保护的从来不是"纯手写"这个形式,而是"真有人懂这段代码、能在它出问题时扛起来"这个实质——而后者,恰恰是这整门课想帮你抵达的状态:不只是会用工具,而是心里真有底。

深入:CPU 优先 与 CI 矩阵

两个折叠,回答两个"为什么要这样规定"的问题——理解了它们,前面那些规矩就不再是死板的条文,而是有道理的工程选择。这也是读规范的正确姿势:别把它当成必须背的教条,而是去问每一条"它在防什么坏情况"——想通了防的是什么,你自然就记住了、也更愿意守。

1 为什么新功能要"CPU 支持优先"? 点击展开

乍看有点反直觉——大家不都冲着 GPU 加速来的吗,为什么新功能反而要先做 CPU?答案前面已经埋好了:CPU 实现是所有后端的参考答案test-backend-ops 校验一个 CUDA / Metal 算子"对不对",靠的就是拿它的结果和 CPU 版逐元素比对(呼应 L31 的 CPU 后端、L33 的后端调度)。如果一个新算子连 CPU 版都没有,就没有东西能给 GPU 版当 ground truth,正确性根本无从验证。再者,CPU 版人人能编、能跑(不挑硬件),维护者审 PR、别的贡献者复现问题都方便。所以"CPU 优先"不是看轻 GPU,而是先把"对"的基准立起来,再谈"快"——先正确、再加速,是这个项目一以贯之的工程顺序。把它和上一课连起来看也很自然:上一课新增一个模型也是"先让它在 CPU 上能转换、能跑通",再谈别的;同一个"先立基准"的思路,在加模型和加算子两处各用了一次。反过来想也站得住:要是允许"先上 GPU 版、CPU 版以后再补",那段时间里这个算子就没有任何参考答案,谁也说不清它到底算得对不对——规范要堵的正是这个洞。先把对错的尺子立起来,再谈谁跑得快,顺序错了,后面全是糊涂账。

2 CI 为什么有几十个 workflow? 点击展开

翻一眼 .github/workflows/,你会看到几十个 yml 文件,乍看吓人。但理出来其实就两类。一类是构建矩阵:每个后端、每个平台各一套——build-cpubuild-cuda-ubuntu / build-cuda-windowsbuild-vulkanbuild-syclbuild-applebuild-android……为什么这么多?因为 llama.cpp 的卖点就是"哪儿都能跑"(呼应 L33 后端调度),那就得在哪儿都编一遍、测一遍,少测一个组合就可能悄悄坏掉。另一类是质量门code-style(命名 / 风格约定检查)、editorconfigpython-lint / python-type-checkbuild-sanitize(ASan/UBSan)等,把"风格统一、没低级错误"也自动卡住。你提一个 PR,这一整套会自动在各平台跑一遍——这就是为什么"在我机器上能编"远远不够:得在这个矩阵的每一格里都绿,才算真的没破坏跨平台支持。看懂这一点,你对"开源大项目怎么在几十种环境里保持不崩"也就有了答案:不是靠人去手动测,而是把"每种环境编一遍、测一遍"写成了自动跑的 CI。这也解释了一个常让新人困惑的现象:一个在自己电脑上明明没问题的 PR,却被 CI 拦了下来——很可能只是某个你手头根本没有的平台编不过。而正因为 CI 替所有人把这些平台都试了一遍,你才不必自己去凑一屋子设备,这是开源协作里一种隐形却巨大的便利。

✅ 关键要点
  • 编译:cmake -B build 配置、cmake --build build --config Release 编译;后端靠开关切换(-DGGML_CUDA=ON 等);产物在 build/bin/
  • 测试:ctest 跑用例;tests/CMakeLists.txtllama_test/llama_build_and_test 宏注册;test-backend-ops 逐算子比对各后端结果。
  • 调试:Debug 构建 + sanitizer(build-sanitize CI);改 ggml 算子务必跑 test-backend-ops
  • 贡献:CONTRIBUTING.md 规定一个 PR 一个功能、CPU 支持优先、squash 合并、标题 <module> : <title> (#NNNN)clang-format(clang-tools v15+)。
  • AI 政策:不接受完全或主要由 AI 生成的 PR;贡献者须能独立理解、调试、维护自己的代码,并如实披露 AI 的使用方式。
💡 设计洞察
这一课最值得带走的,是 llama.cpp 怎么把"对不对"变成一件可自动检验的事。test-backend-ops 不去抽象地论证某个 CUDA kernel "正确",而是把正确操作性地定义成"和 CPU 参考实现逐元素一致"——于是几十种后端、上千个算子的正确性,统统化成一句可以一键跑的断言。这就是为什么 CONTRIBUTING 要"CPU 优先":得先有那个谁都要对齐的参考答案,后面的 CUDA / Metal / Vulkan 才有"对"可言。把这个思路记住,你会发现它远不止用于内核:任何"多实现必须等价"的系统,都能用"挑一个当 ground truth、其余对齐它"来把质量问题自动化。至于那条 AI 政策,底层逻辑也一脉相承——项目要的不是"代码从哪来",而是"有没有一个能对它负责、能在半夜出 bug 时把它修好的人";可维护性,才是开源协作真正的硬通货。也给这门课收个尾:从第一课认识 llama.cpp 是什么,到这一课学会怎么给它提一个站得住脚的 PR,你已经走完了"读懂 -> 动手 -> 贡献"的整条路——剩下的,就是挑一个你真正在意的问题,去把它修好。

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

1. 想编译一个能用上 NVIDIA GPU 的 llama.cpp,CMake 配置该加哪个开关?
  1. -DGGML_METAL=ON
  2. 不用开关,会自动检测
  3. -DGGML_CUDA=ON
  4. --gpu
看答案与解析 点击展开
答案:C。docs/build.md 里,CUDA 后端在配置那一步加 -DGGML_CUDA=ON,例如 cmake -B build -DGGML_CUDA=ON 再 cmake --build build --config Release。每个后端对应一个 -DGGML_* 开关(Metal 默认开、Vulkan 用 -DGGML_VULKAN=ON 等),换后端只改“配置”这一步、不动源码。--gpu 那种是运行时参数、不是编译开关;llama.cpp 也不会自动替你编进 CUDA。
2. 你改了一个 ggml 算子,CONTRIBUTING.md 要求你必须跑哪个测试?
  1. 不用测,CI 会管
  2. test-sampling
  3. test-backend-ops
  4. test-tokenizer-0
看答案与解析 点击展开
答案:C。CONTRIBUTING 明确:改了或新增 ggml 算子,必须跑(必要时补充)test-backend-ops。它把你改的算子在 CPU 和目标后端上各算一遍、逐元素比对,确保各后端结果一致——这正是 llama.cpp 敢同时维护十几种后端的底气。test-tokenizer-0 / test-sampling 各测分词与采样,与算子正确性无关;而“CI 会管”不是借口:本地先跑过,才不至于提了 PR 才发现挂了。
3. llama.cpp 的 CONTRIBUTING.md 对“AI 生成的 PR”是什么态度?
  1. 没有任何限制
  2. 完全禁止使用任何 AI 工具
  3. 只要标注了就随便用
  4. 不接受完全或主要由 AI 生成的 PR;贡献者须能独立理解和维护自己的代码
看答案与解析 点击展开
答案:D。CONTRIBUTING 靠前的 AI 政策写得很清楚:不接受完全或主要由 AI 生成的 PR,用 AI 先写、人再改仍算 AI 生成;AI 只能起辅助作用,贡献者必须能独立理解、调试、维护自己提交的代码,并如实披露 AI 的使用方式。它不是“完全禁止 AI”,也不是“随便用”——核心诉求是“背后得有个真正懂这段代码、能为它负责的人”。这也提醒用 AI 学习的人:让它帮你读懂,别让它替你思考。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 这一课反复出现一个词:test-backend-ops。请顺着它把几条看似分散的规范串起来:(1) 为什么 CONTRIBUTING 要求“新功能 CPU 支持优先”,这和 test-backend-ops 把 CPU 当参考实现有什么关系?(2) 为什么说它是“整个多后端体系的正确性地基”,而不只是一个普通测试?(3) CI 里那几十个 build-* workflow,和它在精神上是一回事吗——都在回答同一个什么问题?(4) 把这一课和上一课连起来:从“加一个模型”到“加一个算子”,“先把 CPU / 参考版立起来”这条线是怎么贯穿的?

Once you can read the source, the natural next step is to act: change a line, fix a bug, add a feature, then turn it into a PR that can be merged. But "acting" has its own process - how to compile this million-line C++ project, how to confirm a change did not break something else, and what rules a PR must follow. This lesson teaches no new internals; it walks you through that real development loop end to end, so the "reading" from thirty-some lessons turns into "can change it, can contribute".

The loop is really three parts: build (use one set of CMake commands to turn source into binaries under build/bin/, flipping a flag to switch to CUDA / Metal / Vulkan and other backends), test (run automated cases with ctest, where test-backend-ops specifically checks that operators agree across backends), and contribute (open a PR per CONTRIBUTING.md: one feature per PR, CPU support first, align style with clang-format). String the three together and you have a full path from "edit code locally" to "accepted upstream". Walk this path once and the playbook for almost any unfamiliar large C++ open-source project looks much the same.

One thing must be stated up front, especially since you may be reading this course with AI help: llama.cpp's CONTRIBUTING.md has an explicit AI policy - it does not accept PRs that are fully or predominantly AI-generated, and it requires contributors to independently understand, debug, and maintain the code they submit. This lesson states that policy as-is, because it directly determines "what kind of contribution this project will accept". Roadmap: build (with a "lifecycle of one contribution" trace) -> test -> debug -> contribution rules -> two deep-dive accordions. To set the tone in one line: every tool in this lesson ultimately serves the same goal - making your change both correct and something the project can carry for the long haul.

🌍 Big picture
This lesson's spine is a loop: build -> change -> test -> open a PR -> CI. Unlike every prior "explain the internals" lesson, this one is about how to actually take part. The core tools are just three: CMake (compile source into binaries, one command set switching CPU / CUDA / Metal / Vulkan via different flags), ctest (run automated tests, of which test-backend-ops matters most - it compares operator results across backends), and CONTRIBUTING.md (the contribution rules, notably the explicit policy on AI-generated PRs). Understand the loop and you turn from "someone who reads the code" into "someone who can write into it in a way the project will accept". And the loop is self-consistent: CONTRIBUTING asks "if you change a ggml operator, run test-backend-ops" and "do a new feature on CPU first" precisely because the CPU implementation is the reference answer for all backends (recall L31/L33) - the rules are not arbitrary, they grow out of the engineering reality of "how do you guarantee dozens of backends all compute right". One more thing to fix in mind first: this loop has a direction - always "build first, then change, then test, and only then submit", and skipping any step (like opening a PR without running tests) comes back to bite you double later. Burning that order into muscle memory matters more than memorizing any single command, because commands you can look up, but a broken process means reworking whole chunks.
🔌 Analogy
Picture joining llama.cpp as joining a giant open-source kitchen. You cannot just plate a dish and serve it: first set up the stove per the recipe (build.md) - gas burner, induction hob, oven (CPU / CUDA / Metal) each light differently (different CMake flags). After cooking a dish you do not get to call it good yourself; it must pass a standardized taste test (ctest / test-backend-ops: the same dish must taste identical made on every stove). Finally, before serving, you follow house rules (CONTRIBUTING): one dish at a time (one feature per PR), a new dish ships a basic version first (CPU first), and plating follows one format (clang-format). There is also a hard rule on the wall: no dishes that were entirely AI-cooked while you cannot explain how they were made - because when something goes wrong, it has to be you, not the AI, who reworks it. This "standardized taste test plus house rules" looks fussy but is all there to catch you: it stops obvious mistakes in your change before it reaches the table, and lets maintainers accept your dish with confidence - without that safety net a kitchen of hundreds of contributors would have descended into chaos long ago.

Build: one set of CMake commands, switching backends

It all starts with the build. llama.cpp builds with CMake, and the basics are just two commands: first configure (detect the compiler and dependencies, generate the build system), then build (actually compile source into binaries). The build output lands under build/bin/ - the llama-cli and llama-server you know are right there. To switch backends (say, to use an NVIDIA GPU) you do not touch the source; you add one flag at the configure step:

# the basic build: configure + build (from docs/build.md)
cmake -B build                              # configure: detect compiler/deps, generate the build system
cmake --build build --config Release        # build: actually compile, output to build/bin/

# switching backend only changes the "configure" step, e.g. enable CUDA:
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release

Each backend has a -DGGML_* flag, backed by the backend machinery from L31-L33 - the same compute graph, a different set of operator implementations. The table below lists the common backends with their flags and target platforms; pick from it before you build:

backendCMake flagtarget platform
CPU (default)no flag neededall platforms, reference impl
CUDA-DGGML_CUDA=ONNVIDIA GPU
Metalon by default (-DGGML_METAL=OFF to disable)Apple silicon
Vulkan-DGGML_VULKAN=ONcross-vendor GPU
HIP / SYCL / MUSA-DGGML_HIP / SYCL / MUSA=ONAMD / Intel / Moore Threads

Building is only the start of the development loop. Freeze the whole "change one thing, end up merged" path into a flow and you see how build, test, and rules string into one line:

Tracing the lifecycle of one contribution: from clone to merge - build the binary, change code, run tests to confirm nothing broke, format, open a PR, pass the CI matrix, finally squash-merge (schematic).
(1) clone
source locally
fork + clone
cmake -B build
--build
(2) build
build/bin/ binaries
confirm it runs
change code
(3) change
your edit
one feature
ctest /
test-backend-ops
(4) test
cases all pass
nothing else broke
clang-format
open PR
(5) PR
one feature . CPU first
follow CONTRIBUTING
CI matrix
review
(6) merge
squash merge
into master

Test: ctest and test-backend-ops

After a change, how do you confirm nothing else broke? Run the tests. llama.cpp's tests are organized with CTest: a pile of test-*.cpp under tests/, registered as cases by a few macros in tests/CMakeLists.txt. The most common are llama_build_and_test (compile a test source and register it) and llama_test (register the same test program with different arguments as several cases - e.g. test-tokenizer-0 runs once per vocab). Once registered, one ctest in build/ runs them all:

# tests/CMakeLists.txt: macros register a test-*.cpp as a ctest case
llama_build_and_test(test-backend-ops.cpp)         # compile and register
llama_test(test-tokenizer-0 NAME ... ARGS ...)     # parameterized: one program, many vocabs

# run tests in build/:
ctest --test-dir build                 # run every case
ctest --test-dir build -R backend-ops  # only those whose name contains backend-ops

Tests fall into a few groups, each minding its own patch: test-tokenizer-* checks that tokenization matches the reference (recall L20), test-quantize-* checks that quantize/dequantize error stays in range (recall L06/L12), test-sampling checks the sampling logic (recall L21). But the heaviest of all is test-backend-ops: it runs every ggml operator on each backend and compares results elementwise. This is what lets llama.cpp dare to maintain a dozen-plus backends at once - if any operator on any backend is wrong, this test catches it on the spot. So it is not just "a test" but the correctness foundation of the whole multi-backend system. A note on its running cost: because it computes on several backends and compares elementwise, test-backend-ops is not fast to run; but that is exactly its value - it presses the hardest-to-eyeball bug, "might some backend quietly compute wrong", into one repeatable automated comparison, trading the machine's patience for the human's peace of mind.

Debug: Debug builds, sanitizers, and operator checking

Tests tell you "it broke"; debugging helps you find "where it broke". The first move is a Debug build (-DCMAKE_BUILD_TYPE=Debug): with debug symbols and aggressive optimization off, you can read the call stack at a crash and single-step. The second is sanitizers - CI has a dedicated build-sanitize that runs tests under ASan / UBSan, catching out-of-bounds access and undefined behavior, the kind of "usually silent, occasionally explodes" bug. When reproducing a CI failure locally, building with the same sanitizer flags often makes the problem surface at once. A handy rule of thumb: an intermittent, hard-to-reproduce crash under a Release build is almost always a memory overrun or undefined behavior - rebuild with Debug + sanitizer and rerun, and that usually beats staring at the code guessing; letting the tools catch the error scene for you is the first principle of debugging.

If you are touching a ggml operator, debugging has a dedicated weapon - the same one: test-backend-ops. It runs the operator you changed on both CPU and the target backend and compares elementwise, telling you immediately "is the result right, and from which element does it diverge". This is exactly why CONTRIBUTING calls it out: change or add a ggml operator and you must run (and extend) test-backend-ops. Add the --verbose logging of the various examples / tools and you can locate the vast majority of inference-level problems. This section only points the way - real debugging skill is earned by practice, but you should at least know where these handy tools sit. There is also a class of problems that live not in the C++ but in the Python conversion scripts or build config - there the python-lint / python-type-check CI checks are your first hint; running them locally saves a round of "submit, then find out the format fails".

Contribution rules: what kind of PR gets accepted

With the tools in hand, the last gate is the rules. CONTRIBUTING.md lists few requirements, but each exists so maintainers can take on your code for the long haul: one feature per PR (a focused change is reviewable), CPU support first (do the new thing on CPU, leave other backends to follow-up PRs), run and extend test-backend-ops if you touched a ggml operator, and align style with clang-format (clang-tools v15+). Maintainers merge by squash, with a commit title in the form <module> : <title> (#NNNN). Laid out as a pass/fail checklist:

scopeone feature per PR
a focused, reviewable change; do not stuff unrelated refactors and fixes into one PR
backendCPU support first
new feature / model on CPU first, CUDA and others in follow-up PRs (CPU is the reference impl)
testtouch ggml -> run test-backend-ops
run it when you change an operator, extend it if needed, keep backends in agreement
styleclang-format + title format
align with clang-format (v15+); squash title as <module> : <title> (#NNNN)

Last, the one to take most seriously - the AI usage policy. CONTRIBUTING.md states it plainly: PRs that are fully or predominantly AI-generated are not accepted; AI-written-then-human-edited still counts as AI-generated. It requires contributors to independently understand, debug, and maintain the code they submit, to disclose how AI was used, and explicitly forbids using AI to write the human-facing parts - PR descriptions, issues, comments. The intent is not hard to see: a PR is a long-term commitment - maintainers review it, integrate it, support it for the long haul; the project never cares "where the code came from" but whether there is a person who can be responsible for it. So if you are learning this course with AI, the safest stance is: let it help you read the code, and keep "design, decisions, being able to explain the why" firmly in your own hands. Put plainly, this policy protects not the form of "handwritten only" but the substance of "someone really understands this code and can carry it when it breaks" - and that substance is exactly the state this whole course wants to bring you to: not just knowing how to use tools, but having solid ground under your feet.

Deeper: CPU-first and the CI matrix

Two accordions answering two "why is it required this way" questions - understand them and the earlier rules stop being rigid clauses and become reasoned engineering choices. This is also the right way to read the rules: do not treat them as dogma to memorize, but ask of each one "what bad case is it preventing" - once you see what is being prevented, you remember it naturally and are more willing to follow it.

1 Why must a new feature be "CPU support first"? click to expand

It looks counterintuitive at first - is not everyone here for GPU acceleration, so why do a new feature on CPU first? The answer was planted earlier: the CPU implementation is the reference answer for all backends. test-backend-ops checks whether a CUDA / Metal operator is "correct" by comparing its result elementwise against the CPU version (recall the CPU backend in L31, backend dispatch in L33). If a new operator does not even have a CPU version, there is nothing to serve as ground truth for the GPU version, and correctness cannot be verified at all. Moreover the CPU version builds and runs for everyone (no special hardware), making it easy for maintainers to review and for other contributors to reproduce issues. So "CPU first" does not slight the GPU; it stands up the baseline of "correct" before talking about "fast" - correctness first, then acceleration, is this project's consistent engineering order. It connects naturally with the last lesson: adding a model there was also "first make it convert and run on CPU", then the rest - the same "set the baseline first" idea, used once for adding models and once for adding operators. The reverse holds too: if "ship the GPU version first, add the CPU version later" were allowed, during that window the operator would have no reference answer at all and nobody could say whether it computes correctly - the rule plugs exactly this hole. Stand up the ruler of right and wrong first, then talk about who runs fast; get the order wrong and the rest is a muddle.

2 Why does CI have dozens of workflows? click to expand

Glance at .github/workflows/ and you see dozens of yml files, daunting at first. But sorted out they are just two kinds. One is the build matrix: one per backend, per platform - build-cpu, build-cuda-ubuntu / build-cuda-windows, build-vulkan, build-sycl, build-apple, build-android... why so many? Because llama.cpp's whole selling point is "runs anywhere" (recall backend dispatch in L33), so it must compile and test everywhere; skip testing one combination and it can quietly break. The other kind is quality gates: code-style (naming / style-convention check), editorconfig, python-lint / python-type-check, build-sanitize (ASan/UBSan), and so on, automatically blocking "inconsistent style, silly mistakes". When you open a PR this whole set runs across platforms automatically - which is why "it builds on my machine" is nowhere near enough: it must be green in every cell of this matrix to count as truly not breaking cross-platform support. See this and you also have the answer to "how does a big open-source project stay un-broken across dozens of environments": not by manual testing, but by writing "compile and test in each environment" into CI that runs itself. This also explains a thing that often puzzles newcomers: a PR that clearly works on your own machine gets blocked by CI - likely just because some platform you do not even have fails to compile. And precisely because CI tries all those platforms for everyone, you do not have to assemble a roomful of devices yourself - an invisible but huge convenience of open-source collaboration.

✅ Key points
  • Build: cmake -B build to configure, cmake --build build --config Release to compile; backends switch via flags (-DGGML_CUDA=ON etc.); output lands in build/bin/.
  • Test: ctest runs cases; tests/CMakeLists.txt registers them via the llama_test/llama_build_and_test macros; test-backend-ops compares operator results across backends.
  • Debug: Debug build + sanitizers (the build-sanitize CI); changing a ggml operator means running test-backend-ops.
  • Contribute: CONTRIBUTING.md mandates one feature per PR, CPU support first, squash merge, title <module> : <title> (#NNNN), and clang-format (clang-tools v15+).
  • AI policy: PRs that are fully or predominantly AI-generated are not accepted; contributors must be able to independently understand, debug, and maintain their code, and must disclose how AI was used.
💡 Design insight
The best thing to take from this lesson is how llama.cpp turns "is it correct" into something automatically checkable. test-backend-ops does not argue abstractly that some CUDA kernel is "correct"; it operationally defines correct as "elementwise identical to the CPU reference" - so the correctness of dozens of backends and thousands of operators collapses into one assertion you can run with a single command. That is why CONTRIBUTING says "CPU first": you need that reference answer everyone aligns to before CUDA / Metal / Vulkan can even have a notion of "right". Keep this idea and you will see it far beyond kernels: any system where "multiple implementations must be equivalent" can automate quality by "picking one as ground truth and aligning the rest to it". As for the AI policy, the underlying logic is the same thread - the project does not care "where the code came from" but "whether there is a person who can be responsible for it, who can fix it when it breaks at 2am"; maintainability is the real hard currency of open-source collaboration. And to close the course: from lesson one learning what llama.cpp is, to this lesson learning how to open a PR that stands up, you have walked the whole path of "understand -> act -> contribute" - what is left is to pick a problem you genuinely care about and go fix it.

🧪 Self-test - think about the design

1. To build a llama.cpp that can use an NVIDIA GPU, which flag do you add at CMake configure time?
  1. -DGGML_METAL=ON
  2. no flag, it auto-detects
  3. -DGGML_CUDA=ON
  4. --gpu
Show answer & explanation click to expand
Answer: C. In docs/build.md, the CUDA backend is enabled at configure time with -DGGML_CUDA=ON, e.g. cmake -B build -DGGML_CUDA=ON then cmake --build build --config Release. Each backend has a -DGGML_* flag (Metal on by default, Vulkan via -DGGML_VULKAN=ON, etc.); switching backend only changes the 'configure' step, not the source. --gpu would be a runtime argument, not a build flag; and llama.cpp will not silently compile CUDA in for you.
2. You changed a ggml operator; which test does CONTRIBUTING.md require you to run?
  1. no need, CI will handle it
  2. test-sampling
  3. test-backend-ops
  4. test-tokenizer-0
Show answer & explanation click to expand
Answer: C. CONTRIBUTING is explicit: if you change or add a ggml operator you must run (and extend if needed) test-backend-ops. It runs the operator on CPU and the target backend and compares elementwise, keeping backends in agreement - exactly what lets llama.cpp maintain a dozen-plus backends at once. test-tokenizer-0 / test-sampling cover tokenization and sampling, unrelated to operator correctness; and 'CI will handle it' is no excuse: running locally first keeps you from discovering a failure only after opening the PR.
3. What is llama.cpp's CONTRIBUTING.md stance on 'AI-generated PRs'?
  1. no restrictions at all
  2. bans any use of AI tools entirely
  3. anything goes as long as you label it
  4. does not accept PRs that are fully or predominantly AI-generated; contributors must be able to independently understand and maintain their code
Show answer & explanation click to expand
Answer: D. The AI policy near the top of CONTRIBUTING is clear: it does not accept PRs that are fully or predominantly AI-generated, and AI-written-then-human-edited still counts as AI-generated; AI may only assist, and contributors must independently understand, debug, and maintain their code and disclose how AI was used. It is neither 'ban AI entirely' nor 'anything goes' - the core demand is 'there must be a person who truly understands this code and can be responsible for it'. A reminder for AI-assisted learners: let it help you read, do not let it think for you.
💭 Open questions (no single right answer - just think or try)
  • One term recurs in this lesson: test-backend-ops. Use it to tie several seemingly scattered rules together: (1) why does CONTRIBUTING require 'CPU support first' for a new feature, and how does that relate to test-backend-ops treating CPU as the reference implementation? (2) why call it 'the correctness foundation of the whole multi-backend system' rather than just an ordinary test? (3) are the dozens of build-* workflows in CI the same thing in spirit - answering which same question? (4) tie this lesson to the previous one: from 'add a model' to 'add an operator', how does the thread 'stand up the CPU / reference version first' run through both?