Files

201 lines
6.0 KiB
Swift

import Foundation
import MLX
import MLXNN
// Port of https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/models/switch_layers.py
public func gatherSort(x: MLXArray, indices: MLXArray) -> (MLXArray, MLXArray, MLXArray) {
let m = indices.dim(-1)
let indices = indices.flattened()
let order = argSort(indices)
let inverseOrder = argSort(order)
return (
x.flattened(start: 0, end: -3)[order.floorDivide(m)],
indices[order],
inverseOrder
)
}
public func scatterUnsort(x: MLXArray, invOrder: MLXArray, shape: [Int]? = nil) -> MLXArray {
var x = x[invOrder]
if let shape {
x = unflatten(x, axis: 0, shape: shape)
}
return x
}
// MARK: - SwitchGLU
public class SwitchGLU: Module {
@ModuleInfo(key: "gate_proj") var gateProj: SwitchLinear
@ModuleInfo(key: "up_proj") var upProj: SwitchLinear
@ModuleInfo(key: "down_proj") var downProj: SwitchLinear
let inputDims: Int
let hiddenDims: Int
let numExperts: Int
let activation: (MLXArray) -> MLXArray
public init(
inputDims: Int,
hiddenDims: Int,
numExperts: Int,
activation: @escaping (MLXArray) -> MLXArray = MLXNN.silu,
bias: Bool = false
) {
self.inputDims = inputDims
self.hiddenDims = hiddenDims
self.numExperts = numExperts
self.activation = activation
self._gateProj.wrappedValue = SwitchLinear(
inputDims: inputDims, outputDims: hiddenDims, numExperts: numExperts, bias: bias)
self._upProj.wrappedValue = SwitchLinear(
inputDims: inputDims, outputDims: hiddenDims, numExperts: numExperts, bias: bias)
self._downProj.wrappedValue = SwitchLinear(
inputDims: hiddenDims, outputDims: inputDims, numExperts: numExperts, bias: bias)
super.init()
}
public func callAsFunction(_ x: MLXArray, _ indices: MLXArray) -> MLXArray {
var x = MLX.expandedDimensions(x, axes: [-2, -3])
let doSort = indices.size >= 64
var idx = indices
var inverseOrder = MLXArray()
if doSort {
(x, idx, inverseOrder) = gatherSort(x: x, indices: indices)
}
let xUp = upProj(x, idx, sortedIndices: doSort)
let xGate = gateProj(x, idx, sortedIndices: doSort)
x = downProj(
activation(xGate) * xUp,
idx,
sortedIndices: doSort)
if doSort {
x = scatterUnsort(x: x, invOrder: inverseOrder, shape: indices.shape)
}
return MLX.squeezed(x, axis: -2)
}
}
public class SwitchLinear: Module, Quantizable {
@ModuleInfo(key: "weight") var weight: MLXArray
@ModuleInfo(key: "bias") var bias: MLXArray?
let inputDims: Int
let outputDims: Int
let numExperts: Int
public init(inputDims: Int, outputDims: Int, numExperts: Int, bias: Bool = true) {
self.inputDims = inputDims
self.outputDims = outputDims
self.numExperts = numExperts
let scale = sqrt(1.0 / Float(inputDims))
self._weight.wrappedValue = MLXRandom.uniform(
low: -scale,
high: scale,
[numExperts, outputDims, inputDims]
)
if bias {
self._bias.wrappedValue = MLXArray.zeros([numExperts, outputDims])
}
super.init()
}
/// Initializer meant for subclasses to provide weight and bias arrays directly.
///
/// This is used e.g. by ``QuantizedSwitchLinear`` to provide quantized weights and biases
/// rather than have ``SwitchLinear`` compute them.
public init(
inputDims: Int, outputDims: Int, numExperts: Int,
weight: MLXArray, bias: MLXArray? = nil
) {
self.inputDims = inputDims
self.outputDims = outputDims
self.numExperts = numExperts
self._weight.wrappedValue = weight
self._bias.wrappedValue = bias
}
public func callAsFunction(
_ x: MLXArray, _ indices: MLXArray, sortedIndices: Bool = false
) -> MLXArray {
let weightT = self.weight.swappedAxes(-1, -2)
var result = MLX.gatherMM(x, weightT, rhsIndices: indices, sortedIndices: sortedIndices)
if let bias = self.bias {
result = result + MLX.expandedDimensions(bias[indices], axis: -2)
}
return result
}
public func toQuantized(groupSize: Int = 64, bits: Int = 4, mode: QuantizationMode) -> Module {
QuantizedSwitchLinear(self, groupSize: groupSize, bits: bits, mode: mode)
}
}
public class QuantizedSwitchLinear: SwitchLinear, Quantized {
@ModuleInfo(key: "scales") var scales: MLXArray
@ModuleInfo(key: "biases") var biases: MLXArray?
public let groupSize: Int
public let bits: Int
public let mode: QuantizationMode
public init(
_ other: SwitchLinear, groupSize: Int = 64, bits: Int = 4, mode: QuantizationMode = .affine
) {
self.groupSize = groupSize
self.bits = bits
self.mode = mode
let (quantizedWeight, scales, biases) = MLX.quantized(
other.weight, groupSize: groupSize, bits: bits, mode: mode)
self._scales.wrappedValue = scales
self._biases.wrappedValue = biases
super.init(
inputDims: other.inputDims, outputDims: other.outputDims, numExperts: other.numExperts,
weight: quantizedWeight, bias: other.bias)
self.freeze()
}
override public func callAsFunction(
_ x: MLXArray, _ indices: MLXArray, sortedIndices: Bool = false
) -> MLXArray {
var result = MLX.gatherQuantizedMM(
x,
self.weight,
scales: self.scales,
biases: self.biases,
rhsIndices: indices,
transpose: true,
groupSize: self.groupSize,
bits: self.bits,
mode: mode,
sortedIndices: sortedIndices
)
if let bias = self.bias {
result = result + MLX.expandedDimensions(bias[indices], axis: -2)
}
return result
}
}