bleam 0.0.10 → 0.0.12

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 (90) hide show
  1. package/dist/ai.cjs +1872 -1182
  2. package/dist/ai.d.cts +267 -234
  3. package/dist/ai.d.ts +267 -234
  4. package/dist/ai.js +1859 -1167
  5. package/dist/cli.cjs +3 -183
  6. package/dist/cli.d.cts +3 -47
  7. package/dist/cli.d.ts +3 -47
  8. package/dist/cli.js +3 -183
  9. package/dist/config.d.cts +1 -1
  10. package/dist/config.d.ts +1 -1
  11. package/dist/{files-Ds1wT8C2.js → files-BXVkPrPN.js} +6 -1
  12. package/dist/{files-Bo7h9fik.cjs → files-DxaQ-Nv0.cjs} +11 -0
  13. package/dist/files.cjs +1 -1
  14. package/dist/files.js +1 -1
  15. package/dist/index.d.cts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/platform.cjs +1 -13
  18. package/dist/platform.d.cts +1 -28
  19. package/dist/platform.d.ts +1 -28
  20. package/dist/platform.js +1 -10
  21. package/dist/schema-B5BfdswF.js +226 -0
  22. package/dist/schema-BnVZOXfu.cjs +286 -0
  23. package/dist/schema-D5eImHxu.d.cts +125 -0
  24. package/dist/schema-SSjokbtw.d.ts +125 -0
  25. package/dist/schema.cjs +10 -2
  26. package/dist/schema.d.cts +2 -2
  27. package/dist/schema.d.ts +2 -2
  28. package/dist/schema.js +2 -2
  29. package/dist/state-Dh3HLixb.js +874 -0
  30. package/dist/state-LssDgpff.cjs +973 -0
  31. package/dist/state.cjs +16 -12
  32. package/dist/state.d.cts +144 -140
  33. package/dist/state.d.ts +145 -140
  34. package/dist/state.js +3 -3
  35. package/dist/{ui-Bg11tvlc.d.ts → ui-1WepaMS4.d.cts} +1 -1
  36. package/dist/{ui-Dd7SXdbg.d.cts → ui-D7bRLYee.d.ts} +7 -7
  37. package/dist/ui.d.cts +1 -1
  38. package/dist/ui.d.ts +1 -1
  39. package/dist/window.d.cts +1 -1
  40. package/dist/window.d.ts +1 -1
  41. package/package.json +3 -1
  42. package/templates/foundation-models/app/index.tsx +77 -15
  43. package/templates/image-generation/app/index.tsx +4 -2
  44. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
  45. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
  46. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
  47. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
  48. package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
  49. package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
  50. package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
  51. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
  52. package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
  53. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
  54. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
  55. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
  56. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
  57. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
  58. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
  59. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
  60. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
  61. package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
  62. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
  63. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
  64. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
  65. package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
  66. package/templates/native/ios/Bleam/AppDelegate.swift +0 -206
  67. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +135 -129
  68. package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
  69. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
  70. package/templates/native/ios/GenerationService/main.swift +174 -48
  71. package/templates/native/ios/Podfile.lock +173 -173
  72. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
  73. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
  74. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +239 -341
  75. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
  76. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -191
  77. package/templates/text-generation/app/index.tsx +124 -0
  78. package/templates/text-generation/app.config.ts +5 -0
  79. package/templates/updates/README.md +43 -199
  80. package/templates/updates/src/index.ts +5 -98
  81. package/templates/updates/src/schema.ts +18 -173
  82. package/dist/schema-Bo5Jvqus.js +0 -90
  83. package/dist/schema-DOOjfXvs.d.ts +0 -58
  84. package/dist/schema-ENSMX_1t.d.cts +0 -58
  85. package/dist/schema-rQ13mrpD.cjs +0 -102
  86. package/dist/state-Bx0VlTlO.cjs +0 -852
  87. package/dist/state-CAwe-Vw1.js +0 -767
  88. package/templates/native/ios/PlatformHelper/main.swift +0 -726
  89. /package/dist/{config-Cms0rvqg.d.ts → config-COcRnn5a.d.cts} +0 -0
  90. /package/dist/{config-CufOVJV3.d.cts → config-Chi-flpJ.d.ts} +0 -0
@@ -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
+ }
@@ -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
+ }