Skip to content

Commit df4d686

Browse files
committed
out-of-band PR feedback
- turn off training mode - clarify documentation - handle MaterializedArray passed as inputs (state) to a compiled function - contents are constant by definition so can be discarded and captured normally
1 parent c9b05e6 commit df4d686

6 files changed

Lines changed: 70 additions & 13 deletions

File tree

Source/MLX/MaterializedArray.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Numerics
1919
/// - You can only create an instance via ``MLXArray/materialized()``
2020
/// or ``materialize(_:)->MaterializedArray``
2121
/// - Mutation methods are marked as unavailable and will `fatalError`
22-
/// if you somehow manage to call them.
22+
/// if you somehow manage to call them (e.g. through dynamic types)
2323
/// - It is declared `@unchecked Sendable` and may be passed freely between
2424
/// tasks, actors, and other concurrency boundaries.
2525
///

Source/MLX/Transforms+Compile.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ final class CompiledFunction: @unchecked (Sendable) {
4343
}
4444

4545
func innerCall(_ arguments: [MLXArray]) -> [MLXArray] {
46-
let stateInputs = inputs.flatMap { $0.innerState() }
46+
// MaterializedArray can never change, so it doesn't need to be
47+
// threaded through the tracer swap below -- it is simply captured
48+
// as a constant, the same as any other array reachable from the
49+
// closure that isn't part of `inputs`/`outputs`.
50+
let stateInputs = inputs.flatMap { $0.innerState() }.filter { !($0 is MaterializedArray) }
4751
let argumentsCount = arguments.count
4852

4953
// inner function to hande the compilation. this is called
@@ -70,7 +74,9 @@ final class CompiledFunction: @unchecked (Sendable) {
7074
let result = f(tracerArguments)
7175

7276
// recapture the state as it may have changed
73-
let stateOutputTracers = outputs.flatMap { $0.innerState() }.map { $0.copyContext() }
77+
let stateOutputTracers = outputs.flatMap { $0.innerState() }
78+
.filter { !($0 is MaterializedArray) }
79+
.map { $0.copyContext() }
7480

7581
// put the original values back in the state
7682
for (s, saved) in zip(stateInputs, savedStateInputs) {
@@ -119,7 +125,7 @@ final class CompiledFunction: @unchecked (Sendable) {
119125
let resultsPlusStateOutput = mlx_vector_array_values(resultVector)
120126

121127
// push the stateOutput into the state
122-
let stateOutput = outputs.flatMap { $0.innerState() }
128+
let stateOutput = outputs.flatMap { $0.innerState() }.filter { !($0 is MaterializedArray) }
123129

124130
for (s, newValues) in zip(stateOutput, resultsPlusStateOutput.suffix(stateOutput.count)) {
125131
s._updateInternal(newValues)

Source/MLXNN/MaterializedModule.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import MLX
1212
/// mutation surface so the wrapped module cannot be modified through this
1313
/// reference.
1414
///
15+
/// This also sets ``Module/training`` to `false`.
16+
///
1517
/// Note: only parameters that can be be mutated, e.g. are wrapped with `@ParameterInfo`,
1618
/// will actually be updated to the `MaterializedArray` type. All others will
1719
/// be evaluated and are materialized, even if they do not have the type
@@ -42,17 +44,20 @@ import MLX
4244
///
4345
/// ## What is sealed
4446
///
45-
/// The following `Module` operations are marked `@available(*, unavailable)`
46-
/// on `MaterializedModule` and will trap if called:
47-
///
48-
/// - `update(parameters:...)` and `update(modules:...)`
49-
/// - `updateModule(key:_:)`
50-
/// - `apply(filter:map:)`
51-
/// - `freeze(...)` / `unfreeze(...)`
52-
/// - `train(_:)`
47+
/// `MaterializedModule` is not a `Module` and does not provide access to the
48+
/// wrapped `Module`. No access to bare `MLXArray` is provided, though
49+
/// for purposes of introspection the ``parameters()`` can give
50+
/// `MaterializedArray`. There are also properties like ``parameterNBytes``
51+
/// and ``parameterCount`` for callers that just need size information.
5352
///
5453
/// ## Calling the wrapped module
5554
///
55+
/// `MaterializedModule`is not a `Module` subclass and cannot conform
56+
/// to protocols that have `Module` requirements. Users may want to split their
57+
/// protocols into requirements for performing inference (which
58+
/// `MaterializedModule` is meant for) and for doing training where `Module`
59+
/// is required and this type is not appropriate (immutable).
60+
///
5661
/// `MaterializedModule` does not itself know how to invoke `base`; that is
5762
/// added per-layer-shape via an extension that constrains `LayerType`.
5863
/// For example, every ``UnaryLayer`` already supports being called with a
@@ -137,6 +142,8 @@ open class MaterializedModule<LayerType: Module>: IndentedDescription, @unchecke
137142

138143
public init(_ base: consuming LayerType) {
139144
self._base = base
145+
146+
self._base.train(false)
140147
self._base.materialize()
141148

142149
// seal the consumed base so that any retained reference held by a

Source/MLXNN/Module.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,7 @@ public enum ModuleValue {
15321532
// cannot check via unwapProperty -- see wrappedValue.set
15331533
}
15341534

1535+
// See also ModuleInfo.Setter
15351536
struct Setter: TypeErasedSetter {
15361537
unowned var info: ParameterInfo<T>
15371538

@@ -1679,6 +1680,7 @@ private protocol TypeErasedSetterProvider {
16791680
}
16801681
}
16811682

1683+
// See also ParameterInfo.Setter
16821684
struct Setter: TypeErasedSetter {
16831685
unowned var info: ModuleInfo<T>
16841686

Source/MLXNN/Quantized.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,11 @@ open class QuantizedLinear: Linear, Quantized {
362362
}
363363

364364
open override var parameterCount: Int {
365-
scales.size * groupSize
365+
if biases != nil {
366+
scales.size * groupSize * 2
367+
} else {
368+
scales.size * groupSize
369+
}
366370
}
367371

368372
public override func unfreeze(

Tests/MLXTests/MaterializedTests.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,44 @@ struct MaterializedTests {
4040
_ = await t.result
4141
}
4242

43+
@Test
44+
func testCompileInputsOutputsMaterialized() {
45+
// A MaterializedArray can never change, so it has nothing for
46+
// compile(inputs:outputs:) to observe or update -- it is simply
47+
// captured as a constant when the function is traced, the same as
48+
// any other value reachable from the closure that isn't part of
49+
// `inputs`/`outputs`. This is the documented
50+
// `compile(inputs: [state], outputs: [state], f)` pattern (see
51+
// compilation.md).
52+
let state = MLXRandom.normal([4, 4]).materialized()
53+
let compiled = compile(inputs: [state], outputs: [state]) { (x: MLXArray) in
54+
x + state
55+
}
56+
57+
let x = MLXRandom.normal([4, 4])
58+
let r1 = compiled(x)
59+
let r2 = x + state
60+
#expect(r1.allClose(r2).item(Bool.self))
61+
62+
// same for a `Module` whose parameters were materialized (e.g. via
63+
// `MaterializedModule`) -- this is the documented
64+
// `compile(inputs: [model], outputs: [model])` pattern.
65+
//
66+
// Note: capturing the interior module is documented as forbidden but
67+
// used here to test. Do not copy this pattern!
68+
let l = Linear(4, 4)
69+
_ = MaterializedModule(l)
70+
71+
let compiledModel = compile(inputs: [l], outputs: [l]) { (x: MLXArray) in
72+
l(x)
73+
}
74+
75+
let input = MLXRandom.normal([4, 4])
76+
let modelResult = compiledModel(input)
77+
let expected = l(input)
78+
#expect(modelResult.allClose(expected).item(Bool.self))
79+
}
80+
4381
@Test
4482
func testMaterializedLinear() async {
4583
let l = Linear(10, 10)

0 commit comments

Comments
 (0)