Comparison(expression="~x", ...) silently ignored once >1 Einsum is mapped
Summary
A loop_bounds Comparison using ~-negation (e.g. expression="~k",
meaning "every rank except k must have iteration count 1 here") works
correctly when mapping a single Einsum, but is silently dropped —
no error, no warning — as soon as map_workload_to_arch is run on two or
more Einsums. The affected spatial Container ends up completely unused,
and the mapper returns a valid but needlessly slow mapping.
Rewriting the identical constraint as an explicit positive union of the
other rank variables (e.g. expression="m|n0" instead of expression="~k")
works correctly in both the single- and multi-Einsum case.
Since this fails silently (not a crash), it's easy to end up with a "valid"
mapping that quietly ignores architecture constraints — this is how we
found it.
Repro
Python API, accelforge @ 59d5d1a (2026-06-30), Python 3.12.3.
import accelforge as af
from accelforge.frontend.arch import Arch, Memory, Container, Compute, Action, Spatial, Comparison
from accelforge.frontend.workload import Workload
def make_workload(two_einsums: bool):
einsums = [{
"name": "MM0",
"tensor_accesses": [
{"name": "A", "projection": {"M": "m", "K": "k"}},
{"name": "B", "projection": {"K": "k", "N": "n0"}},
{"name": "Z0", "projection": {"M": "m", "N": "n0"}, "output": True},
],
}]
if two_einsums:
einsums.append({
"name": "MM1",
"tensor_accesses": [
{"name": "Z0", "projection": {"M": "m", "N": "n1"}},
{"name": "C", "projection": {"N": "n1", "P": "p"}},
{"name": "Z1", "projection": {"M": "m", "P": "p"}, "output": True},
],
})
return Workload(rank_sizes={"M": 8, "K": 8, "N": 8, "P": 8}, bits_per_value={"All": 8}, einsums=einsums)
def make_arch(coarse_exprs, fine_exprs):
"""Two fanout=4 spatial Containers. Each Comparison should restrict its
Container to tiling only the ONE named rank per Einsum (every other rank
in that Einsum forced to iteration count 1 there)."""
return Arch(nodes=[
Memory(name="DRAM", size="inf", leak_power=0, area=0,
tensors={"keep": "~Intermediates", "may_keep": "All"},
actions=[Action(name="read", energy=1, throughput="inf"),
Action(name="write", energy=1, throughput="inf")]),
Memory(name="SRAM", size=1e9, leak_power=0, area=0, tensors={"keep": "All"},
actions=[Action(name="read", energy=0, throughput="inf"),
Action(name="write", energy=0, throughput="inf")]),
Container(name="Coarse", spatial=[Spatial(
name="Coarse", fanout=4, min_usage=1,
loop_bounds=[Comparison(expression=e, operator="==", value=1) for e in coarse_exprs])]),
Container(name="Fine", spatial=[Spatial(
name="Fine", fanout=4, min_usage=1,
loop_bounds=[Comparison(expression=e, operator="==", value=1) for e in fine_exprs])]),
Compute(name="MAC", leak_power=0, area=0, actions=[Action(name="compute", energy=0, throughput=1)]),
])
def run(label, two_einsums, coarse_exprs, fine_exprs):
spec = af.Spec(arch=make_arch(coarse_exprs, fine_exprs), workload=make_workload(two_einsums))
spec.mapper.metrics = af.Metrics.LATENCY
result = spec.map_workload_to_arch(print_progress=False)
lat = result.data["Total<SEP>latency"].iloc[0]
m0 = result.data["MM0<SEP>mapping"].iloc[0]
used = {getattr(n, "name", None) for n in m0.nodes if type(n).__name__ == "Spatial"}
print(f"{label:42s} latency={lat:<6} Containers used in MM0: {used or '{}'}")
run("1 Einsum, negation", False, ["~k"], ["~n0"])
run("2 Einsums, negation", True, ["~k", "~p"], ["~n0", "~n1"])
run("2 Einsums, explicit", True, ["m|n0", "m|n1"], ["m|k", "m|p"])
Observed output
1 Einsum, negation latency=32 Containers used in MM0: {'Coarse', 'Fine'}
2 Einsums, negation latency=1024 Containers used in MM0: {}
2 Einsums, explicit latency=64 Containers used in MM0: {'Coarse', 'Fine'}
Expected
2 Einsums, negation should behave like 2 Einsums, explicit (both express
the same constraint): Coarse/Fine should be used in MM0's mapping, and
latency should be 64, not 1024.
Notes
min_usage=1 is set on both Spatials, which should force full fanout
utilization — it does not help here, since the affected Containers are
dropped from consideration entirely rather than merely under-used.
spec.mapper.metrics = af.Metrics.LATENCY is set, so there's no missing
optimization objective that would explain leaving free parallelism unused.
- This looks like it's specific to
~-negation expression parsing/evaluation
somewhere in the multi-Einsum make_pmappings/join_pmappings path — not
a general Comparison/loop_bounds issue, since the single-Einsum case
with the exact same negation syntax works correctly.
Comparison(expression="~x", ...)silently ignored once >1 Einsum is mappedSummary
A
loop_boundsComparisonusing~-negation (e.g.expression="~k",meaning "every rank except
kmust have iteration count 1 here") workscorrectly when mapping a single Einsum, but is silently dropped —
no error, no warning — as soon as
map_workload_to_archis run on two ormore Einsums. The affected spatial
Containerends up completely unused,and the mapper returns a valid but needlessly slow mapping.
Rewriting the identical constraint as an explicit positive union of the
other rank variables (e.g.
expression="m|n0"instead ofexpression="~k")works correctly in both the single- and multi-Einsum case.
Since this fails silently (not a crash), it's easy to end up with a "valid"
mapping that quietly ignores architecture constraints — this is how we
found it.
Repro
Python API,
accelforge@59d5d1a(2026-06-30), Python 3.12.3.Observed output
Expected
2 Einsums, negationshould behave like2 Einsums, explicit(both expressthe same constraint):
Coarse/Fineshould be used inMM0's mapping, andlatency should be
64, not1024.Notes
min_usage=1is set on bothSpatials, which should force full fanoututilization — it does not help here, since the affected
Containers aredropped from consideration entirely rather than merely under-used.
spec.mapper.metrics = af.Metrics.LATENCYis set, so there's no missingoptimization objective that would explain leaving free parallelism unused.
~-negation expression parsing/evaluationsomewhere in the multi-Einsum
make_pmappings/join_pmappingspath — nota general
Comparison/loop_boundsissue, since the single-Einsum casewith the exact same negation syntax works correctly.