大模型推理引擎vLLM(25): 从--kv-cache-dtype fp8_e5m2时gsm8k答非所问的bug梳理kv cache相应代码片段

目录

[1 E4M3 E5M2的问题](#1 E4M3 E5M2的问题)

[2 BF16 FP16](#2 BF16 FP16)

[2.1 fp16(IEEE half,半精度)](#2.1 fp16(IEEE half,半精度))

[2.2 bf16(bfloat16,Brain Float)](#2.2 bf16(bfloat16,Brain Float))

[2.3 区别对比](#2.3 区别对比)

[3 VLLM里面的kv cache的量化与反量化过程](#3 VLLM里面的kv cache的量化与反量化过程)

[4 先梳理一下读取KV CACHE的代码大体流程:以后会详细梳理整个kv cache相关完整代码,这次只看一部分](#4 先梳理一下读取KV CACHE的代码大体流程:以后会详细梳理整个kv cache相关完整代码,这次只看一部分)

[4.1 Host 入口:谁发起「读 KV 做 Attention」](#4.1 Host 入口:谁发起「读 KV 做 Attention」)

[4.2 宏分发:把 "fp8_e5m2" 绑到 kFp8E5M2](#4.2 宏分发:把 "fp8_e5m2" 绑到 kFp8E5M2)

[4.3 宏链展开:一路传到 kernel 启动](#4.3 宏链展开:一路传到 kernel 启动)

[4.4 GPU 核函数外壳](#4.4 GPU 核函数外壳)

[4.5 核心逻辑:从 cache 读 K(uint8)→ 反量化 → QK](#4.5 核心逻辑:从 cache 读 K(uint8)→ 反量化 → QK)

[4.5.1 定义向量类型(一次读 16 字节)](#4.5.1 定义向量类型(一次读 16 字节))

[4.5.2 从 paged k_cache 读出一段 FP8](#4.5.2 从 paged k_cache 读出一段 FP8)

[4.5.3 反量化成 K_vec(bf16 向量)--- 整条 bug 链的入口](#4.5.3 反量化成 K_vec(bf16 向量)— 整条 bug 链的入口)

[4.5.4 用反量化后的 K 和 Q 算分](#4.5.4 用反量化后的 K 和 Q 算分)

[4.6 scaled_convert:统一入口](#4.6 scaled_convert:统一入口)

[4.7 scaled_vec_conversion:向量拆包,最后落到单字节 E5M2 反量化](#4.7 scaled_vec_conversion:向量拆包,最后落到单字节 E5M2 反量化)

[4.8 最底层:E5M2 字节 → float 也是这次新增加的函数](#4.8 最底层:E5M2 字节 → float 也是这次新增加的函数)

参考文献:


提问、abstract:

exponent 英/ ɪkˈspəʊnənt / 美/ ɪkˈspoʊnənt /

  • n.拥护者,倡导者;代表人物;指数,幂;大师,行家;成分;说明者,说明物
  • adj.说明的

mantissa 英/ mænˈtɪsə / 美/ mænˈtɪsə /

  • n.尾数;假数;定点部分;小数部分

FP8:E4M3 / E5M2(8bit = 1符号 + E + M)

FP16:E5M10(16bit)← 和 E5M2 同 5 指数,尾数 10 不是 2

BF16就是E8M7**(16bit)← 指数同 fp32,尾数砍短**

值 = 符号 × 2^指数 × (1.xxx)

zero point(零点), scale(缩放因子)

1 E4M3 E5M2的问题

这里的命名是硬件/浮点格式里的习惯写法,不是数学里的自然常数 e。

字母 英文全称 中文常见叫法
E Exponent 指数(表示 2 的多少次方 那一部分)
M Mantissa 尾数(表示有效数字/小数精细程度那一部分)

exponent 英/ ɪkˈspəʊnənt / 美/ ɪkˈspoʊnənt /

  • n.拥护者,倡导者;代表人物;指数,幂;大师,行家;成分;说明者,说明物
  • adj.说明的

mantissa 英/ mænˈtɪsə / 美/ mænˈtɪsə /

  • n.尾数;假数;定点部分;小数部分
bash 复制代码
最终值 = 符号 × 2^(指数) × (1.xxx)
                              ↑
                         这里的 1.xxx 来自尾数位
  • 存进 M(尾数位) 的,是 1. 后面的小数部分(有的格式隐含开头的 1,不占用 bit)
  • 所以 (1.xxx) 这一段 一般在 1.0 到不到 2.0 之间
    例如:1.01.251.51.75,不会是 5.01 这种"已经放大过的整数"
  • 整体值小于 1 时,不靠「尾数小于 1」,而是靠 指数为负,把结果缩小。
  • 所以这里尾数永远都是1.xx的形式。

2 BF16 FP16

和 FP8 一样,都是:

| 符号 S | 指数 E | 尾数 M |

数值(归一化时)仍是:

值 = (-1)^S × 2^(指数) × (1.xxx)

  • 指数:2 的多少次方(不是 10,也不是 e)
  • 尾数 (1.xxx) 仍在 1~2 之间;小于 1 的数用 负指数;大于 1 用正指数

差别只在:16 bit 怎么分给 S、E、M。

2.1 fp16(IEEE half,半精度)

字段 位数
符号 S 1
指数 E 5
尾数 M 10

和 E5M2 一样,指数只有 5 bit → 能表示的 2 的幂次范围较窄;尾数 10 bit → 在 1~2 之间台阶很密 → 相对更精细。

2.2 bf16(bfloat16,Brain Float)

字段 位数
符号 S 1
指数 E 8
尾数 M 7

设计目标:指数和 fp32 一样(8 bit),尾数只留 7 bit。

可以把它想成:

bash 复制代码
fp32:|S| 8位指数 | 23位尾数 |
bf16:|S| 8位指数 | 7位尾数  |   ← 相当于 fp32 砍掉后面一截尾数

2.3 区别对比

| fp16 | bf16 |
|-----------|-----------|-------------------------------|
| 总位数 | 16 | 16 |
| 指数 | 5 bit | 8 bit(与 fp32 一致) |
| 尾数 | 10 bit | 7 bit |
| 动态范围 | 小(易溢出/下溢) | 大(接近 fp32) |
| 精度 | 高(尾数多) | 低(尾数少) |
| 和 fp32 转换 | 要专门转换 | 很常见:取 fp32 高 16 bit |
| 大模型里 | 推理、部分训练 | 训练/推理主精度很常见(DeepSeek、Llama 等) |

3 VLLM里面的kv cache的量化与反量化过程

这里对称量化,没有zeropoint,只有一个scale,

bash 复制代码
                    写 KV                         读 KV
         ┌─────────────────────────┐    ┌─────────────────────────┐
  K/V    │ bf16 (或 fp16/float)    │    │ cache 里 8bit FP8       │
  计算   │   ÷ scale               │    │   × scale               │
  结果 ──┤ → E5M2 或 E4M3 编码     ├───►│ → float/bf16/fp16       ├──► Attention
         │ → 存入 KV cache         │    │   (寄存器里即时转换)     │
         └─────────────────────────┘    └─────────────────────────┘

4 先梳理一下读取KV CACHE的代码大体流程:以后会详细梳理整个kv cache相关完整代码,这次只看一部分

前提:启动参数 --kv-cache-dtype fp8_e5m2,模型计算用 bf16,cache 里每个 K/V 元素是 1 字节 uint8(FP8 E5M2)。

4.1 Host 入口:谁发起「读 KV 做 Attention」

vllm015/csrc/attention/paged_attention_v1.cu 160--182

cpp 复制代码
void paged_attention_v1(
    torch::Tensor& out,    // [num_seqs, num_heads, head_size]
    torch::Tensor& query,  // [num_seqs, num_heads, head_size]
    torch::Tensor&
        key_cache,  // [num_blocks, num_heads, head_size/x, block_size, x]
    torch::Tensor&
        value_cache,       // [num_blocks, num_heads, head_size, block_size]
    int64_t num_kv_heads,  // [num_heads]
    double scale,
    torch::Tensor& block_tables,  // [num_seqs, max_num_blocks_per_seq]
    torch::Tensor& seq_lens,      // [num_seqs]
    int64_t block_size, int64_t max_seq_len,
    const std::optional<torch::Tensor>& alibi_slopes,
    const std::string& kv_cache_dtype, torch::Tensor& k_scale,
    torch::Tensor& v_scale, const int64_t tp_rank,
    const int64_t blocksparse_local_blocks,
    const int64_t blocksparse_vert_stride, const int64_t blocksparse_block_size,
    const int64_t blocksparse_head_sliding_step) {
  const bool is_block_sparse = (blocksparse_vert_stride > 1);

  DISPATCH_BY_KV_CACHE_DTYPE(query.dtype(), kv_cache_dtype,
                             CALL_V1_LAUNCHER_BLOCK_SIZE)
}

4.2 宏分发:把 "fp8_e5m2" 绑到 kFp8E5M2

vllm015/csrc/quantization/w8a8/fp8/amd/quant_utils.cuh 746--794行

kv_cache_dtype == "fp8_e5m2"query 是 bf16 时,预处理器展开为

cpp 复制代码
CALL_V1_LAUNCHER_BLOCK_SIZE(__nv_bfloat16, uint8_t, kFp8E5M2);

4.3 宏链展开:一路传到 kernel 启动

vllm015/csrc/attention/paged_attention_v1.cu

子步 行号 宏 / 函数 作用
3a 144--153 CALL_V1_LAUNCHER_BLOCK_SIZE block_size(8/16/32)分支
3b 135--139 CALL_V1_LAUNCHER_SPARSITY 是否 block sparse
3c 127--133 CALL_V1_LAUNCHER 调用 paged_attention_v1_launcher<bf16, uint8_t, block_size, kFp8E5M2, ...>
3d 46--125 paged_attention_v1_launcher head_size switch,准备 grid/block/shared mem
3e 26--40 LAUNCH_PAGED_ATTENTION_V1 paged_attention_v1_kernel<<<grid, block, stream>>>(...)

4.4 GPU 核函数外壳

vllm015/csrc/attention/attention_kernels.cuh

4.5 核心逻辑:从 cache 读 K(uint8)→ 反量化 → QK

vllm015/csrc/attention/attention_kernels.cuh

4.5.1 定义向量类型(一次读 16 字节)

4.5.2 从 paged k_cache 读出一段 FP8

  • k_ptr 指向 cache 里某个 token 的 K
  • k_vec_quantreinterpret_castQuant_vec(例如 4 个 uint8 打包在一个 uint32_t 里)

4.5.3 反量化成 K_vec(bf16 向量)--- 整条 bug 链的入口

4.5.4 用反量化后的 K 和 Q 算分

后面还有 softmax、再读 V(423--427 行,同样 scaled_convert,逻辑与 K 类似)。

在干什么:每个 thread 从 cache 读 FP8 字节 → 转成 bf16 的 K → 参与 attention;读错 = gsm8k 答非所问。

4.6 scaled_convert:统一入口

vllm015/csrc/quantization/w8a8/fp8/amd/quant_utils.cuh

这里:

  • Tout = K_vec(如 bf16_4_t
  • Tin = Quant_vec(如 uint32_t
  • kv_dt = kFp8E5M2

4.7 scaled_vec_conversion:向量拆包,最后落到单字节 E5M2 反量化

其他两个函数也顺便增加了

4.8 最底层:E5M2 字节 → float 也是这次新增加的函数

cpp 复制代码
static inline __device__ float fp8e5m2_to_fp32(const uint8_t& input) {
  union uf16 {
    uint16_t as_bits;
    _Float16 as_value;
  };
  uf16 u16;
  u16.as_bits = (uint16_t)input << 8;
  return (float)u16.as_value;
}

参考文献:

VLLM开源代码

相关推荐
zjun302112 小时前
【昇腾950】如何在昇腾950pr的容器环境上部署vllm
vllm·vllm-ascend·torch_npu·昇腾950
小何code1 天前
人工智能【第55篇】大模型推理优化:vLLM与推理加速技术
vllm·大模型部署·推理优化·pagedattention
清风lsq4 天前
大模型-vllm 自投机解码可行性分析
vllm·大模型推理
大模型推理4 天前
《Nano-vLLM 源码解读》第 12 篇 · ModelRunner:从 prompt 到 token(二)
vllm
清风lsq5 天前
大模型-解析vllm lora 模块
人工智能·vllm·大模型推理
大模型推理5 天前
《Nano-vLLM 源码解读》第 11 篇 · ModelRunner:从 prompt 到 token
vllm
zhangfeng11336 天前
vLLM + AWQ 是什么,为什么有算力架构要求 为什么v100默认不支持
人工智能·语言模型·显卡·vllm
SpikeKing7 天前
LLM - 支持 Hermes 智能体的 vLLM 部署 Qwen3.5 与 Qwen3.6 方案
llm·vllm·qwen3.5·hermes·qwen3.6
zhojiew7 天前
在Ray集群中使用vLLM部署LLM模型并集成Prometheus和Grafana进行指标观测的实践
grafana·prometheus·vllm