Skip to content

Comparison(expression="~x", ...) silently ignored once >1 Einsum is mapped #55

Description

@okaikov

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions