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%
7.3 KB · 186 lines swift
Raw Blame History
1// Author: Simon-Pierre Boucher — contact@spboucher.ai2//3// Datasets tab: the registered datasets (token counts, vocab, size) and the4// "new dataset" flow driving tools/prepare_data.py (TinyStories) or5// tools/prepare_hf_data.py (HF sources & research presets) with a live6// progress console. Python and the tools live in the forge repo, located7// from the configured binary.8import SwiftUI910struct DatasetsView: View {11    @Environment(AppModel.self) private var app12    @State private var datasets: [Dataset] = []13    @State private var showNew = false1415    var body: some View {16        VStack(alignment: .leading, spacing: 12) {17            HStack {18                Text("Datasets").font(.title2.weight(.semibold))19                Spacer()20                Button {21                    showNew = true22                } label: {23                    Label("Nouveau dataset", systemImage: "plus")24                }25            }26            if datasets.isEmpty {27                ContentUnavailableView(28                    "Aucun dataset",29                    systemImage: "cylinder.split.1x2",30                    description: Text("Préparez TinyStories ou un mélange Hugging Face — tout se passe ici, sans terminal."))31            } else {32                Table(datasets) {33                    TableColumn("Nom") { Text($0.name) }34                    TableColumn("Tokens train") {35                        Text($0.trainTokens?.formatted(.number.notation(.compactName)) ?? "?")36                    }37                    TableColumn("Tokens val") {38                        Text($0.valTokens?.formatted(.number.notation(.compactName)) ?? "?")39                    }40                    TableColumn("Vocab") { Text($0.vocabSize.map(String.init) ?? "?") }41                    TableColumn("Taille") {42                        Text(ByteCountFormatter.string(fromByteCount: $0.sizeOnDisk,43                                                       countStyle: .file))44                    }45                    TableColumn("Chemin") {46                        Text($0.path).font(.caption.monospaced())47                            .foregroundStyle(.secondary)48                    }49                }50            }51        }52        .padding()53        .onAppear { datasets = app.datasets() }54        .sheet(isPresented: $showNew, onDismiss: { datasets = app.datasets() }) {55            NewDatasetSheet()56        }57        .navigationTitle("Datasets")58    }59}6061struct NewDatasetSheet: View {62    @Environment(AppModel.self) private var app63    @Environment(\.dismiss) private var dismiss6465    enum Kind: String, CaseIterable {66        case tinystories = "TinyStories"67        case hf = "Hugging Face"68    }6970    @State private var kind: Kind = .tinystories71    @State private var name = "tinystories"72    @State private var vocabSize = 409673    @State private var maxTrainMB = 100.074    @State private var hfChoice = "preset:smollm-web"75    @State private var console: [String] = []76    @State private var running = false77    @State private var failed = false7879    // Mirrors SOURCES/PRESETS in tools/prepare_hf_data.py.80    private let hfChoices: [(label: String, value: String)] = [81        ("Preset · smollm-web (60% FineWeb-Edu + 40% DCLM)", "preset:smollm-web"),82        ("Preset · textbooks (FineWeb-Edu + Cosmopedia)", "preset:textbooks"),83        ("Preset · smol-full (edu+cosmo+math)", "preset:smol-full"),84        ("Preset · decay-anneal (fin de run WSD)", "preset:decay-anneal"),85        ("Source · fineweb-edu", "source:fineweb-edu"),86        ("Source · cosmopedia", "source:cosmopedia"),87        ("Source · finemath", "source:finemath"),88        ("Source · wikipedia-fr", "source:wikipedia-fr"),89        ("Source · tinystories (via HF)", "source:tinystories"),90    ]9192    var body: some View {93        VStack(alignment: .leading, spacing: 12) {94            Text("Nouveau dataset").font(.title2.weight(.semibold))95            Picker("Type", selection: $kind) {96                ForEach(Kind.allCases, id: \.self) { Text($0.rawValue) }97            }98            .pickerStyle(.segmented)99            Form {100                TextField("Nom (dossier dans data/)", text: $name)101                if kind == .hf {102                    Picker("Corpus", selection: $hfChoice) {103                        ForEach(hfChoices, id: \.value) { Text($0.label).tag($0.value) }104                    }105                }106                TextField("vocab_size", value: $vocabSize, format: .number)107                TextField("Taille max train (MB)", value: $maxTrainMB, format: .number)108            }109            .formStyle(.grouped)110            .frame(height: kind == .hf ? 180 : 150)111            .disabled(running)112113            if !console.isEmpty {114                ConsoleView(lines: console)115            }116117            HStack {118                if failed {119                    Label("Échec — voir la console.", systemImage: "xmark.circle")120                        .foregroundStyle(.red)121                }122                Spacer()123                Button(running ? "Fermer (continue en arrière-plan)" : "Fermer") {124                    dismiss()125                }126                Button(running ? "Préparation…" : "Préparer") { prepare() }127                    .keyboardShortcut(.defaultAction)128                    .disabled(running || name.isEmpty)129            }130        }131        .padding()132        .frame(minWidth: 640, minHeight: 480)133    }134135    private var forgeRepo: URL? {136        // <repo>/build/forge → <repo>137        ForgeBinaryLocator.savedBinaryURL?138            .deletingLastPathComponent().deletingLastPathComponent()139    }140141    private func prepare() {142        guard let repo = forgeRepo else {143            console = ["⚠︎ Binaire forge non configuré — Réglages."]144            failed = true145            return146        }147        let outDir = app.store.workspaceURL.appendingPathComponent("data/\(name)")148        var args: [String]149        switch kind {150        case .tinystories:151            args = ["tools/prepare_data.py", "--out", outDir.path,152                    "--vocab-size", String(vocabSize),153                    "--max-train-mb", String(maxTrainMB)]154        case .hf:155            let parts = hfChoice.split(separator: ":", maxSplits: 1).map(String.init)156            args = ["tools/prepare_hf_data.py", "--\(parts[0])", parts[1],157                    "--out", outDir.path, "--vocab-size", String(vocabSize),158                    "--max-train-mb", String(maxTrainMB)]159        }160        running = true161        failed = false162        console = ["$ python3 " + args.joined(separator: " ")]163        Task {164            do {165                let runner = ProcessRunner()166                let (lines, exit) = try await runner.launch(167                    executable: URL(fileURLWithPath: "/usr/bin/env"),168                    arguments: ["python3"] + args,169                    currentDirectory: repo)170                for await (line, isErr) in lines {171                    console.append(isErr ? "⚠︎ " + line : line)172                    if console.count > 2000 { console.removeFirst(500) }173                }174                let status = await exit.value175                running = false176                failed = status.code != 0177                if !failed { console.append("✓ Dataset prêt : \(outDir.path)") }178            } catch {179                running = false180                failed = true181                console.append("⚠︎ " + error.localizedDescription)182            }183        }184    }185}186