Chapter 3 — Unified memory and the buffer substrate
status: polished · path: Muse Glimmer, pinned Muser tree
Prerequisites: Ch 2. You know what an
MTLDevice, a command buffer, an encoder, andset_buffer(slot, buffer, offset)are. This chapter assumes you know nothing about memory architecture.
3.1 The big picture: one DRAM, two brains
Chapter 2 closed on set_buffer(slot, buffer, offset) and deliberately
deferred the memory story — what an MTLBuffer is on this machine, and why
one buffer can hold the whole 16.76 GB model. That deferral leaves a
question standing, and it is the question this chapter answers: where do
the weights actually live while the GPU is reading them, and what does it
cost to put them there? On this machine the answer is short enough to be
surprising, and it starts from the single most important fact about Apple
Silicon for inference.
On a discrete GPU — an NVIDIA card in a desktop — the CPU and the GPU do
not share memory. The CPU sits next to one pool of memory chips
(CPU DRAM, what people call RAM), the GPU sits
next to a separate pool on the card (VRAM),
and the two pools are wired together by a bus (PCIe).
Every time the CPU wants the GPU to see a weight tensor, it must memcpy
the bytes across that bus. A 16.76 GB model means shipping 16.76 GB over
PCIe before the first token can even be thought about.
Apple Silicon is built differently. The CPU cores and the GPU are on the same piece of silicon (a system-on-chip, SoC), and they both point at the same physical pool of DRAM. There is no second pool and no bus to copy across. This single-pool design is called unified memory; Figure 3.1 draws the contrast with the discrete world.
APPLE SILICON (unified memory) DISCRETE GPU (e.g. NVIDIA)
┌─────────────────────────────┐ ┌───────────┐ PCIe ┌───────────┐
│ SoC │ │ CPU │◄════════►│ GPU │
│ ┌──────┐ ┌──────┐ │ │ ┌─────┐ │ bus + │ ┌─────┐ │
│ │ CPU │ │ GPU │ │ │ │ DRAM │ │ memcpy()│ │ VRAM │ │
│ │cores │ │cores │ │ │ └─────┘ │ │ └─────┘ │
│ └──┬───┘ └───┬──┘ │ └───────────┘ └───────────┘
│ │ │ │ two physical DRAM pools;
│ └───────┬────────┘ │ you memcpy weights
│ ┌────┴─────┐ │ across the bus
│ │ one DRAM │ │
│ │ pool │ │
│ │ (96 GB) │ │
│ └──────────┘ │
└─────────────────────────────┘
one physical DRAM; CPU and GPU
read/write the SAME bytes
Figure 3.1: Unified memory (Apple Silicon) vs. the discrete split. On Apple Silicon “uploading weights to the GPU” is a no-op — the bytes are already there. (This diagram is also the seed of Part VI: the remote GB10 node is a discrete-memory machine across a 10GbE wire, which is why the disaggregated lane moves KV tiles, not work-in-progress.)
For inference the consequence is transformative: a model file on disk can be mapped into the process’s address space once, and — because the GPU shares that address space — the GPU can read the weights directly from those pages. There is no second copy. §3.6 shows the exact calls; first, what Metal calls its buffers.
3.2 The three Metal storage modes, and the one Muser uses
Unified memory settles where bytes can live. It does not tell Metal what you intend to do with them, and Metal insists on being told: every allocation carries a declaration. So the question for this section is which declaration Muser makes — for every buffer in the engine, without exception — and why a faster-looking alternative was tried and then handed back.
When you ask the MTLDevice for a buffer, you must tell it a
storage mode — where the bytes physically
live and who can see them. Metal defines three [Metal-PG, "Resource Objects: Storage Modes"]:
StorageModeShared— one copy of the bytes, in unified memory. CPU and GPU read and write the same physical pages; a CPU write is instantly visible to the GPU. This is the Apple Silicon mode.StorageModePrivate— bytes only the GPU can touch; the CPU must stage data through a Shared buffer and a GPU blit copy (a block copy run on the GPU through a dedicated blit encoder, as opposed to the compute encoder that runs kernels). This is how discrete VRAM works.StorageModeManaged— two caches, one per side, explicitly synchronized withsynchronize/didModifyRangecalls. It exists for older discrete-Mac setups.
On Apple Silicon, Private and Managed are simply irrelevant: there is no
separate VRAM to be private to and no two caches to manage. Muser uses
StorageModeShared for every buffer it creates, through one function:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/buffer.rs:7
fn shared_tracked() -> MTLResourceOptions {
// Several accepted Muse paths still cross compute encoders (notably
// target-hidden prefill/capture). Untracked resources are only valid when
// every such dependency has an explicit fence/barrier. b9678d4 enabled
// untracked mode globally before that contract existed and empirically
// changed DFlash conditioning while leaving final greedy IDs unchanged.
MTLResourceOptions::StorageModeShared
}
}
A function that returns one enum constant should not need a paragraph of
commentary above it. This one does, because the interesting part is what
the return value omits: it is just StorageModeShared, with no
HazardTrackingModeUntracked flag beside it. The comment is a scar, and
the story behind it is worth telling in full, because the failure it
records is one this book will keep meeting: a change that produces
correct-looking output and is wrong anyway.
Lineage — the untracked-hazards gamble, and why we declined it. Metal can optionally mark buffers untracked (
HazardTrackingModeUntracked), which tells the driver to skip its automatic dependency tracking between dispatches. Bookkeeping you skip is work you do not pay for, and we had reason to expect a win: the ancestor Ferrite engine ran untracked and made it safe with its own conflict-driven barrier planner, measured there at a real but modest win[ferrite-book Ch 3](Ferrite-lineage numbers, ~2–4 % on that lab’s A18-class hardware). So we took the fork. Commitb9678d4flipped the switch engine-wide — and, in the comment’s own words, did it “before that contract existed”: before every cross-encoder dependency in Muser had an explicit fence standing in for the tracker we had just turned off.What we expected was a small speedup and byte-identical output. What we got was identical output and different internal state. The build empirically changed DFlash [speculative-draft] conditioning while leaving final greedy IDs unchanged (
[crates/muser-engine/src/metal/buffer.rs:7-14]). Read that twice. The tokens matched — so every greedy-output test we had would have gone green. What moved was the speculative draft’s conditioning, which is watched on its own precisely so that a change like this has somewhere to show up.A silent conditioning change with correct-looking output is exactly the failure this engine exists to prevent, so the switch went back. Muser runs Metal’s default tracked storage plus the explicit
memoryBarrierWithScopegroups you met in Ch 2 §2.9. The general rule survives from the ancestor book, and it is worth stating in the abstract because it recurs for the rest of the engine: turning off a safety net is safe if and only if something else provably enforces the ordering. Ferrite had that something — a barrier planner. Muser, at that commit, did not, and the cheaper provable thing was to keep the net on.
3.3 The buffer substrate: three types, one view
Unified memory decides where bytes live; it says nothing about what they mean. An engine that keeps f32 activations, f16 KV planes and immutable quantized weights in a single address space needs some way of stopping itself from confusing them — and it has to do that without a runtime type tag on the hot path, because the hot path runs per token. That is the job of the buffer module, and it is small enough to read in one sitting. Three concrete buffer types and one view type carry the whole engine:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/buffer.rs:28
#[derive(Clone)]
pub struct GpuBuffer {
inner: Buffer,
len: usize,
}
/// Shared Metal storage whose logical elements are IEEE-754 binary16 bits.
///
/// Keeping this distinct from [`GpuBuffer`] prevents an F16 KV plane from
/// being accidentally fingerprinted or indexed as an F32 activation buffer.
#[derive(Clone)]
pub struct GpuHalfBuffer {
inner: Buffer,
len: usize,
}
#[derive(Clone)]
pub struct GpuBytes {
inner: Buffer,
len: usize,
_mmap: Option<std::sync::Arc<memmap2::Mmap>>,
}
#[derive(Clone, Copy)]
pub struct GpuByteView<'a> {
buffer: &'a GpuBytes,
offset: usize,
len: usize,
}
}
Walk them:
GpuBuffer— f32 scratch: the residual stream, activations, logits.lencounts floats. CPU access goes through checkedas_slice/as_mut_sliceviews over the shared storage (buffer.rs:216-232).GpuHalfBuffer— f16 storage (binary16, the 16-bit “half” float; Ch 5 covers its bit layout). The doc comment says why it is a separate type and not a flag: keeping it distinct “prevents an F16 KV plane from being accidentally fingerprinted or indexed as an F32 activation buffer” (buffer.rs:34-37). A KV plane is one layer’s key/value cache buffer pair; every Metal KV plane is f16 on all lanes, and Ch 15 explains the layout.GpuBytes— raw bytes, and the only type that can carry an mmap._mmapholds the file mapping alive; the leading underscore suppresses a lint for a field that exists for its lifetime, not its value — drop the lastGpuBytes, theArccount hits zero, the mapping is released.GpuByteView— a borrowed slice of aGpuBytes:(buffer, offset, len), created by the checkedview(offset, len)method (buffer.rs:153-160, which refuses a view that would run past the end). This is the weight-tensor handle the kernels of Part IV receive.
If you know the ancestor Ferrite book: its GpuBuffer was a six-field
struct with a dtype, a view length, and an arena offset. Muser’s substrate
is deliberately flatter — three typed wrappers instead of one typed field,
and views only where views are needed (byte storage). It can afford to be:
the engine serves one model, so the dispatch code already knows every
buffer’s type statically, and a dtype field would only be describing at
runtime what the compiler could have enforced. That simplification was made
on purpose when the substrate was pulled across from the ancestor, and the
crate documentation records the “pull-and-simplify” provenance
[crates/muser-engine/src/lib.rs:163-171].
3.4 Zero-init is policy, not luck
What is in a buffer at the moment you receive it, and who is allowed to read it before anyone has written it? That sounds like a pedant’s question until you notice what the wrong answer looks like in an inference engine: not a crash, but a plausible token. Garbage that happens to be zero is indistinguishable from a real value, and a stale row in a KV cache is a sentence the model half-remembers from someone else’s conversation.
Metal’s new_buffer does not zero the memory it hands you; contents
are undefined. Inference is full of buffers that are partially written
and then fully read (reductions with guard lanes; ring buffers whose
tail rows are not yet meaningful), so Muser makes initialization explicit:
zerosallocates and CPU-memsets — the default for everything (GpuBytes::zerosatbuffer.rs:59-73,GpuBuffer::zerosat:182-196,GpuHalfBuffer::zerosat:238-244). The cost is paid once at allocation, never on the hot path.uninitializedexists onGpuHalfBufferonly, and its doc comment is a contract, not an invitation:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/buffer.rs:246
/// Allocate without CPU-touching the backing bytes.
///
/// This is for multi-gigabyte KV planes ONLY. Their ring/logical
/// metadata (`MetalKvPlane::origin_logical`/`origin_physical`/`len`)
/// guarantees every row is written by a `store_kv_*` dispatch before any
/// row within `[origin, origin + len)` is ever read back, so the CPU
/// zero-fill `zeros()` performs is pure startup-time cost on an
/// allocation that can be many gigabytes. Every other caller must keep
/// using `zeros()` -- this path leaves stale bytes behind in release
/// builds.
///
/// In debug/test builds the bytes are poisoned (never plain zero)
/// instead of left stale, so a bug that lets a read reach a
/// not-yet-written row is conspicuous rather than silently reading
/// zeros; see `kv_uninitialized_write_then_read_round_trips` below.
pub fn uninitialized(context: &MetalContext, len: usize) -> Result<Self, MetalError> {
}
That last paragraph is the elegant part, and it is the answer to the
question this section opened with. In debug builds the unwritten rows are
filled with 0xDEAD (buffer.rs:270-275), so a contract violation reads
back conspicuously poisoned values instead of plausible zeros. Put the
other way round: the fast path is allowed to leave stale bytes behind
precisely because the slow path refuses to leave believable ones. A bug
that lets a read reach a not-yet-written row cannot hide as a slightly odd
number — it is loud, in exactly the builds where somebody is looking.
Where each path is used, precisely:
- Live session KV planes zero-fill by design. The session constructor
allocates through
MetalKvPlane::new→GpuHalfBuffer::zeros([crates/muser-engine/src/decode.rs:229-244]), with an explicit in-source justification atdecode.rs:1350-1352: “Zero-filled on purpose: wrapped SWA rows must never expose uninitialized storage during a sequence boundary transition.” - Detached remote-install generations use
uninitialized. When a kvpack tile arrives from the GB10 producer, a fresh plane is built withMetalKvPlane::uninitializedand every retained row is uploaded before the plane goes live ([crates/muser-engine/src/decode.rs:1862-1888]) — a bulk-write-then-publish pattern where the write-before-read guarantee is structural.
One correction while we are here, because we nearly walked into it
ourselves. Writing this section we started where you would start — from the
engineering doc — and docs/memory-footprint.md says flatly that “Metal KV
buffers are allocated without a CPU memset.” Taken at face value that
inverts everything above: it makes the fast path the default and the
zero-fill the exception, and this section would have been a footnote.
The two constructors above say otherwise, and an audit had already caught
the same discrepancy. The doc sentence is wrong for live planes —
those zero-fill, as above — and right only about the detached
remote-install generations. We kept the receipt for the correction
[docs/kvpack-merge-handoff.md §3 D2, the 2026-08-20 audit]. The book
inherits the fix, not the error; the general lesson is that source outranks
prose about source, including prose written by the same people.
3.5 Page alignment: the 16 KB contract
Before the mmap story, one piece of arithmetic — the kind that works for
years and then doesn’t. The question is narrow, and it is the one to ask
whenever you hand a driver a pointer to memory you did not allocate: what
exactly are you promising it about that memory? Apple Silicon uses a
16 KB virtual-memory page (not the 4 KB you may know from x86). The
Metal call that wraps external memory — new_buffer_with_bytes_no_copy —
requires a page-aligned pointer and a page-aligned length. A file’s byte
length is not, in general, a multiple of 16,384.
Muser handles this by rounding the Metal-facing length up to the page boundary while keeping the logical length exact — and the code is unusually careful about why that is safe:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/buffer.rs:91
pub fn from_mmap(
context: &MetalContext,
mmap: std::sync::Arc<memmap2::Mmap>,
) -> Result<Self, MetalError> {
// Metal documents that `newBufferWithBytesNoCopy:length:options:
// deallocator:` requires a page-aligned length (the pointer is
// already page-aligned -- POSIX `mmap` always returns one). The raw
// file length has "worked" here only because `mmap` itself reserves
// whole pages under the hood and zero-fills the tail of the last
// one -- that's an undocumented tolerance, not a guarantee, so round
// the length Metal sees up to the page boundary explicitly. Bytes
// between the real file length and that boundary are the kernel's
// own zero-filled mmap tail, so this never reads outside the
// mapping. `GpuBytes::len()` keeps reporting the exact, unrounded
// file length; only the Metal-facing allocation grows.
let mmap_len = mmap.len();
let rounded_len = if mmap_len == 0 {
0
} else {
let page = page_size();
mmap_len
.checked_add(page - 1)
.map(|padded| padded / page * page)
.ok_or(MetalError::Allocation(mmap_len))?
};
let inner = context.device.new_buffer_with_bytes_no_copy(
mmap.as_ptr() as *const std::ffi::c_void,
rounded_len as u64,
shared_tracked(),
None,
);
// … (allocation check elided: see buffer.rs:122-124) …
Ok(Self {
inner,
len: mmap_len,
_mmap: Some(mmap),
})
}
}
(the allocation-size check at buffer.rs:122-124 is elided; everything
else is verbatim.)
That paragraph of comment is doing real epistemic work, and it is worth
slowing down for, because the bug it fixes had never once fired. Passing
the raw file length straight to Metal worked. It worked because mmap
reserves whole pages under the hood and zero-fills the tail of the last
one, so the bytes past the file’s end were always mapped and always zero —
and no test we could write would tell that apart from being right. The comment
separates the two anyway: what Metal guarantees (a page-aligned length is
required) from what merely happened to work (“an undocumented tolerance,
not a guarantee”). Then the code goes and obeys the documented contract.
Nothing was failing when that change was made; the point is that the
alternative was a load path resting on an OS behaviour nobody ever promised
us, and a future macOS is under no obligation to keep providing it.
Two details finish the section. A unit test pins the rounding down
(from_mmap_rounds_metal_length_up_to_the_page_boundary,
buffer.rs:347-376): a 10-byte file yields len() == 10 and a
Metal-facing length that is a page multiple — the logical length stays
exact, only the allocation grows. And the page size itself comes
from POSIX getpagesize() via a one-line extern "C" — avoiding a libc
dependency for one constant (buffer.rs:16-26).
Why does the pointer never need rounding here, where the ancestor engine had to round tensor offsets down to page boundaries? Because of what gets wrapped — the next section.
3.6 Zero-copy at 16.76 GB scale: mmap → one buffer → offset views
This is the payoff, end to end — the section where “the CPU and the GPU share memory” stops being an architecture diagram and becomes a load path that copies nothing. Hold the question a discrete-GPU engineer would ask first while you read it: when do the weights get uploaded? The answer is that there is no upload, and three code locations are enough to show why.
1. The engine mmaps the whole GGUF once. Loading does not read the weights into RAM; it maps the file and records where each tensor lives:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/weights.rs:171
pub fn open(path: &Path, gguf: &GgufFile) -> Result<Self, MuseConfigError> {
let file = File::open(path)
.map_err(|e| MuseConfigError::Geometry(format!("open {}: {e}", path.display())))?;
// SAFETY: the checkpoint is a read-only immutable input for the
// lifetime of this process; we never write through the mapping.
let mmap = unsafe { Mmap::map(&file) }
.map_err(|e| MuseConfigError::Geometry(format!("mmap {}: {e}", path.display())))?;
let mut index = HashMap::with_capacity(gguf.tensors.len());
for t in &gguf.tensors {
let start = (gguf.data_offset + t.offset) as usize;
let n_elem: usize = t.shape.iter().product::<u64>() as usize;
let be = t.dtype.block_elements();
let len = n_elem.div_ceil(be) * t.dtype.block_size();
if start + len > mmap.len() {
return Err(MuseConfigError::Geometry(format!(
"tensor {} runs past end of file ({} + {} > {})",
t.name,
start,
len,
mmap.len()
)));
}
index.insert(t.name.clone(), (start, len, t.dtype, t.shape.clone()));
}
}
The index maps every tensor name to (file_offset, byte_len, dtype, shape) — with a fail-closed bounds check per tensor. The CPU reference
path reads weights straight out of this mapping via TensorView
(weights.rs:40-48: raw bytes plus geometry, “weights are never
materialized as f32” per the module doc).
2. The Metal driver wraps that mapping in one MTLBuffer. The decode
path takes the same Arc<Mmap> and hands it to GpuBytes::from_mmap from
§3.5 — the entire 16.76 GB file becomes one Metal buffer, zero bytes
copied:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/decode.rs:1199
let context = MetalContext::new()?;
let kernels = MetalKernels::new(&context)?;
let mapped_weights = GpuBytes::from_mmap(&context, weights.mapped_file())?;
let residency_set = crate::metal::residency::create_and_attach(
&context.device,
&context.queue,
&[mapped_weights.metal()],
);
}
3. Every weight tensor is a checked offset view of that one buffer.
Each projection remembers only its GGUF layout (TensorLayout, parsed at
load, weights.rs:350-373); at dispatch time it asks the arena for a
slice:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/decode.rs:148
fn view<'a>(&self, mapped: &'a GpuBytes) -> GpuByteView<'a> {
mapped
.view(self.layout.file_offset, self.layout.byte_len)
.unwrap_or_else(|| panic!("validated GGUF tensor {} left mapped file", self.name))
}
}
And the bind — the line Ch 2 §2.8 promised to explain — hands the GPU “which buffer” and “where in it” in a single call:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/encode/multicol.rs:192
encoder.set_buffer(0, Some(weights.metal()), weights.offset() as u64);
}
Put the three together and the discrete-GPU question dissolves rather than gets answered. There is no upload step because there is nowhere to upload to: the file’s pages, the Metal buffer, and the bytes a kernel dereferences are one region of physical memory wearing three names. The figure below is the entire load path — a mapping, a wrapper around it, and arithmetic.
GGUF file on disk (16,756,681,056 B)
┌──────────────────────────────────────────────────────────────────────┐
│ [header] blk.0.attn_q blk.0.attn_k … blk.51.ffn_down output (LM) │
└───────────────────────────────┬──────────────────────────────────────┘
│ mmap() (one Arc<Mmap>, read-only)
▼
unified memory: ONE MTLBuffer over the whole mapping (no copy, ever)
┌──────────────────────────────────────────────────────────────────────┐
│ file start +q_off +k_off … file end │
│ ├ q.weight ──┤ ├ k.weight ─┤ ………………………………………… ├ output.weight ──┤│
└─▲──────────────▲──────────────────────────────────────────▲─────────┘
│ │ │
GpuByteView{ GpuByteView{ GpuByteView{
offset:q_off, offset:k_off, offset:out_off,
len:q_len, len:k_len, len:out_len,
buffer:arena} buffer:arena} buffer:arena}
└─── all views share the SAME MTLBuffer; set_buffer passes the offset ─┘
Figure 3.2: One mmap → one MTLBuffer → one checked offset view per tensor
(offsets symbolic; each is the tensor’s TensorLayout.file_offset from the
GGUF index). No weight byte is copied at load or at dispatch; pages are
demand-fetched by the OS the first time the GPU reads them.
Why one giant buffer plus views, instead of one MTLBuffer per tensor
(hundreds of them)? Each distinct buffer costs Metal bookkeeping, a
virtual-memory mapping, and TLB pressure (the TLB —
translation-lookaside buffer — is the small cache of virtual→physical page
translations; each buffer consumes entries). With one arena there is
exactly one mapping, and the per-tensor “allocation” is a 24-byte struct.
The ancestor book demonstrated the same trade on its engine’s arena
[ferrite-book Ch 3]; Muser’s version is the same idea expressed through
GpuByteView over GpuBytes. We are not alone in the choice: llama.cpp’s
Metal backend maps the whole file and slices it the same way. That
corroboration reaches us second-hand, through the ancestor book’s audit of
ggml_metal_buffer_map [ferrite-book Ch 3] — this book has not re-read
llama.cpp’s source, so we mark it lineage rather than verification.
Keeping 16 GB resident: the residency set
One more substrate piece, and it exists only because of the scale — at a
few hundred megabytes nobody would bother. The mapped arena is
bound by every projection in every command buffer — per token. Rather
than let Metal redo residency bookkeeping for a 16+ GiB allocation each
time, Muser attaches it to an MTLResidencySet once at load:
#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/residency.rs:1
//! Minimal `MTLResidencySet` owner extracted from Ferrite's Metal substrate.
//!
//! The immutable GGUF arena is bound by every projection in every command
//! buffer. Attaching it once lets Metal skip repeating residency work for the
//! 16+ GiB allocation on every token. The Objective-C surface is public on
//! macOS 15+, and absence fails open to the ordinary Metal residency path.
}
create_and_attach (residency.rs:65-108) builds the set with raw
objc::msg_send! calls — the metal crate does not wrap this API — adds
the arena’s buffer, commits, requests residency, and attaches the set to
the queue. On any macOS release without the API it returns None and the
engine proceeds on Metal’s ordinary residency path. That distinction
matters more than it looks: this is an optimization that fails open.
Lose it and tokens still come out, correct, with Metal doing the residency
bookkeeping the long way — which is exactly the opposite of the untracked
gamble earlier in this chapter, where dropping the safety net changed
behaviour silently. Both are engine paths that may not be taken; only one
of them is permitted to be quiet about it.
3.7 What 96 GB buys, revisited from the buffer side
Ch 1 §1.5 budgeted the 96 GB by artifact — how much each thing takes. Now that you know what a buffer actually is, the same budget answers a sharper question: which of these bytes does the engine own, and which is it merely borrowing from the operating system? From the substrate’s point of view the same budget reads:
- Weights are not “used” memory in the ordinary sense — they are a
read-only file mapping, page-cache backed
[docs/memory-footprint.md §Other material allocations]. The OS can evict clean pages under pressure and re-fault them on next access. What the residency set adds is a request that the pages stay resident while serving. - Everything the engine allocates itself — activations, logits,
workspaces, the f16 KV planes, staging shadows — is a
StorageModeSharedbuffer from §3.3. The KV planes dominate: 1.827 GB per slot at full 131,072 context, ×4 slots = 7.306 GB[docs/memory-footprint.md]. - One arena serves all slots. The
MetalShareddesign ([crates/muser-engine/src/decode.rs:954-957]) exists because unified memory makes it legal: four serving sequences share one context, one pipeline set, one mapped arena. On a discrete-GPU machine the same sharing would be possible but the copy story would not — each GPU residency of the weights would be a distinct 16.76 GB VRAM occupation.
3.8 Tradeoffs
Tracked storage vs the untracked gamble. The story is in §3.2’s lineage
box; here is what it costs. Muser pays for Metal’s dependency tracker on
every encode, plus the explicit barrier groups, and gets back a safety net
that the one experiment we ran showed it needed: the engine-wide untracked
build (b9678d4) “empirically changed DFlash conditioning while leaving
final greedy IDs unchanged” [crates/muser-engine/src/metal/buffer.rs:7-14].
Now notice what that sentence does not say. It does not say the untracked
path was slower, or faster — no Muser document isolates the
tracked-vs-untracked encode cost on this machine [unverified]. The ruling
was made on correctness alone, and it would stand unchanged if untracked
turned out to measure quicker.
One arena + views vs one buffer per tensor. Views are cheap (24 bytes, one bounds check) and make the whole model one mapping — but they collapse identity: Metal sees a single buffer, so its tracker cannot distinguish a write to one tensor from a write to another. That is exactly why the weights are views (immutable after load; a write would be a bug worth a crash) while activations and KV planes get their own typed buffers with distinct identity. The split is the design: share what is immutable, individualize what is written.
uninitialized KV planes: speed with a proof obligation. Skipping the
CPU memset on multi-gigabyte planes saves real startup time on allocations
that can be many gigabytes (buffer.rs:248-254) — in exchange, the ring
metadata must guarantee write-before-read for every row in
[origin, origin + len). Muser makes the obligation visible three ways:
the doc comment’s “ONLY”, the debug-build 0xDEAD poison, and the test
that demonstrates it (buffer.rs:328-344). That is the shape of the whole
bargain, and it generalizes past this buffer: when you take a shortcut
whose safety lives in some other module’s invariant, spend part of the
winnings making that invariant loud.
3.9 What’s next
You now know the entire memory story: one DRAM pool, StorageModeShared
everywhere, three typed buffer types plus checked byte views, zero-copy
mmap of the 16.76 GB GGUF into one Metal buffer, a residency set to keep it
warm, and an initialization policy that decides byte-for-byte what may be
read before it is written. One object from Ch 2
is still a black box: MetalContext.library — “a bundle of compiled kernel
functions, addressable by name.” How .metal source text becomes that
bundle, why Muser deliberately keeps three different kernel sources
(fast-math source, strict-f32 source, and a pinned llama.cpp metallib),
and how the engine makes a silent kernel fallback impossible — that is
Ch 4.
References
[crates/muser-engine/src/metal/buffer.rs:7-14]—shared_tracked()and theb9678d4untracked-mode incident comment;:28-56the four substrate types;:59-73, 182-196, 238-244the zero-initializing allocators;:246-277GpuHalfBuffer::uninitializedand the0xDEADpoison;:91-130from_mmapwith the page-rounding contract;:328-376the poison and page-rounding tests;:16-26getpagesize.[crates/muser-engine/src/weights.rs:163-218]—MuseWeights::open(whole-file mmap + per-tensor index with bounds checks);:40-48TensorView;:350-373TensorLayout;:375-378mapped_file().[crates/muser-engine/src/decode.rs:1195-1206]—load_shared: from_mmap + residency set;:148-161Projection::view/nvfp4_scale_view;:229-244, 246-261the twoMetalKvPlaneconstructors;:1350-1352the live-plane zero-fill rationale;:1862-1888the detached remote-install generation;:954-984MetalShared.[crates/muser-engine/src/metal/residency.rs:1-107]— theMTLResidencySetowner (rawmsg_send!, fails open below macOS 15).[crates/muser-engine/src/metal/encode/multicol.rs:192]— the real offset-viewset_bufferbind.[docs/memory-footprint.md]— artifact sizes, KV formula, the on-disk-vs-resident caveat.[docs/kvpack-merge-handoff.md §3 D2]— the 2026-08-20 audit correcting memory-footprint.md’s memset claim for live planes.[ferrite-book Ch 3]— the ancestor’s unified-memory chapter: the arena-view and zero-copy devices this chapter re-grounds; its untracked-hazards A/B (−2 to −4 % on A18-class hardware) and GpuHeap / packed-activations history are Ferrite-lineage.[Metal-PG]— Apple, Metal Programming Guide: “Resource Objects: Storage Modes,” “Tracking Resource Dependencies.”[Metal-SS]— Apple, Metal Shading Language Specification: “Address Spaces.”- glossary — terms introduced this chapter: unified memory, SoC, DRAM, VRAM, PCIe, storage mode, blit, mmap, page fault, zero-copy, page alignment, TLB, MTLBuffer, GpuBuffer, GpuHalfBuffer, GpuBytes, GpuByteView, residency set.