Imported from asckaya/opsd (
AGENTS.md). Install upstream withnpx skills add asckaya/opsd. Copyright stays with the author.
OPSD Implementation Notes
Source-of-truth files (read in this order):
ALGO.md— Algorithm spec (Part 1) + paper REF excerpts (Part 2).slime_plugins/opsd/README.md— User-facing argument table, project conventions, opt-in extensions; must stay in sync withslime/utils/arguments.py.CLAUDE.md/AGENTS.md— Top-level project instructions (one is a symlink to the other), including health-monitoring tables.
ALGO.md Part 1 ⊇ paper: paper is the project algorithm at N=1 with τ_k = y* (ground truth). When in doubt, prefer Part 1 semantics; prefer Part 2 numerics for paper-faithful hyperparameters.
Implementation guidelines
Algorithmic contract
- Default loss =
α · L_distill = α · (1/T) Σ KL(q_mix^t ‖ p^t)(forward, full-vocab). No GRPO PG signal mixed in unless--opsd-mix-with-policy-lossis explicitly set. - Teacher = frozen initial-policy snapshot (
--opsd-freeze-teacher=Truedefault). Teacher forwards run against the snapshot, not the live student weights. - Rollout produces K+1 samples per prompt: 1 random student
y, the other K go to the privileged candidate pool. The student is by construction never one of the τ_k seen by any teacher. - Quality scoring uses all three
ALGO.md§1.1-step-3 terms: length penalty, format penalty, Conf. Conf is rank-normalized across a sample's candidates by default (--opsd-quality-conf-norm rank); change with explicit knob if you want raw log-prob behavior. - Diversity selection = k-center greedy on the configured distance metric. Default
token_jsd(ALGO.md§1.1-step-4 recommended);unigram_jsdis opt-in and changes selection ordering relative to teacher forwards. - Mixture-weight softmax is on raw distributions (no temperature).
ALGO.md§1.1-step-6 has no temperature term; the--opsd-temperatureknob is a project extension defaulting to 1.0 (identity). - Per-position KL clamp
τ=0.05(--opsd-jsd-token-clip) applied AFTER sum-over-vocab, preserving non-negativity. The legacy per-(position, vocab-entry) clip--opsd-pointwise-kl-clipis one-sided and can drive per-token KL negative — keep it default-off.
Numerical / framework invariants
- KL must be non-negative on every step. Monitor
train/opsd_klandtrain/opsd_kl_clamped; they should stay equal. If they diverge, investigate clip config. train/opsd_w_entropy∈ [0, 1]. Health band: 0.3–0.8. If it pins at 0 or 1, mixture is degenerate — check[opsd_dbg/mixture]logs for which logit term saturates (Δ / h / g).train/lr-pg_0/1readsparam_group["lr"]directly; should equal--lrafter the first scheduler step. Do NOT revert toopt_param_scheduler.get_lr(...).- Pipeline-parallel size must be 1 for OPSD:
plugin.py:before_train_step_hookcallsmodel(...)directly, bypassing pipeline scheduling. TP / DP / CP are fine. --opsd-freeze-teacher=Truerequires--enable-weights-backuper(validated inarguments.py).
Where each invariant lives
| Invariant | File:Symbol |
|---|---|
| K+1 rollout split | slime_plugins/opsd/rollout.py:generate_rollout |
| Privileged pool filtering | slime_plugins/opsd/rollout.py:_collect_privileged |
| Quality scoring (Len + Format + Conf) | slime_plugins/opsd/rollout.py:_structural_score + distillation.py:add_conf |
| Diversity selection | slime_plugins/opsd/selection.py:kcenter |
| Mixture weights w_k^t | slime_plugins/opsd/distillation.py:mixture_weights |
| Forward-KL custom autograd (q_mix fused) | slime_plugins/opsd/distillation.py:VocabParallelMixtureKLDiv |
| RKL custom autograd (q_mix fused) | slime_plugins/opsd/distillation.py:VocabParallelMixtureRKLDiv |
| Frozen-teacher swap | slime_plugins/opsd/plugin.py:before_train_step_hook |
| Loss + metrics composition | slime_plugins/opsd/plugin.py:loss_function |
When changing OPSD behavior
- Read
ALGO.mdPart 1 first. Verify your change is consistent with the algorithmic contract; if it isn't, the change must land as an opt-in knob, not a default change. - Update
ALGO.mdif the algorithmic contract changes (Part 1 for spec, Part 2 for paper REF). - Update
slime_plugins/opsd/README.mdif any user-facing argument, project convention, or opt-in extension is added / removed / re-defaulted. - Update
slime/utils/arguments.pywith matching defaults and help text. Keep theargparsehelp short; longer rationale goes into the README's project-conventions or opt-in tables. - Run the diagnostic loop: launch the 1.7B script for a few train steps, check
[opsd_dbg/mixture]and[opsd_dbg/sample]stdout +train/opsd_*TB scalars match the health bands below. - Sweep the docs in the same commit. If a rename, a removed flag, or an algorithm change landed, this file,
slime_plugins/opsd/README.md,ALGO.md,BUG.md, and any affected docstrings must reflect the new state in the same commit — not a follow-up.
Soft rules
- Prefer method-consistent semantics over convenience shortcuts when the two conflict.
- Default values must be paper-aligned where paper specifies them; opt-in extensions never change defaults.
- The mixture-weight
w_entropyderivation inmixture_weightsis sensitive to Python operator precedence — keep the explicit parens around the negation ((-(w * log_w).sum(0)).clamp(min=0)). Do NOT collapse to-(w * log_w).sum(0).clamp(min=0)— it parses as-((w*log_w).sum(0).clamp(min=0)), which clamps the negative sum to 0 then negates to -0. - Keep
[opsd_dbg/mixture]and[opsd_dbg/sample]rank-0 diagnostic prints until the health-band monitoring is verified stable in production. Once removed, document the removal in this file.
Coding style
- Prefer concise code. If a refactor adds layers (helper classes, wrapper functions, indirection) without a concrete reason, the original was right. Three straight lines beat three abstractions.
- Do not use
getattr/hasattras a silent fallback for values that should always be present. Missing fields, mistyped flags, or unset configs are bugs — raise on them, don't paper over with a default.- When the assumption is internal and a violation would be a programmer error, an
assertis enough. - When the assumption is on user-supplied data (CLI args, configs, batch contents), raise an exception with a message that names the field.
- For args registered in
slime/utils/arguments.pywith a default, read them directly (args.opsd_xxx) — the registered default already covers "not set". The only acceptablegetattr(obj, "x", default)is whenxis a genuinely optional knob and the default is the documented behavior.
- When the assumption is internal and a violation would be a programmer error, an
Quick reference: paper Table 6 vs current scripts
| Knob | paper Table 6 (OPSD) | run_qwen3_*_opsd.sh |
Justification for delta |
|---|---|---|---|
| Learning rate | 5e-6 |
5e-6 |
— |
| Effective batch size | 32 |
32 |
— |
| Max completion length | 1024 |
8192 |
math reasoning needs longer chains |
| Generations per prompt | 1 |
K+1=17 (1 student + K=16 candidates) |
project mixture extension (ALGO.md §1.1); paper is N=1 special case |
| Sampling temperature | 1.1 |
1.1 |
— |
| Top-p / Top-k | (not stated) | 0.95 / 20 |
OPSD official training scripts |
| KL clip τ | 0.05 |
--opsd-jsd-token-clip 0.05 |
— |
| LoRA r/α | 64 / 128 |
full fine-tune | slime Megatron path doesn't run LoRA |
| Training steps | 100 |
--num-rollout 1000 (longer schedule) |
longer-horizon experiments |
Health monitoring (TB / stdout quick lookup)
| Metric | Healthy range | Anomaly meaning |
|---|---|---|
rollout/raw_reward |
0.3–0.9 | Too low → student weak / data too hard / truncated; too high → group all-correct, mixture loses diversity |
rollout/truncated_ratio |
< 0.1 | High → --rollout-max-response-len too small for the CoT length |
rollout/zero_std/count_0+count_1 |
< rollout-batch-size |
== batch ⇒ every group has std=0, OPSD input degenerate |
train/opsd_kl |
≥ 0, drifting to a stable value | Negative → almost certainly --opsd-pointwise-kl-clip accidentally on (one-sided); switch back to --opsd-jsd-token-clip |
train/opsd_kl_clamped |
== opsd_kl |
Diverging from opsd_kl mirrors the row above |
train/opsd_w_entropy |
0.3–0.8 | 0 → mixture one-hot; 1 → fully uniform; adjust --opsd-kl-weight ±2× |
train/lr-pg_0/1 |
== --lr |
Reads from param_group["lr"]; if 0, the scheduler never wrote lr in |
train/grad_norm |
0.1–1.0 | Too large → KL clip ineffective or RKL too strong; too small → weak training signal |