Skip to content

Layer split: size device ranges by capacity and keep compute headroom under --max-vram - #32

Open
danielhanchen wants to merge 1 commit into
masterfrom
layer-split-proportional-ranges
Open

danielhanchen wants to merge 1 commit into
masterfrom
layer-split-proportional-ranges

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Summary

With --backend "diffusion=cuda0&cuda1" --split-mode layer, the layer-split partitioner filled the first device up to its weight capacity before assigning anything to the next one. A DiT whose weights fit device 0 was therefore never split, and its compute buffers then had no room. Passing a per-device --max-vram did force a split, but device 0 was still filled to its whole budget with weights and the run failed when the compute buffer was added.

This change sizes the device ranges in proportion to each device's capacity and keeps compute headroom inside each device's --max-vram budget. The range computation lives in a small header (layer_split_ranges.h) so it can be tested on its own.

Results

Kaggle T4x2 (two 15 GB T4s), Z-Image Q8_0, 768x768, 4 steps, same VM and inputs for master and this branch:

Arm master this PR
layer split, live free VRAM all 36 segments on device 0 18 / 18
layer split, --max-vram cuda0=5,cuda1=12 fails: needs 5,879 MB, budget 5,367 MB splits 12 / 24 and finishes

Every successful run is pixel-identical to the same binary's single-device render. Speed is unchanged (about 2.1 to 2.3 s/it everywhere): the gain is capacity, not throughput, since layer split runs one device at a time.

Not covered: row split, three or more devices, unequal devices (unit-level only), MiniMax-H3 (the run used Z-Image).

…mpute headroom under --max-vram

The partitioner filled the first device up to its weight capacity before
using the next one, so a module whose weights fit the main device alone was
never split, and the compute buffers then had no room under the budget. The
2 GiB compute headroom was also taken only from free memory, never from an
explicit --max-vram budget, so a per-device budget was filled entirely with
weights.

Ranges are now sized in proportion to each device's capacity (the lower of
free memory and its --max-vram budget, minus the headroom), as docs/backend.md
already described, with the fill-first plan kept as a fallback when the
proportional ranges do not fit. The planner is a header-only function with a
standalone test (scripts/layer_split_ranges_test.cpp).
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T13:32:21.568129Z 8c56b2b PR opened
🔒 Security Review ✅ Completed 2026-10-14T13:33:27.352406Z 8c56b2b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c56b2b0c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

std::vector<double> caps(capacities.size(), 0.0);
for (size_t i = 0; i < capacities.size(); i++) {
// An unknown capacity (very large sentinel) counts as able to hold everything.
caps[i] = capacities[i] > 0 ? std::min((double)capacities[i], total) : 0.0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve ratios between known device capacities

When the model's total weight size is smaller than each device's capacity, clamping every capacity to total erases all capacity differences and produces an equal split. For example, 4 GB of segments with 4 GB and 12 GB capacities are assigned 2 GB to each device rather than roughly 1 GB/3 GB, placing twice the intended blocks and their compute workload on the smaller device; this defeats the new headroom strategy and can cause that device to run out of compute memory. Only the unknown-capacity sentinel should be normalized without discarding ratios between known capacities.

Useful? React with 👍 / 👎.

@@ -0,0 +1,86 @@
// Standalone check of the layer-split range planner: g++ -std=c++17 -I src scripts/layer_split_ranges_test.cpp && ./a.out

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the committed standalone test script

Remove this standalone test from the commit: CONTRIBUTING.md explicitly requires test code and test scripts to remain local and only their verification results to be reported, while the repository's agent workflow makes those PR expectations applicable here. Keeping this file in scripts/ violates that submission boundary even though it is not wired into the build.

AGENTS.md reference: AGENTS.md:L120-L122

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant