Mandeep Sidhu commited on
Commit
e7a7275
·
1 Parent(s): e4b41e9

Refactor experiment pipeline and add regime paper

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +6 -0
  3. paper/dropout_decay_regime_specific_pressure_law.bbl +73 -0
  4. paper/dropout_decay_regime_specific_pressure_law.pdf +3 -0
  5. paper/dropout_decay_regime_specific_pressure_law.tex +427 -0
  6. paper/figures/calibration_cell_curves.tex +51 -0
  7. paper/figures/final_improvements.tex +23 -0
  8. paper/figures/l20_stage_deltas.tex +47 -0
  9. paper/figures/static_optima_by_regime.tex +66 -0
  10. paper/paper_results_summary.json +520 -0
  11. paper/references.bib +8 -0
  12. paper/tables/coefficient_table.tex +9 -0
  13. paper/tables/l20_stage_table.tex +23 -0
  14. paper/tables/streaming_validation_table.tex +12 -0
  15. runs/smoke_compat/locked_stream/20260605-224834/RESULT_SUMMARY.md +29 -0
  16. runs/smoke_compat/locked_stream/20260605-224834/config.json +103 -0
  17. runs/smoke_compat/locked_stream/20260605-224834/metrics.jsonl +4 -0
  18. runs/smoke_compat/locked_stream/20260605-224834/summary.csv +5 -0
  19. runs/smoke_compat/locked_stream/20260605-224834/summary.json +90 -0
  20. runs/smoke_compat/locked_stream/20260605-224834/trace.jsonl +8 -0
  21. runs/smoke_compat/screen_static/20260605-224821/RESULT_SUMMARY.md +24 -0
  22. runs/smoke_compat/screen_static/20260605-224821/config.json +89 -0
  23. runs/smoke_compat/screen_static/20260605-224821/dropout_curves.svg +24 -0
  24. runs/smoke_compat/screen_static/20260605-224821/metrics.jsonl +2 -0
  25. runs/smoke_compat/screen_static/20260605-224821/model_selection.csv +2 -0
  26. runs/smoke_compat/screen_static/20260605-224821/model_selection.json +29 -0
  27. runs/smoke_compat/screen_static/20260605-224821/summary.csv +3 -0
  28. runs/smoke_compat/screen_static/20260605-224821/summary.json +46 -0
  29. runs/smoke_compat/screen_static/20260605-224821/trace.jsonl +4 -0
  30. scripts/generate_paper_assets.py +579 -0
  31. src/dropout_decay/data/__init__.py +45 -0
  32. src/dropout_decay/datasets/__init__.py +41 -0
  33. src/dropout_decay/{data.py → datasets/corpus.py} +1 -1
  34. src/dropout_decay/experiment.py +5 -1686
  35. src/dropout_decay/experiments/__init__.py +25 -0
  36. src/dropout_decay/experiments/artifacts.py +281 -0
  37. src/dropout_decay/experiments/device.py +37 -0
  38. src/dropout_decay/experiments/parsing.py +148 -0
  39. src/dropout_decay/experiments/progress.py +66 -0
  40. src/dropout_decay/experiments/reports.py +448 -0
  41. src/dropout_decay/experiments/runner.py +555 -0
  42. src/dropout_decay/experiments/training.py +261 -0
  43. src/dropout_decay/model.py +21 -209
  44. src/dropout_decay/models/__init__.py +39 -0
  45. src/dropout_decay/models/gpt.py +227 -0
  46. src/dropout_decay/scheduler.py +5 -37
  47. src/dropout_decay/schedules/__init__.py +24 -0
  48. src/dropout_decay/schedules/dropout.py +56 -0
  49. src/dropout_decay/specs/__init__.py +26 -0
  50. src/dropout_decay/specs/dropout_condition.py +89 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ paper/*.pdf filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -2,6 +2,12 @@
2
  __pycache__/
3
  *.py[cod]
4
  *.pdf
 
 
 
 
 
 
5
  *.npy
6
  .venv/
7
  *.egg-info/
 
2
  __pycache__/
3
  *.py[cod]
4
  *.pdf
5
+ *.aux
6
+ *.blg
7
+ *.fdb_latexmk
8
+ *.fls
9
+ *.log
10
+ *.out
11
  *.npy
12
  .venv/
13
  *.egg-info/
paper/dropout_decay_regime_specific_pressure_law.bbl ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \begin{thebibliography}{8}
2
+ \providecommand{\natexlab}[1]{#1}
3
+ \providecommand{\url}[1]{\texttt{#1}}
4
+ \expandafter\ifx\csname urlstyle\endcsname\relax
5
+ \providecommand{\doi}[1]{doi: #1}\else
6
+ \providecommand{\doi}{doi: \begingroup \urlstyle{rm}\Url}\fi
7
+
8
+ \bibitem[Eldan and Li(2023)]{eldan2023tinystories}
9
+ Ronen Eldan and Yuanzhi Li.
10
+ \newblock Tinystories: How small can language models be and still speak
11
+ coherent english?
12
+ \newblock \emph{arXiv preprint arXiv:2305.07759}, 2023.
13
+ \newblock URL \url{https://arxiv.org/abs/2305.07759}.
14
+
15
+ \bibitem[Gokaslan et~al.(2019)Gokaslan, Cohen, Pavlick, and
16
+ Tellex]{gokaslan2019openwebtext}
17
+ Aaron Gokaslan, Vanya Cohen, Ellie Pavlick, and Stefanie Tellex.
18
+ \newblock Openwebtext corpus.
19
+ \newblock \url{https://zenodo.org/records/3834942}, 2019.
20
+ \newblock Open reproduction of the WebText-style corpus.
21
+
22
+ \bibitem[Hoffmann et~al.(2022)Hoffmann, Borgeaud, Mensch, Buchatskaya, Cai,
23
+ Rutherford, Casas, Hendricks, Welbl, Clark, Hennigan, Noland, Millican,
24
+ van~den Driessche, Damoc, Guy, Osindero, Simonyan, Elsen, Rae, Vinyals, and
25
+ Sifre]{hoffmann2022training}
26
+ Jordan Hoffmann, Sebastian Borgeaud, Arthur Mensch, Elena Buchatskaya, Trevor
27
+ Cai, Eliza Rutherford, Diego de~Las Casas, Lisa~Anne Hendricks, Johannes
28
+ Welbl, Aidan Clark, Tom Hennigan, Eric Noland, Katie Millican, George van~den
29
+ Driessche, Bogdan Damoc, Aurelia Guy, Simon Osindero, Karen Simonyan, Erich
30
+ Elsen, Jack~W. Rae, Oriol Vinyals, and Laurent Sifre.
31
+ \newblock Training compute-optimal large language models.
32
+ \newblock \emph{arXiv preprint arXiv:2203.15556}, 2022.
33
+ \newblock URL \url{https://arxiv.org/abs/2203.15556}.
34
+
35
+ \bibitem[Kaplan et~al.(2020)Kaplan, McCandlish, Henighan, Brown, Chess, Child,
36
+ Gray, Radford, Wu, and Amodei]{kaplan2020scaling}
37
+ Jared Kaplan, Sam McCandlish, Tom Henighan, Tom~B. Brown, Benjamin Chess, Rewon
38
+ Child, Scott Gray, Alec Radford, Jeffrey Wu, and Dario Amodei.
39
+ \newblock Scaling laws for neural language models.
40
+ \newblock \emph{arXiv preprint arXiv:2001.08361}, 2020.
41
+ \newblock URL \url{https://arxiv.org/abs/2001.08361}.
42
+
43
+ \bibitem[Karpathy(2025)]{karpathy_nanochat}
44
+ Andrej Karpathy.
45
+ \newblock nanochat.
46
+ \newblock \url{https://github.com/karpathy/nanochat}, 2025.
47
+ \newblock MIT-licensed repository, accessed 2026-05-29.
48
+
49
+ \bibitem[Merity et~al.(2017)Merity, Xiong, Bradbury, and
50
+ Socher]{merity2017pointer}
51
+ Stephen Merity, Caiming Xiong, James Bradbury, and Richard Socher.
52
+ \newblock Pointer sentinel mixture models.
53
+ \newblock In \emph{International Conference on Learning Representations}, 2017.
54
+ \newblock URL \url{https://arxiv.org/abs/1609.07843}.
55
+
56
+ \bibitem[Srivastava et~al.(2014)Srivastava, Hinton, Krizhevsky, Sutskever, and
57
+ Salakhutdinov]{srivastava2014dropout}
58
+ Nitish Srivastava, Geoffrey Hinton, Alex Krizhevsky, Ilya Sutskever, and Ruslan
59
+ Salakhutdinov.
60
+ \newblock Dropout: A simple way to prevent neural networks from overfitting.
61
+ \newblock \emph{Journal of Machine Learning Research}, 15\penalty0
62
+ (56):\penalty0 1929--1958, 2014.
63
+ \newblock URL \url{https://www.jmlr.org/papers/v15/srivastava14a.html}.
64
+
65
+ \bibitem[Vaswani et~al.(2017)Vaswani, Shazeer, Parmar, Uszkoreit, Jones, Gomez,
66
+ Kaiser, and Polosukhin]{vaswani2017attention}
67
+ Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones,
68
+ Aidan~N. Gomez, Lukasz Kaiser, and Illia Polosukhin.
69
+ \newblock Attention is all you need.
70
+ \newblock In \emph{Advances in Neural Information Processing Systems}, 2017.
71
+ \newblock URL \url{https://arxiv.org/abs/1706.03762}.
72
+
73
+ \end{thebibliography}
paper/dropout_decay_regime_specific_pressure_law.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46ba08475ba2566bc3733eed5403db061a3ce4145ed6bd79965796719a396bc8
3
+ size 267426
paper/dropout_decay_regime_specific_pressure_law.tex ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \documentclass[11pt]{article}
2
+
3
+ \usepackage[margin=1in]{geometry}
4
+ \usepackage{amsmath}
5
+ \usepackage{amssymb}
6
+ \usepackage{booktabs}
7
+ \usepackage{graphicx}
8
+ \usepackage{microtype}
9
+ \usepackage{tikz}
10
+ \usepackage{pgfplots}
11
+ \usepgfplotslibrary{groupplots}
12
+ \pgfplotsset{compat=1.18}
13
+ \usepackage[numbers,sort&compress]{natbib}
14
+ \usepackage[colorlinks=true,linkcolor=blue,citecolor=blue,urlcolor=blue]{hyperref}
15
+
16
+ \title{Regime-Specific Pressure Laws for Dropout Decay in Streaming Language Model Training}
17
+ \author{Anonymous Authors}
18
+ \date{June 2026}
19
+
20
+ \newcommand{\clamp}{\operatorname{clamp}}
21
+ \newcommand{\params}{P}
22
+ \newcommand{\unique}{U_t}
23
+ \newcommand{\sampled}{C_t}
24
+ \newcommand{\dropout}{p_t}
25
+
26
+ \begin{document}
27
+ \maketitle
28
+
29
+ \begin{abstract}
30
+ Dropout is usually selected as a fixed hyperparameter before training, but
31
+ streaming language-model training changes the regularization problem over time:
32
+ early stages expose few unique tokens to a comparatively large model, while
33
+ later stages reveal more data and reduce the need for strong stochastic
34
+ regularization. We study this setting with nanochat-derived causal
35
+ Transformers trained under an expanding-prefix protocol on three text regimes:
36
+ OpenWebText10K, TinyStories, and WikiText-103. Static dropout sweeps show a
37
+ repeatable pattern: for a fixed model, the validation-loss curve over dropout
38
+ is typically unimodal or plateau-shaped, and the optimum falls as unique data
39
+ increases; for a fixed prefix, the optimum rises with model capacity. We turn
40
+ these per-cell optima into a regime-specific pressure law,
41
+ \[
42
+ \dropout=\clamp(p_{\min},p_{\max},
43
+ A\log_{10}(\params/\unique)
44
+ +B\log_{10}(\sampled/\unique)
45
+ +D\log_{10}(\params/\unique)\log_{10}(\sampled/\unique)+C_0),
46
+ \]
47
+ where $\params$ is parameter count, $\unique$ is currently available unique
48
+ tokens, and $\sampled$ is cumulative sampled training tokens. The fitted
49
+ interaction law explains static optimum cells with mean absolute errors of
50
+ 0.0158, 0.0180, and 0.0162 dropout on OpenWebText10K, TinyStories, and
51
+ WikiText-103 respectively. In locked streaming validation, fitted or
52
+ formula-derived decay schedules beat the best fixed-dropout baseline on final
53
+ validation loss in all three regimes at 4M-token validation scale and again in
54
+ larger 51.84M-parameter, 8M-token extrapolations. The large extrapolation tier
55
+ wins all paired seeds across all three regimes (9/9 paired wins), but stage-wise
56
+ analysis shows the schedules are final-loss optimized rather than best at every
57
+ intermediate prefix. The results support a practical hypothesis: dropout decay
58
+ should be calibrated as a regime-specific pressure law, not chosen as a
59
+ universal constant or an arbitrary high-to-low schedule.
60
+ \end{abstract}
61
+
62
+ \section{Introduction}
63
+
64
+ Dropout was introduced as a simple and effective regularizer for neural
65
+ networks \citep{srivastava2014dropout}. In language model practice, however,
66
+ dropout is often treated as a fixed scalar selected before a run. That choice
67
+ is mismatched to streaming settings. When a model begins training on a small
68
+ prefix of a stream, its parameter count can be large relative to the number of
69
+ unique tokens currently available; later, the same model receives more unique
70
+ data and can safely expose more functional capacity. A fixed dropout rate must
71
+ therefore compromise between early overfitting risk and late underfitting risk.
72
+
73
+ The motivating hypothesis of this project was intentionally ambitious: a
74
+ high-capacity model could start with strong regularization on small initial
75
+ data and then decay dropout as streaming data accumulates, allowing capacity to
76
+ come online without catastrophic overfitting. The experiments refined that
77
+ hypothesis. Extremely high initial dropout is not automatically useful; early
78
+ tests with dropout near 0.8 underfit badly. The stronger and better-supported
79
+ claim is that useful dropout is a function of training pressure: model capacity
80
+ relative to the current data prefix, and cumulative optimizer sampling relative
81
+ to that prefix.
82
+
83
+ This paper makes four empirical contributions.
84
+
85
+ \begin{enumerate}
86
+ \item We define calibration cells for streaming dropout: one model, one
87
+ unique-token prefix, one sampled-token budget, and a sweep over fixed
88
+ dropout rates.
89
+ \item We show that calibration cells often have a parabolic or plateau-like
90
+ validation-loss curve over dropout, making the local optimum a usable
91
+ supervised target.
92
+ \item We fit a regime-specific interaction pressure law that maps
93
+ $(\params,\unique,\sampled)$ to a dropout rate.
94
+ \item We validate the resulting schedules against broad fixed-dropout
95
+ baselines across three text regimes and a larger-model/larger-stream
96
+ extrapolation tier.
97
+ \end{enumerate}
98
+
99
+ The central result is not that one coefficient vector works everywhere. It does
100
+ not. The central result is that the pressure-law structure is useful, while the
101
+ coefficients are regime-specific.
102
+
103
+ \section{Related Work}
104
+
105
+ \paragraph{Dropout and regularization.}
106
+ Dropout applies multiplicative Bernoulli noise during training and can be
107
+ understood as an approximate ensemble over subnetworks
108
+ \citep{srivastava2014dropout}. With inverted dropout, an activation $h_i$ is
109
+ transformed as $h'_i=h_i m_i/(1-p)$, $m_i\sim\operatorname{Bernoulli}(1-p)$.
110
+ The expectation is preserved, but the variance grows as $h_i^2p/(1-p)$. This
111
+ captures the key tradeoff in our experiments: low dropout can overfit repeated
112
+ small prefixes, but high dropout can remove too much usable capacity.
113
+
114
+ \paragraph{Transformer language models and scaling.}
115
+ Our model family is a decoder-only Transformer following the attention-based
116
+ architecture of \citet{vaswani2017attention}. Scaling-law work shows that loss
117
+ varies systematically with model size, data size, and compute
118
+ \citep{kaplan2020scaling,hoffmann2022training}. Our study is narrower: it does
119
+ not propose a global loss scaling law. Instead, it asks whether a
120
+ regularization hyperparameter can be mapped to local pressure variables during
121
+ streaming.
122
+
123
+ \paragraph{Text regimes.}
124
+ We evaluate on three text regimes. TinyStories is a synthetic simple-English
125
+ story corpus designed for small language-model studies \citep{eldan2023tinystories}.
126
+ WikiText-103 is a standard word-level language-modeling benchmark derived from
127
+ Wikipedia articles \citep{merity2017pointer}. OpenWebText is an open
128
+ WebText-style corpus \citep{gokaslan2019openwebtext}. We use cached/tokenized
129
+ subsets and therefore report results as regime-level evidence rather than
130
+ claims about full-corpus state of the art.
131
+
132
+ \section{Experimental Setting}
133
+
134
+ \subsection{Model and implementation}
135
+
136
+ All experiments use a standalone research codebase derived from Andrej
137
+ Karpathy's MIT-licensed nanochat repository \citep{karpathy_nanochat}. Chat
138
+ interfaces and deployment scripts are removed; the retained components are the
139
+ core BPE tokenizer and causal Transformer template. Derived source files retain
140
+ the original copyright and permission notice.
141
+
142
+ The architecture family keeps the standard Transformer feed-forward expansion
143
+ ratio of four. The central model sizes are L8-H8-D256 (8.39M parameters),
144
+ L12-H8-D320 (17.37M), L16-H8-D384 (31.46M or 32.71M depending on
145
+ vocabulary/run metadata), and L20-H8-D448 (51.84M). The tokenizer vocabulary is
146
+ 4096 in the reported runs. Unless noted otherwise, training uses AdamW,
147
+ learning rate $3\times 10^{-4}$, weight decay 0.1, gradient clipping 1.0, batch
148
+ size 16, and block size 128. All Torch training runs are MPS-only; the runner
149
+ refuses CUDA, CPU execution, and MPS fallback.
150
+
151
+ \subsection{Streaming protocol}
152
+
153
+ We simulate streaming by revealing larger token prefixes over time. A locked
154
+ streaming run keeps the same model and optimizer state across stages. At stage
155
+ $t$, minibatches are sampled only from the currently revealed prefix
156
+ $\unique$. The two main protocols are:
157
+
158
+ \begin{itemize}
159
+ \item 4M validation: OpenWebText10K and WikiText-103 use 250k, 500k, 1M,
160
+ 2M, and 4M prefixes. TinyStories uses 500k, 1M, 2M, and 4M prefixes. These
161
+ runs use five random seeds.
162
+ \item 8M extrapolation: L20-H8-D448 with an 8M final prefix. These runs use
163
+ three seeds because they are substantially more expensive.
164
+ \end{itemize}
165
+
166
+ Each run compares a dropout schedule against a grid of static-dropout
167
+ baselines. The best static baseline is selected after training from the same
168
+ run family, so the decay schedule competes against a strong fixed-rate oracle
169
+ within the tested grid.
170
+
171
+ \section{From Calibration Cells to a Pressure Law}
172
+
173
+ \subsection{Calibration cell definition}
174
+
175
+ A calibration cell is one static dropout sweep at a fixed tuple:
176
+ \[
177
+ (\text{model architecture}, \params, \unique, \sampled, \text{validation split}).
178
+ \]
179
+ Within that cell, we train/evaluate several fixed dropout values and obtain a
180
+ curve
181
+ \[
182
+ p \mapsto L_{\mathrm{val}}(p \mid \params,\unique,\sampled).
183
+ \]
184
+ The target dropout $p^\star$ is the best observed grid point or, when the best
185
+ point is bracketed by worse neighbors, a local quadratic interpolation of the
186
+ minimum. Boundary optima are retained but downweighted in coefficient fitting
187
+ because the true optimum may lie outside the tested grid.
188
+
189
+ Figure~\ref{fig:cell-curves} shows representative cells. The curves are not
190
+ perfect mathematical parabolas, but they show the practical pattern needed for
191
+ calibration: validation loss improves as dropout moves away from zero, reaches
192
+ a local minimum or plateau, and then rises when dropout becomes too large.
193
+
194
+ \begin{figure}[t]
195
+ \centering
196
+ \input{figures/calibration_cell_curves}
197
+ \caption{Representative static dropout calibration cells. The y-axis is
198
+ validation loss above the best tested dropout in that cell, so each curve is
199
+ normalized to show shape rather than absolute corpus difficulty.}
200
+ \label{fig:cell-curves}
201
+ \end{figure}
202
+
203
+ \subsection{Pressure variables}
204
+
205
+ The formula uses two dimensionless pressure variables:
206
+ \[
207
+ x_t=\log_{10}(\params/\unique), \qquad
208
+ y_t=\log_{10}(\sampled/\unique).
209
+ \]
210
+ The first term measures capacity pressure. Increasing parameters at fixed
211
+ prefix increases memorization capacity and tends to increase useful dropout.
212
+ Increasing unique tokens at fixed model reduces this pressure. The second term
213
+ measures repeated-sampling pressure. If the optimizer consumes many sampled
214
+ tokens while the unique prefix remains small, it repeatedly revisits the same
215
+ data and can benefit from stronger regularization.
216
+
217
+ The fitted interaction family is
218
+ \begin{equation}
219
+ \dropout=\clamp(p_{\min},p_{\max},
220
+ A x_t+B y_t+D x_t y_t+C_0).
221
+ \label{eq:interaction-law}
222
+ \end{equation}
223
+ For schedule generation we use $p_{\min}=0.02$ and $p_{\max}=0.65$ unless a
224
+ run explicitly specifies a different clamp. Static sweeps may still include
225
+ 0.0 as an ablation.
226
+
227
+ \subsection{Regime-specific coefficients}
228
+
229
+ We define a regime as the combination of architecture family, tokenizer, corpus
230
+ family, optimizer protocol, dropout placement, stream protocol, and evaluation
231
+ distribution. Model size and prefix size are not regime changes; they are
232
+ inputs to the formula. Corpus family, tokenizer, or optimizer changes can shift
233
+ the coefficients.
234
+
235
+ Table~\ref{tab:coefficients} gives the fitted interaction coefficients. Each
236
+ regime uses 15 or 16 calibration cells. The fitted mean absolute error is about
237
+ 0.016--0.018 dropout, meaning the coefficient law typically predicts the
238
+ calibrated static optimum to within roughly two hundredths of dropout.
239
+
240
+ \begin{table}[t]
241
+ \centering
242
+ \caption{Regime-specific interaction coefficient fits. L-model and L-prefix
243
+ are leave-model-out and leave-prefix-out mean absolute errors.}
244
+ \label{tab:coefficients}
245
+ \resizebox{\textwidth}{!}{\input{tables/coefficient_table}}
246
+ \end{table}
247
+
248
+ Figure~\ref{fig:static-optima} shows the fitted target dropout values from the
249
+ calibration cells. Across regimes, useful dropout generally decreases as the
250
+ unique prefix grows. At a fixed prefix, larger models tend to require larger
251
+ dropout. The absolute surface differs by corpus regime, which is why the paper
252
+ does not propose universal coefficients.
253
+
254
+ \begin{figure}[t]
255
+ \centering
256
+ \input{figures/static_optima_by_regime}
257
+ \caption{Calibration targets by regime. At fixed model size, target dropout
258
+ tends to fall as more unique tokens are available; at fixed prefix, larger
259
+ models tend to have higher target dropout. WikiText-103 reaches the dropout
260
+ floor earlier than the other regimes under its calibration protocol.}
261
+ \label{fig:static-optima}
262
+ \end{figure}
263
+
264
+ \section{Streaming Results}
265
+
266
+ \subsection{Main validation}
267
+
268
+ Table~\ref{tab:streaming-validation} summarizes the main streaming results.
269
+ The ``Gain'' column is best static final validation loss minus decay final
270
+ validation loss, so positive values favor decay. The best static baseline is
271
+ chosen from the tested fixed-dropout grid in the same run family.
272
+
273
+ \begin{table}[t]
274
+ \centering
275
+ \caption{Streaming validation against best fixed-dropout baselines.}
276
+ \label{tab:streaming-validation}
277
+ \resizebox{\textwidth}{!}{\input{tables/streaming_validation_table}}
278
+ \end{table}
279
+
280
+ OpenWebText10K provides the cleanest five-seed coefficient-derived validation:
281
+ the interaction schedule improves final validation loss by 0.0473 over the
282
+ best static baseline and wins all five paired seeds. TinyStories shows a
283
+ smaller but consistent final-loss margin: 0.0133 over the best static baseline
284
+ with five paired wins. WikiText-103 is more nuanced. The earlier
285
+ formula-derived L12 schedule wins the five-seed 4M run, but the later
286
+ static-cell-fitted interaction schedule decays too quickly for L12 and is only
287
+ near the best static baseline. This negative result is important: a good static
288
+ cell fit does not by itself guarantee the best streaming schedule unless the
289
+ schedule objective is aligned with final-loss optimization.
290
+
291
+ \subsection{L20 8M extrapolation}
292
+
293
+ The strongest comparable evidence is the L20 extrapolation tier. Here the
294
+ model is fixed to L20-H8-D448 with 51.84M parameters, and each regime uses
295
+ its fitted interaction coefficients to produce a frozen schedule before
296
+ training. No coefficients are refit on the L20 results. Across OpenWebText10K,
297
+ TinyStories, and WikiText-103, the decay schedule beats the best static
298
+ baseline on final validation loss and wins all paired seeds (3/3 in each
299
+ regime, 9/9 total).
300
+
301
+ Figure~\ref{fig:final-improvements} visualizes the final-loss gains. The
302
+ largest L20 gain is OpenWebText10K (0.0363), followed by WikiText-103 (0.0184)
303
+ and TinyStories (0.0100). The TinyStories gain is smaller because a static
304
+ dropout near 0.20 is already a strong fixed compromise for that regime.
305
+
306
+ \begin{figure}[t]
307
+ \centering
308
+ \input{figures/final_improvements}
309
+ \caption{Final validation-loss improvement over the best fixed-dropout
310
+ baseline. Positive values favor the decay schedule.}
311
+ \label{fig:final-improvements}
312
+ \end{figure}
313
+
314
+ \subsection{Stage-wise behavior}
315
+
316
+ Final-loss improvement does not imply per-prefix dominance. Table~\ref{tab:l20-stage}
317
+ and Figure~\ref{fig:l20-stage-deltas} compare the L20 schedule against the best
318
+ static baseline at each stage. OpenWebText10K starts worse than static 0.30 at
319
+ 250k but becomes better from 1M onward. TinyStories is nearly tied early,
320
+ slightly worse at 1M/2M, then better at 4M/8M. WikiText-103 is worse through
321
+ the middle of the stream and wins only at the final 8M prefix.
322
+
323
+ \begin{table}[t]
324
+ \centering
325
+ \caption{L20 stage-wise comparison. Delta is decay validation loss minus the
326
+ best static validation loss at the same prefix; negative favors decay.}
327
+ \label{tab:l20-stage}
328
+ \resizebox{\textwidth}{!}{\input{tables/l20_stage_table}}
329
+ \end{table}
330
+
331
+ \begin{figure}[t]
332
+ \centering
333
+ \input{figures/l20_stage_deltas}
334
+ \caption{L20 stage-wise deltas versus the best static dropout at each prefix.
335
+ The schedules are final-loss schedules, not greedy per-prefix optima.}
336
+ \label{fig:l20-stage-deltas}
337
+ \end{figure}
338
+
339
+ \section{Patterns and Interpretation}
340
+
341
+ \paragraph{Moderate high dropout, not massive high dropout.}
342
+ The earliest experiments tested very large dropout and failed. A static 0.8
343
+ dropout stream underfit badly, and a high-dropout decay also lost to moderate
344
+ static dropout. This falsified the naive version of the hypothesis. The
345
+ successful schedules start high only when the pressure law predicts high
346
+ dropout, and high means roughly 0.25--0.45 in the L20 extrapolations, not 0.8.
347
+
348
+ \paragraph{Dropout optima move with pressure.}
349
+ The calibration cells support the core mechanism. For the same model, target
350
+ dropout declines as the unique prefix expands. For the same prefix, target
351
+ dropout generally increases with parameter count. This is consistent with the
352
+ view that dropout is compensating for capacity pressure and repeated sampling.
353
+
354
+ \paragraph{The interaction term matters.}
355
+ The interaction term $xy$ captures the fact that model/data pressure and
356
+ sampled-token pressure are not independent. For example, repeated sampling is
357
+ more dangerous when a large model sees a small prefix than when a smaller model
358
+ sees the same prefix. In WikiText-103, the interaction form reduces calibration
359
+ MAE from 0.0449 for the first-order base formula to 0.0162. Similar interaction
360
+ fits are used for OpenWebText10K and TinyStories.
361
+
362
+ \paragraph{Regime specificity is real.}
363
+ The coefficients differ substantially across regimes. TinyStories needs
364
+ different signs and offsets than OpenWebText10K, and WikiText-103 reaches low
365
+ dropout optima earlier under its calibration protocol. This is not a weakness
366
+ of the pressure-law structure; it is evidence that the corpus/tokenizer/eval
367
+ distribution defines the coefficient regime.
368
+
369
+ \paragraph{Final-loss schedules can sacrifice early local optimality.}
370
+ The L20 stage-wise analysis shows that a schedule can lose to a fixed static
371
+ dropout at early or middle prefixes and still win final loss. This is expected
372
+ if the goal is to train one model continuously through a growing stream, not to
373
+ restart and optimize each prefix independently. Static baselines can be locally
374
+ optimal at one prefix but cannot adapt when the stream changes.
375
+
376
+ \section{Limitations}
377
+
378
+ The experiments are intentionally small compared with frontier language-model
379
+ training. The largest model has 51.84M parameters and the largest stream prefix
380
+ has 8M tokens. All training uses Apple MPS, short sequence length, and a 4096
381
+ vocabulary. We therefore claim a reproducible small-scale phenomenon, not a
382
+ confirmed law at billion-parameter scale.
383
+
384
+ The static coefficient fit optimizes dropout targets extracted from static
385
+ sweeps, while the streaming objective is final validation loss after continued
386
+ training. WikiText-103 shows that these objectives can diverge: the fitted
387
+ interaction schedule is excellent as a static optimum model but decays too
388
+ quickly for L12 streaming. Future work should fit coefficients directly against
389
+ streaming objective functions or include trajectory-aware loss terms.
390
+
391
+ The L20 extrapolation tier uses three seeds for cost reasons. This is balanced
392
+ across all three regimes and supported by five-seed smaller-stream validation,
393
+ but larger seed counts would narrow confidence intervals. We also do not claim
394
+ that the tested fixed-dropout grids contain every possible fixed optimum.
395
+
396
+ \section{Reproducibility}
397
+
398
+ The repository is standalone: cached corpora/tokenizers, saved metrics,
399
+ coefficient files, scripts, and reports are stored in the project. The main
400
+ training entry point is \texttt{scripts/run\_experiments.py}; coefficient
401
+ fitting uses \texttt{scripts/fit\_dropout\_coefficients.py}; this manuscript's
402
+ figures and tables are generated by
403
+ \texttt{scripts/generate\_paper\_assets.py}. Existing result artifacts are read
404
+ from \texttt{runs/}. The experiment runner enforces MPS-only Torch execution.
405
+
406
+ All generated tables in this paper are derived from saved metrics JSONL files
407
+ and coefficient JSON files. The paper-generation summary is written to
408
+ \texttt{paper/paper\_results\_summary.json}.
409
+
410
+ \section{Conclusion}
411
+
412
+ The evidence supports a practical, falsifiable hypothesis: in streaming
413
+ language-model training, dropout should decay according to a regime-specific
414
+ pressure law over model size, currently available unique tokens, and cumulative
415
+ sampled tokens. The hypothesis is stronger than ``larger dropout always helps''
416
+ and weaker than ``one universal formula works everywhere.'' Static calibration
417
+ cells reveal local dropout optima; interaction pressure laws fit those optima
418
+ with low error; and the resulting schedules improve final validation loss over
419
+ the best tested fixed-dropout baselines across three regimes, including a
420
+ larger-model 8M-token extrapolation tier. The next scientific step is to test
421
+ the same fitting procedure at larger scale and to optimize coefficients against
422
+ streaming final-loss objectives directly.
423
+
424
+ \bibliographystyle{plainnat}
425
+ \bibliography{references}
426
+
427
+ \end{document}
paper/figures/calibration_cell_curves.tex ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \begin{tikzpicture}
2
+ \begin{groupplot}[
3
+ group style={group size=3 by 1, horizontal sep=1.0cm},
4
+ width=0.32\textwidth,
5
+ height=0.26\textwidth,
6
+ legend style={font=\scriptsize},
7
+ tick label style={font=\scriptsize},
8
+ label style={font=\scriptsize},
9
+ title style={font=\scriptsize},
10
+ ]
11
+
12
+ \nextgroupplot[
13
+ title={OpenWebText10K, L12, 1M},
14
+ xlabel={Dropout},
15
+ ylabel={Validation loss above best},
16
+ ymin=0,
17
+ grid=both,
18
+ ]
19
+ \addplot+[thick, mark=*] coordinates {(0,0.64089) (0.02,0.41177) (0.05,0.21561) (0.08,0.10277) (0.1,0.06467) (0.14,0.02993) (0.2,0.00000) (0.3,0.03437) (0.4,0.09347) (0.5,0.21260) (0.6,0.37234) (0.7,0.57061) (0.8,0.88258) (0.9,2.06141)};
20
+ \addlegendentry{static sweep}
21
+ \addplot+[only marks, mark=star, mark size=3pt] coordinates {(0.2,0)};
22
+ \addlegendentry{target $p^\star=0.20$}
23
+
24
+
25
+ \nextgroupplot[
26
+ title={TinyStories, L12, 1M},
27
+ xlabel={Dropout},
28
+ ylabel={Validation loss above best},
29
+ ymin=0,
30
+ grid=both,
31
+ ]
32
+ \addplot+[thick, mark=*] coordinates {(0,0.40226) (0.04,0.13035) (0.1,0.02502) (0.2,0.00000) (0.35,0.09066) (0.5,0.22441)};
33
+ \addlegendentry{static sweep}
34
+ \addplot+[only marks, mark=star, mark size=3pt] coordinates {(0.2,0)};
35
+ \addlegendentry{target $p^\star=0.20$}
36
+
37
+
38
+ \nextgroupplot[
39
+ title={WikiText-103, L12, 250k},
40
+ xlabel={Dropout},
41
+ ylabel={Validation loss above best},
42
+ ymin=0,
43
+ grid=both,
44
+ ]
45
+ \addplot+[thick, mark=*] coordinates {(0,0.17165) (0.02,0.10888) (0.04,0.07189) (0.06,0.04641) (0.08,0.02649) (0.1,0.00897) (0.14,0.00363) (0.18,0.00000) (0.2,0.01021) (0.26,0.03365) (0.3,0.04794) (0.4,0.13084)};
46
+ \addlegendentry{static sweep}
47
+ \addplot+[only marks, mark=star, mark size=3pt] coordinates {(0.18,0)};
48
+ \addlegendentry{target $p^\star=0.18$}
49
+
50
+ \end{groupplot}
51
+ \end{tikzpicture}
paper/figures/final_improvements.tex ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ \begin{tikzpicture}
3
+ \begin{axis}[
4
+ ybar,
5
+ bar width=8pt,
6
+ width=0.72\textwidth,
7
+ height=0.34\textwidth,
8
+ ymin=0,
9
+ ylabel={Best static final loss $-$ decay final loss},
10
+ symbolic x coords={OWT10K,TinyStories,WikiText},
11
+ xtick=data,
12
+ xtick={OWT10K,TinyStories,WikiText},
13
+ grid=both,
14
+ legend style={font=\scriptsize, at={(0.5,1.04)}, anchor=south, legend columns=2},
15
+ tick label style={font=\scriptsize},
16
+ label style={font=\scriptsize},
17
+ ]
18
+ \addplot coordinates {(OWT10K,0.04732) (TinyStories,0.01326) (WikiText,0.02966)};
19
+ \addlegendentry{4M validation}
20
+ \addplot coordinates {(OWT10K,0.03631) (TinyStories,0.01000) (WikiText,0.01838)};
21
+ \addlegendentry{L20 8M extrapolation}
22
+ \end{axis}
23
+ \end{tikzpicture}
paper/figures/l20_stage_deltas.tex ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \begin{tikzpicture}
2
+ \begin{groupplot}[
3
+ group style={group size=3 by 1, horizontal sep=1.0cm},
4
+ width=0.32\textwidth,
5
+ height=0.27\textwidth,
6
+ tick label style={font=\scriptsize},
7
+ label style={font=\scriptsize},
8
+ title style={font=\scriptsize},
9
+ ]
10
+
11
+ \nextgroupplot[
12
+ title={OpenWebText10K},
13
+ xlabel={Prefix},
14
+ ylabel={Decay $-$ best static},
15
+ xtick={0,1,2,3,4,5},
16
+ xticklabels={250k,500k,1M,2M,4M,8M},
17
+ grid=both,
18
+ ]
19
+ \addplot+[thick, mark=*] coordinates {(0,0.06300) (1,0.00103) (2,-0.01477) (3,-0.02370) (4,-0.02720) (5,-0.03631)};
20
+ \addplot+[black, dashed, mark=none] coordinates {(0,0.00000) (5,0.00000)};
21
+
22
+
23
+ \nextgroupplot[
24
+ title={TinyStories},
25
+ xlabel={Prefix},
26
+ ylabel={Decay $-$ best static},
27
+ xtick={0,1,2,3,4},
28
+ xticklabels={500k,1M,2M,4M,8M},
29
+ grid=both,
30
+ ]
31
+ \addplot+[thick, mark=*] coordinates {(0,0.00050) (1,0.00729) (2,0.00347) (3,-0.00570) (4,-0.01000)};
32
+ \addplot+[black, dashed, mark=none] coordinates {(0,0.00000) (4,0.00000)};
33
+
34
+
35
+ \nextgroupplot[
36
+ title={WikiText-103},
37
+ xlabel={Prefix},
38
+ ylabel={Decay $-$ best static},
39
+ xtick={0,1,2,3,4,5},
40
+ xticklabels={250k,500k,1M,2M,4M,8M},
41
+ grid=both,
42
+ ]
43
+ \addplot+[thick, mark=*] coordinates {(0,0.00803) (1,0.01591) (2,0.03177) (3,0.03542) (4,0.00371) (5,-0.01838)};
44
+ \addplot+[black, dashed, mark=none] coordinates {(0,0.00000) (5,0.00000)};
45
+
46
+ \end{groupplot}
47
+ \end{tikzpicture}
paper/figures/static_optima_by_regime.tex ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \begin{tikzpicture}
2
+ \begin{groupplot}[
3
+ group style={group size=3 by 1, horizontal sep=1.0cm},
4
+ width=0.32\textwidth,
5
+ height=0.28\textwidth,
6
+ legend style={font=\tiny},
7
+ tick label style={font=\scriptsize},
8
+ label style={font=\scriptsize},
9
+ title style={font=\scriptsize},
10
+ ]
11
+
12
+ \nextgroupplot[
13
+ title={OpenWebText10K},
14
+ xlabel={Unique prefix $U$},
15
+ ylabel={Fitted target dropout},
16
+ xtick={0,1,2,3,4},
17
+ xticklabels={250k,500k,1M,2M,4M},
18
+ ymin=0,
19
+ ymax=0.65,
20
+ grid=both,
21
+ ]
22
+ \addplot+[thick, mark=*] coordinates {(0,0.5062) (1,0.3559) (2,0.2174) (3,0.1364) (4,0.0224)};
23
+ \addlegendentry{L12\_H8\_D320}
24
+ \addplot+[thick, mark=*] coordinates {(0,0.6039) (1,0.4475) (2,0.2837) (3,0.1388) (4,0.0272)};
25
+ \addlegendentry{L16\_H8\_D384}
26
+ \addplot+[thick, mark=*] coordinates {(0,0.3837) (1,0.2401) (2,0.1463) (3,0.0691) (4,0.0000)};
27
+ \addlegendentry{L8\_H8\_D256}
28
+
29
+
30
+ \nextgroupplot[
31
+ title={TinyStories},
32
+ xlabel={Unique prefix $U$},
33
+ ylabel={Fitted target dropout},
34
+ xtick={0,1,2,3,4},
35
+ xticklabels={250k,500k,1M,2M,4M},
36
+ ymin=0,
37
+ ymax=0.65,
38
+ grid=both,
39
+ ]
40
+ \addplot+[thick, mark=*] coordinates {(0,0.4575) (1,0.2947) (2,0.1866) (3,0.0703) (4,0.0303)};
41
+ \addlegendentry{L12\_H8\_D320}
42
+ \addplot+[thick, mark=*] coordinates {(0,0.5699) (2,0.2450) (4,0.0468)};
43
+ \addlegendentry{L16\_H8\_D384}
44
+ \addplot+[thick, mark=*] coordinates {(0,0.3491) (2,0.0977) (4,0.0000)};
45
+ \addlegendentry{L8\_H8\_D256}
46
+
47
+
48
+ \nextgroupplot[
49
+ title={WikiText-103},
50
+ xlabel={Unique prefix $U$},
51
+ ylabel={Fitted target dropout},
52
+ xtick={0,1,2,3,4},
53
+ xticklabels={250k,500k,1M,2M,4M},
54
+ ymin=0,
55
+ ymax=0.65,
56
+ grid=both,
57
+ ]
58
+ \addplot+[thick, mark=*] coordinates {(0,0.1645) (1,0.0299) (2,0.0000) (3,0.0000) (4,0.0000)};
59
+ \addlegendentry{L12\_H8\_D320}
60
+ \addplot+[thick, mark=*] coordinates {(0,0.2459) (1,0.0822) (2,0.0225) (3,0.0000) (4,0.0000)};
61
+ \addlegendentry{L16\_H8\_D384}
62
+ \addplot+[thick, mark=*] coordinates {(0,0.0965) (1,0.0114) (2,0.0000) (3,0.0000) (4,0.0000)};
63
+ \addlegendentry{L8\_H8\_D256}
64
+
65
+ \end{groupplot}
66
+ \end{tikzpicture}
paper/paper_results_summary.json ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "openweb": {
3
+ "coefficients": {
4
+ "regime": "OpenWebText10K",
5
+ "cells": 15,
6
+ "A": 0.08189560629877818,
7
+ "B": -0.023948107975524876,
8
+ "D": 0.1536984646521253,
9
+ "C0": -0.05559686586455526,
10
+ "mae": 0.01579101988615387,
11
+ "rmse": 0.018845563252758567,
12
+ "leave_model_mae": 0.024771721924833498,
13
+ "leave_prefix_mae": 0.030983393290384315,
14
+ "formula": "p = +0.081896 * log10(P / U) -0.023948 * log10(C / U) +0.153698 * log10(P / U) * log10(C / U) -0.055597"
15
+ },
16
+ "small_stream": {
17
+ "n": 5,
18
+ "final_stage": 4,
19
+ "final_prefix": 4000000,
20
+ "condition_final_mean": 4.3981304407119755,
21
+ "condition_final_std": 0.009545784836147743,
22
+ "best_static_condition": "static_dropout_0.14",
23
+ "best_static_mean": 4.44545366615057,
24
+ "best_static_std": 0.012017216742245517,
25
+ "improvement": 0.04732322543859446,
26
+ "paired_deltas": [
27
+ -0.03942108899354935,
28
+ -0.05825117975473404,
29
+ -0.032753005623817444,
30
+ -0.052590467035770416,
31
+ -0.053600385785102844
32
+ ],
33
+ "paired_wins": 5,
34
+ "paired_mean_delta": -0.04732322543859482,
35
+ "paired_std_delta": 0.010738969078098962,
36
+ "stage_deltas": [
37
+ {
38
+ "stage": 0,
39
+ "prefix": 250000,
40
+ "condition_dropout": 0.385,
41
+ "condition_mean": 5.4946602284908295,
42
+ "condition_std": 0.01093302726132647,
43
+ "best_static_condition": "static_dropout_0.3",
44
+ "best_static_mean": 5.448330155014991,
45
+ "best_static_std": 0.0138285316736341,
46
+ "delta": 0.04633007347583806
47
+ },
48
+ {
49
+ "stage": 1,
50
+ "prefix": 500000,
51
+ "condition_dropout": 0.319,
52
+ "condition_mean": 5.071460470557213,
53
+ "condition_std": 0.01179360076463939,
54
+ "best_static_condition": "static_dropout_0.3",
55
+ "best_static_mean": 5.066737298667431,
56
+ "best_static_std": 0.017273470277214743,
57
+ "delta": 0.004723171889781774
58
+ },
59
+ {
60
+ "stage": 2,
61
+ "prefix": 1000000,
62
+ "condition_dropout": 0.227,
63
+ "condition_mean": 4.781069016456604,
64
+ "condition_std": 0.008428247627355752,
65
+ "best_static_condition": "static_dropout_0.3",
66
+ "best_static_mean": 4.79825523942709,
67
+ "best_static_std": 0.01441949497608529,
68
+ "delta": -0.01718622297048622
69
+ },
70
+ {
71
+ "stage": 3,
72
+ "prefix": 2000000,
73
+ "condition_dropout": 0.139,
74
+ "condition_mean": 4.558990895748138,
75
+ "condition_std": 0.014177173687439483,
76
+ "best_static_condition": "static_dropout_0.3",
77
+ "best_static_mean": 4.603498187661171,
78
+ "best_static_std": 0.014129740963263297,
79
+ "delta": -0.04450729191303271
80
+ },
81
+ {
82
+ "stage": 4,
83
+ "prefix": 4000000,
84
+ "condition_dropout": 0.066,
85
+ "condition_mean": 4.3981304407119755,
86
+ "condition_std": 0.009545784836147743,
87
+ "best_static_condition": "static_dropout_0.14",
88
+ "best_static_mean": 4.44545366615057,
89
+ "best_static_std": 0.012017216742245517,
90
+ "delta": -0.04732322543859446
91
+ }
92
+ ]
93
+ },
94
+ "l20_stream": {
95
+ "n": 3,
96
+ "final_stage": 5,
97
+ "final_prefix": 8000000,
98
+ "condition_final_mean": 4.179318442940712,
99
+ "condition_final_std": 0.01344880214961143,
100
+ "best_static_condition": "static_dropout_0.2",
101
+ "best_static_mean": 4.215624410659075,
102
+ "best_static_std": 0.014242200766109402,
103
+ "improvement": 0.03630596771836281,
104
+ "paired_deltas": [
105
+ -0.03935118764638901,
106
+ -0.025513865053653717,
107
+ -0.03717709332704544
108
+ ],
109
+ "paired_wins": 3,
110
+ "paired_mean_delta": -0.03401404867569605,
111
+ "paired_std_delta": 0.007441203715372809,
112
+ "stage_deltas": [
113
+ {
114
+ "stage": 0,
115
+ "prefix": 250000,
116
+ "condition_dropout": 0.433,
117
+ "condition_mean": 5.460076779127121,
118
+ "condition_std": 0.0012588964293079841,
119
+ "best_static_condition": "static_dropout_0.3",
120
+ "best_static_mean": 5.39707263559103,
121
+ "best_static_std": 0.014451385161445679,
122
+ "delta": 0.06300414353609085
123
+ },
124
+ {
125
+ "stage": 1,
126
+ "prefix": 500000,
127
+ "condition_dropout": 0.367,
128
+ "condition_mean": 5.017915904521942,
129
+ "condition_std": 0.005764488447180914,
130
+ "best_static_condition": "static_dropout_0.3",
131
+ "best_static_mean": 5.01688560595115,
132
+ "best_static_std": 0.012061853156578773,
133
+ "delta": 0.0010302985707921763
134
+ },
135
+ {
136
+ "stage": 2,
137
+ "prefix": 1000000,
138
+ "condition_dropout": 0.27,
139
+ "condition_mean": 4.7070309941967325,
140
+ "condition_std": 0.015739562982191573,
141
+ "best_static_condition": "static_dropout_0.3",
142
+ "best_static_mean": 4.72180004666249,
143
+ "best_static_std": 0.02152489745362506,
144
+ "delta": -0.014769052465757326
145
+ },
146
+ {
147
+ "stage": 3,
148
+ "prefix": 2000000,
149
+ "condition_dropout": 0.17600000000000002,
150
+ "condition_mean": 4.487937837839127,
151
+ "condition_std": 0.009026555295310244,
152
+ "best_static_condition": "static_dropout_0.2",
153
+ "best_static_mean": 4.511637444297473,
154
+ "best_static_std": 0.016469397224544248,
155
+ "delta": -0.023699606458346345
156
+ },
157
+ {
158
+ "stage": 4,
159
+ "prefix": 4000000,
160
+ "condition_dropout": 0.09500000000000001,
161
+ "condition_mean": 4.311577834188938,
162
+ "condition_std": 0.015007670903308238,
163
+ "best_static_condition": "static_dropout_0.2",
164
+ "best_static_mean": 4.338780450324218,
165
+ "best_static_std": 0.01698295581756926,
166
+ "delta": -0.027202616135279634
167
+ },
168
+ {
169
+ "stage": 5,
170
+ "prefix": 8000000,
171
+ "condition_dropout": 0.033,
172
+ "condition_mean": 4.179318442940712,
173
+ "condition_std": 0.01344880214961143,
174
+ "best_static_condition": "static_dropout_0.2",
175
+ "best_static_mean": 4.215624410659075,
176
+ "best_static_std": 0.014242200766109402,
177
+ "delta": -0.03630596771836281
178
+ }
179
+ ]
180
+ }
181
+ },
182
+ "tinystories": {
183
+ "coefficients": {
184
+ "regime": "TinyStories",
185
+ "cells": 16,
186
+ "A": -0.08926115772929914,
187
+ "B": -0.1297539607354037,
188
+ "D": 0.2550686141985067,
189
+ "C0": 0.08152490344304414,
190
+ "mae": 0.01799638717496229,
191
+ "rmse": 0.023120178816187012,
192
+ "leave_model_mae": 0.029523601005481504,
193
+ "leave_prefix_mae": 0.03253257579449062,
194
+ "formula": "p = -0.089261 * log10(P / U) -0.129754 * log10(C / U) +0.255069 * log10(P / U) * log10(C / U) +0.081525"
195
+ },
196
+ "small_stream": {
197
+ "n": 5,
198
+ "final_stage": 3,
199
+ "final_prefix": 4000000,
200
+ "condition_final_mean": 2.5311126589775084,
201
+ "condition_final_std": 0.021347338722582713,
202
+ "best_static_condition": "static_dropout_0.08",
203
+ "best_static_mean": 2.5443769969046115,
204
+ "best_static_std": 0.021138839821398296,
205
+ "improvement": 0.013264337927103131,
206
+ "paired_deltas": [
207
+ -0.0005062669515609741,
208
+ -0.021110694855451584,
209
+ -0.009244173765182495,
210
+ -0.016586389392614365,
211
+ -0.014097623527050018
212
+ ],
213
+ "paired_wins": 5,
214
+ "paired_mean_delta": -0.012309029698371887,
215
+ "paired_std_delta": 0.00786865194496459,
216
+ "stage_deltas": [
217
+ {
218
+ "stage": 0,
219
+ "prefix": 500000,
220
+ "condition_dropout": 0.184,
221
+ "condition_mean": 3.232573334872723,
222
+ "condition_std": 0.012322249630223228,
223
+ "best_static_condition": "static_dropout_0.12",
224
+ "best_static_mean": 3.2225750528275965,
225
+ "best_static_std": 0.014316453554089874,
226
+ "delta": 0.009998282045126317
227
+ },
228
+ {
229
+ "stage": 1,
230
+ "prefix": 1000000,
231
+ "condition_dropout": 0.141,
232
+ "condition_mean": 2.8907706700265408,
233
+ "condition_std": 0.00267614743609897,
234
+ "best_static_condition": "static_dropout_0.12",
235
+ "best_static_mean": 2.893001724779606,
236
+ "best_static_std": 0.012108793042410648,
237
+ "delta": -0.002231054753065287
238
+ },
239
+ {
240
+ "stage": 2,
241
+ "prefix": 2000000,
242
+ "condition_dropout": 0.084,
243
+ "condition_mean": 2.6690264880657195,
244
+ "condition_std": 0.020694473868817478,
245
+ "best_static_condition": "static_dropout_0.12",
246
+ "best_static_mean": 2.6794611223042013,
247
+ "best_static_std": 0.016337678176899628,
248
+ "delta": -0.010434634238481788
249
+ },
250
+ {
251
+ "stage": 3,
252
+ "prefix": 4000000,
253
+ "condition_dropout": 0.045,
254
+ "condition_mean": 2.5311126589775084,
255
+ "condition_std": 0.021347338722582713,
256
+ "best_static_condition": "static_dropout_0.08",
257
+ "best_static_mean": 2.5443769969046115,
258
+ "best_static_std": 0.021138839821398296,
259
+ "delta": -0.013264337927103131
260
+ }
261
+ ]
262
+ },
263
+ "l20_stream": {
264
+ "n": 3,
265
+ "final_stage": 4,
266
+ "final_prefix": 8000000,
267
+ "condition_final_mean": 2.208572393283248,
268
+ "condition_final_std": 0.004634657584019386,
269
+ "best_static_condition": "static_dropout_0.2",
270
+ "best_static_mean": 2.218574500953158,
271
+ "best_static_std": 0.003241222983607362,
272
+ "improvement": 0.010002107669909943,
273
+ "paired_deltas": [
274
+ -0.00869341753423214,
275
+ -0.006095590069890022,
276
+ -0.013002954423427582
277
+ ],
278
+ "paired_wins": 3,
279
+ "paired_mean_delta": -0.00926398734251658,
280
+ "paired_std_delta": 0.0034888512447114515,
281
+ "stage_deltas": [
282
+ {
283
+ "stage": 0,
284
+ "prefix": 500000,
285
+ "condition_dropout": 0.253,
286
+ "condition_mean": 3.030243801573912,
287
+ "condition_std": 0.006391037258560546,
288
+ "best_static_condition": "static_dropout_0.2",
289
+ "best_static_mean": 3.02974146977067,
290
+ "best_static_std": 0.006865445597812618,
291
+ "delta": 0.0005023318032422175
292
+ },
293
+ {
294
+ "stage": 1,
295
+ "prefix": 1000000,
296
+ "condition_dropout": 0.209,
297
+ "condition_mean": 2.7195058229068914,
298
+ "condition_std": 0.01655123058742941,
299
+ "best_static_condition": "static_dropout_0.26",
300
+ "best_static_mean": 2.712211973965168,
301
+ "best_static_std": 0.01060853933383755,
302
+ "delta": 0.007293848941723358
303
+ },
304
+ {
305
+ "stage": 2,
306
+ "prefix": 2000000,
307
+ "condition_dropout": 0.137,
308
+ "condition_mean": 2.476810533553362,
309
+ "condition_std": 0.006082638165661795,
310
+ "best_static_condition": "static_dropout_0.2",
311
+ "best_static_mean": 2.4733379955093064,
312
+ "best_static_std": 0.009827865290864221,
313
+ "delta": 0.003472538044055451
314
+ },
315
+ {
316
+ "stage": 3,
317
+ "prefix": 4000000,
318
+ "condition_dropout": 0.077,
319
+ "condition_mean": 2.3110335220893226,
320
+ "condition_std": 0.015477651713059048,
321
+ "best_static_condition": "static_dropout_0.2",
322
+ "best_static_mean": 2.3167371824383736,
323
+ "best_static_std": 0.008877543510132386,
324
+ "delta": -0.00570366034905101
325
+ },
326
+ {
327
+ "stage": 4,
328
+ "prefix": 8000000,
329
+ "condition_dropout": 0.041,
330
+ "condition_mean": 2.208572393283248,
331
+ "condition_std": 0.004634657584019386,
332
+ "best_static_condition": "static_dropout_0.2",
333
+ "best_static_mean": 2.218574500953158,
334
+ "best_static_std": 0.003241222983607362,
335
+ "delta": -0.010002107669909943
336
+ }
337
+ ]
338
+ }
339
+ },
340
+ "wikitext": {
341
+ "coefficients": {
342
+ "regime": "WikiText-103",
343
+ "cells": 15,
344
+ "A": -0.002674916387321836,
345
+ "B": -0.2836210480242947,
346
+ "D": 0.2686063066585002,
347
+ "C0": -0.01899454503425616,
348
+ "mae": 0.016244092648220347,
349
+ "rmse": 0.019131667586788335,
350
+ "leave_model_mae": 0.030438322968748356,
351
+ "leave_prefix_mae": 0.04435212839791333,
352
+ "formula": "p = -0.002675 * log10(P / U) -0.283621 * log10(C / U) +0.268606 * log10(P / U) * log10(C / U) -0.018995"
353
+ },
354
+ "small_stream": {
355
+ "n": 5,
356
+ "final_stage": 4,
357
+ "final_prefix": 4000000,
358
+ "condition_final_mean": 4.080797865241766,
359
+ "condition_final_std": 0.019458062276034267,
360
+ "best_static_condition": "static_dropout_0.1",
361
+ "best_static_mean": 4.1104619488120075,
362
+ "best_static_std": 0.018806640195540816,
363
+ "improvement": 0.02966408357024175,
364
+ "paired_deltas": [
365
+ -0.018418047577142715,
366
+ -0.018113188445568085,
367
+ -0.027215413749217987,
368
+ -0.02507050335407257,
369
+ -0.037036627531051636
370
+ ],
371
+ "paired_wins": 5,
372
+ "paired_mean_delta": -0.0251707561314106,
373
+ "paired_std_delta": 0.007752385341386418,
374
+ "stage_deltas": [
375
+ {
376
+ "stage": 0,
377
+ "prefix": 250000,
378
+ "condition_dropout": 0.3,
379
+ "condition_mean": 5.21483291387558,
380
+ "condition_std": 0.018060268226475495,
381
+ "best_static_condition": "static_dropout_0.18",
382
+ "best_static_mean": 5.16162400841713,
383
+ "best_static_std": 0.014987630048664583,
384
+ "delta": 0.05320890545845014
385
+ },
386
+ {
387
+ "stage": 1,
388
+ "prefix": 500000,
389
+ "condition_dropout": 0.26,
390
+ "condition_mean": 4.80534844994545,
391
+ "condition_std": 0.027829930328897787,
392
+ "best_static_condition": "static_dropout_0.2",
393
+ "best_static_mean": 4.787301687896251,
394
+ "best_static_std": 0.023601504941398347,
395
+ "delta": 0.01804676204919886
396
+ },
397
+ {
398
+ "stage": 2,
399
+ "prefix": 1000000,
400
+ "condition_dropout": 0.18,
401
+ "condition_mean": 4.49377838075161,
402
+ "condition_std": 0.014674108647494532,
403
+ "best_static_condition": "static_dropout_0.14",
404
+ "best_static_mean": 4.500055414438248,
405
+ "best_static_std": 0.014819407090300617,
406
+ "delta": -0.006277033686638234
407
+ },
408
+ {
409
+ "stage": 3,
410
+ "prefix": 2000000,
411
+ "condition_dropout": 0.09,
412
+ "condition_mean": 4.260725708305836,
413
+ "condition_std": 0.018098739272622868,
414
+ "best_static_condition": "static_dropout_0.14",
415
+ "best_static_mean": 4.281563547253609,
416
+ "best_static_std": 0.02138858846966249,
417
+ "delta": -0.020837838947772802
418
+ },
419
+ {
420
+ "stage": 4,
421
+ "prefix": 4000000,
422
+ "condition_dropout": 0.02,
423
+ "condition_mean": 4.080797865241766,
424
+ "condition_std": 0.019458062276034267,
425
+ "best_static_condition": "static_dropout_0.1",
426
+ "best_static_mean": 4.1104619488120075,
427
+ "best_static_std": 0.018806640195540816,
428
+ "delta": -0.02966408357024175
429
+ }
430
+ ]
431
+ },
432
+ "l20_stream": {
433
+ "n": 3,
434
+ "final_stage": 5,
435
+ "final_prefix": 8000000,
436
+ "condition_final_mean": 3.957240684578816,
437
+ "condition_final_std": 0.008487626829204101,
438
+ "best_static_condition": "static_dropout_0.14",
439
+ "best_static_mean": 3.9756230898201466,
440
+ "best_static_std": 0.008723729300606414,
441
+ "improvement": 0.018382405241330613,
442
+ "paired_deltas": [
443
+ -0.016518089920282364,
444
+ -0.00015844032168388367,
445
+ -0.027258697897195816
446
+ ],
447
+ "paired_wins": 3,
448
+ "paired_mean_delta": -0.014645076046387354,
449
+ "paired_std_delta": 0.013646872385042126,
450
+ "stage_deltas": [
451
+ {
452
+ "stage": 0,
453
+ "prefix": 250000,
454
+ "condition_dropout": 0.284,
455
+ "condition_mean": 5.2075243567427,
456
+ "condition_std": 0.013993230772716539,
457
+ "best_static_condition": "static_dropout_0.26",
458
+ "best_static_mean": 5.1994956483443575,
459
+ "best_static_std": 0.013634536236337126,
460
+ "delta": 0.008028708398342133
461
+ },
462
+ {
463
+ "stage": 1,
464
+ "prefix": 500000,
465
+ "condition_dropout": 0.211,
466
+ "condition_mean": 4.816951217750709,
467
+ "condition_std": 0.0272929978517565,
468
+ "best_static_condition": "static_dropout_0.3",
469
+ "best_static_mean": 4.8010413100322085,
470
+ "best_static_std": 0.023497612891923463,
471
+ "delta": 0.015909907718500094
472
+ },
473
+ {
474
+ "stage": 2,
475
+ "prefix": 1000000,
476
+ "condition_dropout": 0.116,
477
+ "condition_mean": 4.538959090908368,
478
+ "condition_std": 0.018682984974041847,
479
+ "best_static_condition": "static_dropout_0.26",
480
+ "best_static_mean": 4.507193277279536,
481
+ "best_static_std": 0.022063926876841285,
482
+ "delta": 0.03176581362883191
483
+ },
484
+ {
485
+ "stage": 3,
486
+ "prefix": 2000000,
487
+ "condition_dropout": 0.03599999999999999,
488
+ "condition_mean": 4.310809387514989,
489
+ "condition_std": 0.02431455246903313,
490
+ "best_static_condition": "static_dropout_0.26",
491
+ "best_static_mean": 4.275388439496358,
492
+ "best_static_std": 0.021064935851257058,
493
+ "delta": 0.03542094801863094
494
+ },
495
+ {
496
+ "stage": 4,
497
+ "prefix": 4000000,
498
+ "condition_dropout": 0.02,
499
+ "condition_mean": 4.1124994705120725,
500
+ "condition_std": 0.024692308091551114,
501
+ "best_static_condition": "static_dropout_0.18",
502
+ "best_static_mean": 4.108785220732291,
503
+ "best_static_std": 0.016546231012667934,
504
+ "delta": 0.003714249779781298
505
+ },
506
+ {
507
+ "stage": 5,
508
+ "prefix": 8000000,
509
+ "condition_dropout": 0.02,
510
+ "condition_mean": 3.957240684578816,
511
+ "condition_std": 0.008487626829204101,
512
+ "best_static_condition": "static_dropout_0.14",
513
+ "best_static_mean": 3.9756230898201466,
514
+ "best_static_std": 0.008723729300606414,
515
+ "delta": -0.018382405241330613
516
+ }
517
+ ]
518
+ }
519
+ }
520
+ }
paper/references.bib CHANGED
@@ -33,6 +33,14 @@
33
  url = {https://arxiv.org/abs/2203.15556}
34
  }
35
 
 
 
 
 
 
 
 
 
36
  @inproceedings{merity2017pointer,
37
  title = {Pointer Sentinel Mixture Models},
38
  author = {Merity, Stephen and Xiong, Caiming and Bradbury, James and Socher, Richard},
 
33
  url = {https://arxiv.org/abs/2203.15556}
34
  }
35
 
36
+ @misc{gokaslan2019openwebtext,
37
+ title = {OpenWebText Corpus},
38
+ author = {Gokaslan, Aaron and Cohen, Vanya and Pavlick, Ellie and Tellex, Stefanie},
39
+ year = {2019},
40
+ howpublished = {\url{https://zenodo.org/records/3834942}},
41
+ note = {Open reproduction of the WebText-style corpus}
42
+ }
43
+
44
  @inproceedings{merity2017pointer,
45
  title = {Pointer Sentinel Mixture Models},
46
  author = {Merity, Stephen and Xiong, Caiming and Bradbury, James and Socher, Richard},
paper/tables/coefficient_table.tex ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ \begin{tabular}{lrrrrrrrr}
2
+ \toprule
3
+ Regime & Cells & A & B & D & C0 & MAE & L-model MAE & L-prefix MAE \\
4
+ \midrule
5
+ OpenWebText10K & 15 & 0.0819 & -0.0239 & 0.1537 & -0.0556 & 0.0158 & 0.0248 & 0.0310 \\
6
+ TinyStories & 16 & -0.0893 & -0.1298 & 0.2551 & 0.0815 & 0.0180 & 0.0295 & 0.0325 \\
7
+ WikiText-103 & 15 & -0.0027 & -0.2836 & 0.2686 & -0.0190 & 0.0162 & 0.0304 & 0.0444 \\
8
+ \bottomrule
9
+ \end{tabular}
paper/tables/l20_stage_table.tex ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \begin{tabular}{lrrrrr}
2
+ \toprule
3
+ Regime & Prefix & Decay dropout & Decay val & Best static val & Delta \\
4
+ \midrule
5
+ OpenWebText10K & 250k & 0.433 & 5.4601 & 5.3971 & +0.0630 \\
6
+ OpenWebText10K & 500k & 0.367 & 5.0179 & 5.0169 & +0.0010 \\
7
+ OpenWebText10K & 1M & 0.270 & 4.7070 & 4.7218 & -0.0148 \\
8
+ OpenWebText10K & 2M & 0.176 & 4.4879 & 4.5116 & -0.0237 \\
9
+ OpenWebText10K & 4M & 0.095 & 4.3116 & 4.3388 & -0.0272 \\
10
+ OpenWebText10K & 8M & 0.033 & 4.1793 & 4.2156 & -0.0363 \\
11
+ TinyStories & 500k & 0.253 & 3.0302 & 3.0297 & +0.0005 \\
12
+ TinyStories & 1M & 0.209 & 2.7195 & 2.7122 & +0.0073 \\
13
+ TinyStories & 2M & 0.137 & 2.4768 & 2.4733 & +0.0035 \\
14
+ TinyStories & 4M & 0.077 & 2.3110 & 2.3167 & -0.0057 \\
15
+ TinyStories & 8M & 0.041 & 2.2086 & 2.2186 & -0.0100 \\
16
+ WikiText-103 & 250k & 0.284 & 5.2075 & 5.1995 & +0.0080 \\
17
+ WikiText-103 & 500k & 0.211 & 4.8170 & 4.8010 & +0.0159 \\
18
+ WikiText-103 & 1M & 0.116 & 4.5390 & 4.5072 & +0.0318 \\
19
+ WikiText-103 & 2M & 0.036 & 4.3108 & 4.2754 & +0.0354 \\
20
+ WikiText-103 & 4M & 0.020 & 4.1125 & 4.1088 & +0.0037 \\
21
+ WikiText-103 & 8M & 0.020 & 3.9572 & 3.9756 & -0.0184 \\
22
+ \bottomrule
23
+ \end{tabular}
paper/tables/streaming_validation_table.tex ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \begin{tabular}{llrrrrr}
2
+ \toprule
3
+ Regime & Protocol & Decay final & Best static final & Gain & Seeds & Paired wins \\
4
+ \midrule
5
+ OpenWebText10K & 4M / 5 seeds & 4.3981 $\pm$ 0.0095 & 4.4455 $\pm$ 0.0120 & 0.0473 & 5 & 5/5 \\
6
+ OpenWebText10K & 8M / 3 seeds & 4.1793 $\pm$ 0.0134 & 4.2156 $\pm$ 0.0142 & 0.0363 & 3 & 3/3 \\
7
+ TinyStories & 4M / 5 seeds & 2.5311 $\pm$ 0.0213 & 2.5444 $\pm$ 0.0211 & 0.0133 & 5 & 5/5 \\
8
+ TinyStories & 8M / 3 seeds & 2.2086 $\pm$ 0.0046 & 2.2186 $\pm$ 0.0032 & 0.0100 & 3 & 3/3 \\
9
+ WikiText-103 & 4M / 5 seeds & 4.0808 $\pm$ 0.0195 & 4.1105 $\pm$ 0.0188 & 0.0297 & 5 & 5/5 \\
10
+ WikiText-103 & 8M / 3 seeds & 3.9572 $\pm$ 0.0085 & 3.9756 $\pm$ 0.0087 & 0.0184 & 3 & 3/3 \\
11
+ \bottomrule
12
+ \end{tabular}
runs/smoke_compat/locked_stream/20260605-224834/RESULT_SUMMARY.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Locked Streaming Dropout Summary
2
+
3
+ Run directory: `runs/smoke_compat/locked_stream/20260605-224834`
4
+
5
+ Model: `smoke` causal Transformer, 134,144 parameters, 1 layers, 1 heads, 16 embedding dim.
6
+ Training per stage: 2 steps. Sampled tokens are cumulative in each stage row. Seeds present: 123.
7
+
8
+ ## Condition Ranking
9
+
10
+ | Condition | Kind | Final dropout | Mean trajectory val loss | Final val loss | Final gap | Dropout path |
11
+ |---|---|---:|---:|---:|---:|---|
12
+ | `static_dropout_0.05` | static | 0.05 | 8.3189 | 8.3194 | 0.0069 | 0.05 -> 0.05 |
13
+ | `smoke_decay` | anchor_decay | 0.05 | 8.3189 | 8.3194 | 0.0014 | 0.10 -> 0.05 |
14
+
15
+ ## Stage Trajectory
16
+
17
+ ### Stage 0: 512 Prefix Tokens
18
+
19
+ | Condition | Dropout | Mean val loss | Mean train loss | Mean gap | N |
20
+ |---|---:|---:|---:|---:|---:|
21
+ | `static_dropout_0.05` | 0.05 | 8.3184 | 8.3184 | -0.0001 | 1 |
22
+ | `smoke_decay` | 0.10 | 8.3185 | 8.3181 | 0.0003 | 1 |
23
+
24
+ ### Stage 1: 768 Prefix Tokens
25
+
26
+ | Condition | Dropout | Mean val loss | Mean train loss | Mean gap | N |
27
+ |---|---:|---:|---:|---:|---:|
28
+ | `smoke_decay` | 0.05 | 8.3194 | 8.3180 | 0.0014 | 1 |
29
+ | `static_dropout_0.05` | 0.05 | 8.3194 | 8.3125 | 0.0069 | 1 |
runs/smoke_compat/locked_stream/20260605-224834/config.json ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "args": {
3
+ "mode": "locked_stream",
4
+ "corpus": null,
5
+ "corpus_glob": null,
6
+ "text_column": "text",
7
+ "use_cached_data": true,
8
+ "output_dir": "runs/smoke_compat",
9
+ "resume_from": null,
10
+ "cache_dir": ".cache/dropout_decay",
11
+ "models": [
12
+ "smoke=1x1x16"
13
+ ],
14
+ "seeds": [
15
+ 123
16
+ ],
17
+ "token_limits": [
18
+ 5000000
19
+ ],
20
+ "stream_token_caps": [
21
+ 512,
22
+ 768
23
+ ],
24
+ "val_tokens": 128,
25
+ "allow_short_corpus": true,
26
+ "force_retokenize": false,
27
+ "vocab_size": 4096,
28
+ "tokenizer_train_chars": 10000000,
29
+ "block_size": 16,
30
+ "batch_size": 2,
31
+ "steps": 2000,
32
+ "stage_steps": 2,
33
+ "dropout_rates": [
34
+ 0.05
35
+ ],
36
+ "decays": [],
37
+ "anchor_decays": [
38
+ {
39
+ "name": "smoke_decay",
40
+ "kind": "anchor_decay",
41
+ "initial": 0.1,
42
+ "final": 0.05,
43
+ "schedule": "log_prefix_anchor",
44
+ "decay_tokens": null,
45
+ "anchors": [
46
+ [
47
+ 512,
48
+ 0.1
49
+ ],
50
+ [
51
+ 768,
52
+ 0.05
53
+ ]
54
+ ]
55
+ }
56
+ ],
57
+ "decay_tokens": null,
58
+ "eval_batches": 1,
59
+ "train_eval_batches": 1,
60
+ "trace_eval_batches": 1,
61
+ "eval_every": 0,
62
+ "log_every": 1,
63
+ "lr": 0.0003,
64
+ "weight_decay": 0.1,
65
+ "grad_clip": 1.0,
66
+ "plateau_delta": 0.01,
67
+ "target_min_dropout": 0.1,
68
+ "min_nonzero_margin": 0.01,
69
+ "min_high_dropout_margin": 0.03,
70
+ "screen_early_stop": false,
71
+ "screen_prune_patience": 3,
72
+ "screen_prune_min_delta": 0.01
73
+ },
74
+ "mode": "locked_stream",
75
+ "seeds": [
76
+ 123
77
+ ],
78
+ "models": [
79
+ {
80
+ "model_name": "smoke",
81
+ "n_layer": 1,
82
+ "n_head": 1,
83
+ "n_embd": 16
84
+ }
85
+ ],
86
+ "device": "mps",
87
+ "torch": "2.12.0",
88
+ "python": "3.11.15 (main, Mar 3 2026, 00:52:57) [Clang 21.0.0 (clang-2100.0.123.102)]",
89
+ "mps_available": true,
90
+ "attribution": "Derived from Andrej Karpathy's nanochat project (https://github.com/karpathy/nanochat), MIT License, Copyright (c) 2025 Andrej Karpathy.",
91
+ "tokenizer_path": ".cache/dropout_decay/tokenizer-v4096.json",
92
+ "encoded_path": ".cache/dropout_decay/tokens-v4096-uint16.npy",
93
+ "train_tokens": 5500842,
94
+ "val_tokens": 128,
95
+ "effective_token_limits": [
96
+ 5000000
97
+ ],
98
+ "effective_stream_token_caps": [
99
+ 512,
100
+ 768
101
+ ],
102
+ "resume_from": null
103
+ }
runs/smoke_compat/locked_stream/20260605-224834/metrics.jsonl ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {"condition": "smoke_decay", "condition_kind": "anchor_decay", "dropout_active_final": 0.1, "dropout_final": 0.05, "dropout_initial": 0.1, "dropout_schedule": "log_prefix_anchor", "elapsed_sec": 0.08690309524536133, "eval_loss": 8.318451881408691, "generalization_gap": 0.00031757354736328125, "model_config": {"block_size": 16, "dropout": 0.1, "n_embd": 16, "n_head": 1, "n_layer": 1, "vocab_size": 4096}, "model_name": "smoke", "n_embd": 16, "n_head": 1, "n_layer": 1, "parameters": 134144, "run_mode": "locked_stream", "seed": 123, "stage": 0, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_eval_loss": 8.318134307861328, "train_loss_last": 8.317201614379883, "val_eval_loss": 8.318451881408691}
2
+ {"condition": "smoke_decay", "condition_kind": "anchor_decay", "dropout_active_final": 0.05, "dropout_final": 0.05, "dropout_initial": 0.1, "dropout_schedule": "log_prefix_anchor", "elapsed_sec": 0.018578290939331055, "eval_loss": 8.319382667541504, "generalization_gap": 0.0013904571533203125, "model_config": {"block_size": 16, "dropout": 0.1, "n_embd": 16, "n_head": 1, "n_layer": 1, "vocab_size": 4096}, "model_name": "smoke", "n_embd": 16, "n_head": 1, "n_layer": 1, "parameters": 134144, "run_mode": "locked_stream", "seed": 123, "stage": 1, "steps": 2, "token_limit": 768, "tokens_seen": 128, "train_eval_loss": 8.317992210388184, "train_loss_last": 8.3181734085083, "val_eval_loss": 8.319382667541504}
3
+ {"condition": "static_dropout_0.05", "condition_kind": "static", "dropout_active_final": 0.05, "dropout_final": 0.05, "dropout_initial": 0.05, "dropout_schedule": "constant", "elapsed_sec": 0.019870996475219727, "eval_loss": 8.318363189697266, "generalization_gap": -8.20159912109375e-05, "model_config": {"block_size": 16, "dropout": 0.05, "n_embd": 16, "n_head": 1, "n_layer": 1, "vocab_size": 4096}, "model_name": "smoke", "n_embd": 16, "n_head": 1, "n_layer": 1, "parameters": 134144, "run_mode": "locked_stream", "seed": 123, "stage": 0, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_eval_loss": 8.318445205688477, "train_loss_last": 8.317625999450684, "val_eval_loss": 8.318363189697266}
4
+ {"condition": "static_dropout_0.05", "condition_kind": "static", "dropout_active_final": 0.05, "dropout_final": 0.05, "dropout_initial": 0.05, "dropout_schedule": "constant", "elapsed_sec": 0.017475128173828125, "eval_loss": 8.319437026977539, "generalization_gap": 0.0068874359130859375, "model_config": {"block_size": 16, "dropout": 0.05, "n_embd": 16, "n_head": 1, "n_layer": 1, "vocab_size": 4096}, "model_name": "smoke", "n_embd": 16, "n_head": 1, "n_layer": 1, "parameters": 134144, "run_mode": "locked_stream", "seed": 123, "stage": 1, "steps": 2, "token_limit": 768, "tokens_seen": 128, "train_eval_loss": 8.312549591064453, "train_loss_last": 8.316361427307129, "val_eval_loss": 8.319437026977539}
runs/smoke_compat/locked_stream/20260605-224834/summary.csv ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ run_mode,condition,condition_kind,stage,token_limit,model_name,n_layer,n_head,n_embd,parameters,dropout_initial,dropout_final,dropout_schedule,n,mean_train_eval_loss,std_train_eval_loss,mean_val_eval_loss,std_val_eval_loss,mean_generalization_gap,std_generalization_gap
2
+ locked_stream,smoke_decay,anchor_decay,0,512,smoke,1,1,16,134144,0.1,0.05,log_prefix_anchor,1,8.318134307861328,0.0,8.318451881408691,0.0,0.00031757354736328125,0.0
3
+ locked_stream,static_dropout_0.05,static,0,512,smoke,1,1,16,134144,0.05,0.05,constant,1,8.318445205688477,0.0,8.318363189697266,0.0,-8.20159912109375e-05,0.0
4
+ locked_stream,smoke_decay,anchor_decay,1,768,smoke,1,1,16,134144,0.1,0.05,log_prefix_anchor,1,8.317992210388184,0.0,8.319382667541504,0.0,0.0013904571533203125,0.0
5
+ locked_stream,static_dropout_0.05,static,1,768,smoke,1,1,16,134144,0.05,0.05,constant,1,8.312549591064453,0.0,8.319437026977539,0.0,0.0068874359130859375,0.0
runs/smoke_compat/locked_stream/20260605-224834/summary.json ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "run_mode": "locked_stream",
4
+ "condition": "smoke_decay",
5
+ "condition_kind": "anchor_decay",
6
+ "stage": 0,
7
+ "token_limit": 512,
8
+ "model_name": "smoke",
9
+ "n_layer": 1,
10
+ "n_head": 1,
11
+ "n_embd": 16,
12
+ "parameters": 134144,
13
+ "dropout_initial": 0.1,
14
+ "dropout_final": 0.05,
15
+ "dropout_schedule": "log_prefix_anchor",
16
+ "n": 1,
17
+ "mean_train_eval_loss": 8.318134307861328,
18
+ "std_train_eval_loss": 0.0,
19
+ "mean_val_eval_loss": 8.318451881408691,
20
+ "std_val_eval_loss": 0.0,
21
+ "mean_generalization_gap": 0.00031757354736328125,
22
+ "std_generalization_gap": 0.0
23
+ },
24
+ {
25
+ "run_mode": "locked_stream",
26
+ "condition": "static_dropout_0.05",
27
+ "condition_kind": "static",
28
+ "stage": 0,
29
+ "token_limit": 512,
30
+ "model_name": "smoke",
31
+ "n_layer": 1,
32
+ "n_head": 1,
33
+ "n_embd": 16,
34
+ "parameters": 134144,
35
+ "dropout_initial": 0.05,
36
+ "dropout_final": 0.05,
37
+ "dropout_schedule": "constant",
38
+ "n": 1,
39
+ "mean_train_eval_loss": 8.318445205688477,
40
+ "std_train_eval_loss": 0.0,
41
+ "mean_val_eval_loss": 8.318363189697266,
42
+ "std_val_eval_loss": 0.0,
43
+ "mean_generalization_gap": -8.20159912109375e-05,
44
+ "std_generalization_gap": 0.0
45
+ },
46
+ {
47
+ "run_mode": "locked_stream",
48
+ "condition": "smoke_decay",
49
+ "condition_kind": "anchor_decay",
50
+ "stage": 1,
51
+ "token_limit": 768,
52
+ "model_name": "smoke",
53
+ "n_layer": 1,
54
+ "n_head": 1,
55
+ "n_embd": 16,
56
+ "parameters": 134144,
57
+ "dropout_initial": 0.1,
58
+ "dropout_final": 0.05,
59
+ "dropout_schedule": "log_prefix_anchor",
60
+ "n": 1,
61
+ "mean_train_eval_loss": 8.317992210388184,
62
+ "std_train_eval_loss": 0.0,
63
+ "mean_val_eval_loss": 8.319382667541504,
64
+ "std_val_eval_loss": 0.0,
65
+ "mean_generalization_gap": 0.0013904571533203125,
66
+ "std_generalization_gap": 0.0
67
+ },
68
+ {
69
+ "run_mode": "locked_stream",
70
+ "condition": "static_dropout_0.05",
71
+ "condition_kind": "static",
72
+ "stage": 1,
73
+ "token_limit": 768,
74
+ "model_name": "smoke",
75
+ "n_layer": 1,
76
+ "n_head": 1,
77
+ "n_embd": 16,
78
+ "parameters": 134144,
79
+ "dropout_initial": 0.05,
80
+ "dropout_final": 0.05,
81
+ "dropout_schedule": "constant",
82
+ "n": 1,
83
+ "mean_train_eval_loss": 8.312549591064453,
84
+ "std_train_eval_loss": 0.0,
85
+ "mean_val_eval_loss": 8.319437026977539,
86
+ "std_val_eval_loss": 0.0,
87
+ "mean_generalization_gap": 0.0068874359130859375,
88
+ "std_generalization_gap": 0.0
89
+ }
90
+ ]
runs/smoke_compat/locked_stream/20260605-224834/trace.jsonl ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {"condition": "smoke_decay", "dropout": 0.1, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 0, "step": 1, "steps": 2, "token_limit": 512, "tokens_seen": 32, "train_batch_loss": 8.31665325164795}
2
+ {"condition": "smoke_decay", "dropout": 0.1, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 0, "step": 2, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_batch_loss": 8.317201614379883}
3
+ {"condition": "smoke_decay", "dropout": 0.05, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 1, "step": 1, "steps": 2, "token_limit": 768, "tokens_seen": 96, "train_batch_loss": 8.316765785217285}
4
+ {"condition": "smoke_decay", "dropout": 0.05, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 1, "step": 2, "steps": 2, "token_limit": 768, "tokens_seen": 128, "train_batch_loss": 8.3181734085083}
5
+ {"condition": "static_dropout_0.05", "dropout": 0.05, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 0, "step": 1, "steps": 2, "token_limit": 512, "tokens_seen": 32, "train_batch_loss": 8.317569732666016}
6
+ {"condition": "static_dropout_0.05", "dropout": 0.05, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 0, "step": 2, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_batch_loss": 8.317625999450684}
7
+ {"condition": "static_dropout_0.05", "dropout": 0.05, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 1, "step": 1, "steps": 2, "token_limit": 768, "tokens_seen": 96, "train_batch_loss": 8.318181037902832}
8
+ {"condition": "static_dropout_0.05", "dropout": 0.05, "event": "train_step", "model_name": "smoke", "run_mode": "locked_stream", "seed": 123, "stage": 1, "step": 2, "steps": 2, "token_limit": 768, "tokens_seen": 128, "train_batch_loss": 8.316361427307129}
runs/smoke_compat/screen_static/20260605-224821/RESULT_SUMMARY.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Static Dropout Screen Summary
2
+
3
+ Run directory: `runs/smoke_compat/screen_static/20260605-224821`
4
+
5
+ ## Models
6
+
7
+ | Model | Params | Layers | Heads | Embedding | Block | Vocab | Seeds |
8
+ |---|---:|---:|---:|---:|---:|---:|---|
9
+ | `smoke` | 134,144 | 1 | 1 | 16 | 16 | 4096 | 123 |
10
+
11
+ ## Best Dropout By Model And Prefix
12
+
13
+ | Model | Prefix tokens | Effective epochs | Best dropout | Mean val loss | Val std | Mean train loss | Mean gap | Plateau/bracket note |
14
+ |---|---:|---:|---:|---:|---:|---:|---:|---|
15
+ | `smoke` | 512 | 0.12 | 0.10 | 8.3185 | 0.0000 | 8.3181 | 0.0003 | not bracketed; best at top of tested grid |
16
+
17
+ ## Model `smoke`
18
+
19
+ ### Prefix 512 Tokens (0.12 Effective Epochs)
20
+
21
+ | Dropout | N | Mean val loss | Val std | Mean train loss | Train std | Mean gap | Gap std | Sampled tokens | Params |
22
+ |---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
23
+ | 0.00 | 1 | 8.3186 | 0.0000 | 8.3134 | 0.0000 | 0.0052 | 0.0000 | 64 | 134,144 |
24
+ | 0.10 | 1 | 8.3185 | 0.0000 | 8.3181 | 0.0000 | 0.0003 | 0.0000 | 64 | 134,144 |
runs/smoke_compat/screen_static/20260605-224821/config.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "args": {
3
+ "mode": "screen_static",
4
+ "corpus": null,
5
+ "corpus_glob": null,
6
+ "text_column": "text",
7
+ "use_cached_data": true,
8
+ "output_dir": "runs/smoke_compat",
9
+ "resume_from": null,
10
+ "cache_dir": ".cache/dropout_decay",
11
+ "models": [
12
+ "smoke=1x1x16"
13
+ ],
14
+ "seeds": [
15
+ 123
16
+ ],
17
+ "token_limits": [
18
+ 512
19
+ ],
20
+ "stream_token_caps": [
21
+ 5000000,
22
+ 10000000,
23
+ 20000000,
24
+ 40000000
25
+ ],
26
+ "val_tokens": 128,
27
+ "allow_short_corpus": true,
28
+ "force_retokenize": false,
29
+ "vocab_size": 4096,
30
+ "tokenizer_train_chars": 10000000,
31
+ "block_size": 16,
32
+ "batch_size": 2,
33
+ "steps": 2,
34
+ "stage_steps": 1000,
35
+ "dropout_rates": [
36
+ 0.0,
37
+ 0.1
38
+ ],
39
+ "decays": [],
40
+ "anchor_decays": [],
41
+ "decay_tokens": null,
42
+ "eval_batches": 1,
43
+ "train_eval_batches": 1,
44
+ "trace_eval_batches": 1,
45
+ "eval_every": 0,
46
+ "log_every": 1,
47
+ "lr": 0.0003,
48
+ "weight_decay": 0.1,
49
+ "grad_clip": 1.0,
50
+ "plateau_delta": 0.01,
51
+ "target_min_dropout": 0.1,
52
+ "min_nonzero_margin": 0.01,
53
+ "min_high_dropout_margin": 0.03,
54
+ "screen_early_stop": false,
55
+ "screen_prune_patience": 3,
56
+ "screen_prune_min_delta": 0.01
57
+ },
58
+ "mode": "screen_static",
59
+ "seeds": [
60
+ 123
61
+ ],
62
+ "models": [
63
+ {
64
+ "model_name": "smoke",
65
+ "n_layer": 1,
66
+ "n_head": 1,
67
+ "n_embd": 16
68
+ }
69
+ ],
70
+ "device": "mps",
71
+ "torch": "2.12.0",
72
+ "python": "3.11.15 (main, Mar 3 2026, 00:52:57) [Clang 21.0.0 (clang-2100.0.123.102)]",
73
+ "mps_available": true,
74
+ "attribution": "Derived from Andrej Karpathy's nanochat project (https://github.com/karpathy/nanochat), MIT License, Copyright (c) 2025 Andrej Karpathy.",
75
+ "tokenizer_path": ".cache/dropout_decay/tokenizer-v4096.json",
76
+ "encoded_path": ".cache/dropout_decay/tokens-v4096-uint16.npy",
77
+ "train_tokens": 5500842,
78
+ "val_tokens": 128,
79
+ "effective_token_limits": [
80
+ 512
81
+ ],
82
+ "effective_stream_token_caps": [
83
+ 5000000,
84
+ 5500842,
85
+ 5500842,
86
+ 5500842
87
+ ],
88
+ "resume_from": null
89
+ }
runs/smoke_compat/screen_static/20260605-224821/dropout_curves.svg ADDED
runs/smoke_compat/screen_static/20260605-224821/metrics.jsonl ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ {"condition": "static_dropout_0", "condition_kind": "static", "dropout_active_final": 0.0, "dropout_final": 0.0, "dropout_initial": 0.0, "dropout_schedule": "constant", "elapsed_sec": 0.5898959636688232, "eval_loss": 8.318593978881836, "generalization_gap": 0.005218505859375, "model_config": {"block_size": 16, "dropout": 0.0, "n_embd": 16, "n_head": 1, "n_layer": 1, "vocab_size": 4096}, "model_name": "smoke", "n_embd": 16, "n_head": 1, "n_layer": 1, "parameters": 134144, "run_mode": "screen_static", "seed": 123, "stage": null, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_eval_loss": 8.313375473022461, "train_loss_last": 8.315808296203613, "val_eval_loss": 8.318593978881836}
2
+ {"condition": "static_dropout_0.1", "condition_kind": "static", "dropout_active_final": 0.1, "dropout_final": 0.1, "dropout_initial": 0.1, "dropout_schedule": "constant", "elapsed_sec": 0.10111308097839355, "eval_loss": 8.318451881408691, "generalization_gap": 0.00031757354736328125, "model_config": {"block_size": 16, "dropout": 0.1, "n_embd": 16, "n_head": 1, "n_layer": 1, "vocab_size": 4096}, "model_name": "smoke", "n_embd": 16, "n_head": 1, "n_layer": 1, "parameters": 134144, "run_mode": "screen_static", "seed": 123, "stage": null, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_eval_loss": 8.318134307861328, "train_loss_last": 8.317201614379883, "val_eval_loss": 8.318451881408691}
runs/smoke_compat/screen_static/20260605-224821/model_selection.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ run_mode,token_limit,model_name,n_layer,n_head,n_embd,parameters,n,best_dropout,best_val_loss,best_val_std,plateau_start_dropout,plateau_end_dropout,plateau_delta,zero_dropout_val_loss,zero_minus_best,best_nonzero_dropout,best_nonzero_val_loss,zero_minus_best_nonzero,max_dropout,max_dropout_val_loss,max_dropout_minus_best,has_nonzero_optimum,meets_target_dropout,curve_json
2
+ screen_static,512,smoke,1,1,16,134144,1,0.1,8.318451881408691,0.0,0.0,0.1,0.01,8.318593978881836,0.00014209747314453125,0.1,8.318451881408691,0.00014209747314453125,0.1,8.318451881408691,0.0,False,True,"[{""dropout"": 0.0, ""mean_generalization_gap"": 0.005218505859375, ""mean_train_loss"": 8.313375473022461, ""mean_val_loss"": 8.318593978881836, ""n"": 1, ""std_val_loss"": 0.0}, {""dropout"": 0.1, ""mean_generalization_gap"": 0.00031757354736328125, ""mean_train_loss"": 8.318134307861328, ""mean_val_loss"": 8.318451881408691, ""n"": 1, ""std_val_loss"": 0.0}]"
runs/smoke_compat/screen_static/20260605-224821/model_selection.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "run_mode": "screen_static",
4
+ "token_limit": 512,
5
+ "model_name": "smoke",
6
+ "n_layer": 1,
7
+ "n_head": 1,
8
+ "n_embd": 16,
9
+ "parameters": 134144,
10
+ "n": 1,
11
+ "best_dropout": 0.1,
12
+ "best_val_loss": 8.318451881408691,
13
+ "best_val_std": 0.0,
14
+ "plateau_start_dropout": 0.0,
15
+ "plateau_end_dropout": 0.1,
16
+ "plateau_delta": 0.01,
17
+ "zero_dropout_val_loss": 8.318593978881836,
18
+ "zero_minus_best": 0.00014209747314453125,
19
+ "best_nonzero_dropout": 0.1,
20
+ "best_nonzero_val_loss": 8.318451881408691,
21
+ "zero_minus_best_nonzero": 0.00014209747314453125,
22
+ "max_dropout": 0.1,
23
+ "max_dropout_val_loss": 8.318451881408691,
24
+ "max_dropout_minus_best": 0.0,
25
+ "has_nonzero_optimum": false,
26
+ "meets_target_dropout": true,
27
+ "curve_json": "[{\"dropout\": 0.0, \"mean_generalization_gap\": 0.005218505859375, \"mean_train_loss\": 8.313375473022461, \"mean_val_loss\": 8.318593978881836, \"n\": 1, \"std_val_loss\": 0.0}, {\"dropout\": 0.1, \"mean_generalization_gap\": 0.00031757354736328125, \"mean_train_loss\": 8.318134307861328, \"mean_val_loss\": 8.318451881408691, \"n\": 1, \"std_val_loss\": 0.0}]"
28
+ }
29
+ ]
runs/smoke_compat/screen_static/20260605-224821/summary.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ run_mode,condition,condition_kind,stage,token_limit,model_name,n_layer,n_head,n_embd,parameters,dropout_initial,dropout_final,dropout_schedule,n,mean_train_eval_loss,std_train_eval_loss,mean_val_eval_loss,std_val_eval_loss,mean_generalization_gap,std_generalization_gap
2
+ screen_static,static_dropout_0,static,,512,smoke,1,1,16,134144,0.0,0.0,constant,1,8.313375473022461,0.0,8.318593978881836,0.0,0.005218505859375,0.0
3
+ screen_static,static_dropout_0.1,static,,512,smoke,1,1,16,134144,0.1,0.1,constant,1,8.318134307861328,0.0,8.318451881408691,0.0,0.00031757354736328125,0.0
runs/smoke_compat/screen_static/20260605-224821/summary.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "run_mode": "screen_static",
4
+ "condition": "static_dropout_0",
5
+ "condition_kind": "static",
6
+ "stage": null,
7
+ "token_limit": 512,
8
+ "model_name": "smoke",
9
+ "n_layer": 1,
10
+ "n_head": 1,
11
+ "n_embd": 16,
12
+ "parameters": 134144,
13
+ "dropout_initial": 0.0,
14
+ "dropout_final": 0.0,
15
+ "dropout_schedule": "constant",
16
+ "n": 1,
17
+ "mean_train_eval_loss": 8.313375473022461,
18
+ "std_train_eval_loss": 0.0,
19
+ "mean_val_eval_loss": 8.318593978881836,
20
+ "std_val_eval_loss": 0.0,
21
+ "mean_generalization_gap": 0.005218505859375,
22
+ "std_generalization_gap": 0.0
23
+ },
24
+ {
25
+ "run_mode": "screen_static",
26
+ "condition": "static_dropout_0.1",
27
+ "condition_kind": "static",
28
+ "stage": null,
29
+ "token_limit": 512,
30
+ "model_name": "smoke",
31
+ "n_layer": 1,
32
+ "n_head": 1,
33
+ "n_embd": 16,
34
+ "parameters": 134144,
35
+ "dropout_initial": 0.1,
36
+ "dropout_final": 0.1,
37
+ "dropout_schedule": "constant",
38
+ "n": 1,
39
+ "mean_train_eval_loss": 8.318134307861328,
40
+ "std_train_eval_loss": 0.0,
41
+ "mean_val_eval_loss": 8.318451881408691,
42
+ "std_val_eval_loss": 0.0,
43
+ "mean_generalization_gap": 0.00031757354736328125,
44
+ "std_generalization_gap": 0.0
45
+ }
46
+ ]
runs/smoke_compat/screen_static/20260605-224821/trace.jsonl ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {"condition": "static_dropout_0", "dropout": 0.0, "event": "train_step", "model_name": "smoke", "run_mode": "screen_static", "seed": 123, "stage": null, "step": 1, "steps": 2, "token_limit": 512, "tokens_seen": 32, "train_batch_loss": 8.318573951721191}
2
+ {"condition": "static_dropout_0", "dropout": 0.0, "event": "train_step", "model_name": "smoke", "run_mode": "screen_static", "seed": 123, "stage": null, "step": 2, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_batch_loss": 8.315808296203613}
3
+ {"condition": "static_dropout_0.1", "dropout": 0.1, "event": "train_step", "model_name": "smoke", "run_mode": "screen_static", "seed": 123, "stage": null, "step": 1, "steps": 2, "token_limit": 512, "tokens_seen": 32, "train_batch_loss": 8.31665325164795}
4
+ {"condition": "static_dropout_0.1", "dropout": 0.1, "event": "train_step", "model_name": "smoke", "run_mode": "screen_static", "seed": 123, "stage": null, "step": 2, "steps": 2, "token_limit": 512, "tokens_seen": 64, "train_batch_loss": 8.317201614379883}
scripts/generate_paper_assets.py ADDED
@@ -0,0 +1,579 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Derived from Andrej Karpathy's nanochat project.
4
+
5
+ MIT License
6
+
7
+ Copyright (c) 2025 Andrej Karpathy
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import csv
23
+ import json
24
+ import math
25
+ from pathlib import Path
26
+ import statistics
27
+
28
+
29
+ ROOT = Path(__file__).resolve().parents[1]
30
+ PAPER_DIR = ROOT / "paper"
31
+ FIG_DIR = PAPER_DIR / "figures"
32
+ TABLE_DIR = PAPER_DIR / "tables"
33
+
34
+
35
+ REGIMES = {
36
+ "openweb": {
37
+ "label": "OpenWebText10K",
38
+ "short": "OWT10K",
39
+ "coeff": ROOT
40
+ / "runs/coefficient_calibration/cross_regime_backtest/openwebtext10k_main_interaction/coefficients.json",
41
+ "cells": ROOT
42
+ / "runs/coefficient_calibration/cross_regime_backtest/openwebtext10k_main_interaction/calibration_cells.csv",
43
+ "curve_run": ROOT / "runs/screen_static/20260525-133008",
44
+ "curve_model": "L12_H8_D320",
45
+ "curve_prefix": 1_000_000,
46
+ "curve_title": "OpenWebText10K, L12, 1M",
47
+ "small_stream": ROOT
48
+ / "runs/openwebtext10k_l16_updated_formula_clean_5seed/locked_stream/20260530-174525",
49
+ "small_condition": "openwebtext10k_interaction",
50
+ "small_model": "L16_H8_D384",
51
+ "small_protocol": "4M / 5 seeds",
52
+ "l20_stream": ROOT
53
+ / "runs/openwebtext10k_l20_8m_extrapolation_3seed/locked_stream/20260601-191150",
54
+ "l20_condition": "openwebtext10k_l20_interaction_8m",
55
+ },
56
+ "tinystories": {
57
+ "label": "TinyStories",
58
+ "short": "TinyStories",
59
+ "coeff": ROOT
60
+ / "runs/coefficient_calibration/tinystories_combined_plus_all_holdouts_interaction/coefficients.json",
61
+ "cells": ROOT
62
+ / "runs/coefficient_calibration/tinystories_combined_plus_all_holdouts_interaction/calibration_cells.csv",
63
+ "curve_run": ROOT
64
+ / "runs/regime_calibration_tinystories_coarse/screen_static/20260529-161726",
65
+ "curve_model": "L12_H8_D320",
66
+ "curve_prefix": 1_000_000,
67
+ "curve_title": "TinyStories, L12, 1M",
68
+ "small_stream": [
69
+ ROOT
70
+ / "runs/streaming_tinystories_interaction_schedule_l12/locked_stream/20260530-053831",
71
+ ROOT
72
+ / "runs/streaming_tinystories_multiseed_validation_l12/locked_stream/20260530-111523",
73
+ ROOT
74
+ / "runs/streaming_tinystories_multiseed_validation_l12/locked_stream/20260530-141335",
75
+ ],
76
+ "small_condition": "interaction",
77
+ "small_model": "L12_H8_D320",
78
+ "small_protocol": "4M / 5 seeds",
79
+ "l20_stream": ROOT
80
+ / "runs/tinystories_l20_8m_extrapolation_3seed/locked_stream/20260602-112249",
81
+ "l20_condition": "tinystories_l20_interaction_8m",
82
+ },
83
+ "wikitext": {
84
+ "label": "WikiText-103",
85
+ "short": "WikiText",
86
+ "coeff": ROOT / "runs/coefficient_calibration/wikitext103_interaction/coefficients.json",
87
+ "cells": ROOT / "runs/coefficient_calibration/wikitext103_interaction/calibration_cells.csv",
88
+ "curve_run": ROOT
89
+ / "runs/wikitext103_interaction_static_screen/screen_static/20260603-153314",
90
+ "curve_model": "L12_H8_D320",
91
+ "curve_prefix": 250_000,
92
+ "curve_title": "WikiText-103, L12, 250k",
93
+ "small_stream": ROOT
94
+ / "runs/wikitext103_l12_streaming_validation_5seed/locked_stream/20260531-093525",
95
+ "small_condition": "wikitext103_formula_l12",
96
+ "small_model": "L12_H8_D320",
97
+ "small_protocol": "4M / 5 seeds",
98
+ "l20_stream": ROOT
99
+ / "runs/wikitext103_l20_8m_extrapolation_3seed/locked_stream/20260603-231952",
100
+ "l20_condition": "wikitext103_l20_interaction_8m",
101
+ },
102
+ }
103
+
104
+
105
+ def read_json(path: Path) -> dict | list:
106
+ return json.loads(path.read_text(encoding="utf-8"))
107
+
108
+
109
+ def read_jsonl(path: Path) -> list[dict]:
110
+ rows = []
111
+ for line in path.read_text(encoding="utf-8").splitlines():
112
+ if line.strip():
113
+ rows.append(json.loads(line))
114
+ return rows
115
+
116
+
117
+ def read_csv(path: Path) -> list[dict]:
118
+ with path.open(newline="", encoding="utf-8") as handle:
119
+ return list(csv.DictReader(handle))
120
+
121
+
122
+ def mean_std(values: list[float]) -> tuple[float, float]:
123
+ if not values:
124
+ return float("nan"), float("nan")
125
+ mean = statistics.fmean(values)
126
+ std = statistics.stdev(values) if len(values) > 1 else 0.0
127
+ return mean, std
128
+
129
+
130
+ def fmt(x: float, digits: int = 4) -> str:
131
+ return f"{x:.{digits}f}"
132
+
133
+
134
+ def tex_escape(value: object) -> str:
135
+ text = str(value)
136
+ return (
137
+ text.replace("\\", "\\textbackslash{}")
138
+ .replace("_", "\\_")
139
+ .replace("&", "\\&")
140
+ .replace("%", "\\%")
141
+ .replace("#", "\\#")
142
+ )
143
+
144
+
145
+ def token_label(value: int) -> str:
146
+ if value >= 1_000_000:
147
+ whole = value / 1_000_000
148
+ return f"{whole:g}M"
149
+ return f"{value // 1000}k"
150
+
151
+
152
+ def metric_stage_summary(run_dir: Path | list[Path], condition_name: str) -> dict:
153
+ run_dirs = run_dir if isinstance(run_dir, list) else [run_dir]
154
+ rows = []
155
+ for path in run_dirs:
156
+ rows.extend(read_jsonl(path / "metrics.jsonl"))
157
+ stages = sorted({int(row["stage"]) for row in rows if row.get("stage") is not None})
158
+ final_stage = max(stages)
159
+ by_condition_stage: dict[tuple[str, int], list[dict]] = {}
160
+ for row in rows:
161
+ if row.get("stage") is None:
162
+ continue
163
+ key = (row["condition"], int(row["stage"]))
164
+ by_condition_stage.setdefault(key, []).append(row)
165
+
166
+ condition_rows = by_condition_stage[(condition_name, final_stage)]
167
+ final_vals = [float(row["val_eval_loss"]) for row in condition_rows]
168
+ condition_final_mean, condition_final_std = mean_std(final_vals)
169
+
170
+ final_static_by_condition: dict[str, list[dict]] = {}
171
+ for row in rows:
172
+ if int(row.get("stage", -1)) != final_stage:
173
+ continue
174
+ if row["condition_kind"] == "static":
175
+ final_static_by_condition.setdefault(row["condition"], []).append(row)
176
+ static_means = {
177
+ condition: statistics.fmean(float(row["val_eval_loss"]) for row in group)
178
+ for condition, group in final_static_by_condition.items()
179
+ }
180
+ best_static_condition = min(static_means, key=static_means.get)
181
+ best_static_rows = final_static_by_condition[best_static_condition]
182
+ best_static_vals = [float(row["val_eval_loss"]) for row in best_static_rows]
183
+ best_static_mean, best_static_std = mean_std(best_static_vals)
184
+
185
+ final_static_by_seed: dict[int, tuple[str, float]] = {}
186
+ for row in rows:
187
+ if int(row.get("stage", -1)) != final_stage or row["condition_kind"] != "static":
188
+ continue
189
+ seed = int(row["seed"])
190
+ value = float(row["val_eval_loss"])
191
+ if seed not in final_static_by_seed or value < final_static_by_seed[seed][1]:
192
+ final_static_by_seed[seed] = (row["condition"], value)
193
+
194
+ paired_deltas = []
195
+ for row in condition_rows:
196
+ seed = int(row["seed"])
197
+ paired_deltas.append(float(row["val_eval_loss"]) - final_static_by_seed[seed][1])
198
+ paired_mean, paired_std = mean_std(paired_deltas)
199
+
200
+ stage_deltas = []
201
+ for stage in stages:
202
+ condition_group = by_condition_stage[(condition_name, stage)]
203
+ condition_mean = statistics.fmean(
204
+ float(row["val_eval_loss"]) for row in condition_group
205
+ )
206
+ static_candidates = []
207
+ for (condition, candidate_stage), group in by_condition_stage.items():
208
+ if candidate_stage != stage:
209
+ continue
210
+ if group[0]["condition_kind"] != "static":
211
+ continue
212
+ static_candidates.append(
213
+ (
214
+ condition,
215
+ statistics.fmean(float(row["val_eval_loss"]) for row in group),
216
+ statistics.stdev(float(row["val_eval_loss"]) for row in group)
217
+ if len(group) > 1
218
+ else 0.0,
219
+ )
220
+ )
221
+ best_static_stage = min(static_candidates, key=lambda item: item[1])
222
+ first = condition_group[0]
223
+ stage_deltas.append(
224
+ {
225
+ "stage": stage,
226
+ "prefix": int(first["token_limit"]),
227
+ "condition_dropout": statistics.fmean(
228
+ float(row["dropout_active_final"]) for row in condition_group
229
+ ),
230
+ "condition_mean": condition_mean,
231
+ "condition_std": statistics.stdev(
232
+ float(row["val_eval_loss"]) for row in condition_group
233
+ )
234
+ if len(condition_group) > 1
235
+ else 0.0,
236
+ "best_static_condition": best_static_stage[0],
237
+ "best_static_mean": best_static_stage[1],
238
+ "best_static_std": best_static_stage[2],
239
+ "delta": condition_mean - best_static_stage[1],
240
+ }
241
+ )
242
+
243
+ return {
244
+ "n": len(condition_rows),
245
+ "final_stage": final_stage,
246
+ "final_prefix": int(condition_rows[0]["token_limit"]),
247
+ "condition_final_mean": condition_final_mean,
248
+ "condition_final_std": condition_final_std,
249
+ "best_static_condition": best_static_condition,
250
+ "best_static_mean": best_static_mean,
251
+ "best_static_std": best_static_std,
252
+ "improvement": best_static_mean - condition_final_mean,
253
+ "paired_deltas": paired_deltas,
254
+ "paired_wins": sum(1 for delta in paired_deltas if delta < 0),
255
+ "paired_mean_delta": paired_mean,
256
+ "paired_std_delta": paired_std,
257
+ "stage_deltas": stage_deltas,
258
+ }
259
+
260
+
261
+ def static_curve_points(run_dir: Path, model: str, prefix: int) -> list[tuple[float, float]]:
262
+ rows = read_json(run_dir / "summary.json")
263
+ curve = []
264
+ for row in rows:
265
+ if row["run_mode"] != "screen_static":
266
+ continue
267
+ if row["condition_kind"] != "static":
268
+ continue
269
+ if row["model_name"] != model:
270
+ continue
271
+ if int(row["token_limit"]) != prefix:
272
+ continue
273
+ curve.append((float(row["dropout_initial"]), float(row["mean_val_eval_loss"])))
274
+ return sorted(curve)
275
+
276
+
277
+ def calibration_points(cells_path: Path) -> dict[str, list[tuple[int, float]]]:
278
+ rows = read_csv(cells_path)
279
+ grouped: dict[str, dict[int, float]] = {}
280
+ for row in rows:
281
+ sampled = int(float(row["sampled_tokens"]))
282
+ unique = int(float(row["unique_tokens"]))
283
+ source = row["source"]
284
+ if "sample_pressure_low" in source:
285
+ continue
286
+ if "heldout_model" in source:
287
+ continue
288
+ model = row["model_name"]
289
+ target = max(0.0, float(row["target_dropout"]))
290
+ grouped.setdefault(model, {})[unique] = target
291
+ return {
292
+ model: sorted(points.items())
293
+ for model, points in sorted(grouped.items())
294
+ if len(points) >= 2
295
+ }
296
+
297
+
298
+ def coeff_summary(regime_key: str, paths: dict) -> dict:
299
+ coeff = read_json(paths["coeff"])
300
+ return {
301
+ "regime": paths["label"],
302
+ "cells": int(coeff["metrics"]["n"]),
303
+ "A": float(coeff["coefficients"]["A"]),
304
+ "B": float(coeff["coefficients"]["B"]),
305
+ "D": float(coeff["coefficients"]["D"]),
306
+ "C0": float(coeff["coefficients"]["C0"]),
307
+ "mae": float(coeff["metrics"]["mae"]),
308
+ "rmse": float(coeff["metrics"]["rmse"]),
309
+ "leave_model_mae": float(coeff["cv"]["leave_model"]["mae"]),
310
+ "leave_prefix_mae": float(coeff["cv"]["leave_prefix"]["mae"]),
311
+ "formula": coeff["formula"],
312
+ }
313
+
314
+
315
+ def tikz_coordinates(points: list[tuple[float, float]], digits: int = 4) -> str:
316
+ return " ".join(f"({x:.4g},{y:.{digits}f})" for x, y in points)
317
+
318
+
319
+ def write_calibration_curve_figure() -> None:
320
+ axes = []
321
+ for key, paths in REGIMES.items():
322
+ raw_curve = static_curve_points(
323
+ paths["curve_run"], paths["curve_model"], paths["curve_prefix"]
324
+ )
325
+ minimum = min(loss for _, loss in raw_curve)
326
+ points = [(dropout, loss - minimum) for dropout, loss in raw_curve]
327
+ best = min(raw_curve, key=lambda item: item[1])
328
+ axes.append(
329
+ rf"""
330
+ \nextgroupplot[
331
+ title={{{paths["curve_title"]}}},
332
+ xlabel={{Dropout}},
333
+ ylabel={{Validation loss above best}},
334
+ ymin=0,
335
+ grid=both,
336
+ ]
337
+ \addplot+[thick, mark=*] coordinates {{{tikz_coordinates(points, digits=5)}}};
338
+ \addlegendentry{{static sweep}}
339
+ \addplot+[only marks, mark=star, mark size=3pt] coordinates {{({best[0]:.4g},0)}};
340
+ \addlegendentry{{target $p^\star={best[0]:.2f}$}}
341
+ """
342
+ )
343
+ content = (
344
+ "\\begin{tikzpicture}\n"
345
+ "\\begin{groupplot}[\n"
346
+ " group style={group size=3 by 1, horizontal sep=1.0cm},\n"
347
+ " width=0.32\\textwidth,\n"
348
+ " height=0.26\\textwidth,\n"
349
+ " legend style={font=\\scriptsize},\n"
350
+ " tick label style={font=\\scriptsize},\n"
351
+ " label style={font=\\scriptsize},\n"
352
+ " title style={font=\\scriptsize},\n"
353
+ "]\n"
354
+ + "\n".join(axes)
355
+ + "\n\\end{groupplot}\n\\end{tikzpicture}\n"
356
+ )
357
+ (FIG_DIR / "calibration_cell_curves.tex").write_text(content, encoding="utf-8")
358
+
359
+
360
+ def write_static_optima_figure() -> None:
361
+ axes = []
362
+ for key, paths in REGIMES.items():
363
+ grouped = calibration_points(paths["cells"])
364
+ labels = sorted({prefix for points in grouped.values() for prefix, _ in points})
365
+ label_to_x = {prefix: idx for idx, prefix in enumerate(labels)}
366
+ plots = []
367
+ for model, points in grouped.items():
368
+ coords = [
369
+ (label_to_x[prefix], target)
370
+ for prefix, target in points
371
+ if prefix in label_to_x
372
+ ]
373
+ plots.append(
374
+ "\\addplot+[thick, mark=*] coordinates {"
375
+ + tikz_coordinates(coords)
376
+ + "};\n"
377
+ + f"\\addlegendentry{{{tex_escape(model)}}}"
378
+ )
379
+ xticks = ",".join(str(i) for i in range(len(labels)))
380
+ xticklabels = ",".join(token_label(prefix) for prefix in labels)
381
+ axes.append(
382
+ rf"""
383
+ \nextgroupplot[
384
+ title={{{paths["label"]}}},
385
+ xlabel={{Unique prefix $U$}},
386
+ ylabel={{Fitted target dropout}},
387
+ xtick={{{xticks}}},
388
+ xticklabels={{{xticklabels}}},
389
+ ymin=0,
390
+ ymax=0.65,
391
+ grid=both,
392
+ ]
393
+ {chr(10).join(plots)}
394
+ """
395
+ )
396
+ content = (
397
+ "\\begin{tikzpicture}\n"
398
+ "\\begin{groupplot}[\n"
399
+ " group style={group size=3 by 1, horizontal sep=1.0cm},\n"
400
+ " width=0.32\\textwidth,\n"
401
+ " height=0.28\\textwidth,\n"
402
+ " legend style={font=\\tiny},\n"
403
+ " tick label style={font=\\scriptsize},\n"
404
+ " label style={font=\\scriptsize},\n"
405
+ " title style={font=\\scriptsize},\n"
406
+ "]\n"
407
+ + "\n".join(axes)
408
+ + "\n\\end{groupplot}\n\\end{tikzpicture}\n"
409
+ )
410
+ (FIG_DIR / "static_optima_by_regime.tex").write_text(content, encoding="utf-8")
411
+
412
+
413
+ def write_l20_stage_delta_figure(summaries: dict[str, dict]) -> None:
414
+ axes = []
415
+ for key, paths in REGIMES.items():
416
+ stage_rows = summaries[key]["l20"]["stage_deltas"]
417
+ labels = [token_label(row["prefix"]) for row in stage_rows]
418
+ coords = [(idx, row["delta"]) for idx, row in enumerate(stage_rows)]
419
+ zero_coords = [(0, 0.0), (len(stage_rows) - 1, 0.0)]
420
+ xticks = ",".join(str(i) for i in range(len(labels)))
421
+ xticklabels = ",".join(labels)
422
+ axes.append(
423
+ rf"""
424
+ \nextgroupplot[
425
+ title={{{paths["label"]}}},
426
+ xlabel={{Prefix}},
427
+ ylabel={{Decay $-$ best static}},
428
+ xtick={{{xticks}}},
429
+ xticklabels={{{xticklabels}}},
430
+ grid=both,
431
+ ]
432
+ \addplot+[thick, mark=*] coordinates {{{tikz_coordinates(coords, digits=5)}}};
433
+ \addplot+[black, dashed, mark=none] coordinates {{{tikz_coordinates(zero_coords, digits=5)}}};
434
+ """
435
+ )
436
+ content = (
437
+ "\\begin{tikzpicture}\n"
438
+ "\\begin{groupplot}[\n"
439
+ " group style={group size=3 by 1, horizontal sep=1.0cm},\n"
440
+ " width=0.32\\textwidth,\n"
441
+ " height=0.27\\textwidth,\n"
442
+ " tick label style={font=\\scriptsize},\n"
443
+ " label style={font=\\scriptsize},\n"
444
+ " title style={font=\\scriptsize},\n"
445
+ "]\n"
446
+ + "\n".join(axes)
447
+ + "\n\\end{groupplot}\n\\end{tikzpicture}\n"
448
+ )
449
+ (FIG_DIR / "l20_stage_deltas.tex").write_text(content, encoding="utf-8")
450
+
451
+
452
+ def write_final_improvement_figure(summaries: dict[str, dict]) -> None:
453
+ labels = []
454
+ coords_small = []
455
+ coords_l20 = []
456
+ for idx, (key, paths) in enumerate(REGIMES.items()):
457
+ labels.append(paths["short"])
458
+ coords_small.append((idx, summaries[key]["small"]["improvement"]))
459
+ coords_l20.append((idx, summaries[key]["l20"]["improvement"]))
460
+ xticks = ",".join(str(i) for i in range(len(labels)))
461
+ xticklabels = ",".join(labels)
462
+ content = rf"""
463
+ \begin{{tikzpicture}}
464
+ \begin{{axis}}[
465
+ ybar,
466
+ bar width=8pt,
467
+ width=0.72\textwidth,
468
+ height=0.34\textwidth,
469
+ ymin=0,
470
+ ylabel={{Best static final loss $-$ decay final loss}},
471
+ symbolic x coords={{{xticklabels}}},
472
+ xtick=data,
473
+ xtick={{{xticklabels}}},
474
+ grid=both,
475
+ legend style={{font=\scriptsize, at={{(0.5,1.04)}}, anchor=south, legend columns=2}},
476
+ tick label style={{font=\scriptsize}},
477
+ label style={{font=\scriptsize}},
478
+ ]
479
+ \addplot coordinates {{{" ".join(f"({label},{y:.5f})" for label, (_, y) in zip(labels, coords_small))}}};
480
+ \addlegendentry{{4M validation}}
481
+ \addplot coordinates {{{" ".join(f"({label},{y:.5f})" for label, (_, y) in zip(labels, coords_l20))}}};
482
+ \addlegendentry{{L20 8M extrapolation}}
483
+ \end{{axis}}
484
+ \end{{tikzpicture}}
485
+ """
486
+ (FIG_DIR / "final_improvements.tex").write_text(content, encoding="utf-8")
487
+
488
+
489
+ def write_tables(summaries: dict[str, dict], coeffs: dict[str, dict]) -> None:
490
+ coeff_lines = [
491
+ "\\begin{tabular}{lrrrrrrrr}",
492
+ "\\toprule",
493
+ "Regime & Cells & A & B & D & C0 & MAE & L-model MAE & L-prefix MAE \\\\",
494
+ "\\midrule",
495
+ ]
496
+ for key in REGIMES:
497
+ row = coeffs[key]
498
+ coeff_lines.append(
499
+ f"{tex_escape(row['regime'])} & {row['cells']} & "
500
+ f"{row['A']:.4f} & {row['B']:.4f} & {row['D']:.4f} & {row['C0']:.4f} & "
501
+ f"{row['mae']:.4f} & {row['leave_model_mae']:.4f} & {row['leave_prefix_mae']:.4f} \\\\"
502
+ )
503
+ coeff_lines.extend(["\\bottomrule", "\\end{tabular}"])
504
+ (TABLE_DIR / "coefficient_table.tex").write_text(
505
+ "\n".join(coeff_lines) + "\n", encoding="utf-8"
506
+ )
507
+
508
+ val_lines = [
509
+ "\\begin{tabular}{llrrrrr}",
510
+ "\\toprule",
511
+ "Regime & Protocol & Decay final & Best static final & Gain & Seeds & Paired wins \\\\",
512
+ "\\midrule",
513
+ ]
514
+ for key, paths in REGIMES.items():
515
+ for protocol_key, protocol_label in [("small", paths["small_protocol"]), ("l20", "8M / 3 seeds")]:
516
+ row = summaries[key][protocol_key]
517
+ val_lines.append(
518
+ f"{tex_escape(paths['label'])} & {tex_escape(protocol_label)} & "
519
+ f"{row['condition_final_mean']:.4f} $\\pm$ {row['condition_final_std']:.4f} & "
520
+ f"{row['best_static_mean']:.4f} $\\pm$ {row['best_static_std']:.4f} & "
521
+ f"{row['improvement']:.4f} & {row['n']} & {row['paired_wins']}/{row['n']} \\\\"
522
+ )
523
+ val_lines.extend(["\\bottomrule", "\\end{tabular}"])
524
+ (TABLE_DIR / "streaming_validation_table.tex").write_text(
525
+ "\n".join(val_lines) + "\n", encoding="utf-8"
526
+ )
527
+
528
+ l20_lines = [
529
+ "\\begin{tabular}{lrrrrr}",
530
+ "\\toprule",
531
+ "Regime & Prefix & Decay dropout & Decay val & Best static val & Delta \\\\",
532
+ "\\midrule",
533
+ ]
534
+ for key, paths in REGIMES.items():
535
+ for row in summaries[key]["l20"]["stage_deltas"]:
536
+ l20_lines.append(
537
+ f"{tex_escape(paths['label'])} & {token_label(row['prefix'])} & "
538
+ f"{row['condition_dropout']:.3f} & {row['condition_mean']:.4f} & "
539
+ f"{row['best_static_mean']:.4f} & {row['delta']:+.4f} \\\\"
540
+ )
541
+ l20_lines.extend(["\\bottomrule", "\\end{tabular}"])
542
+ (TABLE_DIR / "l20_stage_table.tex").write_text(
543
+ "\n".join(l20_lines) + "\n", encoding="utf-8"
544
+ )
545
+
546
+ summary = {
547
+ key: {
548
+ "coefficients": coeffs[key],
549
+ "small_stream": summaries[key]["small"],
550
+ "l20_stream": summaries[key]["l20"],
551
+ }
552
+ for key in REGIMES
553
+ }
554
+ (PAPER_DIR / "paper_results_summary.json").write_text(
555
+ json.dumps(summary, indent=2), encoding="utf-8"
556
+ )
557
+
558
+
559
+ def main() -> None:
560
+ FIG_DIR.mkdir(parents=True, exist_ok=True)
561
+ TABLE_DIR.mkdir(parents=True, exist_ok=True)
562
+ summaries = {}
563
+ coeffs = {}
564
+ for key, paths in REGIMES.items():
565
+ coeffs[key] = coeff_summary(key, paths)
566
+ summaries[key] = {
567
+ "small": metric_stage_summary(paths["small_stream"], paths["small_condition"]),
568
+ "l20": metric_stage_summary(paths["l20_stream"], paths["l20_condition"]),
569
+ }
570
+ write_calibration_curve_figure()
571
+ write_static_optima_figure()
572
+ write_l20_stage_delta_figure(summaries)
573
+ write_final_improvement_figure(summaries)
574
+ write_tables(summaries, coeffs)
575
+ print(json.dumps({"figures": str(FIG_DIR), "tables": str(TABLE_DIR)}, indent=2))
576
+
577
+
578
+ if __name__ == "__main__":
579
+ main()
src/dropout_decay/data/__init__.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from dropout_decay.datasets import (
20
+ CachedTokenizer,
21
+ TokenSplits,
22
+ encode_corpus,
23
+ iter_documents,
24
+ load_cached_splits,
25
+ preview_documents,
26
+ required_token_count,
27
+ resolve_paths,
28
+ train_or_load_tokenizer,
29
+ )
30
+ from dropout_decay.specs import DropoutCondition, ModelSpec, anchor_dropout
31
+
32
+ __all__ = [
33
+ "CachedTokenizer",
34
+ "DropoutCondition",
35
+ "ModelSpec",
36
+ "TokenSplits",
37
+ "anchor_dropout",
38
+ "encode_corpus",
39
+ "iter_documents",
40
+ "load_cached_splits",
41
+ "preview_documents",
42
+ "required_token_count",
43
+ "resolve_paths",
44
+ "train_or_load_tokenizer",
45
+ ]
src/dropout_decay/datasets/__init__.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from dropout_decay.datasets.corpus import (
20
+ CachedTokenizer,
21
+ TokenSplits,
22
+ encode_corpus,
23
+ iter_documents,
24
+ load_cached_splits,
25
+ preview_documents,
26
+ required_token_count,
27
+ resolve_paths,
28
+ train_or_load_tokenizer,
29
+ )
30
+
31
+ __all__ = [
32
+ "CachedTokenizer",
33
+ "TokenSplits",
34
+ "encode_corpus",
35
+ "iter_documents",
36
+ "load_cached_splits",
37
+ "preview_documents",
38
+ "required_token_count",
39
+ "resolve_paths",
40
+ "train_or_load_tokenizer",
41
+ ]
src/dropout_decay/{data.py → datasets/corpus.py} RENAMED
@@ -143,7 +143,7 @@ def train_or_load_tokenizer(
143
  text_column: str,
144
  force_retrain: bool = False,
145
  ) -> object:
146
- from dropout_decay.tokenizer import BpeTokenizer
147
 
148
  tokenizer_path = output_dir / f"tokenizer-v{vocab_size}.json"
149
  if tokenizer_path.exists() and not force_retrain:
 
143
  text_column: str,
144
  force_retrain: bool = False,
145
  ) -> object:
146
+ from dropout_decay.tokenization import BpeTokenizer
147
 
148
  tokenizer_path = output_dir / f"tokenizer-v{vocab_size}.json"
149
  if tokenizer_path.exists() and not force_retrain:
src/dropout_decay/experiment.py CHANGED
@@ -16,1694 +16,13 @@ The above copyright notice and this permission notice shall be included in all
16
  copies or substantial portions of the Software.
17
  """
18
 
19
- from __future__ import annotations
20
 
21
- import argparse
22
- import csv
23
- from collections import defaultdict
24
- from dataclasses import dataclass
25
- from datetime import datetime
26
- import json
27
- import math
28
- import os
29
- from pathlib import Path
30
- import random
31
- import statistics
32
- import sys
33
- import time
34
- from typing import Callable
35
-
36
- import numpy as np
37
- import torch
38
-
39
- from dropout_decay.data import (
40
- encode_corpus,
41
- load_cached_splits,
42
- resolve_paths,
43
- train_or_load_tokenizer,
44
- )
45
- from dropout_decay.license import NANOCHAT_ATTRIBUTION
46
- from dropout_decay.model import DropoutGPT, GPTConfig
47
- from dropout_decay.scheduler import DropoutDecayConfig, DropoutDecayScheduler
48
-
49
-
50
- DEFAULT_DROPOUT_RATES = [0.0, 0.02, 0.05, 0.08, 0.10, 0.14, 0.20, 0.30, 0.50]
51
- SUMMARY_FIELDS = [
52
- "run_mode",
53
- "condition",
54
- "condition_kind",
55
- "stage",
56
- "token_limit",
57
- "model_name",
58
- "n_layer",
59
- "n_head",
60
- "n_embd",
61
- "parameters",
62
- "dropout_initial",
63
- "dropout_final",
64
- "dropout_schedule",
65
- "n",
66
- "mean_train_eval_loss",
67
- "std_train_eval_loss",
68
- "mean_val_eval_loss",
69
- "std_val_eval_loss",
70
- "mean_generalization_gap",
71
- "std_generalization_gap",
72
  ]
73
- SELECTION_FIELDS = [
74
- "run_mode",
75
- "token_limit",
76
- "model_name",
77
- "n_layer",
78
- "n_head",
79
- "n_embd",
80
- "parameters",
81
- "n",
82
- "best_dropout",
83
- "best_val_loss",
84
- "best_val_std",
85
- "plateau_start_dropout",
86
- "plateau_end_dropout",
87
- "plateau_delta",
88
- "zero_dropout_val_loss",
89
- "zero_minus_best",
90
- "best_nonzero_dropout",
91
- "best_nonzero_val_loss",
92
- "zero_minus_best_nonzero",
93
- "max_dropout",
94
- "max_dropout_val_loss",
95
- "max_dropout_minus_best",
96
- "has_nonzero_optimum",
97
- "meets_target_dropout",
98
- "curve_json",
99
- ]
100
-
101
-
102
- @dataclass(frozen=True)
103
- class ModelSpec:
104
- name: str
105
- n_layer: int
106
- n_head: int
107
- n_embd: int
108
-
109
- def config(self, vocab_size: int, block_size: int, dropout: float) -> GPTConfig:
110
- return GPTConfig(
111
- block_size=block_size,
112
- vocab_size=vocab_size,
113
- n_layer=self.n_layer,
114
- n_head=self.n_head,
115
- n_embd=self.n_embd,
116
- dropout=dropout,
117
- )
118
-
119
- def to_dict(self) -> dict[str, int | str]:
120
- return {
121
- "model_name": self.name,
122
- "n_layer": self.n_layer,
123
- "n_head": self.n_head,
124
- "n_embd": self.n_embd,
125
- }
126
-
127
-
128
- @dataclass(frozen=True)
129
- class DropoutCondition:
130
- name: str
131
- kind: str
132
- initial: float
133
- final: float
134
- schedule: str = "constant"
135
- decay_tokens: int | None = None
136
- anchors: tuple[tuple[int, float], ...] = ()
137
-
138
- def to_dict(self) -> dict:
139
- return {
140
- "name": self.name,
141
- "kind": self.kind,
142
- "initial": self.initial,
143
- "final": self.final,
144
- "schedule": self.schedule,
145
- "decay_tokens": self.decay_tokens,
146
- "anchors": [list(anchor) for anchor in self.anchors],
147
- }
148
-
149
- def make_fn(
150
- self,
151
- fallback_decay_tokens: int,
152
- unique_tokens: int | None = None,
153
- ) -> Callable[[int], float]:
154
- if self.kind == "static":
155
- return lambda _tokens_seen, p=self.initial: p
156
- if self.kind == "anchor_decay":
157
- if unique_tokens is None:
158
- raise ValueError("anchor_decay conditions require unique_tokens")
159
- p = anchor_dropout(unique_tokens, self.anchors)
160
- return lambda _tokens_seen, p=p: p
161
- scheduler = DropoutDecayScheduler(
162
- DropoutDecayConfig(
163
- initial_dropout=self.initial,
164
- final_dropout=self.final,
165
- decay_tokens=self.decay_tokens or fallback_decay_tokens,
166
- schedule=self.schedule,
167
- )
168
- )
169
- return scheduler.value
170
-
171
-
172
- def anchor_dropout(unique_tokens: int, anchors: tuple[tuple[int, float], ...]) -> float:
173
- if not anchors:
174
- raise ValueError("anchor dropout schedule requires at least one anchor")
175
- ordered = sorted(anchors)
176
- if unique_tokens <= ordered[0][0]:
177
- return ordered[0][1]
178
- if unique_tokens >= ordered[-1][0]:
179
- return ordered[-1][1]
180
- log_unique = math.log(unique_tokens)
181
- for (left_tokens, left_dropout), (right_tokens, right_dropout) in zip(
182
- ordered, ordered[1:]
183
- ):
184
- if left_tokens <= unique_tokens <= right_tokens:
185
- left_log = math.log(left_tokens)
186
- right_log = math.log(right_tokens)
187
- mix = (log_unique - left_log) / (right_log - left_log)
188
- return left_dropout + mix * (right_dropout - left_dropout)
189
- return ordered[-1][1]
190
-
191
-
192
- def assert_mps_only() -> torch.device:
193
- if os.environ.get("PYTORCH_ENABLE_MPS_FALLBACK") == "1":
194
- raise SystemExit(
195
- "Refusing to run: PYTORCH_ENABLE_MPS_FALLBACK=1 could execute CPU fallbacks."
196
- )
197
- if not torch.backends.mps.is_built():
198
- raise SystemExit("PyTorch was not built with MPS. Stopping.")
199
- if not torch.backends.mps.is_available():
200
- raise SystemExit("MPS is not available. Stopping before any Torch experiment work.")
201
- device = torch.device("mps")
202
- torch.set_default_device(device)
203
- return device
204
-
205
-
206
- def clean_name(value: str) -> str:
207
- cleaned = "".join(
208
- c if c.isalnum() or c in {"-", "_"} else "_" for c in value.strip()
209
- )
210
- return cleaned.strip("_") or "model"
211
-
212
-
213
- def rate_label(rate: float) -> str:
214
- label = f"{rate:.3f}".rstrip("0").rstrip(".")
215
- return label if label else "0"
216
-
217
-
218
- def parse_model_spec(raw: str) -> ModelSpec:
219
- if "=" in raw:
220
- name, dims = raw.split("=", 1)
221
- name = clean_name(name)
222
- else:
223
- dims = raw
224
- name = ""
225
- parts = dims.lower().replace(",", "x").split("x")
226
- if len(parts) != 3:
227
- raise argparse.ArgumentTypeError(
228
- "model specs must look like 8x8x256 or name=8x8x256"
229
- )
230
- try:
231
- n_layer, n_head, n_embd = [int(part) for part in parts]
232
- except ValueError as exc:
233
- raise argparse.ArgumentTypeError("model dimensions must be integers") from exc
234
- if n_layer <= 0 or n_head <= 0 or n_embd <= 0:
235
- raise argparse.ArgumentTypeError("model dimensions must be positive")
236
- if n_embd % n_head != 0:
237
- raise argparse.ArgumentTypeError("n_embd must be divisible by n_head")
238
- if (n_embd // n_head) % 2 != 0:
239
- raise argparse.ArgumentTypeError("n_embd / n_head must be even for rotary attention")
240
- return ModelSpec(
241
- name or f"L{n_layer}_H{n_head}_D{n_embd}",
242
- n_layer,
243
- n_head,
244
- n_embd,
245
- )
246
-
247
-
248
- def parse_decay_spec(raw: str) -> DropoutCondition:
249
- parts = raw.split(":")
250
- if len(parts) not in {3, 4, 5}:
251
- raise argparse.ArgumentTypeError(
252
- "decay specs must look like "
253
- "name:initial:final[:cosine|smoothstep|linear[:decay_tokens]]"
254
- )
255
- name = clean_name(parts[0])
256
- try:
257
- initial = float(parts[1])
258
- final = float(parts[2])
259
- decay_tokens = int(parts[4]) if len(parts) == 5 else None
260
- except ValueError as exc:
261
- raise argparse.ArgumentTypeError(
262
- "decay dropout values and decay_tokens must be numeric"
263
- ) from exc
264
- schedule = parts[3] if len(parts) >= 4 else "cosine"
265
- if schedule not in {"cosine", "smoothstep", "linear"}:
266
- raise argparse.ArgumentTypeError(
267
- "decay schedule must be cosine, smoothstep, or linear"
268
- )
269
- return DropoutCondition(
270
- name=name,
271
- kind="decay",
272
- initial=initial,
273
- final=final,
274
- schedule=schedule,
275
- decay_tokens=decay_tokens,
276
- )
277
-
278
-
279
- def parse_anchor_decay_spec(raw: str) -> DropoutCondition:
280
- if ":" not in raw:
281
- raise argparse.ArgumentTypeError(
282
- "anchor decay specs must look like name:250000=0.60,500000=0.40"
283
- )
284
- name, raw_anchors = raw.split(":", 1)
285
- anchors: list[tuple[int, float]] = []
286
- for piece in raw_anchors.split(","):
287
- if "=" not in piece:
288
- raise argparse.ArgumentTypeError(
289
- "anchor decay anchors must look like token_count=dropout"
290
- )
291
- raw_tokens, raw_dropout = piece.split("=", 1)
292
- try:
293
- token_count = int(raw_tokens)
294
- dropout = float(raw_dropout)
295
- except ValueError as exc:
296
- raise argparse.ArgumentTypeError(
297
- "anchor token counts must be integers and dropout values numeric"
298
- ) from exc
299
- if token_count <= 0:
300
- raise argparse.ArgumentTypeError("anchor token counts must be positive")
301
- if not 0.0 <= dropout < 1.0:
302
- raise argparse.ArgumentTypeError("anchor dropout must satisfy 0 <= p < 1")
303
- anchors.append((token_count, dropout))
304
- anchors = sorted(set(anchors))
305
- if len(anchors) < 2:
306
- raise argparse.ArgumentTypeError("provide at least two dropout anchors")
307
- for left, right in zip(anchors, anchors[1:]):
308
- if right[1] > left[1]:
309
- raise argparse.ArgumentTypeError(
310
- "anchor dropout values must be non-increasing as token counts grow"
311
- )
312
- return DropoutCondition(
313
- name=clean_name(name),
314
- kind="anchor_decay",
315
- initial=anchors[0][1],
316
- final=anchors[-1][1],
317
- schedule="log_prefix_anchor",
318
- anchors=tuple(anchors),
319
- )
320
-
321
-
322
- def default_seeds(mode: str, seeds: list[int] | None) -> list[int]:
323
- if seeds:
324
- return seeds
325
- return [1] if mode == "screen_static" else [1, 2, 3]
326
-
327
-
328
- def set_seed(seed: int) -> None:
329
- random.seed(seed)
330
- np.random.seed(seed)
331
- torch.manual_seed(seed)
332
-
333
-
334
- def batch_seed(seed: int, model: ModelSpec, dropout_code: int, stage: int | None) -> int:
335
- return (
336
- seed * 1_000_003
337
- + model.n_layer * 100_003
338
- + model.n_head * 10_007
339
- + model.n_embd * 101
340
- + dropout_code * 37
341
- + (stage or 0) * 997
342
- )
343
-
344
-
345
- def make_batch(
346
- tokens: np.ndarray,
347
- token_limit: int,
348
- batch_size: int,
349
- block_size: int,
350
- rng: np.random.Generator,
351
- device: torch.device,
352
- ) -> tuple[torch.Tensor, torch.Tensor]:
353
- limit = min(token_limit, len(tokens))
354
- max_start = limit - block_size - 1
355
- if max_start <= 0:
356
- raise ValueError("token_limit is too small for the requested block_size")
357
- starts = rng.integers(0, max_start, size=batch_size)
358
- x_np = np.stack([tokens[start : start + block_size] for start in starts]).astype(
359
- np.int64
360
- )
361
- y_np = np.stack(
362
- [tokens[start + 1 : start + 1 + block_size] for start in starts]
363
- ).astype(np.int64)
364
- return (
365
- torch.tensor(x_np, dtype=torch.long, device=device),
366
- torch.tensor(y_np, dtype=torch.long, device=device),
367
- )
368
-
369
-
370
- @torch.no_grad()
371
- def estimate_loss(
372
- model: DropoutGPT,
373
- tokens: np.ndarray,
374
- token_limit: int,
375
- batches: int,
376
- args: argparse.Namespace,
377
- device: torch.device,
378
- rng_seed: int,
379
- ) -> float:
380
- if batches <= 0:
381
- return float("nan")
382
- model.eval()
383
- rng = np.random.default_rng(rng_seed)
384
- losses: list[float] = []
385
- for _ in range(batches):
386
- x, y = make_batch(tokens, token_limit, args.batch_size, args.block_size, rng, device)
387
- _, loss = model(x, y)
388
- losses.append(float(loss.item()))
389
- model.train()
390
- return float(statistics.fmean(losses))
391
-
392
-
393
- def write_jsonl_row(handle, row: dict) -> None:
394
- handle.write(json.dumps(row, sort_keys=True) + "\n")
395
- handle.flush()
396
-
397
-
398
- def metric_key(row: dict) -> tuple:
399
- return (
400
- row["run_mode"],
401
- row["condition"],
402
- row["condition_kind"],
403
- row.get("stage"),
404
- int(row["token_limit"]),
405
- row["model_name"],
406
- int(row["n_layer"]),
407
- int(row["n_head"]),
408
- int(row["n_embd"]),
409
- int(row["seed"]),
410
- float(row["dropout_initial"]),
411
- float(row["dropout_final"]),
412
- row["dropout_schedule"],
413
- )
414
-
415
-
416
- def planned_metric_key(
417
- *,
418
- mode: str,
419
- condition: DropoutCondition,
420
- model_spec: ModelSpec,
421
- seed: int,
422
- token_limit: int,
423
- stage: int | None = None,
424
- ) -> tuple:
425
- return (
426
- mode,
427
- condition.name,
428
- condition.kind,
429
- stage,
430
- int(token_limit),
431
- model_spec.name,
432
- int(model_spec.n_layer),
433
- int(model_spec.n_head),
434
- int(model_spec.n_embd),
435
- int(seed),
436
- float(condition.initial),
437
- float(condition.final),
438
- condition.schedule,
439
- )
440
-
441
-
442
- def load_metrics(path: Path) -> list[dict]:
443
- if not path.exists():
444
- return []
445
- rows = []
446
- for line in path.read_text(encoding="utf-8").splitlines():
447
- if line.strip():
448
- rows.append(json.loads(line))
449
- return rows
450
-
451
-
452
- def train_segment(
453
- *,
454
- run_mode: str,
455
- condition: DropoutCondition,
456
- model_spec: ModelSpec,
457
- config: GPTConfig,
458
- train_tokens: np.ndarray,
459
- val_tokens: np.ndarray,
460
- token_limit: int,
461
- steps: int,
462
- seed: int,
463
- args: argparse.Namespace,
464
- device: torch.device,
465
- dropout_fn: Callable[[int], float],
466
- metrics_file,
467
- trace_file,
468
- stage: int | None = None,
469
- model: DropoutGPT | None = None,
470
- optimizer: torch.optim.Optimizer | None = None,
471
- tokens_seen_start: int = 0,
472
- ) -> tuple[DropoutGPT, torch.optim.Optimizer, int, dict]:
473
- if model is None:
474
- set_seed(seed)
475
- model = DropoutGPT(config).to(device)
476
- optimizer = torch.optim.AdamW(
477
- model.parameters(),
478
- lr=args.lr,
479
- betas=(0.9, 0.95),
480
- weight_decay=args.weight_decay,
481
- )
482
- else:
483
- torch.manual_seed(seed + 10_000 + (stage or 0))
484
- if optimizer is None:
485
- raise ValueError("optimizer is required when reusing a model")
486
-
487
- model.train()
488
- dropout_code = int(round(condition.initial * 10_000))
489
- rng = np.random.default_rng(batch_seed(seed, model_spec, dropout_code, stage))
490
- tokens_seen = tokens_seen_start
491
- last_loss = float("nan")
492
- active_dropout = condition.initial
493
- t0 = time.time()
494
-
495
- for step in range(1, steps + 1):
496
- active_dropout = dropout_fn(tokens_seen)
497
- model.set_dropout(active_dropout)
498
- x, y = make_batch(train_tokens, token_limit, args.batch_size, args.block_size, rng, device)
499
- _, loss = model(x, y)
500
- optimizer.zero_grad(set_to_none=True)
501
- loss.backward()
502
- if args.grad_clip > 0:
503
- torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
504
- optimizer.step()
505
-
506
- tokens_seen += args.batch_size * args.block_size
507
- last_loss = float(loss.item())
508
- if args.log_every > 0 and step % args.log_every == 0:
509
- write_jsonl_row(
510
- trace_file,
511
- {
512
- "event": "train_step",
513
- "run_mode": run_mode,
514
- "condition": condition.name,
515
- "model_name": model_spec.name,
516
- "seed": seed,
517
- "stage": stage,
518
- "step": step,
519
- "steps": steps,
520
- "token_limit": int(token_limit),
521
- "tokens_seen": int(tokens_seen),
522
- "dropout": float(active_dropout),
523
- "train_batch_loss": last_loss,
524
- },
525
- )
526
- if args.eval_every > 0 and step % args.eval_every == 0:
527
- train_eval = estimate_loss(
528
- model,
529
- train_tokens,
530
- token_limit,
531
- args.trace_eval_batches,
532
- args,
533
- device,
534
- rng_seed=seed + 20_000 + step,
535
- )
536
- val_eval = estimate_loss(
537
- model,
538
- val_tokens,
539
- len(val_tokens),
540
- args.trace_eval_batches,
541
- args,
542
- device,
543
- rng_seed=seed + 30_000 + step,
544
- )
545
- write_jsonl_row(
546
- trace_file,
547
- {
548
- "event": "eval_step",
549
- "run_mode": run_mode,
550
- "condition": condition.name,
551
- "model_name": model_spec.name,
552
- "seed": seed,
553
- "stage": stage,
554
- "step": step,
555
- "steps": steps,
556
- "token_limit": int(token_limit),
557
- "tokens_seen": int(tokens_seen),
558
- "dropout": float(active_dropout),
559
- "train_eval_loss": train_eval,
560
- "val_eval_loss": val_eval,
561
- "generalization_gap": val_eval - train_eval,
562
- },
563
- )
564
-
565
- train_eval = estimate_loss(
566
- model,
567
- train_tokens,
568
- token_limit,
569
- args.train_eval_batches,
570
- args,
571
- device,
572
- rng_seed=seed + 40_000 + (stage or 0),
573
- )
574
- val_eval = estimate_loss(
575
- model,
576
- val_tokens,
577
- len(val_tokens),
578
- args.eval_batches,
579
- args,
580
- device,
581
- rng_seed=seed + 50_000 + (stage or 0),
582
- )
583
- row = {
584
- "run_mode": run_mode,
585
- "condition": condition.name,
586
- "condition_kind": condition.kind,
587
- "seed": seed,
588
- "stage": stage,
589
- "token_limit": int(token_limit),
590
- "steps": int(steps),
591
- "tokens_seen": int(tokens_seen),
592
- "dropout_initial": float(condition.initial),
593
- "dropout_final": float(condition.final),
594
- "dropout_schedule": condition.schedule,
595
- "dropout_active_final": float(active_dropout),
596
- "train_loss_last": last_loss,
597
- "train_eval_loss": train_eval,
598
- "val_eval_loss": val_eval,
599
- "eval_loss": val_eval,
600
- "generalization_gap": val_eval - train_eval,
601
- "elapsed_sec": time.time() - t0,
602
- "parameters": model.num_parameters(),
603
- "model_config": config.to_dict(),
604
- **model_spec.to_dict(),
605
- }
606
- write_jsonl_row(metrics_file, row)
607
- return model, optimizer, tokens_seen, row
608
-
609
-
610
- def summarize(rows: list[dict]) -> list[dict]:
611
- groups: dict[tuple, list[dict]] = defaultdict(list)
612
- for row in rows:
613
- key = (
614
- row["run_mode"],
615
- row["condition"],
616
- row["condition_kind"],
617
- row["stage"],
618
- row["token_limit"],
619
- row["model_name"],
620
- row["n_layer"],
621
- row["n_head"],
622
- row["n_embd"],
623
- row["parameters"],
624
- row["dropout_initial"],
625
- row["dropout_final"],
626
- row["dropout_schedule"],
627
- )
628
- groups[key].append(row)
629
-
630
- summary: list[dict] = []
631
- for group_rows in groups.values():
632
- first = group_rows[0]
633
- item = {field: first[field] for field in SUMMARY_FIELDS if field in first}
634
- item["n"] = len(group_rows)
635
- for source, mean_key, std_key in [
636
- ("train_eval_loss", "mean_train_eval_loss", "std_train_eval_loss"),
637
- ("val_eval_loss", "mean_val_eval_loss", "std_val_eval_loss"),
638
- ("generalization_gap", "mean_generalization_gap", "std_generalization_gap"),
639
- ]:
640
- values = [float(row[source]) for row in group_rows]
641
- item[mean_key] = statistics.fmean(values)
642
- item[std_key] = statistics.stdev(values) if len(values) > 1 else 0.0
643
- summary.append(item)
644
- return sorted(
645
- summary,
646
- key=lambda row: (
647
- row["run_mode"],
648
- row["model_name"],
649
- row["token_limit"],
650
- row["condition"],
651
- row["stage"] or -1,
652
- ),
653
- )
654
-
655
-
656
- def write_csv(path: Path, rows: list[dict], fieldnames: list[str]) -> None:
657
- with path.open("w", newline="", encoding="utf-8") as handle:
658
- writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="ignore")
659
- writer.writeheader()
660
- writer.writerows(rows)
661
-
662
-
663
- def format_duration(seconds: float) -> str:
664
- seconds = max(0, int(seconds))
665
- hours, remainder = divmod(seconds, 3600)
666
- minutes, secs = divmod(remainder, 60)
667
- if hours:
668
- return f"{hours}h{minutes:02d}m"
669
- if minutes:
670
- return f"{minutes}m{secs:02d}s"
671
- return f"{secs}s"
672
-
673
-
674
- class ProgressMeter:
675
- def __init__(self, total: int):
676
- self.total = max(0, total)
677
- self.done = 0
678
- self.started_at = time.time()
679
-
680
- def mark_done(self, row: dict) -> None:
681
- self.done += 1
682
- elapsed = time.time() - self.started_at
683
- mean = elapsed / self.done if self.done else 0.0
684
- remaining = max(0, self.total - self.done)
685
- eta = remaining * mean
686
- stage = row.get("stage")
687
- stage_text = f"stage={stage} " if stage is not None else ""
688
- print(
689
- "progress "
690
- f"{self.done}/{self.total} "
691
- f"eta={format_duration(eta)} "
692
- f"mode={row['run_mode']} "
693
- f"model={row['model_name']} "
694
- f"params={int(row['parameters']):,} "
695
- f"prefix={int(row['token_limit']):,} "
696
- f"{stage_text}"
697
- f"seed={row['seed']} "
698
- f"condition={row['condition']} "
699
- f"dropout={float(row['dropout_active_final']):.3f} "
700
- f"val={row['val_eval_loss']:.4f} "
701
- f"train={row['train_eval_loss']:.4f} "
702
- f"gap={row['generalization_gap']:.4f} "
703
- f"elapsed={format_duration(float(row['elapsed_sec']))}",
704
- flush=True,
705
- )
706
-
707
-
708
- def build_model_selection(summary: list[dict], args: argparse.Namespace) -> list[dict]:
709
- groups: dict[tuple, list[dict]] = defaultdict(list)
710
- for row in summary:
711
- if row["condition_kind"] == "static" and row["stage"] is None:
712
- groups[(row["run_mode"], row["token_limit"], row["model_name"])].append(row)
713
-
714
- selection: list[dict] = []
715
- for (run_mode, token_limit, model_name), rows in groups.items():
716
- curve = sorted(rows, key=lambda row: row["dropout_initial"])
717
- best = min(curve, key=lambda row: row["mean_val_eval_loss"])
718
- plateau = [
719
- row for row in curve
720
- if row["mean_val_eval_loss"] <= best["mean_val_eval_loss"] + args.plateau_delta
721
- ]
722
- zero = next((row for row in curve if row["dropout_initial"] == 0.0), None)
723
- nonzero = [row for row in curve if row["dropout_initial"] > 0.0]
724
- best_nonzero = min(nonzero, key=lambda row: row["mean_val_eval_loss"]) if nonzero else None
725
- max_dropout = max(curve, key=lambda row: row["dropout_initial"])
726
-
727
- zero_loss = zero["mean_val_eval_loss"] if zero else None
728
- zero_minus_best = zero_loss - best["mean_val_eval_loss"] if zero_loss is not None else None
729
- zero_minus_best_nonzero = (
730
- zero_loss - best_nonzero["mean_val_eval_loss"]
731
- if zero_loss is not None and best_nonzero is not None
732
- else None
733
- )
734
- max_minus_best = max_dropout["mean_val_eval_loss"] - best["mean_val_eval_loss"]
735
- has_nonzero_optimum = (
736
- best["dropout_initial"] > 0.0
737
- and zero_minus_best is not None
738
- and zero_minus_best >= args.min_nonzero_margin
739
- and max_minus_best >= args.min_high_dropout_margin
740
- )
741
- curve_json = json.dumps(
742
- [
743
- {
744
- "dropout": row["dropout_initial"],
745
- "mean_val_loss": row["mean_val_eval_loss"],
746
- "std_val_loss": row["std_val_eval_loss"],
747
- "mean_train_loss": row["mean_train_eval_loss"],
748
- "mean_generalization_gap": row["mean_generalization_gap"],
749
- "n": row["n"],
750
- }
751
- for row in curve
752
- ],
753
- sort_keys=True,
754
- )
755
- selection.append(
756
- {
757
- "run_mode": run_mode,
758
- "token_limit": token_limit,
759
- "model_name": model_name,
760
- "n_layer": best["n_layer"],
761
- "n_head": best["n_head"],
762
- "n_embd": best["n_embd"],
763
- "parameters": best["parameters"],
764
- "n": best["n"],
765
- "best_dropout": best["dropout_initial"],
766
- "best_val_loss": best["mean_val_eval_loss"],
767
- "best_val_std": best["std_val_eval_loss"],
768
- "plateau_start_dropout": min(row["dropout_initial"] for row in plateau),
769
- "plateau_end_dropout": max(row["dropout_initial"] for row in plateau),
770
- "plateau_delta": args.plateau_delta,
771
- "zero_dropout_val_loss": zero_loss,
772
- "zero_minus_best": zero_minus_best,
773
- "best_nonzero_dropout": (
774
- best_nonzero["dropout_initial"] if best_nonzero else None
775
- ),
776
- "best_nonzero_val_loss": (
777
- best_nonzero["mean_val_eval_loss"] if best_nonzero else None
778
- ),
779
- "zero_minus_best_nonzero": zero_minus_best_nonzero,
780
- "max_dropout": max_dropout["dropout_initial"],
781
- "max_dropout_val_loss": max_dropout["mean_val_eval_loss"],
782
- "max_dropout_minus_best": max_minus_best,
783
- "has_nonzero_optimum": has_nonzero_optimum,
784
- "meets_target_dropout": best["dropout_initial"] >= args.target_min_dropout,
785
- "curve_json": curve_json,
786
- }
787
- )
788
- return sorted(
789
- selection,
790
- key=lambda row: (
791
- not row["has_nonzero_optimum"],
792
- not row["meets_target_dropout"],
793
- row["best_val_loss"],
794
- ),
795
- )
796
-
797
-
798
- def write_screen_markdown_summary(output_dir: Path, rows: list[dict]) -> None:
799
- if not rows:
800
- return
801
- static_rows = [
802
- row
803
- for row in rows
804
- if row["run_mode"] == "screen_static" and row["condition_kind"] == "static"
805
- ]
806
- if not static_rows:
807
- return
808
-
809
- by_model_prefix_rate: dict[tuple[str, int, float], list[dict]] = defaultdict(list)
810
- for row in rows:
811
- if row["run_mode"] == "screen_static" and row["condition_kind"] == "static":
812
- by_model_prefix_rate[
813
- (
814
- row["model_name"],
815
- int(row["token_limit"]),
816
- float(row["dropout_initial"]),
817
- )
818
- ].append(row)
819
-
820
- aggregates: list[dict] = []
821
- for (model_name, prefix, dropout), group_rows in by_model_prefix_rate.items():
822
- first = group_rows[0]
823
- val_losses = [float(row["val_eval_loss"]) for row in group_rows]
824
- train_losses = [float(row["train_eval_loss"]) for row in group_rows]
825
- gaps = [float(row["generalization_gap"]) for row in group_rows]
826
- aggregates.append(
827
- {
828
- "model_name": model_name,
829
- "token_limit": prefix,
830
- "dropout_initial": dropout,
831
- "n": len(group_rows),
832
- "mean_val_eval_loss": statistics.fmean(val_losses),
833
- "std_val_eval_loss": statistics.stdev(val_losses)
834
- if len(val_losses) > 1
835
- else 0.0,
836
- "mean_train_eval_loss": statistics.fmean(train_losses),
837
- "std_train_eval_loss": statistics.stdev(train_losses)
838
- if len(train_losses) > 1
839
- else 0.0,
840
- "mean_generalization_gap": statistics.fmean(gaps),
841
- "std_generalization_gap": statistics.stdev(gaps)
842
- if len(gaps) > 1
843
- else 0.0,
844
- "parameters": int(first["parameters"]),
845
- "n_layer": int(first["n_layer"]),
846
- "n_head": int(first["n_head"]),
847
- "n_embd": int(first["n_embd"]),
848
- "block_size": int(first["model_config"]["block_size"]),
849
- "vocab_size": int(first["model_config"]["vocab_size"]),
850
- "tokens_seen": int(first["tokens_seen"]),
851
- "seeds": sorted({int(row["seed"]) for row in group_rows}),
852
- }
853
- )
854
-
855
- by_model: dict[str, list[dict]] = defaultdict(list)
856
- for row in aggregates:
857
- by_model[row["model_name"]].append(row)
858
-
859
- model_rows = []
860
- for model_name, model_group in by_model.items():
861
- first = model_group[0]
862
- seeds = sorted({seed for row in model_group for seed in row["seeds"]})
863
- model_rows.append(
864
- {
865
- "model_name": model_name,
866
- "parameters": first["parameters"],
867
- "n_layer": first["n_layer"],
868
- "n_head": first["n_head"],
869
- "n_embd": first["n_embd"],
870
- "block_size": first["block_size"],
871
- "vocab_size": first["vocab_size"],
872
- "seeds": seeds,
873
- }
874
- )
875
-
876
- lines = [
877
- "# Static Dropout Screen Summary",
878
- "",
879
- f"Run directory: `{output_dir}`",
880
- "",
881
- "## Models",
882
- "",
883
- "| Model | Params | Layers | Heads | Embedding | Block | Vocab | Seeds |",
884
- "|---|---:|---:|---:|---:|---:|---:|---|",
885
- ]
886
- for model in sorted(model_rows, key=lambda item: item["parameters"]):
887
- lines.append(
888
- "| "
889
- f"`{model['model_name']}` | {model['parameters']:,} | "
890
- f"{model['n_layer']} | {model['n_head']} | {model['n_embd']} | "
891
- f"{model['block_size']} | {model['vocab_size']} | "
892
- f"{', '.join(str(seed) for seed in model['seeds'])} |"
893
- )
894
- lines.extend(
895
- [
896
- "",
897
- "## Best Dropout By Model And Prefix",
898
- "",
899
- "| Model | Prefix tokens | Effective epochs | Best dropout | Mean val loss | Val std | Mean train loss | Mean gap | Plateau/bracket note |",
900
- "|---|---:|---:|---:|---:|---:|---:|---:|---|",
901
- ]
902
- )
903
- for model_name, model_group in sorted(by_model.items()):
904
- by_prefix: dict[int, list[dict]] = defaultdict(list)
905
- for row in model_group:
906
- by_prefix[int(row["token_limit"])].append(row)
907
- for prefix, prefix_rows in sorted(by_prefix.items()):
908
- best = min(prefix_rows, key=lambda row: row["mean_val_eval_loss"])
909
- rates = [float(row["dropout_initial"]) for row in prefix_rows]
910
- eff_epochs = float(best["tokens_seen"]) / prefix
911
- if best["dropout_initial"] == max(rates):
912
- note = "not bracketed; best at top of tested grid"
913
- elif best["dropout_initial"] == min(rates):
914
- note = "not bracketed; best at bottom of tested grid"
915
- else:
916
- note = "bracketed by tested grid"
917
- lines.append(
918
- "| "
919
- f"`{model_name}` | {prefix:,} | {eff_epochs:.2f} | "
920
- f"{best['dropout_initial']:.2f} | "
921
- f"{best['mean_val_eval_loss']:.4f} | "
922
- f"{best['std_val_eval_loss']:.4f} | "
923
- f"{best['mean_train_eval_loss']:.4f} | "
924
- f"{best['mean_generalization_gap']:.4f} | {note} |"
925
- )
926
-
927
- for model_name, model_group in sorted(by_model.items()):
928
- by_prefix = defaultdict(list)
929
- for row in model_group:
930
- by_prefix[int(row["token_limit"])].append(row)
931
- lines.extend(
932
- [
933
- "",
934
- f"## Model `{model_name}`",
935
- ]
936
- )
937
- for prefix, prefix_rows in sorted(by_prefix.items()):
938
- eff_epochs = float(prefix_rows[0]["tokens_seen"]) / prefix
939
- lines.extend(
940
- [
941
- "",
942
- f"### Prefix {prefix:,} Tokens ({eff_epochs:.2f} Effective Epochs)",
943
- "",
944
- "| Dropout | N | Mean val loss | Val std | Mean train loss | Train std | Mean gap | Gap std | Sampled tokens | Params |",
945
- "|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
946
- ]
947
- )
948
- for row in sorted(prefix_rows, key=lambda item: item["dropout_initial"]):
949
- lines.append(
950
- "| "
951
- f"{row['dropout_initial']:.2f} | {row['n']} | "
952
- f"{row['mean_val_eval_loss']:.4f} | "
953
- f"{row['std_val_eval_loss']:.4f} | "
954
- f"{row['mean_train_eval_loss']:.4f} | "
955
- f"{row['std_train_eval_loss']:.4f} | "
956
- f"{row['mean_generalization_gap']:.4f} | "
957
- f"{row['std_generalization_gap']:.4f} | "
958
- f"{int(row['tokens_seen']):,} | {int(row['parameters']):,} |"
959
- )
960
-
961
- output = "\n".join(lines) + "\n"
962
- (output_dir / "RESULT_SUMMARY.md").write_text(output, encoding="utf-8")
963
-
964
-
965
- def svg_escape(value: object) -> str:
966
- return (
967
- str(value)
968
- .replace("&", "&amp;")
969
- .replace("<", "&lt;")
970
- .replace(">", "&gt;")
971
- .replace('"', "&quot;")
972
- )
973
-
974
-
975
- def write_dropout_curve_svg(output_dir: Path, summary: list[dict]) -> None:
976
- rows = [
977
- row
978
- for row in summary
979
- if row["run_mode"] == "screen_static" and row["condition_kind"] == "static"
980
- ]
981
- if not rows:
982
- return
983
-
984
- grouped: dict[tuple[str, int], list[dict]] = defaultdict(list)
985
- model_params: dict[str, int] = {}
986
- for row in rows:
987
- model_name = row["model_name"]
988
- grouped[(model_name, int(row["token_limit"]))].append(row)
989
- model_params[model_name] = int(row["parameters"])
990
-
991
- models = sorted(model_params, key=lambda name: model_params[name])
992
- prefixes = sorted({int(row["token_limit"]) for row in rows})
993
- panel_w, panel_h = 230, 170
994
- margin_l, margin_t = 58, 34
995
- plot_w, plot_h = 142, 94
996
- gap_x, gap_y = 18, 38
997
- width = margin_l + len(prefixes) * panel_w + gap_x
998
- height = 70 + len(models) * (panel_h + gap_y)
999
- colors = ["#1f77b4", "#d62728", "#2ca02c", "#9467bd", "#ff7f0e"]
1000
-
1001
- parts = [
1002
- f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
1003
- "<style>",
1004
- "text{font-family:Arial,Helvetica,sans-serif;fill:#111827}",
1005
- ".small{font-size:10px}.label{font-size:11px}.title{font-size:15px;font-weight:700}",
1006
- ".axis{stroke:#374151;stroke-width:1}.grid{stroke:#e5e7eb;stroke-width:1}.line{fill:none;stroke-width:2}",
1007
- "</style>",
1008
- '<rect width="100%" height="100%" fill="#ffffff"/>',
1009
- '<text x="24" y="28" class="title">Static dropout law: validation loss vs dropout</text>',
1010
- '<text x="24" y="48" class="label">Each panel uses its own y-scale. Points are one-seed means unless N &gt; 1.</text>',
1011
- ]
1012
-
1013
- for col, prefix in enumerate(prefixes):
1014
- x = margin_l + col * panel_w + plot_w / 2
1015
- parts.append(
1016
- f'<text x="{x:.1f}" y="70" text-anchor="middle" class="label">{prefix:,} prefix tokens</text>'
1017
- )
1018
-
1019
- for row_idx, model_name in enumerate(models):
1020
- row_y = 92 + row_idx * (panel_h + gap_y)
1021
- parts.append(
1022
- f'<text x="24" y="{row_y + 48}" class="label" transform="rotate(-90 24 {row_y + 48})">'
1023
- f'{svg_escape(model_name)} ({model_params[model_name] / 1_000_000:.1f}M)</text>'
1024
- )
1025
- for col, prefix in enumerate(prefixes):
1026
- panel_x = margin_l + col * panel_w
1027
- panel_y = row_y
1028
- curve = sorted(
1029
- grouped.get((model_name, prefix), []),
1030
- key=lambda item: float(item["dropout_initial"]),
1031
- )
1032
- if not curve:
1033
- continue
1034
- losses = [float(item["mean_val_eval_loss"]) for item in curve]
1035
- min_loss, max_loss = min(losses), max(losses)
1036
- pad = max(0.02, (max_loss - min_loss) * 0.08)
1037
- y_min, y_max = min_loss - pad, max_loss + pad
1038
- best = min(curve, key=lambda item: float(item["mean_val_eval_loss"]))
1039
-
1040
- def px(dropout: float) -> float:
1041
- return panel_x + (dropout / 0.9) * plot_w
1042
-
1043
- def py(loss: float) -> float:
1044
- scale = (loss - y_min) / (y_max - y_min)
1045
- return panel_y + plot_h - scale * plot_h
1046
-
1047
- parts.extend(
1048
- [
1049
- f'<line x1="{panel_x:.1f}" y1="{panel_y:.1f}" x2="{panel_x:.1f}" y2="{panel_y + plot_h:.1f}" class="axis"/>',
1050
- f'<line x1="{panel_x:.1f}" y1="{panel_y + plot_h:.1f}" x2="{panel_x + plot_w:.1f}" y2="{panel_y + plot_h:.1f}" class="axis"/>',
1051
- f'<line x1="{panel_x:.1f}" y1="{panel_y:.1f}" x2="{panel_x + plot_w:.1f}" y2="{panel_y:.1f}" class="grid"/>',
1052
- f'<text x="{panel_x:.1f}" y="{panel_y - 6:.1f}" class="small">{y_max:.2f}</text>',
1053
- f'<text x="{panel_x:.1f}" y="{panel_y + plot_h + 13:.1f}" class="small">{y_min:.2f}</text>',
1054
- f'<text x="{panel_x:.1f}" y="{panel_y + plot_h + 28:.1f}" class="small">0</text>',
1055
- f'<text x="{panel_x + plot_w:.1f}" y="{panel_y + plot_h + 28:.1f}" text-anchor="end" class="small">0.9</text>',
1056
- ]
1057
- )
1058
- points = " ".join(
1059
- f"{px(float(item['dropout_initial'])):.1f},{py(float(item['mean_val_eval_loss'])):.1f}"
1060
- for item in curve
1061
- )
1062
- color = colors[row_idx % len(colors)]
1063
- parts.append(f'<polyline points="{points}" class="line" stroke="{color}"/>')
1064
- for item in curve:
1065
- dropout = float(item["dropout_initial"])
1066
- loss = float(item["mean_val_eval_loss"])
1067
- radius = 4 if item is best else 2.7
1068
- fill = "#111827" if item is best else "#ffffff"
1069
- parts.append(
1070
- f'<circle cx="{px(dropout):.1f}" cy="{py(loss):.1f}" r="{radius}" fill="{fill}" stroke="{color}" stroke-width="1.5"/>'
1071
- )
1072
- parts.append(
1073
- f'<text x="{panel_x + plot_w + 8:.1f}" y="{panel_y + 14:.1f}" class="small">'
1074
- f'best p={float(best["dropout_initial"]):.2f}</text>'
1075
- )
1076
- parts.append(
1077
- f'<text x="{panel_x + plot_w + 8:.1f}" y="{panel_y + 28:.1f}" class="small">'
1078
- f'loss={float(best["mean_val_eval_loss"]):.3f}</text>'
1079
- )
1080
-
1081
- parts.append("</svg>")
1082
- (output_dir / "dropout_curves.svg").write_text("\n".join(parts), encoding="utf-8")
1083
-
1084
-
1085
- def write_stream_markdown_summary(output_dir: Path, rows: list[dict]) -> None:
1086
- stream_rows = [row for row in rows if row["run_mode"] == "locked_stream"]
1087
- if not stream_rows:
1088
- return
1089
-
1090
- by_condition_stage: dict[tuple[str, int], list[dict]] = defaultdict(list)
1091
- by_condition: dict[str, list[dict]] = defaultdict(list)
1092
- for row in stream_rows:
1093
- condition = row["condition"]
1094
- by_condition_stage[(condition, int(row["stage"]))].append(row)
1095
- by_condition[condition].append(row)
1096
-
1097
- first = stream_rows[0]
1098
- seeds = sorted({int(row["seed"]) for row in stream_rows})
1099
- conditions = sorted(
1100
- by_condition,
1101
- key=lambda name: (
1102
- by_condition[name][0]["condition_kind"] != "anchor_decay",
1103
- by_condition[name][0]["dropout_initial"],
1104
- name,
1105
- ),
1106
- )
1107
- stages = sorted({int(row["stage"]) for row in stream_rows})
1108
-
1109
- lines = [
1110
- "# Locked Streaming Dropout Summary",
1111
- "",
1112
- f"Run directory: `{output_dir}`",
1113
- "",
1114
- (
1115
- f"Model: `{first['model_name']}` causal Transformer, "
1116
- f"{int(first['parameters']):,} parameters, {first['n_layer']} layers, "
1117
- f"{first['n_head']} heads, {first['n_embd']} embedding dim."
1118
- ),
1119
- (
1120
- f"Training per stage: {first['steps']:,} steps. "
1121
- "Sampled tokens are cumulative in each stage row. "
1122
- f"Seeds present: {', '.join(str(seed) for seed in seeds)}."
1123
- ),
1124
- "",
1125
- "## Condition Ranking",
1126
- "",
1127
- "| Condition | Kind | Final dropout | Mean trajectory val loss | Final val loss | Final gap | Dropout path |",
1128
- "|---|---|---:|---:|---:|---:|---|",
1129
- ]
1130
-
1131
- ranking = []
1132
- for condition in conditions:
1133
- stage_items = []
1134
- for stage in stages:
1135
- group = by_condition_stage.get((condition, stage), [])
1136
- if not group:
1137
- continue
1138
- stage_items.append(
1139
- {
1140
- "stage": stage,
1141
- "token_limit": int(group[0]["token_limit"]),
1142
- "mean_val": statistics.fmean(
1143
- float(row["val_eval_loss"]) for row in group
1144
- ),
1145
- "mean_gap": statistics.fmean(
1146
- float(row["generalization_gap"]) for row in group
1147
- ),
1148
- "mean_dropout": statistics.fmean(
1149
- float(row["dropout_active_final"]) for row in group
1150
- ),
1151
- "kind": group[0]["condition_kind"],
1152
- }
1153
- )
1154
- if not stage_items:
1155
- continue
1156
- final = max(stage_items, key=lambda item: item["stage"])
1157
- ranking.append(
1158
- {
1159
- "condition": condition,
1160
- "kind": stage_items[0]["kind"],
1161
- "trajectory_val": statistics.fmean(item["mean_val"] for item in stage_items),
1162
- "final_val": final["mean_val"],
1163
- "final_gap": final["mean_gap"],
1164
- "final_dropout": final["mean_dropout"],
1165
- "dropout_path": " -> ".join(
1166
- f"{item['mean_dropout']:.2f}" for item in stage_items
1167
- ),
1168
- }
1169
- )
1170
-
1171
- for item in sorted(ranking, key=lambda row: row["trajectory_val"]):
1172
- lines.append(
1173
- "| "
1174
- f"`{item['condition']}` | {item['kind']} | "
1175
- f"{item['final_dropout']:.2f} | {item['trajectory_val']:.4f} | "
1176
- f"{item['final_val']:.4f} | {item['final_gap']:.4f} | "
1177
- f"{item['dropout_path']} |"
1178
- )
1179
-
1180
- lines.extend(["", "## Stage Trajectory", ""])
1181
- for stage in stages:
1182
- stage_groups = {
1183
- condition: by_condition_stage[(condition, stage)]
1184
- for condition in conditions
1185
- if (condition, stage) in by_condition_stage
1186
- }
1187
- if not stage_groups:
1188
- continue
1189
- prefix = int(next(iter(stage_groups.values()))[0]["token_limit"])
1190
- lines.extend(
1191
- [
1192
- f"### Stage {stage}: {prefix:,} Prefix Tokens",
1193
- "",
1194
- "| Condition | Dropout | Mean val loss | Mean train loss | Mean gap | N |",
1195
- "|---|---:|---:|---:|---:|---:|",
1196
- ]
1197
- )
1198
- for condition, group in sorted(
1199
- stage_groups.items(),
1200
- key=lambda item: statistics.fmean(
1201
- float(row["val_eval_loss"]) for row in item[1]
1202
- ),
1203
- ):
1204
- val = statistics.fmean(float(row["val_eval_loss"]) for row in group)
1205
- train = statistics.fmean(float(row["train_eval_loss"]) for row in group)
1206
- gap = statistics.fmean(float(row["generalization_gap"]) for row in group)
1207
- dropout = statistics.fmean(
1208
- float(row["dropout_active_final"]) for row in group
1209
- )
1210
- lines.append(
1211
- "| "
1212
- f"`{condition}` | {dropout:.2f} | {val:.4f} | "
1213
- f"{train:.4f} | {gap:.4f} | {len(group)} |"
1214
- )
1215
- lines.append("")
1216
-
1217
- (output_dir / "RESULT_SUMMARY.md").write_text(
1218
- "\n".join(lines).rstrip() + "\n",
1219
- encoding="utf-8",
1220
- )
1221
-
1222
-
1223
- def static_conditions(dropout_rates: list[float]) -> list[DropoutCondition]:
1224
- return [
1225
- DropoutCondition(
1226
- name=f"static_dropout_{rate_label(rate)}",
1227
- kind="static",
1228
- initial=rate,
1229
- final=rate,
1230
- )
1231
- for rate in dropout_rates
1232
- ]
1233
-
1234
-
1235
- def run_fixed_static_sweep(
1236
- *,
1237
- args: argparse.Namespace,
1238
- model_specs: list[ModelSpec],
1239
- seeds: list[int],
1240
- train_tokens: np.ndarray,
1241
- val_tokens: np.ndarray,
1242
- tokenizer_vocab_size: int,
1243
- token_limits: list[int],
1244
- device: torch.device,
1245
- metrics_file,
1246
- trace_file,
1247
- completed_keys: set[tuple] | None = None,
1248
- ) -> list[dict]:
1249
- rows: list[dict] = []
1250
- completed_keys = completed_keys or set()
1251
- conditions = static_conditions(sorted(set(args.dropout_rates)))
1252
- planned = 0
1253
- for token_limit in token_limits:
1254
- for model_spec in model_specs:
1255
- for condition in conditions:
1256
- for seed in seeds:
1257
- key = planned_metric_key(
1258
- mode=args.mode,
1259
- condition=condition,
1260
- model_spec=model_spec,
1261
- seed=seed,
1262
- token_limit=token_limit,
1263
- )
1264
- if key not in completed_keys:
1265
- planned += 1
1266
- progress = ProgressMeter(planned)
1267
- for token_limit in token_limits:
1268
- for model_spec in model_specs:
1269
- best_val_loss = float("inf")
1270
- worse_streak = 0
1271
- for condition in conditions:
1272
- condition_rows: list[dict] = []
1273
- for seed in seeds:
1274
- key = planned_metric_key(
1275
- mode=args.mode,
1276
- condition=condition,
1277
- model_spec=model_spec,
1278
- seed=seed,
1279
- token_limit=token_limit,
1280
- )
1281
- if key in completed_keys:
1282
- write_jsonl_row(
1283
- trace_file,
1284
- {
1285
- "event": "skipped_completed_condition",
1286
- "run_mode": args.mode,
1287
- "condition": condition.name,
1288
- "model_name": model_spec.name,
1289
- "seed": seed,
1290
- "stage": None,
1291
- "token_limit": int(token_limit),
1292
- },
1293
- )
1294
- continue
1295
- config = model_spec.config(
1296
- tokenizer_vocab_size,
1297
- args.block_size,
1298
- condition.initial,
1299
- )
1300
- model, optimizer, _, row = train_segment(
1301
- run_mode=args.mode,
1302
- condition=condition,
1303
- model_spec=model_spec,
1304
- config=config,
1305
- train_tokens=train_tokens,
1306
- val_tokens=val_tokens,
1307
- token_limit=token_limit,
1308
- steps=args.steps,
1309
- seed=seed,
1310
- args=args,
1311
- device=device,
1312
- dropout_fn=condition.make_fn(
1313
- args.steps * args.batch_size * args.block_size
1314
- ),
1315
- metrics_file=metrics_file,
1316
- trace_file=trace_file,
1317
- )
1318
- rows.append(row)
1319
- condition_rows.append(row)
1320
- completed_keys.add(metric_key(row))
1321
- progress.mark_done(row)
1322
- del model, optimizer
1323
- torch.mps.empty_cache()
1324
-
1325
- if not condition_rows:
1326
- continue
1327
- mean_val_loss = statistics.fmean(
1328
- float(row["val_eval_loss"]) for row in condition_rows
1329
- )
1330
- if mean_val_loss < best_val_loss - args.screen_prune_min_delta:
1331
- best_val_loss = mean_val_loss
1332
- worse_streak = 0
1333
- elif mean_val_loss > best_val_loss + args.screen_prune_min_delta:
1334
- worse_streak += 1
1335
-
1336
- if (
1337
- args.mode == "screen_static"
1338
- and args.screen_early_stop
1339
- and worse_streak >= args.screen_prune_patience
1340
- and condition.initial >= args.target_min_dropout
1341
- ):
1342
- write_jsonl_row(
1343
- trace_file,
1344
- {
1345
- "event": "screen_pruned_model",
1346
- "run_mode": args.mode,
1347
- "model_name": model_spec.name,
1348
- "token_limit": int(token_limit),
1349
- "best_val_loss": best_val_loss,
1350
- "pruned_after_dropout": condition.initial,
1351
- "worse_streak": worse_streak,
1352
- "remaining_dropouts": [
1353
- rate
1354
- for rate in args.dropout_rates
1355
- if rate > condition.initial
1356
- ],
1357
- },
1358
- )
1359
- break
1360
- return rows
1361
-
1362
-
1363
- def run_locked_stream(
1364
- *,
1365
- args: argparse.Namespace,
1366
- model_specs: list[ModelSpec],
1367
- seeds: list[int],
1368
- train_tokens: np.ndarray,
1369
- val_tokens: np.ndarray,
1370
- tokenizer_vocab_size: int,
1371
- stream_caps: list[int],
1372
- device: torch.device,
1373
- metrics_file,
1374
- trace_file,
1375
- completed_keys: set[tuple] | None = None,
1376
- ) -> list[dict]:
1377
- rows: list[dict] = []
1378
- completed_keys = completed_keys or set()
1379
- conditions = args.anchor_decays + static_conditions(args.dropout_rates) + args.decays
1380
- fallback_decay_tokens = (
1381
- args.decay_tokens
1382
- or args.stage_steps * args.batch_size * args.block_size * len(stream_caps)
1383
- )
1384
- planned = 0
1385
- for model_spec in model_specs:
1386
- for condition in conditions:
1387
- for seed in seeds:
1388
- for stage, token_limit in enumerate(stream_caps):
1389
- key = planned_metric_key(
1390
- mode=args.mode,
1391
- condition=condition,
1392
- model_spec=model_spec,
1393
- seed=seed,
1394
- token_limit=token_limit,
1395
- stage=stage,
1396
- )
1397
- if key not in completed_keys:
1398
- planned += 1
1399
- progress = ProgressMeter(planned)
1400
- for model_spec in model_specs:
1401
- for condition in conditions:
1402
- for seed in seeds:
1403
- model = None
1404
- optimizer = None
1405
- tokens_seen = 0
1406
- for stage, token_limit in enumerate(stream_caps):
1407
- key = planned_metric_key(
1408
- mode=args.mode,
1409
- condition=condition,
1410
- model_spec=model_spec,
1411
- seed=seed,
1412
- token_limit=token_limit,
1413
- stage=stage,
1414
- )
1415
- if key in completed_keys:
1416
- write_jsonl_row(
1417
- trace_file,
1418
- {
1419
- "event": "skipped_completed_condition",
1420
- "run_mode": args.mode,
1421
- "condition": condition.name,
1422
- "model_name": model_spec.name,
1423
- "seed": seed,
1424
- "stage": stage,
1425
- "token_limit": int(token_limit),
1426
- },
1427
- )
1428
- continue
1429
- config = model_spec.config(
1430
- tokenizer_vocab_size,
1431
- args.block_size,
1432
- condition.initial,
1433
- )
1434
- model, optimizer, tokens_seen, row = train_segment(
1435
- run_mode=args.mode,
1436
- condition=condition,
1437
- model_spec=model_spec,
1438
- config=config,
1439
- train_tokens=train_tokens,
1440
- val_tokens=val_tokens,
1441
- token_limit=token_limit,
1442
- steps=args.stage_steps,
1443
- seed=seed,
1444
- args=args,
1445
- device=device,
1446
- dropout_fn=condition.make_fn(
1447
- fallback_decay_tokens,
1448
- unique_tokens=token_limit,
1449
- ),
1450
- metrics_file=metrics_file,
1451
- trace_file=trace_file,
1452
- stage=stage,
1453
- model=model,
1454
- optimizer=optimizer,
1455
- tokens_seen_start=tokens_seen,
1456
- )
1457
- rows.append(row)
1458
- completed_keys.add(metric_key(row))
1459
- progress.mark_done(row)
1460
- del model, optimizer
1461
- torch.mps.empty_cache()
1462
- return rows
1463
-
1464
-
1465
- def prepare_data(args: argparse.Namespace, output_dir: Path, required_train_tokens: int):
1466
- cache_dir = Path(args.cache_dir) if args.cache_dir else output_dir / "cache"
1467
- cache_dir.mkdir(parents=True, exist_ok=True)
1468
- if args.use_cached_data:
1469
- if args.force_retokenize:
1470
- raise ValueError("--use-cached-data cannot be combined with --force-retokenize")
1471
- return load_cached_splits(
1472
- cache_dir=cache_dir,
1473
- vocab_size=args.vocab_size,
1474
- max_required_train_tokens=required_train_tokens,
1475
- val_tokens=args.val_tokens,
1476
- allow_short_corpus=args.allow_short_corpus,
1477
- )
1478
-
1479
- paths = resolve_paths(args.corpus, args.corpus_glob)
1480
- tokenizer = train_or_load_tokenizer(
1481
- paths=paths,
1482
- output_dir=cache_dir,
1483
- vocab_size=args.vocab_size,
1484
- tokenizer_train_chars=args.tokenizer_train_chars,
1485
- text_column=args.text_column,
1486
- force_retrain=args.force_retokenize,
1487
- )
1488
- splits = encode_corpus(
1489
- paths=paths,
1490
- tokenizer=tokenizer,
1491
- output_dir=cache_dir,
1492
- max_required_train_tokens=required_train_tokens,
1493
- val_tokens=args.val_tokens,
1494
- text_column=args.text_column,
1495
- allow_short_corpus=args.allow_short_corpus,
1496
- force_reencode=args.force_retokenize,
1497
- )
1498
- return tokenizer, splits
1499
-
1500
-
1501
- def run(args: argparse.Namespace) -> Path:
1502
- device = assert_mps_only()
1503
- seeds = default_seeds(args.mode, args.seeds)
1504
- model_specs = [parse_model_spec(spec) for spec in args.models]
1505
- if args.mode != "locked_stream" and (args.decays or args.anchor_decays):
1506
- raise ValueError("--decays and --anchor-decays are only used with --mode locked_stream")
1507
- if args.resume_from and args.mode == "locked_stream":
1508
- raise ValueError("--resume-from currently supports fixed static sweeps only")
1509
-
1510
- if args.resume_from:
1511
- output_dir = Path(args.resume_from)
1512
- if not output_dir.exists():
1513
- raise FileNotFoundError(f"resume directory does not exist: {output_dir}")
1514
- else:
1515
- run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
1516
- output_dir = Path(args.output_dir) / args.mode / run_id
1517
- output_dir.mkdir(parents=True, exist_ok=True)
1518
- required_train_tokens = max(
1519
- args.stream_token_caps if args.mode == "locked_stream" else args.token_limits
1520
- )
1521
- tokenizer, splits = prepare_data(args, output_dir, required_train_tokens)
1522
- token_limits = [min(limit, len(splits.train)) for limit in args.token_limits]
1523
- stream_caps = [min(limit, len(splits.train)) for limit in args.stream_token_caps]
1524
-
1525
- args_payload = vars(args).copy()
1526
- args_payload["decays"] = [condition.to_dict() for condition in args.decays]
1527
- args_payload["anchor_decays"] = [
1528
- condition.to_dict() for condition in args.anchor_decays
1529
- ]
1530
- config_payload = {
1531
- "args": args_payload,
1532
- "mode": args.mode,
1533
- "seeds": seeds,
1534
- "models": [model.to_dict() for model in model_specs],
1535
- "device": str(device),
1536
- "torch": torch.__version__,
1537
- "python": sys.version,
1538
- "mps_available": torch.backends.mps.is_available(),
1539
- "attribution": NANOCHAT_ATTRIBUTION,
1540
- "tokenizer_path": str(splits.tokenizer_path),
1541
- "encoded_path": str(splits.encoded_path),
1542
- "train_tokens": int(len(splits.train)),
1543
- "val_tokens": int(len(splits.val)),
1544
- "effective_token_limits": [int(limit) for limit in token_limits],
1545
- "effective_stream_token_caps": [int(limit) for limit in stream_caps],
1546
- "resume_from": str(args.resume_from) if args.resume_from else None,
1547
- }
1548
- config_name = "config.resume.json" if args.resume_from else "config.json"
1549
- (output_dir / config_name).write_text(
1550
- json.dumps(config_payload, indent=2),
1551
- encoding="utf-8",
1552
- )
1553
-
1554
- metrics_path = output_dir / "metrics.jsonl"
1555
- trace_path = output_dir / "trace.jsonl"
1556
- existing_rows = load_metrics(metrics_path) if args.resume_from else []
1557
- completed_keys = {metric_key(row) for row in existing_rows}
1558
- with (
1559
- metrics_path.open("a" if args.resume_from else "w", encoding="utf-8") as metrics_file,
1560
- trace_path.open("a" if args.resume_from else "w", encoding="utf-8") as trace_file,
1561
- ):
1562
- if args.mode in {"screen_static", "confirm_static"}:
1563
- new_rows = run_fixed_static_sweep(
1564
- args=args,
1565
- model_specs=model_specs,
1566
- seeds=seeds,
1567
- train_tokens=splits.train,
1568
- val_tokens=splits.val,
1569
- tokenizer_vocab_size=tokenizer.vocab_size,
1570
- token_limits=token_limits,
1571
- device=device,
1572
- metrics_file=metrics_file,
1573
- trace_file=trace_file,
1574
- completed_keys=completed_keys,
1575
- )
1576
- else:
1577
- new_rows = run_locked_stream(
1578
- args=args,
1579
- model_specs=model_specs,
1580
- seeds=seeds,
1581
- train_tokens=splits.train,
1582
- val_tokens=splits.val,
1583
- tokenizer_vocab_size=tokenizer.vocab_size,
1584
- stream_caps=stream_caps,
1585
- device=device,
1586
- metrics_file=metrics_file,
1587
- trace_file=trace_file,
1588
- )
1589
- rows = existing_rows + new_rows
1590
-
1591
- summary = summarize(rows)
1592
- (output_dir / "summary.json").write_text(
1593
- json.dumps(summary, indent=2),
1594
- encoding="utf-8",
1595
- )
1596
- write_csv(output_dir / "summary.csv", summary, SUMMARY_FIELDS)
1597
- if args.mode in {"screen_static", "confirm_static"}:
1598
- selection = build_model_selection(summary, args)
1599
- (output_dir / "model_selection.json").write_text(
1600
- json.dumps(selection, indent=2),
1601
- encoding="utf-8",
1602
- )
1603
- write_csv(output_dir / "model_selection.csv", selection, SELECTION_FIELDS)
1604
- write_screen_markdown_summary(output_dir, rows)
1605
- write_dropout_curve_svg(output_dir, summary)
1606
- elif args.mode == "locked_stream":
1607
- write_stream_markdown_summary(output_dir, rows)
1608
-
1609
- print(
1610
- json.dumps(
1611
- {
1612
- "output_dir": str(output_dir),
1613
- "new_rows": len(new_rows),
1614
- "total_metric_rows": len(rows),
1615
- "summary_rows": len(summary),
1616
- },
1617
- indent=2,
1618
- )
1619
- )
1620
- return output_dir
1621
-
1622
-
1623
- def build_parser() -> argparse.ArgumentParser:
1624
- parser = argparse.ArgumentParser(
1625
- description="MPS-only dropout/model selection experiments"
1626
- )
1627
- parser.add_argument(
1628
- "--mode",
1629
- choices=["screen_static", "confirm_static", "locked_stream"],
1630
- default="screen_static",
1631
- )
1632
- parser.add_argument("--corpus", default=None, help="Text or parquet corpus path")
1633
- parser.add_argument("--corpus-glob", default=None, help="Glob of text/parquet corpus paths")
1634
- parser.add_argument("--text-column", default="text", help="Parquet text column")
1635
- parser.add_argument(
1636
- "--use-cached-data",
1637
- action="store_true",
1638
- help=(
1639
- "Load tokenizer-v{vocab}.json and tokens-v{vocab}-*.npy from --cache-dir "
1640
- "instead of requiring the original text/parquet corpus."
1641
- ),
1642
- )
1643
- parser.add_argument("--output-dir", default="runs")
1644
- parser.add_argument(
1645
- "--resume-from",
1646
- default=None,
1647
- help="Existing fixed-static run directory; completed metric rows are skipped",
1648
- )
1649
- parser.add_argument("--cache-dir", default=".cache/dropout_decay")
1650
- parser.add_argument(
1651
- "--models",
1652
- nargs="+",
1653
- default=["8x8x256"],
1654
- help="Model specs like 8x8x256 or name=8x8x256",
1655
- )
1656
- parser.add_argument("--seeds", nargs="+", type=int, default=None)
1657
- parser.add_argument("--token-limits", nargs="+", type=int, default=[5_000_000])
1658
- parser.add_argument(
1659
- "--stream-token-caps",
1660
- nargs="+",
1661
- type=int,
1662
- default=[5_000_000, 10_000_000, 20_000_000, 40_000_000],
1663
- )
1664
- parser.add_argument("--val-tokens", type=int, default=500_000)
1665
- parser.add_argument("--allow-short-corpus", action="store_true")
1666
- parser.add_argument("--force-retokenize", action="store_true")
1667
- parser.add_argument("--vocab-size", type=int, default=4096)
1668
- parser.add_argument("--tokenizer-train-chars", type=int, default=10_000_000)
1669
- parser.add_argument("--block-size", type=int, default=128)
1670
- parser.add_argument("--batch-size", type=int, default=16)
1671
- parser.add_argument("--steps", type=int, default=2000)
1672
- parser.add_argument("--stage-steps", type=int, default=1000)
1673
- parser.add_argument("--dropout-rates", nargs="*", type=float, default=DEFAULT_DROPOUT_RATES)
1674
- parser.add_argument("--decays", nargs="*", type=parse_decay_spec, default=[])
1675
- parser.add_argument(
1676
- "--anchor-decays",
1677
- nargs="*",
1678
- type=parse_anchor_decay_spec,
1679
- default=[],
1680
- help=(
1681
- "Prefix-token anchor schedules like "
1682
- "fit:250000=0.60,500000=0.40,1000000=0.30"
1683
- ),
1684
- )
1685
- parser.add_argument("--decay-tokens", type=int, default=None)
1686
- parser.add_argument("--eval-batches", type=int, default=64)
1687
- parser.add_argument("--train-eval-batches", type=int, default=32)
1688
- parser.add_argument("--trace-eval-batches", type=int, default=8)
1689
- parser.add_argument("--eval-every", type=int, default=0)
1690
- parser.add_argument("--log-every", type=int, default=100)
1691
- parser.add_argument("--lr", type=float, default=3e-4)
1692
- parser.add_argument("--weight-decay", type=float, default=0.1)
1693
- parser.add_argument("--grad-clip", type=float, default=1.0)
1694
- parser.add_argument("--plateau-delta", type=float, default=0.01)
1695
- parser.add_argument("--target-min-dropout", type=float, default=0.10)
1696
- parser.add_argument("--min-nonzero-margin", type=float, default=0.01)
1697
- parser.add_argument("--min-high-dropout-margin", type=float, default=0.03)
1698
- parser.add_argument("--screen-early-stop", action="store_true")
1699
- parser.add_argument("--screen-prune-patience", type=int, default=3)
1700
- parser.add_argument("--screen-prune-min-delta", type=float, default=0.01)
1701
- return parser
1702
-
1703
-
1704
- def main() -> None:
1705
- args = build_parser().parse_args()
1706
- run(args)
1707
 
1708
 
1709
  if __name__ == "__main__":
 
16
  copies or substantial portions of the Software.
17
  """
18
 
19
+ from dropout_decay.experiments.runner import build_parser, main, run
20
 
21
+ __all__ = [
22
+ "build_parser",
23
+ "main",
24
+ "run",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
 
28
  if __name__ == "__main__":
src/dropout_decay/experiments/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from dropout_decay.experiments.runner import build_parser, main, run
20
+
21
+ __all__ = [
22
+ "build_parser",
23
+ "main",
24
+ "run",
25
+ ]
src/dropout_decay/experiments/artifacts.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import csv
23
+ from collections import defaultdict
24
+ import json
25
+ from pathlib import Path
26
+ import statistics
27
+
28
+ from dropout_decay.specs import DropoutCondition, ModelSpec
29
+
30
+
31
+ SUMMARY_FIELDS = [
32
+ "run_mode",
33
+ "condition",
34
+ "condition_kind",
35
+ "stage",
36
+ "token_limit",
37
+ "model_name",
38
+ "n_layer",
39
+ "n_head",
40
+ "n_embd",
41
+ "parameters",
42
+ "dropout_initial",
43
+ "dropout_final",
44
+ "dropout_schedule",
45
+ "n",
46
+ "mean_train_eval_loss",
47
+ "std_train_eval_loss",
48
+ "mean_val_eval_loss",
49
+ "std_val_eval_loss",
50
+ "mean_generalization_gap",
51
+ "std_generalization_gap",
52
+ ]
53
+ SELECTION_FIELDS = [
54
+ "run_mode",
55
+ "token_limit",
56
+ "model_name",
57
+ "n_layer",
58
+ "n_head",
59
+ "n_embd",
60
+ "parameters",
61
+ "n",
62
+ "best_dropout",
63
+ "best_val_loss",
64
+ "best_val_std",
65
+ "plateau_start_dropout",
66
+ "plateau_end_dropout",
67
+ "plateau_delta",
68
+ "zero_dropout_val_loss",
69
+ "zero_minus_best",
70
+ "best_nonzero_dropout",
71
+ "best_nonzero_val_loss",
72
+ "zero_minus_best_nonzero",
73
+ "max_dropout",
74
+ "max_dropout_val_loss",
75
+ "max_dropout_minus_best",
76
+ "has_nonzero_optimum",
77
+ "meets_target_dropout",
78
+ "curve_json",
79
+ ]
80
+
81
+
82
+ def write_jsonl_row(handle, row: dict) -> None:
83
+ handle.write(json.dumps(row, sort_keys=True) + "\n")
84
+ handle.flush()
85
+
86
+
87
+ def metric_key(row: dict) -> tuple:
88
+ return (
89
+ row["run_mode"],
90
+ row["condition"],
91
+ row["condition_kind"],
92
+ row.get("stage"),
93
+ int(row["token_limit"]),
94
+ row["model_name"],
95
+ int(row["n_layer"]),
96
+ int(row["n_head"]),
97
+ int(row["n_embd"]),
98
+ int(row["seed"]),
99
+ float(row["dropout_initial"]),
100
+ float(row["dropout_final"]),
101
+ row["dropout_schedule"],
102
+ )
103
+
104
+
105
+ def planned_metric_key(
106
+ *,
107
+ mode: str,
108
+ condition: DropoutCondition,
109
+ model_spec: ModelSpec,
110
+ seed: int,
111
+ token_limit: int,
112
+ stage: int | None = None,
113
+ ) -> tuple:
114
+ return (
115
+ mode,
116
+ condition.name,
117
+ condition.kind,
118
+ stage,
119
+ int(token_limit),
120
+ model_spec.name,
121
+ int(model_spec.n_layer),
122
+ int(model_spec.n_head),
123
+ int(model_spec.n_embd),
124
+ int(seed),
125
+ float(condition.initial),
126
+ float(condition.final),
127
+ condition.schedule,
128
+ )
129
+
130
+
131
+ def load_metrics(path: Path) -> list[dict]:
132
+ if not path.exists():
133
+ return []
134
+ rows = []
135
+ for line in path.read_text(encoding="utf-8").splitlines():
136
+ if line.strip():
137
+ rows.append(json.loads(line))
138
+ return rows
139
+
140
+
141
+ def summarize(rows: list[dict]) -> list[dict]:
142
+ groups: dict[tuple, list[dict]] = defaultdict(list)
143
+ for row in rows:
144
+ key = (
145
+ row["run_mode"],
146
+ row["condition"],
147
+ row["condition_kind"],
148
+ row["stage"],
149
+ row["token_limit"],
150
+ row["model_name"],
151
+ row["n_layer"],
152
+ row["n_head"],
153
+ row["n_embd"],
154
+ row["parameters"],
155
+ row["dropout_initial"],
156
+ row["dropout_final"],
157
+ row["dropout_schedule"],
158
+ )
159
+ groups[key].append(row)
160
+
161
+ summary: list[dict] = []
162
+ for group_rows in groups.values():
163
+ first = group_rows[0]
164
+ item = {field: first[field] for field in SUMMARY_FIELDS if field in first}
165
+ item["n"] = len(group_rows)
166
+ for source, mean_key, std_key in [
167
+ ("train_eval_loss", "mean_train_eval_loss", "std_train_eval_loss"),
168
+ ("val_eval_loss", "mean_val_eval_loss", "std_val_eval_loss"),
169
+ ("generalization_gap", "mean_generalization_gap", "std_generalization_gap"),
170
+ ]:
171
+ values = [float(row[source]) for row in group_rows]
172
+ item[mean_key] = statistics.fmean(values)
173
+ item[std_key] = statistics.stdev(values) if len(values) > 1 else 0.0
174
+ summary.append(item)
175
+ return sorted(
176
+ summary,
177
+ key=lambda row: (
178
+ row["run_mode"],
179
+ row["model_name"],
180
+ row["token_limit"],
181
+ row["condition"],
182
+ row["stage"] or -1,
183
+ ),
184
+ )
185
+
186
+
187
+ def write_csv(path: Path, rows: list[dict], fieldnames: list[str]) -> None:
188
+ with path.open("w", newline="", encoding="utf-8") as handle:
189
+ writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="ignore")
190
+ writer.writeheader()
191
+ writer.writerows(rows)
192
+
193
+
194
+ def build_model_selection(summary: list[dict], args: argparse.Namespace) -> list[dict]:
195
+ groups: dict[tuple, list[dict]] = defaultdict(list)
196
+ for row in summary:
197
+ if row["condition_kind"] == "static" and row["stage"] is None:
198
+ groups[(row["run_mode"], row["token_limit"], row["model_name"])].append(row)
199
+
200
+ selection: list[dict] = []
201
+ for (run_mode, token_limit, model_name), rows in groups.items():
202
+ curve = sorted(rows, key=lambda row: row["dropout_initial"])
203
+ best = min(curve, key=lambda row: row["mean_val_eval_loss"])
204
+ plateau = [
205
+ row for row in curve
206
+ if row["mean_val_eval_loss"] <= best["mean_val_eval_loss"] + args.plateau_delta
207
+ ]
208
+ zero = next((row for row in curve if row["dropout_initial"] == 0.0), None)
209
+ nonzero = [row for row in curve if row["dropout_initial"] > 0.0]
210
+ best_nonzero = min(nonzero, key=lambda row: row["mean_val_eval_loss"]) if nonzero else None
211
+ max_dropout = max(curve, key=lambda row: row["dropout_initial"])
212
+
213
+ zero_loss = zero["mean_val_eval_loss"] if zero else None
214
+ zero_minus_best = zero_loss - best["mean_val_eval_loss"] if zero_loss is not None else None
215
+ zero_minus_best_nonzero = (
216
+ zero_loss - best_nonzero["mean_val_eval_loss"]
217
+ if zero_loss is not None and best_nonzero is not None
218
+ else None
219
+ )
220
+ max_minus_best = max_dropout["mean_val_eval_loss"] - best["mean_val_eval_loss"]
221
+ has_nonzero_optimum = (
222
+ best["dropout_initial"] > 0.0
223
+ and zero_minus_best is not None
224
+ and zero_minus_best >= args.min_nonzero_margin
225
+ and max_minus_best >= args.min_high_dropout_margin
226
+ )
227
+ curve_json = json.dumps(
228
+ [
229
+ {
230
+ "dropout": row["dropout_initial"],
231
+ "mean_val_loss": row["mean_val_eval_loss"],
232
+ "std_val_loss": row["std_val_eval_loss"],
233
+ "mean_train_loss": row["mean_train_eval_loss"],
234
+ "mean_generalization_gap": row["mean_generalization_gap"],
235
+ "n": row["n"],
236
+ }
237
+ for row in curve
238
+ ],
239
+ sort_keys=True,
240
+ )
241
+ selection.append(
242
+ {
243
+ "run_mode": run_mode,
244
+ "token_limit": token_limit,
245
+ "model_name": model_name,
246
+ "n_layer": best["n_layer"],
247
+ "n_head": best["n_head"],
248
+ "n_embd": best["n_embd"],
249
+ "parameters": best["parameters"],
250
+ "n": best["n"],
251
+ "best_dropout": best["dropout_initial"],
252
+ "best_val_loss": best["mean_val_eval_loss"],
253
+ "best_val_std": best["std_val_eval_loss"],
254
+ "plateau_start_dropout": min(row["dropout_initial"] for row in plateau),
255
+ "plateau_end_dropout": max(row["dropout_initial"] for row in plateau),
256
+ "plateau_delta": args.plateau_delta,
257
+ "zero_dropout_val_loss": zero_loss,
258
+ "zero_minus_best": zero_minus_best,
259
+ "best_nonzero_dropout": (
260
+ best_nonzero["dropout_initial"] if best_nonzero else None
261
+ ),
262
+ "best_nonzero_val_loss": (
263
+ best_nonzero["mean_val_eval_loss"] if best_nonzero else None
264
+ ),
265
+ "zero_minus_best_nonzero": zero_minus_best_nonzero,
266
+ "max_dropout": max_dropout["dropout_initial"],
267
+ "max_dropout_val_loss": max_dropout["mean_val_eval_loss"],
268
+ "max_dropout_minus_best": max_minus_best,
269
+ "has_nonzero_optimum": has_nonzero_optimum,
270
+ "meets_target_dropout": best["dropout_initial"] >= args.target_min_dropout,
271
+ "curve_json": curve_json,
272
+ }
273
+ )
274
+ return sorted(
275
+ selection,
276
+ key=lambda row: (
277
+ not row["has_nonzero_optimum"],
278
+ not row["meets_target_dropout"],
279
+ row["best_val_loss"],
280
+ ),
281
+ )
src/dropout_decay/experiments/device.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+
23
+ import torch
24
+
25
+
26
+ def assert_mps_only() -> torch.device:
27
+ if os.environ.get("PYTORCH_ENABLE_MPS_FALLBACK") == "1":
28
+ raise SystemExit(
29
+ "Refusing to run: PYTORCH_ENABLE_MPS_FALLBACK=1 could execute CPU fallbacks."
30
+ )
31
+ if not torch.backends.mps.is_built():
32
+ raise SystemExit("PyTorch was not built with MPS. Stopping.")
33
+ if not torch.backends.mps.is_available():
34
+ raise SystemExit("MPS is not available. Stopping before any Torch experiment work.")
35
+ device = torch.device("mps")
36
+ torch.set_default_device(device)
37
+ return device
src/dropout_decay/experiments/parsing.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+
23
+ from dropout_decay.specs import DropoutCondition, ModelSpec
24
+
25
+
26
+ DEFAULT_DROPOUT_RATES = [0.0, 0.02, 0.05, 0.08, 0.10, 0.14, 0.20, 0.30, 0.50]
27
+
28
+
29
+ def clean_name(value: str) -> str:
30
+ cleaned = "".join(
31
+ c if c.isalnum() or c in {"-", "_"} else "_" for c in value.strip()
32
+ )
33
+ return cleaned.strip("_") or "model"
34
+
35
+
36
+ def rate_label(rate: float) -> str:
37
+ label = f"{rate:.3f}".rstrip("0").rstrip(".")
38
+ return label if label else "0"
39
+
40
+
41
+ def parse_model_spec(raw: str) -> ModelSpec:
42
+ if "=" in raw:
43
+ name, dims = raw.split("=", 1)
44
+ name = clean_name(name)
45
+ else:
46
+ dims = raw
47
+ name = ""
48
+ parts = dims.lower().replace(",", "x").split("x")
49
+ if len(parts) != 3:
50
+ raise argparse.ArgumentTypeError(
51
+ "model specs must look like 8x8x256 or name=8x8x256"
52
+ )
53
+ try:
54
+ n_layer, n_head, n_embd = [int(part) for part in parts]
55
+ except ValueError as exc:
56
+ raise argparse.ArgumentTypeError("model dimensions must be integers") from exc
57
+ if n_layer <= 0 or n_head <= 0 or n_embd <= 0:
58
+ raise argparse.ArgumentTypeError("model dimensions must be positive")
59
+ if n_embd % n_head != 0:
60
+ raise argparse.ArgumentTypeError("n_embd must be divisible by n_head")
61
+ if (n_embd // n_head) % 2 != 0:
62
+ raise argparse.ArgumentTypeError("n_embd / n_head must be even for rotary attention")
63
+ return ModelSpec(
64
+ name or f"L{n_layer}_H{n_head}_D{n_embd}",
65
+ n_layer,
66
+ n_head,
67
+ n_embd,
68
+ )
69
+
70
+
71
+ def parse_decay_spec(raw: str) -> DropoutCondition:
72
+ parts = raw.split(":")
73
+ if len(parts) not in {3, 4, 5}:
74
+ raise argparse.ArgumentTypeError(
75
+ "decay specs must look like "
76
+ "name:initial:final[:cosine|smoothstep|linear[:decay_tokens]]"
77
+ )
78
+ name = clean_name(parts[0])
79
+ try:
80
+ initial = float(parts[1])
81
+ final = float(parts[2])
82
+ decay_tokens = int(parts[4]) if len(parts) == 5 else None
83
+ except ValueError as exc:
84
+ raise argparse.ArgumentTypeError(
85
+ "decay dropout values and decay_tokens must be numeric"
86
+ ) from exc
87
+ schedule = parts[3] if len(parts) >= 4 else "cosine"
88
+ if schedule not in {"cosine", "smoothstep", "linear"}:
89
+ raise argparse.ArgumentTypeError(
90
+ "decay schedule must be cosine, smoothstep, or linear"
91
+ )
92
+ return DropoutCondition(
93
+ name=name,
94
+ kind="decay",
95
+ initial=initial,
96
+ final=final,
97
+ schedule=schedule,
98
+ decay_tokens=decay_tokens,
99
+ )
100
+
101
+
102
+ def parse_anchor_decay_spec(raw: str) -> DropoutCondition:
103
+ if ":" not in raw:
104
+ raise argparse.ArgumentTypeError(
105
+ "anchor decay specs must look like name:250000=0.60,500000=0.40"
106
+ )
107
+ name, raw_anchors = raw.split(":", 1)
108
+ anchors: list[tuple[int, float]] = []
109
+ for piece in raw_anchors.split(","):
110
+ if "=" not in piece:
111
+ raise argparse.ArgumentTypeError(
112
+ "anchor decay anchors must look like token_count=dropout"
113
+ )
114
+ raw_tokens, raw_dropout = piece.split("=", 1)
115
+ try:
116
+ token_count = int(raw_tokens)
117
+ dropout = float(raw_dropout)
118
+ except ValueError as exc:
119
+ raise argparse.ArgumentTypeError(
120
+ "anchor token counts must be integers and dropout values numeric"
121
+ ) from exc
122
+ if token_count <= 0:
123
+ raise argparse.ArgumentTypeError("anchor token counts must be positive")
124
+ if not 0.0 <= dropout < 1.0:
125
+ raise argparse.ArgumentTypeError("anchor dropout must satisfy 0 <= p < 1")
126
+ anchors.append((token_count, dropout))
127
+ anchors = sorted(set(anchors))
128
+ if len(anchors) < 2:
129
+ raise argparse.ArgumentTypeError("provide at least two dropout anchors")
130
+ for left, right in zip(anchors, anchors[1:]):
131
+ if right[1] > left[1]:
132
+ raise argparse.ArgumentTypeError(
133
+ "anchor dropout values must be non-increasing as token counts grow"
134
+ )
135
+ return DropoutCondition(
136
+ name=clean_name(name),
137
+ kind="anchor_decay",
138
+ initial=anchors[0][1],
139
+ final=anchors[-1][1],
140
+ schedule="log_prefix_anchor",
141
+ anchors=tuple(anchors),
142
+ )
143
+
144
+
145
+ def default_seeds(mode: str, seeds: list[int] | None) -> list[int]:
146
+ if seeds:
147
+ return seeds
148
+ return [1] if mode == "screen_static" else [1, 2, 3]
src/dropout_decay/experiments/progress.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import time
22
+
23
+
24
+ def format_duration(seconds: float) -> str:
25
+ seconds = max(0, int(seconds))
26
+ hours, remainder = divmod(seconds, 3600)
27
+ minutes, secs = divmod(remainder, 60)
28
+ if hours:
29
+ return f"{hours}h{minutes:02d}m"
30
+ if minutes:
31
+ return f"{minutes}m{secs:02d}s"
32
+ return f"{secs}s"
33
+
34
+
35
+ class ProgressMeter:
36
+ def __init__(self, total: int):
37
+ self.total = max(0, total)
38
+ self.done = 0
39
+ self.started_at = time.time()
40
+
41
+ def mark_done(self, row: dict) -> None:
42
+ self.done += 1
43
+ elapsed = time.time() - self.started_at
44
+ mean = elapsed / self.done if self.done else 0.0
45
+ remaining = max(0, self.total - self.done)
46
+ eta = remaining * mean
47
+ stage = row.get("stage")
48
+ stage_text = f"stage={stage} " if stage is not None else ""
49
+ print(
50
+ "progress "
51
+ f"{self.done}/{self.total} "
52
+ f"eta={format_duration(eta)} "
53
+ f"mode={row['run_mode']} "
54
+ f"model={row['model_name']} "
55
+ f"params={int(row['parameters']):,} "
56
+ f"prefix={int(row['token_limit']):,} "
57
+ f"{stage_text}"
58
+ f"seed={row['seed']} "
59
+ f"condition={row['condition']} "
60
+ f"dropout={float(row['dropout_active_final']):.3f} "
61
+ f"val={row['val_eval_loss']:.4f} "
62
+ f"train={row['train_eval_loss']:.4f} "
63
+ f"gap={row['generalization_gap']:.4f} "
64
+ f"elapsed={format_duration(float(row['elapsed_sec']))}",
65
+ flush=True,
66
+ )
src/dropout_decay/experiments/reports.py ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from collections import defaultdict
22
+ from pathlib import Path
23
+ import statistics
24
+
25
+
26
+ def write_screen_markdown_summary(output_dir: Path, rows: list[dict]) -> None:
27
+ if not rows:
28
+ return
29
+ static_rows = [
30
+ row
31
+ for row in rows
32
+ if row["run_mode"] == "screen_static" and row["condition_kind"] == "static"
33
+ ]
34
+ if not static_rows:
35
+ return
36
+
37
+ by_model_prefix_rate: dict[tuple[str, int, float], list[dict]] = defaultdict(list)
38
+ for row in rows:
39
+ if row["run_mode"] == "screen_static" and row["condition_kind"] == "static":
40
+ by_model_prefix_rate[
41
+ (
42
+ row["model_name"],
43
+ int(row["token_limit"]),
44
+ float(row["dropout_initial"]),
45
+ )
46
+ ].append(row)
47
+
48
+ aggregates: list[dict] = []
49
+ for (model_name, prefix, dropout), group_rows in by_model_prefix_rate.items():
50
+ first = group_rows[0]
51
+ val_losses = [float(row["val_eval_loss"]) for row in group_rows]
52
+ train_losses = [float(row["train_eval_loss"]) for row in group_rows]
53
+ gaps = [float(row["generalization_gap"]) for row in group_rows]
54
+ aggregates.append(
55
+ {
56
+ "model_name": model_name,
57
+ "token_limit": prefix,
58
+ "dropout_initial": dropout,
59
+ "n": len(group_rows),
60
+ "mean_val_eval_loss": statistics.fmean(val_losses),
61
+ "std_val_eval_loss": statistics.stdev(val_losses)
62
+ if len(val_losses) > 1
63
+ else 0.0,
64
+ "mean_train_eval_loss": statistics.fmean(train_losses),
65
+ "std_train_eval_loss": statistics.stdev(train_losses)
66
+ if len(train_losses) > 1
67
+ else 0.0,
68
+ "mean_generalization_gap": statistics.fmean(gaps),
69
+ "std_generalization_gap": statistics.stdev(gaps)
70
+ if len(gaps) > 1
71
+ else 0.0,
72
+ "parameters": int(first["parameters"]),
73
+ "n_layer": int(first["n_layer"]),
74
+ "n_head": int(first["n_head"]),
75
+ "n_embd": int(first["n_embd"]),
76
+ "block_size": int(first["model_config"]["block_size"]),
77
+ "vocab_size": int(first["model_config"]["vocab_size"]),
78
+ "tokens_seen": int(first["tokens_seen"]),
79
+ "seeds": sorted({int(row["seed"]) for row in group_rows}),
80
+ }
81
+ )
82
+
83
+ by_model: dict[str, list[dict]] = defaultdict(list)
84
+ for row in aggregates:
85
+ by_model[row["model_name"]].append(row)
86
+
87
+ model_rows = []
88
+ for model_name, model_group in by_model.items():
89
+ first = model_group[0]
90
+ seeds = sorted({seed for row in model_group for seed in row["seeds"]})
91
+ model_rows.append(
92
+ {
93
+ "model_name": model_name,
94
+ "parameters": first["parameters"],
95
+ "n_layer": first["n_layer"],
96
+ "n_head": first["n_head"],
97
+ "n_embd": first["n_embd"],
98
+ "block_size": first["block_size"],
99
+ "vocab_size": first["vocab_size"],
100
+ "seeds": seeds,
101
+ }
102
+ )
103
+
104
+ lines = [
105
+ "# Static Dropout Screen Summary",
106
+ "",
107
+ f"Run directory: `{output_dir}`",
108
+ "",
109
+ "## Models",
110
+ "",
111
+ "| Model | Params | Layers | Heads | Embedding | Block | Vocab | Seeds |",
112
+ "|---|---:|---:|---:|---:|---:|---:|---|",
113
+ ]
114
+ for model in sorted(model_rows, key=lambda item: item["parameters"]):
115
+ lines.append(
116
+ "| "
117
+ f"`{model['model_name']}` | {model['parameters']:,} | "
118
+ f"{model['n_layer']} | {model['n_head']} | {model['n_embd']} | "
119
+ f"{model['block_size']} | {model['vocab_size']} | "
120
+ f"{', '.join(str(seed) for seed in model['seeds'])} |"
121
+ )
122
+ lines.extend(
123
+ [
124
+ "",
125
+ "## Best Dropout By Model And Prefix",
126
+ "",
127
+ "| Model | Prefix tokens | Effective epochs | Best dropout | Mean val loss | Val std | Mean train loss | Mean gap | Plateau/bracket note |",
128
+ "|---|---:|---:|---:|---:|---:|---:|---:|---|",
129
+ ]
130
+ )
131
+ for model_name, model_group in sorted(by_model.items()):
132
+ by_prefix: dict[int, list[dict]] = defaultdict(list)
133
+ for row in model_group:
134
+ by_prefix[int(row["token_limit"])].append(row)
135
+ for prefix, prefix_rows in sorted(by_prefix.items()):
136
+ best = min(prefix_rows, key=lambda row: row["mean_val_eval_loss"])
137
+ rates = [float(row["dropout_initial"]) for row in prefix_rows]
138
+ eff_epochs = float(best["tokens_seen"]) / prefix
139
+ if best["dropout_initial"] == max(rates):
140
+ note = "not bracketed; best at top of tested grid"
141
+ elif best["dropout_initial"] == min(rates):
142
+ note = "not bracketed; best at bottom of tested grid"
143
+ else:
144
+ note = "bracketed by tested grid"
145
+ lines.append(
146
+ "| "
147
+ f"`{model_name}` | {prefix:,} | {eff_epochs:.2f} | "
148
+ f"{best['dropout_initial']:.2f} | "
149
+ f"{best['mean_val_eval_loss']:.4f} | "
150
+ f"{best['std_val_eval_loss']:.4f} | "
151
+ f"{best['mean_train_eval_loss']:.4f} | "
152
+ f"{best['mean_generalization_gap']:.4f} | {note} |"
153
+ )
154
+
155
+ for model_name, model_group in sorted(by_model.items()):
156
+ by_prefix = defaultdict(list)
157
+ for row in model_group:
158
+ by_prefix[int(row["token_limit"])].append(row)
159
+ lines.extend(
160
+ [
161
+ "",
162
+ f"## Model `{model_name}`",
163
+ ]
164
+ )
165
+ for prefix, prefix_rows in sorted(by_prefix.items()):
166
+ eff_epochs = float(prefix_rows[0]["tokens_seen"]) / prefix
167
+ lines.extend(
168
+ [
169
+ "",
170
+ f"### Prefix {prefix:,} Tokens ({eff_epochs:.2f} Effective Epochs)",
171
+ "",
172
+ "| Dropout | N | Mean val loss | Val std | Mean train loss | Train std | Mean gap | Gap std | Sampled tokens | Params |",
173
+ "|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
174
+ ]
175
+ )
176
+ for row in sorted(prefix_rows, key=lambda item: item["dropout_initial"]):
177
+ lines.append(
178
+ "| "
179
+ f"{row['dropout_initial']:.2f} | {row['n']} | "
180
+ f"{row['mean_val_eval_loss']:.4f} | "
181
+ f"{row['std_val_eval_loss']:.4f} | "
182
+ f"{row['mean_train_eval_loss']:.4f} | "
183
+ f"{row['std_train_eval_loss']:.4f} | "
184
+ f"{row['mean_generalization_gap']:.4f} | "
185
+ f"{row['std_generalization_gap']:.4f} | "
186
+ f"{int(row['tokens_seen']):,} | {int(row['parameters']):,} |"
187
+ )
188
+
189
+ output = "\n".join(lines) + "\n"
190
+ (output_dir / "RESULT_SUMMARY.md").write_text(output, encoding="utf-8")
191
+
192
+
193
+ def svg_escape(value: object) -> str:
194
+ return (
195
+ str(value)
196
+ .replace("&", "&amp;")
197
+ .replace("<", "&lt;")
198
+ .replace(">", "&gt;")
199
+ .replace('"', "&quot;")
200
+ )
201
+
202
+
203
+ def write_dropout_curve_svg(output_dir: Path, summary: list[dict]) -> None:
204
+ rows = [
205
+ row
206
+ for row in summary
207
+ if row["run_mode"] == "screen_static" and row["condition_kind"] == "static"
208
+ ]
209
+ if not rows:
210
+ return
211
+
212
+ grouped: dict[tuple[str, int], list[dict]] = defaultdict(list)
213
+ model_params: dict[str, int] = {}
214
+ for row in rows:
215
+ model_name = row["model_name"]
216
+ grouped[(model_name, int(row["token_limit"]))].append(row)
217
+ model_params[model_name] = int(row["parameters"])
218
+
219
+ models = sorted(model_params, key=lambda name: model_params[name])
220
+ prefixes = sorted({int(row["token_limit"]) for row in rows})
221
+ panel_w, panel_h = 230, 170
222
+ margin_l, margin_t = 58, 34
223
+ plot_w, plot_h = 142, 94
224
+ gap_x, gap_y = 18, 38
225
+ width = margin_l + len(prefixes) * panel_w + gap_x
226
+ height = 70 + len(models) * (panel_h + gap_y)
227
+ colors = ["#1f77b4", "#d62728", "#2ca02c", "#9467bd", "#ff7f0e"]
228
+
229
+ parts = [
230
+ f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
231
+ "<style>",
232
+ "text{font-family:Arial,Helvetica,sans-serif;fill:#111827}",
233
+ ".small{font-size:10px}.label{font-size:11px}.title{font-size:15px;font-weight:700}",
234
+ ".axis{stroke:#374151;stroke-width:1}.grid{stroke:#e5e7eb;stroke-width:1}.line{fill:none;stroke-width:2}",
235
+ "</style>",
236
+ '<rect width="100%" height="100%" fill="#ffffff"/>',
237
+ '<text x="24" y="28" class="title">Static dropout law: validation loss vs dropout</text>',
238
+ '<text x="24" y="48" class="label">Each panel uses its own y-scale. Points are one-seed means unless N &gt; 1.</text>',
239
+ ]
240
+
241
+ for col, prefix in enumerate(prefixes):
242
+ x = margin_l + col * panel_w + plot_w / 2
243
+ parts.append(
244
+ f'<text x="{x:.1f}" y="70" text-anchor="middle" class="label">{prefix:,} prefix tokens</text>'
245
+ )
246
+
247
+ for row_idx, model_name in enumerate(models):
248
+ row_y = 92 + row_idx * (panel_h + gap_y)
249
+ parts.append(
250
+ f'<text x="24" y="{row_y + 48}" class="label" transform="rotate(-90 24 {row_y + 48})">'
251
+ f'{svg_escape(model_name)} ({model_params[model_name] / 1_000_000:.1f}M)</text>'
252
+ )
253
+ for col, prefix in enumerate(prefixes):
254
+ panel_x = margin_l + col * panel_w
255
+ panel_y = row_y
256
+ curve = sorted(
257
+ grouped.get((model_name, prefix), []),
258
+ key=lambda item: float(item["dropout_initial"]),
259
+ )
260
+ if not curve:
261
+ continue
262
+ losses = [float(item["mean_val_eval_loss"]) for item in curve]
263
+ min_loss, max_loss = min(losses), max(losses)
264
+ pad = max(0.02, (max_loss - min_loss) * 0.08)
265
+ y_min, y_max = min_loss - pad, max_loss + pad
266
+ best = min(curve, key=lambda item: float(item["mean_val_eval_loss"]))
267
+
268
+ def px(dropout: float) -> float:
269
+ return panel_x + (dropout / 0.9) * plot_w
270
+
271
+ def py(loss: float) -> float:
272
+ scale = (loss - y_min) / (y_max - y_min)
273
+ return panel_y + plot_h - scale * plot_h
274
+
275
+ parts.extend(
276
+ [
277
+ f'<line x1="{panel_x:.1f}" y1="{panel_y:.1f}" x2="{panel_x:.1f}" y2="{panel_y + plot_h:.1f}" class="axis"/>',
278
+ f'<line x1="{panel_x:.1f}" y1="{panel_y + plot_h:.1f}" x2="{panel_x + plot_w:.1f}" y2="{panel_y + plot_h:.1f}" class="axis"/>',
279
+ f'<line x1="{panel_x:.1f}" y1="{panel_y:.1f}" x2="{panel_x + plot_w:.1f}" y2="{panel_y:.1f}" class="grid"/>',
280
+ f'<text x="{panel_x:.1f}" y="{panel_y - 6:.1f}" class="small">{y_max:.2f}</text>',
281
+ f'<text x="{panel_x:.1f}" y="{panel_y + plot_h + 13:.1f}" class="small">{y_min:.2f}</text>',
282
+ f'<text x="{panel_x:.1f}" y="{panel_y + plot_h + 28:.1f}" class="small">0</text>',
283
+ f'<text x="{panel_x + plot_w:.1f}" y="{panel_y + plot_h + 28:.1f}" text-anchor="end" class="small">0.9</text>',
284
+ ]
285
+ )
286
+ points = " ".join(
287
+ f"{px(float(item['dropout_initial'])):.1f},{py(float(item['mean_val_eval_loss'])):.1f}"
288
+ for item in curve
289
+ )
290
+ color = colors[row_idx % len(colors)]
291
+ parts.append(f'<polyline points="{points}" class="line" stroke="{color}"/>')
292
+ for item in curve:
293
+ dropout = float(item["dropout_initial"])
294
+ loss = float(item["mean_val_eval_loss"])
295
+ radius = 4 if item is best else 2.7
296
+ fill = "#111827" if item is best else "#ffffff"
297
+ parts.append(
298
+ f'<circle cx="{px(dropout):.1f}" cy="{py(loss):.1f}" r="{radius}" fill="{fill}" stroke="{color}" stroke-width="1.5"/>'
299
+ )
300
+ parts.append(
301
+ f'<text x="{panel_x + plot_w + 8:.1f}" y="{panel_y + 14:.1f}" class="small">'
302
+ f'best p={float(best["dropout_initial"]):.2f}</text>'
303
+ )
304
+ parts.append(
305
+ f'<text x="{panel_x + plot_w + 8:.1f}" y="{panel_y + 28:.1f}" class="small">'
306
+ f'loss={float(best["mean_val_eval_loss"]):.3f}</text>'
307
+ )
308
+
309
+ parts.append("</svg>")
310
+ (output_dir / "dropout_curves.svg").write_text("\n".join(parts), encoding="utf-8")
311
+
312
+
313
+ def write_stream_markdown_summary(output_dir: Path, rows: list[dict]) -> None:
314
+ stream_rows = [row for row in rows if row["run_mode"] == "locked_stream"]
315
+ if not stream_rows:
316
+ return
317
+
318
+ by_condition_stage: dict[tuple[str, int], list[dict]] = defaultdict(list)
319
+ by_condition: dict[str, list[dict]] = defaultdict(list)
320
+ for row in stream_rows:
321
+ condition = row["condition"]
322
+ by_condition_stage[(condition, int(row["stage"]))].append(row)
323
+ by_condition[condition].append(row)
324
+
325
+ first = stream_rows[0]
326
+ seeds = sorted({int(row["seed"]) for row in stream_rows})
327
+ conditions = sorted(
328
+ by_condition,
329
+ key=lambda name: (
330
+ by_condition[name][0]["condition_kind"] != "anchor_decay",
331
+ by_condition[name][0]["dropout_initial"],
332
+ name,
333
+ ),
334
+ )
335
+ stages = sorted({int(row["stage"]) for row in stream_rows})
336
+
337
+ lines = [
338
+ "# Locked Streaming Dropout Summary",
339
+ "",
340
+ f"Run directory: `{output_dir}`",
341
+ "",
342
+ (
343
+ f"Model: `{first['model_name']}` causal Transformer, "
344
+ f"{int(first['parameters']):,} parameters, {first['n_layer']} layers, "
345
+ f"{first['n_head']} heads, {first['n_embd']} embedding dim."
346
+ ),
347
+ (
348
+ f"Training per stage: {first['steps']:,} steps. "
349
+ "Sampled tokens are cumulative in each stage row. "
350
+ f"Seeds present: {', '.join(str(seed) for seed in seeds)}."
351
+ ),
352
+ "",
353
+ "## Condition Ranking",
354
+ "",
355
+ "| Condition | Kind | Final dropout | Mean trajectory val loss | Final val loss | Final gap | Dropout path |",
356
+ "|---|---|---:|---:|---:|---:|---|",
357
+ ]
358
+
359
+ ranking = []
360
+ for condition in conditions:
361
+ stage_items = []
362
+ for stage in stages:
363
+ group = by_condition_stage.get((condition, stage), [])
364
+ if not group:
365
+ continue
366
+ stage_items.append(
367
+ {
368
+ "stage": stage,
369
+ "token_limit": int(group[0]["token_limit"]),
370
+ "mean_val": statistics.fmean(
371
+ float(row["val_eval_loss"]) for row in group
372
+ ),
373
+ "mean_gap": statistics.fmean(
374
+ float(row["generalization_gap"]) for row in group
375
+ ),
376
+ "mean_dropout": statistics.fmean(
377
+ float(row["dropout_active_final"]) for row in group
378
+ ),
379
+ "kind": group[0]["condition_kind"],
380
+ }
381
+ )
382
+ if not stage_items:
383
+ continue
384
+ final = max(stage_items, key=lambda item: item["stage"])
385
+ ranking.append(
386
+ {
387
+ "condition": condition,
388
+ "kind": stage_items[0]["kind"],
389
+ "trajectory_val": statistics.fmean(item["mean_val"] for item in stage_items),
390
+ "final_val": final["mean_val"],
391
+ "final_gap": final["mean_gap"],
392
+ "final_dropout": final["mean_dropout"],
393
+ "dropout_path": " -> ".join(
394
+ f"{item['mean_dropout']:.2f}" for item in stage_items
395
+ ),
396
+ }
397
+ )
398
+
399
+ for item in sorted(ranking, key=lambda row: row["trajectory_val"]):
400
+ lines.append(
401
+ "| "
402
+ f"`{item['condition']}` | {item['kind']} | "
403
+ f"{item['final_dropout']:.2f} | {item['trajectory_val']:.4f} | "
404
+ f"{item['final_val']:.4f} | {item['final_gap']:.4f} | "
405
+ f"{item['dropout_path']} |"
406
+ )
407
+
408
+ lines.extend(["", "## Stage Trajectory", ""])
409
+ for stage in stages:
410
+ stage_groups = {
411
+ condition: by_condition_stage[(condition, stage)]
412
+ for condition in conditions
413
+ if (condition, stage) in by_condition_stage
414
+ }
415
+ if not stage_groups:
416
+ continue
417
+ prefix = int(next(iter(stage_groups.values()))[0]["token_limit"])
418
+ lines.extend(
419
+ [
420
+ f"### Stage {stage}: {prefix:,} Prefix Tokens",
421
+ "",
422
+ "| Condition | Dropout | Mean val loss | Mean train loss | Mean gap | N |",
423
+ "|---|---:|---:|---:|---:|---:|",
424
+ ]
425
+ )
426
+ for condition, group in sorted(
427
+ stage_groups.items(),
428
+ key=lambda item: statistics.fmean(
429
+ float(row["val_eval_loss"]) for row in item[1]
430
+ ),
431
+ ):
432
+ val = statistics.fmean(float(row["val_eval_loss"]) for row in group)
433
+ train = statistics.fmean(float(row["train_eval_loss"]) for row in group)
434
+ gap = statistics.fmean(float(row["generalization_gap"]) for row in group)
435
+ dropout = statistics.fmean(
436
+ float(row["dropout_active_final"]) for row in group
437
+ )
438
+ lines.append(
439
+ "| "
440
+ f"`{condition}` | {dropout:.2f} | {val:.4f} | "
441
+ f"{train:.4f} | {gap:.4f} | {len(group)} |"
442
+ )
443
+ lines.append("")
444
+
445
+ (output_dir / "RESULT_SUMMARY.md").write_text(
446
+ "\n".join(lines).rstrip() + "\n",
447
+ encoding="utf-8",
448
+ )
src/dropout_decay/experiments/runner.py ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ from datetime import datetime
23
+ import json
24
+ from pathlib import Path
25
+ import statistics
26
+ import sys
27
+
28
+ import numpy as np
29
+ import torch
30
+
31
+ from dropout_decay.datasets import (
32
+ encode_corpus,
33
+ load_cached_splits,
34
+ resolve_paths,
35
+ train_or_load_tokenizer,
36
+ )
37
+ from dropout_decay.experiments.artifacts import (
38
+ SELECTION_FIELDS,
39
+ SUMMARY_FIELDS,
40
+ build_model_selection,
41
+ load_metrics,
42
+ metric_key,
43
+ planned_metric_key,
44
+ summarize,
45
+ write_csv,
46
+ write_jsonl_row,
47
+ )
48
+ from dropout_decay.experiments.device import assert_mps_only
49
+ from dropout_decay.experiments.parsing import (
50
+ DEFAULT_DROPOUT_RATES,
51
+ default_seeds,
52
+ parse_anchor_decay_spec,
53
+ parse_decay_spec,
54
+ parse_model_spec,
55
+ rate_label,
56
+ )
57
+ from dropout_decay.experiments.progress import ProgressMeter
58
+ from dropout_decay.experiments.reports import (
59
+ write_dropout_curve_svg,
60
+ write_screen_markdown_summary,
61
+ write_stream_markdown_summary,
62
+ )
63
+ from dropout_decay.experiments.training import train_segment
64
+ from dropout_decay.license import NANOCHAT_ATTRIBUTION
65
+ from dropout_decay.specs import DropoutCondition, ModelSpec
66
+
67
+
68
+ def static_conditions(dropout_rates: list[float]) -> list[DropoutCondition]:
69
+ return [
70
+ DropoutCondition(
71
+ name=f"static_dropout_{rate_label(rate)}",
72
+ kind="static",
73
+ initial=rate,
74
+ final=rate,
75
+ )
76
+ for rate in dropout_rates
77
+ ]
78
+
79
+
80
+ def run_fixed_static_sweep(
81
+ *,
82
+ args: argparse.Namespace,
83
+ model_specs: list[ModelSpec],
84
+ seeds: list[int],
85
+ train_tokens: np.ndarray,
86
+ val_tokens: np.ndarray,
87
+ tokenizer_vocab_size: int,
88
+ token_limits: list[int],
89
+ device: torch.device,
90
+ metrics_file,
91
+ trace_file,
92
+ completed_keys: set[tuple] | None = None,
93
+ ) -> list[dict]:
94
+ rows: list[dict] = []
95
+ completed_keys = completed_keys or set()
96
+ conditions = static_conditions(sorted(set(args.dropout_rates)))
97
+ planned = 0
98
+ for token_limit in token_limits:
99
+ for model_spec in model_specs:
100
+ for condition in conditions:
101
+ for seed in seeds:
102
+ key = planned_metric_key(
103
+ mode=args.mode,
104
+ condition=condition,
105
+ model_spec=model_spec,
106
+ seed=seed,
107
+ token_limit=token_limit,
108
+ )
109
+ if key not in completed_keys:
110
+ planned += 1
111
+ progress = ProgressMeter(planned)
112
+ for token_limit in token_limits:
113
+ for model_spec in model_specs:
114
+ best_val_loss = float("inf")
115
+ worse_streak = 0
116
+ for condition in conditions:
117
+ condition_rows: list[dict] = []
118
+ for seed in seeds:
119
+ key = planned_metric_key(
120
+ mode=args.mode,
121
+ condition=condition,
122
+ model_spec=model_spec,
123
+ seed=seed,
124
+ token_limit=token_limit,
125
+ )
126
+ if key in completed_keys:
127
+ write_jsonl_row(
128
+ trace_file,
129
+ {
130
+ "event": "skipped_completed_condition",
131
+ "run_mode": args.mode,
132
+ "condition": condition.name,
133
+ "model_name": model_spec.name,
134
+ "seed": seed,
135
+ "stage": None,
136
+ "token_limit": int(token_limit),
137
+ },
138
+ )
139
+ continue
140
+ config = model_spec.config(
141
+ tokenizer_vocab_size,
142
+ args.block_size,
143
+ condition.initial,
144
+ )
145
+ model, optimizer, _, row = train_segment(
146
+ run_mode=args.mode,
147
+ condition=condition,
148
+ model_spec=model_spec,
149
+ config=config,
150
+ train_tokens=train_tokens,
151
+ val_tokens=val_tokens,
152
+ token_limit=token_limit,
153
+ steps=args.steps,
154
+ seed=seed,
155
+ args=args,
156
+ device=device,
157
+ dropout_fn=condition.make_fn(
158
+ args.steps * args.batch_size * args.block_size
159
+ ),
160
+ metrics_file=metrics_file,
161
+ trace_file=trace_file,
162
+ )
163
+ rows.append(row)
164
+ condition_rows.append(row)
165
+ completed_keys.add(metric_key(row))
166
+ progress.mark_done(row)
167
+ del model, optimizer
168
+ torch.mps.empty_cache()
169
+
170
+ if not condition_rows:
171
+ continue
172
+ mean_val_loss = statistics.fmean(
173
+ float(row["val_eval_loss"]) for row in condition_rows
174
+ )
175
+ if mean_val_loss < best_val_loss - args.screen_prune_min_delta:
176
+ best_val_loss = mean_val_loss
177
+ worse_streak = 0
178
+ elif mean_val_loss > best_val_loss + args.screen_prune_min_delta:
179
+ worse_streak += 1
180
+
181
+ if (
182
+ args.mode == "screen_static"
183
+ and args.screen_early_stop
184
+ and worse_streak >= args.screen_prune_patience
185
+ and condition.initial >= args.target_min_dropout
186
+ ):
187
+ write_jsonl_row(
188
+ trace_file,
189
+ {
190
+ "event": "screen_pruned_model",
191
+ "run_mode": args.mode,
192
+ "model_name": model_spec.name,
193
+ "token_limit": int(token_limit),
194
+ "best_val_loss": best_val_loss,
195
+ "pruned_after_dropout": condition.initial,
196
+ "worse_streak": worse_streak,
197
+ "remaining_dropouts": [
198
+ rate
199
+ for rate in args.dropout_rates
200
+ if rate > condition.initial
201
+ ],
202
+ },
203
+ )
204
+ break
205
+ return rows
206
+
207
+
208
+ def run_locked_stream(
209
+ *,
210
+ args: argparse.Namespace,
211
+ model_specs: list[ModelSpec],
212
+ seeds: list[int],
213
+ train_tokens: np.ndarray,
214
+ val_tokens: np.ndarray,
215
+ tokenizer_vocab_size: int,
216
+ stream_caps: list[int],
217
+ device: torch.device,
218
+ metrics_file,
219
+ trace_file,
220
+ completed_keys: set[tuple] | None = None,
221
+ ) -> list[dict]:
222
+ rows: list[dict] = []
223
+ completed_keys = completed_keys or set()
224
+ conditions = args.anchor_decays + static_conditions(args.dropout_rates) + args.decays
225
+ fallback_decay_tokens = (
226
+ args.decay_tokens
227
+ or args.stage_steps * args.batch_size * args.block_size * len(stream_caps)
228
+ )
229
+ planned = 0
230
+ for model_spec in model_specs:
231
+ for condition in conditions:
232
+ for seed in seeds:
233
+ for stage, token_limit in enumerate(stream_caps):
234
+ key = planned_metric_key(
235
+ mode=args.mode,
236
+ condition=condition,
237
+ model_spec=model_spec,
238
+ seed=seed,
239
+ token_limit=token_limit,
240
+ stage=stage,
241
+ )
242
+ if key not in completed_keys:
243
+ planned += 1
244
+ progress = ProgressMeter(planned)
245
+ for model_spec in model_specs:
246
+ for condition in conditions:
247
+ for seed in seeds:
248
+ model = None
249
+ optimizer = None
250
+ tokens_seen = 0
251
+ for stage, token_limit in enumerate(stream_caps):
252
+ key = planned_metric_key(
253
+ mode=args.mode,
254
+ condition=condition,
255
+ model_spec=model_spec,
256
+ seed=seed,
257
+ token_limit=token_limit,
258
+ stage=stage,
259
+ )
260
+ if key in completed_keys:
261
+ write_jsonl_row(
262
+ trace_file,
263
+ {
264
+ "event": "skipped_completed_condition",
265
+ "run_mode": args.mode,
266
+ "condition": condition.name,
267
+ "model_name": model_spec.name,
268
+ "seed": seed,
269
+ "stage": stage,
270
+ "token_limit": int(token_limit),
271
+ },
272
+ )
273
+ continue
274
+ config = model_spec.config(
275
+ tokenizer_vocab_size,
276
+ args.block_size,
277
+ condition.initial,
278
+ )
279
+ model, optimizer, tokens_seen, row = train_segment(
280
+ run_mode=args.mode,
281
+ condition=condition,
282
+ model_spec=model_spec,
283
+ config=config,
284
+ train_tokens=train_tokens,
285
+ val_tokens=val_tokens,
286
+ token_limit=token_limit,
287
+ steps=args.stage_steps,
288
+ seed=seed,
289
+ args=args,
290
+ device=device,
291
+ dropout_fn=condition.make_fn(
292
+ fallback_decay_tokens,
293
+ unique_tokens=token_limit,
294
+ ),
295
+ metrics_file=metrics_file,
296
+ trace_file=trace_file,
297
+ stage=stage,
298
+ model=model,
299
+ optimizer=optimizer,
300
+ tokens_seen_start=tokens_seen,
301
+ )
302
+ rows.append(row)
303
+ completed_keys.add(metric_key(row))
304
+ progress.mark_done(row)
305
+ del model, optimizer
306
+ torch.mps.empty_cache()
307
+ return rows
308
+
309
+
310
+ def prepare_data(args: argparse.Namespace, output_dir: Path, required_train_tokens: int):
311
+ cache_dir = Path(args.cache_dir) if args.cache_dir else output_dir / "cache"
312
+ cache_dir.mkdir(parents=True, exist_ok=True)
313
+ if args.use_cached_data:
314
+ if args.force_retokenize:
315
+ raise ValueError("--use-cached-data cannot be combined with --force-retokenize")
316
+ return load_cached_splits(
317
+ cache_dir=cache_dir,
318
+ vocab_size=args.vocab_size,
319
+ max_required_train_tokens=required_train_tokens,
320
+ val_tokens=args.val_tokens,
321
+ allow_short_corpus=args.allow_short_corpus,
322
+ )
323
+
324
+ paths = resolve_paths(args.corpus, args.corpus_glob)
325
+ tokenizer = train_or_load_tokenizer(
326
+ paths=paths,
327
+ output_dir=cache_dir,
328
+ vocab_size=args.vocab_size,
329
+ tokenizer_train_chars=args.tokenizer_train_chars,
330
+ text_column=args.text_column,
331
+ force_retrain=args.force_retokenize,
332
+ )
333
+ splits = encode_corpus(
334
+ paths=paths,
335
+ tokenizer=tokenizer,
336
+ output_dir=cache_dir,
337
+ max_required_train_tokens=required_train_tokens,
338
+ val_tokens=args.val_tokens,
339
+ text_column=args.text_column,
340
+ allow_short_corpus=args.allow_short_corpus,
341
+ force_reencode=args.force_retokenize,
342
+ )
343
+ return tokenizer, splits
344
+
345
+
346
+ def run(args: argparse.Namespace) -> Path:
347
+ device = assert_mps_only()
348
+ seeds = default_seeds(args.mode, args.seeds)
349
+ model_specs = [parse_model_spec(spec) for spec in args.models]
350
+ if args.mode != "locked_stream" and (args.decays or args.anchor_decays):
351
+ raise ValueError("--decays and --anchor-decays are only used with --mode locked_stream")
352
+ if args.resume_from and args.mode == "locked_stream":
353
+ raise ValueError("--resume-from currently supports fixed static sweeps only")
354
+
355
+ if args.resume_from:
356
+ output_dir = Path(args.resume_from)
357
+ if not output_dir.exists():
358
+ raise FileNotFoundError(f"resume directory does not exist: {output_dir}")
359
+ else:
360
+ run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
361
+ output_dir = Path(args.output_dir) / args.mode / run_id
362
+ output_dir.mkdir(parents=True, exist_ok=True)
363
+ required_train_tokens = max(
364
+ args.stream_token_caps if args.mode == "locked_stream" else args.token_limits
365
+ )
366
+ tokenizer, splits = prepare_data(args, output_dir, required_train_tokens)
367
+ token_limits = [min(limit, len(splits.train)) for limit in args.token_limits]
368
+ stream_caps = [min(limit, len(splits.train)) for limit in args.stream_token_caps]
369
+
370
+ args_payload = vars(args).copy()
371
+ args_payload["decays"] = [condition.to_dict() for condition in args.decays]
372
+ args_payload["anchor_decays"] = [
373
+ condition.to_dict() for condition in args.anchor_decays
374
+ ]
375
+ config_payload = {
376
+ "args": args_payload,
377
+ "mode": args.mode,
378
+ "seeds": seeds,
379
+ "models": [model.to_dict() for model in model_specs],
380
+ "device": str(device),
381
+ "torch": torch.__version__,
382
+ "python": sys.version,
383
+ "mps_available": torch.backends.mps.is_available(),
384
+ "attribution": NANOCHAT_ATTRIBUTION,
385
+ "tokenizer_path": str(splits.tokenizer_path),
386
+ "encoded_path": str(splits.encoded_path),
387
+ "train_tokens": int(len(splits.train)),
388
+ "val_tokens": int(len(splits.val)),
389
+ "effective_token_limits": [int(limit) for limit in token_limits],
390
+ "effective_stream_token_caps": [int(limit) for limit in stream_caps],
391
+ "resume_from": str(args.resume_from) if args.resume_from else None,
392
+ }
393
+ config_name = "config.resume.json" if args.resume_from else "config.json"
394
+ (output_dir / config_name).write_text(
395
+ json.dumps(config_payload, indent=2),
396
+ encoding="utf-8",
397
+ )
398
+
399
+ metrics_path = output_dir / "metrics.jsonl"
400
+ trace_path = output_dir / "trace.jsonl"
401
+ existing_rows = load_metrics(metrics_path) if args.resume_from else []
402
+ completed_keys = {metric_key(row) for row in existing_rows}
403
+ with (
404
+ metrics_path.open("a" if args.resume_from else "w", encoding="utf-8") as metrics_file,
405
+ trace_path.open("a" if args.resume_from else "w", encoding="utf-8") as trace_file,
406
+ ):
407
+ if args.mode in {"screen_static", "confirm_static"}:
408
+ new_rows = run_fixed_static_sweep(
409
+ args=args,
410
+ model_specs=model_specs,
411
+ seeds=seeds,
412
+ train_tokens=splits.train,
413
+ val_tokens=splits.val,
414
+ tokenizer_vocab_size=tokenizer.vocab_size,
415
+ token_limits=token_limits,
416
+ device=device,
417
+ metrics_file=metrics_file,
418
+ trace_file=trace_file,
419
+ completed_keys=completed_keys,
420
+ )
421
+ else:
422
+ new_rows = run_locked_stream(
423
+ args=args,
424
+ model_specs=model_specs,
425
+ seeds=seeds,
426
+ train_tokens=splits.train,
427
+ val_tokens=splits.val,
428
+ tokenizer_vocab_size=tokenizer.vocab_size,
429
+ stream_caps=stream_caps,
430
+ device=device,
431
+ metrics_file=metrics_file,
432
+ trace_file=trace_file,
433
+ )
434
+ rows = existing_rows + new_rows
435
+
436
+ summary = summarize(rows)
437
+ (output_dir / "summary.json").write_text(
438
+ json.dumps(summary, indent=2),
439
+ encoding="utf-8",
440
+ )
441
+ write_csv(output_dir / "summary.csv", summary, SUMMARY_FIELDS)
442
+ if args.mode in {"screen_static", "confirm_static"}:
443
+ selection = build_model_selection(summary, args)
444
+ (output_dir / "model_selection.json").write_text(
445
+ json.dumps(selection, indent=2),
446
+ encoding="utf-8",
447
+ )
448
+ write_csv(output_dir / "model_selection.csv", selection, SELECTION_FIELDS)
449
+ write_screen_markdown_summary(output_dir, rows)
450
+ write_dropout_curve_svg(output_dir, summary)
451
+ elif args.mode == "locked_stream":
452
+ write_stream_markdown_summary(output_dir, rows)
453
+
454
+ print(
455
+ json.dumps(
456
+ {
457
+ "output_dir": str(output_dir),
458
+ "new_rows": len(new_rows),
459
+ "total_metric_rows": len(rows),
460
+ "summary_rows": len(summary),
461
+ },
462
+ indent=2,
463
+ )
464
+ )
465
+ return output_dir
466
+
467
+
468
+ def build_parser() -> argparse.ArgumentParser:
469
+ parser = argparse.ArgumentParser(
470
+ description="MPS-only dropout/model selection experiments"
471
+ )
472
+ parser.add_argument(
473
+ "--mode",
474
+ choices=["screen_static", "confirm_static", "locked_stream"],
475
+ default="screen_static",
476
+ )
477
+ parser.add_argument("--corpus", default=None, help="Text or parquet corpus path")
478
+ parser.add_argument("--corpus-glob", default=None, help="Glob of text/parquet corpus paths")
479
+ parser.add_argument("--text-column", default="text", help="Parquet text column")
480
+ parser.add_argument(
481
+ "--use-cached-data",
482
+ action="store_true",
483
+ help=(
484
+ "Load tokenizer-v{vocab}.json and tokens-v{vocab}-*.npy from --cache-dir "
485
+ "instead of requiring the original text/parquet corpus."
486
+ ),
487
+ )
488
+ parser.add_argument("--output-dir", default="runs")
489
+ parser.add_argument(
490
+ "--resume-from",
491
+ default=None,
492
+ help="Existing fixed-static run directory; completed metric rows are skipped",
493
+ )
494
+ parser.add_argument("--cache-dir", default=".cache/dropout_decay")
495
+ parser.add_argument(
496
+ "--models",
497
+ nargs="+",
498
+ default=["8x8x256"],
499
+ help="Model specs like 8x8x256 or name=8x8x256",
500
+ )
501
+ parser.add_argument("--seeds", nargs="+", type=int, default=None)
502
+ parser.add_argument("--token-limits", nargs="+", type=int, default=[5_000_000])
503
+ parser.add_argument(
504
+ "--stream-token-caps",
505
+ nargs="+",
506
+ type=int,
507
+ default=[5_000_000, 10_000_000, 20_000_000, 40_000_000],
508
+ )
509
+ parser.add_argument("--val-tokens", type=int, default=500_000)
510
+ parser.add_argument("--allow-short-corpus", action="store_true")
511
+ parser.add_argument("--force-retokenize", action="store_true")
512
+ parser.add_argument("--vocab-size", type=int, default=4096)
513
+ parser.add_argument("--tokenizer-train-chars", type=int, default=10_000_000)
514
+ parser.add_argument("--block-size", type=int, default=128)
515
+ parser.add_argument("--batch-size", type=int, default=16)
516
+ parser.add_argument("--steps", type=int, default=2000)
517
+ parser.add_argument("--stage-steps", type=int, default=1000)
518
+ parser.add_argument("--dropout-rates", nargs="*", type=float, default=DEFAULT_DROPOUT_RATES)
519
+ parser.add_argument("--decays", nargs="*", type=parse_decay_spec, default=[])
520
+ parser.add_argument(
521
+ "--anchor-decays",
522
+ nargs="*",
523
+ type=parse_anchor_decay_spec,
524
+ default=[],
525
+ help=(
526
+ "Prefix-token anchor schedules like "
527
+ "fit:250000=0.60,500000=0.40,1000000=0.30"
528
+ ),
529
+ )
530
+ parser.add_argument("--decay-tokens", type=int, default=None)
531
+ parser.add_argument("--eval-batches", type=int, default=64)
532
+ parser.add_argument("--train-eval-batches", type=int, default=32)
533
+ parser.add_argument("--trace-eval-batches", type=int, default=8)
534
+ parser.add_argument("--eval-every", type=int, default=0)
535
+ parser.add_argument("--log-every", type=int, default=100)
536
+ parser.add_argument("--lr", type=float, default=3e-4)
537
+ parser.add_argument("--weight-decay", type=float, default=0.1)
538
+ parser.add_argument("--grad-clip", type=float, default=1.0)
539
+ parser.add_argument("--plateau-delta", type=float, default=0.01)
540
+ parser.add_argument("--target-min-dropout", type=float, default=0.10)
541
+ parser.add_argument("--min-nonzero-margin", type=float, default=0.01)
542
+ parser.add_argument("--min-high-dropout-margin", type=float, default=0.03)
543
+ parser.add_argument("--screen-early-stop", action="store_true")
544
+ parser.add_argument("--screen-prune-patience", type=int, default=3)
545
+ parser.add_argument("--screen-prune-min-delta", type=float, default=0.01)
546
+ return parser
547
+
548
+
549
+ def main() -> None:
550
+ args = build_parser().parse_args()
551
+ run(args)
552
+
553
+
554
+ if __name__ == "__main__":
555
+ main()
src/dropout_decay/experiments/training.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import random
23
+ import statistics
24
+ import time
25
+ from typing import Callable
26
+
27
+ import numpy as np
28
+ import torch
29
+
30
+ from dropout_decay.experiments.artifacts import write_jsonl_row
31
+ from dropout_decay.models import DropoutGPT, GPTConfig
32
+ from dropout_decay.specs import DropoutCondition, ModelSpec
33
+
34
+
35
+ def set_seed(seed: int) -> None:
36
+ random.seed(seed)
37
+ np.random.seed(seed)
38
+ torch.manual_seed(seed)
39
+
40
+
41
+ def batch_seed(
42
+ seed: int, model: ModelSpec, dropout_code: int, stage: int | None
43
+ ) -> int:
44
+ return (
45
+ seed * 1_000_003
46
+ + model.n_layer * 100_003
47
+ + model.n_head * 10_007
48
+ + model.n_embd * 101
49
+ + dropout_code * 37
50
+ + (stage or 0) * 997
51
+ )
52
+
53
+
54
+ def make_batch(
55
+ tokens: np.ndarray,
56
+ token_limit: int,
57
+ batch_size: int,
58
+ block_size: int,
59
+ rng: np.random.Generator,
60
+ device: torch.device,
61
+ ) -> tuple[torch.Tensor, torch.Tensor]:
62
+ limit = min(token_limit, len(tokens))
63
+ max_start = limit - block_size - 1
64
+ if max_start <= 0:
65
+ raise ValueError("token_limit is too small for the requested block_size")
66
+ starts = rng.integers(0, max_start, size=batch_size)
67
+ x_np = np.stack([tokens[start : start + block_size] for start in starts]).astype(
68
+ np.int64
69
+ )
70
+ y_np = np.stack(
71
+ [tokens[start + 1 : start + 1 + block_size] for start in starts]
72
+ ).astype(np.int64)
73
+ return (
74
+ torch.tensor(x_np, dtype=torch.long, device=device),
75
+ torch.tensor(y_np, dtype=torch.long, device=device),
76
+ )
77
+
78
+
79
+ @torch.no_grad()
80
+ def estimate_loss(
81
+ model: DropoutGPT,
82
+ tokens: np.ndarray,
83
+ token_limit: int,
84
+ batches: int,
85
+ args: argparse.Namespace,
86
+ device: torch.device,
87
+ rng_seed: int,
88
+ ) -> float:
89
+ if batches <= 0:
90
+ return float("nan")
91
+ model.eval()
92
+ rng = np.random.default_rng(rng_seed)
93
+ losses: list[float] = []
94
+ for _ in range(batches):
95
+ x, y = make_batch(
96
+ tokens, token_limit, args.batch_size, args.block_size, rng, device
97
+ )
98
+ _, loss = model(x, y)
99
+ losses.append(float(loss.item()))
100
+ model.train()
101
+ return float(statistics.fmean(losses))
102
+
103
+
104
+ def train_segment(
105
+ *,
106
+ run_mode: str,
107
+ condition: DropoutCondition,
108
+ model_spec: ModelSpec,
109
+ config: GPTConfig,
110
+ train_tokens: np.ndarray,
111
+ val_tokens: np.ndarray,
112
+ token_limit: int,
113
+ steps: int,
114
+ seed: int,
115
+ args: argparse.Namespace,
116
+ device: torch.device,
117
+ dropout_fn: Callable[[int], float],
118
+ metrics_file,
119
+ trace_file,
120
+ stage: int | None = None,
121
+ model: DropoutGPT | None = None,
122
+ optimizer: torch.optim.Optimizer | None = None,
123
+ tokens_seen_start: int = 0,
124
+ ) -> tuple[DropoutGPT, torch.optim.Optimizer, int, dict]:
125
+ if model is None:
126
+ set_seed(seed)
127
+ model = DropoutGPT(config).to(device)
128
+ optimizer = torch.optim.AdamW(
129
+ model.parameters(),
130
+ lr=args.lr,
131
+ betas=(0.9, 0.95),
132
+ weight_decay=args.weight_decay,
133
+ )
134
+ else:
135
+ torch.manual_seed(seed + 10_000 + (stage or 0))
136
+ if optimizer is None:
137
+ raise ValueError("optimizer is required when reusing a model")
138
+
139
+ model.train()
140
+ dropout_code = int(round(condition.initial * 10_000))
141
+ rng = np.random.default_rng(batch_seed(seed, model_spec, dropout_code, stage))
142
+ tokens_seen = tokens_seen_start
143
+ last_loss = float("nan")
144
+ active_dropout = condition.initial
145
+ t0 = time.time()
146
+
147
+ for step in range(1, steps + 1):
148
+ active_dropout = dropout_fn(tokens_seen)
149
+ model.set_dropout(active_dropout)
150
+ x, y = make_batch(
151
+ train_tokens, token_limit, args.batch_size, args.block_size, rng, device
152
+ )
153
+ _, loss = model(x, y)
154
+ optimizer.zero_grad(set_to_none=True)
155
+ loss.backward()
156
+ if args.grad_clip > 0:
157
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
158
+ optimizer.step()
159
+
160
+ tokens_seen += args.batch_size * args.block_size
161
+ last_loss = float(loss.item())
162
+ if args.log_every > 0 and step % args.log_every == 0:
163
+ write_jsonl_row(
164
+ trace_file,
165
+ {
166
+ "event": "train_step",
167
+ "run_mode": run_mode,
168
+ "condition": condition.name,
169
+ "model_name": model_spec.name,
170
+ "seed": seed,
171
+ "stage": stage,
172
+ "step": step,
173
+ "steps": steps,
174
+ "token_limit": int(token_limit),
175
+ "tokens_seen": int(tokens_seen),
176
+ "dropout": float(active_dropout),
177
+ "train_batch_loss": last_loss,
178
+ },
179
+ )
180
+ if args.eval_every > 0 and step % args.eval_every == 0:
181
+ train_eval = estimate_loss(
182
+ model,
183
+ train_tokens,
184
+ token_limit,
185
+ args.trace_eval_batches,
186
+ args,
187
+ device,
188
+ rng_seed=seed + 20_000 + step,
189
+ )
190
+ val_eval = estimate_loss(
191
+ model,
192
+ val_tokens,
193
+ len(val_tokens),
194
+ args.trace_eval_batches,
195
+ args,
196
+ device,
197
+ rng_seed=seed + 30_000 + step,
198
+ )
199
+ write_jsonl_row(
200
+ trace_file,
201
+ {
202
+ "event": "eval_step",
203
+ "run_mode": run_mode,
204
+ "condition": condition.name,
205
+ "model_name": model_spec.name,
206
+ "seed": seed,
207
+ "stage": stage,
208
+ "step": step,
209
+ "steps": steps,
210
+ "token_limit": int(token_limit),
211
+ "tokens_seen": int(tokens_seen),
212
+ "dropout": float(active_dropout),
213
+ "train_eval_loss": train_eval,
214
+ "val_eval_loss": val_eval,
215
+ "generalization_gap": val_eval - train_eval,
216
+ },
217
+ )
218
+
219
+ train_eval = estimate_loss(
220
+ model,
221
+ train_tokens,
222
+ token_limit,
223
+ args.train_eval_batches,
224
+ args,
225
+ device,
226
+ rng_seed=seed + 40_000 + (stage or 0),
227
+ )
228
+ val_eval = estimate_loss(
229
+ model,
230
+ val_tokens,
231
+ len(val_tokens),
232
+ args.eval_batches,
233
+ args,
234
+ device,
235
+ rng_seed=seed + 50_000 + (stage or 0),
236
+ )
237
+ row = {
238
+ "run_mode": run_mode,
239
+ "condition": condition.name,
240
+ "condition_kind": condition.kind,
241
+ "seed": seed,
242
+ "stage": stage,
243
+ "token_limit": int(token_limit),
244
+ "steps": int(steps),
245
+ "tokens_seen": int(tokens_seen),
246
+ "dropout_initial": float(condition.initial),
247
+ "dropout_final": float(condition.final),
248
+ "dropout_schedule": condition.schedule,
249
+ "dropout_active_final": float(active_dropout),
250
+ "train_loss_last": last_loss,
251
+ "train_eval_loss": train_eval,
252
+ "val_eval_loss": val_eval,
253
+ "eval_loss": val_eval,
254
+ "generalization_gap": val_eval - train_eval,
255
+ "elapsed_sec": time.time() - t0,
256
+ "parameters": model.num_parameters(),
257
+ "model_config": config.to_dict(),
258
+ **model_spec.to_dict(),
259
+ }
260
+ write_jsonl_row(metrics_file, row)
261
+ return model, optimizer, tokens_seen, row
src/dropout_decay/model.py CHANGED
@@ -16,212 +16,24 @@ The above copyright notice and this permission notice shall be included in all
16
  copies or substantial portions of the Software.
17
  """
18
 
19
- from __future__ import annotations
20
-
21
- from dataclasses import asdict, dataclass
22
- import math
23
-
24
- import torch
25
- import torch.nn as nn
26
- import torch.nn.functional as F
27
-
28
-
29
- @dataclass(frozen=True)
30
- class GPTConfig:
31
- block_size: int
32
- vocab_size: int
33
- n_layer: int
34
- n_head: int
35
- n_embd: int
36
- dropout: float
37
-
38
- def to_dict(self) -> dict[str, int | float]:
39
- return asdict(self)
40
-
41
-
42
- def rms_norm(x: torch.Tensor) -> torch.Tensor:
43
- return F.rms_norm(x, (x.size(-1),))
44
-
45
-
46
- class Linear(nn.Linear):
47
- """Bias-free linear layer matching the nanochat architectural style."""
48
-
49
- def __init__(self, in_features: int, out_features: int):
50
- super().__init__(in_features, out_features, bias=False)
51
-
52
-
53
- def apply_rotary_emb(
54
- x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
55
- ) -> torch.Tensor:
56
- half = x.shape[-1] // 2
57
- x1, x2 = x[..., :half], x[..., half:]
58
- return torch.cat((x1 * cos + x2 * sin, x1 * (-sin) + x2 * cos), dim=-1)
59
-
60
-
61
- class CausalSelfAttention(nn.Module):
62
- def __init__(self, config: GPTConfig):
63
- super().__init__()
64
- if config.n_embd % config.n_head != 0:
65
- raise ValueError("n_embd must be divisible by n_head")
66
- self.n_head = config.n_head
67
- self.head_dim = config.n_embd // config.n_head
68
- if self.head_dim % 2 != 0:
69
- raise ValueError("rotary attention requires an even head dimension")
70
- self.dropout_p = config.dropout
71
- self.c_q = Linear(config.n_embd, config.n_embd)
72
- self.c_k = Linear(config.n_embd, config.n_embd)
73
- self.c_v = Linear(config.n_embd, config.n_embd)
74
- self.c_proj = Linear(config.n_embd, config.n_embd)
75
- self.resid_dropout = nn.Dropout(config.dropout)
76
- mask = torch.ones(config.block_size, config.block_size, dtype=torch.bool).tril()
77
- self.register_buffer(
78
- "causal_mask",
79
- mask.view(1, 1, config.block_size, config.block_size),
80
- persistent=False,
81
- )
82
-
83
- def set_dropout(self, p: float) -> None:
84
- self.dropout_p = p
85
- self.resid_dropout.p = p
86
-
87
- def forward(
88
- self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
89
- ) -> torch.Tensor:
90
- batch, seq_len, channels = x.shape
91
- q = self.c_q(x).view(batch, seq_len, self.n_head, self.head_dim).transpose(1, 2)
92
- k = self.c_k(x).view(batch, seq_len, self.n_head, self.head_dim).transpose(1, 2)
93
- v = self.c_v(x).view(batch, seq_len, self.n_head, self.head_dim).transpose(1, 2)
94
-
95
- q = rms_norm(apply_rotary_emb(q, cos, sin)) * 1.2
96
- k = rms_norm(apply_rotary_emb(k, cos, sin)) * 1.2
97
-
98
- att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
99
- mask = self.causal_mask[:, :, :seq_len, :seq_len]
100
- att = att.masked_fill(~mask, float("-inf"))
101
- att = F.softmax(att.float(), dim=-1).to(dtype=x.dtype)
102
- att = F.dropout(att, p=self.dropout_p, training=self.training)
103
- y = att @ v
104
- y = y.transpose(1, 2).contiguous().view(batch, seq_len, channels)
105
- return self.resid_dropout(self.c_proj(y))
106
-
107
-
108
- class MLP(nn.Module):
109
- def __init__(self, config: GPTConfig):
110
- super().__init__()
111
- self.c_fc = Linear(config.n_embd, 4 * config.n_embd)
112
- self.c_proj = Linear(4 * config.n_embd, config.n_embd)
113
- self.dropout = nn.Dropout(config.dropout)
114
-
115
- def set_dropout(self, p: float) -> None:
116
- self.dropout.p = p
117
-
118
- def forward(self, x: torch.Tensor) -> torch.Tensor:
119
- x = self.c_fc(x)
120
- x = F.relu(x).square()
121
- x = self.c_proj(x)
122
- return self.dropout(x)
123
-
124
-
125
- class Block(nn.Module):
126
- def __init__(self, config: GPTConfig):
127
- super().__init__()
128
- self.attn = CausalSelfAttention(config)
129
- self.mlp = MLP(config)
130
-
131
- def set_dropout(self, p: float) -> None:
132
- self.attn.set_dropout(p)
133
- self.mlp.set_dropout(p)
134
-
135
- def forward(
136
- self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
137
- ) -> torch.Tensor:
138
- x = x + self.attn(rms_norm(x), cos, sin)
139
- x = x + self.mlp(rms_norm(x))
140
- return x
141
-
142
-
143
- class DropoutGPT(nn.Module):
144
- """Minimal nanochat-style causal Transformer with dynamic dropout control."""
145
-
146
- def __init__(self, config: GPTConfig):
147
- super().__init__()
148
- self.config = config
149
- self.token_embedding = nn.Embedding(config.vocab_size, config.n_embd)
150
- self.embedding_dropout = nn.Dropout(config.dropout)
151
- self.blocks = nn.ModuleList(Block(config) for _ in range(config.n_layer))
152
- self.lm_head = Linear(config.n_embd, config.vocab_size)
153
- self.register_buffer(
154
- "cos",
155
- torch.empty(1, 1, config.block_size, config.n_embd // config.n_head // 2),
156
- persistent=False,
157
- )
158
- self.register_buffer(
159
- "sin",
160
- torch.empty(1, 1, config.block_size, config.n_embd // config.n_head // 2),
161
- persistent=False,
162
- )
163
- self._init_weights()
164
- self._init_rotary()
165
-
166
- @torch.no_grad()
167
- def _init_weights(self) -> None:
168
- nn.init.normal_(self.token_embedding.weight, mean=0.0, std=0.8)
169
- nn.init.normal_(self.lm_head.weight, mean=0.0, std=0.001)
170
- scale = (3.0 ** 0.5) * (self.config.n_embd ** -0.5)
171
- for block in self.blocks:
172
- nn.init.uniform_(block.attn.c_q.weight, -scale, scale)
173
- nn.init.uniform_(block.attn.c_k.weight, -scale, scale)
174
- nn.init.uniform_(block.attn.c_v.weight, -scale, scale)
175
- nn.init.zeros_(block.attn.c_proj.weight)
176
- nn.init.uniform_(block.mlp.c_fc.weight, -0.4 * scale, 0.4 * scale)
177
- nn.init.zeros_(block.mlp.c_proj.weight)
178
-
179
- @torch.no_grad()
180
- def _init_rotary(self) -> None:
181
- head_dim = self.config.n_embd // self.config.n_head
182
- channel_range = torch.arange(
183
- 0,
184
- head_dim,
185
- 2,
186
- dtype=torch.float32,
187
- device=self.cos.device,
188
- )
189
- inv_freq = 1.0 / (100000 ** (channel_range / head_dim))
190
- positions = torch.arange(
191
- self.config.block_size,
192
- dtype=torch.float32,
193
- device=self.cos.device,
194
- )
195
- freqs = torch.outer(positions, inv_freq)
196
- self.cos.copy_(freqs.cos().view(1, 1, self.config.block_size, head_dim // 2))
197
- self.sin.copy_(freqs.sin().view(1, 1, self.config.block_size, head_dim // 2))
198
-
199
- def set_dropout(self, p: float) -> None:
200
- if not 0.0 <= p < 1.0:
201
- raise ValueError("dropout must satisfy 0 <= p < 1")
202
- self.embedding_dropout.p = p
203
- for block in self.blocks:
204
- block.set_dropout(p)
205
-
206
- def num_parameters(self) -> int:
207
- return sum(p.numel() for p in self.parameters())
208
-
209
- def forward(
210
- self, idx: torch.Tensor, targets: torch.Tensor | None = None
211
- ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
212
- _, seq_len = idx.shape
213
- if seq_len > self.config.block_size:
214
- raise ValueError("sequence length exceeds block_size")
215
- cos = self.cos[:, :, :seq_len, :]
216
- sin = self.sin[:, :, :seq_len, :]
217
- x = self.embedding_dropout(rms_norm(self.token_embedding(idx)))
218
- for block in self.blocks:
219
- x = block(x, cos, sin)
220
- logits = self.lm_head(rms_norm(x)).float()
221
- if targets is None:
222
- return logits
223
- loss = F.cross_entropy(
224
- logits.reshape(-1, logits.size(-1)),
225
- targets.reshape(-1),
226
- )
227
- return logits, loss
 
16
  copies or substantial portions of the Software.
17
  """
18
 
19
+ from dropout_decay.models.gpt import (
20
+ Block,
21
+ CausalSelfAttention,
22
+ DropoutGPT,
23
+ GPTConfig,
24
+ Linear,
25
+ MLP,
26
+ apply_rotary_emb,
27
+ rms_norm,
28
+ )
29
+
30
+ __all__ = [
31
+ "Block",
32
+ "CausalSelfAttention",
33
+ "DropoutGPT",
34
+ "GPTConfig",
35
+ "Linear",
36
+ "MLP",
37
+ "apply_rotary_emb",
38
+ "rms_norm",
39
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/dropout_decay/models/__init__.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from dropout_decay.models.gpt import (
20
+ Block,
21
+ CausalSelfAttention,
22
+ DropoutGPT,
23
+ GPTConfig,
24
+ Linear,
25
+ MLP,
26
+ apply_rotary_emb,
27
+ rms_norm,
28
+ )
29
+
30
+ __all__ = [
31
+ "Block",
32
+ "CausalSelfAttention",
33
+ "DropoutGPT",
34
+ "GPTConfig",
35
+ "Linear",
36
+ "MLP",
37
+ "apply_rotary_emb",
38
+ "rms_norm",
39
+ ]
src/dropout_decay/models/gpt.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import asdict, dataclass
22
+ import math
23
+
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class GPTConfig:
31
+ block_size: int
32
+ vocab_size: int
33
+ n_layer: int
34
+ n_head: int
35
+ n_embd: int
36
+ dropout: float
37
+
38
+ def to_dict(self) -> dict[str, int | float]:
39
+ return asdict(self)
40
+
41
+
42
+ def rms_norm(x: torch.Tensor) -> torch.Tensor:
43
+ return F.rms_norm(x, (x.size(-1),))
44
+
45
+
46
+ class Linear(nn.Linear):
47
+ """Bias-free linear layer matching the nanochat architectural style."""
48
+
49
+ def __init__(self, in_features: int, out_features: int):
50
+ super().__init__(in_features, out_features, bias=False)
51
+
52
+
53
+ def apply_rotary_emb(
54
+ x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
55
+ ) -> torch.Tensor:
56
+ half = x.shape[-1] // 2
57
+ x1, x2 = x[..., :half], x[..., half:]
58
+ return torch.cat((x1 * cos + x2 * sin, x1 * (-sin) + x2 * cos), dim=-1)
59
+
60
+
61
+ class CausalSelfAttention(nn.Module):
62
+ def __init__(self, config: GPTConfig):
63
+ super().__init__()
64
+ if config.n_embd % config.n_head != 0:
65
+ raise ValueError("n_embd must be divisible by n_head")
66
+ self.n_head = config.n_head
67
+ self.head_dim = config.n_embd // config.n_head
68
+ if self.head_dim % 2 != 0:
69
+ raise ValueError("rotary attention requires an even head dimension")
70
+ self.dropout_p = config.dropout
71
+ self.c_q = Linear(config.n_embd, config.n_embd)
72
+ self.c_k = Linear(config.n_embd, config.n_embd)
73
+ self.c_v = Linear(config.n_embd, config.n_embd)
74
+ self.c_proj = Linear(config.n_embd, config.n_embd)
75
+ self.resid_dropout = nn.Dropout(config.dropout)
76
+ mask = torch.ones(config.block_size, config.block_size, dtype=torch.bool).tril()
77
+ self.register_buffer(
78
+ "causal_mask",
79
+ mask.view(1, 1, config.block_size, config.block_size),
80
+ persistent=False,
81
+ )
82
+
83
+ def set_dropout(self, p: float) -> None:
84
+ self.dropout_p = p
85
+ self.resid_dropout.p = p
86
+
87
+ def forward(
88
+ self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
89
+ ) -> torch.Tensor:
90
+ batch, seq_len, channels = x.shape
91
+ q = self.c_q(x).view(batch, seq_len, self.n_head, self.head_dim).transpose(1, 2)
92
+ k = self.c_k(x).view(batch, seq_len, self.n_head, self.head_dim).transpose(1, 2)
93
+ v = self.c_v(x).view(batch, seq_len, self.n_head, self.head_dim).transpose(1, 2)
94
+
95
+ q = rms_norm(apply_rotary_emb(q, cos, sin)) * 1.2
96
+ k = rms_norm(apply_rotary_emb(k, cos, sin)) * 1.2
97
+
98
+ att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
99
+ mask = self.causal_mask[:, :, :seq_len, :seq_len]
100
+ att = att.masked_fill(~mask, float("-inf"))
101
+ att = F.softmax(att.float(), dim=-1).to(dtype=x.dtype)
102
+ att = F.dropout(att, p=self.dropout_p, training=self.training)
103
+ y = att @ v
104
+ y = y.transpose(1, 2).contiguous().view(batch, seq_len, channels)
105
+ return self.resid_dropout(self.c_proj(y))
106
+
107
+
108
+ class MLP(nn.Module):
109
+ def __init__(self, config: GPTConfig):
110
+ super().__init__()
111
+ self.c_fc = Linear(config.n_embd, 4 * config.n_embd)
112
+ self.c_proj = Linear(4 * config.n_embd, config.n_embd)
113
+ self.dropout = nn.Dropout(config.dropout)
114
+
115
+ def set_dropout(self, p: float) -> None:
116
+ self.dropout.p = p
117
+
118
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
119
+ x = self.c_fc(x)
120
+ x = F.relu(x).square()
121
+ x = self.c_proj(x)
122
+ return self.dropout(x)
123
+
124
+
125
+ class Block(nn.Module):
126
+ def __init__(self, config: GPTConfig):
127
+ super().__init__()
128
+ self.attn = CausalSelfAttention(config)
129
+ self.mlp = MLP(config)
130
+
131
+ def set_dropout(self, p: float) -> None:
132
+ self.attn.set_dropout(p)
133
+ self.mlp.set_dropout(p)
134
+
135
+ def forward(
136
+ self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
137
+ ) -> torch.Tensor:
138
+ x = x + self.attn(rms_norm(x), cos, sin)
139
+ x = x + self.mlp(rms_norm(x))
140
+ return x
141
+
142
+
143
+ class DropoutGPT(nn.Module):
144
+ """Minimal nanochat-style causal Transformer with dynamic dropout control."""
145
+
146
+ def __init__(self, config: GPTConfig):
147
+ super().__init__()
148
+ self.config = config
149
+ self.token_embedding = nn.Embedding(config.vocab_size, config.n_embd)
150
+ self.embedding_dropout = nn.Dropout(config.dropout)
151
+ self.blocks = nn.ModuleList(Block(config) for _ in range(config.n_layer))
152
+ self.lm_head = Linear(config.n_embd, config.vocab_size)
153
+ self.register_buffer(
154
+ "cos",
155
+ torch.empty(1, 1, config.block_size, config.n_embd // config.n_head // 2),
156
+ persistent=False,
157
+ )
158
+ self.register_buffer(
159
+ "sin",
160
+ torch.empty(1, 1, config.block_size, config.n_embd // config.n_head // 2),
161
+ persistent=False,
162
+ )
163
+ self._init_weights()
164
+ self._init_rotary()
165
+
166
+ @torch.no_grad()
167
+ def _init_weights(self) -> None:
168
+ nn.init.normal_(self.token_embedding.weight, mean=0.0, std=0.8)
169
+ nn.init.normal_(self.lm_head.weight, mean=0.0, std=0.001)
170
+ scale = (3.0 ** 0.5) * (self.config.n_embd ** -0.5)
171
+ for block in self.blocks:
172
+ nn.init.uniform_(block.attn.c_q.weight, -scale, scale)
173
+ nn.init.uniform_(block.attn.c_k.weight, -scale, scale)
174
+ nn.init.uniform_(block.attn.c_v.weight, -scale, scale)
175
+ nn.init.zeros_(block.attn.c_proj.weight)
176
+ nn.init.uniform_(block.mlp.c_fc.weight, -0.4 * scale, 0.4 * scale)
177
+ nn.init.zeros_(block.mlp.c_proj.weight)
178
+
179
+ @torch.no_grad()
180
+ def _init_rotary(self) -> None:
181
+ head_dim = self.config.n_embd // self.config.n_head
182
+ channel_range = torch.arange(
183
+ 0,
184
+ head_dim,
185
+ 2,
186
+ dtype=torch.float32,
187
+ device=self.cos.device,
188
+ )
189
+ inv_freq = 1.0 / (100000 ** (channel_range / head_dim))
190
+ positions = torch.arange(
191
+ self.config.block_size,
192
+ dtype=torch.float32,
193
+ device=self.cos.device,
194
+ )
195
+ freqs = torch.outer(positions, inv_freq)
196
+ self.cos.copy_(freqs.cos().view(1, 1, self.config.block_size, head_dim // 2))
197
+ self.sin.copy_(freqs.sin().view(1, 1, self.config.block_size, head_dim // 2))
198
+
199
+ def set_dropout(self, p: float) -> None:
200
+ if not 0.0 <= p < 1.0:
201
+ raise ValueError("dropout must satisfy 0 <= p < 1")
202
+ self.embedding_dropout.p = p
203
+ for block in self.blocks:
204
+ block.set_dropout(p)
205
+
206
+ def num_parameters(self) -> int:
207
+ return sum(p.numel() for p in self.parameters())
208
+
209
+ def forward(
210
+ self, idx: torch.Tensor, targets: torch.Tensor | None = None
211
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
212
+ _, seq_len = idx.shape
213
+ if seq_len > self.config.block_size:
214
+ raise ValueError("sequence length exceeds block_size")
215
+ cos = self.cos[:, :, :seq_len, :]
216
+ sin = self.sin[:, :, :seq_len, :]
217
+ x = self.embedding_dropout(rms_norm(self.token_embedding(idx)))
218
+ for block in self.blocks:
219
+ x = block(x, cos, sin)
220
+ logits = self.lm_head(rms_norm(x)).float()
221
+ if targets is None:
222
+ return logits
223
+ loss = F.cross_entropy(
224
+ logits.reshape(-1, logits.size(-1)),
225
+ targets.reshape(-1),
226
+ )
227
+ return logits, loss
src/dropout_decay/scheduler.py CHANGED
@@ -16,41 +16,9 @@ The above copyright notice and this permission notice shall be included in all
16
  copies or substantial portions of the Software.
17
  """
18
 
19
- from __future__ import annotations
20
 
21
- from dataclasses import dataclass
22
- import math
23
-
24
-
25
- @dataclass(frozen=True)
26
- class DropoutDecayConfig:
27
- initial_dropout: float = 0.80
28
- final_dropout: float = 0.10
29
- decay_tokens: int = 40_000_000
30
- schedule: str = "cosine"
31
-
32
-
33
- class DropoutDecayScheduler:
34
- """Smoothly maps total processed/observed tokens to an active dropout rate."""
35
-
36
- def __init__(self, config: DropoutDecayConfig):
37
- if not 0.0 <= config.final_dropout <= config.initial_dropout < 1.0:
38
- raise ValueError("dropout values must satisfy 0 <= final <= initial < 1")
39
- if config.decay_tokens <= 0:
40
- raise ValueError("decay_tokens must be positive")
41
- if config.schedule not in {"cosine", "smoothstep", "linear"}:
42
- raise ValueError("schedule must be cosine, smoothstep, or linear")
43
- self.config = config
44
-
45
- def value(self, tokens_seen: int) -> float:
46
- progress = min(max(tokens_seen / self.config.decay_tokens, 0.0), 1.0)
47
- if self.config.schedule == "cosine":
48
- mix = 0.5 * (1.0 + math.cos(math.pi * progress))
49
- elif self.config.schedule == "smoothstep":
50
- smooth = progress * progress * (3.0 - 2.0 * progress)
51
- mix = 1.0 - smooth
52
- else:
53
- mix = 1.0 - progress
54
- return self.config.final_dropout + (
55
- self.config.initial_dropout - self.config.final_dropout
56
- ) * mix
 
16
  copies or substantial portions of the Software.
17
  """
18
 
19
+ from dropout_decay.schedules.dropout import DropoutDecayConfig, DropoutDecayScheduler
20
 
21
+ __all__ = [
22
+ "DropoutDecayConfig",
23
+ "DropoutDecayScheduler",
24
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/dropout_decay/schedules/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from dropout_decay.schedules.dropout import DropoutDecayConfig, DropoutDecayScheduler
20
+
21
+ __all__ = [
22
+ "DropoutDecayConfig",
23
+ "DropoutDecayScheduler",
24
+ ]
src/dropout_decay/schedules/dropout.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ import math
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class DropoutDecayConfig:
27
+ initial_dropout: float = 0.80
28
+ final_dropout: float = 0.10
29
+ decay_tokens: int = 40_000_000
30
+ schedule: str = "cosine"
31
+
32
+
33
+ class DropoutDecayScheduler:
34
+ """Smoothly maps total processed/observed tokens to an active dropout rate."""
35
+
36
+ def __init__(self, config: DropoutDecayConfig):
37
+ if not 0.0 <= config.final_dropout <= config.initial_dropout < 1.0:
38
+ raise ValueError("dropout values must satisfy 0 <= final <= initial < 1")
39
+ if config.decay_tokens <= 0:
40
+ raise ValueError("decay_tokens must be positive")
41
+ if config.schedule not in {"cosine", "smoothstep", "linear"}:
42
+ raise ValueError("schedule must be cosine, smoothstep, or linear")
43
+ self.config = config
44
+
45
+ def value(self, tokens_seen: int) -> float:
46
+ progress = min(max(tokens_seen / self.config.decay_tokens, 0.0), 1.0)
47
+ if self.config.schedule == "cosine":
48
+ mix = 0.5 * (1.0 + math.cos(math.pi * progress))
49
+ elif self.config.schedule == "smoothstep":
50
+ smooth = progress * progress * (3.0 - 2.0 * progress)
51
+ mix = 1.0 - smooth
52
+ else:
53
+ mix = 1.0 - progress
54
+ return self.config.final_dropout + (
55
+ self.config.initial_dropout - self.config.final_dropout
56
+ ) * mix
src/dropout_decay/specs/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from dropout_decay.specs.dropout_condition import DropoutCondition, anchor_dropout
20
+ from dropout_decay.specs.model_spec import ModelSpec
21
+
22
+ __all__ = [
23
+ "DropoutCondition",
24
+ "ModelSpec",
25
+ "anchor_dropout",
26
+ ]
src/dropout_decay/specs/dropout_condition.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derived from Andrej Karpathy's nanochat project.
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Andrej Karpathy
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ import math
23
+ from typing import Callable
24
+
25
+ from dropout_decay.schedules import DropoutDecayConfig, DropoutDecayScheduler
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class DropoutCondition:
30
+ name: str
31
+ kind: str
32
+ initial: float
33
+ final: float
34
+ schedule: str = "constant"
35
+ decay_tokens: int | None = None
36
+ anchors: tuple[tuple[int, float], ...] = ()
37
+
38
+ def to_dict(self) -> dict:
39
+ return {
40
+ "name": self.name,
41
+ "kind": self.kind,
42
+ "initial": self.initial,
43
+ "final": self.final,
44
+ "schedule": self.schedule,
45
+ "decay_tokens": self.decay_tokens,
46
+ "anchors": [list(anchor) for anchor in self.anchors],
47
+ }
48
+
49
+ def make_fn(
50
+ self,
51
+ fallback_decay_tokens: int,
52
+ unique_tokens: int | None = None,
53
+ ) -> Callable[[int], float]:
54
+ if self.kind == "static":
55
+ return lambda _tokens_seen, p=self.initial: p
56
+ if self.kind == "anchor_decay":
57
+ if unique_tokens is None:
58
+ raise ValueError("anchor_decay conditions require unique_tokens")
59
+ p = anchor_dropout(unique_tokens, self.anchors)
60
+ return lambda _tokens_seen, p=p: p
61
+ scheduler = DropoutDecayScheduler(
62
+ DropoutDecayConfig(
63
+ initial_dropout=self.initial,
64
+ final_dropout=self.final,
65
+ decay_tokens=self.decay_tokens or fallback_decay_tokens,
66
+ schedule=self.schedule,
67
+ )
68
+ )
69
+ return scheduler.value
70
+
71
+
72
+ def anchor_dropout(unique_tokens: int, anchors: tuple[tuple[int, float], ...]) -> float:
73
+ if not anchors:
74
+ raise ValueError("anchor dropout schedule requires at least one anchor")
75
+ ordered = sorted(anchors)
76
+ if unique_tokens <= ordered[0][0]:
77
+ return ordered[0][1]
78
+ if unique_tokens >= ordered[-1][0]:
79
+ return ordered[-1][1]
80
+ log_unique = math.log(unique_tokens)
81
+ for (left_tokens, left_dropout), (right_tokens, right_dropout) in zip(
82
+ ordered, ordered[1:]
83
+ ):
84
+ if left_tokens <= unique_tokens <= right_tokens:
85
+ left_log = math.log(left_tokens)
86
+ right_log = math.log(right_tokens)
87
+ mix = (log_unique - left_log) / (right_log - left_log)
88
+ return left_dropout + mix * (right_dropout - left_dropout)
89
+ return ordered[-1][1]