Chisel高级参数化详解
介绍
本文为Chisel高级参数库手册。关于Chisel更多通用信息请参考Getting
Started文档。
随着硬件设计的复杂度的不断提高,模块化对于验证和复用都是非常重要的。Chisel的主要应用案例就是描述各种高度可配置的硬件生成器,我们很快意识到传统的参数化方式迫使设计的源代码非常脆弱,并且限制了组件的重用。
高级参数化
每个Chisel
Module有一个Parameters类的成员参数,其提供在模块之间传递参数的机制。
本节描述以下这些特征:
(1) Parameters类及其相关的方法/成员;
(2) 基本使用模型;
(3) 语法糖;
(4) 向外部用户/程序暴露参数的模板代码; (5) Views(site, here,
up)的高级功能; ## 类和方法
Parameters有以下这些基本方法:
1
2
3
4
5
6
7
8class Parameters {
// 返回类型T的一个值
def apply[T](key:Any):T
// 返回一个新的Parameters类
def alter(mask:(Any,View,View,View)=>Any):Paramters
// 返回一个模块的Parameters实例
def params:Parameters
}
View是一个只包含一个基本方法的类:
1
2
3
4class View {
// 返回类型T的一个值
def apply[T](key:Any):T
}
Parameters有一个工厂对象,其包含一个基本的方法:
1
2
3
4object Parameters {
// 返回一个空的Parameters实例
def empty:Parameters
}
Module工厂对象有一个附加的apply方法:
1
2
3
4object Module {
// 返回一个新的类型T的Module,如果_p!=None,则由一个Parameters实例初始化
def apply[T<:Module](C: => T)(implicit _p: Option[Parameters] = None):T
}
基本使用模型
这个例子示范了最简单的用法: (1)查询参数; (2) 改变Parameters对象; (3)
传递一个Parameters对象到一个Module。
1
2
3
4
5
6
7
8
9
10class 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_paramters)))
}
}
在Module
Tile中,params成员被查询,通过调用Parameters.apply传递key并返回value类型。
在Top中,通过调用Parameters.empty创建了一个空的parameters;然后通过(Any,
View, View, View) =>
Any函数修改参数值并返回一个新的Parameters实例,并赋值给tile_parameters。
将tile_parameters包装在Some:Option[Parameters]之后,当其被传递给chiselMain时,它会被作为第二个参数传递给Module对象。
语法糖: Field[T]
一个简单的例子:
要求返回类型Int必须作为参数传给apply方法;否则Scala编译器会抛出错误:
1
2
3class Tile extends Module {
val width = params[Int]('width')
}
如上所示的代码示例为一种参数查询的方式,还有一种方式如下所示,可以为每个key创建一个case
object,该对象继承自Field[T],并直接传递给params的apply方法。由于Field包含了返回类型信息,所以类型并不需要被传递:
1
2
3
4case object Width extends Field[Int]
class Tile extends Module {
val width = params(Width)
}
文档的剩下内容,假设每个查询的key都是一个继承自Field[T]的case类。
语法糖: Passing & Altering
当具有模块层级结构时,这些Parameters对象会在父模块和子模块之间传递。如果程序员指定,这些对象可以在实例化子对象之前被拷贝,修改。
当发生修改时,Chisel会在内部拷贝存在的key/value映射链,并将提供的key/value映射添加到链的底部(译者注:
类似于JS中的原型链)。
当进行一次查询时,会首先查询链的底部key/value映射。如果没有匹配,则会查询链上的下一级key/value映射,以此类推。如果查询达到链的顶部还没有匹配,则Chisel会触发一个ParameterUndefinedExpection。
当实例化一个子模块时,父模块可以以两种方式传递它的Parameters对象:
1.
给Module工厂方法传递第二个参数,即包装在Option[Parameters],显式地传递其Parameters对象至子模块:
1
2
3
4
5class Tile extends Module {
val width = params(Width)
val core = Module(new Core)(Some(params))
// 显示地传递Tile的参数给Core
}
- 隐式地将其Parameters对象传递给子模块:
1
2
3
4
5class Tile extends Module {
val width = params(Width)
val core = Module(new Core)
// 隐式地传递Tile的参数给Core
}
如果父模块想要拷贝或修改子模块的字典,父模块有两种方法来完成:
1.
向Module工厂方法提供一个偏函数映射作为一个参数。内部地,Chisel会拷贝父模块的Parameters对象并进行修改:
1
2
3
4
5class Tile extends Module {
val width = params(Width)
val core = Module(new Core, {case Width => 32})
// 向Module工厂方法提供偏函数来改变Core的code{Parameters}对象
}
- 调用Parameters.alter方法,该方法会返回一个新的Parameters对象。这种方法让程序员可以访问新的Parameters对象,还能够使用site,here,up,请看2.6,2.7,2.8:
1
2
3
4
5
6class 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))
// 使用Parameters.alter方法来返回一个修改过的Parameters对象。只有当需要site,here或up等机制时才使用。
}
1 | class Tile extends Module { |
ChiseConfig & Boilerplate
Chisel配置顶层参数的机制是通过一个ChiselConfig对象实现的。ChiselConfig.topDefinitions包含最高层的参数定义,如下所示:
1
2
3
4case 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,且设计不能用Module工厂方法包裹起来。这样的原因是为了针对已经存在的设计而保留的向后兼容性,未来我们会修复这个问题的。
如下就是一个调用chiselMain.run的例子:
1
2
3
4
5object Run {
def main(args: Array[String]): Unit = {
chiselMain.run(args, () => new Tile())
}
}
为了用特定的ChiselConfig来实例化设计,可以简单地在调用Chisel编译器时使用--configInstance project_name.configClass_name参数。
使用site
为了帮助设计者表达参数之间传递的关系,我们添加了site机制。为了理解它的功能,从概念上记住,一个被查询的Module的参数成员首先会查看其所在的key/value映射链的最底部的key/value映射。如果不匹配,查询会向上寻找。
假设我们有一些如下形式的模块:
1
2
3
4
5
6
7
8class Core extends Module {
val data_width = params(Width)
...
}
class Cache extends Module {
val line_width = params(Width)
...
}
这里有两个相同的查询参数Width,但是对于这个例子,它们有不同的语义。在Core中,Width表示字长,而在Cache中,Width表示cache
line的宽度。我们希望能够简单地做一个参数查询响应机制。
site机制允许链中间位置的key/value映射可以从链的底部开始进行查询。
看下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class DefaultConfig extends ChiselConfig {
val top:World.TopDefs = {
(pname,size,here) => pname match {
case With => 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'})
}
使用here
如下图所示,如果参数是同一级key/value映射链中其他参数的函数表达式,该参数并不想复制一个值,因为赋予一个新值需要多处变化。那么,它可以通过使用here机制来查询同一层级的key/value映射:1 | class Tile extends Module { |
使用up
up机制允许用户查询父级key/value映射。这等同于直接调用Parameters.apply,但可以在Parameters.alter中完成。具体请看3.6节。
示例
所有参数化方案都需要遵循以下三个准则: (1)
所有可查找到的参数暴露在顶层;
(2) 评估不同节点时源代码绝对不能改变;
(3) 添加新的参数时尽量不用改变源代码。
本章在介绍完每个例子之后,我们会提出最简单的参数化方案以支持期望的设计空间,且不违反三个准则中的任意一个。随着例子的复杂性提高,最简单的设计方案也会随着改变,直到我们使用这里介绍的高级参数化方案。
简单参数
在这个设计中,我们只改变core和cache的参数。最直接的参数化方案就是通过Tile构造函数参数来传递所有的参数。这些值然后会被传递给Core和Cache,通过它们各自的构造函数完成传递:
1
2
3
4
5
6
7
8class Tile(val fpu:Boolean, val ic_set:Int, val ic_way: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 {...}
当探索我们的参数空间时,没有源代码被修改,并且所有的可查找参数暴露在顶层。此外,添加一个新的参数,由于这个例子简单,我们的代码只需要很少的改动。
不相交参数集合
在下一个设计中,我们设计一个芯片,其可以实例化不同的core,每个core有自己的一组参数。如果我们使用简单的解决方案,Tile的构造函数的参数会非常多,因为它必须为所有可能的core包含所有的参数。
有一个更好的办法就是把参数集合成一个配置对象。比如,我们可以把所有的BigCore参数集合到一个BigCoreConfig的case
class中,把所有的SmallCore的参数集合到一个SmallCoreConfig的case类中,它们都继承自CoreConfig。此外,我们让Cache和Tile在它们的构造函数中分别接受CacheConfig和TileConfig。
1
2
3
4
5
6
7
8
9
10
11
12
13
14abstract 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))
...
}
位置无关参数
嵌套配置对象是非常脆弱的,这是因为嵌入配置对象的结构与模块的层次结构强相关。给定一个如上图所示的设计,我们假设其中包含BigCore的IQ和LSQ,以及icache和dcache,实例化一个Memory模块。Memory模块包含一个width参数,为了能让设计符合正确的预期功能,所有的Memory宽度必须设为同样的值。为了确保这个要求,代码可能会这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18case 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:QueeuConfig, 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 == td.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声明并不是可行的方法;这些声明对于强制基础设计要求是非常重要的。
配置对象的这个缺点引领我们向用户参数化方案靠近,即Parameters类型字典的拷贝/修改。我们使用这种key-value结构来存储模块的参数。
为了参数化上图的设计,我们隐式地传递Parameters对象,如果需要修改,则向Module工厂方法提供偏函数。回顾前面的ChiselConfig那一节,MyConfig类(继承自ChiselConfig)必须被传递给Chisel编译器,通过--configInstance选项来配置顶层参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47class 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
// 因为任何模块查询Width都会返回64,所以它的名字不应该对模块是唯一的
}
}
}
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)
}
}
尽管这种参数化方法相当冗长,但是它在添加参数时能表现出较好的扩展性,也不需要改变源代码,并允许单个参数,如Width改变所有的叶子模块。
特定位置参数
我们在前一节看到拷贝并修改一个Parameters对象会非常冗长。如果我们想要添加一个ECC参数到我们的Memory模块,而这个参数取决于Memory实例化的位置,这时候我们需要修改多个父模块的中的代码来重命名每个参数(如: ECC_icache => ECC)如上图所示,我们采用Parameters对象的site功能来获取特定位置信息,从而定制我们想要返回给特定位置值的值。在添加了特定位置信息之后,我们彻底减少了必须要改动的代码的数量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62class 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)
}
派生参数
如上图所示,我们总是希望我们的ROB可以是物理寄存器数量和体系结构寄存器数量差异的大小的4/3。如果我们在MyConfig.top中写明,可能就是这样的:
1
2
3
4
5
6
7
8
9
10
11
12case 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功能来查询同级的参数:
1
2
3
4
5
6
7
8
9
10class 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
}
}
}
重命名参数
上图所示,两个cache模块查询一个sets参数。然而,Tile有ic_sets和dc_sets参数。为了重命名这些参数,我们可以读取父模块的值并修改子模块中的Parameters对象:
1
2
3
4
5
6
7class 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机制来查询父模块的Parameters对象:
1
2
3
4
5
6
7
8
9class 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机制,因为它会变得更加冗余。但是,如果父模块对子模块的Parameters对象做了非常大的改动时,up机制会非常有用,因为所有的改变会包含在Parameters.alter方法中,这个方法可以访问所有三种机制(up, site, here).
外部接口
到目前为止,本文只描述了一些在顶层类(ChiselConfig)操作参数的机制。但是,为了能够实际生成多个C++或Verilog设计,我们需要手动改变这些参数。
我们还要明确设计的约束(参数范围,依赖,约束)以及把一个特定设计的实际实例从有效设计空间表达中分离出来。
带着这些动机,Chisel具一个额外的特征,其基于一个叫做Knobs的概念或者用于探索设计空间的特定参数。这一节将会介绍Knobs以及其使用方法,Dump对象,参数和Knob的约束添加,以及运行Chisel编译器的两种模式:
-configCollect和-configInstance.
Knobs
生成器会有一些参数是固定的,其他的则指示了生成的特定设计节点。这些生成器级的参数,称之为Knobs,其具有一个额外的key-value映射以允许外部程序和用户来轻易地重写它们的值。
Knobs只能在ChiselConfig的子类TopDefinitions中被实例化:
1
2
3
4
5
6
7
8
9
10
11
12
13package 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('NTLES')会被返回。内部地,Chisel会在MyConfig.knobValues中查找并返回1。2.5节所示,执行生成器时需要指定特定的config:
1
sbt run ... --configInstance example.MyConfig
假设我们想要实例化一个新的设计,该设计有两个tile:
可以简单地使用Scala的类继承并重写knobValues的值:
1
2
3
4
5
6
7package example
class MyConfig2 extends MyConfig {
override val knobValues:Any=>Any = {
case 'NTILES' => 2
// will generate new design with 2 tiles
}
}
注意,两个类都可以存在于源代码中,因此两个设计都可以通过命令行被实例化。对于有两个tile的新设计:
1
sbt run --configInstance exmaple.MyConfig2
Dump
顺着Chisel而下,其他的工具可能需要知道特定的参数/Knob赋值。如果需要,只要将Knob/value传给Dump对象,该对象会把name和value写入一个文件,然后返回Knob/value:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package 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
}
}
每个废弃的参数的name和value会被重写到一个*.knb文件,文件在--targetDir path指定的目录中。
约束
现在外部程序/用户可以很容易地重写一个配置的knobValue方法,我们提供了一种机制可以定义合法的Knobs范围。在ChiselConfig中,可以重写另一个称为topConstraint的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14package 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
}
}
现在,如果有人想要用以下的配置和命令实例化我们的设计,会无法通过:
1
2
3
4
5
6
7package example
class BadConfig extends ChiselConfig {
override val knobValues:Any=>Any = {
case 'NTILES' => 5
// would violate our constraint, throws an error
}
}1
2// throws 'Constriant failed' error
sbt run ... --configInstance example.BadConfig
约束可以在设计中的任何位置声明,并不只是在顶层,通过调用Parameters的constant方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package 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())
}
}
1 | sbt runMain example.Run ... --configInstance example.MyConfig |
最后,如果设计者想要知道设计约束,他们可以执行Chisel,用--configCollect project_name.config_name选项,这会把一系列约束打印到一个*.cst文件中,该文件的位置由--targetDir path指定:
1 | sbt runMain example.Run ... --configCollect example.MyConfig --targetDir <path> |
(完)
参考
[1] Bachrach,J.,Vo,H.,Richards,B.,Lee,Y.,Waterman, A., Avižienis,
Wawrzynek, J., Asanovic´ Chisel: Constructing Hardware in a Scala
Embedded Language in DAC ’12.
[2] Odersky, M., Spoon, L., Venners, B. Programming in Scala by
Artima.
[3] Payne, A., Wampler, D. Programming Scala by O’Reilly books.