// Copyright © 2024 Apple Inc. import Foundation import MLX import MLXLMCommon /// Creates a function that decodes configuration data and instantiates a model with the proper configuration private func create( _ configurationType: C.Type, _ modelInit: @escaping (C) -> M ) -> (Data) throws -> M { { data in let configuration = try JSONDecoder.json5().decode(C.self, from: data) return modelInit(configuration) } } /// Registry of model type, e.g 'llama', to functions that can instantiate the model from configuration. /// /// Typically called via ``LLMModelFactory/load(from:configuration:progressHandler:)``. public enum LLMTypeRegistry { /// Shared instance with default model types. public static let shared: ModelTypeRegistry = .init(creators: [ "mistral": create(LlamaConfiguration.self, LlamaModel.init), "llama": create(LlamaConfiguration.self, LlamaModel.init), "phi": create(PhiConfiguration.self, PhiModel.init), "phi3": create(Phi3Configuration.self, Phi3Model.init), "phimoe": create(PhiMoEConfiguration.self, PhiMoEModel.init), "gemma": create(GemmaConfiguration.self, GemmaModel.init), "gemma2": create(Gemma2Configuration.self, Gemma2Model.init), "gemma3": create(Gemma3TextConfiguration.self, Gemma3TextModel.init), "gemma3_text": create(Gemma3TextConfiguration.self, Gemma3TextModel.init), "gemma3n": create(Gemma3nTextConfiguration.self, Gemma3nTextModel.init), "qwen2": create(Qwen2Configuration.self, Qwen2Model.init), "qwen3": create(Qwen3Configuration.self, Qwen3Model.init), "qwen3_moe": create(Qwen3MoEConfiguration.self, Qwen3MoEModel.init), "qwen3_next": create(Qwen3NextConfiguration.self, Qwen3NextModel.init), "qwen3_5": create(Qwen35Configuration.self, Qwen35Model.init), "qwen3_5_moe": create(Qwen35Configuration.self, Qwen35MoEModel.init), "qwen3_5_text": create(Qwen35TextConfiguration.self, Qwen35TextModel.init), "minicpm": create(MiniCPMConfiguration.self, MiniCPMModel.init), "starcoder2": create(Starcoder2Configuration.self, Starcoder2Model.init), "cohere": create(CohereConfiguration.self, CohereModel.init), "openelm": create(OpenElmConfiguration.self, OpenELMModel.init), "internlm2": create(InternLM2Configuration.self, InternLM2Model.init), "deepseek_v3": create(DeepseekV3Configuration.self, DeepseekV3Model.init), "granite": create(GraniteConfiguration.self, GraniteModel.init), "granitemoehybrid": create( GraniteMoeHybridConfiguration.self, GraniteMoeHybridModel.init), "mimo": create(MiMoConfiguration.self, MiMoModel.init), "mimo_v2_flash": create(MiMoV2FlashConfiguration.self, MiMoV2FlashModel.init), "minimax": create(MiniMaxConfiguration.self, MiniMaxModel.init), "glm4": create(GLM4Configuration.self, GLM4Model.init), "glm4_moe": create(GLM4MoEConfiguration.self, GLM4MoEModel.init), "glm4_moe_lite": create(GLM4MoELiteConfiguration.self, GLM4MoELiteModel.init), "acereason": create(Qwen2Configuration.self, Qwen2Model.init), "falcon_h1": create(FalconH1Configuration.self, FalconH1Model.init), "bitnet": create(BitnetConfiguration.self, BitnetModel.init), "smollm3": create(SmolLM3Configuration.self, SmolLM3Model.init), "ernie4_5": create(Ernie45Configuration.self, Ernie45Model.init), "lfm2": create(LFM2Configuration.self, LFM2Model.init), "baichuan_m1": create(BaichuanM1Configuration.self, BaichuanM1Model.init), "exaone4": create(Exaone4Configuration.self, Exaone4Model.init), "gpt_oss": create(GPTOSSConfiguration.self, GPTOSSModel.init), "lille-130m": create(Lille130mConfiguration.self, Lille130mModel.init), "olmoe": create(OlmoEConfiguration.self, OlmoEModel.init), "olmo2": create(Olmo2Configuration.self, Olmo2Model.init), "olmo3": create(Olmo3Configuration.self, Olmo3Model.init), "bailing_moe": create(BailingMoeConfiguration.self, BailingMoeModel.init), "lfm2_moe": create(LFM2MoEConfiguration.self, LFM2MoEModel.init), "nanochat": create(NanoChatConfiguration.self, NanoChatModel.init), "nemotron_h": create(NemotronHConfiguration.self, NemotronHModel.init), "afmoe": create(AfMoEConfiguration.self, AfMoEModel.init), "jamba_3b": create(JambaConfiguration.self, JambaModel.init), "mistral3": create(Mistral3TextConfiguration.self, Mistral3TextModel.init), "apertus": create(ApertusConfiguration.self, ApertusModel.init), ]) } /// Registry of models and any overrides that go with them, e.g. prompt augmentation. /// If asked for an unknown configuration this will use the model/tokenizer as-is. /// /// 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 class LLMRegistry: AbstractModelRegistry, @unchecked Sendable { /// Shared instance with default model configurations. public static let shared = LLMRegistry(modelConfigurations: all()) static public let smolLM_135M_4bit = ModelConfiguration( id: "mlx-community/SmolLM-135M-Instruct-4bit", defaultPrompt: "Tell me about the history of Spain." ) static public let mistralNeMo4bit = ModelConfiguration( id: "mlx-community/Mistral-Nemo-Instruct-2407-4bit", defaultPrompt: "Explain quaternions." ) static public let mistral7B4bit = ModelConfiguration( id: "mlx-community/Mistral-7B-Instruct-v0.3-4bit", defaultPrompt: "Describe the Swift language." ) static public let codeLlama13b4bit = ModelConfiguration( id: "mlx-community/CodeLlama-13b-Instruct-hf-4bit-MLX", defaultPrompt: "func sortArray(_ array: [Int]) -> String { }" ) static public let deepSeekR1_7B_4bit = ModelConfiguration( id: "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit", defaultPrompt: "Is 9.9 greater or 9.11?" ) static public let phi4bit = ModelConfiguration( id: "mlx-community/phi-2-hf-4bit-mlx", // https://www.promptingguide.ai/models/phi-2 defaultPrompt: "Why is the sky blue?" ) static public let phi3_5_4bit = ModelConfiguration( id: "mlx-community/Phi-3.5-mini-instruct-4bit", defaultPrompt: "What is the gravity on Mars and the moon?", extraEOSTokens: ["<|end|>"] ) static public let phi3_5MoE = ModelConfiguration( id: "mlx-community/Phi-3.5-MoE-instruct-4bit", defaultPrompt: "What is the gravity on Mars and the moon?", extraEOSTokens: ["<|end|>"] ) static public let gemma2bQuantized = ModelConfiguration( id: "mlx-community/quantized-gemma-2b-it", // https://www.promptingguide.ai/models/gemma defaultPrompt: "what is the difference between lettuce and cabbage?" ) static public let gemma_2_9b_it_4bit = ModelConfiguration( id: "mlx-community/gemma-2-9b-it-4bit", // https://www.promptingguide.ai/models/gemma defaultPrompt: "What is the difference between lettuce and cabbage?" ) static public let gemma_2_2b_it_4bit = ModelConfiguration( id: "mlx-community/gemma-2-2b-it-4bit", // https://www.promptingguide.ai/models/gemma defaultPrompt: "What is the difference between lettuce and cabbage?" ) static public let gemma3_1B_qat_4bit = ModelConfiguration( id: "mlx-community/gemma-3-1b-it-qat-4bit", defaultPrompt: "What is the difference between a fruit and a vegetable?", extraEOSTokens: [""] ) static public let gemma3n_E4B_it_lm_bf16 = ModelConfiguration( id: "mlx-community/gemma-3n-E4B-it-lm-bf16", defaultPrompt: "What is the difference between a fruit and a vegetable?", // https://ai.google.dev/gemma/docs/core/prompt-structure extraEOSTokens: [""] ) static public let gemma3n_E2B_it_lm_bf16 = ModelConfiguration( id: "mlx-community/gemma-3n-E2B-it-lm-bf16", defaultPrompt: "What is the difference between a fruit and a vegetable?", // https://ai.google.dev/gemma/docs/core/prompt-structure extraEOSTokens: [""] ) static public let gemma3n_E4B_it_lm_4bit = ModelConfiguration( id: "mlx-community/gemma-3n-E4B-it-lm-4bit", defaultPrompt: "What is the difference between a fruit and a vegetable?", // https://ai.google.dev/gemma/docs/core/prompt-structure extraEOSTokens: [""] ) static public let gemma3n_E2B_it_lm_4bit = ModelConfiguration( id: "mlx-community/gemma-3n-E2B-it-lm-4bit", defaultPrompt: "What is the difference between a fruit and a vegetable?", // https://ai.google.dev/gemma/docs/core/prompt-structure extraEOSTokens: [""] ) static public let qwen205b4bit = ModelConfiguration( id: "mlx-community/Qwen1.5-0.5B-Chat-4bit", defaultPrompt: "why is the sky blue?" ) static public let qwen2_5_7b = ModelConfiguration( id: "mlx-community/Qwen2.5-7B-Instruct-4bit", defaultPrompt: "Why is the sky blue?" ) static public let qwen2_5_1_5b = ModelConfiguration( id: "mlx-community/Qwen2.5-1.5B-Instruct-4bit", defaultPrompt: "Why is the sky blue?" ) static public let qwen3_0_6b_4bit = ModelConfiguration( id: "mlx-community/Qwen3-0.6B-4bit", defaultPrompt: "Why is the sky blue?" ) static public let qwen3_1_7b_4bit = ModelConfiguration( id: "mlx-community/Qwen3-1.7B-4bit", defaultPrompt: "Why is the sky blue?" ) static public let qwen3_4b_4bit = ModelConfiguration( id: "mlx-community/Qwen3-4B-4bit", defaultPrompt: "Why is the sky blue?" ) static public let qwen3_8b_4bit = ModelConfiguration( id: "mlx-community/Qwen3-8B-4bit", defaultPrompt: "Why is the sky blue?" ) static public let qwen3MoE_30b_a3b_4bit = ModelConfiguration( id: "mlx-community/Qwen3-30B-A3B-4bit", defaultPrompt: "Why is the sky blue?" ) static public let openelm270m4bit = ModelConfiguration( id: "mlx-community/OpenELM-270M-Instruct", // https://huggingface.co/apple/OpenELM defaultPrompt: "Once upon a time there was" ) static public let llama3_1_8B_4bit = ModelConfiguration( id: "mlx-community/Meta-Llama-3.1-8B-Instruct-4bit", defaultPrompt: "What is the difference between a fruit and a vegetable?" ) static public let llama3_8B_4bit = ModelConfiguration( id: "mlx-community/Meta-Llama-3-8B-Instruct-4bit", defaultPrompt: "What is the difference between a fruit and a vegetable?" ) static public let llama3_2_1B_4bit = ModelConfiguration( id: "mlx-community/Llama-3.2-1B-Instruct-4bit", defaultPrompt: "What is the difference between a fruit and a vegetable?" ) static public let llama3_2_3B_4bit = ModelConfiguration( id: "mlx-community/Llama-3.2-3B-Instruct-4bit", defaultPrompt: "What is the difference between a fruit and a vegetable?" ) static public let deepseek_r1_4bit = ModelConfiguration( id: "mlx-community/DeepSeek-R1-4bit", defaultPrompt: "Tell me about the history of Spain." ) static public let granite3_3_2b_4bit = ModelConfiguration( id: "mlx-community/granite-3.3-2b-instruct-4bit", defaultPrompt: "" ) static public let mimo_7b_sft_4bit = ModelConfiguration( id: "mlx-community/MiMo-7B-SFT-4bit", defaultPrompt: "Why is the sky blue?" ) static public let glm4_9b_4bit = ModelConfiguration( id: "mlx-community/GLM-4-9B-0414-4bit", defaultPrompt: "Why is the sky blue?", toolCallFormat: .glm4 ) static public let acereason_7b_4bit = ModelConfiguration( id: "mlx-community/AceReason-Nemotron-7B-4bit", defaultPrompt: "" ) static public let bitnet_b1_58_2b_4t_4bit = ModelConfiguration( id: "mlx-community/bitnet-b1.58-2B-4T-4bit", defaultPrompt: "Why is the sky blue?" ) static public let baichuan_m1_14b_instruct_4bit = ModelConfiguration( id: "mlx-community/Baichuan-M1-14B-Instruct-4bit-ft", defaultPrompt: "Why is the sky blue?" ) static public let smollm3_3b_4bit = ModelConfiguration( id: "mlx-community/SmolLM3-3B-4bit", defaultPrompt: "Why is the sky blue?" ) static public let ernie_45_0_3BPT_bf16_ft = ModelConfiguration( id: "mlx-community/ERNIE-4.5-0.3B-PT-bf16-ft", defaultPrompt: "Why is the sky blue?" ) static public let lfm2_1_2b_4bit = ModelConfiguration( id: "mlx-community/LFM2-1.2B-4bit", defaultPrompt: "Why is the sky blue?", toolCallFormat: .lfm2 ) static public let exaone_4_0_1_2b_4bit = ModelConfiguration( id: "mlx-community/exaone-4.0-1.2b-4bit", defaultPrompt: "Why is the sky blue?" ) static public let lille_130m_bf16 = ModelConfiguration( id: "mlx-community/lille-130m-instruct-bf16", defaultPrompt: "Why is the sky blue?" ) static public let olmoe_1b_7b_0125_instruct_4bit = ModelConfiguration( id: "mlx-community/OLMoE-1B-7B-0125-Instruct-4bit", defaultPrompt: "Why is the sky blue?" ) static public let olmo_2_1124_7B_Instruct_4bit = ModelConfiguration( id: "mlx-community/OLMo-2-1124-7B-Instruct-4bit", defaultPrompt: "Why is the sky blue?" ) static public let ling_mini_2_2bit = ModelConfiguration( id: "mlx-community/Ling-mini-2.0-2bit-DWQ", defaultPrompt: "Why is the sky blue?" ) static public let granite_4_0_h_tiny_4bit_dwq = ModelConfiguration( id: "mlx-community/Granite-4.0-H-Tiny-4bit-DWQ", defaultPrompt: "" ) static public let lfm2_8b_a1b_3bit_mlx = ModelConfiguration( id: "mlx-community/LFM2-8B-A1B-3bit-MLX", defaultPrompt: "", toolCallFormat: .lfm2 ) static public let nanochat_d20_mlx = ModelConfiguration( id: "dnakov/nanochat-d20-mlx", defaultPrompt: "" ) static public let gpt_oss_20b_MXFP4_Q8 = ModelConfiguration( id: "mlx-community/gpt-oss-20b-MXFP4-Q8", defaultPrompt: "Why is the sky blue?" ) static public let jamba_3b = ModelConfiguration( id: "mlx-community/AI21-Jamba-Reasoning-3B-bf16", defaultPrompt: "" ) private static func all() -> [ModelConfiguration] { [ codeLlama13b4bit, deepSeekR1_7B_4bit, gemma2bQuantized, gemma_2_2b_it_4bit, gemma_2_9b_it_4bit, gemma3_1B_qat_4bit, gemma3n_E4B_it_lm_bf16, gemma3n_E2B_it_lm_bf16, gemma3n_E4B_it_lm_4bit, gemma3n_E2B_it_lm_4bit, granite3_3_2b_4bit, granite_4_0_h_tiny_4bit_dwq, llama3_1_8B_4bit, llama3_2_1B_4bit, llama3_2_3B_4bit, llama3_8B_4bit, mistral7B4bit, mistralNeMo4bit, openelm270m4bit, phi3_5MoE, phi3_5_4bit, phi4bit, qwen205b4bit, qwen2_5_7b, qwen2_5_1_5b, qwen3_0_6b_4bit, qwen3_1_7b_4bit, qwen3_4b_4bit, qwen3_8b_4bit, qwen3MoE_30b_a3b_4bit, smolLM_135M_4bit, deepseek_r1_4bit, mimo_7b_sft_4bit, glm4_9b_4bit, acereason_7b_4bit, bitnet_b1_58_2b_4t_4bit, smollm3_3b_4bit, ernie_45_0_3BPT_bf16_ft, lfm2_1_2b_4bit, baichuan_m1_14b_instruct_4bit, exaone_4_0_1_2b_4bit, lille_130m_bf16, olmoe_1b_7b_0125_instruct_4bit, olmo_2_1124_7B_Instruct_4bit, ling_mini_2_2bit, lfm2_8b_a1b_3bit_mlx, nanochat_d20_mlx, gpt_oss_20b_MXFP4_Q8, jamba_3b, ] } } @available(*, deprecated, renamed: "LLMRegistry", message: "Please use LLMRegistry directly.") public typealias ModelRegistry = LLMRegistry private struct LLMUserInputProcessor: UserInputProcessor { let tokenizer: Tokenizer let configuration: ModelConfiguration let messageGenerator: MessageGenerator internal init( tokenizer: any Tokenizer, configuration: ModelConfiguration, messageGenerator: MessageGenerator ) { self.tokenizer = tokenizer self.configuration = configuration self.messageGenerator = messageGenerator } func prepare(input: UserInput) throws -> LMInput { let messages = messageGenerator.generate(from: input) do { let promptTokens = try tokenizer.applyChatTemplate( messages: messages, tools: input.tools, additionalContext: input.additionalContext) return LMInput(tokens: MLXArray(promptTokens)) } catch TokenizerError.missingChatTemplate { print( "No chat template was included or provided, so converting messages to simple text format. This is not optimal for model performance, so applications should provide a chat template if none is included with the model." ) let prompt = messages .compactMap { $0["content"] as? String } .joined(separator: "\n\n") let promptTokens = tokenizer.encode(text: prompt) return LMInput(tokens: MLXArray(promptTokens)) } } } /// Factory for creating new LLMs. /// /// Callers can use the `shared` instance or create a new instance if custom configuration /// is required. /// /// ```swift /// let modelContainer = try await LLMModelFactory.shared.loadContainer( /// configuration: LLMRegistry.llama3_8B_4bit) /// ``` public final class LLMModelFactory: ModelFactory { public init(typeRegistry: ModelTypeRegistry, modelRegistry: AbstractModelRegistry) { self.typeRegistry = typeRegistry self.modelRegistry = modelRegistry } /// Shared instance with default behavior. public static let shared = LLMModelFactory( typeRegistry: LLMTypeRegistry.shared, modelRegistry: LLMRegistry.shared) /// registry of model type, e.g. configuration value `llama` -> configuration and init methods public let typeRegistry: ModelTypeRegistry /// registry of model id to configuration, e.g. `mlx-community/Llama-3.2-3B-Instruct-4bit` public let modelRegistry: AbstractModelRegistry public func _load( configuration: ResolvedModelConfiguration, tokenizerLoader: any TokenizerLoader ) async throws -> ModelContext { let modelDirectory = configuration.modelDirectory // Load config.json once and decode for both base config and model-specific config let configurationURL = modelDirectory.appending(component: "config.json") let configData: Data do { configData = try Data(contentsOf: configurationURL) } catch { throw ModelFactoryError.configurationFileError( configurationURL.lastPathComponent, configuration.name, error) } let baseConfig: BaseConfiguration do { baseConfig = try JSONDecoder.json5().decode(BaseConfiguration.self, from: configData) } catch let error as DecodingError { throw ModelFactoryError.configurationDecodingError( configurationURL.lastPathComponent, configuration.name, error) } let model: LanguageModel do { model = try await typeRegistry.createModel( configuration: configData, modelType: baseConfig.modelType) } catch let error as DecodingError { throw ModelFactoryError.configurationDecodingError( configurationURL.lastPathComponent, configuration.name, error) } // Load EOS token IDs from config.json, with optional override from generation_config.json var eosTokenIds = Set(baseConfig.eosTokenIds?.values ?? []) let generationConfigURL = modelDirectory.appending(component: "generation_config.json") if let generationData = try? Data(contentsOf: generationConfigURL), let generationConfig = try? JSONDecoder.json5().decode( GenerationConfigFile.self, from: generationData), let genEosIds = generationConfig.eosTokenIds?.values { eosTokenIds = Set(genEosIds) // Override per Python mlx-lm behavior } // Build a ModelConfiguration with loaded EOS token IDs and tool call format var mutableConfiguration = configuration mutableConfiguration.eosTokenIds = eosTokenIds if mutableConfiguration.toolCallFormat == nil { mutableConfiguration.toolCallFormat = ToolCallFormat.infer(from: baseConfig.modelType) } // Load tokenizer and weights in parallel async let tokenizerTask = tokenizerLoader.load( from: configuration.tokenizerDirectory) try loadWeights( modelDirectory: modelDirectory, model: model, perLayerQuantization: baseConfig.perLayerQuantization) let tokenizer = try await tokenizerTask let messageGenerator = if let model = model as? LLMModel { model.messageGenerator(tokenizer: tokenizer) } else { DefaultMessageGenerator() } // Build a ModelConfiguration for the ModelContext let tokenizerSource: TokenizerSource? = configuration.tokenizerDirectory == modelDirectory ? nil : .directory(configuration.tokenizerDirectory) let modelConfig = ModelConfiguration( directory: modelDirectory, tokenizerSource: tokenizerSource, defaultPrompt: configuration.defaultPrompt, extraEOSTokens: mutableConfiguration.extraEOSTokens, eosTokenIds: mutableConfiguration.eosTokenIds, toolCallFormat: mutableConfiguration.toolCallFormat) let processor = LLMUserInputProcessor( tokenizer: tokenizer, configuration: modelConfig, messageGenerator: messageGenerator) return .init( configuration: modelConfig, model: model, processor: processor, tokenizer: tokenizer) } } public class TrampolineModelFactory: NSObject, ModelFactoryTrampoline { public static func modelFactory() -> (any MLXLMCommon.ModelFactory)? { LLMModelFactory.shared } }