Skip to content

Commit 1ac3340

Browse files
noahgiftclaude
andauthored
feat(pretrain): SPEC §82 P1-A — Chinchilla compute-optimal gate warning (#1708)
When `apr pretrain --init <apr>` runs, compute the param count N from the init model's arch dims and check it against train tokens D = num_steps × batch_size × seq_length. Per Chinchilla (arXiv:2203.15556), compute-optimal pretraining requires D ≈ 20·N. Two warning thresholds: - D < 5·N → SEVERE: model will memorize, not generalize - D < 20·N → BELOW-OPTIMAL: model has room for more training Non-fatal — operators may have legitimate reasons to deviate (resume runs, ablation studies). The warning includes a suggested `--num-steps` value to reach 20·N. Triggered only on --init paths (from-scratch synthetic runs are exempt — operator knows what they're doing). Test plan: - estimate_param_count() with Qwen2.5-0.5B dims gives within 2× of 494M - estimator scales appropriately with num_hidden_layers - 2/2 P1-A tests PASS Discharges §82 P1-A item (Δship +1, prevention, ~75 LOC). Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent de50143 commit 1ac3340

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

crates/apr-cli/src/commands/pretrain.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
97121
pub(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

Comments
 (0)