File size: 12,833 Bytes
e9bd690
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/env bash
# =============================================================================
# needle_sweep.sh -- run the retrieval probe across an extension-factor row set
# for ONE model, then score it. Forward-only, cache-only (no fine-tuning).
#
# Reuses the existing CP_EVAL_PPL + CP_EVAL_TOKENS_BIN path unchanged: the .bin
# holds synthetic trials, and per-position mean_nll in the CSV gives retrieval
# NLL at the answer span. See needle_gen.py for the fixed-slot construction.
#
# GROUP-SLOT FAN-OUT: slots = NGPU / CP_SIZE, so EVERY row parallelises --
#   T<=128k -> CP=1 -> 8 concurrent   |  256k -> CP=2 -> 4   |  512k -> CP=4 -> 2
# Each group is pinned to a contiguous device subset so cudaSetDevice(rank) binds
# rank->GPU correctly. Concurrency changes only WHEN work happens, never what is
# computed (independent processes, separate CSVs), so no determinism argument is
# needed -- unlike the search's EvaluatorPool.
#
# RESUMABLE: one marker per (T, cache, variant, block) in $STATE_DIR.
#
# Launch (114M 4k base, NTK, all five factors):
#   MODEL_KEY=114m_run1 CKPT_RUN=1 \
#   MODEL_ARCH="CP_N_EMBD=768 CP_N_LAYER=12 CP_N_HEAD=12 CP_N_KVHEAD=4 CP_FFN=2048 CP_WEIGHT_TYING=1" \
#   FACTORS="8 16 32" CACHES="ntk" \
#     nohup bash Tests/bluscriptcp/needle_sweep.sh > needle_114m.log 2>&1 &
#
# The single searched cell (48M@16x) is a separate invocation:
#   MODEL_KEY=48m_run200 CKPT_RUN=200 MODEL_ARCH="..." FACTORS="16" \
#   CACHES="searched" SEARCHED_LAMBDA=longrope_best_48m_s16_repr.txt bash ...
# =============================================================================
set -euo pipefail
cd "$(dirname "$0")/../.."   # -> repo root

MODEL_KEY=${MODEL_KEY:?set MODEL_KEY (label for the results rows, e.g. 114m_run1)}
# INIT_FROM=<path/to/base.ckpt> loads WEIGHTS ONLY instead of resuming a run.
# Needed whenever the checkpoint's optimizer state cannot be read back in the
# EVAL configuration: a ZeRO/Muon-trained base evaluated forward-only builds a
# plain nn::AdamW, which would try to parse ShardedMuon's state and fail. Also
# the only way to evaluate a base trained at a different world size, since ZeRO
# state is sharded to the size that wrote it. A forward-only probe needs no
# optimizer state at all, so this is the correct loader for it.
INIT_FROM=${INIT_FROM:-}
if [ -n "$INIT_FROM" ]; then
  [ -f "$INIT_FROM" ] || { echo "INIT_FROM: no such file: $INIT_FROM" >&2; exit 1; }
  CKPT_RUN=${CKPT_RUN:-0}
  CKPT_ENV="CP_INIT_FROM=$INIT_FROM"
else
  CKPT_RUN=${CKPT_RUN:?set CKPT_RUN (or INIT_FROM=<path>)}
  CKPT_ENV="CP_CKPT_RESUME=$CKPT_RUN"
fi
MODEL_ARCH=${MODEL_ARCH:?set MODEL_ARCH (must match how CKPT_RUN was trained)}

FACTORS=${FACTORS:-"8 16 32 64 128"}     # extension factors s; T = PHYS_T * s
CACHES=${CACHES:-"ntk"}                  # ntk | yarn | pi | searched
# YARN_SCALE: evaluate under the trainer's YaRN cache instead of the LongRoPE lambda
# ramp. These are DIFFERENT ways to reach the same T and a checkpoint must be scored
# under the one it was TRAINED with, or the numbers are meaningless -- measured:
# run7 (extended at YARN_SCALE=4, m=1.1386) scored under the lambda ramp (scale=1.0)
# had its retrieval FLOOR collapse from 4.17 to 11.34 nats, i.e. it failed even with
# the needle 12 tokens from the probe. bluscriptCP.cpp:914 states the intended matrix:
#   base ckpt     + YARN_SCALE=1   (naive extrapolation @ T)
#   extended ckpt + YARN_SCALE=4   (YaRN @ T)
# Set YARN_SCALE to take that route; leave it empty for the LongRoPE route.
YARN_SCALE=${YARN_SCALE:-}
SEARCHED_LAMBDA=${SEARCHED_LAMBDA:-}     # required when CACHES contains "searched"
PHYS_T=${PHYS_T:-4096}
HEAD_DIM=${HEAD_DIM:-64}
ROPE_THETA=${ROPE_THETA:-500000}
BLOCKS=${BLOCKS:-5}
NEEDLE_LEN=${NEEDLE_LEN:-8}
SEED=${SEED:-0}
GPUS=${GPUS:-0,1,2,3,4,5,6,7}
FILLER=${FILLER:-Data_Loader/Data/edufineweb_val_00000.bin}
DATA_ROOT=${DATA_ROOT:-$PWD/Data_Loader/Data}
NEEDLE_DIR=${NEEDLE_DIR:-needles}
# Absolute offset for the probe+answer span (needle_gen --probe-pos). Unset = the
# end of the trial, the historical layout. Set it to decouple needle-to-probe
# DISTANCE from the needle's ABSOLUTE POSITION, which the default layout welds
# into a single variable. Filenames do NOT encode it, so give each probe position
# its own NEEDLE_DIR/CSV_DIR/STATE_DIR.
PROBE_POS=${PROBE_POS:-}
CSV_DIR=${CSV_DIR:-needle_csv}
STATE_DIR=${STATE_DIR:-needle_state}
RESULTS=${RESULTS:-needle_results.csv}
# Per-rank forward-only logits are (T/CP)*vocab*4 bytes. Keep <= this so a 49 GB
# card holds logits + activations. Pre-flight 2b must confirm it (and that logits
# are fp32 -- a bf16 model triggers an as_type copy that doubles the peak).
MAX_TOK_PER_RANK=${MAX_TOK_PER_RANK:-131072}

EXE=./build/bluscriptCP_exec
export LD_LIBRARY_PATH=BluTrain/Tensor-Implementations/lib:BluTrain/Profiler/lib:${LD_LIBRARY_PATH:-}
IFS=',' read -r -a GPUARR <<< "$GPUS"
NGPU=${#GPUARR[@]}
KV=$(sed -E 's/.*CP_N_KVHEAD=([0-9]+).*/\1/' <<< "$MODEL_ARCH")
mkdir -p "$NEEDLE_DIR" "$CSV_DIR" "$STATE_DIR"
log(){ echo "[$(date '+%F %H:%M:%S')] $*"; }

# ---- pre-flight -------------------------------------------------------------
[ -x "$EXE" ] || { echo "ERROR: $EXE missing -- build first"; exit 1; }
[ -f "$FILLER" ] || { echo "ERROR: filler stream $FILLER not found"; exit 1; }
# Detect a live evaluator by RESOLVED EXECUTABLE, not by command-line text. `pgrep -f
# bluscriptCP_exec` also matches any shell whose argv merely CONTAINS that string --
# including the wrapper that launches this script -- which produced a false "already
# running" abort. `pgrep -x` is no good either: comm truncates to 15 chars and
# "bluscriptCP_exec" is 16. Comparing /proc/PID/exe is exact.
exec_running(){
  local want; want=$(readlink -f "$EXE") || return 1
  local p
  for p in /proc/[0-9]*; do
    [ "$(readlink -f "$p/exe" 2>/dev/null)" = "$want" ] && { echo "${p#/proc/}"; return 0; }
  done
  return 1
}
if pids=$(exec_running); then
  # ALLOW_CONCURRENT_EXE=1 permits a DELIBERATE overlap -- e.g. probing a mid-training
  # checkpoint while the pretrain keeps running. Only use it when the memory headroom is
  # known: a forward-only probe is ~10-15 GB against a training job's ~47 GB on an 80 GB
  # card. The context_rot_2026-08-19 copies of this script already carried this flag; the
  # tracked one never got it.
  if [ "${ALLOW_CONCURRENT_EXE:-0}" = "1" ]; then
    log "WARNING: $EXE already running (pid $pids); ALLOW_CONCURRENT_EXE=1 -- proceeding"
  else
    echo "ERROR: $EXE already running (pid $pids) -- this sweep would oversubscribe GPUs $GPUS"
    echo "  set ALLOW_CONCURRENT_EXE=1 to override when the overlap is intended"
    exit 1
  fi
fi
case " $CACHES " in *" searched "*)
  [ -n "$SEARCHED_LAMBDA" ] || { echo "ERROR: CACHES has 'searched' but SEARCHED_LAMBDA is unset"; exit 1; }
  [ -f "$SEARCHED_LAMBDA" ] || { echo "ERROR: SEARCHED_LAMBDA '$SEARCHED_LAMBDA' not found"; exit 1; };;
esac

# CP degree per row, from the plan's CP table (memory-driven, not a free choice).
cp_for_T(){
  local T=$1 cp=1
  while [ $(( T / cp )) -gt "$MAX_TOK_PER_RANK" ]; do cp=$(( cp * 2 )); done
  echo "$cp"
}
# Verified topology only: pure ring>=4 is known-broken in this repo.
topo_for(){
  local cp=$1
  if   [ "$cp" = "1" ]; then echo ""
  elif [ $(( KV % cp )) -eq 0 ]; then echo "CP_ATTN_MODE=ulysses"
  elif [ $(( cp % KV )) -eq 0 ]; then echo "CP_ATTN_MODE=hybrid CP_ULYSSES_SIZE=$KV"
  else echo "UNSUPPORTED"; fi
}

# lambda file per (cache, head_dim, theta, s). NTK/PI are theta-independent closed
# forms; YaRN depends on base via find_correction_range, so it MUST be regenerated
# per theta. All are head_dim-dependent (vector length = head_dim/2).
lambda_file(){
  local cache=$1 s=$2
  if [ "$cache" = "searched" ]; then echo "$SEARCHED_LAMBDA"; return; fi
  local out="$NEEDLE_DIR/lam_${cache}_hd${HEAD_DIM}_th${ROPE_THETA}_s${s}.txt"
  if [ ! -f "$out" ]; then
    python3 - "$cache" "$s" "$out" "$HEAD_DIM" "$ROPE_THETA" "$PHYS_T" <<'PY'
import sys, importlib.util
cache, s, out, hd, theta, orig = (sys.argv[1], int(sys.argv[2]), sys.argv[3],
                                  int(sys.argv[4]), float(sys.argv[5]), float(sys.argv[6]))
spec = importlib.util.spec_from_file_location('lrs', 'Tests/bluscriptcp/longrope_search.py')
lrs = importlib.util.module_from_spec(spec); spec.loader.exec_module(lrs)
half, lam_max = hd // 2, s * 1.25
if   cache == 'ntk':  lam = lrs.seed_NTK(hd, s, lam_max)
elif cache == 'pi':   lam = lrs.seed_PI(half, s, lam_max)
elif cache == 'yarn': lam = lrs.seed_YaRN(hd, s, orig, 32.0, 1.0, theta, lam_max)
else: sys.exit(f"unknown cache '{cache}'")
lrs.write_factors(out, lam, 0, s, int(orig * s))
PY
  fi
  echo "$out"
}

# Overridable so a REFINED distance grid can be added around a crossing. Set
# VARIANTS to the UNION of old and new points: already-completed ones are skipped
# by their state markers, so only the new points actually run, and the scorer then
# reports the whole grid in one table.
VARIANTS=${VARIANTS:-"p0 p0.25 p0.5 p0.75 p1 absent null"}
log "needle sweep: model=$MODEL_KEY run=$CKPT_RUN factors=[$FACTORS] caches=[$CACHES] blocks=$BLOCKS"
log "  GPUS=$GPUS (n=$NGPU) kv=$KV head_dim=$HEAD_DIM theta=$ROPE_THETA"

for S in $FACTORS; do
  T=$(( PHYS_T * S ))
  CP=$(cp_for_T "$T")
  TOPO=$(topo_for "$CP")
  [ "$TOPO" = "UNSUPPORTED" ] && { echo "ERROR: no verified topology for CP=$CP with kv=$KV at T=$T"; exit 1; }
  [ "$CP" -le "$NGPU" ] || { echo "ERROR: T=$T needs CP=$CP but only $NGPU GPU(s) given"; exit 1; }
  N=$([ "$T" -le 131072 ] && echo 8 || echo 4)      # pinned trials-per-block (plan A1)
  SLOTS=$(( NGPU / CP ))
  log "== s=$S T=$T CP=$CP ${TOPO:-(single-GPU)} N=$N slots=$SLOTS =="

  # .bin files depend only on (T, variant, block) -- shared across models and caches.
  for V in $VARIANTS; do for B in $(seq 0 $((BLOCKS-1))); do
    [ -f "$NEEDLE_DIR/needle_T${T}_${V}_b${B}.bin" ] && continue
    python3 Tests/bluscriptcp/needle_gen.py --T "$T" --variant "$V" --block "$B" \
        --trials "$N" --needle-len "$NEEDLE_LEN" --filler "$FILLER" \
        --seed "$SEED" --out-dir "$NEEDLE_DIR" \
        ${PROBE_POS:+--probe-pos "$PROBE_POS"} >/dev/null
  done; done

  for CACHE in $CACHES; do
    LAM=$(lambda_file "$CACHE" "$S")
    # The lambda vector is length-specific: reject a mismatch loudly rather than
    # scoring a cache tuned for another length (the T == S_search invariant).
    SS=$(awk '/^S_search/{print $2}' "$LAM")
    [ "$SS" = "$T" ] || { echo "ERROR: $LAM has S_search=$SS but this row is T=$T"; exit 1; }

    # ---- job list, then a group-slot loop ----
    jobs=()
    for V in $VARIANTS; do for B in $(seq 0 $((BLOCKS-1))); do jobs+=("$V:$B"); done; done
    running=0; g=0
    for job in "${jobs[@]}"; do
      V=${job%%:*}; B=${job##*:}
      MK="$STATE_DIR/${MODEL_KEY}.T${T}.${CACHE}.${V}.b${B}.done"
      OUTCSV="$CSV_DIR/ppl_needle_T${T}_${V}_b${B}.csv"
      [ -f "$MK" ] && continue
      # pin this group to its own contiguous device subset
      devs=$(IFS=,; echo "${GPUARR[*]:$((g*CP)):$CP}")
      if [ -n "$YARN_SCALE" ]; then
        ROPE_ENV="YARN_SCALE=$YARN_SCALE"          # trainer's YaRN cache
      else
        ROPE_ENV="CP_LONGROPE_FACTORS=$LAM"        # LongRoPE lambda ramp
      fi
      (
        env $MODEL_ARCH CP_SIZE=$CP $TOPO \
            CP_EVAL_PPL=1 CP_T=$T CP_B=1 \
            CP_EVAL_WINDOWS=$N CP_EVAL_SKIP_WINDOWS=0 \
            CP_EVAL_TOKENS_BIN="$NEEDLE_DIR/needle_T${T}_${V}_b${B}.bin" \
            $ROPE_ENV $CKPT_ENV \
            CP_EVAL_OUT="$OUTCSV" CP_DATA_ROOT="$DATA_ROOT" \
            CUDA_VISIBLE_DEVICES=$devs mpirun -np $CP "$EXE" \
          > "$CSV_DIR/${MODEL_KEY}_T${T}_${CACHE}_${V}_b${B}.log" 2>&1 \
          && touch "$MK"
      ) &
      g=$(( (g + 1) % SLOTS )); running=$(( running + 1 ))
      if [ "$running" -ge "$SLOTS" ]; then wait -n; running=$(( running - 1 )); fi
    done
    wait

    # every job must have produced its marker, or scoring would read a stale CSV
    miss=0
    for V in $VARIANTS; do for B in $(seq 0 $((BLOCKS-1))); do
      [ -f "$STATE_DIR/${MODEL_KEY}.T${T}.${CACHE}.${V}.b${B}.done" ] || {
        echo "  MISSING: $V b$B -- see $CSV_DIR/${MODEL_KEY}_T${T}_${CACHE}_${V}_b${B}.log"; miss=1; }
    done; done
    [ "$miss" = "0" ] || { echo "ERROR: s=$S cache=$CACHE incomplete; not scoring"; exit 1; }

    log "  scoring s=$S cache=$CACHE -> $RESULTS"
    python3 Tests/bluscriptcp/needle_score.py \
        --needle-dir "$NEEDLE_DIR" --csv-dir "$CSV_DIR" --T "$T" --blocks "$BLOCKS" \
        --model "$MODEL_KEY" --cache "$CACHE" --factor "$S" \
        --head-dim "$HEAD_DIM" --rope-theta "$ROPE_THETA" --out "$RESULTS" \
        --p-fracs "$(tr ' ' '\n' <<< "$VARIANTS" | grep '^p' | paste -sd,)"
  done
done

log "SWEEP DONE -> $RESULTS  (CSVs in $CSV_DIR, markers in $STATE_DIR)"