Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 110 additions & 123 deletions accelforge/frontend/arch/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@
Self,
TypeVar,
)
import warnings
from pydantic import ConfigDict, PrivateAttr, field_validator, model_validator
from hwcomponents import (
ComponentModel,
get_models,
get_model,
)

from accelforge.util._migration import LATENCY_TO_THROUGHPUT_MIGRATION


# Verify the installed `hwcomponents` exposes the new `get_action_cost` API. If it
# can't be imported, the user is on an old hwcomponents that pre-dates the latency ->
Expand Down Expand Up @@ -87,14 +84,14 @@ class Action(EvalableModel):

latency: EvalsTo[int | float | None] = None
"""
Deprecated; use `throughput` instead. Setting this emits a deprecation warning and
auto-converts to `throughput = 1 / latency`.
Latency of one call of this action in seconds. Per-action latency is multiplied by
the component's latency_scale and the action's latency_scale.
"""

latency_scale: EvalsTo[int | float] = 1
"""
Deprecated; use `throughput_scale` instead. Setting this to a non-default value
emits a deprecation warning and auto-converts to `throughput_scale = 1 / latency_scale`.
The scale factor for latency of this action. Multiplies this action's latency by
this value.
"""

throughput: EvalsTo[int | float | None] = None
Expand All @@ -116,67 +113,6 @@ class Action(EvalableModel):
that are known to the component model, but not accelforge, such as clock
frequency."""

_n_calls: int | float = PrivateAttr(default=0)
"""
How many times this action is performed in the current Einsum. DO NOT SET THIS
VALUE. Used by the model.
"""

_model_running: bool = PrivateAttr(default=False)
""" Guard: True only while the model is actively populating ``_n_calls``. Reading
``n_calls`` outside of that window raises. Set via ``_set_n_calls``. """

@property
def n_calls(self) -> int | float:
"""
The number of times this action is performed in the current Einsum. When
accessed through the total_latency expression, returns the number of calls of
this action in the current Einsum.
"""
if not self._model_running:
raise RuntimeError(
f"Action {self.name!r}.n_calls is only valid while the model is "
f"running; access it from a Component.total_latency expression rather "
f"than directly."
)
return self._n_calls

def _set_n_calls(self, value: int | float) -> None:
self._n_calls = value
self._model_running = True

@model_validator(mode="before")
@classmethod
def _deprecate_latency_fields(cls, data):
if isinstance(data, dict):
if "latency" in data and not "throughput" in data:
l = data.pop("latency")
warnings.warn(
f"Setting `latency` on `{cls.__name__}` is deprecated; use "
f"`throughput` instead. Auto-converting `latency={l!r}` to "
f"`throughput`. Please update your input.\n\n"
f"{LATENCY_TO_THROUGHPUT_MIGRATION}",
DeprecationWarning,
stacklevel=2,
)
l = str(l).strip()
data["throughput"] = f"1 / ({l}) if ({l}) != 0 else float('inf')"
if "latency_scale" in data and not "throughput_scale" in data:
ls = data.pop("latency_scale")
warnings.warn(
f"Setting `latency_scale` on `{cls.__name__}` is deprecated; use "
f"`throughput_scale` instead (with the reciprocal). Auto-converting "
f"`latency_scale={ls!r}` to `throughput_scale`. Please update your "
f"input.\n\n{LATENCY_TO_THROUGHPUT_MIGRATION}",
DeprecationWarning,
stacklevel=2,
)
ls = str(ls).strip()
data["throughput_scale"] = (
f"1 / ({ls}) if ({ls}) != 0 else float('inf')"
)
return data

def _attributes_for_component_model(self) -> dict[str, Any]:
return {
**self.shallow_model_dump(),
Expand Down Expand Up @@ -330,30 +266,32 @@ class Component(Spatialable):
component's energy and latency.
"""

total_latency: str | int | float = "sum(a.n_calls / a.throughput for a in actions)"
total_latency: Any = None
"""
An expression representing the total latency of this component in seconds. This is
used to calculate the latency of a given Einsum. Special variables available are the
following:

- `min`: The minimum value of all arguments to the expression.
- `max`: The maximum value of all arguments to the expression.
- `sum`: The sum of all arguments to the expression.
- `actions`: The list of ``Action`` objects for this component. Action attributes
include n_calls as well as all generally-available action attributes.

Additionally, all component attributes are availble as variables, and all other
functions generally available in parsing. Note this expression is evaluated after
other component attributes are evaluated.

For example, the following expression takes the max bound across separate ports:
``max(a.n_calls / a.throughput for a in actions)``.
REMOVED. A component's latency is the sum, over its actions, of the action
count divided by the action's ``throughput``. Set per-action ``throughput``
to control latency. For a storage with separate read and write ports, set
``separate_read_write_ports: True`` instead of a max-over-ports expression.
"""

@field_validator("total_latency")
@classmethod
def _total_latency_removed(cls, v):
if v is not None:
raise ValueError(
"total_latency was removed. A component's latency is the sum over "
"its actions of the action count divided by the action's "
"throughput, so set per-action `throughput` attributes instead. "
"For a storage with separate read and write ports, set "
"`separate_read_write_ports: True` to model read and write latency "
"independently."
)
return v

latency_scale: EvalsTo[int | float] = 1
"""
Deprecated; use `throughput_scale` instead. Setting this to a non-default value
emits a deprecation warning and auto-converts to `throughput_scale = 1 / latency_scale`.
The scale factor for the latency of this component. Multiplies the calculated
latency of each action.
"""

throughput_scale: EvalsTo[int | float] = 1
Expand All @@ -363,28 +301,6 @@ class Component(Spatialable):
the scale factor is 2, then the final throughput is 2M actions/second.
"""

@model_validator(mode="before")
@classmethod
def _deprecate_latency_scale_on_component(cls, data):
if isinstance(data, dict) and "latency_scale" in data:
ls = data.pop("latency_scale")
warnings.warn(
f"Setting `latency_scale` on `{cls.__name__}` is deprecated; use "
f"`throughput_scale` instead (with the reciprocal). Auto-converting "
f"`latency_scale={ls!r}` to `throughput_scale`. Please update your "
f"input.\n\n{LATENCY_TO_THROUGHPUT_MIGRATION}",
DeprecationWarning,
stacklevel=2,
)
if "throughput_scale" in data:
raise ValueError(
f"Cannot specify both `latency_scale` and `throughput_scale` "
f"on `{cls.__name__}`. Drop the deprecated `latency_scale`."
)
ls = str(ls).strip()
data["throughput_scale"] = f"1 / ({ls}) if ({ls}) != 0 else float('inf')"
return data

n_parallel_instances: EvalsTo[int | float] = 1
"""
The number of parallel instances of this component. Increasing parallel instances
Expand Down Expand Up @@ -431,7 +347,7 @@ def get_component_class(self, trying_to_calculate: str = None) -> str:
"talk to hwcomponents, but was missing necessary attributes. If you do "
"not want to use hwcomponents component_models, ensure that area and "
"leak_power are set, as well as, for each action, "
f"energy and throughput are set.{extra_info}",
f"energy, throughput, and latency are set.{extra_info}",
source_field=f"{self.name}.component_class",
)
return self.component_class
Expand Down Expand Up @@ -783,6 +699,77 @@ def calculate_action_throughput(
)
return self

def calculate_action_latency(
self,
component_models: list[ComponentModel] | None = None,
in_place: bool = False,
) -> Self:
"""
Calculates the latency for each action by this component. Populates the
``<action>.latency`` field. Extends the ``component_modeling_log`` field with
log messages.

Parameters
----------
component_models : list[hwcomponents.ComponentModel] | None
The models to use for latency calculation. If not provided, the models
will be found with `hwcomponents.get_models()`.
in_place : bool
If True, the component will be modified in place. Otherwise, a copy will be
returned.

Returns
-------
Self
A copy of the component with the calculated latency for each action.
"""
if not in_place:
self: Self = self._copy_for_component_modeling()

messages = self.component_modeling_log

for action in self.actions:
messages.append(
f"Calculating latency for {self.name} action {action.name}."
)
if action.latency is not None:
latency = action.latency
messages.append(f"Setting {self.name} latency to {action.latency=}")
elif self._is_dummy():
latency = 0
messages.append("Component is dummy. Setting latency to 0.")
else:
self.populate_component_model(
component_models,
in_place=True,
trying_to_calculate=f"latency for action {action.name}",
)
latency = self.component_model.try_call_arbitrary_action(
action_name=action.name,
_return_estimation_object=True,
**{
**self._attributes_for_component_model(),
**action._attributes_for_component_model(),
},
)
messages.extend(latency.messages)
latency = latency.value.latency
if self.latency_scale != 1:
latency *= self.latency_scale
messages.append(f"Scaling {self.name} latency by {self.latency_scale=}")
if action.latency_scale != 1:
latency *= action.latency_scale
messages.append(
f"Scaling {self.name} latency by {action.latency_scale=}"
)
action.latency = latency
if action.latency < 0:
logging.warning(
f"Component {self.name} action {action.name} has negative latency: "
f"{action.latency}"
)
return self

def calculate_component_costs(
self,
component_models: list[ComponentModel] | None = None,
Expand Down Expand Up @@ -842,6 +829,7 @@ def calculate_component_costs(
self.calculate_area(component_models, in_place=True)
self.calculate_action_energy(component_models, in_place=True)
self.calculate_action_throughput(component_models, in_place=True)
self.calculate_action_latency(component_models, in_place=True)
self.calculate_leak_power(component_models, in_place=True)
if _use_cache:
_set_component_model_cache(cachekey, self)
Expand Down Expand Up @@ -1206,6 +1194,13 @@ class Memory(TensorHolder):
actions: EvalableList[TensorHolderAction] = MEMORY_ACTIONS
""" The actions that this `Memory` can perform. """

separate_read_write_ports: EvalsTo[bool] = False
"""
If True, reads and writes use independent ports: for latency, this memory is
treated as two components, "{name} (read)" and "{name} (write)", which may
overlap in time. If False, reads and writes are serialized.
"""

_n_physical: NoParse[int] = 1
"""
Number of physical units bound to this memory level.
Expand Down Expand Up @@ -1348,20 +1343,12 @@ class Network(Component, Leaf):

actions: EvalableList[Action] = NETWORK_ACTIONS

total_latency: str | int | float = (
"max(max_hops*actions['hop'].latency, max_link_traffic/actions['hop'].throughput)"
)
total_latency: None = None
"""
Models latency as either:
- *Latency-bound*, which means that the latency of the route with the most number of
hops dominate the overall communication latency.
- *Bandwidth-bound*, which means that the traffic over the most congested link
dominates the overall communication latency.

Keywords:
- `max_hops` returns the number of hops in the longest route.
- `max_link_traffic` returns the amount of traffic (in bits) over the most congested
link.
REMOVED. A network's latency is bandwidth-bound: the traffic over the most
congested link divided by the hop action's ``throughput``. Wind-up/down uses
max_hops * the hop action's ``latency``. Setting this attribute raises an
error.
"""

bits_per_value: EvalsTo[dict] = {}
Expand Down
7 changes: 7 additions & 0 deletions accelforge/frontend/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ class Model(EvalableModel):
If using spec to call mapper, leave this configuration as is. The mapper
will make necessary configurations.
"""

_use_new_latency_model: bool = False

def __init__(self, **kwargs):
use_new_latency_model = kwargs.pop("_use_new_latency_model", False)
super().__init__(**kwargs)
self._use_new_latency_model = use_new_latency_model
10 changes: 9 additions & 1 deletion accelforge/frontend/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ def calculate_component_costs(
area: bool = True,
energy: bool = True,
throughput: bool = True,
latency: bool = True,
leak: bool = True,
) -> "Spec":
"""
Expand Down Expand Up @@ -220,10 +221,12 @@ def calculate_component_costs(
Whether to compute and populate energy entries.
throughput : bool, optional
Whether to compute and populate throughput entries.
latency : bool, optional
Whether to compute and populate latency entries.
leak : bool, optional
Whether to compute and populate leak power entries.
"""
if not area and not energy and not throughput and not leak:
if not area and not energy and not throughput and not latency and not leak:
return self

models = hwcomponents.get_models(
Expand Down Expand Up @@ -277,6 +280,11 @@ def calculate_component_costs(
for a in c.actions:
orig_action = orig.actions[a.name]
orig_action.throughput = a.throughput
if latency:
c = c.calculate_action_latency(models)
for a in c.actions:
orig_action = orig.actions[a.name]
orig_action.latency = a.latency
if leak:
c = c.calculate_leak_power(models)
orig.leak_power = c.leak_power
Expand Down
Loading
Loading