FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages/

ハードウェア記述言語"Chisel"のテスト環境の一例"Hardfloat"(1)

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と比較しながらすべてテストを行っているようだ。 f:id:msyksphinz:20171128014436p:plain

加減算、乗算ユニット 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の結果を一致比較しているようだ。さらに解析を進めていく。