FPGA開発日記

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

SonicBOOMのデザインを読み解く (フロントエンドからDispatcher)

SonicBOOMのデザインを読み解いている。少しクリティカルな部分について解析するために以下のようなプログラムを動かしてみよう。

000000008000322e <simple_add>:
    8000322e:   b0002573                csrr    a0,mcycle
    80003232:   301025f3                csrr    a1,misa
    80003236:   0205e593                ori     a1,a1,32
    8000323a:   30159073                csrw    misa,a1
    8000323e:   8082                    ret

CSR命令を大量に発行する。このときにどのようなデータパスで実行されるのだろうか。

まず、GigaBoomConfigの波形を解析すると、フロントエンドからバックエンドには5命令が同時に発行される。この時にすべてのCSR命令は同時に発行されており、ここではスケジューリングがなされていないことが見て取れる。しかし問題はこの後だ。おそらく先頭からIssueできる命令だけをIssueして、それ以外はReplayさせて再度命令を実行しているものと思われる。

f:id:msyksphinz:20210106012342p:plain

その証拠に、CSR命令発行用のIssueユニットは1命令ずつDispatchを受け入れておりずいぶんと長い時間をかけて実行していることが見て取れる。

f:id:msyksphinz:20210106012354p:plain

これ、Issueの条件はどのようになっているのだろうか。基本的にハザードが無ければすぐに発行しているようだ。これは完全にフロントエンド側がどれくらいパイプラインを停止するかに依存している気がする。フロントエンドのパイプライン停止条件についてもう少し細かく見ていく子はできないだろうか。

f:id:msyksphinz:20210106012404p:plain

SonicBOOMのデザインを読み解く (FetchBuffer)

引き続きSonicBOOMのデザインを読んでいる。SonicBOOMではf3にて簡単なデコードが完了すると、バックエンドに命令が渡される。その時のフロントエンドとバックエンドのインタフェースとしてFetchBuffer(fb)に格納される。FetchBufferからバックエンドに命令が発行されるわけだ。

f:id:msyksphinz:20210105005605p:plain

このときFetchBufferに何が格納されるかをチェックしているが、まずはマスクの情報だ。128ビットのフェッチサイズに対して4命令分のはずなのだが、実際には16ビット命令も存在しているので8ビット分のマスクが存在している。マスクは分岐命令(でかつ分岐予測に予測値が存在しない場合)または、JAL命令・JALR命令でDisableに設定される。

  • src/main/scala/ifu/frontend.scala
  for (b <- 0 until nBanks) {
    val bank_data  = f3_data((b+1)*bankWidth*16-1, b*bankWidth*16)
    val bank_mask  = Wire(Vec(bankWidth, Bool()))
    val bank_insts = Wire(Vec(bankWidth, UInt(32.W)))

...
      // Redirect if
      //  1) its a JAL/JALR (unconditional)
      //  2) the BPD believes this is a branch and says we should take it
      f3_redirects(i)    := f3_mask(i) && (
        brsigs.cfi_type === CFI_JAL || brsigs.cfi_type === CFI_JALR ||
        (brsigs.cfi_type === CFI_BR && f3_bpd_resp.io.deq.bits.preds(i).taken && useBPD.B)
      )
...
      redirect_found = redirect_found || f3_redirects(i)
    }
f:id:msyksphinz:20210105005422p:plain
f:id:msyksphinz:20210105005431p:plain

SonicBOOMのデザインを読み解く(TLBの動作確認)

SonicBOOMのデザインを読んでいる。次はTLBについて調査する。

TLBは仮想アドレスから物理アドレスを引いてくるための機構なのだが、テーブルサーチ部分は以下のような構成になっていた。

f:id:msyksphinz:20210103141135p:plain

1つの仮想アドレスに対して4-WAY同時に格納できる構成になっており、最終的にどれかがヒットすればHitsVecが1になる。そうでなければHitsVecは1にならずmissがアサートされる。このミスはリクエストと同じサイクルで通知される。

リクエスト側(つまりフロントエンドパイプライン側)はHitの応答があるまでひたすらリクエストを繰り返している。いいんだけど、これだと電力においてかなり影響がありそうな気がする。

f:id:msyksphinz:20210103141156p:plain

で、ここから先の動作が良く分からない。理想の状態を言うのであれば、以下のような遷移をするはずである。

<Info: CPU mode changed from MachineMode to UserMode>
      1201:M:Sv39:00000000800000c0:P0000800000c0:10200073:sret                            :mstatus=>8000000a00006000 mstatus<=8000000a00006020 sepc=>0000000000002ac8 pc<=0000000000002ac8
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080003000 : PPTE = 0x0000000020001001>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080004000 : PPTE = 0x0000000020001801>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080006010 : PPTE = 0x0000000000000000>
<Page Table Error : 0000000080006010 = 0000000000000000 is not valid Page Table. Generate Exception>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080003000 : PPTE = 0x0000000020001001>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080004000 : PPTE = 0x0000000020001801>
<Info: VAddr = 0x0000000000002ac8 PTEAddr = 0x0000000080006010 : PPTE = 0x0000000000000000>
<Page Table Error : 0000000080006010 = 0000000000000000 is not valid Page Table. Generate Exception>
<Info: GenerateException Code=12, TVAL=0000000000002ac8 PC=0000000000002ac8,00007ffffc9de760>
<Info: Exception. ChangeMode from UserMode to SuprevisorMode>
<Info: Set Program Counter = 0xffffffffffe000c4>
<Info: MemResult::MemTlbError occured.>
<Info: VAddr = 0xffffffffffe000c4 PTEAddr = 0x0000000080003ff8 : PPTE = 0x0000000020001401>
<Info: VAddr = 0xffffffffffe000c4 PTEAddr = 0x0000000080005ff8 : PPTE = 0x00000000200000cf>
<Info: TLB[0] <= 0x000ffffffffffe00(0x0000000080000000)>
<Info: Converted Virtual Address is = 0x00000000800000c4>
      1202:S:Sv39:ffffffffffe000c4:P0000800000c4:14011173:csrrw      x02,0x140,x02        :sscratch=>ffffffffffe086d0 x02=>0000000000000000 sscratch<=0000000000000000 x02<=ffffffffffe086d0
<Info: VAddr = 0xffffffffffe086d8 PTEAddr = 0x0000000080003ff8 : PPTE = 0x0000000020001401>
<Info: VAddr = 0xffffffffffe086d8 PTEAddr = 0x0000000080005ff8 : PPTE = 0x00000000200000cf>
<Info: TLB[8] <= 0x000ffffffffffe08(0x0000000080008000)>
<Info: Converted Virtual Address is = 0x00000000800086d8>
      1203:S:Sv39:ffffffffffe000c8:P0000800000c8:00113423:sd         x01,0x008(x02)       :x02=>ffffffffffe086d0 x01=>0000000000000000 (00000000800086d8)<=0000000000000000
      1204:S:Sv39:ffffffffffe000cc:P0000800000cc:00313c23:sd         x03,0x018(x02)       :x02=>ffffffffffe086d0 x03=>0000000000000000 (00000000800086e8)<=0000000000000000
      1205:S:Sv39:ffffffffffe000d0:P0000800000d0:02413023:sd         x04,0x020(x02)       :x02=>ffffffffffe086d0 x04=>0000000000000000 (00000000800086f0)<=0000000000000000
  1. PTEアドレスが80003000に設定されている(これはSATPが0x80003000に設定されていることと、VPN[3]=0であることから)ので、まずは0x80003000にアクセスする。
  2. 0x80003000がPTEのLeafテーブルではないので、次に0x80004000にアクセスする。
  3. 最後に8x0006010にアクセスする。この時点で有効なテーブルではないので再度例外が発生する。

信号の動きを見ると、まずはTLBからPTWユニットに対して0x00002が出力されている。これはおそらくオフセットを除く0x2ac8の2が出力されたのであろう。

f:id:msyksphinz:20210103141212p:plain

SATPに基づき最初の物理アドレスを計算し、それをPTWがリクエストを出す。そしてレスポンスをベースに次のアドレスを計算することで最終的な物理アドレスを計算する。

f:id:msyksphinz:20210103141223p:plain

最終的に物理アドレスには0しか格納されておらずこれはページテーブルエラーになるので、io_resp_pf_inst (PageFault)が返される。

f:id:msyksphinz:20210103141251p:plain

SonicBOOMのデザインを読み解く(フロントエンド)

RISC-VのOoOコアSonicBOOMのデザインを読み解いている。フロントエンドのパイプライン構成について読み解いていきたい。

SonicBOOMのフェッチ部については大まかに言って3ステージに分かれていると言って良い。

  1. s0:プログラムカウンタから仮想アドレスを命令キャッシュに送り込む。命令キャッシュではそのプログラムカウンタでタグを検索する。その結果はs1ステージで返される。
    • 後述するがどうもこのタグは物理アドレスを保持するタグとなっており、s1ステージで物理アドレスのタグとTLBのタグを比較することになる。
  2. s1:仮想アドレスをTLBに流して検索する。TLBはその仮想プログラムアドレスから物理アドレスを検索する。物理アドレスがヒットすればそのステージのうちに命令キャッシュに渡す。
    • 物理アドレスがヒットすればs1ステージの内に命令キャッシュに送り込み、s1ステージで比較を行い命令キャッシュの物理タグと一致するかを確認する。
  3. s2s1ステージにより物理タグがヒットして物理アドレスが確定すれば、そのまま物理アドレスを引いて命令キャッシュを読む。その結果はs2ステージで読みだされる。

ICacheにTileLinkのマスターになりうるDiplomacyノードが生成されており、もしもヒットしなければそのままTileLinkで外部にリクエストが送出される。つまりs2ステージでTileLinkが駆動される。

f:id:msyksphinz:20210102000952p:plain

一方でTLBがミスした場合はどうなるのか。これはDiplomacyを経由して直接渡されるのではなく、一度特殊なBundleを通じてデータキャッシュに対してアービトレーションを出す。これによりTLBへのアクセスはいったんデータキャッシュに格納されることになり、変換テーブル自体もデータキャッシュに置かれることになる。

TLBの概要

TLBは仮想アドレスから物理アドレスに変換する処理をバッファリングしておくためのもので、当該仮想アドレスをバッファ中に保持して入ればそれを活用し、そうでなければ外部にリクエストを出して取得する。TLBエントリの構成は以下のようになっている。

  • TAGビット:vpnのビットを格納している。
  • Validビット:データビットに有効な値が入っているかどうかを示している。
  • Dataビット:データビット。内訳はPTEの内容と同じ。
  • Levelビット:変換のレベルを示している。
f:id:msyksphinz:20210102001013p:plain

変換の方法については今後しっかり調べていくが、基本的に物理アドレスモード(VM=0)の時はそのままアドレスを返す。そうでないときはTLBのエントリーテーブルを検索している。

f:id:msyksphinz:20210102001036p:plain

SonicBOOMの性能測定 (Coremark)

SonicBOOMの基本的な性能を見るために、次はCoremarkを実行してみることにした。SonicBOOMはパラメータに応じて以下の構成を取ることができる。

f:id:msyksphinz:20201230234012p:plain

Coremarkのソースコードコンパイルについては、以下のリポジトリを活用することにした。デフォルトではCoremarkはRISC-V向けのコンパイル環境などは(もちろん)入っていないが、以下のリポジトリを使えば簡単にビルドできる。

github.com

diff --git a/build-coremark.sh b/build-coremark.sh
index 2449d2d..b026e5c 100755
--- a/build-coremark.sh
+++ b/build-coremark.sh
@@ -9,8 +9,8 @@ cd $BASEDIR/$CM_FOLDER

 # run the compile
 echo "Start compilation"
-make PORT_DIR=../riscv64 compile
+make PORT_DIR=../riscv64 compile ITERATIONS=10
 mv coremark.riscv ../

-make PORT_DIR=../riscv64-baremetal compile
+make PORT_DIR=../riscv64-baremetal compile ITERATIONS=10
 mv coremark.bare.riscv ../

バイナリの実行は以下のようにした。

./simulator-chipyard-SmallBoomConfig  --verbose ../../../riscv-coremark/coremark.bare.riscv 2>&1 | tee coremark.small.log  | grep -v " 3 "
./simulator-chipyard-MediumBoomConfig --verbose ../../../riscv-coremark/coremark.bare.riscv 2>&1 | tee coremark.medium.log | grep -v " 3 "
./simulator-chipyard-LargeBoomConfig  --verbose ../../../riscv-coremark/coremark.bare.riscv 2>&1 | tee coremark.large.log  | grep -v " 3 "
./simulator-chipyard-MegaBoomConfig   --verbose ../../../riscv-coremark/coremark.bare.riscv 2>&1 | tee coremark.mega.log   | grep -v " 3 "
./simulator-chipyard-GigaBoomConfig   --verbose ../../../riscv-coremark/coremark.bare.riscv 2>&1 | tee coremark.large.log  | grep -v " 3 "

実行結果は以下のようになっている。順当にCMK/MHzが向上した。

Coremark Score CMK/MHz
Small 4156452 2.41
Medium 2562284 3.90
Large 1998827 5.00
Mega 1852369 5.40
Giga 1799869 5.56
f:id:msyksphinz:20201231181222p:plain

SonicBOOMのデザインを読み解く(算術演算のデータパスの生成方法)

RISC-VのOoOコアであるSonicBOOMのデザインを勉強している。前回に続いて、以下のテストパタンを用いてALUのデータパスがどのようにして選択されているのかを観察している。

    .section    .text
    .global     simple_add
simple_add:
    addi    x10, x0,  1
    addi    x11, x10, 2
    addi    x12, x11, 3
    addi    x13, x12, 4
    addi    x14, x13, 5
    addi    x15, x14, 6
    addi    x16, x15, 7
    addi    x17, x16, 8
    addi    x18, x17, 9

    mv      x10, x18
    ret

データパスの構成方法について、BOOMでは以下のような記述で演算器を構築することができるようになっている。

  • src/main/scala/exu/core.scala
  // Only holds integer-registerfile execution units.
  val exe_units = new boom.exu.ExecutionUnits(fpu=false)
  • src/main/scala/exu/execution-units/execution-units.scala
/**
 * Top level class to wrap all execution units together into a "collection"
 *
 * @param fpu using a FPU?
 */
class ExecutionUnits(val fpu: Boolean)(implicit val p: Parameters) extends HasBoomCoreParameters
{
...
  if (!fpu) {
    val int_width = issueParams.find(_.iqType == IQT_INT.litValue).get.issueWidth

    for (w <- 0 until memWidth) {
      val memExeUnit = Module(new ALUExeUnit(
        hasAlu = false,
        hasMem = true))

      memExeUnit.io.ll_iresp.ready := DontCare

      exe_units += memExeUnit
    }

    for (w <- 0 until int_width) {
      def is_nth(n: Int): Boolean = w == ((n) % int_width)
      val alu_exe_unit = Module(new ALUExeUnit(
        hasJmpUnit     = is_nth(0),
        hasCSR         = is_nth(1),
        hasRocc        = is_nth(1) && usingRoCC,
        hasMul         = is_nth(2),
        hasDiv         = is_nth(3),
        hasIfpu        = is_nth(4) && usingFPU))
      exe_units += alu_exe_unit
    }
  } else {
    val fp_width = issueParams.find(_.iqType == IQT_FP.litValue).get.issueWidth
    for (w <- 0 until fp_width) {
      val fpu_exe_unit = Module(new FPUExeUnit(hasFpu = true,
                                             hasFdiv = usingFDivSqrt && (w==0),
                                             hasFpiu = (w==0)))
      exe_units += fpu_exe_unit
    }
  }
...

一見すると意味不明だが、最初のforループでメモリアドレス計算のためのALUを生成し、次のループで整数演算用のALUユニットを生成している。ALUユニットの生成においてis_nth()というのを使っているが、これはつまりn個のALUを生成するときに、

  • 0 % n番目のALUはJmpUnit用
  • 1 % n番目のALUはCSR
  • 1 % n番目のALUはRoCC用
  • 2 % n番目のALUはMultiplier用
  • 3 % n番目のALUはDivider用
  • 4 % n番目のALUはFPUの整数側用

という割り付けになっている(記事を書きながら思ったが、つまりこれは4番目以降の演算器を生成してもデフォルト(通常算術演算)用以外のALUを生成することは出来ないということか)。

f:id:msyksphinz:20201230234534p:plain

このALUに入力するためのデータ入力だが、基本的にはiregfileからの入力となっているがそれ以外にBypassからの入力も受け付けている。これらの入力オペランドの選択はiregister_readユニットによって行われており、ここでBypassデータの選択が行われている。

SonicBOOMの性能測定 (Dhrystoneの性能測定)

SonicBOOMの基本的な性能を見るために、Dhrystoneを実行してみることにした。SonicBOOMはパラメータに応じて以下の構成を取ることができる。

f:id:msyksphinz:20201230234012p:plain

これらの構成の違いは、BOOMのconfix-mixins.scalaに定義されている。GigaBoomConfigは以下のように定義されている。

  • generators/chipyard/src/main/scala/config/BoomConfigs.scala
class GigaBoomConfig extends Config(
  new chipyard.iobinders.WithUARTAdapter ++
  new chipyard.iobinders.WithTieOffInterrupts ++
  new chipyard.iobinders.WithBlackBoxSimMem ++
  new chipyard.iobinders.WithTiedOffDebug ++
  new chipyard.iobinders.WithSimSerial ++
  new testchipip.WithTSI ++
  new chipyard.config.WithBootROM ++
  new chipyard.config.WithUART ++
  new chipyard.config.WithL2TLBs(1024) ++
  new freechips.rocketchip.subsystem.WithNoMMIOPort ++
  new freechips.rocketchip.subsystem.WithNoSlavePort ++
  new freechips.rocketchip.subsystem.WithInclusiveCache ++
  new freechips.rocketchip.subsystem.WithNExtTopInterrupts(0) ++
  new boom.common.WithGigaBooms ++                              // giga boom config
  new boom.common.WithNBoomCores(1) ++
  new freechips.rocketchip.subsystem.WithCoherentBusTopology ++
  new freechips.rocketchip.system.BaseConfig)

このうちのWithGigaBoomsにこれらのパラメータが定義されている。

  • src/main/scala/common/config-mixins.scala
class WithGigaBooms extends Config((site, here, up) => {
  case BoomTilesKey => up(BoomTilesKey, site) map { b => b.copy(
    core = b.core.copy(
      fetchWidth = 8,
      decodeWidth = 5,
      numRobEntries = 130,
      issueParams = Seq(
        IssueParams(issueWidth=2, numEntries=24, iqType=IQT_MEM.litValue, dispatchWidth=5),
        IssueParams(issueWidth=5, numEntries=40, iqType=IQT_INT.litValue, dispatchWidth=5),
        IssueParams(issueWidth=2, numEntries=32, iqType=IQT_FP.litValue , dispatchWidth=5)),
      numIntPhysRegisters = 128,
      numFpPhysRegisters = 128,
      numLdqEntries = 32,
      numStqEntries = 32,
      maxBrCount = 20,
      numFetchBufferEntries = 40,
      enablePrefetching=true,
      numDCacheBanks=1, // Duplicate the DCache. For Science
      ftq = FtqParameters(nEntries=40),
      fpu = Some(freechips.rocketchip.tile.FPUParams(sfmaLatency=4, dfmaLatency=4, divSqrt=true))),
    dcache = Some(DCacheParams(rowBits = site(SystemBusKey).beatBytes*8,
                               nSets=64, nWays=8, nMSHRs=8, nTLBEntries=32)),
    icache = Some(ICacheParams(fetchBytes = 4*4, rowBits = site(SystemBusKey).beatBytes*8, nSets=64, nWays=8, prefetch=true))
  )}
  case SystemBusKey => up(SystemBusKey, site).copy(beatBytes = 16)
  case XLen => 64
  case MaxHartIdBits => log2Up(site(BoomTilesKey).size)
})

これらの構成においてそれぞれDhrystoneを走らせた結果が以下のようになった。演算器やそれぞれのリソースの量を増やすとそれなりにサイクル数が減少していることが見て取れる。

Configuration Cycles
SmallBoomConfig 211123
MediumBoomConfig 116078
LargeBoomConfig 88391
MegaBoomConfig 74496
GigaBoomConfig 68039
f:id:msyksphinz:20201230234112p:plain

これに調子に乗って、より大きなコンフィグレーションを作成してTeraBoomConfigを定義してみたが、Verilog生成時にメモリが足りずに落ちてしまった。なんてこった...

class WithTeraBooms extends Config((site, here, up) => {
  case BoomTilesKey => up(BoomTilesKey, site) map { b => b.copy(
    core = b.core.copy(
      fetchWidth = 8,
      decodeWidth = 8,
      numRobEntries = 208,
      issueParams = Seq(
        IssueParams(issueWidth=2, numEntries=24, iqType=IQT_MEM.litValue, dispatchWidth=8),
        IssueParams(issueWidth=8, numEntries=40, iqType=IQT_INT.litValue, dispatchWidth=8),
        IssueParams(issueWidth=4, numEntries=32, iqType=IQT_FP.litValue , dispatchWidth=8)),
      numIntPhysRegisters = 256,
      numFpPhysRegisters = 256,
      numLdqEntries = 48,
      numStqEntries = 48,
      maxBrCount = 20,
      numFetchBufferEntries = 56,
      enablePrefetching=true,
      numDCacheBanks=1, // Duplicate the DCache. For Science
      ftq = FtqParameters(nEntries=40),
      fpu = Some(freechips.rocketchip.tile.FPUParams(sfmaLatency=4, dfmaLatency=4, divSqrt=true))),
    dcache = Some(DCacheParams(rowBits = site(SystemBusKey).beatBytes*8,
      nSets=64, nWays=8, nMSHRs=8, nTLBEntries=32)),
    icache = Some(ICacheParams(fetchBytes = 4*4, rowBits = site(SystemBusKey).beatBytes*8, nSets=64, nWays=8, prefetch=true))
  )}
  case SystemBusKey => up(SystemBusKey, site).copy(beatBytes = 16)
  case XLen => 64
  case MaxHartIdBits => log2Up(site(BoomTilesKey).size)
})