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,1282 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import Foundation
|
|
3
|
+
import MLX
|
|
4
|
+
import MLXNN
|
|
5
|
+
|
|
6
|
+
public struct FluxImageRunnerPaths: Sendable {
|
|
7
|
+
public let transformer: URL
|
|
8
|
+
public let vae: URL
|
|
9
|
+
|
|
10
|
+
public init(transformer: URL, vae: URL) {
|
|
11
|
+
self.transformer = transformer
|
|
12
|
+
self.vae = vae
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public enum FluxImageGenerationMode: Sendable {
|
|
17
|
+
case textToImage
|
|
18
|
+
case imageToImage(sourceImage: CGImage?, maskImage: CGImage?, maskMode: FluxImageMaskMode, referenceImages: [CGImage], referenceInfluences: [Float]?, strength: Float)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public enum FluxImageMaskMode: Sendable {
|
|
22
|
+
case paint
|
|
23
|
+
case preserve
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public struct FluxLoRAConfig: Sendable {
|
|
27
|
+
public let id: String?
|
|
28
|
+
public let path: String
|
|
29
|
+
public let scale: Float
|
|
30
|
+
|
|
31
|
+
public init(id: String? = nil, path: String, scale: Float) {
|
|
32
|
+
self.id = id
|
|
33
|
+
self.path = path
|
|
34
|
+
self.scale = scale
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private enum LoRARuntimeWarnings {
|
|
39
|
+
nonisolated(unsafe) static var skippedShapeMismatchCount = 0
|
|
40
|
+
static let maxWarnings = 12
|
|
41
|
+
|
|
42
|
+
static func warnShapeMismatch(layerName: String?, inputShape: [Int], downShape: [Int], upShape: [Int]) {
|
|
43
|
+
skippedShapeMismatchCount += 1
|
|
44
|
+
guard skippedShapeMismatchCount <= maxWarnings else { return }
|
|
45
|
+
Flux2Debug.warning(
|
|
46
|
+
"Skipping runtime LoRA shape mismatch: layer=\(layerName ?? "unknown") input=\(inputShape), down=\(downShape), up=\(upShape)"
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private struct LoRATensorDiagnostic {
|
|
52
|
+
let key: String
|
|
53
|
+
let shape: [Int]
|
|
54
|
+
let min: Float
|
|
55
|
+
let max: Float
|
|
56
|
+
let mean: Float
|
|
57
|
+
let nonFiniteCount: Int
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private struct LoRADiagnostics {
|
|
61
|
+
let fileName: String
|
|
62
|
+
let tensorCount: Int
|
|
63
|
+
let layerCount: Int
|
|
64
|
+
let rank: Int
|
|
65
|
+
let targetModel: String
|
|
66
|
+
let sampleTensors: [LoRATensorDiagnostic]
|
|
67
|
+
let nonFiniteTensorCount: Int
|
|
68
|
+
let nonFiniteValueCount: Int
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private struct LoRADimensionMismatch {
|
|
72
|
+
let transformerLayer: String
|
|
73
|
+
let loraLayer: String
|
|
74
|
+
let loraName: String
|
|
75
|
+
let expectedInFeatures: Int
|
|
76
|
+
let expectedOutFeatures: Int
|
|
77
|
+
let downShape: [Int]
|
|
78
|
+
let upShape: [Int]
|
|
79
|
+
|
|
80
|
+
var logDescription: String {
|
|
81
|
+
"\(loraName) layer=\(loraLayer) transformer=\(transformerLayer) expected=[out:\(expectedOutFeatures), in:\(expectedInFeatures)] down=\(downShape) up=\(upShape)"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private struct LoRAFileApplyReport {
|
|
86
|
+
let name: String
|
|
87
|
+
let loadedLayerCount: Int
|
|
88
|
+
let matchedLayerCount: Int
|
|
89
|
+
let appliedLayerCount: Int
|
|
90
|
+
let dimensionMismatchCount: Int
|
|
91
|
+
let sampleMatchedLayers: [String]
|
|
92
|
+
let sampleUnmatchedLayers: [String]
|
|
93
|
+
let sampleDimensionMismatches: [LoRADimensionMismatch]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private struct LoRAApplyReport {
|
|
97
|
+
let loadedLayerCount: Int
|
|
98
|
+
let transformerLinearLayerCount: Int
|
|
99
|
+
let matchedLayerCount: Int
|
|
100
|
+
let replacedLayerCount: Int
|
|
101
|
+
let updatedLayerCount: Int
|
|
102
|
+
let dimensionMismatchCount: Int
|
|
103
|
+
let sampleDimensionMismatches: [LoRADimensionMismatch]
|
|
104
|
+
let sampleUnmatchedLoRALayers: [String]
|
|
105
|
+
let sampleTransformerLayers: [String]
|
|
106
|
+
let loraReports: [LoRAFileApplyReport]
|
|
107
|
+
|
|
108
|
+
var appliedLayerCount: Int { replacedLayerCount + updatedLayerCount }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private struct TransformerLinearTarget {
|
|
112
|
+
let module: Module
|
|
113
|
+
let inputFeatures: Int
|
|
114
|
+
let outputFeatures: Int
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private struct LoRAContribution {
|
|
118
|
+
let down: MLXArray
|
|
119
|
+
let up: MLXArray
|
|
120
|
+
let scale: Float
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private final class LoRALinear: Linear, @unchecked Sendable {
|
|
124
|
+
private let down: MLXArray
|
|
125
|
+
private let up: MLXArray
|
|
126
|
+
private let scale: Float
|
|
127
|
+
private let debugName: String?
|
|
128
|
+
|
|
129
|
+
init(base: Linear, down: MLXArray, up: MLXArray, scale: Float, debugName: String?) {
|
|
130
|
+
self.down = down
|
|
131
|
+
self.up = up
|
|
132
|
+
self.scale = scale
|
|
133
|
+
self.debugName = debugName
|
|
134
|
+
super.init(weight: base.weight, bias: base.bias)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
override func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
138
|
+
let result = super.callAsFunction(x)
|
|
139
|
+
guard down.shape.count == 2,
|
|
140
|
+
up.shape.count == 2,
|
|
141
|
+
x.shape.last == down.shape[1],
|
|
142
|
+
down.shape[0] == up.shape[1] else {
|
|
143
|
+
LoRARuntimeWarnings.warnShapeMismatch(
|
|
144
|
+
layerName: debugName,
|
|
145
|
+
inputShape: x.shape,
|
|
146
|
+
downShape: down.shape,
|
|
147
|
+
upShape: up.shape
|
|
148
|
+
)
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
let delta = matmul(matmul(x, down.transposed(1, 0)), up.transposed(1, 0)) * scale
|
|
152
|
+
return result + delta.asType(result.dtype)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private final class LoRAQuantizedLinear: QuantizedLinear, @unchecked Sendable {
|
|
157
|
+
private let down: MLXArray
|
|
158
|
+
private let up: MLXArray
|
|
159
|
+
private let scale: Float
|
|
160
|
+
private let debugName: String?
|
|
161
|
+
|
|
162
|
+
init(base: QuantizedLinear, down: MLXArray, up: MLXArray, scale: Float, debugName: String?) {
|
|
163
|
+
self.down = down
|
|
164
|
+
self.up = up
|
|
165
|
+
self.scale = scale
|
|
166
|
+
self.debugName = debugName
|
|
167
|
+
super.init(
|
|
168
|
+
weight: base.weight,
|
|
169
|
+
bias: base.bias,
|
|
170
|
+
scales: base.scales,
|
|
171
|
+
biases: base.biases,
|
|
172
|
+
groupSize: base.groupSize,
|
|
173
|
+
bits: base.bits,
|
|
174
|
+
mode: base.mode
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
179
|
+
let result = super.callAsFunction(x)
|
|
180
|
+
guard down.shape.count == 2,
|
|
181
|
+
up.shape.count == 2,
|
|
182
|
+
x.shape.last == down.shape[1],
|
|
183
|
+
down.shape[0] == up.shape[1] else {
|
|
184
|
+
LoRARuntimeWarnings.warnShapeMismatch(
|
|
185
|
+
layerName: debugName,
|
|
186
|
+
inputShape: x.shape,
|
|
187
|
+
downShape: down.shape,
|
|
188
|
+
upShape: up.shape
|
|
189
|
+
)
|
|
190
|
+
return result
|
|
191
|
+
}
|
|
192
|
+
let delta = matmul(matmul(x, down.transposed(1, 0)), up.transposed(1, 0)) * scale
|
|
193
|
+
return result + delta.asType(result.dtype)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public final class FluxImageRunner: @unchecked Sendable {
|
|
198
|
+
private static let referenceImageMaxLongSide = 768
|
|
199
|
+
|
|
200
|
+
private let paths: FluxImageRunnerPaths
|
|
201
|
+
private let scheduler = FlowMatchEulerScheduler()
|
|
202
|
+
private var transformer: Flux2Transformer2DModel?
|
|
203
|
+
private var vae: AutoencoderKLFlux2?
|
|
204
|
+
|
|
205
|
+
public init(paths: FluxImageRunnerPaths) {
|
|
206
|
+
self.paths = paths
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public func generate(
|
|
210
|
+
textEmbeddings: MLXArray,
|
|
211
|
+
height requestedHeight: Int,
|
|
212
|
+
width requestedWidth: Int,
|
|
213
|
+
steps: Int,
|
|
214
|
+
guidance: Float,
|
|
215
|
+
seed: UInt64?,
|
|
216
|
+
mode: FluxImageGenerationMode = .textToImage,
|
|
217
|
+
loras: [FluxLoRAConfig] = [],
|
|
218
|
+
sigmas: [Float]? = nil,
|
|
219
|
+
onStage: ((String) -> Void)? = nil,
|
|
220
|
+
onProgress: (@Sendable (Int, Int) -> Void)? = nil
|
|
221
|
+
) throws -> CGImage {
|
|
222
|
+
let (height, width) = LatentUtils.validateDimensions(height: requestedHeight, width: requestedWidth)
|
|
223
|
+
try validateTextEmbeddings(textEmbeddings)
|
|
224
|
+
|
|
225
|
+
if let seed {
|
|
226
|
+
MLXRandom.seed(seed)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
defer {
|
|
230
|
+
// Failed generations can otherwise leave partially loaded weights
|
|
231
|
+
// behind. Keep this path conservative until we add explicit model
|
|
232
|
+
// lifecycle controls and memory telemetry.
|
|
233
|
+
self.transformer = nil
|
|
234
|
+
self.vae = nil
|
|
235
|
+
MLX.Memory.clearCache()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let textLength = textEmbeddings.shape[1]
|
|
239
|
+
let (textIds, outputImageIds, _) = LatentUtils.combinePositionIDs(
|
|
240
|
+
textLength: textLength,
|
|
241
|
+
height: height,
|
|
242
|
+
width: width
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
let conditioning = try encodeImageConditioning(
|
|
246
|
+
mode: mode,
|
|
247
|
+
height: height,
|
|
248
|
+
width: width,
|
|
249
|
+
steps: steps,
|
|
250
|
+
sigmas: sigmas
|
|
251
|
+
)
|
|
252
|
+
var packedLatents = conditioning.initialLatents
|
|
253
|
+
let imageIds = conditioning.imageIds ?? outputImageIds
|
|
254
|
+
let outputSequenceLength = conditioning.outputSequenceLength
|
|
255
|
+
eval(packedLatents)
|
|
256
|
+
|
|
257
|
+
do {
|
|
258
|
+
onStage?("denoise")
|
|
259
|
+
// Keep the Flux transformer alive only for denoising. The VAE is
|
|
260
|
+
// loaded after this scope so both large model families are not
|
|
261
|
+
// resident together on memory-constrained machines.
|
|
262
|
+
let transformer = try loadTransformer()
|
|
263
|
+
try applyLoRAs(loras, to: transformer)
|
|
264
|
+
|
|
265
|
+
let effectiveSteps = scheduler.sigmas.count - 1
|
|
266
|
+
for stepIndex in 0..<effectiveSteps {
|
|
267
|
+
if Task.isCancelled {
|
|
268
|
+
throw Flux2Error.generationCancelled
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let sigma = scheduler.sigmas[stepIndex]
|
|
272
|
+
let timestep = MLXArray([sigma])
|
|
273
|
+
let hiddenStates = conditioning.referenceLatents.map {
|
|
274
|
+
concatenated([packedLatents, $0], axis: 1)
|
|
275
|
+
} ?? packedLatents
|
|
276
|
+
eval(hiddenStates)
|
|
277
|
+
let noisePrediction = transformer(
|
|
278
|
+
hiddenStates: hiddenStates,
|
|
279
|
+
encoderHiddenStates: textEmbeddings,
|
|
280
|
+
timestep: timestep,
|
|
281
|
+
guidance: nil,
|
|
282
|
+
imgIds: imageIds,
|
|
283
|
+
txtIds: textIds
|
|
284
|
+
)
|
|
285
|
+
let outputNoisePrediction = conditioning.referenceLatents == nil
|
|
286
|
+
? noisePrediction
|
|
287
|
+
: noisePrediction[0..., 0..<outputSequenceLength, 0...]
|
|
288
|
+
|
|
289
|
+
packedLatents = scheduler.step(
|
|
290
|
+
modelOutput: outputNoisePrediction,
|
|
291
|
+
timestep: sigma,
|
|
292
|
+
sample: packedLatents
|
|
293
|
+
)
|
|
294
|
+
eval(packedLatents)
|
|
295
|
+
|
|
296
|
+
if let maskedBlend = conditioning.maskedBlend {
|
|
297
|
+
let sigmaNext = stepIndex + 1 < scheduler.sigmas.count ? scheduler.sigmas[stepIndex + 1] : 0
|
|
298
|
+
let freshNoise = MLXRandom.normal(packedLatents.shape)
|
|
299
|
+
let nextSigma = MLXArray([sigmaNext])
|
|
300
|
+
let originalNoised = (1 - nextSigma) * maskedBlend.sourceLatents + nextSigma * freshNoise
|
|
301
|
+
packedLatents = (1 - maskedBlend.maskLatents) * originalNoised + maskedBlend.maskLatents * packedLatents
|
|
302
|
+
eval(packedLatents)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
onProgress?(stepIndex + 1, effectiveSteps)
|
|
306
|
+
|
|
307
|
+
if (stepIndex + 1) % 5 == 0 {
|
|
308
|
+
MLX.Memory.clearCache()
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
self.transformer = nil
|
|
313
|
+
MLX.Memory.clearCache()
|
|
314
|
+
|
|
315
|
+
onStage?("decode")
|
|
316
|
+
let vae = try loadVAE()
|
|
317
|
+
|
|
318
|
+
var patchified = LatentUtils.unpackSequenceToPatchified(packedLatents, height: height, width: width)
|
|
319
|
+
patchified = LatentUtils.denormalizeLatentsWithBatchNorm(
|
|
320
|
+
patchified,
|
|
321
|
+
runningMean: vae.batchNormRunningMean,
|
|
322
|
+
runningVar: vae.batchNormRunningVar
|
|
323
|
+
)
|
|
324
|
+
eval(patchified)
|
|
325
|
+
|
|
326
|
+
let latents = LatentUtils.unpatchifyLatents(patchified)
|
|
327
|
+
eval(latents)
|
|
328
|
+
|
|
329
|
+
let decoded = vae.decode(latents)
|
|
330
|
+
eval(decoded)
|
|
331
|
+
|
|
332
|
+
guard var image = postprocessVAEOutput(decoded) else {
|
|
333
|
+
throw Flux2Error.imageProcessingFailed("Failed to convert VAE output to CGImage")
|
|
334
|
+
}
|
|
335
|
+
if case let .imageToImage(sourceImage?, maskImage?, maskMode, _, _, _) = mode {
|
|
336
|
+
image = Self.compositePreservedSource(
|
|
337
|
+
generated: image,
|
|
338
|
+
source: sourceImage,
|
|
339
|
+
mask: maskImage,
|
|
340
|
+
maskMode: maskMode,
|
|
341
|
+
targetHeight: height,
|
|
342
|
+
targetWidth: width
|
|
343
|
+
) ?? image
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Do not cache the decoder for now. This keeps repeated generations
|
|
347
|
+
// slower, but avoids carrying VAE weights after the image is written.
|
|
348
|
+
self.vae = nil
|
|
349
|
+
MLX.Memory.clearCache()
|
|
350
|
+
return image
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private struct ImageConditioning {
|
|
354
|
+
let initialLatents: MLXArray
|
|
355
|
+
let referenceLatents: MLXArray?
|
|
356
|
+
let imageIds: MLXArray?
|
|
357
|
+
let outputSequenceLength: Int
|
|
358
|
+
let maskedBlend: MaskedBlend?
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private struct MaskedBlend {
|
|
362
|
+
let sourceLatents: MLXArray
|
|
363
|
+
let maskLatents: MLXArray
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private func encodeImageConditioning(
|
|
367
|
+
mode: FluxImageGenerationMode,
|
|
368
|
+
height: Int,
|
|
369
|
+
width: Int,
|
|
370
|
+
steps: Int,
|
|
371
|
+
sigmas: [Float]?
|
|
372
|
+
) throws -> ImageConditioning {
|
|
373
|
+
let randomNoise = LatentUtils.packPatchifiedToSequence(
|
|
374
|
+
LatentUtils.generatePatchifiedLatents(height: height, width: width)
|
|
375
|
+
)
|
|
376
|
+
eval(randomNoise)
|
|
377
|
+
|
|
378
|
+
guard case let .imageToImage(sourceImage, maskImage, maskMode, referenceImages, referenceInfluences, strength) = mode else {
|
|
379
|
+
scheduler.setTimesteps(numInferenceSteps: steps, imageSeqLen: randomNoise.shape[1], strength: 1.0)
|
|
380
|
+
if let sigmas { scheduler.setCustomSigmas(sigmas) }
|
|
381
|
+
return ImageConditioning(
|
|
382
|
+
initialLatents: randomNoise,
|
|
383
|
+
referenceLatents: nil,
|
|
384
|
+
imageIds: nil,
|
|
385
|
+
outputSequenceLength: randomNoise.shape[1],
|
|
386
|
+
maskedBlend: nil
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
guard sourceImage != nil || !referenceImages.isEmpty else {
|
|
391
|
+
throw Flux2Error.invalidConfiguration("Image-to-image mode requires a source image or references")
|
|
392
|
+
}
|
|
393
|
+
guard referenceImages.count <= 3 else {
|
|
394
|
+
throw Flux2Error.invalidConfiguration("Provide at most 3 reference images")
|
|
395
|
+
}
|
|
396
|
+
if let referenceInfluences, referenceInfluences.count != referenceImages.count {
|
|
397
|
+
throw Flux2Error.invalidConfiguration("referenceInfluences count must match reference image count")
|
|
398
|
+
}
|
|
399
|
+
if maskImage != nil, sourceImage == nil {
|
|
400
|
+
throw Flux2Error.invalidConfiguration("Masked image generation requires a source image")
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
scheduler.setTimesteps(numInferenceSteps: steps, imageSeqLen: randomNoise.shape[1], strength: strength)
|
|
404
|
+
if let sigmas { scheduler.setCustomSigmas(sigmas) }
|
|
405
|
+
let vae = try loadVAE()
|
|
406
|
+
let sourcePacked: MLXArray?
|
|
407
|
+
if let sourceImage, strength < 1.0 || maskImage != nil {
|
|
408
|
+
let sourceProcessed = preprocessImageForVAE(sourceImage, targetHeight: height, targetWidth: width)
|
|
409
|
+
let sourceRaw = vae.encode(sourceProcessed, samplePosterior: false)
|
|
410
|
+
var sourcePatchified = LatentUtils.packLatentsToPatchified(sourceRaw)
|
|
411
|
+
sourcePatchified = LatentUtils.normalizeLatentsWithBatchNorm(
|
|
412
|
+
sourcePatchified,
|
|
413
|
+
runningMean: vae.batchNormRunningMean,
|
|
414
|
+
runningVar: vae.batchNormRunningVar
|
|
415
|
+
)
|
|
416
|
+
let packed = LatentUtils.packPatchifiedToSequence(sourcePatchified)
|
|
417
|
+
eval(packed)
|
|
418
|
+
sourcePacked = packed
|
|
419
|
+
} else {
|
|
420
|
+
sourcePacked = nil
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let maskedBlend: MaskedBlend?
|
|
424
|
+
let maskLatents: MLXArray?
|
|
425
|
+
if let sourcePacked, let maskImage {
|
|
426
|
+
var packedMask = Self.packMaskForLatentBlending(maskImage, targetHeight: height, targetWidth: width)
|
|
427
|
+
if maskMode == .preserve {
|
|
428
|
+
packedMask = 1 - packedMask
|
|
429
|
+
}
|
|
430
|
+
eval(packedMask)
|
|
431
|
+
maskLatents = packedMask
|
|
432
|
+
maskedBlend = MaskedBlend(sourceLatents: sourcePacked, maskLatents: packedMask)
|
|
433
|
+
} else {
|
|
434
|
+
maskLatents = nil
|
|
435
|
+
maskedBlend = nil
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let initialLatents: MLXArray
|
|
439
|
+
if let sourcePacked, let maskLatents {
|
|
440
|
+
let sourceNoised = scheduler.scaleNoise(
|
|
441
|
+
sample: sourcePacked,
|
|
442
|
+
sigma: scheduler.initialSigma,
|
|
443
|
+
noise: randomNoise
|
|
444
|
+
)
|
|
445
|
+
initialLatents = (1 - maskLatents) * sourceNoised + maskLatents * randomNoise
|
|
446
|
+
} else if let sourcePacked, strength < 1.0 {
|
|
447
|
+
initialLatents = scheduler.scaleNoise(
|
|
448
|
+
sample: sourcePacked,
|
|
449
|
+
sigma: scheduler.initialSigma,
|
|
450
|
+
noise: randomNoise
|
|
451
|
+
)
|
|
452
|
+
} else {
|
|
453
|
+
initialLatents = randomNoise
|
|
454
|
+
}
|
|
455
|
+
eval(initialLatents)
|
|
456
|
+
|
|
457
|
+
let encodedReferences = try encodeReferenceImages(referenceImages, influences: referenceInfluences, vae: vae)
|
|
458
|
+
self.vae = nil
|
|
459
|
+
MLX.Memory.clearCache()
|
|
460
|
+
|
|
461
|
+
let imageIds: MLXArray?
|
|
462
|
+
if let referencePositionIds = encodedReferences.positionIds {
|
|
463
|
+
let outputImageIds = LatentUtils.generateImagePositionIDs(height: height, width: width)
|
|
464
|
+
imageIds = concatenated([outputImageIds, referencePositionIds], axis: 0)
|
|
465
|
+
} else {
|
|
466
|
+
imageIds = nil
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return ImageConditioning(
|
|
470
|
+
initialLatents: initialLatents,
|
|
471
|
+
referenceLatents: encodedReferences.latents,
|
|
472
|
+
imageIds: imageIds,
|
|
473
|
+
outputSequenceLength: randomNoise.shape[1],
|
|
474
|
+
maskedBlend: maskedBlend
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private static func packMaskForLatentBlending(
|
|
479
|
+
_ mask: CGImage,
|
|
480
|
+
targetHeight: Int,
|
|
481
|
+
targetWidth: Int
|
|
482
|
+
) -> MLXArray {
|
|
483
|
+
let latentHeight = targetHeight / 16
|
|
484
|
+
let latentWidth = targetWidth / 16
|
|
485
|
+
var pixels = [UInt8](repeating: 0, count: latentHeight * latentWidth)
|
|
486
|
+
|
|
487
|
+
guard let context = CGContext(
|
|
488
|
+
data: &pixels,
|
|
489
|
+
width: latentWidth,
|
|
490
|
+
height: latentHeight,
|
|
491
|
+
bitsPerComponent: 8,
|
|
492
|
+
bytesPerRow: latentWidth,
|
|
493
|
+
space: CGColorSpaceCreateDeviceGray(),
|
|
494
|
+
bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
495
|
+
) else {
|
|
496
|
+
return MLXArray.zeros([1, latentHeight * latentWidth, 1])
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
context.interpolationQuality = .high
|
|
500
|
+
context.draw(mask, in: CGRect(x: 0, y: 0, width: latentWidth, height: latentHeight))
|
|
501
|
+
let values = pixels.map { Float($0) / 255 }
|
|
502
|
+
return MLXArray(values).reshaped([1, latentHeight * latentWidth, 1])
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private static func compositePreservedSource(
|
|
506
|
+
generated: CGImage,
|
|
507
|
+
source: CGImage,
|
|
508
|
+
mask: CGImage,
|
|
509
|
+
maskMode: FluxImageMaskMode,
|
|
510
|
+
targetHeight: Int,
|
|
511
|
+
targetWidth: Int
|
|
512
|
+
) -> CGImage? {
|
|
513
|
+
let bytesPerPixel = 4
|
|
514
|
+
let bytesPerRow = bytesPerPixel * targetWidth
|
|
515
|
+
var generatedPixels = [UInt8](repeating: 0, count: targetHeight * bytesPerRow)
|
|
516
|
+
var sourcePixels = [UInt8](repeating: 0, count: targetHeight * bytesPerRow)
|
|
517
|
+
var maskPixels = [UInt8](repeating: 0, count: targetHeight * targetWidth)
|
|
518
|
+
|
|
519
|
+
guard let generatedContext = CGContext(
|
|
520
|
+
data: &generatedPixels,
|
|
521
|
+
width: targetWidth,
|
|
522
|
+
height: targetHeight,
|
|
523
|
+
bitsPerComponent: 8,
|
|
524
|
+
bytesPerRow: bytesPerRow,
|
|
525
|
+
space: CGColorSpaceCreateDeviceRGB(),
|
|
526
|
+
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
|
|
527
|
+
), let sourceContext = CGContext(
|
|
528
|
+
data: &sourcePixels,
|
|
529
|
+
width: targetWidth,
|
|
530
|
+
height: targetHeight,
|
|
531
|
+
bitsPerComponent: 8,
|
|
532
|
+
bytesPerRow: bytesPerRow,
|
|
533
|
+
space: CGColorSpaceCreateDeviceRGB(),
|
|
534
|
+
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
|
|
535
|
+
), let maskContext = CGContext(
|
|
536
|
+
data: &maskPixels,
|
|
537
|
+
width: targetWidth,
|
|
538
|
+
height: targetHeight,
|
|
539
|
+
bitsPerComponent: 8,
|
|
540
|
+
bytesPerRow: targetWidth,
|
|
541
|
+
space: CGColorSpaceCreateDeviceGray(),
|
|
542
|
+
bitmapInfo: CGImageAlphaInfo.none.rawValue
|
|
543
|
+
) else {
|
|
544
|
+
return nil
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let targetRect = CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight)
|
|
548
|
+
generatedContext.interpolationQuality = .high
|
|
549
|
+
generatedContext.draw(generated, in: targetRect)
|
|
550
|
+
sourceContext.interpolationQuality = .high
|
|
551
|
+
sourceContext.draw(source, in: targetRect)
|
|
552
|
+
maskContext.interpolationQuality = .high
|
|
553
|
+
maskContext.draw(mask, in: targetRect)
|
|
554
|
+
|
|
555
|
+
for index in 0..<(targetHeight * targetWidth) {
|
|
556
|
+
let maskValue = maskPixels[index]
|
|
557
|
+
let shouldPreserve = maskMode == .paint ? maskValue < 128 : maskValue >= 128
|
|
558
|
+
guard shouldPreserve else { continue }
|
|
559
|
+
let byteIndex = index * bytesPerPixel
|
|
560
|
+
generatedPixels[byteIndex] = sourcePixels[byteIndex]
|
|
561
|
+
generatedPixels[byteIndex + 1] = sourcePixels[byteIndex + 1]
|
|
562
|
+
generatedPixels[byteIndex + 2] = sourcePixels[byteIndex + 2]
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return generatedContext.makeImage()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private func encodeReferenceImages(
|
|
569
|
+
_ images: [CGImage],
|
|
570
|
+
influences: [Float]?,
|
|
571
|
+
vae: AutoencoderKLFlux2
|
|
572
|
+
) throws -> (latents: MLXArray?, positionIds: MLXArray?) {
|
|
573
|
+
guard !images.isEmpty else {
|
|
574
|
+
return (nil, nil)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
var allLatents: [MLXArray] = []
|
|
578
|
+
var latentHeights: [Int] = []
|
|
579
|
+
var latentWidths: [Int] = []
|
|
580
|
+
|
|
581
|
+
for (index, image) in images.enumerated() {
|
|
582
|
+
let size = Self.resolveReferenceImageDimensions(image)
|
|
583
|
+
let processed = preprocessImageForVAE(image, targetHeight: size.height, targetWidth: size.width)
|
|
584
|
+
let raw = vae.encode(processed, samplePosterior: false)
|
|
585
|
+
var patchified = LatentUtils.packLatentsToPatchified(raw)
|
|
586
|
+
patchified = LatentUtils.normalizeLatentsWithBatchNorm(
|
|
587
|
+
patchified,
|
|
588
|
+
runningMean: vae.batchNormRunningMean,
|
|
589
|
+
runningVar: vae.batchNormRunningVar
|
|
590
|
+
)
|
|
591
|
+
eval(patchified)
|
|
592
|
+
|
|
593
|
+
var packed = LatentUtils.packPatchifiedToSequence(patchified).squeezed(axis: 0)
|
|
594
|
+
if let influence = influences?[index] {
|
|
595
|
+
packed = packed * influence
|
|
596
|
+
eval(packed)
|
|
597
|
+
}
|
|
598
|
+
allLatents.append(packed)
|
|
599
|
+
latentHeights.append(size.height / 16)
|
|
600
|
+
latentWidths.append(size.width / 16)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
let latents = concatenated(allLatents, axis: 0).expandedDimensions(axis: 0)
|
|
604
|
+
let positionIds = LatentUtils.generateReferenceImagePositionIDs(
|
|
605
|
+
latentHeights: latentHeights,
|
|
606
|
+
latentWidths: latentWidths,
|
|
607
|
+
scale: 10
|
|
608
|
+
)
|
|
609
|
+
return (latents, positionIds)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private static func resolveReferenceImageDimensions(_ image: CGImage) -> (height: Int, width: Int) {
|
|
613
|
+
let factor = 16
|
|
614
|
+
let longSide = max(image.height, image.width)
|
|
615
|
+
let scale = min(1.0, CGFloat(referenceImageMaxLongSide) / CGFloat(longSide))
|
|
616
|
+
|
|
617
|
+
func alignedDown(_ value: CGFloat) -> Int {
|
|
618
|
+
max(factor, Int(value) / factor * factor)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return LatentUtils.validateDimensions(
|
|
622
|
+
height: alignedDown(CGFloat(image.height) * scale),
|
|
623
|
+
width: alignedDown(CGFloat(image.width) * scale)
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private func preprocessImageForVAE(_ image: CGImage, targetHeight: Int, targetWidth: Int) -> MLXArray {
|
|
628
|
+
let bytesPerPixel = 4
|
|
629
|
+
let bytesPerRow = bytesPerPixel * targetWidth
|
|
630
|
+
var pixelData = [UInt8](repeating: 0, count: targetHeight * bytesPerRow)
|
|
631
|
+
|
|
632
|
+
guard let context = CGContext(
|
|
633
|
+
data: &pixelData,
|
|
634
|
+
width: targetWidth,
|
|
635
|
+
height: targetHeight,
|
|
636
|
+
bitsPerComponent: 8,
|
|
637
|
+
bytesPerRow: bytesPerRow,
|
|
638
|
+
space: CGColorSpaceCreateDeviceRGB(),
|
|
639
|
+
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue
|
|
640
|
+
) else {
|
|
641
|
+
return MLXRandom.normal([1, 3, targetHeight, targetWidth])
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
context.interpolationQuality = .high
|
|
645
|
+
context.draw(image, in: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight))
|
|
646
|
+
|
|
647
|
+
var floatData = [Float](repeating: 0, count: targetHeight * targetWidth * 3)
|
|
648
|
+
for y in 0..<targetHeight {
|
|
649
|
+
for x in 0..<targetWidth {
|
|
650
|
+
let pixelIndex = y * targetWidth + x
|
|
651
|
+
let byteIndex = y * bytesPerRow + x * bytesPerPixel
|
|
652
|
+
floatData[pixelIndex] = Float(pixelData[byteIndex]) / 127.5 - 1.0
|
|
653
|
+
floatData[targetHeight * targetWidth + pixelIndex] = Float(pixelData[byteIndex + 1]) / 127.5 - 1.0
|
|
654
|
+
floatData[2 * targetHeight * targetWidth + pixelIndex] = Float(pixelData[byteIndex + 2]) / 127.5 - 1.0
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return MLXArray(floatData).reshaped([1, 3, targetHeight, targetWidth])
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private func validateTextEmbeddings(_ textEmbeddings: MLXArray) throws {
|
|
662
|
+
guard textEmbeddings.shape.count == 3 else {
|
|
663
|
+
throw Flux2Error.invalidConfiguration("Expected text embeddings rank 3, got shape \(textEmbeddings.shape)")
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
guard textEmbeddings.shape[0] == 1 else {
|
|
667
|
+
throw Flux2Error.invalidConfiguration("Only batch size 1 is supported for image generation")
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
guard textEmbeddings.shape[2] == Flux2Model.klein4B.jointAttentionDim else {
|
|
671
|
+
throw Flux2Error.invalidConfiguration(
|
|
672
|
+
"Expected Klein 4B text embedding width \(Flux2Model.klein4B.jointAttentionDim), got \(textEmbeddings.shape[2])"
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private func applyLoRAs(_ loras: [FluxLoRAConfig], to model: Flux2Transformer2DModel) throws {
|
|
678
|
+
guard !loras.isEmpty else { return }
|
|
679
|
+
var linearModules: [String: TransformerLinearTarget] = [:]
|
|
680
|
+
for (path, module) in model.leafModules().flattened() {
|
|
681
|
+
if let quantizedLinear = module as? QuantizedLinear {
|
|
682
|
+
guard let dims = Self.quantizedLinearDims(quantizedLinear) else { continue }
|
|
683
|
+
linearModules[path] = TransformerLinearTarget(module: quantizedLinear, inputFeatures: dims.in, outputFeatures: dims.out)
|
|
684
|
+
} else if let linear = module as? Linear {
|
|
685
|
+
guard let dims = Self.linearDims(weightShape: linear.weight.shape) else { continue }
|
|
686
|
+
linearModules[path] = TransformerLinearTarget(module: linear, inputFeatures: dims.in, outputFeatures: dims.out)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
var contributionsByTarget: [String: [LoRAContribution]] = [:]
|
|
691
|
+
var loadedLayerCount = 0
|
|
692
|
+
var matchedLayers = Set<String>()
|
|
693
|
+
var dimensionMismatchCount = 0
|
|
694
|
+
var sampleDimensionMismatches: [LoRADimensionMismatch] = []
|
|
695
|
+
var sampleUnmatchedLoRALayers: [String] = []
|
|
696
|
+
var fileReports: [LoRAFileApplyReport] = []
|
|
697
|
+
for lora in loras {
|
|
698
|
+
let name = lora.id ?? URL(fileURLWithPath: lora.path).deletingPathExtension().lastPathComponent
|
|
699
|
+
let tensors = try loadArrays(url: URL(fileURLWithPath: lora.path))
|
|
700
|
+
let groups = Self.groupLoRATensors(tensors)
|
|
701
|
+
let diagnostics = Self.loraDiagnostics(filePath: lora.path, tensors: tensors, groups: groups)
|
|
702
|
+
Self.logLoRADiagnostics(diagnostics)
|
|
703
|
+
loadedLayerCount += groups.count
|
|
704
|
+
var fileMatchedLayers = Set<String>()
|
|
705
|
+
var fileAppliedLayers = Set<String>()
|
|
706
|
+
var fileMismatchLayers = Set<String>()
|
|
707
|
+
var fileMismatchSamples: [LoRADimensionMismatch] = []
|
|
708
|
+
var fileUnmatchedLayers: [String] = []
|
|
709
|
+
var applied = 0
|
|
710
|
+
for (target, group) in groups {
|
|
711
|
+
guard let down = group.down, let up = group.up else { continue }
|
|
712
|
+
guard let linear = linearModules[target] else {
|
|
713
|
+
if fileUnmatchedLayers.count < 8 { fileUnmatchedLayers.append(target) }
|
|
714
|
+
if sampleUnmatchedLoRALayers.count < 8 { sampleUnmatchedLoRALayers.append(target) }
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
fileMatchedLayers.insert(target)
|
|
718
|
+
matchedLayers.insert(target)
|
|
719
|
+
guard let normalized = Self.normalizedLoRAPair(
|
|
720
|
+
down: down,
|
|
721
|
+
up: up,
|
|
722
|
+
inputFeatures: linear.inputFeatures,
|
|
723
|
+
outputFeatures: linear.outputFeatures
|
|
724
|
+
) else {
|
|
725
|
+
let mismatch = LoRADimensionMismatch(
|
|
726
|
+
transformerLayer: target,
|
|
727
|
+
loraLayer: target,
|
|
728
|
+
loraName: name,
|
|
729
|
+
expectedInFeatures: linear.inputFeatures,
|
|
730
|
+
expectedOutFeatures: linear.outputFeatures,
|
|
731
|
+
downShape: down.shape,
|
|
732
|
+
upShape: up.shape
|
|
733
|
+
)
|
|
734
|
+
dimensionMismatchCount += 1
|
|
735
|
+
fileMismatchLayers.insert(target)
|
|
736
|
+
if sampleDimensionMismatches.count < 8 { sampleDimensionMismatches.append(mismatch) }
|
|
737
|
+
if fileMismatchSamples.count < 4 { fileMismatchSamples.append(mismatch) }
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
740
|
+
let rank = max(1, down.shape.first ?? 1)
|
|
741
|
+
let alphaScale = group.alpha.map { Float($0.item(Float.self)) / Float(rank) } ?? 1
|
|
742
|
+
contributionsByTarget[target, default: []].append(LoRAContribution(
|
|
743
|
+
down: normalized.down,
|
|
744
|
+
up: normalized.up,
|
|
745
|
+
scale: lora.scale * alphaScale
|
|
746
|
+
))
|
|
747
|
+
fileAppliedLayers.insert(target)
|
|
748
|
+
applied += 1
|
|
749
|
+
}
|
|
750
|
+
Flux2Debug.log("Loaded LoRA \"\(name)\": layers=\(diagnostics.layerCount), rank=\(diagnostics.rank), target=\(diagnostics.targetModel)")
|
|
751
|
+
fileReports.append(LoRAFileApplyReport(
|
|
752
|
+
name: name,
|
|
753
|
+
loadedLayerCount: groups.count,
|
|
754
|
+
matchedLayerCount: fileMatchedLayers.count,
|
|
755
|
+
appliedLayerCount: applied,
|
|
756
|
+
dimensionMismatchCount: fileMismatchLayers.count,
|
|
757
|
+
sampleMatchedLayers: Array(fileMatchedLayers.sorted().prefix(8)),
|
|
758
|
+
sampleUnmatchedLayers: fileUnmatchedLayers,
|
|
759
|
+
sampleDimensionMismatches: fileMismatchSamples
|
|
760
|
+
))
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
var moduleUpdates: [String: Module] = [:]
|
|
764
|
+
for target in contributionsByTarget.keys.sorted() {
|
|
765
|
+
guard let contributions = contributionsByTarget[target] else { continue }
|
|
766
|
+
guard let linear = linearModules[target], !contributions.isEmpty else { continue }
|
|
767
|
+
var combinedDown: MLXArray?
|
|
768
|
+
var combinedUp: MLXArray?
|
|
769
|
+
for contribution in contributions {
|
|
770
|
+
let scaledDown = contribution.down * contribution.scale
|
|
771
|
+
if let existingDown = combinedDown, let existingUp = combinedUp {
|
|
772
|
+
combinedDown = concatenated([existingDown, scaledDown], axis: 0)
|
|
773
|
+
combinedUp = concatenated([existingUp, contribution.up], axis: 1)
|
|
774
|
+
} else {
|
|
775
|
+
combinedDown = scaledDown
|
|
776
|
+
combinedUp = contribution.up
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
guard let combinedDown, let combinedUp else { continue }
|
|
780
|
+
let materializedDown = combinedDown.asType(.float16)
|
|
781
|
+
let materializedUp = combinedUp.asType(.float16)
|
|
782
|
+
eval(materializedDown, materializedUp)
|
|
783
|
+
if let quantized = linear.module as? QuantizedLinear {
|
|
784
|
+
moduleUpdates[target] = LoRAQuantizedLinear(
|
|
785
|
+
base: quantized,
|
|
786
|
+
down: materializedDown,
|
|
787
|
+
up: materializedUp,
|
|
788
|
+
scale: 1,
|
|
789
|
+
debugName: target
|
|
790
|
+
)
|
|
791
|
+
} else if let dense = linear.module as? Linear {
|
|
792
|
+
moduleUpdates[target] = LoRALinear(
|
|
793
|
+
base: dense,
|
|
794
|
+
down: materializedDown,
|
|
795
|
+
up: materializedUp,
|
|
796
|
+
scale: 1,
|
|
797
|
+
debugName: target
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
contributionsByTarget.removeAll()
|
|
802
|
+
MLX.Memory.clearCache()
|
|
803
|
+
|
|
804
|
+
let report = LoRAApplyReport(
|
|
805
|
+
loadedLayerCount: loadedLayerCount,
|
|
806
|
+
transformerLinearLayerCount: linearModules.count,
|
|
807
|
+
matchedLayerCount: matchedLayers.count,
|
|
808
|
+
replacedLayerCount: moduleUpdates.count,
|
|
809
|
+
updatedLayerCount: 0,
|
|
810
|
+
dimensionMismatchCount: dimensionMismatchCount,
|
|
811
|
+
sampleDimensionMismatches: sampleDimensionMismatches,
|
|
812
|
+
sampleUnmatchedLoRALayers: sampleUnmatchedLoRALayers,
|
|
813
|
+
sampleTransformerLayers: Array(linearModules.keys.sorted().prefix(8)),
|
|
814
|
+
loraReports: fileReports
|
|
815
|
+
)
|
|
816
|
+
Self.logLoRAApplyReport(report, queuedCount: loras.count)
|
|
817
|
+
|
|
818
|
+
guard !moduleUpdates.isEmpty else {
|
|
819
|
+
let sample = report.sampleDimensionMismatches.first?.logDescription
|
|
820
|
+
?? report.sampleUnmatchedLoRALayers.first
|
|
821
|
+
?? "none"
|
|
822
|
+
throw Flux2Error.invalidConfiguration(
|
|
823
|
+
"No compatible LoRA targets found (loaded=\(report.loadedLayerCount), matched=\(report.matchedLayerCount), dimensionMismatches=\(report.dimensionMismatchCount), sample=\(sample))"
|
|
824
|
+
)
|
|
825
|
+
}
|
|
826
|
+
model.update(modules: ModuleChildren.unflattened(moduleUpdates))
|
|
827
|
+
eval(model.parameters())
|
|
828
|
+
MLX.Memory.clearCache()
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private struct LoRATensorGroup {
|
|
832
|
+
var down: MLXArray?
|
|
833
|
+
var up: MLXArray?
|
|
834
|
+
var alpha: MLXArray?
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private static func linearDims(weightShape: [Int]) -> (out: Int, in: Int)? {
|
|
838
|
+
guard weightShape.count >= 2 else { return nil }
|
|
839
|
+
return (out: weightShape[weightShape.count - 2], in: weightShape[weightShape.count - 1])
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private static func quantizedLinearDims(_ linear: QuantizedLinear) -> (out: Int, in: Int)? {
|
|
843
|
+
let weightShape = linear.weight.shape
|
|
844
|
+
guard weightShape.count >= 2, linear.bits > 0, 32 % linear.bits == 0 else { return nil }
|
|
845
|
+
let out = weightShape[weightShape.count - 2]
|
|
846
|
+
let packedIn = weightShape[weightShape.count - 1]
|
|
847
|
+
return (out: out, in: packedIn * (32 / linear.bits))
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private static func normalizedLoRAPair(
|
|
851
|
+
down: MLXArray,
|
|
852
|
+
up: MLXArray,
|
|
853
|
+
inputFeatures: Int,
|
|
854
|
+
outputFeatures: Int
|
|
855
|
+
) -> (down: MLXArray, up: MLXArray)? {
|
|
856
|
+
guard down.shape.count == 2, up.shape.count == 2 else { return nil }
|
|
857
|
+
if down.shape[1] == inputFeatures,
|
|
858
|
+
up.shape[0] == outputFeatures,
|
|
859
|
+
down.shape[0] == up.shape[1] {
|
|
860
|
+
return (down, up)
|
|
861
|
+
}
|
|
862
|
+
if down.shape[0] == inputFeatures,
|
|
863
|
+
up.shape[1] == outputFeatures,
|
|
864
|
+
down.shape[1] == up.shape[0] {
|
|
865
|
+
return (down.transposed(1, 0), up.transposed(1, 0))
|
|
866
|
+
}
|
|
867
|
+
return nil
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private static func loraDiagnostics(
|
|
871
|
+
filePath: String,
|
|
872
|
+
tensors: [String: MLXArray],
|
|
873
|
+
groups: [String: LoRATensorGroup]
|
|
874
|
+
) -> LoRADiagnostics {
|
|
875
|
+
var sampleTensors: [LoRATensorDiagnostic] = []
|
|
876
|
+
var nonFiniteTensorCount = 0
|
|
877
|
+
var nonFiniteValueCount = 0
|
|
878
|
+
let visibleKeys = tensors.keys.filter { !$0.hasPrefix("__") }.sorted()
|
|
879
|
+
for key in visibleKeys.prefix(3) {
|
|
880
|
+
guard let value = tensors[key] else { continue }
|
|
881
|
+
let diagnostic = tensorDiagnostic(key: key, value: value)
|
|
882
|
+
if diagnostic.nonFiniteCount > 0 {
|
|
883
|
+
nonFiniteTensorCount += 1
|
|
884
|
+
nonFiniteValueCount += diagnostic.nonFiniteCount
|
|
885
|
+
}
|
|
886
|
+
sampleTensors.append(diagnostic)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return LoRADiagnostics(
|
|
890
|
+
fileName: URL(fileURLWithPath: filePath).lastPathComponent,
|
|
891
|
+
tensorCount: visibleKeys.count,
|
|
892
|
+
layerCount: groups.count,
|
|
893
|
+
rank: groups.values.compactMap { $0.down?.shape.first }.first ?? 0,
|
|
894
|
+
targetModel: detectLoRATargetModel(Array(groups.keys)),
|
|
895
|
+
sampleTensors: sampleTensors,
|
|
896
|
+
nonFiniteTensorCount: nonFiniteTensorCount,
|
|
897
|
+
nonFiniteValueCount: nonFiniteValueCount
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private static func tensorDiagnostic(key: String, value: MLXArray) -> LoRATensorDiagnostic {
|
|
902
|
+
let checked = value.asType(.float32)
|
|
903
|
+
eval(checked)
|
|
904
|
+
let values = checked.asArray(Float.self)
|
|
905
|
+
var minValue = Float.greatestFiniteMagnitude
|
|
906
|
+
var maxValue = -Float.greatestFiniteMagnitude
|
|
907
|
+
var sum: Float = 0
|
|
908
|
+
var finiteCount = 0
|
|
909
|
+
var nonFiniteCount = 0
|
|
910
|
+
|
|
911
|
+
for value in values {
|
|
912
|
+
guard value.isFinite else {
|
|
913
|
+
nonFiniteCount += 1
|
|
914
|
+
continue
|
|
915
|
+
}
|
|
916
|
+
minValue = min(minValue, value)
|
|
917
|
+
maxValue = max(maxValue, value)
|
|
918
|
+
sum += value
|
|
919
|
+
finiteCount += 1
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if finiteCount == 0 {
|
|
923
|
+
minValue = 0
|
|
924
|
+
maxValue = 0
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return LoRATensorDiagnostic(
|
|
928
|
+
key: key,
|
|
929
|
+
shape: value.shape,
|
|
930
|
+
min: minValue,
|
|
931
|
+
max: maxValue,
|
|
932
|
+
mean: finiteCount > 0 ? sum / Float(finiteCount) : 0,
|
|
933
|
+
nonFiniteCount: nonFiniteCount
|
|
934
|
+
)
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private static func detectLoRATargetModel(_ layers: [String]) -> String {
|
|
938
|
+
var doubleBlocks = Set<Int>()
|
|
939
|
+
var singleBlocks = Set<Int>()
|
|
940
|
+
for path in layers {
|
|
941
|
+
if path.hasPrefix("transformerBlocks.") {
|
|
942
|
+
doubleBlocks.insert(extractBlockIndex(from: path, prefix: "transformerBlocks."))
|
|
943
|
+
} else if path.hasPrefix("singleTransformerBlocks.") {
|
|
944
|
+
singleBlocks.insert(extractBlockIndex(from: path, prefix: "singleTransformerBlocks."))
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
let maxDouble = doubleBlocks.max() ?? 0
|
|
949
|
+
let maxSingle = singleBlocks.max() ?? 0
|
|
950
|
+
if maxDouble == 4 && maxSingle == 19 { return "klein-4b" }
|
|
951
|
+
if maxDouble == 7 && maxSingle == 23 { return "klein-9b" }
|
|
952
|
+
if maxDouble == 7 && maxSingle == 47 { return "dev" }
|
|
953
|
+
return "unknown"
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private static func extractBlockIndex(from path: String, prefix: String) -> Int {
|
|
957
|
+
guard let range = path.range(of: prefix) else { return 0 }
|
|
958
|
+
let afterPrefix = path[range.upperBound...]
|
|
959
|
+
let index = afterPrefix.prefix(while: { $0.isNumber })
|
|
960
|
+
return Int(index) ?? 0
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
private static func logLoRADiagnostics(_ diagnostics: LoRADiagnostics) {
|
|
964
|
+
Flux2Debug.log(
|
|
965
|
+
"LoRA diagnostics file=\(diagnostics.fileName) tensors=\(diagnostics.tensorCount) layers=\(diagnostics.layerCount) rank=\(diagnostics.rank) target=\(diagnostics.targetModel) nonFiniteTensors=\(diagnostics.nonFiniteTensorCount) nonFiniteValues=\(diagnostics.nonFiniteValueCount)"
|
|
966
|
+
)
|
|
967
|
+
for tensor in diagnostics.sampleTensors {
|
|
968
|
+
Flux2Debug.log(
|
|
969
|
+
"LoRA tensor sample key=\(tensor.key) shape=\(tensor.shape) min=\(tensor.min) max=\(tensor.max) mean=\(tensor.mean) nonFinite=\(tensor.nonFiniteCount)"
|
|
970
|
+
)
|
|
971
|
+
}
|
|
972
|
+
if diagnostics.nonFiniteValueCount > 0 {
|
|
973
|
+
Flux2Debug.warning("LoRA contains non-finite values: file=\(diagnostics.fileName), values=\(diagnostics.nonFiniteValueCount)")
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private static func logLoRAApplyReport(_ report: LoRAApplyReport, queuedCount: Int) {
|
|
978
|
+
Flux2Debug.log(
|
|
979
|
+
"Applied \(queuedCount) queued LoRA files dynamically to transformer: loadedLayers=\(report.loadedLayerCount), transformerLinearLayers=\(report.transformerLinearLayerCount), matchedLayers=\(report.matchedLayerCount), appliedLayers=\(report.appliedLayerCount), dimensionMismatches=\(report.dimensionMismatchCount)"
|
|
980
|
+
)
|
|
981
|
+
for loraReport in report.loraReports {
|
|
982
|
+
if loraReport.appliedLayerCount > 0 {
|
|
983
|
+
Flux2Debug.log(
|
|
984
|
+
"LoRA \"\(loraReport.name)\" used: loaded=\(loraReport.loadedLayerCount), matched=\(loraReport.matchedLayerCount), applied=\(loraReport.appliedLayerCount), mismatches=\(loraReport.dimensionMismatchCount)"
|
|
985
|
+
)
|
|
986
|
+
} else {
|
|
987
|
+
Flux2Debug.warning(
|
|
988
|
+
"LoRA \"\(loraReport.name)\" loaded but not used: loaded=\(loraReport.loadedLayerCount), matched=\(loraReport.matchedLayerCount), applied=0, mismatches=\(loraReport.dimensionMismatchCount)"
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if loraReport.matchedLayerCount < loraReport.loadedLayerCount, !loraReport.sampleUnmatchedLayers.isEmpty {
|
|
993
|
+
Flux2Debug.warning("LoRA \"\(loraReport.name)\" sample unmatched layers: \(loraReport.sampleUnmatchedLayers.joined(separator: ", "))")
|
|
994
|
+
}
|
|
995
|
+
for mismatch in loraReport.sampleDimensionMismatches {
|
|
996
|
+
Flux2Debug.warning("LoRA \"\(loraReport.name)\" dimension mismatch: \(mismatch.logDescription)")
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if report.appliedLayerCount == 0 {
|
|
1001
|
+
for mismatch in report.sampleDimensionMismatches {
|
|
1002
|
+
Flux2Debug.warning("Flux LoRA dimension mismatch: \(mismatch.logDescription)")
|
|
1003
|
+
}
|
|
1004
|
+
Flux2Debug.warning("No Flux LoRA layers were applied. Sample unmatched LoRA layers: \(report.sampleUnmatchedLoRALayers.joined(separator: ", "))")
|
|
1005
|
+
Flux2Debug.warning("Sample transformer linear layers: \(report.sampleTransformerLayers.joined(separator: ", "))")
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private static func groupLoRATensors(_ tensors: [String: MLXArray]) -> [String: LoRATensorGroup] {
|
|
1010
|
+
var rawGroups: [String: LoRATensorGroup] = [:]
|
|
1011
|
+
for (rawKey, value) in tensors {
|
|
1012
|
+
guard let parsed = parseLoRAKey(rawKey) else { continue }
|
|
1013
|
+
var group = rawGroups[parsed.target] ?? LoRATensorGroup()
|
|
1014
|
+
switch parsed.kind {
|
|
1015
|
+
case "down": group.down = value
|
|
1016
|
+
case "up": group.up = value
|
|
1017
|
+
case "alpha": group.alpha = value
|
|
1018
|
+
default: break
|
|
1019
|
+
}
|
|
1020
|
+
rawGroups[parsed.target] = group
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
var groups: [String: LoRATensorGroup] = [:]
|
|
1024
|
+
for (rawTarget, group) in rawGroups {
|
|
1025
|
+
if isCombinedQKVLayer(rawTarget),
|
|
1026
|
+
let up = group.up,
|
|
1027
|
+
up.shape.count == 2,
|
|
1028
|
+
up.shape[0].isMultiple(of: 3) {
|
|
1029
|
+
let outputSize = up.shape[0] / 3
|
|
1030
|
+
let targets = qkvTargets(for: rawTarget)
|
|
1031
|
+
guard targets.count == 3 else { continue }
|
|
1032
|
+
for (index, target) in targets.enumerated() {
|
|
1033
|
+
groups[target] = LoRATensorGroup(
|
|
1034
|
+
down: group.down,
|
|
1035
|
+
up: up[(index * outputSize)..<((index + 1) * outputSize), 0...],
|
|
1036
|
+
alpha: group.alpha
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
} else if let target = mapLoRATarget(rawTarget) {
|
|
1040
|
+
groups[target] = group
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return groups
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private static func parseLoRAKey(_ rawKey: String) -> (target: String, kind: String)? {
|
|
1047
|
+
var key = rawKey
|
|
1048
|
+
for prefix in ["transformer.", "diffusion_model.", "model.diffusion_model.", "base_model.model."] {
|
|
1049
|
+
if key.hasPrefix(prefix) { key.removeFirst(prefix.count) }
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
let patterns: [(String, String)] = [
|
|
1053
|
+
(".lora_down.weight", "down"),
|
|
1054
|
+
(".lora_A.weight", "down"),
|
|
1055
|
+
(".lora.up.weight", "up"),
|
|
1056
|
+
(".lora_up.weight", "up"),
|
|
1057
|
+
(".lora_B.weight", "up"),
|
|
1058
|
+
(".alpha", "alpha"),
|
|
1059
|
+
]
|
|
1060
|
+
for (suffix, kind) in patterns where key.hasSuffix(suffix) {
|
|
1061
|
+
let base = normalizeLoRATarget(String(key.dropLast(suffix.count)))
|
|
1062
|
+
return (base, kind)
|
|
1063
|
+
}
|
|
1064
|
+
return nil
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
private static func normalizeLoRATarget(_ rawTarget: String) -> String {
|
|
1068
|
+
var target = rawTarget
|
|
1069
|
+
for prefix in ["model.diffusion_model.", "base_model.model.", "diffusion_model.", "transformer."] {
|
|
1070
|
+
if target.hasPrefix(prefix) {
|
|
1071
|
+
target.removeFirst(prefix.count)
|
|
1072
|
+
break
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
let doublePrefix = "lora_unet_double_blocks_"
|
|
1077
|
+
let singlePrefix = "lora_unet_single_blocks_"
|
|
1078
|
+
if target.hasPrefix(singlePrefix) {
|
|
1079
|
+
let remainder = String(target.dropFirst(singlePrefix.count))
|
|
1080
|
+
let parts = remainder.split(separator: "_", maxSplits: 1).map(String.init)
|
|
1081
|
+
guard parts.count == 2, Int(parts[0]) != nil else { return target }
|
|
1082
|
+
switch parts[1] {
|
|
1083
|
+
case "linear1": return "single_blocks.\(parts[0]).linear1"
|
|
1084
|
+
case "linear2": return "single_blocks.\(parts[0]).linear2"
|
|
1085
|
+
default: return target
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if target.hasPrefix(doublePrefix) {
|
|
1089
|
+
let remainder = String(target.dropFirst(doublePrefix.count))
|
|
1090
|
+
let parts = remainder.split(separator: "_", maxSplits: 1).map(String.init)
|
|
1091
|
+
guard parts.count == 2, Int(parts[0]) != nil else { return target }
|
|
1092
|
+
let suffix: String
|
|
1093
|
+
switch parts[1] {
|
|
1094
|
+
case "img_attn_qkv": suffix = "img_attn.qkv"
|
|
1095
|
+
case "txt_attn_qkv": suffix = "txt_attn.qkv"
|
|
1096
|
+
case "img_attn_proj": suffix = "img_attn.proj"
|
|
1097
|
+
case "txt_attn_proj": suffix = "txt_attn.proj"
|
|
1098
|
+
case "img_mlp_0": suffix = "img_mlp.0"
|
|
1099
|
+
case "img_mlp_2": suffix = "img_mlp.2"
|
|
1100
|
+
case "txt_mlp_0": suffix = "txt_mlp.0"
|
|
1101
|
+
case "txt_mlp_2": suffix = "txt_mlp.2"
|
|
1102
|
+
default: return target
|
|
1103
|
+
}
|
|
1104
|
+
return "double_blocks.\(parts[0]).\(suffix)"
|
|
1105
|
+
}
|
|
1106
|
+
return target
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private static func isCombinedQKVLayer(_ target: String) -> Bool {
|
|
1110
|
+
target.contains(".img_attn.qkv") || target.contains(".txt_attn.qkv")
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private static func qkvTargets(for target: String) -> [String] {
|
|
1114
|
+
let index = extractBlockIndex(from: target, prefix: "double_blocks.")
|
|
1115
|
+
if target.contains(".img_attn.qkv") {
|
|
1116
|
+
return [
|
|
1117
|
+
"transformerBlocks.\(index).attn.toQ",
|
|
1118
|
+
"transformerBlocks.\(index).attn.toK",
|
|
1119
|
+
"transformerBlocks.\(index).attn.toV",
|
|
1120
|
+
]
|
|
1121
|
+
}
|
|
1122
|
+
if target.contains(".txt_attn.qkv") {
|
|
1123
|
+
return [
|
|
1124
|
+
"transformerBlocks.\(index).attn.addQProj",
|
|
1125
|
+
"transformerBlocks.\(index).attn.addKProj",
|
|
1126
|
+
"transformerBlocks.\(index).attn.addVProj",
|
|
1127
|
+
]
|
|
1128
|
+
}
|
|
1129
|
+
return []
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
private static func mapLoRATarget(_ key: String) -> String? {
|
|
1133
|
+
let direct = key
|
|
1134
|
+
.replacingOccurrences(of: ".processor", with: "")
|
|
1135
|
+
.replacingOccurrences(of: ".to_q", with: ".toQ")
|
|
1136
|
+
.replacingOccurrences(of: ".to_k", with: ".toK")
|
|
1137
|
+
.replacingOccurrences(of: ".to_v", with: ".toV")
|
|
1138
|
+
.replacingOccurrences(of: ".to_out.0", with: ".toOut")
|
|
1139
|
+
.replacingOccurrences(of: ".add_q_proj", with: ".addQProj")
|
|
1140
|
+
.replacingOccurrences(of: ".add_k_proj", with: ".addKProj")
|
|
1141
|
+
.replacingOccurrences(of: ".add_v_proj", with: ".addVProj")
|
|
1142
|
+
.replacingOccurrences(of: ".to_add_out", with: ".toAddOut")
|
|
1143
|
+
if direct.hasPrefix("transformerBlocks.") || direct.hasPrefix("singleTransformerBlocks.") || direct == "xEmbedder" || direct == "contextEmbedder" || direct == "projOut" {
|
|
1144
|
+
return direct
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if key.hasPrefix("double_blocks.") {
|
|
1148
|
+
let index = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
1149
|
+
if key.contains(".img_attn.proj") { return "transformerBlocks.\(index).attn.toOut" }
|
|
1150
|
+
if key.contains(".txt_attn.proj") { return "transformerBlocks.\(index).attn.toAddOut" }
|
|
1151
|
+
if key.contains(".img_mlp.0") { return "transformerBlocks.\(index).ff.activation.proj" }
|
|
1152
|
+
if key.contains(".img_mlp.2") { return "transformerBlocks.\(index).ff.linearOut" }
|
|
1153
|
+
if key.contains(".txt_mlp.0") { return "transformerBlocks.\(index).ffContext.activation.proj" }
|
|
1154
|
+
if key.contains(".txt_mlp.2") { return "transformerBlocks.\(index).ffContext.linearOut" }
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if key.hasPrefix("single_blocks.") {
|
|
1158
|
+
let index = extractBlockIndex(from: key, prefix: "single_blocks.")
|
|
1159
|
+
if key.contains(".linear1") { return "singleTransformerBlocks.\(index).attn.toQkvMlp" }
|
|
1160
|
+
if key.contains(".linear2") { return "singleTransformerBlocks.\(index).attn.toOut" }
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if key.hasPrefix("double_blocks.") {
|
|
1164
|
+
let parts = key.split(separator: ".")
|
|
1165
|
+
guard parts.count >= 3, let index = Int(parts[1]) else { return nil }
|
|
1166
|
+
let suffix = parts.dropFirst(2).joined(separator: ".")
|
|
1167
|
+
switch suffix {
|
|
1168
|
+
case "img_to_q": return "transformerBlocks.\(index).attn.toQ"
|
|
1169
|
+
case "img_to_k": return "transformerBlocks.\(index).attn.toK"
|
|
1170
|
+
case "img_to_v": return "transformerBlocks.\(index).attn.toV"
|
|
1171
|
+
case "img_to_out": return "transformerBlocks.\(index).attn.toOut"
|
|
1172
|
+
case "txt_to_q": return "transformerBlocks.\(index).attn.addQProj"
|
|
1173
|
+
case "txt_to_k": return "transformerBlocks.\(index).attn.addKProj"
|
|
1174
|
+
case "txt_to_v": return "transformerBlocks.\(index).attn.addVProj"
|
|
1175
|
+
case "txt_to_out": return "transformerBlocks.\(index).attn.toAddOut"
|
|
1176
|
+
case "img_mlp_in": return "transformerBlocks.\(index).ff.activation.proj"
|
|
1177
|
+
case "img_mlp_out": return "transformerBlocks.\(index).ff.linearOut"
|
|
1178
|
+
case "txt_mlp_in": return "transformerBlocks.\(index).ffContext.activation.proj"
|
|
1179
|
+
case "txt_mlp_out": return "transformerBlocks.\(index).ffContext.linearOut"
|
|
1180
|
+
default: return nil
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if key.hasPrefix("single_blocks.") {
|
|
1185
|
+
let parts = key.split(separator: ".")
|
|
1186
|
+
guard parts.count >= 3, let index = Int(parts[1]) else { return nil }
|
|
1187
|
+
let suffix = parts.dropFirst(2).joined(separator: ".")
|
|
1188
|
+
switch suffix {
|
|
1189
|
+
case "to_qkv_mlp", "linear1": return "singleTransformerBlocks.\(index).attn.toQkvMlp"
|
|
1190
|
+
case "to_out", "linear2": return "singleTransformerBlocks.\(index).attn.toOut"
|
|
1191
|
+
default: return nil
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if key.hasPrefix("transformer_blocks.") {
|
|
1196
|
+
let parts = key.split(separator: ".")
|
|
1197
|
+
guard parts.count >= 4, let index = Int(parts[1]) else { return nil }
|
|
1198
|
+
let suffix = parts.dropFirst(2).joined(separator: ".")
|
|
1199
|
+
switch suffix {
|
|
1200
|
+
case "attn.to_q": return "transformerBlocks.\(index).attn.toQ"
|
|
1201
|
+
case "attn.to_k": return "transformerBlocks.\(index).attn.toK"
|
|
1202
|
+
case "attn.to_v": return "transformerBlocks.\(index).attn.toV"
|
|
1203
|
+
case "attn.to_out.0", "attn.to_out": return "transformerBlocks.\(index).attn.toOut"
|
|
1204
|
+
case "attn.add_q_proj": return "transformerBlocks.\(index).attn.addQProj"
|
|
1205
|
+
case "attn.add_k_proj": return "transformerBlocks.\(index).attn.addKProj"
|
|
1206
|
+
case "attn.add_v_proj": return "transformerBlocks.\(index).attn.addVProj"
|
|
1207
|
+
case "attn.to_add_out": return "transformerBlocks.\(index).attn.toAddOut"
|
|
1208
|
+
case "ff.net.0.proj", "ff.proj": return "transformerBlocks.\(index).ff.activation.proj"
|
|
1209
|
+
case "ff.net.2", "ff.linear": return "transformerBlocks.\(index).ff.linearOut"
|
|
1210
|
+
case "ff_context.net.0.proj", "ff_context.proj": return "transformerBlocks.\(index).ffContext.activation.proj"
|
|
1211
|
+
case "ff_context.net.2", "ff_context.linear": return "transformerBlocks.\(index).ffContext.linearOut"
|
|
1212
|
+
default: return nil
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if key.hasPrefix("single_transformer_blocks.") {
|
|
1217
|
+
let parts = key.split(separator: ".")
|
|
1218
|
+
guard parts.count >= 4, let index = Int(parts[1]) else { return nil }
|
|
1219
|
+
let suffix = parts.dropFirst(2).joined(separator: ".")
|
|
1220
|
+
switch suffix {
|
|
1221
|
+
case "proj_mlp", "attn.to_qkv_mlp", "linear1": return "singleTransformerBlocks.\(index).attn.toQkvMlp"
|
|
1222
|
+
case "proj_out", "attn.to_out", "linear2": return "singleTransformerBlocks.\(index).attn.toOut"
|
|
1223
|
+
default: return nil
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
switch key {
|
|
1228
|
+
case "img_in", "x_embedder": return "xEmbedder"
|
|
1229
|
+
case "txt_in", "context_embedder": return "contextEmbedder"
|
|
1230
|
+
case "final_layer.linear", "proj_out": return "projOut"
|
|
1231
|
+
default: return nil
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
private func loadTransformer() throws -> Flux2Transformer2DModel {
|
|
1236
|
+
if let transformer {
|
|
1237
|
+
return transformer
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
let profile = ModelRegistry.TransformerRepo.moxinOrgKlein4B8bit.profile
|
|
1241
|
+
let configURL = paths.transformer.appendingPathComponent("config.json")
|
|
1242
|
+
let config = FileManager.default.fileExists(atPath: configURL.path)
|
|
1243
|
+
? (try? Flux2TransformerConfig.load(from: configURL)) ?? .klein4B
|
|
1244
|
+
: .klein4B
|
|
1245
|
+
|
|
1246
|
+
let loaded = Flux2Transformer2DModel(
|
|
1247
|
+
config: config,
|
|
1248
|
+
memoryOptimization: .moderate,
|
|
1249
|
+
usesNormOutBias: profile.usesNormOutBias
|
|
1250
|
+
)
|
|
1251
|
+
transformer = loaded
|
|
1252
|
+
|
|
1253
|
+
var weights = try Flux2WeightLoader.loadWeights(from: paths.transformer)
|
|
1254
|
+
try Flux2WeightLoader.applyTransformerWeights(&weights, to: loaded, profile: profile)
|
|
1255
|
+
weights.removeAll()
|
|
1256
|
+
eval(loaded.parameters())
|
|
1257
|
+
MLX.Memory.clearCache()
|
|
1258
|
+
|
|
1259
|
+
return loaded
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private func loadVAE() throws -> AutoencoderKLFlux2 {
|
|
1263
|
+
if let vae {
|
|
1264
|
+
return vae
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
let configURL = paths.vae.appendingPathComponent("config.json")
|
|
1268
|
+
let config = FileManager.default.fileExists(atPath: configURL.path)
|
|
1269
|
+
? try VAEConfig.load(from: configURL)
|
|
1270
|
+
: .flux2Dev
|
|
1271
|
+
|
|
1272
|
+
let loaded = AutoencoderKLFlux2(config: config)
|
|
1273
|
+
vae = loaded
|
|
1274
|
+
|
|
1275
|
+
let weights = try Flux2WeightLoader.loadWeights(from: paths.vae)
|
|
1276
|
+
try Flux2WeightLoader.applyVAEWeights(weights, to: loaded)
|
|
1277
|
+
eval(loaded.parameters())
|
|
1278
|
+
MLX.Memory.clearCache()
|
|
1279
|
+
|
|
1280
|
+
return loaded
|
|
1281
|
+
}
|
|
1282
|
+
}
|