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.ai2//3// Parsers for Forge's two output channels (RESEARCH.md §3):4// - log.csv: header-driven, the structured metrics source of truth.5// Handles both the 7-column (elapsed_s) and legacy 6-column layouts.6// - stdout: events the CSV doesn't carry (checkpoint saved, fmodel commit,7// run banner), parsed defensively — garbage lines are ignored, never fatal.8import Foundation910struct LogParser {11 // ---- log.csv --------------------------------------------------------1213 struct CSVSchema: Equatable {14 var columns: [String]15 static let current = CSVSchema(columns: [16 "step", "loss", "lr", "grad_norm", "tokens_per_sec", "val_loss",17 "elapsed_s",18 ])19 }2021 private(set) var schema: CSVSchema?2223 /// Feed one CSV line (header or data). Returns a point for data lines.24 mutating func parseCSVLine(_ line: String) -> MetricPoint? {25 let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)26 guard !trimmed.isEmpty else { return nil }27 if trimmed.hasPrefix("step,") {28 schema = CSVSchema(columns: trimmed.split(separator: ",").map(String.init))29 return nil30 }31 let cols = schema?.columns ?? CSVSchema.current.columns32 let parts = trimmed.split(separator: ",", omittingEmptySubsequences: false)33 guard parts.count >= 5 else { return nil }34 func f(_ name: String) -> Double? {35 guard let i = cols.firstIndex(of: name), i < parts.count else { return nil }36 return Double(parts[i])37 }38 guard let step = f("step").map({ Int($0) }),39 let loss = f("loss") else { return nil }40 let val = f("val_loss")41 return MetricPoint(42 step: step,43 trainLoss: loss,44 lr: f("lr") ?? 0,45 gradNorm: f("grad_norm") ?? 0,46 tokensPerSec: f("tokens_per_sec") ?? 0,47 valLoss: (val ?? -1) >= 0 ? val : nil,48 elapsedS: f("elapsed_s"))49 }5051 // ---- stdout events ---------------------------------------------------5253 enum StdoutEvent: Equatable {54 case banner(params: Int, steps: Int, tokensPerStep: Int, backend: String)55 case checkpointSaved(path: String)56 case forgeCommit(manifest: String)57 }5859 static func parseStdout(_ line: String) -> StdoutEvent? {60 if line.hasPrefix("checkpoint saved: ") {61 return .checkpointSaved(path: String(line.dropFirst("checkpoint saved: ".count)))62 }63 if line.hasPrefix("fmodel: ") {64 let rest = line.dropFirst("fmodel: ".count)65 let manifest = rest.split(separator: " ").first.map(String.init) ?? ""66 return .forgeCommit(manifest: manifest)67 }68 if line.hasPrefix("training ") {69 // "training <name>: <p> params, <s> steps, <t> tokens/step, backend=<b>…"70 // The model name may contain digits — only parse after the colon.71 guard let colon = line.range(of: ": ") else { return nil }72 let tail = line[colon.upperBound...]73 let numbers = tail.split(whereSeparator: { !"0123456789".contains($0) })74 .compactMap { Int($0) }75 let backend = line.range(of: "backend=").map {76 String(line[$0.upperBound...].prefix(while: { $0 != "," }))77 }78 if numbers.count >= 3 {79 return .banner(params: numbers[0], steps: numbers[1],80 tokensPerStep: numbers[2], backend: backend ?? "?")81 }82 }83 return nil84 }85}86