Skip to content

MaterializedArray is a Sendable MLXArray#418

Open
davidkoski wants to merge 6 commits into
mainfrom
materialized-array
Open

MaterializedArray is a Sendable MLXArray#418
davidkoski wants to merge 6 commits into
mainfrom
materialized-array

Conversation

@davidkoski

Copy link
Copy Markdown
Collaborator

Proposed changes

  • once an array has been evaluated we can use it as Sendable
  • a subtype of MLXArray that is Sendable

Looking for feedback on this. I will add documentation once I test it a bit. Check out the tests to see how it works.

Checklist

Put an x in the boxes that apply.

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

import Foundation
import Numerics

public final class MaterializedArray: MLXArray, @unchecked Sendable {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new subtype of MLXArray that is Sendable


public final class MaterializedArray: MLXArray, @unchecked Sendable {

init(materialized ctx: consuming mlx_array) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create using materialize(x) or x.materialized()


import Cmlx
import Foundation
import Numerics

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore this -- I had to move them to be part of MLXArray (not an extension) so that I could override them

Comment thread Source/MLX/MLXArray.swift
import Numerics

public final class MLXArray {
public class MLXArray: ExpressibleByArrayLiteral {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • remove final -- it is still closed outside the package
  • I don't think we were seeing any particular benefit from the final
  • add ExpressibleByArrayLiteral to the main type so that subclasses can override

Comment thread Source/MLX/MLXArray.swift Outdated
self.init(ctx)
}

// MARK: - Inits

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the body of MLXArray+Init.swift -- it is moved here because a subclass cannot override an init (or maybe even a method) that is not in the main class (vs extensions).

Comment thread Source/MLXNN/Linear.swift
public let weight: MLXArray
public let bias: MLXArray?
@ParameterInfo public var weight: MLXArray
@ParameterInfo public var bias: MLXArray?

@davidkoski davidkoski Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By making these @ParameterInfo we can replace the MLXArray with a MaterializedArray -- see the tests.

Note: this isn't required but it seemed like it might be useful.


import MLX

open class MaterializedModule<LayerType: Module>: Module, @unchecked Sendable {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A container for a Module that is Sendable.

}
}

extension MaterializedModule where LayerType: UnaryLayer {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use this pattern e.g. for LanguageModel in mlx-swift-lm

Comment thread Source/MLXNN/Module.swift
func update(
parameters: ModuleParameters, verify: VerifyUpdate, path: [String] = [],
modulePath: [String] = [],
mutate: (Module, String, MLXArray, MLXArray) -> Void

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally we update the backing point in an MLXArray. For materialize() I want to replace the MLXArray instance with a MaterializedArray.

let x = MLXArray(10) + 5
let m = materialize(x)
let t = Task {
print(m + 3)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tada, pass MLXArray (materialized) between isolation contexts! The compiler would give an error if we did this with x.


let t = Task {
let i = MLXRandom.normal([10, 10])
let r = lm(i)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same thing with a Module.

Comment thread Package.swift
@@ -1,4 +1,4 @@
// swift-tools-version: 5.12
// swift-tools-version: 6.2

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time to turn on the stricter concurrency checks.

@davidkoski davidkoski force-pushed the materialized-array branch from f18e7c2 to 2e0965e Compare June 8, 2026 05:11
- once an array has been evaluated we can use it as Sendable
- a subtype of MLXArray that is Sendable
@davidkoski davidkoski force-pushed the materialized-array branch from 2e0965e to 18106c8 Compare June 8, 2026 05:34
davidkoski added a commit to ml-explore/mlx-swift-lm that referenced this pull request Jun 9, 2026
- 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
open class MaterializedModule<LayerType: Module>: Module, @unchecked Sendable {

/// Usable by extensions to implement `callAsFunction()` -- anyone else, DO NOT USE.
public let _base: LayerType

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: would it make sense to avoid exposing this as public or at least rename it to something like _unsafeBase? Since callers can mutate the wrapper module through this reference.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! It has to be public because:

  1. the class can't be subclassed as it is final/Sendable (the former being a requirement for the latter)
  2. since it can't be subclassed it has to be extended and the extensions can only access public members

But renaming it _unsafeBase is a good idea!

I have a staged commit where it marks the held model as immutable -- that will give additional runtime protection.

The integration in mlx-swift-lm for Embedders was good but perhaps too simple. I need to do the same for LLM/VLM and see what pops up.

@Test
func testMaterializedLinear() async throws {
let l = Linear(10, 10)
let lm = try MaterializedModule(l)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: MaterializedModule's init is not throwing, so this test can drop both try and throws.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was at one point but not now.

  • remove try

import MLX
import MLXNN
import Testing

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: It would be nice to add a unit test for MaterializedModule.parameters(), so it verify the wrapper still exposes the materialized parameter tree.

davidkoski added a commit to ml-explore/mlx-swift-lm that referenced this pull request Jun 18, 2026
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants