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%
3.6 KB · 86 lines swift
Raw Blame History
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