Upload scripts/run_longeval.sh with huggingface_hub
Browse files- scripts/run_longeval.sh +349 -0
scripts/run_longeval.sh
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# =============================================================================
|
| 3 |
+
# run_longeval.sh -- batch RULER / NIAH / VAL generative evaluation for bluscriptCP.
|
| 4 |
+
#
|
| 5 |
+
# Scores what the model GENERATES (substring match), unlike probe_1b.sh which
|
| 6 |
+
# measures teacher-forced NLL over an answer span already present in the input.
|
| 7 |
+
#
|
| 8 |
+
# Requires a binary built with generation support:
|
| 9 |
+
# make CP_FUSED_ROPE=1 CP_ATTN_SPLIT=1 SM_ARCH=90 bluscript-cp -j$(nproc)
|
| 10 |
+
#
|
| 11 |
+
# Usage:
|
| 12 |
+
# ./run_longeval.sh # all tasks, all checkpoints
|
| 13 |
+
# TASKS="niah_single vt" ./run_longeval.sh # subset
|
| 14 |
+
# CKPTS="arm_c" N=20 ./run_longeval.sh # quick smoke
|
| 15 |
+
#
|
| 16 |
+
# Resumable: each (ckpt,task) writes a .done marker; re-running skips finished
|
| 17 |
+
# pairs. Kill it any time -- completed pairs survive.
|
| 18 |
+
# =============================================================================
|
| 19 |
+
set -uo pipefail
|
| 20 |
+
cd "$(dirname "$0")"
|
| 21 |
+
|
| 22 |
+
ROOT="$(cd ../.. && pwd)"
|
| 23 |
+
export LD_LIBRARY_PATH="$ROOT/Tensor-Implementations/lib:$ROOT/Profiler/lib:${LD_LIBRARY_PATH:-}"
|
| 24 |
+
export OMPI_ALLOW_RUN_AS_ROOT=1 OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1
|
| 25 |
+
|
| 26 |
+
EXE=./build/bluscriptCP_exec
|
| 27 |
+
PY=${PY:-python3}
|
| 28 |
+
OUT=${OUT:-longeval_runs}
|
| 29 |
+
CK=checkpoints_bluscriptcp/rank_0
|
| 30 |
+
|
| 31 |
+
# CP_T is the RoPE cache length. The C++ side pads each decode step up to a
|
| 32 |
+
# multiple of 256 (the Hopper split kernel's T_q guard), so
|
| 33 |
+
# pad_up(PROMPT_LEN + MAX_NEW) must be <= CP_T or it aborts up front.
|
| 34 |
+
T=${T:-16384}
|
| 35 |
+
# COST: a forward at T=16384 fp32 measures 17.8 s on H200 / ~25 s on H100
|
| 36 |
+
# (probe_arm_c: 21 jobs x 8 windows in 12.5 min at slots=4). Naive re-forward
|
| 37 |
+
# decoding costs ONE such forward per generated token, so the defaults matter:
|
| 38 |
+
# fp32 MAX_NEW=32 N=50 -> 190 GPU-h <- do not
|
| 39 |
+
# bf16, early stop, 4 tasks at ~6 forwards + niah_multivalue at ~24 (its
|
| 40 |
+
# MIN_TOK=14 blocks an early break) -> weighted (4*6+24)/5 = 9.6 /prompt
|
| 41 |
+
# -> ~18.7 GPU-h for 1000 prompts -> ~4.7 h on 4 GPUs. Step 2 measures this
|
| 42 |
+
# on BOTH niah_single and niah_multivalue rather than assuming it.
|
| 43 |
+
# HARD CONSTRAINT: PROMPT_LEN + MAX_NEW must be <= 16384 AND pad up to exactly it.
|
| 44 |
+
# bluscriptCP.cpp:2446 dies when pad_up(P_LEN+MAX_NEW) > CP_T, and PAD=256, so
|
| 45 |
+
# pad_up(16368+24)=16640 > 16384 would abort EVERY job. 16360+24 = 16384 exactly.
|
| 46 |
+
MAX_NEW=${MAX_NEW:-24}
|
| 47 |
+
PROMPT_LEN=${PROMPT_LEN:-16360}
|
| 48 |
+
N=${N:-50}
|
| 49 |
+
BF16=${BF16:-1}
|
| 50 |
+
GPUS=${GPUS:-0} # comma list, e.g. "0,1,2,3" -> (ckpt,task) jobs fan out
|
| 51 |
+
NP=${NP:-1} # CP_SIZE per job; GPUs are used for THROUGHPUT, not size
|
| 52 |
+
YARN=${YARN:-4}
|
| 53 |
+
# YARN_ORIG_MAXPOS (YARNOps.cpp:57) defaults to 1024, but the base model was
|
| 54 |
+
# trained at context_length 4096 -- hence the warning EVERY run emits:
|
| 55 |
+
# seq_len(16384) != scale(4.000)*orig_maxpos(1024.0)=4096.0
|
| 56 |
+
# i.e. the YaRN ramp targets 4096 while the cache is SIZED 16384, so positions
|
| 57 |
+
# past ~4k sit outside the range it was parameterised for. YARN_ORIG=4096 makes
|
| 58 |
+
# scale*orig == seq_len exactly. CAUTION: this is a train/eval MISMATCH -- the
|
| 59 |
+
# checkpoints were trained under the 1024 default, so a correctly-parameterised
|
| 60 |
+
# cache is not one they ever saw. Diagnostic, not a guaranteed fix.
|
| 61 |
+
YARN_ORIG=${YARN_ORIG:-}
|
| 62 |
+
GREEDY=${GREEDY:-1} # deterministic: benchmarks must be reproducible
|
| 63 |
+
|
| 64 |
+
# Needle depth grid. MUST match needle_gen.py's p_frac points or the generative
|
| 65 |
+
# curve cannot be overlaid on the NLL curve. Both place the needle at a fraction
|
| 66 |
+
# of ABSOLUTE POSITION (needle_gen p0.25 -> slot 4087 -> fraction 0.25), so the
|
| 67 |
+
# grids correspond directly.
|
| 68 |
+
DEPTHS=${DEPTHS:-"0.0,0.25,0.5,0.75,1.0"}
|
| 69 |
+
|
| 70 |
+
# Minimum substantive tokens before a newline may end generation. 3 suits a
|
| 71 |
+
# 7-digit number or a single word, but niah_multivalue needs THREE values
|
| 72 |
+
# (~12-15 tokens): at 3 a model that emits value 1 then a newline would stop with
|
| 73 |
+
# 1 of 3 and score MISS -- re-creating the bug the guard exists to prevent,
|
| 74 |
+
# precisely for the long-answer task.
|
| 75 |
+
declare -A MINTOK=( [niah_multivalue]=14 )
|
| 76 |
+
|
| 77 |
+
TASKS=${TASKS:-"niah_single niah_multikey niah_multivalue vt val_forward val_backward"}
|
| 78 |
+
# name:checkpoint-file
|
| 79 |
+
CKPTS=${CKPTS:-"base arm_a arm_b arm_c"}
|
| 80 |
+
declare -A CKPT_FILE=(
|
| 81 |
+
[base]=blumodelcp_run7_step_750.ckpt
|
| 82 |
+
[arm_a]=blumodelcp_run12_step_400.ckpt
|
| 83 |
+
[arm_b]=blumodelcp_run15_step_400.ckpt
|
| 84 |
+
[arm_c]=blumodelcp_run16_step_400.ckpt
|
| 85 |
+
)
|
| 86 |
+
# CKPT_EXTRA lets a caller register checkpoints the map does not know about --
|
| 87 |
+
# e.g. a fresh retrain whose run number is only decided at training time.
|
| 88 |
+
# Format: "key=filename key=filename" (filenames relative to $CK).
|
| 89 |
+
if [ -n "${CKPT_EXTRA:-}" ]; then
|
| 90 |
+
for kv in $CKPT_EXTRA; do CKPT_FILE[${kv%%=*}]="${kv#*=}"; done
|
| 91 |
+
fi
|
| 92 |
+
|
| 93 |
+
A1B="CP_N_EMBD=1536 CP_N_LAYER=36 CP_N_HEAD=12 CP_N_KVHEAD=4 CP_FFN=4096 CP_WEIGHT_TYING=0"
|
| 94 |
+
ARCH="$A1B CP_ATTN_MODE=ring CP_ATTN_FUSION=split"
|
| 95 |
+
|
| 96 |
+
mkdir -p "$OUT"/{prompts,gen,state,scores}
|
| 97 |
+
M="$OUT/master.log"
|
| 98 |
+
say() { echo "[$(date -u +%H:%M:%S)] $*" | tee -a "$M"; }
|
| 99 |
+
|
| 100 |
+
[ -x "$EXE" ] || { echo "ERROR: $EXE missing -- build first:"; \
|
| 101 |
+
echo " make CP_FUSED_ROPE=1 CP_ATTN_SPLIT=1 SM_ARCH=90 bluscript-cp -j\$(nproc)"; exit 1; }
|
| 102 |
+
|
| 103 |
+
# Fail early on a binary built without generation support rather than after an
|
| 104 |
+
# hour of prompt generation.
|
| 105 |
+
if ! grep -q CP_GENERATE "$EXE" 2>/dev/null; then
|
| 106 |
+
echo "ERROR: $EXE has no CP_GENERATE support (stale build). Rebuild:"
|
| 107 |
+
echo " make CP_FUSED_ROPE=1 CP_ATTN_SPLIT=1 SM_ARCH=90 bluscript-cp -j\$(nproc)"
|
| 108 |
+
exit 1
|
| 109 |
+
fi
|
| 110 |
+
|
| 111 |
+
say "longeval: T=$T prompt_len=$PROMPT_LEN max_new=$MAX_NEW n=$N greedy=$GREEDY gpus=$GPUS"
|
| 112 |
+
|
| 113 |
+
# ---- 1. prompts (shared across checkpoints; identical bytes => fair compare) --
|
| 114 |
+
HAY=$(ls Data_Loader/Data/flux_val_*.bin 2>/dev/null | head -1)
|
| 115 |
+
[ -z "$HAY" ] && HAY=$(ls Data_Loader/Data/flux_train_*.bin 2>/dev/null | head -1)
|
| 116 |
+
say "haystack: ${HAY:-<synthetic fallback>}"
|
| 117 |
+
[ -z "$HAY" ] && say " WARN: synthetic filler is a weaker distractor than real text -- it INFLATES accuracy. Record this."
|
| 118 |
+
|
| 119 |
+
# A bare "-f the .bin exists" test silently reuses prompts from a different run:
|
| 120 |
+
# a smoke run at N=2 then poisons a main run at N=50 (the C++ dies on "short read
|
| 121 |
+
# on prompt 2" AFTER flushing 2 rows, which the old [ -s ] scoring gate happily
|
| 122 |
+
# scored and marked .done, permanently). The signature covers everything that
|
| 123 |
+
# changes prompt content -- N, length, depths, seed, AND the haystack, so a
|
| 124 |
+
# synthetic-filler run is not reused once the flux shard arrives.
|
| 125 |
+
SIG="N=$N|LEN=$PROMPT_LEN|DEPTHS=$DEPTHS|SEED=0|HAY=$(basename "${HAY:-none}")|HAYSZ=$(stat -c %s "${HAY:-/nonexistent}" 2>/dev/null || echo 0)"
|
| 126 |
+
if [ "$(cat "$OUT/prompts/.signature" 2>/dev/null)" != "$SIG" ]; then
|
| 127 |
+
say "prompt signature changed -- regenerating prompts and clearing state"
|
| 128 |
+
rm -f "$OUT"/prompts/*_prompts.bin "$OUT"/prompts/*_meta.json
|
| 129 |
+
# MUST clear state too: the generation skip below tests .gen, so leaving those
|
| 130 |
+
# markers would skip regeneration and score OLD tokens against the NEW
|
| 131 |
+
# meta.json -- attributing accuracy to depths the tokens never came from.
|
| 132 |
+
rm -f "$OUT"/state/*
|
| 133 |
+
fi
|
| 134 |
+
|
| 135 |
+
GEN_OK=1
|
| 136 |
+
for TASK in $TASKS; do
|
| 137 |
+
if [ ! -f "$OUT/prompts/${TASK}_prompts.bin" ]; then
|
| 138 |
+
say "generating prompts: $TASK"
|
| 139 |
+
$PY Tests/bluscriptcp/longeval_gen.py --task "$TASK" --prompt-len "$PROMPT_LEN" \
|
| 140 |
+
--n "$N" --seed 0 --depths "$DEPTHS" ${HAY:+--haystack "$HAY"} \
|
| 141 |
+
--out-dir "$OUT/prompts" \
|
| 142 |
+
>>"$M" 2>&1 || { say " FAILED to generate $TASK"; GEN_OK=0; continue; }
|
| 143 |
+
fi
|
| 144 |
+
# exact stride: the C++ reader slices the stream at N x PROMPT_LEN x 4 bytes
|
| 145 |
+
WANT=$(( N * PROMPT_LEN * 4 ))
|
| 146 |
+
GOT=$(stat -c %s "$OUT/prompts/${TASK}_prompts.bin" 2>/dev/null || echo 0)
|
| 147 |
+
[ "$GOT" = "$WANT" ] || { say " BAD SIZE $TASK: ${GOT}B want ${WANT}B"; GEN_OK=0; }
|
| 148 |
+
done
|
| 149 |
+
# Only claim the signature if every task really produced prompts, else the next
|
| 150 |
+
# run would match it and never retry the missing ones.
|
| 151 |
+
[ "$GEN_OK" = 1 ] && echo "$SIG" > "$OUT/prompts/.signature"
|
| 152 |
+
|
| 153 |
+
# ---- 2. generate (fanned out over GPUs) -------------------------------------
|
| 154 |
+
# Each (checkpoint, task) pair is one independent job needing NP GPUs, so the
|
| 155 |
+
# GPU list is used for THROUGHPUT: slots = nGPU / NP jobs run concurrently.
|
| 156 |
+
# 24 pairs (4 ckpts x 6 tasks) on 8 GPUs at NP=1 is 8-way parallel.
|
| 157 |
+
IFS=',' read -r -a GPUARR <<< "$GPUS"
|
| 158 |
+
NGPU=${#GPUARR[@]}
|
| 159 |
+
# One job needs ~24.3 GB (measured) of an 80 GB card and drives it to only ~42%
|
| 160 |
+
# util -- B=1 decoding of a 1.06B model cannot saturate an H100, and memory is
|
| 161 |
+
# nowhere near the limit. So oversubscribe: JOBS_PER_GPU>1 runs several jobs per
|
| 162 |
+
# card and buys ~1.8x at 2/GPU. 3/GPU is ~73 GB of 81 -- it fits but leaves
|
| 163 |
+
# little headroom, so it is opt-in rather than the default.
|
| 164 |
+
JOBS_PER_GPU=${JOBS_PER_GPU:-1}
|
| 165 |
+
SLOTS=$(( (NGPU / NP) * JOBS_PER_GPU )); [ "$SLOTS" -lt 1 ] && SLOTS=1
|
| 166 |
+
say "fan-out: ${NGPU} GPU(s), NP=$NP -> $SLOTS concurrent job(s)"
|
| 167 |
+
|
| 168 |
+
jobs=()
|
| 169 |
+
for NAME in $CKPTS; do
|
| 170 |
+
F="${CKPT_FILE[$NAME]:-}"
|
| 171 |
+
[ -n "$F" ] || { say "unknown checkpoint key: $NAME"; continue; }
|
| 172 |
+
[ -f "$CK/$F" ] || { say "SKIP $NAME: $CK/$F not present"; continue; }
|
| 173 |
+
for TASK in $TASKS; do
|
| 174 |
+
# Skip on .gen, not .done: .done is written only by the scoring loop, which
|
| 175 |
+
# runs after `wait` for ALL generation, so a teardown mid-run would otherwise
|
| 176 |
+
# lose every completed pair. Safe because a signature change clears state/.
|
| 177 |
+
[ -f "$OUT/state/${NAME}.${TASK}.gen" ] && { say " $NAME/$TASK already generated"; continue; }
|
| 178 |
+
[ -f "$OUT/prompts/${TASK}_prompts.bin" ] || { say " SKIP $NAME/$TASK: no prompts"; continue; }
|
| 179 |
+
jobs+=("$NAME:$TASK")
|
| 180 |
+
done
|
| 181 |
+
done
|
| 182 |
+
say "${#jobs[@]} job(s) to run"
|
| 183 |
+
|
| 184 |
+
running=0; g=0
|
| 185 |
+
for J in "${jobs[@]:-}"; do
|
| 186 |
+
[ -z "$J" ] && continue
|
| 187 |
+
NAME=${J%%:*}; TASK=${J##*:}
|
| 188 |
+
F="${CKPT_FILE[$NAME]}"
|
| 189 |
+
PB="$OUT/prompts/${TASK}_prompts.bin"
|
| 190 |
+
G="$OUT/gen/${NAME}_${TASK}.csv"
|
| 191 |
+
# g cycles over SLOTS, which now EXCEEDS the GPU count when JOBS_PER_GPU>1,
|
| 192 |
+
# so the device offset must wrap modulo NGPU -- otherwise slots beyond the
|
| 193 |
+
# last GPU would compute an empty CUDA_VISIBLE_DEVICES and every job there
|
| 194 |
+
# would silently land on device 0.
|
| 195 |
+
dev_off=$(( (g * NP) % NGPU ))
|
| 196 |
+
devs=$(IFS=,; echo "${GPUARR[*]:$dev_off:$NP}")
|
| 197 |
+
say " [gpu $devs] $NAME / $TASK"
|
| 198 |
+
(
|
| 199 |
+
env $ARCH CP_SIZE=$NP CP_T=$T CP_B=1 YARN_SCALE=$YARN \
|
| 200 |
+
${BF16:+CP_BF16=$BF16} \
|
| 201 |
+
${YARN_ORIG:+YARN_ORIG_MAXPOS=$YARN_ORIG} \
|
| 202 |
+
CP_INIT_FROM="$PWD/$CK/$F" \
|
| 203 |
+
CP_GENERATE=1 CP_GEN_TOKENS_BIN="$PWD/$PB" \
|
| 204 |
+
CP_GEN_PROMPT_LEN=$PROMPT_LEN CP_GEN_N=$N CP_GEN_MAX_NEW=$MAX_NEW \
|
| 205 |
+
CP_GEN_GREEDY=$GREEDY CP_GEN_OUT="$PWD/$G" \
|
| 206 |
+
CP_GEN_MIN_TOKENS=${MINTOK[$TASK]:-3} \
|
| 207 |
+
CUDA_VISIBLE_DEVICES=$devs \
|
| 208 |
+
mpirun --allow-run-as-root -np $NP "$EXE" \
|
| 209 |
+
> "$OUT/gen/${NAME}_${TASK}.log" 2>&1 \
|
| 210 |
+
&& [ -s "$G" ] && touch "$OUT/state/${NAME}.${TASK}.gen"
|
| 211 |
+
) &
|
| 212 |
+
g=$(( (g + 1) % SLOTS )); running=$(( running + 1 ))
|
| 213 |
+
if [ "$running" -ge "$SLOTS" ]; then wait -n; running=$(( running - 1 )); fi
|
| 214 |
+
done
|
| 215 |
+
wait
|
| 216 |
+
|
| 217 |
+
# ---- 2b. score (cheap, CPU-only, after all generation) ----------------------
|
| 218 |
+
for NAME in $CKPTS; do
|
| 219 |
+
for TASK in $TASKS; do
|
| 220 |
+
G="$OUT/gen/${NAME}_${TASK}.csv"
|
| 221 |
+
MK="$OUT/state/${NAME}.${TASK}.done"
|
| 222 |
+
[ -f "$MK" ] && continue
|
| 223 |
+
if [ ! -s "$G" ]; then
|
| 224 |
+
[ -f "$OUT/gen/${NAME}_${TASK}.log" ] && \
|
| 225 |
+
say " NO OUTPUT $NAME/$TASK -- see $OUT/gen/${NAME}_${TASK}.log"
|
| 226 |
+
continue
|
| 227 |
+
fi
|
| 228 |
+
# A non-empty file is NOT a complete one. If the C++ died partway (e.g. a
|
| 229 |
+
# short read on a stale prompt file) it still flushed the rows it managed,
|
| 230 |
+
# and scoring those would report an accuracy over a handful of prompts and
|
| 231 |
+
# then mark the pair .done. Note: `$(grep -c . f || echo 0)` is WRONG --
|
| 232 |
+
# grep -c prints 0 AND exits 1, so the substitution yields $'0\n0' and
|
| 233 |
+
# [ -eq ] throws. stat above is fine; it prints nothing on failure.
|
| 234 |
+
ROWS=$(grep -c . "$G" 2>/dev/null); ROWS=${ROWS:-0}
|
| 235 |
+
if [ "$ROWS" -ne "$N" ]; then
|
| 236 |
+
say " INCOMPLETE $NAME/$TASK ($ROWS/$N rows) -- not scoring, not marking done"
|
| 237 |
+
continue
|
| 238 |
+
fi
|
| 239 |
+
say " scoring $NAME / $TASK"
|
| 240 |
+
$PY Tests/bluscriptcp/longeval_score.py --gen "$G" \
|
| 241 |
+
--meta "$OUT/prompts/${TASK}_meta.json" \
|
| 242 |
+
--out "$OUT/scores/${NAME}_${TASK}.csv" --show 2 | tee -a "$M"
|
| 243 |
+
touch "$MK"
|
| 244 |
+
done
|
| 245 |
+
done
|
| 246 |
+
|
| 247 |
+
# ---- 3. summary -------------------------------------------------------------
|
| 248 |
+
say "building summary"
|
| 249 |
+
# TASKS must come through argv: the heredoc is QUOTED (<<'PY'), so $TASKS would
|
| 250 |
+
# never expand inside it.
|
| 251 |
+
$PY - "$OUT" "$TASKS" <<'PY' | tee -a "$M"
|
| 252 |
+
import csv, math, os, sys, collections
|
| 253 |
+
out, tasks = sys.argv[1], sys.argv[2].split()
|
| 254 |
+
d = os.path.join(out, "scores")
|
| 255 |
+
|
| 256 |
+
# Filenames are <model>_<task>.csv and BOTH halves contain underscores, so
|
| 257 |
+
# fn.split("_", 1) mis-parses every arm ("arm_b_niah_single" -> "arm").
|
| 258 |
+
# Match the known task list from the tail instead; for/else skips non-matches so
|
| 259 |
+
# `name` cannot silently carry over from the previous iteration.
|
| 260 |
+
per_task = collections.defaultdict(dict) # task -> model -> (hits, n)
|
| 261 |
+
per_depth = collections.defaultdict(dict) # depth -> model -> (hits, n)
|
| 262 |
+
per_task_depth = collections.defaultdict(dict) # (task,depth) -> model -> (hits,n)
|
| 263 |
+
for fn in sorted(os.listdir(d)) if os.path.isdir(d) else []:
|
| 264 |
+
if not fn.endswith(".csv"):
|
| 265 |
+
continue
|
| 266 |
+
for task in tasks:
|
| 267 |
+
if fn.endswith("_" + task + ".csv"):
|
| 268 |
+
name = fn[:-(len(task) + 5)]
|
| 269 |
+
break
|
| 270 |
+
else:
|
| 271 |
+
continue
|
| 272 |
+
rows = list(csv.DictReader(open(os.path.join(d, fn))))
|
| 273 |
+
if not rows:
|
| 274 |
+
continue
|
| 275 |
+
per_task[task][name] = (sum(int(r["ok"]) for r in rows), len(rows))
|
| 276 |
+
for r in rows:
|
| 277 |
+
dep = float(r["depth"])
|
| 278 |
+
h, n = per_task_depth[(task, dep)].get(name, (0, 0))
|
| 279 |
+
per_task_depth[(task, dep)][name] = (h + int(r["ok"]), n + 1)
|
| 280 |
+
|
| 281 |
+
if not per_task:
|
| 282 |
+
print(" no scores yet"); raise SystemExit
|
| 283 |
+
|
| 284 |
+
models = sorted({m for t in per_task.values() for m in t})
|
| 285 |
+
pct = lambda hn: 100.0 * hn[0] / hn[1] if hn[1] else float("nan")
|
| 286 |
+
|
| 287 |
+
print("\n=== per-task accuracy % (secondary/descriptive: ~10 per depth cell) ===")
|
| 288 |
+
print(" " + "task".ljust(18) + "".join(m.rjust(9) for m in models))
|
| 289 |
+
for task in sorted(per_task):
|
| 290 |
+
print(" " + task.ljust(18) + "".join(
|
| 291 |
+
(f"{pct(per_task[task][m]):8.1f}" if m in per_task[task] else " -").rjust(9)
|
| 292 |
+
for m in models))
|
| 293 |
+
|
| 294 |
+
# Degenerate-task exclusion, pre-registered: a task at <=1% overall for EVERY
|
| 295 |
+
# model adds n without information and would shrink the pooled CI spuriously.
|
| 296 |
+
degenerate = {t for t in per_task
|
| 297 |
+
if all(pct(per_task[t][m]) <= 1.0 for m in per_task[t])}
|
| 298 |
+
if degenerate:
|
| 299 |
+
print(f"\n degenerate (<=1% for every model): {sorted(degenerate)}")
|
| 300 |
+
# If EVERY task is degenerate the exclusion would empty the primary table and
|
| 301 |
+
# report nothing at all -- a floor effect is a result, not an absence of one.
|
| 302 |
+
# Fall back to pooling everything and say so, rather than printing a blank table.
|
| 303 |
+
if len(degenerate) == len(per_task):
|
| 304 |
+
print(" ALL tasks degenerate -> FLOOR EFFECT. Pooling everything anyway;")
|
| 305 |
+
print(" differences between models are untestable at this setting (no power).")
|
| 306 |
+
degenerate = set()
|
| 307 |
+
|
| 308 |
+
for (task, dep), byname in per_task_depth.items():
|
| 309 |
+
if task in degenerate:
|
| 310 |
+
continue
|
| 311 |
+
for name, (h, n) in byname.items():
|
| 312 |
+
ph, pn = per_depth[dep].get(name, (0, 0))
|
| 313 |
+
per_depth[dep][name] = (ph + h, pn + n)
|
| 314 |
+
|
| 315 |
+
def wilson(h, n):
|
| 316 |
+
"""95% Wilson half-width in points -- honest near 0, unlike the normal approx."""
|
| 317 |
+
if not n:
|
| 318 |
+
return float("nan")
|
| 319 |
+
z, p = 1.96, h / n
|
| 320 |
+
return 100.0 * z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / (1 + z * z / n)
|
| 321 |
+
|
| 322 |
+
print("\n=== PRIMARY: accuracy % pooled by depth across tasks (+-95% Wilson) ===")
|
| 323 |
+
print(" " + "depth".ljust(8) + "".join(m.rjust(16) for m in models))
|
| 324 |
+
for dep in sorted(per_depth):
|
| 325 |
+
cells = []
|
| 326 |
+
for m in models:
|
| 327 |
+
if m in per_depth[dep]:
|
| 328 |
+
h, n = per_depth[dep][m]
|
| 329 |
+
cells.append(f"{pct((h,n)):6.1f}+-{wilson(h,n):4.1f}".rjust(16))
|
| 330 |
+
else:
|
| 331 |
+
cells.append("-".rjust(16))
|
| 332 |
+
print(" " + f"{dep:<8.2f}" + "".join(cells))
|
| 333 |
+
|
| 334 |
+
with open(os.path.join(out, "summary.csv"), "w", newline="") as f:
|
| 335 |
+
w = csv.writer(f); w.writerow(["task"] + models)
|
| 336 |
+
for task in sorted(per_task):
|
| 337 |
+
w.writerow([task] + [f"{pct(per_task[task][m]):.1f}" if m in per_task[task] else ""
|
| 338 |
+
for m in models])
|
| 339 |
+
with open(os.path.join(out, "depth_summary.csv"), "w", newline="") as f:
|
| 340 |
+
w = csv.writer(f); w.writerow(["depth", "model", "hits", "n", "pct", "wilson95"])
|
| 341 |
+
for dep in sorted(per_depth):
|
| 342 |
+
for m in models:
|
| 343 |
+
if m in per_depth[dep]:
|
| 344 |
+
h, n = per_depth[dep][m]
|
| 345 |
+
w.writerow([dep, m, h, n, f"{pct((h,n)):.1f}", f"{wilson(h,n):.1f}"])
|
| 346 |
+
print(f"\n wrote {out}/summary.csv and {out}/depth_summary.csv")
|
| 347 |
+
PY
|
| 348 |
+
|
| 349 |
+
say "DONE -> $OUT/summary.csv"
|