LLM 推理引擎与内核系统工程原理
摘要:本文从计算机系统的基础理论出发,系统阐述大语言模型推理引擎与内核工程的原理体系。核心论点是:decode 的性能由内存带宽决定而非算力决定,帧时间等于活跃权重字节数除以有效带宽;由此推出全部工程手段的分类------量化削减字节、批处理摊薄字节、预取提高有效带宽、卸载改变带宽档位、投机解码减少搬运次数、分页管理 KV 容量。全文以一次推理请求的生命周期为线索:硬件存储层次(第二章)→ 带宽瓶颈的推导与实测(第三章)→ 计算分解(第四章)→ 调度与批处理(第五章)→ KV cache 与分页显存(第六章)→ 内核设计(第七章)→ 量化(第八章)→ 采样与解码算法(第九章)→ GPU 卸载与异构执行(第十章)→ 并行推理(第十一章)→ 性能工程方法论(第十二章)→ 对拍验证(第十三章)→ 工程案例(第十四章)→ 运行时工程与实测(第十五至二十二章)→ 学习笔记(第二十三章)。所有结论均给出推导或实测数据(主体实测来自 LLMX 引擎在 Qwen3.5-35B-A3B(Gated DeltaNet 线性注意力 + Gated Attention + MoE 混合架构)模型上的测量),不包含未经检验的宣传性表述。
第一章 绪论:推理引擎的工程问题域
大语言模型(Large Language Model, LLM)推理,指模型在完成训练之后,接收一段文本前缀,逐 token 自回归地生成续文的计算过程。给定前缀 x1...t,模型一次前向计算输出下一个 token 的概率分布:
p(xt+1 | x1...t)
生成一个 token 之后,把它追加到输入序列末端,重复前向。这个循环每一步都严格依赖上一步的输出,是一条无法在硬件上并行摊平的串行依赖链。生成一个 2000 token 的回答,就意味着 2000 次串行的模型前向。这一基本事实决定了推理引擎的全部架构形态。
一次推理请求可以拆成两个计算性质截然不同的阶段。prefill 阶段 处理输入前缀:前缀中所有 token 可以同时参与矩阵运算,计算量大、并行度高,属于计算密集型。decode 阶段逐 token 生成:每次前向只有一个新增 token,权重矩阵只能沿"序列=1"的形状使用,计算量小,却必须把全部活跃权重从内存搬到计算单元,属于带宽密集型。同一份模型权重,在 prefill 中每字节被复用几千次,在 decode 中每字节只用一次。这个对比是全文最核心的出发点,第三章将用实测数据展开。
推理引擎的定义因此可以写得很精确:把数学上等价于模型前向的计算,在给定硬件上以可接受的延迟、吞吐与成本实现出来的软件系统。注意"数学上等价"这个限定------引擎的职责不是发明算法,而是不改变模型行为的前提下,在物理约束(显存容量、内存带宽、互连带宽、延迟)内逼近硬件的极限。模型结构(Transformer、MoE)是数学,引擎是数学与物理之间的工程层。两者之间隔着量化、内核优化、显存管理、调度、并行切分、卸载一整条技术链,任何一环出错,要么变慢,要么变错。
推理引擎通常分为五层,见下图:
#mermaid-svg-eCqkl5MSRzpdrAfS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eCqkl5MSRzpdrAfS .error-icon{fill:#552222;}#mermaid-svg-eCqkl5MSRzpdrAfS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eCqkl5MSRzpdrAfS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eCqkl5MSRzpdrAfS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eCqkl5MSRzpdrAfS .marker.cross{stroke:#333333;}#mermaid-svg-eCqkl5MSRzpdrAfS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eCqkl5MSRzpdrAfS p{margin:0;}#mermaid-svg-eCqkl5MSRzpdrAfS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eCqkl5MSRzpdrAfS .cluster-label text{fill:#333;}#mermaid-svg-eCqkl5MSRzpdrAfS .cluster-label span{color:#333;}#mermaid-svg-eCqkl5MSRzpdrAfS .cluster-label span p{background-color:transparent;}#mermaid-svg-eCqkl5MSRzpdrAfS .label text,#mermaid-svg-eCqkl5MSRzpdrAfS span{fill:#333;color:#333;}#mermaid-svg-eCqkl5MSRzpdrAfS .node rect,#mermaid-svg-eCqkl5MSRzpdrAfS .node circle,#mermaid-svg-eCqkl5MSRzpdrAfS .node ellipse,#mermaid-svg-eCqkl5MSRzpdrAfS .node polygon,#mermaid-svg-eCqkl5MSRzpdrAfS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eCqkl5MSRzpdrAfS .rough-node .label text,#mermaid-svg-eCqkl5MSRzpdrAfS .node .label text,#mermaid-svg-eCqkl5MSRzpdrAfS .image-shape .label,#mermaid-svg-eCqkl5MSRzpdrAfS .icon-shape .label{text-anchor:middle;}#mermaid-svg-eCqkl5MSRzpdrAfS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eCqkl5MSRzpdrAfS .rough-node .label,#mermaid-svg-eCqkl5MSRzpdrAfS .node .label,#mermaid-svg-eCqkl5MSRzpdrAfS .image-shape .label,#mermaid-svg-eCqkl5MSRzpdrAfS .icon-shape .label{text-align:center;}#mermaid-svg-eCqkl5MSRzpdrAfS .node.clickable{cursor:pointer;}#mermaid-svg-eCqkl5MSRzpdrAfS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eCqkl5MSRzpdrAfS .arrowheadPath{fill:#333333;}#mermaid-svg-eCqkl5MSRzpdrAfS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eCqkl5MSRzpdrAfS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eCqkl5MSRzpdrAfS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eCqkl5MSRzpdrAfS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eCqkl5MSRzpdrAfS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eCqkl5MSRzpdrAfS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eCqkl5MSRzpdrAfS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eCqkl5MSRzpdrAfS .cluster text{fill:#333;}#mermaid-svg-eCqkl5MSRzpdrAfS .cluster span{color:#333;}#mermaid-svg-eCqkl5MSRzpdrAfS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eCqkl5MSRzpdrAfS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eCqkl5MSRzpdrAfS rect.text{fill:none;stroke-width:0;}#mermaid-svg-eCqkl5MSRzpdrAfS .icon-shape,#mermaid-svg-eCqkl5MSRzpdrAfS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eCqkl5MSRzpdrAfS .icon-shape p,#mermaid-svg-eCqkl5MSRzpdrAfS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eCqkl5MSRzpdrAfS .icon-shape .label rect,#mermaid-svg-eCqkl5MSRzpdrAfS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eCqkl5MSRzpdrAfS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eCqkl5MSRzpdrAfS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eCqkl5MSRzpdrAfS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 硬件层
算子层
显存层
调度层
服务层
API / 会话管理
Tokenizer 编解码
采样器(解码算法)
请求调度器
批处理(continuous batching)
优先级 / 抢占 / 换出
KV Cache 管理器(分页)
显存分配器 / 内存池
换入换出(GPU↔CPU↔NVMe)
GEMM / GEMV 内核
Attention 内核(FlashAttention)
量化内核(INT8/INT4/FP8)
GPU / CPU / 异构内存 / 互连
服务层把文本变成 token 序列,并决定"输出哪个 token"(采样);调度层决定"哪些请求此刻共享一次模型前向"(批处理);显存层管理 KV cache 与权重的存放位置(分页、换出);算子层决定"一次前向怎么在芯片上算出来"(内核);硬件层提供物理资源。设计决策几乎总是横跨多层:例如把 KV cache 换出到 CPU 内存,同时涉及显存管理(块粒度)、调度(被换出序列暂停)、算子(异步拷贝)与硬件(PCIe 带宽)。
推理引擎的工程目标是一个互相挤压的约束五元组:延迟 (单个请求首 token 延迟与每 token 间隔)、吞吐 (单位时间完成的请求数)、显存容量 (模型与 KV cache 能否放下)、成本 (GPU 数量、能耗、内存占用)、正确性(输出与参考实现一致)。这五者不可能同时最优:增加批大小提高吞吐但抬高延迟;KV cache 分页提高显存利用率但引入映射开销;量化减半显存与带宽但引入精度损失。引擎工程的全部工作,就是在给定硬件与模型下对这五元组做显式、可度量的取舍。
全文结构如下:第二章建立硬件模型(存储层次、带宽、Roofline);第三章论证并实证带宽瓶颈(为什么 decode 被带宽锁死,附单机实测数据);第四章做计算分解(prefill/decode 的算子形态与 GEMM/GEMV 区别);第五章讲调度与连续批处理;第六章讲 KV cache 与分页显存管理;第七章深入内核设计(tiling、FlashAttention、算子融合、CUDA Graphs);第八章讲量化原理与工程(含 GGUF 格式细节);第九章讲采样与解码算法(含投机解码);第十章讲 GPU 卸载与异构执行;第十一章讲张量/流水线/数据/专家并行;第十二章讲性能工程与实验方法论;第十三章讲正确性、数值与对拍验证------对拍(与参考实现逐位或容差级对比)是推理引擎工程中最不可省略的环节,本章会给出完整方法论与实测案例;第十四章横向比较 vLLM、TensorRT-LLM、llama.cpp 的设计取舍;第十五至十九章展开运行时工程(模型加载与内存管理、端到端生命周期、MoE 专项、GGUF 二进制布局、注意力机制剖析);第二十至二十二章覆盖稳定性与错误处理、推理与训练的系统性差异、卸载实测案例;第二十三章为学习笔记。
写作前提:本文属于计算机系统基础理论与基础设施工程研究范畴。所有结论要么给出推导,要么给出实测,不引用未经检验的"社区共识";不使用宣传性表述,只陈述可验证的事实与可复现的方法。文中出现的实测数据来自一个自研的单机 CPU/GPU 异构推理引擎(LLMX)对 Qwen3.5-35B-A3B(混合架构:Gated DeltaNet 线性注意力 + Gated Attention + MoE;40 层、词表 248320、GGUF Q4_K 系量化)的真实测量,测试环境为单机 x86-64 平台,具体条件在各处注明。
第二章 硬件基础:存储层次、带宽与 Roofline 模型
引擎的性能上限由硬件决定,而不是由代码决定。理解推理引擎的每一行优化,都要求先把硬件的物理约束建立成模型。本章建立三个工具:存储层次(什么数据在哪、多快、多大)、带宽(搬运字节的成本)、Roofline 模型(一个 kernel 到底受算力还是受带宽限制)。
2.1 GPU 的组织结构
现代 GPU(以 NVIDIA Hopper 架构为例)由若干 GPC(Graphics Processing Cluster) 组成,每个 GPC 含若干 SM(Streaming Multiprocessor) 。SM 是执行单元:每个 SM 有 128 个 FP32 CUDA Core、4 个 Tensor Core(第四代)、256KB 寄存器文件与约 228KB 可配置的共享内存/L1。执行的基本单位是 warp(32 个线程),warp 内所有线程以 SIMT(单指令多线程)方式执行同一条指令,但允许分支发散(发散时两条路径串行执行,这是第一个常见性能陷阱)。warp 调度器每周期从可运行 warp 池中选一个发射指令;SM 上同时驻留的 warp 数(occupancy)决定延迟能否被隐藏。
一个 kernel 的指令执行模型可以抽象为:数据从 HBM 读入 L2,从 L2 读入 L1/共享内存,再从共享内存读入寄存器,参与计算(Tensor Core 直接从寄存器取操作数),结果写回。每一步都是带宽与延迟的交换。
2.2 存储层次
下表给出典型值(H100 SXM 与消费级平台混合参考,数量级即可):
| 层级 | 容量 | 带宽(数量级) | 延迟 |
|---|---|---|---|
| 寄存器 | 64KB×64K/块(256KB/SM) | 数十 TB/s(每 SM 聚合) | ~1 周期 |
| 共享内存 | 228KB/SM | 每 SM 数十 TB/s | ~30 周期 |
| L1/纹理 | 与共享内存共享 | 同共享内存 | ~30 周期 |
| L2 | 50MB(H100) | ~12 TB/s | ~200 周期 |
| HBM3 | 80GB(H100) | 3.35 TB/s | ~700 周期 |
| DDR5(系统内存) | 64-512GB | 50-100 GB/s | ~100ns |
| PCIe 5.0 x16 | --- | 单向 64 GB/s | 微秒级(含协议) |
| NVMe Gen4 | 0.5-8TB | 3-7 GB/s | ~100μs |
关键观察有三条。第一,容量与带宽反向排列:离计算越近的数据越快但也越少。第二,带宽跨度超过三个数量级:HBM 到系统内存差 30 倍以上,系统内存到 SSD 差 20 倍以上。第三,GPU 的"慢"存储(HBM)仍然比 CPU 的"快"存储(DDR5)快 30-60 倍------这就是 GPU 卸载(第十章)中一切收益的来源,也是一切代价(PCIe 往返)的来源。
#mermaid-svg-P6fhXF9olxv79ERP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P6fhXF9olxv79ERP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P6fhXF9olxv79ERP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P6fhXF9olxv79ERP .error-icon{fill:#552222;}#mermaid-svg-P6fhXF9olxv79ERP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P6fhXF9olxv79ERP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P6fhXF9olxv79ERP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P6fhXF9olxv79ERP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P6fhXF9olxv79ERP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P6fhXF9olxv79ERP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P6fhXF9olxv79ERP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P6fhXF9olxv79ERP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P6fhXF9olxv79ERP .marker.cross{stroke:#333333;}#mermaid-svg-P6fhXF9olxv79ERP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P6fhXF9olxv79ERP p{margin:0;}#mermaid-svg-P6fhXF9olxv79ERP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-P6fhXF9olxv79ERP .cluster-label text{fill:#333;}#mermaid-svg-P6fhXF9olxv79ERP .cluster-label span{color:#333;}#mermaid-svg-P6fhXF9olxv79ERP .cluster-label span p{background-color:transparent;}#mermaid-svg-P6fhXF9olxv79ERP .label text,#mermaid-svg-P6fhXF9olxv79ERP span{fill:#333;color:#333;}#mermaid-svg-P6fhXF9olxv79ERP .node rect,#mermaid-svg-P6fhXF9olxv79ERP .node circle,#mermaid-svg-P6fhXF9olxv79ERP .node ellipse,#mermaid-svg-P6fhXF9olxv79ERP .node polygon,#mermaid-svg-P6fhXF9olxv79ERP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P6fhXF9olxv79ERP .rough-node .label text,#mermaid-svg-P6fhXF9olxv79ERP .node .label text,#mermaid-svg-P6fhXF9olxv79ERP .image-shape .label,#mermaid-svg-P6fhXF9olxv79ERP .icon-shape .label{text-anchor:middle;}#mermaid-svg-P6fhXF9olxv79ERP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-P6fhXF9olxv79ERP .rough-node .label,#mermaid-svg-P6fhXF9olxv79ERP .node .label,#mermaid-svg-P6fhXF9olxv79ERP .image-shape .label,#mermaid-svg-P6fhXF9olxv79ERP .icon-shape .label{text-align:center;}#mermaid-svg-P6fhXF9olxv79ERP .node.clickable{cursor:pointer;}#mermaid-svg-P6fhXF9olxv79ERP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-P6fhXF9olxv79ERP .arrowheadPath{fill:#333333;}#mermaid-svg-P6fhXF9olxv79ERP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-P6fhXF9olxv79ERP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-P6fhXF9olxv79ERP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P6fhXF9olxv79ERP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P6fhXF9olxv79ERP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P6fhXF9olxv79ERP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-P6fhXF9olxv79ERP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-P6fhXF9olxv79ERP .cluster text{fill:#333;}#mermaid-svg-P6fhXF9olxv79ERP .cluster span{color:#333;}#mermaid-svg-P6fhXF9olxv79ERP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-P6fhXF9olxv79ERP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P6fhXF9olxv79ERP rect.text{fill:none;stroke-width:0;}#mermaid-svg-P6fhXF9olxv79ERP .icon-shape,#mermaid-svg-P6fhXF9olxv79ERP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P6fhXF9olxv79ERP .icon-shape p,#mermaid-svg-P6fhXF9olxv79ERP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-P6fhXF9olxv79ERP .icon-shape .label rect,#mermaid-svg-P6fhXF9olxv79ERP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P6fhXF9olxv79ERP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-P6fhXF9olxv79ERP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-P6fhXF9olxv79ERP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主机
GPU板上
GPU芯片内
PCIe 5.0 64GB/s
寄存器
~1周期
数十TB/s
共享内存
~30周期
数十TB/s/SM
L2 50MB
~200周期
~12TB/s
HBM3 80GB
~700周期
3.35TB/s
DDR5 系统内存
~100ns
50-100GB/s
NVMe SSD
~100μs
3-7GB/s
2.3 算力与张量核心
矩阵乘是 Transformer 前向的绝对主体(Attention 的 QK^T/AV 与 FFN 的两次线性变换)。GPU 用 Tensor Core 执行矩阵乘:一条 warp 级指令(如 mma.m16n8k16)让 32 个线程共同完成 16×8×16 的矩阵乘累加,单条指令的 FLOP 数远高于 CUDA Core。H100 的稠密 FP16 Tensor Core 算力约 990 TFLOPS,稀疏(2:4 结构化稀疏)翻倍。代价是操作数必须预先排布在寄存器中、形状必须对齐 tile 规格(如 K=16 的倍数)------这决定了内核设计中的"tiling"必须匹配硬件指令的形状(第七章展开)。
2.4 带宽:为什么"字节"比"FLOP"贵
计算一 FLOP 只需要寄存器里的两个数做一次乘加;而搬运一个字节需要:命中缓存则走片上互连,未命中则穿越内存控制器、DRAM 总线、片外物理链路。片上每字节能耗约 0.1-1 pJ 量级,HBM 每字节约 10 pJ,DDR 约 20 pJ 量级,而一次 FLOP 仅约 1 pJ 量级。搬运一个字节的能耗与延迟都高出计算几个数量级。由此得到工程上的第一准则:让每个字节被尽可能多地复用。prefill 能做到每字节复用数千次,所以快;decode 每字节只用一次,所以被带宽锁死------这正是第三章的主题。
2.5 Roofline 模型
Roofline 是判断 kernel 瓶颈归属的标准工具。对一段计算,定义算术强度(每搬运一字节执行多少次浮点运算):
I = FLOPs / Bytes
硬件有两个上限:峰值算力 P(FLOP/s)与峰值带宽 B(Byte/s)。可达到的性能上界为:
min(P, I×B)
两线交点的算术强度 I* = P/B 称为脊点。I < I* 的 kernel 位于带宽主导区(提升算力无用,必须减少字节或提高带宽利用);I > I* 的 kernel 位于计算主导区(必须提升算术效率)。
以 H100 为例:FP16 稠密 Tensor Core P ≈ 990 TFLOPS,B = 3.35 TB/s,脊点 I* ≈ 3×10^5 FLOP/B。这意味着:任何算术强度低于十万 FLOP/B 的 kernel 都被带宽限制。对照下一章的数值:decode 的算术强度在 1-4 FLOP/B 量级------比脊点低约五个数量级。结论不是"GPU 算力没用",而是"decode 阶段根本轮不到算力登场"。Roofline 的工程用途:任何性能优化动手之前,先算目标 kernel 的算术强度,判断瓶颈归属,避免在带宽受限的 kernel 上优化指令吞吐(这类错误在工程中反复出现)。
#mermaid-svg-WlfY44SKdkMuYtGV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WlfY44SKdkMuYtGV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WlfY44SKdkMuYtGV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WlfY44SKdkMuYtGV .error-icon{fill:#552222;}#mermaid-svg-WlfY44SKdkMuYtGV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WlfY44SKdkMuYtGV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WlfY44SKdkMuYtGV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WlfY44SKdkMuYtGV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WlfY44SKdkMuYtGV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WlfY44SKdkMuYtGV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WlfY44SKdkMuYtGV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WlfY44SKdkMuYtGV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WlfY44SKdkMuYtGV .marker.cross{stroke:#333333;}#mermaid-svg-WlfY44SKdkMuYtGV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WlfY44SKdkMuYtGV p{margin:0;}#mermaid-svg-WlfY44SKdkMuYtGV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Roofline:decode kernel 的位置 10^010^210^410^6 10009008007006005004003002001000 性能 (FLOP/B·s)
蓝线为算力上限 P(990 TFLOPS 平台),红线为带宽上限 I×B(以算术强度 I=4 FLOP/B 计),两线交点为脊点(约 3×10^5 FLOP/B);图中纵轴为示意数量级,decode 的实际位置(1-4 FLOP/B)远在坐标左下角、带宽线之下。
2.6 CPU 侧存储细节:缓存层次、TLB 与预取
推理引擎(尤其 CPU 路径与卸载场景)的性能受 CPU 存储子系统支配,本章补全 CPU 侧的机制细节------它们与第三章的"有效带宽"直接相关。
缓存层次 。现代 x86-64 CPU 每核 32-48 KB L1D + 1-2 MB L2,多核共享 16-48 MB L3,之后是 DDR 内存。与 GPU 的差异:缓存是硬件管理的(无显式共享内存),L3 的共享带宽远低于 GPU 的 L2;DDR 带宽(50-100 GB/s 量级)比 HBM 低一个多数量级。decode 的权重流(2.74 GB/token)远超 L3 容量(本项目 16-48 MB 量级),L3 只能缓存一层的局部热点------这正是"热专家 0.03 ms、冷专家 0.44 ms"(D7/D8)的物理基础:常驻 L3 的权重与必须从 DRAM 读取的权重相差 15 倍。
TLB 。虚拟地址到物理地址的映射由页表缓存(TLB)加速。4 KB 页的 TLB 覆盖范围只有几 MB(数百项 × 4 KB),2.74 GB 的权重流意味着每步数万次页表遍历;缺失时硬件页表遍历的延迟在数十纳秒到数百纳秒量级,与 DRAM 延迟同阶------本项目实测 8 流随机读专家权重有效带宽仅 2.6 GB/s(D9),即每次访问都在等页表与缓存。软件预取(96 KB 前距)把页表遍历与数据读取提前,使在途请求重叠,有效带宽恢复到 39.9 GB/s(D10)------预取的作用对象一半是数据行,一半是页表项。大页(2 MB)可把 TLB 覆盖扩大 512 倍,但可用性依赖平台与映射方式(本项目目标平台不可用,属环境约束)。
硬件预取器与软件预取的分工。CPU 硬件预取器擅长顺序访问模式,对随机路由的专家权重几乎无效;软件预取(prefetch 指令)负责在计算期间显式发起下一行/下一页的读取。两者的边界:顺序路径(共享专家、逐层权重流)交给硬件预取器即可;随机路径(专家权重、KV 换入)必须软件预取。错误地把软件预取用于顺序路径(或反之)都会浪费带宽------预取距离是 TLB 覆盖与带宽争抢的平衡点(12.2 节的并发回归即此机制)。
NUMA。多路服务器上,内存按物理位置分属各 CPU(NUMA 节点),跨节点访问带宽降低约一半。权重按节点分布、线程绑定节点(CPU 亲和性)是推理引擎在服务器平台的标配;本项目为单路平台,未涉及,但卸载场景(权重在内存、KV 在显存、数据在 NVMe)的"数据放置"问题与 NUMA 同构------都是让访问落在本地介质。
第三章 带宽瓶颈:为什么推理被带宽锁死
本章回答一个问题:为什么同样的模型,prefill 能跑到接近算力极限,而 decode 每一步都像在"等数据"?答案是算术强度与带宽上限的物理关系,本节给出完整推导,并用一个真实推理引擎(LLMX)在 Qwen3.5-35B-A3B(混合架构)模型上的实测数据逐项验证。
3.1 decode 的算术强度推导
设活跃参数量为 P(稠密模型 P 等于全部参数量;MoE 模型 P 等于共享层参数加上路由命中的专家参数),单位参数存储字节数为 W(FP16 为 2 字节,Q4 量化约为 0.5 字节)。decode 单步(处理 1 个 token):
- 必须从内存读取的字节数:P·W
- 执行的浮点运算数:2P(每个参数一次乘、一次加)
算术强度:
I_d = 2P / (P·W) = 2 / W
代入 W 值:FP16 时 I_d = 1 FLOP/B;Q4 时 I_d ≈ 4 FLOP/B。对照第二章的脊点:H100 上 I* ≈ 990×10^12 / 3.35×10^12 ≈ 2.95×10^5 FLOP/B。decode 的算术强度比脊点低约五个数量级------它位于 Roofline 带宽主导区的最深处,提升算力对 decode 没有任何作用。
由 Roofline 带宽线直接得到每 token 时间下界:
t_d ≥ P·W / B
其中 B 是实际可用的内存带宽。这是全文最重要的公式。它说明:decode 的帧时间由"活跃权重字节数"与"内存带宽"两个量的比值决定,与芯片的 FLOP/s 无关。反读该式:要在带宽 B 的主机上达到 R token/s,必须满足 P·W ≤ B/R。举例:FP16 的 7B 模型 P·W = 14 GB,若目标 10 token/s,需要带宽 ≥ 140 GB/s------这已经超过绝大多数双通道桌面平台的实测带宽,因此"7B FP16 在 CPU 上跑出两位数 token/s"本身就是违背带宽账的陈述,任何宣称都必须先过这一关。
3.2 prefill 为什么不受此限
prefill 一次处理前缀的 S 个 token。若权重只从内存读取一遍(读入后由 L2/寄存器复用),则:
- 浮点运算数:≈ 2·P·S(每个权重与 S 个 token 各做一次乘加)
- 读取字节数:≈ P·W(读一遍;S 个 token 复用同一份权重)
I_p ≈ 2PS / (P·W) = 2S / W
S = 2048、Q4 时 I_p ≈ 8192 FLOP/B,比 decode 高三个数量级,且形状是规则的大批量 GEMM,Tensor Core 利用率高。结论:同一个模型,prefill 由算力决定,decode 由带宽决定。一次请求就是一段计算密集段加一串带宽密集段的串接,任何针对引擎的优化都必须先指明自己作用于哪一段。
3.3 KV cache 的带宽账
decode 的 Attention 每步还要读取该序列的全部历史 KV。每 token 每层的 KV 字节数:
KVC = 2 × n_kv_heads × d_head × 2(FP16,单位:字节)
以 n_kv_heads = 128、d_head = 128 为例:64 KB/token/层;40 层即 2.5 MB/token。上下文 8192 时,仅 KV cache 容量就达约 20 GB(FP16)。更关键的是 decode 每步都要把当前序列的 KV 全量读一遍:上下文越长,每步的 KV 读取字节越多,最终与权重读取同量级。KV cache 量化(FP8/INT8/INT4)直接按比例削减这一项,对长上下文场景的收益与权重量化同级。
3.4 MoE 的修正与代价
MoE 模型每 token 只激活 top-k 个专家(例如 8/64)。活跃权重流降为共享层参数加 k 个专家参数,约为全量的十分之一。按 3.1 的公式,同样的内存带宽下 decode 上限可提升约一个量级。代价是路由随机性:每个 token 命中的专家集合不同,专家权重难以驻留缓存,页表(TLB)覆盖也差。随机访问的延迟与页缺失会显著压低"有效带宽",使 MoE 的带宽收益部分被抵消。3.5 节的实测直接给出了这一项的量化结果。
3.5 实证:单机 CPU 推理引擎的带宽墙
以下数据来自 LLMX 引擎对 Qwen3.5-35B-A3B(40 层、词表 248320、GGUF Q4_K 系量化)在单机 x86-64 平台上的实测(2026-08)。所有数值均为测量值,标注测试条件。
| 编号 | 指标 | 实测值 | 测试条件 |
|---|---|---|---|
| D1 | 内存带宽(实测) | 42.6 GB/s | 多线程复制基准,本机 DRAM 实际可用带宽 |
| D2 | 每 token 活跃权重流 | 2.74 GB | Q4 量化、top-k 路由后逐层累加 |
| D3 | 带宽模型理论上限 | 15.6 token/s | 42.6 / 2.74 |
| D4 | 单序列实测上限 | 16 token/s | 无负载、单序列稳定测量 |
| D5 | 40 层前向帧时间 | 170 ms | 系统负载 82%+ 时(≈5.9 token/s) |
| D6 | 端到端生成 | 60 token / 26064 ms(2.30 token/s) | 含 chat 模板、思考流、KV 写入 |
| D7 | 冷专家 GEMV(随机路由) | 0.44 ms | 单核,权重不在 L3 |
| D8 | 热专家 GEMV(L3 驻留) | 0.03 ms | 同一内核,权重常驻 L3 |
| D9 | 8 流随机专家读 | 2.6 GB/s | 无预取(峰值带宽的 6%) |
| D10 | 同负载 + 96KB 前距预取 | 39.9 GB/s | 软件预取,11 倍于 D9 |
| D11 | 高负载下批处理 | 0.68× 单序列 | 带宽未饱和时批量净负 |
| D12 | GPU 卸载后投影预算 | ~15 ms/token | HBM 带宽 >10 倍于 DRAM |
| D13 | CPU 同路径投影 | 37 ms/token | 与 D12 同一模型同一切分 |
| D14 | GPU 合并 GEMM 收益 | 0.1 ms/token | 启动 30 μs/次,合并后每层少 5 次启动 |
逐项解读:
D1-D4 验证了带宽模型本身。把 D1 与 D2 代入 t_d ≥ P·W/B,得到理论帧时间 64.3 ms、上限 15.6 token/s;实测单序列稳定在 16 token/s,偏差小于 3%。这说明 decode 的帧时间几乎完全由"权重字节数 ÷ 内存带宽"决定,指令执行、调度、启动等其余开销全部落在噪声以内。带宽墙不是"大约存在",而是精确到个位数百分比的支配性约束。
D5-D6 展示了负载下的退化路径。系统负载(Defender 扫描、多进程竞争)抬高延迟并压低有效带宽后,帧时间按同一公式放大。这也解释了为什么推理引擎的性能实验必须控制负载环境------第三章以后的实测数据若在 82% 以上负载下采集,结论只能作为相对证据。
D7-D10 解释了"名义带宽"与"有效带宽"的差距 。冷专家 GEMV 0.44 ms 对热专家 0.03 ms,相差 15 倍;8 个并发流随机读取专家权重时,实测有效带宽只有 2.6 GB/s,是 DRAM 峰值的 6%。这两个现象同根:随机路由使每次访问都可能落在未驻留的页与缓存行上,TLB 缺失的往返时间主导了帧时间。96KB 前距的软件预取把有效带宽拉回 39.9 GB/s(11 倍),逼近 D1 的硬件上限。工程结论:在带宽受限的 decode 路径上,提高有效带宽(预取、大页、缓存友好布局)与减少字节(量化)同等重要。
D11 是带宽账的反证。若系统处于带宽饱和区,增大并行度(批量)应线性提升吞吐;实测批量反而净负(0.68×)。原因即 D7-D10 揭示的:瓶颈是随机访问的延迟与 TLB,不是带宽饱和。批量增加只会加剧缓存与页表竞争。这个现象在 MoE 上尤其明显------它把"带宽受限"部分地转变成了"延迟受限"。
D12-D14 展示了带宽墙的"另一侧"。同样的投影计算,CPU(DRAM 42.6 GB/s)预算约 37 ms/token,卸载到 GPU(HBM 3.35 TB/s)后约 15 ms/token。而 D14 显示:合并 GEMM 启动(每层 5 次 → 1 次)只省 0.1 ms/token------启动与调度开销不是瓶颈,字节搬运才是。这两个数据合起来说明:当内存带宽提升时,帧时间按同一公式下降;当调度开销优化时,几乎不动。带宽模型的预测方向与实际完全一致。
3.6 因果链总结:为什么推理被带宽锁死
把推导与实测串成一条因果链:
- 串行依赖:decode 必须逐 token 进行,第 t+1 步依赖第 t 步的输出,无法用并行摊平;
- 每字节一次使用:每步必须把活跃权重完整搬运一遍,复用度恒为 1(MoE 下活跃集随机,复用度更低);
- 算术强度极低:I_d = 2/W ∈ 1, 4 FLOP/B,比任何现代加速器的脊点低 4-5 个数量级;
- 因此帧时间由字节/带宽决定(D1-D4 实证吻合),算力、启动、调度都不是瓶颈(D14);
- 有效带宽还受延迟与 TLB 制约(D7-D10),使实际速度进一步低于名义带宽给出的上限;
- 一切优化都围绕"字节"展开:量化减字节(W 减 4 倍,帧时间减 4 倍)、批处理摊薄字节(M 个序列共用一次权重读取,吞吐 ×M 至带宽饱和)、投机解码减少搬运次数(一次前向验证多个 token)、卸载把字节搬到带宽更大的介质(DRAM→HBM,上限 ×78)、KV 量化削减长上下文的 KV 读字节。没有任何一项优化绕过带宽账------它们全部是带宽账的不同解法。
#mermaid-svg-tvVi0naeuHbW0YhH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tvVi0naeuHbW0YhH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tvVi0naeuHbW0YhH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tvVi0naeuHbW0YhH .error-icon{fill:#552222;}#mermaid-svg-tvVi0naeuHbW0YhH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tvVi0naeuHbW0YhH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tvVi0naeuHbW0YhH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tvVi0naeuHbW0YhH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tvVi0naeuHbW0YhH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tvVi0naeuHbW0YhH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tvVi0naeuHbW0YhH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tvVi0naeuHbW0YhH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tvVi0naeuHbW0YhH .marker.cross{stroke:#333333;}#mermaid-svg-tvVi0naeuHbW0YhH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tvVi0naeuHbW0YhH p{margin:0;}#mermaid-svg-tvVi0naeuHbW0YhH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tvVi0naeuHbW0YhH .cluster-label text{fill:#333;}#mermaid-svg-tvVi0naeuHbW0YhH .cluster-label span{color:#333;}#mermaid-svg-tvVi0naeuHbW0YhH .cluster-label span p{background-color:transparent;}#mermaid-svg-tvVi0naeuHbW0YhH .label text,#mermaid-svg-tvVi0naeuHbW0YhH span{fill:#333;color:#333;}#mermaid-svg-tvVi0naeuHbW0YhH .node rect,#mermaid-svg-tvVi0naeuHbW0YhH .node circle,#mermaid-svg-tvVi0naeuHbW0YhH .node ellipse,#mermaid-svg-tvVi0naeuHbW0YhH .node polygon,#mermaid-svg-tvVi0naeuHbW0YhH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tvVi0naeuHbW0YhH .rough-node .label text,#mermaid-svg-tvVi0naeuHbW0YhH .node .label text,#mermaid-svg-tvVi0naeuHbW0YhH .image-shape .label,#mermaid-svg-tvVi0naeuHbW0YhH .icon-shape .label{text-anchor:middle;}#mermaid-svg-tvVi0naeuHbW0YhH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tvVi0naeuHbW0YhH .rough-node .label,#mermaid-svg-tvVi0naeuHbW0YhH .node .label,#mermaid-svg-tvVi0naeuHbW0YhH .image-shape .label,#mermaid-svg-tvVi0naeuHbW0YhH .icon-shape .label{text-align:center;}#mermaid-svg-tvVi0naeuHbW0YhH .node.clickable{cursor:pointer;}#mermaid-svg-tvVi0naeuHbW0YhH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tvVi0naeuHbW0YhH .arrowheadPath{fill:#333333;}#mermaid-svg-tvVi0naeuHbW0YhH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tvVi0naeuHbW0YhH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tvVi0naeuHbW0YhH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tvVi0naeuHbW0YhH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tvVi0naeuHbW0YhH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tvVi0naeuHbW0YhH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tvVi0naeuHbW0YhH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tvVi0naeuHbW0YhH .cluster text{fill:#333;}#mermaid-svg-tvVi0naeuHbW0YhH .cluster span{color:#333;}#mermaid-svg-tvVi0naeuHbW0YhH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tvVi0naeuHbW0YhH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tvVi0naeuHbW0YhH rect.text{fill:none;stroke-width:0;}#mermaid-svg-tvVi0naeuHbW0YhH .icon-shape,#mermaid-svg-tvVi0naeuHbW0YhH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tvVi0naeuHbW0YhH .icon-shape p,#mermaid-svg-tvVi0naeuHbW0YhH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tvVi0naeuHbW0YhH .icon-shape .label rect,#mermaid-svg-tvVi0naeuHbW0YhH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tvVi0naeuHbW0YhH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tvVi0naeuHbW0YhH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tvVi0naeuHbW0YhH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 硬件
每token必须
读取活跃权重 P·W 字节
计算 2P FLOPs
内存带宽 B (GB/s)
算力 P_flop (TFLOP/s)
帧时间下限 = P·W/B
算术强度 = 2/W FLOP/B
低于脊点5个数量级 → 带宽主导
实测: 2.74GB/42.6GB/s → 15.6 tok/s ≈ 实测16
3.7 带宽测量的方法论
第三章的带宽账能否成立,取决于带宽怎么测。本节给出可复现的测量协议,全部来自本项目实测。
名义带宽的测量 。用多线程复制基准(内存带宽测试工具或手工 STREAM 类基准)测量平台的可用带宽上限。注意三点:其一,单线程达不到峰值 ------现代 CPU 需要足够线程数(本项目实测需多线程才到 42.6 GB/s);其二,读与写混合 ------纯读带宽与读加写带宽不同,decode 是"读多写少",测量应区分;其三,负载影响------其他进程(防病毒扫描、后台任务)会显著压低可用带宽,测量必须记录负载条件(本项目在 82%+ 负载下帧时间波动 ±30%,就是带宽被抢占的结果)。
有效带宽的测量 。名义带宽 × 利用率才是帧时间公式里的 B。对目标内核,用"读取字节数 ÷ 内核耗时"反推有效带宽,与名义带宽对比:差距揭示延迟/TLB/预取问题(第三章 D9-D10 的 2.6 GB/s 对 42.6 GB/s 即此方法的产物)。带宽模型的自洽性检查:把帧时间公式预测值与实测对照(D3 对 D4 的 15.6 对 16 token/s),偏差超过几个百分点就要找账外开销------这是性能工程的定量纪律,不是定性"感觉慢"。
带宽账的单位约定。GB/s 的 G 必须注明是 10^9 还是 2^30;字节数的计算必须包含量化格式的实际存储字节(Q4_K 不是严格 0.5 B/权重,含 scale/min 码开销,本项目实测 2.74 GB/token 是逐层累加的实测值而非 0.5×参数的估算值)。带宽账的误差来源几乎总是单位与字节口径,而不是模型本身------先校准口径,再谈优化。
第四章 计算分解:一次前向由哪些算子组成,各自吃什么资源
第三章建立了"decode 被带宽锁死"的宏观结论。本章把一次模型前向拆到算子粒度,回答:每个算子的形状、字节数、FLOPs、算术强度各是多少,哪些算子必须优化带宽,哪些必须优化算力。这是内核设计(第七章)与量化(第八章)的共同出发点。
4.1 Transformer 层的结构
一个标准的稠密 Transformer 解码层依次为:
- 输入归一化(RMSNorm);
- 注意力:Q/K/V 投影(三个线性变换)、注意力分数计算(Q·K^T)、加权求和(score·V)、输出投影;
- 残差连接;
- 前馈网络:两个线性变换与中间激活(GeLU/SiLU);
- 残差连接与归一化。
MoE 层把第 4 步替换为:路由器计算 token 到专家的概率分布,取 top-k,把 token 派发给命中的专家执行 GEMV,再把各专家的输出加权合并。路由本身是极小的计算,但它引入的随机访问是第三章 D7-D10 现象的直接来源。
4.2 算子形态:GEMM 与 GEMV
设隐藏维度 D,序列长度 S,批量 M,上下文长度 T,头部数 n_heads、KV 头数 n_kv_heads、头维 d_head(D = n_heads × d_head)。
prefill(S 个 token 并行):线性变换以矩阵乘形式出现------形状 (M·S, D) × (D, D),是规则的批量 GEMM。Q·K^T 的形状为 (M, n_heads, S, T),Attention 权重矩阵 (M, n_heads, S, S)。
decode(1 个 token):线性变换退化为向量乘矩阵------形状 (1, D) × (D, D),即 GEMV;注意力退化为 1 × T 与 T × 1 两次乘加。
两者的硬件差异是本质性的。GEMM 中,权重矩阵的一个元素被 M·S 个结果元素复用,算术强度随批量与序列长度线性增长;GEMV 中,权重每字节恰好贡献一个输出元素,复用度恒为 1。同一个线性层,在 prefill 与 decode 中分别是计算受限与带宽受限,因此引擎必须为同一数学运算维护两种内核(或一个能同时覆盖两种形状的内核)。
4.3 逐算子资源拆解(decode,单 token,Q4 权重,D=2048 量级)
以一层为例,列出各算子的字节数(读权重 + 读/写激活)与 FLOPs(量级):
| 算子 | 形状 | 读权重(Q4) | FLOPs | 算术强度(FLOP/B) | 瓶颈归属 |
|---|---|---|---|---|---|
| Q/K/V 投影 | (1,D)×(D,3D) | 3·D·D·0.5 | 6·D·D | ~4 | 带宽 |
| Q·K^T(全上下文) | 1×T | KV 读 ~2.5MB/层 | 2·T·D | 与 T 相关 | 带宽(长上下文) |
| score·V | T×1 | 同上 | 2·T·D | 与 T 相关 | 带宽 |
| 输出投影 | (1,D)×(D,D) | D·D·0.5 | 2·D·D | ~4 | 带宽 |
| RMSNorm | 逐元素 | 0 | 2D | 极高 | 带宽(读写激活) |
| 上投影(MLP) | (1,D)×(D,4D) | 2·D·D | 8·D·D | ~4 | 带宽 |
| 下投影 | (1,4D)×(4D,D) | 2·D·D | 8·D·D | ~4 | 带宽 |
三个事实:
- decode 中所有线性算子同质:算术强度都在 4 FLOP/B 附近(Q4 权重),全部位于带宽主导区。因此"decode 内核优化"不是针对某一个算子,而是针对整条链路------任何算子漏掉,都会以同样的带宽账拖慢整层。
- 注意力的字节随上下文增长:T 增长时 Q·K^T 与 score·V 的 KV 读取字节线性增长(3.3 节),长上下文下注意力成为与权重同量级的带宽消费者。
- 逐元素算子也不便宜:RMSNorm、残差、位置编码虽无权重,但每层要读写全部激活(每 token 每层 D 个元素)。批量大时这些算子的字节总量与权重同量级,同样需要融合与合并(第七章)。
4.4 MoE 层的字节预算
MoE 层中,共享层(注意力 + 共享专家)每 token 的权重字节是固定的;专家部分每 token 只读取路由命中的 k 个专家。以 Qwen3.5-35B-A3B 的实测为例:全部权重(Q4)约 17.5 GB,活跃权重流实测 2.74 GB/token------约 6.4 倍压缩。若路由行为是完全确定性的(同一 token 恒命中同一专家集),则带宽模型完全可预测;但路由结果随输入变化,冷专家命中造成第三章 D7-D10 的 TLB/延迟惩罚。MoE 的工程要点:带宽账按活跃参数计算,延迟账按访问模式计算,两者都要纳入预算。
4.5 分解对内核设计的含义
- 带宽受限的算子(全部 GEMV 类):优化目标是最大化有效带宽------向量化加载、软件预取、量化去量化与计算融合、避免中间张量往返内存;
- 计算受限的算子(prefill 的 GEMM 与注意力):优化目标是 Tensor Core 利用率------tiling、双缓冲、register blocking;
- 逐元素算子:目标是把多次内存往返合并为一次(算子融合)。
第七章将按此分类逐个展开。
4.6 激活与中间张量的带宽:被忽视的字节
第四章的算子表列出了权重字节与 KV 字节,激活张量是第三项带宽消费者,常被忽视。decode 单步每层读写激活:RMSNorm 前后各一次、残差累加一次、Q/K/V 投影输出、注意力输出、FFN 中间态(MoE 为各专家输出加权合并)------每层激活往返约 6-10 次全量读写。以 D=2048、FP16 计算,单层激活读写约 50-80 KB,40 层约 2-3 MB/token------与 KV 读取同量级,是权重流(2.74 GB)的千分之一量级,但在批量 M 增大时随 M 线性增长(权重字节被摊薄而激活字节不摊薄),最终与权重同量级。
三个工程后果:其一,算子融合(7.4 节)的收益主体是激活 ------RMSNorm 融合进 GEMM epilogue 消除一次全量读写,每层省约 12-25% 的激活往返;其二,激活的存储格式 ------FP16 是默认,BF16 精度相当但某些平台带宽相同,激活量化(q8_0,8.4 节)只在带宽饱和区有价值;其三,批量调度决策------激活带宽随 M 增长的特性,是"批量在带宽未饱和时净负"(D11)的一个组成部分:M 增大时摊薄的权重字节被线性增长的激活字节部分抵消。带宽账的完整形态因此是三笔:权重流 + KV 流 + 激活流,任何一笔记漏,帧时间模型都会系统性偏低。
第五章 调度与批处理:如何让一次权重读取被多个序列复用
第三章给出了 decode 的帧时间公式 t_d ≥ P·W/B,其中 P 是单序列的活跃参数量。批处理的意义在带宽账上非常直接:M 个序列共享同一次模型前向时,权重从内存读取一次,被 M 个序列各自的结果复用,每个序列摊薄到的字节数降为 P·W/M,单序列帧时间降为:
td,M ≥ P·W / (M·B)
直到 M·B 触及带宽饱和点。批处理不是"提高算力利用率"这么简单------它的本质是提高权重字节的复用度,与量化削减字节量在带宽账上是同一类手段(乘 M 与除 4 效果等价)。这是理解推理调度器一切设计的出发点。
5.1 静态批处理的问题
最朴素的批处理是静态批:收集满 M 个请求后一起前向,全部结束后再收集下一批。它有四个系统性缺陷:
- 队头阻塞:批中任何一个慢序列(长上下文、长输出)拖住整批,其他序列即使早完成也必须等待;
- GPU 空转:批次之间必须等待凑齐请求,吞吐波动剧烈;
- 延迟方差大:请求的完成时间取决于"它落进哪一批",与请求本身无关;
- 浪费带宽:批中序列长度差异大时,短序列的 KV 读取拖累整批。
5.2 迭代级调度(continuous batching)
现代引擎采用迭代级调度(iteration-level scheduling,即 continuous batching):每个解码步结束后重新决策批成员。某序列生成完成,立即从批中移出,新的等待序列立刻加入下一迭代。批成员每步都在变化,因此:
- 没有队头阻塞:完成即退出;
- GPU 不空转:只要有等待序列,每步都满载;
- 序列可以跨迭代"暂停":调度器把不再活跃的序列的 KV cache 换出,把显存让给新序列(见 6.4 与第十章)。
请求队列 批(模型前向) 调度器 请求队列 批(模型前向) 调度器 #mermaid-svg-I7PLxbz0Zd990vho{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I7PLxbz0Zd990vho .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I7PLxbz0Zd990vho .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I7PLxbz0Zd990vho .error-icon{fill:#552222;}#mermaid-svg-I7PLxbz0Zd990vho .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I7PLxbz0Zd990vho .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I7PLxbz0Zd990vho .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I7PLxbz0Zd990vho .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I7PLxbz0Zd990vho .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I7PLxbz0Zd990vho .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I7PLxbz0Zd990vho .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I7PLxbz0Zd990vho .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I7PLxbz0Zd990vho .marker.cross{stroke:#333333;}#mermaid-svg-I7PLxbz0Zd990vho svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I7PLxbz0Zd990vho p{margin:0;}#mermaid-svg-I7PLxbz0Zd990vho .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I7PLxbz0Zd990vho text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-I7PLxbz0Zd990vho .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-I7PLxbz0Zd990vho .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-I7PLxbz0Zd990vho .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-I7PLxbz0Zd990vho .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-I7PLxbz0Zd990vho #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-I7PLxbz0Zd990vho .sequenceNumber{fill:white;}#mermaid-svg-I7PLxbz0Zd990vho #sequencenumber{fill:#333;}#mermaid-svg-I7PLxbz0Zd990vho #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-I7PLxbz0Zd990vho .messageText{fill:#333;stroke:none;}#mermaid-svg-I7PLxbz0Zd990vho .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I7PLxbz0Zd990vho .labelText,#mermaid-svg-I7PLxbz0Zd990vho .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-I7PLxbz0Zd990vho .loopText,#mermaid-svg-I7PLxbz0Zd990vho .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-I7PLxbz0Zd990vho .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-I7PLxbz0Zd990vho .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-I7PLxbz0Zd990vho .noteText,#mermaid-svg-I7PLxbz0Zd990vho .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-I7PLxbz0Zd990vho .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I7PLxbz0Zd990vho .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I7PLxbz0Zd990vho .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-I7PLxbz0Zd990vho .actorPopupMenu{position:absolute;}#mermaid-svg-I7PLxbz0Zd990vho .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-I7PLxbz0Zd990vho .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-I7PLxbz0Zd990vho .actor-man circle,#mermaid-svg-I7PLxbz0Zd990vho line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-I7PLxbz0Zd990vho :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop每解码步 决定批成员(running + 新进 waiting)批前向(一次权重读取,M个序列复用)M个序列各得1个token完成序列出批(进入返回路径)新序列入批(加入下步)
实现要点有三。其一,批成员决策发生在每步 :等待队列中按调度策略选序列加入,运行批中按完成/超时移出,被抢占序列(见 5.3)按优先级挂起。其二,KV cache 与批解耦 :序列进出批不改变其 KV 的物理位置,只改变其是否参与当前前向(分页设计使这一点成为可能,第六章)。其三,每步的批形状动态变化:内核必须支持不规则批量(M 变化、序列长度不同),这就是"padded batch 浪费、packed/ragged batch 高效"的由来------把不同长度的序列拼进同一个 tensor 前向,需要 shape 感知的内核或 padding 到最大长度。
5.3 调度策略与抢占
调度器维护三组状态:等待(waiting)、运行(running)、完成(done),并按水位线管理显存。典型策略:
- FCFS:按到达顺序,公平但无视长度差异;
- 最短任务优先(SJF 类):预估剩余 token 数,优先短任务,降低平均延迟但可能饿死长任务;
- 优先级 + 抢占:服务级协议下,高优先级请求可以抢占低优先级序列的 GPU 时隙,被抢占序列的 KV 保留、权重计算让出;
- 长 prefill 抢占:prefill 是突发计算(4.2 节),长 prefill 会压垮同批 decode 的帧间延迟,因此调度器把长 prefill 切块或直接暂停,先让短 decode 完成(5.4)。
5.4 Chunked prefill 与 PD 分离
prefill 与 decode 混批时,prefill 的计算量会挤占 decode 的带宽。两种架构级解法:
Chunked prefill:把长 prefill 切成若干块,每块与 decode 步混批执行。首 token 延迟(TTFT)不再被整段 prefill 阻塞,带宽利用更平滑。代价:chunk 边界处的注意力需要部分重算或保存中间状态(chunked attention 的工程细节见第七章 FlashAttention 部分)。
PD 分离(prefill-decode disaggregation):prefill 与 decode 部署在不同实例上。prefill 实例批量处理长输入、吞吐优先;decode 实例专做带宽受限的逐 token 前向;两者之间通过传输 KV cache 交接(跨节点时为网络传输)。收益是两类负载的硬件形状完全解耦,代价是 KV 传输开销与实例间调度复杂度。
5.6 为什么首 token 慢:TTFT 的根因分解
首 token 延迟(TTFT)对用户体验的影响远大于后续每步,它的根因不在某一种优化上,而是一串物理与结构约束的乘积,必须逐层拆开:
TTFT = t_排队 + t_prefill + t_首步decode
根因一:自回归前缀依赖。 第一个输出 token 的分布依赖全部前缀 token 的注意力结果。前缀内的计算可以并行,但"依赖全部前缀"这一条不可绕过------不存在跳过 prefill 直接生成首 token 的数学途径,除非前缀的 KV 已经被缓存过(根因四)。
根因二:prefill 的计算量与前缀长度、参数量线性相关。 计算受限时:
t_prefill ≈ 2·P·S / C_eff
S 为前缀长度,P 为活跃参数量,C_eff 为有效算力。数字代入:Qwen3.5-35B-A3B(活跃参数约 3B)、S = 1024,FLOP 约 6.1e12。CPU 多核 AVX-512 的有效算力在 1 TFLOP/s 量级,得秒级 prefill;H100 以 50% 利用率计约 5e14 FLOP/s,得约 12 ms。带宽下界对照(第三章公式):2.74 GB 活跃权重流在 42.6 GB/s 下是 64 ms、在 3.35 TB/s 下是 0.8 ms------两端都低于计算下界,即 prefill 在 CPU 与 GPU 上都是计算主导。结论:首 token 慢的首要根因是设备算力对 2PS 这一线性账单的偿还速度;CPU 与 GPU 在此差约三个数量级,这就是"CPU 上首 token 秒级、GPU 上毫秒级"的根源。
根因三:排队与调度。 请求到达后不一定立刻获得算力:批凑齐等待(静态批)、长 prefill 与 decode 抢时间片(无 chunking 时)、实例繁忙(PD 分离下 prefill 实例过载)。调度层对 TTFT 的贡献是方差,不是均值------均值的下界由根因二决定。
根因四:前缀缓存命中与否。 若请求的前缀与已计算过的前缀相同(system prompt、few-shot 样本),其 KV 可以直接复用,prefill 计算量为零,TTFT 趋近于首步 decode 的时间。缓存命中与否把 TTFT 从"秒级"与"毫秒级"之间切换------这是缓存设计(6.3 节)直接作用于 TTFT 的通道。
四层根因的对应解法:前缀依赖不可绕过 → 缓存 KV(根因四);2PS 账单 → 提高算力(GPU)、减少 S(prompt 压缩)、减小 P(MoE 已是最小活跃集);排队 → continuous batching、chunked prefill、PD 分离(5.4 节)。工程上所有"降低首 token 延迟"的手段都能在这四个根因中找到落点,找不到落点的优化手段对 TTFT 无效。
5.5 性能指标
调度器的目标函数由服务指标定义,常用四个:
- TTFT(Time to First Token):请求到达到第一个 token 输出,受 prefill 与排队影响;
- ITL/TPOT(Inter-Token Latency / Time Per Output Token):decode 步间隔,直接对应第三章的帧时间;
- 吞吐:单位时间完成的请求数或生成的 token 数;
- SLO 违约率:超过延迟上限的请求占比。
工程上这三者互相挤压:提高批大小压吞吐、抬 ITL;PD 分离降 TTFT 但增 KV 传输;chunked prefill 平滑 TTFT 但增计算量。没有免费的午餐------引擎的每项调度决策都在这四个指标间做显式权衡,并以此为基础设置水位线与抢占策略。
5.7 调度器的实现细节:状态机与迭代循环
把第五章的调度语义落到实现:调度器是一个以"解码步"为时钟的状态机。本节给出关键实现细节,它们决定调度器能否兑现 continuous batching 的理论收益。
状态与转换。序列的四态:等待(waiting,未开始 prefill)、运行(running,在批中)、暂停(paused,KV 保留、权重让出,第十章换出)、完成(done,返回与清理)。转换由事件驱动:到达(→等待)、批容量空闲(→运行)、prefill 完成(→运行)、生成完成/超时(→完成)、显存水位不足(→暂停+换出)、换回(→等待/运行)。每个转换必须保持不变量:批容量不超预算、KV 块池不超上限、同一序列不重复入批(去重由序列 ID 表保证------这是并发正确性的常见漏洞点,参见第十一章的确定性要求)。
迭代循环。每步的执行顺序:从等待队列选序列入批(按调度策略与水位线)→ 从运行批剔除完成序列 → 组 batch → 前向(第五章:一次权重读取 M 序列复用)→ 采样 → 更新状态与 KV → 触发换出/换入决策。批成员决策的开销必须远小于前向帧时间(本项目实测 64 ms 帧时间量级,调度开销应小于 1%)------调度器内部用 O(1) 的队列与位图,不用排序(优先级用堆或桶)。
水位线与换出决策。显存余量低于水位线时,选择牺牲序列:候选排序用"剩余生成长度预估 × 已投入计算量"(短任务优先、已投入多的先完成),换出以块为粒度异步执行(10.4 节),换入按批需求逐块取回。水位线的两个参数(高水位触发、低水位停止)直接决定吞吐与换出抖动------换出过于激进会反复换入换出(抖动),过于保守则浪费显存。
调度与带宽账的接口。调度器的每一步决策都在改变带宽账的输入:批大小 M(5 章公式)、批内序列的专家并集(17.4 节)、KV 驻留比例(10.2 节)。因此调度器不是"业务逻辑"而是"带宽预算的执行器"------它的所有启发式(策略、优先级、水位线、牺牲选择)都必须能映射回第三章的帧时间公式,映射不回去的策略是装饰性复杂度。
第六章 KV Cache 与分页显存管理:PagedAttention
6.1 KV cache 是什么
自回归生成中,第 t 步需要第 1...t 个 token 的 K、V 张量。朴素实现每步重算全部历史------代价是 O(t²) 的计算;缓存方案把每步新产生的 K、V 追加存储,之后每步只读。KV cache 的容量(3.3 节):
C_KV = 2·L·n_kv_heads·d_head·2·T(FP16,字节)
其中 L 为层数,T 为上下文长度。以 40 层、128 KV 头、头维 128、FP16 的稠密模型为例:每 token 约 2.5 MB,8K 上下文约 20 GB。这是一个与模型权重同量级、且随并发序列数线性增长的显存消费者------KV cache 与权重、激活争夺同一片 HBM,显存管理的全部复杂性由此而来。
6.2 朴素分配的问题
最简单的实现是每个序列预分配"最大上下文长度"的连续显存。三个问题:一是容量浪费 ------绝大多数请求远达不到最大长度,预分配的空间长期闲置;二是碎片化 ------序列进出导致显存空洞,无法合并;三是无法共享------多个请求共享同一系统提示词(system prompt)时,KV 的重复存储与重复计算完全被浪费。在显存被 KV cache 与权重瓜分的现实下,这三项浪费直接折算为并发能力的下降。
6.3 PagedAttention:逻辑块到物理块的映射
PagedAttention(vLLM 提出)把 KV cache 按固定大小分块(典型 16 token/块),每块是连续的物理显存段;每个序列维护一张块表(block table),记录其逻辑块(按 token 顺序编号)到物理块(实际显存地址)的映射:
#mermaid-svg-KUrFm6lbxsu3BX0Z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KUrFm6lbxsu3BX0Z .error-icon{fill:#552222;}#mermaid-svg-KUrFm6lbxsu3BX0Z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KUrFm6lbxsu3BX0Z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .marker.cross{stroke:#333333;}#mermaid-svg-KUrFm6lbxsu3BX0Z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KUrFm6lbxsu3BX0Z p{margin:0;}#mermaid-svg-KUrFm6lbxsu3BX0Z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .cluster-label text{fill:#333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .cluster-label span{color:#333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .cluster-label span p{background-color:transparent;}#mermaid-svg-KUrFm6lbxsu3BX0Z .label text,#mermaid-svg-KUrFm6lbxsu3BX0Z span{fill:#333;color:#333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .node rect,#mermaid-svg-KUrFm6lbxsu3BX0Z .node circle,#mermaid-svg-KUrFm6lbxsu3BX0Z .node ellipse,#mermaid-svg-KUrFm6lbxsu3BX0Z .node polygon,#mermaid-svg-KUrFm6lbxsu3BX0Z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KUrFm6lbxsu3BX0Z .rough-node .label text,#mermaid-svg-KUrFm6lbxsu3BX0Z .node .label text,#mermaid-svg-KUrFm6lbxsu3BX0Z .image-shape .label,#mermaid-svg-KUrFm6lbxsu3BX0Z .icon-shape .label{text-anchor:middle;}#mermaid-svg-KUrFm6lbxsu3BX0Z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KUrFm6lbxsu3BX0Z .rough-node .label,#mermaid-svg-KUrFm6lbxsu3BX0Z .node .label,#mermaid-svg-KUrFm6lbxsu3BX0Z .image-shape .label,#mermaid-svg-KUrFm6lbxsu3BX0Z .icon-shape .label{text-align:center;}#mermaid-svg-KUrFm6lbxsu3BX0Z .node.clickable{cursor:pointer;}#mermaid-svg-KUrFm6lbxsu3BX0Z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .arrowheadPath{fill:#333333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KUrFm6lbxsu3BX0Z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KUrFm6lbxsu3BX0Z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KUrFm6lbxsu3BX0Z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KUrFm6lbxsu3BX0Z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KUrFm6lbxsu3BX0Z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KUrFm6lbxsu3BX0Z .cluster text{fill:#333;}#mermaid-svg-KUrFm6lbxsu3BX0Z .cluster span{color:#333;}#mermaid-svg-KUrFm6lbxsu3BX0Z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KUrFm6lbxsu3BX0Z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KUrFm6lbxsu3BX0Z rect.text{fill:none;stroke-width:0;}#mermaid-svg-KUrFm6lbxsu3BX0Z .icon-shape,#mermaid-svg-KUrFm6lbxsu3BX0Z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KUrFm6lbxsu3BX0Z .icon-shape p,#mermaid-svg-KUrFm6lbxsu3BX0Z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KUrFm6lbxsu3BX0Z .icon-shape .label rect,#mermaid-svg-KUrFm6lbxsu3BX0Z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KUrFm6lbxsu3BX0Z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KUrFm6lbxsu3BX0Z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KUrFm6lbxsu3BX0Z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 物理显存
序列A的逻辑视图
逻辑块0
(token 0-15)
逻辑块1
(token 16-31)
逻辑块2
(token 32-47)
物理块5
物理块2
物理块9
空闲块...
这与操作系统虚拟内存是同一套思想:逻辑地址连续、物理地址任意、按需分配、支持共享与换出。带来的能力:
- 按需分配:序列只占用实际使用过的块,尾部不满的块浪费被限制在半个块内;
- 零拷贝换出:换出/换入以物理块为粒度,无需搬移整个序列的连续区域;
- 前缀共享与写时复制:多个序列共享同一段前缀时,它们共享前缀的物理块(块表指到同一物理块);某个序列要写入共享块时执行 COW(copy-on-write),复制后才写入------beam search 的分支扩展与多请求共享 system prompt 因此只付出一次计算与存储;
- prompt 缓存:把公共前缀(system prompt、few-shot 样本)的 KV 持久化,新请求命中前缀时跳过 prefill,直接复用------长前缀场景下 TTFT 与计算量的下降是两个数量级量级的。
6.4 块管理器的调度语义
显存管理器按块粒度记账:空闲块链表、每序列的块表、每块的引用计数(用于 COW 与共享判定)。调度器(第五章)与块管理器协同的典型流程:
- 新序列到达:估算 prefill 需要的块数,若空闲块不足则触发换出(第十章)或拒绝;
- prefill:逐块分配并写入 KV;
- decode:每步追加一个 token,块满则分配新块;
- 序列完成:引用计数归零的块归还空闲链表;
- 显存水位:空闲块低于阈值时,调度器选择"最久未活跃"或"最低优先级"的序列,其物理块整体换出到 CPU 内存/NVMe(块表保留),换入时按需逐块取回。
块大小的选择是一个工程权衡:块大则块表小、分配次数少,但尾部浪费大(期望浪费 = 半个块 × 序列数);块小则显存利用率高,但块表与 COW 开销上升。主流实现取 16 token 量级,并把"最大序列长度"与"并发上限"作为显存预算的显式参数。
6.5 KV cache 的近似算法(有损方法)
分页解决的是"显存放不放得下";还有一类方法解决"放不下时能否少存"。这类方法修改了模型的计算语义,属于有损压缩,与量化同性质(第八章会给量化的误差分析框架):
- H2O(Heavy Hitter Oracle):观察发现少数 token 的注意力权重长期占优,只保留"重要" token 的 KV,其余丢弃;
- StreamingLLM:保留注意力头部的 sink token(前几个 token)与最近的窗口,支持无限上下文生成;
- token 回收类:按注意力分数或频率淘汰旧 token。
这三类方法都以"丢失部分注意力信息"换取显存与带宽,必须按应用场景评估质量损失,不能视为无损优化。本文后续所有"优化"若涉及有损手段,都会明确标注。
6.6 与带宽账的关系
KV cache 管理与第三章的带宽账交汇于一点:KV 的每字节在 decode 每步都会被读取一次(4.3 节)。因此 KV 的存储格式直接影响 decode 帧时间------FP16 换 FP8 减半、换 INT4 减到四分之一;分页块的选择影响读取的局部性;换出的 KV 换回时占用 PCIe 带宽。显存管理从来不是"放得下"的问题,而是"放得下且读得快"的问题------第六章与第三章共用同一本带宽账。
6.7 显存预算的完整示例
把第六章的各公式合成一个可操作的预算方法。设模型权重 W 字节、KV cache 每 token 每序列 C_tok 字节(6.1 节公式)、激活峰值 A、HBM 容量 H,并发上限 M、每序列最大上下文 T:
H ≥ W + M·C_tok·T + A
三个变量互相挤压,预算过程是显式的:
- 定权重:量化后 W(本项目 Q4 约 17.5 GB,FP16 约 70 GB);
- 定激活:峰值激活 A(prefill 阶段最大,与批大小和序列长度相关);
- 反解并发与上下文:余量 H − W − A 决定 M·T 的乘积------并发与上下文长度的容量交易(trade-off)由此显式化;
- 超预算的处置:降低并发(SLA 约束)、缩短单序列上限(截断或分块)、KV 量化(C_tok 减半/四分之三)、换出(KV 主体放 CPU,GPU 只留活跃块,第十章)。
示例(本项目配置):H=8 GB 消费级 GPU,权重 1.4 GB(全投影上 GPU + CPU 侧专家),KV 与激活按上式计算,配合 CPU 侧专家与 KV 换出,即可稳定运行 35B 级模型------容量账、带宽账、卸载策略三者在这个例子里是同一道题的三个约束。显存预算不是"够不够"的问题,而是"在哪一级约束上取舍"的问题------预算表把取舍显式化,杜绝运行时才发现的隐性不足。
第七章 算子内核设计:tiling、FlashAttention、融合与启动开销
第四章给出了每个算子的资源归属,第三章给出了带宽账。本章进入内核本身:一个 kernel 在芯片上如何组织计算才能逼近 Roofline 上界。所有技巧可归纳为四类:让数据复用(tiling)、让访存与计算重叠(流水)、减少中间张量(融合)、减少启动开销(Graph)。
7.1 GEMM 内核:tiling 的层次
GEMM 的 tiling 是三层结构。块级 tile (如 128×128 输出块):每个 SM 处理一块,块内数据从 HBM 读入共享内存------共享内存作为显式管理的缓存,把 HBM 带宽换成片上带宽;warp 级 tile (如 64×64):块内再分给 32 个线程(1 个 warp)或 warp 组,每个 warp 独立推进;寄存器级 tile:每个线程持有若干输出元素(如 4×4 或 8×8),用寄存器累加器承接 Tensor Core 的 MMA 指令。层次化设计的目标是让每个输出元素在寄存器里完成全部累加,HBM 只访问输入矩阵一次。
两个关键工程点。一是双缓冲 :当前 tile 在 Tensor Core 上计算时,下一个 tile 的输入同时从 HBM 预取进共享内存,访存与计算重叠;二是swizzle:把共享内存的存储布局按 bank 交错,避免同一 warp 内多个线程访问共享内存同一 bank 导致冲突(bank conflict 会把共享内存带宽除以冲突数)。tile 尺寸的选择不是偏好问题:它由寄存器数量、共享内存容量、MMA 指令形状(如 m16n8k16)共同约束,超出一项就牺牲另一项。
split-K 与 stream-K:当 K 维(矩阵乘的归约维)很大而输出 tile 较小时,SM 利用率不足。split-K 把 K 切成多段,各段在多个 SM 上独立累加,最后归约;stream-K 更进一步,把"输出块 × K 段"的二维工作做持久化动态调度,让快慢不一的 SM 从全局工作队列中取任务,消除负载不均衡。
7.2 GEMV 内核:带宽受限内核的写法
GEMV(decode 的主力)没有数据复用可言,tiling 帮不上忙。它的全部性能由"有效带宽"决定,因此优化方向是:
- 最大化单次访存效率:128-bit(16 字节)向量化加载,保证每次内存事务取满一个 cache line,避免字节粒度随机访问;
- 量化数据直接参与计算:权重以量化格式(Q4/Q5/Q6,第八章)存储,内核在寄存器中完成反量化并与激活乘加------绝不在内存中先反量化出 FP16 中间张量(那会制造 2-4 倍的额外带宽);
- SIMD/VNNI 指令:Q4 权重按 32 字节块布局,配合 AVX-512 VNNI 的 dpbusd 指令------每条指令 16 个 lane、每 lane 4 组 8-bit×8-bit 乘加,共 64 个 8 位乘加(Q4 权重展开为 8 位操作数后参与),反量化校正在累加链末端一次性完成(见 8.5 的格式细节);
- 软件预取:第三章 D9-D10 实测,随机访问的专家权重若不加预取只有 2.6 GB/s 有效带宽,96KB 前距预取提升到 39.9 GB/s------预取的距离是页表(TLB)覆盖与带宽争抢的平衡点;
- 行并行与流量控制:MoE 专家权重行数多时按行切分多线程并行(共享专家 512 行并行实测 28→14.3 ms),但并行流数过多会互相争抢带宽(第三章 D11 的批量净负即同一机制),流数需要以实测为准。
7.3 FlashAttention:IO 感知的注意力
标准 Attention 先算完整分数矩阵 S = Q·K^T(形状 T × T),写回 HBM,再读回算 softmax、乘 V。长上下文下 T × T 的中间矩阵读写量是 O(T²),在带宽账上不可接受。FlashAttention 的三个要点:
- 分块(tiling):Q 按块切分,K、V 流式分块读入片上 SRAM,每个 Q 块从头到尾扫一遍 K、V 块,中间结果全部留在片上;
- online softmax:softmax 需要全局的 max 与 sum,分块后每块只能拿到局部统计量。算法维护运行中的 m(局部最大值)与 l(exp 和),遇到更大的 m 时用缩放因子 e^(m_old − m_new) 修正已累积的输出------一次扫描完成,不必重读数据;
- 重缩放:最终输出按 1/l 归一。
效果:显存占用从 O(T²) 降为 O(T),HBM 访问量减少一个数量级,而数学结果与标准 softmax 逐位一致(误差仅来自浮点累加顺序,第十三章框架可验证)。FlashAttention v2 把序列维分配到更多线程/warp 并行并去掉冗余的 attention mask 计算;v3 使用 Hopper 的 TMA(张量内存加速器)做异步块搬运与 WGMMA 指令。
FlashDecoding:decode 时序列维是 1、上下文维是 T,单线程块扫完 T 不够并行。FlashDecoding 把 KV 沿序列切成多段,各段独立算局部注意力(局部 softmax 统计量),最后归并------与 split-K 同构。它还支持批量序列间并行,把"一个 query 扫全序列"摊到多个 SM 上。
chunked prefill 的注意力:prefill 切成块与 decode 混批后,块内注意力需要读取此前块产生的 KV------与 FlashAttention 相同的流式扫描结构天然支持,代价是每块要重扫一次已缓存 KV(带宽账内可预测)。
7.4 算子融合:消灭中间张量
第四章 4.3 节的逐元素算子(RMSNorm、激活、残差)每层要读写全部激活。若每个算子独立成 kernel,激活每层要在 HBM 往返多次。融合(kernel fusion)把相邻算子合并为一个 kernel:
- epilogue 融合:GEMM/GEMV 的输出在寄存器中直接接 RMSNorm、残差、激活函数,再写回------省掉一个完整张量的写与读(带宽账减两倍该张量字节);
- KV 量化融合:KV 投影的输出在写 KV cache 前直接量化存储(FP8/INT8),注意力读取时在片上反量化;
- 投影链融合:MoE 中 gate/up 投影并行计算,融合为一个 kernel 共享一次权重读取(GPU 合并 GEMM 实测每层启动从 5 次降为 1 次,但只省 0.1 ms/token------见 7.6 的取舍)。
7.5 CUDA Graphs:启动开销
GPU kernel 启动有 CPU 侧固定成本(提交、同步)。decode 每层数十个 kernel,每个启动 5-30 μs(实测本项目 30 μs/次),一长串启动开销在 prefill 的短 kernel 场景不可忽略。CUDA Graphs 把一串 kernel 捕获为图,一次提交整图执行,消除逐 kernel 的 CPU 往返。本项目实测:合并 GEMM 启动只省 0.1 ms/token------在带宽主导的 decode 路径上,启动开销占帧时间的比例很小;而在 kernel 短且密的 prefill 路径上,Graph 是标准手段。
7.6 内核选择的判据
任何内核优化动手前,先回答两个问题:该 kernel 的算术强度在哪一侧(第三章公式);瓶颈是带宽、延迟还是启动。带宽受限(decode 全部线性层):优化访存效率与预取;计算受限(prefill GEMM/注意力):优化 tile 结构与 Tensor Core 利用率;延迟受限(冷专家):优化预取与 TLB;启动受限(短 kernel 链):优化 Graph 化与合并。判据来自 Roofline,不是经验------动手前先算一遍。
第八章 量化:把带宽账除下来
8.1 量化映射
量化把连续浮点值映射到有限整数集合。基本映射是仿射变换:
x ≈ q·s + z
其中 q 为整数(INT8/INT4),s 为缩放因子,z 为零点。去掉 z(对称量化)则 x ≈ q·s。量化误差来自两个环节:取整 (舍入误差,量化间隔越大误差越大)与截断(超出表示范围的值被钳到边界,产生削峰误差)。两个环节对误差的贡献不同:间隔由 scale 决定,范围由位宽决定,两者可以独立调节。
量化粒度决定误差的空间分布:
- per-tensor:整张量一个 scale,实现最简,但把不同幅值分布的通道混在一起,小幅度通道相对误差大;
- per-channel(对权重):每输出通道一个 scale,权重的通道间幅值差异被吸收,是权重量化的主流;
- per-token(对激活):每 token 一个 scale,吸收激活的逐 token 动态范围变化(SmoothQuant 与 KV 量化采用);
- per-group:把通道分组(如 32 个权重一组)各自量化,GGUF 的 Q4_K/Q5_K 即此类,精度/开销比最好。
8.2 误差分析框架
量化的误差必须放进可度量框架,否则无法判断"够不够好"。本文采用的度量体系(与第十三章对拍共用):
- 张量级相对误差:反量化张量对原张量的最大/均方相对误差;
- logits 级差异:同输入下量化模型与参考模型输出 logits 的逐位差异与 top-k 命中率;
- 生成级一致性:两模型生成序列的可比较性。
关键事实:误差逐层传播。本项目实测,40 层量化模型与参考实现对拍,层 0 差异约 1e-9,层 6 起约 8,层 10 起约 33(logits 相对偏差量级),最终 logits 的最大误差 5.1e-3~1.28e-1 全部落在容差内(第十三章的容差定义)。这说明:量化误差不是均匀分布的,它随层数累积;因此逐层对拍定位首差层,是量化调参的标准手段。
8.3 权重量化算法
- MinMax:按张量极值定 scale,实现最简,但极值由离群点决定,多数通道精度浪费;
- MSE 最优(OBS 类):对每个权重寻找最小化量化误差的 scale/零点,考虑权重对输出误差的敏感性(Hessian 加权);
- GPTQ:逐列量化,量化当前列后用逆 Hessian 更新剩余列以补偿误差------在高位宽(4-bit)下接近无损;
- AWQ:观察到少量"显著通道"(激活幅度大的通道)主导输出精度。AWQ 不量化这些通道,而是对它们乘以小缩放因子后再量化,1% 通道保留即恢复大部分精度,且不需要 GPTQ 的二次补偿迭代,速度快得多;
- SmoothQuant:把激活的通道间方差转移到权重(逐通道除以一个平滑因子,权重乘回),使激活可以 per-token 量化、权重保持 per-channel 量化------激活量化的主要障碍(动态范围大)由此消除。
8.4 GGUF 量化格式的工程细节
GGUF 的 K 系列格式(llama.cpp 生态事实标准)值得细看,因为它体现了"量化 + 布局 + 指令集"三者耦合的工程设计。以本项目实现的 Q4_K/Q5_K/Q6_K 为例(以下事实经过逐位验证):
| 格式 | 权重位宽 | 分组结构 | 反量化公式 | 备注 |
|---|---|---|---|---|
| Q4_K | 4-bit | 256 权重/超块(144B):d+dmin+scales12+qs128;8 组×32 元素,每组一个 6 位缩放码与 6 位最小码 | x = (d·sc)·q − dmin·m | 无 −8 偏置(与 Q4_0 不同) |
| Q5_K | 4+1-bit | d+dmin+scales12+qh32+qs128(176B) | 同 Q4_K;q = 低 4 位 | (qh 第 5 位 × 16) |
| Q6_K | 6-bit | ql128+qh64+scales16 有符号+d(210B);16 组×16 元素,无 dmin | 存储值 0...63,实际值 = 存储值 − 32 | 自带 +32 偏置 |
两个工程陷阱(都是踩过并修复的真实错误):
- Q4_K 没有偏置。反量化是 (d·sc)·q − dmin·m(d、dmin 为块级 fp16,sc、m 为组级 6 位码),没有 Q4_0 的"−8"对称化------给 Q4_K 加 −8 校正会引入 5160 级偏差(实测 logits top-10 完全不匹配)。
- Q6_K 自带 +32 偏置。存储值域 0...63,真实值 = 存储值 − 32。实现 VNNI 内核时,把存储值直接当无符号数喂给 dpbusd(其 src1 槽位要求无符号),累加后必须减去 32 × Σ q8(8-bit 量化参考值的组内和)才能恢复真值;漏掉这一项会让层 1 之后输出全面爆炸(实测层 6 起偏差 8、层 10 起偏差 33)。
这两个例子说明量化格式的"数学定义"与"硬件指令语义"之间存在细微但致命的耦合,只有通过逐位对拍才能发现(第十三章)。
8.5 硬件指令与量化内核
- AVX-512 VNNI(dpbusd) :一条指令完成 16 个 lane、每 lane 4 组 8-bit×8-bit 乘加,共 64 个 8 位乘加(32 位累加器)。关键语义:src1 必须是无符号、src2 必须有符号------放反了,负权重的 2 的补码解释让数值完全错误(自测中期望 40 实测 2600)。Q4_K 的 qs128(每字节两个 4 位值)展开为每权重一字节后,与量化激活(q8)的操作数配对,恰好按块整喂 dpbusd 的 512 位操作数(16 lane × 4 字节 × 2 操作数);scale 与 min 码以标量校正出现在累加链末端(18.2 节点积形式);
- Tensor Core INT8/FP8:GPU 侧每指令 m16n8k16 的 INT8 或 FP8 矩阵乘,吞吐是 FP16 的 2 倍(INT8)或同档(FP8);FP8 的 E4M3/E5M2 两种格式分别适合权重/激活与梯度;
- KV cache 量化:6.6 节已述,KV 每字节每步读一次,KV 量化直接减 decode 帧时间;常见做法 per-token scale + per-channel scale,FP8/INT8。
量化内核的核心要求是反量化与计算融合:权重保持量化格式在内存中,内核加载后立即反量化到寄存器参与乘加,绝不让反量化后的 FP16 张量落回内存(那会吞掉全部量化收益------带宽账翻 2-4 倍)。
8.6 量化与带宽账
第三章的帧时间公式直接给出量化收益:t_d ≥ P·W/B,W 从 2(FP16)降到 0.5(Q4),帧时间上限同样降到四分之一。实测 2.74 GB/token 的活跃权重流(Q4)若改 FP16 将是约 11 GB/token------单序列上限从 16 token/s 降到约 4 token/s。量化是带宽受限推理中回报最大的单项优化,而其代价(精度损失)由 8.2 的误差框架度量,与对拍体系衔接,形成"量化 → 误差度量 → 容差判定"的闭环。
8.7 量化部署的完整决策流程
把第八章合成部署时的决策流程。输入:模型权重(FP16 原始)、目标硬件(指令集与 Tensor Core 类型)、目标质量(容差)、显存与带宽约束。
- 选位宽:从带宽账反推------帧时间预算 ÷ 权重字节 = 所需带宽档位,选满足档位的位宽(Q8→Q4→Q3 递减,带宽需求线性下降,3.1 节公式);位宽下限由质量容差决定(8.2 节误差框架);
- 选算法:Q8 用 MinMax/MSE 足够;Q4 用 GPTQ 或 AWQ(8.3 节:AWQ 速度快、1% 显著通道保留,GPTQ 精度更高但需二次补偿);激活量化走 SmoothQuant;
- 选粒度:per-channel 起步,组粒度(Q4_K 的 32 权重/组)在精度/开销比上最优(8.1 节);粒度越细精度越好但 scale 字节开销越大------组大小是可调参数;
- 选布局:布局服从指令集(18.2 节:Q4_K 的 32 字节块对齐 512 位加载),布局改动必须重做对拍;
- 端到端验证:对拍三层级全过(13.2 节:位级 → 容差级 → 行为级),容差定义在先(本项目 40 层 max err 5.1e-3~1.28e-1);
- 回归:量化方案进入配置管理,与采样参数、卸载配置一起构成可复现的推理配置。
两个工程要点:其一,量化与内核是耦合的 ------位宽、粒度、布局决定内核的指令选择(VNNI/Tensor Core/FP8),量化方案不是"换个文件格式"而是"换一套内核";其二,误差可测不等于误差可接受------容差判定必须来自任务需求(应用可接受的质量损失),而不是来自模型本身的数值统计。部署决策的每一步都在带宽账(收益)与误差框架(代价)之间显式权衡,跳过任何一步的量化部署都是未经审计的性能赌博。
第九章 采样与解码算法
9.1 采样:从 logits 到 token
模型前向输出词表 V 上的 logits,采样器决定输出哪个 token。工程上采样不是"选最大值",而是把 logits 转成概率分布再按策略抽取,其行为由模型行为与用户需求共同决定。
温度(temperature):softmax 前的缩放:
p_i = exp(z_i/T) / Σ_j exp(z_j/T)
T > 1 使分布更平坦(随机性增大),T < 1 使分布更尖锐(趋向贪心),T → 0 等价于 argmax。温度不改变 logits 的相对顺序,只改变概率的集中度。
Top-k:只保留 logits 最大的 k 个候选,其余概率清零重归一化。k 是绝对数目,与分布形状无关。
Top-p(nucleus sampling):从大到小累加概率,保留累计概率达到 p 的最小候选集。与 top-k 不同,top-p 是分布自适应的:高置信度时候选集小,低置信度时候选集大。两者可以叠加使用。
Min-p:保留概率不低于 p × p_max 的候选(p_max 为最大概率)。min-p 的集合大小也随分布变化,但阈值与最大概率成比例,对"分布非常平坦"的场景更稳健(不会像 top-p 那样在某一步把整个词表都收进来)。
典型采样(typical sampling):保留与分布期望熵接近的 token(按 log 概率与熵的距离),倾向于选择"模型自认为有把握"的 token,用于降低困惑度偏高的退化文本。
惩罚项:重复惩罚(presence/frequency penalty)对已出现 token 的 logits 做减法或缩放;no-repeat-ngram 直接禁止 n-gram 重复。惩罚改变的是分布本身,属于模型行为的修改,必须与采样参数一起作为"推理配置"文档化。
9.2 贪心与束搜索(beam search)
贪心解码每步取 argmax,简单但短视------早期一步的错误选择无法挽回。束搜索维护 B 条候选路径:每步每条路径扩展 V 个候选,取全局 top-B 保留。数学上,束搜索近似求解"使联合概率最大"的全局优化问题(精确求解是指数复杂度的):
argmax over xt+1...T ∏_τ p(x_τ | x1...τ-1)
工程要点:B 条路径共享前缀 → 6.3 节的 KV 共享与 COW 机制是束搜索的显存前提;长度惩罚(length penalty)避免束搜索偏好短句;diverse beam search 引入相似度惩罚使 B 条路径互不重复。束搜索的算力代价是 B 倍前向,在现代大模型推理中已较少用于在线服务,但在离线评测、翻译类任务中仍是标准方法。
#mermaid-svg-8lhfvg3gjCkHgQ8d{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8lhfvg3gjCkHgQ8d .error-icon{fill:#552222;}#mermaid-svg-8lhfvg3gjCkHgQ8d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8lhfvg3gjCkHgQ8d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .marker.cross{stroke:#333333;}#mermaid-svg-8lhfvg3gjCkHgQ8d svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8lhfvg3gjCkHgQ8d p{margin:0;}#mermaid-svg-8lhfvg3gjCkHgQ8d .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .cluster-label text{fill:#333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .cluster-label span{color:#333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .cluster-label span p{background-color:transparent;}#mermaid-svg-8lhfvg3gjCkHgQ8d .label text,#mermaid-svg-8lhfvg3gjCkHgQ8d span{fill:#333;color:#333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .node rect,#mermaid-svg-8lhfvg3gjCkHgQ8d .node circle,#mermaid-svg-8lhfvg3gjCkHgQ8d .node ellipse,#mermaid-svg-8lhfvg3gjCkHgQ8d .node polygon,#mermaid-svg-8lhfvg3gjCkHgQ8d .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8lhfvg3gjCkHgQ8d .rough-node .label text,#mermaid-svg-8lhfvg3gjCkHgQ8d .node .label text,#mermaid-svg-8lhfvg3gjCkHgQ8d .image-shape .label,#mermaid-svg-8lhfvg3gjCkHgQ8d .icon-shape .label{text-anchor:middle;}#mermaid-svg-8lhfvg3gjCkHgQ8d .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8lhfvg3gjCkHgQ8d .rough-node .label,#mermaid-svg-8lhfvg3gjCkHgQ8d .node .label,#mermaid-svg-8lhfvg3gjCkHgQ8d .image-shape .label,#mermaid-svg-8lhfvg3gjCkHgQ8d .icon-shape .label{text-align:center;}#mermaid-svg-8lhfvg3gjCkHgQ8d .node.clickable{cursor:pointer;}#mermaid-svg-8lhfvg3gjCkHgQ8d .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .arrowheadPath{fill:#333333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8lhfvg3gjCkHgQ8d .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8lhfvg3gjCkHgQ8d .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8lhfvg3gjCkHgQ8d .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8lhfvg3gjCkHgQ8d .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8lhfvg3gjCkHgQ8d .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8lhfvg3gjCkHgQ8d .cluster text{fill:#333;}#mermaid-svg-8lhfvg3gjCkHgQ8d .cluster span{color:#333;}#mermaid-svg-8lhfvg3gjCkHgQ8d div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8lhfvg3gjCkHgQ8d .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8lhfvg3gjCkHgQ8d rect.text{fill:none;stroke-width:0;}#mermaid-svg-8lhfvg3gjCkHgQ8d .icon-shape,#mermaid-svg-8lhfvg3gjCkHgQ8d .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8lhfvg3gjCkHgQ8d .icon-shape p,#mermaid-svg-8lhfvg3gjCkHgQ8d .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8lhfvg3gjCkHgQ8d .icon-shape .label rect,#mermaid-svg-8lhfvg3gjCkHgQ8d .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8lhfvg3gjCkHgQ8d .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8lhfvg3gjCkHgQ8d .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8lhfvg3gjCkHgQ8d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} T→0
top-k/top-p/min-p
束搜索
logits (V)
采样策略
argmax 贪心
重归一化分布
扩展 B×V 候选取 top-B
按分布抽样
长度惩罚/多样性惩罚
9.3 投机解码(speculative decoding)
原理:用一个小而快的 draft 模型(或 n-gram、查询缓存)先自回归生成 γ 个候选 token,再用目标模型一次前向并行验证全部候选。验证正确(概率不低于 draft 的预测)的 token 直接采纳;第一个不匹配处按修正拒绝采样概率接受/回退,并以该 token 为起点重新起草。
为什么快 :目标模型的前向次数从 γ 次降为 1 次,decode 的带宽账(每步 PW 字节)同样除以 γ------这是第三章带宽账的直接应用。为什么有上限:验证是带宽饱和的批量前向(γ+1 个 token 的 logits 一次算出,权重只读一遍),因此:
t_spec ≈ P·W / B + t_draft
目标帧时间降为单次前向加 draft 开销,实际加速比等于平均接受长度 γ_avg(draft 与 target 分布越一致越大),典型值 2-3。为什么批处理下收益下降:已有 M 个序列共享权重读取时,验证的边际成本趋近于零,投机带来的只有 draft 开销与复杂度------投机解码在低并发、高延迟敏感场景(单请求、长输出)收益最大。
draft 的来源:独立小模型(同 tokenizer 蒸馏);Medusa/EAGLE------在目标模型头部加多头"提前预测"模块,一次前向并行输出多个未来 token 位置的概率(Medusa 是独立头,EAGLE 使用特征级自回归草稿,接受率更高);n-gram 草稿(lookahead decoding)------用上下文中的高频 n-gram 生成草稿,无需额外模型,适合重复性强的文本。三种方案共享同一个验证框架:draft 分布与 target 分布逐位比较,接受/拒绝规则相同。
9.5 采样参数在内核中的含义:从 logits 张量到输出 token 的机械过程
前四节从"行为"角度描述了采样参数,本节从"内核"角度回答:这些参数在引擎内部到底做了什么,作用在什么数据上,开销多大,相互之间如何组合。
采样器在流水线中的位置。引擎每步产出词表大小的 logits 张量(V 个 FP32 数),采样器是消费它的最后一个环节,位于 decode 的串行关键路径上。两个事实决定采样器的工程地位:其一,logits 的生成(LM head 的 GEMV,形状 (1,D)×(D,V),V = 248320 时权重字节约 250 MB Q4)是每 token 权重流的一部分,采样参数不改变这一字节量;其二,采样本身在 V 上做 O(V) 的遍历与排序,微秒级,相对 64 ms 的 decode 帧时间可以忽略。因此采样参数对性能的影响在"采样器本身"上趋近于零,它的全部意义在于控制模型行为------但实现细节依然有讲究。
内核中的机械步骤。一个典型的采样实现按以下顺序执行:
- top-k 截断:在原始 logits 上做部分排序(partial sort,O(V) 遍历 + O(V log k) 维护堆),保留最大的 k 个。实现要点:不做全排序,V 的遍历可以用 SIMD/归约并行(GPU 上为 block 级归约);
- 温度缩放 :对截断后的 logits 乘 1/T。等价于把概率分布的对数空间差放大 1/T 倍。数值上使用稳定 softmax 的规范形式------先减最大值再除以 T 再取 exp,避免 exp 溢出:
缩放后 z_i = (z_i − z_max) / T - softmax 归一化:只在候选集上计算 exp 与归一化常数------候选集越小,exp 调用越少;
- top-p 截断:对已归一化的概率降序累加,截到累计概率 p。它必须发生在 softmax 之后(需要真实概率),而 top-k 可以发生在 softmax 之前(只需要相对大小)------这就是工程上"top-k 在前、top-p 在后"的原因;
- 抽样:逆变换采样(在候选集的累计分布上二分/线性扫描一个均匀随机数),或 Gumbel-max 技巧(对 logits 加 Gumbel 噪声后取 argmax,数学上等价于按 softmax 分布抽样,且无需排序,并行友好)。
参数组合的行为语义 。温度不改变 logits 相对顺序(T > 0 单调),只改变概率集中度:T 越大,对数概率差被压缩,分布趋于均匀;T → 0 时 argmax 不变,等价于贪心。top-k 是绝对数目截断:被截断的长尾 token 概率再高也不可能被选中,k 相对词表大小的比例决定"多样性预算"。top-p 是概率质量截断:高置信度分布(尖峰)候选集自然小,平坦分布候选集大------与 top-k 的固定 k 相比,它随分布自适应。min-p 以最大概率为参照的相对阈值,在极端平坦的分布下比 top-p 更稳健。组合顺序是语义的一部分:先 top-k 再 top-p 与先 top-p 再 top-k 得到不同的候选集,引擎必须把顺序写死在配置里并文档化(9.1 节已述)。
数值要求。logits 必须保持 FP32:即使权重是 Q4,logits 的累加误差会直接进入分布,改变抽样结果------本项目对拍中 logits 使用 FP32 累加、与参考实现逐位对齐(第十三章)。温度缩放与 softmax 的减 max 操作也在 FP32 域完成,不存在量化。采样器的数值正确性由对拍保证:同种子、同 logits 输入下,引擎的抽样输出必须与参考实现一致(包括 top-k 截断边界上的并列值处理,并列值的选择顺序是常见的隐蔽差异来源)。
内核视角的总结:采样参数不参与任何权重计算,它们操作的是每步产出的 V 个 FP32 数;top-k 是"减小候选集"(降低后续 exp 与排序成本),温度是"缩放"(改变分布集中度),top-p/min-p 是"概率质量截断"(自适应候选集),抽样是"从分布中取一个样本"。它们的开销与 LM head 的字节相比可忽略,但它们决定了"模型输出什么"------行为正确性由对拍框架(第十三章)兜底。
9.4 结构化解码(grammar-constrained)
把输出约束为合法语法(JSON、代码、正则)时,可以在每步把 logits 中违反语法前缀的 token 置零后重归一化------结构化采样。实现上维护一个语法状态机(如解析 LLM 输出的 tokenizer 级前缀栈),每步查询"当前状态下哪些 token 合法"。注意:这不是"后处理校验",而是逐 token 约束,保证输出全程合法;代价是状态机每步查询的开销与拒绝采样引入的分布偏差(约束改变了模型分布,属于行为修改,需文档化)。
第十章 GPU 卸载与异构执行:让每一字节待在最划算的介质上
10.1 问题定义
卸载(offloading)解决的是容量与带宽的双重约束:模型权重加 KV cache 加激活超过 HBM 容量,放不下时只能把一部分数据放在更慢的介质上。第三章的带宽账给出衡量标准:数据放在哪,决定了它被读取时的速度。卸载不是"把东西挪出去"那么简单------它是一道优化问题:在容量约束下,把每个数据项放到"访问频率 × 字节数"最大化的介质上。
三个介质的物理参数(第二章):
| 介质 | 带宽(量级) | 延迟 | 典型容量 |
|---|---|---|---|
| HBM(GPU 显存) | 3.35 TB/s(H100) | ~700 周期 | 80 GB |
| DDR5(CPU 内存) | 42.6 GB/s(本项目实测) | ~100 ns | 64-512 GB |
| NVMe SSD | 3-7 GB/s | ~100 μs | 0.5-8 TB |
| PCIe 5.0 x16 链路 | 单向 64 GB/s | 微秒级 | --- |
带宽差意味着:同样 2.74 GB 的权重流,在 HBM 上是 0.8 ms,在 DDR 上是 64 ms,在 SSD 上是秒级。卸载的每一项设计决策,本质都是在这三档带宽与容量之间做预算。
10.2 卸载什么:按访问频率分层
推理中数据的访问频率差异极大:
- KV cache:decode 每步读当前序列全部 KV------每步访问,频率最高;
- 活跃权重:每个 token 读一遍------每 token 访问一次;
- 共享层权重(注意力、共享专家):每 token 必读;
- 专家权重:仅路由命中的专家被读------访问频率低、随机性强;
- 词嵌入、位置编码等:少量低频。
卸载的最优顺序与直觉相反:先保 KV 与共享层在 GPU(访问频率最高),专家权重可以放 CPU 内存(本项目实测配置 LLMX_GPU_LAYERS=41 即"全投影 GPU、专家与共享层按层分配")。容量不足时,逐级向低频率项扩展卸载范围。对 MoE 而言"专家在 CPU、投影在 GPU"是常见的异构分工:专家 GEMV 是随机访问(TLB/延迟敏感,D7-D10),留在 CPU 的代价被冷缓存放大,需要配合预取。
10.3 按层卸载(layer offloading)
按层粒度卸载是 llama.cpp 生态的事实标准:参数 ngl 指定 GPU 上驻留的层数。本项目实现的语义(与 llama.cpp 对齐):
start_layer = max_layer + 1 − ngl
即"起始层 = 最大层编号 + 1 − 卸载层数"------存在一个 off-by-one 陷阱:层编号从 0 开始,若最大层编号为 39(40 层),卸载 1 层意味着起始层 39,即最后 1 层;写成"max_layer - ngl"就变成 38,少卸载一层。这类边界语义必须与参考实现对拍验证(第十三章),否则出现静默的"多算一层/少算一层"。
按层卸载的执行模型:每层前向时,若该层权重在 GPU 则调用 cuBLAS(GEMV 形态),否则调用 CPU SIMD/VNNI 内核。层间张量(隐藏状态)每层都要穿越 PCIe 一次(decode 时每 token 每层 2×D×2B 字节的往返),因此卸载方案的实际开销 = 权重读取 + 层间张量传输,两者都要进带宽预算。卸载层的比例不是二元的:任何"全 GPU"或"全 CPU"之外的比例都可行,最优比例由显存余量与 PCIe 带宽共同决定------本项目实测 GPU 投影预算约 15 ms/token 对 CPU 37 ms/token(D12-D13):CPU 路径按第三章带宽账已逼近 42.6 GB/s 的带宽极限(帧时间=权重字节/带宽,D1-D4 吻合<3%),GPU 侧 15 ms 对应的有效带宽约 100 GB/s(约为 HBM 3.35 TB/s 的 3%),受 1×N GEMV 形状与启动开销限制;带宽档位给出的是上限空间,实测差距反映两侧实现状态。
10.4 KV cache 的换入换出
KV cache 的卸载以块为粒度(第六章分页):被调度器暂停的序列,其物理块整体拷贝到 CPU 内存(异步 DMA 或 cudaMemcpyAsync),块表保留;换入时逐块取回。工程要点:
- 异步:换出拷贝与当前批的计算重叠,不占用 decode 步;
- 水位线触发:空闲块低于阈值才启动换出,避免无谓搬移;
- 回收顺序:按"最久未活跃"或"预计剩余生成最少"选择牺牲序列------调度器(5.3)与块管理器(6.4)的协同点;
- pin 内存:换出目标必须是页锁定内存,否则 PCIe 拷贝要经过额外的一次内存复制。
10.5 权重卸载:mmap 与预取流水线
权重放在 CPU 内存时的读取路径:DDR → L3 → 内核。放在 SSD 时(权重太大内存也放不下),工程上使用 mmap 把权重文件映射进地址空间,由操作系统页缓存按需换入:首次访问触发缺页 → 从 SSD 读入页缓存 → 之后访问命中页缓存。mmap 的意义是把"文件 I/O"变成"普通内存访问",页缓存作为自动的 LRU 缓存层,配合顺序预读(readahead)让顺序访问的权重流接近 NVMe 顺序带宽。
计算与 I/O 重叠是权重卸载的核心技巧。decode 是串行的层链,但层的权重读取与上一层的计算可以重叠:预取流水线在计算层 i 时,异步发起层 i+1 权重的读取(双缓冲),使带宽被持续占用而不是等待。流水线的停顿只发生在 SSD 带宽低于消耗速率时------因此"权重驻留内存"永远优于"权重在 SSD"(少一道介质),SSD 只是容量兜底。
GPU (HBM) PCIe CPU 引擎 GPU (HBM) PCIe CPU 引擎 #mermaid-svg-FpqEbXmz1oyHCQfL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FpqEbXmz1oyHCQfL .error-icon{fill:#552222;}#mermaid-svg-FpqEbXmz1oyHCQfL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FpqEbXmz1oyHCQfL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FpqEbXmz1oyHCQfL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FpqEbXmz1oyHCQfL .marker.cross{stroke:#333333;}#mermaid-svg-FpqEbXmz1oyHCQfL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FpqEbXmz1oyHCQfL p{margin:0;}#mermaid-svg-FpqEbXmz1oyHCQfL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FpqEbXmz1oyHCQfL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-FpqEbXmz1oyHCQfL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FpqEbXmz1oyHCQfL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-FpqEbXmz1oyHCQfL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-FpqEbXmz1oyHCQfL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-FpqEbXmz1oyHCQfL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-FpqEbXmz1oyHCQfL .sequenceNumber{fill:white;}#mermaid-svg-FpqEbXmz1oyHCQfL #sequencenumber{fill:#333;}#mermaid-svg-FpqEbXmz1oyHCQfL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-FpqEbXmz1oyHCQfL .messageText{fill:#333;stroke:none;}#mermaid-svg-FpqEbXmz1oyHCQfL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FpqEbXmz1oyHCQfL .labelText,#mermaid-svg-FpqEbXmz1oyHCQfL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-FpqEbXmz1oyHCQfL .loopText,#mermaid-svg-FpqEbXmz1oyHCQfL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-FpqEbXmz1oyHCQfL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FpqEbXmz1oyHCQfL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FpqEbXmz1oyHCQfL .noteText,#mermaid-svg-FpqEbXmz1oyHCQfL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-FpqEbXmz1oyHCQfL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FpqEbXmz1oyHCQfL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FpqEbXmz1oyHCQfL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FpqEbXmz1oyHCQfL .actorPopupMenu{position:absolute;}#mermaid-svg-FpqEbXmz1oyHCQfL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-FpqEbXmz1oyHCQfL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FpqEbXmz1oyHCQfL .actor-man circle,#mermaid-svg-FpqEbXmz1oyHCQfL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-FpqEbXmz1oyHCQfL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 层 i 计算期间预取层 i+1 权重(双缓冲重叠) 层 i 为 CPU 层时:VNNI 内核 + 软件预取 loop每层 异步预取 层 i+1 权重(Q4 原始格式)层 i 激活张量层 i GEMV(cuBLAS)层 i 输出激活
一个常被忽略的细节:传输的必须是量化格式的原始权重,不可以在 CPU 上反量化成 FP16 再传------那会让 PCIe 字节量放大 4 倍(本项目实测全量上传反量化权重 22.4 GB,耗时随负载波动,且该成本是每次启动的一次性开销)。量化权重直传 + GPU 侧反量化,是卸载场景下唯一正确的传输方式。
10.6 卸载的性能模型
把 10.2-10.5 合成一个可计算的模型。设权重总量 W,其中 W_g 在 GPU、W_c 在 CPU 内存(W = W_g + W_c),每 token decode 时间:
t ≈ W_c/B_DDR + W_g/B_HBM + (D_cross/B_PCIe) × L_cross
D_cross 为每次跨介质传输的层间张量字节,L_cross 为跨介质层边界数。三个结论:其一,最优卸载比例使三项之和最小(显存容量约束下);其二,层间张量项说明"混合部署的层边界"有额外代价------边界越少越好(这就是为什么"连续区间卸载"优于"交错卸载");其三,DDR 与 HBM 的带宽档位相差约 78 倍(42.6 GB/s 对 3.35 TB/s),但实测投影预算 37 ms(CPU)对 15 ms(GPU)仅差约 2.5 倍------CPU 侧已逼近带宽极限、GPU 侧受 GEMV 形状与启动限制(22.2 节详述);权重在 GPU 的比例仍是解码速度的第一决定因素。
10.7 卸载与第三章带宽账的统一
卸载没有改变带宽账,它改变的是带宽账里的 B:把 t_d ≥ P·W/B 中的 B 从"42.6 GB/s(DDR)"换成"混合带宽"。量化改变 W,批处理除以 M,卸载提高 B------三者的收益在同一个公式里线性叠加,互不排斥。这也是工程判断的准则:当用户问"卸载到 GPU 能快多少",回答是"权重中 GPU 部分的比例 ×(HBM/DDR 带宽比)",而不是任何宣传口径。
10.8 卸载决策的工程清单
把 10.1-10.7 的原则落成可执行的检查清单:
- 先算容量账:模型权重(含量化开销)+ 峰值 KV cache(6.1 节公式 × 并发上限)+ 激活峰值,与 HBM 容量对比,缺额决定卸载总量;
- 再排驻留优先级:KV cache(每步读)> 共享层权重(每 token 必读)> 高频专家(路由统计的命中频率)> 低频专家 > 词嵌入。从高到低驻留 GPU,直到容量用尽;
- 层边界最小化:按连续区间卸载(10.6 节:层间张量穿越 PCIe 有每层字节成本),避免交错部署;
- 传输格式保持量化:量化权重直传、目标侧反量化(10.5 节:反量化后再传放大 4 倍 PCIe 字节);
- 异步化一切搬移:换出/预取与计算重叠(双缓冲),pin 内存作为拷贝目标;
- 验收用带宽账:卸载后帧时间必须符合 10.6 节的混合带宽模型(偏差>10% 即查找账外开销:同步等待、未 pin 内存、PCIe 共享冲突);
- 失败路径:上传/初始化失败时降级为 CPU 路径(本项目 GPU 路径失败即 CPU 兜底重算,零错误输出可能)------卸载是性能优化,不是正确性依赖。
卸载的本质是"数据放置决策":每一字节放在哪个介质,决定了它被读取时的带宽档位。清单的每一条都在回答同一个问题:这一字节的访问频率 × 字节数,配得上哪一档带宽。工程上最容易犯的错误是把卸载当作"把东西挪出去"来理解------正确的理解是"把每一字节放在它该在的地方",二者方向相反。
第十一章 并行推理:TP、PP、DP、EP 与通信账
单卡受容量与带宽双重限制(模型放不下,或放得下但带宽不足)。并行把计算与存储分摊到多卡,但每一次并行都以通信为代价。本章给出四种并行方式的切分语义与通信账,并说明推理场景下的选择准则。
11.1 数据并行(DP)
每张卡持有完整模型副本,各服务不同请求。通信几乎为零(推理时权重只读、无需同步梯度),吞吐线性扩展,代价是显存线性占用。推理场景下 DP 是天然的起步方案:只要模型能放进单卡,DP 就提供最干净的吞吐扩展;权重副本在 NVMe/内存中共享一份源文件,加载是并行的(每卡独立读)。
11.2 张量并行(TP)
单层内的矩阵按维切分到多卡。以线性层 Y = X·W 为例:把 W 按输出维切成 N 份(列切分),每卡持 W_i,算 Y_i = X·W_i,再 all-reduce 汇总。Attention 部分把多头切到多卡,Q·K^T 与 score·V 都只需局部归约(KV 头按卡划分,无需跨卡注意力)。每层每前向两次 all-reduce,通信量 C_TP = 8D 字节/层(FP16):两次 all-reduce,每次收发各 D×2 字节。
TP 的通信量与单卡权重读取同量级------它把"带宽墙"从 HBM 换成了"HBM + NVLink",收益的前提是互连带宽显著高于单卡 HBM 带宽的差额。NVLINK(H100 约 900 GB/s 双向)约为 HBM 的四分之一到三分之一,因此 TP 度 N 的扩展不是线性的:N=2 接近线性,N 再大时通信占比上升。TP 是训练与推理共用的事实标准切分(单层内通信不涉及跨层流水),本项目 GPU 路径的 GEMV 即由 cuBLAS 在单卡内完成,多卡场景的核心理由是单卡 HBM 不够放或带宽不足。
11.3 流水线并行(PP)
按层把模型切成 N 段,每卡一段,数据以 micro-batch 流水推进。通信量小(每段边界一次张量传输),但存在气泡:流水线起停阶段部分卡空闲,气泡占比约 (N-1)/(M+N-1),M 为 micro-batch 数------M 越大气泡越小,但 M 受显存约束。推理场景 PP 的使用少于训练(推理请求天然是流水线:prefill 后接长 decode,decode 阶段整链同时活动,气泡被请求并发填补),主要用于单卡放不下单层(超大模型)的兜底。
11.4 专家并行(EP)
MoE 的专家按卡分配(每卡若干专家),路由把 token 派发到对应卡:all-to-all 通信(每步每个 token 可能去任何一张卡)。EP 的通信量与路由发散程度成正比,与 TP 的正交组合(EP × TP:专家内张量切分 + 专家间分发)是 MoE 大模型的标准部署形态。EP 的随机性使通信模式不规律------与第三章 MoE 的 TLB 问题同源(随机访问在哪个层次都有代价)。
#mermaid-svg-1gbSqLINo032l4iN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1gbSqLINo032l4iN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1gbSqLINo032l4iN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1gbSqLINo032l4iN .error-icon{fill:#552222;}#mermaid-svg-1gbSqLINo032l4iN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1gbSqLINo032l4iN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1gbSqLINo032l4iN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1gbSqLINo032l4iN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1gbSqLINo032l4iN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1gbSqLINo032l4iN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1gbSqLINo032l4iN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1gbSqLINo032l4iN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1gbSqLINo032l4iN .marker.cross{stroke:#333333;}#mermaid-svg-1gbSqLINo032l4iN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1gbSqLINo032l4iN p{margin:0;}#mermaid-svg-1gbSqLINo032l4iN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1gbSqLINo032l4iN .cluster-label text{fill:#333;}#mermaid-svg-1gbSqLINo032l4iN .cluster-label span{color:#333;}#mermaid-svg-1gbSqLINo032l4iN .cluster-label span p{background-color:transparent;}#mermaid-svg-1gbSqLINo032l4iN .label text,#mermaid-svg-1gbSqLINo032l4iN span{fill:#333;color:#333;}#mermaid-svg-1gbSqLINo032l4iN .node rect,#mermaid-svg-1gbSqLINo032l4iN .node circle,#mermaid-svg-1gbSqLINo032l4iN .node ellipse,#mermaid-svg-1gbSqLINo032l4iN .node polygon,#mermaid-svg-1gbSqLINo032l4iN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1gbSqLINo032l4iN .rough-node .label text,#mermaid-svg-1gbSqLINo032l4iN .node .label text,#mermaid-svg-1gbSqLINo032l4iN .image-shape .label,#mermaid-svg-1gbSqLINo032l4iN .icon-shape .label{text-anchor:middle;}#mermaid-svg-1gbSqLINo032l4iN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1gbSqLINo032l4iN .rough-node .label,#mermaid-svg-1gbSqLINo032l4iN .node .label,#mermaid-svg-1gbSqLINo032l4iN .image-shape .label,#mermaid-svg-1gbSqLINo032l4iN .icon-shape .label{text-align:center;}#mermaid-svg-1gbSqLINo032l4iN .node.clickable{cursor:pointer;}#mermaid-svg-1gbSqLINo032l4iN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1gbSqLINo032l4iN .arrowheadPath{fill:#333333;}#mermaid-svg-1gbSqLINo032l4iN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1gbSqLINo032l4iN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1gbSqLINo032l4iN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1gbSqLINo032l4iN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1gbSqLINo032l4iN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1gbSqLINo032l4iN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1gbSqLINo032l4iN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1gbSqLINo032l4iN .cluster text{fill:#333;}#mermaid-svg-1gbSqLINo032l4iN .cluster span{color:#333;}#mermaid-svg-1gbSqLINo032l4iN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1gbSqLINo032l4iN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1gbSqLINo032l4iN rect.text{fill:none;stroke-width:0;}#mermaid-svg-1gbSqLINo032l4iN .icon-shape,#mermaid-svg-1gbSqLINo032l4iN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1gbSqLINo032l4iN .icon-shape p,#mermaid-svg-1gbSqLINo032l4iN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1gbSqLINo032l4iN .icon-shape .label rect,#mermaid-svg-1gbSqLINo032l4iN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1gbSqLINo032l4iN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1gbSqLINo032l4iN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1gbSqLINo032l4iN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 专家并行:专家按卡分配
卡0: 专家A,B
路由器 all-to-all
卡1: 专家C,D
流水线并行:按层分段
层0-19
层20-39
张量并行:一层切到2卡
W 左半
AllReduce
W 右半
11.5 推理场景的选择准则
- 先 DP:模型能进单卡时,DP 无通信、吞吐线性------推理引擎的默认答案;
- 单卡放不下或带宽不足:TP 切单层(通信在 NVLink 上、可控),优先于 PP;
- 单层放不下(极端超大模型):PP 兜底,接受气泡;
- MoE 且卡间互连充裕:EP 把随机访问的专家分摊到多卡,同时摊薄单卡 TLB/延迟压力;
- 所有并行方式都要回到第三章的账:并行的收益上限是"聚合带宽与单卡带宽之比",通信开销直接扣在分子上。任何并行方案的立项评估,先算通信字节与权重字节的比值,再决定切分度。
11.6 通信内核:all-reduce 的归约算法与实现选择
张量并行(11.2 节)的每次前向包含两次 all-reduce,通信内核的实现直接决定 TP 的实际收益。本节点到硬件层面。
归约算法的三种形态:
- Ring all-reduce:把 N 张卡的张量切成 N 段,环形单向传递------每卡每轮收发各一段(2(N-1) 步),总通信量 2×(N-1)/N × 数据量;带宽利用满、实现简单,是默认选择;
- Tree all-reduce:二叉树归约 + 广播,延迟 O(log N) 但带宽利用低,适合小张量、延迟敏感场景;
- NVLink SHARP / 网内归约:把归约下沉到互连硬件(交换机或 NVSwitch 内做 reduce),把"收-算-发"变成硬件单步,延迟与带宽同时改善------依赖特定硬件,不可移植。
通信与计算的带宽账 。TP 的收益上限由通信量与计算量的比值决定(11.5 节)。decode 场景每层通信 8D 字节(D=2048 时 32 KB),单层权重约 12.6 MB(Q4,约 6 个 2048×2048 矩阵),通信占比约 0.25%------通信可被计算隐藏(kernel 内嵌归约、异步流);但 N 增大时通信字节不变、每卡权重字节减半,占比上升。工程判据:通信字节 / 每卡权重字节 > 10% 时,TP 度继续增加不再划算------这个比值与模型维度、量化位宽、卡数相关,动手前先算。
实现陷阱 :归约的浮点累加顺序破坏确定性(13.5 节)------ring 的分段顺序固定即可保持确定性;内核内嵌归约(在 GEMM epilogue 里直接做跨卡 reduce)比独立通信 kernel 少一次张量往返(7.4 节同构);通信与计算的重叠需要流(stream)级异步,同步点只在层边界。TP 的全部工程细节都服从同一原则:通信是带宽账的一个科目,而不是可以脱离带宽账的独立优化。
第十二章 性能工程与实验方法论
性能优化失败的典型路径不是"优化错了地方",而是"没测量就动手"。本章给出可复用的测量与决策纪律,全部原则来自本项目踩过的真实坑。
12.1 瓶颈定位的顺序:先算,再测,后优化
优化的第一步是计算而不是测量:用第三章的公式把目标 kernel 的算术强度算出来,判断它属于带宽主导还是计算主导,直接排除一半以上的无效方向。第二步才用 profiler 验证:CPU 侧用 perf/计时器按阶段拆帧时间(本项目把每层拆成专家、共享、分支三段计时),GPU 侧用 nsys/ncu 看 kernel 时间与带宽利用率。第三步才是动手。跳过第一步直接 profile 的问题:profile 只能告诉你"慢在哪",不能告诉你"为什么慢是物理必然"------带宽受限的 kernel 优化指令吞吐,就是把时间花在物理上不可能赢的地方。
12.2 实验纪律:微基准结论在并发下可能反转
本项目三次真实回归共同指向一条纪律:单线程微基准的结论在并发环境下可能反转,引擎级验证是唯一判据。
- 预取距离实验:单线程冷读下,5 行首预取把内核从 0.606 ms 降到 0.386 ms;并发 8 线程下预取流量 ×5 争抢带宽,内核反而从 0.4 ms 恶化到 1.5 ms。单线程结论完全反转;
- 共享专家并行:共享专家 512 行切 4 任务并行,诊断中 28→14.3 ms;但把共享专家与专家阶段主线程并行(24 任务队列碰撞)后,专家阶段 71→96 ms,整体回归;
- 16 任务阶段拆分:16 个并行任务比 8 任务更差(5.16 vs 1.49 ms)------任务数不是越多越好,流的数量与缓存/TLB 资源互相争抢。
这些实验的共同模式:微基准环境干净(单线程、数据热)、引擎环境嘈杂(并发、数据冷、负载波动),结论在两者之间不可迁移。因此本项目确立的规则:任何性能优化必须在引擎级(真实负载路径)验证后才可发布;引擎级无法验证的优化一律保守回退。
12.3 负载与噪声控制
系统负载(Defender 扫描、其他进程)会让帧时间波动 ±30% 以上(本项目在 82%+ 负载下测量的 40 层帧时间 170 ms,噪声淹没了一切小优化)。测量纪律:
- 计时工具与源码同步重建------用旧构建验证新防护会得到"假卡死"的误判;
- 多次测量取中位数,区分"优化生效"与"负载波动"(±30% 噪声下,10% 级别的优化不可判定);
- 低负载窗口做引擎级 A/B;高负载下只做带宽/带宽上限类的确定结论(物理上限不受负载影响);
- 端到端指标(tok/s、帧时间)与阶段拆解指标(专家/共享/分支)分开记录,后者用于定位,前者用于判定。
12.4 决策流程:诊断先行,预算复核
任何优化的立项顺序:诊断(测量现状与物理上限)→ 计算收益上限(第三章公式)→ 决策(做/不做)→ 实现 → 引擎级验证 → 回归对拍。本项目两个实例:
- GPU 投影合并 GEMM:诊断实测合并后每 token 仅省 0.1 ms(启动 30 μs/次,远低于文档假设的 2.2 ms),收益被负载噪声吞没------诊断先行直接否决立项,避免无谓集成;
- 内核预取升级:诊断实测冷专家单线程 1.15 vs 3.48 ms(3 倍),且数值等价------预取组合保留。
两个实例的共同点是"先用测量杀死或确认假设,再决定投入"。
#mermaid-svg-0jJZU4jD6GzuRXtO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0jJZU4jD6GzuRXtO .error-icon{fill:#552222;}#mermaid-svg-0jJZU4jD6GzuRXtO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0jJZU4jD6GzuRXtO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0jJZU4jD6GzuRXtO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0jJZU4jD6GzuRXtO .marker.cross{stroke:#333333;}#mermaid-svg-0jJZU4jD6GzuRXtO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0jJZU4jD6GzuRXtO p{margin:0;}#mermaid-svg-0jJZU4jD6GzuRXtO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0jJZU4jD6GzuRXtO .cluster-label text{fill:#333;}#mermaid-svg-0jJZU4jD6GzuRXtO .cluster-label span{color:#333;}#mermaid-svg-0jJZU4jD6GzuRXtO .cluster-label span p{background-color:transparent;}#mermaid-svg-0jJZU4jD6GzuRXtO .label text,#mermaid-svg-0jJZU4jD6GzuRXtO span{fill:#333;color:#333;}#mermaid-svg-0jJZU4jD6GzuRXtO .node rect,#mermaid-svg-0jJZU4jD6GzuRXtO .node circle,#mermaid-svg-0jJZU4jD6GzuRXtO .node ellipse,#mermaid-svg-0jJZU4jD6GzuRXtO .node polygon,#mermaid-svg-0jJZU4jD6GzuRXtO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0jJZU4jD6GzuRXtO .rough-node .label text,#mermaid-svg-0jJZU4jD6GzuRXtO .node .label text,#mermaid-svg-0jJZU4jD6GzuRXtO .image-shape .label,#mermaid-svg-0jJZU4jD6GzuRXtO .icon-shape .label{text-anchor:middle;}#mermaid-svg-0jJZU4jD6GzuRXtO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0jJZU4jD6GzuRXtO .rough-node .label,#mermaid-svg-0jJZU4jD6GzuRXtO .node .label,#mermaid-svg-0jJZU4jD6GzuRXtO .image-shape .label,#mermaid-svg-0jJZU4jD6GzuRXtO .icon-shape .label{text-align:center;}#mermaid-svg-0jJZU4jD6GzuRXtO .node.clickable{cursor:pointer;}#mermaid-svg-0jJZU4jD6GzuRXtO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0jJZU4jD6GzuRXtO .arrowheadPath{fill:#333333;}#mermaid-svg-0jJZU4jD6GzuRXtO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0jJZU4jD6GzuRXtO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0jJZU4jD6GzuRXtO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0jJZU4jD6GzuRXtO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0jJZU4jD6GzuRXtO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0jJZU4jD6GzuRXtO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0jJZU4jD6GzuRXtO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0jJZU4jD6GzuRXtO .cluster text{fill:#333;}#mermaid-svg-0jJZU4jD6GzuRXtO .cluster span{color:#333;}#mermaid-svg-0jJZU4jD6GzuRXtO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0jJZU4jD6GzuRXtO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0jJZU4jD6GzuRXtO rect.text{fill:none;stroke-width:0;}#mermaid-svg-0jJZU4jD6GzuRXtO .icon-shape,#mermaid-svg-0jJZU4jD6GzuRXtO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0jJZU4jD6GzuRXtO .icon-shape p,#mermaid-svg-0jJZU4jD6GzuRXtO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0jJZU4jD6GzuRXtO .icon-shape .label rect,#mermaid-svg-0jJZU4jD6GzuRXtO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0jJZU4jD6GzuRXtO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0jJZU4jD6GzuRXtO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0jJZU4jD6GzuRXtO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
新优化想法
Roofline 预判(第三章公式)
诊断实测现状与上限
收益是否超过噪声/风险?
否决并记录(ROI 账本)
实现
引擎级 A/B 验证
验证通过?
回退 + 记录实验
回归对拍(第十三章)
发布
12.5 性能预算表
把帧时间预算显式分配到各阶段,是防止"优化了 A 却毁掉 B"的手段。以本项目 decode 帧时间预算为例(数值随负载波动,结构固定):专家 GEMV(活跃专家权重读取 + TLB 开销)约 37%、共享层约 15%、分支/其余约 45% 的组成比例(负载下实测 63/26/77 ms),每一项都有自己的物理上限(第三章公式)与有效带宽目标。预算表使每次优化都回答三个问题:动的是哪一项、上限是多少、超出上限的部分必然来自哪里(延迟?争抢?)。没有预算表的优化是碰运气,预算表之外还有一本"否决账本"(被诊断否决的立项记录),防止同一个无谓优化被反复提出。
12.6 性能结论的诚实性
性能结论必须标注测量条件:负载、温度、编译选项、CPU 亲和性、是否预热。没有条件的数字不可比较(本项目同一引擎在负载下 5.6-6.2 tok/s、无负载 16 tok/s、端到端 2.30 tok/s,三个数字都是真的,条件不同)。发布任何性能声明前,先写清楚测量条件------这是性能工程的第一诚实性要求。
第十三章 正确性、数值与对拍验证:推理引擎工程的核心方法论
13.1 为什么对拍是核心
推理引擎的本质是"参考实现的数学等价重实现"。引擎在量化格式、指令集、布局、累加顺序上做大量等价变换,每一步都可能引入静默错误。LLM 的错误是不可见 的:中间张量错几个数量级,生成文本依然通顺(本项目排障中,40 层输出全错的模型"看起来完全正常")。人类无法通过阅读生成结果判断引擎是否正确------唯一可靠的途径是机器比对:让引擎与权威参考实现(llama.cpp 的 llama-server)吃同一份权重、同一输入、同一采样参数,逐层逐位比较中间张量与最终输出。对拍因此不是测试环节之一,而是推理引擎工程的核心方法论:没有对拍,引擎的正确性断言没有任何依据。
13.2 对拍的三个层次
| 层次 | 比较对象 | 判据 | 用途 |
|---|---|---|---|
| 位级 | GEMM/GEMV 中间张量 | 逐位一致(或机器 epsilon 内) | 内核实现的确定性验证(本项目 GPU 与 CPU 路径 GEMM 位级一致) |
| 容差级 | logits 与中间激活 | 最大误差在容差内(本项目 40 层全量 max err 5.1e-3~1.28e-1,全部在容差内) | 量化与数值路径的等价性验证 |
| 行为级 | 采样输出 | top-k 命中、生成序列一致 | 端到端行为验证(本项目测试 7 的 top-10 与 llama-server 参考匹配:11 4858 0 1017 13 660 7357 353 20798 1942) |
三个层次对应三类错误:位级对拍抓指令语义错误(dpbusd 的符号槽位、q6_K 的 +32 偏置);容差级对拍抓数值路径错误(反量化公式、累加顺序);行为级对拍抓采样与配置错误(top-k 边界、种子处理)。三层都必须通过,缺一不可。
13.3 对拍对象与条件
参考实现必须是独立维护的权威实现(llama.cpp 生态),而非"自己写的另一份"。对拍条件必须严格对齐:同一 GGUF 权重文件、同一输入序列、同一采样参数与随机种子、同一批量形状。任何一个条件不对齐,对拍失败都不能定位到引擎。本项目还维护了独立的 Python 参考(llamaGEMV_q8 语义),用于内核级逐位比较------Python 参考直接实现数学定义,不经过任何引擎代码,是引擎之外的第二独立实现。
13.4 逐层对拍:首差层定位法
错误在 40 层中传播放大,全模型对拍只能报"不一致",无法定位。逐层对拍(把第 i 层的输出 dump 出来与参考逐层比较)把定位缩小到首差层,偏差量级指示根因方向:
- 首差层 = 层 0、偏差 1e-9 量级:数值路径的微小差异(累加顺序、反量化舍入),通常可接受或对齐实现即可;
- 首差层 = 层 6 起偏差 8、层 10 起偏差 33:指令语义或量化公式错误(本项目 q6_K 的 +32 偏置漏减即此模式------q6_K 张量只有 4 个,但坏内核毁掉全部 40 层);
- 全层一致、仅 logits 偏差:LM head 或采样层问题。
关键纪律:诊断工具必须覆盖全部张量类型×形状组合。本项目曾只测 blk.0(恰好全是 q4_K/q5_K),q6_K 的错误完全漏检(假阴性),浪费 3 小时定位。覆盖率矩阵(类型 × 形状 × 批量)是诊断工具的验收标准,不是可选项。
13.5 数值问题的系统处理
浮点运算不满足结合律:累加顺序不同,结果不同。引擎与参考实现的差异因此分为"可接受的数值噪声"与"结构性错误"两类:
- FP32 累加:logits 与中间激活用 FP32 累加(即使权重是 Q4)------降低累加顺序敏感性;
- 输入量化:激活进入 GEMV 前量化(q8),其语义必须与参考完全一致(本项目曾因工具陈旧------用了旧版"精确 f32 反量化"语义------产生假回归,根因是参考工具没同步新语义,而非引擎出错);
- 容差定义:max err 容差(本项目 40 层全量 5.1e-3~1.28e-1)覆盖量化本身的误差传播,超出即判定错误;
- 确定性:同输入同参数必须同输出------随机采样与并行归约不得引入不确定性(种子与归约顺序固定)。
13.6 对拍作为开发流程
对拍不是发布前的检查,而是开发流程的一部分:
- 先写对拍,再写内核(测试驱动):先实现参考语义的最小对拍,再写内核------本项目 VNNI 内核的自测"期望 40 实测 2600",正是先定参考值再实现指令的产物,指令语义 bug 靠读文档发现不了,只有对拍能抓到;
- 全量回归门禁:本项目 85/85 测试 × 三编译器(clang/GCC/MSVC)全绿 + 测试 7 top-10 匹配 + GPU/批量无回归,是每次提交的前置条件------编译器间差异(未定义行为、优化强度)也由对拍覆盖;
- 工具语义版本管理:对拍工具本身会过时(参考语义演进后,旧工具产生假阴性/假回归),工具必须标注语义版本,陈旧的工具要退役或升级------多工具并存时,验证前先确认工具的语义版本;
- 决策反转同步文档:实现语义一旦反转(如输入量化从"精确 f32"改为"q8"),所有对拍工具与文档必须同步更新------本项目曾因决策反转未同步文档,导致工具与引擎各执一词。
#mermaid-svg-kB6GPbtvUxglcTok{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kB6GPbtvUxglcTok .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kB6GPbtvUxglcTok .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kB6GPbtvUxglcTok .error-icon{fill:#552222;}#mermaid-svg-kB6GPbtvUxglcTok .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kB6GPbtvUxglcTok .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kB6GPbtvUxglcTok .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kB6GPbtvUxglcTok .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kB6GPbtvUxglcTok .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kB6GPbtvUxglcTok .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kB6GPbtvUxglcTok .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kB6GPbtvUxglcTok .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kB6GPbtvUxglcTok .marker.cross{stroke:#333333;}#mermaid-svg-kB6GPbtvUxglcTok svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kB6GPbtvUxglcTok p{margin:0;}#mermaid-svg-kB6GPbtvUxglcTok .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kB6GPbtvUxglcTok .cluster-label text{fill:#333;}#mermaid-svg-kB6GPbtvUxglcTok .cluster-label span{color:#333;}#mermaid-svg-kB6GPbtvUxglcTok .cluster-label span p{background-color:transparent;}#mermaid-svg-kB6GPbtvUxglcTok .label text,#mermaid-svg-kB6GPbtvUxglcTok span{fill:#333;color:#333;}#mermaid-svg-kB6GPbtvUxglcTok .node rect,#mermaid-svg-kB6GPbtvUxglcTok .node circle,#mermaid-svg-kB6GPbtvUxglcTok .node ellipse,#mermaid-svg-kB6GPbtvUxglcTok .node polygon,#mermaid-svg-kB6GPbtvUxglcTok .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kB6GPbtvUxglcTok .rough-node .label text,#mermaid-svg-kB6GPbtvUxglcTok .node .label text,#mermaid-svg-kB6GPbtvUxglcTok .image-shape .label,#mermaid-svg-kB6GPbtvUxglcTok .icon-shape .label{text-anchor:middle;}#mermaid-svg-kB6GPbtvUxglcTok .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kB6GPbtvUxglcTok .rough-node .label,#mermaid-svg-kB6GPbtvUxglcTok .node .label,#mermaid-svg-kB6GPbtvUxglcTok .image-shape .label,#mermaid-svg-kB6GPbtvUxglcTok .icon-shape .label{text-align:center;}#mermaid-svg-kB6GPbtvUxglcTok .node.clickable{cursor:pointer;}#mermaid-svg-kB6GPbtvUxglcTok .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kB6GPbtvUxglcTok .arrowheadPath{fill:#333333;}#mermaid-svg-kB6GPbtvUxglcTok .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kB6GPbtvUxglcTok .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kB6GPbtvUxglcTok .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kB6GPbtvUxglcTok .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kB6GPbtvUxglcTok .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kB6GPbtvUxglcTok .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kB6GPbtvUxglcTok .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kB6GPbtvUxglcTok .cluster text{fill:#333;}#mermaid-svg-kB6GPbtvUxglcTok .cluster span{color:#333;}#mermaid-svg-kB6GPbtvUxglcTok div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kB6GPbtvUxglcTok .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kB6GPbtvUxglcTok rect.text{fill:none;stroke-width:0;}#mermaid-svg-kB6GPbtvUxglcTok .icon-shape,#mermaid-svg-kB6GPbtvUxglcTok .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kB6GPbtvUxglcTok .icon-shape p,#mermaid-svg-kB6GPbtvUxglcTok .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kB6GPbtvUxglcTok .icon-shape .label rect,#mermaid-svg-kB6GPbtvUxglcTok .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kB6GPbtvUxglcTok .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kB6GPbtvUxglcTok .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kB6GPbtvUxglcTok :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 位级一致
容差内
超容差
参考实现 (llama.cpp / Python 数学定义)
同权重/同输入/同参数
逐层对拍
首差层定位
行为级对拍 (top-k)
量化路径确认
根因修复 (指令语义/公式)
发布 (回归门禁 85/85 × 三编译器)
13.7 对拍之外的完整性
对拍保证"与参考一致",不保证"工程完整":加载失败路径、GPU 初始化失败、参数防御(本项目 GPU 路径失败时 CPU 兜底重算,零错误输出可能)、版本检查(cudaRuntimeGetVersion/cudaDriverGetVersion 不匹配时首次 GemmEx 挂起并触发 TDR 干死系统------版本检查把该风险移到初始化期)等,都必须有代码级证明或路径测试。对拍解决"算得对不对",路径测试解决"跑得完不完整",两者共同构成引擎的正确性。
第十四章 工程案例:主流引擎的架构选择与设计取舍
本章以三个代表性引擎为例,说明前十三章的原理在真实系统里如何落地、如何取舍。分析基于公开架构与文档,结论聚焦"设计决策与物理约束的对应关系",不做任何宣传性评价。
14.1 vLLM:服务吞吐优先
vLLM 的架构选择全部服务于"高并发吞吐"这一目标。其核心贡献是 PagedAttention(第六章):KV cache 分页使显存利用率大幅提升、序列进出批零拷贝、前缀共享(COW)与 prompt 缓存成为可能------这些能力直接支撑 continuous batching 的每步动态调度(第五章)。配套工程:CUDA Graphs 捕获整条前向链(7.5)、chunked prefill 与 PD 分离作为生产部署形态(5.4)、投机解码与 prefix caching 作为可选加速。vLLM 的取舍是明确的:为了吞吐,接受调度与分页的复杂度,并让内核(FlashAttention、量化)服从批量形状。
14.2 TensorRT-LLM:编译期优化优先
TensorRT-LLM 把优化放在编译期:引擎构建时做图融合(7.4)、kernel 自动调优(按目标 GPU 枚举 tile/线程配置)、量化方案选择(FP8/INT8/INT4),产出一个针对特定 GPU 序列化的 engine 文件。运行期调度相对简化(静态批形状、显式 KV cache 分配),换取的确定性收益:同一 engine 在同构 GPU 上的行为可预测。它适合"硬件固定、部署大规模、延迟敏感"的生产场景,代价是构建期长、跨硬件不可移植。其内核层(FlashAttention 系、fp8 量化、GPTQ/AWQ 支持)与 vLLM 大量同源------编译期与运行期只是优化时机的差异,物理约束(带宽账)相同。
14.3 llama.cpp:单机与异构优先
llama.cpp 的定位是"单机跑得动、显存小也跑得动"。其工程支柱:GGUF 量化格式(第八章,q4_K/q5_K/q6_K 的布局为 CPU 向量化与 VNNI/NEON 指令量身设计)、mmap 权重加载(10.5,页缓存按需换页)、按层卸载(10.3,ngl 参数)、CPU SIMD 内核与 GPU(cuBLAS)混合路径。它的调度器不面向高并发(服务能力有限),但把"低资源下的带宽账"做到了极致:Q4 权重 + VNNI + 层卸载,让 35B 级模型在 42.6 GB/s 的普通内存上跑出 16 token/s(第三章实测)。llama.cpp 与本文实测引擎(LLMX)同属这一谱系,其正确性同样依赖与参考实现的对拍(本项目 85/85 测试、top-10 匹配、40 层容差对拍均以 llama.cpp 为基准)。
14.4 对比
| 维度 | vLLM | TensorRT-LLM | llama.cpp |
|---|---|---|---|
| 首要目标 | 并发吞吐 | 编译期确定性性能 | 低资源可用性 |
| 调度 | continuous batching + 分页 KV | 静态批形状为主 | 简化调度 |
| 显存 | PagedAttention(核心贡献) | 显式块分配 | mmap + 按层卸载 |
| 量化 | FP8/INT8/INT4 运行时 | 编译期量化 + 工具链 | GGUF 生态(Q4_K 系) |
| 并行 | TP/PP/EP + PD 分离 | TP/PP/EP | 单机/少卡异构 |
| 内核 | FlashAttention/CUDA Graphs | 自动调优 + 图融合 | VNNI/NEON + cuBLAS |
| 适用 | 在线服务、高并发 | 固定硬件生产集群 | 个人/边缘/研究 |
14.5 共同主线
三个引擎的差异在优化时机与部署目标,共同主线是同一组物理约束:decode 的带宽墙(第三章)决定了所有人都做量化与批量;显存容量约束决定了所有人都做 KV 管理(分页/显式分配/mmap);层间串行依赖决定了所有人都做内核融合与启动削减。没有哪一个引擎绕开了带宽账------它们只是把同一本账记在不同的预算表上。工程上选择引擎 = 选择预算表:先明确自己的约束(硬件、并发、延迟目标、显存),再对照本文各章的物理公式估算,而不是跟随任何"最优引擎"的宣传。
第十五章 模型加载与运行时内存管理
15.1 加载路径:从文件到内存
GGUF 文件的结构是自描述的:文件头(魔数、版本号)、元数据区(键值对:架构、层数、头数、词表等)、张量元数据表(每个张量的名字、形状、类型、文件偏移)、张量数据区。加载器的工作是:解析元数据 → 校验与模型声明一致 → 按张量元数据把权重映射到内存。本项目实测:35B 级 Q4 模型(约 17.5 GB 权重)解析加映射耗时约 394 ms------解析本身是元数据量级的开销,大头是文件系统页缓存预热。
两种加载方式对应不同的运行时语义:
- 预读加载:把整个权重文件读入匿名内存。优点:所有权重物理落地、访问无缺页;缺点:加载耗时与文件大小线性(17.5 GB 从 NVMe 读入 DDR 在 3-7 GB/s 下是 2.5-6 秒);
- mmap 映射:把文件映射进地址空间,页缓存按需换页(10.5 节)。优点:加载近乎瞬时、冷权重不占内存、操作系统自动管理缓存;缺点:首次访问缺页延迟、换页抖动不可控。
推理引擎的取舍:权重是只读的、顺序访问的,mmap 的按需换页与顺序预读正好匹配 decode 的访问模式;KV cache 是读写的、必须物理驻留,用显式分配;激活是临时的、生命周期短,用 arena 复用。三种数据三种内存策略,这是运行时内存管理的第一原则。
15.2 内存池与分配策略
推理引擎几乎不用通用分配器(malloc)分配热路径内存,原因有二:碎片化(KV 块、激活区频繁申请释放)与分配开销(每步分配)。标准做法是三级内存池:
- 权重区:加载期一次性建立,只读,生命周期=引擎生命周期;
- KV 块池:固定大小块的自由链表(第六章分页),分配/释放是链表操作,无碎片;块池容量与并发上限绑定,超额时走换出而非分配失败;
- 激活区(arena):临时张量的线式分配------每步前向结束后整体复位,无需逐张量释放。双缓冲(两层交替)让 prefill 与 decode 的激活区可以重叠使用。
GPU 侧的 pin 内存(页锁定内存)用于异步拷贝的源/目标------普通页内存的拷贝需要内核态中转,pin 内存绕过中转直达 DMA(10.4 节)。
15.3 KV 块的内存布局
KV 块的布局直接影响注意力内核的读取效率。每块的字节数 = 每 token 每层 KV 字节 × 块内 token 数。两个布局决策:
- 按层连续 vs 按序列连续:KV 缓存按 (层, 块, token, 头, 维) 排列时,注意力内核顺序读同层 KV 更连续;按 (序列, 层) 排列时换出搬移更高效。二者不可兼得,选择取决于换出频率与读取频率的比值;
- 块内 token 方向:块内 token 连续存放,使"追加一个 token"只需要写块内一个槽位,不必搬移。
15.4 激活内存与临时张量
decode 单步的临时张量(Q/K/V 投影输出、注意力中间结果)在 FP16 下每张数千 KB,40 层全部保留不释放是显存的浪费;arena 的线式分配让这些张量在层边界被覆盖复用。工程上要避免的典型错误:把"安全"误写成"持有引用"------arena 复用意味着任何跨层持久的张量引用都是 bug,正确性由第十三章的对拍框架兜底(引用悬垂会表现为随机数值错误,这正是逐层对拍能抓住的类别)。
15.5 与带宽账的关系
内存管理本身不产生计算,但它决定字节的物理位置与访问路径:权重在 mmap 页缓存(缺页时走 NVMe)、KV 在显式块池(换出时走 PCIe)、激活在 arena(纯 HBM/DDR 内)------三者的带宽档位完全不同(第二章)。运行时内存管理的每一项决策,最终都折算进第三章的帧时间公式:把数据放错介质,就等于把 B 换小。
第十六章 端到端之旅:一次推理请求的完整生命周期
前十五章分别讲了各层原理,本章把一条真实请求从文本到 token 再到文本的全过程串起来,标注每一站的物理与工程约束。以本项目(LLMX,CPU/GPU 异构,Qwen3.5-35B-A3B,Q4)为实例。
16.1 文本 → token:分词器
用户输入的中文字符串先经分词器(tokenizer)切成 token 序列。现代模型使用 BPE/ByteBPE 类分词器,词表规模 15-25 万(本项目 248320)。分词器是纯查表/匹配算法,与模型计算无关,但有两个工程点:其一,中文输入在 Windows 等平台存在编码陷阱------命令行参数按 ANSI 代码页解码(GBK),直接当 UTF-8 使用会把中文输入变成乱码 token(本项目实测模型把乱码识别为 mojibake 并"正常"回答,从结果完全看不出问题,属于"正确跑、错输入"的隐蔽故障,修复方式是宽字符命令行解析后再转 UTF-8);其二,chat 模板(system/user/assistant 角色标记与特殊 token)必须与训练时一致,否则模型输出行为退化(本项目实测裸 prompt 无模板时 Qwen3.5 输出"1. 2. 3."式计数列表,套上模板后恢复为正常对话)。
16.2 引擎初始化
首次请求前完成:CPU 能力检测(AVX-512 VNNI 等指令集门控,决定走哪套内核);GPU 环境检查(CUDA 运行时/驱动版本匹配------本项目实测版本不匹配时首次 GemmEx 调用会挂起并触发 TDR 让系统黑屏,因此把版本检查放在初始化期并做预热调用,把风险前移);权重加载与量化格式校验;KV 块池初始化。
16.3 prefill:首 token 的诞生
输入 token 序列(chat 模板:system/user 角色标记 + 特殊 token)进入 prefill:按层前向,注意力扫全部输入前缀(FlashAttention 分块),每层把 K/V 写入 KV cache 块。prefill 完成后即得首个输出 token 的 logits,采样(第五章)产出第一个 token。TTFT 的构成即 5.6 节的四根因------实测中 prefill 占首 token 等待的主要部分。
16.4 decode 循环:每步的完整动作
每步(实测帧时间约 64 ms 量级、负载下 170 ms)依次执行:
- 采样上一步 logits,得新 token,追加到序列;
- 检查终止条件(EOS token、最大长度、停止词表);
- 位置编码(RoPE)更新;
- 逐层前向:每层先做 RMSNorm,再做注意力(本项目模型为混合架构:10 个全注意力层读本序列全部 KV 并写新 KV,30 个 Gated DeltaNet 层执行常数大小线性状态的更新,见 19.2 节),再做 MoE 前馈(路由器计算 → 路由到命中的专家 → 专家 GEMV(CPU 侧 VNNI 内核 + 预取,GPU 侧 cuBLAS)→ 加权合并),残差累加;
- LM head:把最后一层输出映射到词表 logits(V=248320 的 GEMV,字节约 250 MB Q4------单步权重流的一部分);
- 输出 token,进入下一步。
每步的字节流(2.74 GB 活跃权重 + KV 读写)与计算(约 6.1e12 FLOP 的 1/1024)之间的不匹配,正是第三章带宽墙的逐层重现。
16.5 流式输出与结束
生成的 token 随到随发(流式),由 HTTP/流式接口按 token 边界推送。结束条件四选一:生成 EOS、达到 max_tokens、命中停止词、请求取消。结束后的收尾:KV 块归还块池、序列从批中移出(第五章)、如果开启了 prompt 缓存则保留前缀 KV 供后续请求复用(6.3 节)。
16.6 端到端实测
本项目实测的一次完整请求:加载 394 ms;生成 60 token 耗时 26064 ms(2.30 token/s,含 chat 模板与思考流开销);单序列无负载上限 16 token/s。三个数字对应三个条件(负载、模板、并发),全部与第三章的带宽模型自洽------这就是"端到端"的意义:把每一站的数字对上同一本账,任何一站对不上账,就是该站的工程缺陷。
第十七章 MoE 推理工程专项
MoE(Mixture of Experts)是当前大模型推理负载的主要形态(本项目模型即 Qwen3.5-35B-A3B:35B 总参、约 3B 活跃,40 层 = 10 组 ×(3×DeltaNet+MoE → 1×Gated Attention+MoE),256 个专家、8 路由 + 1 共享),它同时带来带宽账的收益与随机访问的代价。本章把 MoE 的推理工程问题单独展开。
17.1 路由与专家选择
每个 token 前向到 MoE 层时,路由器(一个小型线性层加 softmax)计算该 token 到各专家的概率分布,取 top-k(本项目 k=8)个专家执行,输出按路由权重加权合并:
y = Σ(i∈topk) g_i · E_i(x)
路由计算本身是微小的(D×N_experts 的 GEMV),但它的两个后果主导 MoE 工程:其一,活跃权重流降为共享层 + k 个专家 (实测 2.74 GB/token 对全量约 17.5 GB,6.4 倍压缩)------这是第三章带宽账的直接收益;其二,命中的专家集合随输入随机变化------权重无法预驻留缓存,TLB 与延迟问题由此而来。
17.2 冷专家问题与预取治理
第三章 D7-D10 的实测:随机路由命中的"冷"专家 GEMV 单核 0.44 ms,而同一内核在 L3 驻留(热)下只要 0.03 ms------15 倍差距;8 并发流随机读取专家权重时有效带宽仅 2.6 GB/s(峰值 6%)。根因是页表与缓存的双重未命中:专家权重按路由随机分布在大内存地址空间,每次访问都可能是新的页。
治理手段按成本排序:
- 软件预取:内核在读取当前权重行的同时,按固定前距(本项目实测 96 KB 最优)预取下一行,让内存请求在计算期间在途------实测把有效带宽拉回 39.9 GB/s(11 倍)。预取距离是 TLB 覆盖与带宽争抢的平衡点(距离过短页表未建立,过长浪费带宽);
- 数据布局:把频繁共现的专家权重在文件/内存中按邻接布局,提高页局部性;
- 大页:2 MB 大页提高 TLB 覆盖------本项目实测目标平台不可用,属于平台约束;
- 批量化:同一批内多个 token 命中的专家集合合并,专家权重的访问以"批 × 专家"为单位组织,把随机访问摊成半顺序访问。
17.3 共享专家与路由专家的不同工程处理
MoE 层常含少数共享专家(每 token 必读)与大量路由专家(随机命中)。两类专家在工程上应区别对待:
- 共享专家:访问是确定性的、顺序的,适合并行化------本项目实测共享专家 512 行切 4 任务并行,28→14.3 ms;并行度不是越高越好(16 任务实测更差,任务队列与缓存争抢),存在最优任务数;
- 路由专家:访问随机,按 17.2 治理;GPU 部署时路由专家是卸载的首选对象(第十章:访问频率低 → 放慢介质损失小)。
17.4 批量下的专家交织
批大小为 M 时,M 个序列各命中不同的专家集合,专家的执行集合是各序列命中的并集(可能多达数十个专家)。这带来两个问题:其一,批量形状不规则 ------各专家的 token 数不一,padded 批的浪费大(5.2 节);其二,权重读取量随批的专家并集增长------MoE 的批量带宽账不是简单除 M(第三章公式的 P 在 MoE 下随批变化)。工程上通过专家级调度(按专家聚合 token、分组执行)控制并集规模,或在路由层面做专家负载均衡的训练约束(训练期问题,推理期只能接受并集)。
17.5 与卸载、并行的接口
MoE 的随机性决定它在异构部署中的位置:专家权重是"低频、大块、随机"的数据,是 CPU 内存/SSD 层级的首选驻留对象(10.2 节);多卡场景下专家按卡分配(EP,11.4 节)把随机访问摊到多卡内存,all-to-all 通信与路由发散成正比。MoE 工程的每一层(内核、调度、显存、并行)都受同一个因素驱动:专家权重的随机访问模式。这是区别于稠密模型的最本质工程差异。
第十八章 GGUF 与量化格式的二进制布局
第八章给了量化的数学与算法,本章落到字节:GGUF 文件里一个量化张量到底怎么摆,布局如何为指令集服务。所有细节来自本项目对 llama.cpp 生态的逐字节实现与验证。
18.1 文件结构
GGUF 文件自描述:
- 文件头:魔数("GGUF")、版本号、tensor 数量、metadata KV 数量;
- 元数据区:键值对(架构名、层数、头数、词表大小、上下文长度等),键为字符串、值为有类型的标量或数组;
- 张量元数据表:每个张量一条记录:名字(如 blk.0.attn_q.weight)、形状(dim 列表)、类型(GGML_TYPE_Q4_K 等)、数据偏移;
- 张量数据区:按元数据表的偏移顺序排列的实际字节。
加载器按张量名字解析语义(blk.N.xxx 对应第 N 层),形状决定矩阵维度,类型决定反量化路径。张量命名规范是引擎与模型格式之间的契约:命名不一致(或类型标注错误)会在加载期被元数据校验拦截------这也是为什么对拍必须在"同一文件"上进行。
18.2 Q4_K 的二进制布局
Q4_K 把 256 个权重组织成一个超级块(super-block),共 144 字节(llama.cpp 权威布局):
- 块头:d(fp16,块级缩放)与 dmin(fp16,块级最小偏移)各 2 字节;
- qs128:256 个 4 位量化值,每字节存两个(低 4 位、高 4 位);
- scales12:8 组 × 32 元素,每组编码一个 6 位缩放码 sc 与一个 6 位最小码 m;
- 反量化:x = (d·sc)·q − dmin·m(q 为 4 位值)。
点积形式(llama vec_dot_q4_K_q8_K 语义,本项目按此实现并对拍验证):d = d_in×d_w,dmin = −d_in×dmin_w,组结果 = (Σ qs_w·qs_in)×d×sc_g − dmin×m_g×(Σ qs_in_组)------min 项是组级标量,可与主项分离,在累加链末端一次性完成。Q4_K 的关键语义:没有 −8 偏置(Q4_0 的对称化偏置只存在于非 K 格式)------这是本项目踩过的真实坑:给 Q4_K 加 −8 校正后 logits 偏差 5160 级,top-10 完全不匹配。
18.3 Q5_K 与 Q6_K
- Q5_K:在 Q4_K 的基础上每权重增加 1 位(存储在高 4 位/低 4 位之外的单独字节区),反量化同样无偏置;
- Q6_K :每权重 6 位(ql128 存低 4 位、qh64 存高 2 位),16 权重一组(16 个有符号 8 位 scale),无 dmin;存储值域 0...63,实际值 = 存储值 − 32 (自带 +32 偏置)。这个偏置与硬件指令语义直接相关:AVX-512 VNNI 的 dpbusd 指令要求 src1 槽位为无符号字节,把存储值(无符号 0...63)直接送入后,累加结果包含一个 +32 × Σ q8 的偏移(q8 为对应 8 位参考值),必须在累加链末端一次性减去(实测漏减导致层 6 起偏差 8、层 10 起偏差 33)。Q6_K 张量在模型里数量很少(本项目 40 层中仅 4 个),但错误会毁掉全部层------覆盖所有类型×形状组合的诊断是唯一防漏手段(13.4 节)。
18.4 激活量化与 q8_0
decode 的 GEMV 为利用 VNNI/INT8 路径,把激活(隐藏状态)量化为 8 位(q8_0:每组 32 个值共享一个 scale)。输入量化的语义必须与参考实现完全一致:本项目曾因对拍工具停留在"精确 f32 反量化"的旧语义而产生假回归------工具语义版本与引擎语义版本必须同步(13.6 节)。
18.5 布局对内核的要求
量化格式的布局不是数据格式问题,它直接决定内核怎么写:
- 对齐:块大小设计为 128-bit/512-bit 向量宽的整数倍,保证一次加载取满一个块;
- 防御性检查:批量与列数必须满足布局的倍数约束(本项目输入量化要求长度是 256 与 32 的倍数,调用链与内核双重防御);
- 端到端一致性:布局、指令语义、反量化公式三者任一改动,都要回到对拍框架重新验证(13.6 节),因为三者的耦合错误在输出上是不可见的。
GGUF 的布局设计是"数学定义服从指令集"的典型实例:同样的 Q4 精度,不同布局的 SIMD 效率可以相差数倍------这解释了为什么推理引擎的实现必须精确到字节,而不是"数学上等价"即可。
第十九章 注意力机制深层剖析:MHA/GQA、RoPE 与长上下文
注意力是 Transformer 的计算核心,也是 KV cache 与 FlashAttention 等一切工程结构的数学源头。本章从数学出发,把推理视角下注意力的每个环节拆开。
19.1 注意力数学
给定 query、key、value(head 维度 d_head),单头注意力:
Attention(Q,K,V) = softmax(Q·K^T / √d_head) · V
softmax 前的缩放 √d_head 防止点积随维度增长而饱和(softmax 进入饱和区后梯度消失------这是训练视角的动机,推理视角它固定了数值范围)。三个投影矩阵分别把隐藏状态映射到 Q/K/V 空间,KV 按序列位置累积成 KV cache,Q 每步只有当前 token。
19.2 MHA、GQA、MQA:KV 头数的三个档位
多头注意力(MHA)每头独立的 K/V;**MQA(多查询)**全部头共享一份 K/V(KV 头数=1);**GQA(分组查询)**在两者之间折中:每 g 个 Q 头共享一份 K/V(KV 头数 = n_heads/g)。三者的数学区别只在 K/V 的共享粒度,推理视角的区别在 KV cache 的字节:
C_KV ∝ n_kv_heads
本项目模型的全注意力层为 GQA(16 Q 头、2 KV 头,g=8,头维 256),KV cache 比 MHA 小 8 倍;且 40 层中仅 10 层为全注意力层,其余 30 层为 Gated DeltaNet 线性注意力(每层为常数大小的线性状态,不随上下文增长)。这对带宽账的影响在 3.3 节与 6.6 节已经量化:KV 每步全量读取,KV 头数直接乘进 decode 帧时间。GQA 是"精度与带宽"的折中产物------现代大模型普遍采用,工程上需要在架构说明里显式记录 n_kv_heads,它是 KV 容量与注意力带宽的一切计算的输入。
19.3 RoPE:旋转位置编码
位置信息通过 RoPE(旋转位置编码)注入:对 Q、K 按位置施加旋转矩阵(把 (x1,x2) 旋转角度 θ,θ 随频率递减),使点积 q·k 依赖相对位置。推理视角的三个事实:
- 每步都要旋转:新 token 的 Q、K 在进入注意力前施加该位置的旋转------固定的逐元素计算,与带宽账无关(小字节),但必须在内核链里;
- 旋转与 KV 缓存共存:K 旋转后存入缓存,后续注意力直接使用已旋转的 K------位置信息随缓存持久化,无需重算;
- 外推与插值:训练时位置上限有限,推理超长时位置超出训练范围(外推退化),工程手段包括位置插值(缩放位置索引)与 NTK 类频率修改------这些修改改变模型行为,需作为推理配置文档化并对拍验证(第十三章)。
19.4 因果掩码与 KV cache 的协同
解码器注意力的因果性(token 只能注意自己及之前的位置)在推理中由两个机制协同:prefill 用掩码矩阵屏蔽未来位置(FlashAttention 分块时把未来的块跳过,块内用对角掩码);decode 只有新 token 的 Q,天然只与已缓存的历史 KV 匹配------decode 的 KV cache 结构本身就实现了因果性,无需掩码。这个协同是 KV cache 为什么存在(而非重算)的又一数学依据。
19.5 长上下文:注意力带宽的工程对策
上下文变长时,注意力的 KV 读取字节线性增长(3.3 节),最终超过权重读取成为 decode 的主要带宽消费者。工程对策按"是否修改模型行为"分两类:
- 无损类:KV cache 量化(FP8/INT8,字节减半/减四分之三)、FlashAttention/FlashDecoding 的 IO 优化(7.3 节,减少中间张量)、分页布局的读取局部性(6.3 节);
- 有损类:H2O、StreamingLLM、token 回收(6.5 节)------改变注意力计算的语义,必须按应用质量评估,不能视为无损优化。
判断标准始终是带宽账:长上下文场景下,先算"每步 KV 读字节"与"每步权重读字节"的比值(本项目模型:注意力 KV 每 token 约 20 KB(10 层 × 2 头 × 256 维 × 2(K、V)× 2B),上下文 1024 时每步 KV 读取约 20 MB、约为权重流 2.74 GB 的 0.7%,上下文 8192 时约 160 MB、约 6%;DeltaNet 层状态为常数大小,不随上下文增长),比值到达什么量级,再决定投入哪一类对策。注意力机制的每一层工程决策(头数、位置编码、掩码协同、缓存管理)都落在同一个框架里:数学定义决定计算结构,带宽账决定工程形态。
第二十章 推理引擎的稳定性与错误处理
正确性(第十三章对拍)保证"算得对",稳定性保证"一直算得对"。推理引擎是长寿命进程(服务端常驻数天),负载动态、资源受限、硬件可能出错,稳定性工程与正确性工程是两套体系。
20.1 失败域划分
引擎的失败按影响域分类:
- 可恢复错误:单请求超时、显存分配失败、换出失败------影响当前请求,不影响引擎;
- 可降级错误:GPU 初始化失败、特定内核不可用(指令集不支持)------降级到替代路径(本项目 GPU 路径失败即 CPU 兜底重算,设计目标"零错误输出可能":任何上传/初始化/执行/参数防御失败都走 CPU 路径);
- 不可恢复错误:权重文件损坏、元数据与模型声明不一致、内存越界------引擎应快速失败(fail-fast)并给出可诊断信息,而不是继续产出垃圾输出。
每个错误路径都要有代码级证明或路径测试(13.7 节),不能靠"应该不会发生"。
20.2 参数防御与前置校验
推理引擎的输入是外部可控的(请求文本、采样参数、批量配置),防御原则是在边界校验,在内核信任:参数合法化(范围、对齐倍数------本项目输入量化要求长度是 256 与 32 的倍数,调用链与内核双重防御)、资源预算前置(并发上限、KV 容量、最大序列长度在调度器入口检查,而不是分配时失败)、版本检查前置(CUDA 运行时与驱动版本不匹配会在首次 GemmEx 调用时挂起并触发 TDR------把版本检查放到初始化期,风险前移)。
20.3 资源上限与隔离
无界资源是长寿命服务的头号杀手:无界 KV 增长(长上下文请求)、无界队列(突发流量)、无界临时内存。工程手段:显式并发上限(与 KV 块池容量绑定)、请求级超时与最大 token 数、队列水位线(超过即拒绝或排队)、每请求独立上下文(失败不污染其他请求)。批量调度器(第五章)的水位线机制在这里复用------显存预算本身就是并发上限的物理表达。
20.4 日志与可诊断性
引擎故障的可诊断性由日志决定。原则:正常路径零日志,异常路径全信息 (本项目把性能计时收敛到环境变量门控,主程序输出从 375 行降到 17 行);错误日志包含现场(请求 ID、参数、阶段、字节数),便于复现;工具链与引擎共享同一套诊断语言(类型×形状覆盖矩阵,13.4 节)。另一个隐蔽问题:控制台编码------Windows 下程序输出 UTF-8 字节而控制台按 GBK 解码会产生乱码(本项目实测"日志输出看不懂"的根因即代码页不匹配,而非程序错误),这类环境问题必须在文档中说明查看方式,避免与程序 bug 混淆。
20.5 确定性
同输入同配置必须同输出(13.5 节第 4 条):随机采样用显式种子、并行归约顺序固定、时间不进入决策路径(不依赖 wall-clock 做分支)。确定性是"错误可复现"的前提------没有确定性,对拍与故障排查都无从谈起。工程上把确定性作为正确性基础设施,任何破坏确定性的改动(并行化、缓存复用、arena 覆盖)都要通过对拍回归门禁。
20.6 稳定性与性能的关系
稳定性不是性能的敌人,而是性能结论的前提(12.3 节):负载波动、资源竞争、隐藏的失败路径都会污染测量。一个稳定的引擎才有可重复的性能数据,可重复的数据才是工程决策的依据。反过来说,性能优化(并行、预取、批量)引入的确定性风险(13.5 节)必须由对拍框架兜底------两者互相约束,而不是互相取舍。
第二十一章 推理与训练的系统性差异
推理引擎常被拿来与训练框架(PyTorch、Megatron 等)比较,但两者面对的是不同的物理与工程问题。本章把差异列成可操作的清单------照搬训练框架的架构做推理,是推理引擎工程最常见的失败起点。
21.1 计算形状差异
训练与推理(prefill)都是大批量 GEMM,但推理的 decode 是 GEMV(第四章)------训练里不存在的形状。训练框架的算子与内核全部按大批量 GEMM 优化,对 GEMV 的带宽受限特性没有专门处理;推理引擎必须为同一数学运算维护 GEMV 专用路径(量化直算、预取、向量化,7.2 节)。
21.2 状态差异:静态权重与动态 KV
训练中权重每步更新、状态在优化器与梯度中;推理中权重只读、KV cache 每步增长。三个推论:其一,推理可以 mmap 权重(15.1 节)、共享副本、并行加载(11.1 节 DP 的权重副本零同步)------训练做不到;其二,推理的显存大头是 KV cache(6.1 节),训练是激活与梯度------显存管理对象完全不同(分页、换出、量化都是 KV 特有);其三,推理无反向传播------FlashAttention 在推理只需前向路径,反向相关的重算/存储设计(训练的反向需要保存中间状态)不适用。
21.3 调度差异
训练是预知的工作负载(固定形状、固定步数、无实时要求),调度简化为静态并行;推理是未知到达率、混合形状、延迟敏感的工作负载,需要 continuous batching、优先级、抢占、换出(第五章)。训练框架的静态数据并行(DP/TP/PP 的静态切分)在推理中演化为动态批处理与负载自适应。
21.4 数值差异
训练使用混合精度(FP16/BF16 前向 + FP32 主权重)且误差在梯度更新中被统计平均稀释;推理的量化(第八章)是部署期的单次变换,误差逐层累积且直接进入输出(8.2 节实测的 1e-9 → 8 → 33 传播路径)。因此推理对数值正确性的验证强度远高于训练:训练看 loss 曲线,推理必须逐层对拍(第十三章)。
21.5 正确性验证差异
训练框架的正确性由训练过程本身部分兜底(loss 下降说明大致没错);推理引擎的输出是用户直接消费的产品,中间张量错误不可见(13.1 节),必须与参考实现逐层机器比对。这就是为什么对拍是推理引擎特有的核心方法论------训练框架没有"对拍"这个环节(模型本身是新训练的,没有参考实现)。
21.6 结论:推理引擎是独立领域
推理引擎不是"训练框架去掉反向传播",而是面对不同物理约束(带宽墙、显存容量、延迟 SLO)的独立系统:内核形态(GEMV)、显存对象(KV)、调度模式(动态批)、数值策略(部署期量化)、验证方法(对拍)全部与训练框架不同。理解这一差异,才能解释为什么 vLLM、TensorRT-LLM、llama.cpp 都从零构建而非复用训练栈,也才能避免用训练框架的思维解决推理问题。
第二十二章 卸载实测案例:从配置到验证
本章把第十章的原则落到一个具体案例:LLMX 引擎在 Qwen3.5-35B-A3B(Q4)上的 GPU 卸载实施全过程,包括配置选择、实测数据与踩坑记录。
22.1 配置空间
卸载配置的自由度:卸载层数(ngl)、FP16 权重开关(GPU 侧权重是否存 FP16)、KV 驻留策略。本项目实测的推荐配置为:LLMX_USE_GPU=1、LLMX_GPU_LAYERS=41、LLMX_GPU_FP16=1(显存占用 1.4 GB,全投影上 GPU)。该配置的选择依据是带宽账(10.6 节):投影(attention 与共享专家的线性层)是每 token 必读的共享权重,放 GPU 的边际收益最高;路由专家(随机访问、低频)留 CPU 内存并靠预取治理(17.2 节)。
22.2 实测过程与数据
- 上传成本:全量权重反量化上传实测 22.4 GB(限 4 核反量化),耗时随系统负载大幅波动------一次性成本,但必须计入启动预算(10.5 节:传输量化格式可把该成本除以 4);
- 逐级验证:采用分步验证法------最小配置(版本+预热)→ 边界上传(LAYERS=1)→ GEMM 位级对拍 → FP16 引擎 → 多轮对话 → 长生成 → 批量 → chat 模板 → 端到端。每步验证通过才进下一步,把"卡死 vs 慢"的歧义隔离在每一步;
- 层数语义:ngl 的 off-by-one(10.3 节)在此配置中验证:41 层卸载对应起始层 = 40 + 1 − 41 = 0,即全部 40 层在 GPU,语义自洽;
- 性能对比:GPU 投影预算约 15 ms/token 对 CPU 37 ms/token(D12/D13)------CPU 侧按第三章带宽账已逼近 42.6 GB/s 的带宽极限(D1-D4 吻合<3%),GPU 侧 15 ms 对应有效带宽约 100 GB/s(约为 HBM 3.35 TB/s 的 3%),受 1×N GEMV 形状与启动开销限制;带宽档位(约 78 倍)给出的是上限空间,实测约 2.5 倍差距反映两侧实现状态;
- 失败路径:版本不匹配(cudaRuntimeGetVersion/cudaDriverGetVersion)时首次 GemmEx 挂起并触发 TDR 的案例,验证了版本检查前移与预热调用(1×1×1 GEMM)的必要性------预热把懒加载风险移到初始化期。
22.3 卸载的收益边界
该案例的收益边界值得如实记录:GPU 卸载把 decode 从 37 ms/token(CPU 投影)压到 15 ms/token(GPU 投影)量级,但专家部分的收益被冷访问抵消------随机路由的专家若也上 GPU,其 TLB 问题只是换了个平台,且 KV 与投影挤占显存。卸载的最优解不是"全部上 GPU",而是"按访问频率分介质"(10.2 节)。此外,GPU 合并 GEMM 的 ROI 诊断(D14:仅省 0.1 ms/token)说明:卸载场景下启动开销不是瓶颈,字节搬运才是------这一结论与第三章的带宽账完全一致。
22.4 案例的方法论意义
该案例演示了卸载工程的完整闭环:容量账(显存余量)→ 带宽账(每介质带宽档位)→ 配置决策(按访问频率驻留)→ 分步验证(每步对拍)→ 收益核算(带宽模型核对)→ 否决账本(ROI 诊断)。任何一步缺失,卸载配置就退化为拍脑袋:要么显存不足(容量账没算)、要么收益不符(带宽账没算)、要么数值错乱(验证没做)。卸载不是特性,是工程------它与本文全部章节共用同一套账本与同一套验证纪律。
第二十三章 学习笔记
23.1 公式速查
| 主题 | 公式 | 含义 |
|---|---|---|
| 算术强度 | I = FLOPs/Bytes | 每字节的运算量,决定瓶颈归属 |
| decode 帧时间 | t_d ≥ P·W/B | 帧时间下限 = 活跃权重字节 ÷ 内存带宽 |
| 批量帧时间 | td,M ≥ P·W/(M·B) | 批大小 M 摊薄权重读取 |
| prefill 算术强度 | I_p ≈ 2S/W | 随前缀长度线性增长(计算密集) |
| KV cache 容量 | C_KV = 2·L·n_kv·d_head·2·T | 例:40 层/128 头 FP16 时每 token 2.5 MB |
| TTFT | t_排队 + 2·P·S/C_eff + t_首步 | 排队 + prefill 计算 + 首步 |
| 温度采样 | p_i ∝ exp(z_i/T) | T 缩放 logits 后 softmax |
| 投机解码 | t_spec ≈ P·W/B + t_draft | 验证一次前向,加速比 = 平均接受长度 |
| 层卸载语义 | start = max_layer + 1 − ngl | off-by-one 陷阱点 |
| TP 通信量 | C_TP = 8D 字节/层 | 两次 all-reduce |
23.2 实测数据速查(本文全部实证,LLMX / Qwen3.5-35B-A3B / Q4)
| 编号 | 数据点 | 值 | 对应结论 |
|---|---|---|---|
| D1 | 内存带宽 | 42.6 GB/s | 带宽模型输入 |
| D2 | 活跃权重流 | 2.74 GB/token | MoE 活跃集 |
| D3 | 带宽理论上限 | 15.6 tok/s | 模型预测 |
| D4 | 单序列实测 | 16 tok/s | 与 D3 吻合 ❤️% |
| D5 | 40 层帧时间 | 170 ms(负载 82%+) | 负载退化路径 |
| D6 | 端到端 | 2.30 tok/s(60 token/26 s) | 全链路真实值 |
| D7/D8 | 冷/热专家 GEMV | 0.44 vs 0.03 ms | TLB/延迟主导(15 倍) |
| D9/D10 | 随机读/预取后 | 2.6 vs 39.9 GB/s | 预取 11 倍恢复 |
| D11 | 高负载批量 | 0.68× | 延迟受限反证 |
| D12/D13 | GPU/CPU 投影 | 15 vs 37 ms/token | 卸载的带宽来源 |
| D14 | 合并 GEMM | 省 0.1 ms/token | 启动非瓶颈 |
23.3 核心结论清单
- decode 的性能由带宽决定,不由算力决定;帧时间 = 活跃权重字节 ÷ 有效带宽;
- 有效带宽 ≠ 名义带宽:TLB、预取、负载决定两者差距(6%→94%);
- 一切优化的本质:减少字节(量化/投机/批处理)或提高带宽(卸载/预取/并行);
- MoE 把带宽受限部分转为延迟受限:随机访问需要预取与 TLB 治理;
- TTFT 的根因是前缀依赖 × 2PS 计算量 × 设备算力,加上排队与缓存命中;
- 采样参数不参与权重计算,开销可忽略,但顺序与数值(FP32)决定行为;
- 显存管理(分页/换出/卸载)与带宽账是同一本账;
- 对拍(与参考实现逐层比较)是引擎正确性的唯一可靠判据,三层级缺一不可;
- 微基准结论在并发下可反转,引擎级验证是性能结论的唯一判据;
- 所有性能声明必须附测量条件,没有条件的数字不可比较。
23.4 自测题
- 为什么 decode 是带宽受限而 prefill 是计算受限?用算术强度推导。
- 批大小 M 与量化位宽 W 对帧时间的贡献如何叠加?
- 为什么 MoE 的专家权重需要软件预取?冷/热差距的实测是多少倍?
- KV cache 分页解决了哪三个问题?COW 在什么场景触发?
- 为什么 top-k 在前、top-p 在后?换序会改变什么?
- 投机解码为什么在批处理下收益下降?
- ngl 卸载层数的 off-by-one 陷阱是什么?
- 对拍的三个层次分别抓什么错误?首差层定位法如何用偏差量级判断根因?
- 微基准与引擎级验证的结论为什么可能反转?举例。
- 传输卸载权重时为什么必须保持量化格式?
这些题目全部可以在前二十二章中找到推导或实测依据;不能从本文推导出的答案,不要从宣传材料中找。
23.5 术语表
| 术语 | 定义 | 章节 |
|---|---|---|
| prefill | 处理输入前缀的前向,计算密集 | 四 |
| decode | 逐 token 生成的前向,带宽密集 | 三、四 |
| TTFT | 首 token 延迟:排队 + prefill + 首步 | 五 |
| ITL/TPOT | 相邻输出 token 的间隔 | 五 |
| 算术强度 | FLOPs/Bytes,判断瓶颈归属 | 二 |
| 脊点 | Roofline 两条线的交点 | 二 |
| GEMV | 向量乘矩阵,decode 主力算子 | 四 |
| continuous batching | 每解码步重组批成员的调度 | 五 |
| chunked prefill | prefill 切块与 decode 混批 | 五 |
| PD 分离 | prefill 与 decode 分实例部署 | 五 |
| KV cache | 缓存的键值张量,容量与带宽双账 | 六 |
| PagedAttention | KV 分页,逻辑块映射物理块 | 六 |
| COW | 写时复制,前缀共享时的写隔离 | 六 |
| TLB | 页表缓存,随机访问的有效带宽瓶颈 | 二、三 |
| FlashAttention | IO 感知注意力,片上分块 + online softmax | 七 |
| online softmax | 分块 softmax 的运行统计量修正 | 七 |
| CUDA Graphs | kernel 链捕获,消除启动开销 | 七 |
| Q4_K/Q5_K/Q6_K | GGUF 组量化格式 | 八、十八 |
| dpbusd | AVX-512 VNNI 乘加指令,src1 无符号 | 八 |
| GQA/MQA | KV 头共享的注意力变体 | 十九 |
| RoPE | 旋转位置编码 | 十九 |
| 投机解码 | draft 验证并行,减少目标前向次数 | 九 |
| 卸载 | 数据按访问频率放置到不同介质 | 十 |
| ngl | 按层卸载层数,起始层 = 最大层+1−ngl | 十 |
| TP/PP/DP/EP | 张量/流水线/数据/专家并行 | 十一 |
| 对拍 | 与参考实现逐层机器比对 | 十三 |
| 首差层 | 逐层对拍中第一个不一致的层 | 十三 |
| 有效带宽 | 实测字节/时间,区别于名义带宽 | 三、十二 |