ASIC開発におけるChiselの課題


はじめに

Chiselとはハードウェア記述言語(HDL)の一種です。最近ではRISC-Vというオープンソースのプロセッサの実装に使われることが多く、SystemVerilog/VHDLに次ぐ第3のHDLとしてメジャーになりつつあるようです。

Chiselは言語としてはScalaの内部DSLとして実装されています。つまりソースコードはScalaのソースコードであり、Scalaの豊富な言語機能やエコシステムを全て使うことができます。それによってこれまでのHDLでは難しかった抽象度の高い記述やツールサポートが実現されています。

というわけで「古臭いSystemVerilog/VHDLは捨てて、Chiselに移行しよう」と言いたいところですが、現状のChiselにはいくつか問題点も感じています。数年前にChiselのコードを触っていたので、そこで気付いた点をまとめておきます。
当時やっていたのは「他人が書いた大規模なコードのデバック・改造・ASICへのインプリ」といったところなので、自分で1から書いた場合とはだいぶ違った感想になっているかもしれません。
(また、数年前の話なので現在のChiselでは改善されている点もあると思います。知っている範囲で補足していますが、抜けがあればコメントでご指摘ください)

大きく分けて

の3点について書いていきます。
ChiselやSystemVerilogの言語仕様についてはある程度知っている前提とします。

Chiselの言語仕様にまつわる問題

ChiselはSystemVerilogなどに比べて便利な言語機能がいろいろと実装されています。確かに便利さはわかるのですが、それが仇となっているケースをいくつか感じました。

ビット幅の自動推定

SystemVerilog/VHDLでは基本的に変数のビット幅は全て指定させられますし、特にVHDLではビット幅不一致は厳密にチェックされます。SystemVerilogでもポート幅の不一致についてはチェックしてくれる処理系が多いです。

一方、Chiselではビット幅指定なしで書いても処理系が推定してくれるようになっています。これは書きやすいという部分はあるのですが、非常に見つけづらいバグの原因となります。実際MSBがポート結線中にどこかで消えているようなバグが何件かありました。
これは自動推定している配線の途中で一部だけ明示的に幅指定するような場合に発生します。実装者が自動推定の機構をちゃんと理解していれば回避できますが、よくわかっていない人がなんとなく幅指定したモジュールを間に挟んだりすると、こういうトラブルになります。
コンパイラがWarningでも出してくれればいいのですが、特にそのようなことはなさそうでした。

また、ビット幅の推定に関連して、演算子の使い分けも問題が起きやすいようです。Chiselでは、例えば加算に関して+, +&, +%の3つを提供しており、それぞれビット拡張の仕様が異なります。(++% は拡張無し、+& はありです)
実装者はこれらを適切に使い分けないといけないわけですが、「ビット拡張を考慮せずとりあえず+を使った」のか「ビット拡張しないことを意図して+を使った」のかを区別できません。+がなく、+&+%だけであればまだよかったような気はします。

例えばこんな感じです。

val value = RegInit(0.U(6.W))
when (value + 1.U < 64)

valueは6bitなので最大値は63です。このとき実装者はwhenの条件文は63 + 1 < 64なのでFalseを期待していましたが、実際にはビット拡張されない+を使っているので63 + 1 = 0となり条件はTrueです。
実装者の意図通りにするには+&を使うべきでした。

when

SystemVerilogではあるレジスタの更新条件は一つのalways文に書くことが多いです。しかしChiselでは複数のwhenを使ってイベント毎に分割して書くことができます。
イベント毎に纏まっているので「コンフィグパラメータによって特定のイベントを無効にする」といったものは書きやすいですし、イベントをループやイテレータで発生させることもできます。

しかし「あるレジスタの更新条件を知りたい」と思ったとき、ソースコード全体を検索しなければ正確な条件を知ることはできません。

例えばこんなバクがありました。2つのレジスタが似たような条件で更新されていますが、片方に抜けがあります。

when (a) {
  x := ...
  y := ...
}

...

when (b) {
  x := ...
  // y := が抜けている
}

...

when (c) {
  x := ...
  y := ...
}

同じコードをSystemVerilogで書いたものはこちらですが、個人的にこのコードならなんとなく視覚的なマッチングから「似たような条件で更新されてるけど若干差分があるな。条件抜けてないかな?」と思います。

always_ff ...
  if (c) begin
    x <= ...
    y <= ...
  end else if (b) begin
    x <= ...
    // y <= が抜けている
  end else if (a) begin
    x <= ...
    y <= ...
  end
end

しかしChiselのコードでそれに気づくのは困難に感じました。上記のコードでは抜粋しているので気づきやすいですが、実際にはそれぞれのwhenの間がそこそこ空いていたりします。

もう少し厳しい例として以下のようなものがありました。

when (a) {
  x := ...
  y := ...
}.elsewhen (b) {
  x := ...
}

when (c) {
  x := ...
}
.elsewhen (d) {
  x := ...
  y := ...
}
.elsewhen (e) {
  x := ...
}

ここからyが更新される条件を正しく読み取れるでしょうか?ぱっと見では

if (d) begin
  y <= ...
end else if (a) begin
  y <= ...
end

このような感じに見えるのではないかと思いますが、実際には

if (c) begin
  if (a) begin
    y <= ...
  end
end else if (d) begin
    y <= ...
end begin
  if (a) begin
    y <= ...
  end
end

となります。特に一見yと関係なさそうなcも条件に入ってくるというのが分かりづらいです。この例だけ見ると、「そんなコード書くのが悪い」という気もしてきますが、多人数開発で徐々に条件を増やしていくと、こういったことが起きやすいのではないかと思います。「1つのwhen挿入でFF更新条件全体が大幅に変わりうる」というのはいまいち認識しづらいのではないでしょうか。

特にChiselの主戦場であるGithubのプルリクエストベースの開発とあまり相性が良くないかもしれません。
プルリクエストではソースコードの差分をレビューすることになりますが、この方式では局所的な変更がソースコード全体に影響を及ぼすため、差分ベースでのレビューが難しくなりがちではないかと思います。

クロックとリセット

Chiselは基本的にクロックもリセットも記述せず、暗黙のグローバルクロックと正論理の同期リセットを仮定します。

これはASIC開発言語としてみると、あまり現実的な仮定ではありません。まず、ASICにおいて一般的な負論理の非同期リセットに対応する簡単な方法はありません。(最近非同期リセットは対応したそうですが、正論理だけだったと思います)
また、複数クロックを扱うことは一応可能なものの「普通のI/Oピンと同じ扱いで2つ目のクロックを入力すればよい」という感じなので、あまり自然に取り扱えるという感じではありません。

class A extends Module {
  val io = IO(new Bundle {
    val clockB = Input(Clock())
    val resetB = Input(Bool())
  })

  val a = ...  // 暗黙のグローバルクロック駆動

  withClockAndReset (io.clockB, io.resetB) {
    val b = ... // clockB駆動
  }
}

この点においてChiselはターゲットとしてFPGAを仮定しすぎているのではないかと思います。

Verilogへのトランスパイラであることによる問題

ChiselはVerilogへのトランスパイラとして動作します。これは、シミュレーションや論理合成、その後のバックエンド作業をVerilogベースで行えるという大きな利点があり、この方式でなく全くの新言語を作るというのは現実的ではないと思います。
しかし、トランスパイラであるために発生している問題もいくつか感じています。

Verilogの可読性

これは最近のChisel(というよりVerilog生成ツールであるFIRRTL)のアップデートで改善されたとのことですが、生成されたVerilogの可読性はなかなか厳しいものがありました。
まず、Verilogのwire名は_T_2167といった連番で振られるので、追うのが結構大変です。また、Chisel上では変数をBundleなどで構造化したとしてもVerilog上はバラバラの配線になってしまうので、波形ビューアなどで見るのも大変です。
(SystemVerilogであればinterfacestructを使えば、波形ビューア上も構造化された状態で見ることができます)

さらに、上述したようなwhenの多用により、Verilog上はものすごく深くネストしたif文が生成されます。このあたりを一切見ずに済めば問題ないのですが、実際にはwhenのところで述べたような理由で条件を追わなければならないケースも多々あります。

Chiselとの対応

ChiselとVerilogの対応は以下のようにVerilog上のソースコードコメントでとることができます。これはこれで手掛かりにはなるのですが、Trait定義側の場所しか分からず、どこで実体化しているコードなのかいまいちよくわからないケースもあります。

assign _T_2167 = ...; // @[a.scala 362:74:...]

また、この記述は人間が読むことしか想定しておらず、機械的に処理することができません。(例えばソースコードのフルパスはわかりませんし、記述内容も言語仕様上決まっていません。)
機械的に処理できるフォーマットを決めてデバッガや波形ビューアなどを充実させていくことができれば、開発体験は大きく改善するのではないかと思うのですが、今のところそのような方向性はないようです。

Scalaの内部DSLであることによる問題

ChiselはScalaの内部DSLであるため、LexerやParserなどの実装を省略できたり、IDEなど既存のScalaの資産を使うことができるなど、多くのメリットを得ています。しかし、内部DSLであることによる問題もいくつかありそうです。

コンパイル速度

ChiselからVerilogへのコンパイルはかなり時間がかかります。大きめのCPUデザインなら30分程度かかることも珍しくないと思います。Chiselだけで検証が完結して、最後に一度だけVerilogにする、といった運用が可能なら問題ないのですが、シミュレータやテストベンチなどVerilog系の環境を使うには毎回30分待たされることになります。
Scalaはもともとコンパイルが速い言語ではないですし、Verilogへの変換を担うFIRRTLの処理速度にも問題がありそうです。
Scala自体は十分高速に書ける言語なのですが、パフォーマンスについては結構気を使わないといけない部分があると思っています。おそらく開発リソースを投入すれば解決するのだと思いますが、そういった部分まで手が回っていない印象です。

コンパイルエラー

ソースコードに問題があればコンパイルエラーが出ますが、適切なメッセージが出ないことが多いです。
特にJVMのスタックトレースだけ吐かれることがあり、その場合の原因特定はなかなか難しいです。
もちろん商用のEDAツールであってもいきなりSEGVで死ぬこともありますし、程度問題ではあります。

ただ、Scalaの内部DSLであることにより、適切なエラーメッセージを出すのが難しくなっている部分はありそうです。
Scalaコンパイラが出すエラーメッセージはあくまでChiselをScalaとしてみたときのエラーであり、Chiselとしてのエラーは実行時エラーとして出すしかありません。特に複雑にパラメータ化されたモジュールのパラメータ不整合に関するエラーは、適切なメッセージを出す責任が各モジュール設計者に委ねられており、将来的にも統一的で適切なエラーメッセージというのは期待し辛い状況です。

構文

ChiselはScalaの内部DSLであるため、Scalaにない新規構文は追加できません。Scalaは他言語と比較しても、内部DSLが自然に見えるような機能をいろいろと提供していますが、そうはいっても限界があります。

ChiselとSystemVerilogで同じコードを書いたものが以下です。

class Test extends Module {
  val io = IO(new Bundle {
    val i = Input(UInt(32.W))
    val en = Input(Bool())
    val o = Output(UInt(32.W))
  })

  val r = RegInit(0.U(32.W))

  when (io.en) {
    r := io.i
  }

  io.o := r
}
module Test (
  input         clk,
  input         rst,
  input  [31:0] i,
  input         en,
  output [31:0] o
);

  always_ff @(posedge clk) begin
    if (rst) begin
      o <= 32'h0;
    end else is (en) begin
      o <= i;
    end
  end
endmodule

このレベルの構文で個人的にいまいちだと思うのは

  • ioInput/Outputなど、構文上特別であってほしいものが普通の変数や型に見える
  • リテラルやビット幅指定など、メソッド呼び出し形式が多用される

といったところです。いずれもScalaの内部DSLである以上回避しようがないのは分かるのですが、新言語である以上、構文的な改善も期待したいところで、それに対して十分応えられているとは言えないのではないかと思います。

一般的に内部DSLを使うメリットというのは、ホスト言語とDSLをシームレスに結合可能、というところがあると思います。
(例えば「SQLを専用構文で書いて、組み立てたクエリをホスト言語から呼ぶ」といったような)
しかしChiselにおいてはそのようなホスト言語とのやり取りの必要性が低く、ホスト言語の構文に制約を受けるというデメリットだけが見えてしまっているように感じます。

まとめ

というわけでChiselについて感じている問題点について書いてみました。
残念ながら現時点では汎用のASIC開発言語をSystemVerilogからChiselへ乗り換えることは難しいと思っています。
(RISC-V実装など既存のChisel資産を部分的に使うのはありだと思います)

今回書いた課題には実装の努力で改善できそうな部分も多くあるので、今後に期待したいです。