执行摘要
本 PR 为 vLLM 的 RISC-V CPU 内核添加了编译时向量长度(VLEN)分派支持,通过重构头文件和更新 CMake 构建配置,实现 VLEN=128、256 或标量构建的自动检测与手动指定。解决了之前硬编码 VLEN=128 导致的兼容性问题,确保在多种 RISC-V 硬件上正确构建并发挥向量化性能。变更涉及核心类型定义和构建逻辑,虽存在代码重复权衡,但设计清晰,对低层优化具有参考价值。
功能与动机
为什么做:之前的代码(#36578)在 -march 中硬编码了 zvl128b,导致在 VLEN 不同的硬件(如 256 位的 Spacemit X100)上重建时产生错误结果或段错误。PR body 明确指出,需要支持多个 VLEN 目标以适配不同 RISC-V 平台,避免兼容性问题。
实现拆解
变更主要分为以下步骤:
- CMake 构建配置更新:修改
cmake/cpu_extension.cmake,添加 VLEN 自动检测逻辑。脚本会尝试从 /proc/cpuinfo 读取 zvl<N>b 扩展,并设置 VLLM_RVV_VLEN 变量;如果自动检测失败但编译器支持 RVV 扩展,则触发 FATAL_ERROR,强制用户通过 -DVLLM_RVV_VLEN=128 或 -DVLLM_RVV_VLEN=256 明确指定。这避免了无声回退到标量构建的风险。
- 头文件重构与分派:
- 新增
csrc/cpu/cpu_types_riscv_defs.hpp:定义 VLEN 到 LMUL 后缀的映射宏(如 LMUL_128、LMUL_256)和 intrinsic 宏(如 RVVI)。基于 __riscv_v_min_vlen(由编译器从 -march 定义)进行编译时分派。
- 新增
csrc/cpu/cpu_types_riscv_impl.hpp:包含 VLEN 无关的向量包装类实现,如 FP16Vec8、FP16Vec16。这些类使用宏生成正确的 RISC-V 向量 intrinsic 调用,例如加载和存储操作。
- 修改
csrc/cpu/cpu_types_riscv.hpp:大幅精简,仅作为入口点,包含错误检查(确保在 RVV 编译单元中使用)并引入上述两个头文件。
-
关键源码片段:以下是 cpu_types_riscv_defs.hpp 中的核心部分,展示如何实现 VLEN 分派:
-
测试配套:PR body 描述了测试计划,包括在 VLEN=256 的 Spacemit X100 上构建并运行 vllm bench,但没有新增测试文件,依赖现有测试验证功能。支持 -DVLLM_RVV_VLEN=0 进行标量构建作为回退选项。
关键源码片段
csrc/cpu/cpu_types_riscv_defs.hpp
定义 VLEN 到 LMUL 的映射宏和 intrinsic 宏,是实现编译时分派的核心,确保代码适配不同向量长度。
#ifndef CPU_TYPES_RISCV_DEFS_HPP
#define CPU_TYPES_RISCV_DEFS_HPP
// VLEN-to-LMUL mapping for RISC-V Vector extension.
// 根据编译器的 __riscv_v_min_vlen 定义 LMUL 后缀,支持 VLEN=128 和 256。
// 例如,VLEN=128 时 LMUL_128 映射为 m1,VLEN=256 时映射为 mf2。
#include <riscv_vector.h>
#if __riscv_v_min_vlen == 128
#define LMUL_128 m1 // VLEN=128 时,128 位向量对应 LMUL=m1
#define LMUL_256 m2 // 256 位向量对应 LMUL=m2
#define LMUL_512 m4
#define LMUL_1024 m8
#define BOOL_256 b16 // 布尔类型后缀
#define BOOL_512 b8
#elif __riscv_v_min_vlen == 256
#define LMUL_128 mf2 // VLEN=256 时,128 位向量对应 LMUL=mf2
#define LMUL_256 m1
#define LMUL_512 m2
#define LMUL_1024 m4
#define BOOL_256 b32
#define BOOL_512 b16
#else
#error "cpu_types_riscv_defs.hpp: unsupported __riscv_v_min_vlen"
#endif
// 宏定义:将 intrinsic 与 LMUL 后缀拼接,实现编译时分派
#define _RVV_P2(a, b) a##b
#define RVVI(base, lmul) _RVV_P2(base, lmul) // 例如 RVVI(__riscv_vle16_v_f16, LMUL_128)
// 语义化向量类型定义:基于元素数量命名,方便在代码中使用
typedef RVVTYPE(vfloat16, LMUL_128, _t) fixed_fp16x8_t
__attribute__((riscv_rvv_vector_bits(128))); // 8 个 float16 元素的固定向量
typedef RVVTYPE(vfloat32, LMUL_128, _t) fixed_fp32x4_t
__attribute__((riscv_rvv_vector_bits(128))); // 4 个 float32 元素的固定向量
#endif // CPU_TYPES_RISCV_DEFS_HPP
csrc/cpu/cpu_types_riscv_impl.hpp
包含 VLEN 无关的向量包装类实现,使用宏生成正确的 RISC-V intrinsic 调用,是向量操作的核心逻辑。
#ifndef CPU_TYPES_RISCV_IMPL_HPP
#define CPU_TYPES_RISCV_IMPL_HPP
// 共享的 RVV 向量包装类实现,VLEN 无关:使用 cpu_types_riscv_defs.hpp 中的宏。
// 不要直接包含此文件;通过 cpu_types_riscv.hpp 引入。
#include <algorithm>
#include <torch/all.h>
namespace vec_op {
#define FORCE_INLINE __attribute__((always_inline)) inline
template <typename T>
struct Vec {
constexpr static int get_elem_num() { return T::VEC_ELEM_NUM; }; // 获取向量元素数量
};
// FP16 向量实现示例:FP16Vec8 表示包含 8 个 float16 元素的向量
struct FP16Vec8 : public Vec<FP16Vec8> {
constexpr static int VEC_ELEM_NUM = 8; // 固定元素数量
fixed_fp16x8_t reg; // 使用 defs 中定义的固定向量类型
// 构造函数:从内存加载向量,使用 RVVI 宏根据 LMUL_128 展开正确的 intrinsic
explicit FP16Vec8(const void* ptr)
: reg(RVVI(__riscv_vle16_v_f16, LMUL_128)(
static_cast<const _Float16*>(ptr), VEC_ELEM_NUM)) {};
// 保存向量到内存
void save(void* ptr) const {
RVVI(__riscv_vse16_v_f16, LMUL_128)(static_cast<_Float16*>(ptr), reg,
VEC_ELEM_NUM);
}
void save(void* ptr, int elem_num) const {
RVVI(__riscv_vse16_v_f16, LMUL_128)(static_cast<_Float16*>(ptr), reg,
elem_num); // 支持部分存储
}
};
} // namespace vec_op
#endif // CPU_TYPES_RISCV_IMPL_HPP
评论区精华
Review 讨论中聚焦两个关键点:
风险与影响
技术风险:
- 构建配置依赖
/proc/cpuinfo,在非标准环境可能失败,需用户干预。
- 头文件代码重复增加维护负担,未来修改易出错。
- 缺乏直接测试覆盖,可能遗漏 VLEN 特定场景的 bug。
影响范围:
- 用户:RISC-V 开发者可更灵活构建 vLLM,提升跨平台体验。
- 系统:支持更多硬件变体,扩展部署场景。
- 团队:需熟悉新头文件结构,但变更模块化,便于扩展新 VLEN。
关联脉络
从历史 PR 看,近期 PR 如 #40331(启动性能优化)和 #39959(FlashInfer 升级)涉及 CPU 和构建,但本 PR 是首个针对 RISC-V VLEN 分派的专项改进。它解决了 #36578 引入的硬编码问题,为未来 RISC-V 生态扩展奠定了基础。
参与讨论