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,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
|
+
}
|
|
@@ -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
|
+
}
|