Repository navigation
perf(layer-stream): pack the pinned arenas largest-first when that page-locks less (#1702) - #1713
Merged
Merged
Conversation
…ge-locks less (#1702) plan_pinned_arenas walked the tensors in allocation order and only looked at the arena it opened last, so a large tensor that arrives late stranded the space in front of it. A Qwen3-8B NF4 store is 3.58 GB of decoder tensors and then two 1.24 GB vocabulary matrices: each matrix opened a 2 GiB arena of its own, and 8 GiB was page-locked for a 6.07 GB store. The planner now also plans stable first-fit decreasing and returns it only when it page-locks strictly fewer bytes. A tie keeps the in-order plan, placement for placement, so a store that gains nothing is laid out as before. Placements stay indexed by the caller's tensor order. No config field. Qwen3-8B NF4, RAM tier: 8,589,934,592 -> 6,442,450,944 page-locked bytes. The disk tier's staging ties and is unchanged. The two Qwen2.5-14B geometries of #901 lose about 7% each (arithmetic). Speed is not measured by this commit: the plan and its decision rule are in #1702.
…any run (#1702) benchmarks/harness/issue1702_arena_pack_gate.py runs the plan of #1702: the in-order plan against the packed one, A B B A per seed, every run a fresh process, the RAM tier as the subject and the disk tier as the negative control. The in-order arm is made inside the process by pointing the planner's largest-first walk at its in-order walk. The schedule and every branch of the decision rule are held by tests/test_issue1702_arena_pack_gate.py. benchmarks/gate-1702-arena-packing.md records the rule, the protocol and the strict control, which is run: three deterministic optimizer steps on Qwen3-8B are byte-identical between the two plans on the RAM tier (8 GiB vs 6 GiB page-locked) and on the disk tier, 870 of 870 digests each. The speed series is not run yet.
11 tasks
…umber read off raw stdout (#1702)
…tale result, and does not time on battery (#1702) A rehearsal of the 40-run driver on Qwen2.5-1.5B found the laptop on battery: a step took 0.83 s against 0.31 s on AC. The driver now refuses to start a series on battery power and stops one when the box goes on battery, and a series with any run stamped on battery is INCONCLUSIVE. A run with no result after 15 minutes is stopped and counted as failed, and a result file left by an earlier run of the same name is removed before the run starts. All of it is written into the record before any run of the series.
…asurable at +/-15.1% (#1702) One series of 40 fresh-process Qwen3-8B runs, in-order against packed, A B B A per seed, in a declared window. 40 of 40 completed. The RAM tier's mean difference is +1.96% (per seed -2.71, +4.75, -1.41, +3.58, +5.61). The disk control's mean is -0.63%, so the series stands; its largest difference, 15.13%, is the band. By the rule written in #1702 before any run: no difference measurable at +/-15.13%, and the change is claimed as memory only. The band is wide because the control ran in two regimes; the record says so.
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.
Closes #1702.
What changes
plan_pinned_arenas(src/soup_cli/utils/layer_stream_runtime.py) walked the tensors inallocation order and only looked at the arena it opened last. A large tensor that arrives late
stranded the space in front of it.
It now plans two layouts:
_plan_in_order, same lines);_plan_largest_first): largest tensor first, each into the firstarena with room, placements written back at the ORIGINAL tensor indices.
The second is returned only when it page-locks strictly fewer bytes. On a tie the in-order
plan comes back, placement for placement, so a store that gains nothing is laid out exactly as
before. Same capacity rule, same 2 GiB ceiling, same alignment, same power-of-two trim. No config
field and no CLI flag.
Memory: measured
Qwen3-8B, NF4 double-quant, RAM tier, on
mainplus this change (RTX 5070 Laptop, torch2.14.0+cu130), real runs:
Disk tier: 4,294,967,296 bytes of staging in both arms. The layouts tie there and the in-order
plan is kept.
Arithmetic only, on the two Qwen2.5-14B geometries the #901 tests already hold: NF4 decoder
7,381,975,040 -> 6,845,104,128 (-7.3%), bf16 decoder 28,991,029,248 -> 26,843,545,600 (-7.4%).
Correctness: strict control passed
Three optimizer steps under
torch.use_deterministic_algorithms(True)andCUBLAS_WORKSPACE_CONFIG=:4096:8, one fresh process per arm and mode. After every step: SHA-256of the full logits, the loss, all 144 LoRA gradients and all 144 updated adapter tensors.
That is a statement about three deterministic steps: the two layouts hand the same bytes to the
same computation. It is not a claim about default execution or a long run.
Speed: no difference measurable at +/-15.1%
The plan and the decision rule were written in #1702 before any run and are copied into
benchmarks/gate-1702-arena-packing.md.benchmarks/harness/issue1702_arena_pack_gate.pyrunsit: in-order vs packed, A B B A per seed, 40 fresh-process runs, the RAM tier as the subject and
the disk tier as the negative control. The in-order arm is made inside the process by pointing
_plan_largest_firstat_plan_in_order; there is no switch in the config.One series, 2026-10-07 16:40Z-17:16Z, at
4e692d31, nothing else on the box, AC power in everystamp. 40 of 40 runs completed, no failed run.
ddThe control's mean is inside +/-3%, so the series stands. The band is 15.13%, the largest
|d|inthe control. The RAM tier's +1.96% is inside it: no difference measurable at +/-15.13%, and
this PR claims memory only.
The band is wide because the control ran in two regimes: 15 of its 20 runs have a median step of
1.86-2.01 s and 5 have 1.62-1.64 s, and two seed blocks were split between them. The plan is the
same in both arms on the disk tier, so that is the box, not the change. The RAM tier shows no such
split, but the rule takes the band from the control and the rule was written first. So the series
cannot exclude a change of less than 15% in either direction, and it shows none.
The driver was rehearsed end to end on Qwen2.5-1.5B beforehand (40 of 40 runs completed; not a
measurement of anything). The rehearsal found the laptop on battery, where a step takes 0.83 s
against 0.31 s on AC, so the driver refuses to time on battery and a series with any run stamped
on battery is INCONCLUSIVE. That rule was added to the record before the series ran.
Tests
tests/test_issue1702_pinned_arena_packing.py(17 CPU tests, 1 on CUDA):in-order plan;
it); equal sizes keep their order; zero-byte and over-ceiling tensors keep their contracts;
than the in-order one, and equals it on a tie (339 packed, 61 kept);
allocator exactly the arena sizes, and round-trips through non-blocking host-to-device copies.
Written first: against the planner without the choice, 11 tests fail on the old numbers (10 in
this file, 1 in the #901 file).
Mutations, each killed by a named test:
<-><=)test_equal_cost_disk_staging_retains_original_placementstest_the_same_arena_count_is_not_a_saving_when_the_trim_rounds_uptest_late_vocabulary_weights_reuse_the_decoder_arenas_slacktest_placements_stay_indexed_by_allocation_ordertest_late_vocabulary_weights_reuse_the_decoder_arenas_slacktest_late_vocabulary_weights_reuse_the_decoder_arenas_slacktest_zero_byte_and_oversized_tensors_keep_their_contractstest_late_vocabulary_weights_reuse_the_decoder_arenas_slacktest_equal_sizes_keep_their_allocation_ordertests/test_issue1702_arena_pack_gate.py(44 CPU tests) holds the gate's schedule, every branchof its decision rule and what the driver makes of each ending of a run (finished, hung, died,
stale result, battery power); 24 mutations of the harness are each killed.
One existing assertion changes:
test_an_outlier_raises_the_arena_for_the_store_but_never_past_the_ceilingin
tests/test_issue901_pinned_arenas.py. Its last arena is now 128 MiB instead of 512 MiB,because the 300 MiB outlier is placed first.
Local runs: 932 passed, 9 skipped on the 21 test files that touch the planner, the two sources
or the probe harness (CPU); 7 passed with
-m gpuon the three arena files.ruff checkclean.One interaction to know about: #1531 / #1621
On the disk tier a pinned arena whose base comes back off a sector boundary is shifted inside
the arena, and replaced by the next power of two when it has no slack. A packed arena is filled
more tightly, so if one is filled exactly to its power of two AND its base is misaligned, it
costs the next size up, the same as an in-order arena that happens to be full. The regions stay
aligned and correctly indexed
(
test_a_full_packed_arena_off_a_sector_boundary_takes_the_next_size_up). Staging regions arewhole sectors, so an arena has either no slack or at least one sector of it, and "no slack"
needs the region sizes to sum to an exact power of two. In the measured disk geometries the
plan is the in-order one anyway.
Not covered
A physical 4 GB card, Llama checkpoints, and any model other than Qwen3-8B on real hardware.