chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:46 +08:00
commit 8199cf3c39
90 changed files with 62775 additions and 0 deletions
@@ -0,0 +1,197 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import Accelerate
import CoreML
import CoreGraphics
@available(iOS 16.0, macOS 13.0, *)
extension CGImage {
typealias PixelBufferPFx1 = vImage.PixelBuffer<vImage.PlanarF>
typealias PixelBufferP8x3 = vImage.PixelBuffer<vImage.Planar8x3>
typealias PixelBufferIFx3 = vImage.PixelBuffer<vImage.InterleavedFx3>
typealias PixelBufferI8x3 = vImage.PixelBuffer<vImage.Interleaved8x3>
public enum ShapedArrayError: String, Swift.Error {
case wrongNumberOfChannels
case incorrectFormatsConvertingToShapedArray
case vImageConverterNotInitialized
}
public static func fromShapedArray(_ array: MLShapedArray<Float32>) throws -> CGImage {
// array is [N,C,H,W], where C==3
let channelCount = array.shape[1]
guard channelCount == 3 else {
throw ShapedArrayError.wrongNumberOfChannels
}
let height = array.shape[2]
let width = array.shape[3]
// Normalize each channel into a float between 0 and 1.0
let floatChannels = (0..<channelCount).map { i in
// Normalized channel output
let cOut = PixelBufferPFx1(width: width, height:height)
// Reference this channel in the array and normalize
array[0][i].withUnsafeShapedBufferPointer { ptr, _, strides in
let cIn = PixelBufferPFx1(data: .init(mutating: ptr.baseAddress!),
width: width, height: height,
byteCountPerRow: strides[0]*4)
// Map [-1.0 1.0] -> [0.0 1.0]
cIn.multiply(by: 0.5, preBias: 1.0, postBias: 0.0, destination: cOut)
}
return cOut
}
// Convert to interleaved and then to UInt8
let floatImage = PixelBufferIFx3(planarBuffers: floatChannels)
let uint8Image = PixelBufferI8x3(width: width, height: height)
floatImage.convert(to:uint8Image) // maps [0.0 1.0] -> [0 255] and clips
// Convert to uint8x3 to RGB CGImage (no alpha)
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
let cgImage = uint8Image.makeCGImage(cgImageFormat:
.init(bitsPerComponent: 8,
bitsPerPixel: 3*8,
colorSpace: CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB(),
bitmapInfo: bitmapInfo)!)!
return cgImage
}
public func planarRGBShapedArray(minValue: Float, maxValue: Float)
throws -> MLShapedArray<Float32> {
guard
var sourceFormat = vImage_CGImageFormat(cgImage: self),
var mediumFormat = vImage_CGImageFormat(
bitsPerComponent: 8 * MemoryLayout<UInt8>.size,
bitsPerPixel: 8 * MemoryLayout<UInt8>.size * 4,
colorSpace: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue)),
let width = vImagePixelCount(exactly: self.width),
let height = vImagePixelCount(exactly: self.height)
else {
throw ShapedArrayError.incorrectFormatsConvertingToShapedArray
}
var sourceImageBuffer = try vImage_Buffer(cgImage: self)
var mediumDestination = try vImage_Buffer(width: Int(width), height: Int(height), bitsPerPixel: mediumFormat.bitsPerPixel)
let converter = vImageConverter_CreateWithCGImageFormat(
&sourceFormat,
&mediumFormat,
nil,
vImage_Flags(kvImagePrintDiagnosticsToConsole),
nil)
guard let converter = converter?.takeRetainedValue() else {
throw ShapedArrayError.vImageConverterNotInitialized
}
vImageConvert_AnyToAny(converter, &sourceImageBuffer, &mediumDestination, nil, vImage_Flags(kvImagePrintDiagnosticsToConsole))
var destinationA = try vImage_Buffer(width: Int(width), height: Int(height), bitsPerPixel: 8 * UInt32(MemoryLayout<Float>.size))
var destinationR = try vImage_Buffer(width: Int(width), height: Int(height), bitsPerPixel: 8 * UInt32(MemoryLayout<Float>.size))
var destinationG = try vImage_Buffer(width: Int(width), height: Int(height), bitsPerPixel: 8 * UInt32(MemoryLayout<Float>.size))
var destinationB = try vImage_Buffer(width: Int(width), height: Int(height), bitsPerPixel: 8 * UInt32(MemoryLayout<Float>.size))
var minFloat: [Float] = Array(repeating: minValue, count: 4)
var maxFloat: [Float] = Array(repeating: maxValue, count: 4)
vImageConvert_ARGB8888toPlanarF(&mediumDestination, &destinationA, &destinationR, &destinationG, &destinationB, &maxFloat, &minFloat, .zero)
let destAPtr = destinationA.data.assumingMemoryBound(to: Float.self)
let destRPtr = destinationR.data.assumingMemoryBound(to: Float.self)
let destGPtr = destinationG.data.assumingMemoryBound(to: Float.self)
let destBPtr = destinationB.data.assumingMemoryBound(to: Float.self)
for i in 0..<Int(width) * Int(height) {
if destAPtr.advanced(by: i).pointee == 0 {
destRPtr.advanced(by: i).pointee = -1
destGPtr.advanced(by: i).pointee = -1
destBPtr.advanced(by: i).pointee = -1
}
}
let redData = destinationR.unpaddedData()
let greenData = destinationG.unpaddedData()
let blueData = destinationB.unpaddedData()
let imageData = redData + greenData + blueData
let shapedArray = MLShapedArray<Float32>(data: imageData, shape: [1, 3, self.height, self.width])
return shapedArray
}
private func normalizePixelValues(pixel: UInt8) -> Float {
return (Float(pixel) / 127.5) - 1.0
}
public func toRGBShapedArray(minValue: Float, maxValue: Float)
throws -> MLShapedArray<Float32> {
let image = self
let width = image.width
let height = image.height
let alphaMaskValue: Float = minValue
guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 4 * width, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue),
let ptr = context.data?.bindMemory(to: UInt8.self, capacity: width * height * 4) else {
return []
}
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
var redChannel = [Float](repeating: 0, count: width * height)
var greenChannel = [Float](repeating: 0, count: width * height)
var blueChannel = [Float](repeating: 0, count: width * height)
for y in 0..<height {
for x in 0..<width {
let i = 4 * (y * width + x)
if ptr[i+3] == 0 {
// Alpha mask for controlnets
redChannel[y * width + x] = alphaMaskValue
greenChannel[y * width + x] = alphaMaskValue
blueChannel[y * width + x] = alphaMaskValue
} else {
redChannel[y * width + x] = normalizePixelValues(pixel: ptr[i])
greenChannel[y * width + x] = normalizePixelValues(pixel: ptr[i+1])
blueChannel[y * width + x] = normalizePixelValues(pixel: ptr[i+2])
}
}
}
let colorShape = [1, 1, height, width]
let redShapedArray = MLShapedArray<Float32>(scalars: redChannel, shape: colorShape)
let greenShapedArray = MLShapedArray<Float32>(scalars: greenChannel, shape: colorShape)
let blueShapedArray = MLShapedArray<Float32>(scalars: blueChannel, shape: colorShape)
let shapedArray = MLShapedArray<Float32>(concatenating: [redShapedArray, greenShapedArray, blueShapedArray], alongAxis: 1)
return shapedArray
}
}
extension vImage_Buffer {
func unpaddedData() -> Data {
let bytesPerPixel = self.rowBytes / Int(self.width)
let bytesPerRow = Int(self.width) * bytesPerPixel
var contiguousPixelData = Data(capacity: bytesPerRow * Int(self.height))
for row in 0..<Int(self.height) {
let rowStart = self.data!.advanced(by: row * self.rowBytes)
let rowData = Data(bytes: rowStart, count: bytesPerRow)
contiguousPixelData.append(rowData)
}
return contiguousPixelData
}
}
@@ -0,0 +1,130 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
import Accelerate
@available(iOS 16.2, macOS 13.1, *)
public struct ControlNet: ResourceManaging {
var models: [ManagedMLModel]
public init(modelAt urls: [URL],
configuration: MLModelConfiguration) {
self.models = urls.map { ManagedMLModel(modelAt: $0, configuration: configuration) }
}
/// Load resources.
public func loadResources() throws {
for model in models {
try model.loadResources()
}
}
/// Unload the underlying model to free up memory
public func unloadResources() {
for model in models {
model.unloadResources()
}
}
/// Pre-warm resources
public func prewarmResources() throws {
// Override default to pre-warm each model
for model in models {
try model.loadResources()
model.unloadResources()
}
}
var inputImageDescriptions: [MLFeatureDescription] {
models.map { model in
try! model.perform {
$0.modelDescription.inputDescriptionsByName["controlnet_cond"]!
}
}
}
/// The expected shape of the models image input
public var inputImageShapes: [[Int]] {
inputImageDescriptions.map { desc in
desc.multiArrayConstraint!.shape.map { $0.intValue }
}
}
/// Calculate additional inputs for Unet to generate intended image following provided images
///
/// - Parameters:
/// - latents: Batch of latent samples in an array
/// - timeStep: Current diffusion timestep
/// - hiddenStates: Hidden state to condition on
/// - images: Images for each ControlNet
/// - Returns: Array of predicted noise residuals
func execute(
latents: [MLShapedArray<Float32>],
timeStep: Int,
hiddenStates: MLShapedArray<Float32>,
images: [MLShapedArray<Float32>]
) throws -> [[String: MLShapedArray<Float32>]] {
// Match time step batch dimension to the model / latent samples
let t = MLShapedArray(scalars: [Float(timeStep), Float(timeStep)], shape: [2])
var outputs: [[String: MLShapedArray<Float32>]] = []
for (modelIndex, model) in models.enumerated() {
let inputs = try latents.map { latent in
let dict: [String: Any] = [
"sample": MLMultiArray(latent),
"timestep": MLMultiArray(t),
"encoder_hidden_states": MLMultiArray(hiddenStates),
"controlnet_cond": MLMultiArray(images[modelIndex])
]
return try MLDictionaryFeatureProvider(dictionary: dict)
}
let batch = MLArrayBatchProvider(array: inputs)
let results = try model.perform {
try $0.predictions(fromBatch: batch)
}
// pre-allocate MLShapedArray with a specific shape in outputs
if outputs.isEmpty {
outputs = initOutputs(
batch: latents.count,
shapes: results.features(at: 0).featureValueDictionary
)
}
for n in 0..<results.count {
let result = results.features(at: n)
for k in result.featureNames {
let newValue = result.featureValue(for: k)!.multiArrayValue!
if modelIndex == 0 {
outputs[n][k] = MLShapedArray<Float32>(newValue)
} else {
let outputArray = MLMultiArray(outputs[n][k]!)
let count = newValue.count
let inputPointer = newValue.dataPointer.assumingMemoryBound(to: Float.self)
let outputPointer = outputArray.dataPointer.assumingMemoryBound(to: Float.self)
vDSP_vadd(inputPointer, 1, outputPointer, 1, outputPointer, 1, vDSP_Length(count))
}
}
}
}
return outputs
}
private func initOutputs(batch: Int, shapes: [String: MLFeatureValue]) -> [[String: MLShapedArray<Float32>]] {
var output: [String: MLShapedArray<Float32>] = [:]
for (outputName, featureValue) in shapes {
output[outputName] = MLShapedArray<Float32>(
repeating: 0.0,
shape: featureValue.multiArrayValue!.shape.map { $0.intValue }
)
}
return Array(repeating: output, count: batch)
}
}
@@ -0,0 +1,273 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. and The HuggingFace Team. All Rights Reserved.
import Accelerate
import CoreML
/// How to space timesteps for inference
public enum TimeStepSpacing {
case linspace
case leading
case karras
}
/// A scheduler used to compute a de-noised image
///
/// This implementation matches:
/// [Hugging Face Diffusers DPMSolverMultistepScheduler](https://github.com/huggingface/diffusers/blob/main/src/diffusers/schedulers/scheduling_dpmsolver_multistep.py)
///
/// It uses the DPM-Solver++ algorithm: [code](https://github.com/LuChengTHU/dpm-solver) [paper](https://arxiv.org/abs/2211.01095).
/// Limitations:
/// - Only implemented for DPM-Solver++ algorithm (not DPM-Solver).
/// - Second order only.
/// - Assumes the model predicts epsilon.
/// - No dynamic thresholding.
/// - `midpoint` solver algorithm.
@available(iOS 16.2, macOS 13.1, *)
public final class DPMSolverMultistepScheduler: Scheduler {
public let trainStepCount: Int
public let inferenceStepCount: Int
public let betas: [Float]
public let alphas: [Float]
public let alphasCumProd: [Float]
public let timeSteps: [Int]
public let alpha_t: [Float]
public let sigma_t: [Float]
public let lambda_t: [Float]
public let solverOrder = 2
private(set) var lowerOrderStepped = 0
private var usingKarrasSigmas = false
/// Whether to use lower-order solvers in the final steps. Only valid for less than 15 inference steps.
/// We empirically find this trick can stabilize the sampling of DPM-Solver, especially with 10 or fewer steps.
public let useLowerOrderFinal = true
// Stores solverOrder (2) items
public private(set) var modelOutputs: [MLShapedArray<Float32>] = []
/// Create a scheduler that uses a second order DPM-Solver++ algorithm.
///
/// - Parameters:
/// - stepCount: Number of inference steps to schedule
/// - trainStepCount: Number of training diffusion steps
/// - betaSchedule: Method to schedule betas from betaStart to betaEnd
/// - betaStart: The starting value of beta for inference
/// - betaEnd: The end value for beta for inference
/// - timeStepSpacing: How to space time steps
/// - Returns: A scheduler ready for its first step
public init(
stepCount: Int = 50,
trainStepCount: Int = 1000,
betaSchedule: BetaSchedule = .scaledLinear,
betaStart: Float = 0.00085,
betaEnd: Float = 0.012,
timeStepSpacing: TimeStepSpacing = .linspace
) {
self.trainStepCount = trainStepCount
self.inferenceStepCount = stepCount
switch betaSchedule {
case .linear:
self.betas = linspace(betaStart, betaEnd, trainStepCount)
case .scaledLinear:
self.betas = linspace(pow(betaStart, 0.5), pow(betaEnd, 0.5), trainStepCount).map({ $0 * $0 })
}
self.alphas = betas.map({ 1.0 - $0 })
var alphasCumProd = self.alphas
for i in 1..<alphasCumProd.count {
alphasCumProd[i] *= alphasCumProd[i - 1]
}
self.alphasCumProd = alphasCumProd
switch timeStepSpacing {
case .linspace:
self.timeSteps = linspace(0, Float(self.trainStepCount-1), stepCount+1).dropFirst().reversed().map { Int(round($0)) }
self.alpha_t = vForce.sqrt(self.alphasCumProd)
self.sigma_t = vForce.sqrt(vDSP.subtract([Float](repeating: 1, count: self.alphasCumProd.count), self.alphasCumProd))
case .leading:
let lastTimeStep = trainStepCount - 1
let stepRatio = lastTimeStep / (stepCount + 1)
// Creates integer timesteps by multiplying by ratio
self.timeSteps = (0...stepCount).map { 1 + $0 * stepRatio }.dropFirst().reversed()
self.alpha_t = vForce.sqrt(self.alphasCumProd)
self.sigma_t = vForce.sqrt(vDSP.subtract([Float](repeating: 1, count: self.alphasCumProd.count), self.alphasCumProd))
case .karras:
// sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5)
let scaled = vDSP.multiply(
subtraction: ([Float](repeating: 1, count: self.alphasCumProd.count), self.alphasCumProd),
subtraction: (vDSP.divide(1, self.alphasCumProd), [Float](repeating: 0, count: self.alphasCumProd.count))
)
let sigmas = vForce.sqrt(scaled)
let logSigmas = sigmas.map { log($0) }
let sigmaMin = sigmas.first!
let sigmaMax = sigmas.last!
let rho: Float = 7
let ramp = linspace(0, 1, stepCount)
let minInvRho = pow(sigmaMin, (1 / rho))
let maxInvRho = pow(sigmaMax, (1 / rho))
var karrasSigmas = ramp.map { pow(maxInvRho + $0 * (minInvRho - maxInvRho), rho) }
let karrasTimeSteps = karrasSigmas.map { sigmaToTimestep(sigma: $0, logSigmas: logSigmas) }
self.timeSteps = karrasTimeSteps
karrasSigmas.append(karrasSigmas.last!)
self.alpha_t = vDSP.divide(1, vForce.sqrt(vDSP.add(1, vDSP.square(karrasSigmas))))
self.sigma_t = vDSP.multiply(karrasSigmas, self.alpha_t)
usingKarrasSigmas = true
}
self.lambda_t = zip(self.alpha_t, self.sigma_t).map { α, σ in log(α) - log(σ) }
}
func timestepToIndex(_ timestep: Int) -> Int {
guard usingKarrasSigmas else { return timestep }
return self.timeSteps.firstIndex(of: timestep) ?? 0
}
/// Convert the model output to the corresponding type the algorithm needs.
/// This implementation is for second-order DPM-Solver++ assuming epsilon prediction.
func convertModelOutput(modelOutput: MLShapedArray<Float32>, timestep: Int, sample: MLShapedArray<Float32>) -> MLShapedArray<Float32> {
assert(modelOutput.scalarCount == sample.scalarCount)
let scalarCount = modelOutput.scalarCount
let sigmaIndex = timestepToIndex(timestep)
let (alpha_t, sigma_t) = (self.alpha_t[sigmaIndex], self.sigma_t[sigmaIndex])
return MLShapedArray(unsafeUninitializedShape: modelOutput.shape) { scalars, _ in
assert(scalars.count == scalarCount)
modelOutput.withUnsafeShapedBufferPointer { modelOutput, _, _ in
sample.withUnsafeShapedBufferPointer { sample, _, _ in
for i in 0 ..< scalarCount {
scalars.initializeElement(at: i, to: (sample[i] - modelOutput[i] * sigma_t) / alpha_t)
}
}
}
}
}
/// One step for the first-order DPM-Solver (equivalent to DDIM).
/// See https://arxiv.org/abs/2206.00927 for the detailed derivation.
/// var names and code structure mostly follow https://github.com/huggingface/diffusers/blob/main/src/diffusers/schedulers/scheduling_dpmsolver_multistep.py
func firstOrderUpdate(
modelOutput: MLShapedArray<Float32>,
timestep: Int,
prevTimestep: Int,
sample: MLShapedArray<Float32>
) -> MLShapedArray<Float32> {
let prevIndex = timestepToIndex(prevTimestep)
let currIndex = timestepToIndex(timestep)
let (p_lambda_t, lambda_s) = (Double(lambda_t[prevIndex]), Double(lambda_t[currIndex]))
let p_alpha_t = Double(alpha_t[prevIndex])
let (p_sigma_t, sigma_s) = (Double(sigma_t[prevIndex]), Double(sigma_t[currIndex]))
let h = p_lambda_t - lambda_s
// x_t = (sigma_t / sigma_s) * sample - (alpha_t * (torch.exp(-h) - 1.0)) * model_output
let x_t = weightedSum(
[p_sigma_t / sigma_s, -p_alpha_t * (exp(-h) - 1)],
[sample, modelOutput]
)
return x_t
}
/// One step for the second-order multistep DPM-Solver++ algorithm, using the midpoint method.
/// var names and code structure mostly follow https://github.com/huggingface/diffusers/blob/main/src/diffusers/schedulers/scheduling_dpmsolver_multistep.py
func secondOrderUpdate(
modelOutputs: [MLShapedArray<Float32>],
timesteps: [Int],
prevTimestep t: Int,
sample: MLShapedArray<Float32>
) -> MLShapedArray<Float32> {
let (s0, s1) = (timesteps[back: 1], timesteps[back: 2])
let (m0, m1) = (modelOutputs[back: 1], modelOutputs[back: 2])
let (p_lambda_t, lambda_s0, lambda_s1) = (
Double(lambda_t[timestepToIndex(t)]),
Double(lambda_t[timestepToIndex(s0)]),
Double(lambda_t[timestepToIndex(s1)])
)
let p_alpha_t = Double(alpha_t[timestepToIndex(t)])
let (p_sigma_t, sigma_s0) = (Double(sigma_t[timestepToIndex(t)]), Double(sigma_t[timestepToIndex(s0)]))
let (h, h_0) = (p_lambda_t - lambda_s0, lambda_s0 - lambda_s1)
let r0 = h_0 / h
let D0 = m0
// D1 = (1.0 / r0) * (m0 - m1)
let D1 = weightedSum(
[1/r0, -1/r0],
[m0, m1]
)
// See https://arxiv.org/abs/2211.01095 for detailed derivations
// x_t = (
// (sigma_t / sigma_s0) * sample
// - (alpha_t * (torch.exp(-h) - 1.0)) * D0
// - 0.5 * (alpha_t * (torch.exp(-h) - 1.0)) * D1
// )
let x_t = weightedSum(
[p_sigma_t/sigma_s0, -p_alpha_t * (exp(-h) - 1), -0.5 * p_alpha_t * (exp(-h) - 1)],
[sample, D0, D1]
)
return x_t
}
public func step(output: MLShapedArray<Float32>, timeStep t: Int, sample: MLShapedArray<Float32>) -> MLShapedArray<Float32> {
let stepIndex = timeSteps.firstIndex(of: t) ?? timeSteps.count - 1
let prevTimestep = stepIndex == timeSteps.count - 1 ? 0 : timeSteps[stepIndex + 1]
let lowerOrderFinal = useLowerOrderFinal && stepIndex == timeSteps.count - 1 && timeSteps.count < 15
let lowerOrderSecond = useLowerOrderFinal && stepIndex == timeSteps.count - 2 && timeSteps.count < 15
let lowerOrder = lowerOrderStepped < 1 || lowerOrderFinal || lowerOrderSecond
let modelOutput = convertModelOutput(modelOutput: output, timestep: t, sample: sample)
if modelOutputs.count == solverOrder { modelOutputs.removeFirst() }
modelOutputs.append(modelOutput)
let prevSample: MLShapedArray<Float32>
if lowerOrder {
prevSample = firstOrderUpdate(modelOutput: modelOutput, timestep: t, prevTimestep: prevTimestep, sample: sample)
} else {
prevSample = secondOrderUpdate(
modelOutputs: modelOutputs,
timesteps: [timeSteps[stepIndex - 1], t],
prevTimestep: prevTimestep,
sample: sample
)
}
if lowerOrderStepped < solverOrder {
lowerOrderStepped += 1
}
return prevSample
}
}
func sigmaToTimestep(sigma: Float, logSigmas: [Float]) -> Int {
let logSigma = log(sigma)
let dists = logSigmas.map { logSigma - $0 }
// last index that is not negative, clipped to last index - 1
var lowIndex = dists.reduce(-1) { partialResult, dist in
return dist >= 0 && partialResult < dists.endIndex-2 ? partialResult + 1 : partialResult
}
lowIndex = max(lowIndex, 0)
let highIndex = lowIndex + 1
let low = logSigmas[lowIndex]
let high = logSigmas[highIndex]
// Interpolate sigmas
let w = ((low - logSigma) / (low - high)).clipped(to: 0...1)
// transform interpolated value to time range
let t = (1 - w) * Float(lowIndex) + w * Float(highIndex)
return Int(round(t))
}
extension FloatingPoint {
func clipped(to range: ClosedRange<Self>) -> Self {
return min(max(self, range.lowerBound), range.upperBound)
}
}
@@ -0,0 +1,80 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2024 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
/// A decoder model which produces RGB images from latent samples
@available(iOS 16.2, macOS 13.1, *)
public struct Decoder: ResourceManaging {
/// VAE decoder model
var model: ManagedMLModel
/// Create decoder from Core ML model
///
/// - Parameters:
/// - url: Location of compiled VAE decoder Core ML model
/// - configuration: configuration to be used when the model is loaded
/// - Returns: A decoder that will lazily load its required resources when needed or requested
public init(modelAt url: URL, configuration: MLModelConfiguration) {
self.model = ManagedMLModel(modelAt: url, configuration: configuration)
}
/// Ensure the model has been loaded into memory
public func loadResources() throws {
try model.loadResources()
}
/// Unload the underlying model to free up memory
public func unloadResources() {
model.unloadResources()
}
/// Batch decode latent samples into images
///
/// - Parameters:
/// - latents: Batch of latent samples to decode
/// - scaleFactor: scalar divisor on latents before decoding
/// - Returns: decoded images
public func decode(
_ latents: [MLShapedArray<Float32>],
scaleFactor: Float32,
shiftFactor: Float32 = 0.0
) throws -> [CGImage] {
// Form batch inputs for model
let inputs: [MLFeatureProvider] = try latents.map { sample in
// Reference pipeline scales the latent samples before decoding
let sampleScaled = MLShapedArray<Float32>(
scalars: sample.scalars.map { $0 / scaleFactor + shiftFactor },
shape: sample.shape)
let dict = [inputName: MLMultiArray(sampleScaled)]
return try MLDictionaryFeatureProvider(dictionary: dict)
}
let batch = MLArrayBatchProvider(array: inputs)
// Batch predict with model
let results = try model.perform { model in
try model.predictions(fromBatch: batch)
}
// Transform the outputs to CGImages
let images: [CGImage] = try (0..<results.count).map { i in
let result = results.features(at: i)
let outputName = result.featureNames.first!
let output = result.featureValue(for: outputName)!.multiArrayValue!
return try CGImage.fromShapedArray(MLShapedArray<Float32>(converting: output))
}
return images
}
var inputName: String {
try! model.perform { model in
model.modelDescription.inputDescriptionsByName.first!.key
}
}
}
@@ -0,0 +1,123 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2024 Apple Inc. All Rights Reserved.
import CoreML
/// A scheduler used to compute a de-noised image
@available(iOS 16.2, macOS 13.1, *)
public final class DiscreteFlowScheduler: Scheduler {
public let trainStepCount: Int
public let inferenceStepCount: Int
public var timeSteps = [Int]()
public var betas = [Float]()
public var alphas = [Float]()
public var alphasCumProd = [Float]()
public private(set) var modelOutputs: [MLShapedArray<Float32>] = []
var trainSteps: Float
var shift: Float
var counter: Int
var sigmas = [Float]()
/// Create a scheduler that uses a second order DPM-Solver++ algorithm.
///
/// - Parameters:
/// - stepCount: Number of inference steps to schedule
/// - trainStepCount: Number of training diffusion steps
/// - timeStepShift: Amount to shift the timestep schedule
/// - Returns: A scheduler ready for its first step
public init(
stepCount: Int = 50,
trainStepCount: Int = 1000,
timeStepShift: Float = 3.0
) {
self.trainStepCount = trainStepCount
self.inferenceStepCount = stepCount
self.trainSteps = Float(trainStepCount)
self.shift = timeStepShift
self.counter = 0
let sigmaDistribution = linspace(1, trainSteps, Int(trainSteps)).map { sigmaFromTimestep($0) }
let timeStepDistribution = linspace(sigmaDistribution.first!, sigmaDistribution.last!, stepCount).reversed()
self.timeSteps = timeStepDistribution.map { Int($0 * trainSteps) }
self.sigmas = timeStepDistribution.map { sigmaFromTimestep($0 * trainSteps) }
}
func sigmaFromTimestep(_ timestep: Float) -> Float {
if shift == 1.0 {
return timestep / trainSteps
} else {
// shift * timestep / (1 + (shift - 1) * timestep)
let t = timestep / trainSteps
return shift * t / (1 + (shift - 1) * t)
}
}
func timestepsFromSigmas() -> [Float] {
return sigmas.map { $0 * trainSteps }
}
/// Convert the model output to the corresponding type the algorithm needs.
func convertModelOutput(modelOutput: MLShapedArray<Float32>, timestep: Int, sample: MLShapedArray<Float32>) -> MLShapedArray<Float32> {
assert(modelOutput.scalarCount == sample.scalarCount)
let stepIndex = timeSteps.firstIndex(of: timestep) ?? counter
let sigma = sigmas[stepIndex]
return MLShapedArray<Float>(unsafeUninitializedShape: modelOutput.shape) { result, _ in
modelOutput.withUnsafeShapedBufferPointer { noiseScalars, _, _ in
sample.withUnsafeShapedBufferPointer { latentScalars, _, _ in
for i in 0..<result.count {
let denoised = latentScalars[i] - noiseScalars[i] * sigma
result.initializeElement(
at: i,
to: denoised
)
}
}
}
}
}
public func calculateTimestepsFromSigmas(strength: Float?) -> [Float] {
guard let strength else { return timestepsFromSigmas() }
let startStep = max(inferenceStepCount - Int(Float(inferenceStepCount) * strength), 0)
let actualTimesteps = Array(timestepsFromSigmas()[startStep...])
return actualTimesteps
}
public func step(output: MLShapedArray<Float32>, timeStep t: Int, sample: MLShapedArray<Float32>) -> MLShapedArray<Float32> {
let stepIndex = timeSteps.firstIndex(of: t) ?? counter // TODO: allow float timesteps in scheduler step protocol
let modelOutput = convertModelOutput(modelOutput: output, timestep: t, sample: sample)
modelOutputs.append(modelOutput)
let sigma = sigmas[stepIndex]
var dt = sigma
var prevSigma: Float = 0
if stepIndex < sigmas.count - 1 {
prevSigma = sigmas[stepIndex + 1]
dt = prevSigma - sigma
}
let prevSample: MLShapedArray<Float32> = MLShapedArray<Float>(unsafeUninitializedShape: modelOutput.shape) { result, _ in
modelOutput.withUnsafeShapedBufferPointer { noiseScalars, _, _ in
sample.withUnsafeShapedBufferPointer { latentScalars, _, _ in
for i in 0..<result.count {
let denoised = noiseScalars[i]
let x = latentScalars[i]
let d = (x - denoised) / sigma
let prev_x = x + d * dt
result.initializeElement(
at: i,
to: prev_x
)
}
}
}
}
counter += 1
return prevSample
}
}
@@ -0,0 +1,108 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
/// A encoder model which produces latent samples from RGB images
@available(iOS 16.2, macOS 13.1, *)
public struct Encoder: ResourceManaging {
public enum Error: String, Swift.Error {
case sampleInputShapeNotCorrect
}
/// VAE encoder model + post math and adding noise from schedular
var model: ManagedMLModel
/// Create encoder from Core ML model
///
/// - Parameters:
/// - url: Location of compiled VAE encoder Core ML model
/// - configuration: configuration to be used when the model is loaded
/// - Returns: An encoder that will lazily load its required resources when needed or requested
public init(modelAt url: URL, configuration: MLModelConfiguration) {
self.model = ManagedMLModel(modelAt: url, configuration: configuration)
}
/// Ensure the model has been loaded into memory
public func loadResources() throws {
try model.loadResources()
}
/// Unload the underlying model to free up memory
public func unloadResources() {
model.unloadResources()
}
/// Prediction queue
let queue = DispatchQueue(label: "encoder.predict")
/// Encode image into latent sample
///
/// - Parameters:
/// - image: Input image
/// - scaleFactor: scalar multiplier on latents before encoding image
/// - random
/// - Returns: The encoded latent space as MLShapedArray
public func encode(
_ image: CGImage,
scaleFactor: Float32,
random: inout RandomSource
) throws -> MLShapedArray<Float32> {
let imageData = try image.planarRGBShapedArray(minValue: -1.0, maxValue: 1.0)
guard imageData.shape == inputShape else {
// TODO: Consider auto resizing and croping similar to how Vision or CoreML auto-generated Swift code can accomplish with `MLFeatureValue`
throw Error.sampleInputShapeNotCorrect
}
let dict = [inputName: MLMultiArray(imageData)]
let input = try MLDictionaryFeatureProvider(dictionary: dict)
let result = try model.perform { model in
try model.prediction(from: input)
}
let outputName = result.featureNames.first!
let outputValue = result.featureValue(for: outputName)!.multiArrayValue!
let output = MLShapedArray<Float32>(converting: outputValue)
// DiagonalGaussianDistribution
let mean = output[0][0..<4]
let logvar = MLShapedArray<Float32>(
scalars: output[0][4..<8].scalars.map { min(max($0, -30), 20) },
shape: mean.shape
)
let std = MLShapedArray<Float32>(
scalars: logvar.scalars.map { exp(0.5 * $0) },
shape: logvar.shape
)
let latent = MLShapedArray<Float32>(
scalars: zip(mean.scalars, std.scalars).map {
Float32(random.nextNormal(mean: Double($0), stdev: Double($1)))
},
shape: logvar.shape
)
// Reference pipeline scales the latent after encoding
let latentScaled = MLShapedArray<Float32>(
scalars: latent.scalars.map { $0 * scaleFactor },
shape: [1] + latent.shape
)
return latentScaled
}
var inputDescription: MLFeatureDescription {
try! model.perform { model in
model.modelDescription.inputDescriptionsByName.first!.value
}
}
var inputName: String {
inputDescription.name
}
/// The expected shape of the models latent sample input
var inputShape: [Int] {
inputDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
}
@@ -0,0 +1,127 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import CoreML
/// A class to manage and gate access to a Core ML model
///
/// It will automatically load a model into memory when needed or requested
/// It allows one to request to unload the model from memory
@available(iOS 16.2, macOS 13.1, *)
public final class ManagedMLModel: ResourceManaging {
/// The location of the model
var modelURL: URL
/// The configuration to be used when the model is loaded
var configuration: MLModelConfiguration
/// The loaded model (when loaded)
var loadedModel: MLModel?
/// Queue to protect access to loaded model
var queue: DispatchQueue
/// Create a managed model given its location and desired loaded configuration
///
/// - Parameters:
/// - url: The location of the model
/// - configuration: The configuration to be used when the model is loaded/used
/// - Returns: A managed model that has not been loaded
public init(modelAt url: URL, configuration: MLModelConfiguration) {
self.modelURL = url
self.configuration = configuration
self.loadedModel = nil
self.queue = DispatchQueue(label: "managed.\(url.lastPathComponent)")
}
/// Instantiation and load model into memory
public func loadResources() throws {
try queue.sync {
try loadModel()
}
}
/// Unload the model if it was loaded
public func unloadResources() {
queue.sync {
loadedModel = nil
}
}
/// Perform an operation with the managed model via a supplied closure.
/// The model will be loaded and supplied to the closure and should only be
/// used within the closure to ensure all resource management is synchronized
///
/// - Parameters:
/// - body: Closure which performs and action on a loaded model
/// - Returns: The result of the closure
/// - Throws: An error if the model cannot be loaded or if the closure throws
public func perform<R>(_ body: (MLModel) throws -> R) throws -> R {
return try queue.sync {
try autoreleasepool {
try loadModel()
return try body(loadedModel!)
}
}
}
private func loadModel() throws {
if loadedModel == nil {
loadedModel = try MLModel(contentsOf: modelURL,
configuration: configuration)
}
}
}
@available(iOS 16.2, macOS 13.1, *)
public extension Array where Element == ManagedMLModel {
/// Performs batch predictions using an array of `[ManagedMLModel]` instances in a pipeline.
/// - Parameter batch: Inputs for btached predictions.
/// - Returns: Final prediction results after processing through all models.
/// - Throws: Errors if the array is empty, predictions fail, or results can't be combined.
func predictions(from batch: MLBatchProvider) throws -> MLBatchProvider {
var results = try self.first!.perform { model in
try model.predictions(fromBatch: batch)
}
if self.count == 1 {
return results
}
// Manual pipeline batch prediction
let inputs = batch.arrayOfFeatureValueDictionaries
for stage in self.dropFirst() {
// Combine the original inputs with the outputs of the last stage
let next = try results.arrayOfFeatureValueDictionaries
.enumerated().map { index, dict in
let nextDict = dict.merging(inputs[index]) { out, _ in out }
return try MLDictionaryFeatureProvider(dictionary: nextDict)
}
let nextBatch = MLArrayBatchProvider(array: next)
// Predict
results = try stage.perform { model in
try model.predictions(fromBatch: nextBatch)
}
}
return results
}
}
extension MLFeatureProvider {
var featureValueDictionary: [String : MLFeatureValue] {
self.featureNames.reduce(into: [String : MLFeatureValue]()) { result, name in
result[name] = self.featureValue(for: name)
}
}
}
extension MLBatchProvider {
var arrayOfFeatureValueDictionaries: [[String : MLFeatureValue]] {
(0..<self.count).map {
self.features(at: $0).featureValueDictionary
}
}
}
@@ -0,0 +1,125 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
/// MMDiT noise prediction model for stable diffusion
@available(iOS 16.2, macOS 13.1, *)
public struct MultiModalDiffusionTransformer: ResourceManaging {
/// Model used to predict noise residuals given an input, diffusion time step, and conditional embedding
///
/// It can be in the form of a single model or multiple stages
var models: [ManagedMLModel]
/// Creates a MMDiT noise prediction model
///
/// - Parameters:
/// - url: Location of single MMDiT compiled Core ML model
/// - configuration: Configuration to be used when the model is loaded
/// - Returns: MMDiT model that will lazily load its required resources when needed or requested
public init(modelAt url: URL,
configuration: MLModelConfiguration)
{
self.models = [ManagedMLModel(modelAt: url, configuration: configuration)]
}
/// Load resources.
public func loadResources() throws {
for model in models {
try model.loadResources()
}
}
/// Unload the underlying model to free up memory
public func unloadResources() {
for model in models {
model.unloadResources()
}
}
/// Pre-warm resources
public func prewarmResources() throws {
// Override default to pre-warm each model
for model in models {
try model.loadResources()
model.unloadResources()
}
}
var latentImageEmbeddingsDescription: MLFeatureDescription {
try! models.first!.perform { model in
model.modelDescription.inputDescriptionsByName["latent_image_embeddings"]!
}
}
/// The expected shape of the models latent sample input
public var latentImageEmbeddingsShape: [Int] {
latentImageEmbeddingsDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
var tokenLevelTextEmbeddingsDescription: MLFeatureDescription {
try! models.first!.perform { model in
model.modelDescription.inputDescriptionsByName["token_level_text_embeddings"]!
}
}
/// The expected shape of the geometry conditioning
public var tokenLevelTextEmbeddingsShape: [Int] {
tokenLevelTextEmbeddingsDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
/// Batch prediction noise from latent samples
///
/// - Parameters:
/// - latents: Batch of latent samples in an array
/// - timeStep: Current diffusion timestep
/// - hiddenStates: Hidden state to condition on
/// - Returns: Array of predicted noise residuals
func predictNoise(
latents: [MLShapedArray<Float32>],
timeStep: Float,
tokenLevelTextEmbeddings: MLShapedArray<Float32>,
pooledTextEmbeddings: MLShapedArray<Float32>
) throws -> [MLShapedArray<Float32>] {
// Match time step batch dimension to the model / latent samples
let t = MLShapedArray<Float32>(scalars: [timeStep, timeStep], shape: [2])
// Form batch input to model
let inputs = try latents.enumerated().map {
let dict: [String: Any] = [
"latent_image_embeddings": MLMultiArray($0.element),
"timestep": MLMultiArray(t),
"token_level_text_embeddings": MLMultiArray(tokenLevelTextEmbeddings),
"pooled_text_embeddings": MLMultiArray(pooledTextEmbeddings),
]
return try MLDictionaryFeatureProvider(dictionary: dict)
}
let batch = MLArrayBatchProvider(array: inputs)
// Make predictions
let results = try models.predictions(from: batch)
// Pull out the results in Float32 format
let noise = (0..<results.count).map { i in
let result = results.features(at: i)
let outputName = result.featureNames.first!
let outputNoise = result.featureValue(for: outputName)!.multiArrayValue!
// To conform to this func return type make sure we return float32
// Use the fact that the concatenating constructor for MLMultiArray
// can do type conversion:
let fp32Noise = MLMultiArray(
concatenating: [outputNoise],
axis: 0,
dataType: .float32
)
return MLShapedArray<Float32>(fp32Noise)
}
return noise
}
}
@@ -0,0 +1,194 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2023 Apple Inc. All Rights Reserved.
import Foundation
import NaturalLanguage
import CoreML
#if canImport(NaturalLanguage.NLContextualEmbedding)
@available(iOS 17.0, macOS 14.0, *)
public struct MultilingualTextEncoder: TextEncoderModel {
let adapter: ManagedMLModel?
let embeddingModel: NLContextualEmbedding
// TODO: use maximum sequence length from embedding.
let maximumEmbeddingSequenceLength = 256
/// Creates a multilingual text encoder.
///
/// - Parameters:
/// - url: The location of the compiled Core ML adapter model. The model is a linear projection layer that
/// transforms the contextual embedding size of 512 to the default text encoder CLIP size of 768.
/// - configuration: The configuration to be used when the model is loaded.
/// - script: The scipt of the contextual embedding.
public init(
modelAt url: URL? = nil,
configuration: MLModelConfiguration = .init(),
script: Script = .latin
) {
if let url {
self.adapter = ManagedMLModel(modelAt: url, configuration: configuration)
} else {
self.adapter = nil
}
self.embeddingModel = NLContextualEmbedding(script: script.asNLScript)!
self.embeddingModel.requestAssets { _, _ in }
}
/// Loads model resources into memory.
public func loadResources() throws {
try adapter?.loadResources()
try embeddingModel.load()
}
/// Unloads the model resources to free up memory.
public func unloadResources() {
adapter?.unloadResources()
embeddingModel.unload()
}
/// Encodes the input text.
///
/// - Parameter text: The input text.
/// - Returns: An embedding shaped array.
public func encode(_ text: String) throws -> MLShapedArray<Float> {
guard embeddingModel.hasAvailableAssets else {
throw Error.missingEmbeddingResource
}
// Create the text embedding result.
let embedding = try embeddingModel.embeddingResult(for: text, language: nil)
// Create embedding array from token vectors.
var shapedEmbeddings = MLShapedArray<Double>(
repeating: 0.0,
shape: [1, maximumEmbeddingSequenceLength, embeddingModel.dimension]
)
shapedEmbeddings.withUnsafeMutableShapedBufferPointer { pointer, _, _ in
var tokenIndex = 0
embedding.enumerateTokenVectors(in: text.startIndex ..< text.endIndex) { (tokenEmbeddings, _) -> Bool in
for tokenEmbeddingIndex in 0 ..< tokenEmbeddings.count {
pointer[tokenIndex * embeddingModel.dimension + tokenEmbeddingIndex] = tokenEmbeddings[tokenEmbeddingIndex]
}
tokenIndex += 1
return true
}
}
if adapter == nil {
// Return embeddings with shape [1, 256, 512].
return MLShapedArray(converting: shapedEmbeddings)
} else {
// Project the embeddings to the correct CLIP model input shape of [1, 768, 1, 256].
return try projectEmbeddings(shapedEmbeddings)
}
}
/// Creates the adapter model input feature provider.
private func prepareProjectionInput(_ input: MLShapedArray<Double>) throws -> MLDictionaryFeatureProvider {
guard let adapter else {
fatalError("Cannot prepare projection input without an adapter.")
}
return try adapter.perform { model in
guard let inputDescription = model.modelDescription.inputDescriptionsByName.first?.value else {
throw Error.missingAdapterInput
}
return try MLDictionaryFeatureProvider(dictionary: [inputDescription.name: MLMultiArray(input)])
}
}
/// Processes the adapter model output feature provider.
private func processProjectionOutput(_ output: MLFeatureProvider) throws -> MLShapedArray<Float> {
guard let adapter else {
fatalError("Cannot process projection output without an adapter.")
}
return try adapter.perform { model in
guard let outputDescription = model.modelDescription.outputDescriptionsByName.first?.value else {
throw Error.missingAdapterOutput
}
guard let result = output
.featureValue(for: outputDescription.name)?
.multiArrayValue else {
throw Error.incompatibleAdapterOutputDataFormat(
expected: .multiArray,
actual: outputDescription.type
)
}
return MLShapedArray(converting: result)
}
}
/// Projects the embeddings.
private func projectEmbeddings(_ embeddings: MLShapedArray<Double>) throws -> MLShapedArray<Float> {
guard let adapter else {
fatalError("Cannot project embeddings without an adapter.")
}
let inputFeatureProvider = try prepareProjectionInput(embeddings)
let projection = try adapter.perform { model in
return try model.prediction(from: inputFeatureProvider)
}
return try processProjectionOutput(projection)
}
}
@available(iOS 17.0, macOS 14.0, *)
extension MultilingualTextEncoder {
/// A multilingual text encoder error.
public enum Error: Swift.Error, LocalizedError, Equatable, CustomDebugStringConvertible {
/// An error that indicates that the resource for the embedding is missing.
case missingEmbeddingResource
/// An error that indicates that the adapter model input data has the wrong format.
case incompatibleAdapterInputDataFormat(expected: MLFeatureType, actual: MLFeatureType)
/// An error that indicates that the adapter model output data has the wrong format.
case incompatibleAdapterOutputDataFormat(expected: MLFeatureType, actual: MLFeatureType)
/// An error that indicates that the adapter model is missing an input.
case missingAdapterInput
/// An error that indicates that the adapter model is missing an output.
case missingAdapterOutput
/// A debug description of the error.
public var errorDescription: String? {
debugDescription
}
/// A text representation of the error.
public var debugDescription: String {
switch self {
case .missingEmbeddingResource:
return "Resources required for generating embeddings are missing. Make sure that your device is connected to the internet and try again."
case .incompatibleAdapterInputDataFormat(expected: let expected, actual: let actual):
return "The adapter model input expected to be \(expected) but is \(actual)."
case .incompatibleAdapterOutputDataFormat(expected: let expected, actual: let actual):
return "The adapter model output expected to be \(expected) but is \(actual)."
case .missingAdapterInput:
return "The adapter model is missing an input."
case .missingAdapterOutput:
return "The adapter model is missing an output."
}
}
}
}
#endif
@available(iOS 16.2, macOS 13.1, *)
public enum Script: String {
case latin, cyrillic, cjk
#if canImport(NaturalLanguage.NLScript)
@available(iOS 17.0, macOS 14.0, *)
var asNLScript: NLScript {
switch self {
case .latin: return .latin
case .cyrillic: return .cyrillic
case .cjk: return .simplifiedChinese
}
}
#endif
}
@@ -0,0 +1,119 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
/// A random source consistent with NumPy
///
/// This implementation matches:
/// [NumPy's older randomkit.c](https://github.com/numpy/numpy/blob/v1.0/numpy/random/mtrand/randomkit.c)
///
@available(iOS 16.2, macOS 13.1, *)
struct NumPyRandomSource: RandomNumberGenerator, RandomSource {
struct State {
var key = [UInt32](repeating: 0, count: 624)
var pos: Int = 0
var nextGauss: Double? = nil
}
var state: State
/// Initialize with a random seed
///
/// - Parameters
/// - seed: Seed for underlying Mersenne Twister 19937 generator
/// - Returns random source
init(seed: UInt32) {
state = .init()
var s = seed & 0xffffffff
for i in 0 ..< state.key.count {
state.key[i] = s
s = UInt32((UInt64(1812433253) * UInt64(s ^ (s >> 30)) + UInt64(i) + 1) & 0xffffffff)
}
state.pos = state.key.count
state.nextGauss = nil
}
/// Generate next UInt32 using fast 32bit Mersenne Twister
mutating func nextUInt32() -> UInt32 {
let n = 624
let m = 397
let matrixA: UInt64 = 0x9908b0df
let upperMask: UInt32 = 0x80000000
let lowerMask: UInt32 = 0x7fffffff
var y: UInt32
if state.pos == state.key.count {
for i in 0 ..< (n - m) {
y = (state.key[i] & upperMask) | (state.key[i + 1] & lowerMask)
state.key[i] = state.key[i + m] ^ (y >> 1) ^ UInt32((UInt64(~(y & 1)) + 1) & matrixA)
}
for i in (n - m) ..< (n - 1) {
y = (state.key[i] & upperMask) | (state.key[i + 1] & lowerMask)
state.key[i] = state.key[i + (m - n)] ^ (y >> 1) ^ UInt32((UInt64(~(y & 1)) + 1) & matrixA)
}
y = (state.key[n - 1] & upperMask) | (state.key[0] & lowerMask)
state.key[n - 1] = state.key[m - 1] ^ (y >> 1) ^ UInt32((UInt64(~(y & 1)) + 1) & matrixA)
state.pos = 0
}
y = state.key[state.pos]
state.pos += 1
y ^= (y >> 11)
y ^= (y << 7) & 0x9d2c5680
y ^= (y << 15) & 0xefc60000
y ^= (y >> 18)
return y
}
mutating func next() -> UInt64 {
let low = nextUInt32()
let high = nextUInt32()
return (UInt64(high) << 32) | UInt64(low)
}
/// Generate next random double value
mutating func nextDouble() -> Double {
let a = Double(nextUInt32() >> 5)
let b = Double(nextUInt32() >> 6)
return (a * 67108864.0 + b) / 9007199254740992.0
}
/// Generate next random value from a standard normal
mutating func nextGauss() -> Double {
if let nextGauss = state.nextGauss {
state.nextGauss = nil
return nextGauss
}
var x1, x2, r2: Double
repeat {
x1 = 2.0 * nextDouble() - 1.0
x2 = 2.0 * nextDouble() - 1.0
r2 = x1 * x1 + x2 * x2
} while r2 >= 1.0 || r2 == 0.0
// Box-Muller transform
let f = sqrt(-2.0 * log(r2) / r2)
state.nextGauss = f * x1
return f * x2
}
/// Generates a random value from a normal distribution with given mean and standard deviation.
mutating func nextNormal(mean: Double = 0.0, stdev: Double = 1.0) -> Double {
nextGauss() * stdev + mean
}
/// Generates an array of random values from a normal distribution with given mean and standard deviation.
mutating func normalArray(count: Int, mean: Double = 0.0, stdev: Double = 1.0) -> [Double] {
(0 ..< count).map { _ in nextNormal(mean: mean, stdev: stdev) }
}
/// Generate a shaped array with scalars from a normal distribution with given mean and standard deviation.
mutating func normalShapedArray(_ shape: [Int], mean: Double = 0.0, stdev: Double = 1.0) -> MLShapedArray<Double> {
let count = shape.reduce(1, *)
return .init(scalars: normalArray(count: count, mean: mean, stdev: stdev), shape: shape)
}
}
@@ -0,0 +1,91 @@
import Foundation
import CoreML
/// A random source consistent with NVIDIA curandom
///
/// This implementation references to:
/// https://github.com/dsnz/random/blob/master/philox.py for Philox_M4_32 configuration.
///
@available(iOS 16.2, macOS 13.1, *)
struct NvRandomSource: RandomSource {
public let seed: UInt64
private var offset: UInt32
/// Initialize with a random seed
///
/// - Parameters
/// - seed: Seed for underlying Philox M4 32 generator
/// - Returns random source
public init(seed: UInt32) {
self.seed = UInt64(seed)
offset = 0
}
static private let PHILOX_M4_32: (UInt32, UInt32) = (0xD251_1F53, 0xCD9E_8D57)
static private let PHILOX_W_32: (UInt32, UInt32) = (0x9E37_79B9, 0xBB67_AE85)
static private func philox4Round(counter: inout [[UInt32]], key: [[UInt32]]) {
for i in 0..<counter[0].count {
let v1: UInt64 = UInt64(counter[0][i]) * UInt64(PHILOX_M4_32.0)
let v2: UInt64 = UInt64(counter[2][i]) * UInt64(PHILOX_M4_32.1)
counter[0][i] = UInt32(v2 >> 32) ^ counter[1][i] ^ key[0][i]
counter[1][i] = UInt32(v2 & 0xffff_ffff)
counter[2][i] = UInt32(v1 >> 32) ^ counter[3][i] ^ key[1][i]
counter[3][i] = UInt32(v1 & 0xffff_ffff)
}
}
static private func philox4Bumpkey(key: inout [[UInt32]]) {
for (i, element) in key[0].enumerated() {
key[0][i] = element &+ PHILOX_W_32.0
}
for (i, element) in key[1].enumerated() {
key[1][i] = element &+ PHILOX_W_32.1
}
}
static private func philox4_32(counter: inout [[UInt32]], key: inout [[UInt32]], rounds: Int = 10) {
for _ in 0..<(rounds - 1) {
philox4Round(counter: &counter, key: key)
philox4Bumpkey(key: &key)
}
philox4Round(counter: &counter, key: key)
}
private func boxMuller(_ counter1: [UInt32], _ counter2: [UInt32], mean: Double, stdev: Double) -> [Double] {
// Box-Muller transform
return zip(counter1, counter2).map {
let u: Double = Double($0) / 4294967296.0 + (1.0 / 8589934592.0)
let v: Double = Double($1) * (.pi / 2147483648.0) + (.pi / 4294967296.0)
let radius = stdev * sqrt(-2.0 * log(u))
return radius * sin(v) + mean
}
}
private mutating func normalArray(count: Int, mean: Double, stdev: Double) -> [Double] {
var counter: [[UInt32]] = [
Array(repeating: offset, count: count),
Array(repeating: 0, count: count),
Array(0..<UInt32(count)),
Array(repeating: 0, count: count),
]
offset += 1
var key: [[UInt32]] = [
Array(repeating: UInt32(seed & 0xffff_ffff), count: count),
Array(repeating: UInt32(seed >> 32), count: count),
]
Self.philox4_32(counter: &counter, key: &key)
return boxMuller(counter[0], counter[1], mean: mean, stdev: stdev)
}
/// Generates a random value from a normal distribution with given mean and standard deviation.
mutating func nextNormal(mean: Double = 0.0, stdev: Double = 1.0) -> Double {
return normalArray(count: 1, mean: mean, stdev: stdev)[0]
}
/// Generate a shaped array with scalars from a normal distribution with given mean and standard deviation.
mutating func normalShapedArray(_ shape: [Int], mean: Double = 0.0, stdev: Double = 1.0) -> MLShapedArray<Double> {
let count = shape.reduce(1, *)
return .init(scalars: normalArray(count: count, mean: mean, stdev: stdev), shape: shape)
}
}
@@ -0,0 +1,8 @@
import CoreML
@available(iOS 16.2, macOS 13.1, *)
public protocol RandomSource {
mutating func nextNormal(mean: Double, stdev: Double) -> Double
mutating func normalShapedArray(_ shape: [Int], mean: Double, stdev: Double) -> MLShapedArray<Double>
}
@@ -0,0 +1,20 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
/// Protocol for managing internal resources
public protocol ResourceManaging {
/// Request resources to be loaded and ready if possible
func loadResources() throws
/// Request resources are unloaded / remove from memory if possible
func unloadResources()
}
extension ResourceManaging {
/// Request resources are pre-warmed by loading and unloading
func prewarmResources() throws {
try loadResources()
unloadResources()
}
}
@@ -0,0 +1,167 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
import Accelerate
/// Image safety checking model
@available(iOS 16.2, macOS 13.1, *)
public struct SafetyChecker: ResourceManaging {
/// Safety checking Core ML model
var model: ManagedMLModel
/// Creates safety checker
///
/// - Parameters:
/// - url: Location of compiled safety checking Core ML model
/// - configuration: configuration to be used when the model is loaded
/// - Returns: A safety cherker that will lazily load its required resources when needed or requested
public init(modelAt url: URL, configuration: MLModelConfiguration) {
self.model = ManagedMLModel(modelAt: url, configuration: configuration)
}
/// Ensure the model has been loaded into memory
public func loadResources() throws {
try model.loadResources()
}
/// Unload the underlying model to free up memory
public func unloadResources() {
model.unloadResources()
}
typealias PixelBufferPFx1 = vImage.PixelBuffer<vImage.PlanarF>
typealias PixelBufferP8x1 = vImage.PixelBuffer<vImage.Planar8>
typealias PixelBufferPFx3 = vImage.PixelBuffer<vImage.PlanarFx3>
typealias PixelBufferP8x3 = vImage.PixelBuffer<vImage.Planar8x3>
typealias PixelBufferIFx3 = vImage.PixelBuffer<vImage.InterleavedFx3>
typealias PixelBufferI8x3 = vImage.PixelBuffer<vImage.Interleaved8x3>
typealias PixelBufferI8x4 = vImage.PixelBuffer<vImage.Interleaved8x4>
enum SafetyCheckError: Error {
case imageResizeFailure
case imageToFloatFailure
case modelInputFailure
case unexpectedModelOutput
}
/// Check if image is safe
///
/// - Parameters:
/// - image: Image to check
/// - Returns: Whether the model considers the image to be safe
public func isSafe(_ image: CGImage) throws -> Bool {
let inputName = "clip_input"
let adjustmentName = "adjustment"
let imagesNames = "images"
let inputInfo = try model.perform { model in
model.modelDescription.inputDescriptionsByName
}
let inputShape = inputInfo[inputName]!.multiArrayConstraint!.shape
let width = inputShape[2].intValue
let height = inputShape[3].intValue
let resizedImage = try resizeToRGBA(image, width: width, height: height)
let bufferP8x3 = try getRGBPlanes(of: resizedImage)
let arrayPFx3 = normalizeToFloatShapedArray(bufferP8x3)
guard let input = try? MLDictionaryFeatureProvider(
dictionary:[
// Input that is analyzed for safety
inputName : MLMultiArray(arrayPFx3),
// No adjustment, use default threshold
adjustmentName : MLMultiArray(MLShapedArray<Float32>(scalars: [0], shape: [1])),
// Supplying dummy images to be filtered (will be ignored)
imagesNames : MLMultiArray(shape:[1, 512, 512, 3], dataType: .float16)
]
) else {
throw SafetyCheckError.modelInputFailure
}
let result = try model.perform { model in
try model.prediction(from: input)
}
let output = result.featureValue(for: "has_nsfw_concepts")
guard let unsafe = output?.multiArrayValue?[0].boolValue else {
throw SafetyCheckError.unexpectedModelOutput
}
return !unsafe
}
func resizeToRGBA(_ image: CGImage,
width: Int, height: Int) throws -> CGImage {
guard let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width*4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) else {
throw SafetyCheckError.imageResizeFailure
}
context.interpolationQuality = .high
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let resizedImage = context.makeImage() else {
throw SafetyCheckError.imageResizeFailure
}
return resizedImage
}
func getRGBPlanes(of rgbaImage: CGImage) throws -> PixelBufferP8x3 {
// Reference as interleaved 8 bit vImage PixelBuffer
var emptyFormat = vImage_CGImageFormat()
guard let bufferI8x4 = try? PixelBufferI8x4(
cgImage: rgbaImage,
cgImageFormat:&emptyFormat) else {
throw SafetyCheckError.imageToFloatFailure
}
// Drop the alpha channel, keeping RGB
let bufferI8x3 = PixelBufferI8x3(width: rgbaImage.width, height:rgbaImage.height)
bufferI8x4.convert(to: bufferI8x3, channelOrdering: .RGBA)
// De-interleave into 8-bit planes
return PixelBufferP8x3(interleavedBuffer: bufferI8x3)
}
func normalizeToFloatShapedArray(_ bufferP8x3: PixelBufferP8x3) -> MLShapedArray<Float32> {
let width = bufferP8x3.width
let height = bufferP8x3.height
let means = [0.485, 0.456, 0.406] as [Float]
let stds = [0.229, 0.224, 0.225] as [Float]
// Convert to normalized float 1x3xWxH input (plannar)
let arrayPFx3 = MLShapedArray<Float32>(repeating: 0.0, shape: [1, 3, width, height])
for c in 0..<3 {
arrayPFx3[0][c].withUnsafeShapedBufferPointer { ptr, _, strides in
let floatChannel = PixelBufferPFx1(data: .init(mutating: ptr.baseAddress!),
width: width, height: height,
byteCountPerRow: strides[0]*4)
bufferP8x3.withUnsafePixelBuffer(at: c) { uint8Channel in
uint8Channel.convert(to: floatChannel) // maps [0 255] -> [0 1]
floatChannel.multiply(by: 1.0/stds[c],
preBias: -means[c],
postBias: 0.0,
destination: floatChannel)
}
}
}
return arrayPFx3
}
}
@@ -0,0 +1,78 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
/// A utility for timing events and tracking time statistics
///
/// Typical usage
/// ```
/// let timer: SampleTimer
///
/// for i in 0...<iterationCount {
/// timer.start()
/// doStuff()
/// timer.stop()
/// }
///
/// print(String(format: "mean: %.2f, var: %.2f",
/// timer.mean, timer.variance))
/// ```
@available(iOS 16.2, macOS 13.1, *)
public final class SampleTimer: Codable {
var startTime: CFAbsoluteTime?
var sum: Double = 0.0
var sumOfSquares: Double = 0.0
var count = 0
var samples: [Double] = []
public init() {}
/// Start a sample, noting the current time
public func start() {
startTime = CFAbsoluteTimeGetCurrent()
}
// Stop a sample and record the elapsed time
@discardableResult public func stop() -> Double {
guard let startTime = startTime else {
return 0
}
let elapsed = CFAbsoluteTimeGetCurrent() - startTime
sum += elapsed
sumOfSquares += elapsed * elapsed
count += 1
samples.append(elapsed)
return elapsed
}
/// Mean of all sampled times
public var mean: Double { sum / Double(count) }
/// Variance of all sampled times
public var variance: Double {
guard count > 1 else {
return 0.0
}
return sumOfSquares / Double(count - 1) - mean * mean
}
/// Standard deviation of all sampled times
public var stdev: Double { variance.squareRoot() }
/// Median of all sampled times
public var median: Double {
let sorted = samples.sorted()
let (q, r) = sorted.count.quotientAndRemainder(dividingBy: 2)
if r == 0 {
return (sorted[q] + sorted[q - 1]) / 2.0
} else {
return Double(sorted[q])
}
}
public var allSamples: [Double] {
samples
}
}
@@ -0,0 +1,363 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Accelerate
import CoreML
@available(iOS 16.2, macOS 13.1, *)
public protocol Scheduler {
/// Number of diffusion steps performed during training
var trainStepCount: Int { get }
/// Number of inference steps to be performed
var inferenceStepCount: Int { get }
/// Training diffusion time steps index by inference time step
var timeSteps: [Int] { get }
/// Training diffusion time steps index by inference time step
func calculateTimesteps(strength: Float?) -> [Int]
/// Schedule of betas which controls the amount of noise added at each timestep
var betas: [Float] { get }
/// 1 - betas
var alphas: [Float] { get }
/// Cached cumulative product of alphas
var alphasCumProd: [Float] { get }
/// Standard deviation of the initial noise distribution
var initNoiseSigma: Float { get }
/// Denoised latents
var modelOutputs: [MLShapedArray<Float32>] { get }
/// Compute a de-noised image sample and step scheduler state
///
/// - Parameters:
/// - output: The predicted residual noise output of learned diffusion model
/// - timeStep: The current time step in the diffusion chain
/// - sample: The current input sample to the diffusion model
/// - Returns: Predicted de-noised sample at the previous time step
/// - Postcondition: The scheduler state is updated.
/// The state holds the current sample and history of model output noise residuals
func step(
output: MLShapedArray<Float32>,
timeStep t: Int,
sample s: MLShapedArray<Float32>
) -> MLShapedArray<Float32>
}
@available(iOS 16.2, macOS 13.1, *)
public extension Scheduler {
var initNoiseSigma: Float { 1 }
}
@available(iOS 16.2, macOS 13.1, *)
public extension Scheduler {
/// Compute weighted sum of shaped arrays of equal shapes
///
/// - Parameters:
/// - weights: The weights each array is multiplied by
/// - values: The arrays to be weighted and summed
/// - Returns: sum_i weights[i]*values[i]
func weightedSum(_ weights: [Double], _ values: [MLShapedArray<Float32>]) -> MLShapedArray<Float32> {
let scalarCount = values.first!.scalarCount
assert(weights.count > 1 && values.count == weights.count)
assert(values.allSatisfy({ $0.scalarCount == scalarCount }))
return MLShapedArray(unsafeUninitializedShape: values.first!.shape) { scalars, _ in
scalars.initialize(repeating: 0.0)
for i in 0 ..< values.count {
let w = Float(weights[i])
values[i].withUnsafeShapedBufferPointer { buffer, _, _ in
assert(buffer.count == scalarCount)
// scalars[j] = w * values[i].scalars[j]
cblas_saxpy(Int32(scalarCount), w, buffer.baseAddress, 1, scalars.baseAddress, 1)
}
}
}
}
func addNoise(
originalSample: MLShapedArray<Float32>,
noise: [MLShapedArray<Float32>],
strength: Float
) -> [MLShapedArray<Float32>] {
let startStep = max(inferenceStepCount - Int(Float(inferenceStepCount) * strength), 0)
let alphaProdt = alphasCumProd[timeSteps[startStep]]
let betaProdt = 1 - alphaProdt
let sqrtAlphaProdt = sqrt(alphaProdt)
let sqrtBetaProdt = sqrt(betaProdt)
let noisySamples = noise.map {
weightedSum(
[Double(sqrtAlphaProdt), Double(sqrtBetaProdt)],
[originalSample, $0]
)
}
return noisySamples
}
}
// MARK: - Timesteps
@available(iOS 16.2, macOS 13.1, *)
public extension Scheduler {
func calculateTimesteps(strength: Float?) -> [Int] {
guard let strength else { return timeSteps }
let startStep = max(inferenceStepCount - Int(Float(inferenceStepCount) * strength), 0)
let actualTimesteps = Array(timeSteps[startStep...])
return actualTimesteps
}
}
// MARK: - BetaSchedule
/// How to map a beta range to a sequence of betas to step over
@available(iOS 16.2, macOS 13.1, *)
public enum BetaSchedule {
/// Linear stepping between start and end
case linear
/// Steps using linspace(sqrt(start),sqrt(end))^2
case scaledLinear
}
// MARK: - PNDMScheduler
/// A scheduler used to compute a de-noised image
///
/// This implementation matches:
/// [Hugging Face Diffusers PNDMScheduler](https://github.com/huggingface/diffusers/blob/main/src/diffusers/schedulers/scheduling_pndm.py)
///
/// This scheduler uses the pseudo linear multi-step (PLMS) method only, skipping pseudo Runge-Kutta (PRK) steps
@available(iOS 16.2, macOS 13.1, *)
public final class PNDMScheduler: Scheduler {
public let trainStepCount: Int
public let inferenceStepCount: Int
public let betas: [Float]
public let alphas: [Float]
public let alphasCumProd: [Float]
public let timeSteps: [Int]
public let alpha_t: [Float]
public let sigma_t: [Float]
public let lambda_t: [Float]
public private(set) var modelOutputs: [MLShapedArray<Float32>] = []
// Internal state
var counter: Int
var ets: [MLShapedArray<Float32>]
var currentSample: MLShapedArray<Float32>?
/// Create a scheduler that uses a pseudo linear multi-step (PLMS) method
///
/// - Parameters:
/// - stepCount: Number of inference steps to schedule
/// - trainStepCount: Number of training diffusion steps
/// - betaSchedule: Method to schedule betas from betaStart to betaEnd
/// - betaStart: The starting value of beta for inference
/// - betaEnd: The end value for beta for inference
/// - Returns: A scheduler ready for its first step
public init(
stepCount: Int = 50,
trainStepCount: Int = 1000,
betaSchedule: BetaSchedule = .scaledLinear,
betaStart: Float = 0.00085,
betaEnd: Float = 0.012
) {
self.trainStepCount = trainStepCount
self.inferenceStepCount = stepCount
switch betaSchedule {
case .linear:
self.betas = linspace(betaStart, betaEnd, trainStepCount)
case .scaledLinear:
self.betas = linspace(pow(betaStart, 0.5), pow(betaEnd, 0.5), trainStepCount).map({ $0 * $0 })
}
self.alphas = betas.map({ 1.0 - $0 })
var alphasCumProd = self.alphas
for i in 1..<alphasCumProd.count {
alphasCumProd[i] *= alphasCumProd[i - 1]
}
self.alphasCumProd = alphasCumProd
let stepsOffset = 1 // For stable diffusion
let stepRatio = Float(trainStepCount / stepCount )
let forwardSteps = (0..<stepCount).map {
Int((Float($0) * stepRatio).rounded()) + stepsOffset
}
self.alpha_t = vForce.sqrt(self.alphasCumProd)
self.sigma_t = vForce.sqrt(vDSP.subtract([Float](repeating: 1, count: self.alphasCumProd.count), self.alphasCumProd))
self.lambda_t = zip(self.alpha_t, self.sigma_t).map { α, σ in log(α) - log(σ) }
var timeSteps: [Int] = []
timeSteps.append(contentsOf: forwardSteps.dropLast(1))
timeSteps.append(timeSteps.last!)
timeSteps.append(forwardSteps.last!)
timeSteps.reverse()
self.timeSteps = timeSteps
self.counter = 0
self.ets = []
self.currentSample = nil
}
/// Compute a de-noised image sample and step scheduler state
///
/// - Parameters:
/// - output: The predicted residual noise output of learned diffusion model
/// - timeStep: The current time step in the diffusion chain
/// - sample: The current input sample to the diffusion model
/// - Returns: Predicted de-noised sample at the previous time step
/// - Postcondition: The scheduler state is updated.
/// The state holds the current sample and history of model output noise residuals
public func step(
output: MLShapedArray<Float32>,
timeStep t: Int,
sample s: MLShapedArray<Float32>
) -> MLShapedArray<Float32> {
var timeStep = t
let stepInc = (trainStepCount / inferenceStepCount)
var prevStep = timeStep - stepInc
var modelOutput = output
var sample = s
if counter != 1 {
if ets.count > 3 {
ets = Array(ets[(ets.count - 3)..<ets.count])
}
ets.append(output)
} else {
prevStep = timeStep
timeStep = timeStep + stepInc
}
if ets.count == 1 && counter == 0 {
modelOutput = output
currentSample = sample
} else if ets.count == 1 && counter == 1 {
modelOutput = weightedSum(
[1.0/2.0, 1.0/2.0],
[output, ets[back: 1]]
)
sample = currentSample!
currentSample = nil
} else if ets.count == 2 {
modelOutput = weightedSum(
[3.0/2.0, -1.0/2.0],
[ets[back: 1], ets[back: 2]]
)
} else if ets.count == 3 {
modelOutput = weightedSum(
[23.0/12.0, -16.0/12.0, 5.0/12.0],
[ets[back: 1], ets[back: 2], ets[back: 3]]
)
} else {
modelOutput = weightedSum(
[55.0/24.0, -59.0/24.0, 37/24.0, -9/24.0],
[ets[back: 1], ets[back: 2], ets[back: 3], ets[back: 4]]
)
}
let convertedOutput = convertModelOutput(modelOutput: modelOutput, timestep: timeStep, sample: sample)
modelOutputs.append(convertedOutput)
let prevSample = previousSample(sample, timeStep, prevStep, modelOutput)
counter += 1
return prevSample
}
/// Convert the model output to the corresponding type the algorithm needs.
func convertModelOutput(modelOutput: MLShapedArray<Float32>, timestep: Int, sample: MLShapedArray<Float32>) -> MLShapedArray<Float32> {
assert(modelOutput.scalarCount == sample.scalarCount)
let scalarCount = modelOutput.scalarCount
let (alpha_t, sigma_t) = (self.alpha_t[timestep], self.sigma_t[timestep])
return MLShapedArray(unsafeUninitializedShape: modelOutput.shape) { scalars, _ in
assert(scalars.count == scalarCount)
modelOutput.withUnsafeShapedBufferPointer { modelOutput, _, _ in
sample.withUnsafeShapedBufferPointer { sample, _, _ in
for i in 0 ..< scalarCount {
scalars.initializeElement(at: i, to: (sample[i] - modelOutput[i] * sigma_t) / alpha_t)
}
}
}
}
}
/// Compute sample (denoised image) at previous step given a current time step
///
/// - Parameters:
/// - sample: The current input to the model x_t
/// - timeStep: The current time step t
/// - prevStep: The previous time step tδ
/// - modelOutput: Predicted noise residual the current time step e_θ(x_t, t)
/// - Returns: Computes previous sample x_(tδ)
func previousSample(
_ sample: MLShapedArray<Float32>,
_ timeStep: Int,
_ prevStep: Int,
_ modelOutput: MLShapedArray<Float32>
) -> MLShapedArray<Float32> {
// Compute x_(tδ) using formula (9) from
// "Pseudo Numerical Methods for Diffusion Models on Manifolds",
// Luping Liu, Yi Ren, Zhijie Lin & Zhou Zhao.
// ICLR 2022
//
// Notation:
//
// alphaProdt α_t
// alphaProdtPrev α_(tδ)
// betaProdt (1 - α_t)
// betaProdtPrev (1 - α_(tδ))
let alphaProdt = alphasCumProd[timeStep]
let alphaProdtPrev = alphasCumProd[max(0,prevStep)]
let betaProdt = 1 - alphaProdt
let betaProdtPrev = 1 - alphaProdtPrev
// sampleCoeff = (α_(tδ) - α_t) divided by
// denominator of x_t in formula (9) and plus 1
// Note: (α_(tδ) - α_t) / (sqrt(α_t) * (sqrt(α_(tδ)) + sqr(α_t))) =
// sqrt(α_(tδ)) / sqrt(α_t))
let sampleCoeff = sqrt(alphaProdtPrev / alphaProdt)
// Denominator of e_θ(x_t, t) in formula (9)
let modelOutputDenomCoeff = alphaProdt * sqrt(betaProdtPrev)
+ sqrt(alphaProdt * betaProdt * alphaProdtPrev)
// full formula (9)
let modelCoeff = -(alphaProdtPrev - alphaProdt)/modelOutputDenomCoeff
let prevSample = weightedSum(
[Double(sampleCoeff), Double(modelCoeff)],
[sample, modelOutput]
)
return prevSample
}
}
/// Evenly spaced floats between specified interval
///
/// - Parameters:
/// - start: Start of the interval
/// - end: End of the interval
/// - count: The number of floats to return between [*start*, *end*]
/// - Returns: Float array with *count* elements evenly spaced between at *start* and *end*
func linspace(_ start: Float, _ end: Float, _ count: Int) -> [Float] {
let scale = (end - start) / Float(count - 1)
return (0..<count).map { Float($0)*scale + start }
}
extension Collection {
/// Collection element index from the back. *self[back: 1]* yields the last element
public subscript(back i: Int) -> Element {
return self[index(endIndex, offsetBy: -i)]
}
}
@@ -0,0 +1,97 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2024 Apple Inc. All Rights Reserved.
import CoreML
import Foundation
import Tokenizers
import Hub
@available(iOS 17.0, macOS 14.0, *)
public extension StableDiffusion3Pipeline {
struct ResourceURLs {
public let textEncoderURL: URL
public let textEncoder2URL: URL
public let textEncoderT5URL: URL
public let mmditURL: URL
public let decoderURL: URL
public let encoderURL: URL
public let vocabURL: URL
public let mergesURL: URL
public let configT5URL: URL
public let dataT5URL: URL
public init(resourcesAt baseURL: URL) {
textEncoderURL = baseURL.appending(path: "TextEncoder.mlmodelc")
textEncoder2URL = baseURL.appending(path: "TextEncoder2.mlmodelc")
textEncoderT5URL = baseURL.appending(path: "TextEncoderT5.mlmodelc")
mmditURL = baseURL.appending(path: "MultiModalDiffusionTransformer.mlmodelc")
decoderURL = baseURL.appending(path: "VAEDecoder.mlmodelc")
encoderURL = baseURL.appending(path: "VAEEncoder.mlmodelc")
vocabURL = baseURL.appending(path: "vocab.json")
mergesURL = baseURL.appending(path: "merges.txt")
configT5URL = baseURL.appending(path: "tokenizer_config.json")
dataT5URL = baseURL.appending(path: "tokenizer.json")
}
}
/// Create stable diffusion pipeline using model resources at a
/// specified URL
///
/// - Parameters:
/// - baseURL: URL pointing to directory holding all model and tokenization resources
/// - configuration: The configuration to load model resources with
/// - reduceMemory: Setup pipeline in reduced memory mode
/// - Returns:
/// Pipeline ready for image generation if all necessary resources loaded
init(
resourcesAt baseURL: URL,
configuration config: MLModelConfiguration = .init(),
reduceMemory: Bool = false
) throws {
// Expect URL of each resource
let urls = ResourceURLs(resourcesAt: baseURL)
let tokenizer = try BPETokenizer(mergesAt: urls.mergesURL, vocabularyAt: urls.vocabURL)
let textEncoder = TextEncoderXL(tokenizer: tokenizer, modelAt: urls.textEncoderURL, configuration: config)
// padToken is different in the second XL text encoder
let tokenizer2 = try BPETokenizer(mergesAt: urls.mergesURL, vocabularyAt: urls.vocabURL, padToken: "!")
let textEncoder2 = TextEncoderXL(tokenizer: tokenizer2, modelAt: urls.textEncoder2URL, configuration: config)
// Optional T5 encoder
var textEncoderT5: TextEncoderT5?
if FileManager.default.fileExists(atPath: urls.configT5URL.path),
FileManager.default.fileExists(atPath: urls.dataT5URL.path),
FileManager.default.fileExists(atPath: urls.textEncoderT5URL.path)
{
let tokenizerT5 = try PreTrainedTokenizer(tokenizerConfig: Config(fileURL: urls.configT5URL), tokenizerData: Config(fileURL: urls.dataT5URL))
textEncoderT5 = TextEncoderT5(tokenizer: tokenizerT5, modelAt: urls.textEncoderT5URL, configuration: config)
} else {
textEncoderT5 = nil
}
// Denoiser model
let mmdit = MultiModalDiffusionTransformer(modelAt: urls.mmditURL, configuration: config)
// Image Decoder
let decoder = Decoder(modelAt: urls.decoderURL, configuration: config)
// Optional Image Encoder
let encoder: Encoder?
if FileManager.default.fileExists(atPath: urls.encoderURL.path) {
encoder = Encoder(modelAt: urls.encoderURL, configuration: config)
} else {
encoder = nil
}
// Construct pipeline
self.init(
textEncoder: textEncoder,
textEncoder2: textEncoder2,
textEncoderT5: textEncoderT5,
mmdit: mmdit,
decoder: decoder,
encoder: encoder,
reduceMemory: reduceMemory
)
}
}
@@ -0,0 +1,486 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2024 Apple Inc. All Rights Reserved.
import Accelerate
import CoreGraphics
import CoreImage
import CoreML
import Foundation
@available(iOS 17.0, macOS 14.0, *)
public struct StableDiffusion3Pipeline: StableDiffusionPipelineProtocol {
public typealias Configuration = PipelineConfiguration
public typealias Progress = PipelineProgress
/// Model to generate embeddings for tokenized input text
var textEncoder: TextEncoderXLModel
var textEncoder2: TextEncoderXLModel
var textEncoderT5: TextEncoderT5Model?
/// Model used to predict noise residuals given an input, diffusion time step, and conditional embedding
var mmdit: MultiModalDiffusionTransformer
/// Model used to generate final image from latent diffusion process
var decoder: Decoder
/// Model used to latent space for image2image, and soon, in-painting
var encoder: Encoder?
/// Option to reduce memory during image generation
///
/// If true, the pipeline will lazily load TextEncoder, Unet, Decoder, and SafetyChecker
/// when needed and aggressively unload their resources after
///
/// This will increase latency in favor of reducing memory
var reduceMemory: Bool = false
/// Creates a pipeline using the specified models and tokenizer
///
/// - Parameters:
/// - textEncoder: Model for encoding tokenized text
/// - textEncoder2: Second text encoding model
/// - mmdit: Model for noise prediction on latent samples
/// - decoder: Model for decoding latent sample to image
/// - reduceMemory: Option to enable reduced memory mode
/// - Returns: Pipeline ready for image generation
public init(
textEncoder: TextEncoderXLModel,
textEncoder2: TextEncoderXLModel,
textEncoderT5: TextEncoderT5?,
mmdit: MultiModalDiffusionTransformer,
decoder: Decoder,
encoder: Encoder?,
reduceMemory: Bool = false
) {
self.textEncoder = textEncoder
self.textEncoder2 = textEncoder2
self.textEncoderT5 = textEncoderT5
self.mmdit = mmdit
self.decoder = decoder
self.encoder = encoder
self.reduceMemory = reduceMemory
}
/// Load required resources for this pipeline
///
/// If reducedMemory is true this will instead call prewarmResources instead
/// and let the pipeline lazily load resources as needed
public func loadResources() throws {
if reduceMemory {
try prewarmResources()
} else {
try textEncoder.loadResources()
try textEncoder2.loadResources()
try textEncoderT5?.loadResources()
try mmdit.loadResources()
try decoder.loadResources()
do {
try encoder?.loadResources()
} catch {
print("Error loading resources for vae encoder: \(error)")
}
}
}
/// Unload the underlying resources to free up memory
public func unloadResources() {
textEncoder.unloadResources()
textEncoder2.unloadResources()
textEncoderT5?.unloadResources()
mmdit.unloadResources()
decoder.unloadResources()
encoder?.unloadResources()
}
/// Prewarm resources one at a time
public func prewarmResources() throws {
try textEncoder.prewarmResources()
try textEncoder2.prewarmResources()
try textEncoderT5?.prewarmResources()
try mmdit.prewarmResources()
try decoder.prewarmResources()
do {
try encoder?.prewarmResources()
} catch {
print("Error prewarming resources for vae encoder: \(error)")
}
}
/// Image generation using stable diffusion
/// - Parameters:
/// - configuration: Image generation configuration
/// - progressHandler: Callback to perform after each step, stops on receiving false response
/// - Returns: An array of `imageCount` optional images.
/// The images will be nil if safety checks were performed and found the result to be un-safe
public func generateImages(
configuration config: Configuration,
progressHandler: (Progress) -> Bool = { _ in true }
) throws -> [CGImage?] {
// Setup geometry conditioning for base/refiner inputs
let sd3Input: ModelInputs = try generateConditioning(using: config)
if reduceMemory {
textEncoder.unloadResources()
textEncoder2.unloadResources()
textEncoderT5?.unloadResources()
}
// Setup schedulers
let scheduler: [DiscreteFlowScheduler] = (0..<config.imageCount).map { _ in
DiscreteFlowScheduler(stepCount: config.stepCount, timeStepShift: config.schedulerTimestepShift)
}
// Generate random latent samples from specified seed
var latents: [MLShapedArray<Float32>] = try generateLatentSamples(configuration: config, scheduler: scheduler[0])
// Store denoised latents from scheduler to pass into decoder
var denoisedLatents: [MLShapedArray<Float32>] = latents.map { MLShapedArray(converting: $0) }
if reduceMemory {
encoder?.unloadResources()
}
let timestepStrength: Float? = config.mode == .imageToImage ? config.strength : nil
// Store current model
let mmditModel = mmdit
let mmditHiddenStates = sd3Input.hiddenStates
let mmditPooledStates = sd3Input.pooledStates
let timeSteps: [Float] = scheduler[0].calculateTimestepsFromSigmas(strength: timestepStrength)
// De-noising loop
for (step, t) in timeSteps.enumerated() {
// Expand the latents for classifier-free guidance
// and input to the MMDiT noise prediction model
let latentUnetInput = latents.map {
MLShapedArray<Float32>(concatenating: [$0, $0], alongAxis: 0)
}
// Predict noise residuals from latent samples
// and current time step conditioned on hidden states
var noise = try mmditModel.predictNoise(
latents: latentUnetInput,
timeStep: t,
tokenLevelTextEmbeddings: mmditHiddenStates,
pooledTextEmbeddings: mmditPooledStates
)
noise = performGuidance(noise, config.guidanceScale)
// Have the scheduler compute the previous (t-1) latent
// sample given the predicted noise and current sample
for i in 0..<config.imageCount {
latents[i] = scheduler[i].step(
output: noise[i],
timeStep: scheduler[i].timeSteps[step], // TODO: allow float timesteps in scheduler step protocol
sample: latents[i]
)
denoisedLatents[i] = scheduler[i].modelOutputs.last ?? latents[i]
}
let currentLatentSamples = config.useDenoisedIntermediates ? denoisedLatents : latents
// Report progress
let progress = Progress(
pipeline: self,
prompt: config.prompt,
step: step,
stepCount: timeSteps.count,
currentLatentSamples: currentLatentSamples,
configuration: config
)
if !progressHandler(progress) {
// Stop if requested by handler
return []
}
}
// Unload resources
if reduceMemory {
mmdit.unloadResources()
}
// Decode the latent samples to images
return try decodeToImages(denoisedLatents, configuration: config)
}
func encodePrompt(_ prompt: String) throws -> (MLShapedArray<Float32>, MLShapedArray<Float32>) {
var embeds = MLShapedArray<Float32>()
var pooled = MLShapedArray<Float32>()
let (embeds1, pooledValue1) = try textEncoder.encode(prompt)
let (embeds2, pooledValue2) = try textEncoder2.encode(prompt)
var embedsT5 = try textEncoderT5?.encode(prompt).encoderHiddenStates ?? MLShapedArray<Float32>(repeating: 0, shape: [1, 4096, 1, 77])
// Truncate T5
embedsT5 = truncatedT5Embeds(embedsT5)
let padding1 = MLShapedArray<Float32>(repeating: 0, shape: [1, 77, 2048])
// Base needs concatenated embeddings
// [1, 77, 768], [1, 77, 1280], [1, 77, 2048] -> [1, 77, 4096]
embeds = MLShapedArray<Float32>(
concatenating: [embeds1, embeds2, padding1],
alongAxis: 2
)
// [1, 77, 4096] -> [1, 4096, 1 77]
embeds = toHiddenStates(embeds)
// [1, 4096, 1 77], [1, 4096, 1, 77] -> [1, 4096, 1, 154]
embeds = MLShapedArray<Float32>(
concatenating: [embeds, embedsT5],
alongAxis: 3
)
// [1, 768], [1, 1280] -> [1, 2048]
pooled = MLShapedArray<Float32>(
concatenating: [pooledValue1, pooledValue2],
alongAxis: 1
)
return (embeds, pooled)
}
func generateConditioning(using config: Configuration) throws -> ModelInputs {
// Encode the input prompt and negative prompt
let (promptEmbedding, pooled) = try encodePrompt(config.prompt)
let (negativePromptEmbedding, negativePooled) = try encodePrompt(config.negativePrompt)
// Convert to Unet hidden state representation
// Concatenate the prompt and negative prompt embeddings
let hiddenStates = MLShapedArray(concatenating: [promptEmbedding, negativePromptEmbedding], alongAxis: 0)
let pooledScalars = MLShapedArray(concatenating: [pooled, negativePooled], alongAxis: 0)
let pooledStates = MLShapedArray<Float32>(
scalars: pooledScalars.scalars,
shape: [2, 2048, 1, 1]
)
return ModelInputs(hiddenStates: hiddenStates, pooledStates: pooledStates)
}
func generateLatentSamples(configuration config: Configuration, scheduler: Scheduler) throws -> [MLShapedArray<Float32>] {
var sampleShape = mmdit.latentImageEmbeddingsShape
sampleShape[0] = 1
let stdev = scheduler.initNoiseSigma
var random = randomSource(from: config.rngType, seed: config.seed)
let samples = (0..<config.imageCount).map { _ in
MLShapedArray<Float32>(
converting: random.normalShapedArray(sampleShape, mean: 0.0, stdev: Double(stdev)))
}
if let image = config.startingImage, config.mode == .imageToImage {
guard let encoder else {
throw PipelineError.startingImageProvidedWithoutEncoder
}
let latent = try encoder.encode(image, scaleFactor: config.encoderScaleFactor, random: &random)
return scheduler.addNoise(originalSample: latent, noise: samples, strength: config.strength)
}
return samples
}
func performGuidance(_ noise: [MLShapedArray<Float32>], _ guidanceScale: Float) -> [MLShapedArray<Float32>] {
noise.map { performGuidance($0, guidanceScale) }
}
func performGuidance(_ noise: MLShapedArray<Float32>, _ guidanceScale: Float) -> MLShapedArray<Float32> {
var shape = noise.shape
shape[0] = 1
return MLShapedArray<Float>(unsafeUninitializedShape: shape) { result, _ in
noise.withUnsafeShapedBufferPointer { scalars, _, strides in
for i in 0..<result.count {
// unconditioned + guidance*(text - unconditioned)
let text = scalars[i]
let negText = scalars[strides[0] + i]
let guidance = negText + guidanceScale * (text - negText)
result.initializeElement(
at: i,
to: guidance
)
}
}
}
}
public func decodeToImages(_ latents: [MLShapedArray<Float32>], configuration config: Configuration) throws -> [CGImage?] {
defer {
if reduceMemory {
decoder.unloadResources()
}
}
return try decoder.decode(latents, scaleFactor: config.decoderScaleFactor, shiftFactor: config.decoderShiftFactor)
// TODO: use latent rgb factors with blur for preview images
// This will require a method to decode with either the vae or the rgb factors depending on config
// return try decodePreviewImage(latents, scaleFactor: config.decoderScaleFactor)
}
/// Shape 16 x 3
let rgbFactors: [[Float]] = [
[-0.0645, 0.0177, 0.1052], [ 0.0028, 0.0312, 0.0650],
[ 0.1848, 0.0762, 0.0360], [ 0.0944, 0.0360, 0.0889],
[ 0.0897, 0.0506, -0.0364], [-0.0020, 0.1203, 0.0284],
[ 0.0855, 0.0118, 0.0283], [-0.0539, 0.0658, 0.1047],
[-0.0057, 0.0116, 0.0700], [-0.0412, 0.0281, -0.0039],
[ 0.1106, 0.1171, 0.1220], [-0.0248, 0.0682, -0.0481],
[ 0.0815, 0.0846, 0.1207], [-0.0120, -0.0055, -0.0867],
[-0.0749, -0.0634, -0.0456], [-0.1418, -0.1457, -0.1259]
]
public func decodePreviewImage(
_ latents: [MLShapedArray<Float32>],
scaleFactor: Float32
) throws -> [CGImage] {
let height = 64
let width = 64
let channels = 16
let outputChannels = 3
// Ensure there is a first element in latents and extract its scalars
guard let latentScalars = latents.first?.scalars else {
throw NSError(domain: "DecodeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid latent array"])
}
// The latentScalars is a flat array, we need to reshape and multiply
var reshapedLatent = [Float32](repeating: 0, count: height * width * channels)
// We reorder the indices manually to switch from [channels, height, width] to [height, width, channels]
for h in 0..<height {
for w in 0..<width {
for c in 0..<channels {
let oldIndex = c * height * width + h * width + w
let newIndex = h * width * channels + w * channels + c
reshapedLatent[newIndex] = latentScalars[oldIndex] // 1.5305 + 0.0609
}
}
}
// Prepare to hold the result of the multiplication
var imageArray = [Float32](repeating: 0, count: height * width * outputChannels)
// Perform matrix multiplication using Accelerate
vDSP_mmul(reshapedLatent, 1,
rgbFactors.flatMap { $0 }, 1,
&imageArray, 1,
vDSP_Length(height * width), // number of rows in output
vDSP_Length(outputChannels), // number of columns in output
vDSP_Length(channels)) // common dimension
// Convert imageArray into a CGImage
let latentImage = imageArray.toCGImage(width: width, height: height)
// Apply a Gaussian blur to the preview image to reduce pixeled look
let ciImage = CIImage(cgImage: latentImage!)
let blurFilter = CIFilter(name: "CIGaussianBlur")!
blurFilter.setValue(ciImage, forKey: kCIInputImageKey)
blurFilter.setValue(4.0, forKey: kCIInputRadiusKey)
let context = CIContext()
guard let outputImage = blurFilter.outputImage,
let cgBlurredPreview = context.createCGImage(outputImage, from: ciImage.extent)
else {
throw PipelineError.errorCreatingPreview
}
return [cgBlurredPreview]
}
struct ModelInputs {
var hiddenStates: MLShapedArray<Float32>
var pooledStates: MLShapedArray<Float32>
}
/// Helper function to truncate the T5 embeddings
func truncatedT5Embeds(_ embedding: MLShapedArray<Float32>) -> MLShapedArray<Float32> {
// Unoptimized manual truncation
// e.g. From [1, 4096, 1, 128] to [1, 4096, 1, 77]
let fromShape = embedding.shape
let stateShape = [fromShape[0], fromShape[1], fromShape[2], 77]
var states = MLShapedArray<Float32>(repeating: 0.0, shape: stateShape)
for i0 in 0..<fromShape[0] {
for i1 in 0..<fromShape[1] {
for i2 in 0..<fromShape[2] {
for i3 in 0..<stateShape[3] {
states[scalarAt: i0, i1, i2, i3] = embedding[scalarAt: i0, i1, i2, i3]
}
}
}
}
return states
}
}
extension Array where Element == Float32 {
func toCGImage(width: Int, height: Int) -> CGImage? {
// Define color space and bitmap info
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.premultipliedLast.rawValue
// Calculate bytes per pixel and bytes per row
let bytesPerPixel = 4
let bytesPerRow = width * bytesPerPixel
// Allocate memory for the pixel data
var data = [UInt8](repeating: 0, count: height * bytesPerRow)
// Fill the data array with pixel data
for h in 0..<height {
for w in 0..<width {
let pixelIndex = h * width + w
let dataIndex = h * bytesPerRow + w * bytesPerPixel
let pixelBase = pixelIndex * 3 // Base index for R, G, B values in the source array
// Ensure your source array has enough data
if (pixelBase + 3) < self.count {
let redValue = (self[pixelBase] + 1) / 2 * 255
let bluValue = (self[pixelBase + 1] + 1) / 2 * 255
let grnValue = (self[pixelBase + 2] + 1) / 2 * 255
data[dataIndex] = UInt8(clamp(value: redValue, lower: 0, upper: 255)) // Red
data[dataIndex + 1] = UInt8(clamp(value: bluValue, lower: 0, upper: 255)) // Green
data[dataIndex + 2] = UInt8(clamp(value: grnValue, lower: 0, upper: 255)) // Blue
data[dataIndex + 3] = 255 // Alpha
}
}
}
// Create the context
guard let context = CGContext(data: &data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else {
print("Failed to create CGContext.")
return nil
}
// Create a CGImage from context
guard let smallImage = context.makeImage() else {
return nil
}
// Define the upscaled dimensions
let scaledWidth = width * 8
let scaledHeight = height * 8
// Create a new context with scaled dimensions
guard let largeContext = CGContext(data: nil, width: scaledWidth, height: scaledHeight, bitsPerComponent: 8, bytesPerRow: scaledWidth * 4, space: colorSpace, bitmapInfo: bitmapInfo) else {
return nil
}
// Draw the small image into the large context
largeContext.interpolationQuality = .high
largeContext.draw(smallImage, in: CGRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight))
// Convert the upscaled context to a CGImage
return largeContext.makeImage()
}
/// Helper function to clamp the values within the specified range
private func clamp(value: Float32, lower: UInt8, upper: UInt8) -> UInt8 {
return UInt8(Swift.max(Float32(lower), Swift.min(value, Float32(upper))))
}
}
@@ -0,0 +1,166 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
import NaturalLanguage
@available(iOS 16.2, macOS 13.1, *)
public extension StableDiffusionPipeline {
struct ResourceURLs {
public let textEncoderURL: URL
public let unetURL: URL
public let unetChunk1URL: URL
public let unetChunk2URL: URL
public let decoderURL: URL
public let encoderURL: URL
public let safetyCheckerURL: URL
public let vocabURL: URL
public let mergesURL: URL
public let controlNetDirURL: URL
public let controlledUnetURL: URL
public let controlledUnetChunk1URL: URL
public let controlledUnetChunk2URL: URL
public let multilingualTextEncoderProjectionURL: URL
public init(resourcesAt baseURL: URL) {
textEncoderURL = baseURL.appending(path: "TextEncoder.mlmodelc")
unetURL = baseURL.appending(path: "Unet.mlmodelc")
unetChunk1URL = baseURL.appending(path: "UnetChunk1.mlmodelc")
unetChunk2URL = baseURL.appending(path: "UnetChunk2.mlmodelc")
decoderURL = baseURL.appending(path: "VAEDecoder.mlmodelc")
encoderURL = baseURL.appending(path: "VAEEncoder.mlmodelc")
safetyCheckerURL = baseURL.appending(path: "SafetyChecker.mlmodelc")
vocabURL = baseURL.appending(path: "vocab.json")
mergesURL = baseURL.appending(path: "merges.txt")
controlNetDirURL = baseURL.appending(path: "controlnet")
controlledUnetURL = baseURL.appending(path: "ControlledUnet.mlmodelc")
controlledUnetChunk1URL = baseURL.appending(path: "ControlledUnetChunk1.mlmodelc")
controlledUnetChunk2URL = baseURL.appending(path: "ControlledUnetChunk2.mlmodelc")
multilingualTextEncoderProjectionURL = baseURL.appending(path: "MultilingualTextEncoderProjection.mlmodelc")
}
}
/// Create stable diffusion pipeline using model resources at a
/// specified URL
///
/// - Parameters:
/// - baseURL: URL pointing to directory holding all model and tokenization resources
/// - controlNetModelNames: Specify ControlNet models to use in generation
/// - configuration: The configuration to load model resources with
/// - disableSafety: Load time disable of safety to save memory
/// - reduceMemory: Setup pipeline in reduced memory mode
/// - useMultilingualTextEncoder: Option to use system multilingual NLContextualEmbedding as encoder
/// - script: Optional natural language script to use for the text encoder.
/// - Returns:
/// Pipeline ready for image generation if all necessary resources loaded
init(
resourcesAt baseURL: URL,
controlNet controlNetModelNames: [String],
configuration config: MLModelConfiguration = .init(),
disableSafety: Bool = false,
reduceMemory: Bool = false,
useMultilingualTextEncoder: Bool = false,
script: Script? = nil
) throws {
/// Expect URL of each resource
let urls = ResourceURLs(resourcesAt: baseURL)
let textEncoder: TextEncoderModel
#if canImport(NaturalLanguage.NLScript)
if useMultilingualTextEncoder {
guard #available(macOS 14.0, iOS 17.0, *) else { throw PipelineError.unsupportedOSVersion }
textEncoder = MultilingualTextEncoder(
modelAt: urls.multilingualTextEncoderProjectionURL,
configuration: config,
script: script ?? .latin
)
} else {
let tokenizer = try BPETokenizer(mergesAt: urls.mergesURL, vocabularyAt: urls.vocabURL)
textEncoder = TextEncoder(tokenizer: tokenizer, modelAt: urls.textEncoderURL, configuration: config)
}
#else
let tokenizer = try BPETokenizer(mergesAt: urls.mergesURL, vocabularyAt: urls.vocabURL)
textEncoder = TextEncoder(tokenizer: tokenizer, modelAt: urls.textEncoderURL, configuration: config)
#endif
// ControlNet model
var controlNet: ControlNet? = nil
let controlNetURLs = controlNetModelNames.map { model in
let fileName = model + ".mlmodelc"
return urls.controlNetDirURL.appending(path: fileName)
}
if !controlNetURLs.isEmpty {
controlNet = ControlNet(modelAt: controlNetURLs, configuration: config)
}
// Unet model
let unet: Unet
let unetURL: URL, unetChunk1URL: URL, unetChunk2URL: URL
// if ControlNet available, Unet supports additional inputs from ControlNet
if controlNet == nil {
unetURL = urls.unetURL
unetChunk1URL = urls.unetChunk1URL
unetChunk2URL = urls.unetChunk2URL
} else {
unetURL = urls.controlledUnetURL
unetChunk1URL = urls.controlledUnetChunk1URL
unetChunk2URL = urls.controlledUnetChunk2URL
}
if FileManager.default.fileExists(atPath: unetChunk1URL.path) &&
FileManager.default.fileExists(atPath: unetChunk2URL.path) {
unet = Unet(chunksAt: [unetChunk1URL, unetChunk2URL],
configuration: config)
} else {
unet = Unet(modelAt: unetURL, configuration: config)
}
// Image Decoder
let decoder = Decoder(modelAt: urls.decoderURL, configuration: config)
// Optional safety checker
var safetyChecker: SafetyChecker? = nil
if !disableSafety &&
FileManager.default.fileExists(atPath: urls.safetyCheckerURL.path) {
safetyChecker = SafetyChecker(modelAt: urls.safetyCheckerURL, configuration: config)
}
// Optional Image Encoder
let encoder: Encoder?
if FileManager.default.fileExists(atPath: urls.encoderURL.path) {
encoder = Encoder(modelAt: urls.encoderURL, configuration: config)
} else {
encoder = nil
}
// Construct pipeline
if #available(macOS 14.0, iOS 17.0, *) {
self.init(
textEncoder: textEncoder,
unet: unet,
decoder: decoder,
encoder: encoder,
controlNet: controlNet,
safetyChecker: safetyChecker,
reduceMemory: reduceMemory,
useMultilingualTextEncoder: useMultilingualTextEncoder,
script: script
)
} else {
self.init(
textEncoder: textEncoder,
unet: unet,
decoder: decoder,
encoder: encoder,
controlNet: controlNet,
safetyChecker: safetyChecker,
reduceMemory: reduceMemory
)
}
}
}
@@ -0,0 +1,100 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreGraphics
/// Type of processing that will be performed to generate an image
public enum PipelineMode {
case textToImage
case imageToImage
// case inPainting
}
/// Image generation configuration
public struct PipelineConfiguration: Hashable {
/// Text prompt to guide sampling
public var prompt: String
/// Negative text prompt to guide sampling
public var negativePrompt: String = ""
/// Starting image for image2image or in-painting
public var startingImage: CGImage? = nil
/// Fraction of inference steps to be used in `.imageToImage` pipeline mode
/// Must be between 0 and 1
/// Higher values will result in greater transformation of the `startingImage`
public var strength: Float = 1.0
/// Fraction of inference steps to at which to start using the refiner unet if present in `textToImage` mode
/// Must be between 0 and 1
/// Higher values will result in fewer refiner steps
public var refinerStart: Float = 0.8
/// Number of images to generate
public var imageCount: Int = 1
/// Number of inference steps to perform
public var stepCount: Int = 50
/// Random seed which to start generation
public var seed: UInt32 = 0
/// Controls the influence of the text prompt on sampling process (0=random images)
public var guidanceScale: Float = 7.5
/// List of Images for available ControlNet Models
public var controlNetInputs: [CGImage] = []
/// Safety checks are only performed if `self.canSafetyCheck && !disableSafety`
public var disableSafety: Bool = false
/// Enables progress updates to decode `currentImages` from denoised latent images for better previews
public var useDenoisedIntermediates: Bool = false
/// The type of Scheduler to use.
public var schedulerType: StableDiffusionScheduler = .pndmScheduler
/// The spacing to use for scheduler sigmas and time steps. Only supported when using `.dpmppScheduler`.
public var schedulerTimestepSpacing: TimeStepSpacing = .linspace
/// Resolution dependent shifting of timestep schedules
public var schedulerTimestepShift: Float = 3.0
/// The type of RNG to use
public var rngType: StableDiffusionRNG = .numpyRNG
/// Scale factor to use on the latent after encoding
public var encoderScaleFactor: Float32 = 0.18215
/// Scale factor to use on the latent before decoding
public var decoderScaleFactor: Float32 = 0.18215
/// Shift factor to use on the latent before decoding
public var decoderShiftFactor: Float32 = 0.0
/// If `originalSize` is not the same as `targetSize` the image will appear to be down- or upsampled.
/// Part of SDXLs micro-conditioning as explained in section 2.2 of https://huggingface.co/papers/2307.01952.
public var originalSize: Float32 = 1024
/// `cropsCoordsTopLeft` can be used to generate an image that appears to be cropped from the position `cropsCoordsTopLeft` downwards.
/// Favorable, well-centered images are usually achieved by setting `cropsCoordsTopLeft` to (0, 0).
public var cropsCoordsTopLeft: Float32 = 0
/// For most cases, `target_size` should be set to the desired height and width of the generated image.
public var targetSize: Float32 = 1024
/// Used to simulate an aesthetic score of the generated image by influencing the positive text condition.
public var aestheticScore: Float32 = 6
/// Can be used to simulate an aesthetic score of the generated image by influencing the negative text condition.
public var negativeAestheticScore: Float32 = 2.5
/// Given the configuration, what mode will be used for generation
public var mode: PipelineMode {
guard startingImage != nil else {
return .textToImage
}
guard strength < 1.0 else {
return .textToImage
}
return .imageToImage
}
public init(
prompt: String
) {
self.prompt = prompt
}
}
@available(iOS 16.2, macOS 13.1, *)
public extension StableDiffusionPipeline {
/// Type of processing that will be performed to generate an image
typealias Mode = PipelineMode
/// Image generation configuration
typealias Configuration = PipelineConfiguration
}
@@ -0,0 +1,484 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Accelerate
import CoreGraphics
import CoreML
import Foundation
import NaturalLanguage
/// Schedulers compatible with StableDiffusionPipeline
public enum StableDiffusionScheduler {
/// Scheduler that uses a pseudo-linear multi-step (PLMS) method
case pndmScheduler
/// Scheduler that uses a second order DPM-Solver++ algorithm
case dpmSolverMultistepScheduler
/// Scheduler for rectified flow based multimodal diffusion transformer models
case discreteFlowScheduler
}
/// RNG compatible with StableDiffusionPipeline
public enum StableDiffusionRNG {
/// RNG that matches numpy implementation
case numpyRNG
/// RNG that matches PyTorch CPU implementation.
case torchRNG
/// RNG that matches PyTorch CUDA implementation.
case nvidiaRNG
}
public enum PipelineError: String, Swift.Error {
case missingUnetInputs
case startingImageProvidedWithoutEncoder
case startingText2ImgWithoutTextEncoder
case unsupportedOSVersion
case errorCreatingPreview
}
@available(iOS 16.2, macOS 13.1, *)
public protocol StableDiffusionPipelineProtocol: ResourceManaging {
var canSafetyCheck: Bool { get }
func generateImages(
configuration config: PipelineConfiguration,
progressHandler: (PipelineProgress) -> Bool
) throws -> [CGImage?]
func decodeToImages(
_ latents: [MLShapedArray<Float32>],
configuration config: PipelineConfiguration
) throws -> [CGImage?]
}
@available(iOS 16.2, macOS 13.1, *)
public extension StableDiffusionPipelineProtocol {
var canSafetyCheck: Bool { false }
}
/// A pipeline used to generate image samples from text input using stable diffusion
///
/// This implementation matches:
/// [Hugging Face Diffusers Pipeline](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py)
@available(iOS 16.2, macOS 13.1, *)
public struct StableDiffusionPipeline: StableDiffusionPipelineProtocol {
/// Model to generate embeddings for tokenized input text
var textEncoder: TextEncoderModel
/// Model used to predict noise residuals given an input, diffusion time step, and conditional embedding
var unet: Unet
/// Model used to generate final image from latent diffusion process
var decoder: Decoder
/// Model used to latent space for image2image, and soon, in-painting
var encoder: Encoder?
/// Optional model for checking safety of generated image
var safetyChecker: SafetyChecker? = nil
/// Optional model used before Unet to control generated images by additonal inputs
var controlNet: ControlNet? = nil
/// Reports whether this pipeline can perform safety checks
public var canSafetyCheck: Bool {
safetyChecker != nil
}
/// Option to reduce memory during image generation
///
/// If true, the pipeline will lazily load TextEncoder, Unet, Decoder, and SafetyChecker
/// when needed and aggressively unload their resources after
///
/// This will increase latency in favor of reducing memory
var reduceMemory: Bool = false
/// Option to use system multilingual NLContextualEmbedding as encoder
var useMultilingualTextEncoder: Bool = false
/// Optional natural language script to use for the text encoder.
var script: Script? = nil
/// Creates a pipeline using the specified models and tokenizer
///
/// - Parameters:
/// - textEncoder: Model for encoding tokenized text
/// - unet: Model for noise prediction on latent samples
/// - decoder: Model for decoding latent sample to image
/// - controlNet: Optional model to control generated images by additonal inputs
/// - safetyChecker: Optional model for checking safety of generated images
/// - reduceMemory: Option to enable reduced memory mode
/// - Returns: Pipeline ready for image generation
public init(
textEncoder: TextEncoderModel,
unet: Unet,
decoder: Decoder,
encoder: Encoder?,
controlNet: ControlNet? = nil,
safetyChecker: SafetyChecker? = nil,
reduceMemory: Bool = false
) {
self.textEncoder = textEncoder
self.unet = unet
self.decoder = decoder
self.encoder = encoder
self.controlNet = controlNet
self.safetyChecker = safetyChecker
self.reduceMemory = reduceMemory
}
/// Creates a pipeline using the specified models and tokenizer
///
/// - Parameters:
/// - textEncoder: Model for encoding tokenized text
/// - unet: Model for noise prediction on latent samples
/// - decoder: Model for decoding latent sample to image
/// - controlNet: Optional model to control generated images by additonal inputs
/// - safetyChecker: Optional model for checking safety of generated images
/// - reduceMemory: Option to enable reduced memory mode
/// - useMultilingualTextEncoder: Option to use system multilingual NLContextualEmbedding as encoder
/// - script: Optional natural language script to use for the text encoder.
/// - Returns: Pipeline ready for image generation
@available(iOS 17.0, macOS 14.0, *)
public init(
textEncoder: TextEncoderModel,
unet: Unet,
decoder: Decoder,
encoder: Encoder?,
controlNet: ControlNet? = nil,
safetyChecker: SafetyChecker? = nil,
reduceMemory: Bool = false,
useMultilingualTextEncoder: Bool = false,
script: Script? = nil
) {
self.textEncoder = textEncoder
self.unet = unet
self.decoder = decoder
self.encoder = encoder
self.controlNet = controlNet
self.safetyChecker = safetyChecker
self.reduceMemory = reduceMemory
self.useMultilingualTextEncoder = useMultilingualTextEncoder
self.script = script
}
/// Load required resources for this pipeline
///
/// If reducedMemory is true this will instead call prewarmResources instead
/// and let the pipeline lazily load resources as needed
public func loadResources() throws {
if reduceMemory {
try prewarmResources()
} else {
try unet.loadResources()
try textEncoder.loadResources()
try decoder.loadResources()
try encoder?.loadResources()
try controlNet?.loadResources()
try safetyChecker?.loadResources()
}
}
/// Unload the underlying resources to free up memory
public func unloadResources() {
textEncoder.unloadResources()
unet.unloadResources()
decoder.unloadResources()
encoder?.unloadResources()
controlNet?.unloadResources()
safetyChecker?.unloadResources()
}
// Prewarm resources one at a time
public func prewarmResources() throws {
try textEncoder.prewarmResources()
try unet.prewarmResources()
try decoder.prewarmResources()
try encoder?.prewarmResources()
try controlNet?.prewarmResources()
try safetyChecker?.prewarmResources()
}
/// Image generation using stable diffusion
/// - Parameters:
/// - configuration: Image generation configuration
/// - progressHandler: Callback to perform after each step, stops on receiving false response
/// - Returns: An array of `imageCount` optional images.
/// The images will be nil if safety checks were performed and found the result to be un-safe
public func generateImages(
configuration config: Configuration,
progressHandler: (Progress) -> Bool = { _ in true }
) throws -> [CGImage?] {
// Encode the input prompt
var promptEmbedding = try textEncoder.encode(config.prompt)
if config.guidanceScale >= 1.0 {
// Convert to Unet hidden state representation
// Concatenate the prompt and negative prompt embeddings
let negativePromptEmbedding = try textEncoder.encode(config.negativePrompt)
promptEmbedding = MLShapedArray<Float32>(
concatenating: [negativePromptEmbedding, promptEmbedding],
alongAxis: 0
)
}
if reduceMemory {
textEncoder.unloadResources()
}
let hiddenStates = useMultilingualTextEncoder ? promptEmbedding : toHiddenStates(promptEmbedding)
/// Setup schedulers
let scheduler: [Scheduler] = (0..<config.imageCount).map { _ in
switch config.schedulerType {
case .pndmScheduler: return PNDMScheduler(stepCount: config.stepCount)
case .dpmSolverMultistepScheduler: return DPMSolverMultistepScheduler(stepCount: config.stepCount, timeStepSpacing: config.schedulerTimestepSpacing)
case .discreteFlowScheduler: return DiscreteFlowScheduler(stepCount: config.stepCount, timeStepShift: config.schedulerTimestepShift)
}
}
// Generate random latent samples from specified seed
var latents: [MLShapedArray<Float32>] = try generateLatentSamples(configuration: config, scheduler: scheduler[0])
// Store denoised latents from scheduler to pass into decoder
var denoisedLatents: [MLShapedArray<Float32>] = latents.map { MLShapedArray(converting: $0) }
if reduceMemory {
encoder?.unloadResources()
}
let timestepStrength: Float? = config.mode == .imageToImage ? config.strength : nil
// Convert cgImage for ControlNet into MLShapedArray
let controlNetConds = try config.controlNetInputs.map { cgImage in
let shapedArray = try cgImage.planarRGBShapedArray(minValue: 0.0, maxValue: 1.0)
return MLShapedArray(
concatenating: [shapedArray, shapedArray],
alongAxis: 0
)
}
// De-noising loop
let timeSteps: [Int] = scheduler[0].calculateTimesteps(strength: timestepStrength)
for (step,t) in timeSteps.enumerated() {
// Expand the latents for classifier-free guidance
// and input to the Unet noise prediction model
let latentUnetInput: [MLShapedArray<Float32>]
if config.guidanceScale >= 1.0 {
latentUnetInput = latents.map {
MLShapedArray<Float32>(concatenating: [$0, $0], alongAxis: 0)
}
} else {
latentUnetInput = latents
}
// Before Unet, execute controlNet and add the output into Unet inputs
let additionalResiduals = try controlNet?.execute(
latents: latentUnetInput,
timeStep: t,
hiddenStates: hiddenStates,
images: controlNetConds
)
// Predict noise residuals from latent samples
// and current time step conditioned on hidden states
var noise : [MLShapedArray<Float32>]
if unet.latentSampleShape[0] >= 2 || config.guidanceScale < 1.0 {
// One predict call from the uNet, using batching if needed
noise = try unet.predictNoise(
latents: latentUnetInput,
timeStep: t,
hiddenStates: hiddenStates,
additionalResiduals: additionalResiduals
)
} else {
// Serial predictions from uNet
var hidden0 = MLShapedArray<Float32>(converting: hiddenStates[0])
hidden0 = MLShapedArray(scalars: hidden0.scalars, shape: [1]+hidden0.shape)
let noise_pred_uncond = try unet.predictNoise(
latents: latents,
timeStep: t,
hiddenStates: hidden0,
additionalResiduals: additionalResiduals
)
var hidden1 = MLShapedArray<Float32>(converting: hiddenStates[1])
hidden1 = MLShapedArray(scalars: hidden1.scalars, shape: [1]+hidden1.shape)
let noise_pred_text = try unet.predictNoise(
latents: latents,
timeStep: t,
hiddenStates: hidden1,
additionalResiduals: additionalResiduals
)
noise = [MLShapedArray<Float32>(concatenating: [noise_pred_uncond[0], noise_pred_text[0]],
alongAxis: 0)]
}
if config.guidanceScale >= 1.0 {
noise = performGuidance(noise, config.guidanceScale)
}
// Have the scheduler compute the previous (t-1) latent
// sample given the predicted noise and current sample
for i in 0..<config.imageCount {
latents[i] = scheduler[i].step(
output: noise[i],
timeStep: t,
sample: latents[i]
)
denoisedLatents[i] = scheduler[i].modelOutputs.last ?? latents[i]
}
let currentLatentSamples = config.useDenoisedIntermediates ? denoisedLatents : latents
// Report progress
let progress = Progress(
pipeline: self,
prompt: config.prompt,
step: step,
stepCount: timeSteps.count,
currentLatentSamples: currentLatentSamples,
configuration: config
)
if !progressHandler(progress) {
// Stop if requested by handler
return []
}
}
if reduceMemory {
controlNet?.unloadResources()
unet.unloadResources()
}
// Decode the latent samples to images
return try decodeToImages(denoisedLatents, configuration: config)
}
func generateLatentSamples(configuration config: Configuration, scheduler: Scheduler) throws -> [MLShapedArray<Float32>] {
var sampleShape = unet.latentSampleShape
sampleShape[0] = 1
let stdev = scheduler.initNoiseSigma
var random = randomSource(from: config.rngType, seed: config.seed)
let samples = (0..<config.imageCount).map { _ in
MLShapedArray<Float32>(
converting: random.normalShapedArray(sampleShape, mean: 0.0, stdev: Double(stdev)))
}
if let image = config.startingImage, config.mode == .imageToImage {
guard let encoder else {
throw PipelineError.startingImageProvidedWithoutEncoder
}
let latent = try encoder.encode(image, scaleFactor: config.encoderScaleFactor, random: &random)
return scheduler.addNoise(originalSample: latent, noise: samples, strength: config.strength)
}
return samples
}
public func decodeToImages(_ latents: [MLShapedArray<Float32>], configuration config: Configuration) throws -> [CGImage?] {
let images = try decoder.decode(latents, scaleFactor: config.decoderScaleFactor)
if reduceMemory {
decoder.unloadResources()
}
// If safety is disabled return what was decoded
if config.disableSafety {
return images
}
// If there is no safety checker return what was decoded
guard let safetyChecker = safetyChecker else {
return images
}
// Otherwise change images which are not safe to nil
let safeImages = try images.map { image in
try safetyChecker.isSafe(image) ? image : nil
}
if reduceMemory {
safetyChecker.unloadResources()
}
return safeImages
}
}
/// Sampling progress details
@available(iOS 16.2, macOS 13.1, *)
public struct PipelineProgress {
public let pipeline: StableDiffusionPipelineProtocol
public let prompt: String
public let step: Int
public let stepCount: Int
public let currentLatentSamples: [MLShapedArray<Float32>]
public let configuration: PipelineConfiguration
public var isSafetyEnabled: Bool {
pipeline.canSafetyCheck && !configuration.disableSafety
}
public var currentImages: [CGImage?] {
try! pipeline.decodeToImages(currentLatentSamples, configuration: configuration)
}
}
@available(iOS 16.2, macOS 13.1, *)
public extension StableDiffusionPipeline {
/// Sampling progress details
typealias Progress = PipelineProgress
}
// Helper functions
@available(iOS 16.2, macOS 13.1, *)
extension StableDiffusionPipelineProtocol {
internal func randomSource(from rng: StableDiffusionRNG, seed: UInt32) -> RandomSource {
switch rng {
case .numpyRNG:
return NumPyRandomSource(seed: seed)
case .torchRNG:
return TorchRandomSource(seed: seed)
case .nvidiaRNG:
return NvRandomSource(seed: seed)
}
}
func toHiddenStates(_ embedding: MLShapedArray<Float32>) -> MLShapedArray<Float32> {
// Unoptimized manual transpose [0, 2, None, 1]
// e.g. From [2, 77, 768] to [2, 768, 1, 77]
let fromShape = embedding.shape
let stateShape = [fromShape[0],fromShape[2], 1, fromShape[1]]
var states = MLShapedArray<Float32>(repeating: 0.0, shape: stateShape)
for i0 in 0..<fromShape[0] {
for i1 in 0..<fromShape[1] {
for i2 in 0..<fromShape[2] {
states[scalarAt:i0,i2,0,i1] = embedding[scalarAt:i0, i1, i2]
}
}
}
return states
}
func performGuidance(_ noise: [MLShapedArray<Float32>], _ guidanceScale: Float) -> [MLShapedArray<Float32>] {
noise.map { performGuidance($0, guidanceScale) }
}
func performGuidance(_ noise: MLShapedArray<Float32>, _ guidanceScale: Float) -> MLShapedArray<Float32> {
var shape = noise.shape
shape[0] = 1
return MLShapedArray<Float>(unsafeUninitializedShape: shape) { result, _ in
noise.withUnsafeShapedBufferPointer { scalars, _, strides in
for i in 0 ..< result.count {
// unconditioned + guidance*(text - unconditioned)
result.initializeElement(
at: i,
to: scalars[i] + guidanceScale * (scalars[strides[0] + i] - scalars[i])
)
}
}
}
}
}
@@ -0,0 +1,119 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2023 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
import NaturalLanguage
@available(iOS 17.0, macOS 14.0, *)
public extension StableDiffusionXLPipeline {
struct ResourceURLs {
public let textEncoderURL: URL
public let textEncoder2URL: URL
public let unetURL: URL
public let unetChunk1URL: URL
public let unetChunk2URL: URL
public let unetRefinerURL: URL
public let unetRefinerChunk1URL: URL
public let unetRefinerChunk2URL: URL
public let decoderURL: URL
public let encoderURL: URL
public let vocabURL: URL
public let mergesURL: URL
public init(resourcesAt baseURL: URL) {
textEncoderURL = baseURL.appending(path: "TextEncoder.mlmodelc")
textEncoder2URL = baseURL.appending(path: "TextEncoder2.mlmodelc")
unetURL = baseURL.appending(path: "Unet.mlmodelc")
unetChunk1URL = baseURL.appending(path: "UnetChunk1.mlmodelc")
unetChunk2URL = baseURL.appending(path: "UnetChunk2.mlmodelc")
unetRefinerURL = baseURL.appending(path: "UnetRefiner.mlmodelc")
unetRefinerChunk1URL = baseURL.appending(path: "UnetRefinerChunk1.mlmodelc")
unetRefinerChunk2URL = baseURL.appending(path: "UnetRefinerChunk2.mlmodelc")
decoderURL = baseURL.appending(path: "VAEDecoder.mlmodelc")
encoderURL = baseURL.appending(path: "VAEEncoder.mlmodelc")
vocabURL = baseURL.appending(path: "vocab.json")
mergesURL = baseURL.appending(path: "merges.txt")
}
}
/// Create stable diffusion pipeline using model resources at a
/// specified URL
///
/// - Parameters:
/// - baseURL: URL pointing to directory holding all model and tokenization resources
/// - configuration: The configuration to load model resources with
/// - reduceMemory: Setup pipeline in reduced memory mode
/// - Returns:
/// Pipeline ready for image generation if all necessary resources loaded
init(
resourcesAt baseURL: URL,
configuration config: MLModelConfiguration = .init(),
reduceMemory: Bool = false
) throws {
/// Expect URL of each resource
let urls = ResourceURLs(resourcesAt: baseURL)
let tokenizer = try BPETokenizer(mergesAt: urls.mergesURL, vocabularyAt: urls.vocabURL)
let textEncoder: TextEncoderXL?
if FileManager.default.fileExists(atPath: urls.textEncoderURL.path) {
textEncoder = TextEncoderXL(tokenizer: tokenizer, modelAt: urls.textEncoderURL, configuration: config)
} else {
textEncoder = nil
}
// padToken is different in the second XL text encoder
let tokenizer2 = try BPETokenizer(mergesAt: urls.mergesURL, vocabularyAt: urls.vocabURL, padToken: "!")
let textEncoder2 = TextEncoderXL(tokenizer: tokenizer2, modelAt: urls.textEncoder2URL, configuration: config)
// Unet model
let unet: Unet
if FileManager.default.fileExists(atPath: urls.unetChunk1URL.path) &&
FileManager.default.fileExists(atPath: urls.unetChunk2URL.path) {
unet = Unet(chunksAt: [urls.unetChunk1URL, urls.unetChunk2URL],
configuration: config)
} else {
unet = Unet(modelAt: urls.unetURL, configuration: config)
}
// Refiner Unet model
let unetRefiner: Unet?
if FileManager.default.fileExists(atPath: urls.unetRefinerChunk1URL.path) &&
FileManager.default.fileExists(atPath: urls.unetRefinerChunk2URL.path) {
unetRefiner = Unet(chunksAt: [urls.unetRefinerChunk1URL, urls.unetRefinerChunk2URL],
configuration: config)
} else if FileManager.default.fileExists(atPath: urls.unetRefinerURL.path) {
unetRefiner = Unet(modelAt: urls.unetRefinerURL, configuration: config)
} else {
unetRefiner = nil
}
// Image Decoder
// FIXME: Hardcoding to .cpuAndGPU since ANE doesn't support FLOAT32
let vaeConfig = config.copy() as! MLModelConfiguration
vaeConfig.computeUnits = .cpuAndGPU
let decoder = Decoder(modelAt: urls.decoderURL, configuration: vaeConfig)
// Optional Image Encoder
let encoder: Encoder?
if FileManager.default.fileExists(atPath: urls.encoderURL.path) {
encoder = Encoder(modelAt: urls.encoderURL, configuration: vaeConfig)
} else {
encoder = nil
}
// Construct pipeline
self.init(
textEncoder: textEncoder,
textEncoder2: textEncoder2,
unet: unet,
unetRefiner: unetRefiner,
decoder: decoder,
encoder: encoder,
reduceMemory: reduceMemory
)
}
}
@@ -0,0 +1,400 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2023 Apple Inc. All Rights Reserved.
import Accelerate
import CoreGraphics
import CoreML
import Foundation
import NaturalLanguage
/// A pipeline used to generate image samples from text input using stable diffusion XL
///
/// This implementation matches:
/// [Hugging Face Diffusers XL Pipeline](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py)
@available(iOS 17.0, macOS 14.0, *)
public struct StableDiffusionXLPipeline: StableDiffusionPipelineProtocol {
public typealias Configuration = PipelineConfiguration
public typealias Progress = PipelineProgress
/// Model to generate embeddings for tokenized input text
var textEncoder: TextEncoderXLModel?
var textEncoder2: TextEncoderXLModel
/// Model used to predict noise residuals given an input, diffusion time step, and conditional embedding
var unet: Unet
/// Model used to refine the image, if present
var unetRefiner: Unet?
/// Model used to generate final image from latent diffusion process
var decoder: Decoder
/// Model used to latent space for image2image, and soon, in-painting
var encoder: Encoder?
/// Option to reduce memory during image generation
///
/// If true, the pipeline will lazily load TextEncoder, Unet, Decoder, and SafetyChecker
/// when needed and aggressively unload their resources after
///
/// This will increase latency in favor of reducing memory
var reduceMemory: Bool = false
/// Creates a pipeline using the specified models and tokenizer
///
/// - Parameters:
/// - textEncoder: Model for encoding tokenized text
/// - textEncoder2: Second text encoding model
/// - unet: Model for noise prediction on latent samples
/// - decoder: Model for decoding latent sample to image
/// - reduceMemory: Option to enable reduced memory mode
/// - Returns: Pipeline ready for image generation
public init(
textEncoder: TextEncoderXLModel?,
textEncoder2: TextEncoderXLModel,
unet: Unet,
unetRefiner: Unet?,
decoder: Decoder,
encoder: Encoder?,
reduceMemory: Bool = false
) {
self.textEncoder = textEncoder
self.textEncoder2 = textEncoder2
self.unet = unet
self.unetRefiner = unetRefiner
self.decoder = decoder
self.encoder = encoder
self.reduceMemory = reduceMemory
}
/// Load required resources for this pipeline
///
/// If reducedMemory is true this will instead call prewarmResources instead
/// and let the pipeline lazily load resources as needed
public func loadResources() throws {
if reduceMemory {
try prewarmResources()
} else {
try textEncoder2.loadResources()
try unet.loadResources()
try decoder.loadResources()
do {
try textEncoder?.loadResources()
} catch {
print("Error loading resources for textEncoder: \(error)")
}
// Only prewarm refiner unet on load so it's unloaded until needed
do {
try unetRefiner?.prewarmResources()
} catch {
print("Error loading resources for unetRefiner: \(error)")
}
do {
try encoder?.loadResources()
} catch {
print("Error loading resources for vae encoder: \(error)")
}
}
}
/// Unload the underlying resources to free up memory
public func unloadResources() {
textEncoder?.unloadResources()
textEncoder2.unloadResources()
unet.unloadResources()
unetRefiner?.unloadResources()
decoder.unloadResources()
encoder?.unloadResources()
}
/// Prewarm resources one at a time
public func prewarmResources() throws {
try textEncoder2.prewarmResources()
try unet.prewarmResources()
try decoder.prewarmResources()
do {
try textEncoder?.prewarmResources()
} catch {
print("Error prewarming resources for textEncoder: \(error)")
}
do {
try unetRefiner?.prewarmResources()
} catch {
print("Error prewarming resources for unetRefiner: \(error)")
}
do {
try encoder?.prewarmResources()
} catch {
print("Error prewarming resources for vae encoder: \(error)")
}
}
/// Image generation using stable diffusion
/// - Parameters:
/// - configuration: Image generation configuration
/// - progressHandler: Callback to perform after each step, stops on receiving false response
/// - Returns: An array of `imageCount` optional images.
/// The images will be nil if safety checks were performed and found the result to be un-safe
public func generateImages(
configuration config: Configuration,
progressHandler: (Progress) -> Bool = { _ in true }
) throws -> [CGImage?] {
// Determine input type of Unet
// SDXL Refiner has a latentTimeIdShape of [2, 5]
// SDXL Base has either [12] or [2, 6]
let isRefiner = unet.latentTimeIdShape.last == 5
// Setup geometry conditioning for base/refiner inputs
var baseInput: ModelInputs?
var refinerInput: ModelInputs?
// Check if the first textEncoder is available, which is required for base models
if textEncoder != nil {
baseInput = try generateConditioning(using: config, forRefiner: isRefiner)
}
// Check if the refiner unet exists, or if the current unet is a refiner
if unetRefiner != nil || isRefiner {
refinerInput = try generateConditioning(using: config, forRefiner: true)
}
if reduceMemory {
textEncoder?.unloadResources()
textEncoder2.unloadResources()
}
/// Setup schedulers
let scheduler: [Scheduler] = (0..<config.imageCount).map { _ in
switch config.schedulerType {
case .pndmScheduler: return PNDMScheduler(stepCount: config.stepCount)
case .dpmSolverMultistepScheduler: return DPMSolverMultistepScheduler(stepCount: config.stepCount, timeStepSpacing: config.schedulerTimestepSpacing)
case .discreteFlowScheduler: return DiscreteFlowScheduler(stepCount: config.stepCount, timeStepShift: config.schedulerTimestepShift)
}
}
// Generate random latent samples from specified seed
var latents: [MLShapedArray<Float32>] = try generateLatentSamples(configuration: config, scheduler: scheduler[0])
// Store denoised latents from scheduler to pass into decoder
var denoisedLatents: [MLShapedArray<Float32>] = latents.map { MLShapedArray(converting: $0) }
if reduceMemory {
encoder?.unloadResources()
}
let timestepStrength: Float? = config.mode == .imageToImage ? config.strength : nil
// Store current model
var unetModel = unet
var currentInput = baseInput ?? refinerInput
var unetHiddenStates = currentInput?.hiddenStates
var unetPooledStates = currentInput?.pooledStates
var unetGeometryConditioning = currentInput?.geometryConditioning
let timeSteps: [Int] = scheduler[0].calculateTimesteps(strength: timestepStrength)
// Calculate which step to swap to refiner
let refinerStartStep = Int(Float(timeSteps.count) * config.refinerStart)
// De-noising loop
for (step,t) in timeSteps.enumerated() {
// Expand the latents for classifier-free guidance
// and input to the Unet noise prediction model
let latentUnetInput = latents.map {
MLShapedArray<Float32>(concatenating: [$0, $0], alongAxis: 0)
}
// Switch to refiner if specified
if let refiner = unetRefiner, step == refinerStartStep {
unet.unloadResources()
unetModel = refiner
currentInput = refinerInput
unetHiddenStates = currentInput?.hiddenStates
unetPooledStates = currentInput?.pooledStates
unetGeometryConditioning = currentInput?.geometryConditioning
}
guard let hiddenStates = unetHiddenStates,
let pooledStates = unetPooledStates,
let geometryConditioning = unetGeometryConditioning else {
throw PipelineError.missingUnetInputs
}
// Predict noise residuals from latent samples
// and current time step conditioned on hidden states
var noise = try unetModel.predictNoise(
latents: latentUnetInput,
timeStep: t,
hiddenStates: hiddenStates,
pooledStates: pooledStates,
geometryConditioning: geometryConditioning
)
noise = performGuidance(noise, config.guidanceScale)
// Have the scheduler compute the previous (t-1) latent
// sample given the predicted noise and current sample
for i in 0..<config.imageCount {
latents[i] = scheduler[i].step(
output: noise[i],
timeStep: t,
sample: latents[i]
)
denoisedLatents[i] = scheduler[i].modelOutputs.last ?? latents[i]
}
let currentLatentSamples = config.useDenoisedIntermediates ? denoisedLatents : latents
// Report progress
let progress = Progress(
pipeline: self,
prompt: config.prompt,
step: step,
stepCount: timeSteps.count,
currentLatentSamples: currentLatentSamples,
configuration: config
)
if !progressHandler(progress) {
// Stop if requested by handler
return []
}
}
// Unload resources
if reduceMemory {
unet.unloadResources()
}
unetRefiner?.unloadResources()
// Decode the latent samples to images
return try decodeToImages(denoisedLatents, configuration: config)
}
func encodePrompt(_ prompt: String, forRefiner: Bool = false) throws -> (MLShapedArray<Float32>, MLShapedArray<Float32>) {
var embeds = MLShapedArray<Float32>()
var pooled = MLShapedArray<Float32>()
if forRefiner {
let (embeds2, pooledValue) = try textEncoder2.encode(prompt)
// Refiner only takes textEncoder2 embeddings
// [1, 77, 1280]
embeds = embeds2
pooled = pooledValue
} else {
guard let encoder = textEncoder else {
throw PipelineError.startingText2ImgWithoutTextEncoder
}
let (embeds1, _) = try encoder.encode(prompt)
let (embeds2, pooledValue) = try textEncoder2.encode(prompt)
// Base needs concatenated embeddings
// [1, 77, 768], [1, 77, 1280] -> [1, 77, 2048]
embeds = MLShapedArray<Float32>(
concatenating: [embeds1, embeds2],
alongAxis: 2
)
pooled = pooledValue
}
return (embeds, pooled)
}
func generateConditioning(using config: Configuration, forRefiner: Bool = false) throws -> ModelInputs {
// Encode the input prompt and negative prompt
let (promptEmbedding, pooled) = try encodePrompt(config.prompt, forRefiner: forRefiner)
let (negativePromptEmbedding, negativePooled) = try encodePrompt(config.negativePrompt, forRefiner: forRefiner)
// Convert to Unet hidden state representation
// Concatenate the prompt and negative prompt embeddings
let hiddenStates = toHiddenStates(MLShapedArray(concatenating: [negativePromptEmbedding, promptEmbedding], alongAxis: 0))
let pooledStates = MLShapedArray(concatenating: [negativePooled, pooled], alongAxis: 0)
// Inline helper functions for geometry creation
func refinerGeometry() -> MLShapedArray<Float32> {
let negativeGeometry = MLShapedArray<Float32>(
scalars: [
config.originalSize, config.originalSize,
config.cropsCoordsTopLeft, config.cropsCoordsTopLeft,
config.negativeAestheticScore
],
shape: [1, 5]
)
let positiveGeometry = MLShapedArray<Float32>(
scalars: [
config.originalSize, config.originalSize,
config.cropsCoordsTopLeft, config.cropsCoordsTopLeft,
config.aestheticScore
],
shape: [1, 5]
)
return MLShapedArray<Float32>(concatenating: [negativeGeometry, positiveGeometry], alongAxis: 0)
}
func baseGeometry() -> MLShapedArray<Float32> {
let geometry = MLShapedArray<Float32>(
scalars: [
config.originalSize, config.originalSize,
config.cropsCoordsTopLeft, config.cropsCoordsTopLeft,
config.targetSize, config.targetSize
],
// TODO: This checks if the time_ids input is looking for [12] or [2, 6]
// Remove once model input shapes are ubiquitous
shape: unet.latentTimeIdShape.count > 1 ? [1, 6] : [6]
)
return MLShapedArray<Float32>(concatenating: [geometry, geometry], alongAxis: 0)
}
let geometry = forRefiner ? refinerGeometry() : baseGeometry()
return ModelInputs(hiddenStates: hiddenStates, pooledStates: pooledStates, geometryConditioning: geometry)
}
func generateLatentSamples(configuration config: Configuration, scheduler: Scheduler) throws -> [MLShapedArray<Float32>] {
var sampleShape = unet.latentSampleShape
sampleShape[0] = 1
let stdev = scheduler.initNoiseSigma
var random = randomSource(from: config.rngType, seed: config.seed)
let samples = (0..<config.imageCount).map { _ in
MLShapedArray<Float32>(
converting: random.normalShapedArray(sampleShape, mean: 0.0, stdev: Double(stdev)))
}
if let image = config.startingImage, config.mode == .imageToImage {
guard let encoder else {
throw PipelineError.startingImageProvidedWithoutEncoder
}
let latent = try encoder.encode(image, scaleFactor: config.encoderScaleFactor, random: &random)
return scheduler.addNoise(originalSample: latent, noise: samples, strength: config.strength)
}
return samples
}
public func decodeToImages(_ latents: [MLShapedArray<Float32>], configuration config: Configuration) throws -> [CGImage?] {
defer {
if reduceMemory {
decoder.unloadResources()
}
}
return try decoder.decode(latents, scaleFactor: config.decoderScaleFactor)
}
struct ModelInputs {
var hiddenStates: MLShapedArray<Float32>
var pooledStates: MLShapedArray<Float32>
var geometryConditioning: MLShapedArray<Float32>
}
}
@@ -0,0 +1,102 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
@available(iOS 16.2, macOS 13.1, *)
public protocol TextEncoderModel: ResourceManaging {
func encode(_ text: String) throws -> MLShapedArray<Float32>
}
/// A model for encoding text
@available(iOS 16.2, macOS 13.1, *)
public struct TextEncoder: TextEncoderModel {
/// Text tokenizer
var tokenizer: BPETokenizer
/// Embedding model
var model: ManagedMLModel
/// Creates text encoder which embeds a tokenized string
///
/// - Parameters:
/// - tokenizer: Tokenizer for input text
/// - url: Location of compiled text encoding Core ML model
/// - configuration: configuration to be used when the model is loaded
/// - Returns: A text encoder that will lazily load its required resources when needed or requested
public init(tokenizer: BPETokenizer,
modelAt url: URL,
configuration: MLModelConfiguration) {
self.tokenizer = tokenizer
self.model = ManagedMLModel(modelAt: url, configuration: configuration)
}
/// Ensure the model has been loaded into memory
public func loadResources() throws {
try model.loadResources()
}
/// Unload the underlying model to free up memory
public func unloadResources() {
model.unloadResources()
}
/// Encode input text/string
///
/// - Parameters:
/// - text: Input text to be tokenized and then embedded
/// - Returns: Embedding representing the input text
public func encode(_ text: String) throws -> MLShapedArray<Float32> {
// Get models expected input length
let inputLength = inputShape.last!
// Tokenize, padding to the expected length
var (tokens, ids) = tokenizer.tokenize(input: text, minCount: inputLength)
// Truncate if necessary
if ids.count > inputLength {
tokens = tokens.dropLast(tokens.count - inputLength)
ids = ids.dropLast(ids.count - inputLength)
let truncated = tokenizer.decode(tokens: tokens)
print("Needed to truncate input '\(text)' to '\(truncated)'")
}
// Use the model to generate the embedding
return try encode(ids: ids)
}
/// Prediction queue
let queue = DispatchQueue(label: "textencoder.predict")
func encode(ids: [Int]) throws -> MLShapedArray<Float32> {
let inputName = inputDescription.name
let inputShape = inputShape
let floatIds = ids.map { Float32($0) }
let inputArray = MLShapedArray<Float32>(scalars: floatIds, shape: inputShape)
let inputFeatures = try! MLDictionaryFeatureProvider(
dictionary: [inputName: MLMultiArray(inputArray)])
let result = try model.perform { model in
try model.prediction(from: inputFeatures)
}
let embeddingFeature = result.featureValue(for: "last_hidden_state")
return MLShapedArray<Float32>(converting: embeddingFeature!.multiArrayValue!)
}
var inputDescription: MLFeatureDescription {
try! model.perform { model in
model.modelDescription.inputDescriptionsByName.first!.value
}
}
var inputShape: [Int] {
inputDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
}
@@ -0,0 +1,124 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2023 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
import Tokenizers
@available(iOS 17.0, macOS 14.0, *)
public protocol TextEncoderT5Model: ResourceManaging {
func encode(_ text: String) throws -> TextEncoderT5Output
}
@available(iOS 17.0, macOS 14.0, *)
public struct TextEncoderT5Output {
public let encoderHiddenStates: MLShapedArray<Float32>
}
/// A model for encoding text, suitable for SD3
@available(iOS 17.0, macOS 14.0, *)
public struct TextEncoderT5: TextEncoderT5Model {
/// Text tokenizer
var tokenizer: Tokenizer
/// Embedding model
var model: ManagedMLModel
/// Creates text encoder which embeds a tokenized string
///
/// - Parameters:
/// - tokenizer: Tokenizer for input text
/// - url: Location of compiled text encoding Core ML model
/// - configuration: configuration to be used when the model is loaded
/// - Returns: A text encoder that will lazily load its required resources when needed or requested
public init(tokenizer: Tokenizer,
modelAt url: URL,
configuration: MLModelConfiguration) {
self.tokenizer = tokenizer
self.model = ManagedMLModel(modelAt: url, configuration: configuration)
}
/// Ensure the model has been loaded into memory
public func loadResources() throws {
try model.loadResources()
}
/// Unload the underlying model to free up memory
public func unloadResources() {
model.unloadResources()
}
/// Encode input text/string
///
/// - Parameters:
/// - text: Input text to be tokenized and then embedded
/// - Returns: Embedding representing the input text
public func encode(_ text: String) throws -> TextEncoderT5Output {
// Get models expected input length
let inputLength = inputShape.last!
// Tokenize, padding to the expected length
var tokens = tokenizer.tokenize(text: text)
var ids = tokens.map { tokenizer.convertTokenToId($0) ?? 0 }
// Truncate if necessary
if ids.count > inputLength {
tokens = tokens.dropLast(tokens.count - inputLength)
ids = ids.dropLast(ids.count - inputLength)
print("Needed to truncate input for TextEncoderT5")
}
// Use the model to generate the embedding
let encodedText = try encode(ids: ids)
return encodedText
}
func encode(ids: [Int]) throws -> TextEncoderT5Output {
let inputName = "input_ids"
let inputShape = inputShape
let inputLength = inputShape[1]
let bosToken = tokenizer.bosTokenId ?? 0
let eosToken = tokenizer.eosTokenId ?? 1
let padToken = bosToken
let maskToken = eosToken
// Truncate and pad input to the expected length
let truncatedIds = ids.prefix(inputLength - 1) + [eosToken]
let inputIds = truncatedIds + Array(repeating: padToken, count: inputLength - truncatedIds.count)
let attentionMaskName = "attention_mask"
var attentionMask: [Int] = inputIds.map { token in
token == padToken ? maskToken : padToken
}
attentionMask[0] = bosToken
let floatIds = inputIds.map { Float32($0) }
let floatMask = attentionMask.map { Float32($0) }
let inputArray = MLShapedArray<Float32>(scalars: floatIds, shape: inputShape)
let maskArray = MLShapedArray<Float32>(scalars: floatMask, shape: inputShape)
let inputFeatures = try! MLDictionaryFeatureProvider(
dictionary: [inputName: MLMultiArray(inputArray),
attentionMaskName: MLMultiArray(maskArray)])
let result = try model.perform { model in
try model.prediction(from: inputFeatures)
}
let embeddingFeature = result.featureValue(for: "encoder_hidden_states")
return TextEncoderT5Output(encoderHiddenStates: MLShapedArray<Float32>(converting: embeddingFeature!.multiArrayValue!))
}
var inputDescription: MLFeatureDescription {
try! model.perform { model in
model.modelDescription.inputDescriptionsByName.first!.value
}
}
var inputShape: [Int] {
inputDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
}
@@ -0,0 +1,99 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2023 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
@available(iOS 17.0, macOS 14.0, *)
public protocol TextEncoderXLModel: ResourceManaging {
typealias TextEncoderXLOutput = (hiddenEmbeddings: MLShapedArray<Float32>, pooledOutputs: MLShapedArray<Float32>)
func encode(_ text: String) throws -> TextEncoderXLOutput
}
/// A model for encoding text, suitable for SDXL
@available(iOS 17.0, macOS 14.0, *)
public struct TextEncoderXL: TextEncoderXLModel {
/// Text tokenizer
var tokenizer: BPETokenizer
/// Embedding model
var model: ManagedMLModel
/// Creates text encoder which embeds a tokenized string
///
/// - Parameters:
/// - tokenizer: Tokenizer for input text
/// - url: Location of compiled text encoding Core ML model
/// - configuration: configuration to be used when the model is loaded
/// - Returns: A text encoder that will lazily load its required resources when needed or requested
public init(tokenizer: BPETokenizer,
modelAt url: URL,
configuration: MLModelConfiguration) {
self.tokenizer = tokenizer
self.model = ManagedMLModel(modelAt: url, configuration: configuration)
}
/// Ensure the model has been loaded into memory
public func loadResources() throws {
try model.loadResources()
}
/// Unload the underlying model to free up memory
public func unloadResources() {
model.unloadResources()
}
/// Encode input text/string
///
/// - Parameters:
/// - text: Input text to be tokenized and then embedded
/// - Returns: Embedding representing the input text
public func encode(_ text: String) throws -> TextEncoderXLOutput {
// Get models expected input length
let inputLength = inputShape.last!
// Tokenize, padding to the expected length
var (tokens, ids) = tokenizer.tokenize(input: text, minCount: inputLength)
// Truncate if necessary
if ids.count > inputLength {
tokens = tokens.dropLast(tokens.count - inputLength)
ids = ids.dropLast(ids.count - inputLength)
let truncated = tokenizer.decode(tokens: tokens)
print("Needed to truncate input '\(text)' to '\(truncated)'")
}
// Use the model to generate the embedding
return try encode(ids: ids)
}
func encode(ids: [Int]) throws -> TextEncoderXLOutput {
let inputName = inputDescription.name
let inputShape = inputShape
let floatIds = ids.map { Float32($0) }
let inputArray = MLShapedArray<Float32>(scalars: floatIds, shape: inputShape)
let inputFeatures = try! MLDictionaryFeatureProvider(
dictionary: [inputName: MLMultiArray(inputArray)])
let result = try model.perform { model in
try model.prediction(from: inputFeatures)
}
let embeddingFeature = result.featureValue(for: "hidden_embeds")
let pooledFeature = result.featureValue(for: "pooled_outputs")
return (MLShapedArray<Float32>(converting: embeddingFeature!.multiArrayValue!), MLShapedArray<Float32>(converting: pooledFeature!.multiArrayValue!))
}
var inputDescription: MLFeatureDescription {
try! model.perform { model in
model.modelDescription.inputDescriptionsByName.first!.value
}
}
var inputShape: [Int] {
inputDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
}
@@ -0,0 +1,157 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
/// A random source consistent with PyTorch
///
/// This implementation matches:
/// https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/core/DistributionsHelper.h
/// https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/native/cpu/DistributionTemplates.h
/// https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/native/cpu/DistributionKernels.cpp
/// https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/core/TransformationHelper.h
///
@available(iOS 16.2, macOS 13.1, *)
struct TorchRandomSource: RandomNumberGenerator, RandomSource {
struct State {
var key = [UInt32](repeating: 0, count: 624)
var pos: Int = 0
var nextGauss: Double? = nil
}
var state: State
/// Initialize with a random seed
///
/// - Parameters
/// - seed: Seed for underlying Mersenne Twister 19937 generator
/// - Returns random source
init(seed: UInt32) {
state = .init()
var s = seed & 0xffff_ffff
for i in 0..<state.key.count {
state.key[i] = s
s = UInt32((UInt64(1_812_433_253) * UInt64(s ^ (s >> 30)) + UInt64(i) + 1) & 0xffff_ffff)
}
state.pos = state.key.count
state.nextGauss = nil
}
/// Generate next UInt32 using fast 32bit Mersenne Twister
mutating func nextUInt32() -> UInt32 {
let n = 624
let m = 397
let matrixA: UInt64 = 0x9908_b0df
let upperMask: UInt32 = 0x8000_0000
let lowerMask: UInt32 = 0x7fff_ffff
var y: UInt32
if state.pos == state.key.count {
for i in 0..<(n - m) {
y = (state.key[i] & upperMask) | (state.key[i + 1] & lowerMask)
state.key[i] = state.key[i + m] ^ (y >> 1) ^ UInt32((UInt64(~(y & 1)) + 1) & matrixA)
}
for i in (n - m)..<(n - 1) {
y = (state.key[i] & upperMask) | (state.key[i + 1] & lowerMask)
state.key[i] = state.key[i + (m - n)] ^ (y >> 1) ^ UInt32((UInt64(~(y & 1)) + 1) & matrixA)
}
y = (state.key[n - 1] & upperMask) | (state.key[0] & lowerMask)
state.key[n - 1] = state.key[m - 1] ^ (y >> 1) ^ UInt32((UInt64(~(y & 1)) + 1) & matrixA)
state.pos = 0
}
y = state.key[state.pos]
state.pos += 1
y ^= (y >> 11)
y ^= (y << 7) & 0x9d2c_5680
y ^= (y << 15) & 0xefc6_0000
y ^= (y >> 18)
return y
}
mutating func next() -> UInt64 {
let high = nextUInt32()
let low = nextUInt32()
return (UInt64(high) << 32) | UInt64(low)
}
/// Generate next random double value
mutating func nextDouble() -> Double {
let a = next()
return Double(a & 9_007_199_254_740_991) * (1.0 / 9007199254740992.0)
}
/// Generate next random float value
mutating func nextFloat() -> Float {
let a = nextUInt32()
return Float(a & 16_777_215) * (1.0 / 16777216.0)
}
/// Generate next random value from a standard normal
mutating func nextGauss() -> Double {
if let nextGauss = state.nextGauss {
state.nextGauss = nil
return nextGauss
}
// Box-Muller transform
let u1: Double = nextDouble()
let u2: Double = 1 - nextDouble()
let radius = sqrt(-2.0 * log(u2))
let theta = 2.0 * .pi * u1
state.nextGauss = radius * sin(theta)
return radius * cos(theta)
}
/// Generates a random value from a normal distribution with given mean and standard deviation.
mutating func nextNormal(mean: Double = 0.0, stdev: Double = 1.0) -> Double {
nextGauss() * stdev + mean
}
/// Generates an array of random values from a normal distribution with given mean and standard deviation.
/// This simulates torch.randn([1, 4, 64, 64], dtype=torch.float), note that for dtype=torch.double, it
/// will be slightly different.
mutating func normalArray(count: Int, mean: Double = 0.0, stdev: Double = 1.0) -> [Double] {
// If it is smaller than 16 elements, Torch generates from Box-Muller transform directly.
// Note that even if this is used to generate Float, it will use Double underneath.
guard count >= 16 else {
return (0..<count).map { _ in nextNormal(mean: mean, stdev: stdev) }
}
// Otherwise, Torch first fill a uniform distribution array, then do Box-Muller
// transformation over this array.
var data = (0..<count).map { _ in Double(nextFloat()) }
for i in stride(from: 0, to: count - 15, by: 16) {
for j in 0..<8 {
let u1 = 1 - data[i + j]
let u2 = data[i + j + 8]
let radius = sqrt(-2.0 * log(u1))
let theta = 2.0 * .pi * u2
data[i + j] = radius * cos(theta) * stdev + mean
data[i + j + 8] = radius * sin(theta) * stdev + mean
}
}
if count % 16 != 0 {
for i in (count - 16)..<count {
data[i] = nextDouble()
}
let i = count - 16
for j in 0..<8 {
let u1 = 1 - data[i + j]
let u2 = data[i + j + 8]
let radius = sqrt(-2.0 * log(u1))
let theta = 2.0 * .pi * u2
data[i + j] = radius * cos(theta) * stdev + mean
data[i + j + 8] = radius * sin(theta) * stdev + mean
}
}
return data
}
/// Generate a shaped array with scalars from a normal distribution with given mean and standard deviation.
mutating func normalShapedArray(_ shape: [Int], mean: Double = 0.0, stdev: Double = 1.0) -> MLShapedArray<Double> {
let count = shape.reduce(1, *)
return .init(scalars: normalArray(count: count, mean: mean, stdev: stdev), shape: shape)
}
}
+204
View File
@@ -0,0 +1,204 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
import CoreML
/// U-Net noise prediction model for stable diffusion
@available(iOS 16.2, macOS 13.1, *)
public struct Unet: ResourceManaging {
/// Model used to predict noise residuals given an input, diffusion time step, and conditional embedding
///
/// It can be in the form of a single model or multiple stages
var models: [ManagedMLModel]
/// Creates a U-Net noise prediction model
///
/// - Parameters:
/// - url: Location of single U-Net compiled Core ML model
/// - configuration: Configuration to be used when the model is loaded
/// - Returns: U-net model that will lazily load its required resources when needed or requested
public init(modelAt url: URL,
configuration: MLModelConfiguration) {
self.models = [ManagedMLModel(modelAt: url, configuration: configuration)]
}
/// Creates a U-Net noise prediction model
///
/// - Parameters:
/// - urls: Location of chunked U-Net via urls to each compiled chunk
/// - configuration: Configuration to be used when the model is loaded
/// - Returns: U-net model that will lazily load its required resources when needed or requested
public init(chunksAt urls: [URL],
configuration: MLModelConfiguration) {
self.models = urls.map { ManagedMLModel(modelAt: $0, configuration: configuration) }
}
/// Load resources.
public func loadResources() throws {
for model in models {
try model.loadResources()
}
}
/// Unload the underlying model to free up memory
public func unloadResources() {
for model in models {
model.unloadResources()
}
}
/// Pre-warm resources
public func prewarmResources() throws {
// Override default to pre-warm each model
for model in models {
try model.loadResources()
model.unloadResources()
}
}
var latentSampleDescription: MLFeatureDescription {
try! models.first!.perform { model in
model.modelDescription.inputDescriptionsByName["sample"]!
}
}
/// The expected shape of the models latent sample input
public var latentSampleShape: [Int] {
latentSampleDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
var latentTimeIdDescription: MLFeatureDescription {
try! models.first!.perform { model in
model.modelDescription.inputDescriptionsByName["time_ids"]!
}
}
/// The expected shape of the geometry conditioning
public var latentTimeIdShape: [Int] {
latentTimeIdDescription.multiArrayConstraint!.shape.map { $0.intValue }
}
/// Batch prediction noise from latent samples
///
/// - Parameters:
/// - latents: Batch of latent samples in an array
/// - timeStep: Current diffusion timestep
/// - hiddenStates: Hidden state to condition on
/// - Returns: Array of predicted noise residuals
func predictNoise(
latents: [MLShapedArray<Float32>],
timeStep: Int,
hiddenStates: MLShapedArray<Float32>,
additionalResiduals: [[String: MLShapedArray<Float32>]]? = nil
) throws -> [MLShapedArray<Float32>] {
// Match time step batch dimension to the model / latent samples
let t: MLShapedArray<Float32>
if hiddenStates.shape[0] == 2 {
t = MLShapedArray(scalars: [Float(timeStep), Float(timeStep)], shape: [2])
} else {
t = MLShapedArray(scalars: [Float(timeStep)], shape: [1])
}
// Form batch input to model
let inputs = try latents.enumerated().map {
var dict: [String: Any] = [
"sample" : MLMultiArray($0.element),
"timestep" : MLMultiArray(t),
"encoder_hidden_states": MLMultiArray(hiddenStates)
]
if let residuals = additionalResiduals?[$0.offset] {
for (k, v) in residuals {
dict[k] = MLMultiArray(v)
}
}
return try MLDictionaryFeatureProvider(dictionary: dict)
}
let batch = MLArrayBatchProvider(array: inputs)
// Make predictions
let results = try models.predictions(from: batch)
// Pull out the results in Float32 format
let noise = (0..<results.count).map { i in
let result = results.features(at: i)
let outputName = result.featureNames.first!
let outputNoise = result.featureValue(for: outputName)!.multiArrayValue!
// To conform to this func return type make sure we return float32
// Use the fact that the concatenating constructor for MLMultiArray
// can do type conversion:
let fp32Noise = MLMultiArray(
concatenating: [outputNoise],
axis: 0,
dataType: .float32
)
return MLShapedArray<Float32>(fp32Noise)
}
return noise
}
/// Batch prediction noise from latent samples, for Stable Diffusion XL
///
/// - Parameters:
/// - latents: Batch of latent samples in an array
/// - timeStep: Current diffusion timestep
/// - hiddenStates: Hidden state to condition on
/// - pooledStates: Additional text states to condition on
/// - geometryConditioning: Condition on image geometry
/// - Returns: Array of predicted noise residuals
@available(iOS 17.0, macOS 14.0, *)
func predictNoise(
latents: [MLShapedArray<Float32>],
timeStep: Int,
hiddenStates: MLShapedArray<Float32>,
pooledStates: MLShapedArray<Float32>,
geometryConditioning: MLShapedArray<Float32>
) throws -> [MLShapedArray<Float32>] {
// Match time step batch dimension to the model / latent samples
let t = MLShapedArray<Float32>(scalars:[Float(timeStep), Float(timeStep)],shape:[2])
// Form batch input to model
let inputs = try latents.enumerated().map {
let dict: [String: Any] = [
"sample" : MLMultiArray($0.element),
"timestep" : MLMultiArray(t),
"encoder_hidden_states": MLMultiArray(hiddenStates),
"text_embeds": MLMultiArray(pooledStates),
"time_ids": MLMultiArray(geometryConditioning)
]
return try MLDictionaryFeatureProvider(dictionary: dict)
}
let batch = MLArrayBatchProvider(array: inputs)
// Make predictions
let results = try models.predictions(from: batch)
// Pull out the results in Float32 format
let noise = (0..<results.count).map { i in
let result = results.features(at: i)
let outputName = result.featureNames.first!
let outputNoise = result.featureValue(for: outputName)!.multiArrayValue!
// To conform to this func return type make sure we return float32
// Use the fact that the concatenating constructor for MLMultiArray
// can do type conversion:
let fp32Noise = MLMultiArray(
concatenating: [outputNoise],
axis: 0,
dataType: .float32
)
return MLShapedArray<Float32>(fp32Noise)
}
return noise
}
}
@@ -0,0 +1,58 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
@available(iOS 16.2, macOS 13.1, *)
extension BPETokenizer {
enum FileReadError: Error {
case invalidMergeFileLine(Int)
}
/// Read vocab.json file at URL into a dictionary mapping a String to its Int token id
static func readVocabulary(url: URL) throws -> [String: Int] {
let content = try Data(contentsOf: url)
return try JSONDecoder().decode([String: Int].self, from: content)
}
/// Read merges.txt file at URL into a dictionary mapping bigrams to the line number/rank/priority
static func readMerges(url: URL) throws -> [TokenPair: Int] {
let data = try Data(contentsOf: url)
var merges = [TokenPair: Int]()
var index = 0
var line = [UInt8]()
for byte in data {
if byte == UInt8(ascii: "\n") {
if let pair = try parseMergesLine(line, index: index) {
merges[pair] = index
}
line.removeAll(keepingCapacity: true)
index += 1
} else {
line.append(byte)
}
}
return merges
}
static func parseMergesLine(_ line: [UInt8], index: Int) throws -> TokenPair? {
if line.isEmpty || line.first == UInt8(ascii: "#") {
return nil
}
let pair = line.split(separator: UInt8(ascii: " "))
if pair.count != 2 {
throw FileReadError.invalidMergeFileLine(index + 1)
}
return TokenPair( String(bytes: pair[0]), String(bytes: pair[1]))
}
}
extension String {
init(bytes: some Collection<UInt8>) {
self.init(unsafeUninitializedCapacity: bytes.count) { pointer in
_ = pointer.initialize(fromContentsOf: bytes)
return bytes.count
}
}
}
@@ -0,0 +1,185 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2022 Apple Inc. All Rights Reserved.
import Foundation
/// A tokenizer based on byte pair encoding.
@available(iOS 16.2, macOS 13.1, *)
public struct BPETokenizer {
/// A dictionary that maps pairs of tokens to the rank/order of the merge.
let merges: [TokenPair : Int]
/// A dictionary from of tokens to identifiers.
let vocabulary: [String: Int]
/// The token used for padding
let padToken: String
/// The start token.
let startToken: String = "<|startoftext|>"
/// The end token.
let endToken: String = "<|endoftext|>"
/// The unknown token.
let unknownToken: String = "<|endoftext|>"
var unknownTokenID: Int {
vocabulary[unknownToken, default: 0]
}
/// Creates a tokenizer.
///
/// - Parameters:
/// - merges: A dictionary that maps pairs of tokens to the rank/order of the merge.
/// - vocabulary: A dictionary from of tokens to identifiers.
public init(merges: [TokenPair: Int], vocabulary: [String: Int], padToken: String = "<|endoftext|>") {
self.merges = merges
self.vocabulary = vocabulary
self.padToken = padToken
}
/// Creates a tokenizer by loading merges and vocabulary from URLs.
///
/// - Parameters:
/// - mergesURL: The URL of a text file containing merges.
/// - vocabularyURL: The URL of a JSON file containing the vocabulary.
public init(mergesAt mergesURL: URL, vocabularyAt vocabularyURL: URL, padToken: String = "<|endoftext|>") throws {
self.merges = try Self.readMerges(url: mergesURL)
self.vocabulary = try! Self.readVocabulary(url: vocabularyURL)
self.padToken = padToken
}
/// Tokenizes an input string.
///
/// - Parameters:
/// - input: A string.
/// - minCount: The minimum number of tokens to return.
/// - Returns: An array of tokens and an array of token identifiers.
public func tokenize(input: String, minCount: Int? = nil) -> (tokens: [String], tokenIDs: [Int]) {
var tokens: [String] = []
tokens.append(startToken)
tokens.append(contentsOf: encode(input: input))
tokens.append(endToken)
// Pad if there was a min length specified
if let minLen = minCount, minLen > tokens.count {
tokens.append(contentsOf: repeatElement(padToken, count: minLen - tokens.count))
}
let ids = tokens.map({ vocabulary[$0, default: unknownTokenID] })
return (tokens: tokens, tokenIDs: ids)
}
/// Returns the token identifier for a token.
public func tokenID(for token: String) -> Int? {
vocabulary[token]
}
/// Returns the token for a token identifier.
public func token(id: Int) -> String? {
vocabulary.first(where: { $0.value == id })?.key
}
/// Decodes a sequence of tokens into a fully formed string
public func decode(tokens: [String]) -> String {
String(tokens.joined())
.replacingOccurrences(of: "</w>", with: " ")
.replacingOccurrences(of: startToken, with: "")
.replacingOccurrences(of: endToken, with: "")
}
/// Encode an input string to a sequence of tokens
func encode(input: String) -> [String] {
let normalized = input.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let words = normalized.split(separator: " ")
return words.flatMap({ encode(word: $0) })
}
/// Encode a single word into a sequence of tokens
func encode(word: Substring) -> [String] {
var tokens = word.map { String($0) }
if let last = tokens.indices.last {
tokens[last] = tokens[last] + "</w>"
}
while true {
let pairs = pairs(for: tokens)
let canMerge = pairs.filter { merges[$0] != nil }
if canMerge.isEmpty {
break
}
// If multiple merges are found, use the one with the lowest rank
let shouldMerge = canMerge.min { merges[$0]! < merges[$1]! }!
tokens = update(tokens, merging: shouldMerge)
}
return tokens
}
/// Get the set of adjacent pairs / bigrams from a sequence of tokens
func pairs(for tokens: [String]) -> Set<TokenPair> {
guard tokens.count > 1 else {
return Set()
}
var pairs = Set<TokenPair>(minimumCapacity: tokens.count - 1)
var prev = tokens.first!
for current in tokens.dropFirst() {
pairs.insert(TokenPair(prev, current))
prev = current
}
return pairs
}
/// Update the sequence of tokens by greedily merging instance of a specific bigram
func update(_ tokens: [String], merging bigram: TokenPair) -> [String] {
guard tokens.count > 1 else {
return []
}
var newTokens = [String]()
newTokens.reserveCapacity(tokens.count - 1)
var index = 0
while index < tokens.count {
let remainingTokens = tokens[index...]
if let startMatchIndex = remainingTokens.firstIndex(of: bigram.first) {
// Found a possible match, append everything before it
newTokens.append(contentsOf: tokens[index..<startMatchIndex])
if index < tokens.count - 1 && tokens[startMatchIndex + 1] == bigram.second {
// Full match, merge
newTokens.append(bigram.first + bigram.second)
index = startMatchIndex + 2
} else {
// Only matched the first, no merge
newTokens.append(bigram.first)
index = startMatchIndex + 1
}
} else {
// Didn't find any more matches, append the rest unmerged
newTokens.append(contentsOf: remainingTokens)
break
}
}
return newTokens
}
}
@available(iOS 16.2, macOS 13.1, *)
extension BPETokenizer {
/// A hashable tuple of strings
public struct TokenPair: Hashable {
let first: String
let second: String
init(_ first: String, _ second: String) {
self.first = first
self.second = second
}
}
}
@@ -0,0 +1,21 @@
// For licensing see accompanying LICENSE.md file.
// Copyright (C) 2024 Apple Inc. All Rights Reserved.
import Foundation
import Hub
import Tokenizers
/// Extension to swift-transfomers Hub.swift to load local Config files
public extension Config {
/// Assumes the file is already present at local url.
/// `fileURL` is a complete local file path for the given model
public init(fileURL: URL) throws {
let data = try Data(contentsOf: fileURL)
let parsed = try JSONSerialization.jsonObject(with: data, options: [])
guard var dictionary = parsed as? [String: Any] else { throw Hub.HubClientError.parse }
// Necessary override for loading local tokenizer configs
dictionary["tokenizer_class"] = "T5Tokenizer"
self.init(dictionary)
}
}