Skip to content

Price trainer-rank memory profiles by packed tokens, not sharing ratio - #950

Merged
bradhilton merged 13 commits into
mainfrom
dalinar/planner-sharing-ratio
Sep 24, 2026
Merged

bradhilton merged 13 commits into
mainfrom
dalinar/planner-sharing-ratio

Conversation

@bradhilton

@bradhilton bradhilton commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Memory profiles were extrapolated by the prefix-sharing ratio: bytes_per_token * max(packed, logical / profile.logical_per_packed). So a profile learned without sharing priced a shared plan by its logical tokens. For grad-enabled single-target training requests of at least 64 tokens, this PR prices the profiled model memory by packed tokens, charges head and caller memory per logical token, and extrapolates sharing at most 8x beyond what the profile observed. Everything else keeps the existing extrapolation.

Evidence

Production report. Caladan 061 (Qwen3.6-35B-A3B, 1×H200, ART a655c051), planner report f3f203ed. The first multi-request batch (16,205 packed / 109,065 logical tokens) came right after a single-request calibration step (logical_per_packed = 1.0). The planner predicted a 56.3 GiB peak against 6.9 GiB observed. Its required estimate was 66.5 GiB with 67.7 GiB available, so it was admitted with 1.7% headroom. Across all shared steps in that run, observed peaks fit 66.6 MiB + 459,782 B/packed token.

Controlled measurement. One H200, 8 layers, random weights, 16,384 packed tokens with 512-token suffixes, sharing ratios 1/2/4/8, 3 shuffled repeats. The table shows peak bytes per packed token after warm-up:

model family ratio 1 ratio 8 old prediction on first ratio-8 call
Qwen3.6-35B-A3B GDN-MoE 321.9K 323.5K 18.38 GiB (steady 8.46)
Qwen3-30B-A3B attention-MoE 282.1K 282.0K 16.77 GiB (steady 7.72)
Qwen3-8B dense attention 271.2K 271.2K 39.46 GiB on every repeat (steady 5.01)
Qwen3.5-4B dense GDN 235.8K 237.4K 4.91 GiB

High-branch GDN measurement (same setup, 1 H200, 8 layers, raw log retained). This compares 101 twenty-token branches on a 5,100-token prompt (sharing ratio 72.6, 101 segments) with a single 7,119-token request. The warm peak per packed token rises from 323 KB to 378 KB (+17%), about 3.9 MB per extra segment. That matches one GDN layer's recurrent state (≈4.3 MB per segment), the amount live at a time under full recompute, and is now charged per segment.

The dense rows come from a retained raw log. The MoE rows were transcribed from a live log that was lost when the cluster auto-downed, so treat them as provisional. Ratios 2 and 4 are also flat. The only deviations are the first two calls of each run (cold allocation and two micro-batches), which aren't sharing effects.

One consequence of the old pricing: under a ratio-1 profile, memory-minimal (full-sharing) layouts cost as much as unshared ones, so the split ladder couldn't use sharing to fit a budget.

Changes

Supported invariant

Packed pricing assumes:

  1. Model memory scales with packed rows at the profiled rate B.
  2. Head memory plus the caller's loss memory (saved tensors and backward transients) peaks at no more than 6 KiB per active logical token.
  3. Every packed-priced request has at least 64 tokens, so per-request constants up to 384 KiB, head blocks included, fit within that budget.

Callers heavier than item 2 are unsupported, and the forward_micro_batches docstring says so. main's own assumption, that memory scales with logical rows at the profile's density, is likewise unenforced. The invariant was agreed with both private reviewers before implementation.

Pricing

  • Eligibility (_packed_priced(signature, one_layer_recompute)), all of:

    • ART's default one-layer full recompute, the only mode the evidence covers. That is exactly full / uniform / 1, read at pricing time from the live decoder config, with the decoder in training mode (Megatron checks the decoder's own mode and skips recompute in eval mode). A chunk without a GPT decoder falls back to the constructor settings and requires every submodule to be training.
    • Every grad mode is True.
    • Every request mix is target:single or inactive.

    There is no rate floor. An earlier floor made eligibility depend on the learned ratio, which could switch the logical charge off exactly where sharing is highest.

  • Short requests. A batch containing a single-target request shorter than 64 tokens sets short_requests on its memory signature and keeps the logical extrapolation. A duplicate short request adds no packed row, and per-request caller memory can outgrow a few row charges. The flag is excluded from signature identity, so such a batch shares the profile learned from longer requests and is priced exactly as on main, rather than starting cold.

  • Required = B·max(P, L/(8r)) + 6 KiB·L + grad_segments·GDN_state:

    • P is packed rows, L active logical rows and r the profile's sharing ratio.
    • The logical charge does not depend on layout, so the cost never falls as packed rows grow, and lower ≤ exact holds for split pruning; lower bounds pass zero segments.
    • Beyond 8x the observed sharing, packed rows are priced as if the sharing were 8r, so the model term grows at B/(8r) per logical token rather than main's B/r.
    • The GDN term is one layer's fp32 recurrent states plus conv history per grad segment; backward recomputes one layer at a time.
  • Retained, inside the existing trust windows (ratio ≤ 8r, P ≤ 8x the profile's): R·P + 6 KiB·L. Outside them it falls back to required, as before. The existing split-search guard for retained trust crossings stays.

  • The 6 KiB charge (12 allocator blocks). Each request's head buffers are separate allocations rounded up to 512 B blocks. On an H200, through the real head projection path (a stub bf16 output layer) and backward, a duplicate one-token request peaked at nine blocks plus 8 B (4,616 B) at the end of head forward, and 3,080 B stayed live into backward. The slope was the same at 20,000 and 100,000 duplicates. 061's CISPO caller saves one or two tensors per request (512 B–1 KiB, plus about 5 B per token).

  • Cost. These figures come from the formula with the measured rates:

    • 061's first shared batch: about 8.3 GiB, against 6.9 GiB observed and the report's 56.3 GiB prediction; the logical charge adds 639 MiB, and the clamp is inactive at ratio 6.7.
    • Controlled ratio 8: about 6.3 GiB, against 4.9 GiB observed (the charge is 15% of B·P).
    • The ratio-72.6 GDN branch case: 25 GiB on the first call from a ratio-1 profile, then about 6.1 GiB once the ratio is learned. Observed: 2.5 GiB; main: 171 GiB.
  • The recompute flag is recorded in planner reports and restored on replay. Reports written before this change fail replay as incomplete rank fields, as with earlier new fields.

  • _request_mix_key also classifies labels by their normalized shape, matching _forward_item: an input of shape (1, N) with targets (N, K) is target:(K,), not target:single.

  • Width probes still assume up to two GDN segments per request (an upper bound). Under packed pricing that adds a per-segment charge to the cheap feasibility bound, so a probe can reject a width the exact plan would admit. This is conservative: it can never admit more.

  • Why the scope. Private review found memory that grows with logical rows or segments outside the head and caller budget: wide-label index copies, backward gradients of grad-enabled logits/hidden_states, and GDN branch states in no_grad forwards. The measured and production evidence covers grad-enabled single-target training, so only that path changes.

Tests

  • CPU:
    • The packed-pricing scope test pins exact values for:
      • packed and extrapolated signatures (no_grad, mixed grad, dense output, wide labels) at profile ratio 2;
      • retained charges and the 8x clamp;
      • a packed-row sweep at rates of 1, 24,575 and 100,000 bytes per packed token, where cost never falls;
      • the per-segment GDN charge;
      • recompute detection from the live decoder config and mode;
      • the recorded replay flag.
    • The short-request gate: 63/64/65 tokens for (N,) and (1, N) inputs, with inactive unaffected. After calibrating on a 64-token request, a 63-token batch and a mixed batch reuse that profile (none pre-seeded) and price at main's extrapolation.
    • The clamp: a packed sweep across L/(8r) is monotone, and learning more sharing never cheapens a plan at or below the older ratio.
    • Replay rejects an unrecorded recompute mode.
    • Tests that exercise packed pricing with short fixture requests set the threshold to 1; tests of main's path keep main's expectations.
  • CUDA (skipped without CUDA; they run in the 2xH200 CI job):
    • The per-request slope through the real head, a caller loss and backward, against the charge on the request's logical rows: head only at 1 token; 061's CISPO loss at 64 and 512 tokens; a caller saving a fixed 256 KiB per request at 64 tokens.
    • A negative control: that fixed caller on one-token requests exceeds the charge, which is why short requests are gated.
    • A whole-call check: a profile learned from 16 shared 4,096-token requests with the fixed caller must admit 20 shared 64-token requests at an estimate above their measured peak.

Validation

  • Rebased onto main after Apply trainer-rank memory floors per context-parallel rank #951 merged.
  • CPU-only (CUDA_VISIBLE_DEVICES=""), full tests/unit/test_trainer_rank_*.py plus test_prefix_tree_packing.py on the current head: 1,248 passed, 8 CUDA-only skips. The three failures also fail on current main in isolation: the live-graph collective test hits its wall-clock "hung" deadline, and test_native_checkpoint_gather_after_recovery fails a different pair of variants on each run.
  • Private review: after twelve rounds, the supported-caller invariant was agreed with both reviewers before implementation. Round thirteen found nothing blocking at this head.
  • CUDA on one H200: the six new tests pass (head slope; CISPO at 64 and 512 tokens; the fixed caller at 64 tokens; the one-token negative control; the calibration transition). CI's 2xH200 job also runs this file.
  • Lint and types: ruff and ty are clean.
  • Measurement mode: the controlled measurements ran forward plus loss.backward() (grad-enabled, target_tokens only), and the 061 reports come from grad-enabled training. This covers GDN models with up to 21 segments per batch.

🤖 Generated with Claude Code

@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 14:17 — with GitHub Actions Active
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 15:03 — with GitHub Actions Active
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 15:24 — with GitHub Actions Active
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 16:12 — with GitHub Actions Active
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 16:43 — with GitHub Actions Active
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 17:19 — with GitHub Actions Active
@bradhilton
bradhilton had a problem deploying to trainer-rank-gpu-validation September 24, 2026 18:03 — with GitHub Actions Failure
@bradhilton
bradhilton had a problem deploying to trainer-rank-gpu-validation September 24, 2026 18:32 — with GitHub Actions Failure
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 19:07 — with GitHub Actions Active
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 20:01 — with GitHub Actions Active
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 20:35 — with GitHub Actions Active
bradhilton and others added 12 commits September 24, 2026 21:30
A memory profile learned at a lower logical/packed ratio was scaled up by
the ratio gap, so a profile observed without prefix sharing priced a shared
plan by its logical tokens. Measured forward+backward peaks are flat per
packed token across sharing ratios 1-8 for GDN-MoE, attention-MoE, dense
attention and dense GDN models, so apply profiled bytes per packed token
in both the required and retained estimates. Sharing-ratio trust windows
are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up. Profiles are per packed token, so memory that grows with
logical rows is priced with outputs: the head's int64 copy of wide (2-D)
labels, and the training gradient of dense logits/hidden-state outputs for
requests not marked no_grad. Also remove the estimator logical_tokens
argument from the landing harness and two tests that still passed it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Second review round: moving logical-sized label copies and output gradients
into output_bytes made profile learning subtract bytes that forward-only
observations never held. Instead, keep main's output accounting and the
estimator's logical_tokens argument, and skip the sharing-ratio extrapolation
only when every request mix is single-target or output-free, the case the
production reports and controlled measurements cover. Wide labels and dense
or top-k outputs keep the conservative extrapolation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Third review round: key wide labels by their normalized shape (a flattened
token axis with trailing target dimensions is not single-target), restrict
packed pricing to fully grad-enabled signatures (no_grad GDN branch states are
not charged), and charge 128 B for each logical row beyond the profile's
observed sharing to cover the head's per-row label, position and row-match
buffers. Tests cover the excluded mixes and the wide-label key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fourth review round: saved head indices and masks stay live until backward,
so retained memory also charges logical rows beyond the profile's sharing.
Packed pricing now requires a per-packed rate of at least the per-row charge
times the profile ratio (and the retained row charge requires the same of the
retained rate), keeping more-shared layouts no more expensive so lower-bound
pruning stays sound. All-grad signatures with several slot groups qualify.
The scope test pins exact values for packed and extrapolated signatures,
ratios above one, mixed grad modes and both guards.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fifth review round and a high-branch H200 measurement. Retained memory now
falls back to the logical/packed extrapolation below the rate floor instead
of dropping head rows. Costs are truncated once. Packed pricing requires
per-packed rates of at least twice the per-row charge times the profile
ratio, so fewer packed rows always cost less despite rounding. GDN branch
states grow with segments, not packed rows: 101 twenty-token branches on a
5,100-token prompt (ratio 72.6, 8 layers) raised the warm peak per packed
token from 323 KB to 378 KB, about 3.9 MB per extra segment. Packed pricing
now charges one layer's recurrent state and convolution history per grad
segment under full recompute (every GDN layer otherwise). The split lower
bound passes no segments, so pruning stays sound.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sixth review round. The per-segment GDN charge assumes backward recomputes
one layer at a time, which holds only for ART's default full/uniform/1
recompute; block or multi-layer full recompute, selective recompute, and no
recompute keep more GDN states and activations live, including across split
subforwards. The measurements and production reports cover only that
default, so packed pricing now requires it and every other mode keeps the
logical/packed extrapolation. The flag is recorded in planner reports and
restored on replay. Tests pin the per-layer segment bytes by hand and the
fallback for other recompute modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Seventh review round. Megatron requires an explicit recompute method and
layer count under full recompute and skips recompute in eval mode, so packed
pricing now checks the exact full/uniform/1 settings and that every model
chunk is training, each time it prices. Planner reports record the computed
value and replay restores it. Test stubs declare ART's explicit settings, and
a detector test covers selective, none, block, two-layer uniform, unset and
eval mode against the extrapolated pricing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Eighth review round. Megatron gates recompute on the decoder's training mode
and config, which can diverge from the outer chunk, so the one-layer check
now requires every submodule to be training and reads recompute settings
from the live decoder config (stored fields only for stub models). Replay
rejects a recorded flag that is not a bool. Tests cover a decoder in eval
under a training chunk and show a recorded flag switches single-target
pricing between packed and extrapolated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each request's head buffers are separate allocations rounded up to
512 B blocks, so duplicate one-token requests could exceed the 128 B
row charge. Charge eight blocks per logical row beyond the profile's
sharing; the rate floor scales with it, so lower bounds stay sound.

Recompute detection now reads the decoder's training flag, which is
what Megatron checks, instead of walking every submodule. Tests cover
the live decoder config and replay of an unrecorded recompute mode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On an H200, a duplicate one-token request peaked at nine 512 B blocks
(4,616 B) at the end of head forward, above 4 KiB x 1.1. Charge twelve
blocks per row, and add a CUDA test that runs the real head and backward
for 20,001 duplicates and checks the per-request peak against the charge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Packed pricing now assumes an explicit, agreed invariant: model memory
scales with packed rows, and head plus caller loss memory peaks at no more
than 6 KiB per logical token. The estimate is B*max(P, L/(8r)) + 6 KiB*L
plus GDN segment states, so the logical charge no longer vanishes at the
profile's own density, and sharing beyond 8x the observed ratio is priced
as if it were 8x. Single-target requests under 64 tokens keep the logical
extrapolation. The rate floor is gone: it could switch the charge off where
sharing is highest.

CUDA tests run the real head with 061's CISPO loss and a fixed
per-request caller, and check a 4,096 -> 64 token calibration transition
against the estimator.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bradhilton
bradhilton force-pushed the dalinar/planner-sharing-ratio branch from 95e7ed3 to 058547e Compare September 24, 2026 22:22
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 22:22 — with GitHub Actions Active
The short-request gate was a request-mix key, which gave any batch with
a short request its own profile signature: it started cold on the static
estimate instead of main's calibrated extrapolation. Carry the gate as a
signature flag excluded from identity, so those batches share the profile
and price exactly as on main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bradhilton
bradhilton deployed to trainer-rank-gpu-validation September 24, 2026 22:44 — with GitHub Actions Active
@bradhilton
bradhilton merged commit 70d78b1 into main Sep 24, 2026
7 checks passed

This branch was successfully deployed

1 active deployment
trainer-rank-gpu-validation — ccca0dc7 Deployed Sep 24, 2026 by bradhilton via Run on 2x H200 #654
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