上一课 loader 把权重读成了一张"带名字的张量清单",可这些张量怎么知道自己属于哪种架构(llama?qwen2?)、每层有哪些部件?这一课讲两样东西:llama-arch(架构标识 + 键名约定 + 张量名约定三套表)和 llama-hparams(几层、多宽、每层几个头)——它们合起来,是把"一堆张量"翻译成"一个具体可建图的模型"的说明书。
为什么把这两件事单拎一课?因为它们是 llama.cpp 能用一套代码读懂几十种模型的秘密。同样是 transformer,llama 和 qwen2 的差别,本质上就藏在"用哪套约定、几层多宽、张量怎么命名"里。 搞懂这一课,你就明白新模型是怎么"插进"这个引擎的。
一切从一个问题开始:拿到一个模型,怎么知道它是 llama 还是 qwen2?答案在 L13 讲过的那个 GGUF 元数据键 general.architecture 里。它是一个字符串(比如 "llama"), loader 读出它、在一张名字表里一查,就得到对应的架构枚举 LLM_ARCH_LLAMA。这个枚举一旦定下,后面用哪套键名、哪套张量名、调哪个建图函数(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 里大量"按架构分情况"的逻辑——读哪些超参、张量怎么命名、建图怎么拼——都靠这个枚举来分流。 把"我是谁"收敛成一个枚举值,是让后面所有"按架构区别对待"的代码都能写得整齐的前提。
除了 general.architecture,GGUF 头里还有一批 general.* 的通用元信息(L13 提过):模型名、整体量化档位、量化版本等。它们和架构枚举一起,构成了"这到底是个什么模型"的完整自我介绍。 loader 一上来读的,正是这批通用信息加上架构相关的超参——前者认出"是谁",后者量出"多大"。
这条"字符串 <-> 枚举"的桥也解释了 L02 转换脚本在做的事之一:把一个 HuggingFace 模型转成 GGUF 时,脚本要判断它是哪种架构、把对应的字符串(如 "llama")写进 general.architecture。 写入方和读取方共用同一张 LLM_ARCH_NAMES,一写一读才能严丝合缝。这也是为什么有时一个全新架构的模型,需要先给 llama.cpp 加上对它的支持,转换脚本才认得、才转得出来。
知道了是哪种架构,还得知道具体尺寸:这个 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_train | RoPE 的频率基(位置编码,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; // 访问器, 不是字段! };
这些超参一旦读出来,就成了整个推理的"尺寸基准":建图(L16)时按 n_layer() 决定堆几层、按 n_head(il) 切多少个注意力头、按 n_embd 定各处矩阵的形状; KV cache(L19)按层数和 KV 头数算该开多大。可以说,hparams 是把"一个抽象的 transformer"具体化成"这一个模型"的那组数字。
顺便厘清"参数量"和超参的关系。我们常说的 7B、70B,指的是模型权重里浮点数的总个数(70 亿、700 亿);而这个总数,正是由超参算出来的——大致是 n_layer × 每层各权重矩阵尺寸之和, 而每个矩阵的尺寸又由 n_embd、n_ff 等决定。所以超参不是一堆孤立的数字,它们共同决定了模型有多大。读懂超参,你就能从一个模型的几个数,估出它要吃多少显存。
再说一句那个默认参数 il = 0:它让"对每层都一样"的简单模型用起来很省事——不传层号,默认取第 0 层(也就是所有层)的值。所以接口虽然是"按层取",对老实的同构模型并不啰嗦。 这是个体贴的设计:复杂情况能表达,简单情况不添乱。
这里特别值得记住 n_head_kv 的意义:它直接决定 KV cache 有多大。GQA 的思路就是让多个 Q 头共享一组 K/V,于是 KV 头数远少于 Q 头数,KV cache 也就成倍变小(L19 会细讲)。 所以读一个模型的超参时,n_head 和 n_head_kv 的比值,几乎就告诉了你"这个模型对长上下文友不友好"。一个看似不起眼的超参,背后是显存与速度的大账。
最后一块拼图:loader 读出的张量怎么按名字对上模型结构?靠一套命名约定。每个张量在文件里都有名字,而这些名字不是随便起的,遵循 LLM_TENSOR_NAMES 定义的模板。
看这几个名字就懂了规律: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 这份共同约定对齐。
把一层 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。正因为大家共享同一套名字,针对张量的通用工具(量化、转换、可视化)才能不区分架构地处理任意模型。
顺便提一句:有些张量是可选的——比如不少现代模型的线性层没有 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_KV:llama.block_count 等 -> 由 get_key 读成超参。再量出"有多大"。
LLM_TENSOR_NAMES:blk.N.attn_q 等 -> 对上每块权重。最后"零件对号入座"。
设想一下如果不用表驱动会怎样:每支持一种新模型,就得在引擎主干里写一堆 if (arch == "llama") ... else if (arch == "qwen2") ... 的分支,读超参、命名张量、建图处处都要改。模型一多,这些分支就会织成一张谁也不敢动的网。 表驱动把这些差异从"散落在代码各处的 if"收进"集中的几张表 + 一份建图文件",于是主干代码读起来始终是"照表办事",清清爽爽。
这套"识别架构 -> 读超参 -> 按名取张量"的三步,其实是任何"通用模型加载器"都绕不开的骨架:先搞清是什么、再量出多大、最后把零件对号入座。llama.cpp 把这三步做得极其干净,正是它能在短时间内追上一个又一个新模型的工程根基。 下一课,我们就拿着这份"图纸 + 规格表 + 零件清单",真正动手把它们拼成一张能算的前向计算图。
把这一课和上一课连起来看,会发现一条清晰的主线:L14 的 loader 把字节变成"带名字的张量",L15 的 arch/hparams 给这些张量配上"图纸和规格"。到这里,模型已经从"磁盘上的一个文件"变成了"内存里一个有名有姓、知道自己几层多宽的对象"—— 只差最后一步:把这些零件按图纸真正拼成一张能算的网络。那正是下一课的事。
因为现代架构里,不同层的注意力配置可能不一样。最常见的是 GQA(分组查询注意力):Q 头多、KV 头少,于是 n_head_kv(il) 会小于 n_head(il),用来省 KV cache(L19)。
还有些架构是混合的:某些层用全注意力、某些层用滑动窗口,各层的头数、窗口大小都可能不同。要表达这种"逐层不同",最干净的办法就是把这些数存成按层的数组(n_head_arr 等),取的时候带上层号 il。
所以 n_head(il) 是个方法、不是字段——它背后从数组里按层号取值。对大多数老实的同构模型,每层都一样,il 取默认 0 即可;但接口设计成按层取,才容得下那些"逐层不同"的新架构。这是个"为通用性留余地"的典型取舍。
大致四步,且大多是"填表":① 在 enum llm_arch 里加一项、在 LLM_ARCH_NAMES 里加它的名字;② 实现这架构的超参读取(从 KV 把它特有的几个超参读进 hparams);③ 把它用到的张量在张量名表里登记;④ 写一份建图(L16,src/models/<arch>.cpp)。
关键是第④步往往很轻:因为绝大多数架构用的是同一批积木(注意力、FFN、归一化),新架构多半只是"换个拼法",能复用现成的 build_attn/build_ffn(L16)。只有遇到真正新颖的结构,才需要补一两个新算子(L11)。
这就是为什么 llama.cpp 能跟上层出不穷的新模型:大部分"新架构"在工程上其实是"填几张表 + 复用积木",引擎主干纹丝不动。把"模型的多样性"收进表和建图文件,把"不变的机制"留在主干——这是这套设计最省力的地方。
因为它是三方共享的契约。写入方(L02 的转换脚本)按这套名字把权重写进 GGUF;读取方(L14 的 loader)按名字建 weights_map;建图方(L16)按名字取权重。三方只要都遵守同一套 LLM_TENSOR_NAMES,就能严丝合缝地对上。
正因为有这套约定,"权重"和"代码"才解耦了:你换个工具导出、张量在文件里顺序不同,都不影响——只要名字对得上,建图就能取到对的料。名字成了模型各部件的"身份证",比"第几个张量"这种脆弱的下标稳得多。
它也呼应了 L13 的自描述精神:模型不光自带超参、词表,连"每块权重是哪个部件"都用名字标得明明白白。读懂了命名约定,你再去看任何模型的张量列表,都能一眼认出哪个是第几层的什么——这是读 llama.cpp 模型的一项基本功。
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.
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.
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.
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.
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.
| hyperparameter | meaning |
|---|---|
| n_embd | hidden 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_train | RoPE 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! };
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.
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.
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.
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.
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.
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.
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).
LLM_ARCH_NAMES: "llama" -> LLM_ARCH_LLAMA. First recognize "which model".
LLM_KV: llama.block_count etc. -> read into hyperparameters by get_key. Then measure "how big".
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.
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.
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.
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.