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-box
  • FSH 只取源 FP 寄存器低 16 bit 写内存
  • 规范还强调它们不应修改被搬运的位模式,因此非 canonical NaN payload 也要被保留

Zfh 的 half 算术指令与 single precision 同构,但 fmt 字段取 H, 包括:

  1. FADD.H/FSUB.H/FMUL.H/FDIV.H/FSQRT.H/FMIN.H/FMAX.H
  2. 四种 fused multiply-add
  3. FEQ.H/FLT.H/FLE.HFCLASS.HFSGNJ*.H
  4. half 与 integer/FP32/FP64/FP128 的转换
  5. FMV.X.H/FMV.H.X

ZfhminZfh 的子集,只包含 FLH/FSHFMV.X.H/FMV.H.XFCVT.S.H/FCVT.H.S;

  • 如果实现了 D/Q,还包含 FCVT.D.H/FCVT.H.DFCVT.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 扩展族包括 ZfbfminZvfbfminZvfbfwma

  • 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/FSDFLH/FSH 多数可以复用 FP load/store 的队列和写回端口,只在数据抽取/掩码/NaN-boxing 上加一档 16-bit 格式

对非对齐访问,ISA 只保证 naturally aligned 的 FLH/FSH 原子性, 其他处理继承原 CPU 实现。

FPU

FPU 负责读写 FP 寄存器、NaN-boxing 检查、转换、move、比较、分类和算术:

  • 译码需要识别 _H 指令,并产生 fmt=HtypeTag=H 或等价控制信号
  • FP 寄存器读操作对非搬运类 half 指令要检查 NaN-boxing,高位不全 1 时把源操作数替换为 canonical NaN
  • FP 写回 half 结果时要把高位补 1,保证后续 narrow 操作能看到合法 NaN-boxed 值
  • FMV.X.H 写整数寄存器时做 half 符号位扩展,FMV.H.X 只取整数低 16 bit 并 NaN-box
  • FCVT.*.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 的内部表示,后续 AddRecFNMulAddRecFNDivSqrtRecFN_small 等模块可以直接处理 recoded operand

Rocket Chip / HardFloat recoded-format FPU

HardFloat 的 rawFloatFromRecFNrecExp 高位编码特殊类别:

  • recExp 最高 3 bit 为 000 时表示 zero
  • 110 时表示 infinity
  • 111 时表示 NaN
  • 其他编码覆盖 normalized/subnormal 数
1
2
3
4
5
rawIn.sign ##
(Mux(rawIn.isZero, 0.U(3.W), rawIn.sExp(expWidth, expWidth - 2)) |
Mux(rawIn.isNaN, 1.U, 0.U)) ##
rawIn.sExp(expWidth - 3, 0) ##
rawIn.sig(sigWidth - 2, 0)

典型结构是:

  • 输入侧执行 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 的关系是“共享前后端控制,算子按格式参数化实例化”:

  1. 统一控制路径
    • 解码阶段把指令格式编码成 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 共用
  2. 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 改变
  3. 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 编码宽度
  4. 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 FPU

第一层是微架构外壳复用:

  • XiangShan 没有为 FP16 单独做一套 FALU
    • FADD.H/FSUB.H/FMIN.H/FMAX.H/FSGNJ*.H 仍进入 FALU
    • FMUL.H/FMADD.H/... 仍进入 FMA
    • FCVT.*.H/FCVT.H.* 进入 FCVT/I2F
    • FDIV.H/FSQRT.H 进入 FDivSqrt
    • wrapper 只把译码得到的 fmt=VSew.e16 传给 yunsuan 算子,同时在输入端做 NaN-box 检查

第二层是算子内部的多格式复用

  1. FloatAdder
    • 模块顶层输入宽度按 f64 做成 64 bit,内部同时准备 f64_fp_a = io.fp_af32_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 计算
  2. FloatFMA 的共享粒度更细
    • 也用统一 64-bit 输入和统一三拍流水,但内部显式拆出 fp_a_f16/fp_b_f16/fp_c_f16Ea_f16/Eb_f16/Ec_f16fp_*_significand_f16,并为 f16 定义 biasF16rshiftBasicF16rshiftMaxF16 等参数
    • 乘法部分会生成 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
  3. 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
  4. 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/fp64 one-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
  5. 向量侧的复用更接近 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-bit fflags 合并

因此 XiangShan 的 FP16 复用策略可以概括为:标量复用调度/写回/控制和多格式算子框架,向量复用 64-bit slice 并在 slice 内做多 lane f16 packing

方案三:内部提升到 FP32 计算,再窄化

一种低成本实现思路:FPU 输入把 half 扩展到 single,使用已有 single 算术,再转回 half。它可以用于 faithful emulation half 加、减、乘、除和平方根,也可以作为硬件内部优化。

限制在于 FMA。完整 ZfhFMADD.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-lane fflags 在 wrapper 层合并。
  • PULP Ara:说明每个 lane 包含 VRF slice、SIMD FPU 和 SIMD ALU/Multiplier,并以 64-bit packet 做 SIMD 处理。其 ara_pkg.sv 定义了 FPUSupportHalf/Single/Double/HalfSingleDouble,还定义了 fp16_tfp32_from_fp16 等格式转换函数,说明 FP16 是其可配置 SIMD FPU 数据类型之一
  • ARM NEON/SVE、x86 AVX-FP16 等商业 SIMD/向量扩展也属于同类思想:寄存器按多个 16-bit lane 解释,每个 lane 产生独立浮点结果,再统一处理 predicate/mask、异常和写回