bleam 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +23 -4
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +23 -4
- package/dist/{ui-234Plg7Z.d.cts → ui-Bg11tvlc.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +4 -1
- package/templates/image-generation/app/index.tsx +1 -1
- package/templates/native/.gitattributes +1 -0
- package/templates/native/App.tsx +26 -0
- package/templates/native/app.json +12 -0
- package/templates/native/index.ts +8 -0
- package/templates/native/ios/.xcode.env +11 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/Flux2Config.swift +288 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryConfig.swift +233 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryOptimizationConfig.swift +157 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +425 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/QuantizationConfig.swift +138 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/TransformerRepoProfile.swift +59 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/VAEConfig.swift +134 -0
- package/templates/native/ios/Bleam/AI/Flux2/Flux2Core.swift +55 -0
- package/templates/native/ios/Bleam/AI/Flux2/Loading/WeightLoader.swift +1192 -0
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +1282 -0
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/LatentUtils.swift +191 -0
- package/templates/native/ios/Bleam/AI/Flux2/Scheduler/FlowMatchEulerScheduler.swift +279 -0
- package/templates/native/ios/Bleam/AI/Flux2/Text/KleinTextEncoder.swift +380 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Attention.swift +379 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Embeddings.swift +159 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FeedForward.swift +154 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FusedKernels.swift +198 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2KVCache.swift +36 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Modulation.swift +193 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2ParallelAttention.swift +389 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2RoPE.swift +304 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2SingleBlock.swift +290 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Transformer.swift +530 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2TransformerBlock.swift +362 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/Flux2Debug.swift +101 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/MLXCheckpoint.swift +118 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/MemoryManager.swift +201 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/AutoencoderKL.swift +476 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/ResnetBlock.swift +316 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEDecoder.swift +120 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEEncoder.swift +136 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +373 -0
- package/templates/native/ios/Bleam/Appearance.swift +62 -0
- package/templates/native/ios/Bleam/Bridging-Header.h +3 -0
- package/templates/native/ios/Bleam/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/templates/native/ios/Bleam/Images.xcassets/Contents.json +6 -0
- package/templates/native/ios/Bleam/Info.plist +76 -0
- package/templates/native/ios/Bleam/PrivacyInfo.xcprivacy +48 -0
- package/templates/native/ios/Bleam/SceneDelegate.swift +58 -0
- package/templates/native/ios/Bleam/SplashScreen.storyboard +47 -0
- package/templates/native/ios/Bleam/Supporting/Expo.plist +6 -0
- package/templates/native/ios/Bleam/bleam.entitlements +10 -0
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +1146 -0
- package/templates/native/ios/Bleam.xcodeproj/xcshareddata/xcschemes/Bleam.xcscheme +88 -0
- package/templates/native/ios/GenerationService/GenerationService.entitlements +10 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +591 -0
- package/templates/native/ios/GenerationService/Info.plist +31 -0
- package/templates/native/ios/GenerationService/main.swift +165 -0
- package/templates/native/ios/PlatformHelper/Info.plist +29 -0
- package/templates/native/ios/PlatformHelper/main.swift +335 -0
- package/templates/native/ios/Podfile +198 -0
- package/templates/native/ios/Podfile.lock +2284 -0
- package/templates/native/ios/Podfile.properties.json +5 -0
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +12 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +124 -0
- package/templates/native/metro.config.js +6 -0
- package/templates/native/modules/bleam-runtime/BleamRuntime.podspec +14 -0
- package/templates/native/modules/bleam-runtime/expo-module.config.json +8 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +1258 -0
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +135 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +313 -0
- package/templates/native/modules/bleam-runtime/package.json +5 -0
- package/templates/native/package.json +39 -0
- package/templates/native/scripts/build/mlx-frameworks.ts +421 -0
- package/templates/native/scripts/start/index.ts +474 -0
- package/templates/native/scripts/start/known-failures.ts +20 -0
- package/templates/native/scripts/start/loader.ts +76 -0
- package/templates/native/scripts/start/native-state.ts +242 -0
- package/templates/native/scripts/start/package.json +4 -0
- package/templates/native/scripts/start/swift-packages.ts +414 -0
- package/templates/native/scripts/start/xcode-formatter.ts +106 -0
- package/templates/native/tsconfig.json +14 -0
- package/templates/native/yarn.lock +3749 -0
- package/dist/ui-BJmXhz9Q.d.ts +0 -92
- package/dist/ui-Dd7SXdbg.d.cts +0 -92
- package/dist/ui-Ds88eETu.d.ts +0 -92
- package/dist/ui-TaqnB5SP.d.ts +0 -92
- package/dist/ui-WQvnXL0W.d.ts +0 -92
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import MLX
|
|
3
|
+
|
|
4
|
+
public enum LatentUtils {
|
|
5
|
+
public static func generatePatchifiedLatents(
|
|
6
|
+
batchSize: Int = 1,
|
|
7
|
+
height: Int,
|
|
8
|
+
width: Int,
|
|
9
|
+
latentChannels: Int = 32,
|
|
10
|
+
patchSize: Int = 2,
|
|
11
|
+
seed: UInt64? = nil
|
|
12
|
+
) -> MLXArray {
|
|
13
|
+
if let seed {
|
|
14
|
+
MLXRandom.seed(seed)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let patchifiedChannels = latentChannels * patchSize * patchSize
|
|
18
|
+
let patchifiedHeight = height / (8 * patchSize)
|
|
19
|
+
let patchifiedWidth = width / (8 * patchSize)
|
|
20
|
+
|
|
21
|
+
return MLXRandom.normal([batchSize, patchifiedChannels, patchifiedHeight, patchifiedWidth])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public static func packPatchifiedToSequence(_ patchified: MLXArray) -> MLXArray {
|
|
25
|
+
let shape = patchified.shape
|
|
26
|
+
let batch = shape[0]
|
|
27
|
+
let channels = shape[1]
|
|
28
|
+
let height = shape[2]
|
|
29
|
+
let width = shape[3]
|
|
30
|
+
|
|
31
|
+
return patchified
|
|
32
|
+
.transposed(0, 2, 3, 1)
|
|
33
|
+
.reshaped([batch, height * width, channels])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static func unpackSequenceToPatchified(
|
|
37
|
+
_ sequence: MLXArray,
|
|
38
|
+
height: Int,
|
|
39
|
+
width: Int
|
|
40
|
+
) -> MLXArray {
|
|
41
|
+
let shape = sequence.shape
|
|
42
|
+
let batch = shape[0]
|
|
43
|
+
let channels = shape[2]
|
|
44
|
+
let patchifiedHeight = height / 16
|
|
45
|
+
let patchifiedWidth = width / 16
|
|
46
|
+
|
|
47
|
+
return sequence
|
|
48
|
+
.reshaped([batch, patchifiedHeight, patchifiedWidth, channels])
|
|
49
|
+
.transposed(0, 3, 1, 2)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static func unpatchifyLatents(
|
|
53
|
+
_ patchified: MLXArray,
|
|
54
|
+
latentChannels: Int = 32,
|
|
55
|
+
patchSize: Int = 2
|
|
56
|
+
) -> MLXArray {
|
|
57
|
+
let shape = patchified.shape
|
|
58
|
+
let batch = shape[0]
|
|
59
|
+
let patchifiedHeight = shape[2]
|
|
60
|
+
let patchifiedWidth = shape[3]
|
|
61
|
+
|
|
62
|
+
return patchified
|
|
63
|
+
.reshaped([batch, latentChannels, patchSize, patchSize, patchifiedHeight, patchifiedWidth])
|
|
64
|
+
.transposed(0, 1, 4, 2, 5, 3)
|
|
65
|
+
.reshaped([batch, latentChannels, patchifiedHeight * patchSize, patchifiedWidth * patchSize])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public static func packLatentsToPatchified(
|
|
69
|
+
_ latents: MLXArray,
|
|
70
|
+
patchSize: Int = 2
|
|
71
|
+
) -> MLXArray {
|
|
72
|
+
let shape = latents.shape
|
|
73
|
+
let batch = shape[0]
|
|
74
|
+
let channels = shape[1]
|
|
75
|
+
let height = shape[2]
|
|
76
|
+
let width = shape[3]
|
|
77
|
+
let patchifiedHeight = height / patchSize
|
|
78
|
+
let patchifiedWidth = width / patchSize
|
|
79
|
+
let patchifiedChannels = channels * patchSize * patchSize
|
|
80
|
+
|
|
81
|
+
return latents
|
|
82
|
+
.reshaped([batch, channels, patchifiedHeight, patchSize, patchifiedWidth, patchSize])
|
|
83
|
+
.transposed(0, 2, 4, 1, 3, 5)
|
|
84
|
+
.reshaped([batch, patchifiedHeight, patchifiedWidth, patchifiedChannels])
|
|
85
|
+
.transposed(0, 3, 1, 2)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public static func generateImagePositionIDs(
|
|
89
|
+
height: Int,
|
|
90
|
+
width: Int,
|
|
91
|
+
patchSize: Int = 2
|
|
92
|
+
) -> MLXArray {
|
|
93
|
+
let latentHeight = height / 8
|
|
94
|
+
let latentWidth = width / 8
|
|
95
|
+
let patchesHigh = latentHeight / patchSize
|
|
96
|
+
let patchesWide = latentWidth / patchSize
|
|
97
|
+
let patchCount = patchesHigh * patchesWide
|
|
98
|
+
|
|
99
|
+
let hIndices = MLXArray.arange(patchesHigh, dtype: .int32)
|
|
100
|
+
let wIndices = MLXArray.arange(patchesWide, dtype: .int32)
|
|
101
|
+
let hGrid = MLX.broadcast(hIndices.expandedDimensions(axis: 1), to: [patchesHigh, patchesWide])
|
|
102
|
+
let wGrid = MLX.broadcast(wIndices.expandedDimensions(axis: 0), to: [patchesHigh, patchesWide])
|
|
103
|
+
let zeros = MLXArray.zeros([patchCount], dtype: .int32)
|
|
104
|
+
|
|
105
|
+
return MLX.stacked([
|
|
106
|
+
zeros,
|
|
107
|
+
hGrid.reshaped([patchCount]),
|
|
108
|
+
wGrid.reshaped([patchCount]),
|
|
109
|
+
zeros
|
|
110
|
+
], axis: 1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public static func generateTextPositionIDs(length: Int) -> MLXArray {
|
|
114
|
+
let tokenPositions = MLXArray.arange(length, dtype: .int32)
|
|
115
|
+
let zeros = MLXArray.zeros([length], dtype: .int32)
|
|
116
|
+
|
|
117
|
+
return MLX.stacked([zeros, zeros, zeros, tokenPositions], axis: 1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public static func generateReferenceImagePositionIDs(
|
|
121
|
+
latentHeights: [Int],
|
|
122
|
+
latentWidths: [Int],
|
|
123
|
+
scale: Int = 10
|
|
124
|
+
) -> MLXArray {
|
|
125
|
+
var positions: [Int32] = []
|
|
126
|
+
|
|
127
|
+
for (imageIndex, pair) in zip(latentHeights, latentWidths).enumerated() {
|
|
128
|
+
let time = Int32(scale + scale * imageIndex)
|
|
129
|
+
let (height, width) = pair
|
|
130
|
+
for y in 0..<height {
|
|
131
|
+
for x in 0..<width {
|
|
132
|
+
positions.append(contentsOf: [time, Int32(y), Int32(x), 0])
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let totalLength = zip(latentHeights, latentWidths).reduce(0) { total, pair in
|
|
138
|
+
total + pair.0 * pair.1
|
|
139
|
+
}
|
|
140
|
+
return MLXArray(positions).reshaped([totalLength, 4])
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public static func combinePositionIDs(
|
|
144
|
+
textLength: Int,
|
|
145
|
+
height: Int,
|
|
146
|
+
width: Int,
|
|
147
|
+
patchSize: Int = 2
|
|
148
|
+
) -> (textIds: MLXArray, imageIds: MLXArray, combinedIds: MLXArray) {
|
|
149
|
+
let textIds = generateTextPositionIDs(length: textLength)
|
|
150
|
+
let imageIds = generateImagePositionIDs(height: height, width: width, patchSize: patchSize)
|
|
151
|
+
return (textIds, imageIds, concatenated([textIds, imageIds], axis: 0))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public static func validateDimensions(
|
|
155
|
+
height: Int,
|
|
156
|
+
width: Int,
|
|
157
|
+
patchSize: Int = 2
|
|
158
|
+
) -> (height: Int, width: Int) {
|
|
159
|
+
let factor = 8 * patchSize
|
|
160
|
+
return (
|
|
161
|
+
height: ((height + factor - 1) / factor) * factor,
|
|
162
|
+
width: ((width + factor - 1) / factor) * factor
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public static func denormalizeLatentsWithBatchNorm(
|
|
167
|
+
_ latents: MLXArray,
|
|
168
|
+
runningMean: MLXArray,
|
|
169
|
+
runningVar: MLXArray,
|
|
170
|
+
eps: Float = 1e-4
|
|
171
|
+
) -> MLXArray {
|
|
172
|
+
let channels = runningMean.shape[0]
|
|
173
|
+
let mean = runningMean.reshaped([1, channels, 1, 1])
|
|
174
|
+
let std = sqrt(runningVar.reshaped([1, channels, 1, 1]) + eps)
|
|
175
|
+
|
|
176
|
+
return latents * std + mean
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public static func normalizeLatentsWithBatchNorm(
|
|
180
|
+
_ latents: MLXArray,
|
|
181
|
+
runningMean: MLXArray,
|
|
182
|
+
runningVar: MLXArray,
|
|
183
|
+
eps: Float = 1e-4
|
|
184
|
+
) -> MLXArray {
|
|
185
|
+
let channels = runningMean.shape[0]
|
|
186
|
+
let mean = runningMean.reshaped([1, channels, 1, 1])
|
|
187
|
+
let std = sqrt(runningVar.reshaped([1, channels, 1, 1]) + eps)
|
|
188
|
+
|
|
189
|
+
return (latents - mean) / std
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// FlowMatchEulerScheduler.swift - Flow Matching Euler Scheduler
|
|
2
|
+
// Copyright 2025 Vincent Gourbin
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MLX
|
|
6
|
+
|
|
7
|
+
/// Compute empirical mu for Flux.2 time shifting
|
|
8
|
+
/// Ported from diffusers: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/flux2/pipeline_flux2.py
|
|
9
|
+
public func computeEmpiricalMu(imageSeqLen: Int, numSteps: Int) -> Float {
|
|
10
|
+
let a1: Float = 8.73809524e-05
|
|
11
|
+
let b1: Float = 1.89833333
|
|
12
|
+
let a2: Float = 0.00016927
|
|
13
|
+
let b2: Float = 0.45666666
|
|
14
|
+
|
|
15
|
+
if imageSeqLen > 4300 {
|
|
16
|
+
let mu = a2 * Float(imageSeqLen) + b2
|
|
17
|
+
return mu
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let m_200 = a2 * Float(imageSeqLen) + b2
|
|
21
|
+
let m_10 = a1 * Float(imageSeqLen) + b1
|
|
22
|
+
|
|
23
|
+
let a = (m_200 - m_10) / 190.0
|
|
24
|
+
let b = m_200 - 200.0 * a
|
|
25
|
+
let mu = a * Float(numSteps) + b
|
|
26
|
+
|
|
27
|
+
return mu
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Flow Matching Euler Discrete Scheduler for Flux.2
|
|
31
|
+
///
|
|
32
|
+
/// Implements the flow matching ODE solver with Euler steps.
|
|
33
|
+
/// Based on diffusers FlowMatchEulerDiscreteScheduler
|
|
34
|
+
public class FlowMatchEulerScheduler: @unchecked Sendable {
|
|
35
|
+
/// Number of training timesteps
|
|
36
|
+
public let numTrainTimesteps: Int
|
|
37
|
+
|
|
38
|
+
/// Shift parameter for timestep scheduling
|
|
39
|
+
public let shift: Float
|
|
40
|
+
|
|
41
|
+
/// Current inference timesteps (in [0, 1000] range for compatibility)
|
|
42
|
+
public private(set) var timesteps: [Float] = []
|
|
43
|
+
|
|
44
|
+
/// Current sigmas (noise levels in [0, 1] range)
|
|
45
|
+
public private(set) var sigmas: [Float] = []
|
|
46
|
+
|
|
47
|
+
/// Step index during sampling
|
|
48
|
+
public private(set) var stepIndex: Int = 0
|
|
49
|
+
|
|
50
|
+
public init(
|
|
51
|
+
numTrainTimesteps: Int = 1000,
|
|
52
|
+
shift: Float = 1.0
|
|
53
|
+
) {
|
|
54
|
+
self.numTrainTimesteps = numTrainTimesteps
|
|
55
|
+
self.shift = shift
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Set timesteps for inference with Flux.2 specific scheduling
|
|
59
|
+
/// - Parameters:
|
|
60
|
+
/// - numInferenceSteps: Number of denoising steps
|
|
61
|
+
/// - imageSeqLen: Length of image sequence (for mu calculation)
|
|
62
|
+
/// - strength: Denoising strength for I2I (1.0 = full denoise, 0.5 = start at 50%)
|
|
63
|
+
/// - Returns: The initial timestep index (0 for T2I, >0 for I2I with strength < 1.0)
|
|
64
|
+
@discardableResult
|
|
65
|
+
public func setTimesteps(numInferenceSteps: Int, imageSeqLen: Int? = nil, strength: Float = 1.0) -> Int {
|
|
66
|
+
// Compute mu based on image sequence length (Flux.2 specific), unless a
|
|
67
|
+
// model profile provided an explicit shift override.
|
|
68
|
+
let mu: Float
|
|
69
|
+
if shift != 1.0 {
|
|
70
|
+
mu = shift
|
|
71
|
+
} else if let seqLen = imageSeqLen {
|
|
72
|
+
mu = computeEmpiricalMu(imageSeqLen: seqLen, numSteps: numInferenceSteps)
|
|
73
|
+
} else {
|
|
74
|
+
// Default mu for 1024x1024 (4096 latent patches)
|
|
75
|
+
mu = computeEmpiricalMu(imageSeqLen: 4096, numSteps: numInferenceSteps)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Generate sigmas: linspace(1.0, 1/num_steps, num_steps)
|
|
79
|
+
// This is the Flux.2 specific sigma schedule
|
|
80
|
+
var allSigmas: [Float] = []
|
|
81
|
+
for i in 0..<numInferenceSteps {
|
|
82
|
+
let sigma = 1.0 - Float(i) / Float(numInferenceSteps)
|
|
83
|
+
allSigmas.append(sigma)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Apply time shifting with mu (exponential shift)
|
|
87
|
+
allSigmas = allSigmas.map { sigma in
|
|
88
|
+
timeShift(mu: mu, sigma: 1.0, t: sigma)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Append terminal sigma (0)
|
|
92
|
+
allSigmas.append(0.0)
|
|
93
|
+
|
|
94
|
+
// For I2I with strength < 1.0, skip early steps
|
|
95
|
+
// strength = 1.0 → start from step 0 (full denoising)
|
|
96
|
+
// strength = 0.5 → start from step numSteps/2 (50% denoising)
|
|
97
|
+
// strength = 0.1 → start from step numSteps*0.9 (10% denoising)
|
|
98
|
+
let clampedStrength = max(0.01, min(1.0, strength))
|
|
99
|
+
let initTimestepIndex = numInferenceSteps - Int(Float(numInferenceSteps) * clampedStrength)
|
|
100
|
+
let tStart = max(0, initTimestepIndex)
|
|
101
|
+
|
|
102
|
+
// Slice sigmas from the starting point
|
|
103
|
+
self.sigmas = Array(allSigmas[tStart...])
|
|
104
|
+
|
|
105
|
+
// Timesteps = sigmas * num_train_timesteps (for compatibility)
|
|
106
|
+
self.timesteps = self.sigmas.map { $0 * Float(numTrainTimesteps) }
|
|
107
|
+
|
|
108
|
+
self.stepIndex = 0
|
|
109
|
+
|
|
110
|
+
let effectiveSteps = sigmas.count - 1
|
|
111
|
+
Flux2Debug.log("Scheduler set: \(effectiveSteps) effective steps (strength=\(clampedStrength), mu=\(mu), shift=\(shift))")
|
|
112
|
+
if strength < 1.0 {
|
|
113
|
+
Flux2Debug.log("I2I mode: starting from timestep \(tStart) (skipping \(tStart) steps)")
|
|
114
|
+
}
|
|
115
|
+
Flux2Debug.verbose("Sigmas: \(sigmas.prefix(5))... to \(sigmas.suffix(2))")
|
|
116
|
+
|
|
117
|
+
return tStart
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Get the initial sigma for noise injection (used by I2I)
|
|
121
|
+
public var initialSigma: Float {
|
|
122
|
+
sigmas.first ?? 1.0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Time shift function (exponential) - matches diffusers _time_shift_exponential
|
|
126
|
+
private func timeShift(mu: Float, sigma: Float, t: Float) -> Float {
|
|
127
|
+
// exponential time shift: exp(mu) / (exp(mu) + (1/t - 1)^sigma)
|
|
128
|
+
let expMu = exp(mu)
|
|
129
|
+
let denominator = expMu + pow(1.0 / t - 1.0, sigma)
|
|
130
|
+
return expMu / denominator
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Perform one Euler step
|
|
134
|
+
/// - Parameters:
|
|
135
|
+
/// - modelOutput: Predicted velocity from transformer
|
|
136
|
+
/// - timestep: Current timestep (not used, we track via stepIndex)
|
|
137
|
+
/// - sample: Current noisy sample
|
|
138
|
+
/// - Returns: Updated sample for next step
|
|
139
|
+
public func step(
|
|
140
|
+
modelOutput: MLXArray,
|
|
141
|
+
timestep: Float,
|
|
142
|
+
sample: MLXArray
|
|
143
|
+
) -> MLXArray {
|
|
144
|
+
guard stepIndex < sigmas.count - 1 else {
|
|
145
|
+
return sample
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let sigma = sigmas[stepIndex]
|
|
149
|
+
let sigmaNext = sigmas[stepIndex + 1]
|
|
150
|
+
|
|
151
|
+
// Euler step: x_{t-1} = x_t + (sigma_next - sigma) * v_t
|
|
152
|
+
// where v_t is the velocity (model output)
|
|
153
|
+
let dt = sigmaNext - sigma
|
|
154
|
+
let nextSample = sample + dt * modelOutput
|
|
155
|
+
|
|
156
|
+
stepIndex += 1
|
|
157
|
+
|
|
158
|
+
return nextSample
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Get the current sigma for this step
|
|
162
|
+
public var currentSigma: Float {
|
|
163
|
+
guard stepIndex < sigmas.count else { return 0 }
|
|
164
|
+
return sigmas[stepIndex]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Get initial noise scale
|
|
168
|
+
public func initNoiseSigma(for timestep: Float) -> Float {
|
|
169
|
+
// For flow matching starting at t=1, the initial noise scale is 1
|
|
170
|
+
return 1.0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Scale model input (identity for flow matching)
|
|
174
|
+
public func scaleModelInput(_ sample: MLXArray, timestep: Float) -> MLXArray {
|
|
175
|
+
sample
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Add noise to latents for a given timestep
|
|
179
|
+
public func addNoise(
|
|
180
|
+
originalSamples: MLXArray,
|
|
181
|
+
noise: MLXArray,
|
|
182
|
+
timestep: Float
|
|
183
|
+
) -> MLXArray {
|
|
184
|
+
// For flow matching: x_t = (1 - t) * x_0 + t * noise
|
|
185
|
+
// where t is sigma (in [0, 1] range)
|
|
186
|
+
let sigma = timestep / Float(numTrainTimesteps)
|
|
187
|
+
let t = MLXArray(sigma)
|
|
188
|
+
return (1 - t) * originalSamples + t * noise
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Scale noise for image-to-image generation (flow matching)
|
|
192
|
+
/// This is the core operation for I2I: mix the encoded image with noise based on sigma
|
|
193
|
+
/// - Parameters:
|
|
194
|
+
/// - sample: Original image latents (encoded by VAE)
|
|
195
|
+
/// - sigma: Current sigma/noise level (from the time-shifted schedule, in [0, 1] range)
|
|
196
|
+
/// - noise: Random noise tensor
|
|
197
|
+
/// - Returns: Noisy latents: (1 - sigma) * sample + sigma * noise
|
|
198
|
+
public func scaleNoise(
|
|
199
|
+
sample: MLXArray,
|
|
200
|
+
sigma: Float,
|
|
201
|
+
noise: MLXArray
|
|
202
|
+
) -> MLXArray {
|
|
203
|
+
// For flow matching: x_t = (1 - t) * x_0 + t * noise
|
|
204
|
+
// sigma is already in [0, 1] range after time shifting
|
|
205
|
+
let t = MLXArray(sigma)
|
|
206
|
+
return (1 - t) * sample + t * noise
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Get velocity target for training
|
|
210
|
+
public func getVelocity(
|
|
211
|
+
sample: MLXArray,
|
|
212
|
+
noise: MLXArray,
|
|
213
|
+
timestep: Float
|
|
214
|
+
) -> MLXArray {
|
|
215
|
+
// Velocity target: v = noise - sample
|
|
216
|
+
noise - sample
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Reset scheduler state
|
|
220
|
+
public func reset() {
|
|
221
|
+
stepIndex = 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// Set timesteps with custom sigmas (for Turbo LoRAs)
|
|
225
|
+
///
|
|
226
|
+
/// This bypasses the normal mu/timeshift computation and directly uses
|
|
227
|
+
/// the provided sigma schedule. Used by specialized LoRAs like Flux.2 Turbo
|
|
228
|
+
/// that require pre-computed noise schedules.
|
|
229
|
+
///
|
|
230
|
+
/// The custom sigmas define the noise levels to denoise through. If the final sigma
|
|
231
|
+
/// is not 0.0, a terminal sigma of 0.0 is automatically appended to ensure proper
|
|
232
|
+
/// denoising completion. This matches the standard scheduler behavior where N sigmas
|
|
233
|
+
/// yield N-1 intervals, so N custom sigmas + terminal 0.0 = N denoising steps.
|
|
234
|
+
///
|
|
235
|
+
/// - Parameter customSigmas: Pre-computed sigma schedule.
|
|
236
|
+
/// Should start near 1.0 and end with the second-to-last noise level.
|
|
237
|
+
/// Example for 8-step Turbo: [1.0, 0.6509, 0.4374, 0.2932, 0.1893, 0.1108, 0.0495, 0.00031]
|
|
238
|
+
/// This will produce 8 denoising steps (with terminal 0.0 appended).
|
|
239
|
+
public func setCustomSigmas(_ customSigmas: [Float]) {
|
|
240
|
+
guard !customSigmas.isEmpty else {
|
|
241
|
+
Flux2Debug.log("[Scheduler] Warning: Empty custom sigmas provided, ignoring")
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Append terminal sigma 0.0 if not already present
|
|
246
|
+
// This ensures N custom sigmas produce N denoising steps
|
|
247
|
+
// Even if the last sigma is very small (like 0.00031), we need to add 0.0
|
|
248
|
+
// to complete the final denoising step
|
|
249
|
+
var sigmasWithTerminal = customSigmas
|
|
250
|
+
let lastSigma = customSigmas.last ?? 0.0
|
|
251
|
+
if lastSigma != 0.0 { // If last sigma is not exactly 0.0, append terminal
|
|
252
|
+
sigmasWithTerminal.append(0.0)
|
|
253
|
+
Flux2Debug.log("[Scheduler] Appended terminal sigma 0.0 (last was \(lastSigma))")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
self.sigmas = sigmasWithTerminal
|
|
257
|
+
self.timesteps = sigmasWithTerminal.map { $0 * Float(numTrainTimesteps) }
|
|
258
|
+
self.stepIndex = 0
|
|
259
|
+
|
|
260
|
+
let effectiveSteps = sigmas.count - 1
|
|
261
|
+
Flux2Debug.log("[Scheduler] Set custom sigmas: \(effectiveSteps) effective steps from \(customSigmas.count) input sigmas")
|
|
262
|
+
Flux2Debug.verbose("[Scheduler] Sigmas: \(sigmas)")
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// MARK: - Progress Tracking
|
|
267
|
+
|
|
268
|
+
extension FlowMatchEulerScheduler {
|
|
269
|
+
/// Current progress (0.0 to 1.0)
|
|
270
|
+
public var progress: Float {
|
|
271
|
+
guard !timesteps.isEmpty else { return 0 }
|
|
272
|
+
return Float(stepIndex) / Float(timesteps.count - 1)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Remaining steps
|
|
276
|
+
public var remainingSteps: Int {
|
|
277
|
+
max(0, timesteps.count - 1 - stepIndex)
|
|
278
|
+
}
|
|
279
|
+
}
|