Prhub

#46135 [HARDWARE][POWER] Enable fp16 support for PowerPC

原始 PR 作者 Rukhaiya2004 合并时间 2026-06-23 21:24 文件变更 6 提交数 8 评论 2 代码增减 +333 / -69

执行摘要

为 PowerPC 架构启用 fp16 推理支持

Enable FP16 (half precision) inference support for PowerPC systems. (来自 PR body)

值得精读,尤其是 fp16_to_fp32_bitsfp32_to_fp16_bits 的向量化实现,展示了如何在缺乏硬件 fp16 支持的平台上用软件模拟,设计模式可复用。此外,cpu_types_vsx.hpp 中为 PowerPC 定义的向量类型体系也值得关注。

讨论亮点

此 PR 没有 Review 评论,但由维护者 bigPYJ1151 直接批准。无显著设计争议。

实现拆解

实现拆解:

  1. 向量化转换函数:在 csrc/cpu/cpu_types_vsx.hpp 中新增 fp16_to_fp32_bitsfp32_to_fp16_bits 两个内联函数,使用 VSX 的位操作指令实现 IEEE 754 兼容的 fp16 与 fp32 互转。同时定义 FP16Vec8 向量类型,封装加载、保存操作。
  2. 宏扩展:将 VLLM_DISPATCH_CASE_FLOATING_TYPES 宏扩展为包含 AT_DISPATCH_CASE(at::ScalarType::Half, ...),使所有依赖该宏的 kernel 自动获得 fp16 支持。
  3. 旋转位置编码:在 csrc/cpu/pos_encoding.cpp 中为 c10::Half 特化 rotary_embedding_impl 函数,先用 FP16Vec8 加载,转 FP32Vec8 计算后转回 c10::Half 存储,尾部残留用标量处理。
  4. 注意力 kernel:在 csrc/cpu/cpu_attn_vsx.hpp 中新增 load_row8_B_as_f32<c10::Half> 特化,使 KV cache 为 fp16 时能正确加载并展开为 float32 参与计算。同时在 mla_decode.cpp 中移除先前为 PowerPC 定制的 #ifdef 分支,统一使用与 x86 相同的向量类型,并在 cpu_attn_impl.hpp 中移除 !defined(__powerpc__) 保护条件。
  5. 平台配置:在 vllm/platforms/cpu.py 中,为 PowerPC 架构的 supported_dtypes 添加 torch.float16,确保前端能识别该数据类型。
文件 模块 状态 重要度
csrc/cpu/cpu_types_vsx.hpp 向量化层 modified 7.42
csrc/cpu/pos_encoding.cpp 位置编码 modified 7.23
csrc/cpu/cpu_attn_vsx.hpp 注意力 kernel modified 5.62
csrc/cpu/mla_decode.cpp MLA 解码 modified 5.45
csrc/cpu/cpu_attn_impl.hpp 注意力实现 modified 4.82
vllm/platforms/cpu.py 平台层 modified 4.54

关键符号

fp16_to_fp32_bits fp32_to_fp16_bits rotary_embedding_impl<c10::Half> load_row8_B_as_f32<c10::Half>

关键源码片段

csrc/cpu/cpu_types_vsx.hpp core-logic

核心转换函数和向量类型定义,是 fp16 支持的基石

// 文件 : csrc/cpu/cpu_types_vsx.hpp (head 版本 )
// 已扩展类型分发宏,使所有使用它的 kernel 支持 fp16
#define VLLM_DISPATCH_CASE_FLOATING_TYPES(...) \
  AT_DISPATCH_CASE(at::ScalarType::Float, __VA_ARGS__) \
  AT_DISPATCH_CASE(at::ScalarType::BFloat16, __VA_ARGS__) \
  AT_DISPATCH_CASE(at::ScalarType::Half, __VA_ARGS__)namespace {/* 将 4 个 FP16 值(每个 16 位)转换为 4 个 FP32 值(每个 32 位)
 * 输入 : x 是 4 个 16 位 FP16 打包在 __vector unsigned int 的低 16 位中 */
FORCE_INLINE __vector float fp16_to_fp32_bits(__vector unsigned int x) {
  const __vector unsigned int mask_sign = {0x8000, 0x8000, 0x8000, 0x8000};
  const __vector unsigned int mask_exp = {0x7C00, 0x7C00, 0x7C00, 0x7C00};
  const __vector unsigned int mask_mant = {0x03FF, 0x03FF, 0x03FF, 0x03FF};
  const __vector unsigned int bias_adj = {112, 112, 112, 112}; // 127 - 15 = 112
  const __vector unsigned int exp_max_fp16 = {0x1F, 0x1F, 0x1F, 0x1F};
  const __vector unsigned int exp_max_fp32 = {0xFF, 0xFF, 0xFF, 0xFF};  __vector unsigned int s = (x & mask_sign) << 16;
  __vector unsigned int e = (x & mask_exp) >> 10;
  __vector unsigned int m = (x & mask_mant) << 13;  __vector __bool int is_nan_inf = vec_cmpeq(e, exp_max_fp16);
  __vector unsigned int e_normal = e + bias_adj;
  e = vec_sel(e_normal, exp_max_fp32, is_nan_inf);  return (__vector float)(s | (e << 23) | m);
}/* 将 4 个 FP32 值转换为 4 个 FP16 值,返回打包在 __vector unsigned int 的低 16 位中
 * 包含 rounding 和溢出处理,IEEE 754 兼容 */
FORCE_INLINE __vector unsigned int fp32_to_fp16_bits(__vector float f_in) {
  // ... 完整实现请参考 PR head 版本
  // 关键步骤:解析符号、指数、尾数,偏差调整,rounding to nearest even
}} // anonymous namespace
csrc/cpu/pos_encoding.cpp core-logic

为旋转位置编码添加 fp16 特化,使 RoPE 支持 Half 类型

// 文件 : csrc/cpu/pos_encoding.cpp (head 版本 )
// FP16 特化的旋转位置编码实现,使用 FP16Vec8 加载、FP32Vec8 计算、再转回 Half 存储
template <>
void rotary_embedding_impl<c10::Half>(
    const int64_t* __restrict__ positions,
    c10::Half* __restrict__ query,
    c10::Half* __restrict__ key,
    const c10::Half* __restrict__ cos_sin_cache,
    const int rot_dim, const int64_t query_stride, const int64_t key_stride,
    const int num_heads, const int num_kv_heads, const int head_size,
    const int num_tokens) {
  using scalar_vec_t = vec_op::FP16Vec8;
  constexpr int VEC_ELEM_NUM = scalar_vec_t::get_elem_num();
  const int embed_dim = rot_dim / 2;
  bool flag = (embed_dim % VEC_ELEM_NUM == 0);
  const int loop_upper = flag ? embed_dim : embed_dim - VEC_ELEM_NUM;  auto compute_loop = [&](const int64_t token_head, const c10::Half* cache_ptr,
                          c10::Half* qk) {
    int j = 0;
    for (; j < loop_upper; j += VEC_ELEM_NUM) {
      const int rot_offset = j;
      const int x_index = rot_offset;
      const int y_index = embed_dim + rot_offset;
      const int64_t out_x = token_head + x_index;
      const int64_t out_y = token_head + y_index;      // 以 FP16Vec8 加载 cache 和 qk 数据
      const vec_op::FP16Vec8 cos_fp16(cache_ptr + x_index);
      const vec_op::FP16Vec8 sin_fp16(cache_ptr + y_index);
      const vec_op::FP16Vec8 q_x_fp16(qk + out_x);
      const vec_op::FP16Vec8 q_y_fp16(qk + out_y);      // 转成 FP32Vec8 进行计算
      const vec_op::FP32Vec8 fp32_cos(cos_fp16);
      const vec_op::FP32Vec8 fp32_sin(sin_fp16);
      const vec_op::FP32Vec8 fp32_q_x(q_x_fp16);
      const vec_op::FP32Vec8 fp32_q_y(q_y_fp16);      auto out1 = fp32_q_x * fp32_cos - fp32_q_y * fp32_sin;
      auto out2 = fp32_q_y * fp32_cos + fp32_q_x * fp32_sin;      // 转回 FP16 并保存
      vec_op::FP16Vec8(out1).save(qk + out_x);
      vec_op::FP16Vec8(out2).save(qk + out_y);
    }
    // 尾部残差处理(标量)
    if (!flag) {
      for (; j < embed_dim; ++j) {
        const int x_index = j;
        const int y_index = embed_dim + j;
        const int64_t out_x = token_head + x_index;
        const int64_t out_y = token_head + y_index;
        const float fp32_cos = static_cast<float>(cache_ptr[x_index]);
        const float fp32_sin = static_cast<float>(cache_ptr[y_index]);
        const float fp32_q_x = static_cast<float>(qk[out_x]);
        const float fp32_q_y = static_cast<float>(qk[out_y]);
        qk[out_x] = static_cast<c10::Half>(fp32_q_x * fp32_cos - fp32_q_y * fp32_sin);
        qk[out_y] = static_cast<c10::Half>(fp32_q_y * fp32_cos + fp32_q_x * fp32_sin);
      }
    }
  };  // 外层循环处理所有 tokens 和 heads
#pragma omp parallel for
  for (int token_idx = 0; token_idx < num_tokens; ++token_idx) {
    int64_t pos = positions[token_idx];
    const c10::Half* cache_ptr = cos_sin_cache + pos * rot_dim;
    for (int i = 0; i < num_heads; ++i) {
      const int head_idx = i;
      const int64_t token_head = token_idx * query_stride + head_idx * head_size;
      compute_loop(token_head, cache_ptr, query);
    }
    if (key != nullptr) {
      for (int i = 0; i < num_kv_heads; ++i) {
        const int head_idx = i;
        const int64_t token_head = token_idx * key_stride + head_idx * head_size;
        compute_loop(token_head, cache_ptr, key);
      }
    }
  }
}

评论区精华

无 Review 讨论 other

PR 没有收到 Review 评论,由 Maintainer bigPYJ1151 直接批准。

结论:无争议,直接合并。 · 已解决

风险与影响

主要风险在于 PowerPC 平台上的 fp16 支持是否在所有启用路径中完整:虽然修改了主要 kernel 的宏和特化,但仍有部分 CPU kernel 通过 VLLM_DISPATCH_FLOATING_TYPES 宏间接支持,需确认未遗漏。此外,VSX 位操作转换函数的正确性依赖 IEEE 754 规范,代码中已包含 NaN/Inf 处理,但边界场景(如次正规数)的 rounding 逻辑需要测试验证。风险等级低,影响范围局限于 PowerPC 架构。

对 PowerPC 用户而言,新增 fp16 支持意味着可以运行半精度模型,降低内存带宽和占用,可能获得性能提升。对其他 CPU 架构(x86、ARM)无影响,因为修改是条件编译或特化。对开发团队,维护复杂度略有增加,但代码封装良好,通过宏和特化隔离。

缺少测试覆盖 平台限定变更 向量化位操作

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论