Measure the memory hierarchy and threadgroup memory, which reorders the target model - #31
Merged
Merged
Conversation
… target model
meTile recorded one bandwidth number, 120.6 GB/s streaming from DRAM, and it is not the whole story.
The gap surfaced by accident while scoping attention projections: a 786 KB weight read at 254 GB/s,
twice the recorded ceiling, so something above DRAM was serving it.
Sweeping working-set size finds the levels:
<= 2 MB 2386 GB/s 19.8x DRAM
4 MB 555 4.6x
8 MB 192 1.6x
16 MB 161 1.3x
32 MB 134 1.1x
>= 64 MB 121-128 1.0x
The knee at 2 to 4 MB is a factor of four in one step. That makes fitting a working set the largest
lever in metile/target/agx.py by some distance: choosing the matrix unit over scalar is worth 2.4x to
3.7x, and instruction scheduling is capped at 1.09x and measured unreachable above MSL. A tiling that
fits and a tiling that misses are not the same kernel, and until now the compiler had no way to know
the difference. `read_bandwidth_gbps`, `resident` and `tiling_gain` give a pass something to consult.
Two controls, because a 19x claim earns them. The pass loop re-reads the same addresses, so the
backend could in principle collapse it into a multiply and the fast numbers would be fiction: tripling
the traffic triples the elapsed time at every size, which a collapsed loop could not do. And the sweep
has to reach the known 120.6 GB/s at large sizes or the thread count is too low to saturate and every
number is a lower bound on something else -- it reaches 120.5, and the probe reports the check rather
than assuming it.
The resident regime is recorded as one number rather than four, and finding out why was the useful part.
It measured 1545, 2006, 2138 and 2386 GB/s at 256 KB, 512 KB, 1 MB and 2 MB: bandwidth *rising* with
working set, which cannot be a property of a cache. It is the pass loop again -- a smaller working set
means fewer inner iterations per pass, so bookkeeping takes a larger share, and the effect shrinks as
the set grows. Only the 2 MB figure is close to uncontaminated and it is a floor. Publishing four
numbers would claim a resolution the measurement does not have.
A monotonicity test over the table is what caught that, twice: first the 256 KB entry reading slower
than 512 KB, then 512 KB reading slower than 1 MB. A larger working set is never served faster, so a
table that says otherwise is measuring the harness.
683 pass. Lint and vulture clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…culation was right
The shared-memory passes -- cooperative loads, padding, swizzling, double buffering -- all assume
threadgroup memory is the fast place to put data. On this part that needed checking, because device
memory is not slow when resident: 2386 GB/s under 2 MB against DRAM's 121. The question is not whether
threadgroup memory is fast but whether it beats the cache that would have served the same bytes.
It does, barely. 3361 GB/s contiguous against 2749 for the same read from resident device memory, so
1.22x. That is a much smaller margin than the usual assumption about scratchpad memory and it means no
pass can justify staging on bandwidth alone.
What it gains in peak it gives back in fragility. Across per-lane strides the threadgroup arm spreads
7.69x and the device arm 1.80x, which inverts the usual habit of treating shared memory as forgiving
and device memory as the thing needing careful access. Here device memory is the forgiving one.
The collapses are bank aliasing, and the sweep separates that from any size effect:
stride shared device
16 B 3361 2749
128 B 1216 2079 <- power of two
144 B 2322 2341
256 B 605 2004 <- power of two
512 B 437 1641 <- power of two
144 bytes is one vector larger than 128 and reads nearly twice as fast, so this is alignment rather than
distance. Thirty-two banks of four bytes puts every lane on the same bank at 128.
That confirms `_optimal_pad`, which was written from a stated model of 32 four-byte banks and never
measured. It pads to an odd stride, odd strides do not collapse, so the pass was right for the reason it
claimed; a test now ties the two together so the measurement and the pass cannot drift apart.
Getting here took a wrong diagnosis worth recording. A per-thread span of eight vectors measured about
half speed, which looked like a conflict, so an arm was added that padded the span by one to break it up.
Padding made it worse -- the unpadded span was seven and adding one moved it *onto* 128 bytes rather than
off. Padding is not a direction, it is an arithmetic result, and plus one is not automatically safe. The
sweep replaced the guess.
Controls throughout: elapsed time scales with the pass count at every stride, min 1.88x max 2.00x on
doubling, so no loop is being collapsed.
686 pass. Lint and vulture clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
meTile recorded one bandwidth number — 120.6 GB/s streaming from DRAM — and it isn't the whole story.
The gap surfaced by accident while scoping attention projections: a 786 KB weight read at 254 GB/s, twice
the recorded ceiling, so something above DRAM was serving it.
The hierarchy
The knee at 2→4 MB is a factor of four in one step. That makes fitting a working set the largest lever
in the target model by some distance — choosing the matrix unit over scalar is worth 2.4–3.7x, and
instruction scheduling is capped at 1.09x and measured unreachable above MSL. A tiling that fits and one
that misses are not the same kernel, and until now the compiler had no way to know.
read_bandwidth_gbps,residentandtiling_gaingive a pass something to consult.Threadgroup memory is barely faster, and much more fragile
The shared-memory passes all assume it's the fast place to put data. Measured against a resident device
read over the same 32 KB:
Contiguous, staging wins 1.22x — a far smaller margin than the usual scratchpad assumption, so no pass
can justify staging on bandwidth alone. And across strides the shared arm spreads 7.69x against the
device arm's 1.80x, which inverts the habit of treating shared memory as forgiving and device memory as
the thing needing careful access. Here device memory is the forgiving one.
144 bytes is one vector larger than 128 and reads nearly twice as fast, so the collapses are bank
alignment, not distance — 32 banks of 4 bytes puts every lane on the same bank at 128.
This confirms
_optimal_padIt was written from a stated model of 32 four-byte banks and never measured. It pads to an odd stride,
odd strides don't collapse, so the pass was right for the reason it claimed. A test ties the two together
so they can't drift apart.
Three controls, and two wrong turns
multiply. Tripling traffic triples elapsed time at every size; doubling passes doubles it at every
stride (1.88–2.00x). It isn't folded.
and everything is a lower bound. It reaches 120.5, and the probe reports the check rather than assuming.
256 KB reading slower than 512 KB, then 512 KB slower than 1 MB. That's the pass loop's bookkeeping
taking a bigger share at small sizes, so the regime is recorded as one number (2386, a floor) rather
than four that would claim resolution the probe doesn't have.
conflict. It got worse — the unpadded span was 7 and +1 moved it onto 128 bytes. Padding is an
arithmetic result, not a direction.
686 pass. Lint and vulture clean.
🤖 Generated with Claude Code