Skip to content

Commit bcff540

Browse files
committed
Merge branch 'main' into hops
2 parents 0453ddc + 8d4aeac commit bcff540

11 files changed

Lines changed: 152 additions & 59 deletions

File tree

accelforge/frontend/arch/_flattened_arch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class FlattenedArch:
2929
`frontend`, this one is intentionally *not* an
3030
`EvalableModel`.
3131
"""
32+
3233
def __init__(self, nodes: list["Leaf"]):
3334
self.nodes = nodes
3435

accelforge/frontend/mapper/ffm.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ class FFM(EvalableModel):
6666
max_fused_loops_per_rank_variable: int = 1
6767
""" The maximum number of fused loops in a pmapping for a given rank variable. """
6868

69+
tiling_coarseness: float = 1
70+
"""
71+
When enumerating tile shapes, a tile shape is omitted if it is less than
72+
tiling_coarseness times the next-inner tile shape. Set to 1 to disable. For example,
73+
tiling_coarseness=2 means each enumerated tile shape must be at least 2x the
74+
next-inner tile shape. Recommended to use 1<=tiling_coarseness<=2
75+
explore_imperfect_temporal_loops=True and/or explore_imperfect_spatial_loops=True,
76+
to get good mappings without exploring too many tile shapes.
77+
"""
78+
6979
max_fused_loops: float | int = float("inf")
7080
""" The maximum total number of fused loops in a pmapping. """
7181

@@ -139,6 +149,9 @@ class FFM(EvalableModel):
139149
140150
Only "simple" rank variables (those appearing alone and not inside an expression in
141151
any tensor access) may have imperfect loop bounds.
152+
153+
See tiling_coarseness for how to reduce the number of tile shapes explored when this
154+
is set.
142155
"""
143156

144157
explore_imperfect_temporal_loops: bool = False
@@ -154,6 +167,9 @@ class FFM(EvalableModel):
154167
155168
Only "simple" rank variables (those appearing alone and not inside an expression in
156169
any tensor access) may have imperfect loop bounds.
170+
171+
See tiling_coarseness for how to reduce the number of tile shapes explored when this
172+
is set.
157173
"""
158174

159175
prioritize_reuse_of_unfused_tensors: bool = False
@@ -216,11 +232,3 @@ class FFM(EvalableModel):
216232
If set, append per-step runtime as JSON lines to this file. Used for ablation study
217233
measurements.
218234
"""
219-
220-
_metric_aggregator: Literal["sum", "prod", "any"] = "any"
221-
"""
222-
How to aggregate metrics together to determine whether one pmapping is better than
223-
another. "sum" means that the metrics are added together, "prod" means that the
224-
metrics are multiplied together, and "any" means that any metric being better than
225-
the other is enough to consider it non-dominated.
226-
"""

accelforge/mapper/FFM/_join_pmappings/join_pmappings.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@
3434
Compatibility,
3535
)
3636
from accelforge.mapper.FFM._pareto_df.df_convention import col2reservation
37-
from accelforge.util import _fillna_and__numeric_cast, parallel, delayed, oset
37+
from accelforge.util import (
38+
_fillna_and__numeric_cast,
39+
delayed,
40+
get_n_parallel_jobs,
41+
oset,
42+
parallel,
43+
)
3844

3945
logger = logging.getLogger(__name__)
4046

@@ -62,13 +68,16 @@ def log_total_time(self):
6268
logger.info(f"============================\n")
6369

6470

65-
def _apply_edp_columns(df: pd.DataFrame, metrics: Metrics) -> pd.DataFrame:
66-
if not (metrics & Metrics.ENERGY_DELAY_PRODUCT):
67-
return df
68-
if not (metrics & Metrics.ENERGY):
69-
del df["Total<SEP>energy"]
70-
if not (metrics & Metrics.LATENCY):
71-
del df["Total<SEP>latency"]
71+
def _apply_edp_columns(
72+
df: pd.DataFrame, metrics: Metrics, return_only_objectives: bool = False
73+
) -> pd.DataFrame:
74+
if metrics & Metrics.ENERGY_DELAY_PRODUCT:
75+
if not (metrics & Metrics.ENERGY):
76+
del df["Total<SEP>energy"]
77+
if not (metrics & Metrics.LATENCY):
78+
del df["Total<SEP>latency"]
79+
if return_only_objectives:
80+
df = df[[c for c in df.columns if col_used_in_pareto(c)]]
7281
return df
7382

7483

@@ -87,8 +96,8 @@ def __init__(
8796

8897
compare_to = compare_to.sort_values(by=compare_cols, ascending=False)
8998

90-
if len(compare_to) > 10:
91-
chosen_indices = np.round(np.linspace(0, len(compare_to) - 1, 10))
99+
if len(compare_to) > 100:
100+
chosen_indices = np.round(np.linspace(0, len(compare_to) - 1, 100))
92101
else:
93102
chosen_indices = np.round(np.arange(len(compare_to)))
94103

@@ -713,6 +722,28 @@ def grab_einsum_pmappings() -> (
713722
left = PmappingGroup.group(left, right_tensors)
714723
# print_time("Grouping")
715724

725+
# =============================================================================
726+
# If we're multiprocessing and the left side has fewer groups than the number of
727+
# processes, repeatedly split the largest group in half so that the merge work
728+
# below can fan out across all workers.
729+
# =============================================================================
730+
n_procs = get_n_parallel_jobs()
731+
n_groups = sum(len(v) for v in left.values())
732+
for _ in range(n_procs - n_groups):
733+
best_k, best_i, best_len = None, None, -1
734+
for k, vs in left.items():
735+
for i, (pg, _perm) in enumerate(vs):
736+
length = len(pg.mappings.data) if pg.mappings is not None else 0
737+
if length > best_len:
738+
best_len = length
739+
best_k, best_i = k, i
740+
if best_k is None or best_len < 2:
741+
break # nothing left worth splitting
742+
pg, perm = left[best_k][best_i]
743+
first, second = pg.split_in_half()
744+
left[best_k][best_i] = (first, perm)
745+
left[best_k].append((second, perm))
746+
716747
# ======================================================================
717748
# Remove dead tensors from left and right. This happens after grouping because
718749
# we only reserve space for shared tensors after they're dead (alive is handled

accelforge/mapper/FFM/_join_pmappings/pmapping_dataframe.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,24 @@ def copy(self, copy_df: bool = True) -> "PmappingDataframe":
772772
check_above_subset_below=False,
773773
)
774774

775+
def split_in_half(self) -> tuple["PmappingDataframe", "PmappingDataframe"]:
776+
mid = len(self.data) // 2
777+
half_total = self.n_total_pmappings / 2
778+
half_valid = self.n_valid_pmappings / 2
779+
first = self.update(
780+
data=self.data.iloc[:mid].copy(),
781+
skip_pareto=True,
782+
n_total_pmappings=half_total,
783+
n_valid_pmappings=half_valid,
784+
)
785+
second = self.update(
786+
data=self.data.iloc[mid:].copy(),
787+
skip_pareto=True,
788+
n_total_pmappings=half_total,
789+
n_valid_pmappings=half_valid,
790+
)
791+
return first, second
792+
775793
def limit_capacity(
776794
self,
777795
next_shared_loop_index: int = None,

accelforge/mapper/FFM/_join_pmappings/pmapping_group.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ def tensor_names(self) -> set[str]:
4747
def copy(self) -> "PmappingGroup":
4848
return PmappingGroup(self.compatibility, self.mappings.copy())
4949

50+
def split_in_half(self) -> tuple["PmappingGroup", "PmappingGroup"]:
51+
first_mappings, second_mappings = self.mappings.split_in_half()
52+
return (
53+
PmappingGroup(self.compatibility, first_mappings),
54+
PmappingGroup(self.compatibility, second_mappings),
55+
)
56+
5057
def __len__(self) -> int:
5158
return len(self.mappings)
5259

accelforge/mapper/FFM/_make_pmappings/make_pmappings.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,11 @@ def get_n_computes(spec: Spec, einsum_name: EinsumName | None = None) -> int:
7070
return sum(get_operation_space_size(spec.workload, e) for e in einsums)
7171

7272

73-
def get_per_tensor_size(
74-
workload: Workload, return_n_elements: bool = False
75-
) -> dict[TensorName, int]:
73+
def get_per_tensor_n_elements(workload: Workload) -> dict[TensorName, int]:
7674
sizes = {}
7775
for einsum in workload.einsums:
7876
for tensor in einsum.tensor_names:
7977
sizes[tensor] = get_tensor_size(workload, tensor)
80-
if not return_n_elements:
81-
sizes[tensor] *= einsum.tensor_accesses[tensor].bits_per_value
8278
return sizes
8379

8480

@@ -265,7 +261,7 @@ def get_memories_to_track(
265261
)
266262

267263
tensor_sizes = {}
268-
for tensor, size in get_per_tensor_size(spec.workload).items():
264+
for tensor, size in get_per_tensor_n_elements(spec.workload).items():
269265
scale = 1
270266
for einsum in spec.workload.einsums_with_tensor(tensor):
271267
if einsum.tensor_accesses[tensor].persistent:
@@ -292,9 +288,7 @@ def get_memories_to_track(
292288
.bits_per_value
293289
)
294290
effective_bpv = mem.bits_per_value.get(tensor, workload_bpv)
295-
usage += (
296-
tensor_sizes[tensor] * effective_bpv / (workload_bpv * mem.size)
297-
)
291+
usage += tensor_sizes[tensor] * effective_bpv / mem.size
298292

299293
if usage <= 1:
300294
ignored_resources.add(memory)

accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/make_tile_shapes.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -724,21 +724,47 @@ def _factorize_imperfect(n: int) -> np.ndarray:
724724
return np.array(sorted(oset(factors)))
725725

726726

727-
def get_possible_factor_sizes(n: int, imperfect: bool, inner_size: int) -> list[int]:
727+
@lru_cache(maxsize=10000)
728+
def get_possible_factor_sizes(
729+
outer_size: int, imperfect: bool, inner_size: int, coarseness: float = 1
730+
) -> list[int]:
728731
# Int to protect from numpy overflows
729-
n = int(n)
730-
inner_size = int(inner_size)
731-
if not imperfect:
732-
factors = _factorize(math.ceil(n / inner_size))
733-
return factors * inner_size
732+
outer_size, inner_size = int(outer_size), int(inner_size)
733+
734+
factors = set()
735+
n_tiles = set()
736+
737+
def _try_admit(n: int) -> bool:
738+
if not imperfect and outer_size % n != 0:
739+
return False
740+
if n > outer_size:
741+
return False
742+
743+
# This check: Avoid redundant tile shapes that lead to the same number of tiles,
744+
# which causes the same accesses (from same average shape) but worse latency &
745+
# memory usage (from worse long pole shape).
746+
cur_n_tiles = math.ceil(outer_size / n)
747+
if cur_n_tiles not in n_tiles:
748+
n_tiles.add(cur_n_tiles)
749+
# new_n calculation: Grab the smallest-possible tile shape that would get us
750+
# this number of tiles, which is the best (see above comment).
751+
new_n = math.ceil(outer_size / cur_n_tiles)
752+
factors.add(new_n)
753+
return True
754+
755+
n = inner_size
756+
while n <= outer_size:
757+
# If we successfully admit an n, don't try another n until we hit our coarseness
758+
# target.
759+
if _try_admit(n):
760+
n = max(n + 1, math.ceil(n * coarseness))
761+
else:
762+
n += 1
763+
764+
# One more in case coarseness jumped the max value
765+
_try_admit(outer_size)
734766

735-
factors = _factorize_imperfect(n)
736-
if n % inner_size == 0:
737-
perfects = _factorize(math.ceil(n / inner_size)) * inner_size
738-
assert all(
739-
f in factors for f in perfects
740-
), f"perfects: {perfects} not in factors: {factors}"
741-
return factors[factors >= inner_size]
767+
return np.array(sorted(factors))
742768

743769

744770
def append_vector(matrix: np.ndarray, vector: np.ndarray):
@@ -1470,6 +1496,7 @@ def eval_objective(
14701496
raise RuntimeError("BUG: both inner and outer tiles are unknown")
14711497

14721498
symbol_imperfect = _symbol_is_imperfect(symbol)
1499+
coarseness = job.spec_one_einsum.mapper.tiling_coarseness
14731500
# Use inner size and outer size to generate choices
14741501
if inner_tiles_type in oset(
14751502
["set", "unknown"]
@@ -1480,7 +1507,7 @@ def eval_objective(
14801507
]
14811508
):
14821509
factors = get_possible_factor_sizes(
1483-
outer_size, symbol_imperfect, inner_size
1510+
outer_size, symbol_imperfect, inner_size, coarseness
14841511
)
14851512
choices.append(append_vector(choices_enumerated, factors))
14861513
elif inner_tiles_type == "enumerated":
@@ -1491,7 +1518,7 @@ def eval_objective(
14911518
np.where(choices_enumerated[:, i] == inner_choice)
14921519
]
14931520
factors = get_possible_factor_sizes(
1494-
outer_size, symbol_imperfect, inner_choice
1521+
outer_size, symbol_imperfect, inner_choice, coarseness
14951522
)
14961523
choices.append(append_vector(partition, factors))
14971524
else:
@@ -1503,7 +1530,7 @@ def eval_objective(
15031530
np.where(choices_enumerated[:, i] == outer_choice)
15041531
]
15051532
factors = get_possible_factor_sizes(
1506-
outer_choice, symbol_imperfect, inner_size
1533+
outer_choice, symbol_imperfect, inner_size, coarseness
15071534
)
15081535
choices.append(append_vector(partition, factors))
15091536
elif what_tiles_symbol.is_initial_tile_shape(symbol):

accelforge/mapper/FFM/mappings.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from accelforge.util._frozenset import oset
88
from accelforge.mapper.FFM._make_pmappings.make_pmappings import (
99
get_n_computes,
10-
get_per_tensor_size,
10+
get_per_tensor_n_elements,
1111
)
1212
from typing import Union
1313
from accelforge._accelerated_imports import numpy as np
@@ -123,10 +123,15 @@ def per_tensor_size(self, return_n_elements: bool = False) -> dict[TensorName, i
123123
assert (
124124
self.evaluated_specs is not None
125125
), "Can't get per-tensor size if no Einsums have been mapped."
126-
return get_per_tensor_size(
127-
next(iter(self.evaluated_specs.values())).workload,
128-
return_n_elements=return_n_elements,
129-
)
126+
workload = next(iter(self.evaluated_specs.values())).workload
127+
sizes = get_per_tensor_n_elements(workload)
128+
if not return_n_elements:
129+
for einsum in workload.einsums:
130+
for tensor in einsum.tensor_names:
131+
sizes[tensor] = (
132+
sizes[tensor] * einsum.tensor_accesses[tensor].bits_per_value
133+
)
134+
return sizes
130135

131136
def _update(self, **kwargs):
132137
data = dict(

docs/source/guide/timeloop_compare.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ The following are features **suppported by AccelForge, not Timeloop**:
4444
<https://github.com/Accelergy-Project/accelergy>`_. Hwcomponents is a successor to
4545
Accelergy, and makes it significantly easier to write custom component models.
4646
- **Easy Installation**: AccelForge and dependencies are pip installable.
47+
- **Mapper for imperfectly-factorized loop levels**: The mapper can explore imperfect
48+
mappings for all loop levels. We note two differences between our implementations and
49+
Timeloop's. **(1)** We permit the outermost loop for each rank variable to have a
50+
residual, while Timeloop permits a residual on every loop. **(2)** We only run
51+
imperfect mapping for simple rank variables which do not participate in indexing
52+
expressions, while Timeloop allows every rank variable to be imperfect. Our
53+
constraints allow us to find good mappings while keeping the search space tractable,
54+
while Timeloop may only search the imperfect space with additional user-written
55+
constraints.
4756

4857
The following are features **supported by Timeloop, work-in-progress in AccelForge**:
4958

@@ -53,10 +62,6 @@ The following are features **supported by Timeloop, work-in-progress in AccelFor
5362
- **Layout Support**: Support for the costs of how data is laid out in memory.
5463
- **Skew in Model**: User-defined spatial skews that can split data between multiple
5564
multiple spatial instances.
56-
- **Mapper for $\sim 1-2$ imperfectly-factorized loop levels**: The mapper can explore
57-
imperfect mappings for a handful of loop levels, but becomes intractible for more loop
58-
levels. This, as well as full imperfect mapping, is present in AccelForge, but the API
59-
is not yet implemented.
6065
- **ISL-Based Model**: Timeloop supports high-fidelity modeling with ISL to generate the
6166
accesses for every spatial instance, while AccelForge uses a simpler and faster
6267
analytical model.
@@ -66,8 +71,6 @@ Timeloop**:
6671

6772
- **Skew in Mapping**: Automatically deriving the best skew for a given workload and
6873
architecture.
69-
- **Mapper for all imperfectly-factorized loop levels**: The mapper can explore
70-
imperfect mappings for all loop levels.
7174
- **Einsum-Level Spatial Parallelism**: The mapper can parallelize Einsums across
7275
spatial instances, rather than executing them sequentially.
7376

tests/test_network.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,7 @@ def test_hierarchical(self):
196196
PE_TILE * (PE_TILE - 1)
197197
)
198198
# tile shape
199-
* M_TILE
200-
* MAC_TILE
201-
* BITS_PER_VALUE,
199+
* M_TILE * MAC_TILE * BITS_PER_VALUE,
202200
)
203201
# NOTE: assuming XY routing (as defined in mapping)
204202
self.assertEqual(

0 commit comments

Comments
 (0)