• 这是一个 系列文档,将以由浅入深的方式,系统剖析 ggml 的底层原理,涵盖上下文管理、Tensor 内存管理、计算图定义与执行、后端管理以及后端调度器等核心模块。我们将从最上层的 API 出发,层层深入,直至底层的计算指令。在此过程中,读者可以一并观察和学习 ggml 的设计哲学、工程架构、数据结构设计以及各类优化巧思,相信这些内容对我们的工作和学习都具有启发与借鉴意义。
  • 关于 ggml 的基本介绍,可参考官方 README:https://github.com/ggml-org/ggml
  • 长文预警:全文超过 3 万字
  • 建议在电脑端阅读,以获得更佳体验
  • 本文部分内容由 AI 辅助生成,并已通过人工校验

这是系列文章的第一篇,我们从最基础的 GPT-2 入手。它也是 ggml 官方提供的示例程序之一,代码写得非常清晰。尽管当前主流的大语言模型与 GPT-2 在细节上有所差异,但它们同属 Transformer 架构,核心机理一脉相承。把 GPT-2 研究透彻之后,再面对更复杂的 Transformer 模型,也会更加得心应手。GPT-2 可谓“麻雀虽小,五脏俱全”,非常适合作为学习和研究的起点。

接下来,我们会先用数学公式梳理 GPT-2 的推理过程,再结合 ggml 的源码,逐步剖析代码的具体执行路径,为后续深入理解 ggml 的各个模块奠定基础。

【再次提醒】:建议在电脑端阅读,因为文中包含大量代码和图表,电脑上浏览体验更佳。

好了,我们这就发车 🚀

在这里插入图片描述


GPT-2的推理过程,数学方式表达

下面这几个超参数是 GPT-2 117M 的真实数值:

  • V = 50257 V = 50257 V=50257(词表大小)
  • d = 768 d = 768 d=768(隐藏层维度 / 模型特征维度)
  • h = 12 h = 12 h=12(注意力头数)
  • d k = 64 d_k = 64 dk=64(每个头的维度: 768 / 12 = 64 768 / 12 = 64 768/12=64
  • 4 d = 3072 4d = 3072 4d=3072(MLP 中间隐藏层维度: 4 × 768 = 3072 4 \times 768 = 3072 4×768=3072
  • n c t x = 1024 n_{ctx} = 1024 nctx=1024(最大上下文长度)

为了最直观地体现推理过程,以下以自回归生成阶段(单步处理,即输入 N = 1 N = 1 N=1 个 Token)为例,假设此时 KV Cache 中已保存了 L p a s t = 100 L_{past} = 100 Lpast=100 个历史 Token(因此当前总序列长度 S = 100 + 1 = 101 S = 100 + 1 = 101 S=100+1=101)。

自回归 = 用自己刚说的话,作为说下一句话的上下文,一步一步地往下编,直到编完为止。

GPT-2的核心推理大致分为以下几个阶段

[1. 嵌入阶段] ──> [2. 归一化与 QKV 投影] ──> [3. 包含 Cache 的 Attention] ──> [4. FFN/MLP 升降维] ──> [5. 输出映射与采样]

下面逐一详细介绍:


1. 嵌入层 (Embedding Layer)

假设输入的是单个 Token ID(例如 [15496])和对应的位置索引(例如 [100]):

H 0 = Gather ( W t e , [ 15496 ] ) + Gather ( W p e , [ 100 ] ) \mathbf{H}_0 = \text{Gather}(\mathbf{W}_{te}, [15496]) + \text{Gather}(\mathbf{W}_{pe}, [100]) H0=Gather(Wte,[15496])+Gather(Wpe,[100])

  • W t e \mathbf{W}_{te} Wte ( 50257 × 768 ) (50257 \times 768) (50257×768),词表嵌入矩阵
  • W p e \mathbf{W}_{pe} Wpe ( 1024 × 768 ) (1024 \times 768) (1024×768),位置嵌入矩阵(Position Embedding Matrix),主要作用是给模型提供 Token 在序列中的位置信息。
  • 在深度学习和 PyTorch/TensorFlow 等框架中,Gather(或 Embedding Lookup)本质上就是根据索引(Index)在矩阵中“查表”取行的操作。
  • 输出 H 0 \mathbf{H}_0 H0 ( 1 × 768 ) \mathbf{(1 \times 768)} (1×768)

W t e \mathbf{W}_{te} Wte 的核心作用:把“数字”变成“高维向量”
计算机(以及神经网络)是无法直接理解自然语言的文字(如 “cat”、“苹果”)的。在文本进入模型之前,Tokenizer(分词器)会先将每一个词或子词转换为一个整数 ID(即 Token ID)。
例如:
“hello” → \to ID 15496
“world” → \to ID 2814
但是,单纯的整数(如 15496 和 2814)只有大小关系,没有任何语义联系。
W t e \mathbf{W}_{te} Wte 的作用,就是建立一张“字典”,把这一个个孤立的整数 ID,映射成一个具有丰富语义特征的稠密高维向量(Embedding Vector)。

  1. 为什么需要 W p e \mathbf{W}_{pe} Wpe
    Transformer 的核心组件——自注意力机制(Self-Attention,下面会讲)在计算时是无序的(置换不变性)。对于自注意力层来说,如果不做特殊处理:
    “Cat eats fish”(猫吃鱼)
    “Fish eats cat”(鱼吃猫)
    模型算出来的注意力矩阵是完全一样的,因为它只看 Token 词义本身,不知道词与词之间的先后顺序。为了让模型能识别“词在句子里的位置”,必须把位置编号(如第 0 个词、第 100 个词)也转换成一个向量,和嵌入向量加在一起。
  2. GPT-2 中 W p e \mathbf{W}_{pe} Wpe 的具体参数
    在 GPT-2 117M 模型中:
    维度格式: W p e ∈ ( 1024 × 768 ) \mathbf{W}_{pe} \in (1024 \times 768) Wpe(1024×768)
    1024 1024 1024 ( n c t x n_{ctx} nctx):模型支持的最大上下文长度(Context Length / Sequence Length)。从位置 0 到位置 1023,共 1024 个可能的位置。
    768 768 768 ( d d d):特征维度,必须与词嵌入 W t e \mathbf{W}_{te} Wte 的维度一致,这样才能直接相加。
    参数类型: 可学习参数(Learned Positional Embedding)。 W p e \mathbf{W}_{pe} Wpe 里的权重不是用正弦/余弦函数计算出来的固定值(那是 Transformer 原始论文的 Absolute Sine Position Encoding),而是在预训练(Pre-training)过程中随模型参数一起反向传播训练出来的。这种方式在学术上被称为 可学习的绝对位置编码(Learned Absolute Position Embedding)。优点是实现极简、灵活,拟合能力强,缺点是占用额外的模型参数量。现在的现代大模型(如 LLaMA、Qwen 等)大多已经抛弃了 GPT-2 这种纯靠查表学习的 W p e \mathbf{W}_{pe} Wpe,转而使用 RoPE(Rotary Position Embedding,旋转位置编码)。

2. Transformer Block计算

类似于resnet,Transformer Block重复了12次,我们以第 l l l 层为例

2.1 Layer Normalization 1

X = LayerNorm ( H l − 1 ) \mathbf{X} = \text{LayerNorm}(\mathbf{H}_{l-1}) X=LayerNorm(Hl1)

  • 输入/输出 ( 1 × 768 ) \mathbf{(1 \times 768)} (1×768)

Layer Normalization(层归一化,简称 LN)的核心作用是:在模型每一层的计算过程中,将隐藏层特征向量的数值强制拉回到“均值为 0、方差为 1”的稳定分布,防止数值过大(梯度爆炸)或过小(梯度消失),从而让深层神经网络能够稳定、高效地训练。
LayerNorm 是怎么计算的
在 GPT-2 117M 中,每一个 Token 的特征向量维度是 d = 768 d = 768 d=768
对于某一个 Token 的 768 维特征向量 x = [ x 1 , x 2 , … , x 768 ] \mathbf{x} = [x_1, x_2, \dots, x_{768}] x=[x1,x2,,x768]
LN 的计算分为四步:
Step 1: 计算这 768 个数值的均值(Mean) μ = 1 768 ∑ i = 1 768 x i \mu = \frac{1}{768} \sum_{i=1}^{768} x_i μ=7681i=1768xi
Step 2: 计算这 768 个数值的方差(Variance) σ 2 = 1 768 ∑ i = 1 768 ( x i − μ ) 2 \sigma^2 = \frac{1}{768} \sum_{i=1}^{768} (x_i - \mu)^2 σ2=7681i=1768(xiμ)2
Step 3: 标准化(Standardization)将每个数值减去均值再除以标准差(加上极小值 ϵ = 10 − 5 \epsilon = 10^{-5} ϵ=105 防止除以 zero): x ^ i = x i − μ σ 2 + ϵ \hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} x^i=σ2+ϵ xiμ
经过这一步,这 768 个数变成了均值为 0、方差为 1 的标准正态分布。
Step 4: 缩放和平移(Scale and Shift,可学习参数)为了不破坏模型原有的表达能力,引入两个可学习的参数 γ \gamma γ(缩放)和 β \beta β(平移),维度也是 768: y i = γ i ⋅ x ^ i + β i y_i = \gamma_i \cdot \hat{x}_i + \beta_i yi=γix^i+βi


2.2 自注意力机制 (Self-Attention)

Q, K, V是叠在一起计算的,算完之后再拆分。

Step 1: 线性投影得到 Q, K, V

Q K V = X W q k v + b q k v \mathbf{QKV} = \mathbf{X} \mathbf{W}_{qkv} + \mathbf{b}_{qkv} QKV=XWqkv+bqkv

  • X \mathbf{X} X ( 1 × 768 ) \mathbf{(1 \times 768)} (1×768)
  • W q k v \mathbf{W}_{qkv} Wqkv ( 768 × 2304 ) (768 \times 2304) (768×2304)(注: 2304 = 3 × 768 2304 = 3 \times 768 2304=3×768
  • Q K V \mathbf{QKV} QKV ( 1 × 768 ) × ( 768 × 2304 ) → ( 1 × 2304 ) (1 \times 768) \times (768 \times 2304) \to \mathbf{(1 \times 2304)} (1×768)×(768×2304)(1×2304)

随后切分为 Q, K, V 并按 12 个头重构维度:

  • Q ∈ ( 12 × 1 × 64 ) \mathbf{Q} \in \mathbf{(12 \times 1 \times 64)} Q(12×1×64)
  • K n e w ∈ ( 12 × 1 × 64 ) \mathbf{K}_{new} \in \mathbf{(12 \times 1 \times 64)} Knew(12×1×64)
  • V n e w ∈ ( 12 × 1 × 64 ) \mathbf{V}_{new} \in \mathbf{(12 \times 1 \times 64)} Vnew(12×1×64)

“分头”本质上就是将 768 维的特征空间拆分成 12 个独立的小子空间(每个子空间 64 维),让模型可以并行在 12 个不同的维度/视角下去理解上下文关系。

Step 2: 更新 KV Cache

将新计算的 K n e w \mathbf{K}_{new} Knew V n e w \mathbf{V}_{new} Vnew 拼接到历史上 100 个 Token 的 Cache 矩阵中:

  • 历史 Cache K p a s t , V p a s t ∈ ( 12 × 100 × 64 ) \mathbf{K}_{past}, \mathbf{V}_{past} \in (12 \times 100 \times 64) Kpast,Vpast(12×100×64)
  • 拼接后

K = [ K p a s t   ;   K n e w ] ∈ ( 12 × 101 × 64 ) \mathbf{K} = [\mathbf{K}_{past} \,;\, \mathbf{K}_{new}] \in \mathbf{(12 \times 101 \times 64)} K=[Kpast;Knew](12×101×64)

V = [ V p a s t   ;   V n e w ] ∈ ( 12 × 101 × 64 ) \mathbf{V} = [\mathbf{V}_{past} \,;\, \mathbf{V}_{new}] \in \mathbf{(12 \times 101 \times 64)} V=[Vpast;Vnew](12×101×64)

Step 3: 注意力分数计算 (Scaled Dot-Product)

A r a w = Q K T 64 \mathbf{A}_{raw} = \frac{\mathbf{Q} \mathbf{K}^T}{\sqrt{64}} Araw=64 QKT

  • Q \mathbf{Q} Q ( 12 × 1 × 64 ) (12 \times 1 \times 64) (12×1×64)
  • K T \mathbf{K}^T KT ( 12 × 64 × 101 ) (12 \times 64 \times 101) (12×64×101)(针对最后两维转置)
  • 矩阵相乘 ( 12 × 1 × 64 ) × ( 12 × 64 × 101 ) → A r a w ∈ ( 12 × 1 × 101 ) (12 \times 1 \times 64) \times (12 \times 64 \times 101) \to \mathbf{A}_{raw} \in \mathbf{(12 \times 1 \times 101)} (12×1×64)×(12×64×101)Araw(12×1×101)
Step 4: Masking & Softmax

A = Softmax ( Mask ( A r a w ) ) \mathbf{A} = \text{Softmax}(\text{Mask}(\mathbf{A}_{raw})) A=Softmax(Mask(Araw))

  • 输出 A \mathbf{A} A ( 12 × 1 × 101 ) \mathbf{(12 \times 1 \times 101)} (12×1×101)

在数学上,Mask 的操作是 “加法掩码”(Additive Masking):
Mask ( A r a w ) = A r a w + M \text{Mask}(\mathbf{A}_{raw}) = \mathbf{A}_{raw} + \mathbf{M} Mask(Araw)=Araw+M
其中 Mask 矩阵 M \mathbf{M} M 的取值规则为:
允许看到的位置: M i , j = 0 \mathbf{M}_{i,j} = 0 Mi,j=0
禁止看到的位置: M i , j = − ∞ \mathbf{M}_{i,j} = -\infty Mi,j=(工程上通常用 -1e9 或 -1e4 等极小的负数代替)
为什么是 − ∞ \mathbf{-\infty}
因为随后的步骤是 Softmax \text{Softmax} Softmax。根据 Softmax 公式:
P i = e z i ∑ e z j P_i = \frac{e^{z_i}}{\sum e^{z_j}} Pi=ezjezi
当某个位置的值为 − ∞ -\infty 时, e − ∞ = 0 e^{-\infty} = 0 e=0。这样就把禁止看到的位置的注意力权重归零,使其完全不起作用。

在这里输入 N = 1 N = 1 N=1 个 Token,矩阵 M \mathbf{M} M 的维度是 ( 1 × 101 ) (1 \times 101) (1×101),且全部元素都为 0 0 0
M = [ 0 , 0 , 0 , … , 0 ] \mathbf{M} = [0, 0, 0, \dots, 0] M=[0,0,0,,0]
如果输入 N = 3 N = 3 N=3 个 Token,Mask 矩阵 M ∈ ( 3 × 103 ) \mathbf{M} \in (3 \times 103) M(3×103) 的具体数值:
M = ( 0 0 … 0 0 − ∞ − ∞ 0 0 … 0 0 0 − ∞ 0 0 … 0 0 0 0 ) \mathbf{M} = \begin{pmatrix} 0 & 0 & \dots & 0 & 0 & -\infty & -\infty \\ 0 & 0 & \dots & 0 & 0 & 0 & -\infty \\ 0 & 0 & \dots & 0 & 0 & 0 & 0 \end{pmatrix} M= 000000000000000

Step 5: 加权求和与多头合并

O h e a d s = A V \mathbf{O}_{heads} = \mathbf{A} \mathbf{V} Oheads=AV

  • A \mathbf{A} A ( 12 × 1 × 101 ) (12 \times 1 \times 101) (12×1×101)
  • V \mathbf{V} V ( 12 × 101 × 64 ) (12 \times 101 \times 64) (12×101×64)
  • 矩阵相乘 ( 12 × 1 × 101 ) × ( 12 × 101 × 64 ) → O h e a d s ∈ ( 12 × 1 × 64 ) (12 \times 1 \times 101) \times (12 \times 101 \times 64) \to \mathbf{O}_{heads} \in (12 \times 1 \times 64) (12×1×101)×(12×101×64)Oheads(12×1×64)
  • 合并 12 个头 (Concat) O c a t ∈ ( 1 × 768 ) \mathbf{O}_{cat} \in \mathbf{(1 \times 768)} Ocat(1×768)
Step 6: 输出投影与残差连接

H a t t n = O c a t W p r o j + b p r o j \mathbf{H}_{attn} = \mathbf{O}_{cat} \mathbf{W}_{proj} + \mathbf{b}_{proj} Hattn=OcatWproj+bproj

H m i d = H l − 1 + H a t t n \mathbf{H}_{mid} = \mathbf{H}_{l-1} + \mathbf{H}_{attn} Hmid=Hl1+Hattn

  • O c a t \mathbf{O}_{cat} Ocat ( 1 × 768 ) (1 \times 768) (1×768)
  • W p r o j \mathbf{W}_{proj} Wproj ( 768 × 768 ) (768 \times 768) (768×768)
  • 输出 H m i d \mathbf{H}_{mid} Hmid ( 1 × 768 ) × ( 768 × 768 ) + ( 1 × 768 ) → ( 1 × 768 ) (1 \times 768) \times (768 \times 768) + (1 \times 768) \to \mathbf{(1 \times 768)} (1×768)×(768×768)+(1×768)(1×768)

2.3 前馈神经网络 (MLP)

Step 1: Layer Normalization 2

Y = LayerNorm ( H m i d ) ∈ ( 1 × 768 ) \mathbf{Y} = \text{LayerNorm}(\mathbf{H}_{mid}) \in \mathbf{(1 \times 768)} Y=LayerNorm(Hmid)(1×768)

Step 2: 第一层全连接 (升维至 4 倍)

F 1 = GELU ( Y W f c + b f c ) \mathbf{F}_1 = \text{GELU}(\mathbf{Y} \mathbf{W}_{fc} + \mathbf{b}_{fc}) F1=GELU(YWfc+bfc)

  • Y \mathbf{Y} Y ( 1 × 768 ) (1 \times 768) (1×768)
  • W f c \mathbf{W}_{fc} Wfc ( 768 × 3072 ) (768 \times 3072) (768×3072)
  • 输出 F 1 \mathbf{F}_1 F1 ( 1 × 768 ) × ( 768 × 3072 ) → ( 1 × 3072 ) (1 \times 768) \times (768 \times 3072) \to \mathbf{(1 \times 3072)} (1×768)×(768×3072)(1×3072)
Step 3: 第二层全连接 (降维复原) 与残差连接

F 2 = F 1 W m l p _ p r o j + b m l p _ p r o j \mathbf{F}_2 = \mathbf{F}_1 \mathbf{W}_{mlp\_proj} + \mathbf{b}_{mlp\_proj} F2=F1Wmlp_proj+bmlp_proj

H l = H m i d + F 2 \mathbf{H}_l = \mathbf{H}_{mid} + \mathbf{F}_2 Hl=Hmid+F2

  • F 1 \mathbf{F}_1 F1 ( 1 × 3072 ) (1 \times 3072) (1×3072)
  • W m l p _ p r o j \mathbf{W}_{mlp\_proj} Wmlp_proj ( 3072 × 768 ) (3072 \times 768) (3072×768)
  • 输出 H l \mathbf{H}_l Hl ( 1 × 3072 ) × ( 3072 × 768 ) + ( 1 × 768 ) → ( 1 × 768 ) (1 \times 3072) \times (3072 \times 768) + (1 \times 768) \to \mathbf{(1 \times 768)} (1×3072)×(3072×768)+(1×768)(1×768)

3. 输出层与 Logits 计算 (Language Model Head)

经过 12 层 Transformer Block 循环计算后,得到最终特征:

Step 1: 最终 Layer Normalization

h l a s t = LayerNorm ( H 12 ) ∈ ( 1 × 768 ) \mathbf{h}_{last} = \text{LayerNorm}(\mathbf{H}_{12}) \in \mathbf{(1 \times 768)} hlast=LayerNorm(H12)(1×768)

Step 2: 映射回词表空间

z = h l a s t W l m _ h e a d T \mathbf{z} = \mathbf{h}_{last} \mathbf{W}_{lm\_head}^T z=hlastWlm_headT

  • h l a s t \mathbf{h}_{last} hlast ( 1 × 768 ) \mathbf{(1 \times 768)} (1×768)
  • W l m _ h e a d T \mathbf{W}_{lm\_head}^T Wlm_headT ( 768 × 50257 ) \mathbf{(768 \times 50257)} (768×50257)
  • 输出 z \mathbf{z} z ( 1 × 768 ) × ( 768 × 50257 ) → ( 1 × 50257 ) (1 \times 768) \times (768 \times 50257) \to \mathbf{(1 \times 50257)} (1×768)×(768×50257)(1×50257)

4. 采样与新 Token 生成

向量 z ∈ ( 1 × 50257 ) \mathbf{z} \in \mathbf{(1 \times 50257)} z(1×50257) 包含词表中每个词的分数。

  1. 温度调节与筛选 z ′ = TopP ( TopK ( z T ) ,   p ) \mathbf{z}' = \text{TopP}\Big( \text{TopK}\big( \frac{\mathbf{z}}{T} \big), \, p \Big) z=TopP(TopK(Tz),p)
    维度仍保持为 ( 1 × 50257 ) \mathbf{(1 \times 50257)} (1×50257)(被过滤掉的位置置为 − ∞ -\infty )。
  2. Softmax 概率化:

P = Softmax ( z ′ ) ∈ ( 1 × 50257 ) P = \text{Softmax}(\mathbf{z}') \in \mathbf{(1 \times 50257)} P=Softmax(z)(1×50257)

  1. 随机采样:根据概率分布 P P P 抽样得到一个标量整数,比如采样得到 ID 2814(代表文本 " world")。

这个新的 Token ID 2814 将作为下一个时间步( L p a s t = 101 L_{past} = 101 Lpast=101)的输入,开始下一轮的矩阵计算。

温度调节 (Temperature, T T T)操作:
将 Logits 向量里的每一个数值除以标量温度值 T T T
z scaled = z T \mathbf{z}_{\text{scaled}} = \frac{\mathbf{z}}{T} zscaled=Tz
在随后的 Softmax 中, P i = e z i / T ∑ e z j / T P_i = \frac{e^{z_i / T}}{\sum e^{z_j / T}} Pi=ezj/Tezi/T T T T 控制了概率分布的“陡峭程度”:

  • T < 1.0 T < 1.0 T<1.0(低温度,如 0.2): 放大最高分和低分之间的差距。 Softmax 之后的概率分布会非常尖锐,模型输出极度确定、保守(倾向于永远选最高分的词)。
  • T > 1.0 T > 1.0 T>1.0(高温度,如 1.5): 缩小不同分数之间的差距。概率分布变得平坦(接近均匀分布),模型输出更具随机性和创造力,但也更容易胡言乱语。
  • T = 1.0 T = 1.0 T=1.0: 不改变原始概率分布。

Top-K 过滤
z scaled \mathbf{z}_{\text{scaled}} zscaled 按得分从大到小排序,仅保留前 K K K 个最高分的词,将第 K + 1 K+1 K+1 名及之后的所有词的分数强制重写为 − ∞ -\infty
z top_k [ i ] = { z scaled [ i ] , if Rank ( i ) ≤ K − ∞ , otherwise \mathbf{z}_{\text{top\_k}}[i] = \begin{cases} \mathbf{z}_{\text{scaled}}[i], & \text{if } \text{Rank}(i) \le K \\ -\infty, & \text{otherwise} \end{cases} ztop_k[i]={zscaled[i],,if Rank(i)Kotherwise

Top-P(核采样 / Nucleus Sampling)过滤
对上一步保留的候选词计算临时概率,并按概率降序累加。找到使得累积概率首次达到阈值 p p p(如 p = 0.9 p=0.9 p=0.9)的最小词集边界 M M M。保留这前 M M M 个词,将其余所有词的分数置为 − ∞ -\infty
z ′ [ i ] = { z top_k [ i ] , if  ∑ j = 1 Rank ( i ) P temp [ j ] ≤ p ( 含突破  p  的临界词 ) − ∞ , otherwise \mathbf{z}'[i] = \begin{cases} \mathbf{z}_{\text{top\_k}}[i], & \text{if } \sum_{j=1}^{\text{Rank}(i)} P_{\text{temp}}[j] \le p \quad (\text{含突破 } p \text{ 的临界词}) \\ -\infty, & \text{otherwise} \end{cases} z[i]={ztop_k[i],,if j=1Rank(i)Ptemp[j]p(含突破 p 的临界词)otherwise


能看到这里,你已经战胜全国99%的用户了。不知道你有没有感觉到,transformer的推理过程其实很简单。那么如果认为大模型已经具有了智能,那么人类还剩什么?,欢迎在这里讨论
在这里插入图片描述


接下来我们一口气看完对应的推理代码,冲~

GPT-2的推理过程,逐行代码

先编译一下ggml,很简单

git clone https://github.com/ggml-org/ggml.git
mkdir build && cd build
cmake ..
cmake --build . --config Release -j 8

# 需要从这里 https://huggingface.co/ggerganov/ggml/tree/main 下载 ggml-model-gpt-2-117M.bin
# 其他大点的模型也行,不过下载慢点,运行时对硬件要求稍微高点
# 如果下载不到,可以私信找到,我发给你
./bin/gpt-2-ctx -m ../examples/gpt-2/models/gpt-2-117M/ggml-model-gpt-2-117M.bin -p "I have a dream"

运行之后,你会看到类似的结果:

main: seed = 1784819590
gpt2_model_load: loading model from '../examples/gpt-2/models/gpt-2-117M/ggml-model-gpt-2-117M.bin'
gpt2_model_load: n_vocab = 50257
gpt2_model_load: n_ctx   = 1024
gpt2_model_load: n_embd  = 768
gpt2_model_load: n_head  = 12
gpt2_model_load: n_layer = 12
gpt2_model_load: ftype   = 1
gpt2_model_load: qntvr   = 0
gpt2_model_load: ggml tensor size = 336 bytes
gpt2_model_load: ggml ctx size = 384.88 MB
gpt2_model_load: memory size =    72.00 MB, n_mem = 12288
gpt2_model_load: model size  =   239.08 MB
extract_tests_from_file : No test file found.
test_gpt_tokenizer : 0 tests failed out of 0 tests.
main: prompt: 'I have a dream'
main: number of tokens in prompt = 4, first 8 tokens: 40 423 257 4320 

I have a dream for the future. I want to go to another city."

The man is said to have told police he had a dream and told the officer to get in the car to find out what was going on.

The man was arrested on suspicion of murder, assault with a deadly weapon, attempted murder and possession of a firearm, according to the Crown Prosecution Service.

He is also charged with robbery.

Police say the man told the officer to get into the car and run.

Police are also looking for the man in the early hours of Thursday night.

Anyone with information is asked to call the police line at 1-800-577-TIPS (8477).<|endoftext|>

main: mem per token =  2044708 bytes
main:     load time =   171.11 ms
main:   sample time =     9.43 ms
main:  predict time =   886.50 ms, n_past =      148,     5.99 ms per token
main:    total time =  1085.23 ms

接下来深入 gpt-2-ctx 的源码,看看代码中是如何利用ggml的api实现上述推理过程的。

// 源码位置
examples/gpt-2/main-ctx.cpp

整体流程图大概是这样的,包含并串联了 模型加载、内存预分配、Tokenize 分词、gpt2_eval 前向传播计算(包含完整的 Transformer 内部计算图)、采样(Sampling) 以及 自回归循环 的全过程。

4. 采样与新 Token 生成 (Sampling)

3. Transformer 前向传播 (gpt2_eval)

2. 自回归循环管理 (Loop Control)

1. 模型初始化与加载阶段 (Init & Loading)

单层 Block 计算 (il)

Self-Attention 计算

进入第 il 层

传递给下一层

12 层循环结束

否 (Decode 生成阶段)

是 (Prefill 阶段)

否 (达到最大生成长度)

启动 main() / 解析命令行参数

gpt2_model_load()
读取二进制权重文件

静态内存规划与分配
申请连续内存池 (Model Weights + Global KV Cache)

建立词表映射 & Weight Tying
(lm_head 共享 wte 权重)

::gpt_tokenize()
将 Prompt 转换为 Token ID 序列

生成token控制循环
(i < prompt_len + n_predict)

准备输入批次 (embd)
- Prefill 阶段: 输入 Prompt Tokens
- Decode 阶段: 输入上一步生成的单 Token

调用 gpt2_eval()

嵌入层计算
inpL = Gather(wte, embd) + Gather(wpe, position)

Transformer Block 循环
(il = 0 ... 11)

LayerNorm 1
ggml_norm + scale(ln_1_g) + shift(ln_1_b)

QKV 线性投影
cur = cur * c_attn_attn_w + c_attn_attn_b

切分 Qcur, Kcur, Vcur [768, N]

写入 KV Cache
把 Kcur, Vcur 追加拷贝至 memory_k, memory_v

多头维度重排 (Permute)
Q, K, V -> [64, N/Context, 12]

计算原始注意力得分
KQ = K * Q

Score 缩放 & 因果掩码
KQ_scaled = KQ / sqrt(64)
KQ_masked = diag_mask_inf(KQ_scaled)

Softmax 归一化
KQ_soft_max = Softmax(KQ_masked)

加权求和 & 多头合并
KQV = V * KQ_soft_max
Concat 12 个头 -> [768, N]

注意力输出投影
cur = cur * c_attn_proj_w + c_attn_proj_b

残差连接 1
cur = cur + inpL

LayerNorm 2
ggml_norm + scale(ln_2_g) + shift(ln_2_b)

MLP 升维 (768 -> 3072)
cur = cur * c_mlp_fc_w + c_mlp_fc_b

GELU 激活函数

MLP 降维 (3072 -> 768)
cur = cur * c_mlp_proj_w + c_mlp_proj_b

残差连接 2
inpL = cur + inpFF

最终 LayerNorm
inpL = LayerNorm(inpL)

LM Head 映射回词表空间
logits = inpL * lm_head -> [50257, N]

提取最后一个 Token 的 Logits
[1, 50257]

是否在 Prompt 阶段?

gpt_sample_top_k_top_p()

1. 温度调节 (Temperature Scaling)
logits = logits / T

2. Top-K & Top-P (Nucleus) 筛选
低于阈值的得分置为 -inf

3. Softmax 转化为概率分布 P

4. 根据随机数种子抽样
得到下一个 Token ID

将新 Token ID 加入下一个时间步输入 (embd)

继续读取 Prompt 中的下一个 Batch

打印当前 Token 到标准输出 (打字机效果)

是否遇到结束符 EOS
(ID: 50256)?

更新已处理历史长度 n_past += embd.size()

结束生成 / 释放空间 (ggml_free)

gpt2_eval 是整个 GPT-2 推理程序中最核心的前向传播计算函数。它的主要职责是构建 GGML 计算图(Computational Graph),并在 CPU 上执行从输入 Token 到输出 Logits 的所有矩阵运算。

他的函数声明如下:

// evaluate the transformer
//
//   - model:     the model
//   - n_threads: number of threads to use
//   - n_past:    the context size so far
//   - embd_inp:  the embeddings of the tokens in the context
//   - embd_w:    the predicted logits for the next token
bool gpt2_eval(
        const gpt2_model & model,
        const int n_threads,
        const int n_past,
        const std::vector<gpt_vocab::id> & embd_inp,
              std::vector<float>         & embd_w,
              size_t                     & mem_per_token);
  1. 初始化阶段,主要是预先分配内存
// 1. 动态分配或复用单 Token 临时内存缓冲区
if (mem_per_token > 0 && mem_per_token*N > buf_size) {
    buf_size = 1.1*(mem_per_token*N);
    buf = realloc(buf, buf_size);
}

// 2. 初始化 GGML Context 与计算图 Graph
struct ggml_init_params params = { buf_size, buf, false };
struct ggml_context * ctx0 = ggml_init(params);
struct ggml_cgraph * gf = ggml_new_graph(ctx0);
  1. 输入 Embedding
// 创建并填充 token 嵌入张量
struct ggml_tensor * embd = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, N);
memcpy(embd->data, embd_inp.data(), N*ggml_element_size(embd));
 // 创建位置编码张量
struct ggml_tensor * position = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, N);
for (int i = 0; i < N; ++i) {
    ((int32_t *) position->data)[i] = n_past + i;
}

// W_te + W_pe
// ggml中将执行和构造分开了,先构造计算图,之后在统一计算
struct ggml_tensor * inpL =
    ggml_add(ctx0,
            ggml_get_rows(ctx0, model.wte, embd),
            ggml_get_rows(ctx0, model.wpe, position));
  • 对应数学公式
    H 0 = Gather ( W t e , embd ) + Gather ( W p e , position ) \mathbf{H}_0 = \text{Gather}(\mathbf{W}_{te}, \text{embd}) + \text{Gather}(\mathbf{W}_{pe}, \text{position}) H0=Gather(Wte,embd)+Gather(Wpe,position)

注意:ggml中矩阵的维度和数学中不太一样
前面数学推导中 W t e ∈ ( 50257 × 768 ) \mathbf{W}_{te} \in (50257 \times 768) Wte(50257×768) W p e ∈ ( 1024 × 768 ) \mathbf{W}_{pe} \in (1024 \times 768) Wpe(1024×768)。查表后输出 H 0 ∈ ( N × 768 ) \mathbf{H}_0 \in (N \times 768) H0(N×768)

GGML 代码wte 形状为 [768, 50257]wpe 形状为 [768, 1024]ggml_get_rows 输出形状为 [768, N]
方向上:GGML 里的 [768, N] 与数学推导里的 ( N × 768 ) (N \times 768) (N×768) 互为转置。

  1. Transformer Block 循环
for (int il = 0; il < n_layer; ++il) {
	// 循环中的内容比较多
	// 下面分开介绍
}

3.1 LayerNorm

// [ 768, N]
cur = ggml_norm(ctx0, inpL, hparams.eps);

// cur = ln_1_g*cur + ln_1_b
// [ 768, N]
cur = ggml_add(ctx0,
        ggml_mul(ctx0,
            // 创建一个新的节点张量,其类型与数据源 model.layers[il].ln_1_g 一致,但形状(ne)直接采用 cur 的形状
            ggml_repeat(ctx0, model.layers[il].ln_1_g, cur),
            cur),
        ggml_repeat(ctx0, model.layers[il].ln_1_b, cur));

inpL 执行标准化(ggml_norm),然后通过广播(ggml_repeat)与参数 Gamma(ln_1_g)和 Beta(ln_1_b)逐元素相乘相加。

  • 对应数学公式
    X = LayerNorm ( H l − 1 ) = γ ⊙ x − μ σ 2 + ϵ + β \mathbf{X} = \text{LayerNorm}(\mathbf{H}_{l-1}) = \gamma \odot \frac{\mathbf{x} - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta X=LayerNorm(Hl1)=γσ2+ϵ xμ+β
  • 矩阵维度
    • 输入 inpL[768, N],输出 cur 仍为 [768, N]

3.2 QKV 线性投影

// cur = attn_w*cur + attn_b
// [2304, N]
cur = ggml_mul_mat(ctx0,
        model.layers[il].c_attn_attn_w,
        cur);

cur = ggml_add(ctx0,
        ggml_repeat(ctx0, model.layers[il].c_attn_attn_b, cur),
        cur);

一次性计算 Q, K, V 三个投影向量。

  • 对应数学公式

    Q K V = X W q k v + b q k v \mathbf{QKV} = \mathbf{X} \mathbf{W}_{qkv} + \mathbf{b}_{qkv} QKV=XWqkv+bqkv

  • 矩阵维度

    • 数学推导 X ∈ ( N × 768 ) \mathbf{X} \in (N \times 768) X(N×768) W q k v ∈ ( 768 × 2304 ) \mathbf{W}_{qkv} \in (768 \times 2304) Wqkv(768×2304),乘积为 ( N × 2304 ) (N \times 2304) (N×2304)
    • GGML 代码
      • c_attn_attn_w 形状为 [768, 2304](即在 GGML 中内层为 768,外层为 2304)。
      • 输入 cur[768, N]
      • 执行 ggml_mul_mat 后,输出 cur 形状为 [2304, N]

3.3 QKV 内存切分与 KV Cache 写入

struct ggml_tensor * Qcur = ggml_view_2d(ctx0, cur, n_embd, N, cur->nb[1], 0*sizeof(float)*n_embd);
struct ggml_tensor * Kcur = ggml_view_2d(ctx0, cur, n_embd, N, cur->nb[1], 1*sizeof(float)*n_embd);
struct ggml_tensor * Vcur = ggml_view_2d(ctx0, cur, n_embd, N, cur->nb[1], 2*sizeof(float)*n_embd);

// 调用 `ggml_cpy` 把当前步的 `Kcur` 和 `Vcur` 复制到全局 KV Cache 
if (N >= 1) {
    struct ggml_tensor * k = ggml_view_1d(ctx0, model.memory_k, N*n_embd, (ggml_element_size(model.memory_k)*n_embd)*(il*n_ctx + n_past));
    struct ggml_tensor * v = ggml_view_1d(ctx0, model.memory_v, N*n_embd, (ggml_element_size(model.memory_v)*n_embd)*(il*n_ctx + n_past));

    // 这里为什么调用 ggml_build_forward_expand
    // 因为 cpy_node 仅仅存在于内存上下文中。如果不将其显式添加到计算图 gf 中,后续调用 ggml_graph_compute_with_ctx(ctx0, gf, n_threads) 遍历计算图时,系统根本不知道需要执行这个复制操作,KV Cache 就不会被真正更新
    ggml_build_forward_expand(gf, ggml_cpy(ctx0, Kcur, k));
    ggml_build_forward_expand(gf, ggml_cpy(ctx0, Vcur, v));
}

QKV通过view的方式构造,并没有真正分配内存,底层只是变了一下指针的位置。内存布局大致如下:

内存地址增加方向  ----------------------------------------------------------------------------------->

[ Token 0Q (768) ] [ Token 0K (768) ] [ Token 0V (768) ] | [ Token 1Q (768) ] [ Token 1K (768) ] ...
|<---------------- 连续的 2304float (Token 0) ---------------->|

KV cache的内存布局

字节地址增加方向 -------------------------------------------------------------------------------------------------------->

+-----------------------------------------------------------------------------------------------------------------------+
| ... |                 Layer  il                                                                                 | ... |
+-----------------------------------------------------------------------------------------------------------------------+
      | Token 0 的 K/V | Token 1 的 K/V | ... | Token n_past | ... | Token (n_past + N - 1) | ... | Token 1023 的 K/V |
      +-----------------------------------------------------------------------------------------------------------------+
      ^                                                     ^                                          ^
      |                                                     |                                          |
   Layer il                                              K / V 写入的起始位置                        Layer il
   起始地址                                           (Offset 计算出的地址)                         结束地址
  • 对应数学公式
    K = [ K p a s t   ;   K n e w ] , V = [ V p a s t   ;   V n e w ] \mathbf{K} = [\mathbf{K}_{past} \,;\, \mathbf{K}_{new}], \quad \mathbf{V} = [\mathbf{V}_{past} \,;\, \mathbf{V}_{new}] K=[Kpast;Knew],V=[Vpast;Vnew]

3.4 多头 Scaled Dot-Product Attention

这一步是 Transformer 中最繁复的张量重排与矩阵乘法运算。

    // 1. 重排 Q 为 [64, N, 12]
    struct ggml_tensor * Q =
        ggml_permute(ctx0,
                ggml_cpy(ctx0, Qcur, ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, n_embd/n_head, n_head, N)),
                0, 2, 1, 3);

    // 2. 从 Cache 提取全量 K (包含历史),并重排为 [64, n_past + N, 12]
    struct ggml_tensor * K =
        ggml_permute(ctx0,
                ggml_reshape_3d(ctx0,
                    ggml_view_1d(ctx0, model.memory_k, (n_past + N)*n_embd, il*n_ctx*ggml_element_size(model.memory_k)*n_embd),
                    n_embd/n_head, n_head, n_past + N),
                0, 2, 1, 3);

    // 3. 计算得分 Q * K^T -> [n_past + N, N, 12]
    // 在ggml中 ggml_mul_mat(ctx0, K, Q) 就是与 Q * K^T 等价的,具体细节在后续文章中会介绍
    struct ggml_tensor * KQ = ggml_mul_mat(ctx0, K, Q);

    // 4. 缩放得分
    struct ggml_tensor * KQ_scaled = ggml_scale_inplace(ctx0, KQ, 1.0f/sqrt(float(n_embd)/n_head));

    // 5. 因果掩码 (Additive Mask)
    struct ggml_tensor * KQ_masked = ggml_diag_mask_inf_inplace(ctx0, KQ_scaled, n_past);

    // 6. Softmax
    struct ggml_tensor * KQ_soft_max = ggml_soft_max_inplace(ctx0, KQ_masked);
  1. Q 的维度转换
    • 原始 Qcur[768, N]
    • 先 Reshape 成 3D 维度 [64, 12, N](其中 64 = 768 / 12 64 = 768 / 12 64=768/12)。
    • ggml_permute(..., 0, 2, 1, 3) 进行轴置换,将维度调整为 [64, N, 12]
  2. K 的维度转换
    • 从 Cache 中截取 0 ~ (n_past + N) 的全量 K 内存,长度为 (n_past + N) * 768
    • Reshape 成 [64, 12, n_past + N]
    • ggml_permute 置换后,维度调整为 [64, n_past + N, 12]
  3. ggml_mul_mat(K, Q) 矩阵乘法
    • 根据 GGML 规则:[64, n_past + N][64, N] 在 Batch 维(12 个头)并行相乘。
    • 输出 KQ 的形状为 [n_past + N, N, 12]
    • 对照数学:数学公式中 Q K T \mathbf{Q} \mathbf{K}^T QKT 的形状是 ( 12 × N × ( L p a s t + N ) ) (12 \times N \times (L_{past}+N)) (12×N×(Lpast+N))。GGML 的输出与其互为转置。
  4. Mask & Softmax
    • ggml_diag_mask_inf_inplace 会把未来位置(对角线右上角)填入 − ∞ -\infty 。单步推理( N = 1 N=1 N=1)时因前面全是历史 Token,此算子无实际掩码动作。

3.4 Attention 加权求和与多头合并

 // 1. 提取全量 V 并转置 -> [n_past + N, 64, 12]
 struct ggml_tensor * V_trans =
     ggml_cpy(ctx0,
             ggml_permute(ctx0,
                 ggml_reshape_3d(ctx0,
                     ggml_view_1d(ctx0, model.memory_v, (n_past + N)*n_embd, il*n_ctx*ggml_element_size(model.memory_v)*n_embd),
                     n_embd/n_head, n_head, n_past + N),
                 1, 2, 0, 3),
             ggml_new_tensor_3d(ctx0, model.memory_v->type, n_past + N, n_embd/n_head, n_head));

 // 2. 加权求和 V_trans * KQ_soft_max -> [64, N, 12]
 struct ggml_tensor * KQV = ggml_mul_mat(ctx0, V_trans, KQ_soft_max);

 // 3. 多头合并 (Concat) -> [768, N]
 struct ggml_tensor * KQV_merged = ggml_permute(ctx0, KQV, 0, 2, 1, 3);

 cur = ggml_cpy(ctx0, KQV_merged, ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, n_embd, N));
  1. 对应的数学公式
    O h e a d s = A V \mathbf{O}_{heads} = \mathbf{A} \mathbf{V} Oheads=AV
  • A \mathbf{A} A ( 12 × 1 × 101 ) (12 \times 1 \times 101) (12×1×101)
  • V \mathbf{V} V ( 12 × 101 × 64 ) (12 \times 101 \times 64) (12×101×64)
  • 矩阵相乘 ( 12 × 1 × 101 ) × ( 12 × 101 × 64 ) → O h e a d s ∈ ( 12 × 1 × 64 ) (12 \times 1 \times 101) \times (12 \times 101 \times 64) \to \mathbf{O}_{heads} \in (12 \times 1 \times 64) (12×1×101)×(12×101×64)Oheads(12×1×64)
  • 合并 12 个头 (Concat) O c a t ∈ ( 1 × 768 ) \mathbf{O}_{cat} \in \mathbf{(1 \times 768)} Ocat(1×768)
  1. ggml_mul_mat(V_trans, KQ_soft_max)
    • V_trans ([n_past + N, 64], 12) × \times × KQ_soft_max ([n_past + N, N, 12])。
    • 输出 KQV 的形状为 [64, N, 12]
  2. 合并多头
    • ggml_permute(..., 0, 2, 1, 3) 将轴调回 [64, 12, N]
    • ggml_cpy 连续化内存,展平为 [768, N]cur

3.5 输出投影、残差连接 1 与 MLP 块

// 1. Attention 输出投影
cur = ggml_mul_mat(ctx0, model.layers[il].c_attn_proj_w, cur);
// ggml_repeat 把张量 model.layers[il].c_attn_proj_b 的数据在各个维度上重复复制,使其形状(Shape)扩充到与张量 cur 完全一致,并返回一个新的张量
cur = ggml_add(ctx0, ggml_repeat(ctx0, model.layers[il].c_attn_proj_b, cur), cur);

// 2. 残差连接 1
cur = ggml_add(ctx0, cur, inpL);
struct ggml_tensor * inpFF = cur;

// 3. MLP 模块
{
    // LayerNorm
    cur = ggml_norm(ctx0, inpFF, hparams.eps);
    cur = ggml_add(ctx0, ggml_mul(ctx0, ggml_repeat(ctx0, model.layers[il].ln_2_g, cur), cur), ggml_repeat(ctx0, model.layers[il].ln_2_b, cur));

    // 升维全连接 (768 -> 3072)
    cur = ggml_mul_mat(ctx0, model.layers[il].c_mlp_fc_w, cur);
    cur = ggml_add(ctx0, ggml_repeat(ctx0, model.layers[il].c_mlp_fc_b, cur), cur);

    // GELU 激活
    cur = ggml_gelu(ctx0, cur);

    // 降维全连接 (3072 -> 768)
    cur = ggml_mul_mat(ctx0, model.layers[il].c_mlp_proj_w, cur);
    cur = ggml_add(ctx0, ggml_repeat(ctx0, model.layers[il].c_mlp_proj_b, cur), cur);
}

// 4. 残差连接
inpL = ggml_add(ctx0, cur, inpFF);
  • 对应数学公式
    H a t t n = O c a t W p r o j + b p r o j \mathbf{H}_{attn} = \mathbf{O}_{cat} \mathbf{W}_{proj} + \mathbf{b}_{proj} Hattn=OcatWproj+bproj
    H m i d = H l − 1 + H a t t n \mathbf{H}_{mid} = \mathbf{H}_{l-1} + \mathbf{H}_{attn} Hmid=Hl1+Hattn
    F 1 = GELU ( LayerNorm ( H m i d ) W f c + b f c ) \mathbf{F}_1 = \text{GELU}(\text{LayerNorm}(\mathbf{H}_{mid}) \mathbf{W}_{fc} + \mathbf{b}_{fc}) F1=GELU(LayerNorm(Hmid)Wfc+bfc)
    H l = H m i d + F 1 W m l p _ p r o j + b m l p _ p r o j \mathbf{H}_l = \mathbf{H}_{mid} + \mathbf{F}_1 \mathbf{W}_{mlp\_proj} + \mathbf{b}_{mlp\_proj} Hl=Hmid+F1Wmlp_proj+bmlp_proj
  • 维度与转置对照
    • c_mlp_fc_w 形状为 [768, 3072],乘以 [768, N] 后,输出为 [3072, N]
    • c_mlp_proj_w 形状为 [3072, 768],乘以 [3072, N] 后,输出还原为 [768, N]

3.6 最终 LM Head 与 计算图执行

    // 1. 最终 LayerNorm
    inpL = ggml_norm(ctx0, inpL, hparams.eps);
    inpL = ggml_add(ctx0, ggml_mul(ctx0, ggml_repeat(ctx0, model.ln_f_g, inpL), inpL), ggml_repeat(ctx0, model.ln_f_b, inpL));

    // 2. LM Head 映射到词表
    inpL = ggml_mul_mat(ctx0, model.lm_head, inpL);

    // 3. 构建并执行计算图
    ggml_build_forward_expand(gf, inpL);
    ggml_graph_compute_with_ctx(ctx0, gf, n_threads);

    // 4. 拷贝最后一个 Token 的 Logits 返回
    embd_w.resize(n_vocab);
    memcpy(embd_w.data(), (float *) ggml_get_data(inpL) + (n_vocab*(N-1)), sizeof(float)*n_vocab);

    ggml_free(ctx0);
    return true;
  • 对应数学公式
    z = LayerNorm ( H 12 ) W l m _ h e a d T ∈ R N × 50257 \mathbf{z} = \text{LayerNorm}(\mathbf{H}_{12}) \mathbf{W}_{lm\_head}^T \in \mathbb{R}^{N \times 50257} z=LayerNorm(H12)Wlm_headTRN×50257

  • 矩阵维度

    1. model.lm_head 形状为 [768, 50257]ggml_mul_mat 计算后,输出 inpL 的形状为 [50257, N]
    2. ggml_graph_compute_with_ctx真正触发 CPU 多线程矩阵计算的指令,之前的所有代码仅仅是在构建计算图(DAG,Directed Acyclic Graph, 有向无环图)。
    3. memcpy 仅提取最后一个 Token 对应的 50257 维未归一化得分 Logits 向量返回给外层的采样函数。

gpt2_eval执行的整个计算图大致如下:

  1. 蓝框(蓝色矩形):GGML 算子节点(如 ggml_mul_mat、ggml_add、ggml_norm),对应前向传播中创建的具体 C++ 节点对象。
  2. 灰色圆角矩形:模型中的可学习权重(Weights/Biases),在整个计算图的生命周期中只读且常驻内存。
  3. 红色圆角矩形:全局静态分配的 KV Cache 节点(memory_k / memory_v)。它既接收 ggml_cpy 的写入,也作为注意力机制算子的特征输入。
  4. 数据流线上的维度标注(如 [64, N, 12]):展示了 GGML 底层采用行主序(Row-Major)布局时,数据经过各个算子节点后的张量尺寸(其中 S = n p a s t + N S = n_{past} + N S=npast+N 为包含历史的上下文总长度)。

3. 最终输出层 (Language Model Head)

2. Transformer Block 循环 (x12 Layers)

1. 嵌入层 (Embedding Layer)

输入数据 (Inputs)

前馈神经网络 (MLP Block)

注意力输出 & 残差 1

多头注意力 (Multi-Head Attention)

KV Cache 写入

QKV 投影与切分

LayerNorm 1

Token Vectors [768, N]

Pos Vectors [768, N]

inpL [768, N]

cur [2304, N]

Qcur [768, N]

Q [64, N, 12]

全量 K

K [64, S, 12]

KQ [S, N, 12]

全量 V

V_trans [S, 64, 12]

Weights [S, N, 12]

KQV [64, N, 12]

cur [768, N]

inpL (跳跃连接)

inpFF [768, N]

cur [3072, N]

inpFF (跳跃连接)

下层输入 inpL [768, N]

Logits [50257, N]

embd [N]

position [N]

model.wte
[768, 50257]

model.wpe
[768, 1024]

ggml_get_rows

ggml_get_rows

ggml_add

输入特征 [768, N]

ggml_norm

ggml_mul (Scale)

ggml_add (Shift)

ln_1_g [768]

ln_1_b [768]

c_attn_attn_w
[768, 2304]

c_attn_attn_b
[2304]

ggml_mul_mat

ggml_add

ggml_view_2d (Qcur)

ggml_view_2d (Kcur)

ggml_view_2d (Vcur)

memory_k Cache
[768, 1024, 12]

memory_v Cache
[768, 1024, 12]

ggml_cpy

ggml_cpy

ggml_permute

ggml_permute

ggml_mul_mat

ggml_scale_inplace

ggml_diag_mask_inf_inplace

ggml_soft_max_inplace

ggml_permute

ggml_mul_mat

ggml_permute & ggml_cpy

c_attn_proj_w
[768, 768]

c_attn_proj_b
[768]

ggml_mul_mat

ggml_add

ggml_add (Residual 1)

ggml_norm

ggml_mul

ggml_add

ln_2_g [768]

ln_2_b [768]

c_mlp_fc_w
[768, 3072]

c_mlp_fc_b
[3072]

ggml_mul_mat

ggml_add

ggml_gelu

c_mlp_proj_w
[3072, 768]

c_mlp_proj_b
[768]

ggml_mul_mat

ggml_add

ggml_add (Residual 2)

下一层 / 最终 LN

ggml_norm

ggml_mul

ggml_add

ln_f_g [768]

ln_f_b [768]

lm_head / wte
[768, 50257]

ggml_mul_mat

ggml_get_data / memcpy

至此,还没有结束,gpt2_eval 返回的下一个词的概率,还需要经历如下采样,才能得到具体的文本。

向量 z ∈ ( 1 × 50257 ) \mathbf{z} \in \mathbf{(1 \times 50257)} z(1×50257) 包含词表中每个词的分数。

  1. 温度调节与筛选 z ′ = TopP ( TopK ( z T ) ,   p ) \mathbf{z}' = \text{TopP}\Big( \text{TopK}\big( \frac{\mathbf{z}}{T} \big), \, p \Big) z=TopP(TopK(Tz),p)
    维度仍保持为 ( 1 × 50257 ) \mathbf{(1 \times 50257)} (1×50257)(被过滤掉的位置置为 − ∞ -\infty )。
  2. Softmax 概率化:

P = Softmax ( z ′ ) ∈ ( 1 × 50257 ) P = \text{Softmax}(\mathbf{z}') \in \mathbf{(1 \times 50257)} P=Softmax(z)(1×50257)

  1. 随机采样:根据概率分布 P P P 抽样得到一个标量整数,比如采样得到 ID 2814(代表文本 " world")。

能看到这里,证明你真的想了解ggml的底层原理。
下一篇我们将深入ggml tensor的内存管理,包括ggml自己的内存池,维度管理,reshape操作的底层等等,还有我自己对ggml tensor设计的一些理解。

在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐