🦙 llama.cpp 图解教程llama.cpp Visual Guide 第三部分 · ggml 引擎Part 3 · The ggml engine 08 / 40
第三部分 · ggml 引擎Part 3 · The ggml engine

ggml 核心对象ggml core objects

第二部分给了你张量、量化、后端的直觉;从第三部分起,我们正式拆开 ggml 这台引擎。第一站是"内存"—— ggml 到底怎么管理成千上万个张量的内存?答案出人意料地朴素:一个叫 ggml_context内存池, 加上一套"绝不零散 malloc"的分配哲学。看懂这一层,后面的计算图、执行、算子才有立足之地。

先说清这一课的问题意识:一次推理要凭空造出成千上万个张量——权重、中间激活、KV cache,全是张量。 如果每造一个就向操作系统讨一次内存,光是"申请、记账、归还"的开销就足以拖慢整个引擎。ggml 的回答是:别零买,批发。 它一次性圈下一大块内存,自己在里面精打细算地切——这块内存就叫 ggml_context。这一课就讲它怎么圈地、怎么切、以及为什么这么设计。

顺便把一个名词对上号:这种"一次圈一大块、内部自己切、用完整体释放"的内存管理方式,在系统编程里有个通用名字叫 arena(竞技场 / 区域)分配器,也叫线性分配器区域分配器。它不是 ggml 的独创,而是高性能程序里常见的老把戏; ggml 只是把它用在了"管理一张计算图的所有张量"这个恰到好处的场景上。后面我们说 arena,指的就是 ggml_context 持有的那块大内存。

🔌 生活类比
ggml_context 像一块预先划好的停车场:开门营业时一次性圈下一整片地(mem_size), 之后每停一辆车(建一个张量或对象)就往后挪一个车位(bump 游标),不必每次都跑去物业重新申请地皮(malloc)。 收工时整片场地一次清空,干净利落。把"反复找物业"换成"自己在圈好的地里挪车位",正是 ggml 内存管理的全部精髓。把这个比喻记牢,这一课就成功了一半。

地基三件套

ggml 引擎最底层,其实就三样东西扣在一起:一份配置(你想要多大的池子)、一个内存池本体、以及池子里一个挨一个排着的 对象(每个对象包着一个张量或一张计算图)。先看它们的关系:

配置ggml_init_params
mem_size(多大)· mem_buffer(用谁的内存)· no_alloc(要不要给数据留位)
内存池ggml_context
持有一整块 arena,记着用到哪了(游标)、对象链表头尾
对象ggml_object -> ggml_tensor / ggml_cgraph
池子里一个接一个排开的对象,每个包着一个张量或一张图

这一层的关键词是"池":ggml 不会零散地为每个张量去找系统要内存,而是一次性拿一大块、自己在里面切。 下面三节就把这三件套逐一讲清。

这里先埋一个贯穿全课的对照:"元数据"和"数据"是两回事。元数据是"这个张量长什么样"——它的形状 ne、步长 nb、类型 type、 以及 op/src 这些(L05 讲过的字段),加起来不过几十上百字节;数据则是"那一大片真正的浮点数",一个权重矩阵可能就是几十 MB。 ggml_context 这块 arena,很多时候只装元数据,把笨重的数据留给后端去管。带着这个"轻元数据 / 重数据"的分野往下读,很多设计就顺理成章了。

ggml_context:一个内存池

一切从 ggml_init(params) 开始。你在 ggml_init_params 里告诉 ggml 三件事,它就回给你一个 ggml_context——一块已经备好的内存池:

// 简化自 ggml/include/ggml.h
struct ggml_init_params {
    size_t mem_size;     // arena 总大小
    void * mem_buffer;   // 传 NULL 则由 ggml 内部分配这块内存
    bool   no_alloc;     // true = 只建张量"元数据", 不为张量"数据"留位
};

ctx = ggml_init(params);   // 一次性拿到整块 arena
// ... 在 ctx 里建很多张量 ...
ggml_free(ctx);             // 一次性整体释放
ggml_init(params)
圈一块 arena
(mem_size)
->
ggml_context
内存池: arena + 游标
+ 对象链表
->
ggml_free(ctx)
整块一次性还掉

ggml_init 做的事很直接:先为 ggml_context 这个管理结构本身分配一点空间, 然后准备好那块大 arena——如果你传了 mem_buffer,它就用你给的内存;传 NULL, 它就自己 ggml_aligned_malloc(mem_size) 要一块对齐过的内存(源码见 ggml/src/ggml.c)。 ggml_free 则把这块 arena 整体还掉(只有当这块内存是 ggml 自己分配的、即 mem_buffer_owned 时才释放)。 "能让你传入 mem_buffer"这一点很重要:它意味着 ggml 可以在别人给的内存上工作,方便嵌入到各种环境、或复用一块缓冲反复建图。

🔬 细节 / 源码对应
顺带说说对齐ggml_aligned_malloc 要的不是普通内存,而是对齐到特定边界的内存——因为后端的 SIMD 指令(AVX、NEON 等,L07 提过)往往要求数据地址对齐才能高效甚至正确地读取。arena 内部每切一个对象,也会按 GGML_MEM_ALIGN 对齐。你可以把 arena 理解成一条带刻度的尺子,每个对象都落在整齐的刻度上,而不是随手乱放——这点整齐,换来的是计算时的速度。

所以严格说,ggml_aligned_malloc 与普通 malloc 的区别就在"对齐"二字:普通 malloc 只保证够大、不保证地址落在某个边界上; 而 ggml 要的内存,起始地址必须是某个对齐值(如 16 或 32 字节)的整数倍,这样后端才能放心地用对齐版的 SIMD 加载指令一次搬一大批数。对齐这件小事,体现的是 ggml"处处为后端计算让路"的取向。

🌍 宏观理解
还有一个常被问到的问题:一个程序里能开几个 ggml_context答案是多个,而且这很常见。比如可以用一个 ctx 装模型权重(活得久,整个推理期间都在)、另一个 ctx 装每步推理的计算图(活得短,算完就清)。不同生命周期的东西放进不同的池子,该长留的长留、该速清的速清,互不干扰——这也是 arena 模型带来的便利:一次 ggml_free 就能精准回收一整批同寿命的对象。

这套机制在 llama.cpp 里随处可见。加载一个模型时,loader 会先按 GGUF 头里记的张量数量和大小,估出需要多大的元数据 arenaggml_init 出一个(通常 no_alloc=true 的)context,再把每个权重张量在里面登记一遍;权重的真正数据则由后端缓冲承接(甚至直接 mmap 自文件,见 L13)。 每跑一步推理,又会用另一个 context 临时搭出这一步的计算图、算完即弃。所以你大可以把 ggml_context 想成 ggml 世界里最基本的"工作台": 要干活,先支一个台子;活干完,连台带料一起收走。理解了它,你就理解了 ggml 所有数据结构"住在哪里"。

no-malloc:bump 分配

拿到 arena 之后,每建一个张量、一张图,ggml 都再去找系统要内存,而是在这块 arena 里"往后推一格"。这就是 bump(碰撞指针)分配: 维护一个"用到哪了"的游标,来一个对象就把游标往右挪、就地放下:

bump 分配:所有对象在同一块 arena 里一个接一个排开,游标只进不退
arenaobj1obj2obj3空闲 …游标 ->
链表begin->obj1->obj2->obj3->endbegin/end 是头尾指针

每个对象前面都挂一个小小的 ggml_object 头(记着自己的偏移、大小、指向下一个对象的指针),它们串成一条链表; 游标永远停在最后一个对象的末尾。新建对象时,就从游标处往后切一块。把这个过程写成伪代码就一目了然:

多说一句那个 ggml_object 头里到底装了什么:自己在 arena 里的偏移 offs、占用的大小 size、 指向下一个对象的指针 next,外加一个标记"这是张量还是图"的类型字段。ggml_context 自己则记着链表的头尾 (objects_begin / objects_end)和已放对象数 n_objects。有了尾指针,"在末尾追加"就是 O(1),这是 bump 快的又一面。

🌍 宏观理解
为什么这种"只加不减"的游标能行得通?因为建图阶段几乎只增不删——你是在一口气把整张计算图搭出来,中途很少需要单独释放某个张量。既然没有"挖东墙补西墙"的需求,那最简单的分配器(一个往前推的游标)就够用了,连记录空闲块、合并碎片这些复杂逻辑都省了。这是一种典型的"用使用场景的特点,换分配器的极致简单":等到 L10 真正要复用内存时,才会上更聪明的分配器;而这里的建图阶段,朴素的 bump 反而最合适。
# 对应 ggml/src/ggml.c 的 ggml_new_object / ggml_new_tensor_impl
def new_object(ctx, size):
    cur = ctx.objects_end.offs + ctx.objects_end.size   # 当前游标
    if cur + size > ctx.mem_size:                       # 池子不够了
        abort("arena 空间不足")                          # 不扩容, 直接报错!
    obj = place_at(ctx.mem_buffer + cur)                # 就地放下
    link_into(ctx.objects, obj)                         # 接入链表尾
    return obj
⚠ 注意
这里有两个要点。其一,张量的元数据(那个 ggml_tensor 结构)和它的数据缓冲,都从这同一块 arena 里切——没有"每个张量单独 malloc 一次"这回事。其二,arena 不会自动扩容:游标一旦撞到边界,ggml 直接 abort。所以使用者要事先把池子估得足够大。ggml 提供了 ggml_tensor_overhead() 帮你算"每个张量的元数据要占多少字节",建图时常按 GGML_DEFAULT_GRAPH_SIZE = 2048 个节点的规模留余量。

举个具体感受:"元数据"到底有多轻?一个 ggml_tensor 结构加上对象头,ggml_tensor_overhead() 量出来不过几百字节。 就算一张图有上千个张量,元数据加起来也才几百 KB——和动辄几个 GB 的权重数据相比,几乎可以忽略。这再次印证了"轻元数据 / 重数据"的分野: 在 no_alloc=true 下,ggml_context 这块 arena 只需开几 MB 装下整张图的骨架就绰绰有余,真正吃内存的数据另有去处。

把两种分配方式并排一看,arena 的好处就很直观了:

每张量各自 malloc(ggml 这么做)

上千次系统调用,开销高;小块散落各处、易碎片;释放要一个个 free,容易漏;内存不连续、对缓存不友好。

arena + bump(ggml 的做法)

一次大分配,开销摊薄;对象紧挨着、缓存友好;游标只进不退,分配快到只是"加个数";收工一把 ggml_free 全清。

还有个容易被忽略的细节:bump 分配天然带来确定性。因为对象严格按建立顺序一个挨一个排开,同样的建图代码,每次跑出来的内存布局都一模一样, 这对调试、复现、以及后端按固定偏移读写都很有用。相比之下,malloc 返回的地址是不可预测的,每次运行都可能不同。 ggml 这种"可预测的连续布局",是它能把一张图整体搬到别的内存(比如先在 CPU 上规划、再映射到 GPU 缓冲)的隐形前提。

no_alloc:只建元数据,不占数据

回头看 ggml_init_params 里那个 no_alloc。当它为 true 时,ggml 在 arena 里 只给张量的"元数据"留位(type、ne、nb、op、src 这些,L05 讲过),不为张量的"数据"(那一大片浮点数)分配缓冲

no_alloc = false

arena 里给元数据数据缓冲都留位——张量能直接装下那片浮点数据。

no_alloc = true

只给元数据(type/ne/nb/op/src)留位,数据缓冲留给后端——"先描述、后分配"。

为什么要这样?因为很多时候,我们想做的只是"先把计算图搭出来"——这一步只需要知道每个张量的形状和依赖关系,根本还用不到真正的数据内存。 等图建好、看清全貌,再交给后端统一分配真正的数据缓冲(这正是 L09 惰性建图、L10 内存复用的前提)。所以 no_alloc=true 是"先描述、后分配"这套玩法的开关,你会在 llama.cpp 加载模型、搭计算图时反复见到它。

把这一课的内存观收个尾:一次完整的使用,是这样一条线——ggml_init 圈地(拿到 arena)-> 在 ctx 里 建张量、搭计算图(bump 切元数据)-> 交给后端分配真正的数据并执行(L09、L10)-> ggml_free 一把清空。 你会发现,ggml_context 始终扮演"轻量的脚手架":它让搭建过程几乎不花内存、不碎不漏,把真正的重活(几 GB 的权重数据) 留到看清全局之后、由更懂硬件的后端来扛。地基只负责"把架子稳稳搭起来",这正是它该有的样子。

所以这一课你真正要带走的,不是几个函数名,而是一个心智模型:ggml 里所有张量、所有图,都住在某个 ggml_context 的 arena 里; 它们靠 bump 一个挨一个排开、不单独 malloc;元数据在 ctx 里很轻,数据在后端缓冲里很重;用完整块一清。带着这个模型,下一课我们就去看:在这块 arena 上建起来的张量,是怎么靠 op/src 串成一张计算图的。

深入一点(选读)

下面三个问题,想深究的同学点开看;只想抓主线的可以先跳过。

1 为什么不直接 malloc / new 一个个分配张量? 点击展开

一次推理会建出成千上万个张量。如果每个都单独向系统申请内存,会有三个麻烦:分配器开销(每次 malloc/free 都有成本)、 内存碎片(零散的小块散落各处)、以及释放繁琐(要一个个记得 free)。

arena + bump 把这些一举解决:一次大分配摊薄了申请成本;对象在内存里紧挨着,对 CPU 缓存友好; 收工时 ggml_free 一把全清,不会漏。代价是你得预估池子大小——但对"建一张图"这种规模可预测的场景,这点代价非常划算。

其实这种取舍在系统软件里很普遍:通用分配器(malloc)什么场景都能用,但什么都不特别快;arena 放弃了"随时单独释放任意一块"的灵活性, 换来分配近乎免费、释放一步到位。ggml 之所以敢用 arena,正因为它的使用模式恰好匹配——一张图里的张量同生共死, 要么一起留着算,要么算完一起丢,几乎不存在"中途单独删一个张量"的需求。把通用工具换成贴合场景的专用工具,是性能工程里最常见的提速手法之一。早年很多游戏引擎、编译器内部都用同一招管理临时对象,原理和 ggml 这里如出一辙。

2 arena 不够会怎样?怎么估它的大小? 点击展开

游标撞到 mem_size 边界时,ggml 不会偷偷扩容,而是当场报错失败(调试版会直接 abort,发布版则返回空指针并打印警告;无论哪种,都不悄悄换语义)。 所以 mem_size 必须事先估够。

估法也不神秘:元数据部分 ≈ 张量个数 × ggml_tensor_overhead()(外加对象头); 数据部分(若 no_alloc=false)≈ 各张量字节数之和(用 L05 的 ggml_nbytes 思路)。 建图常按 GGML_DEFAULT_GRAPH_SIZE = 2048 个节点留出余量,省得精打细算。值得一提的是,这个"宁可一次开大、也不要中途不够"的态度,和它"超限直接 abort"的刚硬是一致的:ggml 把"内存够不够"这件事前置到建池子那一刻,让后面的分配永远稳稳当当、没有意外。

3 ggml_context 的内存和"后端内存"是一回事吗? 点击展开

不是,这是新手最容易混的一点。ggml_context 的 arena 在很多场景里只放元数据(配合 no_alloc=true); 而张量真正的数据,是由后端缓冲(如 CPU 内存、CUDA 显存)来分配的——那是 L10 里 ggml-alloc 的活儿。

换句话说,存在两层内存:一层是 ctx 里轻量的"图骨架/元数据",另一层是后端里厚重的"张量数据"。把这两层分开,正是 ggml 能 "在 CPU 上搭好一张图、再把数据分配到 GPU 上去算"的关键。后面两课会把这条线接上。

一个好记的划分:ctx 管"图长什么样",后端管"数有多少"。前者轻、可预测、用 arena 一把搭一把清;后者重、要复用、由 ggml-alloc 精打细算(L10)。 新手只要记住"看到 ggml_context 别以为权重就在里面",就避开了八成的内存困惑。

✅ 关键要点
  • ggml_context 是一个预分配的内存池(arena)ggml_init 一次性备好,ggml_free 一次性释放。
  • no-malloc / bump 分配ggml_new_object 在 arena 里往后推游标、就地放下,不为每个张量单独 malloc;对象串成 ggml_object 链表。
  • arena 不自动扩容,超出即报错失败(调试版 abort、发布版返回 NULL+警告);用 ggml_tensor_overhead() 估大小,图常按 GGML_DEFAULT_GRAPH_SIZE=2048 留余量。
  • no_alloc=true只建元数据、不占数据,为"先建图、后由后端分配"铺路。
  • 存在两层内存:ctx 的元数据 arena,与后端的张量数据缓冲(L10)。
💡 设计洞察
用"一次大分配 + 内部 bump"替代"成千上万次 malloc"——简单、快、可整体迁移、一把释放。正是这块轻量的内存池,让 ggml 能把一张计算图 几乎零成本地搭起来,再整体交给某个后端去分配数据、执行。地基朴素,却撑起了上面所有的精巧——记住"一次圈地、内部挪车位、用完整清"这三步,你就握住了 ggml 内存管理的全部要义。

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

1. ggml 会为每个张量都单独 malloc 一次内存吗?
  1. 会,每建一个张量就 malloc 一次
  2. 用垃圾回收器自动管理
  3. 不会,张量从 ggml_context 预分配的 arena 里 bump 切出,不做 per-tensor malloc
  4. 把每个张量都存到磁盘
看答案与解析 点击展开
答案:C。ggml_init 一次性备好一块 arena,ggml_new_object 在里面往后推游标就地放下;元数据和数据都从这块池子切,避免成千上万次 malloc。
2. ggml_init_params 里 no_alloc=true 意味着什么?
  1. 关闭量化
  2. 什么都不分配
  3. 把 context 设为只读
  4. 只分配张量元数据、不分配数据缓冲,为“先建图、后由后端分配”铺路
看答案与解析 点击展开
答案:D。建计算图只需要形状和依赖,用不到真正的数据内存;no_alloc 让 ctx 只存元数据,数据等图建好后由后端统一分配(L10)。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 为什么 arena 满了 ggml 选择直接 abort,而不是自动扩容?这对使用者提出了什么要求?

Part 2 gave you the intuition for tensors, quantization, and backends; from Part 3 on, we take the ggml engine apart. First stop: memory - how does ggml manage the memory of thousands of tensors? The answer is surprisingly plain: a memory pool called ggml_context, plus a "never scatter-malloc" allocation philosophy. Get this layer and the compute graph, execution, and operators all have ground to stand on.

First, the problem at hand: one inference conjures up thousands of tensors out of thin air - weights, intermediate activations, the KV cache, all tensors. If each one went to the operating system for memory, the overhead of "request, bookkeep, return" alone would drag the whole engine down. ggml's answer is: don't buy retail, buy wholesale. It fences off one big block at once and carves it carefully inside - that block is ggml_context. This lesson is about how it fences, how it carves, and why it is designed this way.

While we are at it, let me name the technique: this way of "grab one big block, carve internally, free wholesale" has a common name in systems programming - an arena (or region) allocator, also called a linear allocator. It is not ggml's invention but a well-worn trick in high-performance code; ggml just applies it to the perfectly-suited scenario of "managing all the tensors of one compute graph". When we say arena below, we mean that big block held by ggml_context.

🔌 Analogy
ggml_context is like a pre-marked parking lot: at opening you fence off a whole plot at once (mem_size), then each car you park (every tensor or object) just moves forward one slot (a bump cursor), without running to the office to requisition land each time (malloc). At close, the whole lot clears in one go - clean and tidy. Swapping "keep calling the office" for "shuffle slots in your own fenced lot" is the whole essence of ggml memory management. Hold onto this image and you are halfway through the lesson.

The three foundation pieces

At ggml's lowest layer there are really just three things buckled together: a config (how big a pool you want), the pool itself, and the objects lined up one after another inside it (each wrapping a tensor or a compute graph). First, how they relate:

configggml_init_params
mem_size (how big) - mem_buffer (whose memory) - no_alloc (reserve data space?)
poolggml_context
holds one whole arena, tracking the cursor (how far used) and the object list head/tail
objectsggml_object -> ggml_tensor / ggml_cgraph
objects laid out one after another, each wrapping a tensor or a graph

The keyword here is "pool": ggml does not scatter-request memory per tensor; it grabs one big block once and carves inside it. The next three sections walk through these three pieces.

Let me plant a contrast that runs through the whole lesson: "metadata" and "data" are two different things. Metadata is "what this tensor looks like" - its shape ne, strides nb, type, and op/src (the fields from L05) - adding up to mere tens or hundreds of bytes; data is "the big slab of actual floats", where one weight matrix might be tens of MB. The ggml_context arena very often holds only metadata, leaving the heavy data to the backend. Read on with this "light metadata / heavy data" split in mind and many design choices fall into place.

ggml_context: a memory pool

It all starts with ggml_init(params). You tell ggml three things in ggml_init_params and it hands back a ggml_context - a ready memory pool:

// simplified from ggml/include/ggml.h
struct ggml_init_params {
    size_t mem_size;     // total arena size
    void * mem_buffer;   // NULL = ggml allocates this block internally
    bool   no_alloc;     // true = build tensor "metadata" only, no data buffer
};

ctx = ggml_init(params);   // grab the whole arena at once
// ... build many tensors in ctx ...
ggml_free(ctx);             // free it all at once
ggml_init(params)
claim one arena
(mem_size)
->
ggml_context
pool: arena + cursor
+ object list
->
ggml_free(ctx)
return the whole
block at once

ggml_init is straightforward: it allocates a little space for the ggml_context management struct itself, then prepares that big arena - if you passed a mem_buffer it uses your memory; pass NULL and it does ggml_aligned_malloc(mem_size) itself (see ggml/src/ggml.c). ggml_free returns the whole arena (only freeing the block if ggml allocated it itself, i.e. mem_buffer_owned). That "you can pass in mem_buffer" matters: ggml can work on memory someone else gave it, handy for embedding into various environments or reusing one buffer to build graphs repeatedly.

🔬 Details / source
A word on alignment. ggml_aligned_malloc wants not just any memory but memory aligned to a particular boundary - because backend SIMD instructions (AVX, NEON, from L07) often require aligned addresses to read efficiently or even correctly. Each object carved inside the arena is also aligned to GGML_MEM_ALIGN. Think of the arena as a ruler with tick marks: every object lands on a tidy tick rather than wherever - and that bit of tidiness buys speed at compute time.

So strictly, the difference between ggml_aligned_malloc and plain malloc is just "alignment": plain malloc only guarantees big-enough, not that the address falls on a boundary; ggml's memory must start at a multiple of some alignment (16 or 32 bytes), so the backend can confidently use aligned SIMD loads to move a batch at once. This small thing reflects ggml's bias of "always making way for backend compute".

🌍 Big picture
One more often-asked question: how many ggml_contexts can a program open? The answer is several, and that is common. For instance, one ctx for the model weights (long-lived, present the whole inference) and another for each step's compute graph (short-lived, cleared once computed). Putting things of different lifetimes into different pools lets the long-lived stay and the short-lived clear fast, without interfering - another convenience of the arena model: one ggml_free precisely reclaims a whole batch of same-lifetime objects.

This mechanism is everywhere in llama.cpp. When loading a model, the loader first estimates how big a metadata arena it needs from the tensor count and sizes in the GGUF header, ggml_inits a (usually no_alloc=true) context, and registers every weight tensor in it; the weights' real data is taken up by backend buffers (or even mmap'd straight from the file, see L13). Each inference step uses another context to temporarily build that step's compute graph, discarded once done. So you can picture ggml_context as ggml's most basic "workbench": to work, set up a bench; when done, clear bench and materials together. Understand it and you understand "where" all of ggml's data structures live.

no-malloc: bump allocation

Once it has the arena, every tensor or graph it builds does not go back to the system for memory; it just "pushes forward one slot" inside the arena. That is bump (pointer-bump) allocation: keep a "how far used" cursor, and for each object move the cursor right and place it in situ:

bump allocation: all objects line up in the same arena, the cursor only moves forward
arenaobj1obj2obj3free ...cursor ->
listbegin->obj1->obj2->obj3->endbegin/end are head/tail pointers

Each object is prefixed with a tiny ggml_object header (recording its offset, size, and a pointer to the next object); they form a linked list, and the cursor always rests at the end of the last object. A new object is carved from the cursor forward. As pseudocode it is clear at a glance:

A word more on what that ggml_object header holds: its offset offs in the arena, the size size it occupies, a pointer next to the next object, plus a type field marking "tensor or graph". ggml_context itself tracks the list head/tail (objects_begin / objects_end) and the object count n_objects. With a tail pointer, "append at the end" is O(1) - another face of why bump is fast.

🌍 Big picture
And why does this "only-add, never-remove" cursor work? Because the build phase is almost append-only - you are constructing the whole compute graph in one go, rarely needing to free a single tensor mid-way. With no "rob Peter to pay Paul" need, the simplest allocator (a forward-pushing cursor) suffices, sparing all the complexity of tracking free blocks and merging fragments. This is a classic "trade the scenario's traits for the allocator's utter simplicity": not until L10 actually needs to reuse memory does a smarter allocator come in; here in the build phase, plain bump is the best fit.
# cf. ggml_new_object / ggml_new_tensor_impl in ggml/src/ggml.c
def new_object(ctx, size):
    cur = ctx.objects_end.offs + ctx.objects_end.size   # current cursor
    if cur + size > ctx.mem_size:                       # pool is out of room
        abort("arena out of space")                     # no growth, just fail!
    obj = place_at(ctx.mem_buffer + cur)                # place in situ
    link_into(ctx.objects, obj)                         # append to the list
    return obj
⚠ Heads-up
Two points here. One, a tensor's metadata (the ggml_tensor struct) and its data buffer are both carved from this same arena - there is no "malloc once per tensor". Two, the arena does not auto-grow: the moment the cursor hits the edge, ggml aborts. So the user must size the pool large enough up front. ggml offers ggml_tensor_overhead() to compute "how many bytes each tensor's metadata takes", and graphs commonly leave headroom for GGML_DEFAULT_GRAPH_SIZE = 2048 nodes.

For a concrete feel: just how light is "metadata"? A ggml_tensor struct plus object header, as measured by ggml_tensor_overhead(), is only a few hundred bytes. Even a graph with thousands of tensors totals just a few hundred KB of metadata - next to the multiple GB of weight data, practically nothing. This again confirms the "light metadata / heavy data" split: under no_alloc=true, the ggml_context arena need only be a few MB to comfortably hold the whole graph's skeleton; the truly memory-hungry data lives elsewhere.

Put the two allocation styles side by side and the arena's benefits are obvious:

malloc per tensor (ggml does not do this)

thousands of syscalls, high overhead; small blocks scattered everywhere, prone to fragmentation; freeing means one-by-one, easy to leak; non-contiguous memory, cache-unfriendly.

arena + bump (ggml's way)

one big allocation, overhead amortized; objects adjacent, cache-friendly; the cursor only moves forward, allocation as fast as "add a number"; at close, one ggml_free clears all.

One easily-missed detail: bump allocation naturally yields determinism. Because objects line up strictly in creation order, the same graph-building code produces the exact same memory layout every run - useful for debugging, reproduction, and for the backend reading/writing at fixed offsets. By contrast, malloc returns unpredictable addresses that may differ each run. ggml's "predictable contiguous layout" is the invisible premise that lets it move a whole graph to other memory (e.g. plan on CPU first, then map to a GPU buffer).

no_alloc: metadata only, no data

Back to that no_alloc in ggml_init_params. When it is true, ggml reserves space in the arena only for a tensor's "metadata" (type, ne, nb, op, src - from L05), not for the tensor's "data" (that big slab of floats).

no_alloc = false

the arena reserves room for metadata and the data buffer - tensors can hold that float data directly.

no_alloc = true

only metadata (type/ne/nb/op/src) gets room; the data buffer is left to the backend - "describe first, allocate later".

Why? Because often all we want is to "build the compute graph first" - that step only needs each tensor's shape and dependencies, not real data memory yet. Once the graph is built and the whole picture is clear, we hand it to the backend to allocate the real data buffers in one pass (exactly the premise of L09's lazy build and L10's memory reuse). So no_alloc=true is the switch for the "describe first, allocate later" approach, which you will see again and again as llama.cpp loads models and builds graphs.

To wrap up this lesson's view of memory: a full usage is one line - ggml_init fences the land (gets the arena) -> build tensors and the compute graph in the ctx (bump-carve metadata) -> hand to the backend to allocate real data and execute (L09, L10) -> ggml_free clears it all. You will notice ggml_context always plays the role of a lightweight scaffold: it makes the building process cost almost no memory, no fragmentation, no leaks, leaving the truly heavy lifting (the multi-GB weight data) for after the whole picture is clear, carried by a more hardware-aware backend. The foundation only "holds the frame steady" - exactly as it should.

So what you should really take away is not a few function names but a mental model: every tensor and every graph in ggml lives in some ggml_context arena; they line up via bump, no individual malloc; metadata is light in the ctx, data is heavy in the backend buffer; clear the whole block when done. With this model, the next lesson goes to see how the tensors built on this arena are strung via op/src into a compute graph.

Going deeper (optional)

Three questions below; open them if you want depth, skip them if you only want the main line.

1 Why not just malloc / new each tensor individually? click to expand

One inference builds thousands of tensors. Requesting memory individually for each brings three headaches: allocator overhead (every malloc/free has a cost), fragmentation (scattered small blocks all over), and tedious freeing (you must remember to free each one).

arena + bump solves all at once: one big allocation amortizes the request cost; objects sit adjacent in memory, cache-friendly; and at close ggml_free clears everything in one shot, nothing leaked. The cost is having to estimate the pool size - but for the predictable scale of "building one graph", that is a very worthwhile trade.

This trade is common in systems software: a general allocator (malloc) works for any scenario but is not especially fast at any; an arena gives up the flexibility of "free any one block anytime" in exchange for near-free allocation and one-step release. ggml dares to use an arena precisely because its usage pattern fits - tensors in a graph live and die together, kept to compute or dropped at once, with almost no need to "delete one tensor mid-way". Swapping a general tool for a scenario-fitting specialized one is one of the most common speedups in performance engineering; old game engines and compilers used the same trick for temporary objects, on the very same principle as ggml here.

2 What if the arena runs out? How do you size it? click to expand

When the cursor hits the mem_size edge, ggml does not silently grow; it fails on the spot (debug builds abort outright; release builds return a NULL pointer and print a warning - either way it does not quietly change semantics). So mem_size must be estimated big enough up front.

The estimate is not mysterious: the metadata part ~= number of tensors x ggml_tensor_overhead() (plus object headers); the data part (if no_alloc=false) ~= the sum of each tensor's bytes (the L05 ggml_nbytes idea). When building a graph, people commonly just leave headroom for GGML_DEFAULT_GRAPH_SIZE = 2048 nodes rather than count precisely. Notably, this "rather open big than fall short mid-way" attitude matches its hard-line "abort on overflow": ggml front-loads the "is there enough memory" question to pool-creation time, so every later allocation is rock-steady, no surprises.

3 Is ggml_context's memory the same as "backend memory"? click to expand

No - this is the thing beginners most often conflate. The ggml_context arena in many scenarios holds only metadata (with no_alloc=true); a tensor's real data is allocated by a backend buffer (CPU memory, CUDA VRAM) - that is L10's ggml-alloc work.

In other words, there are two layers of memory: a light "graph skeleton / metadata" layer in the ctx, and a heavy "tensor data" layer in the backend. Separating these two is exactly what lets ggml "build a graph on the CPU, then allocate the data on the GPU to compute". The next two lessons connect this line.

An easy division to remember: the ctx manages "what the graph looks like", the backend manages "how much data there is". The former is light, predictable, set up and cleared with an arena; the latter is heavy, must be reused, and is carefully managed by ggml-alloc (L10). Beginners need only remember "seeing ggml_context does not mean the weights are inside it" to dodge eighty percent of memory confusion.

✅ Key points
  • ggml_context is a pre-allocated memory pool (arena); ggml_init sets it up once, ggml_free releases it once.
  • no-malloc / bump allocation: ggml_new_object pushes the cursor forward in the arena and places in situ, no per-tensor malloc; objects form a ggml_object linked list.
  • The arena does not auto-grow; overflow fails on the spot (debug abort, release returns NULL+warning); size it with ggml_tensor_overhead(), graphs leave headroom for GGML_DEFAULT_GRAPH_SIZE=2048.
  • no_alloc=true builds metadata only, no data, paving the way for "build the graph first, let the backend allocate later".
  • There are two memory layers: the ctx metadata arena, and the backend tensor-data buffer (L10).
💡 Design insight
Replacing "thousands of mallocs" with "one big allocation + internal bump" - simple, fast, wholesale-movable, freed in one shot. It is this lightweight memory pool that lets ggml build a compute graph at near-zero cost, then hand it wholesale to a backend to allocate data and execute. A plain foundation, yet it carries all the cleverness above it - remember "fence once, shuffle slots inside, clear all when done" and you hold the whole gist of ggml memory management.

🧪 Self-test - think about the design

1. Does ggml malloc memory separately for every single tensor?
  1. Yes, it mallocs once per tensor created
  2. It uses a garbage collector
  3. No - tensors are bump-carved from the arena pre-allocated by ggml_context, with no per-tensor malloc
  4. It stores each tensor to disk
Show answer & explanation click to expand
Answer: C. ggml_init prepares one arena up front; ggml_new_object bumps a cursor and places in situ. Metadata and data are both carved from this pool, avoiding thousands of mallocs.
2. What does no_alloc=true in ggml_init_params mean?
  1. Turn off quantization
  2. Allocate nothing at all
  3. Make the context read-only
  4. Allocate tensor metadata only, no data buffer - paving the way for "build the graph first, let the backend allocate later"
Show answer & explanation click to expand
Answer: D. Building a graph needs only shapes and dependencies, not real data memory; no_alloc keeps the ctx metadata-only, and the backend allocates data once the graph is built (L10).
💭 Open questions (no single right answer - just think or try)
  • Why does ggml abort outright when the arena is full instead of auto-growing? What does that demand of the user?