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.
Files changed (96) hide show
  1. package/dist/cli.cjs +269 -22
  2. package/dist/cli.d.cts +32 -2
  3. package/dist/cli.d.ts +32 -2
  4. package/dist/cli.js +270 -23
  5. package/dist/platform.d.cts +2 -0
  6. package/dist/platform.d.ts +2 -0
  7. package/dist/{ui-1WepaMS4.d.cts → ui-Bg11tvlc.d.ts} +1 -1
  8. package/dist/{ui-Ce-pnUUA.d.ts → ui-Dd7SXdbg.d.cts} +7 -7
  9. package/dist/ui.d.cts +1 -1
  10. package/dist/ui.d.ts +1 -1
  11. package/dist/window.d.cts +1 -1
  12. package/dist/window.d.ts +1 -1
  13. package/package.json +5 -1
  14. package/templates/image-generation/app/index.tsx +1 -1
  15. package/templates/native/.gitattributes +1 -0
  16. package/templates/native/App.tsx +26 -0
  17. package/templates/native/app.json +12 -0
  18. package/templates/native/index.ts +8 -0
  19. package/templates/native/ios/.xcode.env +11 -0
  20. package/templates/native/ios/Bleam/AI/Flux2/Configuration/Flux2Config.swift +288 -0
  21. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryConfig.swift +233 -0
  22. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryOptimizationConfig.swift +157 -0
  23. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +425 -0
  24. package/templates/native/ios/Bleam/AI/Flux2/Configuration/QuantizationConfig.swift +138 -0
  25. package/templates/native/ios/Bleam/AI/Flux2/Configuration/TransformerRepoProfile.swift +59 -0
  26. package/templates/native/ios/Bleam/AI/Flux2/Configuration/VAEConfig.swift +134 -0
  27. package/templates/native/ios/Bleam/AI/Flux2/Flux2Core.swift +55 -0
  28. package/templates/native/ios/Bleam/AI/Flux2/Loading/WeightLoader.swift +1192 -0
  29. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +1282 -0
  30. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/LatentUtils.swift +191 -0
  31. package/templates/native/ios/Bleam/AI/Flux2/Scheduler/FlowMatchEulerScheduler.swift +279 -0
  32. package/templates/native/ios/Bleam/AI/Flux2/Text/KleinTextEncoder.swift +380 -0
  33. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Attention.swift +379 -0
  34. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Embeddings.swift +159 -0
  35. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FeedForward.swift +154 -0
  36. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FusedKernels.swift +198 -0
  37. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2KVCache.swift +36 -0
  38. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Modulation.swift +193 -0
  39. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2ParallelAttention.swift +389 -0
  40. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2RoPE.swift +304 -0
  41. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2SingleBlock.swift +290 -0
  42. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Transformer.swift +530 -0
  43. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2TransformerBlock.swift +362 -0
  44. package/templates/native/ios/Bleam/AI/Flux2/Utils/Flux2Debug.swift +101 -0
  45. package/templates/native/ios/Bleam/AI/Flux2/Utils/MLXCheckpoint.swift +118 -0
  46. package/templates/native/ios/Bleam/AI/Flux2/Utils/MemoryManager.swift +201 -0
  47. package/templates/native/ios/Bleam/AI/Flux2/VAE/AutoencoderKL.swift +476 -0
  48. package/templates/native/ios/Bleam/AI/Flux2/VAE/ResnetBlock.swift +316 -0
  49. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEDecoder.swift +120 -0
  50. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEEncoder.swift +136 -0
  51. package/templates/native/ios/Bleam/AppDelegate.swift +482 -0
  52. package/templates/native/ios/Bleam/Appearance.swift +62 -0
  53. package/templates/native/ios/Bleam/Bridging-Header.h +3 -0
  54. package/templates/native/ios/Bleam/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  55. package/templates/native/ios/Bleam/Images.xcassets/Contents.json +6 -0
  56. package/templates/native/ios/Bleam/Info.plist +76 -0
  57. package/templates/native/ios/Bleam/PrivacyInfo.xcprivacy +48 -0
  58. package/templates/native/ios/Bleam/SceneDelegate.swift +58 -0
  59. package/templates/native/ios/Bleam/SplashScreen.storyboard +47 -0
  60. package/templates/native/ios/Bleam/Supporting/Expo.plist +6 -0
  61. package/templates/native/ios/Bleam/bleam.entitlements +10 -0
  62. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +1147 -0
  63. package/templates/native/ios/Bleam.xcodeproj/xcshareddata/xcschemes/Bleam.xcscheme +88 -0
  64. package/templates/native/ios/GenerationService/GenerationService.entitlements +10 -0
  65. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +591 -0
  66. package/templates/native/ios/GenerationService/Info.plist +31 -0
  67. package/templates/native/ios/GenerationService/main.swift +165 -0
  68. package/templates/native/ios/PlatformHelper/main.swift +726 -0
  69. package/templates/native/ios/Podfile +198 -0
  70. package/templates/native/ios/Podfile.lock +2284 -0
  71. package/templates/native/ios/Podfile.properties.json +5 -0
  72. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +12 -0
  73. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +124 -0
  74. package/templates/native/metro.config.js +6 -0
  75. package/templates/native/modules/bleam-runtime/BleamRuntime.podspec +14 -0
  76. package/templates/native/modules/bleam-runtime/expo-module.config.json +8 -0
  77. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +1258 -0
  78. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +135 -0
  79. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +329 -0
  80. package/templates/native/modules/bleam-runtime/package.json +5 -0
  81. package/templates/native/package.json +39 -0
  82. package/templates/native/scripts/build/mlx-frameworks.ts +421 -0
  83. package/templates/native/scripts/start/index.ts +474 -0
  84. package/templates/native/scripts/start/known-failures.ts +20 -0
  85. package/templates/native/scripts/start/loader.ts +76 -0
  86. package/templates/native/scripts/start/native-state.ts +242 -0
  87. package/templates/native/scripts/start/package.json +4 -0
  88. package/templates/native/scripts/start/swift-packages.ts +414 -0
  89. package/templates/native/scripts/start/xcode-formatter.ts +106 -0
  90. package/templates/native/tsconfig.json +14 -0
  91. package/templates/native/yarn.lock +3749 -0
  92. package/templates/updates/README.md +272 -0
  93. package/templates/updates/src/index.ts +385 -0
  94. package/templates/updates/src/schema.ts +385 -0
  95. package/templates/updates/tsconfig.json +12 -0
  96. package/templates/updates/wrangler.jsonc +19 -0
@@ -0,0 +1,389 @@
1
+ // Flux2ParallelAttention.swift - Fused Attention+FFN for Single-Stream Blocks
2
+ // Copyright 2025 Vincent Gourbin
3
+
4
+ import Foundation
5
+ import MLX
6
+ import MLXNN
7
+
8
+ /// Parallel Self-Attention for Single-Stream Blocks
9
+ ///
10
+ /// This module fuses the QKV projection with MLP gate/up projections
11
+ /// for more efficient computation. Used in the 48 single-stream blocks.
12
+ ///
13
+ /// Architecture:
14
+ /// - Single projection: dim -> (3*innerDim + 2*mlpHidden) for Q,K,V,gate,up
15
+ /// - Self-attention on concatenated text+image
16
+ /// - Output projection: (innerDim + mlpHidden) -> dim
17
+ public class Flux2ParallelSelfAttention: Module, @unchecked Sendable {
18
+ let dim: Int
19
+ let numHeads: Int
20
+ let headDim: Int
21
+ let innerDim: Int
22
+ let mlpHiddenDim: Int
23
+
24
+ // Fused input projection: Q, K, V, MLP gate, MLP up
25
+ // @ModuleInfo allows update(modules:) to replace with LoRA
26
+ @ModuleInfo var toQkvMlp: Linear
27
+
28
+ // QK normalization
29
+ let normQ: FluxRMSNorm
30
+ let normK: FluxRMSNorm
31
+
32
+ // Fused output projection: attention output + MLP output -> dim
33
+ @ModuleInfo var toOut: Linear
34
+
35
+ /// Initialize Parallel Self-Attention
36
+ /// - Parameters:
37
+ /// - dim: Model dimension (6144)
38
+ /// - numHeads: Number of attention heads (48)
39
+ /// - headDim: Dimension per head (128)
40
+ /// - mlpRatio: MLP hidden dimension ratio (default 4.0)
41
+ public init(
42
+ dim: Int,
43
+ numHeads: Int,
44
+ headDim: Int,
45
+ mlpRatio: Float = 3.0
46
+ ) {
47
+ self.dim = dim
48
+ self.numHeads = numHeads
49
+ self.headDim = headDim
50
+ self.innerDim = numHeads * headDim
51
+ self.mlpHiddenDim = Int(Float(dim) * mlpRatio)
52
+
53
+ // Fused projection dimensions:
54
+ // Q: innerDim, K: innerDim, V: innerDim, gate: mlpHidden, up: mlpHidden
55
+ let totalProjDim = innerDim * 3 + mlpHiddenDim * 2
56
+ self._toQkvMlp.wrappedValue = Linear(dim, totalProjDim, bias: false)
57
+
58
+ // QK normalization
59
+ self.normQ = FluxRMSNorm(dim: headDim)
60
+ self.normK = FluxRMSNorm(dim: headDim)
61
+
62
+ // Output projection: attention (innerDim) + mlp (mlpHidden) -> dim (no bias)
63
+ self._toOut.wrappedValue = Linear(innerDim + mlpHiddenDim, dim, bias: false)
64
+ }
65
+
66
+ /// Forward pass
67
+ /// - Parameters:
68
+ /// - hiddenStates: Combined image+text hidden states [B, S, dim]
69
+ /// - rotaryEmb: Optional RoPE embeddings
70
+ /// - Returns: Updated hidden states [B, S, dim]
71
+ public func callAsFunction(
72
+ hiddenStates: MLXArray,
73
+ rotaryEmb: (cos: MLXArray, sin: MLXArray)? = nil
74
+ ) -> MLXArray {
75
+ let batchSize = hiddenStates.shape[0]
76
+ let seqLen = hiddenStates.shape[1]
77
+
78
+ // Fused projection
79
+ let projected = toQkvMlp(hiddenStates)
80
+
81
+ // Split into Q, K, V, gate, up
82
+ var q = projected[0..., 0..., 0..<innerDim]
83
+ var k = projected[0..., 0..., innerDim..<(innerDim * 2)]
84
+ let v = projected[0..., 0..., (innerDim * 2)..<(innerDim * 3)]
85
+ let mlpGate = projected[0..., 0..., (innerDim * 3)..<(innerDim * 3 + mlpHiddenDim)]
86
+ let mlpUp = projected[0..., 0..., (innerDim * 3 + mlpHiddenDim)...]
87
+
88
+ // Reshape for multi-head attention: [B, S, H*D] -> [B, H, S, D]
89
+ q = reshapeForAttention(q, batchSize: batchSize, seqLen: seqLen)
90
+ k = reshapeForAttention(k, batchSize: batchSize, seqLen: seqLen)
91
+ let vReshaped = reshapeForAttention(v, batchSize: batchSize, seqLen: seqLen)
92
+
93
+ // Apply QK normalization
94
+ q = normQ(q)
95
+ k = normK(k)
96
+
97
+ // Apply RoPE if provided
98
+ if let rope = rotaryEmb {
99
+ (q, k) = applyRoPE(q: q, k: k, cos: rope.cos, sin: rope.sin)
100
+ }
101
+
102
+ // Compute attention
103
+ let attnOutput = MLXFast.scaledDotProductAttention(
104
+ queries: q,
105
+ keys: k,
106
+ values: vReshaped,
107
+ scale: Float(1.0 / sqrt(Float(headDim))),
108
+ mask: nil
109
+ )
110
+
111
+ // Reshape attention output: [B, H, S, D] -> [B, S, H*D]
112
+ let attnOut = reshapeFromAttention(attnOutput, batchSize: batchSize, seqLen: seqLen)
113
+
114
+ // Compute MLP output: SwiGLU(gate, up)
115
+ let mlpOut = silu(mlpGate) * mlpUp
116
+
117
+ // Concatenate attention and MLP outputs
118
+ let combined = concatenated([attnOut, mlpOut], axis: -1)
119
+
120
+ // Final projection
121
+ return toOut(combined)
122
+ }
123
+
124
+ // MARK: - Helper Functions
125
+
126
+ /// Forward pass with cached reference K/V (for KV cache acceleration)
127
+ /// - Parameters:
128
+ /// - hiddenStates: Image hidden states only [B, S_img, dim]
129
+ /// - cachedRefKV: Pre-computed reference (text+ref image) K and V [B, H, S_ref, D]
130
+ /// - rotaryEmb: Optional RoPE embeddings - only image portion needed
131
+ /// - Returns: Updated hidden states [B, S_img, dim]
132
+ public func callAsFunction(
133
+ hiddenStates: MLXArray,
134
+ cachedRefKV: (k: MLXArray, v: MLXArray),
135
+ rotaryEmb: (cos: MLXArray, sin: MLXArray)? = nil
136
+ ) -> MLXArray {
137
+ let batchSize = hiddenStates.shape[0]
138
+ let seqLenImg = hiddenStates.shape[1]
139
+ let seqLenRef = cachedRefKV.k.shape[2]
140
+
141
+ // Fused projection for image tokens only
142
+ let projected = toQkvMlp(hiddenStates)
143
+
144
+ // Split into Q, K, V, gate, up
145
+ var q = projected[0..., 0..., 0..<innerDim]
146
+ var k = projected[0..., 0..., innerDim..<(innerDim * 2)]
147
+ let v = projected[0..., 0..., (innerDim * 2)..<(innerDim * 3)]
148
+ let mlpGate = projected[0..., 0..., (innerDim * 3)..<(innerDim * 3 + mlpHiddenDim)]
149
+ let mlpUp = projected[0..., 0..., (innerDim * 3 + mlpHiddenDim)...]
150
+
151
+ // Reshape for multi-head attention
152
+ q = reshapeForAttention(q, batchSize: batchSize, seqLen: seqLenImg)
153
+ k = reshapeForAttention(k, batchSize: batchSize, seqLen: seqLenImg)
154
+ let vReshaped = reshapeForAttention(v, batchSize: batchSize, seqLen: seqLenImg)
155
+
156
+ // Apply QK normalization
157
+ q = normQ(q)
158
+ k = normK(k)
159
+
160
+ // Apply RoPE to image Q/K only
161
+ if let rope = rotaryEmb {
162
+ let imgCos = rope.cos[seqLenRef..<(seqLenRef + seqLenImg)]
163
+ let imgSin = rope.sin[seqLenRef..<(seqLenRef + seqLenImg)]
164
+ (q, k) = applyRoPE(q: q, k: k, cos: imgCos, sin: imgSin)
165
+ }
166
+
167
+ // Concatenate cached reference K/V with image K/V
168
+ let concatK = concatenated([cachedRefKV.k, k], axis: 2)
169
+ let concatV = concatenated([cachedRefKV.v, vReshaped], axis: 2)
170
+
171
+ // Compute attention
172
+ let attnOutput = MLXFast.scaledDotProductAttention(
173
+ queries: q,
174
+ keys: concatK,
175
+ values: concatV,
176
+ scale: Float(1.0 / sqrt(Float(headDim))),
177
+ mask: nil
178
+ )
179
+
180
+ // Reshape attention output
181
+ let attnOut = reshapeFromAttention(attnOutput, batchSize: batchSize, seqLen: seqLenImg)
182
+
183
+ // Compute MLP output
184
+ let mlpOut = silu(mlpGate) * mlpUp
185
+
186
+ // Concatenate and project
187
+ let combined = concatenated([attnOut, mlpOut], axis: -1)
188
+ return toOut(combined)
189
+ }
190
+
191
+ /// Compute K and V for reference tokens only (for KV cache building)
192
+ /// - Parameters:
193
+ /// - hiddenStates: Normalized reference hidden states [B, S_ref, dim]
194
+ /// - batchSize: Batch size
195
+ /// - Returns: (k, v) each [B, H, S_ref, D]
196
+ public func computeRefKV(
197
+ hiddenStates: MLXArray,
198
+ batchSize: Int
199
+ ) -> (k: MLXArray, v: MLXArray) {
200
+ let seqLenRef = hiddenStates.shape[1]
201
+
202
+ let projected = toQkvMlp(hiddenStates)
203
+ var k = projected[0..., 0..., innerDim..<(innerDim * 2)]
204
+ let v = projected[0..., 0..., (innerDim * 2)..<(innerDim * 3)]
205
+
206
+ k = reshapeForAttention(k, batchSize: batchSize, seqLen: seqLenRef)
207
+ let vReshaped = reshapeForAttention(v, batchSize: batchSize, seqLen: seqLenRef)
208
+
209
+ k = normK(k)
210
+
211
+ return (k: k, v: vReshaped)
212
+ }
213
+
214
+ // MARK: - Helper Functions
215
+
216
+ private func reshapeForAttention(_ x: MLXArray, batchSize: Int, seqLen: Int) -> MLXArray {
217
+ x.reshaped([batchSize, seqLen, numHeads, headDim])
218
+ .transposed(0, 2, 1, 3)
219
+ }
220
+
221
+ private func reshapeFromAttention(_ x: MLXArray, batchSize: Int, seqLen: Int) -> MLXArray {
222
+ x.transposed(0, 2, 1, 3)
223
+ .reshaped([batchSize, seqLen, numHeads * headDim])
224
+ }
225
+
226
+ private func applyRoPE(
227
+ q: MLXArray,
228
+ k: MLXArray,
229
+ cos: MLXArray,
230
+ sin: MLXArray
231
+ ) -> (MLXArray, MLXArray) {
232
+ let cosExpanded = cos.expandedDimensions(axes: [0, 1])
233
+ let sinExpanded = sin.expandedDimensions(axes: [0, 1])
234
+
235
+ let qRotated = rotateHalf(q)
236
+ let kRotated = rotateHalf(k)
237
+
238
+ let qOut = q * cosExpanded + qRotated * sinExpanded
239
+ let kOut = k * cosExpanded + kRotated * sinExpanded
240
+
241
+ return (qOut, kOut)
242
+ }
243
+
244
+ private func rotateHalf(_ x: MLXArray) -> MLXArray {
245
+ // x shape: [B, H, S, D]
246
+ // Diffusers approach: reshape to [B, H, S, D/2, 2], then [-imag, real]
247
+ let batchSize = x.shape[0]
248
+ let numHeads = x.shape[1]
249
+ let seqLen = x.shape[2]
250
+ let dim = x.shape[3]
251
+ let halfDim = dim / 2
252
+
253
+ // Reshape to [B, H, S, D/2, 2]
254
+ let xReshaped = x.reshaped([batchSize, numHeads, seqLen, halfDim, 2])
255
+
256
+ // Get real and imag parts (consecutive pairs)
257
+ let xReal = xReshaped[0..., 0..., 0..., 0..., 0] // [B, H, S, D/2]
258
+ let xImag = xReshaped[0..., 0..., 0..., 0..., 1] // [B, H, S, D/2]
259
+
260
+ // Create rotated: stack [-imag, real] and flatten
261
+ let xRotatedStacked = stacked([-xImag, xReal], axis: -1) // [B, H, S, D/2, 2]
262
+ return xRotatedStacked.reshaped([batchSize, numHeads, seqLen, dim]) // [B, H, S, D]
263
+ }
264
+ }
265
+
266
+ /// Alternative implementation with separate projections
267
+ /// (for compatibility with some weight formats)
268
+ public class Flux2ParallelSelfAttentionSplit: Module, @unchecked Sendable {
269
+ let dim: Int
270
+ let numHeads: Int
271
+ let headDim: Int
272
+ let innerDim: Int
273
+ let mlpHiddenDim: Int
274
+
275
+ // Separate projections (var for LoRA injection)
276
+ @ModuleInfo var toQ: Linear
277
+ @ModuleInfo var toK: Linear
278
+ @ModuleInfo var toV: Linear
279
+ @ModuleInfo var mlpGate: Linear
280
+ @ModuleInfo var mlpUp: Linear
281
+
282
+ // QK normalization
283
+ let normQ: FluxRMSNorm
284
+ let normK: FluxRMSNorm
285
+
286
+ // Separate output projections (var for LoRA injection)
287
+ @ModuleInfo var toAttnOut: Linear
288
+ @ModuleInfo var mlpDown: Linear
289
+
290
+ public init(
291
+ dim: Int,
292
+ numHeads: Int,
293
+ headDim: Int,
294
+ mlpRatio: Float = 3.0
295
+ ) {
296
+ self.dim = dim
297
+ self.numHeads = numHeads
298
+ self.headDim = headDim
299
+ self.innerDim = numHeads * headDim
300
+ self.mlpHiddenDim = Int(Float(dim) * mlpRatio)
301
+
302
+ self._toQ = ModuleInfo(wrappedValue: Linear(dim, innerDim, bias: false))
303
+ self._toK = ModuleInfo(wrappedValue: Linear(dim, innerDim, bias: false))
304
+ self._toV = ModuleInfo(wrappedValue: Linear(dim, innerDim, bias: false))
305
+ self._mlpGate = ModuleInfo(wrappedValue: Linear(dim, mlpHiddenDim, bias: false))
306
+ self._mlpUp = ModuleInfo(wrappedValue: Linear(dim, mlpHiddenDim, bias: false))
307
+
308
+ self.normQ = FluxRMSNorm(dim: headDim)
309
+ self.normK = FluxRMSNorm(dim: headDim)
310
+
311
+ self._toAttnOut = ModuleInfo(wrappedValue: Linear(innerDim, dim, bias: false))
312
+ self._mlpDown = ModuleInfo(wrappedValue: Linear(mlpHiddenDim, dim, bias: false))
313
+ }
314
+
315
+ public func callAsFunction(
316
+ hiddenStates: MLXArray,
317
+ rotaryEmb: (cos: MLXArray, sin: MLXArray)? = nil
318
+ ) -> MLXArray {
319
+ let batchSize = hiddenStates.shape[0]
320
+ let seqLen = hiddenStates.shape[1]
321
+
322
+ // Separate projections
323
+ var q = toQ(hiddenStates)
324
+ var k = toK(hiddenStates)
325
+ let v = toV(hiddenStates)
326
+ let gate = mlpGate(hiddenStates)
327
+ let up = mlpUp(hiddenStates)
328
+
329
+ // Reshape for attention
330
+ q = q.reshaped([batchSize, seqLen, numHeads, headDim]).transposed(0, 2, 1, 3)
331
+ k = k.reshaped([batchSize, seqLen, numHeads, headDim]).transposed(0, 2, 1, 3)
332
+ let vReshaped = v.reshaped([batchSize, seqLen, numHeads, headDim]).transposed(0, 2, 1, 3)
333
+
334
+ // QK norm
335
+ q = normQ(q)
336
+ k = normK(k)
337
+
338
+ // RoPE
339
+ if let rope = rotaryEmb {
340
+ let cosExpanded = rope.cos.expandedDimensions(axes: [0, 1])
341
+ let sinExpanded = rope.sin.expandedDimensions(axes: [0, 1])
342
+
343
+ let qRot = rotateHalf(q)
344
+ let kRot = rotateHalf(k)
345
+
346
+ q = q * cosExpanded + qRot * sinExpanded
347
+ k = k * cosExpanded + kRot * sinExpanded
348
+ }
349
+
350
+ // Attention
351
+ let attnOut = MLXFast.scaledDotProductAttention(
352
+ queries: q,
353
+ keys: k,
354
+ values: vReshaped,
355
+ scale: Float(1.0 / sqrt(Float(headDim))),
356
+ mask: nil
357
+ )
358
+
359
+ // Reshape back
360
+ let attnReshaped = attnOut.transposed(0, 2, 1, 3).reshaped([batchSize, seqLen, innerDim])
361
+
362
+ // MLP
363
+ let mlpOut = silu(gate) * up
364
+
365
+ // Separate output projections and add
366
+ return toAttnOut(attnReshaped) + mlpDown(mlpOut)
367
+ }
368
+
369
+ private func rotateHalf(_ x: MLXArray) -> MLXArray {
370
+ // x shape: [B, H, S, D]
371
+ // Diffusers approach: reshape to [B, H, S, D/2, 2], then [-imag, real]
372
+ let batchSize = x.shape[0]
373
+ let numHeads = x.shape[1]
374
+ let seqLen = x.shape[2]
375
+ let dim = x.shape[3]
376
+ let halfDim = dim / 2
377
+
378
+ // Reshape to [B, H, S, D/2, 2]
379
+ let xReshaped = x.reshaped([batchSize, numHeads, seqLen, halfDim, 2])
380
+
381
+ // Get real and imag parts (consecutive pairs)
382
+ let xReal = xReshaped[0..., 0..., 0..., 0..., 0] // [B, H, S, D/2]
383
+ let xImag = xReshaped[0..., 0..., 0..., 0..., 1] // [B, H, S, D/2]
384
+
385
+ // Create rotated: stack [-imag, real] and flatten
386
+ let xRotatedStacked = stacked([-xImag, xReal], axis: -1) // [B, H, S, D/2, 2]
387
+ return xRotatedStacked.reshaped([batchSize, numHeads, seqLen, dim]) // [B, H, S, D]
388
+ }
389
+ }
@@ -0,0 +1,304 @@
1
+ // Flux2RoPE.swift - Rotary Position Embeddings for Flux.2
2
+ // Copyright 2025 Vincent Gourbin
3
+
4
+ import Foundation
5
+ import MLX
6
+ import MLXNN
7
+
8
+ /// Cached RoPE embeddings with LRU eviction
9
+ /// Note: Not Sendable because MLXArray isn't, but thread-safety is ensured by NSLock
10
+ private struct RoPECacheEntry {
11
+ let cos: MLXArray
12
+ let sin: MLXArray
13
+ var lastAccess: UInt64
14
+ }
15
+
16
+ /// 4D Rotary Position Embedding for Flux.2
17
+ ///
18
+ /// Flux.2 uses a 4D RoPE with axes_dims = [32, 32, 32, 32] for a total of 128 dims.
19
+ /// This encodes position information for (T, H, W, L) axes.
20
+ ///
21
+ /// Includes LRU caching to avoid recomputing embeddings for repeated position IDs.
22
+ public class Flux2RoPE: Module, @unchecked Sendable {
23
+ let axesDims: [Int]
24
+ let theta: Float
25
+ let totalDims: Int
26
+
27
+ /// Maximum number of cached entries (LRU eviction when exceeded)
28
+ private let cacheMaxSize: Int
29
+
30
+ /// Cache for computed embeddings, keyed by shape hash
31
+ private var cache: [String: RoPECacheEntry] = [:]
32
+ private var accessCounter: UInt64 = 0
33
+ private let lock = NSLock()
34
+
35
+ /// Initialize RoPE with given axes dimensions
36
+ /// - Parameters:
37
+ /// - axesDims: Dimensions per axis [T, H, W, L], default [32, 32, 32, 32]
38
+ /// - theta: Base frequency for positional encoding, default 2000.0
39
+ /// - cacheMaxSize: Maximum cache entries (default 8)
40
+ public init(
41
+ axesDims: [Int] = [32, 32, 32, 32],
42
+ theta: Float = 2000.0,
43
+ cacheMaxSize: Int = 8
44
+ ) {
45
+ self.axesDims = axesDims
46
+ self.theta = theta
47
+ self.totalDims = axesDims.reduce(0, +)
48
+ self.cacheMaxSize = cacheMaxSize
49
+ }
50
+
51
+ /// Compute frequency bands for given dimension
52
+ private func computeFreqs(dim: Int, maxLen: Int) -> MLXArray {
53
+ let halfDim = dim / 2
54
+ let freqSeq = MLXArray(0..<halfDim).asType(.float32)
55
+ let invFreq = 1.0 / pow(MLXArray(theta), freqSeq / Float(halfDim))
56
+
57
+ let positions = MLXArray(0..<maxLen).asType(.float32).expandedDimensions(axis: 1)
58
+ let freqs = positions * invFreq.expandedDimensions(axis: 0)
59
+
60
+ return freqs
61
+ }
62
+
63
+ /// Generate cache key from position IDs shape
64
+ /// Position IDs with the same shape can encode different image/reference layouts.
65
+ private func cacheKey(for ids: MLXArray) -> String {
66
+ var hash: UInt64 = 1469598103934665603
67
+ for value in ids.asType(.int32).asArray(Int32.self) {
68
+ hash ^= UInt64(bitPattern: Int64(value))
69
+ hash &*= 1099511628211
70
+ }
71
+
72
+ return "rope_shape=\(ids.shape)_axes=\(axesDims)_theta=\(theta)_hash=\(hash)"
73
+ }
74
+
75
+ /// Generate rotary embeddings for position IDs
76
+ /// - Parameter ids: Position IDs [S, 4] where 4 corresponds to (T, H, W, L)
77
+ /// - Returns: Tuple of (cos, sin) embeddings each of shape [S, totalDims]
78
+ public func callAsFunction(_ ids: MLXArray) -> (cos: MLXArray, sin: MLXArray) {
79
+ let key = cacheKey(for: ids)
80
+
81
+ // Check cache with lock
82
+ lock.lock()
83
+ if var entry = cache[key] {
84
+ accessCounter += 1
85
+ entry.lastAccess = accessCounter
86
+ cache[key] = entry
87
+ lock.unlock()
88
+ Flux2Debug.verbose("RoPE cache hit for \(key)")
89
+ return (cos: entry.cos, sin: entry.sin)
90
+ }
91
+ lock.unlock()
92
+
93
+ Flux2Debug.verbose("RoPE cache miss for \(key), computing...")
94
+
95
+ // Compute embeddings
96
+ let (cosEmb, sinEmb) = computeEmbeddings(for: ids)
97
+
98
+ // Store in cache with LRU eviction
99
+ lock.lock()
100
+ accessCounter += 1
101
+
102
+ // Evict LRU entry if cache is full
103
+ if cache.count >= cacheMaxSize {
104
+ if let lruKey = cache.min(by: { $0.value.lastAccess < $1.value.lastAccess })?.key {
105
+ cache.removeValue(forKey: lruKey)
106
+ Flux2Debug.verbose("RoPE cache evicted \(lruKey)")
107
+ }
108
+ }
109
+
110
+ cache[key] = RoPECacheEntry(cos: cosEmb, sin: sinEmb, lastAccess: accessCounter)
111
+ lock.unlock()
112
+
113
+ return (cos: cosEmb, sin: sinEmb)
114
+ }
115
+
116
+ /// Clear the RoPE embedding cache
117
+ public func clearCache() {
118
+ lock.lock()
119
+ cache.removeAll()
120
+ accessCounter = 0
121
+ lock.unlock()
122
+ Flux2Debug.verbose("RoPE cache cleared")
123
+ }
124
+
125
+ /// Compute embeddings (internal method)
126
+ private func computeEmbeddings(for ids: MLXArray) -> (cos: MLXArray, sin: MLXArray) {
127
+ var cosComponents: [MLXArray] = []
128
+ var sinComponents: [MLXArray] = []
129
+
130
+ let seqLen = ids.shape[0]
131
+
132
+ // Process each axis - INTERLEAVE EACH AXIS FIRST, THEN CONCATENATE
133
+ // This matches diffusers: repeat_interleave(2) per axis, then cat all axes
134
+ for (axisIdx, axisDim) in axesDims.enumerated() {
135
+ // Get position indices for this axis
136
+ let axisPositions = ids[0..., axisIdx] // [S]
137
+
138
+ // Compute frequencies for this axis dimension
139
+ // diffusers: freqs = 1.0 / (theta ** (arange(0, dim, 2) / dim))
140
+ // For dim=32: arange(0, 32, 2) = [0, 2, 4, ..., 30] and /32 gives [0, 1/16, 2/16, ...]
141
+ let freqSeq = MLXArray(stride(from: 0, to: axisDim, by: 2)).asType(.float32)
142
+ let invFreq = 1.0 / pow(MLXArray(theta), freqSeq / Float(axisDim))
143
+
144
+ // Compute position * frequency
145
+ let posExpanded = axisPositions.asType(.float32).expandedDimensions(axis: 1) // [S, 1]
146
+ let freqExpanded = invFreq.expandedDimensions(axis: 0) // [1, halfDim]
147
+ let freqs = posExpanded * freqExpanded // [S, halfDim]
148
+
149
+ // Compute cos and sin
150
+ let axisCosHalf = cos(freqs) // [S, halfDim]
151
+ let axisSinHalf = sin(freqs) // [S, halfDim]
152
+
153
+ // Interleave THIS axis: [c0, c0, c1, c1, ...] like repeat_interleave(2, dim=1)
154
+ // [S, halfDim] -> [S, halfDim, 1] -> [S, halfDim, 2] -> [S, axisDim]
155
+ let cosExpanded = axisCosHalf.expandedDimensions(axis: -1) // [S, halfDim, 1]
156
+ let cosInterleaved = concatenated([cosExpanded, cosExpanded], axis: -1) // [S, halfDim, 2]
157
+ let axisCos = cosInterleaved.reshaped([seqLen, axisDim]) // [S, axisDim]
158
+
159
+ let sinExpanded = axisSinHalf.expandedDimensions(axis: -1) // [S, halfDim, 1]
160
+ let sinInterleaved = concatenated([sinExpanded, sinExpanded], axis: -1) // [S, halfDim, 2]
161
+ let axisSin = sinInterleaved.reshaped([seqLen, axisDim]) // [S, axisDim]
162
+
163
+ cosComponents.append(axisCos)
164
+ sinComponents.append(axisSin)
165
+ }
166
+
167
+ // Now concatenate all ALREADY INTERLEAVED axis embeddings
168
+ let cosEmb = concatenated(cosComponents, axis: -1) // [S, totalDims]
169
+ let sinEmb = concatenated(sinComponents, axis: -1) // [S, totalDims]
170
+
171
+ return (cos: cosEmb, sin: sinEmb)
172
+ }
173
+
174
+ /// Apply rotary embeddings to query and key tensors
175
+ /// - Parameters:
176
+ /// - q: Query tensor [B, H, S, D]
177
+ /// - k: Key tensor [B, H, S, D]
178
+ /// - cos: Cosine embeddings [S, D]
179
+ /// - sin: Sine embeddings [S, D]
180
+ /// - Returns: Tuple of rotated (query, key)
181
+ public func apply(
182
+ query q: MLXArray,
183
+ key k: MLXArray,
184
+ cos cosEmb: MLXArray,
185
+ sin sinEmb: MLXArray
186
+ ) -> (query: MLXArray, key: MLXArray) {
187
+ let rotatedQ = applyRotary(q, cos: cosEmb, sin: sinEmb)
188
+ let rotatedK = applyRotary(k, cos: cosEmb, sin: sinEmb)
189
+ return (query: rotatedQ, key: rotatedK)
190
+ }
191
+
192
+ /// Apply rotary embedding to a single tensor
193
+ /// Uses the same approach as diffusers: treat consecutive pairs as (real, imag)
194
+ private func applyRotary(_ x: MLXArray, cos cosEmb: MLXArray, sin sinEmb: MLXArray) -> MLXArray {
195
+ // x shape: [B, H, S, D]
196
+ // cos/sin shape: [S, D]
197
+
198
+ // Try fused Metal kernel first (requires float32 cos/sin)
199
+ let cosFloat32 = cosEmb.dtype == .float32 ? cosEmb : cosEmb.asType(.float32)
200
+ let sinFloat32 = sinEmb.dtype == .float32 ? sinEmb : sinEmb.asType(.float32)
201
+
202
+ if let fused = Flux2FusedKernels.applyRotaryEmb(x, cos: cosFloat32, sin: sinFloat32) {
203
+ return fused
204
+ }
205
+
206
+ // Fall back to reference implementation
207
+ return applyRotaryReference(x, cos: cosEmb, sin: sinEmb)
208
+ }
209
+
210
+ /// Reference implementation of rotary embeddings (fallback when kernel unavailable)
211
+ private func applyRotaryReference(_ x: MLXArray, cos cosEmb: MLXArray, sin sinEmb: MLXArray) -> MLXArray {
212
+ // x shape: [B, H, S, D]
213
+ // cos/sin shape: [S, D]
214
+
215
+ // Reshape for broadcasting
216
+ let cosExpanded = cosEmb.expandedDimensions(axes: [0, 1]) // [1, 1, S, D]
217
+ let sinExpanded = sinEmb.expandedDimensions(axes: [0, 1]) // [1, 1, S, D]
218
+
219
+ // Diffusers approach: reshape x to [B, H, S, D//2, 2] and unbind to get x_real, x_imag
220
+ // x_real = x[..., 0::2], x_imag = x[..., 1::2] (every other element)
221
+ // x_rotated = stack([-x_imag, x_real], dim=-1).flatten(3)
222
+
223
+ let batchSize = x.shape[0]
224
+ let numHeads = x.shape[1]
225
+ let seqLen = x.shape[2]
226
+ let dim = x.shape[3]
227
+ let halfDim = dim / 2
228
+
229
+ // Reshape to [B, H, S, D/2, 2]
230
+ let xReshaped = x.reshaped([batchSize, numHeads, seqLen, halfDim, 2])
231
+
232
+ // Get real and imag parts (consecutive pairs)
233
+ let xReal = xReshaped[0..., 0..., 0..., 0..., 0] // [B, H, S, D/2]
234
+ let xImag = xReshaped[0..., 0..., 0..., 0..., 1] // [B, H, S, D/2]
235
+
236
+ // Create rotated: [-x_imag, x_real] interleaved
237
+ let negXImag = -xImag
238
+ // Stack and flatten: [[-x_imag[0], x_real[0]], [-x_imag[1], x_real[1]], ...]
239
+ let xRotatedStacked = stacked([negXImag, xReal], axis: -1) // [B, H, S, D/2, 2]
240
+ let xRotated = xRotatedStacked.reshaped([batchSize, numHeads, seqLen, dim]) // [B, H, S, D]
241
+
242
+ // Apply rotation: x * cos + x_rotated * sin
243
+ return x * cosExpanded + xRotated * sinExpanded
244
+ }
245
+ }
246
+
247
+ /// Generate position IDs for image latents using GPU-native operations
248
+ /// - Parameters:
249
+ /// - height: Image height in latent space
250
+ /// - width: Image width in latent space
251
+ /// - Returns: Position IDs [H*W, 4]
252
+ public func generateImagePositionIDs(height: Int, width: Int) -> MLXArray {
253
+ // GPU-native implementation using meshgrid-like operations
254
+ // Create H and W coordinate grids
255
+ let hIndices = MLXArray.arange(height, dtype: .int32) // [H]
256
+ let wIndices = MLXArray.arange(width, dtype: .int32) // [W]
257
+
258
+ // Create meshgrid: expand H to [H, W] and W to [H, W] using broadcastTo
259
+ let hExpanded = hIndices.expandedDimensions(axis: 1) // [H, 1]
260
+ let wExpanded = wIndices.expandedDimensions(axis: 0) // [1, W]
261
+ let hGrid = MLX.broadcast(hExpanded, to: [height, width]) // [H, W]
262
+ let wGrid = MLX.broadcast(wExpanded, to: [height, width]) // [H, W]
263
+
264
+ // Flatten to [H*W]
265
+ let hFlat = hGrid.reshaped([height * width])
266
+ let wFlat = wGrid.reshaped([height * width])
267
+
268
+ // Create T=0 and L=0 columns
269
+ let zeros = MLXArray.zeros([height * width], dtype: .int32)
270
+
271
+ // Stack to create [H*W, 4] with format [T, H, W, L]
272
+ return MLX.stacked([zeros, hFlat, wFlat, zeros], axis: 1)
273
+ }
274
+
275
+ /// Generate position IDs for text sequence using GPU-native operations
276
+ /// - Parameter length: Text sequence length
277
+ /// - Returns: Position IDs [length, 4]
278
+ public func generateTextPositionIDs(length: Int) -> MLXArray {
279
+ // GPU-native implementation
280
+ // Create L column as [0, 1, 2, ..., length-1]
281
+ let lIndices = MLXArray.arange(length, dtype: .int32) // [length]
282
+
283
+ // Create T=0, H=0, W=0 columns
284
+ let zeros = MLXArray.zeros([length], dtype: .int32)
285
+
286
+ // Stack to create [length, 4] with format [T, H, W, L]
287
+ return MLX.stacked([zeros, zeros, zeros, lIndices], axis: 1)
288
+ }
289
+
290
+ /// Combine text and image position IDs for joint attention
291
+ /// - Parameters:
292
+ /// - textLength: Text sequence length
293
+ /// - height: Image height in latent space
294
+ /// - width: Image width in latent space
295
+ /// - Returns: Combined position IDs [textLength + H*W, 4]
296
+ public func generateCombinedPositionIDs(
297
+ textLength: Int,
298
+ height: Int,
299
+ width: Int
300
+ ) -> MLXArray {
301
+ let textIds = generateTextPositionIDs(length: textLength)
302
+ let imageIds = generateImagePositionIDs(height: height, width: width)
303
+ return concatenated([textIds, imageIds], axis: 0)
304
+ }