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// Per-run checkpoints with Generate (streamed token-by-token) and Eval4// (loss/ppl) panels — thin drivers over `forge generate` / `forge eval`.5import SwiftUI67struct CheckpointsPanel: View {8 let run: Run9 @Binding var selected: String?10 @Environment(AppModel.self) private var app11 @State private var checkpoints: [Checkpoint] = []12 @State private var prompt = "Once upon a time"13 @State private var temp = 0.814 @State private var topK = 4015 @State private var maxTokens = 20016 @State private var output = ""17 @State private var evalResult: String?18 @State private var busy = false19 @State private var errorText: String?2021 var body: some View {22 VStack(alignment: .leading, spacing: 12) {23 Text("Checkpoints").font(.title3.weight(.semibold))24 if checkpoints.isEmpty {25 Text("Aucun checkpoint pour l'instant (écrits tous les \(run.config.train.checkpointEvery) steps).")26 .foregroundStyle(.secondary)27 } else {28 Picker("Checkpoint", selection: $selected) {29 ForEach(checkpoints) { ck in30 Text("step \(ck.step) · \(ByteCountFormatter.string(fromByteCount: ck.sizeBytes, countStyle: .file))")31 .tag(Optional(ck.path))32 }33 }34 .frame(maxWidth: 420)3536 GroupBox("Génération") {37 VStack(alignment: .leading, spacing: 8) {38 TextField("Prompt", text: $prompt)39 HStack {40 Text("temp")41 Slider(value: $temp, in: 0.1...1.5).frame(width: 100)42 Text(String(format: "%.2f", temp)).monospacedDigit()43 Stepper("top-k \(topK)", value: $topK, in: 1...200, step: 10)44 Stepper("tokens \(maxTokens)", value: $maxTokens,45 in: 20...1000, step: 20)46 Spacer()47 Button(busy ? "…" : "Générer") { generate() }48 .disabled(busy || selected == nil)49 }50 .font(.callout)51 if !output.isEmpty {52 ScrollView {53 Text(output)54 .font(.system(.body, design: .serif))55 .textSelection(.enabled)56 .frame(maxWidth: .infinity, alignment: .leading)57 }58 .frame(height: 160)59 .padding(8)60 .background(.quaternary.opacity(0.4),61 in: RoundedRectangle(cornerRadius: 6))62 }63 }64 }6566 GroupBox("Évaluation") {67 HStack {68 Button(busy ? "…" : "Évaluer (50 batches)") { evaluate() }69 .disabled(busy || selected == nil)70 if let r = evalResult {71 Text(r).font(.callout.monospacedDigit())72 }73 Spacer()74 }75 }76 }77 if let e = errorText {78 Label(e, systemImage: "exclamationmark.triangle").foregroundStyle(.red)79 }80 }81 .onAppear { scan() }82 }8384 private func scan() {85 let dir = URL(fileURLWithPath: run.outDirectory)86 let files = (try? FileManager.default.contentsOfDirectory(87 at: dir, includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey]))88 ?? []89 checkpoints = files.compactMap { url in90 let name = url.lastPathComponent91 guard name.hasPrefix("ckpt_"), name.hasSuffix(".bin"),92 let step = Int(name.dropFirst(5).dropLast(4)) else { return nil }93 let vals = try? url.resourceValues(forKeys: [.fileSizeKey,94 .contentModificationDateKey])95 return Checkpoint(path: url.path, step: step,96 sizeBytes: Int64(vals?.fileSize ?? 0),97 modifiedAt: vals?.contentModificationDate ?? .now,98 valLossAtStep: nil)99 }100 .sorted { $0.step > $1.step }101 if selected == nil { selected = checkpoints.first?.path }102 }103104 private var tokenizerPath: String? {105 let dir = URL(fileURLWithPath: run.datasetPath)106 return (try? FileManager.default.contentsOfDirectory(107 at: dir, includingPropertiesForKeys: nil))?108 .first { $0.pathExtension == "model" }?.path109 }110111 private func generate() {112 guard let ckpt = selected, let forge = ForgeBinaryLocator.savedBinaryURL,113 let tok = tokenizerPath else {114 errorText = "tokenizer introuvable dans le dossier du dataset"115 return116 }117 busy = true118 output = ""119 errorText = nil120 Task {121 defer { busy = false }122 do {123 let runner = ProcessRunner()124 let (lines, exit) = try await runner.launch(125 executable: forge,126 arguments: ["generate", "--checkpoint", ckpt,127 "--tokenizer", tok, "--prompt", prompt,128 "--temp", String(temp), "--top-k", String(topK),129 "--max-tokens", String(maxTokens)],130 currentDirectory: forge.deletingLastPathComponent())131 for await (line, isErr) in lines where !isErr {132 output += line + "\n"133 }134 let status = await exit.value135 if status.code != 0 {136 errorText = "forge generate a échoué (code \(status.code))"137 }138 } catch {139 errorText = error.localizedDescription140 }141 }142 }143144 private func evaluate() {145 guard let ckpt = selected, let forge = ForgeBinaryLocator.savedBinaryURL146 else { return }147 let valBin = run.datasetPath + "/val.bin"148 busy = true149 evalResult = nil150 errorText = nil151 Task {152 defer { busy = false }153 do {154 let runner = ProcessRunner()155 let (lines, exit) = try await runner.launch(156 executable: forge,157 arguments: ["eval", "--checkpoint", ckpt, "--data", valBin,158 "--batches", "50"],159 currentDirectory: forge.deletingLastPathComponent())160 for await (line, _) in lines where line.hasPrefix("val loss") {161 evalResult = line162 }163 let status = await exit.value164 if status.code != 0 {165 errorText = "forge eval a échoué (code \(status.code))"166 }167 } catch {168 errorText = error.localizedDescription169 }170 }171 }172}173