The Embedded Rust Book翻訳の気づきメモ①

Introduction - The Embedded Rust Book

お仕事でもRustをやりたいな、と考えて、組込みでRustを広めるために翻訳を行っています。 翻訳を進める中で、技術的にも英語的にも、学びがあるので、それを綴っていきます。

github.com

一応今の状況としては、Introductionの翻訳が完了しています。

ちゃんとページとして見える環境の整備もTo Doです。

英語

始めてみた単語を並べていきます。

英単語 意味 コメント
fiddle ~をいじくる ハードウェアをいじる、という文脈で出てきます。

Rust

cargo generate

github.com

存在を忘れがちなcargo generateさん。 Cargo.tomlのauthorやproject-nameを埋めてくれるので、教材のテンプレートを作るときには便利そうです。

entry attribute

#[entry] attricuteを指定した関数が、エントリーポイントになります。

rust-embedded.github.io

ざっくりソースコードを確認したところ、#[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年の目標

  • RISC-Vに命令を追加して、Rustから使う
  • 高位合成を使った動的再構成
  • Rust × RISC-VでOS自作
  • 自分でお仕事を取ってくる
  • 人との関わり/アウトプットを増やす

技術的な側面からやりたいと考えていることは、去年からの延長です。 LLVMのバックエンドをいじる方法や、Rust、RISC-V、OSに継続して取り組んでいきます。 ずっと取り組んでいるRustのエミュレータも、一区切りつけたいところです。

お仕事的には、自分がやりたいことを仕事にできるように、自分で仕事を取ってこれるようになりたいです。

登壇やブログ、twitterを通じて、人との関わりを増やしていきたいです。

それでは、本年もよろしくお願いいたします。

RISC-VのFENCE.I命令を調べる

rv32i基本命令をざっと眺めていて、この命令だけなんだろう?と思ったので調査します。

RISC-V specification v2.22.7 Memory Modelセクションに記載があります。

hardware thread間でのメモリ操作命令実行順序を保証するための明示的な命令がFENCE命令、とあります。まぁ、わかります。

specを読み進めると、命令ストリームとデータストリームを同期するために利用する、とあります。

自己書き換えプログラムを考えるとわかりやすいです。自己書き換えプログラムは、実行中のプログラムが、プログラム自身を書き換えるものです。

上記のような自己書き換えプログラムでは、 自身で書き換えた結果の命令 をフェッチしてくる必要があります。 通常、プロセッサは複数ステージのパイプラインで構成されるため、命令を書き換えるstoreが完了する前に、次の命令をフェッチしています。 書き換えた後の命令をフェッチしないといけないところで、命令書き換え前の命令をフェッチしていると、正しくプログラムが動作しないわけです。

そこで、FENCE.I命令で、書き換わった命令メモリの内容を参照することを保証します。 実装する方法はいくつかあり、パイプラインと命令キャッシュをフラッシュする、というのがシンプルな実装になります。 命令フェッチはやり直しになるため、書き換わった命令がフェッチできるわけです。

ということで、真面目にパイプライン構成を作り始めるとケアする必要がある命令ですね。

2018年に読んだ本をまとめよう

これ、毎年やらないと忘れちゃうので備忘録がてら。 大雑把なジャンル別にまとめます。

書評を書く気力が今はないので、羅列だけです。

システムプログラミング、低レイヤ技術

今年の後半あたりから、システムプログラミングや低レイヤ技術に注力しはじめました。

Linux

Linux Device Driver Developmentは、組込みLinux屋さん必読です。誤植が多くて、英語、というハードルはありますが、Linux kernel 4系のdevice driverを解説している本は、現在希少です。

  • Linux Device Driver Development
  • 新装改訂版 Linuxのブートプロセスをみる
  • [試して理解]Linuxのしくみ

OS/仮想化

言語処理系

Go言語でつくるインタプリタがとても良かったです。テスト付きで1歩1歩何かを作る解説書は非常にありがたいです。

FPGA/ハードウェア

アジャイル/組織論

ちょっとくくりが雑ですが、管理とかプロセスという言葉を使いたくないので、アジャイル/組織論という項目でまとめてみました。

プログラミング

主に、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分でも毎日プログラミングする、という目標のもと、活動したのも良かったです。

f:id:tomo-wait-for-it-yuki:20181229073625p:plain
2018年は大草原

サマリ

2018年

  • テックリードっぽい役割で頑張った
  • TCFMとの運命の出会い
  • 低レイヤ技術に広く浅く色々手を出した
  • アウトプットを増やした
  • 毎日プログラミングした
  • Rustとの運命の出会い

2019年やりたいこと

  • アウトプットをさらに増やす
  • 低レイヤ技術の深堀り
  • 毎日プログラミングする
  • Rustの仕事を取ってくる

2018年

テックリードっぽい役割で頑張った

おしごと上半期~統合コックピットシステム~

上半期は、統合コックピットシステムの開発を継続していました。6月で私自身の希望により離脱しました。他案件との掛け持ちを合わせると2年ほどやっていたことになります。 このプロジェクトでは、Linuxユーザーランドのミドルウェアとアプリケーションを担当していました。

途中からはテックリードの役割をやっていた、と思います。チーム全体の生産性を上げるためにあれこれと動いていました。

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に関わりました。

projectacrn.org

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チュートリアルをやっています。

低レイヤを知りたい人のための Cコンパイラ作成入門

今の深層学習ブームから見るに、Domain specific architecture + Domain specific languageの組み合わせが増えるのでは?と思えるので、そこを握れるように動いていきたいです。

参加したイベント

これを見ても、やはり下半期の低レイヤ技術系が目立ちますね。

毎月1回、会社の同僚ともくもく会を開催しています。

人を理解するために、心理学のMBTIを継続して受講しています。今年は4回受講しました。

oss-gate.doorkeeper.jp

techplay.jp

kernelvm14.peatix.com

atnd.org

forcia.connpass.com

rust.connpass.com

deep-compilers.connpass.com

cppmix.connpass.com

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 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自体は普通ですね。CCsds++にしています。

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

pragma SDS data copy

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

SDSoC 環境ヘルプ パフォーマンスのための構造最適化

大型の配列を複数の配列または個別のレジスタに分割し、データへのアクセスを改善してブロック RAM のボトルネックを削除します。

そのままですね。 pragmaでググるとSDAccelの方だけ引っかかりますね。まあ良いのですが。

SDAccel 開発環境ヘルプ (2018.2 XDF 用) pragma HLS array_partition

この分割により、次のようになります。 1 つの大型メモリではなく、複数の小型メモリまたは複数のレジスタを含む RTL が生成されます。 ストレージの読み出しおよび書き込みポートの数が増加します。 デザインのスループットが向上する可能性があります。 より多くのメモリ インスタンスまたはレジスタが必要となります。

構文

#pragma HLS array_partition variable=<name> \
<type>  factor=<int>  dim=<int>

今回の例では、typecompleteになっています。

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でプロジェクトを開くと、次のような回路が生成されていました。

f:id:tomo-wait-for-it-yuki:20181229051323p:plain
SDSoCで生成される回路

確かに、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固有のことが知りたいので、今のところ満足しています。 テストが欲しいなぁ、と思わなくもないですが。

次はとうとうオブジェクトコードを生成するようなので、続きをやるのが楽しみです。