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