2
DCU技术达人
新人贴
gfx936 DCU上实现INT8 KV与INT8 MMAC Attention推理优化
2026-09-28 17:32:49
13
# 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对这三个点展开具体分析。 本文代码仓库:[https://github.com/sinpeyw/qwen3.5-vllm-dcu-optimization](https://github.com/sinpeyw/qwen3.5-vllm-dcu-optimization) ### 1.1 HBM容量收益 不考虑张量并行时,一层 Attention 的 KV 数据量为: ```text 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 数据是: ```text 2 × 4 × 256 × 2 Byte = 4096 Byte ``` 将 K/V 主数据改为 INT8 后,这部分变为: ```text 2 × 4 × 256 × 1 Byte = 2048 Byte ``` 此外,还需要为每个 token、每个 KV 头分别保存 K 和 V 的 FP32 Scale,还需要: ```text 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`,只考虑这部分逻辑负载: ```text BF16 KV traffic = 4096 × L Byte INT8 KV traffic = 2080 × L Byte traffic ratio = 50.78% ``` HBM带宽不变,相同时间读取元素个数提升: ```text 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 已完成量化,两次乘法可以写成: ```text 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 计算。一条容易实现但很难加速的路径是: ```text BF16 K/V → 独立量化 kernel → INT8 Cache → 完整反量化为 BF16 → 原有 BF16 Attention ``` 这条路径可能减少持久缓存容量,却增加了 BF16 读取、INT8 写入、Scale 读写、反量化和额外 kernel launch。更关键的是,QK 和 PV 仍由 BF16 矩阵指令完成,gfx936 的 INT8 MMAC 峰值没有真正被利用。在单请求场景中,新增开销很容易超过节省的 KV 读取时间。 能够产生性能收益的数据流应当更接近: ```text 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`: ```text amax = max(abs(x[i])) scale = amax / 127 q[i] = clamp(round(x[i] / scale), -127, 127) ``` 反量化关系为: ```text 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 中完成: 1. 读取 Q/K/V; 2. 对 Q/K 应用 RoPE,或消费已经旋转后的 Q/K; 3. 在 wave 内归约 Q/K/V 的 absmax; 4. 计算 Scale 并量化; 5. 将 K/V 写入分页 INT8 Cache; 6. 写入 K/V Scale; 7. 在纯 INT8 Attention 路径中跳过无用的 BF16 Q/K 回写。 内核中的主数据流可以简化为: ```cpp 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 元素分块: ```text K cache: [blocks, 4, 16, block_size, 16] ``` 这样,QK 中负责 token 方向的 lane 可以为固定维度组织合并读取。V 保持 token 方向连续: ```text V cache: [blocks, 4, 256, block_size] ``` PV 中,一个 lane 可以连续取得多个 token 在同一输出维度上的 V。两种布局都围绕 16 字节搬运设计,避免把 INT8 的带宽优势消耗在大量标量 byte load 上。 Scale 独立存储为: ```text 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。 其计算过程可以概括为: ```text 定位分页 K/V → INT8 QK MMAC → Scale 恢复与 online softmax → 概率分块量化 → INT8 PV MMAC → 分区归并并输出 BF16 ``` QK 和 PV 的关键不是在 MMAC 前生成完整 BF16 K/V,而是在局部累加结果上恢复 Scale。一个简化的 32-token fragment 计算如下: ```cpp 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 状态和输出。 最终的路由可以概括为: ```text 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适配》](https://blog.csdn.net/Foreverahead/article/details/163175989?spm=1001.2014.3001.5501)和第三篇[《gfx936 DCU上实现INT8 PV MMAC:V Scale融合、概率量化与Fragment分组》](https://blog.csdn.net/Foreverahead/article/details/163176013?spm=1001.2014.3001.5501)将分别展开 QK 与 PV 的内核实现。
全部评论(2)
只看楼主
暂无数据
--
楼主
Level.1
0 帖子
楼主热帖
暂无数据