Skip to content

Commit 4741a02

Browse files
committed
Use MaterializedArray for Sendable conformance
- adopt changes from ml-explore/mlx-swift#418 - we don't need private box types -- the technique becomes general - it also opens up some potential for synchronous evaluation
1 parent feff32d commit 4741a02

4 files changed

Lines changed: 66 additions & 36 deletions

File tree

Libraries/MLXEmbedders/EmbedderModelContainer.swift

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import Foundation
44
import MLXLMCommon
55

6+
// TODO dkoski -- make the example code be synchronous?
7+
68
/// Container for embedder models that guarantees single threaded access.
79
///
810
/// Wrap models used by e.g. the UI in a ModelContainer. Callers can access
@@ -27,52 +29,52 @@ import MLXLMCommon
2729
/// }
2830
/// ```
2931
public final class EmbedderModelContainer: Sendable {
30-
private let context: SerialAccessContainer<EmbedderModelContext>
32+
private let context: EmbedderModelContext
3133

3234
public var configuration: ModelConfiguration {
33-
get async {
34-
await context.read { $0.configuration }
35-
}
35+
context.configuration
3636
}
3737

3838
public var tokenizer: Tokenizer {
39-
get async {
40-
await context.read { $0.tokenizer }
41-
}
39+
context.tokenizer
4240
}
4341

4442
public var poolingStrategy: Pooling.Strategy {
45-
get async {
46-
await context.read { $0.pooling.strategy }
47-
}
43+
context.pooling.strategy
4844
}
4945

5046
public init(context: consuming EmbedderModelContext) {
51-
self.context = .init(context)
47+
self.context = context
5248
}
5349

5450
/// Perform an action on the ``EmbedderModelContext``.
5551
/// Callers _must_ eval any `MLXArray` before returning as `MLXArray` is not `Sendable`.
5652
///
57-
/// - Note: The closure receives `EmbedderModelContext` which is not `Sendable`. This is intentional -
58-
/// the closure runs within the actor's isolation, ensuring thread-safe access to the model.
5953
/// - Note: The `sending` keyword indicates the return value is transferred (not shared) across
6054
/// isolation boundaries, allowing non-Sendable types to be safely returned.
6155
public func perform<R: Sendable>(
6256
_ action: @Sendable (EmbedderModelContext) async throws -> sending R
6357
) async rethrows -> sending R {
64-
try await context.read {
65-
try await action($0)
66-
}
58+
try await action(context)
59+
}
60+
61+
/// Perform an action on the ``EmbedderModelContext``.
62+
///
63+
/// This is the synchronous form of ``perform(_:)`` and has
64+
/// fewer restrictions.
65+
public func perform<R>(
66+
_ action: @Sendable (EmbedderModelContext) throws -> R
67+
) rethrows -> R {
68+
try action(context)
6769
}
6870

6971
@available(*, deprecated, message: "use perform(_: (EmbedderModelContext) -> R) instead")
7072
public func perform<R: Sendable>(
7173
_ action: @Sendable (EmbeddingModel, Tokenizer, Pooling) async throws -> sending R
7274
) async rethrows -> sending R {
73-
try await context.read {
74-
try await action($0.model, $0.tokenizer, $0.pooling)
75-
}
75+
try await action(
76+
context.model, context.tokenizer, context.pooling
77+
)
7678
}
7779

7880
/// Perform an action on the ``EmbedderModelContext`` with additional (non `Sendable`) context values.
@@ -83,32 +85,32 @@ public final class EmbedderModelContainer: Sendable {
8385
_ action: @Sendable (EmbedderModelContext, V) async throws -> R
8486
) async rethrows -> sending R {
8587
let values = SendableBox(values)
86-
return try await context.read {
87-
try await action($0, values.consume())
88-
}
88+
return try await action(context, values.consume())
8989
}
9090

9191
/// Update the owned `EmbedderModelContext`.
9292
/// - Parameter action: update action
93+
@available(
94+
*, unavailable,
95+
message: "mutate EmbedderModelContext before passing to EmbedderModelContainer"
96+
)
9397
public func update(_ action: @Sendable (inout EmbedderModelContext) -> Void) async {
94-
await context.update {
95-
action(&$0)
96-
}
98+
fatalError("update not supported")
9799
}
98100

99101
// MARK: - Thread-safe convenience methods
100102

101103
/// The resolved local model directory for the loaded container.
102104
public var modelDirectory: URL {
103-
get async throws {
104-
try (await configuration).modelDirectory
105+
get throws {
106+
try configuration.modelDirectory
105107
}
106108
}
107109

108110
/// The resolved local tokenizer directory for the loaded container.
109111
public var tokenizerDirectory: URL {
110-
get async throws {
111-
try (await configuration).tokenizerDirectory
112+
get throws {
113+
try configuration.tokenizerDirectory
112114
}
113115
}
114116
}

Libraries/MLXEmbedders/EmbeddingModel.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,31 @@ extension EmbeddingModel {
4848
attentionMask: attentionMask)
4949
}
5050
}
51+
52+
extension MaterializedModule: EmbeddingModel, BaseLanguageModel where LayerType: EmbeddingModel {
53+
54+
public var vocabularySize: Int { _base.vocabularySize }
55+
public var poolingStrategy: Pooling.Strategy? { _base.poolingStrategy }
56+
public var maxPositionEmbeddings: Int? { _base.maxPositionEmbeddings }
57+
58+
public func sanitize(weights: [String: MLXArray]) -> [String: MLXArray] {
59+
_base.sanitize(weights: weights)
60+
}
61+
62+
public func sanitize(weights: [String: MLXArray], metadata: [String: String]) -> [String:
63+
MLXArray]
64+
{
65+
_base.sanitize(weights: weights, metadata: metadata)
66+
}
67+
68+
public func callAsFunction(
69+
_ inputs: MLXArray,
70+
positionIds: MLXArray? = nil,
71+
tokenTypeIds: MLXArray? = nil,
72+
attentionMask: MLXArray? = nil
73+
) -> EmbeddingModelOutput {
74+
return _base(
75+
inputs, positionIds: positionIds, tokenTypeIds: tokenTypeIds,
76+
attentionMask: attentionMask)
77+
}
78+
}

Libraries/MLXEmbedders/ModelFactory.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,18 @@ public class EmbedderRegistry: AbstractModelRegistry, @unchecked Sendable {
126126
///
127127
/// This is created using a ``EmbedderModelFactory`` and often used
128128
/// inside a ``EmbedderModelContainer``.
129-
public struct EmbedderModelContext {
129+
public struct EmbedderModelContext: Sendable {
130130
public var configuration: ModelConfiguration
131-
public var model: any EmbeddingModel
131+
public var model: any EmbeddingModel & Sendable
132132
public var tokenizer: any Tokenizer
133133
public let pooling: Pooling
134134

135135
public init(
136-
configuration: ModelConfiguration, model: any EmbeddingModel,
136+
configuration: ModelConfiguration, model: some EmbeddingModel,
137137
tokenizer: any Tokenizer, pooling: Pooling
138138
) {
139139
self.configuration = configuration
140-
self.model = model
140+
self.model = MaterializedModule(model)
141141
self.tokenizer = tokenizer
142142
self.pooling = pooling
143143
}

Libraries/MLXEmbedders/Pooling.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func loadPooling(modelDirectory: URL, model: EmbeddingModel) -> Pooling {
6363
///
6464
/// `Pooling` takes the sequence of hidden states from a transformer model and collapses them
6565
/// into a single vector using strategies like mean, max, or token selection.
66-
open class Pooling: Module {
66+
public struct Pooling: Sendable {
6767

6868
/// Supported pooling strategies.
6969
public enum Strategy: Sendable {
@@ -82,10 +82,10 @@ open class Pooling: Module {
8282
}
8383

8484
/// The active strategy used for pooling hidden states.
85-
public private(set) var strategy: Strategy
85+
public let strategy: Strategy
8686

8787
/// Optional dimension to truncate the resulting embedding to.
88-
public private(set) var dimension: Int?
88+
public let dimension: Int?
8989

9090
/// Initializes a `Pooling` module with a specific strategy.
9191
/// - Parameters:

0 commit comments

Comments
 (0)