claude-opus-5
on-policy-self-distillation — graded by the task's verifier against a measured baseline.
- backend
- modal
- variant
- claude claude-opus-5
- metric
- AIME24 avg@12 accuracy
- actions
- 148
- duration
- 8h 50m
- recorded
- 2026-08-03
run idopsd-aime24-4gpus__g75tHnA
Everything claude-opus-5 did on this task, in order.
download JSON ↗encrypted chain-of-thought — recorded, not exposed
args
{
"replace_all": false,
"file_path": "/workspace/repo/data_collator.py",
"old_string": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")\nTEACHER_CONTEXT = os.environ.get(\"OPSD_TEACHER_CONTEXT\", \"solution\")",
"new_string": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")\nTEACHER_CONTEXT = os.environ.get(\"OPSD_TEACHER_CONTEXT\", \"solution\")\n# \"left\" keeps the prompt block flush against the completion (see the padding note\n# in __init__); \"right\" reproduces the released recipe.\nPAD_SIDE = os.environ.get(\"OPSD_PAD_SIDE\", \"left\")"
}Edit result
The file /workspace/repo/data_collator.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/workspace/repo/data_collator.py", "oldString": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")\nTEACHER_CONTEXT = os.environ.get(\"OPSD_TEACHER_CONTEXT\", \"solution\")", "newString": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")\nTEACHER_CONTEXT = os.environ.get(\"OPSD_TEACHER_CONTEXT\", \"solution\")\n# \"left\" keeps the prompt block flush against the completion (see the padding note\n# in __init__); \"right\" reproduces the released recipe.\nPAD_SIDE = os.environ.get(\"OPSD_PAD_SIDE\", \"left\")", "originalFile": null, "structuredPatch": [{"oldStart": 30, "oldLines": 6, "newStart": 30, "newLines": 9, "lines": [" # ---------------------------------------------------------------------------", " STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")", " TEACHER_CONTEXT = os.environ.get(\"OPSD_TEACHER_CONTEXT\", \"solution\")", "+# \"left\" keeps the prompt block flush against the completion (see the padding note", "+# in __init__); \"right\" reproduces the released recipe.", "+PAD_SIDE = os.environ.get(\"OPSD_PAD_SIDE\", \"left\")", " ", " ", " class SelfDistillationDataCollator:"]}], "userModified": false, "replaceAll": false}encrypted chain-of-thought — recorded, not exposed
args
{
"replace_all": false,
"file_path": "/workspace/repo/data_collator.py",
"old_string": " self.tokenizer.padding_side = \"left\"",
"new_string": " self.tokenizer.padding_side = PAD_SIDE"
}Edit result
The file /workspace/repo/data_collator.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/workspace/repo/data_collator.py", "oldString": " self.tokenizer.padding_side = \"left\"", "newString": " self.tokenizer.padding_side = PAD_SIDE", "originalFile": null, "structuredPatch": [{"oldStart": 89, "oldLines": 7, "newStart": 89, "newLines": 7, "lines": [" # prompt and completion contiguous (a per-row constant RoPE offset, which attention", " # is invariant to): the same measurement drops to KL ~0.0004.", " print(f\"[DataCollator] Original padding_side: {self.tokenizer.padding_side}\")", "- self.tokenizer.padding_side = \"left\"", "+ self.tokenizer.padding_side = PAD_SIDE", " print(f\"[DataCollator] Set padding_side to: {self.tokenizer.padding_side}\")", " print(f\"[DataCollator] Reason first mode: {self.reason_first}\")", " "]}], "userModified": false, "replaceAll": false}Now the data-selection lever in opsd_train.py:
args
{
"replace_all": false,
"file_path": "/workspace/repo/opsd_train.py",
"old_string": " dataset = load_dataset(\"siyanzhao/Openthoughts_math_30k_opsd\")\n train_dataset = dataset[\"train\"]",
"new_string": " dataset = load_dataset(\"siyanzhao/Openthoughts_math_30k_opsd\")\n train_dataset = dataset[\"train\"]\n\n # ---------------------------------------------------------------------\n # Data selection under a fixed optimizer budget.\n #\n # 100 steps at global batch 32 consumes only 3200 of the 29434 rows, so\n # *which* rows we spend the budget on is itself a method choice. The pool is\n # dominated by `math` (MATH-style school problems, mean verified-trace length\n # 2219 tokens) whereas the target benchmark is competition math. On easy rows\n # the privileged teacher and the student already agree, so the per-token KL --\n # the entire training signal -- is near zero and the step is largely wasted.\n #\n # \"hard\" therefore drops the easiest source and keeps the upper half of the\n # remaining rows by verified-trace length (a difficulty proxy: every row is\n # already correct=True, so length reflects how much reasoning the problem\n # needed, not whether it was solved). This re-spends the fixed budget on rows\n # that actually carry teacher/student divergence.\n # ---------------------------------------------------------------------\n data_select = os.environ.get(\"OPSD_DATA_SELECT\", \"none\")\n if data_select != \"none\":\n n_before = len(train_dataset)\n if data_select == \"hard\":\n train_dataset = train_dataset.filter(lambda r: r[\"source\"] != \"math\")\n counts = sorted(train_dataset[\"generated_token_count\"])\n thresh = counts[len(counts) // 2]\n train_dataset = train_dataset.filter(lambda r: r[\"generated_token_count\"] >= thresh)\n print(f\"[OPSD data] select=hard: {n_before} -> {len(train_dataset)} rows (trace_len >= {thresh})\")\n elif data_select == \"nomath\":\n train_dataset = train_dataset.filter(lambda r: r[\"source\"] != \"math\")\n print(f\"[OPSD data] select=nomath: {n_before} -> {len(train_dataset)} rows\")\n else:\n raise ValueError(f\"unknown OPSD_DATA_SELECT={data_select}\")"
}Edit result
The file /workspace/repo/opsd_train.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/workspace/repo/opsd_train.py", "oldString": " dataset = load_dataset(\"siyanzhao/Openthoughts_math_30k_opsd\")\n train_dataset = dataset[\"train\"]", "newString": " dataset = load_dataset(\"siyanzhao/Openthoughts_math_30k_opsd\")\n train_dataset = dataset[\"train\"]\n\n # ---------------------------------------------------------------------\n # Data selection under a fixed optimizer budget.\n #\n # 100 steps at global batch 32 consumes only 3200 of the 29434 rows, so\n # *which* rows we spend the budget on is itself a method choice. The pool is\n # dominated by `math` (MATH-style school problems, mean verified-trace length\n # 2219 tokens) whereas the target benchmark is competition math. On easy rows\n # the privileged teacher and the student already agree, so the per-token KL --\n # the entire training signal -- is near zero and the step is largely wasted.\n #\n # \"hard\" therefore drops the easiest source and keeps the upper half of the\n # remaining rows by verified-trace length (a difficulty proxy: every row is\n # already correct=True, so length reflects how much reasoning the problem\n # needed, not whether it was solved). This re-spends the fixed budget on rows\n # that actually carry teacher/student divergence.\n # ---------------------------------------------------------------------\n data_select = os.environ.get(\"OPSD_DATA_SELECT\", \"none\")\n if data_select != \"none\":\n n_before = len(train_dataset)\n if data_select == \"hard\":\n train_dataset = train_dataset.filter(lambda r: r[\"source\"] != \"math\")\n counts = sorted(train_dataset[\"generated_token_count\"])\n thresh = counts[len(counts) // 2]\n train_dataset = train_dataset.filter(lambda r: r[\"generated_token_count\"] >= thresh)\n print(f\"[OPSD data] select=hard: {n_before} -> {len(train_dataset)} rows (trace_len >= {thresh})\")\n elif data_select == \"nomath\":\n train_dataset = train_dataset.filter(lambda r: r[\"source\"] != \"math\")\n print(f\"[OPSD data] select=nomath: {n_before} -> {len(train_dataset)} rows\")\n else:\n raise ValueError(f\"unknown OPSD_DATA_SELECT={data_select}\")", "originalFile": null, "structuredPatch": [{"oldStart": 266, "oldLines": 6, "newStart": 266, "newLines": 37, "lines": [" dataset = load_dataset(\"siyanzhao/Openthoughts_math_30k_opsd\")", " train_dataset = dataset[\"train\"]", " ", "+ # ---------------------------------------------------------------------", "+ # Data selection under a fixed optimizer budget.", "+ #", "+ # 100 steps at global batch 32 consumes only 3200 of the 29434 rows, so", "+ # *which* rows we spend the budget on is itself a method choice. The pool is", "+ # dominated by `math` (MATH-style school problems, mean verified-trace length", "+ # 2219 tokens) whereas the target benchmark is competition math. On easy rows", "+ # the privileged teacher and the student already agree, so the per-token KL --", "+ # the entire training signal -- is near zero and the step is largely wasted.", "+ #", "+ # \"hard\" therefore drops the easiest source and keeps the upper half of the", "+ # remaining rows by verified-trace length (a difficulty proxy: every row is", "+ # already correct=True, so length reflects how much reasoning the problem", "+ # needed, not whether it was solved). This re-spends the fixed budget on rows", "+ # that actually carry teacher/student divergence.", "+ # ---------------------------------------------------------------------", "+ data_select = os.environ.get(\"OPSD_DATA_SELECT\", \"none\")", "+ if data_select != \"none\":", "+ n_before = len(train_dataset)", "+ if data_select == \"hard\":", "+ train_dataset = train_dataset.filter(lambda r: r[\"source\"] != \"math\")", "+ counts = sorted(train_dataset[\"generated_token_count\"])", "+ thresh = counts[len(counts) // 2]", "+ train_dataset = train_dataset.filter(lambda r: r[\"generated_token_count\"] >= thresh)", "+ print(f\"[OPSD data] select=hard: {n_before} -> {len(train_dataset)} rows (trace_len >= {thresh})\")", "+ elif data_select == \"nomath\":", "+ train_dataset = train_dataset.filter(lambda r: r[\"source\"] != \"math\")", "+ print(f\"[OPSD data] select=nomath: {n_before} -> {len(train_dataset)} rows\")", "+ else:", "+ raise ValueError(f\"unknown OPSD_DATA_SELECT={data_select}\")", "+", " trainer = OPSDTrainer(", " model=model_args.model_name_or_path,", " args=training_args,"]}], "userModified": false, "replaceAll": false}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "ls -la /workspace/ 2>&1 | head -30; echo \"--- repo:\"; ls -la /workspace/repo/ | head -30; echo \"--- backups anywhere:\"; ls /workspace/*.orig /workspace/repo/*.orig /workspace/repo_orig 2>&1 | head",
"description": "Look for a pristine copy of the repo"
}Bash result
total 1142
drwxr-xr-x 1 root root 360 Aug 3 04:41 .
drwxr-xr-x 1 root root 200 Aug 3 03:51 ..
drwxr-xr-x 1 root root 60 Aug 3 03:48 .live
drwxr-xr-x 1 root root 100 Aug 3 03:48 .metrics
drwxr-xr-x 1 root root 140 Aug 3 03:48 .timer
dr-xr-xr-x 1 root root 47 Aug 2 17:09 baseline
-rwxrwxr-x 1 root root 1359 Aug 2 17:09 dev_eval.sh
-rw-r--r-- 1 root root 5349 Aug 3 03:52 diag.out
-rw-r--r-- 1 root root 5740 Aug 3 03:51 diag.py
-rw-r--r-- 1 root root 4721 Aug 3 03:54 diag2.py
-rw-r--r-- 1 root root 28953 Aug 3 04:41 eval_ABD.log
-rw-rw-r-- 1 root root 16440 Aug 2 17:09 eval_official.py
drwxr-xr-x 1 root root 60 Aug 3 04:41 eval_results
-rw-rw-r-- 1 root root 681639 Aug 2 17:09 paper.pdf
-rw-r--r-- 1 root root 59022 Aug 3 03:51 paper.txt
drwxr-xr-x 1 root root 480 Aug 3 04:48 repo
drwxr-xr-x 1 root root 80 Aug 3 04:06 runs
-rw-r--r-- 1 root root 151052 Aug 3 04:23 runs_ABD.log
-rw-r--r-- 1 root root 4718 Aug 3 03:56 smoke.sh
-rw-r--r-- 1 root root 194880 Aug 3 04:03 smokeA.out
drwxr-xr-x 1 root root 32 Aug 2 17:15 submission
-rwxrwxr-x 1 root root 1357 Aug 2 17:09 timer.sh
-rwxrwxr-x 1 root root 3148 Aug 2 17:09 train_baseline.sh
-rwxrwxr-x 1 root root 4997 Aug 2 17:09 train_capped.sh
--- repo:
total 133
drwxr-xr-x 1 root root 480 Aug 3 04:48 .
drwxr-xr-x 1 root root 360 Aug 3 04:41 ..
drwxr-xr-x 1 root root 80 Aug 3 03:55 .git
-rw-r--r-- 1 root root 34 Aug 2 17:14 .gitignore
-rw-r--r-- 1 root root 7520 Aug 2 17:15 README.md
drwxr-xr-x 1 root root 320 Aug 3 04:07 __pycache__
-rw-r--r-- 1 root root 549 Aug 2 17:14 accelerate.yaml
-rw-r--r-- 1 root root 12707 Aug 3 04:48 data_collator.py
-rw-r--r-- 1 root root 509 Aug 2 17:14 environment.yml
drwxr-xr-x 1 root root 93 Aug 2 17:14 eval
-rw-r--r-- 1 root root 11445 Aug 2 17:14 grpo_train.py
-rw-r--r-- 1 root root 14015 Aug 3 04:48 opsd_train.py
-rw-r--r-- 1 root root 77225 Aug 3 04:04 opsd_trainer.py
drwxr-xr-x 1 root root 4096 Aug 2 17:14 scripts
-rw-r--r-- 1 root root 5242 Aug 2 17:14 sft_train.py
--- backups anywhere:
ls: cannot access '/workspace/*.orig': No such file or directory
ls: cannot access '/workspace/repo/*.orig': No such file or directory
ls: cannot access '/workspace/repo_orig': No such file or directory
[stdout]
total 1142
drwxr-xr-x 1 root root 360 Aug 3 04:41 .
drwxr-xr-x 1 root root 200 Aug 3 03:51 ..
drwxr-xr-x 1 root root 60 Aug 3 03:48 .live
drwxr-xr-x 1 root root 100 Aug 3 03:48 .metrics
drwxr-xr-x 1 root root 140 Aug 3 03:48 .timer
dr-xr-xr-x 1 root root 47 Aug 2 17:09 baseline
-rwxrwxr-x 1 root root 1359 Aug 2 17:09 dev_eval.sh
-rw-r--r-- 1 root root 5349 Aug 3 03:52 diag.out
-rw-r--r-- 1 root root 5740 Aug 3 03:51 diag.py
-rw-r--r-- 1 root root 4721 Aug 3 03:54 diag2.py
-rw-r--r-- 1 root root 28953 Aug 3 04:41 eval_ABD.log
-rw-rw-r-- 1 root root 16440 Aug 2 17:09 eval_official.py
drwxr-xr-x 1 root root 60 Aug 3 04:41 eval_results
-rw-rw-r-- 1 root root 681639 Aug 2 17:09 paper.pdf
-rw-r--r-- 1 root root 59022 Aug 3 03:51 paper.txt
drwxr-xr-x 1 root root 480 Aug 3 04:48 repo
drwxr-xr-x 1 root root 80 Aug 3 04:06 runs
-rw-r--r-- 1 root root 151052 Aug 3 04:23 runs_ABD.log
-rw-r--r-- 1 root root 4718 Aug 3 03:56 smoke.sh
-rw-r--r-- 1 root root 194880 Aug 3 04:03 smokeA.out
drwxr-xr-x 1 root root 32 Aug 2 17:15 submission
-rwxrwxr-x 1 root root 1357 Aug 2 17:09 timer.sh
-rwxrwxr-x 1 root root 3148 Aug 2 17:09 train_baseline.sh
-rwxrwxr-x 1 root root 4997 Aug 2 17:09 train_capped.sh
--- repo:
total 133
drwxr-xr-x 1 root root 480 Aug 3 04:48 .
drwxr-xr-x 1 root root 360 Aug 3 04:41 ..
drwxr-xr-x 1 root root 80 Aug 3 03:55 .git
-rw-r--r-- 1 root root 34 Aug 2 17:14 .gitignore
-rw-r--r-- 1 root root 7520 Aug 2 17:15 README.md
drwxr-xr-x 1 root root 320 Aug 3 04:07 __pycache__
-rw-r--r-- 1 root root 549 Aug 2 17:14 accelerate.yaml
-rw-r--r-- 1 root root 12707 Aug 3 04:48 data_collator.py
-rw-r--r-- 1 root root 509 Aug 2 17:14 environment.yml
drwxr-xr-x 1 root root 93 Aug 2 17:14 eval
-rw-r--r-- 1 root root 11445 Aug 2 17:14 grpo_train.py
-rw-r--r-- 1 root root 14015 Aug 3 04:48 opsd_train.py
-rw-r--r-- 1 root root 77225 Aug 3 04:04 opsd_trainer.py
drwxr-xr-x 1 root root 4096 Aug 2 17:14 scripts
-rw-r--r-- 1 root root 5242 Aug 2 17:14 sft_train.py
--- backups anywhere:
ls: cannot access '/workspace/*.orig': No such file or directory
ls: cannot access '/workspace/repo/*.orig': No such file or directory
ls: cannot access '/workspace/repo_orig': No such file or directory
[metadata] {"noOutputExpected": false}There's a .git in the repo — let me diff my changes against the original to verify that "paper mode" is a faithful reproduction.
args
{
"command": "cd /workspace/repo && git --no-pager diff --stat 2>&1 | head; echo \"=== trainer diff:\"; git --no-pager diff -U2 opsd_trainer.py 2>&1 | head -160",
"description": "Diff trainer against original"
}Bash result
README.md | 4 +--
data_collator.py | 60 +++++++++++++++++++++++++++++--
opsd_train.py | 31 ++++++++++++++++
opsd_trainer.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 186 insertions(+), 15 deletions(-)
=== trainer diff:
diff --git a/opsd_trainer.py b/opsd_trainer.py
index c151bd7..c536d88 100644
--- a/opsd_trainer.py
+++ b/opsd_trainer.py
@@ -189,4 +189,12 @@ class OPSDTrainer(SFTTrainer):
self.top_k_loss = top_k_loss
self.jsd_token_clip = jsd_token_clip
+ # "entry" = released recipe (clamp each vocabulary entry at tau).
+ # "token" = clamp the magnitude of each position's full-vocabulary divergence instead,
+ # preserving its direction. See `generalized_jsd_loss`.
+ self.jsd_clip_mode = os.environ.get("OPSD_CLIP_MODE", "entry")
+ if self.jsd_clip_mode not in ("entry", "token"):
+ raise ValueError(f"OPSD_CLIP_MODE must be 'entry' or 'token', got {self.jsd_clip_mode}")
+ print(f"[OPSD] jsd clip mode: {self.jsd_clip_mode} (tau={self.jsd_token_clip})")
+ self._kl_stat_sum, self._kl_stat_n, self._kl_stat_hi = 0.0, 0, 0
self.use_ema_teacher = use_ema_teacher
self.ema_decay = ema_decay
@@ -371,4 +379,7 @@ class OPSDTrainer(SFTTrainer):
"problem",
"solution",
+ # long-form verified reasoning trace; used when the teacher's privileged
+ # context is the trace rather than the concise solution (see data_collator)
+ "COT_Reason",
]
if self._signature_columns is None:
@@ -390,4 +401,5 @@ class OPSDTrainer(SFTTrainer):
top_k=None,
token_clip=None,
+ clip_mode="entry",
):
"""
@@ -460,6 +472,29 @@ class OPSDTrainer(SFTTrainer):
jsd = beta * kl_teacher + (1 - beta) * kl_student
- # Per-token clipping: cap each token's divergence value
- if token_clip is not None:
+ if clip_mode == "token":
+ # Direction-preserving per-token capping.
+ #
+ # `entry` mode (the released recipe) clamps every *vocabulary entry* at tau. An
+ # entry exceeds tau exactly when the teacher puts real mass on a token the student
+ # under-weights -- i.e. precisely the entries carrying the teacher's correction --
+ # and clamping zeroes their gradient. Measured on Qwen3-1.7B, the teacher's own
+ # argmax entry is clamped at ~11% of positions, and on high-divergence positions
+ # 50-80% of the teacher's probability mass is clamped away. The surviving gradient
+ # there points at the renormalized *tail* of the teacher, so the most informative
+ # positions are the ones whose target gets corrupted.
+ #
+ # Instead, keep the full-vocabulary divergence direction at every position and cap
+ # only its magnitude: scale each position by a detached factor tau/k_n when its
+ # divergence k_n exceeds tau. The loss value still equals min(k_n, tau), so the
+ # heavy tail of stylistic tokens can no longer dominate the batch -- which is the
+ # stated purpose of the clip -- but the gradient at every position remains the true
+ # forward-KL direction, just norm-limited.
+ per_token = jsd.sum(-1)
+ if token_clip is not None:
+ scale = (token_clip / per_token.detach().clamp_min(1e-6)).clamp(max=1.0)
+ per_token = per_token * scale
+ jsd = per_token
+ elif token_clip is not None:
+ # Per-entry clipping: cap each vocabulary entry's divergence contribution
jsd = jsd.clamp(max=token_clip)
@@ -742,5 +777,26 @@ class OPSDTrainer(SFTTrainer):
top_k=self.top_k_loss,
token_clip=self.jsd_token_clip,
+ clip_mode=self.jsd_clip_mode,
)
+ # Cheap running diagnostic: the true (unclipped) per-token forward KL between the
+ # privileged teacher and the student along the rollout, i.e. the raw signal size.
+ with torch.no_grad():
+ for r in range(student_logits_for_loss.shape[0]):
+ m = shifted_labels[r] != -100
+ if not bool(m.any()):
+ continue
+ tlp = F.log_softmax(teacher_logits_for_loss[r][m] / self.temperature, dim=-1)
+ slp = F.log_softmax(student_logits_for_loss[r][m] / self.temperature, dim=-1)
+ k = (tlp.exp() * (tlp - slp)).sum(-1).float()
+ self._kl_stat_sum += float(k.sum())
+ self._kl_stat_n += int(k.numel())
+ self._kl_stat_hi += int((k > (self.jsd_token_clip or 0.05)).sum())
+ del tlp, slp, k
+ if self._kl_stat_n >= 20000 and self.accelerator.is_main_process:
+ print(f"[OPSD kl] step {self.state.global_step}: mean per-token KL(T||S)="
+ f"{self._kl_stat_sum / self._kl_stat_n:.4f} over {self._kl_stat_n} tokens, "
+ f"frac above tau={self._kl_stat_hi / self._kl_stat_n:.3f}")
+ self._kl_stat_sum, self._kl_stat_n, self._kl_stat_hi = 0.0, 0, 0
+
del student_logits_for_loss, teacher_logits_for_loss
@@ -1385,6 +1441,14 @@ class OPSDTrainer(SFTTrainer):
)
- # Get batch-level student prompt length
- student_prompt_len = inputs["student_prompt_length"]
+ # Get batch-level student prompt length.
+ # With vLLM the prompts are decoded and re-tokenized inside the generation helper, so
+ # the prompt block width there is authoritative (a decode/re-encode round trip is not
+ # guaranteed to reproduce the collator's token count). Completions are always padded
+ # to exactly max_completion_length, so the prompt block is the remainder.
+ if self.use_vllm:
+ student_prompt_len = generated_ids.shape[1] - self.generation_config.max_new_tokens
+ inputs["student_prompt_length"] = student_prompt_len
+ else:
+ student_prompt_len = inputs["student_prompt_length"]
# Extract generation part (same slice for all examples since prompts are padded)
@@ -1407,10 +1471,9 @@ class OPSDTrainer(SFTTrainer):
inputs["teacher_attention_mask"] = teacher_attention_mask
- # Create labels for generation tokens
- # Mask prompt tokens (use per-example lengths for accurate masking)
+ # Create labels for generation tokens.
+ # Prompts are LEFT padded, so the whole prompt block (pads + prompt) is columns
+ # [0, student_prompt_len) for every example; mask it wholesale.
labels = generated_ids.clone()
- for i in range(labels.shape[0]):
- actual_prompt_len = inputs["student_prompt_lengths_per_example"][i].item()
- labels[i, :actual_prompt_len] = -100 # Mask actual prompt
+ labels[:, :student_prompt_len] = -100
if self.processing_class.pad_token_id is not None:
@@ -1419,4 +1482,29 @@ class OPSDTrainer(SFTTrainer):
inputs["labels"] = labels
+ # One-time alignment self-check: with left-padded prompts the supervised span must be
+ # exactly the sampled completion, and the prompt block must end flush against it (no
+ # pad tokens wedged in between, which would shift every completion token's RoPE
+ # position away from where it was sampled).
+ if not getattr(self, "_alignment_checked", False) and self.accelerator.is_main_process:
+ self._alignment_checked = True
+ pad_id = self.processing_class.pad_token_id
+ s_ids, t_ids = inputs["student_input_ids"], inputs["teacher_input_ids"]
+ t_len = inputs["teacher_prompt_length"]
+ print(f"\n[OPSD align] student_prompt_block={student_prompt_len} teacher_prompt_block={t_len}")
+ print(f"[OPSD align] student last prompt tokens (row0): "
+ f"{self.processing_class.decode(s_ids[0, student_prompt_len - 6:student_prompt_len])!r}")
+ print(f"[OPSD align] teacher last prompt tokens (row0): "
+ f"{self.processing_class.decode(t_ids[0, t_len - 6:t_len])!r}")
+ print(f"[OPSD align] supervised span (row0) starts: "
+ f"{self.processing_class.decode(s_ids[0][labels[0] != -100][:12])!r}")
+ gap_s = int((s_ids[:, :student_prompt_len] == pad_id).sum(1).max())
+ gap_t = int((t_ids[:, :t_len] == pad_id).sum(1).max())
+ trail_s = int((s_ids[:, student_prompt_len - 1] == pad_id).sum())
+ trail_t = int((t_ids[:, t_len - 1] == pad_id).sum())
+ print(f"[OPSD align] pads inside prompt blocks: student={gap_s} teacher={gap_t} "
+ f"(these are LEADING pads and are harmless)")
+ print(f"[OPSD align] rows whose prompt block ENDS in a pad (must be 0): "
+ f"student={trail_s} teacher={trail_t}\n")
+
# Log prompt and completion texts
self._textual_logs["prompt"].extend(gather_object(prompt_texts))
[stdout]
README.md | 4 +--
data_collator.py | 60 +++++++++++++++++++++++++++++--
opsd_train.py | 31 ++++++++++++++++
opsd_trainer.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 186 insertions(+), 15 deletions(-)
=== trainer diff:
diff --git a/opsd_trainer.py b/opsd_trainer.py
index c151bd7..c536d88 100644
--- a/opsd_trainer.py
+++ b/opsd_trainer.py
@@ -189,4 +189,12 @@ class OPSDTrainer(SFTTrainer):
self.top_k_loss = top_k_loss
self.jsd_token_clip = jsd_token_clip
+ # "entry" = released recipe (clamp each vocabulary entry at tau).
+ # "token" = clamp the magnitude of each position's full-vocabulary divergence instead,
+ # preserving its direction. See `generalized_jsd_loss`.
+ self.jsd_clip_mode = os.environ.get("OPSD_CLIP_MODE", "entry")
+ if self.jsd_clip_mode not in ("entry", "token"):
+ raise ValueError(f"OPSD_CLIP_MODE must be 'entry' or 'token', got {self.jsd_clip_mode}")
+ print(f"[OPSD] jsd clip mode: {self.jsd_clip_mode} (tau={self.jsd_token_clip})")
+ self._kl_stat_sum, self._kl_stat_n, self._kl_stat_hi = 0.0, 0, 0
self.use_ema_teacher = use_ema_teacher
self.ema_decay = ema_decay
@@ -371,4 +379,7 @@ class OPSDTrainer(SFTTrainer):
"problem",
"solution",
+ # long-form verified reasoning trace; used when the teacher's privileged
+ # context is the trace rather than the concise solution (see data_collator)
+ "COT_Reason",
]
if self._signature_columns is None:
@@ -390,4 +401,5 @@ class OPSDTrainer(SFTTrainer):
top_k=None,
token_clip=None,
+ clip_mode="entry",
):
"""
@@ -460,6 +472,29 @@ class OPSDTrainer(SFTTrainer):
jsd = beta * kl_teacher + (1 - beta) * kl_student
- # Per-token clipping: cap each token's divergence value
- if token_clip is not None:
+ if clip_mode == "token":
+ # Direction-preserving per-token capping.
+ #
+ # `entry` mode (the released recipe) clamps every *vocabulary entry* at tau. An
+ # entry exceeds tau exactly when the teacher puts real mass on a token the student
+ # under-weights -- i.e. precisely the entries carrying the teacher's correction --
+ # and clamping zeroes their gradient. Measured on Qwen3-1.7B, the teacher's own
+ # argmax entry is clamped at ~11% of positions, and on high-divergence positions
+ # 50-80% of the teacher's probability mass is clamped away. The surviving gradient
+ # there points at the renormalized *tail* of the teacher, so the most informative
+ # positions are the ones whose target gets corrupted.
+ #
+ # Instead, keep the full-vocabulary divergence direction at every position and cap
+ # only its magnitude: scale each position by a detached factor tau/k_n when its
+ # divergence k_n exceeds tau. The loss value still equals min(k_n, tau), so the
+ # heavy tail of stylistic tokens can no longer dominate the batch -- which is the
+ # stated purpose of the clip -- but the gradient at every position remains the true
+ # forward-KL direction, just norm-limited.
+ per_token = jsd.sum(-1)
+ if token_clip is not None:
+ scale = (token_clip / per_token.detach().clamp_min(1e-6)).clamp(max=1.0)
+ per_token = per_token * scale
+ jsd = per_token
+ elif token_clip is not None:
+ # Per-entry clipping: cap each vocabulary entry's divergence contribution
jsd = jsd.clamp(max=token_clip)
@@ -742,5 +777,26 @@ class OPSDTrainer(SFTTrainer):
top_k=self.top_k_loss,
token_clip=self.jsd_token_clip,
+ clip_mode=self.jsd_clip_mode,
)
+ # Cheap running diagnostic: the true (unclipped) per-token forward KL between the
+ # privileged teacher and the student along the rollout, i.e. the raw signal size.
+ with torch.no_grad():
+ for r in range(student_logits_for_loss.shape[0]):
+ m = shifted_labels[r] != -100
+ if not bool(m.any()):
+ continue
+ tlp = F.log_softmax(teacher_logits_for_loss[r][m] / self.temperature, dim=-1)
+ slp = F.log_softmax(student_logits_for_loss[r][m] / self.temperature, dim=-1)
+ k = (tlp.exp() * (tlp - slp)).sum(-1).float()
+ self._kl_stat_sum += float(k.sum())
+ self._kl_stat_n += int(k.numel())
+ self._kl_stat_hi += int((k > (self.jsd_token_clip or 0.05)).sum())
+ del tlp, slp, k
+ if self._kl_stat_n >= 20000 and self.accelerator.is_main_process:
+ print(f"[OPSD kl] step {self.state.global_step}: mean per-token KL(T||S)="
+ f"{self._kl_stat_sum / self._kl_stat_n:.4f} over {self._kl_stat_n} tokens, "
+ f"frac above tau={self._kl_stat_hi / self._kl_stat_n:.3f}")
+ self._kl_stat_sum, self._kl_stat_n, self._kl_stat_hi = 0.0, 0, 0
+
del student_logits_for_loss, teacher_logits_for_loss
@@ -1385,6 +1441,14 @@ class OPSDTrainer(SFTTrainer):
)
- # Get batch-level student prompt length
- student_prompt_len = inputs["student_prompt_length"]
+ # Get batch-level student prompt length.
+ # With vLLM the prompts are decoded and re-tokenized inside the generation helper, so
+ # the prompt block width there is authoritative (a decode/re-encode round trip is not
+ # guaranteed to reproduce the collator's token count). Completions are always padded
+ # to exactly max_completion_length, so the prompt block is the remainder.
+ if self.use_vllm:
+ student_prompt_len = generated_ids.shape[1] - self.generation_config.max_new_tokens
+ inputs["student_prompt_length"] = student_prompt_len
+ else:
+ student_prompt_len = inputs["student_prompt_length"]
# Extract generation part (same slice for all examples since prompts are padded)
@@ -1407,10 +1471,9 @@ class OPSDTrainer(SFTTrainer):
inputs["teacher_attention_mask"] = teacher_attention_mask
- # Create labels for generation tokens
- # Mask prompt tokens (use per-example lengths for accurate masking)
+ # Create labels for generation tokens.
+ # Prompts are LEFT padded, so the whole prompt block (pads + prompt) is columns
+ # [0, student_prompt_len) for every example; mask it wholesale.
labels = generated_ids.clone()
- for i in range(labels.shape[0]):
- actual_prompt_len = inputs["student_prompt_lengths_per_example"][i].item()
- labels[i, :actual_prompt_len] = -100 # Mask actual prompt
+ labels[:, :student_prompt_len] = -100
if self.processing_class.pad_token_id is not None:
@@ -1419,4 +1482,29 @@ class OPSDTrainer(SFTTrainer):
inputs["labels"] = labels
+ # One-time alignment self-check: with left-padded prompts the supervised span must be
+ # exactly the sampled completion, and the prompt block must end flush against it (no
+ # pad tokens wedged in between, which would shift every completion token's RoPE
+ # position away from where it was sampled).
+ if not getattr(self, "_alignment_checked", False) and self.accelerator.is_main_process:
+ self._alignment_checked = True
+ pad_id = self.processing_class.pad_token_id
+ s_ids, t_ids = inputs["student_input_ids"], inputs["teacher_input_ids"]
+ t_len = inputs["teacher_prompt_length"]
+ print(f"\n[OPSD align] student_prompt_block={student_prompt_len} teacher_prompt_block={t_len}")
+ print(f"[OPSD align] student last prompt tokens (row0): "
+ f"{self.processing_class.decode(s_ids[0, student_prompt_len - 6:student_prompt_len])!r}")
+ print(f"[OPSD align] teacher last prompt tokens (row0): "
+ f"{self.processing_class.decode(t_ids[0, t_len - 6:t_len])!r}")
+ print(f"[OPSD align] supervised span (row0) starts: "
+ f"{self.processing_class.decode(s_ids[0][labels[0] != -100][:12])!r}")
+ gap_s = int((s_ids[:, :student_prompt_len] == pad_id).sum(1).max())
+ gap_t = int((t_ids[:, :t_len] == pad_id).sum(1).max())
+ trail_s = int((s_ids[:, student_prompt_len - 1] == pad_id).sum())
+ trail_t = int((t_ids[:, t_len - 1] == pad_id).sum())
+ print(f"[OPSD align] pads inside prompt blocks: student={gap_s} teacher={gap_t} "
+ f"(these are LEADING pads and are harmless)")
+ print(f"[OPSD align] rows whose prompt block ENDS in a pad (must be 0): "
+ f"student={trail_s} teacher={trail_t}\n")
+
# Log prompt and completion texts
self._textual_logs["prompt"].extend(gather_object(prompt_texts))
[metadata] {"noOutputExpected": false}Verified: in "paper mode" my code is faithful (the original masked pads via the following labels[labels==pad]=-100 line, so my wholesale block-mask is equivalent under right padding).
Launching cycle 2 — the exact paper recipe plus data selection only, so it compares directly against the 5-run 54.22 anchor. Chaining train→eval to avoid idle GPU:
args
{
"command": "cd /workspace && export OPSD_PAD_SIDE=right OPSD_STUDENT_PROMPT=paper OPSD_TEACHER_CONTEXT=solution OPSD_CLIP_MODE=entry OPSD_DATA_SELECT=hard && SEED=42 OUT_DIR=/workspace/runs/DATA bash /workspace/train_capped.sh > /workspace/runs_DATA.log 2>&1 && echo \"TRAIN_OK\" && bash /workspace/dev_eval.sh /workspace/runs/DATA/capped_seed42/checkpoint-100 > /workspace/eval_DATA.log 2>&1; echo \"EXIT=$?\"",
"description": "Cycle 2: paper recipe + hard-data selection, train then eval",
"timeout": 3000000,
"run_in_background": true
}Bash result
Command running in background with ID: b10s9wnju. Output is being written to: /tmp/claude-0/-workspace/5fd2f3dc-5928-49f9-b3bb-a6ebc92f09aa/tasks/b10s9wnju.output. You will be notified when it completes. To check interim output, use Read on that file path.
Session cwd remains /workspace/repo; directory changes made by the backgrounded command do not apply to subsequent commands.
[metadata] {"noOutputExpected": false, "backgroundTaskId": "b10s9wnju", "backgroundCwdHint": "Session cwd remains /workspace/repo; directory changes made by the backgrounded command do not apply to subsequent commands."}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "sleep 115; python - <<'EOF'\nt=open('/workspace/runs_DATA.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n')]\nfor l in lines:\n if any(k in l for k in ['OPSD data','OPSD align','clip mode','padding_side','Traceback','Error:',\"'loss'\",'OPSD kl']):\n print(l[:300])\nEOF\ncat /workspace/.timer/remaining_secs",
"description": "Verify data filter and paper-mode settings took effect",
"timeout": 200000
}Bash result
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
17812
[stdout]
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD data] select=hard: 29434 -> 12048 rows (trace_len >= 3048)
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
17812
[metadata] {"noOutputExpected": false}Data filter active: 29434 → 12048 rows, and right-padding confirms paper mode. Waiting for train→eval (~33 min).
args
{
"command": "sleep 115; sleep 115; sleep 115; python - <<'EOF'\nt=open('/workspace/runs_DATA.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n')]\nsel=[l for l in lines if ('OPSD kl' in l or \"'loss'\" in l or 'Traceback' in l)]\nprint('\\n'.join(sel[-6:]))\nEOF",
"description": "Check KL signal under hard-data selection",
"timeout": 400000
}Bash result
[OPSD kl] step 26: mean per-token KL(T||S)=0.0979 over 20066 tokens, frac above tau=0.353
{'loss': -0.0025, 'grad_norm': 0.04853824898600578, 'learning_rate': 5e-06, 'on_policy_loss': -0.0025, 'epoch': 0.07}
[OPSD kl] step 29: mean per-token KL(T||S)=0.0986 over 24069 tokens, frac above tau=0.321
{'loss': -0.0031, 'grad_norm': 0.054779086261987686, 'learning_rate': 5e-06, 'on_policy_loss': -0.0031, 'epoch': 0.08}
{'loss': -0.0041, 'grad_norm': 0.05320427194237709, 'learning_rate': 5e-06, 'on_policy_loss': -0.0041, 'epoch': 0.08}
[OPSD kl] step 32: mean per-token KL(T||S)=0.1105 over 22827 tokens, frac above tau=0.399
[stdout]
[OPSD kl] step 26: mean per-token KL(T||S)=0.0979 over 20066 tokens, frac above tau=0.353
{'loss': -0.0025, 'grad_norm': 0.04853824898600578, 'learning_rate': 5e-06, 'on_policy_loss': -0.0025, 'epoch': 0.07}
[OPSD kl] step 29: mean per-token KL(T||S)=0.0986 over 24069 tokens, frac above tau=0.321
{'loss': -0.0031, 'grad_norm': 0.054779086261987686, 'learning_rate': 5e-06, 'on_policy_loss': -0.0031, 'epoch': 0.08}
{'loss': -0.0041, 'grad_norm': 0.05320427194237709, 'learning_rate': 5e-06, 'on_policy_loss': -0.0041, 'epoch': 0.08}
[OPSD kl] step 32: mean per-token KL(T||S)=0.1105 over 22827 tokens, frac above tau=0.399
[metadata] {"noOutputExpected": false}Mechanistically decisive: under the paper recipe the loss goes negative (−0.0025 → −0.0041) while the true KL stays flat at ~0.10 — the released objective does not reduce teacher/student divergence at all. Combined with cycle 1, that means OPSD's gain does not come from matching the teacher.
Let me work out what the entry-clipped gradient actually does while this finishes.
args
{
"command": "sleep 115; sleep 115; sleep 115; sleep 115; python - <<'EOF'\nfor f in ['/workspace/runs_DATA.log','/workspace/eval_DATA.log']:\n try: t=open(f,errors='ignore').read()\n except: print(f,\"missing\"); continue\n lines=[l.split('\\r')[-1] for l in t.split('\\n') if l.strip()]\n sel=[l for l in lines if any(k in l for k in ['OPSD kl','train_runtime','FINAL','Traceback','TRAIN_OK'])]\n print(\"==\",f); print('\\n'.join(l[:200] for l in sel[-4:]))\nEOF",
"description": "Check DATA cycle progress",
"timeout": 500000
}Bash result
== /workspace/runs_DATA.log
[OPSD kl] step 80: mean per-token KL(T||S)=0.1814 over 20480 tokens, frac above tau=0.479
[OPSD kl] step 83: mean per-token KL(T||S)=0.1874 over 21153 tokens, frac above tau=0.471
[OPSD kl] step 86: mean per-token KL(T||S)=0.1880 over 23412 tokens, frac above tau=0.444
[OPSD kl] step 89: mean per-token KL(T||S)=0.1991 over 20480 tokens, frac above tau=0.502
/workspace/eval_DATA.log missing
[stdout]
== /workspace/runs_DATA.log
[OPSD kl] step 80: mean per-token KL(T||S)=0.1814 over 20480 tokens, frac above tau=0.479
[OPSD kl] step 83: mean per-token KL(T||S)=0.1874 over 21153 tokens, frac above tau=0.471
[OPSD kl] step 86: mean per-token KL(T||S)=0.1880 over 23412 tokens, frac above tau=0.444
[OPSD kl] step 89: mean per-token KL(T||S)=0.1991 over 20480 tokens, frac above tau=0.502
/workspace/eval_DATA.log missing
[metadata] {"noOutputExpected": false}KL is rising over training (0.098 → 0.199): the released objective moves the student away from the teacher. That's an entropy-injection mechanism, not distillation. Before choosing a direction I need to know how much OPSD actually gains over base — let me check the paper rather than spend an eval.
args
{
"command": "python - <<'EOF'\nt=open('/workspace/paper.txt',errors='ignore').read()\nimport re\nfor m in re.finditer(r'AIME', t):\n s=max(0,m.start()-300); print(\"...\",t[s:m.start()+320].replace('\\n',' ')[:620]); print('---')\nEOF",
"description": "Find AIME baseline numbers in paper"
}Bash result
... delfamilyatthreescales:Qwen3-1.7B,Qwen3-4B,andQwen3-8B,usingtheinstruct-tunedversions.Fortrainingdata,weusethemathematicalreason-ingsubsetofOpenThoughts\050Guhaetal.,2025\051,samplingupto30Kproblem-solutionpairswithchain-of-thoughtreasoning.Weevaluateoncompetition-levelmathematicsbenchmarksincludingAIME2024,AIME2025,HMMT2025.Baselines.Wecompareagainsttwomethodstrainedonthesamedataset:\0501\051SFT,standardsupervised\002ne-tuningonexperttrajectories,whichcanbeseenasoff-policydistilla-tionfromamorepowerfulLLMthatgeneratedthereasoningtraces;\0502\051GRPO\050Shaoetal.,2024\051,grouprelativepolicyoptimizationwithbinary
---
... atthreescales:Qwen3-1.7B,Qwen3-4B,andQwen3-8B,usingtheinstruct-tunedversions.Fortrainingdata,weusethemathematicalreason-ingsubsetofOpenThoughts\050Guhaetal.,2025\051,samplingupto30Kproblem-solutionpairswithchain-of-thoughtreasoning.Weevaluateoncompetition-levelmathematicsbenchmarksincludingAIME2024,AIME2025,HMMT2025.Baselines.Wecompareagainsttwomethodstrainedonthesamedataset:\0501\051SFT,standardsupervised\002ne-tuningonexperttrajectories,whichcanbeseenasoff-policydistilla-tionfromamorepowerfulLLMthatgeneratedthereasoningtraces;\0502\051GRPO\050Shaoetal.,2024\051,grouprelativepolicyoptimizationwithbinaryoutcomere
---
... ndGRPOperformancetodecreaseforsometasksduetoentropycollapseinlatersteps.ForSFT,wetrainonthesamenumberofsamplesasOPSD.SFTperformancedegradesdueto\002ne-tuningonconcisereasoningsolutionsandreducesgenerationlengthattesttime,whereasOPSDtransformsthemintodenselearningsignalthroughrationalization. Method AIME24AIME25HMMT25Average Qwen3-8B Base\050Instruct\051 75.865.643.961.8 +SFT 72.364.242.959.8 +GRPO 76.468.946.764.0 +OPSD 77.8 70.8 45.8 64.8 Qwen3-4B Base\050Instruct\051 74.966.442.261.2 +SFT 70.262.343.458.6 +GRPO 75.668.144.462.7 +OPSD 76.4 68.3 46.1 63.6 Qwen3-1.7B Base\050Instruct\051 51.536.723.137.1 +SFT 48.4
---
... performancetodecreaseforsometasksduetoentropycollapseinlatersteps.ForSFT,wetrainonthesamenumberofsamplesasOPSD.SFTperformancedegradesdueto\002ne-tuningonconcisereasoningsolutionsandreducesgenerationlengthattesttime,whereasOPSDtransformsthemintodenselearningsignalthroughrationalization. Method AIME24AIME25HMMT25Average Qwen3-8B Base\050Instruct\051 75.865.643.961.8 +SFT 72.364.242.959.8 +GRPO 76.468.946.764.0 +OPSD 77.8 70.8 45.8 64.8 Qwen3-4B Base\050Instruct\051 74.966.442.261.2 +SFT 70.262.343.458.6 +GRPO 75.668.144.462.7 +OPSD 76.4 68.3 46.1 63.6 Qwen3-1.7B Base\050Instruct\051 51.536.723.137.1 +SFT 48.436.322
---
... studentgenerationlength,and\0505\051comparisonbetweenfull-vocabularylogitdistillationwithsampled-tokendistillation. 4.3.1.EFFECTOFDIVERGENCEOBJECTIVEAkeydesignchoiceinOPSDisthedivergenceusedforper-tokendistributionmatchingbetweentheprivilegedteacherandthestudent.WecompareforwardKL,reverseKL,andJSDonAIME25withQwen3-1.7BinTable3.Allob-jectivesareevaluatedunderthesamepointwiseclippingschemeforstability.ForwardKLconsistentlyyieldsthestrongestgains,improvingperformancefrom36.7to43.9atstep50andremainingabovethebaselineatstep100.Incontrast,reverseKLandJSDprovidelimitedornega-tiveimprovements.WethereforeadoptforwardKLina
---
... schemeforstability.ForwardKLconsistentlyyieldsthestrongestgains,improvingperformancefrom36.7to43.9atstep50andremainingabovethebaselineatstep100.Incontrast,reverseKLandJSDprovidelimitedornega-tiveimprovements.WethereforeadoptforwardKLinallremainingexperiments.Table3.ComparisonofdivergenceobjectivesonAIME25withQwen3-1.7B.WereportAvg@12atdifferenttrainingsteps.For-wardKLsigni\002cantlyimprovesperformanceoverthebasemodel,whilereverseKLandJSD\050\014=0:5\051showlimitedornegativegains. MethodBaseStep50Step100 ForwardKL\050KL\050p T kp S \051\05136.743.941.1 ReverseKL\050KL\050p S kp T \051\05136.737.535.0 JSD\050\014=0
---
... proportionatelylargevalues.Thismotivatesouruseofpointwiseclippingtocontrolsuchheavy-tailedcontributions.Empirically,thiscon\002gurationachievesthebestdownstreamperformance.WethereforeadopttheTM-offstudent/TM-onteachercon\002guration.Figure4.EffectofPer-TokenpointwiseKLClippingonQwen3-1.7BevaluatedonAIME24.Clippingpreventsperformancecol-lapse. 4.3.3.EFFECTOFPER-TOKENPOINTWISECLIPPINGAsshowninTable5,stylistictokenscanexhibithigherKLdivergencethanmath-relatedtokens,causingthemtodominatethetrainingsignal.Wemitigatethisissueus-ingper-tokenpointwiseclipping.AsshowninFigure4forQwen3-1.7B,clippingstabilizestrainingandpre
---
... ationalcostandmayintroducenoisyoruninformativecontinuations.Tostudythistrade-off,weconductanablationonQwen3-1.7Bbyvaryingthegenerationlengthofon-policysampledstu-dentresponsesamong1024and4096tokensandusefull-Figure5.EffectofGenerationLengthonQwen3-1.7B.Wecom-parestudentgenerationlengthof1024vs4096onAIME25andAIME24.vocabularylogitdistillation.AsshowninFigure5,in-creasingthegenerationlengthdoesnotleadtoconsistentimprovementsacrosseithertask.Weattributethistoearlytokensbeingmorecriticalforlearning:asthestudentgen-erationgrowslonger,latertokensbecomeincreasinglypre-dictabletotheteacherwhenconditionedonasuf\002ciently
---
... standmayintroducenoisyoruninformativecontinuations.Tostudythistrade-off,weconductanablationonQwen3-1.7Bbyvaryingthegenerationlengthofon-policysampledstu-dentresponsesamong1024and4096tokensandusefull-Figure5.EffectofGenerationLengthonQwen3-1.7B.Wecom-parestudentgenerationlengthof1024vs4096onAIME25andAIME24.vocabularylogitdistillation.AsshowninFigure5,in-creasingthegenerationlengthdoesnotleadtoconsistentimprovementsacrosseithertask.Weattributethistoearlytokensbeingmorecriticalforlearning:asthestudentgen-erationgrowslonger,latertokensbecomeincreasinglypre-dictabletotheteacherwhenconditionedonasuf\002cientlylongstude
---
... eduetostoringvocabulary-sizedlogitsateveryposition,indicatingatrade-offbetweenper-formanceandef\002ciency. 8 ===PAGE=== On-PolicySelf-DistillationforLargeLanguageModelsTable4.AblationondivergencecomputationstrategiesforOPSDonQwen3-4Bwith2048generationlengthfordistillation.Wereportpass@8accuracyonAIME25andHMMT25.Full-distributionobjectives\050logitdistillation\051outperformsampled-tokenobjectives. MethodVariant AIME25HMMT25 OPSDw/Full-vocabularylogitdistillation\050Agarwaletal.,2024\051 84.160.0 OPSDw/Sampled-tokendistillation\050Lu&Lab,2025\051 82.157.3 5.RelatedWorkLLMSelf-Training.Ourworkconnectstoalineofre-
---
... === On-PolicySelf-DistillationforLargeLanguageModelsTable4.AblationondivergencecomputationstrategiesforOPSDonQwen3-4Bwith2048generationlengthfordistillation.Wereportpass@8accuracyonAIME25andHMMT25.Full-distributionobjectives\050logitdistillation\051outperformsampled-tokenobjectives. MethodVariant AIME25HMMT25 OPSDw/Full-vocabularylogitdistillation\050Agarwaletal.,2024\051 84.160.0 OPSDw/Sampled-tokendistillation\050Lu&Lab,2025\051 82.157.3 5.RelatedWorkLLMSelf-Training.Ourworkconnectstoalineofre-searchshowingthatLLMscanimprovebygeneratingandexploitingtheirownsupervisionsignals\050Allen-Zhu&Li,2020;Xuetal.,2024b
---
... pledtrajectoriesareincorrect,thelearningsignalvanishesentirely.Incontrast,OPSDprovidesatoken-levelrewardr nateveryposition,enabling\002ne-grainedcreditassignmentevenwhenthe\002nalansweriswrong. 15 ===PAGE=== ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 Accuracy (%) AIME24 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 0 25 50 75 100 Gradient Update Steps 22 24 26 28 30 32 Avg@12 Accuracy (%) HMMT25 0 10 20 T o k e n s G e n e r a t e d ( × 1 0 6 ) 37 38 39 40 41 42 Avg@12 Accuracy (%) Average 10 30 50 70 90 Gradient Update Steps 0.0 0.2 0
---
... satoken-levelrewardr nateveryposition,enabling\002ne-grainedcreditassignmentevenwhenthe\002nalansweriswrong. 15 ===PAGE=== ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 Accuracy (%) AIME24 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 0 25 50 75 100 Gradient Update Steps 22 24 26 28 30 32 Avg@12 Accuracy (%) HMMT25 0 10 20 T o k e n s G e n e r a t e d ( × 1 0 6 ) 37 38 39 40 41 42 Avg@12 Accuracy (%) Average 10 30 50 70 90 Gradient Update Steps 0.0 0.2 0.4 0.6 0.8 1.0 Fraction of Batches Zero Reward Std Frac. of GRPO GRPO OPSD ===PAGE==
---
... 25 0 10 20 T o k e n s G e n e r a t e d ( × 1 0 6 ) 37 38 39 40 41 42 Avg@12 Accuracy (%) Average 10 30 50 70 90 Gradient Update Steps 0.0 0.2 0.4 0.6 0.8 1.0 Fraction of Batches Zero Reward Std Frac. of GRPO GRPO OPSD ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 AIME24 Accuracy (%) w/o per-token KL Clipping w/ per-token KL Clipping ===PAGE=== 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 0 25 50 75 100 Gradient Update Steps 50 52 54 56 58 60 Avg@12 Accuracy (%) AIME24 (Qwen3-1.7B) Gen Length
---
... Fraction of Batches Zero Reward Std Frac. of GRPO GRPO OPSD ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 AIME24 Accuracy (%) w/o per-token KL Clipping w/ per-token KL Clipping ===PAGE=== 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 0 25 50 75 100 Gradient Update Steps 50 52 54 56 58 60 Avg@12 Accuracy (%) AIME24 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 ===PAGE=== c<http://www.ams.org>003.002Copyright \050c\051 1997, 2009 American Mathematical Society \050<http://www.ams.org>\051, with Rese
---
... 58 Avg@12 AIME24 Accuracy (%) w/o per-token KL Clipping w/ per-token KL Clipping ===PAGE=== 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 0 25 50 75 100 Gradient Update Steps 50 52 54 56 58 60 Avg@12 Accuracy (%) AIME24 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 ===PAGE=== c<http://www.ams.org>003.002Copyright \050c\051 1997, 2009 American Mathematical Society \050<http://www.ams.org>\051, with Reserved Font Name CMR7.CMR7Computer ModernMedium"!i©Á K5>Å£ü d,J^«µ
,ðÏ]^¢ zçÒ* òµ+kh©ÓjOwè½Üqì ðk&,æ_?ÌþÉc"
---
[stdout]
... delfamilyatthreescales:Qwen3-1.7B,Qwen3-4B,andQwen3-8B,usingtheinstruct-tunedversions.Fortrainingdata,weusethemathematicalreason-ingsubsetofOpenThoughts\050Guhaetal.,2025\051,samplingupto30Kproblem-solutionpairswithchain-of-thoughtreasoning.Weevaluateoncompetition-levelmathematicsbenchmarksincludingAIME2024,AIME2025,HMMT2025.Baselines.Wecompareagainsttwomethodstrainedonthesamedataset:\0501\051SFT,standardsupervised\002ne-tuningonexperttrajectories,whichcanbeseenasoff-policydistilla-tionfromamorepowerfulLLMthatgeneratedthereasoningtraces;\0502\051GRPO\050Shaoetal.,2024\051,grouprelativepolicyoptimizationwithbinary
---
... atthreescales:Qwen3-1.7B,Qwen3-4B,andQwen3-8B,usingtheinstruct-tunedversions.Fortrainingdata,weusethemathematicalreason-ingsubsetofOpenThoughts\050Guhaetal.,2025\051,samplingupto30Kproblem-solutionpairswithchain-of-thoughtreasoning.Weevaluateoncompetition-levelmathematicsbenchmarksincludingAIME2024,AIME2025,HMMT2025.Baselines.Wecompareagainsttwomethodstrainedonthesamedataset:\0501\051SFT,standardsupervised\002ne-tuningonexperttrajectories,whichcanbeseenasoff-policydistilla-tionfromamorepowerfulLLMthatgeneratedthereasoningtraces;\0502\051GRPO\050Shaoetal.,2024\051,grouprelativepolicyoptimizationwithbinaryoutcomere
---
... ndGRPOperformancetodecreaseforsometasksduetoentropycollapseinlatersteps.ForSFT,wetrainonthesamenumberofsamplesasOPSD.SFTperformancedegradesdueto\002ne-tuningonconcisereasoningsolutionsandreducesgenerationlengthattesttime,whereasOPSDtransformsthemintodenselearningsignalthroughrationalization. Method AIME24AIME25HMMT25Average Qwen3-8B Base\050Instruct\051 75.865.643.961.8 +SFT 72.364.242.959.8 +GRPO 76.468.946.764.0 +OPSD 77.8 70.8 45.8 64.8 Qwen3-4B Base\050Instruct\051 74.966.442.261.2 +SFT 70.262.343.458.6 +GRPO 75.668.144.462.7 +OPSD 76.4 68.3 46.1 63.6 Qwen3-1.7B Base\050Instruct\051 51.536.723.137.1 +SFT 48.4
---
... performancetodecreaseforsometasksduetoentropycollapseinlatersteps.ForSFT,wetrainonthesamenumberofsamplesasOPSD.SFTperformancedegradesdueto\002ne-tuningonconcisereasoningsolutionsandreducesgenerationlengthattesttime,whereasOPSDtransformsthemintodenselearningsignalthroughrationalization. Method AIME24AIME25HMMT25Average Qwen3-8B Base\050Instruct\051 75.865.643.961.8 +SFT 72.364.242.959.8 +GRPO 76.468.946.764.0 +OPSD 77.8 70.8 45.8 64.8 Qwen3-4B Base\050Instruct\051 74.966.442.261.2 +SFT 70.262.343.458.6 +GRPO 75.668.144.462.7 +OPSD 76.4 68.3 46.1 63.6 Qwen3-1.7B Base\050Instruct\051 51.536.723.137.1 +SFT 48.436.322
---
... studentgenerationlength,and\0505\051comparisonbetweenfull-vocabularylogitdistillationwithsampled-tokendistillation. 4.3.1.EFFECTOFDIVERGENCEOBJECTIVEAkeydesignchoiceinOPSDisthedivergenceusedforper-tokendistributionmatchingbetweentheprivilegedteacherandthestudent.WecompareforwardKL,reverseKL,andJSDonAIME25withQwen3-1.7BinTable3.Allob-jectivesareevaluatedunderthesamepointwiseclippingschemeforstability.ForwardKLconsistentlyyieldsthestrongestgains,improvingperformancefrom36.7to43.9atstep50andremainingabovethebaselineatstep100.Incontrast,reverseKLandJSDprovidelimitedornega-tiveimprovements.WethereforeadoptforwardKLina
---
... schemeforstability.ForwardKLconsistentlyyieldsthestrongestgains,improvingperformancefrom36.7to43.9atstep50andremainingabovethebaselineatstep100.Incontrast,reverseKLandJSDprovidelimitedornega-tiveimprovements.WethereforeadoptforwardKLinallremainingexperiments.Table3.ComparisonofdivergenceobjectivesonAIME25withQwen3-1.7B.WereportAvg@12atdifferenttrainingsteps.For-wardKLsigni\002cantlyimprovesperformanceoverthebasemodel,whilereverseKLandJSD\050\014=0:5\051showlimitedornegativegains. MethodBaseStep50Step100 ForwardKL\050KL\050p T kp S \051\05136.743.941.1 ReverseKL\050KL\050p S kp T \051\05136.737.535.0 JSD\050\014=0
---
... proportionatelylargevalues.Thismotivatesouruseofpointwiseclippingtocontrolsuchheavy-tailedcontributions.Empirically,thiscon\002gurationachievesthebestdownstreamperformance.WethereforeadopttheTM-offstudent/TM-onteachercon\002guration.Figure4.EffectofPer-TokenpointwiseKLClippingonQwen3-1.7BevaluatedonAIME24.Clippingpreventsperformancecol-lapse. 4.3.3.EFFECTOFPER-TOKENPOINTWISECLIPPINGAsshowninTable5,stylistictokenscanexhibithigherKLdivergencethanmath-relatedtokens,causingthemtodominatethetrainingsignal.Wemitigatethisissueus-ingper-tokenpointwiseclipping.AsshowninFigure4forQwen3-1.7B,clippingstabilizestrainingandpre
---
... ationalcostandmayintroducenoisyoruninformativecontinuations.Tostudythistrade-off,weconductanablationonQwen3-1.7Bbyvaryingthegenerationlengthofon-policysampledstu-dentresponsesamong1024and4096tokensandusefull-Figure5.EffectofGenerationLengthonQwen3-1.7B.Wecom-parestudentgenerationlengthof1024vs4096onAIME25andAIME24.vocabularylogitdistillation.AsshowninFigure5,in-creasingthegenerationlengthdoesnotleadtoconsistentimprovementsacrosseithertask.Weattributethistoearlytokensbeingmorecriticalforlearning:asthestudentgen-erationgrowslonger,latertokensbecomeincreasinglypre-dictabletotheteacherwhenconditionedonasuf\002ciently
---
... standmayintroducenoisyoruninformativecontinuations.Tostudythistrade-off,weconductanablationonQwen3-1.7Bbyvaryingthegenerationlengthofon-policysampledstu-dentresponsesamong1024and4096tokensandusefull-Figure5.EffectofGenerationLengthonQwen3-1.7B.Wecom-parestudentgenerationlengthof1024vs4096onAIME25andAIME24.vocabularylogitdistillation.AsshowninFigure5,in-creasingthegenerationlengthdoesnotleadtoconsistentimprovementsacrosseithertask.Weattributethistoearlytokensbeingmorecriticalforlearning:asthestudentgen-erationgrowslonger,latertokensbecomeincreasinglypre-dictabletotheteacherwhenconditionedonasuf\002cientlylongstude
---
... eduetostoringvocabulary-sizedlogitsateveryposition,indicatingatrade-offbetweenper-formanceandef\002ciency. 8 ===PAGE=== On-PolicySelf-DistillationforLargeLanguageModelsTable4.AblationondivergencecomputationstrategiesforOPSDonQwen3-4Bwith2048generationlengthfordistillation.Wereportpass@8accuracyonAIME25andHMMT25.Full-distributionobjectives\050logitdistillation\051outperformsampled-tokenobjectives. MethodVariant AIME25HMMT25 OPSDw/Full-vocabularylogitdistillation\050Agarwaletal.,2024\051 84.160.0 OPSDw/Sampled-tokendistillation\050Lu&Lab,2025\051 82.157.3 5.RelatedWorkLLMSelf-Training.Ourworkconnectstoalineofre-
---
... === On-PolicySelf-DistillationforLargeLanguageModelsTable4.AblationondivergencecomputationstrategiesforOPSDonQwen3-4Bwith2048generationlengthfordistillation.Wereportpass@8accuracyonAIME25andHMMT25.Full-distributionobjectives\050logitdistillation\051outperformsampled-tokenobjectives. MethodVariant AIME25HMMT25 OPSDw/Full-vocabularylogitdistillation\050Agarwaletal.,2024\051 84.160.0 OPSDw/Sampled-tokendistillation\050Lu&Lab,2025\051 82.157.3 5.RelatedWorkLLMSelf-Training.Ourworkconnectstoalineofre-searchshowingthatLLMscanimprovebygeneratingandexploitingtheirownsupervisionsignals\050Allen-Zhu&Li,2020;Xuetal.,2024b
---
... pledtrajectoriesareincorrect,thelearningsignalvanishesentirely.Incontrast,OPSDprovidesatoken-levelrewardr nateveryposition,enabling\002ne-grainedcreditassignmentevenwhenthe\002nalansweriswrong. 15 ===PAGE=== ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 Accuracy (%) AIME24 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 0 25 50 75 100 Gradient Update Steps 22 24 26 28 30 32 Avg@12 Accuracy (%) HMMT25 0 10 20 T o k e n s G e n e r a t e d ( × 1 0 6 ) 37 38 39 40 41 42 Avg@12 Accuracy (%) Average 10 30 50 70 90 Gradient Update Steps 0.0 0.2 0
---
... satoken-levelrewardr nateveryposition,enabling\002ne-grainedcreditassignmentevenwhenthe\002nalansweriswrong. 15 ===PAGE=== ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 Accuracy (%) AIME24 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 0 25 50 75 100 Gradient Update Steps 22 24 26 28 30 32 Avg@12 Accuracy (%) HMMT25 0 10 20 T o k e n s G e n e r a t e d ( × 1 0 6 ) 37 38 39 40 41 42 Avg@12 Accuracy (%) Average 10 30 50 70 90 Gradient Update Steps 0.0 0.2 0.4 0.6 0.8 1.0 Fraction of Batches Zero Reward Std Frac. of GRPO GRPO OPSD ===PAGE==
---
... 25 0 10 20 T o k e n s G e n e r a t e d ( × 1 0 6 ) 37 38 39 40 41 42 Avg@12 Accuracy (%) Average 10 30 50 70 90 Gradient Update Steps 0.0 0.2 0.4 0.6 0.8 1.0 Fraction of Batches Zero Reward Std Frac. of GRPO GRPO OPSD ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 AIME24 Accuracy (%) w/o per-token KL Clipping w/ per-token KL Clipping ===PAGE=== 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 0 25 50 75 100 Gradient Update Steps 50 52 54 56 58 60 Avg@12 Accuracy (%) AIME24 (Qwen3-1.7B) Gen Length
---
... Fraction of Batches Zero Reward Std Frac. of GRPO GRPO OPSD ===PAGE=== 0 25 50 75 100 Gradient Update Steps 48 50 52 54 56 58 Avg@12 AIME24 Accuracy (%) w/o per-token KL Clipping w/ per-token KL Clipping ===PAGE=== 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 0 25 50 75 100 Gradient Update Steps 50 52 54 56 58 60 Avg@12 Accuracy (%) AIME24 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 ===PAGE=== c<http://www.ams.org>003.002Copyright \050c\051 1997, 2009 American Mathematical Society \050<http://www.ams.org>\051, with Rese
---
... 58 Avg@12 AIME24 Accuracy (%) w/o per-token KL Clipping w/ per-token KL Clipping ===PAGE=== 0 25 50 75 100 Gradient Update Steps 34 36 38 40 42 44 46 Avg@12 Accuracy (%) AIME25 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 0 25 50 75 100 Gradient Update Steps 50 52 54 56 58 60 Avg@12 Accuracy (%) AIME24 (Qwen3-1.7B) Gen Length 4096 Gen Length 1024 ===PAGE=== c<http://www.ams.org>003.002Copyright \050c\051 1997, 2009 American Mathematical Society \050<http://www.ams.org>\051, with Reserved Font Name CMR7.CMR7Computer ModernMedium"!i©Á K5>Å£ü d,J^«µ
,ðÏ]^¢ zçÒ* òµ+kh©ÓjOwè½Üqì ðk&,æ_?ÌþÉc"
---
[metadata] {"noOutputExpected": false}Two decisive facts from the paper: Qwen3-1.7B base AIME24 = 51.5 (so the 54.22 anchor is only ~+2.7 over base), and the forward-KL ablation reports 43.9 at step 50 but 41.1 at step 100 — the recipe overshoots its own optimum. Let me get the full 1.7B row.
args
{
"command": "python - <<'EOF'\nt=open('/workspace/paper.txt',errors='ignore').read()\ni=t.find('Qwen3-1.7B Base')\nprint(repr(t[i-40:i+400]))\nprint(\"=== clipping fig text ===\")\nj=t.find('Clippingpreventsperformancecol')\nprint(t[j-1200:j+200])\nEOF",
"description": "Extract 1.7B results row"
}Bash result
''
=== clipping fig text ===
generationmodes:ThinkingModeon\050TM-on\051,inwhichthemodelproducesself-re\003ectivechain-of-thoughttokens,andThinkingModeoff\050TM-off\051,inwhichitgeneratesre-sponsesdirectly.Todeterminewhichcombinationyields7
===PAGE===
On-PolicySelf-DistillationforLargeLanguageModelsthemosteffectivelearningsignal,weanalyzetheforwardKLdivergenceKL\050p
T
kp
S
\051acrossallfourstudent/teachermodepairings,categorizingtokensintothreegroups:math\050numerals,operators,andmathematicalkeywords\051,style\050reasoningconnectives\051,andother.Table5reportsthemeanper-tokenKLwithineachcategory.Acrossallmodelsizes,theTM-offstudentpairedwithaTM-onteacheryieldsthelargestKLonmathtokens,in-dicatingstrongersupervisiononmathematicallyrelevanttokens.ThereportedKLvaluescorrespondtotheexpecteddivergenceoverthevocabularyateachposition;asshowninTable5,thisexpectationishighlyskewed,withstylistictokenscontributingdisproportionatelylargevalues.Thismotivatesouruseofpointwiseclippingtocontrolsuchheavy-tailedcontributions.Empirically,thiscon\002gurationachievesthebestdownstreamperformance.WethereforeadopttheTM-offstudent/TM-onteachercon\002guration.Figure4.EffectofPer-TokenpointwiseKLClippingonQwen3-1.7BevaluatedonAIME24.Clippingpreventsperformancecol-lapse.
4.3.3.EFFECTOFPER-TOKENPOINTWISECLIPPINGAsshowninTable5,stylistictokenscanexhibithigherKLdivergencethanmath-relatedtokens,causingthemtodominatethetrainingsignal.W
[stdout]
''
=== clipping fig text ===
generationmodes:ThinkingModeon\050TM-on\051,inwhichthemodelproducesself-re\003ectivechain-of-thoughttokens,andThinkingModeoff\050TM-off\051,inwhichitgeneratesre-sponsesdirectly.Todeterminewhichcombinationyields7
===PAGE===
On-PolicySelf-DistillationforLargeLanguageModelsthemosteffectivelearningsignal,weanalyzetheforwardKLdivergenceKL\050p
T
kp
S
\051acrossallfourstudent/teachermodepairings,categorizingtokensintothreegroups:math\050numerals,operators,andmathematicalkeywords\051,style\050reasoningconnectives\051,andother.Table5reportsthemeanper-tokenKLwithineachcategory.Acrossallmodelsizes,theTM-offstudentpairedwithaTM-onteacheryieldsthelargestKLonmathtokens,in-dicatingstrongersupervisiononmathematicallyrelevanttokens.ThereportedKLvaluescorrespondtotheexpecteddivergenceoverthevocabularyateachposition;asshowninTable5,thisexpectationishighlyskewed,withstylistictokenscontributingdisproportionatelylargevalues.Thismotivatesouruseofpointwiseclippingtocontrolsuchheavy-tailedcontributions.Empirically,thiscon\002gurationachievesthebestdownstreamperformance.WethereforeadopttheTM-offstudent/TM-onteachercon\002guration.Figure4.EffectofPer-TokenpointwiseKLClippingonQwen3-1.7BevaluatedonAIME24.Clippingpreventsperformancecol-lapse.
4.3.3.EFFECTOFPER-TOKENPOINTWISECLIPPINGAsshowninTable5,stylistictokenscanexhibithigherKLdivergencethanmath-relatedtokens,causingthemtodominatethetrainingsignal.W
[metadata] {"noOutputExpected": false}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "python - <<'EOF'\nt=open('/workspace/paper.txt',errors='ignore').read()\ni=t.find('51.536.723.137.1')\nprint(repr(t[i:i+260]))\nEOF",
"description": "Get 1.7B OPSD result"
}Bash result
'51.536.723.137.1\n+SFT\n48.436.322.735.8\n+GRPO\n51.138.323.737.7\n+OPSD\n57.2\n43.9\n29.2\n43.4esizethatearliertokensmaycontributemoretoeffectivedistillationastheycouldrepresentmorecriticalbranchingpointsinthereasoningprocess.AsshowninFigure3,OPSDachieveshighertokenle'
[stdout]
'51.536.723.137.1\n+SFT\n48.436.322.735.8\n+GRPO\n51.138.323.737.7\n+OPSD\n57.2\n43.9\n29.2\n43.4esizethatearliertokensmaycontributemoretoeffectivedistillationastheycouldrepresentmorecriticalbranchingpointsinthereasoningprocess.AsshowninFigure3,OPSDachieveshighertokenle'
[metadata] {"noOutputExpected": false}Paper's headline Qwen3-1.7B AIME24 = 57.2, and its AIME25 headline (43.9) is exactly the step-50 entry from Table 3 — so the paper reports a step-50 checkpoint while the capped scorer evaluates step 100. The ~3-point gap between 57.2 and our 54.22 anchor is the overshoot.
That gives a well-motivated, whitelisted fix: decay the LR so 100 steps of fresh on-policy data produce step-50-scale drift. Let me check cycle 2's result.
args
{
"command": "sleep 115; sleep 115; python - <<'EOF'\nfor f in ['/workspace/runs_DATA.log','/workspace/eval_DATA.log']:\n try: t=open(f,errors='ignore').read()\n except Exception as e: print(f,\"MISSING\"); continue\n lines=[l.split('\\r')[-1] for l in t.split('\\n') if l.strip()]\n sel=[l for l in lines if any(k in l for k in ['train_runtime','FINAL','Traceback','Processed prompts: 100'])]\n print(\"==\",f); print('\\n'.join(l[:170] for l in sel[-4:]))\nEOF\ncat /workspace/.timer/remaining_secs",
"description": "Check DATA eval result",
"timeout": 300000
}Bash result
== /workspace/runs_DATA.log
{'train_runtime': 908.1918, 'train_samples_per_second': 3.523, 'train_steps_per_second': 0.11, 'train_loss': -0.007925688028335572, 'epoch': 0.27}
== /workspace/eval_DATA.log
16640
[stdout]
== /workspace/runs_DATA.log
{'train_runtime': 908.1918, 'train_samples_per_second': 3.523, 'train_steps_per_second': 0.11, 'train_loss': -0.007925688028335572, 'epoch': 0.27}
== /workspace/eval_DATA.log
16640
[metadata] {"noOutputExpected": false}Timer: 4.6 h. Writing the recipe for cycle 3:
args
{
"file_path": "/workspace/submission/recipe.env",
"content": "# OPSD method recipe.\n# lr_scheduler_type: the released capped recipe holds the LR constant for all 100\n# steps. The paper's own reported Qwen3-1.7B numbers are its step-50 checkpoint\n# (its AIME25 headline 43.9 is exactly the step-50 entry of its forward-KL\n# ablation, which falls to 41.1 by step 100), i.e. the recipe overshoots its own\n# optimum well before the budget is spent. With the budget fixed at 100 steps and\n# no checkpoint selection, annealing the LR is the way to spend all 100 steps of\n# fresh on-policy data while accumulating only step-50-scale parameter drift.\nlr_scheduler_type=cosine\n"
}Write result · error
<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>
[error] tool reported failureencrypted chain-of-thought — recorded, not exposed
args
{
"command": "ls -la /workspace/submission/; echo \"---content---\"; cat /workspace/submission/recipe.env 2>&1",
"description": "Check existing recipe.env"
}Bash result
total 2
drwxr-xr-x 1 root root 32 Aug 2 17:15 .
drwxr-xr-x 1 root root 400 Aug 3 05:07 ..
-rw-r--r-- 1 root root 624 Aug 2 17:15 recipe.env
---content---
# OPSD method recipe — whitelisted hyper-parameter overrides (KEY=VALUE per line).
# Empty/all-commented == the OPSD baseline recipe on top of your /workspace/repo
# code changes. The scorer re-trains with THIS file + your repo for 100 steps.
# Budget knobs (max_steps, epochs, batch, gradient_accumulation, num_processes,
# model) are FIXED by the scorer and ignored here.
#
# learning_rate=5e-6
# lora_r=64
# lora_alpha=128
# beta=0
# jsd_token_clip=0.05
# temperature=1.1
# lmbda=1
# max_completion_length=1024 # <= 4096
# fixed_teacher=true
# use_ema_teacher=false
# reason_first=false
# teacher_thinking=false
[stdout]
total 2
drwxr-xr-x 1 root root 32 Aug 2 17:15 .
drwxr-xr-x 1 root root 400 Aug 3 05:07 ..
-rw-r--r-- 1 root root 624 Aug 2 17:15 recipe.env
---content---
# OPSD method recipe — whitelisted hyper-parameter overrides (KEY=VALUE per line).
# Empty/all-commented == the OPSD baseline recipe on top of your /workspace/repo
# code changes. The scorer re-trains with THIS file + your repo for 100 steps.
# Budget knobs (max_steps, epochs, batch, gradient_accumulation, num_processes,
# model) are FIXED by the scorer and ignored here.
#
# learning_rate=5e-6
# lora_r=64
# lora_alpha=128
# beta=0
# jsd_token_clip=0.05
# temperature=1.1
# lmbda=1
# max_completion_length=1024 # <= 4096
# fixed_teacher=true
# use_ema_teacher=false
# reason_first=false
# teacher_thinking=false
[metadata] {"noOutputExpected": false}encrypted chain-of-thought — recorded, not exposed
args
{
"file_path": "/workspace/submission/recipe.env"
}Read result
1 # OPSD method recipe — whitelisted hyper-parameter overrides (KEY=VALUE per line).
2 # Empty/all-commented == the OPSD baseline recipe on top of your /workspace/repo
3 # code changes. The scorer re-trains with THIS file + your repo for 100 steps.
4 # Budget knobs (max_steps, epochs, batch, gradient_accumulation, num_processes,
5 # model) are FIXED by the scorer and ignored here.
6 #
7 # learning_rate=5e-6
8 # lora_r=64
9 # lora_alpha=128
10 # beta=0
11 # jsd_token_clip=0.05
12 # temperature=1.1
13 # lmbda=1
14 # max_completion_length=1024 # <= 4096
15 # fixed_teacher=true
16 # use_ema_teacher=false
17 # reason_first=false
18 # teacher_thinking=false
19
[metadata] {"type": "text", "file": {"filePath": "/workspace/submission/recipe.env", "content": "# OPSD method recipe — whitelisted hyper-parameter overrides (KEY=VALUE per line).\n# Empty/all-commented == the OPSD baseline recipe on top of your /workspace/repo\n# code changes. The scorer re-trains with THIS file + your repo for 100 steps.\n# Budget knobs (max_steps, epochs, batch, gradient_accumulation, num_processes,\n# model) are FIXED by the scorer and ignored here.\n#\n# learning_rate=5e-6\n# lora_r=64\n# lora_alpha=128\n# beta=0\n# jsd_token_clip=0.05\n# temperature=1.1\n# lmbda=1\n# max_completion_length=1024 # <= 4096\n# fixed_teacher=true\n# use_ema_teacher=false\n# reason_first=false\n# teacher_thinking=false\n", "numLines": 19, "startLine": 1, "totalLines": 19}}encrypted chain-of-thought — recorded, not exposed
args
{
"replace_all": false,
"file_path": "/workspace/submission/recipe.env",
"old_string": "# teacher_thinking=false\n",
"new_string": "# teacher_thinking=false\n\n# ---------------------------------------------------------------------------\n# The released capped recipe holds the LR constant for all 100 steps. The paper's\n# own reported Qwen3-1.7B numbers are a step-50 checkpoint -- its AIME25 headline\n# (43.9) is exactly the step-50 entry of its forward-KL ablation, which falls to\n# 41.1 by step 100 -- so the recipe overshoots its own optimum well before the\n# budget is spent. With the budget fixed at 100 steps and no checkpoint\n# selection available, annealing the LR is how to spend all 100 steps of fresh\n# on-policy data while accumulating only step-50-scale parameter drift.\n# ---------------------------------------------------------------------------\nlr_scheduler_type=cosine\n"
}Edit result
The file /workspace/submission/recipe.env has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/workspace/submission/recipe.env", "oldString": "# teacher_thinking=false\n", "newString": "# teacher_thinking=false\n\n# ---------------------------------------------------------------------------\n# The released capped recipe holds the LR constant for all 100 steps. The paper's\n# own reported Qwen3-1.7B numbers are a step-50 checkpoint -- its AIME25 headline\n# (43.9) is exactly the step-50 entry of its forward-KL ablation, which falls to\n# 41.1 by step 100 -- so the recipe overshoots its own optimum well before the\n# budget is spent. With the budget fixed at 100 steps and no checkpoint\n# selection available, annealing the LR is how to spend all 100 steps of fresh\n# on-policy data while accumulating only step-50-scale parameter drift.\n# ---------------------------------------------------------------------------\nlr_scheduler_type=cosine\n", "originalFile": "# OPSD method recipe — whitelisted hyper-parameter overrides (KEY=VALUE per line).\n# Empty/all-commented == the OPSD baseline recipe on top of your /workspace/repo\n# code changes. The scorer re-trains with THIS file + your repo for 100 steps.\n# Budget knobs (max_steps, epochs, batch, gradient_accumulation, num_processes,\n# model) are FIXED by the scorer and ignored here.\n#\n# learning_rate=5e-6\n# lora_r=64\n# lora_alpha=128\n# beta=0\n# jsd_token_clip=0.05\n# temperature=1.1\n# lmbda=1\n# max_completion_length=1024 # <= 4096\n# fixed_teacher=true\n# use_ema_teacher=false\n# reason_first=false\n# teacher_thinking=false\n", "structuredPatch": [{"oldStart": 16, "oldLines": 3, "newStart": 16, "newLines": 14, "lines": [" # use_ema_teacher=false", " # reason_first=false", " # teacher_thinking=false", "+", "+# ---------------------------------------------------------------------------", "+# The released capped recipe holds the LR constant for all 100 steps. The paper's", "+# own reported Qwen3-1.7B numbers are a step-50 checkpoint -- its AIME25 headline", "+# (43.9) is exactly the step-50 entry of its forward-KL ablation, which falls to", "+# 41.1 by step 100 -- so the recipe overshoots its own optimum well before the", "+# budget is spent. With the budget fixed at 100 steps and no checkpoint", "+# selection available, annealing the LR is how to spend all 100 steps of fresh", "+# on-policy data while accumulating only step-50-scale parameter drift.", "+# ---------------------------------------------------------------------------", "+lr_scheduler_type=cosine"]}], "userModified": false, "replaceAll": false}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "sleep 115; sleep 115; sleep 115; sleep 115; sleep 115; python - <<'EOF'\nt=open('/workspace/eval_DATA.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n') if l.strip()]\nsel=[l for l in lines if any(k in l for k in ['FINAL','Traceback','Results written'])]\nprog=[l for l in lines if 'Processed prompts' in l]\nprint('\\n'.join(l[:170] for l in sel[-3:]) or \"(no final yet)\")\nprint(\"last progress:\", prog[-1][:110] if prog else \"none\")\nEOF",
"description": "Check DATA eval result",
"timeout": 700000
}Bash result
(no final yet)
last progress: Processed prompts: 57%|█████▋ | 48/84 [12:40<06:40, 11.12s/it, est. speed input: 6.14 toks/s, output: 1089
[stdout]
(no final yet)
last progress: Processed prompts: 57%|█████▋ | 48/84 [12:40<06:40, 11.12s/it, est. speed input: 6.14 toks/s, output: 1089
[metadata] {"noOutputExpected": false}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "sleep 115; sleep 115; sleep 115; python - <<'EOF'\nt=open('/workspace/eval_DATA.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n') if l.strip()]\nsel=[l for l in lines if any(k in l for k in ['FINAL','Traceback'])]\nprint('\\n'.join(l[:170] for l in sel[-3:]) or \"(no final yet)\")\nEOF\ncat /workspace/.timer/remaining_secs",
"description": "Check DATA eval result",
"timeout": 450000
}Bash result
(no final yet)
15689
[stdout]
(no final yet)
15689
[metadata] {"noOutputExpected": false}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "sleep 115; sleep 115; python - <<'EOF'\nt=open('/workspace/eval_DATA.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n') if l.strip()]\nsel=[l for l in lines if 'FINAL' in l or 'Traceback' in l]\nprint('\\n'.join(l[:170] for l in sel[-3:]) or \"(none)\")\nprog=[l for l in lines if 'Processed prompts' in l]\nprint('\\n'.join(l[:100] for l in prog[-3:]))\nEOF",
"description": "Check DATA eval",
"timeout": 300000
}Bash result
FINAL avg@12 accuracy: 52.78% (190/360 solutions correct on 30 problems)
Processed prompts: 100%|██████████| 84/84 [21:42<00:00, 7.18s/it, est. speed input: 7.64 toks/s, ou
Processed prompts: 100%|██████████| 84/84 [21:42<00:00, 7.18s/it, est. speed input: 7.64 toks/s, ou
Processed prompts: 100%|██████████| 84/84 [21:42<00:00, 15.50s/it, est. speed input: 7.64 toks/s, ou
[stdout]
FINAL avg@12 accuracy: 52.78% (190/360 solutions correct on 30 problems)
Processed prompts: 100%|██████████| 84/84 [21:42<00:00, 7.18s/it, est. speed input: 7.64 toks/s, ou
Processed prompts: 100%|██████████| 84/84 [21:42<00:00, 7.18s/it, est. speed input: 7.64 toks/s, ou
Processed prompts: 100%|██████████| 84/84 [21:42<00:00, 15.50s/it, est. speed input: 7.64 toks/s, ou
[metadata] {"noOutputExpected": false}Hard-data selection: 52.78 — within noise below the anchor, so it's not a win; I'll drop it. Launching cycle 3: paper recipe + cosine LR decay, the overshoot fix.
args
{
"command": "cd /workspace && export OPSD_PAD_SIDE=right OPSD_STUDENT_PROMPT=paper OPSD_TEACHER_CONTEXT=solution OPSD_CLIP_MODE=entry OPSD_DATA_SELECT=none && SEED=42 OUT_DIR=/workspace/runs/COS bash /workspace/train_capped.sh > /workspace/runs_COS.log 2>&1 && echo TRAIN_OK && bash /workspace/dev_eval.sh /workspace/runs/COS/capped_seed42/checkpoint-100 > /workspace/eval_COS.log 2>&1; echo \"EXIT=$?\"",
"description": "Cycle 3: paper recipe + cosine LR decay",
"timeout": 3000000,
"run_in_background": true
}Bash result
Command running in background with ID: b0rshs5wf. Output is being written to: /tmp/claude-0/-workspace/5fd2f3dc-5928-49f9-b3bb-a6ebc92f09aa/tasks/b0rshs5wf.output. You will be notified when it completes. To check interim output, use Read on that file path.
Session cwd remains /workspace/repo; directory changes made by the backgrounded command do not apply to subsequent commands.
[metadata] {"noOutputExpected": false, "backgroundTaskId": "b0rshs5wf", "backgroundCwdHint": "Session cwd remains /workspace/repo; directory changes made by the backgrounded command do not apply to subsequent commands."}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "sleep 115; python - <<'EOF'\nt=open('/workspace/runs_COS.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n')]\nsel=[l for l in lines if any(k in l for k in ['OPSD data','padding_side: r','clip mode',\"'loss'\",'Traceback'])]\nprint('\\n'.join(l[:200] for l in sel[:6]+sel[-3:]))\nEOF",
"description": "Verify cosine schedule active",
"timeout": 200000
}Bash result
(Bash completed with no output)
[metadata] {"noOutputExpected": false}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "sleep 115; python - <<'EOF'\nt=open('/workspace/runs_COS.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n')]\nprint(\"bytes\",len(t))\nsel=[l for l in lines if any(k in l for k in ['ignoring','padding_side','clip mode',\"'loss'\",'Traceback','Error'])]\nprint('\\n'.join(l[:200] for l in sel[-8:]))\nEOF",
"description": "Verify cosine run started",
"timeout": 200000
}Bash result
bytes 140559
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
Loading checkpoint shards: 0%| | 0/2 [00:00<?, ?it/s][DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD] jsd clip mode: entry (tau=0.05)
[OPSD] jsd clip mode: entry (tau=0.05)
[OPSD] jsd clip mode: entry (tau=0.05)
[OPSD] jsd clip mode: entry (tau=0.05)
[stdout]
bytes 140559
[DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
Loading checkpoint shards: 0%| | 0/2 [00:00<?, ?it/s][DataCollator] Original padding_side: left
[DataCollator] Set padding_side to: right
[OPSD] jsd clip mode: entry (tau=0.05)
[OPSD] jsd clip mode: entry (tau=0.05)
[OPSD] jsd clip mode: entry (tau=0.05)
[OPSD] jsd clip mode: entry (tau=0.05)
[metadata] {"noOutputExpected": false}encrypted chain-of-thought — recorded, not exposed
args
{
"command": "sleep 115; sleep 115; python - <<'EOF'\nt=open('/workspace/runs_COS.log',errors='ignore').read()\nlines=[l.split('\\r')[-1] for l in t.split('\\n')]\nsel=[l for l in lines if \"'loss'\" in l]\nprint('\\n'.join(l[:150] for l in sel[:3]+sel[-3:]))\nEOF",
"description": "Confirm LR is decaying",
"timeout": 300000
}Bash result
{'loss': 0.0102, 'grad_norm': 0.1596076637506485, 'learning_rate': 4.998766400914329e-06, 'on_policy_loss': 0.0102, 'epoch': 0.0}
{'loss': 0.0089, 'grad_norm': 0.13663886487483978, 'learning_rate': 4.9889049115077e-06, 'on_policy_loss': 0.0089, 'epoch': 0.0}
{'loss': 0.0073, 'grad_norm': 0.1363430917263031, 'learning_rate': 4.9692208514878445e-06, 'on_policy_loss': 0.0073, 'epoch': 0.01}
{'loss': -0.0001, 'grad_norm': 0.05929773673415184, 'learning_rate': 4.567701435686405e-06, 'on_policy_loss': -0.0001, 'epoch': 0.02}
{'loss': -0.0006, 'grad_norm': 0.06617777794599533, 'learning_rate': 4.475387530939226e-06, 'on_policy_loss': -0.0006, 'epoch': 0.02}
{'loss': -0.0019, 'grad_norm': 0.0680401623249054, 'learning_rate': 4.3752776740761495e-06, 'on_policy_loss': -0.0019, 'epoch': 0.03}
[stdout]
{'loss': 0.0102, 'grad_norm': 0.1596076637506485, 'learning_rate': 4.998766400914329e-06, 'on_policy_loss': 0.0102, 'epoch': 0.0}
{'loss': 0.0089, 'grad_norm': 0.13663886487483978, 'learning_rate': 4.9889049115077e-06, 'on_policy_loss': 0.0089, 'epoch': 0.0}
{'loss': 0.0073, 'grad_norm': 0.1363430917263031, 'learning_rate': 4.9692208514878445e-06, 'on_policy_loss': 0.0073, 'epoch': 0.01}
{'loss': -0.0001, 'grad_norm': 0.05929773673415184, 'learning_rate': 4.567701435686405e-06, 'on_policy_loss': -0.0001, 'epoch': 0.02}
{'loss': -0.0006, 'grad_norm': 0.06617777794599533, 'learning_rate': 4.475387530939226e-06, 'on_policy_loss': -0.0006, 'epoch': 0.02}
{'loss': -0.0019, 'grad_norm': 0.0680401623249054, 'learning_rate': 4.3752776740761495e-06, 'on_policy_loss': -0.0019, 'epoch': 0.03}
[metadata] {"noOutputExpected": false}Cosine decay confirmed active, with a loss trajectory matching the baseline (0.0102 → −0.0019) — a faithful reproduction with only the schedule changed.
One safety step while it runs: my code defaults are currently an untested A+D config, and the scorer passes no env vars. Let me make the defaults reproduce the paper exactly, so the submission is always something I've actually measured.
args
{
"replace_all": false,
"file_path": "/workspace/repo/data_collator.py",
"old_string": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")",
"new_string": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"paper\")"
}Edit result
The file /workspace/repo/data_collator.py has been updated successfully. (file state is current in your context — no need to Read it back)
[metadata] {"filePath": "/workspace/repo/data_collator.py", "oldString": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")", "newString": "STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"paper\")", "originalFile": null, "structuredPatch": [{"oldStart": 28, "oldLines": 7, "newStart": 28, "newLines": 7, "lines": [" # mode whose token distribution we actually want to distill.", " # \"both\" : concise solution followed by the long-form trace.", " # ---------------------------------------------------------------------------", "-STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"eval\")", "+STUDENT_PROMPT_STYLE = os.environ.get(\"OPSD_STUDENT_PROMPT\", \"paper\")", " TEACHER_CONTEXT = os.environ.get(\"OPSD_TEACHER_CONTEXT\", \"solution\")", " # \"left\" keeps the prompt block flush against the completion (see the padding note", " # in __init__); \"right\" reproduces the released recipe."]}], "userModified": false, "replaceAll": false}