The Embedded Rust Book翻訳の気づきメモ①
Introduction - The Embedded Rust Book
お仕事でもRustをやりたいな、と考えて、組込みでRustを広めるために翻訳を行っています。 翻訳を進める中で、技術的にも英語的にも、学びがあるので、それを綴っていきます。
一応今の状況としては、Introductionの翻訳が完了しています。
ちゃんとページとして見える環境の整備もTo Doです。
英語
始めてみた単語を並べていきます。
英単語 | 意味 | コメント |
---|---|---|
fiddle | ~をいじくる | ハードウェアをいじる、という文脈で出てきます。 |
Rust
cargo generate
存在を忘れがちなcargo generateさん。 Cargo.tomlのauthorやproject-nameを埋めてくれるので、教材のテンプレートを作るときには便利そうです。
entry attribute
#[entry]
attricuteを指定した関数が、エントリーポイントになります。
ざっくりソースコードを確認したところ、#[export_name = "main"]
でattributeをつけた関数をmain
というシンボルでエクスポートするようです。
quote!( #[export_name = "main"] #(#attrs)* pub fn #hash() -> ! { #(#vars)* #(#stmts)* } ).into()
後は、リンカスクリプトでシンボルにアドレスを割り振れば、それでエントリポイントになりますね。
その他
QEMUのセクションですが、最初のプロジェクトテンプレート作成あたり、めっちゃ親切です。
テンプレート作成するだけで、cargo generate
使う方法、git
を使う方法、両方使わない方法
の3通りも書いてくれています。
このホスピタリティは、自分にはないものですね…。見習いたいです。
あー、QEMUのページ長い…。
明けましておめでとうございます 2019 edition
明けましておめでとうございます。
新年新たな気持ちで、今年の目標を書いておこうと思います。
2018年の振り返り
前半は、テスト駆動開発やC++、チーム全体のレベルを上げる活動に力を入れていました。
反面、後半は、低レイヤへの情熱を取り戻し、自分だけのために次の技術に広く手を出しました。
また、Rustと運命の出会いを果たし、エミュレータを書いたり、ベアメタルプログラミングしていました。
2019年の目標
技術的な側面からやりたいと考えていることは、去年からの延長です。 LLVMのバックエンドをいじる方法や、Rust、RISC-V、OSに継続して取り組んでいきます。 ずっと取り組んでいるRustのエミュレータも、一区切りつけたいところです。
お仕事的には、自分がやりたいことを仕事にできるように、自分で仕事を取ってこれるようになりたいです。
登壇やブログ、twitterを通じて、人との関わりを増やしていきたいです。
それでは、本年もよろしくお願いいたします。
RISC-VのFENCE.I命令を調べる
rv32i基本命令をざっと眺めていて、この命令だけなんだろう?と思ったので調査します。
RISC-V specification v2.2の2.7 Memory Model
セクションに記載があります。
hardware thread間でのメモリ操作命令実行順序を保証するための明示的な命令がFENCE
命令、とあります。まぁ、わかります。
specを読み進めると、命令ストリームとデータストリームを同期するために利用する、とあります。
自己書き換えプログラムを考えるとわかりやすいです。自己書き換えプログラムは、実行中のプログラムが、プログラム自身を書き換えるものです。
上記のような自己書き換えプログラムでは、 自身で書き換えた結果の命令 をフェッチしてくる必要があります。 通常、プロセッサは複数ステージのパイプラインで構成されるため、命令を書き換えるstoreが完了する前に、次の命令をフェッチしています。 書き換えた後の命令をフェッチしないといけないところで、命令書き換え前の命令をフェッチしていると、正しくプログラムが動作しないわけです。
そこで、FENCE.I
命令で、書き換わった命令メモリの内容を参照することを保証します。
実装する方法はいくつかあり、パイプラインと命令キャッシュをフラッシュする、というのがシンプルな実装になります。
命令フェッチはやり直しになるため、書き換わった命令がフェッチできるわけです。
ということで、真面目にパイプライン構成を作り始めるとケアする必要がある命令ですね。
2018年に読んだ本をまとめよう
これ、毎年やらないと忘れちゃうので備忘録がてら。 大雑把なジャンル別にまとめます。
書評を書く気力が今はないので、羅列だけです。
システムプログラミング、低レイヤ技術
今年の後半あたりから、システムプログラミングや低レイヤ技術に注力しはじめました。
Linux
Linux Device Driver Development
は、組込みLinux屋さん必読です。誤植が多くて、英語、というハードルはありますが、Linux kernel 4系のdevice driverを解説している本は、現在希少です。
OS/仮想化
言語処理系
Go言語でつくるインタプリタがとても良かったです。テスト付きで1歩1歩何かを作る解説書は非常にありがたいです。
FPGA/ハードウェア
アジャイル/組織論
ちょっとくくりが雑ですが、管理
とかプロセス
という言葉を使いたくないので、アジャイル/組織論という項目でまとめてみました。
- アジャイルレトロスペクティブス
- アジャイルな見積りと計画づくり
- リーン・スタートアップ
- カイゼン・ジャーニー
- [イラスト解説]ティール組織
- エンジニアリング組織論への招待
- マネジメントキャリアパス
- Team Geek
- ベタープログラマ
プログラミング
主に、RustとC++に注力していました。C++は社内で勉強会の主催をしたりしました。
- プログラミングRust
- 江添亮の詳説C++17
- Effective C++
- Modern Effective C++
- C++テンプレートテクニック
- エキスパートPythonプログラミング
- アジャイルソフトウェア開発の奥義
- Clean Code
- ユースケース駆動開発実践ガイド
クラウド
その他
薄い本
- 超苦労したFPGAの薄い本
- UEFI読本 基礎編 Linux編
- 自作OS自動化のPoCとしての遺伝的MBR
- C++でできる!OS自作入門
- フルスクラッチで作る!UEFIベアメタルプログラミング1,2
- Memory Allocator Aquarium
- Ryzen SEGV Battle
雑誌
Interfaceとトランジスタ技術を少々。
来年は、もう少し理論系も読もうかな~。
2018年の振り返り
今年もそろそろ終わろうとしているので、2018年に学んだことをまとめておきます。 今年は、下半期での学びが濃かったです。
分岐点は、TCFMに出合ったことと、仕事のプロジェクトを変えたこと、ですね。 その中で、自分はやはり低レイヤの技術が好きなんだ、ということが自覚できたため、下半期はひたすら低レイヤ技術を楽しんでいました。
とにかく、15分でも30分でも毎日プログラミングする、という目標のもと、活動したのも良かったです。
サマリ
2018年
- テックリードっぽい役割で頑張った
- TCFMとの運命の出会い
- 低レイヤ技術に広く浅く色々手を出した
- アウトプットを増やした
- 毎日プログラミングした
- Rustとの運命の出会い
2019年やりたいこと
- アウトプットをさらに増やす
- 低レイヤ技術の深堀り
- 毎日プログラミングする
- Rustの仕事を取ってくる
2018年
テックリードっぽい役割で頑張った
おしごと上半期~統合コックピットシステム~
上半期は、統合コックピットシステムの開発を継続していました。6月で私自身の希望により離脱しました。他案件との掛け持ちを合わせると2年ほどやっていたことになります。 このプロジェクトでは、Linuxユーザーランドのミドルウェアとアプリケーションを担当していました。
途中からはテックリードの役割をやっていた、と思います。チーム全体の生産性を上げるためにあれこれと動いていました。
- GitLab / GitLab CIの立ち上げとレクチャー
- Merge Requestベースの開発プロセスを導入し、コードレビューを担当
- unittestの導入
- C++勉強会を開催
- ジュニアメンバーのメンタリング
- アーキテクチャレベルのリファクタリング
- 設計 / 実装の相談に対応
- 設計プロセスの整備(これはうまくいかなかった…)
PM兼アーキテクトとは、設計やチームメンバー育成の方針が異なり、しばしば対立しました。 目の前の成果を挙げることに注力しており、中長期的に見てチームが強くなれるような施策が考えられていないように見えました。なので、自分でやることにしました。
GitLabを中心とした開発プロセスを整備していったことで、かなり状況が改善しました。 コードレビューの導入も、GitLabを立ち上げていたので、やるだけ!でした。 C++勉強会は、GitLabプロジェクトのwikiベースで実施し、プロダクトコードの問題ある箇所に対して、wikiの内容を参照して、改善をお願いすることもありました。
色々うまくいったこと、学べたことがありました。 もうこのチームは自分が居なくても大丈夫だろうと判断したこと、技術的な興味が低レイヤに移ったこと、面白そうなRISC-V×FPGAというプロジェクトが立ち上がりそうだったこと、と様々な要因が重なった結果、統合コックピット開発からは離脱することにしました。
TCFMとの運命の出会い
おそらく5月くらいだったかと思うのですが、Turing Complete FMの存在を知りました。 なんだこれ、面白すぎる、と思って何回も聞きました。
大学ではプロセッサの研究をしていたこともあり、低レイヤへの熱が一気に再燃しました。 ここから、下半期、怒涛の低レイヤラッシュに繋がります。
低レイヤ技術に広く浅く色々手を出した
おしごと下半期~RISC-V / FPGA / Hypervisor / AGL~
下半期は小粒のプロジェクトをたくさんこなしました(半年で4案件)。低レイヤ以外のお仕事はやんわりお断りしました。
RISC-V × FPGA
かねてより一緒にお仕事したい、と思っていたFPGAのプロが、面白そうなことを始めるタイミングだったので、猛アピールして無理矢理ねじ込んでもらいました。 何したか、内容は書けません♪ でもおそらく世界で初めてのことをやったんじゃないかなぁ、と思います。
残念ながら、FPGAのプロは12月をもって退職されたので、一緒に仕事できたのは本件だけでした。
Hypervisor
組込み業界でもハイエンド寄りでは、Hypervisor技術が注目されています。 個人的には、組込みとHypervisorは相性が悪いので、果たして主流になるかな?という疑問はありますが、案件ベースでHypervisorを学ぶ機会だったので、手を挙げました。
Hypervisorとしては、Xenと組込み向けHypervisor ACRNに関わりました。
Hypervisorは学習するためのリソースが乏しいところが、少し辛かったです。 結局、最低限の知識を仕入れた後は、ソースコードを読むのが最短です。多分。
AGL (Automotive Grade Linux)
AGLと言いつつ、ほとんどAGLは関係ありません。主に新しく搭載したデバイスに対して、Linux device driverの組込みをやっていました。 順調に行けば、関わったものが、CES 2019で展示されるはずです。
アウトプットを増やした
Qiitaに投稿した記事は、24個でした。最近は少しずつ記事のクオリティを上げている、つもりです。
Qiitaに投稿した記事を辿ると、上半期は、テスト駆動開発とC++のトピックが多いです。下半期は、趣味のRustネタを主に投稿していました。
アウトプットすることで、インプットが増えたり、理解が曖昧だったところを調べなおしたりするので、技術力を上げるためにもアウトプットは大事だなぁ、と実感しています。 最近、やる気のある同僚には、アウトプットのメンタリングをしています。一人は実際に踏み出してくれて、もう一人、踏み出してくれそうです。単純に仲間ができてうれしいですね。
毎日プログラミングした
子育てで忙しい、というのはあるのですが、毎日プログラミングをするように心がけました。 高い目標を掲げるとしんどいので、1日15分はやる!ということが目標でした。
1つ1つのコミットは小さいですが、病気で死んでいた日を除いて、毎日githubに何らか成果をコミットしていました。 やはりうまく習慣を作ることが大事ですね。習慣は人生を変えます。 1年前から、また数段プログラマーとしてのレベルが上がっていると思います。
Rustとの運命の出会い
きっかけは忘れてしまいました。今年の6月か7月くらいだったかと思います。 今まで、特定の言語に肩入れすることはなかったのですが、一目惚れです。
ただ、Rustを好きになれたのも、C++と正面から向き合ったおかげです。今でもC++は大好きな言語です。
その他
興味を持ったものに片っ端から手を出してみました。
仕事でx86のSoCを扱っているので、x86を少しでも理解しよう、と思ってRustでx86エミュレータを書いています。x86命令セットはお世辞にも好きになれないですが、これをなんとか作ろうとすることは、色々考えることがあって面白いです。たまに嫌になります。
OS自作にも入門しました。 まだまだ先人たちには遠く及ばないですが、コツコツやっていきたいです。
言語処理系では、Go言語でつくるインタプリタをやってみました。 低レイヤを知りたい人のためのCコンパイラ作成入門や、LLVMのチュートリアルをやっています。
今の深層学習ブームから見るに、Domain specific architecture + Domain specific languageの組み合わせが増えるのでは?と思えるので、そこを握れるように動いていきたいです。
参加したイベント
これを見ても、やはり下半期の低レイヤ技術系が目立ちますね。
毎月1回、会社の同僚ともくもく会を開催しています。
人を理解するために、心理学のMBTIを継続して受講しています。今年は4回受講しました。
2019年やりたいこと
Rustは完全に趣味でやっているのですが、プログラミングは取り組んだ時間がものを言う部分もあるので、やはりお仕事の時間でもRustをやりたいです。 後、人から仕事を取ってきてもらっていると、色々と動きにくいので、自分で仕事を取ってこれるようになりたいです。
そのために、Rustで1本仕事を受注する、というのが2019年で最も大きな目標になります。
後、色々と低レイヤに手を出しているのですが、全部中途半端なので、少しずつ深堀していきたいです。
SDSoC Example catalog(1)~Array Partitioning~
年内にFPGAの記事を1本書いておきたかったので、詳しくなりたいSDSoCについて書きます。 今はノリで使っていて、基礎ができていないので、1歩ずつやります。
何からやろうか、と考えたところ、私は具体例から物事を理解することが得意なので、Xilinx SDSoC Exampleを1つずつ詳細に解析していくことにしました。
今回は、Array Partitioning です。
SDxのバージョンは、2018.2です。(更新しないとなぁ)
Array Partitioning
array_partition/ This example shows how to use array partitioning to improve performance of a hardware function
Key Concepts - Hardware Function Optimization - Array Partitioning Keywords - #pragma HLS ARRAY_PARTITION - complete
説明から察するに、配列を分割してハードウェアで演算するようなロジックを生成するのだと思います。さっそく見ていきましょう。
Makefile
Makefileがあるので、まずこれを見ましょう。
default parameters
Platformのデフォルトはzcu102になるようです。他には、zc706とzc702が指定できるようです。
# FPGA Board Platform (Default ~ zcu102) PLATFORM := zcu102
ユーティリティディレクトリから、ライブラリをインクルードしています。インクルードしている内容については、後述します。
# Points to Utility Directory COMMON_REPO = ../../../ ABS_COMMON_REPO = $(shell readlink -f $(COMMON_REPO)) # Include Libraries include $(ABS_COMMON_REPO)/libs/sds_utils/sds_utils.mk
Makefile Data
リソースの指定は至って普通のMakefileです。
SRC_DIR := src OBJECTS += \ $(pwd)/$(BUILD_DIR)/main.o \ $(pwd)/$(BUILD_DIR)/matmul.o
SDSのオプションが出てきました。こういうものを1つずつ理解していきます。
# SDS Options HW_FLAGS := ifneq ($(TARGET), cpu_emu) HW_FLAGS += -sds-hw matmul_partition_accel matmul.cpp -sds-end endif
SDSoCの説明を探すと、次の記述が見つかりました。
-sds-hw と -sds-end オプションはペアで使用されます。 -sds-hw オプションでは、単一の関数記述のハードウェアへの移動が開始されます。 -sds-end オプションでは、その関数のコンフィギュレーションの詳細なリストが終了されます。
-sds-hw
と-sds-end
はただのラベルで、matmul_partition_accel
は、高位合成する関数名になります。
ソースコードから、matmul_partition_accelを探すと、同名の関数が宣言/定義されています。
cpp/getting_started/array_partition/matmul.h
// Pragma below Specifies sds++ Compiler to Generate a Programmable Logic Design // Which has Direct Memory Interface with DDR and PL. #pragma SDS data zero_copy(in1[0:mat_dim*mat_dim], in2[0:mat_dim*mat_dim], out[0:mat_dim*mat_dim]) void matmul_partition_accel(int *in1, // Read-Only Matrix 1 int *in2, // Read-Only Matrix 2 int *out, // Output Result int mat_dim); // Matrix Dim (assumed only square matrix)
コンパイルフラグを作成しています。普通です。
# Compilation and Link Flags IFLAGS := -I. CFLAGS = -Wall -O3 -c CFLAGS += -I$(sds_utils_HDRS) CFLAGS += $(ADDL_FLAGS) LFLAGS = "$@" "$<"
SDS driver
driver自体は普通ですね。CC
をsds++
にしています。
SDSFLAGS := -sds-pf $(PLATFORM) \ -target-os $(TARGET_OS) ifeq ($(VERBOSE), 1) SDSFLAGS += -verbose # SDS Compiler CC := sds++ $(SDSFLAGS) endif
コンパイルのコマンドも普通な感じです。
.PHONY: all all: $(BUILD_DIR)/$(EXECUTABLE) $(BUILD_DIR)/$(EXECUTABLE): $(OBJECTS) mkdir -p $(BUILD_DIR) @echo 'Building Target: $@' @echo 'Trigerring: SDS++ Linker' cd $(BUILD_DIR) ; $(CC) -o $(EXECUTABLE) $(OBJECTS) $(EMU_FLAGS) @echo 'SDx Completed Building Target: $@' @echo ' ' $(pwd)/$(BUILD_DIR)/%.o: $(pwd)/$(SRC_DIR)/%.cpp @echo 'Building file: $<' @echo 'Invoking: SDS++ Compiler' mkdir -p $(BUILD_DIR) cd $(BUILD_DIR) ; $(CC) $(CFLAGS) -o $(LFLAGS) $(HW_FLAGS) @echo 'Finished building: $<' @echo ' ' ifeq ($(TARGET), cpu_emu) @echo 'Ignore the warning which states that hw function is not a HW accelerator but has pragma applied for cpu_emu mode' @echo ' ' endif
make check
すると、エミュレータが走るようです。
# Check Rule Builds the Sources and Executes on Specified Target check: all ifneq ($(TARGET), hw) ifeq ($(TARGET_OS), linux) cp $(ABS_COMMON_REPO)/utility/emu_run.sh $(BUILD_DIR)/ cd $(BUILD_DIR) ; ./emu_run.sh $(EXECUTABLE) else cd $(BUILD_DIR) ; sdsoc_emulator -timeout 500 endif else $(info "This Release Doesn't Support Automated Hardware Execution") endif
emu_run.sh
を叩いています。
utility/emu_run.sh
を見ると、sdsoc_emulatorを呼び出しています。
sdsoc_emulatorは、qemuベースのエミュレータで、RTLのシミュレーションもできる(ボードがXilinx作成物の場合)もののようです。
#!/usr/bin/env bash if [ -f "$PWD/_sds/init.sh" ] then rm -rf $PWD/_sds/emulation/sd_card sdsoc_emulator -no-reboot |tee emulator.log else cat > "init.sh" <<EOT #!/bin/sh /mnt/$1 reboot EOT echo $PWD/_sds/init.sh >> "_sds/emulation/sd_card.manifest" mv init.sh _sds sdsoc_emulator -no-reboot |tee emulator.log fi
sds_utils.mk
少し寄り道をして、Makefileからライブラリをインクルードするための、sds_utils.mk
の内容を確認します。
libs/sds_utils/sds_utils.mk
sds_utils_HDRS:=$(ABS_COMMON_REPO)/libs/sds_utils/
ヘッダファイルの場所を定義しているだけですね。ヘッダの中身もExampleの中で使うパフォーマンス計測用ヘルパークラスが定義されているだけです。
libs/sds_utils/sds_utils.h
#ifndef SDS_UTILS_H_ #define SDS_UTILS_H_ #include <stdint.h> #include "sds_lib.h" namespace sds_utils { class perf_counter { private: uint64_t tot, cnt, calls; public: perf_counter() : tot(0), cnt(0), calls(0) {}; inline void reset() { tot = cnt = calls = 0; } inline void start() { cnt = sds_clock_counter(); calls++; }; inline void stop() { tot += (sds_clock_counter() - cnt); }; inline uint64_t avg_cpu_cycles() {return (tot / calls); }; }; } #endif
C++ソースコード解析
まずは、動作確認用に作られているソフトウェア実行の関数を確認します。
matrix[M][M]
のデータを乗算しているだけですね。
// Software Matrix Multiplication void matmul(int *C, int *A, int *B, int M) { for (int k = 0; k < M; k++) { for (int j = 0; j < M; j++) { for (int i = 0; i < M; i++) { C[k * M + j] += A[k * M + i] * B[i * M + j]; } } } }
同様の関数を高位合成の対象としたのが、下記のmatmul_partition_accel
になります。
なんでも良いのですが、API合わせておいて欲しいです…。引数の順番が違うんですが。
// Pragma below Specifies sds++ Compiler to Generate a Programmable Logic Design // Which has Direct Memory Interface with DDR and PL. #pragma SDS data zero_copy(in1[0:mat_dim*mat_dim], in2[0:mat_dim*mat_dim], out[0:mat_dim*mat_dim]) void matmul_partition_accel(int *in1, // Read-Only Matrix 1 int *in2, // Read-Only Matrix 2 int *out, // Output Result int mat_dim); // Matrix Dim (assumed only square matrix)
main関数
それでは、main関数を見ていきます。
cpp/getting_started/array_partition.cpp
int main(int argc, char **argv) { bool test_passed = true; static const int columns = MAX_SIZE; static const int rows = MAX_SIZE; // Allocate buffers using sds_alloc int *A = (int *) sds_alloc(sizeof(int) * columns * rows); int *B = (int *) sds_alloc(sizeof(int) * columns * rows); int *C = (int *) sds_alloc(sizeof(int) * columns * rows);
まずは、sds_alloc
でメモリ空間を確保しています。
sds_alloc
SDSoC 開発環境ヘルプ メモリ割り当てを参照して、何者か調べます。
sds_alloc/sds_free を使用してアクセラレータ専用のメモリを割り当てた方がデータが物理的に隣接したメモリに割り当てられて格納されるので、メモリへの読み出しおよび書き込みが速くなり、パフォーマンスを向上できます。
なるほど、連続領域にメモリが確保される、ということみたいですね(ソースコードは公開されているのかしら?)。
データの初期化をして、ソフトウェア実装、ハードウェア実装を順番に呼び出しています。 関数の実行にかかった時間を計測しており、ソフトウェア実装とハードウェア実装との一致比較をして、結果を検証しています。。
for (int i = 0; i < NUM_TIMES; i++) { // Data Initialization for(int i = 0; i < columns * rows; ++i) { A[i] = i; B[i] = i + i; gold[i] = 0; C[i] = 0; } sw_ctr.start(); //Launch the Software Solution matmul(gold, A, B, columns); sw_ctr.stop(); hw_ctr.start(); //Launch the Hardware Solution matmul_partition_accel(A, B, C, columns); hw_ctr.stop(); // Compare the results of hardware to the simulation test_passed = verify(gold, C, columns * rows); }
matmul_partition_accel関数宣言
高位合成対象関数matmul_partition_accel
についているpragmaの意味を調べます。
#pragma SDS data zero_copy(in1[0:mat_dim*mat_dim], in2[0:mat_dim*mat_dim], out[0:mat_dim*mat_dim]) void matmul_partition_accel(int *in1, // Read-Only Matrix 1 int *in2, // Read-Only Matrix 2 int *out, // Output Result int mat_dim); // Matrix Dim (assumed only square matrix)
SDS data zero_copy pragma
COPY プラグマを使用すると、データがホスト プロセッサ メモリからハードウェア関数にコピーされます。最適なデータ ムーバーによりデータ転送が実行されます。詳細は、『SDSoC 環境プロファイリングおよび最適化ガイド』 (UG1235) の「システム パフォーマンスの向上」を参照してください。 ZERO_COPY を使用すると、ハードウェア関数が AXI4 マスター バス インターフェイスを介して共有メモリからデータに直接アクセスします。
なるほど。データムーバーを使うかどうか、が変わってくるということですね。
COPY
を利用すると、データ転送するコードに置き換えられます。
#pragma SDS data copy(A[0:size*size], B[0:size*size]) void foo(int *A, int *B, int size) { ... } void _p0_foo_0(int *A, int *B, int size) { ... cf_send_i(&(_p0_swinst_foo_0.A), A, (size*size) * 4, &_p0_request_0); cf_receive_i(&(_p0_swinst_foo_0.B), B, (size*size) * 4, &_p0_request_1); ... }
ZERO_COPY
を利用すると、 参照 を送信するコードに置き換えられます。
#pragma SDS data zero_copy(A[0:size*size], B[0:size*size]) void foo(int *A, int *B, int size) { ... } void _p0_foo_0(int *A, int *B, int size) { ... cf_send_ref_i(&(_p0_swinst_foo_0.A), A, (size*size) * 4, &_p0_request_0); cf_receive_ref_i(&(_p0_swinst_foo_0.B), B, (size*size) * 4, &_p0_request_1); ... }
cf_send_ref_i および cf_receive_ref_i 関数は配列の参照またはポインターのみをアクセラレータに転送し、アクセラレータは直接メモリにアクセスします。
この違いは、生成されるブロックデザインを見ると明らかでした。
matmul_partition_accel定義
高位合成関数の本体は次の通りです。
void matmul_partition_accel(int *in1, // Read-Only Matrix 1 int *in2, // Read-Only Matrix 2 int *out, // Output Result int mat_dim) // Matrix Dimension { // Local memory is implemented as BRAM memory blocks int A[MAX_SIZE][MAX_SIZE]; int B[MAX_SIZE][MAX_SIZE]; int C[MAX_SIZE][MAX_SIZE]; //partitioning Array A and B #pragma HLS ARRAY_PARTITION variable=A dim=2 complete #pragma HLS ARRAY_PARTITION variable=B dim=1 complete // Burst reads on input matrices from DDR memory // Burst read for matrix A and B // Multiple memory interfaces are supported by default in SDSoC // It is possible to fetch both A and B concurrently. readA: for (int itr = 0, i = 0, j = 0; itr < mat_dim * mat_dim; itr++, j++) { #pragma HLS PIPELINE #pragma HLS LOOP_TRIPCOUNT min=c_size*c_size max=c_size*c_size if (j == mat_dim) { j = 0; i++; } A[i][j] = in1[itr]; B[i][j] = in2[itr]; } // By Default VHLS create single Memory with two ports for each local buffer // which allows maximum two read/write from buffer per clock. // Due to this restriction, lowest loop of mmmult can be unroll by 2 times only. // // However Partition gives instruction to VHLS Complier to split a large array // into small-small memory which allow user to get multiple concurrent accesses. // // To completely unroll the lowest loop of Mmult, A buffer is completely // partitioned for 2nd dimension, and B buffer is completely partitioned // for 1st dimension. Which eventually will improve the overall performance of // matrix multiplication. arraypart1: for (int i = 0; i < mat_dim; i++) { #pragma HLS LOOP_TRIPCOUNT min=c_size max=c_size arraypart2: for (int j = 0; j < mat_dim; j++) { #pragma HLS LOOP_TRIPCOUNT min=c_size max=c_size #pragma HLS PIPELINE int result = 0; arraypart3: for (int k = 0; k < MAX_SIZE; k++) { result += A[i][k] * B[k][j]; } C[i][j] = result; } } // Burst write from output matrices to DDR memory // Burst write from matrix C writeC: for (int itr = 0, i = 0, j = 0; itr < mat_dim * mat_dim; itr++, j++) { #pragma HLS PIPELINE #pragma HLS LOOP_TRIPCOUNT min=c_size*c_size max=c_size*c_size if (j == mat_dim) { j = 0; i++; } out[itr] = C[i][j]; } }
pragmaを1つずつ見ていきます。
HLS ARRAY_PARTITION pragma
まずは、ARRAY_PARTITION
で、ローカルメモリを分割します。
// Local memory is implemented as BRAM memory blocks int A[MAX_SIZE][MAX_SIZE]; int B[MAX_SIZE][MAX_SIZE]; int C[MAX_SIZE][MAX_SIZE]; //partitioning Array A and B #pragma HLS ARRAY_PARTITION variable=A dim=2 complete #pragma HLS ARRAY_PARTITION variable=B dim=1 complete
大型の配列を複数の配列または個別のレジスタに分割し、データへのアクセスを改善してブロック RAM のボトルネックを削除します。
そのままですね。 pragmaでググるとSDAccelの方だけ引っかかりますね。まあ良いのですが。
SDAccel 開発環境ヘルプ (2018.2 XDF 用) pragma HLS array_partition
この分割により、次のようになります。 1 つの大型メモリではなく、複数の小型メモリまたは複数のレジスタを含む RTL が生成されます。 ストレージの読み出しおよび書き込みポートの数が増加します。 デザインのスループットが向上する可能性があります。 より多くのメモリ インスタンスまたはレジスタが必要となります。
構文
#pragma HLS array_partition variable=<name> \ <type> factor=<int> dim=<int>
今回の例では、type
がcomplete
になっています。
complete: 完全分割では、配列を個々の要素に分割します。1 次元配列の場合は、メモリが個々のレジスタに分割されます。これがデフォルトの
です。
なるほど。complete
を指定すると、完全分割になるので、完全な並列アクセスが可能になる、という理解でよさそうです。
HLS PIPELINE / HLS LOOP_TRIPCOUNT pragma
DDRからデータをバースト読み込みしている部分を読みます。
// Burst reads on input matrices from DDR memory // Burst read for matrix A and B // Multiple memory interfaces are supported by default in SDSoC // It is possible to fetch both A and B concurrently. readA: for (int itr = 0, i = 0, j = 0; itr < mat_dim * mat_dim; itr++, j++) { #pragma HLS PIPELINE #pragma HLS LOOP_TRIPCOUNT min=c_size*c_size max=c_size*c_size if (j == mat_dim) { j = 0; i++; } A[i][j] = in1[itr]; B[i][j] = in2[itr]; }
SDAccel 開発環境ヘルプ (2018.2 XDF 用) pragma HLS pipeline
パイプライン処理された関数またはループは、N クロック サイクル (N は関数またはループの開始間隔) ごとに新しい入力を処理できます。PIPELINE プラグマのデフォルトの開始間隔は 1 で、クロック サイクルごとに 1 つの入力を処理します。
PIPELINE
pragmaにより、配列A / 配列Bへの転送が並列で実行されるようです。
SDAccel 開発環境ヘルプ (2018.2 XDF 用) pragma HLS loop_tripcount
LOOP_TRIPCOUNT
pragmaは解析用のpragmaで、合成結果に影響しません。
ループで実行される反復回数を手動で指定することで、解析精度を向上できるようですね。
さて、matrixの演算をしている部分を見ていきます。コメントで重要なことがたくさん書いてあります。
// By Default VHLS create single Memory with two ports for each local buffer // which allows maximum two read/write from buffer per clock. // Due to this restriction, lowest loop of mmmult can be unroll by 2 times only. // // However Partition gives instruction to VHLS Complier to split a large array // into small-small memory which allow user to get multiple concurrent accesses. // // To completely unroll the lowest loop of Mmult, A buffer is completely // partitioned for 2nd dimension, and B buffer is completely partitioned // for 1st dimension. Which eventually will improve the overall performance of // matrix multiplication. arraypart1: for (int i = 0; i < mat_dim; i++) { #pragma HLS LOOP_TRIPCOUNT min=c_size max=c_size arraypart2: for (int j = 0; j < mat_dim; j++) { #pragma HLS LOOP_TRIPCOUNT min=c_size max=c_size #pragma HLS PIPELINE int result = 0; arraypart3: for (int k = 0; k < MAX_SIZE; k++) { result += A[i][k] * B[k][j]; } C[i][j] = result; } }
通常、HLSはローカルバッファーにdual portのBRAMを割り当てるため、ループ展開しても、2並列までしか展開できません。 このExampleでは、ローカルバッファをpartitioningしているため、データ読み書きの並列度を向上することができます。
配列Aは完全分割されており、レジスタが割り当てられています。配列Bは1次元方向に完全分割されています。 結果、下のループは、完全に並列展開されるようになります。
arraypart3: for (int k = 0; k < MAX_SIZE; k++) { result += A[i][k] * B[k][j]; }
build
ビルドして、どのような回路が生成されるか確認します。
$ make all TARGET=hw
Vivadoでプロジェクトを開くと、次のような回路が生成されていました。
確かに、DMAデータムーバが居ません。
画像左側にあるmutmul_partition_accel_1
が高位合成で生成されたアクセラレータです。
m_axi_in1
, m_axi_in2
, m_axi_out_r
と3つのAXIインタフェースが生えています。このAXIインタフェースがMemory Map (AXIMM)という名前で、PSのFPDインタフェースに接続しています。これで、DDRに直接アクセスするわけですね。
正直、PSのAXIインタフェースの使い方がもったいない気がしますが…。
配列のアドレスは、in1_offset
, in2_offset
, out_offset
としてアクセラレータに与えられています。
上の方で見た通り、PSからはポインタだけが渡されています。
捕捉ですが、普通にコピーするアクセラレータを生成すると、adapter_v3_0
のIP内にBRAMまたはFIFOが生成されて、そこにデータがコピーされることになります。
このアクセラレータからDDRに直接アクセスする構成は、大きなデータのうち、一部のみをランダムアクセスするような回路を構成する場合に有効そうですね。
まとめ
- SDSoC環境でのMakefile記述方法を少し理解しました
- 次のSDSoc用関数およびpragmaを学びました
- sds_alloc
- SDS data copy / zero_copy
- HLS ARRAY_PARTITION
- HLS PIPELINE
- HLS LOOP_TRIPCOUNT
1つずつしっかり見ていくのは、重要ですね。 今後も継続していきたいと思います。
参考
LLVM Tutorialをやっている(2)~7章まで~
7. Kaleidoscope: Extending the Language: Mutable Variables — LLVM 8 documentation
tomo-wait-for-it-yuki.hatenablog.com
前回に引き続き、LLVMのTutorial Kaleidoscopeをやっています。 メモを残しながらやっていないので、あまりインフォーマティブな記事にならないですね。
5~7章 (Control Flow
, User-defined Operators
, Mutable Variables
)を完了し、おぼろげながらLLVM IRが読めるようになってきました。
1回、ケアレスミスでJIT Compilerがエラーを吐くようになってしまいました。原因は、Builder.CreateFAdd();
と浮動小数点の加算命令を生成しなければならない部分を、Builder.CreateAdd();
と整数の加算にtypoしていたことでした。
JIT Compilerがミスすると、あまり親切なエラーが出力されず、しょうもないミスで30分くらい悩んでいました。
こういうときは、どうやってデバッグするんだろう?
後は、自作関数をJIT Compilerからシンボル実行する機能を作成するのですが、リンカオプションが抜けており動かなくてハマりました。
# Compile clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core mcjit native` -O3 -o toy -rdynamic
On some platforms, you will need to specify -rdynamic or -Wl,–export-dynamic when linking.
フロントエンド部分は、Go言語でつくるインタプリタから新しいアップデートはないですが、今回は、LLVM固有のことが知りたいので、今のところ満足しています。 テストが欲しいなぁ、と思わなくもないですが。
次はとうとうオブジェクトコードを生成するようなので、続きをやるのが楽しみです。