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