Chiselについて少しずつ勉強を進めているが、SystemVerilogと同様のことが実現できないと、マスターしたとは言えないだろう。
Verilogでも多く使われるパラメータ化、これをどのようにしてChiselで使用するのか、Rocket-Chipのデザインを見ても良く分からないので調査したい。
このあたりについて非常に細かく記述してある資料 Advanced Parameterization Manual を翻訳して調べてみた。 まずは簡単に翻訳した結果を掲載しておきたい。 今回は後半。前半はこちら。
3. 例
パラメータ化には3つの目標がある。
これから紹介する例について説明した後、3つの目標のいずれにも違反せずに、望ましいデザインスペースをサポートするもっとも簡単なパラメータ化の方法を示す。例が複雑になるにつれて、現在の高度なパラメータ化のメソッドに到達するまではもっとも単純なパラメータ化の方法も必要だ。
3.1 単純なパラメータ
シンプルなデザインでは、コア特有のパラメータや、キャッシュのためだけのパラメータなどが存在する。もっとも単純なパラメータ化の方法は、全てのパラメータをTileのコンストラクタの引数として渡すことだ。これらの値は、それぞれのコンストラクタを経由してCore
とCache
に渡される。
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 パラメータセットの分離
次のデザインでは、チップないに異なるコアをインスタンスし、それぞれで異なるパラメータを設定する。上記の単純なソリューションを適用すれば、Tileのコンストラクタに対して、パラメータの数だけ値を設定する必要がある(さらに、2コアからコア数が増えると、さらに設定する項目が多くなる!)
より良い解決章のためには、パラメータのグループをコンフィグレーションオブジェクトに分割することである。例えば、BigCore
に必要なすべてのパラメータをBigCoreConfig
caseクラスに設定し、SmallCore
のパラメータをSmallCoreConfig
クラスに設定し、どちらのクラスもCoreConfig
クラスの継承として定義する。さらに、キャッシュとTileのコンフィグのためにCacheConfig
とTileConfig
を用意して、コンストラクタに渡す。
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なパラメータ
ネストされたコンフィグレーションオブジェクトが脆弱である微妙な理由は、ネストされたコンフィグレーションオブジェクトがモジュールの改装をエンコードするからである。図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 パラメータ
前の節で見たように、パラメータオブジェクトをコピーして変更することは上古府である。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)
図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)
図9では、cacheモジュールはsetsパラメータを参照する。しかし、Tileはic_sets
とdc_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