Repository navigation
Conversation
## Why - First artifact of tile-ai#239: an insert_slice occurrence stated its whole update window as every executing mesh position's share, and a zeros of a mesh-replicated buffer stated the whole buffer; total = per-unit share x positions then billed one gmem write once per position. On a 40-of-132 lane stream probe (12.5 MB buffer, 320 KB window per trip) the insert billed r/w 12.50MB per trip at @ToTal and the zeros billed w 500.00MB, pricing the program at predicted-ns=1117769 against a roofline floor of ideal-ns=13654. ## What - Price at the level the bytes land in, not in the relation: footprint and liveness legitimately need the whole window per position, so the fix lives in call_traffic. When every position performs the same write -- an InsertSlice whose offsets name no mesh coordinate directly, through a captured binding, or through the bounds of an enclosing loop, or a Zeros of a mesh-replicated buffer -- and the level is target-owned and the per-unit share is the whole window, one position's share becomes the window divided among the positions, rounding up, and total counts it once. Mesh-placed windows ((cta.c, 0, 0), or loop bounds naming a coordinate), topology-owned levels (smem/rmem), and shares already smaller than the whole keep per-position billing: those bytes are physically each position's own. This is the tile-ai#194/tile-ai#195 copy rule applied to writes nobody sharded: a sharded boundary already divides the iteration space; a replicated one needs the positions divided out at pricing time. - Probe after: the insert bills r8.00KB/w8.00KB@cta, r320.00KB/w320.00KB@total and the zeros bills w12.50MB@total; predicted-ns=63452 with ideal-ns=13654 and the wave=132/132 header unchanged. A 2x-destination variant drops 1478217 -> 72463; a 132-of-132 variant 3604839 -> 63452. Footprint, peak, logical traffic and compute-cost are unchanged everywhere. - tests/analysis/test_shared_window_billing.py pins every billing regime (shared window, shared zeros, mesh-placed offsets, mesh-bounded loop, private-level fill) plus function-total and footprint invariants. Two goldens moved and were verified per op: test_analysis_families.py (the TiledQKVProjection insert, whose 132 positions run one redundant loop nest) and test_analyze_at_a_size.py's qwen3_1_7b_pd layer_decode total (exactly the nine replicated insert_slice occurrences, each by window x (positions - 1) with loop trips accounted). - docs/tutorial/schedule.md regenerated: the grid/persistent tf.zeros bills its 272MB buffer once (predicted-ns 264231132 -> 54324798 grid, 18317269 -> 10533340 persistent; the persistent insert keeps per-position billing through its mesh-bounded loop). optimize/showcase/migrate tutorial outputs are byte-identical. ## Contract - docs/spec/analysis.md traffic and performance constraints state the carve-out: total = per-unit x positions still holds; what changed is one position's share of a window every position writes alike at a target-owned level. Mesh-placed windows and topology-owned levels keep the whole window as one position's share. ## Risk - Division rounds the per-unit share up, so total can overshoot one window by less than one position's share (64.07KB vs 64.00KB at 132 positions). - The mesh-coordinate test reads HIR value structure only; per-position placement hidden behind a call the walk cannot see through keeps the old over-billing rather than inventing a division. - tests/cli/test_cli.py::test_target_list_expressions_execute_and_show_accepts_their_identities fails on the base commit as well when the file runs in order (passes standalone); pre-existing, unrelated.
## Why - Second artifact of tile-ai#239: a probe occupying 40 of 132 cta lanes printed wave=132/132 with no hint that most positions sit idle, so an under-filled mesh reads exactly like a full one. wave counts the positions the cost model bills; nothing in the report described fill. ## What - The identity line and the JSON wave record now state the occupied lane count next to the declared one. Occupied is the most positions any mesh the selected Function executes activates at the topology level -- its own execution mesh and every mesh region it enters -- and 1 when the Function states no mesh. Text prints lanes=<occupied>/<declared> only when the two differ, so a full fill stays silent; JSON always carries the field. On the 40-lane probe the header now reads wave=132/132 lanes=40/132 while predicted-ns and ideal-ns are unchanged. - wave_of itself is untouched: footprint and liveness depend on its counted-vs-declared semantics, so the new figure lives beside it rather than inside it. - docs/spec/analysis.md states the lanes field and the occupied definition. The tutorials are regenerated; only the showcase pages move (their GQA stages run 1, 8, or 32 of 132 lanes, so six # analysis header lines gain lanes= and nothing else). The same regeneration syncs schedule.ipynb's stored outputs with the billing fix 8351857, whose numbers schedule.md already showed, and rewrites migrate.ipynb's cell sources in the render script's list form; neither changes any content. ## Verification - tests/analysis/test_lane_fill.py: under-filled (lanes=40/132), full fill (silent), meshless (lanes=1/132), and sibling-region max fill, on the text header and the JSON wave record. - tests/analysis: the two by_hand wave-dict assertions updated for the new field; the suite passes with them. tests/cli + tests/inspection: the retained-occurrences golden updated (occupied=132 there); the one unrelated target-list failure in that run passes in isolation. - Tutorial goldens diff-checked per op: showcase changes are header-only. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Why - tile-ai#252: a staged operator gives each stage its own tiling axes, and a chunk length can be an axis of a stage's own scratch without ever touching the entry's inputs. analyze already collects declarations from every reachable function, so a live callee-only dim binds there; but check then died at evaluation with "resolve_dim: unbound DimVar 'BT'", because the evaluator bound dims only from the entry's argument shapes and the CLI never threaded the --dim values into the run. ## What - evaluate() takes dims= and carries them on the evaluation context; every function boundary merges them with what the argument shapes pin, so a supplied extent is visible inside whichever callee declares the dimension, across module boundaries. A supplied extent that contradicts an argument shape is an error naming both values; a dimension nothing supplied still reports unbound, as before. - check threads each run's parsed --dim combination into the evaluator through CheckRequest. - For a name that appears only in a binding the block dropped (the issue's reproducer: an unused scratch = tf.empty(Tensor[(ROWS, BT)]) inside the callee), nothing in the program declares the dimension and binding it anyway would name nothing. Per the issue's own fallback the analyze error now says which functions declare dims: "... has no dimension named ['BT']; no function it reaches declares a dimension", or, when some do, "the dimensions it reaches are BT (stage), ...". ## Verification - tests/fixtures/logical/callee_dim.py: the same helper in-module (Staged), across a module boundary (Rooted), and the dropped-binding case (DeadScratch). - tests/analysis/test_dim_binding.py: live callee-only dims bind in check_program, same module and cross-module; the unknown-name error names the declaring function; the dropped-binding case gets the honest empty form. - tests/evaluator/test_supplied_dims.py: the supplied mapping binds at the entry, crosses module boundaries, must agree with argument shapes, and leaves the nothing-supplied error intact. - tests/cli/test_cli_check.py: check --dim BT=8 on a callee-only dim now runs to PASS; before this change it died with "resolve_dim: unbound DimVar 'BT'". The issue's analyze reproducer now prints "no function it reaches declares a dimension" instead of the entry-centric "it states []". - tests/analysis + tests/evaluator + tests/cli + tests/ir + tests/dsl: 574 passed; the 3 failures are environmental and reproduce at base 8351857 -- two CUDA atom tests need the uninitialized third_party/cutlass submodule, and the target-list test trips on a stale persisted-registry entry in this venv (an nvidia.l20z probe target another workstream added; the test does not strip the " added" marker target list prints for it). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion records ## Why - tile-ai#253, three defects in the CLI/report path: analyze's --json help said "print ... instead of text" while the command required PATH and wrote there, leaving stdout empty; a multi-function text report printed one # memory line per function with no marker of whose record it is or how it aggregates, and summing the lines over-counts because every record already counts everything its function reaches; check --json PATH validated nothing, so a typo'd directory cost a full comparison run before failing with a bare Errno. ## What - analyze's out PATH accepts '-' for stdout, text and JSON alike; the --json help now says render, since PATH was always the destination. - Each # memory line is labelled function=<module.qualified> role=entry|stage covers=self+reached: a record counts its function and everything it reaches, so the entry's line is the program total and the lines must not be summed. The text line's @ToTal figures are the same whole-program counts --json states under functions[i].totals; a regression test pins that. - check --json validates its parent directory at argument-parse time; the refusal names the path. ## Verification - New regression tests: '-' routes analyze text and JSON to stdout; the # memory labels and the text @ToTal == JSON totals agreement hold over a three-function closure; check --json refuses a missing parent at parse time as a usage error. - pytest tests/cli tests/analysis tests/inspection: 477 passed, 1 failed -- the known target-list registry flake that fails identically at base 8351857. - Tutorials regenerated; every docs diff is the label insertion only (old content plus the label reproduces the new byte-for-byte). - The byte-exact schedule golden gemm_8192x17408x5120_tma_store.analyzed.txt gained the same label; its fixture case passes. - pre-commit clean. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks a lot @sablin39 for this PR! The billing problem you found is real, and your reproducer made it easy to follow. I took the PR over and pushed my changes directly to this branch, so you do not need to do anything more. On top of your commits:
Thanks again for the contribution! |
| executed: tuple[str, ...] | ||
| metadata_types: tuple[type[IRMetadata], ...] | ||
| functions: tuple[Function, ...] | ||
| wave: tuple[int, int] | None = None |
There was a problem hiding this comment.
Why do you add wave here instead of func metadata?
| @@ -163,7 +163,7 @@ def build_parser() -> argparse.ArgumentParser: | |||
| ) | |||
| _add_source_argument(analyze) | |||
| analyze.add_argument( | |||
There was a problem hiding this comment.
The original description should be kept here, and the behavior should be corrected.
|
|
||
|
|
||
| def test_a_dim_only_a_child_module_declares_binds(): | ||
| check_program(Rooted, Rooted.entry_function(), dims={"BT": 8}) |
There was a problem hiding this comment.
This method should not be used to test. It should be tested through fixture and cooperate with the cli entrance.
| assert timeline.end_ns - timeline.start_ns == -(-(_ROW * 2 * 1_000_000_000) // rate) | ||
|
|
||
|
|
||
| def test_one_position_is_bound_by_its_bytes_in_flight() -> None: |
There was a problem hiding this comment.
Remove this test and guard it by checking the analysis structure of fixtRue.
| @@ -0,0 +1,95 @@ | |||
| """A window every position writes alike moves its bytes once. | |||
Why
insert_slice/tf.zerosbilled whole windowsonce per mesh position (a 40-of-132-lane probe priced at predicted-ns
1,117,769 against a roofline floor of 13,654), and the lane under-fill
itself was invisible — the header read
wave=132/132on that probe.bound — the evaluator bound dims from entry argument shapes only, and
check --dimnever reached evaluation.analyze --json's help claimed it prints to stdout while theout
PATHwas always the destination; multi-function reports printed oneunlabeled
# memoryline per function although each record counts itsfunction plus everything it reaches (summing the lines double-counts);
check --json PATHvalidated nothing until after a full comparison run.What
InsertSlicewhose offsets name no mesh coordinate (directly,through a capture, or through loop bounds) and a
Zerosof a mesh-replicatedbuffer divide the counted unit's share of a target-owned level by the
position count, so
totalbills the window once. Mesh-coord offsets andtopology-owned levels (smem/rmem) keep per-position billing. Probe after:
predicted-ns 63,452, roofline unchanged; a 2x-destination variant drops
1,478,217 -> 72,463; a 132-of-132 variant 3,604,839 -> 63,452.
occupiedfigure — the most positions any meshthe selected function executes activates at each topology level. The text
header prints
lanes=40/132only when occupied differs from declared; theJSON wave always gains
"occupied".wave_ofsemantics untouched.evaluate(..., dims=)threads supplied extents through everyfunction boundary (a conflict with an argument-pinned shape errors naming
both values);
check --dimnow reaches evaluation. A dim declared only in adead binding errors honestly, naming the dims the program does reach and who
declares them.
-as outPATHstreams text or JSON to stdout; every# memoryline is labeled
function=… role=entry|stage covers=self+reachedwith atest pinning text
@total== JSONfunctions[i].totals;check --jsonrefuses a missing parent directory at parse time, naming the path.
Contract
docs/spec/analysis.md: a write every position performs alike at atarget-owned level is billed once (the
total = per-unit x positionsinvariant itself unchanged); the
lanesfield andoccupieddefinition;the Function-line grammar and the rule that per-function lines MUST NOT be
summed.
docs/spec/cli.md:-writes to stdout;check --jsonrefuses abad target at parse time.
insert_slice occurrences moved for billing; tutorial diffs are label-only.
Risk
totalcan overshoot one windowby less than one position's share.
old over-billing rather than inventing a division.
tests/installed/smoke_analyze.pyedits are unverified (needs an installedpackage).
analyze --compute-coston a mesh-lesscomposed module fails (missing cost context);
--memoryon the sameStopIterations in
wave_of.