Karuta HLS CompilerにおけるHDL埋め込み


この記事はKMC Advent Calendarその2の一部として執筆しました。卒業は20年近く前になってしまうのですが…

はじめに

Karutaはどこにでもいる平凡でごく普通のスクリプト言語と処理系です。(コードは githubにあります)

例えばHello Worldを書きたい時なんかは

print(“Hello World!”)

という感じで極めて退屈に書けます。そこで多少はひねくれたコードを書いてみようと思います。

@ExtIO(output=”led”)
func f(b bool) {
  print(b)
}

process main() {
  var b bool
  while true {
    b = ~b
    wait(100000000)
    f(b)
  }
}

何か変なアノテーションが付いてたりwait()ってなんだよって辺りはスルーして頂いて、これを実行するには
$ karuta sample.karuta --run

といった感じでコマンドラインから--runを付けます。画面に0と1が交互に出てくるのですが、それ以上のことは起きないので適当にCtrl+Cか何かで止めてやってください。

さて、平凡と紹介された奴が平凡だった試しはあんまりないのですが、次は
$ karuta sample.karuta --compile

って実行してみます。処理のログっぽいのが表示されますが、sample.vってファイルが生成されているっぽいですね。ファイル名からすると今年話題になったV言語!かなと思って見てみると…

// Generated from iroha-0.0.1.
// Copied from karuta_wait.v
module wait_cycles(clk, rst, req_valid, req_0, req_ready);
   input        clk;
   input        rst;

<中略>
module sample(
  output led,
  input clk,
  input rst);
  sample_main sample_main_inst(.led(led), .clk(clk), .rst(rst));

endmodule
<後略>

…えーっと、ご存知でしょうか?Verilogってやつです。論理回路を記述するためのアレです。これを適切なツールに入力し、FPGAに回路をダウンロードするとLEDをピカピカさせることができます(ちょっと早いですがメリークリスマス!)。

(撮影は ikubaku10 さん)

論理回路の高位合成

ここまで読んでしまったけど何の話かわからなかったという方は申し訳ございません。本Advent Calendarの他の記事をお楽しみいただければと思います。

これはいわゆる論理回路の高位合成という技術で、一般論は拙著の「 論理回路の高位合成について」 、ここで紹介したKarutaについては 「FPGA向け論理回路設計のためのプログラミング言語処理系 Karuta の紹介」 等、あるいはネット上の他の情報を参考にしてください。

Verilogの埋め込み

拙作の処理系Karutaは様々な工夫を実装してみて有用性を確かめようとしているのですが、この記事ではVerilogを埋め込む機能を簡単に説明します。この機能はソフトウェアの言語処理系におけるinline assemblerのようなもので、低レイヤーのコード(アセンブラやRTL)として書いた方が良い内容を埋め込んでしまうことができます。

埋め込んだ回路とのハンドシェイク

前節の例でのLED点滅のコードではwait()というメソッドを呼び出すことで指定したクロックの間待つ処理を実装しています。Karutaを含めた普通の高位合成はクロック単位の振る舞いをユーザーから隠すことによって書きやすさを実現するため、クロックを扱うのはVerilogのようなRTLを使うのが楽です。
ということで、Karutaのwait()メソッドの定義では実装がVerilogでなされていることがアノテーションによって指示されています。

@embed(resource = "wait_cycles",
 verilog = "karuta_wait.v",
 file= "copy",
 module= "wait_cycles")
func Kernel.__wait(cycles int) {
}

karuta_wait.vの中身は下記のようになってます。( github上のコード )

module wait_cycles(clk, rst, req_valid, req_0, req_ready);
   input        clk;
   input        rst;
   input        req_valid;
   input [31:0] req_0;
   output       req_ready;

   reg          req_ready;

   reg [1:0]    st;
   reg [31:0]   cycles_left;

   always @(posedge clk) begin
      if (rst) begin
         st <= 0;
         cycles_left <= 0;
         req_ready <= 0;
      end else begin
         case (st)
           0: begin
              if (req_valid == 1) begin
                 cycles_left <= req_0;
                 st <= 1;
              end
           end
           1: begin
              cycles_left <= cycles_left - 1;
              if (cycles_left == 0) begin
                 st <= 2;
                 req_ready <= 1;
              end
           end
           2: begin
              st <= 0;
              req_ready <= 0;
           end
         endcase
      end
   end

endmodule

この機能の本来の目的はVerilogで書かれた回路を取り込み、何クロックもかかる計算を投げるための物ですが、このwait()の実装ではreqを受けてからreq_0で指定されたクロックの間何もしないで待ってからackを返すという動作になっています。

組み合わせ回路の埋め込み

ハンドシェイクの回路を埋め込んだ次には組み合わせ回路を埋め込む例を説明します。組み合わせ回路の場合にはKarutaの言語で書けるべきなのかもしれませんが、十分に複雑な場合は組み合わせ回路として合成するのが困難なのが現状なのでKarutaで手続き的に書いて、実際の合成にはVerilogの記述を使うという手法を取ってます。

次のコードはFP16の乗算の1ステージ目のコードで、Karutaのスクリプト言語で実行する場合はこのコードが実行されますが、Verilogを出す時にはfp/fp16bmul.vというファイルのFP16BMulS0Of2というmoduleがコピーされます。

@ExtCombinational(resource="fp16bmul0", verilog="fp/fp16bmul.v", module="FP16BMulS0Of2", file="copy")
func FP16B.mul0of2(a0, a1 #16) (#0, #8, #8, #9) {
  var ret0 #0
  var ret1, ret2 #8
  var ret3 #12
  ret0 = a0[15:15] ^ a0[15:15]
  ret1 = a0[14:7]
  ret2 = a1[14:7]
  var e0, e1 #1
  if ret1 == 0 {
    e0 = 0
  } else {
    e0 = 1
  }
  if ret2 == 0 {
    e1 = 0
  } else {
    e1 = 1
  }
  var ff0 #8 = e0 :: a0[6:0]
  var ff1 #8 = e1 :: a1[6:0]
  var z #16 = ff0 * ff1
  ret3 = z[15:7]
  return ret0, ret1, ret2, ret3
}

Verilogの方は下記のようになってます。

module FP16BMulS0Of2(
 input         clk,
 input         rst,
 input [15:0]  arg_0,
 input [15:0]  arg_1,
 output        ret_0,
 output [7:0]  ret_1,
 output [7:0]  ret_2,
 output [8:0] ret_3);

  // 略:arg*からret*を作る組み合わせ回路
  // 動作としてはFP16B.mul0of2()と同じ

endmodule

こんな感じで作った組み合わせ回路のステージを二つ組み合わせることで乗算回路が作れます。

func FP16B.Mul(x, y #16) (#16) {
  return mul1of2(mul0of2(x, y))
}

今後はbfloat16等や複素数、RGBやそれらのSIMDをライブラリ的に充実させて有用になるか試していく予定です。

最後に

この記事では筆者の開発している処理系Karutaの機能の一つであるVerilogの埋め込みを説明しました。何か興味を持たれましたら筆者にお気軽に連絡して頂ければ幸いです(Twitterは @neonlightdev )。KMCでもFPGAの勉強会が行われておりますので、学生の方は参加をご検討をお願いします。また、(!京都近辺の学生)な方は「高位合成友の会」(Slack teamもあります)をよろしくお願いします。

Karuta自体は言語とその処理系として必要な機能は大体揃った感じなので、来年以降は実用に近い例題を実用に近い品質で動かせるように開発を進めていければと考えてます。