// 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..] = try generateLatentSamples(configuration: config, scheduler: scheduler[0]) // Store denoised latents from scheduler to pass into decoder var denoisedLatents: [MLShapedArray] = 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(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.. (MLShapedArray, MLShapedArray) { var embeds = MLShapedArray() var pooled = MLShapedArray() let (embeds1, pooledValue1) = try textEncoder.encode(prompt) let (embeds2, pooledValue2) = try textEncoder2.encode(prompt) var embedsT5 = try textEncoderT5?.encode(prompt).encoderHiddenStates ?? MLShapedArray(repeating: 0, shape: [1, 4096, 1, 77]) // Truncate T5 embedsT5 = truncatedT5Embeds(embedsT5) let padding1 = MLShapedArray(repeating: 0, shape: [1, 77, 2048]) // Base needs concatenated embeddings // [1, 77, 768], [1, 77, 1280], [1, 77, 2048] -> [1, 77, 4096] embeds = MLShapedArray( 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( concatenating: [embeds, embedsT5], alongAxis: 3 ) // [1, 768], [1, 1280] -> [1, 2048] pooled = MLShapedArray( 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( scalars: pooledScalars.scalars, shape: [2, 2048, 1, 1] ) return ModelInputs(hiddenStates: hiddenStates, pooledStates: pooledStates) } func generateLatentSamples(configuration config: Configuration, scheduler: Scheduler) throws -> [MLShapedArray] { var sampleShape = mmdit.latentImageEmbeddingsShape sampleShape[0] = 1 let stdev = scheduler.initNoiseSigma var random = randomSource(from: config.rngType, seed: config.seed) let samples = (0..( 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], _ guidanceScale: Float) -> [MLShapedArray] { noise.map { performGuidance($0, guidanceScale) } } func performGuidance(_ noise: MLShapedArray, _ guidanceScale: Float) -> MLShapedArray { var shape = noise.shape shape[0] = 1 return MLShapedArray(unsafeUninitializedShape: shape) { result, _ in noise.withUnsafeShapedBufferPointer { scalars, _, strides in for i in 0..], 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], 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.. var pooledStates: MLShapedArray } /// Helper function to truncate the T5 embeddings func truncatedT5Embeds(_ embedding: MLShapedArray) -> MLShapedArray { // 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(repeating: 0.0, shape: stateShape) for i0 in 0.. 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.. UInt8 { return UInt8(Swift.max(Float32(lower), Swift.min(value, Float32(upper)))) } }