Skip to content

Add an opt-in Metal array storage preference - #405

Open
aurascoper wants to merge 6 commits into
JuliaGPU:mainfrom
aurascoper:metal-array-storage-kwarg
Open

Add an opt-in Metal array storage preference#405
aurascoper wants to merge 6 commits into
JuliaGPU:mainfrom
aurascoper:metal-array-storage-kwarg

Conversation

@aurascoper

@aurascoper aurascoper commented Aug 6, 2026

Copy link
Copy Markdown

Summary

  • store backend-specific set_backend(...; kw...) options in a generic backend-keyed preference map
  • let MetalExt consume and validate the storage option it owns
  • keep the portable allocation call as JACC.array(x)
  • preserve existing MtlArray identity for implicit and explicit Metal calls

With Metal's default settings, host-array copies use private storage. Projects can opt into shared storage with JACC.set_backend("Metal"; storage = :shared).

Motivation

This implements the storage-selection contribution discussed in #402 and the generic preference design requested in review.

Metal's MtlArray(x) construction path allocates and copies the source array (Metal.jl array.jl:287-296), with private storage selected by default (array.jl:193-205). On Apple Silicon, shared storage can avoid the private-storage transfer path.

Measurement

On an Apple M4 with Julia 1.12.6 and Metal.jl 1.10.0, constructing 10,000,000 Float32 elements produced these medians across 30 samples per path:

  • private: 9,036.0 microseconds
  • shared: 4,342.0 microseconds
  • :shared was 52% lower than :private

Each construction was followed by Metal.synchronize(). Laptop idle state was uncontrolled. GPU bandwidth for compute-bound workloads remains unmeasured.

Design

Core JACC persists backend options verbatim, and MetalExt owns storage interpretation and validation, so the portable JACC.array(x) signature stays backend-independent. Other preference lifecycle operations continue to use the existing Preferences.jl path.

The Metal specialization preserves the pre-PR identity behavior for existing MtlArray values and restores the explicit JACC.array(backend, x) host-copy method. MetalExt's fully parameterized MtlArray constructors keep JACC's selected storage mode authoritative over Metal.jl's independent default.

Verification

  • Pkg.test(): 223 passed on Apple Silicon with Julia 1.12.6 and Metal.jl 1.10.0
  • focused array_storage_preference: 16 passed
  • coverage includes private and shared host copies, implicit and explicit identity, explicit host conversion, generic persisted state, and invalid Metal values
  • git diff --check: passed

Reference: #402

aurascoper and others added 3 commits August 5, 2026 23:02
JACC.array(x; storage = :shared) on the Metal backend copies host data
into unified SharedStorage memory instead of the default PrivateStorage,
dropping the private-staging blit on Apple Silicon. storage = nothing
(the default) preserves current behavior exactly on every backend; other
backends raise a clear ArgumentError for a non-nothing value.

Implements the offer in JuliaGPU#402.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aurascoper aurascoper changed the title Add opt-in storage-mode keyword to JACC.array (Metal unified memory) Add an opt-in Metal array storage preference Aug 6, 2026
Comment thread src/preferences.jl
@aurascoper

Copy link
Copy Markdown
Author

Addressed the generic backend preference design and narrowed the follow-up to Metal allocation behavior.

Core JACC now stores backend options in a backend-keyed map, and MetalExt reads and validates storage. The latest changes preserve existing MtlArray identity, restore the explicit Metal host-copy method, and make the selected storage mode authoritative over Metal.jl's independent default.

Full suite: 223 passed on Apple Silicon with Julia 1.12.6 and Metal.jl 1.10.0. Focused array_storage_preference: 16 passed.

Could you take another look?

@williamfgc

Copy link
Copy Markdown
Collaborator

@aurascoper thanks for the contribution! @PhilipFackler let me know if this is ready so I can run CI

@williamfgc

Copy link
Copy Markdown
Collaborator

Test this please

Comment thread ext/MetalExt/MetalExt.jl
end

function JACC._array(::MetalBackend, x::AbstractArray)
if _array_storage() == "shared"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should probably be run just once during module load. Can we store this as a module global?

@PhilipFackler
PhilipFackler self-requested a review August 7, 2026 18:44

@PhilipFackler PhilipFackler left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this is good to go. Thanks @aurascoper !

@PhilipFackler

Copy link
Copy Markdown
Collaborator

@williamfgc looks like cousteau has an issue with the hip library. We can probably ignore that for this PR.

Closes PhilipFackler's unresolved review thread on
ext/MetalExt/MetalExt.jl: _array_storage() was re-reading the backend
preference Dict and revalidating it on every JACC._array call, on the
same array-allocation hot path this PR already measures overhead on.

A plain module-load-once cache is not safe here: set_backend is
documented (and tested by array_storage_preference in
test/backend/metal.jl) to apply storage changes live within the same
Julia session, without a restart. That test calls set_backend three
times mid-session, including two invalid-value cases that must throw
on the very next JACC.array call.

Instead, _EXT_PREFS_GENERATION (Ref{Int}, core JACC) is bumped on
both real mutation sites of _EXT_PREFS (_set_extension_preferences,
unset_backend's empty!) — grepped exhaustively, no other write site
exists. MetalExt's _array_storage() caches (generation, value) and
only recomputes on a generation mismatch, so the hot path is a cheap
Int comparison instead of a Dict lookup plus revalidation, while the
live-reload contract the existing test locks in is unchanged.

Verified on Apple Silicon (Julia 1.12.6, Metal.jl 1.10.0):
array_storage_preference 16/16, full suite 223/223 — matches the
pre-fix baseline exactly, no regressions.
@aurascoper

Copy link
Copy Markdown
Author

Addressed the module-global caching note on _array_storage() (e6da682).

A plain load-once cache wasn't safe as-is: array_storage_preference in test/backend/metal.jl calls set_backend three times mid-session (including two invalid-value cases that must throw on the very next JACC.array call), so the value has to stay live-reloadable within a session, not fixed at module load.

Used a generation counter instead: Preferences.Backend._EXT_PREFS_GENERATION (a plain Ref{Int}) is bumped on both real mutation sites of _EXT_PREFS, and _array_storage() caches (generation, value), only recomputing on a mismatch. Hot path is now a single Int comparison instead of a Dict lookup plus revalidation on every allocation, and the live-reload behavior the existing test locks in is unchanged.

Verified on Apple Silicon (Julia 1.12.6, Metal.jl 1.10.0): array_storage_preference 16/16, full suite 223/223 — matches the prior baseline exactly.

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