Avra98 commited on
Commit
624a68e
·
1 Parent(s): f70d730

Resume scripts + paper figures

Browse files

- Fix EVAL_RE regex bug in adaptive_k_cellpolicy_pipeline.py so plateau-based
k-bumping actually fires (metric was always None before).
- Add --initial_adapter so adaptive-k variants can warm-start from a saved
LoRA adapter directory.
- Make strawman_cellpolicy_pipeline.sh accept an optional SFT_INIT env var
for warm-starting from a previously trained baseline checkpoint.
- New launch_finish_repos23.sh: brings up the 6 strawman warm-start
variants and 2 adaptive-k resume variants across all 8 GPUs.
- Add paper figures comparing latent vs vanilla baseline solve / per-cell
exact across the three curriculum stages (plot_stage_progression.py +
stage_progression_{solve,exact}.{pdf,png}).
- Update .gitignore to keep in-flight run output trees out of the code repo.

.gitattributes CHANGED
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.jsonl filter=lfs diff=lfs merge=lfs -text
37
  *.log filter=lfs diff=lfs merge=lfs -text
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.jsonl filter=lfs diff=lfs merge=lfs -text
37
  *.log filter=lfs diff=lfs merge=lfs -text
38
+ *.png filter=lfs diff=lfs merge=lfs -text
39
+ *.pdf filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -25,3 +25,9 @@ tmp_latent_debug/
25
  logs/
26
 
27
  _pushlogs/
 
 
 
 
 
 
 
25
  logs/
26
 
27
  _pushlogs/
28
+
29
+ # In-flight run output trees (kept in HF model repos, not the code repo)
30
+ _runs/strawman_warm_*/
31
+ _runs/adaptive_k_resume_*/
32
+ _runs/launch_finish_repos23_*_pids.txt
33
+ curriculum_cot/
_runs/_paper_figures/plot_stage_progression.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Paper-style figures: Solve rate + Per-cell exact across stages. Two separate figures,
2
+ no titles, no footer text — only axes, lines, markers, legend.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+
8
+ import matplotlib as mpl
9
+ import matplotlib.pyplot as plt
10
+
11
+ OUT_DIR = Path(__file__).resolve().parent
12
+
13
+ # -------------------------------- DATA ---------------------------------------
14
+ STAGES = ["Stage 1", "Stage 2", "Stage 3"]
15
+
16
+ LATENT_SOLVE = [0.70, 0.50, 0.58]
17
+ LATENT_EXACT = [0.95, 0.958, 0.967]
18
+
19
+ BASELINE_SOLVE = [0.78, 0.40, 0.44]
20
+ BASELINE_EXACT = [0.988, 0.88, 0.83]
21
+
22
+ # -------------------------------- STYLE --------------------------------------
23
+ mpl.rcParams.update({
24
+ "font.family": "serif",
25
+ "font.serif": ["DejaVu Serif", "Times New Roman", "Times", "Liberation Serif"],
26
+ "font.size": 12,
27
+ "axes.labelsize": 12,
28
+ "xtick.labelsize": 11,
29
+ "ytick.labelsize": 11,
30
+ "legend.fontsize": 11,
31
+ "axes.spines.top": False,
32
+ "axes.spines.right": False,
33
+ "axes.linewidth": 1.0,
34
+ "lines.linewidth": 2.0,
35
+ "lines.markersize": 8.5,
36
+ "xtick.direction": "in",
37
+ "ytick.direction": "in",
38
+ "xtick.major.size": 4,
39
+ "ytick.major.size": 4,
40
+ "pdf.fonttype": 42,
41
+ "ps.fonttype": 42,
42
+ })
43
+
44
+ LATENT_COLOR = "#1f4f8b"
45
+ BASELINE_COLOR = "#b21e2f"
46
+ GRID_KW = dict(linestyle=":", linewidth=0.7, color="0.7", alpha=0.7)
47
+ x = list(range(len(STAGES)))
48
+
49
+
50
+ def _plot(y_latent, y_baseline, ylim, yticks, ylabel, fname):
51
+ fig, ax = plt.subplots(figsize=(4.6, 3.4), constrained_layout=True)
52
+ ax.plot(
53
+ x, y_latent,
54
+ color=LATENT_COLOR, marker="s", linestyle="-",
55
+ label="Latent (recurrent-hidden)",
56
+ )
57
+ ax.plot(
58
+ x, y_baseline,
59
+ color=BASELINE_COLOR, marker="o", linestyle="--",
60
+ label="Baseline (vanilla 1.5B)",
61
+ )
62
+ ax.set_xticks(x, STAGES)
63
+ ax.set_ylim(*ylim)
64
+ ax.set_yticks(yticks)
65
+ ax.set_ylabel(ylabel)
66
+ ax.grid(True, axis="y", **GRID_KW)
67
+ ax.legend(frameon=False, loc="best")
68
+ fig.savefig(OUT_DIR / f"{fname}.pdf", bbox_inches="tight")
69
+ fig.savefig(OUT_DIR / f"{fname}.png", dpi=300, bbox_inches="tight")
70
+ plt.close(fig)
71
+ print(f"saved {OUT_DIR / f'{fname}.pdf'}")
72
+
73
+
74
+ _plot(
75
+ LATENT_SOLVE, BASELINE_SOLVE,
76
+ ylim=(0.0, 1.0),
77
+ yticks=[0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
78
+ ylabel="Solve rate",
79
+ fname="stage_progression_solve",
80
+ )
81
+ _plot(
82
+ LATENT_EXACT, BASELINE_EXACT,
83
+ ylim=(0.78, 1.00),
84
+ yticks=[0.80, 0.84, 0.88, 0.92, 0.96, 1.00],
85
+ ylabel="Per-cell set-match rate",
86
+ fname="stage_progression_exact",
87
+ )
_runs/_paper_figures/stage_progression_exact.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0734e025a134db1521ff9e06a7a1d00fac1a7786ed7ddd48aba25082b25cfa16
3
+ size 12154
_runs/_paper_figures/stage_progression_exact.png ADDED

Git LFS Details

  • SHA256: abfe30ccf31423d1359956f1a8806fb846e1254eac44a6d7f53bdcd02439c6b9
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
_runs/_paper_figures/stage_progression_solve.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:004adf9132094bc6d0c639c3cca706be383b77efde5ad5313bd1f26beb656f38
3
+ size 11689
_runs/_paper_figures/stage_progression_solve.png ADDED

Git LFS Details

  • SHA256: 4584fce300940509e3eddf1294d040833f6e868e153548f634b7635948b3f745
  • Pointer size: 130 Bytes
  • Size of remote file: 93 kB
_runs/adaptive_k_cellpolicy_pipeline.py CHANGED
@@ -76,16 +76,27 @@ def parse_args() -> argparse.Namespace:
76
  p.add_argument("--train_rows", type=int, default=10000)
77
  p.add_argument("--enable_gc", action="store_true", default=True)
78
  p.add_argument("--seed", type=int, default=0)
 
 
 
 
 
79
  return p.parse_args()
80
 
81
 
82
  # ---- log parsing -----------------------------------------------------------
83
 
84
- EVAL_RE = re.compile(r"exact_set_match_rate.*?([01]\.\d+)")
 
 
 
 
 
 
85
 
86
 
87
  def latest_eval_metric(log_path: Path) -> Optional[float]:
88
- """Return the most recent eval exact_set_match_rate from the SFT train log."""
89
  if not log_path.exists():
90
  return None
91
  last: Optional[float] = None
@@ -343,7 +354,9 @@ def main() -> None:
343
 
344
  history: List[dict] = []
345
  cur_k = int(args.start_k)
346
- cur_init: str = "" # "" -> train from base
 
 
347
  last_metric_at_k: Optional[float] = None
348
  phases_at_k = 0
349
  sft_phase_idx = 0
 
76
  p.add_argument("--train_rows", type=int, default=10000)
77
  p.add_argument("--enable_gc", action="store_true", default=True)
78
  p.add_argument("--seed", type=int, default=0)
79
+ p.add_argument(
80
+ "--initial_adapter",
81
+ default="",
82
+ help="Optional adapter dir to warm-start the first SFT phase from (used for resuming an interrupted adaptive-k run at a non-zero k).",
83
+ )
84
  return p.parse_args()
85
 
86
 
87
  # ---- log parsing -----------------------------------------------------------
88
 
89
+ # The SFT trainer logs eval as e.g.
90
+ # [latent sft eval] parse=1.000 canonical=1.000 exact=0.685 precision=... recall=... solve=0.000
91
+ # i.e. the field is shortened from `exact_set_match_rate` to `exact=`. The
92
+ # original regex `exact_set_match_rate.*?([01]\.\d+)` therefore never matched,
93
+ # leaving `eval_exact_set_match_rate=None` in PIPELINE.log and disabling
94
+ # plateau-based k-bumping. Fix: parse the short form on `[* sft eval]` lines.
95
+ EVAL_RE = re.compile(r"\[\w+ sft eval\][^\n]*?exact=([01]\.\d+)")
96
 
97
 
98
  def latest_eval_metric(log_path: Path) -> Optional[float]:
99
+ """Return the most recent eval per-cell exact match rate from the SFT train log."""
100
  if not log_path.exists():
101
  return None
102
  last: Optional[float] = None
 
354
 
355
  history: List[dict] = []
356
  cur_k = int(args.start_k)
357
+ cur_init: str = args.initial_adapter or ""
358
+ if cur_init:
359
+ log(f" initial_adapter={cur_init}")
360
  last_metric_at_k: Optional[float] = None
361
  phases_at_k = 0
362
  sft_phase_idx = 0
_runs/launch_finish_repos23.sh ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Finish repos 2 (strawman) and 3 (adaptive-k) on all 8 GPUs.
3
+ #
4
+ # GPU 0,1: resume the two half-finished adaptive-k runs at the next k
5
+ # (init from the latest HF checkpoint of phase02_k0 / phase01_k0).
6
+ # GPU 2-7: six warm-started strawman variants, all initialised from the
7
+ # v6_i_sft_v_oversample10 best checkpoint (solve=0.44 @ step 200),
8
+ # sweeping the SFT LR / multi-value oversample / GRPO reward axes.
9
+ #
10
+ # After all eight jobs exit we will have:
11
+ # - adaptive-k variants pushed past their phase-2 plateau
12
+ # - 6 strawman variants whose SFT loss is guaranteed to decrease (warm init)
13
+ # plus a final GRPO phase with a 100-puzzle eval, so each variant publishes
14
+ # a paper-ready (per-cell exact, solve) tuple.
15
+ set -euo pipefail
16
+
17
+ ROOT="/home/ubuntu/curriculum_cot"
18
+ HFROOT="/home/ubuntu/hf_checkpoints"
19
+ TS="$(date +%Y%m%d_%H%M%S)"
20
+ ADAPT_SWEEP="${ROOT}/_runs/adaptive_k_resume_${TS}"
21
+ STRAW_SWEEP="${ROOT}/_runs/strawman_warm_${TS}"
22
+ mkdir -p "${ADAPT_SWEEP}" "${STRAW_SWEEP}"
23
+ PY="${ROOT}/_runs/adaptive_k_cellpolicy_pipeline.py"
24
+ STRAW_PIPE="${ROOT}/_runs/strawman_cellpolicy_pipeline.sh"
25
+ chmod +x "${STRAW_PIPE}"
26
+
27
+ # Warm-start checkpoint for strawman: v6_i_sft_v_oversample10 S3 SFT step 200
28
+ # (best baseline anywhere in the sweep: per-cell exact 0.951, solve 0.440).
29
+ V6I_BEST="${HFROOT}/baseline/v6_i_sft_v_oversample10/s3_sft/checkpoint-step-00200"
30
+ if [[ ! -f "${V6I_BEST}/adapter_model.safetensors" ]]; then
31
+ echo "ERROR: missing v6_i warm-start checkpoint at ${V6I_BEST}" >&2
32
+ exit 1
33
+ fi
34
+
35
+ PIDS_FILE="${ADAPT_SWEEP}/../launch_finish_repos23_${TS}_pids.txt"
36
+ : > "${PIDS_FILE}"
37
+
38
+ launch_adaptive() {
39
+ local variant="$1" gpu="$2" init_adapter="$3" start_k="$4" max_k="$5" eps="$6" max_phases="$7"
40
+ local out="${ADAPT_SWEEP}/${variant}"
41
+ mkdir -p "${out}"
42
+ echo "[launch] ADAPTIVE ${variant} on GPU ${gpu} out=${out} init=${init_adapter}"
43
+ nohup /opt/pytorch/bin/python -u "${PY}" \
44
+ --variant "${variant}" \
45
+ --gpu "${gpu}" \
46
+ --output_root "${out}" \
47
+ --initial_adapter "${init_adapter}" \
48
+ --start_k "${start_k}" \
49
+ --max_k "${max_k}" \
50
+ --steps_per_phase 600 \
51
+ --max_phases_per_k "${max_phases}" \
52
+ --plateau_eps "${eps}" \
53
+ --sft_lr 2e-5 --sft_bs 8 --sft_ga 4 \
54
+ --grpo_steps 1500 --grpo_lr 5e-6 --grpo_bs 8 --grpo_ga 4 --grpo_ng 8 \
55
+ > "${out}/console.log" 2>&1 &
56
+ local pid=$!
57
+ disown "${pid}" || true
58
+ echo "ADAPTIVE_${variant}=${pid}" >> "${PIDS_FILE}"
59
+ }
60
+
61
+ launch_strawman() {
62
+ # Usage: launch_strawman <variant> <gpu> <KEY=VALUE>...
63
+ local variant="$1" gpu="$2"
64
+ shift 2
65
+ local out="${STRAW_SWEEP}/${variant}"
66
+ mkdir -p "${out}"
67
+ echo "[launch] STRAWMAN ${variant} on GPU ${gpu} out=${out}"
68
+ # NB: env KEY=VAL ... bash PIPE — keep all assignments before `bash` and
69
+ # use printf '%q' so values like `2e-5` round-trip through env quoting safely.
70
+ local assignments=""
71
+ for kv in "$@"; do
72
+ assignments+=" ${kv}"
73
+ done
74
+ nohup env VARIANT="${variant}" GPU="${gpu}" OUTPUT_ROOT="${out}" \
75
+ SFT_INIT="${V6I_BEST}" \
76
+ ${assignments} \
77
+ bash "${STRAW_PIPE}" > "${out}/console.log" 2>&1 &
78
+ local pid=$!
79
+ disown "${pid}" || true
80
+ echo "STRAWMAN_${variant}=${pid}" >> "${PIDS_FILE}"
81
+ }
82
+
83
+ # === Adaptive-k resumes (GPUs 0,1) ===========================================
84
+ # Pick the latest k=0 ckpt for each, resume at k=1 with a stricter plateau and
85
+ # 3 phases per k so the bumping logic actually has a chance to fire now that
86
+ # the regex bug is fixed.
87
+ ADAPT_A_INIT="${HFROOT}/adaptive_k/20260525_024629/adaptive_a_eps01/sft_phase02_k0/checkpoint-step-00600"
88
+ ADAPT_B_INIT="${HFROOT}/adaptive_k/20260525_024629/adaptive_b_fastgrow/sft_phase01_k0/checkpoint-step-00800"
89
+
90
+ launch_adaptive adaptive_e_resume_a 0 "${ADAPT_A_INIT}" 1 4 0.005 3
91
+ launch_adaptive adaptive_f_resume_b 1 "${ADAPT_B_INIT}" 1 4 0.005 3
92
+
93
+ # === Strawman warm-started from v6_i (GPUs 2-7) =============================
94
+ # Six variants spanning SFT LR x oversample x GRPO reward to make sure at least
95
+ # one converges robustly. All start from the same warm checkpoint so the SFT
96
+ # loss is guaranteed to decrease from the first step.
97
+
98
+ launch_strawman strawman_warm_a_lr2e5_o3 2 \
99
+ SFT_LR=2e-5 SFT_OVERSAMPLE=3 SFT_MAX_STEPS=1500 \
100
+ GRPO_LR=5e-6 GRPO_MAX_STEPS=1500 \
101
+ EXACT_MATCH_BONUS=1.0 CARD_MISMATCH_PEN=1.5
102
+
103
+ launch_strawman strawman_warm_b_lr2e5_o5 3 \
104
+ SFT_LR=2e-5 SFT_OVERSAMPLE=5 SFT_MAX_STEPS=1500 \
105
+ GRPO_LR=5e-6 GRPO_MAX_STEPS=1500 \
106
+ EXACT_MATCH_BONUS=1.0 CARD_MISMATCH_PEN=1.5
107
+
108
+ launch_strawman strawman_warm_c_lr2e5_o8 4 \
109
+ SFT_LR=2e-5 SFT_OVERSAMPLE=8 SFT_MAX_STEPS=1500 \
110
+ GRPO_LR=5e-6 GRPO_MAX_STEPS=1500 \
111
+ EXACT_MATCH_BONUS=1.0 CARD_MISMATCH_PEN=1.5
112
+
113
+ launch_strawman strawman_warm_d_lr5e5_o10 5 \
114
+ SFT_LR=5e-5 SFT_OVERSAMPLE=10 SFT_MAX_STEPS=1500 \
115
+ GRPO_LR=5e-6 GRPO_MAX_STEPS=1500 \
116
+ EXACT_MATCH_BONUS=1.0 CARD_MISMATCH_PEN=1.5
117
+
118
+ launch_strawman strawman_warm_e_lr1e5_o5 6 \
119
+ SFT_LR=1e-5 SFT_OVERSAMPLE=5 SFT_MAX_STEPS=1500 \
120
+ GRPO_LR=2e-6 GRPO_MAX_STEPS=1500 \
121
+ EXACT_MATCH_BONUS=1.0 CARD_MISMATCH_PEN=1.5
122
+
123
+ launch_strawman strawman_warm_f_lr2e5_o5_sharp 7 \
124
+ SFT_LR=2e-5 SFT_OVERSAMPLE=5 SFT_MAX_STEPS=1500 \
125
+ GRPO_LR=2e-6 GRPO_MAX_STEPS=1500 \
126
+ EXACT_MATCH_BONUS=4.0 CARD_MISMATCH_PEN=2.0
127
+
128
+ echo
129
+ echo "[launch] adaptive sweep root: ${ADAPT_SWEEP}"
130
+ echo "[launch] strawman sweep root: ${STRAW_SWEEP}"
131
+ echo "[launch] PIDs file: ${PIDS_FILE}"
132
+ cat "${PIDS_FILE}"
_runs/strawman_cellpolicy_pipeline.sh CHANGED
@@ -54,6 +54,10 @@ EVAL_ROWS="${EVAL_ROWS:-100}"
54
  TRAIN_ROWS="${TRAIN_ROWS:-10000}"
55
  USE_GC="${USE_GC:-1}" # GC=1 to allow bs 16 on a single 80G GPU
56
  PHASE_WALL_SECS="${PHASE_WALL_SECS:-0}"
 
 
 
 
57
 
58
  TRAIN_JSONL="${ROOT}/data/sudoku_t3_20empty_value_qwen_text_stage1_train.jsonl"
59
  EVAL_JSONL="${ROOT}/data/sudoku_t3_20empty_value_qwen_text_stage1_eval.jsonl"
@@ -90,6 +94,7 @@ if [[ "${USE_GC}" == "1" ]]; then GC_FLAG=(--enable_gradient_checkpointing); fi
90
 
91
  log "===== STRAWMAN ${VARIANT} on GPU ${GPU} ====="
92
  log " SFT lr=${SFT_LR} max_steps=${SFT_MAX_STEPS} bs=${SFT_BS}x${SFT_GA} GC=${USE_GC}"
 
93
  log " GRPO lr=${GRPO_LR} max_steps=${GRPO_MAX_STEPS} ng=${GRPO_NG} bs=${GRPO_BS}x${GRPO_GA}"
94
  log " rewards good=${REWARD_GOOD} bad=${PENALTY_BAD} mal=${PENALTY_MAL} empty=${PENALTY_EMPTY} sng=${PENALTY_SINGLETON} miss=${PENALTY_MISSING} bonus=${EXACT_MATCH_BONUS} card=${CARD_MISMATCH_PEN}"
95
  log " out=${OUTPUT_ROOT}"
@@ -97,14 +102,14 @@ log " out=${OUTPUT_ROOT}"
97
  # ----- Phase 1: SFT at stage_i=3 from BASE (no init adapter) -----
98
  SFT_DIR="${OUTPUT_ROOT}/sft"
99
  mkdir -p "${SFT_DIR}"
100
- log "=== PHASE SFT (stage_i=3, init=BASE) ==="
101
  "${PYTHON_BIN}" -u "${SFT_SCRIPT}" \
102
  --model_name "${MODEL_NAME}" \
103
  --train_jsonl "${TRAIN_JSONL}" \
104
  --eval_jsonl "${EVAL_JSONL}" \
105
  --output_dir "${SFT_DIR}" \
106
  --cache_dir "${ROOT}/.hf_cache" \
107
- --init_adapter_dir "" \
108
  --seed 0 \
109
  --gpu_id 0 \
110
  --stage_i 3 \
 
54
  TRAIN_ROWS="${TRAIN_ROWS:-10000}"
55
  USE_GC="${USE_GC:-1}" # GC=1 to allow bs 16 on a single 80G GPU
56
  PHASE_WALL_SECS="${PHASE_WALL_SECS:-0}"
57
+ # Optional warm-start. Empty = train SFT from base Qwen (true strawman).
58
+ # Otherwise must point at a directory containing an `adapter_model.safetensors`
59
+ # (e.g. a previously trained S3 SFT checkpoint).
60
+ SFT_INIT="${SFT_INIT:-}"
61
 
62
  TRAIN_JSONL="${ROOT}/data/sudoku_t3_20empty_value_qwen_text_stage1_train.jsonl"
63
  EVAL_JSONL="${ROOT}/data/sudoku_t3_20empty_value_qwen_text_stage1_eval.jsonl"
 
94
 
95
  log "===== STRAWMAN ${VARIANT} on GPU ${GPU} ====="
96
  log " SFT lr=${SFT_LR} max_steps=${SFT_MAX_STEPS} bs=${SFT_BS}x${SFT_GA} GC=${USE_GC}"
97
+ log " SFT_INIT=${SFT_INIT:-<base>}"
98
  log " GRPO lr=${GRPO_LR} max_steps=${GRPO_MAX_STEPS} ng=${GRPO_NG} bs=${GRPO_BS}x${GRPO_GA}"
99
  log " rewards good=${REWARD_GOOD} bad=${PENALTY_BAD} mal=${PENALTY_MAL} empty=${PENALTY_EMPTY} sng=${PENALTY_SINGLETON} miss=${PENALTY_MISSING} bonus=${EXACT_MATCH_BONUS} card=${CARD_MISMATCH_PEN}"
100
  log " out=${OUTPUT_ROOT}"
 
102
  # ----- Phase 1: SFT at stage_i=3 from BASE (no init adapter) -----
103
  SFT_DIR="${OUTPUT_ROOT}/sft"
104
  mkdir -p "${SFT_DIR}"
105
+ log "=== PHASE SFT (stage_i=3, init=${SFT_INIT:-BASE}) ==="
106
  "${PYTHON_BIN}" -u "${SFT_SCRIPT}" \
107
  --model_name "${MODEL_NAME}" \
108
  --train_jsonl "${TRAIN_JSONL}" \
109
  --eval_jsonl "${EVAL_JSONL}" \
110
  --output_dir "${SFT_DIR}" \
111
  --cache_dir "${ROOT}/.hf_cache" \
112
+ --init_adapter_dir "${SFT_INIT}" \
113
  --seed 0 \
114
  --gpu_id 0 \
115
  --stage_i 3 \