docs(gemma4-erp-tune): root-cause the 8.6% MFU — attention on Ampere kernels, 29.9% padding
Run-01 was killed at step 19 by operator instruction to root-cause before spending a ~13.9h window. Two independent methods now agree on where the step time went, and neither was the hypothesis the consult panel converged on. Scaling fit (3 points, 2 params, residuals <3ms over an 8x range): A = 6.87e-4 s/token, B = 8.85e-8 s/token^2 quadratic share 20.9% @ w=2048 -> 67.8% @ w=16384 No fixed term was needed, which refutes launch-bound outright. Profiler kernel table (device rows only): attention 22,835.8 ms 65.2% fmha_cutlass*_sm80 dense GEMM 2,774.0 ms 7.9% other 5,739.0 ms 16.4% The attention kernels are sm80 — Ampere-generation CUTLASS running on an sm_120 Blackwell card, with the forward on the gmem fallback tier. That is the mechanism behind 100% SM utilisation at 27 of 304 available TFLOPS. Correctness cleared separately: the sliding mask asserts at max 1024 allowed/row, so the 25 windowed layers were genuinely windowed. The same probe found that right-padding is what pins the 5 global layers to an explicit 4D mask and off the is_causal fast path — measured at 9.4% slower for 24% less loss work at fixed width. The largest available win is not the attention kernel. The corpus is 29.9% padding, and bucket-to-pair + shuffle-to-mix takes it to 0.0% for >=35.5% wall clock, no new dependency, unchanged peak memory. Bucket size turned out not to be a diversity knob — roots per accumulation window are flat across a 256x range, so the global micro-batch shuffle does that work alone and the bucket should be tight. Adds docs/pfi/training-throughput-playbook.md as the durable model-agnostic home (sibling to the quantization playbook), the four probes under scripts/training-probes/ with raw output kept for re-derivation, and a §6 to the sizing doc carrying the Gemma-4-specific numbers and round-2 restart parameters. Measured negatives recorded so they are not re-chased: grouped_mm (0.9% slower, and MoE is only 7.9% of the step), CUDA graphs / torch.compile over the expert loop (no fixed cost to amortise), liger fused CE (~1-3% lever), FA4 on sm_120. Round-1 state preserved: 609MB encode cache, order manifest, truncation report, resume script. No checkpoints — it died at step 19 and the first was due at 100, so the lora_B inert-adapter gate never ran and moves to the restart.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# Training throughput probes
|
||||
|
||||
Instruments for finding where a training step's time actually went. Written
|
||||
2026-08-24 during the Gemma-4 26B-A4B ERP/RP tune investigation; the lessons
|
||||
they produced live in
|
||||
[`docs/pfi/training-throughput-playbook.md`](../../docs/pfi/training-throughput-playbook.md).
|
||||
|
||||
**These are diagnostic instruments, not production code.** They hard-code paths
|
||||
for that run. Adapt the constants at the top; keep the measurement design.
|
||||
|
||||
## The probes
|
||||
|
||||
| script | settles | GPU | runtime |
|
||||
|---|---|---|---|
|
||||
| `step0_mask.py` | mask band structure; which layers keep the `is_causal` fast path | no | ~30 s |
|
||||
| `step2_padding.py` | padding waste, length distribution, CE chunk sizing | no | ~2 min |
|
||||
| `step_bucket.py` | bucketing gain, bucket-size sweep, source diversity | no | ~3 min |
|
||||
| `step1_profile.py` | scaling fit, padding penalty, CE wall clock, kernel table | **yes** | ~15 min |
|
||||
|
||||
Run in that order. Only the last needs the real checkpoint, and it wants an
|
||||
idle card — it loads ~48 GiB and peaks near 77 GiB at `2 × 16,384`.
|
||||
|
||||
## Design rules worth preserving when you adapt these
|
||||
|
||||
**`step1_profile.py` reuses the harness's own `discover_target_modules` and
|
||||
replicates its `compute_loss` byte-for-byte** rather than re-implementing the
|
||||
step. A probe that reimplements the training step measures the probe. If you
|
||||
port this, keep the import from the real harness.
|
||||
|
||||
**`step0_mask.py` needs no weights and no GPU** — SDPA backend selection and
|
||||
mask construction depend on shapes, dtype and mask presence, not on weight
|
||||
values. That is what makes the correctness assertion cheap enough to run before
|
||||
every job.
|
||||
|
||||
**The scaling test takes three points, not two.** Two points over three
|
||||
plausible terms (quadratic, linear, fixed-per-batch) is underdetermined; see
|
||||
playbook §1.1 for the hour that cost.
|
||||
|
||||
**`step_bucket.py` sweeps bucket size deliberately.** The first version
|
||||
re-sorted within each bucket, which silently collapsed every bucket size to a
|
||||
full global sort and made the sweep a no-op. If you change the pairing logic,
|
||||
check that the sweep still varies something.
|
||||
|
||||
## Raw evidence
|
||||
|
||||
`step1-profile-output-2026-08-24.txt` is the unedited output of the run the
|
||||
playbook's numbers come from — scaling points, padding penalty, CE timing, and
|
||||
the full `key_averages()` kernel table. Kept so the claims can be re-derived
|
||||
rather than taken on faith.
|
||||
|
||||
⚠ That table **double-counts**: `key_averages()` lists both the ATen op and the
|
||||
CUDA kernel it launched, each carrying the same self device time. Sum device
|
||||
kernel rows only. See playbook §3.4.
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Step 0 - assert the sliding mask band structure, and record which path
|
||||
mask creation actually takes under the run config (attn_implementation=sdpa).
|
||||
|
||||
Correctness gate: transformers can SILENTLY skip mask creation and pass
|
||||
attention_mask=None, which would make the 25 sliding layers do full causal
|
||||
attention - a different model from the one vLLM serves. This converts
|
||||
"probably fine because we are slow" into a measurement.
|
||||
|
||||
CPU only. No weights. No GPU.
|
||||
"""
|
||||
import torch
|
||||
from transformers import AutoConfig
|
||||
from transformers.masking_utils import (
|
||||
create_causal_mask, create_sliding_window_causal_mask,
|
||||
)
|
||||
|
||||
MODEL = "/tank/aimodels/gemma4-26b-a4b-it-heretic-bf16"
|
||||
N = 16384
|
||||
W = 1024
|
||||
PAD = " " + " " * 20
|
||||
|
||||
cfg = AutoConfig.from_pretrained(MODEL)
|
||||
text = cfg.get_text_config()
|
||||
text._attn_implementation = "sdpa"
|
||||
print("sliding_window %s" % text.sliding_window)
|
||||
print("layers %d (%d sliding / %d full)" % (
|
||||
len(text.layer_types),
|
||||
text.layer_types.count("sliding_attention"),
|
||||
text.layer_types.count("full_attention")))
|
||||
print("_attn_implementation %s" % text._attn_implementation)
|
||||
print()
|
||||
|
||||
|
||||
def build(attn_2d, label):
|
||||
batch = attn_2d.shape[0] if attn_2d is not None else 1
|
||||
embeds = torch.zeros(batch, N, 8, dtype=torch.bfloat16)
|
||||
pos = torch.arange(N).unsqueeze(0)
|
||||
kw = dict(config=text, inputs_embeds=embeds, attention_mask=attn_2d,
|
||||
past_key_values=None, position_ids=pos)
|
||||
full = create_causal_mask(**kw)
|
||||
slide = create_sliding_window_causal_mask(**kw)
|
||||
print("--- %s ---" % label)
|
||||
for name, m in (("full_attention", full), ("sliding_attention", slide)):
|
||||
if m is None:
|
||||
print(" %-20s None -> flash / is_causal path AVAILABLE" % name)
|
||||
continue
|
||||
print(" %-20s tensor shape=%s dtype=%s" % (name, tuple(m.shape), m.dtype))
|
||||
allowed = m if m.dtype == torch.bool else (m == 0)
|
||||
per_row = allowed[0, 0].sum(-1)
|
||||
print("%sallowed/row min=%d max=%d mean=%.1f" % (
|
||||
PAD, per_row.min().item(), per_row.max().item(),
|
||||
per_row.float().mean().item()))
|
||||
if name == "sliding_attention":
|
||||
ok = per_row.max().item() <= W
|
||||
print("%sBAND <= %d ? %s" % (PAD, W, "PASS" if ok else "FAIL"))
|
||||
sat = (per_row >= W).nonzero()
|
||||
if sat.numel():
|
||||
print("%ssaturates at row %d" % (PAD, sat[0].item()))
|
||||
else:
|
||||
print("%slast row allows %d of %d (%s)" % (
|
||||
PAD, per_row[-1].item(), N,
|
||||
"causal-full OK" if per_row[-1].item() == N else "UNEXPECTED"))
|
||||
print()
|
||||
|
||||
|
||||
# 1. no 2D mask at all - the "constraints silently dropped" scenario
|
||||
build(None, "attention_mask=None (no padding info)")
|
||||
|
||||
# 2. all-ones 2D mask - equal-length batch, no padding
|
||||
build(torch.ones(2, N, dtype=torch.long), "all-ones 2D (no padding)")
|
||||
|
||||
# 3. REAL right-padded batch - what collate_mixed actually produces
|
||||
real = torch.ones(2, N, dtype=torch.long)
|
||||
real[1, 6000:] = 0
|
||||
build(real, "right-padded 2D (what collate_mixed emits)")
|
||||
@@ -0,0 +1,126 @@
|
||||
========================================================================
|
||||
loading model
|
||||
========================================================================
|
||||
|
||||
Loading weights: 0%| | 0/1013 [00:00<?, ?it/s]
|
||||
Loading weights: 0%| | 2/1013 [00:00<01:19, 12.71it/s]
|
||||
Loading weights: 0%| | 4/1013 [00:00<01:10, 14.40it/s]
|
||||
Loading weights: 2%|▏ | 25/1013 [00:00<00:16, 61.02it/s]
|
||||
Loading weights: 5%|▍ | 48/1013 [00:00<00:12, 79.42it/s]
|
||||
Loading weights: 7%|▋ | 70/1013 [00:00<00:10, 87.32it/s]
|
||||
Loading weights: 9%|▉ | 92/1013 [00:01<00:10, 91.14it/s]
|
||||
Loading weights: 11%|█ | 113/1013 [00:01<00:09, 92.58it/s]
|
||||
Loading weights: 13%|█▎ | 135/1013 [00:01<00:09, 95.13it/s]
|
||||
Loading weights: 15%|█▌ | 157/1013 [00:01<00:08, 97.82it/s]
|
||||
Loading weights: 18%|█▊ | 179/1013 [00:02<00:08, 101.21it/s]
|
||||
Loading weights: 20%|█▉ | 201/1013 [00:02<00:08, 100.37it/s]
|
||||
Loading weights: 22%|██▏ | 223/1013 [00:02<00:07, 102.12it/s]
|
||||
Loading weights: 24%|██▍ | 244/1013 [00:02<00:07, 104.64it/s]
|
||||
Loading weights: 26%|██▋ | 266/1013 [00:02<00:07, 105.68it/s]
|
||||
Loading weights: 28%|██▊ | 288/1013 [00:03<00:06, 106.87it/s]
|
||||
Loading weights: 30%|███ | 308/1013 [00:03<00:05, 122.26it/s]
|
||||
Loading weights: 32%|███▏ | 322/1013 [00:03<00:05, 117.76it/s]
|
||||
Loading weights: 33%|███▎ | 335/1013 [00:03<00:06, 99.26it/s]
|
||||
Loading weights: 35%|███▍ | 354/1013 [00:03<00:06, 100.53it/s]
|
||||
Loading weights: 37%|███▋ | 375/1013 [00:03<00:06, 104.62it/s]
|
||||
Loading weights: 39%|███▉ | 397/1013 [00:04<00:05, 103.34it/s]
|
||||
Loading weights: 41%|████▏ | 419/1013 [00:04<00:05, 106.05it/s]
|
||||
Loading weights: 44%|████▎ | 441/1013 [00:04<00:05, 109.33it/s]
|
||||
Loading weights: 46%|████▌ | 463/1013 [00:04<00:05, 107.04it/s]
|
||||
Loading weights: 48%|████▊ | 485/1013 [00:04<00:04, 107.29it/s]
|
||||
Loading weights: 50%|█████ | 507/1013 [00:05<00:04, 105.14it/s]
|
||||
Loading weights: 52%|█████▏ | 528/1013 [00:05<00:04, 103.18it/s]
|
||||
Loading weights: 54%|█████▍ | 550/1013 [00:05<00:04, 102.05it/s]
|
||||
Loading weights: 56%|█████▋ | 572/1013 [00:05<00:04, 101.82it/s]
|
||||
Loading weights: 59%|█████▊ | 594/1013 [00:05<00:03, 106.80it/s]
|
||||
Loading weights: 61%|██████ | 616/1013 [00:06<00:03, 104.33it/s]
|
||||
Loading weights: 63%|██████▎ | 638/1013 [00:06<00:03, 103.57it/s]
|
||||
Loading weights: 77%|███████▋ | 778/1013 [00:06<00:00, 320.91it/s]
|
||||
Loading weights: 91%|█████████ | 920/1013 [00:06<00:00, 532.70it/s]
|
||||
Loading weights: 100%|██████████| 1013/1013 [00:06<00:00, 152.35it/s]
|
||||
loaded in 9.8s targets=205
|
||||
final_logit_softcapping = 30.0
|
||||
attn_implementation = sdpa
|
||||
|
||||
========================================================================
|
||||
A. SEQUENCE SCALING (no padding - isolates n)
|
||||
========================================================================
|
||||
2 x 2,048 1.776 s kept=3227 peak= 53.2 GiB
|
||||
2 x 8,192 11.570 s kept=13050 peak= 62.3 GiB
|
||||
2 x 16,384 35.017 s kept=25989 peak= 76.6 GiB
|
||||
|
||||
16384 -> 2048 ratio 19.71x (linear ~8x, launch-bound ~1x, quadratic ~64x)
|
||||
16384 -> 8192 ratio 3.03x (linear ~2x, quadratic ~4x)
|
||||
|
||||
========================================================================
|
||||
B. PADDING PENALTY (same real tokens, with vs without pad)
|
||||
========================================================================
|
||||
2 x 16,384 no padding 35.244 s kept=26048 peak= 76.6 GiB
|
||||
2 x 16,384 50% pad on row 1 38.567 s kept=19640 peak= 77.8 GiB
|
||||
|
||||
========================================================================
|
||||
C. ISOLATED CE WALL CLOCK
|
||||
========================================================================
|
||||
2 x 16,384 (CE timed) 35.329 s kept=26210 peak= 76.6 GiB CE=374 ms (1.1%)
|
||||
2 x 4,096 (CE timed) 4.387 s kept=6512 peak= 55.3 GiB CE=93 ms (2.1%)
|
||||
|
||||
========================================================================
|
||||
D. KERNEL TABLE - one fwd+bwd at 2 x 16,384
|
||||
========================================================================
|
||||
USDT:2026-08-24 22:03:51 574811:574811 SyncActivityProfilerHandler.cpp:52] profiler_start
|
||||
USDT:2026-08-24 22:04:27 574811:574811 SyncActivityProfilerHandler.cpp:59] profiler_stop
|
||||
------------------------------------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
|
||||
Name Self CPU % Self CPU CPU total % CPU total CPU time avg Self CUDA Self CUDA % CUDA total CUDA time avg # of Calls
|
||||
------------------------------------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
|
||||
aten::_efficient_attention_backward 0.00% 573.671us 0.00% 1.681ms 56.028us 16.145s 45.71% 16.156s 538.546ms 30
|
||||
fmha_cutlassB_bf16_aligned_128x64_k65536_sm80(PyTorc... 0.00% 0.000us 0.00% 0.000us 0.000us 16.145s 45.71% 16.145s 538.152ms 30
|
||||
aten::_efficient_attention_forward 0.00% 795.660us 0.01% 1.933ms 32.218us 6.691s 18.94% 6.691s 111.519ms 60
|
||||
fmha_cutlassF_bf16_aligned_32x128_gmem_sm80(PyTorchM... 0.00% 0.000us 0.00% 0.000us 0.000us 6.691s 18.94% 6.691s 111.519ms 60
|
||||
aten::mm 0.51% 180.757ms 0.77% 270.268ms 10.614us 3.745s 10.60% 3.745s 147.063us 25463
|
||||
aten::mul 0.13% 45.428ms 0.18% 64.624ms 12.129us 2.721s 7.70% 2.721s 510.606us 5328
|
||||
aten::copy_ 0.06% 19.823ms 92.46% 32.574s 6.100ms 1.977s 5.60% 1.977s 370.189us 5340
|
||||
void cutlass::Kernel2<cutlass_80_tensorop_bf16_s1681... 0.00% 0.000us 0.00% 0.000us 0.000us 1.542s 4.37% 1.542s 656.804us 2348
|
||||
void at::native::elementwise_kernel<128, 2, at::nati... 0.00% 0.000us 0.00% 0.000us 0.000us 1.033s 2.92% 1.033s 545.219us 1894
|
||||
void cutlass::Kernel2<cutlass_80_tensorop_bf16_s1681... 0.00% 0.000us 0.00% 0.000us 0.000us 868.642ms 2.46% 868.642ms 583.373us 1489
|
||||
void at::native::vectorized_elementwise_kernel<4, at... 0.00% 0.000us 0.00% 0.000us 0.000us 787.028ms 2.23% 787.028ms 395.095us 1992
|
||||
void at::native::unrolled_elementwise_kernel<at::nat... 0.00% 0.000us 0.00% 0.000us 0.000us 697.963ms 1.98% 697.963ms 304.521us 2292
|
||||
aten::masked_fill_ 0.01% 3.403ms 0.01% 4.927ms 27.373us 576.927ms 1.63% 576.927ms 3.205ms 180
|
||||
void at::native::vectorized_elementwise_kernel<4, at... 0.00% 0.000us 0.00% 0.000us 0.000us 561.815ms 1.59% 561.815ms 413.403us 1359
|
||||
void at::native::vectorized_elementwise_kernel<4, at... 0.00% 0.000us 0.00% 0.000us 0.000us 469.818ms 1.33% 469.818ms 459.255us 1023
|
||||
aten::add_ 0.01% 3.730ms 0.02% 6.640ms 7.209us 448.665ms 1.27% 448.665ms 487.150us 921
|
||||
void cutlass::Kernel2<cutlass_80_tensorop_bf16_s1681... 0.00% 0.000us 0.00% 0.000us 0.000us 363.206ms 1.03% 363.206ms 394.789us 920
|
||||
aten::add 0.03% 9.745ms 0.04% 14.287ms 10.205us 356.226ms 1.01% 356.226ms 254.447us 1400
|
||||
void at::native::elementwise_kernel<128, 2, at::nati... 0.00% 0.000us 0.00% 0.000us 0.000us 352.705ms 1.00% 352.705ms 1.959ms 180
|
||||
aten::index 0.01% 4.542ms 0.09% 32.242ms 132.682us 352.404ms 1.00% 352.430ms 1.450ms 243
|
||||
void at::native::vectorized_gather_kernel<16, long>(... 0.00% 0.000us 0.00% 0.000us 0.000us 351.768ms 1.00% 351.768ms 1.933ms 182
|
||||
Memcpy DtoD (Device -> Device) 0.00% 0.000us 0.00% 0.000us 0.000us 341.588ms 0.97% 341.588ms 634.922us 538
|
||||
aten::pow 0.06% 21.418ms 0.11% 37.019ms 18.659us 332.072ms 0.94% 493.271ms 248.624us 1984
|
||||
void at::native::vectorized_elementwise_kernel<4, at... 0.00% 0.000us 0.00% 0.000us 0.000us 330.315ms 0.94% 330.315ms 499.720us 661
|
||||
void at::native::vectorized_elementwise_kernel<4, at... 0.00% 0.000us 0.00% 0.000us 0.000us 316.108ms 0.89% 316.108ms 383.161us 825
|
||||
aten::sum 0.01% 5.258ms 0.02% 7.700ms 13.461us 286.461ms 0.81% 286.464ms 500.811us 572
|
||||
aten::native_dropout 0.02% 6.453ms 0.03% 11.139ms 27.168us 258.244ms 0.73% 258.244ms 629.865us 410
|
||||
void at::native::(anonymous namespace)::fused_dropou... 0.00% 0.000us 0.00% 0.000us 0.000us 258.244ms 0.73% 258.244ms 629.865us 410
|
||||
void at::native::unrolled_elementwise_kernel<at::nat... 0.00% 0.000us 0.00% 0.000us 0.000us 239.055ms 0.68% 239.055ms 583.061us 410
|
||||
aten::_index_put_impl_ 0.01% 4.170ms 3.22% 1.134s 7.508ms 235.816ms 0.67% 236.930ms 1.569ms 151
|
||||
void at::native::elementwise_kernel<128, 4, at::nati... 0.00% 0.000us 0.00% 0.000us 0.000us 231.434ms 0.66% 231.434ms 385.081us 601
|
||||
void at::native::vectorized_elementwise_kernel<4, at... 0.00% 0.000us 0.00% 0.000us 0.000us 226.451ms 0.64% 226.451ms 692.511us 327
|
||||
void at::native::elementwise_kernel<128, 4, at::nati... 0.00% 0.000us 0.00% 0.000us 0.000us 224.222ms 0.63% 224.222ms 2.491ms 90
|
||||
void cutlass::Kernel2<cutlass_80_simt_sgemm_64x128_8... 0.00% 0.000us 0.00% 0.000us 0.000us 219.030ms 0.62% 219.030ms 534.219us 410
|
||||
void at::native::elementwise_kernel<128, 4, at::nati... 0.00% 0.000us 0.00% 0.000us 0.000us 209.409ms 0.59% 209.409ms 1.745ms 120
|
||||
aten::div 0.01% 4.911ms 0.02% 6.652ms 13.278us 192.925ms 0.55% 192.925ms 385.080us 501
|
||||
void (anonymous namespace)::indexing_backward_kernel... 0.00% 0.000us 0.00% 0.000us 0.000us 183.000ms 0.52% 183.000ms 6.100ms 30
|
||||
void at::native::unrolled_elementwise_kernel<at::nat... 0.00% 0.000us 0.00% 0.000us 0.000us 148.335ms 0.42% 148.335ms 988.899us 150
|
||||
aten::mean 0.02% 6.214ms 0.02% 8.406ms 12.717us 144.904ms 0.41% 144.904ms 219.220us 661
|
||||
void at::native::reduce_kernel<512, 1, at::native::R... 0.00% 0.000us 0.00% 0.000us 0.000us 144.904ms 0.41% 144.904ms 219.220us 661
|
||||
aten::_log_softmax 0.00% 527.616us 0.00% 716.309us 13.775us 139.294ms 0.39% 139.294ms 2.679ms 52
|
||||
void at::native::(anonymous namespace)::cunn_SoftMax... 0.00% 0.000us 0.00% 0.000us 0.000us 139.294ms 0.39% 139.294ms 2.679ms 52
|
||||
void at::native::reduce_kernel<512, 1, at::native::R... 0.00% 0.000us 0.00% 0.000us 0.000us 137.743ms 0.39% 137.743ms 286.368us 481
|
||||
void at::native::reduce_kernel<128, 4, at::native::R... 0.00% 0.000us 0.00% 0.000us 0.000us 130.526ms 0.37% 130.526ms 1.088ms 120
|
||||
aten::native_dropout_backward 0.00% 1.378ms 0.01% 3.156ms 15.394us 118.690ms 0.34% 118.690ms 578.975us 205
|
||||
------------------------------------------------------- ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------ ------------
|
||||
Self CPU time total: 35.229s
|
||||
Self CUDA time total: 35.322s
|
||||
|
||||
|
||||
========================================================================
|
||||
E. LAUNCH COUNTS (grouped_mm: 128/layer sequential = no-op, 1 = grouped)
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Steps 1/2/4 - profiler kernel table, sequence scaling, isolated CE timing.
|
||||
|
||||
Loads the real model exactly as erp_sft_harness.runtime does (same
|
||||
from_pretrained args, same PEFT config, same gradient checkpointing, same
|
||||
chunked-CE compute_loss) and measures:
|
||||
|
||||
A. sequence scaling 2x2048 / 2x8192 / 2x16384 fwd+bwd
|
||||
linear-dominated -> time falls ~8x from 16384 to 2048
|
||||
launch-bound -> time barely falls
|
||||
quadratic-dominated -> time falls ~64x
|
||||
B. isolated CE wall clock (CUDA events around the chunked-CE block)
|
||||
C. torch.profiler kernel table, sorted by self CUDA time
|
||||
D. expert-GEMM launch counts (settles grouped_mm without kernel-name
|
||||
archaeology: 128 sequential launches per layer = no-op, 1 = grouped)
|
||||
|
||||
Runs on GPU0, which is reserved and idle. Nothing else touches it.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
import torch
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
from peft import LoraConfig, get_peft_model
|
||||
|
||||
sys.path.insert(0, "/tank/erp-tune/eitri-smithy")
|
||||
from erp_sft_harness.core import IGNORE_INDEX, discover_target_modules
|
||||
|
||||
MODEL = "/tank/aimodels/gemma4-26b-a4b-it-heretic-bf16"
|
||||
CHUNK = 1024
|
||||
MB = 2
|
||||
|
||||
print("=" * 72)
|
||||
print("loading model")
|
||||
print("=" * 72, flush=True)
|
||||
t0 = time.time()
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
MODEL, dtype=torch.bfloat16, device_map={"": 0}, attn_implementation="sdpa",
|
||||
)
|
||||
targets = discover_target_modules(model)
|
||||
model = get_peft_model(model, LoraConfig(
|
||||
r=64, lora_alpha=128, lora_dropout=0.05, target_modules=targets,
|
||||
bias="none", task_type="CAUSAL_LM",
|
||||
))
|
||||
model.enable_input_require_grads()
|
||||
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
|
||||
model.train()
|
||||
print("loaded in %.1fs targets=%d" % (time.time() - t0, len(targets)), flush=True)
|
||||
|
||||
base = model.base_model.model if hasattr(model, "base_model") else model
|
||||
body = base.model
|
||||
lm_head = base.get_output_embeddings()
|
||||
softcap = getattr(model.config.get_text_config(), "final_logit_softcapping", None)
|
||||
print("final_logit_softcapping = %s" % softcap)
|
||||
print("attn_implementation = %s" % model.config.get_text_config()._attn_implementation)
|
||||
print(flush=True)
|
||||
|
||||
ce_ms = {"fwd": 0.0}
|
||||
|
||||
|
||||
def compute_loss(input_ids, attention_mask, labels, time_ce=False):
|
||||
"""Byte-for-byte the harness's compute_loss, with optional CE timing."""
|
||||
hidden = body(input_ids=input_ids, attention_mask=attention_mask,
|
||||
use_cache=False).last_hidden_state
|
||||
flat_hidden = hidden[:, :-1, :].reshape(-1, hidden.size(-1))
|
||||
flat_labels = labels[:, 1:].reshape(-1)
|
||||
keep = flat_labels != IGNORE_INDEX
|
||||
kept_hidden = flat_hidden[keep]
|
||||
kept_labels = flat_labels[keep]
|
||||
kept = int(kept_labels.numel())
|
||||
|
||||
def chunk_loss(chunk_hidden, chunk_labels):
|
||||
logits = lm_head(chunk_hidden).float()
|
||||
if softcap is not None:
|
||||
logits = torch.tanh(logits / softcap) * softcap
|
||||
return torch.nn.functional.cross_entropy(logits, chunk_labels, reduction="sum")
|
||||
|
||||
if time_ce:
|
||||
s, e = torch.cuda.Event(True), torch.cuda.Event(True)
|
||||
torch.cuda.synchronize()
|
||||
s.record()
|
||||
total = torch.zeros((), device=kept_hidden.device, dtype=torch.float32)
|
||||
for start in range(0, kept, CHUNK):
|
||||
total = total + torch.utils.checkpoint.checkpoint(
|
||||
chunk_loss, kept_hidden[start:start + CHUNK],
|
||||
kept_labels[start:start + CHUNK], use_reentrant=False,
|
||||
)
|
||||
if time_ce:
|
||||
e.record()
|
||||
torch.cuda.synchronize()
|
||||
ce_ms["fwd"] = s.elapsed_time(e)
|
||||
return total / kept, kept
|
||||
|
||||
|
||||
def make_batch(n, pad_frac=0.0):
|
||||
"""Synthetic batch. pad_frac trims the SECOND row and right-pads it,
|
||||
mimicking collate_mixed on a heterogeneous pair."""
|
||||
ids = torch.randint(100, 200000, (MB, n), device="cuda")
|
||||
am = torch.ones(MB, n, dtype=torch.long, device="cuda")
|
||||
labels = ids.clone()
|
||||
if pad_frac > 0:
|
||||
keep = int(n * (1 - pad_frac))
|
||||
am[1, keep:] = 0
|
||||
labels[1, keep:] = IGNORE_INDEX
|
||||
# ~40% of real tokens carry loss (measured mean 2188/2752 is higher, but
|
||||
# rp-dialogue assistant-only masking pulls the mix down); use the measured
|
||||
# global ratio 57.7M ctx -> 45.9M targets = 0.795
|
||||
m = torch.rand(labels.shape, device="cuda") > 0.795
|
||||
labels[m] = IGNORE_INDEX
|
||||
return ids, am, labels
|
||||
|
||||
|
||||
def timed(n, pad_frac=0.0, reps=2, time_ce=False, label=""):
|
||||
ids, am, labels = make_batch(n, pad_frac)
|
||||
for _ in range(1): # warmup
|
||||
loss, kept = compute_loss(ids, am, labels)
|
||||
loss.backward()
|
||||
model.zero_grad(set_to_none=True)
|
||||
torch.cuda.synchronize()
|
||||
best = None
|
||||
for _ in range(reps):
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
t = time.perf_counter()
|
||||
loss, kept = compute_loss(ids, am, labels, time_ce=time_ce)
|
||||
loss.backward()
|
||||
torch.cuda.synchronize()
|
||||
dt = time.perf_counter() - t
|
||||
best = dt if best is None else min(best, dt)
|
||||
model.zero_grad(set_to_none=True)
|
||||
peak = torch.cuda.max_memory_allocated() / 2**30
|
||||
print(" %-34s %7.3f s kept=%-6d peak=%5.1f GiB%s" % (
|
||||
label or ("2x%d pad=%.0f%%" % (n, pad_frac * 100)),
|
||||
best, kept, peak,
|
||||
(" CE=%.0f ms (%.1f%%)" % (ce_ms["fwd"], 100 * ce_ms["fwd"] / 1000 / best)) if time_ce else ""))
|
||||
return best
|
||||
|
||||
|
||||
print("=" * 72)
|
||||
print("A. SEQUENCE SCALING (no padding - isolates n)")
|
||||
print("=" * 72, flush=True)
|
||||
t2048 = timed(2048, 0.0, label="2 x 2,048")
|
||||
t8192 = timed(8192, 0.0, label="2 x 8,192")
|
||||
t16384 = timed(16384, 0.0, label="2 x 16,384")
|
||||
print()
|
||||
print(" 16384 -> 2048 ratio %.2fx (linear ~8x, launch-bound ~1x, quadratic ~64x)"
|
||||
% (t16384 / t2048))
|
||||
print(" 16384 -> 8192 ratio %.2fx (linear ~2x, quadratic ~4x)"
|
||||
% (t16384 / t8192))
|
||||
print(flush=True)
|
||||
|
||||
print("=" * 72)
|
||||
print("B. PADDING PENALTY (same real tokens, with vs without pad)")
|
||||
print("=" * 72, flush=True)
|
||||
timed(16384, 0.0, label="2 x 16,384 no padding")
|
||||
timed(16384, 0.5, label="2 x 16,384 50% pad on row 1")
|
||||
print(flush=True)
|
||||
|
||||
print("=" * 72)
|
||||
print("C. ISOLATED CE WALL CLOCK")
|
||||
print("=" * 72, flush=True)
|
||||
timed(16384, 0.0, reps=2, time_ce=True, label="2 x 16,384 (CE timed)")
|
||||
timed(4096, 0.0, reps=2, time_ce=True, label="2 x 4,096 (CE timed)")
|
||||
print(flush=True)
|
||||
|
||||
print("=" * 72)
|
||||
print("D. KERNEL TABLE - one fwd+bwd at 2 x 16,384")
|
||||
print("=" * 72, flush=True)
|
||||
ids, am, labels = make_batch(16384, 0.0)
|
||||
loss, _ = compute_loss(ids, am, labels)
|
||||
loss.backward()
|
||||
model.zero_grad(set_to_none=True)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
with torch.profiler.profile(
|
||||
activities=[torch.profiler.ProfilerActivity.CPU,
|
||||
torch.profiler.ProfilerActivity.CUDA],
|
||||
record_shapes=False, with_stack=False,
|
||||
) as prof:
|
||||
loss, _ = compute_loss(ids, am, labels)
|
||||
loss.backward()
|
||||
torch.cuda.synchronize()
|
||||
model.zero_grad(set_to_none=True)
|
||||
|
||||
print(prof.key_averages().table(sort_by="self_cuda_time_total", row_limit=45))
|
||||
|
||||
print()
|
||||
print("=" * 72)
|
||||
print("E. LAUNCH COUNTS (grouped_mm: 128/layer sequential = no-op, 1 = grouped)")
|
||||
print("=" * 72)
|
||||
rows = []
|
||||
for ev in prof.key_averages():
|
||||
if ev.self_device_time_total <= 0:
|
||||
continue
|
||||
rows.append((ev.key, ev.count, ev.self_device_time_total / 1000.0))
|
||||
rows.sort(key=lambda r: -r[2])
|
||||
print(" %-58s %8s %10s" % ("kernel", "count", "self ms"))
|
||||
for k, c, ms in rows[:30]:
|
||||
print(" %-58s %8d %10.1f" % (k[:58], c, ms))
|
||||
|
||||
total_ms = sum(r[2] for r in rows)
|
||||
print()
|
||||
print(" total self CUDA time %.1f ms" % total_ms)
|
||||
gemm = sum(ms for k, c, ms in rows if any(t in k.lower() for t in
|
||||
("gemm", "cutlass", "sm90", "sm100", "sm120", "nvjet", "ampere", "tensor")))
|
||||
print(" GEMM-ish kernels %.1f ms (%.1f%%)" % (gemm, 100 * gemm / total_ms))
|
||||
print(" non-GEMM %.1f ms (%.1f%%)" % (total_ms - gemm, 100 * (total_ms - gemm) / total_ms))
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Step 2 — padding ratio. Data-side, no GPU, no model.
|
||||
|
||||
Replicates the exact batching the trainer used: SequentialSampler over the
|
||||
encode-cache order, per_device_batch_size=2, collate_mixed right-padding to
|
||||
the pair max. Reports real vs padded token counts and the loss-target count
|
||||
that sizes the chunked CE.
|
||||
"""
|
||||
import json, sys
|
||||
from collections import Counter
|
||||
|
||||
CACHE = "/tank/erp-tune/run-01/encode-cache/encoded-a4b0796de1260930.jsonl"
|
||||
IGNORE_INDEX = -100
|
||||
MB = 2 # per_device_batch_size
|
||||
ACCUM = 8 # gradient_accumulation_steps
|
||||
|
||||
lens, kept_counts, kinds = [], [], []
|
||||
with open(CACHE) as fh:
|
||||
for line in fh:
|
||||
row = json.loads(line)
|
||||
ids = row["input_ids"]
|
||||
labels = row["labels"]
|
||||
lens.append(len(ids))
|
||||
kept_counts.append(sum(1 for x in labels if x != IGNORE_INDEX))
|
||||
kinds.append(row.get("sample_kind", "?"))
|
||||
|
||||
n = len(lens)
|
||||
print(f"records {n:,}")
|
||||
print(f"sample_kind mix {dict(Counter(kinds))}")
|
||||
print()
|
||||
print(f"seq len min/mean/max {min(lens)} / {sum(lens)/n:.0f} / {max(lens)}")
|
||||
print(f"loss targets min/mean/max {min(kept_counts)} / {sum(kept_counts)/n:.0f} / {max(kept_counts)}")
|
||||
print()
|
||||
|
||||
# --- micro-batch padding, exactly as collate_mixed builds it ---
|
||||
real = padded = 0
|
||||
mb_widths, mb_waste, mb_kept = [], [], []
|
||||
for i in range(0, n - n % MB, MB):
|
||||
group = lens[i:i + MB]
|
||||
width = max(group)
|
||||
r = sum(group)
|
||||
p = width * MB
|
||||
real += r
|
||||
padded += p
|
||||
mb_widths.append(width)
|
||||
mb_waste.append(1 - r / p)
|
||||
mb_kept.append(sum(kept_counts[i:i + MB]))
|
||||
|
||||
nb = len(mb_widths)
|
||||
print(f"micro-batches (mb={MB}) {nb:,}")
|
||||
print(f"real tokens {real:,}")
|
||||
print(f"padded tokens {padded:,}")
|
||||
print(f"PADDING WASTE {100 * (1 - real / padded):.1f}% ({padded - real:,} pad tokens)")
|
||||
print()
|
||||
print(f"mb width min/mean/max {min(mb_widths)} / {sum(mb_widths)/nb:.0f} / {max(mb_widths)}")
|
||||
srt = sorted(mb_widths)
|
||||
for q in (50, 75, 90, 95, 99):
|
||||
print(f" p{q} width {srt[int(nb*q/100)]}")
|
||||
print(f"mb at max_seq_len 16384 {sum(1 for w in mb_widths if w >= 16384):,} ({100*sum(1 for w in mb_widths if w>=16384)/nb:.1f}%)")
|
||||
print()
|
||||
srtw = sorted(mb_waste)
|
||||
print(f"per-mb waste p50/p90/max {100*srtw[nb//2]:.1f}% / {100*srtw[int(nb*0.9)]:.1f}% / {100*max(mb_waste):.1f}%")
|
||||
print()
|
||||
print(f"loss targets per mb min/mean/max {min(mb_kept)} / {sum(mb_kept)/nb:.0f} / {max(mb_kept)}")
|
||||
print(f" -> CE chunks per mb (1024) min/mean/max {min(mb_kept)//1024+1} / {sum(mb_kept)/nb/1024:.1f} / {max(mb_kept)//1024+1}")
|
||||
print()
|
||||
|
||||
# --- what length-bucketing would recover (sort by length, then batch) ---
|
||||
order = sorted(range(n), key=lambda i: lens[i])
|
||||
b_real = b_padded = 0
|
||||
for i in range(0, n - n % MB, MB):
|
||||
group = [lens[j] for j in order[i:i + MB]]
|
||||
b_real += sum(group)
|
||||
b_padded += max(group) * MB
|
||||
print("--- counterfactual: length-bucketed sampler ---")
|
||||
print(f"bucketed padded tokens {b_padded:,}")
|
||||
print(f"bucketed waste {100 * (1 - b_real / b_padded):.1f}%")
|
||||
print(f"TOKEN REDUCTION vs current {100 * (1 - b_padded / padded):.1f}%")
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Measure bucket-to-pair / shuffle-to-mix against the REAL encode cache.
|
||||
|
||||
Brokkr's design, validated on measured record lengths rather than a calibrated
|
||||
length model:
|
||||
|
||||
1. sort records by length
|
||||
2. cut into buckets of BUCKET records
|
||||
3. form micro-batches of 2 WITHIN each bucket (adjacent after sort)
|
||||
4. shuffle the resulting MICRO-BATCHES globally, seeded
|
||||
|
||||
Padding efficiency is a property of the pairing only, so step 4 costs nothing
|
||||
and restores root-mixing inside each accumulation window.
|
||||
|
||||
Also applies the fitted cost model from the replica scaling test to convert
|
||||
token savings into predicted wall clock.
|
||||
"""
|
||||
import json
|
||||
import random
|
||||
from collections import Counter
|
||||
|
||||
CACHE = "/tank/erp-tune/run-01/encode-cache/encoded-a4b0796de1260930.jsonl"
|
||||
IGNORE_INDEX = -100
|
||||
MB = 2
|
||||
ACCUM = 8
|
||||
SEED = 20260824
|
||||
|
||||
# fitted on the replica: t(w) = A*w + B*w^2 for a batch of 2 sequences of len w
|
||||
A = 6.8715e-04
|
||||
B = 8.8509e-08
|
||||
|
||||
rows = []
|
||||
with open(CACHE) as fh:
|
||||
for line in fh:
|
||||
r = json.loads(line)
|
||||
rows.append((len(r["input_ids"]), r.get("dataset_id", "?"),
|
||||
r.get("sample_kind", "?")))
|
||||
n = len(rows)
|
||||
print("records %d" % n)
|
||||
print()
|
||||
|
||||
|
||||
def evaluate(order, label, show_roots=False):
|
||||
real = padded = 0
|
||||
widths = []
|
||||
batches = []
|
||||
for i in range(0, n - n % MB, MB):
|
||||
grp = [rows[j] for j in order[i:i + MB]]
|
||||
w = max(g[0] for g in grp)
|
||||
real += sum(g[0] for g in grp)
|
||||
padded += w * MB
|
||||
widths.append(w)
|
||||
batches.append([g[1] for g in grp])
|
||||
nb = len(widths)
|
||||
Ew = sum(widths) / nb
|
||||
Ew2 = sum(w * w for w in widths) / nb
|
||||
t_mb = A * Ew + B * Ew2
|
||||
srt = sorted(widths)
|
||||
print("--- %s ---" % label)
|
||||
print(" padded tokens %s" % f"{padded:,}")
|
||||
print(" waste %.1f%%" % (100 * (1 - real / padded)))
|
||||
print(" E[w] (per-seq) %.0f" % Ew)
|
||||
print(" E[w^2] %.3e" % Ew2)
|
||||
print(" width p50/p90/p99 %d / %d / %d" % (
|
||||
srt[nb // 2], srt[int(nb * .9)], srt[int(nb * .99)]))
|
||||
print(" predicted micro-batch %.3f s (lin %.3f + quad %.3f, quad %.0f%%)" % (
|
||||
t_mb, A * Ew, B * Ew2, 100 * B * Ew2 / t_mb))
|
||||
print(" predicted step (x%d) %.1f s -> %.2f h over 1312 steps" % (
|
||||
ACCUM, t_mb * ACCUM, t_mb * ACCUM * 1312 / 3600))
|
||||
# unpadded micro-batches take the is_causal fast path on the 5 global layers
|
||||
exact = sum(1 for i in range(0, n - n % MB, MB)
|
||||
if len(set(rows[j][0] for j in order[i:i + MB])) == 1)
|
||||
print(" ZERO-PAD micro-batches %d / %d (%.1f%%) <- global layers on is_causal" % (
|
||||
exact, nb, 100 * exact / nb))
|
||||
if show_roots:
|
||||
# root diversity inside an accumulation window
|
||||
div = []
|
||||
for i in range(0, nb - nb % ACCUM, ACCUM):
|
||||
win = [d for b in batches[i:i + ACCUM] for d in b]
|
||||
div.append(len(set(win)))
|
||||
print(" roots per accum window mean %.2f min %d (of %d roots)" % (
|
||||
sum(div) / len(div), min(div), len({r[1] for r in rows})))
|
||||
print()
|
||||
return padded, t_mb
|
||||
|
||||
|
||||
# --- current: encode-cache order, SequentialSampler ---
|
||||
cur_padded, cur_t = evaluate(list(range(n)), "CURRENT (SequentialSampler)", True)
|
||||
|
||||
# --- bucket-to-pair + shuffle-to-mix ---
|
||||
# BUCKET controls the efficiency-vs-diversity trade: records are globally
|
||||
# sorted, cut into buckets of BUCKET, SHUFFLED WITHIN the bucket (not
|
||||
# re-sorted), then paired adjacently. BUCKET=2 is a perfect global sort
|
||||
# (0% waste, worst root mixing); larger buckets admit more length spread
|
||||
# inside a pair but draw partners from a wider slice of the corpus.
|
||||
for BUCKET in (2, 8, 32, 128, 512):
|
||||
by_len = sorted(range(n), key=lambda i: rows[i][0])
|
||||
rng = random.Random(SEED)
|
||||
micro = []
|
||||
for s in range(0, n, BUCKET):
|
||||
chunk = by_len[s:s + BUCKET]
|
||||
rng.shuffle(chunk) # mix WITHIN the length bucket
|
||||
for k in range(0, len(chunk) - len(chunk) % MB, MB):
|
||||
micro.append(chunk[k:k + MB])
|
||||
rng.shuffle(micro) # shuffle-to-mix across buckets
|
||||
order = [i for b in micro for i in b]
|
||||
placed = set(order)
|
||||
order += [i for i in by_len if i not in placed]
|
||||
p, t = evaluate(order, "BUCKET=%d, shuffle within + global micro-batch shuffle" % BUCKET,
|
||||
True)
|
||||
print(" >>> vs current: %.1f%% fewer padded tokens, %.1f%% less wall clock" % (
|
||||
100 * (1 - p / cur_padded), 100 * (1 - t / cur_t)))
|
||||
print()
|
||||
Reference in New Issue
Block a user