2023年5月20日土曜日

NNCase V1.7を試す話(実機実行編)



前回のSim実行編にて、NNCaseのSimが動くところまで確認しました。今回はその続きで、実機で動かす話になります

1.テスト環境

今回の環境(ホスト)は以下の通りです
  • RPi4 4GB(Raspbian 11:bullseye)
  • Docker(23.0.5, build bc4487a)
  • NNCase(v1.7.1)
  • nncaseruntime(v1.7.1)
  • kendryte-standalone-sdk
  • kendryte-gnu-toolchain
基本的な流れとしては、toolchainを入れてからstandalone-sdkを入れ、nncaseのruntimeを入れる形となります

またK210のボードとして、Maix Bitを使いました


2.インストール

まずはtoolchainのインストールからになります。
$ git clone --recursive https://github.com/kendryte/kendryte-gnu-toolchain
$ cd /kendryte-gnu-toolchain/riscv-gcc
$ ./contrib/download_prerequisites
$ cd /kendryte-gnu-toolchain
$ ./configure --prefix=/opt/kendryte-toolchain --with-cmodel=medany --with-arch=rv64imafc --with-abi=lp64f
$ make -j4
今回は/opt/kendryte-toolchainに突っ込みました。次にsdkを入れます。
$ git clone https://github.com/kendryte/kendryte-standalone-sdk
kendryte-standalone-sdk/srcにビルドしたいプロジェクトを突っ込んでいくみたいです。最後にnncaseruntimeを入れていきます。
$ wget https://github.com/kendryte/nncase/releases/download/v1.7.1/nncaseruntime-k210.zip -P ./kendryte-standalone-sdk/lib/nncase/v1
$ unzip -o ./kendryte-standalone-sdk/lib/nncase/v1/nncaseruntime-k210.zip -d ./kendryte-standalone-sdk/lib/nncase/v1
sdkのディレクトリにsrc/lib/nncase/v1があり、この中にruntimeを入れている格好になっています。アップデートの際にはここを上書きすることで対応することができます。

3.コンパイル

前回のSim実行環境のサンプルコードと実機用のコードをまとめたものを上げておきます。
build_yolox.sh内のcompile_pathをsdkの場所に書き換えることで実行できるはずです。
差分としては、Maix Bitのへの対応がメインになります。
コンパイルをすると、モデルの変換結果や実機に入れるbinなどが生成されます。

4.実行結果
実機実行の結果。Simと微妙に異なる

実際に動かすとちゃんと認識していることが分かるかと思います。しかしながらSim結果とは異なっている部分もあり、何起因の差分化は考える必要がありそうです。

実行結果(UART)
1回目実行では1100ms程度かかるらしい

実行速度は1100ms程度のようでした。起動直後の1回しかは測定していないので、もう少し条件を詰めて確認する必要があるかと思われます。

5.まとめ


今回はSim編の続きとして実機実行までの流れをやってみました。とりあえずどんな感じで動かせばよいかぐらいは分かったかと思います。この話の発展として別のDNNモデルを実行したり、性能改善を行ったり、カメラをつけて実行等があるかと思いますが、今回はここまでにしたいと思います。



©2023 shts All Right Reserved.



2023年5月2日火曜日

NNCase V1.7を試す話(Sim実行編)


社会人2年目になり業務量が増えてきた中の人です。今回は以前やっていたが公開していなかったNNCaseのv1.x系(kmodel v5)を試す話になります。
2023/5/20追記:サンプルコードを公開



1.NNCase v1.xの特徴

以前のバージョンと比較した際の一番の変更点としては、従来のコマンドラインベースのものからPythonの1ライブラリとしての実装に変更されています。kmodelの元ネタになるonnxやkerasのモデルを生成するコードに追加することもできます。ビルド済みのwhlが公式から提供されており、CPUはx86-64、OSはWin/Linux/Mac版があります。
これ以外の点では、対応する演算の種類が増えている、K210の上位機種に当たるK510への対応といった部分のアップデートがなされています。これに合わせる形?でkmodelのバージョンもv5に上がっています。

2.テスト環境


今回の環境は以下の通りです。
  • RPi4 4GB(Raspbian 11:bullseye)
  • Docker(23.0.5, build bc4487a)
  • NNCase(v1.7.1)
NNCaseのv1.xはarm64に対応するwhlを出していないのですが、中身を一部変更してビルドしました(基本的にはx86-64と同じコードになります)。またビルドのデバッグがやりたかったので、Docker環境にねじ込みました。今回はK210を動かすところまではいかないので、NNCaseのみの紹介にします

3.インストール


x86-64環境であればインストールは簡単で、公式のリリースからwhlを持ってきてインストールするだけになります。(今回試しているarm64の場合は自前でビルドする必要が出てくるので面倒でした)

4.コンパイルとSim実行


v1.7.1でのコンパイルとSim実行手順は基本的にはここにいろいろ書いてあります。このディレクトリを手元に落としてきて動かすのが最初はよいかと思います。
このディレクトリは以下のような感じになっています。
.
|- tools:モデルの変換とSimを実行するためのコード
|- images:入力画像
|- model:もとになるモデル(onnx)
|- cpu
`-k210:実機実行用のコード
README.mdを見たところ、画像の入ったディレクトリを見に行くオプションがすべてimagesではないところだったのですが、手元で試したところimagesに変更しても動くことが分かりました。
python tools/compile.py model/yolox_nano_224.onnx yolox_nano_224_quant.kmodel --imgs_dir ./images/ --legacy --target k210
python tools/simulate.py yolox_nano_224_quant.kmodel ./images/dog.jpg
環境内でmatplotlibの表示機能が使えるのであれば、Sim結果としてこんな感じの画像が得られるはずです
Sim結果
実機実行の結果とは異なるので要注意

5.まとめ


今回はNNCase v1.7を使ってSimを実行してみました。変なことをしなければすんなり動くはずであり、K210の実機が無くても結果が確認できるのでお手軽に試せるかと思います。

n.おまけ


以前使っていたM5StickV+MaixPyの組み合わせではkmodelのv5に対応していなかったのですが、まさかのkendryteがMaixPyではない別のツールを作って対応しようとしていました。(boardのディレクトリにm5stick的なものがあったが、M5StickVで動くかどうかは不明)。
©2023 shts All Right Reserved.




2023年2月5日日曜日

Verilator使ってみた


https://www.veripool.org/verilator/より引用

Verilog初心者の中の人です。今回はVerilogシミュレーションツールであるVerilatorをお試ししてみました。またこのツールの売りであるSim実行速度を簡易的に計測したので、併せて紹介します。




1.Verilatorとは何か

VerilatorはVerilog/SystemVerilog向けのRTLシミュレーションシステムの一種になります。LGPL3/Artistic License2.0で配布されており商用利用も可です。この手のツールは大体がライセンス必須のクローズドソースであることが多いですが、Verilatorはその点で異なったものになります。Verilatorの売りはSimの実行速度であり、有償のツールと同等かそれ以上の性能が出ると主張しています[1]。

2.お試ししてみる



今回は簡単なUART送信回路を作成し、そのテストベンチをVerilatorで書いてみました。
今回はFPGA開発日記さんの記事を参考にしながら、.vcdの波形出力まで行うテストベンチを書きました。またUART送信回路自体は、FPGA上での実機動作が確認できているものを流用しました。書いてみた感想としてはTB本体の準備はそこまで差がないのですが、Verilator(C++)の方が入力するデータの準備や出てきたデータを料理が楽でした。また実行時間もVerilogのTBを実行していた時と比べ何となく速い気がしました。

3.Verilog版TestBenchとの比較


実行時間が体感で速そうだったので、実際に計測してみました(Verilator公式でも高速実行アピールがあるので、本当だとは思っていましたが)。計測方法としては、対象回路に対して同じタイミングで信号を印加するTBをVerilogとVerilator(C++)で準備し、Simにかかった時間をパラメータごとに10回計測し均しました。計測の諸条件は以下の通りです。
  • PC:XPS13(9310)
  • CPU:Intel Corei7-1185G(3GHz)
  • RAM:16GB
  • OS:Ubuntu 20.04.5 LTS (GNU/Linux 5.4.72-microsoft-standard-WSL2 x86_64)
  • Icarus Verilog version 10.3
  • Verilator 5.005 devel rev v5.004-76-g5ef373500
  • g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
  • Sim内で変わるパラメータは終了ステップ数のみ(100スタートで10倍毎に計測)
  • 実行時間はLinuxのtimeコマンドで計測
  • .VCDファイル生成時間を両者ともに含む
  • コンパイラの最適化設定は両者デフォルトの状態
実測結果は以下のようになりました。

x軸実行ステップ、y軸実行時間のグラフ
(両対数グラフなことに注意)

このグラフから実行ステップ数が短いとVerilatorを使ってもそこまで高速化の恩恵が得られないのですが、ある程度以上のステップ数になるとVerilatorの方が10倍のオーダで高速に実行できそうなことが分かりました。(生データはこちら)

4.その他

不定が扱われない様子
(左:Verilog、右:Verilator)

Verilog側で出ていた出力波形の不定がVerilator側では出ていないことに気が付きました。これはVerilatorが信号を4値(0, 1, Z, X)としてではなく2値(0, 1)として扱うことに起因しているようです[2]。不定伝搬等を検出するためにはいくつかの工夫が必要だと思われます。

©2023 shts All Right Reserved.




2022年12月31日土曜日

2022年にやったことまとめ

手が滑って買ったオシロ

5月以来の更新になった中の人です。社会人になり休日のありがたみを実感しているこの頃であります。今回はブログに載せていないものも含めて、2022年に作ったものを振り返る話になります。



1.PYNQとVitis HLSで作るFMA演算IP

テストで使用したFPGA(PYNQ-Z2)

一応ブログとして挙げたネタなのでそこまで詳しく解説はしないのですが、FMA用のIPを高位合成で作成して性能評価を行った話でした。

2.Rust初心者が、Rust+RPi PicoでST7789使用TFT LCD(Waveshare Pico LCD 1.3)を使う話

とりあえずLCDを動かしてみた

こちらもブログネタで、Rust+RPi PicoでLCDを操作する話でした。これについてはブログを上げた後に裏でいろいろいじってました(が諸事情で非公開)。

3.激安プロジェクタ分解


カバーを外したプロジェクタ

完全なる好奇心で\4k弱のプロジェクタを購入し分解しました。今は動作する状態まで組み立てて使っていないのですが、そのうちLEDの換装とかをして遊ぼうかと思っています。記事を書け。

4.TinyFPGA BX用拡張ボード

左から試作(1)、試作(2)、完成品

TinyFPGA BX用に拡張ボードを作ってました。実は以前にも作っていたのですが、出来がそこまでよくなかったので再設計してみました。あとはHDLの学習もかねて74HC595を制御するドライバの作成なども行っていました。記事を書(ry

5.nncase V1.x試してみた

実際に画像を読ませて認識させた結果

以前nncase V0.4の動作確認記事を書きましたが、あの後で更新がいろいろ入ったので新しいバージョンでの動作確認環境の構築を行いました。aarch64+Dockerの環境でnncaseの実行とK210向けのバイナリ生成まで動く環境を作ることができました。記事を(ry

6.レゴで作る90°ステッパー

作成したステッパー

レゴでシーケンシャルミッション等を作る際に必要な90°ステッパーを作りました。たまたまできてしまった代物でした。記事(ry

7.電源基板を作る話

試作中の基板

ちょっと気が向いたので、電源基板を作っています。オレオレ規格を策定するところから始まっているので先は長そうですが、いろいろ試すいい機会だと思っています。記(ry

8.終わりに
MFTokyo2022に行ってました。

今年は業務でいろいろ作る方に気力を吸い取られたため、あまり活動できなかった部分がありました。来年以降はもう少しいろいろやりたいところです。

...そんなことより記事を書け。


©2022 shts All Right Reserved.




2022年5月29日日曜日

Rust初心者が、Rust+RPi PicoでST7789使用TFT LCD(Waveshare Pico LCD 1.3)を使う話



 いつの間にかひよっこ社会人になっていた中の人です。研究室とは勝手が違う中、ひたすら研修を進めています。
 さて今回は以前紹介したRust+RPi Picoのお話の続き?で、Waveshare社製Pico LCD 1.3をRust環境で使う話になります。



0.開発環境とアップデート


 以前の記事では、WSL上でクロスコンパイルをし、PowerShell環境でelf2uf2を使って最終的なバイナリを作っていたと思います。
 その後、elf2uf2-rsが登場し、.cargo/config.tomlで設定すれば、cargo runだけで.uf2への変換からボードへのアップロードも自動で行えるようになりました。らくちん。さらに、記事を書いた後環境を丸ごと吹き飛ばした関係もあり、今回はすべてPowerShell上に環境を構築しました。
 また以前の記事以降にrp2040-halのアップデート等があり、関数の書き方などが変わっています(rp-rs/sample-projectrp-rs/rp2040-project-templateの方が推奨に切り替わった)。

1.ピンアサインの確認


ピンアサイン

 今回使用するボードも以前と同じくRPi Picoを使います。そしてLCDとして、Waveshare社から発売されているPico LCD 1.3を使います。今回はSwitchScienceで購入しました。
 このLCDを制御しているST7789はSPIで通信するので、PicoのSPI1を使います。注意事項があるとすれば、本来のSPI1であればGP8/GP12のいずれかをデータ転送(Controller In Peripheral Out/Master In Slave Out)で使います。しかしST7789の場合、GP8をデータ/コマンド選択ピン、GP12はローアクティブのリセットピンとして使うので注意が必要です。同様にGP13はバックライトの制御で使うので注意してください。GP8/GP12/GP13はGPIOピンとして使います。


2.画面描画


 今回はFloyd-FishさんのSTM32向けCライブラリを参考に、レジスタの初期設定と各ピクセルの描画を行いました。データシートをよく見ずに書き始めたので、自分が分かっていないレジスタと設定値が存在しています(後から見返したらデータシートに載っていた件)。なので、ここではレジスタの設定値の話はパスします。
 全体のコードはrp-rsさんのLチカのテンプレートをベースに手を入れました。一番悩んだのがSPIやGPIOをラップした関数を作ることで、いまだにわかっていない部分がいろいろあります。が、とりあえず動いたのでここではヨシッ。関門だった初期化関数も一応かけました(この書き方が組み込みRustのお作法にかなっているのかがよくわからん)。

3.まとめ



 今回はRust+RPi PicoでTFT LCDを使う話でした。いろいろな方のコードを切ったりはったり参考に作りました。特にrp2040-halの開発元が公開しているサンプルコードは役立ちました。ちゃんとRustを触るのは初めてで困ることも多かったですが、最低限の機能を実装し、どうにかこうにか動くところまで行けました。まだ使っていないマイコンの機能もあるので、仕事が忙しくなる前にもう少し遊んでみようかと思います(UARTのポーリングし続けないと切られる問題や、GP8の切り替えがdelayを挟まないとデータ転送のタイミングとずれてくる問題)。

4.参考


開発環境周りで参考にした@ochaochaocha3さんのQiita:https://qiita.com/ochaochaocha3/items/1969d76debd6d3b42269








©2022 shts All Right Reserved.

2022年3月16日水曜日

PYNQとVitis HLSで作るFMA演算IP(4):おまけネタ


前回までで、FMA演算IPを作成することができました。今回はおまけ編で、データ型を変えたり並列数を増やしたりしてみます。



1.いろいろなデータ型に対応させる


 せっかくの専用回路だし、いろいろなデータ型に対応させてみましょう。今回は参考例として16bit固定小数点(q4.12)に対応させてみます。
 ...とはいうものの、高位合成用のサンプルコードはテンプレート関数で記述されているうえに、データ型はFMA.hpp内のusing data_type = float;の部分で一元的に管理しています。なので、ここの部分を固定小数点ように書き換えればいいわけです。
 Vitis HLSでは固定小数点用のライブラリとしてap_fixed.hが提供されており、これを使用して記述すればいいわけです(書き換えるとusing data_type = ap_fixed<data_width, 4>;となる)。あとは、データ幅を16bitに下げればOKです(constexpr data_width = 16;)。
  JupyterNotebookのコードについては、最初のせるのarray_typeをnp.int16、fractionを12、enable_fp2fixをTrueにすれば動くはずです。

2.AXI-Streamのデータ幅を増やす


 こちらに関しては、FMA.cppのbus_widthを書き換え、JupyterNotebookのbus_widthを書き換えます。また回路を作成する際に使用したDMA IPのStream Data Widthを指定の値にします。DMA IPの設定値からわかるように、データ幅は64, 128, 256, 512, 1024といった値しかとることができません。注意しましょう。
 データ幅を増やすことで、並列数を増やすことができます。DMAの転送能力次第な部分はありますが、そこそこの速度向上が達成できそうな雰囲気でした(160万個のデータを流して210MFLOP程度)。

3.遭遇したトラブル


  • AXI-Streamのデータ幅について、fp32+1024bitの時やfix16+256bitの場合でHLSのCoSimの段階から結果がおかしくなっていた。CSimの段階では正常な計算結果を返すし、他のデータ幅の場合とはデータ幅とデータ型以外変えていないので、まったくもって謎だった。
  • float配列とデータバスのunionを作成し、型変換を実装する予定だったが、ap_intやap_fixedをunionのメンバに入れるとコンパイルが通らなくなるのでやめた。
  • float配列とCの組み込み整数型のunionを含むコードをVivado HLSで高位合成かけようとしたところ、合成に失敗した。Vitis HLSを使うと、合成がかかった。
  • 回路が動きそうでも動かない場合、Vivadoのプロジェクトを作り直すと動く場合があった。
  • この記事を書くのに、中の人の睡眠時間とGT7のプレイ時間が削られまくった。



©2022 shts All Right Reserved.

PYNQとVitis HLSで作るFMA演算IP(3):実機での実行


 前回前々回で、とりあえ回路情報の入ったbitstreamファイルまで生成できました。今回は実機実装していきます。





1.実機のコードについて


 今回の回路構成の場合、DMAの関数に64bitの配列を渡す必要があるように見えるのですが、実際には計算で使うデータ型(今回の場合はfp32:np.float32)の配列を使用して問題ないです。ただし、配列の総サイズ(bit)が64bitで割り切れる必要があります。割り切れない場合は、ゼロパディングで埋めるといいでしょう。
 今回作成したIPはAXI-Liteで制御するのですが、開始の制御以外にループ回数の指定を行います(0x10にint32でループ回数を書き込む)。また、0x00に1を書き込むことで、IPの開始を制御できます。そのほかとしては、dmaの転送設定を自作IPの開始前に行うあたりが少し特殊かもしれないです。
 一応参考としてIPの実行時間を計測します。具体的にはメモリからデータを読み出し始めてから、書き込みが終わるまでの時間を計測します(自作IPをスタートさせたタイミングで入力データの転送が開始され、受信側のDMA IPが終了するタイミングでIPの実行も終了するため)。

2.実行結果

実行結果。140MFLOPS程度の性能であった。


 サンプルコードではCPUの計算結果との比較を行っているのですが、特段表示が出なければ一致していると思ってください。実行速度に関しては、20万回FMAの計算を実行した場合で大体140MFLOPS程度が出ているようです(実はループ回数をいじると計算回数が変化し、計算速度が変化する)。
 CPUで実行する場合と比べると...まぁ遅いといった感想になるでしょうか。クロック周波数が200MHzしか出ておらず、またFMAの計算に16*5nsかかっていることを踏まえればこの程度かなとは思っています。

3.今後


 自作のFMA演算IPを実機実装することができました。次回はAXI-Streamのデータ幅拡張や異なるデータ型への対応についての話です。




©2022 shts All Right Reserved.

PYNQとVitis HLSで作るFMA演算IP(2):高位合成と回路合成



 前回高位合成用のC++コードを作成しました。今回はそのコードを使って高位合成を行い、その流れで回路まで作成します。




1.高位合成

 今回はVitis HLSを用いてPYNQ-Z2向けに合成を行います。その際の設定は以下の通り。
  • ターゲット周波数:200MHz
  • Uncertainty:12.5%
  • ターゲットデバイス:pynq-z2(xc7z020clg400-1)
  • 言語:C++0x
 今回は2つのソースファイル(FMA.cpp, FMA.hpp)、1つのテストベンチ(FMA_TB.cpp)を登録します。このうちFMA.cppとFMA_TB.cppのCFLAGSに-std=c++0xオプションを付与します。
 合成結果はこんな感じになりました。

高位合成の結果

 Iteration Latencyは16となりましたが、Intervalが1になっていることからいい感じにパイプライン化できたようです。使用率は
  • DSP : 4%(10/220)
  • LUT:3%(1992/53200)
  • FF:1%(1718/106400)
  • BRAM:0%(0/140)
となっています。余裕ありありですね。合成されたデータフローはこんな感じでした。

データフローの一部。2か所ほど"長い棒"がみえるが、
それぞれfmulとfaddの計算ステップとなっていた。

 一応仕様通りに2並列で計算できているようです。また大半のステップがfmulとfaddで占められていることもわかりました。
 この後CoSimを走らせて結果を確認後、IPを出力します(そのまま出力してもいいのだが、Configuration...のボタンからVendor、Library、Description、Display Nameあたりをいじってからいったん出力->Solution Settings内のConfiguration Settingsのconfig_exportにあるipnameをいじってから出力すると、VivadoでIPを読み込んだ際にオリジナルの名前が付き、ベンダー名で検索がかけられる)。

2.回路合成


 高位合成で作成したIPを使って回路を作成します。ZYNQのIPを最初に置いて、Run Connection Automationを走らせるとDDRとFIXED_IOと外部端子がつながるはずです。
 その後、AXI_HPポートの0から3を有効化します。データ幅はすべて64bitにしましょう。

AXI_HPポートの設定。データ幅はすべて64bitとした。

 AXI_HPポートの設定後、IPへ供給するクロックの設定をします。今回は単一のクロックソースかつ周波数が200MHz程度なので、ZYNQのIPから供給します。Clock Configuration->PL Fabric Clocksの順にたどり、FCLK_CLK0を200MHzに設定します。

クロック設定。今回はHLSに合わせて200MHz。

 ZYNQ IPの設定が終わったら、AXI Direct Memory AccessとAXI Interconnectを4つずつ出します。
 AXI Interconnectの方はMasterとSlaveの数をそれぞれ1に設定します。
DMAの方は入力側(W, X, B)と出力側(Y)で少し異なります。入力側のDMA IPについて、Enable Scatter Gather Engineを無効化し、Width of Buffer Legth Registerを26bits、Address Widthを64Bitsにしておきます。データ入力のIPなので、Read Channelのみ有効にしておきます。Memory Map Data WidthとStream Data Widthは64にしておきます。

読み込み側のDMA IPの設定

 出力側のDMA IPについては、入力側で行ったEnable Scatter Gather Enginenの無効化、Width of Buffer Legth Register->26bits、Address Width->64Bitsの操作を同じく行います。その後、Read Channelを無効化し、Write Channelの有効かを行います。Memory Map Data WidthとStream Data WidthはAUTOのままでOKです。

書き込み側のDMA IPの設定

 最後に各IPを接続します。DMAのIPをCPU側から制御するため、個々のIPに割り当てられている名前が必要になります。サンプルコードをそのまま使いたい場合、
  • S_AXI_HP0 -> axi_dma_0(+interconnect) -> w_axi_0
  • S_AXI_HP1 -> axi_dma_1(+interconnect) -> x_axi_1
  • S_AXI_HP2 -> axi_dma_2(+interconnect) -> b_axi_2
  • y_axi_3 -> axi_dma_3(+interconnect) -> S_AXI_HP3
と接続しましょう。

IPの配線図。
Wが0、Xが1、Bが2、Yが3と覚えると分かりやすい。

 この接続を終えると、Run Connection Automationの表示が出るので、すべてにチェックを入れて実行すれば最終的な回路の完成です。この後、アドレスエディタでDMAと自作IPに対してアドレスを割り当てておきましょう。

 Generate Output Productsを実行し、Create HDL WrapperをLet Vivado manage...の方に入れておきます。Generate Bitsteramをクリックすれば回路の合成が始まります(2回に1回ぐらいで失敗するが、Output Productsをリセットしたのち、Generateしなおして再びBitstreamの生成を行えばいけるはず)。
 Bitstreamの生成が終わったら、.bitのファイルと.hwhのファイルを書きだします。画面左上のFile->Export->Export Hardwareを選択し、Platform typeをFixed、Outputを Include bitstreamに設定します。その後、XSAのファイル名を聞かれるので、適当な名前にします(サンプルコードの通りにしたい場合は、design_1_wrapperをdesign_1に変更)。出力先はどこでもよいです。
 .bitと.hwhはこのXSAファイルに入っているので、7zip等を使い取り出します。最終的にこの.bitと.hwhのファイルを.ipynbから読みだしてプログラムを実行することになります。

3.次回

.bitファイルと.hwhファイルが生成できたので、次回はPYNQ側のコードと速度計測を行っていきます。



©2022 shts All Right Reserved.


2022年3月15日火曜日

PYNQとVitis HLSで作るFMA演算IP(1):簡単な仕様策定とHLSコードの作成



 修論の提出まで終わり、無事修士課程を修了できた中の人です。修論ではとある専用計算回路を作成したのですが、時間の都合上PS-PL間の通信部に関してあまり手を入れることができませんでした。そこで今回はその通信部について、PYNQ-Z2へのFMA(積和)演算IPの実装を題材として、いろいろ試してみようと思います。
 今回のコードについてはこちらからどうぞ。



0.開発環境


  • 使用ボード:PYNQ-Z2(PYNQ v2.6)
  • Vivado:2020.1
  • Vitis HLS:2020.1
  • 高位合成言語:C++0x
  • OS:Ubuntu20.04(on WSL2)
 

1.FMA演算IPの簡単な仕様


 今回は、積和演算を同時に複数実行する演算IPを作成します。演算器自体はそこまで重要ではないのですが、今回重要となるのはPS部とIPを接続する通信部です。

 今回使用するPYNQ-Z2において、PS部とIPの通信方法はいくつかあります。どの通信方法を使うかについて少し考えます。
 まずFMAの演算器の個数よりも多くのデータを流すことを考えると、演算器を複数回使いまわす必要があります。その場合にはパイプライン化した方がより高速に実行できるようになります。パイプライン化を行う際に注意すべき点として、パイプラインに流すデータの取得が滞ってしまうとストールすることが挙げられます。
 となると、PS-IP間の通信として連続してデータを送ることができるAXI-Streamを使うとよさげなことがわかります(Xilinx提供のAXI Direct Memory AccessがPL側とAXI-Streamで接続するのも理由の一つ)。

全体のブロック図(W〇Xは要素積を表す)

 転送規格は決まったので、より詳しい部分を決めていきます。AXI-Streamのデータ幅はとりあえず64bitにします(後で広げる)。また実際に計算するデータの型を単精度浮動小数点(FP32)に仮決定しておきます。64bit幅のバスであればFP32のデータを2つ同時に流すことができるので、同時に2のFMA命令を実行できる回路を作成することにします。

自作IPのブロック図

2.HLS用のコード


 さてここからはHLS用のコードについてです(コードについてはこちら)。IP内部のブロック図を基にコードを作成すると、必要になるのが
  • slicer:64bitのデータを32bit x 2個のデータに分割する
  • array_(w, x, b, y):32bit x 2個のデータを保持する
  • fma_unit:y=wx+bを2並列同時に行う
  • packer:32bit x 2個のデータを64bitのデータに分割する
となります。array_(w, x, b, y)に関しては、C言語の配列(C++の生配列)とarray_partitionプラグマを使用します(std::arrayはHLSで合成できないので)。
 以下のコード例では配列を要素ごとに分割して実装しています(data_type=float, field_length=2)。  

data_type w_vec[field_length];
#pragma HLS array_partition variable=w_vec complete dim=0

 fma_unitはfor-loopとloop_unrollプラグマで2並列分(length=field_length=2)の演算器を実装します。
template<typename data_t, uint64_t length>
void fma(data_t w[], data_t x[], data_t b[], data_t y[]){
  for (uint64_t i = 0; i < length; i++){
#pragma HLS UNROLL
    y[i] = w[i] * x[i] + b[i];
  }
}

 slicerとpackerについては、ビットマスクとシフトを使っていい感じにスライス・パックしていくだけです。しかしながら、floatからAXI-Streamで使用するデータ型(ap_axiu)へ単純に代入した場合型キャストが発生し整数部分しか読み取られない問題や、floatのフォーマットでPSから送信されたビット列をintとして解釈し、floatにキャストした際にデータが化ける問題があります。
 これはap_axiu(ap_axis)のデータ部がap_uint(ap_int)で記述されていることに起因しており、c言語でいうところのfloatとunsigned int(int)間のキャストと同じことが発生しています。
 この対策として、変数のポインタをキャストすることで、ビット列の解釈をプログラム上で変更するコードを入れています(floatの内部表現を確認するときにやるやつ)。
inline void slicer(axi_bus_type bit_data, data_type array[]){
#pragma HLS INLINE
  ap_uint<data_bit_width> buffer, bit_mask = -1;
  ap_uint<data_bus_width> bus_buffer;
  bus_buffer = bit_data.data;
  for (uint64_t i = 0; i < field_length; i++){
#pragma HLS UNROLL
    buffer = (bus_buffer >> (data_bit_width * i)) & bit_mask;
    array[i] = *((data_type*) &buffer);
  }
} 

inline void packer(data_type data[], packed_type bus_buffer[]){
#pragma HLS INLINE
  ap_uint<data_bit_width> buffer;
  bus_buffer[0] = 0;
  for (uint64_t i = 0; i < field_length; i++){
#pragma HLS UNROLL
    buffer = *((ap_uint<data_bit_width>*) &data[i]);
    bus_buffer[0] |= ((packed_type)buffer) << (data_bit_width * i);
  }
}

 最後これらの関数をまとめ、HLS向けにラップした関数を準備します(コードはこちら).このラップ関数の実体は.hppに記述するのではなく.cppに記述します(.hppに書くと高位合成のターゲット関数にならない).
 今回はこれに加えてテストベンチを準備します(コードはこちら).基本的には入力データを仕込み,通常のC言語経由とRTL経由で計算を行い比較を行う流れになっています.

3.次回


今回はHLS用のコードまで作成しました。この後は実際の合成をおこないます。
合成して動作確認まで行った後は、
あたりをやっていこうと考えています。


©2022 shts All Right Reserved.

2021年11月19日金曜日

PYNQ-Z2向けのアクリル天板のレーザーカットを頼んでみた話



修論が忙しいのに何も手を付けていない中の人です。修論で自前のFPGAボード(PYNQ-Z2)を使ってるのですが、基本裸運用なのでショートやらなんやらが怖い状態です。今回はその対策としてアクリルの天板を作成してみた話です。

基板には元から足がついているので5mm程度浮いているが,
基本むき出しなので不安が残る





1.設計する


部品の配置図.
配置するエリアのサイズを入れることで,領域内に入りますよアピール.

天板の設計をするにあたり寸法図を探したのですが、ネットの海にはなかったようでした。そこでその辺にノギスが生えていたので各部を測定し寸法を割り出しました。寸法を割り出したのちFusion360で実際の設計を行い、dxfで出力しました。



2.発注する


完成予想図

設計ができたので次は発注です。今回は手間をあまりかけたくなかったので、レーザーカットのサービスを使うこととし、いろいろ探した結果いつも基板を頼んでいるElecrowのサービスを使うことにしました。
基板の発注と異なる点として、領域内であればいくつでも部品を置くことができます。無理に1つの部品にする必要はありませんでした。
先ほど作成したdxfのファイルとともに、カット時の部品の配置図と完成予想図をpdfで添付しました。完成予想図は正直に言っていらないのですが、たまたまあったので入れました。
発送業者に関しては、最近追加された佐川急便を使ってみました。

3.到着と組み立て


届いた天板とFPGA基板.合計5セット届く.

発注が11/10で、到着が11/18だったので1週間ちょっとぐらいでした。おそらく基板と同じ感覚で待てばよさそうです。
実際に組み立てるとこんな感じになりました。PYNQ-Z2に搭載されるチップであればそこまで発熱しないのでファンは不要なのですが、スペースが開いていたので付けました。
スペーサーは秋月、ねじ・ナット・ワッシャーは近所のホームセンターで購入しました。

組み立てた様子.この時はファンが外側についている.
ファンが外に出っ張っていると壊す恐れがあり,この後内側に移設した.

4.まとめ


初めてレーザーカットのサービスを使ってみましたが、特に詰まるところはありませんでした。簡単な形状であればプリント基板を作るよりも楽にできました。印字みたいなこともできるようなので、また機会があれば試してみたいです。


©2021 shts All Right Reserved.


2021年11月12日金曜日

ctypesを使ってPython3からC++の生配列(not std::vector)をいじる


修論がだんだん忙しくなってきた中の人です.研究でPythonからC++のコードをたたく必要が出てきたのでいろいろお試ししていたのですが,その際にいろいろ詰まったので書き残しておきます.今回のコードはこちらに上げてあります.



1.PythonからC++をさわる


Python側からC++のコードを実行する方法はPython.hやSWIG,Boost.Python,pybind11など様々なものがあります(参考1).今回の研究で使用している環境(HLSツールや実行環境)の問題で,以下の制約がありました.

  • 相互呼び出しの機能は不要(PythonからC++を実行できればOK)
  • STLコンテナが使えない(≒std::vectorなどを使う必要がない)
  • g++やCPythonに標準で入っているライブラリのみで行けると非常に楽

いろいろ調べた結果,とりあえずctypesを使ってみることにしました.

2.C++で共有ライブラリを作成する


今回は以下のC++コードをPythonから呼び出します.

// MAC_VEC.hpp
#include <cstdint>
namespace MAC_VEC{
    template<typename data_t>
    void mac_vec(uint32_t size, data_t a[], data_t x[], data_t b[], data_t y[]){
        for(uint32_t i = 0; i < size; i++){
            y[i] = a[i] * x[i] + b[i];
        }
    }
}

ctypesを使う場合テンプレートをそのまま呼び出すことはできないので,型を確定させるラッパーを作っておきます.

// MAC_VEC_WRAPPER.cpp
#include <cstdint>
#include "MAC_VEC.hpp"

extern "C" {
    void MAC_VEC_FP32(uint32_t size, float a[], float x[], float b[], float y[]){
        MAC_VEC::mac_vec<float>(size, a, x, b, y);
    }
    void MAC_VEC_INT16(uint32_t size, int16_t a[], int16_t x[], int16_t b[], int16_t y[]){
        MAC_VEC::mac_vec<int16_t>(size, a, x, b, y);
    }
    void MAC_VEC_UINT32(uint32_t size, uint32_t a[], uint32_t x[], uint32_t b[], uint32_t y[]){
        MAC_VEC::mac_vec<uint32_t>(size, a, x, b, y);
    }
}

とりあえずfloat(fp32),int16, uint32の3パターン準備しました.この時extern "C"をつけないと,コンパイラ側でシンボル名を書き換える操作を行うため,Python側から読めなくなることがあります(参考2).
この二つが準備できたら,g++で共有ライブラリ(.so)を生成します.

// generate_so.sh
g++ MAC_VEC_WRAPPER.cpp -shared -o MAC_VEC.so

コンパイルが通ると,MAC_VEC.soが生成されるはずです.

3.Pythonから動かす


先ほど作成したライブラリをPythonから動かしてみましょう.今回使用するctypesは,Python2.5から標準で入っているライブラリです.
この記事ではMAC_VEC_FP32のみを扱います.残りの二つのに関しては,こちらの実装を確認してください.

// C_API_TEST.py
import ctypes

# load .so file
libc = ctypes.cdll.LoadLibrary("./MAC_VEC.so")

# define params and src/dst data
len_vec = 8
coef = 1.1
vec_x = [i * coef for i in range(len_vec)]
vec_a = [2 * coef for i in range(len_vec)]
vec_b = [(i - 8) * coef for i in range(len_vec)]
vec_y = [0 for i in range(len_vec)]

# define data type and convert method
len_vec_type = ctypes.c_uint32
data_type = ctypes.c_float
vec_type = data_type * len_vec

# convert list-type data
_len_vec = len_vec_type(len_vec)
_vec_x = vec_type(*vec_x)
_vec_a = vec_type(*vec_a)
_vec_b = vec_type(*vec_b)
_vec_y = vec_type(*vec_y)

# execution
libc.MAC_VEC_FP32(_len_vec, _vec_a, _vec_x, _vec_b, _vec_y)

# convert results to list-type
result = list(_vec_y)
print(result)

配列の型を宣言するあたりの作法で詰まりました.配列のデータ型×配列の要素数で表現し,アンパックしたlistを投げ込むことで変換できるようです.
実行すると,こんな感じの結果が得られました.

実行結果.INT16とUINT32の結果も表示している.

とりあえず正しい結果が得られているようです.

4. まとめ


今回はPythonからC++の生配列をいじってみました.ライブラリの追加も最小限で済み,割と簡単に動かすことができました.少し凝ったデータのやり取りをしようとすると大変かもしれないですが,C/C++標準のデータ型であれば十分に使えると思います.


©2021 shts All Right Reserved.

2021年10月13日水曜日

GeekServoで遊んでみた




数か月間ブログの更新をさぼっていた中の人です。今回はLEGOと互換性のあるDCモータ・サーボモータであるGeekServoで遊んでみた話です。



0.GeekServoとは


GeekServoはKittenBot社から発売されているRCサーボモータ・DCモータのシリーズで、日本ではSwitchScienceあたりで購入できます。

1.外見



今回試したのは、GeekServoの9G Motor-Redです。GeekServoのシリーズの中ではこのモデルだけがDCモータで、あとはRCサーボになっています。
出力軸は十字のシャフトになっており、ギアなどを直接接続することができます。また、側面などに丸穴やポッチがあるのでペグやシャフトを接続できます。

2.実際に組んでみる



手元にあった部品を使ってとりあえず組んでみました。


中身はこんな感じです。横軸と縦軸の両方出力軸に設定できるあたりはいいのですが、ギアの段数が多いために終端のガタが大きいのと、伝達できるトルクが少ないのが欠点でしょうか。
ここまで大げさな構造になったのは、軸の位置が縦と横の両方で半ポッチずつずれているためで、縦方向には半ポッチ幅のパーツを挟んで調節し、横方向は12枚歯のベベルギアの位置をハーフブッシュでずらしてかみ合う位置に移動させました。
きれいに0.5ポッチずれているので、半ポッチずらすテクニックがあれば普通に使えそうです。

3.まとめ


今回初めてGeekServoを触ってみたのですが、割と使い勝手がよくびっくりしました。半ポッチずらす必要があるなど多少癖はあるものの、それさえ解消できれば非常に良いものかなと思います。


©2021 shts All Right Reserved.

2021年5月26日水曜日

UnitV2が手に入った



暇な時間が欲しい中の人です.今回はM5Stackから発売されたUnitV2を手に入れたのでテストしていきます.


0.UnitV2とは


UnitV2はM5Stackから発売されたプロトタイピングツールです.
M5Stackのラインナップの中でも最も計算性能が高いものになっています.
UnitV"2"の名前が示すようにUnitVの後継モデルとなっていますが,CPUに互換性はないうえに,2の方はLinuxが動作しています.

1.外観と内容物



UnitV2のサイズはUnitVよりもStickVに近いです.ただUnitV2はバッテリーを含まないのでその分薄くなっています.


内容物はこんな感じです.16GBのSDカードがついてきます.USBはTypeA-TypeC(おそらく2.0)です.


治具(1).GoProのものに似たヒンジになっています.


治具(2).LEGOの部品が付くやつです(赤い部品は付属品ではないです).


手持ちの画像認識系デバイスを並べました(UnitVはレンズ交換済み).UnitVの小ささが際立っていますね.

2.遊ぶ前の儀式


UnitV2とPCをUSBでつなげるとファンが回転し始め,だんだん暖かくなってきます.この時にデバイスマネージャでUnitV2を探すと不明なデバイスとして認識されます.そのためドライバを充てる必要があります.ドライバはチュートリアルのページにあるリンクで配布されているので突っ込んでおきます.


チュートリアルページの通りにドライバを認識させたのち,ブラウザ(Chromeで動作確認済み)を開き,URLのところに UnitV2.py or 10.254.239.1 と入力します.すると最初から入っているでもプログラムを確認できます.

3.デモの様子




こんな感じで動きます.Yoloの実行の様子を見ている感じだと,UnitVやStickVで使われていたK210のアクセラレータ使用時ほどではないにしろ,そこそこの速さで動いている感じです.

4.SSH



UnitV2は初期状態でSSH接続による操作をすることができます.


適当なターミナルソフトで 10.254.239.1:22 にアクセスし,username : m5stack,初期パスワード : 12345678で入れるはずです(本体裏面のシールに書いてある).
組み込みLinuxなので,コマンドポチポチしていくと色々見ることができます.
ただし,スーパーユーザーになれなかったので電源をコマンドラインから切ることができませんでした(中の人がやり方を知らないだけかもしれません).

cpuinfoを読みに行った様子.確かにデュアルコア.

5.その他



Armv7のデュアルコア(1.2GHz)は初期のRPiを凌駕するほどのCPU性能であり,当然のごとく熱がすごいです.この対策として冷却ファンがついているのですが,振動と音が出ます.もしかしたら気になる人がいるかもしれないです.

6.まとめ


UnitV2を初めて動かしてみました.ドライバのインストールと電源をコマンドラインから切れないこと以外は特に気になる点はありませんでした.
今後はncnnやOpenCVを使っていろいろやっていこうと思っています.



cargo run


©2021 shts All Right Reserved.