系列:那些值得深究的源码
难度:⭐⭐⭐⭐(1-5星)
本篇精读:llama.cpp ·ggml-org/llama.cpp[1] · tagb9999,commit47c786924ad1ab7e91da2cdc72fcdb563780c2bd
分析重点:沿着 GGUF 模型装载、量化张量、llama_decode、ggml 计算图和 backend scheduler 这条路径,看 llama.cpp 怎么把本地推理做成可移植的 C/C 运行时。 **前置知识**:能读 C/C,了解 Transformer 推理、KV cache、量化和基本计算图概念。
1. 术语表(前置阅读)
• GGUF:GGML Universal File。llama.cpp 当前主要使用的模型文件格式,保存 metadata、tensor 描述和 tensor 数据;项目内的 gguf-py[2] 包负责读写这类文件,最早的格式讨论可看 ggml 官方 PR ggml-org/ggml#302[3]。• ggml:llama.cpp 背后的张量、计算图和 backend 抽象库。llama.cpp README 明确说它是开发 ggml 新能力的主要 playground,见 README[4]。 • libllama:llama.cpp 对外暴露的 C API 层,主要定义在 include/llama.h[5]。• llama_context:一次推理上下文,保存 runtime 参数、KV memory、batch allocator、backend scheduler、输出缓冲等状态。 • llama_batch / llama_ubatch:外部传入的是 batch,内部会再切成 ubatch。这样大 prompt 和小步 decode 可以共用同一套执行入口。 • KV cache / memory module:自回归推理保存历史 Key/Value 的状态。这里先看它和 decode 主路径的交界,各种 memory implementation 属于另一条分支。 • ggml_cgraph:ggml 的计算图对象。llama.cpp 根据模型结构和当前 ubatch 建图,再交给 backend scheduler 分配和执行。 • backend / backend buffer:CPU、Metal、CUDA、Vulkan、SYCL 等执行后端及其内存缓冲。项目 README 列出了多种硬件后端和 CPU+GPU hybrid inference 能力,见 README Description[4]。 • mmap:把 GGUF 文件中的 tensor 数据映射到进程地址空间。llama.cpp 会在后端支持时把 mmap 区域包装成 backend buffer,避免不必要的数据拷贝。 • quantization type / ftype:模型权重的量化类型,例如 Q4_K_M、Q5_K_M、Q8_0。llama-quantize支持的类型在tools/quantize/quantize.cpp[6] 里登记。• K-quant:ggml 里一组按 super-block 编排的量化格式,例如 block_q4_K、block_q5_K、block_q6_K。它会把 scale、min、high bits 等信息一起放进块结构,不只是把 float 简单截成 4 bit。• graph reuse:当下一次 ubatch 的图拓扑和上一次相同,llama.cpp 可以复用上一张 graph,只重写输入,再交给 backend 执行。
2. 版本、范围和阅读边界
分析版本固定为 llama.cpp tag b9999,commit 47c786924ad1ab7e91da2cdc72fcdb563780c2bd。源码许可为 MIT;后面引用的源码片段均为该版本的短节选和删减。
llama.cpp 的目标超过了让 LLaMA 跑起来。项目 README 把目标写成“minimal setup”和“state-of-the-art performance on a wide range of hardware”,并列出 plain C/C++、Apple silicon、x86 SIMD、RISC-V、1.5 到 8 bit 量化、CUDA、Vulkan、SYCL、CPU+GPU hybrid inference 等能力,这些能力会反过来约束源码结构[4]。如果只为一张 NVIDIA GPU 和一种模型服务,代码可以写得更像深度学习框架里的 kernel launcher;llama.cpp 要同时照顾本地 CPU、手机、Mac、独显、混合卸载和不断变化的 GGUF 模型生态,主路径必须把模型格式、runtime API、计算图和后端执行分开。
模型结构和 CUDA/Metal kernel 不是这条路径的展开对象。真正会反复影响工程判断的是下面四个位置:
include/llama.h[5]src/llama.cpp[7] | ||
src/llama-model-loader.cpp[8]src/llama-model.cpp[9] | ||
src/llama-context.cpp[10] | llama_context::decode() | |
ggml/src/ggml-backend.cpp[11]ggml/src/ggml-common.h[12] |
先用一张架构图交代这条路径。它画的是文件格式、运行时状态、计算图和设备后端之间的边界,不是一次函数调用流程。

图里的比喻也要有边界:GGUF 像货单,只对应 tensor 描述、metadata 和数据偏移;backend buffer 像仓位,只对应权重和中间张量放在哪类内存里;ggml graph 像执行工单,只对应本次 ubatch 要执行的 op DAG。图里最关键的调用方向是:GGUF 不知道设备,llama_context 不直接写设备 kernel,backend 不关心 prompt 是怎么来的。每一层只把下一层需要的结构准备好。
3. 公开 API 很薄,但它先挡住了后端缺失
llama.cpp 对外给的是 C API。llama_model_load_from_file()、llama_init_from_model()、llama_decode() 这些函数让上层 bindings 和工具不必理解 C++ 类层次。加载模型时,src/llama.cpp 里的 llama_model_load_from_file_impl() 会先检查 backend registry,再调用 llama_model_load()。
源码:llama.cpp src/llama.cpp[7],MIT license,tag b9999。
1 2 3 4 5 6 7 8
if (!params.vocab_only && ggml_backend_reg_count() == 0) {
LLAMA_LOG_ERROR("%s: no backends are loaded. hint: use ggml_backend_load() or ggml_backend_load_all() to load a backend before calling this function\n", __func__);
return nullptr;
}
const auto [status, model] = llama_model_load(
metadata, set_tensor_data, set_tensor_data_ud,
path_model, splits, file, params);
这段代码说明 libllama 在入口处暴露了必要前置条件。它把“必须先加载 backend”放在模型加载入口,避免错误延迟到后面的 tensor allocation 才随机暴露。这样的边界很直接,但对 C API 很重要:C API 一旦让错误穿透到更深层,调用方拿到的通常就是空指针、状态码和日志,定位成本会高很多。
模型加载接下来要处理的不只是把文件读到内存。它要先确定 GGUF 里的 metadata 和 tensor 描述,再决定这些 tensor 应该放在哪类 backend buffer 中。
4. GGUF loader 先建立一张统一的权重索引
GGUF 文件里有 metadata,也有 tensor 描述和 tensor 数据。llama.cpp 在 llama_model_loader 中用 gguf_init_from_file(..., no_alloc = true) 读取 metadata 和 tensor header,但先不分配实际 tensor 数据。它随后遍历 GGUF context 里的 tensor,把名字映射到 llama_tensor_weight。
源码:llama.cpp src/llama-model-loader.cpp[8],MIT license,tag b9999。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
struct gguf_init_params params = {
/*.no_alloc = */ true,
/*.ctx = */ &ctx,
};
metadata_ptr.reset(gguf_init_from_file(fname.c_str(), params));
metadata = metadata_ptr.get();
for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) {
std::string tensor_name = std::string(cur->name);
if (weights_map.find(tensor_name) != weights_map.end()) {
throw std::runtime_error(format("invalid model: tensor '%s' is duplicated", ggml_get_name(cur)));
}
n_elements += ggml_nelements(cur);
n_bytes += ggml_nbytes(cur);
weights_map.emplace(tensor_name, llama_tensor_weight(files.back().get(), 0, metadata, cur));
}
这段实现要看 no_alloc = true 和 weights_map。GGUF 先被解释成“有哪些 tensor、叫什么、形状和类型是什么、数据偏移在哪里”。实际数据何时进入内存、进入 CPU buffer 还是 GPU buffer,是后面的 llama_model 决策。这样做让同一个 GGUF 文件可以走不同执行策略:纯 CPU、部分 GPU offload、mmap host buffer、或者不分配权重只读 vocab。
GGUF 在这里更像一份尚未搬货的仓库货单。gguf_init_from_file(..., no_alloc = true) 先把货单读清楚:每箱货叫什么、尺寸多大、在文件里从哪里开始。weights_map 是按名字建立的索引。后面再决定把货放到普通货架、冷库还是装车区,对应 CPU buffer、GPU buffer 或 mmap host buffer。把读货单和搬货分开,是 llama.cpp 能支持多后端和不同 offload 策略的前提。
这个 loader 还处理 split GGUF。每个 split 会检查 split_no,把各 shard 的 tensor 都放进同一张 weights_map,并检查 tensor 数量是否吻合。也就是说,文件拆分只是存储层细节;模型构建层看到的是统一的权重名字空间。这里的可迁移判断很直接:如果格式层已经允许模型被拆成多个文件,runtime 层最好尽早把它重新收敛成一张逻辑索引,避免后续代码到处判断“这个 tensor 来自哪个文件”。

GGUF 的另一个作用是让量化类型从文件格式一路传到计算后端。loader 会统计每个 tensor 的 ggml_type,再推断模型整体的 llama_ftype。后面的 buffer allocation、kernel dispatch 和工具链都依赖这套类型系统,不只是日志展示需要它。
5. backend buffer 把权重放到能执行的位置
模型加载的下一段关键逻辑在 llama_model::load_tensors()。llama_model_loader 已经知道有哪些 tensor,llama_model 现在要为不同 buffer type 建立 ggml_context,再把数据加载进去。代码里最值得看的是 mmap 和 backend buffer 的交界。
源码:llama.cpp src/llama-model.cpp[9],MIT license,tag b9999。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
if (ml.use_mmap && use_mmap_buffer && buffer_from_host_ptr_supported && is_default_buft) {
for (uint32_t idx = 0; idx < ml.files.size(); idx++) {
void * addr = nullptr;
size_t first, last;
ml.get_mapping_range(&first, &last, &addr, idx, ctx);
const size_t max_size = ggml_get_max_tensor_size(ctx);
ggml_backend_buffer_t buf = ggml_backend_dev_buffer_from_host_ptr(
dev, (char *) addr + first, last - first, max_size);
bufs.emplace_back(buf);
buf_map.emplace(idx, buf);
}
} else {
ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors_from_buft(ctx, buft);
bufs.emplace_back(buf);
}
这段逻辑先走设备能力判断:能把 mmap 区域直接包装成 backend buffer,就不额外拷贝;不能,就正常分配后端 buffer。代码没有为某一种硬件写死路径,而是先问 buffer_from_host_ptr_supported、是否默认 buffer type、是否启用 mmap。判断成立才走零拷贝近似路径。
backend buffer 可以继续沿用“仓位”的比喻:同一批权重,有的设备允许直接从原包装箱里取货,也就是把 mmap 出来的 host ptr 包成 backend buffer;有的设备不允许,就要搬到专门仓位。源码先问设备支持什么搬运方式,没有假设所有仓库都一样。这个比喻不覆盖 kernel 执行,只帮助理解权重数据在加载阶段为什么不急着被固定到某一种内存里。
后面还有一行容易被忽略:权重 buffer 会被标成 GGML_BACKEND_BUFFER_USAGE_WEIGHTS,注释里写明这个信息会被 ggml_backend_sched 用来改善 op scheduling。权重放在哪不只是内存问题,也会反过来影响图中算子的落点。模型加载阶段已经在为后端调度提供 hints。

这也是 llama.cpp 可移植性的一个代价。代码不会像单后端推理引擎那样只有一种“权重上传 GPU”的路径;它必须把文件映射、host memory、device buffer、partial offload、Metal 统一内存等情况都压进同一个抽象里。好处是同一套上层 API 能跑很多硬件;代价是模型加载代码里会有不少能力探测和兼容分支。
6. 量化进入张量类型系统
llama.cpp 能在本地设备上跑大模型,量化是主路径能力。README 直接把 1.5-bit 到 8-bit integer quantization 列为项目能力之一,llama-quantize[6] 也维护了一张可用量化类型表,包括 Q4_K_M、Q5_K_M、Q6_K、IQ2_XXS、TQ1_0 等。推理时怎么解释权重,由 ggml 里的块结构和 op 实现决定。
看 block_q4_K 和 block_q6_K 就能明白:量化权重是一种有布局、有 scale、有静态尺寸检查的数据结构,不能只按裸 bitstream 理解。
源码:llama.cpp ggml/src/ggml-common.h[12],MIT license,tag b9999。
1 2 3 4 5 6 7 8 9 10 11 12 13
typedef struct {
ggml_half d; // super-block scale
uint8_t scales[12]; // scales, quantized with 6 bits
uint8_t hmask[QK_K/8]; // quants - high bit
uint8_t qs[QK_K/4]; // quants - low 2 bits
} block_q3_K;
typedef struct {
uint8_t ql[QK_K/2]; // quants, lower 4 bits
uint8_t qh[QK_K/4]; // quants, upper 2 bits
int8_t scales[QK_K/16]; // scales, quantized with 8 bits
ggml_half d; // super-block scale
} block_q6_K;
这段代码把一个常见误解拆开了:量化不是简单地说“4 bit 就省一半显存”。不同量化格式会选择不同块大小、scale 存储、min/zero-point 表达和中间计算格式,压缩率、误差和 kernel 复杂度都不一样。把这些格式变成 ggml_type 后,GGUF 可以存它,loader 可以读它,backend 可以决定自己是否支持对应 op。
量化块可以想成带解包说明的压缩包。包里除了低 bit 的权重,还带着 scale、min、high bits、block size 和静态断言。推理时不会把所有压缩包先统一解成 float 再算,后端会按 ggml_type 选择自己能理解的解包和计算方式。这个比喻对应 block_q*_K 这类结构,不表示量化只是文件压缩;它直接影响 op 实现和 backend 支持矩阵。
一个仓促实现很容易把量化做成“加载时转 float16,然后图里都当 float16 跑”。那样接口简单,但内存优势会在执行前丢掉。llama.cpp 让量化类型贯穿文件、tensor、buffer 和 op dispatch;复杂性留在类型系统和 backend 支持矩阵里,避免运行时四处写特殊判断。
7. llama_decode 的主路径:batch 先变成 ubatch,再变成 graph
外部调用 llama_decode(),内部会进入 llama_context::decode()。这段函数很长,但流程清楚:初始化 batch allocator,处理 memory module,按 n_ubatch 切出 ubatch,调用 process_ubatch(),再把 logits、embedding、backend sampling 结果从对应 backend 异步取回。
主路径可以画成这样:

decode() 不直接用 for 循环跑 token。它先保证 batch 合法,决定哪些 token 需要输出,预留输出缓冲,处理 KV memory slot,再在 ubatch 粒度上执行 graph。建图和执行收束在 process_ubatch()。
源码:llama.cpp src/llama-context.cpp[10],MIT license,tag b9999。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
const auto gparams = graph_params(res, ubatch, mctx, gtype);
if (!graph_reuse_disable && res->can_reuse(gparams)) {
if (cparams.pipeline_parallel) {
ggml_backend_sched_synchronize(sched.get());
}
n_reused++;
} else {
res->reset();
ggml_backend_sched_reset(sched.get());
gf = model.build_graph(gparams);
if (!ggml_backend_sched_alloc_graph(sched.get(), gf)) {
ret = GGML_STATUS_ALLOC_FAILED;
return nullptr;
}
}
res->set_inputs(&ubatch);
const auto status = graph_compute(res->get_gf(), ubatch.n_tokens > 1);
这段代码依次处理 graph topology、graph allocation 和输入写入。gparams 决定图能不能复用;不能复用时要重新 model.build_graph(),并让 backend scheduler 为图分配资源。无论复用还是新建,当前 ubatch 的 token、position、KV memory 等输入都要重新写入 graph tensor。
ggml_cgraph 可以看成一张临时施工图。llama_decode() 收到的 batch 是需求单,ubatch 是这次施工能处理的一小段,build_graph() 把它画成一张 op DAG。graph reuse 复用的是施工图结构;下一次图结构一样时,不重画图纸,只把输入材料换掉。这个比喻只用于解释复用边界:源码里的图是 backend scheduler 能分配 buffer、切分 op 并调用执行接口的数据结构。
llama.cpp 和很多“直接手写推理循环”的差别在这里。它没有把每层 transformer 的执行硬编码进 decode(),而是把当前 ubatch 解释成一张图。后端 scheduler 再决定这张图怎样落到 CPU、GPU 或混合设备上。
8. ggml backend 的边界很窄:graph 进去,status 出来
llama_context::graph_compute() 设置线程数和 CPU threadpool 后,最终调用 ggml_backend_sched_graph_compute_async(sched.get(), gf)。再往下一层,ggml backend 的 dispatch 很薄:同步版本只是先异步提交,再 synchronize;异步版本调用 backend interface 里的 graph_compute。
源码:llama.cpp ggml/src/ggml-backend.cpp[11],MIT license,tag b9999。
1 2 3 4 5 6 7 8 9 10
enum ggml_status ggml_backend_graph_compute(ggml_backend_t backend, struct ggml_cgraph * cgraph){
enum ggml_status err = ggml_backend_graph_compute_async(backend, cgraph);
ggml_backend_synchronize(backend);
return err;
}
enum ggml_status ggml_backend_graph_compute_async(ggml_backend_t backend, struct ggml_cgraph * cgraph){
GGML_ASSERT(backend);
return backend->iface.graph_compute(backend, cgraph);
}
这段代码看起来小,但它决定了扩展方式。新增 backend 时,主路径不应该改 llama_context::decode(),也不应该改 GGUF loader;后端要实现自己的 buffer、op support、graph compute、同步和拷贝接口。llama.cpp 文档里的 backend 目录可以看到许多设备接入说明,例如 CUDA Fedora setup[13]、OpenCL[14]、SYCL[15]、OpenVINO[16]。这些文档是使用和构建层面的入口,源码里的 iface.graph_compute 才是运行时扩展边界。
这层边界有点像把工程外包给不同施工队:主合同只交付施工图,也就是 ggml_cgraph;每个施工队自己决定用什么设备、怎么调度工人、何时同步。只要最后返回 status,主路径不需要知道 CUDA、Metal 或 Vulkan 内部怎么干活。这个比喻对应的是 backend interface 的窄边界,不表示各后端能力完全等价。
这也解释了为什么 backend abstraction 不能做得太“高级”。如果接口设计成“执行一层 LLaMA block”或“执行 attention”,它很快会被模型变体、MoE、multimodal、speculative decoding、不同 KV memory 形态压垮。ggml 的边界是 tensor graph 和 op 支持矩阵,粒度更低,代价是 backend 作者要面对更多底层细节。
9. 一次真实路径串起来看
把前面的代码连起来,一次普通 llama-cli -m model.gguf 推理大致会走下面这条路径。llama-cli 属于工具层,参数解析留在外面;进入 libllama 后,机制才开始沿着加载、建图和后端执行往前推进。

沿着这条路径看,llama.cpp 的设计重心不在某一个“很聪明”的算法,而在几个边界能不能长期保持稳定:
这种分层并不追求教科书式整洁。你在源码里会看到不少兼容分支、模型特例、backend 能力判断和 TODO。它的可取处在于变化被限制在合适的层里。新模型需要补 architecture-specific tensor mapping;新量化类型需要补 ggml type、quant/dequant 和 op 支持;新 backend 需要实现 backend interface;调用方仍然看 llama_model、llama_context 和 llama_decode。
10. 不变量、代价和容易学歪的地方
llama.cpp 的几个不变量很值得带走。
tensor 名字空间必须唯一。llama_model_loader 对重复 tensor name 直接抛错,这让后面的模型构建可以按名字取权重,不用在每个模型文件分支里处理冲突。代价是 GGUF converter 和模型适配层必须严格维护命名规则。
量化类型必须贯穿全链路。GGUF 里的 ggml_type、loader 统计出的 ftype、backend 的 op support、quant block layout 必须是一套系统。只在工具层“支持量化”,运行时再统一还原到高精度,会把本地推理最稀缺的内存优势吃掉。
graph reuse 只能在拓扑一致时发生。process_ubatch() 把可复用条件收敛到 gparams,而且 pipeline parallelism 下复用前要 synchronize,避免覆写还在被 GPU 读取的输入 tensor。这类细节说明,高性能路径不能缓存一切,需要知道缓存什么时候会破坏并发正确性。
backend 抽象不能假装所有设备一样。load_tensors() 里会问设备是否支持 host ptr buffer,是否默认 buffer type,是否需要真实分配;backend docs 里也能看到 CUDA、OpenCL、SYCL、OpenVINO、Snapdragon 等后端各自的安装和能力差异。可移植系统的代码不可能没有分支,关键是分支出现在能力边界,不能散落在业务主路径。
这里也有代价。llama.cpp 的源码对新读者不算友好:模型家族适配文件很多,backend 分支多,GGUF metadata key 和 tensor 命名规则要查着读,llama_context::decode() 也不是一个十几行的主循环。它选择的是“把复杂性留在源码里,换取用户拿到一个小二进制和一个 .gguf 就能跑”。这个选择在本地推理生态里很合理,但不适合照搬到所有服务端系统。一个只跑固定模型、固定 GPU、固定吞吐目标的线上推理服务,完全可以用更专用的 engine 换取更强的端到端优化空间。
11. 可迁移判断清单
• 读一个可移植 runtime,先找格式层、runtime API、计算图和 backend 的边界。边界清楚,比某个单点优化更能解释它为什么能活得久。 • 文件格式不要急着绑定设备策略。GGUF loader 先建立 tensor 索引,把真实分配留给模型和 backend 层,这个顺序值得复用。 • 量化如果是系统能力,就必须进入类型系统。只在导入工具或配置项里出现的量化,通常会在执行路径里泄掉收益。 • backend 抽象粒度宁可低一点,也别绑定某个模型结构。以 graph/op/buffer 为边界,比以“Transformer layer”为边界更能容纳模型演进。 • 能力探测可以接受,关键是把它收在合适边界里。llama.cpp 把 mmap、host ptr buffer、weights usage、op support 放在后端和加载边界,主执行路径才能保持相对稳定。 • 看到 AI 生成的本地推理代码把“加载 GGUF、反量化成 float、for 循环跑每层、最后 softmax”写在一个文件里,要立刻警惕。能跑一个 demo,不等于形成了可维护、可移植、可扩展的 runtime。
12. 参考资料与延伸阅读
• llama.cpp 官方仓库 README: ggml-org/llama.cpptagb9999[1]。• libllama API 定义: include/llama.h[5]。• GGUF Python package 文档: gguf-py/README.md[17]。• GGUF 初始格式讨论: ggml-org/ggml#302[3]。• 量化工具入口: tools/quantize/quantize.cpp[6]。• ggml backend 接口: ggml/src/ggml-backend.cpp[11]。• 后端构建与接入文档入口: docs/backend[18]。
当前文章:第 03 篇《llama.cpp源码分析:GGUF、量化与ggml执行图》。
下一篇预告:第 04 篇《TensorRT-LLM源码分析:构建期优化与运行时执行》,继续看一个更偏服务端和编译器式优化的推理系统。
更新时间:2026 年 7 月 24 日
引用链接
[1] `ggml-org/llama.cpp`: https://github.com/ggml-org/llama.cpp/tree/b9999[2] `gguf-py`: https://github.com/ggml-org/llama.cpp/tree/b9999/gguf-py[3] ggml-org/ggml#302: https://github.com/ggml-org/ggml/pull/302[4] README: https://github.com/ggml-org/llama.cpp/tree/b9999#description[5] `include/llama.h`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/include/llama.h[6] `tools/quantize/quantize.cpp`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/tools/quantize/quantize.cpp[7] `src/llama.cpp`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/src/llama.cpp[8] `src/llama-model-loader.cpp`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/src/llama-model-loader.cpp[9] `src/llama-model.cpp`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/src/llama-model.cpp[10] `src/llama-context.cpp`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/src/llama-context.cpp[11] `ggml/src/ggml-backend.cpp`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/ggml/src/ggml-backend.cpp[12] `ggml/src/ggml-common.h`: https://github.com/ggml-org/llama.cpp/blob/47c786924ad1ab7e91da2cdc72fcdb563780c2bd/ggml/src/ggml-common.h[13] CUDA Fedora setup: https://github.com/ggml-org/llama.cpp/blob/b9999/docs/backend/CUDA-FEDORA.md[14] OpenCL: https://github.com/ggml-org/llama.cpp/blob/b9999/docs/backend/OPENCL.md[15] SYCL: https://github.com/ggml-org/llama.cpp/blob/b9999/docs/backend/SYCL.md[16] OpenVINO: https://github.com/ggml-org/llama.cpp/blob/b9999/docs/backend/OPENVINO.md[17] `gguf-py/README.md`: https://github.com/ggml-org/llama.cpp/blob/b9999/gguf-py/README.md[18] `docs/backend`: https://github.com/ggml-org/llama.cpp/tree/b9999/docs/backend
夜雨聆风