FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

映画「7つの会議」を見てきました

nanakai-movie.jp

七つの会議 (集英社文庫)

七つの会議 (集英社文庫)

映画「7つの会議」を見てきました。企業の隠蔽体質というか、ブラックな部分を暴く映画としては結構面白くて見入ってしまった。

こういう隠蔽の話って、営業とかの話に限らず、エンジニアでも起こりうる話だと思っている。 たとえば性能を達成するために無茶な変更をしてしまっただとか、バグが取り切れていないのにリリースしちゃったりだとか、そういう話ってよくある。

特に私はハードウェア界隈にいて一度ASICを起こしてしまえば修正は大変なので、性能やバグの隠蔽は一大事だ。 とはいえハードウェアに限らずソフトウェア業界にも同じ話は起こりうる。 インフラシステムとかは一度リリースするとなかなか止められない。ソフトウェアの方がそのあたりの問題は深刻じゃないかと思っている。

そういう中で「バグ」とか「問題」とかは決して消えることは無いし、スケジュール通りにリリースできることなんてないだろう。 そんな常にスケジュールとノルマに追いかけられている状況でどのようにプロジェクトを進めればよいのだろう。

私が思うのは、やはり風通しの良いチーム環境づくりと、問題が発生すればすぐに報告できる環境だ。 問題が起きるのはしょうがない。しょうがないので、その問題をいかにして回避できるかチーム全体ですぐに相談できる体制が必要となる。

バグが発生したときになぜなぜ分析をすることは重要だ。 しかしその中で大事なのは決して個人攻撃にならないこと。 「システムを憎んで人を憎まず」という訳で、そのような問題を発生させてしまったシステムを問題視すべきで、個人攻撃になるような環境は避けるべきだ。 それが最終的に、問題を隠すことなくすぐに相談できる環境につながるのだと思う。

まあそういう状況下でどのようにしてスケジュールを合わせ、売り上げを上げていくかという組織全体の問題は難しい。 トップから見てみると、呑気に自分のペースで仕事を進めるエンジニアはムカつくし、そんな奴に限って「エンジニアの一方的な主張」みたいなブログを書いて炎上したりする。 机とかキーボードとかその時の気分で一方的に主張せずに、締め切りと組織のことも考えなきゃね。

オリジナルLLVM Backendを追加しよう (17. 命令フォーマット増強)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

関数コールに対応するためには、EmitPrologueおよびEmitEpilogueを実装しなければならない。 このためには、ロードストア命令・アドレス計算に必要な命令を定義しなければならない。

LUI命令は、Load Immediate Upperに相当する命令だが、これはRISC-V命令セットのうちでU-Typeに相当する。

f:id:msyksphinz:20190211004147p:plain
RISC-Vの命令フォーマット

U-Typeを命令フォーマットで定義した。

  • lib/Target/MYRISCVX/MYRISCVXInstrFormats.td
class FU<bits<7> opcode,
         dag outs, dag ins, string asmstr, list<dag> pattern,
         InstrItinClass itin>: MYRISCVXInst<outs, ins, asmstr, pattern, itin, FrmI>
{
  bits<5>  rd;
  bits<20> imm20;

  let Opcode = opcode;

  let Inst{31-12} = imm20;
  let Inst{11-7}  = rd;
}

これに基づいて、LUI命令をはじめとする命令群を定義していく。

def ORI  : ArithLogicI<0b0010011, 0b110, "ori", or, uimm12, immZExt12, GPR>;
def LUI  : ArithLogicU<0b0110111, "lui", simm20, immSExt12>;
def ADD  : ArithLogicR<0b0110011, 0b000, "add", add, GPR>;
def SRLI : shift_rotate_imm32<0b0010011, 0b101, 0x00, "shli", shl>;

定数を扱うためのパターンも定義してゆく。

// Small immediates
def : Pat<(i32 immSExt12:$in),
          (ADDI ZERO, imm:$in)>;

def : Pat<(i32 immZExt12:$in),
          (ORI ZERO, imm:$in)>;

def : Pat<(i32 immLow12Zero:$in),
          (LUI (HI20 imm:$in))>;

// Arbitrary immediates
def : Pat<(i32 imm:$imm),
           (ORI (LUI (HI20 imm:$imm)), (LO12 imm:$imm))>;

この中で登場するLO12とか、HI20は以下のように定義される。

// Transformation Function - get the lower 12 bits.
def LO12 : SDNodeXForm<imm, [{
  return getImm(N, N->getZExtValue() & 0xfff);
}]>;

// Transformation Function - get the higher 20 bits.
def HI20 : SDNodeXForm<imm, [{
  return getImm(N, (N->getZExtValue() >> 12) & 0xfffff);
}]>;

EmitPrologue, EmitEpilogue, さらにloadImmediate()関数を追加してビルドを実行した。

一応ビルドに成功し、命令の選択はできるようになったが途中で固まってしまう。どこかで無限ループしているか?解析する必要がありそうだ。

$ ./bin/llc -march=myriscvx64 -mcpu=myriscvx64 -relocation-model=pic -filetype=asm ch3.bc -o -
...

'-noabicalls' is not a recognized feature for this target (ignoring feature)
Selecting: t7: ch = MYRISCVXISD::Ret t6, Register:i32 $a0, t6:1

Selecting: t6: ch,glue = CopyToReg t4, Register:i32 $a0, Constant:i32<0>

Selecting: t4: ch = store<(store 4 into %ir.retval)> t0, Constant:i32<0>, FrameIndex:i32<0>, undef:i32

Selecting: t5: i32 = Register $a0

Selecting: t1: i32 = Constant<0>

Selecting: t0: ch = EntryToken

Rustのサンプルプログラムをたくさん写経する

f:id:msyksphinz:20171103203951p:plain

Rustにとても興味がある。 いつもソフトウェア環境はC++を使って構築しているので、Rustに移行すると何か良いことがあるのか?

という訳で今年の目標はRustを覚えることに決めた。 Rustの言語使用自体は分かるような気がするのだが、それで現実的なアプリケーションを書くためにはまだ経験が足りない。

RISC-Vの命令セットシミュレータはC++で書いてあるのだが、それをRustに書き直そうとしていきなり挫折した。 C++とは全く違う考え方で書かないと、どうもコンパイルが通ってくれない。

そこで、たくさん経験を積むためにシンプルなRustのプログラムをたくさん書いて見ることにした。 休みの日はひたすらRustのサンプルプログラムをひたすら写経することにした。 この辺りだ。

www.geocities.jp

ひたすら写経した結果は以下。

github.com

もう少しいろいろRustのサンプルコードを見ていきたい。 現時点ではRustは32bit版しか対応していないようなので、最終的にはRocket-ChipとかのRISC-V 64bit版の対応もできたらいいな。

AXIスライサ・Skid Bufferの勉強

AXIは基本的にReady-Validのハンドシェークで動いているのだけれども、これをひたすら繋いでいくと、ValidとReadyがFFを経由せずにつながってしまい、タイミング的に問題となる。

msyksphinz.hatenablog.com

これを改善するために、各種バスプロトコルにはスライサと呼ばれるFFで区切る部品が存在するのだけれども(一般的スライサと言うのかな?分からない)、いろんな実装方法があって、ただし基本的に考え方は全部同じだと思っている。

http://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20160522/20160522022307.png

FIFOを使ったり、Skid Bufferやストリームレジスタを使う方法があり、私が良く参考にしているのは、ステートマシンでSkid Bufferを構成する方法。以下のページが非常に参考になり、よく勉強させてもらっている。

mfn.cocolog-nifty.com

初めて見たときは少し感動してしまったのだが、要するにいくつかの条件に分けてステートマシンを構成しており、

  • マスター側のReady-Valid (mready, mvalid)
  • 内部のMasterとSlaveが埋まっているときの一時格納用のTValid (tvalid)
  • スレーブ側のReady-Valid (sready, svalid)

  • tvalid=0, mready={0,1}, mvalid=0 : の場合、mvalidが上がっていないというとことは、SkidBuffer内にデータがたまっていないのでSreadyは上げる。Skid Bufferに新しいデータを受け入れ可能。

  • tvalid=0, mready=0, mvalid=1 : の場合、mvalidが上がっておりデータを転送したいがmreadyが下がっているのでまだ転送は完了しない。tvalidにデータを格納できるのでSreadyを上げていてもよい。
  • tvalid=0, mready=1, mvalid=1 : の場合、mvalidが上がっておりmreadyも上がっている。つまりマスター側は転送完了なのでスレーブ側からデータを受け入れてよい。
  • tvalid=1, mready={0,1}, mvalid=0 : マスターが転送したいデータが存在しないにもかかわらず、tvalidが上がることはあり得ない。この条件は無効。
  • tvalid=1, mready=0, mvalid=1 : マスターに転送したいデータが存在しているが、まだ転送は完了していない。tvalidも上がっているので、これ以上スレーブ側からはデータを受け入れられない。
  • tvalid=1, mready=1, mvalid=1 : マスター側はデータの転送が完了し、tvalidが立ち上がっているのでそのデータをマスター側に移す。スレーブ側は受け入れられるようになるのでsreadyを上げる。

というステートマシンを構成すればよい。これをVerilogで記述したものが紹介されている。

ちなみに、skid bufferの他の実装については、以下にも解説が載っている。

www.intel.com

f:id:msyksphinz:20190208162452p:plain

付録の実装はAlteraからIntelに社名が変わった時点で消えてしまっているのだが、誰かがGitHubにアップロードしているのを発見した。ありがたい。

github.com

アウトオブオーダ実行の詳細を勉強する場合の資料には"Understanding the detailed Architecture of AMD's 64 bit Core"がとても良い

いろいろ訳あってCPUのアウトオブオーダ実行とか、マルチコアのキャッシュコヒーレンスの仕組みとかを勉強し直しているのだが、勉強するにあたり非常に良い資料がたくさんあるので紹介。

プロセッサアーキテクチャについて学ぶのには、もちろん教科書として「コンピュータの構成と設計(いわゆるパタヘネ)」 と、より詳細な「コンピュータアーキテクチャ 定量的アプローチ」(いわゆるヘネパタ)が存在している。

これらはアウトオブオーダプロセッサの動作概要について詳しく説明されており、また定量的アプローチとだけあって非常に多くのベンチマーク結果を載せている良書なのであるが、各アーキテクチャの実現方法については少し甘さがあると思っている(まあ、定量的ベンチマーキングのための本なので、当然である)。

より実際の設計に近いところで、どのような実装やどのような方式を取ればよいのか調べるときに、もちろんいろんな資料があるのだが、私が一番最初に勉強したのは、"Understanding the detailed Architecture of AMD's 64 bit Core" という資料である。

かなり古い資料であるが、AMDの当時の最先端の64ビットプロセッサの中身を解析し、アウトオブオーダ処理、メモリアクセス、キャッシュコヒーレンスなどの実現方法について事細かに解説してあるので、一度読んでみることをお勧めする。

Chip Architect: Detailed Architecture of AMD's Opteron

ちなみに、原作者に連絡が取れず許可をいただいていないのだが、私も向学のために日本語に翻訳してみている。 まだ日本語のつながりとか、文法とかも直訳で怪しいけれども。

github.com

マルチコアにおけるキャッシュコヒーレンシ方式の簡単なまとめ (2. ディレクトリベースのキャッシュコヒーレンシ)

過去の日記では、キャッシュスヌープ方式のキャッシュコヒーレンシ制御の方法をざっくりと見たが、今度はもう一つのディレクトリベースのキャッシュコヒーレンシ制御についてみていく。

キャッシュスヌープ方式の記事はこちら。

msyksphinz.hatenablog.com

キャッシュスヌープ方式はこれはこれでよくできた方式だが、コア数が増えると破綻するという問題である。 つまり、コア数が増えていくと至る所で他コアのキャッシュの状態を確認するコヒーレンス要求信号が飛び交い、肝心なキャッシュ制御の性能が落ちてしまう。

なぜこんなことが起きてしまったんだろう?それは、スヌープ方式が「このキャッシュの最新版持っているのはだーれ?」という情報をブロードキャストしないと、最新の情報が手に入らないことに起因する。

では、「このデータならばxxxが最新の状態を管理しているはずだからxxxに聞いてみよう」という風に、最初からデータの最新情報を持っている人が分かっているようにすればどうか?

最新のデータをメモリから取得したければ、そのデータを管理しているノードに問い合わせを行い、管理ノードは誰が最新のデータを持っているのか知っているのでそのノードに通知、最新のデータを持っているノードはそのそのデータを問い合わせ元のノードに転送、とすれば、スヌープキャッシュのようにすべてのコアに問い合わせの信号を送信する必要がなくなる。

という訳で、(例えばアドレスに応じて)各領域でデータの状態を管理するノードを分散し、各ノードには管理しているデータの管理台帳(これをディレクトと呼ぶ)を置いておく方式を「ディレクトリベースのキャッシュコヒーレンス制御」と呼んでいる。

f:id:msyksphinz:20190206105316p:plain
ディレクトリベースのキャッシュコヒーレンス制御方式

例えば、あるデータについて、

  • そのデータを管理しているのはプロセッサ1
  • 最新のデータを持っているのはプロセッサ0 (このことはプロセッサ1のディレクトリに記録してある:貸し出し済み)

の状態であるとき、プロセッサ5がその最新のデータを取得したいとする。この場合どのような手順を取るのか。

f:id:msyksphinz:20190206105949p:plain
  1. バスの構成上プロセッサ1に直接問い合わせできない場合(この図はリングバスだが、例えばダイレクト接続で隣接しているノードにしか問い合わせができないとする)、プロセッサ5は隣のプロセッサ2に問い合わせを投げ、プロセッサ2はプロセッサ1に問い合わせを転送する。
f:id:msyksphinz:20190206110051p:plain
  1. 問い合わせを受けたプロセッサ1は、ディレクトリを見て最新のデータがプロセッサ0のキャッシュに入っていることを確認する。そこで、プロセッサ0に問い合わせを行い、プロセッサ5に対して当該キャッシュラインを転送するように要求する。
  2. プロセッサ0は要求を受け、プロセッサ5にキャッシュラインを転送する。これにより、プロセッサ0とプロセッサ5がキャッシュラインを持っている状態になり、プロセッサ1は自分のディレクトリに当該キャッシュが共有状態であることを記録しておく。
f:id:msyksphinz:20190206110447p:plain

という訳で、非常に簡単な例であるが、ディレクトリベースのキャッシュコヒーレンス制御の方式について考え方をまとめた。 この後、キャッシュがアップデートされた場合のディレクトリのアップデートなど細かい部分はあるが、ここでは基本的な考え方が分かればよいものとしておく。

その基本的な考え方は、スヌープキャッシュの方式の場合にキャッシュの状態を確認するためのブロードキャストのコストを減らし、各ノードが別々にディレクトリという管理台帳を使い、自分の管理下のデータの状態を一括して管理しているというものである。

Western DigitalのRISC-VコアSweRV-EH1 (3. オリジナルプログラムを動かす)

Western DigitalからオリジナルのRISC-VコアSweRVがリリースされ、テストベンチが動かせるようになった。 構造が分かってきたので、今度は自分のプログラムを動かしてみたい。

github.com

マニュアルのメモリマップを見ても正しいようには思えないのだが、とりあえずテキストは0x0000_0000から格納しておけば良いらしい。 以下のようなプログラムを作成し、文字を表示するようにした。 以下は、コンソールに"Hello, msyksphinz"と表示する、つもり。

  • testbench/tests/startup.S
    .section    .text
_start:
...
    li  x10, 0xd0580000
    la  x11, character
loop:
    lb      x1, 0(x11)
    sw      x1, 0(x10)
    addi    x11, x11, 1
    bnez    x1, loop

finish:
    j   finish
...
character:
    .ascii "Hello, msyksphinz"

以下のようにしてrv32モードでコンパイルする。SweRVはRV32なので、64bitモードでコンパイルしてしまわないように。

all: program.hex data.hex

program.hex: program.elf
        elf2hex 8 128 $< > $@

data.hex: program.elf
        elf2hex 8 128 $< > $@

program.elf: startup.S
        riscv32-unknown-elf-as -march=rv32im $< -c -o $<.o
        riscv32-unknown-elf-ld $<.o -Tlink.ld -o $@

hexファイルは、64bit毎にプログラムを並べておけばよい。また、ハーバードアーキテクチャのようなので、プログラム用のprogram.hexと、データ用のdata.hexを用意する。ただし実体は全く同じものだ。

  • program.hex
0000011300000093
0000021300000193
0000031300000293
0000041300000393
0000051300000493
00000597d0580537
0005808303458593
0015859300152023
0000006ffe009ae3
0000001300000013
0000001300000013
0000001300000013
6d202c6f6c6c6548
6e696870736b7973
000000000000007a
0000000000000000

これをreadmemhされるように配置し、さらにテストベンチの実行時間も伸ばす。

diff --git a/testbench/test_tb_top.cpp b/testbench/test_tb_top.cpp
index 92dd89e..7538ecf 100644
--- a/testbench/test_tb_top.cpp
+++ b/testbench/test_tb_top.cpp
@@ -47,7 +47,7 @@ int main(int argc, char** argv) {


   // Simulate
-  for(auto i=0; i<200; ++i){
+  for(auto i=0; i<2000; ++i){
     clkCnt++;
     if(i<10)  {
        tb->reset_l  = 0;

これでシミュレーションを実行した。

make -f $RV_ROOT/tools/Makefile verilator-run
cp /home/msyksphinz/work/riscv/swerv_eh1/testbench/tests/*.hex .
./obj_dir/Vtb_top
Start of sim
Hello, msyksphinz
End of sim

正しく動いたぞ!

f:id:msyksphinz:20190205002042p:plain
自作プログラムが動作し、"Helo, msyksphinz"と表示された。