File size: 6,960 Bytes
cf65d18 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | #!/usr/bin/env bash
# =============================================================================
# run_peer_eval.sh -- NIAH on peer base checkpoints, scored identically to ours.
#
# WHAT THIS ANSWERS
# Our 1.06B scores 48% on niah_single at 16k. Every published comparison is
# either the wrong metric (RULER 13-task average, not single-NIAH), the wrong
# checkpoint type (instruct, not base), or the wrong size. Running the peers
# through OUR harness removes all three mismatches at once.
#
# MODELS (all BASE checkpoints)
# Qwen3-0.6B/1.7B/4B/8B-Base dense attention, native 32,768 ctx, no YaRN.
# The ladder gives a measured retrieval-vs-scale
# curve without training anything. Qwen3-1.7B is
# the closest peer to our 1.06B (1.41B non-emb
# vs our 0.906B); Qwen3-4B matches our 4B target.
# Spark-X2.5-4B-Base hybrid: 27 sliding-window(512) + 9 full-attention
# layers, claims NATIVE 1M context, publishes no
# NIAH/RULER number at all. custom_code, so vLLM
# cannot load it -- peer_run.py falls back to hf.
#
# FAIRNESS NOTES, all of which belong in any writeup
# * One shared haystack TEXT; each model cut to its own exact token count.
# * Qwen3/Spark run INSIDE their native windows at 16k. Ours is YaRN-extended
# from 4k and at 16k sits at the very edge. Not the same condition.
# * Peers use a KV cache; our CP_GENERATE re-runs a full forward per token.
# That is an engine difference, not a capability difference.
#
# usage: ./run_peer_eval.sh # all models, all lengths
# MODELS=qwen3-4b LENS=16384 ./run_peer_eval.sh
# =============================================================================
set -uo pipefail
cd "$(dirname "$0")"
PY=${PY:-python3}
ROOT=${ROOT:-$PWD/peer_out}
HAY=${HAY:-$ROOT/haystack.txt}
LENS=${LENS:-"1024 2048 4096 8192 16384"}
N=${N:-100}
TASKS=${TASKS:-"niah_single val_backward"}
MAX_NEW=${MAX_NEW:-24}
DEPTHS=${DEPTHS:-"0.0,0.25,0.5,0.75,1.0"}
GPUS=${GPUS:-"0 1 2 3 4 5 6 7"}
MODELS=${MODELS:-"qwen3-0.6b qwen3-1.7b qwen3-4b qwen3-8b spark-4b"}
# Spark-X2.5 needs transformers 4.57.x: modeling_spark.py targets 4.57 and breaks
# on 5.x twice over -- _tied_weights_keys became a mapping, and create_causal_mask
# lost its `input_embeds` kwarg. Rather than shim each API change, give it the
# version it was written for. Qwen3 stays on the vLLM venv.
PY_SPARK=${PY_SPARK:-/home/blubridge/venvs/tf457/bin/python}
declare -A PYFOR=( [spark-4b]="$PY_SPARK" )
declare -A REPO=(
[qwen3-0.6b]=Qwen/Qwen3-0.6B-Base
[qwen3-1.7b]=Qwen/Qwen3-1.7B-Base
[qwen3-4b]=Qwen/Qwen3-4B-Base
[qwen3-8b]=Qwen/Qwen3-8B-Base
[spark-4b]=XHToken/Spark-X2.5-4B-Base
)
mkdir -p "$ROOT"
M="$ROOT/master.log"
say(){ echo "[$(date -u +%H:%M:%S)] $*" | tee -a "$M" >&2; }
say "=== peer NIAH eval ==="
for m in $MODELS; do [ -n "${REPO[$m]:-}" ] || { say "unknown model key: $m"; exit 1; }; done
# ---- 1. one shared haystack ------------------------------------------------
if [ ! -s "$HAY" ]; then
say "building shared haystack"
$PY make_haystack.py --out "$HAY" --tokens 4000000 >>"$M" 2>&1 \
|| { say "FATAL: haystack build failed"; exit 1; }
fi
say "haystack $(stat -Lc%s "$HAY") bytes"
# ---- 2. prompts, per model (tokenizer differs) and per length --------------
for m in $MODELS; do
for L in $LENS; do
d="$ROOT/prompts/$m/$L"
# Must check EVERY task, not just the first: an interrupt between tasks would
# otherwise leave this dir looking complete and the generation step would fail
# on the missing prompts file.
have=1; for t in $TASKS; do [ -f "$d/${t}_meta.json" ] || have=0; done
[ "$have" = 1 ] && { say "prompts exist $m/$L"; continue; }
mkdir -p "$d"
for t in $TASKS; do
$PY peer_gen.py --tokenizer "${REPO[$m]}" --haystack "$HAY" --task "$t" \
--prompt-len "$L" --n "$N" --depths "$DEPTHS" --seed 0 --out-dir "$d" \
>>"$M" 2>&1 || { say "FATAL: prompt gen failed $m/$L/$t"; exit 1; }
done
say "prompts $m/$L"
done
done
# ---- 3. generate: a real work queue, not round-robin ----------------------
# The previous version picked the GPU by LAUNCH INDEX (i % NG). `wait -n` kept
# NG jobs in flight, but the card was chosen by counter rather than by which one
# was actually free -- and a 16,384-token job takes ~16x a 1,024-token one, so
# the counter desynchronised from real occupancy within seconds and left half
# the box idle. Now: one persistent worker per GPU, each pulling the next job
# from a shared cursor under flock, so a card is never idle while work remains.
#
# Jobs are ordered LONGEST FIRST (16384 -> 1024). Longest-processing-time-first
# is the standard greedy makespan heuristic; scheduling the big jobs last would
# end the run with one 16k job while seven GPUs sit idle.
JOBS="$ROOT/joblist.txt"; : > "$JOBS"
for L in $(echo $LENS | tr ' ' '\n' | sort -rn); do
for m in $MODELS; do
for t in $TASKS; do
out="$ROOT/gen/$L/${m}_${t}.jsonl"
# require the FULL row count: a job killed mid-write would otherwise look
# done to a `-s` test and silently truncate that cell.
if [ -f "$out" ] && [ "$(wc -l < "$out")" -eq "$N" ]; then continue; fi
mkdir -p "$(dirname "$out")"
echo "$m|$L|$t" >> "$JOBS"
done
done
done
NJOB=$(grep -c . "$JOBS" 2>/dev/null || echo 0)
say "queue: $NJOB job(s) to run on $(echo $GPUS | wc -w) GPU(s), longest first"
if [ "$NJOB" -gt 0 ]; then
CUR="$ROOT/.cursor"; echo 0 > "$CUR"
worker(){
local g="$1" idx line m L t
while :; do
idx=$(flock "$CUR" -c 'n=$(cat '"$CUR"'); echo $((n+1)) > '"$CUR"'; echo $n')
line=$(sed -n "$((idx+1))p" "$JOBS")
[ -n "$line" ] || break
m=${line%%|*}; rest=${line#*|}; L=${rest%%|*}; t=${rest##*|}
say " [gpu $g] $m / $L / $t"
CUDA_VISIBLE_DEVICES=$g "${PYFOR[$m]:-$PY}" peer_run.py --model "${REPO[$m]}" \
--prompts "$ROOT/prompts/$m/$L/${t}_prompts.jsonl" \
--out "$ROOT/gen/$L/${m}_${t}.jsonl" --max-new "$MAX_NEW" \
> "$ROOT/gen/$L/${m}_${t}.log" 2>&1 \
|| say " FAIL [gpu $g] $m/$L/$t (see $ROOT/gen/$L/${m}_${t}.log)"
done
}
for g in $GPUS; do worker "$g" & done
wait
fi
say "generation done"
# ---- 4. score, one summary per length --------------------------------------
CSV=$(echo $MODELS | tr ' ' ','); TSV=$(echo $TASKS | tr ' ' ',')
for L in $LENS; do
say "scoring $L"
$PY peer_score.py --gen-dir "$ROOT/gen/$L" --meta-dir "$ROOT/prompts" \
--out-dir "$ROOT/results/$L" --length "$L" --models "$CSV" --tasks "$TSV" \
>>"$M" 2>&1 || say " WARN: scoring failed at $L"
[ -f "$ROOT/results/$L/summary.csv" ] && say " -> $(sed -n 2p "$ROOT/results/$L/summary.csv")"
done
say "=== PEER EVAL DONE -> $ROOT/results ==="
|