bleam 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +3 -183
- package/dist/cli.d.cts +2 -46
- package/dist/cli.d.ts +2 -46
- package/dist/cli.js +3 -183
- 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 -28
- package/dist/platform.d.ts +1 -28
- 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-1WepaMS4.d.cts} +1 -1
- package/dist/{ui-Dd7SXdbg.d.cts → ui-CHc4xEs_.d.ts} +7 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.cts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +1 -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 -206
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -122
- 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 -191
- package/templates/text-generation/app/index.tsx +93 -0
- package/templates/text-generation/app.config.ts +5 -0
- package/templates/updates/README.md +43 -199
- package/templates/updates/src/index.ts +5 -98
- package/templates/updates/src/schema.ts +18 -173
- package/templates/native/ios/PlatformHelper/main.swift +0 -726
- /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,201 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public enum BonsaiTextSnapshotValidator {
|
|
4
|
+
private struct Quantization: Decodable, Equatable {
|
|
5
|
+
let groupSize: Int
|
|
6
|
+
let bits: Int
|
|
7
|
+
let mode: String
|
|
8
|
+
|
|
9
|
+
private enum CodingKeys: String, CodingKey {
|
|
10
|
+
case groupSize = "group_size"
|
|
11
|
+
case bits
|
|
12
|
+
case mode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
init(groupSize: Int, bits: Int, mode: String = "affine") {
|
|
16
|
+
self.groupSize = groupSize
|
|
17
|
+
self.bits = bits
|
|
18
|
+
self.mode = mode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
init(from decoder: Decoder) throws {
|
|
22
|
+
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
23
|
+
groupSize = try values.decode(Int.self, forKey: .groupSize)
|
|
24
|
+
bits = try values.decode(Int.self, forKey: .bits)
|
|
25
|
+
mode = try values.decodeIfPresent(String.self, forKey: .mode) ?? "affine"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private struct Qwen3Identity: Decodable {
|
|
30
|
+
struct RopeScaling: Decodable {
|
|
31
|
+
let ropeType: String
|
|
32
|
+
let factor: Float
|
|
33
|
+
let originalMaximumPositions: Int
|
|
34
|
+
|
|
35
|
+
private enum CodingKeys: String, CodingKey {
|
|
36
|
+
case ropeType = "rope_type"
|
|
37
|
+
case factor
|
|
38
|
+
case originalMaximumPositions = "original_max_position_embeddings"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let hiddenActivation: String
|
|
43
|
+
let layerTypes: [String]
|
|
44
|
+
let ropeScaling: RopeScaling
|
|
45
|
+
let quantization: Quantization
|
|
46
|
+
|
|
47
|
+
private enum CodingKeys: String, CodingKey {
|
|
48
|
+
case hiddenActivation = "hidden_act"
|
|
49
|
+
case layerTypes = "layer_types"
|
|
50
|
+
case ropeScaling = "rope_scaling"
|
|
51
|
+
case quantization
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private struct Qwen35TextIdentity: Decodable {
|
|
56
|
+
let modelType: String
|
|
57
|
+
let vocabularySize: Int
|
|
58
|
+
let hiddenSize: Int
|
|
59
|
+
let intermediateSize: Int
|
|
60
|
+
let hiddenLayerCount: Int
|
|
61
|
+
let attentionHeadCount: Int
|
|
62
|
+
let keyValueHeadCount: Int
|
|
63
|
+
let headDimension: Int
|
|
64
|
+
let maximumPositionCount: Int
|
|
65
|
+
let quantization: Quantization
|
|
66
|
+
|
|
67
|
+
private enum CodingKeys: String, CodingKey {
|
|
68
|
+
case modelType = "model_type"
|
|
69
|
+
case vocabularySize = "vocab_size"
|
|
70
|
+
case hiddenSize = "hidden_size"
|
|
71
|
+
case intermediateSize = "intermediate_size"
|
|
72
|
+
case hiddenLayerCount = "num_hidden_layers"
|
|
73
|
+
case attentionHeadCount = "num_attention_heads"
|
|
74
|
+
case keyValueHeadCount = "num_key_value_heads"
|
|
75
|
+
case headDimension = "head_dim"
|
|
76
|
+
case maximumPositionCount = "max_position_embeddings"
|
|
77
|
+
case quantization
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private struct Qwen35Identity: Decodable {
|
|
82
|
+
let architectures: [String]
|
|
83
|
+
let modelType: String
|
|
84
|
+
let textConfiguration: Qwen35TextIdentity
|
|
85
|
+
let quantization: Quantization
|
|
86
|
+
|
|
87
|
+
private enum CodingKeys: String, CodingKey {
|
|
88
|
+
case architectures
|
|
89
|
+
case modelType = "model_type"
|
|
90
|
+
case textConfiguration = "text_config"
|
|
91
|
+
case quantization
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public static func validate(modelURL: URL, profile: BonsaiTextModelProfile) throws {
|
|
96
|
+
var isDirectory: ObjCBool = false
|
|
97
|
+
guard FileManager.default.fileExists(atPath: modelURL.path, isDirectory: &isDirectory),
|
|
98
|
+
isDirectory.boolValue else {
|
|
99
|
+
throw BonsaiTextError.invalidModelPath(modelURL.path)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
do {
|
|
103
|
+
switch profile.backend {
|
|
104
|
+
case .qwen3:
|
|
105
|
+
try validateQwen3(modelURL: modelURL, profile: profile)
|
|
106
|
+
case .qwen35:
|
|
107
|
+
try validateQwen35(modelURL: modelURL, profile: profile)
|
|
108
|
+
}
|
|
109
|
+
} catch let error as BonsaiTextError {
|
|
110
|
+
throw error
|
|
111
|
+
} catch {
|
|
112
|
+
throw BonsaiTextError.snapshotMismatch(model: profile.id, detail: error.localizedDescription)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private static func validateQwen3(modelURL: URL, profile: BonsaiTextModelProfile) throws {
|
|
117
|
+
let configuration = try Qwen3ModelConfiguration.load(from: modelURL)
|
|
118
|
+
try configuration.validate()
|
|
119
|
+
let data = try Data(contentsOf: modelURL.appendingPathComponent("config.json"))
|
|
120
|
+
let identity = try JSONDecoder().decode(Qwen3Identity.self, from: data)
|
|
121
|
+
guard configuration.architectures == ["Qwen3ForCausalLM"], configuration.modelType == "qwen3" else {
|
|
122
|
+
throw mismatch(profile, "expected Qwen3ForCausalLM/qwen3 architecture")
|
|
123
|
+
}
|
|
124
|
+
guard configuration.vocabularySize == profile.vocabularySize,
|
|
125
|
+
configuration.hiddenSize == profile.hiddenSize,
|
|
126
|
+
configuration.intermediateSize == profile.intermediateSize,
|
|
127
|
+
configuration.hiddenLayerCount == profile.hiddenLayerCount,
|
|
128
|
+
configuration.attentionHeadCount == profile.attentionHeadCount,
|
|
129
|
+
configuration.keyValueHeadCount == profile.keyValueHeadCount,
|
|
130
|
+
configuration.headDimension == profile.headDimension,
|
|
131
|
+
configuration.maximumPositionCount == profile.contextLength else {
|
|
132
|
+
throw mismatch(profile, "Qwen3 dimensions do not match the stable model ID")
|
|
133
|
+
}
|
|
134
|
+
let expectedRopeTheta: Float = switch profile.id {
|
|
135
|
+
case .ternaryBonsai4B: 5_000_000
|
|
136
|
+
default: 1_000_000
|
|
137
|
+
}
|
|
138
|
+
guard configuration.rmsNormEpsilon == 1e-6,
|
|
139
|
+
configuration.ropeTheta == expectedRopeTheta,
|
|
140
|
+
configuration.attentionBias == false,
|
|
141
|
+
configuration.tieWordEmbeddings == (profile.id != .ternaryBonsai8B),
|
|
142
|
+
identity.hiddenActivation == "silu",
|
|
143
|
+
identity.layerTypes.count == profile.hiddenLayerCount,
|
|
144
|
+
identity.layerTypes.allSatisfy({ $0 == "full_attention" }),
|
|
145
|
+
identity.ropeScaling.ropeType == "yarn",
|
|
146
|
+
identity.ropeScaling.factor == 4,
|
|
147
|
+
identity.ropeScaling.originalMaximumPositions == profile.contextLength / 4 else {
|
|
148
|
+
throw mismatch(profile, "Qwen3 architecture settings do not match the stable model ID")
|
|
149
|
+
}
|
|
150
|
+
guard configuration.quantization?.bits == 2,
|
|
151
|
+
configuration.quantization?.groupSize == 128,
|
|
152
|
+
identity.quantization == Quantization(groupSize: 128, bits: 2) else {
|
|
153
|
+
throw mismatch(profile, "expected packed 2-bit/group-128 quantization")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private static func validateQwen35(modelURL: URL, profile: BonsaiTextModelProfile) throws {
|
|
158
|
+
let data = try Data(contentsOf: modelURL.appendingPathComponent("config.json"))
|
|
159
|
+
let identity = try JSONDecoder().decode(Qwen35Identity.self, from: data)
|
|
160
|
+
let text = identity.textConfiguration
|
|
161
|
+
guard identity.architectures == ["Qwen3_5ForConditionalGeneration"],
|
|
162
|
+
identity.modelType == "qwen3_5", text.modelType == "qwen3_5_text" else {
|
|
163
|
+
throw mismatch(profile, "expected Qwen3.5 conditional-generation/text architecture")
|
|
164
|
+
}
|
|
165
|
+
guard text.vocabularySize == profile.vocabularySize,
|
|
166
|
+
text.hiddenSize == profile.hiddenSize,
|
|
167
|
+
text.intermediateSize == profile.intermediateSize,
|
|
168
|
+
text.hiddenLayerCount == profile.hiddenLayerCount,
|
|
169
|
+
text.attentionHeadCount == profile.attentionHeadCount,
|
|
170
|
+
text.keyValueHeadCount == profile.keyValueHeadCount,
|
|
171
|
+
text.headDimension == profile.headDimension,
|
|
172
|
+
text.maximumPositionCount == profile.contextLength else {
|
|
173
|
+
throw mismatch(profile, "Qwen3.5 dimensions do not match the stable model ID")
|
|
174
|
+
}
|
|
175
|
+
let requiredQuantization = Quantization(groupSize: 128, bits: 2)
|
|
176
|
+
guard identity.quantization == requiredQuantization,
|
|
177
|
+
text.quantization == requiredQuantization else {
|
|
178
|
+
throw mismatch(profile, "Qwen3.5 requires 2-bit/group-128 at both snapshot levels")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let backendConfiguration = try Qwen35Configuration.load(from: modelURL)
|
|
182
|
+
try backendConfiguration.validate(expectedQuantization: Qwen35Quantization(groupSize: 128, bits: 2))
|
|
183
|
+
guard backendConfiguration.rmsNormEps == 1e-6,
|
|
184
|
+
backendConfiguration.attentionBias == false,
|
|
185
|
+
backendConfiguration.tieWordEmbeddings == false,
|
|
186
|
+
backendConfiguration.fullAttentionInterval == 4,
|
|
187
|
+
backendConfiguration.linearConvKernelDim == 4,
|
|
188
|
+
backendConfiguration.linearKeyHeadDim == 128,
|
|
189
|
+
backendConfiguration.linearValueHeadDim == 128,
|
|
190
|
+
backendConfiguration.linearNumKeyHeads == 16,
|
|
191
|
+
backendConfiguration.linearNumValueHeads == 48,
|
|
192
|
+
backendConfiguration.ropeTheta == 10_000_000,
|
|
193
|
+
backendConfiguration.partialRotaryFactor == 0.25 else {
|
|
194
|
+
throw mismatch(profile, "Qwen3.5 architecture settings do not match the stable model ID")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private static func mismatch(_ profile: BonsaiTextModelProfile, _ detail: String) -> BonsaiTextError {
|
|
199
|
+
.snapshotMismatch(model: profile.id, detail: detail)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public struct GenerationParameters: Sendable {
|
|
4
|
+
public var maximumNewTokens: Int
|
|
5
|
+
public var temperature: Float
|
|
6
|
+
public var topP: Float
|
|
7
|
+
public var topK: Int?
|
|
8
|
+
public var repetitionPenalty: Float
|
|
9
|
+
public var repetitionContextSize: Int
|
|
10
|
+
public var seed: UInt64?
|
|
11
|
+
|
|
12
|
+
public init(
|
|
13
|
+
maximumNewTokens: Int = 512,
|
|
14
|
+
temperature: Float = 0.7,
|
|
15
|
+
topP: Float = 0.95,
|
|
16
|
+
topK: Int? = nil,
|
|
17
|
+
repetitionPenalty: Float = 1,
|
|
18
|
+
repetitionContextSize: Int = 64,
|
|
19
|
+
seed: UInt64? = nil
|
|
20
|
+
) {
|
|
21
|
+
self.maximumNewTokens = maximumNewTokens
|
|
22
|
+
self.temperature = temperature
|
|
23
|
+
self.topP = topP
|
|
24
|
+
self.topK = topK
|
|
25
|
+
self.repetitionPenalty = repetitionPenalty
|
|
26
|
+
self.repetitionContextSize = repetitionContextSize
|
|
27
|
+
self.seed = seed
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static let balanced = GenerationParameters()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public enum GenerationFinishReason: String, Codable, Sendable {
|
|
34
|
+
case endOfSequence
|
|
35
|
+
case length
|
|
36
|
+
case cancelled
|
|
37
|
+
case callbackStopped
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public struct TokenGenerationResult: Sendable {
|
|
41
|
+
public let promptTokenCount: Int
|
|
42
|
+
public let tokenIDs: [Int]
|
|
43
|
+
public let finishReason: GenerationFinishReason
|
|
44
|
+
|
|
45
|
+
public init(promptTokenCount: Int, tokenIDs: [Int], finishReason: GenerationFinishReason) {
|
|
46
|
+
self.promptTokenCount = promptTokenCount
|
|
47
|
+
self.tokenIDs = tokenIDs
|
|
48
|
+
self.finishReason = finishReason
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public enum TextGenerationError: LocalizedError {
|
|
53
|
+
case emptyPrompt
|
|
54
|
+
case invalidMaximumNewTokens(Int)
|
|
55
|
+
case contextExceeded(requested: Int, maximum: Int)
|
|
56
|
+
case invalidTokenID(Int)
|
|
57
|
+
case invalidTemperature(Float)
|
|
58
|
+
case invalidTopP(Float)
|
|
59
|
+
case invalidTopK(Int)
|
|
60
|
+
case invalidRepetitionPenalty(Float)
|
|
61
|
+
case invalidRepetitionContextSize(Int)
|
|
62
|
+
|
|
63
|
+
public var errorDescription: String? {
|
|
64
|
+
switch self {
|
|
65
|
+
case .emptyPrompt:
|
|
66
|
+
"The pre-tokenized prompt is empty"
|
|
67
|
+
case .invalidMaximumNewTokens(let value):
|
|
68
|
+
"Maximum new tokens must be nonnegative, got \(value)"
|
|
69
|
+
case .contextExceeded(let requested, let maximum):
|
|
70
|
+
"Requested context length \(requested) exceeds model maximum \(maximum)"
|
|
71
|
+
case .invalidTokenID(let value):
|
|
72
|
+
"Token ID \(value) is outside the model vocabulary"
|
|
73
|
+
case .invalidTemperature(let value):
|
|
74
|
+
"Temperature must be nonnegative, got \(value)"
|
|
75
|
+
case .invalidTopP(let value):
|
|
76
|
+
"Top-p must be in (0, 1], got \(value)"
|
|
77
|
+
case .invalidTopK(let value):
|
|
78
|
+
"Top-k must be positive, got \(value)"
|
|
79
|
+
case .invalidRepetitionPenalty(let value):
|
|
80
|
+
"Repetition penalty must be positive, got \(value)"
|
|
81
|
+
case .invalidRepetitionContextSize(let value):
|
|
82
|
+
"Repetition context size must be nonnegative, got \(value)"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import MLX
|
|
2
|
+
|
|
3
|
+
/// Encoding is an independent capability; callers do not need a causal LM head.
|
|
4
|
+
public protocol HiddenStateModel: AnyObject {
|
|
5
|
+
var vocabularySize: Int { get }
|
|
6
|
+
var maximumContextLength: Int { get }
|
|
7
|
+
|
|
8
|
+
func hiddenStates(
|
|
9
|
+
for inputIDs: MLXArray,
|
|
10
|
+
at layerIndices: Set<Int>,
|
|
11
|
+
attentionMask: MLXArray?
|
|
12
|
+
) -> [Int: MLXArray]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public protocol CausalLanguageModel: AnyObject {
|
|
16
|
+
var vocabularySize: Int { get }
|
|
17
|
+
var maximumContextLength: Int { get }
|
|
18
|
+
|
|
19
|
+
func makeCache() -> [TextKVCache]
|
|
20
|
+
func logits(for inputIDs: MLXArray, cache: [TextKVCache]) -> MLXArray
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import MLX
|
|
2
|
+
|
|
3
|
+
public final class TextKVCache {
|
|
4
|
+
private var keys: MLXArray?
|
|
5
|
+
private var values: MLXArray?
|
|
6
|
+
|
|
7
|
+
public private(set) var length = 0
|
|
8
|
+
|
|
9
|
+
public init() {}
|
|
10
|
+
|
|
11
|
+
public func update(keys newKeys: MLXArray, values newValues: MLXArray) -> (MLXArray, MLXArray) {
|
|
12
|
+
keys = keys.map { concatenated([$0, newKeys], axis: 2) } ?? newKeys
|
|
13
|
+
values = values.map { concatenated([$0, newValues], axis: 2) } ?? newValues
|
|
14
|
+
length += newKeys.dim(2)
|
|
15
|
+
return (keys!, values!)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public func clear() {
|
|
19
|
+
keys = nil
|
|
20
|
+
values = nil
|
|
21
|
+
length = 0
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import MLX
|
|
2
|
+
|
|
3
|
+
public enum TokenSampler {
|
|
4
|
+
public static func validate(_ parameters: GenerationParameters) throws {
|
|
5
|
+
guard parameters.maximumNewTokens >= 0 else {
|
|
6
|
+
throw TextGenerationError.invalidMaximumNewTokens(parameters.maximumNewTokens)
|
|
7
|
+
}
|
|
8
|
+
guard parameters.temperature >= 0 else {
|
|
9
|
+
throw TextGenerationError.invalidTemperature(parameters.temperature)
|
|
10
|
+
}
|
|
11
|
+
guard parameters.topP > 0, parameters.topP <= 1 else {
|
|
12
|
+
throw TextGenerationError.invalidTopP(parameters.topP)
|
|
13
|
+
}
|
|
14
|
+
if let topK = parameters.topK, topK <= 0 {
|
|
15
|
+
throw TextGenerationError.invalidTopK(topK)
|
|
16
|
+
}
|
|
17
|
+
guard parameters.repetitionPenalty > 0 else {
|
|
18
|
+
throw TextGenerationError.invalidRepetitionPenalty(parameters.repetitionPenalty)
|
|
19
|
+
}
|
|
20
|
+
guard parameters.repetitionContextSize >= 0 else {
|
|
21
|
+
throw TextGenerationError.invalidRepetitionContextSize(parameters.repetitionContextSize)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public static func sample(
|
|
26
|
+
logits: MLXArray,
|
|
27
|
+
parameters: GenerationParameters,
|
|
28
|
+
tokenHistory: [Int]
|
|
29
|
+
) throws -> MLXArray {
|
|
30
|
+
try validate(parameters)
|
|
31
|
+
let selected: MLXArray
|
|
32
|
+
switch logits.ndim {
|
|
33
|
+
case 3: selected = logits[0, -1]
|
|
34
|
+
case 2: selected = logits[0]
|
|
35
|
+
default: selected = logits
|
|
36
|
+
}
|
|
37
|
+
let values = selected
|
|
38
|
+
|
|
39
|
+
if parameters.repetitionPenalty != 1,
|
|
40
|
+
parameters.repetitionContextSize > 0,
|
|
41
|
+
!tokenHistory.isEmpty {
|
|
42
|
+
let recent = Set(tokenHistory.suffix(parameters.repetitionContextSize))
|
|
43
|
+
let valid = recent.filter { $0 >= 0 && $0 < values.dim(-1) }
|
|
44
|
+
if !valid.isEmpty {
|
|
45
|
+
let indices = MLXArray(valid.sorted().map(Int32.init))
|
|
46
|
+
let repeated = MLX.take(values, indices, axis: -1)
|
|
47
|
+
values[indices] = MLX.where(
|
|
48
|
+
repeated .> 0,
|
|
49
|
+
repeated / parameters.repetitionPenalty,
|
|
50
|
+
repeated * parameters.repetitionPenalty
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if parameters.temperature == 0 {
|
|
56
|
+
return argMax(values).asType(Int32.self)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Match mlx-lm ordering: select top-p from untempered probabilities.
|
|
60
|
+
let probabilities = softmax(values, axis: -1)
|
|
61
|
+
let candidateIndices: MLXArray
|
|
62
|
+
if let topK = parameters.topK, topK < probabilities.dim(-1) {
|
|
63
|
+
candidateIndices = argPartition(probabilities, kth: -topK, axis: -1)[(-topK)...]
|
|
64
|
+
} else {
|
|
65
|
+
candidateIndices = MLXArray.arange(probabilities.dim(-1))
|
|
66
|
+
}
|
|
67
|
+
let candidates = take(probabilities, candidateIndices, axis: -1)
|
|
68
|
+
let order = argSort(-candidates, axis: -1)
|
|
69
|
+
let sortedIndices = take(candidateIndices, order, axis: -1)
|
|
70
|
+
let sortedProbabilities = take(candidates, order, axis: -1)
|
|
71
|
+
let cumulativeBefore = cumsum(sortedProbabilities, axis: -1) - sortedProbabilities
|
|
72
|
+
let filtered = MLX.where(
|
|
73
|
+
cumulativeBefore .< parameters.topP,
|
|
74
|
+
log(sortedProbabilities) / parameters.temperature,
|
|
75
|
+
MLX.full(sortedProbabilities.shape, values: -Float.infinity)
|
|
76
|
+
)
|
|
77
|
+
return sortedIndices[MLXRandom.categorical(filtered)].asType(Int32.self)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -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
|
+
}
|