# RESEARCH.md — Ground truth Forge pour Forge Studio ### Source : lecture directe du code Forge (même auteur, même machine), août 2026 ## 1. CLI `forge` (src/main.cpp) ``` forge train --config --data --out [--resume ] [--backend metal|cpu] forge generate --checkpoint --tokenizer --prompt [--temp t] [--top-k k] [--max-tokens n] [--seed s] forge eval --checkpoint --data [--batches n] forge export --checkpoint --out [--dtype f32|f16|bf16] [--shard-mb n] [--tag label] forge info [--config ] ``` - Les flags sont `--key value` ; un flag sans valeur vaut `"true"`. - `generate`/`eval` acceptent un repo `.forge` (répertoire) ou un manifest.json. - Exit code 0 = succès ; erreurs fatales → stderr + abort (code ≠ 0). ## 2. Schéma de config (src/nn/config.h) — vérité au 2026-08-05 ### `model` (défauts entre parenthèses) name("model") · n_layers(6) · d_model(384) · n_heads(6) · n_kv_heads(=n_heads, GQA si <) · d_ff(1024) · vocab_size(4096) · context_length(512) · tied_embeddings(true) · use_rope(true) · rope_theta(10000) · norm("rmsnorm"|"layernorm") · norm_eps(1e-6) · activation("swiglu"|"gelu"|"relu2") · dropout(0) · quant("none"|"int8"|"ternary") · qk_norm(false) · final_softcap(0) · scale_embeddings(false) · attention_bias(false) · head_dim(0=auto) · nope_every(0) · norm_placement("pre"|"post"|"sandwich") · rope_scale_factor(0=off) · rope_scale_low(1) · rope_scale_high(4) · rope_scale_orig_ctx(8192) · sliding_window(0) · sliding_global_every(0) · rope_theta_global(0) · attn_softcap(0) · n_experts(0) · moe_top_k(2) · moe_aux_weight(0.01) · n_shared_experts(0) · moe_scoring("softmax"|"sigmoid") · moe_norm_topk(true) · routed_scaling_factor(1) · moe_d_ff(0) · first_k_dense(0) · moe_bias_gamma(0) Validations (levées comme exceptions au parse) : d_model % n_heads == 0 (sauf head_dim explicite) ; n_heads % n_kv_heads == 0 ; head_dim pair ; moe_top_k ∈ [1, n_experts] ; n_shared_experts ⇒ n_experts>0 ; first_k_dense ∈ [0, n_layers] ; enums valides. ### `train` lr(6e-4) · min_lr_ratio(0.1) · warmup_steps(2000) · max_steps(100000) · schedule("cosine"|"wsd") · wsd_decay_frac(0.15) · optimizer("adamw"|"muon") · muon_lr(0.02) · muon_momentum(0.95) · beta1(0.9) · beta2(0.95) · eps(1e-8) · weight_decay(0.1) · grad_clip(1.0, 0=off) · batch_size(32, MICRO-batch) · grad_accum_steps(1) · precision("f32", parsé mais pas encore honoré) · checkpoint_every(1000, 0=off) · forge_save(true) · forge_dtype("f32"|"f16"|"bf16") · eval_every(500, 0=off) · eval_batches(20) · seed(1337) · deterministic(false) Dérivés : tokens/step = batch_size × context_length × grad_accum_steps ; param count = formule de ModelConfig::num_params() (répliquée dans ForgeConfig.swift, à cross-checker via `forge info`). ## 3. Métriques structurées — log.csv (PAS besoin de patch JSONL) `/log.csv`, en-tête écrit au step 0, **une ligne par step, flushée** : ``` step,loss,lr,grad_norm,tokens_per_sec,val_loss,elapsed_s 584,2.296893,3.680000e-04,0.558962,13692.0,-1.000000,3861.402 ``` - `val_loss = -1.0` quand pas d'éval à ce step (eval tous les `eval_every` steps, à step ≡ eval_every-1 mod eval_every). - `elapsed_s` (ajouté commit 147560e) = wall-clock depuis le début du run. - Resume : le fichier est rouvert en append (pas de second en-tête) ; les anciens runs peuvent avoir 6 colonnes (sans elapsed_s) → parser dirigé par l'en-tête. - stdout humain en parallèle : `step %6lld | loss %.4f | lr %.2e | gnorm %.3f | %f tok/s[ | val %.4f]` + lignes `checkpoint saved: ` et `fmodel: — N tensors reused…`. Première ligne : `training : params, steps, tokens/step, backend=…, opt=…, sched=…`. ## 4. Checkpoints & resume - `/ckpt_%06d.bin` tous les `checkpoint_every` steps + `ckpt_latest.bin` à chaque fois + checkpoint final à max_steps. Format binaire FRGE v1 (poids + état optimiseur + step + config JSON embarquée). - **`--resume ` EXISTE** et reprend au step sauvé (dataloader re-seedé du step). - `.forge` : si forge_save, chaque checkpoint committe aussi les poids dans `/model.forge/` (manifests + shards contenu-adressés ; `tools/fmodel.py log` pour l'historique). ## 5. Signaux `forge train` n'installe AUCUN handler SIGINT/SIGTERM : le process meurt immédiatement, sans checkpoint de sortie. Conséquence UI : « Stop » = SIGTERM après confirmation ; la reprise se fait du dernier `ckpt_latest.bin` (perte ≤ checkpoint_every steps). L'UI doit l'annoncer honnêtement. ## 6. Datasets (tools/) - `prepare_data.py --out data/tinystories [--vocab-size N] [--max-train-mb MB] [--tokenizer path]` → `train.bin`, `val.bin` (en-tête {magic 20240520, version 1, num_tokens} puis tokens uint16), `tokN.model` (forgebpe v1, texte). - `prepare_hf_data.py --source|--mix|--preset … --out ` : 13 sources HF streamées, mélanges pondérés (voir --list). - Contrainte dure : `model.vocab_size` == vocab du tokenizer du dataset. - Token count d'un .bin : lire l'en-tête int32[3] (offset 8 = num_tokens) — pas de division de taille de fichier. ## 7. Signing / notarization (extrait de zyquo-term, RÉEL) - Identité : `Developer ID Application: Simon-Pierre Boucher (3YM54G49SN)` (Team 3YM54G49SN), certificat dans le login keychain. - Profil notarytool : `MacLustr-Notarize` (keychain profile). - Pattern : SwiftPM canonique (pas de .xcodeproj) → `scripts/package-app.sh` (bundle + codesign ad-hoc pour dev) → `scripts/notarize.sh` (Developer ID + hardened runtime + DMG + notarytool submit --wait + stapler). - Zyquo-term signe : binaire (avec entitlements) puis .app puis .dmg, options `--force --options runtime --timestamp`. ## 8. Décisions - **Métriques : log.csv suffit** (structuré, flushé, versionné par en-tête) — pas de patch --metrics-file nécessaire ; parser regex du stdout gardé en fallback pour détecter `checkpoint saved:` en live. - **Build : SwiftPM** (pattern zyquo-term), pas de .xcodeproj — Xcode 26 présent si besoin d'archive. - **Sandbox : non** (l'app pilote des binaires externes arbitraires) ; Developer ID + Hardened Runtime, comme zyquo-term. ## 9. État d'avancement Studio - [x] M1 fondations : Package.swift, modèles Codable complets (schéma §2), services (ProcessRunner, LogParser, MetricsStore, RunStore, BinaryLocator, SystemInfo), LTTB + EMA, app squelette naviguable, scripts build/package/notarize. - [x] M2 (essentiel) : éditeur New Run avec presets, panneau dérivé (params/tokens/ epochs/badge mémoire), preview LR, validation inline, import/export JSON. Reste : formulaire exhaustif des knobs de variantes (éditables via import JSON en attendant). - [x] M3 : chart interactif — crosshair + callout, pinch-zoom + pan, pastille follow-live, double-clic/fit, log-Y, marqueur best-val. - [x] M4 (partiel) : charts secondaires (LR/tok·s/grad-norm + seuil clip), récupération de crash (jamais d'état menteur), watchdog 30 s. Reste : axes liés entre charts, annotations checkpoint cliquables, exports PNG. - [x] M5 : vue Compare — overlay val/EMA de 2-8 runs, axe steps/TOKENS/temps, diff d'hyperparams auto dans la légende, table triable, export CSV. - [x] M6 : onglet Datasets (tokens/vocab/taille) + flow "Nouveau dataset" pilotant prepare_data.py et prepare_hf_data.py (presets HF) avec console live ; panneau Checkpoints + Générer (streamé) + Évaluer sur les runs terminés. - [x] M7 : icône générée (flame/forge, iconset complet), notifications locales de fin/échec de run, **stress test 200k points : ingest CSV complet 200k lignes < 5 s (mesuré ~1.1 s), snapshot LTTB+EMA 200k → 1200 pts < 250 ms** — le refresh UI à 2-10 Hz reste donc hors du chemin critique. - [ ] M8 : DMG notarisé — `scripts/notarize.sh` prêt (identité + profil réels) ; à lancer quand on fige une version.