@@ -26,7 +26,9 @@ checklist so future-us understands the trade-offs.
2626 scope from M9; mixed-precision master weights: in scope from M16.)
2727- Drop-in replacement for EMLX. We borrow where it's clearly right, but
2828 we're not constrained by its API.
29- - ` Emily.Stream ` as a public API — MLX streams stay internal in v1.
29+ - ` Emily.Stream ` beyond the narrow ` with_stream/2 ` + ` new/1 ` +
30+ ` synchronize/1 ` surface M14 introduces for the documented
31+ "big model, multi-process serving" pattern.
3032
3133## Architecture
3234
@@ -635,7 +637,7 @@ checked in alongside the goldens.
635637 macOS (CPU) or Linux+CUDA. Emits a complete ` ExlaGoldenData ` module:
636638 ` elixir bench/exla_golden_gen.exs ` .
637639
638- ### M14 — Serving concurrency cookbook + pooled-serving helper
640+ ### M14 — Serving concurrency: stream-per-process + cookbook
639641
640642` Emily.Compiler.__partitions_options__/1 ` raises on
641643` max_concurrency > 1 ` — correct (Metal isn't safe for concurrent
@@ -644,36 +646,33 @@ single Emily-backed `Nx.Serving` cannot scale past one concurrent
644646request. Production users will hit this in week one. M14 stops being
645647silent about it and ships a tested pattern.
646648
647- - ** ` Emily.Serving.start_pool/2 ` ** : helper that starts N
648- ` Nx.Serving ` instances behind a pool (poolboy or a hand-rolled
649- Registry round-robin; pick the lighter dep). Each Serving runs in
650- its own worker, dispatching requests in parallel across pool
651- members rather than within one member.
652- - ** Documented pattern** : "for K concurrent inference requests, start
653- K servings". Trade-off: each pool member loads its own weights —
654- fine for small models, painful for Qwen3-7B+.
655- - ** Alternative for large models** : stream-per-process via MLX's
656- ` mx::scheduler::new_stream ` . Expose ` Native.set_default_stream/1 `
657- and ` Emily.Stream.with_stream/2 ` ; multi-process serving with one
658- shared model + per-process MLX streams becomes the documented
659- "big model" path. (Promotes streams from internal-only — see
649+ - ** Stream-per-process** : the primary deliverable. Expose
650+ ` Native.set_default_stream/1 ` and ` Emily.Stream.with_stream/2 `
651+ via MLX's ` mx::scheduler::new_stream ` . Each process gets its own
652+ Metal command queue; one shared model, per-process streams, no
653+ weight duplication. (Promotes streams from internal-only — see
660654 Project Decisions — to a narrowly-scoped public surface.)
655+ - ** Cookbook: pooled servings** : documented pattern — "for K
656+ concurrent inference requests, start K ` Nx.Serving ` instances
657+ behind your own pool (poolboy, Registry round-robin, etc.)".
658+ No library code; clients bring their own pool since Emily already
659+ behaves correctly under that model. Trade-off: each pool member
660+ loads its own weights — fine for small models, painful for
661+ Qwen3-7B+.
661662- ** README + moduledoc updates** : surface the limitation and both
662663 patterns prominently. Today neither is mentioned outside a buried
663664 comment in ` Emily.Compiler ` .
664665
665666** Testing** :
666- - Pool helper test: K servings, K parallel requests, assert wall-clock
667- is ~ K× faster than serial.
668667- Stream test: two processes, two streams, same model loaded once;
669668 no SIGSEGV under sustained parallel load
670669 (` test/soak/backend_concurrency_test.exs ` documents the SIGSEGV
671670 story for the unstreamed case — this is the negative control).
672- - ` :serving_full ` opt-in: end-to-end Qwen3 pool plus per-stream
673- large-model pattern.
671+ - ` :serving_full ` opt-in: end-to-end per-stream large-model
672+ pattern.
674673
675- ** Exit:** both patterns documented; pool helper shipped; concurrency
676- soak demonstrates the streamed path is stable.
674+ ** Exit:** both patterns documented; concurrency soak demonstrates
675+ the streamed path is stable.
677676
678677### M15 — Native linalg
679678
0 commit comments