bleam 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.cjs +608 -59
- package/dist/ai.d.cts +217 -89
- package/dist/ai.d.ts +217 -89
- package/dist/ai.js +601 -59
- package/dist/cli.cjs +142 -94
- package/dist/cli.d.cts +11 -27
- package/dist/cli.d.ts +11 -27
- package/dist/cli.js +143 -95
- package/dist/files.d.cts +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/platform.cjs +1 -13
- package/dist/platform.d.cts +1 -26
- package/dist/platform.d.ts +1 -26
- package/dist/platform.js +1 -10
- package/dist/schema.d.cts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/state.d.cts +1 -1
- package/dist/state.d.ts +1 -1
- package/dist/{ui-Bg11tvlc.d.ts → ui-CHc4xEs_.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +2 -1
- package/templates/image-generation/app/index.tsx +3 -1
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +1 -1
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +7 -2
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextGenerator.swift +46 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextModel.swift +107 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Bonsai/BonsaiTextSnapshotValidator.swift +201 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/GenerationTypes.swift +85 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/ModelCapabilities.swift +21 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TextKVCache.swift +23 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Core/TokenSampler.swift +79 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3CheckpointLoader.swift +161 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Configuration.swift +119 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3InferenceSession.swift +84 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Layers.swift +197 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen3/Qwen3Model.swift +130 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Checkpoint.swift +102 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Configuration.swift +140 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Model.swift +112 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/Qwen35/Qwen35Session.swift +121 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridCache.swift +42 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridFullAttention.swift +57 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLayers.swift +58 -0
- package/templates/native/ios/Bleam/AI/TextGeneration/QwenHybrid/QwenHybridLinearAttention.swift +111 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +0 -97
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +128 -121
- package/templates/native/ios/GenerationService/BonsaiTextGenerationRunner.swift +103 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +19 -7
- package/templates/native/ios/GenerationService/main.swift +174 -48
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +3 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +44 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +225 -5
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +47 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +0 -175
- package/templates/text-generation/app/index.tsx +93 -0
- package/templates/text-generation/app.config.ts +5 -0
- package/templates/updates/README.md +116 -0
- package/templates/updates/src/index.ts +292 -0
- package/templates/updates/src/schema.ts +230 -0
- package/templates/updates/tsconfig.json +12 -0
- package/templates/updates/wrangler.jsonc +19 -0
- package/templates/native/ios/PlatformHelper/Info.plist +0 -29
- package/templates/native/ios/PlatformHelper/main.swift +0 -335
- /package/dist/{files-DwA7pzr3.d.cts → files-4ZEoAWiv.d.ts} +0 -0
- /package/dist/{files-VrkQlKIT.d.ts → files-Dt5mbzLq.d.cts} +0 -0
- /package/dist/{schema-DOOjfXvs.d.ts → schema-CYh6n8GS.d.ts} +0 -0
- /package/dist/{schema-ENSMX_1t.d.cts → schema-oeOrd3i1.d.cts} +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import MLX
|
|
3
|
+
|
|
4
|
+
final class BonsaiTextGenerationRunner: @unchecked Sendable {
|
|
5
|
+
private let request: BonsaiTextGenerationRequest
|
|
6
|
+
private let emit: @Sendable (BonsaiTextGenerationEvent) -> Void
|
|
7
|
+
private let stateLock = NSLock()
|
|
8
|
+
private var cancellationRequested = false
|
|
9
|
+
private var generatedTokenIDs: [Int] = []
|
|
10
|
+
|
|
11
|
+
init(
|
|
12
|
+
request: BonsaiTextGenerationRequest,
|
|
13
|
+
emit: @escaping @Sendable (BonsaiTextGenerationEvent) -> Void
|
|
14
|
+
) {
|
|
15
|
+
self.request = request
|
|
16
|
+
self.emit = emit
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func cancel() {
|
|
20
|
+
stateLock.withLock { cancellationRequested = true }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func run(isCancelled externalCancellation: @escaping @Sendable () -> Bool) async {
|
|
24
|
+
defer { MLX.Memory.clearCache() }
|
|
25
|
+
do {
|
|
26
|
+
let profile = try BonsaiTextModelProfile.resolve(request.model)
|
|
27
|
+
let parameters = try generationParameters(defaults: profile.defaultGenerationParameters)
|
|
28
|
+
let endTokenIDs = request.endTokenIDs.isEmpty ? profile.defaultEndTokenIDs : Set(request.endTokenIDs)
|
|
29
|
+
let result = try await BonsaiTextGenerator.generate(
|
|
30
|
+
profile: profile,
|
|
31
|
+
modelURL: URL(fileURLWithPath: request.modelPath, isDirectory: true),
|
|
32
|
+
inputIDs: request.inputIDs,
|
|
33
|
+
endTokenIDs: endTokenIDs,
|
|
34
|
+
parameters: parameters,
|
|
35
|
+
isCancelled: { [weak self] in
|
|
36
|
+
externalCancellation() || (self?.isCancellationRequested ?? true)
|
|
37
|
+
},
|
|
38
|
+
onToken: { [weak self, emit, id = request.id] tokenID in
|
|
39
|
+
guard let tokenIDs = self?.append(tokenID) else { return false }
|
|
40
|
+
emit(.init(type: .update, id: id, tokenIDs: tokenIDs))
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if result.finishReason == .cancelled || shouldCancel(externalCancellation) {
|
|
46
|
+
emit(.init(type: .canceled, id: request.id, tokenIDs: result.tokenIDs, finishReason: GenerationFinishReason.cancelled.rawValue))
|
|
47
|
+
} else {
|
|
48
|
+
emit(.init(type: .completed, id: request.id, tokenIDs: result.tokenIDs, finishReason: result.finishReason.rawValue))
|
|
49
|
+
}
|
|
50
|
+
} catch is CancellationError {
|
|
51
|
+
emit(.init(type: .canceled, id: request.id, tokenIDs: generatedTokens, finishReason: GenerationFinishReason.cancelled.rawValue))
|
|
52
|
+
} catch {
|
|
53
|
+
emit(.init(type: .failed, id: request.id, tokenIDs: generatedTokens, message: error.localizedDescription))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private var isCancellationRequested: Bool {
|
|
58
|
+
stateLock.withLock { cancellationRequested }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private var generatedTokens: [Int] {
|
|
62
|
+
stateLock.withLock { generatedTokenIDs }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func append(_ tokenID: Int) -> [Int] {
|
|
66
|
+
stateLock.withLock {
|
|
67
|
+
generatedTokenIDs.append(tokenID)
|
|
68
|
+
return generatedTokenIDs
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func shouldCancel(_ externalCancellation: @escaping @Sendable () -> Bool) -> Bool {
|
|
73
|
+
Task.isCancelled || externalCancellation() || isCancellationRequested
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func generationParameters(defaults: GenerationParameters) throws -> GenerationParameters {
|
|
77
|
+
let seed: UInt64?
|
|
78
|
+
if let requestedSeed = request.seed {
|
|
79
|
+
guard let converted = UInt64(exactly: requestedSeed) else {
|
|
80
|
+
throw BonsaiTextError.invalidSeed(requestedSeed)
|
|
81
|
+
}
|
|
82
|
+
seed = converted
|
|
83
|
+
} else {
|
|
84
|
+
seed = defaults.seed
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let parameters = GenerationParameters(
|
|
88
|
+
maximumNewTokens: request.maximumNewTokens ?? defaults.maximumNewTokens,
|
|
89
|
+
temperature: request.temperature.map(Float.init) ?? defaults.temperature,
|
|
90
|
+
topP: request.topP.map(Float.init) ?? defaults.topP,
|
|
91
|
+
topK: request.topK ?? defaults.topK,
|
|
92
|
+
repetitionPenalty: request.repetitionPenalty.map(Float.init) ?? defaults.repetitionPenalty,
|
|
93
|
+
repetitionContextSize: request.repetitionContextSize ?? defaults.repetitionContextSize,
|
|
94
|
+
seed: seed
|
|
95
|
+
)
|
|
96
|
+
try TokenSampler.validate(parameters)
|
|
97
|
+
guard parameters.temperature.isFinite, parameters.topP.isFinite,
|
|
98
|
+
parameters.repetitionPenalty.isFinite else {
|
|
99
|
+
throw BonsaiTextError.invalidGenerationParameters("floating-point values must be finite")
|
|
100
|
+
}
|
|
101
|
+
return parameters
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -7,6 +7,7 @@ import UniformTypeIdentifiers
|
|
|
7
7
|
final class ImageGenerationRunner: @unchecked Sendable {
|
|
8
8
|
private let request: GenerationWorkerRequest
|
|
9
9
|
private let emit: @Sendable (GenerationWorkerEvent) -> Void
|
|
10
|
+
private let stateLock = NSLock()
|
|
10
11
|
private var isCancelled = false
|
|
11
12
|
|
|
12
13
|
init(
|
|
@@ -18,7 +19,7 @@ final class ImageGenerationRunner: @unchecked Sendable {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
func cancel() {
|
|
21
|
-
isCancelled = true
|
|
22
|
+
stateLock.withLock { isCancelled = true }
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
func run(isCancelled externalCancellation: @escaping @Sendable () -> Bool) async throws {
|
|
@@ -88,10 +89,13 @@ final class ImageGenerationRunner: @unchecked Sendable {
|
|
|
88
89
|
// First service version is deliberately memory-first: the runner releases
|
|
89
90
|
// text encoder, transformer, and VAE stages instead of keeping a warm
|
|
90
91
|
// pipeline. Add warm caching later only behind explicit idle trimming.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
let runner = FluxImageRunner(
|
|
93
|
+
paths: FluxImageRunnerPaths(
|
|
94
|
+
transformer: URL(fileURLWithPath: request.transformerPath, isDirectory: true),
|
|
95
|
+
vae: URL(fileURLWithPath: request.vaePath, isDirectory: true)
|
|
96
|
+
),
|
|
97
|
+
transformerProfile: transformerProfile()
|
|
98
|
+
)
|
|
95
99
|
var fluxStage = "denoise"
|
|
96
100
|
do {
|
|
97
101
|
return try runner.generate(
|
|
@@ -147,11 +151,13 @@ final class ImageGenerationRunner: @unchecked Sendable {
|
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
private func shouldCancel(_ externalCancellation: @escaping @Sendable () -> Bool) -> Bool {
|
|
150
|
-
Task.isCancelled || isCancelled || externalCancellation()
|
|
154
|
+
Task.isCancelled || stateLock.withLock { isCancelled } || externalCancellation()
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
private func validateRequest() throws {
|
|
154
|
-
guard request.model == "flux2-klein-4b-8bit-abliterated"
|
|
158
|
+
guard request.model == "flux2-klein-4b-8bit-abliterated"
|
|
159
|
+
|| request.model == "ternary-bonsai-image-4b"
|
|
160
|
+
else {
|
|
155
161
|
throw GenerationWorkerFailure(message: "Unsupported image model: \(request.model)")
|
|
156
162
|
}
|
|
157
163
|
guard !request.prompt.isEmpty else {
|
|
@@ -264,6 +270,12 @@ final class ImageGenerationRunner: @unchecked Sendable {
|
|
|
264
270
|
}
|
|
265
271
|
}
|
|
266
272
|
|
|
273
|
+
private func transformerProfile() -> Flux2TransformerRepoProfile {
|
|
274
|
+
request.model == "ternary-bonsai-image-4b"
|
|
275
|
+
? ModelRegistry.TransformerRepo.prismMLBonsaiKlein4B2bit.profile
|
|
276
|
+
: ModelRegistry.TransformerRepo.moxinOrgKlein4B8bit.profile
|
|
277
|
+
}
|
|
278
|
+
|
|
267
279
|
private static func validateSafetensors(atPath path: String) throws {
|
|
268
280
|
let url = URL(fileURLWithPath: path)
|
|
269
281
|
let fileSize: UInt64
|
|
@@ -2,68 +2,130 @@ import Foundation
|
|
|
2
2
|
import MLX
|
|
3
3
|
|
|
4
4
|
private final class GenerationRuntime: @unchecked Sendable {
|
|
5
|
+
private enum OperationKind {
|
|
6
|
+
case image
|
|
7
|
+
case text
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
private let modelWarmTimeout: Duration = .seconds(60)
|
|
11
|
+
private let stateLock = NSLock()
|
|
12
|
+
private var activeID: String?
|
|
13
|
+
private var activeKind: OperationKind?
|
|
6
14
|
private var activeTask: Task<Void, Never>?
|
|
7
|
-
private var
|
|
15
|
+
private var activeImageRunner: ImageGenerationRunner?
|
|
16
|
+
private var activeTextRunner: BonsaiTextGenerationRunner?
|
|
8
17
|
private var trimTask: Task<Void, Never>?
|
|
9
18
|
private var isCancelled = false
|
|
19
|
+
private var operationEpoch: UInt64 = 0
|
|
20
|
+
private var isShuttingDown = false
|
|
10
21
|
|
|
11
22
|
var isRunning: Bool {
|
|
12
|
-
activeTask != nil
|
|
23
|
+
stateLock.withLock { activeTask != nil }
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
func
|
|
26
|
+
func startImage(
|
|
16
27
|
request: GenerationWorkerRequest,
|
|
17
28
|
emit: @escaping @Sendable (GenerationWorkerEvent) -> Void
|
|
18
29
|
) -> Bool {
|
|
19
|
-
|
|
30
|
+
stateLock.withLock {
|
|
31
|
+
guard activeTask == nil, !isShuttingDown else { return false }
|
|
32
|
+
trimTask?.cancel()
|
|
33
|
+
trimTask = nil
|
|
34
|
+
isCancelled = false
|
|
35
|
+
operationEpoch &+= 1
|
|
36
|
+
activeID = request.jobID.uuidString
|
|
37
|
+
activeKind = .image
|
|
38
|
+
let runner = ImageGenerationRunner(request: request, emit: emit)
|
|
39
|
+
activeImageRunner = runner
|
|
40
|
+
activeTask = Task { [weak self] in
|
|
41
|
+
await self?.runImage(request, runner: runner, emit: emit)
|
|
42
|
+
}
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func startText(
|
|
48
|
+
request: BonsaiTextGenerationRequest,
|
|
49
|
+
emit: @escaping @Sendable (BonsaiTextGenerationEvent) -> Void
|
|
50
|
+
) -> Bool {
|
|
51
|
+
stateLock.withLock {
|
|
52
|
+
guard activeTask == nil, !isShuttingDown else { return false }
|
|
53
|
+
trimTask?.cancel()
|
|
54
|
+
trimTask = nil
|
|
55
|
+
isCancelled = false
|
|
56
|
+
operationEpoch &+= 1
|
|
57
|
+
activeID = request.id
|
|
58
|
+
activeKind = .text
|
|
59
|
+
let runner = BonsaiTextGenerationRunner(request: request, emit: emit)
|
|
60
|
+
activeTextRunner = runner
|
|
61
|
+
activeTask = Task { [weak self] in
|
|
62
|
+
await self?.runText(request, runner: runner)
|
|
63
|
+
}
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func cancelImage() -> Bool {
|
|
69
|
+
let operation = stateLock.withLock { () -> (Task<Void, Never>?, ImageGenerationRunner?)? in
|
|
70
|
+
guard activeKind == .image else { return nil }
|
|
71
|
+
isCancelled = true
|
|
72
|
+
trimTask?.cancel()
|
|
73
|
+
trimTask = nil
|
|
74
|
+
return (activeTask, activeImageRunner)
|
|
75
|
+
}
|
|
76
|
+
guard let operation else { return false }
|
|
77
|
+
operation.0?.cancel()
|
|
78
|
+
operation.1?.cancel()
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
20
81
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
82
|
+
func cancelText(id: String) -> Bool {
|
|
83
|
+
let operation = stateLock.withLock { () -> (Task<Void, Never>?, BonsaiTextGenerationRunner?)? in
|
|
84
|
+
guard activeKind == .text, activeID == id else { return nil }
|
|
85
|
+
isCancelled = true
|
|
86
|
+
trimTask?.cancel()
|
|
87
|
+
trimTask = nil
|
|
88
|
+
return (activeTask, activeTextRunner)
|
|
26
89
|
}
|
|
90
|
+
guard let operation else { return false }
|
|
91
|
+
operation.0?.cancel()
|
|
92
|
+
operation.1?.cancel()
|
|
27
93
|
return true
|
|
28
94
|
}
|
|
29
95
|
|
|
30
|
-
func
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
96
|
+
func beginShutdown() -> Bool {
|
|
97
|
+
stateLock.withLock {
|
|
98
|
+
guard activeTask == nil, !isShuttingDown else { return false }
|
|
99
|
+
isShuttingDown = true
|
|
100
|
+
operationEpoch &+= 1
|
|
101
|
+
trimTask?.cancel()
|
|
102
|
+
trimTask = nil
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
36
105
|
}
|
|
37
106
|
|
|
38
107
|
func trimModelsNow() async {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
await Task.yield()
|
|
46
|
-
MLX.Memory.clearCache()
|
|
108
|
+
stateLock.withLock {
|
|
109
|
+
trimTask?.cancel()
|
|
110
|
+
trimTask = nil
|
|
111
|
+
MLX.Memory.clearCache()
|
|
112
|
+
MLX.Memory.clearCache()
|
|
113
|
+
}
|
|
47
114
|
print("[GenerationService] trim active=\(MLX.Memory.activeMemory / 1_048_576)MB cache=\(MLX.Memory.cacheMemory / 1_048_576)MB")
|
|
48
115
|
}
|
|
49
116
|
|
|
50
|
-
private func
|
|
117
|
+
private func runImage(
|
|
51
118
|
_ request: GenerationWorkerRequest,
|
|
119
|
+
runner: ImageGenerationRunner,
|
|
52
120
|
emit: @escaping @Sendable (GenerationWorkerEvent) -> Void
|
|
53
121
|
) async {
|
|
54
|
-
let runner = ImageGenerationRunner(request: request, emit: emit)
|
|
55
|
-
activeRunner = runner
|
|
56
122
|
defer {
|
|
57
|
-
|
|
58
|
-
activeRunner = nil
|
|
59
|
-
}
|
|
60
|
-
activeTask = nil
|
|
61
|
-
scheduleTrimAfterIdle()
|
|
123
|
+
finish(id: request.jobID.uuidString, imageRunner: runner)
|
|
62
124
|
}
|
|
63
125
|
|
|
64
126
|
do {
|
|
65
127
|
try await runner.run(isCancelled: { [weak self] in
|
|
66
|
-
Task.isCancelled || (self?.
|
|
128
|
+
Task.isCancelled || (self?.cancellationRequested ?? false)
|
|
67
129
|
})
|
|
68
130
|
} catch is CancellationError {
|
|
69
131
|
emit(.init(type: .canceled))
|
|
@@ -76,17 +138,59 @@ private final class GenerationRuntime: @unchecked Sendable {
|
|
|
76
138
|
}
|
|
77
139
|
}
|
|
78
140
|
|
|
141
|
+
private func runText(
|
|
142
|
+
_ request: BonsaiTextGenerationRequest,
|
|
143
|
+
runner: BonsaiTextGenerationRunner
|
|
144
|
+
) async {
|
|
145
|
+
defer { finish(id: request.id, textRunner: runner) }
|
|
146
|
+
await runner.run(isCancelled: { [weak self] in
|
|
147
|
+
Task.isCancelled || (self?.cancellationRequested ?? true)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private var cancellationRequested: Bool {
|
|
152
|
+
stateLock.withLock { isCancelled }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private func finish(
|
|
156
|
+
id: String,
|
|
157
|
+
imageRunner: ImageGenerationRunner? = nil,
|
|
158
|
+
textRunner: BonsaiTextGenerationRunner? = nil
|
|
159
|
+
) {
|
|
160
|
+
let finished = stateLock.withLock {
|
|
161
|
+
guard activeID == id else { return false }
|
|
162
|
+
if let imageRunner, activeImageRunner !== imageRunner { return false }
|
|
163
|
+
if let textRunner, activeTextRunner !== textRunner { return false }
|
|
164
|
+
activeID = nil
|
|
165
|
+
activeKind = nil
|
|
166
|
+
activeImageRunner = nil
|
|
167
|
+
activeTextRunner = nil
|
|
168
|
+
activeTask = nil
|
|
169
|
+
isCancelled = false
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
if finished { scheduleTrimAfterIdle() }
|
|
173
|
+
}
|
|
174
|
+
|
|
79
175
|
private func scheduleTrimAfterIdle() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
176
|
+
stateLock.withLock {
|
|
177
|
+
trimTask?.cancel()
|
|
178
|
+
let scheduledEpoch = operationEpoch
|
|
179
|
+
trimTask = Task { [weak self] in
|
|
180
|
+
guard let self else { return }
|
|
181
|
+
do {
|
|
182
|
+
try await Task.sleep(for: modelWarmTimeout)
|
|
183
|
+
} catch {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
guard !Task.isCancelled else { return }
|
|
187
|
+
self.stateLock.withLock {
|
|
188
|
+
guard self.activeTask == nil, self.operationEpoch == scheduledEpoch else { return }
|
|
189
|
+
self.trimTask = nil
|
|
190
|
+
MLX.Memory.clearCache()
|
|
191
|
+
MLX.Memory.clearCache()
|
|
192
|
+
}
|
|
87
193
|
}
|
|
88
|
-
guard !Task.isCancelled else { return }
|
|
89
|
-
await self.trimModelsNow()
|
|
90
194
|
}
|
|
91
195
|
print("[GenerationService] model trim scheduled 60s")
|
|
92
196
|
}
|
|
@@ -94,10 +198,11 @@ private final class GenerationRuntime: @unchecked Sendable {
|
|
|
94
198
|
|
|
95
199
|
private final class GenerationService: NSObject, GenerationServiceProtocol {
|
|
96
200
|
private weak var connection: NSXPCConnection?
|
|
97
|
-
private let runtime
|
|
201
|
+
private let runtime: GenerationRuntime
|
|
98
202
|
|
|
99
|
-
init(connection: NSXPCConnection) {
|
|
203
|
+
init(connection: NSXPCConnection, runtime: GenerationRuntime) {
|
|
100
204
|
self.connection = connection
|
|
205
|
+
self.runtime = runtime
|
|
101
206
|
}
|
|
102
207
|
|
|
103
208
|
func startGeneration(requestData: Data, reply: @escaping (Bool) -> Void) {
|
|
@@ -107,14 +212,27 @@ private final class GenerationService: NSObject, GenerationServiceProtocol {
|
|
|
107
212
|
return
|
|
108
213
|
}
|
|
109
214
|
|
|
110
|
-
reply(runtime.
|
|
215
|
+
reply(runtime.startImage(request: request) { [weak self] event in
|
|
111
216
|
self?.emit(event)
|
|
112
217
|
})
|
|
113
218
|
}
|
|
114
219
|
|
|
115
220
|
func cancelGeneration(reply: @escaping (Bool) -> Void) {
|
|
116
|
-
runtime.
|
|
117
|
-
|
|
221
|
+
reply(runtime.cancelImage())
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func startTextGeneration(requestData: Data, reply: @escaping (Bool) -> Void) {
|
|
225
|
+
guard let request = try? JSONDecoder().decode(BonsaiTextGenerationRequest.self, from: requestData) else {
|
|
226
|
+
reply(false)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
reply(runtime.startText(request: request) { [weak self] event in
|
|
230
|
+
self?.emitText(event)
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func cancelTextGeneration(id: String, reply: @escaping (Bool) -> Void) {
|
|
235
|
+
reply(runtime.cancelText(id: id))
|
|
118
236
|
}
|
|
119
237
|
|
|
120
238
|
func memoryTelemetry(reply: @escaping (Data?) -> Void) {
|
|
@@ -126,7 +244,7 @@ private final class GenerationService: NSObject, GenerationServiceProtocol {
|
|
|
126
244
|
}
|
|
127
245
|
|
|
128
246
|
func shutdown(reply: @escaping (Bool) -> Void) {
|
|
129
|
-
guard
|
|
247
|
+
guard runtime.beginShutdown() else {
|
|
130
248
|
reply(false)
|
|
131
249
|
return
|
|
132
250
|
}
|
|
@@ -142,13 +260,21 @@ private final class GenerationService: NSObject, GenerationServiceProtocol {
|
|
|
142
260
|
let client = connection?.remoteObjectProxy as? GenerationServiceClientProtocol
|
|
143
261
|
client?.generationWorkerDidEmit(eventData: data)
|
|
144
262
|
}
|
|
263
|
+
|
|
264
|
+
private func emitText(_ event: BonsaiTextGenerationEvent) {
|
|
265
|
+
guard let data = try? JSONEncoder().encode(event) else { return }
|
|
266
|
+
let client = connection?.remoteObjectProxy as? GenerationServiceClientProtocol
|
|
267
|
+
client?.textGenerationWorkerDidEmit(eventData: data)
|
|
268
|
+
}
|
|
145
269
|
}
|
|
146
270
|
|
|
147
271
|
private final class GenerationServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|
272
|
+
private let runtime = GenerationRuntime()
|
|
273
|
+
|
|
148
274
|
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
|
|
149
275
|
connection.exportedInterface = NSXPCInterface(with: GenerationServiceProtocol.self)
|
|
150
276
|
connection.remoteObjectInterface = NSXPCInterface(with: GenerationServiceClientProtocol.self)
|
|
151
|
-
connection.exportedObject = GenerationService(connection: connection)
|
|
277
|
+
connection.exportedObject = GenerationService(connection: connection, runtime: runtime)
|
|
152
278
|
connection.resume()
|
|
153
279
|
return true
|
|
154
280
|
}
|
|
@@ -3,10 +3,13 @@ import Foundation
|
|
|
3
3
|
@objc protocol GenerationServiceProtocol {
|
|
4
4
|
func startGeneration(requestData: Data, reply: @escaping (Bool) -> Void)
|
|
5
5
|
func cancelGeneration(reply: @escaping (Bool) -> Void)
|
|
6
|
+
func startTextGeneration(requestData: Data, reply: @escaping (Bool) -> Void)
|
|
7
|
+
func cancelTextGeneration(id: String, reply: @escaping (Bool) -> Void)
|
|
6
8
|
func memoryTelemetry(reply: @escaping (Data?) -> Void)
|
|
7
9
|
func shutdown(reply: @escaping (Bool) -> Void)
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
@objc protocol GenerationServiceClientProtocol {
|
|
11
13
|
func generationWorkerDidEmit(eventData: Data)
|
|
14
|
+
func textGenerationWorkerDidEmit(eventData: Data)
|
|
12
15
|
}
|
|
@@ -122,3 +122,47 @@ struct GenerationWorkerLoRA: Codable, Sendable {
|
|
|
122
122
|
let path: String
|
|
123
123
|
let scale: Double
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
struct BonsaiTextGenerationRequest: Codable, Sendable {
|
|
127
|
+
let id: String
|
|
128
|
+
let model: String
|
|
129
|
+
let modelPath: String
|
|
130
|
+
let inputIDs: [Int]
|
|
131
|
+
let endTokenIDs: [Int]
|
|
132
|
+
let maximumNewTokens: Int?
|
|
133
|
+
let temperature: Double?
|
|
134
|
+
let topP: Double?
|
|
135
|
+
let topK: Int?
|
|
136
|
+
let repetitionPenalty: Double?
|
|
137
|
+
let repetitionContextSize: Int?
|
|
138
|
+
let seed: Int?
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
struct BonsaiTextGenerationEvent: Codable, Sendable {
|
|
142
|
+
enum EventType: String, Codable, Sendable {
|
|
143
|
+
case update
|
|
144
|
+
case completed
|
|
145
|
+
case canceled
|
|
146
|
+
case failed
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let type: EventType
|
|
150
|
+
let id: String
|
|
151
|
+
let tokenIDs: [Int]
|
|
152
|
+
let finishReason: String?
|
|
153
|
+
let message: String?
|
|
154
|
+
|
|
155
|
+
init(
|
|
156
|
+
type: EventType,
|
|
157
|
+
id: String,
|
|
158
|
+
tokenIDs: [Int] = [],
|
|
159
|
+
finishReason: String? = nil,
|
|
160
|
+
message: String? = nil
|
|
161
|
+
) {
|
|
162
|
+
self.type = type
|
|
163
|
+
self.id = id
|
|
164
|
+
self.tokenIDs = tokenIDs
|
|
165
|
+
self.finishReason = finishReason
|
|
166
|
+
self.message = message
|
|
167
|
+
}
|
|
168
|
+
}
|