FPGA開発日記

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

RustでRISC-V命令セットシミュレータを作ろう (7. trait, generics, 64-bitモードへの拡張)

f:id:msyksphinz:20190224185310p:plain

Rustで作るRISC-Vシミュレータ。基本的な形が出来上がった。32bitの整数演算命令はある程度PASSできるようになっている。 C++で作ったRISC-Vシミュレータ同様、64bitモードもサポートして動作させるようにしたい。

C++版では、32bitと64bitモードの実装はtemplateを使って実現していた。具体的には、int32_tint64_tの2つの場合で実装し、オプションでどちらを使うか切り替える。

さて、Rustで同じことを表現したい場合はどうしようか。シミュレータのメインクラスにあたる部分はRustではstructを使って表現している。

pub struct Riscv32Env {
    // m_bitmode: RiscvBitMode,
    pub m_pc: AddrT,
    m_previous_pc: AddrT,
    m_regs: [XlenT; 32],
    pub m_memory: [u8; DRAM_SIZE], // memory
    pub m_csr: RiscvCsr<XlenT>,

    pub m_priv: PrivMode,
    m_maxpriv: PrivMode,
...

これに対してジェネリクスを導入して32bitと64bitの切り替えを行うために、以下のようなものを導入したとする。

pub struct<W> RiscvEnv {
    pub m_pc: AddrT,
    m_previous_pc: AddrT,
    m_regs: [W; 32],
...

なんか難易度が一気に上がった気がする。具体的には、ジェネリクスですべてパラメータ化した部分を、整数などの値でキャストしなければならなくなってしまった。 正しいやり方が全く分からない。

   let reg_data : W = self.reg_read(rs_addr);
   let res = reg_data + 5;

とかやろうとするとすごく怒られた。やり方が間違っているのかもしれないけど。

   let reg_data : W = self.reg_read(rs_addr);
   let res = reg_data + NumCast::from(5).unwrap();

えー、こういうこと書くの?すごく面倒なんだけど。そもそも、Bit Extractionの記述が猛烈に煩雑になるんだけど。

  let extracted_bits : W = (NumCast::from(1).unwrap << reg_data & NumCast::from(15).unwrap()) - NumCast::from(1).unwrap();

何だこりゃ。自分で書いておいて、煩雑すぎて意味が分からない。

このあたりをどのようにテンプレートのように記述すればきれいに書けるのか全く分からなかったので、とりあえずジェネリクスは諦めてRV32とRV64で別々のクラスを作ってしまった。 もったいない。

github.com

  • src/riscv32_core.rs
pub struct Riscv32Env {
...
}
impl Riscv32Env {
    pub fn new() -> Riscv32Env {
        Riscv32Env {
...
}
pub trait Riscv32Core {
    fn get_rs1_addr(inst: InstT) -> RegAddrT;
...
}
impl Riscv32Core for Riscv32Env {
    fn get_rs1_addr(inst: InstT) -> RegAddrT {
        return ((inst >> 15) & 0x1f) as RegAddrT;
    }
...
}
  • src/riscv64_cores.rs
pub struct Riscv64Env {
...
}
impl Riscv64Env {
    pub fn new() -> Riscv64Env {
        Riscv64Env {
...
}
pub trait Riscv64Core {
    fn get_rs1_addr(inst: InstT) -> RegAddrT;
    fn get_rs2_addr(inst: InstT) -> RegAddrT;
...
}
impl Riscv64Core for Riscv64Env {
    fn get_rs1_addr(inst: InstT) -> RegAddrT {
        return ((inst >> 15) & 0x1f) as RegAddrT;
...
}

Csrの実装などは、どうにか上手くジェネリクスを導入で来た気がする。

pub struct RiscvCsrBase<W> {
    pub m_csr: W,
}

impl RiscvCsrBase<i32> {
    pub fn new() -> RiscvCsrBase<i32> {
        RiscvCsrBase { m_csr: 0x0 }
    }
...
impl RiscvCsrBase<i32> {
    pub fn new() -> RiscvCsrBase<i32> {
        RiscvCsrBase { m_csr: 0x0 }
    }
...
impl RiscvCsrBase<i64> {
    pub fn new() -> RiscvCsrBase<i64> {
        RiscvCsrBase { m_csr: 0x0 }
    }

しかし、同じRiscvCsrBaseをベースにしているのだから、RiscvCsrBase<i32>RiscvCsrBase<i64>の両方で実装を定義しなくても、templateを使ったときみたいに型のキャストとかも自動的に実行してくれればよいのに。 そういうことを言い始めると、安全でないプログラムになってしまうのかなあ。

とりあえず、コンパイルできることろまではたどり着いた。テストパタンは全くPASSしていないので、デバッグが必要だ。

AWS F1インスタンス上のFireSimでBOOMコアをシミュレーションする試行(1. 環境の立ち上げ)

AWS F1インスタンス上でRISC-Vコアを動かすことのできるFireSimは、徐々にバージョンが上がっており、現在はBOOM(Berkeley Out-of Order Machine)のLinux起動もサポートできるようになっているらしい。

fires.im

一度、F1インスタンスチュートリアルはやってみたことがあるのだが、しばらく時間もたっているし、前回はRocketコアで検証した環境を、BOOMコアを使って再検証してみたい。

チュートリアルを見ながら、再度FireSimをF1インスタンス上に構築するチュートリアルを試してみることにした。以下の資料を参考にした。

docs.fires.im

FireSimを使用するためには、以下のインスタンスを用意する必要がある。

  • マネージャインスタンス : SSHでユーザがログインして操作をおコアぬためのインスタンスです。FireSimのリポジトリのクローンや、FireSimマネージャを使用してデザインのビルドやシミュレーションを行います。
  • ビルドファーム : FireSimにより起動・終了されるインスタンスで、FPGAデザインのビルドのために使われます。このインスタンス内でVivadoが起動し、FPGA向けのデザインが生成されます。
  • ランファーム : F1インスタンスとM4インスタンスの集合体で、シミュレーションが行われるのがこのインスタンスです。ランファームを同時に複数立ち上げて、並列シミュレーションを実行することも可能です。

FireSimは、基本的に以下の方法で立ち上げる。

まずはF1インスタンスを使用するための環境構築をおこなう。

FireSimを使用するためには、F1インスタンスが使用できるAWSりーじょうん内で作業が必要で、F1インスタンスが使用できるAWSリージョンは、

  • US East(North Virginia) : us-east-1
  • US West(Oregon) : us-west-2
  • EU West(Ireland) : eu-west-1

が使用可能となっている。Tokyoリージョンは使用できないので、今回はNorth Virginiaリージョンを使用しました。

North Virginiaのリージョンを使用したことがない場合、インスタンスにアクセスするためのSSHキーペアを作成する必要がある。 これにはEC2マネジメントコンソールから、"Key Pairs"をクリックし、"firesim"という名前のキーペアを作成する。そして暗号鍵であるfiresim.pemをダウンロードし、マネージャインスタンスにアクセスするために使用する。 詳細は、AWS EC2インスタンスSSHアクセス方法を確認すること。コンソールからログイン、PuTTYなどのWindowsクライアントなど、どの環境を使用するかで作業方法が異なる。

f:id:msyksphinz:20190405011237p:plain
North VirginaリージョンでSSHのキーペアを作成する。firesimキーペアを作成した。

EC2インスタンスの制限解除

North Virginaリージョンでも、デフォルトではF1インスタンスは使用できないようになっているため、まずはNorth VirginiaリージョンにおいてFireSimで使用するインスタンスの上限を解放する。 f1.2xlargeインスタンスは1つ、c4.4xlargeインスタンスは2つ使用できるように解放する。

この解放作業を行うためには、https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-resource-limits.htmlを参照する必要がある。 EC2マネジメントコンソールにログインし、[Limits]ページから該当するインスタンスの[Current Limit]の欄を確認する。 これが0ならば、[Request limit increase]をクリックし、制限を解除するためのリクエストを行う。この制限の解除作業には、1日程度かかる。

f:id:msyksphinz:20190405011503p:plain
F1インスタンスの制限解除

t2.nanoインスタンスを立ち上げ、コンフィグレーションコマンドを実行する

AWS外の、ローカルマシン上でAWSを制御するためのコマンドを用意しても良いのだが、設定が複雑なため、そのため、AWS上にに制御用のインスタンスを作ってそのうえで作業を行う。 このインスタンスはFireSimの設定を行うだけなので小さなインスタンスでよく、t2.nanoを使用する。

以下の作業は、すべて"North Virginia"リージョンで実行する必要がある。

EC2マネジメントコンソールから[Launch Instance]を実行する。AMIには、"Amazon Linux AMI..."を使用する。インスタンスのサイズとしたはt2.nanoを選択する。 細かな設定は必要ないので、[Review and Launch]をクリックし、最終確認ページで[Launch]をクリックする。

f:id:msyksphinz:20190405011543p:plain
AMIの選択。Amazon Linuxを使用する。
f:id:msyksphinz:20190405011615p:plain
t2.nanoインスタンス上で作業を行う。

Launchする際にキーペアを求められますので、先ほど作成したfiresimキーペアを指定し、起動する。

f:id:msyksphinz:20190405011643p:plain
キーペアの指定。firesimキーペアを指定する。

インスタンスが立ち上がると、t2.nanoインスタンスにログインします。このとき、先ほど用いたキーペアから生成した鍵を指定します。今回は、PuTTYからログインしました。ログイン名は"ec2-user"を指定すると、ログインできます。

f:id:msyksphinz:20190405011734p:plain
t2.nanoインスタンスにログインする。

ログインすると、awsのユーザアカウントの設定を行う。この設定を行うことで、Linuxのコンソール画面からAWSの様々なサービスを制御できるようになる。

$ aws configure

次に、Amazon Linux上で必要なパッケージをインストールする。

sudo yum -y install python-pip
sudo pip install boto3

次に、FireSimを構成するためのスクリプトを実行する。このスクリプトGitHub上に置かれているので、ダウンロードし、実行する。

wget https://raw.githubusercontent.com/firesim/firesim/master/scripts/aws-setup.py
python aws-setup.py

このスクリプトは何をしているかというと、AWSのユーザアカウント上にfiresim用のVPC(Virtual Private Cloud)を作成し、さらにfiresimという名前のセキュリティグループを作成しているらしい。

[ec2-user@ip-xxx ~]$ wget https://raw.githubusercontent.com/firesim/firesim/master/scripts/aws-setup.py
--2019-04-04 15:12:41--  https://raw.githubusercontent.com/firesim/firesim/master/scripts/aws-setup.py
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.248.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.248.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2800 (2.7K) [text/plain]
Saving to: ‘aws-setup.py’

100%[=========================================================================================================================================================================>] 2,800       --.-K/s   in 0s

2019-04-04 15:12:41 (77.3 MB/s) - ‘aws-setup.py’ saved [2800/2800]

[ec2-user@ip-xxx ~]$ python aws-setup.py
Creating VPC for FireSim...
Success!
Creating a subnet in the VPC for each availability zone...
Success!
Creating a security group for FireSim...
Success!
[ec2-user@ip-xxx ~]$

最後に、作業用に使用したt2.nanoインスタンスを削除する。もうこのインスタンスは使用しないので、完全に削除してしまって構わない。

オリジナルLLVM Backendを追加しよう (25. 可変引数・動的スタック割り当て)

https://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20181123/20181123225150.png

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

jonathan2251.github.io

第9章の後半では、可変長引数と、動的スタックの割り当てを実装する。

可変長引数のサポート

可変長引数は、例えば以下のようなコードをどのようにコンパイルするか、という問題だ。

#include <stdarg.h>

int sum_i(int amount, ...)
{
  int i = 0;
  int val = 0;
  int sum = 0;

  va_list vl;
  va_start(vl, amount);
  for (i = 0; i < amount; i++)
  {
    val = va_arg(vl, int);
    sum += val;
  }
  va_end(vl);

  return sum;
}

int test_vararg()
{
  int a = sum_i(6, 0, 1, 2, 3, 4, 5);

  return a;
}

この場合、まず行うことは引数からamountを取得し、引数の全体サイズを取得し、引数の各値をメモリからロードし、加算を行うという処理になる。 引数の数は6つのなので、スタックに6つの値が積み上げられ、関数に飛ぶ。

// int test_vararg() 内
  addiu $2, $zero, 5
  st  $2, 24($sp)
  addiu $2, $zero, 4
  st  $2, 20($sp)
  addiu $2, $zero, 3
  st  $2, 16($sp)
  addiu $2, $zero, 2
  st  $2, 12($sp)
  addiu $2, $zero, 1
  st  $2, 8($sp)
  addiu $2, $zero, 0
  st  $2, 4($sp)
  addiu $2, $zero, 6
  st  $2, 0($sp)
  • `lib/Target/MYRISCVX/Cpu0ISelLowering.cpp
SDValue MYRISCVXTargetLowering::lowerVASTART(SDValue Op, SelectionDAG &DAG) const {
...
  // vastart just stores the address of the VarArgsFrameIndex slot into the
  // memory location argument.
  const Value *SV = cast<SrcValueSDNode>(Op.getOperand(2))->getValue();
  return DAG.getStore(Op.getOperand(0), DL, FI, Op.getOperand(1),
                      MachinePointerInfo(SV));
}
//@LowerFormalArguments {
/// LowerFormalArguments - transform physical registers into virtual registers
/// and generate load operations for arguments places on the stack.
SDValue
MYRISCVXTargetLowering::LowerFormalArguments(SDValue Chain,
...
  if (IsVarArg)
    writeVarArgRegs(OutChains, MYRISCVXCCInfo, Chain, DL, DAG);
...
void MYRISCVXTargetLowering::writeVarArgRegs(std::vector<SDValue> &OutChains,
                                         const MYRISCVXCC &CC, SDValue Chain,
...
  // Copy the integer registers that have not been used for argument passing
  // to the argument register save area. For O32, the save area is allocated
  // in the caller's stack frame, while for N32/64, it is allocated in the
  // callee's stack frame.
  for (unsigned I = Idx; I < NumRegs; ++I, VaArgOffset += RegSize) {
    unsigned Reg = addLiveIn(MF, ArgRegs[I], RC);
    SDValue ArgValue = DAG.getCopyFromReg(Chain, DL, Reg, RegTy);
    FI = MFI.CreateFixedObject(RegSize, VaArgOffset, true);
    SDValue PtrOff = DAG.getFrameIndex(FI, getPointerTy(DAG.getDataLayout()));
    SDValue Store = DAG.getStore(Chain, DL, ArgValue, PtrOff,
                                 MachinePointerInfo());
    cast<StoreSDNode>(Store.getNode())->getMemOperand()->setValue(
        (Value *)nullptr);
    OutChains.push_back(Store);
  }

動的スタック割り当てのサポート

C言語ではほとんど使わないらしい。allocaという関数は初めて知った。スタック上にメモリを動的に取る関数らしい。

  • ch9_3_alloc.cpp
int weight_sum(int x1, int x2, int x3, int x4, int x5, int x6)
{
//  int *b = (int*)alloca(sizeof(int) * 1 * x1);
  int* b = (int*)__builtin_alloca(sizeof(int) * 1 * x1);
  int *a = b;
  *b = x3;

  int weight = sum(3*x1, x2, x3, x4, 2*x5, x6);

  return (weight + (*a));
}

これをclangでIRとして出力すると、以下のようになっていた。なるほど、allocというIRが呼ばれるのか。

; Function Attrs: noinline nounwind optnone
define dso_local i32 @_Z10weight_sumiiiiii(i32 signext %x1, i32 signext %x2, i32 signext %x3, i32 signext %x4, i32 signext %x5, i32 signext %x6) #0 {
entry:
  %x1.addr = alloca i32, align 4
  %x2.addr = alloca i32, align 4
  %x3.addr = alloca i32, align 4
  %x4.addr = alloca i32, align 4
...

基本的には、adjustStackPtrを使ってこれらのスタックを処理する関数を生成するようだ。 上記のC言語では、コンパイル時にallocで確保されるスタックのサイズを特定することができない。 そこで、スタックのサイズを示しているx1のサイズだけスタックポインタを移動し、その領域を確保するようにしている。 その際にallocが呼ばれる前のspの場所をfpに保存しておき、関数から戻るときは、spの値をfpから書き戻す。

// Eliminate ADJCALLSTACKDOWN, ADJCALLSTACKUP pseudo instructions
MachineBasicBlock::iterator MYRISCVXFrameLowering::
eliminateCallFramePseudoInstr(MachineFunction &MF, MachineBasicBlock &MBB,
                              MachineBasicBlock::iterator I) const {
  unsigned SP = MYRISCVX::SP;
  if (!hasReservedCallFrame(MF)) {
    int64_t Amount = I->getOperand(0).getImm();
    if (I->getOpcode() == MYRISCVX::ADJCALLSTACKDOWN)
      Amount = -Amount;

    // const MYRISCVXSEInstrInfo &TII =
    //     *static_cast<const MYRISCVXSEInstrInfo*>(STI.getInstrInfo());

    STI.getInstrInfo()->adjustStackPtr(SP, Amount, MBB, I);
  }

  return MBB.erase(I);
f:id:msyksphinz:20190403234557p:plain

もう一つの方法は、allocよりも上の領域へのアクセスはfpを使い、allocよりも下の領域はspを使ってアクセスする、というものらしい。 この方式のほうが、spだけを使うよりも処理を高速化することができる、ということだ。

f:id:msyksphinz:20190403234809p:plain

オリジナルLLVM Backendを追加しよう (24. Tail call optimizationの実装)

https://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20181123/20181123225150.png

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

jonathan2251.github.io

関数コールの最適化の一つとして、Tail call optimization(末尾再帰呼び出し)を実装してみる。

ja.wikipedia.org

LLVMにはTail CallというIRが用意されており、これで末尾再帰呼び出しを表現して、最適化に使用することができるようだ。

サンプルプログラム :

int factorial(int x)
{
  if (x > 0)
    return x*factorial(x-1);
  else
    return 1;
}

int test_tailcall(int a)
{
  return factorial(a);
}

これをコンパイルする。まずは-O0オプションから。

./bin/clang -O0 -c -target mips-unknown-linux-gnu ../lbdex/input/ch9_2_tailcall.cpp -emit-llvm
./bin/llvm-dis ch9_2_tailcall.bc -o -
define dso_local i32 @_Z9factoriali(i32 signext %x) #0 {
entry:
  %retval = alloca i32, align 4
  %x.addr = alloca i32, align 4
  store i32 %x, i32* %x.addr, align 4
  %0 = load i32, i32* %x.addr, align 4
  %cmp = icmp sgt i32 %0, 0
  br i1 %cmp, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  %1 = load i32, i32* %x.addr, align 4
  %2 = load i32, i32* %x.addr, align 4
  %sub = sub nsw i32 %2, 1
  %call = call i32 @_Z9factoriali(i32 signext %sub)
  %mul = mul nsw i32 %1, %call
  store i32 %mul, i32* %retval, align 4
  br label %return

if.else:                                          ; preds = %entry
  store i32 1, i32* %retval, align 4
  br label %return

return:                                           ; preds = %if.else, %if.then
  %3 = load i32, i32* %retval, align 4
  ret i32 %3
}

; Function Attrs: noinline optnone
define dso_local i32 @_Z13test_tailcalli(i32 signext %a) #0 {
entry:
  %a.addr = alloca i32, align 4
  store i32 %a, i32* %a.addr, align 4
  %0 = load i32, i32* %a.addr, align 4
  %call = call i32 @_Z9factoriali(i32 signext %0)
  ret i32 %call
}

普通の関数呼び出しが実行されている。これを、最適化していくとどうなるのか。

./bin/clang -O1 -c -target mips-unknown-linux-gnu ../lbdex/input/ch9_2_tailcall.cpp -emit-llvm
./bin/llvm-dis ch9_2_tailcall.bc -o -
; Function Attrs: nounwind readnone
define dso_local i32 @_Z9factoriali(i32 signext %x) local_unnamed_addr #0 {
entry:
  %cmp3 = icmp sgt i32 %x, 0
  br i1 %cmp3, label %if.then, label %return

if.then:                                          ; preds = %entry, %if.then
  %x.tr5 = phi i32 [ %sub, %if.then ], [ %x, %entry ]
  %accumulator.tr4 = phi i32 [ %mul, %if.then ], [ 1, %entry ]
  %sub = add nsw i32 %x.tr5, -1
  %mul = mul nsw i32 %x.tr5, %accumulator.tr4
  %cmp = icmp sgt i32 %x.tr5, 1
  br i1 %cmp, label %if.then, label %return

return:                                           ; preds = %if.then, %entry
  %accumulator.tr.lcssa = phi i32 [ 1, %entry ], [ %mul, %if.then ]
  ret i32 %accumulator.tr.lcssa
}

; Function Attrs: nounwind readnone
define dso_local i32 @_Z13test_tailcalli(i32 signext %a) local_unnamed_addr #0 {
entry:
  %call = tail call i32 @_Z9factoriali(i32 signext %a)
  ret i32 %call
}

おや、test_tailcallが末尾再帰として認識された。

これを-enable-myriscvx-tail-callsを付加してアセンブリを出力するとどうなるのか。

_Z13test_tailcalli:
# %bb.0:                                # %entry
    lui x10, %hi(_gp_disp)
    addi    x10, x10, %lo(_gp_disp)
    addi    x2, x2, -16
    .cfi_def_cfa_offset 16
    sw  x10, 12(x2)
    lw  x10, 12(x2)
    lw  x3, %call16(_Z9factoriali)(x3)
    jalr    x3
    addi    x2, x2, 16
    jalr    x1
_Z13test_tailcalli:
# %bb.0:                                # %entry
    lui x10, %hi(_gp_disp)
    addi    x10, x10, %lo(_gp_disp)
    lw  x3, %call16(_Z9factoriali)(x3)
    jalr    x3

確かに、スタックの操作がなくなってそのままfactorialを呼び出しているようだ。

オリジナルLLVM Backendを追加しよう (23. llvm-projectを使ってテスト環境を構築する)

https://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20181123/20181123225150.png

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

jonathan2251.github.io

関数コールもかなり動くようになってきて、これまでに多くのテストパタンを確認してきたのだが、リグレッションテストを作っておく必要があるように感じている。 とりあえず、マニュアルにあったllvm-projectの環境とテスト環境を使用して、リファレンスのテスト環境が正しく動作することを確認しよう。 LLVM Release 7.0をターゲットに環境を構築する。

github.com

まずは、通常通りllvmのビルドを行う。この時、compiler-rtのプロジェクトもビルドするように設定しておく。compiler-rtはランタイムライブラリやパフォーマンス解析ツールのパッケージらしい。

git clone https://github.com/llvm/llvm-project.git --depth 1 -b release/7.x
cd llvm-project
mkdir build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE="Debug" -DLLVM_TARGETS_TO_BUILD="X86;Mips" -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RISCV" -DCMAKE_ENABLE_PROJECTS="clang;compiler-rt" ../llvm
cmake --build . -- -j8

次に、テストスイートをダウンロードしてビルドしてみる。

git clone https://github.com/llvm/llvm-test-suite.git test-suite -b release/7.x
cmake -DCMAKE_C_COMPILER=${HOME}/llvm-project/build/bin/clang -DCMAKE_CXX_COMPILER=${HOME}/llvm-project/build/bin/clang++ -C../test-suite/cmake/caches/Debug.cmake ../test-suite
cmake --build .

ビルドが完了すると、テストを実行してみる。

${HOME}/llvm-project/build/bin/llvm-lit -v -j 8 -o results.json .

909個のテストが実行された。全体としては数10分程度かかった。ただし、テストは上手くいったようだ。

size..note.ABI-tag: 32
size..plt: 32
size..rodata: 37
size..text: 1825
**********
PASS: test-suite :: SingleSource/UnitTests/Vectorizer/gcc-loops.test (909 of 909)
********** TEST 'test-suite :: SingleSource/UnitTests/Vectorizer/gcc-loops.test' RESULTS **********
compile_time: 13.9219
exec_time: 43.2344
hash: "65ff90ec20c719ac27991a4a195a56ad"
link_time: 0.2344
size: 95600
size..bss: 229680
size..comment: 56
size..data: 16
size..debug_abbrev: 1420
size..debug_info: 21411
size..debug_line: 6607
size..debug_macinfo: 1
size..debug_pubnames: 5024
size..debug_pubtypes: 2576
size..debug_ranges: 2064
size..debug_str: 14964
size..dynamic: 512
size..dynsym: 576
size..eh_frame: 3336
size..eh_frame_hdr: 812
size..fini: 9
size..fini_array: 8
size..gcc_except_table: 352
size..gnu.hash: 48
size..gnu.version: 48
size..gnu.version_r: 112
size..got: 16
size..got.plt: 184
size..init: 23
size..init_array: 16
size..interp: 28
size..note.ABI-tag: 32
size..plt: 336
size..rodata: 250
size..text: 12561
**********
Testing Time: 807.45s
********************
Failing Tests (1):
    test-suite :: MultiSource/Applications/ClamAV/clamscan.test

  Expected Passes    : 908
  Unexpected Failures: 1

aws-plugin-for-slurmを使ってAWS上でクラスターを作りたい

https://d2908q01vomqb2.cloudfront.net/1b6453892473a467d07372d45eb05abc2031647a/2018/10/04/ref-arch.jpg

速いコンピュータが欲しいんです...

AWSを使用すると一時的にでも高速なコンピュータでジョブを動かすことができるが、高性能なマシンはお値段も高いので、作業中に常時立ち上げておくのはもったいない。 そこで、普段の作業の時はもっと弱いサーバ(もしくはローカル環境)で作業して、ビルドやシミュレーションなどの速度が要求されるような環境ではオンデマンドど高速なサーバを立ち上げたい。 この方法を色々調べていたのだが、slurmというジョブ管理ツールを使ってAWS上でクラスタを構成する方法があるらしいということを知った。 これを使うと、ジョブを高性能クラスタに投げる、ということが可能だろうか?

github.com

結構な時間を掛けて格闘していたのだが、なんとなくやり方が分かってきた。

slurmというのは、ヘッドノードからクラスタのノードに対してジョブを投入するためのツールだ。これをAWSに組み込めるかどうかを見ていく。

最初に、使用するOSとパッケージを指定する。ここでは CentOS 7 (x86_64) - with Updates HVM を使用する(というか、推奨されている)。

aws.amazon.com

このCentOSのパッケージをSubscribeする。そうしないとCloud FoundationでこのOSをデプロイすることができない。

まず、ローカル(つまりAWSのサーバではなく)のPCに上記のgithubリポジトリ、そしてslurmのソースコードをダウンロードしておく。

mkdir aws
cd aws
git clone https://github.com/aws-samples/aws-plugin-for-slurm.git
cd aws-plugin-for-slurm
wget https://download.schedmd.com/slurm/slurm-17.11.8.tar.bz2

これをAWS S3のバケットにクローンしておくらしい。S3のバケットは、全ユーザでユニークでなければならないらしい。s3://slurm-common-[ユニークな文字列]とした。 aws-plugin-for-slurm/slurm_headnode_cloudformation.ymlをいくつか編集しないといけないようだ。

s3のバケット名の設定と、デフォルトで使用するAMIを指定する。ami-045f38c93733dd48d(CentOS 7 (x86_64) - with Updates HVM) を指定した。

Parameters:
  S3Bucket:
    Description: Basic SLURM Cluster S3
    Default: s3://slurm-common-[ユニークな文字列]
    Type: String
...
  BaseAMI:
    Description: AMI to use for ephermal compute nodes. Only CentOS/RHEL is supported
    Type: String
    Default: ami-045f38c93733dd48d
...

そして、リポジトリとslurmのパッケージをs3にコピーする。

cd aws/aws-plugin-for-slurm
aws s3 sync . s3://slurm-common-[ユニークな文字列]

次に、編集したaws-plugin-for-slurm/slurm_headnode_cloudformation.ymlをCouldFoundationで読み込み、クラスタを構成する。

https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks

[Create Stack]をクリックして、編集した slurm_headnode_cloudformation.yml をテンプレートとして読み込ませる。これで、Slurmを使用したクラスタのテンプレートが読み込まれる。

f:id:msyksphinz:20190330212108p:plain

これでNextを押し続けると、最終的な確認とともにクラスタ構成が作成される。

クラスタ構成の作成が始まる。しばらくしたら、StatusがCREATE_COMPLETEに変わる(これには10分程度必要となる)。

f:id:msyksphinz:20190330212327p:plain
slurm-clusterの構成完了画面

リソースの一覧を見てSlurmManagementEC2がCOMPLETEになるの時間がかかるようだ。たしかに、YAMLの設定ではこの設定が一番長い。

  SlurmManagementEC2:
    DependsOn: PublicSubnet1
    Type: AWS::EC2::Instance
    Properties:
...
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          sudo sed -i "s|enforcing|disabled|g" /etc/selinux/config
          sudo setenforce 0
          sudo yum update -y
          sudo yum --nogpgcheck install wget curl epel-release nano nfs-utils -y
...
          sudo sed -i "s|@BASEAMI@|${BaseAMI}|g" /home/centos/slurm-aws-startup.sh
          sudo sed -i "s|@SLURMROLE@|${SlurmCInstanceProfile}|g" /home/centos/slurm-aws-startup.sh
          sudo sed -i "s|@PRIVATE1@|${PrivateSubnet1}|g" /home/centos/slurm-aws-startup.sh
          sudo sed -i "s|@PRIVATE2@|${PrivateSubnet2}|g" /home/centos/slurm-aws-startup.sh
          sudo sed -i "s|@PRIVATE3@|${PrivateSubnet3}|g" /home/centos/slurm-aws-startup.sh
          aws s3 cp ${S3Bucket}/slurm-aws-shutdown.sh /home/centos/slurm-aws-shutdown.sh
          chmod +x /home/centos/slurm-mgmtd.sh
          sudo /home/centos/slurm-mgmtd.sh ${SlurmVersion} ${PrivateSubnet1.AvailabilityZone},${PrivateSubnet2.AvailabilityZone},${PrivateSubnet3.AvailabilityZone} ${SLURMComputeIPRange}
          cfn-signal -e $? --stack ${AWS::StackName} --resource SlurmManagementEC2WaitCondition --region ${AWS::Region}

EC2の構成を見ると、新たなslurm-headnodeのEC2インスタンスが作られている。

f:id:msyksphinz:20190330213610p:plain

ログインすると、Slurmのコマンド群が使えるようになっているのが確認できる。

[centos@ip-10-0-0-212 ~]$ sinfo
PARTITION AVAIL  TIMELIMIT  NODES  STATE NODELIST
all*         up   infinite      0    n/a
gpu          up   infinite      0    n/a
[centos@ip-10-0-0-212 ~]$ squeue
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)

次にsbatchのテストをしてみる。以下のようなバッチファイルを作成して流してみる。

#!/bin/bash

#SBATCH --nodes=1
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=1

env
[centos@ip-10-0-0-212 ~]$ sbatch ./test.sbatch
Submitted batch job 14
[centos@ip-10-0-0-212 ~]$ sinfo
PARTITION AVAIL  TIMELIMIT  NODES  STATE NODELIST
all*         up   infinite      1   mix# ip-10-0-1-11
gpu          up   infinite      1   mix# ip-10-0-1-11
[centos@ip-10-0-0-212 ~]$ squeue
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
                14       all test.sba   centos CF       1:17      1 ip-10-0-1-11

EC2インスタンスの一覧を見ると、ジョブ用のサーバが立ち上がっていることが分かる。何とか通信はできているようだ。

f:id:msyksphinz:20190330214240p:plain

ただし、ジョブを投げようとするとタイムアウトしてしまう。これは少し解析が必要だ。

$ time srun env
srun: error: Node failure on ip-10-0-1-11
srun: Force Terminated job 15
srun: error: Job allocation 15 has been revoked

real    10m10.613s
user    0m0.014s
sys     0m0.035s

オリジナルLLVM Backendを追加しよう (23. 命令の定義からAsmPrinterへの変換プロセスまとめ)

https://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20181123/20181123225150.png

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

jonathan2251.github.io

LLVMには独自のIRを定義できる仕組みがあり、それを使って命令を変換したりして最終的に命令を生成する。 例えば、Return命令をどのようにして最終的にllcが命令として出力するのかについてまとめる。

例えば、MYRISCVXRetという関数から戻る命令のためのIRを定義する。 ISDというのは、SelectionDAGノードの型を示すものらしい。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// Return
def MYRISCVXRet : SDNode<"MYRISCVXISD::Ret", SDTNone,
                         [SDNPHasChain, SDNPOptInGlue, SDNPVariadic]>;

これはLLVMの実装からは、MYRISCVXISD::Retとして参照できるようになる。 DAGは、以下の定義によって使用される。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
let isReturn=1, isTerminator=1, hasDelaySlot=0, isBarrier=1, hasCtrlDep=1 in
  def RetRA : MYRISCVXPseudo<(outs), (ins), "", [(MYRISCVXRet)]>;

RetRAノードを定義する。isReturnとisTerminatorはリターン命令であり、BasicBlockの最後のノードである、ということを示していると思う。

このMYRSCVXISD::RetノードはC言語のreturn命令などで使用されるのだが、Instruction Selectionの段階でMYRISCVX::RetRAに変換される。これは同等なので変換しても良い。

ISEL: Starting selection on root node: t62: ch = MYRISCVXISD::Ret t61
Selecting: t62: ch = MYRISCVXISD::Ret t61
ISEL: Starting pattern match
  Morphed node: t62: ch = RetRA t61
ISEL: Match complete!

このMYRISCVX::RetRAは、以下のMYRISCVXSEISelLowering.cppの実装によってMYRISCVX::JALRに変換される。 これが最終的にjalr raという命令に変換されるわけだ。

llvm.org

This function is called for all pseudo instructions that remain after register allocation. Many pseudo instructions are created to help register allocation. This is the place to convert them into real instructions. The target can edit MI in place, or it can insert new instructions and erase MI. The function should return true if anything was changed.

//@expandPostRAPseudo
/// Expand Pseudo instructions into real backend instructions
bool MYRISCVXSEInstrInfo::expandPostRAPseudo(MachineInstr &MI) const {
  //@expandPostRAPseudo-body
  MachineBasicBlock &MBB = *MI.getParent();
  switch (MI.getDesc().getOpcode()) {
    default:
      return false;
    case MYRISCVX::RetRA:
      expandRetLR(MBB, MI);
      break;
  }
  MBB.erase(MI);
  return true;
}

void MYRISCVXSEInstrInfo::expandRetLR(MachineBasicBlock &MBB,
                                      MachineBasicBlock::iterator I) const {
  BuildMI(MBB, I, I->getDebugLoc(), get(MYRISCVX::JALR)).addReg(MYRISCVX::RA);
}

Cpu0の命令変換の説明では、以下のように書いてある。