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

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

Chisel Bootcamp - Module3.6(4) - Scalaの型パラメータ(ジェネリクス型)のChiselへの応用について

スポンサーリンク

前回のChisel-Bootcampの学習ではChiselの異なる方同士の接続の法則についてを見ていった。

www.tech-diningyo.info

今回も引き続きModule3.6を見ていく。今日はジェネリクスとChiselの型の階層について。

ジェネリクス

ジェネリクスについて、、、とか書いておいて何なんだけど、よく知らんのよね、この概念自体を。。 ということで毎度お世話になります、Wikipediaさん。

総称型(generic type)、あるいはパラメタ付型(parametric type)とは、型付けされたプログラミング言語においてデータ型の定義とそれを参照する式(型式)の一部にパラメタを許すことによって類似した構造を持つ複数のデータ型を一括して定義して、それらを選択利用する仕組みである。
https://ja.wikipedia.org/wiki/%E7%B7%8F%E7%A7%B0%E5%9E%8B

なるほど、わからん。 ということで探した別の方の記事がこちら

qiita.com

あれだ、ScalaDocとか見てるとよく見るやつ。

abstract class ReadyValidIO[+T <: Data](gen: T) extends Bundle

chisel.eecs.berkeley.edu

あんまり深くは突っ込まないで、そこに指定してある型しか入らんのだろーな、位に考えてた。 とりあえずBootcampのサンプルを見ていこう。

サンプルコードその1

これは紹介するのに丁度いいコード、、ということらしい。 各々のコメント部分に書いてあるのが、Seqの中に含まれているデータの型を示している。

val seq1 = Seq("1", "2", "3") // Type is Seq[String]
val seq2 = Seq(1, 2, 3)       // Type is Seq[Int]
val seq3 = Seq(1, "2", true)  // Type is Seq[Any]

サンプルコードその2

上記は生成時にデータを入れており、それを元にコンパイラ型推論出来るが、そうは行かないケースもあるというのが次の例。 このケースでは、空のSeqを作ることになるので型の指定をしないとval defaultの型が定まらずに以降の処理が出来ない、ということらしい。

//val default = Seq() // Error!
val default = Seq[String]() // ユーザーはコンパイラにデフォルトの型がSeq[String]と教えてやる必要がある
Seq(1, "2", true).foldLeft(default){ (strings, next) =>
    next match {
        case s: String => strings ++ Seq(s)
        case _ => strings
    }
}
  • 実行結果
default: Seq[String] = List()
res2_1: Seq[String] = List("2")

サンプルコードその3

次がジェネリクスを使った例。Scalaでは型パラメータが正しい名称っぽい??

def time[T](block: => T): T = {
    val t0 = System.nanoTime()
    val result = block
    val t1 = System.nanoTime()
    val timeMillis = (t1 - t0) / 1000000.0
    println(s"Block took $timeMillis milliseconds!")
    result
}

// 1から100万までを足し合わせる
val int = time { (1 to 1000000).reduce(_ + _) }
println(s"Add 1 through a million is $int")

// 100万以下の数字から16進文字列に直した時にbeafを含む最大の数字を見つける
val string = time {
    (1 to 1000000).map(_.toHexString).filter(_.contains("beef")).last
}
println(s"The largest number under a million that has beef: $string")
  • 実行結果
Block took 15.082196 milliseconds!
Add 1 through a million is 1784293664
Block took 105.76495 milliseconds!
The largest number under a million that has beef: ebeef

動きを見てると、Tを返す匿名関数をもらって、Tを結果として返す、、ということになる、、はず。 ここはもう少しきちんと見なきゃだなぁ。。

Chiselの型の階層

このジェネリクスの考えをChiselでどのように使うのか、というのがここからの話。 うまく使っていくためには、Chiselの基本的な型がどのような構造をしているのかを知っておく必要があるとのこと。 文字で書くと、直感的でないので図にしたものをペタっと。

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

見てもらうとわかる通りChiselの設計で使用されるUIntBoolBundleなどなどは全てDataから派生したものになっている。 このDataにChiselでハードウェアを記述する際に使用する:=のようなメソッドが定義されている。 以下はDataのコードから抜粋したもの

abstract class Data extends HasId with NamedComponent {

  ~略~

  final def := (that: Data)(implicit sourceInfo: SourceInfo, connectionCompileOptions: CompileOptions): Unit = this.connect(that)(sourceInfo, connectionCompileOptions)
  final def <> (that: Data)(implicit sourceInfo: SourceInfo, connectionCompileOptions: CompileOptions): Unit = this.bulkConnect(that)(sourceInfo, connectionCompileOptions)

このようなDataから派生したデータをRegWireなのでChiselのハードウェアを構成する要素に入れることで、そのDataの情報に基づいて、レジスタを実体化することが出来るようになっている。

  object Reg {
    import chisel3.core.{Binding, CompileOptions}
    import chisel3.internal.sourceinfo.SourceInfo
    import chisel3.internal.throwException

    // Passthrough for chisel3.core.Reg
    // TODO: make val Reg = chisel3.core.Reg once we eliminate the legacy Reg constructor
    def apply[T <: Data](t: T)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): T =
      chisel3.core.Reg(t)

ぶっちゃけ、このことだけ知ってればそれなりにChiselのライブラリをどう使うかは結構わかるような気もする。
以前はVecを使ってレジスタを複数個まとめて定義するときにVec(Reg(UInt(8.W)))じゃないの??とか思ったりもしたんだけど、上記のVecDataから派生していることを抑えておけばReg(Vec(4, UInt(8.W)))になるというのは頷ける話。

例題:ジェネリクスを使ったシフトレジスタ

最後にここまでの話をまとめた例題がBootcampに載っているので、それを見て今日はお終いにしよう。

class ShiftRegisterIO[T <: Data](gen: T, n: Int) extends Bundle {
    require (n >= 0, "Shift register must have non-negative shift")

    val in = Input(gen.cloneType)
    val out = Output(Vec(n + 1, gen.cloneType)) // + 1 because in is included in out
    override def cloneType: this.type = (new ShiftRegisterIO(gen, n)).asInstanceOf[this.type]
}

class ShiftRegister[T <: Data](gen: T, n: Int) extends Module {
    val io = IO(new ShiftRegisterIO(gen, n))

    io.out.foldLeft(io.in) { case (in, out) =>
        out := in
        RegNext(in)
    }
}

class ShiftRegisterTester[T <: Bits](c: ShiftRegister[T]) extends PeekPokeTester(c) {
    println(s"Testing ShiftRegister of type ${c.io.in} and depth ${c.io.out.length}")
    for (i <- 0 until 10) {
        poke(c.io.in, i)
        println(s"$i: ${peek(c.io.out)}")
        step(1)
    }
}

Driver(() => new ShiftRegister(UInt(4.W), 5)) { c => new ShiftRegisterTester(c) }
Driver(() => new ShiftRegister(SInt(6.W), 3)) { c => new ShiftRegisterTester(c) }

これまでBootcampを通して作ってきたモジュールと大きく異なるのはクラスの宣言時にジェネリクスを使っており、使う際にChiselのどの型で生成するかを決定できるようになっていること。 この例ではUIntSIntの切り替えのみの例になっているが、BundleもChiselのDataから派生しているため、これをうまく使うことでいろいろパラメタライズの幅が広がりそう。 そのへんをうまく使ってるのがChiselの各種ライブラリということだな。

  • 実行結果
[info] [0.001] Elaborating design...
[info] [0.059] Done elaborating.
Total FIRRTL Compile Time: 712.0 ms
Total FIRRTL Compile Time: 29.8 ms
End of dependency graph
Circuit state created
[info] [0.001] SEED 1552313783108
[info] [0.003] Testing ShiftRegister of type chisel3.core.UInt@25 and depth 6
[info] [0.007] 0: Vector(0, 0, 0, 0, 0, 0)
[info] [0.009] 1: Vector(1, 0, 0, 0, 0, 0)
[info] [0.016] 2: Vector(2, 1, 0, 0, 0, 0)
[info] [0.018] 3: Vector(3, 2, 1, 0, 0, 0)
[info] [0.019] 4: Vector(4, 3, 2, 1, 0, 0)
[info] [0.021] 5: Vector(5, 4, 3, 2, 1, 0)
[info] [0.023] 6: Vector(6, 5, 4, 3, 2, 1)
[info] [0.025] 7: Vector(7, 6, 5, 4, 3, 2)
[info] [0.027] 8: Vector(8, 7, 6, 5, 4, 3)
[info] [0.028] 9: Vector(9, 8, 7, 6, 5, 4)
test cmd4HelperShiftRegister Success: 0 tests passed in 15 cycles taking 0.048256 seconds
[info] [0.031] RAN 10 CYCLES PASSED
[info] [0.000] Elaborating design...
[info] [0.010] Done elaborating.
Total FIRRTL Compile Time: 26.6 ms
Total FIRRTL Compile Time: 15.6 ms
End of dependency graph
Circuit state created
[info] [0.000] SEED 1552313784311
[info] [0.001] Testing ShiftRegister of type chisel3.core.SInt@1f and depth 4
[info] [0.002] 0: Vector(0, 28, 28, 28)
[info] [0.004] 1: Vector(1, 0, 28, 28)
[info] [0.005] 2: Vector(2, 1, 0, 28)
[info] [0.006] 3: Vector(3, 2, 1, 0)
[info] [0.007] 4: Vector(4, 3, 2, 1)
[info] [0.009] 5: Vector(5, 4, 3, 2)
[info] [0.010] 6: Vector(6, 5, 4, 3)
[info] [0.011] 7: Vector(7, 6, 5, 4)
[info] [0.013] 8: Vector(8, 7, 6, 5)
[info] [0.015] 9: Vector(9, 8, 7, 6)
test cmd4HelperShiftRegister Success: 0 tests passed in 15 cycles taking 0.016668 seconds
[info] [0.016] RAN 10 CYCLES PASSED

概念はこれまでのChisel-Bootcampの学習やChiselのソースコードを見ていてわかってきているが、使いこなしてパラメタライズの幅を広げるにはもう少し自分で書いてみるしか無いぁ、、、というのが正直なところ。とりあえずこういう機能があって、いろいろ出来そうということを踏まえて自分で実装する際の選択肢の一つとして試していかねば。