From 0fbf8e1273f3d2de77a1651e6a6e925189cc8281 Mon Sep 17 00:00:00 2001 From: Tanner Andrulis Date: Mon, 24 Aug 2026 09:55:46 -0400 Subject: [PATCH 1/3] More compact YAML dumps --- accelforge/util/_yaml.py | 50 ++++- scripts/make_training_workload.py | 345 ++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 scripts/make_training_workload.py diff --git a/accelforge/util/_yaml.py b/accelforge/util/_yaml.py index a92b76ff..51f70c25 100755 --- a/accelforge/util/_yaml.py +++ b/accelforge/util/_yaml.py @@ -8,6 +8,7 @@ from typing import Callable, List, Dict, Any, OrderedDict, Tuple import ruamel.yaml import warnings +from ruamel.yaml.comments import CommentedMap, CommentedSeq from ruamel.yaml.error import ReusedAnchorWarning from jinja2 import StrictUndefined, Environment, FileSystemLoader, pass_context, nodes from jinja2.ext import Extension @@ -407,8 +408,17 @@ def ordereddict_to_dict(self, dictionary: OrderedDict) -> Dict[str, Any]: return self.represent_dict(dictionary) +def _update_in_place(values, f): + items = values.items() if isinstance(values, dict) else enumerate(values) + for k, v in list(items): + values[k] = f(v) + return values + + @recursive_mutator_stop def recursive_unorder_dict(to_unorder: Dict[str, Any]) -> Dict[str, Any]: + if isinstance(to_unorder, (CommentedMap, CommentedSeq)): + return _update_in_place(to_unorder, recursive_unorder_dict) if isinstance(to_unorder, dict): return {k: recursive_unorder_dict(v) for k, v in to_unorder.items()} elif isinstance(to_unorder, list): @@ -418,7 +428,9 @@ def recursive_unorder_dict(to_unorder: Dict[str, Any]) -> Dict[str, Any]: @recursive_mutator_stop def callables2strings(to_convert: Dict[str, Any]) -> Dict[str, Any]: - if isinstance(to_convert, dict): + if isinstance(to_convert, (CommentedMap, CommentedSeq)): + to_convert = _update_in_place(to_convert, callables2strings) + elif isinstance(to_convert, dict): to_convert = {k: callables2strings(v) for k, v in to_convert.items()} elif isinstance(to_convert, list): to_convert = [callables2strings(v) for v in to_convert] @@ -427,6 +439,36 @@ def callables2strings(to_convert: Dict[str, Any]) -> Dict[str, Any]: return to_convert +def _flow_length(obj) -> float: + """ + Length of obj rendered in flow style on one line, or inf if it can't be (multi-line + strings, comments). + """ + ca = getattr(obj, "ca", None) + if ca is not None and (ca.comment or ca.items): + return float("inf") + if isinstance(obj, dict): + return 2 + sum(_flow_length(k) + _flow_length(v) + 4 for k, v in obj.items()) + if isinstance(obj, list): + return 2 + sum(_flow_length(v) + 2 for v in obj) + s = str(obj) + return float("inf") if "\n" in s else len(s) + + +@recursive_mutator_stop +def compact_flow(obj): + """Set flow style on dicts and lists that fit on one line.""" + if not isinstance(obj, (dict, list)): + return obj + _update_in_place(obj, compact_flow) + if _flow_length(obj) > 100: + return obj + if not isinstance(obj, (CommentedMap, CommentedSeq)): + obj = CommentedMap(obj) if isinstance(obj, dict) else CommentedSeq(obj) + obj.fa.set_flow_style() + return obj + + def write_yaml_file(filepath: str, content: Dict[str, Any]) -> None: """ Write YAML content to a file @@ -451,7 +493,8 @@ def to_yaml_string(content: Dict[str, Any]) -> str: with LockAcquirer(): dumpstream = io.StringIO() get_base_yaml().dump( - callables2strings(recursive_unorder_dict(content)), stream=dumpstream + compact_flow(callables2strings(recursive_unorder_dict(content))), + stream=dumpstream, ) return dumpstream.getvalue() @@ -459,7 +502,8 @@ def to_yaml_string(content: Dict[str, Any]) -> str: def get_base_yaml() -> ruamel.yaml.YAML: yaml = ruamel.yaml.YAML(typ="rt") # yaml.default_flow_style = None - yaml.indent(mapping=4, sequence=4, offset=2) + yaml.indent(mapping=2, sequence=2, offset=0) + yaml.width = 120 yaml.preserve_quotes = True def recursive_mutator_stop(func): diff --git a/scripts/make_training_workload.py b/scripts/make_training_workload.py new file mode 100644 index 00000000..34736c8a --- /dev/null +++ b/scripts/make_training_workload.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 + +""" +VIBE CODE WARNING: Entirely LLM-generated. Humans have verified outputs on a small set +of samples. Inspect any generated workloads before using them. + + +Convert an inference workload into its training backward-pass workload. + +For each Einsum Y = X * W, creates: + +- dW = X * dY +- W = prev_W (copy) +- new_W = W * dW * MV_W +- dX = W * dY. + +Operands produced by other Einsums (e.g., K in QK = Q * K) get gradients but no +copy/update. Two-tensor Einsums become elementwise dX = dY. Tensors with several +consumers get one gradient contribution per consumer plus a sum. + +Renames includes input, output, and weight for each Einsum (weight omitted for +two-tensor Einsums). Tensors with dependencies from the inference pass are renamed +forward_dependent, and should be kept in backing storage. + +Bits per value references four variables: gradient_bits, weight_bits, activation_bits, +optimizer_bits. Optimizer bits are PER WEIGHT VALUE, not per optimizer value. + +Usage: + python scripts/make_training_workload.py workload.yaml -D BATCH_SIZE=1 -o + training.yaml +""" + +import argparse +import ast +import re +import sys + +from ruamel.yaml.comments import CommentedSeq + +from accelforge import Spec +from accelforge.frontend.workload import Einsum, TensorAccess, Workload, _ISL_REGEX +from accelforge.util import _yaml +from accelforge.util._setexpressions import InvertibleSet + +BITS_DEFAULTS = {"gradient": 16, "weight": 16, "activation": 16, "optimizer": 64} + +DEFAULT_RENAMES = { + "einsums": [ + { + "name": "default", + "tensor_accesses": [ + { + "name": "input", + "source": "Inputs & Intermediates if len(All) == 3 else Inputs", + "expected_count": 1, + }, + {"name": "output", "source": "Outputs", "expected_count": 1}, + { + "name": "weight", + "source": "~(input | output)", + "expected_count": "1 if len(All) == 3 else 0", + }, + {"name": "forward_dependent", "source": "Nothing"}, + ], + } + ] +} + + +def idents(strings): + return {v for s in strings for v in re.findall(_ISL_REGEX, s)} + + +def classify(einsum: Einsum) -> tuple[str, str | None, str]: + """Return the (input, weight, output) tensors of an evaluated Einsum.""" + renames = {} + for r in einsum.renames: + if isinstance(r.source, InvertibleSet): + renames.setdefault(r.name, r.source.instance) + + def single(slot): + s = renames.get(slot, ()) + if len(s) != 1: + raise ValueError(f"{einsum.name}: rename {slot!r} = {sorted(s)}, need 1") + return next(iter(s)) + + n = len(einsum.tensor_accesses) + if n not in (2, 3): + raise ValueError(f"Einsum {einsum.name} has {n} tensors; only 2 or 3 supported") + return single("input"), single("weight") if n == 3 else None, single("output") + + +def grad(t): + return "d" + t + + +def convert(workload: Workload) -> tuple[Workload, list[str]]: + """Return the training workload and per-Einsum comments for an evaluated + inference workload.""" + producers, consumers, ranks = {}, {}, {} + for e in workload.einsums: + for a in e.tensor_accesses: + ranks.setdefault(a.name, tuple(a.projection)) + if a.output: + producers[a.name] = e.name + else: + consumers.setdefault(a.name, []).append(e.name) + pending = {t: len(c) for t, c in consumers.items()} + einsums, comments, copied = [], [], set() + + def fresh(name): + if name in ranks: + raise ValueError(f"Generated tensor {name} collides with a forward tensor") + return name + + def contribution(t, consumer): + return grad(t) if len(consumers[t]) == 1 else f"{grad(t)}_{consumer}" + + def add(comment, out, ins, renames=None, source=None, copy=False): + """out and ins are (tensor, projection, bits_per_value) triples; source + is the forward Einsum whose per-Einsum bounds carry over.""" + comments.append(comment) + accesses = [ + TensorAccess(name=t, projection=dict(p), bits_per_value=b, output=n == 0) + for n, (t, p, b) in enumerate([out] + ins) + ] + kwargs = {} + if source is not None: + variables = idents(x for _, p, _ in [out] + ins for x in p.values()) + kwargs["iteration_space_shape"] = [ + s for s in source.iteration_space_shape if idents([s]) <= variables + ] + einsums.append( + Einsum( + name=out[0], + tensor_accesses=accesses, + renames=renames or {}, + is_copy_operation=copy, + **kwargs, + ) + ) + + def add_sum(t): + assert ( + pending[t] == 0 + ), f"{t} gradient incomplete; Einsums not in produce-before-consume order?" + proj = {r: r.lower() for r in ranks[t]} + add( + f"Sum the gradient contributions to {t}.", + (grad(t), proj, "gradient_bits"), + [(contribution(t, c), proj, "gradient_bits") for c in consumers[t]], + renames={"input": "Inputs", "weight": "Nothing"}, + ) + + def ensure_copied(w, proj, source): + if w in copied: + return + copied.add(w) + prev = fresh(f"prev_{w}") + add( + f"Copy {w} from off-chip.", + (w, proj, "weight_bits"), + [(prev, proj, "weight_bits")], + renames={ + "input": prev, + "output": w, + "weight": "Nothing", + "forward_dependent": prev, + }, + source=source, + copy=True, + ) + + def add_update(w, proj, source): + if len(consumers[w]) > 1: + add_sum(w) + ensure_copied(w, proj, source) + new, mv = fresh(f"new_{w}"), fresh(f"MV_{w}") + add( + f"Update {w} with gradient {grad(w)}.", + (new, proj, "weight_bits"), + [ + (w, proj, "weight_bits"), + (grad(w), proj, "gradient_bits"), + (mv, proj, "optimizer_bits"), + ], + renames={ + "input": "Inputs", + "output": new, + "weight": "Nothing", + "forward_dependent": new, + }, + source=source, + ) + + for e in reversed(list(workload.einsums)): + x, w, y = classify(e) + dy = grad(y) + if len(consumers.get(y, ())) > 1: + add_sum(y) + proj = {a.name: a.projection for a in e.tensor_accesses} + + if w is None: + dx = fresh(contribution(x, e.name)) + add( + f"Backward of {e.name}.", + (dx, proj[x], "gradient_bits"), + [(dy, proj[y], "gradient_bits")], + source=e, + copy=e.is_copy_operation, + ) + pending[x] -= 1 + continue + + true_weight = w not in producers + dw, dx = fresh(contribution(w, e.name)), fresh(contribution(x, e.name)) + + # Weight gradient is input * output gradient. + add( + f"Backward of {e.name}: gradient of {w}.", + (dw, proj[w], "gradient_bits"), + [(x, proj[x], "activation_bits"), (dy, proj[y], "gradient_bits")], + renames={"input": x, "output": dy, "weight": dw, "forward_dependent": x}, + source=e, + ) + pending[w] -= 1 + if true_weight: + if pending[w] == 0: + add_update(w, proj[w], e) + ensure_copied(w, proj[w], e) + + # Input gradient is weight * output gradient. + renames = {"input": dy, "output": dx, "weight": w} + if not true_weight: + renames["forward_dependent"] = w + add( + f"Backward of {e.name}: gradient of {x}.", + (dx, proj[x], "gradient_bits"), + [ + (w, proj[w], "weight_bits" if true_weight else "activation_bits"), + (dy, proj[y], "gradient_bits"), + ], + renames=renames, + source=e, + ) + pending[x] -= 1 + + # Pristine inputs with several consumers never hit a producer; sum them here. + for t in consumers: + if t not in producers and t not in copied and len(consumers[t]) > 1: + add_sum(t) + assert all(v == 0 for v in pending.values()), f"Unconsumed contributions: {pending}" + + training = Workload( + einsums=einsums, + rank_sizes=dict(workload.rank_sizes), + iteration_space_shape=dict(workload.iteration_space_shape), + persistent_tensors="forward_dependent", + ) + return training, comments + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument("workload", help="path to the inference workload YAML") + parser.add_argument("-o", "--output", help="output path (default: stdout)") + parser.add_argument( + "-D", + "--define", + action="append", + default=[], + metavar="KEY=VALUE", + help="jinja variable for parsing the workload, e.g. -D BATCH_SIZE=1", + ) + for k, v in BITS_DEFAULTS.items(): + parser.add_argument(f"--{k}-bits", type=int, default=v, help=f"default {v}") + args = parser.parse_args() + + defines = {} + for d in args.define: + k, _, v = d.partition("=") + try: + defines[k] = ast.literal_eval(v) + except (ValueError, SyntaxError): + defines[k] = v + + spec = Spec.from_yaml(args.workload, jinja_parse_data=defines) + workload = spec._spec_eval_expressions(eval_arch=False).workload + + producers = {t for e in workload.einsums for t in e.output_tensor_names} + print(f"{'Einsum':<24}{'input':<20}{'weight':<20}{'output':<20}", file=sys.stderr) + for e in workload.einsums: + x, w, y = classify(e) + kind = ( + "copy" + if e.is_copy_operation + else ( + "elementwise" + if w is None + else "weight" if w not in producers else "forward intermediate" + ) + ) + print(f"{e.name:<24}{x:<20}{w or '-':<20}{y:<20}{kind}", file=sys.stderr) + + training, comments = convert(workload) + dump = training.model_dump(exclude_defaults=True) + dump["einsums"] = CommentedSeq(dump["einsums"]) + for i, (einsum, comment) in enumerate(zip(dump["einsums"], comments)): + if "renames" in einsum: + einsum["renames"] = {r["name"]: r["source"] for r in einsum["renames"]} + dump["einsums"].yaml_set_comment_before_after_key(i, f"\n{comment}", indent=2) + + externals = ", ".join( + grad(t) for t in producers if not workload.einsums_with_tensor_as_input(t) + ) + source_desc = args.workload + (f" (jinja: {defines})" if defines else "") + text = ( + f"# Training backward pass generated from {source_desc}\n" + f"# by scripts/make_training_workload.py.\n" + f"#\n" + f"# Assume as input {externals}. Precisions are gradient_bits, weight_bits, activation_bits,\n" + f"# optimizer_bits. Note optimizer bits is optimizer bits PER WEIGHT VALUE, not per\n" + f"# optimizer value. Tensors with dependencies from the inference pass are\n" + f'# "forward_dependent", and should be kept in backing storage.\n' + ) + _yaml.to_yaml_string( + { + "variables": { + f"{k}_bits": getattr(args, f"{k}_bits") for k in BITS_DEFAULTS + }, + "renames": DEFAULT_RENAMES, + "workload": dump, + } + ) + if args.output: + with open(args.output, "w") as f: + f.write(text) + print(f"Wrote {args.output}", file=sys.stderr) + else: + print(text) + + +if __name__ == "__main__": + main() From f673d133a59b6332b5606678531babb4dec5919e Mon Sep 17 00:00:00 2001 From: Tanner Andrulis Date: Mon, 24 Aug 2026 10:40:35 -0400 Subject: [PATCH 2/3] Multiple bits per values in set expressions, einsum_has_reduction --- accelforge/frontend/mapping/__init__.py | 1 + accelforge/frontend/spec.py | 10 ++++- accelforge/frontend/workload.py | 38 ++++++++++++++++++- accelforge/util/_setexpressions.py | 9 +++-- .../test_mapping.py | 4 +- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/accelforge/frontend/mapping/__init__.py b/accelforge/frontend/mapping/__init__.py index 5f1daad5..93743272 100644 --- a/accelforge/frontend/mapping/__init__.py +++ b/accelforge/frontend/mapping/__init__.py @@ -18,6 +18,7 @@ "Temporal", "TensorHolder", "TensorName", + "TextBox", "TilePattern", "Toll", ] diff --git a/accelforge/frontend/spec.py b/accelforge/frontend/spec.py index c9059c68..6cef81fd 100755 --- a/accelforge/frontend/spec.py +++ b/accelforge/frontend/spec.py @@ -157,10 +157,16 @@ def _spec_eval_expressions( st["workload"] = evaluated_workload if einsum_name is not None: - renames = evaluated_workload.einsums[einsum_name].renames - st.update(**{k.name: k.source for k in renames}) + einsum = evaluated_workload.einsums[einsum_name] + st.update(**{k.name: k.source for k in einsum.renames}) + n_computes = evaluated_workload.n_computes(einsum_name) + n_outputs = min( + evaluated_workload.get_tensor_size(t) for t in einsum.output_tensor_names + ) + st["einsum_has_reduction"] = n_computes > n_outputs else: st.update(evaluated_workload.empty_renames()) + st["einsum_has_reduction"] = True if eval_arch: evaluated_arch, st = self.arch._eval_expressions(st) diff --git a/accelforge/frontend/workload.py b/accelforge/frontend/workload.py index ea581b2d..361d417b 100755 --- a/accelforge/frontend/workload.py +++ b/accelforge/frontend/workload.py @@ -128,7 +128,7 @@ class TensorAccess(EvalableModel): backing_storage_size_scale: float = 1.0 """ If != 1, then the backing storage size will be scaled by this factor. """ - bits_per_value: int | str | None = None + bits_per_value: EvalsTo[int | None] = None """ Bits per value for this tensor. """ def model_post_init(self, __context__=None) -> None: @@ -678,6 +678,7 @@ def _eval_expressions(self, symbol_table: dict[str, Any], *args, **kwargs): outputs = self.output_tensor_names all_ = inputs | outputs persistent = oset(t.name for t in self.tensor_accesses if t.persistent) + element_bits = {} element_to_child_space = {} all_rank_variables = self.rank_variables for tensor in self.tensor_names: @@ -707,6 +708,7 @@ def _eval_expressions(self, symbol_table: dict[str, Any], *args, **kwargs): space_type=TensorName, child_access_name="rank_variables", element_to_child_space=element_to_child_space, + element_bits_per_value=element_bits, ) kwargs_rank_variables = dict( full_space=all_rank_variables, @@ -809,7 +811,20 @@ def _eval_expressions(self, symbol_table: dict[str, Any], *args, **kwargs): source_field=f"tensor_accesses[{t.name}].bits_per_value", ) if t.bits_per_value is None: - t.bits_per_value = bits_per_value[t.name] + t.bits_per_value = eval_expression( + bits_per_value[t.name], + st, + attr_name=f"bits_per_value[{t.name}]", + ) + + element_bits.update( + {t.name: t.bits_per_value for t in evaluated.tensor_accesses} + ) + for r in evaluated.renames: + if isinstance(r.source, InvertibleSet) and all( + t in element_bits for t in r.source.instance + ): + r.source.element_bits_per_value = element_bits if symbol_table.get("workload_persistent_tensors", None): rename_st_with_evaluated = {**st} @@ -1336,3 +1351,22 @@ def get_compute_intensity(self, einsum_name: str) -> float: self.get_tensor_size(tensor) for tensor in self.einsums[einsum_name].tensor_names ) + + def get_per_tensor_compute_intensity(self) -> dict[TensorName, float]: + """ + Returns the compute intensity of each tensor, defined as the sum of the number + of computes of each Einsum that accesses the tensor, divided by the number of + elements in the tensor. + + Returns + ------- + dict[TensorName, float] + The compute intensity of each tensor in #computes / #tensor elements. + """ + return { + tensor: sum( + self.n_computes(e.name) for e in self.einsums_with_tensor(tensor) + ) + / self.get_tensor_size(tensor) + for tensor in self.tensor_names + } diff --git a/accelforge/util/_setexpressions.py b/accelforge/util/_setexpressions.py index 96b8dae0..f721120a 100755 --- a/accelforge/util/_setexpressions.py +++ b/accelforge/util/_setexpressions.py @@ -26,6 +26,7 @@ class InvertibleSet(BaseModel, Generic[T]): space_type: type[T] # child_access_name: Optional[str] = None element_to_child_space: Optional[dict[str, Any]] = None + element_bits_per_value: Optional[dict[str, int]] = None _bits_per_value: Optional[int] = None def __init__(self, *args, **kwargs): @@ -118,6 +119,7 @@ def to_my_space(self, other) -> Union[set, "InvertibleSet"]: space_type=self.space_type, # child_access_name=self.child_access_name, element_to_child_space=self.element_to_child_space, + element_bits_per_value=self.element_bits_per_value, ) @staticmethod @@ -190,6 +192,7 @@ def iter_one_element_sets(self) -> Iterator["InvertibleSet[T]"]: space_type=self.space_type, # child_access_name=self.child_access_name, element_to_child_space=self.element_to_child_space, + element_bits_per_value=self.element_bits_per_value, ) @property @@ -294,7 +297,7 @@ def eval_set_expression_dict( symbol_table: dict[str, InvertibleSet], expected_space: type[T], location: str, - disjoint: bool=True, + disjoint: bool = True, ) -> list[tuple[str, "frozenset[T]", Any]]: """ Evaluate a dict whose keys are set expressions, returning an ordered list of @@ -310,7 +313,7 @@ def eval_set_expression_dict( ) evaluated: list[tuple[str, Any, Any]] = [] - + symbol_table = symbol_table.copy() symbol_table["Other"] = symbol_table["All"] @@ -324,7 +327,7 @@ def _eval(i): ).instance symbol_table["Other"] -= ins return k, ins, v - + eval_order = [i for i in range(len(items)) if i not in others] + others for i in eval_order: evaluated.append(_eval(i)) diff --git a/tests/vibe_see_readme_in_this_dir/test_mapping.py b/tests/vibe_see_readme_in_this_dir/test_mapping.py index 5bfef1b9..867fb150 100644 --- a/tests/vibe_see_readme_in_this_dir/test_mapping.py +++ b/tests/vibe_see_readme_in_this_dir/test_mapping.py @@ -541,7 +541,7 @@ def test_split_loop_with_multiple_rank_variables(self): ] ) m.nodes[0]._einsum_to_rank_variable["E"] = "m" - m.split_loop_with_multiple_rank_variables(einsum_name="E") + m._split_loop_with_multiple_rank_variables(einsum_name="E") temporals = m.get_nodes_of_type(Temporal) self.assertEqual(len(temporals), 1) self.assertEqual(temporals[0].rank_variable, "m") @@ -553,7 +553,7 @@ def test_split_tensor_holders_with_multiple_tensors(self): Compute(einsum="E", component="MAC"), ] ) - m.split_tensor_holders_with_multiple_tensors() + m._split_tensor_holders_with_multiple_tensors() storages = [n for n in m.nodes if isinstance(n, Storage)] self.assertEqual(len(storages), 2) From 7ff28a6294d3baf50851e484f662a4fb16706b7c Mon Sep 17 00:00:00 2001 From: Tanner Andrulis Date: Mon, 24 Aug 2026 10:41:27 -0400 Subject: [PATCH 3/3] Typo fix --- accelforge/frontend/workload.py | 4 ++-- accelforge/util/_setexpressions.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/accelforge/frontend/workload.py b/accelforge/frontend/workload.py index 361d417b..1e130552 100755 --- a/accelforge/frontend/workload.py +++ b/accelforge/frontend/workload.py @@ -708,7 +708,7 @@ def _eval_expressions(self, symbol_table: dict[str, Any], *args, **kwargs): space_type=TensorName, child_access_name="rank_variables", element_to_child_space=element_to_child_space, - element_bits_per_value=element_bits, + element_to_bits_per_value=element_bits, ) kwargs_rank_variables = dict( full_space=all_rank_variables, @@ -824,7 +824,7 @@ def _eval_expressions(self, symbol_table: dict[str, Any], *args, **kwargs): if isinstance(r.source, InvertibleSet) and all( t in element_bits for t in r.source.instance ): - r.source.element_bits_per_value = element_bits + r.source.element_to_bits_per_value = element_bits if symbol_table.get("workload_persistent_tensors", None): rename_st_with_evaluated = {**st} diff --git a/accelforge/util/_setexpressions.py b/accelforge/util/_setexpressions.py index f721120a..8565cc7c 100755 --- a/accelforge/util/_setexpressions.py +++ b/accelforge/util/_setexpressions.py @@ -26,7 +26,7 @@ class InvertibleSet(BaseModel, Generic[T]): space_type: type[T] # child_access_name: Optional[str] = None element_to_child_space: Optional[dict[str, Any]] = None - element_bits_per_value: Optional[dict[str, int]] = None + element_to_bits_per_value: Optional[dict[str, int]] = None _bits_per_value: Optional[int] = None def __init__(self, *args, **kwargs): @@ -119,7 +119,7 @@ def to_my_space(self, other) -> Union[set, "InvertibleSet"]: space_type=self.space_type, # child_access_name=self.child_access_name, element_to_child_space=self.element_to_child_space, - element_bits_per_value=self.element_bits_per_value, + element_to_bits_per_value=self.element_to_bits_per_value, ) @staticmethod @@ -192,7 +192,7 @@ def iter_one_element_sets(self) -> Iterator["InvertibleSet[T]"]: space_type=self.space_type, # child_access_name=self.child_access_name, element_to_child_space=self.element_to_child_space, - element_bits_per_value=self.element_bits_per_value, + element_to_bits_per_value=self.element_to_bits_per_value, ) @property