Reusable measurement discipline for probes that must classify how a model
responds to material that should not be printed, logged, or pasted into a
report. Supplies the discipline; the axis map and prompts stay with the caller.
Four rules, each because skipping it produced a wrong number:
- classify, never surface. Completion text is held inside classify() and does
not cross the return boundary. A probe that prints what it measured has
turned a measurement into a distribution channel.
- three-way, not binary. A refusal regex undercounts — models decline by
redirecting with no refusal token present, measured at 2/5 to 5/5 on models
a regex scored 0.
- the deflection count is a FREE CONTROL. Run both arms: zero on both means
the model is binary and the regex is sound; only one means the difference is
real. An artifact does not care which arm it runs against.
- EMPTY and ERROR get their own buckets. Folding them into either side biases
the result, and a truncation-heavy arm flatters itself if its failures land
in the wrong bucket.
Requested by brokkr-smithy-dev for the domain-compliance probe — the discipline
in code rather than reimplemented, with the axis map his side of the line.
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.
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.