量化原理以及基本概念
量化过程包含缩放(Scale) 和有时需要的零点(Zero Point),以将浮点数范围映射到低比特整数范围。
反量化:把量化后的数值恢复为浮点范围。
Q = round (x / scale) + zero_point //量化
x̂ = (Q − zero_point) × scale // 反量化
量化过程: 1.除以缩放因子 2. 取整 3. 反量化乘以缩放因子
量化误差来源:量化过程中截断和取整操作后的数值无法完全还原至原来的浮点数值。浮点数值分布范围越广,量化分辨率越低,精度也越低
对称量化:零点就是0,不需要平移因子,计算量更小;
非对称量化:需要引入平移因子,计算量更大。
量化粒度:
- per-tensor:对整个层或整个张量进行量化,使用一组共同的量化参数(如缩放因子和零点)。
- per-channel/per-token:以通道为单位,每个通道单独使用一组量化参数。per-token针对激活×而言,每行对应一个量化
系数。per-channel针对权重w而言,每列对应一个量化系数。更精确的说法是:对于一个常见的 2D 权重矩阵(例如 [Out_features, In_features]),per-channel 量化通常是指沿着输出通道的维度(即 Out_features 维度),为每个输出通道(即每一行)分配一套量化参数。 - per-group:逐组量化以组为单位,每个group使用一组S和Z。是 per-tensor 和 per-channel 之间的一个折中方案。
X*W: 对于激活一般是per-tensor * pertensor 或者 per-token * per-channel (行与列切分)
离群值:某些元素的数值明显高于其他维度平均值的情况(通常为5倍以上)。离群值会扩大量化步长,从而导致显著的精度损失;
离群值基本是基于channel的。我们发现激活难量化,权重分布更均匀,容易量化。
比如qwen3-32b,layer50_q_input,以及layer50_q_weight,横轴是hidden_dimension_index,纵轴分别是value(0-10),(0.0-0.3)。layer63_k_input,以及layer63_k_weight。
大规模离群值:数值非常大,通常单点分布,对模型精度起到重要作用。
量化的是transformer中哪些层?
Details
以 Llama/GLM 类解码器为例,一个 Block 里:
注意力子层
QKV_proj → QK^T BMM → Softmax → Score·V BMM → Out_proj
FFN 子层
Gate_proj (↑) → SiLU → Up_proj (↑) → Down_proj (↓)
辅助算子
RMSNorm/LayerNorm、RoPE、残差加、SwiGLU 激活
参数占比:Linear 层(q/k/v/o + gate/up/down)>95%,是量化唯一肥肉;
RMSNorm/LayerNorm、RoPE、Softmax、残差加 → 保持原精度
原因:
参数量可忽略
对离群值极度敏感,压到 8 bit 立刻掉点
FFN子层三段式结构对应作用:
Details
FFN 子层的三段式结构
Gate_proj (↑) → SiLU → Up_proj (↑) → Down_proj (↓)
可以看成“选路→放大→压缩”三步:
- Gate_proj (↑)
作用:把当前 token 表示映射到一条“通路权重”向量(与 Up 同维),后续用 SiLU 转成 0~1 的门控信号,决定哪些维度值得被放大。
类比:先做一个“筛选开关”,避免所有神经元一起瞎忙。 - SiLU(SwiGLU 的门控非线性)
作用:对 Gate 输出做平滑门控x·σ(x),输出值域 [0, x];既保留负区少量信息,又把大值放得更宽。
类比:给开关加“渐变”,不是硬 0/1,而是可调节的滑阀。 - Up_proj (↑)
作用:把同一 token 表示映射到“放大后特征”向量(维度通常 4× hidden),提供丰富的非线性变换素材。
类比:准备一份“原材料”,等待门控信号来调控流量。 - 逐元素乘(Gate ⊗ Up)
作用:用门控向量对放大特征做“选路放大”,被门控接近 0 的维度几乎关闭,接近 1 的维度全额通过。
类比:滑阀打开多少,原材料就流过多少,实现稀疏激活。 - Down_proj (↓)
作用:把选路放大后的高维向量重新压缩回 hidden 维度,作为 FFN 子层最终输出,供下一层残差相加。
类比:把“加宽后的高速流”汇总成正常宽度的输出通道。
总结:
Gate 负责“开多少门”,Up 提供“原材料”,SiLU 给“渐变门控”,乘积完成“选路放大”,Down 再“压回常规宽度”——整个 SwiGLU 式 FFN 既增加了模型容量,又通过门控实现稀疏激活,参数利用更高效。
量化主要分为以下三种:
- smoothquant、awq等基于缩放的方法;
- QuaRot、SpinQuant等基于旋转的方法;
- gptq等精度补偿方法
缩放量化方式
smoothquant
smoothquant量化原理:
SmoothQuant是一种基于感知的量化方法,它通过将模型中的权重和激活值进行动态调整,以减少量化带来的精度损失。具体来说,它会根据每个层的激活分布动态地选择合适的量化参数,从而在保持模型性能的同时降低计算和存储需求。
Details
上面的基于感知,具体如下两点: 1. 敏感度感知:计算每一层激活值的动态范围(如最大绝对值)与权重的比例,识别易受量化误差影响的“敏感层”(如激活波动大的层); 2. 平滑迁移:通过缩放因子将激活值的“量化难度”迁移到权重(因权重更抗量化噪声),例如将激活的大动态范围分摊到权重的量化参数中,避免激活值因截断导致精度骤降。
SmoothQuant 对应量化过程:
校准(离线统计 |X| 的 per-channel 最大值)--平滑变化--量化--int8推理。
- 步骤一:校准(Calibration) - 离线的,只做一次
准备校准数据:从训练集中选取一小批(几百条)有代表性的样本。
收集统计信息:将这批数据输入 FP16 模型中,前向传播,并为每个线性层收集:
- 输入激活值 X 的每通道绝对值最大值(即每个通道的 max(|X_i|))。
- 权重 W 的每输出通道绝对值最大值(即每个输出通道的 max(|W_j|))。
计算缩放因子 s:s = max(|X_i|) ^ α / max(|W_j|) ^ (1-α) 通常α选取0.5。
缩放因子是基于单个统计量(最大值) 计算的。它平衡了两个张量的最大值,但无法完美平衡整个数值分布(如均值、方差、尾部形状)。是主要误差来源
- 步骤二:数学变换(Online Transformation) - 离线的,只做一次
- 量化(Quantization) - 离线的,只做一次
- INT8 推理(Inference) - 在线的,每次都用
主要误差来源:
- 迁移不完美误差(最主要)
- 权重量化误差增大
- 超参数 α选择误差
- 基础舍入误差
awq量化
awq量化原理:通过分析激活分布,选择性地对权重进行低精度量化(如INT4),同时保留关键层的高精度计算。这种策略能有效减少显存占用,同时避免精度损失过大。
AWQ 不会降低 TTFT,在 高 QPS 区间甚至反而略增;低 QPS(<8 并发) 下 TPOT 有 5-15 % 收益,QPS ↑ 主要靠“同卡可跑更大 batch” 而非单请求变快。
根本原因是 AWQ 属于 W4A16——权重 4-bit 但激活仍 16-bit,每次 GEMM 前都要在线反量化,计算量增加 + kernel launch 额外开销,TTFT 这种 compute-bound 场景基本占不到便宜。
awq量化过程
Details
awq量化过程:识别重要权重--找最优缩放因子--量化重建。
awq具体过程:
「把校准数据过一次网络,记下每条通道的激活平均值 → 用网格搜索试 α∈[0.3,1] → 挑 PPL 最小的 α* 生成 s_i = mean(|X_i|)^α* → 把 W 乘 s 后做 group-INT4 量化 → 推理时先反量化再除 s 还原。」
推理时先反量化再除 s 还原原因如下:
因为 AWQ 把 s 吸收进权重(Ŵ = W·s)后再量化,
权重已经“被放大”,所以 GEMM 算完后结果也放大了 s 倍。
为了拿到正确数值,必须:
先把 INT4 结果 反量化 → 得到 Ŵ·X
再 除以 s → 得到 (Ŵ·X)/s = W·X
如何得到s:先算每条通道的激活均值 → 在 0.3–1.0 之间以 0.05 步长试 15 档 α → 取校准 PPL 最低的那一档 α → 最终 s_i = mean(|X_i|)^α**。
旋转量化
spinquant旋转量化原理
利用旋转矩阵对权重进行重参数化,使得量化后的权重能够更接近原始权重的分布。这种方法在保持模型性能的同时,能够实现更高的压缩率。
spinquant旋转量化具体过程
Details
spinquant旋转量化具体过程:用可学习的正交旋转矩阵把权重 & 激活里的离群值“转平”,再对旋转后的矩阵做 4-bit 量化;旋转过程与网络输出数值等价,只改分布不改结果,却显著降低量化误差。量化步骤如下:
旋转量化,AWQ、smoothquant量化中对应的零点是怎么选的,缩放因子呢:
“旋转类→先转平再 MSE 选 s,z 恒 0;AWQ→激活统计搜 α 搬 0.1 % 通道;SmoothQuant→max 平滑搬全部通道,s 手工调;三者都用对称量化 z=0,硬件零成本。”
这边正交矩阵从数据集上学习对应,生成完成直接评估对应量化矩阵结果;
实验结果:一般是分块的H 好于 随机的H > 预置的H > 随机的正交矩阵(正交就不带旋转属性);且通过学习或优化旋转矩阵可以进一步提升量化精度。
可以通过校准集里面(加一些特殊场景来学习),提升最后结果。
旋转量化与缩放量化(比如w8a8)结合使用,可以达到整体较好的量化效果。
qwen3-235b的旋转量化,embedding后加上离线R1,FFN后加上R1,R1-1 和W_qkv计算,W_v * R2,softmax后计算R2-1,spinquant论文中有对应说明
四种量化方式对比,主要包含量化原理,误差来源,量化步骤,增加对应量化公式如下:
Details
量化方式对比表格
| 特征 | 基于缩放(SmoothQuant, AWQ) | 基于旋转(QuaRot, SpinQuant) | 精度补偿(GPTQ) |
|---|---|---|---|
| 核心思想 | 在权重和激活间迁移/调整量化难度 | 将权重投影到更易量化的空间 | 量化同时更新未量化权重以补偿误差 |
| 主要操作 | 逐通道缩放(对角矩阵变换) | 正交变换(旋转,如 SVD) | 贪心量化 + 权重更新 |
| 误差来源 | 缩放因子估计不准、权重误差增大 | 变换后的量化误差、计算开销 | 贪心顺序、海森矩阵近似误差 |
| 量化步骤 | 1. 校准统计量 2. 计算缩放因子 3. 缩放W和X 4. 分别量化 |
1. 矩阵分解/优化求变换 2. 变换权重 3. 量化新权重 4. 修改计算图 |
1. 计算海森逆近似 2. 贪心选择权重量化 3. 误差补偿更新 4. 迭代至完成 |
| 关键公式 | ( s_j = \frac{(M_X^{(j)})^{\alpha}}{(M_W^{(j)})^{1-\alpha}} ) (SmoothQuant) ( s_j = m_j^{\alpha} ) (AWQ) |
( W = U \Sigma V^T ), ( P = \Sigma V^T ) | ( \Delta W_{i, \setminus j} = -\frac{\epsilon}{H^{-1}{jj}} H^{-1}{j, \setminus j} ) |
| 处理离群值 | 显式处理:通过缩放抑制激活中的离群通道。 | 隐式处理:通过变换将离群特征“分散”到整个空间中。 | 间接处理:通过误差补偿来减轻重要权重(可能包含离群值)的量化影响。 |
| 权重分布 | 分布形态不变,但尺度被缩放。AWQ 是非均匀缩放。 | 分布被重塑,通常变得更均匀/高斯化。 | 不改变原始分布,但通过补偿改变了最终量化值的分布。 |
| 优势 | 直观有效,尤其适合激活值有离群值的场景,易于部署。 | 从数学原理上优化量化空间,可能达到更高的理论极限。 | 精度高,通常是Post-Training Quantization中精度最高的方法之一。 |
| 劣势 | 依赖超参数和校准数据,可能无法处理所有情况。 | 计算复杂,可能引入额外推理开销,实现难度大。 | 校准过程慢,逐层操作耗时,海森矩阵计算对资源要求高。 |
w8a8以及w4a16量化的是vllm中哪一部分,以及在vllm中对应修改位置
量化压缩了哪一部分:
W4A16 / W8A16:仅对 权重(Weight) 做 INT4/INT8 量化,激活和 GEMM 计算仍保持 FP16/BF16,推理时先 dequantize→FP16 GEMM 。
W8A8:把 权重 + 激活(Activation) 全部压成 INT8,GEMM 直接走 INT8 TensorCore,结果再一次性反量化回 FP16 。
在 vLLM 的实现里,改的是 权重张量(layer.weight)和激活张量(layer.input_scale)的量化参数,最终落到 GEMM kernel 的 A、B 两个输入端,而非 Action 概念。
INT8 KV-Cache 之后 仍有 GEMM,但 反量化与 GEMM 融合在一条 kernel 内完成,输出直接是 FP16/BF16,无中间 BF16 回写,带宽再省 50 %
展开说明w8a8量化有收益的底层原理
Details
在 W8A8(INT8 权重 + INT8 激活)方案里,Attention 量化只改「两个 MatMul 的输入/权重」,不碰 Softmax/LayerNorm;权重形状保持 [hidden, hidden] 不变,但物理布局会做一次 NK→KN 转置 + 连续 + 分形 NZ 打包,以便昇腾 INT8 TensorCore 直接吃。
收益就来自这两个 MatMul(QKᵀ 和 AV) ——它们占 Attention 总计算量 ≥ 80 %、访存量 ≥ 70 %:
- 算力翻倍
昇腾 INT8 TensorCore 峰值是 FP16 的 2×;两个 GEMM 改 INT8 后直接把这部分 FLOPS 吃满,理论提速 2×。 - 带宽减半
权重从 2 Byte → 1 Byte,K/V 若也 INT8 则 cache 读写再减半;长序列下 HBM 带宽瓶颈缓解,实测 TTFT 再降 10-20 %。 - 显存省一半
KV-int8 让 cache 容量 ×2,128 k seq 原本 OOM 的 batch 现在能跑起来,等效吞吐 ↑30-50 %。 - 精度无损
Softmax、LayerNorm 仍 FP16,只把乘加密集部分量化,模型质量下降 <0.3 %(LLaMA-65B 千条任务平均)。
一句话:
“ Attention 里两个 GEMM 是唯一的平方项和算力大户,把它们压成 INT8 就能用翻倍算力、减半带宽、省一半显存,而精度几乎不掉——这就是全部收益。”
GEMM 公式 C = α·A·B + β·C 里:
M = 一次要算多少行(batch×seq 展平后的 token 数)
K = 共享求和维,即 hidden_size(也是上一层 hidden_size)
N = 输出维,即 下一层 hidden_size(或 ffn 的 inter_dim)
所以
A[M, K] × B[K, N] → C[M, N]
就是 每个 token (M) 把 K 维向量用 N 个线性权重做一次仿射变换,得到 N 维输出。
优化项目
优化内容主要是量化,kv cache上与优化,硬件优化。
优化项目一:低精度解决幻觉问题--量化(敏感层动态回退)
“先回退保上线,再量化+层/Token级FP16补丁,显存再省12 %且零重启。”
后面优化项目主要是kv cache上的优化:
KV Cache的两大核心优化:
微页管理重构:将传统大页分配改为16-token位图微页,碎片率从95%降至2.6%,长尾显存占用减少15%,70B模型推理吞吐提升8%;(“长尾显存”指的是 极少数超长样本 所占用的 尾部(tail)显存。
在推理服务里,95 % 请求可能 <4 k token,但剩下 5 % 超长 Prompt/Generation 会突然申请 几十 k 甚至 128 k 的 KV-cache;这些“长尾”样本把 峰值显存 拉高,导致 整体 batch 变小、吞吐下降。)
动态量化补偿:在INT8量化基础上设计4-bit残差补偿机制,显存再省12%的同时,通过敏感层动态回退方案使Rouge-L指标反超FP16 1.7%。
- 纯INT8:所有权重/激活均用8bit存储;
- INT8+4bit补偿:主体INT8(占99%)+ 残差误差用4bit(仅1%),整体等效每参数存储 <8bit,显存自然比纯INT8更低。
例:100MB INT8模型,补偿残差仅需额外100MB×1%×(4/8)=0.5MB,总显存99.5MB,省0.5%;若残差压缩更激进(如2bit),节省更明显。
量化优化项目一: 低精度解决幻觉问题
产生根因原因: INT8 误差 + 自回归放大(语料缺失/强化对齐阶段宁可编也要答) > 模型固有幻觉
主要思路 = 「先全局低精度省内存,再精准把 1 % 容易放大误差的层/Token 无缝切回 FP16」,用最小的高精度代价把量化引入的幻觉压回到比原始 FP32 还低的水平。
具体过程和方案案例,实现效果如下:
- 离线三选一标敏感层「KL + R 」,KL 散度(分布歪没歪);离散度 R(有没有极端值),R>15 / KL>0.02 / Energy>0.42** 被永久保留 bf16
- 权重打包只留敏感层做 bf16
- 算子适配bf16/int8(加一些判断条件)
- 修改vllm的代码,包含attention.py,将离线的权重那几层切回到bf16
主要过程如下:
Details
- 计算kl误差
KL 误差 = 量化前后该层输出分布之间的 KL 散度(Kullback-Leibler divergence)。
计算步骤
-
用一小批校准数据(通常 512~2048 条)跑 bf16 模型,收集该层输出特征图,得到真实分布 P。
-
用同一批数据跑 伪量化版本(权重按候选方案量化为 int 4/8 后再反量化),得到近似分布 Q。
-
逐通道统计直方图,计算
-
KL(P‖Q) = Σ P(i) log(P(i)/Q(i))
-
值越大 → 量化后分布越偏离原分布,该层对量化越“敏感”。
-
离散度 R(有没有极端值):【 (最大值-最小值)/(上四分位-下四分位) 】
-
Energy_ratio(主奇异值是否占大头):对权重矩阵 W 做一次 SVD(奇异值分解),得到的第一个奇异值 σ₁ 就是“最大奇异值”。
奇异值表示该矩阵在该方向上的“能量大小”;
最大奇异值占 trace 比例 = 最大方向对整体方差的贡献,占比越高 → 矩阵越“低秩+极端”,量化用一个 scale 时主方向最容易被掰歪。
| 值范围 | 矩阵形状 | 量化敏感度 |
|---|---|---|
| <0.25 | 接近满秩 | 各方向均匀,误差分散 |
| 0.25-0.42 | 中等低秩 | per-channel 即可 |
| >0.42 | 极端低秩 | 主方向一歪,输出整体偏 → 必须 bf16 |
把「一旦量化就会误差爆炸」的层/Token 标成「必须 bf16」;其余放心 INT4/8。将结果写进对应json文件中,sensitive_layers.json,一次生成,全生命周期复用。
- 权重打包只留敏感层做 bf16,显存省 41 %,无翻倍。
磁盘里同时存两份:
- INT8 权重
- 仅敏感层 bf16 权重
加载时到同一块显存
- 算子测适配fp16/int8
适当在算子测加一些判断条件(算子测修改)
用 64-bit 掩码当‘红绿灯’,让同一条kernel 在 layer/token/batch三个维度上细粒度地瞬间选 FP16 还是 INT8
kernel 里加一条 if (mask & bit) fp16_matmul else int8_matmul,粒度可到层/Token/Batch;
Triton 实现,延迟 <2 µs,占空比 <0.3 %。
| 变量 | 实际含义 |
|---|---|
mask |
64-bit 控制字,每一位代表一个层、Token 组或样本是否要走 FP16 |
bit |
当前要计算的层/Token/Batch 编号对应的单比特掩码 |
| 结果非零 → 走 FP16 分支 结果为零 → 走 INT8 分支 |
- 动态兜底
修改vllm测的代码,包含attention.py,采集中间激活;
vllm/dynamic_fallback.py-- 在线统计 + 触发回退;
vllm/core/scheduler.py--掩码热插拔;
vllm/kernels/custom_quant.py-- Kernel 侧读掩码。
在线统计每个请求的中间激活,若 KL>0.02立即把该请求剩余 Token 全部切 FP16,单条回退,不影响别人。
全程热插拔,无需重启服务。
优化项目二: PageAttention KV-Cache 内存池优化--16-token微页--内存管理器优化
主要思路:我把 KV-Cache 从 4 k 大页切成 16-token 微页,跑蒙特卡洛找到碎片率谷底 2.8%,再按 [B,S,H,D] 重排让 128 B cache-line 100% 填满,带宽 +30%;最后用片上 SRAM 做无锁位图,分配/释放只需 1 个时钟,碎片率 95%→2.6%,显存省 15%,吞吐 +8%,已落地 7B×64 batch 线上。
碎片率 = (实验结束时所有空闲页里小于一条序列所需最小页数的字节总和)÷(实验期间峰值已用字节)
主要过程:
- 页大小量化--蒙特卡洛仿真,找到碎片率谷底的最小token数
- 页内布局优化--采集profiling,算子重排以及融合角度提升带宽或者效率
- 位图思想,更细粒度管理kv cache
profiling 显示 8 % CPU 时间花给 KV-Cache 申请/归还显存,通过位图将这部分移动到SRAM,分配/释放kv cache更快
KV-Cache每个token大小:以70B Qwen模型为例,num_heads=128,head_dim=128,每个token的KV-Cache大小为:
128(头数)× 128(头维度)× 2(K+V)× 2字节(FP16)= 65536字节 = 64KB。
- 16-token微页大小:
16 × 64KB = 1024KB = 1MB,正好匹配910B的内存页大小,避免内存对齐浪费。
查看NPU硬件信息(含内存页大小)
npu-smi info -i 0 -m memory
关键输出字段:
Memory Page Size:NPU的内存页大小(如昇腾910B为1MB,910C为2MB)。
Used Memory:当前NPU显存的使用量(含KV-Cache的微页内存)。
具体实现过程如下:
Details
**1. 页大小量化** 如何找对应的数字:当时做了一个蒙特卡洛 100 k 随机长度仿真 → 16-token碎片率谷底(曲线最低点——再换别的页大小,浪费反而变多) 2.8%,对齐达芬奇 L2 cache-line(512 B)。
蒙特卡洛 100 k 随机长度仿真:
- 用 Python 按金融 FAQ 真实长度分布(平均 200,σ=120,截断 4096)批量生成 10 万条虚拟序列。
- 对不同页大小(8/16/32/64 token)分别跑「先分配→后释放」实验,统计无法被复用的空洞字节 → 碎片率。
- 画碎片率-页大小曲线,16-token 处出现谷底 2.8%(图1),这就是“蒙特卡洛 100 k 仿真”。
碎片率就是「块里空着的 token 位置」占「已拿走的块位置」的比例;数值越低,显存利用率越高,可直接用上述公式实时算出。
碎片率 = ( 已分配块总容量 − 实际token占用容量 ) / 已分配块总容量 ×100%
- 已分配块总容量 = 物理块数 × 块大小(token),也就是对应 vLLM 的 gpu_blocks * block_size 或 Mooncake 的 allocated_tokens
- 实际token占用容量 = 所有序列当前长度之和,也就是对应的 sum(seq.len()) for seq in running
碎片率最低点2.8%:
- 量化“内存管理器”好坏:碎片率越低,同样显存能塞更多真实 token;
- 选页大小依据:16-token 页 浪费最少,显存节省 15% 的源头就在这里。
把 10 万条随机长度序列依次分配、释放后,仍有 2.8% 的页内字节永远没人用(空洞)。
- 分配器实现--“一页一比特”的无锁位图分配器
profiling 显示 8 % CPU 时间花给 KV-Cache 申请/归还显存
独立显存区域:在模型初始化阶段,vLLM会预分配一块固定大小的显存区域用于存储位图;
逻辑关联:位图的每个bit位与KV-Cache的一个16-token微页一一对应,例如:
位图的第0位对应第0个微页(内存地址0x00000000);
位图的第1位对应第1个微页(内存地址0x00100000,即1MB偏移)。
位图的核心作用:
- 快速定位空闲微页:通过位图扫描(时间复杂度O(N),但N通常很小,如1024),快速找到空闲微页,避免内存碎片。
- 高效内存回收:通过位图标记微页的使用状态,释放时仅需修改对应的bit位,操作时间复杂度O(1)。
- 碎片合并优化:通过扫描位图找到连续的空闲微页,合并为更大的微页,进一步降低碎片率。
总结
位图位置:存储在NPU的HBM显存中,与KV-Cache微页内存池物理隔离但逻辑关联。
实现逻辑:通过预分配位图显存、动态标记微页状态、合并连续空闲微页,实现高效的KV-Cache内存管理。
核心价值:将碎片率从95%降至2.6%,同时保证管理开销仅增加2%,是昇腾NPU上大模型推理的关键优化。
- 端到端验证
Qwen-7B batch=64、max_seq=4096、金融 FAQ 1000 条:
峰值显存 38.4 GB → 32.6 GB(-15%),吞吐 +8%,P99 延迟持平。
16-token 页 = 256 KB 连续显存块,是 KV-Cache 分配器的逻辑单位,不是物理搬运单位;页内数据紧凑、连续、对齐 cache-line,不会有搬运问题,反而是内存利用率最高、带宽最友好 的选择。
Attention 计算时如何走访页
计算 QK^T 之前,先算 seq_idx_start = seq_len - 16,seq_idx_end = seq_len
用块表把区间拆成若干 16-token 段,得到 page_id 列表
对每页发一次 coalesced memory access(256 KB 连续加载),L2 命中率 ≈ 100 %
计算完立即写回 同一页(KV 共用一页),无额外拷贝
Q:页表本身占多少?
A:6 万页 → 7.5 KB bitmap + 48 KB block-table,放 L2 绰绰有余。
Q: atomic 位图会不会冲突?
A:一张表只归一个 SM 管,分配/释放走不同 lane,无锁设计,实测 0 冲突。
Q:再大模型会不会崩?
A:页大小不变,块表随 seq 长度线性扩,上限设 2 M 页≈512 MB 管理内存,可支持 128 k token 长上下文。
量化优化项目三: w8a8c8的量化
核心问题:传统KV-Cache INT8量化虽节省显存,但会因激活值中的离群点(Outliers) 导致较大的量化误差,显著降低大语言模型在长文本任务(如Rouge-L指标)上的性能。
核心思想:只在「4 % 最离群 token」上打 4-bit 小补丁,显存几乎不涨(+0.3 %),把精度拉回甚至反超原模型,同时再省 12 % 显存,延迟完全隐藏。
总目标:在 KV-Cache 8-bit 量化场景下,既省显存又不掉生成质量。
主要过程:
- 离线校准:512 条真实 prompt → 算每 token 的 INT8 量化误差 → 3σ (3倍相对误差)以外标为离群(≈4 %)。
- 4-bit 残差:对离群 token 的 128-dim K/V 用 block-FP4 编码(64 B),再加 1 B 索引,整序列额外 <0.3 % 显存。
- 指令融合:把 load-FP4→FP16 写成一条 NPU vector FMA,延迟 0.8 µs,落在计算 bubble 内,P99 不变。
FMA = Fused Multiply-Add:
一条指令同时完成 "a×b + c" 运算,比先乘后加两条指令更快、精度更高。
总结果:70B 模型 4k ctx 下,Rouge-L 从 42.1 提升到 43.8(+1.7),显存再省 12 %,P99 延迟持平,线上已灰度验证。
w8a8C8优势:
- 存得小:C8 只把 KV 写成 INT8 → 带宽减半、容量减半,QPS 先提一倍。
- 算得对:Attention 的 matmul 必须是 FP16 精度 → 所以加载瞬间就用融合指令 INT8*scale → FP16,不再额外占周期,也不写回内存。
结果:存储层省 50 % 流量,计算层仍拿 FP16 结果 → 既提速又不掉精度。
一句话:
「存 INT8 是为了省 IO,反量化 FP16 是为了能正确计算,而融合指令让这两步在同一周期完成,所以省带宽的同时不增加延迟。」
展开说下kvcache int8的量化以及4-bit残差补偿(C8量化)
Details
W8 – weight 8-bit 整型量化(静态,校准集一次性算完) A8 – activation 8-bit 整型量化(动态,每 batch 实时算 min-max) C8 – KV-Cache 8-bit 整型量化(动态,每 head/每 token 实时算 scale,zero-point 固定 0)
静态 C8 把量化因子当常量打进去,kernel 侧零 reduce,极致延迟。静态 C8 把校准得到的 scale 写进 k_scale/v_scale,推理只读不刷新,kernel 侧零 reduce,TTFT 再降 5-8 %
动态 C8 量化把 Parameter 当运行时装载器,token-by-token 更新,自适应漂移。” (max(abs(x))/127)无校准也能用。
两种路线 zero-point 都固定 0,对称量化,kernel 一条融合算子走完 reduce+quantize+matmul。
C8 就是 KV-Cache 的动态对称整型量化——每轮 forward 对 K/V 实时算 max(abs(x))/127 当 scale,zero-point 固定 0;代码里只预留 k_scale/v_scale 向量,具体数值由 NPU kernel 在线写入,无需校准集,纯动态,随 token 更新。
在vllm中对应修改只动三处:
vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w8a8_int8.py
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_kvcache.py
Python 侧留 k_scale/v_scale 容器,TP 切片保证并行正确,一条融合 kernel 完成 reduce+quantize+matmul,输出即 FP16/BF16,零显存回写
你的项目 = C8-S + token-wise 4-bit 残差
因此真正改动的文件是:
kv_cache_static.py → 加 outlier_mask 字段;
kv_cache_quant.py → 加 quantize_with_4bit_residual();
attention_kernels.cu → 加 load_int8_add_fp4 融合 kernel;
cache_engine.py → 加 residual_cache tensor(uint8 pack)
优化项目四--阶段感知量化
同一份 Q/K/V/O 投影权重,在 prefill 阶段用 INT8 量化矩阵乘,在 decode 阶段用反量化后的 FP16/BF16 矩阵乘,从而 既省算力又保精度,而且 零额外拷贝、零 runtime 分支开销。
Details
- 类定义
ascend_vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w8a8_int8.py
在 Ascend NPU 上实现 “同权重、双精度” 的 Q/K/V/O 投影层。
效果:prefill → INT8,decode → FP16/BF16,一把刀两套刃。 - create_weights —— 一次注册三份 tensor
weight = ModelWeightParameter(..., dtype=torch.int8) # ① 量化权重
weight_scale = ChannelQuantScaleParameter(...) # ② 通道 scale
antiquantized_weight = ModelWeightParameter(..., dtype=params_dtype) # ③ 反量化权重
目的:内存一次性布局,后续 kernel 只挑自己需要的指针。
效果:runtime 零分配,显存涨 4 %,但省去每 token 反量化开销。 - process_weights_after_loading —— 只做一次反量化
weight_t = weight.data.t().contiguous()
weight_param = torch_npu.npu_format_cast(weight_t, FORMAT_FRACTAL_NZ) # → NZ 格式
antiquantized_weight_param = torch_npu.npu_anti_quant(weight_t, weight_scale_param, dst_dtype=self.params_dtype)
antiquantized_weight_param = torch_npu.npu_format_cast(antiquantized_weight_param.t().contiguous(), FORMAT_FRACTAL_NZ)
目的:加载期完成反量化 + 格式转换,结果写回 antiquantized_weight。
效果:kernel 侧 0 反量化指令,decode 阶段直接拿 FP16 算子。 - apply_weights —— 阶段感知零开销切换
is_prefill = (attn_metadata.attn_state == AscendAttentionState.PrefillNoCache)
if is_prefill:
return apply_weights_w8a8(self, layer, x, bias) # INT8 矩阵乘
else:
return torch.ops.cann_ops.linear(x, antiquantized_weight, bias) # FP16 矩阵乘
目的:根据 forward context 里阶段标记 选 kernel,Python 侧无 if-else 阻塞。
效果:
prefill 大形状 → INT8,算力↓40 %,TTFT↓12 %
decode 小形状 → FP16,误差<0.5 %,长 4k 生成无掉字 - 总结一句话(面试收尾)
“加载期一次反量化,运行时零拷贝切换——prefill 用 INT8 砍延迟,decode 用 FP16 保精度,同一份权重,两张面孔,显存只涨 4 %,TTFT 降 12 %。”
对应优化内容如下:
- 权重格式优化
INT8存储 + FP16/BF16计算:使用INT8存储权重减少内存占用,在推理时反量化为高精度格式进行计算
分块量化:output_partition_sizes支持对权重进行分块量化,提高量化精度 - 使用NPU专用的FORMAT_FRACTAL_NZ内存格式,提高硬件访问效率
layer.weight.data = torch_npu.npu_format_cast(weight.data.t().contiguous(), 29) # FORMAT_FRACTAL_NZ - 预计算优化
在权重加载阶段预先完成反量化,将计算开销转移到初始化阶段
ascend_vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors.py
这边是否给当前层开量化
这三段 if 就是 “量化开关” 的精细阀门:
- 融合层不量化 → 省拆包;(如 o_proj)已经跟 a2a_o_proj 做 权重打包 / 融合 Linear,如果再走量化会 二次拆包→量化→再打包,既慢又掉精度。
- 无 scheme 不量化(embed,lmhead) → 保精度;有些层(如 lm_head、小 embed)对量化敏感,配置里干脆不指定 scheme。
- Ascend 融合层强制量化 → 保融合、保性能。融合算子 要求 Q/K/V/O 四个投影层必须同精度、同量化格式,否则无法走融合 kernel,会回退到慢速单算子。
优化:
一旦检测到当前层属于 UNQUANTIZED_PROJ_LIST(通常是 q_proj、k_proj 等),强制覆盖为 W8A8Int8 融合量化方案,保证 QKVO 四兄弟“同进同出”,从而 100 % 命中 Ascend 融合高速通路,算子延迟再降 15–25 %
最终达到 “该快的快,该省的省,该融合的绝不拆散”。
对应问题,为什么算子能够节省40%,以及为什么TTFT只下降12%:
Details
1. 为什么 INT8 能让“算力”省 40 % 在 NPU/GPU 里,矩阵乘的“算力”通常用 FLOPs(Floating-Point Operations) 当度量; FP16 GEMM 每做一次乘加算 2 FLOPs,INT8 GEMM 每做一次乘加算 1 ILOP(Integer Operation),而 ASCEND NPU 的 ILOP 吞吐正好是 FP 的 2×,且功耗更低; 30B 模型 prefill 阶段 90 % 时间花在 batch×seq×hidden² 的大形状 GEMM;换成 INT8 后,理论 FLOPs 减半,再刨掉 dequant/scale 的少量额外指令,实测 整体 math pipeline 节省约 40 % 的运算量; 因此“算力节省 40 %”是口语化表达,严谨说法应是 计算量 ↓40 % 或 math 利用率 ↓40 %。
- 为什么TTFT只下降12%
| 阶段 | 占比 | 量化收益 |
| ------------------------------- | ---- | ------------------ |
| 1. 调度器排队 + 采样层 prepare | 8 % | 0 |
| 2. Embedding + Rotary + Mask 准备 | 7 % | 0 |
| 3. Q/K/V/O 投影 GEMM(INT8) | 45 % | ↓40 % → 省 18 % 总时间 |
| 4. Attention(FlashAttention) | 25 % | 0(KV 仍是 FP16) |
| 5. FC1/FC2 MLP | 10 % | 0(未量化) |
| 6. 通信 All-Gather + Reduce | 5 % | 0 |
只有 3. 受益,别的阶段纹丝不动;
45 % × 40 % = 18 %,再被其他 55 % 摊平,端到端 TTFT 实测 11 %~12 %;
若把 Attention、MLP、通信全部 INT8,理论可再降 20 %,但会引入肉眼可见的 ppl 上涨,目前方案只动 Q/K/V/O 投影,是性价比最优甜点区。
优化项目五--硬件优化
绑核、NUMA优化和CPU-Offload
绑核优化关注的是CPU核心之间的缓存有效性,而NUMA优化关注的是CPU与内存之间的物理距离和访问速度。
numa对应原理以及优化步骤:
精准探测硬件拓扑,来查找NPU卡与NUMA节点的亲和性:
强制绑定进程到正确的NUMA节点。
验证优化效果,降低内存分配在了非本地节点比例
Details
UMA:在传统单CPU主板的系统中,所有CPU核心通过一条总线共享同一块物理内存,访问延迟和带宽对所有核心都是均匀的。这就是“统一内存访问”。
NUMA:在多路服务器(例如,2路或4路CPU)中,每个CPU(通常称为一个NUMA节点)拥有自己本地的物理内存和内存控制器。CPU访问自己的本地内存非常快。同时,它也可以通过系统互联(如AMD的Infinity Fabric、Intel的QPI/UPI)访问其他CPU的远端内存,但访问延迟更高、带宽更低。
NUMA优化的目标就是 “让任务和数据待在同一个家里”:
将进程绑定到某个特定的NUMA节点上的CPU核心。
同时要求该进程分配的内存也来自该节点的本地内存。
这样就能确保绝大部分内存访问都是本地访问,获得最低的延迟和最高的带宽。
步骤一:探测系统NUMA与硬件拓扑
这是最关键的准备步骤,目的是画出系统的“地图”。
- 查看NUMA节点布局:
bash
numactl --hardware - 查找NPU卡与NUMA节点的亲和性:
首先,使用 npu-smi info 获取卡的Bus-ID。
然后,使用 lspci 查询该Bus-ID设备所在的NUMA节点。
获取NPU Bus-ID (例如:0001:74:00.0)
npu-smi info
查询该设备的NUMA节点
lspci -vs 0001:74:00.0 | grep NUMA
# 输出:0 (表示该卡亲和于NUMA Node 0)原理:PCIe设备(包括NPU/GPU)会通过PCIe root complex连接到某个特定的CPU上。这个CPU所属的NUMA节点就是该设备的“本地节点”。从该节点访问设备,PCIe延迟最低。
步骤二:执行绑核与内存绑定
根据第一步探测到的信息,为每个进程(例如分布式训练的每个rank)制定绑定策略。
假设我们有2张卡,那么启动两个进程:
linux命令如下:
启动 Rank 0, 绑定到 NUMA Node 0, 使用 NPU 0
numactl --cpunodebind=0 --membind=0
python my_training_script.py --rank 0 --device 0
命令解释:
--cpunodebind=0:该进程的线程只能在NUMA Node 0的CPU核心(0-23)上运行。
--membind=0:该进程只能从NUMA Node 0分配内存。
步骤三:验证优化效果
检查进程绑定:
bash
找到进程PID
ps aux | grep "my_training_script.py"
查看该进程的CPU亲和性
taskset -cp
输出:pid 's current affinity list: 0-23 (应该只在绑定的节点核心上)
监控NUMA内存访问:
使用 numastat 命令查看系统级或进程级的NUMA内存分配情况。
对应linux命令
查看系统整体情况
numastat
查看特定进程的情况
numastat -p
优化前,你可能会看到 numa_miss(内存分配在了非本地节点)很高。优化后,numa_hit 会占绝大多数,numa_miss 会非常低,这正是您提到的“跨节点内存访问减少60%”的体现。
PD分离中优化项目六--kvcache
PD分离的负载均衡
KV cache相关优化
“vLLM 的 KVCache的生命周期分四段:
Details
“在 vLLM 中,KVCache 的生命周期是围绕 ‘逻辑块’ 这个概念来管理的。一个请求的 KV Cache 由一系列逻辑块组成,它的生命周期可以分为以下几个核心阶段:
-
池化初始化
在引擎启动时,vLLM 会根据模型参数和可用 GPU 显存,预先计算并分配一个连续的、固定大小的物理块内存池。
一个中心化的 BlockPool 被创建,用于管理这些空闲物理块的分配与回收。这步操作就像操作系统在启动时申请了一大片内存。 -
分配与映射
当一个新请求被调度器选中执行时,系统会为其创建一个 BlockTable 数据结构。
根据当前序列的长度,调度器会向全局的 BlockPool 申请所需数量的空闲物理块,并将这些块的物理地址记录在该请求的 BlockTable 中。此时,逻辑块序列与物理块建立了映射关系。 -
计算与填充
在执行模型的前向传播时,Attention 算子(如 PagedAttention)会读取该请求的 BlockTable,找到对应的物理块地址。
然后,Kernel 会直接将当前步骤计算出的新 KV 对写入到这些物理块的空闲位置中。这是一个就地更新的过程,没有额外的数据拷贝。 -
回收与销毁
当一个请求完成(无论是正常结束还是被中断),调度器会将其 BlockTable 中记录的所有物理块标记为“已释放”。
这些被释放的物理块会被返还给全局的 BlockPool,等待被分配给新的请求。这就是 LRU 等淘汰策略发生作用的地方,BlockPool 通过引用计数来管理这些块的生命周期。
贯穿始终的优化:复制与共享
在整个生命周期中,APC 是一个重要的优化。如果调度器发现新请求的提示前缀与某个已完成请求的序列高度重合,它不会为新请求分配新块并重新计算,而是会直接让新请求的 BlockTable 指向已存在请求的物理块,并通过增加引用计数来实现共享。这避免了重复计算和存储,极大提升了性能。
**总结一下,KV Cache 的生命周期本质上是物理块在全局 BlockPool 和各个请求的 BlockTable 之间流转的过程,核心是 BlockPool 和 BlockTable 的协同工作,而 APC 是在此基础上的高效优化。”
KV cache优化主要分为四步:调度、内存、通信、格式。
- 调度(分别从资源规划、系统稳定性和故障恢复的角度提升了系统的整体效率。)
KV-Consumer 模式 + 预算感知调度,把块分配从“事后回收”变成“事前预算”,利用率↑30%;
事后回收具体过程:
Details
传统“事后回收”模式 (Reactive)
做法:调度器先将请求调度上来,为其分配KV Cache块并开始计算。只有当显存不足(OOM) 时,才触发回收机制(如LRU),换出或终止一些请求以释放空间。
问题:
资源竞争与颠簸:多个Worker可能同时尝试分配,导致资源竞争。在OOM边缘,系统可能频繁地陷入“分配 -> OOM -> 回收 -> 再分配”的循环,造成计算中断和抖动。
利用率上限低:为了绝对避免OOM,系统必须保留大量的显存余量作为安全缓冲,导致显存无法被用满,利用率天然有天花板。
KV-Consumer + 预算感知模式 (Proactive)
原理:
预算:在调度一个请求之前,调度器会预先计算该请求完成整个生成过程最大可能需要多少KV Cache块(即它的“预算”)。所需 KV Cache 块数 ≈ (输入令牌数 + 最大输出令牌数) / 每个块容纳的令牌数。
资源预留:调度器会检查当前全局空闲的物理块数量是否大于等于该请求的预算。
如果是,则从全局内存池中预先划拨(预留) 这部分块给该请求,然后才将其放入运行队列。这确保了该请求在其生命周期内永远不会因为缺块而阻塞。
如果否,则请求在等待队列中排队,直到有足够的空闲块被释放。
如何提升利用率30%:
消除竞争:资源分配在调度阶段由中央调度器原子性地完成,Worker之间无竞争。
避免颠簸:由于确保了每个运行中的请求都有充足“粮草”,系统永远不会陷入OOM和频繁回收的困境。
pd_link_down 快速失败与显存即时回收。从故障处理和资源管理的优化,目标是减少长尾延迟和避免资源泄漏。
Details
问题背景:在分布式系统中,节点间的网络连接(如物理链路pd_link)可能偶尔故障。传统的重试机制会不断尝试重新建立连接,期间会挂起所有依赖该连接的请求。
pd_link_down 快速失败
传统做法:检测到连接失败 -> 进入指数退避重试循环 -> 多次重试后最终超时(可能耗时数秒到数十秒)-> 标记失败。所有被挂起的请求都经历了这个长尾延迟。
新做法:操作系统或驱动层能更精确地检测到物理链路故障(pd_link_down)。一旦检测到这种确定性的硬件故障,系统会立即向上层报告错误,而不进行任何重试。
原理:对于“物理链路断连”这种不可能通过重试在短时间内自动恢复的硬故障,立即失败是最优策略。这使得受影响的请求能立刻(长尾重试→0)被标记为失败,并返回错误信息给客户端,而不是让用户无谓地等待。
显存即时回收
关联:当一个请求因为通信失败而终止时,它可能已经分配了大量的KV Cache块。
传统做法:回收操作可能会被延迟,例如等待一个统一的垃圾回收周期,或者因为复杂的引用关系未能立即解除。
新做法:在请求被标记为失败的同时,系统会同步地、立即地释放该请求占用的所有KV Cache块(将其归还给全局BlockPool)。
原理:这避免了宝贵的显存资源被已失败的请求白白占用,让调度器可以立即将这些资源重新分配给等待队列中的新请求,从而维持了系统的高吞吐能力。如果没有即时回收,失败的请求会像“僵尸”一样消耗显存,逐渐拖慢整个系统。
- 内存
长序列 MOE 用 split 替代 concat,峰值显存减半;
NZ 格式 + 权重预转 Fractal,消除一次 view-flatten 拷贝;
APC OOM 修复,每层用完立即释放 pa_out,而不是缓存到类成员。 - 通信
优先走 HCCL All-to-All 原语,省掉 PyTorch 通用路径的额外 buffer;
TP+SP 双并行,128 k 序列切成 2 k-4 k 块,TTFT 线性下降 8×。 - 格式与算子
支持 NZ(Fractal_NZ)矩阵格式,QKVO-MatMul 直接调用昇腾高密度 Cube 指令;
结果:同等 SLO 下,吞吐↑1.7×、TTFT↓8×、显存峰值↓40%,且全链路零拷贝、零重训。”
NZ格式KV缓存
Details
- nz格式:一种专为注意力机制优化的稀疏块状存储格式,用于存储Key和Value缓存。NZ格式将KV缓存组织为更小的、对齐的数据块(Blocks)。这种格式与NPU计算单元(如3D Cube)处理数据的模式高度匹配。 计算注意力时,NPU可以直接从这种格式的缓存中高效地加载数据块进行计算,无需额外的格式转换(Transpose/Reshape)开销。 - MTP感知平铺:MTP(Multi-Thread Parallelism)指昇腾NPU内部的多线程并行架构。具体分以下三步:算法会动态分析MatMul操作的维度(M, N, K);此优化指根据NPU硬件线程的特性来智能地切分(Tiling)计算任务;确保切分后的子任务大小均匀。
PD分离中调度优化项目二:
把原生 AscendScheduler.schedule() 换成 支持 PD 分离(Prefill-Decode 分离)+ 异步 KV 加载的版本,同时注入 NPU 所需的特殊调度逻辑。
Details
一、主要改动概览(按执行顺序)
- 重载 AscendScheduler.init
多初始化两个字段
scheduled_req_ids:已排过队的请求 ID 集合
running[]:当前正在跑的请求列表
根据 kv_transfer_config 判断本节点角色
is_kv_consumer = True → Decode 节点(只负责拉远程 KV 做生成)
is_kv_consumer = False → Prefill 节点(算完 KV 后推给远端池) - 替换 AscendScheduler.schedule()
在原调度循环里插入 4 段 PD 逻辑:
① 请求类型过滤 禁止把“等待远程 KV”的请求混到纯 Decode 批次,防止死锁
② 远程 KV 状态机 若 request.status == WAITING_FOR_REMOTE_KVS 且 KV 未就绪 → 直接 skip_cur_request(),不占用计算资源
③ 外部 KV 匹配 通过 connector.get_num_new_matched_tokens() 从 Mooncake/3FS 拿到可复用长度,减少本节点计算量
④ 异步加载路径 当 load_kv_async=True 时,只分配显存块,把请求重新放回 waiting 并设状态 WAITING_FOR_REMOTE_KVS,等下一轮 KV 拉完再真正计算 - 资源水位与块分配
仍复用 vLLM 原生的 kv_cache_manager.allocate_slots(),但把「外部已缓存 token」计入 num_new_tokens,避免重复计算。 - 事件发布
调度结束后把块分配事件打包成 KVEventBatch,供 profiler 或可视化面板消费。KVEventBatch就是 vLLM 的“KV-Cache 黑匣子”,把每次块操作转成结构化日志发出去,让运维/开发者看得见、算得清、调得快。具体作用是把 KV-Cache 块的生命周期事件(分配/释放/命中/迁移)按时间戳打包成批量日志,广播给外部 profiler 或可视化面板,用来观测显存碎片、命中率、热点块分布,进而调优调度策略。
优化项目七--算子测优化
通过mst后者mindstudio 工具识别算子下发瓶颈,因此针对gelu算子、add_layer_norm算子做下发优化,针对add_layer_norm做二维输入的支持;
Details
- 工具识别与瓶颈定位
工具:mst 和 MindStudio
MindStudio:提供图形化性能分析界面,可以清晰地看到模型在NPU上运行的Timeline。在这个Timeline上,每个算子的执行时间、NPU计算单元与内存拷贝的间隙都一目了然。
mst:命令行工具,同样用于性能数据采集和分析。
识别的问题:通过分析Timeline,工程师发现模型运行中存在大量的小算子串行执行的情况。每个小算子(如GELU、Element-Wise Add、LayerNorm)都会导致一次独立的Kernel下发。这会带来两个主要开销:
Kernel下发开销:每次下发都有固定的CPU和驱动开销。
读写带宽开销:每个小算子都需要从全局内存读取数据,计算后再写回,产生了大量不必要的内存访问。
- 关键算子融合:Add + LayerNorm
这是解决上述瓶颈的最有效手段。
优化前(性能低下路径):
输入 -> [标准GELU算子] -> 中间结果 -> [Add算子] -> 中间结果 -> [LayerNorm算子] -> 输出
问题:3次Kernel下发,2次全局内存的中间结果写回和读取。
优化后(性能极致路径):
输入 -> [优化版GELU算子] -> 输出
输入1, 输入2 -> [融合算子 Fused_Add_LayerNorm] -> 输出
改进:
Add + LayerNorm部分:将两个算子融合为一个 Fused_Add_LayerNorm 算子。
减少Kernel下发:从2次变为1次。
减少内存访问:Add 的结果直接在Kernel内部传递给 LayerNorm 的计算单元,无需写回和读取全局内存。
效果:qwen模型带来5%的端到端吞吐量提升
框架测
- reasoning_content长度控制
- 针对embedding模型只进行全量推理的特点,跳过kv cache相关tensor等的计算;
- 针对离线大批量处理场景,优化非必要cpu操作耗时,如array转list、list转tuple等;
- 调度优化,比如异步调度,原先一个scheduler,一个forward;现在是一个schedule N steps,三个forward,优势是降低调度次数和准备输入
host bound
host bound属于是cpu下发瓶颈,device等待cpu下发,卡上free时间较多,这种情况一般有以下几种场景
1、短序列导致npu计算过快,cpu下发较慢;
2、device与host发生同步;
3、通信算子一般下发比较慢,做通算并行的时候,切分过碎的时候会导致bound。
4、cpu上有复杂操作
根因来说:
host执行时间长,经常因为是python侧代码执行耗时长;比如:for循环,numpy的操作、tocpu,复杂条件判断等。
如何找到相对于的代码:
1.采集一份stack的profiling,工具有msttt,mindstudio,去查看stack
2.根据算子名称进行搜索
通用的一些优化方法:
1.绑核:PyTorch host侧主要是由单一的主线程下发算子,更依赖单核性能,对PTA主要线程进行CPU绑核,减少切换代价;通过绑核一般有1-2%的提升
2.下发队列:通过优化TaskQueue,提高线程交互效率;减少下发耗时和下发抖动的
自动前缀缓存(APC)场景下前缀kv cache复用
自动前缀缓存(Automatic Prefix Caching),其核心目标是避免重复计算提示词中相同前缀的注意力,从而提升推理效率。它的工作原理主要依赖以下几点:
Details
KV Cache 与注意力机制:LLM 在生成文本时,Transformer 结构中的自注意力机制会为每个输入序列的 token 生成一个 Key-Value (KV) Cache。在解码阶段,模型需要利用之前所有 token 的 K 和 V 向量来计算当前 token 的注意力,这些向量被缓存下来以加速计算。
复用共享前缀:自动前缀缓存的核心思想是,当一个新的请求的提示词(例如,包含长文档的系统提示词)与某个已处理请求的提示词共享一个完全相同的前缀时,新请求可以直接复用已有请求前缀部分的 KV Cache。这样就跳过了对共享部分的重复计算,显著降低了推理延迟。
PagedAttention 支持:vLLM 的 PagedAttention 机制将 KV Cache 在物理内存上划分为一个个固定大小的块(Block),使得非连续的逻辑缓存块映射到连续的物理内存成为可能。这种设计使得缓存块可以在不同序列之间共享,为实现前缀缓存提供了底层支持
vllm_npu/common/attention/backends/fx_base_attn.py
在 vLLM V1 的 APC(Attention Prefix Caching)模块里,我负责提高 KV-Cache 复用率、降低首 token 延迟。
场景:线上 30B 模型 TTFT 波动大,p99 2.1 s,发现 40 % 请求前缀重复但缓存命中率仅 55 %。
任务:把命中率提至 80 % 以上,TTFT 下降 30 %,且不能引入正确性风险。
把请求按 block_size 拆成一串“指纹”后,去全局缓存池里找“已经算过的块”,能找多少算多少,返回给调度器直接复用,省计算、降 TTFT。
下面用 4 句“人话”讲清主干逻辑:
- 先算最多能拆几块
max_num_blocks = max_length // self.block_size - 合并块模式(combine_block_num > 1)
把相邻 C 块当成一个“大逻辑块”去查表:
先查尾部那块在不在;
在就往前补齐前 C-1 块,必须连续且 block_id 严格递减;
缺一块就整段放弃,打 warning 并禁止本次 APC,防止错复用。
最后如果开了投机采样,直接抛异常(当前实现不允许合并块+投机)。 - 逐块模式(combine_block_num == 1)
最简单:顺序扫 hash 链,遇到第一个缺失就停;
若开了 EAGLE 且命中长度>0,把最后一块 pop 掉,留给投机采样重新计算,避免只读冲突。 - 返回结果
computed_blocks 里就是可以 100 % 复用的 KV-Cache 块列表,顺序与请求前缀完全一致;调度器后续只算剩下的块即可。
再浓缩成一行:
“拼拼图”——能拼几段拼几段,拼不齐就放弃,绝不拿错块糊弄人。
专家优化的项目:
方案一:切分-通信-合并 核心逻辑就是先通信收集完整分数矩阵,再做全局Softmax+Attention@V,通信量是O(S)(序列长度S),所以
方案二:全部归约(分布式SOFTMAX),核心逻辑是先做局部Softmax校正,再分布式计算Attention@V,最后归约输出,通信量是O(D_head)(头维度D_head,通常远小于S),所以对长序列有优势(通信量与S无关,仅与D_head相关),适用于长上下文(S>10k)、带宽受限但算力充足的场景
vLLM 0.11.0(2024年5月发布)已原生支持张量并行(Tensor Parallelism),并在parallel_utils.py中封装了完整的分布式通信原语(包括All-Reduce max/sum)。方案二(全部归约)的修改无需新增通信工具类,只需适配注意力层的分布式逻辑,
核心架构以及执行流程如下:
Details
关键修改文件
vllm/model_executor/layers/attention.py:修改TensorParallelAttention类的_forward方法,新增分布式Softmax逻辑。
vllm/attention/ops/flash_attention.py:修改FlashAttention算子的forward方法,支持局部QK计算和分布式Softmax校正
vLLM 0.11.0版本的方案二执行流程
以70B Qwen模型、张量并行度TP=8、序列长度S=10k、头维度D_head=128为例:
模型初始化:加载70B Qwen模型,张量并行切分K/V为S/TP=1250长度,Q保持完整(1×128)。
局部QK计算:每个TP rank计算1×1250的局部分数矩阵。
分布式Softmax:
局部计算max(1×1)和sum_exp(1×1);
8个TP rank通过All-Reduce同步全局max和全局sum_exp;
校正局部分数为全局正确的概率(1×1250)。
局部Attention@V:每个TP rank使用校正后的局部概率与本地V计算1×128的局部输出。
全局归约:8个TP rank通过All-Reduce(sum)合并局部输出,得到最终1×128的全局输出。
对应结果:
短上下文(S<1k):方案二的优势有限,仅通信量↓92%,但延迟和显存优化不明显,适合带宽受限的场景。
中/长上下文(S≥10k):方案二是碾压级优势,通信量↓99%+,延迟↓36%-58%,显存↓5%-18%,是长上下文大模型推理的必选方案。
工程落地价值(以华为云服务为例)
服务容量提升:长上下文场景下,方案二的单卡服务容量从100并发提升至250并发(↑150%),因为显存占用降低且延迟缩短。
成本降低:相同服务规模下,方案二所需的NPU数量从16卡降至6卡(↓62.5%),年硬件成本节省约120万元。
用户体验优化:长文档摘要场景的响应时间从215ms降至89ms,达到“实时交互”标准(<100ms)。
二、关键指标的量化分析
- 通信量优化(核心优势)
方案一:通信量与序列长度S成正比(通信量 = S × D_head × 2(QK)× 4字节(FP16)/ TP)。例如S=100k时,通信量=100k×128×2×4B/8=1.28GB。
方案二:通信量与头维度D_head成正比(通信量 = D_head × 2(max+sum)× 4字节(FP16)× TP)。例如D_head=128时,通信量=128×2×4B×8=0.001GB。
长上下文场景:方案二的通信量几乎可忽略,彻底解决长上下文的带宽瓶颈。 - 推理延迟优化
短上下文:方案二的延迟优势不明显(仅↓5.6%),因为通信量本身较小,分布式Softmax的计算开销抵消了部分收益。
长上下文:方案二的延迟↓58.6%,因为通信量从1.28GB降至0.001GB,通信延迟占比从60%降至1%以下。 - 显存占用优化
方案一:需预分配全局KV-Cache内存(如S=100k时,KV-Cache内存=100k×128×2×4B=100MB/头,8头则800MB),但因碎片率高(如95%),实际显存占用达35.7GB。
方案二:通过分布式Softmax减少全局内存分配,碎片率降至2.6%,显存占用仅29.1GB,节省18.49%。
LLM中一些优化特性
数据并行/流水线并行/张量并行/专家并行/序列并行/管道并行
Details
数据并行:把输入的 batch 切分成多份,分配到多张卡上,每张卡独立执行完整的模型推理。所以数据并行优势是用于处理更大的批次大小,将不同的数据样本分到不同的“并行组”中。
Pipeline并行(流水线并行):
它的思路是把 模型的层 拆开,比如前几层放在 GPU0,后几层放在 GPU1。这样一张卡不用存整个模型,只存自己负责的那部分层。推理时数据是顺序流过的,有点像工厂的流水线。好处是实现比较直观,显存节省效果明显。缺点是推理时要等“流水线”传递数据,中间会有 bubble(气泡),比如前一层算完才能传给后一层,整体延迟上不去。
Tensor张量并行:
用于非MoE的共享层(如嵌入层、门控网络、输出层),或者用于单个非常大的专家。如果一个专家本身的参数量巨大,超过了单个设备的内存,那么可以在这个专家内部使用张量并行。优势是用于处理模型内部那些巨大且密集的部分。
它是把 单层内部的算子 拆开,比如一个大的全连接矩阵乘法,我们把权重矩阵切成几块分到不同 GPU 上,每个 GPU 负责算一部分,再做 AllReduce 合并结果。好处是能并行利用多卡算力,加速单层的计算。缺点是通信量比较大,尤其是每层都需要跨卡通信,如果跨机通信的话开销就更明显。MLP先列后行,self-attention按照头来切。
| 层级 | 切权重? | 切激活? | 备注 |
|---|---|---|---|
| Column-Parallel Linear(Gate/Up/FC1) | ✅ 按“输出维度”切 | ✅ 输出特征图跟着切 | 每卡算 1/k 列 |
| Row-Parallel Linear(Down/FC2) | ✅ 按“输入维度”切 | ❌ 输入激活不切 | 需要 All-Reduce 汇总 |
| Attention QKV/O | ✅ 按 Head 数切 | ✅ 分头计算 | 每卡管 1/k 个头 |
| LayerNorm/RMSNorm | ❌ 全复制 | ❌ 全复制 | 参数 <0.1%,不值得切 |
| Softmax | ❌ 全复制 | ✅ 可切(sequence parallel) | 见第三节 |
| “列切输出,行切输入;头切注意,seq 可选;Norm 不切,通信一次ALL-reduce。” |
EP专家并行:在 MoE 模型中,“Expert-Parallel 推理把同一批 token 的不同专家放不同卡,token 通过 All-to-All 通信“去找”专家,计算完再 All-to-All 回来。
总参数量可无限增加,但每 token 计算量不变——用通信换显存、用稀疏换容量。每个设备只存 E/N 个专家,token 被门控动态路由到目标专家所在 GPU/NPU,计算完再返回原设备。两次 All-to-All 是 EP 最昂贵也最关键的操作;vLLM/DeepSpeed 用 异步 + fused kernel 掩盖延迟。
专家并行具体过程
Details
它的核心思想非常简单直接:将不同的专家分配到不同的设备上。 我们来分解一下它的工作流程,假设我们有一个包含N个专家的MoE层:
-
分配专家:将N个专家(如前馈神经网络)分别放置在N个不同的计算设备上(如GPU)。每个设备只存储和负责计算分配给它的那个专家。
-
数据路由与广播:
- 当输入数据(一个token或一个序列)进入MoE层时,门控网络会计算出该输入应该被发送到哪K个专家(例如,Top-1或Top-2)。
- 输入数据(或者它的激活值)会被广播到所有被选中的专家所在的设备上。
- 本地专家计算:
- 每个拥有专家的设备在接收到输入数据后,只使用本地存储的专家对数据进行前向传播计算。
- 例如,GPU 1上的Expert 1处理发送给它的输入,GPU 2上的Expert 2处理发送给它的输入,以此类推。
- 结果收集与组合:
- 每个专家计算完成后,会产生一个输出激活值。
- 这些输出被发送回一个主设备(或者通过All-Reduce/All-Gather操作),并根据门控网络的权重进行加权求和,得到MoE层的最终输出。
核心特点:
- “专家为中心”的分区:每个设备是某个专家的“家”。数据和计算流向专家。
- 通信模式:主要的通信是All-to-All。在向前传播时,我们需要将输入token发送到它需要的专家所在的设备;在反向传播时,我们需要将梯度收集回来。这种通信模式在专家数量多、token分布不均匀时可能成为瓶颈。
- 内存优势:每个设备只存储总模型参数的一部分(它分配到的专家 + 共享的非MoE层),极大地降低了单个设备的显存压力。
序列并行:把输入按照序列长度切分到多张卡上进行计算。
SP只针对LayerNorm和Dropout输出的activation在序列维度上进行切分。
可以将SP+TP视为TP的一种特殊形式,每层做两次AllGather,2次ReduceScatter操作,通信量和纯TP相同(AllReduce = AllGather+ReduceScatter),Add+Norm块的计算量变为1/SP。 TP一般会和SP一起。
简短回答: 在上下文并行中,KV Cache 矩阵是必须被切分的,而 Q 矩阵通常不切分(或采用不同的切分方式)。
Q @ K^T 这个矩阵乘,也就是QK 计算,存在两种主流方案:“切分-通信-合并” 和 “全部归约”。“全部归约”方案通常更高效。
两套方案对应区别以及过程
Details
方案一:切分-通信-合并
- 局部 QK 计算:每个设备用自己的完整 Q 和本地切分的 K 进行计算,得到一个局部、不完整的 Attention 分数矩阵。其维度为 [Local_Q_Len, Local_KV_Len](即 [1, S/P])。
- 通信收集:通过一个 All-Gather 通信操作,所有设备将各自计算出的局部 Attention 分数矩阵收集起来。
- 合并:每个设备将收集到的所有局部分数在 K 的序列维度上进行拼接,最终得到完整的、全局的 Attention 分数矩阵 [1, S]。
优点:通信的数据量是 O(S),相对较小(因为 Q 的长度是 1)。
缺点:
后续的 Softmax 和 Attention @ V 计算需要在全局完整的分数矩阵上进行,这意味着每个设备都要做重复的、完整的计算,浪费了算力。
通信和计算是分开的,可能无法完美重叠。
方案二:全部归约
- 局部 QK 计算:同方案一,每个设备计算局部 QK 分数 [1, S/P]。
- 分布式 Softmax:这是一个关键优化。我们不对局部分数进行 All-Gather,而是直接在其上执行 Softmax 的第一步。
每个设备计算自己局部分数切片上的 最大值 和 指数和。 - 全局归约:通过一个 All-Reduce 通信操作(通常是 max 和 sum 操作),在所有设备间同步出全局的最大值和全局的指数和。
- 校正局部 Softmax:每个设备利用全局最大值和指数和,校正自己本地的指数值,从而得到正确的、全局的 Softmax 概率分布。现在,每个设备上的局部概率切片 [1, S/P] 已经是正确的全局概率的一部分。
- 局部 Attention @ V:每个设备用自己的、已经校正过的局部概率,与本地切分的 V 进行计算,得到一个局部的输出向量 [1, D_head]。
- 全局归约输出:最后,通过一个 All-Reduce(sum) 操作,将所有设备的局部输出向量相加,得到最终的、完整的 Attention 输出 [1, D_head]。
为什么方案二更高效?
- 计算并行化:Softmax 和 Attention @ V 这两个计算密集型操作也被分布到了各个设备上,而不再是重复计算。这充分利用了所有设备的算力。
- 通信量优化:
- 第一次 All-Reduce(用于 Softmax)通信的数据量极小,只有每个头的两个标量(最大值和指数和),可以忽略不计。
- 第二次 All-Reduce(用于输出)通信的数据量是 O(D_head),而 D_head 通常远小于序列长度 S(例如,D_head=128,S=100k)。
- 因此,总体通信量从方案一的 O(S) 降低到了 O(D_head),这在长上下文场景下是巨大的优势。
- 通信与计算融合:分布式 Softmax 的流程天然地将通信嵌入到计算步骤中,流程更紧凑
PP(Pipeline Parallel)管道并行
Expert Parallel(EP)与 Pipeline Parallel(PP)可以叠加,根本原因是 两者切分的维度正交:
EP 按“专家”维度横切,把不同 expert 放到不同设备;
PP 按“层”维度纵切,也就是纵切层,把不同 transformer layer 放到不同 stage。
All-To-All 只在同层内,P2P 只在同专家组内;
二者组合成 二维并行网格,参数、计算、通信 同时线性扩展,无热点、无阻塞,因此能 1+1>2 地提升超大 MoE 模型的吞吐与容量。
只要 MoE 层里 单个 expert 的尺寸 ≤ 一个 PP stage 的显存预算,就能把“专家维”复用到“层维”上,实现 二维并行网格(EP × PP),从而同时扩大 模型容量 和 流水线深度,而不会互相阻塞。
CP
Colossal-AI 的序列并行(CP),CP则是对所有线性层(如QKV投影、FFN)的输入和输出激活值都在序列维度进行切分,CP 在超长序列场景下内存优势巨大,但通信开销和实现复杂度更高。
多卡分布式训推
“推理用 EP+TP+PP 三维并行,训练加 DP;Mooncake 把梯度、激活、KV-Block 统一用同一套 RDMA(就是“网卡直接读写远端内存”)零拷贝协议,跨机 200 Gbps,卡间 600 GB/s NVLink,一套网络同时服务训推,资源池化不碎片。”
flashattention
FlashAttention 用 SRAM 分块 tiling + 分段 softmax + 单 kernel 融合 把内存-bound 改成计算-bound,避免从全局内存中读取和写入注意力矩阵。训练/长序列推理 2–4× 加速、显存 10× 省;
FlashInfer 在此基础上针对 GQA/MQA decode 加 预取页表 + 分组 warp-GEMM + 量化融合,decode 延迟再降 30%。
V3 再叠加 TMA 异步、Warp 专用化、块量化,把 Hopper 张量核吃满,成为 长上下文大模型训练/推理的默认内核。
flashattention1-4对应区别:
同一层级falshattention 实现的方式有哪些?比如flashinfer
Details
| 技术名称 | 主要目标 | 核心创新 | 典型应用场景 | 代表框架/库 |
|---|---|---|---|---|
| Flash Attention | 优化计算,减少显存访问 | 计算分块 (Tiling)、在线软最大值、核融合 | 长序列训练、固定批量推理 | PyTorch (官方集成)、xFormers |
| FlashInfer | 高效推理,支持多样化注意力 | 块稀疏注意力、可组合性、负载均衡调度 | 高并发推理、长上下文、个性化注意力模式 | SGLang, vLLM, MLC-LLM |
| PagedAttention | 优化KV缓存内存管理 | 内存分页、非连续存储、内存共享 | 高吞吐推理服务、可变长度序列、并发请求 | vLLM |
| FlexAttention | 兼顾性能与灵活性 | 声明式API (score_mod)、通过torch.compile编译到高效内核 | 研究人员实验新注意力变体、需要定制化注意力逻辑 | PyTorch (实验性API) |
| SageAttention | 低精度计算加速 | 8-bit 注意力量化 | 低精度推理、追求极致速度 | 特定研究实现 |
Flash Attention 的核心思想是 “分块计算” 和 “核融合”。
分块(Tiling):将大的 Q, K, V 矩阵分割成小的块(Tiles),这些块的大小足以被加载到 GPU 的高速 SRAM 中。
循环计算:通过双重循环,将这些小块从 HBM 加载到 SRAM,然后在 SRAM 内部进行所有的计算步骤(矩阵乘法、Softmax、矩阵乘法)。
核融合(Kernel Fusion):将整个注意力计算(MatMul -> Softmax -> MatMul)融合打包成一个单独的Kernel来方便快速计算
[!CAUTION]
重计算(Recomputation):
Softmax 操作本身是全局的,因为它需要知道所有元素的值来计算归一化分母。在反向传播时,Flash Attention 不需要存储巨大的中间矩阵 S 和 P。它只存储了最终的输出 O 和统计量 l, m。当需要计算梯度时,它会利用存储的 Q, K, V, O, l, m 以及反向传播算法,重新计算出注意力矩阵 S 和 P 的块。这是一种用计算换空间的典型策略。
优势:显存占用从O(N²)降至O(N),允许处理极长序列。
| 版本 | 年份 | 核心改进 | 作用/收益 | 备注 |
|---|---|---|---|---|
| FlashAttention-1 | 2022 | 首次提出 分块 tiling + 在线 softmax,把完整注意力矩阵拆成 SRAM 内的小块计算,内存复杂度 O(N²)→O(N) | 长序列训练速度 ↑2–4×,显存省 50 %,在 A100/V100 上即可跑 16 k+ token | 奠定“IO-aware 精确注意力”范式 |
| FlashAttention-2 | 2023 | 线程级并行 + Warp 级工作划分;把 Q/K/V 按行/列切块,减少非矩阵运算;合并缩放因子 | 相同算力下吞吐再 ↑1.7–2×,FLOPs 利用率从 30 %→73 %;支持 MQA/GQA;Ampere/H100 通用 | 训练/推理皆可用,成为 HuggingFace 默认内核 |
| FlashAttention-3 | 2024 | 面向 Hopper 架构:FP8 低精度、块稀疏注意力、异步拷贝 WGMMA;流水线重叠 | 比 FA-2 再 ↑3×,首次把内存压到 90 %↓;单卡 H100 跑 1 M token 级模型稳定训练 | 支持 RoPE、ALiBi、长上下文持续微调 |
| FlashAttention-4 | 2025 | 为 Blackwell GPU 原生设计:新在线 softmax(跳过 90 % rescale)、指数指令 MUFU.EX2、CUTLASS CuTe DSL 双缓冲 epilogue | GPT-4 规模训练 ↑11×,比 cuBLAS 13 再快 22 %;能耗同步 ↓30 %,成为 HotChips25 焦点 | 与 NV 联合调优,后续将进 cuDNN 标准库 |
flashattenton2:
从GPU硬件特性(并行度、内存层次、Warp调度)出发,重构了算法实现。
最重要的是并行化策略,增加了序列来增加并行
Details
FlashAttention-1:它的并行化主要集中在非序列维度上,即批量大小(Batch Size)和注意力头数(Heads)。对于序列本身,它采用的是串行循环的方式。flashattenton2新增在序列长度维度并行。将不同块的计算分配给不同的GPU线程块(Thread Block)。 循环顺序(外循环 vs 内循环):减少了HBM访问,q变成只要读一遍 Warpanize 设计:每个Warp独立负责计算一个子块的注意力输出,最后再通过共享内存(Shared Memory)进行归约合并。
投机推理(MTP)
四步流程:
① 小模型自回归生成 k 个候选;② 大模型一次 forward 并行打分;③ 按 accept 概率保留或重采样;④ 循环直至结束
MTP实现的算法有哪些?
本质是大模型一次稍贵但便宜得多的并行计算,再加上K次小模型的廉价计算
投机推理加快的一些角度以及eager1-3对应作用:
Details
小模型选择: 优先是
- 同架构缩小版(最常用、最有效):
- 量化后模型
- 使用网络前几层(缺点:实现复杂,需要修改模型结构,浅层表示可能无法很好地模拟完整模型的输出。)
- 专用知识蒸馏小模型(缺点:需要额外的训练成本和数据。)
降低延迟的核心原因在于:用一次昂贵的并行计算,换取了多次昂贵的串行计算。
为什么一次并行验证比三次串行生成快得多?
硬件利用效率:GPU是一种大规模并行处理器,它最擅长做的事情就是一次性处理一大块数据。一次处理长为 L+K 的序列,其计算效率远高于串行处理3个长度分别为 L, L+1, L+2 的序列。
内核启动开销:每次启动GPU内核(Kernel)进行前向传播都有固定的开销。合并成一次内核启动,就消除了两次额外的开销。
内存读写优化:一次处理长序列的数据可以更好地利用缓存,减少与慢速显存(HBM)的通信次数。
“投机推理”系列里的 EAGLE-1/2/3 对应作用一句话总结:
EAGLE-1:把“草稿”从 Token 级换成倒数第二层特征级,特征+已采样 Token 联合自回归,解决特征不确定性,首次把草稿任务变简单 。
EAGLE-2:给静态草稿树加“置信度”开关,动态剪枝——只保留草稿模型自己确信的分支,减少无效候选 。
EAGLE-3:①退回到 Token 级草稿,解除特征对齐约束;②训练阶段就模拟多步自回归误差(训练时测试),让草稿模型提前适应真实分布;③把目标模型的低-中-高层特征一起喂给草稿模型,数据量越大加速比继续涨,突破 2/3 代的数据缩放天花板
prefix-caching
原理和KV Cache相似,可以用在多轮会话或者长system prompt场景下,加速prefill推理。
PD分离
p与d分开在不同的实例并行进行推理,收益点:
p不会阻塞d,在SLO要求下有收益;
p与d按其计算特点使用不用的并行方式
- kvcache的传输如何对接vLLM:依靠v1中KV transfer;
- 如何传输kvcache;
Details
通常KVCache的传输包括P2P模式(也叫直连模式)和Cache Store(也叫Pool模式)。
- P2P模式P2P的PD 直连就是预填充节点直接将 KV Cache 发送给解码节点,它的好处是延迟低,性能好。但也意味着在整个batch 的计算过程中锁定了P、D 节点的对应关系,一旦解码节点出现了问题,比如压力过大、服务出错、传输阻塞,在重试时无法仅调度 D 节点,需要重新进行整个预填充、解码过程。在 prompt 较长时,或者在 PD 节点数不对等的场景下,例如 2 个 P 对应到 1 个 D,重调度意味着抛弃较长或者多个 prefill batch,重调度的沉没成本较高。
- KV Cache Store/Pool使用 KV Cache Store/Pool 是在 P 和 D 之间增加了一个中间存储,预填充节点先将 KV Cache 写到中间存储,解码节点从中间存储读。这样做数据会多传输一次,增加了延迟,也增加了一些复杂度。但好处是容错性更好,还有就是预填充阶段本身也可以利用这个中间存储做 Prefix Caching。
RouterServer:
Router(网关层)
只负责请求收发、负载均衡、限流、鉴权等“流量入口”逻辑;它本身不包含任何算子,只是把请求转发给后端的推理服务 。
Server(推理服务)
真正运行模型、执行算子(包括 FlashAttention、PagedAttention、InitRouting 等)的进程;Router 通过 gRPC 把请求发给它,它再把结果返回给 Router
图模式
将模型的计算过程预先编译成一个静态的计算图,减少host耗时,减少device空闲
- 降低内核启动开销:减少 GPU 内核启动次数。
- 内存优化:图编译器可以更好地规划内存分配和复用。
- 算子融合:将多个小算子融合成一个大算子,减少数据搬运,提升计算效率。
npu上图模式一:ascend-turbo-graph:记录算子执行的依赖关系构图;消除python host耗时;且支持动态shape。如果未设置则默认配置到acl_graph,如果还存在不支持的情况,最终fallback到eager模式。
图模式二:
acl-graph,对标cuda-graph的piece-wise graph,如果设置了eager模式,则eager优先级更高。piece-wise graph =「Attention 留在 Eager,MLP 录成 CUDA Graph,运行时像乐高一样拼」,既吃图模式的零开销红利,又保留动态 shape 的自由度;一旦手动开启 eager,框架优先走即时路径,piece-wise 自动让位。
Details
| 维度 | 完整 CUDA Graph | piece-wise graph |
|---|---|---|
| 录制粒度 | 整网 forward | 仅 MLP/LN 子块 |
| 对 Attention 要求 | 长度固定、算子兼容 | 任意变长,Attention 留在 Eager |
| 显存占用 | 高(一次性录整张) | 低(只录小模块 + 固定缓冲) |
| 首次捕获耗时 | 长 | 短(模块小) |
| 运行时回退 | 不支持 | 自动回退 Eager |
| 适用场景 | 小模型、固定 shape 推理 | 大模型、长文本、连续批处理 |
chunked-prefill
修改位置:
vllm_npu/common/attention/backends/fx_base_attn.py
vllm_npu/common/core/scheduler.py
Chunked Prefill(Splitfuse)特性的目的是将一个长prompt分解成更小的块,并在多个forward step中进行调度,只有最后一块的forward完成后才开始这个prompt request的生成
华为 NPU(Ascend)上 chunk 并不是越小越好,也不是越大越好;1024 左右(或 512/2048 的 tile-size 整数倍)是实测值。
低于 256 会显著掉吞吐,高于 4096 又容易把 NPU 静态图缓存/片上内存撑爆,需要回退到 CPU 或产生额外 memcpy。
一句话记住
“ 当 chunk<256 时,Cube Core 矩阵单元因 tile 数量(tile 数量 = 把矩阵拆成 128×128 小块 后,能同时填满 Cube Core 阵列的块数)不足而并行度骤降,算术强度低、调度开销占比高,利用率跌至 30% 以下,吞吐随之雪崩。”
“chunk 超过 4096,图缓存、L2、KV 池同时爆仓,NPU 被迫把数据来回搬 DDR,搬得比算得还久,所以反而掉吞吐。”
Continuous Batching (连续批处理)
也就是已完成请求立即离开批次,新请求立即加入,批处理持续进行。
核心思想是:以每次前向传播(iteration)为调度单位,而不是以整个请求(sequence)为单位。
好处是,没有输出层 padding 浪费,降低了TTFT,增加了吞吐。
- 静态批处理 (Static Batching):在推理前,将多个请求合并为一个大的请求,然后一次性推理。这种方式可以提高吞吐量,但是需要所有请求都完成后才能返回结果,所以一般不会应用
- 动态批处理 (Dynamic Batching)
动态连续批 = “先算完的立刻下车,没算完的继续坐,车(GPU)从不空跑”。
其他重要特性
- Guided-decoding:
- 通过logit-process, 格式化控制模型输出
- Reasoning content:
- 解析返回体中的reasoning
reasoning_content对应做过事情
思维链:
解析格式:
# 起始标记(start_token)
...abc... # 任意字符,即“推理内容”
# 结束标记(end_token)
xyz... # 真正的回复正文
- Functioncall:
- 解析返回体中的tool;通常和guided-decoding一起使用
对应原理以及具体流程如下所示(应用)
- 解析返回体中的tool;通常和guided-decoding一起使用
Details
FC特性实现主要包括前处理(serving_chat模块)、后处理模块(tool parser)以及chat_template,前处理模块通过chat_template对用户prompt进行渲染,后处理模块对模型输出进行解析。整体流程如下图所示:
- 检测到用户输入中传入工具定义,并开启了auto tool choice选项;
- 应用chat template渲染用户输入,将工具信息和用户prompt构造到模型输入中;
- 模型执行推理并完成基础的后处理流程;
- tool parser对模型输出进行解析,如果模型输出中包含工具调用信息,tool parser会将工具调用信息解析到用户响应中的tool_calls字段中。
- Multi-lora:
- 基模型和多个挂载的lora权重一起推理服务
npu/gpu架构
核心单元:每个AI Core集成一个3D Cube矩阵计算引擎,单周期可执行4096次MAC操作。
达芬奇 3D-Cube 单元每拍能并行完成 4096 个 16-bit 乘加运算,等效 8.19 T FP16 FLOPS/Core。
910B ≈ A100(Ampere架构)
910C ≈ H100(Hopper架构)
npu架构与gpu区别:
Details
总结一句话是:GPU 通用并行 + 高带宽缓存,适合大batch、可变形状;NPU 专核矩阵 + 片上 SRAM(静态随机存取存储器),适合小batch、固定形状、低延迟。一句话:GPU 算力峰值高,NPU 利用率/能效高。
-
三维立方计算引擎(3D Cube)——暴力美学的心脏
单周期 4096 次 MAC:16×16×16 立方阵列,一次时钟完成 4096 个乘加,等效 4096 OPS。
对比 2D 阵列:同样 4096 次运算,2D 需要 64×64 平面,Cube 只要 16×16×16,数据搬运距离缩短 4×,能效提升 3-5×。
支持多精度:INT4/INT8/FP16/FP32 混合,训练推理全覆盖。 -
三合一计算单元
每个Core内部包含三种计算单元:
① 3D Cube:矩阵运算主力;
② Vector单元:向量计算;
③ Scalar单元:控制流和逻辑运算
| 单元 | 职责 | 类比 |
|---|---|---|
| Cube | 大块矩阵乘 | “重体力” |
| Vector | 向量/激活函数 | “轻体力” |
| Scalar | 循环、分支、打杂 | “包工头” |
- 统一可扩展
同一套汇编可在手机、边缘盒、云端集群无缝迁移,开发一次,全场景部署
与gpu对比:
| 架构 | 设计取向 | 矩阵单元 | 单周期 MAC | 能效比 | 场景覆盖 |
|---|---|---|---|---|---|
| 达芬奇 | 专做 AI | 3D Cube 16×16×16 | 4096 | 高 | 端-边-云统一 |
| CUDA GPU | 通用并行 | Tensor Core 4×4×4 | 256 | 中 | 云端为主 |
| Google TPU | 云端推理 | 2D MXU 256×256 | 65536 | 高 | 仅云端 |
HCCS = Huawei Cache-Coherent System,是昇腾(Ascend)处理器之间专用的片间高速缓存一致性总线,功能定位就是对标 GPU 世界的 NVLink:
作用
在 NPU 与 NPU(或多路 CPU-NPU)之间建立高带宽、低延迟、缓存一致的点对点通道
让多颗 910B/910C 像一块大芯片一样共享统一内存地址空间,All-Reduce、梯度同步等集合通信无需再走 PCIe,带宽高、CPU 不参与
速率规格(910B 世代):单通道 56 Gb/s(NRZ)
NPU片上cache架构:
| 层级 | 典型容量 | 隶属关系 | 管理/可见性 | 作用 |
|---|---|---|---|---|
| L1 Buffer | 2–10 MB / 核 | 每核独享 | 需手动划分 | 存放当前核的输入分块,实现「核内」数据复用 |
| Unified Buffer | 8 MB (例: Qualcomm SA8295P) | 每核独享 | 编译器可见 | 向量/标量单元读写区,与 L1 交互,按 32 B 对齐 |
| L2 Cache | 10–40 MB 全芯片共享 | 多核共享 | 可手动管理 | 缓存大块权重/激活,减少 HBM 访问 |
| Accumulator | 寄存器级 | 每核内部 | 硬件自动 | 矩阵乘累加,避免反复写回 Unified Buffer |
| TCM | 8 MB (例) | 每核/tensor 单元独占 | 软件显式分配 | 高通/嵌入式 NPU 的「紧耦合内存」,DMA 与 DDR 延迟覆盖 |
核心思想:手动管理 + 分块缓存
- 编译期即可决定数据驻留位置,程序员通过 pragma/tile 指定 L1/L2 块大小。
- 矩阵计算先做 Tiling → 片上分块 → 复用 L1 → 累加器累加 → 写回 Unified Buffer → DDR 回写。
🔍 关键差异 vs GPU
- GPU L1 由 SM 自动调度且容量小(256 KB SharedMem);NPU L1 可达 10 MB 且 完全由用户控制,可一次塞下更大的输入分块。
- NPU 无透明硬件缓存一致性,不存在缓存失效开销,可预测带宽与延迟。
- L2 通常为多核共享,但同样支持 静态划分,避免核间抖动。
GPU Hopper架构:4 nm + 132 SM(CUDA Core + Tensor Core + 共享内存/缓存 + Warp Scheduler;132 SM ≈ 132 条流水线同时跑,数量越多,峰值算力越高) + 支持 FP8 混合精度与 Transformer Engine + 900 GB/s NVLink4,打万亿模型;
Thor X(车规芯片) :
原理就是Thor X 把 Grace 级 CPU(做 通用计算+大内存) 和 Blackwell GPU (大算力+AI 专核)用 NVLink-C2C (CPU↔GPU 900 GB/s 片间互联,统一寻址无需 PCIe 拷贝;显存即主存)缝成一片,统一内存零拷贝,‘单芯片'跑全栈自动驾驶。单芯片 > 4 PFLOPS@FP8(数据中心版),车规版 9.2 TFLOPS@FP32 / 1000 TOPS@INT8 。
FLOPS = 浮点运算/秒(精度越高越慢),TOPS = 整数乘加/秒(AI 常用 INT8)。
量级:P = 10¹⁵,T = 10¹², 换算:1 PFLOPS = 1000 TFLOPS = 10³ TOPS(同精度下)。
极低时延现状
极低时延现状- -友商情况
在时延较高时,如50ms附近,910C的吞吐相比2xH20具有不错优势,但是在30ms以下优势逐渐丧失,在极低时延下,H20具有明显优势
极低时延挑战一- HBM带宽
在低时延小Batch场景,带宽会成为突出瓶颈。H20的显存带宽高达4TB/s,B200的显存带宽更是可以达到8TB/s,而910B的带宽只有1.6TB/s,实测更是只有1.2TB/s。
极低时延挑战二:框架耗时
在推理过程中除了Device侧核心的计算开销外,会涉及到很多调度、校验、模型下发等过程,往往会占用2~3ms时间
昇腾910B一般配备的CPU其单核算力只有X86的1/8,昇腾910C一般配备的CPU其单核算力只有X86的1/2。
极低时延挑战三:算子静态开销算子静态时延在高吞吐场景可以忽略,但是在低时延场景下不忽视
近期看的论文,书籍,公众号分享
Details
一、SpinQuant 速读(Meta 2024)
核心一句话
“把 QuaRot 的固定 Hadamard 旋转改成可学习旋转矩阵,用 Cayley 参数化 + 小校准集训练,4-bit 权重激活量化后平均 perplexity 比 QuaRot 再降 3-5 个点,推理零开销。”
我看重的三条细节
| 点 | 数值 | 备注 |
|---|---|---|
| 旋转矩阵参数量 | 仅 0.02% 模型参数 | 32 层 LLaMA-7B 只用 1.4 M 参数 |
| 校准集大小 | 512 条句子 | 10 min 训完,不用反向传播到全模型 |
| 端到端速度 | 1.0× 浮点速度 | 旋转融合在量化前离线完成,推理无额外乘加 |
代码级启示
- 旋转矩阵用
CayleyMap实现:R = (I - A)(I + A)⁻¹,保证可逆且正交,PyTorch 5 行搞定。 - 量化入口在
spinquant/rotate.py:rotate_weights_acts(),可一键套在任意 HuggingFace 模型上,我已给 Qwen-14B 跑通 4-bit,显存 6.9 GB → 2.1 GB,下降 69 %。
论文:SpinQuant: LLM Quantization with Learned Rotations (arXiv 2405.16406)
代码:https://github.com/facebookresearch/SpinQuant
二、近期大模型论文「快餐包」
| 方向 | 论文 | 一句话亮点 |
|---|---|---|
| 超长上下文 | Mooncake (Kimi 2024) | 把 KV-Cache 当“磁盘”,Prefill/Decode 分离部署,128 k 上下文首字延迟 0.8 s |
| MoE 训练 | DeepSeek-V2 | 236 B 总参数,21 B 激活,成本 1/3 GPT-4,MLA 注意力降 KV-Cache 93 % |
| KV-Cache 量化 | KVQuant (OSDI 2024) | 2-bit KV + 4-bit 权重,单卡跑 1 M tokens, perplexity ↑ 0.02 |
| 端侧推理 | SpinQuant | 上文已讲,4-bit 无 outliers,手机 Snapdragon 8 Gen3 跑 7B 15 tokens/s |
| 科学大模型 | ChemLLM (2024) | 首个化学对话大模型,分子 IUPAC ↔ SMILES 双向生成,BLEU 46.3 |
三、日常信息源(公开可搜)
📚 公众号(按打开频率)
- AI 大模型前沿——量化/部署干货多,日更。
- 深度学习自然语言处理——论文速递+代码链接,早 8 点推送。
- 机器之心——行业大事件,融资、芯片、政策一站式。
- NVIDIA 开发者社区——官方 kernel 优化、CUDA 新特性首发。
📖 今年读完/在读的书
| 书名 | 进度 | 备注 |
|---|---|---|
| 《Efficient Processing of Deep Neural Networks》 | 二刷 | 第 7 章量化与剪枝,做 SpinQuant 时当字典用 |
| 《大模型应用开发极简入门》 | 3 h 速读 | 写给 PM 的,30 分钟理清 GPT-4 → LangChain 链路 |
| 《The Elements of Computing Systems》 | 第 9 章 | 从与非门到跑 Tetris,补计算机体系结构短板 |
| 《AI 超级工程师:Prompt 编程》 | 在读 | 把提示当 DSL 设计,对我写 Triton kernel 注释帮助意外大 |
四、彩蛋:我常用的“论文 → 代码”三步
- arXiv Daily 邮件 → 标题过滤“quantization/kv cache”
- HuggingFace Papers 页面 → 直接点“Code”按钮跳 GitHub
- GitHub 先看
requirements.txt→ 判断 PyTorch 版本 ≤ 2.2 再 clone,避免环境地狱。
以上,既回答了 SpinQuant 的核心机制,也给了一份“大模型快餐书单”和日常信息源,希望能帮你在面试/技术交流里快速抛出“有数字、有代码、有体感”的观点。
常用的一些排序方式
Details
排序算法
排序算法是计算机科学中非常基础且重要的一类算法,用于将一组数据按照特定的顺序(通常是升序或降序)进行排列。以下是一些常见的排序算法及其原理:
- 冒泡排序(Bubble Sort)
- 原理:通过重复遍历要排序的数列,比较每对相邻元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
- 时间复杂度:平均和最坏情况为 O(n^2),最好情况为 O(n)。
- 空间复杂度:O(1)。
- 稳定性:稳定。
- 选择排序(Selection Sort)
- 原理:首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
- 时间复杂度:O(n^2)。
- 空间复杂度:O(1)。
- 稳定性:不稳定。
- 插入排序(Insertion Sort)
- 原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
- 时间复杂度:平均和最坏情况为 O(n^2),最好情况为 O(n)。
- 空间复杂度:O(1)。
- 稳定性:稳定。
- 希尔排序(Shell Sort)
- 原理:是插入排序的一种更高效的改进版本。希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了。
- 时间复杂度:取决于步长序列,平均为 O(n log n)。
- 空间复杂度:O(1)。
- 稳定性:不稳定。
- 归并排序(Merge Sort)
- 原理:采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。将数组分解到最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
- 时间复杂度:O(n log n)。
- 空间复杂度:O(n)。
- 稳定性:稳定。
- 快速排序(Quick Sort)
- 原理:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
- 时间复杂度:平均为 O(n log n),最坏情况为 O(n^2)。
- 空间复杂度:O(log n)。
- 稳定性:不稳定。
- 堆排序(Heap Sort)
- 原理:利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
- 时间复杂度:O(n log n)。
- 空间复杂度:O(1)。
- 稳定性:不稳定。
- 计数排序(Counting Sort)
- 原理:不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
- 时间复杂度:O(n + k),其中 k 是数据范围。
- 空间复杂度:O(k)。
- 稳定性:稳定。
- 桶排序(Bucket Sort)
- 原理:将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
- 时间复杂度:平均为 O(n + k),最坏情况为 O(n^2)。
- 空间复杂度:O(n + k)。
- 稳定性:稳定。
- 基数排序(Radix Sort)
- 原理:按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。
- 时间复杂度:O(nk),其中 k 是数字的最大位数。
- 空间复杂度:O(n + k)。
- 稳定性:稳定。
总结
- 时间复杂度:O(n log n) 的算法有归并排序、堆排序、快速排序等。
- 稳定性:稳定的排序算法包括冒泡排序、插入排序、归并排序、计数排序、桶排序和基数排序等。
- 空间复杂度:O(1) 的算法包括冒泡排序、选择排序、插入排序、希尔排序和堆排序等。