SPB Git

Repository Public

NewRunSheet.swift 10.0 KB 243 lines swift

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
Files 94570c75210a2acf467858b3c58005f0730d3bb4
Menu

Theme