Skip to content

ltm: agg→target link score over-subscripts an arrayed synthetic agg in the broadcast case (agg[D1] feeding to[D1,D2]) #528

@bpowers

Description

@bpowers

Summary

emit_agg_to_target_link_scores in src/simlin-engine/src/db_ltm.rs (~lines 4224-4360, nested in model_ltm_variables) builds the agg → target half of the link score for a reference routed through a synthetic aggregate node. When that synthetic agg is arrayed (AggNode.result_dims non-empty -- e.g. $⁚ltm⁚agg⁚0 over [D1] minted from a sliced reducer SUM(matrix[D1,*]) inside an apply-to-all-over-D1 target equation), the per-target-element link-score equation subscripts the agg ident by the full target element tuple, but it should subscript by the target element's projection onto agg.result_dims's axes.

Concretely, at ~line 4292-4302 there is already a comment acknowledging this:

// An arrayed agg (`result_dims` non-empty) is element-pinned in the
// per-target-element equation just like an arrayed dep that shares
// `to`'s dims: `$⁚ltm⁚agg⁚0` → `$⁚ltm⁚agg⁚0[<element>]`. This is
// exact when `result_dims` equals `to`'s dimensions (the diagonal
// `agg[d1] → to[d1]` case -- a partial-reduce sub-expression in an
// A2A target over exactly that dim, e.g. `x[D1] = ... + SUM(matrix[D1,*])`);
// for the rarer broadcast case (`agg[D1]` into `to[D1,D2]`) the
// element tuple over-subscripts the agg, which is a known imprecision.
if agg_is_arrayed {
    deps_to_subscript.insert(agg_canonical.clone());
}

The link-score name is already projected correctly: agg_name_for_target(element) (~line 4318) projects the target element tuple onto result_dim_positions (the positions of agg.result_dims within to's dims) to produce agg[<slot>]. It's the equation body that is wrong: the agg ident lands in deps_to_subscript, so the per-target-element scalar equation (built by build_partial_equation_shaped / the per-element machinery) pins it to to's full element tuple [<element>] instead of [<slot>]. For the diagonal case (agg[D1] → to[D1], result_dims == to's dims) the full tuple is the projection, so it's exact. For the broadcast case (agg[D1] feeding to[D1,D2], where the agg result is broadcast across D2) the equation references $⁚ltm⁚agg⁚0[d1,d2] -- an out-of-shape over-subscript of an agg that is only dimensioned over [D1].

Why it matters

Low. It's a magnitude/shape imprecision in the agg→target link score for that one (rare) broadcast topology -- not a crash, and the polarity/sign behavior is unaffected. The common case -- a partial-reduce sub-expression in an A2A target over exactly the reduced-into dimension (x[D1] = ... + SUM(matrix[D1,*]), the diagonal) -- is correct and is covered by Phase 4's AC4.2 tests. No required Phase-4 test exercises the broadcast case through model_ltm_variables: variable-backed partial reducers (row_sum[D1] = SUM(matrix[D1,*]), where row_sum is the agg) route through try_cross_dimensional_link_scores, which already handles partial-reduce naming correctly and does not go through emit_agg_to_target_link_scores; only an inline sliced reducer whose enclosing equation has more iterated dims than the reducer's Iterated axes would hit this path, which no current model or fixture does. Worth tracking so the imprecision is on record rather than silently relied upon.

Components affected

  • src/simlin-engine/src/db_ltm.rs -- emit_agg_to_target_link_scores (~lines 4224-4360; the deps_to_subscript.insert(agg_canonical) at ~4300, and the per-target-element equation construction below it -- the agg side of the link-score name via agg_name_for_target / result_dim_positions is already correct; the equation body is the gap). The symmetric source side, emit_source_to_agg_link_scores (~lines 4106-4208) + read_slice_rows / ReadSliceRow, is correct (it iterates only the read-slice rows with the proper Iterated-axis slot); this issue is the agg→target half only.
  • src/simlin-engine/src/ltm_augment.rs -- build_partial_equation_shaped / wrap_non_matching_in_previous / classify_expr0_subscript_shape (the per-element partial builder that consumes deps_to_subscript and pins the agg ident to the full target element).
  • src/simlin-engine/src/ltm_agg.rs -- AggNode.read_slice / AxisRead / compute_read_slice / result_dims (the descriptor that records which axes are Iterated, i.e. what the projection target is).
  • src/simlin-engine/src/db_analysis.rs -- emit_agg_routed_edges (the element-graph side already projects the target element onto agg.result_dims for the agg[<iterated>] → to[e] fan-out, the same way expand_same_element's Bare arm does -- the fix should mirror that projection on the link-score equation side).

Possible approaches

  • When emitting the agg→target link score, project the target element tuple onto just agg.result_dims's axes (using result_dim_positions, which is already computed for the name) before pinning the agg ident in the per-target-element equation -- i.e. pass the projected slot, not the full tuple, into the partial builder for the agg ident specifically (the other shared-dim deps still pin to the full tuple). This is the same projection emit_agg_routed_edges / expand_same_element's Bare arm does in the element graph.
  • This naturally piggybacks on ltm: unify the element-graph and link-score reference-site walkers behind a single classification IR #520 (unify the element-graph and link-score reference-site walkers behind one classification IR): once the link-score emitter consumes the same ThroughAgg { agg } routing and agg.read_slice the element graph already does, the projection logic exists in exactly one place.
  • If the inline-sliced-reducer-with-broadcast topology is judged not worth the extra projection plumbing right now, the diagonal case is at least correct and tested, and this issue documents the gap with a pointer to the comment.

Discovery context

Identified during the Phase 4 code review of the LTM arrays hardening epic (#488; commits ec018d0b / bfb61d08 on branch ltm-arrays-hardening; design plan and per-phase tasks under docs/implementation-plans/2026-05-11-ltm-arrays-hardening/, see phase_04.md AC4.2). Phase 4 introduced sliced-reducer hoisting and arrayed synthetic aggregate nodes; this is a documented edge case in that freshly-added code path -- not a regression (no arrayed synthetic agg existed before Phase 4). The carve-out comment in emit_agg_to_target_link_scores is on record but not yet in an issue.

Tracking

Part of LTM tracking epic #488. Related to: #514 (sliced-reducer hoisting -- the Phase-4 work that introduced this code path; distinct: #514 is about whether sliced reducers are hoisted at all, this is a shape imprecision in the agg→target link score once an arrayed synthetic agg exists), #510 (degenerate link score for disjoint-dimension arrayed→arrayed edges with per-element target equations -- a different topology and a different code path, try_cross_dimensional_link_scores), #520 (unify the reference-site walkers -- the refactor whose IR would make the fix a one-liner), #527 (expand_same_element over-conservative for mapped dims -- same flavor of "the projection should mirror the element graph" but a different site, db_analysis.rs, and a different axis-correspondence question), and the LTM array-support umbrella #273.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ltmLoops that Matter (LTM) analysis subsystem

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions