Skip to content

MaterializedArray is a Sendable MLXArray#418

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

MaterializedArray is a Sendable MLXArray#418
davidkoski wants to merge 11 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()

Comment thread Source/MLX/MLXArray+Init.swift
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
Comment thread Source/MLXNN/Linear.swift
Comment thread Source/MLXNN/MaterializedModule.swift Outdated
}
}

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

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.

Added docs to show how this is done -- the some / any dance took a while to work out.

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.

Comment thread Tests/MLXTests/MaterializedTests.swift Outdated
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.

Comment thread Tests/MLXTests/MaterializedTests.swift Outdated

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 Outdated
@davidkoski davidkoski force-pushed the materialized-array branch 2 times, most recently 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.

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 now exposed via _spi so callers can't see it but implementations can.

Comment thread Tests/MLXTests/MaterializedTests.swift Outdated
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
@ronaldmannak

Copy link
Copy Markdown
Contributor

@davidkoski Is there any ETA on merging this PR? I'm working on a MLX-Swift-lm PR where this would be extremely useful

@davidkoski

Copy link
Copy Markdown
Collaborator Author

I think I have tested out most of this in my mlx-swift-lm branch (not all of it is pushed), so I can focus on wrapping this up. I will try to get this part in this week.

@ronaldmannak

Copy link
Copy Markdown
Contributor

Great! Thanks David

@davidkoski davidkoski force-pushed the materialized-array branch from 7030ed3 to f717b7f Compare July 8, 2026 17:36

// MARK: - Expressible by literals

extension 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.

This gets moved inside the class definition -- required for subclasses.

/// because `UnaryLayer` is not a `Module`.
///
/// The class is `open` so you could give it a try.
open class MaterializedModule<LayerType: Module>: IndentedDescription, @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.

Note: this has been changed and is no longer a Module itself. That keeps the API surface smaller and means there are less confusing things that it exposes (like children() producing mutable Module instances).

In mlx-swift-lm (ml-explore/mlx-swift-lm#335) this worked out with a little surgery on the protocols.

Callers can still get a view of the parameters (now recast as MaterializedArray) and there is a common theme of counting the bytes for various reasons, now restored.

Comment thread Source/MLXNN/Module.swift
Comment on lines -406 to +414
public func update(parameters: ModuleParameters) -> Self {
public func update(parameters: ModuleParameters) {

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.

These are source breaking changes, though I expect they were never used. This was matching the python implementation:

def update(self, parameters: dict, strict: bool = True) -> Module

I guess this would allow a caller to chain calls, maybe layer.update(...).notSure(). It was never used in mlx-swift-lm.

Technically this is no longer required -- it was a side effect of the original implementation of MaterializedModule being is-a Module. I think it is a worthwhile cleanup so I am leaving it in.

@davidkoski davidkoski requested a review from angeloskath July 8, 2026 18:13
davidkoski added a commit to ml-explore/mlx-swift-lm that referenced this pull request Jul 10, 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.

3 participants