@@ -94,6 +94,30 @@ pub(crate) struct ResolvedHp {
9494 pub target_val_loss : f32 ,
9595}
9696
97+ /// SPEC §82 P1-A: Estimate transformer parameter count from arch dims.
98+ ///
99+ /// Formula (decoder-only, tied or untied embedding):
100+ /// N ≈ vocab × hidden (embedding)
101+ /// + L × (4·hidden² + 3·hidden·intermediate) (per-layer attn + ffn)
102+ /// + hidden (final norm)
103+ ///
104+ /// Embedding is counted once (assumes tied lm_head; for untied add a 2nd
105+ /// `vocab × hidden`). This is a coarse estimate suitable for Chinchilla
106+ /// scaling sanity checks, not a precise param report — for that, use
107+ /// `apr inspect --json | jq .parameters`.
108+ fn estimate_param_count ( arch : & TransformerConfig ) -> u64 {
109+ let vocab = arch. vocab_size as u64 ;
110+ let hidden = arch. hidden_size as u64 ;
111+ let inter = arch. intermediate_size as u64 ;
112+ let layers = arch. num_hidden_layers as u64 ;
113+ let embed = vocab. saturating_mul ( hidden) ;
114+ let attn_per_layer = 4u64 . saturating_mul ( hidden) . saturating_mul ( hidden) ;
115+ let ffn_per_layer = 3u64 . saturating_mul ( hidden) . saturating_mul ( inter) ;
116+ let per_layer = attn_per_layer. saturating_add ( ffn_per_layer) ;
117+ let layer_total = layers. saturating_mul ( per_layer) ;
118+ embed. saturating_add ( layer_total) . saturating_add ( hidden)
119+ }
120+
97121pub ( crate ) fn mode_defaults (
98122 mode : PretrainMode ,
99123 vocab_size : u32 ,
@@ -172,6 +196,42 @@ pub(crate) fn run(
172196
173197 let hp = mode_defaults ( mode, vocab_size, lr, warmup_steps, target_val_loss) ;
174198
199+ // SPEC §82 P1-A: Chinchilla compute-optimal gate (arXiv:2203.15556).
200+ // Compute-optimal pretraining requires train tokens D ≈ 20·N where N is
201+ // the parameter count. If D < 5·N we're severely under-trained; the
202+ // model will memorize the small corpus instead of generalizing.
203+ //
204+ // Triggered for `--init` runs where we can read the arch dims to
205+ // estimate N; from-scratch synthetic runs are exempt because the
206+ // operator usually knows what they're doing. Non-fatal warning only.
207+ if let Some ( arch) = init_arch. as_ref ( ) {
208+ let n_params = estimate_param_count ( arch) ;
209+ let d_tokens = ( num_steps as u64 )
210+ . saturating_mul ( batch_size as u64 )
211+ . saturating_mul ( seq_length as u64 ) ;
212+ let ratio = d_tokens as f64 / n_params as f64 ;
213+ if ratio < 5.0 {
214+ eprintln ! (
215+ "[P1-A] Chinchilla gate WARNING: train tokens D = {} ({:.1}M) is {:.2}× param count N = {} ({:.1}M); \
216+ Chinchilla compute-optimal target is D ≈ 20·N. Run is severely under-trained — \
217+ expect val_loss plateau driven by capacity exhaustion, not optimization. \
218+ Consider increasing --num-steps to ~{} or reducing model size.",
219+ d_tokens, d_tokens as f64 / 1e6 ,
220+ ratio,
221+ n_params, n_params as f64 / 1e6 ,
222+ ( 20 * n_params) / ( batch_size as u64 * seq_length as u64 ) ,
223+ ) ;
224+ } else if ratio < 20.0 {
225+ eprintln ! (
226+ "[P1-A] Chinchilla gate: train tokens D = {} ({:.1}M) is {:.1}× param count N = {} ({:.1}M); \
227+ below compute-optimal 20·N target — model has room for more training.",
228+ d_tokens, d_tokens as f64 / 1e6 ,
229+ ratio,
230+ n_params, n_params as f64 / 1e6 ,
231+ ) ;
232+ }
233+ }
234+
175235 // Validation: GATE-TRAIN-003 requires target_val_loss > 0.
176236 if hp. target_val_loss <= 0.0 {
177237 return Err ( CliError :: ValidationFailed ( format ! (
@@ -819,6 +879,47 @@ mod tests {
819879 std:: fs:: write ( dir. join ( "vocab.json" ) , json) . expect ( "write vocab.json" ) ;
820880 }
821881
882+ /// SPEC §82 P1-A: parameter count estimator should be order-of-magnitude
883+ /// correct for known reference models. Qwen2.5-0.5B has ~500M params;
884+ /// our coarse formula should be within 2× of that.
885+ #[ test]
886+ fn estimate_param_count_qwen2_05b_within_2x ( ) {
887+ let mut cfg = TransformerConfig :: llama2_7b ( ) ;
888+ cfg. hidden_size = 896 ;
889+ cfg. num_hidden_layers = 24 ;
890+ cfg. num_attention_heads = 14 ;
891+ cfg. num_kv_heads = 2 ;
892+ cfg. intermediate_size = 4864 ;
893+ cfg. vocab_size = 151936 ;
894+ let n = estimate_param_count ( & cfg) ;
895+ // True Qwen2.5-0.5B = ~494M. Our estimate counts tied embedding once
896+ // and ignores GQA reduction; expect ~400-700M.
897+ let ref_params: u64 = 494_000_000 ;
898+ assert ! (
899+ n > ref_params / 2 && n < ref_params * 2 ,
900+ "Qwen2.5-0.5B estimate {n} should be within 2× of 494M" ,
901+ ) ;
902+ }
903+
904+ /// SPEC §82 P1-A: estimator should scale super-linearly with depth.
905+ #[ test]
906+ fn estimate_param_count_scales_with_layers ( ) {
907+ let mut cfg = TransformerConfig :: llama2_7b ( ) ;
908+ cfg. hidden_size = 512 ;
909+ cfg. num_hidden_layers = 1 ;
910+ cfg. intermediate_size = 2048 ;
911+ cfg. vocab_size = 32000 ;
912+ let n1 = estimate_param_count ( & cfg) ;
913+ cfg. num_hidden_layers = 24 ;
914+ let n24 = estimate_param_count ( & cfg) ;
915+ // 24× per-layer params + shared embedding ≈ 5-6× total for small models
916+ // where embedding dominates per-layer contribution.
917+ assert ! (
918+ n24 > n1 * 4 ,
919+ "24-layer model {n24} should be at least 4× 1-layer model {n1}" ,
920+ ) ;
921+ }
922+
822923 #[ test]
823924 fn preflight_accepts_matching_vocab ( ) {
824925 // GATE-ARCH-370M-011 acceptance case: tokenizer vocab.json with
0 commit comments