bleam 0.0.8 → 0.0.10
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 +269 -22
- package/dist/cli.d.cts +32 -2
- package/dist/cli.d.ts +32 -2
- package/dist/cli.js +270 -23
- package/dist/platform.d.cts +2 -0
- package/dist/platform.d.ts +2 -0
- package/dist/{ui-1WepaMS4.d.cts → ui-Bg11tvlc.d.ts} +1 -1
- package/dist/{ui-Ce-pnUUA.d.ts → ui-Dd7SXdbg.d.cts} +7 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.cts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +5 -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 +482 -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 +1147 -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/main.swift +726 -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 +329 -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/templates/updates/README.md +272 -0
- package/templates/updates/src/index.ts +385 -0
- package/templates/updates/src/schema.ts +385 -0
- package/templates/updates/tsconfig.json +12 -0
- package/templates/updates/wrangler.jsonc +19 -0
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
// WeightLoader.swift - Load model weights from safetensors
|
|
2
|
+
// Copyright 2025 Vincent Gourbin
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MLX
|
|
6
|
+
import MLXNN
|
|
7
|
+
|
|
8
|
+
/// Utilities for loading Flux.2 model weights from safetensors files
|
|
9
|
+
public class Flux2WeightLoader {
|
|
10
|
+
|
|
11
|
+
/// Load all weights from a model directory
|
|
12
|
+
/// - Parameter modelPath: Path to directory containing safetensors files
|
|
13
|
+
/// - Returns: Dictionary of weight name to MLXArray
|
|
14
|
+
public static func loadWeights(from modelPath: String) throws -> [String: MLXArray] {
|
|
15
|
+
let fm = FileManager.default
|
|
16
|
+
let contents = try fm.contentsOfDirectory(atPath: modelPath)
|
|
17
|
+
let safetensorFiles = contents.filter { $0.hasSuffix(".safetensors") }.sorted()
|
|
18
|
+
|
|
19
|
+
if safetensorFiles.isEmpty {
|
|
20
|
+
throw Flux2WeightLoaderError.noWeightsFound(modelPath)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Flux2Debug.log("Found \(safetensorFiles.count) safetensor files in \(modelPath)")
|
|
24
|
+
|
|
25
|
+
var allWeights: [String: MLXArray] = [:]
|
|
26
|
+
|
|
27
|
+
for filename in safetensorFiles {
|
|
28
|
+
let filePath = "\(modelPath)/\(filename)"
|
|
29
|
+
let weights = try loadArrays(url: URL(fileURLWithPath: filePath))
|
|
30
|
+
|
|
31
|
+
for (key, value) in weights {
|
|
32
|
+
let normalizedKey = normalizeUREVTransformerKey(key, filename: filename, modelPath: modelPath)
|
|
33
|
+
allWeights[normalizedKey] = value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Flux2Debug.log("Loaded \(weights.count) tensors from \(filename)")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return allWeights
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static func normalizeUREVTransformerKey(_ key: String, filename: String, modelPath: String) -> String {
|
|
43
|
+
let isUREVCheckpoint = filename == "urevF2K4BFp16.safetensors"
|
|
44
|
+
&& modelPath.split(separator: "/").contains("urev")
|
|
45
|
+
let prefix = "model.diffusion_model."
|
|
46
|
+
guard isUREVCheckpoint, key.hasPrefix(prefix) else { return key }
|
|
47
|
+
return String(key.dropFirst(prefix.count))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Load weights from URL
|
|
51
|
+
public static func loadWeights(from url: URL) throws -> [String: MLXArray] {
|
|
52
|
+
try loadWeights(from: url.path)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// MARK: - Transformer Weight Mapping
|
|
56
|
+
|
|
57
|
+
/// Detect if weights are in BFL (Black Forest Labs) native format
|
|
58
|
+
private static func isBFLFormat(_ weights: [String: MLXArray]) -> Bool {
|
|
59
|
+
// BFL format uses "double_blocks" and "single_blocks" directly
|
|
60
|
+
// Diffusers format uses "transformer_blocks" and "single_transformer_blocks"
|
|
61
|
+
return weights.keys.contains { $0.hasPrefix("double_blocks.") || $0.hasPrefix("single_blocks.") }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Convert HuggingFace transformer weight keys to Swift module paths
|
|
65
|
+
/// Also handles dequantization of quanto qint8 weights
|
|
66
|
+
/// Supports both Diffusers format and BFL native format
|
|
67
|
+
/// Note: For Diffusers quanto format, entries are removed from `weights` as they are mapped
|
|
68
|
+
/// to avoid holding both raw quanto (~32GB) and dequantified float16 (~60GB) simultaneously.
|
|
69
|
+
static func mapTransformerWeights(
|
|
70
|
+
_ weights: inout [String: MLXArray],
|
|
71
|
+
profile: Flux2TransformerRepoProfile
|
|
72
|
+
) -> [String: MLXArray] {
|
|
73
|
+
if profile.checkpointFormat == .bfl || isBFLFormat(weights) {
|
|
74
|
+
Flux2Debug.log("Detected BFL native format weights")
|
|
75
|
+
return mapBFLTransformerWeights(&weights)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Flux2Debug.log("Detected Diffusers format weights for \(profile.repoId)")
|
|
79
|
+
return mapDiffusersTransformerWeights(&weights, profile: profile)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Detect whether the raw transformer checkpoint is already stored in a packed
|
|
83
|
+
/// quantized format that should be dequantized during load rather than requantized
|
|
84
|
+
/// again at runtime.
|
|
85
|
+
static func isNativePrequantizedTransformerCheckpoint(_ weights: [String: MLXArray]) -> Bool {
|
|
86
|
+
let hasPackedWeights = weights.contains { key, value in
|
|
87
|
+
key.hasSuffix(".weight") && value.dtype == .uint32
|
|
88
|
+
}
|
|
89
|
+
let hasQuantizationSidecars = weights.keys.contains { $0.hasSuffix(".scales") || $0.hasSuffix(".biases") }
|
|
90
|
+
|
|
91
|
+
return hasPackedWeights && hasQuantizationSidecars
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static func isNativePrequantizedTransformerCheckpoint(
|
|
95
|
+
_ weights: [String: MLXArray],
|
|
96
|
+
profile: Flux2TransformerRepoProfile
|
|
97
|
+
) -> Bool {
|
|
98
|
+
switch profile.checkpointFormat {
|
|
99
|
+
case .packedDiffusers, .bfl:
|
|
100
|
+
return isNativePrequantizedTransformerCheckpoint(weights)
|
|
101
|
+
case .diffusers, .diffusersQuanto:
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Map weights in BFL (Black Forest Labs) native format
|
|
107
|
+
/// BFL format uses fused QKV and different naming conventions
|
|
108
|
+
/// Entries are removed from the input dict as they are processed to minimize peak memory,
|
|
109
|
+
/// avoiding holding both raw bf16 weights and mapped/converted weights simultaneously.
|
|
110
|
+
private static func mapBFLTransformerWeights(_ weights: inout [String: MLXArray]) -> [String: MLXArray] {
|
|
111
|
+
var mapped: [String: MLXArray] = [:]
|
|
112
|
+
|
|
113
|
+
// Debug: count double_blocks keys
|
|
114
|
+
let doubleBlockKeys = weights.keys.filter { $0.hasPrefix("double_blocks.") }
|
|
115
|
+
Flux2Debug.log("BFL format: found \(doubleBlockKeys.count) double_blocks keys")
|
|
116
|
+
for key in doubleBlockKeys.prefix(5) {
|
|
117
|
+
Flux2Debug.verbose(" double_block key: \(key)")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Iterate over snapshot of keys; remove each entry after processing to free memory
|
|
121
|
+
let allKeys = Array(weights.keys)
|
|
122
|
+
for key in allKeys {
|
|
123
|
+
guard let value = weights.removeValue(forKey: key) else { continue }
|
|
124
|
+
// BFL format uses fused QKV weights that need to be split
|
|
125
|
+
// double_blocks.{i}.img_attn.qkv.weight -> split into toQ, toK, toV
|
|
126
|
+
// double_blocks.{i}.txt_attn.qkv.weight -> split into addQProj, addKProj, addVProj
|
|
127
|
+
|
|
128
|
+
if let mappedKey = mapCurrentBFLTransformerKey(key) {
|
|
129
|
+
mapped[mappedKey] = value
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if key.contains(".img_attn.qkv.weight") {
|
|
134
|
+
Flux2Debug.verbose("Splitting img_attn QKV: \(key) shape=\(value.shape)")
|
|
135
|
+
// Extract block index
|
|
136
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
137
|
+
// Split fused QKV into Q, K, V (shape: [3*dim, dim] -> 3x [dim, dim])
|
|
138
|
+
let dim = value.shape[0] / 3
|
|
139
|
+
let q = value[0..<dim, 0...]
|
|
140
|
+
let k = value[dim..<(2*dim), 0...]
|
|
141
|
+
let v = value[(2*dim)..., 0...]
|
|
142
|
+
mapped["transformerBlocks.\(blockIdx).attn.toQ.weight"] = q
|
|
143
|
+
mapped["transformerBlocks.\(blockIdx).attn.toK.weight"] = k
|
|
144
|
+
mapped["transformerBlocks.\(blockIdx).attn.toV.weight"] = v
|
|
145
|
+
} else if key.contains(".txt_attn.qkv.weight") {
|
|
146
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
147
|
+
let dim = value.shape[0] / 3
|
|
148
|
+
let q = value[0..<dim, 0...]
|
|
149
|
+
let k = value[dim..<(2*dim), 0...]
|
|
150
|
+
let v = value[(2*dim)..., 0...]
|
|
151
|
+
mapped["transformerBlocks.\(blockIdx).attn.addQProj.weight"] = q
|
|
152
|
+
mapped["transformerBlocks.\(blockIdx).attn.addKProj.weight"] = k
|
|
153
|
+
mapped["transformerBlocks.\(blockIdx).attn.addVProj.weight"] = v
|
|
154
|
+
} else if key.contains(".img_attn.proj.weight") {
|
|
155
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
156
|
+
mapped["transformerBlocks.\(blockIdx).attn.toOut.weight"] = value
|
|
157
|
+
} else if key.contains(".txt_attn.proj.weight") {
|
|
158
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
159
|
+
mapped["transformerBlocks.\(blockIdx).attn.toAddOut.weight"] = value
|
|
160
|
+
} else if key.contains(".img_attn.norm.query_norm.scale") {
|
|
161
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
162
|
+
mapped["transformerBlocks.\(blockIdx).attn.normQ.weight"] = value
|
|
163
|
+
} else if key.contains(".img_attn.norm.key_norm.scale") {
|
|
164
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
165
|
+
mapped["transformerBlocks.\(blockIdx).attn.normK.weight"] = value
|
|
166
|
+
} else if key.contains(".txt_attn.norm.query_norm.scale") {
|
|
167
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
168
|
+
mapped["transformerBlocks.\(blockIdx).attn.normAddedQ.weight"] = value
|
|
169
|
+
} else if key.contains(".txt_attn.norm.key_norm.scale") {
|
|
170
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
171
|
+
mapped["transformerBlocks.\(blockIdx).attn.normAddedK.weight"] = value
|
|
172
|
+
} else if key.contains(".img_mlp.0.weight") {
|
|
173
|
+
// BFL: img_mlp.0 is the gated linear (produces 2x for SwiGLU)
|
|
174
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
175
|
+
mapped["transformerBlocks.\(blockIdx).ff.activation.proj.weight"] = value
|
|
176
|
+
} else if key.contains(".img_mlp.2.weight") {
|
|
177
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
178
|
+
mapped["transformerBlocks.\(blockIdx).ff.linearOut.weight"] = value
|
|
179
|
+
} else if key.contains(".txt_mlp.0.weight") {
|
|
180
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
181
|
+
mapped["transformerBlocks.\(blockIdx).ffContext.activation.proj.weight"] = value
|
|
182
|
+
} else if key.contains(".txt_mlp.2.weight") {
|
|
183
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
184
|
+
mapped["transformerBlocks.\(blockIdx).ffContext.linearOut.weight"] = value
|
|
185
|
+
} else if key.hasPrefix("single_blocks.") && key.contains(".linear1.weight") {
|
|
186
|
+
// Single blocks have fused QKV+MLP
|
|
187
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "single_blocks.")
|
|
188
|
+
mapped["singleTransformerBlocks.\(blockIdx).attn.toQkvMlp.weight"] = value
|
|
189
|
+
} else if key.hasPrefix("single_blocks.") && key.contains(".linear2.weight") {
|
|
190
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "single_blocks.")
|
|
191
|
+
mapped["singleTransformerBlocks.\(blockIdx).attn.toOut.weight"] = value
|
|
192
|
+
} else if key.hasPrefix("single_blocks.") && key.contains(".norm.query_norm.scale") {
|
|
193
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "single_blocks.")
|
|
194
|
+
mapped["singleTransformerBlocks.\(blockIdx).attn.normQ.weight"] = value
|
|
195
|
+
} else if key.hasPrefix("single_blocks.") && key.contains(".norm.key_norm.scale") {
|
|
196
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "single_blocks.")
|
|
197
|
+
mapped["singleTransformerBlocks.\(blockIdx).attn.normK.weight"] = value
|
|
198
|
+
} else if key == "img_in.weight" {
|
|
199
|
+
Flux2Debug.log("BFL: mapping img_in -> xEmbedder: \(value.shape)")
|
|
200
|
+
mapped["xEmbedder.weight"] = value
|
|
201
|
+
} else if key == "txt_in.weight" {
|
|
202
|
+
Flux2Debug.log("BFL: mapping txt_in -> contextEmbedder: \(value.shape)")
|
|
203
|
+
mapped["contextEmbedder.weight"] = value
|
|
204
|
+
} else if key == "time_in.in_layer.weight" {
|
|
205
|
+
Flux2Debug.log("BFL: mapping time_in.in_layer -> timestepEmbedder.linear1: \(value.shape)")
|
|
206
|
+
mapped["timeGuidanceEmbed.timestepEmbedder.linear1.weight"] = value
|
|
207
|
+
} else if key == "time_in.out_layer.weight" {
|
|
208
|
+
Flux2Debug.log("BFL: mapping time_in.out_layer -> timestepEmbedder.linear2: \(value.shape)")
|
|
209
|
+
mapped["timeGuidanceEmbed.timestepEmbedder.linear2.weight"] = value
|
|
210
|
+
} else if key == "double_stream_modulation_img.lin.weight" {
|
|
211
|
+
mapped["doubleStreamModulationImg.linear.weight"] = value
|
|
212
|
+
} else if key == "double_stream_modulation_txt.lin.weight" {
|
|
213
|
+
mapped["doubleStreamModulationTxt.linear.weight"] = value
|
|
214
|
+
} else if key == "single_stream_modulation.lin.weight" {
|
|
215
|
+
mapped["singleStreamModulation.linear.weight"] = value
|
|
216
|
+
} else if key == "final_layer.adaLN_modulation.1.weight" {
|
|
217
|
+
// IMPORTANT: BFL format vs Diffusers format weight layout difference
|
|
218
|
+
//
|
|
219
|
+
// The normOut layer (AdaLayerNormContinuous) projects conditioning to scale+shift:
|
|
220
|
+
// params = linear(silu(conditioning)) // [B, dim * 2]
|
|
221
|
+
// scale = params[0:dim]
|
|
222
|
+
// shift = params[dim:]
|
|
223
|
+
//
|
|
224
|
+
// BFL format stores the linear weight as [shift_weights | scale_weights]
|
|
225
|
+
// Diffusers format stores it as [scale_weights | shift_weights]
|
|
226
|
+
//
|
|
227
|
+
// Without this swap, bf16 BFL models produce inverted scale/shift values,
|
|
228
|
+
// causing ~10x higher output magnitude and posterized/noisy images.
|
|
229
|
+
//
|
|
230
|
+
// Fix: Swap the two halves of the weight matrix to match diffusers layout.
|
|
231
|
+
let dim = value.shape[0] / 2 // 3072 for Klein 4B, 6144 for Dev
|
|
232
|
+
let shiftRows = value[0..<dim, 0...] // First half (shift in BFL)
|
|
233
|
+
let scaleRows = value[dim..., 0...] // Second half (scale in BFL)
|
|
234
|
+
let swapped = concatenated([scaleRows, shiftRows], axis: 0)
|
|
235
|
+
Flux2Debug.log("BFL: Swapped normOut.linear.weight halves (dim=\(dim)): \(value.shape) -> \(swapped.shape)")
|
|
236
|
+
mapped["normOut.linear.weight"] = swapped
|
|
237
|
+
} else if key == "final_layer.linear.weight" {
|
|
238
|
+
mapped["projOut.weight"] = value
|
|
239
|
+
} else {
|
|
240
|
+
Flux2Debug.verbose("BFL format: unmapped key \(key)")
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Input dict is now empty — raw BFL weight references freed
|
|
245
|
+
Flux2Debug.log("Input dict cleared — freed raw BFL weight entries")
|
|
246
|
+
Flux2Debug.log("Mapped \(mapped.count) BFL format weights")
|
|
247
|
+
|
|
248
|
+
// Debug: print mapped keys by category
|
|
249
|
+
let transformerBlockKeys2 = mapped.keys.filter { $0.hasPrefix("transformerBlocks.") }
|
|
250
|
+
let singleBlockKeys = mapped.keys.filter { $0.hasPrefix("singleTransformerBlocks.") }
|
|
251
|
+
Flux2Debug.log(" - transformerBlocks keys: \(transformerBlockKeys2.count)")
|
|
252
|
+
Flux2Debug.log(" - singleTransformerBlocks keys: \(singleBlockKeys.count)")
|
|
253
|
+
|
|
254
|
+
Flux2Debug.verbose("transformerBlocks keys:")
|
|
255
|
+
for key in transformerBlockKeys2.sorted().prefix(10) {
|
|
256
|
+
Flux2Debug.verbose(" - \(key): \(mapped[key]!.shape)")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// DEBUG: Print raw BFL weight statistics BEFORE conversion
|
|
260
|
+
Flux2Debug.log("=== Raw BFL Weight Statistics (BEFORE conversion) ===")
|
|
261
|
+
for key in ["xEmbedder.weight", "contextEmbedder.weight",
|
|
262
|
+
"transformerBlocks.0.attn.toQ.weight",
|
|
263
|
+
"transformerBlocks.0.ff.activation.proj.weight",
|
|
264
|
+
"singleTransformerBlocks.0.attn.toQkvMlp.weight"] {
|
|
265
|
+
if let w = mapped[key] {
|
|
266
|
+
eval(w)
|
|
267
|
+
// Convert to float32 for accurate stats
|
|
268
|
+
let wf32 = w.asType(.float32)
|
|
269
|
+
let absVal = MLX.abs(wf32)
|
|
270
|
+
let meanVal = mean(absVal).item(Float.self)
|
|
271
|
+
let maxVal = MLX.max(absVal).item(Float.self)
|
|
272
|
+
let minVal = MLX.min(absVal).item(Float.self)
|
|
273
|
+
let hasNaN = any(isNaN(wf32)).item(Bool.self)
|
|
274
|
+
let numInf = sum(MLX.abs(wf32) .> 1e30).item(Int.self) // Count very large values as proxy for inf
|
|
275
|
+
Flux2Debug.log(" \(key): dtype=\(w.dtype), shape=\(w.shape), mean=\(meanVal), max=\(maxVal), min=\(minVal), hasNaN=\(hasNaN), numInf=\(numInf)")
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Convert bfloat16 → float16 directly (no intermediate f32)
|
|
280
|
+
// bf16 has wider exponent range (8-bit) than f16 (5-bit), so values outside
|
|
281
|
+
// f16 range [−65504, 65504] would overflow to inf. MLX handles this correctly
|
|
282
|
+
// by saturating to ±inf, which is the same behavior as the f32 intermediate path.
|
|
283
|
+
// Going direct saves ~24GB of temporary f32 allocations for Dev-size models.
|
|
284
|
+
// Progressive removal from `mapped` avoids holding both dicts simultaneously.
|
|
285
|
+
var converted: [String: MLXArray] = [:]
|
|
286
|
+
var convertedCount = 0
|
|
287
|
+
let mappedKeys = Array(mapped.keys)
|
|
288
|
+
for key in mappedKeys {
|
|
289
|
+
guard let value = mapped.removeValue(forKey: key) else { continue }
|
|
290
|
+
if value.dtype == .bfloat16 {
|
|
291
|
+
converted[key] = value.asType(.float16)
|
|
292
|
+
convertedCount += 1
|
|
293
|
+
} else {
|
|
294
|
+
converted[key] = value
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
Flux2Debug.log("Converted \(convertedCount) bfloat16 weights to float16 (direct)")
|
|
298
|
+
|
|
299
|
+
// DEBUG: Print weight statistics for key layers (AFTER processing)
|
|
300
|
+
Flux2Debug.log("=== BFL Weight Statistics (AFTER processing) ===")
|
|
301
|
+
for key in ["xEmbedder.weight", "contextEmbedder.weight",
|
|
302
|
+
"transformerBlocks.0.attn.toQ.weight",
|
|
303
|
+
"transformerBlocks.0.ff.activation.proj.weight",
|
|
304
|
+
"singleTransformerBlocks.0.attn.toQkvMlp.weight"] {
|
|
305
|
+
if let w = converted[key] {
|
|
306
|
+
eval(w)
|
|
307
|
+
let wf32 = w.asType(.float32)
|
|
308
|
+
let absVal = MLX.abs(wf32)
|
|
309
|
+
let meanVal = mean(absVal).item(Float.self)
|
|
310
|
+
let maxVal = MLX.max(absVal).item(Float.self)
|
|
311
|
+
let minVal = MLX.min(absVal).item(Float.self)
|
|
312
|
+
let hasNaN = any(isNaN(wf32)).item(Bool.self)
|
|
313
|
+
let numInf = sum(MLX.abs(wf32) .> 1e30).item(Int.self) // Count very large values as proxy for inf
|
|
314
|
+
Flux2Debug.log(" \(key): dtype=\(w.dtype), shape=\(w.shape), mean=\(meanVal), max=\(maxVal), min=\(minVal), hasNaN=\(hasNaN), numInf=\(numInf)")
|
|
315
|
+
} else {
|
|
316
|
+
Flux2Debug.log(" \(key): NOT FOUND")
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return converted
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private static func mapCurrentBFLTransformerKey(_ key: String) -> String? {
|
|
324
|
+
if key.hasPrefix("double_blocks.") {
|
|
325
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "double_blocks.")
|
|
326
|
+
|
|
327
|
+
if let suffix = key.split(separator: ".", maxSplits: 2, omittingEmptySubsequences: false).last {
|
|
328
|
+
switch String(suffix) {
|
|
329
|
+
case "img_to_q.weight": return "transformerBlocks.\(blockIdx).attn.toQ.weight"
|
|
330
|
+
case "img_to_q.scales": return "transformerBlocks.\(blockIdx).attn.toQ.scales"
|
|
331
|
+
case "img_to_q.biases": return "transformerBlocks.\(blockIdx).attn.toQ.biases"
|
|
332
|
+
case "img_to_k.weight": return "transformerBlocks.\(blockIdx).attn.toK.weight"
|
|
333
|
+
case "img_to_k.scales": return "transformerBlocks.\(blockIdx).attn.toK.scales"
|
|
334
|
+
case "img_to_k.biases": return "transformerBlocks.\(blockIdx).attn.toK.biases"
|
|
335
|
+
case "img_to_v.weight": return "transformerBlocks.\(blockIdx).attn.toV.weight"
|
|
336
|
+
case "img_to_v.scales": return "transformerBlocks.\(blockIdx).attn.toV.scales"
|
|
337
|
+
case "img_to_v.biases": return "transformerBlocks.\(blockIdx).attn.toV.biases"
|
|
338
|
+
case "img_to_out.weight": return "transformerBlocks.\(blockIdx).attn.toOut.weight"
|
|
339
|
+
case "img_to_out.scales": return "transformerBlocks.\(blockIdx).attn.toOut.scales"
|
|
340
|
+
case "img_to_out.biases": return "transformerBlocks.\(blockIdx).attn.toOut.biases"
|
|
341
|
+
case "txt_to_q.weight": return "transformerBlocks.\(blockIdx).attn.addQProj.weight"
|
|
342
|
+
case "txt_to_q.scales": return "transformerBlocks.\(blockIdx).attn.addQProj.scales"
|
|
343
|
+
case "txt_to_q.biases": return "transformerBlocks.\(blockIdx).attn.addQProj.biases"
|
|
344
|
+
case "txt_to_k.weight": return "transformerBlocks.\(blockIdx).attn.addKProj.weight"
|
|
345
|
+
case "txt_to_k.scales": return "transformerBlocks.\(blockIdx).attn.addKProj.scales"
|
|
346
|
+
case "txt_to_k.biases": return "transformerBlocks.\(blockIdx).attn.addKProj.biases"
|
|
347
|
+
case "txt_to_v.weight": return "transformerBlocks.\(blockIdx).attn.addVProj.weight"
|
|
348
|
+
case "txt_to_v.scales": return "transformerBlocks.\(blockIdx).attn.addVProj.scales"
|
|
349
|
+
case "txt_to_v.biases": return "transformerBlocks.\(blockIdx).attn.addVProj.biases"
|
|
350
|
+
case "txt_to_out.weight": return "transformerBlocks.\(blockIdx).attn.toAddOut.weight"
|
|
351
|
+
case "txt_to_out.scales": return "transformerBlocks.\(blockIdx).attn.toAddOut.scales"
|
|
352
|
+
case "txt_to_out.biases": return "transformerBlocks.\(blockIdx).attn.toAddOut.biases"
|
|
353
|
+
case "img_norm_q.weight": return "transformerBlocks.\(blockIdx).attn.normQ.weight"
|
|
354
|
+
case "img_norm_k.weight": return "transformerBlocks.\(blockIdx).attn.normK.weight"
|
|
355
|
+
case "txt_norm_q.weight": return "transformerBlocks.\(blockIdx).attn.normAddedQ.weight"
|
|
356
|
+
case "txt_norm_k.weight": return "transformerBlocks.\(blockIdx).attn.normAddedK.weight"
|
|
357
|
+
case "img_mlp_in.weight": return "transformerBlocks.\(blockIdx).ff.activation.proj.weight"
|
|
358
|
+
case "img_mlp_in.scales": return "transformerBlocks.\(blockIdx).ff.activation.proj.scales"
|
|
359
|
+
case "img_mlp_in.biases": return "transformerBlocks.\(blockIdx).ff.activation.proj.biases"
|
|
360
|
+
case "img_mlp_out.weight": return "transformerBlocks.\(blockIdx).ff.linearOut.weight"
|
|
361
|
+
case "img_mlp_out.scales": return "transformerBlocks.\(blockIdx).ff.linearOut.scales"
|
|
362
|
+
case "img_mlp_out.biases": return "transformerBlocks.\(blockIdx).ff.linearOut.biases"
|
|
363
|
+
case "txt_mlp_in.weight": return "transformerBlocks.\(blockIdx).ffContext.activation.proj.weight"
|
|
364
|
+
case "txt_mlp_in.scales": return "transformerBlocks.\(blockIdx).ffContext.activation.proj.scales"
|
|
365
|
+
case "txt_mlp_in.biases": return "transformerBlocks.\(blockIdx).ffContext.activation.proj.biases"
|
|
366
|
+
case "txt_mlp_out.weight": return "transformerBlocks.\(blockIdx).ffContext.linearOut.weight"
|
|
367
|
+
case "txt_mlp_out.scales": return "transformerBlocks.\(blockIdx).ffContext.linearOut.scales"
|
|
368
|
+
case "txt_mlp_out.biases": return "transformerBlocks.\(blockIdx).ffContext.linearOut.biases"
|
|
369
|
+
default: return nil
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if key.hasPrefix("single_blocks.") {
|
|
375
|
+
let blockIdx = extractBlockIndex(from: key, prefix: "single_blocks.")
|
|
376
|
+
|
|
377
|
+
if let suffix = key.split(separator: ".", maxSplits: 2, omittingEmptySubsequences: false).last {
|
|
378
|
+
switch String(suffix) {
|
|
379
|
+
case "to_qkv_mlp.weight": return "singleTransformerBlocks.\(blockIdx).attn.toQkvMlp.weight"
|
|
380
|
+
case "to_qkv_mlp.scales": return "singleTransformerBlocks.\(blockIdx).attn.toQkvMlp.scales"
|
|
381
|
+
case "to_qkv_mlp.biases": return "singleTransformerBlocks.\(blockIdx).attn.toQkvMlp.biases"
|
|
382
|
+
case "to_out.weight": return "singleTransformerBlocks.\(blockIdx).attn.toOut.weight"
|
|
383
|
+
case "to_out.scales": return "singleTransformerBlocks.\(blockIdx).attn.toOut.scales"
|
|
384
|
+
case "to_out.biases": return "singleTransformerBlocks.\(blockIdx).attn.toOut.biases"
|
|
385
|
+
case "norm_q.weight": return "singleTransformerBlocks.\(blockIdx).attn.normQ.weight"
|
|
386
|
+
case "norm_k.weight": return "singleTransformerBlocks.\(blockIdx).attn.normK.weight"
|
|
387
|
+
default: return nil
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
switch key {
|
|
393
|
+
case "x_embedder.weight":
|
|
394
|
+
return "xEmbedder.weight"
|
|
395
|
+
case "x_embedder.scales":
|
|
396
|
+
return "xEmbedder.scales"
|
|
397
|
+
case "x_embedder.biases":
|
|
398
|
+
return "xEmbedder.biases"
|
|
399
|
+
case "context_embedder.weight":
|
|
400
|
+
return "contextEmbedder.weight"
|
|
401
|
+
case "context_embedder.scales":
|
|
402
|
+
return "contextEmbedder.scales"
|
|
403
|
+
case "context_embedder.biases":
|
|
404
|
+
return "contextEmbedder.biases"
|
|
405
|
+
case "time_embed_1.weight":
|
|
406
|
+
return "timeGuidanceEmbed.timestepEmbedder.linear1.weight"
|
|
407
|
+
case "time_embed_1.scales":
|
|
408
|
+
return "timeGuidanceEmbed.timestepEmbedder.linear1.scales"
|
|
409
|
+
case "time_embed_1.biases":
|
|
410
|
+
return "timeGuidanceEmbed.timestepEmbedder.linear1.biases"
|
|
411
|
+
case "time_embed_2.weight":
|
|
412
|
+
return "timeGuidanceEmbed.timestepEmbedder.linear2.weight"
|
|
413
|
+
case "time_embed_2.scales":
|
|
414
|
+
return "timeGuidanceEmbed.timestepEmbedder.linear2.scales"
|
|
415
|
+
case "time_embed_2.biases":
|
|
416
|
+
return "timeGuidanceEmbed.timestepEmbedder.linear2.biases"
|
|
417
|
+
case "double_mod_img.linear.weight":
|
|
418
|
+
return "doubleStreamModulationImg.linear.weight"
|
|
419
|
+
case "double_mod_img.linear.scales":
|
|
420
|
+
return "doubleStreamModulationImg.linear.scales"
|
|
421
|
+
case "double_mod_img.linear.biases":
|
|
422
|
+
return "doubleStreamModulationImg.linear.biases"
|
|
423
|
+
case "double_mod_txt.linear.weight":
|
|
424
|
+
return "doubleStreamModulationTxt.linear.weight"
|
|
425
|
+
case "double_mod_txt.linear.scales":
|
|
426
|
+
return "doubleStreamModulationTxt.linear.scales"
|
|
427
|
+
case "double_mod_txt.linear.biases":
|
|
428
|
+
return "doubleStreamModulationTxt.linear.biases"
|
|
429
|
+
case "single_mod.linear.weight":
|
|
430
|
+
return "singleStreamModulation.linear.weight"
|
|
431
|
+
case "single_mod.linear.scales":
|
|
432
|
+
return "singleStreamModulation.linear.scales"
|
|
433
|
+
case "single_mod.linear.biases":
|
|
434
|
+
return "singleStreamModulation.linear.biases"
|
|
435
|
+
case "norm_out.weight":
|
|
436
|
+
return "normOut.linear.weight"
|
|
437
|
+
case "norm_out.scales":
|
|
438
|
+
return "normOut.linear.scales"
|
|
439
|
+
case "norm_out.biases":
|
|
440
|
+
return "normOut.linear.biases"
|
|
441
|
+
case "proj_out.weight":
|
|
442
|
+
return "projOut.weight"
|
|
443
|
+
case "proj_out.scales":
|
|
444
|
+
return "projOut.scales"
|
|
445
|
+
case "proj_out.biases":
|
|
446
|
+
return "projOut.biases"
|
|
447
|
+
default:
|
|
448
|
+
return nil
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private static func quantizedBaseKey(for mappedKey: String) -> String? {
|
|
453
|
+
if mappedKey.hasSuffix(".scales") {
|
|
454
|
+
return String(mappedKey.dropLast(7)) + ".weight"
|
|
455
|
+
}
|
|
456
|
+
if mappedKey.hasSuffix(".biases") {
|
|
457
|
+
return String(mappedKey.dropLast(7)) + ".weight"
|
|
458
|
+
}
|
|
459
|
+
return nil
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private static func inferQuantizationBits(packedShape: [Int], expectedShape: [Int]) -> Int? {
|
|
463
|
+
guard packedShape.count == expectedShape.count,
|
|
464
|
+
let packedCols = packedShape.last,
|
|
465
|
+
let expectedCols = expectedShape.last,
|
|
466
|
+
packedCols > 0,
|
|
467
|
+
expectedCols > 0 else {
|
|
468
|
+
return nil
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let numerator = packedCols * 32
|
|
472
|
+
guard numerator % expectedCols == 0 else {
|
|
473
|
+
return nil
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let bits = numerator / expectedCols
|
|
477
|
+
return bits > 0 ? bits : nil
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private static func inferGroupSize(expectedShape: [Int], scalesShape: [Int]) -> Int? {
|
|
481
|
+
guard expectedShape.count == scalesShape.count,
|
|
482
|
+
let expectedCols = expectedShape.last,
|
|
483
|
+
let scaleGroups = scalesShape.last,
|
|
484
|
+
scaleGroups > 0,
|
|
485
|
+
expectedCols > 0,
|
|
486
|
+
expectedCols % scaleGroups == 0 else {
|
|
487
|
+
return nil
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return expectedCols / scaleGroups
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private static func maybeSwapNormOutRows(
|
|
494
|
+
_ weight: MLXArray,
|
|
495
|
+
mappedKey: String,
|
|
496
|
+
profile: Flux2TransformerRepoProfile
|
|
497
|
+
) -> MLXArray {
|
|
498
|
+
guard profile.needsNormOutSwap,
|
|
499
|
+
mappedKey == "normOut.linear.weight",
|
|
500
|
+
weight.shape.count == 2 else {
|
|
501
|
+
return weight
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let dim = weight.shape[0] / 2
|
|
505
|
+
guard dim > 0 else { return weight }
|
|
506
|
+
let shiftRows = weight[0..<dim, 0...]
|
|
507
|
+
let scaleRows = weight[dim..., 0...]
|
|
508
|
+
return concatenated([scaleRows, shiftRows], axis: 0)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private static func requiresStrictPackedValidation(for profile: Flux2TransformerRepoProfile) -> Bool {
|
|
512
|
+
profile.checkpointFormat == .packedDiffusers
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private static func isBonsaiTernaryProfile(_ profile: Flux2TransformerRepoProfile) -> Bool {
|
|
516
|
+
profile.repoId == ModelRegistry.TransformerRepo.prismMLBonsaiKlein4B2bit.rawValue
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private static func validateCriticalTransformerCoverage(
|
|
520
|
+
updates: [String: MLXArray],
|
|
521
|
+
packedModulePaths: Set<String> = [],
|
|
522
|
+
flatParameters: [String: MLXArray],
|
|
523
|
+
profile: Flux2TransformerRepoProfile
|
|
524
|
+
) throws {
|
|
525
|
+
guard requiresStrictPackedValidation(for: profile) else {
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let missingParameters = flatParameters.keys.sorted().filter { key in
|
|
530
|
+
updates[key] == nil && !packedModulePaths.contains(parameterModulePath(for: key))
|
|
531
|
+
}
|
|
532
|
+
guard missingParameters.isEmpty else {
|
|
533
|
+
let sample = missingParameters.prefix(12).joined(separator: ", ")
|
|
534
|
+
let missingByTopLevel = Dictionary(grouping: missingParameters) { key in
|
|
535
|
+
key.split(separator: ".").first.map(String.init) ?? key
|
|
536
|
+
}
|
|
537
|
+
let summary = missingByTopLevel
|
|
538
|
+
.map { "\($0.key): \($0.value.count)" }
|
|
539
|
+
.sorted()
|
|
540
|
+
.joined(separator: ", ")
|
|
541
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
542
|
+
"Missing \(missingParameters.count) transformer parameters for \(profile.repoId): \(sample). Summary: \(summary)"
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private static func parameterModulePath(for key: String) -> String {
|
|
548
|
+
if key.hasSuffix(".weight") {
|
|
549
|
+
return String(key.dropLast(7))
|
|
550
|
+
}
|
|
551
|
+
if key.hasSuffix(".bias") {
|
|
552
|
+
return String(key.dropLast(5))
|
|
553
|
+
}
|
|
554
|
+
if key.hasSuffix(".scales") {
|
|
555
|
+
return String(key.dropLast(7))
|
|
556
|
+
}
|
|
557
|
+
if key.hasSuffix(".biases") {
|
|
558
|
+
return String(key.dropLast(7))
|
|
559
|
+
}
|
|
560
|
+
return key
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private static func supportsPackedAffineRuntime(bits: Int) -> Bool {
|
|
564
|
+
switch bits {
|
|
565
|
+
case 2, 3, 4, 5, 6, 8:
|
|
566
|
+
return true
|
|
567
|
+
default:
|
|
568
|
+
return false
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/// Extract block index from key like "double_blocks.3.img_attn.qkv.weight"
|
|
573
|
+
private static func extractBlockIndex(from key: String, prefix: String) -> Int {
|
|
574
|
+
guard key.hasPrefix(prefix) else { return 0 }
|
|
575
|
+
let afterPrefix = String(key.dropFirst(prefix.count))
|
|
576
|
+
guard let dotIndex = afterPrefix.firstIndex(of: ".") else { return 0 }
|
|
577
|
+
let indexStr = String(afterPrefix[..<dotIndex])
|
|
578
|
+
return Int(indexStr) ?? 0
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/// Map weights in Diffusers format (HuggingFace/quanto quantized)
|
|
582
|
+
/// Entries are removed from the input dict as they are processed to minimize peak memory.
|
|
583
|
+
/// This avoids holding both raw quanto data (~32GB) and dequantified float16 (~60GB) simultaneously.
|
|
584
|
+
private static func mapDiffusersTransformerWeights(
|
|
585
|
+
_ weights: inout [String: MLXArray],
|
|
586
|
+
profile: Flux2TransformerRepoProfile
|
|
587
|
+
) -> [String: MLXArray] {
|
|
588
|
+
var mapped: [String: MLXArray] = [:]
|
|
589
|
+
var quantizedData: [String: MLXArray] = [:] // base_key -> ._data
|
|
590
|
+
var quantizedScales: [String: MLXArray] = [:] // base_key -> ._scale
|
|
591
|
+
let hasExplicitTimeTextEmbed = weights.keys.contains { key in
|
|
592
|
+
let normalizedKey = key.hasPrefix("transformer.") ? String(key.dropFirst(12)) : key
|
|
593
|
+
return normalizedKey.hasPrefix("time_text_embed.")
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// First pass: extract entries from input dict and remove them to free memory
|
|
597
|
+
let allKeys = Array(weights.keys)
|
|
598
|
+
for key in allKeys {
|
|
599
|
+
guard let value = weights.removeValue(forKey: key) else { continue }
|
|
600
|
+
|
|
601
|
+
var processedKey = key
|
|
602
|
+
|
|
603
|
+
// Remove common prefixes
|
|
604
|
+
if processedKey.hasPrefix("transformer.") {
|
|
605
|
+
processedKey = String(processedKey.dropFirst(12))
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Check for quanto quantization suffixes
|
|
609
|
+
if processedKey.hasSuffix("._data") {
|
|
610
|
+
let baseKey = String(processedKey.dropLast(6)) // Remove "._data"
|
|
611
|
+
let mappedKey = mapTransformerKeySimple(baseKey, hasExplicitTimeTextEmbed: hasExplicitTimeTextEmbed)
|
|
612
|
+
quantizedData[mappedKey] = value
|
|
613
|
+
} else if processedKey.hasSuffix("._scale") {
|
|
614
|
+
let baseKey = String(processedKey.dropLast(7)) // Remove "._scale"
|
|
615
|
+
let mappedKey = mapTransformerKeySimple(baseKey, hasExplicitTimeTextEmbed: hasExplicitTimeTextEmbed)
|
|
616
|
+
quantizedScales[mappedKey] = value
|
|
617
|
+
} else if processedKey.hasSuffix(".input_scale") || processedKey.hasSuffix(".output_scale") {
|
|
618
|
+
// Skip activation scales for quanto (value is released)
|
|
619
|
+
continue
|
|
620
|
+
} else {
|
|
621
|
+
// Non-quantized weight, map directly
|
|
622
|
+
let mappedKey = mapTransformerKeySimple(processedKey, hasExplicitTimeTextEmbed: hasExplicitTimeTextEmbed)
|
|
623
|
+
mapped[mappedKey] = value
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// Input dict is now empty — raw safetensors references freed
|
|
627
|
+
Flux2Debug.log("Input dict cleared — freed raw weight entries")
|
|
628
|
+
|
|
629
|
+
// Second pass: dequantize qint8 weights to float16
|
|
630
|
+
// Release quanto data/scale entries progressively to keep peak ≈ 60GB (float16 only)
|
|
631
|
+
var dequantCount = 0
|
|
632
|
+
let quantizedKeys = Array(quantizedData.keys)
|
|
633
|
+
for key in quantizedKeys {
|
|
634
|
+
guard let data = quantizedData.removeValue(forKey: key) else { continue }
|
|
635
|
+
|
|
636
|
+
if let scale = quantizedScales.removeValue(forKey: key) {
|
|
637
|
+
// Dequantize: weight = data.float() * scale, then convert to float16
|
|
638
|
+
let dequantized = (data.asType(.float32) * scale).asType(.float16)
|
|
639
|
+
mapped[key] = dequantized
|
|
640
|
+
dequantCount += 1
|
|
641
|
+
} else {
|
|
642
|
+
// No scale found, use data as-is
|
|
643
|
+
mapped[key] = data.asType(.float16)
|
|
644
|
+
Flux2Debug.warning("No scale found for quantized weight: \(key)")
|
|
645
|
+
}
|
|
646
|
+
// Periodically materialize and clear GPU cache
|
|
647
|
+
if dequantCount % 50 == 0 {
|
|
648
|
+
eval(mapped.values.map { $0 })
|
|
649
|
+
MLX.Memory.clearCache()
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
Flux2Debug.log("Dequantized \(dequantCount) qint8 weights to float16")
|
|
653
|
+
|
|
654
|
+
// DEBUG: Print weight statistics for key layers
|
|
655
|
+
Flux2Debug.log("=== Diffusers Weight Statistics ===")
|
|
656
|
+
for key in ["xEmbedder.weight", "contextEmbedder.weight",
|
|
657
|
+
"transformerBlocks.0.attn.toQ.weight",
|
|
658
|
+
"transformerBlocks.0.ff.activation.proj.weight",
|
|
659
|
+
"singleTransformerBlocks.0.attn.toQkvMlp.weight"].prefix(5) {
|
|
660
|
+
if let w = mapped[key] {
|
|
661
|
+
eval(w)
|
|
662
|
+
let absVal = MLX.abs(w)
|
|
663
|
+
let meanVal = mean(absVal).item(Float.self)
|
|
664
|
+
let maxVal = MLX.max(absVal).item(Float.self)
|
|
665
|
+
let minVal = MLX.min(absVal).item(Float.self)
|
|
666
|
+
Flux2Debug.log(" \(key): shape=\(w.shape), mean=\(meanVal), max=\(maxVal), min=\(minVal)")
|
|
667
|
+
} else {
|
|
668
|
+
Flux2Debug.log(" \(key): NOT FOUND")
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return mapped
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/// Map key without handling quantization suffixes (called after suffix processing)
|
|
676
|
+
private static func mapTransformerKeySimple(_ key: String, hasExplicitTimeTextEmbed: Bool = false) -> String {
|
|
677
|
+
var mapped = key
|
|
678
|
+
|
|
679
|
+
// Map single stream blocks (checkpoint may use various formats)
|
|
680
|
+
mapped = mapped.replacingOccurrences(of: "single_transformer_blocks.", with: "singleTransformerBlocks.")
|
|
681
|
+
mapped = mapped.replacingOccurrences(of: "single_transformerBlocks.", with: "singleTransformerBlocks.")
|
|
682
|
+
|
|
683
|
+
// Map double stream blocks (both formats)
|
|
684
|
+
mapped = mapped.replacingOccurrences(of: "transformer_blocks.", with: "transformerBlocks.")
|
|
685
|
+
|
|
686
|
+
// Map attention components - both underscore and camelCase from checkpoint
|
|
687
|
+
mapped = mapped.replacingOccurrences(of: "attn.to_q.", with: "attn.toQ.")
|
|
688
|
+
mapped = mapped.replacingOccurrences(of: "attn.to_k.", with: "attn.toK.")
|
|
689
|
+
mapped = mapped.replacingOccurrences(of: "attn.to_v.", with: "attn.toV.")
|
|
690
|
+
mapped = mapped.replacingOccurrences(of: "attn.toQ.", with: "attn.toQ.")
|
|
691
|
+
mapped = mapped.replacingOccurrences(of: "attn.toK.", with: "attn.toK.")
|
|
692
|
+
mapped = mapped.replacingOccurrences(of: "attn.toV.", with: "attn.toV.")
|
|
693
|
+
mapped = mapped.replacingOccurrences(of: "attn.to_out.0.", with: "attn.toOut.")
|
|
694
|
+
mapped = mapped.replacingOccurrences(of: "attn.toOut.0.", with: "attn.toOut.")
|
|
695
|
+
mapped = mapped.replacingOccurrences(of: "attn.add_q_proj.", with: "attn.addQProj.")
|
|
696
|
+
mapped = mapped.replacingOccurrences(of: "attn.add_k_proj.", with: "attn.addKProj.")
|
|
697
|
+
mapped = mapped.replacingOccurrences(of: "attn.add_v_proj.", with: "attn.addVProj.")
|
|
698
|
+
mapped = mapped.replacingOccurrences(of: "attn.addQProj.", with: "attn.addQProj.")
|
|
699
|
+
mapped = mapped.replacingOccurrences(of: "attn.addKProj.", with: "attn.addKProj.")
|
|
700
|
+
mapped = mapped.replacingOccurrences(of: "attn.addVProj.", with: "attn.addVProj.")
|
|
701
|
+
mapped = mapped.replacingOccurrences(of: "attn.to_add_out.", with: "attn.toAddOut.")
|
|
702
|
+
mapped = mapped.replacingOccurrences(of: "attn.toAddOut.", with: "attn.toAddOut.")
|
|
703
|
+
|
|
704
|
+
// Map single block attention (fused QKV+MLP)
|
|
705
|
+
// Model uses toQkvMlp, checkpoint uses to_qkv_mlp_proj
|
|
706
|
+
mapped = mapped.replacingOccurrences(of: "attn.to_qkv_mlp_proj.", with: "attn.toQkvMlp.")
|
|
707
|
+
|
|
708
|
+
// Map to_out without .0. (single stream blocks)
|
|
709
|
+
mapped = mapped.replacingOccurrences(of: "attn.to_out.", with: "attn.toOut.")
|
|
710
|
+
|
|
711
|
+
// Map norms - both formats
|
|
712
|
+
mapped = mapped.replacingOccurrences(of: "attn.norm_q.", with: "attn.normQ.")
|
|
713
|
+
mapped = mapped.replacingOccurrences(of: "attn.norm_k.", with: "attn.normK.")
|
|
714
|
+
mapped = mapped.replacingOccurrences(of: "attn.normQ.", with: "attn.normQ.")
|
|
715
|
+
mapped = mapped.replacingOccurrences(of: "attn.normK.", with: "attn.normK.")
|
|
716
|
+
mapped = mapped.replacingOccurrences(of: "attn.norm_added_q.", with: "attn.normAddedQ.")
|
|
717
|
+
mapped = mapped.replacingOccurrences(of: "attn.norm_added_k.", with: "attn.normAddedK.")
|
|
718
|
+
mapped = mapped.replacingOccurrences(of: "attn.normAddedQ.", with: "attn.normAddedQ.")
|
|
719
|
+
mapped = mapped.replacingOccurrences(of: "attn.normAddedK.", with: "attn.normAddedK.")
|
|
720
|
+
mapped = mapped.replacingOccurrences(of: "norm1_context.", with: "norm1Context.")
|
|
721
|
+
mapped = mapped.replacingOccurrences(of: "norm2_context.", with: "norm2Context.")
|
|
722
|
+
|
|
723
|
+
// Map feedforward
|
|
724
|
+
// Model uses SwiGLU with activation.proj for input, checkpoint uses linear_in
|
|
725
|
+
mapped = mapped.replacingOccurrences(of: "ff_context.linear_in.", with: "ffContext.activation.proj.")
|
|
726
|
+
mapped = mapped.replacingOccurrences(of: "ff_context.linear_out.", with: "ffContext.linearOut.")
|
|
727
|
+
mapped = mapped.replacingOccurrences(of: ".ff.linear_in.", with: ".ff.activation.proj.")
|
|
728
|
+
mapped = mapped.replacingOccurrences(of: ".ff.linear_out.", with: ".ff.linearOut.")
|
|
729
|
+
// Generic fallbacks
|
|
730
|
+
mapped = mapped.replacingOccurrences(of: "ff_context.", with: "ffContext.")
|
|
731
|
+
mapped = mapped.replacingOccurrences(of: "linear_in.", with: "activation.proj.")
|
|
732
|
+
mapped = mapped.replacingOccurrences(of: "linear_out.", with: "linearOut.")
|
|
733
|
+
|
|
734
|
+
// Map embeddings
|
|
735
|
+
mapped = mapped.replacingOccurrences(of: "x_embedder.", with: "xEmbedder.")
|
|
736
|
+
mapped = mapped.replacingOccurrences(of: "context_embedder.", with: "contextEmbedder.")
|
|
737
|
+
mapped = mapped.replacingOccurrences(of: "contextEmbedder.", with: "contextEmbedder.")
|
|
738
|
+
mapped = mapped.replacingOccurrences(of: "time_text_embed.linear_1.", with: "timeGuidanceEmbed.timestepEmbedder.linear1.")
|
|
739
|
+
mapped = mapped.replacingOccurrences(of: "time_text_embed.linear_2.", with: "timeGuidanceEmbed.timestepEmbedder.linear2.")
|
|
740
|
+
if hasExplicitTimeTextEmbed {
|
|
741
|
+
mapped = mapped.replacingOccurrences(of: "time_guidance_embed.linear_1.", with: "timeGuidanceEmbed.guidanceEmbedder.linear1.")
|
|
742
|
+
mapped = mapped.replacingOccurrences(of: "time_guidance_embed.linear_2.", with: "timeGuidanceEmbed.guidanceEmbedder.linear2.")
|
|
743
|
+
} else {
|
|
744
|
+
mapped = mapped.replacingOccurrences(of: "time_guidance_embed.linear_1.", with: "timeGuidanceEmbed.timestepEmbedder.linear1.")
|
|
745
|
+
mapped = mapped.replacingOccurrences(of: "time_guidance_embed.linear_2.", with: "timeGuidanceEmbed.timestepEmbedder.linear2.")
|
|
746
|
+
}
|
|
747
|
+
mapped = mapped.replacingOccurrences(of: "time_text_embed.", with: "timeGuidanceEmbed.")
|
|
748
|
+
mapped = mapped.replacingOccurrences(of: "time_guidance_embed.", with: "timeGuidanceEmbed.")
|
|
749
|
+
|
|
750
|
+
// Map timestep embedder
|
|
751
|
+
mapped = mapped.replacingOccurrences(of: "timestep_embedder.", with: "timestepEmbedder.")
|
|
752
|
+
mapped = mapped.replacingOccurrences(of: "guidance_embedder.", with: "guidanceEmbedder.")
|
|
753
|
+
mapped = mapped.replacingOccurrences(of: "linear_1.", with: "linear1.")
|
|
754
|
+
mapped = mapped.replacingOccurrences(of: "linear_2.", with: "linear2.")
|
|
755
|
+
|
|
756
|
+
// Map modulation layers
|
|
757
|
+
mapped = mapped.replacingOccurrences(of: "double_stream_modulation_img.", with: "doubleStreamModulationImg.")
|
|
758
|
+
mapped = mapped.replacingOccurrences(of: "double_stream_modulation_txt.", with: "doubleStreamModulationTxt.")
|
|
759
|
+
mapped = mapped.replacingOccurrences(of: "single_stream_modulation.", with: "singleStreamModulation.")
|
|
760
|
+
|
|
761
|
+
// Map output
|
|
762
|
+
mapped = mapped.replacingOccurrences(of: "norm_out.", with: "normOut.")
|
|
763
|
+
mapped = mapped.replacingOccurrences(of: "proj_out.", with: "projOut.")
|
|
764
|
+
mapped = mapped.replacingOccurrences(of: "normOut.", with: "normOut.")
|
|
765
|
+
mapped = mapped.replacingOccurrences(of: "projOut.", with: "projOut.")
|
|
766
|
+
|
|
767
|
+
return mapped
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// MARK: - VAE Weight Mapping
|
|
771
|
+
|
|
772
|
+
/// Convert HuggingFace VAE weight keys to Swift module paths
|
|
773
|
+
/// Also transposes Conv2d weights from PyTorch OIHW format to MLX OHWI format
|
|
774
|
+
public static func mapVAEWeights(_ weights: [String: MLXArray]) -> [String: MLXArray] {
|
|
775
|
+
var mapped: [String: MLXArray] = [:]
|
|
776
|
+
|
|
777
|
+
for (key, var value) in weights {
|
|
778
|
+
var newKey = key
|
|
779
|
+
|
|
780
|
+
// Transpose Conv2d weights from PyTorch OIHW to MLX OHWI format
|
|
781
|
+
// Conv2d weights have 4 dimensions and key ends with ".weight"
|
|
782
|
+
// PyTorch: [out_channels, in_channels, kernel_h, kernel_w]
|
|
783
|
+
// MLX: [out_channels, kernel_h, kernel_w, in_channels]
|
|
784
|
+
if key.hasSuffix(".weight") && value.ndim == 4 {
|
|
785
|
+
value = value.transposed(0, 2, 3, 1) // OIHW -> OHWI
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Map encoder/decoder
|
|
789
|
+
newKey = newKey.replacingOccurrences(of: "encoder.conv_in.", with: "encoder.convIn.")
|
|
790
|
+
newKey = newKey.replacingOccurrences(of: "encoder.conv_out.", with: "encoder.convOut.")
|
|
791
|
+
newKey = newKey.replacingOccurrences(of: "encoder.conv_norm_out.", with: "encoder.convNormOut.")
|
|
792
|
+
newKey = newKey.replacingOccurrences(of: "decoder.conv_in.", with: "decoder.convIn.")
|
|
793
|
+
newKey = newKey.replacingOccurrences(of: "decoder.conv_out.", with: "decoder.convOut.")
|
|
794
|
+
newKey = newKey.replacingOccurrences(of: "decoder.conv_norm_out.", with: "decoder.convNormOut.")
|
|
795
|
+
|
|
796
|
+
// Map down/up blocks
|
|
797
|
+
newKey = newKey.replacingOccurrences(of: "down_blocks.", with: "downBlocks.")
|
|
798
|
+
newKey = newKey.replacingOccurrences(of: "up_blocks.", with: "upBlocks.")
|
|
799
|
+
|
|
800
|
+
// Map mid_block - special handling for tuple structure
|
|
801
|
+
// mid_block.resnets.0 -> midBlock.0 (resnet1)
|
|
802
|
+
// mid_block.attentions.0 -> midBlock.1 (attention)
|
|
803
|
+
// mid_block.resnets.1 -> midBlock.2 (resnet2)
|
|
804
|
+
// Apply to both encoder and decoder
|
|
805
|
+
newKey = newKey.replacingOccurrences(of: "encoder.mid_block.resnets.0.", with: "encoder.midBlock.0.")
|
|
806
|
+
newKey = newKey.replacingOccurrences(of: "encoder.mid_block.attentions.0.", with: "encoder.midBlock.1.")
|
|
807
|
+
newKey = newKey.replacingOccurrences(of: "encoder.mid_block.resnets.1.", with: "encoder.midBlock.2.")
|
|
808
|
+
newKey = newKey.replacingOccurrences(of: "decoder.mid_block.resnets.0.", with: "decoder.midBlock.0.")
|
|
809
|
+
newKey = newKey.replacingOccurrences(of: "decoder.mid_block.attentions.0.", with: "decoder.midBlock.1.")
|
|
810
|
+
newKey = newKey.replacingOccurrences(of: "decoder.mid_block.resnets.1.", with: "decoder.midBlock.2.")
|
|
811
|
+
|
|
812
|
+
// Map ResNet blocks in down/up blocks
|
|
813
|
+
// The model uses tuples: (blocks: [ResnetBlock2D], upsample/downsample: Upsample2D?)
|
|
814
|
+
// This flattens as: upBlocks.{idx}.0.{resnet_idx} for blocks, upBlocks.{idx}.1 for upsample
|
|
815
|
+
// Checkpoint uses: up_blocks.{idx}.resnets.{resnet_idx}
|
|
816
|
+
newKey = newKey.replacingOccurrences(of: "resnets.", with: "0.")
|
|
817
|
+
newKey = newKey.replacingOccurrences(of: "conv_shortcut.", with: "convShortcut.")
|
|
818
|
+
|
|
819
|
+
// Map upsamplers and downsamplers
|
|
820
|
+
// upsamplers.0.conv -> 1.conv (position 1 in tuple)
|
|
821
|
+
// downsamplers.0.conv -> 1.conv (position 1 in tuple)
|
|
822
|
+
newKey = newKey.replacingOccurrences(of: "upsamplers.0.conv.", with: "1.conv.")
|
|
823
|
+
newKey = newKey.replacingOccurrences(of: "downsamplers.0.conv.", with: "1.conv.")
|
|
824
|
+
|
|
825
|
+
// Map attention
|
|
826
|
+
newKey = newKey.replacingOccurrences(of: "attentions.", with: "attentions.")
|
|
827
|
+
newKey = newKey.replacingOccurrences(of: "group_norm.", with: "groupNorm.")
|
|
828
|
+
newKey = newKey.replacingOccurrences(of: "to_q.", with: "toQ.")
|
|
829
|
+
newKey = newKey.replacingOccurrences(of: "to_k.", with: "toK.")
|
|
830
|
+
newKey = newKey.replacingOccurrences(of: "to_v.", with: "toV.")
|
|
831
|
+
newKey = newKey.replacingOccurrences(of: "to_out.0.", with: "toOut.") // VAE attention has indexed to_out
|
|
832
|
+
newKey = newKey.replacingOccurrences(of: "to_out.", with: "toOut.")
|
|
833
|
+
|
|
834
|
+
// Map quant conv - order matters! More specific patterns first
|
|
835
|
+
newKey = newKey.replacingOccurrences(of: "post_quant_conv.", with: "postQuantConv.")
|
|
836
|
+
newKey = newKey.replacingOccurrences(of: "quant_conv.", with: "quantConv.")
|
|
837
|
+
|
|
838
|
+
// Map batch norm (bn -> latentBatchNorm)
|
|
839
|
+
newKey = newKey.replacingOccurrences(of: "bn.", with: "latentBatchNorm.")
|
|
840
|
+
newKey = newKey.replacingOccurrences(of: "running_mean", with: "runningMean")
|
|
841
|
+
newKey = newKey.replacingOccurrences(of: "running_var", with: "runningVar")
|
|
842
|
+
newKey = newKey.replacingOccurrences(of: "num_batches_tracked", with: "numBatchesTracked")
|
|
843
|
+
|
|
844
|
+
mapped[newKey] = value
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return mapped
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// MARK: - Weight Application
|
|
851
|
+
|
|
852
|
+
/// Apply weights to a transformer model
|
|
853
|
+
/// Note: `weights` is passed inout — for Diffusers quanto format, entries are freed during mapping
|
|
854
|
+
/// to reduce peak memory from ~98GB to ~71GB.
|
|
855
|
+
public static func applyTransformerWeights(
|
|
856
|
+
_ weights: inout [String: MLXArray],
|
|
857
|
+
to model: Flux2Transformer2DModel,
|
|
858
|
+
profile: Flux2TransformerRepoProfile
|
|
859
|
+
) throws {
|
|
860
|
+
let mapped = mapTransformerWeights(&weights, profile: profile)
|
|
861
|
+
|
|
862
|
+
// Use MLX's built-in weight loading - flatten to get full paths
|
|
863
|
+
let flattenedArray = model.parameters().flattened()
|
|
864
|
+
var flatParameters: [String: MLXArray] = [:]
|
|
865
|
+
for (key, value) in flattenedArray {
|
|
866
|
+
flatParameters[key] = value
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
var updates: [String: MLXArray] = [:]
|
|
870
|
+
var moduleUpdates: [String: Module] = [:]
|
|
871
|
+
var packedModulePaths = Set<String>()
|
|
872
|
+
var notFound = 0
|
|
873
|
+
|
|
874
|
+
// Debug: print flattened model parameter keys by category
|
|
875
|
+
let modelTransformerBlocks = flatParameters.keys.filter { $0.hasPrefix("transformerBlocks.") }
|
|
876
|
+
let modelSingleBlocks = flatParameters.keys.filter { $0.hasPrefix("singleTransformerBlocks.") }
|
|
877
|
+
let modelTimeEmbed = flatParameters.keys.filter { $0.contains("timeGuidanceEmbed") || $0.contains("xEmbedder") }
|
|
878
|
+
Flux2Debug.verbose("Model parameter keys:")
|
|
879
|
+
Flux2Debug.verbose(" - transformerBlocks params: \(modelTransformerBlocks.count)")
|
|
880
|
+
Flux2Debug.verbose(" - singleTransformerBlocks params: \(modelSingleBlocks.count)")
|
|
881
|
+
Flux2Debug.verbose(" - time/embedder params: \(modelTimeEmbed.count)")
|
|
882
|
+
Flux2Debug.verbose(" - total params: \(flatParameters.count)")
|
|
883
|
+
for key in modelTimeEmbed.sorted() {
|
|
884
|
+
Flux2Debug.verbose(" \(key)")
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
var linearModules: [String: Linear] = [:]
|
|
888
|
+
for (path, module) in model.leafModules().flattened() {
|
|
889
|
+
if let linear = module as? Linear {
|
|
890
|
+
linearModules[path] = linear
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
var dequantizedWeights: [String: MLXArray] = [:]
|
|
895
|
+
var packedRuntimeCount = 0
|
|
896
|
+
var denseFallbackCount = 0
|
|
897
|
+
var unsupportedFallbackBits = Set<Int>()
|
|
898
|
+
let requiresBonsaiTernaryPackedRuntime = isBonsaiTernaryProfile(profile)
|
|
899
|
+
|
|
900
|
+
for (key, value) in mapped where value.dtype == .uint32 {
|
|
901
|
+
guard let scales = mapped["\(key.dropLast(6))scales"],
|
|
902
|
+
let biases = mapped["\(key.dropLast(6))biases"] else {
|
|
903
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
904
|
+
"Packed transformer weight \(key) is missing matching scales/biases tensors for \(profile.repoId)"
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
guard let expectedShape = flatParameters[key]?.shape else {
|
|
909
|
+
if requiresBonsaiTernaryPackedRuntime {
|
|
910
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
911
|
+
"Bonsai ternary packed tensor \(key) is not present in the instantiated Klein transformer for \(profile.repoId)"
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
Flux2Debug.warning(
|
|
916
|
+
"Skipping extra packed transformer tensor \(key) not present in instantiated model for \(profile.repoId)"
|
|
917
|
+
)
|
|
918
|
+
continue
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
guard let bits = inferQuantizationBits(packedShape: value.shape, expectedShape: expectedShape),
|
|
922
|
+
let groupSize = inferGroupSize(expectedShape: expectedShape, scalesShape: scales.shape) else {
|
|
923
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
924
|
+
"Unable to infer quantization metadata for \(key): packed=\(value.shape) scales=\(scales.shape) expected=\(expectedShape)"
|
|
925
|
+
)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if requiresBonsaiTernaryPackedRuntime && (bits != 2 || groupSize != 128) {
|
|
929
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
930
|
+
"Bonsai ternary requires packed 2-bit/groupSize=128 runtime weights, got bits=\(bits), groupSize=\(groupSize) for \(key) in \(profile.repoId)"
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
let modulePath = parameterModulePath(for: key)
|
|
935
|
+
if supportsPackedAffineRuntime(bits: bits), let linear = linearModules[modulePath] {
|
|
936
|
+
let quantizedLinear = QuantizedLinear(
|
|
937
|
+
weight: value,
|
|
938
|
+
bias: linear.bias,
|
|
939
|
+
scales: scales,
|
|
940
|
+
biases: biases,
|
|
941
|
+
groupSize: groupSize,
|
|
942
|
+
bits: bits,
|
|
943
|
+
mode: .affine
|
|
944
|
+
)
|
|
945
|
+
moduleUpdates[modulePath] = quantizedLinear
|
|
946
|
+
packedModulePaths.insert(modulePath)
|
|
947
|
+
packedRuntimeCount += 1
|
|
948
|
+
continue
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if supportsPackedAffineRuntime(bits: bits) {
|
|
952
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
953
|
+
"Packed transformer weight \(key) inferred as \(bits)-bit/groupSize=\(groupSize), but no matching Linear module exists at \(modulePath) for \(profile.repoId)"
|
|
954
|
+
)
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if !supportsPackedAffineRuntime(bits: bits) {
|
|
958
|
+
if requiresBonsaiTernaryPackedRuntime {
|
|
959
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
960
|
+
"Bonsai ternary requires packed 2-bit MLX affine runtime; \(key) inferred unsupported bits=\(bits) for \(profile.repoId)"
|
|
961
|
+
)
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
unsupportedFallbackBits.insert(bits)
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
let dense = maybeSwapNormOutRows(
|
|
968
|
+
dequantized(
|
|
969
|
+
value,
|
|
970
|
+
scales: scales,
|
|
971
|
+
biases: biases,
|
|
972
|
+
groupSize: groupSize,
|
|
973
|
+
bits: bits
|
|
974
|
+
).asType(.float16),
|
|
975
|
+
mappedKey: key,
|
|
976
|
+
profile: profile
|
|
977
|
+
)
|
|
978
|
+
dequantizedWeights[key] = dense
|
|
979
|
+
denseFallbackCount += 1
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if !moduleUpdates.isEmpty {
|
|
983
|
+
model.update(modules: ModuleChildren.unflattened(moduleUpdates))
|
|
984
|
+
Flux2Debug.log("Loaded \(packedRuntimeCount) packed transformer weights as QuantizedLinear modules")
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if !unsupportedFallbackBits.isEmpty {
|
|
988
|
+
let bits = unsupportedFallbackBits.sorted().map(String.init).joined(separator: ", ")
|
|
989
|
+
Flux2Debug.warning(
|
|
990
|
+
"Packed transformer uses unsupported affine runtime bits [\(bits)]; falling back to dense float16 for \(profile.repoId)"
|
|
991
|
+
)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if !dequantizedWeights.isEmpty {
|
|
995
|
+
Flux2Debug.log("Dequantized \(denseFallbackCount) packed transformer weights to float16 fallback")
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if requiresBonsaiTernaryPackedRuntime {
|
|
999
|
+
guard denseFallbackCount == 0 else {
|
|
1000
|
+
throw Flux2WeightLoaderError.weightMismatch(
|
|
1001
|
+
"Bonsai ternary must not dequantize packed transformer weights; \(denseFallbackCount) weights fell back to dense for \(profile.repoId)"
|
|
1002
|
+
)
|
|
1003
|
+
}
|
|
1004
|
+
Flux2Debug.log("Bonsai ternary packed runtime verified: \(packedRuntimeCount) 2-bit/groupSize=128 QuantizedLinear modules")
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
for (key, value) in mapped {
|
|
1008
|
+
if key.hasSuffix(".scales") || key.hasSuffix(".biases") {
|
|
1009
|
+
continue
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if packedModulePaths.contains(parameterModulePath(for: key)) {
|
|
1013
|
+
continue
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
let effectiveValue: MLXArray
|
|
1017
|
+
if let dequantized = dequantizedWeights[key] {
|
|
1018
|
+
effectiveValue = dequantized
|
|
1019
|
+
} else {
|
|
1020
|
+
effectiveValue = maybeSwapNormOutRows(value, mappedKey: key, profile: profile)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if flatParameters.keys.contains(key) {
|
|
1024
|
+
updates[key] = effectiveValue
|
|
1025
|
+
} else {
|
|
1026
|
+
notFound += 1
|
|
1027
|
+
if notFound <= 10 {
|
|
1028
|
+
Flux2Debug.log("Warning: No parameter found for key: \(key)")
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if notFound > 10 {
|
|
1034
|
+
Flux2Debug.log("... and \(notFound - 10) more missing parameters")
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Update model with new weights using the flattened format
|
|
1038
|
+
try validateCriticalTransformerCoverage(
|
|
1039
|
+
updates: updates,
|
|
1040
|
+
packedModulePaths: packedModulePaths,
|
|
1041
|
+
flatParameters: flatParameters,
|
|
1042
|
+
profile: profile
|
|
1043
|
+
)
|
|
1044
|
+
_ = model.update(parameters: ModuleParameters.unflattened(updates))
|
|
1045
|
+
|
|
1046
|
+
// Debug: print some weight shapes
|
|
1047
|
+
for (key, value) in updates.sorted(by: { $0.key < $1.key }).prefix(10) {
|
|
1048
|
+
Flux2Debug.log(" Weight \(key): shape=\(value.shape), dtype=\(value.dtype)")
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
Flux2Debug.log("Applied \(updates.count) weights to transformer (\(notFound) not found)")
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/// Apply weights to a VAE model
|
|
1055
|
+
public static func applyVAEWeights(
|
|
1056
|
+
_ weights: [String: MLXArray],
|
|
1057
|
+
to model: AutoencoderKLFlux2
|
|
1058
|
+
) throws {
|
|
1059
|
+
let mapped = mapVAEWeights(weights)
|
|
1060
|
+
|
|
1061
|
+
// Check for BatchNorm running stats
|
|
1062
|
+
if let runningMean = mapped["latentBatchNorm.runningMean"],
|
|
1063
|
+
let runningVar = mapped["latentBatchNorm.runningVar"] {
|
|
1064
|
+
model.loadBatchNormStats(runningMean: runningMean, runningVar: runningVar)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Flatten model parameters for key matching
|
|
1068
|
+
let flattenedArray = model.parameters().flattened()
|
|
1069
|
+
var flatParameters: [String: MLXArray] = [:]
|
|
1070
|
+
for (key, value) in flattenedArray {
|
|
1071
|
+
flatParameters[key] = value
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Debug: print some VAE parameter keys
|
|
1075
|
+
Flux2Debug.verbose("VAE model parameter keys (first 20 of \(flatParameters.count) flattened):")
|
|
1076
|
+
for key in flatParameters.keys.sorted().prefix(20) {
|
|
1077
|
+
Flux2Debug.verbose(" - \(key)")
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
var updates: [String: MLXArray] = [:]
|
|
1081
|
+
var notFound = 0
|
|
1082
|
+
|
|
1083
|
+
for (key, value) in mapped {
|
|
1084
|
+
if key.contains("running") { continue } // Skip running stats, handled above
|
|
1085
|
+
|
|
1086
|
+
if flatParameters.keys.contains(key) {
|
|
1087
|
+
updates[key] = value
|
|
1088
|
+
} else {
|
|
1089
|
+
notFound += 1
|
|
1090
|
+
if notFound <= 10 {
|
|
1091
|
+
Flux2Debug.log("VAE Warning: No parameter found for key: \(key)")
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if notFound > 10 {
|
|
1097
|
+
Flux2Debug.log("... and \(notFound - 10) more missing VAE parameters")
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
_ = model.update(parameters: ModuleParameters.unflattened(updates))
|
|
1101
|
+
|
|
1102
|
+
Flux2Debug.log("Applied \(updates.count) weights to VAE (\(notFound) not found)")
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// MARK: - Quantized Weight Loading
|
|
1106
|
+
|
|
1107
|
+
/// Load quantized transformer weights
|
|
1108
|
+
public static func loadQuantizedTransformer(
|
|
1109
|
+
from modelPath: String,
|
|
1110
|
+
quantization: TransformerQuantization
|
|
1111
|
+
) throws -> [String: MLXArray] {
|
|
1112
|
+
let weights = try loadWeights(from: modelPath)
|
|
1113
|
+
|
|
1114
|
+
// For qint8, weights may already be quantized in safetensors
|
|
1115
|
+
// If not, we need to quantize them here
|
|
1116
|
+
if quantization != .bf16 {
|
|
1117
|
+
Flux2Debug.log("Loading \(quantization.rawValue) quantized weights")
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return weights
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// MARK: - Verification
|
|
1124
|
+
|
|
1125
|
+
/// Verify that all required weights are present
|
|
1126
|
+
public static func verifyTransformerWeights(_ weights: [String: MLXArray]) -> (complete: Bool, missing: [String]) {
|
|
1127
|
+
let requiredPrefixes = [
|
|
1128
|
+
"xEmbedder",
|
|
1129
|
+
"contextEmbedder",
|
|
1130
|
+
"timeGuidanceEmbed",
|
|
1131
|
+
"transformerBlocks.0",
|
|
1132
|
+
"singleTransformerBlocks.0",
|
|
1133
|
+
"normOut",
|
|
1134
|
+
"projOut"
|
|
1135
|
+
]
|
|
1136
|
+
|
|
1137
|
+
var missing: [String] = []
|
|
1138
|
+
|
|
1139
|
+
for prefix in requiredPrefixes {
|
|
1140
|
+
let hasPrefix = weights.keys.contains { $0.hasPrefix(prefix) }
|
|
1141
|
+
if !hasPrefix {
|
|
1142
|
+
missing.append(prefix)
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
return (missing.isEmpty, missing)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/// Get summary of loaded weights
|
|
1150
|
+
public static func summarizeWeights(_ weights: [String: MLXArray]) {
|
|
1151
|
+
var totalParams: Int64 = 0
|
|
1152
|
+
var byPrefix: [String: Int64] = [:]
|
|
1153
|
+
|
|
1154
|
+
for (key, array) in weights {
|
|
1155
|
+
let params = Int64(array.shape.reduce(1, *))
|
|
1156
|
+
totalParams += params
|
|
1157
|
+
|
|
1158
|
+
// Group by first component
|
|
1159
|
+
let prefix = String(key.split(separator: ".").first ?? Substring(key))
|
|
1160
|
+
byPrefix[prefix, default: 0] += params
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
Flux2Debug.log("Weight Summary:")
|
|
1164
|
+
for (prefix, params) in byPrefix.sorted(by: { $0.value > $1.value }) {
|
|
1165
|
+
let gb = Float(params * 2) / 1_000_000_000 // bf16 = 2 bytes
|
|
1166
|
+
Flux2Debug.log(" \(prefix): \(params) params (~\(String(format: "%.2f", gb))GB)")
|
|
1167
|
+
}
|
|
1168
|
+
Flux2Debug.log("Total: \(totalParams) parameters")
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// MARK: - Errors
|
|
1173
|
+
|
|
1174
|
+
public enum Flux2WeightLoaderError: LocalizedError {
|
|
1175
|
+
case noWeightsFound(String)
|
|
1176
|
+
case weightMismatch(String)
|
|
1177
|
+
case fileNotFound(String)
|
|
1178
|
+
case incompatibleShape(expected: [Int], got: [Int], key: String)
|
|
1179
|
+
|
|
1180
|
+
public var errorDescription: String? {
|
|
1181
|
+
switch self {
|
|
1182
|
+
case .noWeightsFound(let path):
|
|
1183
|
+
return "No safetensors files found in: \(path)"
|
|
1184
|
+
case .weightMismatch(let message):
|
|
1185
|
+
return "Weight mismatch: \(message)"
|
|
1186
|
+
case .fileNotFound(let path):
|
|
1187
|
+
return "File not found: \(path)"
|
|
1188
|
+
case .incompatibleShape(let expected, let got, let key):
|
|
1189
|
+
return "Incompatible shape for \(key): expected \(expected), got \(got)"
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|