RISC-Vの実装であるRocket-Chipには、標準的な5ステージパイプラインであるRocket-Core、さらにアウトオブオーダ実行が可能なBOOM(Berkeley Out-of-Order Machine)-V1, BOOM-V2などが存在する。
これらのCPUコアはいずれもハードウェア浮動小数点演算器を持っており、つまりこれらの演算器もChiselで記述してあるということになる。 また、この浮動小数点演算器は別リポジトリとして独立に管理されており、そのリポジトリ単体で浮動小数点演算器のテスト環境が構築できるようになっている。
Chiselで記述されたハードウェア浮動小数点演算器"hardfloat"
まずは、単体でScalaのsbtが実行できる環境が必要だ。以下のウェブサイトを参考にして、sbtをインストールしておく。
sbt Reference Manual — Linux への sbt のインストール
echo "deb https://dl.bintray.com/sbt/debian /" | sudo tee -a /etc/apt/sources.list.d/sbt.list sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823 sudo apt-get update sudo apt-get install sbt
make
を実行するとシミュレーションを開始することが出来る。私の環境では、数10分かかった (しかも、どうやらBerkeleyのTestFloatをダウンロードしてきて、検算しているようである)。
ログを見ていると、単精度浮動小数点、倍精度浮動小数点、変換も含めて、TestFloatと比較しながらすべてテストを行っているようだ。
加減算、乗算ユニット MulAddRecFN
加減算、乗算を行うためのユニットは、hardfloat/src/main/scala/MulAddRecFN.scala
に定義してある。
class MulAddRecFN(expWidth: Int, sigWidth: Int) extends Module { val io = new Bundle { val op = Bits(INPUT, 2) ...
入出力ピンとしては、
val io = new Bundle { val op = Bits(INPUT, 2) val a = Bits(INPUT, expWidth + sigWidth + 1) val b = Bits(INPUT, expWidth + sigWidth + 1) val c = Bits(INPUT, expWidth + sigWidth + 1) val roundingMode = UInt(INPUT, 3) val detectTininess = UInt(INPUT, 1) val out = Bits(OUTPUT, expWidth + sigWidth + 1) val exceptionFlags = Bits(OUTPUT, 5) }
が定義されているのだが、不思議なのは、
val mulAddResult =
(mulAddRecFNToRaw_preMul.io.mulAddA *
mulAddRecFNToRaw_preMul.io.mulAddB) +&
mulAddRecFNToRaw_preMul.io.mulAddC
ええー、まさか四則演算子がそのままハードウェアに変換されているのかなあ。それは非常に効率の悪そうな演算回路が作られそうだが。。。
いろいろ調べていると、Rocket-Chipの浮動小数点演算器は別のものが使われているのだろうか?
生成されたVerilogコードを見ているとFPUFMAPipe
というモジュールがRocket-Chip内で定義されていた。
- src/main/scala/tile/FPU.scala
class FPUFMAPipe(val latency: Int, val t: FType)(implicit p: Parameters) extends FPUModule()(p) { require(latency>0) val io = new Bundle { val in = Valid(new FPInput).flip val out = Valid(new FPResult) } val valid = Reg(next=io.in.valid) val in = Reg(new FPInput) when (io.in.valid) { val one = UInt(1) << (t.sig + t.exp - 1) val zero = (io.in.bits.in1 ^ io.in.bits.in2) & (UInt(1) << (t.sig + t.exp)) val cmd_fma = io.in.bits.ren3 val cmd_addsub = io.in.bits.swap23 in := io.in.bits when (cmd_addsub) { in.in2 := one } when (!(cmd_fma || cmd_addsub)) { in.in3 := zero } } val fma = Module(new MulAddRecFNPipe((latency-1) min 2, t.exp, t.sig)) fma.io.validin := valid fma.io.op := in.fmaCmd
MulAddRecFNPipe
などの基本的なモジュールは、HardFloatと同じものを再利用している。
ただし、FPUFMAPipeはパイプラインを切るために、HardFloatとは違いものを使っているみたいだなあ。
ところどころ、HardFloatのモジュールを使っているところもあるみたいだ。 この辺りは、HardFloatで基本的な機能を検証しておいて、本当に必要なパイプラインなどはRocket-Chipの中のモジュールで定義する、という考え方のようだ。
val i2fResults = for (t <- floatTypes) yield { val i2f = Module(new hardfloat.INToRecFN(xLen, t.exp, t.sig)) i2f.io.signedIn := ~in.bits.typ(0) i2f.io.in := intValue i2f.io.roundingMode := in.bits.rm
話がそれたが、このHardFloat、テストの際には以下のようにして各モードでテストを読んでいる。
- hardfloat/src/main/scala/tests.scala
//*** CHANGE THIS NAME (HOW??): object FMATest { def main(args: Array[String]): Unit = { val testArgs = args.slice(1, args.length) args(0) match { case "f16FromRecF16" => chiselMain(testArgs, () => Module(new ValExec_f16FromRecF16)) case "f32FromRecF32" => chiselMain(testArgs, () => Module(new ValExec_f32FromRecF32)) ...
これでChiselでのテストと、TestFloat-Genの結果を一致比較しているようだ。さらに解析を進めていく。