Repository Public
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// New Run editor: preset picker, the high-traffic model/train fields, the4// live derived panel (params, tokens/step, epochs, memory badge) and the LR5// schedule preview. Inline validation gates the Start button. (The long6// tail of variant knobs lives in the "Avancé" JSON editor — full form in M2.)7import Charts8import SwiftUI910struct NewRunSheet: View {11 @Environment(AppModel.self) private var app12 @Environment(\.dismiss) private var dismiss13 @State private var config = ForgeConfig()14 @State private var runName = "run"15 @State private var datasetPath = ""16 @State private var startError: String?1718 private var datasets: [Dataset] { app.datasets() }19 private var dataset: Dataset? { datasets.first { $0.path == datasetPath } }20 private var errors: [String] {21 var e = config.validationErrors22 if datasetPath.isEmpty { e.append("choisir un dataset") }23 if let v = dataset?.vocabSize, v != config.model.vocabSize {24 e.append("vocab_size (\(config.model.vocabSize)) ≠ tokenizer du dataset (\(v))")25 }26 return e27 }2829 var body: some View {30 VStack(spacing: 0) {31 HStack {32 Text("Nouveau run").font(.title2.weight(.semibold))33 Spacer()34 presetMenu35 }36 .padding()37 Divider()38 HSplitView {39 Form {40 Section("Run") {41 TextField("Nom", text: $runName)42 Picker("Dataset", selection: $datasetPath) {43 Text("—").tag("")44 ForEach(datasets) { ds in45 Text("\(ds.name) · vocab \(ds.vocabSize ?? 0) · \((ds.trainTokens ?? 0).formatted(.number.notation(.compactName))) tokens")46 .tag(ds.path)47 }48 }49 .onChange(of: datasetPath) {50 if let v = dataset?.vocabSize { config.model.vocabSize = v }51 }52 }53 Section("Modèle") {54 intField("n_layers", $config.model.nLayers)55 intField("d_model", $config.model.dModel)56 intField("n_heads", $config.model.nHeads)57 intField("n_kv_heads (GQA)", $config.model.nKvHeads)58 intField("d_ff", $config.model.dFf)59 intField("context_length", $config.model.contextLength)60 Picker("activation", selection: $config.model.activation) {61 ForEach(["swiglu", "gelu", "relu2"], id: \.self) { Text($0) }62 }63 Picker("norm", selection: $config.model.norm) {64 ForEach(["rmsnorm", "layernorm"], id: \.self) { Text($0) }65 }66 Toggle("tied_embeddings", isOn: $config.model.tiedEmbeddings)67 Toggle("qk_norm", isOn: $config.model.qkNorm)68 intField("sliding_window (0 = full)", $config.model.slidingWindow)69 }70 Section("Entraînement") {71 doubleField("lr", $config.train.lr)72 intField("max_steps", $config.train.maxSteps)73 intField("warmup_steps", $config.train.warmupSteps)74 Picker("schedule", selection: $config.train.schedule) {75 ForEach(["cosine", "wsd"], id: \.self) { Text($0) }76 }77 Picker("optimizer", selection: $config.train.optimizer) {78 ForEach(["adamw", "muon"], id: \.self) { Text($0) }79 }80 intField("batch_size (micro)", $config.train.batchSize)81 intField("grad_accum_steps", $config.train.gradAccumSteps)82 intField("checkpoint_every", $config.train.checkpointEvery)83 intField("eval_every", $config.train.evalEvery)84 }85 }86 .formStyle(.grouped)87 .frame(minWidth: 380)8889 DerivedPanel(config: config, dataset: dataset)90 .frame(minWidth: 300)91 .padding()92 }93 Divider()94 HStack {95 if let first = errors.first {96 Label(first, systemImage: "exclamationmark.triangle")97 .foregroundStyle(.red)98 .font(.callout)99 }100 if let startError {101 Text(startError).foregroundStyle(.red).font(.callout)102 }103 Spacer()104 Button("Annuler") { dismiss() }105 Button("Démarrer l'entraînement") {106 Task {107 do {108 config.model.name = runName109 try await app.supervisor.start(110 config: config, datasetPath: datasetPath,111 name: runName)112 dismiss()113 } catch {114 startError = error.localizedDescription115 }116 }117 }118 .keyboardShortcut(.defaultAction)119 .disabled(!errors.isEmpty)120 }121 .padding()122 }123 .frame(minWidth: 860, minHeight: 620)124 .onAppear {125 if let first = datasets.first { datasetPath = first.path }126 }127 }128129 private var presetMenu: some View {130 Menu("Presets") {131 ForEach(Presets.all, id: \.name) { preset in132 Button(preset.name) {133 config = preset.config134 runName = preset.config.model.name135 if let v = dataset?.vocabSize { config.model.vocabSize = v }136 }137 }138 }139 .frame(width: 120)140 }141142 private func intField(_ label: String, _ value: Binding<Int>) -> some View {143 TextField(label, value: value, format: .number)144 }145146 private func doubleField(_ label: String, _ value: Binding<Double>) -> some View {147 TextField(label, value: value, format: .number.precision(.significantDigits(1...6)))148 }149}150151struct DerivedPanel: View {152 let config: ForgeConfig153 let dataset: Dataset?154155 var body: some View {156 VStack(alignment: .leading, spacing: 14) {157 Text("Dérivés").font(.headline)158 derived("Paramètres",159 config.model.paramCount.formatted(.number.notation(.compactName)))160 derived("Tokens/step", config.tokensPerStep.formatted())161 derived("Tokens totaux",162 config.totalTokens.formatted(.number.notation(.compactName)))163 if let t = dataset?.trainTokens, t > 0 {164 derived("Epochs sur le dataset",165 String(format: "%.2f", Double(config.totalTokens) / Double(t)))166 }167 memoryBadge168 Divider()169 Text("Schedule LR").font(.headline)170 Chart {171 let steps = stride(from: 0, to: config.train.maxSteps,172 by: max(1, config.train.maxSteps / 200))173 ForEach(Array(steps), id: \.self) { s in174 LineMark(x: .value("step", s),175 y: .value("lr", config.train.lrAt(step: s)))176 .foregroundStyle(.purple)177 }178 }179 .frame(height: 140)180 Spacer()181 }182 }183184 private var memoryBadge: some View {185 let bytes = SystemInfo.estimatedTrainingBytes(config: config)186 let machine = SystemInfo.memoryBytes187 let gb = Double(bytes) / 1e9188 let ok = bytes < machine * 8 / 10189 return HStack {190 Image(systemName: ok ? "memorychip" : "exclamationmark.triangle.fill")191 Text(String(format: "~%.1f GB estimés / %.0f GB unifiés", gb,192 Double(machine) / 1e9))193 }194 .font(.callout)195 .foregroundStyle(ok ? Color.secondary : Color.orange)196 }197198 private func derived(_ label: String, _ value: String) -> some View {199 HStack {200 Text(label).foregroundStyle(.secondary)201 Spacer()202 Text(value).monospacedDigit()203 }204 .font(.callout)205 }206}207208enum Presets {209 struct Preset {210 let name: String211 let config: ForgeConfig212 }213214 // Mirrors configs/ in the forge repo (gpt-10m-1epoch, gpt-50m…).215 static let all: [Preset] = {216 var p10 = ForgeConfig()217 p10.model.name = "gpt-10m"218 p10.model.nLayers = 6; p10.model.dModel = 384; p10.model.nHeads = 6219 p10.model.nKvHeads = 6; p10.model.dFf = 1024; p10.model.vocabSize = 4096220 p10.model.contextLength = 512221 p10.train.lr = 6e-4; p10.train.warmupSteps = 58; p10.train.maxSteps = 584222 p10.train.batchSize = 64; p10.train.gradAccumSteps = 1223 p10.train.checkpointEvery = 200; p10.train.evalEvery = 100224225 var p50 = ForgeConfig()226 p50.model.name = "gpt-50m"227 p50.model.nLayers = 10; p50.model.dModel = 640; p50.model.nHeads = 10228 p50.model.nKvHeads = 10; p50.model.dFf = 1728; p50.model.vocabSize = 4096229 p50.model.contextLength = 1024230 p50.train.lr = 5e-4; p50.train.warmupSteps = 117; p50.train.maxSteps = 1170231 p50.train.batchSize = 8; p50.train.gradAccumSteps = 8232 p50.train.checkpointEvery = 200; p50.train.evalEvery = 100233234 var muon = p50235 muon.model.name = "gpt-50m-muon"236 muon.train.optimizer = "muon"; muon.train.schedule = "wsd"237238 return [Preset(name: "gpt-10m (1 epoch)", config: p10),239 Preset(name: "gpt-50m", config: p50),240 Preset(name: "gpt-50m Muon+WSD", config: muon)]241 }()242}243