Skip to content

Commit 74dfb9a

Browse files
rahul-tuliclaude
andcommitted
refactor(train): config-file-first CLI generated from a typed pydantic schema
Make scripts/train.py config-file-first without changing what any run does and without touching the model layer. - New src/speculators/train/config.py is the single source of truth: ~75 tunables as typed fields on cohesive pydantic-settings groups (verifier/draft/data/ generation/loss/optimizer/scheduler/trainer/logging + per-algo dflash/dspark/ peagle/mtp). The argparse CLI is GENERATED from these fields, so adding an argument is one typed field -- train.py has no per-flag add_argument. - --config run.yaml supplies any argument; precedence is CLI > YAML > coded defaults; --config is optional (no file == the old pure-CLI behaviour). - --dump-config prints the fully-resolved grouped YAML (a valid --config file); the resolved run.yaml is also saved next to each checkpoint for reproducibility. It runs the draft-init contract first, so a scaffold is never emitted for a config that would fail to load. - Provenance = the argparse.SUPPRESS'd namespace (not model_fields_set, which the nested partial-update machinery pollutes). A YAML leaf counts as provided only under its owning group block; a misplaced key is reported as unrecognised and kept out of provenance so it can't mis-fire the draft-init conflict check. - Every bad input is a clean argparse error (exit code 2, no traceback): schema ValidationError and a broken --config file (missing/unreadable/malformed/ non-mapping) alike; a missing required field names the flag to set. Phase 1: the resolved config is flattened back to the vars(args)-shaped dict the five SpeculatorModel classes consume via **kwargs, so the model layer and Trainer are untouched; only main()'s TrainerConfig reads the typed groups. Pushing typed configs into the model layer is deferred to a follow-up PR. Rebased onto current main. Absorbs the flags added since the branch point and ports them into the schema so behaviour is unchanged: - #736 D-PACE: --per-position-loss-weight, --dpace-alpha as typed DFlash fields, with the "dpace requires ce loss / alpha in (0,1]" check as a cross-group model_validator. - #651 scheduler: --scheduler-warmup-ratio, and --scheduler-type as a Literal (linear/cosine/none), threaded through the typed TrainerConfig build. - #749 default sliding-window attention: --sliding-window-indices renamed to --full-attention-indices (inverted semantics), matching upstream. The decoder-shaping flag set (used by the draft-init conflict check) is derived from the schema via a field tag, so it is not a second place to edit when a shaping knob is added. Equivalence is guarded by tests/unit/train/test_golden_cli_equivalence.py: the refactored flat working-dict is asserted byte-value-identical to the pre-refactor parser across a 17-invocation matrix (expected values in one golden_flat_dict.json -- a _baseline all-defaults dict + per-invocation deltas), and the generated CLI's option-string set is frozen against the pre-refactor parser (golden_cli_flags.json) so no flag can be silently renamed or dropped. Removes src/speculators/utils/argparse_utils.py and its test: train.py was the only caller of explicitly_provided_dests, which the SUPPRESS'd-namespace provenance replaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rahul-Tuli <rtuli@redhat.com>
1 parent 6030a44 commit 74dfb9a

9 files changed

Lines changed: 2104 additions & 651 deletions

File tree

scripts/train.py

Lines changed: 125 additions & 564 deletions
Large diffs are not rendered by default.

src/speculators/train/config.py

Lines changed: 1012 additions & 0 deletions
Large diffs are not rendered by default.

src/speculators/utils/argparse_utils.py

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
[
2+
"--block-size",
3+
"--checkpoint-freq",
4+
"--confidence-head-alpha",
5+
"--confidence-head-with-markov",
6+
"--d2t-path",
7+
"--data-path",
8+
"--deterministic-cuda",
9+
"--dflash-decay-gamma",
10+
"--down-sample-ratio",
11+
"--down-sample-ratio-min",
12+
"--dpace-alpha",
13+
"--draft-arch",
14+
"--draft-attn-impl",
15+
"--draft-config",
16+
"--draft-hidden-act",
17+
"--draft-vocab-size",
18+
"--dry-run",
19+
"--embed-requires-grad",
20+
"--enable-confidence-head",
21+
"--epochs",
22+
"--fc-norm",
23+
"--from-pretrained",
24+
"--full-attention-indices",
25+
"--help",
26+
"--hidden-states-dtype",
27+
"--hidden-states-path",
28+
"--legacy-data",
29+
"--log-dir",
30+
"--log-freq",
31+
"--logger",
32+
"--loss-fn",
33+
"--lr",
34+
"--markov-head-type",
35+
"--markov-rank",
36+
"--mask-token-id",
37+
"--max-anchors",
38+
"--max-retries",
39+
"--muon-adjust-lr-fn",
40+
"--muon-lr",
41+
"--muon-momentum",
42+
"--muon-ns-steps",
43+
"--muon-weight-decay",
44+
"--no-confidence-head-with-markov",
45+
"--no-embed-requires-grad",
46+
"--no-enable-confidence-head",
47+
"--no-norm-before-fc",
48+
"--no-norm-before-residual",
49+
"--no-norm-output",
50+
"--no-resume-from-checkpoint",
51+
"--noise-std",
52+
"--norm-before-fc",
53+
"--norm-before-residual",
54+
"--norm-output",
55+
"--num-depths",
56+
"--num-layers",
57+
"--num-speculative-steps",
58+
"--num-workers",
59+
"--on-generate",
60+
"--on-missing",
61+
"--optimizer",
62+
"--per-position-loss-weight",
63+
"--prefetch-factor",
64+
"--request-timeout",
65+
"--run-name",
66+
"--save-best",
67+
"--save-path",
68+
"--scheduler-num-cosine-cycles",
69+
"--scheduler-total-steps",
70+
"--scheduler-type",
71+
"--scheduler-warmup-ratio",
72+
"--scheduler-warmup-steps",
73+
"--seed",
74+
"--sliding-window",
75+
"--sliding-window-non-causal",
76+
"--speculator-type",
77+
"--step-weight-beta",
78+
"--t2d-path",
79+
"--target-layer-ids",
80+
"--token-freq-path",
81+
"--total-seq-len",
82+
"--train-data-ratio",
83+
"--trust-remote-code",
84+
"--ttt-step-loss-decay",
85+
"--ttt-steps",
86+
"--use-off-policy-tokens",
87+
"--verifier-name-or-path",
88+
"--vllm-endpoint",
89+
"--weight-decay",
90+
"-h"
91+
]
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Shared CLI invocation matrix for the proof-of-equivalence golden test.
2+
3+
Each entry is ``(name, argv_tail)`` where ``argv_tail`` are the arguments passed
4+
after ``train.py`` (the required ``--verifier-name-or-path`` is prepended by the
5+
harness). The same matrix is used to (a) capture golden ``vars(args)`` fixtures
6+
from the pre-refactor parser and (b) assert the refactored config layer produces
7+
the identical flat working-dict. See ``test_golden_cli_equivalence.py``.
8+
9+
The matrix intentionally exercises every speculator type and the tricky
10+
draft-init / optimizer / loss / normalization branches -- these are the paths
11+
most likely to drift during the refactor.
12+
"""
13+
14+
# A dummy verifier path keeps ``parse_args()`` fully offline (no model download);
15+
# nothing in argument parsing touches the network.
16+
DUMMY_VERIFIER = "dummy-verifier"
17+
18+
INVOCATIONS: list[tuple[str, list[str]]] = [
19+
("eagle3_default", []),
20+
("dflash_default", ["--speculator-type", "dflash"]),
21+
("dspark_default", ["--speculator-type", "dspark"]),
22+
("peagle_default", ["--speculator-type", "peagle"]),
23+
("mtp_default", ["--speculator-type", "mtp"]),
24+
("from_pretrained", ["--from-pretrained", "/tmp/some/ckpt"]),
25+
("draft_config", ["--draft-config", "/tmp/some/decoder"]),
26+
(
27+
"dflash_full_attention_indices",
28+
[
29+
"--speculator-type",
30+
"dflash",
31+
"--sliding-window",
32+
"1024",
33+
"--full-attention-indices",
34+
"0",
35+
"2",
36+
],
37+
),
38+
(
39+
"scheduler_warmup_ratio",
40+
[
41+
"--scheduler-type",
42+
"cosine",
43+
"--scheduler-warmup-ratio",
44+
"0.1",
45+
],
46+
),
47+
("muon_optimizer", ["--optimizer", "muon", "--muon-lr", "0.01"]),
48+
("compound_loss", ["--loss-fn", '{"ce": 0.1, "tv": 0.9}']),
49+
("dry_run", ["--dry-run"]),
50+
(
51+
"norm_toggles",
52+
["--no-norm-before-fc", "--no-norm-output", "--fc-norm"],
53+
),
54+
(
55+
"numeric_overrides",
56+
[
57+
"--epochs",
58+
"5",
59+
"--lr",
60+
"2e-4",
61+
"--num-layers",
62+
"3",
63+
"--draft-vocab-size",
64+
"32000",
65+
"--total-seq-len",
66+
"4096",
67+
"--checkpoint-freq",
68+
"0.5",
69+
],
70+
),
71+
(
72+
"dspark_heads",
73+
[
74+
"--speculator-type",
75+
"dspark",
76+
"--markov-rank",
77+
"128",
78+
"--markov-head-type",
79+
"gated",
80+
"--no-enable-confidence-head",
81+
"--confidence-head-alpha",
82+
"0.5",
83+
],
84+
),
85+
(
86+
"peagle_cod",
87+
[
88+
"--speculator-type",
89+
"peagle",
90+
"--num-depths",
91+
"4",
92+
"--down-sample-ratio",
93+
"0.6",
94+
"--down-sample-ratio-min",
95+
"0.1",
96+
],
97+
),
98+
# Kitchen sink: toggle every otherwise-untouched bool (store_true and
99+
# BooleanOptionalAction --no- forms), the list field with real values, and a
100+
# few non-default Literals -- so the golden proof locks their generated CLI
101+
# *shape*, not just their defaults.
102+
(
103+
"kitchen_sink",
104+
[
105+
"--speculator-type",
106+
"dspark",
107+
"--save-best",
108+
"--deterministic-cuda",
109+
"--no-resume-from-checkpoint",
110+
"--use-off-policy-tokens",
111+
"--legacy-data",
112+
"--trust-remote-code",
113+
"--embed-requires-grad",
114+
"--no-norm-before-residual",
115+
"--no-confidence-head-with-markov",
116+
"--target-layer-ids",
117+
"2",
118+
"3",
119+
"5",
120+
"--optimizer",
121+
"adamw",
122+
"--scheduler-type",
123+
"cosine",
124+
"--markov-head-type",
125+
"rnn",
126+
],
127+
),
128+
]
129+
130+
131+
def full_argv(argv_tail: list[str]) -> list[str]:
132+
"""Prepend the program name and required verifier path to an argv tail."""
133+
return ["train.py", "--verifier-name-or-path", DUMMY_VERIFIER, *argv_tail]

0 commit comments

Comments
 (0)