Repository navigation
Phase A1: activation-memory model (f(seq,mbs,CP,recompute) replacing flat 82 GiB) - #162
Conversation
scaleout.py hard-codes NON_PARAM_HEADROOM_GIB = 82.0 for every 93 L candidate. That 82 GiB is a single G28 point: 4 L on one node, no recompute, K=1. It is blind to seq, micro-batch, CP, recompute policy, and layers-per-stage, all of which move the real activation set. This adds a component-wise model of the per-stage activation peak as f(seq, mbs, CP, recompute, layers_on_stage, mla_on_stage, K), with every term tagged MEASURED (AttnRes payload/mixer from G6) or ESTIMATED (EP dispatch buffer, MLA transient, frag/NCCL residual), and a driver that reproduces the G46 param/state numbers (19.25 B params/gpu, 141.1 GiB state) to the decimal. Findings: - Under production recompute=full the 93 L per-stage peak is ~59-65 GiB, BELOW the flat 82 -- so the flat constant is conservative for the recompute regime, and the 28-node floor is not threatened by activation. The binding stage is the LAST one (LM-head logits), not the mid-pipeline payload stage. - But mbs=2 @ seq8192 or seq=16384 pushes a single stage over 82 GiB, where the flat constant instead UNDER-provisions. - The back-solved 37.5 GiB residual (frag+NCCL) is now the load-bearing unknown; one two-seq trainer mem report is enough to split constant from seq-scaling. Not yet reconciled into mem_budget.py/scaleout.py; standalone laptop copy under ~/workspace/kimi_k3/ matches this to the decimal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove all measured-data inputs per review; measurements become independent cross-checks only: - optimizer state bytes now use the analytic dist_muon 2+4+8/dp formula (mem_budget.OPTIMIZER_BYTES_PER_PARAM) instead of the measured 7.87 bytes/param - AttnRes mixer derived from the (K+1)-slot fp32 upcast shape instead of being anchored to G6's 7.1/12.2 GB; the residual gap to G6 is reported, not tuned away - MLA transient defaults to flash/CK/AITER style (no [B,nh,S,S] score in HBM, linear in S); the MuonClip QK-clip rescore is a default-off optional term - drop calibrate_residual; fragmentation is now an explicit fraction of the itemized live set, and the 4 L / 82 GiB comparison is a printed cross-check - rename StageSpec -> PipelineStageInputs (per-stage input bundle, not whole model) crosscheck_* helpers surface the analytic-vs-measured gaps (state +0.87 bytes, mixer ~2.75x, 4 L headroom +43 GiB) as findings to chase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
[Agent reply] Pushed v2 (b077e32) implementing all 7 review threads -- the model is now fully bottom-up, no measured value is a model INPUT. Measurements moved to
The +43 GiB 4 L gap is the honest headline: the bottom-up itemization only explains ~39 of the measured 82 GiB, so there is real memory we have not modeled yet. That is now visible instead of hidden inside a back-solved residual. For the StageSpec rename I went with PipelineStageInputs; say the word if you prefer StageGeometry. |
Add mem_probe.py (allocator-history capture around one warm step, with
--recompute/--mbs, no torch.profiler active) and mem_snapshot_attrib.py
(buckets the snapshot's segments by python alloc frames; regresses each
bucket vs S*B). Sweeping (seq, mbs, recompute) separates the retired G28
"82 GiB headroom" into its parts and shows nothing is unmodelled.
Findings:
- The +43 GiB the itemized model "could not explain" was two confounds
stacked on a single-point anchor, not unmodelled memory:
(1) 44.79 GiB of LAZY Muon optimizer state that peak-after_optimizer
mislabeled as activation (true resident 181.2 GiB, not the 135.9
after_optimizer reports before the first step);
(2) a backend artifact: the proxy ran eager KDA, whose chunked
linear-attention state dominates the transient (~50 GiB at seq4096).
- Direct fla (fused production path) measurement in yewang_kimi_k3: the
transient collapses to a seq-INVARIANT ~9.7 GiB, matching the analytic
fused-path prediction (9.1 GiB) to 0.6 GiB. The +43 GiB residual closes.
- Root cause of the ~9.7 GiB fla transient (peak-live device_traces decode):
it is NOT activation. It is the distributed-optimizer parameter all-gather
(layer_wise_optimizer.allgather_params: local-shard flatten 1.73 GiB +
per-rank recv buffers 7.98 GiB of bf16). Param-sized and token-independent;
once fla makes fwd/bwd activation cheap the per-step peak is
optimizer-step-bound, not activation-bound.
Model corrections: crosscheck_headroom now compares analytic activation
against the measured transient (peak - resident) in the correct rc=full
regime, anchored on the fla production transient; the eager number is
available only to expose the backend artifact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
[Agent reply] Empirical 4L memory attribution -- the +43 GiB "unexplained headroom" is fully accounted for (nothing unmodelled)Method (Ye's): capture the real PyTorch allocator trace of the 4L proxy, sweep The +43 GiB was two confounds on a single-point anchor, not unmodelled memory(1) 44.79 GiB of LAZY Muon state. The G28 "82 GiB headroom" was defined as (2) An eager-KDA backend artifact. The proxy ran Direct fla measurement closes the gapRe-ran the identical sweep with The +43 GiB residual closes to ~0. The analytic activation model was correct for the production path; the eager proxy was the confound. Root cause of the ~9.7 GiB fla transient: it is NOT activationReplaying At the peak essentially no fwd/bwd activation is live ( Consequences for the model
|
| MIXER_ACCUM_FP32_TEMPS = 2 # running prefix accumulator + fp32 output (fwd) | ||
|
|
||
|
|
||
| def attn_res_mixer_bytes(s_local, mbs, k, with_backward, |
There was a problem hiding this comment.
What is your payload vs mixer? Can you change to specific input/output or internal state naming
There was a problem hiding this comment.
[Agent reply] Done in 673f2ca. Renamed to input/output vs internal-state: attn_res_payload_bytes -> attn_res_io_bytes (the block's bf16 INPUT/OUTPUT, the (1+K) packed residual-stream slots that cross a boundary) and attn_res_mixer_bytes -> attn_res_internal_bytes (the live fp32 temporaries INTERNAL to the mix op). Constant MIXER_ACCUM_FP32_TEMPS -> ATTN_RES_INTERNAL_FP32_TEMPS, crosscheck_mixer -> crosscheck_attn_res_internal, and the part keys are now attn_res_io / attn_res_internal.
| would cost. | ||
| """ | ||
| nh = DIMS.num_attn_heads | ||
| flash_ws = mbs * nh * s_local * (DIMS.qk_head_dim + DIMS.v_head_dim) * BF16 |
There was a problem hiding this comment.
Is this for q, k, v tensor in fwd? If so, it should be activation. But you name it workspace? What workspace is it for?
There was a problem hiding this comment.
[Agent reply] Correct -- it is the per-head Q/K/V/O attention activation the flash/CK/AITER kernel reads and writes, so it IS activation, not a workspace. That was a bad name. Renamed flash_ws -> qkvo_activation in 673f2ca and fixed the docstring to say 'This is activation, not a scratch workspace.' The full [B,nh,S,S] score is never materialized (flash-style), so it is linear in S.
| # Fragmentation / allocator slack as an EXPLICIT fraction of the itemized live | ||
| # set. This is an ESTIMATED modeling assumption, not a back-solved absolute; the | ||
| # crosscheck_headroom against G28 tells us whether the fraction is right. | ||
| FRAG_FRACTION = 0.07 # ESTIMATED: caching-allocator slack, ~7% of live bytes |
There was a problem hiding this comment.
Let's get rid of this estimated thing. For all things we cannot explain, we should report a gap for future investigation, instead of inventing some factors to make us feel like we understand but we actually not
There was a problem hiding this comment.
[Agent reply] Agreed. Removed the FRAG_FRACTION=0.07 factor entirely in 673f2ca -- stage_activation_gib no longer takes frag_fraction and TOTAL is now the plain sum of the itemized components. Anything we cannot derive from shapes is surfaced as an explicit GAP by the crosscheck helpers, never absorbed by an invented factor. (See the follow-up note below on the known unmodeled MLA buffers -- softmax LSE, dq_acc -- which we deliberately leave as a measured gap rather than guess.)
… factor, move probes
Address Ye's four inline review comments on the activation-memory model:
* AttnRes block terms renamed to input/output vs internal-state naming:
attn_res_payload_bytes -> attn_res_io_bytes (the block's bf16 INPUT/OUTPUT,
the (1+K) packed residual-stream slots crossing a boundary) and
attn_res_mixer_bytes -> attn_res_internal_bytes (the live fp32 temporaries
INTERNAL to the mix op). Constant MIXER_ACCUM_FP32_TEMPS ->
ATTN_RES_INTERNAL_FP32_TEMPS; crosscheck_mixer -> crosscheck_attn_res_internal;
part keys attn_res_io / attn_res_internal.
* MLA transient's flash_ws renamed to qkvo_activation: it is the per-head
Q/K/V/O attention activation the flash/CK/AITER kernel reads and writes, not
a scratch workspace. Docstring corrected to say so.
* Removed the FRAG_FRACTION=0.07 allocator-slack factor entirely. TOTAL is now
the plain sum of itemized components; anything we cannot derive from shapes is
surfaced as an explicit GAP by the crosscheck helpers, never absorbed by an
invented factor.
* Moved mem_probe.py and mem_snapshot_attrib.py from tools/ to tests/ (they are
measurement/validation tools, not part of the analytic model); internal
module-path strings updated to kimi_k3.tests.mem_*.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The crosscheck note mislabeled the AttnRes fp32 temporaries as living in a "CK AttnRes block". AttnRes does NOT call Composable Kernel: block/attn_res.py is a pure-torch fp32 eager oracle (transcribed from HF KimiLinearModel._apply_attn_res) and block/attn_res_triton.py is a fused Triton fast path. CK/AITER is the MLA attention path, not this mix. Also give the x2.75 fwd gap (analytic 2.58 GB vs G6 7.1 GB) a named source instead of a vague "source read": the eager mix materialises the [T, K+1, H] stack in fp32 THREE times (the cat, the fp32 upcast, the RMS-normalised copy), not twice, plus the score/output temporaries -- so accum_temps=2 undercounts. Left as an explicit gap (not tuned away) until the Triton fused path, which avoids the 3x materialisation, is the measured baseline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Phase A1 of the projected-performance estimator (the throughput/memory layer on top of Wen's parameter oracle). Replaces
scaleout.py's flatNON_PARAM_HEADROOM_GIB = 82.0with a component-wise activation model asf(seq, mbs, CP, recompute, layers_on_stage, mla_on_stage, K).Two files, both under
kimi_k3/tools/:activation_mem_model.py-- pure-Python model, no torch. Each activation term is one function with its formula + scaling + a MEASURED/ESTIMATED tag.activation_mem_report.py-- driver importing the repo's ownmem_budget.params_per_gpu+scaleout.aligned_layout(the reconciliation point). Run:python -m kimi_k3.tools.activation_mem_report.Why the flat 82 GiB is wrong
It is a single G28 point: 4 L on one node, no recompute, K=1. It is reused flat for every 93 L candidate, blind to seq, mbs, CP, recompute, and layers-per-stage.
Validation (why the numbers are trustworthy)
params/gpu = 19.25 B,state = 141.1 GiB(dist_muon@dp8).(1+1) x 8192 x 7168 x 2 = 224.0 MiB.Findings
recompute=full, the 93 L per-stage peak is ~59-65 GiB, below the flat 82 -- so the flat constant is conservative for the recompute regime and the 28-node floor (G46) is not threatened by activation. The binding stage is the last one (LM-head logits), not the mid-pipeline payload stage.mbs=2 @ seq8192orseq=16384pushes a single stage over 82 GiB -- there the flat constant instead under-provisions.[B,nh,S,S]score is query-chunked, so it is linear in S in memory (quadratic stays in time only). MoE dispatch is the larger transient at production width.Not in scope / next
mem_budget.py/scaleout.py-- kept standalone for review first.Base branch
dev/yewang12/kimi-k3-proj_perf_estimatoris Wen's latesta2c2ad1with no extra commits, so this diff is exactly the two new files.Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com