ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

ChiselFlatSpecを使ったテストを並列化して高速に実行する方法

スポンサーリンク

今日はChiselのテストの時にTips的なやつを。
ChiselFlatSpecを使って作ったテストクラス内の各テストは通常、実装した順に逐次実行される。
この単一クラス内のテストを並列に実行する方法はないのかしら??と探してみたところ、めちゃくちゃお手軽にテストを並列実行できるのがわかったのでそれをご紹介。

ChiselFlatSpecを使ったテストを並列実行する方法

やり方はものすごく簡単なので先に書いてしまう。

  • ChiselFlatSpecを使って作ったテスト用のクラスにParallelTestExecutionをミックスインする

ただ、これだけ。
これだけでScalaTestがいい感じに並列実行してくれます。 なおこれをつかって並列化すると以前に紹介したBeforeAndAfterAllConfigMapを使ったプログラム引数の処理が無視される。 だが、これはリグレッションテストのみを実行するメイン関数でも作っておけばそれで良い気もしている。

www.tech-diningyo.info

気が向けばもう少し中身を見てみようかなーくらい。

例を使って確認してみる

これだけじゃあんまりなので、一応適当なモジュールを作って確認していく。
サンプルはこちら

import chisel3._
/**
  * 入力信号を所定のサイクル遅らせて出力
  * @param delay 何サイクル出力を遅らせるか(delay > 0)
  */
class DelayBuf(delay: Int) extends Module {
  val io = IO(new Bundle{
    val in = Input(UInt(32.W))
    val out = Output(UInt(32.W))
  })

  val delayBuf = Seq(WireInit(io.in)) ++ Seq.fill(delay)(RegInit(0.U(32.W)))

  delayBuf.zip(delayBuf.tail).foreach { case (curr, next) => next := curr }
  io.out := delayBuf(delay)
}

動き的にはコメントに書いたものが全てで、入力信号indelayに指定したサイクル数遅延させるものだ。

  • delay == 0の場合 ただのパススルー。

f:id:diningyo-kpuku-jougeki:20190417232936p:plain

  • delay == 1の場合 1サイクル遅延する

f:id:diningyo-kpuku-jougeki:20190417232945p:plain

  • delay == 9の場合 9サイクル遅延する

f:id:diningyo-kpuku-jougeki:20190417232955p:plain

テスト用クラス

この遅延モジュールに対して、以下のようなテストクラスを作成する。
it should ...for式でループすることによってループ部分の指定個数分のテストが作成されることになる。
この一回のテストの中で適当な回数io.inの値を変化させて、出力を比較する作りにした。

import chisel3.iotesters.{Driver, PeekPokeTester, ChiselFlatSpec}
import org.scalatest.ParallelTestExecution // これで並列化出来る

/**
  * DelayBufのテストのスーパークラス
  */
abstract class DelayedBufTester extends ChiselFlatSpec {

  behavior of "DelayBuf"

  for (n <- 0 until 600) {
    it should s"${n}サイクル信号が遅れて出力される" in {
      Driver.execute(Array[String](
        "--generate-vcd-output=on",
        s"--target-dir=test_run_dir/delay-${n}" // 出力先を遅延サイクル数でユニークに。
      ), () => new DelayBuf(n)) {
        c => new PeekPokeTester(c) {
          reset()
          step(1)

          poke(c.io.in, n)

          step(n)

          var prev = n
          for (setVal <- 0 until 500) {
            poke(c.io.in, setVal + n)

            for (_ <- 0 until n) {
              expect(c.io.out, prev)
              step(1)
            }
            expect(c.io.out, setVal + n)
            prev = setVal + n
            step(1)
          }
        }
      } should be (true)
    }
  }
}

Driver.executeの第一引数を見てもらえればわかる通り、--target-dirオプションを指定して、出力先をテスト毎に変更するようにした。なおこれを行わなくても、テスト自体は並列に実行され結果も問題なかった。(どうやってるんだろ??)
今回は並列に実行した時の比較を行うために、上記のDelayedBufTesterを継承した以下の2つのクラスを用意して、個々にテストを実行する。

/**
  * シーケンシャルにテストするモジュール
  */
class SequentialTester extends DelayedBufTester

/**
  * パラレルにテストするモジュール
  */
class ParallelTester extends DelayedBufTester with ParallelTestExecution

テストは以下で実行可能。

  • SequentialTesterのみの実行
sbt "testOnly SequentialTester"
  • ParallelTesterのみの実行
sbt "testOnly ParallelTester"

実行結果は以下のような感じにPASSすることが確認できた。以下の結果は遅延サイクル数を0-1サイクルに限定して実施した結果。

[info] Done compiling.
[info] [0.002] Elaborating design...
[info] [0.125] Done elaborating.
Total FIRRTL Compile Time: 285.5 ms
Total FIRRTL Compile Time: 81.4 ms
file loaded in 0.133196499 seconds, 4 symbols, 1 statements
[info] [0.001] SEED 1555506337253
test DelayBuf Success: 500 tests passed in 507 cycles in 0.074487 seconds 6806.58 Hz
[info] [0.056] RAN 501 CYCLES PASSED
[info] [0.000] Elaborating design...
[info] [0.051] Done elaborating.
Total FIRRTL Compile Time: 15.6 ms
Total FIRRTL Compile Time: 17.8 ms
file loaded in 0.030885614 seconds, 6 symbols, 2 statements
[info] [0.000] SEED 1555506338158
test DelayBuf Success: 1000 tests passed in 1008 cycles in 0.032762 seconds 30767.51 Hz
[info] [0.033] RAN 1002 CYCLES PASSED
[info] SequentialTester:
[info] DelayBuf
[info] - should 0サイクル信号が遅れて出力される
[info] - should 1サイクル信号が遅れて出力される
[info] ScalaTest
[info] Run completed in 1 second, 244 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
[success] Total time: 3 s, completed 2019/04/17 22:05:38

並列化の結果

CPUの使用率

では並列に実行した時に、どうなるかを確認してみよう。 まずはCPUの使用率から。
#因みに実施したPCのCPUは"AMD Ryzen 7 1700"。

  • 並列化なしの場合

f:id:diningyo-kpuku-jougeki:20190417232903p:plain

  • 並列化有りの場合

f:id:diningyo-kpuku-jougeki:20190417232757p:plain

実行時間

こちらは結果をわかりやすくするために、遅延サイクル数を0-599にした場合の実行時間となっている。

  • 並列化なし
[info] ScalaTest
[info] Run completed in 19 minutes, 5 seconds.
[info] Total number of tests run: 600
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 600, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 600, Failed 0, Errors 0, Passed 600
[success] Total time: 1151 s, completed 2019/04/16 23:26:50
  • 並列化あり
[info] ScalaTest
[info] Run completed in 1 minute, 48 seconds.
[info] Total number of tests run: 600
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 600, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 600, Failed 0, Errors 0, Passed 600
[success] Total time: 109 s, completed 2019/04/16 23:29:28

まとめると以下のようになった。
バックエンドをverilatorにした場合、各テストごとにビルドが実行されるためそこまで差が出なかった。

バックエンド 並列化なし 並列化あり 実行速度比
treadle 1151秒 109秒 10.6倍
verilator 1707秒 812秒 2.1倍

何がいいって冒頭に書いたとおりParallelTestExecutionをミックスインするだけでいいってとこですよね。これだけで勝手にクラス内のテストまで並列実行してくれるだなんて。。。 因みに複数個のテストクラスがある場合は、何もしなくてもそのテストクラス毎に並列化してくれるみたい。 こういうしっかりした仕組みがあるテストハーネスを使ってハードウェアのテスト実行環境を構築できるのはChselのいいところ。

ということで今日はChiselのテストを並列化して高速化する方法についてでした。