Best-effort internal splitting for TrainerRank forwards - #831
Conversation
Contract: dp_rank_forward tries a bounded, deterministic split ladder (fewest subforwards that fit, partitioned along the prefix tree) before refusing; admission of each subforward accounts for the retained memory of earlier subforwards (conservatively the full estimate until a retained profile exists); outputs are reconstructed in caller order; refusal is worded as 'unable to find a feasible split' rather than a claim of infeasibility; the same machinery applies to forward_micro_batches' minimum wave; telemetry and MicroBatchStats report subforward_count. 7 of 8 gates fail on the pre-split tree; the cumulative-accounting refusal guard passes and must keep passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the unsplit plan (cost-optimal, then memory-minimal) cannot be admitted, TrainerRank now tries a bounded, deterministic split ladder (2, 4, ... subforwards, partitioned along the canonical prefix tree so shared-prefix siblings stay together) and executes the fewest subforwards that fit, keeping every returned graph live together and reconstructing outputs in caller order. The same ladder rescues forward_micro_batches' minimum wave. Admission follows the research thread's cumulative memory invariant: chunks are priced with the cheap no-sharing/full-sharing bounds (planner pricing only in between), execute larger-ephemeral-first, and subforward j is charged the retained memory of every earlier subforward; the backward upper bound (all retained graphs plus the largest reusable workspace) must also fit. The retained fraction is learned online per signature (conservative default 1.0, so a cold oversized call refuses until a profile exists). The complete ordered split is admitted before any execution; there is no retry after the first forward, and a later-subforward failure raises TrainerRankPartialExecutionError. Splitting is disabled under expert parallelism in this release. Refusals are worded as 'unable to find a feasible split', never as a claim of infeasibility. Telemetry gains subforward_count and subforward_request_indices; MicroBatchStats gains subforward_count. A test-only usable-memory cap (ART_TRAINER_RANK_TEST_MEMORY_LIMIT_BYTES, gated by the hooks flag) lets GPU gates induce conversion/decline deterministically. Gates: 8 CPU contract tests (written and fail-verified first) and a GPU split-conversion phase mirroring the sealed research cell (Qwen3.5-4B, 4 layers, CP1, 4 inputs): unlimited runs unsplit; a cap just below the unsplit requirement converts to 2 subforwards with output parity (mean 0.041%), combined and reverse-order backward with all graphs live; a sub-request cap refuses before execution. 496 CPU tests and GPU anchor parity green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rung check, ballast GPU arm Addresses the research-thread review of the splitting follow-up. - Retained fraction is a physical ratio (retained bytes / the same forward's observed peak), None until observed, max-merged afterwards, and trusted only within the profile's packed-token trust range and near its observed sharing ratio. Measuring it against the estimate clamped to 1.0 on the cold call (static estimate ~20x below the real peak) and, max-merged, pinned the signature to "everything retained". - Rung feasibility is one inequality: all retained memory plus the largest ephemeral share, which implies every cumulative per-subforward check in any order. The cheap full-sharing lower bound rejects a rung without planning; the planner runs only for the rung that executes. The backward term is documented as a heuristic and measured under real pressure, not claimed. - Checkpoint slots are ensured exactly once per call: the ensure is a world collective and the ladder's length depends on DP-local inputs, so per-chunk ensures would have deadlocked DP>1. Minimum-wave split outcomes are agreed across DP ranks with one collective. - Any execution-time memory failure of an admitted split raises TrainerRankPartialExecutionError (now in the public __module__ tuple); partitioning is described as prefix-local depth-first order. - Telemetry adds predicted_peak_bytes / usable_limit_bytes. - Tests: scale-trusted retained profile, max-merge once observed, one slot-ensure per call, partial execution on first/later subforward, independent slot-graph sentinels per subforward. - Acceptance: --pressure cap|ballast. The ballast arm sizes real pressure from the measured retained fraction (f=0.990 on this cell: a 141 MB conversion window on 28.6 GB), gates the observed forward+backward peak against both the admitted budget and the predicted peak, and adds a no_grad conversion (f~0) at 60% of the unsplit requirement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Second commit series addresses the research-thread review. Point by point: 1. Retained-profile hardening — done. 2. Backward term — kept as a heuristic, worded as one, and measured. Admission is now a single rung check, all retained memory plus the largest ephemeral share. That inequality implies every per-subforward check (transient peak plus retained-before-it) in any execution order, so it is the whole forward invariant; as a backward term it is the headroom the caller's backward can count on, which the code, design brief and PR now call a heuristic for backward workspace rather than an upper bound. The ballast arm gates on it: observed forward+backward peak must stay within both the admitted budget and the predicted peak (numbers below). 3. Ballast arm — done ( Cold policy — kept: refuse until profiled; no prior baked in. The forward-only nature of the observation is documented in Checkpoint-slot lifetime — added EP>1 — unchanged (explicit refusal). Planning-cost bound — done via the cheap precheck. The full-sharing lower bound per chunk (one O(tokens) CPU scan, from CPU copies of the rows made once per call) rejects a rung with no planning at all; a surviving rung is priced exactly with cost-optimal layouts and, failing that, memory-minimal ones, whose packed tokens equal the lower bound — so the planner runs for at most one rung, the one that executes. Minor — any execution-time memory failure of an admitted split (first subforward included) is One thing the restructuring surfaced that the review did not: |
… benefit is workload-dependent Per the research-thread review: do not generalize from the 99%-retained landing cell (the full-height research cell retained ~92%, so CP/GDN or output-heavy training shapes may have gigabytes of splittable transient memory). Record the two accepted limitations: conservative lower-bound refusals when profile trust changes with sharing ratio, and cold no_grad refusal until a compatible profile exists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Wording updated per the review: |
Summary
Follow-up to #826. When the unsplit plan (cost-optimal, then memory-minimal) cannot be admitted,
TrainerRank.dp_rank_forwardnow tries a bounded, deterministic split ladder — 2, 4, … subforwards, cutting the requests in prefix-local depth-first order into token-balanced chunks — and executes the fewest subforwards that fit, keeping every returned graph live together and reconstructing outputs in caller order. The same ladder rescuesforward_micro_batcheswhen even the minimum wave cannot fit unsplit.Contract (as agreed): try not to raise when splitting would make execution feasible; account for all returned graphs being live together; if finding out is too expensive or fragile, refuse — worded as "unable to find a feasible split", never as a claim that none exists.
Design
Noneuntil observed, then max-merged (an observed 1.0 is distinct from "unobserved"); trusted only within the profile's packed-token trust range and near its observed sharing ratio, so a small profiled forward cannot authorize a much larger split. Unobserved means 1.0, so a cold oversized call refuses until a profile exists. The observation is taken at forward return and says nothing about backward (documented limitation).TrainerRankPartialExecutionError(aTrainerRankMemoryError) naming how many subforwards completed. Each subforward's outputs carry their own slot-graph sentinel, so slot load/step stays blocked until every subforward's graph is released.last_forward_telemetry()gainssubforward_count,subforward_request_indices,predicted_peak_bytes,usable_limit_bytes;MicroBatchStatsgainssubforward_count. A test-only usable-memory cap (ART_TRAINER_RANK_TEST_MEMORY_LIMIT_BYTES, gated byART_TRAINER_RANK_TEST_HOOKS) drives the deterministic GPU arm.Design brief:
dev/trainer_rank_planner_design.md("Best-effort internal splitting").Evidence
CPU gates (
tests/unit/test_trainer_rank_split.py, 15 tests; the original 8 were written and fail-verified before implementation): split instead of raise; unsplit reports one subforward; nested caller-order reconstruction; bounded ladder + honest refusal wording with the planner never running for a failed rung; cumulative live-graph admission refuses when halves fit alone but not together; retained profile enables the split; profile trusted only near its observed scale; observations max-merged once observed; minimum-wave splitting; deterministic partitions; one slot-ensure collective per call; execution-time failure reported as partial execution (first and later subforward); independent slot-graph sentinels per subforward (load/step blocked until the last subforward's graph is released).GPU gate (
dev/trainer_rank_landing_acceptance.py --phase split-conversion), mirroring the research thread's sealed split-conversion cell (Qwen3.5-4B, 4 layers, CP1, 4 × 12,288-token inputs), fresh process per arm, H200:--pressure capunlimitedsubforward_count1); forward peak 26.0 GB, forward+backward 26.3 GB--pressure capconversion (cap just below the unsplit requirement)--pressure capbounded-decline (cap below one request)--pressure ballasttraining forward (no test hooks)--pressure ballastbounded-decline (budget 3.3 GiB, below one request)--pressure ballastno_gradforwardThe ballast arm's main lesson: splitting cannot shrink retained activations, only the transient share.
no_gradforwards (reference/old-policy logprobs) are the demonstrated high-value case, with wide-margin physical-safety evidence. The training benefit is workload-dependent and small in this sealed landing cell (~0.5%); the research thread's full-height cell retained closer to 92%, so CP/GDN, output-heavy or workspace-heavy training shapes may have several gigabytes of splittable transient memory, and grad-enabled support is kept for them.Regression: 537 CPU tests (acceptance, tree-equivalence, trainer-rank shard, split gates),
prek --all-files.Known limitations
no_gradcall likewise refuses until a compatible profile exists (a later simplification could modelno_gradretained memory directly from the known output bytes).🤖 Generated with Claude Code