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.5 KB · 99 lines swift
Raw Blame History
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Forge Studio — GUI companion for the Forge LLM training framework.4import SwiftUI56@main7struct ForgeStudioApp: App {8    @State private var model = AppModel()910    var body: some Scene {11        WindowGroup("Forge Studio") {12            ContentView()13                .environment(model)14                .frame(minWidth: 980, minHeight: 640)15        }16        Settings {17            SettingsView()18                .environment(model)19        }20    }21}2223@Observable24@MainActor25final class AppModel {26    var store: RunStore27    var supervisor: RunSupervisor28    var forgeDevice: String?29    var forgeError: String?3031    init() {32        let workspace = ForgeBinaryLocator.savedWorkspaceURL33            ?? FileManager.default.homeDirectoryForCurrentUser34                .appendingPathComponent("ForgeStudioWorkspace")35        let store = RunStore(workspace: workspace)36        self.store = store37        self.supervisor = RunSupervisor(store: store)38        recoverOrphans()39        RunSupervisor.requestNotificationAuth()40        Task { await self.validateForge() }41    }4243    /// Crash recovery: a run persisted as active means the app died mid-run.44    /// If its forge PID is still alive we leave the process alone and record45    /// the truth (stopped tracking); if it's gone, the run is marked stopped46    /// with an honest reason. A run never stays in a lying state.47    private func recoverOrphans() {48        for var run in store.runs where run.state.isActive {49            if let pid = run.pid, kill(pid, 0) == 0 {50                run.failureReason =51                    "L'app a quitté pendant le run ; forge (pid \(pid)) tourne encore — "52                    + "métriques reprises depuis log.csv, contrôle du process perdu."53            } else {54                run.failureReason =55                    "L'app a quitté pendant le run ; forge n'est plus actif. "56                    + "Reprise possible depuis ckpt_latest.bin."57            }58            run.state = .stopped59            run.pid = nil60            store.upsert(run)61        }62    }6364    func validateForge() async {65        guard let binary = ForgeBinaryLocator.savedBinaryURL66            ?? ForgeBinaryLocator.candidates().first else {67            forgeError = "Binaire forge introuvable — choisissez-le dans les Réglages."68            return69        }70        switch await ForgeBinaryLocator.validate(binary: binary) {71        case .success(let v):72            forgeDevice = v.version73            forgeError = nil74            ForgeBinaryLocator.save(binary: binary)75        case .failure(let e):76            forgeError = e.localizedDescription77        }78    }7980    func datasets() -> [Dataset] {81        let dataDir = store.workspaceURL.appendingPathComponent("data")82        let forgeData = ForgeBinaryLocator.savedBinaryURL?83            .deletingLastPathComponent().deletingLastPathComponent()84            .appendingPathComponent("data")85        var found: [Dataset] = []86        for root in [dataDir, forgeData].compactMap({ $0 }) {87            guard let subdirs = try? FileManager.default.contentsOfDirectory(88                at: root, includingPropertiesForKeys: nil) else { continue }89            for dir in subdirs {90                if let ds = Dataset.scan(directory: dir),91                   !found.contains(where: { $0.path == ds.path }) {92                    found.append(ds)93                }94            }95        }96        return found.sorted { $0.name < $1.name }97    }98}99