一个 Token 如何生成下一个 Token?拆解 Attention、KV Cache、FFN 与 MoE

2 阅读11分钟

算法层回答的不是“在哪块 GPU 上跑”,而是“这一步数学上应该得到什么”。在 zLLM 中,它首先是一组设备无关的规格、数据流和 CPU f32 reference:CPU oracle 给出可读、可测的正确性基准,Metal、CUDA、ROCm、Vulkan 与 NPU kernel 则是在同一语义上的加速实现。

这一区分很重要。高性能 kernel 可以换布局、融合算子、降低精度或并行提交,但 不能悄悄改变 causal mask、RoPE、Top-K 路由、KV 写入位置或数值定义。新算法先 在 CPU/reference 上钉住 shape、边界与结果,再与设备实现做 oracle 对拍;通过 oracle 只证明局部语义一致,最终仍要执行完整 prefill/decode 并验证输出与性能。

1. 从 token 到下一 token

可以把一层 Transformer 类比为一次有结构的思考,但不要把类比当成生物学结论: 模型的“角度、形状、视觉、语义、历史、关系”并不是六个预先命名的槽位,而是训练 后分布在高维向量中的特征方向。一个维度通常也没有独立、稳定的人类解释。

完整路径是:

  1. 分词:tokenizer 把文本或多模态占位符变成 token id。token 是词、子词、 字节片段或特殊符号,不必等同于一个汉字或单词。
  2. 向量化:embedding 按 id 查表,把离散 token 变成 hidden_size 维向量; 位置编码再让注意力知道顺序与相对位置。
  3. 逐层加工:每层先用 attention 从当前与历史 token 聚合相关信息,再用 FFN 或 MoE 对每个 token 的表示做非线性特征变换;归一化与残差连接贯穿其中。
  4. 输出:final norm 与 LM head 把 hidden 投影到词表大小的 logits,采样或 贪心选择一个 token。decode 把它作为下一轮输入,直到 EOS 或达到长度上限。

“升维、矩阵乘法、合并、降维”究竟是什么

若一行 hidden 写成 x ∈ R^d,线性层本质是 y = xW。矩阵乘法把旧坐标的 加权组合映射到一组新坐标:它既可把 d 维投影到更宽的 d_ff 维,也可投影成 Q/K/V,或把多头结果映射回 d 维。

  • 升维不是凭空增加事实,而是提供更宽的中间工作空间,使门控与非线性能够 表达更多特征组合。
  • 多头把同一 hidden 投影到多个子空间。不同 head 可能学到局部、语义、位置、 指代或视觉关系,但并没有人工规定“第 3 头就是形状”。
  • 合并通常是对 value 的加权求和,再拼接多个 head;它是在聚合信息,不是把 原句复制一遍。
  • 降维用输出投影或 FFN down projection 回到 hidden_size,从而能与残差相加 并送入下一层。它是学习得到的投影,不等于简单删除末尾若干维。

因此“一层的实际思考过程”更准确地说,是 attention 的跨 token 读取与 FFN 的 逐 token 特征变换交替进行;层数提供连续的表示修正,而向量维度提供每一步的表达 空间。

2. Attention:决定此刻应读取哪些信息

经典 scaled dot-product attention 为:

Attention(Q,K,V) = softmax(QKᵀ / √dₖ + mask)V

可以用三个问题理解它:Q 是“当前在找什么”,K 是“每段历史用什么索引被找到”, V 是“找到后取回什么内容”。causal mask 保证生成第 N 个 token 时看不到未来。 《Attention Is All You Need》用全局 self-attention 替代了当时序列模型中的循环主干,但今天的主流模型已经发展出多种计算与存储折中。

这些方案并不都是“门限分组”,应按压缩发生的位置区分:

方案核心办法主要减少什么zLLM 对应
MHA,多头注意力每个 Q head 有自己的 K/V head基线方案,表达力强但 KV 较大通用多头几何与 block/reference 语义
MQA所有 Q head 共享一组 K/VKV Cache 与 K/V 带宽可视为 num_kv_heads = 1 的 GQA 退化形态
GQA,分组查询注意力一组 Q heads 共享一个 K/V headKV Cache、K/V 投影和带宽attention/gqa.rs,支持 full/sliding window 与 hybrid layer
MLA / Gated MLA把 KV 压到低秩 latent,需要时重建;可带输出门KV 表示与投影开销attention/mla.rs,用于 GLM-5.2、DeepSeek-V3、Kimi-K3 等编排
Sliding / Block Attention只看最近窗口或显式可见块长上下文 attention 计算gqa::CausalWindowattention/block.rs
DSA / MSA学习索引器或块索引,每个 query 只选 Top-K token/块长上下文的 QK 与 AV 计算attention/dsa.rsattention/msa.rs
压缩稀疏注意力保留近期窗口,把更早历史池化压缩后选择或全读远端历史的存储与计算attention/compressed_sparse.rs
Gated DeltaNet / KDA用短卷积和固定大小 recurrent state 递推历史避免随上下文线性增长的完整 KVattention/gated_delta_net.rsattention/kda.rs

门控、分组、低秩和稀疏选择是四件不同的事。门控控制信息通过多少;GQA 让多个 query head 共享 KV;MLA 压缩表示;DSA/MSA 只让当前 query 聚焦一部分历史。 它们都像人类思考中的“聚焦”:当前问题不必同时、同强度地翻阅全部记忆,但工程上 必须明确这种聚焦是否仍扫描全历史来做选择,否则“稀疏结果”未必带来稀疏计算。

3. KV Cache:历史信息的可复用表示

自回归生成每轮只多一个 token。如果每轮都重新计算此前全部 token 的 K 和 V, 大量矩阵乘法会被重复。KV Cache 保存各层已经得到的历史 K/V 或等价状态,decode 只计算新 token,再让它查询历史并追加新记录。

它可类比为“工作记忆的索引和内容”,但不是原始对话的无损副本:

  • GQA 保存较少的 KV heads;MHA/GQA 的容量通常仍随 token 数线性增长。
  • MLA 保存归一化后的低秩 latent 与 RoPE 分量,读取时再重建所需表示。
  • zLLM 的 MLA cache 支持 F16,也定义了 latent INT8 per-group、RoPE 保持 F16 的布局;量化减少容量,但会引入可测的数值误差。
  • sliding window 只需保留有效窗口;DSA/MSA 还要维护索引信息。
  • Gated DeltaNet 与 KDA 保存固定大小 recurrent/短卷积 state,它们与 full attention KV 的更新语义不同,不能强塞进同一种存储抽象。

kv_cache/mod.rs 负责逻辑层到 cache 槽的映射、GQA/MLA 形态、格式、步长、容量 和有效长度等设备无关语义;backend 才负责 buffer 分配、驻留位置、量化 kernel 与同步。KV 跟随负责该层计算的设备驻留,多机按连续完整层切分时也不跨网络搬运 每层 KV。

4. FFN:每层内部的特征加工

Attention 解决“从别的 token 取什么”,FFN 解决“当前 token 取到信息后怎样变换”。 主流 gated MLP 可概括为:

y = W_down(act(xW_gate) ⊙ (xW_up))

gate/up 把 hidden 投影到更宽的 intermediate 空间,激活函数与逐元素乘法形成非线性 选择,down 再投影回 hidden。zLLM 的 moe/dense_mlp.rs 保存这一平台无关数据流, 当前激活规格覆盖 SiLU、clamped SiLU、SiTU、OpenAI 风格 SwiGLU 与 GELU-Tanh; 矩阵乘法仍由 backend capability 实现。

Dense FFN 与 MoE

Dense FFN 的整套 gate/up/down 权重对每个 token 都参与计算。它可被口语化为“一层 的全部专家都工作”,但源码里 dense MLP 是一个完整网络块,并不是先存在许多专家 再全部选中。

MoE 则放置多组 expert FFN,由 router 给每个 token 打分并只激活 Top-K routed experts;shared experts 可始终执行。这个设计更接近大脑中“不同区域按任务活跃”的 类比:总参数容量很大,但单个 token 的活跃计算量较小。这个类比只解释稀疏激活, 不意味着 Transformer expert 对应固定脑区或具备可命名的人格。

zLLM 的 FFN/MoE 模块边界如下:

模块功能
moe/dense_mlp.rsdense gated MLP 的规格、激活与 gate/up → activation → down 数据流
moe/routing.rs路由分数、Top-K、分组 assignment 与活跃 expert 统计
moe/topk_moe.rsrouted/shared expert 的组合、路由后缩放和输出累加
moe/latent_moe.rs原 hidden 做路由,低维 latent 进入 routed expert,shared MLP 仍读原 hidden
moe/prefill.rs多 token prefill 的 expert 分组执行与合并
moe/expert_predictor.rs根据真实历史路由预测后续 expert,服务异步预取;不持权重、不执行 I/O

MoE 首先降低的是每 token 的计算量,不会自动降低模型总权重。高吞吐、低延迟场合 通常需要让大量甚至全部 expert 权重驻留内存/显存,所以显存占用仍接近完整 MoE 模型;router 省下 FLOPs,却没有让未选 expert 从文件中消失。

在容量受限且性能要求较低的场合,可以把冷 expert 放在 CPU 内存或 SSD,路由后 按需流式加载;zLLM 也把 expert source、prefetch 与预测反馈放在 backend/runtime 边界上。但这会引入 I/O 延迟、预取命中率、并发工作集和抖动问题。只有加载与当前 层计算真正重叠,且命中率足够高,流式 expert 才可能在可接受延迟下换取较低驻留量。

5. zLLM 算法层的模块地图

目录/模块它定义什么它不负责什么
attention/mod.rs注意力家族的设备无关规格入口设备 buffer 与 command submission
attention/rope.rs位置旋转、布局与 reference模型 tokenizer
attention/gqa.rsmla.rsGQA/MLA 几何、窗口、投影关系与 f32 reference平台专属融合 kernel
attention/dsa.rsmsa.rstoken/block 评分、因果 Top-K 与稀疏选择语义宣称所有选择路径天然是 O(K)
attention/compressed_sparse.rs滑窗、压缩历史、可见位置与选择计划具体压缩 buffer 的物理驻留
attention/gated_delta_net.rskda.rsrecurrent/conv state 的形态、更新与 reference将其伪装成普通 KV Cache
attention/hybrid.rsfull attention 与线性/recurrent attention 的混合层状态决定设备放置策略
attention/hyper_connection.rsattn_res.rs多残差流、attention residual 等层内连接语义HTTP 会话或跨节点传输
moe/*dense FFN、Top-K/latent MoE、路由、共享 expert 与预取反馈语义SSD/显存分配和 DMA
kv_cache/*cache 逻辑布局、格式、容量、有效长度与持久化数据边界某平台的分配、同步和 kernel
runtime/prefill.rs通用 chunk、batch、stage 与完整层循环某个模型的 layer 顺序
runtime/generation.rstoken 生成、EOS、位置推进生命周期具体 attention/FFN 数学
runtime/<model>/按模型规格组合 embedding、每层 attention + FFN、norm、LM head复制 backend 算法实现

生产执行只有三种完整任务:NewPrefill 创建新 session 与 cache,AppendPrefill 在已有历史后追加一段 token,DecodeRound 输入一个 token、跑完所有层、追加状态并 产生下一个 token。算法层规定正确顺序,runtime 组合完整模型,backend 管资源与 提交,kernel 加速局部计算。

6. CPU oracle 如何守住正确性

CPU oracle 的价值不是“CPU 也能勉强跑模型”,而是把复杂设备优化拆成可验证问题:

  1. 用小而真实的 shape 验证分词后位置、mask、RoPE、矩阵维度和 cache append。
  2. 用确定输入对比 CPU f32 reference 与设备 kernel,检查 shape、有限值、绝对/ 相对误差,以及稀疏索引和 MoE expert id 这类离散结果。
  3. 逐层比较 hidden、路由与 logits,定位误差从哪一层开始放大。
  4. 固定 token 序列做 prefill/decode 回归,并测试 cache 边界、追加前缀和长上下文。
  5. 最后跑完整模型任务;编译通过、单算子 oracle 通过、固定 token 一致与真机性能 达标是四种不同状态,不能互相替代。

容差必须按 dtype、量化方式和误差传播设定,不能把统一的宽松阈值当作正确。对于 Top-K 路由、causal 选择、cache 长度和提交位置,很多不变量必须精确相等;对于 F16/BF16/量化矩阵输出,才使用有依据的 atol/rtol。即使局部 kernel 对拍通过, 也可能因为额外转换、同步或不合适的 shape 让端到端性能倒退,因此正确性 oracle 之后仍需要完整 prefill、decode 吞吐、峰值内存与稳定性验证。

这就是算法层在 zLLM 中的定位:它不是又一套设备实现,而是所有设备实现共同遵守 的数学合同。CPU reference 让合同可执行、可回归;attention/KV 定义怎样读取历史, FFN/MoE 定义怎样加工当前表示,runtime 再把它们组合成一次完整而可验证的生成。


中文原文:zhuai.tech/blog/algori…

English:zhuai.tech/en/blog/alg…

zLLM GitHub:github.com/zllm-lab/zl…