unparallelled commited on
Commit
d067a93
·
verified ·
1 Parent(s): fe26edf

Upload scripts/mix64k_h200.sh with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/mix64k_h200.sh +343 -0
scripts/mix64k_h200.sh ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # 32k -> 64k extension on the MIXED corpus. 8x H200, cp=4 dp=2.
4
+ #
5
+ # THE EXPERIMENT
6
+ # The 2026-09-12 run extended to 64k on 100% b5 (every doc >= 65,536 tokens)
7
+ # and destroyed retrieval: 67/65/52/29 -> 45/17/2/1 at 8k/16k/32k/64k, while
8
+ # training loss FELL 2.89 -> 1.14. Data composition is the leading suspect
9
+ # (100% long-context data is documented to cause severe degradation).
10
+ # This run changes ONE variable: the data is now 50% b5 / 30% b3 / 20% b1.
11
+ # LR, schedule shape, parent and window are all held identical so a null
12
+ # result is attributable.
13
+ #
14
+ # PARENT is ext64k step 695 -- the SAME starting point the failed cont64k run
15
+ # used, so the only changed variable is the data. Its own measured scores
16
+ # (67/65/52/29, logs/ext64k_len_*/summary.csv on HF) are the step-0 baseline.
17
+ # Starting instead from the 32k model would top out at 1,714 cumulative 64k
18
+ # steps, short of the 1,894 where the cliff actually appeared -- the run could
19
+ # miss the failure regime entirely and prove nothing.
20
+ #
21
+ # WHY cp=4 dp=2 AND NOT cp=8
22
+ # 16,384 tok/rank MEASURED 108,493 MB, which fits H200's 143 GB with room.
23
+ # dp=2 halves the micro-steps per optimizer step (8 -> 4) instead of adding
24
+ # ring hops: ~6.75 s/step projected vs 7.81 measured for cp=8 on H100.
25
+ #
26
+ # SEGMENTED, BECAUSE EVAL NEEDS THE GPUS
27
+ # CP_MAX_STEPS is ALWAYS the full 1714 -- never the segment target. That
28
+ # keeps the WSD schedule identical across segments and keeps the guard at
29
+ # bluscriptCP.cpp:1006 (warmup+stable >= max_steps-1) satisfied. A watcher
30
+ # stops the trainer when the segment's checkpoint lands. Resumes pin
31
+ # CP_REWARMUP_ANCHOR=0; its default (-1) re-anchors warmup to start_step and
32
+ # would re-ramp the LR at every resume.
33
+ #
34
+ # WHAT KILLED THE LAST RUN'S POST-MORTEM
35
+ # CP_CKPT_KEEP=2 pruned every checkpoint before the collapse, so the last
36
+ # good model was unrecoverable and the failure could not be bisected. Here a
37
+ # janitor preserves rank_0 of EVERY save (3.75 GB; eval only ever loads
38
+ # rank_0 at NP=1) under a prefix the pruner cannot match, and uploads on a
39
+ # coarser cadence. Worst case you lose 100 steps.
40
+ #
41
+ # COLLAPSE DETECTION IS FREE AND CONTINUOUS
42
+ # The cliff is visible in training loss long before any eval: the failed run
43
+ # went 2.886 -> 2.642 -> 2.185 across steps 1100-1249 on a 50-step mean.
44
+ # detect_cliff() compares the current 50-step mean to the 200-700 baseline.
45
+ # Loss CANNOT prove retrieval is intact -- that is the whole lesson -- so the
46
+ # evals still run; the detector only decides WHICH checkpoints to evaluate.
47
+ # =============================================================================
48
+ set -uo pipefail
49
+ cd "$(dirname "$0")"
50
+ ROOT="$(cd ../.. && pwd)"
51
+ export LD_LIBRARY_PATH="$ROOT/Tensor-Implementations/lib:$ROOT/Profiler/lib:${LD_LIBRARY_PATH:-}"
52
+ export OMPI_ALLOW_RUN_AS_ROOT=1 OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1
53
+ export PATH=/usr/local/cuda/bin:$PATH
54
+
55
+ PY=${PY:-/root/Adhi/BluTrain/envv/bin/python3}
56
+ HF=${HF:-/root/Adhi/BluTrain/envv/bin/hf}
57
+ EXE=./build/bluscriptCP_exec
58
+ CK=checkpoints_bluscriptcp/rank_0
59
+ LOG=mix64k_logs; mkdir -p "$LOG"
60
+ M="$LOG/master.log"
61
+ REPO=unparallelled/BluScriptCP
62
+
63
+ T=65536; NP=8; CPSZ=4; GPUS=0,1,2,3,4,5,6,7 # dp = NP/CPSZ = 2
64
+ ORIG=32768; SCALE=2 # 2 * 32768 == 65536
65
+ STEPS=${STEPS:-1300} # past the cliff with margin; mix50 allows 1716
66
+ WARM=100; TAIL=2; FREQ=100; KEEP=2
67
+ STABLE=$(( STEPS - WARM - TAIL )) # plateau throughout, like the old ext64k
68
+ GB=524288
69
+ DATA=Data_Loader/Data/mix50
70
+ PARENT="$CK/ext64k_step695.ckpt"
71
+ PARENT_SZ=5225752094
72
+ HAY="$PWD/Data_Loader/Data/b2_8192_16384/val_shard_00000.bin"
73
+ EVAL_A=${EVAL_A:-695} # A/B point vs the failed ext64k
74
+ EVAL_B=${EVAL_B:-1200} # collapse check
75
+ UPLOAD_EVERY=${UPLOAD_EVERY:-300}
76
+
77
+ say(){ echo "[$(date -u +%H:%M:%S)] $*" | tee -a "$M" >&2; }
78
+ die(){ say "FATAL: $*"; exit 1; }
79
+
80
+ # ---------------------------------------------------------------- preflight --
81
+ $PY -c "import tiktoken" 2>/dev/null || die "tiktoken missing (eval would score nothing, silently)"
82
+ [ -x "$EXE" ] || die "$EXE not built"
83
+ STAMP=build/objects/.cp_rope_flags
84
+ [ -f "$STAMP" ] || die "no build-flag stamp; rebuild: make CP_FUSED_ROPE=1 CP_ATTN_SPLIT=1 SM_ARCH=90 bluscript-cp -j\$(nproc)"
85
+ grep -q 'CP_FUSED_ROPE=1' "$STAMP" && grep -q 'CP_ATTN_SPLIT=1' "$STAMP" || \
86
+ die "binary lacks the split kernels ($(cat "$STAMP")). CP_ATTN_FUSION=split will throw at the first forward. Rebuild: make CP_FUSED_ROPE=1 CP_ATTN_SPLIT=1 SM_ARCH=90 bluscript-cp -j\$(nproc)"
87
+ [ -f "$PARENT" ] || die "parent missing: $PARENT"
88
+ [ "$(stat -c%s "$PARENT")" = "$PARENT_SZ" ] || die "parent wrong size: $(stat -c%s "$PARENT") (want $PARENT_SZ)"
89
+ [ -f "$HAY" ] || die "haystack missing: $HAY"
90
+ [ -d "$DATA" ] || die "$DATA missing -- run mix_buckets.py first"
91
+ [ $((SCALE*ORIG)) -eq $T ] || die "YaRN invariant: $SCALE * $ORIG != $T"
92
+ [ $((NP % CPSZ)) -eq 0 ] || die "NP=$NP not divisible by CP_SIZE=$CPSZ"
93
+ [ $(( WARM + STABLE )) -lt $(( STEPS - 1 )) ] || die "illegal WSD: $WARM+$STABLE >= $STEPS-1"
94
+ # the manifest counts batches the way DataLoaderLite actually consumes them;
95
+ # total_tokens//global_batch silently overshoots and the loader wraps in silence
96
+ if [ -f "$DATA/MANIFEST.txt" ]; then
97
+ USABLE=$(awk '/^usable_steps/{print $2}' "$DATA/MANIFEST.txt")
98
+ [ "$STEPS" -le "$USABLE" ] || die "STEPS=$STEPS exceeds usable_steps=$USABLE -- the loader would WRAP silently"
99
+ say "manifest: usable_steps=$USABLE, running $STEPS"
100
+ else
101
+ say "WARN: no MANIFEST.txt in $DATA -- cannot rule out a silent loader wrap"
102
+ fi
103
+ FREEG=$(df -BG --output=avail / | tail -1 | tr -dc 0-9)
104
+ [ "$FREEG" -ge 120 ] || die "only ${FREEG} GB free; need ~120 (30 GB live ckpts + ~64 GB preserved rank_0)"
105
+
106
+ say "=== 32k -> 64k on the MIXED corpus (8x H200, cp=$CPSZ dp=$((NP/CPSZ))) ==="
107
+ say "parent: ext64k step 695 (64k model, 67/65/52/29) -- weights-only: its 4-rank ZeRO sidecars cannot load at world_size 8"
108
+ say "data: $DATA $(awk '/^mix /{printf "%s=%s ", $2, $4}' "$DATA/MANIFEST.txt" 2>/dev/null)"
109
+ say "YaRN: SCALE=$SCALE ORIG=$ORIG -> $((SCALE*ORIG)) == CP_T=$T OK"
110
+ say "WSD: warmup $WARM, stable $STABLE, tail $TAIL -> plateau at 1.8e-4 throughout"
111
+ say "evals: $EVAL_A (mid-run health), $EVAL_B (the failed run cliffed at its step 1199)"
112
+ say "disk: ${FREEG} GB free"
113
+
114
+ ARCH="CP_N_EMBD=1536 CP_N_LAYER=36 CP_N_HEAD=12 CP_N_KVHEAD=4 CP_FFN=4096 CP_WEIGHT_TYING=0"
115
+ BASE="CP_SIZE=$CPSZ CP_ATTN_MODE=ring CP_ATTN_FUSION=split CP_T=$T \
116
+ YARN_SCALE=$SCALE YARN_ORIG_MAXPOS=$ORIG \
117
+ CP_ZERO=1 CP_ZERO_V2=1 CP_ZERO_SYMMETRIC=1 CP_BF16=1 CP_OPTIMIZER=muon \
118
+ CP_WARMUP=$WARM CP_REWARMUP=$WARM CP_REWARMUP_PEAK=0.3 \
119
+ CP_STABLE=$STABLE CP_CKPT=1 CP_CKPT_FREQ=$FREQ CP_CKPT_KEEP=$KEEP"
120
+
121
+ # ------------------------------------------------------------- upload helper --
122
+ # A missing source is a LOUD failure and every upload is confirmed by the byte
123
+ # size HF reports back. The old helper was `[ -e "$1" ] || return 0`, which
124
+ # returned SUCCESS when the file was absent -- that is how 17 GB of checkpoints
125
+ # were lost with nothing in the log.
126
+ up_verify(){ # src dst
127
+ local src=$1 dst=$2
128
+ [ -f "$src" ] || { say " MISSING SOURCE (NOT UPLOADED): $src"; return 1; }
129
+ $HF upload "$REPO" "$src" "$dst" >>"$LOG/upload.log" 2>&1 || { say " UPLOAD FAILED $dst"; return 1; }
130
+ $PY - "$REPO" "$src" "$dst" <<'PYEOF'
131
+ import sys, os
132
+ from huggingface_hub import HfApi
133
+ repo, loc, rem = sys.argv[1], sys.argv[2], sys.argv[3]
134
+ want = os.path.getsize(loc)
135
+ got = {f.rfilename: (f.size or 0) for f in HfApi().repo_info(repo, files_metadata=True).siblings}.get(rem)
136
+ print((" VERIFIED " if got == want else " *** MISMATCH/ABSENT ") + f"{rem} local={want:,} remote={got}")
137
+ sys.exit(0 if got == want else 1)
138
+ PYEOF
139
+ }
140
+
141
+ # ---------------------------------------------------------------- janitor ----
142
+ # Preserve rank_0 of EVERY save before CP_CKPT_KEEP can prune it, and upload on
143
+ # a coarser cadence (HF deletes do not reclaim LFS storage in this repo).
144
+ start_janitor(){
145
+ ( mkdir -p "$LOG/keep"
146
+ while sleep 45; do
147
+ RN=$(cat "$LOG/runnum" 2>/dev/null) || continue
148
+ for f in $CK/blumodelcp_run${RN}_step_*.ckpt; do
149
+ [ -e "$f" ] || continue
150
+ case "$f" in *keep_*) continue;; esac
151
+ S=$(basename "$f" | grep -oE 'step_[0-9]+' | cut -d_ -f2)
152
+ [ -n "$S" ] || continue
153
+ K="$CK/keep_step_${S}.ckpt"
154
+ if [ ! -f "$K" ]; then
155
+ cp "$f" "$K" 2>/dev/null && echo "$S" >> "$LOG/keep/preserved.txt"
156
+ if [ $(( S % UPLOAD_EVERY )) -eq 0 ]; then
157
+ up_verify "$K" "checkpoints/mix64k_step_${S}.ckpt" >>"$M" 2>&1 &
158
+ fi
159
+ fi
160
+ done
161
+ # drop orphaned sidecars: CP_CKPT_KEEP prunes .ckpt but never .zero_g*,
162
+ # the leak that filled a 193 GB disk on 2026-09-09
163
+ for s in checkpoints_bluscriptcp/rank_*/*.zero_g*; do
164
+ [ -e "$s" ] || continue
165
+ [ -f "${s%.zero_g*}.ckpt" ] || rm -f "$s"
166
+ done
167
+ done ) & JANITOR=$!
168
+ }
169
+ stop_janitor(){ kill "${JANITOR:-}" 2>/dev/null; wait "${JANITOR:-}" 2>/dev/null; }
170
+ trap 'stop_janitor' EXIT
171
+
172
+ # ------------------------------------------------------------ cliff detector --
173
+ # Verdict from the 50-step rolling mean vs the 200-700 baseline. The failed run:
174
+ # baseline ~2.87, then 2.642 (steps 1150-1199) and 2.185 (1200-1249). Per-step
175
+ # SD is ~0.16, so a 50-step mean has SE ~0.023 and a 0.20 drop is ~9 sigma --
176
+ # this cannot fire on noise.
177
+ detect_cliff(){ # prints: CLEAN|CLIFF <onset_step> <baseline> <current>
178
+ $PY - "$LOG/train.log" <<'PYEOF'
179
+ import re, sys, statistics as st
180
+ rows = [(int(a), float(b)) for a, b in
181
+ re.findall(r"^step +(\d+) \| loss: ([0-9.]+)", open(sys.argv[1], errors="ignore").read(), re.M)]
182
+ # NODATA, never CLEAN, when the baseline cannot be computed. Reporting "clean"
183
+ # because the evidence is missing is the failure direction that matters here --
184
+ # b4cont/train.log does exactly this, since it resumes at step 750 and has no
185
+ # steps in the 200-700 baseline window.
186
+ if len(rows) < 250:
187
+ print("NODATA 0 0 0"); raise SystemExit
188
+ d = dict(rows)
189
+ base = [d[s] for s in range(200, 701) if s in d]
190
+ if len(base) < 100:
191
+ print("NODATA 0 0 0"); raise SystemExit
192
+ B = st.mean(base)
193
+ THRESH = B - 0.20
194
+ onset = 0
195
+ for s in range(750, max(d) + 1):
196
+ w = [d[k] for k in range(s - 49, s + 1) if k in d]
197
+ if len(w) == 50 and st.mean(w) < THRESH:
198
+ onset = s; break
199
+ cur = st.mean([d[k] for k in range(max(d) - 49, max(d) + 1) if k in d] or [B])
200
+ print(f"{'CLIFF' if onset else 'CLEAN'} {onset} {B:.3f} {cur:.3f}")
201
+ PYEOF
202
+ }
203
+
204
+ # ------------------------------------------------------------------ probe ----
205
+ if [ ! -f "$LOG/probe.ok" ]; then
206
+ # grad_accum=1 means global_batch == B*T*dp_size, NOT T. At dp=2 the minimum
207
+ # micro-batch is 131,072 tokens; passing T alone dies in the divisibility
208
+ # guard (bluscriptCP.cpp:1462) before the first step, and MPI_ABORT eats the
209
+ # message so the log shows only "MPI_ABORT ... errorcode 1".
210
+ say "PHASE 1: 3-step probe ($(( T / CPSZ )) tok/rank, global_batch $(( T * NP / CPSZ )))"
211
+ env $ARCH $BASE CP_STABLE=0 CP_CKPT=0 \
212
+ CP_INIT_FROM="$PWD/$PARENT" CP_DATA_ROOT="$PWD/$DATA" \
213
+ CP_B=1 CP_GLOBAL_BATCH=$(( T * NP / CPSZ )) CP_MAX_STEPS=3 \
214
+ CUDA_VISIBLE_DEVICES=$GPUS mpirun --allow-run-as-root -np $NP "$EXE" \
215
+ > "$LOG/probe.log" 2>&1
216
+ RC=$?
217
+ # The probe must PROVE it trained. An earlier version only grepped for OOM,
218
+ # so a probe that aborted on every rank was called "PROBE OK" and the real
219
+ # run died 73 s later.
220
+ grep -qaiE 'out of memory|bad_alloc' "$LOG/probe.log" && { say "PROBE OOM at cp=$CPSZ"; exit 1; }
221
+ grep -qaE 'terminate called|std::runtime_error|Signal: Aborted' "$LOG/probe.log" && {
222
+ say "PROBE CRASHED:"; grep -aA1 'terminate called' "$LOG/probe.log" | grep -a 'what()' | head -1 >&2; exit 1; }
223
+ NSTEP=$(grep -acE '^step +[0-9]+ \|' "$LOG/probe.log")
224
+ PEAK=$(grep -aoE 'mem: [0-9]+MB' "$LOG/probe.log" | tail -1)
225
+ [ "$RC" -eq 0 ] && [ "$NSTEP" -ge 1 ] && [ -n "$PEAK" ] || {
226
+ say "PROBE FAILED (rc=$RC steps=$NSTEP peak='$PEAK')"; tail -15 "$LOG/probe.log" >&2; exit 1; }
227
+ say "PROBE OK: $NSTEP steps, peak $PEAK"
228
+ touch "$LOG/probe.ok"
229
+ fi
230
+
231
+ # ------------------------------------------------------------- run a segment --
232
+ run_seg(){ # $1 = target step
233
+ local TGT="$1"
234
+ [ -f "$LOG/seg_${TGT}.done" ] && { say "segment to $TGT already done"; return 0; }
235
+ local MODE
236
+ if [ -f "$LOG/runnum" ]; then
237
+ MODE="CP_CKPT_RESUME=$(cat "$LOG/runnum") CP_REWARMUP_ANCHOR=0"
238
+ say "resuming to step $TGT"
239
+ else
240
+ MODE="CP_INIT_FROM=$PWD/$PARENT CP_CKPT_NEW_RUN=1"
241
+ say "starting fresh from $(basename "$PARENT"), target step $TGT"
242
+ fi
243
+ ( for i in $(seq 1 6000); do
244
+ RN=$(cat "$LOG/runnum" 2>/dev/null || \
245
+ grep -aoE 'CP_Training_log[0-9]+' "$LOG/train.log" 2>/dev/null | tail -1 | grep -oE '[0-9]+')
246
+ [ -n "$RN" ] || { sleep 20; continue; }
247
+ echo "$RN" > "$LOG/runnum"
248
+ n=$(ls checkpoints_bluscriptcp/rank_*/blumodelcp_run${RN}_step_${TGT}.ckpt 2>/dev/null | wc -l)
249
+ if [ "$n" -ge "$NP" ]; then
250
+ sleep 5
251
+ for p in $(pgrep -f 'mpirun.*bluscriptCP_exec') $(pgrep -f 'build/bluscriptCP_exec'); do kill "$p" 2>/dev/null; done
252
+ exit 0
253
+ fi
254
+ sleep 20
255
+ done ) & WPID=$!
256
+ env $ARCH $BASE $MODE CP_DATA_ROOT="$PWD/$DATA" \
257
+ CP_B=1 CP_GLOBAL_BATCH=$GB CP_MAX_STEPS=$STEPS \
258
+ CUDA_VISIBLE_DEVICES=$GPUS mpirun --allow-run-as-root -np $NP "$EXE" \
259
+ >> "$LOG/train.log" 2>&1
260
+ kill $WPID 2>/dev/null; wait $WPID 2>/dev/null
261
+ grep -qaiE 'out of memory|bad_alloc' "$LOG/train.log" && die "OOM"
262
+ local RN; RN=$(cat "$LOG/runnum" 2>/dev/null) || die "no run number"
263
+ local SRC="blumodelcp_run${RN}_step_${TGT}.ckpt"
264
+ local n; n=$(ls checkpoints_bluscriptcp/rank_*/"$SRC" 2>/dev/null | wc -l)
265
+ [ "$n" -ge "$NP" ] || die "step $TGT checkpoint missing ($n/$NP ranks)"
266
+ local SAFE="mix64k${TGT}_${SRC}"
267
+ [ -f "$CK/$SAFE" ] || cp "$CK/$SRC" "$CK/$SAFE"
268
+ echo "$SAFE" > "$LOG/seg_${TGT}.ckpt"; touch "$LOG/seg_${TGT}.done"
269
+ say "step $TGT reached (run $RN), preserved as $SAFE, $(grep -aE '^step ' "$LOG/train.log" | tail -1 | grep -oE 'loss: [0-9.]+')"
270
+ up_verify "$CK/$SAFE" "checkpoints/mix64k_step_${TGT}.ckpt" >>"$M" 2>&1 &
271
+ }
272
+
273
+ # --------------------------------------------------------------- run an eval --
274
+ eval_at(){ # $1 = label $2 = checkpoint basename in rank_0
275
+ local TAG="$1" CKF="$2"
276
+ [ -f "$CK/$CKF" ] || { say "eval $TAG: $CKF not present, SKIP"; return 1; }
277
+ [ -f "$LOG/eval_${TAG}.done" ] && { say "eval $TAG already done"; return 0; }
278
+ say "EVAL $TAG -- 4 lengths, one GPU each ($CKF)"
279
+ for LEN in 8192 16360 32744 65512; do
280
+ ( GEN_DATA_ROOT=$DATA HAY="$HAY" PY=$PY OUT=mix64k_${TAG}_len_$LEN TASKS="niah_single" \
281
+ CKPTS="m$TAG" N=100 PROMPT_LEN=$LEN MAX_NEW=24 T=$T \
282
+ DEPTHS="0.0,0.25,0.5,0.75,1.0" YARN_ORIG=$ORIG YARN=$SCALE \
283
+ GPUS=$(( (LEN==8192)?0:(LEN==16360)?1:(LEN==32744)?2:3 )) JOBS_PER_GPU=1 \
284
+ BF16=1 GREEDY=1 CKPT_EXTRA="m$TAG=$CKF" ./run_longeval.sh \
285
+ >> "$LOG/eval_${TAG}_$LEN.log" 2>&1 ) &
286
+ done
287
+ wait
288
+ for LEN in 8192 16360 32744 65512; do
289
+ [ -s "mix64k_${TAG}_len_$LEN/summary.csv" ] \
290
+ && say " $TAG len $LEN -> $(sed -n 2p mix64k_${TAG}_len_$LEN/summary.csv)" \
291
+ || say " $TAG len $LEN -> NO RESULT"
292
+ done
293
+ touch "$LOG/eval_${TAG}.done"
294
+ }
295
+
296
+ # =============================== the experiment ==============================
297
+ start_janitor
298
+
299
+ run_seg "$EVAL_A" || die "segment to $EVAL_A failed"
300
+ eval_at "$EVAL_A" "$(cat "$LOG/seg_${EVAL_A}.ckpt")"
301
+ say "baseline for all of these = this run's own parent, measured: 67.0/65.0/52.0/29.0"
302
+
303
+ run_seg "$EVAL_B" || die "segment to $EVAL_B failed"
304
+ eval_at "$EVAL_B" "$(cat "$LOG/seg_${EVAL_B}.ckpt")"
305
+
306
+ read -r VERDICT ONSET BASE_L CUR_L <<<"$(detect_cliff)"
307
+ say "cliff detector: $VERDICT (onset=$ONSET baseline=$BASE_L current=$CUR_L)"
308
+
309
+ [ "$VERDICT" = "NODATA" ] && die "cliff detector has no baseline (needs steps 200-700 in $LOG/train.log). Refusing to call this run clean on missing evidence -- inspect the loss curve by hand."
310
+
311
+ if [ "$VERDICT" = "CLIFF" ]; then
312
+ # BRANCH A -- collapsed. Evaluate the last preserved checkpoint BEFORE onset,
313
+ # which is exactly what CP_CKPT_KEEP=2 destroyed last time.
314
+ # ONSET is the END of the first 50-step window under threshold, so the window
315
+ # opened at ONSET-49 and the last save known-clean is below that.
316
+ PRE=$(( ((ONSET - 49) / FREQ) * FREQ ))
317
+ say "BRANCH A: collapse detected at step $ONSET -- evaluating the pre-cliff checkpoint at $PRE"
318
+ if [ -f "$CK/keep_step_${PRE}.ckpt" ]; then
319
+ eval_at "pre${PRE}" "keep_step_${PRE}.ckpt"
320
+ up_verify "$CK/keep_step_${PRE}.ckpt" "checkpoints/mix64k_pre_step_${PRE}.ckpt" >>"$M" 2>&1
321
+ else
322
+ say " keep_step_${PRE}.ckpt missing; preserved steps: $(sort -n "$LOG/keep/preserved.txt" 2>/dev/null | tr '\n' ' ')"
323
+ fi
324
+ say "RESULT: the mix did NOT prevent the collapse. Data composition is not the (only) cause."
325
+ else
326
+ # BRANCH B -- clean at EVAL_B. Run out the mix and evaluate the endpoint.
327
+ say "BRANCH B: no cliff by $EVAL_B (the pure-b5 run was already at val 2.00 here) -- continuing to $STEPS"
328
+ run_seg "$STEPS" || die "segment to $STEPS failed"
329
+ eval_at "$STEPS" "$(cat "$LOG/seg_${STEPS}.ckpt")"
330
+ read -r V2 O2 B2 C2 <<<"$(detect_cliff)"
331
+ say "cliff detector after full pass: $V2 (onset=$O2 baseline=$B2 current=$C2)"
332
+ if [ "$V2" = "CLIFF" ]; then
333
+ PRE=$(( ((O2 - 49) / FREQ) * FREQ ))
334
+ say "collapse appeared later, at $O2 -- evaluating the pre-cliff checkpoint at $PRE"
335
+ [ -f "$CK/keep_step_${PRE}.ckpt" ] && eval_at "pre${PRE}" "keep_step_${PRE}.ckpt"
336
+ else
337
+ say "RESULT: a FULL pass on the mix with no cliff. The pure-b5 run collapsed at ~1150."
338
+ fi
339
+ fi
340
+
341
+ stop_janitor
342
+ say "=== preserved checkpoints: $(ls $CK/keep_step_*.ckpt 2>/dev/null | wc -l) ==="
343
+ say "=== DONE -- see $M; every upload above was byte-verified against HF ==="