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.
Files changed (92) hide show
  1. package/dist/cli.cjs +23 -4
  2. package/dist/cli.d.cts +2 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +23 -4
  5. package/dist/{ui-234Plg7Z.d.cts → ui-Bg11tvlc.d.ts} +7 -7
  6. package/dist/ui.d.ts +1 -1
  7. package/dist/window.d.ts +1 -1
  8. package/package.json +4 -1
  9. package/templates/image-generation/app/index.tsx +1 -1
  10. package/templates/native/.gitattributes +1 -0
  11. package/templates/native/App.tsx +26 -0
  12. package/templates/native/app.json +12 -0
  13. package/templates/native/index.ts +8 -0
  14. package/templates/native/ios/.xcode.env +11 -0
  15. package/templates/native/ios/Bleam/AI/Flux2/Configuration/Flux2Config.swift +288 -0
  16. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryConfig.swift +233 -0
  17. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryOptimizationConfig.swift +157 -0
  18. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +425 -0
  19. package/templates/native/ios/Bleam/AI/Flux2/Configuration/QuantizationConfig.swift +138 -0
  20. package/templates/native/ios/Bleam/AI/Flux2/Configuration/TransformerRepoProfile.swift +59 -0
  21. package/templates/native/ios/Bleam/AI/Flux2/Configuration/VAEConfig.swift +134 -0
  22. package/templates/native/ios/Bleam/AI/Flux2/Flux2Core.swift +55 -0
  23. package/templates/native/ios/Bleam/AI/Flux2/Loading/WeightLoader.swift +1192 -0
  24. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +1282 -0
  25. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/LatentUtils.swift +191 -0
  26. package/templates/native/ios/Bleam/AI/Flux2/Scheduler/FlowMatchEulerScheduler.swift +279 -0
  27. package/templates/native/ios/Bleam/AI/Flux2/Text/KleinTextEncoder.swift +380 -0
  28. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Attention.swift +379 -0
  29. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Embeddings.swift +159 -0
  30. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FeedForward.swift +154 -0
  31. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FusedKernels.swift +198 -0
  32. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2KVCache.swift +36 -0
  33. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Modulation.swift +193 -0
  34. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2ParallelAttention.swift +389 -0
  35. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2RoPE.swift +304 -0
  36. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2SingleBlock.swift +290 -0
  37. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Transformer.swift +530 -0
  38. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2TransformerBlock.swift +362 -0
  39. package/templates/native/ios/Bleam/AI/Flux2/Utils/Flux2Debug.swift +101 -0
  40. package/templates/native/ios/Bleam/AI/Flux2/Utils/MLXCheckpoint.swift +118 -0
  41. package/templates/native/ios/Bleam/AI/Flux2/Utils/MemoryManager.swift +201 -0
  42. package/templates/native/ios/Bleam/AI/Flux2/VAE/AutoencoderKL.swift +476 -0
  43. package/templates/native/ios/Bleam/AI/Flux2/VAE/ResnetBlock.swift +316 -0
  44. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEDecoder.swift +120 -0
  45. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEEncoder.swift +136 -0
  46. package/templates/native/ios/Bleam/AppDelegate.swift +373 -0
  47. package/templates/native/ios/Bleam/Appearance.swift +62 -0
  48. package/templates/native/ios/Bleam/Bridging-Header.h +3 -0
  49. package/templates/native/ios/Bleam/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  50. package/templates/native/ios/Bleam/Images.xcassets/Contents.json +6 -0
  51. package/templates/native/ios/Bleam/Info.plist +76 -0
  52. package/templates/native/ios/Bleam/PrivacyInfo.xcprivacy +48 -0
  53. package/templates/native/ios/Bleam/SceneDelegate.swift +58 -0
  54. package/templates/native/ios/Bleam/SplashScreen.storyboard +47 -0
  55. package/templates/native/ios/Bleam/Supporting/Expo.plist +6 -0
  56. package/templates/native/ios/Bleam/bleam.entitlements +10 -0
  57. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +1146 -0
  58. package/templates/native/ios/Bleam.xcodeproj/xcshareddata/xcschemes/Bleam.xcscheme +88 -0
  59. package/templates/native/ios/GenerationService/GenerationService.entitlements +10 -0
  60. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +591 -0
  61. package/templates/native/ios/GenerationService/Info.plist +31 -0
  62. package/templates/native/ios/GenerationService/main.swift +165 -0
  63. package/templates/native/ios/PlatformHelper/Info.plist +29 -0
  64. package/templates/native/ios/PlatformHelper/main.swift +335 -0
  65. package/templates/native/ios/Podfile +198 -0
  66. package/templates/native/ios/Podfile.lock +2284 -0
  67. package/templates/native/ios/Podfile.properties.json +5 -0
  68. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +12 -0
  69. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +124 -0
  70. package/templates/native/metro.config.js +6 -0
  71. package/templates/native/modules/bleam-runtime/BleamRuntime.podspec +14 -0
  72. package/templates/native/modules/bleam-runtime/expo-module.config.json +8 -0
  73. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +1258 -0
  74. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +135 -0
  75. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +313 -0
  76. package/templates/native/modules/bleam-runtime/package.json +5 -0
  77. package/templates/native/package.json +39 -0
  78. package/templates/native/scripts/build/mlx-frameworks.ts +421 -0
  79. package/templates/native/scripts/start/index.ts +474 -0
  80. package/templates/native/scripts/start/known-failures.ts +20 -0
  81. package/templates/native/scripts/start/loader.ts +76 -0
  82. package/templates/native/scripts/start/native-state.ts +242 -0
  83. package/templates/native/scripts/start/package.json +4 -0
  84. package/templates/native/scripts/start/swift-packages.ts +414 -0
  85. package/templates/native/scripts/start/xcode-formatter.ts +106 -0
  86. package/templates/native/tsconfig.json +14 -0
  87. package/templates/native/yarn.lock +3749 -0
  88. package/dist/ui-BJmXhz9Q.d.ts +0 -92
  89. package/dist/ui-Dd7SXdbg.d.cts +0 -92
  90. package/dist/ui-Ds88eETu.d.ts +0 -92
  91. package/dist/ui-TaqnB5SP.d.ts +0 -92
  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
+ }