Making Models
This document follows the 2_making_models.ipynb tutorial.
Basic Components
Models can be created by subclassing the ComponentModel
class. Models estimate the energy, area, and leakage power of a component. Each model
requires the following:
component_name: The name of the component. This may also be a list of components if multiple aliases are used.priority: This is used to break ties if multiple models support a given query.A call to
super().__init__(area, leak_power, subcomponents). This is used to initialize the model and set the area and leakage power.
Models can also have actions. Actions are functions that return a tuple of (energy,
latency) for a specific action. For the TernaryMAC model, we have an action called
mac that returns the energy and latency of a ternary MAC operation. The
action() decorator makes this function visible as an
action. The function should return (energy_in_Joules, latency_in_seconds).
Models can also be scaled to support a range of different parameters. For example,
the TernaryMAC model can be scaled to support a range of different technology nodes.
This is done by calling the self.scale function in the __init__ method of the
model. The self.scale function takes the following arguments:
parameter_name: The name of the parameter to scale.parameter_value: The value of the parameter to scale.reference_value: The reference value of the parameter.area_scaling_function: The scaling function to use for area. UseNoneif no scaling should be done.energy_scaling_function: The scaling function to use for dynamic energy. UseNoneif no scaling should be done.latency_scaling_function: The scaling function to use for latency. UseNoneif no scaling should be done.leak_scaling_function: The scaling function to use for leakage power. UseNoneif no scaling should be done.
Note: Area, Energy, Latency, and Leak Power are always in alphabetical order in the function arguments.
Many different scaling functions are defined and available in
hwcomponents.scaling.
from hwcomponents import ComponentModel, action
from hwcomponents.scaling import (
tech_node_area,
tech_node_energy,
tech_node_leak,
tech_node_latency
)
class TernaryMAC(ComponentModel):
"""
A ternary MAC unit, which multiplies two ternary values and accumulates the result.
Parameters
----------
accum_n_bits : int
The width of the accumulator in bits.
tech_node : int
The technology node in meters.
Attributes
----------
accum_n_bits : int
The width of the accumulator in bits.
tech_node : int
The technology node in meters.
"""
component_name: str | list[str] = 'TernaryMAC'
""" Name of the component. Must be a string or list/tuple of strings. """
priority = 0.3
"""
Priority determines which model is used when multiple models are available for a
given component. Higher priority models are used first. Must be a number between 0
and 1.
"""
def __init__(self, accum_n_bits: int, tech_node: int):
# Provide an area and leakage power for the component. All units are in
# standard units without any prefixes (Joules, Watts, meters, etc.).
super().__init__(
area=5e-12 * accum_n_bits,
leak_power=1e-3 * accum_n_bits
)
# The following scales the tech_node to the given tech_node node from 40nm.
# The scaling functions for area, energy, and leakage are defined in
# hwcomponents.scaling. The energy scaling will affect the functions decorated
# with @action.
self.tech_node = self.scale(
"tech_node",
tech_node,
40e-9,
tech_node_area,
tech_node_energy,
tech_node_latency,
tech_node_leak,
)
self.accum_n_bits = accum_n_bits
# Raising an error says that this model can't estimate and other models instead
# should be used instead. Good error messages are essential for users debugging
# their designs.
assert 4 <= accum_n_bits <= 8, \
f'Accumulation number of bits {accum_n_bits} outside supported range [4, 8]'
# The action decorator makes this function visible as an action. The
# function should return a tuple of (energy, latency).
@action
def mac(self, clock_gated: bool = False):
"""
Returns the energy and latency to perform a ternary MAC operation.
Parameters
----------
clock_gated : bool
Whether the MAC is clock gated during this operation.
Returns
-------
(energy, latency)
The energy in Joules and latency in seconds for a ternary MAC operation.
"""
self.logger.info(f'TernaryMAC Model is estimating '
f'energy for mac_random.')
if clock_gated:
return 0.0, 0.0
return 0.002e-12 * (self.accum_n_bits + 0.25), 0.0
mac = TernaryMAC(accum_n_bits=8, tech_node=16e-9) # Scale the TernaryMAC to 16nm
e, l = mac.mac()
print(f'TernaryMAC energy is {e:.2e}J (latency {l:.2e}s). Area is {mac.area:.2e}m^2. Leak power is {mac.leak_power:.2e}W')
Scaling by Number of Bits
Some actions may depend on the number of bits being accessesed. For example, you may
want to charge for the energy and latency per bit of a DRAM read. To do this, you can
use the bits_per_action argument of the action()
decorator. This decorator takes a string that is the name of the parameter to scale by.
For example, we can scale the energy and latency of a DRAM read by the number of bits
being read. In this example, the DRAM yields width bits per read, so energy and
latency are scaled by bits_per_action / width.
class LPDDR4(ComponentModel):
"""
LPDDR4 DRAM energy model.
"""
component_name = ["DRAM", "dram"]
priority = 0.3
def __init__(self):
super().__init__(area=100e-3, leak_power=1e-3)
self.width = 1
# If read() is called with a different bits_per_action, the energy will be scaled
# by the number of bits.
@action(bits_per_action="width")
def read(self) -> float:
"""
Returns the energy to read data from the DRAM.
Parameters
----------
bits_per_action : int
The number of bits to read.
Returns
-------
float
The energy to read the data in Joules.
"""
return 8e-12, 0
lpddr4 = LPDDR4()
energy1, latency1 = lpddr4.read(bits_per_action=1)
energy50, latency50 = lpddr4.read(bits_per_action=50)
print(f"Read energy for one bit: {energy1}")
print(f"Read energy for fifty bits: {energy50}")
print(f"Read latency for one bit: {latency1}")
print(f"Read latency for fifty bits: {latency50}")
Compound Models
We can create compound models by combining multiple component models. Here, we’ll show
the SmartBufferSRAM model from the hwcomponents-library package.This is an SRAM
with an address generator that sequentially reads addresses in the SRAM.
We’ll use the following components:
A SRAM buffer
Two registers: one that that holds the current address, and one that holds the increment value
An adder that adds the increment value to the current address
One new functionality is used here. The subcomponents argument to the
ComponentModel constructor is used to register
subcomponents.
The area, energy, latency, and leak power of subcomponents will NOT be scaled by the
component’s area_scale, energy_scale, latency_scale, and
leak_power_scale; if you want to scale the subcomponents, multiply the
subcomponents’ area_scale, energy_scale, latency_scale, and
leak_power_scale by the desired scale factor.
from hwcomponents_cacti import SRAM
from hwcomponents_library import AladdinAdder, AladdinRegister
from hwcomponents import ComponentModel, action
import math
class SmartBufferSRAM(ComponentModel):
"""
An SRAM with an address generator that sequentially reads addresses in the SRAM.
Parameters
----------
tech_node: The technology node in meters.
width: The width of the read and write ports in bits. This is the number of bits
that are accssed by any one read/write. Total size = width * depth.
depth: The number of entries in the SRAM, each with `width` bits. Total size =
width * depth.
n_rw_ports: The number of read/write ports. Bandwidth will increase with more
ports.
n_banks: The number of banks. Bandwidth will increase with more banks.
Attributes
----------
sram: The SRAM buffer.
address_reg: The register that holds the current address.
delta_reg: The register that holds the increment value.
adder: The adder that adds the increment value to the current address.
"""
component_name = ["smart_buffer_sram", "smartbuffer_sram", "smartbuffersram"]
priority = 0.3
def __init__(
self,
tech_node: float,
width: int,
depth: int,
n_rw_ports: int=1,
n_banks: int=1,
):
self.sram: SRAM = SRAM(
tech_node=tech_node,
width=width,
depth=depth,
n_rw_ports=n_rw_ports,
n_banks=n_banks,
)
self.address_bits = max(math.ceil(math.log2(depth)), 1)
self.width = width
self.address_reg = AladdinRegister(width=self.address_bits, tech_node=tech_node)
self.delta_reg = AladdinRegister(width=self.address_bits, tech_node=tech_node)
self.adder = AladdinAdder(width=self.address_bits, tech_node=tech_node)
# If there are subcomponents, we can omit the leakage power and area in the
# super constructor. If we pass them, their values will be added to the total.
super().__init__(subcomponents=[
self.sram,
self.address_reg,
self.delta_reg,
self.adder,
])
@action(bits_per_action="width", pipelined_subcomponents=True)
def read(self) -> float:
"""
Returns the energy consumed by a read operation in Joules.
Parameters
----------
bits_per_action: int
The number of bits to read.
Returns
-------
float
The energy consumed by a read operation in Joules.
"""
# Can omit the return value if there are subcomponents; return value is assumed
# to be sum of subcomponents. Return something to add it to the sum.
self.sram.read(bits_per_action=self.width)
self.address_reg.read()
self.delta_reg.read()
self.adder.add()
@action(bits_per_action="width", pipelined_subcomponents=True)
def write(self) -> float:
"""
Returns the energy consumed by a write operation in Joules.
Parameters
----------
bits_per_action: int
The number of bits to write.
Returns
-------
float
The energy consumed by a write operation in Joules.
"""
# Can omit the return value if there are subcomponents; return value is assumed
# to be sum of subcomponents. Return something to add it to the sum.
self.sram.write(bits_per_action=self.width)
self.address_reg.write()
self.delta_reg.read()
self.adder.add()
smartbuffer_sram = SmartBufferSRAM(
tech_node=16e-9,
width=32,
depth=1024,
n_rw_ports=1,
n_banks=1,
)
read_energy, read_latency = smartbuffer_sram.read(bits_per_action=32)
write_energy, write_latency = smartbuffer_sram.write(bits_per_action=32)
print(f'Read energy: {read_energy} J', )
print(f'Write energy: {write_energy} J')
print(f'Read latency: {read_latency}s', )
print(f'Write latency: {write_latency}s')
print(f'Area: {smartbuffer_sram.area} m^2')
print(f'Leak power: {smartbuffer_sram.leak_power} W')
The latency of subcomponents is generally summed. However, if the subcomponents are
pipelined for a given action, then the pipelined_subcomponents argument to the
action() decorator should be set to True. This will cause
the latency of the action to be the max of the latency returned and all subcomponent
latencies.
Installing Models and Making them Globally Visible
An example model is provided in the notebooks/model_example directory, which can be
installed with the following command:
cd notebooks/model_example
pip3 install .
The README.md file in the notebooks/model_example directory contains information
on how to make models installable. Keep the following in mind while you’re changing the
model:
The model name should be prefixed with
hwcomponents_. This allows HWComponents to find the model when it is installed.The
__init__.pyfile should import all Model classes that you’d like to be visible to HWComponents.If you’re iterating on an model, you can use the
pip3 install -e .command to install the model in editable mode. This allows you to make changes to the model without having to reinstall it.