Files

191 lines
7.7 KiB
Swift

// Copyright © 2025 Apple Inc.
import Foundation
import MLX
/// The fundamental configuration for any MLX-based model.
///
/// `BaseConfiguration` provides the metadata necessary to identify the model architecture
/// (`modelType`) and describes the quantization parameters used to compress the model's weights.
/// It is designed to be decoded directly from a model repository's `config.json`.
public struct BaseConfiguration: Codable, Sendable {
/// The architecture identifier (e.g., "bert", "roberta", "xlm-roberta").
public let modelType: String
/// Configuration parameters for weight quantization.
///
/// MLX uses group-wise quantization to reduce memory footprint. This struct
/// defines how weights are grouped and the precision (bits) used for each group.
public struct Quantization: Codable, Sendable, Equatable {
/// Initializes a new quantization configuration.
/// - Parameters:
/// - groupSize: The number of weights that share the same scale and bias.
/// - bits: The bit-depth of the quantized weights (e.g., 4 or 8).
public init(groupSize: Int, bits: Int) {
self.groupSize = groupSize
self.bits = bits
}
/// The size of the quantization group.
public let groupSize: Int
/// The number of bits per weight.
public let bits: Int
/// Internal storage for the quantization mode.
private var _mode: QuantizationMode? = nil
/// The quantization method to use (defaults to `.affine`).
///
/// Affine quantization (asymmetric) uses both a scale and a zero-point
/// to map floating point values to integers.
public var mode: QuantizationMode { _mode ?? .affine }
/// Converts the configuration into a tuple format compatible with `MLX.quantize`.
public var asTuple: (Int, Int, QuantizationMode) { (groupSize, bits, mode) }
enum CodingKeys: String, CodingKey {
case groupSize = "group_size"
case bits = "bits"
case _mode = "mode"
}
}
/// Instructions for handling individual layers during the quantization process.
public enum QuantizationOption: Sendable {
/// Do not quantize this specific layer (keep it in high precision).
case skip
/// Quantize this layer using the provided parameters.
case quantize(Quantization)
}
/// A container for per-layer quantization settings.
///
/// This allows for "Mixed-Precision" or "Heterogeneous" quantization, where
/// sensitive layers (like the embedding head) can be kept at higher precision
/// while the rest of the model is compressed.
public struct PerLayerQuantization: Sendable {
/// The default quantization for any layer not explicitly named in `perLayerQuantization`.
public var quantization: Quantization? = nil
/// A dictionary mapping layer paths (e.g., "model.embed_tokens") to their quantization options.
public var perLayerQuantization: [String: QuantizationOption]
public init(
quantization: BaseConfiguration.Quantization? = nil,
perLayerQuantization: [String: BaseConfiguration.QuantizationOption]
) {
self.quantization = quantization
self.perLayerQuantization = perLayerQuantization
}
/// Resolves the quantization parameters for a specific layer.
/// - Parameter layer: The path/name of the layer.
/// - Returns: The `Quantization` settings to apply, or `nil` if the layer should be skipped.
public func quantization(layer: String) -> Quantization? {
if let perLayer = perLayerQuantization[layer] {
switch perLayer {
case .skip:
return nil
case .quantize(let quantization):
return quantization
}
} else {
return quantization
}
}
}
/// An internal container designed to handle the mixed JSON structure found in `config.json`.
///
/// Quantization configs in MLX often interleave global keys (like `bits`) with
/// specific layer keys (like `model.layers.0...`). This container uses manual
/// decoding to separate these interleaved values.
struct QuantizationContainer: Codable, Sendable {
var quantization: Quantization
var perLayerQuantization: PerLayerQuantization
/// A custom CodingKey used to iterate over arbitrary layer names in JSON.
internal struct _DictionaryCodingKey: CodingKey {
internal let stringValue: String
internal let intValue: Int?
internal init(stringValue: String) {
self.stringValue = stringValue
self.intValue = Int(stringValue)
}
internal init(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
}
init(from decoder: any Decoder) throws {
// 1. Decode global quantization (bits/group_size) from the current level
self.quantization = try Quantization(from: decoder)
// 2. Decode interleaved per-layer values
var perLayerQuantization = [String: QuantizationOption]()
let container = try decoder.container(keyedBy: _DictionaryCodingKey.self)
for key in container.allKeys {
switch key.stringValue {
case Quantization.CodingKeys.groupSize.rawValue: continue
case Quantization.CodingKeys.bits.rawValue: continue
case Quantization.CodingKeys._mode.rawValue: continue
default:
// If the value is a boolean 'false', we treat it as .skip
if let f = try? container.decode(Bool.self, forKey: key) {
if !f {
perLayerQuantization[key.stringValue] = .skip
}
} else {
// Otherwise, try to decode a specific Quantization object for this layer
perLayerQuantization[key.stringValue] = .quantize(
try container.decode(Quantization.self, forKey: key))
}
}
}
self.perLayerQuantization = PerLayerQuantization(
quantization: quantization, perLayerQuantization: perLayerQuantization)
}
func encode(to encoder: any Encoder) throws {
try quantization.encode(to: encoder)
var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
for (key, value) in perLayerQuantization.perLayerQuantization {
switch value {
case .skip:
try container.encode(false, forKey: .init(stringValue: key))
case .quantize(let q):
try container.encode(q, forKey: .init(stringValue: key))
}
}
}
}
/// Internal storage for quantization details extracted from `config.json`.
var quantizationContainer: QuantizationContainer?
/// The default quantization settings.
@available(*, deprecated, message: "Please use perLayerQuantization instead")
public var quantization: Quantization? {
quantizationContainer?.quantization
}
/// The per-layer quantization settings, including the default fallback.
public var perLayerQuantization: PerLayerQuantization? {
quantizationContainer?.perLayerQuantization
}
enum CodingKeys: String, CodingKey {
case modelType = "model_type"
case quantizationContainer = "quantization"
}
}