bleam 0.0.10 → 0.0.11

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 (68) hide show
  1. package/dist/ai.cjs +608 -59
  2. package/dist/ai.d.cts +217 -89
  3. package/dist/ai.d.ts +217 -89
  4. package/dist/ai.js +601 -59
  5. package/dist/cli.cjs +3 -183
  6. package/dist/cli.d.cts +2 -46
  7. package/dist/cli.d.ts +2 -46
  8. package/dist/cli.js +3 -183
  9. package/dist/files.d.cts +1 -1
  10. package/dist/files.d.ts +1 -1
  11. package/dist/platform.cjs +1 -13
  12. package/dist/platform.d.cts +1 -28
  13. package/dist/platform.d.ts +1 -28
  14. package/dist/platform.js +1 -10
  15. package/dist/schema.d.cts +1 -1
  16. package/dist/schema.d.ts +1 -1
  17. package/dist/state.d.cts +1 -1
  18. package/dist/state.d.ts +1 -1
  19. package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
  20. package/dist/{ui-Dd7SXdbg.d.cts → ui-CHc4xEs_.d.ts} +7 -7
  21. package/dist/ui.d.cts +1 -1
  22. package/dist/ui.d.ts +1 -1
  23. package/dist/window.d.cts +1 -1
  24. package/dist/window.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/templates/image-generation/app/index.tsx +3 -1
  27. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  28. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  29. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  30. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  31. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  32. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  33. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  34. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  35. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  36. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  37. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  38. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  39. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  40. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  41. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  42. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  43. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  44. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  45. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  46. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  47. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  48. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  49. package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
  50. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -122
  51. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  52. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  53. package/templates/native/ios/GenerationService/main.swift +174 -48
  54. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  55. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  56. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
  57. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  58. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
  59. package/templates/text-generation/app/index.tsx +93 -0
  60. package/templates/text-generation/app.config.ts +5 -0
  61. package/templates/updates/README.md +43 -199
  62. package/templates/updates/src/index.ts +5 -98
  63. package/templates/updates/src/schema.ts +18 -173
  64. package/templates/native/ios/PlatformHelper/main.swift +0 -726
  65. /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
  66. /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
  67. /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
  68. /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
@@ -0,0 +1,119 @@
1
+ import Foundation
2
+
3
+ public struct Qwen3ModelConfiguration: Decodable, Sendable {
4
+ public struct RopeScaling: Decodable, Sendable {
5
+ public let ropeType: String
6
+ public let factor: Float
7
+ public let originalMaximumPositions: Int
8
+ public let betaFast: Float
9
+ public let betaSlow: Float
10
+
11
+ enum CodingKeys: String, CodingKey {
12
+ case ropeType = "rope_type"
13
+ case type
14
+ case factor
15
+ case originalMaximumPositions = "original_max_position_embeddings"
16
+ case betaFast = "beta_fast"
17
+ case betaSlow = "beta_slow"
18
+ }
19
+
20
+ public init(from decoder: Decoder) throws {
21
+ let values = try decoder.container(keyedBy: CodingKeys.self)
22
+ ropeType = try values.decodeIfPresent(String.self, forKey: .ropeType)
23
+ ?? values.decodeIfPresent(String.self, forKey: .type)
24
+ ?? "yarn"
25
+ factor = try values.decode(Float.self, forKey: .factor)
26
+ originalMaximumPositions = try values.decode(Int.self, forKey: .originalMaximumPositions)
27
+ betaFast = try values.decodeIfPresent(Float.self, forKey: .betaFast) ?? 32
28
+ betaSlow = try values.decodeIfPresent(Float.self, forKey: .betaSlow) ?? 1
29
+ }
30
+ }
31
+
32
+ public struct Quantization: Decodable, Sendable {
33
+ public let bits: Int
34
+ public let groupSize: Int
35
+
36
+ enum CodingKeys: String, CodingKey {
37
+ case bits
38
+ case groupSize = "group_size"
39
+ }
40
+ }
41
+
42
+ public let architectures: [String]
43
+ public let modelType: String
44
+ public let vocabularySize: Int
45
+ public let hiddenSize: Int
46
+ public let intermediateSize: Int
47
+ public let hiddenLayerCount: Int
48
+ public let attentionHeadCount: Int
49
+ public let keyValueHeadCount: Int
50
+ public let maximumPositionCount: Int
51
+ public let rmsNormEpsilon: Float
52
+ public let ropeTheta: Float
53
+ public let tieWordEmbeddings: Bool
54
+ public let attentionBias: Bool
55
+ public let headDimension: Int
56
+ public let ropeScaling: RopeScaling?
57
+ public let quantization: Quantization?
58
+
59
+ enum CodingKeys: String, CodingKey {
60
+ case architectures
61
+ case modelType = "model_type"
62
+ case vocabularySize = "vocab_size"
63
+ case hiddenSize = "hidden_size"
64
+ case intermediateSize = "intermediate_size"
65
+ case hiddenLayerCount = "num_hidden_layers"
66
+ case attentionHeadCount = "num_attention_heads"
67
+ case keyValueHeadCount = "num_key_value_heads"
68
+ case maximumPositionCount = "max_position_embeddings"
69
+ case rmsNormEpsilon = "rms_norm_eps"
70
+ case ropeTheta = "rope_theta"
71
+ case tieWordEmbeddings = "tie_word_embeddings"
72
+ case attentionBias = "attention_bias"
73
+ case headDimension = "head_dim"
74
+ case ropeScaling = "rope_scaling"
75
+ case quantization
76
+ }
77
+
78
+ public init(from decoder: Decoder) throws {
79
+ let values = try decoder.container(keyedBy: CodingKeys.self)
80
+ architectures = try values.decodeIfPresent([String].self, forKey: .architectures) ?? []
81
+ modelType = try values.decodeIfPresent(String.self, forKey: .modelType) ?? "qwen3"
82
+ vocabularySize = try values.decode(Int.self, forKey: .vocabularySize)
83
+ hiddenSize = try values.decode(Int.self, forKey: .hiddenSize)
84
+ intermediateSize = try values.decode(Int.self, forKey: .intermediateSize)
85
+ hiddenLayerCount = try values.decode(Int.self, forKey: .hiddenLayerCount)
86
+ attentionHeadCount = try values.decode(Int.self, forKey: .attentionHeadCount)
87
+ keyValueHeadCount = try values.decode(Int.self, forKey: .keyValueHeadCount)
88
+ maximumPositionCount = try values.decode(Int.self, forKey: .maximumPositionCount)
89
+ rmsNormEpsilon = try values.decodeIfPresent(Float.self, forKey: .rmsNormEpsilon) ?? 1e-6
90
+ ropeTheta = try values.decodeIfPresent(Float.self, forKey: .ropeTheta) ?? 1_000_000
91
+ tieWordEmbeddings = try values.decodeIfPresent(Bool.self, forKey: .tieWordEmbeddings) ?? true
92
+ attentionBias = try values.decodeIfPresent(Bool.self, forKey: .attentionBias) ?? false
93
+ headDimension = try values.decodeIfPresent(Int.self, forKey: .headDimension)
94
+ ?? hiddenSize / attentionHeadCount
95
+ ropeScaling = try values.decodeIfPresent(RopeScaling.self, forKey: .ropeScaling)
96
+ quantization = try values.decodeIfPresent(Quantization.self, forKey: .quantization)
97
+ }
98
+
99
+ public static func load(from modelURL: URL) throws -> Qwen3ModelConfiguration {
100
+ try JSONDecoder().decode(
101
+ Qwen3ModelConfiguration.self,
102
+ from: Data(contentsOf: modelURL.appendingPathComponent("config.json"))
103
+ )
104
+ }
105
+
106
+ public func validate() throws {
107
+ guard modelType == "qwen3", architectures.isEmpty || architectures.contains("Qwen3ForCausalLM") else {
108
+ throw Qwen3CheckpointError.unsupportedArchitecture(architectures.first ?? modelType)
109
+ }
110
+ guard hiddenSize > 0, intermediateSize > 0, hiddenLayerCount > 0,
111
+ attentionHeadCount > 0, keyValueHeadCount > 0,
112
+ attentionHeadCount.isMultiple(of: keyValueHeadCount), headDimension > 0 else {
113
+ throw Qwen3CheckpointError.invalidConfiguration("invalid model dimensions")
114
+ }
115
+ if let ropeScaling, ropeScaling.ropeType != "yarn" {
116
+ throw Qwen3CheckpointError.invalidConfiguration("unsupported RoPE scaling type \(ropeScaling.ropeType)")
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,84 @@
1
+ import MLX
2
+
3
+ public final class Qwen3InferenceSession: @unchecked Sendable {
4
+ private let model: Qwen3CausalLM
5
+ private let endTokenIDs: Set<Int>
6
+
7
+ public init(model: Qwen3CausalLM, endTokenIDs: Set<Int> = [151643, 151645]) {
8
+ self.model = model
9
+ self.endTokenIDs = endTokenIDs
10
+ }
11
+
12
+ public func generate(
13
+ inputIDs: [Int],
14
+ parameters: GenerationParameters = .balanced,
15
+ isCancelled: (() -> Bool)? = nil,
16
+ onToken: ((Int) -> Bool)? = nil
17
+ ) throws -> TokenGenerationResult {
18
+ try TokenSampler.validate(parameters)
19
+ guard !inputIDs.isEmpty else { throw TextGenerationError.emptyPrompt }
20
+ for token in inputIDs where token < 0 || token >= model.vocabularySize {
21
+ throw TextGenerationError.invalidTokenID(token)
22
+ }
23
+ let requestedLength = inputIDs.count + parameters.maximumNewTokens
24
+ guard requestedLength <= model.maximumContextLength else {
25
+ throw TextGenerationError.contextExceeded(
26
+ requested: requestedLength,
27
+ maximum: model.maximumContextLength
28
+ )
29
+ }
30
+ guard parameters.maximumNewTokens > 0 else {
31
+ return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: [], finishReason: .length)
32
+ }
33
+ if isCancelled?() == true {
34
+ return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: [], finishReason: .cancelled)
35
+ }
36
+ if let seed = parameters.seed { MLXRandom.seed(seed) }
37
+
38
+ let cache = model.makeCache()
39
+ defer {
40
+ cache.forEach { $0.clear() }
41
+ MLX.Memory.clearCache()
42
+ }
43
+ var logits = model.logits(
44
+ for: MLXArray(inputIDs.map(Int32.init)).reshaped([1, inputIDs.count]),
45
+ cache: cache
46
+ )
47
+ var generated: [Int] = []
48
+ var history = inputIDs
49
+ var finishReason: GenerationFinishReason = .length
50
+
51
+ for _ in 0..<parameters.maximumNewTokens {
52
+ if isCancelled?() == true {
53
+ finishReason = .cancelled
54
+ break
55
+ }
56
+ let sampled = try TokenSampler.sample(
57
+ logits: logits,
58
+ parameters: parameters,
59
+ tokenHistory: history
60
+ )
61
+ MLX.eval(sampled)
62
+ let token = Int(sampled.item(Int32.self))
63
+ if endTokenIDs.contains(token) {
64
+ finishReason = .endOfSequence
65
+ break
66
+ }
67
+ generated.append(token)
68
+ history.append(token)
69
+ if onToken?(token) == false {
70
+ finishReason = .callbackStopped
71
+ break
72
+ }
73
+ logits = model.logits(
74
+ for: MLXArray([Int32(token)]).reshaped([1, 1]),
75
+ cache: cache
76
+ )
77
+ }
78
+ return TokenGenerationResult(
79
+ promptTokenCount: inputIDs.count,
80
+ tokenIDs: generated,
81
+ finishReason: finishReason
82
+ )
83
+ }
84
+ }
@@ -0,0 +1,197 @@
1
+ import Foundation
2
+ import MLX
3
+ import MLXNN
4
+
5
+ public final class Qwen3RMSNorm: Module, UnaryLayer {
6
+ @ParameterInfo public var weight: MLXArray
7
+ public let epsilon: Float
8
+
9
+ public init(dimensions: Int, epsilon: Float) {
10
+ _weight = ParameterInfo(wrappedValue: MLXArray.ones([dimensions]))
11
+ self.epsilon = epsilon
12
+ super.init()
13
+ }
14
+
15
+ public func callAsFunction(_ x: MLXArray) -> MLXArray {
16
+ MLXFast.rmsNorm(x, weight: weight, eps: epsilon)
17
+ }
18
+ }
19
+
20
+ public final class Qwen3RotaryEmbedding: Module {
21
+ private let dimensions: Int
22
+ private let base: Float
23
+ private let frequencies: MLXArray?
24
+ private let magnitudeScale: Float
25
+
26
+ public init(configuration: Qwen3ModelConfiguration) {
27
+ dimensions = configuration.headDimension
28
+ base = configuration.ropeTheta
29
+ if let scaling = configuration.ropeScaling {
30
+ precondition(configuration.headDimension.isMultiple(of: 2))
31
+ func correctionDimension(_ rotations: Float) -> Float {
32
+ Float(configuration.headDimension)
33
+ * log(Float(scaling.originalMaximumPositions) / (rotations * 2 * .pi))
34
+ / (2 * log(configuration.ropeTheta))
35
+ }
36
+ let low = max(Int(floor(correctionDimension(scaling.betaFast))), 0)
37
+ let high = min(Int(ceil(correctionDimension(scaling.betaSlow))), configuration.headDimension - 1)
38
+ let safeHigh = low == high ? Float(high) + 0.001 : Float(high)
39
+ let ramp = clip(
40
+ (MLXArray(0..<(configuration.headDimension / 2)).asType(.float32) - Float(low))
41
+ / (safeHigh - Float(low)),
42
+ min: 0,
43
+ max: 1
44
+ )
45
+ let exponent = MLXArray(stride(from: 0, to: configuration.headDimension, by: 2))
46
+ .asType(.float32) / Float(configuration.headDimension)
47
+ let unscaled = pow(configuration.ropeTheta, exponent)
48
+ let interpolated = scaling.factor * unscaled
49
+ let mask = 1 - ramp
50
+ frequencies = (interpolated * unscaled) / (interpolated * mask + unscaled * (1 - mask))
51
+ magnitudeScale = scaling.factor <= 1 ? 1 : 0.1 * log(scaling.factor) + 1
52
+ } else {
53
+ frequencies = nil
54
+ magnitudeScale = 1
55
+ }
56
+ super.init()
57
+ }
58
+
59
+ public func callAsFunction(_ x: MLXArray, offset: Int) -> MLXArray {
60
+ MLXFast.RoPE(
61
+ magnitudeScale == 1 ? x : x * magnitudeScale,
62
+ dimensions: dimensions,
63
+ traditional: false,
64
+ base: frequencies == nil ? base : nil,
65
+ scale: 1,
66
+ offset: offset,
67
+ freqs: frequencies
68
+ )
69
+ }
70
+ }
71
+
72
+ public final class Qwen3SelfAttention: Module {
73
+ private let headCount: Int
74
+ private let keyValueHeadCount: Int
75
+ private let headDimension: Int
76
+ private let scale: Float
77
+
78
+ @ModuleInfo public var q_proj: Linear
79
+ @ModuleInfo public var k_proj: Linear
80
+ @ModuleInfo public var v_proj: Linear
81
+ @ModuleInfo public var o_proj: Linear
82
+ @ModuleInfo public var q_norm: Qwen3RMSNorm
83
+ @ModuleInfo public var k_norm: Qwen3RMSNorm
84
+ @ModuleInfo public var rope: Qwen3RotaryEmbedding
85
+
86
+ public init(configuration: Qwen3ModelConfiguration) {
87
+ headCount = configuration.attentionHeadCount
88
+ keyValueHeadCount = configuration.keyValueHeadCount
89
+ headDimension = configuration.headDimension
90
+ scale = 1 / sqrt(Float(configuration.headDimension))
91
+ _q_proj = ModuleInfo(wrappedValue: Linear(
92
+ configuration.hiddenSize,
93
+ configuration.attentionHeadCount * configuration.headDimension,
94
+ bias: configuration.attentionBias
95
+ ))
96
+ _k_proj = ModuleInfo(wrappedValue: Linear(
97
+ configuration.hiddenSize,
98
+ configuration.keyValueHeadCount * configuration.headDimension,
99
+ bias: configuration.attentionBias
100
+ ))
101
+ _v_proj = ModuleInfo(wrappedValue: Linear(
102
+ configuration.hiddenSize,
103
+ configuration.keyValueHeadCount * configuration.headDimension,
104
+ bias: configuration.attentionBias
105
+ ))
106
+ _o_proj = ModuleInfo(wrappedValue: Linear(
107
+ configuration.attentionHeadCount * configuration.headDimension,
108
+ configuration.hiddenSize,
109
+ bias: configuration.attentionBias
110
+ ))
111
+ _q_norm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
112
+ dimensions: configuration.headDimension,
113
+ epsilon: configuration.rmsNormEpsilon
114
+ ))
115
+ _k_norm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
116
+ dimensions: configuration.headDimension,
117
+ epsilon: configuration.rmsNormEpsilon
118
+ ))
119
+ _rope = ModuleInfo(wrappedValue: Qwen3RotaryEmbedding(configuration: configuration))
120
+ super.init()
121
+ }
122
+
123
+ public func callAsFunction(_ hiddenStates: MLXArray, mask: MLXArray?, cache: TextKVCache?) -> MLXArray {
124
+ let batchSize = hiddenStates.dim(0)
125
+ let sequenceLength = hiddenStates.dim(1)
126
+ var queries = q_proj(hiddenStates).reshaped([batchSize, sequenceLength, headCount, headDimension])
127
+ var keys = k_proj(hiddenStates).reshaped([batchSize, sequenceLength, keyValueHeadCount, headDimension])
128
+ var values = v_proj(hiddenStates).reshaped([batchSize, sequenceLength, keyValueHeadCount, headDimension])
129
+ queries = q_norm(queries).transposed(0, 2, 1, 3)
130
+ keys = k_norm(keys).transposed(0, 2, 1, 3)
131
+ values = values.transposed(0, 2, 1, 3)
132
+ let offset = cache?.length ?? 0
133
+ queries = rope(queries, offset: offset)
134
+ keys = rope(keys, offset: offset)
135
+ if let cache {
136
+ (keys, values) = cache.update(keys: keys, values: values)
137
+ }
138
+ let attended = MLXFast.scaledDotProductAttention(
139
+ queries: queries,
140
+ keys: keys,
141
+ values: values,
142
+ scale: scale,
143
+ mask: mask
144
+ )
145
+ return o_proj(attended.transposed(0, 2, 1, 3).reshaped([
146
+ batchSize, sequenceLength, headCount * headDimension,
147
+ ]))
148
+ }
149
+ }
150
+
151
+ public final class Qwen3FeedForward: Module, @unchecked Sendable {
152
+ @ModuleInfo public var gate_proj: Linear
153
+ @ModuleInfo public var up_proj: Linear
154
+ @ModuleInfo public var down_proj: Linear
155
+
156
+ nonisolated(unsafe) private static let activate: (MLXArray, MLXArray) -> MLXArray = compile {
157
+ silu($0) * $1
158
+ }
159
+
160
+ public init(configuration: Qwen3ModelConfiguration) {
161
+ _gate_proj = ModuleInfo(wrappedValue: Linear(configuration.hiddenSize, configuration.intermediateSize, bias: false))
162
+ _up_proj = ModuleInfo(wrappedValue: Linear(configuration.hiddenSize, configuration.intermediateSize, bias: false))
163
+ _down_proj = ModuleInfo(wrappedValue: Linear(configuration.intermediateSize, configuration.hiddenSize, bias: false))
164
+ super.init()
165
+ }
166
+
167
+ public func callAsFunction(_ x: MLXArray) -> MLXArray {
168
+ down_proj(Self.activate(gate_proj(x), up_proj(x)))
169
+ }
170
+ }
171
+
172
+ public final class Qwen3DecoderBlock: Module {
173
+ @ModuleInfo public var self_attn: Qwen3SelfAttention
174
+ @ModuleInfo public var mlp: Qwen3FeedForward
175
+ @ModuleInfo public var input_layernorm: Qwen3RMSNorm
176
+ @ModuleInfo public var post_attention_layernorm: Qwen3RMSNorm
177
+
178
+ public init(configuration: Qwen3ModelConfiguration) {
179
+ _self_attn = ModuleInfo(wrappedValue: Qwen3SelfAttention(configuration: configuration))
180
+ _mlp = ModuleInfo(wrappedValue: Qwen3FeedForward(configuration: configuration))
181
+ _input_layernorm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
182
+ dimensions: configuration.hiddenSize,
183
+ epsilon: configuration.rmsNormEpsilon
184
+ ))
185
+ _post_attention_layernorm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
186
+ dimensions: configuration.hiddenSize,
187
+ epsilon: configuration.rmsNormEpsilon
188
+ ))
189
+ super.init()
190
+ }
191
+
192
+ public func callAsFunction(_ hiddenStates: MLXArray, mask: MLXArray?, cache: TextKVCache?) -> MLXArray {
193
+ var hidden = hiddenStates + self_attn(input_layernorm(hiddenStates), mask: mask, cache: cache)
194
+ hidden = hidden + mlp(post_attention_layernorm(hidden))
195
+ return hidden
196
+ }
197
+ }
@@ -0,0 +1,130 @@
1
+ import MLX
2
+ import MLXNN
3
+
4
+ public final class Qwen3TransformerModel: Module, HiddenStateModel {
5
+ public let configuration: Qwen3ModelConfiguration
6
+
7
+ @ModuleInfo public var embed_tokens: Embedding
8
+ public var layers: [Qwen3DecoderBlock]
9
+ @ModuleInfo public var norm: Qwen3RMSNorm
10
+
11
+ public var vocabularySize: Int { configuration.vocabularySize }
12
+ public var maximumContextLength: Int { configuration.maximumPositionCount }
13
+
14
+ public init(configuration: Qwen3ModelConfiguration) {
15
+ self.configuration = configuration
16
+ _embed_tokens = ModuleInfo(wrappedValue: Embedding(
17
+ embeddingCount: configuration.vocabularySize,
18
+ dimensions: configuration.hiddenSize
19
+ ))
20
+ layers = (0..<configuration.hiddenLayerCount).map { _ in
21
+ Qwen3DecoderBlock(configuration: configuration)
22
+ }
23
+ _norm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
24
+ dimensions: configuration.hiddenSize,
25
+ epsilon: configuration.rmsNormEpsilon
26
+ ))
27
+ super.init()
28
+ }
29
+
30
+ public func callAsFunction(_ inputIDs: MLXArray, cache: [TextKVCache]? = nil) -> MLXArray {
31
+ var hidden = embed_tokens(inputIDs)
32
+ let mask = causalMask(sequenceLength: inputIDs.dim(1), offset: cache?.first?.length ?? 0)
33
+ for (index, layer) in layers.enumerated() {
34
+ hidden = layer(hidden, mask: mask, cache: cache?[index])
35
+ }
36
+ return norm(hidden)
37
+ }
38
+
39
+ public func hiddenStates(
40
+ for inputIDs: MLXArray,
41
+ at layerIndices: Set<Int>,
42
+ attentionMask: MLXArray? = nil
43
+ ) -> [Int: MLXArray] {
44
+ var hidden = embed_tokens(inputIDs)
45
+ let mask = causalMask(sequenceLength: inputIDs.dim(1), offset: 0, attentionMask: attentionMask)
46
+ var result: [Int: MLXArray] = [:]
47
+ if layerIndices.contains(0) {
48
+ MLX.eval(hidden)
49
+ result[0] = hidden
50
+ }
51
+ for (index, layer) in layers.enumerated() {
52
+ hidden = layer(hidden, mask: mask, cache: nil)
53
+ let layerIndex = index + 1
54
+ if layerIndices.contains(layerIndex) {
55
+ let state = layerIndex == layers.count ? norm(hidden) : hidden
56
+ MLX.eval(state)
57
+ result[layerIndex] = state
58
+ }
59
+ }
60
+ return result
61
+ }
62
+
63
+ private func causalMask(
64
+ sequenceLength: Int,
65
+ offset: Int,
66
+ attentionMask: MLXArray? = nil
67
+ ) -> MLXArray? {
68
+ if sequenceLength == 1, attentionMask == nil { return nil }
69
+ let totalLength = sequenceLength + offset
70
+ let rows = MLXArray.arange(sequenceLength, dtype: .float32).expandedDimensions(axis: 1)
71
+ let columns = MLXArray.arange(totalLength, dtype: .float32).expandedDimensions(axis: 0)
72
+ var mask = MLX.where(
73
+ columns .<= rows + Float(offset),
74
+ MLXArray(Float(0)),
75
+ MLXArray(-Float.infinity)
76
+ ).reshaped([1, 1, sequenceLength, totalLength])
77
+ if let attentionMask {
78
+ let padding = MLX.where(
79
+ attentionMask .== Int32(1),
80
+ MLXArray(Float(0)),
81
+ MLXArray(Float(-1e9))
82
+ ).reshaped([attentionMask.dim(0), 1, 1, attentionMask.dim(1)])
83
+ mask = mask + padding
84
+ }
85
+ return mask
86
+ }
87
+ }
88
+
89
+ public final class Qwen3CausalLM: Module, CausalLanguageModel {
90
+ public let configuration: Qwen3ModelConfiguration
91
+ @ModuleInfo public var model: Qwen3TransformerModel
92
+ @ModuleInfo public var lm_head: Linear
93
+ public var usesTiedOutputHead: Bool
94
+
95
+ public var vocabularySize: Int { configuration.vocabularySize }
96
+ public var maximumContextLength: Int { configuration.maximumPositionCount }
97
+
98
+ public init(configuration: Qwen3ModelConfiguration) {
99
+ self.configuration = configuration
100
+ usesTiedOutputHead = configuration.tieWordEmbeddings
101
+ _model = ModuleInfo(wrappedValue: Qwen3TransformerModel(configuration: configuration))
102
+ _lm_head = ModuleInfo(wrappedValue: Linear(configuration.hiddenSize, configuration.vocabularySize, bias: false))
103
+ super.init()
104
+ }
105
+
106
+ public func logits(for inputIDs: MLXArray, cache: [TextKVCache]) -> MLXArray {
107
+ project(model(inputIDs, cache: cache))
108
+ }
109
+
110
+ public func makeCache() -> [TextKVCache] {
111
+ (0..<configuration.hiddenLayerCount).map { _ in TextKVCache() }
112
+ }
113
+
114
+ private func project(_ hiddenStates: MLXArray) -> MLXArray {
115
+ guard usesTiedOutputHead else { return lm_head(hiddenStates) }
116
+ if let embedding = model.embed_tokens as? QuantizedEmbedding {
117
+ return MLX.quantizedMM(
118
+ hiddenStates,
119
+ embedding.weight,
120
+ scales: embedding.scales,
121
+ biases: embedding.biases,
122
+ transpose: true,
123
+ groupSize: embedding.groupSize,
124
+ bits: embedding.bits,
125
+ mode: .affine
126
+ )
127
+ }
128
+ return matmul(hiddenStates, model.embed_tokens.weight.T)
129
+ }
130
+ }
@@ -0,0 +1,102 @@
1
+ import Foundation
2
+ import MLX
3
+ import MLXNN
4
+
5
+ public enum Qwen35Checkpoint {
6
+ private struct Index: Decodable {
7
+ let weightMap: [String: String]
8
+
9
+ private enum CodingKeys: String, CodingKey { case weightMap = "weight_map" }
10
+ }
11
+
12
+ public static func load(
13
+ from directory: URL,
14
+ expectedQuantization: Qwen35Quantization? = nil
15
+ ) throws -> Qwen35ForCausalLM {
16
+ let config = try Qwen35Configuration.load(from: directory)
17
+ try config.validate(expectedQuantization: expectedQuantization)
18
+ let index = try checkpointIndex(in: directory)
19
+ try validateFiles(index: index, directory: directory, config: config)
20
+ let model = Qwen35ForCausalLM(config: config)
21
+ guard config.quantization.mode.lowercased() == "affine" else {
22
+ throw Qwen35Error.invalidCheckpoint("unsupported quantization mode \(config.quantization.mode)")
23
+ }
24
+ MLXNN.quantize(model: model, groupSize: config.quantization.groupSize, bits: config.quantization.bits, mode: .affine)
25
+
26
+ var weights: [String: MLXArray] = [:]
27
+ var usesUpstreamNormOffsets = false
28
+ for file in Set(index.weightMap.values) {
29
+ let arrays = try MLX.loadArrays(url: directory.appendingPathComponent(file))
30
+ usesUpstreamNormOffsets = usesUpstreamNormOffsets || arrays.contains { name, value in
31
+ name.hasSuffix("conv1d.weight") && value.ndim == 3 && value.dim(2) != 1
32
+ }
33
+ for (name, var value) in arrays {
34
+ guard let key = modelKey(from: name) else { continue }
35
+ if key.hasSuffix("conv1d.weight"), value.ndim == 3, value.dim(2) != 1 {
36
+ value = value.transposed(0, 2, 1)
37
+ }
38
+ weights[key] = value
39
+ }
40
+ }
41
+ if usesUpstreamNormOffsets {
42
+ for key in Array(weights.keys) where isOffsetNorm(key) && weights[key]?.ndim == 1 {
43
+ weights[key] = weights[key]! + 1
44
+ }
45
+ }
46
+ guard !weights.isEmpty else { throw Qwen35Error.invalidCheckpoint("no language-model tensors") }
47
+ do {
48
+ try model.update(parameters: ModuleParameters.unflattened(weights), verify: [.noUnusedKeys, .shapeMismatch])
49
+ } catch {
50
+ throw Qwen35Error.invalidCheckpoint(error.localizedDescription)
51
+ }
52
+ MLX.eval(model)
53
+ MLX.Memory.clearCache()
54
+ return model
55
+ }
56
+
57
+ private static func checkpointIndex(in directory: URL) throws -> Index {
58
+ let indexURL = directory.appendingPathComponent("model.safetensors.index.json")
59
+ if FileManager.default.fileExists(atPath: indexURL.path) {
60
+ return try JSONDecoder().decode(Index.self, from: Data(contentsOf: indexURL))
61
+ }
62
+ let single = directory.appendingPathComponent("model.safetensors")
63
+ guard FileManager.default.fileExists(atPath: single.path) else { throw Qwen35Error.missingFile(single.lastPathComponent) }
64
+ let names = try MLX.loadArrays(url: single).keys
65
+ return Index(weightMap: Dictionary(uniqueKeysWithValues: names.map { ($0, single.lastPathComponent) }))
66
+ }
67
+
68
+ private static func validateFiles(index: Index, directory: URL, config: Qwen35Configuration) throws {
69
+ var required = [
70
+ "language_model.model.embed_tokens.weight",
71
+ "language_model.model.layers.0.linear_attn.in_proj_qkv.weight",
72
+ "language_model.model.layers.0.mlp.gate_proj.weight",
73
+ "language_model.model.norm.weight",
74
+ ]
75
+ if let fullLayer = config.layerTypes.firstIndex(of: "full_attention") {
76
+ required.append("language_model.model.layers.\(fullLayer).self_attn.q_proj.weight")
77
+ }
78
+ if !config.tieWordEmbeddings { required.append("language_model.lm_head.weight") }
79
+ for key in required where index.weightMap[key] == nil {
80
+ throw Qwen35Error.invalidCheckpoint("missing tensor \(key)")
81
+ }
82
+ for file in Set(index.weightMap.values) where !FileManager.default.fileExists(atPath: directory.appendingPathComponent(file).path) {
83
+ throw Qwen35Error.missingFile(file)
84
+ }
85
+ }
86
+
87
+ private static func modelKey(from checkpointKey: String) -> String? {
88
+ let modelPrefix = "language_model.model."
89
+ if checkpointKey.hasPrefix(modelPrefix) { return "model." + checkpointKey.dropFirst(modelPrefix.count) }
90
+ let headPrefix = "language_model.lm_head."
91
+ if checkpointKey.hasPrefix(headPrefix) { return "lm_head." + checkpointKey.dropFirst(headPrefix.count) }
92
+ return nil
93
+ }
94
+
95
+ private static func isOffsetNorm(_ key: String) -> Bool {
96
+ key.hasSuffix(".input_layernorm.weight")
97
+ || key.hasSuffix(".post_attention_layernorm.weight")
98
+ || key.hasSuffix(".q_norm.weight")
99
+ || key.hasSuffix(".k_norm.weight")
100
+ || key == "model.norm.weight"
101
+ }
102
+ }