FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

Chiselのパラメタライズについて調査(2. Advanced Parameterization Manualの続き)

Chiselについて少しずつ勉強を進めているが、SystemVerilogと同様のことが実現できないと、マスターしたとは言えないだろう。

Verilogでも多く使われるパラメータ化、これをどのようにしてChiselで使用するのか、Rocket-Chipのデザインを見ても良く分からないので調査したい。

このあたりについて非常に細かく記述してある資料 Advanced Parameterization Manual を翻訳して調べてみた。 まずは簡単に翻訳した結果を掲載しておきたい。 今回は後半。前半はこちら

3. 例

パラメータ化には3つの目標がある。

  1. 全ての検索できるパラメータは、トップレベルから参照できる。
  2. 異なるポイントから評価を実行しても、ソースコードは変更されない。
  3. 新しいパラメータを追加してもソースコードの変更は最小限で済む。

これから紹介する例について説明した後、3つの目標のいずれにも違反せずに、望ましいデザインスペースをサポートするもっとも簡単なパラメータ化の方法を示す。例が複雑になるにつれて、現在の高度なパラメータ化のメソッドに到達するまではもっとも単純なパラメータ化の方法も必要だ。

3.1 単純なパラメータ

pict

図4. 幾つかのパラメータを渡す簡単な方法は、コンストラクタの引数に直接渡すことだ。

シンプルなデザインでは、コア特有のパラメータや、キャッシュのためだけのパラメータなどが存在する。もっとも単純なパラメータ化の方法は、全てのパラメータをTileのコンストラクタの引数として渡すことだ。これらの値は、それぞれのコンストラクタを経由してCoreCacheに渡される。

class Tile (val fpu:Boolean, val ic_sets:Int, val ic_ways:Int, val dc_sets:Int, val dc_ways:Int) extends Module { 
  val core = Module(new Core(fpu)) 
  val icache = Module(new Cache(ic_sets,ic_ways) 
  val dcache = Module(new Cache(dc_sets,dc_ways)) 
  ... 
} 
class Core (val fpu:Boolean) {...} 
class Cache(val sets:Int, val ways:Int) extends Module {...}

パラメータを参照するためのソースコードの変更は必要なく、全ての検索可能なパラメータは、Topから参照できる。さらに、新しいパラメータを追加するのは、このパラメータがシンプルであるため、ソースコードの変更は最小限で済む。

3.2 パラメータセットの分離

pict

図5. パラメータセットを分離することによって、パラメータセットをコンフィグレーションオブジェクトにグループ化してコンストラクタの引数に渡すことができる。

次のデザインでは、チップないに異なるコアをインスタンスし、それぞれで異なるパラメータを設定する。上記の単純なソリューションを適用すれば、Tileのコンストラクタに対して、パラメータの数だけ値を設定する必要がある(さらに、2コアからコア数が増えると、さらに設定する項目が多くなる!)

より良い解決章のためには、パラメータのグループをコンフィグレーションオブジェクトに分割することである。例えば、BigCoreに必要なすべてのパラメータをBigCoreConfigcaseクラスに設定し、SmallCoreのパラメータをSmallCoreConfigクラスに設定し、どちらのクラスもCoreConfigクラスの継承として定義する。さらに、キャッシュとTileのコンフィグのためにCacheConfigTileConfigを用意して、コンストラクタに渡す。

abstract class CoreConfig {} 
case class BigCoreConfig(iq_depth:Int, lsq_depth:Int) extends CoreConfig 
case class SmallCoreConfig(fpu:Boolean) extends CoreConfig 
case class CacheConfig(sets:Int, ways:Int) 
case class TileConfig(cc:CoreConfig, icc:CacheConfig, dcc:CacheConfig) 
 
class Tile (val tc:TileConfig) extends Module { 
  val core = tc.cc match { 
    case bcc:BigCoreConfig => Module(new BigCore(tc.bcc)) 
    case scc:SmallCoreConfig => Module(new SmallCore(tc.scc)) 
  } 
  val icache = Module(new Cache(tc.icc) 
  val dcache = Module(new Cache(tc.dcc)) 
  ... 
} 
...

3.3 Location-Independentなパラメータ

pict

図6. Location-Independentなパラメータでは、全てのモジュールはパラメータディレクトリを持ち、パラメータをコピーするか、値を変更して子供のモジュールに渡す。

ネストされたコンフィグレーションオブジェクトが脆弱である微妙な理由は、ネストされたコンフィグレーションオブジェクトがモジュールの改装をエンコードするからである。図6に示す新しいデザインを見ると、BigCoreがIQとLSQを含んでおり、icacheとdcacheがメモリモジュールをインスタンスしている。このメモリモジュールはwidthパラメータを含んでおり、昨日を正しく構成するために、全てのメモリの幅は同一に設定されなければならない。この要求を満たすために、以下のようなコードを記述する:

case class MemConfig(size:Int, banks:Int, width:Int) 
case class CacheConfig(sets:Int, ways:Int, mc:MemConfig) 
case class QueueConfig(depth:Int, mc:MemConfig) 
case class BigCoreConfig(iqc:QueueConfig, lsqc:QueueConfig, mc:MemConfig) 
 
case class TileConfig(cc:CoreConfig, icc:CacheConfig, dcc:CacheConfig) 
 
class Tile (val tc:TileConfig) extends Module { 
  val core = tc.cc match { 
    case bcc:BigCoreConfig => Module(new BigCore(tc.bcc)) 
    case scc:SmallCoreConfig => Module(new SmallCore(tc.scc)) 
  } 
  val icache = Module(new Cache(tc.icc) 
  val dcache = Module(new Cache(tc.dcc)) 
 
  require(tc.dcc.mc.width == tc.icc.mc.width) 
  require(tc.bcc.iqc.mc.width == tc.bcc.lsqc.mc.width) 
  require(tc.dcc.mc.width == tc.bcc.lsqc.mc.width) 
  ... 
} 
...

require構文は非常に脆弱であり、デザイン中の改装の任意の変更によりこれらの構文を書き直す必要がある。このrequire構文を取り除くことは不可能である; この構文は基本的なデザインの要求を満たすために必要である。

コンフィグレーションオブジェクトの問題は、カスタムパラメータ化の解決方法の最初のきおぬ、つまりタイプパラメータのコピー/変更につながる。私たちは、key-value構造(mapもしくは辞書)により、モジュールのパラメータを格納する方法である。

図6. のデザインをパラメタライズするためには、暗黙的にParameterオブジェクトを渡すか、パラメータの変更が必要ならばPartialFunctionをモジュールファクトリに提供するという方法がある。第2章のMyConfig(ChiselConfigの継承)を思い出すと、Chiselコンパイラ--configInstanceフラグを使用してトップレベルのパラメータを指定する:

class DefaultConfig() extends ChiselConfig { 
  val top:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case IQ_depth => 10 
      case LSQ_depth =>10 
      case Ic_sets => 128 
      case Ic_ways => 2 
      case Dc_sets => 512 
      case Dc_ways => 4 
      case Width => 64 
      // since any module querying Width should return 64, the name should NOT be unique to modules 
    } 
  } 
} 
class Tile extends Module { 
  val core = Module(new Core)(params) 
  val ic_sets = params(Ic_sets) 
  val ic_ways = params(Ic_ways) 
  val icache = Module(new Cache, {case Sets => ic_sets; case Ways => ic_ways}) 
  // we can rename Ic_sets to Sets, effectively isolating Cache's query keys from any design hierarchy dependence 
  val dc_sets = params(Dc_sets) 
  val dc_ways = params(Dc_ways) 
  val dcache = Module(new Cache, {case Sets => dc_sets; case Ways => dc_ways}) 
  // similarly we rename Dc_sets to Sets and Dc_ways to Ways 
} 
class Core extends Module { 
  val iqdepth = params(IQ_depth) 
  val iq = Module(new Queue, {case Depth => iqdepth}) 
  val lsqdepth = params(LSQ_depth) 
  val lsq = Module(new Queue, {case Depth => lsqdepth}) 
  ... 
} 
class Queue extends Module { 
  val depth = params(Depth) 
  val mem = Module(new Memory,{case Size => depth}) 
  ... 
} 
class Cache extends Module { 
  val sets = params(Sets) 
  val ways = params(Ways) 
  val mem = Module(new Memory,{case Size => sets*ways}) 
} 
class Memory extends Module { 
  val size = params(Size) 
  val width = params(Width) 
}

3.4 Local-Specific パラメータ

pict

図7. Location-dependentパラメータでは、siteのメカニズムを使用してトップレベルでパラメータをカスタマイズすることができる。

前の節で見たように、パラメータオブジェクトをコピーして変更することは上古府である。ECCパラメータをメモリモジュールに追加したい場合、メモリをどこにインスタンスしたかにより依存することになる。複数の親に対してパラメータを名前変更するようなソースコードを記述することになる(例えば、ECC_icache=>ECC)。

図7.に示したデザインでは、パラメータに対してsiteの機能を使用して、Location-Specificな情報を取得するようにする。そしてLocation-Specificな値を返すようにする。Location-Specificな情報を記述した後、コード変更の必要性が大幅に削減される:

class DefaultConfig() extends ChiselConfig { 
  val top:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case Depth => site(Queue_type) match { 
        case 'iq' => 20 
        case 'lsq' => 10 
      } 
      case Sets => site(Cache_type) match { 
        case 'i' => 128 
        case 'd' => 512 
      } 
      case Ways => site(Cache_type) match { 
        case 'i' => 2 
        case 'd' => 4 
      } 
      case Width => 64 
      // since any module querying Width should return 64, the name should NOT be unique to modules 
      case ECC => site(Location) match { 
        'incore' => false 
        'incache' => true 
      } 
    } 
  } 
} 
class Tile (val params:Parameters) extends Module { 
  val core = Module(new Core,{Location => 'incore'}) 
  // we can give core and its child modules a location identifier 
 
  val cacheparams = params.alter({Location => 'incache'}) 
  // we can give both caches and all their child modules a location identifier 
  val icache = Module(new ICache)(cacheparams) 
  val dcache = Module(new DCache)(cacheparams) 
} 
class Core extends Module { 
  val iq = Module(new IQ) 
  val lsq = Module(new LSQ) 
  ... 
} 
class IQ extends Module { 
  val depth = params(Depth) 
  val mem = Module(new Memory, {Size = depth}) 
  // in some cases, using copy/alter is preferred instead of \code{site} (see Design Heuristics for more details) 
  ... 
} 
class LSQ extends Module { 
  val depth = params(Depth) 
  val mem = Module(new Memory, {Size = depth}) 
  ... 
} 
class ICache extends Module { 
  val sets = params(Sets) 
  val ways = params(Ways) 
  val mem = Module(new Memory,{Size => sets*ways}) 
} 
class DCache extends Module { 
  val sets = params(Sets) 
  val ways = params(Ways) 
  val mem = Module(new Memory, {Size => sets*ways}) 
} 
class Memory extends Module { 
  val size = params(Size) 
  val ecc = params(ECC) 
}

3.5 派生パラメータ(Derivative Parameters)

pict

他のトップレベルパラメータからパラメータを派生させるためには、here機能を使用してパラメータ値を複製する機能を使用できる。

図8では、ROBは常に物理レジスタの数とアーキテクチャレジスタの数の差分の4/3が定義される。これをMyConfig.topで表現するためには、以下のように実装できる:

case object NUM_arch_reg extends Field[Int] 
case object NUM_phy_reg extends Field[Int] 
case object ROB_size extends Field[Int] 
class DefaultConfig() extends ChiselConfig { 
  val top:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case NUM_arch_reg => 32 
      case NUM_phy_reg => 64 
      case ROB_size => 4*(64-32)/3 
  } 
}

しかし、もし後で物理レジスタの数を増やすとなると、ROBサイズの制約を常に思い出して更新する必要がある。この潜在的な間違いを防ぐためには、'here'の機能を使用して同じグループのパラメータの値を取得することができる:

class DefaultConfig() extends ChiselConfig { 
  val top:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case NUM_arch_reg => 32 
      case NUM_phy_reg => 64 
      case ROB_size => 4*(here(NUM_phy_reg) - here(NUM_arch_reg))/3 
  } 
}

3.6 パラメータのリネーミング(Renaming Parameters)

pict

図9. 前の値に従って、パラメータをリネームしたり、コードに従って値を変えるためには、upのメカニズム使ってパラメータオブジェクトの親の値を参照することができる。

図9では、cacheモジュールはsetsパラメータを参照する。しかし、Tileはic_setsdc_setsをパラメータを持っている。パラメータをリネームするためには、親の値と読み込み、子供のパラメータオブジェクトを変更することができる:

class Tile extends Module { 
  val ic_sets = params(Ic_sets) 
  val ic = Module(new Cache,{case Sets => ic_sets}) 
  val dc_sets = params(Ic_sets) 
  val dc = Module(new Cache,{case Sets => dc_sets}) 
    ... 
}

他の方法として、Parameters.alterメソッド中の'up'メカニズムを使用して親のモジュールのParameterオブジェクトを参照することができる:

class Tile extends Module { 
  val ic_params = params.alter( 
    (pname,site,here,up) => pname match { 
      case Sets => up(Ic_sets) 
    } 
  ) 
  val ic = Module(new Cache)(ic_params) 
  ... 
}

一般的に、'up'メカニズムは冗長になるため使用するべきではない。しかし、3つの中心的なメカニズム(up, site, here)にアクセスできるので、全ての変更にParameter.alterメソッドが含まれている可能性があるため、親が子のParameterオブジェクトを大幅に変更している場合に役に立つ。

4. 外部インタフェース

これまで、本ドキュメントはパラメータをトップレベルクラス(ChiselConfig)で変更する方法について説明してきた。しかし実際に複数のC++Verilogのデザインを生成する場合、私たちはこれらのパラメータを手動で変更する必要がある。

デザインの制約(パラメータの範囲、依存、制約)を表現する場合、特定のデザインの実際のインスタンスを有効なデザインスペースの表現から分離する場合に好まれる。

このような理由から、ChiselはKnobと呼ばれるデザインスペースを俯瞰するために特別に作成されたパラメータの概念をベースとした機能が存在する。本章では、Knobとその使用方法、オブジェクトダンプ、パラメータ・Knobへの制約の追加、Chiselコンパイラの2種類の実行モード: --configCollect--configInstanceについて説明する。

4.1 Knob

ジェネレータには固定されたいくつかのパラメータと、生成される特定の設計ポイントを指示するものがある。Knobと呼ばれるこれらのジェネレータレベルのパラメータには、外部プログラムとユーザーが簡単に値を上書きできるように、追加のキーと値のマッピングが存在する。

KnobはChiselConfigのサブクラスのトップ定義にのみインスタンスすることができる。

package example 
class MyConfig extends ChiselConfig { 
  val topDefinitions:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case NTiles => Knob('NTILES') 
      case .... => .... // other non-generator parameters go here 
    } 
  } 
  override val knobValues:Any=>Any = { 
    case 'NTILES' => 1 // generator parameter assignment 
  } 
}

NTilesがtopDefinitions内でマッチした場合、Knob('NTILES')が返される。内部では、ChiselはMyConfig.knobValuesからNTILES探索し、1が返される。2.5節で説明したように、特定のconfigを使ってGeneratorを実行するためにはフラグが必要である:

sbt run ... --configInstance example.MyConfig

新しいデザインで2つのタイルをインスタンスしたい場合: Scalaクラスの継承とknobValuesメソッドのオーバーライトを使用する:

package example 
class MyConfig2 extends MyConfig { 
  override val knobValues:Any=>Any = { 
    case 'NTILES' => 2 // will generate new design with 2 tiles 
  } 
}

どちらのクラスもソースコード中に共存することができる、したがってどちらのデザインもコマンドライン中からインスタンスすることができる。2つのタイルを含んだ新しいデザインは、以下のようにして呼び出すことができる:

sbt run ... --configInstance example.MyConfig2

4.2 ダンプ

Chiselよりも下流では、他のツールが特定のparameter/Knobアサインの情報が必要になる場合がある。そうであれば、Knob/valueをダンプオブジェクトに渡すことにより、名前とその値をファイルに書き出し、そしてKnob/valueを返す:

package example 
class MyConfig extends ChiselConfig { 
  val topDefinitions:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case Width => Dump('Width',64) // will return 64. Requires naming the parameter as the 1st argument 
      case NTiles => Dump(Knob('NTILES')) // will return Knob('NTILES'), no name needed 
    } 
  } 
  override val knobValues:Any=>Any = { 
    case 'NTILES' => 1 // generator parameter assignment 
  } 
}

ダンプその値は*.knbファイルに書き出される。書き出すディレクトリは--targetDirパスで指定できる。

4.3 制約

外部のプログラムやユーザがコンフィグレーションのknobValueメソッドを上書きしたい場合があり、Knobに有効な範囲での値を定義するメカニズムを定義されている。ChiselConfigではtopConstraintsにより他のメソッドを上書きすることができる:

package example 
class MyConfig extends ChiselConfig { 
  val topDefinitions:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case NTiles => Knob('NTILES') 
    } 
  } 
  override val topConstraints:List[ViewSym=>Ex[Boolean]] 
    = List( { ex => ex(NTiles) >  0 }, 
            { ex => ex(NTiles) <= 4 }) 
  override val knobValues:Any=>Any = { 
    case 'NTILES' => 1 // generator parameter assignment 
  } 
}

もし誰かがデザインを以下のようにしてインスタンスしようとすると、エラーが発生する:

package example 
class BadConfig extends ChiselConfig { 
  override val knobValues:Any=>Any = { 
    case 'NTILES' => 5 // would violate our constraint, throws an error 
  } 
} 
 
// throws 'Constriant failed' error 
sbt run ... --configInstance example.BadConfig

パラメータの制約メソッドを呼び出すことで、トップレベルだけでなく、制約はデザイン中のどこにで組み込むことができる:

package example 
class MyConfig extends ChiselConfig { 
  val topDefinitions:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case NTiles => Knob('NTILES') 
    } 
  } 
  override val knobValues:Any=>Any = { 
    case 'NTILES' => 1 // generator parameter assignment 
  } 
} 
class Tile extends Module { 
  params.constrain( ex => ex(NTiles) >  0 ) 
  params.constrain( ex => ex(NTiles) <= 4 ) 
} 
object Run { 
  def main(args: Array[String]): Unit = { 
    chiselMain.run(args, () => new Tile()) 
  } 
} 
 
sbt runMain example.Run ... --configInstance example.MyConfig

最後に、もしデザイナがデザインの制約を知りたければ、Chiselに--configCollect project_name.config_nameオプションを追加すればよい。これにより--targetDirパスのディレクトリにすべての制約を*.cstファイルとしてダンプする:

sbt runMain example.Run ... --configCollect example.MyConfig --targetDir <path>

5. デザインヒューリスティクス

TODO