FPGA開発日記

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

Chiselのパラメタライズについて調査(1. Advanced Parameterization Manualを読む)

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

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

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

1. イントロダクション

このドキュメントは、Chisel内で高度なパラメータライブラリを使用するためのマニュアルである。ハードウェア構築言語としてのChiselに関する一般的な情報については、Getting Startedのドキュメントを参照すること。

ハードウェア設計が複雑になるにつれて、メンテナンスと検証のためにモジュール性が必要になる。Chiselの主な使用事例は、多様で高度にパラメータ化されたハードウェアジェネレータを記述することであり、従来のパラメータ化手法は、脆弱性を設計のソースコードに強制し、コンポーネントの再利用を制限していた。

このドキュメントの概要は以下の通りである。第2章では、高度なパラメータ化メカニズムの基本的なオブジェクトとメソッド、およびそれを使用するために必要な定型文について説明する。第3章では、ますます複雑化する一連の設計パターンについて説明する。各例では、問題を解決する最も簡単なパラメータ化スキームを提案します。例が複雑になるにつれて、前述の高度なパラメータ化メカニズムに到達するまで、パラメータ化要件も同様になる。第4章では、ノブの概念と、設計制約とパラメータオブジェクトとの関係について説明する。最後に第5章では、高度なパラメータ化を使用する際に従うべき複数の設計ヒューリスティックについて説明します。

2. 高度なパラメタライズ

すべてのChiselのモジュールには、モジュール間でパラメータを渡すためのメカニズムを提供するクラスパラメータのメンバパラメータがあります。このセクションでは、以下の機能について説明します。

  1. Parametersクラスおよび関連するメソッド/メンバー
  2. 基本的な使用モデル。
  3. シンタックスシュガー;
  4. パラメータを外部ユーザ/プログラムに公開する定型文。
  5. Views経由の高度な機能 (site, here, up)

2.1 クラスとメソッド

パラメータクラスは、以下のベースメソッドを持っている。

class Parameters { 
  // T型の値を返す。
  def apply[T](key:Any):T 
 
  // 新しいParameterクラスを返す。
  def alter(mask:(Any,View,View,View)=>Any):Parameters 
 
  // モジュールのパラメータインスタンスを返す。
  def params:Parameters 
}

Viewは、ベースメソッドを含んでいるクラスである。

class View { 
  // T型の値を返す。
  def apply[T](key:Any):T 
}

Parametersは、1つの基本メソッドを含んでいるファクトリオブジェクトを持っている。

object Parameters { 
  // 空のパラメータインスタンスを返す。
  def empty:Parameters 
}

モジュールオブジェクトファクトリは、applyメソッドを持っている。

object Module { 
  // T型の新しいモジュールを返す。このモジュールは_p != Noneならば、パラメータで初期化されている。
  def apply[T<:Module](c: =>T)(implicit _p: Option[Parameters] = None):T 
}

2.2 基本的な使用モデル

以下の例は

  1. パラメータの問い合わせ(quering)
  2. パラメータオブジェクトの変更(altering)
  3. パラメータオブジェクトをモジュールに渡す

の基本的な使用方法の例である。

class Tile extends Module { 
  val width = params[Int]('width') 
} 
object Top { 
  val parameters = Parameters.empty 
  val tile_parameters = parameters.alter( (key,site,here,up) => { 
    case 'width' => 64 
  }) 
  def main(args:Array[String]) = { 
    chiselMain(args,()=>Module(new Tile)(Some(tile_parameters))) 
  } 
}

Tileモジュールでは、paramsメンバはキーの値とParameters.applyで呼ばれることによって、その値を返している。

Topモジュールでは、Parameters.empyが呼ばれることによって空のパラメータが作成され、(Any, View, View, View)=>Anyの関数にalterされる。この関数は新しいパラメータインスタンスを返し、tile_parametersに渡される。

tile_parametersSome:Option[parameters]にラッピングされた後、モジュールの2番目の引数に渡され、chiselMainに渡される。

2.3 シンタックスシュガー: Field[T]

単純な例ではapplyメソッドで返す型(Int)を付加しなければならない。これを除去するとコンパイラがエラーを返す:

class Tile extends Module { 
  val width = params[Int]('width') 
}

一方で、Field[T]を継承する形で各パラメータのcase objectを作成することができる。この場合は、paramsから直接applyメソッドを渡すことができる。これは、Fieldが返す方の情報を持っているからであり、それ以上型の情報を渡す必要がないからである。

case object Width extends Field[Int] 
class Tile extends Module { 
  val width = params(Width) 
}

以降のドキュメントでは、全ての問い合わせのキーは、Field[T]クラスの継承でcase classが作成されているものとする。

2.4 シンタックスシュガー: PassingとAltering

pict

図1. メモリのkey/valueチェインとフラットマップ

モジュールが階層化されていると、Parametersオブジェクトは親のモジュールkらこのモジュールへと渡される。これはプログラマによって指定され、これらのオブジェクトはインスタンスされた子供へとコピーされ変更される。

パラメータが変更されると、Chiselは内部で既存のチェインのkey/valueマッピングをコピーし、チェインの最後に新たなkey/valueマッピングとして接続する。クエリが評価されると、チェインの最後尾のkey/valueの値がクエリされる。もしそのクエリがマッチしなければ、クエリはチェイン中の次のkey/valueの値を評価していく。もしクエリがチェインのトップに到達し、それ以上マッチしなければChiselはParameterUndefinedException例外を発行する。

子供をインスタンスする場合、親はParameterを2つの方法で渡すことができる。

  1. パラメータオブジェクトを、子供の引数としてモジュールファクトリに明示的に渡すもの。Option[Parameters]:でラッピングする。
class Tile extends Module { 
  val width = params(Width) 
  val core = Module(new Core)(Some(params)) 
  // TileのパラメータをCoreに明示的に渡す。
}
  1. パラメータオブジェクトを子供に暗黙的に渡す。
class Tile extends Module { 
  val width = params(Width) 
  val core = Module(new Core) 
  // TileのパラメータをCoreに暗黙的に渡す。
}

もし親が子供の辞書にパラメータをコピー/変更したい場合、2種類の方法がある。

  1. PartialFunctionマッピングを使用して、引数をモジュールファクトリに渡す。内部的には、Chiselは親のパラメータオブジェクトをコピーして、AlterをApplyする。
class Tile extends Module { 
  val width = params(Width) 
  val core = Module(new Core,{case Width => 32}) 
  // PartialFuncitonをモジュールのファクトリコンストラクタに渡して、Coreの\code{Parameters}`を変更する。
}

2.Parameter.alter関数を使用して、新しいパラメータオブジェクトを返す。このアプローチでは、プログラマは新しいパラメータオブジェクトに置くセス氏、site, here, upを使用することができる。

class Tile extends Module { 
  val width = params(Width) 
  val core_params = params.alter( 
    (pname,site,here,up) => pname match { 
      case Width => 32 
    }) 
  val core = Module(new Core)(Some(core_params)) 
  // Parameter.alterを使用して、変換したパラメータオブジェクトを返す。site, here, upメカニズムが必要な時のみ使用する。
}

図1. で示したより複雑な例は、以下のようにして表現できる。

class Tile extends Module { 
  ... 
  val core = Module(new Core, {case FPU => true; case QDepth => 20; case Width => 64}) 
} 
class Core extends Module { 
  val fpu = params(FPU) 
  val width = params(Width) 
  val depth = params(Depth) 
  val queue = Module(new Queue,{case Depth => depth*2; case Width => 32}) 
} 
class Queue extends Module { 
  val depth = params(Depth) 
  val width = params(Width) 
  val mem = Module(new Memory,{case Size => depth * width}) 
} 
class Memory extends Module { 
  val size = params(Size) 
  val width = params(Width) 
}

2.5 ChiselConfigとBoilerPlate

Chiselのメカニズムで、とっぷれえbるのパラメータをChiselConfigオブジェクトを用いてシードする。ChiselConfig.topDefinitionsでは最高位のパラメータの定義を以下の構成で保持している。

case object Width extends Field[Int] 
class DefaultConfig extends ChiselConfig { 
  val topDefinitions:World.TopDefs = { 
    (pname,site,here) => pname match { 
      case Width => 32 
    } 
  } 
}

通常、デザインはchiselMain.applyを使用してデザインをインスタンスする。Chiselのパラメータメカニズムを使用して正確にChiselConfigをシードするためには、デザインがモジュールファクトリに囲まれていない状態でchiselMain.runを呼び出す必要がある。この理由は既存のデザインに対するバックワードの互換性を守るためであるが、この制約は将来解消する予定である。

chiselMain.runを呼び出す例である。

object Run { 
  def main(args: Array[String]): Unit = { 
    chiselMain.run(args, () => new Tile()) 
  } 
}

特定のChiselConfigを用いてデザインをインスタンスしたい場合、単純にChiselのコンパイラ--configInstance project_name.configClass_nameの引数を付けて呼び出せばよい。

2.6 siteの使用

pict

図2(a). site(Location)はCoreを返す。

pict

図2(b). site(Location)はCacheを返す。

パラメータ間の依存を表現したい場合、siteのメカニズムを使用することができる。この機能を理解するために、クエリ化されたモジュールのパラメータを最初に見たときに、チェイン中の最後尾のkey/valueマッピングが呼び出されることを思い出そう。もしマッチングしなければ、次のチェインへと移っていく。

以下のような、複数のモジュールがあったとしよう。

class Core extends Module { 
  val data_width = params(Width) 
  ... 
} 
class Cache extends Module { 
  val line_width = params(Width) 
  ... 
}

残念ながら、Widthのパラメータは名前は同一だが文法的には別の意味を持っている。Coreの中ではWidthはワードサイズを示しているが、Cache中ではキャッシュラインの意味を持っている。これらについて、簡単にパラメータを識別できるようになりたい。

siteのメカニズムを使用すれば、チェインの途中のkey/valueマッピングを使用することができる。

以下の例を考える。

class DefaultConfig extends ChiselConfig { 
  val top:World.TopDefs = { 
    (pname,site,here) => pname match { 
       case Width => site(Location) match { 
         case 'core' => 64 // data width 
         case 'cache' => 128  // cache line width 
       } 
    } 
  } 
} 
class Tile extends Module { 
  val core = Module(new Core, {case Location => 'core'}) 
  val cache = Module(new Cache, {case Location => 'cache'}) 
}

トップレベルでのkey/valueマッピングは、siteを使用してLocationのチェインを使用している。なんの値が返されるか('core'もしくは'cache')によって、トップレベルのkey/valueマッピングでは異なる値が返される(図2)。

2.7 hereの使用

pict

図3. 128 or 4の数値を直接使うのではなく、here(Sets)とhere(Ways)を使用することができる。

パラメータが、key/valueマッピングチェインの同一グループ中の他のパラメータに対する決定的な関数であれば、値を複製してしまうと、多くの変更が伴ってしまうため、それは避けたい。その代わりに、クエリが同一グループのkey/valueマッピングである場合に、hereを使用することができる:

class Tile extends Module { 
  val cache_params = params.alter( 
    (pname, site, here, up) => pname match { 
      case Sets => 128 
      case Ways => 4 
      case Size => here(Sets)*here(Ways) 
    }) 
  val cache = Module(new Cache)(cache_params) 
}

2.8 upの使用

upのメカニズムは、ユーザがkey/valueマッピングの親のグループをクエリしたいときに有効である。Parameters.applyを直接読んだものと同一であるが、さらにParameters.alterを読んだものと同じ意味になる。3.6章の例を参考にすること。