bleam 0.0.8 → 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-Ce-pnUUA.d.ts → 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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// ResnetBlock.swift - ResNet blocks for VAE
|
|
2
|
+
// Copyright 2025 Vincent Gourbin
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MLX
|
|
6
|
+
import MLXNN
|
|
7
|
+
|
|
8
|
+
/// Group Normalization for VAE (NHWC format for MLX)
|
|
9
|
+
public class FluxGroupNorm: Module, @unchecked Sendable {
|
|
10
|
+
let numGroups: Int
|
|
11
|
+
let numChannels: Int
|
|
12
|
+
let eps: Float
|
|
13
|
+
let weight: MLXArray
|
|
14
|
+
let bias: MLXArray
|
|
15
|
+
|
|
16
|
+
public init(numGroups: Int, numChannels: Int, eps: Float = 1e-6) {
|
|
17
|
+
self.numGroups = numGroups
|
|
18
|
+
self.numChannels = numChannels
|
|
19
|
+
self.eps = eps
|
|
20
|
+
self.weight = MLXArray.ones([numChannels])
|
|
21
|
+
self.bias = MLXArray.zeros([numChannels])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
25
|
+
// x shape: [B, H, W, C] (NHWC format for MLX)
|
|
26
|
+
let shape = x.shape
|
|
27
|
+
let B = shape[0]
|
|
28
|
+
let H = shape[1]
|
|
29
|
+
let W = shape[2]
|
|
30
|
+
let C = shape[3]
|
|
31
|
+
|
|
32
|
+
// Mixed precision: compute statistics in Float32 for numerical stability
|
|
33
|
+
let inputDtype = x.dtype
|
|
34
|
+
let xFloat = x.asType(.float32)
|
|
35
|
+
|
|
36
|
+
// Reshape to [B, H, W, G, C/G]
|
|
37
|
+
let reshaped = xFloat.reshaped([B, H, W, numGroups, C / numGroups])
|
|
38
|
+
|
|
39
|
+
// Compute mean and variance per group (over H, W, and C/G within each group)
|
|
40
|
+
let mean = MLX.mean(reshaped, axes: [1, 2, 4], keepDims: true)
|
|
41
|
+
let variance = MLX.mean(pow(reshaped - mean, 2), axes: [1, 2, 4], keepDims: true)
|
|
42
|
+
|
|
43
|
+
// Normalize in Float32
|
|
44
|
+
let normalized = (reshaped - mean) / sqrt(variance + eps)
|
|
45
|
+
|
|
46
|
+
// Reshape back to [B, H, W, C] and convert to original dtype
|
|
47
|
+
let out = normalized.reshaped([B, H, W, C]).asType(inputDtype)
|
|
48
|
+
|
|
49
|
+
// Apply weight and bias [1, 1, 1, C] for NHWC
|
|
50
|
+
let weightReshaped = weight.reshaped([1, 1, 1, C])
|
|
51
|
+
let biasReshaped = bias.reshaped([1, 1, 1, C])
|
|
52
|
+
|
|
53
|
+
return out * weightReshaped + biasReshaped
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Batch Normalization for VAE latent space (Flux.2 specific) - NHWC format for MLX
|
|
58
|
+
public class BatchNorm2d: Module, @unchecked Sendable {
|
|
59
|
+
let numFeatures: Int
|
|
60
|
+
let eps: Float
|
|
61
|
+
let momentum: Float
|
|
62
|
+
let trackRunningStats: Bool
|
|
63
|
+
|
|
64
|
+
let weight: MLXArray
|
|
65
|
+
let bias: MLXArray
|
|
66
|
+
var runningMean: MLXArray
|
|
67
|
+
var runningVar: MLXArray
|
|
68
|
+
|
|
69
|
+
public init(
|
|
70
|
+
numFeatures: Int,
|
|
71
|
+
eps: Float = 1e-5,
|
|
72
|
+
momentum: Float = 0.1,
|
|
73
|
+
trackRunningStats: Bool = true
|
|
74
|
+
) {
|
|
75
|
+
self.numFeatures = numFeatures
|
|
76
|
+
self.eps = eps
|
|
77
|
+
self.momentum = momentum
|
|
78
|
+
self.trackRunningStats = trackRunningStats
|
|
79
|
+
|
|
80
|
+
self.weight = MLXArray.ones([numFeatures])
|
|
81
|
+
self.bias = MLXArray.zeros([numFeatures])
|
|
82
|
+
self.runningMean = MLXArray.zeros([numFeatures])
|
|
83
|
+
self.runningVar = MLXArray.ones([numFeatures])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public func callAsFunction(_ x: MLXArray, training: Bool = false) -> MLXArray {
|
|
87
|
+
// x shape: [B, H, W, C] (NHWC format for MLX)
|
|
88
|
+
let shape = x.shape
|
|
89
|
+
let C = shape[3]
|
|
90
|
+
|
|
91
|
+
// Mixed precision: compute statistics in Float32 for numerical stability
|
|
92
|
+
let inputDtype = x.dtype
|
|
93
|
+
let xFloat = x.asType(.float32)
|
|
94
|
+
|
|
95
|
+
if training {
|
|
96
|
+
// Compute batch statistics (over B, H, W) in Float32
|
|
97
|
+
let mean = MLX.mean(xFloat, axes: [0, 1, 2])
|
|
98
|
+
let variance = MLX.mean(pow(xFloat - mean.reshaped([1, 1, 1, C]), 2), axes: [0, 1, 2])
|
|
99
|
+
|
|
100
|
+
// Update running stats (keep in Float32)
|
|
101
|
+
if trackRunningStats {
|
|
102
|
+
runningMean = (1 - momentum) * runningMean + momentum * mean
|
|
103
|
+
runningVar = (1 - momentum) * runningVar + momentum * variance
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Normalize with batch stats in Float32, then convert back
|
|
107
|
+
let normalized = (xFloat - mean.reshaped([1, 1, 1, C])) / sqrt(variance.reshaped([1, 1, 1, C]) + eps)
|
|
108
|
+
let out = normalized.asType(inputDtype)
|
|
109
|
+
return out * weight.reshaped([1, 1, 1, C]) + bias.reshaped([1, 1, 1, C])
|
|
110
|
+
} else {
|
|
111
|
+
// Use running stats - normalize in Float32
|
|
112
|
+
let runningMeanFloat = runningMean.asType(.float32)
|
|
113
|
+
let runningVarFloat = runningVar.asType(.float32)
|
|
114
|
+
let normalized = (xFloat - runningMeanFloat.reshaped([1, 1, 1, C])) / sqrt(runningVarFloat.reshaped([1, 1, 1, C]) + eps)
|
|
115
|
+
let out = normalized.asType(inputDtype)
|
|
116
|
+
return out * weight.reshaped([1, 1, 1, C]) + bias.reshaped([1, 1, 1, C])
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// ResNet block for VAE encoder/decoder
|
|
122
|
+
public class ResnetBlock2D: Module, @unchecked Sendable {
|
|
123
|
+
let inChannels: Int
|
|
124
|
+
let outChannels: Int
|
|
125
|
+
|
|
126
|
+
let norm1: FluxGroupNorm
|
|
127
|
+
let conv1: Conv2d
|
|
128
|
+
let norm2: FluxGroupNorm
|
|
129
|
+
let conv2: Conv2d
|
|
130
|
+
let convShortcut: Conv2d?
|
|
131
|
+
|
|
132
|
+
public init(
|
|
133
|
+
inChannels: Int,
|
|
134
|
+
outChannels: Int? = nil,
|
|
135
|
+
numGroups: Int = 32
|
|
136
|
+
) {
|
|
137
|
+
self.inChannels = inChannels
|
|
138
|
+
self.outChannels = outChannels ?? inChannels
|
|
139
|
+
|
|
140
|
+
self.norm1 = FluxGroupNorm(numGroups: numGroups, numChannels: inChannels)
|
|
141
|
+
self.conv1 = Conv2d(
|
|
142
|
+
inputChannels: inChannels,
|
|
143
|
+
outputChannels: self.outChannels,
|
|
144
|
+
kernelSize: 3,
|
|
145
|
+
padding: 1
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
self.norm2 = FluxGroupNorm(numGroups: numGroups, numChannels: self.outChannels)
|
|
149
|
+
self.conv2 = Conv2d(
|
|
150
|
+
inputChannels: self.outChannels,
|
|
151
|
+
outputChannels: self.outChannels,
|
|
152
|
+
kernelSize: 3,
|
|
153
|
+
padding: 1
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Shortcut projection if channels change
|
|
157
|
+
if inChannels != self.outChannels {
|
|
158
|
+
self.convShortcut = Conv2d(
|
|
159
|
+
inputChannels: inChannels,
|
|
160
|
+
outputChannels: self.outChannels,
|
|
161
|
+
kernelSize: 1
|
|
162
|
+
)
|
|
163
|
+
} else {
|
|
164
|
+
self.convShortcut = nil
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
169
|
+
var hidden = norm1(x)
|
|
170
|
+
hidden = silu(hidden)
|
|
171
|
+
hidden = conv1(hidden)
|
|
172
|
+
|
|
173
|
+
hidden = norm2(hidden)
|
|
174
|
+
hidden = silu(hidden)
|
|
175
|
+
hidden = conv2(hidden)
|
|
176
|
+
|
|
177
|
+
// Shortcut
|
|
178
|
+
let shortcut: MLXArray
|
|
179
|
+
if let convShortcut = convShortcut {
|
|
180
|
+
shortcut = convShortcut(x)
|
|
181
|
+
} else {
|
|
182
|
+
shortcut = x
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return hidden + shortcut
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Downsample block for VAE encoder
|
|
190
|
+
public class Downsample2D: Module, @unchecked Sendable {
|
|
191
|
+
let conv: Conv2d
|
|
192
|
+
|
|
193
|
+
public init(channels: Int, useConv: Bool = true) {
|
|
194
|
+
if useConv {
|
|
195
|
+
self.conv = Conv2d(
|
|
196
|
+
inputChannels: channels,
|
|
197
|
+
outputChannels: channels,
|
|
198
|
+
kernelSize: 3,
|
|
199
|
+
stride: 2
|
|
200
|
+
)
|
|
201
|
+
} else {
|
|
202
|
+
// Average pooling fallback (not typical for VAE)
|
|
203
|
+
self.conv = Conv2d(
|
|
204
|
+
inputChannels: channels,
|
|
205
|
+
outputChannels: channels,
|
|
206
|
+
kernelSize: 3,
|
|
207
|
+
stride: 2
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
213
|
+
let paddedInput = padded(x, widths: [[0, 0], [0, 1], [0, 1], [0, 0]])
|
|
214
|
+
return conv(paddedInput)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// Upsample block for VAE decoder (NHWC format for MLX)
|
|
219
|
+
public class Upsample2D: Module, @unchecked Sendable {
|
|
220
|
+
let conv: Conv2d
|
|
221
|
+
|
|
222
|
+
public init(channels: Int, useConv: Bool = true) {
|
|
223
|
+
self.conv = Conv2d(
|
|
224
|
+
inputChannels: channels,
|
|
225
|
+
outputChannels: channels,
|
|
226
|
+
kernelSize: 3,
|
|
227
|
+
padding: 1
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
232
|
+
// x shape: [B, H, W, C] (NHWC format for MLX)
|
|
233
|
+
let shape = x.shape
|
|
234
|
+
let B = shape[0]
|
|
235
|
+
let H = shape[1]
|
|
236
|
+
let W = shape[2]
|
|
237
|
+
let C = shape[3]
|
|
238
|
+
|
|
239
|
+
// Upsample by 2x using nearest neighbor interpolation
|
|
240
|
+
// The correct approach is to expand dimensions and then reshape:
|
|
241
|
+
// [B, H, W, C] -> [B, H, 1, W, 1, C] -> broadcast to [B, H, 2, W, 2, C] -> [B, H*2, W*2, C]
|
|
242
|
+
|
|
243
|
+
// Reshape to add extra dimensions for broadcasting
|
|
244
|
+
var upsampled = x.reshaped([B, H, 1, W, 1, C])
|
|
245
|
+
|
|
246
|
+
// Tile/broadcast to duplicate each element along H and W
|
|
247
|
+
// Use concatenation to duplicate along each new axis
|
|
248
|
+
upsampled = concatenated([upsampled, upsampled], axis: 2) // [B, H, 2, W, 1, C]
|
|
249
|
+
upsampled = concatenated([upsampled, upsampled], axis: 4) // [B, H, 2, W, 2, C]
|
|
250
|
+
|
|
251
|
+
// Reshape to merge the duplicated dimensions
|
|
252
|
+
upsampled = upsampled.reshaped([B, H * 2, W * 2, C])
|
|
253
|
+
|
|
254
|
+
return conv(upsampled)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Self-attention block for VAE (mid-block) - NHWC format for MLX
|
|
259
|
+
/// Uses Linear layers (as in HuggingFace checkpoint) instead of Conv2d
|
|
260
|
+
public class AttentionBlock: Module, @unchecked Sendable {
|
|
261
|
+
let channels: Int
|
|
262
|
+
let numHeads: Int
|
|
263
|
+
let headDim: Int
|
|
264
|
+
|
|
265
|
+
let groupNorm: FluxGroupNorm
|
|
266
|
+
let toQ: Linear
|
|
267
|
+
let toK: Linear
|
|
268
|
+
let toV: Linear
|
|
269
|
+
let toOut: Linear
|
|
270
|
+
|
|
271
|
+
public init(channels: Int, numHeads: Int = 1, numGroups: Int = 32) {
|
|
272
|
+
self.channels = channels
|
|
273
|
+
self.numHeads = numHeads
|
|
274
|
+
self.headDim = channels / numHeads
|
|
275
|
+
|
|
276
|
+
self.groupNorm = FluxGroupNorm(numGroups: numGroups, numChannels: channels)
|
|
277
|
+
self.toQ = Linear(channels, channels)
|
|
278
|
+
self.toK = Linear(channels, channels)
|
|
279
|
+
self.toV = Linear(channels, channels)
|
|
280
|
+
self.toOut = Linear(channels, channels)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
284
|
+
// x shape: [B, H, W, C] (NHWC format for MLX)
|
|
285
|
+
let residual = x
|
|
286
|
+
let shape = x.shape
|
|
287
|
+
let B = shape[0]
|
|
288
|
+
let H = shape[1]
|
|
289
|
+
let W = shape[2]
|
|
290
|
+
let C = shape[3]
|
|
291
|
+
|
|
292
|
+
// Normalize
|
|
293
|
+
var hidden = groupNorm(x)
|
|
294
|
+
|
|
295
|
+
// Reshape to [B, H*W, C] for Linear projection
|
|
296
|
+
hidden = hidden.reshaped([B, H * W, C])
|
|
297
|
+
|
|
298
|
+
// Project to Q, K, V using Linear layers
|
|
299
|
+
let q = toQ(hidden) // [B, HW, C]
|
|
300
|
+
let k = toK(hidden).transposed(0, 2, 1) // [B, C, HW]
|
|
301
|
+
let v = toV(hidden) // [B, HW, C]
|
|
302
|
+
|
|
303
|
+
// Attention
|
|
304
|
+
let scale = Float(1.0 / sqrt(Float(C)))
|
|
305
|
+
let attnWeights = softmax(matmul(q, k) * scale, axis: -1)
|
|
306
|
+
let attnOut = matmul(attnWeights, v) // [B, HW, C]
|
|
307
|
+
|
|
308
|
+
// Project output
|
|
309
|
+
hidden = toOut(attnOut)
|
|
310
|
+
|
|
311
|
+
// Reshape back to [B, H, W, C]
|
|
312
|
+
hidden = hidden.reshaped([B, H, W, C])
|
|
313
|
+
|
|
314
|
+
return hidden + residual
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// VAEDecoder.swift - VAE Decoder for Flux.2
|
|
2
|
+
// Copyright 2025 Vincent Gourbin
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MLX
|
|
6
|
+
import MLXNN
|
|
7
|
+
|
|
8
|
+
/// VAE Decoder for Flux.2
|
|
9
|
+
///
|
|
10
|
+
/// Decodes latent representations back to RGB images.
|
|
11
|
+
/// Architecture:
|
|
12
|
+
/// - Initial conv: latent_channels -> 512
|
|
13
|
+
/// - Mid block with attention
|
|
14
|
+
/// - Up blocks with ResNet blocks and upsampling
|
|
15
|
+
/// - Final conv: 128 -> 3
|
|
16
|
+
public class VAEDecoder: Module, @unchecked Sendable {
|
|
17
|
+
let config: VAEConfig
|
|
18
|
+
|
|
19
|
+
let convIn: Conv2d
|
|
20
|
+
let midBlock: (resnet1: ResnetBlock2D, attention: AttentionBlock, resnet2: ResnetBlock2D)
|
|
21
|
+
let upBlocks: [(blocks: [ResnetBlock2D], upsample: Upsample2D?)]
|
|
22
|
+
let convNormOut: FluxGroupNorm
|
|
23
|
+
let convOut: Conv2d
|
|
24
|
+
|
|
25
|
+
public init(config: VAEConfig = .flux2Dev) {
|
|
26
|
+
self.config = config
|
|
27
|
+
|
|
28
|
+
let blockOutChannels = config.blockOutChannels // [128, 256, 512, 512]
|
|
29
|
+
let reversedChannels = blockOutChannels.reversed() // [512, 512, 256, 128]
|
|
30
|
+
|
|
31
|
+
// Initial convolution
|
|
32
|
+
self.convIn = Conv2d(
|
|
33
|
+
inputChannels: config.latentChannels,
|
|
34
|
+
outputChannels: blockOutChannels.last!,
|
|
35
|
+
kernelSize: 3,
|
|
36
|
+
padding: 1
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Mid block
|
|
40
|
+
let midChannels = blockOutChannels.last!
|
|
41
|
+
self.midBlock = (
|
|
42
|
+
resnet1: ResnetBlock2D(inChannels: midChannels, numGroups: config.normNumGroups),
|
|
43
|
+
attention: AttentionBlock(channels: midChannels, numGroups: config.normNumGroups),
|
|
44
|
+
resnet2: ResnetBlock2D(inChannels: midChannels, numGroups: config.normNumGroups)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// Up blocks (reversed order)
|
|
48
|
+
var blocks: [(blocks: [ResnetBlock2D], upsample: Upsample2D?)] = []
|
|
49
|
+
var prevChannels = midChannels
|
|
50
|
+
|
|
51
|
+
for (i, outChannels) in Array(reversedChannels).enumerated() {
|
|
52
|
+
var resBlocks: [ResnetBlock2D] = []
|
|
53
|
+
|
|
54
|
+
// Diffusers adds +1 to layers_per_block for ALL up blocks in the decoder
|
|
55
|
+
let numLayers = config.layersPerBlock + 1
|
|
56
|
+
|
|
57
|
+
for j in 0..<numLayers {
|
|
58
|
+
let inCh = (j == 0) ? prevChannels : outChannels
|
|
59
|
+
resBlocks.append(ResnetBlock2D(
|
|
60
|
+
inChannels: inCh,
|
|
61
|
+
outChannels: outChannels,
|
|
62
|
+
numGroups: config.normNumGroups
|
|
63
|
+
))
|
|
64
|
+
}
|
|
65
|
+
prevChannels = outChannels
|
|
66
|
+
|
|
67
|
+
// Upsample except for last block
|
|
68
|
+
let upsample: Upsample2D?
|
|
69
|
+
if i < Array(reversedChannels).count - 1 {
|
|
70
|
+
upsample = Upsample2D(channels: outChannels)
|
|
71
|
+
} else {
|
|
72
|
+
upsample = nil
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
blocks.append((blocks: resBlocks, upsample: upsample))
|
|
76
|
+
}
|
|
77
|
+
self.upBlocks = blocks
|
|
78
|
+
|
|
79
|
+
// Output
|
|
80
|
+
self.convNormOut = FluxGroupNorm(numGroups: config.normNumGroups, numChannels: blockOutChannels[0])
|
|
81
|
+
self.convOut = Conv2d(
|
|
82
|
+
inputChannels: blockOutChannels[0],
|
|
83
|
+
outputChannels: config.outChannels,
|
|
84
|
+
kernelSize: 3,
|
|
85
|
+
padding: 1
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public func callAsFunction(_ z: MLXArray) -> MLXArray {
|
|
90
|
+
// z shape: [B, latent_channels, H/8, W/8] (NCHW from transformer)
|
|
91
|
+
// Convert to NHWC for MLX Conv2d
|
|
92
|
+
var hidden = z.transposed(0, 2, 3, 1) // [B, H/8, W/8, latent_channels]
|
|
93
|
+
|
|
94
|
+
// Initial conv
|
|
95
|
+
hidden = convIn(hidden)
|
|
96
|
+
|
|
97
|
+
// Mid block
|
|
98
|
+
hidden = midBlock.resnet1(hidden)
|
|
99
|
+
hidden = midBlock.attention(hidden)
|
|
100
|
+
hidden = midBlock.resnet2(hidden)
|
|
101
|
+
|
|
102
|
+
// Up blocks
|
|
103
|
+
for (resBlocks, upsample) in upBlocks {
|
|
104
|
+
for resBlock in resBlocks {
|
|
105
|
+
hidden = resBlock(hidden)
|
|
106
|
+
}
|
|
107
|
+
if let us = upsample {
|
|
108
|
+
hidden = us(hidden)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Output
|
|
113
|
+
hidden = convNormOut(hidden)
|
|
114
|
+
hidden = silu(hidden)
|
|
115
|
+
hidden = convOut(hidden)
|
|
116
|
+
|
|
117
|
+
// Convert back to NCHW for output: [B, H, W, 3] -> [B, 3, H, W]
|
|
118
|
+
return hidden.transposed(0, 3, 1, 2)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// VAEEncoder.swift - VAE Encoder for Flux.2
|
|
2
|
+
// Copyright 2025 Vincent Gourbin
|
|
3
|
+
|
|
4
|
+
import Foundation
|
|
5
|
+
import MLX
|
|
6
|
+
import MLXNN
|
|
7
|
+
|
|
8
|
+
/// VAE Encoder for Flux.2
|
|
9
|
+
///
|
|
10
|
+
/// Encodes RGB images to latent space.
|
|
11
|
+
/// Architecture:
|
|
12
|
+
/// - Initial conv: 3 -> 128
|
|
13
|
+
/// - Down blocks with ResNet blocks and downsampling
|
|
14
|
+
/// - Mid block with attention
|
|
15
|
+
/// - Final conv: 512 -> latent_channels * 2 (for mean and variance)
|
|
16
|
+
public class VAEEncoder: Module, @unchecked Sendable {
|
|
17
|
+
let config: VAEConfig
|
|
18
|
+
|
|
19
|
+
let convIn: Conv2d
|
|
20
|
+
let downBlocks: [(blocks: [ResnetBlock2D], downsample: Downsample2D?)]
|
|
21
|
+
let midBlock: (resnet1: ResnetBlock2D, attention: AttentionBlock, resnet2: ResnetBlock2D)
|
|
22
|
+
let convNormOut: FluxGroupNorm
|
|
23
|
+
let convOut: Conv2d
|
|
24
|
+
|
|
25
|
+
public init(config: VAEConfig = .flux2Dev) {
|
|
26
|
+
self.config = config
|
|
27
|
+
|
|
28
|
+
let blockOutChannels = config.blockOutChannels // [128, 256, 512, 512]
|
|
29
|
+
|
|
30
|
+
// Initial convolution
|
|
31
|
+
self.convIn = Conv2d(
|
|
32
|
+
inputChannels: config.inChannels,
|
|
33
|
+
outputChannels: blockOutChannels[0],
|
|
34
|
+
kernelSize: 3,
|
|
35
|
+
padding: 1
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// Down blocks
|
|
39
|
+
var blocks: [(blocks: [ResnetBlock2D], downsample: Downsample2D?)] = []
|
|
40
|
+
var prevChannels = blockOutChannels[0]
|
|
41
|
+
|
|
42
|
+
for (i, outChannels) in blockOutChannels.enumerated() {
|
|
43
|
+
var resBlocks: [ResnetBlock2D] = []
|
|
44
|
+
|
|
45
|
+
for _ in 0..<config.layersPerBlock {
|
|
46
|
+
resBlocks.append(ResnetBlock2D(
|
|
47
|
+
inChannels: prevChannels,
|
|
48
|
+
outChannels: outChannels,
|
|
49
|
+
numGroups: config.normNumGroups
|
|
50
|
+
))
|
|
51
|
+
prevChannels = outChannels
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Downsample except for last block
|
|
55
|
+
let downsample: Downsample2D?
|
|
56
|
+
if i < blockOutChannels.count - 1 {
|
|
57
|
+
downsample = Downsample2D(channels: outChannels)
|
|
58
|
+
} else {
|
|
59
|
+
downsample = nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
blocks.append((blocks: resBlocks, downsample: downsample))
|
|
63
|
+
}
|
|
64
|
+
self.downBlocks = blocks
|
|
65
|
+
|
|
66
|
+
// Mid block
|
|
67
|
+
let midChannels = blockOutChannels.last!
|
|
68
|
+
self.midBlock = (
|
|
69
|
+
resnet1: ResnetBlock2D(inChannels: midChannels, numGroups: config.normNumGroups),
|
|
70
|
+
attention: AttentionBlock(channels: midChannels, numGroups: config.normNumGroups),
|
|
71
|
+
resnet2: ResnetBlock2D(inChannels: midChannels, numGroups: config.normNumGroups)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// Output
|
|
75
|
+
self.convNormOut = FluxGroupNorm(numGroups: config.normNumGroups, numChannels: midChannels)
|
|
76
|
+
// Output channels: 2 * latent_channels for mean and log_variance
|
|
77
|
+
self.convOut = Conv2d(
|
|
78
|
+
inputChannels: midChannels,
|
|
79
|
+
outputChannels: config.latentChannels * 2,
|
|
80
|
+
kernelSize: 3,
|
|
81
|
+
padding: 1
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
86
|
+
// x shape: [B, 3, H, W] (NCHW from input)
|
|
87
|
+
// Convert to NHWC for MLX Conv2d
|
|
88
|
+
var hidden = x.transposed(0, 2, 3, 1) // [B, H, W, 3]
|
|
89
|
+
|
|
90
|
+
// Initial conv
|
|
91
|
+
hidden = convIn(hidden)
|
|
92
|
+
|
|
93
|
+
// Down blocks
|
|
94
|
+
for (resBlocks, downsample) in downBlocks {
|
|
95
|
+
for resBlock in resBlocks {
|
|
96
|
+
hidden = resBlock(hidden)
|
|
97
|
+
}
|
|
98
|
+
if let ds = downsample {
|
|
99
|
+
hidden = ds(hidden)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Mid block
|
|
104
|
+
hidden = midBlock.resnet1(hidden)
|
|
105
|
+
hidden = midBlock.attention(hidden)
|
|
106
|
+
hidden = midBlock.resnet2(hidden)
|
|
107
|
+
|
|
108
|
+
// Output
|
|
109
|
+
hidden = convNormOut(hidden)
|
|
110
|
+
hidden = silu(hidden)
|
|
111
|
+
hidden = convOut(hidden)
|
|
112
|
+
|
|
113
|
+
// Convert back to NCHW: [B, H/8, W/8, latent_channels*2] -> [B, latent_channels*2, H/8, W/8]
|
|
114
|
+
return hidden.transposed(0, 3, 1, 2)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Encode and sample from the latent distribution
|
|
118
|
+
/// - Parameter x: Input image [B, 3, H, W] (NCHW format)
|
|
119
|
+
/// - Returns: Sampled latent [B, latent_channels, H/8, W/8] (NCHW format)
|
|
120
|
+
public func encode(_ x: MLXArray, samplePosterior: Bool = true) -> MLXArray {
|
|
121
|
+
let h = self.callAsFunction(x) // Output is NCHW
|
|
122
|
+
|
|
123
|
+
// Split into mean and log_variance (NCHW format)
|
|
124
|
+
let mean = h[0..., 0..<config.latentChannels, 0..., 0...]
|
|
125
|
+
let logVar = h[0..., config.latentChannels..., 0..., 0...]
|
|
126
|
+
|
|
127
|
+
if samplePosterior {
|
|
128
|
+
// Reparameterization trick
|
|
129
|
+
let std = exp(0.5 * logVar)
|
|
130
|
+
let noise = MLXRandom.normal(mean.shape)
|
|
131
|
+
return mean + std * noise
|
|
132
|
+
} else {
|
|
133
|
+
return mean
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|