Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/DETAILS.md
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ system prompt with a billing header that changes on every request; the server pi
| `--conversation-cache-min-free-mib N` | RAM floor, default 2560. Parking and session files are skipped when less than the snapshot plus the floor is available. |
| `--memory-limit-mib N` (or `STRATA_MEMORY_LIMIT_MIB`) | Total RAM this engine's container may use, for when a container cannot see its own limit. Config `args`: `"--memory-limit-mib", "102400"`. `0` = none. |

The RAM figure the floor is checked against is the smallest of `MemAvailable`, the room under each cgroup limit visible to
The RAM figure the floor is checked against is the smallest of `MemAvailable` (macOS: the Metal engine's one reading of free memory, also used for its streaming arena - free and purgeable pages and the clean file cache from `host_statistics64`, not other programs' inactive anonymous memory), the room under each cgroup limit visible to
the engine (v2 `memory.max` and `memory.high`, v1 `memory.limit_in_bytes`, less clean inactive file cache) and the
`--memory-limit-mib` cap. The startup line `strata generate: memory guard: limit X GiB (source: flag|cgroup|meminfo) ...` names
the source. It covers parking and session files only, not the startup sizing of the arena, the resident budget or the Windows commit check.
Expand Down Expand Up @@ -729,7 +729,8 @@ curl -X POST "http://127.0.0.1:8080/slots/0?action=restore" -H "Content-Type: ap
- Clients still send their messages and images; the file holds engine state and token identity, not a chat export. A follow-up
that continues the restored conversation reuses its state; a short tail may be read again. Only the deepest checkpoint is saved.
- A file is bound to the model files (sampled by size and first and last MiB, not by path) and to the settings that change what
the cells mean (engine version, CUDA or HIP, `--kv`, `STRATA_KV_ROT`, `--kv-resident`, `--max-context`, `--mtp-window`, the
the cells mean (engine version, CUDA, HIP or Metal - on Metal also the decode window's layout, so two builds of one version with
different layouts are told apart -, `--kv`, `STRATA_KV_ROT`, `--kv-resident`, `--max-context`, `--mtp-window`, the
rope configuration, a loaded control vector and the arithmetic switches). Sampling, seeds, draft tuning and the expert
tier are not in it. Do not change model files while their sessions are kept. The hash detects accidental corruption and does
not authenticate a file: restore only files this engine wrote.
Expand All @@ -739,7 +740,7 @@ curl -X POST "http://127.0.0.1:8080/slots/0?action=restore" -H "Content-Type: ap
file had replaced the old one. Requests must be `application/json` (else `415`) and come from no browser page, Strata's own
page or a trusted origin.
- Not supported with `--layer-split`, `--peer-device`, `--batch` or `--prompt-cache 0`. The engine has been run on Linux and CUDA
only for this feature. Exact token replay across restarts is not guaranteed: expert residency and CPU/GPU rounding can
and on macOS/Metal ([MAC.md](MAC.md)) for this feature. Exact token replay across restarts is not guaranteed: expert residency and CPU/GPU rounding can
change later output.

### The web page (Chat, Images, Monitor, About)
Expand Down
2 changes: 2 additions & 0 deletions docs/MAC.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Intel engines are unchanged.
| Gyro-S (agentionai, one 58.5 GB file) | 29.7 GB (the other 28.8 GB is the n-gram table) | 38 GB | 48 GB; streams below that |
| Gyro-M (agentionai, one 92.0 GB file) | 63.2 GB (+ the 28.8 GB table) | 71 GB | 96 GB; streams below that |

- **The engine's own memory grows with the context.** Beside the mapped file, the engine holds its buffers and the decode window's K/V for the whole `--max-context`. Measured on the M5 Max with Qwen3.8-Flash-Next IQ3_S (phys_footprint 5 s after the engine is ready): 7.68 GiB at 64K context and 10.61 GiB at 262K. Before this version the engine also kept an unused second copy of the K/V in the session buffer, resident in full: 9.27 GiB at 64K and 16.99 GiB at 262K.
- **The n-gram table.** Each pack also has a 28.8 GB n-gram table (the model's second file). It is read from the SSD a few rows at
a time and does not count against the memory above.
- **Disk.** The original Q2_0 and IQ3 packs download roughly 66 to 84 GB, plus preparation and draft data. Unsloth packs are much
Expand Down Expand Up @@ -223,6 +224,7 @@ from each other: top-1 agreement 95.4% against 95.8%, mean KL 0.0207 against 0.0
- **Context:** measured up to 32K (needles, KL). Setup recommends 32K below 96 GB and 64K from 96 GB. Up to the model's 262K can
be set, but it is not measured.
- **One request at a time.** `--parallel` (batch slots) is not ported. Several clients queue.
- **Conversation cache and session files** (`--conversation-cache-mib`, `/slots/0?action=save|restore`) work as on the PC; their snapshots carry the Mac decode window's own K/V. Measured on the M5 Max (128 GB) with Qwen3.8-Flash-Next IQ3_S at 262K context, a 28,785-token conversation: parked as 798 MB (with its checkpoints) in 49 ms and back in 41 ms; as a session file 679 MB, saved in 0.16 s and restored in a new engine in 0.10 s. Both times the follow-up decoded the same 95 tokens as the conversation that never left. Size: about 15.4 KB per token plus 118 MB per running state (the live one and each checkpoint kept with it). A session file's fingerprint names the Mac engine and its decode window's layout (the bytes per cell of each K/V part and the sizes of the running state), so one from the PC engine does not restore on the Mac (not tried), and a Mac build whose window layout differs refuses the file instead of misreading it (checked with a file from an earlier build of this change: refused, the engine kept serving).
- **Not ported:** the control vector (`--control-vector-scaled`) and the PC tuning (`--calibrate`).
- **Streaming** is measured at 32 GB only.
- **Families:** the original Qwen3.8-Flash-Next, Unsloth's UD-Q4/Q5/Q6_K_XL and agentionai's Gyro-S are measured. Setup repacks
Expand Down
28 changes: 28 additions & 0 deletions metal/src/engine/host_memory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// metal/src/engine/host_memory.cpp - see host_memory.h.
#include "host_memory.h"

#include "arena_budget.h"

#include <cstdint>
#include <mach/mach.h>
#include <sys/sysctl.h>

namespace strata::metal_engine {

bool host_memory_now(double& available, double& phys) {
available = 0;
phys = 0;
uint64_t total = 0;
size_t len = sizeof total;
if (sysctlbyname("hw.memsize", &total, &len, nullptr, 0) != 0 || total == 0) return false;
vm_statistics64_data_t vs{};
mach_msg_type_number_t cnt = HOST_VM_INFO64_COUNT;
if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t) &vs, &cnt) != KERN_SUCCESS) return false;
const double pg = (double) vm_page_size;
phys = (double) total;
available = arena_avail(vs.free_count * pg, vs.purgeable_count * pg, vs.inactive_count * pg, vs.speculative_count * pg,
vs.external_page_count * pg);
return true;
}

} // namespace strata::metal_engine
16 changes: 16 additions & 0 deletions metal/src/engine/host_memory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// metal/src/engine/host_memory.h - the memory macOS can give the engine now, read one way for every user: the
// streaming arena (window.cpp) and the memory guard of parking and session files
// (src/core/conversation_memory.cpp, sample_host_memory).
//
// The rule is arena_budget.h's arena_avail: free and purgeable pages and the clean file cache (inactive and speculative
// pages, at most as many as are file-backed). Other programs' inactive anonymous memory is left out: macOS frees it
// only by compressing or swapping it.
#pragma once

namespace strata::metal_engine {

/// `available`: bytes macOS can give now without paging anything out (the rule above); `phys`: hw.memsize. False
/// (both 0) when the VM statistics or the RAM size cannot be read.
bool host_memory_now(double& available, double& phys);

} // namespace strata::metal_engine
28 changes: 14 additions & 14 deletions metal/src/engine/prompt_chain.inc
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,13 @@ bool Window::pc_chunk(const int64_t* tokens, int nT, int64_t pos0, std::string&
gemm(b.mixh, b.Wbe, b.be, nT, 48, N);
PcGdnPArgs ga{(uint32_t) nT, 16, 48, 10240, eps_, 0};
D(P_(b.lgdn, "gdn_pre"),
{mtl::Arg::B(b.qkv), mtl::Arg::B(ghist_[hp][(size_t) gi]), mtl::Arg::B(W_, T(bl + "ssm_conv1d.weight").offset),
{mtl::Arg::B(b.qkv), mtl::Arg::B(slot_.ghist[hp][(size_t) gi]), mtl::Arg::B(W_, T(bl + "ssm_conv1d.weight").offset),
mtl::Arg::B(b.al), mtl::Arg::B(b.be), mtl::Arg::B(W_, T(bl + "ssm_dt.bias").offset),
mtl::Arg::B(W_, T(bl + "ssm_a").offset), mtl::Arg::B(b.gqn), mtl::Arg::B(b.gkn), mtl::Arg::B(b.gvc),
mtl::Arg::B(b.ggb), mtl::Arg::B(b.gkq), mtl::Arg::B(ghist_[1 - hp][(size_t) gi]), mtl::Arg::V(ga)},
mtl::Arg::B(b.ggb), mtl::Arg::B(b.gkq), mtl::Arg::B(slot_.ghist[1 - hp][(size_t) gi]), mtl::Arg::V(ga)},
16 + 24, nT, 256);
D(P_(b.lgdn, "gdn_scanr_4", mtl::Constants().i(6, 0)),
{mtl::Arg::B(gst_[(size_t) gi]), mtl::Arg::B(b.gqn), mtl::Arg::B(b.gkn), mtl::Arg::B(b.gvc), mtl::Arg::B(b.ggb),
{mtl::Arg::B(slot_.gst[(size_t) gi]), mtl::Arg::B(b.gqn), mtl::Arg::B(b.gkn), mtl::Arg::B(b.gvc), mtl::Arg::B(b.ggb),
mtl::Arg::B(b.gkq), mtl::Arg::B(b.go), mtl::Arg::V(ga)},
48 * 4, 1, 32 * 4);
D(P_(b.lgdn, "gdn_post"), {mtl::Arg::B(b.go), mtl::Arg::B(b.z), mtl::Arg::B(W_, T(bl + "ssm_norm.weight").offset),
Expand All @@ -233,17 +233,17 @@ bool Window::pc_chunk(const int64_t* tokens, int nT, int64_t pos0, std::string&
{mtl::Arg::B(b.qfull), mtl::Arg::B(b.kcur), mtl::Arg::B(b.vcur), mtl::Arg::B(b.iqr), mtl::Arg::B(b.ikr),
mtl::Arg::B(W_, T(bl + "attn_q_norm.weight").offset), mtl::Arg::B(W_, T(bl + "attn_k_norm.weight").offset),
mtl::Arg::B(W_, T(bl + "indexer.q_norm.weight").offset), mtl::Arg::B(W_, T(bl + "indexer.k_norm.weight").offset),
mtl::Arg::B(cs_), mtl::Arg::B(b.qcur), mtl::Arg::B(b.qidx), mtl::Arg::B(kq_[(size_t) qi]), mtl::Arg::B(vq_[(size_t) qi]),
mtl::Arg::B(ksc_[(size_t) qi]), mtl::Arg::B(vsc_[(size_t) qi]), mtl::Arg::B(tail_[(size_t) qi]),
mtl::Arg::B(dead_[(size_t) qi]), mtl::Arg::B(pooled_[(size_t) qi]), mtl::Arg::V(a), mtl::Arg::B(mrope_)},
mtl::Arg::B(cs_), mtl::Arg::B(b.qcur), mtl::Arg::B(b.qidx), mtl::Arg::B(slot_.kq[(size_t) qi]), mtl::Arg::B(slot_.vq[(size_t) qi]),
mtl::Arg::B(slot_.ksc[(size_t) qi]), mtl::Arg::B(slot_.vsc[(size_t) qi]), mtl::Arg::B(slot_.tail[(size_t) qi]),
mtl::Arg::B(slot_.dead[(size_t) qi]), mtl::Arg::B(slot_.pooled[(size_t) qi]), mtl::Arg::V(a), mtl::Arg::B(mrope_)},
33, nT, 256);
if (pos0 + nT > PC_QB) { // some row's context is past the budget: the indexer selects
if (b.k4)
D(P_(b.lqsa, "qsa_score_mm"), {mtl::Arg::B(b.qidx), mtl::Arg::B(pooled_[(size_t) qi]), mtl::Arg::B(dead_[(size_t) qi]),
D(P_(b.lqsa, "qsa_score_mm"), {mtl::Arg::B(b.qidx), mtl::Arg::B(slot_.pooled[(size_t) qi]), mtl::Arg::B(slot_.dead[(size_t) qi]),
mtl::Arg::B(b.scores), mtl::Arg::V(a)},
(uint32_t) (((pos0 + nT) / 4 + 1 + 63) / 64), (nT + 15) / 16, 128);
else
D(P_(b.lqsa, "qsa_score"), {mtl::Arg::B(b.qidx), mtl::Arg::B(pooled_[(size_t) qi]), mtl::Arg::B(dead_[(size_t) qi]),
D(P_(b.lqsa, "qsa_score"), {mtl::Arg::B(b.qidx), mtl::Arg::B(slot_.pooled[(size_t) qi]), mtl::Arg::B(slot_.dead[(size_t) qi]),
mtl::Arg::B(b.scores), mtl::Arg::V(a)},
(uint32_t) (((pos0 + nT) / 4 + 1 + 7) / 8), nT, 256);
}
Expand All @@ -252,12 +252,12 @@ bool Window::pc_chunk(const int64_t* tokens, int nT, int64_t pos0, std::string&
const uint32_t nq = (uint32_t) nT * 24 * 256;
D(P_(b.lqsa, "qsa_q_f16"), {mtl::Arg::B(b.qcur), mtl::Arg::B(b.qcurh), mtl::Arg::V(nq)}, (nq + 255) / 256, 1, 256);
D(P_(b.lqsa, "qsa_attn_mm", mtl::Constants().b(7, true), "qsa_attn_mm#k4"),
{mtl::Arg::B(b.qcurh), mtl::Arg::B(kq_[(size_t) qi]), mtl::Arg::B(vq_[(size_t) qi]), mtl::Arg::B(ksc_[(size_t) qi]),
mtl::Arg::B(vsc_[(size_t) qi]), mtl::Arg::B(b.qids), mtl::Arg::B(b.part), mtl::Arg::V(a)},
{mtl::Arg::B(b.qcurh), mtl::Arg::B(slot_.kq[(size_t) qi]), mtl::Arg::B(slot_.vq[(size_t) qi]), mtl::Arg::B(slot_.ksc[(size_t) qi]),
mtl::Arg::B(slot_.vsc[(size_t) qi]), mtl::Arg::B(b.qids), mtl::Arg::B(b.part), mtl::Arg::V(a)},
PC_QCHUNKS, 2, 32, nT);
} else {
D(P_(b.lqsa, "qsa_attn"), {mtl::Arg::B(b.qcur), mtl::Arg::B(kq_[(size_t) qi]), mtl::Arg::B(vq_[(size_t) qi]),
mtl::Arg::B(ksc_[(size_t) qi]), mtl::Arg::B(vsc_[(size_t) qi]), mtl::Arg::B(b.qids),
D(P_(b.lqsa, "qsa_attn"), {mtl::Arg::B(b.qcur), mtl::Arg::B(slot_.kq[(size_t) qi]), mtl::Arg::B(slot_.vq[(size_t) qi]),
mtl::Arg::B(slot_.ksc[(size_t) qi]), mtl::Arg::B(slot_.vsc[(size_t) qi]), mtl::Arg::B(b.qids),
mtl::Arg::B(b.part), mtl::Arg::V(a)},
PC_QCHUNKS, 2, 256, nT);
}
Expand Down Expand Up @@ -361,7 +361,7 @@ bool Window::pc_chunk(const int64_t* tokens, int nT, int64_t pos0, std::string&
{mtl::Arg::B(b.keyn), mtl::Arg::B(b.qn), mtl::Arg::B(b.pval), mtl::Arg::B(W_, off("blk.1.ple_norm_conv.weight")),
mtl::Arg::B(b.gated), mtl::Arg::B(b.normW), mtl::Arg::V(pa2)}, HC, nT, 256);
D(P(lple_, "ple_conv_add", {}, "ple_conv_add"),
{mtl::Arg::B(pleHist_), mtl::Arg::B(b.normW), kw_arg(), mtl::Arg::B(b.gated),
{mtl::Arg::B(slot_.pleHist), mtl::Arg::B(b.normW), kw_arg(), mtl::Arg::B(b.gated),
mtl::Arg::B(b.R), mtl::Arg::V(pa2)}, HC * N / 256, nT, 256);
}
}
Expand All @@ -378,7 +378,7 @@ bool Window::pc_chunk(const int64_t* tokens, int nT, int64_t pos0, std::string&
if (ple_on_) {
using namespace strata::kernels;
const float* nw = b.normW.as<float>();
float* hist = pleHist_.as<float>();
float* hist = slot_.pleHist.as<float>();
for (int c = 0; c < NG_HC_DIM; ++c) {
float* col = hist + (size_t) c * NG_HIST;
float tmp[NG_HIST * 2];
Expand Down
Loading
Loading