// Copyright © 2024 Apple Inc. import Foundation import MLX import MLXNN /// Time/Height/Width struct to represent information about input images. public struct THW: Sendable { public let t: Int public let h: Int public let w: Int public init(_ t: Int, _ h: Int, _ w: Int) { self.t = t self.h = h self.w = w } public var values: (Int, Int, Int) { (t, h, w) } public var product: Int { t * h * w } } /// Representation of ``LanguageModel`` input. /// /// This can contain text (tokens), prepared images (`MLXArray`), or other media as /// needed. ``LMInput`` is produced by ``UserInputProcessor`` in response /// to ``UserInput``. /// /// The ``ModelContext`` holds the ``UserInputProcessor`` associated with a /// ``LanguageModel``. public struct LMInput { public let text: Text public let image: ProcessedImage? public let video: ProcessedVideo? public let audio: ProcessedAudio? /// Representation of tokenized input text. public struct Text { /// input token array public let tokens: MLXArray /// optional mask array public let mask: MLXArray? public init(tokens: MLXArray, mask: MLXArray? = nil) { self.tokens = tokens self.mask = mask } public subscript( indices: MLXArrayIndex..., stream stream: StreamOrDevice = .default ) -> Text { Text(tokens: tokens[indices, stream: stream], mask: mask?[indices, stream: stream]) } public subscript( text indices: MLXArrayIndex..., stream stream: StreamOrDevice = .default ) -> Text { Text(tokens: tokens[indices, stream: stream], mask: mask) } } /// Representation of prepared input image(s). public struct ProcessedImage { /// Concatenated pixels from one or more images public let pixels: MLXArray /// Time, height, and width of the images public let frames: [THW]? /// Desired number of soft tokens after visual pooling/projecting. public let softTokenCount: Int? public init( pixels: MLXArray, frames: [THW]? = nil, softTokenCount: Int? = nil ) { self.pixels = pixels self.frames = frames self.softTokenCount = softTokenCount } } /// Representation of prepared input video(s). /// For now, this is virtually identical to ProcessedImage. public struct ProcessedVideo { public let pixels: MLXArray public let frames: [THW]? public init( pixels: MLXArray, frames: [THW]? = nil ) { self.pixels = pixels self.frames = frames } } /// Representation of prepared input audio. public struct ProcessedAudio { public let features: MLXArray public let invalidMask: MLXArray? public let sampleRate: Double public let channelCount: Int public let duration: TimeInterval public let sampleCount: Int public let tokenCount: Int public init( features: MLXArray, invalidMask: MLXArray? = nil, sampleRate: Double, channelCount: Int, duration: TimeInterval, sampleCount: Int, tokenCount: Int ) { self.features = features self.invalidMask = invalidMask self.sampleRate = sampleRate self.channelCount = channelCount self.duration = duration self.sampleCount = sampleCount self.tokenCount = tokenCount } } public init(tokens: MLXArray, mask: MLXArray? = nil) { self.init(text: .init(tokens: tokens, mask: mask)) } public init( text: LMInput.Text, image: LMInput.ProcessedImage? = nil, video: LMInput.ProcessedVideo? = nil, audio: LMInput.ProcessedAudio? = nil ) { self.text = text self.image = image self.video = video self.audio = audio } } /// ``LanguageModel`` step output. This is consumed internally /// by the ``TokenIterator``. public struct LMOutput { /// logits (one hot vector of probabilities for tokens) public let logits: MLXArray /// optional ``State`` to carry forward into the next step public let state: State? public struct State { public let crossAttentionStates: MLXArray? public init(crossAttentionStates: MLXArray? = nil) { self.crossAttentionStates = crossAttentionStates } } public init(logits: MLXArray, state: LMOutput.State? = nil) { self.logits = logits self.state = state } } /// The result of the call to ``LanguageModel/prepare(_:cache:windowSize:)`` public enum PrepareResult { /// tokens to process by the ``TokenIterator`` case tokens(LMInput.Text) /// logits representing the next token case logits(LMOutput) } /// Interface for all Language Models (e.g. LLM, VLM). /// /// The language model is typically called by the ``TokenIterator`` and it: /// /// - consumes the ``LMInput`` /// - calls ``prepare(_:cache:windowSize:)`` to initialize the KVCache and consume the prompt /// - calls ``callAsFunction(_:cache:state:)-9kuvf`` for each token, producing an ``LMOutput`` /// - the ``TokenIterator`` accumulates this information into a ``GenerateResult`` public protocol LanguageModel: Module { /// Prepare the cache state and consume the ``LMInput``. /// /// This can return: /// - ``PrepareResult/tokens(_:)`` if the caller should evaluate the (remaining) tokens normally /// - ``PrepareResult/logits(_:)`` to produce the next token from the prompt func prepare(_ input: LMInput, cache: [KVCache], windowSize: Int?) throws -> PrepareResult /// Primary entry point to produce a step (single token) from the model func callAsFunction(_ input: LMInput.Text, cache: [KVCache]?, state: LMOutput.State?) -> LMOutput /// Models may implement this simplified interface if they do not produce any ``LMOutput/State`` func callAsFunction(_ inputs: MLXArray, cache: [KVCache]?) -> MLXArray /// create a new array of ``KVCache`` -- automatic implementation if self /// implements ``KVCacheDimensionProvider`` func newCache(parameters: GenerateParameters?) -> [KVCache] /// Optionally preprocess the weights and modify / remove values as needed. func sanitize(weights: [String: MLXArray]) -> [String: MLXArray] /// Optionally preprocess the weights with access to safetensor metadata. /// /// The default implementation forwards to ``sanitize(weights:)``. /// Models can override this to inspect metadata (e.g. check `metadata["format"] == "mlx"`) /// and skip or customize sanitization accordingly. func sanitize(weights: [String: MLXArray], metadata: [String: String]) -> [String: MLXArray] } extension LanguageModel { public func callAsFunction(_ input: LMInput.Text, cache: [KVCache]?, state: LMOutput.State?) -> LMOutput { let logits = callAsFunction(input.tokens, cache: cache) return .init(logits: logits) } public func callAsFunction(_ inputs: MLXArray, cache: [KVCache]?) -> MLXArray { fatalError("callAsFunction(inputs:cache:) not implemented for \(Self.self)") } } extension LanguageModel { public func sanitize(weights: [String: MLXArray]) -> [String: MLXArray] { weights } public func sanitize(weights: [String: MLXArray], metadata: [String: String]) -> [String: MLXArray] { sanitize(weights: weights) } } /// Optional protocol that can be implemented by ``LanguageModel`` and will /// provide an automatic implementation of ``LanguageModel/newCache(parameters:)`` public protocol KVCacheDimensionProvider { var kvHeads: [Int] { get } } extension LanguageModel where Self: KVCacheDimensionProvider { public func newCache(parameters: GenerateParameters?) -> [KVCache] { // Create one cache per layer (kvHeads.count = number of layers) // The number of heads per layer (kvHeads[i]) is not used for cache creation let numLayers = kvHeads.count // Follow Python logic: use RotatingKVCache if maxKVSize is provided if let maxKVSize = parameters?.maxKVSize { return (0 ..< numLayers).map { _ in RotatingKVCache(maxSize: maxKVSize, keep: 4) } } else { return (0 ..< numLayers).map { _ in KVCacheSimple() } } } }