🦙 llama.cpp 图解教程llama.cpp Visual Guide 第四部分 · llama 推理内部Part 4 · Inside llama inference 15 / 40
第四部分 · llama 推理内部Part 4 · Inside llama inference

架构与超参Architecture & hyperparameters

上一课 loader 把权重读成了一张"带名字的张量清单",可这些张量怎么知道自己属于哪种架构(llama?qwen2?)、每层有哪些部件?这一课讲两样东西:llama-arch(架构标识 + 键名约定 + 张量名约定三套表)和 llama-hparams(几层、多宽、每层几个头)——它们合起来,是把"一堆张量"翻译成"一个具体可建图的模型"的说明书

为什么把这两件事单拎一课?因为它们是 llama.cpp 能用一套代码读懂几十种模型的秘密。同样是 transformer,llama 和 qwen2 的差别,本质上就藏在"用哪套约定、几层多宽、张量怎么命名"里。 搞懂这一课,你就明白新模型是怎么"插进"这个引擎的。

🔌 生活类比
架构与超参像一套建筑图纸 + 规格表:图纸(llm_arch + 张量命名约定)说明"这是哪种楼、每层有哪些构件、各叫什么名";规格表(hparams)给出具体尺寸—— "几层、多宽、每层几个注意力头"。上一课的 loader 就像备好了一堆贴着标签的建材,而这一课的图纸和规格表,告诉它按什么名字提哪块料、按什么尺寸搭起来

架构标识:llm_arch

一切从一个问题开始:拿到一个模型,怎么知道它是 llama 还是 qwen2?答案在 L13 讲过的那个 GGUF 元数据键 general.architecture 里。它是一个字符串(比如 "llama"), loader 读出它、在一张名字表里一查,就得到对应的架构枚举 LLM_ARCH_LLAMA。这个枚举一旦定下,后面用哪套键名、哪套张量名、调哪个建图函数(L16),就全都定了。

general.architecture
GGUF 里的字符串
= "llama"
->
LLM_ARCH_LLAMA
查 LLM_ARCH_NAMES
得到架构枚举
->
一套约定
KV 键 / 张量名 / 建图(L16)

落到源码(简化自 src/llama-arch.{h,cpp}):

// 简化自 src/llama-arch.h / src/llama-arch.cpp
enum llm_arch { LLM_ARCH_LLAMA, LLM_ARCH_QWEN2, /* ... 几十种 ... */ };
// LLM_ARCH_NAMES: 枚举 <-> 字符串
{ LLM_ARCH_LLAMA, "llama" }, { LLM_ARCH_QWEN2, "qwen2" }, /* ... */

这张 LLM_ARCH_NAMES 表是双向的桥:写文件时(L02 转换脚本)把架构枚举翻成字符串写进 GGUF,读文件时把字符串翻回枚举。一个小小的字符串,就是整个后续流程的总开关—— 它一变,loader 找的键、建图用的函数全跟着变。所以你可以把 general.architecture 理解成模型在对引擎说:"请按 llama 这套规矩来对待我。"

为什么要用一个枚举、而不是到处用字符串比较?因为枚举又快又不易写错,且能当 switch / 表格的下标。llama.cpp 里大量"按架构分情况"的逻辑——读哪些超参、张量怎么命名、建图怎么拼——都靠这个枚举来分流。 把"我是谁"收敛成一个枚举值,是让后面所有"按架构区别对待"的代码都能写得整齐的前提。

🌍 宏观理解
顺带感受一下这套机制的容量:llama.cpp 支持的架构早已是几十种——llama、qwen、mistral、phi、gemma、deepseek、stablelm…… 全都靠这一个 llm_arch 枚举区分。新模型层出不穷,可它们绝大多数都是 transformer 的变体、差异有限;于是"再多一种架构"在引擎眼里,往往只是枚举里多一个值、表里多几行。这种"用一个枚举撑起一个生态"的容量,正是表驱动设计的威力。

除了 general.architecture,GGUF 头里还有一批 general.* 的通用元信息(L13 提过):模型名、整体量化档位、量化版本等。它们和架构枚举一起,构成了"这到底是个什么模型"的完整自我介绍。 loader 一上来读的,正是这批通用信息加上架构相关的超参——前者认出"是谁",后者量出"多大"。

这条"字符串 <-> 枚举"的桥也解释了 L02 转换脚本在做的事之一:把一个 HuggingFace 模型转成 GGUF 时,脚本要判断它是哪种架构、把对应的字符串(如 "llama")写进 general.architecture。 写入方和读取方共用同一张 LLM_ARCH_NAMES,一写一读才能严丝合缝。这也是为什么有时一个全新架构的模型,需要先给 llama.cpp 加上对它的支持,转换脚本才认得、才转得出来。

超参 llama_hparams

知道了是哪种架构,还得知道具体尺寸:这个 llama 是 7B 还是 70B?多少层、多宽、每层几个头?这些数叫超参(hyperparameters),装在 llama_hparams 里,全部由上一课的 get_key 从 GGUF 的 KV 读出。

超参含义
n_embd隐藏维度(一个 token 向量多宽)
n_layer()层数(多少个 transformer block)
n_head(il)第 il 层的注意力头数
n_head_kv(il)第 il 层的 KV 头数(GQA 时 < n_head)
n_ff(il)第 il 层 FFN 的中间维度
rope_freq_base_trainRoPE 的频率基(位置编码,L16)
// 简化自 src/llama-hparams.h
struct llama_hparams {
    uint32_t n_embd;  uint32_t n_ctx_train;  float rope_freq_base_train;  float f_norm_rms_eps;
    std::array<uint32_t, LLAMA_MAX_LAYERS> n_head_arr, n_head_kv_arr, n_ff_arr; // 按层存
    uint32_t n_layer() const;  uint32_t n_head(uint32_t il = 0) const;        // 访问器, 不是字段!
};
⚠ 两个易踩的坑
n_layer()n_head(il) 这些不是普通字段,而是访问器方法(注意有括号)——现代架构里不同层的头数、注意力类型可能不一样(比如 GQA、滑窗层),头数按层存进 n_head_arr 这样的数组,取的时候要带层号 il;把它当成定值字段去用,迟早出错。
n_vocab(词表大小)不在 llama_hparams 里!它属于分词器,来自 llama_vocab::n_tokens()(L20 会讲)——记住:hparams 管的是"网络的形状",词表是另一码事,归分词器管。

这些超参一旦读出来,就成了整个推理的"尺寸基准":建图(L16)时按 n_layer() 决定堆几层、按 n_head(il) 切多少个注意力头、按 n_embd 定各处矩阵的形状; KV cache(L19)按层数和 KV 头数算该开多大。可以说,hparams 是把"一个抽象的 transformer"具体化成"这一个模型"的那组数字。

顺便厘清"参数量"和超参的关系。我们常说的 7B、70B,指的是模型权重里浮点数的总个数(70 亿、700 亿);而这个总数,正是由超参算出来的——大致是 n_layer × 每层各权重矩阵尺寸之和, 而每个矩阵的尺寸又由 n_embdn_ff 等决定。所以超参不是一堆孤立的数字,它们共同决定了模型有多大。读懂超参,你就能从一个模型的几个数,估出它要吃多少显存。

🔬 细节 / 源码对应
表里没列全的超参还有不少,各有用处:n_ctx_train 是模型训练时的上下文长度(你能开多长上下文的参考上限);f_norm_rms_eps 是 RMSNorm 里防止除零的小常数(L11 的归一化用到);n_rot 是 RoPE 实际旋转的维数。这些数看着琐碎,却个个都会在建图(L16)时被某个算子精确用到——少一个、错一个,算出来的就不是这个模型了。

再说一句那个默认参数 il = 0:它让"对每层都一样"的简单模型用起来很省事——不传层号,默认取第 0 层(也就是所有层)的值。所以接口虽然是"按层取",对老实的同构模型并不啰嗦。 这是个体贴的设计:复杂情况能表达,简单情况不添乱。

这里特别值得记住 n_head_kv 的意义:它直接决定 KV cache 有多大。GQA 的思路就是让多个 Q 头共享一组 K/V,于是 KV 头数远少于 Q 头数,KV cache 也就成倍变小(L19 会细讲)。 所以读一个模型的超参时,n_headn_head_kv 的比值,几乎就告诉了你"这个模型对长上下文友不友好"。一个看似不起眼的超参,背后是显存与速度的大账。

张量命名约定

最后一块拼图:loader 读出的张量怎么按名字对上模型结构?靠一套命名约定。每个张量在文件里都有名字,而这些名字不是随便起的,遵循 LLM_TENSOR_NAMES 定义的模板。

张量名模板(LLM_TENSOR_NAMES):按部件 + 层号命名,建图时照名取权重
名字token_embdblk.0.attn_qblk.0.ffn_gateoutput_norm

看这几个名字就懂了规律:token_embd 是词嵌入表(开头那层),blk.0.attn_q 是第 0 层的注意力 Q 投影、blk.0.ffn_gate 是第 0 层 FFN 的门控、output_norm 是最后的输出归一。 名字里 blk.%d%d 会被层号填进去——这正是 n_layer() 派上用场的地方:循环 0 到 n_layer(),每层按模板拼出该层各张量的名字。

名字由一个叫 LLM_TN 的小构造器拼出来(它的 tn(...) 调用,把"部件 + 后缀 + 层号"组装成完整张量名)。建图(L16)时,每要一个权重,就用这个构造器拼出名字、去上一课的 weights_map 里查—— 这就把"加载"和"建图"两课用名字这根线串了起来:loader 按名字存,建图按名字取,中间靠 LLM_TENSOR_NAMES 这份共同约定对齐。

🌍 宏观理解
这也回答了上一课留的悬念——为什么 weights_map 要按名字索引。因为名字是稳定的契约:换个导出工具、张量排列变了也不怕,只要名字这套约定不变,建图就总能精确取到它要的那块权重。名字这层抽象,把"权重物理上躺在哪"和"逻辑上是哪个部件"彻底解耦了。

把一层 transformer 的张量名列全,规律就更清楚了:注意力部分有 attn_q/attn_k/attn_v/attn_output 四个投影、加一个 attn_norm;FFN 部分有 ffn_gate/ffn_up/ffn_down 三个矩阵、加一个 ffn_norm。 每一层都按这套模板复制一遍,前面加 blk.层号.。看懂这张"一层有哪些权重"的清单,你就看懂了 transformer 一个 block 的全部可学习参数。

为什么用 . 点号分层级命名(blk.0.attn_q.weight)?因为这天然形成一棵层级树blk 下是各层、层下是各部件、部件下是 weight/bias。这种命名既清晰、又方便按前缀批量匹配—— 比如想找第 0 层的所有权重,匹配 blk.0. 前缀即可。一个好的命名约定,不只是"起个名",而是把结构信息编码进了名字本身。

这套命名还是跨架构通用的:不管 llama 还是 qwen2,第 0 层的注意力 Q 投影都叫 blk.0.attn_q。正因为大家共享同一套名字,针对张量的通用工具(量化、转换、可视化)才能不区分架构地处理任意模型。

🔬 细节 / 源码对应
再补一点 LLM_TN 的细节:它拼出的完整名字通常还带个后缀,区分 .weight.bias——同一个部件可能有权重、也可能有偏置。所以 tn(LLM_TENSOR_ATTN_Q, "weight", il) 拼出的是 blk.il.attn_q.weight。把"部件名模板 + 后缀 + 层号"三者交给一个构造器统一拼,既避免了到处手写字符串容易出的错,也让"改个命名规则"只需动一处。

顺便提一句:有些张量是可选的——比如不少现代模型的线性层没有 bias,那 .bias 那个张量在文件里就根本不存在。建图时按架构知道"这层该有哪些张量",缺的可选项就跳过。 命名约定加上"哪些必需、哪些可选"的知识,才完整描述了一个架构的张量构成。

自描述如何在架构层兑现

把三样东西连起来看,L13 说的"自描述"就在架构层完整兑现了:general.architecture 选定 arch;带 %s 的 KV 键(如 llama.block_count)用架构名填模板、由 get_key 读出超参; 张量按 LLM_TENSOR_NAMES 命名一一对上。三套约定一咬合,loader 读出的"一堆张量"就成了"一个有名有姓、有形有状的具体模型",随时可以交给 L16 建图。

值得回味的是这种设计的"表驱动"味道:架构是一张名字表、键是一张键名表、张量是一张张量名表。引擎的主干代码不写死任何一种模型,而是"照表办事"。于是支持一个新模型,多半不是改引擎,而是往这几张表里加几行 + 写一份建图(L16)。

架构名表

LLM_ARCH_NAMES"llama" -> LLM_ARCH_LLAMA。先认出"是什么模型"。

键名表

LLM_KVllama.block_count 等 -> 由 get_key 读成超参。再量出"有多大"。

张量名表

LLM_TENSOR_NAMESblk.N.attn_q 等 -> 对上每块权重。最后"零件对号入座"。

设想一下如果用表驱动会怎样:每支持一种新模型,就得在引擎主干里写一堆 if (arch == "llama") ... else if (arch == "qwen2") ... 的分支,读超参、命名张量、建图处处都要改。模型一多,这些分支就会织成一张谁也不敢动的网。 表驱动把这些差异从"散落在代码各处的 if"收进"集中的几张表 + 一份建图文件",于是主干代码读起来始终是"照表办事",清清爽爽。

这套"识别架构 -> 读超参 -> 按名取张量"的三步,其实是任何"通用模型加载器"都绕不开的骨架:先搞清是什么、再量出多大、最后把零件对号入座。llama.cpp 把这三步做得极其干净,正是它能在短时间内追上一个又一个新模型的工程根基。 下一课,我们就拿着这份"图纸 + 规格表 + 零件清单",真正动手把它们拼成一张能算的前向计算图。

把这一课和上一课连起来看,会发现一条清晰的主线:L14 的 loader 把字节变成"带名字的张量",L15 的 arch/hparams 给这些张量配上"图纸和规格"。到这里,模型已经从"磁盘上的一个文件"变成了"内存里一个有名有姓、知道自己几层多宽的对象"—— 只差最后一步:把这些零件按图纸真正拼成一张能算的网络。那正是下一课的事。

1 为什么 n_head 写成 n_head(il) 带层号? 点击展开

因为现代架构里,不同层的注意力配置可能不一样。最常见的是 GQA(分组查询注意力):Q 头多、KV 头少,于是 n_head_kv(il) 会小于 n_head(il),用来省 KV cache(L19)。

还有些架构是混合的:某些层用全注意力、某些层用滑动窗口,各层的头数、窗口大小都可能不同。要表达这种"逐层不同",最干净的办法就是把这些数存成按层的数组n_head_arr 等),取的时候带上层号 il

所以 n_head(il) 是个方法、不是字段——它背后从数组里按层号取值。对大多数老实的同构模型,每层都一样,il 取默认 0 即可;但接口设计成按层取,才容得下那些"逐层不同"的新架构。这是个"为通用性留余地"的典型取舍。

2 加一个新架构,要改哪几处? 点击展开

大致四步,且大多是"填表":① 在 enum llm_arch 里加一项、在 LLM_ARCH_NAMES 里加它的名字;② 实现这架构的超参读取(从 KV 把它特有的几个超参读进 hparams);③ 把它用到的张量在张量名表里登记;④ 写一份建图(L16,src/models/<arch>.cpp)。

关键是第④步往往很轻:因为绝大多数架构用的是同一批积木(注意力、FFN、归一化),新架构多半只是"换个拼法",能复用现成的 build_attn/build_ffn(L16)。只有遇到真正新颖的结构,才需要补一两个新算子(L11)。

这就是为什么 llama.cpp 能跟上层出不穷的新模型:大部分"新架构"在工程上其实是"填几张表 + 复用积木",引擎主干纹丝不动。把"模型的多样性"收进表和建图文件,把"不变的机制"留在主干——这是这套设计最省力的地方。

3 张量命名约定为什么这么重要? 点击展开

因为它是三方共享的契约。写入方(L02 的转换脚本)按这套名字把权重写进 GGUF;读取方(L14 的 loader)按名字建 weights_map;建图方(L16)按名字取权重。三方只要都遵守同一套 LLM_TENSOR_NAMES,就能严丝合缝地对上。

正因为有这套约定,"权重"和"代码"才解耦了:你换个工具导出、张量在文件里顺序不同,都不影响——只要名字对得上,建图就能取到对的料。名字成了模型各部件的"身份证",比"第几个张量"这种脆弱的下标稳得多。

它也呼应了 L13 的自描述精神:模型不光自带超参、词表,连"每块权重是哪个部件"都用名字标得明明白白。读懂了命名约定,你再去看任何模型的张量列表,都能一眼认出哪个是第几层的什么——这是读 llama.cpp 模型的一项基本功。

✅ 关键要点
  • llm_arch 由 GGUF 的 general.architecture 选定("llama" -> LLM_ARCH_LLAMA),决定后续用哪套约定与建图。
  • llama_hparams 给规格(n_embd / n_layer() / n_head(il) ...);n_layer()/n_head(il)方法(按层),n_vocab 不在其中(来自 vocab,L20)。
  • 张量按 LLM_TENSOR_NAMES 命名(token_embd/blk.N.attn_q/output_norm),由 LLM_TNtn() 拼出。
  • 三者咬合 = 自描述兑现:选 arch、填模板读超参、按名字对张量,"一堆张量"成"具体模型"。
  • 加新架构 ≈ 往这几张表加几行 + 写一份建图(L16),引擎主干不动。
💡 设计洞察
把"模型长什么样"编码成三张表——架构名表、键名表、张量名表——于是一套引擎代码能读懂几十种架构。这是典型的表驱动设计:把"会变的部分"(不同模型的差异)集中进数据(表),让"不变的机制"(读取、建图、执行)留在代码里。 它和 L05/L12 那条"结构不变、可换的部分集中起来"的思路一脉相承——只不过这次可换的不是数据类型,而是整个模型架构。读懂了这一课,你就明白了 llama.cpp"海纳百川"的底层套路。

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

1. GGUF 里哪个 KV 决定按哪套架构建图?
  1. general.name
  2. general.architecture(如 "llama"、"qwen2")
  3. version
  4. general.file_type
看答案与解析 点击展开
答案:B。loader 读出 general.architecture 字符串,在 LLM_ARCH_NAMES 里查得 LLM_ARCH_LLAMA 等枚举;这个枚举决定后续用哪套 KV/张量约定与建图函数(L16)。
2. 为什么 hparams 把头数写成 n_head(il) 带层号?
  1. 不同层的头数/注意力类型可能不同(GQA、滑窗),按层取最通用
  2. 为了让推理更快
  3. 随机决定的
  4. 写错了,应该是字段
看答案与解析 点击展开
答案:A。现代架构里各层注意力配置可能不同(GQA 让 KV 头少于 Q 头、混合架构逐层不同),所以头数按层存进 n_head_arr,n_head(il) 是按层取值的访问器方法(不是字段)。
3. 加载器怎么把文件里的张量对应到模型结构?
  1. 按文件里的张量顺序
  2. 靠 LLM_TENSOR_NAMES 的命名约定(token_embd / blk.N.attn_q ...)按名字在 weights_map 里查
  3. 按张量大小排序
  4. 随机匹配
看答案与解析 点击展开
答案:B。张量名遵循 LLM_TENSOR_NAMES 模板(blk.%d 里填层号),由 LLM_TN 的 tn() 拼出;建图按名字去 weights_map 取权重。名字是稳定契约,跨工具、跨分片都不怕。
💭 发散思考(没有标准答案,动手或动脑想想)
  • llm_arch、llama_hparams、LLM_TENSOR_NAMES 三者各管什么?它们怎么合起来把“一堆张量”变成“一个具体可建图的模型”?

Last lesson the loader read the weights into a "name-indexed tensor list", but how do those tensors know which architecture they belong to (llama? qwen2?), and which parts each layer has? This lesson covers two things: llama-arch (architecture identity + key-name convention + tensor-name convention - three tables) and llama-hparams (how many layers, how wide, how many heads per layer) - together the blueprint that turns "a pile of tensors" into "a concrete, graph-able model".

Why a whole lesson on these two? Because they are the secret behind llama.cpp reading dozens of models with one codebase. Both are transformers, yet the difference between llama and qwen2 essentially hides in "which conventions, how many layers/wide, how tensors are named". Get this lesson and you understand how a new model "plugs into" the engine.

🔌 Analogy
Architecture and hyperparameters are like a blueprint + spec sheet: the blueprint (llm_arch + tensor naming convention) says "which kind of building, which parts per floor, what each is named"; the spec sheet (hparams) gives the dimensions - "how many floors, how wide, how many attention heads per floor". Last lesson's loader is like a stack of labeled materials ready to go, and this lesson's blueprint and spec sheet tell it which part to fetch by what name, and at what size to assemble.

Architecture identity: llm_arch

It all starts with one question: given a model, how do you know it is llama or qwen2? The answer is in that GGUF metadata key from L13, general.architecture. It is a string (e.g. "llama"); the loader reads it, looks it up in a name table, and gets the matching architecture enum LLM_ARCH_LLAMA. Once that enum is fixed, which key names, which tensor names, and which graph builder (L16) to use are all fixed.

general.architecture
string in GGUF
= "llama"
->
LLM_ARCH_LLAMA
look up LLM_ARCH_NAMES
get the arch enum
->
one set of conventions
KV keys / tensor names / graph(L16)

In source (simplified from src/llama-arch.{h,cpp}):

// simplified from src/llama-arch.h / src/llama-arch.cpp
enum llm_arch { LLM_ARCH_LLAMA, LLM_ARCH_QWEN2, /* ... dozens ... */ };
// LLM_ARCH_NAMES: enum <-> string
{ LLM_ARCH_LLAMA, "llama" }, { LLM_ARCH_QWEN2, "qwen2" }, /* ... */

This LLM_ARCH_NAMES table is a two-way bridge: on write (L02's conversion script) it turns the arch enum into a string written into GGUF; on read it turns the string back into an enum. A tiny string is the master switch for everything downstream - change it and the keys the loader seeks and the function used to build the graph all change with it. So you can read general.architecture as the model telling the engine: "please treat me by the llama rules."

Why an enum rather than string comparisons everywhere? Because an enum is fast, hard to mistype, and can index a switch / a table. The many "branch by architecture" decisions in llama.cpp - which hyperparameters to read, how tensors are named, how the graph is assembled - all route off this enum. Collapsing "who am I" into one enum value is what lets all the later "treat each architecture differently" code stay tidy.

🌍 Big picture
Get a feel for this mechanism's capacity: llama.cpp already supports dozens of architectures - llama, qwen, mistral, phi, gemma, deepseek, stablelm... - all distinguished by this one llm_arch enum. New models keep appearing, but the vast majority are transformer variants with limited differences; so "one more architecture" is, to the engine, often just one more enum value and a few more table rows. That capacity of "one enum holding up an ecosystem" is the power of table-driven design.

Beyond general.architecture, the GGUF header carries a set of generic general.* metadata (mentioned in L13): model name, overall quantization level, quantization version, and so on. Together with the architecture enum, they form a complete self-introduction of "what this model even is". What the loader reads first is exactly this generic info plus the architecture-specific hyperparameters - the former recognizes "who", the latter measures "how big".

This "string <-> enum" bridge also explains one thing L02's conversion script does: when converting a HuggingFace model to GGUF, the script must decide which architecture it is and write the matching string (e.g. "llama") into general.architecture. Writer and reader share the same LLM_ARCH_NAMES, so write and read mesh perfectly. This is also why a brand-new architecture sometimes needs support added to llama.cpp first, before the conversion script recognizes and can convert it.

Hyperparameters: llama_hparams

Knowing the architecture, you still need the concrete dimensions: is this llama 7B or 70B? How many layers, how wide, how many heads per layer? These numbers are the hyperparameters, held in llama_hparams, all read from the GGUF KVs by last lesson's get_key.

hyperparametermeaning
n_embdhidden dimension (how wide a token vector is)
n_layer()layer count (how many transformer blocks)
n_head(il)attention head count of layer il
n_head_kv(il)KV head count of layer il (< n_head under GQA)
n_ff(il)FFN intermediate dim of layer il
rope_freq_base_trainRoPE frequency base (position encoding, L16)
// simplified from src/llama-hparams.h
struct llama_hparams {
    uint32_t n_embd;  uint32_t n_ctx_train;  float rope_freq_base_train;  float f_norm_rms_eps;
    std::array<uint32_t, LLAMA_MAX_LAYERS> n_head_arr, n_head_kv_arr, n_ff_arr; // stored per layer
    uint32_t n_layer() const;  uint32_t n_head(uint32_t il = 0) const;        // accessors, not fields!
};
⚠ Two easy traps
(1) n_layer(), n_head(il) are not plain fields but accessor methods (note the parentheses) - in modern architectures different layers may have different head counts or attention types (GQA, sliding-window), so head counts are stored per layer in arrays like n_head_arr, fetched with a layer index il; treat it as a constant field and you will eventually be wrong.
(2) n_vocab (vocab size) is not in llama_hparams! It belongs to the tokenizer, from llama_vocab::n_tokens() (L20) - remember: hparams govern "the shape of the network"; the vocab is a separate matter, owned by the tokenizer.

Once read, these hyperparameters become the inference's "size baseline": graph-building (L16) stacks layers by n_layer(), splits heads by n_head(il), sets matrix shapes by n_embd; the KV cache (L19) sizes itself by layer count and KV head count. In short, hparams are the set of numbers that turn "an abstract transformer" into "this particular model".

While we are at it, untangle "parameter count" from hyperparameters. The 7B, 70B we casually say refers to the total count of floats in the model's weights (7 billion, 70 billion); and that total is computed from the hyperparameters - roughly n_layer x the sum of each layer's weight-matrix sizes, where each matrix's size is set by n_embd, n_ff, etc. So hyperparameters are not isolated numbers; together they determine how big the model is. Read the hyperparameters and you can estimate a model's VRAM appetite from just a few numbers.

🔬 Details / source
Plenty of hyperparameters the table omits each have their use: n_ctx_train is the context length the model was trained at (a reference ceiling for how long a context you can open); f_norm_rms_eps is the small constant in RMSNorm that avoids divide-by-zero (used by L11's normalization); n_rot is the number of dimensions RoPE actually rotates. These look trivial, yet each is used precisely by some operator at graph time (L16) - miss one or get one wrong, and what you compute is no longer this model.

One more word on that default il = 0: it makes the common "same for every layer" model effortless - pass no index, and it defaults to layer 0's (i.e. every layer's) value. So although the interface is "fetch per layer", it is not verbose for honest homogeneous models. A considerate design: it can express the complex case without cluttering the simple one.

Worth specially remembering is what n_head_kv means: it directly decides how big the KV cache is. GQA's idea is to let several Q heads share one set of K/V, so KV heads are far fewer than Q heads, and the KV cache shrinks several-fold (L19 covers this). So reading a model's hyperparameters, the ratio of n_head to n_head_kv nearly tells you "how friendly this model is to long context". A seemingly minor hyperparameter, with a big VRAM-and-speed account behind it.

Tensor naming convention

The last piece: how do the loader's tensors line up by name with the model structure? Via a naming convention. Every tensor in the file has a name, and these names are not arbitrary - they follow the templates defined in LLM_TENSOR_NAMES.

tensor-name templates (LLM_TENSOR_NAMES): named by part + layer index; graph-building fetches weights by name
nametoken_embdblk.0.attn_qblk.0.ffn_gateoutput_norm

These names reveal the pattern: token_embd is the token-embedding table (the first layer), blk.0.attn_q is layer 0's attention Q projection, blk.0.ffn_gate is layer 0's FFN gate, output_norm is the final output norm. The %d in blk.%d is filled with the layer index - exactly where n_layer() earns its keep: loop 0 to n_layer(), and per layer build that layer's tensor names from the templates.

Names are built by a small constructor called LLM_TN (its tn(...) call assembles "part + suffix + layer index" into a full tensor name). At graph time (L16), each time a weight is needed, this constructor builds the name and looks it up in last lesson's weights_map - which strings together the "loading" and "graph" lessons via names: the loader stores by name, the graph fetches by name, aligned through the shared LLM_TENSOR_NAMES convention.

🌍 Big picture
This also answers last lesson's cliffhanger - why weights_map is keyed by name. Because a name is a stable contract: a different export tool, a different tensor order, no problem - as long as the naming convention holds, graph-building can always fetch the exact weight it wants. That layer of naming fully decouples "where a weight physically sits" from "which part it logically is".

List a transformer layer's tensor names in full and the pattern is clearer still: the attention part has four projections attn_q/attn_k/attn_v/attn_output plus an attn_norm; the FFN part has three matrices ffn_gate/ffn_up/ffn_down plus an ffn_norm. Every layer replicates this template, prefixed with blk.<index>.. Understand this "what weights a layer has" list and you understand all the learnable parameters of one transformer block.

Why dot-separated hierarchical names (blk.0.attn_q.weight)? Because it naturally forms a hierarchy tree: under blk are the layers, under a layer the parts, under a part weight/bias. Such naming is both clear and convenient for prefix matching in bulk - to find all of layer 0's weights, match the blk.0. prefix. A good naming convention is not just "giving a name"; it encodes structural information into the name itself.

This naming is also cross-architecture: whether llama or qwen2, layer 0's attention Q projection is named blk.0.attn_q. Because everyone shares one naming set, generic tensor tools (quantization, conversion, visualization) can process any model without caring about architecture.

🔬 Details / source
A bit more LLM_TN detail: the full name it builds usually carries a suffix too, distinguishing .weight from .bias - a part may have a weight and possibly a bias. So tn(LLM_TENSOR_ATTN_Q, "weight", il) builds blk.il.attn_q.weight. Handing "part-name template + suffix + layer index" to one constructor avoids the errors of hand-writing strings everywhere, and means "changing a naming rule" touches just one place.

By the way: some tensors are optional - many modern models' linear layers have no bias, so the .bias tensor simply does not exist in the file. At graph time, the architecture knows "which tensors this layer should have", and missing optional ones are skipped. The naming convention plus knowledge of "which are required, which optional" together fully describe an architecture's tensor makeup.

How self-description cashes out at the architecture layer

Connect the three things and L13's "self-description" cashes out fully at the architecture layer: general.architecture selects the arch; %s keys (like llama.block_count) fill the template with the arch name and are read by get_key into hyperparameters; tensors line up by their LLM_TENSOR_NAMES names. Once the three conventions mesh, the loader's "pile of tensors" becomes "a concrete model with names, shapes, and sizes", ready to hand to L16 for graph-building.

What is worth savoring is the table-driven flavor of this design: architecture is a name table, keys are a key-name table, tensors are a tensor-name table. The engine's trunk hard-codes no single model; it "acts by the tables". So supporting a new model is mostly not editing the engine, but adding a few rows to these tables + writing one graph builder (L16).

Arch-name table

LLM_ARCH_NAMES: "llama" -> LLM_ARCH_LLAMA. First recognize "which model".

Key-name table

LLM_KV: llama.block_count etc. -> read into hyperparameters by get_key. Then measure "how big".

Tensor-name table

LLM_TENSOR_NAMES: blk.N.attn_q etc. -> line up each weight. Finally "slot the parts into place".

Imagine if it were not table-driven: every new model would mean a pile of if (arch == "llama") ... else if (arch == "qwen2") ... branches in the engine trunk - reading hyperparameters, naming tensors, building graphs, all needing edits everywhere. With enough models, those branches weave into a web no one dares touch. Table-driven design gathers these differences from "ifs scattered through the code" into "a few centralized tables + one graph file", so the trunk code always reads as "act by the tables" - clean and clear.

These three steps - "recognize the architecture -> read hyperparameters -> fetch tensors by name" - are really the inescapable skeleton of any "general model loader": first figure out what it is, then measure how big, finally slot the parts into place. llama.cpp does these three exceptionally cleanly, the engineering basis for it catching up to one new model after another so quickly. Next lesson, carrying this "blueprint + spec sheet + parts list", we actually assemble them into a runnable forward compute graph.

Connecting this lesson with the last reveals a clear through-line: L14's loader turns bytes into "named tensors", and L15's arch/hparams give those tensors "a blueprint and spec". By here, the model has gone from "a file on disk" to "an in-memory object with names, knowing how many layers and how wide it is" - one step short: actually assembling these parts by the blueprint into a runnable network. That is exactly the next lesson.

1 Why is n_head written as n_head(il) with a layer index? Click to expand

Because in modern architectures different layers may have different attention configs. The most common is GQA (grouped-query attention): many Q heads, few KV heads, so n_head_kv(il) is smaller than n_head(il), used to shrink the KV cache (L19).

Some architectures are hybrid: some layers use full attention, others sliding windows, with different head counts and window sizes per layer. To express this "per-layer difference", the cleanest way is to store these numbers as per-layer arrays (n_head_arr etc.), fetched with the layer index il.

So n_head(il) is a method, not a field - behind it, a value is pulled from an array by layer index. For most honest homogeneous models every layer is the same and il defaults to 0; but designing the interface to fetch per layer is what accommodates those "per-layer different" new architectures. A classic "leave room for generality" trade-off.

2 What does adding a new architecture touch? Click to expand

Roughly four steps, mostly "filling tables": (1) add an entry to enum llm_arch and its name to LLM_ARCH_NAMES; (2) implement this architecture's hyperparameter reading (read its specific hyperparameters from the KVs into hparams); (3) register the tensors it uses in the tensor-name table; (4) write one graph builder (L16, src/models/<arch>.cpp).

The key is that step (4) is often light: because the vast majority of architectures use the same building blocks (attention, FFN, normalization), a new architecture is mostly "a different arrangement", reusing the existing build_attn/build_ffn (L16). Only a truly novel structure needs one or two new operators (L11).

This is why llama.cpp keeps up with the endless stream of new models: most "new architectures" are, in engineering terms, "fill a few tables + reuse blocks", with the engine trunk untouched. Folding "model diversity" into tables and graph files, and keeping "the invariant machinery" in the trunk - that is where this design saves the most effort.

3 Why is the tensor naming convention so important? Click to expand

Because it is a three-party contract. The writer (L02's conversion script) writes weights into GGUF by these names; the reader (L14's loader) builds weights_map by name; the graph builder (L16) fetches weights by name. As long as all three honor the same LLM_TENSOR_NAMES, they line up perfectly.

Because of this convention, "weights" and "code" are decoupled: export with a different tool, a different tensor order in the file - none of it matters, as long as names match, graph-building fetches the right material. A name becomes each part's "ID card", far steadier than the fragile "which-th tensor" index.

It also echoes L13's self-description spirit: a model carries not only its hyperparameters and vocab but even labels "which part each weight is" clearly by name. Read the naming convention and, looking at any model's tensor list, you can recognize at a glance which is what of which layer - a basic skill for reading llama.cpp models.

✅ Key points
  • llm_arch is selected by GGUF's general.architecture ("llama" -> LLM_ARCH_LLAMA), deciding the conventions and graph builder used.
  • llama_hparams gives the spec (n_embd / n_layer() / n_head(il) ...); n_layer()/n_head(il) are methods (per layer), and n_vocab is not among them (from vocab, L20).
  • Tensors are named by LLM_TENSOR_NAMES (token_embd/blk.N.attn_q/output_norm), built by LLM_TN's tn().
  • The three meshing = self-description cashed out: pick arch, fill templates to read hyperparameters, line up tensors by name; "a pile of tensors" becomes "a concrete model".
  • Adding a new architecture ~= add a few rows to these tables + write one graph builder (L16), engine trunk untouched.
💡 Design insight
Encoding "what a model looks like" into three tables - an arch-name table, a key-name table, a tensor-name table - lets one engine codebase read dozens of architectures. This is classic table-driven design: fold "the parts that vary" (differences between models) into data (tables), and keep "the invariant machinery" (reading, graph-building, execution) in code. It is of a piece with that L05/L12 idea of "structure fixed, the swappable parts gathered together" - only this time what is swappable is not a data type but an entire model architecture. Get this lesson and you understand llama.cpp's underlying recipe for "taking in all rivers".

🧪 Self-test - think about the design

1. Which GGUF KV decides which architecture's graph to build?
  1. general.name
  2. general.architecture (e.g. "llama", "qwen2")
  3. version
  4. general.file_type
Show answer & explanation click to expand
Answer: B. The loader reads the general.architecture string and looks it up in LLM_ARCH_NAMES to get an enum like LLM_ARCH_LLAMA; that enum decides the KV/tensor conventions and graph builder used (L16).
2. Why does hparams write head count as n_head(il) with a layer index?
  1. different layers may have different head counts/attention types (GQA, sliding-window), so per-layer is most general
  2. to make inference faster
  3. decided at random
  4. it is a typo; it should be a field
Show answer & explanation click to expand
Answer: A. In modern architectures per-layer attention configs can differ (GQA gives fewer KV than Q heads; hybrids differ by layer), so head counts are stored per layer in n_head_arr, and n_head(il) is an accessor method (not a field) fetching by layer.
3. How does the loader map a file's tensors onto the model structure?
  1. by the tensor order in the file
  2. by the LLM_TENSOR_NAMES naming convention (token_embd / blk.N.attn_q ...), looking up weights_map by name
  3. by sorting on tensor size
  4. by random matching
Show answer & explanation click to expand
Answer: B. Tensor names follow LLM_TENSOR_NAMES templates (blk.%d filled with the layer index), built by LLM_TN's tn(); graph-building fetches weights from weights_map by name. A name is a stable contract, robust across tools and splits.
💭 Open questions (no single right answer - just think or try)
  • What do llm_arch, llama_hparams, and LLM_TENSOR_NAMES each govern? How do they combine to turn 'a pile of tensors' into 'a concrete, graph-able model'?