ggml 是 llama.cpp 的计算引擎,而在它眼里一切数据都是"张量"——权重、激活值、KV cache,全是张量。 这一课把 ggml_tensor 这个结构体讲到"摸得着":什么是 shape、什么是 stride、为什么 ggml 用"行优先", 以及为什么"转置一个张量"几乎不花钱。看懂这一层,后面所有计算图和算子的示意图你都能读懂;可以说,张量是读 ggml 源码的"第一块积木"。
一个张量说白了就是"一块连续内存 + 一份怎么解释它的说明书"。说明书里有:数据类型 (type,比如 F32、F16、Q4_0)、每个维度有多少元素(ne[],ggml 最多 4 维), 以及一个指向那块内存的指针 data。先看"形状"长什么样:
顺便厘清"几维"这件事:0 维是一个标量(一个数),1 维是一个向量(一串数,比如一个词向量), 2 维是一个矩阵(比如一层的权重),3 维、4 维则常见于"带批次、带多头"的中间结果。ggml 看 ne[] 里实际大于 1 的维数来判断张量是几维;没用到的高维就填 1。所以当你看到 ne=[4,3,1,1] 时,它其实就是个 4×3 的二维张量。
注意 ggml 的约定:ne[0] 是最内层、变化最快的维度(在内存里挨着摆),ne[1] 是"下一排", 以此类推。这和很多人习惯的"行 × 列"写法正好相反——后面有专门的深挖讲这个坑。把 ggml_tensor 的核心字段摊开看,就这么几样:
struct ggml_tensor { enum ggml_type type; // F32 / F16 / Q4_0 ... int64_t ne[4]; // #elements per dim (ne[0] = innermost) size_t nb[4]; // byte strides enum ggml_op op; // how it was produced (graph node) struct ggml_tensor * src[...]; // inputs (back-pointers) struct ggml_tensor * view_src; // set if this tensor is a view void * data; // the actual bytes char name[...]; }; // 简化自 ggml/include/ggml.h, GGML_MAX_DIMS = 4
这里有两组字段值得记住。ne / nb / type / data 描述"这块数据长什么样、在哪";而 op / src 描述"它是怎么算出来的"—— op 是产生它的算子(比如矩阵乘),src 是指向输入张量的反向指针。后面这组正是 ggml "先建计算图、再执行"的关键:每个张量都记得自己的来历,把它们顺着 src 串起来,就是一张计算图 (课 03 提过、第三部分会展开)。另外别忽略 type:同样 1000 个元素,F32 占 4000 字节、Q4_0 只占 500 多字节—— 类型直接决定了这块内存有多大,这也是量化能省显存的根。
再说说 type。ggml 支持一长串数据类型:全精度的 F32、半精度的 F16 / BF16,以及一大家子量化类型(Q4_0、Q8_0、 Q4_K 等等,下一课专门讲)。同一个张量结构、同一套 ne/nb 逻辑,能装下这么多种类型,靠的就是用 type 这一个字段统一描述"每个元素(或每块)多少字节、怎么解释"。这种"结构不变、类型可换"的设计,让量化能无缝接进 已有的张量与算子体系,而不必为每种精度各写一套。也正因如此,换个量化等级(比如从 Q4_0 换到 Q5_K) 对上层代码几乎是透明的——变的只是 type 和每块的字节数,ne/nb 的那套逻辑原封不动。
有人可能会问:装个多维数组,直接用 C++ 的 std::vector 或裸数组不就行了,何必单独造一个 ggml_tensor?因为 ggml 要的远不止"存一堆数":它要能把运算描述成图(靠 op/src)、要能 让数据躺在不同后端的内存上(靠 buffer)、要能统一容纳量化等多种类型(靠 type 和按块的 nb)、还要能 零拷贝地变形(靠 ne/nb 与 view_src)。这些需求叠在一起,才有了这个看似简单、其实精心设计的结构体——它是整个引擎的"原子"。
内存其实是一维的(一长条字节),可张量是多维的,把多维"压平"到一维靠的就是 stride(步长)。 ggml 用 nb[i] 记录"第 i 维每加 1,要在内存里跳过多少字节"。ggml 是行优先的: ne[0] 那一维在内存里连续摆放、步长最小。看一个 ne=[3,2](每排 3 个、共 2 排)的 F32 张量是怎么躺在内存里的:
看这条字节流就懂了:同一排里相邻元素隔 4 字节(一个 F32),所以 nb[0]=4;从第 0 排跳到第 1 排 (高亮那个 a10)要跨过整整一排 3 个元素 = 12 字节,所以 nb[1]=12。这套规则写成公式,就藏在 ggml.h 的注释里:nb[0]=ggml_type_size(type)(一个元素多少字节)、 nb[1]=nb[0]*(ne[0]/ggml_blck_size(type))(跨一整排的字节,普通类型就是"每元素字节 × 一排元素数")、 再往上 nb[i]=nb[i-1]*ne[i-1](严格说公式里还有一个"对齐填充"项,连续、无填充的张量这一项为 0,这里略去)。于是给任意一个多维下标,把它和 nb 点乘一下,就得到字节偏移:
# 多维下标 (i0, i1, i2, i3) -> 内存里的字节偏移 offset = i0*nb[0] + i1*nb[1] + i2*nb[2] + i3*nb[3] ptr = (char*)tensor->data + offset # 就是这个元素的地址
为什么是 4 维(GGML_MAX_DIMS = 4)?因为推理里的张量基本不超过 4 维——典型的就像 "批次 × 序列 × 头数 × 每头维度"这种组合,4 维足够覆盖;用一个定长的小数组存 ne/nb,既简单又快,不必动态分配。 还有一个容易忽略的点:data 指向的内存不一定在 CPU 上——它可能在 GPU 显存里,张量另有一个 buffer 字段记录"这块数据归哪个后端管",这正好呼应课 07 要讲的多后端(同一个张量结构,数据可以躺在不同硬件上)。
把前面的公式用一个具体例子走一遍,印象会更深。还用上面那张内存图里的 ne=[3,2] 的 F32 张量:每个元素 4 字节,于是 nb[0]=4;跨一整排要越过 3 个元素,于是 nb[1]=4×3=12。想取第 1 排第 2 个元素(下标 i1=1, i0=2,从 0 数起,也就是图里的 a12),它的字节偏移就是 1×nb[1] + 2×nb[0] = 12 + 8 = 20—— 正好是内存图里 a12 底下标的那个 20。整块张量一共 3×2×4 = 24 字节。你看,只要有 ne、nb 和 data,任意一个元素的地址 都能一步算出来,这就是 stride 的全部威力;至于 ne=[4,3] 之类的别的形状,留给本课末尾的思考题自己算一遍。
反过来,知道了字节布局,你也就明白了为什么"按行遍历"通常比"按列遍历"快:顺着 ne[0](行内)走, 访问的是内存里连续相邻的字节,对 CPU 缓存最友好;而按高维跳着走,每次都跨一大步,更容易频繁缺失缓存、拖慢速度。 很多算子实现都刻意顺着连续维来安排循环,正是这个道理——布局决定性能,这条直觉在后面看内核实现时还会反复用到。
现在来看 ggml 一个非常"省"的设计。既然"形状(ne/nb)"和"数据(data)"是分开存的,那么很多"改变形状" 的操作,根本不用动数据,只要改几个数字。最典型的就是转置:把一个 [行, 列] 的矩阵转成 [列, 行],ggml 只是 交换 ne[0] 与 ne[1]、同时交换 nb[0] 与 nb[1],data 一个字节都不搬:
ne=[3,2],nb=[4,12]
data -> 一块真实内存
ne=[2,3],nb=[12,4]
view_src -> 指回原张量,data 不变
顺带提一个常见操作:广播(broadcast)。当两个形状不完全相同的张量要做逐元素运算(比如给每一行都加上同一个偏置向量), ggml 允许某些维度上"一个元素当很多元素用"——靠的还是 stride 的小把戏:把那一维的 nb 设成 0,下标怎么变、 地址都不动,于是同一个值被反复读取,看起来就像"复制"了一遍,实际上一个字节都没多占。这又是一次"改 nb 而不搬 data"的典型, 和 view、转置一脉相承。
就拿转置来说,把它摊开看最清楚:同一排内存,原张量横着读、转置竖着读,6 个值一个都没动。
一个最常见的 reshape 例子:把形状 [n_embd, n_tokens] 的激活,按多头注意力的需要"摊"成 [head_dim, n_head, n_tokens]——元素总数没变(n_embd = head_dim × n_head),数据也没搬, 只是重新解释了 ne/nb,就把"一个大向量"看成了"若干个头各自的小向量"。课 04 说的多头注意力里,大量这种"同一块数据、换个形状看"的操作, 靠的全是视图,几乎不产生额外拷贝。
顺便说清"连续"到底指什么:一个张量连续,意思是它的元素在内存里就是紧挨着、按 ne[0]、ne[1]… 顺序一个不落地排的 (也就是 nb 严格按前面的公式递推)。转置、某些切片会打破这种整齐:元素还是那些元素,但"走的顺序"和"内存摆放"对不上了,于是 ggml_is_contiguous 返回假。多数算子能直接吃连续张量;遇到必须连续的场合,ggml_cont 会按当前形状把数据重新誊抄成一块整齐的新内存。记住这条,你调试形状相关的问题时会少踩很多坑。
结构体里还有个不起眼但很实用的字段 name:每个张量可以带一个名字。这在调试时很有用(打印计算图时一眼认出 "这是哪个权重"),而且 GGUF 文件里的每个权重张量本来就是带名字存的(像 blk.0.attn_q.weight 这种), 加载时按名字对号入座。所以"名字"不只是注释,它是模型权重和代码之间的索引。你在 GGUF 工具或调试日志里看到的那一串张量名,正是来自这个字段。
下面三个问题,想深究的同学点开看;只想抓主线的可以先跳过。
在 numpy / PyTorch 里,习惯把最后一维当作内存里连续的维度(行优先、C-order):一个形状 [batch, seq, dim] 的张量,dim 是连续的。ggml 反过来:它把连续的那一维放在 ne[0](最前面),所以同一个张量在 ggml 里写成 ne = [dim, seq, batch]——维度顺序整个反过来。
这不是谁对谁错,只是约定不同;但读 ggml 代码、看张量形状时一定要在脑子里切换过来,否则很容易把行当成列、把 batch 看成 dim。 一个好记的口诀:ggml 的 ne[0] 永远是"最贴着内存、变化最快"的那一维。
普通类型(F32、F16)里,nb[0] 就是一个元素的字节数。但量化类型(如 Q4_0)不是"一个元素一个值", 而是把一整块(如 32 个权重)打包压成定长字节,单个权重没法独立寻址。
所以 ggml 用 ggml_blck_size(type)(一块里有几个元素)和 ggml_type_size(type)(一块多少字节) 来描述。nb[1] 公式里那个 ne[0] / ggml_blck_size(type),意思就是"这一排里有多少块"。 明白这点,你就懂了为什么量化张量不能像普通数组那样随便按单元素下标去取——得按块解量化(第三部分的量化格式课会细讲)。
直接用 ggml_nbytes(tensor)。直觉上,一个连续张量的字节数约等于"最高维元素数 × 最高维步长" (ne[k] * nb[k] 取最高维),也就是把各维元素数乘起来、再乘上每元素(或每块)的字节数。
这在估算显存占用时很有用:模型权重占多少、KV cache 占多少,本质上都是这么一类张量的字节数加总。非连续张量、量化张量的算法略有不同, 但 ggml_nbytes 已经替你把这些情况都处理好了,直接调用即可。
ggml is llama.cpp's compute engine, and in its eyes all data is "tensors" - weights, activations, the KV cache, all tensors. This lesson makes the ggml_tensor struct tangible: what shape is, what stride is, why ggml is "row-major", and why "transposing a tensor" costs almost nothing. Understand this layer and you can read every compute-graph and operator diagram that follows; tensors are, you could say, the first building block for reading ggml source.
A tensor is really "one contiguous block of memory + a manual for how to read it". The manual has: the data type (type, e.g. F32, F16, Q4_0), how many elements per dimension (ne[], up to 4 dims in ggml), and a pointer data to that memory. First, what "shape" looks like:
A quick clarification of "how many dims": 0-D is a scalar (one number), 1-D a vector (e.g. one embedding), 2-D a matrix (e.g. a layer's weights), and 3-D / 4-D show up in "batched, multi-head" intermediates. ggml infers a tensor's rank from how many of ne[] are greater than 1; unused higher dims are filled with 1. So ne=[4,3,1,1] is really just a 4x3 two-dimensional tensor.
Note ggml's convention: ne[0] is the innermost, fastest-changing dimension (laid out contiguously in memory), ne[1] is "the next row", and so on. This is the opposite of the "rows x cols" ordering many people are used to - a dedicated deep-dive below covers this trap. Here are the core fields of ggml_tensor:
struct ggml_tensor { enum ggml_type type; // F32 / F16 / Q4_0 ... int64_t ne[4]; // #elements per dim (ne[0] = innermost) size_t nb[4]; // byte strides enum ggml_op op; // how it was produced (graph node) struct ggml_tensor * src[...]; // inputs (back-pointers) struct ggml_tensor * view_src; // set if this tensor is a view void * data; // the actual bytes char name[...]; }; // simplified from ggml/include/ggml.h, GGML_MAX_DIMS = 4
Two groups of fields are worth remembering. ne / nb / type / data describe "what this data looks like and where it is"; while op / src describe "how it was computed" - op is the operator that produced it (e.g. matmul), src are back-pointers to input tensors. That second group is the key to ggml's "build the graph first, then execute": every tensor remembers its origin, and stringing them together via src is a compute graph (mentioned in lesson 03, expanded in Part 3). Don't overlook type either: the same 1000 elements take 4000 bytes as F32 but only ~500 as Q4_0 - the type alone decides how big this memory is, which is the root of how quantization saves memory.
About type: ggml supports a long list of data types - full-precision F32, half-precision F16 / BF16, and a whole family of quantized types (Q4_0, Q8_0, Q4_K, etc. - next lesson). The same tensor struct and the same ne/nb logic hold all of them, because one field, type, uniformly describes "how many bytes per element (or per block), and how to interpret them". This "same structure, swappable type" design lets quantization plug seamlessly into the existing tensor and operator machinery without rewriting it per precision. Because of this, switching quantization levels (say Q4_0 to Q5_K) is nearly transparent to higher-level code - only the type and per-block byte count change; the ne/nb logic is untouched.
You might ask: to hold a multi-dim array, why not just use C++'s std::vector or a raw array - why a dedicated ggml_tensor? Because ggml needs far more than "store some numbers": it must describe computation as a graph (via op/src), let data live in different backends' memory (via buffer), uniformly hold many types including quantized ones (via type and per-block nb), and reshape with zero copies (via ne/nb and view_src). All these needs together produced this seemingly simple but carefully designed struct - the engine's "atom".
Memory is actually one-dimensional (a long strip of bytes), but tensors are multi-dimensional; what flattens many dims into one is stride. ggml uses nb[i] to record "how many bytes to skip in memory when dim i increases by 1". ggml is row-major: the ne[0] dimension is laid out contiguously with the smallest stride. Here is how an ne=[3,2] (3 per row, 2 rows) F32 tensor lies in memory:
This byte stream makes it click: adjacent elements in a row are 4 bytes apart (one F32), so nb[0]=4; jumping from row 0 to row 1 (the highlighted a10) skips a whole row of 3 elements = 12 bytes, so nb[1]=12. As a formula - straight from the comments in ggml.h: nb[0]=ggml_type_size(type) (bytes per element), nb[1]=nb[0]*(ne[0]/ggml_blck_size(type)) (bytes to cross one row; for plain types just "bytes-per-element x elements-per-row"), and above that nb[i]=nb[i-1]*ne[i-1] (strictly there is also an alignment "padding" term, which is 0 for contiguous unpadded tensors and is omitted here). So given any multi-dim index, dot it with nb to get the byte offset:
# multi-dim index (i0, i1, i2, i3) -> byte offset in memory offset = i0*nb[0] + i1*nb[1] + i2*nb[2] + i3*nb[3] ptr = (char*)tensor->data + offset # the address of this element
Why 4 dims (GGML_MAX_DIMS = 4)? Because inference tensors rarely exceed 4 dims - a typical shape is "batch x sequence x heads x per-head-dim", and 4 is enough; storing ne/nb in a small fixed-size array is simple and fast, with no dynamic allocation. One more easily-missed point: the memory data points to is not necessarily on the CPU - it may live in GPU memory, and the tensor has a separate buffer field recording "which backend owns this data". That foreshadows the multi-backend story of lesson 07 (the same tensor struct, with data living on different hardware).
Walking the formula through a concrete example makes it stick. Using the same ne=[3,2] F32 tensor from the memory diagram above: each element is 4 bytes, so nb[0]=4; crossing a whole row skips 3 elements, so nb[1]=4x3=12. To fetch row 1, element 2 (index i1=1, i0=2, zero-based - a12 in the diagram), the byte offset is 1*nb[1] + 2*nb[0] = 12 + 8 = 20 - exactly the 20 marked under a12. The whole tensor is 3x2x4 = 24 bytes. So with ne, nb, and data, any element's address is one step away - that is the full power of stride; other shapes like ne=[4,3] are left for this lesson's closing exercise.
Conversely, knowing the byte layout explains why "row-major traversal" is usually faster than "column-major": walking along ne[0] (within a row) touches contiguous, adjacent bytes, friendliest to the CPU cache; jumping along higher dims takes a big stride each time and misses the cache more often. Many operator implementations deliberately loop along the contiguous dimension for exactly this reason - layout decides performance, an intuition we will reuse when reading kernel implementations later.
Now a very "thrifty" ggml design. Since "shape (ne/nb)" and "data" are stored separately, many "reshape" operations need not touch the data at all - just change a few numbers. The classic is transpose: turning a [rows, cols] matrix into [cols, rows], ggml merely swaps ne[0] with ne[1] and nb[0] with nb[1], moving data by not a single byte:
ne=[3,2], nb=[4,12]
data -> a real block of memory
ne=[2,3], nb=[12,4]
view_src -> points back, data unchanged
One common operation in passing: broadcast. When two not-quite-same-shape tensors do an element-wise op (e.g. adding the same bias vector to every row), ggml lets some dimension use "one element as many" - again via a stride trick: set that dim's nb to 0, so no matter how the index changes the address does not, and the same value is re-read, looking "copied" while taking not one extra byte. Another "change nb, don't move data" classic, of a piece with views and transpose.
Take transpose itself, laid out in full: the same row of memory, read across as the original and down as the transpose - none of the 6 values moved.
A very common reshape: take activations of shape [n_embd, n_tokens] and "fan them out" for multi-head attention into [head_dim, n_head, n_tokens] - the element count is unchanged (n_embd = head_dim x n_head) and no data moves; only ne/nb are reinterpreted, turning "one big vector" into "several heads' small vectors". The multi-head attention from lesson 04 is full of these "same data, different shape" operations, all done with views and almost no extra copies.
To spell out what "contiguous" means: a tensor is contiguous when its elements sit adjacent in memory, in ne[0], ne[1], ... order without gaps (i.e. nb follows the formula above exactly). Transpose and some slices break this: same elements, but the "walk order" no longer matches the memory layout, so ggml_is_contiguous returns false. Most operators handle contiguous tensors directly; where contiguity is required, ggml_cont copies the data into a fresh tidy block in the current shape. Remember this and you will dodge many shape-related debugging traps.
The struct also has a humble but handy field, name: each tensor can carry a name. This helps when debugging (spotting "which weight is this" when printing the graph), and GGUF stores each weight tensor with a name (like blk.0.attn_q.weight), matched up by name at load time. So a "name" is not just a comment - it is the index between model weights and code. The tensor names you see in GGUF tools or debug logs come straight from this field.
Three questions below; open them if you want depth, skip them if you only want the main line.
In numpy / PyTorch, the last dimension is the contiguous one (row-major, C-order): for a tensor of shape [batch, seq, dim], dim is contiguous. ggml flips this: it puts the contiguous dimension at ne[0] (first), so the same tensor is written ne = [dim, seq, batch] - the whole order reversed.
Neither is "right"; it is just a different convention. But when reading ggml code and shapes you must mentally switch, or you will mistake rows for columns and batch for dim. A handy mnemonic: ggml's ne[0] is always the "memory-adjacent, fastest-changing" dimension.
For plain types (F32, F16), nb[0] is just the bytes of one element. But quantized types (like Q4_0) are not "one value per element"; they pack a whole block (e.g. 32 weights) into fixed-size bytes, so a single weight is not independently addressable.
So ggml uses ggml_blck_size(type) (elements per block) and ggml_type_size(type) (bytes per block). The ne[0] / ggml_blck_size(type) in the nb[1] formula means "how many blocks in a row". Once you get this, you see why a quantized tensor cannot be indexed element-by-element like a plain array - you must dequantize by block (Part 3's quantization-format lesson covers this in detail).
Use ggml_nbytes(tensor) directly. Intuitively, a contiguous tensor's byte count is about "highest-dim element count x highest-dim stride" (ne[k] * nb[k] at the top dim) - i.e. multiply all dims' element counts together, times bytes per element (or per block).
This is handy for estimating memory use: how much the weights take, how much the KV cache takes, are all essentially sums of such tensor byte counts. Non-contiguous and quantized tensors compute slightly differently, but ggml_nbytes already handles those cases - just call it.