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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bleam",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"packageManager": "yarn@1.22.22",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"react-native": "0.86.0",
|
|
40
40
|
"tsx": "^4.21.0",
|
|
41
41
|
"typescript": "~6.0.3",
|
|
42
|
+
"wrangler": "4.110.0",
|
|
42
43
|
"ws": "^8.21.0",
|
|
43
44
|
"yocto-spinner": "^1.2.1",
|
|
44
45
|
"yoctocolors": "^2.1.2"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateImage } from 'bleam/ai'
|
|
1
|
+
import { bonsai, generateImage } from 'bleam/ai'
|
|
2
2
|
import { sx } from 'bleam/styles'
|
|
3
3
|
import { Button } from 'bleam/ui'
|
|
4
4
|
import { Window } from 'bleam/window'
|
|
@@ -6,6 +6,7 @@ import { useState } from 'react'
|
|
|
6
6
|
import { Image, Text, View } from 'react-native'
|
|
7
7
|
|
|
8
8
|
const appName = 'Bleam Basic'
|
|
9
|
+
const model = bonsai('ternary-bonsai-image-4b')
|
|
9
10
|
|
|
10
11
|
export default function App() {
|
|
11
12
|
const [uri, setUri] = useState<string>()
|
|
@@ -15,6 +16,7 @@ export default function App() {
|
|
|
15
16
|
setStatus('Generating...')
|
|
16
17
|
try {
|
|
17
18
|
const result = await generateImage({
|
|
19
|
+
model,
|
|
18
20
|
prompt: 'A precise editorial illustration of a small native Mac app',
|
|
19
21
|
aspectRatio: '1:1',
|
|
20
22
|
})
|
|
@@ -152,7 +152,7 @@ public enum ModelRegistry {
|
|
|
152
152
|
case klein4B_bf16 = "klein4b-bf16"
|
|
153
153
|
case klein4B_8bit = "klein4b-8bit"
|
|
154
154
|
case klein4B_4bit = "klein4b-4bit"
|
|
155
|
-
case bonsaiKlein4B_2bit = "bonsai-
|
|
155
|
+
case bonsaiKlein4B_2bit = "ternary-bonsai-image-4b"
|
|
156
156
|
case bonsaiKlein4B_1bit = "bonsai-klein4b-1bit"
|
|
157
157
|
case klein9B_8bit = "klein9b-8bit"
|
|
158
158
|
case klein9B_4bit = "klein9b-4bit"
|
|
@@ -198,12 +198,17 @@ public final class FluxImageRunner: @unchecked Sendable {
|
|
|
198
198
|
private static let referenceImageMaxLongSide = 768
|
|
199
199
|
|
|
200
200
|
private let paths: FluxImageRunnerPaths
|
|
201
|
+
private let transformerProfile: Flux2TransformerRepoProfile
|
|
201
202
|
private let scheduler = FlowMatchEulerScheduler()
|
|
202
203
|
private var transformer: Flux2Transformer2DModel?
|
|
203
204
|
private var vae: AutoencoderKLFlux2?
|
|
204
205
|
|
|
205
|
-
public init(
|
|
206
|
+
public init(
|
|
207
|
+
paths: FluxImageRunnerPaths,
|
|
208
|
+
transformerProfile: Flux2TransformerRepoProfile = ModelRegistry.TransformerRepo.moxinOrgKlein4B8bit.profile
|
|
209
|
+
) {
|
|
206
210
|
self.paths = paths
|
|
211
|
+
self.transformerProfile = transformerProfile
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
public func generate(
|
|
@@ -1237,7 +1242,7 @@ public final class FluxImageRunner: @unchecked Sendable {
|
|
|
1237
1242
|
return transformer
|
|
1238
1243
|
}
|
|
1239
1244
|
|
|
1240
|
-
let profile =
|
|
1245
|
+
let profile = transformerProfile
|
|
1241
1246
|
let configURL = paths.transformer.appendingPathComponent("config.json")
|
|
1242
1247
|
let config = FileManager.default.fileExists(atPath: configURL.path)
|
|
1243
1248
|
? (try? Flux2TransformerConfig.load(from: configURL)) ?? .klein4B
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import MLX
|
|
3
|
+
|
|
4
|
+
public enum BonsaiTextGenerator {
|
|
5
|
+
public static func generate(
|
|
6
|
+
profile: BonsaiTextModelProfile,
|
|
7
|
+
modelURL: URL,
|
|
8
|
+
inputIDs: [Int],
|
|
9
|
+
endTokenIDs: Set<Int>? = nil,
|
|
10
|
+
parameters: GenerationParameters? = nil,
|
|
11
|
+
isCancelled: @escaping @Sendable () -> Bool,
|
|
12
|
+
onToken: @escaping @Sendable (Int) -> Bool
|
|
13
|
+
) async throws -> TokenGenerationResult {
|
|
14
|
+
try BonsaiTextSnapshotValidator.validate(modelURL: modelURL, profile: profile)
|
|
15
|
+
let parameters = parameters ?? profile.defaultGenerationParameters
|
|
16
|
+
let endTokenIDs = endTokenIDs ?? profile.defaultEndTokenIDs
|
|
17
|
+
defer { MLX.Memory.clearCache() }
|
|
18
|
+
|
|
19
|
+
switch profile.backend {
|
|
20
|
+
case .qwen3:
|
|
21
|
+
try Task.checkCancellation()
|
|
22
|
+
let model = try Qwen3CheckpointLoader.load(from: modelURL)
|
|
23
|
+
try Task.checkCancellation()
|
|
24
|
+
return try Qwen3InferenceSession(model: model, endTokenIDs: endTokenIDs).generate(
|
|
25
|
+
inputIDs: inputIDs,
|
|
26
|
+
parameters: parameters,
|
|
27
|
+
isCancelled: { Task.isCancelled || isCancelled() },
|
|
28
|
+
onToken: onToken
|
|
29
|
+
)
|
|
30
|
+
case .qwen35:
|
|
31
|
+
let model = try Qwen35Checkpoint.load(
|
|
32
|
+
from: modelURL,
|
|
33
|
+
expectedQuantization: Qwen35Quantization(groupSize: 128, bits: 2)
|
|
34
|
+
)
|
|
35
|
+
let session = Qwen35Session(model: model)
|
|
36
|
+
defer { session.reset() }
|
|
37
|
+
return try await session.generate(
|
|
38
|
+
inputIDs: inputIDs,
|
|
39
|
+
parameters: parameters,
|
|
40
|
+
endTokenIDs: endTokenIDs,
|
|
41
|
+
isCancelled: { Task.isCancelled || isCancelled() },
|
|
42
|
+
onToken: onToken
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public enum BonsaiTextModelID: String, CaseIterable, Codable, Sendable {
|
|
4
|
+
case ternaryBonsai17B = "ternary-bonsai-1.7b"
|
|
5
|
+
case ternaryBonsai4B = "ternary-bonsai-4b"
|
|
6
|
+
case ternaryBonsai8B = "ternary-bonsai-8b"
|
|
7
|
+
case ternaryBonsai27B = "ternary-bonsai-27b"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public enum BonsaiTextBackend: Sendable {
|
|
11
|
+
case qwen3
|
|
12
|
+
case qwen35
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public struct BonsaiTextModelProfile: Sendable {
|
|
16
|
+
public let id: BonsaiTextModelID
|
|
17
|
+
public let backend: BonsaiTextBackend
|
|
18
|
+
public let vocabularySize: Int
|
|
19
|
+
public let hiddenSize: Int
|
|
20
|
+
public let intermediateSize: Int
|
|
21
|
+
public let hiddenLayerCount: Int
|
|
22
|
+
public let attentionHeadCount: Int
|
|
23
|
+
public let keyValueHeadCount: Int
|
|
24
|
+
public let headDimension: Int
|
|
25
|
+
public let contextLength: Int
|
|
26
|
+
public let defaultEndTokenIDs: Set<Int>
|
|
27
|
+
public let defaultGenerationParameters: GenerationParameters
|
|
28
|
+
|
|
29
|
+
public static func resolve(_ value: String) throws -> Self {
|
|
30
|
+
guard let id = BonsaiTextModelID(rawValue: value) else {
|
|
31
|
+
throw BonsaiTextError.unsupportedModel(value)
|
|
32
|
+
}
|
|
33
|
+
return profile(for: id)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static func profile(for id: BonsaiTextModelID) -> Self {
|
|
37
|
+
switch id {
|
|
38
|
+
case .ternaryBonsai17B:
|
|
39
|
+
return Self(
|
|
40
|
+
id: id, backend: .qwen3, vocabularySize: 151_669, hiddenSize: 2_048,
|
|
41
|
+
intermediateSize: 6_144, hiddenLayerCount: 28, attentionHeadCount: 16,
|
|
42
|
+
keyValueHeadCount: 8, headDimension: 128, contextLength: 32_768,
|
|
43
|
+
defaultEndTokenIDs: [151_643, 151_645],
|
|
44
|
+
defaultGenerationParameters: GenerationParameters(
|
|
45
|
+
maximumNewTokens: 256, temperature: 0.5, topP: 0.85,
|
|
46
|
+
repetitionPenalty: 1, repetitionContextSize: 64
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
case .ternaryBonsai4B:
|
|
50
|
+
return Self(
|
|
51
|
+
id: id, backend: .qwen3, vocabularySize: 151_669, hiddenSize: 2_560,
|
|
52
|
+
intermediateSize: 9_728, hiddenLayerCount: 36, attentionHeadCount: 32,
|
|
53
|
+
keyValueHeadCount: 8, headDimension: 128, contextLength: 32_768,
|
|
54
|
+
defaultEndTokenIDs: [151_643, 151_645],
|
|
55
|
+
defaultGenerationParameters: GenerationParameters(
|
|
56
|
+
maximumNewTokens: 256, temperature: 0.5, topP: 0.85,
|
|
57
|
+
repetitionPenalty: 1, repetitionContextSize: 64
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
case .ternaryBonsai8B:
|
|
61
|
+
return Self(
|
|
62
|
+
id: id, backend: .qwen3, vocabularySize: 151_669, hiddenSize: 4_096,
|
|
63
|
+
intermediateSize: 12_288, hiddenLayerCount: 36, attentionHeadCount: 32,
|
|
64
|
+
keyValueHeadCount: 8, headDimension: 128, contextLength: 65_536,
|
|
65
|
+
defaultEndTokenIDs: [151_643, 151_645],
|
|
66
|
+
defaultGenerationParameters: GenerationParameters(
|
|
67
|
+
maximumNewTokens: 256, temperature: 0.5, topP: 0.85,
|
|
68
|
+
repetitionPenalty: 1, repetitionContextSize: 64
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
case .ternaryBonsai27B:
|
|
72
|
+
return Self(
|
|
73
|
+
id: id, backend: .qwen35, vocabularySize: 248_320, hiddenSize: 5_120,
|
|
74
|
+
intermediateSize: 17_408, hiddenLayerCount: 64, attentionHeadCount: 24,
|
|
75
|
+
keyValueHeadCount: 4, headDimension: 256, contextLength: 262_144,
|
|
76
|
+
defaultEndTokenIDs: [248_044, 248_046],
|
|
77
|
+
defaultGenerationParameters: GenerationParameters(
|
|
78
|
+
maximumNewTokens: 2_048, temperature: 0.7, topP: 0.95, topK: 20,
|
|
79
|
+
repetitionPenalty: 1, repetitionContextSize: 128
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public enum BonsaiTextError: LocalizedError {
|
|
87
|
+
case unsupportedModel(String)
|
|
88
|
+
case invalidModelPath(String)
|
|
89
|
+
case snapshotMismatch(model: BonsaiTextModelID, detail: String)
|
|
90
|
+
case invalidSeed(Int)
|
|
91
|
+
case invalidGenerationParameters(String)
|
|
92
|
+
|
|
93
|
+
public var errorDescription: String? {
|
|
94
|
+
switch self {
|
|
95
|
+
case .unsupportedModel(let value):
|
|
96
|
+
"Unsupported Bonsai text model: \(value)"
|
|
97
|
+
case .invalidModelPath(let path):
|
|
98
|
+
"Bonsai model path is not a directory: \(path)"
|
|
99
|
+
case .snapshotMismatch(let model, let detail):
|
|
100
|
+
"Snapshot does not match \(model.rawValue): \(detail)"
|
|
101
|
+
case .invalidSeed(let seed):
|
|
102
|
+
"Seed must be nonnegative, got \(seed)"
|
|
103
|
+
case .invalidGenerationParameters(let detail):
|
|
104
|
+
"Invalid generation parameters: \(detail)"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -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
|
+
}
|