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 <json> --data <dir> --out <dir> [--resume <ckpt>] [--backend metal|cpu]
forge generate --checkpoint <ckpt|.forge> --tokenizer <model> --prompt <text>
[--temp t] [--top-k k] [--max-tokens n] [--seed s]
forge eval --checkpoint <ckpt|.forge> --data <val.bin> [--batches n]
forge export --checkpoint <ckpt.bin> --out <model.forge> [--dtype f32|f16|bf16]
[--shard-mb n] [--tag label]
forge info [--config <json>]- Les flags sont
--key value; un flag sans valeur vaut"true". generate/evalacceptent 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)
<out>/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.402val_loss = -1.0quand pas d'éval à ce step (eval tous leseval_everysteps, à 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: <path>etfmodel: <manifest> — N tensors reused…. Première ligne :training <name>: <params> params, <steps> steps, <tps> tokens/step, backend=…, opt=…, sched=….
- lignes
4. Checkpoints & resume
<out>/ckpt_%06d.bintous lescheckpoint_everysteps +ckpt_latest.binà chaque fois + checkpoint final à max_steps. Format binaire FRGE v1 (poids + état optimiseur- step + config JSON embarquée).
--resume <ckpt>EXISTE et reprend au step sauvé (dataloader re-seedé du step)..forge: si forge_save, chaque checkpoint committe aussi les poids dans<out>/model.forge/(manifests + shards contenu-adressés ;tools/fmodel.py logpour 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 <dir>: 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
- 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.
- 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).
- M3 : chart interactif — crosshair + callout, pinch-zoom + pan, pastille follow-live, double-clic/fit, log-Y, marqueur best-val.
- 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.
- 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.
- 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.
- 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.shprêt (identité + profil réels) ; à lancer quand on fige une version.