Repository navigation
perf(runtime): matching ANode views the block instead of copying it (draft) - #2019
nicolas-abril wants to merge 3 commits into
Conversation
blk_half returns a view on the host. Freed views merge through midpoint bits and the last one frees the root, so splits no longer leak. Large blocks are aligned to their size for that, and a piece over 16x smaller than its root is copied so a slice can't pin a large block unbounded. comp.ts is over its ttok cap: this is a draft. Refs #1991 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view of it Matching an ANode views the block, but rebuilding one allocated a block and copied both halves into it, so a fork tree that splits, writes its leaves and joins still paid a copy per level. When both halves are live, unshared views, the left aligned to their parent and the right right after it, blk_node returns the parent as a view (or the block itself at its root). No sibling has died, so no midpoint bit is set, and freeing the result climbs from the parent as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This corrupts the heap in GPU runs. Host views assume the block they split is aligned to its own size, because Repro: each iteration, a bang fills a 2^16-cell
The second number is the sum of the array that only the host touched, so live host memory is being overwritten. It reproduces every time. There's also a leak in the other direction: the device has no views, so it frees a host view it receives as a plain block, and the whole block is never freed. With the host splitting a 2^16-cell array and a bang summing one half, peak memory goes 96 MB → 177 MB from 300 to 600 iterations (main: 20 → 25 MB, then flat around 33 MB). I'm working on a fix here: a view only of a block aligned to its own size, and the bitmap moved into the shared heap so the device frees views as well. Both repros will become tests. |
A view finds its parent by masking its address, so its block must be aligned to its own size. Only the host's large blocks are: the device's are aligned to a page, so when the host split an array a bang made, the second half to die freed memory that was not the block, and live host cells were overwritten. blk_half now splits a block in place only when it is aligned, and copies it otherwise. The device read a host view as a plain block and freed it as one, so its block was never freed. With a GPU the midpoint bits now sit in the heap past the banks, where both sides reach them (H[H_VIEW]), and the device frees views as the host does. A view of terms that dies moves its cells to a block of its own first, so term_drop no longer recurses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #1991. A draft to look at, not ready to merge:
comp.tsis 713 tokens over its cap, sogates/repo.tsfails, and no perf bench uses array splits yet. This doesn't fix #1991 either; see the numbers below.Matching
ANodecopies both halves today. Views were removed in64adb37dbbecause a block changed size class on every split and leaked (#804). This brings them back on the host and merges the pieces when they're freed, so nothing leaks. The device still copies.How it works
blk_halfreturns a view of the same block. Its aux has bit 5 set and the root block's span in bits 6-10.VIEW_BITS). The second sibling to die climbs a level, and the last one frees the root. This is why large blocks must be aligned to their own size.heap_alloc_misstakes only the gap it needs, with a CAS on the bump pointer. Only large host blocks use the CAS. Using it on every miss made GPU time much worse (merkle3.16x).blk_halfis forced inline and its copy path moved out toblk_part. Otherwise clang stops inlining it and loses the constantarrandhi.Results
All local on an ARM Mac, one thread, best of 3.
mainis017f83fd, which differs from this branch's base only inbend.ts.Array.to_listArray.getloopArray<U32>by matching, 1600 timesmap), 400 timesbfspeak memory, 8 threadsOn the #1991 repro
Array.to_listgets about 31% faster but still takes 2.8x the time and 4.2x the instructions of theArray.getloop, so a flat walk is still the direct fix for that issue.Rebuilding as a view (second commit)
Matching gave views, but
ANode{xs, ys}still allocated a block and copied both halves, so a fork tree that splits, writes its leaves and joins paid a copy per level. Nowblk_nodechecks whether both halves are live, unshared views, the left aligned to their parent and the right right after it. If so, it returns the parent as a view, or the block itself at its root. No sibling has died, so no midpoint bit is set, and freeing climbs from the parent as before. Halves out of order (ANode{ys, xs}) or from two arrays are still copied.tests/run/array_view_join.bendcovers all of these.This is the #1777 pattern written with plain
match/ANode, no@unsafe: 2^20U32s, a fork tree 4 deep, each leaf writing its 2^16 cells withArray.set, 300 rounds, one thread, best of 3 (local ARM Mac, on this branch rebased onto 05c0dba):match/ANode@unsafeArray.fork/Array.joinSo the safe version now matches the unsafe one. Not changed:
map-style code,ALeaf{f(x)}allocates a fresh one-cell block, so the bottom halves are never views. On 2^16 cells, 400 times, it is 0.21 s on every variant. Rebuilding a matched leaf in place would be a separate change.comp.tsat 66,881 / 66,000.Gates for this commit:
gates/test.tspasses 1820 / 1820 on the branch as pushed, and 1839 / 1839 rebased onto 05c0dba.gates/perf.tsgives 120 / 124 on both commits in back-to-back runs, and its time ratios swing 0.7x to 1.3x between runs on a busy cluster, so that is noise. Memory per bench matches the first commit within 0.2 MB.Views with a GPU (third commit)
The first commit corrupted the heap in GPU runs, and leaked there. See the comment below for the repro.
blk_halfsplits a block in place only when it's aligned, and copies it otherwise. This costs nothing on the host, whose large blocks are aligned already.H[H_VIEW], where both sides can reach them, and the device frees views the same way the host does. They're 32-bit words, throughatomic_fetch_xorwith a fence before and an acquire after, the way the reference counts drop. Without a GPU they stay in the separate 128 GB reservation: that heap grows by being laid out again, which copies the banks.Probes on an Apple M4, with the GPU on:
1408921600 477252523776 458745(wrong)1408921600 477252The last row's gap is code layout, not work. The leaves' 7-instruction loop retires the same 2.68 B instructions, but in this binary clang places it across a 64-byte line. Built with
-falign-loops=64, it runs in 0.09 s, the same as before. Without a bang, it's 0.09 s on both.tests/run/array_view_device.bendis the first probe at 4 iterations. It runs on the GPU in the test gate, and the first two commits fail it. The leak needs a heap the gate can't size, so it has no test.Costs:
comp.tsat 67,172 / 66,000.Gates for this commit:
gates/test.tspasses 1821 / 1821.gates/perf.tsgives 117 / 124, against 119 / 124 for the second commit, back to back. Memory is the same on every bench, and compile, parallel and GPU times are within 1% geomean. Sequential time is 1.04x geomean because ofkmeans(1.60x) andeditdist(1.21x). Run locally on one thread, both are identical to the second commit (8.29 B against 8.30 B and 8.46 B against 8.48 B cycles), so that's the cluster.Gates
gates/test.ts: 1819/1819 on this branch. It can't see leaks: the old leaking views passed it too. No test is added here.gates/repo.ts: fails,bend2/comp.ts: 66713 > 66000 ttok.gates/perf.ts, 5 rounds interleaved with main, run on017f83fd: CPU time 1.000-1.003x, GPU time 0.995x, memory within 0.2%, compile time 1.035x. It passed 124/124 three times and 123/124 twice, against main's five clean passes. The miss ismerklecompile at about 1.16x against the 1.15x limit, and main reaches 1.15x there too.Known costs
tree-radixgoes from 0.48 s to 0.68 s, not yet explained; it still passes its pin. Without it the compile geomean is about 1.015x.MAP_NORESERVEand stops if the mapping fails (with a GPU, the bitmap is in the heap). This has only run on macOS, so a Linux box with strict overcommit is a real question.🤖 Generated with Claude Code