# track 3: streaming publication, two-layer prompt lookahead, warm scratch

## deliverables / status

- candidate only: `/tmp/sol_async_stream/sol_async_stream.patch` (three engine files; not applied to the shared checkout).
- source and cpu-only regression harness: `/tmp/sol_async_stream/vendor/strata/metal/src/engine/`, `/tmp/sol_async_stream/test_stream.cpp`.
- exact compiler invocations and outputs: `/tmp/sol_async_stream/verification.json`.
- no gpu execution, model reads, ssd benchmarks, or port 8080 access occurred.
- this is **not** an implementation or validation of live cpu-to-gpu lock-free publication. that part of the requested design is rejected pending a real metal synchronization contract. no performance or quality win is claimed.

## ownership / invariant

`ExpertStream` owns expert lifetime, pending io, slot residency, and prompt scratch backing. `Window` owns router prediction dispatches and the metal views over scratch. the existing shared-event handshake is the publication boundary. selected expert bytes, expert arithmetic, and resident/missing plan membership must remain valid until that layer finishes. no new kernel interface or weight format is needed for this safe candidate.

## why immediate io_main smap writes are not just nondeterminism

`io_main` finishes each `pread` outside `mu_`, then decrements `Slot::left` under `mu_`. only the last completed part sets `state = 0`; that existing seq-cst atomic store already gives the host an acquire/release completion boundary for every piece. urgent jobs publish smap, speculative jobs intentionally do not.

changing only that speculative condition to `atomic_ref<uint32_t>(smap[key]).store(..., release)` does **not** establish a synchronization relationship with the existing metal `device const uint*` reads. a c++ release store is not a gpu acquire, and replacing metal reads with relaxed atomics alone would not prove host/gpu data visibility either. the issue is full expert payload visibility, not merely a tear-free 32-bit slot number. this needs an apple-supported shared-resource synchronization scheme (or existing shared-event boundary), not an assumption about apple silicon cache coherence.

related lifetime traps:

1. `victim()` invalidates smap and immediately makes backing bytes reusable. the current layer must never be a victim while its resident pass executes. during decode `after_layer(l)`, the gpu can also reach layer `l+1`'s router. existing `ahead(l+2)` only avoided its *target* layer, not the router currently in flight. the candidate protects current layer throughout a turn and additionally next layer during after-turn work.
2. `cancel_slot()` is safe only for state-1 queued reads: no piece has begun, no completion may publish it, and its map entry must still be absent. the existing queue callers check this. it is not a completed-slot eviction primitive.
3. `retire_unused_pre()` parks queued, reading, or completed unchosen speculative slots in limbo. the old `release_limbo()` cleared `where_` but never smap, because those slots were never published. once speculative residency is published, that omission becomes a stale address into recycled expert bytes. the candidate clears smap before recycling.
4. `route_topk` in `prompt.metal` uses smap both for resident probability scaling and the cold-expert pruning mode. `moe_route` and fused `route_body` in `router.metal` use it for moce; `plan_body` in `moe.metal` snapshots each expert's resident/missing classification once into the plan. gu/down then resolve the slot again from smap. the plan is fixed, so adding residency need not inherently duplicate work, but removing/recycling an expert the plan already considered resident corrupts computation.
5. `moe_mm.metal` also resolves each tile's expert address from smap. its existing prompt handshake guarantees the required experts exist before those dispatches. no atomic kernel rewrites were made because the candidate keeps that contract.

## candidate behavior

### handshake publication

`STRATA_METAL_PUBLISH_PREFETCH=1` (candidate default) publishes completed, non-limbo slots for layers `l+1..l+2` at `at_layer(l)`, before the gpu is released. it uses the existing atomic `state` completion marker, under the slot metadata mutex. the current layer is excluded because decode's resident expert pass can overlap this host callback. no io thread writes speculative smap entries while gpu routers are live.

already-published entries stay published; victim and limbo recycling explicitly invalidate them. the old non-split moce path's determinism-only wait/evict block is retained when this flag is zero and skipped when enabled. setting the flag to zero restores the old speculative publication policy (the strengthened lifetime guards remain).

this is bounded handshake publication, **not** lock-free io publication. publication scans at most `2 * ne_` map entries per layer, independent of conversation length. benefit requires reads to have completed by an earlier handshake; it cannot expose a completion that arrives after that boundary to the immediately following router.

### two-layer prompt lookahead

`STRATA_METAL_PROMPT_PRED2=1` adds the `l+2` router pass on layer `l`'s existing FFN input. the existing router-weight/logit/weight scratch is reused sequentially; only another `MT * TOPK` int32 id buffer is allocated. measured and actual buffer layout account for it identically. no model input is altered and prediction weights are never used as actual expert mixture weights.

ids are consumed at the current handshake, ordered by token frequency, then queued behind urgent work and one-layer predictions. `STRATA_METAL_PROMPT_PRED2_MAX` caps newly submitted two-layer predictions (default 64; clamped 0..512). all slots remain bounded by the arena. current-layer and next-layer residency are protected during selection; no speculative submission waits for space. absent capacity, it stops. layers 46/47 skip out-of-range lookahead; prediction-off leaves existing profile fallback.

existing decode two-layer prediction already exists through `STRATA_METAL_PREFETCH2`; no redundant decode router pass was added. its eviction protection is strengthened as above.

`STRATA_METAL_PROMPT_START_LAYERS=4` optionally queues profile-ranked startup experts for layers 0..3. default remains one layer. layer 0 keeps its existing cap; later startup layers each queue at most 32 nonresident experts and cannot evict layer 0. this is deliberately bounded, not a full-model upfront prediction sweep. the extra router pass and wrong predictions may cost more than overlap saves; measurement is still required.

### warm prompt scratch

`STRATA_METAL_PROMPT_BUF_REUSE=1` retains the single existing separate scratch mapping **and its metal buffer views** across turns, skipping the per-turn `free`/`delete`/`munmap`/`purge_device_allocations` path. memory is bounded by one configured prompt chunk allocation; this retains physical memory and may reduce expert-cache headroom on a 24 gb machine. default stays off. the arena-backed path is unchanged. prompt teardown is shared in one helper and runs at window destruction before the separate mapping is released.

this avoids the suspected lifecycle overhead structurally, but the task's roughly 300 ms claim was not remeasured and must not be reported as a measured win. `MADV_FREE` was not shipped on live no-copy metal buffers: physical reclaimability while metal retains/wires those pages and gpu write/reuse semantics need verification. neither `madvise` nor warm retention is a magic smaller working set. a truly smaller 512-row resident scratch design needs row-capacity selection at initialization; retaining a 1024-row allocation because the *current* T is 512 does not shrink it.

## verification actually run

- requested flags alone: `stream.cpp` syntax passed; `window.cpp` failed on the pre-existing missing `cuda_runtime.h` include path.
- with the actual CMake metal compatibility include directories and the two required CMake string defines: both modified translation units passed `clang++ -fsyntax-only -std=gnu++20 -arch arm64 -DSTRATA_GPU_METAL=1` (all other include flags retained).
- cpu-only regression executable linked against the actual modified `stream.cpp`, with unused gpu-dependent functions dead stripped. it tests incomplete and limbo slots excluded from publication; completed publication; executing-layer and next-router victim exclusion; map invalidation before recycling; queued cancellation; separate-scratch retain/release; real `at_layer` submission of both prediction distances; final-layer bounds and queued cancellation.
- the final executable passed with address sanitizer + undefined behavior sanitizer, exit 0, no diagnostics.
- `git diff --check` passed in the isolated temporary git repository.
- final diff reviewed visually; repeated router dispatch code was consolidated into one distance loop and prompt cleanup into one helper.

## merge gate

this is a candidate, not a production-ready speed claim. integration needs gpu correctness/parity on cold/warm, prompt/decode transitions, partial chunks, layer 47 rollover, split-turn on/off, and active moce modes; quality checks matter because changed residency changes existing moce routing. also measure memory pressure and warm-turn latency with retention enabled. none were run under this track's explicit no-gpu/no-benchmark rule. do not enable a live async smap mode merely because nondeterminism is allowed: arithmetic races and stale expert addresses are not nondeterminism.
