@@ -517,40 +517,82 @@ version is a future follow-on.
517517enabled and pass conformance; benchmark shows a measurable speedup
518518over the M9 baseline (target ≥1.5× on M3 hardware).
519519
520- ### M12 — Zero-copy binary round-trip
520+ ### M12 — Zero-copy binary round-trip ( ` to_binary ` )
521521
522522PLAN design decision #9 claims unified-memory zero-copy for
523523` from_binary ` / ` to_binary ` . The current code memcpys unconditionally
524- (` emily_nif.cpp:57-58 ` , ` :74-84 ` ). M12 delivers the claim.
524+ (` emily_nif.cpp:57-58 ` , ` :74-84 ` ). M12 delivers the claim for
525+ ` to_binary ` ; ` from_binary ` is deferred to M12.5 because MLX's Metal
526+ ` allocator::Buffer ` stores an ` MTL::Buffer* ` , not a raw CPU pointer,
527+ so wrapping a BEAM heap pointer is unsound without routing through
528+ ` MTL::Device::newBufferWithBytesNoCopy ` .
525529
526530- ** ` to_binary ` ** : wrap the materialized MLX buffer pointer as a BEAM
527531 resource binary via ` enif_make_resource_binary ` , with the resource
528532 retaining a refcount on the MLX array so the buffer survives until
529533 the BEAM binary is GC'd. No copy; the BEAM binary aliases MLX
530534 storage directly.
531- - ** ` from_binary ` ** : when the input binary is heap-resident and
532- page-aligned (and the MLX array would otherwise live on the unified
533- arena), construct the array with a deleter that releases the BEAM
534- binary refcount instead of freeing. Fall back to the current memcpy
535- path when alignment doesn't hold or when the source is a sub-binary.
535+ - ** ` from_binary ` ** : deferred. See M12.5.
536536- ** Stride-aware materialize** : ` to_binary ` currently routes through
537537 ` mx::contiguous ` ; for already-contiguous arrays this is a no-op, but
538538 the wrap-as-resource path needs an explicit guard since aliasing a
539539 non-contiguous buffer would lie about its layout.
540540
541541** Testing** :
542- - Allocate a 256 MB tensor, round-trip through ` to_binary ` then
543- ` from_binary ` , assert MLX active memory grew by ~ 256 MB not ~ 512 MB .
544- - Soak: repeated round-trip with cache-clear, assert peak memory is
545- bounded by the working-set size, not 2× it .
542+ - Allocate a tensor, call ` to_binary ` , assert MLX active memory did
543+ not grow (aliasing, not copying) .
544+ - Soak: repeated ` to_binary ` with cache-clear, assert peak memory is
545+ bounded by the working-set size.
546546- Correctness: the M2 property suite must still pass — this is a perf
547547 change, not a semantics change.
548- - Refcount safety: drop the MLX-side reference, then read the BEAM
549- binary; must not segfault. Use-after-free is the failure mode, so
550- this milestone gates on an AddressSanitizer build in CI.
548+ - Refcount safety: drop the original tensor reference, then read the
549+ BEAM binary returned by ` to_binary ` ; must not segfault. Use-after-
550+ free is the failure mode, so this milestone gates on an
551+ AddressSanitizer build in CI.
552+
553+ ** Exit:** ` to_binary ` zero-copy verified by allocator stats; M2
554+ property suite green; lifecycle and soak tests verify refcount
555+ safety. AddressSanitizer CI deferred (macOS SIP prevents
556+ ` DYLD_INSERT_LIBRARIES ` propagation through ` /bin/sh ` -launched BEAM;
557+ requires a custom ` --enable-sanitizers=address ` OTP build).
558+ ` EMILY_ASAN=1 ` Makefile flag ships for users with sanitizer-enabled
559+ OTP.
560+
561+ ### M12.5 — ` from_binary ` zero-copy via MTL no-copy buffer
562+
563+ Deferred half of M12. The BEAM → MLX direction can't be done by
564+ wrapping a BEAM pointer as an ` mx::allocator::Buffer ` — Metal's
565+ allocator stores ` MTL::Buffer* ` , not a raw CPU pointer, so a wrapped
566+ heap pointer would be dereferenced as an ` MTL::Buffer* ` and crash on
567+ GPU dispatch. True zero-copy requires registering the BEAM memory
568+ with Metal.
569+
570+ - ** NIF changes** : accept ` fine::Term ` instead of ` ErlNifBinary ` so
571+ we can ` enif_make_copy ` the term into a persistent ` ErlNifEnv ` and
572+ keep the refc binary alive. Call
573+ ` MTL::Device::newBufferWithBytesNoCopy:length:options:deallocator: `
574+ to hand the BEAM pointer to Metal; the deallocator block calls
575+ ` enif_free_env ` . Wrap the resulting ` MTL::Buffer* ` as an
576+ ` allocator::Buffer ` and pass to the 4-arg ` mx::array ` constructor.
577+ - ** MLX integration** : register the MTL::Buffer with MLX's
578+ residency set so command buffers keep it resident. The residency
579+ API is not in public headers — either upstream a public entry
580+ point or bypass via implementation-detail APIs.
581+ - ** Build** : link the Metal framework from the NIF; add metal-cpp
582+ headers to the build.
583+ - ** Pre-conditions** : heap-resident refc binary, page-aligned,
584+ page-sized. Fall back to the M12 memcpy path otherwise.
551585
552- ** Exit:** zero-copy verified by allocator stats; M2 property suite
553- green; AddressSanitizer build clean.
586+ ** Testing** :
587+ - Allocate a 256 MB page-aligned binary, ` from_binary ` →
588+ ` to_binary ` , assert MLX active memory grew by ~ 256 MB not ~ 512 MB.
589+ - Weight-loading soak: simulate Bumblebee's mmap'd-weights path with
590+ realistic tensor counts, assert peak memory matches the
591+ theoretical working-set lower bound.
592+ - Refcount safety under ASan, same pattern as M12.
593+
594+ ** Exit:** ` from_binary ` zero-copy verified by allocator stats for
595+ page-aligned inputs; PLAN decision #9 claim fully delivered.
554596
555597### M13 — EXLA gradient conformance
556598
0 commit comments