RISC-V FP16 扩展的 CPU 微架构实现
RISC-V FP16 扩展的 CPU 微架构实现方案
ISA
Zfh/Zfhmin (FP16)
Zfh 是 IEEE 754 binary16 标量浮点扩展,依赖 F
- half 值在浮点寄存器中遵循 RISC-V 的 NaN-boxing 规则:窄格式值放在低位,高位全 1
- 对 half 来说,如果
FLEN=64,合法 half 值应位于低 16 bit,高 48 bit 全 1 - 若源操作数没有正确 NaN-box,非搬运类 FP 操作必须把它当作 half canonical NaN
FLH/FSH 是 16-bit 浮点 load/store
FLH写回 FP 寄存器时要 NaN-boxFSH只取源 FP 寄存器低 16 bit 写内存- 规范还强调它们不应修改被搬运的位模式,因此非 canonical NaN payload 也要被保留
Zfh 的 half 算术指令与 single precision 同构,但 fmt 字段取 H, 包括:
FADD.H/FSUB.H/FMUL.H/FDIV.H/FSQRT.H/FMIN.H/FMAX.H- 四种 fused multiply-add
FEQ.H/FLT.H/FLE.H,FCLASS.H,FSGNJ*.H- half 与 integer/FP32/FP64/FP128 的转换
FMV.X.H/FMV.H.X
Zfhmin 是 Zfh 的子集,只包含 FLH/FSH、FMV.X.H/FMV.H.X、FCVT.S.H/FCVT.H.S;
- 如果实现了
D/Q,还包含FCVT.D.H/FCVT.H.D、FCVT.Q.H/FCVT.H.Q Zfhmin不包含FSGNJ.H,规范认为 half 值在 FP 寄存器间搬运可以用FSGNJ.S完成
half 的加减乘除和平方根可以通过 half->single、single 运算、single->half 进行 faithful emulation;但 half FMA 用这种方式在 RNE/RMM 下可能产生 1 ulp 误差,因此完整 Zfh 若声明支持 FMA,硬件或微码需要保证真正的 fused half 语义
BF16
BF16 和 FP16 共用很多实现问题。
BF16 扩展族包括 Zfbfmin、Zvfbfmin、Zvfbfwma。
Zfbfmin主要提供 BF16 与 FP32 的标量转换,并复用FLH/FSH/FMV.X.H/FMV.H.X作为 16-bit 数据搬运- 向量侧增加 BF16<->FP32 转换和 BF16 widening MAC
BF16 格式是 sign 1 bit、exponent 8 bit、fraction 7 bit,动态范围接近 FP32,但精度低于 FP16
微架构
LSU
LSU 需要支持 16-bit 浮点搬运
- 对
Zfhmin/Zfh来说,FLH/FSH是必需项 - 对
Zfbfmin来说,BF16 也复用 16-bit load/store 和 move 类数据搬运
因此 LSU 的设计可以把 FP16/BF16 当作一种 16-bit FP memory format 处理。
FLH 的 load 数据宽度是 16 bit
- DCache 返回的数据需要按地址低位抽取 halfword,再送到 FP 写回路径
- 写 FP 寄存器时必须 NaN-box:
- 如果
FLEN=64,写回值是{48'hffff_ffff_ffff, load_data[15:0]} - 如果
FLEN=32,高 16 bit 全 1
- 如果
FLH不能 canonicalize NaN payload,因为规范要求搬运指令保留原始 16-bit 位模式
FSH 的 store 数据来自 FP 源寄存器低 16 bit
- LSU 不应检查或修正源寄存器的 NaN-boxing 高位,也不应改写 NaN payload
FSH是纯 bit-level store:地址、权限、异常和 store queue 行为与普通 halfword store 类似,数据来源从整数寄存器换成 FP store-data 通路
对乱序核,FLH/FSH 还需要接入已有 load/store queue、store-to-load forwarding、replay 和异常恢复机制
- 实现上通常只增加一种 FP halfword load/store uop 类型,地址生成仍走整数 AGU
- 若已有
FLW/FLD/FSW/FSD,FLH/FSH多数可以复用 FP load/store 的队列和写回端口,只在数据抽取/掩码/NaN-boxing 上加一档 16-bit 格式
对非对齐访问,ISA 只保证 naturally aligned 的 FLH/FSH 原子性, 其他处理继承原 CPU 实现。
FPU
FPU 负责读写 FP 寄存器、NaN-boxing 检查、转换、move、比较、分类和算术:
- 译码需要识别
_H指令,并产生fmt=H、typeTag=H或等价控制信号 - FP 寄存器读操作对非搬运类 half 指令要检查 NaN-boxing,高位不全 1 时把源操作数替换为 canonical NaN
- FP 写回 half 结果时要把高位补 1,保证后续 narrow 操作能看到合法 NaN-boxed 值
FMV.X.H写整数寄存器时做 half 符号位扩展,FMV.H.X只取整数低 16 bit 并 NaN-boxFCVT.*.H/FCVT.H.*需要接入舍入模式、非法操作、溢出、下溢、非精确等fflags- 完整
Zfh的 half FMA 必须是 fused 语义,不能由 FP32 FMA 加两端转换近似替代
方案一:recoded 多格式 FPU (Rocket Chip/HardFloat)
HardFloat 在其 FPU 内部使用 recoded format,核心变化是 exponent 多 1 bit,以便在 FPU 中更高效地处理 subnormal
本质上相当于增加指数空间来额外表示 subnomal, 但多余的编码空间就冗余了,所以可以用来表示特殊数
这样 subnormal 在进入算术单元前已经被规整到更接近 normal 的内部表示,后续 AddRecFN、MulAddRecFN、DivSqrtRecFN_small 等模块可以直接处理 recoded operand

HardFloat 的 rawFloatFromRecFN 用 recExp 高位编码特殊类别:
recExp最高 3 bit 为000时表示 zero- 为
110时表示 infinity - 为
111时表示 NaN - 其他编码覆盖 normalized/subnormal 数
1 | rawIn.sign ## |
典型结构是:
- 输入侧执行 unbox + recode,把 FP RF 中的 NaN-boxed IEEE half 转成内部 recoded half
- 输出侧执行 sanitize + IEEE encode + box,把内部结果转成合法 NaN-boxed FP RF 值
- FMA、FP-to-FP、FP-to-int、int-to-FP、compare/classify、div/sqrt 使用格式参数实例化或复用参数化 HardFloat 模块
- 写回仲裁、scoreboard 和 bypass 网络把 half pipeline 当作又一种 FP pipe 处理
优点是:
- IEEE corner case、subnormal、rounding 和
fflags可以沿用已有 FPU 框架 - 适合面积可控且重视规范一致性的通用核
缺点是: half 吞吐通常还是标量吞吐,不天然提供 packed/SIMD FP16 性能
按照具体算术单元拆开看,recoded 多格式方案中 FP16 与 FP32/FP64 的关系是“共享前后端控制,算子按格式参数化实例化”:
- 统一控制路径
- 解码阶段把指令格式编码成
typeTagIn/typeTagOut,例如 half 指令使用FType.H,single 使用FType.S,double 使用FType.D - FP 寄存器文件通常按最大格式保存 recoded 值,并在读端通过
unbox(x, tag, exactType)检查 NaN-boxing;若 half 源操作数高位不是合法 box,则给算术单元的是该格式的 canonical NaN - 写回端通过
box(x, tag)把 narrow recoded 结果重新嵌入最大格式,因此 half 结果在 FP RF 中仍表现为合法 NaN-boxed 值 - scoreboard、流水 valid、kill/flush、
frm舍入模式、fflags写回和最终写回仲裁可以和 FP32/FP64 共用
- 解码阶段把指令格式编码成
- FMA、加法、乘法
FPUFMAPipe(t)内部实例化MulAddRecFNPipe(..., t.exp, t.sig);当t=FType.H时,乘法器宽度、对阶移位、规格化和舍入宽度都是 half 精度,而不是把 half 先转换成 single 再走 FP32 FMA- 标量加/减和乘法可复用 FMA pipe:加/减通过固定乘数为 1、乘法通过加数为 0 的方式进入同一个 fused datapath;但 H/S/D 通常是不同参数的 pipe 实例,通过
typeTagOut选择哪条 pipe 有效 - 因此 FP16 与 FP32/FP64 共享 FMA wrapper、流水控制和异常处理协议,但尾数乘法阵列、加法对齐宽度、rounding 输入宽度随
FType.H/S/D改变
- FP-to-FP、int-to-FP、FP-to-int、compare/classify
IntToFP会对floatTypes中每个目标格式生成INToRecFN(xLen, t.exp, t.sig),再用typeTagOut选择 half/single/double 结果- FP-to-FP widening 可以主要做 recoded 格式变换和 NaN canonicalize;narrowing 则需要
RecFNToRecFN(maxType.exp, maxType.sig, outType.exp, outType.sig)这样的舍入转换单元 - compare/classify/min/max/fsgnj 多数走 fast pipe,控制路径和 FP32/FP64 共用,但分类字段、sNaN/qNaN 判断和转换输出仍依赖
FType.H的 recoded 编码宽度
- div/sqrt
DivSqrtRecFN_small(t.exp, t.sig, 0)可按floatTypes为 H/S/D 分别实例化,输入由typeTagOut选通- FP16 除法/开方使用
exp=5/sig=11的较窄迭代状态、余数和舍入路径;FP32/FP64 使用更宽的参数实例 - 外围的 busy/in-flight、kill、ready/valid、写回选择和
exceptionFlags合并逻辑可以共用,但实际迭代 datapath 不必与 FP32/FP64 共用同一套位宽
方案二:IEEE754 多格式 FPU 数据通路
half 加法、乘法、FMA、比较、转换等作为 H/S/D 多格式算子的一部分,或者作为独立 half lane 放进标量/向量 FPU wrapper。
设计重点是:
- 标量 half 与 single/double 共享发射队列、寄存器读写、写回端口和 CSR/fflags 路径
- 执行单元内部按
fmt选择 H/S/D datapath - half FMA 使用真正的 fused 乘加和单次舍入
- 加法、比较、min/max、符号注入可以做成统一 multi-format datapath
- divider/sqrt 可以为 H/S/D 各自实例化参数化迭代器,也可以共享一套可变精度迭代器
XiangShan 就采用这样的设计,其标量 half 指令复用 FPU 调度器、算子内部用 fmt 选择 f16/f32/f64 数据通路

第一层是微架构外壳复用:
- XiangShan 没有为 FP16 单独做一套 FALU
FADD.H/FSUB.H/FMIN.H/FMAX.H/FSGNJ*.H仍进入FALUFMUL.H/FMADD.H/...仍进入FMAFCVT.*.H/FCVT.H.*进入FCVT/I2FFDIV.H/FSQRT.H进入FDivSqrt- wrapper 只把译码得到的
fmt=VSew.e16传给 yunsuan 算子,同时在输入端做 NaN-box 检查
第二层是算子内部的多格式复用
FloatAdder- 模块顶层输入宽度按 f64 做成 64 bit,内部同时准备
f64_fp_a = io.fp_a、f32_fp_a = io.fp_a(31,0)、f16_fp_a = io.fp_a(15,0) - f64 走
FloatAdderF64Pipeline,f32/f16 走FloatAdderF32F16MixedPipeline - 最后根据寄存后的
fp_format选择F16/F32/F64结果 - 当结果是 f16 时,输出通过
Cat(Fill(48, resultNeedBox), F16_result)做 NaN-box 写回 - FP16 复用了 FALU 这个功能单元壳、控制和部分 mixed pipeline,但并不是简单把 half 全部转换成 FP32 再用完整 FP32 adder 计算
- 模块顶层输入宽度按 f64 做成 64 bit,内部同时准备
FloatFMA的共享粒度更细- 也用统一 64-bit 输入和统一三拍流水,但内部显式拆出
fp_a_f16/fp_b_f16/fp_c_f16、Ea_f16/Eb_f16/Ec_f16、fp_*_significand_f16,并为 f16 定义biasF16、rshiftBasicF16、rshiftMaxF16等参数 - 乘法部分会生成 f16 的部分积序列
pp_seq_f16和 addend 序列,再与 f32/f64 路径共用 CSA、移位、规格化、舍入、special-case/fflags 等大框架,通过is_fp64/is_fp32的 mux 选择不同精度的数据片段 - 最终 f16 结果同样被
Cat(Fill(48, 1.U), fp_result_f16)boxed
- 也用统一 64-bit 输入和统一三拍流水,但内部显式拆出
FCVT/I2F- wrapper 根据
fuOpType中的 widen/narrow 编码和sew=fp_fmt解码output1H,覆盖 8/16/32/64-bit 输出宽度; - 转换核心内部使用
FType(5,11)、FType(8,24)、FType(11,53)这样的格式参数生成 int<->FP、FP<->FP 转换 datapath
- wrapper 根据
FDivSqrt- wrapper
FDivSqrt统一接收src0/src1/rm/fp_fmt,对 f16/f32 做 NaN-box 检查,再把fp_format_i := fp_fmt传给yunsuan.fpu.FloatDivider FloatDivider/FloatDividerR64内部有fp_format_is_fp16/fp32/fp64one-hot,并为不同格式定义不同的 remainder/shift/round 参数,例如 f16 使用REM_W_f16_*、R_SHIFT_NUM_LIMIT_f16_*、out_frac_post_*_f16_*等信号- 迭代次数也按格式缩放,代码里
iter_num_needed = Mux(fp_format_q_is_fp16, 1.U, Mux(fp_format_q_is_fp32, 3.U, 8.U)),说明 f16/f32/f64 共享 divider/sqrt 大框架,但不是用同样宽度、同样迭代长度无差别计算 - 输出端 wrapper 按
outCtrl.fpu.get.fmt选择结果并完成 boxing
- wrapper
- 向量侧的复用更接近 SIMD packing
VectorFloatAdder的 64-bit slice 内可以按格式切成 1 个 f64、2 个 f32 或 4 个 f16 lane- 两个
FloatAdderF32WidenF16MixedPipeline提供 f32 和部分 f16/widen 路径 FloatAdderF16Pipeline处理额外的 f16 lane- 最后把
U_F16_3/U_F16_2/U_F16_1/U_F16_0拼成 64-bit f16 结果,并把每个 f16 lane 的 5-bitfflags合并
因此 XiangShan 的 FP16 复用策略可以概括为:标量复用调度/写回/控制和多格式算子框架,向量复用 64-bit slice 并在 slice 内做多 lane f16 packing
方案三:内部提升到 FP32 计算,再窄化
一种低成本实现思路:FPU 输入把 half 扩展到 single,使用已有 single 算术,再转回 half。它可以用于 faithful emulation half 加、减、乘、除和平方根,也可以作为硬件内部优化。
限制在于 FMA。完整 Zfh 的 FMADD.H/FMSUB.H/FNMADD.H/FNMSUB.H 要求 fused 语义,不能拆成 FCVT.S.H + FMADD.S + FCVT.H.S。如果 CPU 只实现 Zfhmin,这个问题不存在;如果 CPU 声明完整 Zfh,则 half FMA 仍需要专门的 fused datapath 或足够精确的内部乘加路径。
方案四:向量/SIMD FP16
若实现 Zvfhmin/Zvfh,FPU/VPU 的核心问题变成 lane packing 和 per-lane exception 合并。一个 64-bit slice 可容纳 4 个 f16 lane,一个 128-bit 向量执行单元可容纳 8 个 f16 lane。
这类设计需要:
- 每个 lane 独立产生 5-bit
fflags - 按 mask、tail、
vl/vstart有效性 OR-reduce lane flags - mask-off、tail-undisturbed、tail-agnostic 通过统一 merge 单元处理
- widening 操作显式支持 f16->f32、f32->f64 的指数 bias 调整、subnormal 归一化和尾数补零
- reduction 通过 uop 拆分和 fold 控制多轮完成,而不是单独做一个所有 lane 的大树
采用向量/SIMD FP16 路线的代表实现包括:
- XiangShan:RTL 中
VFALU/VFMA/VFDivSqrt/VCVT把 128-bit vector datapath 拆成两个 64-bit yunsuan vector slice;VectorFloatAdder的 64-bit slice 可组织 1 个 f64、2 个 f32 或 4 个 f16 lane,128-bit uop 因而可覆盖 8 个 f16 元素。mask/tail、reduction fold 和 per-lanefflags在 wrapper 层合并。 - PULP Ara:说明每个 lane 包含 VRF slice、SIMD FPU 和 SIMD ALU/Multiplier,并以 64-bit packet 做 SIMD 处理。其
ara_pkg.sv定义了FPUSupportHalf/Single/Double/HalfSingleDouble,还定义了fp16_t、fp32_from_fp16等格式转换函数,说明 FP16 是其可配置 SIMD FPU 数据类型之一 - ARM NEON/SVE、x86 AVX-FP16 等商业 SIMD/向量扩展也属于同类思想:寄存器按多个 16-bit lane 解释,每个 lane 产生独立浮点结果,再统一处理 predicate/mask、异常和写回