-
Notifications
You must be signed in to change notification settings - Fork 349
Optimize memory usage #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Joannis
wants to merge
5
commits into
ml-explore:main
Choose a base branch
from
wendylabsinc:jo/mlxarray-view
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
518ccd8
Add MLXArrayOf<Scalar> ~Copyable view over MLXArray
Joannis ad43f11
Reuse compiled and gradient transforms
Joannis 560e08b
Reduce optimizer state and gradient norm graphs
Joannis 8271bc9
Reduce interop and array view overhead
Joannis 1dda52e
Add evaluation contention benchmarks
Joannis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| // Copyright © 2026 Apple Inc. | ||
|
|
||
| import Dispatch | ||
| import Foundation | ||
| import MLX | ||
| import MLXNN | ||
|
|
||
| private struct BenchmarkResult { | ||
| let name: String | ||
| let iterations: Int | ||
| let minMs: Double | ||
| let medianMs: Double | ||
| let meanMs: Double | ||
| } | ||
|
|
||
| private func measure( | ||
| name: String, warmup: Int = 5, iterations: Int = 100, _ body: () -> Void | ||
| ) -> BenchmarkResult { | ||
| for _ in 0 ..< warmup { body() } | ||
|
|
||
| var samples = [Double]() | ||
| samples.reserveCapacity(iterations) | ||
| for _ in 0 ..< iterations { | ||
| let start = DispatchTime.now().uptimeNanoseconds | ||
| body() | ||
| let end = DispatchTime.now().uptimeNanoseconds | ||
| samples.append(Double(end - start) / 1_000_000) | ||
| } | ||
| samples.sort() | ||
|
|
||
| return BenchmarkResult( | ||
| name: name, | ||
| iterations: iterations, | ||
| minMs: samples.first!, | ||
| medianMs: samples[samples.count / 2], | ||
| meanMs: samples.reduce(0, +) / Double(iterations)) | ||
| } | ||
|
|
||
| private func printResults(_ results: [BenchmarkResult]) { | ||
| print("Benchmark Iters Min ms Median ms Mean ms") | ||
| for result in results { | ||
| let name = result.name.padding(toLength: 44, withPad: " ", startingAt: 0) | ||
| let values = String( | ||
| format: "%6d %10.4f %10.4f %10.4f", result.iterations, result.minMs, | ||
| result.medianMs, result.meanMs) | ||
| print("\(name) \(values)") | ||
| } | ||
| } | ||
|
|
||
| private func graphConstruction() -> BenchmarkResult { | ||
| measure(name: "Graph construction (200 elementwise ops)") { | ||
| var x = MLXArray.zeros([4, 4]) | ||
| for _ in 0 ..< 200 { x = x + 1 } | ||
| precondition(x.shape == [4, 4]) | ||
| } | ||
| } | ||
|
|
||
| private func realizedEval() -> BenchmarkResult { | ||
| let x = MLXArray.ones([4, 4]) | ||
| eval(x) | ||
| return measure(name: "eval() of an already-realized array") { eval(x) } | ||
| } | ||
|
|
||
| private func mlpForward() -> [BenchmarkResult] { | ||
| let l1 = Linear(128, 256) | ||
| let l2 = Linear(256, 256) | ||
| let l3 = Linear(256, 10) | ||
| let input = MLXRandom.normal([32, 128]) | ||
|
|
||
| func forward(_ x: MLXArray) -> MLXArray { | ||
| l3(relu(l2(relu(l1(x))))) | ||
| } | ||
|
|
||
| let compiled = compile(inputs: [l1, l2, l3], forward) | ||
| let expected = forward(input) | ||
| let actual = compiled(input) | ||
| eval(expected, actual) | ||
| precondition(allClose(expected, actual).item(Bool.self)) | ||
|
|
||
| return [ | ||
| measure(name: "MLP forward + eval (uncompiled)") { eval(forward(input)) }, | ||
| measure(name: "MLP forward + eval (compiled)") { eval(compiled(input)) }, | ||
| ] | ||
| } | ||
|
|
||
| private func evalContention(device: Device) -> [BenchmarkResult] { | ||
| let workerCount = 4 | ||
| let workPerWorker = 8 | ||
| let streams = (0 ..< workerCount).map { _ in Stream(device) } | ||
|
|
||
| let runWorker: @Sendable (Int) -> Void = { worker in | ||
| let stream = StreamOrDevice.stream(streams[worker]) | ||
| var x = full([64, 64], values: Float(worker), stream: stream) | ||
| for _ in 0 ..< workPerWorker { | ||
| x = add(x, 1, stream: stream) | ||
| x = multiply(x, 1.0001, stream: stream) | ||
| } | ||
| eval(x) | ||
| } | ||
|
|
||
| let sequential = measure(name: "4 graph + eval workers (sequential)", iterations: 25) { | ||
| for worker in 0 ..< workerCount { runWorker(worker) } | ||
| } | ||
| let concurrent = measure(name: "4 graph + eval workers (concurrent)", iterations: 25) { | ||
| DispatchQueue.concurrentPerform(iterations: workerCount, execute: runWorker) | ||
| } | ||
| return [sequential, concurrent] | ||
| } | ||
|
|
||
| @main | ||
| private struct Benchmarks { | ||
| static func main() { | ||
| let requested = CommandLine.arguments.dropFirst().first | ||
| let device: Device | ||
| switch requested { | ||
| case nil: device = Device.defaultDevice() | ||
| case "gpu": device = .gpu | ||
| case "cpu": device = .cpu | ||
| default: | ||
| fatalError("usage: Benchmarks [cpu|gpu]") | ||
| } | ||
|
|
||
| Device.withDefaultDevice(device) { | ||
| var results = [graphConstruction(), realizedEval()] | ||
| results.append(contentsOf: mlpForward()) | ||
| results.append(contentsOf: evalContention(device: device)) | ||
| printResults(results) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,16 @@ extension MLXArray { | |
| ?? self.size | ||
| } | ||
|
|
||
| func copy(from: UnsafeRawBufferPointer, toContiguous output: UnsafeMutableRawBufferPointer) { | ||
| /// Copy `self`'s (possibly non-contiguous) backing, starting at `base`, into a | ||
| /// contiguous `output` buffer. | ||
| /// | ||
| /// `base` is treated as a bare starting point, not a pre-sized region: for the | ||
| /// non-contiguous case the actual reachable byte range (which can extend *before* | ||
| /// `base` when strides are negative) is computed internally and bounds-checked via | ||
| /// `RawSpan`, rather than trusting unchecked pointer arithmetic to stay in bounds. | ||
| func copy(from base: UnsafeRawPointer, toContiguous output: UnsafeMutableRawBufferPointer) { | ||
| guard !output.isEmpty else { return } | ||
|
|
||
| let contiguousDimension = self.contiguousToDimension() | ||
| let shape = self.shape | ||
| let strides = self.internalStrides | ||
|
|
@@ -69,22 +78,18 @@ extension MLXArray { | |
| // the index of the current source item | ||
| var index = Array.init(repeating: 0, count: ndim) | ||
|
|
||
| // output pointer | ||
| var dest = output.baseAddress! | ||
|
|
||
| while true { | ||
| // compute the source index by multiplying the index by the | ||
| // stride for each dimension | ||
|
|
||
| // note: in the case where the array has negative strides / offset | ||
| // the base pointer we have will have the offset already applied, | ||
| // e.g. asStrided(a, [3, 3], strides: [-3, -1], offset: 8) | ||
| // Keep this in step with the odometer below. Recomputing it from every index and | ||
| // stride made traversal O(numberOfChunks * rank). | ||
| var sourceIndex = 0 | ||
|
|
||
| let sourceIndex = zip(index, strides).reduce(0) { $0 + ($1.0 * $1.1) } | ||
| // output byte offset | ||
| var destOffset = 0 | ||
|
|
||
| // convert to byte pointer | ||
| let src = from.baseAddress! + sourceIndex * itemSize | ||
| dest.copyMemory(from: src, byteCount: destItemSize) | ||
| while true { | ||
| // offset relative to the span's own start -- always >= 0 by construction, | ||
| // since minSourceIndex is the true minimum reachable sourceIndex | ||
| let spanOffset = (sourceIndex - minSourceIndex) * itemSize | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it is maybe missing a commit -- |
||
| let chunk = source.extracting(spanOffset ..< (spanOffset + destItemSize)) | ||
|
|
||
| // next output address | ||
| dest += destItemSize | ||
|
|
@@ -99,9 +104,11 @@ extension MLXArray { | |
| } | ||
|
|
||
| index[dimension] = 0 | ||
| sourceIndex -= (shape[dimension] - 1) * strides[dimension] | ||
| } else { | ||
| // just increment the dimension and we are done | ||
| index[dimension] += 1 | ||
| sourceIndex += strides[dimension] | ||
| break | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
build issue here -- missing a comma on 363