bleam 0.0.9 → 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.
- package/dist/ai.cjs +608 -59
- package/dist/ai.d.cts +217 -89
- package/dist/ai.d.ts +217 -89
- package/dist/ai.js +601 -59
- package/dist/cli.cjs +142 -94
- package/dist/cli.d.cts +11 -27
- package/dist/cli.d.ts +11 -27
- package/dist/cli.js +143 -95
- package/dist/files.d.cts +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/platform.cjs +1 -13
- package/dist/platform.d.cts +1 -26
- package/dist/platform.d.ts +1 -26
- package/dist/platform.js +1 -10
- package/dist/schema.d.cts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/state.d.cts +1 -1
- package/dist/state.d.ts +1 -1
- package/dist/{ui-Bg11tvlc.d.ts → ui-CHc4xEs_.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +2 -1
- package/templates/image-generation/app/index.tsx +3 -1
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +0 -97
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -121
- package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
- package/templates/native/ios/GenerationService/main.swift +174 -48
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -175
- package/templates/text-generation/app/index.tsx +93 -0
- package/templates/text-generation/app.config.ts +5 -0
- package/templates/updates/README.md +116 -0
- package/templates/updates/src/index.ts +292 -0
- package/templates/updates/src/schema.ts +230 -0
- package/templates/updates/tsconfig.json +12 -0
- package/templates/updates/wrangler.jsonc +19 -0
- package/templates/native/ios/PlatformHelper/Info.plist +0 -29
- package/templates/native/ios/PlatformHelper/main.swift +0 -335
- /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
- /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
- /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
- /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +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
|
+
}
|