はじめに
この記事では私が開発しているKaruta(カルタ)というプログラミング言語と処理系の簡単な紹介をします。
Karutaはプログラミング言語としてはGoやJavaScriptのような雰囲気になるように作っていますが、主にFPGAで動く回路を設計することを目的としています。
Karutaは https://github.com/nlsynth/karuta からソースコードを入手してビルドし、karutaコマンドを実行できる状態にすれば試せるのですが、この記事を書いてる時点では品質が微妙なので間違いなく色々なバグや珍動作に遭遇するのでご了承ください。
(Ubuntuをご利用の方は $ snap install karuta
でインストールできます)
(Karutaのようなプログラミング言語からの回路を生成する技術は「高位合成」と呼ばれており、興味のある方はこちらからご覧になってください https://qiita.com/nekoaddict/items/cddde13a1322e94f44b8 )
とりあえずスクリプト言語
とりあえずHello worldです。hello.karuta というファイルを作り
print(“Hello, world!”)
という内容で保存してコマンドラインから
$ karuta hello.karuta
とやると
print: Hello World
って表示されます。定義したメソッドを呼び出す形で次のようにも書けます
func main() {
print(“Hello, world!”)
}
main()
ちょっと計算をします。
shared x int[4]
func main() {
var t int = 0
for var i int = 0; i < 2; ++i {
t += x[i] * x[i]
}
x[2] = t
}
x[0] = 3
x[1] = 4
main()
print(x[2])
スクリプト言語なのに型を書いて宣言しているところに違和感があるかもしれませんが、どこかで見たような言語仕様かと思います。このまま実行すると25と表示されます。
アクセラレータを作れるか?
さて、ここで話は大きく変わりますが、FPGAでなんらかの計算のアクセラレータを書きたくなったとします。
今時はほとんどのFPGAがarm社のバス規格AXIに準拠することを想定した作りになってるので、次のようなアーキテクチャを想定します。
一般的に見かける構成ですが、CPUがslave側から動作に必要なパラメータを設定し、アクセラレータはmaster側から外部のメモリにアクセスして計算を行うという形です。
まずAXI slave側ですが、
@AxiSlave()
shared regs int[4]
と配列の宣言に@AxiSlave()というアノテーションを付けることでこの配列が外部からアクセスできるようになります。master側は@AxiMaster()を付けることで、外部に対してデータ転送を行えるようになります。
@AxiSlave()
shared regs int[4]
@AxiMaster()
shared buf int[4]
func main() {
// slave側へのアクセスを待つ
regs.waitAccess()
// slaveの0ワード目の値をアドレスとして使う
var addr int = regs[0]
// メモリ上のaddrから2ワードをbuf[0]以降に読み込む
buf.load(addr, 2, 0)
buf[2] = buf[0] * buf[0] + buf[1] * buf[1]
// メモリ上のaddr + 8に1ワード分buf[2]以降から書き込む
buf.store(addr + 8, 1, 2)
}
master側の配列のload()やstore()といったメソッドを呼ぶことで外部のメモリとのデータ転送を行っています。
スクリプト言語として実行することもできますが(別のスレッドを作ってregsにアクセスする記述が必要です)、FPGAの設計にしたいので続きはこんな感じです。
compile()
writeHdl(“my_accelerator.v”)
これをkarutaコマンドに与えて実行するとmy_accelerator.vというVerilogファイル(手元で実行したところ1173行)が出力されます。AXIのmaster, slaveのポートを1セットずつ持ったモジュールが含まれるので、お手元の合成ツール(Vivadoでは確認済み、Quartusはそのうち確認します)に入力して実機動作が可能になるはずです。
(配列を複数個宣言することで、複数のAXIインタフェースを使えます)
こんなノリでコードを書いていくことで今流行りの例のジャンルやあのアプリの回路が簡単に書ける!というのを目標として日々開発しています。
紹介しなかった機能
オブジェクト指向の利用
ここまでの説明では触れませんでしたが、Karutaはプロトタイプベースのオブジェクト指向言語です。
shared M.sub object = clone()
func M.sub.g() {
// 何か処理をする
}
func M.f() {
// メソッド呼び出し
sub.g()
}
というように複数のオブジェクトからなる設計を作り、メンバー変数やメソッドにアクセスすることができます。
また、compile()メソッドはオブジェクトの構成のスナップショットを取ってRTLに変換する操作を行います。
並列処理とコミュニケーションの機能
ここまでの説明では処理はmain()から始まっていましたが、メソッドにアノテーションを付けることで任意のメソッドをスレッドの入り口とすることができます。
func main() {
// 実行される
}
@ThreadEntry()
shared f() {
// これも実行される
}
@ThreadEntry(num=4)
shared th(idx int) {
// 4並列で実行される(同じステートマシンが4つ出力される)
// スレッドローカルなメンバー変数や配列を使えば簡単に並列処理が書ける…はず
}
複数のスレッド間が同じ配列やメンバー変数をアクセスする場合、調停する回路が出力されます。また、channel(FIFO)やmailboxといった機能を利用してコミュニケーションができます。
mailbox mb int
channel ch int
@ThreadEntry()
func th1() {
mb.notify()
ch.write(123)
}
@ThreadEntry()
func th2() {
mb.wait()
ch.read()
}
HDLとの連携
スクリプト言語だけで記述するのが困難な回路はVerilogで記述し、Karuta言語のメソッドでラップする機能があります。これと演算子のオーバーロードを組み合わせることで、FP16などのカスタムな演算器を持ったデータ型を定義することができます。
@ExtIO(input = “dipsw”, output=”led”)
func f(led bool) (bool) {
// 合成した際、Verilogにoutput ledとinput dipswを生成し、
// 引数の値をledに出力しdipswの値を返り値として返す動作をする。
return 0
}
@ExtCombinational(resource=”extmod”, verilog=”extmod.v”, module=”extmod”)
func f(arg0, arg1 #32) (#32) {
// 外部にあるextmod.vのモジュールextmodをインスタンス化し、そこに定義された
// 組み合わせ回路に引数を入力して出力を返り値とする
return 0
}
@ExtEntry()
func f(arg0 #32) (#32) {
// このメソッドを外部からハンドシェイクによって呼び出せるようになる
// 合成後のトップレベルモジュールのinputから引数を受け取り、outputから返り値を返す
return 0
}
@ExtStub()
func f(arg0 #32) (#32) {
// このメソッドと同じsignatureの外部モジュールをハンドシェイクで呼び出す
// 合成後のトップレベルモジュールのoutputから引数を渡し、outputから返り値を受け取る
return 0
}
IRの機能
Karutaと同時に中間言語Iroha (Intermediate Representation Of Hardware Abstraction)
https://github.com/nlsynth/iroha の開発も進めており、最適化やVerilogの出力に利用しています。
その他の機能
他にも色々な機能がありますが、特徴的なものを挙げます
// 16bitの値を二つ受け取って、それを逆にして返す
// (返り値は引数の次に書きます)
func swap(x, y #16) (#16, #16) {
return (y, x)
}
// ビット選択と連結の演算子を使ってエンディアンを変える
func bswap(x #32) (#32) {
return x[7:0] :: x[15:8] :: x[23:16] :: x[31:24]
}
カスタムなデータ型を定義する機能もあって、FP16や複素数等を定義するのに便利になるように開発をしてます。
最後に
Karutaの現状
Karutaは今のところ僕の余暇の時間だけで開発されていて、一通りの機能は揃ったものの信頼性や性能の面ではまだまだなのが現状です。今から数年かけてその辺りを改善しつつ、用途やビジネスモデル(というか、楽しみ方)の方向性を模索していければと思っています。
日々の進捗もしくは愚痴はこちら > https://twitter.com/neonlightdev
高位合成友の会について
RTLのハードウェア記述言語を直接使わずにここで説明したような抽象度の高いプログラミング言語を使って設計をする手法は「高位合成」と呼ばれていますが、業界の長年の努力にも関わらずイマイチな普及具合なのが現状です。
そんな状況で自分が使う気になれるような処理系を作ろうとアマチュアやアカデミックあるいは半プロな開発者達が自作の処理系を持ち寄って「高位合成友の会」というイベントを不定期に開催しています。ご興味を持たれた方は参加頂けると幸いです。
また、多くの開発者がGitHubやTwitterでのコンタクトを受け付けていますので、適宜コミュニケーションを取っていただければ幸いです。
過去のイベントはこちら > https://hls.connpass.com/
独自言語を作りはじめた理由
今まで開発された高位合成の処理系の大半がCやC++(もしくはその変種)を入力とし、一部がPythonやJavaあるいはその他一部で人気の高い言語を入力としています。ソフトウェアを記述するための既存の言語を入力とした場合、ユーザーが読み書きに慣れていて既存のコードの資産も流用できるというメリットがあると考えられますし、言語処理系の開発者にとっても色々と楽ができます。
しかしながら、それらの言語は元々ソフトウェアを効率よく記述してCPUで効率よく実行することを想定しているため論理回路固有の機能を追加するのに不便だと感じることがありました。また、論理回路の記述のために元の言語を拡張したくなることもありますが、互換性やシンプルさを保ったまま行うのが困難だったりもします。
この状況認識で我慢するか違う言語を探すか新言語を作ってしまうという選択肢がありますが、とりあえずKarutaは自作を選んでみました。うまくいくかわかりませんが、今のところ楽しんでます。