FPGA開発日記

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

FPGA開発日記 カテゴリ別インデックス

RISC-VにおけるRVWMOの仕様について読み直す

続きを読む

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (4. メモリアクセスユニットについて)

NaxRiscvのドキュメントを、改めて位置から読んでみることにしたいと思う。

spinalhdl.github.io

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com


分岐予測

現在、分岐予測はパイプライン内の二か所で実装されている:

  • フェッチ予測 :BTBとGShare予測器で構成されている
  • デコード予測 :RAS予測を使用することができ、BTB予測がない場合は静的ジャンプとgshare分岐予測を行うことができる

以下は分岐予測パイプラインの図示である:

フェッチ予測

フェッチステージでは、BTBは与えられたフェッチワードについていくつかの予測を行う。 - ターゲットPC - 命令の性質(分岐かどうか) - 命令の最後の部分がワードのどのスライスであるか(スライスはRVCなしで4バイト、RVCありで2バイト)

この予測を使用し、最終的にはGShare予測を使用して、予測を適用する。

デコード予測

命令がフェッチされると、その段階での予測を改善するために使用できる情報がかなり多く得られる。

フェッチBTB予測がジャンプ/分岐を行わなかった場合、デコード予測は以下のいずれかを行う可能性がある。

  • 何も行わない
  • 静的ジャンプ(call,j)を適用する
  • GShare予測があった場合、その分岐を適用する
  • 命令がリターン(ret)の場合、RAS予測を適用する

フェッチBTBがジャンプ/分岐を行った場合、デコード予測は以下を行う可能性がある。

  • 命令がジャンプ/分岐でなかった場合、ジャンプ/分岐を元に戻す
  • RASが別の値を提供した場合、予測を修正する
  • 予測対象が正しかったと仮定する(分岐ターゲットを計算し、それを予測されたPCと比較するのは、組み合わせパスが多すぎる)

その後、予測結果はジャンプ/分岐の循環バッファに格納され、実行ユニットの分岐ロジックによって最終的な修正のために使用される。

RASは再スケジュール時にポインタを修復するが、投機的に更新された値は修復しない。

ジャンプ/分岐用循環バッファ

  • Push : デコード予測中
  • Read: 実行ユニットが分岐実行中に、最終的に誤った予測分岐を修正する
  • Pop: 命令確定後、分岐予測器の学習もトリガーする

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (4. メモリアクセスユニットについて)

NaxRiscvのドキュメントを、改めて位置から読んでみることにしたいと思う。

spinalhdl.github.io

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com


メモリ・システム

ロードストアの統合

LSUの実装の特徴は以下の通りである。

  • LQ / SQ : 通常、それぞれ16
  • AGUからのロード : ロードレイテンシを削減するために、LQにロードパイプライン用のデータがない場合、LQのレジスタを介さずにAGUが直接、最新の計算結果を提供できる
  • ロードヒット予測 : ロードのレイテンシを削減するため(6サイクルから3サイクルに)、キャッシュヒット予測機能があり、特定の命令を推測的に起動する
  • ハザード予測 : ストアの場合、アドレスとデータは両方とも発行キューを通じて提供される。そのため、遅延したデータは遅延したアドレスも作成し、ストアからロードへのハザードが発生する可能性がある。これを削減するために、ハザード予測機能がロードに追加された。
  • ストア to ロード・バイパス : 指定されたロードが同じサイズの単一ストアに依存している場合、ロード・パイプラインはストアのライトバックを待たずにストア値をバイパスすることがある
  • 並列メモリ変換 : ロードの場合、レイテンシを低減するために、メモリ変換はキャッシュ・リードと並列に実行される。
  • 共有アドレス・パイプライン : ロードとストアは、仮想アドレスを変換し、ハザードをチェックするために同じパイプラインを使用する。

共有およびライトバックパイプラインの図をいくつか示す:

MMU

MMUの実装の特徴は以下の通りである。

  • 2D構成 : ページテーブルの各レベルに対して、パラメータ化された数のダイレクト・マップ・トランケーション・キャッシュを指定できる。
  • ハードウェア・リフィル : これは安価であるため
  • キャッシュ・ダイレクト・ヒット : タイミングを改善するために、命令キャッシュがMMU TLBストレージに対してウェイタグを直接確認できるようにする(面積を犠牲にして)。

RV32では、デフォルト構成は次のとおりである。

  • 4ウェイ*レベル0(4KBページ)TLBの32エントリ
  • 2ウェイ*レベル1(4MBページ)TLBの32エントリ

各ウェイを分散RAMに推論することで、TLBキャッシュの領域は低く抑えられる。

MMU設計の例をいくつか紹介しよう。

コヒーレンシ

CPU上では、メモリーのコヒーレンシは以下の方法で実装されている。

  • メモリ・ブロック(データ・キャッシュ内)には、Permissionレベルがある(unloaded < shared < unique[dirty])。
  • shared Permissionは読み取り専用アクセスのみを許可する。
  • unique Permissionは読み取り/書き込みアクセスを許可する。
  • データ・キャッシュが許可をアップグレードしたい場合、取得要求を発行する(リード・バス上)。
  • データ・キャッシュが領域を解放したい場合、リリース要求を発行する(ライト・バス上)
  • インターコネクトは、指定されたアドレスのデータ・キャッシュの権限をダウングレードできる(プローブバス上)
  • プローブ要求は、データキャッシュのストアパイプラインを再利用して処理される
  • CPUによるアトミックなロード/ストアアクセスを許可するために、ロックインターフェースにより指定されたアドレスの権限ダウングレードを防止できる

NaxRiscvネイティブのデータキャッシュ・インターフェースはメモリに対してカスタム仕様だが、Tilelinkにブリッジできるように作られている。...

SoCの観点では、複数のマスタを接続するために、L2コヒーレント・キャッシュまたはキャッシュレスのコヒーレンシ・ハブのいずれかを選択できる。

L2キャッシュ

L2キャッシュには以下の特性がある。

  • Tilelinkインターフェースの提供(CPU/DMA用にアップ、メモリ用にダウン)
  • L1と一体型(NaxRiscv用はD$のみ)であるため、必要なプローブのみを送信するが、常にL1のコピーを持つ。
  • PLRUラインの排除
  • ノンブロッキング
  • マルチウェイ
  • データメモリはシンプルなデュアルポートRAMとして実装(FPGAに最適
  • アドレス/ソースに応じてアクセスを選択的にキャッシュ可能(ビデオDMAアクセスをキャッシュしない場合に有用

デフォルト構成におけるタイミングは以下の通り: - ヒットレイテンシ 5サイクル - ミスレイテンシペナルティ 4サイクル

アーキテクチャは主に以下の通り:

  • リクエストは中央集権化されたタグフェッチ/コントローラに送られる。
  • そのコントローラは、以下に説明するように、いくつかのパイプライン/FSMに供給することができる。
  • プローブコントローラは、L1キャッシュからデータを削除する必要がある場合、L2キャッシュラインを退避させるか、または固有の許可を取得する必要がある場合に使用される
  • キャッシュラインを再読み込みするか、キャッシュされていない読み取りアクセスを行う必要がある場合は、down.a を読み込む
  • 例えば up.c.release の場合は、up.d に直接応答を送信する
  • データコレクタにリクエストを送信し、データコレクタはメインメモリ/キャッシュに書き込み、さらに up.d にデータを送信する
  • キャッシュヒットおよびキャッシュエビクト時に使用されるリードキャッシュにリクエストを送信する...

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (3. 実行ユニットについて)

NaxRiscvのドキュメントを、改めて位置から読んでみることにしたいと思う。

spinalhdl.github.io

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com


実行ユニット

実行ユニットの主な特徴は、それに依存する命令をウェイクアップする方法である。

  • 静的ウェイクアップ : 固定レイテンシ(ストールなし)のEUの場合、発行キューは静的ウェイクアップが可能である。これにより、complete-to-useの遅延を2サイクル短縮できる。
  • 動的ウェイクアップ : 可変レイテンシ(ストールあり)のEUの場合、EUは発行キューにウェイクアップ・コマンドを送信する責任がある。

以下は、2つのEU(静的ウェイクアップと動的ウェイクアップのそれぞれ1つ)が、依存する命令をウェイクアップさせるために、発行キューとやりとりする様子を示した図解である。

図は本論文より引用

カスタム命令

NaxRiscvにカスタム命令を追加するにはいくつかの方法がある。本節ではいくつかのデモを示す。

SIMD add命令

整数レジスタファイル上で動作するSIMD加算(4x8ビット加算器)を実装するプラグインを定義しよう。

このプラグインは、ALUプラグインの実装をよりシンプルにするExecutionUnitElementSimpleをベースとする。このようなプラグインは、指定の実行ユニット(ExecutionUnitBaseがホスト)を構成するために使用できる。

例えば、プラグイン構成は次のようになる。

plugins += new ExecutionUnitBase("ALU0")
plugins += new IntFormatPlugin("ALU0")
plugins += new SrcPlugin("ALU0", earlySrc = true)
plugins += new IntAluPlugin("ALU0", aluStage = 0)
plugins += new ShiftPlugin("ALU0" , aluStage = 0)
plugins += new ShiftPlugin("ALU0" , aluStage = 0)
plugins += new SimdAddPlugin("ALU0") // <- このプラグインを実装することになる

プラグインの実装

以下はこのプラグインをどのように実装するかの例である: (https://github.com/SpinalHDL/NaxRiscv/blob/d44ac3a3a3a4328cf2c654f9a46171511a798fae/src/main/scala/naxriscv/execute/SimdAddPlugin.scala#L36)

package naxriscv.execute

import spinal.core._
import spinal.lib._
import naxriscv._
import naxriscv.riscv._
import naxriscv.riscv.IntRegFile
import naxriscv.interfaces.{RS1, RS2}
import naxriscv.utilities.Plugin

    //このプラグインの例では SIMD_ADD と呼ばれる以下の処理を行う新しい命令を実装する
//
//RD : Regfile Destination, RS : Regfile Source
//RD( 7 downto  0) = RS1( 7 downto  0) + RS2( 7 downto  0)
//RD(16 downto  8) = RS1(16 downto  8) + RS2(16 downto  8)
//RD(23 downto 16) = RS1(23 downto 16) + RS2(23 downto 16)
//RD(31 downto 24) = RS1(31 downto 24) + RS2(31 downto 24)
//
//Instruction encoding :
//0000000----------000-----0001011   <- Custom0 func3=0 func7=0
//       |RS2||RS1|   |RD |
//
    //Note :  RS1, RS2, RD のビット位置はRISC-Vの仕様に準拠しており、このISAのすべての命令において共通である。


object SimdAddPlugin{
      //命令タイプとエンコーディングを定義する
  val ADD4 = IntRegFile.TypeR(M"0000000----------000-----0001011")
}

    //ExecutionUnitElementSimpleは、同じeuIdを持つExecutionUnitBaseによって提供されるパイプラインに結合されるベースクラスである。
    // これは、カスタム命令の実装を容易にするための多数のユーティリティを提供する。
    // ここでは、SIMD加算をレジスタファイルに追加するプラグインを実装する。
    // staticLatency=true は、このプラグインがパイプラインを停止させることは決してないことを指定し、その結果に依存する命令を静的に発行キューで実行できるようにする。
class SimdAddPlugin(val euId : String) extends ExecutionUnitElementSimple(euId, staticLatency = true) {
      //このプラグインが完全に組み合わせ回路で構成されると仮定する
  override def euWritebackAt = 0

      // セットアップコードは、プラグインが互いのことを指定するもので、手遅れになる前に設定する。
  override val setup = create early new Setup{
    //Let's assume we only support RV32 for now
    assert(Global.XLEN.get == 32)

            //現在のプラグインがADD4命令を実装することを実行ユニットベースに指定する。
    add(SimdAddPlugin.ADD4)
  }

  override val logic = create late new Logic{
    val process = new ExecuteArea(stageId = 0) {
      //Get the RISC-V RS1/RS2 values from the register file
      val rs1 = stage(eu(IntRegFile, RS1)).asUInt
      val rs2 = stage(eu(IntRegFile, RS2)).asUInt

      //Do some computation
      val rd = UInt(32 bits)
      rd( 7 downto  0) := rs1( 7 downto  0) + rs2( 7 downto  0)
      rd(16 downto  8) := rs1(16 downto  8) + rs2(16 downto  8)
      rd(23 downto 16) := rs1(23 downto 16) + rs2(23 downto 16)
      rd(31 downto 24) := rs1(31 downto 24) + rs2(31 downto 24)

      //Provide the computation value for the writeback
      wb.payload := rd.asBits
    }
  }
}

NaxRiscv の生成

次に、新しいプラグイン付きのNaxRiscvを生成するために、以下のAppを実行することができる : (https://github.com/SpinalHDL/NaxRiscv/blob/d44ac3a3a3a4328cf2c654f9a46171511a798fae/src/main/scala/naxriscv/execute/SimdAddPlugin.scala#L71)

object SimdAddNaxGen extends App{
  import naxriscv.compatibility._
  import naxriscv.utilities._

  def plugins = {
    //Get a default list of plugins
    val l = Config.plugins(
      withRdTime = false,
      aluCount    = 2,
      decodeCount = 2
    )
    //Add our plugin to the two ALUs
    l += new SimdAddPlugin("ALU0")
    l += new SimdAddPlugin("ALU1")
    l
  }

  //Create a SpinalHDL configuration that will be used to generate the hardware
  val spinalConfig = SpinalConfig(inlineRom = true)
  spinalConfig.addTransformationPhase(new MemReadDuringWriteHazardPhase)
  spinalConfig.addTransformationPhase(new MultiPortWritesSymplifier)

  //Generate the NaxRiscv verilog file
  val report = spinalConfig.generateVerilog(new NaxRiscv(xlen = 32, plugins))

  //Generate some C header files used by the verilator testbench to connect to the DUT
  report.toplevel.framework.getService[DocPlugin].genC()
}

このAppを実行するために、NaxRiscvのディレクトリで以下を実行する:

sbt "runMain naxriscv.execute.SimdAddNaxGen"

ソフトウェアテスト

次に、アセンブリテストコードを書こう : (https://github.com/SpinalHDL/NaxSoftware/tree/849679c70b238ceee021bdfd18eb2e9809e7bdd0/baremetal/simdAdd)

.globl _start
_start:

#include "../../driver/riscv_asm.h"
#include "../../driver/sim_asm.h"
#include "../../driver/custom_asm.h"

    //Test 1
    li x1, 0x01234567
    li x2, 0x01FF01FF
    opcode_R(CUSTOM0, 0x0, 0x00, x3, x1, x2) //x3 = ADD4(x1, x2)

    //Print result value
    li x4, PUT_HEX
    sw x3, 0(x4)

    //Check result
    li x5, 0x02224666
    bne x3, x5, fail

    j pass

pass:
    j pass
fail:
    j fail

以下のようにしてコンパイルする

make clean rv32im

シミュレーション

src/test/cpp/naxriscv のシミュレーションを実行する (最初にreadmeに書いてあるセットアップを行う必要がある)

make clean compile
./obj_dir/VNaxRiscv --load-elf ../../../../ext/NaxSoftware/baremetal/simdAdd/build/rv32im/simdAdd.elf --spike-disable --pass-symbol pass --fail-symbol fail --trace

シェルに2224666と表示されれば成功である :D

Conclusion

したがって、この例では、追加のデコードの指定方法や、マルチサイクルALUの定義方法については紹介していない。(TODO)。 しかし、IntAluPlugin、ShiftPlugin、DivPlugin、MulPlugin、BranchPluginでは、同じExecutionUnitElementSimpleベースクラスを使用して、それらの処理を行っている。

また、ExecutionUnitElementSimpleベースクラスを使用する必要はなく、LoadPlugin、StorePlugin、EnvCallPluginのように、より基本的なアクセスも可能である。

ハードコアな方法

同じ命令の例だが、ExecutionUnitElementSimpleの機能を使用せずに実装した例を以下に示す: (https://github.com/SpinalHDL/NaxRiscv/blob/72b80e3345ecc3a25ca913f2b741e919a3f4c970/src/main/scala/naxriscv/execute/SimdAddPlugin.scala#L100)

object SimdAddRawPlugin{
  val SEL = Stageable(Bool()) //Will be used to identify when we are executing a ADD4
  val ADD4 = IntRegFile.TypeR(M"0000000----------000-----0001011")
}

class SimdAddRawPlugin(euId : String) extends Plugin {
  import SimdAddRawPlugin._
  val setup = create early new Area{
    val eu = findService[ExecutionUnitBase](_.euId == euId)
    eu.retain() //We don't want the EU to generate itself before we are done with it

    //Specify all the ADD4 requirements
    eu.addMicroOp(ADD4)
    eu.setCompletion(ADD4, stageId = 0)
    eu.setStaticWake(ADD4, stageId = 0)
    eu.setDecodingDefault(SEL, False)
    eu.addDecoding(ADD4, SEL, True)

    //IntFormatPlugin provide a shared point to write into the register file with some optional carry extensions
    val intFormat = findService[IntFormatPlugin](_.euId == euId)
    val writeback = intFormat.access(stageId = 0, writeLatency = 0)
  }

  val logic = create late new Area{
    val eu = setup.eu
    val writeback = setup.writeback
    val stage = eu.getExecute(stageId = 0)

    //Get the RISC-V RS1/RS2 values from the register file
    val rs1 = stage(eu(IntRegFile, RS1)).asUInt
    val rs2 = stage(eu(IntRegFile, RS2)).asUInt

    //Do some computation
    val rd = UInt(32 bits)
    rd( 7 downto  0) := rs1( 7 downto  0) + rs2( 7 downto  0)
    rd(16 downto  8) := rs1(16 downto  8) + rs2(16 downto  8)
    rd(23 downto 16) := rs1(23 downto 16) + rs2(23 downto 16)
    rd(31 downto 24) := rs1(31 downto 24) + rs2(31 downto 24)

    //Provide the computation value for the writeback
    writeback.valid   := stage(SEL)
    writeback.payload := rd.asBits

    //Now the EU has every requirements set for the generation (from this plugin perspective)
    eu.release()
  }
}

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (12. PcPluginの実装解析)

PcPluginの実装の解析

github.com

とりあえずまずはChatGPTにこのソースコードを解説してもらおうと思う。

- ジャンプロジックの設定:

ジャンプロジックでは、登録されたジャンプ要求を優先度に応じてソートし、優先度が高いものから順に評価します。
ジャンプ命令が有効であれば、それに応じてプログラムカウンタが更新されます。これは、異なるジャンプグループを適切に扱うことで複数のジャンプ要求をまとめて処理します。

- プログラムカウンタの初期化とインクリメントロジック:

初期化ロジックでは、システムが完全に起動するまでのサイクル数を計算し、その間はプログラムカウンタの更新を遅延させます。
PCの計算では、ジャンプ命令による更新がない場合は通常のインクリメントを行います。ジャンプ命令が発行された場合は、プログラムカウンタをジャンプ先のアドレスに設定し直します。

最初のジャンプロジックの設定は以下のソースコードの部分だと思われる:

    val jump = new Area {
      val sortedByStage = jumpsSpec.sortWith(_.priority > _.priority)
      val valids = sortedByStage.map(_.interface.valid)
      val cmds = sortedByStage.map(_.interface.payload)
      val oh = OHMasking.firstV2(valids.asBits)

      val grouped = sortedByStage.groupByLinked(_.aggregationPriority).toList.sortBy(_._1).map(_._2)
      var target = PC()
      for(group <- grouped){
        val indexes = group.map(e => sortedByStage.indexOf(e))
        val goh = indexes.map(i => oh(i))
        val mux = OhMux.or(goh, group.map(_.interface.pc))
        if(group == grouped.head) target := mux else when(goh.orR){
          KeepAttribute(target)
          target \= mux
        }
      }

      val pcLoad = Flow(JumpCmd(pcWidth = widthOf(PC)))
      pcLoad.valid := jumpsSpec.map(_.interface.valid).orR
      pcLoad.pc    := target
    }

これが以下のようにVerilogに変換されている。もう何が何だか分からない。

  reg        [39:0]   PcPlugin_logic_jump_target_1;
  wire       [5:0]    _zz_PcPlugin_logic_jump_oh;
  wire                _zz_PcPlugin_logic_jump_oh_1;
  wire                _zz_PcPlugin_logic_jump_oh_2;
  wire                _zz_PcPlugin_logic_jump_oh_3;
  wire                _zz_PcPlugin_logic_jump_oh_4;
  wire                _zz_PcPlugin_logic_jump_oh_5;
  reg        [5:0]    _zz_PcPlugin_logic_jump_oh_6;
  wire       [5:0]    PcPlugin_logic_jump_oh;
  (* keep , syn_keep *) wire       [39:0]   PcPlugin_logic_jump_target /* synthesis syn_keep = 1 */ ;
  wire                _zz_PcPlugin_logic_jump_target_1;
  wire                PcPlugin_logic_jump_pcLoad_valid;
  wire       [39:0]   PcPlugin_logic_jump_pcLoad_payload_pc;
    PcPlugin_logic_jump_target_1 = PcPlugin_logic_jump_target;
      PcPlugin_logic_jump_target_1 = (_zz_PcPlugin_logic_jump_target_1 ? BtbPlugin_setup_btbJump_payload_pc : 40'h0000000000);
  assign _zz_PcPlugin_logic_jump_oh = {BtbPlugin_setup_btbJump_valid,{FetchCachePlugin_setup_redoJump_valid,{AlignerPlugin_setup_sequenceJump_valid,{DecoderPredictionPlugin_setup_decodeJump_valid,{CommitPlugin_setup_jump_valid,PrivilegedPlugin_setup_jump_valid}}}}};
  assign _zz_PcPlugin_logic_jump_oh_1 = _zz_PcPlugin_logic_jump_oh[0];
  assign _zz_PcPlugin_logic_jump_oh_2 = _zz_PcPlugin_logic_jump_oh[1];
  assign _zz_PcPlugin_logic_jump_oh_3 = _zz_PcPlugin_logic_jump_oh[2];
  assign _zz_PcPlugin_logic_jump_oh_4 = _zz_PcPlugin_logic_jump_oh[3];
  assign _zz_PcPlugin_logic_jump_oh_5 = _zz_PcPlugin_logic_jump_oh[4];
    _zz_PcPlugin_logic_jump_oh_6[0] = (_zz_PcPlugin_logic_jump_oh_1 && (! 1'b0));
    _zz_PcPlugin_logic_jump_oh_6[1] = (_zz_PcPlugin_logic_jump_oh_2 && (! _zz_PcPlugin_logic_jump_oh_1));
    _zz_PcPlugin_logic_jump_oh_6[2] = (_zz_PcPlugin_logic_jump_oh_3 && (! (|{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1})));
    _zz_PcPlugin_logic_jump_oh_6[3] = (_zz_PcPlugin_logic_jump_oh_4 && (! (|{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}})));
    _zz_PcPlugin_logic_jump_oh_6[4] = (_zz_PcPlugin_logic_jump_oh_5 && (! (|{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}})));
    _zz_PcPlugin_logic_jump_oh_6[5] = (_zz_PcPlugin_logic_jump_oh[5] && (! (|{_zz_PcPlugin_logic_jump_oh_5,{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}}})));
  assign PcPlugin_logic_jump_oh = _zz_PcPlugin_logic_jump_oh_6;
  assign PcPlugin_logic_jump_target = ((((PcPlugin_logic_jump_oh[0] ? PrivilegedPlugin_setup_jump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[1] ? CommitPlugin_setup_jump_payload_pc : 40'h0000000000)) | ((PcPlugin_logic_jump_oh[2] ? DecoderPredictionPlugin_setup_decodeJump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[3] ? AlignerPlugin_setup_sequenceJump_payload_pc : 40'h0000000000))) | (PcPlugin_logic_jump_oh[4] ? FetchCachePlugin_setup_redoJump_payload_pc : 40'h0000000000));
  assign _zz_PcPlugin_logic_jump_target_1 = PcPlugin_logic_jump_oh[5];
  assign when_PcPlugin_l55 = (|_zz_PcPlugin_logic_jump_target_1);
  assign PcPlugin_logic_jump_pcLoad_valid = (|{PrivilegedPlugin_setup_jump_valid,{CommitPlugin_setup_jump_valid,{BtbPlugin_setup_btbJump_valid,{DecoderPredictionPlugin_setup_decodeJump_valid,{AlignerPlugin_setup_sequenceJump_valid,FetchCachePlugin_setup_redoJump_valid}}}}});
  assign PcPlugin_logic_jump_pcLoad_payload_pc = PcPlugin_logic_jump_target_1;
    if(PcPlugin_logic_jump_pcLoad_valid) begin
    if(PcPlugin_logic_jump_pcLoad_valid) begin
      PcPlugin_logic_fetchPc_pc = PcPlugin_logic_jump_pcLoad_payload_pc;
    if(PcPlugin_logic_jump_pcLoad_valid) begin

例えば jump_oh の実装は以下の感じでめっちゃややこしくなっている。

  assign _zz_PcPlugin_logic_jump_oh = {BtbPlugin_setup_btbJump_valid,{FetchCachePlugin_setup_redoJump_valid,{AlignerPlugin_setup_sequenceJump_valid,{DecoderPredictionPlugin_setup_decodeJump_valid,{CommitPlugin_setup_jump_valid,PrivilegedPlugin_setup_jump_valid}}}}};
  assign _zz_PcPlugin_logic_jump_oh_1 = _zz_PcPlugin_logic_jump_oh[0];
  assign _zz_PcPlugin_logic_jump_oh_2 = _zz_PcPlugin_logic_jump_oh[1];
  assign _zz_PcPlugin_logic_jump_oh_3 = _zz_PcPlugin_logic_jump_oh[2];
  assign _zz_PcPlugin_logic_jump_oh_4 = _zz_PcPlugin_logic_jump_oh[3];
  assign _zz_PcPlugin_logic_jump_oh_5 = _zz_PcPlugin_logic_jump_oh[4];
    _zz_PcPlugin_logic_jump_oh_6[0] = (_zz_PcPlugin_logic_jump_oh_1 && (! 1'b0));
    _zz_PcPlugin_logic_jump_oh_6[1] = (_zz_PcPlugin_logic_jump_oh_2 && (! _zz_PcPlugin_logic_jump_oh_1));
    _zz_PcPlugin_logic_jump_oh_6[2] = (_zz_PcPlugin_logic_jump_oh_3 && (! (|{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1})));
    _zz_PcPlugin_logic_jump_oh_6[3] = (_zz_PcPlugin_logic_jump_oh_4 && (! (|{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}})));
    _zz_PcPlugin_logic_jump_oh_6[4] = (_zz_PcPlugin_logic_jump_oh_5 && (! (|{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}})));
    _zz_PcPlugin_logic_jump_oh_6[5] = (_zz_PcPlugin_logic_jump_oh[5] && (! (|{_zz_PcPlugin_logic_jump_oh_5,{_zz_PcPlugin_logic_jump_oh_4,{_zz_PcPlugin_logic_jump_oh_3,{_zz_PcPlugin_logic_jump_oh_2,_zz_PcPlugin_logic_jump_oh_1}}}})));
  assign PcPlugin_logic_jump_oh = _zz_PcPlugin_logic_jump_oh_6;
  assign PcPlugin_logic_jump_target = ((((PcPlugin_logic_jump_oh[0] ? PrivilegedPlugin_setup_jump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[1] ? CommitPlugin_setup_jump_payload_pc : 40'h0000000000)) | ((PcPlugin_logic_jump_oh[2] ? DecoderPredictionPlugin_setup_decodeJump_payload_pc : 40'h0000000000) | (PcPlugin_logic_jump_oh[3] ? AlignerPlugin_setup_sequenceJump_payload_pc : 40'h0000000000))) | (PcPlugin_logic_jump_oh[4] ? FetchCachePlugin_setup_redoJump_payload_pc : 40'h0000000000));

次のfetchPCがPCのインクリメントの部分だと思われる。

    val fetchPc = new Area{
      //PC calculation without Jump
      val output = Stream(PC)
      val pcReg = Reg(PC) init(resetVector) addAttribute(Verilator.public)
      val correction = False
      val correctionReg = RegInit(False) setWhen(correction) clearWhen(output.fire)
      val corrected = correction || correctionReg
      val pcRegPropagate = False
      val inc = RegInit(False) clearWhen(correction || pcRegPropagate) setWhen(output.fire) clearWhen(!output.valid && output.ready)
      val pc = pcReg + (U(inc) << sliceRange.high+1)


      val flushed = False

      when(inc) {
        pc(sliceRange) := 0
      }

      when(jump.pcLoad.valid) {
        correction := True
        pc := jump.pcLoad.pc
        flushed := True
      }

      when(init.booted && (output.ready || correction || pcRegPropagate)){
        pcReg := pc
      }

      pc(0) := False
      if(!RVC) pc(1) := False

      val fetcherHalt = False
      output.valid := !fetcherHalt && init.booted
      output.payload := pc
    }

これも以下のようにVerilogに変換されていた。

  assign _zz_PcPlugin_logic_fetchPc_pc_1 = ({3'd0,PcPlugin_logic_fetchPc_inc} <<< 2'd3);
  assign _zz_PcPlugin_logic_fetchPc_pc = {36'd0, _zz_PcPlugin_logic_fetchPc_pc_1};
    PcPlugin_logic_fetchPc_correction = 1'b0;
      PcPlugin_logic_fetchPc_correction = 1'b1;
  assign PcPlugin_logic_fetchPc_output_fire = (PcPlugin_logic_fetchPc_output_valid && PcPlugin_logic_fetchPc_output_ready);
  assign PcPlugin_logic_fetchPc_corrected = (PcPlugin_logic_fetchPc_correction || PcPlugin_logic_fetchPc_correctionReg);
  assign PcPlugin_logic_fetchPc_pcRegPropagate = 1'b0;
  assign when_PcPlugin_l82 = (PcPlugin_logic_fetchPc_correction || PcPlugin_logic_fetchPc_pcRegPropagate);
  assign when_PcPlugin_l82_1 = ((! PcPlugin_logic_fetchPc_output_valid) && PcPlugin_logic_fetchPc_output_ready);
    PcPlugin_logic_fetchPc_pc = (PcPlugin_logic_fetchPc_pcReg + _zz_PcPlugin_logic_fetchPc_pc);
    if(PcPlugin_logic_fetchPc_inc) begin
      PcPlugin_logic_fetchPc_pc[2 : 2] = 1'b0;
      PcPlugin_logic_fetchPc_pc = PcPlugin_logic_jump_pcLoad_payload_pc;
    PcPlugin_logic_fetchPc_pc[0] = 1'b0;
    PcPlugin_logic_fetchPc_pc[1] = 1'b0;
    PcPlugin_logic_fetchPc_flushed = 1'b0;
      PcPlugin_logic_fetchPc_flushed = 1'b1;
  assign when_PcPlugin_l98 = (PcPlugin_logic_init_booted && ((PcPlugin_logic_fetchPc_output_ready || PcPlugin_logic_fetchPc_correction) || PcPlugin_logic_fetchPc_pcRegPropagate));
  assign PcPlugin_logic_fetchPc_fetcherHalt = 1'b0;
  assign PcPlugin_logic_fetchPc_output_valid = ((! PcPlugin_logic_fetchPc_fetcherHalt) && PcPlugin_logic_init_booted);
  assign PcPlugin_logic_fetchPc_output_payload = PcPlugin_logic_fetchPc_pc;
  assign PcPlugin_logic_fetchPc_output_ready = FetchPlugin_stages_0_ready;
  assign FetchPlugin_stages_0_valid = PcPlugin_logic_fetchPc_output_valid;
  assign FetchPlugin_stages_0_Fetch_FETCH_PC = PcPlugin_logic_fetchPc_output_payload;
      PcPlugin_logic_fetchPc_pcReg <= 40'h0080000000;
      PcPlugin_logic_fetchPc_correctionReg <= 1'b0;
      PcPlugin_logic_fetchPc_inc <= 1'b0;
      if(PcPlugin_logic_fetchPc_correction) begin
        PcPlugin_logic_fetchPc_correctionReg <= 1'b1;
      if(PcPlugin_logic_fetchPc_output_fire) begin
        PcPlugin_logic_fetchPc_correctionReg <= 1'b0;
        PcPlugin_logic_fetchPc_inc <= 1'b0;
      if(PcPlugin_logic_fetchPc_output_fire) begin
        PcPlugin_logic_fetchPc_inc <= 1'b1;
        PcPlugin_logic_fetchPc_inc <= 1'b0;
        PcPlugin_logic_fetchPc_pcReg <= PcPlugin_logic_fetchPc_pc;

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (2. )

NaxRiscvのドキュメントを、改めて位置から読んでみることにしたいと思う。

spinalhdl.github.io

msyksphinz.hatenablog.com


Frontend

デコーダ

特になし。

物理レジスタ割り当て

物理レジスタ割り当ては、割り当てられていない物理レジスタのインデックスを含む循環バッファを実装することで行われる。 これは、分散RAMを搭載したFPGAにうまく適合する。

アーキテクチャ・レジスタから物理レジスタ

アーキテクチャ・レジスタファイルから物理レジスタへの変換は、以下の3つのテーブルを実装することで行われる。

  • 投機的マッピング:アーキテクチャ・インデックスから物理インデックスへの変換。命令デコード後に更新され、分散RAMで実装される
  • コミット後マッピング : アーキテクチャ・インデックスから物理インデックスへの変換。命令のコミット後に更新され、分散RAMで実装される
  • Location : アーキテクチャ・インデックスから、使用すべきマッピング・テーブル(投機的マッピングテーブル またはコミット後マッピングテーブル)への変換。レジスタとして実装される(分岐の予測ミスが発生した場合はクリアする必要がある)。

これにより、パイプラインが分岐を誤って予測した場合に、変換の状態を即座に元に戻すことができる。

物理インデックスからROB ID

依存関係の物理レジスタファイルが計算されると、それらは依存するROB IDに変換される。これは次の2つの方法で行われる。

  • ROBマッピング:物理インデックスからROB IDに変換する分散RAM
  • ビジー:指定されたROB IDがまだ実行中であるかどうかを指定する。これは命令がディスパッチされたときに設定され、命令が完了したときにクリアされる。

ディスパッチ/命令発行

現在の実装に関する具体的なポイントをいくつか挙げてみる。

  • ユニファイドディスパッチ・命令発行:主に領域を節約するため、また各エントリーの使用頻度を最大限に高めるため
  • 2Dキュー:エントリーはC=decodeCount列、L=slotCount/decodeCount行に配置される
  • 行プッシュ : キューに何かがプッシュされると、その行全体が「消費」される。たとえその行が完全に使用されていない場合でも、行全体を消費する。
  • 圧縮なし : 空行には圧縮が適用されない。プッシュが行われるごとに、ホワイトキューが1行シフトする。これにより、行列FFのより優れた推論と、より小さく高速なROB IDウェイクロジックが可能になる。
  • マトリックスベース : どの命令を格納するかは、ハーフマトリックスとして実行される内容によって決まる
  • 古い順 : 複数の命令が特定の実行ユニットで同時にディスパッチされる場合、古い命令が選択される
  • ROB IDによるウェイクアップ : ダイナミックウェイクアップの場合、ROB IDが識別子として使用される(物理レジスタファイルIDではない)

そのため、全体的には、タイミングを維持するために超えないようにするには、32スロットのキューが限界のようだ。また、現在の設計では、キューの占有面積はCPU全体と比較してそれほど大きな問題ではないようだ。

以下にいくつかの図を示す:

オープンソース・アウトオブオーダCPU NaxRiscvのドキュメントを読んでいく (1. イントロダクション)

NaxRiscvのドキュメントを、改めて位置から読んでみることにしたいと思う。

spinalhdl.github.io


イントロダクション

NaxRiscv

NaxRiscv は現在、以下の特徴を持つコアである。

  • レジスタ・リネーミング付きアウト・オブ・オーダ実行
  • スーパースカラ(例:2デコード、3実行ユニット、2リタイア)
  • (RV32/RV64) IMAFDCSU(Linux / Buildroot / Debian
  • ポータブルHDLだが、分散RAM付きのターゲットFPGA(これまで使用された参照はXilinxシリーズ7)
  • (比較的) 低い面積使用率と高い fmax をターゲットとする (最高の IPC ではない)
  • 分散型ハードウェア演算 (プラグインでパラメータ設定可能な空のトップレベル)
  • カスタマイズを容易にするパイプラインフレームワークをベースとしたフロントエンドの実現
  • 複数のリフィルおよびライトバックスロットを備えたノンブロッキングデータキャッシュ
  • BTB + GSHARE + RAS ブランチ予測
  • ハードウェアリフィル MMU (SV32、SV39)
  • 投機的キャッシュヒット予測により、レイテンシ3サイクルでロードを実行
  • verilatorシミュレーションとKonata(gem5ファイルフォーマット)によるパイプラインの視覚化
  • RISCV外部デバッグサポートv. 0.13.2の実装によるJTAG / OpenOCD / GDBサポート
  • Tilelinkによるメモリコヒーレンシのサポート
  • オプションのコヒーレントL2キャッシュ

プロジェクトの開発と現状

  • このプロジェクトはフリーかつオープンソースである
  • ハードウェア(ArtyA7 / Litex)上でDebian/Buildroot/Linuxを上流で実行できる
  • 2021年10月に開始し、その後NLnetから資金援助を受けた(https://nlnet.nl/project/NaxRiscv/#ack)

第三者によるドキュメントもこちらで入手可能(CEA-Letiより): https://github.com/SpinalHDL/naxriscv_doc

なぜFPGAをターゲットにしたOoOコアなのか

理由はいくつかある

  • シングルスレッドのパフォーマンスを向上させるため。

VexRiscvでLinuxを実行したテストでは、マルチコアが役立つとしても、「ほとんど」のアプリケーションはそれを活用するように作られていないことが明らかになった。 - メモリレイテンシを隠蔽するため(FPGAに大きなL2キャッシュを搭載できるほどのメモリはない)。 - より高度なハードウェア記述パラダイム(Scala/SpinalHDL)を試すため。 - 個人的な興味から。

また、世の中にはOoOのオープンソース・ソフトコアがあまり存在していなかった(Marocchino、RSD、OPAなど)。 いくつかの指標において、より良い結果を出すことができるという賭けだった。 そして、より良いパフォーマンス(面積の犠牲を払って)を提供することで、1命令発行/インオーダ・コアのソフトコアを置き換えるのに十分なほど良い結果を出すことができるだろう。