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,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
|
+
}
|