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,161 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import MLX
|
|
3
|
+
import MLXNN
|
|
4
|
+
|
|
5
|
+
public enum Qwen3CheckpointError: LocalizedError {
|
|
6
|
+
case missingFile(String)
|
|
7
|
+
case unsupportedArchitecture(String)
|
|
8
|
+
case unsupportedQuantization(bits: Int?, groupSize: Int?)
|
|
9
|
+
case invalidConfiguration(String)
|
|
10
|
+
case invalidCheckpoint(String)
|
|
11
|
+
|
|
12
|
+
public var errorDescription: String? {
|
|
13
|
+
switch self {
|
|
14
|
+
case .missingFile(let file): "Qwen3 checkpoint is missing \(file)"
|
|
15
|
+
case .unsupportedArchitecture(let architecture): "Unsupported Qwen3 architecture \(architecture)"
|
|
16
|
+
case .unsupportedQuantization(let bits, let groupSize):
|
|
17
|
+
"Bonsai Qwen3 requires 2-bit/group-128 affine weights, got bits=\(bits.map(String.init) ?? "nil") group=\(groupSize.map(String.init) ?? "nil")"
|
|
18
|
+
case .invalidConfiguration(let message): "Invalid Qwen3 configuration: \(message)"
|
|
19
|
+
case .invalidCheckpoint(let message): "Invalid Qwen3 checkpoint: \(message)"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public enum Qwen3CheckpointLoader {
|
|
25
|
+
private struct SafetensorsIndex: Decodable {
|
|
26
|
+
let weightMap: [String: String]
|
|
27
|
+
|
|
28
|
+
enum CodingKeys: String, CodingKey {
|
|
29
|
+
case weightMap = "weight_map"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static func load(from modelURL: URL) throws -> Qwen3CausalLM {
|
|
34
|
+
let (configuration, quantization, tensors) = try loadSnapshot(from: modelURL, requireLanguageHead: true)
|
|
35
|
+
|
|
36
|
+
let model = Qwen3CausalLM(configuration: configuration)
|
|
37
|
+
MLXNN.quantize(
|
|
38
|
+
model: model,
|
|
39
|
+
groupSize: quantization.groupSize,
|
|
40
|
+
bits: quantization.bits,
|
|
41
|
+
mode: .affine
|
|
42
|
+
)
|
|
43
|
+
do {
|
|
44
|
+
try model.update(
|
|
45
|
+
parameters: ModuleParameters.unflattened(tensors),
|
|
46
|
+
verify: [.noUnusedKeys, .shapeMismatch]
|
|
47
|
+
)
|
|
48
|
+
} catch {
|
|
49
|
+
throw Qwen3CheckpointError.invalidCheckpoint(error.localizedDescription)
|
|
50
|
+
}
|
|
51
|
+
model.usesTiedOutputHead = configuration.tieWordEmbeddings && tensors["lm_head.weight"] == nil
|
|
52
|
+
MLX.eval(model)
|
|
53
|
+
MLX.Memory.clearCache()
|
|
54
|
+
return model
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public static func loadTransformer(from modelURL: URL) throws -> Qwen3TransformerModel {
|
|
58
|
+
let (configuration, quantization, tensors) = try loadSnapshot(from: modelURL, requireLanguageHead: false)
|
|
59
|
+
let model = Qwen3TransformerModel(configuration: configuration)
|
|
60
|
+
MLXNN.quantize(
|
|
61
|
+
model: model,
|
|
62
|
+
groupSize: quantization.groupSize,
|
|
63
|
+
bits: quantization.bits,
|
|
64
|
+
mode: .affine
|
|
65
|
+
)
|
|
66
|
+
let transformerTensors: [String: MLXArray] = Dictionary(uniqueKeysWithValues: tensors.compactMap { name, value in
|
|
67
|
+
guard name.hasPrefix("model.") else { return nil }
|
|
68
|
+
return (String(name.dropFirst("model.".count)), value)
|
|
69
|
+
})
|
|
70
|
+
do {
|
|
71
|
+
try model.update(
|
|
72
|
+
parameters: ModuleParameters.unflattened(transformerTensors),
|
|
73
|
+
verify: [.noUnusedKeys, .shapeMismatch]
|
|
74
|
+
)
|
|
75
|
+
} catch {
|
|
76
|
+
throw Qwen3CheckpointError.invalidCheckpoint(error.localizedDescription)
|
|
77
|
+
}
|
|
78
|
+
MLX.eval(model)
|
|
79
|
+
MLX.Memory.clearCache()
|
|
80
|
+
return model
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static func loadSnapshot(
|
|
84
|
+
from modelURL: URL,
|
|
85
|
+
requireLanguageHead: Bool
|
|
86
|
+
) throws -> (Qwen3ModelConfiguration, Qwen3ModelConfiguration.Quantization, [String: MLXArray]) {
|
|
87
|
+
let configuration = try Qwen3ModelConfiguration.load(from: modelURL)
|
|
88
|
+
try configuration.validate()
|
|
89
|
+
guard let quantization = configuration.quantization,
|
|
90
|
+
quantization.bits == 2, quantization.groupSize == 128 else {
|
|
91
|
+
throw Qwen3CheckpointError.unsupportedQuantization(
|
|
92
|
+
bits: configuration.quantization?.bits,
|
|
93
|
+
groupSize: configuration.quantization?.groupSize
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
let files = try checkpointFiles(at: modelURL)
|
|
97
|
+
var tensors: [String: MLXArray] = [:]
|
|
98
|
+
for file in files {
|
|
99
|
+
for (name, value) in try loadArrays(url: modelURL.appendingPathComponent(file)) {
|
|
100
|
+
tensors[name] = value
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
try validateTensors(
|
|
104
|
+
tensors,
|
|
105
|
+
configuration: configuration,
|
|
106
|
+
requireLanguageHead: requireLanguageHead
|
|
107
|
+
)
|
|
108
|
+
return (configuration, quantization, tensors)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static func checkpointFiles(at modelURL: URL) throws -> [String] {
|
|
112
|
+
let fileManager = FileManager.default
|
|
113
|
+
let indexURL = modelURL.appendingPathComponent("model.safetensors.index.json")
|
|
114
|
+
if fileManager.fileExists(atPath: indexURL.path) {
|
|
115
|
+
let index = try JSONDecoder().decode(SafetensorsIndex.self, from: Data(contentsOf: indexURL))
|
|
116
|
+
let files = Set(index.weightMap.values).sorted()
|
|
117
|
+
for file in files where !fileManager.fileExists(atPath: modelURL.appendingPathComponent(file).path) {
|
|
118
|
+
throw Qwen3CheckpointError.missingFile(file)
|
|
119
|
+
}
|
|
120
|
+
return files
|
|
121
|
+
}
|
|
122
|
+
let file = "model.safetensors"
|
|
123
|
+
guard fileManager.fileExists(atPath: modelURL.appendingPathComponent(file).path) else {
|
|
124
|
+
throw Qwen3CheckpointError.missingFile(file)
|
|
125
|
+
}
|
|
126
|
+
return [file]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static func validateTensors(
|
|
130
|
+
_ tensors: [String: MLXArray],
|
|
131
|
+
configuration: Qwen3ModelConfiguration,
|
|
132
|
+
requireLanguageHead: Bool
|
|
133
|
+
) throws {
|
|
134
|
+
var requiredModules = ["model.embed_tokens"]
|
|
135
|
+
for index in 0..<configuration.hiddenLayerCount {
|
|
136
|
+
let prefix = "model.layers.\(index)"
|
|
137
|
+
requiredModules += [
|
|
138
|
+
"\(prefix).self_attn.q_proj", "\(prefix).self_attn.k_proj",
|
|
139
|
+
"\(prefix).self_attn.v_proj", "\(prefix).self_attn.o_proj",
|
|
140
|
+
"\(prefix).mlp.gate_proj", "\(prefix).mlp.up_proj", "\(prefix).mlp.down_proj",
|
|
141
|
+
]
|
|
142
|
+
for norm in ["input_layernorm", "post_attention_layernorm", "self_attn.q_norm", "self_attn.k_norm"] {
|
|
143
|
+
let key = "\(prefix).\(norm).weight"
|
|
144
|
+
guard tensors[key] != nil else { throw Qwen3CheckpointError.invalidCheckpoint("missing tensor \(key)") }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if requireLanguageHead, !configuration.tieWordEmbeddings { requiredModules.append("lm_head") }
|
|
148
|
+
for module in requiredModules {
|
|
149
|
+
for suffix in ["weight", "scales", "biases"] {
|
|
150
|
+
let key = "\(module).\(suffix)"
|
|
151
|
+
guard tensors[key] != nil else { throw Qwen3CheckpointError.invalidCheckpoint("missing tensor \(key)") }
|
|
152
|
+
}
|
|
153
|
+
guard tensors["\(module).weight"]?.dtype == .uint32 else {
|
|
154
|
+
throw Qwen3CheckpointError.invalidCheckpoint("\(module).weight is not packed uint32")
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
guard tensors["model.norm.weight"] != nil else {
|
|
158
|
+
throw Qwen3CheckpointError.invalidCheckpoint("missing tensor model.norm.weight")
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public struct Qwen3ModelConfiguration: Decodable, Sendable {
|
|
4
|
+
public struct RopeScaling: Decodable, Sendable {
|
|
5
|
+
public let ropeType: String
|
|
6
|
+
public let factor: Float
|
|
7
|
+
public let originalMaximumPositions: Int
|
|
8
|
+
public let betaFast: Float
|
|
9
|
+
public let betaSlow: Float
|
|
10
|
+
|
|
11
|
+
enum CodingKeys: String, CodingKey {
|
|
12
|
+
case ropeType = "rope_type"
|
|
13
|
+
case type
|
|
14
|
+
case factor
|
|
15
|
+
case originalMaximumPositions = "original_max_position_embeddings"
|
|
16
|
+
case betaFast = "beta_fast"
|
|
17
|
+
case betaSlow = "beta_slow"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public init(from decoder: Decoder) throws {
|
|
21
|
+
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
22
|
+
ropeType = try values.decodeIfPresent(String.self, forKey: .ropeType)
|
|
23
|
+
?? values.decodeIfPresent(String.self, forKey: .type)
|
|
24
|
+
?? "yarn"
|
|
25
|
+
factor = try values.decode(Float.self, forKey: .factor)
|
|
26
|
+
originalMaximumPositions = try values.decode(Int.self, forKey: .originalMaximumPositions)
|
|
27
|
+
betaFast = try values.decodeIfPresent(Float.self, forKey: .betaFast) ?? 32
|
|
28
|
+
betaSlow = try values.decodeIfPresent(Float.self, forKey: .betaSlow) ?? 1
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public struct Quantization: Decodable, Sendable {
|
|
33
|
+
public let bits: Int
|
|
34
|
+
public let groupSize: Int
|
|
35
|
+
|
|
36
|
+
enum CodingKeys: String, CodingKey {
|
|
37
|
+
case bits
|
|
38
|
+
case groupSize = "group_size"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public let architectures: [String]
|
|
43
|
+
public let modelType: String
|
|
44
|
+
public let vocabularySize: Int
|
|
45
|
+
public let hiddenSize: Int
|
|
46
|
+
public let intermediateSize: Int
|
|
47
|
+
public let hiddenLayerCount: Int
|
|
48
|
+
public let attentionHeadCount: Int
|
|
49
|
+
public let keyValueHeadCount: Int
|
|
50
|
+
public let maximumPositionCount: Int
|
|
51
|
+
public let rmsNormEpsilon: Float
|
|
52
|
+
public let ropeTheta: Float
|
|
53
|
+
public let tieWordEmbeddings: Bool
|
|
54
|
+
public let attentionBias: Bool
|
|
55
|
+
public let headDimension: Int
|
|
56
|
+
public let ropeScaling: RopeScaling?
|
|
57
|
+
public let quantization: Quantization?
|
|
58
|
+
|
|
59
|
+
enum CodingKeys: String, CodingKey {
|
|
60
|
+
case architectures
|
|
61
|
+
case modelType = "model_type"
|
|
62
|
+
case vocabularySize = "vocab_size"
|
|
63
|
+
case hiddenSize = "hidden_size"
|
|
64
|
+
case intermediateSize = "intermediate_size"
|
|
65
|
+
case hiddenLayerCount = "num_hidden_layers"
|
|
66
|
+
case attentionHeadCount = "num_attention_heads"
|
|
67
|
+
case keyValueHeadCount = "num_key_value_heads"
|
|
68
|
+
case maximumPositionCount = "max_position_embeddings"
|
|
69
|
+
case rmsNormEpsilon = "rms_norm_eps"
|
|
70
|
+
case ropeTheta = "rope_theta"
|
|
71
|
+
case tieWordEmbeddings = "tie_word_embeddings"
|
|
72
|
+
case attentionBias = "attention_bias"
|
|
73
|
+
case headDimension = "head_dim"
|
|
74
|
+
case ropeScaling = "rope_scaling"
|
|
75
|
+
case quantization
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public init(from decoder: Decoder) throws {
|
|
79
|
+
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
80
|
+
architectures = try values.decodeIfPresent([String].self, forKey: .architectures) ?? []
|
|
81
|
+
modelType = try values.decodeIfPresent(String.self, forKey: .modelType) ?? "qwen3"
|
|
82
|
+
vocabularySize = try values.decode(Int.self, forKey: .vocabularySize)
|
|
83
|
+
hiddenSize = try values.decode(Int.self, forKey: .hiddenSize)
|
|
84
|
+
intermediateSize = try values.decode(Int.self, forKey: .intermediateSize)
|
|
85
|
+
hiddenLayerCount = try values.decode(Int.self, forKey: .hiddenLayerCount)
|
|
86
|
+
attentionHeadCount = try values.decode(Int.self, forKey: .attentionHeadCount)
|
|
87
|
+
keyValueHeadCount = try values.decode(Int.self, forKey: .keyValueHeadCount)
|
|
88
|
+
maximumPositionCount = try values.decode(Int.self, forKey: .maximumPositionCount)
|
|
89
|
+
rmsNormEpsilon = try values.decodeIfPresent(Float.self, forKey: .rmsNormEpsilon) ?? 1e-6
|
|
90
|
+
ropeTheta = try values.decodeIfPresent(Float.self, forKey: .ropeTheta) ?? 1_000_000
|
|
91
|
+
tieWordEmbeddings = try values.decodeIfPresent(Bool.self, forKey: .tieWordEmbeddings) ?? true
|
|
92
|
+
attentionBias = try values.decodeIfPresent(Bool.self, forKey: .attentionBias) ?? false
|
|
93
|
+
headDimension = try values.decodeIfPresent(Int.self, forKey: .headDimension)
|
|
94
|
+
?? hiddenSize / attentionHeadCount
|
|
95
|
+
ropeScaling = try values.decodeIfPresent(RopeScaling.self, forKey: .ropeScaling)
|
|
96
|
+
quantization = try values.decodeIfPresent(Quantization.self, forKey: .quantization)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public static func load(from modelURL: URL) throws -> Qwen3ModelConfiguration {
|
|
100
|
+
try JSONDecoder().decode(
|
|
101
|
+
Qwen3ModelConfiguration.self,
|
|
102
|
+
from: Data(contentsOf: modelURL.appendingPathComponent("config.json"))
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public func validate() throws {
|
|
107
|
+
guard modelType == "qwen3", architectures.isEmpty || architectures.contains("Qwen3ForCausalLM") else {
|
|
108
|
+
throw Qwen3CheckpointError.unsupportedArchitecture(architectures.first ?? modelType)
|
|
109
|
+
}
|
|
110
|
+
guard hiddenSize > 0, intermediateSize > 0, hiddenLayerCount > 0,
|
|
111
|
+
attentionHeadCount > 0, keyValueHeadCount > 0,
|
|
112
|
+
attentionHeadCount.isMultiple(of: keyValueHeadCount), headDimension > 0 else {
|
|
113
|
+
throw Qwen3CheckpointError.invalidConfiguration("invalid model dimensions")
|
|
114
|
+
}
|
|
115
|
+
if let ropeScaling, ropeScaling.ropeType != "yarn" {
|
|
116
|
+
throw Qwen3CheckpointError.invalidConfiguration("unsupported RoPE scaling type \(ropeScaling.ropeType)")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import MLX
|
|
2
|
+
|
|
3
|
+
public final class Qwen3InferenceSession: @unchecked Sendable {
|
|
4
|
+
private let model: Qwen3CausalLM
|
|
5
|
+
private let endTokenIDs: Set<Int>
|
|
6
|
+
|
|
7
|
+
public init(model: Qwen3CausalLM, endTokenIDs: Set<Int> = [151643, 151645]) {
|
|
8
|
+
self.model = model
|
|
9
|
+
self.endTokenIDs = endTokenIDs
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public func generate(
|
|
13
|
+
inputIDs: [Int],
|
|
14
|
+
parameters: GenerationParameters = .balanced,
|
|
15
|
+
isCancelled: (() -> Bool)? = nil,
|
|
16
|
+
onToken: ((Int) -> Bool)? = nil
|
|
17
|
+
) throws -> TokenGenerationResult {
|
|
18
|
+
try TokenSampler.validate(parameters)
|
|
19
|
+
guard !inputIDs.isEmpty else { throw TextGenerationError.emptyPrompt }
|
|
20
|
+
for token in inputIDs where token < 0 || token >= model.vocabularySize {
|
|
21
|
+
throw TextGenerationError.invalidTokenID(token)
|
|
22
|
+
}
|
|
23
|
+
let requestedLength = inputIDs.count + parameters.maximumNewTokens
|
|
24
|
+
guard requestedLength <= model.maximumContextLength else {
|
|
25
|
+
throw TextGenerationError.contextExceeded(
|
|
26
|
+
requested: requestedLength,
|
|
27
|
+
maximum: model.maximumContextLength
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
guard parameters.maximumNewTokens > 0 else {
|
|
31
|
+
return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: [], finishReason: .length)
|
|
32
|
+
}
|
|
33
|
+
if isCancelled?() == true {
|
|
34
|
+
return TokenGenerationResult(promptTokenCount: inputIDs.count, tokenIDs: [], finishReason: .cancelled)
|
|
35
|
+
}
|
|
36
|
+
if let seed = parameters.seed { MLXRandom.seed(seed) }
|
|
37
|
+
|
|
38
|
+
let cache = model.makeCache()
|
|
39
|
+
defer {
|
|
40
|
+
cache.forEach { $0.clear() }
|
|
41
|
+
MLX.Memory.clearCache()
|
|
42
|
+
}
|
|
43
|
+
var logits = model.logits(
|
|
44
|
+
for: MLXArray(inputIDs.map(Int32.init)).reshaped([1, inputIDs.count]),
|
|
45
|
+
cache: cache
|
|
46
|
+
)
|
|
47
|
+
var generated: [Int] = []
|
|
48
|
+
var history = inputIDs
|
|
49
|
+
var finishReason: GenerationFinishReason = .length
|
|
50
|
+
|
|
51
|
+
for _ in 0..<parameters.maximumNewTokens {
|
|
52
|
+
if isCancelled?() == true {
|
|
53
|
+
finishReason = .cancelled
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
let sampled = try TokenSampler.sample(
|
|
57
|
+
logits: logits,
|
|
58
|
+
parameters: parameters,
|
|
59
|
+
tokenHistory: history
|
|
60
|
+
)
|
|
61
|
+
MLX.eval(sampled)
|
|
62
|
+
let token = Int(sampled.item(Int32.self))
|
|
63
|
+
if endTokenIDs.contains(token) {
|
|
64
|
+
finishReason = .endOfSequence
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
generated.append(token)
|
|
68
|
+
history.append(token)
|
|
69
|
+
if onToken?(token) == false {
|
|
70
|
+
finishReason = .callbackStopped
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
logits = model.logits(
|
|
74
|
+
for: MLXArray([Int32(token)]).reshaped([1, 1]),
|
|
75
|
+
cache: cache
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return TokenGenerationResult(
|
|
79
|
+
promptTokenCount: inputIDs.count,
|
|
80
|
+
tokenIDs: generated,
|
|
81
|
+
finishReason: finishReason
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import MLX
|
|
3
|
+
import MLXNN
|
|
4
|
+
|
|
5
|
+
public final class Qwen3RMSNorm: Module, UnaryLayer {
|
|
6
|
+
@ParameterInfo public var weight: MLXArray
|
|
7
|
+
public let epsilon: Float
|
|
8
|
+
|
|
9
|
+
public init(dimensions: Int, epsilon: Float) {
|
|
10
|
+
_weight = ParameterInfo(wrappedValue: MLXArray.ones([dimensions]))
|
|
11
|
+
self.epsilon = epsilon
|
|
12
|
+
super.init()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
16
|
+
MLXFast.rmsNorm(x, weight: weight, eps: epsilon)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public final class Qwen3RotaryEmbedding: Module {
|
|
21
|
+
private let dimensions: Int
|
|
22
|
+
private let base: Float
|
|
23
|
+
private let frequencies: MLXArray?
|
|
24
|
+
private let magnitudeScale: Float
|
|
25
|
+
|
|
26
|
+
public init(configuration: Qwen3ModelConfiguration) {
|
|
27
|
+
dimensions = configuration.headDimension
|
|
28
|
+
base = configuration.ropeTheta
|
|
29
|
+
if let scaling = configuration.ropeScaling {
|
|
30
|
+
precondition(configuration.headDimension.isMultiple(of: 2))
|
|
31
|
+
func correctionDimension(_ rotations: Float) -> Float {
|
|
32
|
+
Float(configuration.headDimension)
|
|
33
|
+
* log(Float(scaling.originalMaximumPositions) / (rotations * 2 * .pi))
|
|
34
|
+
/ (2 * log(configuration.ropeTheta))
|
|
35
|
+
}
|
|
36
|
+
let low = max(Int(floor(correctionDimension(scaling.betaFast))), 0)
|
|
37
|
+
let high = min(Int(ceil(correctionDimension(scaling.betaSlow))), configuration.headDimension - 1)
|
|
38
|
+
let safeHigh = low == high ? Float(high) + 0.001 : Float(high)
|
|
39
|
+
let ramp = clip(
|
|
40
|
+
(MLXArray(0..<(configuration.headDimension / 2)).asType(.float32) - Float(low))
|
|
41
|
+
/ (safeHigh - Float(low)),
|
|
42
|
+
min: 0,
|
|
43
|
+
max: 1
|
|
44
|
+
)
|
|
45
|
+
let exponent = MLXArray(stride(from: 0, to: configuration.headDimension, by: 2))
|
|
46
|
+
.asType(.float32) / Float(configuration.headDimension)
|
|
47
|
+
let unscaled = pow(configuration.ropeTheta, exponent)
|
|
48
|
+
let interpolated = scaling.factor * unscaled
|
|
49
|
+
let mask = 1 - ramp
|
|
50
|
+
frequencies = (interpolated * unscaled) / (interpolated * mask + unscaled * (1 - mask))
|
|
51
|
+
magnitudeScale = scaling.factor <= 1 ? 1 : 0.1 * log(scaling.factor) + 1
|
|
52
|
+
} else {
|
|
53
|
+
frequencies = nil
|
|
54
|
+
magnitudeScale = 1
|
|
55
|
+
}
|
|
56
|
+
super.init()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public func callAsFunction(_ x: MLXArray, offset: Int) -> MLXArray {
|
|
60
|
+
MLXFast.RoPE(
|
|
61
|
+
magnitudeScale == 1 ? x : x * magnitudeScale,
|
|
62
|
+
dimensions: dimensions,
|
|
63
|
+
traditional: false,
|
|
64
|
+
base: frequencies == nil ? base : nil,
|
|
65
|
+
scale: 1,
|
|
66
|
+
offset: offset,
|
|
67
|
+
freqs: frequencies
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public final class Qwen3SelfAttention: Module {
|
|
73
|
+
private let headCount: Int
|
|
74
|
+
private let keyValueHeadCount: Int
|
|
75
|
+
private let headDimension: Int
|
|
76
|
+
private let scale: Float
|
|
77
|
+
|
|
78
|
+
@ModuleInfo public var q_proj: Linear
|
|
79
|
+
@ModuleInfo public var k_proj: Linear
|
|
80
|
+
@ModuleInfo public var v_proj: Linear
|
|
81
|
+
@ModuleInfo public var o_proj: Linear
|
|
82
|
+
@ModuleInfo public var q_norm: Qwen3RMSNorm
|
|
83
|
+
@ModuleInfo public var k_norm: Qwen3RMSNorm
|
|
84
|
+
@ModuleInfo public var rope: Qwen3RotaryEmbedding
|
|
85
|
+
|
|
86
|
+
public init(configuration: Qwen3ModelConfiguration) {
|
|
87
|
+
headCount = configuration.attentionHeadCount
|
|
88
|
+
keyValueHeadCount = configuration.keyValueHeadCount
|
|
89
|
+
headDimension = configuration.headDimension
|
|
90
|
+
scale = 1 / sqrt(Float(configuration.headDimension))
|
|
91
|
+
_q_proj = ModuleInfo(wrappedValue: Linear(
|
|
92
|
+
configuration.hiddenSize,
|
|
93
|
+
configuration.attentionHeadCount * configuration.headDimension,
|
|
94
|
+
bias: configuration.attentionBias
|
|
95
|
+
))
|
|
96
|
+
_k_proj = ModuleInfo(wrappedValue: Linear(
|
|
97
|
+
configuration.hiddenSize,
|
|
98
|
+
configuration.keyValueHeadCount * configuration.headDimension,
|
|
99
|
+
bias: configuration.attentionBias
|
|
100
|
+
))
|
|
101
|
+
_v_proj = ModuleInfo(wrappedValue: Linear(
|
|
102
|
+
configuration.hiddenSize,
|
|
103
|
+
configuration.keyValueHeadCount * configuration.headDimension,
|
|
104
|
+
bias: configuration.attentionBias
|
|
105
|
+
))
|
|
106
|
+
_o_proj = ModuleInfo(wrappedValue: Linear(
|
|
107
|
+
configuration.attentionHeadCount * configuration.headDimension,
|
|
108
|
+
configuration.hiddenSize,
|
|
109
|
+
bias: configuration.attentionBias
|
|
110
|
+
))
|
|
111
|
+
_q_norm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
|
|
112
|
+
dimensions: configuration.headDimension,
|
|
113
|
+
epsilon: configuration.rmsNormEpsilon
|
|
114
|
+
))
|
|
115
|
+
_k_norm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
|
|
116
|
+
dimensions: configuration.headDimension,
|
|
117
|
+
epsilon: configuration.rmsNormEpsilon
|
|
118
|
+
))
|
|
119
|
+
_rope = ModuleInfo(wrappedValue: Qwen3RotaryEmbedding(configuration: configuration))
|
|
120
|
+
super.init()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public func callAsFunction(_ hiddenStates: MLXArray, mask: MLXArray?, cache: TextKVCache?) -> MLXArray {
|
|
124
|
+
let batchSize = hiddenStates.dim(0)
|
|
125
|
+
let sequenceLength = hiddenStates.dim(1)
|
|
126
|
+
var queries = q_proj(hiddenStates).reshaped([batchSize, sequenceLength, headCount, headDimension])
|
|
127
|
+
var keys = k_proj(hiddenStates).reshaped([batchSize, sequenceLength, keyValueHeadCount, headDimension])
|
|
128
|
+
var values = v_proj(hiddenStates).reshaped([batchSize, sequenceLength, keyValueHeadCount, headDimension])
|
|
129
|
+
queries = q_norm(queries).transposed(0, 2, 1, 3)
|
|
130
|
+
keys = k_norm(keys).transposed(0, 2, 1, 3)
|
|
131
|
+
values = values.transposed(0, 2, 1, 3)
|
|
132
|
+
let offset = cache?.length ?? 0
|
|
133
|
+
queries = rope(queries, offset: offset)
|
|
134
|
+
keys = rope(keys, offset: offset)
|
|
135
|
+
if let cache {
|
|
136
|
+
(keys, values) = cache.update(keys: keys, values: values)
|
|
137
|
+
}
|
|
138
|
+
let attended = MLXFast.scaledDotProductAttention(
|
|
139
|
+
queries: queries,
|
|
140
|
+
keys: keys,
|
|
141
|
+
values: values,
|
|
142
|
+
scale: scale,
|
|
143
|
+
mask: mask
|
|
144
|
+
)
|
|
145
|
+
return o_proj(attended.transposed(0, 2, 1, 3).reshaped([
|
|
146
|
+
batchSize, sequenceLength, headCount * headDimension,
|
|
147
|
+
]))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public final class Qwen3FeedForward: Module, @unchecked Sendable {
|
|
152
|
+
@ModuleInfo public var gate_proj: Linear
|
|
153
|
+
@ModuleInfo public var up_proj: Linear
|
|
154
|
+
@ModuleInfo public var down_proj: Linear
|
|
155
|
+
|
|
156
|
+
nonisolated(unsafe) private static let activate: (MLXArray, MLXArray) -> MLXArray = compile {
|
|
157
|
+
silu($0) * $1
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public init(configuration: Qwen3ModelConfiguration) {
|
|
161
|
+
_gate_proj = ModuleInfo(wrappedValue: Linear(configuration.hiddenSize, configuration.intermediateSize, bias: false))
|
|
162
|
+
_up_proj = ModuleInfo(wrappedValue: Linear(configuration.hiddenSize, configuration.intermediateSize, bias: false))
|
|
163
|
+
_down_proj = ModuleInfo(wrappedValue: Linear(configuration.intermediateSize, configuration.hiddenSize, bias: false))
|
|
164
|
+
super.init()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public func callAsFunction(_ x: MLXArray) -> MLXArray {
|
|
168
|
+
down_proj(Self.activate(gate_proj(x), up_proj(x)))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public final class Qwen3DecoderBlock: Module {
|
|
173
|
+
@ModuleInfo public var self_attn: Qwen3SelfAttention
|
|
174
|
+
@ModuleInfo public var mlp: Qwen3FeedForward
|
|
175
|
+
@ModuleInfo public var input_layernorm: Qwen3RMSNorm
|
|
176
|
+
@ModuleInfo public var post_attention_layernorm: Qwen3RMSNorm
|
|
177
|
+
|
|
178
|
+
public init(configuration: Qwen3ModelConfiguration) {
|
|
179
|
+
_self_attn = ModuleInfo(wrappedValue: Qwen3SelfAttention(configuration: configuration))
|
|
180
|
+
_mlp = ModuleInfo(wrappedValue: Qwen3FeedForward(configuration: configuration))
|
|
181
|
+
_input_layernorm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
|
|
182
|
+
dimensions: configuration.hiddenSize,
|
|
183
|
+
epsilon: configuration.rmsNormEpsilon
|
|
184
|
+
))
|
|
185
|
+
_post_attention_layernorm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
|
|
186
|
+
dimensions: configuration.hiddenSize,
|
|
187
|
+
epsilon: configuration.rmsNormEpsilon
|
|
188
|
+
))
|
|
189
|
+
super.init()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public func callAsFunction(_ hiddenStates: MLXArray, mask: MLXArray?, cache: TextKVCache?) -> MLXArray {
|
|
193
|
+
var hidden = hiddenStates + self_attn(input_layernorm(hiddenStates), mask: mask, cache: cache)
|
|
194
|
+
hidden = hidden + mlp(post_attention_layernorm(hidden))
|
|
195
|
+
return hidden
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import MLX
|
|
2
|
+
import MLXNN
|
|
3
|
+
|
|
4
|
+
public final class Qwen3TransformerModel: Module, HiddenStateModel {
|
|
5
|
+
public let configuration: Qwen3ModelConfiguration
|
|
6
|
+
|
|
7
|
+
@ModuleInfo public var embed_tokens: Embedding
|
|
8
|
+
public var layers: [Qwen3DecoderBlock]
|
|
9
|
+
@ModuleInfo public var norm: Qwen3RMSNorm
|
|
10
|
+
|
|
11
|
+
public var vocabularySize: Int { configuration.vocabularySize }
|
|
12
|
+
public var maximumContextLength: Int { configuration.maximumPositionCount }
|
|
13
|
+
|
|
14
|
+
public init(configuration: Qwen3ModelConfiguration) {
|
|
15
|
+
self.configuration = configuration
|
|
16
|
+
_embed_tokens = ModuleInfo(wrappedValue: Embedding(
|
|
17
|
+
embeddingCount: configuration.vocabularySize,
|
|
18
|
+
dimensions: configuration.hiddenSize
|
|
19
|
+
))
|
|
20
|
+
layers = (0..<configuration.hiddenLayerCount).map { _ in
|
|
21
|
+
Qwen3DecoderBlock(configuration: configuration)
|
|
22
|
+
}
|
|
23
|
+
_norm = ModuleInfo(wrappedValue: Qwen3RMSNorm(
|
|
24
|
+
dimensions: configuration.hiddenSize,
|
|
25
|
+
epsilon: configuration.rmsNormEpsilon
|
|
26
|
+
))
|
|
27
|
+
super.init()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public func callAsFunction(_ inputIDs: MLXArray, cache: [TextKVCache]? = nil) -> MLXArray {
|
|
31
|
+
var hidden = embed_tokens(inputIDs)
|
|
32
|
+
let mask = causalMask(sequenceLength: inputIDs.dim(1), offset: cache?.first?.length ?? 0)
|
|
33
|
+
for (index, layer) in layers.enumerated() {
|
|
34
|
+
hidden = layer(hidden, mask: mask, cache: cache?[index])
|
|
35
|
+
}
|
|
36
|
+
return norm(hidden)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public func hiddenStates(
|
|
40
|
+
for inputIDs: MLXArray,
|
|
41
|
+
at layerIndices: Set<Int>,
|
|
42
|
+
attentionMask: MLXArray? = nil
|
|
43
|
+
) -> [Int: MLXArray] {
|
|
44
|
+
var hidden = embed_tokens(inputIDs)
|
|
45
|
+
let mask = causalMask(sequenceLength: inputIDs.dim(1), offset: 0, attentionMask: attentionMask)
|
|
46
|
+
var result: [Int: MLXArray] = [:]
|
|
47
|
+
if layerIndices.contains(0) {
|
|
48
|
+
MLX.eval(hidden)
|
|
49
|
+
result[0] = hidden
|
|
50
|
+
}
|
|
51
|
+
for (index, layer) in layers.enumerated() {
|
|
52
|
+
hidden = layer(hidden, mask: mask, cache: nil)
|
|
53
|
+
let layerIndex = index + 1
|
|
54
|
+
if layerIndices.contains(layerIndex) {
|
|
55
|
+
let state = layerIndex == layers.count ? norm(hidden) : hidden
|
|
56
|
+
MLX.eval(state)
|
|
57
|
+
result[layerIndex] = state
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private func causalMask(
|
|
64
|
+
sequenceLength: Int,
|
|
65
|
+
offset: Int,
|
|
66
|
+
attentionMask: MLXArray? = nil
|
|
67
|
+
) -> MLXArray? {
|
|
68
|
+
if sequenceLength == 1, attentionMask == nil { return nil }
|
|
69
|
+
let totalLength = sequenceLength + offset
|
|
70
|
+
let rows = MLXArray.arange(sequenceLength, dtype: .float32).expandedDimensions(axis: 1)
|
|
71
|
+
let columns = MLXArray.arange(totalLength, dtype: .float32).expandedDimensions(axis: 0)
|
|
72
|
+
var mask = MLX.where(
|
|
73
|
+
columns .<= rows + Float(offset),
|
|
74
|
+
MLXArray(Float(0)),
|
|
75
|
+
MLXArray(-Float.infinity)
|
|
76
|
+
).reshaped([1, 1, sequenceLength, totalLength])
|
|
77
|
+
if let attentionMask {
|
|
78
|
+
let padding = MLX.where(
|
|
79
|
+
attentionMask .== Int32(1),
|
|
80
|
+
MLXArray(Float(0)),
|
|
81
|
+
MLXArray(Float(-1e9))
|
|
82
|
+
).reshaped([attentionMask.dim(0), 1, 1, attentionMask.dim(1)])
|
|
83
|
+
mask = mask + padding
|
|
84
|
+
}
|
|
85
|
+
return mask
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public final class Qwen3CausalLM: Module, CausalLanguageModel {
|
|
90
|
+
public let configuration: Qwen3ModelConfiguration
|
|
91
|
+
@ModuleInfo public var model: Qwen3TransformerModel
|
|
92
|
+
@ModuleInfo public var lm_head: Linear
|
|
93
|
+
public var usesTiedOutputHead: Bool
|
|
94
|
+
|
|
95
|
+
public var vocabularySize: Int { configuration.vocabularySize }
|
|
96
|
+
public var maximumContextLength: Int { configuration.maximumPositionCount }
|
|
97
|
+
|
|
98
|
+
public init(configuration: Qwen3ModelConfiguration) {
|
|
99
|
+
self.configuration = configuration
|
|
100
|
+
usesTiedOutputHead = configuration.tieWordEmbeddings
|
|
101
|
+
_model = ModuleInfo(wrappedValue: Qwen3TransformerModel(configuration: configuration))
|
|
102
|
+
_lm_head = ModuleInfo(wrappedValue: Linear(configuration.hiddenSize, configuration.vocabularySize, bias: false))
|
|
103
|
+
super.init()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public func logits(for inputIDs: MLXArray, cache: [TextKVCache]) -> MLXArray {
|
|
107
|
+
project(model(inputIDs, cache: cache))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public func makeCache() -> [TextKVCache] {
|
|
111
|
+
(0..<configuration.hiddenLayerCount).map { _ in TextKVCache() }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private func project(_ hiddenStates: MLXArray) -> MLXArray {
|
|
115
|
+
guard usesTiedOutputHead else { return lm_head(hiddenStates) }
|
|
116
|
+
if let embedding = model.embed_tokens as? QuantizedEmbedding {
|
|
117
|
+
return MLX.quantizedMM(
|
|
118
|
+
hiddenStates,
|
|
119
|
+
embedding.weight,
|
|
120
|
+
scales: embedding.scales,
|
|
121
|
+
biases: embedding.biases,
|
|
122
|
+
transpose: true,
|
|
123
|
+
groupSize: embedding.groupSize,
|
|
124
|
+
bits: embedding.bits,
|
|
125
|
+
mode: .affine
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return matmul(hiddenStates, model.embed_tokens.weight.T)
|
|
129
|
+
}
|
|
130
|
+
}
|