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,140 @@
1
+ import Foundation
2
+
3
+ public struct Qwen35Quantization: Decodable, Equatable, Sendable {
4
+ public let groupSize: Int
5
+ public let bits: Int
6
+ public let mode: String
7
+
8
+ private enum CodingKeys: String, CodingKey {
9
+ case groupSize = "group_size"
10
+ case bits
11
+ case mode
12
+ }
13
+
14
+ public init(groupSize: Int, bits: Int, mode: String = "affine") {
15
+ self.groupSize = groupSize
16
+ self.bits = bits
17
+ self.mode = mode
18
+ }
19
+
20
+ public init(from decoder: Decoder) throws {
21
+ let container = try decoder.container(keyedBy: CodingKeys.self)
22
+ groupSize = try container.decode(Int.self, forKey: .groupSize)
23
+ bits = try container.decode(Int.self, forKey: .bits)
24
+ mode = try container.decodeIfPresent(String.self, forKey: .mode) ?? "affine"
25
+ }
26
+ }
27
+
28
+ public struct Qwen35Configuration: Decodable, Sendable, QwenHybridConfiguration {
29
+ public let modelType: String
30
+ public let hiddenSize: Int
31
+ public let intermediateSize: Int
32
+ public let numHiddenLayers: Int
33
+ public let numAttentionHeads: Int
34
+ public let numKeyValueHeads: Int
35
+ public let headDim: Int
36
+ public let vocabSize: Int
37
+ public let maxPositionEmbeddings: Int
38
+ public let rmsNormEps: Float
39
+ public let hiddenAct: String
40
+ public let attentionBias: Bool
41
+ public let tieWordEmbeddings: Bool
42
+ public let layerTypes: [String]
43
+ public let fullAttentionInterval: Int
44
+ public let linearConvKernelDim: Int
45
+ public let linearKeyHeadDim: Int
46
+ public let linearValueHeadDim: Int
47
+ public let linearNumKeyHeads: Int
48
+ public let linearNumValueHeads: Int
49
+ public let ropeTheta: Float
50
+ public let partialRotaryFactor: Float
51
+ public let quantization: Qwen35Quantization
52
+
53
+ private enum RootKeys: String, CodingKey {
54
+ case modelType = "model_type"
55
+ case textConfig = "text_config"
56
+ case quantization
57
+ case quantizationConfig = "quantization_config"
58
+ }
59
+ private enum TextKeys: String, CodingKey {
60
+ case hiddenSize = "hidden_size", intermediateSize = "intermediate_size", numHiddenLayers = "num_hidden_layers"
61
+ case numAttentionHeads = "num_attention_heads", numKeyValueHeads = "num_key_value_heads", headDim = "head_dim"
62
+ case vocabSize = "vocab_size", maxPositionEmbeddings = "max_position_embeddings", rmsNormEps = "rms_norm_eps"
63
+ case hiddenAct = "hidden_act", attentionBias = "attention_bias", tieWordEmbeddings = "tie_word_embeddings"
64
+ case layerTypes = "layer_types", fullAttentionInterval = "full_attention_interval"
65
+ case linearConvKernelDim = "linear_conv_kernel_dim", linearKeyHeadDim = "linear_key_head_dim"
66
+ case linearValueHeadDim = "linear_value_head_dim", linearNumKeyHeads = "linear_num_key_heads"
67
+ case linearNumValueHeads = "linear_num_value_heads", ropeParameters = "rope_parameters"
68
+ }
69
+ private enum RopeKeys: String, CodingKey {
70
+ case ropeTheta = "rope_theta"
71
+ case partialRotaryFactor = "partial_rotary_factor"
72
+ }
73
+
74
+ public init(from decoder: Decoder) throws {
75
+ let root = try decoder.container(keyedBy: RootKeys.self)
76
+ modelType = try root.decode(String.self, forKey: .modelType)
77
+ quantization = try root.decodeIfPresent(Qwen35Quantization.self, forKey: .quantization)
78
+ ?? root.decode(Qwen35Quantization.self, forKey: .quantizationConfig)
79
+ let text = try root.nestedContainer(keyedBy: TextKeys.self, forKey: .textConfig)
80
+ hiddenSize = try text.decode(Int.self, forKey: .hiddenSize)
81
+ intermediateSize = try text.decode(Int.self, forKey: .intermediateSize)
82
+ numHiddenLayers = try text.decode(Int.self, forKey: .numHiddenLayers)
83
+ numAttentionHeads = try text.decode(Int.self, forKey: .numAttentionHeads)
84
+ numKeyValueHeads = try text.decode(Int.self, forKey: .numKeyValueHeads)
85
+ headDim = try text.decode(Int.self, forKey: .headDim)
86
+ vocabSize = try text.decode(Int.self, forKey: .vocabSize)
87
+ maxPositionEmbeddings = try text.decode(Int.self, forKey: .maxPositionEmbeddings)
88
+ rmsNormEps = try text.decode(Float.self, forKey: .rmsNormEps)
89
+ hiddenAct = try text.decode(String.self, forKey: .hiddenAct)
90
+ attentionBias = try text.decode(Bool.self, forKey: .attentionBias)
91
+ tieWordEmbeddings = try text.decode(Bool.self, forKey: .tieWordEmbeddings)
92
+ layerTypes = try text.decode([String].self, forKey: .layerTypes)
93
+ fullAttentionInterval = try text.decode(Int.self, forKey: .fullAttentionInterval)
94
+ linearConvKernelDim = try text.decode(Int.self, forKey: .linearConvKernelDim)
95
+ linearKeyHeadDim = try text.decode(Int.self, forKey: .linearKeyHeadDim)
96
+ linearValueHeadDim = try text.decode(Int.self, forKey: .linearValueHeadDim)
97
+ linearNumKeyHeads = try text.decode(Int.self, forKey: .linearNumKeyHeads)
98
+ linearNumValueHeads = try text.decode(Int.self, forKey: .linearNumValueHeads)
99
+ let rope = try text.nestedContainer(keyedBy: RopeKeys.self, forKey: .ropeParameters)
100
+ ropeTheta = try rope.decode(Float.self, forKey: .ropeTheta)
101
+ partialRotaryFactor = try rope.decode(Float.self, forKey: .partialRotaryFactor)
102
+ }
103
+
104
+ public static func load(from directory: URL) throws -> Self {
105
+ try JSONDecoder().decode(Self.self, from: Data(contentsOf: directory.appendingPathComponent("config.json")))
106
+ }
107
+
108
+ public func validate(expectedQuantization: Qwen35Quantization? = nil) throws {
109
+ guard modelType == "qwen3_5" else { throw Qwen35Error.invalidConfiguration("model_type is \(modelType)") }
110
+ guard numHiddenLayers > 0, layerTypes.count == numHiddenLayers else { throw Qwen35Error.invalidConfiguration("layer_types count does not match num_hidden_layers") }
111
+ guard fullAttentionInterval > 0 else { throw Qwen35Error.invalidConfiguration("full_attention_interval must be positive") }
112
+ guard layerTypes.indices.allSatisfy({ layerTypes[$0] == (($0 + 1).isMultiple(of: fullAttentionInterval) ? "full_attention" : "linear_attention") }) else {
113
+ throw Qwen35Error.invalidConfiguration("layer_types does not match full_attention_interval")
114
+ }
115
+ guard hiddenAct == "silu" else { throw Qwen35Error.invalidConfiguration("hidden_act must be silu") }
116
+ if let expectedQuantization, quantization != expectedQuantization {
117
+ throw Qwen35Error.invalidConfiguration("checkpoint quantization does not match the requested runtime policy")
118
+ }
119
+ }
120
+ }
121
+
122
+ public enum Qwen35Error: LocalizedError {
123
+ case invalidConfiguration(String)
124
+ case invalidCheckpoint(String)
125
+ case missingFile(String)
126
+ case emptyPrompt
127
+ case contextExceeded(requested: Int, maximum: Int)
128
+ case decodeBeforePrefill
129
+
130
+ public var errorDescription: String? {
131
+ switch self {
132
+ case .invalidConfiguration(let message): "Invalid Qwen3.5 configuration: \(message)"
133
+ case .invalidCheckpoint(let message): "Invalid Qwen3.5 checkpoint: \(message)"
134
+ case .missingFile(let file): "Missing Qwen3.5 file: \(file)"
135
+ case .emptyPrompt: "Qwen3.5 requires at least one input token"
136
+ case .contextExceeded(let requested, let maximum): "Qwen3.5 context length \(requested) exceeds \(maximum)"
137
+ case .decodeBeforePrefill: "Qwen3.5 decode called before prefill"
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,112 @@
1
+ import MLX
2
+ import MLXNN
3
+
4
+ public final class Qwen35MLP: Module, @unchecked Sendable {
5
+ @ModuleInfo public var gate_proj: Linear
6
+ @ModuleInfo public var up_proj: Linear
7
+ @ModuleInfo public var down_proj: Linear
8
+
9
+ public init(config: Qwen35Configuration) {
10
+ _gate_proj = ModuleInfo(wrappedValue: Linear(config.hiddenSize, config.intermediateSize, bias: false))
11
+ _up_proj = ModuleInfo(wrappedValue: Linear(config.hiddenSize, config.intermediateSize, bias: false))
12
+ _down_proj = ModuleInfo(wrappedValue: Linear(config.intermediateSize, config.hiddenSize, bias: false))
13
+ super.init()
14
+ }
15
+
16
+ public func callAsFunction(_ input: MLXArray) -> MLXArray {
17
+ down_proj(MLXNN.silu(gate_proj(input)) * up_proj(input))
18
+ }
19
+ }
20
+
21
+ public final class Qwen35DecoderLayer: Module, @unchecked Sendable {
22
+ @ModuleInfo public var self_attn: QwenHybridFullAttention?
23
+ @ModuleInfo public var linear_attn: QwenHybridLinearAttention?
24
+ @ModuleInfo public var mlp: Qwen35MLP
25
+ @ModuleInfo public var input_layernorm: QwenHybridRMSNorm
26
+ @ModuleInfo public var post_attention_layernorm: QwenHybridRMSNorm
27
+
28
+ public init(config: Qwen35Configuration, layerIndex: Int) {
29
+ let isFullAttention = config.layerTypes[layerIndex] == "full_attention"
30
+ _self_attn = ModuleInfo(wrappedValue: isFullAttention ? QwenHybridFullAttention(config: config) : nil)
31
+ _linear_attn = ModuleInfo(wrappedValue: isFullAttention ? nil : QwenHybridLinearAttention(config: config))
32
+ _mlp = ModuleInfo(wrappedValue: Qwen35MLP(config: config))
33
+ _input_layernorm = ModuleInfo(wrappedValue: QwenHybridRMSNorm(dimensions: config.hiddenSize, epsilon: config.rmsNormEps))
34
+ _post_attention_layernorm = ModuleInfo(wrappedValue: QwenHybridRMSNorm(dimensions: config.hiddenSize, epsilon: config.rmsNormEps))
35
+ super.init()
36
+ }
37
+
38
+ public func callAsFunction(_ input: MLXArray, mask: MLXArray?, cache: QwenHybridLayerCache) -> MLXArray {
39
+ let normalized = input_layernorm(input)
40
+ let mixed = linear_attn.map { $0(normalized, cache: cache) } ?? self_attn!(normalized, mask: mask, cache: cache)
41
+ let residual = input + mixed
42
+ return residual + mlp(post_attention_layernorm(residual))
43
+ }
44
+ }
45
+
46
+ public final class Qwen35Model: Module, @unchecked Sendable {
47
+ public let config: Qwen35Configuration
48
+ @ModuleInfo public var embed_tokens: Embedding
49
+ public var layers: [Qwen35DecoderLayer]
50
+ @ModuleInfo public var norm: QwenHybridRMSNorm
51
+
52
+ public init(config: Qwen35Configuration) {
53
+ self.config = config
54
+ _embed_tokens = ModuleInfo(wrappedValue: Embedding(embeddingCount: config.vocabSize, dimensions: config.hiddenSize))
55
+ layers = config.layerTypes.indices.map { Qwen35DecoderLayer(config: config, layerIndex: $0) }
56
+ _norm = ModuleInfo(wrappedValue: QwenHybridRMSNorm(dimensions: config.hiddenSize, epsilon: config.rmsNormEps))
57
+ super.init()
58
+ }
59
+
60
+ public func callAsFunction(_ tokenIDs: MLXArray, cache: [QwenHybridLayerCache]) -> MLXArray {
61
+ var hidden = embed_tokens(tokenIDs)
62
+ let length = tokenIDs.dim(1)
63
+ let offset = cache.first?.position ?? 0
64
+ let rows = MLXArray.arange(length, dtype: .float32).expandedDimensions(axis: 1)
65
+ let columns = MLXArray.arange(length + offset, dtype: .float32).expandedDimensions(axis: 0)
66
+ let mask: MLXArray? = length == 1 ? nil : MLX.where(
67
+ columns .<= rows + Float(offset),
68
+ MLXArray(Float(0)),
69
+ MLXArray(-Float.infinity)
70
+ ).reshaped([1, 1, length, length + offset])
71
+ for index in layers.indices {
72
+ hidden = layers[index](hidden, mask: mask, cache: cache[index])
73
+ }
74
+ cache.forEach { $0.position += length }
75
+ return norm(hidden)
76
+ }
77
+ }
78
+
79
+ public final class Qwen35ForCausalLM: Module, @unchecked Sendable {
80
+ public let config: Qwen35Configuration
81
+ public var model: Qwen35Model
82
+ @ModuleInfo public var lm_head: Linear?
83
+
84
+ public init(config: Qwen35Configuration) {
85
+ self.config = config
86
+ model = Qwen35Model(config: config)
87
+ _lm_head = ModuleInfo(wrappedValue: config.tieWordEmbeddings ? nil : Linear(config.hiddenSize, config.vocabSize, bias: false))
88
+ super.init()
89
+ }
90
+
91
+ public func callAsFunction(_ tokenIDs: MLXArray, cache: [QwenHybridLayerCache]) -> MLXArray {
92
+ let hidden = model(tokenIDs, cache: cache)
93
+ if let lm_head { return lm_head(hidden) }
94
+ if let embedding = model.embed_tokens as? QuantizedEmbedding {
95
+ return MLX.quantizedMM(
96
+ hidden,
97
+ embedding.weight,
98
+ scales: embedding.scales,
99
+ biases: embedding.biases,
100
+ transpose: true,
101
+ groupSize: embedding.groupSize,
102
+ bits: embedding.bits,
103
+ mode: embedding.mode
104
+ )
105
+ }
106
+ return MLX.matmul(hidden, model.embed_tokens.weight.T)
107
+ }
108
+
109
+ public func makeCache() -> [QwenHybridLayerCache] {
110
+ config.layerTypes.map { _ in QwenHybridLayerCache() }
111
+ }
112
+ }
@@ -0,0 +1,121 @@
1
+ import Foundation
2
+ import MLX
3
+
4
+ public final class Qwen35Session: @unchecked Sendable {
5
+ public let model: Qwen35ForCausalLM
6
+ public let cache: [QwenHybridLayerCache]
7
+ private var nextLogits: MLXArray?
8
+
9
+ public var contextLength: Int { cache.first?.position ?? 0 }
10
+ public var maximumContextLength: Int { model.config.maxPositionEmbeddings }
11
+
12
+ public init(model: Qwen35ForCausalLM) {
13
+ self.model = model
14
+ cache = model.makeCache()
15
+ }
16
+
17
+ public func reset() {
18
+ cache.forEach { $0.clear() }
19
+ nextLogits = nil
20
+ MLX.Memory.clearCache()
21
+ }
22
+
23
+ public func prefill(
24
+ tokenIDs: [Int],
25
+ chunkSize: Int = 512,
26
+ isCancelled: (@Sendable () -> Bool)? = nil
27
+ ) async throws {
28
+ guard !tokenIDs.isEmpty else { throw Qwen35Error.emptyPrompt }
29
+ guard contextLength + tokenIDs.count <= maximumContextLength else {
30
+ throw Qwen35Error.contextExceeded(requested: contextLength + tokenIDs.count, maximum: maximumContextLength)
31
+ }
32
+ var offset = 0
33
+ while offset < tokenIDs.count {
34
+ if isCancelled?() == true { throw CancellationError() }
35
+ try Task.checkCancellation()
36
+ let end = min(offset + max(1, chunkSize), tokenIDs.count)
37
+ let input = MLXArray(tokenIDs[offset..<end].map(Int32.init)).reshaped([1, end - offset])
38
+ let logits = model(input, cache: cache)[0..., -1, 0...]
39
+ MLX.eval(logits)
40
+ nextLogits = logits
41
+ offset = end
42
+ if offset < tokenIDs.count { MLX.Memory.clearCache() }
43
+ await Task.yield()
44
+ }
45
+ }
46
+
47
+ public func decodeNext(parameters: GenerationParameters = .balanced, tokenHistory: [Int] = []) throws -> Int {
48
+ try Task.checkCancellation()
49
+ guard let nextLogits else { throw Qwen35Error.decodeBeforePrefill }
50
+ guard contextLength < maximumContextLength else {
51
+ throw Qwen35Error.contextExceeded(requested: contextLength + 1, maximum: maximumContextLength)
52
+ }
53
+ let tokenArray = try TokenSampler.sample(logits: nextLogits, parameters: parameters, tokenHistory: tokenHistory)
54
+ MLX.eval(tokenArray)
55
+ let token = Int(tokenArray.item(Int32.self))
56
+ let input = MLXArray([Int32(token)]).reshaped([1, 1])
57
+ let logits = model(input, cache: cache)[0..., -1, 0...]
58
+ MLX.asyncEval(logits)
59
+ self.nextLogits = logits
60
+ return token
61
+ }
62
+
63
+ public func generate(
64
+ inputIDs: [Int],
65
+ parameters: GenerationParameters = .balanced,
66
+ endTokenIDs: Set<Int> = [151643, 151645],
67
+ chunkSize: Int = 512,
68
+ isCancelled: (@Sendable () -> Bool)? = nil,
69
+ onToken: (@Sendable (Int) -> Bool)? = nil
70
+ ) async throws -> TokenGenerationResult {
71
+ try TokenSampler.validate(parameters)
72
+ guard !inputIDs.isEmpty else { throw TextGenerationError.emptyPrompt }
73
+ for token in inputIDs where token < 0 || token >= model.config.vocabSize {
74
+ throw TextGenerationError.invalidTokenID(token)
75
+ }
76
+ let requestedLength = inputIDs.count + parameters.maximumNewTokens
77
+ guard requestedLength <= maximumContextLength else {
78
+ throw TextGenerationError.contextExceeded(requested: requestedLength, maximum: maximumContextLength)
79
+ }
80
+ if let seed = parameters.seed { MLXRandom.seed(seed) }
81
+ reset()
82
+ if parameters.maximumNewTokens == 0 {
83
+ return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: [], finishReason: .length)
84
+ }
85
+ if isCancelled?() == true || Task.isCancelled {
86
+ return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: [], finishReason: .cancelled)
87
+ }
88
+ do {
89
+ try await prefill(tokenIDs: inputIDs, chunkSize: chunkSize, isCancelled: isCancelled)
90
+ } catch is CancellationError {
91
+ return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: [], finishReason: .cancelled)
92
+ }
93
+ var generated: [Int] = []
94
+ var history = inputIDs
95
+ var finishReason: GenerationFinishReason = .length
96
+ generated.reserveCapacity(parameters.maximumNewTokens)
97
+ for _ in 0..<parameters.maximumNewTokens {
98
+ if isCancelled?() == true || Task.isCancelled {
99
+ finishReason = .cancelled
100
+ break
101
+ }
102
+ let token = try decodeNext(parameters: parameters, tokenHistory: history)
103
+ if endTokenIDs.contains(token) {
104
+ finishReason = .endOfSequence
105
+ break
106
+ }
107
+ generated.append(token)
108
+ history.append(token)
109
+ if onToken?(token) == false {
110
+ finishReason = .callbackStopped
111
+ break
112
+ }
113
+ await Task.yield()
114
+ }
115
+ return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: generated, finishReason: finishReason)
116
+ }
117
+ }
118
+
119
+ // CausalLanguageModel currently requires [TextKVCache], which cannot carry the
120
+ // convolution and recurrent states needed by hybrid layers. Qwen35Session still
121
+ // uses the generic generation parameters, sampler, result, and error types.
@@ -0,0 +1,42 @@
1
+ import MLX
2
+
3
+ public final class QwenHybridKVCache: @unchecked Sendable {
4
+ public private(set) var keys: MLXArray?
5
+ public private(set) var values: MLXArray?
6
+
7
+ public var length: Int { keys?.dim(2) ?? 0 }
8
+
9
+ public init() {}
10
+
11
+ public func update(keys newKeys: MLXArray, values newValues: MLXArray) -> (MLXArray, MLXArray) {
12
+ if let keys, let values {
13
+ self.keys = MLX.concatenated([keys, newKeys], axis: 2)
14
+ self.values = MLX.concatenated([values, newValues], axis: 2)
15
+ } else {
16
+ keys = newKeys
17
+ values = newValues
18
+ }
19
+ return (keys!, values!)
20
+ }
21
+
22
+ public func clear() {
23
+ keys = nil
24
+ values = nil
25
+ }
26
+ }
27
+
28
+ public final class QwenHybridLayerCache: @unchecked Sendable {
29
+ public let kv = QwenHybridKVCache()
30
+ public var convolutionState: MLXArray?
31
+ public var recurrentState: MLXArray?
32
+ public var position = 0
33
+
34
+ public init() {}
35
+
36
+ public func clear() {
37
+ kv.clear()
38
+ convolutionState = nil
39
+ recurrentState = nil
40
+ position = 0
41
+ }
42
+ }
@@ -0,0 +1,57 @@
1
+ import Foundation
2
+ import MLX
3
+ import MLXNN
4
+
5
+ public final class QwenHybridFullAttention: Module, @unchecked Sendable {
6
+ private let numHeads: Int
7
+ private let numKVHeads: Int
8
+ private let headDim: Int
9
+ private let rotaryDimensions: Int
10
+ private let ropeBase: Float
11
+ private let scale: Float
12
+
13
+ @ModuleInfo public var q_proj: Linear
14
+ @ModuleInfo public var k_proj: Linear
15
+ @ModuleInfo public var v_proj: Linear
16
+ @ModuleInfo public var o_proj: Linear
17
+ @ModuleInfo public var q_norm: QwenHybridRMSNorm
18
+ @ModuleInfo public var k_norm: QwenHybridRMSNorm
19
+
20
+ public init(config: any QwenHybridConfiguration) {
21
+ numHeads = config.numAttentionHeads
22
+ numKVHeads = config.numKeyValueHeads
23
+ headDim = config.headDim
24
+ rotaryDimensions = Int(Float(config.headDim) * config.partialRotaryFactor)
25
+ ropeBase = config.ropeTheta
26
+ scale = 1 / sqrt(Float(config.headDim))
27
+ _q_proj = ModuleInfo(wrappedValue: Linear(config.hiddenSize, config.numAttentionHeads * config.headDim * 2, bias: config.attentionBias))
28
+ _k_proj = ModuleInfo(wrappedValue: Linear(config.hiddenSize, config.numKeyValueHeads * config.headDim, bias: config.attentionBias))
29
+ _v_proj = ModuleInfo(wrappedValue: Linear(config.hiddenSize, config.numKeyValueHeads * config.headDim, bias: config.attentionBias))
30
+ _o_proj = ModuleInfo(wrappedValue: Linear(config.numAttentionHeads * config.headDim, config.hiddenSize, bias: config.attentionBias))
31
+ _q_norm = ModuleInfo(wrappedValue: QwenHybridRMSNorm(dimensions: config.headDim, epsilon: config.rmsNormEps))
32
+ _k_norm = ModuleInfo(wrappedValue: QwenHybridRMSNorm(dimensions: config.headDim, epsilon: config.rmsNormEps))
33
+ super.init()
34
+ }
35
+
36
+ public func callAsFunction(_ input: MLXArray, mask: MLXArray?, cache: QwenHybridLayerCache) -> MLXArray {
37
+ let batch = input.dim(0)
38
+ let length = input.dim(1)
39
+ let projected = q_proj(input).reshaped([batch, length, numHeads, headDim * 2]).split(parts: 2, axis: -1)
40
+ var query = q_norm(projected[0]).transposed(0, 2, 1, 3)
41
+ let outputGate = projected[1].reshaped([batch, length, numHeads * headDim])
42
+ var key = k_norm(k_proj(input).reshaped([batch, length, numKVHeads, headDim])).transposed(0, 2, 1, 3)
43
+ var value = v_proj(input).reshaped([batch, length, numKVHeads, headDim]).transposed(0, 2, 1, 3)
44
+ query = MLXFast.RoPE(query, dimensions: rotaryDimensions, traditional: false, base: ropeBase, scale: 1, offset: cache.position)
45
+ key = MLXFast.RoPE(key, dimensions: rotaryDimensions, traditional: false, base: ropeBase, scale: 1, offset: cache.position)
46
+ (key, value) = cache.kv.update(keys: key, values: value)
47
+ let attended = MLXFast.scaledDotProductAttention(
48
+ queries: query,
49
+ keys: key,
50
+ values: value,
51
+ scale: scale,
52
+ mask: mask?.asType(query.dtype)
53
+ )
54
+ let merged = attended.transposed(0, 2, 1, 3).reshaped([batch, length, numHeads * headDim])
55
+ return o_proj(merged * MLXNN.sigmoid(outputGate))
56
+ }
57
+ }
@@ -0,0 +1,58 @@
1
+ import MLX
2
+ import MLXNN
3
+
4
+ public protocol QwenHybridConfiguration: Sendable {
5
+ var hiddenSize: Int { get }
6
+ var numAttentionHeads: Int { get }
7
+ var numKeyValueHeads: Int { get }
8
+ var headDim: Int { get }
9
+ var partialRotaryFactor: Float { get }
10
+ var attentionBias: Bool { get }
11
+ var rmsNormEps: Float { get }
12
+ var ropeTheta: Float { get }
13
+ var linearNumValueHeads: Int { get }
14
+ var linearNumKeyHeads: Int { get }
15
+ var linearKeyHeadDim: Int { get }
16
+ var linearValueHeadDim: Int { get }
17
+ var linearConvKernelDim: Int { get }
18
+ }
19
+
20
+ public final class QwenHybridRMSNorm: Module, UnaryLayer, @unchecked Sendable {
21
+ @ParameterInfo public var weight: MLXArray
22
+ public let epsilon: Float
23
+
24
+ public init(dimensions: Int, epsilon: Float) {
25
+ _weight = ParameterInfo(wrappedValue: MLXArray.zeros([dimensions]))
26
+ self.epsilon = epsilon
27
+ super.init()
28
+ }
29
+
30
+ public func callAsFunction(_ input: MLXArray) -> MLXArray {
31
+ MLXFast.rmsNorm(input, weight: weight, eps: epsilon)
32
+ }
33
+ }
34
+
35
+ public final class QwenHybridGatedRMSNorm: Module, @unchecked Sendable {
36
+ @ParameterInfo public var weight: MLXArray
37
+ public let epsilon: Float
38
+
39
+ public init(dimensions: Int, epsilon: Float) {
40
+ _weight = ParameterInfo(wrappedValue: MLXArray.zeros([dimensions]))
41
+ self.epsilon = epsilon
42
+ super.init()
43
+ }
44
+
45
+ public func callAsFunction(_ input: MLXArray, gate: MLXArray) -> MLXArray {
46
+ let normalized = MLXFast.rmsNorm(input, weight: weight, eps: epsilon)
47
+ return (MLXNN.silu(gate.asType(.float32)) * normalized.asType(.float32)).asType(input.dtype)
48
+ }
49
+ }
50
+
51
+ public final class QwenHybridDepthwiseConv1D: Module, @unchecked Sendable {
52
+ @ParameterInfo public var weight: MLXArray
53
+
54
+ public init(channels: Int, kernelSize: Int) {
55
+ _weight = ParameterInfo(wrappedValue: MLX.zeros([channels, kernelSize, 1]))
56
+ super.init()
57
+ }
58
+ }
@@ -0,0 +1,111 @@
1
+ import Foundation
2
+ import MLX
3
+ import MLXNN
4
+
5
+ public final class QwenHybridLinearAttention: Module, @unchecked Sendable {
6
+ private let numValueHeads: Int
7
+ private let numKeyHeads: Int
8
+ private let keyHeadDim: Int
9
+ private let valueHeadDim: Int
10
+ private let keyDimension: Int
11
+ private let valueDimension: Int
12
+ private let convolutionDimension: Int
13
+ private let kernelSize: Int
14
+ private let keyNormWeight: MLXArray
15
+
16
+ @ModuleInfo public var in_proj_qkv: Linear
17
+ @ModuleInfo public var in_proj_z: Linear
18
+ @ModuleInfo public var in_proj_b: Linear
19
+ @ModuleInfo public var in_proj_a: Linear
20
+ @ModuleInfo public var out_proj: Linear
21
+ @ModuleInfo public var norm: QwenHybridGatedRMSNorm
22
+ @ModuleInfo public var conv1d: QwenHybridDepthwiseConv1D
23
+ @ParameterInfo public var dt_bias: MLXArray
24
+ @ParameterInfo public var A_log: MLXArray
25
+
26
+ public init(config: any QwenHybridConfiguration) {
27
+ numValueHeads = config.linearNumValueHeads
28
+ numKeyHeads = config.linearNumKeyHeads
29
+ keyHeadDim = config.linearKeyHeadDim
30
+ valueHeadDim = config.linearValueHeadDim
31
+ keyDimension = config.linearNumKeyHeads * config.linearKeyHeadDim
32
+ valueDimension = config.linearNumValueHeads * config.linearValueHeadDim
33
+ convolutionDimension = keyDimension * 2 + valueDimension
34
+ kernelSize = config.linearConvKernelDim
35
+ keyNormWeight = MLX.ones([config.linearKeyHeadDim])
36
+ _in_proj_qkv = ModuleInfo(wrappedValue: Linear(config.hiddenSize, convolutionDimension, bias: false))
37
+ _in_proj_z = ModuleInfo(wrappedValue: Linear(config.hiddenSize, valueDimension, bias: false))
38
+ _in_proj_b = ModuleInfo(wrappedValue: Linear(config.hiddenSize, numValueHeads, bias: false))
39
+ _in_proj_a = ModuleInfo(wrappedValue: Linear(config.hiddenSize, numValueHeads, bias: false))
40
+ _out_proj = ModuleInfo(wrappedValue: Linear(valueDimension, config.hiddenSize, bias: false))
41
+ _norm = ModuleInfo(wrappedValue: QwenHybridGatedRMSNorm(dimensions: valueHeadDim, epsilon: config.rmsNormEps))
42
+ _conv1d = ModuleInfo(wrappedValue: QwenHybridDepthwiseConv1D(channels: convolutionDimension, kernelSize: kernelSize))
43
+ _dt_bias = ParameterInfo(wrappedValue: MLX.ones([numValueHeads]))
44
+ _A_log = ParameterInfo(wrappedValue: MLX.zeros([numValueHeads]))
45
+ super.init()
46
+ }
47
+
48
+ public func callAsFunction(_ input: MLXArray, cache: QwenHybridLayerCache) -> MLXArray {
49
+ let batch = input.dim(0)
50
+ let length = input.dim(1)
51
+ let mixed = causalConvolution(in_proj_qkv(input), cache: cache)
52
+ let pieces = mixed.split(indices: [keyDimension, keyDimension * 2], axis: -1)
53
+ var query = pieces[0].reshaped([batch, length, numKeyHeads, keyHeadDim])
54
+ var key = pieces[1].reshaped([batch, length, numKeyHeads, keyHeadDim])
55
+ let value = pieces[2].reshaped([batch, length, numValueHeads, valueHeadDim]).asType(.float32)
56
+ query = MLXFast.rmsNorm(query.asType(.float32), weight: keyNormWeight, eps: 1e-6) / Float(keyHeadDim)
57
+ key = MLXFast.rmsNorm(key.asType(.float32), weight: keyNormWeight, eps: 1e-6) / sqrt(Float(keyHeadDim))
58
+ let gate = MLX.exp(-MLX.exp(A_log.asType(.float32)) * MLXNN.softplus(in_proj_a(input).asType(.float32) + dt_bias))
59
+ let beta = MLX.sigmoid(in_proj_b(input)).asType(.float32)
60
+ let initialState = cache.recurrentState ?? MLX.zeros([batch, numValueHeads, valueHeadDim, keyHeadDim], dtype: .float32)
61
+ let updated = QwenHybridGatedDelta.update(query: query, key: key, value: value, gate: gate, beta: beta, state: initialState)
62
+ cache.recurrentState = updated.state
63
+ let normalized = norm(
64
+ updated.output.asType(input.dtype).reshaped([-1, valueHeadDim]),
65
+ gate: in_proj_z(input).reshaped([-1, valueHeadDim])
66
+ )
67
+ return out_proj(normalized.reshaped([batch, length, valueDimension]))
68
+ }
69
+
70
+ private func causalConvolution(_ input: MLXArray, cache: QwenHybridLayerCache) -> MLXArray {
71
+ let length = input.dim(1)
72
+ if let previous = cache.convolutionState {
73
+ let combined = MLX.concatenated([previous, input], axis: 1)
74
+ cache.convolutionState = combined[0..., (combined.dim(1) - kernelSize + 1)..., 0...]
75
+ return MLXNN.silu(MLX.conv1d(combined, conv1d.weight, padding: 0, groups: convolutionDimension))
76
+ }
77
+ let result = MLXNN.silu(MLX.conv1d(input, conv1d.weight, padding: kernelSize - 1, groups: convolutionDimension))
78
+ let paddedInput = padded(input, widths: [[0, 0], [max(0, kernelSize - 1 - length), 0], [0, 0]])
79
+ cache.convolutionState = paddedInput[0..., (paddedInput.dim(1) - kernelSize + 1)..., 0...]
80
+ return result[0..., 0..<length, 0...]
81
+ }
82
+ }
83
+
84
+ public enum QwenHybridGatedDelta {
85
+ public static func update(
86
+ query: MLXArray,
87
+ key: MLXArray,
88
+ value: MLXArray,
89
+ gate: MLXArray,
90
+ beta: MLXArray,
91
+ state initialState: MLXArray
92
+ ) -> (output: MLXArray, state: MLXArray) {
93
+ let repeats = value.dim(2) / key.dim(2)
94
+ let query = repeats == 1 ? query : MLX.repeated(query, count: repeats, axis: 2)
95
+ let key = repeats == 1 ? key : MLX.repeated(key, count: repeats, axis: 2)
96
+ var state = initialState
97
+ var outputs: [MLXArray] = []
98
+ outputs.reserveCapacity(query.dim(1))
99
+ for index in 0..<query.dim(1) {
100
+ let q = query[0..., index, 0..., 0...]
101
+ let k = key[0..., index, 0..., 0...]
102
+ let v = value[0..., index, 0..., 0...]
103
+ state = state * gate[0..., index, 0...].expandedDimensions(axes: [2, 3])
104
+ let remembered = (state * k.expandedDimensions(axis: 2)).sum(axis: -1)
105
+ let delta = (v - remembered) * beta[0..., index, 0...].expandedDimensions(axis: -1)
106
+ state = state + k.expandedDimensions(axis: 2) * delta.expandedDimensions(axis: -1)
107
+ outputs.append((state * q.expandedDimensions(axis: 2)).sum(axis: -1))
108
+ }
109
+ return (MLX.stacked(outputs, axis: 1), state)
110
+ }
111
+ }