diff --git a/Applications/LLMEval/ContentView.swift b/Applications/LLMEval/ContentView.swift index b050d99..99f7094 100644 --- a/Applications/LLMEval/ContentView.swift +++ b/Applications/LLMEval/ContentView.swift @@ -148,9 +148,9 @@ struct ContentView: View { } @Observable +@MainActor class LLMEvaluator { - @MainActor var running = false var output = "" @@ -172,91 +172,87 @@ class LLMEvaluator { enum LoadState { case idle - case loaded(LLMModel, Tokenizers.Tokenizer) + case loaded(ModelContainer) } var loadState = LoadState.idle /// load and return the model -- can be called multiple times, subsequent calls will /// just return the loaded model - func load() async throws -> (LLMModel, Tokenizers.Tokenizer) { + func load() async throws -> ModelContainer { switch loadState { case .idle: // limit the buffer cache MLX.GPU.set(cacheLimit: 20 * 1024 * 1024) - let (model, tokenizer) = try await LLM.load(configuration: modelConfiguration) { + let modelContainer = try await LLM.loadModelContainer(configuration: modelConfiguration) + { [modelConfiguration] progress in - DispatchQueue.main.sync { + Task { @MainActor in self.modelInfo = "Downloading \(modelConfiguration.name): \(Int(progress.fractionCompleted * 100))%" } } self.modelInfo = "Loaded \(modelConfiguration.id). Weights: \(MLX.GPU.activeMemory / 1024 / 1024)M" - loadState = .loaded(model, tokenizer) - return (model, tokenizer) + loadState = .loaded(modelContainer) + return modelContainer - case .loaded(let model, let tokenizer): - return (model, tokenizer) + case .loaded(let modelContainer): + return modelContainer } } func generate(prompt: String) async { - let canGenerate = await MainActor.run { - if running { - return false - } else { - running = true - self.output = "" - return true - } - } + guard !running else { return } - guard canGenerate else { return } + running = true + self.output = "" do { - let (model, tokenizer) = try await load() + let modelContainer = try await load() + // augment the prompt as needed let prompt = modelConfiguration.prepare(prompt: prompt) - let promptTokens = tokenizer.encode(text: prompt) + + let promptTokens = await modelContainer.perform { _, tokenizer in + tokenizer.encode(text: prompt) + } // each time you generate you will get something new MLXRandom.seed(UInt64(Date.timeIntervalSinceReferenceDate * 1000)) - let result = await LLM.generate( - promptTokens: promptTokens, parameters: generateParameters, model: model, - tokenizer: tokenizer, extraEOSTokens: modelConfiguration.extraEOSTokens - ) { tokens in - // update the output -- this will make the view show the text as it generates - if tokens.count % displayEveryNTokens == 0 { - let text = tokenizer.decode(tokens: tokens) - await MainActor.run { - self.output = text + let result = await modelContainer.perform { model, tokenizer in + LLM.generate( + promptTokens: promptTokens, parameters: generateParameters, model: model, + tokenizer: tokenizer, extraEOSTokens: modelConfiguration.extraEOSTokens + ) { tokens in + // update the output -- this will make the view show the text as it generates + if tokens.count % displayEveryNTokens == 0 { + let text = tokenizer.decode(tokens: tokens) + Task { @MainActor in + self.output = text + } } - } - if tokens.count >= maxTokens { - return .stop - } else { - return .more + if tokens.count >= maxTokens { + return .stop + } else { + return .more + } } } // update the text if needed, e.g. we haven't displayed because of displayEveryNTokens - await MainActor.run { - if result.output != self.output { - self.output = result.output - } - running = false - self.stat = " Tokens/second: \(String(format: "%.3f", result.tokensPerSecond))" + if result.output != self.output { + self.output = result.output } + self.stat = " Tokens/second: \(String(format: "%.3f", result.tokensPerSecond))" } catch { - await MainActor.run { - running = false - output = "Failed: \(error)" - } + output = "Failed: \(error)" } + + running = false } } diff --git a/Applications/LLMEval/ViewModels/DeviceStat.swift b/Applications/LLMEval/ViewModels/DeviceStat.swift index 7bf322e..f1dca3c 100644 --- a/Applications/LLMEval/ViewModels/DeviceStat.swift +++ b/Applications/LLMEval/ViewModels/DeviceStat.swift @@ -3,33 +3,22 @@ import LLM import MLX @Observable -class DeviceStat { +final class DeviceStat: @unchecked Sendable { + + @MainActor var gpuUsage = GPU.snapshot() - private var initialGPUSnapshot = GPU.snapshot() + + private let initialGPUSnapshot = GPU.snapshot() private var timer: Timer? init() { - startTimer() - } - - deinit { - stopTimer() - } - - private func startTimer() { - timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in - self?.updateStats() + self?.updateGPUUsages() } } - private func stopTimer() { + deinit { timer?.invalidate() - timer = nil - } - - private func updateStats() { - updateGPUUsages() } private func updateGPUUsages() { diff --git a/Applications/LoRATrainingExample/ContentView.swift b/Applications/LoRATrainingExample/ContentView.swift index 03eecc3..8100a83 100644 --- a/Applications/LoRATrainingExample/ContentView.swift +++ b/Applications/LoRATrainingExample/ContentView.swift @@ -2,6 +2,7 @@ import LLM import MLX +import MLXNN import MLXOptimizers import MLXRandom import SwiftUI @@ -94,25 +95,26 @@ struct ContentView: View { } /// Progress reporting with a title. -struct Progress: Equatable { +struct Progress: Equatable, Sendable { let title: String let current: Double? let limit: Double? } @Observable +@MainActor class LoRAEvaluator { - enum State { + enum State: Sendable { case idle case training case evaluate case failed(String) } - enum ModelState { + enum ModelState: Sendable { case idle - case loaded(LLMModel, Tokenizer) + case loaded(ModelContainer) } var state = State.idle @@ -131,7 +133,7 @@ class LoRAEvaluator { private let evaluateShowEvery = 8 private let maxTokens = 200 - private func loadModel() async throws -> (LLMModel, Tokenizer) { + private func loadModel() async throws -> ModelContainer { switch self.model { case .idle: let name = modelConfiguration.name @@ -139,22 +141,21 @@ class LoRAEvaluator { progress = .init(title: "Loading \(name)", current: 0, limit: 1) } - let (model, tokenizer) = try await LLM.load(configuration: modelConfiguration) { + let modelContainer = try await LLM.loadModelContainer(configuration: modelConfiguration) + { progress in - if progress.fractionCompleted < 1.0 { - DispatchQueue.main.sync { - self.progress = .init( - title: "Download \(name)", current: progress.fractionCompleted, - limit: 1.0) - } + Task { @MainActor in + self.progress = .init( + title: "Download \(name)", current: progress.fractionCompleted, + limit: 1.0) } } eval(model) - self.model = .loaded(model, tokenizer) - return (model, tokenizer) + self.model = .loaded(modelContainer) + return modelContainer - case .loaded(let model, let tokenizer): - return (model, tokenizer) + case .loaded(let modelContainer): + return modelContainer } } @@ -173,6 +174,17 @@ class LoRAEvaluator { } } + nonisolated private func loraLayers(model: Module) -> LoRALinearLayers { + guard let layerProvider = model as? LoRAModel else { + // the layerProvider will indicate which Linear layers need to be replaced + fatalError( + "Model \(type(of: model)) (\(modelConfiguration.name)) must implement the LoRALayerProvider protocol" + ) + } + + return Array(layerProvider.loraLinearLayers().suffix(loraLayers)) + } + private func startInner() async throws { // setup GPU.set(cacheLimit: 32 * 1024 * 1024) @@ -182,15 +194,13 @@ class LoRAEvaluator { } // load the model - let (model, tokenizer) = try await loadModel() + let modelContainer = try await loadModel() // apply LoRA adapters and train - guard let layerProvider = model as? LoRAModel else { - state = .failed("Model must implement the LoRALayerProvider protocol") - return + await modelContainer.perform { model, _ in + LoRATrain.convert( + model: model, layers: loraLayers(model: model)) } - LoRATrain.convert( - model: model, layers: Array(layerProvider.loraLinearLayers().suffix(loraLayers))) let train = try loadLoRAData(name: "train") let valid = try loadLoRAData(name: "valid") @@ -199,45 +209,47 @@ class LoRAEvaluator { return } - let optimizer = Adam(learningRate: learningRate) - try await LoRATrain.train( - model: model, train: train, validate: valid, optimizer: optimizer, tokenizer: tokenizer, - parameters: parameters - ) { progress in - await MainActor.run { - switch progress { - case .train(let i, _, _, _): - self.progress = .init( - title: "Train", current: Double(i), limit: Double(parameters.iterations)) - case .validation: - output += "\n" - default: - break + try await modelContainer.perform { model, tokenizer in + let optimizer = Adam(learningRate: learningRate) + try LoRATrain.train( + model: model, train: train, validate: valid, optimizer: optimizer, + tokenizer: tokenizer, + parameters: parameters + ) { progress in + Task { @MainActor in + switch progress { + case .train(let i, _, _, _): + self.progress = .init( + title: "Train", current: Double(i), limit: Double(parameters.iterations) + ) + case .validation: + output += "\n" + default: + break + } + output += progress.description + "\n" } - output += progress.description + "\n" + return .more } - - return .more } // done training, test - await MainActor.run { - self.progress = .init(title: "Testing", current: nil, limit: nil) - } + self.progress = .init(title: "Testing", current: nil, limit: nil) guard let test = try loadLoRAData(name: "test") else { state = .failed("Failed to load test data") return } - let loss = LoRATrain.evaluate( - model: model, dataset: test, tokenizer: tokenizer, batchSize: 1, batchCount: 0) - await MainActor.run { - self.progress = nil - self.output += "\n" - self.output += "Test loss \(loss.formatted()), ppl \(exp(loss).formatted())\n" - self.state = .evaluate + let loss = await modelContainer.perform { model, tokenizer in + LoRATrain.evaluate( + model: model, dataset: test, tokenizer: tokenizer, batchSize: 1, batchCount: 0) } + + self.progress = nil + self.output += "\n" + self.output += "Test loss \(loss.formatted()), ppl \(exp(loss).formatted())\n" + self.state = .evaluate } func evaluate(prompt: String) async { @@ -256,30 +268,32 @@ class LoRAEvaluator { MLXRandom.seed(UInt64(Date.timeIntervalSinceReferenceDate * 1000)) - let (model, tokenizer) = try await loadModel() + let modelContainer = try await loadModel() // prepare the prompt let preparedPrompt = modelConfiguration.prepare(prompt: prompt) - let promptTokens = tokenizer.encode(text: preparedPrompt) + let promptTokens = await modelContainer.perform { _, tokenizer in + tokenizer.encode(text: preparedPrompt) + } // evaluate - let result = await LLM.generate( - promptTokens: promptTokens, parameters: generateParameters, model: model, - tokenizer: tokenizer, - extraEOSTokens: modelConfiguration.extraEOSTokens, - didGenerate: { tokens in - if tokens.count % evaluateShowEvery == 0 { - let fullOutput = tokenizer.decode(tokens: tokens) - await MainActor.run { - self.output = fullOutput + let result = await modelContainer.perform { model, tokenizer in + LLM.generate( + promptTokens: promptTokens, parameters: generateParameters, model: model, + tokenizer: tokenizer, + extraEOSTokens: modelConfiguration.extraEOSTokens, + didGenerate: { tokens in + if tokens.count % evaluateShowEvery == 0 { + let fullOutput = tokenizer.decode(tokens: tokens) + Task { @MainActor in + self.output = fullOutput + } } - } - return tokens.count >= maxTokens ? .stop : .more - }) - - await MainActor.run { - self.output = result.output - self.progress = nil + return tokens.count >= maxTokens ? .stop : .more + }) } + + self.output = result.output + self.progress = nil } } diff --git a/Applications/MNISTTrainer/ContentView.swift b/Applications/MNISTTrainer/ContentView.swift index 4827d80..774f5b1 100644 --- a/Applications/MNISTTrainer/ContentView.swift +++ b/Applications/MNISTTrainer/ContentView.swift @@ -9,7 +9,7 @@ import SwiftUI struct TrainingView: View { - @Binding var trainer: Trainer + @Binding var trainer: ModelState var body: some View { VStack { @@ -27,7 +27,7 @@ struct TrainingView: View { case .untrained: Button("Train") { Task { - try! await trainer.run() + try! await trainer.train() } } case .trained(let model), .predict(let model): @@ -46,7 +46,7 @@ struct TrainingView: View { struct ContentView: View { // the training loop - @State var trainer = Trainer() + @State var trainer = ModelState() var body: some View { switch trainer.state { @@ -58,19 +58,33 @@ struct ContentView: View { } } +@MainActor @Observable -class Trainer { +class ModelState { enum State { case untrained - case trained(LeNet) - case predict(LeNet) + case trained(LeNetContainer) + case predict(LeNetContainer) } var state: State = .untrained var messages = [String]() - func run() async throws { + func train() async throws { + let model = LeNetContainer() + try await model.train(output: self) + self.state = .trained(model) + } +} + +actor LeNetContainer { + + private let model = LeNet() + + let mnistImageSize: CGSize = CGSize(width: 28, height: 28) + + func train(output: ModelState) async throws { // Note: this is pretty close to the code in `mnist-tool`, just // wrapped in an Observable to make it easy to display in SwiftUI @@ -117,18 +131,26 @@ class Trainer { let end = Date.timeIntervalSinceReferenceDate // add to messages -- triggers display + let accuracyItem = accuracy.item(Float.self) await MainActor.run { - messages.append( + output.messages.append( """ - Epoch \(e): test accuracy \(accuracy.item(Float.self).formatted()) + Epoch \(e): test accuracy \(accuracyItem.formatted()) Time: \((end - start).formatted()) """ ) } } - await MainActor.run { - state = .trained(model) + } + + func evaluate(image: CGImage) -> Int? { + let pixelData = image.grayscaleImage(with: mnistImageSize)?.pixelData() + if let pixelData { + let x = pixelData.reshaped([1, 28, 28, 1]).asType(.float32) / 255.0 + return argMax(model(x)).item() + } else { + return nil } } } diff --git a/Applications/MNISTTrainer/PredictionView.swift b/Applications/MNISTTrainer/PredictionView.swift index 67906f2..5aa8353 100644 --- a/Applications/MNISTTrainer/PredictionView.swift +++ b/Applications/MNISTTrainer/PredictionView.swift @@ -54,7 +54,7 @@ extension Path { struct PredictionView: View { @State var path: Path = Path() @State var prediction: Int? - let model: LeNet + let model: LeNetContainer let canvasSize = 150.0 let mnistImageSize: CGSize = CGSize(width: 28, height: 28) @@ -84,14 +84,12 @@ struct PredictionView: View { func predict() { let imageRenderer = ImageRenderer( content: Canvas(path: $path).frame(width: 150, height: 150)) - guard - let pixelData = imageRenderer.cgImage?.grayscaleImage(with: mnistImageSize)?.pixelData() - else { - return + + if let image = imageRenderer.cgImage { + Task { + self.prediction = await model.evaluate(image: image) + } } - // modify input vector to match training in MNIST/Files.swift - let x = pixelData.reshaped([1, 28, 28, 1]).asType(.float32) / 255.0 - prediction = argMax(model(x)).item() } } diff --git a/Libraries/LLM/Cohere.swift b/Libraries/LLM/Cohere.swift index e68f02a..d331fde 100644 --- a/Libraries/LLM/Cohere.swift +++ b/Libraries/LLM/Cohere.swift @@ -178,7 +178,7 @@ public class CohereModel: Module, LLMModel { } } -public struct CohereConfiguration: Codable { +public struct CohereConfiguration: Codable, Sendable { var hiddenSize: Int var hiddenLayers: Int diff --git a/Libraries/LLM/Configuration.swift b/Libraries/LLM/Configuration.swift index 258cb6d..fac8066 100644 --- a/Libraries/LLM/Configuration.swift +++ b/Libraries/LLM/Configuration.swift @@ -2,7 +2,7 @@ import Foundation -public enum StringOrNumber: Codable, Equatable { +public enum StringOrNumber: Codable, Equatable, Sendable { case string(String) case float(Float) @@ -26,7 +26,7 @@ public enum StringOrNumber: Codable, Equatable { } } -public enum ModelType: String, Codable { +public enum ModelType: String, Codable, Sendable { case mistral case llama case phi @@ -80,10 +80,10 @@ public enum ModelType: String, Codable { } } -public struct BaseConfiguration: Codable { +public struct BaseConfiguration: Codable, Sendable { public let modelType: ModelType - public struct Quantization: Codable { + public struct Quantization: Codable, Sendable { public init(groupSize: Int, bits: Int) { self.groupSize = groupSize self.bits = bits diff --git a/Libraries/LLM/Evaluate.swift b/Libraries/LLM/Evaluate.swift index 9a1ae63..cebe92f 100644 --- a/Libraries/LLM/Evaluate.swift +++ b/Libraries/LLM/Evaluate.swift @@ -55,7 +55,7 @@ private func sample(logits: MLXArray, temp: Float, topP: Float = 1.0) -> MLXArra } /// Parameters for text generation, see ``TokenIterator`` -public struct GenerateParameters { +public struct GenerateParameters: Sendable { /// sampling temperature public var temperature: Float = 0.6 @@ -129,7 +129,7 @@ public struct TokenIterator: Sequence, IteratorProtocol { } } -public struct GenerateResult { +public struct GenerateResult: Sendable { /// input tokens public let promptTokens: [Int] @@ -161,7 +161,7 @@ public struct GenerateResult { } } -public enum GenerateDisposition { +public enum GenerateDisposition: Sendable { case more case stop } @@ -178,8 +178,8 @@ public enum GenerateDisposition { public func generate( promptTokens: [Int], parameters: GenerateParameters, model: LLMModel, tokenizer: Tokenizer, extraEOSTokens: Set? = nil, - didGenerate: ([Int]) async -> GenerateDisposition -) async -> GenerateResult { + didGenerate: ([Int]) -> GenerateDisposition +) -> GenerateResult { var start = Date.timeIntervalSinceReferenceDate var promptTime: TimeInterval = 0 @@ -211,7 +211,7 @@ public func generate( tokens.append(t) - if await didGenerate(tokens) == .stop { + if didGenerate(tokens) == .stop { break } } diff --git a/Libraries/LLM/Gemma.swift b/Libraries/LLM/Gemma.swift index 14a96b1..cfaac76 100644 --- a/Libraries/LLM/Gemma.swift +++ b/Libraries/LLM/Gemma.swift @@ -200,7 +200,7 @@ public class GemmaModel: Module, LLMModel { } } -public struct GemmaConfiguration: Codable { +public struct GemmaConfiguration: Codable, Sendable { var hiddenSize: Int var hiddenLayers: Int diff --git a/Libraries/LLM/LLMModel.swift b/Libraries/LLM/LLMModel.swift index 241cbba..ece5509 100644 --- a/Libraries/LLM/LLMModel.swift +++ b/Libraries/LLM/LLMModel.swift @@ -3,8 +3,48 @@ import Foundation import MLX import MLXNN +import Tokenizers -// Interface for all LLM Models +/// Container for models that guarantees single threaded access. +/// +/// Wrap models used by e.g. the UI in a ModelContainer. Callers can access +/// the model and/or tokenizer: +/// +/// ```swift +/// let promptTokens = await modelContainer.perform { _, tokenizer in +/// tokenizer.encode(text: prompt) +/// } +/// ``` +/// +/// or: +/// +/// ```swift +/// let result = await modelContainer.perform { model, tokenizer in +/// LLM.generate( +/// promptTokens: promptTokens, parameters: generateParameters, model: model, +/// tokenizer: tokenizer, extraEOSTokens: modelConfiguration.extraEOSTokens +/// ) { tokens in +/// ... +/// } +/// } +/// ``` +public actor ModelContainer { + let model: LLMModel + let tokenizer: Tokenizer + + public init(model: LLMModel, tokenizer: Tokenizer) { + self.model = model + self.tokenizer = tokenizer + } + + /// Perform an action on the model and/or tokenizer. Callers _must_ eval any `MLXArray` before returning as + /// `MLXArray` is not `Sendable`. + public func perform(_ action: @Sendable (LLMModel, Tokenizer) throws -> R) rethrows -> R { + try action(model, tokenizer) + } +} + +/// Interface for all LLM Models public protocol LLMModel: Module { var vocabularySize: Int { get } diff --git a/Libraries/LLM/Llama.swift b/Libraries/LLM/Llama.swift index c948b60..bbdd73a 100644 --- a/Libraries/LLM/Llama.swift +++ b/Libraries/LLM/Llama.swift @@ -283,7 +283,7 @@ public class LlamaModel: Module, LLMModel { } } -public struct LlamaConfiguration: Codable { +public struct LlamaConfiguration: Codable, Sendable { var hiddenSize: Int var hiddenLayers: Int diff --git a/Libraries/LLM/Load.swift b/Libraries/LLM/Load.swift index 8d25b24..0f8e7b6 100644 --- a/Libraries/LLM/Load.swift +++ b/Libraries/LLM/Load.swift @@ -2,7 +2,7 @@ import AsyncAlgorithms import Foundation -import Hub +@preconcurrency import Hub import MLX import MLXNN import MLXRandom @@ -15,7 +15,7 @@ struct LLMError: Error { /// Load and return the model and tokenizer public func load( hub: HubApi = HubApi(), configuration: ModelConfiguration, - progressHandler: @escaping (Progress) -> Void = { _ in } + progressHandler: @Sendable @escaping (Progress) -> Void = { _ in } ) async throws -> (LLMModel, Tokenizer) { do { let tokenizer = try await loadTokenizer(configuration: configuration, hub: hub) @@ -82,3 +82,12 @@ public func load( hub: hub, configuration: newConfiguration, progressHandler: progressHandler) } } + +public func loadModelContainer( + hub: HubApi = HubApi(), configuration: ModelConfiguration, + progressHandler: @Sendable @escaping (Progress) -> Void = { _ in } +) async throws -> ModelContainer { + let (model, tokenizer) = try await load( + hub: hub, configuration: configuration, progressHandler: progressHandler) + return ModelContainer(model: model, tokenizer: tokenizer) +} diff --git a/Libraries/LLM/Lora.swift b/Libraries/LLM/Lora.swift index 81438c9..65c8e72 100644 --- a/Libraries/LLM/Lora.swift +++ b/Libraries/LLM/Lora.swift @@ -325,7 +325,7 @@ public enum LoRATrain { ) /// LoRA training parameters - public struct Parameters { + public struct Parameters: Sendable { /// number of prompts to evaluate per iteration public var batchSize = 4 @@ -506,7 +506,7 @@ public enum LoRATrain { try save(arrays: parameters, url: url) } - public enum Progress: CustomStringConvertible { + public enum Progress: CustomStringConvertible, Sendable { case train( iteration: Int, trainingLoss: Float, iterationsPerSecond: Double, tokensPerSecond: Double) @@ -530,7 +530,7 @@ public enum LoRATrain { } } - public enum ProgressDisposition { + public enum ProgressDisposition: Sendable { case stop case more } @@ -549,8 +549,8 @@ public enum LoRATrain { public static func train( model: Module, train: [String], validate: [String], optimizer: Optimizer, loss: @escaping LoraLossFunction = loss, tokenizer: Tokenizer, parameters: Parameters, - progress: (Progress) async -> ProgressDisposition - ) async throws { + progress: (Progress) -> ProgressDisposition + ) throws { // def train(model, train_set, val_set, optimizer, loss, tokenizer, args) let lossValueGrad = valueAndGrad(model: model) { model, arrays in @@ -587,7 +587,7 @@ public enum LoRATrain { let iterationsPerSecond = Double(parameters.stepsPerReport) / (now - start) let tokensPerSecond = Double(tokenCount) / (now - start) - if await progress( + if progress( .train( iteration: iteration, trainingLoss: trainingLoss, iterationsPerSecond: iterationsPerSecond, tokensPerSecond: tokensPerSecond)) @@ -609,7 +609,7 @@ public enum LoRATrain { batchSize: parameters.batchSize, batchCount: parameters.validationBatches) let now = Date.timeIntervalSinceReferenceDate - if await progress( + if progress( .validation( iteration: iteration, validationLoss: validationLoss, validationTime: now - validationStart)) == .stop @@ -624,7 +624,7 @@ public enum LoRATrain { if let adapterURL = parameters.adapterURL, (iteration + 1) % parameters.saveEvery == 0 { try saveLoRAWeights(model: model, url: adapterURL) - if await progress(.save(iteration: iteration, url: adapterURL)) == .stop { + if progress(.save(iteration: iteration, url: adapterURL)) == .stop { break } diff --git a/Libraries/LLM/Models.swift b/Libraries/LLM/Models.swift index 62581d4..2579948 100644 --- a/Libraries/LLM/Models.swift +++ b/Libraries/LLM/Models.swift @@ -9,9 +9,9 @@ import Hub /// The python tokenizers have a very rich set of implementations and configuration. The /// swift-tokenizers code handles a good chunk of that and this is a place to augment that /// implementation, if needed. -public struct ModelConfiguration { +public struct ModelConfiguration: Sendable { - public enum Identifier { + public enum Identifier: Sendable { case id(String) case directory(URL) } @@ -42,13 +42,13 @@ public struct ModelConfiguration { /// custom preparation logic for the prompt. custom tokenizers provide more capability, but this /// allows some minor formtting changes, e.g. wrapping the user input in the expected prompt /// format - private let preparePrompt: ((String) -> String)? + private let preparePrompt: (@Sendable (String) -> String)? public init( id: String, tokenizerId: String? = nil, overrideTokenizer: String? = nil, defaultPrompt: String = "hello", extraEOSTokens: Set = [], - preparePrompt: ((String) -> String)? = nil + preparePrompt: (@Sendable (String) -> String)? = nil ) { self.id = .id(id) self.tokenizerId = tokenizerId @@ -62,7 +62,7 @@ public struct ModelConfiguration { directory: URL, tokenizerId: String? = nil, overrideTokenizer: String? = nil, defaultPrompt: String = "hello", extraEOSTokens: Set = [], - preparePrompt: ((String) -> String)? = nil + preparePrompt: (@Sendable (String) -> String)? = nil ) { self.id = .directory(directory) self.tokenizerId = tokenizerId @@ -88,8 +88,10 @@ public struct ModelConfiguration { } } + @MainActor public static var registry = [String: ModelConfiguration]() + @MainActor public static func register(configurations: [ModelConfiguration]) { bootstrap() @@ -98,6 +100,7 @@ public struct ModelConfiguration { } } + @MainActor public static func configuration(id: String) -> ModelConfiguration { bootstrap() @@ -226,14 +229,16 @@ extension ModelConfiguration { "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\nYou are a helpful assistant<|eot_id|>\n<|start_header_id|>user<|end_header_id|>\n\(prompt)<|eot_id|>\n<|start_header_id|>assistant<|end_header_id|>" } - private enum BootstrapState { + private enum BootstrapState: Sendable { case idle case bootstrapping case bootstrapped } + @MainActor static private var bootstrapState = BootstrapState.idle + @MainActor static func bootstrap() { switch bootstrapState { case .idle: diff --git a/Libraries/LLM/OpenELM.swift b/Libraries/LLM/OpenELM.swift index b49587e..be8c85a 100644 --- a/Libraries/LLM/OpenELM.swift +++ b/Libraries/LLM/OpenELM.swift @@ -218,7 +218,7 @@ public class OpenELMModel: Module, LLMModel { } } -public struct OpenElmConfiguration: Codable { +public struct OpenElmConfiguration: Codable, Sendable { var modelType: String var headDimensions: Int var numTransformerLayers: Int diff --git a/Libraries/LLM/Phi.swift b/Libraries/LLM/Phi.swift index ae757c8..4c9c357 100644 --- a/Libraries/LLM/Phi.swift +++ b/Libraries/LLM/Phi.swift @@ -186,7 +186,7 @@ public class PhiModel: Module, LLMModel { } } -public struct PhiConfiguration: Codable { +public struct PhiConfiguration: Codable, Sendable { var maxPositionalEmbeddings = 2048 var vocabularySize = 51200 var hiddenSize = 2560 diff --git a/Libraries/LLM/Phi3.swift b/Libraries/LLM/Phi3.swift index d35d4de..5fc4b72 100644 --- a/Libraries/LLM/Phi3.swift +++ b/Libraries/LLM/Phi3.swift @@ -190,7 +190,7 @@ public class Phi3Model: Module, LLMModel { } } -public struct Phi3Configuration: Codable { +public struct Phi3Configuration: Codable, Sendable { var hiddenSize: Int var hiddenLayers: Int diff --git a/Libraries/LLM/Qwen2.swift b/Libraries/LLM/Qwen2.swift index 9a478de..c7af72c 100644 --- a/Libraries/LLM/Qwen2.swift +++ b/Libraries/LLM/Qwen2.swift @@ -216,7 +216,7 @@ public class Qwen2Model: Module, LLMModel { } } -public struct Qwen2Configuration: Codable { +public struct Qwen2Configuration: Codable, Sendable { var hiddenSize: Int var hiddenLayers: Int var intermediateSize: Int diff --git a/Libraries/LLM/Starcoder2.swift b/Libraries/LLM/Starcoder2.swift index ce1b1dd..ae44e5c 100644 --- a/Libraries/LLM/Starcoder2.swift +++ b/Libraries/LLM/Starcoder2.swift @@ -189,7 +189,7 @@ public class Starcoder2Model: Module, LLMModel { } } -public struct Starcoder2Configuration: Codable { +public struct Starcoder2Configuration: Codable, Sendable { var hiddenSize: Int var hiddenLayers: Int var intermediateSize: Int diff --git a/Libraries/MNIST/Files.swift b/Libraries/MNIST/Files.swift index 9164f69..a6ae063 100644 --- a/Libraries/MNIST/Files.swift +++ b/Libraries/MNIST/Files.swift @@ -6,17 +6,17 @@ import MLX // based on https://github.com/ml-explore/mlx-examples/blob/main/mnist/mnist.py -public enum Use: String, Hashable { +public enum Use: String, Hashable, Sendable { case test case training } -public enum DataKind: String, Hashable { +public enum DataKind: String, Hashable, Sendable { case images case labels } -public struct FileKind: Hashable, CustomStringConvertible { +public struct FileKind: Hashable, CustomStringConvertible, Sendable { let use: Use let data: DataKind @@ -30,15 +30,15 @@ public struct FileKind: Hashable, CustomStringConvertible { } } -struct LoadInfo { +struct LoadInfo: Sendable { let name: String let offset: Int - let convert: (MLXArray) -> MLXArray + let convert: @Sendable (MLXArray) -> MLXArray } let baseURL = URL(string: "http://yann.lecun.com/exdb/mnist/")! -let files = [ +private let files = [ FileKind(.training, .images): LoadInfo( name: "train-images-idx3-ubyte.gz", offset: 16, diff --git a/Libraries/MNIST/Random.swift b/Libraries/MNIST/Random.swift index 66fb7e7..50dcafa 100644 --- a/Libraries/MNIST/Random.swift +++ b/Libraries/MNIST/Random.swift @@ -13,7 +13,7 @@ import Foundation // // Derived from public domain C implementation by Sebastiano Vigna // See http://xoshiro.di.unimi.it/splitmix64.c -public struct SplitMix64: RandomNumberGenerator { +public struct SplitMix64: RandomNumberGenerator, Sendable { private var state: UInt64 public init(seed: UInt64) { diff --git a/Package.swift b/Package.swift index 7e66105..0c381d0 100644 --- a/Package.swift +++ b/Package.swift @@ -15,8 +15,8 @@ let package = Package( targets: ["MLXMNIST"]), ], dependencies: [ - .package(url: "https://github.com/ml-explore/mlx-swift", from: "0.12.1"), - .package(url: "https://github.com/huggingface/swift-transformers", from: "0.1.8"), + .package(url: "https://github.com/ml-explore/mlx-swift", from: "0.16.0"), + .package(url: "https://github.com/huggingface/swift-transformers", from: "0.1.9"), .package(url: "https://github.com/1024jp/GzipSwift", "6.0.1" ... "6.0.1"), .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), ], diff --git a/Tools/LinearModelTraining/LinearModelTraining.swift b/Tools/LinearModelTraining/LinearModelTraining.swift index cf25724..fb10704 100644 --- a/Tools/LinearModelTraining/LinearModelTraining.swift +++ b/Tools/LinearModelTraining/LinearModelTraining.swift @@ -7,11 +7,19 @@ import MLXNN import MLXOptimizers import MLXRandom -extension MLX.DeviceType: ExpressibleByArgument { - public init?(argument: String) { - self.init(rawValue: argument) +#if swift(>=6.0) + extension MLX.DeviceType: @retroactive ExpressibleByArgument { + public init?(argument: String) { + self.init(rawValue: argument) + } } -} +#else + extension MLX.DeviceType: ExpressibleByArgument { + public init?(argument: String) { + self.init(rawValue: argument) + } + } +#endif @main struct Train: AsyncParsableCommand { diff --git a/Tools/llm-tool/Arguments.swift b/Tools/llm-tool/Arguments.swift index 5f67a67..281dcd6 100644 --- a/Tools/llm-tool/Arguments.swift +++ b/Tools/llm-tool/Arguments.swift @@ -4,12 +4,24 @@ import ArgumentParser import Foundation /// Extension to allow URL command line arguments. -extension URL: ExpressibleByArgument { - public init?(argument: String) { - if argument.contains("://") { - self.init(string: argument) - } else { - self.init(filePath: argument) +#if swift(>=6.0) + extension URL: @retroactive ExpressibleByArgument { + public init?(argument: String) { + if argument.contains("://") { + self.init(string: argument) + } else { + self.init(filePath: argument) + } } } -} +#else + extension URL: ExpressibleByArgument { + public init?(argument: String) { + if argument.contains("://") { + self.init(string: argument) + } else { + self.init(filePath: argument) + } + } + } +#endif diff --git a/Tools/llm-tool/LLMTool.swift b/Tools/llm-tool/LLMTool.swift index 7b9d47f..6c54058 100644 --- a/Tools/llm-tool/LLMTool.swift +++ b/Tools/llm-tool/LLMTool.swift @@ -9,19 +9,20 @@ import Tokenizers @main struct LLMTool: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( abstract: "Command line tool for generating text and manipulating LLMs", subcommands: [EvaluateCommand.self, LoRACommand.self], defaultSubcommand: EvaluateCommand.self) } /// Command line arguments for loading a model. -struct ModelArguments: ParsableArguments { +struct ModelArguments: ParsableArguments, Sendable { @Option(name: .long, help: "Name of the huggingface model or absolute path to directory") var model: String = "mlx-community/Mistral-7B-v0.1-hf-4bit-mlx" - func load() async throws -> (LLMModel, Tokenizer, ModelConfiguration) { + @Sendable + func load() async throws -> (ModelContainer, ModelConfiguration) { let modelConfiguration: ModelConfiguration if self.model.hasPrefix("/") { @@ -29,15 +30,15 @@ struct ModelArguments: ParsableArguments { modelConfiguration = ModelConfiguration(directory: URL(filePath: self.model)) } else { // identifier - modelConfiguration = ModelConfiguration.configuration(id: model) + modelConfiguration = await ModelConfiguration.configuration(id: model) } - let (model, tokenizer) = try await LLM.load(configuration: modelConfiguration) - return (model, tokenizer, modelConfiguration) + let modelContainer = try await LLM.loadModelContainer(configuration: modelConfiguration) + return (modelContainer, modelConfiguration) } } /// Command line arguments for controlling generation of text. -struct GenerateArguments: ParsableArguments { +struct GenerateArguments: ParsableArguments, Sendable { @Option( name: .shortAndLong, @@ -98,13 +99,13 @@ struct GenerateArguments: ParsableArguments { func generate( promptTokens: [Int], model: LLMModel, tokenizer: Tokenizer, extraEOSTokens: Set? = nil - ) async + ) -> GenerateResult { // track how much we have printed var printed = 0 - return await LLM.generate( + return LLM.generate( promptTokens: promptTokens, parameters: generateParameters, model: model, tokenizer: tokenizer, extraEOSTokens: extraEOSTokens ) { tokens in @@ -128,7 +129,7 @@ struct GenerateArguments: ParsableArguments { } /// Argument package for adjusting and reporting memory use. -struct MemoryArguments: ParsableArguments { +struct MemoryArguments: ParsableArguments, Sendable { @Flag(name: .long, help: "Show memory stats") var memoryStats = false @@ -204,7 +205,7 @@ struct MemoryArguments: ParsableArguments { struct EvaluateCommand: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "eval", abstract: "evaluate prompt and generate text" ) @@ -215,23 +216,27 @@ struct EvaluateCommand: AsyncParsableCommand { @MainActor mutating func run() async throws { - let (model, tokenizer, modelConfiguration) = try await memory.start(args.load) + let (modelContainer, modelConfiguration) = try await memory.start(args.load) if !generate.quiet { print("Model loaded -> \(modelConfiguration.id)") } - let (prompt, promptTokens) = try generate.tokenizePrompt( - configuration: modelConfiguration, tokenizer: tokenizer) + let (prompt, promptTokens) = try await modelContainer.perform { [generate] _, tokenizer in + try generate.tokenizePrompt( + configuration: modelConfiguration, tokenizer: tokenizer) + } if !generate.quiet { print("Starting generation ...") print(prompt, terminator: "") } - let result = await generate.generate( - promptTokens: promptTokens, model: model, tokenizer: tokenizer, - extraEOSTokens: modelConfiguration.extraEOSTokens) + let result = await modelContainer.perform { [generate] model, tokenizer in + generate.generate( + promptTokens: promptTokens, model: model, tokenizer: tokenizer, + extraEOSTokens: modelConfiguration.extraEOSTokens) + } print() if !generate.quiet { diff --git a/Tools/llm-tool/LoraCommands.swift b/Tools/llm-tool/LoraCommands.swift index 4c422e5..deecc57 100644 --- a/Tools/llm-tool/LoraCommands.swift +++ b/Tools/llm-tool/LoraCommands.swift @@ -12,7 +12,7 @@ import Tokenizers struct LoRACommand: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "lora", abstract: "LoRA commands", subcommands: [ @@ -22,7 +22,7 @@ struct LoRACommand: AsyncParsableCommand { } /// Common arguments for loading a LoRA mdoel with adapter weights -struct LoRAModelArguments: ParsableArguments { +struct LoRAModelArguments: ParsableArguments, Sendable { @OptionGroup var args: ModelArguments @@ -35,13 +35,15 @@ struct LoRAModelArguments: ParsableArguments { /// Load the model and apply the LoRA adapters. /// /// This does not load the adapter weights as they may not exist yet. - func load() async throws -> (LLMModel, Tokenizer, ModelConfiguration) { - let (model, tokenizer, modelConfiguration) = try await args.load() + func load() async throws -> (ModelContainer, ModelConfiguration) { + let (modelContainer, modelConfiguration) = try await args.load() // convert some of the Linear layers to LoRALinear - LoRATrain.convert(model: model, layers: loraLayers(model: model)) + await modelContainer.perform { model, _ in + LoRATrain.convert(model: model, layers: loraLayers(model: model)) + } - return (model, tokenizer, modelConfiguration) + return (modelContainer, modelConfiguration) } func loraLayers(model: Module) -> LoRALinearLayers { @@ -72,7 +74,7 @@ struct LoRAModelArguments: ParsableArguments { struct LoRATrainCommand: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "train", abstract: "LoRA training" ) @@ -121,14 +123,18 @@ struct LoRATrainCommand: AsyncParsableCommand { @MainActor mutating func run() async throws { - let (model, tokenizer, _) = try await args.load() - args.describe(model: model) + let (modelContainer, _) = try await args.load() + await modelContainer.perform { [args] model, _ in + args.describe(model: model) + } memory.start() if resume { print("Loading pretrained adapters from \(args.adapter.path())") - try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + try await modelContainer.perform { [args] model, _ in + try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + } } // load the train/validation data @@ -143,21 +149,24 @@ struct LoRATrainCommand: AsyncParsableCommand { } // train - let optimizer = Adam(learningRate: learningRate) - try await LoRATrain.train( - model: model, train: train, validate: valid, optimizer: optimizer, tokenizer: tokenizer, - parameters: parameters - ) { progress in - print(progress) - return .more + try await modelContainer.perform { [args, parameters, learningRate] model, tokenizer in + let optimizer = Adam(learningRate: learningRate) + try LoRATrain.train( + model: model, train: train, validate: valid, optimizer: optimizer, + tokenizer: tokenizer, + parameters: parameters + ) { progress in + print(progress) + return .more + } + try LoRATrain.saveLoRAWeights(model: model, url: args.adapter) } - try LoRATrain.saveLoRAWeights(model: model, url: args.adapter) } } struct LoRAFuseCommand: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "fuse", abstract: "Fuse lora adapter weights back in to original model" ) @@ -180,13 +189,18 @@ struct LoRAFuseCommand: AsyncParsableCommand { outputURL = HubApi().localRepoLocation(repo) } - let (model, _, modelConfiguration) = try await args.load() + let (modelContainer, modelConfiguration) = try await args.load() // load the prepared weights - try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + try await modelContainer.perform { [args] model, _ in + try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + } // fuse them back into Linear/QuantizedLinear - LoRATrain.fuse(model: model, layers: args.loraLayers(model: model), deQuantize: deQuantize) + await modelContainer.perform { [args, deQuantize] model, _ in + LoRATrain.fuse( + model: model, layers: args.loraLayers(model: model), deQuantize: deQuantize) + } // make the new directory and copy files from source model try FileManager.default.createDirectory(at: outputURL, withIntermediateDirectories: true) @@ -204,8 +218,10 @@ struct LoRAFuseCommand: AsyncParsableCommand { } // write them back out - let weights = Dictionary(uniqueKeysWithValues: model.parameters().flattened()) - try save(arrays: weights, url: outputURL.appending(component: "weights.safetensors")) + try await modelContainer.perform { model, _ in + let weights = Dictionary(uniqueKeysWithValues: model.parameters().flattened()) + try save(arrays: weights, url: outputURL.appending(component: "weights.safetensors")) + } print("Fused weights written to \(outputURL.path())") print("Use with:\n\tllm-tool eval --model \(output)") @@ -215,7 +231,7 @@ struct LoRAFuseCommand: AsyncParsableCommand { struct LoRATestCommand: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "test", abstract: "LoRA testing" ) @@ -231,15 +247,22 @@ struct LoRATestCommand: AsyncParsableCommand { @MainActor mutating func run() async throws { - let (model, tokenizer, _) = try await args.load() - args.describe(model: model) - try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + let (modelContainer, _) = try await args.load() + await modelContainer.perform { [args] model, _ in + args.describe(model: model) + } + try await modelContainer.perform { [args] model, _ in + try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + } memory.start() let test = try loadLoRAData(directory: data, name: "test") - let loss = LoRATrain.evaluate( - model: model, dataset: test, tokenizer: tokenizer, batchSize: batchSize, batchCount: 0) + let loss = await modelContainer.perform { [batchSize] model, tokenizer in + LoRATrain.evaluate( + model: model, dataset: test, tokenizer: tokenizer, batchSize: batchSize, + batchCount: 0) + } print("Test loss \(loss.formatted()), ppl \(exp(loss).formatted())") } @@ -248,7 +271,7 @@ struct LoRATestCommand: AsyncParsableCommand { struct LoRAEvalCommand: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "eval", abstract: "LoRA evaluation" ) @@ -259,14 +282,20 @@ struct LoRAEvalCommand: AsyncParsableCommand { @MainActor mutating func run() async throws { - let (model, tokenizer, modelConfiguration) = try await args.load() - args.describe(model: model) - try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + let (modelContainer, modelConfiguration) = try await args.load() + await modelContainer.perform { [args] model, _ in + args.describe(model: model) + } + try await modelContainer.perform { [args] model, _ in + try LoRATrain.loadLoRAWeights(model: model, url: args.adapter) + } memory.start() - let (prompt, promptTokens) = try generate.tokenizePrompt( - configuration: modelConfiguration, tokenizer: tokenizer) + let (prompt, promptTokens) = try await modelContainer.perform { [generate] _, tokenizer in + try generate.tokenizePrompt( + configuration: modelConfiguration, tokenizer: tokenizer) + } if !generate.quiet { print("Starting generation ...") @@ -274,9 +303,11 @@ struct LoRAEvalCommand: AsyncParsableCommand { } // generate and print the result - let _ = await generate.generate( - promptTokens: promptTokens, model: model, tokenizer: tokenizer, - extraEOSTokens: modelConfiguration.extraEOSTokens) + await modelContainer.perform { [generate] model, tokenizer in + let _ = generate.generate( + promptTokens: promptTokens, model: model, tokenizer: tokenizer, + extraEOSTokens: modelConfiguration.extraEOSTokens) + } print() } } diff --git a/Tools/mnist-tool/MNISTTool.swift b/Tools/mnist-tool/MNISTTool.swift index 1199784..c401031 100644 --- a/Tools/mnist-tool/MNISTTool.swift +++ b/Tools/mnist-tool/MNISTTool.swift @@ -10,17 +10,25 @@ import MNIST @main struct MNISTTool: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( abstract: "Command line tool for training mnist models", subcommands: [Train.self], defaultSubcommand: Train.self) } -extension MLX.DeviceType: ExpressibleByArgument { - public init?(argument: String) { - self.init(rawValue: argument) +#if swift(>=6.0) + extension MLX.DeviceType: @retroactive ExpressibleByArgument { + public init?(argument: String) { + self.init(rawValue: argument) + } } -} +#else + extension MLX.DeviceType: ExpressibleByArgument { + public init?(argument: String) { + self.init(rawValue: argument) + } + } +#endif struct Train: AsyncParsableCommand { diff --git a/mlx-swift-examples.xcodeproj/project.pbxproj b/mlx-swift-examples.xcodeproj/project.pbxproj index 1ef0232..aa6ed38 100644 --- a/mlx-swift-examples.xcodeproj/project.pbxproj +++ b/mlx-swift-examples.xcodeproj/project.pbxproj @@ -1132,6 +1132,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_ASSET_PATHS = "\"Applications/LoRATrainingExample/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -1180,6 +1181,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; @@ -1224,6 +1226,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Applications/LoRATrainingExample/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -1265,6 +1268,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; @@ -1305,6 +1309,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1331,6 +1336,7 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -1370,6 +1376,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1389,6 +1396,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; @@ -1430,6 +1438,7 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1478,6 +1487,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -1522,6 +1532,7 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1563,6 +1574,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -1605,6 +1617,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1631,6 +1644,7 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -1670,6 +1684,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1689,6 +1704,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; @@ -1729,6 +1745,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1779,6 +1796,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; VERSIONING_SYSTEM = "apple-generic"; @@ -1822,6 +1840,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1865,6 +1884,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; VERSIONING_SYSTEM = "apple-generic"; @@ -1876,7 +1896,35 @@ isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEAD_CODE_STRIPPING = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; EXCLUDED_ARCHS = x86_64; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; ONLY_ACTIVE_ARCH = YES; }; name = Debug; @@ -1885,7 +1933,34 @@ isa = XCBuildConfiguration; buildSettings = { ARCHS = "$(ARCHS_STANDARD)"; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEAD_CODE_STRIPPING = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; EXCLUDED_ARCHS = x86_64; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; ONLY_ACTIVE_ARCH = YES; }; name = Release; @@ -1925,6 +2000,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1952,6 +2028,7 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -1991,6 +2068,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2010,6 +2088,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; @@ -2049,6 +2128,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -2075,6 +2155,7 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -2114,6 +2195,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2133,6 +2215,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; @@ -2176,6 +2259,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_ASSET_PATHS = "\"Applications/MNISTTrainer/Preview Content\""; ENABLE_PREVIEWS = YES; @@ -2223,6 +2307,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -2267,6 +2352,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Applications/MNISTTrainer/Preview Content\""; ENABLE_NS_ASSERTIONS = NO; @@ -2307,6 +2393,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -2351,6 +2438,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_ASSET_PATHS = "\"Applications/LLMEval/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -2398,6 +2486,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; @@ -2442,6 +2531,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Applications/LLMEval/Preview Content\""; DEVELOPMENT_TEAM = ""; @@ -2482,6 +2572,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; @@ -2587,8 +2678,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; requirement = { - branch = main; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 2.3.1; }; }; C34E491A2B69C43600FCB841 /* XCRemoteSwiftPackageReference "GzipSwift" */ = { @@ -2612,7 +2703,7 @@ repositoryURL = "https://github.com/huggingface/swift-transformers"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.1.8; + minimumVersion = 0.1.9; }; }; C392736E2B60699100368D5D /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { @@ -2620,7 +2711,7 @@ repositoryURL = "https://github.com/apple/swift-argument-parser.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.3.0; + minimumVersion = 1.4.0; }; }; C3FBCB1F2B8520B00007E490 /* XCRemoteSwiftPackageReference "mlx-swift" */ = { @@ -2628,7 +2719,7 @@ repositoryURL = "https://github.com/ml-explore/mlx-swift"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.12.1; + minimumVersion = 0.16.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/mlx-swift-examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mlx-swift-examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 48a7b7d..09fa619 100644 --- a/mlx-swift-examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mlx-swift-examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ml-explore/mlx-swift", "state" : { - "revision" : "36d63a1fc386a551df14f5b67df1756dc17d2ebc", - "version" : "0.12.1" + "revision" : "597aaa5f465b4b9a17c8646b751053f84e37925b", + "version" : "0.16.0" } }, { @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version" : "1.3.0" + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/swift-markdown-ui", "state" : { - "branch" : "main", - "revision" : "c0daf6eb79d97964180f3113868c990bd1c4a007" + "revision" : "9a8119b37e09a770367eeb26e05267c75d854053", + "version" : "2.3.1" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/huggingface/swift-transformers", "state" : { - "revision" : "fc6543263e4caed9bf6107466d625cfae9357f08", - "version" : "0.1.8" + "revision" : "e72d032ed742dcc8b364780ce4e02b25ab7a09b0", + "version" : "0.1.9" } } ],