SPB Git

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%
8.0 KB

# 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)

text
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/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)

<out>/log.csv, en-tête écrit au step 0, une ligne par step, flushée :

text
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: <path> et fmodel: <manifest> — N tensors reused…. Première ligne : training <name>: <params> params, <steps> steps, <tps> tokens/step, backend=…, opt=…, sched=….

# 4. Checkpoints & resume

  • <out>/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 <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 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 <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.sh prêt (identité + profil réels) ; à lancer quand on fige une version.