gfx936 DCU上实现INT8 KV与INT8 MMAC Attention推理优化
前言
我们在一张gfx936 DCU上为Qwen3.5-27B做了一轮完整的INT8 KV与 INT8 MMAC Attention实现与优化,从在线量化、分页存储,一直做到低精度运算的INT8 QK、INT8 PV 。
比赛环境:
| 项目 | 配置 |
|---|---|
| 加速卡 | 单卡 gfx936,64 GiB 显存 |
| 软件栈 | DTK 26.04、Python 3.10、PyTorch 2.10.0 |
| 推理框架 | 赛事版 vLLM,基于 v0.18.1 修改 |
| 模型 | Qwen3.5-27B,单卡运行 |
| 服务方式 | OpenAI 兼容接口,请求串行执行,max_concurrency=1 |
整体思路:
- 首个Prefill Chunk:使用成熟的高精度 Attention算子计算,但使用融合的 KV 生产内核把 K/V 写成 INT8存储,留给后续阶段复用。
- 非首个Prefill Chunk:把 Query也量化为 INT8,让历史和当前块的 INT8 K/V 在同一个online softmax内核里完成 INT8 QK 和 INT8 PV。
- Decode 阶段:使用 INT8 KV,并以
v_mmac_i32_16x16x32_i8重写 QK/PV 低精度计算,按 256 token 分区。 - 配套优化:实现融合 RoPE、Q/K/V 动态量化、Scale 写入和分页 KV 写入等优化。
最终效果:初步版本在官方评测相对量化前的提交提高约 0.4 分,精度扣分仍为 0,实现了正向收益。但由于时间原因,量化引入的很多额外开销尚且未作深度优化和消除。(折腾了四五天,比赛截止前几个小时才把正确的量化版本跑通且有正向收益 /(ㄒoㄒ)/~~)
本文先分析INT8 KV和INT8 MMAC Attention算子的理论收益,然后分别介绍如何实现。
1. INT8 KV 的理论性能收益来源:结合DCU的MMAC指令
讨论 INT8 KV Cache量化时,很多人想到的收益仅仅是“显存容量“,可以容纳更多的KV和组更大batch进行计算。但其实远非如此。INT8 KV量化,还可以让元素位宽减半,单次HBM读取指令的元素翻倍,访存速度近似翻倍。同时,结合使用INT8的MMAC指令,相对于BF16的MMAC指令,该指令算力峰值近似翻倍, 计算速度翻倍。
比赛中很多人认为INT8 KV没有收益,也有不少人实现完成了KV Cache量化,却没有取得端到端收益。这通常是因为只看到了INT8 KV所带来的”显存容量“这一点,且恰好在比赛的单请求的特定测试负载下,显存容量并非瓶颈,自然并无法转化为端到端收益。但是对于后两点来说,即便在单请求的负载下,仍然是可以取得一些收益的,却被很多人忽视了。
下面结合DCU对这三个点展开具体分析。
1.1 HBM容量收益
不考虑张量并行时,一层 Attention 的 KV 数据量为:
KV_bytes = 2 × sequence_length × num_kv_heads × head_dim × bytes_per_element
Qwen3.5-27B 是混合结构,共 64 层,其中 16 层使用全 Attention。全 Attention 部分采用 24 个 Query 头、4 个 KV 头,head dimension 为 256。因此,每个 token、每个全 Attention 层的原始 BF16 KV 数据是:
2 × 4 × 256 × 2 Byte = 4096 Byte
将 K/V 主数据改为 INT8 后,这部分变为:
2 × 4 × 256 × 1 Byte = 2048 Byte
此外,还需要为每个 token、每个 KV 头分别保存 K 和 V 的 FP32 Scale,还需要:
2 × 4 × 4 Byte = 32 Byte
所以KV逻辑大小从 4096 Byte 降到 2080 Byte,约为 BF16 的 50.78%。
为何比赛负载没有收益:
评测负载固定为 max_concurrency=1,单请求最长上下文为 32K。按 16 个全 Attention 层计算,BF16 KV Cache 的最大容量需求约为 4096 Byte × 32768 × 16 = 2 GiB。单卡 DCU 显存约为 63.98 GiB,实测模型权重加载占用 50.38 GiB,仅扣除权重后理论上仍剩约 13.60 GiB。即使还需为计算图、激活值和算子工作区保留显存,KV Cache 容量也足以容纳单个 32K 请求。因此在本次比赛负载下,显存容量并非瓶颈,INT8 KV 带来的存储容量缩减无法直接转化为吞吐收益。
何种负载下有收益:负载允许组batch,且显存容量限制了batch的大小;prefix cache命中率高,显存可以保留更多的历史KV来复用;显存容量是瓶颈。
1.2 HBM带宽收益
按前面的布局,每个历史 token、每个全 Attention 层需要读取的 BF16 K/V 主数据为 4096 Byte。改成 INT8 后,K/V 主数据与 FP32 Scale 合计为 2080 Byte。若历史长度为 L,只考虑这部分逻辑负载:
BF16 KV traffic = 4096 × L Byte
INT8 KV traffic = 2080 × L Byte
traffic ratio = 50.78%
HBM带宽不变,相同时间读取元素个数提升:
4096 / 2080 ≈ 1.97x
对于Prefill来算,Prefill Attention是计算瓶颈,提升KV读取速度,整体收益不明显。Prefill如果分chunk之后,非首个Prefill chunk同样需要读取已有 KV,理论也能获得一部分 HBM 收益,但是可能仍然是计算瓶颈,收益不大。
对于Decode来说, 每一步只产生一个新 Query,却要读取此前所有 token 的 K/V,因此Attention瓶颈在KV的HBM访存,且上下文越长,访存瓶颈越加剧。INT8 KV将读取的数据量降低一半,提升读取速度,理论上有收益。
但 1.97x 只是 K/V 有效负载的带宽上限,不是 Decode Attention 的预期加速比。实际路径还需要读取 block table 和 Scale,维护 online softmax 状态,并执行分页寻址、分区归并和输出写回,这些流量与计算不会随 K/V 位宽一起减半。只有 Attention 内核直接读取分页 INT8 K/V,并在 MMAC 累加结果和 online softmax 数据流中融合应用 Scale,减少的 K/V 字节数才能真正进入时延账;如果先反量化并写出一份完整 BF16 K/V,中间张量的额外写入和再次读取会重新消耗 HBM 带宽。第 3 节将具体介绍 INT8 KV 的直接消费路径
1.3 Tensor Core 峰值收益
DCU 的计算单元不仅包含普通标量和向量运算通路,还提供面向矩阵乘加的专用矩阵计算单元,称为 Tensor Core;在 gfx936 的指令层,它通过 MMAC指令体现。MMAC 不是让单个线程独立计算一个小矩阵,而是由一个 wave64 协同提供输入 fragment,并把一个矩阵输出块的累加结果分布在各个 lane 的 VGPR 中。这样可以用较少的指令完成高密度点积,也是我们重写 QK 和 PV 的硬件基础。
BF16 基线路径涉及的指令是 v_mmac_f32_16x16x16_bf16。它以 BF16 作为输入,由一个 wave 计算 16×16×16 的矩阵块,并使用 FP32 累加:前两个 16 表示输出块的 M、N 维,最后一个 16 表示单条指令覆盖的 K 维。
INT8 路径使用 v_mmac_i32_16x16x32_i8。它同样生成 16×16 输出块,但输入改为 INT8、累加器改为 INT32,单条指令覆盖的 K 维从 16 增加到 32。
也就是说,在相同输出块和相近指令发射条件下,INT8 MMAC 一次可以完成两倍的乘加元素数,因此 gfx936 的 INT8 MMAC 理论峰值约为 BF16 MMAC 的两倍。后续 Scale 恢复和浮点 softmax 则在矩阵累加之后完成。
QK 和 PV 是Attention主要的矩阵计算。若 Q、K 和 V 已完成量化,两次乘法可以写成:
Score ≈ MMAC_INT8(Q_int8, K_int8^T) × sQ × sK / sqrt(d)
Output ≈ MMAC_INT8(P_int8, V_int8) × sP
第二个式子是简写。实际实现先把每个 token 的 sV 融入 softmax 权重,再对 P × sV 分块量化,因此最终恢复的是概率块的 Scale,不能在整个 PV 结束后统一乘一个 V Scale。
对于 head dimension 256,INT8 QK 的 K 维需要 8 个 MMAC step,BF16 路径需要 16 个 step;PV 也采用相同的低精度矩阵核心。
在 Prefill Attention中,QK/PV 计算占主导,INT8 存在接近两倍峰值算力的理论空间。Decode 也使用同样的 INT8 QK/PV,但单请求 Decode Attention更受历史 KV 读取限制,Tensor Core 峰值通常不是它的第一收益来源。
同样,峰值算力比不等于 Attention 内核的实际加速比。MMAC 的 K-step 减半,只加速 QK 和 PV 中的矩阵乘加部分;RoPE、online softmax、分页寻址和输出转换原本就存在,也不会因此变快。INT8 路线还会额外引入 absmax 归约、Scale 计算、除法、舍入以及 PV 概率再量化。如果这些新增操作独立执行,或者产生额外的全局内存中间结果,其成本可能超过 INT8 MMAC 节省的计算时间。在后面,第 2章将介绍如何融合KV量化生产过程,第 3章则介绍如何把 Scale 应用、softmax和 INT8 QK/PV 组织在同一条消费链路中。
1.4 为什么不少 INT8 KV Cache 实现没有收益
只把 KV Cache 的存储类型改成 INT8,并不等于 Attention 已经使用 INT8 计算。一条容易实现但很难加速的路径是:
BF16 K/V
→ 独立量化 kernel
→ INT8 Cache
→ 完整反量化为 BF16
→ 原有 BF16 Attention
这条路径可能减少持久缓存容量,却增加了 BF16 读取、INT8 写入、Scale 读写、反量化和额外 kernel launch。更关键的是,QK 和 PV 仍由 BF16 矩阵指令完成,gfx936 的 INT8 MMAC 峰值没有真正被利用。在单请求场景中,新增开销很容易超过节省的 KV 读取时间。
能够产生性能收益的数据流应当更接近:
RoPE + absmax + 量化 + INT8 KV 写入
→ Attention 直接读取分页 INT8 K/V
→ INT8 QK MMAC
→ online softmax 与 Scale 恢复
→ 概率分块量化
→ INT8 PV MMAC
这要求重构 Attention 内核,而不只是替换 Cache dtype。Prefill Attention 的 QK/PV 更偏计算受限,INT8 MMAC 的理论峰值约为 BF16 MMAC 的两倍,因此重点是让两次矩阵乘法都原生进入 INT8 指令。Decode Attention 更偏显存带宽受限,重点是直接读取 INT8 K/V,将历史主数据流量约减半。两条路径的收益来源不同,但都要求避免完整反量化中间张量。
我们最终围绕这两类成本重构了完整数据流:
- KV 不再先写入 BF16 Cache 后二次量化,而是在 producer 中融合 RoPE、absmax、动态量化、Scale 写入和分页 INT8 KV 写入;Query 只量化一次,并由后续分段复用。
- 消费端不生成完整 BF16 K/V:QK 将 INT32 点积结果与 sQ×sK 融合恢复,PV 则先把每个 token 的 sV 融入 softmax 权重,再对新的权重块进行量化并执行 INT8 MMAC。
- 这样,INT8 数据从 Cache 写入一直保持到 QK/PV 的矩阵计算阶段,避免了完整反量化和额外中间张量。
下面第2章将展开这条数据流的INT8 KV生产端,第3章介绍 Decode 和带历史KV的 Prefill Chunk如何直接消费这些 INT8 数据。
2. INT8 KV 如何产生
INT8 KV 的生产位置和方式会直接决定它是否有性能价值。首先应该否定的一种朴素做法是:按原路径写出 BF16 KV,再启动一个独立 kernel 扫描缓存并转成 INT8。因为这样会多读一遍 BF16、多写一遍 INT8,还会增加临时空间和 kernel launch,性能退化无法接收。
我们的核心思路是:把量化并入原本就要执行的 KV 生产阶段。
2.1 动态量化粒度:要生成什么样的数据
我们采用对称 INT8、per-token、per-head Scale。对一个长度为 D 的向量 x:
amax = max(abs(x[i]))
scale = amax / 127
q[i] = clamp(round(x[i] / scale), -127, 127)
反量化关系为:
x[i] ≈ q[i] × scale
量化范围使用 [-127, 127],以保持正负对称。amax=0 时显式给出安全 Scale 或全零输出,避免除零。
per-token、per-head 并不是理论上误差最小的粒度和最优方法。很多文章指出(如KIVI) ,K 中常有稳定的通道离群值,K 更适合 per-channel/group,V 更适合 per-token,并提出采用非统一的量化方法。这里选择统一规则,一方面是因为简单,它在 gfx936 上具有固定的归约范围、简单的 Scale 寻址和较低的运行时开销,精度风险再通过下游任务验证约束。另外是因为作者时间有限,尚且未来得及探索复杂方法(/(ㄒoㄒ)/~~)。
需要注意,“反量化关系”只是定义数值近似关系。我们的最终实现中不会真的生成完整 BF16 K/V,这一点后文会继续解释。
2.2 融合 RoPE、量化与分页写入:什么时候、怎样生成
Q/K/V 投影完成后,Q/K 需要经过 RoPE,K/V 需要写入分页 Cache。本文将这段数据流称为 KV producer。为了避免先写 BF16 KV、再启动独立量化 kernel,最终实现把动态量化直接融合进 producer。
最终 KV 生产内核在一次 HIP kernel 中完成:
- 读取 Q/K/V;
- 对 Q/K 应用 RoPE,或消费已经旋转后的 Q/K;
- 在 wave 内归约 Q/K/V 的 absmax;
- 计算 Scale 并量化;
- 将 K/V 写入分页 INT8 Cache;
- 写入 K/V Scale;
- 在纯 INT8 Attention 路径中跳过无用的 BF16 Q/K 回写。
内核中的主数据流可以简化为:
float4 q = load4(query, token, q_head);
float4 k = load4(key, token, kv_head);
float4 v = load4(value, token, kv_head);
q = apply_rope(q, position);
k = apply_rope(k, position);
if (quantize_query)
wave_quantize_store(q, query_int8, q_scale);
wave_quantize_store(k, paged_key_cache[slot], k_scale[slot]);
wave_quantize_store(v, paged_value_cache[slot], v_scale[slot]);
首个Prefill Chunk只生成 INT8 K/V,当前 Attention仍走高精度路径,Query保留在高精度数据流中;带历史 Prefill Chunk和 Decode则同时量化 Query。Query只量化一次,随后可被多个历史分区(split-K)复用,KV 的分页槽位映射也只计算一次。融合的价值不只是少启动一个 kernel,更重要的是避免把中间 BF16 数据重新写回显存再读出。
同一 producer需要同时覆盖单 token Decode和数千 token Prefill。最终实现按 token 数选择最优并行配置:1、2、超过 2 个 token分别使用 1、2、8 个 wave,避免小形状承担过多同步开销,也避免大形状并行度不足。
2.3 物理布局与显存管理:量化数据如何排列存储
量化数据产生后,还需要同时解决两个问题:怎样排列才能被 QK/PV 高效读取,以及怎样接入 vLLM 原有的分页缓存和显存规划。前者决定 Attention 内核的实际访存效率,后者决定服务能否在固定显存预算下稳定启动。
QK 和 PV 对数据读取方向的要求不同,因此 K 和 V 没有强行使用相同布局。K 按 16 个 head-dimension 元素分块:
K cache: [blocks, 4, 16, block_size, 16]
这样,QK 中负责 token 方向的 lane 可以为固定维度组织合并读取。V 保持 token 方向连续:
V cache: [blocks, 4, 256, block_size]
PV 中,一个 lane 可以连续取得多个 token 在同一输出维度上的 V。两种布局都围绕 16 字节搬运设计,避免把 INT8 的带宽优势消耗在大量标量 byte load 上。
Scale 独立存储为:
K/V scale: [blocks, block_size, 4], FP32
需要注意的是,由于前文已经分析INT8 KV容量并不能在比赛负载中带来收益,因此为了简单和兼容性起见,作者在初版代码并未为INT8 量化重构KV页的结构。仍沿用 BF16 大小的物理页面,只在页面前半部分存放紧凑 INT8 数据,Attention 内核只读取这部分有效数据。此时能够取得读取速度收益,但页面的物理分配没有减半。
KV 数据区仍占 BF16 页面分配的 100%,再加约 0.78% 的 Scale,每个逻辑 token 的持久分配约为 BF16 的 100.78%。代码中的为 K/V Scale 各创建 [num_blocks, block_size, 4] 的 FP32 tensor;用固定显存预算主动扣除 Scale空间,而不是依赖启动参数预留。
如果测试负载有组batch,或者有prefix cache,可以进一步考虑为INT8量化专门设计KV 页结构。
3. INT8 KV 如何被消费:INT8 MMAC指令
将 KV 写成 INT8 只完成了一半工作。如果消费端先把整份缓存恢复成 BF16,再调用普通 Attention,省下的存储会被反量化和中间张量开销抵消。
作者的核心思路是:使用DCU的低精度INT8 MMAC相关指令,改造内核,Attention 内核直接从分页缓存加载 INT8 K/V fragment,并将其送入 MMAC,整个过程中不生成完整 BF16 K/V 中间张量。这样既避免了反量化带来的开销,也能利用INT8 MMAC指令的翻倍算力峰值。
3.1 首个Prefill Chunk:高精度计算,融合写入 INT8 KV
首个 Prefill 块没有历史KV缓存可读。最终实现继续调用成熟的高精度 Attention,只让融合 producer 把当前 K/V 写成 INT8,为后续 Prefill 和 Decode 复用。这样既保留首块高精度内核的调度优势,也没有再运行独立的量化 kernel。
3.2 非首个Prefill Chunk:在一次 online softmax 中消费
分块 Prefill 已经积累至少 4096 token KV后,producer 同时生成 INT8 Query,并由 gfx936_int8_prefix_attention 消费历史KV与当前块的 INT8 K/V。Q、历史 K/V 和当前 K/V 在同一个 Attention 内完成 INT8 QK、online softmax 和 INT8 PV。
注意,不能让历史 INT8 KV 和当前 BF16 K/V 分别计算两次 Attention,再依靠两个 log-sum-exp 状态合并。数学上可以完成合并,但实现上会增加一次 Attention 数据流、状态写回和归并,使本来有限的 INT8 收益被额外开销吃掉。最终路径因此坚持在一个 online softmax 生命周期中处理完整上下文。
3.3 Decode:直接读取分页 INT8 KV
paged_attention_int8_kv 接收分页 INT8 K/V、逐 token Scale、INT8 Query 和 Query Scale。block table 只负责定位物理页,K/V fragment 从页面进入寄存器或 LDS 后直接送入 MMAC,中间不生成完整 BF16 K/V tensor。
其计算过程可以概括为:
定位分页 K/V
→ INT8 QK MMAC
→ Scale 恢复与 online softmax
→ 概率分块量化
→ INT8 PV MMAC
→ 分区归并并输出 BF16
QK 和 PV 的关键不是在 MMAC 前生成完整 BF16 K/V,而是在局部累加结果上恢复 Scale。一个简化的 32-token fragment 计算如下:
int32x4 qk = mmac_int8(q_int8, k_int8);
float score = float(qk) * q_scale * k_scale[token] * softmax_scale;
float p = online_softmax_update(score);
float p_with_v_scale = p * v_scale[token];
float p_scale = block_absmax(p_with_v_scale) / 127.0f;
int8x8 p_int8 = quantize(p_with_v_scale, p_scale);
int32x4 pv = mmac_int8(p_int8, v_int8);
output += float(pv) * p_scale;
这种路径同时使用了两类收益:历史 K/V 主数据读取量降低,QK 和 PV 也直接进入 v_mmac_i32_16x16x32_i8。按 256 token 分区后,各工作组独立处理一段历史上下文,再合并 softmax 状态和输出。
最终的路由可以概括为:
Q/K/V(BF16)
├─ 首个Prefill Chunk
│ 高精度 Attention
│ + 融合 RoPE、动态量化和 INT8 K/V 写入
├─ 非首个Prefill Chunk
│ INT8 Q + 历史/当前 INT8 K/V
│ + 单内核 INT8 QK、online softmax、INT8 PV
└─ Decode
分页 INT8 K/V
+ INT8 QK、online softmax、INT8 PV
4. BW1000 DCU 实测结果
局部 MMAC 更快,不代表服务一定同比加速。我们把测试边界分为三级:单个算子、完整 Attention 层数据流,以及真实服务端到端。下文的加速比均定义为“对照耗时 / 候选耗时”,不同表格的对照边界不相同。
4.1 算子级收益对比
先看 INT8 路线中三个可以独立计时的核心算子。
| 算子边界 | 对照 | INT8 候选 | 实测结果 |
|---|---|---|---|
| QK 内层矩阵块 | BF16 QK | INT8 QK MMAC | 长上下文约 1.55x-1.63x;256-token 小块接近持平 |
| PV 内层计算 | BF16 PV | INT8 PV MMAC | 4K/8K/16K 为 1.1049x/1.0979x/1.2653x |
| KV producer | 未消除重复写回的生产路径 | 融合 RoPE、量化和分页写入 | 32.747 us -> 12.122 us,即 2.7016x |
QK 和 PV 的结果说明,两次低精度矩阵乘法本身都有收益;producer 的结果则说明,量化开销能否被融合掉同样关键。
producer 的 workgroup 形状也会改变结果。一个 wave 是 gfx936 上锁步执行的 64 个线程;下表固定以 BF16 producer 为对照,只改变 INT8 producer 的 workgroup wave 数。旧自动策略在大 Prefill 块上选择 4 waves,优化后改为 8 waves。
| token 数 | 旧 4-wave 配置相对控制组 | 优化后 8-wave 配置相对控制组 |
|---|---|---|
| 2242 | 0.7455x | 1.1269x |
| 4096 | 0.7351x | 1.1155x |
小 token 下,各配置只有零点几微秒差异;真正有信息量的是数千 token 的 Prefill 块。旧 4-wave 路径明显回退,改为 8 waves 后才恢复正收益。最终 producer 按 token 数选择 1/2/8 个 wave,分别覆盖 1、2、超过 2 个 token 的形状。
4.2 Attention 层收益对比
算子成立后,还要把 RoPE、动态量化、KV 写入、QK、online softmax、PV 和输出放进更完整的计时边界。这里涉及两个容易混淆的概念。
第一,“完整生命周期”不仅包含 Attention,还包含生成它所需数据的前置工作。BF16 对照执行 RoPE、写入 BF16 K/V,再调用 BF16 Attention;INT8 候选执行 RoPE、Q/K/V 动态量化、写入 INT8 K/V 和 Scale,再调用使用 INT8 QK/PV 的 Attention。
第二,INT8 Attention 候选本身还经历了一次并行组织调优。早期内核中,一个 workgroup 处理 64 个 Query 行;改进后由 8 个 wave 协同处理 128 个 Query 行,使同一份 K/V tile 可以被更多 Query 复用。此前代码和测试中把两者简称为 QTile64 和 QTile128,本文后面直接称为“64-query-row 内核”和“128-query-row 内核”。二者都使用 INT8 QK/PV,这组比较不涉及 BF16 对照。
| 比较层级 | 对照 | 候选 | 计时范围 | 加速 |
|---|---|---|---|---|
| Attention 完整生命周期 | RoPE + BF16 K/V 写入 + BF16 Attention | RoPE + 动态量化 + INT8 K/V、Scale 写入 + 128-query-row INT8 Attention | 数据生产与 Attention 消费 | 1.0613x |
| INT8 Attention 内核调优 | 64-query-row INT8 Attention | 128-query-row INT8 Attention | QK、online softmax、PV 和输出 | 1.3158x |
真正回答“从 BF16 改成 INT8 是否仍然更快”的是第一行:加入 absmax、Scale、量化和缓存写入后,完整生命周期仍有 1.0613x 的收益。第二行的 1.3158x 只说明 128-query-row 内核优于旧的 64-query-row INT8 内核,不能把它当作 INT8 相对 BF16 的加速比。
在Prefill中,通常分为多个Chunk执行,并非一开始就使用INT8 Attention最好。为了决定从哪个Chunk开始启用 INT8,我们固定采用“BF16 完整生命周期”为对照,将各历史长度下的“全 INT8 完整生命周期”作为候选:
| 历史上下文 + 当前块 | 对照 | 候选 | 加速(BF16 耗时 / INT8 耗时) |
|---|---|---|---|
0 + 4K | BF16 数据生产 + BF16 Attention | INT8 数据生产 + INT8 Attention | 0.9140x |
4K + 4K | BF16 数据生产 + BF16 Attention | INT8 数据生产 + INT8 Attention | 1.0298x |
8K + 4K | BF16 数据生产 + BF16 Attention | INT8 数据生产 + INT8 Attention | 1.0801x |
12K + 2242 | BF16 数据生产 + BF16 Attention | INT8 数据生产 + INT8 Attention | 1.1168x |
Prefill首块的 0.9140x 表明在线量化成本尚未被覆盖;历史越长,INT8 QK/PV 和 KV 读取收益越明显。因此最终路由没有全局启用 INT8,而是让首块继续使用高精度 Attention,同时写入 INT8 K/V 供后续复用;历史达到 4K 后,再启用完整 INT8 Attention。将这些已测形状按最终路由组合,得到前面的 1.0705x。
排查 Decode 时还额外测试了一组用于batch负载的数据。这里的 BF16 对照同样包含 RoPE、BF16 KV 写入和 BF16 Attention;INT8 候选包含融合动态量化、INT8 KV 写入以及 INT8 QK/PV Attention。
| 历史长度 | 合成 batch | BF16 生命周期 | INT8 生命周期 | 加速 | 最大绝对误差 |
|---|---|---|---|---|---|
| 8K | 2 | 425.504 us | 100.256 us | 4.2442x | 0.001587 |
| 16K | 2 | 432.240 us | 160.384 us | 2.6950x | 0.000854 |
| 8K | 10 | 472.224 us | 340.960 us | 1.3850x | 0.001343 |
| 16K | 10 | 837.903 us | 617.728 us | 1.3564x | 0.000977 |
这些数据证明了合成 batch 形状下的 INT8 KV Attention 层生命周期具有收益,而且收益并不随 batch 单调增加。但是初赛吞吐脚本固定 max_concurrency=1,两条或十条请求只是串行样本数,不会形成服务 batch。
4.3 服务端到端收益对比
端到端测试统一比较两条服务路径。BF16 对照保留相同的模型、推理框架和非量化优化,K/V 存储与 Attention 计算均使用 BF16;INT8 候选在此基础上启用最终集成数据流:首个 Prefill Chunk 使用高精度 Attention 并写入 INT8 K/V,后续 Prefill Chunk 和 Decode 直接消费 INT8 K/V,以 INT8 MMAC 完成 QK/PV,同时启用融合 RoPE、动态量化和分页写入。
需要注意的是,一次单请求 hipprof 中,Prefill 和 Decode Attention 分别只占所记录 HIP 内核时间的 7.556% 和 3.918%;将包含量化与缓存写入成本的完整路径收益代入 Amdahl 定律,可兑现到完整请求的理想收益大约只有 0.4%-0.8%,因此端到端上限本来就不高。
下面只保留能够说明最终路线收益的测试。负百分比表示时延下降。
| 验证范围 | BF16 对照 | 最终 INT8 候选 | 结果 |
|---|---|---|---|
| 8K-16K,若干串行请求 | 输出吞吐 7.991030 tok/s,平均 TTFT 3784.290 ms,平均 TPOT 47.755 ms | 输出吞吐 8.033833 tok/s,平均 TTFT 3773.134 ms,平均 TPOT 47.292 ms | 两组输出长度持平;吞吐提高 0.536%,平均 TTFT 降低 0.295%,平均 TPOT 降低 0.970% |
| 官方平台提交 | 保留相同非量化优化、尚未启用本文 INT8 KV 数据流的提交版本 | 启用完整 INT8 KV 写入与消费路径的最终版本 | 最终得分提高约 0.4 分,精度扣分保持为 0 |
本地完整精度测试共 109 条。INT8 候选在 HotpotQA、GovReport、Retrieval 和 Aggregation 上分别得到 77.96/32.97/100/100,BF16 对照为 77.96/32.83/100/100,没有观察到精度下降。官方平台测量的提分足以确认整条 INT8 数据流取得了小幅端到端正收益。
5. 踩坑经验
5.1 只压缩存储,没有使用原生 INT8 计算
早期路径从 INT8 Cache 读取 K,在寄存器中转成 BF16,再调用 BF16 MMAC。真实 Qwen tile 上只有 BF16 路径的 0.71x-1.00x:K 的读取量虽然减少,但转换和 Scale 开销抵消了收益,QK 也没有使用 INT8 MMAC。INT8 KV 要取得性能收益,消费端必须直接使用低精度数据,不能在 Attention 前恢复完整 BF16 K/V。
5.2 在错误的阶段强制启用 INT8
Prefill阶段曾被强制改成“先写 INT8 页面,再从页面读取做全 INT8 Attention”,结果输出吞吐下降 26.954%,平均 TTFT 增加约 59.2%,平均 TPOT 增加约 2.0%。首块没有历史 KV 的带宽收益,却要承担完整在线量化和分页读取成本。
另一条路径让历史 INT8 KV 与当前 BF16 K/V 分别计算两次 Attention,再合并两个 LSE 状态,完整生命周期为 39.151 ms 对 13.332 ms,只有 0.3405x。最终方案保留首块高精度 Attention,并让带历史的当前块与历史 KV 在同一个 online softmax 状态中完成计算。
5.3 局部内核已经加速,生产路由却没有生效
一次合成单序列生命周期测试在 8K/16K 下达到 5.6363x/3.7543x,随后同容器服务测试却回退 2.034%。检查运行日志后发现,Qwen3.5 在 RoPE 前还有 Q/K RMSNorm,原有图匹配模式没有命中真实调用点,测得的融合 producer 根本没有进入服务路径。后来通过真实调用点接入、Meta/FakeTensor 支持、编译缓存身份隔离和运行时路由日志才确认候选真正生效。
5.4 跑得很快,也可能只是少算了数据
INT8 PV 的早期实现按连续 0-31、32-63 token 计算 Scale,却没有对齐 MMAC fragment 的真实 token 归属。错误版本把正常的 [95,12] 输出变成 [1024,1024],表面吞吐从约 7.9968 升到 19.2006 tok/s。这不是优化,而是错误概率分组改变了生成和停止行为。
类似地,8 Byte V load 只读取了 lane 所需数据的一半,partition 512 也曾越过正确 fragment 映射。低精度内核必须同时验证数学参考、硬件 fragment、跨页地址和输出文本,不能只检查 kernel 是否完成以及结果是否为有限数。
5.5 输出长度会掩盖真实执行性能
一次两请求测试表面吞吐提高 2.4196%,但候选多生成了 5 个 token,整体执行时间反而增加;按控制输出长度和候选 TTFT/TPOT 粗略归一化后约为 -0.40%。相反,前面的十请求测试虽然 TPOT 改善约 1.24%,却因少生成 43 个 token 而得到负的原始吞吐。
5.6 旁路显存必须进入统一预算
INT8 K/V 主数据变小,并不意味着可以忽略 Scale、Query 工作区和分区归并空间。早期旁路分配没有完整进入 KV planner,平台曾在服务初始化阶段因缺少少量连续显存而 OOM。最终实现让 planner 显式计入每 token、每层 32 Byte 的 K/V Scale,并复用 Query 和 Decode workspace,同时保留必要的显存余量。
后续文章
本文介绍完整 INT8 KV Attention 方案。后续第二篇《gfx936 DCU上实现INT8 QK MMAC:分页访存、Fragment映射与GQA适配》和第三篇《gfx936 DCU上实现INT8 PV MMAC:V Scale融合、概率量化与Fragment分组》将分别展开 QK 与 PV 的内核实现。

284

被折叠的 条评论
为什么被折叠?



