docs(training-playbook): §4 — when the artifact lies about itself
The playbook covered why a run is SLOW. It did not cover the more expensive
failure: a run that COMPLETES, reports plausible numbers, and is wrong about
itself. Seven of those turned up on the Gemma-4 ERP/RP tune between 08-24 and
08-26 and not one raised an error.
New §4, seven landmines plus a pre-launch checklist:
4.1 a cache key must cover the MEANING of the cached thing. The encode
cache missed the impersonation mask; run 2 would have reused run 1's
unmasked encodings and written impersonation_mask_sha256 into its own
manifest while doing it. No error, no count change, normal loss curve.
4.2 validating a VALUE is not validating the PARAMETER. warmup_ratio was
in range and deleted from transformers 5. Build kwargs as data and
diff the NAMES against the installed signature -- you cannot check the
argument list of a call you have already made.
4.3 record what the run RESOLVED to, never what it requested. Run 1
recorded no attention backend, so an MFU panel profiled the serving
seat under sdpa and recommended adopting flex_attention for a run that
was already using it.
4.4 never train from a dirty tree; harness_commit will name a commit that
does not describe the run. Annotate afterwards, never edit the shipped
artifact -- and state what is NOT wrong, or the note casts doubt on
every field it omits.
4.5 a watchdog whose pgrep pattern appears in its own argv can only ever
return "alive". The inert-gate shape in a liveness check.
4.6 an instrument nobody runs is not an instrument. Mutation-check any
test guarding a property that fails silently.
4.7 fix a stale measurement at the source. "~4.3 HOURS to rebuild the
encode cache" (really 145.5 s) was copied into a new launcher by the
same person who had just measured the real number.
4.8 the pre-launch honesty checklist, ten minutes.
Also:
- Header and framing widened. The file is now a training playbook with a
throughput half and an integrity half; the filename stays for inbound links.
- Sections 4-7 renumbered to 5-8. External refs are all to §1.1 and §3.4 and
are unaffected.
- Four rows added to the superseded-claims table, including the kernel table /
68% quadratic / 8.6% MFU set, which describe the serving seat rather than
the training run.
- gemma4-erp-tune-sizing.md §6 carries a correction banner with the explicit
falls/survives split, because that is the doc someone actually reads before
a run.
This commit is contained in:
@@ -62,13 +62,26 @@ repeats the playbook, you are re-litigating — record the delta in the playbook
|
||||
instead. When a playbook claim turns out wrong, don't just fix it: add a dated
|
||||
row to its superseded-claims table so old docs stop misleading people.
|
||||
|
||||
## Training throughput
|
||||
## Training runs
|
||||
|
||||
Same contract as quantization, different subject: **`docs/pfi/training-throughput-playbook.md`
|
||||
is the durable home** for why a training run is slow — the 10-minute scaling
|
||||
triage that names the regime before you profile, the padding/masking landmines,
|
||||
the profiler traps, and its own superseded-claims table. Read it before
|
||||
hypothesising about kernels.
|
||||
Same contract as quantization, different subject:
|
||||
**`docs/pfi/training-throughput-playbook.md` is the durable home** for spending
|
||||
a training window without wasting it. Two halves, and you want different ones at
|
||||
different moments:
|
||||
|
||||
- **§1–§3, why a run is SLOW** — the 10-minute scaling triage that names the
|
||||
regime before you profile, the padding/masking landmines, the profiler traps,
|
||||
the serving-path and base-viability pre-flights. Read before hypothesising
|
||||
about kernels.
|
||||
- **§4, why a run LIES about itself** — cache keys that miss a semantic change,
|
||||
values validated while the parameter was deleted, provenance recorded from a
|
||||
dirty tree, backends never recorded at all, watchdogs that watch themselves.
|
||||
**Read §4 before you launch**, and run its §4.8 checklist. Every failure in it
|
||||
produced a run that completed, reported plausible numbers, and was wrong — none
|
||||
raised an error.
|
||||
|
||||
(The filename still says "throughput" because things link to it; the scope is
|
||||
wider than the name.)
|
||||
|
||||
The instruments are committed at [`scripts/training-probes/`](scripts/training-probes/)
|
||||
with raw output kept alongside, so the claims can be re-derived rather than
|
||||
@@ -77,7 +90,10 @@ taken on faith.
|
||||
⚠ **Measure before you argue.** The playbook exists because a four-model
|
||||
frontier panel produced four self-retractions in ninety minutes on this
|
||||
question, and every one of them was a derivation while every survivor was a
|
||||
measurement.
|
||||
measurement. The §4 corollary is sharper: **a completed run is not evidence it
|
||||
did what you configured.** Two of that panel's conclusions were later voided
|
||||
outright because the benchmark and the trainer had silently different attention
|
||||
backends and nobody enumerated the delta.
|
||||
|
||||
## Purpose
|
||||
|
||||
|
||||
@@ -364,6 +364,38 @@ Model-agnostic lessons from this investigation are in
|
||||
probes are at [`scripts/training-probes/`](../../scripts/training-probes/).
|
||||
What follows is Gemma-4-specific.
|
||||
|
||||
> ## ⚠⚠ CORRECTION 2026-08-26 — MUCH OF THIS SECTION MEASURES THE WRONG PROCESS
|
||||
>
|
||||
> **The benchmarks below were run against the SERVING seat with
|
||||
> `attn_implementation="sdpa"` set explicitly. Training was running
|
||||
> `flex_attention` the whole time.** `ATTN_IMPLEMENTATION = "flex_attention"`
|
||||
> was a module constant passed unconditionally into `from_pretrained`, and
|
||||
> run 1's step-time distribution (n=1,445; min 11.84 / p50 19.75 / p99 30.52 /
|
||||
> max 45.79 s/it, the max being step 1's compile) confirms it stayed compiled —
|
||||
> a dynamo fallback sits in the hundreds of seconds per step.
|
||||
>
|
||||
> **FALLS** — describes sdpa, not the training run:
|
||||
> the three-point scaling fit and its 68% quadratic share; the kernel table
|
||||
> (`fmha_cutlassF/B` sm80, `EFFICIENT_ATTENTION`, attention 65.2%); the **8.6%
|
||||
> MFU** figure quoted above and throughout; the projection that elementwise
|
||||
> becomes the largest line item post-fix; and "adopt `flex_attention`" as the
|
||||
> round-two headline lever — **which round one already had.**
|
||||
>
|
||||
> **SURVIVES** — measured on the live training run:
|
||||
> the padding/bucketing win (44.3 → 20.1 s/it); the zero-pad fast-path
|
||||
> second-order effect; the eval-battery noise-floor work.
|
||||
>
|
||||
> ⚠ **Do not assume the direction of the correction.** Training's real MFU is
|
||||
> *unmeasured*, not obviously better. Flex with a BlockMask ought to beat
|
||||
> dense-masked sdpa, but that is a prediction and this investigation has been
|
||||
> unkind to those.
|
||||
>
|
||||
> The root cause was procedural, not technical, and it is written up as
|
||||
> playbook **§4.3**: run 1 recorded no attention backend in its provenance, so
|
||||
> the benchmark/trainer delta was invisible and nobody enumerated it. Run 2
|
||||
> onward records `attn_implementation_requested` **and** `_resolved`, plus the
|
||||
> torch/transformers versions and dynamo's compile counters.
|
||||
|
||||
### 6.1 Where the step time goes
|
||||
|
||||
Real checkpoint, GPU0, `attn_implementation="sdpa"`, PEFT + gradient
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
# Training throughput playbook — how to find where the step time went
|
||||
# Training playbook — spending a training window without wasting it
|
||||
|
||||
_Sibling to [`model-quantization-playbook.md`](model-quantization-playbook.md).
|
||||
That one is for making a model small; this one is for making a training run
|
||||
fast. Same contract: **model-agnostic lessons live here, model-specific ones
|
||||
stay in the per-model artifact and link up.**_
|
||||
That one is for making a model small; this one is for spending a training
|
||||
window well. Same contract: **model-agnostic lessons live here, model-specific
|
||||
ones stay in the per-model artifact and link up.**_
|
||||
|
||||
First written 2026-08-24 out of the Gemma-4 26B-A4B ERP/RP tune, which ran at
|
||||
8.6% MFU and cost a four-model frontier panel and most of a night to explain.
|
||||
The worked example in §7 is that run. The lessons above it are not about
|
||||
First written 2026-08-24 as a throughput playbook, out of the Gemma-4 26B-A4B
|
||||
ERP/RP tune that ran at 8.6% MFU and cost a four-model frontier panel and most
|
||||
of a night to explain. **§1–§3 are still that**: how to find where the step
|
||||
time went. **§4 is the other half**, added 2026-08-26 — how to keep a run's
|
||||
artifacts from lying about what they are. The filename still says
|
||||
`training-throughput-playbook.md` because things link to it; the scope is
|
||||
wider than the name.
|
||||
|
||||
The worked example in §8 is that first run. The lessons above it are not about
|
||||
Gemma-4.
|
||||
|
||||
> **Read this before hypothesising about kernels.** The single most expensive
|
||||
> **Read §1–§3 before hypothesising about kernels.** The single most expensive
|
||||
> failure in that investigation was not a wrong hypothesis. It was *four
|
||||
> people, including four frontier models, reasoning confidently from
|
||||
> arithmetic instead of spending ten minutes on a measurement that settled
|
||||
> it.* Two of the panel's conclusions were retracted by their own authors
|
||||
> within the hour. Every retraction was a derivation; every survivor was a
|
||||
> measurement.
|
||||
>
|
||||
> **Read §4 before you launch.** Every failure in it produced a run that
|
||||
> completed, reported plausible numbers, and was wrong about itself. None
|
||||
> raised an error. Two of them cost a panel and a night *after* the fact,
|
||||
> chasing a configuration the run was already in.
|
||||
|
||||
---
|
||||
|
||||
@@ -450,7 +461,346 @@ the free discriminator: **if both arms return zero deflections the model is
|
||||
binary; if only one does, the regex is fine and the difference is real.** An
|
||||
instrument artifact does not care which arm it runs against.
|
||||
|
||||
## 4. Panel / consult discipline for perf work
|
||||
## 4. When the artifact lies about itself
|
||||
|
||||
§1–§3 are about a run that is *slow*. This section is about a run that
|
||||
**completes, reports plausible numbers, and is wrong** — and about the derived
|
||||
artifacts that go on repeating the wrong thing afterwards.
|
||||
|
||||
Every failure below was found on the Gemma-4 ERP/RP tune between 2026-08-24 and
|
||||
2026-08-26. **Not one of them raised an error.** They are ordered by how much
|
||||
they cost.
|
||||
|
||||
> **The shape they share**, stated by brokkr-smithy-dev on 2026-08-26 after the
|
||||
> third instance in a day: *when you change what an artifact means, every
|
||||
> derived artifact keyed on the old meaning is now a liar.* Caches, logs,
|
||||
> comments, manifests, benchmark write-ups. The fix is always the same — put the
|
||||
> meaning in the key — and the reason it keeps happening is that the old
|
||||
> artifact still loads, still parses, and still looks right.
|
||||
|
||||
### ⚠ 4.1 ⭐⭐ A cache key must cover the MEANING of the cached thing, not just its inputs
|
||||
|
||||
The encode cache for the ERP corpus was keyed on corpus identity, `max_seq_len`,
|
||||
base-model path and chat-template sha. Run 2 then added an **impersonation
|
||||
loss-mask** — 813 turns whose labels change from trained to ignored.
|
||||
|
||||
The mask was not in the key. Run 2 would have hit run 1's 609 MB cache, reused
|
||||
its **unmasked** encodings, trained the impersonation straight back in, and
|
||||
written `impersonation_mask_sha256` into its own provenance manifest while doing
|
||||
it.
|
||||
|
||||
Nothing downstream could have caught it:
|
||||
|
||||
| signal | what it would have shown |
|
||||
|---|---|
|
||||
| error / exception | none — a cache hit is the happy path |
|
||||
| sample count | unchanged — 0 samples were fully masked |
|
||||
| record count | unchanged — 20,982 either way |
|
||||
| loss curve | normal |
|
||||
| provenance manifest | asserts the mask was applied |
|
||||
|
||||
A seven-hour run whose artifact claims a property it does not have, invisible
|
||||
from every number anyone would think to check.
|
||||
|
||||
**The rule.** A cache key covers every input that can change the *semantics* of
|
||||
the output, not just the ones that change its *shape*. Concretely:
|
||||
|
||||
- Hash the **content** of every auxiliary file (mask, filter list, label map),
|
||||
not its path. A path is stable across an edit; that is the whole problem.
|
||||
- Carry an explicit **`ENCODE_VERSION`** integer and bump it whenever the
|
||||
encoder's output can change for identical inputs. Cheap, blunt, and it
|
||||
catches the cases you did not think to hash.
|
||||
- **Extract the key computation into a named, tested function.** Ours became
|
||||
`core.encode_cache_key` specifically so a test could assert that a masked run
|
||||
and an unmasked run get different keys — and that a legitimate resume still
|
||||
gets the same one. An inline dict comprehension inside the loader cannot be
|
||||
tested and will not be.
|
||||
|
||||
```python
|
||||
def test_encode_cache_key_separates_a_masked_run_from_an_unmasked_one():
|
||||
unmasked = encode_cache_key(ordered, **common)
|
||||
masked = encode_cache_key(ordered, **common, impersonation_mask_sha256="d"*64)
|
||||
assert unmasked != masked
|
||||
# and the cache must still HIT on a legitimate resume
|
||||
assert masked == encode_cache_key(ordered, **common, impersonation_mask_sha256="d"*64)
|
||||
```
|
||||
|
||||
**Generalises past caches.** Any memoised, derived, or checkpointed artifact has
|
||||
this problem: encode caches, tokenised datasets, precomputed embeddings,
|
||||
distillation logits, eval-result files. If it was derived under one meaning and
|
||||
is reused under another, it is now a liar.
|
||||
|
||||
### ⚠ 4.2 ⭐⭐ Validating a VALUE is not validating the PARAMETER
|
||||
|
||||
Run 2 died after the full encode and after all 1,013 weight shards had loaded:
|
||||
|
||||
TypeError: TrainingArguments.__init__() got an unexpected keyword
|
||||
argument 'warmup_ratio'
|
||||
|
||||
`warmup_ratio` exists in transformers 4. It is **gone in 5.15.1**, which keeps
|
||||
only `warmup_steps`. The harness had careful config-level validation — it
|
||||
checked `0.0 <= warmup_ratio < 1.0` and rejected an unknown scheduler name — and
|
||||
none of it could have caught this. **The value was in range. The parameter had
|
||||
been deleted.**
|
||||
|
||||
The check that catches it has to run against the **installed library**, not
|
||||
against your own schema:
|
||||
|
||||
```python
|
||||
def assert_training_arguments_accepts(kwargs: dict, cls) -> None:
|
||||
accepted = set(inspect.signature(cls.__init__).parameters)
|
||||
unknown = sorted(set(kwargs) - accepted)
|
||||
if unknown:
|
||||
raise TypeError(f"{cls.__name__} does not accept {unknown}. "
|
||||
f"It accepts: {', '.join(sorted(accepted))}")
|
||||
```
|
||||
|
||||
Which requires building the kwargs as **data** first — *you cannot check the
|
||||
argument list of a call you have already made.* That single structural change is
|
||||
the lesson; the signature diff is trivial once the kwargs are a dict.
|
||||
|
||||
Three riders:
|
||||
|
||||
- **Keep the portable unit in config, convert at the call site.** We kept
|
||||
`warmup_ratio` in the config file and convert to `warmup_steps` on the wire.
|
||||
A ratio survives a change in corpus size; a step count silently becomes a
|
||||
different fraction of the run.
|
||||
- **Mirror the framework's own arithmetic exactly.** HF ceilings *twice* —
|
||||
records into micro-batches, then micro-batches into optimizer steps. One
|
||||
combined division gave 1,311 steps for our corpus where the trainer reports
|
||||
1,312, which would have put warmup a step short. Pin it with a test against a
|
||||
step count a real run actually printed.
|
||||
- **The failure mode is the expensive one: late.** Anything checkable from the
|
||||
config, the library signature, or a file on disk belongs *before* the
|
||||
tokenizer, the encode, and the model load. Ours now dies in under a second.
|
||||
|
||||
### ⚠ 4.3 ⭐⭐ Record what the run RESOLVED to, never what it requested
|
||||
|
||||
Run 1's provenance recorded no attention backend at all. Six weeks of
|
||||
conclusions rested on the answer.
|
||||
|
||||
An MFU investigation had profiled the **serving seat** with
|
||||
`attn_implementation="sdpa"` explicitly set, produced a kernel table
|
||||
(`fmha_cutlass*_sm80`, `EFFICIENT_ATTENTION`, attention 65.2% of step), an 8.6%
|
||||
MFU figure, and a headline recommendation: **adopt `flex_attention` for round
|
||||
two.**
|
||||
|
||||
Training had been running `flex_attention` the entire time.
|
||||
|
||||
ATTN_IMPLEMENTATION = "flex_attention" # module constant
|
||||
...from_pretrained(..., attn_implementation=ATTN_IMPLEMENTATION)
|
||||
|
||||
A panel, a kernel profile and three rounds of arithmetic went toward a
|
||||
configuration the run was already in. The single biggest round-two optimisation
|
||||
identified was a thing training already had.
|
||||
|
||||
**The correction has to be stated as a split, because some of the work
|
||||
survives.** After the flex finding, on this investigation:
|
||||
|
||||
FALLS — describes the sdpa seat, not the training run
|
||||
the three-point scaling fit and its 68% quadratic share
|
||||
the kernel table
|
||||
the 8.6% MFU number
|
||||
"adopt flex_attention" as the round-two headline lever
|
||||
|
||||
SURVIVES — measured on the live training run
|
||||
the padding/bucketing win, 44.3 -> 20.1 s/it
|
||||
the zero-pad fast-path second-order effect
|
||||
the eval-battery noise-floor work (a different instrument entirely)
|
||||
|
||||
⚠ **Do not assume the direction of the correction.** Training's real MFU is now
|
||||
*unmeasured*, not obviously better. Flex with a BlockMask ought to beat
|
||||
dense-masked sdpa — but that is a prediction, and predictions have done badly
|
||||
here.
|
||||
|
||||
**What to record, and why two fields:**
|
||||
|
||||
```python
|
||||
"attn_implementation_requested": ATTN_IMPLEMENTATION, # a constant
|
||||
"attn_implementation_resolved": model.config._attn_implementation, # what happened
|
||||
"torch_version": torch.__version__,
|
||||
"transformers_version": transformers.__version__,
|
||||
"dynamo_counters": _dynamo_counters(), # best-effort, nullable, never raises
|
||||
```
|
||||
|
||||
Requested and resolved are **different claims**. The requested value is a line
|
||||
in your source; the resolved value is what the library settled on after checking
|
||||
availability, and only that one describes the run. A framework that silently
|
||||
downgrades an unavailable backend will make them differ, and that difference is
|
||||
exactly what you want on the record.
|
||||
|
||||
The dynamo counters matter for the same reason: dynamo's fallback to
|
||||
**uncompiled** flex is silent, roughly 20× slower, and documented not to work
|
||||
correctly through the backward pass. The counters are the only in-band evidence
|
||||
it did not happen.
|
||||
|
||||
**When the run is already going and the field is missing** — as ours was — you
|
||||
can often still answer it, but only forensically. For us:
|
||||
|
||||
1. **Source**, for what was requested (unconditional constant, no fallback flag).
|
||||
2. **A config-only side probe** on the same library versions, for what it
|
||||
resolves to: `AutoConfig.from_pretrained(path, attn_implementation=...)` then
|
||||
read `_attn_implementation`. No weights, no GPU, seconds.
|
||||
3. **The step-time distribution**, for whether it stayed compiled. Run 1's
|
||||
1,445 logged steps ran min 11.84 / p50 19.75 / p99 30.52 / max 45.79 s/it,
|
||||
the maximum being step 1's compile. A dynamo fallback would sit in the
|
||||
hundreds of seconds per step. Nothing in the trace approaches it.
|
||||
|
||||
That is three sources of evidence to replace one recorded field, and it only
|
||||
worked because the source tree happened to still be on disk. Record the field.
|
||||
|
||||
### ⚠ 4.4 ⭐⭐ Never train from a dirty tree — the provenance commit will be a lie
|
||||
|
||||
Run 1's manifest recorded `harness_commit 35a4e8e`. The working tree carried
|
||||
**224 uncommitted lines** across two modules for the entire run, so the recorded
|
||||
commit predates the code that trained the adapter. The artifact is **not
|
||||
reproducible from the commit it names**, and nothing says so.
|
||||
|
||||
`_git_commit()` calling `git rev-parse HEAD` is not wrong; it is *incomplete*.
|
||||
It answers "what is HEAD" when the question is "what code ran."
|
||||
|
||||
**Add a cleanliness assertion to the pre-flight**, beside the corpus and holdout
|
||||
checks:
|
||||
|
||||
```bash
|
||||
git diff --quiet && git diff --cached --quiet || {
|
||||
echo "REFUSING: working tree is dirty; harness_commit would not describe this run"
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
Record `git describe --always --dirty` rather than a bare SHA if you want a
|
||||
softer version, but an outright refusal is better: a run long enough to be worth
|
||||
provenance is long enough to be worth one commit first.
|
||||
|
||||
**Correcting it after the fact — annotate, never edit.** We left
|
||||
`provenance.json` untouched and wrote a `PROVENANCE-NOTE.md` beside it. Editing
|
||||
a shipped artifact so it says something it did not say is the worse failure.
|
||||
|
||||
⚠ **State what is NOT wrong.** A bare correction note casts doubt over every
|
||||
field it does not mention, and the next reader has no way to tell which. Ours
|
||||
ends by listing what remains accurate — recipe sha, root shas, base model path
|
||||
and revision, template sha, the override triple, the dropped ids.
|
||||
|
||||
### ⚠ 4.5 ⭐ A watchdog whose pattern matches its own argv can only ever return "alive"
|
||||
|
||||
The training monitor polled liveness with:
|
||||
|
||||
```bash
|
||||
while pgrep -f "erp_sft_harness --config" >/dev/null; do sleep 60; done
|
||||
echo "PROCESS EXITED"
|
||||
```
|
||||
|
||||
`pgrep -f` matches full command lines — **including the monitor's own**, because
|
||||
the pattern is right there in its argv. The loop matched itself, so the exit
|
||||
branch was unreachable **for every possible input**. The run crashed and the
|
||||
watchdog reported nothing, because it was watching itself.
|
||||
|
||||
It also blocked the recovery: the launcher's already-running guard used the same
|
||||
pattern, matched the monitor, and refused to start the replacement run.
|
||||
|
||||
**This is the inert-gate shape in a liveness check** — a test that cannot return
|
||||
the failing verdict for any input. It is the same defect as an assertion
|
||||
comparing a value to itself, and it hides better, because a watchdog that never
|
||||
fires looks exactly like a system that never breaks.
|
||||
|
||||
Fixes, in order of preference:
|
||||
|
||||
```bash
|
||||
RUNPID=$(pgrep -f "<pattern>" | head -1) # resolve ONCE, from a clean shell
|
||||
while kill -0 "$RUNPID" 2>/dev/null; do sleep 60; done
|
||||
```
|
||||
|
||||
- **Poll a captured PID, not a pattern.** `kill -0` cannot self-match.
|
||||
- If you must pattern-match, **break the literal** so it is absent from your own
|
||||
argv (`"erp_sft_harn""ess --config"`), and put the guard in a **file** rather
|
||||
than an inline `ssh host '...'` — the invoking command line is argv too.
|
||||
- Never `pkill -f` a shared pattern: it is handle-blind and kills every other
|
||||
agent's monitor on the box along with yours.
|
||||
|
||||
**Test the negative.** Whatever the gate is, construct the input that should
|
||||
make it fire and confirm that it does. Every gate in this project that has ever
|
||||
caught anything was one somebody deliberately broke first.
|
||||
|
||||
### ⚠ 4.6 ⭐ An instrument nobody runs is not an instrument
|
||||
|
||||
The harness test suite was **10 passed / 4 failed**, and there was **no pytest
|
||||
installed in the training venv at all** — so nothing had ever run it. A run had
|
||||
already shipped an adapter through it.
|
||||
|
||||
The good version of that news: all four failures were *stale tests*, not broken
|
||||
code. Each asserted a behaviour that had been deliberately changed —
|
||||
`chat_template_path` became required, the provenance schema grew three keys,
|
||||
persona trimming gave way to the unfittable path. The bad version: nobody knew
|
||||
that, because the instrument was dark.
|
||||
|
||||
- **Installing the test runner is part of standing up the training venv**, not a
|
||||
later nicety. It is three pure-Python packages and touches nothing in the
|
||||
torch/transformers stack.
|
||||
- **Repair stale tests to the current contract; do not delete them.** Each
|
||||
rewrite is an opportunity to write down *why* the behaviour changed — ours now
|
||||
carry the corpus measurement showing the removed persona-trimming costs zero
|
||||
samples on this corpus.
|
||||
- **Mutation-check any test guarding an invisible property.** A test for a
|
||||
silent failure is itself silent when it is vacuous. Break the code
|
||||
deliberately and confirm the test fails:
|
||||
|
||||
| deliberate break | test that must fail |
|
||||
|---|---|
|
||||
| OR-merge → last-wins | whole-run masking |
|
||||
| drop the sha assertion | corpus/mask disagreement |
|
||||
| resolve indices after the merge | source-index resolution |
|
||||
| re-add the removed kwarg | installed-signature check |
|
||||
|
||||
If breaking it changes nothing, you have documentation, not a test.
|
||||
|
||||
### ⚠ 4.7 ⭐ Fix a stale measurement at the SOURCE, or the copy carries it forward
|
||||
|
||||
A launcher script carried the comment *"the 609 MB encode cache (2.5 min to
|
||||
reuse, ~4.3 HOURS to rebuild)."* That figure predated the encoder's own
|
||||
parallelisation. Measured on run 2: **145.5 seconds** on 32 workers. Off by a
|
||||
factor of 106.
|
||||
|
||||
It was believed twice — once to project an 11.7-hour window for a 7.6-hour job,
|
||||
and once when it was copied verbatim into a **new** launcher written by the same
|
||||
person who had just measured the real number. **The stale figure propagated in
|
||||
the same motion that was supposed to retire it.**
|
||||
|
||||
- When a measurement in a comment is superseded, `grep` the repo for the number
|
||||
before you fix the one in front of you.
|
||||
- Copying a header wholesale copies its claims wholesale. Re-read boilerplate
|
||||
you paste for facts that have expired.
|
||||
- Prefer a **dated** measurement in prose — "145.5 s on 32 workers, measured
|
||||
2026-08-26" — over a bare figure. A dated claim invites a re-check; a bare one
|
||||
reads as timeless.
|
||||
|
||||
### 4.8 The pre-launch honesty checklist
|
||||
|
||||
Ten minutes, before the window opens. Every item is something that produced a
|
||||
completed, plausible, wrong run above.
|
||||
|
||||
```
|
||||
[ ] working tree committed git diff --quiet && git diff --cached --quiet
|
||||
[ ] test suite green and the runner is actually installed
|
||||
[ ] cache key covers the change bump the version integer; hash aux file CONTENT
|
||||
[ ] kwargs checked by NAME against the installed library signature
|
||||
[ ] config validated before tokenizer, encode and model load
|
||||
[ ] provenance records RESOLVED backend, library versions, aux-file shas
|
||||
[ ] present-and-null, not absent a run that claims nothing must say so explicitly
|
||||
[ ] watchdog tested negative kill something and confirm it fires
|
||||
[ ] stale numbers grepped for the figure, repo-wide, not just in view
|
||||
```
|
||||
|
||||
⚠ **The present-and-null line is load-bearing and the least obvious.** Emit
|
||||
every provenance key always, `null` when unused. A manifest that *omits* a key
|
||||
when there was nothing to report is indistinguishable from one written by a
|
||||
harness too old to know the key exists — so an adapter trained without your
|
||||
safeguard becomes byte-indistinguishable from one trained with it. Present-and-
|
||||
null is a statement; absent is an accident.
|
||||
|
||||
---
|
||||
|
||||
## 5. Panel / consult discipline for perf work
|
||||
|
||||
Perf investigations are unusually good at generating confident wrong answers,
|
||||
because the arithmetic is easy and the ground truth is expensive. Specific
|
||||
@@ -470,7 +820,7 @@ guards, learned the hard way:
|
||||
|
||||
---
|
||||
|
||||
## 5. Superseded claims — do not follow these
|
||||
## 6. Superseded claims — do not follow these
|
||||
|
||||
| claim | status | replaced by |
|
||||
|---|---|---|
|
||||
@@ -480,8 +830,12 @@ guards, learned the hard way:
|
||||
| The chunked CE is a 2–5× under-estimated cost centre | **WRONG** (2026-08-24) | Measured 1.1% of step forward, ≲3% with recompute |
|
||||
| `attn_implementation="flash_attention_2"` is the per-layer lever | **NOT A FLAG** (2026-08-24) | All-or-nothing at `from_pretrained`; per-layer needs a custom fn on `ALL_ATTENTION_FUNCTIONS`. FA2 also caps head_dim at 256. |
|
||||
| Bucket size ~256 is needed to preserve source diversity | **UNNECESSARY** (2026-08-24) | Diversity is flat in bucket size; the global micro-batch shuffle does that work — see §3.2 |
|
||||
| The 2026-08-24 kernel table / 68% quadratic share / 8.6% MFU describe the TRAINING run | **WRONG** (2026-08-26) | They describe the **serving seat**, benchmarked with `attn_implementation="sdpa"` set explicitly. Training ran `flex_attention` throughout. Training's real MFU is now *unmeasured* — see §4.3 for the full falls/survives split, and do not assume the correction's direction. |
|
||||
| "Adopt `flex_attention`" is the round-two headline lever | **ALREADY BANKED** (2026-08-26) | It was live in round one. A panel, a kernel profile and three rounds of arithmetic went toward a configuration the run was already in — see §4.3 |
|
||||
| The encode cache takes ~4.3 hours to rebuild | **WRONG** (2026-08-26) | **145.5 s** on 32 workers, measured on run 2. The stale figure predated the encoder's own parallelisation, was used to project an 11.7 h window for a 7.6 h job, and was then copied verbatim into a new launcher — see §4.7 |
|
||||
| `warmup_ratio` is a valid `TrainingArguments` kwarg | **REMOVED IN transformers 5** (2026-08-26) | 5.15.1 keeps only `warmup_steps`. Keep the ratio in config, convert at the call site, and diff kwarg NAMES against the installed signature — see §4.2 |
|
||||
|
||||
## 6. Measured negatives — don't re-chase
|
||||
## 7. Measured negatives — don't re-chase
|
||||
|
||||
- **Fused MoE kernel (`grouped_mm`) as the throughput fix.** Measured 0.9%
|
||||
*slower* than the Python loop and bit-identical. Independently, dense GEMM is
|
||||
@@ -498,7 +852,7 @@ guards, learned the hard way:
|
||||
|
||||
---
|
||||
|
||||
## 7. Worked example — Gemma-4 26B-A4B ERP/RP tune, 2026-08-24
|
||||
## 8. Worked example — Gemma-4 26B-A4B ERP/RP tune, 2026-08-24
|
||||
|
||||
Model-specific detail lives in
|
||||
[`gemma4-erp-tune-sizing.md`](gemma4-erp-tune-sizing.md) §6. The short version,
|
||||
|
||||
Reference in New Issue
Block a user