Skip to content

feat(arithmetic)!: executeOp scratch sized per converted operand + OUT_WRITE aliasing (#296 Stage 1)#334

Merged
LeoBuron merged 2 commits into
developfrom
bounded-scratch-stage1
Jul 10, 2026
Merged

feat(arithmetic)!: executeOp scratch sized per converted operand + OUT_WRITE aliasing (#296 Stage 1)#334
LeoBuron merged 2 commits into
developfrom
bounded-scratch-stage1

Conversation

@LeoBuron

@LeoBuron LeoBuron commented Jul 9, 2026

Copy link
Copy Markdown
Member

Stage 1 of the bounded-scratch design for #296 (spec: docs/superpowers/specs/2026-07-09-bounded-scratch-design.md; Stage 2 — O(chunk) conversions — follows as its own PR). Executed via SDD: per-task reviews + final whole-branch review (verdict: merge-ready).

What changes

  1. executeOp prologue scratch is sized per actually-converted operand — one summed VLA, each row sized to its own operand, zero bytes when every operand already matches the arithmetic dtype (the entire FLOAT32 training path). Previously: scratch[3][maxElems·4] unconditionally — 73,728 B at HAR's conv3 even when nothing converted.
  2. rawData output-aliasing: FLOAT32-arithmetic → FLOAT32-target OUT_WRITE ops let the kernel write the target directly (the epilogue there was a same-type memmove). Gated hard: never for OUT_ACC_* (accumulate must keep the intermediate distinct), never for non-FLOAT32 targets (the SYM→SYM epilogue is a load-bearing width-restoring requant), and never when the target's data pointer equals a post-prologue operand pointer — unless the new opSpec_t.writesInPlaceSafe (zero-init false) declares the kernel elementwise. The three SGD update kernels opt in (verified strictly elementwise).
  3. Stack-watermark budgets recalibrated to the measured post-change peaks (+8 KiB slack policy unchanged, never-raise rule intact, Linux stays uncalibrated); the two Python fixture tests updated to the new measured values.

Measured effect (HAR, EPOCHS=1, macOS arm64)

config before after Δ new peak site
float 99,560 B 27,328 B −72.6 % conv3 weight-grad OUT_ACC (rawData 24,576 + ~2.7 K frames) — staging there is load-bearing, aliasing an accumulate is semantically impossible
sym8 149,008 B 100,080 B −48,928 B SGD param op on conv3: 1 converted row (24,576) + rawData (24,576) + the 49,152 B repack chain that Stage 2 removes (+~1.8 K frames; the removed idle rows were 49,152 B, frames grew ~224 B from the new bookkeeping)

Projection after Stage 2: sym ≈ 51 KB → total training RAM sym8 ≈ 205 KB vs float ≈ 212 KB — the #322 inversion flips in sym's favor at HAR scale.

Safety argument for the aliasing (reviewers' conjunct→killer map)

  • Drop OUT_WRITE conjunct → testAccDynamicIntoFloatIsExactAndAccumulates goes RED (aliased ACC doubles the increment).
  • Drop ARITH_FLOAT32 conjunct → testOutWriteSymToFloatEqualsConvertTensor goes RED (raw mantissas reach the float target).
  • Drop the FLOAT32-target conjunct → testExecuteOpNeverAliasesSymTarget goes RED (SYM width-restore skipped).
  • Invert the self-alias scan → testExecuteOpDoesNotAliasWhenTargetIsAnInput goes RED (mutation-verified during Task 1).

Known contract edge (documented, deferred to Stage 2's doc pass): the self-alias check is exact-base-pointer equality — overlapping sub-views (constructible only via raw-pointer tensorInitFloat wiring) are outside the contract.

Verification

  • ctest --preset unit_test_debug 72/72; focused UnitTestExecuteOp 26/26 (4 new aliasing tests + 1 new 3-input mixed-dtype prologue pin); uv run pytest 29/29; cmake --build --preset examples all targets.
  • Strict TDD per task with recorded mutation checks (offset-drop mutant and alias-scan-inversion mutant each flipped exactly their intended test).
  • Watermark check script passes against the new budgets for both fresh measurement logs.

Refs #296 (Stage 2 PR will close it), #322 (stack narrative), #300 axis-1 addendum.

🤖 Generated with Claude Code

@LeoBuron LeoBuron force-pushed the bounded-scratch-stage1 branch from a33b8f3 to 38b2797 Compare July 9, 2026 21:28
Independent review (fable) of #325 found two real bugs in the gate job:

- run: steps use bash -e by default; an unhandled gh pr list failure (API
  hiccup) would abort the script before `proceed` is ever written, failing
  the gate job itself -- the exact red-X-on-transient-flake outcome #325
  was meant to eliminate, now just relocated to api.github.com instead of
  apt.llvm.org. `|| dup=0` makes a lookup failure fail open (full run
  proceeds) instead of failing the gate.
- `gh pr list` defaults to --limit 30; past that many concurrently open
  PRs, a real duplicate could go undetected (fails open the same way,
  just silently -- harmless but worth closing). --limit 100 covers this
  repo's realistic ceiling.
…ing + OUT_WRITE aliasing (#296 Stage 1)

- executeOp prologue: one summed scratch VLA sized to the operands that
  actually convert (zero bytes when all dtypes match the arithmetic),
  replacing the unconditional scratch[3][maxElems*4]
- rawData aliasing: FLOAT32-arithmetic -> FLOAT32-target OUT_WRITE kernels
  write the target directly; gated on no post-prologue operand aliasing the
  target unless the new opSpec_t.writesInPlaceSafe (elementwise contract)
  is set; never for OUT_ACC (staging load-bearing) or non-FLOAT32 targets
  (width-restore load-bearing)
- the three SGD update kernels opt in (verified elementwise)
- stack-watermark budgets recalibrated to measured post-change peaks:
  float 99560 -> 27328 B (peak moved to conv3 weight-grad OUT_ACC),
  sym 149008 -> 100080 B (repack chain remains for Stage 2); pytest
  fixtures updated to the new measured values

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant