Repository navigation
Pack line by line when a sliver would read one element per cache line - #11
Merged
Merged
Conversation
intensli_7, C[e,c,b,f,a] = A[a,b,c,d,e] * B[d,f]: the M composite follows
C (e,c,b,a), so a pack-A sliver reads MR values of e (A stride dim^4) over
kc steps of d (dim^3), one cache line per element, and each line's other
elements (A's unit-stride a) are needed only dim^3 M rows later. At
power-of-two dims those lines alias in a few sets of every cache level and
none survives: pack A costs one memory latency per element (Rome dim16 58.8
ns/elem, dim32 148; Cascade Lake dim24 45, dim32 55; padding every axis:
1.4).
Plan: `_pack_split` (src/planning/pack_split.jl) decides per operand, in
`_plan_contract`, whether the sliver axis steps the operand by a line or more
while a later axis is unit-stride and the per-sliver walk's reuse window
exceeds the core's L2 share (twice that for complex). A split enumerates the
free composite with a line's worth of the unit-stride axis right after a
prefix of E coordinates, and makes mc/nc whole E x L groups. The decision is a
12-byte `PackSplit` per operand in the plan.
Path: `_NestPath{UNPACKED_B, AFF, SPLIT}` carries the split flags, set by
`_select_path` from the plan; only that path enumerates `_split_group` and
packs each block through `_pack_block_transposed!` (src/packing/
transposed.jl), which reads every line once, K innermost when K steps stay
within a page. Unsplit plans compile main's nest unchanged. The TensorOperations
path hint predicts no split, so `_hint_holds` still folds and a split plan
crosses the existing barrier to its own path.
Compile latency and call floor: the structural test is inlined, the window test
is out of line and eltype-independent, and the block-extent search is behind a
dynamic call reached only by plans that split (96 B per `tensorcontract!` on
those; `execute!` 0 B). A fully static decision cost +0.2-0.3 s first call;
a 96-byte `PackSplit` pair cost ~20 ns per call.
Verification on this workstation (shared, timings indicative only):
- Plans of all 852 :contract suite case x eltype combinations (synthetic,
TCCG 8-32, batched; F32/F64/C32/C64) are identical to main except the 45
that split: intensli_6/7/8 dims 12-32 (complex from 16), intensli_3/4 dim32
real, ccsd_7_dim32 Float32.
- intensli_7 via tensorcontract!: F64 dim24 235 -> 100 ms, C64 dim24
484 -> 203 ms, F64 dim16 5.7 -> 4.9 ms.
- probe_ttfx first call: main F64 2.9-3.2 s / C64 2.0-2.2 s, branch
3.0-3.1 s / 2.0-2.1 s.
- probe_call_floor: 0 B/call everywhere, within ~10 ns of main.
- Full test suite passes (29689).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same-node A/B of d1ff0e0 (main, branch, branch, main in one job; QS median, branch/main, best of two) showed the split winning for real eltypes at dims >= 16 but losing in two regimes: * K steps within a page (intensli_4/6/8): each sliver element's lines are then a stream the prefetchers follow, so the split only saves refetching them. Float64 still gains on icelake-server and znver4 (0.37-0.85), but ComplexF64, with 4 elements per line and a costlier planar scatter, lost on znver4 (intensli_6/8 dim24/32: 1.22-1.36) and znver2 (dims 16-32: 1.15-1.27, intensli_4_dim32 1.25); icelake-server gained 0.71-0.97. * A reuse window that exceeds L2 but whose sliver pitch is not a page multiple (dim 12: 13.5 KB, 162 KB): the lines stay in the L3 share. Float64 intensli_6/7/8_dim12 lost on znver4 (1.21/1.32/1.11), were neutral on znver2 (0.98-1.00) and gained on icelake-server (0.73-0.76). Rule now (`_pack_split_window`): * complex operands whose K step is within a page never split; * the window must exceed the core's L2 share when the sliver pitch is a whole number of pages (the gathered lines fold into a few sets of every level), else L2 + the core's L3 share (`_split_capacity`); doubled for complex as before. Splits per profile (suite :contract cases, deterministic, cache sizes of the benchmark nodes: icelake-server 8362 L2 1.25 MB / L3 1.5 MB per core; znver4 9474F 1 MB / 5.3 MB; znver2 7742 512 KB / 4 MB): icelake, genoa (29 each): Float64 and Float32 intensli_3/4_dim32, intensli_6/7/8_dim16/24/32 (+ ccsd_7_dim32 Float32); ComplexF64 and ComplexF32 intensli_7_dim16/24/32. rome (36): the same, plus Float64 ccsd_7_dim32, intensli_3/4_dim24; ComplexF64 intensli_3_dim32, intensli_7_dim12; ComplexF32 ccsd_7_dim32, intensli_3_dim32. Every other plan is identical to main on each profile (852 per profile). Kept wins (node A/B of d1ff0e0): intensli_7 Float64 dim16/32 0.05-0.63, ComplexF64 dim16/32 0.20-0.90, intensli_3_dim32 Float64 0.33-0.67, intensli_6/8 Float64 dim16-32 on icelake-server/znver4 0.37-0.85. Given up: icelake-server Float64 dim12 (0.73-0.76) and ComplexF64 intensli_6/8 (0.71-0.97). Still split and still losing on the measured nodes: ComplexF64 intensli_7_dim24 (znver4 1.06, znver2 1.13; icelake 0.86) and znver2 Float64 intensli_4_dim24/32 (1.10/1.33), intensli_6_dim16 (1.11). Nothing in the cache profile separates these from the kept wins. Tests: the forced-split contractions give intensli_7 page-sized K steps and run the both-operand (K within a page) case for real eltypes only. Full suite passes (29625). probe_ttfx first call: main Float64 3.1-3.2 s / ComplexF64 2.1-2.3 s, branch 3.2-3.3 s / 2.2 s. probe_call_floor: 0 B/call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The planner test assumed the host's L2 is below the dim-16 reuse window (4 MB), which fails on Apple silicon CI runners with a larger L2. Check the decision at fixed capacities instead, and that the host plan agrees with the decision at the host's own capacity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lkdvos
added a commit
that referenced
this pull request
Sep 29, 2026
Path marker: `_nest_path` takes both split flags and a `panel` flag; the
panel path is `_PanelPath{_NestPath{U, AFF, SPLIT}}` from a second
512-entry table, its C ramp flags `true` unless that side is split. Only
the chosen path is compiled.
Hint: `_hint_holds` still checks the panel decision; `_splits` and
`_split_path` now also cover a `_PanelPath` hint, so a split plan on the
TensorOperations route crosses the barrier to its own panel path.
Planning: `_pack_split` sees each operand's storage eltype size (lines,
page test, K walk) and whether its packed format is complex (the walk's
scatter cost), not `T`; for same-eltype plans these equal the previous
arguments. B is not split when C needs a panel: the panel holds `jc`
blocks in C's own N order, which a split N group does not enumerate
contiguously. A split under the panel is fine (M rows land at their
natural panel offsets).
Packing: `_pack_block_transposed!` converts to `_element_type(format, T)`,
so a real operand on a mixed-domain kernel's `RealFormat` side packs as
real (it was a MethodError in `panel_store!`). test_mixed.jl gains a
forced-split intensli_7 row on the RealComplexKernel.
Same-eltype code_llvm of execute!, _select_path, _execute_path!,
_execute_hinted!, _hint_holds and _execute_nest! (F64, C64: unpacked B,
packed B, forced A split) equals origin/main's after normalising
numbering, apart from the workspace's dereferenceable size.
Co-Authored-By: Claude Opus 5.5 <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.
Pack line by line when a sliver would read one element per cache line
Problem
TCCG
intensli_7,C[e,c,b,f,a] = A[a,b,c,d,e] * B[d,f]: the M composite follows C (e,c,b,a), so a pack-A sliver reads MR values ofe(A stride dim^4) over kc steps ofd(dim^3). Every element is its own cache line, and that line's other elements (A's unit-stridea, the slowest M coordinate) are needed only dim^3 M rows later. At power-of-two dims the lines touched in between alias in a few sets at every cache level and none survives: pack A costs one memory latency per element (Rome dim16 58.8 ns/elem, dim32 148; Cascade Lake dim24 45, dim32 55). Padding every axis by one element gives 1.4 ns/elem, which confirms the diagnosis.Fix
Plan (
src/planning/pack_split.jl)._pack_splitruns once per operand in_plan_contract. A group splits when all of these hold:A split plan enumerates the free composite with one line's worth (
L) of the unit-stride axis right after a prefix ofEcoordinates. The result is a 12-bytePackSplitstored in the plan (mpack/npack).mc/ncbecome wholeE x Lgroups and may use the budget the blocking reserved for the requestedkcwhen the K extent clamps it. The packed format, the K order and the kernels do not change.Path (
src/execution/barrier.jl)._NestPath{UNPACKED_B, AFF, SPLIT}carries the split flags._select_path(plan)reads them from the plan, so execution just follows the path. The path table grows from 128 to 512 singletons. A split group's ramp flags arefalse.Execution (
src/execution/execute.jl,src/packing/transposed.jl). Only the split path enumerates_split_groupand packs each macro block with_pack_block_transposed!, which reads every line once and scatters it intoLslivers' panels. The K step is innermost when K steps stay within a page. The walker is@noinline: inlined, it slows the nest's micro-kernel loop (unsplit ccsd_t_3_dim16 was 1.17-1.24x slower in an earlier in-process A/B). Unsplit plans compile exactly main's nest.Compile latency:
_hint_holdsstill folds for automatic kernels.tensorcontract!allocates 96 B per call (boxed arguments). This happens only for contractions whose pack walk misses L2 (milliseconds and up).execute!stays at 0 B.When a group splits
A group splits only if the natural walk can neither keep its lines nor stream them:
Which plans change (deterministic)
Plans were dumped for 852
:contractsuite plans per profile, using the benchmark nodes' cache sizes. Every plan that does not split is identical to main.Measurements
Same-node A/B on one core per node class (icelake = Xeon Ice Lake, genoa = EPYC Zen 4, rome = EPYC Zen 2): one allocation runs main, branch, branch, main over the
:contractsynthetic and TCCG cases (TCCG dims 12-32), Float64 and ComplexF64. Values are branch/main QuasiStrided time, best of the two rounds (< 1 = faster); cases with every ratio within 5% are omitted. No correctness mismatches against StridedBLAS.Float64
ComplexF64
Known remaining losses: ComplexF64 intensli_7_dim24 on genoa and rome, and Float64 intensli_4_dim24 on rome still split and are 1.09-1.16x slower. Nothing in the cache profile separates them from wins with the same layout on the other nodes, so they are left rather than special-cased. The first node round (d1ff0e0, split rule without the complex and L3 conditions) lost 1.11-1.36x on genoa/rome for ComplexF64 intensli_4/6/8 and Float64 dim-12 cases; this rule no longer splits those (now 0.96-1.02).
Compile latency and call floor (local, indicative):
probe_ttfx.jl, 3 interleaved runs): main Float64 3.1-3.2 s, ComplexF64 2.1-2.3 s; branch 3.2-3.3 s and 2.2 s.probe_call_floor.jl): 0 B/call in every case, timings within noise of main.Tests
The new tests pin the new behaviour:
tensorcontract!through a split plan matches StridedNative.The full suite passes.
🤖 Generated with Claude Code