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

模型加载Model loading

第三部分我们把 GGUF 文件格式(L13)和 ggml 引擎(L08-L12)讲透了。这一课上到 llama 层——看 llama_model_loader 怎么把一个 .gguf 真正加载成内存里的模型:读 metadata、把权重整理成一张按名字索引的张量清单、用 mmap 让数据零拷贝就位,并处理"一个模型拆成多文件"的分片。

为什么要专门讲加载?因为它是从磁盘字节到可用模型的第一道关:L13 教会我们 GGUF 长什么样,但"看懂格式"和"把它变成一个能建图、能推理的 llama_model"之间, 还隔着 loader 这一层。读懂它,你就接上了"文件"和"模型"两端。

🔌 生活类比
llama_model_loader 像一个收货验货员:先看箱单(GGUF 的 metadata 和 tensor info)、再核对货品清单(把每个张量按名字登记进 weights_map)、 最后按单提货(mmap 让每个张量的 data 指针落到文件里对应的位置)。分片就是一批货分装好几个箱子,验货员按箱子上的 of-N 编号逐箱核对、并成一份总清单。

加载总览:loader 在做什么

打开一个 .gguf,loader 大致走四步:读头部、建清单、映射数据、让指针就位。它不"算"任何东西,只负责把磁盘上的字节整理成内存里有名有姓、随时可取的张量

1

gguf_init_from_file

读 GGUF 头:metadata(KV 超参,L13)+ 每个张量的 tensor info(name / dims / type / offset)。

2

建 weights_map

把每个张量按名字登记:来源文件号 idx、在文件里的偏移 offs、以及张量本身。

3

mmap 文件

(use_mmap 时)把整个文件只读映射进地址空间,准备零拷贝取数(L13)。

4

load_data_for

逐张量让 data 指针落到映射上对应位置(或在不用 mmap 时读入)。

把这张图落到结构体上(简化自 src/llama-model-loader.h):

// 简化自 src/llama-model-loader.h
struct llama_tensor_weight { uint16_t idx; size_t offs; ggml_tensor * tensor; }; // 来源文件号 + 偏移 + 张量
struct llama_model_loader {
    gguf_context_ptr metadata_ptr;                       // GGUF 头(KV + tensor infos)
    std::map<std::string, llama_tensor_weight> weights_map; // 按张量名索引的清单
    bool use_mmap;  llama_mmaps mappings;                // 零拷贝数据
};

逐个看:metadata_ptr 持有整份 GGUF 头,所有超参、词表、模板都在里头(L13 的自描述);weights_map 是这一课的主角——一张按名字的张量清单, 键是张量名(如 blk.0.attn_q.weight),值记着这张量来自第几个文件(idx,为分片准备)、在文件里的偏移(offs)、以及对应的 ggml_tensor

注意一个关键设计:清单用的是 map(按名字),不是 vector(按顺序)。为什么?因为接下来(L15)这些张量要按名字对应到模型架构里的具体位置;而且分片时同一个逻辑模型的张量散在多个文件里, 按名字查比按下标稳得多。loader 在这里就把"散落的字节"收敛成了"一张能按名字点名的清单"。

🌍 宏观理解
把 loader 放到整个推理流程里看,它的位置很特别:加载只发生一次,而后面的 decode(L17)会被反复调用成千上万次。正因为只做一次,loader 可以"慢工出细活"地把模型整理周全——读全 metadata、建好完整清单、接好所有数据指针;之后得到的 llama_model只读的,可以被许多次推理、甚至许多个会话反复复用,不必再碰磁盘。把"一次性的重活"和"反复做的快活"分开,是这套设计省时省力的根源。

"整理成清单"这一步看似平淡,其实是把后面一切操作变简单的关键。磁盘上的张量只是一段段连续字节,彼此之间没有任何"我是谁"的信息;loader 给它们配上名字、记下位置、建好索引, 之后无论是按架构建图(L16)、还是热插拔 LoRA(L24 预告),都能按名字精确点到某一个张量。先把混沌整理成有序,后面才能从容操作——这是工程里很常见、却常被忽视的一步。

🔬 细节 / 源码对应
这里也能看清几个层次的分工:最底下是 gguf 库(L13),只懂"GGUF 这种文件怎么解析",连"权重"是什么都不关心;往上是 ggml(L08-L12),只懂张量和计算,不关心文件;而 loader 正好夹在两者之间——它用 gguf 库读出原始信息,再用 ggml 建出张量,把"文件世界"翻译成"张量世界"。理解 loader,其实就是理解这道翻译是怎么发生的。

你平时跑 llama.cpp 时,启动那一大段刷屏的日志——"loaded meta data with N key-value pairs"、一行行张量名和类型——多半就是 loader 在汇报它的工作:读了多少 KV、识别出哪种架构、每个张量多大、用没用上 mmap。 下次看到这些日志,你就知道屏幕背后正是这一课讲的流程在跑。

💡 实战
上面"四步"是概念上的顺序,真实代码里它们常交织在一起——比如读 tensor info 的同时就建好了 weights_map 的条目,建条目时就记下了 mmap 里的偏移。教学上分成四步是为了看清职责,工程上则是一遍扫描尽量把能做的都做了。理解了每一步"在干什么",看真实代码时就不会被它们交错的写法绕晕。

读超参与张量

loader 怎么知道模型有几层、多宽?不靠猜,全从 GGUF 的 metadata KV 里读——用一个模板方法 get_key,把"键"映射到具体的超参字段。键本身是自描述的(L13),下一课(L15)会讲这些键怎么按架构拼出来。

读超参

get_key("llama.block_count") 等 -> 填进 llama_hparams 的字段(n_layer / n_embd ...,L15)。读的是 GGUF 头里的 metadata KV

读张量

按名字进 weights_map -> create_tensor 建元数据 -> load_data_for 落数据(mmap 零拷贝)。读的是 GGUF 头里的 tensor info 与数据段。

# 伪代码: loader 的典型用法
ml = llama_model_loader(path)              # 内部 gguf_init_from_file 读头部
ml.get_key(LLM_KV_BLOCK_COUNT, n_layer)   # 从 KV 读超参(L15)
for name, w in ml.weights_map:             # 遍历张量清单
    t = create_tensor(name, w.tensor->ne)   # 在 ggml_context 里建元数据(L08)
    ml.load_data_for(t)                    # use_mmap: data 指进映射; 否则读入

这段把前几课串了起来:get_key 读出的超参,下一课(L15)会装进 llama_hparamscreate_tensorggml_context(L08)里只建元数据 (形状/类型,配合 L08 讲的 no_alloc);load_data_for 才让真正的权重数据就位——而"就位"在 mmap 下就是把 data 指针指进文件映射,一个字节都不拷(L13 的零拷贝)。

所以 loader 读出来的每个张量,都对得上 L13 里那条 tensor info(name / dims / type / offset):名字进了 weights_map 的键,dims/type 建成 ggml 张量的元数据,offset 则告诉 load_data_for 去文件的哪个位置取数。L13 讲"文件里怎么存",这一课讲"loader 怎么把它读回内存",两课正好首尾相接。

这里藏着一个和 L13"头部轻、尾部重"呼应的巧思:loader 读那一小段头部(metadata + tensor info,通常几十 KB),就把模型的全部结构搞清楚了——几层、多宽、有哪些张量、各在文件哪个位置; 之后才按需去碰那几个 GB 的权重数据。正因为"描述"和"数据"在文件里是分开的,loader 才能用很小的代价先建好整张清单和骨架,把真正的大块留到 mmap 按页惰性载入。这一步的轻量,直接决定了大模型能不能"秒开"。

🔬 细节 / 源码对应
还要留意 create_tensor 这一步的轻——它在 ggml_context 里建的只是张量的元数据(形状、类型、名字),并不为那几 GB 的浮点数据另开缓冲(这正是 L08 讲的 no_alloc)。于是装下"整个模型的骨架"只要几 MB 的 context,真正占地方的权重则由 mmap 映射承接。元数据归元数据、数据归数据,两者分头安放——这是 L08 内存观在加载阶段的直接兑现,也让"先把结构建全、数据按需就位"成为可能。

再把 offs 这个字段说细一点:它记的是这个张量的数据在文件里的字节偏移(对应 L13 tensor info 的 offset)。有了它,load_data_for 才能在 mmap 映射里直接算出这个张量从哪开始—— 映射基址 + 数据段起点 + offs,一步定位,不用顺序扫描。这正是 L13 讲的"按 offset 定位张量"在加载代码里的落地。

读张量时 loader 还会做一些一致性检查:比如某个张量的形状要和架构期望的对得上、类型是否被支持。这些检查放在加载期做最划算——一旦放行,后面建图、推理就可以默认"张量都是对的", 不必每步重新提防。把校验集中在入口,是让后续代码能写得干净利落的前提。

分片:一个模型拆成多文件

特别大的模型(几十上百 GB)常被拆成多个文件,方便下载与分发。loader 把这些片当成一个逻辑模型读:按编号挨个打开、把各片的张量并进同一张 weights_map

分片 -> 一个逻辑模型:按 of-N 编号逐片打开,张量并进同一张 weights_map
磁盘model-00001-of-0000300002-of-0000300003-of-00003-> 一个逻辑模型

分片靠几样东西对上:文件名格式 "%s-%05d-of-%05d.gguf"(由 llama_split_path 拼,src/llama.cpp)告诉你"第几片、共几片";元数据键 split.countLLM_KV_SPLIT_COUNT)记总片数;而每个张量在 weights_map 里的 idx 字段,正是记着"我来自第几片"。

这也解释了前面为什么 weights_map 要按名字:分片把同一个模型的张量分散到多个文件,唯有按名字才能跨文件把它们统一查到、拼成完整的一份。对使用者来说,分不分片几乎无感——你只管给入口一个路径,loader 在背后把碎片拼好。

🌍 宏观理解
为什么要分片?一是下载与分发友好:一个 200 GB 的模型切成几十个几 GB 的片,断点续传、并行下载、镜像同步都更容易;二是有些文件系统对单文件大小有上限,分片能绕开;三是方便按需取用。你在 HuggingFace 上看到的大模型,权重文件往往就是 ...-of-00010.gguf 这样一长串。

一个自然的疑问:分片之后,前面说的 mmap 零拷贝还成立吗?成立——loader 给每一片各做一个映射(mappings 是一组映射、不是一个),每个张量的 idx 记着它属于哪一片, load_data_for 就去对应那片的映射里取数。所以"分片"和"零拷贝"是正交的两件事:分片解决"文件太大不好搬",mmap 解决"数据太大不想拷",两者叠加,超大模型既好分发、又能秒加载。

顺便消除一个误解:分片不是什么特殊模式。单文件模型其实就是"只有一片"的退化情形——split.count 为 1(或干脆没有这个键)。loader 的代码统一按"可能有多片"来写,单文件只是片数为一的特例。 这种"把特例当成通例的一种"的写法,让代码更简单、也更少出错。

各家工具(包括 L02 的 gguf-py)在导出大模型时,会按一个目标分片大小自动切,并把 split.* 这几个键写进每一片——loader 读出来就能无缝拼回。 写入方和读取方共享同一套分片约定,正是 GGUF 自描述精神(L13)在"多文件"维度上的延伸。

入口与衔接

从外面看,加载就是一个函数调用。它的调用链很直白:

llama_model_load_from_file
公开入口
(或 _from_splits)
->
..._impl
内部实现
->
llama_model_load
用 loader 读
metadata + 张量
->
llama_model
权重 data 已指进 mmap

公开入口 llama_model_load_from_file(以及分片版 llama_model_load_from_splits)都汇到 ..._impl,再调静态的 llama_model_load,由它创建 loader、读出 metadata 与张量, 最终返回一个 llama_model——里面的权重张量,data 指针已经指进了 mmap 映射,随取随用。

加载到此为止。下一课(L15)我们就接着问:这一堆"带名字的张量",怎么知道自己属于哪种架构(llama?qwen2?)、每层有哪些部件、怎么按超参组织起来——也就是 loader 读出的东西,到底要按什么图纸拼成一个模型。

稍微展开一下加载完拿到的 llama_model 到底是什么:它是一个只读的对象,装着所有权重张量(data 指针指进 mmap)、加上从 metadata 读出的超参(L15 会装进 llama_hparams)和词表(L20)。 它不含任何"会话状态"——没有 KV cache、没有当前算到哪。这正是下一层(L17)要把 llama_modelllama_context 分开的伏笔:知识(权重)只读可共享,状态(KV/进度)每会话一份。加载这一课,交付的就是那份"只读的知识"。

🌍 宏观理解
顺带一提:加载失败是有明确信号的——magic 不对、version 不认识、某个必需张量缺失,loader 都会当场报错而不是带病继续。这种"加载期就把问题暴露出来"的做法,比"跑到一半才崩"友好得多,也是把校验集中在 loader 这一层的好处。

把这一课收个尾:loader 站在"格式"和"模型"之间,向下它只关心 GGUF 的字节怎么排(L13),向上它只交付一份干净的、按名字可查的、数据已就位的张量集合。它不懂什么是注意力、不懂 llama 和 qwen 有什么不同——这些是上面几课的事。 正是这种"各司其职、边界清晰"的分层,让 llama.cpp 能一边支持越来越多的新格式细节、一边支持越来越多的新架构,而两边的改动很少互相牵连。读懂了加载,你就握住了从磁盘到模型的第一环。

最后看一眼那条"入口 -> _impl -> _load"的调用链为什么要分这么多层。最外层 llama_model_load_from_file稳定的公开 C API,要长期不变、给各种语言绑定调用; 中间的 _impl 和静态 llama_model_load 是内部实现,可以随时重构。把"对外承诺"和"对内实现"分开,是库设计的基本功——你调的是一个十年不变的名字,它背后怎么演进你不必关心。

1 为什么用 mmap,而不是把权重全读进内存? 点击展开

这正是 L13 讲过的零拷贝。权重动辄几个 GB,如果老老实实 read() 进一块内存,既慢又占地方。mmap 则把文件映射进地址空间,张量 data 指针看着像普通内存、实则指向磁盘页,真正用到哪页操作系统才载入。

好处有三:启动几乎不花时间在"搬数据"上(秒加载);物理内存按需、可被系统回收;多个进程映射同一个文件还能共享同一份物理页——同机起多个实例时省内存。所以 loader 默认 use_mmap=true,只有某些后端或平台才关掉它。

换句话说,loader 在这一步做的不是"把权重读进来",而是"把权重的位置接好"。数据始终躺在文件里,需要时才一页页流进来,这是大模型能在普通机器上跑起来的关键之一。

2 weights_map 为什么按名字索引,而不是按顺序? 点击展开

因为张量最终要按名字对应到模型架构里的具体位置。下一课会看到,blk.0.attn_q.weight 这种名字是有约定的(LLM_TENSOR_NAMES),建图时正是拿名字去 weights_map 里取对应权重。

按顺序(下标)则很脆:换个导出工具、张量排列略有不同,下标就全错位了;而名字是稳定的契约。更要紧的是分片——同一个逻辑模型的张量散在多个文件里,只有按名字才能跨文件把它们统一查到。

所以 map 既稳又省心:不管张量物理上躺在哪个文件、第几位,只要名字对得上,就能精确点名。loader 把"物理排布"和"逻辑名字"解耦,是后续一切按名字操作的基础。

3 分片到底怎么对上的? 点击展开

靠三个元数据键加一套文件名约定。键有 split.no(这是第几片)、split.count(一共几片)、split.tensors.count(总张量数);文件名则是 "...-00001-of-00003.gguf" 这种 of-N 编号。

loader 拿到首片,从 split.count 知道还有几片,再用 llama_split_path 按编号拼出其余文件名、挨个打开,把每片的张量并进同一张 weights_map;每个张量的 idx 记下它来自第几片,方便 load_data_for 时去对的文件取数。

对调用方来说,分片几乎透明:给一个路径(或用 _from_splits 给一组),loader 在背后把碎片拼成一份完整模型。这种"物理上分、逻辑上合"的设计,让超大模型既好分发、又不增加使用复杂度。

✅ 关键要点
  • llama_model_loader = 读 GGUF metadata(用 get_key 取超参)+ 建 weights_map(按名字的张量清单)+ mmap 数据。
  • weights_map 用 map 按名字索引(不是按顺序),为"按名字建图"(L15/L16)和分片跨文件查找打底。
  • 权重数据默认 mmap 零拷贝就位(L13),load_data_for 让 data 指针指进映射。
  • 分片:文件名 "%s-%05d-of-%05d.gguf" + split.count,loader 当成一个逻辑模型读。
  • 入口 llama_model_load_from_file -> _impl -> llama_model_load,最终返回 llama_model
💡 设计洞察
loader 把"解析格式"和"使用模型"干净地隔开——它只负责把字节变成带名字的张量清单 + 就位的数据指针,至于这些张量怎么接成一张前向网络,是 L15(架构)和 L16(建图)的事。 正因为这道边界清晰,"支持新的格式细节"和"支持新的模型架构"才能各自演进、互不打扰。一个好的加载层,存在感越低越好:它把脏活做完,让上层只看到一个干净的模型。

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

1. llama_model_loader 主要做什么?
  1. 训练这个模型
  2. 读 GGUF 的 metadata(超参)和 tensor infos、建按名字的张量清单、(按 use_mmap)把权重数据 mmap 或读入
  3. 把权重重新量化
  4. 编译 GPU kernel
看答案与解析 点击展开
答案:B。loader 不计算,只把字节整理成可用模型:gguf_init_from_file 读头部、weights_map 按名字登记每个张量、use_mmap 时让 data 指针指进文件映射(L13 零拷贝)。
2. 一个被分片的大模型,文件名长什么样?
  1. 一个 .zip 压缩包
  2. model-00001-of-00003.gguf 这种 of-N 编号
  3. 永远是单文件,不能分片
  4. 随机哈希名
看答案与解析 点击展开
答案:B。分片文件名由 llama_split_path 按 "%s-%05d-of-%05d.gguf" 拼出;split.count 记总片数;loader 按编号逐片打开、并进同一张 weights_map。
3. 加载器怎么知道模型有多少层、多大维度?
  1. 在代码里硬编码
  2. 用 get_key(llm_kv, ...) 从 GGUF 的 metadata KV 里读(自描述)
  3. 靠猜测
  4. 读一个外部 config.json
看答案与解析 点击展开
答案:B。超参全在 GGUF 的 metadata KV 里(L13 自描述);loader 用模板方法 get_key 把键映射到具体超参字段,无需外部配置或猜测。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 结合 L13,说说 llama_model_loader 为什么用 mmap 加载权重数据能做到“秒加载”又省内存。(提示:零拷贝/按页/共享)

Part 3 took apart the GGUF format (L13) and the ggml engine (L08-L12). This lesson moves up to the llama layer - seeing how llama_model_loader turns a .gguf into an in-memory model: reading metadata, organizing the weights into a name-indexed tensor list, using mmap to bring data into place zero-copy, and handling "one model split across several files".

Why a whole lesson on loading? Because it is the first gate from disk bytes to a usable model: L13 taught us what GGUF looks like, but between "understanding the format" and "turning it into a graph-able, inferable llama_model" sits this loader layer. Read it and you connect the two ends - "file" and "model".

🔌 Analogy
llama_model_loader is like a goods-receiving clerk: first read the manifest (GGUF metadata and tensor info), then check the inventory (register each tensor by name into weights_map), and finally pick stock by the list (mmap lands each tensor's data pointer at its place in the file). A split model is one shipment packed into several boxes; the clerk checks them by the of-N number on each box and merges them into one list.

Loading overview: what the loader does

Opening a .gguf, the loader roughly takes four steps: read the header, build the list, map the data, point the pointers. It computes nothing - it just organizes disk bytes into named, ready-to-fetch tensors in memory.

1

gguf_init_from_file

Read the GGUF header: metadata (KV hyperparameters, L13) + each tensor's info (name / dims / type / offset).

2

build weights_map

Register each tensor by name: source file idx, offset offs in the file, and the tensor itself.

3

mmap the file

(when use_mmap) map the whole file read-only into the address space, ready for zero-copy fetch (L13).

4

load_data_for

Per tensor, land the data pointer at its place in the mapping (or read it in when not using mmap).

Landing that figure on the struct (simplified from src/llama-model-loader.h):

// simplified from src/llama-model-loader.h
struct llama_tensor_weight { uint16_t idx; size_t offs; ggml_tensor * tensor; }; // source file idx + offset + tensor
struct llama_model_loader {
    gguf_context_ptr metadata_ptr;                       // GGUF header(KV + tensor infos)
    std::map<std::string, llama_tensor_weight> weights_map; // list indexed by tensor name
    bool use_mmap;  llama_mmaps mappings;                // zero-copy data
};

Field by field: metadata_ptr holds the whole GGUF header, with all hyperparameters, vocab, and template inside (L13's self-description); weights_map is this lesson's star - a name-indexed tensor list whose key is the tensor name (e.g. blk.0.attn_q.weight) and whose value records which file the tensor came from (idx, for splits), its offset in the file (offs), and the matching ggml_tensor.

Note a key design: the list is a map (by name), not a vector (by order). Why? Because next (L15) these tensors must be matched by name to specific positions in the model architecture; and with splits, one logical model's tensors are scattered across several files, where look-up by name is far steadier than by index. Right here the loader collapses "scattered bytes" into "a list you can call by name".

🌍 Big picture
Placed in the whole inference pipeline, the loader's position is special: loading happens once, while the decode that follows (L17) is called thousands of times. Precisely because it runs once, the loader can take its time to organize the model thoroughly - read all metadata, build the complete list, wire up every data pointer; the resulting llama_model is read-only and can be reused across many inferences, even many sessions, without touching disk again. Separating "the one-time heavy work" from "the repeated fast work" is the root of this design's efficiency.

This "organize into a list" step looks mundane but is the key to making everything later simple. On disk, tensors are just runs of contiguous bytes with no "who am I" information; the loader gives them names, records positions, and builds an index, so that whether building a graph by architecture (L16) or hot-swapping a LoRA (L24), the engine can point precisely at one tensor by name. Turning chaos into order first is what lets everything afterward proceed calmly - a common but underappreciated step in engineering.

🔬 Details / source
You can also see the division of labor across layers here: at the bottom is the gguf library (L13), which only knows "how to parse a GGUF file" and cares nothing for what a "weight" is; above it is ggml (L08-L12), which only knows tensors and computation, not files; and the loader sits exactly between them - using the gguf library to read raw info, then ggml to build tensors, translating the "file world" into the "tensor world". Understanding the loader is really understanding how that translation happens.

When you run llama.cpp, that wall of startup logs - "loaded meta data with N key-value pairs", lines of tensor names and types - is mostly the loader reporting its work: how many KVs it read, which architecture it recognized, how big each tensor is, whether mmap was used. Next time you see those logs, you will know the process behind the screen is exactly what this lesson describes.

💡 Tip
The "four steps" above are a conceptual order; in real code they often interleave - e.g. reading a tensor info also builds its weights_map entry, and building the entry records the offset into the mmap. Splitting into four steps is for seeing the responsibilities clearly; in engineering, one pass does as much as it can at once. Once you grasp what each step "is doing", the interleaved real code will not confuse you.

Reading hyperparameters and tensors

How does the loader know how many layers, how wide? Not by guessing - it reads it all from the GGUF metadata KVs, via a templated get_key that maps a "key" to a specific hyperparameter field. The keys are self-describing (L13); the next lesson (L15) covers how they are templated per architecture.

Read hyperparameters

get_key("llama.block_count") etc. -> fill fields of llama_hparams (n_layer / n_embd ..., L15). Reads the metadata KV in the GGUF header.

Read tensors

By name into weights_map -> create_tensor builds metadata -> load_data_for places data (mmap zero-copy). Reads the tensor info in the GGUF header, then the data section.

# pseudocode: typical loader usage
ml = llama_model_loader(path)              # internally gguf_init_from_file reads the header
ml.get_key(LLM_KV_BLOCK_COUNT, n_layer)   # read a hyperparameter from the KVs(L15)
for name, w in ml.weights_map:             # walk the tensor list
    t = create_tensor(name, w.tensor->ne)   # build metadata in ggml_context(L08)
    ml.load_data_for(t)                    # use_mmap: data points into the map; else read in

This snippet ties the earlier lessons together: the hyperparameters get_key reads will be packed into llama_hparams next lesson (L15); create_tensor builds only metadata (shape/type) in the ggml_context (L08, with the no_alloc idea); and load_data_for brings the real weight data into place - which, under mmap, just means pointing the data pointer into the file mapping, copying not a byte (L13's zero-copy).

So every tensor the loader reads lines up with that tensor info from L13 (name / dims / type / offset): the name becomes a key in weights_map, dims/type build the ggml tensor's metadata, and the offset tells load_data_for where in the file to fetch from. L13 covered "how it is stored in the file", this lesson covers "how the loader reads it back into memory" - the two meet end to end.

Here is a clever touch echoing L13's "light head, heavy tail": the loader first reads that small header (metadata + tensor info, usually tens of KB) and thereby learns the model's entire structure - how many layers, how wide, which tensors, where each sits in the file; only then does it touch those GB of weight data on demand. Because "description" and "data" are separated in the file, the loader can build the whole list and skeleton at tiny cost and leave the real bulk to mmap's lazy, page-by-page loading. The lightness of this step directly decides whether a large model can "open instantly".

🔬 Details / source
Note also how light the create_tensor step is - in the ggml_context it builds only a tensor's metadata (shape, type, name), reserving no buffer for those GB of float data (exactly L08's no_alloc). So holding "the whole model's skeleton" takes only a few MB of context, while the weights that actually take space are carried by the mmap mapping. Metadata as metadata, data as data, placed separately - the direct cash-out of L08's memory model at load time, and what makes "build the full structure first, bring data into place on demand" possible.

A bit more on the offs field: it records this tensor's data byte offset in the file (matching L13's tensor info offset). With it, load_data_for can directly compute where this tensor starts in the mmap mapping - mapping base + data-section start + offs, located in one step, no sequential scan. This is exactly L13's "addressing tensors by offset" realized in loading code.

While reading tensors the loader also does some consistency checks: a tensor's shape must match what the architecture expects, its type must be supported. Doing these at load time is most economical - once they pass, graph-building and inference can assume "the tensors are all correct" without re-guarding at every step. Centralizing validation at the gate is what lets the later code stay clean.

Splits: one model across several files

Very large models (tens to hundreds of GB) are often split into multiple files for easier download and distribution. The loader reads these shards as one logical model: opening them by number and merging each shard's tensors into the same weights_map.

shards -> one logical model: open by of-N number, merge tensors into one weights_map
diskmodel-00001-of-0000300002-of-0000300003-of-00003-> one logical model

Splits line up via a few things: the filename format "%s-%05d-of-%05d.gguf" (built by llama_split_path, src/llama.cpp) tells you "which shard, of how many"; the metadata key split.count (LLM_KV_SPLIT_COUNT) records the total shard count; and each tensor's idx field in weights_map records "which shard I came from".

This also explains why weights_map is keyed by name: splits scatter one model's tensors across files, and only by name can they be looked up uniformly across files and stitched into a complete whole. To the caller, split-or-not is nearly invisible - you just hand the entry point a path, and the loader stitches the pieces behind the scenes.

🌍 Big picture
Why split at all? First, download and distribution friendliness: a 200 GB model cut into dozens of few-GB shards is easier to resume, download in parallel, and mirror; second, some file systems cap single-file size, which splitting sidesteps; third, it eases selective loading. The large models you see on HuggingFace often have weight files like a long ...-of-00010.gguf series.

A natural question: after splitting, does the earlier mmap zero-copy still hold? It does - the loader makes one mapping per shard (mappings is a set, not a single map), each tensor's idx records which shard it belongs to, and load_data_for fetches from that shard's mapping. So "splitting" and "zero-copy" are orthogonal: splitting solves "the file is too big to move around", mmap solves "the data is too big to copy"; together, a huge model is both easy to distribute and instant to load.

Clearing up a misconception: splitting is not a special mode. A single-file model is just the degenerate "one shard" case - split.count is 1 (or the key is simply absent). The loader's code is written uniformly as "there may be multiple shards", and a single file is just the one-shard special case. Treating the special case as one instance of the general case keeps the code simpler and less error-prone.

Tooling (including L02's gguf-py) splits a large model automatically by a target shard size when exporting, writing the split.* keys into every shard - which the loader reads to stitch them back seamlessly. Writer and reader sharing one split convention is GGUF's self-describing spirit (L13) extended into the "multiple files" dimension.

Entry points and the hand-off

From the outside, loading is one function call, with a straightforward chain:

llama_model_load_from_file
public entry
(or _from_splits)
->
..._impl
internal impl
->
llama_model_load
loader reads
metadata + tensors
->
llama_model
weight data points into mmap

The public entry llama_model_load_from_file (and the split version llama_model_load_from_splits) both funnel into ..._impl, then call the static llama_model_load, which creates the loader, reads metadata and tensors, and returns a llama_model - whose weight tensors already have their data pointers pointing into the mmap mapping, ready to use.

Loading ends here. The next lesson (L15) asks what comes next: this pile of "named tensors" - how does it know which architecture it belongs to (llama? qwen2?), which parts each layer has, how it is organized by the hyperparameters - that is, by what blueprint the loader's output is assembled into a model.

A bit more on what the llama_model you get after loading actually is: a read-only object holding all weight tensors (data pointers into mmap), plus the hyperparameters read from metadata (L15 packs them into llama_hparams) and the vocab (L20). It contains no "session state" - no KV cache, no notion of where computation currently is. That foreshadows why the next layer (L17) separates llama_model from llama_context: knowledge (weights) is read-only and shareable, state (KV/progress) is per-session. What this loading lesson delivers is exactly that "read-only knowledge".

🌍 Big picture
By the way: load failures are signaled clearly - a wrong magic, an unknown version, a missing required tensor, and the loader errors out on the spot rather than limping on. This "surface problems at load time" approach is far friendlier than "crash halfway through", and is a benefit of centralizing validation in the loader layer.

To close this lesson: the loader stands between "format" and "model" - downward it only cares how GGUF's bytes are laid out (L13), upward it only delivers a clean, name-addressable set of tensors with data in place. It knows nothing of attention, nothing of how llama differs from qwen - those belong to the lessons above. This very "each does its job, with clear boundaries" layering is what lets llama.cpp keep supporting more format details on one side and more architectures on the other, with the two rarely entangling. Understand loading and you hold the first link from disk to model.

Finally, a look at why the "entry -> _impl -> _load" chain has so many layers. The outermost llama_model_load_from_file is the stable public C API, meant to stay unchanged for the long run and be called by bindings in many languages; the inner _impl and static llama_model_load are implementation that can be refactored anytime. Separating "the outward promise" from "the inward implementation" is library-design basics - you call a name that holds for years, and need not care how it evolves underneath.

1 Why mmap rather than reading all weights into memory? Click to expand

This is exactly L13's zero-copy. Weights are often several GB; dutifully read()-ing them into a buffer is slow and space-hungry. mmap instead maps the file into the address space - a tensor's data pointer looks like ordinary memory but points at disk pages, loaded by the OS only when actually touched.

Three benefits: startup spends almost no time "moving data" (instant load); physical memory is on-demand and reclaimable; and multiple processes mapping the same file can share the same physical pages - saving memory when running several instances on one machine. So the loader defaults to use_mmap=true, turned off only on certain backends or platforms.

In other words, what the loader does here is not "read the weights in" but "wire up where the weights are". The data stays in the file and streams in page by page on demand - one key to running large models on ordinary machines.

2 Why is weights_map keyed by name, not by order? Click to expand

Because tensors must ultimately be matched by name to specific positions in the model architecture. Next lesson you will see that names like blk.0.attn_q.weight follow a convention (LLM_TENSOR_NAMES), and graph-building fetches the matching weight from weights_map by name.

By order (index) it would be fragile: a different export tool, a slightly different tensor arrangement, and every index is off. A name is a stable contract. More importantly, with splits, one logical model's tensors are scattered across files - only by name can they be looked up uniformly.

So a map is both steady and convenient: no matter which file a tensor physically sits in, or in what position, as long as the name matches it can be called out precisely. The loader decouples "physical layout" from "logical name" - the basis for everything that follows operating by name.

3 How exactly do splits line up? Click to expand

Via three metadata keys plus a filename convention. The keys are split.no (which shard this is), split.count (how many in total), and split.tensors.count (total tensor count); the filename is the of-N number "...-00001-of-00003.gguf".

Given the first shard, the loader learns the count from split.count, uses llama_split_path to build the other filenames by number, opens each, and merges its tensors into the same weights_map; each tensor's idx records which shard it came from, so load_data_for fetches from the right file.

To the caller, splits are nearly transparent: hand over a path (or a set via _from_splits), and the loader stitches the pieces into one complete model. This "physically split, logically whole" design lets huge models be easy to distribute without adding usage complexity.

✅ Key points
  • llama_model_loader = read GGUF metadata (hyperparameters via get_key) + build weights_map (a name-indexed tensor list) + mmap the data.
  • weights_map is a map keyed by name (not by order), underpinning "build-by-name" (L15/L16) and cross-file lookup for splits.
  • Weight data defaults to mmap zero-copy in place (L13); load_data_for points the data pointer into the mapping.
  • Splits: filename "%s-%05d-of-%05d.gguf" + split.count; the loader reads them as one logical model.
  • Entry llama_model_load_from_file -> _impl -> llama_model_load, returning a llama_model.
💡 Design insight
The loader cleanly separates "parsing the format" from "using the model" - it only turns bytes into a name-indexed tensor list + data pointers in place; how those tensors are wired into a forward network is L15's (architecture) and L16's (graph) job. Because that boundary is clear, "supporting new format details" and "supporting new model architectures" can each evolve without disturbing the other. A good loading layer is best when barely noticed: it does the dirty work so the upper layers see only a clean model.

🧪 Self-test - think about the design

1. What does llama_model_loader mainly do?
  1. train the model
  2. read GGUF metadata (hyperparameters) and tensor infos, build a name-indexed tensor list, and (per use_mmap) mmap or read the weight data
  3. re-quantize the weights
  4. compile GPU kernels
Show answer & explanation click to expand
Answer: B. The loader computes nothing; it organizes bytes into a usable model: gguf_init_from_file reads the header, weights_map registers each tensor by name, and with use_mmap the data pointers point into the file mapping (L13 zero-copy).
2. What do the filenames of a split large model look like?
  1. a single .zip archive
  2. of-N numbering like model-00001-of-00003.gguf
  3. always a single file, never split
  4. random hash names
Show answer & explanation click to expand
Answer: B. Split filenames are built by llama_split_path as "%s-%05d-of-%05d.gguf"; split.count records the total; the loader opens each by number and merges into one weights_map.
3. How does the loader know the model's layer count and dimensions?
  1. hard-coded in the code
  2. it reads them from the GGUF metadata KVs via get_key(llm_kv, ...) (self-describing)
  3. by guessing
  4. by reading an external config.json
Show answer & explanation click to expand
Answer: B. Hyperparameters live in the GGUF metadata KVs (L13 self-description); the loader's templated get_key maps a key to a specific field, with no external config or guessing.
💭 Open questions (no single right answer - just think or try)
  • Drawing on L13, explain why llama_model_loader's mmap loading of weight data achieves 'instant load' and saves memory. (hint: zero-copy / paging / sharing)